@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,594 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import { Counter } from 'prom-client';
|
|
4
|
+
import { sql } from 'drizzle-orm';
|
|
5
|
+
import type { Db } from '@antasphere/chassis-db';
|
|
6
|
+
import type { UsageDownstream, UsageEvent, UsageSink } from '@antasphere/chassis-contract';
|
|
7
|
+
import type { Env } from '../env.js';
|
|
8
|
+
import type { Logger } from '../logger.js';
|
|
9
|
+
import type { Auth } from '../identity/better-auth.js';
|
|
10
|
+
import type { AuditService } from '../audit/service.js';
|
|
11
|
+
import { parseSuperadminEmails } from '../accounts/superadmin.js';
|
|
12
|
+
import { PGBOSS_SCHEMA, PgBossTimers, type TimerJob, type Timers } from './timers.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* pg-boss job runtime. Queue creation and worker registration follow
|
|
16
|
+
* SERVICE_ROLE:
|
|
17
|
+
* - api: start({ supervise:false, schedule:false, migrate:false }) — can
|
|
18
|
+
* send jobs, runs no maintenance, needs no DDL rights.
|
|
19
|
+
* - worker: full start, registers handlers.
|
|
20
|
+
* - all: both in one process (single-container default).
|
|
21
|
+
* pg-boss serializes its own SCHEMA install internally, but NOT createQueue —
|
|
22
|
+
* so the whole install section runs under the app's advisory lock (below).
|
|
23
|
+
* First boot must run with role all|worker once.
|
|
24
|
+
*/
|
|
25
|
+
export const USAGE_QUEUE = 'usage-events';
|
|
26
|
+
export const AUDIT_PURGE_QUEUE = 'audit-purge';
|
|
27
|
+
/**
|
|
28
|
+
* Where a usage batch goes when the retry budget of `USAGE_QUEUE` runs out
|
|
29
|
+
* (pg-boss's dead letter, PRDCT-2635): a worker logs it at error level,
|
|
30
|
+
* counts it, and re-sends every event to `USAGE_QUEUE` after
|
|
31
|
+
* `heldDelaySeconds` — forever. Failed usage is held, never lost.
|
|
32
|
+
*/
|
|
33
|
+
export const USAGE_HELD_QUEUE = 'usage-events-held';
|
|
34
|
+
/**
|
|
35
|
+
* The singleton key a window hold (PRDCT-2644) carries on the held queue:
|
|
36
|
+
* pg-boss's dead-letter copy carries none, so the held worker tells a
|
|
37
|
+
* retry-budget hold from a window hold by it. The held queue's policy is
|
|
38
|
+
* `standard` and the send names no `singletonSeconds`, so the key enforces
|
|
39
|
+
* nothing; it is a label.
|
|
40
|
+
*/
|
|
41
|
+
const WINDOW_HOLD_PREFIX = 'occurred_at_window:';
|
|
42
|
+
const windowHoldKey = (id: string): string => WINDOW_HOLD_PREFIX + id;
|
|
43
|
+
const isWindowHoldKey = (key: string | null | undefined): boolean =>
|
|
44
|
+
typeof key === 'string' && key.startsWith(WINDOW_HOLD_PREFIX);
|
|
45
|
+
export const APIKEY_EXPIRY_QUEUE = 'apikey-expiry-sweep';
|
|
46
|
+
export const IDEMPOTENCY_PURGE_QUEUE = 'idempotency-purge';
|
|
47
|
+
export const ORPHAN_USER_PURGE_QUEUE = 'orphan-user-purge';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A TOOL's own job: its queue, its optional nightly schedule, its handler.
|
|
51
|
+
* The chassis creates the queue (and sets or clears the schedule) INSIDE the
|
|
52
|
+
* install advisory lock, between its own queues, in declaration order, and
|
|
53
|
+
* registers the handler as a poller after the lock is released — exactly
|
|
54
|
+
* where a chassis job of the same kind sits.
|
|
55
|
+
*
|
|
56
|
+
* `schedule.enabled === false` UNSCHEDULES: the queue still exists, so the
|
|
57
|
+
* schedule can be flipped later (the audit-purge pattern). A job with no
|
|
58
|
+
* `schedule` is a queue and a poller only: the queue a ONE-OFF TIMER rides
|
|
59
|
+
* (`Timers.schedule`, `./timers.ts`), whose handler receives the payload the
|
|
60
|
+
* timer was scheduled with and the job's id and key. A run the cron fires
|
|
61
|
+
* carries no payload: `data` is null there. The handler resolves whatever
|
|
62
|
+
* domain service it needs LAZILY, at run time: the jobs are created before
|
|
63
|
+
* the services that need the storage driver.
|
|
64
|
+
*
|
|
65
|
+
* `handler` is a method signature on purpose: a `JobDeclaration<MyPayload>`
|
|
66
|
+
* then sits in the slot's `JobDeclaration[]` without a cast, and a handler
|
|
67
|
+
* that takes no arguments (every cron before the timers) still fits.
|
|
68
|
+
*/
|
|
69
|
+
export interface JobDeclaration<TData extends object = object> {
|
|
70
|
+
queue: string;
|
|
71
|
+
schedule?: { cron: string; enabled?: boolean };
|
|
72
|
+
handler(data: TData | null, job: TimerJob): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Advisory-lock key serializing pg-boss's install DDL across replicas.
|
|
77
|
+
*
|
|
78
|
+
* pg-boss wraps its schema migration in its own advisory lock, but
|
|
79
|
+
* `createQueue()` calls `pgboss.create_queue()` (queue-row insert + per-queue
|
|
80
|
+
* partition CREATE TABLE/attach) with NO lock: two or more all|worker
|
|
81
|
+
* replicas booting a fresh database reliably deadlock there (Postgres
|
|
82
|
+
* DeadLockReport; scale drill, I2). Same session-scoped pattern as the
|
|
83
|
+
* migration lock (packages/chassis-db/src/migrate.ts). The key is distinct from the
|
|
84
|
+
* migration lock (7432001) and the last-owner locks (7432002 trigger xact /
|
|
85
|
+
* 7432003 session) — see internal/scale-drill-findings.md.
|
|
86
|
+
*/
|
|
87
|
+
const PGBOSS_INSTALL_LOCK_KEY = 7_432_004;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run pg-boss's one-time install section (schema + queues + schedules) under
|
|
91
|
+
* a session-scoped pg_advisory_lock on a dedicated client — never the pool
|
|
92
|
+
* (a session lock parked on a pooled connection would poison the pool). Held
|
|
93
|
+
* across boot setup ONLY; steady-state send/work never touches it.
|
|
94
|
+
*/
|
|
95
|
+
async function withInstallLock(
|
|
96
|
+
connectionString: string,
|
|
97
|
+
logger: Logger,
|
|
98
|
+
fn: () => Promise<void>
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const client = new pg.Client({ connectionString });
|
|
101
|
+
await client.connect();
|
|
102
|
+
try {
|
|
103
|
+
await client.query('SELECT pg_advisory_lock($1)', [PGBOSS_INSTALL_LOCK_KEY]);
|
|
104
|
+
logger.info({ scope: 'pgboss' }, 'advisory lock acquired, installing pg-boss schema and queues');
|
|
105
|
+
await fn();
|
|
106
|
+
logger.info({ scope: 'pgboss' }, 'pg-boss install up to date');
|
|
107
|
+
} finally {
|
|
108
|
+
try {
|
|
109
|
+
await client.query('SELECT pg_advisory_unlock($1)', [PGBOSS_INSTALL_LOCK_KEY]);
|
|
110
|
+
} finally {
|
|
111
|
+
await client.end();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface Jobs {
|
|
117
|
+
boss: PgBoss;
|
|
118
|
+
/** One-off timers on the tool's declared queues (`./timers.ts`). */
|
|
119
|
+
timers: Timers;
|
|
120
|
+
stop: () => Promise<void>;
|
|
121
|
+
/** The counters boot registers on the app's /metrics registry. */
|
|
122
|
+
promMetrics: Counter<string>[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createJobs(
|
|
126
|
+
env: Pick<
|
|
127
|
+
Env,
|
|
128
|
+
| 'DATABASE_URL'
|
|
129
|
+
| 'SERVICE_ROLE'
|
|
130
|
+
| 'AUDIT_RETENTION_DAYS'
|
|
131
|
+
| 'ORPHAN_USER_RETENTION_HOURS'
|
|
132
|
+
| 'SUPERADMIN_EMAILS'
|
|
133
|
+
>,
|
|
134
|
+
db: Db,
|
|
135
|
+
/** The pool behind `db`: a keyed timer's replace runs on a client of its own (`./timers.ts`). */
|
|
136
|
+
pool: pg.Pool,
|
|
137
|
+
logger: Logger,
|
|
138
|
+
downstreamUsage: UsageDownstream,
|
|
139
|
+
/** Orphan purge deletes through Better Auth's own internalAdapter (FK-safe cascade). */
|
|
140
|
+
auth: Auth,
|
|
141
|
+
/** System-actor audit rows for the orphan purge. */
|
|
142
|
+
audit: AuditService,
|
|
143
|
+
/** The tool's own jobs, installed and polled between the chassis ones. */
|
|
144
|
+
toolJobs: readonly JobDeclaration[] = [],
|
|
145
|
+
/** The usage queue's retry budget and the hold before a re-drive (a test shrinks both). */
|
|
146
|
+
usageRetry: UsageRetry = DEFAULT_USAGE_RETRY
|
|
147
|
+
): Promise<Jobs> {
|
|
148
|
+
const isWorker = env.SERVICE_ROLE === 'all' || env.SERVICE_ROLE === 'worker';
|
|
149
|
+
// registers: [] — boot attaches it to the app registry.
|
|
150
|
+
const held = new Counter({
|
|
151
|
+
name: 'usage_events_held_total',
|
|
152
|
+
help: 'Usage events held for a re-drive, by reason: retry_budget (the retry budget ran out), occurred_at_window (the hub would refuse the date)',
|
|
153
|
+
labelNames: ['reason'] as const,
|
|
154
|
+
registers: []
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const boss = new PgBoss({
|
|
158
|
+
connectionString: env.DATABASE_URL,
|
|
159
|
+
schema: PGBOSS_SCHEMA,
|
|
160
|
+
// Completed jobs archive after an hour and the archive is dropped after a
|
|
161
|
+
// week — the pgboss tables stay bounded on a busy instance.
|
|
162
|
+
archiveCompletedAfterSeconds: 3600,
|
|
163
|
+
deleteAfterDays: 7,
|
|
164
|
+
...(isWorker ? {} : { supervise: false, schedule: false, migrate: false })
|
|
165
|
+
});
|
|
166
|
+
boss.on('error', (err) => logger.error({ err }, 'pg-boss error'));
|
|
167
|
+
|
|
168
|
+
const retentionDays = env.AUDIT_RETENTION_DAYS;
|
|
169
|
+
const orphanRetentionHours = env.ORPHAN_USER_RETENTION_HOURS;
|
|
170
|
+
|
|
171
|
+
if (isWorker) {
|
|
172
|
+
// The install section — pg-boss schema (start) + queue partitions
|
|
173
|
+
// (createQueue) + nightly schedules — is one-time DDL and runs under the
|
|
174
|
+
// install advisory lock so concurrent fresh-DB boots serialize instead
|
|
175
|
+
// of deadlocking in pgboss.create_queue. Handler registration below runs
|
|
176
|
+
// AFTER release: work() is an in-process poller, no DDL, and steady-state
|
|
177
|
+
// must never hold the lock.
|
|
178
|
+
await withInstallLock(env.DATABASE_URL, logger, async () => {
|
|
179
|
+
await boss.start();
|
|
180
|
+
// The held queue first, then the usage queue pointed at it as its dead
|
|
181
|
+
// letter — ON THE QUEUE, never on a send: an api-role replica sends
|
|
182
|
+
// without DDL rights and may roll before the worker of this release,
|
|
183
|
+
// and pg-boss's job row holds a foreign key on the dead letter it
|
|
184
|
+
// names, so a per-send dead letter would fail every send until the
|
|
185
|
+
// held queue exists (/code-review, lane D). pg-boss stamps the queue's
|
|
186
|
+
// dead letter on each job row at insert, so it covers every job sent
|
|
187
|
+
// after `updateQueue` ran, whoever sent it; on an installation that
|
|
188
|
+
// predates the held queue, a job an api-role replica sends between its
|
|
189
|
+
// own boot and this worker's install carries none (verifier round 2,
|
|
190
|
+
// accepted: one rollout's gap, and only a long outage on top would end
|
|
191
|
+
// it in a terminal failure). What pg-boss 10 gives a
|
|
192
|
+
// held job: the dead-letter copy carries the ORIGINAL retry limit (ten)
|
|
193
|
+
// and no delay, so a re-send that throws (a local insert refused while
|
|
194
|
+
// fetches still succeed) is retried ten times back to back, then that
|
|
195
|
+
// held job fails for good; the retry options on the held queue below
|
|
196
|
+
// apply to jobs sent to it, not to the copies pg-boss makes (verifier
|
|
197
|
+
// round 1, accepted: the path needs the database to refuse eleven
|
|
198
|
+
// inserts in a row while serving the fetches).
|
|
199
|
+
await boss.createQueue(USAGE_HELD_QUEUE, {
|
|
200
|
+
name: USAGE_HELD_QUEUE,
|
|
201
|
+
retryLimit: 10,
|
|
202
|
+
retryDelay: 60,
|
|
203
|
+
retryBackoff: true
|
|
204
|
+
});
|
|
205
|
+
await boss.createQueue(USAGE_QUEUE);
|
|
206
|
+
await boss.updateQueue(USAGE_QUEUE, { name: USAGE_QUEUE, deadLetter: USAGE_HELD_QUEUE });
|
|
207
|
+
// Audit retention queue: 0 = keep forever; the queue always exists so
|
|
208
|
+
// the schedule can be flipped later.
|
|
209
|
+
await boss.createQueue(AUDIT_PURGE_QUEUE);
|
|
210
|
+
if (retentionDays > 0) {
|
|
211
|
+
await boss.schedule(AUDIT_PURGE_QUEUE, '0 3 * * *');
|
|
212
|
+
} else {
|
|
213
|
+
await boss.unschedule(AUDIT_PURGE_QUEUE).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
await boss.createQueue(APIKEY_EXPIRY_QUEUE);
|
|
216
|
+
await boss.schedule(APIKEY_EXPIRY_QUEUE, '0 3 * * *');
|
|
217
|
+
await boss.createQueue(IDEMPOTENCY_PURGE_QUEUE);
|
|
218
|
+
await boss.schedule(IDEMPOTENCY_PURGE_QUEUE, '0 3 * * *');
|
|
219
|
+
// The tool's queues: same install section, same lock, declaration order.
|
|
220
|
+
for (const job of toolJobs) {
|
|
221
|
+
await boss.createQueue(job.queue);
|
|
222
|
+
if (!job.schedule) continue;
|
|
223
|
+
if (job.schedule.enabled !== false) {
|
|
224
|
+
await boss.schedule(job.queue, job.schedule.cron);
|
|
225
|
+
} else {
|
|
226
|
+
await boss.unschedule(job.queue).catch(() => {});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Orphaned-user GC: 0 = disabled; the queue always exists so the
|
|
230
|
+
// schedule can be flipped later (audit-purge pattern).
|
|
231
|
+
await boss.createQueue(ORPHAN_USER_PURGE_QUEUE);
|
|
232
|
+
if (orphanRetentionHours > 0) {
|
|
233
|
+
await boss.schedule(ORPHAN_USER_PURGE_QUEUE, '0 3 * * *');
|
|
234
|
+
} else {
|
|
235
|
+
await boss.unschedule(ORPHAN_USER_PURGE_QUEUE).catch(() => {});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Usage events flow through the durable queue (at-least-once); the
|
|
240
|
+
// downstream sink (no-op locally, the rail in cloud) dedupes by ULID.
|
|
241
|
+
// A downstream that takes batches (the hub poster) gets the drained
|
|
242
|
+
// batch whole; a throw fails every job of it back to the queue's retry.
|
|
243
|
+
//
|
|
244
|
+
// A batch downstream may answer an outcome naming events it HELD rather
|
|
245
|
+
// than posted (PRDCT-2644: an `occurredAt` outside the hub's window).
|
|
246
|
+
// Each is sent to the held queue, which re-drives it here after
|
|
247
|
+
// `heldDelaySeconds`, forever, as it does a retry-budget hold: a
|
|
248
|
+
// future-dated event is posted once its time has come, a stale one stays
|
|
249
|
+
// held until an operator acts. No event the hub would refuse on its date
|
|
250
|
+
// is ever posted, and none is dropped.
|
|
251
|
+
await boss.work<UsageEvent>(USAGE_QUEUE, { batchSize: 50 }, async (jobs) => {
|
|
252
|
+
if (downstreamUsage.emitBatch) {
|
|
253
|
+
const outcome = await downstreamUsage.emitBatch(jobs.map((job) => job.data));
|
|
254
|
+
if (outcome && outcome.held.length > 0) {
|
|
255
|
+
const heldIds = new Set(outcome.held.map((h) => h.id));
|
|
256
|
+
const heldJobs = jobs.filter((job) => heldIds.has(job.data.id));
|
|
257
|
+
for (const job of heldJobs) {
|
|
258
|
+
await boss.send(USAGE_HELD_QUEUE, job.data, { singletonKey: windowHoldKey(job.data.id) });
|
|
259
|
+
}
|
|
260
|
+
logger.warn(
|
|
261
|
+
{
|
|
262
|
+
ids: heldJobs.map((job) => job.data.id),
|
|
263
|
+
reason: 'occurred_at_window',
|
|
264
|
+
retryAfterSeconds: usageRetry.heldDelaySeconds
|
|
265
|
+
},
|
|
266
|
+
'usage: events outside the hub’s occurredAt window — held, never posted, re-driven after the hold; check this instance’s clock (usage_events_held_total{reason="occurred_at_window"})'
|
|
267
|
+
);
|
|
268
|
+
// Counted once the sends are in, for the reason the held worker
|
|
269
|
+
// below gives: a send that throws fails this batch back to its
|
|
270
|
+
// retry, which would count the held events again.
|
|
271
|
+
held.inc({ reason: 'occurred_at_window' }, heldJobs.length);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
for (const job of jobs) {
|
|
275
|
+
await downstreamUsage.emit(job.data);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
logger.debug({ count: jobs.length }, 'usage events flushed');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// The budget's end is a HOLD, never a drop (PRDCT-2635): pg-boss moves a
|
|
282
|
+
// batch that exhausted its retries here; each event is logged where an
|
|
283
|
+
// operator sees it, counted on /metrics, and re-sent to the usage queue
|
|
284
|
+
// after the hold — round after round, until the hub accepts it. An
|
|
285
|
+
// outage of a day, an older hub, a wrong secret: the usage waits.
|
|
286
|
+
//
|
|
287
|
+
// Two kinds of job land here, told apart by the singleton key: pg-boss's
|
|
288
|
+
// dead-letter copy of a batch that ran out of retries carries none, the
|
|
289
|
+
// usage worker's window hold (PRDCT-2644) carries `windowHoldKey`. A
|
|
290
|
+
// window hold was logged and counted where it was held, so it is only
|
|
291
|
+
// re-driven here: logging or counting it again would name it a
|
|
292
|
+
// retry-budget hold, which it is not.
|
|
293
|
+
await boss.work<UsageEvent>(USAGE_HELD_QUEUE, { batchSize: 50, includeMetadata: true }, async (jobs) => {
|
|
294
|
+
const budget = jobs.filter((job) => !isWindowHoldKey(job.singletonKey));
|
|
295
|
+
if (budget.length > 0) {
|
|
296
|
+
logger.error(
|
|
297
|
+
{
|
|
298
|
+
count: budget.length,
|
|
299
|
+
ids: budget.map((job) => job.data.id),
|
|
300
|
+
retryAfterSeconds: usageRetry.heldDelaySeconds
|
|
301
|
+
},
|
|
302
|
+
'usage: events exhausted the retry budget — held, re-driven after the hold; check the hub, HUB_CLIENT_ID/HUB_CLIENT_SECRET and the instance’s registry entry (usage_events_held_total)'
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
for (const job of jobs) {
|
|
306
|
+
await boss.send(USAGE_QUEUE, job.data, {
|
|
307
|
+
...usageSendOptions(job.data, usageRetry),
|
|
308
|
+
startAfter: usageRetry.heldDelaySeconds
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Counted once the re-sends are in: a re-send that throws midway
|
|
312
|
+
// fails the held batch back to its own retries, which re-send the
|
|
313
|
+
// whole batch (the hub answers duplicate for what already went) and
|
|
314
|
+
// would count it again if the count came first.
|
|
315
|
+
if (budget.length > 0) held.inc({ reason: 'retry_budget' }, budget.length);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Audit retention: a nightly purge keeps audit_log from growing without
|
|
319
|
+
// bound (machine reads land a row each — see audit/service.ts).
|
|
320
|
+
await boss.work(AUDIT_PURGE_QUEUE, async () => {
|
|
321
|
+
if (retentionDays <= 0) return;
|
|
322
|
+
// Delete in bounded batches: one huge DELETE would seq-scan and blow the
|
|
323
|
+
// pool's statement_timeout on a large table (retention then silently
|
|
324
|
+
// never runs). Each batch touches at most BATCH rows (fast, index-backed
|
|
325
|
+
// by audit_log_created_idx) and loops until the tail is drained.
|
|
326
|
+
const BATCH = 5000;
|
|
327
|
+
let total = 0;
|
|
328
|
+
for (let i = 0; i < 10_000; i++) {
|
|
329
|
+
const res = await db.execute(sql`
|
|
330
|
+
DELETE FROM audit_log
|
|
331
|
+
WHERE id IN (
|
|
332
|
+
SELECT id FROM audit_log
|
|
333
|
+
WHERE created_at < now() - make_interval(days => ${retentionDays})
|
|
334
|
+
ORDER BY created_at
|
|
335
|
+
LIMIT ${BATCH}
|
|
336
|
+
)
|
|
337
|
+
`);
|
|
338
|
+
const deleted = res.rowCount ?? 0;
|
|
339
|
+
total += deleted;
|
|
340
|
+
if (deleted < BATCH) break;
|
|
341
|
+
}
|
|
342
|
+
logger.info({ retentionDays, deleted: total }, 'audit retention purge ran');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// API-key expiry: resolution already rejects expired keys fail-closed —
|
|
346
|
+
// this nightly sweep only normalizes them to the visible terminal state
|
|
347
|
+
// (revoked_at = expires_at). api_keys is a tiny bounded table, so one
|
|
348
|
+
// unbatched UPDATE is fine (the audit-purge batching lesson applies to
|
|
349
|
+
// unbounded tables only).
|
|
350
|
+
await boss.work(APIKEY_EXPIRY_QUEUE, async () => {
|
|
351
|
+
const res = await db.execute(sql`
|
|
352
|
+
UPDATE api_keys
|
|
353
|
+
SET revoked_at = expires_at
|
|
354
|
+
WHERE expires_at < now() AND revoked_at IS NULL
|
|
355
|
+
`);
|
|
356
|
+
logger.info({ retired: res.rowCount ?? 0 }, 'api key expiry sweep ran');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Idempotency replay rows carry a 24h TTL (a protocol constant, the
|
|
360
|
+
// Stripe convention — not an env knob), so the table can never accumulate
|
|
361
|
+
// beyond a day's create traffic: one unbatched, index-backed DELETE is
|
|
362
|
+
// fine (the audit-purge batching lesson applies to unbounded tables only).
|
|
363
|
+
await boss.work(IDEMPOTENCY_PURGE_QUEUE, async () => {
|
|
364
|
+
const res = await db.execute(sql`
|
|
365
|
+
DELETE FROM idempotency_keys WHERE expires_at < now()
|
|
366
|
+
`);
|
|
367
|
+
logger.info({ deleted: res.rowCount ?? 0 }, 'idempotency purge ran');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// The tool's pollers, registered where they always were: after the
|
|
371
|
+
// chassis purges, before the orphan GC. One job per fetch (pg-boss's
|
|
372
|
+
// default batch), its payload and its key handed to the handler: a timer
|
|
373
|
+
// (`./timers.ts`) rides the same poller as a cron.
|
|
374
|
+
for (const job of toolJobs) {
|
|
375
|
+
await boss.work<object>(job.queue, { includeMetadata: true }, async (jobs) => {
|
|
376
|
+
for (const fetched of jobs) {
|
|
377
|
+
await job.handler(fetched.data ?? null, {
|
|
378
|
+
id: fetched.id,
|
|
379
|
+
queue: job.queue,
|
|
380
|
+
key: fetched.singletonKey ?? null
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Orphaned-user GC: setup-race losers, fail-closed SSO login strands
|
|
387
|
+
// (cloud), and any other path leave Better Auth `user` rows with NO
|
|
388
|
+
// workspace_members row — they can sign in but 401 everywhere, and they
|
|
389
|
+
// accumulate forever. Cloud makes some of them CREDENTIAL-BEARING: a
|
|
390
|
+
// fail-closed hub login strands a user + `antasphere` account row
|
|
391
|
+
// HOLDING a live encrypted offline grant (Better Auth writes tokens
|
|
392
|
+
// before the after-hook revokes the session), minted at an
|
|
393
|
+
// unauthenticated-reachable rate during a hub outage — so a hub link
|
|
394
|
+
// must NEVER protect a row from collection (that would accumulate
|
|
395
|
+
// dormant refresh families without bound).
|
|
396
|
+
//
|
|
397
|
+
// What distinguishes the LEGIT zero-membership user (the cloud operator
|
|
398
|
+
// pre-break-glass, a hub user whose last org was removed — both real
|
|
399
|
+
// dashboard users of the /me zero state) from the strand is the LIVE
|
|
400
|
+
// SESSION: the strand's was revoked by the fail-closed login; the legit
|
|
401
|
+
// user holds one. So the sweep collects zero-membership users with NO
|
|
402
|
+
// unexpired session, REGARDLESS of any provider link, plus two explicit
|
|
403
|
+
// exclusions: a live pending invitation (mid-onboarding) and the
|
|
404
|
+
// SUPERADMIN_EMAILS allowlist (the break-glass operator must survive
|
|
405
|
+
// even after their session lapses — ADR 010's "arm the allowlist"
|
|
406
|
+
// guidance, now enforced instead of advised).
|
|
407
|
+
//
|
|
408
|
+
// SAFETY INVARIANTS:
|
|
409
|
+
// - a user with ANY membership row — even a deactivated one — is a
|
|
410
|
+
// real member and is NEVER touched;
|
|
411
|
+
// - ── HARD CONSTRAINT (user-scoped federation) ──────────────────────
|
|
412
|
+
// the purge must NEVER delete an `antasphere` account row while
|
|
413
|
+
// leaving an `origin='hub'` membership row behind: that would make
|
|
414
|
+
// the reconciler's `no_link` branch — today unreachable for
|
|
415
|
+
// hub-origin principals, and the one unconditional fail-open —
|
|
416
|
+
// REACHABLE, i.e. a fail-open authorization hole. Held by
|
|
417
|
+
// construction: the only deletion here is the WHOLE user through
|
|
418
|
+
// Better Auth's internalAdapter.deleteUser (the exact path both GDPR
|
|
419
|
+
// delete surfaces use), whose FK cascade removes account rows AND
|
|
420
|
+
// membership rows together, and only ever for users the immediately
|
|
421
|
+
// preceding re-check proved to have ZERO membership rows. Never
|
|
422
|
+
// replace this with a partial cleanup (account rows, tokens) — whole
|
|
423
|
+
// user or nothing.
|
|
424
|
+
await boss.work(ORPHAN_USER_PURGE_QUEUE, async () => {
|
|
425
|
+
if (orphanRetentionHours <= 0) return; // 0 = disabled (schedule is off too)
|
|
426
|
+
const authCtx = await auth.$context;
|
|
427
|
+
// As ONE json parameter (never a JS array param — driver array
|
|
428
|
+
// serialization is not worth trusting in a deletion query).
|
|
429
|
+
const superadmins = JSON.stringify([...parseSuperadminEmails(env.SUPERADMIN_EMAILS)]);
|
|
430
|
+
const BATCH = 50;
|
|
431
|
+
let total = 0;
|
|
432
|
+
const sample: string[] = [];
|
|
433
|
+
for (let i = 0; i < 200; i++) {
|
|
434
|
+
// Candidates: zero memberships, past the grace period, NO live
|
|
435
|
+
// (unexpired) session, no LIVE pending invitation for their email —
|
|
436
|
+
// an orphan someone just re-invited is mid-onboarding, not garbage
|
|
437
|
+
// (the invitation row exists before its accept URL does, so this
|
|
438
|
+
// closes the scan-then-accept race for the invited case) — and not
|
|
439
|
+
// a superadmin-allowlisted address. The exclusions live in the
|
|
440
|
+
// query (not a JS skip): a skipped candidate would otherwise fill
|
|
441
|
+
// the batch forever and starve the real orphans behind it.
|
|
442
|
+
const res = await db.execute(sql`
|
|
443
|
+
SELECT u.id, u.email FROM "user" u
|
|
444
|
+
WHERE NOT EXISTS (
|
|
445
|
+
SELECT 1 FROM workspace_members wm WHERE wm.user_id = u.id
|
|
446
|
+
)
|
|
447
|
+
AND u.created_at < now() - make_interval(hours => ${orphanRetentionHours})
|
|
448
|
+
AND NOT EXISTS (
|
|
449
|
+
SELECT 1 FROM session s WHERE s.user_id = u.id AND s.expires_at > now()
|
|
450
|
+
)
|
|
451
|
+
AND NOT EXISTS (
|
|
452
|
+
SELECT 1 FROM invitations inv
|
|
453
|
+
WHERE lower(inv.email) = lower(u.email)
|
|
454
|
+
AND inv.accepted_at IS NULL
|
|
455
|
+
AND inv.revoked_at IS NULL
|
|
456
|
+
AND inv.expires_at > now()
|
|
457
|
+
)
|
|
458
|
+
-- BG-2: the allowlist protects exactly what break-glass ACCEPTS —
|
|
459
|
+
-- a VERIFIED allowlisted address. An unverified squat of an
|
|
460
|
+
-- allowlisted email is no operator and gets no shelter.
|
|
461
|
+
AND NOT EXISTS (
|
|
462
|
+
SELECT 1 FROM jsonb_array_elements_text(${superadmins}::jsonb) AS sa(email)
|
|
463
|
+
WHERE sa.email = lower(u.email) AND u.email_verified
|
|
464
|
+
)
|
|
465
|
+
-- CLOUD-3: the setup operator (instance_settings.operator_user_id)
|
|
466
|
+
-- is NEVER an orphan — on cloud they hold no membership by
|
|
467
|
+
-- design, and deleting them loses the only local credential and
|
|
468
|
+
-- the only break-glass identity. Durable, allowlist or not.
|
|
469
|
+
AND u.id IS DISTINCT FROM (SELECT operator_user_id FROM instance_settings LIMIT 1)
|
|
470
|
+
LIMIT ${BATCH}
|
|
471
|
+
`);
|
|
472
|
+
const rows = res.rows as unknown as Array<{ id: string; email: string }>;
|
|
473
|
+
if (rows.length === 0) break;
|
|
474
|
+
let progressed = 0;
|
|
475
|
+
for (const row of rows) {
|
|
476
|
+
// Final re-check immediately before the delete: a membership OR a
|
|
477
|
+
// live session (the user signing in mid-sweep) can appear between
|
|
478
|
+
// the candidate scan and here — either one disqualifies.
|
|
479
|
+
const still = await db.execute(sql`
|
|
480
|
+
SELECT 1 FROM workspace_members WHERE user_id = ${row.id}
|
|
481
|
+
UNION ALL
|
|
482
|
+
SELECT 1 FROM session WHERE user_id = ${row.id} AND expires_at > now()
|
|
483
|
+
LIMIT 1
|
|
484
|
+
`);
|
|
485
|
+
if ((still.rows?.length ?? 0) > 0) continue;
|
|
486
|
+
try {
|
|
487
|
+
await authCtx.internalAdapter.deleteUser(row.id);
|
|
488
|
+
await authCtx.internalAdapter.deleteUserSessions(row.id);
|
|
489
|
+
total += 1;
|
|
490
|
+
progressed += 1;
|
|
491
|
+
if (sample.length < 20) sample.push(row.email);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
// Skip and keep sweeping: a failed delete (e.g. the 0009
|
|
494
|
+
// last-owner trigger backstopping the pathological race where
|
|
495
|
+
// the user gained a sole-owner membership mid-sweep) aborts
|
|
496
|
+
// THIS user only.
|
|
497
|
+
logger.warn({ err, userId: row.id }, 'orphan purge: skipping user (delete failed)');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (rows.length < BATCH) break;
|
|
501
|
+
if (progressed === 0) break; // every candidate failed/was skipped — no hot loop
|
|
502
|
+
}
|
|
503
|
+
if (total > 0) {
|
|
504
|
+
// INSTANCE-attributed system row (workspace_id NULL, ADR 014):
|
|
505
|
+
// orphans belong to no workspace, so no workspace's audit trail is
|
|
506
|
+
// the honest home — the row is an operator-level record.
|
|
507
|
+
await audit.write({
|
|
508
|
+
workspaceId: null,
|
|
509
|
+
principal: null, // → actorVia 'system'
|
|
510
|
+
action: 'user.orphan_purge',
|
|
511
|
+
resourceType: 'user',
|
|
512
|
+
metadata: { deleted: total, retentionHours: orphanRetentionHours, sample }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
logger.info({ retentionHours: orphanRetentionHours, deleted: total }, 'orphan user purge ran');
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
// api role: migrate:false start() runs no DDL and creates no queues — it
|
|
519
|
+
// only verifies the pgboss schema exists ('pg-boss is not installed' on a
|
|
520
|
+
// fresh DB until an all|worker boot installs it; internal/scale-drill-findings.md), so it
|
|
521
|
+
// needs neither the install lock nor DDL rights.
|
|
522
|
+
await boss.start();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
boss,
|
|
527
|
+
timers: new PgBossTimers(boss, pool, logger),
|
|
528
|
+
stop: async () => {
|
|
529
|
+
await boss.stop({ graceful: true, wait: true });
|
|
530
|
+
},
|
|
531
|
+
promMetrics: [held]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The UsageSink bound into the registry: durable, batched emission via
|
|
537
|
+
* pg-boss. Domain code calls emit() and never blocks on the downstream.
|
|
538
|
+
*/
|
|
539
|
+
export interface UsageRetry {
|
|
540
|
+
/** Retries before a batch is held (pg-boss `retryLimit`). */
|
|
541
|
+
limit: number;
|
|
542
|
+
/** The first retry's delay in seconds, doubled at each retry (`retryDelay` + `retryBackoff`). */
|
|
543
|
+
delaySeconds: number;
|
|
544
|
+
/** How long a held event waits before its next round through the queue. */
|
|
545
|
+
heldDelaySeconds: number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The queue's retry budget: ten retries with exponential backoff from thirty
|
|
550
|
+
* seconds, about eight and a half hours in all — a hub deploy (the hub
|
|
551
|
+
* deploys first, and answers 404 until it has) or an outage of an afternoon
|
|
552
|
+
* costs nothing but time. The budget's END is a hold, not a loss
|
|
553
|
+
* (PRDCT-2635): the batch moves to `USAGE_HELD_QUEUE`, is logged and
|
|
554
|
+
* counted, and comes back through the queue an hour later, and an hour
|
|
555
|
+
* after that, until the hub accepts it. At-least-once is the posture; the
|
|
556
|
+
* hub dedupes by ULID.
|
|
557
|
+
*/
|
|
558
|
+
export const DEFAULT_USAGE_RETRY: UsageRetry = { limit: 10, delaySeconds: 30, heldDelaySeconds: 3600 };
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The ONE statement of how a usage event is sent: the sink and the held
|
|
562
|
+
* queue's re-drive share it. The dead letter (where the batch goes when the
|
|
563
|
+
* budget runs out: held, never dropped) is the QUEUE's setting, installed
|
|
564
|
+
* by the worker boot, so a send names none and an api-role replica needs
|
|
565
|
+
* nothing to exist beyond the usage queue itself.
|
|
566
|
+
*/
|
|
567
|
+
export function usageSendOptions(event: UsageEvent, retry: UsageRetry): PgBoss.SendOptions {
|
|
568
|
+
return {
|
|
569
|
+
// The event's ULID, for a reader of the queue table; on a standard
|
|
570
|
+
// pg-boss queue a singleton key enforces nothing (verifier round 1), and
|
|
571
|
+
// idempotency is the receiver's job anyway (dedupe by ULID, per contract).
|
|
572
|
+
singletonKey: event.id,
|
|
573
|
+
retryLimit: retry.limit,
|
|
574
|
+
retryDelay: retry.delaySeconds,
|
|
575
|
+
retryBackoff: true
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export class PgBossUsageSink implements UsageSink {
|
|
580
|
+
constructor(
|
|
581
|
+
private readonly boss: PgBoss,
|
|
582
|
+
private readonly logger: Logger,
|
|
583
|
+
private readonly retry: UsageRetry = DEFAULT_USAGE_RETRY
|
|
584
|
+
) {}
|
|
585
|
+
|
|
586
|
+
async emit(event: UsageEvent): Promise<void> {
|
|
587
|
+
try {
|
|
588
|
+
await this.boss.send(USAGE_QUEUE, event, usageSendOptions(event, this.retry));
|
|
589
|
+
} catch (err) {
|
|
590
|
+
// Usage must never break the request path.
|
|
591
|
+
this.logger.error({ err, meter: event.meter }, 'usage emit failed');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|