@starci/hfs 5.1.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 +54 -0
- package/README.md +11 -2
- 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/db/postgres.mjs +9 -1
- 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 +65 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +9 -9
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +67 -15
- package/runtime/knowledge/hfs/slots.yaml +174 -30
- package/runtime/modules/kernel/failure-codes.yaml +67 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
- package/runtime/scripts/hfs/architecture/config.mjs +25 -1
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- 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/transport-owner.d.mts +1 -2
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- 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/manifest-shape.d.mts +1 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
- 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/rules/stacks-local.d.mts +7 -0
- package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
- package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
- package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
- package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
- package/runtime/scripts/hfs/secret-dev-init.mjs +79 -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 +15 -8
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
- package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +1 -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/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +50 -3
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.d.mts +2 -2
- package/scaffold/app.mjs +16 -18
- package/scaffold/lite-exports.mjs +1 -15
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -0
- package/scripts/sync-bundles.d.mts +8 -0
- package/src/main.mjs +23 -2
- package/stack/cli.mjs +6 -1
- package/stack/context.d.mts +15 -0
- package/stack/context.mjs +1 -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-cli.d.mts +25 -0
- package/stack/local-cli.mjs +89 -0
- package/stack/local-config.d.mts +17 -0
- package/stack/local-config.mjs +37 -0
- package/stack/local-custody.d.mts +24 -0
- package/stack/local-custody.mjs +109 -0
- package/stack/local-doctor.d.mts +6 -0
- package/stack/local-doctor.mjs +49 -0
- package/stack/local-down.d.mts +8 -0
- package/stack/local-down.mjs +20 -0
- package/stack/local-host.d.mts +42 -0
- package/stack/local-host.mjs +163 -0
- package/stack/local-logs.d.mts +8 -0
- package/stack/local-logs.mjs +32 -0
- package/stack/local-ports.d.mts +7 -0
- package/stack/local-ports.mjs +39 -0
- package/stack/local-prepare.d.mts +22 -0
- package/stack/local-prepare.mjs +62 -0
- package/stack/local-run.d.mts +71 -0
- package/stack/local-run.mjs +200 -0
- package/stack/local-state.d.mts +35 -0
- package/stack/local-state.mjs +29 -0
- package/stack/local-status.d.mts +6 -0
- package/stack/local-status.mjs +36 -0
- package/stack/local-up.d.mts +11 -0
- package/stack/local-up.mjs +169 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -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 +15 -12
- package/sync/index.mjs +114 -28
- 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/module/jest.config.js.tpl +8 -0
- package/templates/app/module/src/index.ts.tpl +2 -0
- package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
- package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
- package/templates/app/module/src/manifest/index.ts.tpl +1 -0
- package/templates/app/module/src/public/index.ts.tpl +2 -0
- package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
- package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
- package/templates/app/module/src/template/index.ts.tpl +1 -0
- package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
- package/templates/app/module/src/workbench/index.ts.tpl +2 -0
- 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 +85 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +24 -12
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +43 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +21 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +6 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +76 -13
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +44 -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/get-jwks.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -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/session.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -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 +6 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +21 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +6 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +145 -0
- 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/google-start.request.ts +17 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.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 +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +19 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +209 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +84 -0
- 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-attempt.entity.ts +18 -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/identity-refresh-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +10 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +93 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +40 -0
- 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/app.module.ts +6 -5
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +12 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +40 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +50 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +3 -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 +28 -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/index.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- 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 +22 -4
- 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/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- 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/common/site-shell.tsx.partial +3 -3
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
- 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/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
- 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/components/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
- 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/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ObjectStorageOptions as StorageSettings } from "@starci/object-storage"
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly region: string
|
|
3
|
+
/**
|
|
4
|
+
* S3-compatible object storage (MinIO in lite) parsed once at API boot: the settings of `@starci/object-storage` (its clock is
|
|
5
|
+
* the app's `Clock`) and the one bucket of the app's objects. The key layout belongs to the domain that stores the object.
|
|
6
|
+
*/
|
|
7
|
+
export interface ObjectStorageOptions extends Omit<StorageSettings, "clock"> {
|
|
9
8
|
/** The bucket of the app's objects. */
|
|
10
9
|
readonly bucket: string
|
|
11
|
-
/** The access key id. */
|
|
12
|
-
readonly accessKeyId: Secret
|
|
13
|
-
/** The secret access key. */
|
|
14
|
-
readonly secretAccessKey: Secret
|
|
15
|
-
/** The lifetime of a presigned URL in seconds. */
|
|
16
|
-
readonly presignTtlSeconds: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** One presigned request: the URL the browser calls and when it stops working. */
|
|
20
|
-
export interface PresignedRequest {
|
|
21
|
-
/** The presigned URL. */
|
|
22
|
-
readonly url: string
|
|
23
|
-
/** The expiry instant, ISO-8601. */
|
|
24
|
-
readonly expiresAt: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** The object storage port: presigned PUT and GET of one key and a server-side delete. */
|
|
28
|
-
export interface ObjectStorage {
|
|
29
|
-
/** A presigned PUT of `key` with its content type fixed. */
|
|
30
|
-
presignPut(key: string, contentType: string): Promise<PresignedRequest>
|
|
31
|
-
/** A presigned GET of `key`. */
|
|
32
|
-
presignGet(key: string): Promise<PresignedRequest>
|
|
33
|
-
/** Deletes `key`. */
|
|
34
|
-
remove(key: string): Promise<void>
|
|
35
10
|
}
|
package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { DomainError } from "@modules/platform/errors"
|
|
2
2
|
import type { ErrorKind } from "@modules/platform/errors"
|
|
3
3
|
|
|
4
|
-
/** Codes of the passport integration. */
|
|
4
|
+
/** Codes of the passport integration: the wire codes @starci/auth names for the token, key and Google legs (the kit spec holds the parity). */
|
|
5
5
|
export enum PassportErrorCode {
|
|
6
|
-
/** The access token failed signature or
|
|
6
|
+
/** The access token failed signature, issuer, audience or expiry verification. */
|
|
7
7
|
AccessTokenInvalid = "PASSPORT_ACCESS_TOKEN_INVALID",
|
|
8
|
-
/** The
|
|
9
|
-
|
|
10
|
-
/** The
|
|
11
|
-
|
|
8
|
+
/** The signing key set is unusable. */
|
|
9
|
+
KeyInvalid = "PASSPORT_KEY_INVALID",
|
|
10
|
+
/** The request carried no email or password. */
|
|
11
|
+
MissingCredentials = "PASSPORT_MISSING_CREDENTIALS",
|
|
12
|
+
/** The Google round trip could not be proven to be ours (state, PKCE or cookie). */
|
|
13
|
+
GoogleStateInvalid = "PASSPORT_GOOGLE_STATE_INVALID",
|
|
14
|
+
/** Google refused the consent or answered no usable account. */
|
|
15
|
+
GoogleRejected = "PASSPORT_GOOGLE_REJECTED",
|
|
16
|
+
/** This deployment has no Google client. */
|
|
17
|
+
GoogleUnavailable = "PASSPORT_GOOGLE_UNAVAILABLE",
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
/** How each passport
|
|
20
|
+
/** How each passport code travels. */
|
|
15
21
|
export const PASSPORT_ERROR_KINDS: Record<PassportErrorCode, ErrorKind> = {
|
|
16
22
|
[PassportErrorCode.AccessTokenInvalid]: "unauthenticated",
|
|
17
|
-
[PassportErrorCode.
|
|
18
|
-
[PassportErrorCode.
|
|
23
|
+
[PassportErrorCode.KeyInvalid]: "internal",
|
|
24
|
+
[PassportErrorCode.MissingCredentials]: "invalid",
|
|
25
|
+
[PassportErrorCode.GoogleStateInvalid]: "unauthenticated",
|
|
26
|
+
[PassportErrorCode.GoogleRejected]: "unauthenticated",
|
|
27
|
+
[PassportErrorCode.GoogleUnavailable]: "unavailable",
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/** The one error class of the passport integration. */
|
|
@@ -1,12 +1,36 @@
|
|
|
1
|
+
export { AuthError, digestOf, isAcceptablePassword, isPresentablePassword, normalizeEmail } from "@starci/auth/server"
|
|
2
|
+
export type {
|
|
3
|
+
AccountDirectory,
|
|
4
|
+
AccountStanding,
|
|
5
|
+
AuthAuthenticator,
|
|
6
|
+
AuthDoors,
|
|
7
|
+
AuthPasswords,
|
|
8
|
+
AuthSessions,
|
|
9
|
+
AuthSubject,
|
|
10
|
+
GoogleProfile,
|
|
11
|
+
HttpExchange,
|
|
12
|
+
NewSession,
|
|
13
|
+
RateCounter,
|
|
14
|
+
RefreshTokenRecord,
|
|
15
|
+
SessionStore,
|
|
16
|
+
SessionTransaction,
|
|
17
|
+
SignInBudget,
|
|
18
|
+
} from "@starci/auth/server"
|
|
19
|
+
export type { AccessGrant, GoogleRedirect, JwksDocument } from "@starci/auth/contracts"
|
|
1
20
|
export { parsePassportConfig } from "./passport.config"
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
export {
|
|
22
|
+
OWN_TOKEN_VERIFIER,
|
|
23
|
+
PASSPORT_ACCOUNTS,
|
|
24
|
+
PASSPORT_AUTHENTICATOR,
|
|
25
|
+
PASSPORT_DOORS,
|
|
26
|
+
PASSPORT_OPTIONS,
|
|
27
|
+
PASSPORT_PASSWORDS,
|
|
28
|
+
PASSPORT_RATE_COUNTER,
|
|
29
|
+
PASSPORT_SESSION_STORE,
|
|
30
|
+
PASSPORT_SESSIONS,
|
|
31
|
+
PASSPORT_SIGN_IN_BUDGET,
|
|
32
|
+
} from "./passport.decorators"
|
|
33
|
+
export { PASSPORT_ERROR_KINDS, PassportError, PassportErrorCode } from "./errors/passport.error"
|
|
34
|
+
export { passportKit } from "./passport.kit"
|
|
35
|
+
export { passportModule } from "./passport.registration"
|
|
36
|
+
export type { PassportOptions, VerifyAccessToken } from "./passport.options"
|
package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts
CHANGED
|
@@ -5,13 +5,9 @@ import { parsePassportConfig } from "./passport.config"
|
|
|
5
5
|
const CONFIG: unique symbol = Symbol("passport.config")
|
|
6
6
|
|
|
7
7
|
const REQUIRED = {
|
|
8
|
-
GOOGLE_CLIENT_ID: "client",
|
|
9
|
-
GOOGLE_CLIENT_SECRET: "client-secret",
|
|
10
|
-
GOOGLE_CALLBACK_URL: "https://api.example.com/auth/google/callback",
|
|
11
8
|
AUTH_JWT_ISSUER: "https://api.example.com",
|
|
12
|
-
AUTH_JWT_AUDIENCE: "app",
|
|
13
9
|
AUTH_JWT_PRIVATE_KEY: "key",
|
|
14
|
-
|
|
10
|
+
AUTH_WEB_ORIGIN: "https://app.example.com/home",
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
const build = async () => {
|
|
@@ -21,14 +17,54 @@ const build = async () => {
|
|
|
21
17
|
|
|
22
18
|
const parse = async (extra: Record<string, string>) => (await build()).parsePassportConfig(new EnvSource({ ...REQUIRED, ...extra }))
|
|
23
19
|
|
|
20
|
+
describe("the required keys", () => {
|
|
21
|
+
it("reads the issuer, the web origin and the signing key, and defaults the lifetimes and the budget", async () => {
|
|
22
|
+
const options = await parse({})
|
|
23
|
+
expect(options.issuer).toBe("https://api.example.com")
|
|
24
|
+
expect(options.returnOrigin).toBe("https://app.example.com")
|
|
25
|
+
expect(options.keys).toHaveLength(1)
|
|
26
|
+
expect(options.audience).toBeUndefined()
|
|
27
|
+
expect(options).toMatchObject({
|
|
28
|
+
accessTtlSeconds: 900,
|
|
29
|
+
refreshTtlSeconds: 2_592_000,
|
|
30
|
+
bcryptCost: 10,
|
|
31
|
+
google: null,
|
|
32
|
+
cookie: { name: "refresh_token" },
|
|
33
|
+
budget: { loginAttempts: 10, resetAttempts: 5, windowMs: 900_000 },
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("refuses a boot without the signing key", async () => {
|
|
38
|
+
const subject = await build()
|
|
39
|
+
const { AUTH_JWT_PRIVATE_KEY: _key, ...without } = REQUIRED
|
|
40
|
+
expect(() => subject.parsePassportConfig(new EnvSource(without))).toThrow()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("the Google client", () => {
|
|
45
|
+
it("is declared by its client id and then needs its secret and callback", async () => {
|
|
46
|
+
const options = await parse({ GOOGLE_CLIENT_ID: "client", GOOGLE_CLIENT_SECRET: "secret", GOOGLE_CALLBACK_URL: "https://api.example.com/auth/google/callback" })
|
|
47
|
+
expect(options.google).toMatchObject({ clientId: "client", callbackUrl: "https://api.example.com/auth/google/callback" })
|
|
48
|
+
await expect(parse({ GOOGLE_CLIENT_ID: "client" })).rejects.toBeDefined()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("the key set", () => {
|
|
53
|
+
it("keeps the previous public key beside the signing key while a rotation runs", async () => {
|
|
54
|
+
const options = await parse({ AUTH_JWT_KEY_ID: "new", AUTH_JWT_PREVIOUS_PUBLIC_KEY: "previous-pem", AUTH_JWT_PREVIOUS_KEY_ID: "old" })
|
|
55
|
+
expect(options.keys.map((key) => key.keyId)).toEqual(["new", "old"])
|
|
56
|
+
expect(options.keys[1]?.publicKey).toBe("previous-pem")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
24
60
|
describe("AUTH_COOKIE_DOMAIN", () => {
|
|
25
61
|
it("leaves the refresh cookie host-only when the key is not declared", async () => {
|
|
26
|
-
expect((await parse({})).
|
|
62
|
+
expect((await parse({})).cookie.domain).toBeUndefined()
|
|
27
63
|
})
|
|
28
64
|
|
|
29
65
|
it("reads a bare registrable domain", async () => {
|
|
30
|
-
expect((await parse({ AUTH_COOKIE_DOMAIN: "example.com" })).
|
|
31
|
-
expect((await parse({ AUTH_COOKIE_DOMAIN: "Sub.Example.CO.uk" })).
|
|
66
|
+
expect((await parse({ AUTH_COOKIE_DOMAIN: "example.com" })).cookie.domain).toBe("example.com")
|
|
67
|
+
expect((await parse({ AUTH_COOKIE_DOMAIN: "Sub.Example.CO.uk" })).cookie.domain).toBe("sub.example.co.uk")
|
|
32
68
|
})
|
|
33
69
|
|
|
34
70
|
it.each(["https://example.com", "example.com:443", "example.com/path", ".example.com", "localhost", "127.0.0.1", "exa mple.com", "-a.example.com"])(
|
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
import type { EnvSource } from "@modules/platform/config"
|
|
2
2
|
import type { PassportOptions } from "./passport.options"
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const ACCESS_TTL_SECONDS = 900
|
|
5
|
+
const REFRESH_TTL_SECONDS = 2_592_000
|
|
6
|
+
const BCRYPT_COST = 10
|
|
7
|
+
const LOGIN_ATTEMPTS = 10
|
|
8
|
+
const RESET_ATTEMPTS = 5
|
|
9
|
+
const ATTEMPT_WINDOW_MS = 900_000
|
|
10
|
+
|
|
11
|
+
/** The Google client, or null when the deployment declares none (the Google door is then unavailable). */
|
|
12
|
+
const readGoogle = (env: EnvSource): PassportOptions["google"] =>
|
|
13
|
+
env.has("GOOGLE_CLIENT_ID")
|
|
14
|
+
? {
|
|
15
|
+
clientId: env.string("GOOGLE_CLIENT_ID"),
|
|
16
|
+
clientSecret: env.secret("GOOGLE_CLIENT_SECRET"),
|
|
17
|
+
callbackUrl: env.url("GOOGLE_CALLBACK_URL"),
|
|
18
|
+
}
|
|
19
|
+
: null
|
|
20
|
+
|
|
21
|
+
/** The key set: the signing key, and the previous public key while its access tokens are still valid (a rotation). */
|
|
22
|
+
const readKeys = (env: EnvSource): PassportOptions["keys"] => {
|
|
23
|
+
const previous = env.optional("AUTH_JWT_PREVIOUS_PUBLIC_KEY")
|
|
24
|
+
return [
|
|
25
|
+
{ privateKey: env.secret("AUTH_JWT_PRIVATE_KEY"), keyId: env.optional("AUTH_JWT_KEY_ID") },
|
|
26
|
+
...(previous === undefined ? [] : [{ publicKey: previous, keyId: env.optional("AUTH_JWT_PREVIOUS_KEY_ID") }]),
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Reads the sign-in configuration with no credential or localhost defaults; secrets read `<KEY>_FILE` first. */
|
|
5
31
|
export const parsePassportConfig = (env: EnvSource): PassportOptions => ({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
name: "refresh_token",
|
|
20
|
-
path: "/auth/refresh",
|
|
21
|
-
ttlSeconds: env.int("AUTH_REFRESH_TTL_SECONDS", 2_592_000),
|
|
22
|
-
cookieDomain: env.optionalDomain("AUTH_COOKIE_DOMAIN"),
|
|
32
|
+
issuer: env.url("AUTH_JWT_ISSUER"),
|
|
33
|
+
audience: env.optional("AUTH_JWT_AUDIENCE"),
|
|
34
|
+
keys: readKeys(env),
|
|
35
|
+
accessTtlSeconds: env.int("AUTH_ACCESS_TTL_SECONDS", ACCESS_TTL_SECONDS),
|
|
36
|
+
refreshTtlSeconds: env.int("AUTH_REFRESH_TTL_SECONDS", REFRESH_TTL_SECONDS),
|
|
37
|
+
bcryptCost: env.int("AUTH_BCRYPT_COST", BCRYPT_COST),
|
|
38
|
+
returnOrigin: new URL(env.url("AUTH_WEB_ORIGIN")).origin,
|
|
39
|
+
google: readGoogle(env),
|
|
40
|
+
cookie: { name: "refresh_token", domain: env.optionalDomain("AUTH_COOKIE_DOMAIN") },
|
|
41
|
+
budget: {
|
|
42
|
+
loginAttempts: env.int("AUTH_LOGIN_ATTEMPTS", LOGIN_ATTEMPTS),
|
|
43
|
+
resetAttempts: env.int("AUTH_RESET_ATTEMPTS", RESET_ATTEMPTS),
|
|
44
|
+
windowMs: env.duration("AUTH_ATTEMPT_WINDOW", ATTEMPT_WINDOW_MS),
|
|
23
45
|
},
|
|
24
46
|
})
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { AUTH_DOORS_SERVICE, AUTH_PASSWORDS, AUTH_SESSIONS, AUTH_SIGN_IN_BUDGET } from "@starci/auth/server"
|
|
1
2
|
import { injector } from "@modules/platform/composition"
|
|
2
3
|
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
4
|
import type { PassportOptions, VerifyAccessToken } from "./passport.options"
|
|
4
|
-
import type { FindPasswordAccount } from "./strategies/local.strategy"
|
|
5
5
|
|
|
6
6
|
/** Token of the passport options. */
|
|
7
7
|
export const PASSPORT_OPTIONS: unique symbol = Symbol("integrations.passport.options")
|
|
8
8
|
/** Token of the verifier of the app's own access tokens. */
|
|
9
9
|
export const OWN_TOKEN_VERIFIER: unique symbol = Symbol("integrations.passport.own-token-verifier")
|
|
10
|
-
/** Token of the
|
|
11
|
-
export const
|
|
10
|
+
/** Token of the session store the identity domain implements over the package's tables. */
|
|
11
|
+
export const PASSPORT_SESSION_STORE: unique symbol = Symbol("integrations.passport.session-store")
|
|
12
|
+
/** Token of the account directory the identity domain implements over the user table. */
|
|
13
|
+
export const PASSPORT_ACCOUNTS: unique symbol = Symbol("integrations.passport.accounts")
|
|
14
|
+
/** Token of the rate counter the identity domain implements over its attempts table. */
|
|
15
|
+
export const PASSPORT_RATE_COUNTER: unique symbol = Symbol("integrations.passport.rate-counter")
|
|
16
|
+
/** Token of the sign-in decisions (password and Google) the identity domain makes. */
|
|
17
|
+
export const PASSPORT_AUTHENTICATOR: unique symbol = Symbol("integrations.passport.authenticator")
|
|
12
18
|
|
|
13
19
|
/** Injects the passport options. Parameter type: PassportOptions. */
|
|
14
20
|
export const InjectPassportOptions = (): TypedParameterDecorator<PassportOptions> => injector<PassportOptions>(PASSPORT_OPTIONS)
|
|
@@ -17,6 +23,12 @@ export const InjectPassportOptions = (): TypedParameterDecorator<PassportOptions
|
|
|
17
23
|
export const InjectOwnTokenVerifier = (): TypedParameterDecorator<VerifyAccessToken> =>
|
|
18
24
|
injector<VerifyAccessToken>(OWN_TOKEN_VERIFIER)
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
export const
|
|
22
|
-
|
|
26
|
+
/** Token of the sign-in doors. */
|
|
27
|
+
export const PASSPORT_DOORS: typeof AUTH_DOORS_SERVICE = AUTH_DOORS_SERVICE
|
|
28
|
+
/** Token of the rotating sessions. */
|
|
29
|
+
export const PASSPORT_SESSIONS: typeof AUTH_SESSIONS = AUTH_SESSIONS
|
|
30
|
+
/** Token of the password hasher. */
|
|
31
|
+
export const PASSPORT_PASSWORDS: typeof AUTH_PASSWORDS = AUTH_PASSWORDS
|
|
32
|
+
/** Token of the failures-only sign-in budget. */
|
|
33
|
+
export const PASSPORT_SIGN_IN_BUDGET: typeof AUTH_SIGN_IN_BUDGET = AUTH_SIGN_IN_BUDGET
|
|
34
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { AUTH_ERROR_CODES, AUTH_ERROR_KINDS } from "@starci/auth/contracts"
|
|
3
|
+
import { generateTestKeyPair } from "@starci/auth/testing"
|
|
4
|
+
import { PASSPORT_ERROR_KINDS, PassportErrorCode } from "./errors/passport.error"
|
|
5
|
+
import { passportKit } from "./passport.kit"
|
|
6
|
+
import type { PassportOptions } from "./passport.options"
|
|
7
|
+
|
|
8
|
+
const KIT: unique symbol = Symbol("passport.kit")
|
|
9
|
+
|
|
10
|
+
const FAR_FUTURE = 32_503_680_000_000
|
|
11
|
+
const LONG_AGO = 0
|
|
12
|
+
|
|
13
|
+
const build = async () => {
|
|
14
|
+
const moduleRef = await Test.createTestingModule({ providers: [{ provide: KIT, useValue: { passportKit } }] }).compile()
|
|
15
|
+
return moduleRef.get<{ passportKit: typeof passportKit }>(KIT).passportKit
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const optionsFor = (keys: PassportOptions["keys"]): PassportOptions => ({
|
|
19
|
+
issuer: "https://api.example.test",
|
|
20
|
+
audience: undefined,
|
|
21
|
+
keys,
|
|
22
|
+
accessTtlSeconds: 60,
|
|
23
|
+
refreshTtlSeconds: 3600,
|
|
24
|
+
reuseGraceMs: undefined,
|
|
25
|
+
bcryptCost: 4,
|
|
26
|
+
returnOrigin: "https://app.example.test",
|
|
27
|
+
google: null,
|
|
28
|
+
cookie: { name: "refresh_token" },
|
|
29
|
+
budget: { loginAttempts: 10, resetAttempts: 5, windowMs: 900_000 },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("PassportErrorCode", () => {
|
|
33
|
+
it("names every PASSPORT wire code of @starci/auth with the same kind", () => {
|
|
34
|
+
const wire = Object.values(AUTH_ERROR_CODES).filter((code) => code.startsWith("PASSPORT_"))
|
|
35
|
+
|
|
36
|
+
expect(Object.values(PassportErrorCode).sort()).toEqual(wire.sort())
|
|
37
|
+
for (const code of Object.values(PassportErrorCode)) expect(PASSPORT_ERROR_KINDS[code]).toBe(AUTH_ERROR_KINDS[code])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("passportKit", () => {
|
|
42
|
+
it("verifies a token its own signer issued and builds the principal from it", async () => {
|
|
43
|
+
const create = await build()
|
|
44
|
+
const kit = create(optionsFor([{ privateKey: generateTestKeyPair().privateKey, keyId: "k1" }]))
|
|
45
|
+
const { token } = await kit.tokens.sign({ sub: "user-1", email: "a@example.test", sessionId: "s-1" }, new Date(FAR_FUTURE))
|
|
46
|
+
|
|
47
|
+
expect(await kit.verify(token)).toEqual({ kind: "ok", value: { id: "user-1", role: "authenticated", sessionId: "s-1", email: "a@example.test" } })
|
|
48
|
+
expect((await kit.tokens.jwks()).keys).toHaveLength(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("refuses an expired token, a token of another key and a token that is no token", async () => {
|
|
52
|
+
const create = await build()
|
|
53
|
+
const kit = create(optionsFor([{ privateKey: generateTestKeyPair().privateKey }]))
|
|
54
|
+
const stranger = create(optionsFor([{ privateKey: generateTestKeyPair().privateKey }]))
|
|
55
|
+
const expired = await kit.tokens.sign({ sub: "user-1", sessionId: "s-1" }, new Date(LONG_AGO))
|
|
56
|
+
const foreign = await stranger.tokens.sign({ sub: "user-1", sessionId: "s-1" }, new Date(FAR_FUTURE))
|
|
57
|
+
|
|
58
|
+
for (const token of [expired.token, foreign.token, "not-a-token"]) {
|
|
59
|
+
expect(await kit.verify(token)).toEqual({ kind: "refused", code: PassportErrorCode.AccessTokenInvalid })
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("keeps verifying the token of a retired key while its public key stays in the set", async () => {
|
|
64
|
+
const create = await build()
|
|
65
|
+
const retired = generateTestKeyPair()
|
|
66
|
+
const before = create(optionsFor([{ privateKey: retired.privateKey, keyId: "old" }]))
|
|
67
|
+
const { token } = await before.tokens.sign({ sub: "user-1", sessionId: "s-1" }, new Date(FAR_FUTURE))
|
|
68
|
+
const after = create(optionsFor([{ privateKey: generateTestKeyPair().privateKey, keyId: "new" }, { publicKey: retired.publicKey, keyId: "old" }]))
|
|
69
|
+
|
|
70
|
+
expect((await after.verify(token)).kind).toBe("ok")
|
|
71
|
+
expect((await after.tokens.jwks()).keys.map((key) => key["kid"])).toEqual(["new", "old"])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AuthPrincipalVerifier, AuthTokens } from "@starci/auth/server"
|
|
2
|
+
import { ok, refused } from "@modules/platform/primitives"
|
|
3
|
+
import { PassportErrorCode } from "./errors/passport.error"
|
|
4
|
+
import type { PassportOptions, VerifyAccessToken } from "./passport.options"
|
|
5
|
+
|
|
6
|
+
/** The signer of the app's own access tokens (it also serves the JWKS) and the verifier `domain/identity` turns into a principal. */
|
|
7
|
+
export interface PassportKit {
|
|
8
|
+
/** Signs access tokens and publishes every key of the set. */
|
|
9
|
+
readonly tokens: AuthTokens
|
|
10
|
+
/** Verifies one bearer token against the app's own key set. */
|
|
11
|
+
readonly verify: VerifyAccessToken
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds the kit from the typed options. The key set is imported on the first use, so a bad key fails the first verification
|
|
16
|
+
* instead of hiding behind a default; the package verifies against its own key set only, never a remote one.
|
|
17
|
+
*/
|
|
18
|
+
export const passportKit = (options: PassportOptions): PassportKit => {
|
|
19
|
+
const tokens = new AuthTokens(options)
|
|
20
|
+
const verifier = new AuthPrincipalVerifier(tokens)
|
|
21
|
+
const verify: VerifyAccessToken = async (token) => {
|
|
22
|
+
const verified = await verifier.verify(token)
|
|
23
|
+
return verified.kind === "ok" ? ok(verified.value) : refused(PassportErrorCode.AccessTokenInvalid)
|
|
24
|
+
}
|
|
25
|
+
return { tokens, verify }
|
|
26
|
+
}
|
|
@@ -1,68 +1,39 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AuthOptions } from "@starci/auth/server"
|
|
2
2
|
import type { Outcome } from "@modules/platform/primitives"
|
|
3
3
|
import type { PassportErrorCode } from "./errors/passport.error"
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The typed settings of the sign-in, parsed once at boot by `passport.config.ts` and handed to @starci/auth: the signing keys
|
|
7
|
+
* (the first with a private key signs; every key verifies and is published as the JWKS), the access and refresh lifetimes, the
|
|
8
|
+
* Google client, the refresh cookie and the sign-in budget. The stores the package needs (sessions, accounts, rate counter)
|
|
9
|
+
* are not settings: `domain/identity` provides them.
|
|
10
|
+
*/
|
|
11
|
+
export type PassportOptions = Pick<
|
|
12
|
+
AuthOptions,
|
|
13
|
+
| "issuer"
|
|
14
|
+
| "audience"
|
|
15
|
+
| "keys"
|
|
16
|
+
| "accessTtlSeconds"
|
|
17
|
+
| "refreshTtlSeconds"
|
|
18
|
+
| "reuseGraceMs"
|
|
19
|
+
| "bcryptCost"
|
|
20
|
+
| "returnOrigin"
|
|
21
|
+
| "google"
|
|
22
|
+
| "cookie"
|
|
23
|
+
| "budget"
|
|
24
|
+
>
|
|
25
|
+
|
|
5
26
|
/** The claims admitted from one of the app's own access tokens after every cryptographic and semantic check. */
|
|
6
27
|
export interface OwnPrincipal {
|
|
7
28
|
/** The user identifier from the verified `sub` claim (a row of auth.users). */
|
|
8
29
|
readonly id: string
|
|
9
30
|
/** The verified email claim when the user has one. */
|
|
10
31
|
readonly email?: string
|
|
32
|
+
/** The device session the token belongs to, when the token names one. */
|
|
33
|
+
readonly sessionId?: string
|
|
11
34
|
/** The role every access token the app issues carries; any other role is refused. */
|
|
12
35
|
readonly role: "authenticated"
|
|
13
36
|
}
|
|
14
37
|
|
|
15
38
|
/** Verifies one bearer token against the app's own key set and returns only claims that passed every check. */
|
|
16
39
|
export type VerifyAccessToken = (token: string) => Promise<Outcome<OwnPrincipal, PassportErrorCode>>
|
|
17
|
-
|
|
18
|
-
/** Issues one short-lived access token for a signed-in user. */
|
|
19
|
-
export type IssueAccessToken = (user: { readonly id: string; readonly email?: string }) => Promise<string>
|
|
20
|
-
|
|
21
|
-
/** Google OAuth 2.0 client configuration (authorization code with state and PKCE). */
|
|
22
|
-
export interface GoogleOptions {
|
|
23
|
-
/** The OAuth client id. */
|
|
24
|
-
readonly clientId: string
|
|
25
|
-
/** The OAuth client secret. */
|
|
26
|
-
readonly clientSecret: Secret
|
|
27
|
-
/** The absolute callback URL of the back end's Google callback door. */
|
|
28
|
-
readonly callbackUrl: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The app's own asymmetric access tokens and the JWKS that publishes their public key. */
|
|
32
|
-
export interface TokenOptions {
|
|
33
|
-
/** The issuer (`iss`), the app's public API origin. */
|
|
34
|
-
readonly issuer: string
|
|
35
|
-
/** The audience (`aud`) every token names. */
|
|
36
|
-
readonly audience: string
|
|
37
|
-
/** The ES256 private key in PKCS#8 PEM; its public half is served as the JWKS. */
|
|
38
|
-
readonly privateKey: Secret
|
|
39
|
-
/** The key id (`kid`) of the signing key. */
|
|
40
|
-
readonly keyId: string
|
|
41
|
-
/** The access-token lifetime in seconds. */
|
|
42
|
-
readonly accessTtlSeconds: number
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The rotating refresh token: an opaque value held only in an httpOnly, Secure, SameSite=Strict cookie. */
|
|
46
|
-
export interface RefreshCookieOptions {
|
|
47
|
-
/** The cookie name. */
|
|
48
|
-
readonly name: string
|
|
49
|
-
/** The path the browser sends the cookie to: the refresh door only. */
|
|
50
|
-
readonly path: string
|
|
51
|
-
/** The refresh-token lifetime in seconds. */
|
|
52
|
-
readonly ttlSeconds: number
|
|
53
|
-
/**
|
|
54
|
-
* The bare registrable domain every cookie passport sets and clears carries as `Domain` (`example.com`, no scheme,
|
|
55
|
-
* no port), so a web app on another subdomain can forward the cookie to this API. Unset keeps the cookies host-only.
|
|
56
|
-
*/
|
|
57
|
-
readonly cookieDomain?: string | undefined
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The passport integration configuration parsed once at API boot. */
|
|
61
|
-
export interface PassportOptions {
|
|
62
|
-
/** Google OAuth 2.0. */
|
|
63
|
-
readonly google: GoogleOptions
|
|
64
|
-
/** The app's own access tokens. */
|
|
65
|
-
readonly tokens: TokenOptions
|
|
66
|
-
/** The refresh cookie. */
|
|
67
|
-
readonly refresh: RefreshCookieOptions
|
|
68
|
-
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AuthModule } from "@starci/auth/server"
|
|
2
|
+
import type { AccountDirectory, RateCounter, SessionStore } from "@starci/auth/server"
|
|
3
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
4
|
+
import { CLOCK } from "@modules/platform/clock"
|
|
5
|
+
import type { Clock } from "@modules/platform/clock"
|
|
6
|
+
import { IDS } from "@modules/platform/ids"
|
|
7
|
+
import type { Ids } from "@modules/platform/ids"
|
|
8
|
+
import { PASSPORT_ACCOUNTS, PASSPORT_AUTHENTICATOR, PASSPORT_RATE_COUNTER, PASSPORT_SESSION_STORE } from "./passport.decorators"
|
|
9
|
+
import type { PassportOptions } from "./passport.options"
|
|
10
|
+
|
|
11
|
+
/** Whether the app root registers the sign-in package globally: the root decides, never the module. */
|
|
12
|
+
export interface PassportRegistration {
|
|
13
|
+
/** True when every module of the app may inject what the package provides, without importing it. */
|
|
14
|
+
readonly isGlobal: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The sign-in package as a Nest module: the typed options, the clock and ids of the app, and the stores the identity domain provides. */
|
|
18
|
+
export const passportModule = (options: PassportOptions, registration: PassportRegistration): DynamicModule =>
|
|
19
|
+
AuthModule.registerAsync({
|
|
20
|
+
isGlobal: registration.isGlobal,
|
|
21
|
+
authenticator: PASSPORT_AUTHENTICATOR,
|
|
22
|
+
inject: [PASSPORT_SESSION_STORE, PASSPORT_ACCOUNTS, PASSPORT_RATE_COUNTER, CLOCK, IDS],
|
|
23
|
+
useFactory: (sessions: SessionStore, accounts: AccountDirectory, rate: RateCounter, clock: Clock, ids: Ids) => ({
|
|
24
|
+
...options,
|
|
25
|
+
sessions,
|
|
26
|
+
accounts,
|
|
27
|
+
rate,
|
|
28
|
+
clock,
|
|
29
|
+
ids,
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { Principal, PublicExecuteParams } from "./cqrs.contracts"
|
|
2
|
-
export { InjectQueryBus } from "./cqrs.decorators"
|
|
2
|
+
export { InjectCommandBus, InjectQueryBus } from "./cqrs.decorators"
|
|
3
3
|
export { ICQRSHandler } from "./cqrs.handler"
|
|
4
4
|
export { CqrsModule } from "./cqrs.module"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { identityEntities } from "@modules/domain/identity"
|
|
1
2
|
import { httpSecurityEntities } from "@modules/platform/http-security"
|
|
2
3
|
import type { EntityClass } from "./database.options"
|
|
3
4
|
|
|
@@ -7,4 +8,4 @@ import type { EntityClass } from "./database.options"
|
|
|
7
8
|
* file, from the deep path `@modules/platform/database/entities`; DatabaseModule and the database barrel never do, since the
|
|
8
9
|
* capability barrels import the database capability back.
|
|
9
10
|
*/
|
|
10
|
-
export const PRIMARY_ENTITIES: ReadonlyArray<EntityClass> = [...httpSecurityEntities]
|
|
11
|
+
export const PRIMARY_ENTITIES: ReadonlyArray<EntityClass> = [...httpSecurityEntities, ...identityEntities]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { DatabaseModule } from "./database.module"
|
|
2
|
+
export { callDbFunction } from "./database.client"
|
|
2
3
|
export { DATABASE_PROBE, InjectDatabaseProbe, InjectPrimaryEntityManager, PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
|
|
3
4
|
export { DatabaseProbeService } from "./database-probe.service"
|
|
4
5
|
export type { DatabaseConnectionOptions } from "./database.options"
|
|
@@ -6,7 +6,7 @@ export type { HttpSecurityOptions } from "./http-security.options"
|
|
|
6
6
|
export { HTTP_SECURITY_MESSAGES } from "./messages/http-security.messages"
|
|
7
7
|
export { OriginGuard, ServerToServer } from "./origin.guard"
|
|
8
8
|
export { httpSecurityEntities } from "./persistence/entities"
|
|
9
|
-
export { RateLimitGuard } from "./rate-limit.guard"
|
|
9
|
+
export { RateLimit, RateLimitGuard, RateTier } from "./rate-limit.guard"
|
|
10
10
|
export { applyHttpSecurity } from "./edge-security.service"
|
|
11
11
|
export { ReadinessService } from "./readiness.service"
|
|
12
12
|
export { InjectReadiness } from "./readiness.decorators"
|
package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts
CHANGED
|
@@ -129,6 +129,17 @@ describe("RateLimitGuard", () => {
|
|
|
129
129
|
expect(entityManager.update).not.toHaveBeenCalled()
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
+
it("counts with an increment the database evaluates, so concurrent hits never overwrite each other", async () => {
|
|
133
|
+
const { entityManager, counting } = liveWindow(2)
|
|
134
|
+
const { guard } = await build(entityManager)
|
|
135
|
+
|
|
136
|
+
await guard.canActivate(contextOf("127.0.0.1"))
|
|
137
|
+
|
|
138
|
+
const assignment: unknown = counting?.callsOf("set")[0]?.[0]
|
|
139
|
+
const increment: unknown = typeof assignment === "object" && assignment !== null && "hits" in assignment && typeof assignment.hits === "function" ? Reflect.apply(assignment.hits, undefined, []) : undefined
|
|
140
|
+
expect(increment).toBe("hits + 1")
|
|
141
|
+
})
|
|
142
|
+
|
|
132
143
|
it("refuses a caller whose shared window is over the default limit", async () => {
|
|
133
144
|
const { guard } = await build(liveWindow(3).entityManager)
|
|
134
145
|
|
|
@@ -27,9 +27,16 @@ export const buildWiringProof = (): WiringProof => ({
|
|
|
27
27
|
schema: "public",
|
|
28
28
|
},
|
|
29
29
|
passport: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
issuer: "https://api.fixture.example",
|
|
31
|
+
audience: "fixture",
|
|
32
|
+
keys: [{ privateKey: new Secret("fixture-only"), keyId: "k1" }],
|
|
33
|
+
accessTtlSeconds: 900,
|
|
34
|
+
refreshTtlSeconds: 2592000,
|
|
35
|
+
bcryptCost: 4,
|
|
36
|
+
returnOrigin: "https://app.fixture.example",
|
|
37
|
+
google: null,
|
|
38
|
+
cookie: { name: "refresh_token" },
|
|
39
|
+
budget: { loginAttempts: 10, resetAttempts: 5, windowMs: 900000 },
|
|
33
40
|
},
|
|
34
41
|
objectStorage: {
|
|
35
42
|
endpoint: "https://storage.fixture.example",
|
|
@@ -37,7 +44,18 @@ export const buildWiringProof = (): WiringProof => ({
|
|
|
37
44
|
bucket: "fixture",
|
|
38
45
|
accessKeyId: new Secret("fixture-only"),
|
|
39
46
|
secretAccessKey: new Secret("fixture-only"),
|
|
40
|
-
|
|
47
|
+
defaultExpiresIn: 300,
|
|
48
|
+
},
|
|
49
|
+
mail: {
|
|
50
|
+
host: "127.0.0.1",
|
|
51
|
+
port: 2525,
|
|
52
|
+
security: "none",
|
|
53
|
+
from: { name: "Fixture", address: "no-reply@fixture.example" },
|
|
54
|
+
webOrigin: "https://app.fixture.example",
|
|
55
|
+
brandName: "Fixture",
|
|
56
|
+
defaultLanguage: "en",
|
|
57
|
+
confirmTtlSeconds: 86400,
|
|
58
|
+
resetTtlSeconds: 3600,
|
|
41
59
|
},
|
|
42
60
|
},
|
|
43
61
|
overrides: [
|