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