diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 77f13ee6..bdfd738c 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -15,7 +15,10 @@ * (`export { companies as c } from`), an export of a locally bound * alias (`import { companies }; export { companies as co }`), and a * binding derived from a namespace (`export const co = ns.companies`) - * all propagate symbol identity to the consumer. The DEFAULT-export + * all propagate symbol identity to the consumer. Declaration exports + * tolerate a type annotation and prior declarators + * (`export const co: typeof companies = companies`, + * `export const dummy = 0, co = companies`). The DEFAULT-export * slot is an export name like any other (`export { companies as * default } from`, `export default companies`), so a bare default * import of a conduit resolves against the map too; namespace-member @@ -79,12 +82,15 @@ * residual is accepted and reviews of DI provider modules carry it. * - Computed member access with a NON-literal name (obj[verb]()) is not * statically resolvable; literal computed access (obj['insert']()) is - * flagged, and a computed member CALL whose key is a text-only template - * literal (obj[`insert`]()) fails closed anywhere — template text never - * reaches the lexer's code output, so such a call is indistinguishable - * from a runtime-constructed verb (a template key WITH interpolation is - * a non-literal computed member, above). Constructing the verb at - * runtime is adjacent to eval and is expected to be caught in review. + * flagged, and a computed member — access or call, in any position — + * whose key is a text-only template literal (obj[`insert`](), + * ns[`companies`]) fails closed anywhere — template text never reaches + * the lexer's code output, so such a member is indistinguishable from a + * runtime-constructed one (a template key WITH interpolation is a + * non-literal computed member, above). Invoking a write/exec verb via + * `.apply`/`.call`/`.bind` likewise fails closed anywhere. Constructing + * the member at runtime is adjacent to eval and is expected to be caught + * in review. * - The DB_FACTORY_IMPORTERS enumeration counts the import edges * hasSymbolImportEdge can see (named import, literal dynamic package * import with symbol use, namespace member use). Destructuring a factory @@ -492,6 +498,19 @@ function stripExprDressing(raw: string): string { /** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */ const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; +/** + * Declaration-head shapes: a declarator's initializer may sit behind a type + * annotation (`export const co: typeof companies = companies`) or behind + * prior declarators (`export const dummy = 0, co = companies`). TYPE_ANN + * admits `=>` inside the type text but stops at a bare `=` (the + * initializer); DECL_LIST skips prior declarators whose initializers are + * comma-free. Both are approximations of the declarator grammar — exotic + * prior initializers (an array or call containing a comma) fall to the + * value-flow residual. + */ +const TYPE_ANN = `(?:\\s*:\\s*(?:[^=;\\n]|=>)*?)?`; +const DECL_LIST = `(?:[\\w$]+${TYPE_ANN}\\s*=\\s*[^,;\\n]*,\\s*)*`; + interface FileFacts { rel: string; code: string; @@ -591,7 +610,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema for (const ns of aliases.namespaces) { for (const m of f.code.matchAll( new RegExp( - `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, + `export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, 'g', ), )) { @@ -600,7 +619,10 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema } for (const local of aliases.named) { for (const m of f.code.matchAll( - new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${local}\\b`, 'g'), + new RegExp( + `export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`, + 'g', + ), )) { mine.named.add(m[1]!); } @@ -734,7 +756,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set { name: 'E81 template-literal computed member call', src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`, }, + { + name: 'E82 template-literal computed member as write target', + src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\`]).values({}); }`, + }, + { + name: 'E83 template-literal computed member in schema default export', + src: `import c from './evasion-mid35.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid35.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns[\`companies\`];`, + }, + ], + }, + { + name: 'E84 template-literal computed member in factory extraction', + src: `import { mk } from './evasion-mid36.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid36.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns[\`createDb\`];`, + }, + ], + }, + { + name: 'E85 type-annotated re-export of schema binding', + src: `import { co } from './evasion-mid37.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid37.ts', + src: `import { companies } from '@mosaicstack/db';\nexport const co: typeof companies = companies;`, + }, + ], + }, + { + name: 'E86 second-declarator re-export of schema binding', + src: `import { co } from './evasion-mid38.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid38.ts', + src: `import { companies } from '@mosaicstack/db';\nexport const dummy = 0,\n co = companies;`, + }, + ], + }, + { + name: 'E87 type-annotated re-export of factory binding', + src: `import { mk } from './evasion-mid39.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid39.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport const mk: typeof createDb = createDb;`, + }, + ], + }, + { + name: 'E88 second-declarator re-export of factory binding', + src: `import { mk } from './evasion-mid40.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid40.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport const d0 = 0,\n mk = createDb;`, + }, + ], + }, + { + name: 'E89 apply-invoked write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.apply(db, [companies]).values({}); }`, + }, + { + name: 'E90 call-invoked write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.call(db, companies).values({}); }`, + }, + { + name: 'E91 apply-invoked bracket verb in capability-free file', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool['execute'].apply(this.pool, ['TRUNCATE ' + t]); } }`, + }, + { + name: 'E92 template-keyed receiver verb in capability-free file', + src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool[\`execute\`].apply(this.pool, ['TRUNCATE ' + t]); } }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -1910,6 +2042,10 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'clean: comment mentioning a table is not SQL', src: `// syncs hierarchy_grants downstream\nexport const n = 1;`, }, + { + name: 'clean: interpolated template key is a non-literal computed member, not a text-only key', + src: `export function g(o: Record void>, k: string) { o[\`\${k}\`](); }`, + }, ]; it('analyzer flags every known evasion form', () => { @@ -1934,7 +2070,11 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { driverConduits: synthCap.driver, factoryConduits: synthCap.factory, }; - const v = analyzeFile(synthetic, synthCtx); + // An evasion is caught when ANY file in the chain is flagged: some + // laundering routes fail closed at the HELPER (origin), not the + // consumer — e.g. a template-keyed conduit export — which keeps the + // chain out of the tree just as effectively. + const v = [synthetic, ...extraFacts].flatMap((ff) => analyzeFile(ff, synthCtx)); expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0); } });