Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions doc/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,13 @@ description too: the `*.toml`/`*.txt` config files the resolution read, the
root's `foundry.lock`, `soldeer.lock`, `.gitmodules` and `package.json`, and the
`package.json`, `foundry.toml` and `remappings.txt` of every package the bundle
holds files of — `json` for a `package.json`, `resource` otherwise, so `stasis
extract` restores them. Credentials stay behind: a `.toml` config loses its
`[rpc_endpoints]` and `[etherscan]` tables (at the top level or in a profile)
and keys such as `eth_rpc_url` or `etherscan_api_key` (any key named like a
key, token, secret or password), every carried file loses the user info of its
URLs (`https://user:token@host` is `https://host`), and `hardhat.config.*`,
being code that may hold keys, is never carried.
extract` restores them. They are carried as written, as `--package-json` carries
`package.json`: stasis doesn't edit them, so whatever they hold — an
`eth_rpc_url` or `[rpc_endpoints]` URL with its API key, an `[etherscan]` key,
the credentials in a `.gitmodules` URL — is in the bundle too. Keep secrets in
the environment (`${VAR}` in `foundry.toml`) rather than in these files, or
don't pass `--manifests`. `hardhat.config.*`, being code, and `.env` files are
never carried.

Rust entries are crate roots (`src/main.rs`, `src/lib.rs`, `src/bin/*.rs`,
`tests/*.rs`, …): their `mod` declarations resolve as siblings, as rustc does,
Expand Down
2 changes: 1 addition & 1 deletion stasis/bin/stasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function usage(prefix = '') {
remappings that one file lists; an import must reach a .sol file inside the project, and a
dependency's only other dependencies' files; --manifests also carries foundry.toml,
remappings.txt, foundry.lock, soldeer.lock, .gitmodules and package.json of the project and its
bundled dependencies, without their RPC/Etherscan keys, secret-named keys and URL credentials)
bundled dependencies, as written: RPC/Etherscan keys and URL credentials in them included)
stasis bundle [--add] [--output=(path|-)] path/to/file.php ...
stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
stasis bundle --metro [--metro-resolver] --platforms=ios,android [--platforms=web] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts) ...
Expand Down
8 changes: 3 additions & 5 deletions stasis/src/cmd/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
discoverSolidityConfig,
expandSolidityEntries,
} from '../loaders/solidity.js'
import { redactFoundryToml, scrubUrlCredentials } from '../loaders/foundry.js'
import { buildBashTree, collectBashFilesFromDisk } from '../loaders/bash.js'
import { buildRustTree, collectRustFilesFromDisk } from '../loaders/rust.js'
import { VENDOR_DIR as CARGO_VENDOR_DIR, createCargoContext } from '../loaders/cargo.js'
Expand Down Expand Up @@ -240,8 +239,8 @@ function assembleCodeBundle({
// The build-description files of a Solidity bundle (--manifests), as Map<path, text>: `configFiles`
// (what discoverSolidityConfig read, when named `*.toml`/`*.txt`) plus the SOLIDITY_*_MANIFESTS
// that exist, for the root and for each package dir `classifyDep`/package.json places a bundled
// source in. Files inside the root only. Credentials stay behind: a `.toml` config loses its RPC
// endpoint and Etherscan tables and secret-named keys, and every file its URLs' user info.
// source in. Files inside the root only, carried as written: whatever they hold (an RPC URL with
// its API key, an Etherscan key, a URL's credentials) is in the bundle too, as with --package-json.
function solidityManifests(baseDir, sources, configFiles, classifyDep) {
const wanted = new Set([...configFiles.filter((f) => f.endsWith('.toml') || f.endsWith('.txt')), ...SOLIDITY_ROOT_MANIFESTS])
const dirs = new Set()
Expand All @@ -267,8 +266,7 @@ function solidityManifests(baseDir, sources, configFiles, classifyDep) {
throw err
}
if (!isUtf8(buf)) throw new Error(`Solidity manifest is not valid UTF-8: ${rel}`)
const text = buf.toString('utf8')
out.set(rel, rel.endsWith('.toml') ? redactFoundryToml(text, rel) : scrubUrlCredentials(text))
out.set(rel, buf.toString('utf8'))
}
return out
}
Expand Down
32 changes: 0 additions & 32 deletions stasis/src/loaders/foundry.js
Original file line number Diff line number Diff line change
Expand Up @@ -817,35 +817,3 @@ export function foundryProject(baseDir, { env = process.env } = {}) {
const envUsed = [...(env.FOUNDRY_PROFILE ? [`FOUNDRY_PROFILE=${env.FOUNDRY_PROFILE}`] : []), ...(envName === null ? [] : [envName])]
return { remappings, libs: config.libs, files: relFiles, envUsed }
}

// --- Carrying configs (`--manifests`) -------------------------------------------------------

// What in a foundry.toml holds credentials rather than build settings: the RPC endpoint and
// Etherscan tables (provider URLs embed API keys), wherever they sit, and keys named like one.
const SECRET_TABLES = new Set(['rpc_endpoints', 'etherscan'])
const SECRET_KEY_RE = /^(?:eth_rpc_url|eth_rpc_jwt|eth_rpc_headers|fork_url)$|(?:^|_)(?:api_key|key|secret|token|password|passphrase|mnemonic|private_key|jwt)$/u

// `scheme://user:password@host` or `scheme://token@host` -> `scheme://host`, anywhere in a text.
export const scrubUrlCredentials = (text) => text.replaceAll(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@"'<>]+@/giu, '$1')

// A foundry.toml without its credentials: a SECRET_TABLES table goes whole (every line up to the
// next header), a pair under one or keyed like a secret goes line for line, and URLs lose their
// user info. Everything else is kept verbatim. Throws a TomlError naming `file` on text that
// isn't TOML: what can't be read can't be redacted.
export function redactFoundryToml(text, file = null) {
const lines = text.split('\n')
const entries = tomlEntries(text, { file })
const drop = new Set()
const isSecretTable = (segs) => segs.some((seg) => SECRET_TABLES.has(seg))
entries.forEach((e, idx) => {
const segs = e.path.map(snakeCase)
if (e.header) {
if (!isSecretTable(segs)) return
const next = entries.slice(idx + 1).find((x) => x.header)
for (let i = e.first; i < (next ? next.first : lines.length); i++) drop.add(i)
} else if (isSecretTable(segs) || SECRET_KEY_RE.test(segs.at(-1))) {
for (let i = e.first; i <= e.last; i++) drop.add(i)
}
})
return scrubUrlCredentials(lines.filter((_, i) => !drop.has(i)).join('\n'))
}
16 changes: 9 additions & 7 deletions tests/bundle-cmd.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,8 @@ test('buildSolidityBundle refuses a remapping target outside the project root',
))
}))

test('buildSolidityBundle with manifests leaves credentials and a dependency\'s outside `extends` behind', withTmp(async (t, tmp) => {
writeProject(tmp, {
test('buildSolidityBundle with manifests carries configs as written, never `.env`, hardhat.config.* or a dependency\'s outside `extends`', withTmp(async (t, tmp) => {
const files = {
'foundry.toml': [
'[profile.default]',
'src = "src"',
Expand All @@ -514,14 +514,16 @@ test('buildSolidityBundle with manifests leaves credentials and a dependency\'s
'src/A.sol': 'import "dep/D.sol";\n',
'lib/dep/src/D.sol': 'contract D {}\n',
'lib/dep/foundry.toml': '[profile.default]\nextends = "../../.env"\n',
})
}
writeProject(tmp, files)
const { result: bundle, lines } = await captureStderr(() => buildSolidityBundle({ cwd: tmp, entries: ['src'], manifests: true, env: {} }))
// The submodule's own foundry.toml is carried; the `.env` it names is neither read as config nor carried.
// The configs are carried byte for byte, whatever they hold; stasis doesn't edit them.
const carried = [...bundle.sources].filter(([p]) => !p.endsWith('.sol'))
t.assert.deepEqual(carried.map(([p]) => p).toSorted(), ['.gitmodules', 'foundry.toml', 'lib/dep/foundry.toml'])
for (const [, text] of carried) t.assert.doesNotMatch(text, /KEY\d/u)
t.assert.equal(bundle.sources.get('foundry.toml'), '[profile.default]\nsrc = "src"\n\n[fmt]\nline_length = 100\n')
t.assert.match(bundle.sources.get('.gitmodules'), /url = https:\/\/github\.com\/o\/dep\.git/u)
for (const [p, text] of carried) t.assert.equal(text, files[p], p)
// `.env` and hardhat.config.* are never carried, and the submodule's `extends` reaching the
// project's `.env` is neither read as config nor carried.
for (const [, text] of bundle.sources) t.assert.doesNotMatch(text, /KEY[67]/u)
t.assert.ok(lines.some((l) => l.includes("Skipping a dependency's config") && l.includes('outside the dependency')))
}))

Expand Down
27 changes: 2 additions & 25 deletions tests/solidity-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
readRemappingsFile,
resolveSolImport,
} from '../stasis/src/loaders/solidity.js'
import { findRemappingsWithContext, foundryProject, foundryTomlRemappings, redactFoundryToml, scrubUrlCredentials } from '../stasis/src/loaders/foundry.js'
import { findRemappingsWithContext, foundryProject, foundryTomlRemappings } from '../stasis/src/loaders/foundry.js'

const fixtures = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'solidity-bundle')

Expand Down Expand Up @@ -607,30 +607,7 @@ test('foundry.toml profiles: legacy [<name>] tables, case-insensitive names, quo
t.assert.deepEqual(foundryTomlRemappings('remappings = ["@top/=lib/top/"]\n').map((r) => r.name), ['@top/'])
})

test('redactFoundryToml drops RPC/Etherscan tables and secret-named keys, keeping the rest verbatim', (t) => {
const toml = [
'# build',
'[profile.default]',
'src = "src"',
'eth_rpc_url = "https://eth.example/v2/K1"',
'remappings = [',
' "a/=b/", # comment',
']',
'etherscan = { mainnet = { key = "K2" } }',
'[profile.default.rpc_endpoints]',
'sepolia = "https://x/K3"',
'[rpc_endpoints]',
'"weird name" = "https://x/K4"',
'[etherscan]',
'mainnet = { key = "K5" }',
'[fmt]',
'repo = "https://user:K6@host/r"',
'',
].join('\n')
t.assert.equal(redactFoundryToml(toml), '# build\n[profile.default]\nsrc = "src"\nremappings = [\n "a/=b/", # comment\n]\n[fmt]\nrepo = "https://host/r"\n')
t.assert.equal(scrubUrlCredentials('https://t@github.com/o/r git@github.com:o/r https://h/p@v1'), 'https://github.com/o/r git@github.com:o/r https://h/p@v1')
// what can't be read as TOML can't be redacted: the file and line are named
t.assert.throws(() => redactFoundryToml('[rpc_endpoints]\nmainnet = "https://k@h" junk\n', 'foundry.toml'), { name: 'TomlError', message: 'foundry.toml:2: unexpected text after the value' })
test('foundryTomlRemappings names the line of a foundry.toml that isn\'t TOML', (t) => {
t.assert.throws(() => foundryTomlRemappings('[profile.default]\nremappings = ["a/=b/"\n'), { name: 'TomlError', message: 'line 2: unterminated array' })
})

Expand Down
Loading