@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,41 +1,50 @@
|
|
|
1
|
-
//
|
|
2
|
-
// database-sql.mjs;
|
|
1
|
+
// The database rules of a lite app's db/ tree (design 4.2, L02-L09). SQL migrations are parsed with libpg-query through
|
|
2
|
+
// database-sql.mjs; Git history and generated-type checks are split into bounded sibling modules.
|
|
3
3
|
import { found, readJson, readText } from './read.mjs';
|
|
4
|
-
import { configFindings, parseToml, typesFindings } from './database-config.mjs';
|
|
5
4
|
import { defaultGit, migrationShapeFindings } from './database-migrations.mjs';
|
|
6
5
|
import { migrationSetPolicyFindings, sqlAnalysis } from './database-sql.mjs';
|
|
7
6
|
import { migrationSetDefinerFindings } from './database-plpgsql.mjs';
|
|
8
7
|
import { databaseAccessFindings } from './database-access.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { DB_DIR, DB_MIGRATION_SHAPE, isActiveMigration, LITE_DB_MIGRATION, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
|
|
10
9
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
11
10
|
import { eachInOrder } from '../../lib/in-order.mjs';
|
|
12
11
|
|
|
13
12
|
export { migrationStamp } from './database-migrations.mjs';
|
|
14
13
|
export {
|
|
15
|
-
|
|
16
|
-
DB_POLICY_TAUTOLOGY, DB_VIEW_UNSAFE, DB_STORAGE_PREFIX,
|
|
14
|
+
DB_APP_ROLE, DB_FORCE_RLS_DEFAULT, DB_POLICY_TAUTOLOGY, DB_VIEW_UNSAFE, DB_STORAGE_PREFIX,
|
|
17
15
|
} from './database-access.mjs';
|
|
18
16
|
export const DB_DEFINER_REVOKE = 'DB_DEFINER_REVOKE';
|
|
19
17
|
export {
|
|
20
|
-
CONFIG_FILE,
|
|
21
|
-
DB_CONFIG_POLICY,
|
|
22
18
|
DB_DEFINER_SAFE,
|
|
23
19
|
DB_DYNAMIC_DDL,
|
|
24
20
|
DB_MIGRATION_SHAPE,
|
|
25
21
|
DB_POLICY_SHAPE,
|
|
26
22
|
DB_RLS_REQUIRED,
|
|
27
23
|
DB_STORAGE_POLICY,
|
|
28
|
-
|
|
29
|
-
TYPES_FILE,
|
|
24
|
+
LITE_DB_MIGRATION,
|
|
30
25
|
} from './database-constants.mjs';
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
/** The schemas the app's tables live in; no schema is exposed to a browser, which reaches data only through the back end. */
|
|
28
|
+
const APP_SCHEMAS = new Set(['public']);
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
/** A .starcistacks seed is the one SQL file a lite app keeps outside db/. */
|
|
31
|
+
const STACK_SEED = /^\.starcistacks\/[^/]+\/seeds\/.+\.sql$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* LITE_DB_MIGRATION: a lite app's schema lives only under db/: no supabase/ tree and no SQL file outside db/ (but a
|
|
35
|
+
* .starcistacks seed), and every SQL file under db/ is a migration, the archive, the seed or nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function liteDbPathFindings(files) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (file === 'supabase' || file.startsWith('supabase/')) {
|
|
41
|
+
findings.push(found(LITE_DB_MIGRATION, file, `${file} is a Supabase project file; a lite app keeps its schema in ${MIGRATIONS_DIR}/<ts14>_<kebab>.sql (move the migrations, delete supabase/)`, {}));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!file.endsWith('.sql') || STACK_SEED.test(file)) continue;
|
|
45
|
+
if (!file.startsWith(`${DB_DIR}/`)) findings.push(found(LITE_DB_MIGRATION, file, `${file} is SQL outside ${DB_DIR}/; a schema change is one ${MIGRATIONS_DIR}/<ts14>_<kebab>.sql`, {}));
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
async function migrationAnalyses({ repoRoot, migrations, exposed, forceRls, findings }) {
|
|
@@ -54,38 +63,26 @@ async function migrationAnalyses({ repoRoot, migrations, exposed, forceRls, find
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
57
|
-
* Judge the
|
|
58
|
-
*
|
|
59
|
-
* lite apps are judged even before their initial Supabase files have all been scaffolded.
|
|
66
|
+
* Judge the db/ tree of one lite app. Git is injected for deterministic specs; this function starts no
|
|
67
|
+
* database and no Docker process. A full app keeps its TypeORM migrations under be/ and is judged here only for a stray db/ tree.
|
|
60
68
|
*/
|
|
61
|
-
export async function checkDatabase({ repoRoot, files, base, git, edition,
|
|
69
|
+
export async function checkDatabase({ repoRoot, files, base, git, edition, db } = {}) {
|
|
62
70
|
const findings = [];
|
|
71
|
+
const declaration = readJson(repoRoot, 'hfs.json');
|
|
72
|
+
const lite = (edition ?? declaration?.edition) === 'lite';
|
|
63
73
|
const migrations = files.filter(isActiveMigration).sort(byCodeUnit);
|
|
74
|
+
if (!lite && !migrations.length) return findings;
|
|
75
|
+
const declared = db === undefined ? declaration?.db : db;
|
|
64
76
|
const archived = files.filter((file) => file.startsWith(`${MIGRATIONS_ARCHIVE_DIR}/`));
|
|
65
|
-
|
|
66
|
-
const declared = supabase === undefined ? declaration?.supabase : supabase;
|
|
67
|
-
const hasConfig = files.includes(CONFIG_FILE);
|
|
68
|
-
const supabaseConnection = declaresSupabaseConnection(declaration);
|
|
69
|
-
if (!migrations.length && !hasConfig && !files.includes(TYPES_FILE) && !declared && !supabaseConnection && (edition ?? declaration?.edition) !== 'lite') return findings;
|
|
70
|
-
|
|
71
|
-
let config = null;
|
|
72
|
-
if (hasConfig) {
|
|
73
|
-
const result = await configResult(repoRoot, declared);
|
|
74
|
-
config = result.config;
|
|
75
|
-
findings.push(...result.findings);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const apiSchemas = (config?.api?.schemas ?? []).filter((schema) => typeof schema === 'string');
|
|
79
|
-
const exposed = new Set(['public', ...apiSchemas]);
|
|
77
|
+
if (lite) findings.push(...liteDbPathFindings(files));
|
|
80
78
|
const forceRls = new Set((declared?.forceRls ?? []).map((name) => (name.includes('.') ? name : `public.${name}`)));
|
|
81
79
|
findings.push(...await migrationShapeFindings({ repoRoot, migrations, archived, git: git ?? defaultGit, base }));
|
|
82
80
|
|
|
83
|
-
const analyses = await migrationAnalyses({ repoRoot, migrations, exposed, forceRls, findings });
|
|
81
|
+
const analyses = await migrationAnalyses({ repoRoot, migrations, exposed: APP_SCHEMAS, forceRls, findings });
|
|
84
82
|
findings.push(
|
|
85
83
|
...migrationSetPolicyFindings(analyses),
|
|
86
|
-
...migrationSetDefinerFindings(analyses,
|
|
87
|
-
...await databaseAccessFindings({ analyses, exposed,
|
|
88
|
-
...await typesFindings({ repoRoot, files, emitTypes, declaration: declared }),
|
|
84
|
+
...migrationSetDefinerFindings(analyses, APP_SCHEMAS),
|
|
85
|
+
...await databaseAccessFindings({ analyses, exposed: APP_SCHEMAS, db: declared }),
|
|
89
86
|
);
|
|
90
87
|
return findings;
|
|
91
88
|
}
|
|
@@ -75,7 +75,7 @@ function scriptFindings(rel, pkg) {
|
|
|
75
75
|
const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {};
|
|
76
76
|
return Object.keys(scripts)
|
|
77
77
|
.filter((name) => LITE_FORBIDDEN_SCRIPT.test(name))
|
|
78
|
-
.map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local
|
|
78
|
+
.map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local db service and optional contract specs, never an e2e, stack or browser run`, { name }));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** The backing services of the manifest (ruleParams.be.backing): name -> {clients, patterns, images, editions?}. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// lite-secrets.mjs - L17 extends the one R06 secret scan for edition lite. It keeps the ordinary
|
|
2
2
|
// HFS_PLAINTEXT_SECRET judgement, then adds the reduced lite custody tree and provider-shaped literals
|
|
3
|
-
//
|
|
3
|
+
// in the trees that hold no secret at all (fe/ and db/). Findings name shapes, never captured values.
|
|
4
4
|
import { Buffer } from 'node:buffer';
|
|
5
5
|
import { secretFindings } from './secrets.mjs';
|
|
6
6
|
import { found, readText } from './read.mjs';
|
|
@@ -40,11 +40,11 @@ const validJwt = candidate => {
|
|
|
40
40
|
|
|
41
41
|
const liteSecretPatterns = line => {
|
|
42
42
|
const names = LITE_SECRET_PATTERNS.filter(pattern => pattern.re.test(line)).map(pattern => pattern.name);
|
|
43
|
-
if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('
|
|
43
|
+
if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('signed-jwt');
|
|
44
44
|
return names;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('
|
|
47
|
+
const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('db/');
|
|
48
48
|
|
|
49
49
|
function custodyPathFinding(file, repo, findings) {
|
|
50
50
|
if (repo.profile === 'app') {
|
|
@@ -71,7 +71,7 @@ function providerSecretFindings(repoRoot, file, repo, findings) {
|
|
|
71
71
|
for (const pattern of liteSecretPatterns(line)) {
|
|
72
72
|
if (seen.has(pattern)) continue;
|
|
73
73
|
seen.add(pattern);
|
|
74
|
-
findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or
|
|
74
|
+
findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or db/.`, { line: index + 1, pattern }));
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -90,7 +90,7 @@ export function liteSecretCustodyFindings({ repoRoot, files, repo }) {
|
|
|
90
90
|
* Drop-in extension of secretFindings for scripts/hfs/check.mjs. When an L17 finding identifies the same file/line as
|
|
91
91
|
* the generic R06 scanner (a complete JWT, for example), the specific lite code wins so one physical issue is emitted once.
|
|
92
92
|
*/
|
|
93
|
-
export function
|
|
93
|
+
export function liteSecretFindings(input) {
|
|
94
94
|
const ordinary = secretFindings(input);
|
|
95
95
|
const lite = liteSecretCustodyFindings(input);
|
|
96
96
|
const liteJwtAuthority = (input.repo.edition ?? 'full') === 'lite';
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
// R252: inspect executable source for plaintext loads
|
|
1
|
+
// R252: inspect executable source for plaintext loads and tracing.
|
|
2
2
|
// Only source is read. Secret files and sealed envelopes are excluded before any read,
|
|
3
3
|
// and findings carry a path and a fixed explanation, never source text or values.
|
|
4
4
|
import { found, readJson, readText } from './read.mjs';
|
|
5
5
|
|
|
6
6
|
const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
|
|
7
7
|
const XTRACE = 'HFS_SCRIPT_XTRACE';
|
|
8
|
-
const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
|
|
9
|
-
const SUPABASE_OWNER = 'be.integrations.supabase';
|
|
10
8
|
const SECRET_ENV_FILE = /(?:^|\/)(?:secrets?\.env|\.env)(?:\.[^/]*)?/iu;
|
|
11
9
|
const SECRET_NIVO_FILE = /(?:^|\/)\.nivo-lite(?:\/|$)/iu;
|
|
12
10
|
const SECRET_ENC_FILE = /\.enc$/iu;
|
|
@@ -36,7 +34,6 @@ const isLoad = (line) => LOAD_PATTERNS.some((pattern) => pattern.test(line));
|
|
|
36
34
|
const READ_CALL = /\b(?:readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|load_dotenv|loadEnvFile|dotenv\s*\.\s*(?:config|parse))\s*\(/giu;
|
|
37
35
|
const TRACE_COMMAND = /(?:^|[\s;&|('"`]|#!)(?:set|\/?(?:[\w.-]+\/)*(?:bash|sh|dash|zsh|ksh))\s+/giu;
|
|
38
36
|
const SHELL_OPTIONS = /\bSHELLOPTS\s*=\s*['"]?([^\s;'"]*)/giu;
|
|
39
|
-
const SERVICE_ROLE_NAME = /(?:^|[^a-z0-9])(?:[a-z0-9]+[_-])*service[_-]?role(?:[_-][a-z0-9]+)*(?=$|[^a-z0-9])/iu;
|
|
40
37
|
|
|
41
38
|
/** Retain executable strings, including remote shell commands, but discard comment-only lines. */
|
|
42
39
|
function executableText(text) {
|
|
@@ -130,11 +127,6 @@ function enablesTracing(text) {
|
|
|
130
127
|
return false;
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
function integrationOwns(file, resolver) {
|
|
134
|
-
if (!file.startsWith('be/src/modules/integrations/supabase/')) return false;
|
|
135
|
-
return !resolver?.ownerOf || resolver.ownerOf(file)?.slot === SUPABASE_OWNER;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
130
|
function isSource(file) {
|
|
139
131
|
if (isSecretFile(file) || EXCLUDED_TREE.test(file) || TEST_FILE.test(file)) return false;
|
|
140
132
|
return SOURCE.test(file) || WORKFLOW.test(file) || DOCKER.test(file)
|
|
@@ -161,7 +153,6 @@ export function scriptHygieneFindings({ repoRoot, files = [], all = files, repo,
|
|
|
161
153
|
const text = executableText(raw);
|
|
162
154
|
if (plaintextLoad(text)) findings.push(found(PLAINTEXT_LOAD, file, 'Executable source loads a plaintext secret file; use starci app secret with sealed envelopes.'));
|
|
163
155
|
if (enablesTracing(text)) findings.push(found(XTRACE, file, 'Executable source enables shell tracing or secret reveal; remove xtrace and --reveal.'));
|
|
164
|
-
if (SERVICE_ROLE_NAME.test(text.replace(/([a-z0-9])([A-Z])/gu, '$1_$2')) && !integrationOwns(file, resolver)) findings.push(found(SERVICE_ROLE, file, 'Service-role access belongs only to be/src/modules/integrations/supabase/ and its configuration.'));
|
|
165
156
|
}
|
|
166
157
|
return findings;
|
|
167
158
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// R276: lite
|
|
1
|
+
// R276: lite runs exactly its own PostgreSQL (the db component, image supabase/postgres) with pgvector, admits only the backing services available to its edition,
|
|
2
2
|
// and runs a declared Redis as one service with AOF fsynced at least every second on a named volume and no eviction
|
|
3
3
|
// (ruling:R212/case-10, owner:lite-vector-is-pgvector); sidecar SQLite persists on declared named volumes.
|
|
4
4
|
import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
|
|
5
5
|
import { composeEntries } from '../../lib/compose-values.mjs';
|
|
6
6
|
import { repositoryOf } from '../../lib/stack-services.mjs';
|
|
7
|
+
import { isLiteDatabaseImage } from '../../lib/swarm-stack.mjs';
|
|
7
8
|
import { loadSlotManifest } from '../slots.mjs';
|
|
8
9
|
import { judgedInEdition } from '../edition-slots.mjs';
|
|
9
10
|
import { readJson } from './read.mjs';
|
|
@@ -43,12 +44,12 @@ function imageIssues(components, services, { catalog, declared }) {
|
|
|
43
44
|
for (const [id, entry] of Object.entries(inventory)) {
|
|
44
45
|
if (!present(entry?.image)) continue;
|
|
45
46
|
const service = serviceOfImage(catalog, entry.image);
|
|
46
|
-
if (DATABASE_IMAGE.test(repositoryOf(entry.image)))
|
|
47
|
+
if (DATABASE_IMAGE.test(repositoryOf(entry.image)) && !isLiteDatabaseImage(entry.image))
|
|
47
48
|
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
48
|
-
message: 'lite
|
|
49
|
+
message: 'lite runs one database, the db component of image supabase/postgres; no other Postgres or Mongo image' });
|
|
49
50
|
else if (service !== null && !judgedInEdition(catalog[service], 'lite'))
|
|
50
51
|
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
51
|
-
message: `lite must not run ${service}; vector search uses
|
|
52
|
+
message: `lite must not run ${service}; vector search uses pgvector in the db service (owner:lite-vector-is-pgvector)` });
|
|
52
53
|
else if (service !== null && !declared.has(service))
|
|
53
54
|
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
54
55
|
message: `lite runs ${service} only when hfs.json sides.be.backing declares it for the capability that needs it (ruling:R212/case-10)` });
|
|
@@ -81,7 +81,8 @@ function stateful(component, service) {
|
|
|
81
81
|
function recoveryComponents({ doc, spec, model }) {
|
|
82
82
|
const components = mapping(doc.components), services = mapping(model?.services);
|
|
83
83
|
const selected = new Set([...Object.keys(mapping(spec.components)), ...Object.keys(services)]);
|
|
84
|
-
|
|
84
|
+
// Every declared backing (the lite db: backing.db naming its component) owes a backup, as every stateful component does.
|
|
85
|
+
const required = new Set(Object.entries(mapping(doc.backing)).filter(([, entry]) => typeof entry?.provider === 'string').map(([id]) => id));
|
|
85
86
|
for (const id of selected) if (stateful(mapping(components[id]), mapping(services[id]))) required.add(id);
|
|
86
87
|
return [...required];
|
|
87
88
|
}
|
|
@@ -290,7 +291,7 @@ function sqlTemplateText(node) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
function recurringObservations({ doc, repoRoot, files, file, issues }) {
|
|
293
|
-
const sql = file.startsWith('
|
|
294
|
+
const sql = file.startsWith('db/migrations/') && file.endsWith('.sql') && !file.startsWith('db/migrations/archive/');
|
|
294
295
|
const sourceFile = SOURCE.test(file) && !TEST.test(file);
|
|
295
296
|
if ((!sql && !sourceFile) || !publicReference(repoRoot, files, file)) return [];
|
|
296
297
|
const text = readText(repoRoot, file);
|
|
@@ -92,8 +92,8 @@ function swarmDeploymentFiles(repoRoot, files) {
|
|
|
92
92
|
function liteEnvironmentFindings({ doc, environments, deployments }) {
|
|
93
93
|
const findings = [];
|
|
94
94
|
const delivery = deployments.length || Object.values(environments).some(spec => Array.isArray(spec?.composeFiles) && spec.composeFiles.length);
|
|
95
|
-
if (delivery && (doc.backing?.
|
|
96
|
-
findings.push(found(CODE, DECLARATION, `${DECLARATION}: lite Swarm delivery must declare backing.
|
|
95
|
+
if (delivery && (doc.backing?.db?.provider !== 'postgres' || doc.backing?.db?.component !== 'db'))
|
|
96
|
+
findings.push(found(CODE, DECLARATION, `${DECLARATION}: lite Swarm delivery must declare backing.db with provider: postgres and component: db (its db service)`));
|
|
97
97
|
for (const [environment, spec] of Object.entries(environments)) {
|
|
98
98
|
if (!Array.isArray(spec?.composeFiles) || !spec.composeFiles.length) continue;
|
|
99
99
|
if (spec.runtime !== 'docker-swarm' || spec.composeFiles.some(file => typeof file !== 'string' || !file.startsWith('infra/swarm/')))
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// `deployment.json` and `k8s/` are not in the list, so they fall out of it;
|
|
6
6
|
// - the declaration (read by scripts/lib/stack-declaration.mjs, the one reader of it) states a sonar service, a local
|
|
7
7
|
// Sonar is owned by the host (`stack.owner: host`, root `.claude/ext/sonar`).
|
|
8
|
-
// The lite overlay admits Swarm compute and
|
|
8
|
+
// The lite overlay admits Swarm compute and its own db service. Runtime rules are shared with the stack gates.
|
|
9
9
|
// The declaration's quality/delivery services contract (custody, CI wiring, project keys) stays check-starcistacks's.
|
|
10
10
|
import { braceVariants, globExpression } from '../../lib/glob.mjs';
|
|
11
11
|
import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../../lib/stack-declaration.mjs';
|
|
@@ -49,7 +49,7 @@ function sideCrossing(repo, fromSide, toSide, toPath) {
|
|
|
49
49
|
function importAllowedApp({ repo, root, sides }, fromPath, toPath) {
|
|
50
50
|
const from = splitSide(fromPath);
|
|
51
51
|
const to = splitSide(toPath);
|
|
52
|
-
// A side file may read an app-root path (
|
|
52
|
+
// A side file may read an app-root path (db/types/) only through a declared read, exactly as it reads the other side.
|
|
53
53
|
if (from && from.side !== to?.side) return sideCrossing(repo, from.side, to?.side ?? null, toPath);
|
|
54
54
|
if (from && to) return sides[from.side].importAllowed(from.rest, to.rest);
|
|
55
55
|
return root.importAllowed(fromPath, toPath);
|
|
@@ -120,6 +120,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
|
|
|
120
120
|
connections: Object.freeze((s.connections ?? []).map(connectionView)),
|
|
121
121
|
providers,
|
|
122
122
|
reads: Object.freeze([...(s.reads ?? [])]),
|
|
123
|
+
dbFunctions: Object.freeze((s.dbFunctions ?? []).map((entry) => Object.freeze({ ...entry }))),
|
|
123
124
|
...(s.grammar === undefined ? {} : { grammar: s.grammar }),
|
|
124
125
|
...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map(entry => Object.freeze({ ...entry }))) }),
|
|
125
126
|
manifestVersion: manifest.version,
|
|
@@ -221,7 +222,8 @@ function createScopeResolver(manifest, repo) {
|
|
|
221
222
|
trackingOf,
|
|
222
223
|
/** True only for a path a slot owns and requires to be committed. */
|
|
223
224
|
isTracked: (p) => trackingOf(p) === 'tracked',
|
|
224
|
-
|
|
225
|
+
// The app's own database functions (hfs.json sides.be.dbFunctions) join the canon's list (R334).
|
|
226
|
+
ruleParams: () => withDeclaredDbFunctions(ruleParams(manifest, profile, edition), repo.dbFunctions),
|
|
225
227
|
allowedImports: (tier) => manifest.tiers[profile]?.[tier]?.mayImport ?? null,
|
|
226
228
|
});
|
|
227
229
|
}
|
|
@@ -235,13 +237,17 @@ function createScopeResolver(manifest, repo) {
|
|
|
235
237
|
*/
|
|
236
238
|
export function createSlotResolver(manifest, repo) {
|
|
237
239
|
if (repo.profile !== APP_SCOPE) return createScopeResolver(manifest, repo);
|
|
238
|
-
// The root declares no connection of its own, yet a provider slot of the app profile
|
|
240
|
+
// The root declares no connection of its own, yet a provider slot of the app profile is enabled by a
|
|
239
241
|
// side's connection with that provider: the root scope reads the union of the sides' connections.
|
|
240
242
|
const root = createScopeResolver(manifest, { ...repo, connections: SIDES.flatMap((side) => repo.sides[side].connections) });
|
|
241
243
|
const sides = Object.fromEntries(PROFILES.map((side) => [side, createScopeResolver(manifest, repo.sides[side])]));
|
|
242
244
|
return appResolver(repo, root, sides);
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
/** The rule parameters with the app's declared database functions appended to `dbFunctions` (be only), frozen. */
|
|
248
|
+
const withDeclaredDbFunctions = (params, declared = []) => (params.dbFunctions === undefined && !declared.length ? params
|
|
249
|
+
: Object.freeze({ ...params, dbFunctions: Object.freeze([...(params.dbFunctions ?? []), ...declared]) }));
|
|
250
|
+
|
|
245
251
|
/** The rule parameters of one profile (be: infraOwners, suffixes, bannedSuffixes and the rest over the shared ruleParams.common fileLines and duplicateBlock; fe: common alone or with its overrides; runtime: ruleParams.runtime of a runtime manifest), as a frozen deep copy. Under `edition` lite the `lite` overrides of ruleParams.<profile> merge over the base (the `lite` key itself is never returned). */
|
|
246
252
|
export function ruleParams(manifest, profile, edition = 'full') {
|
|
247
253
|
const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// pg-parse.mjs - the one SQL parse entry of the hfs database rules (scripts/hfs/rules/database.mjs): the WASM build
|
|
2
2
|
// of libpg-query, the real PostgreSQL parser compiled to WebAssembly (no native build, no postgres server). It is
|
|
3
|
-
// loaded lazily: a check of an app with no
|
|
3
|
+
// loaded lazily: a check of an app with no db/ tree never pays the module init. parseSql gives the statement
|
|
4
4
|
// tree of a migration (`{version, stmts}`, each stmt `{stmt, stmt_location, stmt_len}` with byte offsets); a
|
|
5
5
|
// plpgsql body (a DO block's or a function's `as` source) is judged through parsePlpgsqlBody, which wraps it in a
|
|
6
6
|
// probe CREATE FUNCTION because libpg-query parses plpgsql only in that form. A parse failure throws the
|
|
@@ -10,7 +10,7 @@ let loading = null;
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The libpg-query module (wasm build), initialized once per process. A bare dynamic import so the package can be
|
|
13
|
-
* absent until a
|
|
13
|
+
* absent until a db/ tree exists to judge; the caller reports the load failure, not this module.
|
|
14
14
|
*/
|
|
15
15
|
function loadPgParser() {
|
|
16
16
|
loading ??= (async () => {
|
|
@@ -100,7 +100,7 @@ const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Z
|
|
|
100
100
|
const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
|
|
101
101
|
|
|
102
102
|
/** True when a bare variable `name` holds secret material as an identifier segment (DB_PASSWORD, dbPassword,
|
|
103
|
-
*
|
|
103
|
+
* STORAGE_SECRET_ACCESS_KEY) and does not end in a config suffix (PASSWORD_MIN_LENGTH, SECRET_FILE,
|
|
104
104
|
* tokenBudget). The same name judgement the `assigned-secret` patterns make, for callers that read keys. */
|
|
105
105
|
export const secretNameLike = (name) => SECRET_NAME_TEST.test(name) && !BENIGN_TAIL_TEST.test(name);
|
|
106
106
|
|
|
@@ -117,7 +117,7 @@ export const SECRET_PATTERNS = [
|
|
|
117
117
|
{ name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
118
118
|
{ name: 'age-secret-key', re: /\bAGE-SECRET-KEY-1[0-9A-Z]{20,}\b/ },
|
|
119
119
|
{ name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
|
|
120
|
-
// A value written `env(NAME)` (
|
|
120
|
+
// A value written `env(NAME)` (a config reference form) names a variable, not a secret.
|
|
121
121
|
// A keyword-assigned value that names itself a stand-in (a test stub's `accessToken: "fixture-..."`) is no
|
|
122
122
|
// candidate; only this heuristic takes the exemption, never a provider-shaped token above.
|
|
123
123
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
@@ -10,6 +10,9 @@ const BACKING_IMAGE_SQL_AND_CACHE = 'postgres(?:ql)?|postgresql-repmgr|mysql(?:-
|
|
|
10
10
|
const BACKING_IMAGE_BROKERS_AND_OTHERS = '(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq';
|
|
11
11
|
const BACKING_IMAGE = new RegExp(`^(?:${BACKING_IMAGE_SQL_AND_CACHE}|${BACKING_IMAGE_BROKERS_AND_OTHERS})$`, 'i');
|
|
12
12
|
const mapping = value => plain(value) ? value : {};
|
|
13
|
+
|
|
14
|
+
/** True for the one database image a lite app runs (its db component): supabase/postgres, pinned by digest. */
|
|
15
|
+
export const isLiteDatabaseImage = image => typeof image === 'string' && /^supabase\/postgres(?::[^@]+)?@sha256:[a-f0-9]{64}$/i.test(image);
|
|
13
16
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
14
17
|
const configured = value => Object.keys(mapping(value)).length > 0;
|
|
15
18
|
const schedulerImage = image => present(image) && /(?:^|\/)swarm-cronjob(?:[:@]|$)/.test(image);
|
|
@@ -49,8 +52,8 @@ export function swarmInventoryIssues({ components, lite = false }) {
|
|
|
49
52
|
const add = (at, message) => errors.push({ at, message });
|
|
50
53
|
for (const [id, entry] of Object.entries(catalog)) {
|
|
51
54
|
const component = mapping(entry);
|
|
52
|
-
if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
|
|
53
|
-
add(`components.${id}.image`, 'lite
|
|
55
|
+
if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)) && !isLiteDatabaseImage(component.image))
|
|
56
|
+
add(`components.${id}.image`, 'lite runs no backing image but its db component (supabase/postgres); storage is the external S3 endpoint, identity the API');
|
|
54
57
|
jobDeclarationIssues(id, component, catalog, add);
|
|
55
58
|
}
|
|
56
59
|
return errors;
|
|
@@ -117,8 +120,8 @@ export function swarmModelIssues({ model, components, bindings, lite = false })
|
|
|
117
120
|
add(`services.${id}.ports`, 'Swarm ports must be an array of published port definitions');
|
|
118
121
|
else if (service.ports?.length && component.role !== 'ingress')
|
|
119
122
|
add(`services.${id}.ports`, 'only a component with role ingress may publish Swarm ports');
|
|
120
|
-
if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)))
|
|
121
|
-
add(`services.${id}.image`, 'lite stack
|
|
123
|
+
if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)) && !isLiteDatabaseImage(service.image))
|
|
124
|
+
add(`services.${id}.image`, 'lite stack runs no backing image but its db service (supabase/postgres)');
|
|
122
125
|
jobServiceIssues(id, service, component, services, bound, add);
|
|
123
126
|
}
|
|
124
127
|
for (const id of Object.keys(bound)) {
|
package/scaffold/add-app.mjs
CHANGED
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
} from "../runtime/scripts/hfs/slots.mjs";
|
|
8
8
|
import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
9
9
|
import { ScaffoldError, pascalOf } from "./service.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import { PARTIAL_VARIABLES, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
|
|
10
|
+
import { PARTIAL_VARIABLES, coreFeApps, jsonText, packageJsonText } from "./app.mjs";
|
|
12
11
|
|
|
13
12
|
/** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
|
|
14
13
|
const CORE_DIRS = Object.freeze({
|
|
@@ -66,8 +65,7 @@ function planAppTemplates(repo, name) {
|
|
|
66
65
|
if (!fs.existsSync(dir)) continue;
|
|
67
66
|
for (const relative of listFiles(dir)) byPath.set(relative, { relative: `${targetBase}/${relative}`, body: readTemplate(dir, relative) });
|
|
68
67
|
}
|
|
69
|
-
return
|
|
70
|
-
.map(file => ({ relative: file.path, body: file.content }));
|
|
68
|
+
return [...byPath.values()];
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
/** The package and TypeScript manifests of a new Next workspace. */
|
|
@@ -85,12 +83,6 @@ function planWorkspaceManifests(root, repo, name) {
|
|
|
85
83
|
);
|
|
86
84
|
const workspacePackage = JSON.parse(fs.readFileSync(firstPackage, "utf8"));
|
|
87
85
|
workspacePackage.name = `@${repo.project}/${name}`;
|
|
88
|
-
if (hasAuthCallback(repo)) {
|
|
89
|
-
const pins = bundledPins();
|
|
90
|
-
workspacePackage.dependencies ??= {};
|
|
91
|
-
for (const dependency of ["@supabase/ssr", "@supabase/supabase-js", "server-only"])
|
|
92
|
-
workspacePackage.dependencies[dependency] = pins[dependency].version;
|
|
93
|
-
}
|
|
94
86
|
const appTemplates = path.join(TEMPLATES_DIR, "fe", "app");
|
|
95
87
|
return [
|
|
96
88
|
{
|
|
@@ -21,7 +21,7 @@ const managedScripts = (repo, source) => {
|
|
|
21
21
|
|
|
22
22
|
const CLI_OPTIONS = `import type { DatabaseConnectionOptions } from "@modules/platform/database"
|
|
23
23
|
|
|
24
|
-
/** Everything the lite cli needs: the least-privilege
|
|
24
|
+
/** Everything the lite cli needs: the least-privilege PostgreSQL connection. */
|
|
25
25
|
export interface CliAppOptions {
|
|
26
26
|
/** The connections the built-in migrate and seed groups operate on. */
|
|
27
27
|
readonly connections: ReadonlyArray<DatabaseConnectionOptions>
|
|
@@ -36,8 +36,8 @@ export const createCliAppOptions = (primary: DatabaseConnectionOptions): CliAppO
|
|
|
36
36
|
const MIGRATE_RUN = `import { CommandRunner, SubCommand } from "nest-commander"
|
|
37
37
|
import { MigrationRunnerService } from "@modules/platform/database"
|
|
38
38
|
|
|
39
|
-
@SubCommand({ name: "run", description: "
|
|
40
|
-
/** \`cli migrate run\`: delegates schema authority to the managed
|
|
39
|
+
@SubCommand({ name: "run", description: "Apply the pending db migrations" })
|
|
40
|
+
/** \`cli migrate run\`: delegates schema authority to the managed db:push script (starci app db migrate). */
|
|
41
41
|
export class RunCli extends CommandRunner {
|
|
42
42
|
constructor(private readonly migrations: MigrationRunnerService) {
|
|
43
43
|
super()
|
|
@@ -53,7 +53,7 @@ export class RunCli extends CommandRunner {
|
|
|
53
53
|
const SEED_RUN = `import { CommandRunner, SubCommand } from "nest-commander"
|
|
54
54
|
import { SeedRunnerService } from "@modules/platform/database"
|
|
55
55
|
|
|
56
|
-
@SubCommand({ name: "run", description: "Run the tracked
|
|
56
|
+
@SubCommand({ name: "run", description: "Run the tracked seed through the application role" })
|
|
57
57
|
/** \`cli seed run\`: executes the one tracked seed through the shared least-privilege EntityManager. */
|
|
58
58
|
export class RunSeedsCli extends CommandRunner {
|
|
59
59
|
constructor(private readonly seeds: SeedRunnerService) {
|
|
@@ -91,7 +91,7 @@ import { SeedCli } from "./seed.cli"
|
|
|
91
91
|
import { RunSeedsCli } from "./subs/run.cli"
|
|
92
92
|
|
|
93
93
|
@Module({ providers: [SeedCli, RunSeedsCli] })
|
|
94
|
-
/** The lite seed group: the command and its one tracked
|
|
94
|
+
/** The lite seed group: the command and its one tracked seed runner. */
|
|
95
95
|
export class SeedModule {}
|
|
96
96
|
`;
|
|
97
97
|
|
|
@@ -144,7 +144,7 @@ function enableLiteCliDatabase(root) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Creates the optional lite cli app, its typed CliEnv/readCliEnv boundary and
|
|
147
|
+
* Creates the optional lite cli app, its typed CliEnv/readCliEnv boundary and its migrate/seed groups on first `add cli`.
|
|
148
148
|
* Existing cli trees are never repaired or overwritten: drift remains a check finding.
|
|
149
149
|
*/
|
|
150
150
|
export function ensureLiteCli({ root, manifest, repo }) {
|
package/scaffold/add-compose.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export function readComposeGroup({ repoRoot, repo, app, area, noun }) {
|
|
|
36
36
|
const skeleton = repo.edition === 'lite' ? 'skeleton-lite/apps/api' : 'skeleton/apps/__app__';
|
|
37
37
|
const template = path.join(TEMPLATES_DIR, 'be', ...skeleton.split('/'), 'src', 'compose', `${area}.ts`);
|
|
38
38
|
const text = fs.readFileSync(exists ? file : template, 'utf8').replace(/\r\n/g, '\n');
|
|
39
|
-
const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken:
|
|
39
|
+
const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken: VerifyAccessToken' : '';
|
|
40
40
|
const marker = `export const ${area}Modules = (${argument}): Array<Type<unknown> | DynamicModule> => [`;
|
|
41
41
|
const declarationAt = compositionCode(text).indexOf(`\n${marker}`);
|
|
42
42
|
const active = declarationAt >= 0;
|
|
@@ -72,8 +72,8 @@ const databasePatches = (root) => {
|
|
|
72
72
|
"lite add webhook needs the scaffolded platform/database capability",
|
|
73
73
|
);
|
|
74
74
|
let index = fs.readFileSync(indexFile, "utf8").replace(/\r\n/g, "\n");
|
|
75
|
-
if (!index.includes('export { recordWebhookDelivery } from "./webhook-inbox.client"'))
|
|
76
|
-
index = `${index.trimEnd()}\nexport { recordWebhookDelivery } from "./webhook-inbox.client"\n`;
|
|
75
|
+
if (!index.includes('export { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"'))
|
|
76
|
+
index = `${index.trimEnd()}\nexport { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"\n`;
|
|
77
77
|
if (!index.split("\n").some((line) => line.startsWith("export { ") && line.includes("DatabaseErrorCode") && line.endsWith(' } from "./errors/database.error"')))
|
|
78
78
|
index = `${index.trimEnd()}\nexport { DatabaseError, DatabaseErrorCode } from "./errors/database.error"\n`;
|
|
79
79
|
let error = withSqlIdentifierError(fs.readFileSync(errorFile, "utf8").replace(/\r\n/g, "\n"));
|
|
@@ -169,7 +169,7 @@ export function addLiteWebhookInbox({
|
|
|
169
169
|
validateProcessingOwner({ root, ...values, provider });
|
|
170
170
|
const Provider = pascalOf(provider);
|
|
171
171
|
const table = `${snakeOf(provider)}_inbox`;
|
|
172
|
-
const migrationsDir = path.join(root, "
|
|
172
|
+
const migrationsDir = path.join(root, "db", "migrations");
|
|
173
173
|
const migrations = fs.existsSync(migrationsDir)
|
|
174
174
|
? fs.readdirSync(migrationsDir).filter((file) => MIGRATION.test(file)).sort(byCodeUnit)
|
|
175
175
|
: [];
|
|
@@ -197,7 +197,7 @@ export function addLiteWebhookInbox({
|
|
|
197
197
|
const events = eventFiles(root, provider, renderValues);
|
|
198
198
|
const planned = [
|
|
199
199
|
{
|
|
200
|
-
relative: `
|
|
200
|
+
relative: `db/migrations/${stamp}_${suffix}`,
|
|
201
201
|
text: template("migration.sql.tpl", renderValues),
|
|
202
202
|
},
|
|
203
203
|
{ relative: `${base}/index.ts`, text: template("index.ts.tpl", renderValues) },
|
|
@@ -221,10 +221,6 @@ export function addLiteWebhookInbox({
|
|
|
221
221
|
relative: `${base}/${provider}-inbox.service.spec.ts`,
|
|
222
222
|
text: template("service.spec.ts.tpl", renderValues),
|
|
223
223
|
},
|
|
224
|
-
{
|
|
225
|
-
relative: `${base}/persistence/${provider}-inbox.sql.ts`,
|
|
226
|
-
text: template("sql.ts.tpl", renderValues),
|
|
227
|
-
},
|
|
228
224
|
{
|
|
229
225
|
relative: `be/src/modules/events/${provider}/${provider}-delivery-recorded.event.ts`,
|
|
230
226
|
text: template("delivery-recorded.event.ts.tpl", renderValues),
|
package/scaffold/add-plan.mjs
CHANGED
|
@@ -162,7 +162,7 @@ function appendEntries(entries, context, planned) {
|
|
|
162
162
|
|
|
163
163
|
/** Planned app migrations append after every active migration, before any generated file is written. */
|
|
164
164
|
function requireMigrationOrder(repoRoot, planned) {
|
|
165
|
-
const directory = path.join(repoRoot, '
|
|
165
|
+
const directory = path.join(repoRoot, 'db', 'migrations');
|
|
166
166
|
const migrations = planned.filter(file => path.dirname(file.absolute) === directory);
|
|
167
167
|
if (migrations.length === 0) return;
|
|
168
168
|
const existing = fs.existsSync(directory) ? fs.readdirSync(directory) : [];
|