@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,624 @@
|
|
|
1
|
+
import { and, desc, eq, isNotNull, isNull, sql } from 'drizzle-orm';
|
|
2
|
+
import { projectRoleAtLeast } from '@antasphere/chassis-contract';
|
|
3
|
+
import { projectArchiveRoute, projectCreateRoute, projectGetRoute, projectMemberAddRoute, projectMemberRemoveRoute, projectMemberRoleRoute, projectMembersListRoute, projectsListRoute, projectTeamRemoveRoute, projectTeamRoleRoute, projectUnarchiveRoute, projectUpdateRoute } from '@antasphere/chassis-contract/routes';
|
|
4
|
+
import { projectMembers, projects, projectTeams, user as userTable, workspaceMembers, workspaceTeams } from '@antasphere/chassis-db';
|
|
5
|
+
import { requireAuth, requireNonGuest } from '../middleware/auth-context.js';
|
|
6
|
+
import { holdLiveMembership } from '../members/removal.js';
|
|
7
|
+
import { createdAtText, cursorRowId, decodeKeysetCursor, encodeKeysetCursor, keysetBefore, pageOf } from '../pagination.js';
|
|
8
|
+
import { PROJECTS_ID, projectGrantPredicate, projectRole, projectRoleExpression } from '../projects/access.js';
|
|
9
|
+
const err = (code, message) => ({ error: { code, message } });
|
|
10
|
+
const notFound = () => err('not_found', 'Project not found');
|
|
11
|
+
/** A Postgres foreign-key violation (SQLSTATE 23503) on the named constraint, down drizzle's `cause` chain. */
|
|
12
|
+
function isForeignKeyViolation(e, constraint) {
|
|
13
|
+
for (let cur = e, depth = 0; cur instanceof Error && depth < 10; cur = cur.cause, depth++) {
|
|
14
|
+
const link = cur;
|
|
15
|
+
if (link.code === '23503')
|
|
16
|
+
return link.constraint === constraint;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/** The foreign key a team's place on a project rides on: the one violation that means "the team went". */
|
|
21
|
+
const PROJECT_TEAM_FK = 'project_teams_team_id_workspace_teams_id_fk';
|
|
22
|
+
const toWire = (p) => ({
|
|
23
|
+
id: p.id,
|
|
24
|
+
name: p.name,
|
|
25
|
+
description: p.description,
|
|
26
|
+
metadata: p.metadata,
|
|
27
|
+
archivedAt: p.archivedAt?.toISOString() ?? null,
|
|
28
|
+
createdBy: p.createdBy,
|
|
29
|
+
createdAt: p.createdAt.toISOString(),
|
|
30
|
+
updatedAt: p.updatedAt.toISOString(),
|
|
31
|
+
myRole: p.myRole,
|
|
32
|
+
memberCount: p.memberCount
|
|
33
|
+
});
|
|
34
|
+
const memberToWire = (m) => ({
|
|
35
|
+
kind: 'person',
|
|
36
|
+
userId: m.userId,
|
|
37
|
+
email: m.email,
|
|
38
|
+
name: m.name,
|
|
39
|
+
role: m.role,
|
|
40
|
+
addedBy: m.addedBy,
|
|
41
|
+
createdAt: m.createdAt.toISOString()
|
|
42
|
+
});
|
|
43
|
+
const teamToWire = (t) => ({
|
|
44
|
+
kind: 'team',
|
|
45
|
+
teamId: t.teamId,
|
|
46
|
+
slug: t.slug,
|
|
47
|
+
name: t.name,
|
|
48
|
+
membersCount: Number(t.membersCount),
|
|
49
|
+
hubTeamId: t.hubTeamId,
|
|
50
|
+
role: t.role,
|
|
51
|
+
addedBy: t.addedBy,
|
|
52
|
+
createdAt: t.createdAt.toISOString()
|
|
53
|
+
});
|
|
54
|
+
/** How many people a team seats: the people who hold the project role through it. */
|
|
55
|
+
const TEAM_MEMBERS_COUNT = sql `(SELECT count(*)::int FROM workspace_team_members prj_tmc WHERE prj_tmc.team_id = ${sql.raw('"workspace_teams"."id"')})`;
|
|
56
|
+
/** The wire columns of a project, the caller's role and the member count computed in the same read. */
|
|
57
|
+
const projectSelection = (principal) => ({
|
|
58
|
+
id: projects.id,
|
|
59
|
+
name: projects.name,
|
|
60
|
+
description: projects.description,
|
|
61
|
+
metadata: projects.metadata,
|
|
62
|
+
archivedAt: projects.archivedAt,
|
|
63
|
+
createdBy: projects.createdBy,
|
|
64
|
+
createdAt: projects.createdAt,
|
|
65
|
+
updatedAt: projects.updatedAt,
|
|
66
|
+
myRole: projectRoleExpression(principal, PROJECTS_ID),
|
|
67
|
+
// People and teams alike: what the members list shows.
|
|
68
|
+
memberCount: sql `(SELECT count(*)::int FROM project_members prj_count WHERE prj_count.project_id = ${PROJECTS_ID})
|
|
69
|
+
+ (SELECT count(*)::int FROM project_teams prj_tcount WHERE prj_tcount.project_id = ${PROJECTS_ID})`
|
|
70
|
+
});
|
|
71
|
+
// The members list pages with the value-carrying cursor (`keysetBeforeValue`):
|
|
72
|
+
// project_members HARD-deletes rows, so a cursor naming a member removed
|
|
73
|
+
// between two pages would compare against NULL under `keysetBefore` and end
|
|
74
|
+
// the roster early. The timestamp rides as Postgres rendered it, never as a
|
|
75
|
+
// JS Date, so a millisecond shared by two rows loses nobody.
|
|
76
|
+
const memberSelection = {
|
|
77
|
+
id: projectMembers.id,
|
|
78
|
+
createdAtText: createdAtText(projectMembers.createdAt),
|
|
79
|
+
userId: workspaceMembers.userId,
|
|
80
|
+
email: userTable.email,
|
|
81
|
+
name: userTable.name,
|
|
82
|
+
role: projectMembers.role,
|
|
83
|
+
addedBy: projectMembers.addedBy,
|
|
84
|
+
createdAt: projectMembers.createdAt
|
|
85
|
+
};
|
|
86
|
+
/** A team's entry on a project, joined to the team; the same cursor columns as a person's. */
|
|
87
|
+
const teamSelection = {
|
|
88
|
+
id: projectTeams.id,
|
|
89
|
+
createdAtText: createdAtText(projectTeams.createdAt),
|
|
90
|
+
teamId: workspaceTeams.id,
|
|
91
|
+
slug: workspaceTeams.slug,
|
|
92
|
+
name: workspaceTeams.name,
|
|
93
|
+
membersCount: TEAM_MEMBERS_COUNT,
|
|
94
|
+
hubTeamId: workspaceTeams.hubTeamId,
|
|
95
|
+
role: projectTeams.role,
|
|
96
|
+
addedBy: projectTeams.addedBy,
|
|
97
|
+
createdAt: projectTeams.createdAt
|
|
98
|
+
};
|
|
99
|
+
export function registerProjectRoutes(api, deps) {
|
|
100
|
+
const { db } = deps;
|
|
101
|
+
// Projects are a workspace-level surface: a guest membership exists for
|
|
102
|
+
// principal resolution only (D2) and is refused the whole subtree, flat, on
|
|
103
|
+
// both editions and for every credential. The predicates refuse a guest a
|
|
104
|
+
// second time (`projects/access.ts`): this gate is the truthful answer, that
|
|
105
|
+
// clause is what a tool's own rule inherits.
|
|
106
|
+
for (const path of ['/projects', '/projects/*']) {
|
|
107
|
+
api.use(path, requireAuth());
|
|
108
|
+
api.use(path, requireNonGuest());
|
|
109
|
+
}
|
|
110
|
+
/** One project of the request's workspace AS THE CALLER reads it, or null (= 404). */
|
|
111
|
+
async function readProject(conn, principal, id) {
|
|
112
|
+
const [row] = await conn
|
|
113
|
+
.select(projectSelection(principal))
|
|
114
|
+
.from(projects)
|
|
115
|
+
.where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId)))
|
|
116
|
+
.limit(1);
|
|
117
|
+
if (!row || row.myRole === null)
|
|
118
|
+
return null;
|
|
119
|
+
return { ...row, myRole: row.myRole };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The gate of every mutation, in its fixed order: 404, then 403, then 409.
|
|
123
|
+
* Returns the project as the caller reads it, or the response to send.
|
|
124
|
+
*/
|
|
125
|
+
async function gateMutation(c, id, min, opts = {}) {
|
|
126
|
+
const principal = c.get('principal');
|
|
127
|
+
const project = await readProject(db, principal, id);
|
|
128
|
+
if (!project)
|
|
129
|
+
return { response: c.json(notFound(), 404) };
|
|
130
|
+
if (!projectRoleAtLeast(project.myRole, min)) {
|
|
131
|
+
return {
|
|
132
|
+
response: c.json(err('insufficient_project_role', `This needs the ${min} role on the project`), 403)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (project.archivedAt !== null && !opts.allowArchived) {
|
|
136
|
+
return { response: c.json(archived(), 409) };
|
|
137
|
+
}
|
|
138
|
+
return { project };
|
|
139
|
+
}
|
|
140
|
+
const archived = () => err('project_archived', 'The project is archived: unarchive it to change it');
|
|
141
|
+
/**
|
|
142
|
+
* Runs a membership write while holding the project row FOR SHARE, after
|
|
143
|
+
* re-reading its archive state under that lock. Archiving UPDATEs the row,
|
|
144
|
+
* so it waits for the write (or the write sees it): an archive landing
|
|
145
|
+
* between the gate and the write can never be overtaken. `null` = archived.
|
|
146
|
+
*/
|
|
147
|
+
async function whileLive(principal, id, write) {
|
|
148
|
+
return db.transaction(async (tx) => {
|
|
149
|
+
const [row] = await tx
|
|
150
|
+
.select({ archivedAt: projects.archivedAt })
|
|
151
|
+
.from(projects)
|
|
152
|
+
.where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId)))
|
|
153
|
+
.for('share')
|
|
154
|
+
.limit(1);
|
|
155
|
+
if (!row || row.archivedAt !== null)
|
|
156
|
+
return null;
|
|
157
|
+
return write(tx);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ── List ─────────────────────────────────────────────────────────────────
|
|
161
|
+
api.openapi(projectsListRoute, async (c) => {
|
|
162
|
+
const principal = c.get('principal');
|
|
163
|
+
const { cursor, limit, archived: archivedFilter } = c.req.valid('query');
|
|
164
|
+
const cursorId = cursorRowId(cursor);
|
|
165
|
+
const rows = await db
|
|
166
|
+
.select(projectSelection(principal))
|
|
167
|
+
.from(projects)
|
|
168
|
+
.where(and(eq(projects.workspaceId, principal.workspaceId),
|
|
169
|
+
// The read rule, the same builder a tool uses: a member's page is
|
|
170
|
+
// the projects they hold a grant on, an owner's or admin's is all.
|
|
171
|
+
projectGrantPredicate(principal, PROJECTS_ID, { atLeast: 'viewer', access: 'read' }), ...(archivedFilter === 'false' ? [isNull(projects.archivedAt)] : []), ...(archivedFilter === 'true' ? [isNotNull(projects.archivedAt)] : []), ...(cursorId
|
|
172
|
+
? [
|
|
173
|
+
keysetBefore({
|
|
174
|
+
table: projects,
|
|
175
|
+
id: projects.id,
|
|
176
|
+
createdAt: projects.createdAt,
|
|
177
|
+
workspaceId: projects.workspaceId,
|
|
178
|
+
cursorId,
|
|
179
|
+
workspace: principal.workspaceId
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
: [])))
|
|
183
|
+
.orderBy(desc(projects.createdAt), desc(projects.id))
|
|
184
|
+
.limit(limit + 1);
|
|
185
|
+
const { page, nextCursor } = pageOf(rows, limit);
|
|
186
|
+
// The WHERE is the rule: every selected row carries a role.
|
|
187
|
+
return c.json({ projects: page.map((p) => toWire({ ...p, myRole: p.myRole })), nextCursor }, 200);
|
|
188
|
+
});
|
|
189
|
+
// ── Create ───────────────────────────────────────────────────────────────
|
|
190
|
+
// Any non-guest member creates a project and becomes its first manager in
|
|
191
|
+
// the SAME transaction: a project is never born without someone to manage it
|
|
192
|
+
// (an owner or admin could anyway, but the creator should not need one).
|
|
193
|
+
api.openapi(projectCreateRoute, async (c) => {
|
|
194
|
+
const principal = c.get('principal');
|
|
195
|
+
const body = c.req.valid('json');
|
|
196
|
+
const created = await db.transaction(async (tx) => {
|
|
197
|
+
const [membership] = await tx
|
|
198
|
+
.select({ id: workspaceMembers.id })
|
|
199
|
+
.from(workspaceMembers)
|
|
200
|
+
.where(and(eq(workspaceMembers.workspaceId, principal.workspaceId), eq(workspaceMembers.userId, principal.userId), eq(workspaceMembers.isActive, true)))
|
|
201
|
+
// Held until the creator's grant commits, like every add's
|
|
202
|
+
// (`holdLiveMembership`): a removal of the creator waits for it.
|
|
203
|
+
.for('share')
|
|
204
|
+
.limit(1);
|
|
205
|
+
if (!membership)
|
|
206
|
+
return null;
|
|
207
|
+
const [project] = await tx
|
|
208
|
+
.insert(projects)
|
|
209
|
+
.values({
|
|
210
|
+
workspaceId: principal.workspaceId,
|
|
211
|
+
name: body.name,
|
|
212
|
+
description: body.description ?? null,
|
|
213
|
+
metadata: body.metadata ?? {},
|
|
214
|
+
createdBy: principal.userId
|
|
215
|
+
})
|
|
216
|
+
.returning({ id: projects.id });
|
|
217
|
+
await tx.insert(projectMembers).values({
|
|
218
|
+
projectId: project.id,
|
|
219
|
+
memberId: membership.id,
|
|
220
|
+
role: 'manager',
|
|
221
|
+
addedBy: principal.userId
|
|
222
|
+
});
|
|
223
|
+
return readProject(tx, principal, project.id);
|
|
224
|
+
});
|
|
225
|
+
// The principal resolved from an active membership a moment ago; losing
|
|
226
|
+
// it mid-request is a revoked caller, answered like any other.
|
|
227
|
+
if (!created)
|
|
228
|
+
return c.json(err('unauthenticated', 'Authentication required'), 401);
|
|
229
|
+
c.set('audit', {
|
|
230
|
+
action: 'project.create',
|
|
231
|
+
resourceType: 'project',
|
|
232
|
+
resourceId: created.id,
|
|
233
|
+
metadata: { name: created.name }
|
|
234
|
+
});
|
|
235
|
+
return c.json(toWire(created), 201);
|
|
236
|
+
});
|
|
237
|
+
// ── Get ──────────────────────────────────────────────────────────────────
|
|
238
|
+
api.openapi(projectGetRoute, async (c) => {
|
|
239
|
+
const principal = c.get('principal');
|
|
240
|
+
const { id } = c.req.valid('param');
|
|
241
|
+
const project = await readProject(db, principal, id);
|
|
242
|
+
if (!project)
|
|
243
|
+
return c.json(notFound(), 404);
|
|
244
|
+
return c.json(toWire(project), 200);
|
|
245
|
+
});
|
|
246
|
+
// ── Update ───────────────────────────────────────────────────────────────
|
|
247
|
+
api.openapi(projectUpdateRoute, async (c) => {
|
|
248
|
+
const principal = c.get('principal');
|
|
249
|
+
const { id } = c.req.valid('param');
|
|
250
|
+
const patch = c.req.valid('json');
|
|
251
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
252
|
+
if ('response' in gate)
|
|
253
|
+
return gate.response;
|
|
254
|
+
// `archived_at IS NULL` again in the WHERE: an archive landing between
|
|
255
|
+
// the gate and this write must win.
|
|
256
|
+
const updated = await db
|
|
257
|
+
.update(projects)
|
|
258
|
+
.set({
|
|
259
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
260
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
261
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
262
|
+
updatedAt: new Date()
|
|
263
|
+
})
|
|
264
|
+
.where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId), isNull(projects.archivedAt)))
|
|
265
|
+
.returning({ id: projects.id });
|
|
266
|
+
if (updated.length === 0)
|
|
267
|
+
return c.json(archived(), 409);
|
|
268
|
+
const project = (await readProject(db, principal, id));
|
|
269
|
+
c.set('audit', {
|
|
270
|
+
action: 'project.update',
|
|
271
|
+
resourceType: 'project',
|
|
272
|
+
resourceId: id,
|
|
273
|
+
metadata: { fields: Object.keys(patch) }
|
|
274
|
+
});
|
|
275
|
+
return c.json(toWire(project), 200);
|
|
276
|
+
});
|
|
277
|
+
// ── Archive / unarchive ──────────────────────────────────────────────────
|
|
278
|
+
// A project is never deleted. Archiving cascades onto NOTHING: what a tool
|
|
279
|
+
// linked to the project keeps working; only writes THROUGH a project grant
|
|
280
|
+
// stop (the archived-write rule of the predicate).
|
|
281
|
+
api.openapi(projectArchiveRoute, async (c) => {
|
|
282
|
+
const principal = c.get('principal');
|
|
283
|
+
const { id } = c.req.valid('param');
|
|
284
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
285
|
+
if ('response' in gate)
|
|
286
|
+
return gate.response;
|
|
287
|
+
const updated = await db
|
|
288
|
+
.update(projects)
|
|
289
|
+
.set({ archivedAt: new Date(), updatedAt: new Date() })
|
|
290
|
+
.where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId), isNull(projects.archivedAt)))
|
|
291
|
+
.returning({ id: projects.id });
|
|
292
|
+
if (updated.length === 0)
|
|
293
|
+
return c.json(archived(), 409);
|
|
294
|
+
const project = (await readProject(db, principal, id));
|
|
295
|
+
c.set('audit', { action: 'project.archive', resourceType: 'project', resourceId: id });
|
|
296
|
+
return c.json(toWire(project), 200);
|
|
297
|
+
});
|
|
298
|
+
api.openapi(projectUnarchiveRoute, async (c) => {
|
|
299
|
+
const principal = c.get('principal');
|
|
300
|
+
const { id } = c.req.valid('param');
|
|
301
|
+
const gate = await gateMutation(c, id, 'manager', { allowArchived: true });
|
|
302
|
+
if ('response' in gate)
|
|
303
|
+
return gate.response;
|
|
304
|
+
const updated = await db
|
|
305
|
+
.update(projects)
|
|
306
|
+
.set({ archivedAt: null, updatedAt: new Date() })
|
|
307
|
+
.where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId), isNotNull(projects.archivedAt)))
|
|
308
|
+
.returning({ id: projects.id });
|
|
309
|
+
if (updated.length === 0) {
|
|
310
|
+
return c.json(err('project_not_archived', 'The project is not archived'), 409);
|
|
311
|
+
}
|
|
312
|
+
const project = (await readProject(db, principal, id));
|
|
313
|
+
c.set('audit', { action: 'project.unarchive', resourceType: 'project', resourceId: id });
|
|
314
|
+
return c.json(toWire(project), 200);
|
|
315
|
+
});
|
|
316
|
+
// ── Members ──────────────────────────────────────────────────────────────
|
|
317
|
+
// People and teams in ONE list, newest first, on one value-carrying cursor
|
|
318
|
+
// (`created_at`, `id`) that both tables answer: the cursor is applied inside
|
|
319
|
+
// each branch, so a page is the same page whichever kind the boundary row
|
|
320
|
+
// is. Raw SQL because a set operation is where drizzle's builder stops.
|
|
321
|
+
api.openapi(projectMembersListRoute, async (c) => {
|
|
322
|
+
const principal = c.get('principal');
|
|
323
|
+
const { id } = c.req.valid('param');
|
|
324
|
+
const { cursor, limit } = c.req.valid('query');
|
|
325
|
+
if ((await projectRole(db, principal, id)) === null)
|
|
326
|
+
return c.json(notFound(), 404);
|
|
327
|
+
const after = decodeKeysetCursor(cursor);
|
|
328
|
+
const before = (createdAt, rowId) => after
|
|
329
|
+
? sql `AND (${sql.raw(createdAt)}, ${sql.raw(rowId)}) < (${after.createdAt}::timestamptz, ${after.id}::uuid)`
|
|
330
|
+
: sql ``;
|
|
331
|
+
const result = await db.execute(sql `
|
|
332
|
+
SELECT 'person' AS kind, pm.id, pm.created_at::text AS created_at_text, pm.created_at, pm.role, pm.added_by,
|
|
333
|
+
wm.user_id, u.email, u.name,
|
|
334
|
+
NULL::uuid AS team_id, NULL::text AS slug, NULL::int AS members_count, NULL::text AS hub_team_id
|
|
335
|
+
FROM project_members pm
|
|
336
|
+
JOIN workspace_members wm ON wm.id = pm.member_id
|
|
337
|
+
JOIN "user" u ON u.id = wm.user_id
|
|
338
|
+
WHERE pm.project_id = ${id} ${before('pm.created_at', 'pm.id')}
|
|
339
|
+
UNION ALL
|
|
340
|
+
SELECT 'team' AS kind, pt.id, pt.created_at::text AS created_at_text, pt.created_at, pt.role, pt.added_by,
|
|
341
|
+
NULL::text AS user_id, NULL::text AS email, t.name,
|
|
342
|
+
t.id AS team_id, t.slug,
|
|
343
|
+
(SELECT count(*)::int FROM workspace_team_members tm WHERE tm.team_id = t.id) AS members_count,
|
|
344
|
+
t.hub_team_id
|
|
345
|
+
FROM project_teams pt
|
|
346
|
+
JOIN workspace_teams t ON t.id = pt.team_id
|
|
347
|
+
WHERE pt.project_id = ${id} ${before('pt.created_at', 'pt.id')}
|
|
348
|
+
ORDER BY created_at DESC, id DESC
|
|
349
|
+
LIMIT ${limit + 1}
|
|
350
|
+
`);
|
|
351
|
+
const rows = result.rows;
|
|
352
|
+
const page = rows.slice(0, limit);
|
|
353
|
+
const last = rows.length > limit ? page[page.length - 1] : undefined;
|
|
354
|
+
const members = page.map((row) => row.kind === 'person'
|
|
355
|
+
? memberToWire({
|
|
356
|
+
userId: row.user_id,
|
|
357
|
+
email: row.email,
|
|
358
|
+
name: row.name,
|
|
359
|
+
role: row.role,
|
|
360
|
+
addedBy: row.added_by,
|
|
361
|
+
createdAt: new Date(row.created_at)
|
|
362
|
+
})
|
|
363
|
+
: teamToWire({
|
|
364
|
+
teamId: row.team_id,
|
|
365
|
+
slug: row.slug,
|
|
366
|
+
name: row.name,
|
|
367
|
+
membersCount: row.members_count ?? 0,
|
|
368
|
+
hubTeamId: row.hub_team_id,
|
|
369
|
+
role: row.role,
|
|
370
|
+
addedBy: row.added_by,
|
|
371
|
+
createdAt: new Date(row.created_at)
|
|
372
|
+
}));
|
|
373
|
+
return c.json({
|
|
374
|
+
members,
|
|
375
|
+
nextCursor: last ? encodeKeysetCursor({ id: last.id, createdAtText: last.created_at_text }) : null
|
|
376
|
+
}, 200);
|
|
377
|
+
});
|
|
378
|
+
/** One member of one project, by the person's user id, or undefined. */
|
|
379
|
+
async function findProjectMember(conn, projectId, userId) {
|
|
380
|
+
const [row] = await conn
|
|
381
|
+
.select(memberSelection)
|
|
382
|
+
.from(projectMembers)
|
|
383
|
+
.innerJoin(workspaceMembers, eq(projectMembers.memberId, workspaceMembers.id))
|
|
384
|
+
.innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
|
|
385
|
+
.where(and(eq(projectMembers.projectId, projectId), eq(workspaceMembers.userId, userId)))
|
|
386
|
+
.limit(1);
|
|
387
|
+
return row;
|
|
388
|
+
}
|
|
389
|
+
/** One team's entry on one project, by the team id, or undefined. */
|
|
390
|
+
async function findProjectTeam(conn, projectId, teamId) {
|
|
391
|
+
const [row] = await conn
|
|
392
|
+
.select(teamSelection)
|
|
393
|
+
.from(projectTeams)
|
|
394
|
+
.innerJoin(workspaceTeams, eq(projectTeams.teamId, workspaceTeams.id))
|
|
395
|
+
.where(and(eq(projectTeams.projectId, projectId), eq(workspaceTeams.id, teamId)))
|
|
396
|
+
.limit(1);
|
|
397
|
+
return row;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Adds a team of the request's workspace to the project (PRDCT-2794): the
|
|
401
|
+
* team side of the members POST. A team of another workspace, or none, is
|
|
402
|
+
* 404 `team_not_found`; a repeat is 409 `already_member`.
|
|
403
|
+
*/
|
|
404
|
+
async function addTeam(c, id, teamId, role) {
|
|
405
|
+
const principal = c.get('principal');
|
|
406
|
+
const [team] = await db
|
|
407
|
+
.select({
|
|
408
|
+
teamId: workspaceTeams.id,
|
|
409
|
+
slug: workspaceTeams.slug,
|
|
410
|
+
name: workspaceTeams.name,
|
|
411
|
+
membersCount: TEAM_MEMBERS_COUNT,
|
|
412
|
+
hubTeamId: workspaceTeams.hubTeamId
|
|
413
|
+
})
|
|
414
|
+
.from(workspaceTeams)
|
|
415
|
+
.where(and(eq(workspaceTeams.id, teamId), eq(workspaceTeams.workspaceId, principal.workspaceId)))
|
|
416
|
+
.limit(1);
|
|
417
|
+
if (!team)
|
|
418
|
+
return c.json(err('team_not_found', 'No team of this workspace matches'), 404);
|
|
419
|
+
let inserted;
|
|
420
|
+
try {
|
|
421
|
+
inserted = await whileLive(principal, id, (tx) => tx
|
|
422
|
+
.insert(projectTeams)
|
|
423
|
+
.values({ projectId: id, teamId: team.teamId, role, addedBy: principal.userId })
|
|
424
|
+
.onConflictDoNothing({ target: [projectTeams.projectId, projectTeams.teamId] })
|
|
425
|
+
.returning({ createdAt: projectTeams.createdAt }));
|
|
426
|
+
}
|
|
427
|
+
catch (cause) {
|
|
428
|
+
// The team went between the read above and the insert (a delete at the
|
|
429
|
+
// hub's next pass, an admin's): the foreign key says so, and the answer
|
|
430
|
+
// is the same 404 as a team that never was (verifier round 1, F2).
|
|
431
|
+
if (isForeignKeyViolation(cause, PROJECT_TEAM_FK)) {
|
|
432
|
+
return c.json(err('team_not_found', 'No team of this workspace matches'), 404);
|
|
433
|
+
}
|
|
434
|
+
throw cause;
|
|
435
|
+
}
|
|
436
|
+
if (inserted === null)
|
|
437
|
+
return c.json(archived(), 409);
|
|
438
|
+
const [row] = inserted;
|
|
439
|
+
if (!row)
|
|
440
|
+
return c.json(err('already_member', 'This team is already a member of the project'), 409);
|
|
441
|
+
c.set('audit', {
|
|
442
|
+
action: 'project.team_add',
|
|
443
|
+
resourceType: 'project',
|
|
444
|
+
resourceId: id,
|
|
445
|
+
metadata: { teamId: team.teamId, role }
|
|
446
|
+
});
|
|
447
|
+
return c.json(teamToWire({ ...team, role, addedBy: principal.userId, createdAt: row.createdAt }), 201);
|
|
448
|
+
}
|
|
449
|
+
api.openapi(projectMemberAddRoute, async (c) => {
|
|
450
|
+
const principal = c.get('principal');
|
|
451
|
+
const { id } = c.req.valid('param');
|
|
452
|
+
const body = c.req.valid('json');
|
|
453
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
454
|
+
if ('response' in gate)
|
|
455
|
+
return gate.response;
|
|
456
|
+
if (body.teamId !== undefined)
|
|
457
|
+
return (await addTeam(c, id, body.teamId, body.role));
|
|
458
|
+
// The target comes from THIS workspace's own roster, by user id or by
|
|
459
|
+
// email, and nowhere else: nothing is invited, claimed or minted here.
|
|
460
|
+
const [target] = await db
|
|
461
|
+
.select({
|
|
462
|
+
id: workspaceMembers.id,
|
|
463
|
+
userId: workspaceMembers.userId,
|
|
464
|
+
email: userTable.email,
|
|
465
|
+
name: userTable.name,
|
|
466
|
+
origin: workspaceMembers.origin,
|
|
467
|
+
isActive: workspaceMembers.isActive
|
|
468
|
+
})
|
|
469
|
+
.from(workspaceMembers)
|
|
470
|
+
.innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
|
|
471
|
+
.where(and(eq(workspaceMembers.workspaceId, principal.workspaceId), body.userId !== undefined
|
|
472
|
+
? eq(workspaceMembers.userId, body.userId)
|
|
473
|
+
: sql `lower(${userTable.email}) = ${body.email.toLowerCase()}`))
|
|
474
|
+
.limit(1);
|
|
475
|
+
if (!target || !target.isActive) {
|
|
476
|
+
return c.json(err('member_not_found', 'No active member of this workspace matches'), 404);
|
|
477
|
+
}
|
|
478
|
+
// A guest membership exists for principal resolution only (D2): it can
|
|
479
|
+
// never hold a project grant. The predicates would ignore the row anyway;
|
|
480
|
+
// refusing it here keeps the table honest.
|
|
481
|
+
if (target.origin === 'guest') {
|
|
482
|
+
return c.json(err('guest_target', 'A guest cannot be a project member: invite them to the workspace as a member first'), 403);
|
|
483
|
+
}
|
|
484
|
+
// The read above builds the answer; the one that counts is the locked
|
|
485
|
+
// re-read inside the insert's transaction (`holdLiveMembership`): a
|
|
486
|
+
// removal that landed since is seen here, one still running waits for
|
|
487
|
+
// this insert and takes it in its second pass.
|
|
488
|
+
const inserted = await whileLive(principal, id, async (tx) => {
|
|
489
|
+
if (!(await holdLiveMembership(tx, principal.workspaceId, { memberId: target.id }))) {
|
|
490
|
+
return 'member_gone';
|
|
491
|
+
}
|
|
492
|
+
// A grant already there answers `already_member` WITHOUT an insert (the
|
|
493
|
+
// verifier's F4: a duplicate add beside a removal deadlocked, 14 of 40
|
|
494
|
+
// removals answered 500). Under the membership share lock, a removal's
|
|
495
|
+
// uncommitted delete of this grant keeps the row visible to a plain
|
|
496
|
+
// read, so the add answers 409 and never waits on that delete, while
|
|
497
|
+
// the removal's update waits on the add's share lock: the insert's
|
|
498
|
+
// conflict check would wait on the delete, a cycle.
|
|
499
|
+
const [existing] = await tx
|
|
500
|
+
.select({ id: projectMembers.id })
|
|
501
|
+
.from(projectMembers)
|
|
502
|
+
.where(and(eq(projectMembers.projectId, id), eq(projectMembers.memberId, target.id)))
|
|
503
|
+
.limit(1);
|
|
504
|
+
if (existing)
|
|
505
|
+
return [];
|
|
506
|
+
return tx
|
|
507
|
+
.insert(projectMembers)
|
|
508
|
+
.values({ projectId: id, memberId: target.id, role: body.role, addedBy: principal.userId })
|
|
509
|
+
.onConflictDoNothing({ target: [projectMembers.projectId, projectMembers.memberId] })
|
|
510
|
+
.returning({ createdAt: projectMembers.createdAt });
|
|
511
|
+
});
|
|
512
|
+
if (inserted === null)
|
|
513
|
+
return c.json(archived(), 409);
|
|
514
|
+
if (inserted === 'member_gone') {
|
|
515
|
+
return c.json(err('member_not_found', 'No active member of this workspace matches'), 404);
|
|
516
|
+
}
|
|
517
|
+
const [row] = inserted;
|
|
518
|
+
if (!row) {
|
|
519
|
+
return c.json(err('already_member', 'This person is already a member of the project'), 409);
|
|
520
|
+
}
|
|
521
|
+
// The answer is built from what the insert returned and the target row
|
|
522
|
+
// read above, never from a re-read outside the transaction: a removal
|
|
523
|
+
// landing in between would turn a succeeded add into a 500.
|
|
524
|
+
const member = { ...target, role: body.role, addedBy: principal.userId, createdAt: row.createdAt };
|
|
525
|
+
c.set('audit', {
|
|
526
|
+
action: 'project.member_add',
|
|
527
|
+
resourceType: 'project',
|
|
528
|
+
resourceId: id,
|
|
529
|
+
metadata: { targetUserId: target.userId, role: body.role }
|
|
530
|
+
});
|
|
531
|
+
return c.json(memberToWire(member), 201);
|
|
532
|
+
});
|
|
533
|
+
api.openapi(projectMemberRoleRoute, async (c) => {
|
|
534
|
+
const principal = c.get('principal');
|
|
535
|
+
const { id, userId } = c.req.valid('param');
|
|
536
|
+
const { role } = c.req.valid('json');
|
|
537
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
538
|
+
if ('response' in gate)
|
|
539
|
+
return gate.response;
|
|
540
|
+
const target = await findProjectMember(db, id, userId);
|
|
541
|
+
if (!target)
|
|
542
|
+
return c.json(err('member_not_found', 'This person is not a member of the project'), 404);
|
|
543
|
+
const done = await whileLive(principal, id, (tx) => tx.update(projectMembers).set({ role }).where(eq(projectMembers.id, target.id)));
|
|
544
|
+
if (done === null)
|
|
545
|
+
return c.json(archived(), 409);
|
|
546
|
+
c.set('audit', {
|
|
547
|
+
action: 'project.member_role',
|
|
548
|
+
resourceType: 'project',
|
|
549
|
+
resourceId: id,
|
|
550
|
+
metadata: { targetUserId: userId, from: target.role, to: role }
|
|
551
|
+
});
|
|
552
|
+
return c.json(memberToWire({ ...target, role }), 200);
|
|
553
|
+
});
|
|
554
|
+
// A manager removes anyone; any member removes THEMSELVES, whatever their
|
|
555
|
+
// role. There is no last-manager guard: a workspace owner or admin manages
|
|
556
|
+
// every project, so none is ever locked.
|
|
557
|
+
api.openapi(projectMemberRemoveRoute, async (c) => {
|
|
558
|
+
const principal = c.get('principal');
|
|
559
|
+
const { id, userId } = c.req.valid('param');
|
|
560
|
+
const self = userId === principal.userId;
|
|
561
|
+
const gate = await gateMutation(c, id, self ? 'viewer' : 'manager');
|
|
562
|
+
if ('response' in gate)
|
|
563
|
+
return gate.response;
|
|
564
|
+
const target = await findProjectMember(db, id, userId);
|
|
565
|
+
if (!target)
|
|
566
|
+
return c.json(err('member_not_found', 'This person is not a member of the project'), 404);
|
|
567
|
+
const done = await whileLive(principal, id, (tx) => tx.delete(projectMembers).where(eq(projectMembers.id, target.id)));
|
|
568
|
+
if (done === null)
|
|
569
|
+
return c.json(archived(), 409);
|
|
570
|
+
c.set('audit', {
|
|
571
|
+
action: 'project.member_remove',
|
|
572
|
+
resourceType: 'project',
|
|
573
|
+
resourceId: id,
|
|
574
|
+
metadata: { targetUserId: userId, role: target.role, self }
|
|
575
|
+
});
|
|
576
|
+
return c.json(memberToWire(target), 200);
|
|
577
|
+
});
|
|
578
|
+
// ── Teams on a project (PRDCT-2794) ──────────────────────────────────────
|
|
579
|
+
// The same gate and order as a person's entry: 404, 403, 409. A team is
|
|
580
|
+
// never "itself" (no self-removal branch): a manager acts, or an owner or
|
|
581
|
+
// admin of the workspace.
|
|
582
|
+
api.openapi(projectTeamRoleRoute, async (c) => {
|
|
583
|
+
const principal = c.get('principal');
|
|
584
|
+
const { id, teamId } = c.req.valid('param');
|
|
585
|
+
const { role } = c.req.valid('json');
|
|
586
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
587
|
+
if ('response' in gate)
|
|
588
|
+
return gate.response;
|
|
589
|
+
const target = await findProjectTeam(db, id, teamId);
|
|
590
|
+
if (!target)
|
|
591
|
+
return c.json(err('team_not_found', 'This team is not a member of the project'), 404);
|
|
592
|
+
const done = await whileLive(principal, id, (tx) => tx.update(projectTeams).set({ role }).where(eq(projectTeams.id, target.id)));
|
|
593
|
+
if (done === null)
|
|
594
|
+
return c.json(archived(), 409);
|
|
595
|
+
c.set('audit', {
|
|
596
|
+
action: 'project.team_role',
|
|
597
|
+
resourceType: 'project',
|
|
598
|
+
resourceId: id,
|
|
599
|
+
metadata: { teamId, from: target.role, to: role }
|
|
600
|
+
});
|
|
601
|
+
return c.json(teamToWire({ ...target, role }), 200);
|
|
602
|
+
});
|
|
603
|
+
api.openapi(projectTeamRemoveRoute, async (c) => {
|
|
604
|
+
const principal = c.get('principal');
|
|
605
|
+
const { id, teamId } = c.req.valid('param');
|
|
606
|
+
const gate = await gateMutation(c, id, 'manager');
|
|
607
|
+
if ('response' in gate)
|
|
608
|
+
return gate.response;
|
|
609
|
+
const target = await findProjectTeam(db, id, teamId);
|
|
610
|
+
if (!target)
|
|
611
|
+
return c.json(err('team_not_found', 'This team is not a member of the project'), 404);
|
|
612
|
+
const done = await whileLive(principal, id, (tx) => tx.delete(projectTeams).where(eq(projectTeams.id, target.id)));
|
|
613
|
+
if (done === null)
|
|
614
|
+
return c.json(archived(), 409);
|
|
615
|
+
c.set('audit', {
|
|
616
|
+
action: 'project.team_remove',
|
|
617
|
+
resourceType: 'project',
|
|
618
|
+
resourceId: id,
|
|
619
|
+
metadata: { teamId, role: target.role }
|
|
620
|
+
});
|
|
621
|
+
return c.json(teamToWire(target), 200);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
//# sourceMappingURL=projects.js.map
|