@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,646 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
3
|
+
import {
|
|
4
|
+
createDatabase,
|
|
5
|
+
createTestApp,
|
|
6
|
+
extractCookie,
|
|
7
|
+
readJson,
|
|
8
|
+
startPostgres,
|
|
9
|
+
type TestApp,
|
|
10
|
+
host
|
|
11
|
+
} from './helpers.js';
|
|
12
|
+
import { FakeHub, type HubUserFixture } from '@antasphere/chassis-server/testing';
|
|
13
|
+
import * as sso from './sso-helpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Live org reconciliation (identity/hub-reconcile.ts): org and membership
|
|
17
|
+
* truth live ONLY at the hub, read AS THE USER via each user's own grant
|
|
18
|
+
* (never a service key), and every request into a projected workspace
|
|
19
|
+
* reflects it — the live gate runs the cached `reconcile()`, every SSO
|
|
20
|
+
* login the TTL-bypassing force pass. One pass projects missing orgs,
|
|
21
|
+
* syncs names/roles/hub_status/is_default, and deactivates `origin='hub'`
|
|
22
|
+
* memberships the hub no longer asserts.
|
|
23
|
+
*
|
|
24
|
+
* Failure posture pinned here: only a definitive 200 list mutates state;
|
|
25
|
+
* transient hub errors serve local state (nothing deactivated) inside the
|
|
26
|
+
* stale window; probes are retry-throttled. Enforcement verdicts
|
|
27
|
+
* (revoked/suspended/unavailable/grant-dead) are the live-gate suite's.
|
|
28
|
+
*
|
|
29
|
+
* Dials are shrunk so propagation is observable in milliseconds; the stale
|
|
30
|
+
* window is pinned LONG so this suite never trips hub_unavailable.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const OWNER = { email: 'owner@reconcile.test', name: 'Op Owner', password: 'op-owner-password-123' };
|
|
34
|
+
|
|
35
|
+
const ORG_ONE = '33333333-aaaa-4bbb-8ccc-000000000001';
|
|
36
|
+
const ORG_TWO = '33333333-aaaa-4bbb-8ccc-000000000002';
|
|
37
|
+
const ORG_THREE = '33333333-aaaa-4bbb-8ccc-000000000003';
|
|
38
|
+
const ORG_TINA = '33333333-aaaa-4bbb-8ccc-000000000005';
|
|
39
|
+
const ORG_FRESH = '33333333-aaaa-4bbb-8ccc-000000000006';
|
|
40
|
+
const ORG_WEIRD = '33333333-aaaa-4bbb-8ccc-000000000009';
|
|
41
|
+
const ORG_DANA = '33333333-aaaa-4bbb-8ccc-00000000000a';
|
|
42
|
+
const ORG_BG = '33333333-aaaa-4bbb-8ccc-00000000000b';
|
|
43
|
+
const ORG_OPX = '33333333-aaaa-4bbb-8ccc-00000000000c';
|
|
44
|
+
|
|
45
|
+
const DIALS = {
|
|
46
|
+
reconcileTtlMs: 120,
|
|
47
|
+
reconcileStaleMaxMs: 60_000,
|
|
48
|
+
retryMs: 250,
|
|
49
|
+
orgsTimeoutMs: 400,
|
|
50
|
+
tokenTimeoutMs: 2_000
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
54
|
+
/** Let the per-user reconcile cache expire so the next request runs a fresh pass. */
|
|
55
|
+
const expireTtl = () => sleep(DIALS.reconcileTtlMs + 40);
|
|
56
|
+
|
|
57
|
+
let container: StartedPostgreSqlContainer;
|
|
58
|
+
let hub: FakeHub;
|
|
59
|
+
let app: TestApp;
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
[container, hub] = await Promise.all([startPostgres(), FakeHub.start({ clientId: host.hubClientId })]);
|
|
63
|
+
app = await createTestApp(
|
|
64
|
+
await createDatabase(container, 'hub_reconcile'),
|
|
65
|
+
{
|
|
66
|
+
EDITION: 'cloud',
|
|
67
|
+
HUB_ISSUER_URL: hub.issuer,
|
|
68
|
+
HUB_CLIENT_ID: host.hubClientId,
|
|
69
|
+
HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001',
|
|
70
|
+
METRICS_TOKEN: 'reconcile-metrics-token',
|
|
71
|
+
// The break-glass lifeboat suite below acts as the setup operator.
|
|
72
|
+
SUPERADMIN_EMAILS: OWNER.email
|
|
73
|
+
},
|
|
74
|
+
{ hubDials: DIALS }
|
|
75
|
+
);
|
|
76
|
+
const res = await app.app.request(
|
|
77
|
+
'/api/v1/setup',
|
|
78
|
+
sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Reconcile', owner: OWNER })
|
|
79
|
+
);
|
|
80
|
+
expect(res.status).toBe(201);
|
|
81
|
+
}, 240_000);
|
|
82
|
+
|
|
83
|
+
afterAll(async () => {
|
|
84
|
+
await app?.stop();
|
|
85
|
+
await Promise.all([container?.stop(), hub?.stop()]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const me = async (cookie: string, workspaceId?: string) =>
|
|
89
|
+
app.app.request('/api/v1/me', {
|
|
90
|
+
headers: { cookie, ...(workspaceId ? { 'x-workspace-id': workspaceId } : {}) }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const orgFetches = () => hub.orgsRequests.length;
|
|
94
|
+
|
|
95
|
+
async function workspaceIdOf(centralAccountId: string): Promise<string> {
|
|
96
|
+
const { rows } = await app.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
|
|
97
|
+
centralAccountId
|
|
98
|
+
]);
|
|
99
|
+
expect(rows).toHaveLength(1);
|
|
100
|
+
return rows[0].id as string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function membershipRow(centralAccountId: string, email: string) {
|
|
104
|
+
const { rows } = await app.db.pool.query(
|
|
105
|
+
`SELECT m.role, m.origin, m.is_active FROM workspace_members m
|
|
106
|
+
JOIN workspaces w ON w.id = m.workspace_id
|
|
107
|
+
JOIN "user" u ON u.id = m.user_id
|
|
108
|
+
WHERE w.central_account_id = $1 AND u.email = $2`,
|
|
109
|
+
[centralAccountId, email]
|
|
110
|
+
);
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe('login-time reconciliation (the fail-closed assertLogin pass)', () => {
|
|
115
|
+
const rick: HubUserFixture = {
|
|
116
|
+
sub: 'hub-rick',
|
|
117
|
+
email: 'rick@reconcile.test',
|
|
118
|
+
name: 'Rick Reconciled',
|
|
119
|
+
workspaceId: ORG_ONE,
|
|
120
|
+
role: 'owner',
|
|
121
|
+
workspaceName: 'Rick Org'
|
|
122
|
+
};
|
|
123
|
+
let rickCookie: string;
|
|
124
|
+
|
|
125
|
+
it('ONE login materializes ALL of the user’s hub orgs, not just the one they came from', async () => {
|
|
126
|
+
// The hub knows rick in a SECOND org before he ever logs in here.
|
|
127
|
+
hub.setUserOrg('hub-rick', ORG_TWO, { name: 'Second Org', role: 'member' });
|
|
128
|
+
rickCookie = await sso.ssoLogin(app, hub, rick);
|
|
129
|
+
|
|
130
|
+
const body = await readJson(await me(rickCookie));
|
|
131
|
+
const byName = Object.fromEntries(
|
|
132
|
+
body.workspaces.map((w: { name: string; role: string; hubOrigin: boolean }) => [w.name, w])
|
|
133
|
+
);
|
|
134
|
+
expect(byName['Rick Org']).toMatchObject({ role: 'owner', hubOrigin: true });
|
|
135
|
+
expect(byName['Second Org']).toMatchObject({ role: 'member', hubOrigin: true });
|
|
136
|
+
expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
|
|
137
|
+
{ role: 'member', origin: 'hub', is_active: true }
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('a fresh hub org appears IMMEDIATELY after a re-login (force pass bypasses the TTL)', async () => {
|
|
142
|
+
await me(rickCookie); // warm the cache …
|
|
143
|
+
hub.setUserOrg('hub-rick', ORG_FRESH, { name: 'Fresh Org', role: 'admin' });
|
|
144
|
+
await sso.ssoLogin(app, hub, rick); // … the login must NOT wait for its expiry
|
|
145
|
+
const body = await readJson(await me(rickCookie));
|
|
146
|
+
expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Fresh Org');
|
|
147
|
+
hub.removeUserOrg('hub-rick', ORG_FRESH); // keep the later pins focused
|
|
148
|
+
await expireTtl();
|
|
149
|
+
await me(rickCookie); // sweep it back out
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('request-time reconciliation (the live gate’s read-through)', () => {
|
|
153
|
+
it('an org created at the hub appears at the next reload', async () => {
|
|
154
|
+
hub.setUserOrg('hub-rick', ORG_THREE, { name: 'Third Org', role: 'member' });
|
|
155
|
+
await expireTtl();
|
|
156
|
+
const body = await readJson(await me(rickCookie));
|
|
157
|
+
expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Third Org');
|
|
158
|
+
expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
|
|
159
|
+
{ role: 'member', origin: 'hub', is_active: true }
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('rename and role change at the hub propagate at the next reload', async () => {
|
|
164
|
+
hub.setUserOrg('hub-rick', ORG_THREE, { name: 'Third Org GmbH', role: 'admin' });
|
|
165
|
+
await expireTtl();
|
|
166
|
+
const body = await readJson(await me(rickCookie));
|
|
167
|
+
const third = body.workspaces.find((w: { name: string }) => w.name === 'Third Org GmbH');
|
|
168
|
+
expect(third).toMatchObject({ role: 'admin', hubOrigin: true });
|
|
169
|
+
expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
|
|
170
|
+
{ role: 'admin', origin: 'hub', is_active: true }
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('a hub-side removal deactivates the projection with exactly ONE audit row; the workspace dies', async () => {
|
|
175
|
+
hub.removeUserOrg('hub-rick', ORG_THREE);
|
|
176
|
+
await expireTtl();
|
|
177
|
+
const body = await readJson(await me(rickCookie));
|
|
178
|
+
expect(body.workspaces.map((w: { name: string }) => w.name)).not.toContain('Third Org GmbH');
|
|
179
|
+
expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
|
|
180
|
+
{ role: 'admin', origin: 'hub', is_active: false }
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
// Naming the swept workspace now fails closed (no membership resolves
|
|
184
|
+
// — the miss-hook retries one cached reconcile and still finds none).
|
|
185
|
+
const wsThree = await workspaceIdOf(ORG_THREE);
|
|
186
|
+
expect((await me(rickCookie, wsThree)).status).toBe(401);
|
|
187
|
+
|
|
188
|
+
// Exactly ONE audit row per flip — system actor, hub_reconcile reason.
|
|
189
|
+
await expireTtl();
|
|
190
|
+
await me(rickCookie); // an extra pass must NOT audit again (row already inactive)
|
|
191
|
+
const { rows: audit } = await app.db.pool.query(
|
|
192
|
+
`SELECT a.actor_via FROM audit_log a JOIN workspaces w ON w.id = a.workspace_id
|
|
193
|
+
WHERE w.central_account_id = $1 AND a.action = 'member.deactivate'
|
|
194
|
+
AND a.metadata->>'reason' = 'hub_reconcile'`,
|
|
195
|
+
[ORG_THREE]
|
|
196
|
+
);
|
|
197
|
+
expect(audit).toEqual([{ actor_via: 'system' }]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('an unknown-role entry keeps the membership ALIVE but is never projected/synced', async () => {
|
|
201
|
+
// A brand-new org with an unknown role: never projected.
|
|
202
|
+
hub.setUserOrg('hub-rick', ORG_WEIRD, { name: 'Weird Org', role: 'emperor' });
|
|
203
|
+
await expireTtl();
|
|
204
|
+
const body = await readJson(await me(rickCookie));
|
|
205
|
+
expect(body.workspaces.map((w: { name: string }) => w.name)).not.toContain('Weird Org');
|
|
206
|
+
const { rows } = await app.db.pool.query(`SELECT 1 FROM workspaces WHERE central_account_id = $1`, [
|
|
207
|
+
ORG_WEIRD
|
|
208
|
+
]);
|
|
209
|
+
expect(rows).toHaveLength(0);
|
|
210
|
+
|
|
211
|
+
// An EXISTING org whose entry turns unknown-role: kept alive (never
|
|
212
|
+
// read as absence), local role kept (no sync from a malformed entry).
|
|
213
|
+
hub.setUserOrg('hub-rick', ORG_ONE, { name: 'Rick Org', role: 'emperor' });
|
|
214
|
+
await expireTtl();
|
|
215
|
+
await me(rickCookie);
|
|
216
|
+
expect(await membershipRow(ORG_ONE, rick.email)).toEqual([
|
|
217
|
+
{ role: 'owner', origin: 'hub', is_active: true }
|
|
218
|
+
]);
|
|
219
|
+
hub.setUserOrg('hub-rick', ORG_ONE, { name: 'Rick Org', role: 'owner' });
|
|
220
|
+
hub.removeUserOrg('hub-rick', ORG_WEIRD);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('failure posture: hub errors fail OPEN inside the stale window, retry-throttled', () => {
|
|
225
|
+
it('5xx: local state served, nothing deactivated, ONE probe per retry window', async () => {
|
|
226
|
+
await expireTtl();
|
|
227
|
+
await me(rickCookie); // fresh definitive pass first
|
|
228
|
+
hub.orgsMode = 'http500';
|
|
229
|
+
try {
|
|
230
|
+
await expireTtl();
|
|
231
|
+
const before = orgFetches();
|
|
232
|
+
const first = await readJson(await me(rickCookie));
|
|
233
|
+
expect(first.workspaces.length).toBeGreaterThanOrEqual(2); // ORG_ONE + ORG_TWO intact
|
|
234
|
+
expect(orgFetches()).toBe(before + 1);
|
|
235
|
+
// Within retryMs: no second probe, still 200 on local state.
|
|
236
|
+
expect((await me(rickCookie)).status).toBe(200);
|
|
237
|
+
expect((await me(rickCookie)).status).toBe(200);
|
|
238
|
+
expect(orgFetches()).toBe(before + 1);
|
|
239
|
+
} finally {
|
|
240
|
+
hub.orgsMode = 'ok';
|
|
241
|
+
}
|
|
242
|
+
expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
|
|
243
|
+
{ role: 'member', origin: 'hub', is_active: true }
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('timeout: the awaited pass is bounded by orgsTimeoutMs and fails open', async () => {
|
|
248
|
+
await sleep(DIALS.retryMs + 40);
|
|
249
|
+
await me(rickCookie);
|
|
250
|
+
hub.orgsDelayMs = DIALS.orgsTimeoutMs + 400;
|
|
251
|
+
try {
|
|
252
|
+
await expireTtl();
|
|
253
|
+
const startedAt = Date.now();
|
|
254
|
+
expect((await me(rickCookie)).status).toBe(200);
|
|
255
|
+
expect(Date.now() - startedAt).toBeLessThan(DIALS.orgsTimeoutMs + 600);
|
|
256
|
+
} finally {
|
|
257
|
+
hub.orgsDelayMs = 0;
|
|
258
|
+
}
|
|
259
|
+
expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
|
|
260
|
+
{ role: 'member', origin: 'hub', is_active: true }
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('recovers after the throttle window: the next reload reconciles again', async () => {
|
|
265
|
+
hub.setUserOrg('hub-rick', ORG_FRESH, { name: 'Fresh Org', role: 'member' });
|
|
266
|
+
await sleep(DIALS.retryMs + 40);
|
|
267
|
+
const body = await readJson(await me(rickCookie));
|
|
268
|
+
expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Fresh Org');
|
|
269
|
+
hub.removeUserOrg('hub-rick', ORG_FRESH);
|
|
270
|
+
await expireTtl();
|
|
271
|
+
await me(rickCookie);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('cost discipline: cache, single-flight — and machine credentials reconcile too', () => {
|
|
276
|
+
it('N concurrent reloads share ONE fetch; a fresh cache costs zero', async () => {
|
|
277
|
+
await expireTtl();
|
|
278
|
+
hub.orgsDelayMs = 100; // hold all racers in flight together
|
|
279
|
+
let before: number;
|
|
280
|
+
try {
|
|
281
|
+
before = orgFetches();
|
|
282
|
+
const responses = await Promise.all(Array.from({ length: 8 }, () => me(rickCookie)));
|
|
283
|
+
for (const res of responses) expect(res.status).toBe(200);
|
|
284
|
+
} finally {
|
|
285
|
+
hub.orgsDelayMs = 0;
|
|
286
|
+
}
|
|
287
|
+
expect(orgFetches()).toBe(before + 1); // single-flight
|
|
288
|
+
expect((await me(rickCookie)).status).toBe(200); // within the TTL
|
|
289
|
+
expect(orgFetches()).toBe(before + 1); // cache hit, no fetch
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('an API key into a projected workspace reconciles like a session (a key IS its user)', async () => {
|
|
293
|
+
const minted = await app.app.request('/api/v1/api-keys', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'content-type': 'application/json', cookie: rickCookie },
|
|
296
|
+
body: JSON.stringify({ name: 'reconcile key', scopes: [host.scopes.read] })
|
|
297
|
+
});
|
|
298
|
+
expect(minted.status).toBe(201);
|
|
299
|
+
const { key } = await readJson(minted);
|
|
300
|
+
await expireTtl();
|
|
301
|
+
const before = orgFetches();
|
|
302
|
+
const res = await app.app.request('/api/v1/me', { headers: { authorization: `Bearer ${key}` } });
|
|
303
|
+
expect(res.status).toBe(200);
|
|
304
|
+
const body = await readJson(res);
|
|
305
|
+
// The user-scoped model: /me lists ALL the holder's workspaces…
|
|
306
|
+
expect(body.workspaces.length).toBeGreaterThanOrEqual(2);
|
|
307
|
+
// …and the request ran a real as-the-user reconcile pass.
|
|
308
|
+
expect(orgFetches()).toBe(before + 1);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('the sweep’s blast radius: ONLY hub rows in projected workspaces', () => {
|
|
314
|
+
it('an empty definitive list deactivates hub projections and NOTHING else', async () => {
|
|
315
|
+
const tina: HubUserFixture = {
|
|
316
|
+
sub: 'hub-tina',
|
|
317
|
+
email: 'tina@reconcile.test',
|
|
318
|
+
workspaceId: ORG_TINA,
|
|
319
|
+
role: 'owner',
|
|
320
|
+
workspaceName: 'Tina Org'
|
|
321
|
+
};
|
|
322
|
+
const cookie = await sso.ssoLogin(app, hub, tina);
|
|
323
|
+
expect((await me(cookie)).status).toBe(200);
|
|
324
|
+
const { rows: users } = await app.db.pool.query(`SELECT id FROM "user" WHERE email = $1`, [tina.email]);
|
|
325
|
+
const tinaId = users[0].id as string;
|
|
326
|
+
|
|
327
|
+
// Three rows the sweep must NEVER touch: a local row in an unprojected
|
|
328
|
+
// workspace, a guest row, and a hub-origin RESIDUE row whose workspace
|
|
329
|
+
// is not a projection (centralAccountId NULL).
|
|
330
|
+
const mkWorkspace = async (name: string) => {
|
|
331
|
+
const { rows } = await app.db.pool.query(`INSERT INTO workspaces (name) VALUES ($1) RETURNING id`, [
|
|
332
|
+
name
|
|
333
|
+
]);
|
|
334
|
+
return rows[0].id as string;
|
|
335
|
+
};
|
|
336
|
+
const localWs = await mkWorkspace('Tina Local');
|
|
337
|
+
const guestWs = await mkWorkspace('Tina Guest Host');
|
|
338
|
+
const residueWs = await mkWorkspace('Tina Residue');
|
|
339
|
+
await app.db.pool.query(
|
|
340
|
+
`INSERT INTO workspace_members (workspace_id, user_id, role, origin, is_active) VALUES
|
|
341
|
+
($1, $4, 'member', 'local', true),
|
|
342
|
+
($2, $4, 'member', 'guest', true),
|
|
343
|
+
($3, $4, 'member', 'hub', true)`,
|
|
344
|
+
[localWs, guestWs, residueWs, tinaId]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// The hub now asserts NO orgs for tina.
|
|
348
|
+
hub.clearUserOrgs('hub-tina');
|
|
349
|
+
await expireTtl();
|
|
350
|
+
// The FIRST request into the projected workspace runs the sweep and is
|
|
351
|
+
// refused by the gate's own verdict — enforcement and freshness are one
|
|
352
|
+
// pass now.
|
|
353
|
+
const swept = await me(cookie);
|
|
354
|
+
expect(swept.status).toBe(401);
|
|
355
|
+
expect((await readJson(swept)).error.code).toBe('membership_revoked');
|
|
356
|
+
|
|
357
|
+
const { rows: after } = await app.db.pool.query(
|
|
358
|
+
`SELECT w.name, m.origin, m.is_active FROM workspace_members m
|
|
359
|
+
JOIN workspaces w ON w.id = m.workspace_id
|
|
360
|
+
WHERE m.user_id = $1 ORDER BY w.name`,
|
|
361
|
+
[tinaId]
|
|
362
|
+
);
|
|
363
|
+
expect(after).toEqual([
|
|
364
|
+
{ name: 'Tina Guest Host', origin: 'guest', is_active: true },
|
|
365
|
+
{ name: 'Tina Local', origin: 'local', is_active: true },
|
|
366
|
+
{ name: 'Tina Org', origin: 'hub', is_active: false }, // the ONE swept row
|
|
367
|
+
{ name: 'Tina Residue', origin: 'hub', is_active: true } // unprojected: out of scope
|
|
368
|
+
]);
|
|
369
|
+
const { rows: audit } = await app.db.pool.query(
|
|
370
|
+
`SELECT a.workspace_id FROM audit_log a
|
|
371
|
+
WHERE a.action = 'member.deactivate' AND a.metadata->>'reason' = 'hub_reconcile'
|
|
372
|
+
AND a.resource_id = $1`,
|
|
373
|
+
[tinaId]
|
|
374
|
+
);
|
|
375
|
+
expect(audit).toHaveLength(1);
|
|
376
|
+
|
|
377
|
+
// Tina still reaches her LOCAL workspace: the session survives; only
|
|
378
|
+
// the hub-asserted membership died.
|
|
379
|
+
const local = await me(cookie, localWs);
|
|
380
|
+
expect(local.status).toBe(200);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('the unknown-workspace retry: a named miss reconciles once and re-resolves — all three credential kinds', () => {
|
|
385
|
+
const dana: HubUserFixture = {
|
|
386
|
+
sub: 'hub-dana',
|
|
387
|
+
email: 'dana@reconcile.test',
|
|
388
|
+
workspaceId: ORG_DANA,
|
|
389
|
+
role: 'owner',
|
|
390
|
+
workspaceName: 'Dana Org'
|
|
391
|
+
};
|
|
392
|
+
let danaCookie: string;
|
|
393
|
+
let wsTwo: string;
|
|
394
|
+
|
|
395
|
+
it('“invited at the hub, clicks a deep link” resolves without a re-login (session)', async () => {
|
|
396
|
+
danaCookie = await sso.ssoLogin(app, hub, dana);
|
|
397
|
+
// Granted membership of rick's ORG_TWO at the hub AFTER her login; its
|
|
398
|
+
// workspace already exists locally (rick's projection) — the deep-link
|
|
399
|
+
// URL names it, but dana holds no local membership row yet.
|
|
400
|
+
hub.setUserOrg('hub-dana', ORG_TWO, { name: 'Second Org', role: 'member' });
|
|
401
|
+
wsTwo = await workspaceIdOf(ORG_TWO);
|
|
402
|
+
await expireTtl(); // the retry rides the CACHED reconcile — let it expire
|
|
403
|
+
const res = await me(danaCookie, wsTwo);
|
|
404
|
+
expect(res.status).toBe(200);
|
|
405
|
+
const body = await readJson(res);
|
|
406
|
+
expect(body.activeWorkspaceId).toBe(wsTwo);
|
|
407
|
+
expect(body.role).toBe('member');
|
|
408
|
+
expect(await membershipRow(ORG_TWO, dana.email)).toEqual([
|
|
409
|
+
{ role: 'member', origin: 'hub', is_active: true }
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('an API key naming a freshly granted org resolves through the same retry', async () => {
|
|
414
|
+
const minted = await app.app.request('/api/v1/api-keys', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'content-type': 'application/json', cookie: danaCookie },
|
|
417
|
+
body: JSON.stringify({ name: 'dana key', scopes: [host.scopes.read] })
|
|
418
|
+
});
|
|
419
|
+
expect(minted.status).toBe(201);
|
|
420
|
+
const { key } = await readJson(minted);
|
|
421
|
+
// Deactivate the row the session retry just projected — the KEY's own
|
|
422
|
+
// miss must re-project it.
|
|
423
|
+
await app.db.pool.query(
|
|
424
|
+
`UPDATE workspace_members SET is_active = false
|
|
425
|
+
WHERE workspace_id = $1 AND user_id = (SELECT id FROM "user" WHERE email = $2)`,
|
|
426
|
+
[wsTwo, dana.email]
|
|
427
|
+
);
|
|
428
|
+
await expireTtl();
|
|
429
|
+
const res = await app.app.request('/api/v1/me', {
|
|
430
|
+
headers: { authorization: `Bearer ${key}`, 'x-workspace-id': wsTwo }
|
|
431
|
+
});
|
|
432
|
+
expect(res.status).toBe(200);
|
|
433
|
+
expect((await readJson(res)).activeWorkspaceId).toBe(wsTwo);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('an OAuth bearer naming a freshly granted org resolves through the same retry', async () => {
|
|
437
|
+
const bearer = await sso.oauthBearer(app, danaCookie);
|
|
438
|
+
await app.db.pool.query(
|
|
439
|
+
`UPDATE workspace_members SET is_active = false
|
|
440
|
+
WHERE workspace_id = $1 AND user_id = (SELECT id FROM "user" WHERE email = $2)`,
|
|
441
|
+
[wsTwo, dana.email]
|
|
442
|
+
);
|
|
443
|
+
await expireTtl();
|
|
444
|
+
const res = await app.app.request('/api/v1/me', {
|
|
445
|
+
headers: { authorization: `Bearer ${bearer}`, 'x-workspace-id': wsTwo }
|
|
446
|
+
});
|
|
447
|
+
expect(res.status).toBe(200);
|
|
448
|
+
expect((await readJson(res)).activeWorkspaceId).toBe(wsTwo);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('a workspace the hub does not grant stays a fail-closed 401 (one retry, no oracle)', async () => {
|
|
452
|
+
await expireTtl();
|
|
453
|
+
// A real workspace she has no grant for, and a phantom id: same answer.
|
|
454
|
+
const solo = await me(danaCookie, await workspaceIdOf(ORG_ONE));
|
|
455
|
+
expect(solo.status).toBe(401);
|
|
456
|
+
const phantom = await me(danaCookie, '99999999-9999-4999-8999-999999999999');
|
|
457
|
+
expect(phantom.status).toBe(401);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('observability', () => {
|
|
462
|
+
it('hub_reconcile_passes_total and hub_grant_refreshes_total are registered and counting', async () => {
|
|
463
|
+
const res = await app.app.request('/metrics', {
|
|
464
|
+
headers: { authorization: 'Bearer reconcile-metrics-token' }
|
|
465
|
+
});
|
|
466
|
+
expect(res.status).toBe(200);
|
|
467
|
+
const text = await res.text();
|
|
468
|
+
expect(text).toMatch(/hub_reconcile_passes_total\{outcome="ok"\} [1-9]/);
|
|
469
|
+
expect(text).toMatch(/hub_reconcile_passes_total\{outcome="inconclusive"\} [1-9]/);
|
|
470
|
+
expect(text).toMatch(/hub_grant_refreshes_total/);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('break-glass claim on a PROJECTED workspace: the origin=local lifeboat (pin)', () => {
|
|
475
|
+
// Decision 2 (user-scoped federation): cloud setup mints no workspace, so
|
|
476
|
+
// operator recovery targets PROJECTED workspaces — break-glass
|
|
477
|
+
// claim-ownership writes an `origin='local'` owner row there, and the
|
|
478
|
+
// reconcile sweep is scoped to `origin='hub'` rows ONLY. This pin proves
|
|
479
|
+
// the whole lifeboat: the zero-membership operator claims a projection,
|
|
480
|
+
// gains dashboard access, and KEEPS it through the maximal sweep — a
|
|
481
|
+
// reconcile pass whose hub org list is EMPTY.
|
|
482
|
+
let operatorCookie: string;
|
|
483
|
+
let wsBg: string;
|
|
484
|
+
|
|
485
|
+
it('a zero-membership operator claims ownership of a projected workspace', async () => {
|
|
486
|
+
// A hub user projects ORG_BG first (the workspace break-glass targets).
|
|
487
|
+
await sso.ssoLogin(app, hub, {
|
|
488
|
+
sub: 'hub-bg-owner',
|
|
489
|
+
email: 'bg-owner@reconcile.test',
|
|
490
|
+
workspaceId: ORG_BG,
|
|
491
|
+
role: 'owner',
|
|
492
|
+
workspaceName: 'BG Org'
|
|
493
|
+
});
|
|
494
|
+
wsBg = await workspaceIdOf(ORG_BG);
|
|
495
|
+
|
|
496
|
+
// The operator: setup ran in beforeAll — a verified user with ZERO
|
|
497
|
+
// memberships (cloud setup creates no workspace). The break-glass door
|
|
498
|
+
// is the wired-but-hidden password sign-in.
|
|
499
|
+
const signIn = await app.app.request(
|
|
500
|
+
'/api/v1/auth/sign-in/email',
|
|
501
|
+
sso.json({ email: OWNER.email, password: OWNER.password })
|
|
502
|
+
);
|
|
503
|
+
expect(signIn.status).toBe(200);
|
|
504
|
+
operatorCookie = extractCookie(signIn);
|
|
505
|
+
const zero = await readJson(await me(operatorCookie));
|
|
506
|
+
expect(zero.workspaces).toEqual([]);
|
|
507
|
+
|
|
508
|
+
const claim = await app.app.request('/api/v1/admin/break-glass/claim-ownership', {
|
|
509
|
+
...sso.json({ workspaceId: wsBg }),
|
|
510
|
+
headers: { 'content-type': 'application/json', cookie: operatorCookie, 'x-forwarded-for': '10.88.0.1' }
|
|
511
|
+
});
|
|
512
|
+
expect(claim.status).toBe(200);
|
|
513
|
+
const claimed = await readJson(claim);
|
|
514
|
+
expect(claimed.role).toBe('owner');
|
|
515
|
+
expect(claimed.created).toBe(true);
|
|
516
|
+
|
|
517
|
+
// Dashboard access: the projected workspace resolves for the operator,
|
|
518
|
+
// via the local row (the live gate skips hub enforcement for it).
|
|
519
|
+
const meRes = await me(operatorCookie, wsBg);
|
|
520
|
+
expect(meRes.status).toBe(200);
|
|
521
|
+
const body = await readJson(meRes);
|
|
522
|
+
expect(body.activeWorkspaceId).toBe(wsBg);
|
|
523
|
+
expect(body.origin).toBe('local');
|
|
524
|
+
expect(body.role).toBe('owner');
|
|
525
|
+
|
|
526
|
+
// The row break-glass wrote is origin='local' — the sweep's blind spot,
|
|
527
|
+
// by design.
|
|
528
|
+
const { rows } = await app.db.pool.query(
|
|
529
|
+
`SELECT m.origin, m.role, m.is_active FROM workspace_members m
|
|
530
|
+
JOIN "user" u ON u.id = m.user_id
|
|
531
|
+
WHERE m.workspace_id = $1 AND u.email = $2`,
|
|
532
|
+
[wsBg, OWNER.email]
|
|
533
|
+
);
|
|
534
|
+
expect(rows).toEqual([{ origin: 'local', role: 'owner', is_active: true }]);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('the claim survives an EMPTY-hub-list reconcile (the maximal sweep)', async () => {
|
|
538
|
+
// Give the operator a hub identity + one hub org of their own: the D9
|
|
539
|
+
// trusted link rides the verified operator email.
|
|
540
|
+
const operatorSsoCookie = await sso.ssoLogin(app, hub, {
|
|
541
|
+
sub: 'hub-operator-bg',
|
|
542
|
+
email: OWNER.email,
|
|
543
|
+
workspaceId: ORG_OPX,
|
|
544
|
+
role: 'owner',
|
|
545
|
+
workspaceName: 'Operator Own Org'
|
|
546
|
+
});
|
|
547
|
+
const wsOpx = await workspaceIdOf(ORG_OPX);
|
|
548
|
+
expect((await me(operatorSsoCookie, wsOpx)).status).toBe(200);
|
|
549
|
+
|
|
550
|
+
// The hub now asserts NOTHING for the operator: the next reconcile pass
|
|
551
|
+
// runs with an EMPTY org list — the maximal deactivation sweep.
|
|
552
|
+
hub.clearUserOrgs('hub-operator-bg');
|
|
553
|
+
await expireTtl();
|
|
554
|
+
const swept = await me(operatorSsoCookie, wsOpx);
|
|
555
|
+
expect(swept.status).toBe(401); // the hub-origin row went with the sweep
|
|
556
|
+
|
|
557
|
+
// …but the break-glass claim row is origin='local': untouched. The
|
|
558
|
+
// operator keeps full dashboard access to the projected workspace.
|
|
559
|
+
const kept = await me(operatorSsoCookie, wsBg);
|
|
560
|
+
expect(kept.status).toBe(200);
|
|
561
|
+
const body = await readJson(kept);
|
|
562
|
+
expect(body.activeWorkspaceId).toBe(wsBg);
|
|
563
|
+
expect(body.origin).toBe('local');
|
|
564
|
+
expect(body.role).toBe('owner');
|
|
565
|
+
const { rows } = await app.db.pool.query(
|
|
566
|
+
`SELECT m.is_active FROM workspace_members m
|
|
567
|
+
JOIN "user" u ON u.id = m.user_id
|
|
568
|
+
WHERE m.workspace_id = $1 AND u.email = $2`,
|
|
569
|
+
[wsBg, OWNER.email]
|
|
570
|
+
);
|
|
571
|
+
expect(rows).toEqual([{ is_active: true }]);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe('a severed hub link fails CLOSED for a hub-origin principal (CLOUD-1, PRDCT-1356)', () => {
|
|
576
|
+
const ORG_SEV = '33333333-aaaa-4bbb-8ccc-00000000000d';
|
|
577
|
+
const sev: HubUserFixture = {
|
|
578
|
+
sub: 'hub-sev',
|
|
579
|
+
email: 'sev@reconcile.test',
|
|
580
|
+
name: 'Sev Ered',
|
|
581
|
+
workspaceId: ORG_SEV,
|
|
582
|
+
role: 'member',
|
|
583
|
+
workspaceName: 'Sev Org'
|
|
584
|
+
};
|
|
585
|
+
const SEV_ENV = {
|
|
586
|
+
EDITION: 'cloud',
|
|
587
|
+
HUB_CLIENT_ID: host.hubClientId,
|
|
588
|
+
HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001'
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
it('no account row + hub-origin memberships → 401 hub_grant_expired, never the fail-open no_link', async () => {
|
|
592
|
+
// Own database + own boots: the grant store caches the hub access token
|
|
593
|
+
// in PROCESS memory, so the severed row is only observed by a fresh
|
|
594
|
+
// process — which is the honest shape anyway (a replica restart, the
|
|
595
|
+
// next deploy).
|
|
596
|
+
const url = await createDatabase(container, 'hub_severed');
|
|
597
|
+
const first = await createTestApp(url, { ...SEV_ENV, HUB_ISSUER_URL: hub.issuer }, { hubDials: DIALS });
|
|
598
|
+
let cookie: string;
|
|
599
|
+
let userId: string;
|
|
600
|
+
try {
|
|
601
|
+
const setup = await first.app.request(
|
|
602
|
+
'/api/v1/setup',
|
|
603
|
+
sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Severed', owner: OWNER })
|
|
604
|
+
);
|
|
605
|
+
expect(setup.status).toBe(201);
|
|
606
|
+
cookie = await sso.ssoLogin(first, hub, sev);
|
|
607
|
+
const ws = await first.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
|
|
608
|
+
ORG_SEV
|
|
609
|
+
]);
|
|
610
|
+
expect(ws.rows).toHaveLength(1);
|
|
611
|
+
const ok = await first.app.request('/api/v1/me', {
|
|
612
|
+
headers: { cookie, 'x-workspace-id': ws.rows[0].id }
|
|
613
|
+
});
|
|
614
|
+
expect(ok.status).toBe(200);
|
|
615
|
+
const u = await first.db.pool.query(`SELECT id FROM "user" WHERE email = $1`, [sev.email]);
|
|
616
|
+
userId = u.rows[0].id as string;
|
|
617
|
+
// Sever the link the way an unlink (now refused) or a partial cleanup
|
|
618
|
+
// would: the account row goes, the hub-origin membership stays.
|
|
619
|
+
await first.db.pool.query(`DELETE FROM account WHERE user_id = $1 AND provider_id = 'antasphere'`, [
|
|
620
|
+
userId
|
|
621
|
+
]);
|
|
622
|
+
} finally {
|
|
623
|
+
await first.stop();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const second = await createTestApp(url, { ...SEV_ENV, HUB_ISSUER_URL: hub.issuer }, { hubDials: DIALS });
|
|
627
|
+
try {
|
|
628
|
+
const ws = await second.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
|
|
629
|
+
ORG_SEV
|
|
630
|
+
]);
|
|
631
|
+
const res = await second.app.request('/api/v1/me', {
|
|
632
|
+
headers: { cookie, 'x-workspace-id': ws.rows[0].id }
|
|
633
|
+
});
|
|
634
|
+
expect(res.status).toBe(401);
|
|
635
|
+
expect((await readJson(res)).error.code).toBe('hub_grant_expired');
|
|
636
|
+
// The membership row itself was NOT swept (no org truth was read).
|
|
637
|
+
const rows = await second.db.pool.query(
|
|
638
|
+
`SELECT origin, is_active FROM workspace_members WHERE user_id = $1`,
|
|
639
|
+
[userId]
|
|
640
|
+
);
|
|
641
|
+
expect(rows.rows).toEqual([{ origin: 'hub', is_active: true }]);
|
|
642
|
+
} finally {
|
|
643
|
+
await second.stop();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
});
|