@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,468 @@
|
|
|
1
|
+
// src/plugins/audit-log/index.ts
|
|
2
|
+
async function sha256Hex(input) {
|
|
3
|
+
const data = new TextEncoder().encode(input);
|
|
4
|
+
const buffer = await crypto.subtle.digest("SHA-256", data);
|
|
5
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
var auditWriteChains = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function withAuditChainLock(db, fn) {
|
|
9
|
+
const run = () => db.transaction(async (tx) => {
|
|
10
|
+
if (tx.dialect === "pg") {
|
|
11
|
+
if (!tx.rawQuery)
|
|
12
|
+
throw new Error("PostgreSQL audit hash chains require rawQuery advisory-lock support");
|
|
13
|
+
await tx.rawQuery("SELECT pg_advisory_xact_lock(117993, 1)");
|
|
14
|
+
}
|
|
15
|
+
return fn(tx);
|
|
16
|
+
});
|
|
17
|
+
if (db.dialect === "pg")
|
|
18
|
+
return run();
|
|
19
|
+
const previous = auditWriteChains.get(db) ?? Promise.resolve();
|
|
20
|
+
const result = previous.then(run, run);
|
|
21
|
+
auditWriteChains.set(db, result.then(() => void 0, () => void 0));
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
var AUDIT_EXPORT_COLUMNS = [
|
|
25
|
+
"id",
|
|
26
|
+
"timestamp",
|
|
27
|
+
"eventType",
|
|
28
|
+
"actorId",
|
|
29
|
+
"actorType",
|
|
30
|
+
"targetId",
|
|
31
|
+
"targetType",
|
|
32
|
+
"ipAddress",
|
|
33
|
+
"userAgent",
|
|
34
|
+
"outcome",
|
|
35
|
+
"metadata",
|
|
36
|
+
"previousHash",
|
|
37
|
+
"createdAt"
|
|
38
|
+
];
|
|
39
|
+
var CSV_SPECIAL_RE = /[",\n\r]/;
|
|
40
|
+
var CSV_FORMULA_PREFIX_RE = /^[=+\-@\t\r]/;
|
|
41
|
+
function toCsvCell(value) {
|
|
42
|
+
if (value == null)
|
|
43
|
+
return "";
|
|
44
|
+
const raw = value instanceof Date ? value.toISOString() : String(value);
|
|
45
|
+
const str = CSV_FORMULA_PREFIX_RE.test(raw) ? `'${raw}` : raw;
|
|
46
|
+
if (CSV_SPECIAL_RE.test(str))
|
|
47
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
48
|
+
return str;
|
|
49
|
+
}
|
|
50
|
+
function entriesToCsv(entries) {
|
|
51
|
+
const header = AUDIT_EXPORT_COLUMNS.join(",");
|
|
52
|
+
const rows = entries.map((entry) => {
|
|
53
|
+
const record = entry;
|
|
54
|
+
return AUDIT_EXPORT_COLUMNS.map((column) => toCsvCell(record[column])).join(",");
|
|
55
|
+
});
|
|
56
|
+
return [header, ...rows].join("\n");
|
|
57
|
+
}
|
|
58
|
+
async function computeEntryHash(entry) {
|
|
59
|
+
const values = AUDIT_EXPORT_COLUMNS.map((column) => {
|
|
60
|
+
const value = entry[column];
|
|
61
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
62
|
+
});
|
|
63
|
+
return sha256Hex(JSON.stringify(values));
|
|
64
|
+
}
|
|
65
|
+
async function inspectChain(entries, anchor) {
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
const brokenLinks2 = [];
|
|
68
|
+
if (!anchor) {
|
|
69
|
+
brokenLinks2.push({
|
|
70
|
+
entryId: "anchor",
|
|
71
|
+
expected: "persistent zero-entry audit-chain anchor",
|
|
72
|
+
actual: null
|
|
73
|
+
});
|
|
74
|
+
} else if (anchor.entryCount !== 0 || anchor.lastHash !== null) {
|
|
75
|
+
brokenLinks2.push({
|
|
76
|
+
entryId: "anchor",
|
|
77
|
+
expected: "entry_count=0 and last_hash=null",
|
|
78
|
+
actual: `${anchor.entryCount}:${anchor.lastHash ?? "null"}`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { tail: null, brokenLinks: brokenLinks2 };
|
|
82
|
+
}
|
|
83
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
84
|
+
const duplicateHashes = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const hash = await computeEntryHash(entry);
|
|
87
|
+
if (hashes.has(hash))
|
|
88
|
+
duplicateHashes.add(hash);
|
|
89
|
+
hashes.set(hash, entry);
|
|
90
|
+
}
|
|
91
|
+
const brokenLinks = [];
|
|
92
|
+
const roots = entries.filter((entry) => entry.previousHash == null);
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.previousHash != null && !hashes.has(entry.previousHash)) {
|
|
95
|
+
brokenLinks.push({
|
|
96
|
+
entryId: entry.id,
|
|
97
|
+
expected: "hash of an existing predecessor",
|
|
98
|
+
actual: entry.previousHash
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const hash of duplicateHashes) {
|
|
103
|
+
brokenLinks.push({
|
|
104
|
+
entryId: hashes.get(hash).id,
|
|
105
|
+
expected: "unique entry hash",
|
|
106
|
+
actual: hash
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (roots.length !== 1) {
|
|
110
|
+
for (const root of roots.slice(1)) {
|
|
111
|
+
brokenLinks.push({
|
|
112
|
+
entryId: root.id,
|
|
113
|
+
expected: "exactly one chain root",
|
|
114
|
+
actual: null
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (roots.length === 0) {
|
|
118
|
+
brokenLinks.push({
|
|
119
|
+
entryId: entries[0].id,
|
|
120
|
+
expected: "one entry with previousHash=null",
|
|
121
|
+
actual: entries[0].previousHash
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const byPreviousHash = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry.previousHash == null)
|
|
128
|
+
continue;
|
|
129
|
+
const successors = byPreviousHash.get(entry.previousHash) ?? [];
|
|
130
|
+
successors.push(entry);
|
|
131
|
+
byPreviousHash.set(entry.previousHash, successors);
|
|
132
|
+
}
|
|
133
|
+
const visited = /* @__PURE__ */ new Set();
|
|
134
|
+
let current = roots.length === 1 ? roots[0] : null;
|
|
135
|
+
let tail = null;
|
|
136
|
+
while (current && !visited.has(current)) {
|
|
137
|
+
visited.add(current);
|
|
138
|
+
tail = current;
|
|
139
|
+
const hash = await computeEntryHash(current);
|
|
140
|
+
const successors = byPreviousHash.get(hash) ?? [];
|
|
141
|
+
if (successors.length > 1) {
|
|
142
|
+
for (const successor of successors.slice(1)) {
|
|
143
|
+
brokenLinks.push({
|
|
144
|
+
entryId: successor.id,
|
|
145
|
+
expected: "a predecessor with only one successor",
|
|
146
|
+
actual: successor.previousHash
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
current = successors.length === 1 ? successors[0] : null;
|
|
151
|
+
}
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (!visited.has(entry)) {
|
|
154
|
+
brokenLinks.push({
|
|
155
|
+
entryId: entry.id,
|
|
156
|
+
expected: "entry reachable from the chain root",
|
|
157
|
+
actual: entry.previousHash
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!anchor) {
|
|
162
|
+
brokenLinks.push({
|
|
163
|
+
entryId: tail?.id ?? entries[0].id,
|
|
164
|
+
expected: "persisted terminal audit-chain anchor",
|
|
165
|
+
actual: null
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
if (anchor.entryCount !== entries.length) {
|
|
169
|
+
brokenLinks.push({
|
|
170
|
+
entryId: tail?.id ?? entries[0].id,
|
|
171
|
+
expected: `anchor entry count ${anchor.entryCount}`,
|
|
172
|
+
actual: String(entries.length)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (tail) {
|
|
176
|
+
const tailHash = await computeEntryHash(tail);
|
|
177
|
+
if (anchor.lastHash !== tailHash) {
|
|
178
|
+
brokenLinks.push({
|
|
179
|
+
entryId: tail.id,
|
|
180
|
+
expected: anchor.lastHash ?? "non-null terminal hash",
|
|
181
|
+
actual: tailHash
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { tail, brokenLinks };
|
|
187
|
+
}
|
|
188
|
+
async function queryAuditLog(db, options) {
|
|
189
|
+
const where = [];
|
|
190
|
+
if (options?.userId != null)
|
|
191
|
+
where.push({ field: "actorId", operator: "=", value: options.userId });
|
|
192
|
+
if (options?.eventType)
|
|
193
|
+
where.push({ field: "eventType", operator: "=", value: options.eventType });
|
|
194
|
+
if (options?.from)
|
|
195
|
+
where.push({ field: "timestamp", operator: ">=", value: options.from });
|
|
196
|
+
if (options?.to)
|
|
197
|
+
where.push({ field: "timestamp", operator: "<=", value: options.to });
|
|
198
|
+
return db.findMany({
|
|
199
|
+
model: "audit_log",
|
|
200
|
+
where: where.length > 0 ? where : void 0,
|
|
201
|
+
limit: options?.limit,
|
|
202
|
+
offset: options?.offset,
|
|
203
|
+
sortBy: { field: "timestamp", direction: "desc" }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function auditLog(config = {}) {
|
|
207
|
+
const allowedEvents = config.events ?? null;
|
|
208
|
+
const hashChain = config.hashChain ?? false;
|
|
209
|
+
function shouldLog(eventType) {
|
|
210
|
+
if (!allowedEvents)
|
|
211
|
+
return true;
|
|
212
|
+
return allowedEvents.includes(eventType);
|
|
213
|
+
}
|
|
214
|
+
async function readChainAnchor(db) {
|
|
215
|
+
return db.findOne({
|
|
216
|
+
model: "audit_chain_state",
|
|
217
|
+
where: [{ field: "id", operator: "=", value: "1" }]
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function writeEntry(db, entry) {
|
|
221
|
+
if (!hashChain) {
|
|
222
|
+
await db.create({ model: "audit_log", data: { ...entry, previousHash: null } });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await withAuditChainLock(db, async (tx) => {
|
|
226
|
+
const anchor = await readChainAnchor(tx);
|
|
227
|
+
if (!anchor || !Number.isInteger(anchor.entryCount) || anchor.entryCount < 0 || anchor.entryCount === 0 !== (anchor.lastHash === null)) {
|
|
228
|
+
throw new Error("Cannot append without a valid audit hash-chain anchor");
|
|
229
|
+
}
|
|
230
|
+
if (anchor.entryCount === 0) {
|
|
231
|
+
const existing = await tx.findMany({
|
|
232
|
+
model: "audit_log",
|
|
233
|
+
limit: 1
|
|
234
|
+
});
|
|
235
|
+
if (existing.length > 0) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
"Cannot append to an unchained legacy audit log; call rebaselineChain() first"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const created = await tx.create({
|
|
242
|
+
model: "audit_log",
|
|
243
|
+
data: { ...entry, previousHash: anchor.lastHash }
|
|
244
|
+
});
|
|
245
|
+
const lastHash = await computeEntryHash(created);
|
|
246
|
+
const updated = await tx.update({
|
|
247
|
+
model: "audit_chain_state",
|
|
248
|
+
where: [{ field: "id", operator: "=", value: anchor.id }],
|
|
249
|
+
data: { lastHash, entryCount: anchor.entryCount + 1, updatedAt: /* @__PURE__ */ new Date() }
|
|
250
|
+
});
|
|
251
|
+
if (!updated)
|
|
252
|
+
throw new Error("Audit hash-chain anchor disappeared during append");
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function writeAuthEntry(db, entry, logger) {
|
|
256
|
+
try {
|
|
257
|
+
await writeEntry(db, entry);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
try {
|
|
260
|
+
logger?.error({ plugin: "audit-log", error }, "audit log write failed");
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
name: "audit-log",
|
|
267
|
+
models: [{
|
|
268
|
+
name: "audit_log",
|
|
269
|
+
fields: {
|
|
270
|
+
id: { type: "string", required: true },
|
|
271
|
+
timestamp: { type: "date", required: true },
|
|
272
|
+
eventType: { type: "string", required: true },
|
|
273
|
+
actorId: { type: "string" },
|
|
274
|
+
actorType: { type: "string", required: true },
|
|
275
|
+
targetId: { type: "string" },
|
|
276
|
+
targetType: { type: "string" },
|
|
277
|
+
ipAddress: { type: "string" },
|
|
278
|
+
userAgent: { type: "string" },
|
|
279
|
+
outcome: { type: "string", required: true },
|
|
280
|
+
metadata: { type: "string" },
|
|
281
|
+
previousHash: { type: "string" },
|
|
282
|
+
createdAt: { type: "date", required: true }
|
|
283
|
+
}
|
|
284
|
+
}, {
|
|
285
|
+
name: "audit_chain_state",
|
|
286
|
+
fields: {
|
|
287
|
+
id: { type: "string", required: true },
|
|
288
|
+
lastHash: { type: "string" },
|
|
289
|
+
entryCount: { type: "number", required: true },
|
|
290
|
+
updatedAt: { type: "date", required: true }
|
|
291
|
+
}
|
|
292
|
+
}],
|
|
293
|
+
hooks: {
|
|
294
|
+
async afterLogin(ctx, result) {
|
|
295
|
+
if (!shouldLog("LOGIN_SUCCESS"))
|
|
296
|
+
return result;
|
|
297
|
+
await writeAuthEntry(ctx.db, {
|
|
298
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
299
|
+
eventType: "LOGIN_SUCCESS",
|
|
300
|
+
actorId: result.user.id,
|
|
301
|
+
actorType: "user",
|
|
302
|
+
targetId: null,
|
|
303
|
+
targetType: null,
|
|
304
|
+
ipAddress: ctx.meta?.ipAddress ?? null,
|
|
305
|
+
userAgent: ctx.meta?.userAgent ?? null,
|
|
306
|
+
outcome: "success",
|
|
307
|
+
metadata: null
|
|
308
|
+
}, ctx.config.logger);
|
|
309
|
+
return result;
|
|
310
|
+
},
|
|
311
|
+
async onLoginFailure(ctx) {
|
|
312
|
+
if (!shouldLog("LOGIN_FAILURE"))
|
|
313
|
+
return;
|
|
314
|
+
await writeAuthEntry(ctx.db, {
|
|
315
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
316
|
+
eventType: "LOGIN_FAILURE",
|
|
317
|
+
actorId: null,
|
|
318
|
+
actorType: "anonymous",
|
|
319
|
+
targetId: null,
|
|
320
|
+
targetType: null,
|
|
321
|
+
ipAddress: null,
|
|
322
|
+
userAgent: null,
|
|
323
|
+
outcome: "failure",
|
|
324
|
+
metadata: JSON.stringify({ identifier: ctx.identifier, error: ctx.error.message })
|
|
325
|
+
}, ctx.config.logger);
|
|
326
|
+
},
|
|
327
|
+
async beforeLogout(ctx) {
|
|
328
|
+
if (!shouldLog("LOGOUT"))
|
|
329
|
+
return;
|
|
330
|
+
await writeAuthEntry(ctx.db, {
|
|
331
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
332
|
+
eventType: "LOGOUT",
|
|
333
|
+
actorId: null,
|
|
334
|
+
actorType: "user",
|
|
335
|
+
targetId: null,
|
|
336
|
+
targetType: null,
|
|
337
|
+
ipAddress: ctx.meta?.ipAddress ?? null,
|
|
338
|
+
userAgent: ctx.meta?.userAgent ?? null,
|
|
339
|
+
outcome: "success",
|
|
340
|
+
metadata: null
|
|
341
|
+
}, ctx.config.logger);
|
|
342
|
+
},
|
|
343
|
+
async afterRegister(ctx, user) {
|
|
344
|
+
if (!shouldLog("REGISTER"))
|
|
345
|
+
return;
|
|
346
|
+
await writeAuthEntry(ctx.db, {
|
|
347
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
348
|
+
eventType: "REGISTER",
|
|
349
|
+
actorId: user.id,
|
|
350
|
+
actorType: "user",
|
|
351
|
+
targetId: user.id,
|
|
352
|
+
targetType: "user",
|
|
353
|
+
ipAddress: ctx.meta?.ipAddress ?? null,
|
|
354
|
+
userAgent: ctx.meta?.userAgent ?? null,
|
|
355
|
+
outcome: "success",
|
|
356
|
+
metadata: null
|
|
357
|
+
}, ctx.config.logger);
|
|
358
|
+
},
|
|
359
|
+
async afterTokenRefresh(ctx, result) {
|
|
360
|
+
if (!shouldLog("TOKEN_REFRESH"))
|
|
361
|
+
return result;
|
|
362
|
+
await writeAuthEntry(ctx.db, {
|
|
363
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
364
|
+
eventType: "TOKEN_REFRESH",
|
|
365
|
+
actorId: null,
|
|
366
|
+
actorType: "user",
|
|
367
|
+
targetId: null,
|
|
368
|
+
targetType: null,
|
|
369
|
+
ipAddress: ctx.meta?.ipAddress ?? null,
|
|
370
|
+
userAgent: ctx.meta?.userAgent ?? null,
|
|
371
|
+
outcome: "success",
|
|
372
|
+
metadata: null
|
|
373
|
+
}, ctx.config.logger);
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
methods: (ctx) => ({
|
|
378
|
+
async getAuditLog(options) {
|
|
379
|
+
return queryAuditLog(ctx.db, options);
|
|
380
|
+
},
|
|
381
|
+
async logCustomEvent(event) {
|
|
382
|
+
const eventType = event.eventType;
|
|
383
|
+
if (allowedEvents && !allowedEvents.includes(eventType))
|
|
384
|
+
return;
|
|
385
|
+
await writeEntry(ctx.db, {
|
|
386
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
387
|
+
eventType,
|
|
388
|
+
actorId: event.actorId ?? null,
|
|
389
|
+
actorType: event.actorType ?? "system",
|
|
390
|
+
targetId: event.targetId ?? null,
|
|
391
|
+
targetType: event.targetType ?? null,
|
|
392
|
+
ipAddress: event.ipAddress ?? null,
|
|
393
|
+
userAgent: event.userAgent ?? null,
|
|
394
|
+
outcome: event.outcome ?? "success",
|
|
395
|
+
metadata: event.metadata ? JSON.stringify(event.metadata) : null
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
async rebaselineChain() {
|
|
399
|
+
if (!hashChain)
|
|
400
|
+
throw new Error("Enable hashChain before rebaselining the audit log");
|
|
401
|
+
return withAuditChainLock(ctx.db, async (tx) => {
|
|
402
|
+
const anchor = await readChainAnchor(tx);
|
|
403
|
+
if (!anchor)
|
|
404
|
+
throw new Error("Cannot rebaseline without the persistent audit hash-chain anchor");
|
|
405
|
+
if (anchor.entryCount !== 0 || anchor.lastHash !== null) {
|
|
406
|
+
throw new Error("Cannot rebaseline an audit hash chain that has already been initialized");
|
|
407
|
+
}
|
|
408
|
+
const entries = await tx.findMany({ model: "audit_log" });
|
|
409
|
+
if (entries.some((entry) => entry.previousHash !== null)) {
|
|
410
|
+
throw new Error("Cannot rebaseline a partially or previously chained audit log");
|
|
411
|
+
}
|
|
412
|
+
entries.sort((left, right) => {
|
|
413
|
+
const timeDifference = left.createdAt.getTime() - right.createdAt.getTime();
|
|
414
|
+
if (timeDifference !== 0)
|
|
415
|
+
return timeDifference;
|
|
416
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
417
|
+
});
|
|
418
|
+
let previousHash = null;
|
|
419
|
+
const rebaselined = [];
|
|
420
|
+
for (const entry of entries) {
|
|
421
|
+
const updated = await tx.update({
|
|
422
|
+
model: "audit_log",
|
|
423
|
+
where: [{ field: "id", operator: "=", value: entry.id }],
|
|
424
|
+
data: { previousHash }
|
|
425
|
+
});
|
|
426
|
+
if (!updated)
|
|
427
|
+
throw new Error(`Audit entry ${entry.id} disappeared during rebaseline`);
|
|
428
|
+
rebaselined.push(updated);
|
|
429
|
+
previousHash = await computeEntryHash(updated);
|
|
430
|
+
}
|
|
431
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
432
|
+
const updatedAnchor = await tx.update({
|
|
433
|
+
model: "audit_chain_state",
|
|
434
|
+
where: [{ field: "id", operator: "=", value: anchor.id }],
|
|
435
|
+
data: { lastHash: previousHash, entryCount: entries.length, updatedAt }
|
|
436
|
+
});
|
|
437
|
+
if (!updatedAnchor)
|
|
438
|
+
throw new Error("Audit hash-chain anchor disappeared during rebaseline");
|
|
439
|
+
const { brokenLinks } = await inspectChain(rebaselined, updatedAnchor);
|
|
440
|
+
if (brokenLinks.length > 0)
|
|
441
|
+
throw new Error("Rebaselined audit hash chain failed verification");
|
|
442
|
+
return { valid: true, totalEntries: entries.length, brokenLinks: [] };
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
async exportEntries(format = "json", options) {
|
|
446
|
+
const entries = await queryAuditLog(ctx.db, options);
|
|
447
|
+
if (format === "csv")
|
|
448
|
+
return entriesToCsv(entries);
|
|
449
|
+
return JSON.stringify(entries, null, 2);
|
|
450
|
+
},
|
|
451
|
+
async verifyChain() {
|
|
452
|
+
return withAuditChainLock(ctx.db, async (tx) => {
|
|
453
|
+
const entries = await tx.findMany({ model: "audit_log" });
|
|
454
|
+
const anchor = await readChainAnchor(tx);
|
|
455
|
+
const { brokenLinks } = await inspectChain(entries, anchor);
|
|
456
|
+
return {
|
|
457
|
+
valid: brokenLinks.length === 0,
|
|
458
|
+
totalEntries: entries.length,
|
|
459
|
+
brokenLinks
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
export {
|
|
467
|
+
auditLog
|
|
468
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/plugins/data-isolation/index.ts
|
|
21
|
+
var data_isolation_exports = {};
|
|
22
|
+
__export(data_isolation_exports, {
|
|
23
|
+
dataIsolation: () => dataIsolation
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(data_isolation_exports);
|
|
26
|
+
var import_node_async_hooks = require("async_hooks");
|
|
27
|
+
|
|
28
|
+
// src/core/errors.ts
|
|
29
|
+
var FortressError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
statusCode;
|
|
32
|
+
retryAfter;
|
|
33
|
+
details;
|
|
34
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
35
|
+
oauthError;
|
|
36
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
37
|
+
oauthDescription;
|
|
38
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
39
|
+
oauthErrorUri;
|
|
40
|
+
constructor(code, message, statusCode, options) {
|
|
41
|
+
super(message, { cause: options?.cause });
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.statusCode = statusCode;
|
|
44
|
+
this.retryAfter = options?.retryAfter;
|
|
45
|
+
this.details = options?.details;
|
|
46
|
+
this.oauthError = options?.oauthError;
|
|
47
|
+
this.oauthDescription = options?.oauthDescription;
|
|
48
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
49
|
+
}
|
|
50
|
+
toJSON() {
|
|
51
|
+
return {
|
|
52
|
+
code: this.code,
|
|
53
|
+
message: this.message,
|
|
54
|
+
statusCode: this.statusCode,
|
|
55
|
+
...this.details !== void 0 && { details: this.details }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var Errors = {
|
|
60
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
61
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
62
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
63
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
64
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
65
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
66
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
67
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
68
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
69
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
70
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
71
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
72
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
73
|
+
/**
|
|
74
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
75
|
+
*
|
|
76
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
77
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
78
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
79
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
80
|
+
* on the machine-readable `error` field.
|
|
81
|
+
*
|
|
82
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
83
|
+
* - `invalid_client` → 401
|
|
84
|
+
* - everything else → 400
|
|
85
|
+
*/
|
|
86
|
+
oauth: (error, description, options) => {
|
|
87
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
88
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
89
|
+
return new FortressError(code, description ?? error, status, {
|
|
90
|
+
oauthError: error,
|
|
91
|
+
oauthDescription: description,
|
|
92
|
+
oauthErrorUri: options?.errorUri,
|
|
93
|
+
cause: options?.cause
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
98
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
99
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
100
|
+
* service-method call would produce.
|
|
101
|
+
*
|
|
102
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
103
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
104
|
+
* failures still become structured `FortressError`s.
|
|
105
|
+
*/
|
|
106
|
+
fromHttpResponse: (status, body) => {
|
|
107
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
108
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
109
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
110
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function isFortressErrorCode(code) {
|
|
114
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
|
|
115
|
+
}
|
|
116
|
+
function statusToErrorCode(status) {
|
|
117
|
+
if (status === 401)
|
|
118
|
+
return "UNAUTHORIZED";
|
|
119
|
+
if (status === 403)
|
|
120
|
+
return "FORBIDDEN";
|
|
121
|
+
if (status === 404)
|
|
122
|
+
return "NOT_FOUND";
|
|
123
|
+
if (status === 409)
|
|
124
|
+
return "CONFLICT";
|
|
125
|
+
if (status === 422)
|
|
126
|
+
return "UNPROCESSABLE_ENTITY";
|
|
127
|
+
if (status === 429)
|
|
128
|
+
return "RATE_LIMITED";
|
|
129
|
+
if (status === 503)
|
|
130
|
+
return "SERVICE_UNAVAILABLE";
|
|
131
|
+
if (status >= 500)
|
|
132
|
+
return "DATABASE_ERROR";
|
|
133
|
+
return "BAD_REQUEST";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/plugins/data-isolation/index.ts
|
|
137
|
+
var bypassStore = new import_node_async_hooks.AsyncLocalStorage();
|
|
138
|
+
function currentBypassState() {
|
|
139
|
+
return bypassStore.getStore();
|
|
140
|
+
}
|
|
141
|
+
function dataIsolation(config) {
|
|
142
|
+
return {
|
|
143
|
+
name: "data-isolation",
|
|
144
|
+
models: [{
|
|
145
|
+
name: "user_scope_assignment",
|
|
146
|
+
fields: {
|
|
147
|
+
id: { type: "number", required: true },
|
|
148
|
+
userId: { type: "number", required: true, references: { model: "user", field: "id" } },
|
|
149
|
+
scopeName: { type: "string", required: true },
|
|
150
|
+
scopeValue: { type: "string", required: true },
|
|
151
|
+
createdAt: { type: "date", required: true }
|
|
152
|
+
}
|
|
153
|
+
}],
|
|
154
|
+
async scopeRules(userId, model, ctx) {
|
|
155
|
+
const state = currentBypassState();
|
|
156
|
+
if (state?.all)
|
|
157
|
+
return null;
|
|
158
|
+
const filters = [];
|
|
159
|
+
const defaults = {};
|
|
160
|
+
for (const scope of config.scopes) {
|
|
161
|
+
if (state?.scopes.has(scope.name))
|
|
162
|
+
continue;
|
|
163
|
+
const applies = scope.models.includes("*") || scope.models.includes(model);
|
|
164
|
+
if (!applies)
|
|
165
|
+
continue;
|
|
166
|
+
const value = await scope.resolveValue(userId, ctx);
|
|
167
|
+
if (value === void 0 || value === null) {
|
|
168
|
+
if (config.unresolvedScope === "skip")
|
|
169
|
+
continue;
|
|
170
|
+
throw Errors.forbidden(`Data isolation scope '${scope.name}' could not be resolved`);
|
|
171
|
+
}
|
|
172
|
+
filters.push({ field: scope.field, operator: "=", value });
|
|
173
|
+
defaults[scope.field] = value;
|
|
174
|
+
}
|
|
175
|
+
return filters.length > 0 ? { filters, defaults } : null;
|
|
176
|
+
},
|
|
177
|
+
methods: () => ({
|
|
178
|
+
/**
|
|
179
|
+
* Execute a callback with a specific scope bypassed.
|
|
180
|
+
* Queries within the callback will not have the named scope filter applied.
|
|
181
|
+
*
|
|
182
|
+
* The bypass is async-context-local: it applies only to async work
|
|
183
|
+
* spawned inside `fn` and never leaks across concurrent requests.
|
|
184
|
+
*/
|
|
185
|
+
async withoutScope(scopeName, fn) {
|
|
186
|
+
const existing = currentBypassState();
|
|
187
|
+
const nextScopes = new Set(existing?.scopes ?? []);
|
|
188
|
+
nextScopes.add(scopeName);
|
|
189
|
+
const next = { all: existing?.all ?? false, scopes: nextScopes };
|
|
190
|
+
return bypassStore.run(next, fn);
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Execute a callback with all scopes bypassed.
|
|
194
|
+
* Use with caution \u2014 no row-level isolation is applied.
|
|
195
|
+
*
|
|
196
|
+
* The bypass is async-context-local. Calls outside the callback (and
|
|
197
|
+
* concurrent requests running on different async contexts) are
|
|
198
|
+
* unaffected.
|
|
199
|
+
*/
|
|
200
|
+
async unscoped(fn) {
|
|
201
|
+
const existing = currentBypassState();
|
|
202
|
+
const next = { all: true, scopes: new Set(existing?.scopes ?? []) };
|
|
203
|
+
return bypassStore.run(next, fn);
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
209
|
+
0 && (module.exports = {
|
|
210
|
+
dataIsolation
|
|
211
|
+
});
|