@bajustone/fortress 1.0.0-rc.1
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 +1011 -0
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/SECURITY.md +197 -0
- package/bin/fortress.ts +667 -0
- package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
- package/dist/auth-service-BkzZkWfH.d.ts +307 -0
- package/dist/config-B2366s_G.d.ts +665 -0
- package/dist/config-GtlVt6ZD.d.cts +665 -0
- package/dist/convert-routes-BLCQT57f.d.ts +72 -0
- package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
- package/dist/crypto.cjs +61 -0
- package/dist/crypto.d.cts +36 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.js +35 -0
- package/dist/drift-BT1wtMDJ.d.cts +22 -0
- package/dist/drift-k9LiYpRP.d.ts +22 -0
- package/dist/drizzle/pg.cjs +481 -0
- package/dist/drizzle/pg.d.cts +15 -0
- package/dist/drizzle/pg.d.ts +15 -0
- package/dist/drizzle/pg.js +454 -0
- package/dist/drizzle.cjs +1442 -0
- package/dist/drizzle.d.cts +85 -0
- package/dist/drizzle.d.ts +85 -0
- package/dist/drizzle.js +1411 -0
- package/dist/express.cjs +1357 -0
- package/dist/express.d.cts +333 -0
- package/dist/express.d.ts +333 -0
- package/dist/express.js +1314 -0
- package/dist/fetcher.cjs +77 -0
- package/dist/fetcher.d.cts +7 -0
- package/dist/fetcher.d.ts +7 -0
- package/dist/fetcher.js +41 -0
- package/dist/fortress-BmSvFfqK.d.cts +1089 -0
- package/dist/fortress-uA-_cheR.d.ts +1089 -0
- package/dist/hono.cjs +1530 -0
- package/dist/hono.d.cts +514 -0
- package/dist/hono.d.ts +514 -0
- package/dist/hono.js +1483 -0
- package/dist/index-CxEkfCA0.d.cts +77 -0
- package/dist/index-CxEkfCA0.d.ts +77 -0
- package/dist/index-D054pcb-.d.cts +146 -0
- package/dist/index-jAMbbUAY.d.ts +146 -0
- package/dist/index.cjs +9046 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +8935 -0
- package/dist/jwt.cjs +255 -0
- package/dist/jwt.d.cts +90 -0
- package/dist/jwt.d.ts +90 -0
- package/dist/jwt.js +227 -0
- package/dist/otel.cjs +108 -0
- package/dist/otel.d.cts +62 -0
- package/dist/otel.d.ts +62 -0
- package/dist/otel.js +73 -0
- package/dist/plugins/account-lockout.cjs +322 -0
- package/dist/plugins/account-lockout.d.cts +42 -0
- package/dist/plugins/account-lockout.d.ts +42 -0
- package/dist/plugins/account-lockout.js +295 -0
- package/dist/plugins/admin.cjs +2378 -0
- package/dist/plugins/admin.d.cts +72 -0
- package/dist/plugins/admin.d.ts +72 -0
- package/dist/plugins/admin.js +2351 -0
- package/dist/plugins/api-key.cjs +792 -0
- package/dist/plugins/api-key.d.cts +121 -0
- package/dist/plugins/api-key.d.ts +121 -0
- package/dist/plugins/api-key.js +765 -0
- package/dist/plugins/audit-log.cjs +493 -0
- package/dist/plugins/audit-log.d.cts +86 -0
- package/dist/plugins/audit-log.d.ts +86 -0
- package/dist/plugins/audit-log.js +468 -0
- package/dist/plugins/data-isolation.cjs +211 -0
- package/dist/plugins/data-isolation.d.cts +49 -0
- package/dist/plugins/data-isolation.d.ts +49 -0
- package/dist/plugins/data-isolation.js +186 -0
- package/dist/plugins/email-verification.cjs +273 -0
- package/dist/plugins/email-verification.d.cts +44 -0
- package/dist/plugins/email-verification.d.ts +44 -0
- package/dist/plugins/email-verification.js +246 -0
- package/dist/plugins/magic-link.cjs +231 -0
- package/dist/plugins/magic-link.d.cts +39 -0
- package/dist/plugins/magic-link.d.ts +39 -0
- package/dist/plugins/magic-link.js +204 -0
- package/dist/plugins/oauth.cjs +1696 -0
- package/dist/plugins/oauth.d.cts +406 -0
- package/dist/plugins/oauth.d.ts +406 -0
- package/dist/plugins/oauth.js +1669 -0
- package/dist/plugins/openapi.cjs +1185 -0
- package/dist/plugins/openapi.d.cts +6 -0
- package/dist/plugins/openapi.d.ts +6 -0
- package/dist/plugins/openapi.js +1158 -0
- package/dist/plugins/rate-limit/express.cjs +55 -0
- package/dist/plugins/rate-limit/express.d.cts +64 -0
- package/dist/plugins/rate-limit/express.d.ts +64 -0
- package/dist/plugins/rate-limit/express.js +30 -0
- package/dist/plugins/rate-limit/hono.cjs +51 -0
- package/dist/plugins/rate-limit/hono.d.cts +59 -0
- package/dist/plugins/rate-limit/hono.d.ts +59 -0
- package/dist/plugins/rate-limit/hono.js +26 -0
- package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
- package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
- package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
- package/dist/plugins/rate-limit/sveltekit.js +24 -0
- package/dist/plugins/rate-limit.cjs +354 -0
- package/dist/plugins/rate-limit.d.cts +140 -0
- package/dist/plugins/rate-limit.d.ts +140 -0
- package/dist/plugins/rate-limit.js +325 -0
- package/dist/plugins/social-login.cjs +905 -0
- package/dist/plugins/social-login.d.cts +114 -0
- package/dist/plugins/social-login.d.ts +114 -0
- package/dist/plugins/social-login.js +880 -0
- package/dist/plugins/tenancy.cjs +780 -0
- package/dist/plugins/tenancy.d.cts +108 -0
- package/dist/plugins/tenancy.d.ts +108 -0
- package/dist/plugins/tenancy.js +753 -0
- package/dist/plugins/two-factor.cjs +555 -0
- package/dist/plugins/two-factor.d.cts +67 -0
- package/dist/plugins/two-factor.d.ts +67 -0
- package/dist/plugins/two-factor.js +526 -0
- package/dist/plugins/webauthn.cjs +786 -0
- package/dist/plugins/webauthn.d.cts +72 -0
- package/dist/plugins/webauthn.d.ts +72 -0
- package/dist/plugins/webauthn.js +766 -0
- package/dist/plugins/webhook.cjs +819 -0
- package/dist/plugins/webhook.d.cts +254 -0
- package/dist/plugins/webhook.d.ts +254 -0
- package/dist/plugins/webhook.js +789 -0
- package/dist/protect-D1v_0upK.d.cts +123 -0
- package/dist/protect-DsxV_-dJ.d.ts +123 -0
- package/dist/sveltekit.cjs +1258 -0
- package/dist/sveltekit.d.cts +420 -0
- package/dist/sveltekit.d.ts +420 -0
- package/dist/sveltekit.js +1207 -0
- package/dist/testing.cjs +4294 -0
- package/dist/testing.d.cts +197 -0
- package/dist/testing.d.ts +197 -0
- package/dist/testing.js +4264 -0
- package/dist/types-et0R8tsC.d.cts +217 -0
- package/dist/types-et0R8tsC.d.ts +217 -0
- package/docs/adapter-typed-helpers.md +192 -0
- package/docs/admin-recipes.md +227 -0
- package/docs/architecture.md +434 -0
- package/docs/ci/github-actions.yml +59 -0
- package/docs/ci.md +109 -0
- package/docs/compatibility.md +115 -0
- package/docs/deployment.md +305 -0
- package/docs/hardening.md +118 -0
- package/docs/host-owned-routes.md +154 -0
- package/docs/migrations/0001-schema-version.md +54 -0
- package/docs/migrations/0002-initial-schema.md +84 -0
- package/docs/migrations/upgrade-guide.md +160 -0
- package/docs/observability.md +394 -0
- package/docs/plugins/account-lockout.md +131 -0
- package/docs/plugins/admin.md +402 -0
- package/docs/plugins/api-key.md +327 -0
- package/docs/plugins/audit-log.md +261 -0
- package/docs/plugins/data-isolation.md +186 -0
- package/docs/plugins/email-verification.md +121 -0
- package/docs/plugins/magic-link.md +123 -0
- package/docs/plugins/oauth.md +544 -0
- package/docs/plugins/openapi.md +250 -0
- package/docs/plugins/rate-limit.md +223 -0
- package/docs/plugins/social-login.md +337 -0
- package/docs/plugins/tenancy.md +122 -0
- package/docs/plugins/two-factor.md +191 -0
- package/docs/plugins/webauthn.md +188 -0
- package/docs/plugins/webhook.md +242 -0
- package/docs/policy-as-code.md +156 -0
- package/docs/route-manifest.md +46 -0
- package/docs/security.md +261 -0
- package/docs/threat-model.md +161 -0
- package/examples/README.md +119 -0
- package/examples/express-app/index.ts +747 -0
- package/examples/hono-app/docker-compose.yml +14 -0
- package/examples/hono-app/index.ts +1009 -0
- package/examples/policy/fortress.policy.json +47 -0
- package/examples/sveltekit-app/README.md +125 -0
- package/examples/sveltekit-app/src/app.d.ts +16 -0
- package/examples/sveltekit-app/src/hooks.server.ts +23 -0
- package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
- package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
- package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
- package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
- package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
- package/migrations/pg/0001_schema_version.down.sql +2 -0
- package/migrations/pg/0001_schema_version.sql +8 -0
- package/migrations/pg/0002_initial_schema.down.sql +36 -0
- package/migrations/pg/0002_initial_schema.sql +376 -0
- package/migrations/pg/0003_auth_continuation.down.sql +6 -0
- package/migrations/pg/0003_auth_continuation.sql +30 -0
- package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/pg/0004_tenant_default_unique.sql +14 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
- package/migrations/pg/0006_canonical_email.down.sql +3 -0
- package/migrations/pg/0006_canonical_email.sql +8 -0
- package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.sql +8 -0
- package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
- package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
- package/migrations/sqlite/0001_schema_version.down.sql +2 -0
- package/migrations/sqlite/0001_schema_version.sql +8 -0
- package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
- package/migrations/sqlite/0002_initial_schema.sql +376 -0
- package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
- package/migrations/sqlite/0003_auth_continuation.sql +45 -0
- package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
- package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
- package/migrations/sqlite/0006_canonical_email.sql +8 -0
- package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
- package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
- package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
- package/package.json +398 -0
- package/src/adapters/database/index.ts +63 -0
- package/src/adapters/database/types.ts +22 -0
- package/src/changelog-script.test.ts +21 -0
- package/src/cli.test.ts +79 -0
- package/src/core/auth/auth-admin.test.ts +247 -0
- package/src/core/auth/auth-endpoints.ts +558 -0
- package/src/core/auth/auth-observer.test.ts +191 -0
- package/src/core/auth/auth-service.ts +1464 -0
- package/src/core/auth/email.test.ts +17 -0
- package/src/core/auth/email.ts +11 -0
- package/src/core/auth/hooks.test.ts +251 -0
- package/src/core/auth/impersonation.test.ts +179 -0
- package/src/core/auth/jwt.test.ts +184 -0
- package/src/core/auth/jwt.ts +239 -0
- package/src/core/auth/login-timing.test.ts +77 -0
- package/src/core/auth/password-policy.test.ts +253 -0
- package/src/core/auth/password-policy.ts +220 -0
- package/src/core/auth/password.test.ts +42 -0
- package/src/core/auth/password.ts +62 -0
- package/src/core/auth/post-auth-gate.test.ts +258 -0
- package/src/core/auth/post-auth-gate.ts +228 -0
- package/src/core/auth/refresh-token.test.ts +86 -0
- package/src/core/auth/refresh-token.ts +105 -0
- package/src/core/auth/timing-safe.ts +23 -0
- package/src/core/config.ts +212 -0
- package/src/core/endpoint.ts +149 -0
- package/src/core/errors.ts +199 -0
- package/src/core/fetcher-interop.test.ts +106 -0
- package/src/core/fortress.test.ts +354 -0
- package/src/core/fortress.ts +550 -0
- package/src/core/http/call.test.ts +148 -0
- package/src/core/http/call.ts +149 -0
- package/src/core/http/cookie-serialize.test.ts +198 -0
- package/src/core/http/cookie-serialize.ts +107 -0
- package/src/core/http/csrf.test.ts +140 -0
- package/src/core/http/csrf.ts +173 -0
- package/src/core/http/dispatch.ts +652 -0
- package/src/core/http/error-response.test.ts +69 -0
- package/src/core/http/error-response.ts +93 -0
- package/src/core/http/fortress-rbac.test.ts +43 -0
- package/src/core/http/fortress-rbac.ts +127 -0
- package/src/core/http/handle-request.test.ts +441 -0
- package/src/core/http/handle-request.ts +354 -0
- package/src/core/http/match.test.ts +122 -0
- package/src/core/http/match.ts +126 -0
- package/src/core/http/outbound.test.ts +34 -0
- package/src/core/http/outbound.ts +105 -0
- package/src/core/http/plugin-middleware.ts +67 -0
- package/src/core/http/principal.test.ts +345 -0
- package/src/core/http/principal.ts +86 -0
- package/src/core/http/protect.test.ts +220 -0
- package/src/core/http/protect.ts +394 -0
- package/src/core/http/token-extraction.test.ts +59 -0
- package/src/core/http/token-extraction.ts +53 -0
- package/src/core/iam/explain.test.ts +177 -0
- package/src/core/iam/explain.ts +302 -0
- package/src/core/iam/iam-endpoints.ts +779 -0
- package/src/core/iam/iam-service.test.ts +829 -0
- package/src/core/iam/iam-service.ts +1137 -0
- package/src/core/iam/permission-cache.test.ts +115 -0
- package/src/core/iam/permission-cache.ts +71 -0
- package/src/core/iam/permission-check-observer.test.ts +90 -0
- package/src/core/iam/permission-evaluator.test.ts +291 -0
- package/src/core/iam/permission-evaluator.ts +198 -0
- package/src/core/iam/permission-sync.test.ts +166 -0
- package/src/core/iam/permission-sync.ts +192 -0
- package/src/core/iam/resource-sync.test.ts +70 -0
- package/src/core/iam/resource-sync.ts +182 -0
- package/src/core/iam/service-account-http.test.ts +529 -0
- package/src/core/iam/service-account.test.ts +282 -0
- package/src/core/internal-adapter.test.ts +389 -0
- package/src/core/internal-adapter.ts +435 -0
- package/src/core/json-schema.ts +50 -0
- package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
- package/src/core/manifest/drift.ts +103 -0
- package/src/core/manifest/route-manifest.test.ts +142 -0
- package/src/core/manifest/route-manifest.ts +154 -0
- package/src/core/migrate.test.ts +72 -0
- package/src/core/migrations/engine.test.ts +308 -0
- package/src/core/migrations/engine.ts +548 -0
- package/src/core/migrations/migrations.ts +1771 -0
- package/src/core/migrations/pg-migration.integration-test.ts +353 -0
- package/src/core/migrations/upgrade-fixture.test.ts +378 -0
- package/src/core/observability/db-instrumentation.ts +205 -0
- package/src/core/observability/listener-list.test.ts +124 -0
- package/src/core/observability/listener-list.ts +73 -0
- package/src/core/observability/logger.test.ts +43 -0
- package/src/core/observability/logger.ts +49 -0
- package/src/core/observability/spans.test.ts +193 -0
- package/src/core/observability/types.ts +80 -0
- package/src/core/openapi-drift.test.ts +41 -0
- package/src/core/openapi.ts +47 -0
- package/src/core/plugin-methods-map.ts +75 -0
- package/src/core/plugin-runner.test.ts +233 -0
- package/src/core/plugin-runner.ts +276 -0
- package/src/core/plugin.ts +210 -0
- package/src/core/policy/apply.ts +272 -0
- package/src/core/policy/diff.ts +288 -0
- package/src/core/policy/loader.test.ts +63 -0
- package/src/core/policy/loader.ts +214 -0
- package/src/core/policy/policy.test.ts +232 -0
- package/src/core/policy/types.ts +105 -0
- package/src/core/schema-builder.test.ts +660 -0
- package/src/core/schema-builder.ts +881 -0
- package/src/core/standard-schema.ts +56 -0
- package/src/core/types.ts +258 -0
- package/src/core/validation.test.ts +195 -0
- package/src/core/validation.ts +192 -0
- package/src/drizzle/adapter.test.ts +425 -0
- package/src/drizzle/adapter.ts +474 -0
- package/src/drizzle/index.ts +31 -0
- package/src/drizzle/pg/adapter.integration-test.ts +456 -0
- package/src/drizzle/pg/index.ts +13 -0
- package/src/drizzle/pg/pg.integration-test.ts +1539 -0
- package/src/drizzle/pg/schema.ts +539 -0
- package/src/drizzle/pg-error-map.test.ts +218 -0
- package/src/drizzle/pg-error-map.ts +151 -0
- package/src/drizzle/schema.ts +544 -0
- package/src/drizzle/sqlite-serialization.test.ts +106 -0
- package/src/express/express-api-key-user-routes.test.ts +241 -0
- package/src/express/express.test.ts +442 -0
- package/src/express/handle.ts +191 -0
- package/src/express/index.ts +62 -0
- package/src/express/middleware.ts +551 -0
- package/src/express/protect.ts +154 -0
- package/src/express/validated.test.ts +86 -0
- package/src/express/validated.ts +99 -0
- package/src/fetcher/index.ts +81 -0
- package/src/hono/convert-routes.test.ts +220 -0
- package/src/hono/convert-routes.ts +196 -0
- package/src/hono/converters.test.ts +50 -0
- package/src/hono/converters.ts +43 -0
- package/src/hono/handle.ts +79 -0
- package/src/hono/helpers.ts +2 -0
- package/src/hono/hono-api-key-user-routes.test.ts +225 -0
- package/src/hono/hono-handlers.test.ts +861 -0
- package/src/hono/hono.test.ts +454 -0
- package/src/hono/index.ts +101 -0
- package/src/hono/middleware/auth.ts +236 -0
- package/src/hono/middleware/auth.types.test.ts +94 -0
- package/src/hono/middleware/csrf.test.ts +127 -0
- package/src/hono/middleware/csrf.ts +68 -0
- package/src/hono/middleware/error-handler.ts +12 -0
- package/src/hono/middleware/plugin-middleware.ts +35 -0
- package/src/hono/middleware/rbac.ts +131 -0
- package/src/hono/middleware/security-headers.test.ts +88 -0
- package/src/hono/middleware/security-headers.ts +68 -0
- package/src/hono/openapi.ts +237 -0
- package/src/hono/protect.ts +87 -0
- package/src/hono/validated.test.ts +123 -0
- package/src/hono/validated.ts +62 -0
- package/src/index.ts +309 -0
- package/src/integration.test.ts +718 -0
- package/src/internal/changelog.ts +56 -0
- package/src/otel/index.ts +158 -0
- package/src/otel/otel-types.test.ts +35 -0
- package/src/otel/otel.test.ts +235 -0
- package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
- package/src/plugins/account-lockout/index.ts +267 -0
- package/src/plugins/admin/admin-api-key.test.ts +438 -0
- package/src/plugins/admin/index.ts +1612 -0
- package/src/plugins/api-key/api-key.test.ts +684 -0
- package/src/plugins/api-key/core.ts +336 -0
- package/src/plugins/api-key/index.ts +315 -0
- package/src/plugins/audit-log/audit-log.test.ts +749 -0
- package/src/plugins/audit-log/index.ts +680 -0
- package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
- package/src/plugins/data-isolation/index.ts +157 -0
- package/src/plugins/email-verification/email-verification.test.ts +199 -0
- package/src/plugins/email-verification/index.ts +190 -0
- package/src/plugins/magic-link/index.ts +129 -0
- package/src/plugins/magic-link/magic-link.test.ts +156 -0
- package/src/plugins/oauth/id-token.ts +86 -0
- package/src/plugins/oauth/index.ts +2145 -0
- package/src/plugins/oauth/jwks.test.ts +32 -0
- package/src/plugins/oauth/jwks.ts +182 -0
- package/src/plugins/oauth/oauth.test.ts +2220 -0
- package/src/plugins/oauth/pkce.ts +58 -0
- package/src/plugins/openapi/index.ts +298 -0
- package/src/plugins/openapi/openapi.test.ts +425 -0
- package/src/plugins/openapi/spec-builder.ts +287 -0
- package/src/plugins/rate-limit/express.test.ts +89 -0
- package/src/plugins/rate-limit/express.ts +86 -0
- package/src/plugins/rate-limit/hono.test.ts +60 -0
- package/src/plugins/rate-limit/hono.ts +74 -0
- package/src/plugins/rate-limit/index.ts +383 -0
- package/src/plugins/rate-limit/memory-store.ts +61 -0
- package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
- package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
- package/src/plugins/rate-limit/sveltekit.ts +66 -0
- package/src/plugins/social-login/index.ts +715 -0
- package/src/plugins/social-login/providers/apple.ts +34 -0
- package/src/plugins/social-login/providers/discord.ts +26 -0
- package/src/plugins/social-login/providers/github.ts +54 -0
- package/src/plugins/social-login/providers/google.ts +23 -0
- package/src/plugins/social-login/providers/index.ts +22 -0
- package/src/plugins/social-login/providers/microsoft.ts +35 -0
- package/src/plugins/social-login/providers/oidc.ts +37 -0
- package/src/plugins/social-login/providers/providers.test.ts +211 -0
- package/src/plugins/social-login/social-login.test.ts +675 -0
- package/src/plugins/social-login/types.ts +80 -0
- package/src/plugins/tenancy/index.ts +544 -0
- package/src/plugins/tenancy/tenancy.test.ts +323 -0
- package/src/plugins/two-factor/index.ts +569 -0
- package/src/plugins/two-factor/two-factor.test.ts +235 -0
- package/src/plugins/webauthn/index.ts +554 -0
- package/src/plugins/webauthn/webauthn.test.ts +472 -0
- package/src/plugins/webhook/builtin-events.ts +29 -0
- package/src/plugins/webhook/delivery.test.ts +51 -0
- package/src/plugins/webhook/delivery.ts +154 -0
- package/src/plugins/webhook/hooks.ts +89 -0
- package/src/plugins/webhook/index.ts +445 -0
- package/src/plugins/webhook/queue/database.ts +67 -0
- package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
- package/src/plugins/webhook/queue/in-memory.ts +71 -0
- package/src/plugins/webhook/queue/types.ts +51 -0
- package/src/plugins/webhook/registry.test.ts +48 -0
- package/src/plugins/webhook/registry.ts +64 -0
- package/src/plugins/webhook/signing.ts +45 -0
- package/src/plugins/webhook/ssrf.ts +198 -0
- package/src/plugins/webhook/types.ts +138 -0
- package/src/plugins/webhook/webhook.test.ts +324 -0
- package/src/sveltekit/actions.ts +233 -0
- package/src/sveltekit/catch-all.ts +43 -0
- package/src/sveltekit/cookies.ts +136 -0
- package/src/sveltekit/handle.ts +356 -0
- package/src/sveltekit/helpers.ts +69 -0
- package/src/sveltekit/index.ts +74 -0
- package/src/sveltekit/protect.ts +80 -0
- package/src/sveltekit/sveltekit-types.test.ts +31 -0
- package/src/sveltekit/sveltekit.test.ts +799 -0
- package/src/sveltekit/types.ts +134 -0
- package/src/sveltekit/validated.test.ts +117 -0
- package/src/sveltekit/validated.ts +78 -0
- package/src/testing/adapter-conformance.test.ts +408 -0
- package/src/testing/checks.test.ts +124 -0
- package/src/testing/checks.ts +304 -0
- package/src/testing/index.ts +107 -0
package/bin/fortress.ts
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
|
|
4
|
+
import type { EndpointDefinition } from '../src/core/endpoint';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { authComponentSchemas, authEndpoints } from '../src/core/auth/auth-endpoints';
|
|
8
|
+
import { iamComponentSchemas, iamEndpoints } from '../src/core/iam/iam-endpoints';
|
|
9
|
+
import { parseResourceFile } from '../src/core/iam/resource-sync';
|
|
10
|
+
import { detectRouteManifestDrift, hasRouteManifestDrift } from '../src/core/manifest/drift';
|
|
11
|
+
import { buildRouteManifest } from '../src/core/manifest/route-manifest';
|
|
12
|
+
import { getFortressMigrations, getLatestMigrationVersion } from '../src/core/migrations/migrations';
|
|
13
|
+
import { loadPolicy, resolvePolicyPath } from '../src/core/policy/loader';
|
|
14
|
+
import { buildOpenAPISpec } from '../src/plugins/openapi/spec-builder';
|
|
15
|
+
import { checkPublicRoutes } from '../src/testing/checks';
|
|
16
|
+
|
|
17
|
+
const HELP_TEXT = `
|
|
18
|
+
fortress — CLI tool for @bajustone/fortress
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
fortress <command> [options]
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
init Scaffold config, .env template, and fortress.resources.json
|
|
25
|
+
sync:push Push resources from JSON file to database
|
|
26
|
+
sync:pull Pull resources from database to JSON file
|
|
27
|
+
sync:types Generate TypeScript types from fortress.resources.json
|
|
28
|
+
generate-secret Generate a 64-byte cryptographically random hex secret
|
|
29
|
+
openapi Generate OpenAPI 3.1 spec from endpoint definitions
|
|
30
|
+
schemas Generate schema code from endpoint definitions
|
|
31
|
+
manifest Generate route-security manifest from endpoint metadata
|
|
32
|
+
manifest:check Check route manifest drift against endpoints/OpenAPI/RBAC
|
|
33
|
+
check:routes Alias for manifest:check (CI namespace)
|
|
34
|
+
check:public-routes Fail if any non-allow-listed route is public/oauth-protocol
|
|
35
|
+
check:migrations Alias for migrate:check (CI namespace)
|
|
36
|
+
policy:summary Parse fortress.policy.json and print a summary (offline)
|
|
37
|
+
policy:diff How-to: run diffPolicy() programmatically (DB-bound)
|
|
38
|
+
policy:apply How-to: run applyPolicyPlan() programmatically (DB-bound)
|
|
39
|
+
policy:check How-to: assert in-sync policy in CI (DB-bound)
|
|
40
|
+
migrate:status Show bundled Fortress schema migration status
|
|
41
|
+
migrate:up Print/apply guidance for pending bundled migrations
|
|
42
|
+
migrate:down Print rollback SQL for bundled migrations
|
|
43
|
+
migrate:diff Explain live schema drift checking API
|
|
44
|
+
migrate:check Check bundled migration catalog consistency
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--help, -h Show this help message
|
|
48
|
+
|
|
49
|
+
openapi options:
|
|
50
|
+
--out, -o <file> Output file (default: stdout)
|
|
51
|
+
--title <title> API title (default: 'Fortress Auth API')
|
|
52
|
+
--version <ver> API version (default: '1.0.0')
|
|
53
|
+
|
|
54
|
+
schemas options:
|
|
55
|
+
--format <fmt> Schema format: 'zod' | 'json-schema' (default: 'json-schema')
|
|
56
|
+
--out, -o <file> Output file (default: stdout)
|
|
57
|
+
|
|
58
|
+
manifest options:
|
|
59
|
+
--out, -o <file> Output file (default: stdout)
|
|
60
|
+
|
|
61
|
+
migration options:
|
|
62
|
+
--dialect <dialect> sqlite | pg (default: sqlite)
|
|
63
|
+
--out, -o <file> Write SQL/status to file where supported
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
fortress init
|
|
67
|
+
fortress sync:types
|
|
68
|
+
fortress generate-secret
|
|
69
|
+
fortress openapi --out openapi.json
|
|
70
|
+
fortress schemas --format zod --out src/generated/fortress-schemas.ts
|
|
71
|
+
fortress manifest --out route-manifest.json
|
|
72
|
+
fortress manifest:check
|
|
73
|
+
fortress check:routes
|
|
74
|
+
fortress check:public-routes
|
|
75
|
+
fortress check:migrations
|
|
76
|
+
fortress policy:summary --file fortress.policy.production.json
|
|
77
|
+
`.trim();
|
|
78
|
+
|
|
79
|
+
const CONFIG_TEMPLATE = `import type { FortressConfig } from '@bajustone/fortress';
|
|
80
|
+
|
|
81
|
+
const config: FortressConfig = {
|
|
82
|
+
database: undefined!, // Replace with your DatabaseAdapter (e.g. createDrizzleAdapter(db))
|
|
83
|
+
jwt: {
|
|
84
|
+
key: process.env.FORTRESS_JWT_SECRET!,
|
|
85
|
+
issuer: 'my-app',
|
|
86
|
+
accessTokenExpirySeconds: 900, // 15 minutes
|
|
87
|
+
refreshTokenExpirySeconds: 604800, // 7 days
|
|
88
|
+
},
|
|
89
|
+
plugins: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default config;
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const ENV_TEMPLATE = `# Fortress configuration
|
|
96
|
+
# Generate a secret with: fortress generate-secret
|
|
97
|
+
FORTRESS_JWT_SECRET=
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const RESOURCES_TEMPLATE = JSON.stringify(
|
|
101
|
+
{
|
|
102
|
+
$schema: 'https://github.com/bajustone/fortress/blob/main/schemas/fortress.resources.schema.json',
|
|
103
|
+
resources: {
|
|
104
|
+
article: {
|
|
105
|
+
description: 'Blog articles',
|
|
106
|
+
actions: ['create', 'read', 'update', 'delete', 'publish'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
function cmdInit(): void {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
const files: Array<{ path: string; content: string; label: string }> = [
|
|
117
|
+
{ path: join(cwd, 'fortress.config.ts'), content: CONFIG_TEMPLATE, label: 'fortress.config.ts' },
|
|
118
|
+
{ path: join(cwd, '.env.example'), content: ENV_TEMPLATE, label: '.env.example' },
|
|
119
|
+
{ path: join(cwd, 'fortress.resources.json'), content: RESOURCES_TEMPLATE, label: 'fortress.resources.json' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
if (existsSync(file.path)) {
|
|
124
|
+
console.log(` skip ${file.label} (already exists)`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
writeFileSync(file.path, file.content, 'utf-8');
|
|
128
|
+
console.log(` create ${file.label}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('\nDone! Next steps:');
|
|
133
|
+
console.log(' 1. Run "fortress generate-secret" and paste the value into .env');
|
|
134
|
+
console.log(' 2. Configure your database adapter in fortress.config.ts');
|
|
135
|
+
console.log(' 3. Define your resources in fortress.resources.json');
|
|
136
|
+
console.log(' 4. Run "fortress sync:types" to generate TypeScript types');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cmdSyncPush(): void {
|
|
140
|
+
console.log('sync:push requires a running database connection.\n');
|
|
141
|
+
console.log('Use this in your application code instead:\n');
|
|
142
|
+
console.log(' import { createFortress } from \'@bajustone/fortress\';');
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(' const fortress = createFortress(config);');
|
|
145
|
+
console.log(' await fortress.iam.syncResources(\'push\');');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cmdSyncPull(): void {
|
|
149
|
+
console.log('sync:pull requires a running database connection.\n');
|
|
150
|
+
console.log('Use this in your application code instead:\n');
|
|
151
|
+
console.log(' import { createFortress } from \'@bajustone/fortress\';');
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(' const fortress = createFortress(config);');
|
|
154
|
+
console.log(' await fortress.iam.syncResources(\'pull\');');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cmdSyncTypes(): void {
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const resourcesPath = join(cwd, 'fortress.resources.json');
|
|
160
|
+
|
|
161
|
+
if (!existsSync(resourcesPath)) {
|
|
162
|
+
console.error('Error: fortress.resources.json not found in the current directory.');
|
|
163
|
+
console.error('Run "fortress init" to create one.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
const raw = readFileSync(resourcesPath, 'utf-8');
|
|
170
|
+
parsed = parseResourceFile(JSON.parse(raw));
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.error(`Error: ${err instanceof Error ? err.message : 'Could not parse fortress.resources.json'}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resourceNames = Object.keys(parsed.resources);
|
|
178
|
+
const allActions = new Set<string>();
|
|
179
|
+
const resourceActionMap: Record<string, string[]> = {};
|
|
180
|
+
|
|
181
|
+
for (const [name, resource] of Object.entries(parsed.resources)) {
|
|
182
|
+
resourceActionMap[name] = resource.actions;
|
|
183
|
+
for (const action of resource.actions) {
|
|
184
|
+
allActions.add(action);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let output = '// Auto-generated by "fortress sync:types" — do not edit manually\n\n';
|
|
189
|
+
|
|
190
|
+
// Resource union type
|
|
191
|
+
const resourceUnion = resourceNames.length > 0
|
|
192
|
+
? resourceNames.map(name => JSON.stringify(name)).join(' | ')
|
|
193
|
+
: 'never';
|
|
194
|
+
output += `export type FortressResource = ${resourceUnion};\n\n`;
|
|
195
|
+
|
|
196
|
+
// Action union type (all unique actions across resources)
|
|
197
|
+
const sortedActions = [...allActions].sort();
|
|
198
|
+
const actionUnion = sortedActions.length > 0
|
|
199
|
+
? sortedActions.map(action => JSON.stringify(action)).join(' | ')
|
|
200
|
+
: 'never';
|
|
201
|
+
output += `export type FortressAction = ${actionUnion};\n\n`;
|
|
202
|
+
|
|
203
|
+
// Per-resource action map
|
|
204
|
+
output += 'export interface FortressResourceActions {\n';
|
|
205
|
+
for (const [name, resource] of Object.entries(parsed.resources)) {
|
|
206
|
+
const actions = resource.actions.length > 0
|
|
207
|
+
? resource.actions.map(action => JSON.stringify(action)).join(' | ')
|
|
208
|
+
: 'never';
|
|
209
|
+
output += ` ${JSON.stringify(name)}: ${actions};\n`;
|
|
210
|
+
}
|
|
211
|
+
output += '}\n';
|
|
212
|
+
|
|
213
|
+
const outPath = join(cwd, 'fortress.resources.d.ts');
|
|
214
|
+
writeFileSync(outPath, output, 'utf-8');
|
|
215
|
+
console.log(`Generated ${outPath}`);
|
|
216
|
+
console.log(` ${resourceNames.length} resource(s), ${allActions.size} unique action(s)`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cmdGenerateSecret(): void {
|
|
220
|
+
const bytes = new Uint8Array(64);
|
|
221
|
+
crypto.getRandomValues(bytes);
|
|
222
|
+
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
223
|
+
console.log(hex);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseArg(args: string[], flag: string, alias?: string): string | undefined {
|
|
227
|
+
for (let i = 0; i < args.length; i++) {
|
|
228
|
+
if (args[i] === flag || (alias && args[i] === alias)) {
|
|
229
|
+
return args[i + 1];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cmdOpenAPI(args: string[]): void {
|
|
236
|
+
const title = parseArg(args, '--title') ?? 'Fortress Auth API';
|
|
237
|
+
const version = parseArg(args, '--version') ?? '1.0.0';
|
|
238
|
+
const outFile = parseArg(args, '--out') ?? parseArg(args, '-o');
|
|
239
|
+
|
|
240
|
+
const allEndpoints = [
|
|
241
|
+
...Object.values(authEndpoints),
|
|
242
|
+
...Object.values(iamEndpoints),
|
|
243
|
+
];
|
|
244
|
+
const componentSchemas = { ...authComponentSchemas, ...iamComponentSchemas };
|
|
245
|
+
|
|
246
|
+
const spec = buildOpenAPISpec(allEndpoints, componentSchemas, { title, version });
|
|
247
|
+
const json = JSON.stringify(spec, null, 2);
|
|
248
|
+
|
|
249
|
+
if (outFile) {
|
|
250
|
+
writeFileSync(outFile, json, 'utf-8');
|
|
251
|
+
console.log(`OpenAPI spec written to ${outFile}`);
|
|
252
|
+
const pathCount = Object.keys(spec.paths).length;
|
|
253
|
+
console.log(` ${pathCount} endpoints, OpenAPI 3.1.0`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
process.stdout.write(json);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildCoreFortressForCli(): { endpoints: EndpointDefinition[]; config: { plugins: []; csrf: undefined }; readonly manifest: ReturnType<typeof buildRouteManifest> } {
|
|
261
|
+
const endpoints = [
|
|
262
|
+
...Object.values(authEndpoints) as EndpointDefinition[],
|
|
263
|
+
...Object.values(iamEndpoints) as EndpointDefinition[],
|
|
264
|
+
];
|
|
265
|
+
return {
|
|
266
|
+
endpoints,
|
|
267
|
+
config: { plugins: [], csrf: undefined },
|
|
268
|
+
get manifest() {
|
|
269
|
+
return buildRouteManifest(this as any);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function cmdManifest(args: string[]): void {
|
|
275
|
+
const outFile = parseArg(args, '--out') ?? parseArg(args, '-o');
|
|
276
|
+
const fortress = buildCoreFortressForCli();
|
|
277
|
+
const manifest = buildRouteManifest(fortress as any);
|
|
278
|
+
const json = JSON.stringify(manifest, null, 2);
|
|
279
|
+
|
|
280
|
+
if (outFile) {
|
|
281
|
+
writeFileSync(outFile, json, 'utf-8');
|
|
282
|
+
console.log(`Route manifest written to ${outFile}`);
|
|
283
|
+
console.log(` ${manifest.length} route(s)`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
process.stdout.write(json);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cmdManifestCheck(): void {
|
|
291
|
+
const fortress = buildCoreFortressForCli();
|
|
292
|
+
const openapi = buildOpenAPISpec(
|
|
293
|
+
fortress.endpoints,
|
|
294
|
+
{ ...authComponentSchemas, ...iamComponentSchemas },
|
|
295
|
+
{ title: 'Fortress Auth API', version: '1.0.0' },
|
|
296
|
+
);
|
|
297
|
+
const drift = detectRouteManifestDrift(fortress as any, { openapi });
|
|
298
|
+
|
|
299
|
+
if (hasRouteManifestDrift(drift)) {
|
|
300
|
+
console.error('Route manifest drift detected:');
|
|
301
|
+
console.error(JSON.stringify(drift, null, 2));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log('Route manifest check passed.');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function cmdCheckPublicRoutes(args: string[]): void {
|
|
309
|
+
// Optional `--allow <method> <path>` repeated entries augment the
|
|
310
|
+
// default Fortress allow-list. For app-level checks (with plugins
|
|
311
|
+
// mounted), call `checkPublicRoutes(fortress)` from your CI script
|
|
312
|
+
// against your real fortress instance.
|
|
313
|
+
const allow: string[] = [];
|
|
314
|
+
for (let i = 0; i < args.length; i++) {
|
|
315
|
+
if (args[i] === '--allow' && args[i + 1])
|
|
316
|
+
allow.push(args[++i]);
|
|
317
|
+
}
|
|
318
|
+
const fortress = buildCoreFortressForCli();
|
|
319
|
+
const result = checkPublicRoutes(fortress as any, { allow });
|
|
320
|
+
if (!result.ok) {
|
|
321
|
+
console.error('Public-route check failed:');
|
|
322
|
+
for (const msg of result.messages)
|
|
323
|
+
console.error(` - ${msg}`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
console.log(`Public-route check passed (${result.unexpected.length} unexpected, allow-list ok).`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function cmdPolicySummary(args: string[]): Promise<void> {
|
|
330
|
+
const filePath = parseArg(args, '--file') ?? parseArg(args, '-f');
|
|
331
|
+
const env = parseArg(args, '--env');
|
|
332
|
+
const resolved = await resolvePolicyPath({ filePath, env });
|
|
333
|
+
if (!resolved) {
|
|
334
|
+
console.error('No fortress.policy.json (or fortress.policy.<env>.json) found in the current directory.');
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const { policy, filePath: loaded } = await loadPolicy({ filePath, env });
|
|
338
|
+
console.log(`Policy file: ${loaded}`);
|
|
339
|
+
console.log(` resources: ${(policy.resources ?? []).length}`);
|
|
340
|
+
console.log(` roles: ${(policy.roles ?? []).length}`);
|
|
341
|
+
console.log(` groups: ${(policy.groups ?? []).length}`);
|
|
342
|
+
console.log(` serviceAccounts: ${(policy.serviceAccounts ?? []).length}`);
|
|
343
|
+
for (const role of policy.roles ?? []) {
|
|
344
|
+
console.log(` role ${role.name}: ${role.permissions.length} permission(s)`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function cmdPolicyHowto(name: 'diff' | 'apply' | 'check'): void {
|
|
349
|
+
console.log(`policy:${name} requires a live database connection.\n`);
|
|
350
|
+
console.log('Run from your application code instead:\n');
|
|
351
|
+
console.log(' import { createFortress } from \'@bajustone/fortress\';');
|
|
352
|
+
console.log(' import {');
|
|
353
|
+
console.log(' loadPolicy, diffPolicy, applyPolicyPlan,');
|
|
354
|
+
console.log(' } from \'@bajustone/fortress\';');
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(' const fortress = createFortress(config);');
|
|
357
|
+
console.log(' const { policy } = await loadPolicy();');
|
|
358
|
+
console.log(' const plan = await diffPolicy(policy, fortress.iam);');
|
|
359
|
+
if (name === 'diff') {
|
|
360
|
+
console.log(' console.log(plan.ops);');
|
|
361
|
+
}
|
|
362
|
+
else if (name === 'apply') {
|
|
363
|
+
console.log(' const result = await applyPolicyPlan(plan, fortress.iam);');
|
|
364
|
+
console.log(' if (result.errors.length) throw new Error(JSON.stringify(result.errors));');
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.log(' if (!plan.inSync) {');
|
|
368
|
+
console.log(' console.error(\'policy drift\', plan.ops);');
|
|
369
|
+
console.log(' process.exit(1);');
|
|
370
|
+
console.log(' }');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseDialect(args: string[]): 'sqlite' | 'pg' {
|
|
375
|
+
const dialect = parseArg(args, '--dialect') ?? 'sqlite';
|
|
376
|
+
if (dialect !== 'sqlite' && dialect !== 'pg') {
|
|
377
|
+
console.error(`Unsupported dialect '${dialect}'. Use sqlite or pg.`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
return dialect;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function writeOrPrint(content: string, args: string[], label: string): void {
|
|
384
|
+
const outFile = parseArg(args, '--out') ?? parseArg(args, '-o');
|
|
385
|
+
if (outFile) {
|
|
386
|
+
writeFileSync(outFile, content, 'utf-8');
|
|
387
|
+
console.log(`${label} written to ${outFile}`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
process.stdout.write(content);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function cmdMigrateStatus(args: string[]): void {
|
|
395
|
+
const dialect = parseDialect(args);
|
|
396
|
+
const migrations = getFortressMigrations(dialect);
|
|
397
|
+
const body = {
|
|
398
|
+
dialect,
|
|
399
|
+
latestVersion: getLatestMigrationVersion(dialect),
|
|
400
|
+
bundledMigrations: migrations.map(migration => ({
|
|
401
|
+
version: migration.version,
|
|
402
|
+
name: migration.name,
|
|
403
|
+
})),
|
|
404
|
+
note: 'For live database status, call getMigrationStatus(databaseAdapter) from application code so Fortress can use your configured connection.',
|
|
405
|
+
};
|
|
406
|
+
writeOrPrint(`${JSON.stringify(body, null, 2)}\n`, args, 'Migration status');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function cmdMigrateUp(args: string[]): void {
|
|
410
|
+
const dialect = parseDialect(args);
|
|
411
|
+
const migrations = getFortressMigrations(dialect);
|
|
412
|
+
const dataMigration = migrations.find(migration => migration.dataStep);
|
|
413
|
+
if (dataMigration) {
|
|
414
|
+
console.error(
|
|
415
|
+
`Cannot export SQL-only migrations: ${dataMigration.name} includes the Unicode-aware data step '${dataMigration.dataStep}'. `
|
|
416
|
+
+ 'Run fortress.migrate() with your DatabaseAdapter so cleanup and constraints execute atomically.',
|
|
417
|
+
);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const sql = migrations
|
|
421
|
+
.map(migration => `-- ${String(migration.version).padStart(4, '0')}_${migration.name}.sql\n${migration.up}`)
|
|
422
|
+
.join('\n\n');
|
|
423
|
+
writeOrPrint(`${sql}\n`, args, 'Migration SQL');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function cmdMigrateDown(args: string[]): void {
|
|
427
|
+
const dialect = parseDialect(args);
|
|
428
|
+
const sql = getFortressMigrations(dialect)
|
|
429
|
+
.sort((a, b) => b.version - a.version)
|
|
430
|
+
.map(migration => `-- ${String(migration.version).padStart(4, '0')}_${migration.name}.down.sql\n${migration.down}`)
|
|
431
|
+
.join('\n\n');
|
|
432
|
+
writeOrPrint(`${sql}\n`, args, 'Rollback SQL');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function cmdMigrateDiff(): void {
|
|
436
|
+
console.log('Live migration drift checks require your application database adapter.');
|
|
437
|
+
console.log('Use this in application or CI code:');
|
|
438
|
+
console.log(' import { detectMigrationDrift, hasMigrationDrift } from \'@bajustone/fortress\';');
|
|
439
|
+
console.log(' const drift = await detectMigrationDrift(fortress.config.database);');
|
|
440
|
+
console.log(' if (hasMigrationDrift(drift)) process.exit(1);');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function cmdMigrateCheck(args: string[]): void {
|
|
444
|
+
const dialect = parseDialect(args);
|
|
445
|
+
const migrations = getFortressMigrations(dialect);
|
|
446
|
+
const versions = new Set<number>();
|
|
447
|
+
for (const migration of migrations) {
|
|
448
|
+
if (versions.has(migration.version)) {
|
|
449
|
+
console.error(`Duplicate migration version ${migration.version}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
versions.add(migration.version);
|
|
453
|
+
if (!migration.up.trim() || !migration.down.trim()) {
|
|
454
|
+
console.error(`Migration ${migration.version} (${migration.name}) is missing up/down SQL`);
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
console.log(`Migration catalog check passed (${dialect}, latest=${getLatestMigrationVersion(dialect)}).`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function cmdSchemas(args: string[]): void {
|
|
462
|
+
const format = parseArg(args, '--format') ?? 'json-schema';
|
|
463
|
+
const outFile = parseArg(args, '--out') ?? parseArg(args, '-o');
|
|
464
|
+
|
|
465
|
+
if (format === 'json-schema') {
|
|
466
|
+
const allSchemas = { ...authComponentSchemas, ...iamComponentSchemas };
|
|
467
|
+
const json = JSON.stringify(allSchemas, null, 2);
|
|
468
|
+
|
|
469
|
+
if (outFile) {
|
|
470
|
+
writeFileSync(outFile, json, 'utf-8');
|
|
471
|
+
console.log(`JSON Schema written to ${outFile}`);
|
|
472
|
+
console.log(` ${Object.keys(allSchemas).length} component schemas`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
process.stdout.write(json);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (format === 'zod') {
|
|
481
|
+
const allSchemas = { ...authComponentSchemas, ...iamComponentSchemas };
|
|
482
|
+
const allEndpoints = [
|
|
483
|
+
...Object.values(authEndpoints),
|
|
484
|
+
...Object.values(iamEndpoints),
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
let output = '// Auto-generated by "fortress schemas --format zod" — do not edit manually\n';
|
|
488
|
+
output += '// Requires: zod\n\n';
|
|
489
|
+
output += 'import { z } from \'zod\';\n\n';
|
|
490
|
+
|
|
491
|
+
// Generate component schemas
|
|
492
|
+
output += '// ── Component Schemas ─────────────────────────────────────────────\n\n';
|
|
493
|
+
for (const [name, schema] of Object.entries(allSchemas)) {
|
|
494
|
+
output += `export const ${name}Schema = ${jsonSchemaToZodCodegen(schema as any)};\n\n`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Generate endpoint input schemas
|
|
498
|
+
output += '// ── Endpoint Input Schemas ────────────────────────────────────────\n\n';
|
|
499
|
+
for (const ep of allEndpoints) {
|
|
500
|
+
if (ep.input?.body) {
|
|
501
|
+
const handlerName = ep.handler.charAt(0).toUpperCase() + ep.handler.slice(1);
|
|
502
|
+
output += `export const ${handlerName}BodySchema = ${jsonSchemaToZodCodegen(ep.input.body)};\n\n`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (outFile) {
|
|
507
|
+
writeFileSync(outFile, output, 'utf-8');
|
|
508
|
+
console.log(`Zod schemas written to ${outFile}`);
|
|
509
|
+
console.log(` ${Object.keys(allSchemas).length} component schemas, ${allEndpoints.filter((e: any) => e.input?.body).length} body schemas`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
process.stdout.write(output);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.error(`Unknown format: ${format}. Supported: json-schema, zod`);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Convert a JSON Schema object to Zod code string (codegen, not runtime). */
|
|
522
|
+
function jsonSchemaToZodCodegen(schema: any): string {
|
|
523
|
+
if (schema.$ref) {
|
|
524
|
+
const name = schema.$ref.replace('#/components/schemas/', '');
|
|
525
|
+
return `${name}Schema`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (schema.oneOf) {
|
|
529
|
+
const variants = schema.oneOf.map((s: any) => jsonSchemaToZodCodegen(s));
|
|
530
|
+
return `z.union([${variants.join(', ')}])`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (schema.anyOf) {
|
|
534
|
+
const variants = schema.anyOf.map((s: any) => jsonSchemaToZodCodegen(s));
|
|
535
|
+
return `z.union([${variants.join(', ')}])`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (schema.enum) {
|
|
539
|
+
const values = schema.enum.map((v: any) => JSON.stringify(v));
|
|
540
|
+
return `z.enum([${values.join(', ')}])`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
switch (schema.type) {
|
|
544
|
+
case 'string': {
|
|
545
|
+
let s = 'z.string()';
|
|
546
|
+
if (schema.format === 'email')
|
|
547
|
+
s += '.email()';
|
|
548
|
+
if (schema.format === 'uri')
|
|
549
|
+
s += '.url()';
|
|
550
|
+
if (schema.minLength)
|
|
551
|
+
s += `.min(${schema.minLength})`;
|
|
552
|
+
if (schema.maxLength)
|
|
553
|
+
s += `.max(${schema.maxLength})`;
|
|
554
|
+
if (schema.nullable)
|
|
555
|
+
s += '.nullable()';
|
|
556
|
+
return s;
|
|
557
|
+
}
|
|
558
|
+
case 'number':
|
|
559
|
+
return schema.nullable ? 'z.number().nullable()' : 'z.number()';
|
|
560
|
+
case 'integer':
|
|
561
|
+
return schema.nullable ? 'z.number().int().nullable()' : 'z.number().int()';
|
|
562
|
+
case 'boolean':
|
|
563
|
+
return schema.nullable ? 'z.boolean().nullable()' : 'z.boolean()';
|
|
564
|
+
case 'null':
|
|
565
|
+
return 'z.null()';
|
|
566
|
+
case 'array': {
|
|
567
|
+
const items = schema.items ? jsonSchemaToZodCodegen(schema.items) : 'z.any()';
|
|
568
|
+
return `z.array(${items})`;
|
|
569
|
+
}
|
|
570
|
+
case 'object': {
|
|
571
|
+
if (!schema.properties) {
|
|
572
|
+
return schema.additionalProperties ? 'z.record(z.string(), z.any())' : 'z.object({})';
|
|
573
|
+
}
|
|
574
|
+
const required = new Set(schema.required ?? []);
|
|
575
|
+
const fields = Object.entries(schema.properties).map(([key, propSchema]: [string, any]) => {
|
|
576
|
+
const zodType = jsonSchemaToZodCodegen(propSchema);
|
|
577
|
+
return required.has(key) ? ` ${key}: ${zodType}` : ` ${key}: ${zodType}.optional()`;
|
|
578
|
+
});
|
|
579
|
+
return `z.object({\n${fields.join(',\n')},\n})`;
|
|
580
|
+
}
|
|
581
|
+
default:
|
|
582
|
+
return 'z.any()';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// --- Main ---
|
|
587
|
+
|
|
588
|
+
const args = process.argv.slice(2);
|
|
589
|
+
const command = args[0];
|
|
590
|
+
|
|
591
|
+
if (!command || command === '--help' || command === '-h') {
|
|
592
|
+
console.log(HELP_TEXT);
|
|
593
|
+
process.exit(0);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
switch (command) {
|
|
597
|
+
case 'init':
|
|
598
|
+
cmdInit();
|
|
599
|
+
break;
|
|
600
|
+
case 'sync:push':
|
|
601
|
+
cmdSyncPush();
|
|
602
|
+
break;
|
|
603
|
+
case 'sync:pull':
|
|
604
|
+
cmdSyncPull();
|
|
605
|
+
break;
|
|
606
|
+
case 'sync:types':
|
|
607
|
+
cmdSyncTypes();
|
|
608
|
+
break;
|
|
609
|
+
case 'generate-secret':
|
|
610
|
+
cmdGenerateSecret();
|
|
611
|
+
break;
|
|
612
|
+
case 'openapi':
|
|
613
|
+
cmdOpenAPI(args.slice(1));
|
|
614
|
+
break;
|
|
615
|
+
case 'schemas':
|
|
616
|
+
cmdSchemas(args.slice(1));
|
|
617
|
+
break;
|
|
618
|
+
case 'manifest':
|
|
619
|
+
cmdManifest(args.slice(1));
|
|
620
|
+
break;
|
|
621
|
+
case 'manifest:check':
|
|
622
|
+
cmdManifestCheck();
|
|
623
|
+
break;
|
|
624
|
+
case 'migrate:status':
|
|
625
|
+
cmdMigrateStatus(args.slice(1));
|
|
626
|
+
break;
|
|
627
|
+
case 'migrate:up':
|
|
628
|
+
cmdMigrateUp(args.slice(1));
|
|
629
|
+
break;
|
|
630
|
+
case 'migrate:down':
|
|
631
|
+
cmdMigrateDown(args.slice(1));
|
|
632
|
+
break;
|
|
633
|
+
case 'migrate:diff':
|
|
634
|
+
cmdMigrateDiff();
|
|
635
|
+
break;
|
|
636
|
+
case 'migrate:check':
|
|
637
|
+
cmdMigrateCheck(args.slice(1));
|
|
638
|
+
break;
|
|
639
|
+
case 'check:routes':
|
|
640
|
+
cmdManifestCheck();
|
|
641
|
+
break;
|
|
642
|
+
case 'check:public-routes':
|
|
643
|
+
cmdCheckPublicRoutes(args.slice(1));
|
|
644
|
+
break;
|
|
645
|
+
case 'check:migrations':
|
|
646
|
+
cmdMigrateCheck(args.slice(1));
|
|
647
|
+
break;
|
|
648
|
+
case 'policy:summary':
|
|
649
|
+
void cmdPolicySummary(args.slice(1)).catch((err) => {
|
|
650
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
651
|
+
process.exitCode = 1;
|
|
652
|
+
});
|
|
653
|
+
break;
|
|
654
|
+
case 'policy:diff':
|
|
655
|
+
cmdPolicyHowto('diff');
|
|
656
|
+
break;
|
|
657
|
+
case 'policy:apply':
|
|
658
|
+
cmdPolicyHowto('apply');
|
|
659
|
+
break;
|
|
660
|
+
case 'policy:check':
|
|
661
|
+
cmdPolicyHowto('check');
|
|
662
|
+
break;
|
|
663
|
+
default:
|
|
664
|
+
console.error(`Unknown command: ${command}`);
|
|
665
|
+
console.error('Run "fortress --help" for usage information.');
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|