@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,771 @@
|
|
|
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 { eq } from 'drizzle-orm';
|
|
13
|
+
import { projects, projectTeams, workspaceTeams } from '@antasphere/chassis-db';
|
|
14
|
+
import { FakeHub, type HubUserFixture } from '@antasphere/chassis-server/testing';
|
|
15
|
+
import { projectOrgTeams } from '../../src/identity/hub-projection.js';
|
|
16
|
+
import * as sso from './sso-helpers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The hub's teams and refusals, as the chassis reads them (the hub's
|
|
20
|
+
* `GET /orgs` as the person): each org entry carries the caller's OWN teams,
|
|
21
|
+
* which the reconcile projects into `workspace_teams` + the person's seats in
|
|
22
|
+
* `workspace_team_members`; the body's top-level `denied` names the
|
|
23
|
+
* organizations that do not open the tool to them, which `/me` hands the
|
|
24
|
+
* refusal page as `hubDenied` beside the hub's `/no-access` URL.
|
|
25
|
+
*
|
|
26
|
+
* Pinned here: seats follow the hub on every pass (added, removed, a team
|
|
27
|
+
* renamed in place); a swept org takes the person's seats with the
|
|
28
|
+
* membership (and the project grants, as before); the denied hint follows
|
|
29
|
+
* the last definitive pass and survives an inconclusive one; a person
|
|
30
|
+
* whose only org was swept is re-admitted by a bare zero-state /me once the
|
|
31
|
+
* hub lists it again (that read runs the cached pass itself); the
|
|
32
|
+
* self-hosted edition carries none of it. And (PRDCT-2813) the reconcile
|
|
33
|
+
* also reads the organization's WHOLE team list (`GET /teams`, as the person,
|
|
34
|
+
* the org in `X-Workspace-Id`) at login and then at most once per
|
|
35
|
+
* `orgTeamsTtlMs` per org: every team of the org lands, a team deleted at
|
|
36
|
+
* the hub goes with its seats and its project entries, a local team is
|
|
37
|
+
* never touched, and a failed read keeps the list and waits `retryMs`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const OWNER = { email: 'owner@hub-teams.test', name: 'Op Owner', password: 'op-owner-password-teams-1' };
|
|
41
|
+
|
|
42
|
+
const ORG_A = '27270000-aaaa-4bbb-8ccc-000000000001';
|
|
43
|
+
const ORG_B = '27270000-aaaa-4bbb-8ccc-000000000002';
|
|
44
|
+
const ORG_DENY = '27270000-aaaa-4bbb-8ccc-000000000003';
|
|
45
|
+
const ORG_C = '27270000-aaaa-4bbb-8ccc-000000000004';
|
|
46
|
+
const TEAM_1 = '27270000-dddd-4eee-8fff-000000000001';
|
|
47
|
+
const TEAM_2 = '27270000-dddd-4eee-8fff-000000000002';
|
|
48
|
+
const TEAM_3 = '27270000-dddd-4eee-8fff-000000000003';
|
|
49
|
+
const ORG_T = '27270000-aaaa-4bbb-8ccc-000000000005';
|
|
50
|
+
const TEAM_A = '27270000-dddd-4eee-8fff-00000000000a';
|
|
51
|
+
const TEAM_B = '27270000-dddd-4eee-8fff-00000000000b';
|
|
52
|
+
const TEAM_C = '27270000-dddd-4eee-8fff-00000000000c';
|
|
53
|
+
const ORG_P = '27270000-aaaa-4bbb-8ccc-000000000006';
|
|
54
|
+
|
|
55
|
+
const DIALS = {
|
|
56
|
+
reconcileTtlMs: 120,
|
|
57
|
+
reconcileStaleMaxMs: 60_000,
|
|
58
|
+
retryMs: 250,
|
|
59
|
+
orgsTimeoutMs: 400,
|
|
60
|
+
tokenTimeoutMs: 2_000,
|
|
61
|
+
orgTeamsTtlMs: 1_000
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The reconciler's clock (`hubNow`): its TTL, its retry throttle and the
|
|
65
|
+
* team-list window read this, so the tests move it forward instead of
|
|
66
|
+
* sleeping against 120 ms and 250 ms windows a loaded machine overshoots.
|
|
67
|
+
* It only moves when a test says so: a pass right after another sits inside
|
|
68
|
+
* every window whatever the machine's load.
|
|
69
|
+
*/
|
|
70
|
+
let clock = Date.now();
|
|
71
|
+
const now = () => clock;
|
|
72
|
+
const advance = (ms: number) => {
|
|
73
|
+
clock += ms;
|
|
74
|
+
};
|
|
75
|
+
/** Real time, for what the reconciler's clock does not govern (a request in flight, the database's own `now()`). */
|
|
76
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
77
|
+
/** Let the per-user reconcile cache (and the failure throttle) expire so the next request runs a fresh pass. */
|
|
78
|
+
const expireTtl = () => advance(Math.max(DIALS.reconcileTtlMs, DIALS.retryMs) + 40);
|
|
79
|
+
/** Let an org's team list go stale so the next pass reads it again. */
|
|
80
|
+
const expireOrgTeams = () => advance(DIALS.orgTeamsTtlMs + 60);
|
|
81
|
+
|
|
82
|
+
let container: StartedPostgreSqlContainer;
|
|
83
|
+
let hub: FakeHub;
|
|
84
|
+
let app: TestApp;
|
|
85
|
+
|
|
86
|
+
beforeAll(async () => {
|
|
87
|
+
[container, hub] = await Promise.all([startPostgres(), FakeHub.start({ clientId: host.hubClientId })]);
|
|
88
|
+
app = await createTestApp(
|
|
89
|
+
await createDatabase(container, 'hub_teams'),
|
|
90
|
+
{
|
|
91
|
+
EDITION: 'cloud',
|
|
92
|
+
HUB_ISSUER_URL: hub.issuer,
|
|
93
|
+
HUB_CLIENT_ID: host.hubClientId,
|
|
94
|
+
HUB_CLIENT_SECRET: 'integration-test-hub-secret-teams',
|
|
95
|
+
METRICS_TOKEN: 'hub-teams-metrics-token'
|
|
96
|
+
},
|
|
97
|
+
{ hubDials: DIALS, hubNow: now }
|
|
98
|
+
);
|
|
99
|
+
const res = await app.app.request(
|
|
100
|
+
'/api/v1/setup',
|
|
101
|
+
sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Teams', owner: OWNER })
|
|
102
|
+
);
|
|
103
|
+
expect(res.status).toBe(201);
|
|
104
|
+
}, 240_000);
|
|
105
|
+
|
|
106
|
+
afterAll(async () => {
|
|
107
|
+
await app?.stop();
|
|
108
|
+
await Promise.all([container?.stop(), hub?.stop()]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const me = (cookie: string) => app.app.request('/api/v1/me', { headers: { cookie } });
|
|
112
|
+
|
|
113
|
+
async function workspaceIdOf(centralAccountId: string): Promise<string> {
|
|
114
|
+
const { rows } = await app.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
|
|
115
|
+
centralAccountId
|
|
116
|
+
]);
|
|
117
|
+
expect(rows).toHaveLength(1);
|
|
118
|
+
return rows[0].id as string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function teamsOf(centralAccountId: string) {
|
|
122
|
+
const { rows } = await app.db.pool.query(
|
|
123
|
+
`SELECT t.id, t.hub_team_id, t.slug, t.name FROM workspace_teams t
|
|
124
|
+
JOIN workspaces w ON w.id = t.workspace_id
|
|
125
|
+
WHERE w.central_account_id = $1
|
|
126
|
+
ORDER BY t.hub_team_id`,
|
|
127
|
+
[centralAccountId]
|
|
128
|
+
);
|
|
129
|
+
return rows as Array<{ id: string; hub_team_id: string; slug: string; name: string }>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function seatsOf(email: string, centralAccountId: string): Promise<string[]> {
|
|
133
|
+
const { rows } = await app.db.pool.query(
|
|
134
|
+
`SELECT t.hub_team_id FROM workspace_team_members s
|
|
135
|
+
JOIN workspace_teams t ON t.id = s.team_id
|
|
136
|
+
JOIN workspace_members m ON m.id = s.member_id
|
|
137
|
+
JOIN workspaces w ON w.id = m.workspace_id
|
|
138
|
+
JOIN "user" u ON u.id = m.user_id
|
|
139
|
+
WHERE u.email = $1 AND w.central_account_id = $2
|
|
140
|
+
ORDER BY t.hub_team_id`,
|
|
141
|
+
[email, centralAccountId]
|
|
142
|
+
);
|
|
143
|
+
return rows.map((r) => r.hub_team_id as string);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe('the teams projection', () => {
|
|
147
|
+
const tess: HubUserFixture = {
|
|
148
|
+
sub: 'hub-tess',
|
|
149
|
+
email: 'tess@hub-teams.test',
|
|
150
|
+
name: 'Tess Teams',
|
|
151
|
+
workspaceId: ORG_A,
|
|
152
|
+
role: 'member',
|
|
153
|
+
workspaceName: 'Org A'
|
|
154
|
+
};
|
|
155
|
+
let cookie = '';
|
|
156
|
+
|
|
157
|
+
it('a login projects the person’s two teams in an org and seats them in both', async () => {
|
|
158
|
+
hub.setOrgTeams(ORG_A, [
|
|
159
|
+
{ id: TEAM_1, slug: 'design', name: 'Design' },
|
|
160
|
+
{ id: TEAM_2, slug: 'sales', name: 'Sales' }
|
|
161
|
+
]);
|
|
162
|
+
hub.setUserOrg(tess.sub, ORG_A, {
|
|
163
|
+
name: 'Org A',
|
|
164
|
+
role: 'member',
|
|
165
|
+
teams: [
|
|
166
|
+
{ id: TEAM_1, slug: 'design', name: 'Design' },
|
|
167
|
+
{ id: TEAM_2, slug: 'sales', name: 'Sales' }
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
// A second org of hers, so her session keeps resolving while ORG_A is swept below.
|
|
171
|
+
hub.setUserOrg(tess.sub, ORG_B, { name: 'Org B', role: 'member' });
|
|
172
|
+
cookie = await sso.ssoLogin(app, hub, tess);
|
|
173
|
+
|
|
174
|
+
const teams = await teamsOf(ORG_A);
|
|
175
|
+
expect(teams.map((t) => [t.hub_team_id, t.slug, t.name])).toEqual([
|
|
176
|
+
[TEAM_1, 'design', 'Design'],
|
|
177
|
+
[TEAM_2, 'sales', 'Sales']
|
|
178
|
+
]);
|
|
179
|
+
expect(await seatsOf(tess.email, ORG_A)).toEqual([TEAM_1, TEAM_2]);
|
|
180
|
+
// An org whose entry carries no teams projects none.
|
|
181
|
+
expect(await teamsOf(ORG_B)).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('a later pass removes the seat the hub dropped, keeps the other, and renames a team in place', async () => {
|
|
185
|
+
const before = await teamsOf(ORG_A);
|
|
186
|
+
// The org still holds both teams; she left Sales, and Design was renamed.
|
|
187
|
+
hub.setOrgTeams(ORG_A, [
|
|
188
|
+
{ id: TEAM_1, slug: 'design-studio', name: 'Design Studio' },
|
|
189
|
+
{ id: TEAM_2, slug: 'sales', name: 'Sales' }
|
|
190
|
+
]);
|
|
191
|
+
hub.setUserOrg(tess.sub, ORG_A, {
|
|
192
|
+
name: 'Org A',
|
|
193
|
+
role: 'member',
|
|
194
|
+
teams: [{ id: TEAM_1, slug: 'design-studio', name: 'Design Studio' }]
|
|
195
|
+
});
|
|
196
|
+
expireTtl();
|
|
197
|
+
expect((await me(cookie)).status).toBe(200);
|
|
198
|
+
|
|
199
|
+
expect(await seatsOf(tess.email, ORG_A)).toEqual([TEAM_1]);
|
|
200
|
+
const after = await teamsOf(ORG_A);
|
|
201
|
+
const renamed = after.find((t) => t.hub_team_id === TEAM_1)!;
|
|
202
|
+
expect(renamed).toMatchObject({ slug: 'design-studio', name: 'Design Studio' });
|
|
203
|
+
// Same row: the team is renamed where it stands, never re-created.
|
|
204
|
+
expect(renamed.id).toBe(before.find((t) => t.hub_team_id === TEAM_1)!.id);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('an org swept from the list deactivates the membership and deletes the person’s seats and project grants', async () => {
|
|
208
|
+
const workspaceId = await workspaceIdOf(ORG_A);
|
|
209
|
+
// A project grant in ORG_A, the way it is made locally (an owner of the org adds her).
|
|
210
|
+
const owner: HubUserFixture = {
|
|
211
|
+
sub: 'hub-teams-owner',
|
|
212
|
+
email: 'owner-a@hub-teams.test',
|
|
213
|
+
name: 'Owner A',
|
|
214
|
+
workspaceId: ORG_A,
|
|
215
|
+
role: 'owner',
|
|
216
|
+
workspaceName: 'Org A'
|
|
217
|
+
};
|
|
218
|
+
const ownerCookie = await sso.ssoLogin(app, hub, owner);
|
|
219
|
+
const headers = {
|
|
220
|
+
cookie: ownerCookie,
|
|
221
|
+
'x-workspace-id': workspaceId,
|
|
222
|
+
'content-type': 'application/json',
|
|
223
|
+
'x-forwarded-for': sso.nextIp()
|
|
224
|
+
};
|
|
225
|
+
const project = await app.app.request('/api/v1/projects', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers,
|
|
228
|
+
body: JSON.stringify({ name: 'Teams project' })
|
|
229
|
+
});
|
|
230
|
+
expect(project.status).toBe(201);
|
|
231
|
+
const projectId = (await readJson(project)).id as string;
|
|
232
|
+
const grant = await app.app.request(`/api/v1/projects/${projectId}/members`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers,
|
|
235
|
+
body: JSON.stringify({ email: tess.email, role: 'viewer' })
|
|
236
|
+
});
|
|
237
|
+
expect(grant.status).toBe(201);
|
|
238
|
+
expect(await seatsOf(tess.email, ORG_A)).toEqual([TEAM_1]);
|
|
239
|
+
|
|
240
|
+
const { rows: memberRows } = await app.db.pool.query(
|
|
241
|
+
`SELECT m.id FROM workspace_members m JOIN "user" u ON u.id = m.user_id
|
|
242
|
+
WHERE u.email = $1 AND m.workspace_id = $2`,
|
|
243
|
+
[tess.email, workspaceId]
|
|
244
|
+
);
|
|
245
|
+
const memberId = memberRows[0].id as string;
|
|
246
|
+
|
|
247
|
+
hub.removeUserOrg(tess.sub, ORG_A);
|
|
248
|
+
expireTtl();
|
|
249
|
+
await me(cookie);
|
|
250
|
+
|
|
251
|
+
const { rows: after } = await app.db.pool.query(`SELECT is_active FROM workspace_members WHERE id = $1`, [
|
|
252
|
+
memberId
|
|
253
|
+
]);
|
|
254
|
+
expect(after).toEqual([{ is_active: false }]);
|
|
255
|
+
const { rows: seats } = await app.db.pool.query(
|
|
256
|
+
`SELECT id FROM workspace_team_members WHERE member_id = $1`,
|
|
257
|
+
[memberId]
|
|
258
|
+
);
|
|
259
|
+
expect(seats).toEqual([]);
|
|
260
|
+
const { rows: grants } = await app.db.pool.query(`SELECT id FROM project_members WHERE member_id = $1`, [
|
|
261
|
+
memberId
|
|
262
|
+
]);
|
|
263
|
+
expect(grants).toEqual([]);
|
|
264
|
+
// The team itself stays: the projection is the hub's teams, the seat was hers.
|
|
265
|
+
expect((await teamsOf(ORG_A)).map((t) => t.hub_team_id)).toEqual([TEAM_1, TEAM_2]);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('the denied hint on /me', () => {
|
|
270
|
+
const dora: HubUserFixture = {
|
|
271
|
+
sub: 'hub-dora',
|
|
272
|
+
email: 'dora@hub-teams.test',
|
|
273
|
+
name: 'Dora Denied',
|
|
274
|
+
workspaceId: ORG_B,
|
|
275
|
+
role: 'member',
|
|
276
|
+
workspaceName: 'Org B'
|
|
277
|
+
};
|
|
278
|
+
const noAccessUrl = () => `${hub.issuer.replace(/\/+$/, '')}/no-access?client_id=${host.hubClientId}`;
|
|
279
|
+
let cookie = '';
|
|
280
|
+
|
|
281
|
+
it('a denied org in the hub’s answer reaches /me as hubDenied, with the ready-made no-access URL', async () => {
|
|
282
|
+
hub.setUserDenied(dora.sub, [{ id: ORG_DENY, name: 'Restricted Org' }]);
|
|
283
|
+
cookie = await sso.ssoLogin(app, hub, dora);
|
|
284
|
+
const body = await readJson(await me(cookie));
|
|
285
|
+
expect(body.workspace).not.toBeNull();
|
|
286
|
+
expect(body.hubDenied).toEqual([{ id: ORG_DENY, name: 'Restricted Org' }]);
|
|
287
|
+
expect(body.hubNoAccessUrl).toBe(noAccessUrl());
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('an inconclusive pass keeps the last list', async () => {
|
|
291
|
+
hub.orgsMode = 'http500';
|
|
292
|
+
try {
|
|
293
|
+
const passesBefore = hub.orgsRequests.length;
|
|
294
|
+
expireTtl();
|
|
295
|
+
const body = await readJson(await me(cookie));
|
|
296
|
+
expect(hub.orgsRequests.length).toBeGreaterThan(passesBefore);
|
|
297
|
+
expect(body.hubDenied).toEqual([{ id: ORG_DENY, name: 'Restricted Org' }]);
|
|
298
|
+
} finally {
|
|
299
|
+
hub.orgsMode = 'ok';
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('a later definitive pass with denied: [] empties it', async () => {
|
|
304
|
+
hub.setUserDenied(dora.sub, []);
|
|
305
|
+
expireTtl();
|
|
306
|
+
await me(cookie); // the pass runs in the gate, before the handler reads the hint
|
|
307
|
+
const body = await readJson(await me(cookie));
|
|
308
|
+
expect(body.hubDenied).toEqual([]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('the zero-membership state names the refusing org too', async () => {
|
|
312
|
+
// Her only org now restricts the tool to teams she is not in: the hub
|
|
313
|
+
// leaves it out of `orgs` and names it in `denied`.
|
|
314
|
+
hub.removeUserOrg(dora.sub, ORG_B);
|
|
315
|
+
hub.setUserDenied(dora.sub, [{ id: ORG_B, name: 'Org B' }]);
|
|
316
|
+
expireTtl();
|
|
317
|
+
const revoked = await me(cookie);
|
|
318
|
+
expect(revoked.status).toBe(401);
|
|
319
|
+
expect((await readJson(revoked)).error.code).toBe('membership_revoked');
|
|
320
|
+
|
|
321
|
+
const zero = await me(cookie);
|
|
322
|
+
expect(zero.status).toBe(200);
|
|
323
|
+
const body = await readJson(zero);
|
|
324
|
+
expect(body.workspace).toBeNull();
|
|
325
|
+
expect(body.workspaces).toEqual([]);
|
|
326
|
+
expect(body.hubDenied).toEqual([{ id: ORG_B, name: 'Org B' }]);
|
|
327
|
+
expect(body.hubNoAccessUrl).toBe(noAccessUrl());
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('re-admission of a single-organization person', () => {
|
|
332
|
+
// Her ONLY org: once it is swept, her session resolves to no workspace and
|
|
333
|
+
// the live gate never runs for it — the zero-state /me runs the pass.
|
|
334
|
+
const rita: HubUserFixture = {
|
|
335
|
+
sub: 'hub-rita',
|
|
336
|
+
email: 'rita@hub-teams.test',
|
|
337
|
+
name: 'Rita Readmitted',
|
|
338
|
+
workspaceId: ORG_C,
|
|
339
|
+
role: 'member',
|
|
340
|
+
workspaceName: 'Org C'
|
|
341
|
+
};
|
|
342
|
+
let cookie = '';
|
|
343
|
+
|
|
344
|
+
it('a bare /me re-admits her once the hub lists her only organization again', async () => {
|
|
345
|
+
hub.setUserOrg(rita.sub, ORG_C, {
|
|
346
|
+
name: 'Org C',
|
|
347
|
+
role: 'member',
|
|
348
|
+
teams: [{ id: TEAM_3, slug: 'ops', name: 'Ops' }]
|
|
349
|
+
});
|
|
350
|
+
cookie = await sso.ssoLogin(app, hub, rita);
|
|
351
|
+
const workspaceId = await workspaceIdOf(ORG_C);
|
|
352
|
+
expect(await seatsOf(rita.email, ORG_C)).toEqual([TEAM_3]);
|
|
353
|
+
|
|
354
|
+
// The owner takes her off the team the tool is open to: the hub leaves
|
|
355
|
+
// the org out of her list and names it as denied.
|
|
356
|
+
hub.removeUserOrg(rita.sub, ORG_C);
|
|
357
|
+
hub.setUserDenied(rita.sub, [{ id: ORG_C, name: 'Org C' }]);
|
|
358
|
+
expireTtl();
|
|
359
|
+
const revoked = await me(cookie);
|
|
360
|
+
expect(revoked.status).toBe(401);
|
|
361
|
+
expect((await readJson(revoked)).error.code).toBe('membership_revoked');
|
|
362
|
+
const zero = await readJson(await me(cookie));
|
|
363
|
+
expect(zero.workspace).toBeNull();
|
|
364
|
+
expect(zero.hubDenied).toEqual([{ id: ORG_C, name: 'Org C' }]);
|
|
365
|
+
expect(await seatsOf(rita.email, ORG_C)).toEqual([]);
|
|
366
|
+
|
|
367
|
+
// The polling page within the TTL: the zero-state read rides the cache,
|
|
368
|
+
// never a hub call per request. One read first, so the window starts
|
|
369
|
+
// here (the reads above and the seats query may have outlived the test
|
|
370
|
+
// dial's 120 ms TTL), then two reads inside it.
|
|
371
|
+
await me(cookie);
|
|
372
|
+
const before = hub.orgsRequests.length;
|
|
373
|
+
await me(cookie);
|
|
374
|
+
await me(cookie);
|
|
375
|
+
expect(hub.orgsRequests.length).toBe(before);
|
|
376
|
+
|
|
377
|
+
// Re-seated at the hub: a BARE /me (no X-Workspace-Id) past the TTL
|
|
378
|
+
// answers the normal shape with her workspace.
|
|
379
|
+
hub.setUserOrg(rita.sub, ORG_C, {
|
|
380
|
+
name: 'Org C',
|
|
381
|
+
role: 'member',
|
|
382
|
+
teams: [{ id: TEAM_3, slug: 'ops', name: 'Ops' }]
|
|
383
|
+
});
|
|
384
|
+
hub.setUserDenied(rita.sub, []);
|
|
385
|
+
expireTtl();
|
|
386
|
+
const back = await me(cookie);
|
|
387
|
+
expect(back.status).toBe(200);
|
|
388
|
+
const body = await readJson(back);
|
|
389
|
+
expect(hub.orgsRequests.length).toBe(before + 1);
|
|
390
|
+
expect(body.workspace).toMatchObject({ id: workspaceId, name: 'Org C', hubOrigin: true });
|
|
391
|
+
expect(body.activeWorkspaceId).toBe(workspaceId);
|
|
392
|
+
expect(body.role).toBe('member');
|
|
393
|
+
expect(body.workspaces.map((w: { id: string }) => w.id)).toEqual([workspaceId]);
|
|
394
|
+
expect(body.hubDenied).toEqual([]);
|
|
395
|
+
const { rows } = await app.db.pool.query(
|
|
396
|
+
`SELECT m.is_active FROM workspace_members m JOIN "user" u ON u.id = m.user_id
|
|
397
|
+
WHERE u.email = $1 AND m.workspace_id = $2`,
|
|
398
|
+
[rita.email, workspaceId]
|
|
399
|
+
);
|
|
400
|
+
expect(rows).toEqual([{ is_active: true }]);
|
|
401
|
+
expect(await seatsOf(rita.email, ORG_C)).toEqual([TEAM_3]);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('self-hosted edition: no teams, no hint', () => {
|
|
406
|
+
// One oss app for both cases: the first reads /me with the owner's
|
|
407
|
+
// membership live, the second after it went inactive.
|
|
408
|
+
let oss: TestApp;
|
|
409
|
+
let cookie: string;
|
|
410
|
+
|
|
411
|
+
beforeAll(async () => {
|
|
412
|
+
oss = await createTestApp(await createDatabase(container, 'hub_teams_oss'));
|
|
413
|
+
const setup = await oss.app.request(
|
|
414
|
+
'/api/v1/setup',
|
|
415
|
+
sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'TeamsOss', owner: OWNER })
|
|
416
|
+
);
|
|
417
|
+
expect(setup.status).toBe(201);
|
|
418
|
+
const signIn = await oss.app.request(
|
|
419
|
+
'/api/v1/auth/sign-in/email',
|
|
420
|
+
sso.json({ email: OWNER.email, password: OWNER.password })
|
|
421
|
+
);
|
|
422
|
+
expect(signIn.status).toBe(200);
|
|
423
|
+
cookie = extractCookie(signIn);
|
|
424
|
+
}, 240_000);
|
|
425
|
+
|
|
426
|
+
afterAll(async () => {
|
|
427
|
+
await oss?.stop();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('/me carries no hubDenied and the two tables stay empty across a login', async () => {
|
|
431
|
+
const res = await oss.app.request('/api/v1/me', { headers: { cookie } });
|
|
432
|
+
expect(res.status).toBe(200);
|
|
433
|
+
const body = await readJson(res);
|
|
434
|
+
expect('hubDenied' in body).toBe(false);
|
|
435
|
+
expect('hubNoAccessUrl' in body).toBe(false);
|
|
436
|
+
const { rows } = await oss.db.pool.query(
|
|
437
|
+
`SELECT (SELECT count(*) FROM workspace_teams)::int AS teams,
|
|
438
|
+
(SELECT count(*) FROM workspace_team_members)::int AS seats`
|
|
439
|
+
);
|
|
440
|
+
expect(rows).toEqual([{ teams: 0, seats: 0 }]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('the zero-membership /me runs no hub pass (no reconciler)', async () => {
|
|
444
|
+
// A second owner keeps the workspace owned (the last-owner guard), so
|
|
445
|
+
// the signed-in owner's own membership can go inactive.
|
|
446
|
+
const { rows: ws } = await oss.db.pool.query(`SELECT id FROM workspaces LIMIT 1`);
|
|
447
|
+
const siblingId = 'hub-teams-oss-sibling-owner';
|
|
448
|
+
await oss.db.pool.query(
|
|
449
|
+
`INSERT INTO "user" (id, name, email, email_verified) VALUES ($1, 'Sibling Owner', 'sibling@teams-oss.test', true)`,
|
|
450
|
+
[siblingId]
|
|
451
|
+
);
|
|
452
|
+
await oss.db.pool.query(
|
|
453
|
+
`INSERT INTO workspace_members (workspace_id, user_id, role, is_active) VALUES ($1, $2, 'owner', true)`,
|
|
454
|
+
[ws[0].id, siblingId]
|
|
455
|
+
);
|
|
456
|
+
await oss.db.pool.query(`UPDATE workspace_members SET is_active = false WHERE user_id <> $1`, [
|
|
457
|
+
siblingId
|
|
458
|
+
]);
|
|
459
|
+
const before = hub.orgsRequests.length;
|
|
460
|
+
const res = await oss.app.request('/api/v1/me', { headers: { cookie } });
|
|
461
|
+
expect(res.status).toBe(200);
|
|
462
|
+
const body = await readJson(res);
|
|
463
|
+
expect(body.workspace).toBeNull();
|
|
464
|
+
expect(body.workspaces).toEqual([]);
|
|
465
|
+
expect('hubDenied' in body).toBe(false);
|
|
466
|
+
expect(hub.orgsRequests.length).toBe(before);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('the organization’s whole team list (PRDCT-2813)', () => {
|
|
471
|
+
const tom: HubUserFixture = {
|
|
472
|
+
sub: 'hub-tom',
|
|
473
|
+
email: 'tom@hub-teams.test',
|
|
474
|
+
name: 'Tom Teamlist',
|
|
475
|
+
workspaceId: ORG_T,
|
|
476
|
+
role: 'member',
|
|
477
|
+
workspaceName: 'Org T'
|
|
478
|
+
};
|
|
479
|
+
const A = { id: TEAM_A, slug: 'alpha', name: 'Alpha' };
|
|
480
|
+
const B = { id: TEAM_B, slug: 'beta', name: 'Beta' };
|
|
481
|
+
const C = { id: TEAM_C, slug: 'gamma', name: 'Gamma' };
|
|
482
|
+
let cookie = '';
|
|
483
|
+
const hubIdsOf = async () => (await teamsOf(ORG_T)).map((t) => t.hub_team_id);
|
|
484
|
+
|
|
485
|
+
it('at login every team of the org lands, while the person is seated only in her own', async () => {
|
|
486
|
+
hub.setOrgTeams(ORG_T, [A, B, C]);
|
|
487
|
+
hub.setUserOrg(tom.sub, ORG_T, { name: 'Org T', role: 'member', teams: [A] });
|
|
488
|
+
const teamsBefore = hub.teamsRequests.length;
|
|
489
|
+
cookie = await sso.ssoLogin(app, hub, tom);
|
|
490
|
+
|
|
491
|
+
expect((await teamsOf(ORG_T)).map((t) => [t.hub_team_id, t.slug, t.name])).toEqual([
|
|
492
|
+
[TEAM_A, 'alpha', 'Alpha'],
|
|
493
|
+
[TEAM_B, 'beta', 'Beta'],
|
|
494
|
+
[TEAM_C, 'gamma', 'Gamma']
|
|
495
|
+
]);
|
|
496
|
+
expect(await seatsOf(tom.email, ORG_T)).toEqual([TEAM_A]);
|
|
497
|
+
|
|
498
|
+
// Read as the person: her own grant token, the hub org in X-Workspace-Id.
|
|
499
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
500
|
+
const read = hub.teamsRequests.at(-1)!;
|
|
501
|
+
expect(read.orgId).toBe(ORG_T);
|
|
502
|
+
expect(read.auth).toMatch(/^Bearer .+/);
|
|
503
|
+
expect(read.auth).toBe(hub.orgsRequests.at(-1)!.auth);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('a pass inside orgTeamsTtlMs does not read the list again; one past it does', async () => {
|
|
507
|
+
const orgsBefore = hub.orgsRequests.length;
|
|
508
|
+
const teamsBefore = hub.teamsRequests.length;
|
|
509
|
+
advance(DIALS.reconcileTtlMs + 40);
|
|
510
|
+
expect((await me(cookie)).status).toBe(200);
|
|
511
|
+
expect(hub.orgsRequests.length).toBeGreaterThan(orgsBefore);
|
|
512
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore);
|
|
513
|
+
|
|
514
|
+
expireOrgTeams();
|
|
515
|
+
expect((await me(cookie)).status).toBe(200);
|
|
516
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('a login reads the list whatever the window says: a team made at the hub a moment ago lands at once', async () => {
|
|
520
|
+
// The list was just read (the test above): inside orgTeamsTtlMs a pass
|
|
521
|
+
// leaves it, a LOGIN does not (Romain's rule: at login, then every five
|
|
522
|
+
// minutes). The team D made at the hub between the two is here right after.
|
|
523
|
+
const D = { id: '26260000-dddd-4ddd-8ddd-00000000000d', slug: 'delta', name: 'Delta' };
|
|
524
|
+
hub.setOrgTeams(ORG_T, [A, B, C, D]);
|
|
525
|
+
const teamsBefore = hub.teamsRequests.length;
|
|
526
|
+
cookie = await sso.ssoLogin(app, hub, tom);
|
|
527
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
528
|
+
expect(await hubIdsOf()).toContain(D.id);
|
|
529
|
+
hub.setOrgTeams(ORG_T, [A, B, C]);
|
|
530
|
+
expireOrgTeams();
|
|
531
|
+
expect((await me(cookie)).status).toBe(200);
|
|
532
|
+
expect(await hubIdsOf()).not.toContain(D.id);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('a team deleted at the hub goes with its project entries; a local team stays', async () => {
|
|
536
|
+
const workspaceId = await workspaceIdOf(ORG_T);
|
|
537
|
+
const teamC = (await teamsOf(ORG_T)).find((t) => t.hub_team_id === TEAM_C)!;
|
|
538
|
+
const [project] = await app.db.db
|
|
539
|
+
.insert(projects)
|
|
540
|
+
.values({ workspaceId, name: 'Team list project' })
|
|
541
|
+
.returning({ id: projects.id });
|
|
542
|
+
await app.db.db.insert(projectTeams).values({ projectId: project!.id, teamId: teamC.id, role: 'viewer' });
|
|
543
|
+
const [local] = await app.db.db
|
|
544
|
+
.insert(workspaceTeams)
|
|
545
|
+
.values({ workspaceId, hubTeamId: null, slug: 'local-crew', name: 'Local crew' })
|
|
546
|
+
.returning({ id: workspaceTeams.id });
|
|
547
|
+
|
|
548
|
+
hub.setOrgTeams(ORG_T, [A, B]);
|
|
549
|
+
expireOrgTeams();
|
|
550
|
+
expect((await me(cookie)).status).toBe(200);
|
|
551
|
+
|
|
552
|
+
expect(await hubIdsOf()).toEqual([TEAM_A, TEAM_B, null]);
|
|
553
|
+
const { rows: grants } = await app.db.pool.query(`SELECT id FROM project_teams WHERE team_id = $1`, [
|
|
554
|
+
teamC.id
|
|
555
|
+
]);
|
|
556
|
+
expect(grants).toEqual([]);
|
|
557
|
+
const { rows: localRows } = await app.db.pool.query(`SELECT slug FROM workspace_teams WHERE id = $1`, [
|
|
558
|
+
local!.id
|
|
559
|
+
]);
|
|
560
|
+
expect(localRows).toEqual([{ slug: 'local-crew' }]);
|
|
561
|
+
expect(await seatsOf(tom.email, ORG_T)).toEqual([TEAM_A]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('a team renamed at the hub follows, on the same row', async () => {
|
|
565
|
+
const before = (await teamsOf(ORG_T)).find((t) => t.hub_team_id === TEAM_B)!;
|
|
566
|
+
hub.setOrgTeams(ORG_T, [A, { id: TEAM_B, slug: 'beta-squad', name: 'Beta Squad' }]);
|
|
567
|
+
expireOrgTeams();
|
|
568
|
+
expect((await me(cookie)).status).toBe(200);
|
|
569
|
+
const after = (await teamsOf(ORG_T)).find((t) => t.hub_team_id === TEAM_B)!;
|
|
570
|
+
expect(after).toMatchObject({ id: before.id, slug: 'beta-squad', name: 'Beta Squad' });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('a failed read (500, then an older hub’s 404) keeps the list and waits retryMs before the next', async () => {
|
|
574
|
+
// A list that WOULD delete B if it were read.
|
|
575
|
+
hub.setOrgTeams(ORG_T, [A]);
|
|
576
|
+
hub.teamsMode = 'http500';
|
|
577
|
+
try {
|
|
578
|
+
expireOrgTeams();
|
|
579
|
+
const teamsBefore = hub.teamsRequests.length;
|
|
580
|
+
expect((await me(cookie)).status).toBe(200);
|
|
581
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
582
|
+
expect(await hubIdsOf()).toEqual([TEAM_A, TEAM_B, null]);
|
|
583
|
+
|
|
584
|
+
// Another pass inside retryMs: no read. A LOGIN inside retryMs: no
|
|
585
|
+
// read either (a login skips the window, never the failure throttle,
|
|
586
|
+
// verifier round 2).
|
|
587
|
+
advance(DIALS.reconcileTtlMs + 40);
|
|
588
|
+
expect((await me(cookie)).status).toBe(200);
|
|
589
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
590
|
+
cookie = await sso.ssoLogin(app, hub, tom);
|
|
591
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 1);
|
|
592
|
+
|
|
593
|
+
// Past retryMs: read again, an older hub this time.
|
|
594
|
+
hub.teamsMode = 'http404';
|
|
595
|
+
advance(DIALS.retryMs + 40);
|
|
596
|
+
expect((await me(cookie)).status).toBe(200);
|
|
597
|
+
expect(hub.teamsRequests.length).toBe(teamsBefore + 2);
|
|
598
|
+
expect(await hubIdsOf()).toEqual([TEAM_A, TEAM_B, null]);
|
|
599
|
+
} finally {
|
|
600
|
+
hub.teamsMode = 'ok';
|
|
601
|
+
hub.setOrgTeams(ORG_T, [A, { id: TEAM_B, slug: 'beta-squad', name: 'Beta Squad' }]);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('hub_org_teams_refresh_total is on /metrics and counts the successful reads', async () => {
|
|
606
|
+
const res = await app.app.request('/metrics', {
|
|
607
|
+
headers: { authorization: 'Bearer hub-teams-metrics-token' }
|
|
608
|
+
});
|
|
609
|
+
expect(res.status).toBe(200);
|
|
610
|
+
const text = await res.text();
|
|
611
|
+
expect(text).toMatch(/hub_org_teams_refresh_total\{outcome="ok"\} [1-9]/);
|
|
612
|
+
expect(text).toMatch(/hub_org_teams_refresh_total\{outcome="inconclusive"\} [1-9]/);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('an empty page behind a lost cursor is a cut list: nothing is deleted until an honest read (verifier round 1, F1)', async () => {
|
|
616
|
+
// An org of more than one page (the client asks limit=100): 102 teams.
|
|
617
|
+
const pagy: HubUserFixture = {
|
|
618
|
+
sub: 'hub-pagy',
|
|
619
|
+
email: 'pagy@hub-teams.test',
|
|
620
|
+
name: 'Pagy Paged',
|
|
621
|
+
workspaceId: ORG_P,
|
|
622
|
+
role: 'member',
|
|
623
|
+
workspaceName: 'Org P'
|
|
624
|
+
};
|
|
625
|
+
const many = Array.from({ length: 102 }, (_, i) => {
|
|
626
|
+
const n = String(i + 1).padStart(3, '0');
|
|
627
|
+
return { id: `27270000-eeee-4eee-8fff-000000000${n}`, slug: `t-${n}`, name: `Team ${n}` };
|
|
628
|
+
});
|
|
629
|
+
const idOf = (n: number) => many[n - 1]!.id;
|
|
630
|
+
const pagedIds = async () => (await teamsOf(ORG_P)).map((t) => t.hub_team_id);
|
|
631
|
+
hub.setOrgTeams(ORG_P, many);
|
|
632
|
+
hub.setUserOrg(pagy.sub, ORG_P, { name: 'Org P', role: 'member', teams: [many[0]!] });
|
|
633
|
+
const firstRead = hub.teamsRequests.length;
|
|
634
|
+
const pagyCookie = await sso.ssoLogin(app, hub, pagy);
|
|
635
|
+
expect(hub.teamsRequests.length).toBe(firstRead + 2);
|
|
636
|
+
expect(await pagedIds()).toHaveLength(102);
|
|
637
|
+
expect(await seatsOf(pagy.email, ORG_P)).toEqual([idOf(1)]);
|
|
638
|
+
|
|
639
|
+
// Project entries on a team of the first page and on one of the second
|
|
640
|
+
// (the one an unread page carries: the damage F1 describes).
|
|
641
|
+
const workspaceId = await workspaceIdOf(ORG_P);
|
|
642
|
+
const [project] = await app.db.db
|
|
643
|
+
.insert(projects)
|
|
644
|
+
.values({ workspaceId, name: 'Paged project' })
|
|
645
|
+
.returning({ id: projects.id });
|
|
646
|
+
const local = new Map((await teamsOf(ORG_P)).map((t) => [t.hub_team_id, t.id]));
|
|
647
|
+
for (const n of [50, 101]) {
|
|
648
|
+
await app.db.db
|
|
649
|
+
.insert(projectTeams)
|
|
650
|
+
.values({ projectId: project!.id, teamId: local.get(idOf(n))!, role: 'viewer' });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// t-102 goes at the hub, and the cursor of page 1 is lost: page 2 answers empty.
|
|
654
|
+
hub.setOrgTeams(ORG_P, many.slice(0, 101));
|
|
655
|
+
hub.teamsCursorLost = true;
|
|
656
|
+
try {
|
|
657
|
+
const okBefore = await refreshOkCount();
|
|
658
|
+
const before = hub.teamsRequests.length;
|
|
659
|
+
expireOrgTeams();
|
|
660
|
+
expect((await me(pagyCookie)).status).toBe(200);
|
|
661
|
+
// Both pages were read, the read counted ok, and nothing was deleted.
|
|
662
|
+
expect(hub.teamsRequests.length).toBe(before + 2);
|
|
663
|
+
expect(await refreshOkCount()).toBe(okBefore + 1);
|
|
664
|
+
expect(await pagedIds()).toHaveLength(102);
|
|
665
|
+
const { rows: entries } = await app.db.pool.query(
|
|
666
|
+
`SELECT team_id FROM project_teams WHERE project_id = $1`,
|
|
667
|
+
[project!.id]
|
|
668
|
+
);
|
|
669
|
+
expect(entries).toHaveLength(2);
|
|
670
|
+
} finally {
|
|
671
|
+
hub.teamsCursorLost = false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// The next honest read deletes what the hub dropped, and only that.
|
|
675
|
+
expireOrgTeams();
|
|
676
|
+
expect((await me(pagyCookie)).status).toBe(200);
|
|
677
|
+
const after = await pagedIds();
|
|
678
|
+
expect(after).toHaveLength(101);
|
|
679
|
+
expect(after).not.toContain(idOf(102));
|
|
680
|
+
const { rows: kept } = await app.db.pool.query(
|
|
681
|
+
`SELECT team_id FROM project_teams WHERE project_id = $1 ORDER BY team_id`,
|
|
682
|
+
[project!.id]
|
|
683
|
+
);
|
|
684
|
+
expect(kept.map((r) => r.team_id).sort()).toEqual([local.get(idOf(50)), local.get(idOf(101))].sort());
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('a team projected while the list was being read survives that read’s delete; an older one does not (verifier round 1, F5)', async () => {
|
|
688
|
+
const workspaceId = await workspaceIdOf(ORG_T);
|
|
689
|
+
const OLD = '27270000-dddd-4eee-8fff-0000000000f1';
|
|
690
|
+
const LATE = '27270000-dddd-4eee-8fff-0000000000f2';
|
|
691
|
+
// Projected BEFORE the read, gone from the hub's list: the pass deletes it.
|
|
692
|
+
await app.db.db.insert(workspaceTeams).values({
|
|
693
|
+
workspaceId,
|
|
694
|
+
hubTeamId: OLD,
|
|
695
|
+
slug: 'old-gone',
|
|
696
|
+
name: 'Old gone',
|
|
697
|
+
createdAt: new Date(Date.now() - 60_000)
|
|
698
|
+
});
|
|
699
|
+
// Held under the client's own timeout (the dial's orgsTimeoutMs, 400 ms),
|
|
700
|
+
// long enough for a write to land while the read is out.
|
|
701
|
+
hub.teamsDelayMs = 250;
|
|
702
|
+
try {
|
|
703
|
+
expireOrgTeams();
|
|
704
|
+
const before = hub.teamsRequests.length;
|
|
705
|
+
const pending = me(cookie);
|
|
706
|
+
// The read has begun (the hub holds it): another replica projects a team
|
|
707
|
+
// the list it is reading cannot carry yet.
|
|
708
|
+
for (let i = 0; hub.teamsRequests.length === before && i < 200; i++) await sleep(10);
|
|
709
|
+
expect(hub.teamsRequests.length).toBe(before + 1);
|
|
710
|
+
await sleep(20);
|
|
711
|
+
await app.db.db
|
|
712
|
+
.insert(workspaceTeams)
|
|
713
|
+
.values({ workspaceId, hubTeamId: LATE, slug: 'late-comer', name: 'Late comer' });
|
|
714
|
+
expect((await pending).status).toBe(200);
|
|
715
|
+
} finally {
|
|
716
|
+
hub.teamsDelayMs = 0;
|
|
717
|
+
}
|
|
718
|
+
const ids = await hubIdsOf();
|
|
719
|
+
expect(ids).toContain(LATE);
|
|
720
|
+
expect(ids).not.toContain(OLD);
|
|
721
|
+
// Tidy: the late team is not the hub's; the next honest pass would take it.
|
|
722
|
+
await app.db.db.delete(workspaceTeams).where(eq(workspaceTeams.hubTeamId, LATE));
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('projectOrgTeams deletes nothing from an incomplete list, and the rest from a complete one (verifier round 1, mutation #11)', async () => {
|
|
726
|
+
const workspaceId = await workspaceIdOf(ORG_T);
|
|
727
|
+
expect(await hubIdsOf()).toEqual(expect.arrayContaining([TEAM_A, TEAM_B]));
|
|
728
|
+
await app.db.db.transaction((tx) => projectOrgTeams(tx, { workspaceId, teams: [A], complete: false }));
|
|
729
|
+
expect(await hubIdsOf()).toEqual(expect.arrayContaining([TEAM_A, TEAM_B]));
|
|
730
|
+
await app.db.db.transaction((tx) => projectOrgTeams(tx, { workspaceId, teams: [A], complete: true }));
|
|
731
|
+
const ids = await hubIdsOf();
|
|
732
|
+
expect(ids).toContain(TEAM_A);
|
|
733
|
+
expect(ids).not.toContain(TEAM_B);
|
|
734
|
+
// The local team is never the projection's.
|
|
735
|
+
expect(ids).toContain(null);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('an EMPTY org list deletes every projected team and never a local one (verifier round 1, mutation #10)', async () => {
|
|
739
|
+
const workspaceId = await workspaceIdOf(ORG_T);
|
|
740
|
+
// B back as a projected team she is NOT seated in (the call above took it).
|
|
741
|
+
await app.db.db.transaction((tx) => projectOrgTeams(tx, { workspaceId, teams: [A, B], complete: false }));
|
|
742
|
+
const [local] = await app.db.db
|
|
743
|
+
.insert(workspaceTeams)
|
|
744
|
+
.values({ workspaceId, hubTeamId: null, slug: 'local-survivor', name: 'Local survivor' })
|
|
745
|
+
.returning({ id: workspaceTeams.id });
|
|
746
|
+
expect(await hubIdsOf()).toEqual(expect.arrayContaining([TEAM_A, TEAM_B]));
|
|
747
|
+
|
|
748
|
+
hub.setOrgTeams(ORG_T, []);
|
|
749
|
+
expireOrgTeams();
|
|
750
|
+
expect((await me(cookie)).status).toBe(200);
|
|
751
|
+
|
|
752
|
+
const { rows } = await app.db.pool.query(
|
|
753
|
+
`SELECT id, hub_team_id FROM workspace_teams WHERE workspace_id = $1`,
|
|
754
|
+
[workspaceId]
|
|
755
|
+
);
|
|
756
|
+
const hubIds = rows.map((r) => r.hub_team_id as string | null);
|
|
757
|
+
expect(hubIds).not.toContain(TEAM_B);
|
|
758
|
+
// Her own seat's team may come back through `projectTeamSeats`; nothing else projected stays.
|
|
759
|
+
expect(hubIds.filter((h) => h !== null && h !== TEAM_A)).toEqual([]);
|
|
760
|
+
expect(rows.map((r) => r.id)).toContain(local!.id);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
async function refreshOkCount(): Promise<number> {
|
|
765
|
+
const res = await app.app.request('/metrics', {
|
|
766
|
+
headers: { authorization: 'Bearer hub-teams-metrics-token' }
|
|
767
|
+
});
|
|
768
|
+
expect(res.status).toBe(200);
|
|
769
|
+
const match = /hub_org_teams_refresh_total\{outcome="ok"\} (\d+)/.exec(await res.text());
|
|
770
|
+
return match ? Number(match[1]) : 0;
|
|
771
|
+
}
|