@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
|
@@ -160,6 +160,107 @@ function childLine(lines, block, key) {
|
|
|
160
160
|
return at;
|
|
161
161
|
return -1;
|
|
162
162
|
}
|
|
163
|
+
/** The service of `services:` that `member` is the sealed credential of: named like the member (`sonar`), or one whose credential custody path is the member's file. */
|
|
164
|
+
function toolingServiceOf(document, member) {
|
|
165
|
+
const services = (document["services"] ?? {});
|
|
166
|
+
for (const [name, service] of Object.entries(services)) {
|
|
167
|
+
if (service === null || typeof service !== "object")
|
|
168
|
+
continue;
|
|
169
|
+
const owns = (service.credentials ?? []).some((credential) => new RegExp(`/${member}\\.enc(?:\\.yaml)?$`, "u").test(credential?.custody?.path ?? ""));
|
|
170
|
+
if (name === member || owns)
|
|
171
|
+
return { name, service };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A tooling member (sonar.enc.yaml: the analysis token a CI job reads) is a credential of a service, not a secret the app reads at runtime, so it never
|
|
177
|
+
* becomes an `environments.<env>.secrets` entry (CI custody refuses a runtime-granted CI secret, APP_SECRET_IN_CI). Its binding is the service's
|
|
178
|
+
* `credentials` entry (custody names the sealed file) and the `ci.secrets` entry that reads it. A missing binding of the shape the template renders
|
|
179
|
+
* (`credentials: []`) is written; any other shape returns the exact binding to add by hand.
|
|
180
|
+
*/
|
|
181
|
+
function bindTooling(file, text, document, repository, environment, member, keys, found) {
|
|
182
|
+
const bound = new Set((found.service.credentials ?? []).map((credential) => credential.key));
|
|
183
|
+
const missing = keys.filter((key) => !bound.has(key));
|
|
184
|
+
if (!missing.length)
|
|
185
|
+
return { bound: [], remedy: "" };
|
|
186
|
+
const project = found.service.projects?.[0]?.key ?? repository;
|
|
187
|
+
const custodyPath = `.starcistacks/${environment}/secrets/${member}.enc.yaml`;
|
|
188
|
+
const first = !(found.service.credentials ?? []).length;
|
|
189
|
+
const credentials = missing.map((key, index) => ({ id: first && index === 0 ? "analysis" : key.toLowerCase().replaceAll("_", "-"), project, key, custody: { repository, path: custodyPath } }));
|
|
190
|
+
const ciSecrets = credentials.map((credential) => ({ name: credential.key, credential: credential.id }));
|
|
191
|
+
const remedy = `${member} is a tooling credential of services.${found.name}, not an app runtime secret, so it is not declared in environments.${environment}.secrets; bind it by hand: services.${found.name}.credentials: ${JSON.stringify(credentials)} and services.${found.name}.ci.secrets: ${JSON.stringify(ciSecrets)}`;
|
|
192
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
193
|
+
const lines = text.replace(/\r\n/gu, "\n").split("\n");
|
|
194
|
+
const root = lines.findIndex((line) => /^services:\s*(?:#.*)?$/u.test(line));
|
|
195
|
+
const serviceLine = root < 0 ? -1 : childLine(lines, blockOf(lines, root), found.name);
|
|
196
|
+
const credentialsLine = serviceLine < 0 ? -1 : childLine(lines, blockOf(lines, serviceLine), "credentials");
|
|
197
|
+
if (credentialsLine < 0 || !/^ *credentials:\s*\[\s*\]\s*(?:#.*)?$/u.test(lines[credentialsLine]) || !first)
|
|
198
|
+
return { bound: [], remedy };
|
|
199
|
+
const pad = indentOf(lines[credentialsLine]);
|
|
200
|
+
const ciLine = childLine(lines, blockOf(lines, serviceLine), "ci");
|
|
201
|
+
const expected = structuredClone(document);
|
|
202
|
+
const target = expected["services"][found.name];
|
|
203
|
+
target.credentials = credentials;
|
|
204
|
+
if (target.ci !== undefined)
|
|
205
|
+
target.ci.secrets = [...(target.ci.secrets ?? []), ...ciSecrets];
|
|
206
|
+
if (target.ci !== undefined) {
|
|
207
|
+
const ci = ciLine < 0 ? "" : lines[ciLine];
|
|
208
|
+
const secrets = `[${ciSecrets.map((entry) => `{name: ${entry.name}, credential: ${entry.credential}}`).join(", ")}]`;
|
|
209
|
+
if (/secrets:\s*\[\s*\]/u.test(ci))
|
|
210
|
+
lines[ciLine] = ci.replace(/secrets:\s*\[\s*\]/u, `secrets: ${secrets}`);
|
|
211
|
+
else if (/^ *ci:\s*\{.*\}\s*$/u.test(ci) && !/secrets:/u.test(ci))
|
|
212
|
+
lines[ciLine] = ci.replace(/\}\s*$/u, `, secrets: ${secrets}}`);
|
|
213
|
+
else
|
|
214
|
+
return { bound: [], remedy };
|
|
215
|
+
}
|
|
216
|
+
const block = credentials.flatMap((credential) => [`${" ".repeat(pad + 2)}- id: ${credential.id}`, `${" ".repeat(pad + 4)}project: ${JSON.stringify(credential.project)}`, `${" ".repeat(pad + 4)}key: ${credential.key}`, `${" ".repeat(pad + 4)}custody: {repository: ${JSON.stringify(credential.custody.repository)}, path: ${credential.custody.path}}`]);
|
|
217
|
+
lines.splice(credentialsLine, 1, `${" ".repeat(pad)}credentials:`, ...block);
|
|
218
|
+
const edited = lines.join(newline);
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = parseYaml(edited);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
parsed = undefined;
|
|
225
|
+
}
|
|
226
|
+
if (JSON.stringify(parsed) !== JSON.stringify(expected))
|
|
227
|
+
return { bound: [], remedy };
|
|
228
|
+
fs.writeFileSync(file, edited);
|
|
229
|
+
return { bound: missing, remedy: "" };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The tooling members of the app (a sealed `.starcistacks/<env>/secrets/<member>.enc.yaml` that a service of application-stacks.yaml owns): each is bound
|
|
233
|
+
* to its service when `write` and the binding is missing in the shape the template renders. The runtime `secrets:` entries an earlier `secret set` declared for
|
|
234
|
+
* the member are named, not removed: the owner deletes them (CI custody refuses the token while one stands).
|
|
235
|
+
*/
|
|
236
|
+
export function bindToolingMembers(repoRoot, write) {
|
|
237
|
+
const file = path.join(repoRoot, ".starcistacks", "application-stacks.yaml");
|
|
238
|
+
if (!fs.existsSync(file))
|
|
239
|
+
return [];
|
|
240
|
+
const text = fs.readFileSync(file, "utf8");
|
|
241
|
+
const document = parseYaml(text) ?? {}; // reason: parsed YAML boundary, read from disk
|
|
242
|
+
const repository = repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) });
|
|
243
|
+
const found = [];
|
|
244
|
+
for (const environment of Object.keys(document.environments ?? {})) {
|
|
245
|
+
const directory = path.join(repoRoot, ".starcistacks", environment, "secrets");
|
|
246
|
+
if (!fs.existsSync(directory))
|
|
247
|
+
continue;
|
|
248
|
+
for (const name of fs.readdirSync(directory).filter((entry) => entry.endsWith(MEMBER_SUFFIX)).sort()) {
|
|
249
|
+
const member = name.slice(0, -MEMBER_SUFFIX.length);
|
|
250
|
+
const service = toolingServiceOf(document, member);
|
|
251
|
+
if (service === null)
|
|
252
|
+
continue;
|
|
253
|
+
const keys = envelopeOf(fs.readFileSync(path.join(directory, name), "utf8")).keys;
|
|
254
|
+
const entries = Array.isArray(document.environments?.[environment]?.secrets) ? document.environments?.[environment]?.secrets : [];
|
|
255
|
+
const runtimeEntries = entries.filter((entry) => memberOfEntry(entry) === member).map((entry) => String(entry.name ?? entry.key ?? member));
|
|
256
|
+
const outcome = write
|
|
257
|
+
? bindTooling(file, fs.readFileSync(file, "utf8"), parseYaml(fs.readFileSync(file, "utf8")) ?? {}, repository, environment, member, keys, service)
|
|
258
|
+
: { bound: [], remedy: (service.service.credentials ?? []).length ? "" : `services.${service.name} has no credential for ${member}` };
|
|
259
|
+
found.push({ environment, member, service: service.name, outcome, runtimeEntries });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return found;
|
|
263
|
+
}
|
|
163
264
|
/**
|
|
164
265
|
* Declares each of `keys` of `member` in environment `environment`'s `secrets:` of application-stacks.yaml, one entry per key
|
|
165
266
|
* (`name: <kebab of the key>, member, key, where: secrets/<member>`), so the key is materialized as its own `<KEY>_FILE`. A key
|
|
@@ -175,6 +276,11 @@ export function declareKeys(repoRoot, environment, member, keys) {
|
|
|
175
276
|
const spec = document.environments?.[environment];
|
|
176
277
|
if (!spec)
|
|
177
278
|
return { declared: [], byHand: [] };
|
|
279
|
+
const tooling = toolingServiceOf(document, member);
|
|
280
|
+
if (tooling !== null) {
|
|
281
|
+
const outcome = bindTooling(file, text, document, repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) }), environment, member, keys, tooling);
|
|
282
|
+
return outcome.bound.length || outcome.remedy ? { declared: [], byHand: [], tooling: outcome } : { declared: [], byHand: [] };
|
|
283
|
+
}
|
|
178
284
|
const serviceKeys = declaredServiceKeys(document, repositoryName(repoRoot, { env: identityQueryEnvironment(process.env, serviceCredentialNames(document)) }), environment, member);
|
|
179
285
|
const missing = keys.filter((key) => !declaredKeys(spec, member, keys).includes(key) && !serviceKeys.includes(key));
|
|
180
286
|
if (!missing.length)
|
|
@@ -24,6 +24,7 @@ import { setValues } from "./secret-prompt.mjs";
|
|
|
24
24
|
import { stackContext } from "../../../stack/context.mjs";
|
|
25
25
|
import { StackError } from "../../../stack/errors.mjs";
|
|
26
26
|
import { serverKey } from "../../../stack/server-key.mjs";
|
|
27
|
+
import { seedDevCustody } from "./secret-dev-init.mjs";
|
|
27
28
|
export { envelopeOf, formatOf, plaintextOf } from "./secret-document.mjs";
|
|
28
29
|
export { setSecretValues } from "./secret-store.mjs";
|
|
29
30
|
/** The actions of `starci app secret`, in usage order. */
|
|
@@ -76,7 +77,7 @@ function parse(argv) {
|
|
|
76
77
|
/** The operands and flags of one secret action, as the usage of `starci app secret` prints them. */
|
|
77
78
|
export function secretUsage(verb) {
|
|
78
79
|
if (verb === "init")
|
|
79
|
-
return "";
|
|
80
|
+
return "[--env <dev env> [--execute]]";
|
|
80
81
|
if (verb === "set")
|
|
81
82
|
return "--env <env> <slug> <KEY> [KEY ...] [--generate <bytes>] [--from-file KEY=path ...]";
|
|
82
83
|
if (verb === "get")
|
|
@@ -100,8 +101,8 @@ export function secretUsage(verb) {
|
|
|
100
101
|
return "--env <env>";
|
|
101
102
|
}
|
|
102
103
|
function editOptionsFor(verb, opts) {
|
|
103
|
-
if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients")
|
|
104
|
-
throw new SecretError("--execute is only valid for unset, rename, remove, recipients, migrate, split and server-key");
|
|
104
|
+
if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients" && verb !== "init")
|
|
105
|
+
throw new SecretError("--execute is only valid for init --env, unset, rename, remove, recipients, migrate, split and server-key");
|
|
105
106
|
if (opts.execute !== undefined && verb === "recipients" && !opts.add && !opts.remove)
|
|
106
107
|
throw new SecretError("--execute is only valid for recipients with --add or --remove");
|
|
107
108
|
if ((opts.rotate !== undefined || opts.backup !== undefined || opts.restore !== undefined) && verb !== "server-key")
|
|
@@ -128,8 +129,8 @@ function outputOptionsFor(verb, opts) {
|
|
|
128
129
|
}
|
|
129
130
|
/** The environment leads the operands (`--env <env> <slug> ...` is checked and read as `<env> <slug> ...`); init takes none, audit may name one. */
|
|
130
131
|
function environmentOf(verb, opts) {
|
|
131
|
-
if (verb === "init" && opts.env !== undefined)
|
|
132
|
-
throw new SecretError("--
|
|
132
|
+
if (verb === "init" && opts.env === undefined && opts.execute !== undefined)
|
|
133
|
+
throw new SecretError("--execute is only valid for init --env <dev env>");
|
|
133
134
|
if (verb !== "init" && verb !== "audit" && opts.env === undefined)
|
|
134
135
|
throw new SecretError(`usage: starci app secret ${verb} ${secretUsage(verb)}`);
|
|
135
136
|
if (verb === "audit" && opts.positional.length)
|
|
@@ -141,7 +142,7 @@ function optionsFor(verb, opts) {
|
|
|
141
142
|
environmentOf(verb, opts);
|
|
142
143
|
const count = opts.positional.length;
|
|
143
144
|
const bounds = {
|
|
144
|
-
init: [0,
|
|
145
|
+
init: [0, 1],
|
|
145
146
|
list: [1, 1],
|
|
146
147
|
materialize: [1, 1],
|
|
147
148
|
verify: [1, 1],
|
|
@@ -192,7 +193,11 @@ function fromFiles(entries, keys) {
|
|
|
192
193
|
return files;
|
|
193
194
|
}
|
|
194
195
|
/** What `set` declared in the environment's stack `secrets:`, or the keys to declare by hand; nothing when all were declared. */
|
|
195
|
-
function declarationLine(environment, { declared, byHand }) {
|
|
196
|
+
function declarationLine(environment, { declared, byHand, tooling }) {
|
|
197
|
+
if (tooling?.bound.length)
|
|
198
|
+
return `bound ${tooling.bound.join(", ")} to the service credentials and ci.secrets of application-stacks.yaml (a tooling member is not a runtime secret)\n`;
|
|
199
|
+
if (tooling?.remedy)
|
|
200
|
+
return `${tooling.remedy}\n`;
|
|
196
201
|
if (declared.length)
|
|
197
202
|
return `declared ${declared.join(", ")} in environments.${environment}.secrets of application-stacks.yaml\n`;
|
|
198
203
|
if (byHand.length)
|
|
@@ -253,10 +258,12 @@ export async function secretMain(argv, { stdout = (s) => process.stdout.write(s)
|
|
|
253
258
|
const [verb, ...rest] = argv;
|
|
254
259
|
try {
|
|
255
260
|
if (!VERBS.has(verb))
|
|
256
|
-
throw new SecretError("usage: starci app secret init | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
|
|
261
|
+
throw new SecretError("usage: starci app secret init [--env <dev env> [--execute]] | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
|
|
257
262
|
const opts = parse(rest);
|
|
258
263
|
optionsFor(verb, opts);
|
|
259
264
|
const repoRoot = path.resolve(cwd, opts.cwd ?? ".");
|
|
265
|
+
if (verb === "init" && opts.env !== undefined)
|
|
266
|
+
return seedDevCustody(repoRoot, opts.env, { execute: opts.execute === true, sops: () => sops ?? defaultSops(env, repoRoot, platform, tools, hostIdentity), stdout, random });
|
|
260
267
|
if (verb === "init")
|
|
261
268
|
return publicStorage(() => initializeSecrets(repoRoot, { env, platform, tools, stdout, host: hostIdentity }));
|
|
262
269
|
if (verb === "audit")
|
|
@@ -232,7 +232,7 @@ export function readRepoDeclaration(manifest, repoRoot) {
|
|
|
232
232
|
function createScopeResolver(manifest, repo) {
|
|
233
233
|
const profile = repo.profile;
|
|
234
234
|
const edition = repo.edition ?? 'full';
|
|
235
|
-
const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
235
|
+
const slots = manifest.slots.filter((s) => s.managedOnly !== true && s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
236
236
|
const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
|
|
237
237
|
const classifyPath = (input) => classifyIn(scope, input);
|
|
238
238
|
/** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { StackProblem } from './stack-config-kit.mjs';
|
|
2
|
+
export declare const LOCAL_CONFIG_CODE = "STACKS_LOCAL_CONFIG";
|
|
3
|
+
/** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
|
|
4
|
+
export declare const GENERATORS: readonly string[];
|
|
5
|
+
/** A key of a sealed member of the environment. */
|
|
6
|
+
export interface CustodyRef {
|
|
7
|
+
member: string;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
/** A command the verbs run: an argv list (never a shell line), run from `cwd` (app-root relative). */
|
|
11
|
+
export interface LocalCommand {
|
|
12
|
+
name: string;
|
|
13
|
+
argv: string[];
|
|
14
|
+
cwd: string;
|
|
15
|
+
heavy: 'install' | 'build' | 'none';
|
|
16
|
+
skipIfExists: string | null;
|
|
17
|
+
env: Record<string, string>;
|
|
18
|
+
files: string[];
|
|
19
|
+
/** Seed only: `migrate` runs it once the database is migrated, `ready` once the processes answer (a seed that signs up through the api). */
|
|
20
|
+
after: 'migrate' | 'ready';
|
|
21
|
+
}
|
|
22
|
+
/** A compose service the environment starts and how its readiness is observed. */
|
|
23
|
+
export interface LocalService {
|
|
24
|
+
name: string;
|
|
25
|
+
wait: 'healthy' | 'started' | 'tcp';
|
|
26
|
+
port: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A native app process. */
|
|
29
|
+
export interface LocalProcess extends LocalCommand {
|
|
30
|
+
port: string | null;
|
|
31
|
+
health: HttpProbe | null;
|
|
32
|
+
}
|
|
33
|
+
/** One HTTP probe: the readiness of a process, a `ready` row or a `verify` row. */
|
|
34
|
+
export interface HttpProbe {
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
method: string;
|
|
38
|
+
status: number;
|
|
39
|
+
contains: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** The per-environment database: its compose service, its own login role and where the app reads its URL. */
|
|
42
|
+
export interface LocalDatabase {
|
|
43
|
+
service: string;
|
|
44
|
+
port: string;
|
|
45
|
+
name: string;
|
|
46
|
+
role: string;
|
|
47
|
+
urlEnv: string;
|
|
48
|
+
urlAs: 'env' | 'file';
|
|
49
|
+
adminUser: string;
|
|
50
|
+
password: CustodyRef;
|
|
51
|
+
adminPassword: CustodyRef;
|
|
52
|
+
appRole: string | null;
|
|
53
|
+
appPassword: CustodyRef | null;
|
|
54
|
+
}
|
|
55
|
+
/** One dev-only value the owner-armed `secret init` creates once. */
|
|
56
|
+
export interface CustodyGenerator extends CustodyRef {
|
|
57
|
+
kind: string;
|
|
58
|
+
bytes: number;
|
|
59
|
+
publicKey: string | null;
|
|
60
|
+
}
|
|
61
|
+
/** The normalised `local` block of one environment. */
|
|
62
|
+
export interface LocalBlock {
|
|
63
|
+
project: string;
|
|
64
|
+
ports: Record<string, number>;
|
|
65
|
+
services: LocalService[];
|
|
66
|
+
database: LocalDatabase | null;
|
|
67
|
+
composeEnv: Array<CustodyRef & {
|
|
68
|
+
name: string;
|
|
69
|
+
}>;
|
|
70
|
+
/** Least grants: the keys one compose service may read, mounted from `STACK_GRANT_<NAME>` (a directory holding only those keys). */
|
|
71
|
+
grants: Array<{
|
|
72
|
+
name: string;
|
|
73
|
+
keys: string[];
|
|
74
|
+
}>;
|
|
75
|
+
/** Data directories under the state base (`${data}/<name>`, `STACK_DATA_DIR`), created by `up` before anything starts: bind mounts and file stores of native processes. */
|
|
76
|
+
dirs: string[];
|
|
77
|
+
custody: CustodyGenerator[];
|
|
78
|
+
setup: LocalCommand[];
|
|
79
|
+
seed: LocalCommand[];
|
|
80
|
+
processes: LocalProcess[];
|
|
81
|
+
ready: HttpProbe[];
|
|
82
|
+
verify: HttpProbe[];
|
|
83
|
+
origins: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
url: string;
|
|
86
|
+
}>;
|
|
87
|
+
resources: {
|
|
88
|
+
diskGb: number;
|
|
89
|
+
ramMb: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
|
|
93
|
+
export declare function normalizeLocal(raw: unknown, { project, env }: {
|
|
94
|
+
project: string;
|
|
95
|
+
env: string;
|
|
96
|
+
}): {
|
|
97
|
+
config: LocalBlock | null;
|
|
98
|
+
problems: StackProblem[];
|
|
99
|
+
};
|
|
100
|
+
/** Replaces `${port.<name>}` with the chosen host port. */
|
|
101
|
+
export declare const expandPorts: (value: string, ports: Readonly<Record<string, number>>) => string;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// stack-local-config.mjs - reads and judges `environments.<env>.local`, the declaration of a dev environment (runtime docker-compose) that
|
|
2
|
+
// `starci app stack up|down|doctor|prepare|status|logs|verify --env <env>` runs (PROJECT-STACKS-6). It holds names, ports, argv lists and
|
|
3
|
+
// shapes only, never a value: custody keys are references into .starcistacks/<env>/secrets (PROJECT-STACKS-5). The authority for the fields is
|
|
4
|
+
// this module; the same normaliser serves `starci app check` (rule R340) and the verbs, so a block the check passes is a block the verbs run.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { isPlainObject as plain } from '../../engine/plain-object.mjs';
|
|
7
|
+
import { closedFields, collector, patternField } from './stack-config-kit.mjs';
|
|
8
|
+
export const LOCAL_CONFIG_CODE = 'STACKS_LOCAL_CONFIG';
|
|
9
|
+
const NAME = /^[a-z][a-z0-9-]{0,40}$/u;
|
|
10
|
+
const KEY = /^[A-Z][A-Z0-9_]{0,63}$/u;
|
|
11
|
+
const PROJECT = /^[a-z0-9][a-z0-9_-]{0,62}$/u;
|
|
12
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]{0,40}$/u;
|
|
13
|
+
const SQL_NAME = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
14
|
+
/** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
|
|
15
|
+
export const GENERATORS = Object.freeze(['hex', 'base64', 'password', 'uuid', 'rsa-pem', 'ec-pem', 'ed25519-pem']);
|
|
16
|
+
const TOP = ['project', 'ports', 'services', 'database', 'composeEnv', 'grants', 'dirs', 'custody', 'setup', 'seed', 'processes', 'ready', 'verify', 'origins', 'resources'];
|
|
17
|
+
const COMMAND = ['name', 'argv', 'cwd', 'heavy', 'skipIfExists', 'env', 'files'];
|
|
18
|
+
const SEED = [...COMMAND, 'after'];
|
|
19
|
+
const PROCESS = [...COMMAND, 'port', 'health'];
|
|
20
|
+
const text = (value) => typeof value === 'string' && value.trim() !== '';
|
|
21
|
+
/** A relative path inside the app root: no drive, no `..`, forward slashes. */
|
|
22
|
+
function relative(c, at, value) {
|
|
23
|
+
if (typeof value !== 'string' || value === '' || path.posix.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || value.includes('\\') || path.posix.normalize(value).split('/').includes('..')) {
|
|
24
|
+
c.problem(at, 'must be a relative path inside the app root');
|
|
25
|
+
return '.';
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function ref(c, at, value) {
|
|
30
|
+
if (!closedFields(c, at, value, ['member', 'key'], 'local'))
|
|
31
|
+
return { member: '', key: '' };
|
|
32
|
+
const entry = value;
|
|
33
|
+
patternField(c, `${at}.member`, entry.member, /^[a-z0-9][a-z0-9-]*$/u);
|
|
34
|
+
patternField(c, `${at}.key`, entry.key, KEY);
|
|
35
|
+
return { member: String(entry.member), key: String(entry.key) };
|
|
36
|
+
}
|
|
37
|
+
function strings(c, at, value, pattern = null) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
if (!Array.isArray(value) || value.some((item) => !text(item))) {
|
|
41
|
+
c.problem(at, 'must be a list of non-empty strings');
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
if (pattern)
|
|
45
|
+
value.forEach((item, index) => patternField(c, `${at}[${index}]`, item, pattern));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function command(c, at, value, allowed, names) {
|
|
49
|
+
const empty = { name: '', argv: [], cwd: '.', heavy: 'none', skipIfExists: null, env: {}, files: [], after: 'migrate' };
|
|
50
|
+
if (!closedFields(c, at, value, allowed, 'local'))
|
|
51
|
+
return empty;
|
|
52
|
+
const entry = value;
|
|
53
|
+
const name = String(entry.name ?? '');
|
|
54
|
+
if (patternField(c, `${at}.name`, entry.name, NAME)) {
|
|
55
|
+
if (names.has(name))
|
|
56
|
+
c.problem(`${at}.name`, `${name} is declared twice`);
|
|
57
|
+
names.add(name);
|
|
58
|
+
}
|
|
59
|
+
const argv = strings(c, `${at}.argv`, entry.argv);
|
|
60
|
+
if (argv.length === 0)
|
|
61
|
+
c.problem(`${at}.argv`, 'must hold the command and its arguments (an argv list, never a shell line)');
|
|
62
|
+
const heavy = entry.heavy ?? 'none';
|
|
63
|
+
if (!['install', 'build', 'none'].includes(String(heavy)))
|
|
64
|
+
c.problem(`${at}.heavy`, 'must be install, build or none (the heavy-command governor class of the command)');
|
|
65
|
+
let env = {};
|
|
66
|
+
if (entry.env !== undefined) {
|
|
67
|
+
if (!plain(entry.env))
|
|
68
|
+
c.problem(`${at}.env`, 'must be a mapping of non-secret values');
|
|
69
|
+
else {
|
|
70
|
+
env = entry.env;
|
|
71
|
+
for (const [key, item] of Object.entries(env)) {
|
|
72
|
+
patternField(c, `${at}.env.${key}`, key, KEY);
|
|
73
|
+
if (typeof item !== 'string')
|
|
74
|
+
c.problem(`${at}.env.${key}`, 'must be a string');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name, argv, cwd: entry.cwd === undefined ? '.' : relative(c, `${at}.cwd`, entry.cwd), heavy: heavy,
|
|
80
|
+
skipIfExists: entry.skipIfExists === undefined ? null : relative(c, `${at}.skipIfExists`, entry.skipIfExists), env,
|
|
81
|
+
files: strings(c, `${at}.files`, entry.files, KEY), after: afterOf(c, at, entry.after),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function afterOf(c, at, value) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return 'migrate';
|
|
87
|
+
if (value !== 'migrate' && value !== 'ready')
|
|
88
|
+
c.problem(`${at}.after`, 'must be migrate or ready');
|
|
89
|
+
return value === 'ready' ? 'ready' : 'migrate';
|
|
90
|
+
}
|
|
91
|
+
function probe(c, at, value) {
|
|
92
|
+
const empty = { name: '', url: '', method: 'GET', status: 200, contains: null };
|
|
93
|
+
if (!closedFields(c, at, value, ['name', 'url', 'method', 'status', 'contains'], 'local'))
|
|
94
|
+
return empty;
|
|
95
|
+
const entry = value;
|
|
96
|
+
patternField(c, `${at}.name`, entry.name, NAME);
|
|
97
|
+
const url = String(entry.url ?? '');
|
|
98
|
+
if (!/^http:\/\/(localhost|127\.0\.0\.1|[a-z0-9-]+\.localhost)(:\$\{port\.[a-z][a-z0-9-]*\}|:[0-9]+)?(\/.*)?$/u.test(url))
|
|
99
|
+
c.problem(`${at}.url`, 'must be an http URL on the loopback (localhost, 127.0.0.1 or *.localhost), its port a literal or ${port.<name>}');
|
|
100
|
+
const method = String(entry.method ?? 'GET');
|
|
101
|
+
if (!['GET', 'HEAD', 'POST'].includes(method))
|
|
102
|
+
c.problem(`${at}.method`, 'must be GET, HEAD or POST');
|
|
103
|
+
const status = entry.status === undefined ? 200 : entry.status;
|
|
104
|
+
if (!Number.isInteger(status) || status < 100 || status > 599)
|
|
105
|
+
c.problem(`${at}.status`, 'must be an HTTP status');
|
|
106
|
+
return { name: String(entry.name ?? ''), url, method, status: status, contains: text(entry.contains) ? entry.contains : null };
|
|
107
|
+
}
|
|
108
|
+
function portsOf(c, value) {
|
|
109
|
+
const ports = {};
|
|
110
|
+
if (!plain(value) || Object.keys(value).length === 0) {
|
|
111
|
+
c.problem('ports', 'must name at least one port: the host ports the environment publishes or listens on');
|
|
112
|
+
return ports;
|
|
113
|
+
}
|
|
114
|
+
const seen = new Map();
|
|
115
|
+
for (const [name, port] of Object.entries(value)) {
|
|
116
|
+
patternField(c, `ports.${name}`, name, NAME);
|
|
117
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
118
|
+
c.problem(`ports.${name}`, 'must be a port from 1024 to 65535');
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (seen.has(port))
|
|
122
|
+
c.problem(`ports.${name}`, `is the port of ${seen.get(port)} too`);
|
|
123
|
+
seen.set(port, name);
|
|
124
|
+
ports[name] = port;
|
|
125
|
+
}
|
|
126
|
+
return ports;
|
|
127
|
+
}
|
|
128
|
+
function servicesOf(c, value, ports) {
|
|
129
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
130
|
+
c.problem('services', 'must list the compose services `up` starts, dependencies first');
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return value.map((item, index) => {
|
|
134
|
+
const at = `services[${index}]`;
|
|
135
|
+
if (!closedFields(c, at, item, ['name', 'wait', 'port'], 'local'))
|
|
136
|
+
return { name: '', wait: 'started', port: null };
|
|
137
|
+
const entry = item;
|
|
138
|
+
patternField(c, `${at}.name`, entry.name, NAME);
|
|
139
|
+
const wait = String(entry.wait ?? 'healthy');
|
|
140
|
+
if (!['healthy', 'started', 'tcp'].includes(wait))
|
|
141
|
+
c.problem(`${at}.wait`, 'must be healthy (the compose healthcheck), tcp (the port accepts) or started');
|
|
142
|
+
const port = entry.port === undefined ? null : String(entry.port);
|
|
143
|
+
if (wait === 'tcp' && (port === null || !Object.hasOwn(ports, port)))
|
|
144
|
+
c.problem(`${at}.port`, 'must name a declared port when wait is tcp');
|
|
145
|
+
if (port !== null && !Object.hasOwn(ports, port))
|
|
146
|
+
c.problem(`${at}.port`, `${port} is not a declared port`);
|
|
147
|
+
return { name: String(entry.name ?? ''), wait: wait, port };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function databaseOf(c, value, ports, services, role) {
|
|
151
|
+
if (value === undefined)
|
|
152
|
+
return null;
|
|
153
|
+
if (!closedFields(c, 'database', value, ['service', 'port', 'name', 'role', 'urlEnv', 'urlAs', 'adminUser', 'password', 'adminPassword', 'appRole', 'appPassword'], 'local'))
|
|
154
|
+
return null;
|
|
155
|
+
const entry = value;
|
|
156
|
+
patternField(c, 'database.service', entry.service, NAME);
|
|
157
|
+
if (!services.some((service) => service.name === entry.service))
|
|
158
|
+
c.problem('database.service', 'must be one of the started services');
|
|
159
|
+
if (!Object.hasOwn(ports, String(entry.port)))
|
|
160
|
+
c.problem('database.port', 'must name a declared port');
|
|
161
|
+
const own = entry.role === undefined ? role : String(entry.role);
|
|
162
|
+
patternField(c, 'database.role', own, SQL_NAME);
|
|
163
|
+
const name = entry.name === undefined ? own : String(entry.name);
|
|
164
|
+
patternField(c, 'database.name', name, SQL_NAME);
|
|
165
|
+
patternField(c, 'database.urlEnv', entry.urlEnv, KEY);
|
|
166
|
+
const urlAs = String(entry.urlAs ?? 'file');
|
|
167
|
+
if (!['env', 'file'].includes(urlAs))
|
|
168
|
+
c.problem('database.urlAs', 'must be env (the URL in the process environment) or file (<KEY>_FILE, preferred)');
|
|
169
|
+
const adminUser = String(entry.adminUser ?? 'postgres');
|
|
170
|
+
patternField(c, 'database.adminUser', adminUser, SQL_NAME);
|
|
171
|
+
if (own === adminUser || own === 'postgres')
|
|
172
|
+
c.problem('database.role', 'must be a role of this environment alone, never the superuser and never a role another environment uses');
|
|
173
|
+
const appRole = entry.appRole === undefined ? null : String(entry.appRole);
|
|
174
|
+
if (appRole !== null)
|
|
175
|
+
patternField(c, 'database.appRole', appRole, SQL_NAME);
|
|
176
|
+
if ((appRole === null) !== (entry.appPassword === undefined))
|
|
177
|
+
c.problem('database.appPassword', 'is declared with appRole, both or neither: the app role the migrations create gets its own password per environment');
|
|
178
|
+
if (appRole !== null && (appRole === own || appRole === adminUser))
|
|
179
|
+
c.problem('database.appRole', 'must be a role other than the owner role and the superuser');
|
|
180
|
+
return {
|
|
181
|
+
appRole, appPassword: entry.appPassword === undefined ? null : ref(c, 'database.appPassword', entry.appPassword),
|
|
182
|
+
service: String(entry.service), port: String(entry.port), name, role: own, urlEnv: String(entry.urlEnv), urlAs: urlAs, adminUser,
|
|
183
|
+
password: ref(c, 'database.password', entry.password), adminPassword: ref(c, 'database.adminPassword', entry.adminPassword),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function custodyOf(c, value) {
|
|
187
|
+
if (value === undefined)
|
|
188
|
+
return [];
|
|
189
|
+
if (!Array.isArray(value)) {
|
|
190
|
+
c.problem('custody', 'must be a list of generated dev values');
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return value.map((item, index) => {
|
|
194
|
+
const at = `custody[${index}]`;
|
|
195
|
+
if (!closedFields(c, at, item, ['member', 'key', 'kind', 'bytes', 'publicKey'], 'local'))
|
|
196
|
+
return { member: '', key: '', kind: 'hex', bytes: 32, publicKey: null };
|
|
197
|
+
const entry = item;
|
|
198
|
+
const base = ref(c, at, { member: entry.member, key: entry.key });
|
|
199
|
+
if (!GENERATORS.includes(String(entry.kind)))
|
|
200
|
+
c.problem(`${at}.kind`, `must be one of ${GENERATORS.join(', ')}`);
|
|
201
|
+
const bytes = entry.bytes === undefined ? 32 : entry.bytes;
|
|
202
|
+
if (!Number.isInteger(bytes) || bytes < 8 || bytes > 1024)
|
|
203
|
+
c.problem(`${at}.bytes`, 'must be a whole byte count from 8 to 1024');
|
|
204
|
+
if (entry.publicKey !== undefined) {
|
|
205
|
+
if (entry.kind === 'rsa-pem' || entry.kind === 'ec-pem' || entry.kind === 'ed25519-pem')
|
|
206
|
+
patternField(c, `${at}.publicKey`, entry.publicKey, KEY);
|
|
207
|
+
else
|
|
208
|
+
c.problem(`${at}.publicKey`, 'is only for kind rsa-pem, ec-pem or ed25519-pem');
|
|
209
|
+
}
|
|
210
|
+
return { ...base, kind: String(entry.kind), bytes: bytes, publicKey: text(entry.publicKey) ? entry.publicKey : null };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function listOf(c, at, value, each) {
|
|
214
|
+
if (value === undefined)
|
|
215
|
+
return [];
|
|
216
|
+
if (!Array.isArray(value)) {
|
|
217
|
+
c.problem(at, 'must be a list');
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
return value.map((item, index) => each(item, index));
|
|
221
|
+
}
|
|
222
|
+
function referencedPorts(c, block) {
|
|
223
|
+
const known = new Set(Object.keys(block.ports));
|
|
224
|
+
const check = (at, value) => {
|
|
225
|
+
for (const match of value.matchAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu))
|
|
226
|
+
if (!known.has(match[1]))
|
|
227
|
+
c.problem(at, `names the undeclared port ${match[1]}`);
|
|
228
|
+
};
|
|
229
|
+
for (const item of [...block.ready, ...block.verify])
|
|
230
|
+
check(`url of ${item.name}`, item.url);
|
|
231
|
+
for (const origin of block.origins)
|
|
232
|
+
check(`origin ${origin.name}`, origin.url);
|
|
233
|
+
for (const run of [...block.setup, ...block.seed, ...block.processes]) {
|
|
234
|
+
for (const part of run.argv)
|
|
235
|
+
check(`argv of ${run.name}`, part);
|
|
236
|
+
for (const value of Object.values(run.env))
|
|
237
|
+
check(`env of ${run.name}`, value);
|
|
238
|
+
}
|
|
239
|
+
for (const process of block.processes) {
|
|
240
|
+
if (process.port !== null && !known.has(process.port))
|
|
241
|
+
c.problem(`processes.${process.name}.port`, `${process.port} is not a declared port`);
|
|
242
|
+
if (process.health)
|
|
243
|
+
check(`health of ${process.name}`, process.health.url);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
|
|
247
|
+
export function normalizeLocal(raw, { project, env }) {
|
|
248
|
+
const c = collector();
|
|
249
|
+
if (!closedFields(c, 'local', raw, TOP, 'local'))
|
|
250
|
+
return { config: null, problems: c.problems };
|
|
251
|
+
const entry = raw;
|
|
252
|
+
const compose = String(entry.project ?? `${project}-${env}`).toLowerCase().replaceAll(/[^a-z0-9_-]+/gu, '-');
|
|
253
|
+
patternField(c, 'project', compose, PROJECT);
|
|
254
|
+
const ports = portsOf(c, entry.ports);
|
|
255
|
+
const services = servicesOf(c, entry.services, ports);
|
|
256
|
+
const role = `${project}_${env}`.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '_').replace(/^[^a-z]+/u, 'a');
|
|
257
|
+
const names = new Set();
|
|
258
|
+
const block = {
|
|
259
|
+
project: compose, ports, services,
|
|
260
|
+
database: databaseOf(c, entry.database, ports, services, role),
|
|
261
|
+
composeEnv: listOf(c, 'composeEnv', entry.composeEnv, (item, index) => {
|
|
262
|
+
const at = `composeEnv[${index}]`;
|
|
263
|
+
if (!closedFields(c, at, item, ['name', 'member', 'key'], 'local'))
|
|
264
|
+
return { name: '', member: '', key: '' };
|
|
265
|
+
const value = item;
|
|
266
|
+
patternField(c, `${at}.name`, value.name, KEY);
|
|
267
|
+
return { name: String(value.name), ...ref(c, at, { member: value.member, key: value.key }) };
|
|
268
|
+
}),
|
|
269
|
+
grants: listOf(c, 'grants', entry.grants, (item, index) => {
|
|
270
|
+
const at = `grants[${index}]`;
|
|
271
|
+
if (!closedFields(c, at, item, ['name', 'keys'], 'local'))
|
|
272
|
+
return { name: '', keys: [] };
|
|
273
|
+
const value = item;
|
|
274
|
+
patternField(c, `${at}.name`, value.name, NAME);
|
|
275
|
+
const keys = strings(c, `${at}.keys`, value.keys, KEY);
|
|
276
|
+
if (keys.length === 0)
|
|
277
|
+
c.problem(`${at}.keys`, 'must list the keys the service reads');
|
|
278
|
+
return { name: String(value.name), keys };
|
|
279
|
+
}),
|
|
280
|
+
dirs: strings(c, 'dirs', entry.dirs, NAME_PATTERN),
|
|
281
|
+
custody: custodyOf(c, entry.custody),
|
|
282
|
+
setup: listOf(c, 'setup', entry.setup, (item, index) => command(c, `setup[${index}]`, item, COMMAND, names)),
|
|
283
|
+
seed: listOf(c, 'seed', entry.seed, (item, index) => command(c, `seed[${index}]`, item, SEED, names)),
|
|
284
|
+
processes: listOf(c, 'processes', entry.processes, (item, index) => {
|
|
285
|
+
const at = `processes[${index}]`;
|
|
286
|
+
const base = command(c, at, item, PROCESS, names);
|
|
287
|
+
const value = plain(item) ? item : {};
|
|
288
|
+
return { ...base, port: value.port === undefined ? null : String(value.port), health: value.health === undefined ? null : probe(c, `${at}.health`, { name: base.name || 'health', ...value.health }) };
|
|
289
|
+
}),
|
|
290
|
+
ready: listOf(c, 'ready', entry.ready, (item, index) => probe(c, `ready[${index}]`, item)),
|
|
291
|
+
verify: listOf(c, 'verify', entry.verify, (item, index) => probe(c, `verify[${index}]`, item)),
|
|
292
|
+
origins: listOf(c, 'origins', entry.origins, (item, index) => {
|
|
293
|
+
const at = `origins[${index}]`;
|
|
294
|
+
if (!closedFields(c, at, item, ['name', 'url'], 'local'))
|
|
295
|
+
return { name: '', url: '' };
|
|
296
|
+
const value = item;
|
|
297
|
+
patternField(c, `${at}.name`, value.name, NAME);
|
|
298
|
+
return { name: String(value.name), url: String(value.url ?? '') };
|
|
299
|
+
}),
|
|
300
|
+
resources: { diskGb: 10, ramMb: 4096 },
|
|
301
|
+
};
|
|
302
|
+
if (entry.resources !== undefined && closedFields(c, 'resources', entry.resources, ['diskGb', 'ramMb'], 'local')) {
|
|
303
|
+
const value = entry.resources;
|
|
304
|
+
for (const [key, floor] of [['diskGb', 1], ['ramMb', 512]]) {
|
|
305
|
+
if (value[key] === undefined)
|
|
306
|
+
continue;
|
|
307
|
+
if (!Number.isInteger(value[key]) || value[key] < floor)
|
|
308
|
+
c.problem(`resources.${key}`, `must be a whole number from ${floor}`);
|
|
309
|
+
else
|
|
310
|
+
block.resources[key] = value[key];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
referencedPorts(c, block);
|
|
314
|
+
const generated = new Set(block.custody.flatMap((item) => [`${item.member}/${item.key}`, ...(item.publicKey ? [`${item.member}/${item.publicKey}`] : [])]));
|
|
315
|
+
if (generated.size < block.custody.length)
|
|
316
|
+
c.problem('custody', 'declares a key twice');
|
|
317
|
+
return c.problems.length === 0 ? { config: block, problems: [] } : { config: null, problems: c.problems };
|
|
318
|
+
}
|
|
319
|
+
/** Replaces `${port.<name>}` with the chosen host port. */
|
|
320
|
+
export const expandPorts = (value, ports) => value.replaceAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu, (_whole, name) => String(ports[name] ?? ''));
|
|
@@ -535,7 +535,7 @@ const checkStacksManifest = (manifest, model, { stacksRoot, environment, root, r
|
|
|
535
535
|
const lite = repo.edition === 'lite';
|
|
536
536
|
checkStacksTree({ stacksRoot, manifest, environmentNames, add, lite });
|
|
537
537
|
checkKubernetesDeclaration(manifest, add);
|
|
538
|
-
for (const issue of swarmInventoryIssues({ components: manifest.components, lite }))
|
|
538
|
+
for (const issue of swarmInventoryIssues({ components: manifest.components, lite, swarm: new Set(Object.values(object(manifest.environments) ? manifest.environments : {}).filter((spec) => spec?.runtime === 'docker-swarm').flatMap((spec) => Object.keys(object(spec.components) ? spec.components : {}))) }))
|
|
539
539
|
add('HFS_STACKS_SHAPE', issue.at, issue.message);
|
|
540
540
|
checkSelectedEnvironment({ environment, environmentNames, environments, stacksRoot, ids, root, model, manifest, lite, repo, add });
|
|
541
541
|
};
|