@starci/hfs 5.2.0 → 5.3.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 +34 -0
- package/README.md +1 -1
- package/cache/hfs-family.d.mts +2 -3
- package/cache/hfs-family.mjs +4 -9
- package/cache/secret-records.d.mts +7 -0
- package/cache/secret-records.mjs +12 -0
- package/cache/session.mjs +4 -2
- package/codemod/account-doors.d.mts +36 -0
- package/codemod/account-doors.mjs +128 -0
- package/codemod/main.mjs +31 -1
- package/codemod/module-definition.d.mts +52 -0
- package/codemod/module-definition.mjs +242 -0
- package/codemod/quality-binding.d.mts +33 -0
- package/codemod/quality-binding.mjs +38 -0
- package/codemod/secrets-cache.d.mts +35 -0
- package/codemod/secrets-cache.mjs +70 -0
- package/codemod/sops-rules.d.mts +34 -0
- package/codemod/sops-rules.mjs +69 -0
- package/codemod/theme-tokens.mjs +48 -2
- package/lint/run.d.mts +6 -0
- package/lint/run.mjs +63 -10
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +43 -10
- package/runtime/knowledge/hfs/fe-stack.yaml +3 -3
- package/runtime/knowledge/hfs/rules.yaml +13 -9
- package/runtime/knowledge/hfs/slots.yaml +105 -12
- package/runtime/modules/kernel/failure-codes.yaml +5 -4
- package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
- package/runtime/scripts/hfs/architecture/config.mjs +25 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
- package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
- package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
- package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
- package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
- package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
- package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
- package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
- package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
- package/runtime/scripts/hfs/secret-errors.mjs +8 -0
- package/runtime/scripts/hfs/secret-init.d.mts +6 -0
- package/runtime/scripts/hfs/secret-init.mjs +23 -0
- package/runtime/scripts/hfs/secret-sops.mjs +9 -1
- package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
- package/runtime/scripts/hfs/secret-standard.mjs +106 -0
- package/runtime/scripts/hfs/secret.mjs +5 -1
- package/runtime/scripts/hfs/view.d.mts +23 -0
- package/runtime/scripts/hfs/view.mjs +68 -0
- package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
- package/runtime/scripts/lib/declaration-surface.mjs +55 -1
- package/runtime/scripts/lib/secret-records.d.mts +13 -0
- package/runtime/scripts/lib/secret-records.mjs +83 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-queue-lite.mjs +1 -3
- package/scaffold/app.d.mts +2 -2
- package/scaffold/app.mjs +10 -12
- package/scaffold/lite-exports.mjs +1 -15
- package/scripts/sync-bundles.d.mts +8 -0
- package/stack/images/openclaw-runtime/Dockerfile +18 -0
- package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
- package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
- package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
- package/stack/local-up.mjs +3 -0
- package/stack/runtime-layers.d.mts +21 -0
- package/stack/runtime-layers.mjs +67 -0
- package/sync/hygiene.mjs +3 -1
- package/sync/index.d.mts +2 -0
- package/sync/index.mjs +33 -2
- package/templates/app/auth-doors.sql.partial +115 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
- package/templates/app/package-scripts/core.json +1 -1
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +9 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +3 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +19 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +4 -0
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +24 -4
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +33 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +3 -10
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +6 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +3 -10
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +56 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +30 -30
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +7 -16
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +113 -49
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +42 -53
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +3 -10
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +17 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +6 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +29 -33
- package/templates/be/inbox/module-definition.ts.tpl +2 -9
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
- package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +2 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/table/module-definition.ts.tpl +2 -9
- package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
- package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +0 -38
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +0 -54
package/sync/index.mjs
CHANGED
|
@@ -237,6 +237,9 @@ export function sonarScope(presets) {
|
|
|
237
237
|
const tests = preset.filter(glob => !glob.endsWith('/**'));
|
|
238
238
|
return { tests, exclusions: [...new Set([...preset, '**/.next/**', '**/node_modules/**'])] };
|
|
239
239
|
}
|
|
240
|
+
/** The managed ESLint config at the app root that lints the root workspace packages. */
|
|
241
|
+
export const PACKAGES_LINT_FILE = 'eslint.config.mjs';
|
|
242
|
+
const LINE = '\n';
|
|
240
243
|
const SOURCE_FILE = /\.[cm]?ts$/;
|
|
241
244
|
const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
|
|
242
245
|
/**
|
|
@@ -396,6 +399,30 @@ const moduleExclusions = (dirs) => dirs.flatMap(dir => [`${dir}/src/workbench/**
|
|
|
396
399
|
function registryTargets(app, source) {
|
|
397
400
|
return registryFiles(app.project, source?.modules ?? []).map((file) => ({ path: file.path, scope: APP_SCOPE, mode: 'file', content: file.content, hash: hashOf(file.content) }));
|
|
398
401
|
}
|
|
402
|
+
/**
|
|
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
|
|
404
|
+
* reaches `packages/`, so this managed file lints their source by subpath. The back-end canon judges everything but the front-end subpaths
|
|
405
|
+
* (`src/workbench/` outside its contract, `src/public/`), the front-end canon and the selected Grammar family those, each over the package view of
|
|
406
|
+
* the app root. An app without a root package has no such file; the family factory for packages is the family's `<name>GrammarPackagesConfig`.
|
|
407
|
+
*/
|
|
408
|
+
function packagesLintTargets(app, source) {
|
|
409
|
+
if (!(source?.modules?.length || source?.otherPackages?.length))
|
|
410
|
+
return [];
|
|
411
|
+
const grammar = grammarLintFor(app.sides.fe.grammar);
|
|
412
|
+
const factory = grammar.package ? grammar.factory.replace(/Config$/, 'PackagesConfig') : null;
|
|
413
|
+
const content = [
|
|
414
|
+
'import { loadPackagesHfs as loadBePackages, starciBePackagesConfig } from "@starci/eslint-canon-be"',
|
|
415
|
+
'import { loadPackagesHfs as loadFePackages, starciFePackagesConfig } from "@starci/eslint-canon-fe"',
|
|
416
|
+
...(grammar.package ? [`import { ${factory} } from "${grammar.package}"`] : []),
|
|
417
|
+
'',
|
|
418
|
+
'const be = loadBePackages(import.meta.url)',
|
|
419
|
+
'const fe = loadFePackages(import.meta.url)',
|
|
420
|
+
'',
|
|
421
|
+
`export default [...(await starciBePackagesConfig({ hfs: be })), ...starciFePackagesConfig({ hfs: fe })${grammar.package ? `, ...${factory}({ hfs: fe })` : ''}]`,
|
|
422
|
+
'',
|
|
423
|
+
].join(LINE);
|
|
424
|
+
return [{ path: PACKAGES_LINT_FILE, scope: APP_SCOPE, mode: 'file', content, hash: hashOf(content) }];
|
|
425
|
+
}
|
|
399
426
|
export function variables(app, scope, presets, sonarKey, manifest = loadSlotManifest(), source = { files: [], read: () => '' }) {
|
|
400
427
|
const stack = source.stack ?? { images: [], passEnv: [] };
|
|
401
428
|
const fe = app.sides.fe;
|
|
@@ -429,7 +456,9 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
429
456
|
typecheck: `${modulesFirst}npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck`,
|
|
430
457
|
// The typed lint reads the types the fe packages declare (their dist), so the packages are built first.
|
|
431
458
|
lintPrefix: `${modulesFirst}${packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '}`,
|
|
432
|
-
|
|
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.
|
|
460
|
+
modulesFirst,
|
|
461
|
+
moduleTestStep: modules.length ? ' - name: module packages\n run: npm run test:modules\n' : '',
|
|
433
462
|
nodeMajor: String(NODE_MAJOR),
|
|
434
463
|
// The deploy server's age identity the restore workflow writes (stack/server-key.mjs owns the path).
|
|
435
464
|
serverAgeIdentity: SERVER_AGE_IDENTITY,
|
|
@@ -503,7 +532,7 @@ function renderOnce(hfs, presets, { sonarKey, readTemplate = readBundled, manife
|
|
|
503
532
|
declaredTargets.push({ path: 'README.md', mode: 'block', markdown: true, template: 'app/readme-badges/README.md' });
|
|
504
533
|
const targets = declaredTargets
|
|
505
534
|
.filter(target => !(app.edition === 'lite' && source?.workPresent === false && target.path === '.starciwork/.gitignore'));
|
|
506
|
-
return [...(scope === APP_SCOPE ? registryTargets(app, source) : []), ...targets.map((target) => {
|
|
535
|
+
return [...(scope === APP_SCOPE ? [...registryTargets(app, source), ...packagesLintTargets(app, source)] : []), ...targets.map((target) => {
|
|
507
536
|
const template = target.path === 'codecov.yml' && source?.quality?.codecov.enabled === false
|
|
508
537
|
? '{{header}}\n# Codecov is disabled in the manifest.\ncodecov:\n require_ci_to_pass: false\n'
|
|
509
538
|
: readTemplate(target.template);
|
|
@@ -657,6 +686,8 @@ const LITE_RETIRED_DIRS = Object.freeze(['supabase/types']);
|
|
|
657
686
|
/** The files a lite app's sync retires, each with the reason the dry run prints. */
|
|
658
687
|
const LITE_RETIRED_FILES = Object.freeze({
|
|
659
688
|
[RETIRED_DB_DOCKERFILE]: `the db image moved to ${DB_DOCKERFILE} (ruling lite-db-image-in-stacks)`,
|
|
689
|
+
'be/src/modules/domain/identity/persistence/entities/auth-user.entity.ts': 'the sign-in reaches accounts through the account doors (be/src/modules/domain/identity/persistence/account-doors.port.ts); the application role holds no grant on schema auth',
|
|
690
|
+
'be/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts': 'the sign-in reaches accounts through the account doors (be/src/modules/domain/identity/persistence/account-doors.port.ts); the application role holds no grant on schema auth',
|
|
660
691
|
'.github/workflows/db-deploy.yml': 'the stack deploy migrates the database (a verified backup, the expand migrations before the services, the contract migrations after the accept; ruling app-cicd-auto-deploy)',
|
|
661
692
|
});
|
|
662
693
|
/** Why a retired file goes: a retired file's own reason, else the folder's. */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
-- The account doors of a lite app: the only way app_be reaches auth.users and auth.identities (it holds no grant on schema auth). Eight
|
|
2
|
+
-- SECURITY DEFINER functions with a pinned search_path, EXECUTE revoked from public and the browser roles and granted to app_be alone: an
|
|
3
|
+
-- account comes back only for an exact address or id, a hash only for an exact address, and no door writes role or app metadata; the hash
|
|
4
|
+
-- is written only by a new account and by be_auth_set_password.
|
|
5
|
+
-- The text is the canon owner of the doors (templates/app/auth-doors.sql.partial); the back end calls them through persistence/account-doors.port.ts.
|
|
6
|
+
|
|
7
|
+
-- Looks up an address in lower case; the bootstrap index serves email as written, not lower(email).
|
|
8
|
+
create index if not exists users_email_lower_idx on auth.users (lower(email)) where is_anonymous = false;
|
|
9
|
+
|
|
10
|
+
-- A person by sign-in address (lower-cased), with the stored bcrypt hash so the back end can verify it.
|
|
11
|
+
CREATE FUNCTION public.be_auth_user_by_email(p_email text)
|
|
12
|
+
RETURNS TABLE(id uuid, email text, encrypted_password text, email_confirmed_at timestamp with time zone, disabled boolean)
|
|
13
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
14
|
+
SET search_path TO ''
|
|
15
|
+
AS $$
|
|
16
|
+
select u.id, u.email::text, u.encrypted_password::text, u.email_confirmed_at,
|
|
17
|
+
(u.deleted_at is not null or (u.banned_until is not null and u.banned_until > now()))
|
|
18
|
+
from auth.users u
|
|
19
|
+
where lower(u.email) = p_email and u.is_anonymous = false
|
|
20
|
+
order by u.created_at asc nulls last
|
|
21
|
+
limit 1;
|
|
22
|
+
$$;
|
|
23
|
+
|
|
24
|
+
-- One person by id.
|
|
25
|
+
CREATE FUNCTION public.be_auth_user_by_id(p_user uuid)
|
|
26
|
+
RETURNS TABLE(id uuid, email text, last_sign_in_at timestamp with time zone, meta jsonb, disabled boolean)
|
|
27
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
28
|
+
SET search_path TO ''
|
|
29
|
+
AS $$
|
|
30
|
+
select u.id, u.email::text, u.last_sign_in_at, coalesce(u.raw_user_meta_data, '{}'::jsonb),
|
|
31
|
+
(u.deleted_at is not null or (u.banned_until is not null and u.banned_until > now()))
|
|
32
|
+
from auth.users u where u.id = p_user;
|
|
33
|
+
$$;
|
|
34
|
+
|
|
35
|
+
-- The person a Google account is linked to, if any.
|
|
36
|
+
CREATE FUNCTION public.be_auth_user_by_google(p_provider_id text) RETURNS uuid
|
|
37
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
38
|
+
SET search_path TO ''
|
|
39
|
+
AS $$
|
|
40
|
+
select i.user_id from auth.identities i where i.provider = 'google' and i.provider_id = p_provider_id limit 1;
|
|
41
|
+
$$;
|
|
42
|
+
|
|
43
|
+
-- Creates a person; a null hash is an invited or Google-only account that has no password yet.
|
|
44
|
+
CREATE FUNCTION public.be_auth_create_user(p_id uuid, p_email text, p_hash text, p_confirmed boolean, p_meta jsonb, p_now timestamp with time zone)
|
|
45
|
+
RETURNS uuid
|
|
46
|
+
LANGUAGE sql VOLATILE SECURITY DEFINER
|
|
47
|
+
SET search_path TO ''
|
|
48
|
+
AS $$
|
|
49
|
+
insert into auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at,
|
|
50
|
+
raw_app_meta_data, raw_user_meta_data, created_at, updated_at)
|
|
51
|
+
values ('00000000-0000-0000-0000-000000000000', p_id, 'authenticated', 'authenticated', p_email, p_hash,
|
|
52
|
+
case when p_confirmed then p_now end, case when p_hash is null and not p_confirmed then p_now end,
|
|
53
|
+
'{"provider":"email","providers":["email"]}'::jsonb, p_meta, p_now, p_now)
|
|
54
|
+
returning id;
|
|
55
|
+
$$;
|
|
56
|
+
|
|
57
|
+
-- Links a Google account to a person.
|
|
58
|
+
CREATE FUNCTION public.be_auth_link_google(p_user uuid, p_provider_id text, p_data jsonb, p_now timestamp with time zone) RETURNS void
|
|
59
|
+
LANGUAGE sql VOLATILE SECURITY DEFINER
|
|
60
|
+
SET search_path TO ''
|
|
61
|
+
AS $$
|
|
62
|
+
insert into auth.identities (provider_id, user_id, identity_data, provider, last_sign_in_at, created_at, updated_at)
|
|
63
|
+
values (p_provider_id, p_user, p_data, 'google', p_now, p_now, p_now)
|
|
64
|
+
on conflict (provider_id, provider) do update set last_sign_in_at = p_now, updated_at = p_now;
|
|
65
|
+
$$;
|
|
66
|
+
|
|
67
|
+
-- Records a sign-in; a verified Google address also confirms the mailbox; profile keys only fill what the person has not set.
|
|
68
|
+
CREATE FUNCTION public.be_auth_touch_user(p_user uuid, p_now timestamp with time zone, p_confirm boolean, p_meta jsonb) RETURNS void
|
|
69
|
+
LANGUAGE sql VOLATILE SECURITY DEFINER
|
|
70
|
+
SET search_path TO ''
|
|
71
|
+
AS $$
|
|
72
|
+
update auth.users
|
|
73
|
+
set last_sign_in_at = p_now,
|
|
74
|
+
updated_at = p_now,
|
|
75
|
+
email_confirmed_at = case when p_confirm then coalesce(email_confirmed_at, p_now) else email_confirmed_at end,
|
|
76
|
+
raw_user_meta_data = p_meta || coalesce(raw_user_meta_data, '{}'::jsonb)
|
|
77
|
+
where id = p_user;
|
|
78
|
+
$$;
|
|
79
|
+
|
|
80
|
+
-- Stamps the confirmation of a mailbox (a spent confirmation link); a mailbox that is already confirmed keeps its date and the sign-in record is untouched.
|
|
81
|
+
CREATE FUNCTION public.be_auth_confirm_user(p_user uuid, p_now timestamp with time zone) RETURNS void
|
|
82
|
+
LANGUAGE sql VOLATILE SECURITY DEFINER
|
|
83
|
+
SET search_path TO ''
|
|
84
|
+
AS $$
|
|
85
|
+
update auth.users set email_confirmed_at = coalesce(email_confirmed_at, p_now), updated_at = p_now where id = p_user;
|
|
86
|
+
$$;
|
|
87
|
+
|
|
88
|
+
-- Sets a new password hash; when p_confirm is true (a spent reset link proved the mailbox) the address is confirmed too.
|
|
89
|
+
CREATE FUNCTION public.be_auth_set_password(p_user uuid, p_hash text, p_confirm boolean, p_now timestamp with time zone) RETURNS void
|
|
90
|
+
LANGUAGE sql VOLATILE SECURITY DEFINER
|
|
91
|
+
SET search_path TO ''
|
|
92
|
+
AS $$
|
|
93
|
+
update auth.users
|
|
94
|
+
set encrypted_password = p_hash,
|
|
95
|
+
email_confirmed_at = case when p_confirm then coalesce(email_confirmed_at, p_now) else email_confirmed_at end,
|
|
96
|
+
updated_at = p_now
|
|
97
|
+
where id = p_user;
|
|
98
|
+
$$;
|
|
99
|
+
|
|
100
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_user_by_email(text) FROM PUBLIC, anon, authenticated;
|
|
101
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_user_by_email(text) TO app_be;
|
|
102
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_user_by_id(uuid) FROM PUBLIC, anon, authenticated;
|
|
103
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_user_by_id(uuid) TO app_be;
|
|
104
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_user_by_google(text) FROM PUBLIC, anon, authenticated;
|
|
105
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_user_by_google(text) TO app_be;
|
|
106
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_create_user(uuid, text, text, boolean, jsonb, timestamp with time zone) FROM PUBLIC, anon, authenticated;
|
|
107
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_create_user(uuid, text, text, boolean, jsonb, timestamp with time zone) TO app_be;
|
|
108
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_link_google(uuid, text, jsonb, timestamp with time zone) FROM PUBLIC, anon, authenticated;
|
|
109
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_link_google(uuid, text, jsonb, timestamp with time zone) TO app_be;
|
|
110
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_touch_user(uuid, timestamp with time zone, boolean, jsonb) FROM PUBLIC, anon, authenticated;
|
|
111
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_touch_user(uuid, timestamp with time zone, boolean, jsonb) TO app_be;
|
|
112
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_confirm_user(uuid, timestamp with time zone) FROM PUBLIC, anon, authenticated;
|
|
113
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_confirm_user(uuid, timestamp with time zone) TO app_be;
|
|
114
|
+
REVOKE EXECUTE ON FUNCTION public.be_auth_set_password(uuid, text, boolean, timestamp with time zone) FROM PUBLIC, anon, authenticated;
|
|
115
|
+
GRANT EXECUTE ON FUNCTION public.be_auth_set_password(uuid, text, boolean, timestamp with time zone) TO app_be;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"build:be": "cd be && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
1
|
+
"build:be": "{{modulesFirst}}cd be && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
2
2
|
"build:fe": "{{buildFe}}",
|
|
3
3
|
"codegen": "node scripts/codegen.mjs",
|
|
4
4
|
"codegen:graphql": "node scripts/codegen.mjs --graphql",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{appScripts}}
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
|
-
"test": "cd be && jest --selectProjects unit --coverage
|
|
7
|
+
"test": "{{modulesFirst}}cd be && jest --selectProjects unit --coverage",
|
|
8
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
9
9
|
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
10
10
|
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{appScripts}}
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
|
-
"test": "cd be && jest --selectProjects unit --coverage
|
|
7
|
+
"test": "{{modulesFirst}}cd be && jest --selectProjects unit --coverage",
|
|
8
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
9
9
|
{{> app/package-scripts/db.json}}
|
|
10
10
|
{{backingScripts}}
|
|
@@ -58,8 +58,11 @@ environments:
|
|
|
58
58
|
db: 54322
|
|
59
59
|
api: 3001
|
|
60
60
|
web: 3000
|
|
61
|
+
mail: 1025
|
|
62
|
+
mailui: 8025
|
|
61
63
|
services:
|
|
62
64
|
- {name: db, wait: healthy}
|
|
65
|
+
- {name: mail, wait: healthy}
|
|
63
66
|
database:
|
|
64
67
|
service: db
|
|
65
68
|
port: db
|
|
@@ -97,6 +100,11 @@ environments:
|
|
|
97
100
|
AUTH_WEB_ORIGIN: "http://localhost:${port.web}"
|
|
98
101
|
AUTH_JWT_AUDIENCE: {{project}}
|
|
99
102
|
AUTH_JWT_KEY_ID: dev-1
|
|
103
|
+
MAIL_HOST: 127.0.0.1
|
|
104
|
+
MAIL_PORT: "${port.mail}"
|
|
105
|
+
MAIL_SECURITY: none
|
|
106
|
+
MAIL_FROM: no-reply@{{project}}.example.invalid
|
|
107
|
+
MAIL_FROM_NAME: {{project}}
|
|
100
108
|
STORAGE_ENDPOINT: "http://127.0.0.1:9000"
|
|
101
109
|
STORAGE_REGION: us-east-1
|
|
102
110
|
STORAGE_BUCKET: {{project}}-dev
|
|
@@ -120,6 +128,7 @@ environments:
|
|
|
120
128
|
origins:
|
|
121
129
|
- {name: web, url: "http://localhost:${port.web}"}
|
|
122
130
|
- {name: api, url: "http://localhost:${port.api}"}
|
|
131
|
+
- {name: mail, url: "http://localhost:${port.mailui}"}
|
|
123
132
|
production:
|
|
124
133
|
status: supported
|
|
125
134
|
runtime: docker-swarm
|
|
@@ -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`), 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, runs the seed, 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
|
|
|
@@ -26,5 +26,7 @@ another environment.
|
|
|
26
26
|
| down | `starci app stack down --env dev --execute`: ends the processes and stops the compose project; volumes and sealed values stay. |
|
|
27
27
|
| 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. |
|
|
28
28
|
|
|
29
|
+
**Mail.** The confirmation and reset mails of the sign-in go to the `mail` service on its SMTP port (`MAIL_HOST`, `MAIL_PORT`, `MAIL_SECURITY: none`: a plain channel that only the loopback sink accepts) and nothing leaves the machine. Open the web UI at the `mail` origin `up` prints (`http://localhost:8025` by default), sign up in the web app and follow the link from the message there. A deployed environment names a real SMTP server and `MAIL_SECURITY: starttls` or `tls`.
|
|
30
|
+
|
|
29
31
|
The database volume `db-data` is rebuildable development data: `starci app db migrate` recreates its schema from `db/bootstrap.sql` and
|
|
30
32
|
`db/migrations`. Key names of the environment are in `.starcistacks/dev/runtime/env/KEYS.md`.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# The dev environment's dependencies, run by `starci app stack up --env dev --execute` (PROJECT-STACKS-3, PROJECT-STACKS-6).
|
|
2
2
|
# Host ports come from environments.dev.local.ports through STACK_PORT_<NAME>; the superuser password is read from a file through the least
|
|
3
|
-
# grant STACK_GRANT_DB (a directory holding only POSTGRES_PASSWORD), never from the environment. Every port publishes on loopback only.
|
|
3
|
+
# grant STACK_GRANT_DB (a directory holding only POSTGRES_PASSWORD), never from the environment. Every port publishes on loopback only.
|
|
4
|
+
# `mail` is the dev mail sink (Mailpit, an unmodified upstream image pinned by digest in contexts/stack/pins.yaml): the api sends the
|
|
5
|
+
# confirmation and reset mails to its SMTP port, over a plain channel that exists on loopback only, and a person reads them in its web UI. The api and web run natively against these containers.
|
|
4
6
|
name: {{project}}-dev
|
|
5
7
|
services:
|
|
6
8
|
db:
|
|
@@ -21,5 +23,21 @@ services:
|
|
|
21
23
|
retries: 30
|
|
22
24
|
start_period: 10s
|
|
23
25
|
restart: unless-stopped
|
|
26
|
+
mail:
|
|
27
|
+
image: axllent/mailpit:v1.31.6@sha256:23fe39e81bde596a918000b3135c0f85dc8bb4c9ea6be274b051143f3e300387
|
|
28
|
+
environment:
|
|
29
|
+
MP_SMTP_BIND_ADDR: 0.0.0.0:1025
|
|
30
|
+
MP_UI_BIND_ADDR: 0.0.0.0:8025
|
|
31
|
+
MP_MAX_MESSAGES: "500"
|
|
32
|
+
ports:
|
|
33
|
+
- "127.0.0.1:${STACK_PORT_MAIL}:1025"
|
|
34
|
+
- "127.0.0.1:${STACK_PORT_MAILUI}:8025"
|
|
35
|
+
healthcheck:
|
|
36
|
+
test: ["CMD", "/mailpit", "readyz"]
|
|
37
|
+
interval: 5s
|
|
38
|
+
timeout: 5s
|
|
39
|
+
retries: 20
|
|
40
|
+
start_period: 5s
|
|
41
|
+
restart: unless-stopped
|
|
24
42
|
volumes:
|
|
25
43
|
db-data: {}
|
|
@@ -18,5 +18,8 @@ Names only. Dev values are generated once by the owner-armed `starci app secret
|
|
|
18
18
|
| `AUTH_JWT_PRIVATE_KEY` | API Ed25519 (EdDSA) signing key (member `auth`) |
|
|
19
19
|
| `STORAGE_ENDPOINT`, `STORAGE_REGION`, `STORAGE_BUCKET` | API storage address |
|
|
20
20
|
| `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_ACCESS_KEY` | API storage credentials (member `storage`) |
|
|
21
|
+
| `MAIL_HOST`, `MAIL_PORT`, `MAIL_SECURITY`, `MAIL_FROM`, `MAIL_FROM_NAME` | API outgoing mail server (the dev `mail` service, Mailpit, on its SMTP port: a plain channel on loopback only) and the sender of the confirmation and reset mails |
|
|
22
|
+
| `MAIL_DEFAULT_LANGUAGE`, `MAIL_CONFIRM_TTL_SECONDS`, `MAIL_RESET_TTL_SECONDS` | API mail language (`vi` or `en`) and link lifetimes; optional |
|
|
23
|
+
| `MAIL_USER`, `MAIL_PASSWORD` | API mail server login; optional, the password reads `MAIL_PASSWORD_FILE` |
|
|
21
24
|
| `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_GRAPHQL_URL` | Front-end site, auth doors and GraphQL transport |
|
|
22
25
|
| `DATABASE_URL` | `starci app db` verbs; the stack verb composes it in memory for the migration |
|
|
@@ -17,6 +17,11 @@ services:
|
|
|
17
17
|
AUTH_JWT_KEY_ID: production-1
|
|
18
18
|
AUTH_COOKIE_DOMAIN: {{project}}.example.invalid
|
|
19
19
|
AUTH_JWT_PRIVATE_KEY_FILE: /run/secrets/auth-jwt-private-key
|
|
20
|
+
MAIL_HOST: smtp.{{project}}.example.invalid
|
|
21
|
+
MAIL_PORT: "587"
|
|
22
|
+
MAIL_SECURITY: starttls
|
|
23
|
+
MAIL_FROM: no-reply@{{project}}.example.invalid
|
|
24
|
+
MAIL_FROM_NAME: {{project}}
|
|
20
25
|
STORAGE_ENDPOINT: https://storage.{{project}}.example.invalid
|
|
21
26
|
STORAGE_REGION: us-east-1
|
|
22
27
|
STORAGE_BUCKET: {{project}}-production
|
|
@@ -23,6 +23,10 @@ This inventory declares names, consumers and mounted file paths. Scaffolding gen
|
|
|
23
23
|
| `STORAGE_BUCKET` | API storage bucket |
|
|
24
24
|
| `STORAGE_ACCESS_KEY_ID` | API storage credential; reads `STORAGE_ACCESS_KEY_ID_FILE` at `/run/secrets/storage-access-key-id` |
|
|
25
25
|
| `STORAGE_SECRET_ACCESS_KEY` | API storage credential; reads `STORAGE_SECRET_ACCESS_KEY_FILE` at `/run/secrets/storage-secret-access-key` |
|
|
26
|
+
| `MAIL_HOST`, `MAIL_PORT`, `MAIL_SECURITY` | API outgoing mail server and how its channel is secured (`starttls` is required to succeed, `tls` is implicit TLS) |
|
|
27
|
+
| `MAIL_FROM`, `MAIL_FROM_NAME` | API sender address and name of the confirmation and reset mails |
|
|
28
|
+
| `MAIL_DEFAULT_LANGUAGE`, `MAIL_CONFIRM_TTL_SECONDS`, `MAIL_RESET_TTL_SECONDS` | API mail language (`vi` or `en`) and link lifetimes; optional |
|
|
29
|
+
| `MAIL_USER`, `MAIL_PASSWORD` | API mail server login; optional, the password reads `MAIL_PASSWORD_FILE` at `/run/secrets/mail-password` |
|
|
26
30
|
| `DATABASE_URL` | `starci app db` verbs (migrate, dump) and `contract:emit`, from the operator's environment |
|
|
27
31
|
| `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
|
|
28
32
|
| `NEXT_PUBLIC_API_URL` | Front-end auth doors of the API |
|
|
@@ -24,10 +24,6 @@ begin
|
|
|
24
24
|
end;
|
|
25
25
|
$$;
|
|
26
26
|
|
|
27
|
-
grant usage on schema auth to app_be;
|
|
28
|
-
grant select, insert, update on table auth.users to app_be;
|
|
29
|
-
grant select, insert on table auth.identities to app_be;
|
|
30
|
-
|
|
31
27
|
-- The device sessions and rotating refresh tokens of @starci/auth. A token is stored by its sha256 only; a rotated token stays,
|
|
32
28
|
-- marked used, so a replay is detected (a replay inside the grace window is a racing tab, an older one ends the session).
|
|
33
29
|
-- identity_attempts counts the sign-in attempts of one address in a window every replica shares (the budget of failures).
|
|
@@ -87,6 +83,30 @@ create policy identity_refresh_tokens_app_be_all on public.identity_refresh_toke
|
|
|
87
83
|
create policy identity_attempts_app_be_all on public.identity_attempts
|
|
88
84
|
for all to app_be using (current_user = 'app_be') with check (current_user = 'app_be');
|
|
89
85
|
|
|
86
|
+
{{> app/auth-doors.sql.partial}}
|
|
87
|
+
|
|
88
|
+
-- The single-use links that confirm a mailbox or reset a password (the email doors of sign-in): only the sha256 of the token is
|
|
89
|
+
-- stored, a spent link stays marked used, and a new link of the same purpose ends the older ones.
|
|
90
|
+
create table public.identity_email_tokens (
|
|
91
|
+
id uuid primary key,
|
|
92
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
93
|
+
purpose varchar(16) not null check (purpose in ('confirm', 'reset')),
|
|
94
|
+
token_hash varchar(64) not null,
|
|
95
|
+
expires_at timestamptz not null,
|
|
96
|
+
used_at timestamptz,
|
|
97
|
+
created_at timestamptz not null,
|
|
98
|
+
constraint identity_email_tokens_hash_key unique (token_hash)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
create index identity_email_tokens_user_idx on public.identity_email_tokens (user_id, purpose);
|
|
102
|
+
|
|
103
|
+
alter table public.identity_email_tokens enable row level security;
|
|
104
|
+
alter table public.identity_email_tokens force row level security;
|
|
105
|
+
revoke all on table public.identity_email_tokens from public;
|
|
106
|
+
grant select, insert, update, delete on table public.identity_email_tokens to app_be;
|
|
107
|
+
create policy identity_email_tokens_app_be_all on public.identity_email_tokens
|
|
108
|
+
for all to app_be using (current_user = 'app_be') with check (current_user = 'app_be');
|
|
109
|
+
|
|
90
110
|
create table public.profiles (
|
|
91
111
|
id uuid primary key references auth.users (id) on delete cascade,
|
|
92
112
|
display_name text not null check (char_length(display_name) between 1 and 120),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import type { MailLanguage } from "@modules/integrations/mail"
|
|
2
3
|
import type { HttpExchange } from "@modules/integrations/passport"
|
|
3
4
|
|
|
4
5
|
/** One cookie door of sign-in: the request and response the strategy or the refresh cookie work on. */
|
|
@@ -9,3 +10,35 @@ export interface ExchangeRequest {
|
|
|
9
10
|
|
|
10
11
|
/** A request with nothing in it. */
|
|
11
12
|
export type EmptyRequest = Record<string, never>
|
|
13
|
+
|
|
14
|
+
/** The posted data of the signup door. */
|
|
15
|
+
export interface SignupData {
|
|
16
|
+
/** The sign-in address. */
|
|
17
|
+
readonly email: string
|
|
18
|
+
/** The chosen password. */
|
|
19
|
+
readonly password: string
|
|
20
|
+
/** The language of the confirmation mail, when the request names one. */
|
|
21
|
+
readonly language: MailLanguage | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The posted data of the resend and forgot doors. */
|
|
25
|
+
export interface AddressData {
|
|
26
|
+
/** The address the mail goes to. */
|
|
27
|
+
readonly email: string
|
|
28
|
+
/** The language of the mail, when the request names one. */
|
|
29
|
+
readonly language: MailLanguage | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The link token of the confirm door. */
|
|
33
|
+
export interface TokenData {
|
|
34
|
+
/** The token the link carries. */
|
|
35
|
+
readonly token: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The posted data of the reset door. */
|
|
39
|
+
export interface ResetData {
|
|
40
|
+
/** The token the link carries. */
|
|
41
|
+
readonly token: string
|
|
42
|
+
/** The new password. */
|
|
43
|
+
readonly password: string
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { Command } from "@nestjs/cqrs"
|
|
3
|
+
import type { PublicExecuteParams } from "@modules/platform/cqrs"
|
|
4
|
+
import type { TokenData } from "./auth.contracts"
|
|
5
|
+
|
|
6
|
+
/** Spends a confirmation link and confirms the mailbox. */
|
|
7
|
+
export class ConfirmCommand extends Command<void> {
|
|
8
|
+
constructor(readonly params: PublicExecuteParams<TokenData>) {
|
|
9
|
+
super()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { CommandHandler } from "@nestjs/cqrs"
|
|
3
|
+
import { IdentityEmailService } from "@modules/domain/identity"
|
|
4
|
+
import { ICQRSHandler } from "@modules/platform/cqrs"
|
|
5
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
6
|
+
import type { Logger } from "@modules/platform/logging"
|
|
7
|
+
import { ConfirmCommand } from "./confirm.command"
|
|
8
|
+
|
|
9
|
+
@CommandHandler(ConfirmCommand)
|
|
10
|
+
/** Spends a confirmation link and confirms the mailbox. */
|
|
11
|
+
export class ConfirmHandler extends ICQRSHandler<ConfirmCommand, void> {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectLogger() logger: Logger,
|
|
14
|
+
private readonly email: IdentityEmailService,
|
|
15
|
+
) {
|
|
16
|
+
super(logger)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected override process(command: ConfirmCommand): Promise<void> {
|
|
20
|
+
return this.email.confirm(command.params.request.token)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { Command } from "@nestjs/cqrs"
|
|
3
|
+
import type { PublicExecuteParams } from "@modules/platform/cqrs"
|
|
4
|
+
import type { AddressData } from "./auth.contracts"
|
|
5
|
+
|
|
6
|
+
/** Mails a password reset link when the address has an account; the answer never says whether it does. */
|
|
7
|
+
export class ForgotCommand extends Command<void> {
|
|
8
|
+
constructor(readonly params: PublicExecuteParams<AddressData>) {
|
|
9
|
+
super()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { CommandHandler } from "@nestjs/cqrs"
|
|
3
|
+
import { IdentityEmailService } from "@modules/domain/identity"
|
|
4
|
+
import { ICQRSHandler } from "@modules/platform/cqrs"
|
|
5
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
6
|
+
import type { Logger } from "@modules/platform/logging"
|
|
7
|
+
import { ForgotCommand } from "./forgot.command"
|
|
8
|
+
|
|
9
|
+
@CommandHandler(ForgotCommand)
|
|
10
|
+
/** Mails a password reset link when the address has an account; the answer never says whether it does. */
|
|
11
|
+
export class ForgotHandler extends ICQRSHandler<ForgotCommand, void> {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectLogger() logger: Logger,
|
|
14
|
+
private readonly email: IdentityEmailService,
|
|
15
|
+
) {
|
|
16
|
+
super(logger)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected override process(command: ForgotCommand): Promise<void> {
|
|
20
|
+
return this.email.forgot(command.params.request.email, command.params.request.language)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { Command } from "@nestjs/cqrs"
|
|
3
|
+
import type { PublicExecuteParams } from "@modules/platform/cqrs"
|
|
4
|
+
import type { AddressData } from "./auth.contracts"
|
|
5
|
+
|
|
6
|
+
/** Mails a new confirmation link to an unconfirmed account; the answer never says whether the address has one. */
|
|
7
|
+
export class ResendCommand extends Command<void> {
|
|
8
|
+
constructor(readonly params: PublicExecuteParams<AddressData>) {
|
|
9
|
+
super()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { CommandHandler } from "@nestjs/cqrs"
|
|
3
|
+
import { IdentityEmailService } from "@modules/domain/identity"
|
|
4
|
+
import { ICQRSHandler } from "@modules/platform/cqrs"
|
|
5
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
6
|
+
import type { Logger } from "@modules/platform/logging"
|
|
7
|
+
import { ResendCommand } from "./resend.command"
|
|
8
|
+
|
|
9
|
+
@CommandHandler(ResendCommand)
|
|
10
|
+
/** Mails a new confirmation link to an unconfirmed account; the answer never says whether the address has one. */
|
|
11
|
+
export class ResendHandler extends ICQRSHandler<ResendCommand, void> {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectLogger() logger: Logger,
|
|
14
|
+
private readonly email: IdentityEmailService,
|
|
15
|
+
) {
|
|
16
|
+
super(logger)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected override process(command: ResendCommand): Promise<void> {
|
|
20
|
+
return this.email.resend(command.params.request.email, command.params.request.language)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { Command } from "@nestjs/cqrs"
|
|
3
|
+
import type { PublicExecuteParams } from "@modules/platform/cqrs"
|
|
4
|
+
import type { ResetData } from "./auth.contracts"
|
|
5
|
+
|
|
6
|
+
/** Spends a reset link: sets the new password and ends every session of the person. */
|
|
7
|
+
export class ResetCommand extends Command<void> {
|
|
8
|
+
constructor(readonly params: PublicExecuteParams<ResetData>) {
|
|
9
|
+
super()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { CommandHandler } from "@nestjs/cqrs"
|
|
3
|
+
import { IdentityEmailService } from "@modules/domain/identity"
|
|
4
|
+
import { ICQRSHandler } from "@modules/platform/cqrs"
|
|
5
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
6
|
+
import type { Logger } from "@modules/platform/logging"
|
|
7
|
+
import { ResetCommand } from "./reset.command"
|
|
8
|
+
|
|
9
|
+
@CommandHandler(ResetCommand)
|
|
10
|
+
/** Spends a reset link: sets the new password and ends every session of the person. */
|
|
11
|
+
export class ResetHandler extends ICQRSHandler<ResetCommand, void> {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectLogger() logger: Logger,
|
|
14
|
+
private readonly email: IdentityEmailService,
|
|
15
|
+
) {
|
|
16
|
+
super(logger)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected override process(command: ResetCommand): Promise<void> {
|
|
20
|
+
return this.email.reset(command.params.request.token, command.params.request.password)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { Command } from "@nestjs/cqrs"
|
|
3
|
+
import type { PublicExecuteParams } from "@modules/platform/cqrs"
|
|
4
|
+
import type { SignupData } from "./auth.contracts"
|
|
5
|
+
|
|
6
|
+
/** Creates an unconfirmed password account and mails the confirmation link; the answer never says whether the address already had an account. */
|
|
7
|
+
export class SignupCommand extends Command<void> {
|
|
8
|
+
constructor(readonly params: PublicExecuteParams<SignupData>) {
|
|
9
|
+
super()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { CommandHandler } from "@nestjs/cqrs"
|
|
3
|
+
import { IdentityEmailService } from "@modules/domain/identity"
|
|
4
|
+
import { ICQRSHandler } from "@modules/platform/cqrs"
|
|
5
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
6
|
+
import type { Logger } from "@modules/platform/logging"
|
|
7
|
+
import { SignupCommand } from "./signup.command"
|
|
8
|
+
|
|
9
|
+
@CommandHandler(SignupCommand)
|
|
10
|
+
/** Creates an unconfirmed password account and mails the confirmation link; the answer never says whether the address already had an account. */
|
|
11
|
+
export class SignupHandler extends ICQRSHandler<SignupCommand, void> {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectLogger() logger: Logger,
|
|
14
|
+
private readonly email: IdentityEmailService,
|
|
15
|
+
) {
|
|
16
|
+
super(logger)
|
|
17
|
+
}
|
|
18
|
+
|
|
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)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
|
|
1
|
+
import { defineConfigurableModule } from "@starci/nest-kit"
|
|
3
2
|
import type { AuthApiOptions } from "./auth.options"
|
|
4
3
|
|
|
5
|
-
const built = new ConfigurableModuleBuilder<AuthApiOptions>().setClassMethodName("register").build()
|
|
6
|
-
type Host = ConfigurableModuleHost<AuthApiOptions, "register", "create", {}>
|
|
7
|
-
|
|
8
4
|
/** The configurable-module base of the auth feature module. */
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
|
|
12
|
-
/** The options of a synchronous registration, for the register signature. */
|
|
13
|
-
export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
defineConfigurableModule<AuthApiOptions>()
|