@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,572 @@
|
|
|
1
|
+
import { Counter } from 'prom-client';
|
|
2
|
+
import { and, eq, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';
|
|
3
|
+
import { workspaceMembers, workspaces, type Db } from '@antasphere/chassis-db';
|
|
4
|
+
import type { AuditService } from '../audit/service.js';
|
|
5
|
+
import type { Logger } from '../logger.js';
|
|
6
|
+
import { deleteMembershipGrants, type MembershipRemovalHook } from '../members/removal.js';
|
|
7
|
+
import { projectOrgMembership, projectOrgTeams, projectTeamSeats } from './hub-projection.js';
|
|
8
|
+
import type { HubDeniedOrg, HubUserClient, LoginAccessToken } from './hub-user-client.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Live org reconciliation (internal/federation.md, user-scoped federation): org
|
|
12
|
+
* and membership truth live ONLY at the hub, and the tool reflects it by
|
|
13
|
+
* reading the hub's caller-scoped `GET /orgs` AS THE USER (hub-user-client)
|
|
14
|
+
* — every SSO login runs the TTL-bypassing `forceReconcile()` (fail-CLOSED:
|
|
15
|
+
* a login whose pass fails is revoked — reconcile IS the projection), and
|
|
16
|
+
* the live gate (hub-live-gate.ts) runs the cached `reconcile()` on every
|
|
17
|
+
* request into a projected workspace. One pass fetches the user's full hub
|
|
18
|
+
* org list and makes the local projections match: project missing orgs,
|
|
19
|
+
* sync names/roles, sync `workspaces.hub_status` and the user's
|
|
20
|
+
* `workspace_members.is_default`, deactivate `origin='hub'` memberships the
|
|
21
|
+
* hub no longer asserts.
|
|
22
|
+
*
|
|
23
|
+
* Failure posture: a pass mutates state ONLY on a definitive 200 list.
|
|
24
|
+
* Transient hub errors keep local state serving (nothing deactivated) —
|
|
25
|
+
* the ENFORCEMENT of prolonged staleness is the gate's, via the windowed
|
|
26
|
+
* `reconcile()` verdict (fresh ≤ staleMaxMs → serve; beyond it, failing →
|
|
27
|
+
* `hub_unavailable`). A definitive `grant_dead` is its own verdict — the
|
|
28
|
+
* gate answers 401 `hub_grant_expired` immediately, no stale window.
|
|
29
|
+
*
|
|
30
|
+
* Cost discipline: per-user TTL cache (~10 s) + single-flight, so N
|
|
31
|
+
* concurrent requests cost one hub fetch; a pass on an expired cache IS
|
|
32
|
+
* awaited (freshness at request time is the whole point) but is bounded by
|
|
33
|
+
* the client's tight fetch timeout; failures re-probe at most once per
|
|
34
|
+
* `retryMs`. An oss boot never constructs this class.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Every dial of the live federation loop — one override surface for tests. */
|
|
38
|
+
export interface HubFederationDials {
|
|
39
|
+
/** Per-user reconcile TTL (the identity-path freshness bound). */
|
|
40
|
+
reconcileTtlMs: number;
|
|
41
|
+
/** Definitive-pass staleness bound; beyond it a failing hub gates fail-closed. */
|
|
42
|
+
reconcileStaleMaxMs: number;
|
|
43
|
+
/** Failed-pass re-probe throttle: at most one hub attempt per user per interval. */
|
|
44
|
+
retryMs: number;
|
|
45
|
+
/** GET /orgs fetch timeout (awaited in the identity path — keep tight). */
|
|
46
|
+
orgsTimeoutMs: number;
|
|
47
|
+
/** Token-endpoint fetch timeout (hub-grant.ts). */
|
|
48
|
+
tokenTimeoutMs: number;
|
|
49
|
+
/** Serve cached access tokens only to exp − this (hub-grant.ts). */
|
|
50
|
+
accessSkewMs: number;
|
|
51
|
+
/** Watchdog cutting a wedged refresh's dedicated lock connection (hub-grant.ts). */
|
|
52
|
+
lockWatchdogMs: number;
|
|
53
|
+
/**
|
|
54
|
+
* How old an organization's whole team list (`GET /teams`, PRDCT-2813) may
|
|
55
|
+
* grow before a pass reads it again, per org per replica. A failed read is
|
|
56
|
+
* re-tried at most once per `retryMs`.
|
|
57
|
+
*/
|
|
58
|
+
orgTeamsTtlMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_FEDERATION_DIALS: HubFederationDials = {
|
|
62
|
+
reconcileTtlMs: 10_000,
|
|
63
|
+
reconcileStaleMaxMs: 15 * 60_000,
|
|
64
|
+
retryMs: 15_000,
|
|
65
|
+
orgsTimeoutMs: 1_500,
|
|
66
|
+
tokenTimeoutMs: 5_000,
|
|
67
|
+
accessSkewMs: 60_000,
|
|
68
|
+
// Must cover probe + presentation under the refresh lock (hub-grant.ts
|
|
69
|
+
// clamps it up if not): 5 s + 5 s + 2 s headroom.
|
|
70
|
+
lockWatchdogMs: 12_000,
|
|
71
|
+
orgTeamsTtlMs: 5 * 60_000
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One pass's own outcome (what the metric labels count):
|
|
76
|
+
* - 'ok': definitive list applied;
|
|
77
|
+
* - 'no_link': the user holds no hub identity — nothing to reconcile,
|
|
78
|
+
* cached like a success;
|
|
79
|
+
* - 'grant_dead': the user's hub grant is definitively dead (cached like a
|
|
80
|
+
* success — the answer is real; a browser login heals it);
|
|
81
|
+
* - 'inconclusive': transient hub error, fail open;
|
|
82
|
+
* - 'error': unexpected local failure, fail open.
|
|
83
|
+
*/
|
|
84
|
+
export type ReconcilePassOutcome = 'ok' | 'no_link' | 'grant_dead' | 'inconclusive' | 'error';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The gate's windowed verdict:
|
|
88
|
+
* - 'ok': definitive data within the stale window — serve on local rows;
|
|
89
|
+
* - 'grant_dead': the grant is dead — the gate refuses immediately;
|
|
90
|
+
* - 'unavailable': no definitive pass within the window and the hub keeps
|
|
91
|
+
* failing — the gate fails closed.
|
|
92
|
+
*/
|
|
93
|
+
export type ReconcileVerdict = { kind: 'ok' } | { kind: 'grant_dead' } | { kind: 'unavailable' };
|
|
94
|
+
|
|
95
|
+
interface CacheEntry {
|
|
96
|
+
/** Timestamp of the last DEFINITIVE pass (the TTL + stale window anchor here). */
|
|
97
|
+
fetchedAt: number;
|
|
98
|
+
/** Timestamp of the last attempt, success or failure (outage throttle). */
|
|
99
|
+
lastAttemptAt: number;
|
|
100
|
+
/** The last DEFINITIVE outcome ('ok' | 'no_link' | 'grant_dead'). */
|
|
101
|
+
lastDefinitive: Extract<ReconcilePassOutcome, 'ok' | 'no_link' | 'grant_dead'> | null;
|
|
102
|
+
/**
|
|
103
|
+
* The organizations the hub's last definitive list named as NOT opening
|
|
104
|
+
* this tool to the person (`denied`). A transient hint for the refusal
|
|
105
|
+
* page, never an access input: the hub is the truth, and nothing here is
|
|
106
|
+
* stored. An inconclusive pass keeps the previous list; `no_link` and
|
|
107
|
+
* `grant_dead` clear it.
|
|
108
|
+
*/
|
|
109
|
+
denied: HubDeniedOrg[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** When an organization's whole team list was last read, per replica. */
|
|
113
|
+
interface OrgTeamsEntry {
|
|
114
|
+
/** The last successful read (0 = never). */
|
|
115
|
+
refreshedAt: number;
|
|
116
|
+
/** The last failed read (0 = none): the retry throttle and the once-per-window log. */
|
|
117
|
+
lastAttemptAt: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What one team-list refresh ended on (the metric's `outcome` label). */
|
|
121
|
+
type OrgTeamsRefreshOutcome = 'ok' | 'inconclusive' | 'grant_dead' | 'no_link' | 'error';
|
|
122
|
+
|
|
123
|
+
/** Bound the cache — a user-id flood must never balloon memory. */
|
|
124
|
+
const MAX_CACHE_ENTRIES = 10_000;
|
|
125
|
+
|
|
126
|
+
export interface HubOrgReconcilerDeps {
|
|
127
|
+
db: Db;
|
|
128
|
+
client: HubUserClient;
|
|
129
|
+
audit: AuditService;
|
|
130
|
+
logger: Logger;
|
|
131
|
+
dials: HubFederationDials;
|
|
132
|
+
/** The tool's half of a removal (the `membershipRemoval` slot), run in the sweep's transaction. */
|
|
133
|
+
onMembershipRemoval?: MembershipRemovalHook | undefined;
|
|
134
|
+
/** Test seam. */
|
|
135
|
+
now?: () => number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class HubOrgReconciler {
|
|
139
|
+
/** local userId → pass freshness. */
|
|
140
|
+
private readonly cache = new Map<string, CacheEntry>();
|
|
141
|
+
private readonly inFlight = new Map<string, Promise<ReconcilePassOutcome>>();
|
|
142
|
+
/** hub org id → its whole team list's freshness (bounded like the user cache). */
|
|
143
|
+
private readonly orgTeams = new Map<string, OrgTeamsEntry>();
|
|
144
|
+
private readonly now: () => number;
|
|
145
|
+
readonly dials: HubFederationDials;
|
|
146
|
+
|
|
147
|
+
/** Registered into the Prometheus registry by boot (cloud only). */
|
|
148
|
+
readonly promMetrics: Counter[];
|
|
149
|
+
private readonly passes: Counter;
|
|
150
|
+
private readonly orgTeamsRefreshes: Counter;
|
|
151
|
+
|
|
152
|
+
constructor(private readonly deps: HubOrgReconcilerDeps) {
|
|
153
|
+
this.now = deps.now ?? Date.now;
|
|
154
|
+
this.dials = deps.dials;
|
|
155
|
+
// registers: [] — boot attaches this to the app registry on cloud; an
|
|
156
|
+
// oss boot never constructs this class, so the metric never exists there.
|
|
157
|
+
this.passes = new Counter({
|
|
158
|
+
name: 'hub_reconcile_passes_total',
|
|
159
|
+
help: 'Hub org-list reconciliation passes by outcome',
|
|
160
|
+
labelNames: ['outcome'] as const,
|
|
161
|
+
registers: []
|
|
162
|
+
});
|
|
163
|
+
this.orgTeamsRefreshes = new Counter({
|
|
164
|
+
name: 'hub_org_teams_refresh_total',
|
|
165
|
+
help: 'Reads of a hub organization’s whole team list by outcome',
|
|
166
|
+
labelNames: ['outcome'] as const,
|
|
167
|
+
registers: []
|
|
168
|
+
});
|
|
169
|
+
this.promMetrics = [this.passes, this.orgTeamsRefreshes];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The identity-path read-through: cached per user (~10 s), single-flight,
|
|
174
|
+
* retry-throttled after failures. Awaits one pass on expiry — the request
|
|
175
|
+
* that triggered it observes the reconciled state — and NEVER throws.
|
|
176
|
+
* Returns the windowed verdict the live gate enforces.
|
|
177
|
+
*/
|
|
178
|
+
async reconcile(localUserId: string): Promise<ReconcileVerdict> {
|
|
179
|
+
const now = this.now();
|
|
180
|
+
const entry = this.cache.get(localUserId);
|
|
181
|
+
if (!entry || now - entry.fetchedAt >= this.dials.reconcileTtlMs) {
|
|
182
|
+
let flight = this.inFlight.get(localUserId);
|
|
183
|
+
if (!flight) {
|
|
184
|
+
// Outage throttle: after a FAILED pass (lastAttemptAt advanced past
|
|
185
|
+
// the last success), at most one re-probe per retryMs — a down hub
|
|
186
|
+
// costs each user one bounded timeout per window. A successful pass
|
|
187
|
+
// is never throttled beyond its own TTL.
|
|
188
|
+
const throttled =
|
|
189
|
+
entry !== undefined &&
|
|
190
|
+
entry.lastAttemptAt > entry.fetchedAt &&
|
|
191
|
+
now - entry.lastAttemptAt < this.dials.retryMs;
|
|
192
|
+
if (!throttled) {
|
|
193
|
+
flight = this.startPass(localUserId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (flight) await flight;
|
|
197
|
+
}
|
|
198
|
+
return this.verdict(localUserId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The login-time pass (assertLogin step 3): TTL and throttle deliberately
|
|
203
|
+
* bypassed — a login is the freshest signal there is, and `login` carries
|
|
204
|
+
* the callback access token so no refresh is needed. Returns the PASS'S
|
|
205
|
+
* OWN outcome (not the windowed verdict): the fail-CLOSED login requires
|
|
206
|
+
* this very pass to have succeeded, never a recent cached one.
|
|
207
|
+
*/
|
|
208
|
+
async forceReconcile(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome> {
|
|
209
|
+
const flight = this.runPass(localUserId, login).finally(() => {
|
|
210
|
+
if (this.inFlight.get(localUserId) === flight) this.inFlight.delete(localUserId);
|
|
211
|
+
});
|
|
212
|
+
// Concurrent cached reconciles join this pass instead of doubling it.
|
|
213
|
+
this.inFlight.set(localUserId, flight);
|
|
214
|
+
return flight;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private startPass(localUserId: string): Promise<ReconcilePassOutcome> {
|
|
218
|
+
const flight = this.runPass(localUserId, undefined).finally(() => {
|
|
219
|
+
if (this.inFlight.get(localUserId) === flight) this.inFlight.delete(localUserId);
|
|
220
|
+
});
|
|
221
|
+
this.inFlight.set(localUserId, flight);
|
|
222
|
+
return flight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private verdict(localUserId: string): ReconcileVerdict {
|
|
226
|
+
const entry = this.cache.get(localUserId);
|
|
227
|
+
const now = this.now();
|
|
228
|
+
if (entry && entry.lastDefinitive && now - entry.fetchedAt < this.dials.reconcileStaleMaxMs) {
|
|
229
|
+
// Definitive data within the window. A definitive DEAD grant refuses
|
|
230
|
+
// immediately (no stale grace — Romain's decision 3); 'ok'/'no_link'
|
|
231
|
+
// serve on local rows.
|
|
232
|
+
return entry.lastDefinitive === 'grant_dead' ? { kind: 'grant_dead' } : { kind: 'ok' };
|
|
233
|
+
}
|
|
234
|
+
// No definitive pass within the window (or ever, e.g. a replica that
|
|
235
|
+
// restarted mid-outage) and the hub keeps failing: fail closed.
|
|
236
|
+
return { kind: 'unavailable' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async runPass(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome> {
|
|
240
|
+
try {
|
|
241
|
+
const result = await this.deps.client.orgs(localUserId, login);
|
|
242
|
+
if (result.kind === 'no_link') {
|
|
243
|
+
// A purely local user: the hub asserts nothing about them, and that
|
|
244
|
+
// IS the definitive answer. Nothing is swept.
|
|
245
|
+
//
|
|
246
|
+
// EXCEPT (CLOUD-1, PRDCT-1356) when the user HOLDS origin='hub'
|
|
247
|
+
// membership rows: those rows exist only because a hub link once
|
|
248
|
+
// projected them, so "no link" means the link was SEVERED (an
|
|
249
|
+
// unlinked account row, a partial cleanup) — and treating that as
|
|
250
|
+
// the fail-open definitive answer, cached like a success, was the
|
|
251
|
+
// one way to switch cloud enforcement off permanently. It fails
|
|
252
|
+
// CLOSED instead, as a dead grant: the gate answers 401
|
|
253
|
+
// hub_grant_expired, and a browser SSO re-login (which re-links)
|
|
254
|
+
// heals it. Local/guest rows are untouched — they never reconcile.
|
|
255
|
+
const [hubRow] = await this.deps.db
|
|
256
|
+
.select({ id: workspaceMembers.id })
|
|
257
|
+
.from(workspaceMembers)
|
|
258
|
+
.where(and(eq(workspaceMembers.userId, localUserId), eq(workspaceMembers.origin, 'hub')))
|
|
259
|
+
.limit(1);
|
|
260
|
+
if (hubRow) {
|
|
261
|
+
this.deps.logger.warn(
|
|
262
|
+
{ userId: localUserId },
|
|
263
|
+
'hub org reconcile: no hub link but hub-origin memberships exist — link severed; failing CLOSED'
|
|
264
|
+
);
|
|
265
|
+
this.remember(localUserId, 'grant_dead');
|
|
266
|
+
this.passes.inc({ outcome: 'link_severed' });
|
|
267
|
+
return 'grant_dead';
|
|
268
|
+
}
|
|
269
|
+
this.remember(localUserId, 'no_link');
|
|
270
|
+
this.passes.inc({ outcome: 'no_link' });
|
|
271
|
+
return 'no_link';
|
|
272
|
+
}
|
|
273
|
+
if (result.kind === 'grant_dead') {
|
|
274
|
+
// Definitive: the grant family is gone. Freshness-wise a real
|
|
275
|
+
// answer (cached); enforcement is the gate's 401 hub_grant_expired.
|
|
276
|
+
// Nothing is swept — org truth was not read, only the grant state.
|
|
277
|
+
this.remember(localUserId, 'grant_dead');
|
|
278
|
+
this.passes.inc({ outcome: 'grant_dead' });
|
|
279
|
+
return 'grant_dead';
|
|
280
|
+
}
|
|
281
|
+
if (result.kind === 'inconclusive') {
|
|
282
|
+
// Loudly logged by the client already; retry at the next window.
|
|
283
|
+
this.remember(localUserId, null);
|
|
284
|
+
this.passes.inc({ outcome: 'inconclusive' });
|
|
285
|
+
return 'inconclusive';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Definitive 200 list — the ONLY thing that may ever mutate state.
|
|
289
|
+
// Every listed org is kept, suspended ones included (visible but
|
|
290
|
+
// blocked: the synced hub_status is what refuses their requests);
|
|
291
|
+
// role-null entries are kept but not projected/synced.
|
|
292
|
+
const keep = new Set(result.orgs.map((o) => o.id));
|
|
293
|
+
|
|
294
|
+
// Deactivation sweep FIRST (removal must win over projection within
|
|
295
|
+
// one pass): ONE conditional UPDATE covering exactly the rows the hub
|
|
296
|
+
// no longer asserts — origin='hub' AND active AND the workspace IS a
|
|
297
|
+
// projection whose hub org is off the keep-set. local/guest rows and
|
|
298
|
+
// rows in non-projected workspaces are structurally out of scope.
|
|
299
|
+
// `is_active = true` in the WHERE makes the flip observable: only the
|
|
300
|
+
// pass that actually flipped a row audits it — one logical removal
|
|
301
|
+
// writes ONE member.deactivate row.
|
|
302
|
+
const keepIds = [...keep];
|
|
303
|
+
const sweptWorkspaces = this.deps.db
|
|
304
|
+
.select({ id: workspaces.id })
|
|
305
|
+
.from(workspaces)
|
|
306
|
+
.where(
|
|
307
|
+
keepIds.length > 0
|
|
308
|
+
? and(isNotNull(workspaces.centralAccountId), notInArray(workspaces.centralAccountId, keepIds))
|
|
309
|
+
: isNotNull(workspaces.centralAccountId)
|
|
310
|
+
);
|
|
311
|
+
// The project grants that rode on a swept membership go WITH it, in the
|
|
312
|
+
// same transaction. The sweep deactivates the row and never deletes it
|
|
313
|
+
// (a re-add at the hub reactivates the very same row), so the foreign
|
|
314
|
+
// key's cascade never fires here: without this delete, a person removed
|
|
315
|
+
// at the hub and added back later would find every old project grant
|
|
316
|
+
// waiting. A removal is a removal: a re-add starts with none. (A LOCAL
|
|
317
|
+
// deactivation by an admin is a pause, not a removal, and keeps them.)
|
|
318
|
+
const deactivated = await this.deps.db.transaction(async (tx) => {
|
|
319
|
+
const rows = await tx
|
|
320
|
+
.update(workspaceMembers)
|
|
321
|
+
.set({ isActive: false })
|
|
322
|
+
.where(
|
|
323
|
+
and(
|
|
324
|
+
eq(workspaceMembers.userId, localUserId),
|
|
325
|
+
eq(workspaceMembers.origin, 'hub'),
|
|
326
|
+
eq(workspaceMembers.isActive, true),
|
|
327
|
+
inArray(workspaceMembers.workspaceId, sweptWorkspaces)
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
.returning({ id: workspaceMembers.id, workspaceId: workspaceMembers.workspaceId });
|
|
331
|
+
// The grants and the seats go with the membership: the one statement
|
|
332
|
+
// of what a removal takes, shared with the tool's own removal route.
|
|
333
|
+
await deleteMembershipGrants(
|
|
334
|
+
tx,
|
|
335
|
+
rows.map((row) => ({ memberId: row.id, workspaceId: row.workspaceId, userId: localUserId })),
|
|
336
|
+
this.deps.onMembershipRemoval
|
|
337
|
+
);
|
|
338
|
+
return rows;
|
|
339
|
+
});
|
|
340
|
+
for (const row of deactivated) {
|
|
341
|
+
await this.deps.audit.write({
|
|
342
|
+
workspaceId: row.workspaceId,
|
|
343
|
+
principal: null,
|
|
344
|
+
action: 'member.deactivate',
|
|
345
|
+
resourceType: 'member',
|
|
346
|
+
resourceId: localUserId,
|
|
347
|
+
metadata: { reason: 'hub_reconcile' }
|
|
348
|
+
});
|
|
349
|
+
this.deps.logger.info(
|
|
350
|
+
{ userId: localUserId, workspaceId: row.workspaceId },
|
|
351
|
+
'hub reconcile: membership no longer asserted by the hub — local membership deactivated'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Then project/upsert every well-formed entry (role upserts are
|
|
356
|
+
// deliberately NOT audited — the login-projection precedent), the
|
|
357
|
+
// person's team seats in it, and sync the org-level hub_status. One
|
|
358
|
+
// failing entry logs and moves on: a single bad org must never starve
|
|
359
|
+
// the others.
|
|
360
|
+
for (const org of result.orgs) {
|
|
361
|
+
try {
|
|
362
|
+
if (org.role !== null) {
|
|
363
|
+
const projected = await projectOrgMembership(this.deps.db, {
|
|
364
|
+
localUserId,
|
|
365
|
+
hubWorkspaceId: org.id,
|
|
366
|
+
hubWorkspaceName: org.name,
|
|
367
|
+
role: org.role
|
|
368
|
+
});
|
|
369
|
+
// The person's own seats in this org's teams: one transaction
|
|
370
|
+
// per org, so a seat set is never half-applied.
|
|
371
|
+
await this.deps.db.transaction(async (tx) => {
|
|
372
|
+
await projectTeamSeats(tx, {
|
|
373
|
+
workspaceId: projected.workspaceId,
|
|
374
|
+
memberId: projected.memberId,
|
|
375
|
+
teams: org.teams
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
// Then the org's whole team list, when it is due (never fails the pass).
|
|
379
|
+
await this.refreshOrgTeams(localUserId, org.id, projected.workspaceId, login !== undefined);
|
|
380
|
+
}
|
|
381
|
+
// Org-level suspension is LOCALLY MATERIALIZED truth: the gate
|
|
382
|
+
// (and guests' requests — the accepted staleness bound) read this
|
|
383
|
+
// column, never a hub round-trip of their own. A role-null entry
|
|
384
|
+
// still syncs it when the projection already exists.
|
|
385
|
+
await this.deps.db
|
|
386
|
+
.update(workspaces)
|
|
387
|
+
.set({ hubStatus: org.status })
|
|
388
|
+
.where(and(eq(workspaces.centralAccountId, org.id), ne(workspaces.hubStatus, org.status)));
|
|
389
|
+
} catch (err) {
|
|
390
|
+
this.deps.logger.error(
|
|
391
|
+
{ err, userId: localUserId, hubOrgId: org.id },
|
|
392
|
+
'hub reconcile: projecting one org failed — continuing with the rest'
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Materialize the hub-level default org on the user's membership rows
|
|
398
|
+
// (selection happens inside the resolvers, before any gate — the flag
|
|
399
|
+
// must live locally). Clear-then-set in ONE transaction: the partial
|
|
400
|
+
// unique index (user_id WHERE is_default) forbids two trues even
|
|
401
|
+
// transiently, so a single both-ways UPDATE could trip it on row
|
|
402
|
+
// order. A cross-replica race still can — the violation aborts only
|
|
403
|
+
// this step and self-heals next pass.
|
|
404
|
+
try {
|
|
405
|
+
const defaultOrgId = result.orgs.find((o) => o.isDefault)?.id ?? null;
|
|
406
|
+
let defaultWorkspaceId: string | null = null;
|
|
407
|
+
if (defaultOrgId) {
|
|
408
|
+
const [ws] = await this.deps.db
|
|
409
|
+
.select({ id: workspaces.id })
|
|
410
|
+
.from(workspaces)
|
|
411
|
+
.where(eq(workspaces.centralAccountId, defaultOrgId))
|
|
412
|
+
.limit(1);
|
|
413
|
+
defaultWorkspaceId = ws?.id ?? null;
|
|
414
|
+
}
|
|
415
|
+
await this.deps.db.transaction(async (tx) => {
|
|
416
|
+
await tx
|
|
417
|
+
.update(workspaceMembers)
|
|
418
|
+
.set({ isDefault: false })
|
|
419
|
+
.where(
|
|
420
|
+
and(
|
|
421
|
+
eq(workspaceMembers.userId, localUserId),
|
|
422
|
+
eq(workspaceMembers.isDefault, true),
|
|
423
|
+
...(defaultWorkspaceId ? [ne(workspaceMembers.workspaceId, defaultWorkspaceId)] : [])
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
if (defaultWorkspaceId) {
|
|
427
|
+
await tx
|
|
428
|
+
.update(workspaceMembers)
|
|
429
|
+
.set({ isDefault: true })
|
|
430
|
+
.where(
|
|
431
|
+
and(
|
|
432
|
+
eq(workspaceMembers.userId, localUserId),
|
|
433
|
+
eq(workspaceMembers.workspaceId, defaultWorkspaceId),
|
|
434
|
+
eq(workspaceMembers.isDefault, false)
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
} catch (err) {
|
|
440
|
+
this.deps.logger.warn(
|
|
441
|
+
{ err, userId: localUserId },
|
|
442
|
+
'hub reconcile: default-org sync failed — selection falls back to oldest-active until the next pass'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.remember(localUserId, 'ok', result.denied);
|
|
447
|
+
this.passes.inc({ outcome: 'ok' });
|
|
448
|
+
return 'ok';
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// Fail open, always: reconciliation is freshness; enforcement lives
|
|
451
|
+
// in the gate's windowed verdict.
|
|
452
|
+
this.remember(localUserId, null);
|
|
453
|
+
this.passes.inc({ outcome: 'error' });
|
|
454
|
+
this.deps.logger.error(
|
|
455
|
+
{ err, userId: localUserId },
|
|
456
|
+
'hub org reconcile pass failed — serving local state'
|
|
457
|
+
);
|
|
458
|
+
return 'error';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Read the organization's WHOLE team list as this person and project it
|
|
464
|
+
* (PRDCT-2813), when the last read is older than `orgTeamsTtlMs` (or never
|
|
465
|
+
* happened) and no failed read happened within `retryMs`. The LOGIN pass
|
|
466
|
+
* reads it whatever the window says (`force`): a login is the freshest
|
|
467
|
+
* signal there is, and Romain's rule is "at login, then every five minutes". A refinement
|
|
468
|
+
* of a pass whose org list already succeeded: it never throws and never
|
|
469
|
+
* fails the pass; a failed read keeps the previous list and is logged once
|
|
470
|
+
* per throttle window.
|
|
471
|
+
*
|
|
472
|
+
* No lock: two replicas (or two passes) may both refresh the same org. The
|
|
473
|
+
* upsert is idempotent and the delete is by membership of the list each
|
|
474
|
+
* one read, so the last writer leaves the hub's list, whichever it is.
|
|
475
|
+
*/
|
|
476
|
+
private async refreshOrgTeams(
|
|
477
|
+
localUserId: string,
|
|
478
|
+
hubOrgId: string,
|
|
479
|
+
workspaceId: string,
|
|
480
|
+
/** A login: the freshest signal there is, read whatever the window says (the login pass's own rule). */
|
|
481
|
+
force = false
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
const now = this.now();
|
|
484
|
+
const entry = this.orgTeams.get(hubOrgId);
|
|
485
|
+
if (entry) {
|
|
486
|
+
// A login skips the window, never the failure throttle: a hub whose
|
|
487
|
+
// team list hangs must not cost every sign-in one timeout per org
|
|
488
|
+
// (verifier round 2).
|
|
489
|
+
if (!force && entry.refreshedAt > 0 && now - entry.refreshedAt < this.dials.orgTeamsTtlMs) return;
|
|
490
|
+
if (entry.lastAttemptAt > entry.refreshedAt && now - entry.lastAttemptAt < this.dials.retryMs) return;
|
|
491
|
+
}
|
|
492
|
+
let outcome: OrgTeamsRefreshOutcome;
|
|
493
|
+
try {
|
|
494
|
+
// Stamped BEFORE the read, on the DATABASE's clock (the one the rows'
|
|
495
|
+
// `created_at` is written from, whatever the app host's says; never the
|
|
496
|
+
// test seam): a team projected here while the hub answered is newer
|
|
497
|
+
// than the list and is not deleted by it.
|
|
498
|
+
const [clock] = (await this.deps.db.execute(sql`SELECT now() AS now`)).rows as Array<{
|
|
499
|
+
now: Date | string;
|
|
500
|
+
}>;
|
|
501
|
+
const readAt = new Date(clock!.now);
|
|
502
|
+
const result = await this.deps.client.orgTeams(localUserId, hubOrgId);
|
|
503
|
+
if (result.kind === 'ok') {
|
|
504
|
+
await this.deps.db.transaction(async (tx) => {
|
|
505
|
+
await projectOrgTeams(tx, { workspaceId, teams: result.teams, complete: result.complete, readAt });
|
|
506
|
+
});
|
|
507
|
+
outcome = 'ok';
|
|
508
|
+
} else {
|
|
509
|
+
outcome = result.kind;
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
this.deps.logger.warn({ err, hubOrgId }, 'hub reconcile: projecting the org’s team list failed');
|
|
513
|
+
outcome = 'error';
|
|
514
|
+
}
|
|
515
|
+
this.orgTeamsRefreshes.inc({ outcome });
|
|
516
|
+
const at = this.now();
|
|
517
|
+
pruneOversized(this.orgTeams, (e) => at - e.refreshedAt >= this.dials.orgTeamsTtlMs);
|
|
518
|
+
const prev = this.orgTeams.get(hubOrgId);
|
|
519
|
+
if (outcome === 'ok') {
|
|
520
|
+
this.orgTeams.set(hubOrgId, { refreshedAt: at, lastAttemptAt: prev?.lastAttemptAt ?? 0 });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this.orgTeams.set(hubOrgId, { refreshedAt: prev?.refreshedAt ?? 0, lastAttemptAt: at });
|
|
524
|
+
if (outcome !== 'error') {
|
|
525
|
+
this.deps.logger.warn(
|
|
526
|
+
{ hubOrgId, outcome },
|
|
527
|
+
'hub reconcile: the org’s team list could not be read — keeping the previous one'
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* The organizations the person's last definitive hub list named as not
|
|
534
|
+
* opening this tool to them (`GET /me`'s `hubDenied`, the refusal page's
|
|
535
|
+
* words). Empty when unknown: never read, evicted, or cleared by a
|
|
536
|
+
* `no_link` / `grant_dead` pass. Per replica, like the rest of the cache.
|
|
537
|
+
*/
|
|
538
|
+
deniedOrgs(localUserId: string): HubDeniedOrg[] {
|
|
539
|
+
return this.cache.get(localUserId)?.denied ?? [];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private remember(
|
|
543
|
+
localUserId: string,
|
|
544
|
+
definitive: Extract<ReconcilePassOutcome, 'ok' | 'no_link' | 'grant_dead'> | null,
|
|
545
|
+
denied: HubDeniedOrg[] = []
|
|
546
|
+
): void {
|
|
547
|
+
const now = this.now();
|
|
548
|
+
pruneOversized(this.cache, (entry) => now - entry.fetchedAt >= this.dials.reconcileTtlMs);
|
|
549
|
+
const prev = this.cache.get(localUserId);
|
|
550
|
+
this.cache.set(localUserId, {
|
|
551
|
+
fetchedAt: definitive ? now : (prev?.fetchedAt ?? 0),
|
|
552
|
+
lastAttemptAt: now,
|
|
553
|
+
lastDefinitive: definitive ?? prev?.lastDefinitive ?? null,
|
|
554
|
+
// 'ok' carries the fresh list; a failed pass keeps the last one; a
|
|
555
|
+
// definitive no_link / grant_dead has no list to show.
|
|
556
|
+
denied: definitive === 'ok' ? denied : definitive === null ? (prev?.denied ?? []) : []
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Keep a cache under MAX_CACHE_ENTRIES: drop expired entries first, then oldest-inserted. */
|
|
562
|
+
export function pruneOversized<V>(cache: Map<string, V>, expired: (value: V) => boolean): void {
|
|
563
|
+
if (cache.size < MAX_CACHE_ENTRIES) return;
|
|
564
|
+
for (const [key, value] of cache) {
|
|
565
|
+
if (expired(value)) cache.delete(key);
|
|
566
|
+
}
|
|
567
|
+
while (cache.size >= MAX_CACHE_ENTRIES) {
|
|
568
|
+
const oldest = cache.keys().next();
|
|
569
|
+
if (oldest.done) break;
|
|
570
|
+
cache.delete(oldest.value);
|
|
571
|
+
}
|
|
572
|
+
}
|