@starci/hfs 4.0.10 → 4.1.0
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.
- package/CHANGELOG.md +1 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +221 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +98 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -11,6 +11,102 @@ const isSpace = char => /\s/u.test(char);
|
|
|
11
11
|
const isWordStart = char => /[\p{L}_]/u.test(char);
|
|
12
12
|
const isWordPart = char => /[\p{L}\p{N}_$]/u.test(char);
|
|
13
13
|
|
|
14
|
+
const commentEnd = (text, index) => {
|
|
15
|
+
if (text[index] === '-' && text[index + 1] === '-') {
|
|
16
|
+
while (index < text.length && text[index] !== '\n') index += 1;
|
|
17
|
+
return index;
|
|
18
|
+
}
|
|
19
|
+
if (text[index] !== '/' || text[index + 1] !== '*') return null;
|
|
20
|
+
const end = text.indexOf('*/', index + 2);
|
|
21
|
+
return end === -1 ? text.length : end + 2;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const singleQuotedEnd = (text, index, push) => {
|
|
25
|
+
if (text[index] !== "'") return null;
|
|
26
|
+
index += 1;
|
|
27
|
+
while (index < text.length) {
|
|
28
|
+
if (text[index] === "'" && text[index + 1] === "'") { index += 2; continue; }
|
|
29
|
+
if (text[index] === "'") break;
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
push('string', '');
|
|
33
|
+
return index + 1;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const quotedIdentifierEnd = (text, index, push) => {
|
|
37
|
+
if (text[index] !== '"') return null;
|
|
38
|
+
let value = '';
|
|
39
|
+
index += 1;
|
|
40
|
+
while (index < text.length) {
|
|
41
|
+
if (text[index] === '"' && text[index + 1] === '"') { value += '"'; index += 2; continue; }
|
|
42
|
+
if (text[index] === '"') break;
|
|
43
|
+
value += text[index];
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
push('ident', value);
|
|
47
|
+
return index + 1;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const dollarTokenEnd = (text, index, push) => {
|
|
51
|
+
if (text[index] !== '$') return null;
|
|
52
|
+
const param = /^\$(\d+)/u.exec(text.slice(index, index + 12));
|
|
53
|
+
if (param) { push('param', param[0]); return index + param[0].length; }
|
|
54
|
+
const tag = /^\$([\p{L}_][\p{L}\p{N}_]*)?\$/u.exec(text.slice(index));
|
|
55
|
+
if (!tag) return null;
|
|
56
|
+
const end = text.indexOf(tag[0], index + tag[0].length);
|
|
57
|
+
push('string', '');
|
|
58
|
+
return end === -1 ? text.length : end + tag[0].length;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const numberEnd = (text, index, push) => {
|
|
62
|
+
if (!/\d/u.test(text[index])) return null;
|
|
63
|
+
let end = index;
|
|
64
|
+
while (end < text.length && /[\d.]/u.test(text[end])) end += 1;
|
|
65
|
+
push('number', text.slice(index, end));
|
|
66
|
+
return end;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const wordEnd = (text, index, push) => {
|
|
70
|
+
if (!isWordStart(text[index])) return null;
|
|
71
|
+
let end = index;
|
|
72
|
+
while (end < text.length && isWordPart(text[end])) end += 1;
|
|
73
|
+
const word = text.slice(index, end);
|
|
74
|
+
if (text[end] !== "'" || !/^[EeBbXxNn]$/u.test(word)) { push('word', word); return end; }
|
|
75
|
+
const escaped = /^[Ee]$/u.test(word);
|
|
76
|
+
end += 1;
|
|
77
|
+
while (end < text.length) {
|
|
78
|
+
if (escaped && text[end] === '\\') { end += 2; continue; }
|
|
79
|
+
if (text[end] === "'" && text[end + 1] === "'") { end += 2; continue; }
|
|
80
|
+
if (text[end] === "'") break;
|
|
81
|
+
end += 1;
|
|
82
|
+
}
|
|
83
|
+
push('string', '');
|
|
84
|
+
return end + 1;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const punctuationEnd = (text, index, push) => {
|
|
88
|
+
const two = text.slice(index, index + 2);
|
|
89
|
+
if (['::', '<=', '>=', '<>', '!=', '||', '->'].includes(two)) { push('punct', two); return index + 2; }
|
|
90
|
+
push('punct', text[index]);
|
|
91
|
+
return index + 1;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function consumeToken(text, index, push) {
|
|
95
|
+
const comment = commentEnd(text, index);
|
|
96
|
+
if (comment !== null) return comment;
|
|
97
|
+
if (text[index] === HOLE) { push('hole', HOLE); return index + 1; }
|
|
98
|
+
const singleQuoted = singleQuotedEnd(text, index, push);
|
|
99
|
+
if (singleQuoted !== null) return singleQuoted;
|
|
100
|
+
const identifier = quotedIdentifierEnd(text, index, push);
|
|
101
|
+
if (identifier !== null) return identifier;
|
|
102
|
+
const dollarToken = dollarTokenEnd(text, index, push);
|
|
103
|
+
if (dollarToken !== null) return dollarToken;
|
|
104
|
+
const number = numberEnd(text, index, push);
|
|
105
|
+
if (number !== null) return number;
|
|
106
|
+
const word = wordEnd(text, index, push);
|
|
107
|
+
return word ?? punctuationEnd(text, index, push);
|
|
108
|
+
}
|
|
109
|
+
|
|
14
110
|
/** Tokens: {t: 'word'|'ident'|'string'|'number'|'param'|'hole'|'punct', v, up?} (`up` is the uppercase word). */
|
|
15
111
|
export function tokenizeSql(text) {
|
|
16
112
|
const tokens = [];
|
|
@@ -19,80 +115,7 @@ export function tokenizeSql(text) {
|
|
|
19
115
|
while (i < text.length) {
|
|
20
116
|
const char = text[i];
|
|
21
117
|
if (isSpace(char)) { i += 1; continue; }
|
|
22
|
-
|
|
23
|
-
if (char === '/' && text[i + 1] === '*') {
|
|
24
|
-
const end = text.indexOf('*/', i + 2);
|
|
25
|
-
i = end === -1 ? text.length : end + 2;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (char === HOLE) { push('hole', HOLE); i += 1; continue; }
|
|
29
|
-
if (char === "'") {
|
|
30
|
-
i += 1;
|
|
31
|
-
while (i < text.length) {
|
|
32
|
-
if (text[i] === "'" && text[i + 1] === "'") { i += 2; continue; }
|
|
33
|
-
if (text[i] === "'") break;
|
|
34
|
-
i += 1;
|
|
35
|
-
}
|
|
36
|
-
i += 1;
|
|
37
|
-
push('string', '');
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (char === '"') {
|
|
41
|
-
let value = '';
|
|
42
|
-
i += 1;
|
|
43
|
-
while (i < text.length) {
|
|
44
|
-
if (text[i] === '"' && text[i + 1] === '"') { value += '"'; i += 2; continue; }
|
|
45
|
-
if (text[i] === '"') break;
|
|
46
|
-
value += text[i];
|
|
47
|
-
i += 1;
|
|
48
|
-
}
|
|
49
|
-
i += 1;
|
|
50
|
-
push('ident', value);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (char === '$') {
|
|
54
|
-
const param = /^\$(\d+)/u.exec(text.slice(i, i + 12));
|
|
55
|
-
if (param) { push('param', param[0]); i += param[0].length; continue; }
|
|
56
|
-
const tag = /^\$([\p{L}_][\p{L}\p{N}_]*)?\$/u.exec(text.slice(i));
|
|
57
|
-
if (tag) {
|
|
58
|
-
const end = text.indexOf(tag[0], i + tag[0].length);
|
|
59
|
-
i = end === -1 ? text.length : end + tag[0].length;
|
|
60
|
-
push('string', '');
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (/\d/u.test(char)) {
|
|
65
|
-
let j = i;
|
|
66
|
-
while (j < text.length && /[\d.]/u.test(text[j])) j += 1;
|
|
67
|
-
push('number', text.slice(i, j));
|
|
68
|
-
i = j;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (isWordStart(char)) {
|
|
72
|
-
let j = i;
|
|
73
|
-
while (j < text.length && isWordPart(text[j])) j += 1;
|
|
74
|
-
const word = text.slice(i, j);
|
|
75
|
-
if (text[j] === "'" && /^[EeBbXxNn]$/u.test(word)) {
|
|
76
|
-
const escaped = /^[Ee]$/u.test(word);
|
|
77
|
-
j += 1;
|
|
78
|
-
while (j < text.length) {
|
|
79
|
-
if (escaped && text[j] === '\\') { j += 2; continue; }
|
|
80
|
-
if (text[j] === "'" && text[j + 1] === "'") { j += 2; continue; }
|
|
81
|
-
if (text[j] === "'") break;
|
|
82
|
-
j += 1;
|
|
83
|
-
}
|
|
84
|
-
i = j + 1;
|
|
85
|
-
push('string', '');
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
push('word', word);
|
|
89
|
-
i = j;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const two = text.slice(i, i + 2);
|
|
93
|
-
if (['::', '<=', '>=', '<>', '!=', '||', '->'].includes(two)) { push('punct', two); i += 2; continue; }
|
|
94
|
-
push('punct', char);
|
|
95
|
-
i += 1;
|
|
118
|
+
i = consumeToken(text, i, push);
|
|
96
119
|
}
|
|
97
120
|
return tokens;
|
|
98
121
|
}
|
|
@@ -114,7 +137,7 @@ const isWord = (token, up) => token?.t === 'word' && token.up === up;
|
|
|
114
137
|
* The table reference at `start`: {parts, table, alias, next}, {dynamic: true}, or null (a subquery, a function, nothing).
|
|
115
138
|
* `next` is the index after the reference and its alias.
|
|
116
139
|
*/
|
|
117
|
-
function
|
|
140
|
+
function tableNameAt(tokens, start) {
|
|
118
141
|
let i = start;
|
|
119
142
|
while (isWord(tokens[i], 'ONLY') || isWord(tokens[i], 'LATERAL')) i += 1;
|
|
120
143
|
const first = tokens[i];
|
|
@@ -123,11 +146,36 @@ function tableRef(tokens, start, { columns = false } = {}) {
|
|
|
123
146
|
const parts = [nameOf(first)];
|
|
124
147
|
i += 1;
|
|
125
148
|
while (tokens[i]?.v === '.' && isName(tokens[i + 1])) { parts.push(nameOf(tokens[i + 1])); i += 2; }
|
|
149
|
+
return { parts, next: i };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function tableAliasAt(tokens, index) {
|
|
153
|
+
if (isWord(tokens[index], 'AS') && isName(tokens[index + 1])) return { alias: nameOf(tokens[index + 1]), next: index + 2 };
|
|
154
|
+
if (isName(tokens[index]) && !(tokens[index].t === 'word' && CLAUSE_WORDS.has(tokens[index].up))) return { alias: nameOf(tokens[index]), next: index + 1 };
|
|
155
|
+
return { alias: null, next: index };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function columnListEnd(tokens, start) {
|
|
159
|
+
let i = start;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
do {
|
|
162
|
+
if (tokens[i].v === '(') depth += 1;
|
|
163
|
+
else if (tokens[i].v === ')') depth -= 1;
|
|
164
|
+
i += 1;
|
|
165
|
+
} while (i < tokens.length && depth > 0);
|
|
166
|
+
return i;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function tableRef(tokens, start, { columns = false } = {}) {
|
|
170
|
+
const reference = tableNameAt(tokens, start);
|
|
171
|
+
if (!reference) return null;
|
|
172
|
+
if (reference.dynamic) return reference;
|
|
173
|
+
const { parts } = reference;
|
|
174
|
+
let i = reference.next;
|
|
126
175
|
if (tokens[i]?.v === '(' && !columns) return null;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (alias && tokens[i]?.v === '(') { let depth = 0; do { if (tokens[i].v === '(') depth += 1; else if (tokens[i].v === ')') depth -= 1; i += 1; } while (i < tokens.length && depth > 0); }
|
|
176
|
+
const { alias, next } = tableAliasAt(tokens, i);
|
|
177
|
+
i = next;
|
|
178
|
+
if (alias && tokens[i]?.v === '(') i = columnListEnd(tokens, i);
|
|
131
179
|
return { parts, table: parts.at(-1), schema: parts.length > 1 ? parts.at(-2) : null, alias, next: i };
|
|
132
180
|
}
|
|
133
181
|
|
|
@@ -138,26 +186,39 @@ function statementsOf(tokens) {
|
|
|
138
186
|
for (const token of tokens) {
|
|
139
187
|
if (token.t === 'punct' && token.v === '(') depth += 1;
|
|
140
188
|
if (token.t === 'punct' && token.v === ')') depth -= 1;
|
|
141
|
-
if (token.t === 'punct' && token.v === ';' && depth <= 0) { if (current.length) statements.push(current); current = []; continue; }
|
|
189
|
+
if (token.t === 'punct' && token.v === ';' && depth <= 0) { if (current.length) { statements.push(current); } current = []; continue; }
|
|
142
190
|
current.push(token);
|
|
143
191
|
}
|
|
144
192
|
if (current.length) statements.push(current);
|
|
145
193
|
return statements;
|
|
146
194
|
}
|
|
147
195
|
|
|
196
|
+
function cteNameBefore(tokens, index) {
|
|
197
|
+
let afterAs = index + 1;
|
|
198
|
+
if (isWord(tokens[afterAs], 'NOT')) afterAs += 1;
|
|
199
|
+
if (isWord(tokens[afterAs], 'MATERIALIZED')) afterAs += 1;
|
|
200
|
+
if (tokens[afterAs]?.v !== '(') return null;
|
|
201
|
+
let nameIndex = index - 1;
|
|
202
|
+
if (tokens[nameIndex]?.v === ')') {
|
|
203
|
+
let depth = 0;
|
|
204
|
+
do {
|
|
205
|
+
if (tokens[nameIndex].v === ')') depth += 1;
|
|
206
|
+
else if (tokens[nameIndex].v === '(') depth -= 1;
|
|
207
|
+
nameIndex -= 1;
|
|
208
|
+
} while (nameIndex >= 0 && depth > 0);
|
|
209
|
+
}
|
|
210
|
+
const before = tokens[nameIndex - 1];
|
|
211
|
+
if (isName(tokens[nameIndex]) && (isWord(before, 'WITH') || isWord(before, 'RECURSIVE') || before?.v === ',')) return nameOf(tokens[nameIndex]);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
148
215
|
/** The CTE names a statement declares: `name [(cols)] AS [NOT] [MATERIALIZED] (`. */
|
|
149
216
|
function cteNames(tokens) {
|
|
150
217
|
const names = new Set();
|
|
151
218
|
for (let i = 1; i < tokens.length; i += 1) {
|
|
152
219
|
if (!isWord(tokens[i], 'AS')) continue;
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
if (isWord(tokens[j], 'MATERIALIZED')) j += 1;
|
|
156
|
-
if (tokens[j]?.v !== '(') continue;
|
|
157
|
-
let k = i - 1;
|
|
158
|
-
if (tokens[k]?.v === ')') { let depth = 0; do { if (tokens[k].v === ')') depth += 1; else if (tokens[k].v === '(') depth -= 1; k -= 1; } while (k >= 0 && depth > 0); }
|
|
159
|
-
const before = tokens[k - 1];
|
|
160
|
-
if (isName(tokens[k]) && (isWord(before, 'WITH') || isWord(before, 'RECURSIVE') || before?.v === ',')) names.add(nameOf(tokens[k]));
|
|
220
|
+
const name = cteNameBefore(tokens, i);
|
|
221
|
+
if (name !== null) names.add(name);
|
|
161
222
|
}
|
|
162
223
|
return names;
|
|
163
224
|
}
|
|
@@ -219,51 +280,160 @@ function equalityColumns(conjunct, qualifiers) {
|
|
|
219
280
|
* a LIMIT/FETCH at the statement depth, no FROM at all (one row), only aggregates without GROUP BY, or an equality on
|
|
220
281
|
* every column of the primary key or of one unique key of the first table of FROM.
|
|
221
282
|
*/
|
|
283
|
+
function aggregateSelectItems(tokens, level, select, from) {
|
|
284
|
+
const items = [];
|
|
285
|
+
let item = [];
|
|
286
|
+
for (let i = select + 1; i < from; i += 1) {
|
|
287
|
+
if (level[i] === 0 && tokens[i].v === ',' && tokens[i].t === 'punct') { items.push(item); item = []; continue; }
|
|
288
|
+
if (!(items.length === 0 && item.length === 0 && (isWord(tokens[i], 'DISTINCT') || isWord(tokens[i], 'ALL')))) item.push(tokens[i]);
|
|
289
|
+
}
|
|
290
|
+
items.push(item);
|
|
291
|
+
return items;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function whereRegion(tokens, level, where) {
|
|
295
|
+
const region = [];
|
|
296
|
+
for (let i = where + 1; i < tokens.length; i += 1) {
|
|
297
|
+
if (level[i] === 0 && tokens[i].t === 'word' && WHERE_END.has(tokens[i].up)) break;
|
|
298
|
+
region.push({ token: tokens[i], level: level[i] });
|
|
299
|
+
}
|
|
300
|
+
return region;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The tokens of a WHERE region split at its top-level AND; the AND of a BETWEEN stays inside its conjunct. */
|
|
304
|
+
function splitConjuncts(region) {
|
|
305
|
+
const conjuncts = [[]];
|
|
306
|
+
let between = false;
|
|
307
|
+
for (const entry of region) {
|
|
308
|
+
const top = entry.level === 0;
|
|
309
|
+
if (top && isWord(entry.token, 'BETWEEN')) between = true;
|
|
310
|
+
const isAnd = top && isWord(entry.token, 'AND');
|
|
311
|
+
if (isAnd && !between) { conjuncts.push([]); continue; }
|
|
312
|
+
if (isAnd) between = false;
|
|
313
|
+
conjuncts.at(-1).push(entry.token);
|
|
314
|
+
}
|
|
315
|
+
return conjuncts;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function uniqueWhereColumns(region, primary) {
|
|
319
|
+
if (region.some(entry => entry.level === 0 && isWord(entry.token, 'OR'))) return null;
|
|
320
|
+
const conjuncts = splitConjuncts(region);
|
|
321
|
+
const qualifiers = new Set([primary.table, primary.alias].filter(Boolean));
|
|
322
|
+
const columns = new Set();
|
|
323
|
+
for (const conjunct of conjuncts) for (const column of equalityColumns(conjunct, qualifiers)) columns.add(column);
|
|
324
|
+
return columns;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function findTopWord(tokens, level, up, from = 0) {
|
|
328
|
+
for (let i = from; i < tokens.length; i += 1) { if (level[i] === 0 && isWord(tokens[i], up)) return i; }
|
|
329
|
+
return -1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const hasTopGroupBy = (tokens, level) => tokens.some((token, i) => level[i] === 0 && isWord(token, 'GROUP') && isWord(tokens[i + 1], 'BY'));
|
|
333
|
+
|
|
334
|
+
function isUniqueBounded(tokens, level, from, primary, uniqueSets) {
|
|
335
|
+
const where = findTopWord(tokens, level, 'WHERE', from);
|
|
336
|
+
if (!uniqueSets?.length || where < 0) return false;
|
|
337
|
+
const columns = uniqueWhereColumns(whereRegion(tokens, level, where), primary);
|
|
338
|
+
return Boolean(columns) && covers(uniqueSets, columns);
|
|
339
|
+
}
|
|
340
|
+
|
|
222
341
|
function selectBound(tokens, level, uniqueSetsOf, cte) {
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
const select = find('SELECT');
|
|
227
|
-
const from = find('FROM', select);
|
|
342
|
+
if (findTopWord(tokens, level, 'LIMIT') >= 0 || findTopWord(tokens, level, 'FETCH') >= 0) return { bounded: true, basis: 'limit' };
|
|
343
|
+
const select = findTopWord(tokens, level, 'SELECT');
|
|
344
|
+
const from = findTopWord(tokens, level, 'FROM', select);
|
|
228
345
|
if (from < 0) return { bounded: true, basis: 'no from' };
|
|
229
|
-
|
|
230
|
-
if (groupBy < 0) {
|
|
231
|
-
const items = [];
|
|
232
|
-
let item = [];
|
|
233
|
-
for (let i = select + 1; i < from; i += 1) {
|
|
234
|
-
if (at0(i) && tokens[i].v === ',' && tokens[i].t === 'punct') { items.push(item); item = []; continue; }
|
|
235
|
-
if (!(items.length === 0 && item.length === 0 && (isWord(tokens[i], 'DISTINCT') || isWord(tokens[i], 'ALL')))) item.push(tokens[i]);
|
|
236
|
-
}
|
|
237
|
-
items.push(item);
|
|
238
|
-
if (items.length && items.every(isAggregateItem)) return { bounded: true, basis: 'aggregate' };
|
|
239
|
-
}
|
|
346
|
+
if (!hasTopGroupBy(tokens, level) && aggregateSelectItems(tokens, level, select, from).every(isAggregateItem)) return { bounded: true, basis: 'aggregate' };
|
|
240
347
|
const primary = tableRef(tokens, from + 1);
|
|
241
348
|
if (!primary || primary.dynamic || cte.has(primary.table)) return { bounded: false, basis: 'unbounded', table: primary?.table ?? null };
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
349
|
+
if (isUniqueBounded(tokens, level, from, primary, uniqueSetsOf(primary.table))) return { bounded: true, basis: 'unique' };
|
|
350
|
+
return { bounded: false, basis: 'unbounded', table: primary.table };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function recordSqlReference(list, ref, reads, cte, state) {
|
|
354
|
+
if (!ref) return;
|
|
355
|
+
if (ref.dynamic) { state.dynamic += 1; return; }
|
|
356
|
+
if (ref.schema && SYSTEM_SCHEMAS.has(ref.schema)) return;
|
|
357
|
+
if (ref.table.startsWith('pg_')) return;
|
|
358
|
+
if (list === reads && !ref.schema && cte.has(ref.table)) return;
|
|
359
|
+
list.push({ table: ref.table, schema: ref.schema });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function recordTableList(tokens, start, list, record, commaIsPunctuation) {
|
|
363
|
+
let i = start;
|
|
364
|
+
for (;;) {
|
|
365
|
+
const ref = tableRef(tokens, i);
|
|
366
|
+
record(list, ref);
|
|
367
|
+
if (ref && tokens[ref.next]?.v === ',' && (!commaIsPunctuation || tokens[ref.next].t === 'punct')) { i = ref.next + 1; continue; }
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Where the table reference of a write verb starts (an offset from the verb) and whether it may carry a column list; null for any other token.
|
|
373
|
+
function writeTarget(token, next, previous) {
|
|
374
|
+
if (token.up === 'INSERT' && isWord(next, 'INTO')) return { offset: 2, columns: true };
|
|
375
|
+
if (token.up === 'UPDATE' && !(previous?.t === 'word' && NOT_A_WRITE_BEFORE_UPDATE.has(previous.up))) return { offset: 1, columns: false };
|
|
376
|
+
if ((token.up === 'DELETE' && isWord(next, 'FROM')) || (token.up === 'MERGE' && isWord(next, 'INTO'))) return { offset: 2, columns: false };
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function processWriteToken(tokens, level, index, token, record, state) {
|
|
381
|
+
const target = writeTarget(token, tokens[index + 1], state.previous);
|
|
382
|
+
if (target) {
|
|
383
|
+
if (level[index] === 0) state.hasWriteVerb = true;
|
|
384
|
+
record(state.writes, tableRef(tokens, index + target.offset, { columns: target.columns }));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (token.up !== 'TRUNCATE') return false;
|
|
388
|
+
let start = index + 1;
|
|
389
|
+
if (isWord(tokens[start], 'TABLE')) start += 1;
|
|
390
|
+
recordTableList(tokens, start, state.writes, record, false);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function processReadToken(tokens, index, token, record, state, mergeOrDelete) {
|
|
395
|
+
if (token.up === 'FROM' && !isWord(state.previous, 'DISTINCT') && !isWord(state.previous, 'DELETE') && state.parens.at(-1) !== true) {
|
|
396
|
+
recordTableList(tokens, index + 1, state.reads, record, true);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (token.up === 'JOIN') { record(state.reads, tableRef(tokens, index + 1)); return; }
|
|
400
|
+
if (token.up === 'USING' && mergeOrDelete && tokens[index + 1]?.v !== '(') record(state.reads, tableRef(tokens, index + 1));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function trackParenthesis(tokens, index, state) {
|
|
404
|
+
const token = tokens[index];
|
|
405
|
+
if (token.t !== 'punct') return false;
|
|
406
|
+
if (token.v === '(') { state.parens.push(index > 0 && tokens[index - 1].t === 'word' && FROM_INSIDE.has(tokens[index - 1].up)); return true; }
|
|
407
|
+
if (token.v !== ')') return false;
|
|
408
|
+
state.parens.pop();
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function statementReferences(tokens, level, cte) {
|
|
413
|
+
const state = { writes: [], reads: [], parens: [], dynamic: 0, hasWriteVerb: false };
|
|
414
|
+
const record = (list, ref) => recordSqlReference(list, ref, state.reads, cte, state);
|
|
415
|
+
const mergeOrDelete = tokens.some(token => isWord(token, 'MERGE')) || tokens.some(token => isWord(token, 'DELETE'));
|
|
416
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
417
|
+
const token = tokens[i];
|
|
418
|
+
if (trackParenthesis(tokens, i, state) || token.t !== 'word') continue;
|
|
419
|
+
state.previous = tokens[i - 1];
|
|
420
|
+
if (processWriteToken(tokens, level, i, token, record, state)) {
|
|
421
|
+
if (token.up === 'DELETE') i += 1;
|
|
422
|
+
continue;
|
|
264
423
|
}
|
|
424
|
+
processReadToken(tokens, i, token, record, state, mergeOrDelete);
|
|
265
425
|
}
|
|
266
|
-
return
|
|
426
|
+
return state;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function statementAnalysis(tokens, uniqueSetsOf) {
|
|
430
|
+
const cte = cteNames(tokens);
|
|
431
|
+
const level = depths(tokens);
|
|
432
|
+
const references = statementReferences(tokens, level, cte);
|
|
433
|
+
const selects = [];
|
|
434
|
+
const hasSelect = tokens.some((candidate, i) => level[i] === 0 && isWord(candidate, 'SELECT'));
|
|
435
|
+
if (hasSelect && !references.hasWriteVerb) selects.push(selectBound(tokens, level, uniqueSetsOf, cte));
|
|
436
|
+
return { ...references, selects };
|
|
267
437
|
}
|
|
268
438
|
|
|
269
439
|
/**
|
|
@@ -277,51 +447,11 @@ export function analyzeSql(text, { uniqueSetsOf = () => null } = {}) {
|
|
|
277
447
|
const selects = [];
|
|
278
448
|
let dynamic = 0;
|
|
279
449
|
for (const tokens of statementsOf(tokenizeSql(text))) {
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
if (ref.dynamic) { dynamic += 1; return; }
|
|
286
|
-
if (ref.schema && SYSTEM_SCHEMAS.has(ref.schema)) return;
|
|
287
|
-
if (ref.table.startsWith('pg_')) return;
|
|
288
|
-
if (list === reads && !ref.schema && cte.has(ref.table)) return;
|
|
289
|
-
list.push({ table: ref.table, schema: ref.schema });
|
|
290
|
-
};
|
|
291
|
-
const mergeOrDelete = tokens.some(token => isWord(token, 'MERGE')) || tokens.some(token => isWord(token, 'DELETE'));
|
|
292
|
-
let hasWriteVerb = false;
|
|
293
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
294
|
-
const token = tokens[i];
|
|
295
|
-
if (token.t === 'punct' && token.v === '(') { parens.push(i > 0 && tokens[i - 1].t === 'word' && FROM_INSIDE.has(tokens[i - 1].up)); continue; }
|
|
296
|
-
if (token.t === 'punct' && token.v === ')') { parens.pop(); continue; }
|
|
297
|
-
if (token.t !== 'word') continue;
|
|
298
|
-
const previous = tokens[i - 1];
|
|
299
|
-
if (token.up === 'INSERT' && isWord(tokens[i + 1], 'INTO')) { if (level[i] === 0) hasWriteVerb = true; record(writes, tableRef(tokens, i + 2, { columns: true })); }
|
|
300
|
-
else if (token.up === 'UPDATE' && !(previous?.t === 'word' && NOT_A_WRITE_BEFORE_UPDATE.has(previous.up))) { if (level[i] === 0) hasWriteVerb = true; record(writes, tableRef(tokens, i + 1)); }
|
|
301
|
-
else if (token.up === 'DELETE' && isWord(tokens[i + 1], 'FROM')) { if (level[i] === 0) hasWriteVerb = true; record(writes, tableRef(tokens, i + 2)); i += 1; }
|
|
302
|
-
else if (token.up === 'MERGE' && isWord(tokens[i + 1], 'INTO')) { if (level[i] === 0) hasWriteVerb = true; record(writes, tableRef(tokens, i + 2)); }
|
|
303
|
-
else if (token.up === 'TRUNCATE') {
|
|
304
|
-
let j = i + 1;
|
|
305
|
-
if (isWord(tokens[j], 'TABLE')) j += 1;
|
|
306
|
-
for (;;) {
|
|
307
|
-
const ref = tableRef(tokens, j);
|
|
308
|
-
record(writes, ref);
|
|
309
|
-
if (ref && tokens[ref.next]?.v === ',') { j = ref.next + 1; continue; }
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
} else if (token.up === 'FROM' && !isWord(previous, 'DISTINCT') && !isWord(previous, 'DELETE') && parens.at(-1) !== true) {
|
|
313
|
-
let j = i + 1;
|
|
314
|
-
for (;;) {
|
|
315
|
-
const ref = tableRef(tokens, j);
|
|
316
|
-
record(reads, ref);
|
|
317
|
-
if (ref && tokens[ref.next]?.v === ',' && tokens[ref.next].t === 'punct') { j = ref.next + 1; continue; }
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
} else if (token.up === 'JOIN') record(reads, tableRef(tokens, i + 1));
|
|
321
|
-
else if (token.up === 'USING' && mergeOrDelete && tokens[i + 1]?.v !== '(') record(reads, tableRef(tokens, i + 1));
|
|
322
|
-
}
|
|
323
|
-
const hasSelect = tokens.some((candidate, i) => level[i] === 0 && isWord(candidate, 'SELECT'));
|
|
324
|
-
if (hasSelect && !hasWriteVerb) selects.push(selectBound(tokens, level, uniqueSetsOf, cte));
|
|
450
|
+
const statement = statementAnalysis(tokens, uniqueSetsOf);
|
|
451
|
+
for (const write of statement.writes) writes.push(write);
|
|
452
|
+
for (const read of statement.reads) reads.push(read);
|
|
453
|
+
for (const select of statement.selects) selects.push(select);
|
|
454
|
+
dynamic += statement.dynamic;
|
|
325
455
|
}
|
|
326
456
|
return { writes, reads, selects, dynamic };
|
|
327
457
|
}
|
|
@@ -16,7 +16,7 @@ export const importedFrom = (kit, checker, node, modules, names = null) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/(?:ssr|supabase-js|auth-js|postgrest-js|storage-js)(?:\/|$)/u
|
|
19
|
-
.test(String(declaration?.getSourceFile?.().fileName ?? '').
|
|
19
|
+
.test(String(declaration?.getSourceFile?.().fileName ?? '').replaceAll('\\', '/'));
|
|
20
20
|
|
|
21
21
|
const typeFromSupabase = (checker, node) => {
|
|
22
22
|
let type;
|
|
@@ -72,60 +72,70 @@ const fromRequestBody = (kit, checker, node, seen = new Set()) => {
|
|
|
72
72
|
&& fromRequestBody(kit, checker, declaration.initializer, seen));
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
function verifyJwtCall(kit, checker, file, node, counters, violations) {
|
|
76
|
+
counters.verifyCalls += 1;
|
|
77
|
+
if (file.slot !== BE_SUPABASE_SLOT) {
|
|
78
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${BE_SUPABASE_SLOT}; the integration is the one token verifier.`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const options = objectLiteral(kit, checker, node.arguments[2]);
|
|
82
|
+
const property = name => options ? kit.propertyOf(options, name) : null;
|
|
83
|
+
const values = name => {
|
|
84
|
+
const entry = property(name);
|
|
85
|
+
return entry ? staticStrings(kit, checker, kit.valueOfProperty(entry)) : null;
|
|
86
|
+
};
|
|
87
|
+
const issuer = property('issuer');
|
|
88
|
+
const audience = values('audience');
|
|
89
|
+
const algorithms = values('algorithms');
|
|
90
|
+
const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && audience?.includes('authenticated')
|
|
91
|
+
&& algorithms !== null && algorithms.length > 0;
|
|
92
|
+
if (!good) {
|
|
93
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'Supabase jwtVerify must use createRemoteJWKSet(...) and pin issuer, audience `authenticated`, and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
|
|
94
|
+
} else counters.verifiedCalls += 1;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function inspectJwtCall(kit, checker, file, node, counters, violations) {
|
|
99
|
+
const binding = importedCall(kit, checker, node);
|
|
100
|
+
if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
|
|
101
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; use the Supabase JWKS verifier.`, { module: binding.module, method: binding.name });
|
|
102
|
+
}
|
|
103
|
+
if (binding?.module === 'jose' && binding.name === 'jwtVerify') return verifyJwtCall(kit, checker, file, node, counters, violations);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inspectRequestClaims(kit, file, node, checker, violations) {
|
|
108
|
+
const { ts } = kit;
|
|
109
|
+
if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && CLAIM_NAMES.has(accessName(ts, node))) {
|
|
110
|
+
const claim = accessName(ts, node);
|
|
111
|
+
if (fromRequestBody(kit, checker, node.expression)) reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${claim} is read from the request body; authorization claims come only from the verified token or the database.`, { claim });
|
|
112
|
+
}
|
|
113
|
+
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && fromRequestBody(kit, checker, node.initializer)) {
|
|
114
|
+
for (const element of node.name.elements) {
|
|
115
|
+
const property = element.propertyName ?? element.name;
|
|
116
|
+
if (ts.isIdentifier(property) && CLAIM_NAMES.has(property.text)) reportAt(violations, kit, BE_JWT_VERIFIED, file, element, `${property.text} is destructured from the request body; authorization claims come only from the verified token or the database.`, { claim: property.text });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function inspectBackendNode(kit, checker, file, node, counters, violations) {
|
|
122
|
+
const handled = kit.ts.isCallExpression(node) && inspectJwtCall(kit, checker, file, node, counters, violations);
|
|
123
|
+
if (!handled) inspectRequestClaims(kit, file, node, checker, violations);
|
|
124
|
+
}
|
|
125
|
+
|
|
75
126
|
export function checkBackendJwt(input) {
|
|
76
127
|
const kit = machineKit(input);
|
|
77
|
-
const { ts } = kit;
|
|
78
128
|
const violations = [];
|
|
79
129
|
const integrations = [...input.graph.files.values()].filter((file) => file.slot === BE_SUPABASE_SLOT);
|
|
80
130
|
if (!integrations.length) return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifiedCalls: 0 } };
|
|
81
|
-
|
|
82
|
-
let verifiedCalls = 0;
|
|
131
|
+
const counters = { verifyCalls: 0, verifiedCalls: 0 };
|
|
83
132
|
for (const file of input.graph.files.values()) {
|
|
84
133
|
const checker = kit.checkerOf(file.sourceFile);
|
|
85
|
-
kit.walk(file.sourceFile,
|
|
86
|
-
if (ts.isCallExpression(node)) {
|
|
87
|
-
const binding = importedCall(kit, checker, node);
|
|
88
|
-
if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
|
|
89
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; use the Supabase JWKS verifier.`, { module: binding.module, method: binding.name });
|
|
90
|
-
}
|
|
91
|
-
if (binding?.module === 'jose' && binding.name === 'jwtVerify') {
|
|
92
|
-
verifyCalls += 1;
|
|
93
|
-
if (file.slot !== BE_SUPABASE_SLOT) {
|
|
94
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${BE_SUPABASE_SLOT}; the integration is the one token verifier.`);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
const options = objectLiteral(kit, checker, node.arguments[2]);
|
|
98
|
-
const property = (name) => options ? kit.propertyOf(options, name) : null;
|
|
99
|
-
const values = (name) => {
|
|
100
|
-
const entry = property(name);
|
|
101
|
-
return entry ? staticStrings(kit, checker, kit.valueOfProperty(entry)) : null;
|
|
102
|
-
};
|
|
103
|
-
const issuer = property('issuer');
|
|
104
|
-
const audience = values('audience');
|
|
105
|
-
const algorithms = values('algorithms');
|
|
106
|
-
const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && audience?.includes('authenticated')
|
|
107
|
-
&& algorithms !== null && algorithms.length > 0;
|
|
108
|
-
if (!good) {
|
|
109
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'Supabase jwtVerify must use createRemoteJWKSet(...) and pin issuer, audience `authenticated`, and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
|
|
110
|
-
} else verifiedCalls += 1;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && CLAIM_NAMES.has(accessName(ts, node))) {
|
|
114
|
-
const claim = accessName(ts, node);
|
|
115
|
-
if (fromRequestBody(kit, checker, node.expression)) reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${claim} is read from the request body; authorization claims come only from the verified token or the database.`, { claim });
|
|
116
|
-
}
|
|
117
|
-
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && fromRequestBody(kit, checker, node.initializer)) {
|
|
118
|
-
for (const element of node.name.elements) {
|
|
119
|
-
const property = element.propertyName ?? element.name;
|
|
120
|
-
if (ts.isIdentifier(property) && CLAIM_NAMES.has(property.text)) reportAt(violations, kit, BE_JWT_VERIFIED, file, element, `${property.text} is destructured from the request body; authorization claims come only from the verified token or the database.`, { claim: property.text });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
124
|
-
});
|
|
134
|
+
kit.walk(file.sourceFile, node => inspectBackendNode(kit, checker, file, node, counters, violations));
|
|
125
135
|
}
|
|
126
|
-
if (verifyCalls === 0) {
|
|
136
|
+
if (counters.verifyCalls === 0) {
|
|
127
137
|
const anchor = integrations.find((file) => path.posix.basename(file.rel).endsWith('.jwks.ts')) ?? integrations[0];
|
|
128
138
|
reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, `${BE_SUPABASE_SLOT} has no jose.jwtVerify call backed by createRemoteJWKSet.`);
|
|
129
139
|
}
|
|
130
|
-
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls, verifiedCalls } };
|
|
140
|
+
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, verifiedCalls: counters.verifiedCalls } };
|
|
131
141
|
}
|