@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,474 @@
|
|
|
1
|
+
import type { Column, SQL, Table } from 'drizzle-orm';
|
|
2
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
3
|
+
import type { WhereClause } from '../adapters/database/types';
|
|
4
|
+
|
|
5
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
|
+
import { and, eq, getTableColumns, gt, gte, inArray, isNull, like, lt, lte, ne, sql } from 'drizzle-orm';
|
|
7
|
+
import { Errors } from '../core/errors';
|
|
8
|
+
import { rethrowDbError } from './pg-error-map';
|
|
9
|
+
import { fortressPgSchema } from './pg/schema';
|
|
10
|
+
import { fortressSchema } from './schema';
|
|
11
|
+
|
|
12
|
+
export type DrizzleDialect = 'sqlite' | 'pg';
|
|
13
|
+
|
|
14
|
+
export interface DrizzleAdapterOptions {
|
|
15
|
+
/** Override default fortress table definitions with your own Drizzle tables */
|
|
16
|
+
tables?: Partial<Record<string, Table>>;
|
|
17
|
+
/** Database dialect — controls query execution and default table definitions (default: 'sqlite') */
|
|
18
|
+
dialect?: DrizzleDialect;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildTableMap(schema: typeof fortressSchema | typeof fortressPgSchema): Record<string, Table> {
|
|
22
|
+
return {
|
|
23
|
+
schema_version: schema.schemaVersion,
|
|
24
|
+
user: schema.users,
|
|
25
|
+
login_identifier: schema.loginIdentifiers,
|
|
26
|
+
refresh_token: schema.refreshTokens,
|
|
27
|
+
auth_continuation: schema.authContinuations,
|
|
28
|
+
group: schema.groups,
|
|
29
|
+
group_user: schema.groupUsers,
|
|
30
|
+
service_account: schema.serviceAccounts,
|
|
31
|
+
resource: schema.resources,
|
|
32
|
+
permission: schema.permissions,
|
|
33
|
+
role: schema.roles,
|
|
34
|
+
role_permission: schema.rolePermissions,
|
|
35
|
+
role_binding: schema.roleBindings,
|
|
36
|
+
direct_permission_binding: schema.directPermissionBindings,
|
|
37
|
+
email_verification_token: schema.emailVerificationTokens,
|
|
38
|
+
magic_link_token: schema.magicLinkTokens,
|
|
39
|
+
api_key: schema.apiKeys,
|
|
40
|
+
two_factor_secret: schema.twoFactorSecrets,
|
|
41
|
+
backup_code: schema.backupCodes,
|
|
42
|
+
trusted_device: schema.trustedDevices,
|
|
43
|
+
social_account: schema.socialAccounts,
|
|
44
|
+
tenant: schema.tenants,
|
|
45
|
+
tenant_user: schema.tenantUsers,
|
|
46
|
+
oauth_client: schema.oauthClients,
|
|
47
|
+
oauth_authorization_code: schema.oauthAuthorizationCodes,
|
|
48
|
+
oauth_access_token: schema.oauthAccessTokens,
|
|
49
|
+
oauth_refresh_token: schema.oauthRefreshTokens,
|
|
50
|
+
oauth_pending_flow: schema.oauthPendingFlows,
|
|
51
|
+
oauth_signing_key: schema.oauthSigningKeys,
|
|
52
|
+
user_scope_assignment: schema.userScopeAssignments,
|
|
53
|
+
account_lockout: schema.accountLockouts,
|
|
54
|
+
audit_log: schema.auditLogs,
|
|
55
|
+
audit_chain_state: schema.auditChainState,
|
|
56
|
+
webhook_endpoint: schema.webhookEndpoints,
|
|
57
|
+
webhook_delivery: schema.webhookDeliveries,
|
|
58
|
+
webauthn_credential: schema.webauthnCredentials,
|
|
59
|
+
webauthn_challenge: schema.webauthnChallenges,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SQLITE_DEFAULT_TABLE_MAP = buildTableMap(fortressSchema);
|
|
64
|
+
const PG_DEFAULT_TABLE_MAP = buildTableMap(fortressPgSchema);
|
|
65
|
+
|
|
66
|
+
function getColumn(table: Table, field: string): Column {
|
|
67
|
+
const columns = getTableColumns(table);
|
|
68
|
+
// Try exact match first
|
|
69
|
+
if (columns[field])
|
|
70
|
+
return columns[field];
|
|
71
|
+
|
|
72
|
+
// Convert snake_case field names to camelCase column references
|
|
73
|
+
const camelCase = field.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
74
|
+
if (columns[camelCase])
|
|
75
|
+
return columns[camelCase];
|
|
76
|
+
|
|
77
|
+
throw Errors.badRequest(`Unknown field: ${field} on table`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildWhereCondition(table: Table, where: WhereClause[]): SQL | undefined {
|
|
81
|
+
// A missing/empty where would compile to `.where(undefined)`, which matches
|
|
82
|
+
// EVERY row — a silent full-table update/delete footgun. findMany/count
|
|
83
|
+
// guard the empty case before calling (an unfiltered read is legal); the
|
|
84
|
+
// mutating paths (update/delete) and findOne pass their required where here
|
|
85
|
+
// directly, so rejecting empty here fails those closed.
|
|
86
|
+
if (where.length === 0)
|
|
87
|
+
throw Errors.badRequest('A non-empty where clause is required (empty where would match all rows)');
|
|
88
|
+
const conditions = where.map((clause) => {
|
|
89
|
+
const column = getColumn(table, clause.field);
|
|
90
|
+
|
|
91
|
+
switch (clause.operator) {
|
|
92
|
+
case '=':
|
|
93
|
+
return eq(column, clause.value as any);
|
|
94
|
+
case '!=':
|
|
95
|
+
return ne(column, clause.value as any);
|
|
96
|
+
case 'in':
|
|
97
|
+
return inArray(column, clause.value as any[]);
|
|
98
|
+
case 'gt':
|
|
99
|
+
return gt(column, clause.value as any);
|
|
100
|
+
case 'lt':
|
|
101
|
+
return lt(column, clause.value as any);
|
|
102
|
+
case 'gte':
|
|
103
|
+
return gte(column, clause.value as any);
|
|
104
|
+
case 'lte':
|
|
105
|
+
return lte(column, clause.value as any);
|
|
106
|
+
case 'like':
|
|
107
|
+
return like(column, clause.value as string);
|
|
108
|
+
case 'isNull':
|
|
109
|
+
return isNull(column);
|
|
110
|
+
default:
|
|
111
|
+
throw Errors.badRequest(`Unsupported operator: ${clause.operator}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return conditions.length === 1 ? conditions[0] : and(...conditions);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Replace undefined values with null for database compatibility. */
|
|
119
|
+
function sanitizeData(data: Record<string, unknown>): Record<string, unknown> {
|
|
120
|
+
const result: Record<string, unknown> = {};
|
|
121
|
+
for (const [key, value] of Object.entries(data)) {
|
|
122
|
+
result[key] = value === undefined ? null : value;
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Return true if a column name names a subject-id at the fortress API
|
|
129
|
+
* surface (RFC 7519 §4.1.2). Used to translate the database-native id type
|
|
130
|
+
* (commonly bigserial/integer) to fortress's opaque string-id contract on
|
|
131
|
+
* read — see {@link stringifyIds}.
|
|
132
|
+
*
|
|
133
|
+
* Matches:
|
|
134
|
+
* - `id` exactly (every model's primary key)
|
|
135
|
+
* - any camelCase field ending in `Id` (`userId`, `roleId`, `tenantId`, …)
|
|
136
|
+
*
|
|
137
|
+
* Does NOT match:
|
|
138
|
+
* - `tokenId`-like fields that are already strings in the schema (the
|
|
139
|
+
* transform is a no-op on strings, so this is safe)
|
|
140
|
+
* - timestamp fields (no `Id` suffix)
|
|
141
|
+
*/
|
|
142
|
+
const ID_FIELD_RE = /[a-z]Id$/;
|
|
143
|
+
function isIdField(key: string): boolean {
|
|
144
|
+
return key === 'id' || ID_FIELD_RE.test(key);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Recursively stringify every id-like field in a row. Pure function, runs
|
|
149
|
+
* on every read path (findOne, findMany, rawQuery, create.returning,
|
|
150
|
+
* update.returning, delete.returning) so consumers always see string ids
|
|
151
|
+
* regardless of whether the underlying column is `integer`, `bigserial`,
|
|
152
|
+
* `uuid`, or `text`.
|
|
153
|
+
*
|
|
154
|
+
* - Numbers and bigints ⇒ stringified via `String(...)`.
|
|
155
|
+
* - Strings ⇒ passed through unchanged.
|
|
156
|
+
* - `null`/`undefined` ⇒ passed through (nullable FKs).
|
|
157
|
+
* - Arrays and nested objects are walked (rawQuery results may nest).
|
|
158
|
+
*/
|
|
159
|
+
function stringifyIds<T>(row: T): T {
|
|
160
|
+
if (row == null || typeof row !== 'object')
|
|
161
|
+
return row;
|
|
162
|
+
if (Array.isArray(row))
|
|
163
|
+
return row.map(stringifyIds) as unknown as T;
|
|
164
|
+
const out: Record<string, unknown> = {};
|
|
165
|
+
for (const [key, value] of Object.entries(row as Record<string, unknown>)) {
|
|
166
|
+
if (isIdField(key) && (typeof value === 'number' || typeof value === 'bigint')) {
|
|
167
|
+
out[key] = String(value);
|
|
168
|
+
}
|
|
169
|
+
else if (value && typeof value === 'object' && !(value instanceof Date)) {
|
|
170
|
+
out[key] = stringifyIds(value);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
out[key] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out as T;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildRawSql(sqlText: string, params: unknown[] = []): SQL {
|
|
180
|
+
if (params.length === 0)
|
|
181
|
+
return sql.raw(sqlText);
|
|
182
|
+
|
|
183
|
+
if (sqlText.includes('?')) {
|
|
184
|
+
const parts = sqlText.split('?');
|
|
185
|
+
if (parts.length - 1 !== params.length) {
|
|
186
|
+
throw Errors.badRequest(`rawQuery placeholder count (${parts.length - 1}) does not match params (${params.length})`);
|
|
187
|
+
}
|
|
188
|
+
let query: SQL = sql.raw(parts[0]);
|
|
189
|
+
for (let i = 0; i < params.length; i++) {
|
|
190
|
+
query = sql`${query}${params[i]}${sql.raw(parts[i + 1])}`;
|
|
191
|
+
}
|
|
192
|
+
return query;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw Errors.badRequest(
|
|
196
|
+
'rawQuery uses ? positional placeholders on every dialect; params were provided but no ? placeholders were found',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeRawRows<T>(result: unknown): T[] {
|
|
201
|
+
if (Array.isArray(result))
|
|
202
|
+
return result as T[];
|
|
203
|
+
const rows = (result as { rows?: unknown })?.rows;
|
|
204
|
+
if (Array.isArray(rows))
|
|
205
|
+
return rows as T[];
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sqliteStatementReturnsRows(sqlText: string): boolean {
|
|
210
|
+
const normalized = sqlText.trimStart().toLowerCase();
|
|
211
|
+
return normalized.startsWith('select')
|
|
212
|
+
|| normalized.startsWith('with')
|
|
213
|
+
|| normalized.startsWith('pragma')
|
|
214
|
+
|| normalized.includes(' returning ');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Minimal Drizzle DB interface — accepts any Drizzle database instance.
|
|
219
|
+
* Drizzle DB types vary by dialect (BunSQLiteDatabase, PostgresJsDatabase, etc.)
|
|
220
|
+
* so we use a loose structural type rather than importing a specific one.
|
|
221
|
+
*/
|
|
222
|
+
// eslint-disable-next-line ts/no-unsafe-function-type -- Drizzle DB methods have dialect-specific signatures
|
|
223
|
+
interface DrizzleDB { insert: Function; select: Function; update: Function; delete: Function; transaction: Function }
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create a DatabaseAdapter backed by any Drizzle instance.
|
|
227
|
+
* Works with PostgreSQL and SQLite (bun:sqlite, better-sqlite3).
|
|
228
|
+
*
|
|
229
|
+
* @param db - Any Drizzle database instance
|
|
230
|
+
* @param options - Optional table overrides and dialect configuration
|
|
231
|
+
*/
|
|
232
|
+
export function createDrizzleAdapter(db: DrizzleDB, options?: DrizzleAdapterOptions): DatabaseAdapter {
|
|
233
|
+
const dialect = options?.dialect ?? 'sqlite';
|
|
234
|
+
const defaults = dialect === 'pg' ? PG_DEFAULT_TABLE_MAP : SQLITE_DEFAULT_TABLE_MAP;
|
|
235
|
+
const tableMap: Record<string, Table> = { ...defaults, ...(options?.tables as Record<string, Table>) };
|
|
236
|
+
const isSqlite = dialect === 'sqlite';
|
|
237
|
+
// SQLite drivers used by Drizzle (better-sqlite3 / bun:sqlite) expose a
|
|
238
|
+
// single synchronous connection. Manual async transactions must therefore
|
|
239
|
+
// be serialized: an `await` between BEGIN and COMMIT would otherwise allow a
|
|
240
|
+
// second request to issue BEGIN on the same connection. SQLite is
|
|
241
|
+
// single-writer anyway, so this preserves correctness with predictable
|
|
242
|
+
// queuing semantics.
|
|
243
|
+
let sqliteTxChain: Promise<unknown> = Promise.resolve();
|
|
244
|
+
const sqliteTxContext = new AsyncLocalStorage<boolean>();
|
|
245
|
+
|
|
246
|
+
// Finding #16: a standalone (non-transaction) op touches the single SQLite
|
|
247
|
+
// connection immediately, so a plain write issued while a transaction is
|
|
248
|
+
// mid-BEGIN…COMMIT would interleave into that open transaction and be caught
|
|
249
|
+
// by its ROLLBACK. Route every standalone op through the SAME chain that
|
|
250
|
+
// serializes transactions. Ops already running inside a transaction callback
|
|
251
|
+
// bypass the queue — they must run directly on the open transaction rather
|
|
252
|
+
// than deadlock behind the tx that owns the chain.
|
|
253
|
+
function serializeSqlite<T>(op: () => Promise<T>): Promise<T> {
|
|
254
|
+
if (!isSqlite || sqliteTxContext.getStore())
|
|
255
|
+
return op();
|
|
256
|
+
const result = sqliteTxChain.then(op, op);
|
|
257
|
+
sqliteTxChain = result.catch(() => undefined);
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Execute a query expecting a single row (or undefined). SQLite uses .get(), PG awaits the query. */
|
|
262
|
+
async function execOne<T>(query: any): Promise<T | undefined> {
|
|
263
|
+
const row = isSqlite
|
|
264
|
+
? (query.get() as T | undefined)
|
|
265
|
+
: ((await query) as T[])[0];
|
|
266
|
+
return row === undefined ? undefined : stringifyIds(row);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Execute a query expecting an array of rows. SQLite uses .all(), PG awaits the query. */
|
|
270
|
+
async function execMany<T>(query: any): Promise<T[]> {
|
|
271
|
+
const rows = isSqlite
|
|
272
|
+
? (query.all() as T[])
|
|
273
|
+
: ((await query) as T[]);
|
|
274
|
+
return rows.map(stringifyIds);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Execute a query where the result is discarded. SQLite uses .run(), PG awaits the query. */
|
|
278
|
+
async function execRun(query: any): Promise<void> {
|
|
279
|
+
if (isSqlite) {
|
|
280
|
+
query.run();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await query;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getTable(model: string): Table {
|
|
287
|
+
const table = tableMap[model];
|
|
288
|
+
if (!table) {
|
|
289
|
+
throw Errors.badRequest(`Unknown model: ${model}`);
|
|
290
|
+
}
|
|
291
|
+
return table;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Build a DatabaseAdapter backed by a specific Drizzle instance (db or tx) */
|
|
295
|
+
function buildAdapter(drizzle: DrizzleDB): DatabaseAdapter {
|
|
296
|
+
const self: DatabaseAdapter = {
|
|
297
|
+
get dialect() { return dialect; },
|
|
298
|
+
|
|
299
|
+
async create<T>(params: { model: string; data: Record<string, unknown> }): Promise<T> {
|
|
300
|
+
return serializeSqlite<T>(async () => {
|
|
301
|
+
const table = getTable(params.model);
|
|
302
|
+
try {
|
|
303
|
+
const result = await execOne<T>((drizzle as any).insert(table).values(sanitizeData(params.data) as any).returning());
|
|
304
|
+
return result as T;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
// Translate driver constraint errors (pg SQLSTATEs, sqlite
|
|
308
|
+
// SQLITE_CONSTRAINT_*) into the matching FortressError so
|
|
309
|
+
// unique-violation / FK-violation / etc. surface as CONFLICT /
|
|
310
|
+
// UNPROCESSABLE_ENTITY without every host writing the same try/catch.
|
|
311
|
+
rethrowDbError(err, dialect);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
async findOne<T>(params: { model: string; where: WhereClause[] }): Promise<T | null> {
|
|
317
|
+
return serializeSqlite<T | null>(async () => {
|
|
318
|
+
const table = getTable(params.model);
|
|
319
|
+
const condition = buildWhereCondition(table, params.where);
|
|
320
|
+
const result = await execOne<T>((drizzle as any).select().from(table).where(condition).limit(1));
|
|
321
|
+
return (result as T) ?? null;
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async findMany<T>(params: {
|
|
326
|
+
model: string;
|
|
327
|
+
where?: WhereClause[];
|
|
328
|
+
limit?: number;
|
|
329
|
+
offset?: number;
|
|
330
|
+
sortBy?: { field: string; direction: 'asc' | 'desc' };
|
|
331
|
+
}): Promise<T[]> {
|
|
332
|
+
return serializeSqlite<T[]>(async () => {
|
|
333
|
+
const table = getTable(params.model);
|
|
334
|
+
let query = (drizzle as any).select().from(table).$dynamic();
|
|
335
|
+
|
|
336
|
+
if (params.where && params.where.length > 0) {
|
|
337
|
+
const condition = buildWhereCondition(table, params.where);
|
|
338
|
+
query = query.where(condition);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (params.sortBy) {
|
|
342
|
+
const column = getColumn(table, params.sortBy.field);
|
|
343
|
+
query = query.orderBy(
|
|
344
|
+
params.sortBy.direction === 'desc' ? sql`${column} desc` : sql`${column} asc`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (params.limit) {
|
|
349
|
+
query = query.limit(params.limit);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (params.offset) {
|
|
353
|
+
query = query.offset(params.offset);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return execMany<T>(query);
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async update<T>(params: { model: string; where: WhereClause[]; data: Record<string, unknown> }): Promise<T | null> {
|
|
361
|
+
return serializeSqlite<T | null>(async () => {
|
|
362
|
+
const table = getTable(params.model);
|
|
363
|
+
const condition = buildWhereCondition(table, params.where);
|
|
364
|
+
const query = (drizzle as any).update(table).set(sanitizeData(params.data) as any).where(condition).returning();
|
|
365
|
+
try {
|
|
366
|
+
// SQLite drivers only step the statement for rows that are read from
|
|
367
|
+
// RETURNING. Use .all() so UPDATEs that match multiple rows actually
|
|
368
|
+
// apply to every row (family/session revocation depends on this),
|
|
369
|
+
// while preserving the adapter contract of returning one row/null.
|
|
370
|
+
if (isSqlite) {
|
|
371
|
+
const rows = (query.all() as T[]).map(stringifyIds);
|
|
372
|
+
return rows[0] ?? null;
|
|
373
|
+
}
|
|
374
|
+
const result = await execOne<T>(query);
|
|
375
|
+
return (result as T) ?? null;
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
rethrowDbError(err, dialect);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
async delete(params: { model: string; where: WhereClause[] }): Promise<void> {
|
|
384
|
+
return serializeSqlite<void>(async () => {
|
|
385
|
+
const table = getTable(params.model);
|
|
386
|
+
const condition = buildWhereCondition(table, params.where);
|
|
387
|
+
try {
|
|
388
|
+
await execRun((drizzle as any).delete(table).where(condition));
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
// FK violation on delete is the common case here — surfaces as
|
|
392
|
+
// UNPROCESSABLE_ENTITY rather than a raw driver error.
|
|
393
|
+
rethrowDbError(err, dialect);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
async count(params: { model: string; where?: WhereClause[] }): Promise<number> {
|
|
399
|
+
return serializeSqlite<number>(async () => {
|
|
400
|
+
const table = getTable(params.model);
|
|
401
|
+
let query = (drizzle as any).select({ count: sql<number>`count(*)` }).from(table).$dynamic();
|
|
402
|
+
|
|
403
|
+
if (params.where && params.where.length > 0) {
|
|
404
|
+
const condition = buildWhereCondition(table, params.where);
|
|
405
|
+
query = query.where(condition);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const result = await execOne<{ count: number | string }>(query);
|
|
409
|
+
return Number(result?.count) || 0;
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async rawQuery<T>(sqlText: string, params?: unknown[]): Promise<T[]> {
|
|
414
|
+
return serializeSqlite<T[]>(async () => {
|
|
415
|
+
const query = buildRawSql(sqlText, params ?? []);
|
|
416
|
+
if (isSqlite) {
|
|
417
|
+
if (typeof (drizzle as any).all === 'function' && sqliteStatementReturnsRows(sqlText))
|
|
418
|
+
return normalizeRawRows<T>((drizzle as any).all(query)).map(stringifyIds);
|
|
419
|
+
if (typeof (drizzle as any).run === 'function') {
|
|
420
|
+
(drizzle as any).run(query);
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
if (typeof (drizzle as any).execute === 'function')
|
|
424
|
+
return normalizeRawRows<T>(await (drizzle as any).execute(query)).map(stringifyIds);
|
|
425
|
+
throw Errors.badRequest('rawQuery is not supported by this SQLite Drizzle driver');
|
|
426
|
+
}
|
|
427
|
+
if (typeof (drizzle as any).execute !== 'function')
|
|
428
|
+
throw Errors.badRequest('rawQuery is not supported by this Drizzle driver');
|
|
429
|
+
return normalizeRawRows<T>(await (drizzle as any).execute(query)).map(stringifyIds);
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
async transaction<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T> {
|
|
434
|
+
if (dialect === 'sqlite') {
|
|
435
|
+
// SQLite transactions are serialized on one connection. A nested
|
|
436
|
+
// tx.transaction(...) from inside the callback would otherwise be
|
|
437
|
+
// queued behind the still-open outer transaction while the outer
|
|
438
|
+
// callback awaits it: a self-deadlock. Detect that context and fail
|
|
439
|
+
// clearly instead of hanging.
|
|
440
|
+
if (sqliteTxContext.getStore()) {
|
|
441
|
+
throw Errors.badRequest('Nested transactions are not supported by the SQLite Drizzle adapter');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const run = async (): Promise<T> => {
|
|
445
|
+
(drizzle as any).run(sql`BEGIN IMMEDIATE`);
|
|
446
|
+
try {
|
|
447
|
+
const result = await sqliteTxContext.run(true, () => fn(self));
|
|
448
|
+
(drizzle as any).run(sql`COMMIT`);
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
(drizzle as any).run(sql`ROLLBACK`);
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const result = sqliteTxChain.then(run, run);
|
|
458
|
+
sqliteTxChain = result.catch(() => undefined);
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// PostgreSQL: use Drizzle's native async transaction
|
|
463
|
+
return (drizzle as any).transaction(async (tx: DrizzleDB) => {
|
|
464
|
+
const txAdapter = buildAdapter(tx);
|
|
465
|
+
return fn(txAdapter);
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
return self;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return buildAdapter(db);
|
|
474
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM adapter for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Works with any Drizzle database instance — PostgreSQL or SQLite.
|
|
5
|
+
* Use the SQLite-flavoured `fortressSchema` aggregate for SQLite, or import
|
|
6
|
+
* `@bajustone/fortress/drizzle/pg` for the PostgreSQL flavour. Tables are
|
|
7
|
+
* exported via the aggregate (typed as `Record<string, AnySQLiteTable>`) so
|
|
8
|
+
* the public API stays statically typed for JSR; consumers who want
|
|
9
|
+
* column-level inference should declare their own Drizzle schema and pass it
|
|
10
|
+
* via the `tables` option.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { drizzle } from 'drizzle-orm/postgres-js';
|
|
15
|
+
* import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
|
|
16
|
+
*
|
|
17
|
+
* const db = drizzle(sql);
|
|
18
|
+
* const adapter = createDrizzleAdapter(db, { dialect: 'pg' });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export { createDrizzleAdapter } from './adapter';
|
|
25
|
+
export type { DrizzleAdapterOptions, DrizzleDialect } from './adapter';
|
|
26
|
+
export { findSqlstate, rethrowDbError } from './pg-error-map';
|
|
27
|
+
// Re-export only the schema objects — individual table exports cause JSR "slow types"
|
|
28
|
+
// errors because sqliteTable()/pgTable() return types are too complex for JSR to infer.
|
|
29
|
+
// Consumers access tables via: fortressSchema.users, fortressPgSchema.roles, etc.
|
|
30
|
+
export { fortressPgSchema } from './pg/schema';
|
|
31
|
+
export { fortressSchema } from './schema';
|