@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,4 +1,5 @@
1
- import type { SupabaseOptions } from "@modules/integrations/supabase"
1
+ import type { PassportOptions } from "@modules/integrations/passport"
2
+ import type { ObjectStorageOptions } from "@modules/integrations/object-storage"
2
3
  import type { ServerOptions } from "@modules/platform/config"
3
4
  import type { DatabaseConnectionOptions } from "@modules/platform/database"
4
5
  import type { HttpSecurityOptions } from "@modules/platform/http-security"
@@ -9,8 +10,10 @@ export interface ApiOptions {
9
10
  readonly server: ServerOptions
10
11
  /** The origin allowlist and rate limits. */
11
12
  readonly httpSecurity: HttpSecurityOptions
12
- /** The least-privilege PostgreSQL connection to the Supabase project. */
13
+ /** The least-privilege PostgreSQL connection to the app's own database. */
13
14
  readonly database: DatabaseConnectionOptions
14
- /** Supabase Auth, Storage and token-verification configuration. */
15
- readonly supabase: SupabaseOptions
15
+ /** Google OAuth, the app's own access tokens and the refresh cookie. */
16
+ readonly passport: PassportOptions
17
+ /** S3-compatible object storage. */
18
+ readonly objectStorage: ObjectStorageOptions
16
19
  }
@@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"
2
2
  import type { DynamicModule } from "@nestjs/common"
3
3
  import { APP_FILTER, APP_GUARD } from "@nestjs/core"
4
4
  import { AuthGuard, CredentialGuard } from "@modules/domain/identity"
5
- import { createSupabaseAccessTokenVerifier } from "@modules/integrations/supabase"
5
+ import { tokenKit } from "@modules/integrations/passport"
6
6
  import { ErrorsFilter } from "@modules/platform/errors"
7
7
  import { OriginGuard, RateLimitGuard } from "@modules/platform/http-security"
8
8
  import { platformModules } from "./compose/platform"
@@ -18,18 +18,18 @@ import type { ApiOptions } from "./api.options"
18
18
  export class AppModule {
19
19
  /** Builds the module from options parsed once at boot; nothing here reads the environment. */
20
20
  static register(options: ApiOptions): DynamicModule {
21
- const verifyAccessToken = createSupabaseAccessTokenVerifier(options.supabase)
21
+ const tokens = tokenKit(options.passport.tokens)
22
22
  return {
23
23
  module: AppModule,
24
24
  imports: [
25
25
  ...platformModules(options),
26
- ...domainModules(verifyAccessToken),
26
+ ...domainModules(tokens.verify),
27
27
  ...systemModules(),
28
28
  ...apiModules(),
29
29
  ...webhooksModules(),
30
30
  ],
31
31
  providers: [
32
- ...integrationProviders(options.supabase, verifyAccessToken),
32
+ ...integrationProviders(options, tokens.verify),
33
33
  { provide: APP_FILTER, useClass: ErrorsFilter },
34
34
  { provide: APP_GUARD, useClass: RateLimitGuard },
35
35
  { provide: APP_GUARD, useClass: OriginGuard },
@@ -1,10 +1,10 @@
1
1
  import type { DynamicModule, Type } from "@nestjs/common"
2
2
  import { IdentityModule } from "@modules/domain/identity"
3
3
  import { LivenessModule } from "@modules/domain/liveness"
4
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
4
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
5
5
 
6
- /** The domain capabilities of the API, with the app's verified Supabase identity adapter. */
7
- export const domainModules = (verifyAccessToken: VerifySupabaseAccessToken): Array<Type<unknown> | DynamicModule> => [
6
+ /** The domain capabilities of the API, with the verifier of the app's own access tokens. */
7
+ export const domainModules = (verifyAccessToken: VerifyAccessToken): Array<Type<unknown> | DynamicModule> => [
8
8
  IdentityModule.register({ isGlobal: true, verifyAccessToken }),
9
9
  LivenessModule.register({ isGlobal: true }),
10
10
  ]
@@ -1,16 +1,12 @@
1
1
  import type { Provider } from "@nestjs/common"
2
- import {
3
- SUPABASE_ACCESS_TOKEN_VERIFIER,
4
- SUPABASE_ADMIN_CLIENT,
5
- createSupabaseAdminClient,
6
- } from "@modules/integrations/supabase"
7
- import type { SupabaseOptions, VerifySupabaseAccessToken } from "@modules/integrations/supabase"
2
+ import { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS } from "@modules/integrations/passport"
3
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
4
+ import { OBJECT_STORAGE, createObjectStorage } from "@modules/integrations/object-storage"
5
+ import type { ApiOptions } from "../api.options"
8
6
 
9
- /** The Supabase integration ports bound once for this app. */
10
- export const integrationProviders = (
11
- options: SupabaseOptions,
12
- verifyAccessToken: VerifySupabaseAccessToken,
13
- ): Array<Provider> => [
14
- { provide: SUPABASE_ADMIN_CLIENT, useValue: createSupabaseAdminClient(options) },
15
- { provide: SUPABASE_ACCESS_TOKEN_VERIFIER, useValue: verifyAccessToken },
7
+ /** The passport and object-storage integration ports bound once for this app. */
8
+ export const integrationProviders = (options: ApiOptions, verifyAccessToken: VerifyAccessToken): Array<Provider> => [
9
+ { provide: PASSPORT_OPTIONS, useValue: options.passport },
10
+ { provide: OWN_TOKEN_VERIFIER, useValue: verifyAccessToken },
11
+ { provide: OBJECT_STORAGE, useValue: createObjectStorage(options.objectStorage) },
16
12
  ]
@@ -1,10 +1,11 @@
1
1
  import type { DynamicModule } from "@nestjs/common"
2
2
  import { IDENTITY_ERROR_KINDS, IDENTITY_MESSAGES } from "@modules/domain/identity"
3
- import { SUPABASE_ERROR_KINDS } from "@modules/integrations/supabase"
3
+ import { PASSPORT_ERROR_KINDS } from "@modules/integrations/passport"
4
4
  import { ClockModule } from "@modules/platform/clock"
5
5
  import { CONFIG_ERROR_KINDS } from "@modules/platform/config"
6
6
  import { CqrsModule } from "@modules/platform/cqrs"
7
7
  import { DATABASE_ERROR_KINDS, DatabaseModule } from "@modules/platform/database"
8
+ import { PRIMARY_ENTITIES } from "@modules/platform/database/entities"
8
9
  import { ERRORS_MESSAGES, ErrorsModule } from "@modules/platform/errors"
9
10
  import { HTTP_SECURITY_ERROR_KINDS, HTTP_SECURITY_MESSAGES, HttpSecurityModule } from "@modules/platform/http-security"
10
11
  import { I18nModule } from "@modules/platform/i18n"
@@ -37,7 +38,7 @@ export const platformModules = (options: ApiOptions): Array<DynamicModule> => [
37
38
  DATABASE_ERROR_KINDS,
38
39
  HTTP_SECURITY_ERROR_KINDS,
39
40
  IDENTITY_ERROR_KINDS,
40
- SUPABASE_ERROR_KINDS,
41
+ PASSPORT_ERROR_KINDS,
41
42
  ],
42
43
  }),
43
44
  CqrsModule.register({ isGlobal: true }),
@@ -48,5 +49,6 @@ export const platformModules = (options: ApiOptions): Array<DynamicModule> => [
48
49
  DatabaseModule.register({
49
50
  isGlobal: true,
50
51
  connections: [options.database],
52
+ entities: PRIMARY_ENTITIES,
51
53
  }),
52
54
  ]
@@ -5,8 +5,8 @@ import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
5
5
  export interface CliEnv {
6
6
  /** The logical connection name declared in `hfs.json`. */
7
7
  readonly name: string
8
- /** The Supabase PostgreSQL provider. */
9
- readonly provider: "supabase"
8
+ /** The PostgreSQL provider. */
9
+ readonly provider: "postgres"
10
10
  /** The least-privilege database connection URL, read through the secret source. */
11
11
  readonly url: Secret
12
12
  /** The isolated PostgreSQL schema. */
@@ -1,16 +1,17 @@
1
1
  import type { DynamicModule } from "@nestjs/common"
2
2
  import { ClockModule } from "@modules/platform/clock"
3
3
  import { DatabaseModule } from "@modules/platform/database"
4
+ import { PRIMARY_ENTITIES } from "@modules/platform/database/entities"
4
5
  import { IdsModule } from "@modules/platform/ids"
5
6
  import { HTTP_ERROR_KINDS, HTTP_MESSAGES, HttpModule } from "@modules/platform/http"
6
7
  import { LoggingModule } from "@modules/platform/logging"
7
8
  import type { CliAppOptions } from "../cli.options"
8
9
 
9
- /** The shared clock, logger and least-privilege connection used by Supabase CLI commands. */
10
+ /** The shared clock, logger and least-privilege connection used by the operator commands. */
10
11
  export const platformModules = (options: CliAppOptions): Array<DynamicModule> => [
11
12
  ClockModule.register({ isGlobal: true }),
12
13
  IdsModule.register({ isGlobal: true }),
13
14
  LoggingModule.register({ isGlobal: true }),
14
15
  HttpModule.register({ isGlobal: true }),
15
- DatabaseModule.register({ isGlobal: true, connections: options.connections }),
16
+ DatabaseModule.register({ isGlobal: true, connections: options.connections, entities: PRIMARY_ENTITIES }),
16
17
  ]
@@ -5,8 +5,8 @@ import type { Request } from "express"
5
5
  import { mock } from "@starci/jest-preset"
6
6
  import type { Principal } from "@modules/platform/cqrs"
7
7
  import { ok, refused } from "@modules/platform/primitives"
8
- import { SupabaseErrorCode } from "@modules/integrations/supabase"
9
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
8
+ import { PassportErrorCode } from "@modules/integrations/passport"
9
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
10
10
  import { AuthGuard } from "./auth.guard"
11
11
  import type { CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
12
12
  import { IdentityErrorCode } from "./errors/identity.error"
@@ -26,7 +26,7 @@ interface GuardRequest {
26
26
  }
27
27
 
28
28
  const build = async (metadata: PublicMetadata | undefined, credential?: CredentialMetadata) => {
29
- const verifier = mock<{ verify: VerifySupabaseAccessToken }>()
29
+ const verifier = mock<{ verify: VerifyAccessToken }>()
30
30
  const reflector = mock<Reflector>()
31
31
  reflector.getAllAndOverride.mockImplementation((key: unknown) =>
32
32
  key === CREDENTIAL_KEY ? credential : key === PUBLIC_KEY ? metadata : undefined,
@@ -129,7 +129,7 @@ describe("AuthGuard credential doors", () => {
129
129
 
130
130
  it("refuses a door that is not public when the token fails verification", async () => {
131
131
  const { guard, verify } = await build(undefined)
132
- verify.mockResolvedValue(refused(SupabaseErrorCode.AccessTokenInvalid))
132
+ verify.mockResolvedValue(refused(PassportErrorCode.AccessTokenInvalid))
133
133
  const { context, request } = contextOf("bearer forged-token")
134
134
 
135
135
  await expect(guard.canActivate(context)).rejects.toThrow(IdentityErrorCode.Unauthenticated)
@@ -1,7 +1,7 @@
1
1
  import { Injectable } from "@nestjs/common"
2
2
  import type { CanActivate, ExecutionContext } from "@nestjs/common"
3
3
  import type { Reflector } from "@nestjs/core"
4
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
4
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
5
5
  import { InjectReflector } from "@modules/platform/composition"
6
6
  import type { Principal } from "@modules/platform/cqrs"
7
7
  import { requestOf } from "@modules/platform/http-security"
@@ -20,12 +20,12 @@ const bearerToken = (authorization: string | undefined): string | undefined => {
20
20
  @Injectable()
21
21
  /**
22
22
  * The fourth app guard and default-deny gate: a credential door requires the matching principal from `CredentialGuard`,
23
- * a public door states its reason, and every other door requires a verified Supabase principal. It never calls another guard.
23
+ * a public door states its reason, and every other door requires a principal verified from the app's own access token. It never calls another guard.
24
24
  */
25
25
  export class AuthGuard implements CanActivate {
26
26
  constructor(
27
27
  @InjectReflector() private readonly reflector: Reflector,
28
- @InjectAccessTokenVerifier() private readonly verifyAccessToken: VerifySupabaseAccessToken,
28
+ @InjectAccessTokenVerifier() private readonly verifyAccessToken: VerifyAccessToken,
29
29
  ) {}
30
30
 
31
31
  /** Verifies session tokens only for doors without public or credential metadata, then applies the admission policy. */
@@ -6,7 +6,7 @@ declare module "express-serve-static-core" {
6
6
  interface Request {
7
7
  /** The identity `CredentialGuard` authenticated on a door marked `@Credential`; never a session principal. */
8
8
  credentialPrincipal?: CredentialPrincipal
9
- /** The authenticated Supabase caller, when the door is not public. */
9
+ /** The authenticated caller, when the door is not public. */
10
10
  principal?: Principal
11
11
  }
12
12
  }
@@ -1,26 +1,33 @@
1
- import { SetMetadata, createParamDecorator } from "@nestjs/common"
1
+ import { SetMetadata, applyDecorators, createParamDecorator } from "@nestjs/common"
2
2
  import type { ExecutionContext } from "@nestjs/common"
3
3
  import { injector } from "@modules/platform/composition"
4
- import type { TypedParameterDecorator } from "@modules/platform/composition"
5
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
6
- import { requestOf } from "@modules/platform/http-security"
4
+ import type { DoorDecorator, TypedParameterDecorator } from "@modules/platform/composition"
5
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
6
+ import { ServerToServer, requestOf } from "@modules/platform/http-security"
7
7
  import type { Principal } from "@modules/platform/cqrs"
8
8
  import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
9
9
  import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
10
+ import { PublicReason } from "./identity.contracts"
10
11
  import type { PublicMetadata } from "./identity.contracts"
11
12
 
12
13
  /** Token of the access-token verifier the composition root hands to identity. */
13
14
  export const ACCESS_TOKEN_VERIFIER: unique symbol = Symbol("domain.identity.access-token-verifier")
14
15
 
15
- /** Injects the access-token verifier identity was registered with. Parameter type: VerifySupabaseAccessToken. */
16
- export const InjectAccessTokenVerifier = (): TypedParameterDecorator<VerifySupabaseAccessToken> =>
17
- injector<VerifySupabaseAccessToken>(ACCESS_TOKEN_VERIFIER)
16
+ /** Injects the access-token verifier identity was registered with. Parameter type: VerifyAccessToken. */
17
+ export const InjectAccessTokenVerifier = (): TypedParameterDecorator<VerifyAccessToken> =>
18
+ injector<VerifyAccessToken>(ACCESS_TOKEN_VERIFIER)
18
19
 
19
20
  /** Metadata key of `@Public`. */
20
21
  export const PUBLIC_KEY = "domain.identity.public"
21
22
 
22
- /** Opens a door to anonymous callers, stating why; every other door needs a credential or a verified Supabase session. */
23
- export const Public = (metadata: PublicMetadata): ReturnType<typeof SetMetadata> => SetMetadata(PUBLIC_KEY, metadata)
23
+ /**
24
+ * Opens a door to anonymous callers, stating why; every other door needs a credential or a verified session.
25
+ * A signed webhook proves its caller by its signature, so it owes no browser origin.
26
+ */
27
+ export const Public = (metadata: PublicMetadata): DoorDecorator =>
28
+ metadata.reason === PublicReason.SignedWebhook
29
+ ? applyDecorators(SetMetadata(PUBLIC_KEY, metadata), ServerToServer())
30
+ : applyDecorators(SetMetadata(PUBLIC_KEY, metadata))
24
31
 
25
32
  /** Metadata key of `@Credential`. */
26
33
  export const CREDENTIAL_KEY: unique symbol = Symbol("domain.identity.credential")
@@ -32,11 +39,14 @@ export const CREDENTIAL_AUTHENTICATOR: unique symbol = Symbol("domain.identity.c
32
39
  export const InjectCredentialAuthenticator = (): TypedParameterDecorator<CredentialAuthenticator> =>
33
40
  injector<CredentialAuthenticator>(CREDENTIAL_AUTHENTICATOR)
34
41
 
35
- /** Marks a door authenticated by its own credential; `CredentialGuard` authenticates it and `AuthGuard` admits it. */
36
- export const Credential = (metadata: CredentialMetadata): ReturnType<typeof SetMetadata> =>
37
- SetMetadata(CREDENTIAL_KEY, metadata)
42
+ /**
43
+ * Marks a door authenticated by its own credential; `CredentialGuard` authenticates it and `AuthGuard` admits it. The
44
+ * credential travels in a header no browser attaches by itself, so the door owes no browser origin.
45
+ */
46
+ export const Credential = (metadata: CredentialMetadata): DoorDecorator =>
47
+ applyDecorators(SetMetadata(CREDENTIAL_KEY, metadata), ServerToServer())
38
48
 
39
- /** The authenticated Supabase caller established by the default-deny guard. */
49
+ /** The authenticated caller established by the default-deny guard. */
40
50
  export const CurrentPrincipal = createParamDecorator((_data: unknown, context: ExecutionContext): Principal => {
41
51
  const principal = requestOf(context).principal
42
52
  if (!principal) throw new IdentityError({ code: IdentityErrorCode.Unauthenticated })
@@ -1,11 +1,11 @@
1
1
  import type { Type } from "@nestjs/common"
2
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
2
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
3
3
  import type { CredentialAuthenticator } from "./credential.contracts"
4
4
 
5
5
  /** The identity capability's credential and session verification options. */
6
6
  export interface IdentityOptions {
7
7
  /** The app-owned authenticator of alternate credentials; a credential door fails closed while it is absent. */
8
8
  readonly credentialAuthenticator?: Type<CredentialAuthenticator>
9
- /** Cryptographically verifies a Supabase access token before it can become a principal. */
10
- readonly verifyAccessToken: VerifySupabaseAccessToken
9
+ /** Cryptographically verifies one of the app's own access tokens before it can become a principal. */
10
+ readonly verifyAccessToken: VerifyAccessToken
11
11
  }
@@ -0,0 +1,4 @@
1
+ export { createObjectStorage } from "./object-storage.client"
2
+ export { parseObjectStorageConfig } from "./object-storage.config"
3
+ export { InjectObjectStorage, OBJECT_STORAGE } from "./object-storage.decorators"
4
+ export type { ObjectStorage, PresignedRequest, ObjectStorageOptions } from "./object-storage.options"
@@ -0,0 +1,22 @@
1
+ import { Secret } from "@modules/platform/config"
2
+ import { createObjectStorage } from "./object-storage.client"
3
+
4
+ const options = {
5
+ endpoint: "http://127.0.0.1:9000",
6
+ region: "us-east-1",
7
+ bucket: "app",
8
+ accessKeyId: new Secret("access"),
9
+ secretAccessKey: new Secret("secret"),
10
+ presignTtlSeconds: 300,
11
+ }
12
+
13
+ describe("createObjectStorage", () => {
14
+ it("presigns a path-style PUT of one key in the bucket, valid for the configured lifetime", async () => {
15
+ const storage = createObjectStorage(options, () => new Date("2026-10-29T00:00:00.000Z"))
16
+ const request = await storage.presignPut("users/u1/avatar.png", "image/png")
17
+
18
+ expect(new URL(request.url).pathname).toBe("/app/users/u1/avatar.png")
19
+ expect(new URL(request.url).searchParams.get("X-Amz-Expires")).toBe("300")
20
+ expect(request.expiresAt).toBe("2026-10-29T00:05:00.000Z")
21
+ })
22
+ })
@@ -0,0 +1,25 @@
1
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
2
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
3
+ import type { ObjectStorage, PresignedRequest, ObjectStorageOptions } from "./object-storage.options"
4
+
5
+ /** The one S3 client of the app over S3-compatible storage (path-style, as MinIO serves it). */
6
+ export const createObjectStorage = (options: ObjectStorageOptions, now: () => Date = () => new Date()): ObjectStorage => {
7
+ const client = new S3Client({
8
+ endpoint: options.endpoint,
9
+ region: options.region,
10
+ forcePathStyle: true,
11
+ credentials: { accessKeyId: options.accessKeyId.reveal(), secretAccessKey: options.secretAccessKey.reveal() },
12
+ })
13
+ const expiresAt = (): string => new Date(now().getTime() + options.presignTtlSeconds * 1000).toISOString()
14
+ const presign = async (command: PutObjectCommand | GetObjectCommand): Promise<PresignedRequest> => ({
15
+ url: await getSignedUrl(client, command, { expiresIn: options.presignTtlSeconds }),
16
+ expiresAt: expiresAt(),
17
+ })
18
+ return {
19
+ presignPut: (key, contentType) => presign(new PutObjectCommand({ Bucket: options.bucket, Key: key, ContentType: contentType })),
20
+ presignGet: (key) => presign(new GetObjectCommand({ Bucket: options.bucket, Key: key })),
21
+ remove: async (key) => {
22
+ await client.send(new DeleteObjectCommand({ Bucket: options.bucket, Key: key }))
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ import type { EnvSource } from "@modules/platform/config"
2
+ import type { ObjectStorageOptions } from "./object-storage.options"
3
+
4
+ /** Reads the object storage configuration with no credential or endpoint defaults; secrets read `<KEY>_FILE` first. */
5
+ export const parseObjectStorageConfig = (env: EnvSource): ObjectStorageOptions => ({
6
+ endpoint: env.url("STORAGE_ENDPOINT"),
7
+ region: env.string("STORAGE_REGION"),
8
+ bucket: env.string("STORAGE_BUCKET"),
9
+ accessKeyId: env.secret("STORAGE_ACCESS_KEY_ID"),
10
+ secretAccessKey: env.secret("STORAGE_SECRET_ACCESS_KEY"),
11
+ presignTtlSeconds: env.int("STORAGE_PRESIGN_TTL_SECONDS", 300),
12
+ })
@@ -0,0 +1,9 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { ObjectStorage } from "./object-storage.options"
4
+
5
+ /** Token of the object storage port. */
6
+ export const OBJECT_STORAGE: unique symbol = Symbol("integrations.object-storage")
7
+
8
+ /** Injects the object storage port. Parameter type: ObjectStorage. */
9
+ export const InjectObjectStorage = (): TypedParameterDecorator<ObjectStorage> => injector<ObjectStorage>(OBJECT_STORAGE)
@@ -0,0 +1,35 @@
1
+ import type { Secret } from "@modules/platform/config"
2
+
3
+ /** S3-compatible object storage (MinIO in lite) parsed once at API boot. */
4
+ export interface ObjectStorageOptions {
5
+ /** The S3 endpoint origin. */
6
+ readonly endpoint: string
7
+ /** The region the endpoint signs for. */
8
+ readonly region: string
9
+ /** The bucket of the app's objects. */
10
+ readonly bucket: string
11
+ /** The access key id. */
12
+ readonly accessKeyId: Secret
13
+ /** The secret access key. */
14
+ readonly secretAccessKey: Secret
15
+ /** The lifetime of a presigned URL in seconds. */
16
+ readonly presignTtlSeconds: number
17
+ }
18
+
19
+ /** One presigned request: the URL the browser calls and when it stops working. */
20
+ export interface PresignedRequest {
21
+ /** The presigned URL. */
22
+ readonly url: string
23
+ /** The expiry instant, ISO-8601. */
24
+ readonly expiresAt: string
25
+ }
26
+
27
+ /** The object storage port: presigned PUT and GET of one key and a server-side delete. */
28
+ export interface ObjectStorage {
29
+ /** A presigned PUT of `key` with its content type fixed. */
30
+ presignPut(key: string, contentType: string): Promise<PresignedRequest>
31
+ /** A presigned GET of `key`. */
32
+ presignGet(key: string): Promise<PresignedRequest>
33
+ /** Deletes `key`. */
34
+ remove(key: string): Promise<void>
35
+ }
@@ -0,0 +1,22 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Codes of the passport integration. */
5
+ export enum PassportErrorCode {
6
+ /** The access token failed signature or required-claim verification. */
7
+ AccessTokenInvalid = "PASSPORT_ACCESS_TOKEN_INVALID",
8
+ /** The email and password do not match an account. */
9
+ CredentialsRefused = "PASSPORT_CREDENTIALS_REFUSED",
10
+ /** The OAuth state or PKCE verifier of a callback does not match the request that started it. */
11
+ OAuthStateInvalid = "PASSPORT_OAUTH_STATE_INVALID",
12
+ }
13
+
14
+ /** How each passport integration code travels. */
15
+ export const PASSPORT_ERROR_KINDS: Record<PassportErrorCode, ErrorKind> = {
16
+ [PassportErrorCode.AccessTokenInvalid]: "unauthenticated",
17
+ [PassportErrorCode.CredentialsRefused]: "unauthenticated",
18
+ [PassportErrorCode.OAuthStateInvalid]: "unauthenticated",
19
+ }
20
+
21
+ /** The one error class of the passport integration. */
22
+ export class PassportError extends DomainError<PassportErrorCode> {}
@@ -0,0 +1,12 @@
1
+ export { parsePassportConfig } from "./passport.config"
2
+ export { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS, PASSWORD_ACCOUNTS } from "./passport.decorators"
3
+ export { PASSPORT_ERROR_KINDS, PassportErrorCode } from "./errors/passport.error"
4
+ export { createTokenKit, tokenKit } from "./passport.tokens"
5
+ export type { TokenKit } from "./passport.tokens"
6
+ export { hashPassword, verifyPassword } from "./passport.password"
7
+ export { clearRefreshCookie, newRefreshToken, refreshTokenHash, setRefreshCookie } from "./passport.cookie"
8
+ export { GoogleStrategy } from "./strategies/google.strategy"
9
+ export type { GoogleIdentity } from "./strategies/google.strategy"
10
+ export { LocalStrategy } from "./strategies/local.strategy"
11
+ export type { FindPasswordAccount, PasswordAccount } from "./strategies/local.strategy"
12
+ export type { OwnPrincipal, PassportOptions, VerifyAccessToken } from "./passport.options"
@@ -0,0 +1,23 @@
1
+ import type { EnvSource } from "@modules/platform/config"
2
+ import type { PassportOptions } from "./passport.options"
3
+
4
+ /** Reads the passport configuration with no credential or localhost defaults; secrets read `<KEY>_FILE` first. */
5
+ export const parsePassportConfig = (env: EnvSource): PassportOptions => ({
6
+ google: {
7
+ clientId: env.string("GOOGLE_CLIENT_ID"),
8
+ clientSecret: env.secret("GOOGLE_CLIENT_SECRET"),
9
+ callbackUrl: env.url("GOOGLE_CALLBACK_URL"),
10
+ },
11
+ tokens: {
12
+ issuer: env.url("AUTH_JWT_ISSUER"),
13
+ audience: env.string("AUTH_JWT_AUDIENCE"),
14
+ privateKey: env.secret("AUTH_JWT_PRIVATE_KEY"),
15
+ keyId: env.string("AUTH_JWT_KEY_ID"),
16
+ accessTtlSeconds: env.int("AUTH_ACCESS_TTL_SECONDS", 900),
17
+ },
18
+ refresh: {
19
+ name: "refresh_token",
20
+ path: "/auth/refresh",
21
+ ttlSeconds: env.int("AUTH_REFRESH_TTL_SECONDS", 2_592_000),
22
+ },
23
+ })
@@ -0,0 +1,29 @@
1
+ import { createHash, randomBytes } from "node:crypto"
2
+ import type { CookieOptions, Response } from "express"
3
+ import type { RefreshCookieOptions } from "./passport.options"
4
+
5
+ /** A fresh opaque refresh token (256 bits) and the sha256 the session store keeps instead of it. */
6
+ export const newRefreshToken = (): { readonly token: string; readonly hash: string } => {
7
+ const token = randomBytes(32).toString("base64url")
8
+ return { token, hash: refreshTokenHash(token) }
9
+ }
10
+
11
+ /** The sha256 a refresh token is stored and looked up by; the token itself is never stored. */
12
+ export const refreshTokenHash = (token: string): string => createHash("sha256").update(token).digest("hex")
13
+
14
+ const attributes = (options: RefreshCookieOptions): CookieOptions => ({
15
+ httpOnly: true,
16
+ secure: true,
17
+ sameSite: "strict",
18
+ path: options.path,
19
+ })
20
+
21
+ /** Sets the rotated refresh token: httpOnly, Secure, SameSite=Strict and sent to the refresh door only. */
22
+ export const setRefreshCookie = (response: Response, options: RefreshCookieOptions, token: string): void => {
23
+ response.cookie(options.name, token, { ...attributes(options), maxAge: options.ttlSeconds * 1000 })
24
+ }
25
+
26
+ /** Clears the refresh cookie on sign-out or on a detected reuse. */
27
+ export const clearRefreshCookie = (response: Response, options: RefreshCookieOptions): void => {
28
+ response.clearCookie(options.name, attributes(options))
29
+ }
@@ -0,0 +1,22 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { PassportOptions, VerifyAccessToken } from "./passport.options"
4
+ import type { FindPasswordAccount } from "./strategies/local.strategy"
5
+
6
+ /** Token of the passport options. */
7
+ export const PASSPORT_OPTIONS: unique symbol = Symbol("integrations.passport.options")
8
+ /** Token of the verifier of the app's own access tokens. */
9
+ export const OWN_TOKEN_VERIFIER: unique symbol = Symbol("integrations.passport.own-token-verifier")
10
+ /** Token of the password-account lookup the identity domain provides. */
11
+ export const PASSWORD_ACCOUNTS: unique symbol = Symbol("integrations.passport.password-accounts")
12
+
13
+ /** Injects the passport options. Parameter type: PassportOptions. */
14
+ export const InjectPassportOptions = (): TypedParameterDecorator<PassportOptions> => injector<PassportOptions>(PASSPORT_OPTIONS)
15
+
16
+ /** Injects the own-token verifier. Parameter type: VerifyAccessToken. */
17
+ export const InjectOwnTokenVerifier = (): TypedParameterDecorator<VerifyAccessToken> =>
18
+ injector<VerifyAccessToken>(OWN_TOKEN_VERIFIER)
19
+
20
+ /** Injects the password-account lookup. Parameter type: FindPasswordAccount. */
21
+ export const InjectPasswordAccounts = (): TypedParameterDecorator<FindPasswordAccount> =>
22
+ injector<FindPasswordAccount>(PASSWORD_ACCOUNTS)
@@ -0,0 +1,63 @@
1
+ import type { Secret } from "@modules/platform/config"
2
+ import type { Outcome } from "@modules/platform/primitives"
3
+ import type { PassportErrorCode } from "./errors/passport.error"
4
+
5
+ /** The claims admitted from one of the app's own access tokens after every cryptographic and semantic check. */
6
+ export interface OwnPrincipal {
7
+ /** The user identifier from the verified `sub` claim (a row of auth.users). */
8
+ readonly id: string
9
+ /** The verified email claim when the user has one. */
10
+ readonly email?: string
11
+ /** The role every access token the app issues carries; any other role is refused. */
12
+ readonly role: "authenticated"
13
+ }
14
+
15
+ /** Verifies one bearer token against the app's own key set and returns only claims that passed every check. */
16
+ export type VerifyAccessToken = (token: string) => Promise<Outcome<OwnPrincipal, PassportErrorCode>>
17
+
18
+ /** Issues one short-lived access token for a signed-in user. */
19
+ export type IssueAccessToken = (user: { readonly id: string; readonly email?: string }) => Promise<string>
20
+
21
+ /** Google OAuth 2.0 client configuration (authorization code with state and PKCE). */
22
+ export interface GoogleOptions {
23
+ /** The OAuth client id. */
24
+ readonly clientId: string
25
+ /** The OAuth client secret. */
26
+ readonly clientSecret: Secret
27
+ /** The absolute callback URL of the back end's Google callback door. */
28
+ readonly callbackUrl: string
29
+ }
30
+
31
+ /** The app's own asymmetric access tokens and the JWKS that publishes their public key. */
32
+ export interface TokenOptions {
33
+ /** The issuer (`iss`), the app's public API origin. */
34
+ readonly issuer: string
35
+ /** The audience (`aud`) every token names. */
36
+ readonly audience: string
37
+ /** The ES256 private key in PKCS#8 PEM; its public half is served as the JWKS. */
38
+ readonly privateKey: Secret
39
+ /** The key id (`kid`) of the signing key. */
40
+ readonly keyId: string
41
+ /** The access-token lifetime in seconds. */
42
+ readonly accessTtlSeconds: number
43
+ }
44
+
45
+ /** The rotating refresh token: an opaque value held only in an httpOnly, Secure, SameSite=Strict cookie. */
46
+ export interface RefreshCookieOptions {
47
+ /** The cookie name. */
48
+ readonly name: string
49
+ /** The path the browser sends the cookie to: the refresh door only. */
50
+ readonly path: string
51
+ /** The refresh-token lifetime in seconds. */
52
+ readonly ttlSeconds: number
53
+ }
54
+
55
+ /** The passport integration configuration parsed once at API boot. */
56
+ export interface PassportOptions {
57
+ /** Google OAuth 2.0. */
58
+ readonly google: GoogleOptions
59
+ /** The app's own access tokens. */
60
+ readonly tokens: TokenOptions
61
+ /** The refresh cookie. */
62
+ readonly refresh: RefreshCookieOptions
63
+ }
@@ -0,0 +1,11 @@
1
+ import { compare, hash } from "bcryptjs"
2
+
3
+ /** The bcrypt cost of a new password hash; existing `$2a$`/`$2b$` hashes (auth.users.encrypted_password) verify at their own cost. */
4
+ const COST = 12
5
+
6
+ /** Hashes a new password with bcrypt. */
7
+ export const hashPassword = (password: string): Promise<string> => hash(password, COST)
8
+
9
+ /** True when `password` matches the stored bcrypt hash; a missing hash never matches. */
10
+ export const verifyPassword = async (password: string, stored: string | null): Promise<boolean> =>
11
+ stored === null ? false : compare(password, stored)