@starci/hfs 5.3.0 → 5.5.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 (137) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -3
  3. package/cache/test-family.d.mts +1 -1
  4. package/cache/test-family.mjs +20 -15
  5. package/codemod/account-doors.mjs +7 -1
  6. package/codemod/app-prettier.d.mts +5 -0
  7. package/codemod/app-prettier.mjs +27 -0
  8. package/codemod/explicit-surface.mjs +1 -25
  9. package/codemod/main.mjs +14 -5
  10. package/codemod/module-definition.d.mts +10 -3
  11. package/codemod/module-definition.mjs +32 -17
  12. package/codemod/seeds-layout.d.mts +36 -0
  13. package/codemod/seeds-layout.mjs +113 -0
  14. package/codemod/sops-rules.d.mts +1 -1
  15. package/codemod/sops-rules.mjs +8 -3
  16. package/lint/run.d.mts +5 -4
  17. package/lint/run.mjs +53 -27
  18. package/package.json +1 -1
  19. package/release/lint-report.mjs +4 -3
  20. package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
  21. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  22. package/runtime/knowledge/hfs/rules.yaml +15 -2
  23. package/runtime/knowledge/hfs/slots.yaml +15 -18
  24. package/runtime/modules/kernel/failure-codes.yaml +64 -3
  25. package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
  26. package/runtime/scripts/hfs/check.mjs +4 -1
  27. package/runtime/scripts/hfs/coverage-scope.d.mts +15 -0
  28. package/runtime/scripts/hfs/coverage-scope.mjs +26 -0
  29. package/runtime/scripts/hfs/db-squash.mjs +9 -2
  30. package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
  31. package/runtime/scripts/hfs/rules/app-root.mjs +4 -1
  32. package/runtime/scripts/hfs/rules/ci-standard.mjs +5 -4
  33. package/runtime/scripts/hfs/rules/coverage-below.mjs +12 -4
  34. package/runtime/scripts/hfs/rules/database-config.mjs +8 -7
  35. package/runtime/scripts/hfs/rules/database-constants.d.mts +2 -1
  36. package/runtime/scripts/hfs/rules/database-constants.mjs +2 -1
  37. package/runtime/scripts/hfs/rules/database.d.mts +2 -2
  38. package/runtime/scripts/hfs/rules/database.mjs +7 -6
  39. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  40. package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -1
  41. package/runtime/scripts/hfs/rules/monorepo.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/seeds.d.mts +21 -0
  43. package/runtime/scripts/hfs/rules/seeds.mjs +194 -0
  44. package/runtime/scripts/hfs/rules/stacks.mjs +8 -4
  45. package/runtime/scripts/hfs/slots.d.mts +2 -0
  46. package/runtime/scripts/hfs/slots.mjs +1 -0
  47. package/runtime/scripts/hfs/stack-seeds-config.d.mts +88 -0
  48. package/runtime/scripts/hfs/stack-seeds-config.mjs +239 -0
  49. package/runtime/scripts/hfs/view.d.mts +25 -0
  50. package/runtime/scripts/hfs/view.mjs +41 -0
  51. package/scaffold/add-app.mjs +2 -2
  52. package/scaffold/add-cli-lite.mjs +41 -14
  53. package/scaffold/add-module.mjs +6 -3
  54. package/scaffold/lite-exports.mjs +1 -1
  55. package/scaffold/module-registry.mjs +10 -8
  56. package/src/main.mjs +8 -8
  57. package/stack/deploy.d.mts +3 -0
  58. package/stack/deploy.mjs +4 -1
  59. package/stack/local-up.mjs +25 -3
  60. package/stack/seeds.d.mts +29 -0
  61. package/stack/seeds.mjs +197 -0
  62. package/sync/canon-layout.d.mts +38 -0
  63. package/sync/canon-layout.mjs +62 -0
  64. package/sync/ci-commands.d.mts +2 -1
  65. package/sync/ci-commands.mjs +10 -7
  66. package/sync/compile-guard.d.mts +36 -0
  67. package/sync/compile-guard.mjs +189 -0
  68. package/sync/index.d.mts +22 -3
  69. package/sync/index.mjs +80 -32
  70. package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
  71. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
  72. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +26 -0
  73. package/templates/app/skeleton/.starcistacks/dev/README.md +21 -10
  74. package/templates/app/skeleton/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  75. package/templates/app/skeleton/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  76. package/templates/app/skeleton/.starcistacks/dev/seeds/seeds.yaml +3 -0
  77. package/templates/app/skeleton/.starcistacks/production/seeds/data/.gitkeep +0 -0
  78. package/templates/app/skeleton/.starcistacks/production/seeds/files/.gitkeep +0 -0
  79. package/templates/app/skeleton/.starcistacks/production/seeds/seeds.yaml +2 -0
  80. package/templates/app/skeleton/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  81. package/templates/app/skeleton/.starcistacks/seeds/data/.gitkeep +0 -0
  82. package/templates/app/skeleton/.starcistacks/seeds/files/.gitkeep +0 -0
  83. package/templates/app/skeleton/.starcistacks/seeds/seeds.yaml +2 -0
  84. package/templates/app/skeleton/.starcistacks/seeds/sql/.gitkeep +0 -0
  85. package/templates/app/skeleton/README.md +1 -1
  86. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  87. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/data/.gitkeep +0 -0
  88. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/files/.gitkeep +0 -0
  89. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/seeds.yaml +2 -0
  90. package/templates/app/skeleton-lite/.starcistacks/dev/seeds/sql/.gitkeep +0 -0
  91. package/templates/app/skeleton-lite/.starcistacks/production/seeds/data/.gitkeep +0 -0
  92. package/templates/app/skeleton-lite/.starcistacks/production/seeds/files/.gitkeep +0 -0
  93. package/templates/app/skeleton-lite/.starcistacks/production/seeds/seeds.yaml +2 -0
  94. package/templates/app/skeleton-lite/.starcistacks/production/seeds/sql/.gitkeep +0 -0
  95. package/templates/app/skeleton-lite/.starcistacks/seeds/data/.gitkeep +0 -0
  96. package/templates/app/skeleton-lite/.starcistacks/seeds/files/.gitkeep +0 -0
  97. package/templates/app/skeleton-lite/.starcistacks/seeds/seeds.yaml +2 -0
  98. package/templates/app/skeleton-lite/.starcistacks/seeds/sql/.gitkeep +0 -0
  99. package/templates/app/skeleton-lite/README.md +2 -2
  100. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +3 -1
  101. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +2 -0
  102. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +6 -1
  103. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +14 -1
  104. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +5 -1
  105. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +0 -1
  106. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +7 -0
  107. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +15 -0
  108. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +9 -3
  109. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +79 -16
  110. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +39 -9
  111. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +108 -20
  112. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +34 -7
  113. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +28 -4
  114. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
  115. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +6 -1
  116. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +9 -2
  117. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +11 -2
  118. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +63 -10
  119. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +10 -2
  120. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +25 -4
  121. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +8 -1
  122. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +3 -1
  123. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +3 -3
  124. package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +1 -1
  125. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +3 -2
  126. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +6 -2
  127. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +14 -9
  128. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +3 -3
  129. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +32 -7
  130. package/templates/be/tool-config/jest.config.js +1 -1
  131. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +4 -1
  132. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +3 -0
  133. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +14 -6
  134. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +1 -0
  135. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +1 -0
  136. package/templates/app/module/jest.config.js.tpl +0 -8
  137. /package/templates/app/skeleton/.starcistacks/dev/seeds/{primary-notes.sql → sql/primary-notes.sql} +0 -0
package/sync/index.mjs CHANGED
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { braceVariants } from '../runtime/scripts/lib/glob.mjs';
24
- import { codecovPaths, coverageComponents, jestCoverage, sonarCoverageExclusions } from '../runtime/scripts/hfs/coverage-scope.mjs';
24
+ import { codecovPaths, coverageComponents, jestCoverage, packagesCoverage, sonarCoverageExclusions } from '../runtime/scripts/hfs/coverage-scope.mjs';
25
25
  import { APP_SCOPE, SIDES, litePresenceOf, loadSlotManifest, resolveAppDeclaration, slotInEdition } from '../runtime/scripts/hfs/slots.mjs';
26
26
  import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
27
27
  import { declaredSlotEnabled } from '../runtime/scripts/hfs/declaration-slots.mjs';
@@ -29,12 +29,14 @@ import { feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
29
29
  import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs/rules/docker.mjs';
30
30
  import { isFile } from '../runtime/scripts/lib/fs-kind.mjs';
31
31
  import { readDeclaredSonarKey } from './sonar-key.mjs';
32
+ import { RETIRED_GROUP, guardWrites, heldLines, isGuardedSource } from './compile-guard.mjs';
32
33
  import { readDeclaredStack, stackWorkflowVariables } from './stack-images.mjs';
33
34
  import { DB_DOCKERFILE, DB_IMAGE_APP, RETIRED_DB_DOCKERFILE } from '../db/image.mjs';
34
35
  import { byCodeUnit } from '../report/order.mjs';
35
36
  import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
36
37
  import { ciWorkflowCommands } from './ci-commands.mjs';
37
38
  import { MODULE_PREFIX, moduleNames, registryFiles } from '../scaffold/module-registry.mjs';
39
+ import { canonImport, canonList } from './canon-layout.mjs';
38
40
  import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
39
41
  import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
40
42
  import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
@@ -99,7 +101,7 @@ function slotTargets(slot, scope, edition, manifest, hasTemplate, enabled) {
99
101
  const template = managedTemplate(scope, group, file, hasTemplate);
100
102
  if (!template)
101
103
  throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${group} has no template for it`);
102
- return { path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id };
104
+ return { path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id, group };
103
105
  });
104
106
  }
105
107
  function unlistedTargets(scope, edition) {
@@ -248,16 +250,23 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
248
250
  * lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
249
251
  * their content. An app with no be folder has no source files.
250
252
  */
251
- /** The folders of `packages/` that are neither a module package nor the registry. */
253
+ /**
254
+ * The packages of `packages/` that are neither a module package nor the registry: a folder that holds a `package.json` and a `src/` (an install folder
255
+ * the workspaces leave there, `packages/node_modules`, is none).
256
+ */
252
257
  function otherPackages(root) {
253
258
  try {
254
259
  return fs.readdirSync(path.join(root, 'packages'), { withFileTypes: true })
255
- .filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules').map(entry => entry.name).sort(byCodeUnit);
260
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules'
261
+ && isFile(path.join(root, 'packages', entry.name, 'package.json'), { followLinks: false }) && fs.existsSync(path.join(root, 'packages', entry.name, 'src')))
262
+ .map(entry => entry.name).sort(byCodeUnit);
256
263
  }
257
264
  catch {
258
265
  return [];
259
266
  }
260
267
  }
268
+ /** The app-relative folders of the root packages whose logic the back end's unit run measures: every module package and every other package, never the registry. */
269
+ export const rootPackageFolders = (source) => [...(source?.modules ?? []).map(name => `packages/${MODULE_PREFIX}${name}`), ...(source?.otherPackages ?? []).map(name => `packages/${name}`)];
261
270
  export function appSource(root, { parseYaml, notices } = {}) {
262
271
  const files = [];
263
272
  const walk = (dir) => {
@@ -282,7 +291,7 @@ export function appSource(root, { parseYaml, notices } = {}) {
282
291
  walk(dir);
283
292
  return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: (file) => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
284
293
  modules: moduleNames(root), otherPackages: otherPackages(root),
285
- workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: (file) => isFile(path.join(root, file), { followLinks: false }),
294
+ workPresent: fs.existsSync(path.join(root, '.starciwork')), hasContracts: fs.existsSync(path.join(root, 'be', 'contracts')), hasImage: (file) => isFile(path.join(root, file), { followLinks: false }),
286
295
  stack: readDeclaredStack(root, { parseYaml }), quality: readDeclaredQuality(root, { parseYaml }),
287
296
  badges: readDeclaredBadges(root, { parseYaml, notices }),
288
297
  qualityDeclared: hasDeclaredServices(root, { parseYaml }) };
@@ -336,7 +345,7 @@ export function appScripts(app, source) {
336
345
  }),
337
346
  ...fe.map(entry => line(`start:${entry.name}`, `npm run start -w ${workspace(entry)}`)),
338
347
  ...(app.edition !== 'lite' && app.optionalSlots.includes('app.browser') ? [line('test:browser', 'playwright test -c browser/playwright.config.ts')] : []),
339
- ...(source?.modules?.length ? [line('build:modules', 'turbo run build --filter=./packages/*'), line('test:modules', 'turbo run test --filter=./packages/module-*')] : []),
348
+ ...(rootPackageFolders(source).length ? [line('build:modules', 'turbo run build --filter=./packages/*')] : []),
340
349
  line('docker:build', images.map(image => `npm run docker:build:${image.entry.name}`).join(' && ')),
341
350
  ...images.map(image => line(`docker:build:${image.entry.name}`, imageCommand(image))),
342
351
  ].join('\n ');
@@ -376,11 +385,13 @@ const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/wo
376
385
  /**
377
386
  * The Dockerfile of every declared app, [{ path, content }] (app-relative): written once by starci app scaffold from templates/<side>/image/<kind>
378
387
  * and then the app's own, judged by the docker rules (R187-R191). A Next image copies the fe packages only when the fe side opts into
379
- * them and the be contracts only when the fe side reads them, so each COPY names a folder the app has.
388
+ * them and the be contracts only when the fe side reads them and the back end holds them (`source.hasContracts`: a back end that emits no
389
+ * contract has no be/contracts/, and a COPY of a missing folder fails the image build), so each COPY names a folder the app has.
380
390
  */
381
391
  export function imageFiles(app, source) {
382
392
  const side = app.sides.fe;
383
- const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
393
+ const contracts = (side.reads ?? []).includes('be/contracts/') && source?.hasContracts === true;
394
+ const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...(contracts ? ['be/contracts'] : [])];
384
395
  const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
385
396
  // The folders outside be/ that the back end declares it reads (the generated database types of a lite back end, db/types/) are in every one of its images.
386
397
  const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
@@ -393,32 +404,49 @@ export function imageFiles(app, source) {
393
404
  });
394
405
  }
395
406
  /** Every value a template of `scope` can name, derived from hfs.json and the presets. */
396
- /** What a module package keeps out of the measured scope: its workbench (drawn with Grammar, a front end has no tests) and its specs. The logic of its template and manifest is measured. */
397
- const moduleExclusions = (dirs) => dirs.flatMap(dir => [`${dir}/src/workbench/**`, `${dir}/src/**/index.ts`]);
398
- /** The managed files of the registry package for the module packages the app has (empty when it has none). */
407
+ /**
408
+ * The `packages` option of the managed be/jest.config.js (`starciJestConfig`), laid out as prettier lays it out (width 120, indent 4): the root packages whose
409
+ * specs the unit run holds, with the files measured in them; an app without one has no such option.
410
+ */
411
+ export function jestPackagesSource(packages) {
412
+ if (packages.folders.length === 0)
413
+ return '';
414
+ const list = (key, items) => {
415
+ const one = ` ${key}: [${items.map(item => JSON.stringify(item)).join(', ')}],`;
416
+ return one.length <= 120 ? one : [` ${key}: [`, ...items.map(item => ` ${JSON.stringify(item)},`), ' ],'].join('\n');
417
+ };
418
+ return ['', ' packages: {', list('folders', packages.folders), list('files', packages.files), list('excludes', packages.excludes), ' },'].join('\n');
419
+ }
420
+ /**
421
+ * The managed files of the registry package for the module packages the app has (empty when it has none). An app that keeps its modules as data
422
+ * (hfs.json `modules: "data"`) has no rendered registry: a module is a seed there, and packages/modules, if any, is the app's own.
423
+ */
399
424
  function registryTargets(app, source) {
425
+ if (app.modules === 'data')
426
+ return [];
400
427
  return registryFiles(app.project, source?.modules ?? []).map((file) => ({ path: file.path, scope: APP_SCOPE, mode: 'file', content: file.content, hash: hashOf(file.content) }));
401
428
  }
402
429
  /**
403
- * The root `eslint.config.mjs` of an app that has root workspace packages (packages/agent-sdk, packages/module-<name>, the registry): no side config
430
+ * The root `eslint.config.mjs` of an app that has root workspace packages (packages/<name>: the agent SDK, a block library, a module package, the registry): no side config
404
431
  * reaches `packages/`, so this managed file lints their source by subpath. The back-end canon judges everything but the front-end subpaths
405
432
  * (`src/workbench/` outside its contract, `src/public/`), the front-end canon and the selected Grammar family those, each over the package view of
406
433
  * the app root. An app without a root package has no such file; the family factory for packages is the family's `<name>GrammarPackagesConfig`.
434
+ * It is laid out as the canon Prettier config prints it (`canonList`, `canonImport`), so the app's format check passes it as rendered.
407
435
  */
408
436
  function packagesLintTargets(app, source) {
409
437
  if (!(source?.modules?.length || source?.otherPackages?.length))
410
438
  return [];
411
439
  const grammar = grammarLintFor(app.sides.fe.grammar);
412
- const factory = grammar.package ? grammar.factory.replace(/Config$/, 'PackagesConfig') : null;
440
+ const family = grammar.package ? { name: grammar.package, factory: grammar.factory.replace(/Config$/, 'PackagesConfig') } : null;
413
441
  const content = [
414
442
  'import { loadPackagesHfs as loadBePackages, starciBePackagesConfig } from "@starci/eslint-canon-be"',
415
443
  'import { loadPackagesHfs as loadFePackages, starciFePackagesConfig } from "@starci/eslint-canon-fe"',
416
- ...(grammar.package ? [`import { ${factory} } from "${grammar.package}"`] : []),
444
+ ...(family ? [canonImport({ names: [family.factory], from: family.name })] : []),
417
445
  '',
418
446
  'const be = loadBePackages(import.meta.url)',
419
447
  'const fe = loadFePackages(import.meta.url)',
420
448
  '',
421
- `export default [...(await starciBePackagesConfig({ hfs: be })), ...starciFePackagesConfig({ hfs: fe })${grammar.package ? `, ...${factory}({ hfs: fe })` : ''}]`,
449
+ canonList({ head: 'export default ', items: ['...(await starciBePackagesConfig({ hfs: be }))', '...starciFePackagesConfig({ hfs: fe })', ...(family ? [`...${family.factory}({ hfs: fe })`] : [])] }),
422
450
  '',
423
451
  ].join(LINE);
424
452
  return [{ path: PACKAGES_LINT_FILE, scope: APP_SCOPE, mode: 'file', content, hash: hashOf(content) }];
@@ -430,10 +458,11 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
430
458
  const lite = app.edition === 'lite';
431
459
  const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
432
460
  const quality = source.quality ?? qualityServices({});
433
- const modules = source.modules ?? [];
434
- const moduleDirs = modules.map(name => `packages/${MODULE_PREFIX}${name}`);
435
- // The module packages are built before anything imports them (typecheck, lint, the fe build and the be run all follow the registry).
436
- const modulesFirst = modules.length ? 'npm run build:modules --silent && ' : '';
461
+ // The root packages (the agent SDK, the module packages and the rest) are built before anything imports them (typecheck, lint, the fe build and the be
462
+ // run), and their logic is measured by the back end's unit run like its own (packagesCoverage, BE-TEST-13).
463
+ const rootPackages = packagesCoverage(rootPackageFolders(source));
464
+ const moduleDirs = rootPackages.folders;
465
+ const modulesFirst = moduleDirs.length ? 'npm run build:modules --silent && ' : '';
437
466
  const grammarLint = scope === 'fe' ? grammarLintFor(fe.grammar) : null;
438
467
  return {
439
468
  header: HEADER(scope),
@@ -456,9 +485,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
456
485
  typecheck: `${modulesFirst}npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck`,
457
486
  // The typed lint reads the types the fe packages declare (their dist), so the packages are built first.
458
487
  lintPrefix: `${modulesFirst}${packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '}`,
459
- // The unit run, the back-end build and every CI job that imports a module package find it built (build:modules precedes them); the logic of a package is held at 100 per file by its own jest run, a step of the fast job.
488
+ // The unit run, the back-end build and every CI job that imports a root package find it built (build:modules precedes them); its specs run in the unit run and its shards.
460
489
  modulesFirst,
461
- moduleTestStep: modules.length ? ' - name: module packages\n run: npm run test:modules\n' : '',
462
490
  nodeMajor: String(NODE_MAJOR),
463
491
  // The deploy server's age identity the restore workflow writes (stack/server-key.mjs owns the path).
464
492
  serverAgeIdentity: SERVER_AGE_IDENTITY,
@@ -471,12 +499,13 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
471
499
  sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : []), ...moduleDirs.map(dir => `${dir}/src`)].join(','),
472
500
  lcovReport: LCOV_REPORT,
473
501
  cpdExclusions: SONAR_CPD_EXCLUSIONS.join(','),
474
- ...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality, { app, packages }),
502
+ ...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality, { app, packages, rootPackages: moduleDirs.length > 0 }),
475
503
  // The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
476
504
  jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
477
- coverageExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].join(',') : '',
478
- codecovExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
479
- codecovPaths: scope === APP_SCOPE ? [...codecovPaths(manifest, providers), ...moduleDirs.flatMap(dir => [`${dir}/src/manifest/**`, `${dir}/src/template/**`])].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
505
+ jestPackages: scope === 'be' ? jestPackagesSource(rootPackages) : '',
506
+ coverageExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...rootPackages.excludes].join(',') : '',
507
+ codecovExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...rootPackages.excludes].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
508
+ codecovPaths: scope === APP_SCOPE ? [...codecovPaths(manifest, providers), ...moduleDirs.map(dir => `${dir}/src/**`)].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
480
509
  codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
481
510
  codecovTarget: quality.codecov.target,
482
511
  dependabotUpdates: dependabotUpdates(quality.dependabot),
@@ -548,7 +577,7 @@ function renderOnce(hfs, presets, { sonarKey, readTemplate = readBundled, manife
548
577
  const markers = target.markdown ? { begin: '<!-- >>> starci app sync (managed block; do not edit) >>> -->', end: '<!-- <<< starci app sync <<< -->' } : { begin: BLOCK_BEGIN, end: BLOCK_END };
549
578
  const spacing = target.markdown ? '\n' : '';
550
579
  const managed = target.mode === 'block' ? `${markers.begin}\n${spacing}${trailingNewline(body)}${spacing}${markers.end}\n` : body;
551
- return { path: where, scope, mode: target.mode, markdown: target.markdown, markers, content: managed, hash: hashOf(managed) };
580
+ return { path: where, scope, mode: target.mode, markdown: target.markdown, markers, content: managed, hash: hashOf(managed), ...(target.group === undefined ? {} : { group: target.group }) };
552
581
  })];
553
582
  });
554
583
  }
@@ -714,6 +743,16 @@ export function retiredFiles(root, hfs) {
714
743
  walk(file);
715
744
  return found;
716
745
  }
746
+ /** The writes of `targets` and the deletions of `retired` the compile guard judges: every file sync would change, with its managed group. */
747
+ export function guardedWrites(root, targets, retired) {
748
+ const changed = checkTargets(root, targets).filter(result => result.status !== 'ok' && isGuardedSource(result.path));
749
+ const writes = changed.map((result) => {
750
+ const target = targets.find(candidate => candidate.path === result.path);
751
+ const file = path.join(root, target.path);
752
+ return { path: target.path, group: target.group ?? target.path, text: writtenContent(target, fs.existsSync(file) ? lf(fs.readFileSync(file, 'utf8')) : '') };
753
+ });
754
+ return [...writes, ...retired.map((file) => ({ path: file, group: RETIRED_GROUP, text: null }))];
755
+ }
717
756
  /** `starci app sync [--execute] [--cwd <dir>]` at the app root: the dry run reports drift, --execute writes; returns 0 clean, 1 drift, 2 usage. */
718
757
  function syncOptions(argv) {
719
758
  const write = argv.includes('--execute');
@@ -738,7 +777,7 @@ function reportSyncCheck(root, targets, out, retired = []) {
738
777
  out(`starci app sync: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx --no-install starci app sync --execute"' : ''}`);
739
778
  return bad.length || retired.length ? 1 : 0;
740
779
  }
741
- export async function runSync(argv, { cwd = process.cwd(), out = (line) => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
780
+ export async function runSync(argv, { cwd = process.cwd(), out = (line) => process.stdout.write(`${line}\n`), presets, parseYaml, loadTs } = {}) {
742
781
  const options = syncOptions(argv);
743
782
  if (!options) {
744
783
  out('usage: starci app sync [--execute] [--cwd <dir>]');
@@ -754,18 +793,27 @@ export async function runSync(argv, { cwd = process.cwd(), out = (line) => proce
754
793
  for (const notice of new Set(notices))
755
794
  out(notice);
756
795
  if (write) {
757
- const written = writeTargets(root, targets);
796
+ const { held, unchecked } = guardWrites(root, guardedWrites(root, targets, retired), loadTs);
797
+ const holds = new Set(held.flatMap(group => group.files));
798
+ const free = targets.filter(target => !holds.has(target.path));
799
+ const written = writeTargets(root, free);
800
+ const removed = retired.filter(file => !holds.has(file));
758
801
  for (const file of written)
759
802
  out(`wrote ${file}`);
760
- for (const file of retired) {
803
+ for (const file of removed) {
761
804
  fs.rmSync(path.join(root, file));
762
805
  out(`removed ${file} (retired)`);
763
806
  }
764
807
  for (const dir of LITE_RETIRED_DIRS)
765
- if (retired.length && fs.existsSync(path.join(root, dir)) && !fs.readdirSync(path.join(root, dir)).length)
808
+ if (removed.length && fs.existsSync(path.join(root, dir)) && !fs.readdirSync(path.join(root, dir)).length)
766
809
  fs.rmdirSync(path.join(root, dir));
767
- out(`starci app sync: ${written.length} written, ${targets.length - written.length} already in sync`);
768
- return 0;
810
+ for (const project of unchecked)
811
+ out(`notice: the writes compiled by ${project} were not type-checked: its TypeScript is not installed (run npm install, then "npx --no-install starci app check")`);
812
+ for (const group of held)
813
+ for (const line of heldLines(group))
814
+ out(line);
815
+ out(`starci app sync: ${written.length} written, ${free.length - written.length} already in sync${holds.size ? `, ${holds.size} held` : ''}`);
816
+ return held.length ? 1 : 0;
769
817
  }
770
818
  return reportSyncCheck(root, targets, out, retired);
771
819
  }
@@ -92,7 +92,7 @@ jobs:
92
92
  run: npm run typecheck
93
93
  env:
94
94
  NODE_OPTIONS: {{nodeOptions}}
95
- {{moduleTestStep}}
95
+
96
96
  lint:
97
97
  needs: plan
98
98
  if: ${{ needs.plan.outputs.checks == 'true' }}
@@ -101,7 +101,7 @@ jobs:
101
101
  strategy:
102
102
  fail-fast: false
103
103
  matrix:
104
- side: [be, fe]
104
+ side: {{lintSides}}
105
105
  steps:
106
106
  {{> app/ci-workflows/install-steps.yml}}
107
107
  {{> app/ci-workflows/lint-base.yml}}
@@ -92,7 +92,7 @@ jobs:
92
92
  run: npm run typecheck
93
93
  env:
94
94
  NODE_OPTIONS: {{nodeOptions}}
95
- {{moduleTestStep}}
95
+
96
96
  lint:
97
97
  needs: plan
98
98
  if: ${{ needs.plan.outputs.checks == 'true' }}
@@ -101,7 +101,7 @@ jobs:
101
101
  strategy:
102
102
  fail-fast: false
103
103
  matrix:
104
- side: [be, fe]
104
+ side: {{lintSides}}
105
105
  steps:
106
106
  {{> app/ci-workflows/install-steps.yml}}
107
107
  {{> app/ci-workflows/lint-base.yml}}
@@ -40,6 +40,32 @@ environments:
40
40
  runbook: README.md
41
41
  # Custody starts empty; add real sealed members after owner provisioning.
42
42
  secrets: []
43
+ # `starci app stack up --env dev --execute` reads this block (PROJECT-STACKS-6): the compose services in dependency order, each
44
+ # awaited on its healthcheck, then readiness and the origins. The app images come from `starci app stack prepare --env dev --execute`.
45
+ local:
46
+ ports:
47
+ core: 3001
48
+ app: 3000
49
+ landing: 3002
50
+ services:
51
+ - {name: postgres, wait: healthy}
52
+ - {name: core, wait: healthy, port: core}
53
+ - {name: app, wait: healthy, port: app}
54
+ - {name: landing, wait: healthy, port: landing}
55
+ ready:
56
+ - {name: core-live, url: "http://127.0.0.1:${port.core}/health/live"}
57
+ - {name: core-ready, url: "http://127.0.0.1:${port.core}/health/ready"}
58
+ - {name: app-live, url: "http://127.0.0.1:${port.app}/health/live"}
59
+ - {name: landing-live, url: "http://127.0.0.1:${port.landing}/health/live"}
60
+ verify:
61
+ - {name: core-live, url: "http://127.0.0.1:${port.core}/health/live"}
62
+ - {name: core-ready, url: "http://127.0.0.1:${port.core}/health/ready"}
63
+ - {name: app-live, url: "http://127.0.0.1:${port.app}/health/live"}
64
+ - {name: landing-live, url: "http://127.0.0.1:${port.landing}/health/live"}
65
+ origins:
66
+ - {name: app, url: "http://127.0.0.1:${port.app}"}
67
+ - {name: landing, url: "http://127.0.0.1:${port.landing}"}
68
+ - {name: core, url: "http://127.0.0.1:${port.core}"}
43
69
  production:
44
70
  status: supported
45
71
  runtime: docker-swarm
@@ -1,16 +1,27 @@
1
1
  # Development stack
2
2
 
3
- Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
4
- Starter release images and external secrets must be prepared before starting the container model. Local source development remains available through the app scripts.
3
+ The dev environment is one command: `starci app stack up --env dev --execute`. It checks the machine, opens the dev custody, starts the
4
+ compose services of `infra/compose/stack.yaml` in Docker Linux containers (the pinned local PostgreSQL first, then the core API and the product
5
+ and landing front ends), waits for each healthcheck and for readiness, and prints the origins. What it runs is the `local` block of
6
+ `environments.dev` in `.starcistacks/application-stacks.yaml` (PROJECT-STACKS-6 in contexts/project/stacks.md); this runbook never carries a
7
+ docker command, an agent never runs docker by hand. Trust authentication is confined to this local, unpublished database.
8
+
9
+ **A new machine.** Install what `starci app stack doctor --env dev` reports missing (it prints the exact winget, brew or apt step per tool; Node
10
+ from `.nvmrc`, npm, git, Docker with Compose v2 running Linux containers, WSL2 on Windows, sops and age), then run `prepare` and `up`.
11
+
12
+ **Custody.** The API reads its database connection from the owner-controlled file `/run/starci-secrets/{{project}}/dev/primary-db-url`, which
13
+ the compose model mounts into the API; materialize its real sealed member there before `up`. The scaffold creates no fake secret or
14
+ successful proof.
5
15
 
6
16
  | Command | Operation |
7
17
  | --- | --- |
8
- | prepare | Build the app images and provide the local database connection through sealed custody. Materialize its real sealed member to the owner-controlled file `/run/starci-secrets/{{project}}/dev/primary-db-url`; Docker Compose mounts that file into the API. |
9
- | doctor | From this directory, run `docker compose config -f infra/compose/stack.yaml`. Confirm image, secret and database availability. |
10
- | up | Run `docker compose -f infra/compose/stack.yaml up -d` from this directory after preparation. |
11
- | status | Run `docker compose -f infra/compose/stack.yaml ps`. |
12
- | logs | Run `docker compose -f infra/compose/stack.yaml logs core`; never print mounted credentials. |
13
- | down | Run `docker compose -f infra/compose/stack.yaml down`. |
14
- | verification | Confirm the API `/health/live` and `/health/ready` endpoints, then run `npm run lint` from the app root. |
18
+ | prepare | `starci app stack prepare --env dev --execute`: checks the machine, opens the custody and builds the app images. |
19
+ | doctor | `starci app stack doctor --env dev`: read-only table of host prerequisites with the owner-run install step per OS, custody, compose render and ports. |
20
+ | up | `starci app stack up --env dev --execute`: prepare, then the services in order and readiness; prints the origins. |
21
+ | status | `starci app stack status --env dev`: desired against running and every readiness probe. |
22
+ | logs | `starci app stack logs --env dev --service core` (a compose service, bounded and redacted; never prints mounted credentials). |
23
+ | down | `starci app stack down --env dev --execute`: stops the compose project; volumes and sealed values stay. |
24
+ | verification | `starci app stack verify --env dev`: the declared liveness and readiness probes after a real start, then `npm run lint` from the app root. |
15
25
 
16
- Keep real sealed members under `.starcistacks/dev/secrets/*.enc`, add them to the manifest after sealing and inventory their key names in the environment record. The scaffold creates no fake secret or successful proof.
26
+ Keep real sealed members under `.starcistacks/dev/secrets/*.enc.yaml`, add them to the manifest after sealing and inventory their key names in
27
+ `.starcistacks/dev/runtime/env/KEYS.md`.
@@ -0,0 +1,3 @@
1
+ schema: starci/seeds@1
2
+ entries:
3
+ - {id: primary-notes, kind: sql, path: sql/primary-notes.sql, target: primary, mode: upsert}
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -36,7 +36,7 @@ npm run build:fe
36
36
  The core api app reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS` (comma-separated origins allowed to send state-changing browser
37
37
  requests) and `PRIMARY_DB_URL` at boot; every door is closed until it is marked public or sign-in is designed. The cli app reads
38
38
  `PRIMARY_DB_URL`: `npm run migrate` applies the migrations of the primary database before the api starts, and
39
- `npm run cli -- seed run` runs the dev seeds of `.starcistacks/dev/seeds` (run both from the app root). The product app reads
39
+ `npm run cli -- seed run` runs the SQL seeds of `.starcistacks/seeds/sql/`, then those of `.starcistacks/dev/seeds/sql/` (`--env <env>` names another environment; run both from the app root). The product app reads
40
40
  `NEXT_PUBLIC_SITE_URL` and `NEXT_PUBLIC_GRAPHQL_URL` (the back end it calls through `fe/apps/app/src/modules/api`, read once the first GraphQL operation exists), the landing `NEXT_PUBLIC_APP_URL` (the product app it hands the reader over to). A missing key stops the process with an error that names it.
41
41
 
42
42
  Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.*.buildArgs.values`. Replace the placeholder origins for the product's `NEXT_PUBLIC_SITE_URL` and the landing's `NEXT_PUBLIC_APP_URL` with your product app's public origin before building deployment images.
@@ -2,7 +2,7 @@
2
2
 
3
3
  The dev environment is one command: `starci app stack up --env dev --execute`. It checks the machine, opens the dev custody, runs the setup
4
4
  commands, starts the dependencies in Docker Linux containers (the `db` service: the official postgres image plus pgvector, built from
5
- `.starcistacks/images/db/Dockerfile`; the `mail` service: Mailpit, the dev mail sink), gives the environment its own database role, migrates, runs the seed, starts the api and web
5
+ `.starcistacks/images/db/Dockerfile`; the `mail` service: Mailpit, the dev mail sink), gives the environment its own database role, migrates, applies the seed levels `.starcistacks/seeds/` then `.starcistacks/dev/seeds/`, starts the api and web
6
6
  processes natively and waits for readiness. What it runs is the `local` block of `environments.dev` in `.starcistacks/application-stacks.yaml`
7
7
  (PROJECT-STACKS-6 in contexts/project/stacks.md); this runbook never carries a docker command, an agent never runs docker by hand.
8
8
 
@@ -20,7 +20,7 @@ another environment.
20
20
  | --- | --- |
21
21
  | prepare | `starci app stack prepare --env dev --execute`: checks the machine, opens the custody, runs the setup commands and builds the images. |
22
22
  | doctor | `starci app stack doctor --env dev`: read-only table of host prerequisites with the owner-run install step per OS, custody, compose render and ports. |
23
- | up | `starci app stack up --env dev --execute`: prepare, then the services, the database role, migrations with a receipt, the seed, the processes and readiness; prints the origins. |
23
+ | up | `starci app stack up --env dev --execute`: prepare, then the services, the database role, migrations with a receipt, the seed levels `.starcistacks/seeds/` then `.starcistacks/dev/seeds/`, each entry once per content hash, the processes and readiness; prints the origins. |
24
24
  | status | `starci app stack status --env dev`: desired against running, the migration receipt and every readiness probe. |
25
25
  | logs | `starci app stack logs --env dev --service api` (a process or a compose service, bounded and redacted). |
26
26
  | down | `starci app stack down --env dev --execute`: ends the processes and stops the compose project; volumes and sealed values stay. |
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -0,0 +1,2 @@
1
+ schema: starci/seeds@1
2
+ entries: []
@@ -12,10 +12,10 @@ TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, Postgre
12
12
 
13
13
  ## Repository layout
14
14
 
15
- - `db/`: forward-only SQL migrations, seed data, and generated database types.
15
+ - `db/`: forward-only SQL migrations and generated database types.
16
16
  - `be/`: the API, shared platform capabilities, verified identity admission, and the passport and storage integrations.
17
17
  - `fe/`: the web app; it holds no credential, signs in through `fe/apps/web/src/modules/auth` and reaches data through `fe/apps/web/src/modules/api`.
18
- - `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
18
+ - `.starcistacks/`: value-free environment metadata, encrypted-secret custody and the seed levels (`seeds/` for every environment, `<env>/seeds/` for one, each a `seeds.yaml` manifest of `sql/`, `data/` and `files/`).
19
19
  - `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
20
20
 
21
21
  ## Status badges
@@ -25,7 +25,8 @@ end;
25
25
  $$;
26
26
 
27
27
  -- The device sessions and rotating refresh tokens of @starci/auth. A token is stored by its sha256 only; a rotated token stays,
28
- -- marked used, so a replay is detected (a replay inside the grace window is a racing tab, an older one ends the session).
28
+ -- marked used, so a replay is detected: a replay inside the grace window gets a successor of its own (raced_count bounds how many),
29
+ -- an older one ends the session and every token of it.
29
30
  -- identity_attempts counts the sign-in attempts of one address in a window every replica shares (the budget of failures).
30
31
  -- The app owns auth.users (db/bootstrap.sql) and the role app_be.
31
32
 
@@ -48,6 +49,7 @@ create table public.identity_refresh_tokens (
48
49
  created_at timestamptz not null default now(),
49
50
  expires_at timestamptz not null,
50
51
  used_at timestamptz,
52
+ raced_count integer not null default 0,
51
53
  constraint identity_refresh_tokens_hash_key unique (token_hash)
52
54
  );
53
55
 
@@ -17,6 +17,8 @@ export interface SignupData {
17
17
  readonly email: string
18
18
  /** The chosen password. */
19
19
  readonly password: string
20
+ /** The person's full name, when the request names one. */
21
+ readonly name: string | undefined
20
22
  /** The language of the confirmation mail, when the request names one. */
21
23
  readonly language: MailLanguage | undefined
22
24
  }
@@ -17,6 +17,11 @@ export class SignupHandler extends ICQRSHandler<SignupCommand, void> {
17
17
  }
18
18
 
19
19
  protected override process(command: SignupCommand): Promise<void> {
20
- return this.email.signup(command.params.request.email, command.params.request.password, command.params.request.language)
20
+ return this.email.signup(
21
+ command.params.request.email,
22
+ command.params.request.password,
23
+ command.params.request.language,
24
+ command.params.request.name,
25
+ )
21
26
  }
22
27
  }
@@ -15,7 +15,20 @@ import { ResetHandler } from "./application/reset.handler"
15
15
  import { ConfigurableModuleClass } from "./auth.module-definition"
16
16
 
17
17
  @Module({
18
- providers: [LoginHandler, RefreshHandler, SessionHandler, LogoutHandler, GoogleBeginHandler, GoogleCompleteHandler, GetJwksHandler, SignupHandler, ResendHandler, ConfirmHandler, ForgotHandler, ResetHandler],
18
+ providers: [
19
+ LoginHandler,
20
+ RefreshHandler,
21
+ SessionHandler,
22
+ LogoutHandler,
23
+ GoogleBeginHandler,
24
+ GoogleCompleteHandler,
25
+ GetJwksHandler,
26
+ SignupHandler,
27
+ ResendHandler,
28
+ ConfirmHandler,
29
+ ForgotHandler,
30
+ ResetHandler,
31
+ ],
19
32
  })
20
33
  /** The auth feature: the handlers behind the sign-in doors. */
21
34
  export class AuthApiModule extends ConfigurableModuleClass {}
@@ -96,7 +96,11 @@ export class AuthController {
96
96
  @Public({ reason: PublicReason.AuthHandshake })
97
97
  @RateLimit(RateTier.Strict)
98
98
  signup(@Body() body: SignupRequest): Promise<void> {
99
- return this.commandBus.execute(new SignupCommand({ request: { email: body.email, password: body.password, language: body.language } }))
99
+ return this.commandBus.execute(
100
+ new SignupCommand({
101
+ request: { email: body.email, password: body.password, name: body.name, language: body.language },
102
+ }),
103
+ )
100
104
  }
101
105
 
102
106
  /** Mails a new confirmation link; always answers 204. */
@@ -14,5 +14,4 @@ export class ResetRequest {
14
14
  @MinLength(1)
15
15
  @MaxLength(128)
16
16
  password!: string
17
-
18
17
  }
@@ -15,6 +15,13 @@ export class SignupRequest {
15
15
  @MaxLength(128)
16
16
  password!: string
17
17
 
18
+ /** The person's full name, kept on the account's profile keys; left out, the account has none. */
19
+ @IsOptional()
20
+ @IsString()
21
+ @MinLength(1)
22
+ @MaxLength(200)
23
+ name?: string
24
+
18
25
  /** The language of the mail; the app's default when left out. */
19
26
  @IsOptional()
20
27
  @IsIn(["vi", "en"])