@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
|
@@ -1,57 +1,17 @@
|
|
|
1
|
-
// identity.mjs - the back-end identity machine. Under lite the one token verifier is be.integrations.passport:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
|
|
1
|
+
// identity.mjs - the back-end identity machine. Under lite the one token verifier is @starci/auth, composed by be.integrations.passport:
|
|
2
|
+
// no app file calls jose's jwtVerify, decodes a token unverified or reads authorization claims from a request body (R329), and the
|
|
3
|
+
// passport integration imports @starci/auth/server. In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { machineKit } from './machine-ast.mjs';
|
|
7
|
-
import {
|
|
6
|
+
import { jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
|
|
8
7
|
import { accessName } from '../../lib/ts-ast.mjs';
|
|
9
8
|
const PASSPORT_SLOT = 'be.integrations.passport';
|
|
9
|
+
const PACKAGE_ENTRY = '@starci/auth/server';
|
|
10
10
|
const BE_JWT_VERIFIED = 'BE_AUTH_JWT_OWN_JWKS';
|
|
11
11
|
/** The rule ids the back-end identity machine reports. */
|
|
12
12
|
export const IDENTITY_RULE_IDS = Object.freeze(['BE_AUTH_JWT_OWN_JWKS', 'BE_JWT_ALG_PINNED']);
|
|
13
13
|
const JWT_MODULES = new Set(['jose', 'jsonwebtoken', 'jwt-decode']);
|
|
14
14
|
const CLAIM_NAMES = new Set(['role', 'roles', 'membership', 'memberships']);
|
|
15
|
-
const objectLiteral = (kit, checker, node) => {
|
|
16
|
-
const current = unwrap(kit.ts, node);
|
|
17
|
-
if (!current)
|
|
18
|
-
return null;
|
|
19
|
-
if (kit.ts.isObjectLiteralExpression(current))
|
|
20
|
-
return current;
|
|
21
|
-
if (!kit.ts.isIdentifier(current))
|
|
22
|
-
return null;
|
|
23
|
-
for (const declaration of kit.declarationsOf(checker, current)) {
|
|
24
|
-
if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
25
|
-
const value = unwrap(kit.ts, declaration.initializer);
|
|
26
|
-
if (kit.ts.isObjectLiteralExpression(value))
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
};
|
|
32
|
-
const staticStrings = (kit, checker, node) => {
|
|
33
|
-
const current = unwrap(kit.ts, node);
|
|
34
|
-
if (kit.ts.isStringLiteralLike(current))
|
|
35
|
-
return [current.text];
|
|
36
|
-
if (kit.ts.isArrayLiteralExpression(current)) {
|
|
37
|
-
const literals = current.elements.filter((element) => kit.ts.isStringLiteralLike(element));
|
|
38
|
-
if (literals.length === current.elements.length)
|
|
39
|
-
return literals.map((element) => element.text);
|
|
40
|
-
}
|
|
41
|
-
const value = kit.stringValue(checker, current);
|
|
42
|
-
return value === null ? null : [value];
|
|
43
|
-
};
|
|
44
|
-
const jwksArgument = (kit, checker, node) => {
|
|
45
|
-
const current = unwrap(kit.ts, node);
|
|
46
|
-
if (!current)
|
|
47
|
-
return false;
|
|
48
|
-
if (kit.ts.isCallExpression(current))
|
|
49
|
-
return importedFrom(kit, checker, current.expression, new Set(['jose']), new Set(['createLocalJWKSet']));
|
|
50
|
-
if (!kit.ts.isIdentifier(current))
|
|
51
|
-
return false;
|
|
52
|
-
return kit.declarationsOf(checker, current).some((declaration) => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
53
|
-
&& jwksArgument(kit, checker, declaration.initializer));
|
|
54
|
-
};
|
|
55
15
|
const importedCall = (kit, checker, call) => {
|
|
56
16
|
const direct = kit.importBinding(checker, call.expression);
|
|
57
17
|
if (direct?.module === 'jwt-decode' && direct.name === 'default')
|
|
@@ -84,41 +44,16 @@ const fromRequestBody = (kit, checker, node, seen = new Set()) => {
|
|
|
84
44
|
return kit.declarationsOf(checker, current).some(declaration => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
85
45
|
&& fromRequestBody(kit, checker, declaration.initializer, seen));
|
|
86
46
|
};
|
|
87
|
-
function verifyJwtCall(kit, checker, file, node, counters, violations) {
|
|
88
|
-
counters.verifyCalls += 1;
|
|
89
|
-
if (file.slot !== PASSPORT_SLOT) {
|
|
90
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${PASSPORT_SLOT}; the integration is the one token verifier.`);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
const options = objectLiteral(kit, checker, node.arguments[2]);
|
|
94
|
-
const property = (name) => options ? kit.propertyOf(options, name) : null;
|
|
95
|
-
const values = (name) => {
|
|
96
|
-
const entry = property(name);
|
|
97
|
-
return entry ? staticStrings(kit, checker, kit.valueOfProperty(entry)) : null;
|
|
98
|
-
};
|
|
99
|
-
const issuer = property('issuer');
|
|
100
|
-
const audience = values('audience');
|
|
101
|
-
const algorithms = values('algorithms');
|
|
102
|
-
// The audience is pinned by a literal or, as R260 accepts, by typed configuration (the skeleton's TokenOptions.audience).
|
|
103
|
-
const audienceEntry = property('audience');
|
|
104
|
-
const pinnedAudience = (audience !== null && audience.length > 0)
|
|
105
|
-
|| (audienceEntry != null && fromConfig(kit.ts, checker, kit.valueOfProperty(audienceEntry)));
|
|
106
|
-
const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && pinnedAudience
|
|
107
|
-
&& algorithms !== null && algorithms.length > 0;
|
|
108
|
-
if (!good) {
|
|
109
|
-
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify must check the app\'s own key set (createLocalJWKSet(...) over the keys integrations/passport signs with) and pin issuer, audience and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
|
|
110
|
-
}
|
|
111
|
-
else
|
|
112
|
-
counters.verifiedCalls += 1;
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
47
|
function inspectJwtCall(kit, checker, file, node, counters, violations) {
|
|
116
48
|
const binding = importedCall(kit, checker, node);
|
|
117
49
|
if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
|
|
118
50
|
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; verify it through integrations/passport.`, { module: binding.module, method: binding.name });
|
|
119
51
|
}
|
|
120
|
-
if (binding?.module === 'jose' && binding.name === 'jwtVerify')
|
|
121
|
-
|
|
52
|
+
if (binding?.module === 'jose' && binding.name === 'jwtVerify') {
|
|
53
|
+
counters.verifyCalls += 1;
|
|
54
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify is called in the app; @starci/auth verifies the own access tokens, reached through ' + PASSPORT_SLOT + '.');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
122
57
|
return false;
|
|
123
58
|
}
|
|
124
59
|
function inspectRequestClaims(kit, file, node, checker, violations) {
|
|
@@ -146,18 +81,18 @@ function checkOwnJwks(input) {
|
|
|
146
81
|
const violations = [];
|
|
147
82
|
const integrations = [...input.graph.files.values()].filter((file) => file.slot === PASSPORT_SLOT);
|
|
148
83
|
if (!integrations.length)
|
|
149
|
-
return { violations, coverage: { status: 'not-applicable', integrationFiles: 0,
|
|
150
|
-
const counters = { verifyCalls: 0
|
|
84
|
+
return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifyCalls: 0 } };
|
|
85
|
+
const counters = { verifyCalls: 0 };
|
|
151
86
|
for (const file of input.graph.files.values()) {
|
|
152
87
|
const checker = kit.checkerOf(file.sourceFile);
|
|
153
88
|
kit.walk(file.sourceFile, (node) => inspectBackendNode(kit, checker, file, node, counters, violations));
|
|
154
89
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls,
|
|
90
|
+
const composes = integrations.some((file) => file.sourceFile.statements.some((statement) => kit.ts.isImportDeclaration(statement)
|
|
91
|
+
&& kit.ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === PACKAGE_ENTRY));
|
|
92
|
+
const anchor = integrations.find((file) => path.posix.basename(file.rel) === 'index.ts') ?? integrations[0];
|
|
93
|
+
if (!composes && anchor)
|
|
94
|
+
reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, PASSPORT_SLOT + ' does not import ' + PACKAGE_ENTRY + ', the one verifier of the own access tokens.');
|
|
95
|
+
return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, composesPackage: composes } };
|
|
161
96
|
}
|
|
162
97
|
/** R260 BE_JWT_ALG_PINNED: every jose jwtVerify call pins a literal secure algorithm list and a configured issuer, and binds the principal by a configured audience or the authenticated role. */
|
|
163
98
|
export function checkJwtPolicy(input) {
|
|
@@ -9,7 +9,7 @@ import type { MachineContext } from './machine-ast.mjs';
|
|
|
9
9
|
*
|
|
10
10
|
* - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
|
|
11
11
|
* not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
|
|
12
|
-
* `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
|
|
12
|
+
* `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use or a `defineConfigurableModule`/`defineCapabilityModule` call of `@starci/nest-kit`) in a feature file that is neither the application module, the
|
|
13
13
|
* transport module of its own folder nor that module's definition are refused;
|
|
14
14
|
* - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
|
|
15
15
|
* in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
|
|
@@ -11,7 +11,7 @@ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
|
11
11
|
*
|
|
12
12
|
* - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
|
|
13
13
|
* not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
|
|
14
|
-
* `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
|
|
14
|
+
* `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use or a `defineConfigurableModule`/`defineCapabilityModule` call of `@starci/nest-kit`) in a feature file that is neither the application module, the
|
|
15
15
|
* transport module of its own folder nor that module's definition are refused;
|
|
16
16
|
* - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
|
|
17
17
|
* in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
|
|
@@ -99,6 +99,8 @@ function reportModuleClassLocation(file, name, place, composedModuleOf, report)
|
|
|
99
99
|
if (file.rel !== own)
|
|
100
100
|
report(file, name, `${name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
|
|
101
101
|
}
|
|
102
|
+
/** The module-definition helpers of @starci/nest-kit: each builds the definition of one module, like `ConfigurableModuleBuilder`. */
|
|
103
|
+
const DEFINITION_HELPERS = ['defineConfigurableModule', 'defineCapabilityModule'];
|
|
102
104
|
function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
|
|
103
105
|
const own = ownModuleOf(place, file.rel, env.composedModuleOf);
|
|
104
106
|
const isDefinition = file.rel === definitionOf(own);
|
|
@@ -106,6 +108,9 @@ function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
|
|
|
106
108
|
if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common') && !isDefinition) {
|
|
107
109
|
env.report(file, node, `ConfigurableModuleBuilder builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
|
|
108
110
|
}
|
|
111
|
+
else if (ts.isCallExpression(node) && !isDefinition && DEFINITION_HELPERS.some(name => kit.isImportOf(checker, node.expression, name, '@starci/nest-kit'))) {
|
|
112
|
+
env.report(file, node, `${node.expression.getText(file.sourceFile)} builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
|
|
113
|
+
}
|
|
109
114
|
else if (ts.isHeritageClause(node) && file.rel !== own) {
|
|
110
115
|
for (const type of node.types)
|
|
111
116
|
if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A workspace package of the app root: a first-party package that both sides import through its package entry points and never past
|
|
3
|
+
* them. `root` is the real path of its folder, `relative` the folder from the app root, `exports` the `exports` of its manifest.
|
|
4
|
+
*/
|
|
5
|
+
export interface RootPackage {
|
|
6
|
+
root: string;
|
|
7
|
+
relative: string;
|
|
8
|
+
name: string;
|
|
9
|
+
exports: unknown;
|
|
10
|
+
manifest: Record<string, unknown>;
|
|
11
|
+
workspace: true;
|
|
12
|
+
app: false;
|
|
13
|
+
}
|
|
14
|
+
/** The slots whose directories at the app root are first-party packages of the app (agent SDK, module packages, the managed registry). */
|
|
15
|
+
export declare const ROOT_PACKAGE_SLOTS: ReadonlySet<string>;
|
|
16
|
+
/** A package folder with a regular package.json (a link is not one). */
|
|
17
|
+
export declare function packageDirectory(absolute: string): boolean;
|
|
18
|
+
/** The folders the `workspaces` of the app root's package.json name that no side owns, as absolute paths in code-unit order. */
|
|
19
|
+
export declare function rootWorkspaceDirectories(packageRoot: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* The record of one package folder of the app root, or null when it is no named package inside the app root.
|
|
22
|
+
* `canonical` is the file-identity function of the caller (the real path), so a record agrees with the files the graph reads.
|
|
23
|
+
*/
|
|
24
|
+
export declare function rootPackageRecord(packageRoot: string, directory: string, canonical: (file: string) => string): RootPackage | null;
|
|
25
|
+
/** The names of the workspace packages of the app root: what the back-end canon must not read as third-party libraries. */
|
|
26
|
+
export declare function rootPackageNames(packageRoot: string): Set<string>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// root-packages.mjs - the workspace packages of an app ROOT (packages/agent-sdk, packages/module-<name>, the managed packages/modules).
|
|
2
|
+
// They lie outside both side folders and belong to no side; both sides import them by package name, through their package entry points
|
|
3
|
+
// (the `exports` of the manifest), so the import graph reads them as first-party packages and the back-end canon never reads them as
|
|
4
|
+
// a third-party library. The app root's one package.json lists them in `workspaces`; the patterns that start with a side folder
|
|
5
|
+
// (`fe/apps/*`) are that side's own workspaces and are not read here.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readJsonFile as readJson } from '../../lib/json.mjs';
|
|
9
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
10
|
+
import { isInside } from '../../lib/walk.mjs';
|
|
11
|
+
import { slash } from '../../lib/path-key.mjs';
|
|
12
|
+
/** The slots whose directories at the app root are first-party packages of the app (agent SDK, module packages, the managed registry). */
|
|
13
|
+
export const ROOT_PACKAGE_SLOTS = new Set(['app.packages', 'app.module-registry']);
|
|
14
|
+
const SIDE_PREFIX = /^(?:be|fe)\//;
|
|
15
|
+
/** A workspace pattern this reader expands (a literal path, or `*` as a whole segment); the others are judged by the workspace rules. */
|
|
16
|
+
const expandablePattern = (written) => !path.isAbsolute(written) && written.split('/').every(segment => segment !== '..' && (!segment.includes('*') || segment === '*'));
|
|
17
|
+
function expandSegment(base, segment) {
|
|
18
|
+
if (segment !== '*')
|
|
19
|
+
return [path.join(base, segment)];
|
|
20
|
+
if (!fs.existsSync(base) || !fs.lstatSync(base).isDirectory())
|
|
21
|
+
return [];
|
|
22
|
+
return fs.readdirSync(base, { withFileTypes: true }).filter(item => item.isDirectory()).sort((a, b) => byCodeUnit(a.name, b.name)).map(item => path.join(base, item.name));
|
|
23
|
+
}
|
|
24
|
+
/** A package folder with a regular package.json (a link is not one). */
|
|
25
|
+
export function packageDirectory(absolute) {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.lstatSync(absolute).isDirectory())
|
|
28
|
+
return false;
|
|
29
|
+
const manifest = fs.lstatSync(path.join(absolute, 'package.json'));
|
|
30
|
+
return manifest.isFile() && !manifest.isSymbolicLink();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The folders the `workspaces` of the app root's package.json name that no side owns, as absolute paths in code-unit order. */
|
|
37
|
+
export function rootWorkspaceDirectories(packageRoot) {
|
|
38
|
+
const manifest = readJson(path.join(packageRoot, 'package.json'));
|
|
39
|
+
const declared = manifest?.workspaces;
|
|
40
|
+
const patterns = Array.isArray(declared) ? declared : declared?.packages;
|
|
41
|
+
const found = new Set();
|
|
42
|
+
for (const pattern of Array.isArray(patterns) ? patterns : []) {
|
|
43
|
+
if (typeof pattern !== 'string' || !pattern.trim())
|
|
44
|
+
continue;
|
|
45
|
+
const written = slash(pattern.trim()).replace(/^\.\//, '');
|
|
46
|
+
if (SIDE_PREFIX.test(written) || !expandablePattern(written))
|
|
47
|
+
continue;
|
|
48
|
+
let bases = [packageRoot];
|
|
49
|
+
for (const segment of written.split('/'))
|
|
50
|
+
bases = bases.flatMap(base => expandSegment(base, segment));
|
|
51
|
+
for (const base of bases)
|
|
52
|
+
found.add(path.resolve(base));
|
|
53
|
+
}
|
|
54
|
+
return [...found].sort(byCodeUnit);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The record of one package folder of the app root, or null when it is no named package inside the app root.
|
|
58
|
+
* `canonical` is the file-identity function of the caller (the real path), so a record agrees with the files the graph reads.
|
|
59
|
+
*/
|
|
60
|
+
export function rootPackageRecord(packageRoot, directory, canonical) {
|
|
61
|
+
if (!packageDirectory(directory) || !isInside(packageRoot, directory))
|
|
62
|
+
return null;
|
|
63
|
+
const manifest = readJson(path.join(directory, 'package.json'));
|
|
64
|
+
const name = manifest?.['name'];
|
|
65
|
+
if (manifest === null || typeof name !== 'string' || !name)
|
|
66
|
+
return null;
|
|
67
|
+
return { root: canonical(directory), relative: slash(path.relative(packageRoot, directory)), name, exports: manifest['exports'], manifest, workspace: true, app: false };
|
|
68
|
+
}
|
|
69
|
+
/** The names of the workspace packages of the app root: what the back-end canon must not read as third-party libraries. */
|
|
70
|
+
export function rootPackageNames(packageRoot) {
|
|
71
|
+
const names = new Set();
|
|
72
|
+
for (const directory of rootWorkspaceDirectories(packageRoot)) {
|
|
73
|
+
const record = rootPackageRecord(packageRoot, directory, (file) => file);
|
|
74
|
+
if (record !== null)
|
|
75
|
+
names.add(record.name);
|
|
76
|
+
}
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
@@ -15,8 +15,7 @@ import type { ArchitectureViolation, CheckInput } from './hfs-graph.mjs';
|
|
|
15
15
|
* - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
|
|
16
16
|
* reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
|
|
17
17
|
* client at all every such reference is a finding that says the repository has no client;
|
|
18
|
-
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls
|
|
19
|
-
* (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
|
|
18
|
+
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
|
|
20
19
|
* second transport wherever it is imported, by static import, `import()` or `require()`;
|
|
21
20
|
* - an `ApolloClient` is constructed only inside the client folder;
|
|
22
21
|
* - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
|
|
@@ -17,8 +17,7 @@ import { propertyValue } from './function-spec-providers.mjs';
|
|
|
17
17
|
* - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
|
|
18
18
|
* reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
|
|
19
19
|
* client at all every such reference is a finding that says the repository has no client;
|
|
20
|
-
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls
|
|
21
|
-
* (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
|
|
20
|
+
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
|
|
22
21
|
* second transport wherever it is imported, by static import, `import()` or `require()`;
|
|
23
22
|
* - an `ApolloClient` is constructed only inside the client folder;
|
|
24
23
|
* - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
|
|
@@ -105,17 +104,15 @@ function isApolloClientConstruction(ts, kit, checker, node) {
|
|
|
105
104
|
const binding = kit.importBinding(checker, node.expression);
|
|
106
105
|
return Boolean(binding?.name === 'ApolloClient' && binding.module.startsWith('@apollo/client'));
|
|
107
106
|
}
|
|
108
|
-
/** The one auth door of the session issuer's client (`modules/auth/auth-door.ts`, slot fe.modules.auth): it calls the app's own auth doors with axios. */
|
|
109
|
-
const isAuthDoor = (file) => file.slot === 'fe.modules.auth' && path.posix.basename(file.rel) === 'auth-door.ts';
|
|
110
107
|
function inspectLibraryImport({ ts, file, node, transport, report }) {
|
|
111
108
|
const specifier = moduleSpecifierOf(ts, node);
|
|
112
109
|
if (!specifier || !ts.isStringLiteralLike(specifier))
|
|
113
110
|
return;
|
|
114
111
|
const library = specifier.text.split('/')[0];
|
|
115
|
-
if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot)
|
|
112
|
+
if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot))))
|
|
116
113
|
return;
|
|
117
114
|
const where = library === REST_LIBRARY
|
|
118
|
-
? 'axios belongs to the api owner (modules/api/rest, the api package)
|
|
115
|
+
? 'axios belongs to the api owner (modules/api/rest, the api package) only: call their functions'
|
|
119
116
|
: `the transport is Apollo GraphQL (${transport}) with axios for the rest calls of modules/api/rest`;
|
|
120
117
|
report(file, node, `${specifier.text} is a second HTTP transport in ${file.rel}; ${where}.`, { library: specifier.text });
|
|
121
118
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CompilerOptions, CreateProgramOptions, Diagnostic, ModuleResolutionCache, ModuleResolutionHost, Node, Program, ProjectReference, SourceFile, TypeChecker } from 'typescript';
|
|
2
|
-
import type { ArchitectureConfig, TypeScriptApi } from './config.mjs';
|
|
2
|
+
import type { ArchitectureConfig, RootPackage, TypeScriptApi } from './config.mjs';
|
|
3
3
|
import type { ProjectRead } from '../typescript-programs.mjs';
|
|
4
4
|
import type { ArchitectureError, ImportEdge, LoadedTypeScript, ModuleReferences, WorkspaceRecord } from './typescript.mjs';
|
|
5
5
|
/** One parsed tsconfig with its compiler program. */
|
|
@@ -56,5 +56,8 @@ export interface TypeContextHelpers {
|
|
|
56
56
|
relativePath(root: string, fileName: string): string;
|
|
57
57
|
packageExported(ts: TypeScriptApi, workspace: WorkspaceRecord, specifier: string, actualTarget: string): boolean;
|
|
58
58
|
boundaryViolation(root: string, edge: ImportEdge, fromWorkspace: WorkspaceRecord | null, toWorkspace: WorkspaceRecord | null, ruleId: string, message: string): ArchitectureError;
|
|
59
|
+
rootPackageOf(config: Pick<ArchitectureConfig, 'rootPackages'>, specifier: string): RootPackage | null;
|
|
60
|
+
rootPackageEntryDeclared(item: RootPackage, specifier: string): boolean;
|
|
61
|
+
rootPackageEntries(item: RootPackage): string[];
|
|
59
62
|
}
|
|
60
63
|
export declare function contextFromProjects(config: ArchitectureConfig, loaded: LoadedTypeScript, paths: string[], helpers: TypeContextHelpers): TypeContext;
|
|
@@ -241,8 +241,38 @@ function appendEdge(from, sourceFile, project, owningWorkspace, reference, actua
|
|
|
241
241
|
errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_EXPORT_BYPASS', 'Cross-package imports must use the target package name and a declared package export.'));
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* An import of one of the app's root workspace packages (`@scope/agent-sdk`, `@scope/agent-sdk/workbench`). The package lies outside the side
|
|
246
|
+
* folder, yet it is first-party source both sides may import: through its package entry points (the `exports` of its manifest) and never past
|
|
247
|
+
* them. An entry point is no import that leaves the side and adds no edge (the package is not part of this side's graph; the package's own
|
|
248
|
+
* lint judges its files); a path into the package's source is refused. Returns true when the reference was settled here.
|
|
249
|
+
*/
|
|
250
|
+
function rootPackageReference(sourceFile, project, reference, state) {
|
|
251
|
+
const { config, errors, helpers, host, ts } = state;
|
|
252
|
+
if (!config.rootPackages?.length)
|
|
253
|
+
return false;
|
|
254
|
+
const item = helpers.rootPackageOf(config, reference.specifier);
|
|
255
|
+
if (item === null)
|
|
256
|
+
return false;
|
|
257
|
+
if (!helpers.rootPackageEntryDeclared(item, reference.specifier)) {
|
|
258
|
+
errors.push({
|
|
259
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
|
|
260
|
+
project: project.relative,
|
|
261
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
262
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
263
|
+
specifier: reference.specifier,
|
|
264
|
+
message: `Internal import ${reference.specifier} reaches into the package ${item.name} past its package entry points; import ${helpers.rootPackageEntries(item).join(', ')} instead.`,
|
|
265
|
+
});
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
// An entry point that a path alias sends outside the package is no entry point: the ordinary boundary judges it.
|
|
269
|
+
const resolved = helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host, resolutionCache(state, project));
|
|
270
|
+
return resolved === undefined || helpers.isInside(item.root, helpers.canonical(resolved));
|
|
271
|
+
}
|
|
244
272
|
function processReference(from, sourceFile, project, owningWorkspace, reference, state) {
|
|
245
273
|
const { config, ts, workspaceNames, workspaces, host, errors, helpers } = state;
|
|
274
|
+
if (rootPackageReference(sourceFile, project, reference, state))
|
|
275
|
+
return;
|
|
246
276
|
// A workspace package is read at its source, never at its build: the same import resolves whether or not dist exists.
|
|
247
277
|
const workspaceTarget = workspaces.filter(item => item.root !== helpers.canonical(config.root)).map(item => helpers.workspaceSourceEntry(ts, item, reference.specifier)).find(Boolean);
|
|
248
278
|
const resolvedName = workspaceTarget ?? helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host, resolutionCache(state, project));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TypeContext } from './type-context.mjs';
|
|
2
2
|
import type { Node, SourceFile, TypeChecker } from 'typescript';
|
|
3
|
-
import type { ArchitectureConfig, TypeScriptApi } from './config.mjs';
|
|
3
|
+
import type { ArchitectureConfig, RootPackage, TypeScriptApi } from './config.mjs';
|
|
4
4
|
export { unwrapExpression } from '../../lib/ts-analysis.mjs';
|
|
5
5
|
/** The target repository's loaded TypeScript: the API, where it resolved from and its version. */
|
|
6
6
|
export interface LoadedTypeScript {
|
|
@@ -82,6 +82,12 @@ export declare function exportTargetStrings(value: unknown): string[];
|
|
|
82
82
|
export declare function workspaceSourceEntry(ts: TypeScriptApi, workspace: WorkspaceRecord, specifier: string): string | null;
|
|
83
83
|
/** Every source file the package.json `exports` of a workspace package maps (each non-wildcard subpath, dist back to source): its public entries. */
|
|
84
84
|
export declare function workspaceExportSources(ts: TypeScriptApi, workspace: WorkspaceRecord): string[];
|
|
85
|
+
/** The root workspace package of the app (`packages/agent-sdk`) a specifier names, its name or a subpath of it; the longest name when several match. */
|
|
86
|
+
export declare function rootPackageOf(config: Pick<ArchitectureConfig, 'rootPackages'>, specifier: string): RootPackage | null;
|
|
87
|
+
/** Whether a specifier into a root package names one of its entry points: a subpath its `exports` declare, or its root through `exports`, `main`/`types` or `src/index`. A path into its source is not an entry point. */
|
|
88
|
+
export declare function rootPackageEntryDeclared(item: RootPackage, specifier: string): boolean;
|
|
89
|
+
/** The subpaths a root package declares for import (`.` first), for the message of a refused deep import. */
|
|
90
|
+
export declare function rootPackageEntries(item: RootPackage): string[];
|
|
85
91
|
/** Parse configured tsconfigs, then build only selected programs and their project-reference closure. */
|
|
86
92
|
export declare function buildTypeScriptContext(config: ArchitectureConfig, injectedTypeScript?: TypeScriptApi | null, paths?: string[]): TypeContext;
|
|
87
93
|
export declare function relativePath(root: string, fileName: string): string;
|
|
@@ -385,6 +385,26 @@ function packageExported(ts, workspace, specifier, actualTarget) {
|
|
|
385
385
|
return expected === canonical(actualTarget) || sourceOfTarget(ts, workspace, target) === canonical(actualTarget);
|
|
386
386
|
});
|
|
387
387
|
}
|
|
388
|
+
/** The root workspace package of the app (`packages/agent-sdk`) a specifier names, its name or a subpath of it; the longest name when several match. */
|
|
389
|
+
export function rootPackageOf(config, specifier) {
|
|
390
|
+
let found = null;
|
|
391
|
+
for (const item of config.rootPackages ?? [])
|
|
392
|
+
if (sameOrUnder(specifier, item.name) && (found === null || item.name.length > found.name.length))
|
|
393
|
+
found = item;
|
|
394
|
+
return found;
|
|
395
|
+
}
|
|
396
|
+
/** Whether a specifier into a root package names one of its entry points: a subpath its `exports` declare, or its root through `exports`, `main`/`types` or `src/index`. A path into its source is not an entry point. */
|
|
397
|
+
export function rootPackageEntryDeclared(item, specifier) {
|
|
398
|
+
if (exportCandidates(item, specifier).length > 0)
|
|
399
|
+
return true;
|
|
400
|
+
return specifier === item.name && !item.exports && (manifestEntryTargets(item.manifest).length > 0 || packageIndexSource(item) !== null);
|
|
401
|
+
}
|
|
402
|
+
/** The subpaths a root package declares for import (`.` first), for the message of a refused deep import. */
|
|
403
|
+
export function rootPackageEntries(item) {
|
|
404
|
+
const declaration = item.exports;
|
|
405
|
+
const keys = declaration && typeof declaration === 'object' && !Array.isArray(declaration) ? Object.keys(declaration).filter(key => key.startsWith('.')) : ['.'];
|
|
406
|
+
return (keys.length ? keys : ['.']).map(key => (key === '.' ? item.name : `${item.name}${key.slice(1)}`));
|
|
407
|
+
}
|
|
388
408
|
function boundaryViolation(root, edge, fromWorkspace, toWorkspace, ruleId, message) {
|
|
389
409
|
return {
|
|
390
410
|
ruleId,
|
|
@@ -413,7 +433,7 @@ function typeScriptContext(config, loaded, paths) {
|
|
|
413
433
|
canonical, compilerError, isInside, slash, readTypeScriptProject, typeScriptProjectReferencePath,
|
|
414
434
|
sameOrUnder, createTypeScriptProgram, isProductionSource, sourceLocation, workspaceMetadata, workspaceOf,
|
|
415
435
|
pathAliasMatches, moduleReferences, workspaceSourceEntry, resolveTypeScriptModule, crossesSide, assetExtension: ASSET_EXTENSION,
|
|
416
|
-
relativePath, packageExported, boundaryViolation,
|
|
436
|
+
relativePath, packageExported, boundaryViolation, rootPackageOf, rootPackageEntryDeclared, rootPackageEntries,
|
|
417
437
|
});
|
|
418
438
|
}
|
|
419
439
|
export function relativePath(root, fileName) {
|
|
@@ -6,7 +6,7 @@ const HINTS = {
|
|
|
6
6
|
'parameter type': 'Give the parameter a type: `(value: Input)`.',
|
|
7
7
|
'property type': 'Give the property a type: `name: Result = ...`.',
|
|
8
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
|
|
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`; a module definition is `export const { ConfigurableModuleClass, OPTIONS_TYPE } = defineCapabilityModule<XOptions>()` (or `defineConfigurableModule`) of @starci/nest-kit, which states the types (`starci app codemod module-definition` writes it).',
|
|
10
10
|
'exported value': 'Declare the type (an interface or a type alias) and annotate the export with it: `export const name: Shape = ...`.',
|
|
11
11
|
};
|
|
12
12
|
/** The `--isolatedDeclarations` flag name TypeScript ends its reasons with says nothing to the reader. */
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// R255-R256 inspect the final database access state: the application role, forced RLS, policies, views and storage prefixes.
|
|
1
|
+
// R255-R256 inspect the final database access state: the application role (its attributes and every grant on schema auth), forced RLS, policies, views and storage prefixes.
|
|
2
2
|
import { found } from './read.mjs';
|
|
3
3
|
import { parseSql } from '../sql/pg-parse.mjs';
|
|
4
4
|
import { eachInOrder } from '../../lib/in-order.mjs';
|
|
@@ -20,6 +20,10 @@ const AND = ['AND', 'EXPR'].join('_');
|
|
|
20
20
|
const OR = ['OR', 'EXPR'].join('_');
|
|
21
21
|
const NOT = ['NOT', 'EXPR'].join('_');
|
|
22
22
|
const SEQUENCE = ['OBJECT', 'SEQUENCE'].join('_');
|
|
23
|
+
const SCHEMA = ['OBJECT', 'SCHEMA'].join('_');
|
|
24
|
+
const TABLE = ['OBJECT', 'TABLE'].join('_');
|
|
25
|
+
const ALL_IN_SCHEMA = ['ACL', 'TARGET', 'ALL', 'IN', 'SCHEMA'].join('_');
|
|
26
|
+
const ACCOUNT_SCHEMA = 'auth';
|
|
23
27
|
const MATVIEW = ['OBJECT', 'MATVIEW'].join('_');
|
|
24
28
|
const note = (code, source, message, extra = {}) => found(code, source.file, `${source.file}:${source.line} ${message}`, { line: source.line, ...extra });
|
|
25
29
|
function someNode(node, predicate) {
|
|
@@ -157,6 +161,39 @@ function roleFindings(analyses, appRole) {
|
|
|
157
161
|
}
|
|
158
162
|
return findings;
|
|
159
163
|
}
|
|
164
|
+
/** What one grant statement names inside schema auth: the schema, a table or sequence of it, every table or sequence in it. */
|
|
165
|
+
function authTargets(grant) {
|
|
166
|
+
if (grant.objtype === SCHEMA)
|
|
167
|
+
return grant.objects.filter((object) => sval(object) === ACCOUNT_SCHEMA).map(() => `schema ${ACCOUNT_SCHEMA}`);
|
|
168
|
+
if (![TABLE, SEQUENCE].includes(grant.objtype))
|
|
169
|
+
return [];
|
|
170
|
+
if (grant.targtype === ALL_IN_SCHEMA)
|
|
171
|
+
return grant.objects.filter((object) => sval(object) === ACCOUNT_SCHEMA).map(() => `all ${grant.objtype === TABLE ? 'tables' : 'sequences'} in schema ${ACCOUNT_SCHEMA}`);
|
|
172
|
+
return grant.objects.filter((object) => object.RangeVar?.schemaname === ACCOUNT_SCHEMA).map((object) => relationName(object.RangeVar));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* R255: the application role holds no privilege on schema auth, on a table or sequence in it, or by default privilege for the
|
|
176
|
+
* schema. The migrations are applied in order, so a grant that a later migration revokes (the contract step of an app that moved
|
|
177
|
+
* to the identity account doors) is not held; what remains at the end is reported at the grant that left it.
|
|
178
|
+
*/
|
|
179
|
+
function authSchemaFindings(analyses, appRole) {
|
|
180
|
+
const held = new Map();
|
|
181
|
+
for (const { file, facts } of analyses) {
|
|
182
|
+
const statements = [
|
|
183
|
+
...facts.grants.map((grant) => ({ grant, targets: authTargets(grant) })),
|
|
184
|
+
...facts.defaultPrivileges.map((grant) => ({ grant, targets: grant.schemas.includes(ACCOUNT_SCHEMA) ? [`default privileges in schema ${ACCOUNT_SCHEMA}`] : [] })),
|
|
185
|
+
].filter(({ grant }) => grant.grantees.includes(appRole));
|
|
186
|
+
for (const { grant, targets } of statements) {
|
|
187
|
+
for (const target of targets) {
|
|
188
|
+
const state = held.get(target) ?? { target, privileges: new Set() };
|
|
189
|
+
updatePrivileges(state, grant.grant, grant.all ? ['all'] : grant.privileges);
|
|
190
|
+
held.set(target, { ...state, file, line: grant.line });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...held.values()].filter((state) => state.privileges.size > 0)
|
|
195
|
+
.map((state) => note(DB_APP_ROLE, state, `${appRole} holds a grant on ${state.target}; reach accounts through the identity account doors (SECURITY DEFINER functions), never through a grant on schema ${ACCOUNT_SCHEMA}; an app on the old sign-in glue runs "starci app codemod account-doors", "starci app sync --execute" and "starci app codemod account-doors --contract" in one release`, { target: state.target }));
|
|
196
|
+
}
|
|
160
197
|
function roleStatementFindings(statement, appRole) {
|
|
161
198
|
const { type, node } = statement;
|
|
162
199
|
if (!['CreateRoleStmt', 'AlterRoleStmt'].includes(type))
|
|
@@ -299,6 +336,7 @@ export async function databaseAccessFindings({ analyses, exposed, db = {} }) {
|
|
|
299
336
|
return [
|
|
300
337
|
...policyFindings(policies, appRole, helpers),
|
|
301
338
|
...roleFindings(analyses, appRole),
|
|
339
|
+
...authSchemaFindings(analyses, appRole),
|
|
302
340
|
...forceFindings(analyses, declared, appRole),
|
|
303
341
|
...viewFindings(analyses, grants, exposed),
|
|
304
342
|
...storagePrefixFindings(policies, helpers),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Parsed, Repo } from './rule-types.mjs';
|
|
2
2
|
/** The npm workspaces of every app, in this order. */
|
|
3
3
|
export declare const WORKSPACES: readonly string[];
|
|
4
|
+
/** The workspace of the app's own root packages (slot app.packages: packages/agent-sdk, packages/module-<name>), added after the fe ones once one is tracked. */
|
|
5
|
+
export declare const ROOT_PACKAGES_WORKSPACE = "packages/*";
|
|
6
|
+
/** The workspaces the tracked tree `files` calls for: the fe ones, then `packages/*` when a root package tracks its package.json. */
|
|
7
|
+
export declare const workspacesFor: (files: readonly string[]) => readonly string[];
|
|
4
8
|
/** The scripts of every fe app workspace, exactly: turbo runs build, dev and typecheck; start serves a build. The lint is the root's (`starci app check --gate lint`), never a workspace script. */
|
|
5
9
|
export declare const FE_APP_SCRIPTS: Readonly<Record<string, string>>;
|
|
6
10
|
/** The package manager every app root declares (turbo reads it to parse the lockfile); the starci app scaffold writes it. */
|