@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
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Hono app using Fortress
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates EVERY feature Fortress offers:
|
|
5
|
+
*
|
|
6
|
+
* Core: registration, login, logout, token refresh, sessions, impersonation,
|
|
7
|
+
* RBAC (roles, groups, permissions), password policy
|
|
8
|
+
*
|
|
9
|
+
* Plugins: rate-limit, account-lockout, email-verification, two-factor,
|
|
10
|
+
* magic-link, api-key, social-login, tenancy, data-isolation,
|
|
11
|
+
* audit-log, webhook, oauth
|
|
12
|
+
*
|
|
13
|
+
* Middleware: auth, RBAC, CSRF, security headers
|
|
14
|
+
*
|
|
15
|
+
* Helpers: getUserId, getClaims, getDb, getScopedDb
|
|
16
|
+
*
|
|
17
|
+
* Run: bun run dev
|
|
18
|
+
* Or: bun run examples/hono-app/index.ts
|
|
19
|
+
*/
|
|
20
|
+
import { Hono } from 'hono';
|
|
21
|
+
import { createFortress, email, endpoint, int, obj, str, strict } from '../../src';
|
|
22
|
+
import {
|
|
23
|
+
convertRoutes,
|
|
24
|
+
createCsrfMiddleware,
|
|
25
|
+
createHonoMiddleware,
|
|
26
|
+
createSecurityHeadersMiddleware,
|
|
27
|
+
getClaims,
|
|
28
|
+
getDb,
|
|
29
|
+
getScopedDb,
|
|
30
|
+
getSubject,
|
|
31
|
+
getUserId,
|
|
32
|
+
mountFortress,
|
|
33
|
+
protectedRoute,
|
|
34
|
+
vBody,
|
|
35
|
+
vParam,
|
|
36
|
+
vQuery,
|
|
37
|
+
} from '../../src/hono';
|
|
38
|
+
import { accountLockout } from '../../src/plugins/account-lockout';
|
|
39
|
+
import { admin } from '../../src/plugins/admin';
|
|
40
|
+
import { apiKey } from '../../src/plugins/api-key';
|
|
41
|
+
import { auditLog } from '../../src/plugins/audit-log';
|
|
42
|
+
import { dataIsolation } from '../../src/plugins/data-isolation';
|
|
43
|
+
import { emailVerification } from '../../src/plugins/email-verification';
|
|
44
|
+
import { magicLink } from '../../src/plugins/magic-link';
|
|
45
|
+
import { oauth } from '../../src/plugins/oauth';
|
|
46
|
+
import { openapi } from '../../src/plugins/openapi';
|
|
47
|
+
import { rateLimit } from '../../src/plugins/rate-limit';
|
|
48
|
+
import { honoRateLimit } from '../../src/plugins/rate-limit/hono';
|
|
49
|
+
import { socialLogin } from '../../src/plugins/social-login';
|
|
50
|
+
import { tenancy } from '../../src/plugins/tenancy';
|
|
51
|
+
import { twoFactor } from '../../src/plugins/two-factor';
|
|
52
|
+
import { builtinEvents, webhook } from '../../src/plugins/webhook';
|
|
53
|
+
import { createTestAdapter } from '../../src/testing';
|
|
54
|
+
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
// 1. Create Fortress instance with ALL plugins
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
|
|
59
|
+
const db = createTestAdapter();
|
|
60
|
+
|
|
61
|
+
const hostOwnedStatsEndpoint = endpoint('GET', '/api/host-owned-stats')
|
|
62
|
+
.summary('Host-owned route protected by Fortress metadata')
|
|
63
|
+
.description('Demonstrates protectedRoute(): this Hono handler is app-owned, but Fortress supplies auth, RBAC, plugin middleware, and validation from endpoint metadata.')
|
|
64
|
+
.tags('App')
|
|
65
|
+
.security('bearer')
|
|
66
|
+
.permission('fortress', 'viewResources')
|
|
67
|
+
.response(200, 'Host route stats', obj({ users: int(), subjectType: str() }, 'users', 'subjectType'))
|
|
68
|
+
.handler('hostOwnedStats')
|
|
69
|
+
.build();
|
|
70
|
+
|
|
71
|
+
// ── Observability (optional but demonstrated) ─────────────────────────────
|
|
72
|
+
// A minimal console-backed logger satisfies the FortressLogger interface.
|
|
73
|
+
// Drop in `pino()` or `fastify.log` for production.
|
|
74
|
+
const logger = {
|
|
75
|
+
level: 'info',
|
|
76
|
+
fatal: (...args: unknown[]): void => console.error('[fortress:fatal]', ...args),
|
|
77
|
+
error: (...args: unknown[]): void => console.error('[fortress:error]', ...args),
|
|
78
|
+
warn: (...args: unknown[]): void => console.warn('[fortress:warn]', ...args),
|
|
79
|
+
// eslint-disable-next-line no-console -- example-only; real apps pass pino()
|
|
80
|
+
info: (...args: unknown[]): void => console.info('[fortress:info]', ...args),
|
|
81
|
+
// eslint-disable-next-line no-console -- example-only; real apps pass pino()
|
|
82
|
+
debug: (...args: unknown[]): void => console.debug('[fortress:debug]', ...args),
|
|
83
|
+
// eslint-disable-next-line no-console -- example-only; real apps pass pino()
|
|
84
|
+
trace: (...args: unknown[]): void => console.debug('[fortress:trace]', ...args),
|
|
85
|
+
silent: (): void => {},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const fortress = createFortress({
|
|
89
|
+
jwt: {
|
|
90
|
+
key: 'dev-secret-minimum-32-bytes-long!',
|
|
91
|
+
// key: ['new-secret-32-bytes-minimum!!!!!!', 'old-secret-32-bytes-minimum!!!!!!'], // ← secret rotation
|
|
92
|
+
accessTokenExpirySeconds: 900,
|
|
93
|
+
refreshTokenExpirySeconds: 604800,
|
|
94
|
+
},
|
|
95
|
+
database: db,
|
|
96
|
+
passwordPolicy: { minLength: 10 },
|
|
97
|
+
logger,
|
|
98
|
+
// To wire OpenTelemetry metrics/spans, install @opentelemetry/api and:
|
|
99
|
+
//
|
|
100
|
+
// import { createOtelTelemetry } from '../../src/otel';
|
|
101
|
+
// observability: await createOtelTelemetry({ name: 'fortress-example' }),
|
|
102
|
+
//
|
|
103
|
+
// The adapter is the ONLY file in the repo that imports @opentelemetry/api
|
|
104
|
+
// (dynamically), so this example stays workerd/Deno-clean when not used.
|
|
105
|
+
|
|
106
|
+
// Plugin order matters — hooks run in array order
|
|
107
|
+
plugins: [
|
|
108
|
+
// ── Admin (IAM route protection + bootstrap) ──
|
|
109
|
+
admin({
|
|
110
|
+
apiKeyRoutes: true,
|
|
111
|
+
bootstrap: {
|
|
112
|
+
enabled: Boolean(process.env.FORTRESS_ADMIN_BOOTSTRAP_SECRET),
|
|
113
|
+
secret: process.env.FORTRESS_ADMIN_BOOTSTRAP_SECRET,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
// ── Gate plugins (reject early) ──
|
|
118
|
+
rateLimit({
|
|
119
|
+
login: { maxPerIp: 100, maxPerAccount: 10, windowSeconds: 60 },
|
|
120
|
+
register: { maxPerIp: 50, windowSeconds: 60 },
|
|
121
|
+
refresh: { maxPerIp: 120, windowSeconds: 60 },
|
|
122
|
+
oauthToken: { maxPerIp: 60, windowSeconds: 60 },
|
|
123
|
+
apiKeyIssue: { maxPerIp: 10, maxPerUser: 10, windowSeconds: 3600 },
|
|
124
|
+
// Named rules consumed by honoRateLimit() on user-owned routes.
|
|
125
|
+
rules: {
|
|
126
|
+
api: { maxPerIp: 200, maxPerUser: 1000, windowSeconds: 60 },
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
accountLockout({
|
|
130
|
+
maxFailedAttempts: 3,
|
|
131
|
+
lockoutDurationSeconds: 60,
|
|
132
|
+
escalation: true,
|
|
133
|
+
}),
|
|
134
|
+
emailVerification({
|
|
135
|
+
requireVerification: false, // set true in production
|
|
136
|
+
onSendVerification: async (email, token, userId) => {
|
|
137
|
+
console.warn(`[email-verification] userId=${userId} email=${email} token=${token}`);
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
// ── Auth enhancement plugins ──
|
|
142
|
+
twoFactor({
|
|
143
|
+
secretEncryptionKey: process.env.FORTRESS_TOTP_ENCRYPTION_KEY!,
|
|
144
|
+
totp: { issuer: 'Fortress Example' },
|
|
145
|
+
}),
|
|
146
|
+
magicLink({
|
|
147
|
+
onSendMagicLink: async (email, token) => {
|
|
148
|
+
console.warn(`[magic-link] email=${email} token=${token}`);
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
apiKey({ prefix: 'fortress', maxKeysPerSubject: 5, routes: true }),
|
|
152
|
+
// ^ `routes: true` mounts the self-service HTTP endpoints:
|
|
153
|
+
// POST /api-key/keys — create a key for the caller
|
|
154
|
+
// GET /api-key/keys — list the caller's keys
|
|
155
|
+
// DELETE /api-key/keys/:id — revoke one of the caller's keys
|
|
156
|
+
// POST /api-key/keys/:id/rotate — rotate one of the caller's keys
|
|
157
|
+
// The admin plugin above adds admin-side routes when `apiKeyRoutes: true`:
|
|
158
|
+
// GET /admin/users/:userId/api-keys — list any user's keys
|
|
159
|
+
// DELETE /admin/users/:userId/api-keys/:id — revoke any user's key
|
|
160
|
+
// Both require the `apiKey:manage` permission (auto-registered into
|
|
161
|
+
// `fortress-admin` via `/iam/admin/bootstrap`).
|
|
162
|
+
socialLogin({
|
|
163
|
+
tokenEncryptionKey: process.env.FORTRESS_SOCIAL_TOKEN_KEY ?? 'dev-social-token-key-32-bytes!!!',
|
|
164
|
+
providers: [
|
|
165
|
+
// Placeholder credentials — shows the config API but won't complete real OAuth flows
|
|
166
|
+
{ name: 'google', clientId: 'GOOGLE_CLIENT_ID', clientSecret: 'GOOGLE_SECRET' },
|
|
167
|
+
{ name: 'github', clientId: 'GITHUB_CLIENT_ID', clientSecret: 'GITHUB_SECRET' },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// ── Multi-tenancy & data isolation ──
|
|
172
|
+
tenancy(),
|
|
173
|
+
dataIsolation({
|
|
174
|
+
scopes: [{
|
|
175
|
+
name: 'org',
|
|
176
|
+
field: 'orgId',
|
|
177
|
+
models: ['document'],
|
|
178
|
+
resolveValue: async (userId, ctx) => {
|
|
179
|
+
const assignment = await ctx.db.findOne<{ scopeValue: string }>({
|
|
180
|
+
model: 'user_scope_assignment',
|
|
181
|
+
where: [
|
|
182
|
+
{ field: 'userId', operator: '=', value: userId },
|
|
183
|
+
{ field: 'scopeName', operator: '=', value: 'org' },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
// Missing assignments fail closed (FORBIDDEN) by default.
|
|
187
|
+
return assignment?.scopeValue ?? null;
|
|
188
|
+
},
|
|
189
|
+
}],
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
// ── Observability plugins (log last) ──
|
|
193
|
+
auditLog({ hashChain: true }),
|
|
194
|
+
webhook({
|
|
195
|
+
// Declare a custom event alongside the built-in auth events.
|
|
196
|
+
events: [...builtinEvents(), { name: 'order.shipped', schema: obj({ orderId: str() }, 'orderId') }],
|
|
197
|
+
delivery: {
|
|
198
|
+
// Demo transport: log instead of making a real outbound request. Omit
|
|
199
|
+
// `fetch` in production to use the SSRF-safe (IP-pinning) default.
|
|
200
|
+
fetch: async (req) => {
|
|
201
|
+
console.warn(`[webhook] → ${req.url}`, await req.text());
|
|
202
|
+
return new Response(null, { status: 200 });
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
// ── OAuth server ──
|
|
208
|
+
oauth({
|
|
209
|
+
issuerUrl: 'http://localhost:3000',
|
|
210
|
+
scopePermissionMap: {
|
|
211
|
+
'read:users': { resource: 'user', action: 'list' },
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
|
|
215
|
+
// ── Host-owned routes metadata ──
|
|
216
|
+
// protectedRoute() below uses this endpoint metadata for auth/RBAC and
|
|
217
|
+
// OpenAPI can include it alongside Fortress-owned endpoints.
|
|
218
|
+
{ name: 'host-routes', routes: { hostOwnedStats: hostOwnedStatsEndpoint } },
|
|
219
|
+
|
|
220
|
+
// ── OpenAPI (API docs) ──
|
|
221
|
+
// convertRoutes turns createRoute-style objects into EndpointDefinitions
|
|
222
|
+
// using your own schema converter (Zod, Valibot, TypeBox, etc.)
|
|
223
|
+
openapi({
|
|
224
|
+
title: 'Fortress Example API',
|
|
225
|
+
version: '0.0.15',
|
|
226
|
+
additionalEndpoints: convertRoutes(
|
|
227
|
+
[
|
|
228
|
+
// These would normally be imported from your route modules, e.g.:
|
|
229
|
+
// import { loginRoute, listUsersRoute } from './modules/auth/routes';
|
|
230
|
+
{
|
|
231
|
+
method: 'get',
|
|
232
|
+
path: '/health',
|
|
233
|
+
tags: ['App'],
|
|
234
|
+
summary: 'Health check',
|
|
235
|
+
responses: {
|
|
236
|
+
200: {
|
|
237
|
+
description: 'OK',
|
|
238
|
+
content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string' } } } } },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
{
|
|
244
|
+
// User brings their own converter, e.g. z.toJSONSchema for Zod v4
|
|
245
|
+
// Here we use identity since schemas are already JSON Schema
|
|
246
|
+
schemaConverter: s => s as Record<string, unknown>,
|
|
247
|
+
},
|
|
248
|
+
),
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── Demonstrate the observer surfaces ─────────────────────────────────────
|
|
254
|
+
// Auth observer: fires on login/failure/logout/register/refresh/token-reuse.
|
|
255
|
+
fortress.auth.addAuthObserver((event) => {
|
|
256
|
+
logger.info({ event }, `auth.${event.eventType}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// IAM observer: fires on role/permission/binding mutations.
|
|
260
|
+
fortress.iam.addIamObserver((event) => {
|
|
261
|
+
logger.info({ event }, `iam.${event.eventType}`);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Permission check observer: SYNCHRONOUS, hot path. Keep it bounded.
|
|
265
|
+
fortress.iam.addPermissionCheckObserver((event) => {
|
|
266
|
+
if (!event.allowed) {
|
|
267
|
+
logger.warn(
|
|
268
|
+
{ resource: event.resource, action: event.action, cached: event.cached },
|
|
269
|
+
'permission denied',
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Route-security manifest: the same generated inventory adapters use to
|
|
275
|
+
// decide which paths are Fortress-managed. Useful for CI drift checks or
|
|
276
|
+
// startup visibility.
|
|
277
|
+
logger.info(
|
|
278
|
+
{
|
|
279
|
+
routes: fortress.manifest.length,
|
|
280
|
+
rbacRoutes: fortress.manifest.filter(route => route.classification === 'rbac').length,
|
|
281
|
+
},
|
|
282
|
+
'fortress route manifest generated',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
286
|
+
// 2. Create Hono app + middleware
|
|
287
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
288
|
+
|
|
289
|
+
const app = new Hono();
|
|
290
|
+
|
|
291
|
+
const { authMiddleware, rbacMiddleware, errorHandler, pluginMiddleware } = createHonoMiddleware(fortress, {
|
|
292
|
+
routeMap: {
|
|
293
|
+
// User management
|
|
294
|
+
'GET /api/users': { resource: 'user', action: 'list' },
|
|
295
|
+
'POST /api/users': { resource: 'user', action: 'create' },
|
|
296
|
+
// Groups
|
|
297
|
+
'POST /api/groups': { resource: 'group', action: 'create' },
|
|
298
|
+
'POST /api/groups/:id/members': { resource: 'group', action: 'manage' },
|
|
299
|
+
'DELETE /api/groups/:id/members/:userId': { resource: 'group', action: 'manage' },
|
|
300
|
+
// Audit log
|
|
301
|
+
'GET /api/audit-log': { resource: 'audit-log', action: 'read' },
|
|
302
|
+
'POST /api/audit-log/verify-chain': { resource: 'audit-log', action: 'read' },
|
|
303
|
+
// Webhooks
|
|
304
|
+
'POST /api/webhooks': { resource: 'webhook', action: 'manage' },
|
|
305
|
+
'GET /api/webhooks': { resource: 'webhook', action: 'manage' },
|
|
306
|
+
'DELETE /api/webhooks/:id': { resource: 'webhook', action: 'manage' },
|
|
307
|
+
// Admin
|
|
308
|
+
'POST /admin/impersonate': { resource: 'user', action: 'impersonate' },
|
|
309
|
+
'GET /admin/lockout/:identifier': { resource: 'lockout', action: 'read' },
|
|
310
|
+
'POST /admin/lockout/:identifier/reset': { resource: 'lockout', action: 'manage' },
|
|
311
|
+
},
|
|
312
|
+
skipPaths: ['/health', '/auth/*', '/magic-link/*', '/email/*', '/social/*'],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Global middleware
|
|
316
|
+
app.use('*', createSecurityHeadersMiddleware());
|
|
317
|
+
app.use('*', createCsrfMiddleware({
|
|
318
|
+
skipPaths: ['/auth', '/oauth', '/magic-link', '/email', '/social', '/health'],
|
|
319
|
+
}));
|
|
320
|
+
app.onError(errorHandler);
|
|
321
|
+
|
|
322
|
+
// IAM routes: auth + admin plugin middleware (default deny via admin plugin)
|
|
323
|
+
app.use('/iam/*', authMiddleware);
|
|
324
|
+
app.use('/iam/*', pluginMiddleware.afterAuth);
|
|
325
|
+
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
+
// 3. Public routes
|
|
328
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
+
|
|
330
|
+
// curl http://localhost:3000/health
|
|
331
|
+
app.get('/health', c => c.json({ status: 'ok' }));
|
|
332
|
+
|
|
333
|
+
// ── Validated Request Helpers (vBody / vParam / vQuery) ──
|
|
334
|
+
// vBody/vParam/vQuery extract request data AND validate it at runtime against
|
|
335
|
+
// the supplied Standard Schema. On failure they throw FortressError with code
|
|
336
|
+
// VALIDATION_ERROR (HTTP 422) — the same shape every fortress-managed endpoint
|
|
337
|
+
// produces — so the registered Hono error handler formats it identically.
|
|
338
|
+
|
|
339
|
+
// `email()` enforces the email format at runtime (ReDoS-safe pattern); `strict()`
|
|
340
|
+
// rejects unknown body keys (no over-posting). Both come from the fetcher-backed
|
|
341
|
+
// schema builder re-exported at `@bajustone/fortress/fetcher`.
|
|
342
|
+
const RegisterBody = strict(obj({ email: email(), password: str({ min: 8 }), name: str() }, 'email', 'password', 'name'));
|
|
343
|
+
const LoginBody = obj({ identifier: str(), password: str() }, 'identifier', 'password');
|
|
344
|
+
|
|
345
|
+
// curl -X POST http://localhost:3000/auth/register -H 'Content-Type: application/json' \
|
|
346
|
+
// -d '{"email":"new@example.com","password":"MyPassword123!","name":"New User"}'
|
|
347
|
+
app.post('/auth/register', async (c) => {
|
|
348
|
+
const { email, password, name } = await vBody(c, RegisterBody);
|
|
349
|
+
const user = await fortress.auth.createUser({ email, password, name });
|
|
350
|
+
return c.json({ data: user }, 201);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// curl -X POST http://localhost:3000/auth/login -H 'Content-Type: application/json' \
|
|
354
|
+
// -d '{"identifier":"admin@example.com","password":"Password123!"}'
|
|
355
|
+
app.post('/auth/login', async (c) => {
|
|
356
|
+
const { identifier, password } = await vBody(c, LoginBody);
|
|
357
|
+
const result = await fortress.auth.login(identifier, password, {
|
|
358
|
+
ipAddress: c.req.header('x-forwarded-for'),
|
|
359
|
+
userAgent: c.req.header('user-agent'),
|
|
360
|
+
});
|
|
361
|
+
return c.json({ data: result });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ── vParam / vQuery demo ──
|
|
365
|
+
// curl 'http://localhost:3000/echo/42?greeting=hello' → 200 { id: '42', greeting: 'hello' }
|
|
366
|
+
// curl 'http://localhost:3000/echo/42' → 422 VALIDATION_ERROR (missing greeting)
|
|
367
|
+
const EchoParam = obj({ id: str('Echo ID') }, 'id');
|
|
368
|
+
const EchoQuery = obj({ greeting: str('Greeting') }, 'greeting');
|
|
369
|
+
app.get('/echo/:id', async (c) => {
|
|
370
|
+
const { id } = await vParam(c, EchoParam);
|
|
371
|
+
const { greeting } = await vQuery(c, EchoQuery);
|
|
372
|
+
return c.json({ id, greeting });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// curl -X POST http://localhost:3000/auth/refresh -H 'Content-Type: application/json' \
|
|
376
|
+
// -d '{"refreshToken":"<token>"}'
|
|
377
|
+
app.post('/auth/refresh', async (c) => {
|
|
378
|
+
const { refreshToken } = await c.req.json();
|
|
379
|
+
const result = await fortress.auth.refresh(refreshToken);
|
|
380
|
+
return c.json({ data: result });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// curl -X POST http://localhost:3000/auth/logout -H 'Content-Type: application/json' \
|
|
384
|
+
// -d '{"refreshToken":"<token>"}'
|
|
385
|
+
app.post('/auth/logout', async (c) => {
|
|
386
|
+
const { refreshToken } = await c.req.json();
|
|
387
|
+
await fortress.auth.logout(refreshToken);
|
|
388
|
+
return c.json({ data: { loggedOut: true } });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ── Magic Link (passwordless login) ──
|
|
392
|
+
|
|
393
|
+
// curl -X POST http://localhost:3000/magic-link/send -H 'Content-Type: application/json' \
|
|
394
|
+
// -d '{"email":"admin@example.com"}'
|
|
395
|
+
// (token is logged to console — copy it for the verify step)
|
|
396
|
+
app.post('/magic-link/send', async (c) => {
|
|
397
|
+
const { email } = await c.req.json();
|
|
398
|
+
const result = await fortress.plugins['magic-link'].sendMagicLink(email);
|
|
399
|
+
return c.json({ data: result });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// curl -X POST http://localhost:3000/magic-link/verify -H 'Content-Type: application/json' \
|
|
403
|
+
// -d '{"token":"<token-from-console>"}'
|
|
404
|
+
app.post('/magic-link/verify', async (c) => {
|
|
405
|
+
const { token } = await c.req.json();
|
|
406
|
+
const result = await fortress.plugins['magic-link'].verify(token);
|
|
407
|
+
return c.json({ data: result });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ── Email Verification ──
|
|
411
|
+
|
|
412
|
+
// curl -X POST http://localhost:3000/email/verify -H 'Content-Type: application/json' \
|
|
413
|
+
// -d '{"token":"<token-from-console>"}'
|
|
414
|
+
app.post('/email/verify', async (c) => {
|
|
415
|
+
const { token } = await c.req.json();
|
|
416
|
+
const result = await fortress.plugins['email-verification'].verify(token);
|
|
417
|
+
return c.json({ data: result });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── Social Login ──
|
|
421
|
+
|
|
422
|
+
// curl http://localhost:3000/social/providers
|
|
423
|
+
app.get('/social/providers', (c) => {
|
|
424
|
+
const providers = fortress.plugins['social-login'].getProviders();
|
|
425
|
+
return c.json({ data: providers });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// curl 'http://localhost:3000/social/authorize/google?redirect_uri=http://localhost:3000/callback'
|
|
429
|
+
// (returns authorization URL — in a real app you'd redirect the browser)
|
|
430
|
+
app.get('/social/authorize/:provider', async (c) => {
|
|
431
|
+
const provider = c.req.param('provider');
|
|
432
|
+
const redirectUri = c.req.query('redirect_uri') ?? 'http://localhost:3000/callback';
|
|
433
|
+
const result = await fortress.plugins['social-login'].getAuthorizationUrl(provider, redirectUri);
|
|
434
|
+
return c.json({ data: result });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
438
|
+
// 4. Protected auth routes (require JWT)
|
|
439
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
440
|
+
|
|
441
|
+
app.use('/auth/me', authMiddleware);
|
|
442
|
+
app.use('/auth/sessions/*', authMiddleware);
|
|
443
|
+
app.use('/auth/sessions', authMiddleware);
|
|
444
|
+
app.use('/auth/2fa/enable', authMiddleware);
|
|
445
|
+
app.use('/auth/2fa/confirm-setup', authMiddleware);
|
|
446
|
+
app.use('/auth/2fa/disable', authMiddleware);
|
|
447
|
+
app.use('/auth/email/*', authMiddleware);
|
|
448
|
+
app.use('/auth/api-keys/*', authMiddleware);
|
|
449
|
+
app.use('/auth/api-keys', authMiddleware);
|
|
450
|
+
app.use('/auth/social/*', authMiddleware);
|
|
451
|
+
|
|
452
|
+
// curl http://localhost:3000/auth/me -H 'Authorization: Bearer <token>'
|
|
453
|
+
app.get('/auth/me', async (c) => {
|
|
454
|
+
const userId = getUserId(c);
|
|
455
|
+
const claims = getClaims(c);
|
|
456
|
+
const user = await fortress.auth.me(userId);
|
|
457
|
+
return c.json({ data: { user, claims } });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ── Sessions ──
|
|
461
|
+
|
|
462
|
+
// curl http://localhost:3000/auth/sessions -H 'Authorization: Bearer <token>'
|
|
463
|
+
app.get('/auth/sessions', async (c) => {
|
|
464
|
+
const userId = getUserId(c);
|
|
465
|
+
const sessions = await fortress.auth.listSessions(userId);
|
|
466
|
+
return c.json({ data: sessions });
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// curl -X DELETE http://localhost:3000/auth/sessions/1 -H 'Authorization: Bearer <token>'
|
|
470
|
+
app.delete('/auth/sessions/:id', async (c) => {
|
|
471
|
+
const userId = getUserId(c);
|
|
472
|
+
const tokenId = c.req.param('id');
|
|
473
|
+
await fortress.auth.revokeSession(userId, tokenId);
|
|
474
|
+
return c.json({ data: { revoked: true } });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// curl -X DELETE http://localhost:3000/auth/sessions -H 'Authorization: Bearer <token>' \
|
|
478
|
+
// -H 'Content-Type: application/json' -d '{"currentTokenId":1}'
|
|
479
|
+
app.delete('/auth/sessions', async (c) => {
|
|
480
|
+
const userId = getUserId(c);
|
|
481
|
+
const { currentTokenId } = await c.req.json();
|
|
482
|
+
await fortress.auth.revokeAllOtherSessions(userId, currentTokenId);
|
|
483
|
+
return c.json({ data: { revokedOthers: true } });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ── Two-Factor Auth ──
|
|
487
|
+
|
|
488
|
+
// curl -X POST http://localhost:3000/auth/2fa/enable -H 'Authorization: Bearer <token>'
|
|
489
|
+
app.post('/auth/2fa/enable', async (c) => {
|
|
490
|
+
const userId = getUserId(c);
|
|
491
|
+
const result = await fortress.plugins['two-factor'].enable(userId);
|
|
492
|
+
return c.json({ data: { otpauthUrl: result.otpauthUrl, backupCodes: result.backupCodes } });
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// curl -X POST http://localhost:3000/auth/2fa/confirm-setup -H 'Authorization: Bearer <token>' \
|
|
496
|
+
// -H 'Content-Type: application/json' -d '{"code":"123456"}'
|
|
497
|
+
app.post('/auth/2fa/confirm-setup', async (c) => {
|
|
498
|
+
const userId = getUserId(c);
|
|
499
|
+
const { code } = await c.req.json();
|
|
500
|
+
const result = await fortress.plugins['two-factor'].confirmSetup(userId, code, {
|
|
501
|
+
userAgent: c.req.header('user-agent'),
|
|
502
|
+
});
|
|
503
|
+
return c.json({ data: result });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Pending-login completion is public: the single-use continuation is the credential.
|
|
507
|
+
// curl -X POST http://localhost:3000/auth/2fa/verify \
|
|
508
|
+
// -H 'Content-Type: application/json' \
|
|
509
|
+
// -d '{"continuationToken":"<from-pending-login>","code":"123456"}'
|
|
510
|
+
app.post('/auth/2fa/verify', async (c) => {
|
|
511
|
+
const { continuationToken, code } = await c.req.json();
|
|
512
|
+
const result = await fortress.plugins['two-factor'].verify(continuationToken, code, {
|
|
513
|
+
userAgent: c.req.header('user-agent'),
|
|
514
|
+
});
|
|
515
|
+
return c.json({ data: result });
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// curl -X POST http://localhost:3000/auth/2fa/disable -H 'Authorization: Bearer <token>'
|
|
519
|
+
app.post('/auth/2fa/disable', async (c) => {
|
|
520
|
+
const userId = getUserId(c);
|
|
521
|
+
await fortress.plugins['two-factor'].disable(userId);
|
|
522
|
+
return c.json({ data: { disabled: true } });
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ── Email Verification (resend) ──
|
|
526
|
+
|
|
527
|
+
// curl -X POST http://localhost:3000/auth/email/send-verification -H 'Authorization: Bearer <token>'
|
|
528
|
+
// (token is logged to console)
|
|
529
|
+
app.post('/auth/email/send-verification', async (c) => {
|
|
530
|
+
const userId = getUserId(c);
|
|
531
|
+
const result = await fortress.plugins['email-verification'].sendVerification(userId);
|
|
532
|
+
return c.json({ data: result });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ── API Keys ──
|
|
536
|
+
|
|
537
|
+
// curl -X POST http://localhost:3000/auth/api-keys -H 'Authorization: Bearer <token>' \
|
|
538
|
+
// -H 'Content-Type: application/json' -d '{"name":"My CI Key","scopes":["read:users"]}'
|
|
539
|
+
app.post('/auth/api-keys', async (c) => {
|
|
540
|
+
const userId = getUserId(c);
|
|
541
|
+
const { name, scopes } = await c.req.json();
|
|
542
|
+
const result = await fortress.plugins['api-key'].createKey({ subject: { type: 'USER', id: userId }, name, scopes });
|
|
543
|
+
return c.json({ data: result }, 201);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// curl http://localhost:3000/auth/api-keys -H 'Authorization: Bearer <token>'
|
|
547
|
+
app.get('/auth/api-keys', async (c) => {
|
|
548
|
+
const userId = getUserId(c);
|
|
549
|
+
const keys = await fortress.plugins['api-key'].listKeys({ subject: { type: 'USER', id: userId } });
|
|
550
|
+
return c.json({ data: keys });
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// curl -X DELETE http://localhost:3000/auth/api-keys/1 -H 'Authorization: Bearer <token>'
|
|
554
|
+
app.delete('/auth/api-keys/:id', async (c) => {
|
|
555
|
+
const userId = getUserId(c);
|
|
556
|
+
const keyId = c.req.param('id');
|
|
557
|
+
await fortress.plugins['api-key'].revokeKey({ subject: { type: 'USER', id: userId }, id: keyId });
|
|
558
|
+
return c.json({ data: { revoked: true } });
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// curl -X POST http://localhost:3000/auth/api-keys/1/rotate -H 'Authorization: Bearer <token>'
|
|
562
|
+
app.post('/auth/api-keys/:id/rotate', async (c) => {
|
|
563
|
+
const userId = getUserId(c);
|
|
564
|
+
const keyId = c.req.param('id');
|
|
565
|
+
const result = await fortress.plugins['api-key'].rotateKey({ subject: { type: 'USER', id: userId }, id: keyId });
|
|
566
|
+
return c.json({ data: result });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// ── Service Accounts ──
|
|
570
|
+
//
|
|
571
|
+
// Service accounts are first-class IAM principals — no sessions or passwords,
|
|
572
|
+
// authenticated by long-lived api keys. Typical use: CI/CD, devices, M2M.
|
|
573
|
+
// These endpoints are admin-facing (the admin user creates service accounts
|
|
574
|
+
// and mints keys for them). Once a key is issued, the service account sends
|
|
575
|
+
// it in `Authorization: ApiKey <key>` or `X-API-Key: <key>` — see
|
|
576
|
+
// `/deploy/run` below for the consuming side.
|
|
577
|
+
|
|
578
|
+
// curl -X POST http://localhost:3000/admin/service-accounts \
|
|
579
|
+
// -H 'Authorization: Bearer <admin-token>' \
|
|
580
|
+
// -H 'Content-Type: application/json' \
|
|
581
|
+
// -d '{"name":"ci-deploy","displayName":"CI Deploy"}'
|
|
582
|
+
app.post('/admin/service-accounts', async (c) => {
|
|
583
|
+
const { name, displayName, description } = await c.req.json();
|
|
584
|
+
const sa = await fortress.iam.createServiceAccount({ name, displayName, description });
|
|
585
|
+
return c.json({ data: sa }, 201);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Grant a service account a role.
|
|
589
|
+
// curl -X POST http://localhost:3000/admin/service-accounts/1/roles \
|
|
590
|
+
// -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
|
|
591
|
+
// -d '{"roleId":42}'
|
|
592
|
+
app.post('/admin/service-accounts/:id/roles', async (c) => {
|
|
593
|
+
const saId = c.req.param('id');
|
|
594
|
+
const { roleId, tenantId } = await c.req.json();
|
|
595
|
+
await fortress.iam.bindRoleToServiceAccount(saId, String(roleId), tenantId);
|
|
596
|
+
return c.json({ data: { bound: true } });
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Mint an api key for a service account. The raw key is returned exactly once;
|
|
600
|
+
// store it in your CI secrets store immediately.
|
|
601
|
+
// curl -X POST http://localhost:3000/admin/service-accounts/1/keys \
|
|
602
|
+
// -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
|
|
603
|
+
// -d '{"name":"ci-deploy-github-actions"}'
|
|
604
|
+
app.post('/admin/service-accounts/:id/keys', async (c) => {
|
|
605
|
+
const saId = c.req.param('id');
|
|
606
|
+
const { name, scopes, expiresAt } = await c.req.json();
|
|
607
|
+
const result = await fortress.plugins['api-key'].createKey({
|
|
608
|
+
subject: { type: 'SERVICE_ACCOUNT', id: saId },
|
|
609
|
+
name,
|
|
610
|
+
scopes,
|
|
611
|
+
expiresAt,
|
|
612
|
+
});
|
|
613
|
+
return c.json({ data: result }, 201);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Deactivate (kill-switch) or delete a service account.
|
|
617
|
+
// curl -X PATCH http://localhost:3000/admin/service-accounts/1 \
|
|
618
|
+
// -H 'Authorization: Bearer <admin-token>' -H 'Content-Type: application/json' \
|
|
619
|
+
// -d '{"isActive":false}'
|
|
620
|
+
app.patch('/admin/service-accounts/:id', async (c) => {
|
|
621
|
+
const saId = c.req.param('id');
|
|
622
|
+
const patch = await c.req.json();
|
|
623
|
+
const updated = await fortress.iam.updateServiceAccount(saId, patch);
|
|
624
|
+
return c.json({ data: updated });
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Hard delete (cascades to role bindings, direct permission bindings, and keys).
|
|
628
|
+
// curl -X DELETE http://localhost:3000/admin/service-accounts/1 \
|
|
629
|
+
// -H 'Authorization: Bearer <admin-token>'
|
|
630
|
+
app.delete('/admin/service-accounts/:id', async (c) => {
|
|
631
|
+
const saId = c.req.param('id');
|
|
632
|
+
await fortress.iam.deleteServiceAccount(saId);
|
|
633
|
+
return c.json({ data: { deleted: true } });
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// The consuming side — an endpoint a service account would call. This is
|
|
637
|
+
// mounted under the standard `authMiddleware`, so the api-key plugin's
|
|
638
|
+
// `resolvePrincipal` hook handles `Authorization: ApiKey <key>` /
|
|
639
|
+
// `X-API-Key` headers transparently. A JWT-authenticated browser session
|
|
640
|
+
// would work on exactly the same route.
|
|
641
|
+
//
|
|
642
|
+
// curl -X POST http://localhost:3000/deploy/run -H 'Authorization: ApiKey <key>'
|
|
643
|
+
app.post('/deploy/run', authMiddleware, async (c) => {
|
|
644
|
+
const subject = getSubject(c);
|
|
645
|
+
const allowed = await fortress.iam.checkPermission(subject, 'deploy', 'run');
|
|
646
|
+
if (!allowed)
|
|
647
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
648
|
+
|
|
649
|
+
// ... trigger the deploy ...
|
|
650
|
+
return c.json({ data: { deployId: 'dpl_abc123', startedBy: subject } });
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// ── Social Login (linked accounts) ──
|
|
654
|
+
|
|
655
|
+
// curl http://localhost:3000/auth/social/accounts -H 'Authorization: Bearer <token>'
|
|
656
|
+
app.get('/auth/social/accounts', async (c) => {
|
|
657
|
+
const userId = getUserId(c);
|
|
658
|
+
const accounts = await fortress.plugins['social-login'].getLinkedAccounts(userId);
|
|
659
|
+
return c.json({ data: accounts });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// curl -X DELETE http://localhost:3000/auth/social/accounts/google -H 'Authorization: Bearer <token>'
|
|
663
|
+
app.delete('/auth/social/accounts/:provider', async (c) => {
|
|
664
|
+
const userId = getUserId(c);
|
|
665
|
+
const provider = c.req.param('provider');
|
|
666
|
+
await fortress.plugins['social-login'].unlinkAccount(userId, provider);
|
|
667
|
+
return c.json({ data: { unlinked: true } });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
671
|
+
// 5. Tenant routes (require JWT)
|
|
672
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
673
|
+
|
|
674
|
+
app.use('/api/tenants/*', authMiddleware);
|
|
675
|
+
app.use('/api/tenants', authMiddleware);
|
|
676
|
+
|
|
677
|
+
// curl -X POST http://localhost:3000/api/tenants -H 'Authorization: Bearer <token>' \
|
|
678
|
+
// -H 'Content-Type: application/json' -d '{"name":"Acme Corp","taxId":"acme"}'
|
|
679
|
+
app.post('/api/tenants', async (c) => {
|
|
680
|
+
const { name, taxId, description } = await c.req.json();
|
|
681
|
+
const tenant = await fortress.plugins.tenancy.createTenant({ name, taxId, description });
|
|
682
|
+
return c.json({ data: tenant }, 201);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// curl http://localhost:3000/api/tenants -H 'Authorization: Bearer <token>'
|
|
686
|
+
app.get('/api/tenants', async (c) => {
|
|
687
|
+
const userId = getUserId(c);
|
|
688
|
+
const tenants = await fortress.plugins.tenancy.getUserTenants(userId);
|
|
689
|
+
return c.json({ data: tenants });
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// curl -X POST http://localhost:3000/api/tenants/switch -H 'Authorization: Bearer <token>' \
|
|
693
|
+
// -H 'Content-Type: application/json' -d '{"taxId":"acme"}'
|
|
694
|
+
app.post('/api/tenants/switch', async (c) => {
|
|
695
|
+
const userId = getUserId(c);
|
|
696
|
+
const { taxId } = await c.req.json();
|
|
697
|
+
// userId is derived server-side from the verified token, never the body.
|
|
698
|
+
await fortress.plugins.tenancy.switchTenant({ taxId, userId });
|
|
699
|
+
return c.json({ data: { switched: true } });
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// curl -X POST http://localhost:3000/api/tenants/1/members -H 'Authorization: Bearer <token>' \
|
|
703
|
+
// -H 'Content-Type: application/json' -d '{"userId":2}'
|
|
704
|
+
app.post('/api/tenants/:id/members', async (c) => {
|
|
705
|
+
const tenantId = c.req.param('id');
|
|
706
|
+
const { userId } = await c.req.json();
|
|
707
|
+
await fortress.plugins.tenancy.addUserToTenant(userId, tenantId);
|
|
708
|
+
return c.json({ data: { added: true } });
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
712
|
+
// 6. RBAC-protected routes (require JWT + permissions)
|
|
713
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
714
|
+
|
|
715
|
+
// Rate-limit all /api/* routes using the named 'api' rule defined above.
|
|
716
|
+
// Sits before authMiddleware so abusive unauthenticated hits are rejected
|
|
717
|
+
// early without the cost of principal resolution.
|
|
718
|
+
app.use('/api/*', honoRateLimit(fortress, 'api'));
|
|
719
|
+
|
|
720
|
+
app.use('/api/users', authMiddleware);
|
|
721
|
+
app.use('/api/groups/*', authMiddleware);
|
|
722
|
+
app.use('/api/groups', authMiddleware);
|
|
723
|
+
app.use('/api/users', rbacMiddleware);
|
|
724
|
+
app.use('/api/groups/*', rbacMiddleware);
|
|
725
|
+
app.use('/api/groups', rbacMiddleware);
|
|
726
|
+
|
|
727
|
+
// Host-owned route protected by endpoint metadata instead of an adapter routeMap.
|
|
728
|
+
// Runs the same core protections as fortress-managed routes, then calls this
|
|
729
|
+
// Hono handler. Register before mountFortress so the app-owned handler wins.
|
|
730
|
+
// curl http://localhost:3000/api/host-owned-stats -H 'Authorization: Bearer <token>'
|
|
731
|
+
app.get('/api/host-owned-stats', protectedRoute(fortress, hostOwnedStatsEndpoint, async (_c, ctx) => {
|
|
732
|
+
const users = await fortress.config.database.count({ model: 'user' });
|
|
733
|
+
return { users, subjectType: ctx.subject?.type ?? 'UNKNOWN' };
|
|
734
|
+
}));
|
|
735
|
+
|
|
736
|
+
// curl http://localhost:3000/api/users -H 'Authorization: Bearer <token>'
|
|
737
|
+
app.get('/api/users', async (c) => {
|
|
738
|
+
// getDb() returns the request-scoped adapter (tenant-aware when tenancy plugin is active)
|
|
739
|
+
const reqDb = getDb(c);
|
|
740
|
+
const users = await reqDb.findMany({ model: 'user' });
|
|
741
|
+
return c.json({ data: users });
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// curl -X POST http://localhost:3000/api/users -H 'Authorization: Bearer <token>' \
|
|
745
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
|
|
746
|
+
// -d '{"email":"new@example.com","name":"New User"}'
|
|
747
|
+
app.post('/api/users', async (c) => {
|
|
748
|
+
const body = await c.req.json();
|
|
749
|
+
const user = await fortress.auth.createUser({ ...body, password: 'TempPassword123!' });
|
|
750
|
+
return c.json({ data: user }, 201);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// ── Groups ──
|
|
754
|
+
|
|
755
|
+
// curl -X POST http://localhost:3000/api/groups -H 'Authorization: Bearer <token>' \
|
|
756
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
|
|
757
|
+
// -d '{"name":"engineering","description":"Engineering team"}'
|
|
758
|
+
app.post('/api/groups', async (c) => {
|
|
759
|
+
const { name, description } = await c.req.json();
|
|
760
|
+
const group = await fortress.iam.createGroup(name, description);
|
|
761
|
+
return c.json({ data: group }, 201);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// curl -X POST http://localhost:3000/api/groups/1/members -H 'Authorization: Bearer <token>' \
|
|
765
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"userId":2}'
|
|
766
|
+
app.post('/api/groups/:id/members', async (c) => {
|
|
767
|
+
const groupId = c.req.param('id');
|
|
768
|
+
const { userId } = await c.req.json();
|
|
769
|
+
await fortress.iam.addUserToGroup(groupId, userId);
|
|
770
|
+
return c.json({ data: { added: true } });
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// curl -X DELETE http://localhost:3000/api/groups/1/members/2 -H 'Authorization: Bearer <token>'
|
|
774
|
+
app.delete('/api/groups/:id/members/:userId', async (c) => {
|
|
775
|
+
const groupId = c.req.param('id');
|
|
776
|
+
const userId = c.req.param('userId');
|
|
777
|
+
await fortress.iam.removeUserFromGroup(groupId, userId);
|
|
778
|
+
return c.json({ data: { removed: true } });
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
782
|
+
// 7. Admin routes (require JWT + elevated permissions)
|
|
783
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
784
|
+
|
|
785
|
+
app.use('/api/audit-log/*', authMiddleware);
|
|
786
|
+
app.use('/api/audit-log', authMiddleware);
|
|
787
|
+
app.use('/api/webhooks/*', authMiddleware);
|
|
788
|
+
app.use('/api/webhooks', authMiddleware);
|
|
789
|
+
app.use('/admin/*', authMiddleware);
|
|
790
|
+
app.use('/api/audit-log/*', rbacMiddleware);
|
|
791
|
+
app.use('/api/audit-log', rbacMiddleware);
|
|
792
|
+
app.use('/api/webhooks/*', rbacMiddleware);
|
|
793
|
+
app.use('/api/webhooks', rbacMiddleware);
|
|
794
|
+
app.use('/admin/*', rbacMiddleware);
|
|
795
|
+
|
|
796
|
+
// ── Audit Log ──
|
|
797
|
+
|
|
798
|
+
// curl 'http://localhost:3000/api/audit-log?limit=20' -H 'Authorization: Bearer <token>'
|
|
799
|
+
app.get('/api/audit-log', async (c) => {
|
|
800
|
+
const options: Record<string, unknown> = {};
|
|
801
|
+
const userId = c.req.query('userId');
|
|
802
|
+
const eventType = c.req.query('eventType');
|
|
803
|
+
const limit = c.req.query('limit');
|
|
804
|
+
if (userId)
|
|
805
|
+
options.userId = Number(userId);
|
|
806
|
+
if (eventType)
|
|
807
|
+
options.eventType = eventType;
|
|
808
|
+
if (limit)
|
|
809
|
+
options.limit = Number(limit);
|
|
810
|
+
const entries = await fortress.plugins['audit-log'].getAuditLog(options);
|
|
811
|
+
return c.json({ data: entries });
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// curl -X POST http://localhost:3000/api/audit-log/verify-chain -H 'Authorization: Bearer <token>' \
|
|
815
|
+
// -H 'X-Fortress-CSRF: 1'
|
|
816
|
+
app.post('/api/audit-log/verify-chain', async (c) => {
|
|
817
|
+
const result = await fortress.plugins['audit-log'].verifyChain();
|
|
818
|
+
return c.json({ data: result });
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ── Impersonation ──
|
|
822
|
+
|
|
823
|
+
// curl -X POST http://localhost:3000/admin/impersonate -H 'Authorization: Bearer <token>' \
|
|
824
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"targetUserId":2}'
|
|
825
|
+
app.post('/admin/impersonate', async (c) => {
|
|
826
|
+
const adminId = getUserId(c);
|
|
827
|
+
const { targetUserId, reason } = await c.req.json();
|
|
828
|
+
const result = await fortress.auth.impersonate(adminId, targetUserId, { reason });
|
|
829
|
+
return c.json({ data: result });
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// ── Account Lockout ──
|
|
833
|
+
|
|
834
|
+
// curl http://localhost:3000/admin/lockout/user@example.com -H 'Authorization: Bearer <token>'
|
|
835
|
+
app.get('/admin/lockout/:identifier', async (c) => {
|
|
836
|
+
const identifier = c.req.param('identifier');
|
|
837
|
+
const status = await fortress.plugins['account-lockout'].getLockoutStatus(identifier);
|
|
838
|
+
return c.json({ data: status });
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// curl -X POST http://localhost:3000/admin/lockout/user@example.com/reset \
|
|
842
|
+
// -H 'Authorization: Bearer <token>' -H 'X-Fortress-CSRF: 1'
|
|
843
|
+
app.post('/admin/lockout/:identifier/reset', async (c) => {
|
|
844
|
+
const identifier = c.req.param('identifier');
|
|
845
|
+
await fortress.plugins['account-lockout'].resetLockout(identifier);
|
|
846
|
+
return c.json({ data: { reset: true } });
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ── Webhooks ──
|
|
850
|
+
|
|
851
|
+
// curl -X POST http://localhost:3000/api/webhooks -H 'Authorization: Bearer <token>' \
|
|
852
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' \
|
|
853
|
+
// -d '{"url":"https://example.com/hook","events":["auth.login.success","order.shipped"]}'
|
|
854
|
+
// A signing secret is generated and returned ONCE on the response — store it now.
|
|
855
|
+
app.post('/api/webhooks', async (c) => {
|
|
856
|
+
const { url, events, secret } = await c.req.json();
|
|
857
|
+
const endpoint = await fortress.plugins.webhook.registerEndpoint(url, events, secret ? { secret } : undefined);
|
|
858
|
+
return c.json({ data: endpoint }, 201);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// curl http://localhost:3000/api/webhooks -H 'Authorization: Bearer <token>'
|
|
862
|
+
app.get('/api/webhooks', async (c) => {
|
|
863
|
+
const endpoints = await fortress.plugins.webhook.listEndpoints();
|
|
864
|
+
return c.json({ data: endpoints });
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// curl -X DELETE http://localhost:3000/api/webhooks/1 -H 'Authorization: Bearer <token>'
|
|
868
|
+
app.delete('/api/webhooks/:id', async (c) => {
|
|
869
|
+
const id = c.req.param('id');
|
|
870
|
+
await fortress.plugins.webhook.removeEndpoint(id);
|
|
871
|
+
return c.json({ data: { removed: true } });
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Emit a custom event — delivered to every endpoint subscribed to 'order.shipped'.
|
|
875
|
+
// curl -X POST http://localhost:3000/api/events/order-shipped -H 'Authorization: Bearer <token>' \
|
|
876
|
+
// -H 'Content-Type: application/json' -H 'X-Fortress-CSRF: 1' -d '{"orderId":"o_123"}'
|
|
877
|
+
app.post('/api/events/order-shipped', async (c) => {
|
|
878
|
+
const { orderId } = await c.req.json();
|
|
879
|
+
await fortress.plugins.webhook.emit('order.shipped', { orderId }, { idempotencyKey: orderId });
|
|
880
|
+
return c.json({ data: { emitted: true } });
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// ── Data Isolation (demonstrates getScopedDb) ──
|
|
884
|
+
|
|
885
|
+
app.use('/api/documents', authMiddleware);
|
|
886
|
+
|
|
887
|
+
// curl http://localhost:3000/api/documents -H 'Authorization: Bearer <token>'
|
|
888
|
+
// getScopedDb() applies row-level isolation — queries are automatically filtered
|
|
889
|
+
// by the user's org scope (configured in the dataIsolation plugin above).
|
|
890
|
+
app.get('/api/documents', async (c) => {
|
|
891
|
+
const scopedDb = await getScopedDb(c, 'document');
|
|
892
|
+
const docs = await scopedDb.findMany({ model: 'document' });
|
|
893
|
+
return c.json({ data: docs });
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
897
|
+
// 8. Mount Fortress routes (auth, IAM, plugins, OAuth, OpenAPI)
|
|
898
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
899
|
+
//
|
|
900
|
+
// `mountFortress` registers a single Hono middleware that detects any
|
|
901
|
+
// Fortress-managed path (`/auth/*`, `/iam/*`, `/oauth/*`, `/openapi*`,
|
|
902
|
+
// plugin routes) and delegates to `fortress.handleRequest`. The custom
|
|
903
|
+
// `/auth/*` handlers above are registered FIRST, so Hono's first-match
|
|
904
|
+
// routing means they win — `mountFortress` only handles paths the user
|
|
905
|
+
// did not handle manually.
|
|
906
|
+
//
|
|
907
|
+
// Auth issuing endpoints (login/refresh/impersonate) inside core
|
|
908
|
+
// dispatch automatically attach `Set-Cookie` headers using
|
|
909
|
+
// `FortressConfig.cookies` defaults.
|
|
910
|
+
mountFortress(app, fortress);
|
|
911
|
+
|
|
912
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
913
|
+
// 9. Seed data on startup
|
|
914
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
915
|
+
|
|
916
|
+
async function seed(): Promise<void> {
|
|
917
|
+
// Create users
|
|
918
|
+
const admin = await fortress.auth.createUser({
|
|
919
|
+
email: 'admin@example.com',
|
|
920
|
+
password: 'Password123!',
|
|
921
|
+
name: 'Admin User',
|
|
922
|
+
});
|
|
923
|
+
const user = await fortress.auth.createUser({
|
|
924
|
+
email: 'user@example.com',
|
|
925
|
+
password: 'Password123!',
|
|
926
|
+
name: 'Regular User',
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Mark emails as verified
|
|
930
|
+
await db.update({ model: 'user', where: [{ field: 'id', operator: '=', value: admin.id }], data: { emailVerified: true } });
|
|
931
|
+
await db.update({ model: 'user', where: [{ field: 'id', operator: '=', value: user.id }], data: { emailVerified: true } });
|
|
932
|
+
|
|
933
|
+
// Bootstrap admin user — creates fortress-admin role with all IAM permissions
|
|
934
|
+
await fortress.plugins.admin.bootstrap({ userId: admin.id });
|
|
935
|
+
|
|
936
|
+
// Create app-specific roles with permissions
|
|
937
|
+
const adminRole = await fortress.iam.createRole('admin', [
|
|
938
|
+
{ resource: 'user', action: 'list' },
|
|
939
|
+
{ resource: 'user', action: 'create' },
|
|
940
|
+
{ resource: 'user', action: 'read' },
|
|
941
|
+
{ resource: 'user', action: 'impersonate' },
|
|
942
|
+
{ resource: 'group', action: 'create' },
|
|
943
|
+
{ resource: 'group', action: 'manage' },
|
|
944
|
+
{ resource: 'audit-log', action: 'read' },
|
|
945
|
+
{ resource: 'webhook', action: 'manage' },
|
|
946
|
+
{ resource: 'lockout', action: 'read' },
|
|
947
|
+
{ resource: 'lockout', action: 'manage' },
|
|
948
|
+
], 'Full admin access');
|
|
949
|
+
|
|
950
|
+
const viewerRole = await fortress.iam.createRole('viewer', [
|
|
951
|
+
{ resource: 'user', action: 'list' },
|
|
952
|
+
{ resource: 'user', action: 'read' },
|
|
953
|
+
], 'Read-only access');
|
|
954
|
+
|
|
955
|
+
// Bind roles
|
|
956
|
+
await fortress.iam.bindRole('USER', admin.id, adminRole.id);
|
|
957
|
+
await fortress.iam.bindRole('USER', user.id, viewerRole.id);
|
|
958
|
+
|
|
959
|
+
// Create a group and add admin
|
|
960
|
+
const engineering = await fortress.iam.createGroup('engineering', 'Engineering team');
|
|
961
|
+
await fortress.iam.addUserToGroup(engineering.id, admin.id);
|
|
962
|
+
|
|
963
|
+
// Create a tenant and add admin
|
|
964
|
+
const tenant = await fortress.plugins.tenancy.createTenant({ name: 'Acme Corp', taxId: 'acme' });
|
|
965
|
+
await fortress.plugins.tenancy.addUserToTenant(admin.id, tenant.id);
|
|
966
|
+
|
|
967
|
+
// Register a webhook endpoint
|
|
968
|
+
await fortress.plugins.webhook.registerEndpoint(
|
|
969
|
+
'https://example.com/webhooks',
|
|
970
|
+
['LOGIN_SUCCESS', 'REGISTER'],
|
|
971
|
+
'webhook-signing-secret',
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
// ── Typed in-process client ─────────────────────────────────────────
|
|
975
|
+
// `fortress.call.*` is the typed client surface. Each handler is
|
|
976
|
+
// inferred from its EndpointDefinition: input shape matches the body/
|
|
977
|
+
// query/params schemas, output matches the 2xx response schema.
|
|
978
|
+
//
|
|
979
|
+
// const { accessToken, user } = await fortress.call.login({
|
|
980
|
+
// identifier: 'admin@example.com',
|
|
981
|
+
// password: 'Password123!',
|
|
982
|
+
// });
|
|
983
|
+
//
|
|
984
|
+
// Under the hood, each call serializes to a Request, runs through
|
|
985
|
+
// fortress.handleRequest (full pipeline: middleware → token verify →
|
|
986
|
+
// RBAC → validation → dispatch), and parses the JSON body. Non-2xx
|
|
987
|
+
// responses throw a `FortressError` with the original `code`/`statusCode`.
|
|
988
|
+
// Same type surface a future over-the-wire client SDK will expose.
|
|
989
|
+
|
|
990
|
+
console.warn('');
|
|
991
|
+
console.warn('╔══════════════════════════════════════════════╗');
|
|
992
|
+
console.warn('║ Fortress Example App ║');
|
|
993
|
+
console.warn('╠══════════════════════════════════════════════╣');
|
|
994
|
+
console.warn('║ Admin: admin@example.com / Password123! ║');
|
|
995
|
+
console.warn('║ User: user@example.com / Password123! ║');
|
|
996
|
+
console.warn('║ Tenant: Acme Corp (taxId: acme) ║');
|
|
997
|
+
console.warn('║ Group: engineering ║');
|
|
998
|
+
console.warn('╠══════════════════════════════════════════════╣');
|
|
999
|
+
console.warn('║ http://localhost:3000 ║');
|
|
1000
|
+
console.warn('╚══════════════════════════════════════════════╝');
|
|
1001
|
+
console.warn('');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
seed().catch(console.error);
|
|
1005
|
+
|
|
1006
|
+
export default {
|
|
1007
|
+
port: 3000,
|
|
1008
|
+
fetch: app.fetch,
|
|
1009
|
+
};
|