@starci/hfs 5.2.0 → 5.4.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 (223) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/cache/hfs-family.d.mts +2 -3
  4. package/cache/hfs-family.mjs +4 -9
  5. package/cache/secret-records.d.mts +7 -0
  6. package/cache/secret-records.mjs +12 -0
  7. package/cache/session.mjs +4 -2
  8. package/codemod/account-doors.d.mts +36 -0
  9. package/codemod/account-doors.mjs +134 -0
  10. package/codemod/app-prettier.d.mts +5 -0
  11. package/codemod/app-prettier.mjs +27 -0
  12. package/codemod/explicit-surface.mjs +1 -25
  13. package/codemod/main.mjs +35 -1
  14. package/codemod/module-definition.d.mts +59 -0
  15. package/codemod/module-definition.mjs +257 -0
  16. package/codemod/quality-binding.d.mts +33 -0
  17. package/codemod/quality-binding.mjs +38 -0
  18. package/codemod/secrets-cache.d.mts +35 -0
  19. package/codemod/secrets-cache.mjs +70 -0
  20. package/codemod/sops-rules.d.mts +34 -0
  21. package/codemod/sops-rules.mjs +69 -0
  22. package/codemod/theme-tokens.mjs +48 -2
  23. package/lint/run.d.mts +6 -0
  24. package/lint/run.mjs +63 -10
  25. package/package.json +1 -1
  26. package/runtime/knowledge/hfs/canon-pins.yaml +43 -10
  27. package/runtime/knowledge/hfs/fe-stack.yaml +3 -3
  28. package/runtime/knowledge/hfs/rules.yaml +13 -9
  29. package/runtime/knowledge/hfs/slots.yaml +105 -12
  30. package/runtime/modules/kernel/failure-codes.yaml +5 -4
  31. package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
  32. package/runtime/scripts/hfs/architecture/config.mjs +25 -1
  33. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
  34. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
  35. package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
  36. package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
  37. package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
  38. package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
  39. package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
  40. package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
  41. package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
  43. package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
  44. package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
  45. package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
  46. package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
  47. package/runtime/scripts/hfs/secret-errors.mjs +8 -0
  48. package/runtime/scripts/hfs/secret-init.d.mts +6 -0
  49. package/runtime/scripts/hfs/secret-init.mjs +23 -0
  50. package/runtime/scripts/hfs/secret-sops.mjs +9 -1
  51. package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
  52. package/runtime/scripts/hfs/secret-standard.mjs +106 -0
  53. package/runtime/scripts/hfs/secret.mjs +5 -1
  54. package/runtime/scripts/hfs/view.d.mts +23 -0
  55. package/runtime/scripts/hfs/view.mjs +68 -0
  56. package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
  57. package/runtime/scripts/lib/declaration-surface.mjs +55 -1
  58. package/runtime/scripts/lib/secret-records.d.mts +13 -0
  59. package/runtime/scripts/lib/secret-records.mjs +83 -0
  60. package/scaffold/add-inbox-lite.mjs +2 -2
  61. package/scaffold/add-queue-lite.mjs +1 -3
  62. package/scaffold/app.d.mts +2 -2
  63. package/scaffold/app.mjs +10 -12
  64. package/scaffold/lite-exports.mjs +1 -15
  65. package/scripts/sync-bundles.d.mts +8 -0
  66. package/stack/images/openclaw-runtime/Dockerfile +18 -0
  67. package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
  68. package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
  69. package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
  70. package/stack/local-up.mjs +3 -0
  71. package/stack/runtime-layers.d.mts +21 -0
  72. package/stack/runtime-layers.mjs +67 -0
  73. package/sync/compile-guard.d.mts +36 -0
  74. package/sync/compile-guard.mjs +189 -0
  75. package/sync/hygiene.mjs +3 -1
  76. package/sync/index.d.mts +11 -1
  77. package/sync/index.mjs +61 -10
  78. package/templates/app/auth-doors.sql.partial +115 -0
  79. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  80. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  81. package/templates/app/package-scripts/core.json +1 -1
  82. package/templates/app/package-scripts/package.json +1 -1
  83. package/templates/app/package-scripts-lite/package.json +1 -1
  84. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +9 -0
  85. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +3 -1
  86. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +19 -1
  87. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -0
  88. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +5 -0
  89. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +4 -0
  90. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +27 -5
  91. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +33 -0
  92. package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
  93. package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
  94. package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
  95. package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
  96. package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
  97. package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
  98. package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
  99. package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
  100. package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
  101. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
  102. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +3 -10
  103. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +6 -1
  104. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +3 -10
  105. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +56 -1
  106. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
  107. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
  108. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
  109. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
  110. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
  111. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +30 -30
  112. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +7 -16
  113. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +113 -49
  114. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +42 -53
  115. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
  116. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
  117. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
  118. package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
  119. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
  120. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
  121. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
  122. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +3 -10
  123. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +6 -3
  124. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +6 -1
  125. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +17 -0
  126. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +6 -0
  127. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +29 -33
  128. package/templates/be/inbox/module-definition.ts.tpl +2 -9
  129. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
  130. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  131. package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
  132. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
  133. package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
  134. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
  135. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  136. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
  137. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
  138. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
  139. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
  140. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
  141. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
  142. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
  143. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
  144. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
  145. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
  146. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
  147. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
  148. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
  149. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
  150. package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
  151. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
  152. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
  153. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
  154. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
  155. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
  156. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
  157. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
  158. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
  159. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
  162. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
  163. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
  164. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
  165. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
  166. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
  167. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
  168. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
  169. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
  170. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
  171. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
  172. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +3 -1
  173. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +6 -0
  174. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +3 -0
  175. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +1 -0
  176. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
  177. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +4 -0
  178. package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
  179. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
  180. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
  181. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
  182. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
  183. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
  184. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +2 -9
  185. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +1 -1
  186. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  187. package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
  188. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -0
  189. package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
  190. package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
  191. package/templates/be/table/module-definition.ts.tpl +2 -9
  192. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
  193. package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
  194. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
  195. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
  196. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
  197. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
  198. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
  199. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
  200. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
  201. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
  202. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
  203. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
  204. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
  205. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
  206. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
  207. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
  208. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
  209. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
  210. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
  211. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
  212. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
  213. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
  214. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
  215. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
  216. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
  217. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
  218. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
  219. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
  220. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
  221. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
  222. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +0 -38
  223. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +0 -54
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.4.0 - 2026-11-03
4
+
5
+ - Fixed: `starci app sync --execute` never leaves an app uncompilable. Before it writes a managed TypeScript file or deletes a retired one, it compiles each front-end and back-end project with the change applied (the app's own TypeScript; the changed files and every file importing them, against what is on disk). A managed group whose render adds a type error, in its own files or in an app file that imports one, is held whole: `HFS_SYNC_GROUP_HELD`, exit 1, naming the group, its files and the first errors (what the app must add or still imports); the rest is written, and a side without TypeScript installed is named in a notice (`sync/compile-guard.mts`).
6
+ - Fixed: `starci app codemod module-definition` drops every imported name the rewrite made unused (`ConfigurableModuleCls`, `ConfigurableModuleHost`, `ConfigurableModuleBuilder`), keeps the names the file still uses and `import type`, keeps the builder's JSDoc, and writes each file as the app's Prettier prints it, so no TS6133 and no prettier-hook refusal follow it (`codemod/app-prettier.mts`).
7
+ - Changed: `starci app codemod account-doors --contract` says why it writes nothing: the contract migration is already in the chain, or no migration grants the app role on schema `auth`.
8
+ - Changed: the lite sign-in renders `@starci/auth` 0.2.1: `identity_refresh_tokens.raced_count` in the baseline (the package's `identity-sessions.sql`), the refresh-token entity's `racedCount` and the session store's `markTokenRaced`. An app whose baseline holds the 0.2.0 tables adds `@starci/auth/migrations/identity-refresh-raced-successors.sql` as a migration.
9
+ - Changed: the canon pins name `@starci/auth` 0.2.1, `@starci/auth-oidc` 0.1.1 and the Grammar family 0.21.16.
10
+ - Upgrade, for an app on the CLI 1.1.0-alpha.49 / hfs 5.0.8 render moving to this version: (1) `npx starci app codemod module-definition --execute`, then `npm install`; (2) `npx starci app sync --execute`, which writes everything that compiles and holds the auth-lite sign-in glue while the app's own identity code differs, naming each missing or still-imported symbol; (3) adopt `@starci/mail` and make the identity domain provide what the held errors name; (4) `npx starci app sync --execute` again, which writes the glue once it compiles; (5) `npx starci app codemod account-doors --execute`, and `--contract` when the app still grants on schema `auth`; (6) add the `raced_count` migration above.
11
+
12
+ ## 5.3.0 - 2026-11-03
13
+
14
+ - Fixed: the secrets family of the committed check cache stores codes, paths, positions and digests only (`cache/secret-records.mts`, `runtime/scripts/lib/secret-records.mts`), and the one secret judgement of R06 judges a file of `.starcistacks/cache/check/secrets/` by that contract, so `starci app check` can no longer write a cache the staged guard refuses (`HFS_PLAINTEXT_SECRET` for the `secrets/` folder name). A cache written before reads back as digests. Codemod: `starci app codemod secrets-cache`.
15
+ - Added: `@starci/nest-kit` in the starter dependencies and in every module-definition template: a module definition is one call (`defineConfigurableModule<XOptions>()`, `defineCapabilityModule<XOptions>()` for a capability's representative module). The lint on stated export types (R368) accepts a destructured export of those two calls and no other; the digest of the file carries the call, so a change of the options type reaches the importers; `capability-module-shape` (R45) accepts the helper and `module-per-transport` confines it to the definition file. Codemod: `starci app codemod module-definition` rewrites the destructured and the typed builder forms and adds the dependency.
16
+ - Fixed: `HFS_MONO_WORKSPACES` asks for `packages/*` after the fe workspaces once the app tracks a root package (slot `app.packages`, the module packages), so `starci app sync` and `starci app check` keep the entry `app add module` writes; the managed `test` and `build:be` scripts build the module packages first and the CI fast job runs `npm run test:modules` (PROJECT-3, BE-TEST-13).
17
+ - Added: `starci app stack up` builds the harness runtime layer an app image stands on (`openclaw-runtime`) from the recipe this package ships (`stack/images/openclaw-runtime`, kept byte-equal to the harness `images/openclaw-runtime` by `npm run bundles`) when the host neither holds nor can pull it, as a governed heavy build.
18
+ - Fixed: a secret member no creation rule of `.sops.yaml` reaches is refused `SECRET_SOPS_RULE_MISSING` before sops runs (a rule written for the legacy `.enc` spelling does not reach a `.enc.yaml` member), naming the path and `starci app secret init --env <env>`. Codemod: `starci app codemod sops-rules` migrates the legacy rule to the managed one (public recipients only).
19
+ - Fixed: `starci app secret set` on a tooling member (`sonar.enc.yaml`, any member a service credential's custody path names) no longer declares it in `environments.<env>.secrets` (which made CI custody refuse the analysis token); it binds the member to its service (`credentials` and `ci.secrets`, the shape the template renders) or prints the exact binding. Codemod: `starci app codemod quality-binding` binds an app scaffolded before and names a runtime entry an earlier set declared.
20
+ - Added: the lite starter depends on `@starci/mail` and renders the five email doors (BE `IdentityEmailService` and `identity_email_tokens`; FE `/auth/signup`, `/auth/resend`, `/auth/forgot`, `/auth/confirm`, `/auth/reset`, `EmailDoorForm`, the managed `modules/auth/email-doors.ts`), a Mailpit dev sink pinned by digest (`mail-sink.spec.mts`), and the stack links `@starci/mail` in the scaffold type-check.
21
+ - Fixed: the `theme-tokens` codemod drops an alias whose rename would name itself (`--starci-core-accent: var(--accent)` became `--accent: var(--accent)`, which is invalid at computed-value time and blanked every brand colour) and a rule block that held only such aliases, with its comment; R61 now lists the stylelint rule `no-self-reference` (`@starci/stylelint-canon` 2.6.0) among its enforcers.
22
+ - Changed: the scaffold type-check runs the back-end and front-end chains and both editions concurrently (the same checks, async spawns).
23
+ - Fixed: an app's root workspace packages (`packages/agent-sdk`, `packages/module-<name>`, the managed `packages/modules`) are first-party packages of the import graph. A side imports one by its package name through the `exports` of its manifest: that is no import that leaves the side (`ARCH_INTERNAL_IMPORT_OUTSIDE` stopped the front-end lint run on `@scope/agent-sdk/workbench`) and adds no edge; an import into the package's source past its entry points stays refused, with a message that lists the entry points. The packages are read from the app root's `workspaces` (patterns that start with no side folder) and from the slots `app.packages` and `app.module-registry` (`architecture/root-packages.mts`, `rootPackages` of the architecture config). Spec: `architecture-root-packages.spec.mts`.
24
+ - Added: the slots `app.packages`, `app.module-registry` and `app.packages-lint` in the runtime slot manifest (3.17.0; the first two were only in `contexts/hfs/slots.yaml`, so `packages/` classified as `HFS_SLOT_UNDECLARED`). `eslint.config.mjs` at the app root is the managed config of the root packages, no longer a forbidden tool config.
25
+ - Added: `starci app sync` renders `eslint.config.mjs` at the app root whenever the app has a root package: the back-end canon over the back-end subpaths of `packages/<pkg>/src/**`, the front-end canon and the Grammar family's `<family>GrammarPackagesConfig` over `src/workbench/**` and `src/public/**`. `starci app check --gate lint` runs it as a third scope (`packages`, from the app root, uncached) beside `be` and `fe`. The package view of the slot resolver (`openPackagesHfsView`, `declaredPackagesHfsView` in `view.mts`) is the profile of a side over the paths of the app root; a package file is of the tier `package` and has no owner. Spec: `lint-packages.spec.mts`.
26
+ - Changed: the lite sign-in and the email doors reach accounts through the account doors. The baseline migration includes `templates/app/auth-doors.sql.partial`, which is now the source of truth of the doors (it is no longer a byte copy of an app's own migration): eight SECURITY DEFINER functions `public.be_auth_user_by_email`, `_by_id`, `_by_google`, `be_auth_create_user`, `be_auth_link_google`, `be_auth_touch_user`, `be_auth_confirm_user(user, now)` (stamps the confirmation only, no sign-in record) and `be_auth_set_password(user, hash, confirm, now)` (sets the hash and, when `confirm` is true, confirms the mailbox), each with a pinned `search_path` and EXECUTE revoked from public, anon and authenticated and granted to `app_be` alone, plus a `lower(email)` index on `auth.users` the by-address door reads. `be_auth_set_password` is a four-argument function: an app that already has a three-argument one of its own keeps both. The three grants of `app_be` on schema `auth`, `auth.users` and `auth.identities` are gone from the baseline, so one injectable query no longer reaches every account's hash, role and metadata. The eight doors are in `ruleParams.be.dbFunctions`; an app lists none of them in hfs.json.
27
+ - Changed: the managed identity glue (slot `be.identity.sign-in`): `AuthUserEntity` and `AuthIdentityEntity` are deleted; the account directory, the authenticator and `IdentityEmailService` (signup insert, mailbox confirm, password reset) call the doors through the new managed `persistence/account-doors.port.ts`, which validates every answer (`returnedRows`, `rowShape`); the database decides `disabled` at its own now() (a banned account no longer receives a reset mail). The rate counter over `identity_attempts` and the fail-closed refusal mapping are unchanged.
28
+ - Added: `IdentitySignInService.endOthers(principal)` signs the caller out of every other device, and `Principal.sessionId` carries the device session of the access token. The authenticator logs an account created by Google sign-in as `identity.sign_in.account_created` (new managed `identity-sign-in.log-events.ts`) with the account id only. The lite starter carries `platform/database/database.client.ts` and exports `callDbFunction`.
29
+ - Changed: R255 (`DB_APP_ROLE`) also refuses any grant to the declared back-end role on schema `auth`, on a table or sequence in it, or by default privilege for it, with no transition tolerance. The migrations are judged in order, so a later migration that revokes a grant clears it. BE-AUTH-2, BE-PERSISTENCE-4 and BE-PERSISTENCE-17 follow.
30
+ - Added: `starci app codemod account-doors [--contract] [--execute]`, the upgrade of a lite app already on the template: it writes the expand migration `db/migrations/<ts>_account-doors.sql` with the doors the app lacks (a door is known by name and argument types, so an app that has the six earlier doors gets only the two new ones) and, with `--contract`, the migration `<ts>_contract_revoke-auth-grants.sql` that revokes the three grants from the declared role.
31
+ - Changed: `starci app sync --execute` deletes the retired `auth-user.entity.ts` and `auth-identity.entity.ts` of an app on the old glue.
32
+ - Upgrade, for a lite app scaffolded at 3.16.0 or later (the old glue grants `app_be` on `auth.users`): the new glue calls functions the app's database does not have until the expand migration is applied. Ship these three steps in one release, in this order; R255 reports the old grants until step 3 is in the chain, so they cannot ship apart.
33
+ 1. `npx starci app codemod account-doors --execute` (expand): writes the migration that creates the doors the app lacks and the index.
34
+ 2. `npx starci app sync --execute`: moves the glue onto the doors and deletes the two entity files; `npx starci app check` is green except R255 until step 3.
35
+ 3. `npx starci app codemod account-doors --contract --execute` (contract): writes the migration that revokes the old grants. It refuses while an account door is undefined or a back-end source still maps `schema: "auth"`.
36
+ 4. The deploy applies the expand migration before the services and the contract migration after the accept.
37
+ 5. Rollback: the release before this one still reads the auth tables, so a rollback to it after the accept must restore the grants by hand, as the database owner:
38
+ ```sql
39
+ grant usage on schema auth to app_be;
40
+ grant select, insert, update on table auth.users to app_be;
41
+ grant select, insert on table auth.identities to app_be;
42
+ ```
43
+ (use the role `db.appRole` of hfs.json when it is not app_be). Revoke them again with the contract migration when the rollback is undone.
44
+ 6. An app that already defines the six earlier doors (Nivo) runs step 1 too and gets only `be_auth_confirm_user` and the four-argument `be_auth_set_password`; its hfs.json `sides.be.dbFunctions` may drop the doors, which the canon now lists.
45
+
3
46
  ## 5.2.0 - 2026-11-03
4
47
 
5
48
  - Added: a scaffolded app of each edition type-checks and builds on every side (`scaffold-type-check.spec.mts`: one install per edition, `tsc` of the back end and its specs, the back-end build, the workspace packages, `tsc` and `next build` of each front-end app). The templates follow the Grammar contract: `Link` and `Page` for `SiteShell` and `NotFoundNotice`, a callable `toast`, the session reducers as a type, explicit next-intl navigation generics, optional access to the test world's apps and connections, no unused import in the migrations fixture. The lite dev api process no longer runs `--transpile-only`.
package/README.md CHANGED
@@ -148,7 +148,7 @@ Every file of a spec-required role of `ruleParams.be.unitRoles` — a `*.service
148
148
 
149
149
  The spec skeleton is `Test.createTestingModule({ providers: [Service, { provide: TOKEN, useValue: double }, ...] }).compile()` and `moduleRef.get(Service)`, with one provider per constructor dependency and nothing else, every double imported from `@starci/jest-preset` (the root), and one placeholder `it` per public method. The double of a token comes from `ruleParams.be.specDoubles` of the slot manifest, the table the lint law `spec-infra-double-from-kit` holds a spec to (`*_ENTITY_MANAGER` -> `mockEntityManager()`, `CLOCK` -> `new FakeClock(...)`, `EVENT_BUS` -> `recordingEventBus()`, `QUEUE_OUTBOX` -> `recordingQueueOutbox()`, `CACHE` -> `fakeCache(clock)`, lock, lease, fence and hold -> `fakeLock(clock)`, ids -> `fakeIds()`, `*_OPTIONS` -> a literal to fill in, everything else and every class -> `mock<T>()`), so the skeleton satisfies the law by construction: no cast, no `new` of the service, no ambient clock. A back end only (a front end has no services); the files are written for prettier (print width 120).
150
150
 
151
- `starci app check` runs ESLint once per side through the app's installed managed flat config: the backend canon on `be/`, and the universal FE canon plus the explicitly selected Grammar-family package on `fe/`. It also reads the app's `hfs.json` and tracked paths (`git ls-files`), checks the work tree (the root rules once, the side rules per side), and runs the whole architecture machine over each side folder. HFS findings carry their why code and Vietnamese text (`modules/kernel/failure-codes.yaml`); ESLint findings retain their rule id and failure code.
151
+ `starci app check` runs ESLint once per side through the app's installed managed flat config: the backend canon on `be/`, and the universal FE canon plus the explicitly selected Grammar-family package on `fe/`; an app with root workspace packages (`packages/agent-sdk`, `packages/module-<name>`) has a third run from the app root through the managed root `eslint.config.mjs` that `starci app sync` renders, over `packages/<pkg>/src/**` by subpath (the backend canon on everything but `src/workbench/` and `src/public/`, the workbench contract kept; the universal FE canon and the Grammar family on those two, the contract left out). It also reads the app's `hfs.json` and tracked paths (`git ls-files`), checks the work tree (the root rules once, the side rules per side), and runs the whole architecture machine over each side folder. HFS findings carry their why code and Vietnamese text (`modules/kernel/failure-codes.yaml`); ESLint findings retain their rule id and failure code.
152
152
 
153
153
  Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file checks (`sync/managed.mjs`) and the prettier check (`sync/format.mjs`) live in `sync/` because they read the templates and the repository's own install, and reach `starci app check` as `extraFindings`; the jest / vitest preset the coverage exclusions come from and prettier are read from the repository's `node_modules`, so run `npm ci` first):
154
154
 
@@ -207,6 +207,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
207
207
 
208
208
  The managed files are judged by `sync/managed.mjs` and `sync/ts-strict.mjs` (the package renders the templates; the runtime copy does not). The list of managed
209
209
  files is the `managedBy` slots of `slots.yaml`; `starci app sync --execute` renders them and `starci app check` compares them, so a hand edit and a forgotten `sync` are the same finding.
210
+ `starci app sync --execute` never leaves the app uncompilable (`sync/compile-guard.mjs`): before it writes a managed TypeScript source or deletes a retired one, the side's own TypeScript compiles the app with those writes over the disk, and a managed group (`managedBy`) whose writes add a type error, in its own files or in an app file that imports one, is held whole: `HFS_SYNC_GROUP_HELD` (exit 1) names the group, its files and the errors (the names the render reads that the app does not provide yet, or the names it drops that the app still imports), and the rest of the render is written. The held files stay `HFS_SYNC_DRIFT` until the app provides what the render reads and sync runs again; a side whose TypeScript is not installed is written and named in a notice.
210
211
  Each finding is reported once: the managed ESLint and stylelint factory configs under R17, `tsconfig.json` under R22 when it names a flag, a workflow or hook that lost a canon step under R13 (or R19 for the format step), everything else under R05.
211
212
  The root and both sides are rendered by the one mechanism. The root: the `scripts` block of `app.package-manifest` (`dev:be`, `dev:fe`, `build:be`, `build:fe`, `start:<app>`, `lint`, `lint:fix`, `test`, `test:integration`, `test:e2e`, `test:contract`, `test:world`, `codegen`, `contract:emit`, `migrate`, `typecheck`, ...; a be script runs from `be/`, where its tsconfig and jest configuration are), `app.format-config`, `app.hooks`, the CI slots (`app.ci`, `app.ci-e2e`, `app.ci-images`), `app.quality-config` and the managed block of `app.git-meta`. A side: its `be.tool-config` or `fe.tool-config` slot — the side's tsconfig (resolving `@starci/tsconfig` from the root `node_modules`) and its managed ESLint factory config, the back end's build tsconfig, tests tsconfig and jest configuration, the front end's stylelint configuration. `lint` is `starci app check --gate lint`: ESLint over each side with its canon and stylelint over fe; `lint:fix` is the same with `--fix`. The front end has no test script, no test configuration and no e2e or coverage file: it has no tests, and `FE_NO_TESTS` (R97) refuses any spec, e2e file, test tool or test script under `fe/`. `turbo.json` stays the app's own
212
213
  (slot `app.task-graph`): it carries the task graph, which no preset can render.
@@ -1,11 +1,10 @@
1
1
  import type { HeavyNodeRun } from '../runtime/scripts/api/node/run-node.mjs';
2
2
  import { type MachineFn, type RepositoryResult } from '../runtime/scripts/hfs/check.mjs';
3
3
  import type { Presets } from '../sync/index.mjs';
4
+ import { SECRETS_FOLDER, SECRET_CODES } from './secret-records.mjs';
4
5
  import type { CheckSession, FamilyReport } from './types.mjs';
5
6
  export declare const HFS_FOLDER = "check/hfs";
6
- export declare const SECRETS_FOLDER = "check/secrets";
7
- /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
8
- export declare const SECRET_CODES: ReadonlySet<string>;
7
+ export { SECRETS_FOLDER, SECRET_CODES };
9
8
  /** What the structure pass answers. */
10
9
  export interface PassResult {
11
10
  /** The whole-app check result of the pass (the shape `printCheck` reads), or the slot result when only secrets were asked for. */
@@ -16,15 +16,10 @@ import { withRuleMemo } from '../runtime/scripts/hfs/rule-memo.mjs';
16
16
  import { RecordSet, scopedStep } from './records.mjs';
17
17
  import { FILE_RULES_STEP, RULE_STEP_PREFIX, UNIT_STEP_PREFIX, createStructureMemo } from './structure-memo.mjs';
18
18
  import { makeReport } from './report.mjs';
19
+ import { SECRETS_FOLDER, SECRET_CODES, safeSecretFinding, safeSecretRecords } from './secret-records.mjs';
19
20
  import { sideKey } from './side-key.mjs';
20
21
  export const HFS_FOLDER = 'check/hfs';
21
- export const SECRETS_FOLDER = 'check/secrets';
22
- /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
23
- export const SECRET_CODES = new Set([
24
- 'HFS_PLAINTEXT_SECRET', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_LITE_SECRET_CUSTODY', 'HFS_IDENTITY_CUSTODY',
25
- 'HFS_AGE_KEY_CUSTODY', 'RT_SECRET_TRACKED', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'SEALED_CUSTODY_LOCATION',
26
- 'APP_SECRET_IN_CI', 'CI_SECRET_SEALED', 'BE_LOG_SECRET', 'BE_SECRET_DEFAULT', 'HFS_DOCKER_SECRETS',
27
- ]);
22
+ export { SECRETS_FOLDER, SECRET_CODES };
28
23
  const compact = (finding) => ({
29
24
  path: finding.path ?? '.', line: finding.line ?? 0, column: finding.column ?? 0, rule: finding.code, message: String(finding.message ?? ''),
30
25
  });
@@ -169,8 +164,8 @@ export async function runStructurePass(session, input) {
169
164
  const secretDecision = session.decide('secrets');
170
165
  const prior = session.records(SECRETS_FOLDER);
171
166
  const secretFindings = result.findings.filter((finding) => SECRET_CODES.has(finding.code));
172
- session.replace(SECRETS_FOLDER, findingRecords(secretFindings, session.index.hashOf.bind(session.index), 'secrets'));
173
- secretsReport = makeReport({ family: 'secrets', mode: secretDecision.mode, reason: secretDecision.reason, checked: filesChecked, reused: filesReused, previous: prior.flatMap((record) => record.findings), current: secretFindings.map(compact) });
167
+ session.replace(SECRETS_FOLDER, safeSecretRecords(findingRecords(secretFindings, session.index.hashOf.bind(session.index), 'secrets')));
168
+ secretsReport = makeReport({ family: 'secrets', mode: secretDecision.mode, reason: secretDecision.reason, checked: filesChecked, reused: filesReused, previous: prior.flatMap((record) => record.findings.map(safeSecretFinding)), current: secretFindings.map((finding) => safeSecretFinding(compact(finding))) });
174
169
  session.report(secretsReport);
175
170
  }
176
171
  return { result, contracts, machineReused: reasons, report, secretsReport };
@@ -0,0 +1,7 @@
1
+ import { SECRETS_FOLDER, SECRET_CODES } from '../runtime/scripts/lib/secret-records.mjs';
2
+ import type { CacheRecord, Finding } from './types.mjs';
3
+ export { SECRETS_FOLDER, SECRET_CODES };
4
+ /** A finding as the secrets cache stores it: the same code, path and position, the message as its digest. */
5
+ export declare function safeSecretFinding(finding: Finding): Finding;
6
+ /** The records as the secrets cache stores them: each finding safe, no `data`. */
7
+ export declare function safeSecretRecords(records: readonly CacheRecord[]): CacheRecord[];
@@ -0,0 +1,12 @@
1
+ // secret-records.mts - the writer side of the secrets records contract (runtime/scripts/lib/secret-records.mts): the cache is committed, so the
2
+ // records of `check/secrets` are projected to codes, paths, positions and digests before they are stored or compared, whoever produced them.
3
+ import { messageDigest, SECRETS_FOLDER, SECRET_CODES } from '../runtime/scripts/lib/secret-records.mjs';
4
+ export { SECRETS_FOLDER, SECRET_CODES };
5
+ /** A finding as the secrets cache stores it: the same code, path and position, the message as its digest. */
6
+ export function safeSecretFinding(finding) {
7
+ return { path: finding.path, line: finding.line, column: finding.column, rule: finding.rule, message: messageDigest(finding.message) };
8
+ }
9
+ /** The records as the secrets cache stores them: each finding safe, no `data`. */
10
+ export function safeSecretRecords(records) {
11
+ return records.map((record) => ({ path: record.path, step: record.step, hash: record.hash, findings: record.findings.map(safeSecretFinding) }));
12
+ }
package/cache/session.mjs CHANGED
@@ -8,6 +8,7 @@ import { createContentIndex } from './content-index.mjs';
8
8
  import { computeFingerprint, fingerprintDifferences, parseFingerprint, serializeFingerprint } from './fingerprint.mjs';
9
9
  import { hashFile } from './hash.mjs';
10
10
  import { groupByArea, parseRecords, serializeRecords } from './jsonl.mjs';
11
+ import { SECRETS_FOLDER, safeSecretRecords } from './secret-records.mjs';
11
12
  import { readCacheDir, resolveTrust, verifyCommitted } from './trust.mjs';
12
13
  import { CACHE_DIR } from './types.mjs';
13
14
  /** `STARCI_APP_CHECK_CACHE`: `full` ignores the cache (the release gate sets it), `verify` is read by the caller (runs diff, then full, compares). */
@@ -122,10 +123,11 @@ export function createDiskSession(options) {
122
123
  return global() ?? { mode: 'diff', reason: `against ${trust.reason}` };
123
124
  },
124
125
  records(folder) {
125
- return replaced.get(folder)?.records ?? trustedRecords(folder);
126
+ return replaced.get(folder)?.records ?? (folder === SECRETS_FOLDER ? safeSecretRecords(trustedRecords(folder)) : trustedRecords(folder));
126
127
  },
127
128
  replace(folder, records, replaceOptions) {
128
- replaced.set(folder, { records, depth: replaceOptions?.depth ?? 2 });
129
+ // The secrets family never stores secret material, whoever produced the records (secret-records.mts).
130
+ replaced.set(folder, { records: folder === SECRETS_FOLDER ? safeSecretRecords(records) : records, depth: replaceOptions?.depth ?? 2 });
129
131
  },
130
132
  unchanged: (paths, captured) => paths.every((file) => {
131
133
  const now = hashFile(path.join(root, file));
@@ -0,0 +1,36 @@
1
+ /** One migration the codemod writes (or would write). */
2
+ export interface AccountDoorsChange {
3
+ file: string;
4
+ phase: 'expand' | 'contract';
5
+ why: string;
6
+ }
7
+ /** One case left for a person. */
8
+ export interface AccountDoorsFinding {
9
+ file: string;
10
+ reason: string;
11
+ }
12
+ /** The report of the upgrade (`starci app codemod account-doors`). */
13
+ export interface AccountDoorsReport {
14
+ schema: string;
15
+ root: string;
16
+ write: boolean;
17
+ phase: 'expand' | 'contract';
18
+ changes: AccountDoorsChange[];
19
+ findings: AccountDoorsFinding[];
20
+ next: string[];
21
+ }
22
+ /**
23
+ * Upgrade of a lite app that signs in over direct grants on auth.users and auth.identities to the account doors. The expand step (default)
24
+ * writes the migration that creates the doors the app lacks, with the text the lite baseline includes; the app then runs `starci app sync --execute`
25
+ * (the sign-in glue switches to the doors and the two entity files go). The contract step (`contract`) writes the migration that revokes the three
26
+ * grants, once the doors exist and no be source maps the auth tables; R255 reports the grants until that migration is in the chain. Nothing is written while a case is left for a person, and a step that is already
27
+ * done changes nothing.
28
+ */
29
+ export declare function migrateAccountDoors({ root, write, contract, now }: {
30
+ root: string;
31
+ write?: boolean;
32
+ contract?: boolean;
33
+ now?: () => number;
34
+ }): AccountDoorsReport;
35
+ /** Render the migrations and what is left for a person as Markdown; JSON consumers read the same report object. */
36
+ export declare function accountDoorsMarkdown(report: AccountDoorsReport): string;
@@ -0,0 +1,134 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { nextStamp } from '../db/new.mjs';
5
+ import { MIGRATIONS_DIR } from '../runtime/scripts/hfs/rules/database-constants.mjs';
6
+ import { filesUnder, relativePosix } from './source-tree.mjs';
7
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ /** The owner of the account doors: the partial the lite baseline includes, byte for byte. */
9
+ const DOORS_PARTIAL = path.join(PACKAGE_ROOT, 'templates', 'app', 'auth-doors.sql.partial');
10
+ /** A grant to anyone on schema auth or on a table in it. */
11
+ const AUTH_GRANT = /\bgrant\b[^;]*?\bon\b\s+(?:schema\s+auth\b|(?:table\s+)?auth\.)/iu;
12
+ const EXPAND_NAME = 'account-doors';
13
+ const CONTRACT_NAME = 'contract_revoke-auth-grants';
14
+ const SOURCE = /\.[cm]?tsx?$/iu;
15
+ const ENTITY_FILES = ['auth-user.entity.ts', 'auth-identity.entity.ts'];
16
+ /** The argument types of a signature, the `p_` parameter names dropped and case and spacing folded: a door is known by its name and types alone. */
17
+ const typesOf = (args) => args.split(',').map((arg) => arg.trim().replace(/^p_\w+\s+/u, '').replace(/\s+/gu, ' ').toLowerCase()).join(', ');
18
+ /** The partial split into its head (the comment and the index) and its doors, in the partial's order. */
19
+ function partialParts(text) {
20
+ const lines = text.split('\n');
21
+ const doors = [...text.matchAll(/^(?:-- [^\n]*\n)?CREATE FUNCTION public\.(\w+)\(([^)]*)\)[\s\S]*?\$\$;\n/gmu)].map((match) => {
22
+ const own = lines.filter((line) => /^(?:REVOKE|GRANT) EXECUTE ON FUNCTION public\.(\w+)\(/u.exec(line)?.[1] === match[1] && typesOf(/\(([^)]*)\)/u.exec(line)[1]) === typesOf(match[2]));
23
+ return { name: match[1], types: typesOf(match[2]), block: match[0], grants: `${own.join('\n')}\n` };
24
+ });
25
+ return { head: text.slice(0, text.indexOf(doors[0].block)), doors };
26
+ }
27
+ /** The doors a text defines, as name(types) keys. */
28
+ const definedDoors = (statements) => [...statements.matchAll(/create\s+(?:or\s+replace\s+)?function\s+public\.(\w+)\s*\(([^)]*)\)/giu)].map((match) => `${match[1].toLowerCase()}(${typesOf(match[2])})`);
29
+ const migrationsOf = (root) => {
30
+ const dir = path.join(root, MIGRATIONS_DIR);
31
+ return fs.existsSync(dir) ? fs.readdirSync(dir).filter((name) => name.endsWith('.sql')).sort() : [];
32
+ };
33
+ const textOf = (root, name) => fs.readFileSync(path.join(root, MIGRATIONS_DIR, name), 'utf8');
34
+ /** The statements of a migration, its line comments left out. */
35
+ const statementsOf = (root, name) => textOf(root, name).replace(/--[^\n]*/gu, '');
36
+ /** The application role hfs.json declares (`db.appRole`), app_be by default. */
37
+ function appRoleOf(root) {
38
+ const role = JSON.parse(fs.readFileSync(path.join(root, 'hfs.json'), 'utf8')).db?.appRole;
39
+ return typeof role === 'string' && /^[a-z_][a-z0-9_]*$/u.test(role) ? role : 'app_be';
40
+ }
41
+ /** The be source that still maps or names the auth tables: an entity of schema auth, or the retired entity files. */
42
+ function accountTableReferences(root) {
43
+ const be = path.join(root, 'be', 'src');
44
+ if (!fs.existsSync(be))
45
+ return [];
46
+ return filesUnder(be, SOURCE).map((file) => relativePosix(root, file)).filter((file) => {
47
+ const retired = ENTITY_FILES.some((name) => file.endsWith(`/${name}`));
48
+ return retired || /schema:\s*["']auth["']/u.test(fs.readFileSync(path.join(root, file), 'utf8'));
49
+ });
50
+ }
51
+ function contractText(role) {
52
+ return [
53
+ '-- contract: the back end reaches auth.users and auth.identities through the account doors (public.be_auth_*), so the application role',
54
+ '-- gives up its direct grants. The release before the doors still reads those tables, so a rollback to it after the accept needs them granted again.',
55
+ `revoke select, insert, update on table auth.users from ${role};`,
56
+ `revoke select, insert on table auth.identities from ${role};`,
57
+ `revoke usage on schema auth from ${role};`,
58
+ '',
59
+ ].join('\n');
60
+ }
61
+ /**
62
+ * Upgrade of a lite app that signs in over direct grants on auth.users and auth.identities to the account doors. The expand step (default)
63
+ * writes the migration that creates the doors the app lacks, with the text the lite baseline includes; the app then runs `starci app sync --execute`
64
+ * (the sign-in glue switches to the doors and the two entity files go). The contract step (`contract`) writes the migration that revokes the three
65
+ * grants, once the doors exist and no be source maps the auth tables; R255 reports the grants until that migration is in the chain. Nothing is written while a case is left for a person, and a step that is already
66
+ * done changes nothing.
67
+ */
68
+ export function migrateAccountDoors({ root, write = false, contract = false, now = Date.now }) {
69
+ const phase = contract ? 'contract' : 'expand';
70
+ const report = { schema: 'starci/account-doors-report@1', root, write, phase, changes: [], findings: [], next: [] };
71
+ const declaration = JSON.parse(fs.readFileSync(path.join(root, 'hfs.json'), 'utf8'));
72
+ if (declaration.edition !== 'lite') {
73
+ report.findings.push({ file: 'hfs.json', reason: 'the account doors belong to the lite edition; a full app signs in through its identity provider' });
74
+ return report;
75
+ }
76
+ const names = migrationsOf(root);
77
+ const parts = partialParts(fs.readFileSync(DOORS_PARTIAL, 'utf8'));
78
+ const defined = new Set(names.flatMap((name) => definedDoors(statementsOf(root, name))));
79
+ const missing = parts.doors.filter((door) => !defined.has(`${door.name}(${door.types})`));
80
+ const stamp = nextStamp(names, now());
81
+ if (!contract) {
82
+ if (!missing.length)
83
+ return report;
84
+ const file = `${MIGRATIONS_DIR}/${stamp}_${EXPAND_NAME}.sql`;
85
+ const kept = missing.length === parts.doors.length ? 'all' : missing.map((door) => door.name).join(', ');
86
+ report.changes.push({ file, phase, why: `creates the account doors the app lacks (${kept}; SECURITY DEFINER, pinned search_path, EXECUTE for the application role alone) and the lower(email) index the by-address door reads` });
87
+ report.next.push('run `npx starci app sync --execute` (the sign-in glue moves onto the doors and the retired entity files are deleted), then verify and release both together', 'then run `npx starci app codemod account-doors --contract --execute` to revoke the old grants (R255 reports them until that migration is in the chain) and release the three together');
88
+ const text = `${parts.head}${missing.map((door) => door.block).join('\n')}\n${missing.map((door) => door.grants).join('')}`;
89
+ if (write)
90
+ fs.writeFileSync(path.join(root, file), text, { flag: 'wx' });
91
+ return report;
92
+ }
93
+ if (missing.length)
94
+ report.findings.push({ file: MIGRATIONS_DIR, reason: `no migration defines the account doors ${missing.map((door) => door.name).join(', ')}: run the expand step first` });
95
+ for (const file of accountTableReferences(root)) {
96
+ report.findings.push({ file, reason: 'still maps or names the auth tables; run `npx starci app sync --execute` so the sign-in glue uses the doors, and release that before the contract migration' });
97
+ }
98
+ if (report.findings.length)
99
+ return report;
100
+ if (names.some((name) => name.includes(`_${CONTRACT_NAME}`))) {
101
+ report.next.push('the contract migration is already in the chain: nothing is left to revoke');
102
+ return report;
103
+ }
104
+ if (!names.some((name) => AUTH_GRANT.test(statementsOf(root, name)))) {
105
+ report.next.push('no migration grants the application role on schema auth (the app reached accounts through the doors from the start): nothing to revoke');
106
+ return report;
107
+ }
108
+ const file = `${MIGRATIONS_DIR}/${stamp}_${CONTRACT_NAME}.sql`;
109
+ report.changes.push({ file, phase, why: 'revokes the direct grants on schema auth, auth.users and auth.identities from the application role' });
110
+ report.next.push('the deploy applies it right after the accept; the release before the doors cannot sign in after a rollback unless the grants are given back');
111
+ if (write)
112
+ fs.writeFileSync(path.join(root, file), contractText(appRoleOf(root)), { flag: 'wx' });
113
+ return report;
114
+ }
115
+ const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
116
+ /** Render the migrations and what is left for a person as Markdown; JSON consumers read the same report object. */
117
+ export function accountDoorsMarkdown(report) {
118
+ const out = ['# account-doors', '', `Step ${report.phase}: ${report.changes.length} migration${report.changes.length === 1 ? '' : 's'} ${report.write ? 'written' : 'to write'}, ${report.findings.length} left for a person.`, ''];
119
+ if (report.findings.length) {
120
+ out.push('## Left for a person', '', '| location | reason |', '|---|---|');
121
+ for (const finding of report.findings)
122
+ out.push(`| ${cell(finding.file)} | ${cell(finding.reason)} |`);
123
+ out.push('');
124
+ }
125
+ if (report.changes.length) {
126
+ out.push('## Migrations', '', '| file | step | why |', '|---|---|---|');
127
+ for (const change of report.changes)
128
+ out.push(`| ${cell(change.file)} | ${change.phase} | ${cell(change.why)} |`);
129
+ out.push('');
130
+ }
131
+ if (report.next.length)
132
+ out.push('## Next', '', ...report.next.map((line) => `- ${line}`), '');
133
+ return `${out.join('\n')}\n`;
134
+ }
@@ -0,0 +1,5 @@
1
+ import { type Prettier } from '../sync/format.mjs';
2
+ /** The app's own Prettier, from the first side or the root that installs one, or undefined. */
3
+ export declare function prettierOf(root: string, folders: readonly string[]): Prettier | undefined;
4
+ /** `text` formatted by the app's Prettier the way `app check --format` judges it, or unchanged when the file is ignored or cannot be formatted. */
5
+ export declare function formatted(prettier: Prettier, root: string, file: string, text: string): Promise<string>;
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadPrettier } from '../sync/format.mjs';
4
+ /** The app's own Prettier, from the first side or the root that installs one, or undefined. */
5
+ export function prettierOf(root, folders) {
6
+ for (const folder of [...folders, root]) {
7
+ try {
8
+ return loadPrettier(folder);
9
+ }
10
+ catch { /* the next folder */ }
11
+ }
12
+ return undefined;
13
+ }
14
+ /** `text` formatted by the app's Prettier the way `app check --format` judges it, or unchanged when the file is ignored or cannot be formatted. */
15
+ export async function formatted(prettier, root, file, text) {
16
+ const ignorePath = path.join(root, '.prettierignore');
17
+ try {
18
+ const info = await prettier.getFileInfo(file, { ignorePath: fs.existsSync(ignorePath) ? ignorePath : undefined, resolveConfig: true });
19
+ if (info.ignored || !info.inferredParser)
20
+ return text;
21
+ const options = (await prettier.resolveConfig(file, { editorconfig: true, useCache: true })) ?? {};
22
+ return await prettier.format(text, { ...options, filepath: file });
23
+ }
24
+ catch {
25
+ return text;
26
+ }
27
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { declarationSurface, isEmittableSource } from '../runtime/scripts/lib/declaration-surface.mjs';
5
5
  import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
6
- import { loadPrettier } from '../sync/format.mjs';
6
+ import { formatted, prettierOf } from './app-prettier.mjs';
7
7
  import { applyEdits, importEdits } from './explicit-surface-edit.mjs';
8
8
  import { conventionType, MAX_TYPE_LENGTH, renderType } from './explicit-surface-render.mjs';
9
9
  import { GENERATED, relativePosix, sideFolders } from './source-tree.mjs';
@@ -212,30 +212,6 @@ async function sideSources(root, side, folder) {
212
212
  visit(folder);
213
213
  return { files: files.sort(), scope: selects === undefined ? 'walk' : 'canon' };
214
214
  }
215
- /** The app's own Prettier, from the first side or the root that installs one, or undefined. */
216
- function prettierOf(root, folders) {
217
- for (const folder of [...folders, root]) {
218
- try {
219
- return loadPrettier(folder);
220
- }
221
- catch { /* the next folder */ }
222
- }
223
- return undefined;
224
- }
225
- /** `text` formatted by the app's Prettier the way `app check --format` judges it, or unchanged when the file is ignored or cannot be formatted. */
226
- async function formatted(prettier, root, file, text) {
227
- const ignorePath = path.join(root, '.prettierignore');
228
- try {
229
- const info = await prettier.getFileInfo(file, { ignorePath: fs.existsSync(ignorePath) ? ignorePath : undefined, resolveConfig: true });
230
- if (info.ignored || !info.inferredParser)
231
- return text;
232
- const options = (await prettier.resolveConfig(file, { editorconfig: true, useCache: true })) ?? {};
233
- return await prettier.format(text, { ...options, filepath: file });
234
- }
235
- catch {
236
- return text;
237
- }
238
- }
239
215
  /** The side-relative name a report gives `absolute`. */
240
216
  const nameOf = (root, absolute) => relativePosix(root, absolute);
241
217
  /** Plans every open file of one program, checks that the rewritten files read no worse than the originals, and returns what each file becomes. */
package/codemod/main.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { accountDoorsMarkdown, migrateAccountDoors } from './account-doors.mjs';
3
4
  import { codemodMain as buttonIntentMain } from './button-intent.mjs';
4
5
  import { migratePageNavigation, pageNavigationMarkdown } from './page-navigation.mjs';
5
6
  import { loadingStatesMarkdown, migrateLoadingStates } from './loading-states.mjs';
@@ -9,10 +10,14 @@ import { migrateSpacingRoles, spacingRolesMarkdown } from './spacing-roles.mjs';
9
10
  import { migrateSurfaceCardKinds, surfaceCardKindsMarkdown } from './surface-card-kinds.mjs';
10
11
  import { explicitSurfaceMarkdown, migrateExplicitSurface } from './explicit-surface.mjs';
11
12
  import { directiveFirstMarkdown, migrateDirectiveFirst } from './directive-first.mjs';
13
+ import { migrateModuleDefinition, moduleDefinitionMarkdown } from './module-definition.mjs';
14
+ import { migrateQualityBinding, qualityBindingMarkdown } from './quality-binding.mjs';
15
+ import { migrateSopsRules, sopsRulesMarkdown } from './sops-rules.mjs';
16
+ import { migrateSecretsCache, secretsCacheMarkdown } from './secrets-cache.mjs';
12
17
  import { migrateThemeTokens, themeTokensMarkdown } from './theme-tokens.mjs';
13
18
  import { migrateTierBinding, tierBindingMarkdown } from './tier-binding.mjs';
14
19
  /** The codemods `starci app codemod` runs, by name. */
15
- export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states', 'explicit-surface', 'directive-first']);
20
+ export const CODEMODS = Object.freeze(['button-intent', 'spacing-roles', 'theme-tokens', 'small-renames', 'page-navigation', 'section-frame', 'surface-card-kinds', 'tier-binding', 'loading-states', 'explicit-surface', 'directive-first', 'secrets-cache', 'module-definition', 'sops-rules', 'quality-binding', 'account-doors']);
16
21
  /** The arguments of every report-style `starci app codemod`: --execute, --json, --format and --out; any other is refused. */
17
22
  export function spacingRolesArguments(argv) {
18
23
  const options = { write: false, format: 'markdown' };
@@ -46,6 +51,18 @@ export function spacingRolesArguments(argv) {
46
51
  }
47
52
  /** One of the report-style codemods run at an app root: its report object and the Markdown of it. */
48
53
  function runCodemod(name, root, write) {
54
+ if (name === 'quality-binding') {
55
+ const report = migrateQualityBinding({ root, write });
56
+ return { report, markdown: qualityBindingMarkdown(report) };
57
+ }
58
+ if (name === 'sops-rules') {
59
+ const report = migrateSopsRules({ root, write });
60
+ return { report, markdown: sopsRulesMarkdown(report) };
61
+ }
62
+ if (name === 'secrets-cache') {
63
+ const report = migrateSecretsCache({ root, write });
64
+ return { report, markdown: secretsCacheMarkdown(report) };
65
+ }
49
66
  if (name === 'directive-first') {
50
67
  const report = migrateDirectiveFirst({ root, write });
51
68
  return { report, markdown: directiveFirstMarkdown(report) };
@@ -105,6 +122,19 @@ async function explicitSurfaceMain(argv, io) {
105
122
  const report = await migrateExplicitSurface({ root: path.resolve(io.cwd), write: options.write });
106
123
  return emit(options, { report, markdown: explicitSurfaceMarkdown(report) }, io);
107
124
  }
125
+ /** `starci app codemod module-definition`: it formats each rewritten definition with the app's Prettier, which is asynchronous. */
126
+ async function moduleDefinitionMain(argv, io) {
127
+ const options = reportArguments(argv);
128
+ const report = await migrateModuleDefinition({ root: path.resolve(io.cwd), write: options.write });
129
+ return emit(options, { report, markdown: moduleDefinitionMarkdown(report) }, io);
130
+ }
131
+ /** `starci app codemod account-doors [--contract]`: the expand migration of the account doors, or (a release later) the contract migration that revokes the old grants. */
132
+ function accountDoorsMain(argv, io) {
133
+ const contract = argv.includes('--contract');
134
+ const options = reportArguments(argv.filter((arg) => arg !== '--contract'));
135
+ const report = migrateAccountDoors({ root: path.resolve(io.cwd), write: options.write, contract });
136
+ return emit(options, { report, markdown: accountDoorsMarkdown(report) }, io);
137
+ }
108
138
  /**
109
139
  * CLI boundary of `starci app codemod <name>`. Dry run by default; --execute writes only the safe rewrites. A codemod
110
140
  * that leaves a case for a person exits 1 with the complete report.
@@ -115,6 +145,10 @@ export function codemodMain(argv, io) {
115
145
  return buttonIntentMain(argv, io);
116
146
  if (name === 'explicit-surface')
117
147
  return explicitSurfaceMain(rest, io);
148
+ if (name === 'account-doors')
149
+ return accountDoorsMain(rest, io);
150
+ if (name === 'module-definition')
151
+ return moduleDefinitionMain(rest, io);
118
152
  if (name === undefined || !CODEMODS.includes(name))
119
153
  throw new Error(`starci app codemod takes ${CODEMODS.join(' or ')}`);
120
154
  const options = reportArguments(rest);
@@ -0,0 +1,59 @@
1
+ import type { Prettier } from '../sync/format.mjs';
2
+ /** The TypeScript compiler API of the app's own install (loadTypescript), typed by the version this package develops against. */
3
+ type Ts = typeof import('typescript');
4
+ /** One definition the migration rewrites: which form it was written in, the helper it became and the names it exports. */
5
+ export interface ModuleDefinitionChange {
6
+ file: string;
7
+ from: 'destructured builder' | 'typed builder';
8
+ helper: string;
9
+ names: string[];
10
+ }
11
+ /** One case left for a person. */
12
+ export interface ModuleDefinitionFinding {
13
+ file: string;
14
+ line: number;
15
+ code: string;
16
+ reason: string;
17
+ }
18
+ /** The report of a migration (`starci app codemod module-definition`). */
19
+ export interface ModuleDefinitionReport {
20
+ schema: string;
21
+ root: string;
22
+ write: boolean;
23
+ filesScanned: number;
24
+ filesChanged: string[];
25
+ changes: ModuleDefinitionChange[];
26
+ findings: ModuleDefinitionFinding[];
27
+ /** The `@starci/nest-kit` requirement of the app's root package.json: added by `--execute` when a definition changed, then `npm install` resolves it. */
28
+ dependency: {
29
+ name: string;
30
+ version: string;
31
+ declared: boolean;
32
+ added: boolean;
33
+ };
34
+ }
35
+ /** Rewrite one definition file's text to the helper form; `text` unchanged when the file holds no definition the codemod can rewrite. */
36
+ export declare function transformModuleDefinition({ ts, text, file }: {
37
+ ts: Ts;
38
+ text: string;
39
+ file?: string;
40
+ }): {
41
+ text: string;
42
+ changes: ModuleDefinitionChange[];
43
+ findings: ModuleDefinitionFinding[];
44
+ };
45
+ /** What `migrateModuleDefinition` takes: the app root, whether to write, and the compiler and formatter of the app (found in it when left out; `prettier: null` writes the codemod's own layout). */
46
+ export interface ModuleDefinitionOptions {
47
+ root: string;
48
+ write?: boolean;
49
+ ts?: Ts | null;
50
+ prettier?: Prettier | null;
51
+ }
52
+ /**
53
+ * Preview or apply the rewrite in one app: every `*.module-definition.ts` under its sides, each rewritten file formatted by the app's Prettier
54
+ * (so `app check --format` and the app's hook accept it as written), then the dependency in the root package.json.
55
+ */
56
+ export declare function migrateModuleDefinition({ root, write, ts, prettier }: ModuleDefinitionOptions): Promise<ModuleDefinitionReport>;
57
+ /** Render the rewrite and what is left for a person as Markdown; JSON consumers read the same report object. */
58
+ export declare function moduleDefinitionMarkdown(report: ModuleDefinitionReport): string;
59
+ export {};