@antasphere/chassis-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +82 -0
- package/dist/accounts/deletion.d.ts +163 -0
- package/dist/accounts/deletion.d.ts.map +1 -0
- package/dist/accounts/deletion.js +360 -0
- package/dist/accounts/deletion.js.map +1 -0
- package/dist/accounts/erasure-log.d.ts +66 -0
- package/dist/accounts/erasure-log.d.ts.map +1 -0
- package/dist/accounts/erasure-log.js +117 -0
- package/dist/accounts/erasure-log.js.map +1 -0
- package/dist/accounts/index.d.ts +6 -0
- package/dist/accounts/index.d.ts.map +1 -0
- package/dist/accounts/index.js +6 -0
- package/dist/accounts/index.js.map +1 -0
- package/dist/accounts/mint-refusal.d.ts +47 -0
- package/dist/accounts/mint-refusal.d.ts.map +1 -0
- package/dist/accounts/mint-refusal.js +59 -0
- package/dist/accounts/mint-refusal.js.map +1 -0
- package/dist/accounts/signup-duplicate.d.ts +14 -0
- package/dist/accounts/signup-duplicate.d.ts.map +1 -0
- package/dist/accounts/signup-duplicate.js +25 -0
- package/dist/accounts/signup-duplicate.js.map +1 -0
- package/dist/accounts/superadmin.d.ts +27 -0
- package/dist/accounts/superadmin.d.ts.map +1 -0
- package/dist/accounts/superadmin.js +46 -0
- package/dist/accounts/superadmin.js.map +1 -0
- package/dist/api/apikeys.d.ts +14 -0
- package/dist/api/apikeys.d.ts.map +1 -0
- package/dist/api/apikeys.js +124 -0
- package/dist/api/apikeys.js.map +1 -0
- package/dist/api/audit.d.ts +4 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +75 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/break-glass.d.ts +48 -0
- package/dist/api/break-glass.d.ts.map +1 -0
- package/dist/api/break-glass.js +217 -0
- package/dist/api/break-glass.js.map +1 -0
- package/dist/api/cli-auth.d.ts +84 -0
- package/dist/api/cli-auth.d.ts.map +1 -0
- package/dist/api/cli-auth.js +177 -0
- package/dist/api/cli-auth.js.map +1 -0
- package/dist/api/create-api.d.ts +123 -0
- package/dist/api/create-api.d.ts.map +1 -0
- package/dist/api/create-api.js +1165 -0
- package/dist/api/create-api.js.map +1 -0
- package/dist/api/demo-passes.d.ts +45 -0
- package/dist/api/demo-passes.d.ts.map +1 -0
- package/dist/api/demo-passes.js +143 -0
- package/dist/api/demo-passes.js.map +1 -0
- package/dist/api/errors.d.ts +5 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +5 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/export.d.ts +63 -0
- package/dist/api/export.d.ts.map +1 -0
- package/dist/api/export.js +393 -0
- package/dist/api/export.js.map +1 -0
- package/dist/api/files.d.ts +40 -0
- package/dist/api/files.d.ts.map +1 -0
- package/dist/api/files.js +150 -0
- package/dist/api/files.js.map +1 -0
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/invitations.d.ts +50 -0
- package/dist/api/invitations.d.ts.map +1 -0
- package/dist/api/invitations.js +313 -0
- package/dist/api/invitations.js.map +1 -0
- package/dist/api/members.d.ts +25 -0
- package/dist/api/members.d.ts.map +1 -0
- package/dist/api/members.js +522 -0
- package/dist/api/members.js.map +1 -0
- package/dist/api/onboarding.d.ts +26 -0
- package/dist/api/onboarding.d.ts.map +1 -0
- package/dist/api/onboarding.js +23 -0
- package/dist/api/onboarding.js.map +1 -0
- package/dist/api/openapi-doc.d.ts +21 -0
- package/dist/api/openapi-doc.d.ts.map +1 -0
- package/dist/api/openapi-doc.js +25 -0
- package/dist/api/openapi-doc.js.map +1 -0
- package/dist/api/projects.d.ts +24 -0
- package/dist/api/projects.d.ts.map +1 -0
- package/dist/api/projects.js +624 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/scope-routes.d.ts +12 -0
- package/dist/api/scope-routes.d.ts.map +1 -0
- package/dist/api/scope-routes.js +2 -0
- package/dist/api/scope-routes.js.map +1 -0
- package/dist/api/sso-connect.d.ts +70 -0
- package/dist/api/sso-connect.d.ts.map +1 -0
- package/dist/api/sso-connect.js +164 -0
- package/dist/api/sso-connect.js.map +1 -0
- package/dist/api/sso-logout.d.ts +41 -0
- package/dist/api/sso-logout.d.ts.map +1 -0
- package/dist/api/sso-logout.js +34 -0
- package/dist/api/sso-logout.js.map +1 -0
- package/dist/api/teams.d.ts +43 -0
- package/dist/api/teams.d.ts.map +1 -0
- package/dist/api/teams.js +447 -0
- package/dist/api/teams.js.map +1 -0
- package/dist/api/workspaces.d.ts +114 -0
- package/dist/api/workspaces.d.ts.map +1 -0
- package/dist/api/workspaces.js +462 -0
- package/dist/api/workspaces.js.map +1 -0
- package/dist/apikeys/index.d.ts +3 -0
- package/dist/apikeys/index.d.ts.map +1 -0
- package/dist/apikeys/index.js +3 -0
- package/dist/apikeys/index.js.map +1 -0
- package/dist/apikeys/peppers.d.ts +53 -0
- package/dist/apikeys/peppers.d.ts.map +1 -0
- package/dist/apikeys/peppers.js +97 -0
- package/dist/apikeys/peppers.js.map +1 -0
- package/dist/apikeys/service.d.ts +56 -0
- package/dist/apikeys/service.d.ts.map +1 -0
- package/dist/apikeys/service.js +196 -0
- package/dist/apikeys/service.js.map +1 -0
- package/dist/app.d.ts +65 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +142 -0
- package/dist/app.js.map +1 -0
- package/dist/audit/filters.d.ts +38 -0
- package/dist/audit/filters.d.ts.map +1 -0
- package/dist/audit/filters.js +97 -0
- package/dist/audit/filters.js.map +1 -0
- package/dist/audit/index.d.ts +3 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +3 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/service.d.ts +65 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +103 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/boot.d.ts +8 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +822 -0
- package/dist/boot.js.map +1 -0
- package/dist/constant-time.d.ts +3 -0
- package/dist/constant-time.d.ts.map +1 -0
- package/dist/constant-time.js +10 -0
- package/dist/constant-time.js.map +1 -0
- package/dist/email/driver.d.ts +16 -0
- package/dist/email/driver.d.ts.map +1 -0
- package/dist/email/driver.js +136 -0
- package/dist/email/driver.js.map +1 -0
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +4 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/shell.d.ts +72 -0
- package/dist/email/shell.d.ts.map +1 -0
- package/dist/email/shell.js +146 -0
- package/dist/email/shell.js.map +1 -0
- package/dist/email/templates.d.ts +83 -0
- package/dist/email/templates.d.ts.map +1 -0
- package/dist/email/templates.js +96 -0
- package/dist/email/templates.js.map +1 -0
- package/dist/entitlements/check.d.ts +140 -0
- package/dist/entitlements/check.d.ts.map +1 -0
- package/dist/entitlements/check.js +276 -0
- package/dist/entitlements/check.js.map +1 -0
- package/dist/entitlements/clock-skew.d.ts +27 -0
- package/dist/entitlements/clock-skew.d.ts.map +1 -0
- package/dist/entitlements/clock-skew.js +58 -0
- package/dist/entitlements/clock-skew.js.map +1 -0
- package/dist/entitlements/gate.d.ts +113 -0
- package/dist/entitlements/gate.d.ts.map +1 -0
- package/dist/entitlements/gate.js +430 -0
- package/dist/entitlements/gate.js.map +1 -0
- package/dist/entitlements/hub-machine-token.d.ts +83 -0
- package/dist/entitlements/hub-machine-token.d.ts.map +1 -0
- package/dist/entitlements/hub-machine-token.js +175 -0
- package/dist/entitlements/hub-machine-token.js.map +1 -0
- package/dist/entitlements/hub-subject.d.ts +32 -0
- package/dist/entitlements/hub-subject.d.ts.map +1 -0
- package/dist/entitlements/hub-subject.js +68 -0
- package/dist/entitlements/hub-subject.js.map +1 -0
- package/dist/entitlements/index.d.ts +9 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +9 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/poster.d.ts +81 -0
- package/dist/entitlements/poster.d.ts.map +1 -0
- package/dist/entitlements/poster.js +169 -0
- package/dist/entitlements/poster.js.map +1 -0
- package/dist/entitlements/profiles.d.ts +121 -0
- package/dist/entitlements/profiles.d.ts.map +1 -0
- package/dist/entitlements/profiles.js +212 -0
- package/dist/entitlements/profiles.js.map +1 -0
- package/dist/entitlements/slot.d.ts +33 -0
- package/dist/entitlements/slot.d.ts.map +1 -0
- package/dist/entitlements/slot.js +75 -0
- package/dist/entitlements/slot.js.map +1 -0
- package/dist/env.d.ts +293 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +383 -0
- package/dist/env.js.map +1 -0
- package/dist/files/http.d.ts +23 -0
- package/dist/files/http.d.ts.map +1 -0
- package/dist/files/http.js +66 -0
- package/dist/files/http.js.map +1 -0
- package/dist/files/index.d.ts +5 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +5 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/serve.d.ts +43 -0
- package/dist/files/serve.d.ts.map +1 -0
- package/dist/files/serve.js +71 -0
- package/dist/files/serve.js.map +1 -0
- package/dist/files/service.d.ts +69 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +188 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/spool.d.ts +21 -0
- package/dist/files/spool.d.ts.map +1 -0
- package/dist/files/spool.js +47 -0
- package/dist/files/spool.js.map +1 -0
- package/dist/identity/better-auth.d.ts +3927 -0
- package/dist/identity/better-auth.d.ts.map +1 -0
- package/dist/identity/better-auth.js +896 -0
- package/dist/identity/better-auth.js.map +1 -0
- package/dist/identity/demo-pass-plugin.d.ts +29 -0
- package/dist/identity/demo-pass-plugin.d.ts.map +1 -0
- package/dist/identity/demo-pass-plugin.js +88 -0
- package/dist/identity/demo-pass-plugin.js.map +1 -0
- package/dist/identity/demo-pass-rules.d.ts +84 -0
- package/dist/identity/demo-pass-rules.d.ts.map +1 -0
- package/dist/identity/demo-pass-rules.js +223 -0
- package/dist/identity/demo-pass-rules.js.map +1 -0
- package/dist/identity/demo-pass.d.ts +159 -0
- package/dist/identity/demo-pass.d.ts.map +1 -0
- package/dist/identity/demo-pass.js +319 -0
- package/dist/identity/demo-pass.js.map +1 -0
- package/dist/identity/hub-grant.d.ts +182 -0
- package/dist/identity/hub-grant.d.ts.map +1 -0
- package/dist/identity/hub-grant.js +594 -0
- package/dist/identity/hub-grant.js.map +1 -0
- package/dist/identity/hub-jwt.d.ts +22 -0
- package/dist/identity/hub-jwt.d.ts.map +1 -0
- package/dist/identity/hub-jwt.js +77 -0
- package/dist/identity/hub-jwt.js.map +1 -0
- package/dist/identity/hub-live-gate.d.ts +17 -0
- package/dist/identity/hub-live-gate.d.ts.map +1 -0
- package/dist/identity/hub-live-gate.js +146 -0
- package/dist/identity/hub-live-gate.js.map +1 -0
- package/dist/identity/hub-logout.d.ts +88 -0
- package/dist/identity/hub-logout.d.ts.map +1 -0
- package/dist/identity/hub-logout.js +158 -0
- package/dist/identity/hub-logout.js.map +1 -0
- package/dist/identity/hub-projection.d.ts +103 -0
- package/dist/identity/hub-projection.d.ts.map +1 -0
- package/dist/identity/hub-projection.js +130 -0
- package/dist/identity/hub-projection.js.map +1 -0
- package/dist/identity/hub-reconcile.d.ts +151 -0
- package/dist/identity/hub-reconcile.d.ts.map +1 -0
- package/dist/identity/hub-reconcile.js +400 -0
- package/dist/identity/hub-reconcile.js.map +1 -0
- package/dist/identity/hub-sso.d.ts +363 -0
- package/dist/identity/hub-sso.d.ts.map +1 -0
- package/dist/identity/hub-sso.js +536 -0
- package/dist/identity/hub-sso.js.map +1 -0
- package/dist/identity/hub-user-client.d.ts +222 -0
- package/dist/identity/hub-user-client.d.ts.map +1 -0
- package/dist/identity/hub-user-client.js +406 -0
- package/dist/identity/hub-user-client.js.map +1 -0
- package/dist/identity/index.d.ts +13 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +13 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/oauth-jwt.d.ts +32 -0
- package/dist/identity/oauth-jwt.d.ts.map +1 -0
- package/dist/identity/oauth-jwt.js +122 -0
- package/dist/identity/oauth-jwt.js.map +1 -0
- package/dist/identity/resolve-membership.d.ts +28 -0
- package/dist/identity/resolve-membership.d.ts.map +1 -0
- package/dist/identity/resolve-membership.js +69 -0
- package/dist/identity/resolve-membership.js.map +1 -0
- package/dist/identity/signing-key.d.ts +49 -0
- package/dist/identity/signing-key.d.ts.map +1 -0
- package/dist/identity/signing-key.js +144 -0
- package/dist/identity/signing-key.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/invitations/index.d.ts +2 -0
- package/dist/invitations/index.d.ts.map +1 -0
- package/dist/invitations/index.js +2 -0
- package/dist/invitations/index.js.map +1 -0
- package/dist/invitations/service.d.ts +36 -0
- package/dist/invitations/service.d.ts.map +1 -0
- package/dist/invitations/service.js +122 -0
- package/dist/invitations/service.js.map +1 -0
- package/dist/jobs/index.d.ts +3 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +3 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/pgboss.d.ts +119 -0
- package/dist/jobs/pgboss.d.ts.map +1 -0
- package/dist/jobs/pgboss.js +521 -0
- package/dist/jobs/pgboss.js.map +1 -0
- package/dist/jobs/timers.d.ts +127 -0
- package/dist/jobs/timers.d.ts.map +1 -0
- package/dist/jobs/timers.js +228 -0
- package/dist/jobs/timers.js.map +1 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +64 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/errors.d.ts +37 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +77 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/http.d.ts +35 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +80 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inputs.d.ts +19 -0
- package/dist/mcp/inputs.d.ts.map +1 -0
- package/dist/mcp/inputs.js +28 -0
- package/dist/mcp/inputs.js.map +1 -0
- package/dist/mcp/projects.d.ts +31 -0
- package/dist/mcp/projects.d.ts.map +1 -0
- package/dist/mcp/projects.js +315 -0
- package/dist/mcp/projects.js.map +1 -0
- package/dist/mcp/server.d.ts +52 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +93 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/teams.d.ts +7 -0
- package/dist/mcp/teams.d.ts.map +1 -0
- package/dist/mcp/teams.js +62 -0
- package/dist/mcp/teams.js.map +1 -0
- package/dist/mcp/tool-kit.d.ts +67 -0
- package/dist/mcp/tool-kit.d.ts.map +1 -0
- package/dist/mcp/tool-kit.js +74 -0
- package/dist/mcp/tool-kit.js.map +1 -0
- package/dist/members/removal.d.ts +95 -0
- package/dist/members/removal.d.ts.map +1 -0
- package/dist/members/removal.js +143 -0
- package/dist/members/removal.js.map +1 -0
- package/dist/middleware/auth-body.d.ts +25 -0
- package/dist/middleware/auth-body.d.ts.map +1 -0
- package/dist/middleware/auth-body.js +55 -0
- package/dist/middleware/auth-body.js.map +1 -0
- package/dist/middleware/auth-context.d.ts +170 -0
- package/dist/middleware/auth-context.d.ts.map +1 -0
- package/dist/middleware/auth-context.js +251 -0
- package/dist/middleware/auth-context.js.map +1 -0
- package/dist/middleware/body-cap.d.ts +17 -0
- package/dist/middleware/body-cap.d.ts.map +1 -0
- package/dist/middleware/body-cap.js +31 -0
- package/dist/middleware/body-cap.js.map +1 -0
- package/dist/middleware/body-refusal.d.ts +20 -0
- package/dist/middleware/body-refusal.d.ts.map +1 -0
- package/dist/middleware/body-refusal.js +5 -0
- package/dist/middleware/body-refusal.js.map +1 -0
- package/dist/middleware/cross-site.d.ts +61 -0
- package/dist/middleware/cross-site.d.ts.map +1 -0
- package/dist/middleware/cross-site.js +59 -0
- package/dist/middleware/cross-site.js.map +1 -0
- package/dist/middleware/hub-managed.d.ts +45 -0
- package/dist/middleware/hub-managed.d.ts.map +1 -0
- package/dist/middleware/hub-managed.js +56 -0
- package/dist/middleware/hub-managed.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +14 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +222 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +12 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +12 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/json-depth.d.ts +32 -0
- package/dist/middleware/json-depth.d.ts.map +1 -0
- package/dist/middleware/json-depth.js +80 -0
- package/dist/middleware/json-depth.js.map +1 -0
- package/dist/middleware/no-store.d.ts +21 -0
- package/dist/middleware/no-store.d.ts.map +1 -0
- package/dist/middleware/no-store.js +32 -0
- package/dist/middleware/no-store.js.map +1 -0
- package/dist/middleware/oauth-public.d.ts +16 -0
- package/dist/middleware/oauth-public.d.ts.map +1 -0
- package/dist/middleware/oauth-public.js +50 -0
- package/dist/middleware/oauth-public.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +182 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +277 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-id.d.ts +10 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +42 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/middleware/scopes.d.ts +33 -0
- package/dist/middleware/scopes.d.ts.map +1 -0
- package/dist/middleware/scopes.js +139 -0
- package/dist/middleware/scopes.js.map +1 -0
- package/dist/middleware/security-headers.d.ts +53 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +96 -0
- package/dist/middleware/security-headers.js.map +1 -0
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +3 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/metrics.d.ts +17 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +82 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/otel.d.ts +20 -0
- package/dist/observability/otel.d.ts.map +1 -0
- package/dist/observability/otel.js +54 -0
- package/dist/observability/otel.js.map +1 -0
- package/dist/pagination.d.ts +57 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +90 -0
- package/dist/pagination.js.map +1 -0
- package/dist/platform/edition.d.ts +57 -0
- package/dist/platform/edition.d.ts.map +1 -0
- package/dist/platform/edition.js +90 -0
- package/dist/platform/edition.js.map +1 -0
- package/dist/platform/entitlements.d.ts +26 -0
- package/dist/platform/entitlements.d.ts.map +1 -0
- package/dist/platform/entitlements.js +28 -0
- package/dist/platform/entitlements.js.map +1 -0
- package/dist/platform/events.d.ts +87 -0
- package/dist/platform/events.d.ts.map +1 -0
- package/dist/platform/events.js +32 -0
- package/dist/platform/events.js.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/local-identity.d.ts +39 -0
- package/dist/platform/local-identity.d.ts.map +1 -0
- package/dist/platform/local-identity.js +87 -0
- package/dist/platform/local-identity.js.map +1 -0
- package/dist/platform/registry.d.ts +23 -0
- package/dist/platform/registry.d.ts.map +1 -0
- package/dist/platform/registry.js +5 -0
- package/dist/platform/registry.js.map +1 -0
- package/dist/platform/usage.d.ts +11 -0
- package/dist/platform/usage.d.ts.map +1 -0
- package/dist/platform/usage.js +12 -0
- package/dist/platform/usage.js.map +1 -0
- package/dist/platform/workspaces.d.ts +25 -0
- package/dist/platform/workspaces.d.ts.map +1 -0
- package/dist/platform/workspaces.js +44 -0
- package/dist/platform/workspaces.js.map +1 -0
- package/dist/projects/access.d.ts +40 -0
- package/dist/projects/access.d.ts.map +1 -0
- package/dist/projects/access.js +133 -0
- package/dist/projects/access.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +2 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/route-label.d.ts +23 -0
- package/dist/route-label.d.ts.map +1 -0
- package/dist/route-label.js +29 -0
- package/dist/route-label.js.map +1 -0
- package/dist/routes/health.d.ts +30 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +54 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/wellknown.d.ts +11 -0
- package/dist/routes/wellknown.d.ts.map +1 -0
- package/dist/routes/wellknown.js +45 -0
- package/dist/routes/wellknown.js.map +1 -0
- package/dist/secret.d.ts +30 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +97 -0
- package/dist/secret.js.map +1 -0
- package/dist/setup-transport.d.ts +23 -0
- package/dist/setup-transport.d.ts.map +1 -0
- package/dist/setup-transport.js +52 -0
- package/dist/setup-transport.js.map +1 -0
- package/dist/state.d.ts +21 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +4 -0
- package/dist/state.js.map +1 -0
- package/dist/storage/driver.d.ts +38 -0
- package/dist/storage/driver.d.ts.map +1 -0
- package/dist/storage/driver.js +14 -0
- package/dist/storage/driver.js.map +1 -0
- package/dist/storage/factory.d.ts +4 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +20 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local.d.ts +21 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +88 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/s3.d.ts +29 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +82 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/testing/fake-hub.d.ts +425 -0
- package/dist/testing/fake-hub.d.ts.map +1 -0
- package/dist/testing/fake-hub.js +1074 -0
- package/dist/testing/fake-hub.js.map +1 -0
- package/dist/testing/helpers.d.ts +124 -0
- package/dist/testing/helpers.d.ts.map +1 -0
- package/dist/testing/helpers.js +174 -0
- package/dist/testing/helpers.js.map +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/setup-origins.d.ts +21 -0
- package/dist/testing/setup-origins.d.ts.map +1 -0
- package/dist/testing/setup-origins.js +57 -0
- package/dist/testing/setup-origins.js.map +1 -0
- package/dist/testing/sso-helpers.d.ts +43 -0
- package/dist/testing/sso-helpers.d.ts.map +1 -0
- package/dist/testing/sso-helpers.js +159 -0
- package/dist/testing/sso-helpers.js.map +1 -0
- package/dist/tool-definition.d.ts +382 -0
- package/dist/tool-definition.d.ts.map +1 -0
- package/dist/tool-definition.js +46 -0
- package/dist/tool-definition.js.map +1 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +8 -0
- package/dist/util/index.js.map +1 -0
- package/package.json +161 -0
- package/src/accounts/deletion.ts +389 -0
- package/src/accounts/erasure-log.ts +140 -0
- package/src/accounts/index.ts +5 -0
- package/src/accounts/mint-refusal.ts +67 -0
- package/src/accounts/signup-duplicate.ts +21 -0
- package/src/accounts/superadmin.ts +46 -0
- package/src/api/apikeys.ts +147 -0
- package/src/api/audit.ts +83 -0
- package/src/api/break-glass.ts +294 -0
- package/src/api/cli-auth.ts +308 -0
- package/src/api/create-api.ts +1403 -0
- package/src/api/demo-passes.ts +207 -0
- package/src/api/errors.ts +13 -0
- package/src/api/export.ts +482 -0
- package/src/api/files.ts +200 -0
- package/src/api/index.ts +29 -0
- package/src/api/invitations.ts +396 -0
- package/src/api/members.ts +617 -0
- package/src/api/onboarding.ts +50 -0
- package/src/api/openapi-doc.ts +34 -0
- package/src/api/projects.ts +788 -0
- package/src/api/scope-routes.ts +12 -0
- package/src/api/sso-connect.ts +246 -0
- package/src/api/sso-logout.ts +76 -0
- package/src/api/teams.ts +575 -0
- package/src/api/workspaces.ts +649 -0
- package/src/apikeys/index.ts +2 -0
- package/src/apikeys/peppers.ts +118 -0
- package/src/apikeys/service.ts +223 -0
- package/src/app.ts +228 -0
- package/src/audit/filters.ts +101 -0
- package/src/audit/index.ts +9 -0
- package/src/audit/service.ts +154 -0
- package/src/boot.ts +953 -0
- package/src/constant-time.ts +9 -0
- package/src/email/driver.ts +139 -0
- package/src/email/index.ts +26 -0
- package/src/email/shell.ts +201 -0
- package/src/email/templates.ts +184 -0
- package/src/entitlements/check.ts +435 -0
- package/src/entitlements/clock-skew.ts +58 -0
- package/src/entitlements/gate.ts +636 -0
- package/src/entitlements/hub-machine-token.ts +205 -0
- package/src/entitlements/hub-subject.ts +77 -0
- package/src/entitlements/index.ts +37 -0
- package/src/entitlements/poster.ts +265 -0
- package/src/entitlements/profiles.ts +303 -0
- package/src/entitlements/slot.ts +105 -0
- package/src/env.ts +456 -0
- package/src/files/http.ts +77 -0
- package/src/files/index.ts +4 -0
- package/src/files/serve.ts +115 -0
- package/src/files/service.ts +209 -0
- package/src/files/spool.ts +58 -0
- package/src/identity/better-auth.ts +1103 -0
- package/src/identity/demo-pass-plugin.ts +121 -0
- package/src/identity/demo-pass-rules.ts +229 -0
- package/src/identity/demo-pass.ts +390 -0
- package/src/identity/hub-grant.ts +704 -0
- package/src/identity/hub-jwt.ts +78 -0
- package/src/identity/hub-live-gate.ts +167 -0
- package/src/identity/hub-logout.ts +174 -0
- package/src/identity/hub-projection.ts +251 -0
- package/src/identity/hub-reconcile.ts +572 -0
- package/src/identity/hub-sso.ts +696 -0
- package/src/identity/hub-user-client.ts +554 -0
- package/src/identity/index.ts +56 -0
- package/src/identity/oauth-jwt.ts +128 -0
- package/src/identity/resolve-membership.ts +104 -0
- package/src/identity/signing-key.ts +164 -0
- package/src/index.ts +65 -0
- package/src/invitations/index.ts +1 -0
- package/src/invitations/service.ts +171 -0
- package/src/jobs/index.ts +25 -0
- package/src/jobs/pgboss.ts +594 -0
- package/src/jobs/timers.ts +287 -0
- package/src/logger.ts +67 -0
- package/src/mcp/errors.ts +111 -0
- package/src/mcp/http.ts +129 -0
- package/src/mcp/index.ts +28 -0
- package/src/mcp/inputs.ts +33 -0
- package/src/mcp/projects.ts +480 -0
- package/src/mcp/server.ts +174 -0
- package/src/mcp/teams.ts +97 -0
- package/src/mcp/tool-kit.ts +148 -0
- package/src/members/removal.ts +216 -0
- package/src/middleware/auth-body.ts +58 -0
- package/src/middleware/auth-context.ts +384 -0
- package/src/middleware/body-cap.ts +33 -0
- package/src/middleware/body-refusal.ts +23 -0
- package/src/middleware/cross-site.ts +122 -0
- package/src/middleware/hub-managed.ts +61 -0
- package/src/middleware/idempotency.ts +264 -0
- package/src/middleware/index.ts +33 -0
- package/src/middleware/json-depth.ts +74 -0
- package/src/middleware/no-store.ts +30 -0
- package/src/middleware/oauth-public.ts +54 -0
- package/src/middleware/rate-limit.ts +432 -0
- package/src/middleware/request-id.ts +54 -0
- package/src/middleware/scopes.ts +150 -0
- package/src/middleware/security-headers.ts +129 -0
- package/src/observability/index.ts +2 -0
- package/src/observability/metrics.ts +102 -0
- package/src/observability/otel.ts +79 -0
- package/src/pagination.ts +118 -0
- package/src/platform/edition.ts +140 -0
- package/src/platform/entitlements.ts +47 -0
- package/src/platform/events.ts +71 -0
- package/src/platform/index.ts +7 -0
- package/src/platform/local-identity.ts +90 -0
- package/src/platform/registry.ts +28 -0
- package/src/platform/usage.ts +13 -0
- package/src/platform/workspaces.ts +41 -0
- package/src/projects/access.ts +160 -0
- package/src/projects/index.ts +7 -0
- package/src/route-label.ts +29 -0
- package/src/routes/health.ts +79 -0
- package/src/routes/index.ts +2 -0
- package/src/routes/wellknown.ts +67 -0
- package/src/secret.ts +115 -0
- package/src/setup-transport.ts +46 -0
- package/src/state.ts +23 -0
- package/src/storage/driver.ts +48 -0
- package/src/storage/factory.ts +34 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/local.ts +93 -0
- package/src/storage/s3.ts +109 -0
- package/src/testing/fake-hub.ts +1316 -0
- package/src/testing/helpers.ts +254 -0
- package/src/testing/index.ts +11 -0
- package/src/testing/setup-origins.ts +58 -0
- package/src/testing/sso-helpers.ts +178 -0
- package/src/tool-definition.ts +463 -0
- package/src/util/index.ts +17 -0
- package/test/fixtures/onboarding-backfill.sql +13 -0
- package/test/fixtures/operator-user-id-backfill.sql +18 -0
- package/test/host/dr-lib.sh +253 -0
- package/test/host/drizzle/0000_chassis_baseline.sql +438 -0
- package/test/host/drizzle/0001_last_owner_guard.sql +42 -0
- package/test/host/drizzle/meta/0000_snapshot.json +3566 -0
- package/test/host/drizzle/meta/_journal.json +20 -0
- package/test/host/drizzle.config.ts +17 -0
- package/test/host/identity.ts +41 -0
- package/test/host/minimal-host.ts +35 -0
- package/test/host/minimal-tool.ts +123 -0
- package/test/integration/account-oracle-uniformity.test.ts +284 -0
- package/test/integration/account-recovery.test.ts +236 -0
- package/test/integration/account.test.ts +120 -0
- package/test/integration/api-quota.test.ts +208 -0
- package/test/integration/apikey-expiry.test.ts +178 -0
- package/test/integration/apikey-pepper-rotation.test.ts +190 -0
- package/test/integration/boot-posture.test.ts +169 -0
- package/test/integration/break-glass.test.ts +484 -0
- package/test/integration/cloud-lifecycle.test.ts +233 -0
- package/test/integration/credential-boundaries.test.ts +195 -0
- package/test/integration/default-workspace.test.ts +414 -0
- package/test/integration/demo-pass-rules.test.ts +332 -0
- package/test/integration/demo-pass.test.ts +1222 -0
- package/test/integration/dr-verify-real-dump.test.ts +154 -0
- package/test/integration/edition.test.ts +868 -0
- package/test/integration/email-change.test.ts +394 -0
- package/test/integration/empty-tool.test.ts +267 -0
- package/test/integration/entitlements-null-hook.test.ts +187 -0
- package/test/integration/entitlements.test.ts +693 -0
- package/test/integration/export-entries.test.ts +321 -0
- package/test/integration/files.test.ts +320 -0
- package/test/integration/first-boot-claim.test.ts +131 -0
- package/test/integration/gdpr.test.ts +532 -0
- package/test/integration/helpers.ts +29 -0
- package/test/integration/hub-grant.test.ts +505 -0
- package/test/integration/hub-live-gate.test.ts +612 -0
- package/test/integration/hub-managed-membership.test.ts +559 -0
- package/test/integration/hub-org-selector.test.ts +281 -0
- package/test/integration/hub-reconcile.test.ts +646 -0
- package/test/integration/hub-sso.test.ts +704 -0
- package/test/integration/hub-teams.test.ts +771 -0
- package/test/integration/idempotency.test.ts +261 -0
- package/test/integration/invite-verification.test.ts +210 -0
- package/test/integration/last-owner-race.test.ts +263 -0
- package/test/integration/m9-fixes.test.ts +290 -0
- package/test/integration/mcp-projects.test.ts +463 -0
- package/test/integration/member-removal-race.test.ts +466 -0
- package/test/integration/member-removal.test.ts +794 -0
- package/test/integration/oauth-mcp.test.ts +477 -0
- package/test/integration/oauth-workspace.test.ts +371 -0
- package/test/integration/observability.test.ts +183 -0
- package/test/integration/onboarding.test.ts +268 -0
- package/test/integration/orphan-purge.test.ts +451 -0
- package/test/integration/pagination.test.ts +344 -0
- package/test/integration/platform-core.test.ts +305 -0
- package/test/integration/platform.test.ts +246 -0
- package/test/integration/pool-survives-restart.test.ts +80 -0
- package/test/integration/projects-cloud.test.ts +193 -0
- package/test/integration/projects.test.ts +1310 -0
- package/test/integration/security-fixes.test.ts +190 -0
- package/test/integration/setup-transport.test.ts +118 -0
- package/test/integration/signin-wall.test.ts +103 -0
- package/test/integration/signing-key-rotation.test.ts +349 -0
- package/test/integration/sso-helpers.ts +15 -0
- package/test/integration/sso-logout.test.ts +283 -0
- package/test/integration/teams-cloud.test.ts +194 -0
- package/test/integration/teams.test.ts +551 -0
- package/test/integration/timers.test.ts +377 -0
- package/test/integration/two-factor.test.ts +301 -0
- package/test/integration/usage-window.test.ts +161 -0
- package/test/integration/workspace-create-cloud.test.ts +523 -0
- package/test/integration/workspace-create.test.ts +510 -0
- package/test/integration/workspace-scoping.test.ts +530 -0
- package/test/integration/zz-adv-replay-owner.test.ts +205 -0
- package/test/unit/apikey-peppers.test.ts +110 -0
- package/test/unit/apikey-prefix.test.ts +34 -0
- package/test/unit/audit-filters.test.ts +135 -0
- package/test/unit/auth-origins.test.ts +92 -0
- package/test/unit/body-cap.test.ts +84 -0
- package/test/unit/clock-skew.test.ts +68 -0
- package/test/unit/cross-site.test.ts +166 -0
- package/test/unit/declared-credits.test.ts +13 -0
- package/test/unit/demo-pass-rules.test.ts +192 -0
- package/test/unit/email-driver.test.ts +127 -0
- package/test/unit/end-pool.test.ts +181 -0
- package/test/unit/entitlement-check.test.ts +492 -0
- package/test/unit/entitlement-gate.test.ts +1333 -0
- package/test/unit/entitlement-profiles.test.ts +289 -0
- package/test/unit/erasure-log.test.ts +72 -0
- package/test/unit/error-mapping.test.ts +89 -0
- package/test/unit/export-entries.test.ts +85 -0
- package/test/unit/export-sanitize.test.ts +27 -0
- package/test/unit/files-head-route.test.ts +155 -0
- package/test/unit/files-http.test.ts +97 -0
- package/test/unit/head-rewrite.test.ts +72 -0
- package/test/unit/hub-create-org.test.ts +181 -0
- package/test/unit/hub-jwt.test.ts +93 -0
- package/test/unit/hub-sso-params.test.ts +39 -0
- package/test/unit/hub-subject.test.ts +83 -0
- package/test/unit/idempotency-crypto.test.ts +45 -0
- package/test/unit/json-depth.test.ts +88 -0
- package/test/unit/logger-redaction.test.ts +66 -0
- package/test/unit/mcp-errors.test.ts +110 -0
- package/test/unit/mcp-inputs.test.ts +106 -0
- package/test/unit/mcp-project-hints.test.ts +26 -0
- package/test/unit/no-store.test.ts +49 -0
- package/test/unit/no-tool-migration-filename.test.ts +70 -0
- package/test/unit/observability.test.ts +64 -0
- package/test/unit/openapi-doc.test.ts +93 -0
- package/test/unit/rate-limit-consume-on.test.ts +76 -0
- package/test/unit/rate-limit.test.ts +39 -0
- package/test/unit/readyz-storage.test.ts +157 -0
- package/test/unit/request-quota.test.ts +146 -0
- package/test/unit/scopes.test.ts +66 -0
- package/test/unit/security-headers.test.ts +131 -0
- package/test/unit/serve-content-type.test.ts +74 -0
- package/test/unit/setup-transport.test.ts +29 -0
- package/test/unit/storage-local.test.ts +53 -0
- package/test/unit/superadmin.test.ts +32 -0
- package/test/unit/timers.test.ts +287 -0
- package/test/unit/tool-definition-guards.test.ts +166 -0
- package/test/unit/tool-entitlements-guard.test.ts +168 -0
- package/test/unit/usage-poster.test.ts +542 -0
- package/test/unit/usage-window.test.ts +48 -0
- package/test/unit/workspace-create-audit.test.ts +48 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
3
|
+
import { createPlatform, type BootResult, type ToolDefinition } from '../../src/index.js';
|
|
4
|
+
import { TimerError, type TimerJob, type Timers } from '../../src/jobs/index.js';
|
|
5
|
+
import type { JobDeclaration } from '../../src/jobs/pgboss.js';
|
|
6
|
+
import { createDatabase, makeCreateTestApp, startPostgres, type TestApp } from '../../src/testing/index.js';
|
|
7
|
+
import { minimalTool } from '../host/minimal-tool.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One-off timers on a real pg-boss (PRDCT-3254): a tool declares a queue with
|
|
11
|
+
* no schedule, a service arms a timer on it, the handler runs once with the
|
|
12
|
+
* payload, the job's id and its key. A key replaces the pending timer under
|
|
13
|
+
* the per-key lock, a cancel withdraws it, an undeclared queue is named, a
|
|
14
|
+
* timer survives a restart, and an api-role replica schedules and cancels.
|
|
15
|
+
*
|
|
16
|
+
* The file carries its own tool (two queues: a timer queue and a nightly
|
|
17
|
+
* cron), so it runs in the chassis package only: the tool's integration run
|
|
18
|
+
* excludes it, like `empty-tool.test.ts`.
|
|
19
|
+
*
|
|
20
|
+
* pg-boss polls every two seconds: every wait below is a poll of the
|
|
21
|
+
* recorded runs or of `pgboss.job`, with a deadline, never a bare sleep but
|
|
22
|
+
* where the point is that something does NOT happen.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const TIMER_QUEUE = 'things-timer';
|
|
26
|
+
const NIGHTLY_QUEUE = 'things-nightly';
|
|
27
|
+
const NIGHTLY_CRON = '0 3 * * *';
|
|
28
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
|
+
|
|
30
|
+
const DEADLINE_MS = 15_000;
|
|
31
|
+
const STEP_MS = 200;
|
|
32
|
+
|
|
33
|
+
interface TimerData {
|
|
34
|
+
n: number | string;
|
|
35
|
+
fail?: boolean;
|
|
36
|
+
/** How long the handler holds the run, in ms (a running timer for the replace and cancel cases). */
|
|
37
|
+
holdMs?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Run {
|
|
41
|
+
data: TimerData | null;
|
|
42
|
+
job: TimerJob;
|
|
43
|
+
at: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const runs: Run[] = [];
|
|
47
|
+
const nightlyRuns: Array<{ data: object | null; job: TimerJob }> = [];
|
|
48
|
+
const failedOnce = new Set<string>();
|
|
49
|
+
|
|
50
|
+
const timerJob: JobDeclaration<TimerData> = {
|
|
51
|
+
queue: TIMER_QUEUE,
|
|
52
|
+
async handler(data, job) {
|
|
53
|
+
runs.push({ data, job, at: Date.now() });
|
|
54
|
+
if (data?.holdMs) await sleep(data.holdMs);
|
|
55
|
+
if (data?.fail === true) {
|
|
56
|
+
const n = String(data.n);
|
|
57
|
+
if (!failedOnce.has(n)) {
|
|
58
|
+
failedOnce.add(n);
|
|
59
|
+
throw new Error(`timer ${n}: the first run fails on purpose`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const nightlyJob: JobDeclaration = {
|
|
66
|
+
queue: NIGHTLY_QUEUE,
|
|
67
|
+
schedule: { cron: NIGHTLY_CRON },
|
|
68
|
+
async handler(data, job) {
|
|
69
|
+
nightlyRuns.push({ data, job });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
74
|
+
type NoEnv = {};
|
|
75
|
+
interface TimersDomain {
|
|
76
|
+
timers: Timers;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The minimal test tool, its domain the `Timers` its services slot received, its jobs the two queues. */
|
|
80
|
+
const tool: ToolDefinition<NoEnv, TimersDomain> = {
|
|
81
|
+
identity: minimalTool.identity,
|
|
82
|
+
runtime: minimalTool.runtime,
|
|
83
|
+
db: minimalTool.db,
|
|
84
|
+
scopes: minimalTool.scopes,
|
|
85
|
+
services: (core) => ({ timers: core.timers }),
|
|
86
|
+
jobs: () => [timerJob, nightlyJob],
|
|
87
|
+
api: {
|
|
88
|
+
// A tool with no domain references no blob (the empty tool's policy); no file is uploaded here.
|
|
89
|
+
filePolicy: () => ({ blobInUse: async () => false, blobReadScope: () => undefined })
|
|
90
|
+
},
|
|
91
|
+
mcp: minimalTool.mcp,
|
|
92
|
+
copy: minimalTool.copy
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type TimersApp = TestApp<BootResult<NoEnv, TimersDomain>>;
|
|
96
|
+
|
|
97
|
+
const createTestApp = makeCreateTestApp(createPlatform(tool).boot);
|
|
98
|
+
|
|
99
|
+
let container: StartedPostgreSqlContainer;
|
|
100
|
+
let mainUrl: string;
|
|
101
|
+
let app: TimersApp;
|
|
102
|
+
const extraApps: TimersApp[] = [];
|
|
103
|
+
|
|
104
|
+
function sleep(ms: number): Promise<void> {
|
|
105
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function waitFor<T>(label: string, probe: () => Promise<T | undefined> | T | undefined): Promise<T> {
|
|
109
|
+
const deadline = Date.now() + DEADLINE_MS;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const value = await probe();
|
|
112
|
+
if (value !== undefined) return value;
|
|
113
|
+
if (Date.now() > deadline) throw new Error(`timed out after ${DEADLINE_MS} ms waiting for ${label}`);
|
|
114
|
+
await sleep(STEP_MS);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const runsOf = (n: TimerData['n']) => runs.filter((r) => r.data?.n === n);
|
|
119
|
+
|
|
120
|
+
async function rowOf(
|
|
121
|
+
target: TimersApp,
|
|
122
|
+
id: string
|
|
123
|
+
): Promise<{ state: string; start_after: Date; keep_until: Date } | undefined> {
|
|
124
|
+
const { rows } = await target.db.pool.query(
|
|
125
|
+
'SELECT state, start_after, keep_until FROM pgboss.job WHERE id = $1',
|
|
126
|
+
[id]
|
|
127
|
+
);
|
|
128
|
+
return rows[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function stateOf(id: string): Promise<string | undefined> {
|
|
132
|
+
return (await rowOf(app, id))?.state;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function countByKey(key: string, states: string[]): Promise<number> {
|
|
136
|
+
const { rows } = await app.db.pool.query(
|
|
137
|
+
'SELECT count(*)::int AS n FROM pgboss.job WHERE name = $1 AND singleton_key = $2 AND state = ANY($3)',
|
|
138
|
+
[TIMER_QUEUE, key, states]
|
|
139
|
+
);
|
|
140
|
+
return rows[0].n as number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function rejection(promise: Promise<unknown>): Promise<unknown> {
|
|
144
|
+
return promise.then(
|
|
145
|
+
() => null,
|
|
146
|
+
(e: unknown) => e
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
beforeAll(async () => {
|
|
151
|
+
container = await startPostgres();
|
|
152
|
+
mainUrl = await createDatabase(container, 'timers_main');
|
|
153
|
+
app = await createTestApp(mainUrl);
|
|
154
|
+
}, 240_000);
|
|
155
|
+
|
|
156
|
+
afterAll(async () => {
|
|
157
|
+
for (const extra of extraApps) await extra.stop().catch(() => {});
|
|
158
|
+
await app?.stop().catch(() => {});
|
|
159
|
+
await container?.stop();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('one-off timers on a declared queue', () => {
|
|
163
|
+
it('the services slot receives the same Timers as the boot result', () => {
|
|
164
|
+
expect(app.tool.timers).toBe(app.jobs.timers);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('a. inSeconds: the handler runs once with the payload, the id, the queue, no key', async () => {
|
|
168
|
+
const id = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 1 }, { inSeconds: 1 });
|
|
169
|
+
expect(id).toMatch(UUID);
|
|
170
|
+
const run = await waitFor('the run of n=1', () => runsOf(1)[0]);
|
|
171
|
+
expect(run.data).toEqual({ n: 1 });
|
|
172
|
+
expect(run.job).toEqual({ id, queue: TIMER_QUEUE, key: null });
|
|
173
|
+
await waitFor('n=1 completed', async () => ((await stateOf(id)) === 'completed' ? true : undefined));
|
|
174
|
+
expect(runsOf(1)).toHaveLength(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('b. at: the handler runs once at the instant', async () => {
|
|
178
|
+
const id = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'b' }, { at: new Date(Date.now() + 1000) });
|
|
179
|
+
expect(id).toMatch(UUID);
|
|
180
|
+
const run = await waitFor('the run of n=b', () => runsOf('b')[0]);
|
|
181
|
+
expect(run.data).toEqual({ n: 'b' });
|
|
182
|
+
expect(run.job).toEqual({ id, queue: TIMER_QUEUE, key: null });
|
|
183
|
+
await waitFor('n=b completed', async () => ((await stateOf(id)) === 'completed' ? true : undefined));
|
|
184
|
+
expect(runsOf('b')).toHaveLength(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('c. a key replaces the pending timer: the first is cancelled, only the second runs', async () => {
|
|
188
|
+
const first = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'first' }, { inSeconds: 120, key: 'k1' });
|
|
189
|
+
const second = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'second' }, { inSeconds: 1, key: 'k1' });
|
|
190
|
+
expect(second).not.toBe(first);
|
|
191
|
+
expect(await stateOf(first)).toBe('cancelled');
|
|
192
|
+
expect(await countByKey('k1', ['created', 'retry'])).toBe(1);
|
|
193
|
+
|
|
194
|
+
const run = await waitFor('the run of key k1', () => runs.find((r) => r.job.key === 'k1'));
|
|
195
|
+
expect(run.data).toEqual({ n: 'second' });
|
|
196
|
+
expect(run.job).toEqual({ id: second, queue: TIMER_QUEUE, key: 'k1' });
|
|
197
|
+
await sleep(4000);
|
|
198
|
+
expect(runs.filter((r) => r.job.key === 'k1')).toHaveLength(1);
|
|
199
|
+
expect(runsOf('first')).toHaveLength(0);
|
|
200
|
+
expect(await stateOf(first)).toBe('cancelled');
|
|
201
|
+
}, 40_000);
|
|
202
|
+
|
|
203
|
+
it('d. cancel by id withdraws the timer; a second cancel answers 0', async () => {
|
|
204
|
+
const startedAt = Date.now();
|
|
205
|
+
const id = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'd' }, { inSeconds: 2 });
|
|
206
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { id })).resolves.toBe(1);
|
|
207
|
+
expect(await stateOf(id)).toBe('cancelled');
|
|
208
|
+
await sleep(Math.max(0, startedAt + 5000 - Date.now()));
|
|
209
|
+
expect(runsOf('d')).toHaveLength(0);
|
|
210
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { id })).resolves.toBe(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('e. cancel by key withdraws the pending keyed timer; an unknown key answers 0', async () => {
|
|
214
|
+
const id = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'e' }, { inSeconds: 120, key: 'k-e' });
|
|
215
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'k-e' })).resolves.toBe(1);
|
|
216
|
+
expect(await stateOf(id)).toBe('cancelled');
|
|
217
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'never' })).resolves.toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('f. an undeclared queue is named, nothing is inserted, the keyed path rolls back', async () => {
|
|
221
|
+
const countNoSuch = async () =>
|
|
222
|
+
(await app.db.pool.query("SELECT count(*)::int AS n FROM pgboss.job WHERE name = 'no-such-queue'"))
|
|
223
|
+
.rows[0].n as number;
|
|
224
|
+
|
|
225
|
+
const plain = await rejection(app.jobs.timers.schedule('no-such-queue', {}, { inSeconds: 1 }));
|
|
226
|
+
expect(plain).toBeInstanceOf(TimerError);
|
|
227
|
+
expect((plain as TimerError).code).toBe('unknown_queue');
|
|
228
|
+
expect(await countNoSuch()).toBe(0);
|
|
229
|
+
|
|
230
|
+
const keyed = await rejection(
|
|
231
|
+
app.jobs.timers.schedule('no-such-queue', {}, { inSeconds: 1, key: 'k-f' })
|
|
232
|
+
);
|
|
233
|
+
expect(keyed).toBeInstanceOf(TimerError);
|
|
234
|
+
expect((keyed as TimerError).code).toBe('unknown_queue');
|
|
235
|
+
expect(await countNoSuch()).toBe(0);
|
|
236
|
+
|
|
237
|
+
// The client went back to the pool outside a transaction: the next keyed schedule works.
|
|
238
|
+
const id = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'f-after' }, { inSeconds: 120, key: 'k-f' });
|
|
239
|
+
expect(id).toMatch(UUID);
|
|
240
|
+
expect(await stateOf(id)).toBe('created');
|
|
241
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'k-f' })).resolves.toBe(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('g. a timer 40 days ahead is kept until after its start', async () => {
|
|
245
|
+
const id = await app.jobs.timers.schedule(
|
|
246
|
+
TIMER_QUEUE,
|
|
247
|
+
{ n: 'g' },
|
|
248
|
+
{ at: new Date(Date.now() + 40 * 24 * 3600 * 1000) }
|
|
249
|
+
);
|
|
250
|
+
const row = await rowOf(app, id);
|
|
251
|
+
expect(row).toBeDefined();
|
|
252
|
+
expect(row!.keep_until.getTime()).toBeGreaterThan(row!.start_after.getTime());
|
|
253
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { id })).resolves.toBe(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('h. retry: limit 1 runs twice and completes; limit 0 runs once and fails', async () => {
|
|
257
|
+
const retried = await app.jobs.timers.schedule(
|
|
258
|
+
TIMER_QUEUE,
|
|
259
|
+
{ n: 'h1', fail: true },
|
|
260
|
+
{ inSeconds: 0, retry: { limit: 1 } }
|
|
261
|
+
);
|
|
262
|
+
const once = await app.jobs.timers.schedule(
|
|
263
|
+
TIMER_QUEUE,
|
|
264
|
+
{ n: 'h0', fail: true },
|
|
265
|
+
{ inSeconds: 0, retry: { limit: 0 } }
|
|
266
|
+
);
|
|
267
|
+
await waitFor('h1 completed', async () => ((await stateOf(retried)) === 'completed' ? true : undefined));
|
|
268
|
+
await waitFor('h0 failed', async () => ((await stateOf(once)) === 'failed' ? true : undefined));
|
|
269
|
+
expect(runsOf('h1')).toHaveLength(2);
|
|
270
|
+
expect(runsOf('h1').every((r) => r.job.id === retried)).toBe(true);
|
|
271
|
+
expect(runsOf('h0')).toHaveLength(1);
|
|
272
|
+
}, 40_000);
|
|
273
|
+
|
|
274
|
+
it('i. twelve concurrent schedules under one key leave exactly one pending timer', async () => {
|
|
275
|
+
const ids = await Promise.all(
|
|
276
|
+
Array.from({ length: 12 }, (_, i) =>
|
|
277
|
+
app.jobs.timers.schedule(TIMER_QUEUE, { n: `race-${i}` }, { inSeconds: 120, key: 'race' })
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
expect(new Set(ids).size).toBe(12);
|
|
281
|
+
expect(await countByKey('race', ['created', 'retry'])).toBe(1);
|
|
282
|
+
expect(await countByKey('race', ['cancelled'])).toBe(11);
|
|
283
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'race' })).resolves.toBe(1);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("j. a cron's run hands the handler null, and the cron is still declared", async () => {
|
|
287
|
+
// What the cron does: a send with no payload.
|
|
288
|
+
await app.jobs.boss.send({ name: NIGHTLY_QUEUE });
|
|
289
|
+
const run = await waitFor('the nightly run', () => nightlyRuns[0]);
|
|
290
|
+
expect(run.data).toBeNull();
|
|
291
|
+
expect(run.job.key).toBeNull();
|
|
292
|
+
expect(run.job.queue).toBe(NIGHTLY_QUEUE);
|
|
293
|
+
const schedules = await app.jobs.boss.getSchedules();
|
|
294
|
+
expect(schedules.find((s) => s.name === NIGHTLY_QUEUE)).toMatchObject({
|
|
295
|
+
name: NIGHTLY_QUEUE,
|
|
296
|
+
cron: NIGHTLY_CRON
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('k. a timer survives a restart: scheduled on A, run by B after A stopped', async () => {
|
|
301
|
+
const restartUrl = await createDatabase(container, 'timers_restart');
|
|
302
|
+
const a = await createTestApp(restartUrl);
|
|
303
|
+
extraApps.push(a);
|
|
304
|
+
const id = await a.jobs.timers.schedule(TIMER_QUEUE, { n: 'k' }, { inSeconds: 4 });
|
|
305
|
+
await a.stop();
|
|
306
|
+
const stoppedAt = Date.now();
|
|
307
|
+
expect(runsOf('k')).toHaveLength(0);
|
|
308
|
+
|
|
309
|
+
const b = await createTestApp(restartUrl);
|
|
310
|
+
extraApps.push(b);
|
|
311
|
+
const run = await waitFor('the run of n=k on B', () => runsOf('k')[0]);
|
|
312
|
+
expect(run.job.id).toBe(id);
|
|
313
|
+
expect(run.at).toBeGreaterThan(stoppedAt);
|
|
314
|
+
expect(runsOf('k')).toHaveLength(1);
|
|
315
|
+
}, 120_000);
|
|
316
|
+
|
|
317
|
+
it('m. a replace during a RUNNING keyed timer whose run then fails: the old run ends failed, never retry; one pending under the key', async () => {
|
|
318
|
+
// retry: one more attempt a minute later, which the replace must take away.
|
|
319
|
+
const first = await app.jobs.timers.schedule(
|
|
320
|
+
TIMER_QUEUE,
|
|
321
|
+
{ n: 'm-old', fail: true, holdMs: 3000 },
|
|
322
|
+
{ inSeconds: 0, key: 'k-m', retry: { limit: 1, delaySeconds: 60 } }
|
|
323
|
+
);
|
|
324
|
+
await waitFor('m-old running', async () => ((await stateOf(first)) === 'active' ? true : undefined));
|
|
325
|
+
const second = await app.jobs.timers.schedule(
|
|
326
|
+
TIMER_QUEUE,
|
|
327
|
+
{ n: 'm-new' },
|
|
328
|
+
{ inSeconds: 120, key: 'k-m' }
|
|
329
|
+
);
|
|
330
|
+
await waitFor('m-old failed', async () => ((await stateOf(first)) === 'failed' ? true : undefined));
|
|
331
|
+
expect(await countByKey('k-m', ['created', 'retry'])).toBe(1);
|
|
332
|
+
expect(await stateOf(second)).toBe('created');
|
|
333
|
+
expect(runsOf('m-old')).toHaveLength(1);
|
|
334
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'k-m' })).resolves.toBe(1);
|
|
335
|
+
}, 40_000);
|
|
336
|
+
|
|
337
|
+
it('n. a cancel during a RUNNING timer withdraws nothing (0, still active), then the run completes; a cancel by id takes a failing run’s retry away', async () => {
|
|
338
|
+
const running = await app.jobs.timers.schedule(
|
|
339
|
+
TIMER_QUEUE,
|
|
340
|
+
{ n: 'n-run', holdMs: 4000 },
|
|
341
|
+
{ inSeconds: 0, key: 'k-n' }
|
|
342
|
+
);
|
|
343
|
+
await waitFor('n-run running', async () => ((await stateOf(running)) === 'active' ? true : undefined));
|
|
344
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { id: running })).resolves.toBe(0);
|
|
345
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { key: 'k-n' })).resolves.toBe(0);
|
|
346
|
+
expect(await stateOf(running)).toBe('active');
|
|
347
|
+
await waitFor('n-run completed', async () =>
|
|
348
|
+
(await stateOf(running)) === 'completed' ? true : undefined
|
|
349
|
+
);
|
|
350
|
+
expect(runsOf('n-run')).toHaveLength(1);
|
|
351
|
+
|
|
352
|
+
const failing = await app.jobs.timers.schedule(
|
|
353
|
+
TIMER_QUEUE,
|
|
354
|
+
{ n: 'n-fail', fail: true, holdMs: 3000 },
|
|
355
|
+
{ inSeconds: 0, retry: { limit: 2, delaySeconds: 60 } }
|
|
356
|
+
);
|
|
357
|
+
await waitFor('n-fail running', async () => ((await stateOf(failing)) === 'active' ? true : undefined));
|
|
358
|
+
await expect(app.jobs.timers.cancel(TIMER_QUEUE, { id: failing })).resolves.toBe(0);
|
|
359
|
+
await waitFor('n-fail failed', async () => ((await stateOf(failing)) === 'failed' ? true : undefined));
|
|
360
|
+
expect(runsOf('n-fail')).toHaveLength(1);
|
|
361
|
+
}, 40_000);
|
|
362
|
+
|
|
363
|
+
it('l. an api-role replica schedules and cancels; the all-role replica runs the timer', async () => {
|
|
364
|
+
const api = await createTestApp(mainUrl, { SERVICE_ROLE: 'api' });
|
|
365
|
+
extraApps.push(api);
|
|
366
|
+
expect(api.tool.timers).toBe(api.jobs.timers);
|
|
367
|
+
|
|
368
|
+
const id = await api.jobs.timers.schedule(TIMER_QUEUE, { n: 'l' }, { inSeconds: 1 });
|
|
369
|
+
expect(id).toMatch(UUID);
|
|
370
|
+
const run = await waitFor('the run of n=l', () => runsOf('l')[0]);
|
|
371
|
+
expect(run.job).toEqual({ id, queue: TIMER_QUEUE, key: null });
|
|
372
|
+
|
|
373
|
+
const pending = await app.jobs.timers.schedule(TIMER_QUEUE, { n: 'l-pending' }, { inSeconds: 120 });
|
|
374
|
+
await expect(api.jobs.timers.cancel(TIMER_QUEUE, { id: pending })).resolves.toBe(1);
|
|
375
|
+
expect(await stateOf(pending)).toBe('cancelled');
|
|
376
|
+
}, 120_000);
|
|
377
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
3
|
+
import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
4
|
+
import {
|
|
5
|
+
createDatabase,
|
|
6
|
+
createTestApp,
|
|
7
|
+
extractCookie,
|
|
8
|
+
readJson,
|
|
9
|
+
RecordingEmailDriver,
|
|
10
|
+
startPostgres,
|
|
11
|
+
type TestApp,
|
|
12
|
+
host
|
|
13
|
+
} from './helpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optional per-user 2FA — TOTP + one-time backup codes (ADR 009):
|
|
17
|
+
* - enrollment is password-gated and PENDING until one TOTP verifies;
|
|
18
|
+
* - an enrolled user's password sign-in answers { twoFactorRedirect } with
|
|
19
|
+
* NO session; /two-factor/verify-totp (or a backup code) completes it;
|
|
20
|
+
* - a backup code works exactly once;
|
|
21
|
+
* - the email-OTP sign-in path gets the same second-factor step (the
|
|
22
|
+
* custom after-hook in identity/better-auth.ts — the plugin only covers
|
|
23
|
+
* password sign-in, and an emailed code must not collapse 2FA into
|
|
24
|
+
* mailbox control);
|
|
25
|
+
* - disable is password-gated and destroys secret + backup codes;
|
|
26
|
+
* - machine principals (API keys) never see a 2FA step;
|
|
27
|
+
* - sign-up stays closed and discovery reports auth.twoFactor.
|
|
28
|
+
*
|
|
29
|
+
* TOTP codes are computed in-test from the enrolled secret (RFC 6238,
|
|
30
|
+
* HMAC-SHA1, 6 digits, 30 s period — the plugin's 1.6.15 defaults; verify
|
|
31
|
+
* allows a ±1-period window so mid-test timing is safe).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const OWNER = { email: 'owner@tf.test', name: 'Tf Owner', password: 'tf-owner-password-123' };
|
|
35
|
+
|
|
36
|
+
let container: StartedPostgreSqlContainer;
|
|
37
|
+
let app: TestApp;
|
|
38
|
+
let mailer: RecordingEmailDriver;
|
|
39
|
+
let ownerCookie: string;
|
|
40
|
+
let totpSecret: string; // base32, parsed from the enrollment totpURI
|
|
41
|
+
let backupCodes: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Distinct per-call IPs keep the per-IP login/two-factor walls out of the way
|
|
44
|
+
// (the per-EMAIL sign-in bucket still applies — this file stays under it).
|
|
45
|
+
let ipCounter = 0;
|
|
46
|
+
const nextIp = () => `10.99.0.${++ipCounter}`;
|
|
47
|
+
|
|
48
|
+
const json = (body: unknown, cookie?: string) => ({
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
'x-forwarded-for': nextIp(),
|
|
53
|
+
...(cookie ? { cookie } : {})
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function signIn(email: string, password: string): Promise<Response> {
|
|
59
|
+
return app.app.request('/api/v1/auth/sign-in/email', json({ email, password }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function meWithCookie(cookie: string): Promise<Response> {
|
|
63
|
+
return app.app.request('/api/v1/me', { headers: { cookie } });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── In-test TOTP (RFC 6238 / RFC 4648 base32) ───────────────────────────────
|
|
67
|
+
|
|
68
|
+
function base32Decode(encoded: string): Buffer {
|
|
69
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
70
|
+
let bits = 0;
|
|
71
|
+
let value = 0;
|
|
72
|
+
const out: number[] = [];
|
|
73
|
+
for (const ch of encoded.replace(/=+$/, '').toUpperCase()) {
|
|
74
|
+
const idx = alphabet.indexOf(ch);
|
|
75
|
+
if (idx === -1) throw new Error(`invalid base32 character: ${ch}`);
|
|
76
|
+
value = (value << 5) | idx;
|
|
77
|
+
bits += 5;
|
|
78
|
+
if (bits >= 8) {
|
|
79
|
+
out.push((value >>> (bits - 8)) & 0xff);
|
|
80
|
+
bits -= 8;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return Buffer.from(out);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function totpNow(base32Secret: string, digits = 6, periodSeconds = 30): string {
|
|
87
|
+
const counter = Math.floor(Date.now() / (periodSeconds * 1000));
|
|
88
|
+
const buf = Buffer.alloc(8);
|
|
89
|
+
buf.writeBigUInt64BE(BigInt(counter));
|
|
90
|
+
const hmac = createHmac('sha1', base32Decode(base32Secret)).update(buf).digest();
|
|
91
|
+
const offset = hmac[hmac.length - 1]! & 0xf;
|
|
92
|
+
const code =
|
|
93
|
+
(((hmac[offset]! & 0x7f) << 24) |
|
|
94
|
+
((hmac[offset + 1]! & 0xff) << 16) |
|
|
95
|
+
((hmac[offset + 2]! & 0xff) << 8) |
|
|
96
|
+
(hmac[offset + 3]! & 0xff)) %
|
|
97
|
+
10 ** digits;
|
|
98
|
+
return String(code).padStart(digits, '0');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function userFlags(email: string): Promise<{ twoFactorEnabled: boolean | null } | null> {
|
|
102
|
+
const { rows } = await app.db.pool.query<{ two_factor_enabled: boolean | null }>(
|
|
103
|
+
`SELECT two_factor_enabled FROM "user" WHERE email = $1`,
|
|
104
|
+
[email]
|
|
105
|
+
);
|
|
106
|
+
return rows[0] ? { twoFactorEnabled: rows[0].two_factor_enabled } : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
beforeAll(async () => {
|
|
110
|
+
container = await startPostgres();
|
|
111
|
+
mailer = new RecordingEmailDriver(); // delivering driver → email-OTP login is on
|
|
112
|
+
app = await createTestApp(await createDatabase(container, 'twofactor'), {}, { email: mailer });
|
|
113
|
+
await app.app.request(
|
|
114
|
+
'/api/v1/setup',
|
|
115
|
+
json({ setupToken: 'integration-test-setup-token', instanceName: 'Tf', owner: OWNER })
|
|
116
|
+
);
|
|
117
|
+
ownerCookie = extractCookie(await signIn(OWNER.email, OWNER.password));
|
|
118
|
+
}, 120_000);
|
|
119
|
+
|
|
120
|
+
afterAll(async () => {
|
|
121
|
+
await app?.stop();
|
|
122
|
+
await container?.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('2FA lifecycle (TOTP + backup codes)', () => {
|
|
126
|
+
it('discovery reports auth.twoFactor: true', async () => {
|
|
127
|
+
const info = await readJson(await app.app.request('/api/v1/instance'));
|
|
128
|
+
expect(info.auth.twoFactor).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('enrollment is password-gated: a wrong password answers 400', async () => {
|
|
132
|
+
const res = await app.app.request(
|
|
133
|
+
'/api/v1/auth/two-factor/enable',
|
|
134
|
+
json({ password: 'not-the-password-123' }, ownerCookie)
|
|
135
|
+
);
|
|
136
|
+
expect(res.status).toBe(400);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('enroll: /two-factor/enable issues a totpURI and 10 one-time backup codes', async () => {
|
|
140
|
+
const res = await app.app.request(
|
|
141
|
+
'/api/v1/auth/two-factor/enable',
|
|
142
|
+
json({ password: OWNER.password }, ownerCookie)
|
|
143
|
+
);
|
|
144
|
+
expect(res.status).toBe(200);
|
|
145
|
+
const body = await readJson(res);
|
|
146
|
+
expect(body.totpURI).toMatch(/^otpauth:\/\/totp\//);
|
|
147
|
+
expect(body.backupCodes).toHaveLength(10);
|
|
148
|
+
for (const code of body.backupCodes as string[]) {
|
|
149
|
+
expect(code).toMatch(/^[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}$/);
|
|
150
|
+
}
|
|
151
|
+
backupCodes = body.backupCodes;
|
|
152
|
+
const secret = new URL(body.totpURI as string).searchParams.get('secret');
|
|
153
|
+
expect(secret).toBeTruthy();
|
|
154
|
+
totpSecret = secret!;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('pending enrollment does NOT gate sign-in yet (no verified authenticator)', async () => {
|
|
158
|
+
const res = await signIn(OWNER.email, OWNER.password);
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
const body = await readJson(res);
|
|
161
|
+
expect(body.twoFactorRedirect).toBeUndefined();
|
|
162
|
+
// A real session came back.
|
|
163
|
+
expect((await meWithCookie(extractCookie(res))).status).toBe(200);
|
|
164
|
+
expect((await userFlags(OWNER.email))?.twoFactorEnabled).not.toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('activation: verifying a computed TOTP flips twoFactorEnabled', async () => {
|
|
168
|
+
const res = await app.app.request(
|
|
169
|
+
'/api/v1/auth/two-factor/verify-totp',
|
|
170
|
+
json({ code: totpNow(totpSecret) }, ownerCookie)
|
|
171
|
+
);
|
|
172
|
+
expect(res.status).toBe(200);
|
|
173
|
+
// Activation rotates the session — adopt the fresh cookie.
|
|
174
|
+
ownerCookie = extractCookie(res);
|
|
175
|
+
expect((await meWithCookie(ownerCookie)).status).toBe(200);
|
|
176
|
+
expect((await userFlags(OWNER.email))?.twoFactorEnabled).toBe(true);
|
|
177
|
+
const { rows } = await app.db.pool.query<{ verified: boolean | null }>(`SELECT verified FROM two_factor`);
|
|
178
|
+
expect(rows).toHaveLength(1);
|
|
179
|
+
expect(rows[0]!.verified).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('password sign-in now requires the second factor: redirect, wrong code 401, valid TOTP completes', async () => {
|
|
183
|
+
// One pending sign-in exercises the whole step (a rejected code does not
|
|
184
|
+
// consume the pending state, so the valid code reuses the same cookie).
|
|
185
|
+
const res = await signIn(OWNER.email, OWNER.password);
|
|
186
|
+
expect(res.status).toBe(200);
|
|
187
|
+
const body = await readJson(res);
|
|
188
|
+
expect(body.twoFactorRedirect).toBe(true);
|
|
189
|
+
const pending = extractCookie(res);
|
|
190
|
+
// The cookies it set do NOT authenticate — the pending-2FA state is not a session.
|
|
191
|
+
expect((await meWithCookie(pending)).status).toBe(401);
|
|
192
|
+
|
|
193
|
+
const wrong = totpNow(totpSecret) === '000000' ? '000001' : '000000';
|
|
194
|
+
const denied = await app.app.request(
|
|
195
|
+
'/api/v1/auth/two-factor/verify-totp',
|
|
196
|
+
json({ code: wrong }, pending)
|
|
197
|
+
);
|
|
198
|
+
expect(denied.status).toBe(401);
|
|
199
|
+
|
|
200
|
+
const done = await app.app.request(
|
|
201
|
+
'/api/v1/auth/two-factor/verify-totp',
|
|
202
|
+
json({ code: totpNow(totpSecret) }, pending)
|
|
203
|
+
);
|
|
204
|
+
expect(done.status).toBe(200);
|
|
205
|
+
expect((await meWithCookie(extractCookie(done))).status).toBe(200);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('a backup code completes sign-in once and is then consumed', async () => {
|
|
209
|
+
const code = backupCodes[0]!;
|
|
210
|
+
const pending = extractCookie(await signIn(OWNER.email, OWNER.password));
|
|
211
|
+
const first = await app.app.request(
|
|
212
|
+
'/api/v1/auth/two-factor/verify-backup-code',
|
|
213
|
+
json({ code }, pending)
|
|
214
|
+
);
|
|
215
|
+
expect(first.status).toBe(200);
|
|
216
|
+
expect((await meWithCookie(extractCookie(first))).status).toBe(200);
|
|
217
|
+
|
|
218
|
+
// The same code again, on a fresh pending sign-in: consumed → rejected.
|
|
219
|
+
const pending2 = extractCookie(await signIn(OWNER.email, OWNER.password));
|
|
220
|
+
const second = await app.app.request(
|
|
221
|
+
'/api/v1/auth/two-factor/verify-backup-code',
|
|
222
|
+
json({ code }, pending2)
|
|
223
|
+
);
|
|
224
|
+
expect(second.status).toBe(401);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('email-OTP sign-in cannot bypass the second factor', async () => {
|
|
228
|
+
mailer.sent.length = 0;
|
|
229
|
+
const sent = await app.app.request(
|
|
230
|
+
'/api/v1/auth/email-otp/send-verification-otp',
|
|
231
|
+
json({ email: OWNER.email, type: 'sign-in' })
|
|
232
|
+
);
|
|
233
|
+
expect(sent.status).toBe(200);
|
|
234
|
+
const otp = /\b(\d{6})\b/.exec(mailer.sent[0]?.text ?? '')?.[1];
|
|
235
|
+
expect(otp).toBeTruthy();
|
|
236
|
+
|
|
237
|
+
const res = await app.app.request('/api/v1/auth/sign-in/email-otp', json({ email: OWNER.email, otp }));
|
|
238
|
+
expect(res.status).toBe(200);
|
|
239
|
+
const body = await readJson(res);
|
|
240
|
+
expect(body.twoFactorRedirect).toBe(true);
|
|
241
|
+
const pending = extractCookie(res);
|
|
242
|
+
// No session came back...
|
|
243
|
+
expect((await meWithCookie(pending)).status).toBe(401);
|
|
244
|
+
// ...and the pending state completes through the SAME verify-totp step.
|
|
245
|
+
const done = await app.app.request(
|
|
246
|
+
'/api/v1/auth/two-factor/verify-totp',
|
|
247
|
+
json({ code: totpNow(totpSecret) }, pending)
|
|
248
|
+
);
|
|
249
|
+
expect(done.status).toBe(200);
|
|
250
|
+
expect((await meWithCookie(extractCookie(done))).status).toBe(200);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('machine principals (API keys) never see a 2FA step', async () => {
|
|
254
|
+
const minted = await readJson(
|
|
255
|
+
await app.app.request(
|
|
256
|
+
'/api/v1/api-keys',
|
|
257
|
+
json({ name: 'tf-key', scopes: [host.scopes.read] }, ownerCookie)
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
// The key belongs to the 2FA-enabled owner; bearer auth is single-step.
|
|
261
|
+
const res = await app.app.request('/api/v1/me', {
|
|
262
|
+
headers: { authorization: `Bearer ${minted.key}` }
|
|
263
|
+
});
|
|
264
|
+
expect(res.status).toBe(200);
|
|
265
|
+
const body = await readJson(res);
|
|
266
|
+
expect(body.via).toBe('api_key');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('sign-up stays closed (the 2FA plugin reopens nothing)', async () => {
|
|
270
|
+
const res = await app.app.request(
|
|
271
|
+
'/api/v1/auth/sign-up/email',
|
|
272
|
+
json({ email: 'squatter@tf.test', password: 'squatter-password-123', name: 'Squatter' })
|
|
273
|
+
);
|
|
274
|
+
expect(res.status).toBe(403);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('disable requires the password gate and removes 2FA entirely', async () => {
|
|
278
|
+
const denied = await app.app.request(
|
|
279
|
+
'/api/v1/auth/two-factor/disable',
|
|
280
|
+
json({ password: 'not-the-password-123' }, ownerCookie)
|
|
281
|
+
);
|
|
282
|
+
expect(denied.status).toBe(400);
|
|
283
|
+
|
|
284
|
+
const res = await app.app.request(
|
|
285
|
+
'/api/v1/auth/two-factor/disable',
|
|
286
|
+
json({ password: OWNER.password }, ownerCookie)
|
|
287
|
+
);
|
|
288
|
+
expect(res.status).toBe(200);
|
|
289
|
+
ownerCookie = extractCookie(res); // disable also rotates the session
|
|
290
|
+
|
|
291
|
+
expect((await userFlags(OWNER.email))?.twoFactorEnabled).toBe(false);
|
|
292
|
+
const { rows } = await app.db.pool.query(`SELECT id FROM two_factor`);
|
|
293
|
+
expect(rows).toHaveLength(0);
|
|
294
|
+
|
|
295
|
+
// Sign-in is single-step again.
|
|
296
|
+
const signedIn = await signIn(OWNER.email, OWNER.password);
|
|
297
|
+
expect(signedIn.status).toBe(200);
|
|
298
|
+
expect((await readJson(signedIn)).twoFactorRedirect).toBeUndefined();
|
|
299
|
+
expect((await meWithCookie(extractCookie(signedIn))).status).toBe(200);
|
|
300
|
+
});
|
|
301
|
+
});
|