@starci/hfs 5.4.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.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -3
- package/cache/test-family.d.mts +1 -1
- package/cache/test-family.mjs +20 -15
- package/codemod/main.mjs +6 -1
- package/codemod/seeds-layout.d.mts +36 -0
- package/codemod/seeds-layout.mjs +113 -0
- package/codemod/sops-rules.d.mts +1 -1
- package/codemod/sops-rules.mjs +8 -3
- package/lint/run.d.mts +5 -4
- package/lint/run.mjs +53 -27
- package/package.json +1 -1
- package/release/lint-report.mjs +4 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +15 -2
- package/runtime/knowledge/hfs/slots.yaml +15 -18
- package/runtime/modules/kernel/failure-codes.yaml +64 -3
- package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +4 -1
- package/runtime/scripts/hfs/coverage-scope.d.mts +15 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +26 -0
- package/runtime/scripts/hfs/db-squash.mjs +9 -2
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/app-root.mjs +4 -1
- package/runtime/scripts/hfs/rules/ci-standard.mjs +5 -4
- package/runtime/scripts/hfs/rules/coverage-below.mjs +12 -4
- package/runtime/scripts/hfs/rules/database-config.mjs +8 -7
- package/runtime/scripts/hfs/rules/database-constants.d.mts +2 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +2 -1
- package/runtime/scripts/hfs/rules/database.d.mts +2 -2
- package/runtime/scripts/hfs/rules/database.mjs +7 -6
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +1 -1
- package/runtime/scripts/hfs/rules/seeds.d.mts +21 -0
- package/runtime/scripts/hfs/rules/seeds.mjs +194 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +8 -4
- package/runtime/scripts/hfs/slots.d.mts +2 -0
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-seeds-config.d.mts +88 -0
- package/runtime/scripts/hfs/stack-seeds-config.mjs +239 -0
- package/runtime/scripts/hfs/view.d.mts +25 -0
- package/runtime/scripts/hfs/view.mjs +41 -0
- package/scaffold/add-app.mjs +2 -2
- package/scaffold/add-cli-lite.mjs +41 -14
- package/scaffold/add-module.mjs +6 -3
- package/scaffold/lite-exports.mjs +1 -1
- package/scaffold/module-registry.mjs +10 -8
- package/src/main.mjs +8 -8
- package/stack/deploy.d.mts +3 -0
- package/stack/deploy.mjs +4 -1
- package/stack/local-up.mjs +25 -3
- package/stack/seeds.d.mts +29 -0
- package/stack/seeds.mjs +197 -0
- package/sync/canon-layout.d.mts +38 -0
- package/sync/canon-layout.mjs +62 -0
- package/sync/ci-commands.d.mts +2 -1
- package/sync/ci-commands.mjs +10 -7
- package/sync/index.d.mts +13 -2
- package/sync/index.mjs +52 -24
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +26 -0
- package/templates/app/skeleton/.starcistacks/dev/README.md +21 -10
- package/templates/app/skeleton/.starcistacks/dev/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/seeds.yaml +3 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton/.starcistacks/production/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton/.starcistacks/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton/.starcistacks/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/production/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/data/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/files/.gitkeep +0 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/seeds.yaml +2 -0
- package/templates/app/skeleton-lite/.starcistacks/seeds/sql/.gitkeep +0 -0
- package/templates/app/skeleton-lite/README.md +2 -2
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +6 -1
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +14 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +5 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +0 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +7 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +15 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +9 -3
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +79 -16
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +39 -9
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +108 -20
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +34 -7
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +28 -4
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +6 -1
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +9 -2
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +11 -2
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +59 -9
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +4 -1
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +25 -4
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +8 -1
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +3 -1
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +3 -3
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +6 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +14 -9
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +32 -7
- package/templates/be/tool-config/jest.config.js +1 -1
- package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +4 -1
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +14 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +1 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +1 -0
- package/templates/app/module/jest.config.js.tpl +0 -8
- /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';
|
|
@@ -36,6 +36,7 @@ import { byCodeUnit } from '../report/order.mjs';
|
|
|
36
36
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
37
37
|
import { ciWorkflowCommands } from './ci-commands.mjs';
|
|
38
38
|
import { MODULE_PREFIX, moduleNames, registryFiles } from '../scaffold/module-registry.mjs';
|
|
39
|
+
import { canonImport, canonList } from './canon-layout.mjs';
|
|
39
40
|
import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
|
|
40
41
|
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
41
42
|
import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
|
|
@@ -249,16 +250,23 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
|
|
|
249
250
|
* lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
|
|
250
251
|
* their content. An app with no be folder has no source files.
|
|
251
252
|
*/
|
|
252
|
-
/**
|
|
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
|
+
*/
|
|
253
257
|
function otherPackages(root) {
|
|
254
258
|
try {
|
|
255
259
|
return fs.readdirSync(path.join(root, 'packages'), { withFileTypes: true })
|
|
256
|
-
.filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules'
|
|
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);
|
|
257
263
|
}
|
|
258
264
|
catch {
|
|
259
265
|
return [];
|
|
260
266
|
}
|
|
261
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}`)];
|
|
262
270
|
export function appSource(root, { parseYaml, notices } = {}) {
|
|
263
271
|
const files = [];
|
|
264
272
|
const walk = (dir) => {
|
|
@@ -283,7 +291,7 @@ export function appSource(root, { parseYaml, notices } = {}) {
|
|
|
283
291
|
walk(dir);
|
|
284
292
|
return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: (file) => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
|
|
285
293
|
modules: moduleNames(root), otherPackages: otherPackages(root),
|
|
286
|
-
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 }),
|
|
287
295
|
stack: readDeclaredStack(root, { parseYaml }), quality: readDeclaredQuality(root, { parseYaml }),
|
|
288
296
|
badges: readDeclaredBadges(root, { parseYaml, notices }),
|
|
289
297
|
qualityDeclared: hasDeclaredServices(root, { parseYaml }) };
|
|
@@ -337,7 +345,7 @@ export function appScripts(app, source) {
|
|
|
337
345
|
}),
|
|
338
346
|
...fe.map(entry => line(`start:${entry.name}`, `npm run start -w ${workspace(entry)}`)),
|
|
339
347
|
...(app.edition !== 'lite' && app.optionalSlots.includes('app.browser') ? [line('test:browser', 'playwright test -c browser/playwright.config.ts')] : []),
|
|
340
|
-
...(source
|
|
348
|
+
...(rootPackageFolders(source).length ? [line('build:modules', 'turbo run build --filter=./packages/*')] : []),
|
|
341
349
|
line('docker:build', images.map(image => `npm run docker:build:${image.entry.name}`).join(' && ')),
|
|
342
350
|
...images.map(image => line(`docker:build:${image.entry.name}`, imageCommand(image))),
|
|
343
351
|
].join('\n ');
|
|
@@ -377,11 +385,13 @@ const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/wo
|
|
|
377
385
|
/**
|
|
378
386
|
* The Dockerfile of every declared app, [{ path, content }] (app-relative): written once by starci app scaffold from templates/<side>/image/<kind>
|
|
379
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
|
|
380
|
-
* them and the be contracts only when the fe side reads them
|
|
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.
|
|
381
390
|
*/
|
|
382
391
|
export function imageFiles(app, source) {
|
|
383
392
|
const side = app.sides.fe;
|
|
384
|
-
const
|
|
393
|
+
const contracts = (side.reads ?? []).includes('be/contracts/') && source?.hasContracts === true;
|
|
394
|
+
const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...(contracts ? ['be/contracts'] : [])];
|
|
385
395
|
const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
386
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.
|
|
387
397
|
const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
|
|
@@ -394,32 +404,49 @@ export function imageFiles(app, source) {
|
|
|
394
404
|
});
|
|
395
405
|
}
|
|
396
406
|
/** Every value a template of `scope` can name, derived from hfs.json and the presets. */
|
|
397
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
*/
|
|
400
424
|
function registryTargets(app, source) {
|
|
425
|
+
if (app.modules === 'data')
|
|
426
|
+
return [];
|
|
401
427
|
return registryFiles(app.project, source?.modules ?? []).map((file) => ({ path: file.path, scope: APP_SCOPE, mode: 'file', content: file.content, hash: hashOf(file.content) }));
|
|
402
428
|
}
|
|
403
429
|
/**
|
|
404
|
-
* The root `eslint.config.mjs` of an app that has root workspace packages (packages
|
|
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
|
|
405
431
|
* reaches `packages/`, so this managed file lints their source by subpath. The back-end canon judges everything but the front-end subpaths
|
|
406
432
|
* (`src/workbench/` outside its contract, `src/public/`), the front-end canon and the selected Grammar family those, each over the package view of
|
|
407
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.
|
|
408
435
|
*/
|
|
409
436
|
function packagesLintTargets(app, source) {
|
|
410
437
|
if (!(source?.modules?.length || source?.otherPackages?.length))
|
|
411
438
|
return [];
|
|
412
439
|
const grammar = grammarLintFor(app.sides.fe.grammar);
|
|
413
|
-
const
|
|
440
|
+
const family = grammar.package ? { name: grammar.package, factory: grammar.factory.replace(/Config$/, 'PackagesConfig') } : null;
|
|
414
441
|
const content = [
|
|
415
442
|
'import { loadPackagesHfs as loadBePackages, starciBePackagesConfig } from "@starci/eslint-canon-be"',
|
|
416
443
|
'import { loadPackagesHfs as loadFePackages, starciFePackagesConfig } from "@starci/eslint-canon-fe"',
|
|
417
|
-
...(
|
|
444
|
+
...(family ? [canonImport({ names: [family.factory], from: family.name })] : []),
|
|
418
445
|
'',
|
|
419
446
|
'const be = loadBePackages(import.meta.url)',
|
|
420
447
|
'const fe = loadFePackages(import.meta.url)',
|
|
421
448
|
'',
|
|
422
|
-
|
|
449
|
+
canonList({ head: 'export default ', items: ['...(await starciBePackagesConfig({ hfs: be }))', '...starciFePackagesConfig({ hfs: fe })', ...(family ? [`...${family.factory}({ hfs: fe })`] : [])] }),
|
|
423
450
|
'',
|
|
424
451
|
].join(LINE);
|
|
425
452
|
return [{ path: PACKAGES_LINT_FILE, scope: APP_SCOPE, mode: 'file', content, hash: hashOf(content) }];
|
|
@@ -431,10 +458,11 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
431
458
|
const lite = app.edition === 'lite';
|
|
432
459
|
const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
|
|
433
460
|
const quality = source.quality ?? qualityServices({});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
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 && ' : '';
|
|
438
466
|
const grammarLint = scope === 'fe' ? grammarLintFor(fe.grammar) : null;
|
|
439
467
|
return {
|
|
440
468
|
header: HEADER(scope),
|
|
@@ -457,9 +485,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
457
485
|
typecheck: `${modulesFirst}npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck`,
|
|
458
486
|
// The typed lint reads the types the fe packages declare (their dist), so the packages are built first.
|
|
459
487
|
lintPrefix: `${modulesFirst}${packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '}`,
|
|
460
|
-
// The unit run, the back-end build and every CI job that imports a
|
|
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.
|
|
461
489
|
modulesFirst,
|
|
462
|
-
moduleTestStep: modules.length ? ' - name: module packages\n run: npm run test:modules\n' : '',
|
|
463
490
|
nodeMajor: String(NODE_MAJOR),
|
|
464
491
|
// The deploy server's age identity the restore workflow writes (stack/server-key.mjs owns the path).
|
|
465
492
|
serverAgeIdentity: SERVER_AGE_IDENTITY,
|
|
@@ -472,12 +499,13 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
472
499
|
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : []), ...moduleDirs.map(dir => `${dir}/src`)].join(','),
|
|
473
500
|
lcovReport: LCOV_REPORT,
|
|
474
501
|
cpdExclusions: SONAR_CPD_EXCLUSIONS.join(','),
|
|
475
|
-
...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 }),
|
|
476
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).
|
|
477
504
|
jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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') : '',
|
|
481
509
|
codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
|
|
482
510
|
codecovTarget: quality.codecov.target,
|
|
483
511
|
dependabotUpdates: dependabotUpdates(quality.dependabot),
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
run: npm run typecheck
|
|
93
93
|
env:
|
|
94
94
|
NODE_OPTIONS: {{nodeOptions}}
|
|
95
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
4
|
-
|
|
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 |
|
|
9
|
-
| doctor |
|
|
10
|
-
| up |
|
|
11
|
-
| status |
|
|
12
|
-
| logs |
|
|
13
|
-
| down |
|
|
14
|
-
| verification |
|
|
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
|
|
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`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
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,
|
|
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. |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
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
|
|
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
|
|
@@ -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(
|
|
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: [
|
|
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(
|
|
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. */
|
|
@@ -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"])
|
package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts
CHANGED
|
@@ -32,6 +32,21 @@ describe("IdentityAccountDirectoryService.find", () => {
|
|
|
32
32
|
expect(await addressless.directory.find("u-1")).toEqual({ id: "u-1", email: null, disabled: false })
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
it("answers the full name the profile keys hold, and none for an empty or a missing one", async () => {
|
|
36
|
+
const named = await build([accountByIdRow("named")])
|
|
37
|
+
const blank = await build([{ ...accountByIdRow(), meta: { full_name: "" } }])
|
|
38
|
+
const raw = await build([{ ...accountByIdRow(), meta: null }])
|
|
39
|
+
|
|
40
|
+
expect(await named.directory.find("u-1")).toEqual({
|
|
41
|
+
id: "u-1",
|
|
42
|
+
email: "a@example.test",
|
|
43
|
+
disabled: false,
|
|
44
|
+
name: "Ada Lovelace",
|
|
45
|
+
})
|
|
46
|
+
expect((await blank.directory.find("u-1"))?.name).toBeUndefined()
|
|
47
|
+
expect((await raw.directory.find("u-1"))?.name).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
35
50
|
it("answers undefined for an account that does not exist", async () => {
|
|
36
51
|
const { directory } = await build([])
|
|
37
52
|
|
|
@@ -3,10 +3,10 @@ import { Injectable } from "@nestjs/common"
|
|
|
3
3
|
import type { EntityManager } from "typeorm"
|
|
4
4
|
import type { AccountDirectory, AccountStanding } from "@modules/integrations/passport"
|
|
5
5
|
import { InjectPrimaryEntityManager } from "@modules/platform/database"
|
|
6
|
-
import { accountDoors } from "./persistence/account-doors.port"
|
|
6
|
+
import { accountDoors, fullNameOf } from "./persistence/account-doors.port"
|
|
7
7
|
|
|
8
8
|
@Injectable()
|
|
9
|
-
/** The package's account directory over the account doors: whether an account still stands, for a refresh to go on. */
|
|
9
|
+
/** The package's account directory over the account doors: whether an account still stands, for a refresh to go on, and the person's full name. */
|
|
10
10
|
export class IdentityAccountDirectoryService implements AccountDirectory {
|
|
11
11
|
constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
|
|
12
12
|
|
|
@@ -14,6 +14,12 @@ export class IdentityAccountDirectoryService implements AccountDirectory {
|
|
|
14
14
|
async find(userId: string): Promise<AccountStanding | undefined> {
|
|
15
15
|
const account = await accountDoors.userById(this.entityManager, userId)
|
|
16
16
|
if (account === undefined) return undefined
|
|
17
|
-
|
|
17
|
+
const name = fullNameOf(account.meta)
|
|
18
|
+
return {
|
|
19
|
+
id: account.id,
|
|
20
|
+
email: account.email,
|
|
21
|
+
disabled: account.disabled,
|
|
22
|
+
...(name === undefined ? {} : { name }),
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
}
|