|
14 | 14 | * For v1 we apply a deliberately simple **regex allow-list** over the |
15 | 15 | * extracted body — full TypeScript AST analysis is deferred to v2. Anything |
16 | 16 | * the regex rejects (top-level `import`, `require(` / esbuild's `__require(`, |
17 | | - * `fetch(`, `process.*`, `globalThis.*`, `eval`, `new Function`, `.sudo(`) makes |
18 | | - * extraction **throw**. |
| 17 | + * `fetch(`, `process.*`, `globalThis.*`, `eval`, `new Function`, `.sudo(`, |
| 18 | + * `.create(`) makes extraction **throw**. |
| 19 | + * |
| 20 | + * The last two are one family: a member that is REAL on the host |
| 21 | + * `ScopedContext`/`ObjectRepository` and absent from the VM's `ctx.api`, so the |
| 22 | + * same handler source passes an in-process test and TypeErrors the moment the |
| 23 | + * build lowers it into a body. `.create(` carries one wrinkle `.sudo(` does not |
| 24 | + * — see its entry in `FORBIDDEN_PATTERNS`. |
19 | 25 | * |
20 | 26 | * ⚠️ What that throw costs the BUILD depends on the flag, and the two outcomes |
21 | 27 | * are not the same one. This header used to claim only the second (#10678): |
@@ -213,6 +219,49 @@ const FORBIDDEN_PATTERNS: Array<{ rx: RegExp; reason: string }> = [ |
213 | 219 | + 'before-hook (`ctx.input.<field> = ...`), or leave this handler bundled so it runs in-process ' |
214 | 220 | + 'where `sudo()` exists', |
215 | 221 | }, |
| 222 | + // [#16249] Same family as `.sudo(` above, one layer over: the host |
| 223 | + // `ObjectRepository` aliases `create(data)` to `insert(data)`, the spec |
| 224 | + // contract `IScopedObjectRepository` declares `insert` and NOT `create` |
| 225 | + // (packages/spec/src/contracts/scoped-context.ts — `create` is listed there |
| 226 | + // as measured and deliberately excluded), and the VM installs exactly |
| 227 | + // `insert / update / delete / updateMany / deleteMany / upsert` as the |
| 228 | + // `ctx.api.object()` write leaves (`installCtx`, |
| 229 | + // runtime/src/sandbox/quickjs-runner.ts). So a lowered body's `.create()` is |
| 230 | + // `TypeError: not a function` on its FIRST run, and under a hook's default |
| 231 | + // `onError: 'abort'` that aborts the triggering write with a message naming |
| 232 | + // no member — the blind message #14010 measured for `sudo()`. |
| 233 | + // |
| 234 | + // What made this worse than an omission: the extractor ledger |
| 235 | + // (`HOOK_BODY_WRITE_PATTERNS`, packages/lint) ADVERTISED `.create({…})` as |
| 236 | + // legal `api-crud-literal` syntax and graded its payload as a live write, so |
| 237 | + // the one layer that actively told an author how to write it named a spelling |
| 238 | + // that cannot run. That entry is withdrawn in the same change; refusing here |
| 239 | + // is what makes build time say what the contract already said. |
| 240 | + // |
| 241 | + // ⛔ The alternative — installing a `create` leaf in `installCtx` — is |
| 242 | + // rejected on purpose: it would have the SANDBOX ratify a verb the CONTRACT |
| 243 | + // never declared, which is the wrong direction under contract-first. |
| 244 | + // |
| 245 | + // Receiver-loose like `.sudo(` (a local alias `const repo = |
| 246 | + // ctx.api.object('x'); repo.create(…)` must not slip through), with ONE |
| 247 | + // carve-out that `.sudo(` needs no equivalent of: `Object` is a real sandbox |
| 248 | + // global (pinned in `SANDBOX_GLOBALS`), so `Object.create(null)` is working, |
| 249 | + // lowerable code. Refusing it would turn a correct body into a bundled |
| 250 | + // closure — and a hard failure under `--strict-body` — which is a false |
| 251 | + // refusal, not the safe direction. The lookbehind excludes that ONE receiver |
| 252 | + // and nothing else: `myObject.create(` still matches, because `\b` requires a |
| 253 | + // word boundary before `Object`. |
| 254 | + { |
| 255 | + rx: /(?<!\bObject\s*)\.\s*create\s*\(/, |
| 256 | + reason: |
| 257 | + '`create()` is not reachable from a sandboxed body — the VM\'s `ctx.api.object()` installs ' |
| 258 | + + '`insert` / `update` / `delete` / `updateMany` / `deleteMany` / `upsert` and no `create` leaf, so ' |
| 259 | + + 'the call is a TypeError at run time (and under a hook\'s default `onError: \'abort\'` that aborts ' |
| 260 | + + 'the triggering write). Spell the same payload `.insert({ ... })`, which is the member the sandbox ' |
| 261 | + + 'actually has and the only insert verb the spec contract declares; `Object.create()` is unaffected. ' |
| 262 | + + 'Alternatively leave this handler bundled so it runs in-process, where the host repository\'s ' |
| 263 | + + '`create()` alias exists', |
| 264 | + }, |
216 | 265 | ]; |
217 | 266 |
|
218 | 267 | const CAPABILITY_PATTERNS: Array<{ rx: RegExp; cap: 'api.read' | 'api.write' | 'crypto.uuid' | 'log' }> = [ |
|
0 commit comments