@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.
Files changed (241) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -8
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +289 -0
  5. package/db/dump.mjs +12 -11
  6. package/db/entity-list.mjs +9 -0
  7. package/db/migrate.mjs +84 -260
  8. package/db/new.mjs +46 -0
  9. package/db/post-cutover.mjs +13 -17
  10. package/db/postgres.mjs +80 -0
  11. package/emit/contracts.mjs +5 -9
  12. package/lint/run.mjs +12 -2
  13. package/package.json +5 -3
  14. package/release/gate.mjs +82 -31
  15. package/release/index.mjs +19 -8
  16. package/release/push.mjs +4 -4
  17. package/release/records.mjs +54 -0
  18. package/release/urgent.mjs +129 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +54 -24
  20. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  21. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  22. package/runtime/knowledge/hfs/rules.yaml +250 -157
  23. package/runtime/knowledge/hfs/slots.yaml +199 -174
  24. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  25. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  26. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  27. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  28. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  29. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  30. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  31. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  32. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  33. package/runtime/modules/kernel/command-policy.yaml +1 -1
  34. package/runtime/modules/kernel/failure-codes.yaml +86 -115
  35. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  36. package/runtime/scripts/hfs/app-wiring.mjs +23 -2
  37. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  38. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  39. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  40. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  41. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  42. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  43. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  44. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  45. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  46. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  47. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  48. package/runtime/scripts/hfs/check.mjs +8 -8
  49. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  50. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  51. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  52. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  53. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  54. package/runtime/scripts/hfs/manifest-shape.mjs +6 -5
  55. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  56. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  57. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  58. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  59. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  60. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  61. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  62. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  63. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  64. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  65. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  66. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  67. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  68. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  69. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  70. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  71. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  72. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  73. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  74. package/runtime/scripts/hfs/slots.mjs +8 -2
  75. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  76. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  77. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  78. package/scaffold/add-app.mjs +2 -10
  79. package/scaffold/add-cli-lite.mjs +6 -6
  80. package/scaffold/add-compose.mjs +1 -1
  81. package/scaffold/add-inbox-lite.mjs +4 -8
  82. package/scaffold/add-plan.mjs +1 -1
  83. package/scaffold/add-queue-lite.mjs +16 -24
  84. package/scaffold/add-table.mjs +34 -26
  85. package/scaffold/add.mjs +2 -2
  86. package/scaffold/app.mjs +19 -86
  87. package/scaffold/lite-exports.mjs +1 -1
  88. package/src/main.mjs +24 -22
  89. package/stack/cli.mjs +10 -2
  90. package/stack/deploy.mjs +21 -4
  91. package/stack/target-binding.mjs +0 -60
  92. package/stack/urgent-floor.mjs +147 -0
  93. package/sync/ci-commands.mjs +3 -1
  94. package/sync/index.mjs +32 -8
  95. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +4 -1
  96. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  97. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  98. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +4 -1
  99. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  100. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  101. package/templates/app/gitignore-lite +0 -3
  102. package/templates/app/package-scripts/db.json +9 -0
  103. package/templates/app/package-scripts/package.json +0 -1
  104. package/templates/app/package-scripts-lite/package.json +1 -1
  105. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  106. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  107. package/templates/app/skeleton/README.md +1 -1
  108. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  109. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  110. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  111. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  112. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  113. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  114. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  115. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  116. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  117. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  118. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  119. package/templates/app/skeleton-lite/README.md +11 -11
  120. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  121. package/templates/app/tool-config-lite/prettierignore +0 -1
  122. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  123. package/templates/be/inbox/database-client.ts.tpl +13 -4
  124. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  125. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  126. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  127. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  128. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  129. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  130. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  131. package/templates/be/inbox/service.ts.tpl +1 -2
  132. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  133. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  134. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  135. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  136. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  137. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  138. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  139. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  140. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  141. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  142. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  143. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -7
  144. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +3 -0
  145. package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
  146. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +15 -1
  147. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +10 -9
  148. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +1 -1
  149. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +34 -18
  150. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +29 -8
  151. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +80 -4
  152. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +39 -11
  153. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  154. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  155. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  156. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  157. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  158. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  159. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  160. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  161. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  162. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  163. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  164. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  165. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +23 -13
  166. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  167. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  168. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  169. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  170. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  171. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  172. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  173. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  174. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  175. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  176. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  177. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  178. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  179. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  180. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  181. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  182. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  183. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  184. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  185. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  186. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  187. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  188. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  189. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
  190. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  191. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  192. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  193. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  194. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  195. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  196. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  197. package/templates/be/table/entities.index.ts.tpl +4 -0
  198. package/templates/be/table/entity.ts.tpl +21 -0
  199. package/templates/be/table/index.ts.tpl +1 -0
  200. package/templates/be/table/module.ts.tpl +1 -1
  201. package/templates/be/table/service.api.ts.tpl +10 -11
  202. package/templates/be/table/service.ts.tpl +5 -6
  203. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  204. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  205. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  206. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  207. package/db/local-migrate.mjs +0 -194
  208. package/db/reset.mjs +0 -125
  209. package/emit/db-types.mjs +0 -271
  210. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  211. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  212. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  213. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  214. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  215. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  216. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  217. package/scaffold/supabase-ports.mjs +0 -26
  218. package/templates/app/package-scripts/supabase.json +0 -7
  219. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  220. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  221. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  222. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  223. package/templates/be/inbox/sql.ts.tpl +0 -16
  224. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  225. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  226. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  227. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  228. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  229. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  230. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  231. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  232. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  233. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  234. package/templates/be/table/rows.ts.tpl +0 -4
  235. package/templates/be/table/sql.api.ts.tpl +0 -7
  236. package/templates/be/table/sql.ts.tpl +0 -4
  237. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  238. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  239. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  240. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  241. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
@@ -1,18 +1,50 @@
1
- import { createClient } from "@supabase/supabase-js"
2
- import type { Session, SupabaseClient } from "@supabase/supabase-js"
3
- import { supabaseAnonKey, supabaseUrl } from "@/modules/config"
1
+ import { callAuthDoor } from "./auth-door"
2
+ import type { DoorAnswer } from "./auth-door"
4
3
  import type { AuthClient, AuthFailure, AuthSession } from "./contracts"
4
+ import { heldSession, heldToken, hold, listen } from "./token-store"
5
5
 
6
- let authentication: SupabaseClient["auth"] | undefined
6
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null
7
7
 
8
- /**
9
- * The one Supabase client of the front end, used for Supabase Auth only (the PKCE flow runs in the browser: a returning
10
- * redirect's code is exchanged by `detectSessionInUrl`, no route handler). It never calls `.from()`, `.rpc()` or storage:
11
- * data goes through modules/api to the back end, which verifies the access token.
12
- */
13
- const authOf = (): SupabaseClient["auth"] =>
14
- (authentication ??= createClient(supabaseUrl(), supabaseAnonKey(), {
15
- auth: { flowType: "pkce", detectSessionInUrl: true, persistSession: true, autoRefreshToken: true },
16
- }).auth)
8
+ /** The token and session an auth door issued, narrowed from its unread body; undefined when the body is not one. */
9
+ const issuedOf = (body: unknown): { readonly token: string; readonly session: AuthSession } | undefined => {
10
+ if (!isRecord(body) || typeof body.accessToken !== "string" || !isRecord(body.user) || typeof body.user.id !== "string") return undefined
11
+ const email = typeof body.user.email === "string" ? body.user.email : undefined
12
+ return { token: body.accessToken, session: { userId: body.user.id, ...(email === undefined ? {} : { email }) } }
13
+ }
17
14
 
18
- // {{> fe/auth/supabase-client.ts.tpl}}
15
+ /** The failure kind of a door's status: refused credentials, rejected input, or the API unavailable. */
16
+ const kindOf = (status: number): string => {
17
+ if (status === 400 || status === 401 || status === 403) return "refused"
18
+ return status === 422 ? "invalid" : "unavailable"
19
+ }
20
+
21
+ const failureOf = (answer: DoorAnswer): AuthFailure => ({ code: `AUTH_${String(answer.status)}`, kind: kindOf(answer.status), message: "" })
22
+
23
+ const ok = (answer: DoorAnswer): boolean => answer.status >= 200 && answer.status < 300
24
+
25
+ /** Rotates the refresh cookie and holds the new access token; signs out when the API refuses the cookie. */
26
+ const refresh = async (): Promise<string | undefined> => {
27
+ const answer = await callAuthDoor("refresh")
28
+ hold(ok(answer) ? issuedOf(answer.body) : undefined)
29
+ return heldToken()
30
+ }
31
+
32
+ /** The lite session issuer's client: the app's own auth doors through `auth-door.ts`, the token in `token-store.ts`. */
33
+ export const authClient: AuthClient = {
34
+ session: async () => heldSession() ?? ((await refresh()) === undefined ? undefined : heldSession()),
35
+ accessToken: async () => heldToken() ?? refresh(),
36
+ refreshAccessToken: refresh,
37
+ signInWithPassword: async (email, password) => {
38
+ const answer = await callAuthDoor("sign-in", { email, password })
39
+ const issued = ok(answer) ? issuedOf(answer.body) : undefined
40
+ if (issued === undefined) return failureOf(answer)
41
+ hold(issued)
42
+ return undefined
43
+ },
44
+ signOut: async () => {
45
+ const answer = await callAuthDoor("sign-out")
46
+ hold(undefined)
47
+ return ok(answer) ? undefined : failureOf(answer)
48
+ },
49
+ onChange: listen,
50
+ }
@@ -0,0 +1,31 @@
1
+ import type { AuthSession } from "./contracts"
2
+
3
+ /** The signed-in reader and the access token, in memory only; the refresh token never reaches the browser's script. */
4
+ interface Held {
5
+ readonly token: string
6
+ readonly session: AuthSession
7
+ }
8
+
9
+ let held: Held | undefined
10
+ const listeners = new Set<(session: AuthSession | undefined) => void>()
11
+
12
+ /** The held access token, or undefined when signed out. */
13
+ export const heldToken = (): string | undefined => held?.token
14
+
15
+ /** The held session, or undefined when signed out. */
16
+ export const heldSession = (): AuthSession | undefined => held?.session
17
+
18
+ /** Replaces the held token and session (undefined signs out) and tells every listener. */
19
+ export const hold = (next: Held | undefined): void => {
20
+ held = next
21
+ for (const listener of listeners) listener(held?.session)
22
+ }
23
+
24
+ /** Subscribes to session changes; the listener hears the current session first. */
25
+ export const listen = (listener: (session: AuthSession | undefined) => void): (() => void) => {
26
+ listeners.add(listener)
27
+ listener(held?.session)
28
+ return () => {
29
+ listeners.delete(listener)
30
+ }
31
+ }
@@ -6,8 +6,5 @@ const required = (name: string, value: string | undefined): string => {
6
6
 
7
7
  /** The public origin used for metadata. */
8
8
  export const siteUrl = (): string => required("NEXT_PUBLIC_SITE_URL", process.env.NEXT_PUBLIC_SITE_URL)
9
- /** The public Supabase project origin, read by modules/auth only. */
10
- export const supabaseUrl = (): string => required("NEXT_PUBLIC_SUPABASE_URL", process.env.NEXT_PUBLIC_SUPABASE_URL)
11
- /** The public anonymous key of Supabase Auth; the back end verifies every access token it is sent. */
12
- export const supabaseAnonKey = (): string =>
13
- required("NEXT_PUBLIC_SUPABASE_ANON_KEY", process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
9
+ /** The public origin of the API whose auth doors modules/auth calls; the browser holds no credential of its own. */
10
+ export const apiUrl = (): string => required("NEXT_PUBLIC_API_URL", process.env.NEXT_PUBLIC_API_URL)
@@ -1,194 +0,0 @@
1
- // Explicit-local migration applies only the app's active SQL chain through the Supabase API owner.
2
- // Supabase CLI v2.119.0: db/push, migration-pending, migration-list and db-config.toml-read (read 2026-10-18).
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { createHash } from 'node:crypto';
6
- import { parseEnv } from 'node:util';
7
- import { parse as parseToml } from 'smol-toml';
8
- import { databasePush } from '../runtime/scripts/api/supabase/database-push.mjs';
9
- import { isMigrationName, migrationStamp } from '../runtime/scripts/hfs/rules/database-migrations.mjs';
10
- import { CONFIG_FILE, MIGRATIONS_DIR } from '../runtime/scripts/hfs/rules/database-constants.mjs';
11
- import { failure, refusal } from '../stack/errors.mjs';
12
- import { assertLocalDatabase, boundSupabaseLauncher, nativeDatabaseEnvironment } from '../stack/target-binding.mjs';
13
-
14
- const configError = (reason) => refusal('DB_CONFIG_INVALID', reason);
15
- const encrypted = (value) => typeof value === 'string' && value.includes('encrypted:');
16
- const byteOrder = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
17
-
18
- /** Never quote a parser/tool diagnostic: config, env and child streams can contain credentials. */
19
- function readFile(file, optional = false) {
20
- try {
21
- const stat = fs.lstatSync(file);
22
- if (!stat.isFile() || stat.isSymbolicLink()) throw configError('local migration inputs must be regular files');
23
- return fs.readFileSync(file, 'utf8');
24
- } catch (error) {
25
- if (optional && error.code === 'ENOENT') return null;
26
- throw configError('local migration inputs must be readable regular files');
27
- }
28
- }
29
-
30
- function requireDirectory(directory) {
31
- try {
32
- const stat = fs.lstatSync(directory);
33
- if (stat.isDirectory() && !stat.isSymbolicLink()) return;
34
- } catch { /* The refusal below withholds filesystem diagnostics. */ }
35
- throw configError('local migration requires the existing app directories, without links');
36
- }
37
-
38
- function requirePlainConfig(value) {
39
- if (encrypted(value)) throw configError('local migration does not decrypt encrypted configuration');
40
- if (value && typeof value === 'object') for (const item of Object.values(value)) requirePlainConfig(item);
41
- }
42
-
43
- /** The pinned CLI searches these exact files; interpolation/escapes are held rather than partially interpreted. */
44
- function projectEnvironment(root, ambient) {
45
- const selected = ambient.SUPABASE_ENV || 'development';
46
- if (!/^[A-Za-z0-9_-]+$/.test(selected)) throw configError('local migration requires an unambiguous Supabase environment selector');
47
- const names = [`.env.${selected}.local`, ...(selected === 'test' ? [] : ['.env.local']), `.env.${selected}`, '.env'];
48
- const values = {};
49
- const files = [];
50
- for (const dir of [path.join(root, 'supabase'), root]) {
51
- for (const name of names) {
52
- const file = path.join(dir, name);
53
- const text = readFile(file, true);
54
- files.push([file, text]);
55
- if (text === null) continue;
56
- // Pinned godotenv expansion and double-quote unescaping can synthesize the encryption prefix.
57
- if (encrypted(text) || /[$\\]/.test(text)) throw configError('local migration requires literal unencrypted dotenv inputs');
58
- // Keep only the common literal assignment grammar shared by Node and the pinned CLI.
59
- for (const line of text.split(/\r?\n/)) {
60
- if (/^\s*(?:#.*)?$/.test(line)) continue;
61
- if (!/^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(?:"[^"\r\n]*"|'[^'\r\n]*'|[^'"#\r\n]*)(?:\s+#.*)?\s*$/.test(line)) {
62
- throw configError('local migration requires literal single-line dotenv assignments');
63
- }
64
- }
65
- let parsed;
66
- try { parsed = parseEnv(text); } catch { throw configError('local migration dotenv inputs are not readable assignments'); }
67
- for (const [key, value] of Object.entries(parsed)) {
68
- if (!Object.hasOwn(ambient, key) && !Object.hasOwn(values, key)) values[key] = value;
69
- }
70
- }
71
- }
72
- return { values: { ...values, ...ambient }, files };
73
- }
74
-
75
- function expanded(value, env) {
76
- if (typeof value !== 'string') return value;
77
- const reference = /^env\(([A-Za-z_][A-Za-z0-9_]*)\)$/.exec(value);
78
- const result = reference ? env[reference[1]] : value;
79
- if (typeof result !== 'string' || result.startsWith('env(')) throw configError('local migration configuration has an unresolved environment reference');
80
- return result;
81
- }
82
-
83
- const overridden = (name, value, env) => expanded(typeof env[name] === 'string' && env[name] !== '' ? env[name] : value, env);
84
-
85
- function targetIdentity(config, env) {
86
- const project = overridden('SUPABASE_PROJECT_ID', config.project_id, env);
87
- const portValue = overridden('SUPABASE_DB_PORT', config.db?.port, env);
88
- const port = typeof portValue === 'string' && /^\d+$/.test(portValue) ? Number(portValue) : portValue;
89
- const enabled = overridden('SUPABASE_DB_MIGRATIONS_ENABLED', config.db?.migrations?.enabled ?? true, env);
90
- if (typeof project !== 'string' || !/^[A-Za-z0-9_-]+$/.test(project) || !Number.isInteger(port) || port < 1 || port > 65535) {
91
- throw configError('local migration requires an explicit local project identity and database port');
92
- }
93
- if (enabled !== true && !(typeof enabled === 'string' && /^(?:1|t|T|true|TRUE|True)$/.test(enabled))) {
94
- throw configError('local migration requires migrations to be explicitly enabled by the effective configuration');
95
- }
96
- return { project, port };
97
- }
98
-
99
- /** Snapshot only in memory; no plaintext configuration, values or hashes reach the result. */
100
- function inputs(ctx, nativeTarget = null) {
101
- const root = path.resolve(ctx.cwd);
102
- requireDirectory(root);
103
- requireDirectory(path.join(root, 'supabase'));
104
- requireDirectory(path.join(root, MIGRATIONS_DIR));
105
- const appText = readFile(path.join(root, 'hfs.json'));
106
- let app;
107
- try { app = JSON.parse(appText); } catch { throw configError('local migration requires a readable app declaration'); }
108
- if (app?.kind !== 'app' || !['full', 'lite'].includes(app.edition ?? 'full')) throw configError('local migration requires a lite or full app');
109
- const configText = readFile(path.join(root, CONFIG_FILE));
110
- let config;
111
- try { config = parseToml(configText); } catch { throw configError('local migration requires valid Supabase TOML'); }
112
- requirePlainConfig(config);
113
- for (const value of Object.values(ctx.env)) if (encrypted(value)) throw configError('local migration does not decrypt encrypted environment values');
114
- const { values: env, files: envFiles } = projectEnvironment(root, ctx.env);
115
- if (nativeTarget && (config.experimental?.stack === true
116
- || env.SUPABASE_EXPERIMENTAL_STACK && !/^(?:0|false)$/i.test(env.SUPABASE_EXPERIMENTAL_STACK)
117
- || env.SUPABASE_CLI_BINARY_OVERRIDE)) throw configError('the bound local target requires the declared legacy Supabase backend and installed launcher');
118
- const launcher = nativeTarget ? boundSupabaseLauncher(root) : null;
119
- const target = targetIdentity(config, env);
120
- let names;
121
- try { names = fs.readdirSync(path.join(root, MIGRATIONS_DIR)).sort(byteOrder); } catch { throw configError('local migrations are not readable'); }
122
- // Match the pinned CLI's unanchored first-entry legacy-init skip, including nested names.
123
- const legacy = /([0-9]{14})_init\.sql/.exec(names[0] ?? '');
124
- if (legacy && Number(legacy[1]) < 20211209000000) throw refusal('DB_STAMP_ORDER', 'the local chain contains a legacy init migration the installed CLI skips');
125
- const migrations = [];
126
- const stamps = new Set();
127
- for (const name of names.filter((entry) => entry.endsWith('.sql'))) {
128
- const stamp = name.slice(0, name.indexOf('_'));
129
- if (!isMigrationName(name) || migrationStamp(stamp) === null) throw refusal('DB_MIGRATION_NAME', 'local migrations require valid UTC-stamped filenames');
130
- if (stamps.has(stamp)) throw refusal('DB_STAMP_COLLISION', 'local migrations contain a duplicate timestamp');
131
- stamps.add(stamp);
132
- migrations.push([name, readFile(path.join(root, MIGRATIONS_DIR, name))]);
133
- }
134
- const digest = createHash('sha256').update(JSON.stringify({ root, appText, configText, envFiles, env, target, migrations })).digest('hex');
135
- return { root, target, names: migrations.map(([name]) => name), digest, nativeTarget, launcher };
136
- }
137
-
138
- function unchanged(ctx, snapshot) {
139
- const current = inputs(ctx, snapshot.nativeTarget);
140
- if (current.digest !== snapshot.digest) throw failure('DB_FINAL_MISMATCH', 'local migration inputs changed during the operation');
141
- return current;
142
- }
143
-
144
- /** Only the specific Supabase owner can spawn; failures and malformed JSON never expose child output. */
145
- function push(ctx, snapshot, dryRun) {
146
- const env = snapshot.nativeTarget ? nativeDatabaseEnvironment(ctx.env, snapshot.nativeTarget,
147
- { 'SUPABASE_SERVICES_HOSTNAME': '127.0.0.1', 'SUPABASE_EXPERIMENTAL_STACK': '0' }) : ctx.env;
148
- const run = snapshot.nativeTarget ? (file, args, options) => {
149
- if (file !== process.execPath || args[0] !== snapshot.launcher) throw failure('DB_CLI_MISSING', 'the bound database launcher cannot fall back to a global tool');
150
- return ctx.spawn(file, args, options);
151
- } : ctx.spawn;
152
- const result = databasePush(snapshot.root, ['--local', '--skip-vault', '--workdir', snapshot.root, '--output-format', 'json',
153
- ...(snapshot.nativeTarget ? ['--profile', 'supabase'] : []), ...(dryRun ? ['--dry-run'] : ['--yes'])], { env, run });
154
- if (result?.error || result?.signal || result?.status !== 0) throw failure('DB_CLI_FAILED', 'the local Supabase migration command failed; database output was withheld');
155
- let payload;
156
- try { payload = JSON.parse(result.stdout); } catch { throw failure('DB_CLI_FAILED', 'the local migration command returned unreadable JSON'); }
157
- if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.hasOwn(payload, 'error') || payload._tag === 'Error' || payload.dryRun !== dryRun || typeof payload.upToDate !== 'boolean'
158
- || !Array.isArray(payload.migrations) || !payload.migrations.every((name) => typeof name === 'string')
159
- || !Array.isArray(payload.seeds) || payload.seeds.length !== 0 || !Array.isArray(payload.roles) || payload.roles.length !== 0) {
160
- throw failure('DB_PUSH_STRAY', 'the local migration command returned an unexpected result');
161
- }
162
- const pending = payload.migrations;
163
- const expected = snapshot.names.slice(snapshot.names.length - pending.length);
164
- if (pending.length > snapshot.names.length || pending.some((name, index) => name !== expected[index]) || payload.upToDate !== (pending.length === 0)) {
165
- throw failure('DB_PUSH_STRAY', 'the local migration result is not an ordered suffix of the authored chain');
166
- }
167
- return pending;
168
- }
169
-
170
- /** Preview by default; the caller admits execute only after the existing database arming check. No history repair, reset or linked fallback. */
171
- export function migrateLocalDatabase(ctx, { execute = false, nativeTarget = null } = {}) {
172
- const snapshot = inputs(ctx, nativeTarget);
173
- assertLocalDatabase(nativeTarget, snapshot.target);
174
- const pending = push(ctx, snapshot, true);
175
- unchanged(ctx, snapshot);
176
- let applied = [];
177
- if (execute && pending.length > 0) {
178
- const fresh = push(ctx, snapshot, true);
179
- unchanged(ctx, snapshot);
180
- if (fresh.some((name, index) => name !== pending[index]) || fresh.length !== pending.length) throw failure('DB_FINAL_MISMATCH', 'the local migration plan changed before apply');
181
- applied = push(ctx, snapshot, false);
182
- unchanged(ctx, snapshot);
183
- if (applied.length !== fresh.length || applied.some((name, index) => name !== fresh[index])) {
184
- throw failure('DB_FINAL_MISMATCH', 'the applied local migrations differ from the reviewed pending plan');
185
- }
186
- if (push(ctx, snapshot, true).length !== 0) throw failure('DB_FINAL_MISMATCH', 'the local migration version history is not fully applied');
187
- unchanged(ctx, snapshot);
188
- }
189
- return {
190
- ok: true,
191
- lines: [`target local project ${snapshot.target.project}`, `pending SQL ${pending.join(', ') || 'none'}`, execute && applied.length ? `applied ${applied.length} migration(s); local version history is current` : 'nothing applied'],
192
- data: { target: 'local', pending, pushed: applied, executed: execute && applied.length > 0, done: pending.length === 0 || execute },
193
- };
194
- }
package/db/reset.mjs DELETED
@@ -1,125 +0,0 @@
1
- // reset.mjs - linked reset requires explicit custody choices, a recent verified encrypted backup and the migration arming rule.
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { runVerb } from '../release/index.mjs';
6
- import { requireArmed } from './migrate.mjs';
7
- import { failure, refusal } from '../stack/errors.mjs';
8
- import { backupSession, backupArgs, backupRecipients, backupTool, encryptFile, queryRows, validateReceipt, runCli } from './backup-receipt.mjs';
9
-
10
- /** Metadata fingerprints allow verification without printing or retaining rows, object keys or auth identities in the result. */
11
- function inventory(session, schema) {
12
- const tables = queryRows(session, `select tablename from pg_catalog.pg_tables where schemaname = '${schema}' and tablename not in ('schema_migrations', 'migrations') order by tablename;`);
13
- return tables.map(({ tablename }) => {
14
- if (typeof tablename !== 'string' || !/^[a-z_][a-z0-9_]*$/u.test(tablename)) throw failure('DB_RESET_VERIFY_FAILED', 'invalid preservation table inventory');
15
- const rows = queryRows(session, `begin read only; select count(*)::text as count, md5(coalesce(string_agg(md5(row_to_json(t)::text), '' order by md5(row_to_json(t)::text)), '')) as hash from "${schema}"."${tablename}" t; commit;`);
16
- if (rows.length !== 1 || !/^\d+$/u.test(String(rows[0].count)) || !/^[a-f0-9]{32}$/u.test(rows[0].hash ?? '')) throw failure('DB_RESET_VERIFY_FAILED', 'invalid preservation fingerprint');
17
- return { table: tablename, count: String(rows[0].count), hash: rows[0].hash };
18
- });
19
- }
20
-
21
- /** age reads identity bytes on stdin or the configured identity file; neither identity nor SQL is placed in argv or diagnostics. */
22
- function decryptPart(ctx, encrypted, plaintext) {
23
- const key = ctx.env.SOPS_AGE_KEY;
24
- const identity = key ? '-' : ctx.env.SOPS_AGE_KEY_FILE;
25
- if (!identity) throw refusal('DB_RESET_IDENTITY_MISSING', '--auth/--storage keep needs SOPS_AGE_KEY or SOPS_AGE_KEY_FILE to restore the temporary encrypted part');
26
- backupTool(ctx, 'age', ['--decrypt', '--identity', identity, '-o', plaintext, encrypted], key ? { input: `${key}\n` } : {});
27
- }
28
-
29
- function preservePart(session, scratch, schema, recipients) {
30
- const before = inventory(session, schema);
31
- const plain = path.join(scratch, `${schema}.sql`);
32
- const encrypted = `${plain}.age`;
33
- runCli(session, ['db', 'dump', '--data-only', '--schema', schema, '--exclude', `${schema}.schema_migrations,${schema}.migrations`, '--file', plain, ...backupArgs(session)]);
34
- encryptFile(session.ctx, plain, encrypted, recipients);
35
- fs.rmSync(plain, { force: true });
36
- // Prove the selected identity can restore this exact part before the destructive call.
37
- decryptPart(session.ctx, encrypted, plain);
38
- fs.rmSync(plain, { force: true });
39
- return { schema, before, encrypted, plain };
40
- }
41
-
42
- function restorePart(session, part) {
43
- // Supabase retains some managed tables. Replace their data to avoid duplicate users, buckets or migration-independent metadata.
44
- decryptPart(session.ctx, part.encrypted, part.plain);
45
- const tables = part.before.map((row) => `"${part.schema}"."${row.table}"`);
46
- const source = fs.readFileSync(part.plain, 'utf8');
47
- fs.writeFileSync(part.plain, `begin;\nset constraints all deferred;\n${tables.length ? `truncate ${tables.join(', ')} cascade;\n` : ''}${source}\ncommit;\n`, { mode: 0o600 });
48
- try { runCli(session, ['db', 'query', '--file', part.plain, '--output', 'json', '--agent', 'no', ...backupArgs(session)]); }
49
- finally { fs.rmSync(part.plain, { force: true }); }
50
- if (JSON.stringify(inventory(session, part.schema)) !== JSON.stringify(part.before)) throw failure('DB_RESET_VERIFY_FAILED', `${part.schema} data differs after restore; use the full backup receipt for recovery`);
51
- }
52
-
53
- function expectedHead(session) {
54
- const dir = path.join(session.ctx.cwd, session.database.migrationsDir ?? 'supabase/migrations');
55
- let expected;
56
- try { expected = fs.readdirSync(dir).filter((name) => /^\d{14}_[A-Za-z0-9_-]+\.sql$/u.test(name)).map((name) => name.slice(0, 14)).sort(); }
57
- catch { throw refusal('DB_MIGRATIONS_MISSING', 'migration directory is not readable'); }
58
- if (!expected.length || new Set(expected).size !== expected.length) throw refusal('DB_MIGRATIONS_MISSING', 'migration versions must be present and unique');
59
- return expected;
60
- }
61
-
62
- function verifyHead(session) {
63
- const expected = expectedHead(session);
64
- const actual = queryRows(session, 'select version from supabase_migrations.schema_migrations order by version;').map((row) => row.version);
65
- if (JSON.stringify(expected) !== JSON.stringify(actual)) throw failure('DB_RESET_HEAD_MISMATCH', 'reset migration history differs from repository migration versions');
66
- return expected;
67
- }
68
-
69
- /** Dry run validates custody and prints a plan; execute preserves chosen managed data, resets, restores and verifies the exact migration head. */
70
- function resetDatabase(ctx, options = {}) {
71
- if (options.linked !== true || options.local || options.urlEnv !== undefined) throw refusal('DB_RESET_LINKED_REQUIRED', 'reset requires --linked; use npm run db:reset for a local database');
72
- if (!['keep', 'drop'].includes(options.auth) || !['keep', 'drop'].includes(options.storage)) throw refusal('DB_RESET_CHOICE_MISSING', 'explicit --auth keep|drop and --storage keep|drop are required');
73
- const session = backupSession(ctx, options);
74
- const receipt = validateReceipt(session, options.backup ? path.resolve(ctx.cwd, options.backup) : undefined);
75
- const lines = [`target: linked project ${session.target.ref}`, `backup: verified encrypted receipt from ${receipt.at}`,
76
- options.auth === 'keep' ? 'auth: encrypt all auth data tables before reset, restore in a transaction and verify' : 'auth: users and identities are deleted',
77
- options.storage === 'keep' ? 'storage: preserve and verify buckets and objects metadata' : 'storage: empty storage.objects metadata; files in the storage backend are not deleted by SQL',
78
- 'reset: supabase db reset --linked; verify migration versions against repository'];
79
- if (!options.execute) return { ok: true, lines: [...lines, 'dry run: nothing changed'], data: { executed: false, auth: options.auth, storage: options.storage, ref: session.target.ref } };
80
- requireArmed(ctx);
81
- // Refuse unsupported CLI file execution before resetting: SQL containing identity data must never travel in argv.
82
- if (options.auth === 'keep' || options.storage === 'keep') {
83
- const help = runCli(session, ['db', 'query', '--help']).text;
84
- if (!help.includes('--file')) throw refusal('DB_RESET_QUERY_FILE_UNSUPPORTED', 'the Supabase CLI must support db query --file for transactional restore');
85
- }
86
- expectedHead(session);
87
- const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-db-reset-'));
88
- const parts = [];
89
- let resetStarted = false;
90
- let complete = false;
91
- try {
92
- const keep = ['auth', 'storage'].filter((schema) => options[schema] === 'keep');
93
- const recipients = keep.length ? backupRecipients(ctx.cwd) : [];
94
- for (const schema of keep) parts.push(preservePart(session, scratch, schema, recipients));
95
- resetStarted = true;
96
- runCli(session, ['db', 'reset', '--yes', ...backupArgs(session)]);
97
- for (const part of parts) restorePart(session, part);
98
- if (options.auth === 'drop') {
99
- const rows = queryRows(session, 'select (select count(*) from auth.users)::text as users, (select count(*) from auth.identities)::text as identities;');
100
- if (rows.length !== 1 || String(rows[0].users) !== '0' || String(rows[0].identities) !== '0') throw failure('DB_RESET_VERIFY_FAILED', 'auth users or identities remain after reset');
101
- }
102
- if (options.storage === 'drop') {
103
- runCli(session, ['db', 'query', 'begin; delete from storage.objects; commit;', '--output', 'json', '--agent', 'no', ...backupArgs(session)]);
104
- const rows = queryRows(session, 'select count(*)::text as count from storage.objects;');
105
- if (rows.length !== 1 || String(rows[0].count) !== '0') throw failure('DB_RESET_VERIFY_FAILED', 'storage.objects metadata was not emptied');
106
- }
107
- const versions = verifyHead(session);
108
- complete = true;
109
- return { ok: true, lines: [...lines, 'reset completed; managed data choices and migration head verified'], data: { executed: true, auth: options.auth, storage: options.storage, ref: session.target.ref, versions } };
110
- } finally {
111
- // Preserve encrypted recovery parts after a reset/restore failure; remove every plaintext file on every exit.
112
- for (const name of fs.readdirSync(scratch)) if (!name.endsWith('.age')) fs.rmSync(path.join(scratch, name), { force: true });
113
- if (complete || !resetStarted) fs.rmSync(scratch, { recursive: true, force: true });
114
- else ctx.stderr(`DB_RESET_RECOVERY_PARTS: encrypted recovery parts retained at ${scratch}\n`);
115
- }
116
- }
117
-
118
- export function dbResetMain(argv, io = {}) {
119
- return runVerb({ label: 'starci app db reset', name: 'db reset', code: 'DB_USAGE', io, argv,
120
- spec: { boolean: ['--linked', '--execute', '--json', '--local'], value: ['--auth', '--storage', '--backup', '--url-env'] },
121
- run: (ctx, flags, positional) => {
122
- if (positional.length) throw refusal('DB_USAGE', 'reset takes flags only');
123
- return resetDatabase(ctx, { linked: flags.linked, local: flags.local, urlEnv: flags['url-env'], execute: flags.execute, auth: flags.auth, storage: flags.storage, backup: flags.backup });
124
- } });
125
- }