@starci/hfs 4.9.11 → 4.10.1
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 +18 -0
- package/README.md +7 -9
- package/db/backup-receipt.mjs +10 -47
- package/db/drift.mjs +1 -1
- package/db/dump.mjs +11 -10
- package/db/entity-list.mjs +1 -1
- package/db/migrate.mjs +83 -259
- package/db/new.mjs +46 -0
- package/db/post-cutover.mjs +13 -17
- package/db/postgres.mjs +80 -0
- package/emit/contracts.mjs +5 -9
- package/package.json +1 -1
- package/release/gate.mjs +3 -15
- package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
- package/runtime/knowledge/hfs/design-truths.yaml +1 -1
- package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
- package/runtime/knowledge/hfs/rules.yaml +179 -157
- package/runtime/knowledge/hfs/slots.yaml +185 -172
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
- package/runtime/knowledge/patterns/be/projections.yaml +7 -5
- package/runtime/knowledge/patterns/be/queues.yaml +8 -8
- package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
- package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
- package/runtime/knowledge/patterns/be/saga.yaml +3 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +65 -115
- package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entity-registry.mjs +53 -0
- package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
- package/runtime/scripts/hfs/architecture/index.mjs +2 -3
- package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
- package/runtime/scripts/hfs/architecture/owners.mjs +11 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
- package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
- package/runtime/scripts/hfs/architecture/tiers.mjs +14 -3
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
- package/runtime/scripts/hfs/check.mjs +6 -6
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
- package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
- package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
- package/runtime/scripts/hfs/manifest-shape.mjs +9 -5
- package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
- package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
- package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
- package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
- package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
- package/runtime/scripts/hfs/rules/database.mjs +36 -39
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
- package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
- package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +8 -2
- package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/runtime/scripts/lib/swarm-stack.mjs +7 -4
- package/scaffold/add-app.mjs +2 -10
- package/scaffold/add-cli-lite.mjs +6 -6
- package/scaffold/add-compose.mjs +1 -1
- package/scaffold/add-inbox-lite.mjs +4 -8
- package/scaffold/add-plan.mjs +1 -1
- package/scaffold/add-queue-lite.mjs +16 -24
- package/scaffold/add-table.mjs +34 -26
- package/scaffold/add.mjs +2 -2
- package/scaffold/app.mjs +19 -86
- package/scaffold/lite-exports.mjs +1 -1
- package/src/main.mjs +19 -20
- package/stack/target-binding.mjs +0 -60
- package/stack/urgent-floor.mjs +2 -2
- package/sync/index.mjs +32 -8
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
- package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
- package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
- package/templates/app/gitignore-lite +0 -3
- package/templates/app/package-scripts/db.json +9 -0
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
- package/templates/app/skeleton-lite/README.md +11 -11
- package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
- package/templates/app/tool-config-lite/prettierignore +0 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
- package/templates/be/inbox/database-client.ts.tpl +13 -4
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
- package/templates/be/inbox/platform/migration.sql.tpl +22 -0
- package/templates/be/inbox/service.spec.ts.tpl +2 -2
- package/templates/be/inbox/service.ts.tpl +1 -2
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
- package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
- package/templates/be/table/entities.index.ts.tpl +4 -0
- package/templates/be/table/entity.ts.tpl +21 -0
- package/templates/be/table/index.ts.tpl +1 -0
- package/templates/be/table/module.ts.tpl +1 -1
- package/templates/be/table/service.api.ts.tpl +10 -11
- package/templates/be/table/service.ts.tpl +5 -6
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
- package/db/local-migrate.mjs +0 -194
- package/db/reset.mjs +0 -125
- package/emit/db-types.mjs +0 -271
- package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
- package/runtime/scripts/api/supabase/database-push.mjs +0 -5
- package/runtime/scripts/api/supabase/database-query.mjs +0 -5
- package/runtime/scripts/api/supabase/lib.mjs +0 -55
- package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
- package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
- package/scaffold/supabase-ports.mjs +0 -26
- package/templates/app/package-scripts/supabase.json +0 -7
- package/templates/app/skeleton-lite/supabase/config.toml +0 -48
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
- package/templates/be/inbox/sql.ts.tpl +0 -16
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
- package/templates/be/table/rows.ts.tpl +0 -4
- package/templates/be/table/sql.api.ts.tpl +0 -7
- package/templates/be/table/sql.ts.tpl +0 -4
- package/templates/fe/auth/supabase-client.ts.tpl +0 -38
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
- /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// lib.mjs - resolve and spawn the app-local Supabase CLI for this API system, with no shell.
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
|
|
6
|
-
const candidateNames = (platform) => platform === 'win32'
|
|
7
|
-
? ['supabase.exe', 'supabase', 'supabase.cmd']
|
|
8
|
-
: ['supabase'];
|
|
9
|
-
|
|
10
|
-
/** Resolve the app's installed npm launcher before executable discovery; npm command shims cannot run without a shell. */
|
|
11
|
-
function resolveSupabase(appRoot, { env = process.env, platform = process.platform, exists = fs.existsSync } = {}) {
|
|
12
|
-
const launcher = path.join(appRoot, 'node_modules', 'supabase', 'dist', 'supabase.js');
|
|
13
|
-
if (exists(launcher)) return { file: process.execPath, args: [launcher] };
|
|
14
|
-
const names = candidateNames(platform);
|
|
15
|
-
const local = path.join(appRoot, 'node_modules', '.bin');
|
|
16
|
-
for (const name of names) {
|
|
17
|
-
const file = path.join(local, name);
|
|
18
|
-
if (exists(file)) return { file, args: [] };
|
|
19
|
-
}
|
|
20
|
-
const delimiter = platform === 'win32' ? ';' : ':';
|
|
21
|
-
for (const directory of String(env.PATH ?? '').split(delimiter).filter(Boolean)) {
|
|
22
|
-
for (const name of names) {
|
|
23
|
-
const file = path.join(directory, name);
|
|
24
|
-
if (exists(file)) return { file, args: [] };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { file: 'supabase', args: [] };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Run one shell-free Supabase call at the app root; an unusable command shim returns an ENOEXEC spawn failure. */
|
|
31
|
-
export const supabaseSpawn = (appRoot, args, {
|
|
32
|
-
env = process.env,
|
|
33
|
-
supabase,
|
|
34
|
-
timeout = 60_000,
|
|
35
|
-
run = spawnSync,
|
|
36
|
-
platform = process.platform,
|
|
37
|
-
exists = fs.existsSync,
|
|
38
|
-
} = {}) => {
|
|
39
|
-
const command = supabase === undefined || supabase === null ? resolveSupabase(appRoot, { env, platform, exists }) : { file: supabase, args: [] };
|
|
40
|
-
if (/\.(?:cmd|bat)$/iu.test(command.file)) return {
|
|
41
|
-
status: null, stdout: '', stderr: '', signal: null,
|
|
42
|
-
error: Object.assign(new Error('The Supabase command shim requires a shell; use an executable or the app-local npm launcher.'), { code: 'ENOEXEC' }),
|
|
43
|
-
};
|
|
44
|
-
return run(command.file, [...command.args, ...args], {
|
|
45
|
-
cwd: appRoot,
|
|
46
|
-
encoding: 'utf8',
|
|
47
|
-
env,
|
|
48
|
-
timeout,
|
|
49
|
-
windowsHide: true,
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** Invoke one Supabase command prefix with caller arguments and runner options. */
|
|
54
|
-
export const supabaseCall = (prefix, appRoot, args = [], options = {}) =>
|
|
55
|
-
supabaseSpawn(appRoot, [...prefix, ...args], options);
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// migration-repair.mjs - migration history repair for an app's local or linked Supabase database.
|
|
2
|
-
import { supabaseCall } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The spawn result of supabase migration repair; arguments and runner options remain with the caller. */
|
|
5
|
-
export function migrationRepair(...args) { return supabaseCall(['migration', 'repair'], ...args); }
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
const CACHE = new Map();
|
|
5
|
-
|
|
6
|
-
/** A static TypeScript property name, as emitted by `supabase gen types typescript`. */
|
|
7
|
-
const propertyName = (typescript, name) => (typescript.isIdentifier(name) || typescript.isStringLiteralLike(name) ? name.text : null);
|
|
8
|
-
|
|
9
|
-
/** The type-literal members below property `name`, or null when the generated shape is not statically readable. */
|
|
10
|
-
const propertyMembers = (typescript, members, name) => {
|
|
11
|
-
const property = members.find(member => typescript.isPropertySignature(member) && propertyName(typescript, member.name) === name);
|
|
12
|
-
return property?.type && typescript.isTypeLiteralNode(property.type) ? property.type.members : null;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function addTableNames(typescript, members, tables) {
|
|
16
|
-
if (!members) return;
|
|
17
|
-
for (const member of members) {
|
|
18
|
-
if (!typescript.isPropertySignature(member)) continue;
|
|
19
|
-
const name = propertyName(typescript, member.name);
|
|
20
|
-
if (name) tables.add(name.toLowerCase());
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readTableNames(typescript, file, tables) {
|
|
25
|
-
const source = typescript.createSourceFile(file, fs.readFileSync(file, 'utf8'), typescript.ScriptTarget.Latest, true, typescript.ScriptKind.TS);
|
|
26
|
-
if (source.parseDiagnostics.length !== 0) return;
|
|
27
|
-
const database = source.statements.find(statement => typescript.isTypeAliasDeclaration(statement) && statement.name.text === 'Database');
|
|
28
|
-
const databaseMembers = database && typescript.isTypeLiteralNode(database.type) ? database.type.members : null;
|
|
29
|
-
const publicMembers = databaseMembers ? propertyMembers(typescript, databaseMembers, 'public') : null;
|
|
30
|
-
const tableMembers = publicMembers ? propertyMembers(typescript, publicMembers, 'Tables') : null;
|
|
31
|
-
addTableNames(typescript, tableMembers, tables);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The lower-case keys of `Database['public']['Tables']`, parsed with the architecture context's target compiler.
|
|
36
|
-
* An absent, malformed or unreadable contract proves no table. The result is cached once per app root.
|
|
37
|
-
*/
|
|
38
|
-
export function supabaseTablesOf(appRoot, typescript) {
|
|
39
|
-
const root = path.resolve(appRoot);
|
|
40
|
-
if (CACHE.has(root)) return CACHE.get(root);
|
|
41
|
-
const tables = new Set();
|
|
42
|
-
const file = path.join(root, 'supabase', 'types', 'database.types.ts');
|
|
43
|
-
try {
|
|
44
|
-
readTableNames(typescript, file, tables);
|
|
45
|
-
} catch {
|
|
46
|
-
// Missing and unreadable generated contracts are fail-closed: no table is proven.
|
|
47
|
-
}
|
|
48
|
-
CACHE.set(root, tables);
|
|
49
|
-
return tables;
|
|
50
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { machineKit } from './machine-ast.mjs';
|
|
2
|
-
import { callName, chainParts, contextTypesClient, isDatabaseType, isSupabaseCall, jwtPolicyProblems, moduleNameOf, reportAt, supabaseClientType, supabaseDataAccess, SUPABASE_MODULES } from './supabase-ast.mjs';
|
|
3
|
-
import { checkBackendJwt } from './supabase-be.mjs';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* L10-L16, the slot-driven Supabase machine. On the back end Supabase provides Auth/Storage with one owner,
|
|
7
|
-
* `be.integrations.supabase`. On the front end Supabase is auth only (owner ruling 2026-10-13): `fe.modules.auth` is the one
|
|
8
|
-
* importer of `@supabase/*` and calls only the auth API; data, storage, rpc and realtime go through the back end (modules/api).
|
|
9
|
-
* The checks below recognise provider calls and types by their imported package declarations, not by local variable or type names.
|
|
10
|
-
*/
|
|
11
|
-
export const FRONTEND_SUPABASE_RULE_IDS = Object.freeze(['FE_SUPABASE_CLIENT_OWNER', 'FE_SERVICE_ROLE_FORBIDDEN']);
|
|
12
|
-
export const BACKEND_SUPABASE_RULE_IDS = Object.freeze(['BE_SUPABASE_CLIENT_OWNER', 'BE_SUPABASE_JWT_VERIFIED', 'BE_JWT_ALG_PINNED']);
|
|
13
|
-
|
|
14
|
-
const FE_CLIENT_OWNER = 'FE_SUPABASE_CLIENT_OWNER';
|
|
15
|
-
const BE_CLIENT_OWNER = 'BE_SUPABASE_CLIENT_OWNER';
|
|
16
|
-
const FE_SERVICE_ROLE = 'FE_SERVICE_ROLE_FORBIDDEN';
|
|
17
|
-
const BE_SUPABASE_SLOT = 'be.integrations.supabase';
|
|
18
|
-
const FE_AUTH_SLOT = 'fe.modules.auth';
|
|
19
|
-
/** The front-end provider slot: enabled exactly when a connection declares provider supabase. */
|
|
20
|
-
const FE_SUPABASE_PROVIDER_SLOT = 'fe.route.callback';
|
|
21
|
-
const FE_CONFIG_SLOT = 'fe.modules.config';
|
|
22
|
-
const BE_DATA_MEMBERS = new Set(['from', 'rpc', 'schema', 'functions', 'channel', 'realtime', 'getChannels', 'removeChannel', 'removeAllChannels']);
|
|
23
|
-
/** The members of a Supabase client that read or write data, files or channels: never called from the front end. */
|
|
24
|
-
const FE_DATA_MEMBERS = new Set(['from', 'rpc', 'schema', 'storage', 'functions', 'realtime', 'channel', 'getChannels', 'removeChannel', 'removeAllChannels']);
|
|
25
|
-
const supabaseSlotEnabled = (input, slotId) => {
|
|
26
|
-
const slot = input.graph.resolver.slot(slotId);
|
|
27
|
-
return Boolean(slot && input.graph.resolver.slotEnabled(slot));
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const CLIENT_FACTORY = /^create(?:Client|ServerClient|BrowserClient)$/u;
|
|
31
|
-
|
|
32
|
-
function inspectSupabaseImport(scope, node) {
|
|
33
|
-
const { kit, file, slots, ruleId, violations, counters } = scope;
|
|
34
|
-
const moduleName = moduleNameOf(kit.ts, node);
|
|
35
|
-
if (!moduleName || !SUPABASE_MODULES.has(moduleName)) return;
|
|
36
|
-
counters.imports += 1;
|
|
37
|
-
if (!slots.has(file.slot)) reportAt(violations, kit, ruleId, file, node, `${moduleName} is imported by ${file.rel}, whose slot is ${file.slot ?? 'unowned'}; Supabase imports belong only to ${[...slots].join(' or ')}.`, { module: moduleName, slot: file.slot ?? null });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function inspectClientTypeReference(scope, node) {
|
|
41
|
-
const { kit, checker, file, ruleId, violations, counters } = scope;
|
|
42
|
-
if (!kit.ts.isTypeReferenceNode(node) || !supabaseClientType(kit, checker, node)) return;
|
|
43
|
-
counters.clients += 1;
|
|
44
|
-
if (!isDatabaseType(kit.ts, node.typeArguments?.[0])) reportAt(violations, kit, ruleId, file, node, 'SupabaseClient must carry the committed Database type as SupabaseClient<Database>.');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function inspectClientFactoryCall(scope, node) {
|
|
48
|
-
const { kit, checker, file, ruleId, violations, counters } = scope;
|
|
49
|
-
const binding = kit.importBinding(checker, node.expression);
|
|
50
|
-
if (!binding || !SUPABASE_MODULES.has(binding.module) || !CLIENT_FACTORY.test(binding.name)) return;
|
|
51
|
-
counters.clients += 1;
|
|
52
|
-
if (!isDatabaseType(kit.ts, node.typeArguments?.[0]) && !contextTypesClient(kit, checker, node)) {
|
|
53
|
-
reportAt(violations, kit, ruleId, file, node, `${binding.name} creates an untyped Supabase client; create it as ${binding.name}<Database>(...) (or give it the contextual type SupabaseClient<Database>).`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function inspectProviderCall(scope, node) {
|
|
58
|
-
const { kit, checker, file, slots, ruleId, violations, counters, reportedData } = scope;
|
|
59
|
-
const { ts } = kit;
|
|
60
|
-
const data = supabaseDataAccess(kit, checker, node, BE_DATA_MEMBERS, { storageAllowed: true });
|
|
61
|
-
if (data) {
|
|
62
|
-
if (!reportedData.has(data.node.pos)) {
|
|
63
|
-
reportedData.add(data.node.pos);
|
|
64
|
-
reportAt(violations, kit, ruleId, file, data.node, `A Supabase .${data.name} data call is made from ${file.slot ?? 'an unowned path'}; the back-end SDK is Auth/Storage only, including inside ${BE_SUPABASE_SLOT}. Data uses EntityManager as app_be.`, { member: data.name, slot: file.slot ?? null });
|
|
65
|
-
}
|
|
66
|
-
if (ts.isCallExpression(node)) counters.calls += 1;
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (!isSupabaseCall(kit, checker, node)) return;
|
|
70
|
-
const properties = chainParts(ts, node);
|
|
71
|
-
const method = callName(ts, node);
|
|
72
|
-
if (!properties.includes('auth') && !properties.includes('storage')) return;
|
|
73
|
-
counters.calls += 1;
|
|
74
|
-
if (!slots.has(file.slot)) reportAt(violations, kit, ruleId, file, node, `A Supabase ${method ?? 'client'} call is made from ${file.slot ?? 'an unowned path'}; database, auth and storage calls belong only to ${[...slots].join(' or ')}.`, { method, slot: file.slot ?? null });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function inspectOwnershipNode(scope, node) {
|
|
78
|
-
inspectSupabaseImport(scope, node);
|
|
79
|
-
inspectClientTypeReference(scope, node);
|
|
80
|
-
const { ts } = scope.kit;
|
|
81
|
-
if (!ts.isCallExpression(node) && !ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return;
|
|
82
|
-
if (ts.isCallExpression(node)) inspectClientFactoryCall(scope, node);
|
|
83
|
-
inspectProviderCall(scope, node);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function checkClientOwnership(input, { slots, ruleId }) {
|
|
87
|
-
const kit = machineKit(input);
|
|
88
|
-
const violations = [];
|
|
89
|
-
const counters = { imports: 0, clients: 0, calls: 0 };
|
|
90
|
-
for (const file of input.graph.files.values()) {
|
|
91
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
92
|
-
const scope = { kit, checker, file, slots, ruleId, violations, counters, reportedData: new Set() };
|
|
93
|
-
kit.walk(file.sourceFile, node => inspectOwnershipNode(scope, node));
|
|
94
|
-
}
|
|
95
|
-
return { violations, coverage: { status: 'checked', ...counters, ownerSlots: [...slots] } };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const words = text => String(text).replace(/([a-z0-9])([A-Z])/gu, '$1_$2').toUpperCase();
|
|
99
|
-
const serviceRoleText = text => /(?:^|[^A-Z0-9])SERVICE[_ -]?ROLE(?:[^A-Z0-9]|$)/iu.test(words(text));
|
|
100
|
-
const credentialEnv = name => /(?:^|_)(?:SECRET|PRIVATE|TOKEN|PASSWORD)$/u.test(name) || /(?:^|_)SERVICE_ROLE(?:_|$)/u.test(name);
|
|
101
|
-
const CORE_PUBLIC_ENV = new Set(['NEXT_PUBLIC_SUPABASE_URL', 'NEXT_PUBLIC_SUPABASE_ANON_KEY', 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY', 'NEXT_PUBLIC_SITE_URL']);
|
|
102
|
-
|
|
103
|
-
const envRead = (ts, node) => {
|
|
104
|
-
if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return null;
|
|
105
|
-
const object = node.expression;
|
|
106
|
-
const processEnv = ts.isPropertyAccessExpression(object) && ts.isIdentifier(object.expression) && object.expression.text === 'process' && object.name.text === 'env';
|
|
107
|
-
const importMetaEnv = ts.isPropertyAccessExpression(object) && object.name.text === 'env' && object.expression.kind === ts.SyntaxKind.MetaProperty;
|
|
108
|
-
if (!processEnv && !importMetaEnv) return null;
|
|
109
|
-
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
110
|
-
return ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/** Reports a SERVICE_ROLE identifier or string literal: the browser and Next process never hold the service role. */
|
|
114
|
-
function inspectServiceRoleText(ts, node, report) {
|
|
115
|
-
if (ts.isIdentifier(node) && serviceRoleText(node.text)) report(node, 'A SERVICE_ROLE identifier appears in the front end; the browser and Next process never hold the Supabase service role.', node.text);
|
|
116
|
-
if (ts.isStringLiteralLike(node) && serviceRoleText(node.text)) report(node, 'A service_role string appears in the front end; the service role belongs behind the back-end guard chain.', node.text);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Counts an environment read and reports a credential-shaped or product-specific public variable. */
|
|
120
|
-
function inspectEnvironmentRead(state, file, node, name) {
|
|
121
|
-
state.reads += 1;
|
|
122
|
-
if (credentialEnv(name)) state.report(node, `${name} is a credential-shaped environment read in the front end; no *_SECRET, *_PRIVATE, *_TOKEN, *_PASSWORD or service-role value enters fe/.`, name);
|
|
123
|
-
else if (name.startsWith('NEXT_PUBLIC_') && !CORE_PUBLIC_ENV.has(name) && file.slot !== FE_CONFIG_SLOT) {
|
|
124
|
-
state.report(node, `${name} is not a core Supabase/site public variable and is read outside ${FE_CONFIG_SLOT}; product public configuration is declared by the one config owner.`, name);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function checkFrontendSecretNames(input) {
|
|
129
|
-
const kit = machineKit(input);
|
|
130
|
-
const { ts } = kit;
|
|
131
|
-
const violations = [];
|
|
132
|
-
const state = { reads: 0, report: null };
|
|
133
|
-
for (const file of input.graph.files.values()) {
|
|
134
|
-
const seen = new Set();
|
|
135
|
-
state.report = (node, message, name) => {
|
|
136
|
-
const key = `${node.pos}:${name ?? ''}`;
|
|
137
|
-
if (seen.has(key)) return;
|
|
138
|
-
seen.add(key);
|
|
139
|
-
reportAt(violations, kit, FE_SERVICE_ROLE, file, node, message, name ? { name } : {});
|
|
140
|
-
};
|
|
141
|
-
kit.walk(file.sourceFile, node => {
|
|
142
|
-
inspectServiceRoleText(ts, node, state.report);
|
|
143
|
-
const name = envRead(ts, node);
|
|
144
|
-
if (name) inspectEnvironmentRead(state, file, node, name);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
return { violations, coverage: { status: 'checked', envReads: state.reads } };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function inspectFrontendNode(scope, node) {
|
|
151
|
-
const { kit, checker, file, violations, counters, reportedData } = scope;
|
|
152
|
-
const { ts } = kit;
|
|
153
|
-
const moduleName = moduleNameOf(ts, node);
|
|
154
|
-
if (moduleName?.startsWith('@supabase/')) {
|
|
155
|
-
counters.imports += 1;
|
|
156
|
-
if (file.slot !== FE_AUTH_SLOT) reportAt(violations, kit, FE_CLIENT_OWNER, file, node, `${moduleName} is imported by ${file.rel}, whose slot is ${file.slot ?? 'unowned'}; on the front end Supabase is auth only and ${FE_AUTH_SLOT} (modules/auth) is its one importer.`, { module: moduleName, slot: file.slot ?? null });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (!ts.isCallExpression(node) && !ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return;
|
|
160
|
-
const data = supabaseDataAccess(kit, checker, node, FE_DATA_MEMBERS);
|
|
161
|
-
if (data) {
|
|
162
|
-
if (!reportedData.has(data.node.pos)) {
|
|
163
|
-
reportedData.add(data.node.pos);
|
|
164
|
-
reportAt(violations, kit, FE_CLIENT_OWNER, file, data.node, `A Supabase .${data.name} call is made from the front end; Supabase on the front end is auth only - read and write data, files and realtime through the back end (a modules/api GraphQL operation, an SWR hook, hooks/socketio).`, { member: data.name, slot: file.slot ?? null });
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (!isSupabaseCall(kit, checker, node)) return;
|
|
169
|
-
counters.calls += 1;
|
|
170
|
-
const parts = chainParts(ts, node);
|
|
171
|
-
if (file.slot !== FE_AUTH_SLOT && parts.includes('auth')) reportAt(violations, kit, FE_CLIENT_OWNER, file, node, `A Supabase auth call is made from ${file.slot ?? 'an unowned path'}; the auth client is called only inside ${FE_AUTH_SLOT}, which exports the session and the access token.`, { slot: file.slot ?? null });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function checkFrontendClientOwner(input) {
|
|
175
|
-
const kit = machineKit(input);
|
|
176
|
-
const violations = [];
|
|
177
|
-
const counters = { imports: 0, calls: 0 };
|
|
178
|
-
for (const file of input.graph.files.values()) {
|
|
179
|
-
const scope = { kit, checker: kit.checkerOf(file.sourceFile), file, violations, counters, reportedData: new Set() };
|
|
180
|
-
kit.walk(file.sourceFile, node => inspectFrontendNode(scope, node));
|
|
181
|
-
}
|
|
182
|
-
return { violations, coverage: { status: 'checked', ...counters, ownerSlots: [FE_AUTH_SLOT] } };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function checkFrontendSupabase(input) {
|
|
186
|
-
const owner = checkFrontendClientOwner(input);
|
|
187
|
-
if (!supabaseSlotEnabled(input, FE_SUPABASE_PROVIDER_SLOT)) return {
|
|
188
|
-
violations: owner.violations,
|
|
189
|
-
coverage: { status: owner.coverage.imports || owner.coverage.calls || owner.violations.length ? 'checked' : 'not-applicable', owner: owner.coverage, reason: 'no connection declares provider supabase; SDK ownership is still checked' },
|
|
190
|
-
};
|
|
191
|
-
const secrets = checkFrontendSecretNames(input);
|
|
192
|
-
return {
|
|
193
|
-
violations: [...owner.violations, ...secrets.violations],
|
|
194
|
-
coverage: { status: 'checked', owner: owner.coverage, secrets: secrets.coverage },
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function checkBackendSupabase(input) {
|
|
199
|
-
const owner = checkClientOwnership(input, { slots: new Set([BE_SUPABASE_SLOT]), ruleId: BE_CLIENT_OWNER });
|
|
200
|
-
if (!supabaseSlotEnabled(input, BE_SUPABASE_SLOT)) return {
|
|
201
|
-
violations: owner.violations,
|
|
202
|
-
coverage: { status: owner.coverage.imports || owner.coverage.calls || owner.violations.length ? 'checked' : 'not-applicable', owner: owner.coverage, reason: 'the Supabase integration slot is disabled; SDK ownership is still checked' },
|
|
203
|
-
};
|
|
204
|
-
const jwt = checkBackendJwt(input);
|
|
205
|
-
const policy = checkJwtPolicy(input);
|
|
206
|
-
return {
|
|
207
|
-
violations: [...owner.violations, ...jwt.violations, ...policy.violations],
|
|
208
|
-
coverage: { status: 'checked', owner: owner.coverage, jwt: jwt.coverage, jwtPolicy: policy.coverage },
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function checkJwtPolicy(input) {
|
|
213
|
-
const kit = machineKit(input);
|
|
214
|
-
const violations = [];
|
|
215
|
-
let verifyCalls = 0;
|
|
216
|
-
for (const file of input.graph.files.values()) {
|
|
217
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
218
|
-
kit.walk(file.sourceFile, node => {
|
|
219
|
-
if (!kit.ts.isCallExpression(node) || !kit.isImportOf(checker, node.expression, 'jwtVerify', 'jose')) return;
|
|
220
|
-
verifyCalls += 1;
|
|
221
|
-
const problems = jwtPolicyProblems(kit.ts, checker, node);
|
|
222
|
-
if (problems.length) reportAt(violations, kit, 'BE_JWT_ALG_PINNED', file, node, problems.join(' '));
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return { violations, coverage: { status: 'checked', verifyCalls } };
|
|
226
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// supabase-ports.mjs - the local Supabase ports of one lite app, derived from its project name.
|
|
2
|
-
// The Supabase CLI default ports (54321-54329) belong to whichever stack starts first, so a scaffolded app never uses them: it
|
|
3
|
-
// writes its own port block into supabase/config.toml, one block of ten consecutive ports per project name, so two apps (and a
|
|
4
|
-
// proof run beside another stack) never contend. The same name always gets the same block.
|
|
5
|
-
|
|
6
|
-
/** The first port of the block range and how many blocks it holds (41000-44999); clear of the CLI defaults (543xx) and of the Next dev port. */
|
|
7
|
-
const RANGE_START = 41000;
|
|
8
|
-
const BLOCK_SIZE = 10;
|
|
9
|
-
const BLOCK_COUNT = 400;
|
|
10
|
-
/** The offsets inside one block, by config.toml section. */
|
|
11
|
-
const OFFSETS = Object.freeze({ shadow: 0, api: 1, db: 2, studio: 3, inbucket: 4, analytics: 7, pooler: 9 });
|
|
12
|
-
|
|
13
|
-
/** The template variables of the port block: `supabasePort<Name>` for every section above. */
|
|
14
|
-
export const SUPABASE_PORT_VARIABLES = Object.freeze(Object.keys(OFFSETS).map((key) => `supabasePort${key[0].toUpperCase()}${key.slice(1)}`));
|
|
15
|
-
|
|
16
|
-
/** FNV-1a over the project name: stable, dependency-free. */
|
|
17
|
-
const hash = (text) => [...text].reduce((value, char) => Math.imul(value ^ char.charCodeAt(0), 16777619) >>> 0, 2166136261);
|
|
18
|
-
|
|
19
|
-
/** The first port of the project's block. */
|
|
20
|
-
const supabasePortBase = (project) => RANGE_START + (hash(String(project)) % BLOCK_COUNT) * BLOCK_SIZE;
|
|
21
|
-
|
|
22
|
-
/** { supabasePortApi: '41231', ... }: the variables a skeleton file fills. */
|
|
23
|
-
export function supabasePortVars(project) {
|
|
24
|
-
const base = supabasePortBase(project);
|
|
25
|
-
return Object.fromEntries(Object.entries(OFFSETS).map(([key, offset]) => [`supabasePort${key[0].toUpperCase()}${key.slice(1)}`, String(base + offset)]));
|
|
26
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
project_id = "{{project}}"
|
|
2
|
-
|
|
3
|
-
[api]
|
|
4
|
-
enabled = true
|
|
5
|
-
port = {{supabasePortApi}}
|
|
6
|
-
schemas = []
|
|
7
|
-
extra_search_path = ["public", "extensions"]
|
|
8
|
-
max_rows = 1000
|
|
9
|
-
|
|
10
|
-
[db]
|
|
11
|
-
port = {{supabasePortDb}}
|
|
12
|
-
shadow_port = {{supabasePortShadow}}
|
|
13
|
-
major_version = 17
|
|
14
|
-
|
|
15
|
-
[db.pooler]
|
|
16
|
-
enabled = false
|
|
17
|
-
port = {{supabasePortPooler}}
|
|
18
|
-
|
|
19
|
-
[studio]
|
|
20
|
-
port = {{supabasePortStudio}}
|
|
21
|
-
|
|
22
|
-
[inbucket]
|
|
23
|
-
port = {{supabasePortInbucket}}
|
|
24
|
-
|
|
25
|
-
[analytics]
|
|
26
|
-
port = {{supabasePortAnalytics}}
|
|
27
|
-
|
|
28
|
-
[edge_runtime]
|
|
29
|
-
enabled = false
|
|
30
|
-
|
|
31
|
-
[storage]
|
|
32
|
-
enabled = true
|
|
33
|
-
file_size_limit = "50MiB"
|
|
34
|
-
|
|
35
|
-
[auth]
|
|
36
|
-
enabled = true
|
|
37
|
-
site_url = "http://127.0.0.1:3000"
|
|
38
|
-
additional_redirect_urls = ["http://127.0.0.1:3000/"]
|
|
39
|
-
enable_signup = false
|
|
40
|
-
jwt_expiry = 3600
|
|
41
|
-
|
|
42
|
-
[auth.email]
|
|
43
|
-
enable_signup = false
|
|
44
|
-
|
|
45
|
-
[auth.external.google]
|
|
46
|
-
enabled = false
|
|
47
|
-
client_id = "env(SUPABASE_AUTH_GOOGLE_CLIENT_ID)"
|
|
48
|
-
secret = "env(SUPABASE_AUTH_GOOGLE_SECRET)"
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { sql } from "@modules/platform/database"
|
|
2
|
-
|
|
3
|
-
/** Writes only the typed envelope; a reactor or job reads the committed row afterward. */
|
|
4
|
-
export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload, correlation_id)
|
|
5
|
-
VALUES ($1, $2, $3::jsonb, $4)`
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** The JSON envelope makes a completed null value distinct from a pending SQL-null outcome. */
|
|
2
|
-
export interface ClientOutcomeRow<T> {
|
|
3
|
-
/** The operation's complete JSON result, including expected refusals. */
|
|
4
|
-
readonly value: T
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/** The row returned by the atomic client claim. */
|
|
8
|
-
export interface ClaimClientRow<T> {
|
|
9
|
-
/** The digest recorded by the first operation under this scoped key. */
|
|
10
|
-
readonly request_fingerprint: string | null
|
|
11
|
-
/** The saved result, or null only for a claim inserted by this transaction. */
|
|
12
|
-
readonly outcome: ClientOutcomeRow<T> | null
|
|
13
|
-
/** An existing row had no completed outcome, so it must never run an effect again. */
|
|
14
|
-
readonly incomplete: boolean
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** The persisted outcome returned to the first caller. */
|
|
18
|
-
export interface SavedClientOutcomeRow<T> {
|
|
19
|
-
/** The normalized JSON result as the database stores it. */
|
|
20
|
-
readonly outcome: ClientOutcomeRow<T>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** A claimed or deleted delivery key returned by inbox persistence. */
|
|
24
|
-
export interface InboxEventRow {
|
|
25
|
-
/** The event or client key owned by the row. */
|
|
26
|
-
readonly event_id: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The bounded retention batch's aggregate result. */
|
|
30
|
-
export interface InboxSweepRow {
|
|
31
|
-
/** Number of completed client claims removed by this transaction. */
|
|
32
|
-
readonly deleted_count: number
|
|
33
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { sql } from "@modules/platform/database"
|
|
2
|
-
|
|
3
|
-
/** The unique source and delivery id let only one transaction record or process each delivery. */
|
|
4
|
-
export const CLAIM_EVENT = sql`INSERT INTO public.inbox (source, event_id)
|
|
5
|
-
VALUES ($1, $2)
|
|
6
|
-
ON CONFLICT (source, event_id) DO NOTHING
|
|
7
|
-
RETURNING event_id`
|
|
8
|
-
|
|
9
|
-
/** A conflicting insert locks the existing row and returns its committed fingerprint and outcome after the winner commits. */
|
|
10
|
-
export const CLAIM_CLIENT = sql`INSERT INTO public.inbox (source, event_id, request_fingerprint, claimed_at)
|
|
11
|
-
VALUES ($1, $2, $3, $4)
|
|
12
|
-
ON CONFLICT (source, event_id) DO UPDATE SET outcome = COALESCE(inbox.outcome, '{}'::jsonb)
|
|
13
|
-
RETURNING request_fingerprint, outcome, (outcome IS NOT NULL AND (jsonb_typeof(outcome) <> 'object' OR NOT (outcome ? 'value'))) AS incomplete`
|
|
14
|
-
|
|
15
|
-
/** The JSON envelope distinguishes a completed null result from an uncompleted claim in this transaction. */
|
|
16
|
-
export const SAVE_CLIENT_OUTCOME = sql`WITH saved AS (
|
|
17
|
-
UPDATE public.inbox SET outcome = $3::jsonb
|
|
18
|
-
WHERE source = $1 AND event_id = $2
|
|
19
|
-
RETURNING outcome
|
|
20
|
-
)
|
|
21
|
-
SELECT outcome FROM saved LIMIT 1`
|
|
22
|
-
|
|
23
|
-
/** Retention never deletes delivery rows or an incomplete claim; its bounded batch skips rows held by active retries. */
|
|
24
|
-
export const SWEEP_CLIENTS = sql`WITH expired AS (
|
|
25
|
-
SELECT source, event_id FROM public.inbox
|
|
26
|
-
WHERE source LIKE 'client:%' AND request_fingerprint IS NOT NULL AND outcome IS NOT NULL AND jsonb_typeof(outcome) = 'object' AND outcome ? 'value' AND claimed_at < $1
|
|
27
|
-
ORDER BY claimed_at, source, event_id LIMIT $2 FOR UPDATE SKIP LOCKED
|
|
28
|
-
), deleted AS (
|
|
29
|
-
DELETE FROM public.inbox AS claims USING expired
|
|
30
|
-
WHERE claims.source = expired.source AND claims.event_id = expired.event_id
|
|
31
|
-
RETURNING claims.event_id
|
|
32
|
-
)
|
|
33
|
-
SELECT count(*)::int AS deleted_count FROM deleted`
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { sql } from "@modules/platform/database"
|
|
2
|
-
|
|
3
|
-
/** Records one pending @@provider@@ delivery and returns its durable inbox identity only to the winning transaction. */
|
|
4
|
-
export const INSERT_@@providerUpper@@_INBOX = sql`INSERT INTO public.@@table@@ (provider, delivery_id, payload)
|
|
5
|
-
VALUES ($1, $2, $3::jsonb)
|
|
6
|
-
ON CONFLICT (provider, delivery_id) DO NOTHING
|
|
7
|
-
RETURNING id`
|
|
8
|
-
|
|
9
|
-
/** The reactor or job marks a recorded delivery complete in the transaction that applies its effect. Never called by the intake. */
|
|
10
|
-
export const COMPLETE_@@providerUpper@@_INBOX = sql`WITH completed AS (
|
|
11
|
-
UPDATE public.@@table@@
|
|
12
|
-
SET processed_at = now()
|
|
13
|
-
WHERE id = $1
|
|
14
|
-
RETURNING id
|
|
15
|
-
)
|
|
16
|
-
SELECT id FROM completed`
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { sql } from "@modules/platform/database"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Claims a delivery in one statement ($1 kind, $2 job key, $3 worker, $4 lease end, $5 payload, $6 instant). A new key inserts the row
|
|
5
|
-
* with token 1; an existing key is claimed again only when the job failed or its lease expired, and then the token is bumped by the
|
|
6
|
-
* store. A done job and a live claim match nothing, so no row comes back.
|
|
7
|
-
*/
|
|
8
|
-
export const CLAIM_JOB = sql`INSERT INTO public.jobs (kind, job_key, status, fencing_token, claimed_by, lease_expires_at, payload, created_at, updated_at)
|
|
9
|
-
VALUES ($1, $2, 'running', 1, $3, $4, $5::jsonb, $6, $6)
|
|
10
|
-
ON CONFLICT (job_key) DO UPDATE
|
|
11
|
-
SET fencing_token = jobs.fencing_token + 1, claimed_by = $3, lease_expires_at = $4, status = 'running', error = NULL, updated_at = $6
|
|
12
|
-
WHERE jobs.status = 'failed' OR (jobs.status = 'running' AND jobs.lease_expires_at < $6)
|
|
13
|
-
RETURNING id, kind, fencing_token, current_step, payload`
|
|
14
|
-
|
|
15
|
-
/** The guarded writes wrap their UPDATE in a CTE: TypeORM returns a bare `UPDATE ... RETURNING` as `[rows, count]`, a SELECT as rows. */
|
|
16
|
-
|
|
17
|
-
/** Records a step ($1 job id, $2 expected token, $3 step, $4 instant); no row comes back when the token is stale. */
|
|
18
|
-
export const ADVANCE_JOB = sql`WITH changed AS (UPDATE public.jobs SET current_step = $3, updated_at = $4
|
|
19
|
-
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
20
|
-
RETURNING id)
|
|
21
|
-
SELECT id FROM changed`
|
|
22
|
-
|
|
23
|
-
/** Completes the job ($1 job id, $2 expected token, $3 instant). */
|
|
24
|
-
export const COMPLETE_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'done', lease_expires_at = NULL, updated_at = $3
|
|
25
|
-
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
26
|
-
RETURNING id)
|
|
27
|
-
SELECT id FROM changed`
|
|
28
|
-
|
|
29
|
-
/** Fails the job ($1 job id, $2 expected token, $3 reason, $4 instant). */
|
|
30
|
-
export const FAIL_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'failed', error = $3, lease_expires_at = NULL, updated_at = $4
|
|
31
|
-
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
32
|
-
RETURNING id)
|
|
33
|
-
SELECT id FROM changed`
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { sql } from "@modules/platform/database"
|
|
2
|
-
|
|
3
|
-
/** Writes one job in the outbox ($1 queue, $2 payload, $3 instant, $4 correlation id); the relay hands it to BullMQ after the commit. */
|
|
4
|
-
export const INSERT_QUEUE_ROW = sql`INSERT INTO public.queue (queue, payload, created_at, correlation_id)
|
|
5
|
-
VALUES ($1, $2::jsonb, $3, $4)`
|
|
6
|
-
|
|
7
|
-
/** Locks and reads the oldest rows that wait ($1 batch size); a second relay skips the rows this one holds. */
|
|
8
|
-
export const SELECT_WAITING_ROWS = sql`SELECT id, queue, payload, coalesce(correlation_id, id::text) AS "correlationId" FROM public.queue
|
|
9
|
-
WHERE sent_at IS NULL ORDER BY created_at, id LIMIT $1 FOR UPDATE SKIP LOCKED`
|
|
10
|
-
|
|
11
|
-
/** Marks the rows as handed to BullMQ ($1 ids, $2 instant). */
|
|
12
|
-
export const MARK_ROWS_SENT = sql`UPDATE public.queue SET sent_at = $2 WHERE id = ANY($1::uuid[])`
|
package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { DomainError } from "@modules/platform/errors"
|
|
2
|
-
import type { ErrorKind } from "@modules/platform/errors"
|
|
3
|
-
|
|
4
|
-
/** Codes of the Supabase integration. */
|
|
5
|
-
export enum SupabaseErrorCode {
|
|
6
|
-
/** The access token failed signature or required-claim verification. */
|
|
7
|
-
AccessTokenInvalid = "SUPABASE_ACCESS_TOKEN_INVALID",
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** How each Supabase integration code travels. */
|
|
11
|
-
export const SUPABASE_ERROR_KINDS: Record<SupabaseErrorCode, ErrorKind> = {
|
|
12
|
-
[SupabaseErrorCode.AccessTokenInvalid]: "unauthenticated",
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** The one error class of the Supabase integration. */
|
|
16
|
-
export class SupabaseError extends DomainError<SupabaseErrorCode> {}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { createSupabaseAdminClient } from "./supabase.client"
|
|
2
|
-
export { parseSupabaseConfig } from "./supabase.config"
|
|
3
|
-
export { SUPABASE_ACCESS_TOKEN_VERIFIER, SUPABASE_ADMIN_CLIENT } from "./supabase.decorators"
|
|
4
|
-
export { SUPABASE_ERROR_KINDS, SupabaseErrorCode } from "./errors/supabase.error"
|
|
5
|
-
export { createSupabaseAccessTokenVerifier } from "./supabase.jwks"
|
|
6
|
-
export type { SupabaseOptions, VerifySupabaseAccessToken } from "./supabase.options"
|
package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Test } from "@nestjs/testing"
|
|
2
|
-
import { Secret } from "@modules/platform/config"
|
|
3
|
-
import { createSupabaseAdminClient } from "./supabase.client"
|
|
4
|
-
import type { SupabaseAdminClient } from "./supabase.client"
|
|
5
|
-
import { SUPABASE_ADMIN_CLIENT } from "./supabase.decorators"
|
|
6
|
-
|
|
7
|
-
describe("createSupabaseAdminClient", () => {
|
|
8
|
-
it("exposes only the Auth administration and Storage surfaces of a session-less service client", async () => {
|
|
9
|
-
const moduleRef = await Test.createTestingModule({
|
|
10
|
-
providers: [
|
|
11
|
-
{
|
|
12
|
-
provide: SUPABASE_ADMIN_CLIENT,
|
|
13
|
-
useFactory: () =>
|
|
14
|
-
createSupabaseAdminClient({
|
|
15
|
-
url: "http://127.0.0.1:54321",
|
|
16
|
-
serviceRoleKey: new Secret("service-role"),
|
|
17
|
-
}),
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
}).compile()
|
|
21
|
-
const client = moduleRef.get<SupabaseAdminClient>(SUPABASE_ADMIN_CLIENT)
|
|
22
|
-
|
|
23
|
-
expect(Object.keys(client).sort()).toEqual(["auth", "storage"])
|
|
24
|
-
expect(typeof client.auth.createUser).toBe("function")
|
|
25
|
-
expect(typeof client.storage.from).toBe("function")
|
|
26
|
-
})
|
|
27
|
-
})
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createClient } from "@supabase/supabase-js"
|
|
2
|
-
import type { SupabaseClient } from "@supabase/supabase-js"
|
|
3
|
-
import type { Database } from "../../../../../supabase/types/database.types"
|
|
4
|
-
import type { SupabaseOptions } from "./supabase.options"
|
|
5
|
-
|
|
6
|
-
/** The privileged surface exposed by this integration: Auth administration and Storage, never database queries. */
|
|
7
|
-
export interface SupabaseAdminClient {
|
|
8
|
-
/** Supabase Auth administration. */
|
|
9
|
-
readonly auth: SupabaseClient<Database>["auth"]["admin"]
|
|
10
|
-
/** Supabase Storage administration. */
|
|
11
|
-
readonly storage: SupabaseClient<Database>["storage"]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Creates the back-end-only client and narrows it to Auth and Storage administration. */
|
|
15
|
-
export const createSupabaseAdminClient = (options: SupabaseOptions): SupabaseAdminClient => {
|
|
16
|
-
const client: SupabaseClient<Database> = createClient<Database>(options.url, options.serviceRoleKey.reveal(), {
|
|
17
|
-
auth: {
|
|
18
|
-
autoRefreshToken: false,
|
|
19
|
-
detectSessionInUrl: false,
|
|
20
|
-
persistSession: false,
|
|
21
|
-
},
|
|
22
|
-
})
|
|
23
|
-
return { auth: client.auth.admin, storage: client.storage }
|
|
24
|
-
}
|