@starci/hfs 5.0.8 → 5.2.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 +30 -0
- package/README.md +10 -1
- package/cache/import-graph.mjs +6 -61
- package/cache/lint-family.d.mts +10 -1
- package/cache/lint-family.mjs +12 -5
- package/codemod/directive-first.d.mts +48 -0
- package/codemod/directive-first.mjs +136 -0
- package/codemod/explicit-surface-edit.d.mts +14 -0
- package/codemod/explicit-surface-edit.mjs +57 -0
- package/codemod/explicit-surface-names.d.mts +30 -0
- package/codemod/explicit-surface-names.mjs +135 -0
- package/codemod/explicit-surface-render.d.mts +29 -0
- package/codemod/explicit-surface-render.mjs +197 -0
- package/codemod/explicit-surface.d.mts +50 -0
- package/codemod/explicit-surface.mjs +374 -0
- package/codemod/main.d.mts +7 -5
- package/codemod/main.mjs +37 -16
- package/codemod/source-tree.d.mts +8 -0
- package/codemod/source-tree.mjs +23 -0
- package/db/postgres.mjs +9 -1
- package/lint/eslint-list.cjs +29 -0
- package/lint/run.d.mts +19 -5
- package/lint/run.mjs +47 -14
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +31 -10
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +107 -8
- package/runtime/knowledge/hfs/slots.yaml +75 -24
- package/runtime/modules/kernel/failure-codes.yaml +62 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- 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/explicit-surface-rule.d.mts +58 -0
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- 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/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.mjs +10 -7
- 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/lib/declaration-surface.d.mts +35 -0
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
- package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +49 -0
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.mjs +7 -7
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -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/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 +166 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -0
- package/sync/index.d.mts +13 -12
- package/sync/index.mjs +83 -28
- 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/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -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/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/auth.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -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 +13 -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 +90 -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/modules/domain/identity/account-directory.service.spec.ts +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -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-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 +17 -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 +78 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
- package/templates/be/inbox/entities.index.ts.tpl +1 -1
- package/templates/be/inbox/module-definition.ts.tpl +8 -2
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
- package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
- 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 +10 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +23 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -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 +12 -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/database.port.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
- package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/table/entities.index.ts.tpl +1 -1
- package/templates/be/table/module-definition.ts.tpl +8 -3
- 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 -3
- package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- package/templates/fe/common/app-layout.tsx.partial +1 -1
- 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/useSession.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +18 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +20 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
- 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]/layout.tsx +4 -3
- 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/layouts/AppLayout/index.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
- package/templates/fe/units/page/index.tsx.tpl +3 -2
- package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
- 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 -37
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -36
- 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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { declarationSurface, isEmittableSource } from '../lib/declaration-surface.mjs';
|
|
2
|
+
/** What to write, by what the classifier says to annotate. */
|
|
3
|
+
const HINTS = {
|
|
4
|
+
'return type': 'Write the return type after the parameter list: `(): Result =>` or `function name(): Result`.',
|
|
5
|
+
'variable type': 'Give the variable a type: `export const name: Result = ...`.',
|
|
6
|
+
'parameter type': 'Give the parameter a type: `(value: Input)`.',
|
|
7
|
+
'property type': 'Give the property a type: `name: Result = ...`.',
|
|
8
|
+
'accessor type': 'Give the accessor a type: `get name(): Result` (a setter takes it from its parameter).',
|
|
9
|
+
'destructured export': 'A destructuring pattern cannot carry a type per name: export each name as its own variable with a type, `export const name: Type = source.name`.',
|
|
10
|
+
'exported value': 'Declare the type (an interface or a type alias) and annotate the export with it: `export const name: Shape = ...`.',
|
|
11
|
+
};
|
|
12
|
+
/** The `--isolatedDeclarations` flag name TypeScript ends its reasons with says nothing to the reader. */
|
|
13
|
+
const plain = (reason) => reason.replace(/ with --isolatedDeclarations\.?/, '').replace(/\.$/, '');
|
|
14
|
+
/**
|
|
15
|
+
* The rule. `ts` is the canon's own TypeScript (its peer dependency); the side's `module` option comes from the typed program when the file is linted with one.
|
|
16
|
+
*
|
|
17
|
+
* @param input - `ts` is the compiler.
|
|
18
|
+
* @returns An ESLint rule reporting each export of a TypeScript source whose type is inferred.
|
|
19
|
+
*/
|
|
20
|
+
export const explicitSurfaceRule = ({ ts }) => ({
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: { description: 'An exported declaration states its type: the return type, variable type or parameter type is written, not inferred from a body or from other files.' },
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
inferred: 'HFS_EXPLICIT_EXPORT_SURFACE: `{{name}}` is exported with an inferred type ({{reason}}), so importers cannot read it from this file and every one of them is linted again whenever the file changes. Annotate the {{annotate}}. {{hint}}',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
Program() {
|
|
32
|
+
const file = context.filename;
|
|
33
|
+
if (!isEmittableSource(file))
|
|
34
|
+
return;
|
|
35
|
+
const { sourceCode } = context;
|
|
36
|
+
const module = sourceCode.parserServices?.program?.getCompilerOptions().module;
|
|
37
|
+
const whole = { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
38
|
+
let surface;
|
|
39
|
+
try {
|
|
40
|
+
surface = declarationSurface(ts, file, sourceCode.text, module);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
surface = null;
|
|
44
|
+
}
|
|
45
|
+
if (surface === null) {
|
|
46
|
+
context.report({ loc: whole, messageId: 'inferred', data: { name: 'this file', reason: 'the declaration emit could not read it', annotate: 'exported value', hint: HINTS['exported value'] } });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const problem of surface.problems) {
|
|
50
|
+
context.report({
|
|
51
|
+
loc: { start: sourceCode.getLocFromIndex(problem.start), end: sourceCode.getLocFromIndex(problem.start + problem.length) },
|
|
52
|
+
messageId: 'inferred',
|
|
53
|
+
data: { name: problem.name, reason: plain(problem.reason), annotate: problem.annotate, hint: HINTS[problem.annotate] },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -121,7 +121,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
|
121
121
|
/** The kind of a parsed manifest: `kind`, or app when absent (knowledge/hfs/slots.yaml carries none). */
|
|
122
122
|
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
123
123
|
const strList = (v) => stringList(v);
|
|
124
|
-
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'rewrittenBy', 'logic'];
|
|
124
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy', 'rewrittenBy', 'logic', 'managedOnly'];
|
|
125
125
|
/** The roles a file slot may play that a rule reads (`role`): the entity registry of persistence.md BE-PERSISTENCE-3 (architecture/entity-registry.mjs). */
|
|
126
126
|
const SLOT_ROLES = ['entity-registry'];
|
|
127
127
|
/** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
|
|
@@ -235,6 +235,8 @@ function slotListProblems(slot, at) {
|
|
|
235
235
|
bad.push(`${at}: composedBy must be a non-empty list of unique app kinds`);
|
|
236
236
|
if (slot.budget !== undefined && !budgetValid(slot.budget))
|
|
237
237
|
bad.push(`${at}: budget must map names to positive integers`);
|
|
238
|
+
if (slot.managedOnly !== undefined && (slot.managedOnly !== true || slot.managedBy === undefined))
|
|
239
|
+
bad.push(`${at}: managedOnly is true and needs managedBy: the slot only lists files sync renders, and the slot owning the path judges them`);
|
|
238
240
|
for (const key of ['managedBy', 'liteManagedBy'])
|
|
239
241
|
if (slot[key] !== undefined && !NAME.test(String(slot[key])))
|
|
240
242
|
bad.push(`${at}: ${key} must be a template id`);
|
|
@@ -33,6 +33,8 @@ import { stacksIngressFindings } from './stacks-ingress.mjs';
|
|
|
33
33
|
import { stacksLiteBackingFindings } from './stacks-lite-backing.mjs';
|
|
34
34
|
import { stackToolingFindings } from './stack-tooling.mjs';
|
|
35
35
|
import { stacksDeployFindings } from './stacks-deploy.mjs';
|
|
36
|
+
import { stacksLocalFindings } from './stacks-local.mjs';
|
|
37
|
+
import { stacksServiceImageFindings } from './stacks-service-images.mjs';
|
|
36
38
|
import { stacksAuthCookieDomainFindings } from './stacks-auth-cookie-domain.mjs';
|
|
37
39
|
import { ciCustodyFindings } from './ci-custody.mjs';
|
|
38
40
|
import { workflowContextsFindings } from './workflow-contexts.mjs';
|
|
@@ -49,6 +51,7 @@ export const A15_CODES = [
|
|
|
49
51
|
'HFS_WORKFLOW_CONTEXT',
|
|
50
52
|
'CI_STANDARD',
|
|
51
53
|
'CI_SECRET_SEALED', 'APP_SECRET_IN_CI', 'CI_DECLARATION_DRIFT',
|
|
54
|
+
'STACKS_LOCAL_CONFIG_INVALID', 'STACKS_DEV_UNDECLARED', 'STACKS_RUNBOOK_DOCKER_COMMAND',
|
|
52
55
|
'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BOOT_PROOF_TIMEOUT', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_AUTH_COOKIE_DOMAIN_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_REDIS_PERSISTENCE', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
|
|
53
56
|
// codes-end
|
|
54
57
|
];
|
|
@@ -87,6 +90,8 @@ export const FINDERS = [
|
|
|
87
90
|
stacksLiteBackingFindings,
|
|
88
91
|
stackToolingFindings,
|
|
89
92
|
stacksDeployFindings,
|
|
93
|
+
stacksLocalFindings,
|
|
94
|
+
stacksServiceImageFindings,
|
|
90
95
|
stacksAuthCookieDomainFindings,
|
|
91
96
|
ciCustodyFindings,
|
|
92
97
|
// finders-end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Repo } from './rule-types.mjs';
|
|
2
|
+
/** Findings of the dev environments of the app root. */
|
|
3
|
+
export declare function stacksLocalFindings({ repoRoot, repo, isRoot }: {
|
|
4
|
+
repoRoot: string;
|
|
5
|
+
repo: Repo;
|
|
6
|
+
isRoot: boolean;
|
|
7
|
+
}): import("./rule-types.mjs").Finding[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// stacks-local.mjs - R372 and R373: the dev environment of an app is declared runnable and its runbook names the harness verb.
|
|
2
|
+
// R372 STACKS_LOCAL_CONFIG_INVALID every docker-compose environment carries a `local` block that the dev verbs of `starci app stack`
|
|
3
|
+
// normalise (scripts/hfs/stack-local-config.mjs), and its compose services exist in the compose files;
|
|
4
|
+
// STACKS_DEV_UNDECLARED a lite app declares at least one docker-compose environment (the dev kind of PROJECT-STACKS-3).
|
|
5
|
+
// R373 STACKS_RUNBOOK_DOCKER_COMMAND the runbook of a docker-compose environment names `starci app stack up --env <env>` and carries no
|
|
6
|
+
// `docker build|run|compose|create|start|stop|pull|push` or `docker stack deploy` command (rules.md rule 8).
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
|
|
10
|
+
import { findStackDeclaration, readDeclaration, STACK_ROOT } from '../../lib/stack-declaration.mjs';
|
|
11
|
+
import { normalizeLocal } from '../stack-local-config.mjs';
|
|
12
|
+
import { found, readJson } from './read.mjs';
|
|
13
|
+
const DECLARATION = `${STACK_ROOT}/application-stacks.yaml`;
|
|
14
|
+
const ENV_NAME = /^[a-z0-9][a-z0-9-]*$/u;
|
|
15
|
+
const DOCKER_COMMAND = /^\s*(?:[$>]\s*)?(?:\w+=\S+\s+)*docker(?:-compose)?\s+(?:build|buildx|run|compose|create|start|stop|rm|pull|push|stack\s+deploy|stack\s+rm|service)\b/u;
|
|
16
|
+
const mapping = (value) => (plain(value) ? value : {});
|
|
17
|
+
function composeServices(envDir, spec) {
|
|
18
|
+
const names = new Set();
|
|
19
|
+
for (const relative of Array.isArray(spec.composeFiles) ? spec.composeFiles : []) {
|
|
20
|
+
if (typeof relative !== 'string' || path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..'))
|
|
21
|
+
continue;
|
|
22
|
+
for (const name of Object.keys(mapping(readDeclaration(path.join(envDir, relative)).doc?.services)))
|
|
23
|
+
names.add(name);
|
|
24
|
+
}
|
|
25
|
+
return names;
|
|
26
|
+
}
|
|
27
|
+
function localFindings({ repoRoot, project, environment, spec }) {
|
|
28
|
+
const at = `environments.${environment}.local`;
|
|
29
|
+
if (spec.local === undefined)
|
|
30
|
+
return [found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `environments.${environment} runs docker-compose and declares no local block: declare the ports, services, processes and probes \`starci app stack up --env ${environment}\` runs`)];
|
|
31
|
+
const { config, problems } = normalizeLocal(spec.local, { project, env: environment });
|
|
32
|
+
const findings = problems.map((problem) => found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `${at}.${problem.at}: ${problem.message}`));
|
|
33
|
+
if (config) {
|
|
34
|
+
const services = composeServices(path.join(repoRoot, STACK_ROOT, environment), spec);
|
|
35
|
+
for (const service of config.services)
|
|
36
|
+
if (!services.has(service.name))
|
|
37
|
+
findings.push(found('STACKS_LOCAL_CONFIG_INVALID', DECLARATION, `${at}.services: ${service.name} is not a service of the compose files of ${environment}`));
|
|
38
|
+
}
|
|
39
|
+
return findings;
|
|
40
|
+
}
|
|
41
|
+
function runbookFindings({ repoRoot, environment, spec }) {
|
|
42
|
+
const relative = typeof spec.runbook === 'string' ? spec.runbook : 'README.md';
|
|
43
|
+
const file = path.join(repoRoot, STACK_ROOT, environment, relative);
|
|
44
|
+
const display = path.relative(repoRoot, file).split(path.sep).join('/');
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = fs.readFileSync(file, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const findings = text.split(/\r?\n/u).map((line, index) => ({ line, index })).filter(({ line }) => DOCKER_COMMAND.test(line) || /`docker\s+(?:build|run|compose|create|start|stop|pull|push|stack)\b/u.test(line))
|
|
53
|
+
.map(({ index }) => found('STACKS_RUNBOOK_DOCKER_COMMAND', `${display}:${index + 1}`, `the runbook of ${environment} carries a docker command; Docker is run by the harness: name \`starci app stack up --env ${environment}\` instead`));
|
|
54
|
+
if (!text.includes(`starci app stack up --env ${environment}`))
|
|
55
|
+
findings.push(found('STACKS_RUNBOOK_DOCKER_COMMAND', display, `the runbook of ${environment} does not name \`starci app stack up --env ${environment}\`, the one command that brings the environment up`));
|
|
56
|
+
return findings;
|
|
57
|
+
}
|
|
58
|
+
/** Findings of the dev environments of the app root. */
|
|
59
|
+
export function stacksLocalFindings({ repoRoot, repo, isRoot }) {
|
|
60
|
+
if (repo?.kind !== 'app' || !['lite', 'full'].some((value) => value === repo.edition) || isRoot === false)
|
|
61
|
+
return [];
|
|
62
|
+
const { doc } = findStackDeclaration(repoRoot);
|
|
63
|
+
if (!doc)
|
|
64
|
+
return [];
|
|
65
|
+
const project = String(readJson(repoRoot, 'hfs.json')?.project ?? 'app');
|
|
66
|
+
const judged = Object.entries(mapping(mapping(doc).environments)).filter(([environment, spec]) => ENV_NAME.test(environment) && plain(spec) && spec.runtime === 'docker-compose');
|
|
67
|
+
const findings = judged.flatMap(([environment, spec]) => [...localFindings({ repoRoot, project, environment, spec }), ...runbookFindings({ repoRoot, environment, spec })]);
|
|
68
|
+
if (repo.edition === 'lite' && judged.length === 0)
|
|
69
|
+
findings.push(found('STACKS_DEV_UNDECLARED', DECLARATION, 'a lite app declares its dev environment: an environment with runtime docker-compose and a local block, run by `starci app stack up --env dev` (PROJECT-STACKS-3, PROJECT-STACKS-6)'));
|
|
70
|
+
return findings;
|
|
71
|
+
}
|
|
@@ -99,6 +99,9 @@ function liteEnvironmentFindings({ doc, environments, deployments }) {
|
|
|
99
99
|
for (const [environment, spec] of Object.entries(environments)) {
|
|
100
100
|
if (!Array.isArray(spec?.composeFiles) || !spec.composeFiles.length)
|
|
101
101
|
continue;
|
|
102
|
+
// The dev kind (PROJECT-STACKS-3): docker-compose sources under infra/compose/ with the local block `starci app stack up` runs; judged by R372.
|
|
103
|
+
if (spec.runtime === 'docker-compose' && spec.local !== undefined && spec.composeFiles.every((file) => typeof file === 'string' && file.startsWith('infra/compose/')))
|
|
104
|
+
continue;
|
|
102
105
|
if (spec.runtime !== 'docker-swarm' || spec.composeFiles.some((file) => typeof file !== 'string' || !file.startsWith('infra/swarm/')))
|
|
103
106
|
findings.push(found(CODE, DECLARATION, `${DECLARATION}: environments.${environment} lite delivery must use runtime: docker-swarm and infra/swarm sources`));
|
|
104
107
|
}
|
|
@@ -111,11 +114,13 @@ function undeclaredSwarmFindings(deployments, environments, sources) {
|
|
|
111
114
|
return found(CODE, file, `${file}: Swarm deployment ${reason}`);
|
|
112
115
|
});
|
|
113
116
|
}
|
|
117
|
+
/** The component ids a docker-swarm environment binds: the only ones the lite refusal of backing images judges (a dev compose environment runs its own MinIO, Redis and agent runtime). */
|
|
118
|
+
const swarmBoundComponents = (environments) => new Set(Object.values(environments).filter((spec) => spec?.runtime === 'docker-swarm').flatMap((spec) => Object.keys(mapping(spec.components))));
|
|
114
119
|
/** Declared Swarm sources are checked for both editions; lite additionally refuses local managed backing images. */
|
|
115
120
|
export function stacksRuntimeFindings({ repoRoot, files, doc, edition = 'full' }) {
|
|
116
121
|
const findings = [], lite = edition === 'lite', components = mapping(doc?.components), environments = mapping(doc?.environments);
|
|
117
122
|
const deployments = swarmDeploymentFiles(repoRoot, files), sources = new Set();
|
|
118
|
-
findings.push(...swarmInventoryIssues({ components, lite }).map((issue) => issueFinding(DECLARATION, issue)));
|
|
123
|
+
findings.push(...swarmInventoryIssues({ components, lite, swarm: swarmBoundComponents(environments) }).map((issue) => issueFinding(DECLARATION, issue)));
|
|
119
124
|
if (lite)
|
|
120
125
|
findings.push(...liteEnvironmentFindings({ doc, environments, deployments }));
|
|
121
126
|
for (const [environment, entry] of Object.entries(environments)) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { found } from './read.mjs';
|
|
2
|
+
import type { Repo } from './rule-types.mjs';
|
|
3
|
+
type Finding = ReturnType<typeof found>;
|
|
4
|
+
/** The fork repositories the harness owns, by service name. */
|
|
5
|
+
export declare function forkImages(): Record<string, string>;
|
|
6
|
+
/** Findings of R374 for the app root. */
|
|
7
|
+
export declare function stacksServiceImageFindings({ repoRoot, repo, isRoot }: {
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
repo: Repo;
|
|
10
|
+
isRoot: boolean;
|
|
11
|
+
}): Finding[];
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// stacks-service-images.mjs - R374: a customised service runs from an image the app builds, never from an upstream image with code mounted into it.
|
|
2
|
+
// STACKS_CODE_BIND_MOUNT a stack or compose file bind-mounts code, a script, a plugin or a writable repository path into a service container.
|
|
3
|
+
// Allowed: a named volume, a secret or config grant (not volumes), a bind under the data root (`${data}`, `${STACK_DATA_DIR}`) or a least grant (`${STACK_GRANT_<NAME>}`),
|
|
4
|
+
// the docker socket read-only, and a read-only plain data file (json, yaml, toml, conf, ini, properties, txt, pem, crt, csv).
|
|
5
|
+
// STACKS_IMAGE_BASE_UNPINNED a Dockerfile of `.starcistacks/images/<name>/` has a FROM that is not digest-pinned.
|
|
6
|
+
// STACKS_IMAGE_BASE_NOT_FORK the image folder of a service the harness owns a fork image for (knowledge/hfs/fork-images.yaml) does not end FROM that fork.
|
|
7
|
+
// STACKS_IMAGE_ENV_DIVERGES a service that has an image folder runs a literal third-party image in some environment: every environment runs the
|
|
8
|
+
// image built from the folder (compose `build.context`, or a project-owned or variable image reference), so dev and production share it.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
|
|
12
|
+
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
13
|
+
import { findStackDeclaration, readDeclaration, STACK_ROOT } from '../../lib/stack-declaration.mjs';
|
|
14
|
+
import { found, readJson } from './read.mjs';
|
|
15
|
+
const FORKS = new URL('../../../knowledge/hfs/fork-images.yaml', import.meta.url);
|
|
16
|
+
const ENV_NAME = /^[a-z0-9][a-z0-9-]*$/u;
|
|
17
|
+
const DATA_ROOT = /^(?:\$\{(?:data|STACK_DATA_DIR|STACK_GRANT_[A-Z0-9_]+)\}|\$(?:data|STACK_DATA_DIR|STACK_GRANT_[A-Z0-9_]+))(?:[\\/]|$)/u;
|
|
18
|
+
const DATA_FILE = /\.(?:json|ya?ml|toml|conf|cnf|cfg|ini|properties|txt|pem|crt|csv)$/iu;
|
|
19
|
+
const SOCKET = '/var/run/docker.sock';
|
|
20
|
+
const mapping = (value) => (plain(value) ? value : {});
|
|
21
|
+
/** The fork repositories the harness owns, by service name. */
|
|
22
|
+
export function forkImages() {
|
|
23
|
+
return mapping(mapping(parseYaml(fs.readFileSync(FORKS, 'utf8'))).images);
|
|
24
|
+
}
|
|
25
|
+
/** One volume entry as { source, readOnly } when it is a bind mount (a host path), else null. */
|
|
26
|
+
function bindOf(entry) {
|
|
27
|
+
if (typeof entry === 'string') {
|
|
28
|
+
const [source = '', , mode = ''] = entry.split(':');
|
|
29
|
+
if (!/^(?:\.|\/|~|\$\{|\$[A-Za-z])/u.test(source))
|
|
30
|
+
return null;
|
|
31
|
+
return { source, readOnly: mode.split(',').includes('ro') };
|
|
32
|
+
}
|
|
33
|
+
const long = mapping(entry);
|
|
34
|
+
if (long.type !== 'bind' || typeof long.source !== 'string')
|
|
35
|
+
return null;
|
|
36
|
+
return { source: long.source, readOnly: long.read_only === true };
|
|
37
|
+
}
|
|
38
|
+
/** Whether a bind mount is a data volume or plain read-only config data, the line R374 draws. */
|
|
39
|
+
const allowedBind = ({ source, readOnly }) => DATA_ROOT.test(source) || (readOnly && (source === SOCKET || DATA_FILE.test(source)));
|
|
40
|
+
const baseOf = (reference) => reference.replace(/@sha256:[0-9a-f]+$/iu, '').replace(/:[^/:]+$/u, '');
|
|
41
|
+
function bindFindings(display, environment, services) {
|
|
42
|
+
return Object.entries(services).flatMap(([name, service]) => (Array.isArray(mapping(service).volumes) ? mapping(service).volumes : [])
|
|
43
|
+
.map(bindOf).filter((bind) => bind !== null && !allowedBind(bind))
|
|
44
|
+
.map((bind) => found('STACKS_CODE_BIND_MOUNT', display, `${environment}: service ${name} bind-mounts ${bind.source}; code, plugins and scripts are baked into an image built from .starcistacks/images/${name}/ (FROM the pinned base, or the harness fork image), and only data volumes, secrets and read-only plain config data are mounted`)));
|
|
45
|
+
}
|
|
46
|
+
function dockerfileFindings(repoRoot, forks) {
|
|
47
|
+
const images = path.join(repoRoot, STACK_ROOT, 'images');
|
|
48
|
+
if (!fs.existsSync(images))
|
|
49
|
+
return [];
|
|
50
|
+
return fs.readdirSync(images, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
51
|
+
const display = `${STACK_ROOT}/images/${entry.name}/Dockerfile`;
|
|
52
|
+
let text;
|
|
53
|
+
try {
|
|
54
|
+
text = fs.readFileSync(path.join(images, entry.name, 'Dockerfile'), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const stages = new Set();
|
|
60
|
+
const bases = [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
63
|
+
const from = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/iu.exec(line);
|
|
64
|
+
if (!from)
|
|
65
|
+
continue;
|
|
66
|
+
const ref = from[1];
|
|
67
|
+
if (from[2])
|
|
68
|
+
stages.add(from[2].toLowerCase());
|
|
69
|
+
if (stages.has(ref.toLowerCase()) || ref === 'scratch')
|
|
70
|
+
continue;
|
|
71
|
+
bases.push(ref);
|
|
72
|
+
if (!/@sha256:[0-9a-f]{64}/iu.test(ref))
|
|
73
|
+
findings.push(found('STACKS_IMAGE_BASE_UNPINNED', display, `FROM ${ref} is not pinned by digest (name@sha256:...)`));
|
|
74
|
+
}
|
|
75
|
+
const fork = forks[entry.name];
|
|
76
|
+
const last = bases.at(-1);
|
|
77
|
+
if (fork !== undefined && last !== undefined && baseOf(last) !== fork)
|
|
78
|
+
findings.push(found('STACKS_IMAGE_BASE_NOT_FORK', display, `the harness owns the StarCi fork image ${fork} for ${entry.name}; the final stage must be FROM ${fork}@sha256:..., not ${baseOf(last)}`));
|
|
79
|
+
return findings;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Whether a service's image/build is the one built from its image folder, or a project-owned or variable reference. */
|
|
83
|
+
function builtFromFolder(service, name, project) {
|
|
84
|
+
const build = service.build;
|
|
85
|
+
const context = typeof build === 'string' ? build : typeof mapping(build).context === 'string' ? String(mapping(build).context) : '';
|
|
86
|
+
if (context !== '' && context.replaceAll('\\', '/').includes(`images/${name}`))
|
|
87
|
+
return true;
|
|
88
|
+
const image = typeof service.image === 'string' ? service.image : '';
|
|
89
|
+
return image.includes('${') || image.startsWith(`${project}/`) || image.startsWith(`${project}-`) || image.includes(`/${project}/`);
|
|
90
|
+
}
|
|
91
|
+
/** Findings of R374 for the app root. */
|
|
92
|
+
export function stacksServiceImageFindings({ repoRoot, repo, isRoot }) {
|
|
93
|
+
if (repo?.kind !== 'app' || !['lite', 'full'].some((value) => value === repo.edition) || isRoot === false)
|
|
94
|
+
return [];
|
|
95
|
+
const { doc } = findStackDeclaration(repoRoot);
|
|
96
|
+
if (!doc)
|
|
97
|
+
return [];
|
|
98
|
+
const project = String(readJson(repoRoot, 'hfs.json')?.project ?? 'app');
|
|
99
|
+
const folders = path.join(repoRoot, STACK_ROOT, 'images');
|
|
100
|
+
const withFolder = new Set(fs.existsSync(folders) ? fs.readdirSync(folders, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : []);
|
|
101
|
+
const findings = dockerfileFindings(repoRoot, forkImages());
|
|
102
|
+
for (const [environment, spec] of Object.entries(mapping(mapping(doc).environments))) {
|
|
103
|
+
if (!ENV_NAME.test(environment) || !plain(spec))
|
|
104
|
+
continue;
|
|
105
|
+
const files = Array.isArray(spec.composeFiles) ? spec.composeFiles : [];
|
|
106
|
+
for (const relative of files) {
|
|
107
|
+
if (typeof relative !== 'string' || path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..'))
|
|
108
|
+
continue;
|
|
109
|
+
const file = path.join(repoRoot, STACK_ROOT, environment, relative);
|
|
110
|
+
const display = path.relative(repoRoot, file).split(path.sep).join('/');
|
|
111
|
+
const services = mapping(readDeclaration(file).doc?.services);
|
|
112
|
+
findings.push(...bindFindings(display, environment, services));
|
|
113
|
+
for (const [name, service] of Object.entries(services)) {
|
|
114
|
+
if (withFolder.has(name) && !builtFromFolder(mapping(service), name, project))
|
|
115
|
+
findings.push(found('STACKS_IMAGE_ENV_DIVERGES', display, `${environment}: service ${name} has an image folder but runs ${String(mapping(service).image)}; every environment runs the image built from .starcistacks/images/${name}/ so dev and production share one image`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SecretSops } from "./secret-sops.mjs";
|
|
2
|
+
import type { CustodyGenerator } from "./stack-local-config.mjs";
|
|
3
|
+
/** One generated value (or a private/public pair) by its kind; the `password` kind is URL- and SQL-safe. */
|
|
4
|
+
export declare function generateValues(entry: CustodyGenerator, random?: (bytes: number) => Buffer): Record<string, string>;
|
|
5
|
+
/** Seals the missing dev values of `environment`; a dry run unless `execute`. Returns the exit code. */
|
|
6
|
+
export declare function seedDevCustody(repoRoot: string, environment: string, { execute, sops, stdout, random }: {
|
|
7
|
+
execute: boolean;
|
|
8
|
+
sops: () => SecretSops;
|
|
9
|
+
stdout: (text: string) => unknown;
|
|
10
|
+
random?: (bytes: number) => Buffer;
|
|
11
|
+
}): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// secret-dev-init.mjs - `starci app secret init --env <dev env> [--execute]`: creates the dev-only values the environment's `local.custody`
|
|
2
|
+
// declares (PROJECT-STACKS-5: sealed only, owner-armed, created once, never replaced). It refuses an environment that is not a docker-compose
|
|
3
|
+
// dev environment (vps and production never get generated values), skips a key its member already holds, and refuses a half-present pair
|
|
4
|
+
// instead of minting a replacement. Values are generated in memory and sealed in one write per member; names are all it prints.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { generateKeyPairSync, randomBytes, randomUUID } from "node:crypto";
|
|
7
|
+
import { envelopeOf } from "./secret-document.mjs";
|
|
8
|
+
import { SecretError } from "./secret-errors.mjs";
|
|
9
|
+
import { fileOf, secretsDirectory, writeSecret } from "./secret-store.mjs";
|
|
10
|
+
import { declareKeys } from "./secret-standard.mjs";
|
|
11
|
+
import { isLocalEnvironment, loadLocalConfig } from "../../../stack/local-config.mjs";
|
|
12
|
+
/** One generated value (or a private/public pair) by its kind; the `password` kind is URL- and SQL-safe. */
|
|
13
|
+
export function generateValues(entry, random = randomBytes) {
|
|
14
|
+
if (entry.kind === "hex")
|
|
15
|
+
return { [entry.key]: random(entry.bytes).toString("hex") };
|
|
16
|
+
if (entry.kind === "base64")
|
|
17
|
+
return { [entry.key]: random(entry.bytes).toString("base64") };
|
|
18
|
+
if (entry.kind === "password")
|
|
19
|
+
return { [entry.key]: random(entry.bytes).toString("base64url") };
|
|
20
|
+
if (entry.kind === "uuid")
|
|
21
|
+
return { [entry.key]: randomUUID() };
|
|
22
|
+
if (entry.kind === "rsa-pem" || entry.kind === "ec-pem" || entry.kind === "ed25519-pem") {
|
|
23
|
+
const pair = entry.kind === "rsa-pem"
|
|
24
|
+
? generateKeyPairSync("rsa", { modulusLength: 2048, privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } })
|
|
25
|
+
: entry.kind === "ec-pem"
|
|
26
|
+
? generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } })
|
|
27
|
+
: generateKeyPairSync("ed25519", { privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } });
|
|
28
|
+
return { [entry.key]: pair.privateKey, ...(entry.publicKey ? { [entry.publicKey]: pair.publicKey } : {}) };
|
|
29
|
+
}
|
|
30
|
+
throw new SecretError(`the generator ${entry.kind} is unknown`);
|
|
31
|
+
}
|
|
32
|
+
/** Seals the missing dev values of `environment`; a dry run unless `execute`. Returns the exit code. */
|
|
33
|
+
export function seedDevCustody(repoRoot, environment, { execute, sops, stdout, random = randomBytes }) {
|
|
34
|
+
if (!isLocalEnvironment(repoRoot, environment))
|
|
35
|
+
throw new SecretError(`secret init --env ${environment}: only a docker-compose dev environment gets generated values; a vps or production value is set by the owner with secret set (PROJECT-STACKS-5)`);
|
|
36
|
+
const local = loadLocalConfig({ repoRoot, env: environment });
|
|
37
|
+
if (local.custody.length === 0)
|
|
38
|
+
throw new SecretError(`environment ${environment} declares no local.custody: nothing to generate; the owner seals its values with secret set`);
|
|
39
|
+
const directory = secretsDirectory(repoRoot, environment);
|
|
40
|
+
const byMember = new Map();
|
|
41
|
+
for (const entry of local.custody)
|
|
42
|
+
byMember.set(entry.member, [...(byMember.get(entry.member) ?? []), entry]);
|
|
43
|
+
const writes = [];
|
|
44
|
+
for (const [member, entries] of byMember) {
|
|
45
|
+
const file = fileOf(directory, member);
|
|
46
|
+
const held = fs.existsSync(file) ? envelopeOf(fs.readFileSync(file, "utf8")).keys : [];
|
|
47
|
+
const values = {};
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const names = [entry.key, ...(entry.publicKey ? [entry.publicKey] : [])];
|
|
50
|
+
const have = names.filter((name) => held.includes(name));
|
|
51
|
+
if (have.length === names.length) {
|
|
52
|
+
stdout(`kept ${environment}/${member} ${entry.key} (already sealed, never replaced)\n`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (have.length > 0)
|
|
56
|
+
throw new SecretError(`${environment}/${member} holds ${have.join(", ")} but not the rest of its pair ${names.join(", ")}: a replacement is never minted for an existing sealed value; the owner removes the pair with secret unset and runs init again`);
|
|
57
|
+
Object.assign(values, generateValues(entry, random));
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(values).length)
|
|
60
|
+
writes.push({ file, member, values });
|
|
61
|
+
}
|
|
62
|
+
for (const write of writes) {
|
|
63
|
+
const names = Object.keys(write.values);
|
|
64
|
+
if (!execute) {
|
|
65
|
+
stdout(`would seal ${environment}/${write.member} (${names.join(", ")})\n`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
writeSecret({ file: write.file, values: write.values, age: [], sops: sops() });
|
|
69
|
+
stdout(`sealed ${environment}/${write.member} (${names.join(", ")})\n`);
|
|
70
|
+
const declaration = declareKeys(repoRoot, environment, write.member, names);
|
|
71
|
+
if (declaration.declared.length)
|
|
72
|
+
stdout(`declared ${declaration.declared.join(", ")} in environments.${environment}.secrets of application-stacks.yaml\n`);
|
|
73
|
+
}
|
|
74
|
+
if (writes.length === 0)
|
|
75
|
+
stdout("nothing to create: every declared dev value is sealed\n");
|
|
76
|
+
else if (!execute)
|
|
77
|
+
stdout("dry run: nothing was written; arm it with --execute (the owner's yes)\n");
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -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],
|
|
@@ -253,10 +254,12 @@ export async function secretMain(argv, { stdout = (s) => process.stdout.write(s)
|
|
|
253
254
|
const [verb, ...rest] = argv;
|
|
254
255
|
try {
|
|
255
256
|
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]");
|
|
257
|
+
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
258
|
const opts = parse(rest);
|
|
258
259
|
optionsFor(verb, opts);
|
|
259
260
|
const repoRoot = path.resolve(cwd, opts.cwd ?? ".");
|
|
261
|
+
if (verb === "init" && opts.env !== undefined)
|
|
262
|
+
return seedDevCustody(repoRoot, opts.env, { execute: opts.execute === true, sops: () => sops ?? defaultSops(env, repoRoot, platform, tools, hostIdentity), stdout, random });
|
|
260
263
|
if (verb === "init")
|
|
261
264
|
return publicStorage(() => initializeSecrets(repoRoot, { env, platform, tools, stdout, host: hostIdentity }));
|
|
262
265
|
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;
|