fix(db): close round-4 review evasions in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Default-export slot treated as an export name at both ends (helper-side export-default passes, consumer-side bare-default-import resolution); namespace-member write targets match the source's exported names via a per-file memberSyms union; factory conduits carry a name map computed to a fixpoint so renames propagate hop by hop (export-from, export-of-local, export default); tracked dynamic imports of capability conduits confer capability; factory destructure-rename from a schema namespace; static module-namespace createRequire destructure; spread-argument writes; DCL keywords (GRANT/REVOKE ON, LOCK TABLE); bracketed conventional DI receiver. Permanent controls E40-E53.
This commit is contained in:
@@ -15,11 +15,17 @@
|
||||
* (`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. Destructuring a
|
||||
* NON-class property from a schema namespace over-approximates: the
|
||||
* binding is treated as a schema namespace itself (fail-closed), and
|
||||
* single-alias derivation from a namespace (`const t = ns.companies`)
|
||||
* is folded in to a fixed depth.
|
||||
* all propagate symbol identity to the consumer. 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
|
||||
* write targets match the source's EXPORTED names — renames included
|
||||
* (`M.co` after `export { companies as co } from`) — not just the
|
||||
* original class symbols. Destructuring a NON-class property from a
|
||||
* schema namespace over-approximates: the binding is treated as a
|
||||
* schema namespace itself (fail-closed), and single-alias derivation
|
||||
* from a namespace (`const t = ns.companies`) is folded in to a
|
||||
* fixed depth.
|
||||
* (ii) Literal prong — a class-table name inside a string or template
|
||||
* span that also carries SQL context fails outside the allowlist.
|
||||
* Spans are produced by a real lexer, so comments cannot hide code
|
||||
@@ -27,7 +33,13 @@
|
||||
* migrations are excluded from this prong only (per contract).
|
||||
* (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE
|
||||
* when it imports a database driver or the createDb/createPgliteDb
|
||||
* factories (directly or via namespace). In a raw-capable file
|
||||
* factories — directly, via namespace, by destructuring a factory
|
||||
* symbol out of a schema namespace, or from a capability CONDUIT.
|
||||
* Conduits carry their own exported-name maps computed to a
|
||||
* fixpoint, so a factory renamed at any hop (`export { createDb as
|
||||
* mk } from`, `export default createDb`) still marks every module
|
||||
* down the chain, and a tracked dynamic import of a conduit is an
|
||||
* import edge like a static one. In a raw-capable file
|
||||
* outside the allowlist and register, EVERY `.execute(`, `.query(`,
|
||||
* and `.unsafe(` call fails, on any receiver, with any argument —
|
||||
* there is no tagged-template exemption (§6.3(b): "regardless of what
|
||||
@@ -43,7 +55,9 @@
|
||||
* Unanalyzable import routes fail everywhere except their own enumerated
|
||||
* disposition registers below: a dynamic import whose specifier is not a
|
||||
* single string literal, and createRequire (tracked through import aliasing
|
||||
* — `createRequire as x` — and destructuring from the module namespace; a
|
||||
* — `createRequire as x` — and destructuring from the module namespace,
|
||||
* whether that namespace came from a static `import * as` or a tracked
|
||||
* dynamic import; a
|
||||
* route that hides the NAME entirely, e.g. re-exporting createRequire from a
|
||||
* helper, falls under the value-flow residual). A LITERAL dynamic import is
|
||||
* an import edge like any static import (contract rev 9): its bindings
|
||||
@@ -543,6 +557,21 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
|
||||
mine.named.add(m[1]!);
|
||||
}
|
||||
}
|
||||
// The default slot is an export name like any other:
|
||||
// `export default companies;` / `export default mod.companies;`
|
||||
for (const m of f.code.matchAll(/export\s+default\s+(\w+)\s*;/g)) {
|
||||
if (aliases.named.includes(m[1]!)) mine.named.add('default');
|
||||
if (aliases.namespaces.includes(m[1]!)) mine.ns.add('default');
|
||||
}
|
||||
for (const ns of aliases.namespaces) {
|
||||
if (
|
||||
new RegExp(
|
||||
`export\\s+default\\s+${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
|
||||
).test(f.code)
|
||||
) {
|
||||
mine.named.add('default');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mine.named.size + mine.ns.size > before) {
|
||||
conduits.set(f.rel, mine);
|
||||
@@ -556,6 +585,10 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
|
||||
interface ClassAliases {
|
||||
named: string[]; // local identifiers bound to class-table symbols
|
||||
namespaces: string[]; // namespace identifiers over a schema source
|
||||
// Member names under which a class symbol is reachable on SOME imported
|
||||
// schema source (class symbols plus every renamed conduit export the file
|
||||
// imports) — the alternation for namespace-member write targets.
|
||||
memberSyms: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,13 +638,21 @@ function destructureBindings(
|
||||
function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
|
||||
const named = new Set<string>();
|
||||
const namespaces = new Set<string>();
|
||||
const memberSyms = new Set<string>(CLASS_SYMBOLS);
|
||||
for (const m of f.code.matchAll(IMPORT_RE)) {
|
||||
const [, typeOnly, , namedList, nsName, , spec] = m;
|
||||
const [, typeOnly, defaultWith, namedList, nsName, defaultBare, spec] = m;
|
||||
if (typeOnly) continue;
|
||||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||
for (const n of src.named) memberSyms.add(n);
|
||||
if (nsName) namespaces.add(nsName);
|
||||
if (namedList) importBindings(namedList, src, named, namespaces);
|
||||
// A default import binds whatever the source exports as `default`.
|
||||
const dflt = defaultWith ?? defaultBare;
|
||||
if (dflt) {
|
||||
if (src.named.has('default')) named.add(dflt);
|
||||
if (src.ns.has('default')) namespaces.add(dflt);
|
||||
}
|
||||
}
|
||||
// Literal dynamic imports of a schema source are import edges like any
|
||||
// other (contract rev 9): both binding shapes feed prong (i).
|
||||
@@ -621,6 +662,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
||||
const [, pattern, nsName, , spec] = m;
|
||||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||
for (const n of src.named) memberSyms.add(n);
|
||||
if (nsName) namespaces.add(nsName);
|
||||
if (pattern) destructureBindings(pattern, src, named, namespaces);
|
||||
}
|
||||
@@ -629,7 +671,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
||||
// const { schema } = ns; → nested namespace (fail-closed)
|
||||
// const t = ns.companies; → named alias
|
||||
// const s2 = ns.schema; → namespace alias
|
||||
const nsSrc: SchemaExports = { named: new Set(CLASS_SYMBOLS), ns: new Set() };
|
||||
const nsSrc: SchemaExports = { named: memberSyms, ns: new Set() };
|
||||
for (let pass = 0; pass < 2; pass += 1) {
|
||||
for (const ns of [...namespaces]) {
|
||||
for (const m of f.code.matchAll(
|
||||
@@ -643,12 +685,12 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
if (CLASS_SYMBOLS.includes(m[2]!)) named.add(m[1]!);
|
||||
if (memberSyms.has(m[2]!)) named.add(m[1]!);
|
||||
else namespaces.add(m[1]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { named: [...named], namespaces: [...namespaces] };
|
||||
return { named: [...named], namespaces: [...namespaces], memberSyms };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -658,27 +700,34 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
||||
* another driver conduit; importing ANYTHING from one confers driver
|
||||
* capability (the conduit module itself is additionally flagged by prong
|
||||
* (iii)'s literal check, so these routes fail at both ends).
|
||||
* `factory` — re-exports createDb/createPgliteDb from the db package or
|
||||
* another factory conduit (via `export … from`, or by exporting a local
|
||||
* binding of the factory under any name); importing ANYTHING from one
|
||||
* confers factory capability — the conduit may rename the symbol, so there
|
||||
* is no consumer-side name gate.
|
||||
* `factory` — carries a NAME MAP (module → the exported names under which a
|
||||
* factory is reachable), computed to a fixpoint: `export … from` propagates
|
||||
* the SOURCE's exported factory names through renames (star and star-as
|
||||
* copy them all), and a local binding of a factory (named import, namespace
|
||||
* member extraction, or tracked dynamic-import destructure) exported under
|
||||
* any name — braces, declaration, or `export default` — adds that name.
|
||||
* Importing ANYTHING from a factory conduit (statically or via a tracked
|
||||
* dynamic import) confers factory capability — the conduit may rename the
|
||||
* symbol at any hop, so there is no consumer-side name gate.
|
||||
*/
|
||||
function computeCapabilityConduits(
|
||||
files: FileFacts[],
|
||||
fileSet: Set<string>,
|
||||
): { driver: Set<string>; factory: Set<string> } {
|
||||
const driver = new Set<string>();
|
||||
const factory = new Set<string>();
|
||||
// Factory conduits carry a NAME MAP (module → exported names under which a
|
||||
// factory is reachable) so renames propagate hop by hop, exactly like the
|
||||
// schema export map — a literal-name gate at any hop would launder.
|
||||
const factoryNames = new Map<string, Set<string>>();
|
||||
const isDriverSpec = (rel: string, spec: string): boolean => {
|
||||
if (DRIVER_SPECIFIERS.includes(spec)) return true;
|
||||
const r = resolveSpecifier(rel, spec, fileSet);
|
||||
return r !== null && driver.has(r);
|
||||
};
|
||||
const isFactorySpec = (rel: string, spec: string): boolean => {
|
||||
if (spec === '@mosaicstack/db') return true;
|
||||
const factoryNamesOf = (rel: string, spec: string): Set<string> | null => {
|
||||
if (spec === '@mosaicstack/db') return new Set(DB_FACTORY_SYMBOLS);
|
||||
const r = resolveSpecifier(rel, spec, fileSet);
|
||||
return r !== null && factory.has(r);
|
||||
return r !== null ? (factoryNames.get(r) ?? null) : null;
|
||||
};
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
@@ -691,71 +740,93 @@ function computeCapabilityConduits(
|
||||
driver.add(f.rel);
|
||||
changed = true;
|
||||
}
|
||||
if (!factory.has(f.rel) && isFactorySpec(f.rel, spec)) {
|
||||
const named = m[2];
|
||||
if (named === undefined || /\b(createDb|createPgliteDb)\b/.test(named)) {
|
||||
factory.add(f.rel);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's
|
||||
// named branch above; `export x from` is not valid syntax — covered.
|
||||
// Export-of-local factory bindings: a module that BINDS the factory
|
||||
// (named import, namespace member, or tracked dynamic import) and
|
||||
// exports that binding under any name is a factory conduit even with
|
||||
// no `export … from` clause (`export const mk = mod.createDb`).
|
||||
if (!factory.has(f.rel)) {
|
||||
const locals = new Set<string>();
|
||||
const nss = new Set<string>();
|
||||
for (const im of f.code.matchAll(IMPORT_RE)) {
|
||||
const [, typeOnly, , namedList, nsName, , spec] = im;
|
||||
if (typeOnly || !isFactorySpec(f.rel, spec!)) continue;
|
||||
if (nsName) nss.add(nsName);
|
||||
if (namedList) {
|
||||
for (const part of namedList.split(',')) {
|
||||
const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
||||
if (am) locals.add(am[2] ?? am[1]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const dm of f.code.matchAll(
|
||||
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||||
)) {
|
||||
const [, pattern, nsName, , spec] = dm;
|
||||
if (!isFactorySpec(f.rel, spec!)) continue;
|
||||
if (nsName) nss.add(nsName);
|
||||
if (pattern) {
|
||||
for (const part of pattern.split(',')) {
|
||||
const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim());
|
||||
if (pm) locals.add(pm[2] ?? pm[1]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const ns of nss) {
|
||||
for (const am of f.code.matchAll(
|
||||
new RegExp(
|
||||
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`,
|
||||
'g',
|
||||
),
|
||||
)) {
|
||||
locals.add(am[1]!);
|
||||
}
|
||||
}
|
||||
for (const local of locals) {
|
||||
if (
|
||||
new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) ||
|
||||
new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code)
|
||||
) {
|
||||
factory.add(f.rel);
|
||||
changed = true;
|
||||
break;
|
||||
const mine = factoryNames.get(f.rel) ?? new Set<string>();
|
||||
const before = mine.size;
|
||||
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||||
if (m[1]) continue;
|
||||
const src = factoryNamesOf(f.rel, m[3]!);
|
||||
if (src === null || src.size === 0) continue;
|
||||
if (m[2] === undefined) {
|
||||
// `export *` / `export * as x` — over-approximated to the source's
|
||||
// names (membership is what confers capability on consumers).
|
||||
for (const n of src) mine.add(n);
|
||||
} else {
|
||||
for (const part of m[2].split(',')) {
|
||||
const seg = part.trim();
|
||||
if (!seg || seg.startsWith('type ')) continue;
|
||||
const am = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||||
if (src.has(am ? am[1]! : seg)) mine.add(am ? am[2]! : seg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Locals bound to a factory (named import — aliased or not — namespace
|
||||
// member extraction, or tracked dynamic-import destructure), then
|
||||
// exported under ANY name and by ANY form, braces or default included
|
||||
// (`export const mk = mod.createDb`, `export default createDb`).
|
||||
const locals = new Set<string>();
|
||||
const nss = new Map<string, Set<string>>();
|
||||
for (const im of f.code.matchAll(IMPORT_RE)) {
|
||||
const [, typeOnly, , namedList, nsName, , spec] = im;
|
||||
if (typeOnly) continue;
|
||||
const src = factoryNamesOf(f.rel, spec!);
|
||||
if (src === null || src.size === 0) continue;
|
||||
if (nsName) nss.set(nsName, src);
|
||||
if (namedList) {
|
||||
for (const part of namedList.split(',')) {
|
||||
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
||||
if (am && src.has(am[1]!)) locals.add(am[2] ?? am[1]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const dm of f.code.matchAll(
|
||||
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||||
)) {
|
||||
const [, pattern, nsName, , spec] = dm;
|
||||
const src = factoryNamesOf(f.rel, spec!);
|
||||
if (src === null || src.size === 0) continue;
|
||||
if (nsName) nss.set(nsName, src);
|
||||
if (pattern) {
|
||||
for (const part of pattern.split(',')) {
|
||||
const pm = /^(\w+)\s*(?::\s*(\w+))?/.exec(part.trim());
|
||||
if (pm && src.has(pm[1]!)) locals.add(pm[2] ?? pm[1]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [ns, src] of nss) {
|
||||
for (const am of f.code.matchAll(
|
||||
new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(\\w+)\\b`, 'g'),
|
||||
)) {
|
||||
if (src.has(am[2]!)) locals.add(am[1]!);
|
||||
}
|
||||
}
|
||||
if (locals.size > 0) {
|
||||
for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
|
||||
for (const part of m[1]!.split(',')) {
|
||||
const seg = part.trim();
|
||||
if (!seg || seg.startsWith('type ')) continue;
|
||||
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(seg);
|
||||
if (am && locals.has(am[1]!)) mine.add(am[2] ?? am[1]!);
|
||||
}
|
||||
}
|
||||
for (const local of locals) {
|
||||
if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) {
|
||||
mine.add(local);
|
||||
}
|
||||
if (new RegExp(`export\\s+default\\s+${local}\\b`).test(f.code)) {
|
||||
mine.add('default');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mine.size > before) {
|
||||
factoryNames.set(f.rel, mine);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { driver, factory };
|
||||
return { driver, factory: new Set(factoryNames.keys()) };
|
||||
}
|
||||
|
||||
/** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */
|
||||
@@ -854,6 +925,25 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!);
|
||||
if (alias) crNames.add(alias[1]!);
|
||||
}
|
||||
// Destructuring from a STATIC or dynamically-bound module namespace:
|
||||
// import * as M from 'node:module'; const { createRequire: x } = M;
|
||||
const modNs = new Set<string>();
|
||||
for (const m of code.matchAll(/import\s*\*\s*as\s+(\w+)\s*from\s*['"](?:node:)?module['"]/g)) {
|
||||
modNs.add(m[1]!);
|
||||
}
|
||||
for (const m of code.matchAll(
|
||||
/(?:const|let|var)\s+(\w+)\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g,
|
||||
)) {
|
||||
modNs.add(m[1]!);
|
||||
}
|
||||
for (const ns of modNs) {
|
||||
for (const m of code.matchAll(
|
||||
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'),
|
||||
)) {
|
||||
const alias = /\bcreateRequire\s*(?::\s*(\w+))?/.exec(m[1]!);
|
||||
if (alias) crNames.add(alias[1] ?? 'createRequire');
|
||||
}
|
||||
}
|
||||
for (const n of crNames) {
|
||||
if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) {
|
||||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
||||
@@ -869,7 +959,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
// Word boundaries live inside each alternative: the bracket form ends in
|
||||
// `]` (non-word), where a trailing `\b` could never match.
|
||||
const targets: string[] = [...aliases.named].map((a) => `${a}\\b`);
|
||||
const symAlt = CLASS_SYMBOLS.join('|');
|
||||
// Namespace members match the source's EXPORTED names (renames included),
|
||||
// not just the original class symbols.
|
||||
const symAlt = [...aliases.memberSyms].join('|');
|
||||
for (const ns of aliases.namespaces) {
|
||||
// Allow intermediate property segments (ns.schema.companies — nested
|
||||
// namespace re-exports) and literal computed access (ns['companies']).
|
||||
@@ -877,9 +969,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
|
||||
}
|
||||
if (targets.length > 0) {
|
||||
// `\(\s*\(*` tolerates argument parenthesization: .insert((companies)).
|
||||
// The argument prefix tolerates parenthesization, spread, and array
|
||||
// wrapping: .insert((companies)), .insert(...[companies]).
|
||||
const writeRe = new RegExp(
|
||||
`\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`,
|
||||
`\\.\\s*(insert|update|delete)\\s*\\(\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`,
|
||||
'g',
|
||||
);
|
||||
for (const m of code.matchAll(writeRe)) {
|
||||
@@ -924,7 +1017,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
// block comments, and quoting (including escaped quotes in span text).
|
||||
if (!inAllowlist && !isSchemaDefinition) {
|
||||
const kw =
|
||||
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)';
|
||||
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy|on|lock(?:\\s+table)?)';
|
||||
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
|
||||
const q = `(?:\\\\?["'\`])?`;
|
||||
const sqlAdjacentRe = new RegExp(
|
||||
@@ -956,6 +1049,13 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||||
}
|
||||
// A TRACKED dynamic import of a conduit is an import edge like any
|
||||
// static one (contract rev 9) — for capability too, not just prong (i).
|
||||
for (const m of f.code.matchAll(/=\s*await\s+import\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) {
|
||||
const r = resolveSpecifier(rel, m[2]!, fileSet);
|
||||
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||||
}
|
||||
const driverImport = literalDriver || conduitDriver;
|
||||
// Factory capability: a named/namespace edge to the factory symbols, OR
|
||||
// ANY value import from a factory conduit — the conduit may re-export
|
||||
@@ -967,6 +1067,12 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
(aliases.namespaces.length > 0 &&
|
||||
new RegExp(
|
||||
`\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`,
|
||||
).test(code)) ||
|
||||
// Destructuring a factory symbol OUT of a schema namespace confers
|
||||
// capability whatever the local rename: const { createDb: mk } = dbns.
|
||||
(aliases.namespaces.length > 0 &&
|
||||
new RegExp(
|
||||
`(?:const|let|var)\\s*\\{[^}]*\\b(?:createDb|createPgliteDb)\\b[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:${aliases.namespaces.join('|')})\\b`,
|
||||
).test(code));
|
||||
if (driverImport) {
|
||||
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' });
|
||||
@@ -995,8 +1101,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||||
if (!driverImport) {
|
||||
// DI residual backstop: db/client-shaped receivers fire regardless of
|
||||
// detected capability (a handle can arrive by injection).
|
||||
// The receiver may be a dotted name OR a literal bracketed member with
|
||||
// a conventional name: this['db'].query(…).
|
||||
for (const m of code.matchAll(
|
||||
/\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g,
|
||||
/(?:\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\[\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\s*\])\s*\.\s*(execute|query|unsafe)\s*\(/g,
|
||||
)) {
|
||||
violations.push({
|
||||
file: rel,
|
||||
@@ -1356,6 +1464,114 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
||||
name: 'E39 computed-member eval',
|
||||
src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`,
|
||||
},
|
||||
{
|
||||
name: 'E40 schema symbol renamed to default through conduit',
|
||||
src: `import c from './evasion-mid9.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid9.ts',
|
||||
src: `export { companies as default } from '@mosaicstack/db';`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E41 export default of locally bound schema symbol',
|
||||
src: `import c from './evasion-mid10.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid10.ts',
|
||||
src: `import { companies } from '@mosaicstack/db';\nexport default companies;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E42 export default of dynamic namespace member',
|
||||
src: `import c from './evasion-mid11.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid11.ts',
|
||||
src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E43 namespace member under renamed export',
|
||||
src: `import * as M from './evasion-mid12.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid12.ts',
|
||||
src: `export { companies as co } from '@mosaicstack/db';`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E44 dynamic namespace member under renamed export',
|
||||
src: `import { db } from './x.js';\nconst M = await import('./evasion-mid13.js');\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid13.ts',
|
||||
src: `export { companies as co } from '@mosaicstack/db';`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E45 two-hop factory export-from rename',
|
||||
src: `import { mk } from './evasion-mid15.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid14.ts',
|
||||
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||||
},
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid15.ts',
|
||||
src: `export { mk } from './evasion-mid14.js';`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E46 export default of locally bound factory',
|
||||
src: `import mk from './evasion-mid16.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid16.ts',
|
||||
src: `import { createDb } from '@mosaicstack/db';\nexport default createDb;`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E47 tracked dynamic import of a factory conduit',
|
||||
src: `const F = await import('./evasion-mid17.js');\nexport async function f(t: string) { await F.mk('u').execute('DELETE FROM ' + t); }`,
|
||||
extras: [
|
||||
{
|
||||
rel: 'packages/db/src/evasion-mid17.ts',
|
||||
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'E48 factory destructure-rename from schema namespace',
|
||||
src: `import * as dbns from '@mosaicstack/db';\nconst { createDb: mk } = dbns;\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||
},
|
||||
{
|
||||
name: 'E49 createRequire destructured from static module namespace',
|
||||
src: `import * as M from 'node:module';\nconst { createRequire: mkReq } = M;\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`,
|
||||
},
|
||||
{
|
||||
name: 'E50 spread-argument schema write',
|
||||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(...[companies]).values({}); }`,
|
||||
},
|
||||
{
|
||||
name: 'E51 GRANT ON class table in SQL span',
|
||||
src: `export const q = 'GRANT SELECT ON hierarchy_grants TO auditor';`,
|
||||
},
|
||||
{
|
||||
name: 'E52 LOCK TABLE class table in SQL span',
|
||||
src: `export const q = 'LOCK TABLE hierarchy_grants IN ACCESS EXCLUSIVE MODE';`,
|
||||
},
|
||||
{
|
||||
name: 'E53 bracketed conventional receiver',
|
||||
src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`,
|
||||
},
|
||||
];
|
||||
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user