@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +23 -3
  2. package/bin/stasis.js +375 -170
  3. package/package.json +44 -11
  4. package/src/audit-corrections.js +68 -0
  5. package/src/audit.js +243 -59
  6. package/src/babel-worker.cjs +65 -0
  7. package/src/babel.js +131 -0
  8. package/src/bundle-from-lockfile.js +12 -18
  9. package/src/bundle-reader.js +77 -0
  10. package/src/cmd/build.js +95 -128
  11. package/src/cmd/bundle.js +877 -641
  12. package/src/cmd/diff.js +2 -19
  13. package/src/cmd/github-bundle.js +39 -0
  14. package/src/cmd/output.js +36 -0
  15. package/src/cmd/sbom.js +4 -18
  16. package/src/diff.js +67 -124
  17. package/src/json-stream.js +283 -0
  18. package/src/loader-mock.js +8 -15
  19. package/src/loaders/bash.js +49 -116
  20. package/src/loaders/cargo.js +1699 -0
  21. package/src/loaders/foundry.js +923 -0
  22. package/src/loaders/paths.js +33 -0
  23. package/src/loaders/php.js +339 -394
  24. package/src/loaders/rust.js +3254 -242
  25. package/src/loaders/solidity-ownership.js +531 -0
  26. package/src/loaders/solidity.js +412 -162
  27. package/src/loaders/toml.js +68 -0
  28. package/src/metro-resolver.js +150 -0
  29. package/src/mock.js +57 -165
  30. package/src/parse.js +4 -16
  31. package/src/resolve-fields.js +331 -0
  32. package/src/resolve-node.js +416 -0
  33. package/src/resolve-typescript.js +427 -0
  34. package/src/rollup.js +5 -0
  35. package/src/sbom.js +64 -92
  36. package/src/scan.js +234 -156
  37. package/src/vfs-bundle/entries.js +140 -0
  38. package/src/vfs-bundle/github.js +193 -0
  39. package/src/vfs-bundle/soldeer.js +125 -0
  40. package/src/vfs-bundle/tree.js +606 -0
  41. package/src/vfs-bundle.js +74 -0
  42. package/src/why.js +434 -0
  43. package/src/apis/npm/index.js +0 -36
  44. package/src/apis/npm/semver.cjs +0 -44
  45. package/src/cmd/extract.js +0 -193
@@ -1,193 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
3
- import { brotliDecompressSync } from 'node:zlib'
4
-
5
- import { Bundle } from '@exodus/stasis-core/bundle'
6
- import { Lockfile } from '@exodus/stasis-core/lockfile'
7
- import { sha512integrity } from '@exodus/stasis-core/state-util'
8
- import { moduleFileKey } from '@exodus/stasis-core/util'
9
-
10
- const FILE_LOCK = 'stasis.lock.json'
11
-
12
- // Re-derive a Lockfile from an already-parsed Bundle. A bundle records each file's
13
- // content (UTF-8 source for code / 'resource', base64 for 'resource:base64'); a
14
- // lockfile records each file's SRI digest -- always taken over the same UTF-8
15
- // bytes the bundle stored (since the lockfile hashes the lockfile-visible content
16
- // without decoding). Both
17
- // share the same per-package bucket shape (name/version/files) plus the
18
- // entries/config metadata, so the conversion is just "hash each file's bytes"
19
- // while carrying everything else across verbatim.
20
- //
21
- // Requires a v1 bundle: v0 records no package name/version (Bundle.parse infers
22
- // buckets with null metadata), and Lockfile.serialize must attest both for
23
- // every node_modules bucket.
24
- //
25
- // The digest is taken over the same UTF-8 bytes we write to disk, so it equals
26
- // what `stasis run --lock=add` would record for the extracted tree and what
27
- // `stasis prune` recomputes when validating it.
28
- // Reject a 'resource:base64' content string that isn't canonical base64. Buffer.from
29
- // silently drops invalid characters and accepts non-canonical padding, so a tampered
30
- // bundle could ship `XXXX_invalid_chars==` and have the decoded bytes hash to whatever
31
- // it wanted -- the result would still self-consistently round-trip through `extract`
32
- // (decode → write → re-hash → match), but the on-disk file would diverge from what
33
- // any honest producer would have written. Fail closed at the schema-boundary by
34
- // requiring decode → re-encode to be a fixed point.
35
- function assertCanonicalBase64(content, file) {
36
- const buf = Buffer.from(content, 'base64')
37
- if (buf.toString('base64') !== content) {
38
- throw new Error(`extract: non-canonical base64 in resource:base64 content for ${file}`)
39
- }
40
- return buf
41
- }
42
-
43
- export function lockfileFromBundle(bundle) {
44
- const modules = new Map()
45
- for (const [dir, { name, version, ecosystem, files }] of bundle.modules) {
46
- const hashed = Object.create(null)
47
- for (const [rel, content] of Object.entries(files)) {
48
- // The lockfile records sha512 of each file's raw on-disk bytes -- the same
49
- // hash `stasis run` produces. The bundle stores 'resource:base64' files as
50
- // base64 text, so we decode back to the raw bytes before hashing; code and
51
- // 'resource' files are stored as raw UTF-8 and hash identically either way
52
- // (Node hashes a string as its UTF-8 bytes). assertCanonicalBase64 closes
53
- // the lying-tag hole: a non-canonical base64 string is rejected outright.
54
- const file = dir === '.' ? rel : `${dir}/${rel}`
55
- const bytes = bundle.formats.get(file) === 'resource:base64'
56
- ? assertCanonicalBase64(content, file)
57
- : content
58
- hashed[rel] = sha512integrity(bytes)
59
- }
60
- // Carry the dependency `ecosystem` across so the derived lockfile records the
61
- // same ecosystem tag the bundle did.
62
- modules.set(dir, ecosystem === undefined
63
- ? { name, version, files: hashed }
64
- : { name, version, ecosystem, files: hashed })
65
- }
66
- // Carry the bundle's resolution + format maps across so the derived lockfile
67
- // attests them too (Bundle.parse already validated the paths) -- without them
68
- // the lockfile would be a legacy bytes-only one, and a later frozen run
69
- // against the same bundle would skip the resolution/format cross-checks.
70
- return new Lockfile({
71
- config: bundle.config,
72
- entries: bundle.entries,
73
- modules,
74
- imports: bundle.imports,
75
- formats: bundle.formats,
76
- })
77
- }
78
-
79
- // Extract a brotli-compressed stasis code bundle (`stasis.code.br`) back onto
80
- // disk: write every bundled source to `output` (default: cwd), preserving its
81
- // project-relative path, and drop a matching `stasis.lock.json` alongside them.
82
- // The lockfile is derived from the bundle's own contents, so the extracted tree
83
- // validates against it out of the box (e.g. `stasis prune`). For legacy v0
84
- // bundles only the sources are extracted -- see the in-body comment.
85
- //
86
- // Returns `{ dir, files }` where `files` is the number of sources written
87
- // (the lockfile is not counted).
88
- export function extractCommand({ cwd = process.cwd(), bundleFile, output } = {}) {
89
- if (!bundleFile) throw new Error('extract: a bundle file is required')
90
- const bundleAbs = resolve(cwd, bundleFile)
91
- if (!existsSync(bundleAbs)) throw new Error(`extract: bundle file not found: ${bundleAbs}`)
92
-
93
- let bundle
94
- try {
95
- bundle = Bundle.parse(brotliDecompressSync(readFileSync(bundleAbs)).toString('utf-8'))
96
- } catch (cause) {
97
- // Bundle.parse rejects malformed shapes (e.g. a lockfile JSON fed by mistake,
98
- // or a brotli blob that isn't a stasis bundle) with bare message-less asserts;
99
- // wrap so the user sees which file failed instead of an empty error.
100
- throw new Error(`extract: not a valid stasis bundle: ${bundleAbs}`, { cause })
101
- }
102
-
103
- // v0 legacy bundles record no package name/version (Bundle.parse infers buckets
104
- // with null metadata), so no lockfile can be restored from them:
105
- // Lockfile.serialize requires both for every node_modules bucket, and a
106
- // null-metadata workspace entry would be rejected later by State.addFile's
107
- // name/version cross-check. The sources themselves are still fully
108
- // recoverable -- extract them and skip the lockfile, with a warning.
109
- const withLockfile = bundle.version === Bundle.VERSION
110
-
111
- const outDir = resolve(cwd, output ?? '.')
112
- const outPrefix = outDir === sep ? outDir : `${outDir}${sep}`
113
- const lockAbs = join(outDir, FILE_LOCK)
114
-
115
- // Plan first, write later: validate every target path and confirm it stays
116
- // inside outDir before touching disk, so a malformed bundle can't write a
117
- // partial tree. Walk bundle.modules directly rather than the flattening
118
- // `sources` getter, which silently last-wins when two buckets collapse to
119
- // the same path. Treat the bundle as untrusted input and verify containment
120
- // two ways: the resolved path must be BOTH non-escaping relative to outDir
121
- // (no leading '..', not absolute -- `relative` returns an absolute path
122
- // across Windows drives) AND lexically under `outDir/` (the trailing
123
- // separator rejects sibling dirs that merely share outDir as a name prefix,
124
- // e.g. `<outDir>-evil`). These checks are lexical only: pre-existing
125
- // symlinks inside outDir are followed by the writes, so untrusted bundles
126
- // belong in a fresh directory (see doc/extract.md).
127
- const writes = []
128
- const targets = new Set()
129
- for (const [dir, { files }] of bundle.modules) {
130
- for (const [rel, content] of Object.entries(files)) {
131
- // A `directory` capture (`stasis run --fs`) keys a listing at the directory's
132
- // OWN path; it isn't a file to write (its child files recreate the directory),
133
- // and writing one would collide with those children at the file-vs-directory
134
- // guard below. Skip it on disk -- the derived lockfile still attests its hash
135
- // (lockfileFromBundle hashes every bundle entry, directory listings included).
136
- // The format is looked up under the canonical key (moduleFileKey, so a
137
- // package-ROOT listing's rel==='' resolves to the dir, not `${dir}/`); the
138
- // write path below keeps the literal join, so a NON-directory empty file name
139
- // still trips the non-canonical-path guard rather than being accepted here.
140
- if (bundle.formats.get(moduleFileKey(dir, rel)) === 'directory') continue
141
- const file = dir === '.' ? rel : `${dir}/${rel}`
142
- if (typeof content !== 'string') throw new Error(`extract: bundle file content is not a string: ${file}`)
143
- const abs = resolve(outDir, file)
144
- const relToOut = relative(outDir, abs)
145
- if (relToOut.startsWith('..') || isAbsolute(relToOut) || !abs.startsWith(outPrefix)) {
146
- throw new Error(`extract: bundle path escapes output dir: ${file}`)
147
- }
148
- // Only canonical paths are accepted: a path that changes under the
149
- // resolve+relative round-trip (mid-path '..' or '.', empty segments, an
150
- // empty file name) is nothing State or the Solidity bundler ever emit,
151
- // and writing it would desync the on-disk tree from the lockfile's
152
- // recorded paths.
153
- if (relToOut !== file) throw new Error(`extract: non-canonical bundle path: ${file}`)
154
- if (targets.has(abs)) throw new Error(`extract: duplicate bundle path: ${file}`)
155
- if (withLockfile && abs === lockAbs) {
156
- throw new Error(`extract: bundle contains ${FILE_LOCK}, which would collide with the derived lockfile`)
157
- }
158
- targets.add(abs)
159
- // Resources are decoded back to their original bytes: 'resource:base64' from
160
- // base64 (rejected if non-canonical -- a tampered bundle can't ship lying
161
- // padding/invalid-char base64), 'resource' (and code) is written as the raw
162
- // UTF-8 string.
163
- const data = bundle.formats.get(file) === 'resource:base64'
164
- ? assertCanonicalBase64(content, file)
165
- : content
166
- writes.push([abs, data])
167
- }
168
- }
169
- // A planned file may not also be needed as a parent directory of another --
170
- // the mkdirSync/writeFileSync pair below would otherwise throw halfway
171
- // through the writes.
172
- for (const [abs] of writes) {
173
- for (let d = dirname(abs); d.length > outDir.length; d = dirname(d)) {
174
- if (targets.has(d)) {
175
- throw new Error(`extract: bundle path used as both file and directory: ${relative(outDir, d)}`)
176
- }
177
- }
178
- }
179
- const lockText = withLockfile ? lockfileFromBundle(bundle).serialize() : null
180
-
181
- for (const [abs, content] of writes) {
182
- mkdirSync(dirname(abs), { recursive: true })
183
- writeFileSync(abs, content)
184
- }
185
- mkdirSync(outDir, { recursive: true }) // for an empty bundle, where no write created it
186
- if (withLockfile) writeFileSync(lockAbs, lockText)
187
-
188
- console.warn(`[stasis] Extracted ${writes.length} file(s)${withLockfile ? ` and ${FILE_LOCK}` : ''} to ${outDir}`)
189
- if (!withLockfile) {
190
- console.warn(`[stasis] Warning: legacy v0 bundle records no package name/version, so ${FILE_LOCK} can not be restored and was not written`)
191
- }
192
- return { dir: outDir, files: writes.length }
193
- }