@starci/hfs 4.9.9 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -8
- package/db/backup-receipt.mjs +10 -47
- package/db/drift.mjs +289 -0
- package/db/dump.mjs +12 -11
- package/db/entity-list.mjs +9 -0
- package/db/migrate.mjs +84 -260
- 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/lint/run.mjs +12 -2
- package/package.json +5 -3
- package/release/gate.mjs +82 -31
- package/release/index.mjs +19 -8
- package/release/push.mjs +4 -4
- package/release/records.mjs +54 -0
- package/release/urgent.mjs +129 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +54 -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 +250 -157
- package/runtime/knowledge/hfs/slots.yaml +199 -174
- 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 +86 -115
- package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
- package/runtime/scripts/hfs/app-wiring.mjs +23 -2
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- 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/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/transport-owner.mjs +7 -3
- package/runtime/scripts/hfs/check.mjs +8 -8
- 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 +6 -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 +24 -22
- package/stack/cli.mjs +10 -2
- package/stack/deploy.mjs +21 -4
- package/stack/target-binding.mjs +0 -60
- package/stack/urgent-floor.mjs +147 -0
- package/sync/ci-commands.mjs +3 -1
- package/sync/index.mjs +32 -8
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +4 -1
- 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/ci-workflows-lite/github/workflows/stack-deploy.yml +4 -1
- 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/src/modules/domain/identity/identity.decorators.ts +18 -7
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +15 -1
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +10 -9
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +34 -18
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +29 -8
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +80 -4
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +39 -11
- 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 +23 -13
- 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 +6 -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/modules/platform/http-security/index.ts +1 -1
- 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
|
@@ -8,13 +8,29 @@ services:
|
|
|
8
8
|
HTTP_SECURITY_ALLOWED_ORIGINS: http://dev.{{project}}.example.invalid
|
|
9
9
|
PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
|
|
10
10
|
PRIMARY_DB_SCHEMA: public
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
GOOGLE_CLIENT_ID: REPLACE_WITH_GOOGLE_OAUTH_CLIENT_ID
|
|
12
|
+
GOOGLE_CLIENT_SECRET_FILE: /run/secrets/google-client-secret
|
|
13
|
+
GOOGLE_CALLBACK_URL: http://dev.{{project}}.example.invalid/auth/google/callback
|
|
14
|
+
AUTH_JWT_ISSUER: http://dev.{{project}}.example.invalid
|
|
15
|
+
AUTH_JWT_AUDIENCE: {{project}}
|
|
16
|
+
AUTH_JWT_KEY_ID: dev-1
|
|
17
|
+
AUTH_JWT_PRIVATE_KEY_FILE: /run/secrets/auth-jwt-private-key
|
|
18
|
+
STORAGE_ENDPOINT: https://storage.{{project}}.example.invalid
|
|
19
|
+
STORAGE_REGION: us-east-1
|
|
20
|
+
STORAGE_BUCKET: {{project}}-dev
|
|
21
|
+
STORAGE_ACCESS_KEY_ID_FILE: /run/secrets/storage-access-key-id
|
|
22
|
+
STORAGE_SECRET_ACCESS_KEY_FILE: /run/secrets/storage-secret-access-key
|
|
13
23
|
secrets:
|
|
14
24
|
- source: primary-db-url
|
|
15
25
|
target: primary-db-url
|
|
16
|
-
- source:
|
|
17
|
-
target:
|
|
26
|
+
- source: google-client-secret
|
|
27
|
+
target: google-client-secret
|
|
28
|
+
- source: auth-jwt-private-key
|
|
29
|
+
target: auth-jwt-private-key
|
|
30
|
+
- source: storage-access-key-id
|
|
31
|
+
target: storage-access-key-id
|
|
32
|
+
- source: storage-secret-access-key
|
|
33
|
+
target: storage-secret-access-key
|
|
18
34
|
networks: [app]
|
|
19
35
|
healthcheck:
|
|
20
36
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
@@ -66,6 +82,38 @@ services:
|
|
|
66
82
|
restart_policy:
|
|
67
83
|
condition: on-failure
|
|
68
84
|
delay: 5s
|
|
85
|
+
db:
|
|
86
|
+
image: "supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2"
|
|
87
|
+
environment:
|
|
88
|
+
POSTGRES_DB: postgres
|
|
89
|
+
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
|
|
90
|
+
secrets:
|
|
91
|
+
- source: db-password
|
|
92
|
+
target: db-password
|
|
93
|
+
volumes:
|
|
94
|
+
- db-data:/var/lib/postgresql/data
|
|
95
|
+
networks: [app]
|
|
96
|
+
healthcheck:
|
|
97
|
+
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
|
|
98
|
+
interval: 10s
|
|
99
|
+
timeout: 5s
|
|
100
|
+
start_period: 30s
|
|
101
|
+
retries: 5
|
|
102
|
+
deploy:
|
|
103
|
+
replicas: 1
|
|
104
|
+
placement:
|
|
105
|
+
constraints: ["node.labels.{{project}} == dev"]
|
|
106
|
+
update_config:
|
|
107
|
+
parallelism: 1
|
|
108
|
+
order: stop-first
|
|
109
|
+
failure_action: rollback
|
|
110
|
+
monitor: 30s
|
|
111
|
+
rollback_config:
|
|
112
|
+
parallelism: 1
|
|
113
|
+
order: stop-first
|
|
114
|
+
restart_policy:
|
|
115
|
+
condition: on-failure
|
|
116
|
+
delay: 5s
|
|
69
117
|
ingress:
|
|
70
118
|
image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
|
|
71
119
|
configs:
|
|
@@ -104,13 +152,28 @@ secrets:
|
|
|
104
152
|
primary-db-url:
|
|
105
153
|
external: true
|
|
106
154
|
name: "{{project}}-dev-primary-db-url-v1"
|
|
107
|
-
|
|
155
|
+
db-password:
|
|
156
|
+
external: true
|
|
157
|
+
name: "{{project}}-dev-db-password-v1"
|
|
158
|
+
google-client-secret:
|
|
159
|
+
external: true
|
|
160
|
+
name: "{{project}}-dev-google-client-secret-v1"
|
|
161
|
+
auth-jwt-private-key:
|
|
108
162
|
external: true
|
|
109
|
-
name: "{{project}}-dev-
|
|
163
|
+
name: "{{project}}-dev-auth-jwt-private-key-v1"
|
|
164
|
+
storage-access-key-id:
|
|
165
|
+
external: true
|
|
166
|
+
name: "{{project}}-dev-storage-access-key-id-v1"
|
|
167
|
+
storage-secret-access-key:
|
|
168
|
+
external: true
|
|
169
|
+
name: "{{project}}-dev-storage-secret-access-key-v1"
|
|
110
170
|
configs:
|
|
111
171
|
ingress-config:
|
|
112
172
|
file: ./Caddyfile
|
|
113
173
|
volumes:
|
|
174
|
+
db-data:
|
|
175
|
+
external: true
|
|
176
|
+
name: "{{project}}-dev-db-data"
|
|
114
177
|
ingress-data:
|
|
115
178
|
external: true
|
|
116
179
|
name: "{{project}}-dev-ingress-data"
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
# Environment key roster
|
|
2
2
|
|
|
3
|
-
This roster declares names only. Scaffolding generates the
|
|
3
|
+
This roster declares names only. Scaffolding generates the six declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set dev` before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Store product credential values in sealed custody and materialize private KEY files outside the repository.
|
|
4
4
|
|
|
5
5
|
| Key | Consumer |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| `PORT` | API listener |
|
|
8
8
|
| `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
|
|
9
|
-
| `PRIMARY_DB_URL` | API
|
|
9
|
+
| `PRIMARY_DB_URL` | API least-privilege database connection; `.starcistacks/dev/secrets/primary-db-url.enc` maps to external `{{project}}-dev-primary-db-url-v1` and mounted `/run/secrets/primary-db-url` |
|
|
10
10
|
| `PRIMARY_DB_SCHEMA` | API primary database schema |
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
11
|
+
| `POSTGRES_PASSWORD` | The db service's superuser password; `.starcistacks/dev/secrets/db-password.enc` maps to external `{{project}}-dev-db-password-v1` and mounted `/run/secrets/db-password` |
|
|
12
|
+
| `GOOGLE_CLIENT_ID` | API Google OAuth client |
|
|
13
|
+
| `GOOGLE_CLIENT_SECRET` | API Google OAuth client; `.starcistacks/dev/secrets/google-client-secret.enc` maps to external `{{project}}-dev-google-client-secret-v1` and mounted `/run/secrets/google-client-secret` |
|
|
14
|
+
| `GOOGLE_CALLBACK_URL` | API Google OAuth callback door |
|
|
15
|
+
| `AUTH_JWT_ISSUER` | API access-token issuer |
|
|
16
|
+
| `AUTH_JWT_AUDIENCE` | API access-token audience |
|
|
17
|
+
| `AUTH_JWT_KEY_ID` | API signing key id, published in the JWKS |
|
|
18
|
+
| `AUTH_JWT_PRIVATE_KEY` | API ES256 signing key; `.starcistacks/dev/secrets/auth-jwt-private-key.enc` maps to external `{{project}}-dev-auth-jwt-private-key-v1` and mounted `/run/secrets/auth-jwt-private-key` |
|
|
19
|
+
| `STORAGE_ENDPOINT` | API S3-compatible storage endpoint |
|
|
20
|
+
| `STORAGE_REGION` | API storage signing region |
|
|
21
|
+
| `STORAGE_BUCKET` | API storage bucket |
|
|
22
|
+
| `STORAGE_ACCESS_KEY_ID` | API storage credential; `.starcistacks/dev/secrets/storage-access-key-id.enc` maps to external `{{project}}-dev-storage-access-key-id-v1` and mounted `/run/secrets/storage-access-key-id` |
|
|
23
|
+
| `STORAGE_SECRET_ACCESS_KEY` | API storage credential; `.starcistacks/dev/secrets/storage-secret-access-key.enc` maps to external `{{project}}-dev-storage-secret-access-key-v1` and mounted `/run/secrets/storage-secret-access-key` |
|
|
24
|
+
| `DATABASE_URL` | `starci app db` verbs (migrate, dump, plan) and `contract:emit`, from the operator's environment |
|
|
13
25
|
| `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
|
|
26
|
+
| `NEXT_PUBLIC_API_URL` | Front-end auth doors of the API |
|
|
14
27
|
| `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
|
|
15
|
-
| `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
|
|
16
|
-
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
|
|
17
28
|
|
|
18
|
-
Public front-end configuration is supplied at build time and stays outside sealed custody.
|
|
29
|
+
Public front-end configuration is supplied at build time and stays outside sealed custody. The database verbs read `DATABASE_URL` from the operator's environment; the GitHub secret store carries only the age identity.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Production stack
|
|
2
2
|
|
|
3
3
|
The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
|
|
4
|
-
|
|
4
|
+
The `db` service (image supabase/postgres) supplies PostgreSQL with pg_cron, pg_net, pgmq and pgvector on a named volume; sign-in is the API's own (Passport) and storage is the S3-compatible MinIO the environment names. The app and its dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
|
|
5
5
|
The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. Public records under `.starcistacks/production/infra/swarm/` explicitly mark those owner-run operations as pending.
|
|
6
6
|
Its application image references contain illustrative all-zero digests; they identify no published image and must be replaced before deployment.
|
|
7
7
|
|
|
8
|
-
The runtime template tracks no encrypted members. Scaffolding generates the
|
|
8
|
+
The runtime template tracks no encrypted members. Scaffolding generates the six declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set production` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
|
|
9
9
|
|
|
10
10
|
| Command | Operation |
|
|
11
11
|
| --- | --- |
|
|
@@ -26,13 +26,13 @@ Every item is pending. Swarm initialization, provider URIs, DNS changes and key
|
|
|
26
26
|
| Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress volumes. |
|
|
27
27
|
| DNS | Replace the reserved `example.invalid` hosts consistently in the manifest, Caddyfile and public configuration; publish address records for the web and API hosts and retain a provider receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
|
|
28
28
|
| TLS | Caddy issues certificates after real DNS resolves. Provision `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data` on the selected node; retain an issuance and renewal receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
|
|
29
|
-
| Database | Provision
|
|
29
|
+
| Database | Provision `{{project}}-production-db-data` on the stateful node; after the first deploy create the app_be login through the baseline migration (`starci app db migrate --url-env DATABASE_URL --execute`). Record the recovery owner without copying credentials here. |
|
|
30
30
|
| Secret custody | Replace generated placeholder values with real values at the manifest's declared `.starcistacks/production/secrets/*.enc` paths; record their keys in `.starcistacks/production/runtime/env/KEYS.md`. The app's age identity stays only in the host's untracked `.claude/secret.env`. |
|
|
31
|
-
| External secrets | Materialize the sealed members in owner custody and create `{{project}}-production
|
|
32
|
-
| Public build configuration | Build the front end with its declared public site and GraphQL URLs
|
|
31
|
+
| External secrets | Materialize the sealed members in owner custody and create the `{{project}}-production-<member>-v1` external secret of each declared member. The API reads mounted files; rotation creates new versioned names before updating the model. |
|
|
32
|
+
| Public build configuration | Build the front end with its declared public site, API and GraphQL URLs. Public browser values stay outside sealed secret custody. |
|
|
33
33
|
| Immutable release | Replace the illustrative all-zero application digests in the manifest, model and `.starcistacks/production/infra/swarm/releases/images.lock.yaml` with digests produced by the verified release. Store each rendered release under `/srv/{{project}}/releases/<digest>`. Retain the last accepted record in `.starcistacks/production/infra/swarm/releases/previous.yaml`, then move the current pointer and update `.starcistacks/production/infra/swarm/releases/current.yaml` after verification. |
|
|
34
34
|
| Rollback | Retain the previous model and images, redeploy that model if convergence or health fails, and record the resulting convergence receipt. Database changes follow an expand/contract sequence. |
|
|
35
|
-
| Backup | Provision the manifest's
|
|
35
|
+
| Backup | Provision the manifest's thirty daily encrypted off-host database exports (`starci app db dump`) and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
|
|
36
36
|
| Restore proof | Perform a restore into isolated infrastructure, verify the restored database and ingress state, and attach a dated receipt with artifact digests to `.starcistacks/production/infra/swarm/recovery/restore-proof.md`. Its initial record is pending; no restore has been run by scaffolding. |
|
|
37
37
|
|
|
38
38
|
No host scheduler is installed. Scheduled application work belongs to the back-end scheduler or an explicitly declared replicated job.
|
package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Status: pending owner execution. No backup or restore has been run by scaffolding.
|
|
4
4
|
|
|
5
|
-
The production operator owns recovery of the
|
|
5
|
+
The production operator owns recovery of the db service's volume `{{project}}-production-db-data` and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision thirty daily encrypted off-host database exports through `starci app db dump`. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
|
|
6
6
|
|
|
7
|
-
Before relying on the backup policy, restore
|
|
7
|
+
Before relying on the backup policy, restore a `starci app db dump` archive and both volume snapshots into isolated infrastructure. Record the actual off-host destination, encryption custody, backup artifact digests, restoration time and operator. Verify database connectivity and the API readiness endpoint, then verify the restored TLS and ingress configuration. Attach the dated result here without credentials.
|
|
@@ -8,13 +8,29 @@ services:
|
|
|
8
8
|
HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
|
|
9
9
|
PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
|
|
10
10
|
PRIMARY_DB_SCHEMA: public
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
GOOGLE_CLIENT_ID: REPLACE_WITH_GOOGLE_OAUTH_CLIENT_ID
|
|
12
|
+
GOOGLE_CLIENT_SECRET_FILE: /run/secrets/google-client-secret
|
|
13
|
+
GOOGLE_CALLBACK_URL: https://api.{{project}}.example.invalid/auth/google/callback
|
|
14
|
+
AUTH_JWT_ISSUER: https://api.{{project}}.example.invalid
|
|
15
|
+
AUTH_JWT_AUDIENCE: {{project}}
|
|
16
|
+
AUTH_JWT_KEY_ID: production-1
|
|
17
|
+
AUTH_JWT_PRIVATE_KEY_FILE: /run/secrets/auth-jwt-private-key
|
|
18
|
+
STORAGE_ENDPOINT: https://storage.{{project}}.example.invalid
|
|
19
|
+
STORAGE_REGION: us-east-1
|
|
20
|
+
STORAGE_BUCKET: {{project}}-production
|
|
21
|
+
STORAGE_ACCESS_KEY_ID_FILE: /run/secrets/storage-access-key-id
|
|
22
|
+
STORAGE_SECRET_ACCESS_KEY_FILE: /run/secrets/storage-secret-access-key
|
|
13
23
|
secrets:
|
|
14
24
|
- source: primary-db-url
|
|
15
25
|
target: primary-db-url
|
|
16
|
-
- source:
|
|
17
|
-
target:
|
|
26
|
+
- source: google-client-secret
|
|
27
|
+
target: google-client-secret
|
|
28
|
+
- source: auth-jwt-private-key
|
|
29
|
+
target: auth-jwt-private-key
|
|
30
|
+
- source: storage-access-key-id
|
|
31
|
+
target: storage-access-key-id
|
|
32
|
+
- source: storage-secret-access-key
|
|
33
|
+
target: storage-secret-access-key
|
|
18
34
|
networks: [app]
|
|
19
35
|
healthcheck:
|
|
20
36
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
@@ -66,6 +82,38 @@ services:
|
|
|
66
82
|
restart_policy:
|
|
67
83
|
condition: on-failure
|
|
68
84
|
delay: 5s
|
|
85
|
+
db:
|
|
86
|
+
image: "supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2"
|
|
87
|
+
environment:
|
|
88
|
+
POSTGRES_DB: postgres
|
|
89
|
+
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
|
|
90
|
+
secrets:
|
|
91
|
+
- source: db-password
|
|
92
|
+
target: db-password
|
|
93
|
+
volumes:
|
|
94
|
+
- db-data:/var/lib/postgresql/data
|
|
95
|
+
networks: [app]
|
|
96
|
+
healthcheck:
|
|
97
|
+
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
|
|
98
|
+
interval: 10s
|
|
99
|
+
timeout: 5s
|
|
100
|
+
start_period: 30s
|
|
101
|
+
retries: 5
|
|
102
|
+
deploy:
|
|
103
|
+
replicas: 1
|
|
104
|
+
placement:
|
|
105
|
+
constraints: ["node.labels.{{project}} == production"]
|
|
106
|
+
update_config:
|
|
107
|
+
parallelism: 1
|
|
108
|
+
order: stop-first
|
|
109
|
+
failure_action: rollback
|
|
110
|
+
monitor: 30s
|
|
111
|
+
rollback_config:
|
|
112
|
+
parallelism: 1
|
|
113
|
+
order: stop-first
|
|
114
|
+
restart_policy:
|
|
115
|
+
condition: on-failure
|
|
116
|
+
delay: 5s
|
|
69
117
|
ingress:
|
|
70
118
|
image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
|
|
71
119
|
configs:
|
|
@@ -105,13 +153,28 @@ secrets:
|
|
|
105
153
|
primary-db-url:
|
|
106
154
|
external: true
|
|
107
155
|
name: "{{project}}-production-primary-db-url-v1"
|
|
108
|
-
|
|
156
|
+
db-password:
|
|
157
|
+
external: true
|
|
158
|
+
name: "{{project}}-production-db-password-v1"
|
|
159
|
+
google-client-secret:
|
|
160
|
+
external: true
|
|
161
|
+
name: "{{project}}-production-google-client-secret-v1"
|
|
162
|
+
auth-jwt-private-key:
|
|
109
163
|
external: true
|
|
110
|
-
name: "{{project}}-production-
|
|
164
|
+
name: "{{project}}-production-auth-jwt-private-key-v1"
|
|
165
|
+
storage-access-key-id:
|
|
166
|
+
external: true
|
|
167
|
+
name: "{{project}}-production-storage-access-key-id-v1"
|
|
168
|
+
storage-secret-access-key:
|
|
169
|
+
external: true
|
|
170
|
+
name: "{{project}}-production-storage-secret-access-key-v1"
|
|
111
171
|
configs:
|
|
112
172
|
ingress-config:
|
|
113
173
|
file: ./Caddyfile
|
|
114
174
|
volumes:
|
|
175
|
+
db-data:
|
|
176
|
+
external: true
|
|
177
|
+
name: "{{project}}-production-db-data"
|
|
115
178
|
ingress-data:
|
|
116
179
|
external: true
|
|
117
180
|
name: "{{project}}-production-ingress-data"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Production environment key roster
|
|
2
2
|
|
|
3
|
-
This inventory declares names, consumers and mounted file paths. Scaffolding generates the
|
|
3
|
+
This inventory declares names, consumers and mounted file paths. Scaffolding generates the six declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set production` after owner provisioning and before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Scaffolding supplies no deployment credential or successful deployment.
|
|
4
4
|
|
|
5
5
|
| Key | Consumer |
|
|
6
6
|
| --- | --- |
|
|
@@ -8,14 +8,22 @@ This inventory declares names, consumers and mounted file paths. Scaffolding gen
|
|
|
8
8
|
| `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
|
|
9
9
|
| `PRIMARY_DB_URL` | API least-privilege database connection; reads `PRIMARY_DB_URL_FILE` at `/run/secrets/primary-db-url` |
|
|
10
10
|
| `PRIMARY_DB_SCHEMA` | API primary database schema |
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
11
|
+
| `POSTGRES_PASSWORD` | The db service's superuser password; reads `POSTGRES_PASSWORD_FILE` at `/run/secrets/db-password` |
|
|
12
|
+
| `GOOGLE_CLIENT_ID` | API Google OAuth client |
|
|
13
|
+
| `GOOGLE_CLIENT_SECRET` | API Google OAuth client; reads `GOOGLE_CLIENT_SECRET_FILE` at `/run/secrets/google-client-secret` |
|
|
14
|
+
| `GOOGLE_CALLBACK_URL` | API Google OAuth callback door |
|
|
15
|
+
| `AUTH_JWT_ISSUER` | API access-token issuer |
|
|
16
|
+
| `AUTH_JWT_AUDIENCE` | API access-token audience |
|
|
17
|
+
| `AUTH_JWT_KEY_ID` | API signing key id, published in the JWKS |
|
|
18
|
+
| `AUTH_JWT_PRIVATE_KEY` | API ES256 signing key; reads `AUTH_JWT_PRIVATE_KEY_FILE` at `/run/secrets/auth-jwt-private-key` |
|
|
19
|
+
| `STORAGE_ENDPOINT` | API S3-compatible storage endpoint |
|
|
20
|
+
| `STORAGE_REGION` | API storage signing region |
|
|
21
|
+
| `STORAGE_BUCKET` | API storage bucket |
|
|
22
|
+
| `STORAGE_ACCESS_KEY_ID` | API storage credential; reads `STORAGE_ACCESS_KEY_ID_FILE` at `/run/secrets/storage-access-key-id` |
|
|
23
|
+
| `STORAGE_SECRET_ACCESS_KEY` | API storage credential; reads `STORAGE_SECRET_ACCESS_KEY_FILE` at `/run/secrets/storage-secret-access-key` |
|
|
24
|
+
| `DATABASE_URL` | `starci app db` verbs (migrate, dump, plan) and `contract:emit`, from the operator's environment |
|
|
13
25
|
| `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
|
|
26
|
+
| `NEXT_PUBLIC_API_URL` | Front-end auth doors of the API |
|
|
14
27
|
| `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
|
|
15
|
-
| `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
|
|
16
|
-
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
|
|
17
|
-
| `SUPABASE_ACCESS_TOKEN` | Production database deployment |
|
|
18
|
-
| `SUPABASE_DB_PASSWORD` | Production database deployment |
|
|
19
|
-
| `SUPABASE_PROJECT_ID` | Production database deployment |
|
|
20
28
|
|
|
21
|
-
Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in
|
|
29
|
+
Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in the six `.starcistacks/production/secrets/*.enc` members at the declared paths; their versioned external secrets are `{{project}}-production-<member>-v1`. The database deploy workflow reads `DATABASE_URL` from the sealed `database-url` member of its environment; it never enters an API container.
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# {{project}}
|
|
2
2
|
|
|
3
|
-
One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and
|
|
3
|
+
One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and its own PostgreSQL with SQL migrations in `db/` as the schema authority.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and
|
|
7
|
+
The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and signs in only through the API's own auth doors (`fe/apps/web/src/modules/auth`, which holds the access token in memory while the refresh token stays in an httpOnly cookie); every data read and write goes through the API, which verifies the access token against its own keys.
|
|
8
8
|
|
|
9
9
|
## Stack
|
|
10
10
|
|
|
11
|
-
TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`,
|
|
11
|
+
TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, PostgreSQL (the `db` service, image supabase/postgres), Passport for Google and password sign-in, S3-compatible storage (MinIO), npm workspaces, and Turbo for front-end tasks.
|
|
12
12
|
|
|
13
13
|
## Repository layout
|
|
14
14
|
|
|
15
|
-
- `
|
|
16
|
-
- `be/`: the API, shared platform capabilities, verified identity admission, and
|
|
17
|
-
- `fe/`: the web app;
|
|
15
|
+
- `db/`: forward-only SQL migrations, seed data, and generated database types.
|
|
16
|
+
- `be/`: the API, shared platform capabilities, verified identity admission, and the passport and storage integrations.
|
|
17
|
+
- `fe/`: the web app; it holds no credential, signs in through `fe/apps/web/src/modules/auth` and reaches data through `fe/apps/web/src/modules/api`.
|
|
18
18
|
- `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
|
|
19
19
|
- `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
|
|
|
24
24
|
|
|
25
25
|
## Development
|
|
26
26
|
|
|
27
|
-
Every development dependency runs in Docker Linux containers.
|
|
27
|
+
Every development dependency runs in Docker Linux containers. Create the local database once with `npm run db:create`, start it with `npm run db:start` (one container of the pinned `db` image, on 127.0.0.1:54322), export `DATABASE_URL=postgresql://supabase_admin@127.0.0.1:54322/postgres` and apply `db/bootstrap.sql` and the migrations with `npm run db:push`; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies, the database included, run on Docker Swarm; pgvector owns vector search.
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
npm install
|
|
@@ -36,17 +36,17 @@ npm run build:be
|
|
|
36
36
|
npm run build:fe
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMARY_DB_SCHEMA`, `
|
|
39
|
+
The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMARY_DB_SCHEMA`, the Google OAuth keys `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` and `GOOGLE_CALLBACK_URL`, its own token keys `AUTH_JWT_ISSUER`, `AUTH_JWT_AUDIENCE`, `AUTH_JWT_KEY_ID` and `AUTH_JWT_PRIVATE_KEY`, and the storage keys `STORAGE_ENDPOINT`, `STORAGE_REGION`, `STORAGE_BUCKET`, `STORAGE_ACCESS_KEY_ID` and `STORAGE_SECRET_ACCESS_KEY`. The database tooling reads `DATABASE_URL`. The web app reads `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_API_URL`, and, once the first GraphQL operation exists, `NEXT_PUBLIC_GRAPHQL_URL`. Missing configuration stops the relevant process with a named error; no template supplies credential defaults.
|
|
40
40
|
|
|
41
|
-
Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.web.buildArgs.values`. Before building deployment images, replace the placeholders with your site's
|
|
41
|
+
Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.web.buildArgs.values`. Before building deployment images, replace the placeholders with your site's and your API's public origins. These values reach every browser; every credential stays in sealed runtime custody.
|
|
42
42
|
|
|
43
|
-
`npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local
|
|
43
|
+
`npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local database (`npm run db:start`, then `npm run db:push`); a lite app has no e2e world.
|
|
44
44
|
|
|
45
45
|
The public runtime template tracks only sealed member names. Scaffolding creates this app's own age recipient and encrypts generated placeholder values into the declared `.starcistacks/<environment>/secrets/*.enc` members. Keep the matching host age identity in untracked local custody, then use `starci app secret set` to replace each placeholder with an actual value before deployment. Product apps retain sealed member custody. Runtime specs use disposable copies with ephemeral identities and remove them afterwards.
|
|
46
46
|
|
|
47
47
|
## Database changes
|
|
48
48
|
|
|
49
|
-
Create a forward-only migration
|
|
49
|
+
Create a forward-only migration with `npm run db:new -- <kebab-name>` (it lands in `db/migrations`), keep each table's RLS policies and grants beside the table, apply it with `npm run db:push`, and run `npm run contract:emit` to refresh `db/types/database.types.ts`. `starci app add table <name> --fe` adds the table with its GraphQL feature on the API and its operations and SWR hooks in the web app; `npm run codegen:graphql` regenerates the web app's wire types from `be/contracts/<app>/schema.graphql`.
|
|
50
50
|
|
|
51
51
|
## Work
|
|
52
52
|
|
|
@@ -6,7 +6,7 @@ begin
|
|
|
6
6
|
end;
|
|
7
7
|
$$;
|
|
8
8
|
|
|
9
|
-
-- The attributes a
|
|
9
|
+
-- The attributes a migration may change: SUPERUSER and BYPASSRLS stay as create role set them.
|
|
10
10
|
alter role app_be login noinherit nocreatedb nocreaterole;
|
|
11
11
|
|
|
12
12
|
create schema if not exists private;
|
|
@@ -24,6 +24,26 @@ begin
|
|
|
24
24
|
end;
|
|
25
25
|
$$;
|
|
26
26
|
|
|
27
|
+
-- The rotating refresh sessions of integrations/passport (auth.users comes from db/bootstrap.sql).
|
|
28
|
+
-- One row per issued refresh token, stored by its sha256 only. A family is one sign-in; rotation revokes the presented row
|
|
29
|
+
-- and inserts its successor, and presenting a revoked row revokes the whole family (reuse detection).
|
|
30
|
+
create table auth.refresh_sessions (
|
|
31
|
+
id uuid primary key default gen_random_uuid(),
|
|
32
|
+
family_id uuid not null,
|
|
33
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
34
|
+
token_hash char(64) not null unique,
|
|
35
|
+
expires_at timestamptz not null,
|
|
36
|
+
revoked_at timestamptz,
|
|
37
|
+
created_at timestamptz not null default now()
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
create index refresh_sessions_family_idx on auth.refresh_sessions (family_id);
|
|
41
|
+
|
|
42
|
+
grant usage on schema auth to app_be;
|
|
43
|
+
grant select, insert, update on table auth.users to app_be;
|
|
44
|
+
grant select, insert on table auth.identities to app_be;
|
|
45
|
+
grant select, insert, update on table auth.refresh_sessions to app_be;
|
|
46
|
+
|
|
27
47
|
create table public.profiles (
|
|
28
48
|
id uuid primary key references auth.users (id) on delete cascade,
|
|
29
49
|
display_name text not null check (char_length(display_name) between 1 and 120),
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { mockEntityManager } from "@starci/jest-preset"
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
|
-
import { sql } from "./database.sql"
|
|
4
3
|
import { DatabaseErrorCode } from "./errors/database.error"
|
|
5
|
-
import { recordWebhookDelivery } from "./webhook-inbox.client"
|
|
4
|
+
import { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
const INSERT = sql`SELECT id FROM record_spec_inbox($1, $2, $3::jsonb)`
|
|
6
|
+
const INSERT = webhookReceiptInsert("spec_inbox")
|
|
9
7
|
const PAYLOAD = { id: "delivery-1" }
|
|
10
8
|
|
|
11
9
|
const record = (manager: EntityManager, deliveryId: string | undefined) =>
|
|
12
|
-
recordWebhookDelivery({ manager,
|
|
10
|
+
recordWebhookDelivery({ manager, table: "spec_inbox", provider: "spec", deliveryId, payload: PAYLOAD })
|
|
11
|
+
|
|
12
|
+
describe("webhookReceiptInsert", () => {
|
|
13
|
+
it("refuses a table name that is not a lowercase identifier", () => {
|
|
14
|
+
expect(() => webhookReceiptInsert("spec; drop table x")).toThrow(TypeError)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
13
17
|
|
|
14
18
|
describe("recordWebhookDelivery", () => {
|
|
15
19
|
it("records a new delivery through the caller's transaction manager", async () => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { EntityManager } from "typeorm"
|
|
2
2
|
import { DatabaseError, DatabaseErrorCode } from "./errors/database.error"
|
|
3
|
-
import type { SqlText } from "./database.sql"
|
|
4
3
|
|
|
5
4
|
interface RecordWebhookDelivery {
|
|
6
5
|
readonly manager: EntityManager
|
|
7
|
-
|
|
6
|
+
/** The provider inbox table under public, created by its db migration. */
|
|
7
|
+
readonly table: string
|
|
8
8
|
readonly provider: string
|
|
9
9
|
readonly deliveryId: string | undefined
|
|
10
10
|
readonly payload: object
|
|
@@ -12,13 +12,22 @@ interface RecordWebhookDelivery {
|
|
|
12
12
|
|
|
13
13
|
type WebhookReceiptId = string | undefined
|
|
14
14
|
|
|
15
|
+
/** A provider inbox table name: a lowercase identifier, never a value. */
|
|
16
|
+
const TABLE = /^[a-z_][a-z0-9_]*$/
|
|
17
|
+
|
|
18
|
+
/** The conflict-safe insert of one pending delivery into `public.<table>`, answering its id only to the winning transaction. */
|
|
19
|
+
export const webhookReceiptInsert = (table: string): string => {
|
|
20
|
+
if (!TABLE.test(table)) throw new TypeError(`${table} is not a provider inbox table name`)
|
|
21
|
+
return `INSERT INTO public.${table} (provider, delivery_id, payload) VALUES ($1, $2, $3::jsonb) ON CONFLICT (provider, delivery_id) DO NOTHING RETURNING id`
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Records one pending delivery using the intake's transaction manager and returns its durable receipt identity.
|
|
17
26
|
* Business processing and the completion marker belong to the reactor or job that consumes the committed row.
|
|
18
27
|
*/
|
|
19
28
|
export const recordWebhookDelivery = async ({
|
|
20
29
|
manager,
|
|
21
|
-
|
|
30
|
+
table,
|
|
22
31
|
provider,
|
|
23
32
|
deliveryId,
|
|
24
33
|
payload,
|
|
@@ -27,7 +36,7 @@ export const recordWebhookDelivery = async ({
|
|
|
27
36
|
if (!normalizedDeliveryId) {
|
|
28
37
|
throw new DatabaseError({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
|
|
29
38
|
}
|
|
30
|
-
const rows: Array<{ readonly id: string }> = await manager.query(
|
|
39
|
+
const rows: Array<{ readonly id: string }> = await manager.query(webhookReceiptInsert(table), [
|
|
31
40
|
provider,
|
|
32
41
|
normalizedDeliveryId,
|
|
33
42
|
JSON.stringify(payload),
|
|
@@ -16,3 +16,50 @@ on public.inbox for delete to app_be
|
|
|
16
16
|
using (source like 'client:%' and request_fingerprint is not null and outcome is not null);
|
|
17
17
|
|
|
18
18
|
grant update, delete on table public.inbox to app_be;
|
|
19
|
+
|
|
20
|
+
-- The client claim, its saved outcome and the bounded retention sweep, called through callDbFunction of platform/database (R334).
|
|
21
|
+
-- A conflicting claim locks the existing row and returns its committed fingerprint and outcome after the winner commits.
|
|
22
|
+
create function public.inbox_claim_client(p_source text, p_event_id text, p_fingerprint varchar, p_claimed_at timestamptz)
|
|
23
|
+
returns table (request_fingerprint varchar, outcome jsonb, incomplete boolean)
|
|
24
|
+
language sql
|
|
25
|
+
as $$
|
|
26
|
+
insert into public.inbox as i (source, event_id, request_fingerprint, claimed_at)
|
|
27
|
+
values (p_source, p_event_id, p_fingerprint, p_claimed_at)
|
|
28
|
+
on conflict (source, event_id) do update set outcome = coalesce(i.outcome, '{}'::jsonb)
|
|
29
|
+
returning i.request_fingerprint, i.outcome,
|
|
30
|
+
(i.outcome is not null and (jsonb_typeof(i.outcome) <> 'object' or not (i.outcome ? 'value')))
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
create function public.inbox_save_client_outcome(p_source text, p_event_id text, p_outcome jsonb)
|
|
34
|
+
returns table (outcome jsonb)
|
|
35
|
+
language sql
|
|
36
|
+
as $$
|
|
37
|
+
update public.inbox as i set outcome = p_outcome
|
|
38
|
+
where i.source = p_source and i.event_id = p_event_id
|
|
39
|
+
returning i.outcome
|
|
40
|
+
$$;
|
|
41
|
+
|
|
42
|
+
-- Retention never deletes delivery rows or an incomplete claim; its bounded batch skips rows held by active retries.
|
|
43
|
+
create function public.inbox_sweep_clients(p_cutoff timestamptz, p_limit integer)
|
|
44
|
+
returns table (deleted_count integer)
|
|
45
|
+
language sql
|
|
46
|
+
as $$
|
|
47
|
+
with expired as (
|
|
48
|
+
select source, event_id from public.inbox
|
|
49
|
+
where source like 'client:%' and request_fingerprint is not null and outcome is not null
|
|
50
|
+
and jsonb_typeof(outcome) = 'object' and outcome ? 'value' and claimed_at < p_cutoff
|
|
51
|
+
order by claimed_at, source, event_id limit p_limit for update skip locked
|
|
52
|
+
), deleted as (
|
|
53
|
+
delete from public.inbox as claims using expired
|
|
54
|
+
where claims.source = expired.source and claims.event_id = expired.event_id
|
|
55
|
+
returning claims.event_id
|
|
56
|
+
)
|
|
57
|
+
select count(*)::integer from deleted
|
|
58
|
+
$$;
|
|
59
|
+
|
|
60
|
+
revoke all on function public.inbox_claim_client(text, text, varchar, timestamptz) from public;
|
|
61
|
+
grant execute on function public.inbox_claim_client(text, text, varchar, timestamptz) to app_be;
|
|
62
|
+
revoke all on function public.inbox_save_client_outcome(text, text, jsonb) from public;
|
|
63
|
+
grant execute on function public.inbox_save_client_outcome(text, text, jsonb) to app_be;
|
|
64
|
+
revoke all on function public.inbox_sweep_clients(timestamptz, integer) from public;
|
|
65
|
+
grant execute on function public.inbox_sweep_clients(timestamptz, integer) to app_be;
|
|
@@ -3,7 +3,9 @@ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
|
3
3
|
import { EventBusErrorCode } from "./errors/event-bus.error"
|
|
4
4
|
import { BaseEvent } from "./event-bus.contracts"
|
|
5
5
|
import { EventBusService } from "./event-bus.service"
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// The statement callDbFunction of platform/database sends for event_bus_write.
|
|
8
|
+
const INSERT_OUTBOX_EVENT = "SELECT * FROM event_bus_write($1, $2, $3, $4)"
|
|
7
9
|
|
|
8
10
|
class ReceiptRecordedEvent extends BaseEvent {
|
|
9
11
|
readonly eventId = "receipt-1"
|
|
@@ -4,7 +4,7 @@ import { isCorrelationId } from "@modules/platform/logging"
|
|
|
4
4
|
import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
|
|
5
5
|
import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
|
|
6
6
|
import type { EventBus } from "./event-bus.port"
|
|
7
|
-
import {
|
|
7
|
+
import { callDbFunction } from "@modules/platform/database"
|
|
8
8
|
|
|
9
9
|
@Injectable()
|
|
10
10
|
/** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
|
|
@@ -18,6 +18,6 @@ export class EventBusService implements EventBus {
|
|
|
18
18
|
eventName: event.eventName,
|
|
19
19
|
payload: event.payload,
|
|
20
20
|
}
|
|
21
|
-
await tx
|
|
21
|
+
await callDbFunction(tx, "event_bus_write", [event.eventId, event.eventName, JSON.stringify(envelope), correlationId])
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -7,7 +7,12 @@ import { CLOCK } from "@modules/platform/clock"
|
|
|
7
7
|
import { InboxError, InboxErrorCode } from "./errors/inbox.error"
|
|
8
8
|
import { requestFingerprint } from "./request-fingerprint.policy"
|
|
9
9
|
import { PostgresInbox } from "./inbox.service"
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
// The statements callDbFunction of platform/database sends for each inbox function.
|
|
12
|
+
const CLAIM_EVENT = "SELECT * FROM inbox_claim_event($1, $2)"
|
|
13
|
+
const CLAIM_CLIENT = "SELECT * FROM inbox_claim_client($1, $2, $3, $4)"
|
|
14
|
+
const SAVE_CLIENT_OUTCOME = "SELECT * FROM inbox_save_client_outcome($1, $2, $3)"
|
|
15
|
+
const SWEEP_CLIENTS = "SELECT * FROM inbox_sweep_clients($1, $2)"
|
|
11
16
|
|
|
12
17
|
const runClient = <T>(
|
|
13
18
|
inbox: PostgresInbox,
|