@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,482 @@
|
|
|
1
|
+
import type { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { finished } from 'node:stream/promises';
|
|
4
|
+
import { and, asc, eq, gt, isNull, sql } from 'drizzle-orm';
|
|
5
|
+
import { ZipFile } from 'yazl';
|
|
6
|
+
import {
|
|
7
|
+
apiKeys,
|
|
8
|
+
auditLog,
|
|
9
|
+
files,
|
|
10
|
+
instanceSettings,
|
|
11
|
+
invitations,
|
|
12
|
+
projectMembers,
|
|
13
|
+
projects,
|
|
14
|
+
projectTeams,
|
|
15
|
+
user as userTable,
|
|
16
|
+
workspaceMembers,
|
|
17
|
+
workspaces,
|
|
18
|
+
workspaceTeamMembers,
|
|
19
|
+
workspaceTeams,
|
|
20
|
+
type Db
|
|
21
|
+
} from '@antasphere/chassis-db';
|
|
22
|
+
import type { Env } from '../env.js';
|
|
23
|
+
import type { ScopeRoutes } from './scope-routes.js';
|
|
24
|
+
import type { Logger } from '../logger.js';
|
|
25
|
+
import type { AuditService } from '../audit/service.js';
|
|
26
|
+
import { blobKey, type StorageDriver } from '../storage/driver.js';
|
|
27
|
+
import { requireAuth, requireNonGuest, requireRole } from '../middleware/auth-context.js';
|
|
28
|
+
import { rateLimit, type RateLimiters } from '../middleware/rate-limit.js';
|
|
29
|
+
import type { ClientIpFn } from '../middleware/rate-limit.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GET /workspace/export — the GDPR/portability bundle: every workspace table
|
|
33
|
+
* as JSON, the audit log as NDJSON, and every live blob, streamed as one zip
|
|
34
|
+
* (yazl; modeled on the files-content streaming precedent in files.ts).
|
|
35
|
+
* Secrets never leave: invitations drop tokenHash, api-keys drop secretHash.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const AUDIT_BATCH = 500;
|
|
39
|
+
|
|
40
|
+
export interface ExportRouteDeps {
|
|
41
|
+
db: Db;
|
|
42
|
+
env: Pick<Env, 'EDITION' | 'APP_VERSION'>;
|
|
43
|
+
storage: StorageDriver;
|
|
44
|
+
audit: AuditService;
|
|
45
|
+
limiters: RateLimiters;
|
|
46
|
+
clientIp: ClientIpFn;
|
|
47
|
+
logger: Logger;
|
|
48
|
+
/** The export route the tool instantiated: its OpenAPI summary names the tool's export scope. */
|
|
49
|
+
workspaceExportRoute: ScopeRoutes['workspaceExportRoute'];
|
|
50
|
+
/** The tool's `api.exportEntries` slot, already bound to its domain. Absent = the chassis sections only. */
|
|
51
|
+
exportEntries?: ExportEntriesFn;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One entry a tool adds to the bundle: written as `<name>.json`, its rows as a JSON array. */
|
|
55
|
+
export interface ExportEntry {
|
|
56
|
+
name: string;
|
|
57
|
+
rows: unknown[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What the `api.exportEntries` slot returns: the tool's entries for ONE
|
|
62
|
+
* workspace. It reads with the handle the route itself reads with (`db`, no
|
|
63
|
+
* transaction), and it filters on `workspaceId` itself.
|
|
64
|
+
*/
|
|
65
|
+
export type ExportEntriesFn = (db: Db, workspaceId: string) => Promise<ExportEntry[]>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The entry names the chassis writes itself, without their extension. A tool
|
|
69
|
+
* entry that lands on one of them is refused: never a silent overwrite, never
|
|
70
|
+
* two zip entries of one name. (`files/…` needs no line here: a sanitized name
|
|
71
|
+
* holds no `/`.) `audit-log` is reserved although its file is `.ndjson`, so
|
|
72
|
+
* that the bundle never holds two audit logs.
|
|
73
|
+
*/
|
|
74
|
+
export const RESERVED_EXPORT_ENTRY_NAMES = [
|
|
75
|
+
'manifest',
|
|
76
|
+
'workspace',
|
|
77
|
+
'members',
|
|
78
|
+
'invitations',
|
|
79
|
+
'api-keys',
|
|
80
|
+
'projects',
|
|
81
|
+
'project_members',
|
|
82
|
+
'teams',
|
|
83
|
+
'team_members',
|
|
84
|
+
'project_teams',
|
|
85
|
+
'audit-log',
|
|
86
|
+
'files',
|
|
87
|
+
'skipped-blobs'
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
/** A tool entry, ready to write: its zip entry name and its bytes. */
|
|
91
|
+
export interface ResolvedExportEntry {
|
|
92
|
+
entryName: string;
|
|
93
|
+
buffer: Buffer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Tool entries → zip entries, BEFORE the stream starts: every name goes
|
|
98
|
+
* through `sanitizeEntryName`, every `rows` through `jsonBuffer`, and a name
|
|
99
|
+
* that collides (compared without case, since the bundle is extracted on
|
|
100
|
+
* case-insensitive disks too) with a chassis entry or with another tool entry
|
|
101
|
+
* throws. Order is the tool's.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveExportEntries(entries: ExportEntry[]): ResolvedExportEntry[] {
|
|
104
|
+
if (!Array.isArray(entries)) throw new Error('export entries: the tool must return an array of entries');
|
|
105
|
+
const taken = new Set<string>(RESERVED_EXPORT_ENTRY_NAMES);
|
|
106
|
+
return entries.map((entry) => {
|
|
107
|
+
if (typeof entry?.name !== 'string' || !Array.isArray(entry.rows)) {
|
|
108
|
+
throw new Error('export entries: every entry is { name: string, rows: unknown[] }');
|
|
109
|
+
}
|
|
110
|
+
const name = sanitizeEntryName(entry.name);
|
|
111
|
+
const key = name.toLowerCase();
|
|
112
|
+
if (taken.has(key)) {
|
|
113
|
+
throw new Error(`export entries: the name "${name}" is already an entry of the bundle`);
|
|
114
|
+
}
|
|
115
|
+
taken.add(key);
|
|
116
|
+
return { entryName: `${name}.json`, buffer: jsonBuffer(entry.rows) };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Zip-slip guard: entry names embed the user-supplied originalName, so strip
|
|
122
|
+
* path separators (`/`, `\`) and control chars, and cap the length — an
|
|
123
|
+
* uploaded name like `../../etc/cron.d/x` must never place an entry outside
|
|
124
|
+
* the extraction directory.
|
|
125
|
+
*/
|
|
126
|
+
export function sanitizeEntryName(name: string): string {
|
|
127
|
+
// eslint-disable-next-line no-control-regex
|
|
128
|
+
const cleaned = name.replace(/[/\\\u0000-\u001f\u007f]/g, '_');
|
|
129
|
+
return (cleaned || 'file').slice(0, 150);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function jsonBuffer(value: unknown): Buffer {
|
|
133
|
+
return Buffer.from(JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Keyset walk of the audit log, one NDJSON line per row (never buffers the table). */
|
|
137
|
+
async function* auditNdjson(db: Db, workspaceId: string): AsyncGenerator<string> {
|
|
138
|
+
let lastId = 0;
|
|
139
|
+
for (;;) {
|
|
140
|
+
const rows = await db
|
|
141
|
+
.select()
|
|
142
|
+
.from(auditLog)
|
|
143
|
+
.where(and(eq(auditLog.workspaceId, workspaceId), gt(auditLog.id, lastId)))
|
|
144
|
+
.orderBy(asc(auditLog.id))
|
|
145
|
+
.limit(AUDIT_BATCH);
|
|
146
|
+
if (rows.length === 0) return;
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
yield JSON.stringify({ ...row, createdAt: row.createdAt.toISOString() }) + '\n';
|
|
149
|
+
lastId = row.id;
|
|
150
|
+
}
|
|
151
|
+
if (rows.length < AUDIT_BATCH) return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function registerExportRoutes(api: OpenAPIHono, deps: ExportRouteDeps): void {
|
|
156
|
+
const { db, env, storage, audit, limiters, clientIp, logger } = deps;
|
|
157
|
+
|
|
158
|
+
// Gates, in order: rate limit (IP AND user — exports are expensive) →
|
|
159
|
+
// authenticated → admin+. Machine principals additionally passed the
|
|
160
|
+
// fail-closed export-scope gate in authContext before landing here.
|
|
161
|
+
api.use(
|
|
162
|
+
'/workspace/export',
|
|
163
|
+
rateLimit(limiters.workspaceExport, clientIp, async (c) => {
|
|
164
|
+
const principal = c.get('principal');
|
|
165
|
+
return principal ? [`u:${principal.userId}`] : [];
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
api.use('/workspace/export', requireAuth());
|
|
169
|
+
// Guest capability limit (D2): the whole-workspace export is the named
|
|
170
|
+
// workspace-scope surface guests must never reach. Defense in depth in
|
|
171
|
+
// front of the admin gate — a guest is minted role 'member' and the role
|
|
172
|
+
// is locked (api/members.ts), but export is exactly the surface where a
|
|
173
|
+
// second, origin-keyed wall is cheap and the failure catastrophic.
|
|
174
|
+
api.use('/workspace/export', requireNonGuest());
|
|
175
|
+
api.use('/workspace/export', requireRole('admin'));
|
|
176
|
+
|
|
177
|
+
api.openapi(deps.workspaceExportRoute, async (c) => {
|
|
178
|
+
const principal = c.get('principal')!;
|
|
179
|
+
const workspaceId = principal.workspaceId;
|
|
180
|
+
|
|
181
|
+
const [instance] = await db.select().from(instanceSettings).limit(1);
|
|
182
|
+
const [{ liveFiles }] = (await db
|
|
183
|
+
.select({ liveFiles: sql<number>`count(*)::int` })
|
|
184
|
+
.from(files)
|
|
185
|
+
.where(and(eq(files.workspaceId, workspaceId), isNull(files.deletedAt)))) as [{ liveFiles: number }];
|
|
186
|
+
|
|
187
|
+
// The tool's entries (the `api.exportEntries` slot), computed and
|
|
188
|
+
// serialized BEFORE the audit row and before the first byte: a
|
|
189
|
+
// contribution that throws, or whose name collides, answers the standard
|
|
190
|
+
// 500 with no audit row (the middleware writes none on a 4xx/5xx, and the
|
|
191
|
+
// session row below is not reached) and never a truncated zip. They are
|
|
192
|
+
// held in memory until their place in the pump: bounded tables only, the
|
|
193
|
+
// same stance as the chassis sections of step 2. No slot: nothing runs.
|
|
194
|
+
const toolEntries = deps.exportEntries
|
|
195
|
+
? resolveExportEntries(await deps.exportEntries(db, workspaceId))
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
// Exactly one audit row either way: machine reads are audited by the
|
|
199
|
+
// middleware (it picks this up from c.set), session GETs are not
|
|
200
|
+
// (audit/service.ts) — so sessions write directly here.
|
|
201
|
+
const auditInfo = {
|
|
202
|
+
action: 'workspace.export',
|
|
203
|
+
resourceType: 'workspace',
|
|
204
|
+
resourceId: workspaceId,
|
|
205
|
+
metadata: { files: liveFiles }
|
|
206
|
+
};
|
|
207
|
+
c.set('audit', auditInfo);
|
|
208
|
+
if (principal.via === 'session') {
|
|
209
|
+
await audit.write({
|
|
210
|
+
workspaceId,
|
|
211
|
+
principal,
|
|
212
|
+
...auditInfo,
|
|
213
|
+
requestId: c.get('requestId'),
|
|
214
|
+
ip: clientIp(c)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const zip = new ZipFile();
|
|
219
|
+
// @types/yazl types outputStream as the legacy NodeJS.ReadableStream; at
|
|
220
|
+
// runtime it is a real Readable (a PassThrough), which destroy/toWeb need.
|
|
221
|
+
const output = zip.outputStream as Readable;
|
|
222
|
+
|
|
223
|
+
// A client abort must tear down the source stream and the zip output or
|
|
224
|
+
// every cancelled download leaks a descriptor/S3 socket (files.ts rule).
|
|
225
|
+
let currentBlobStream: Readable | null = null;
|
|
226
|
+
c.req.raw.signal.addEventListener('abort', () => {
|
|
227
|
+
currentBlobStream?.destroy();
|
|
228
|
+
output.destroy(new Error('client aborted the export download'));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Sequential pump, started before returning: entries are added in a
|
|
232
|
+
// fixed order while the output already streams. An error mid-pump
|
|
233
|
+
// destroys the output stream → the client sees a truncated (invalid)
|
|
234
|
+
// zip rather than a silently incomplete "valid" one — accepted: HTTP has
|
|
235
|
+
// no way to fail a 200 that already started streaming.
|
|
236
|
+
void (async () => {
|
|
237
|
+
// 1. Manifest first (no skip list here — it is not known yet; the
|
|
238
|
+
// skips land in skipped-blobs.json, written last).
|
|
239
|
+
zip.addBuffer(
|
|
240
|
+
jsonBuffer({
|
|
241
|
+
formatVersion: 1,
|
|
242
|
+
exportedAt: new Date().toISOString(),
|
|
243
|
+
instance: {
|
|
244
|
+
instanceId: instance?.instanceId ?? null,
|
|
245
|
+
name: instance?.name ?? null,
|
|
246
|
+
edition: env.EDITION,
|
|
247
|
+
version: env.APP_VERSION
|
|
248
|
+
},
|
|
249
|
+
workspaceId
|
|
250
|
+
}),
|
|
251
|
+
'manifest.json'
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// 2. The small bounded tables. Secrets are excluded by SELECTION, not
|
|
255
|
+
// by post-filtering: invitations.tokenHash and apiKeys.secretHash
|
|
256
|
+
// are simply never queried.
|
|
257
|
+
const [workspace] = await db
|
|
258
|
+
.select({ id: workspaces.id, name: workspaces.name, createdAt: workspaces.createdAt })
|
|
259
|
+
.from(workspaces)
|
|
260
|
+
.where(eq(workspaces.id, workspaceId))
|
|
261
|
+
.limit(1);
|
|
262
|
+
zip.addBuffer(jsonBuffer(workspace ?? null), 'workspace.json');
|
|
263
|
+
|
|
264
|
+
const memberRows = await db
|
|
265
|
+
.select({
|
|
266
|
+
id: workspaceMembers.id,
|
|
267
|
+
userId: workspaceMembers.userId,
|
|
268
|
+
email: userTable.email,
|
|
269
|
+
name: userTable.name,
|
|
270
|
+
role: workspaceMembers.role,
|
|
271
|
+
isActive: workspaceMembers.isActive,
|
|
272
|
+
createdAt: workspaceMembers.createdAt,
|
|
273
|
+
lastSeenAt: workspaceMembers.lastSeenAt
|
|
274
|
+
})
|
|
275
|
+
.from(workspaceMembers)
|
|
276
|
+
.innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
|
|
277
|
+
.where(eq(workspaceMembers.workspaceId, workspaceId))
|
|
278
|
+
.orderBy(asc(workspaceMembers.createdAt), asc(workspaceMembers.id));
|
|
279
|
+
zip.addBuffer(jsonBuffer(memberRows), 'members.json');
|
|
280
|
+
|
|
281
|
+
const invitationRows = await db
|
|
282
|
+
.select({
|
|
283
|
+
id: invitations.id,
|
|
284
|
+
email: invitations.email,
|
|
285
|
+
role: invitations.role,
|
|
286
|
+
invitedBy: invitations.invitedBy,
|
|
287
|
+
expiresAt: invitations.expiresAt,
|
|
288
|
+
acceptedAt: invitations.acceptedAt,
|
|
289
|
+
revokedAt: invitations.revokedAt,
|
|
290
|
+
createdAt: invitations.createdAt
|
|
291
|
+
})
|
|
292
|
+
.from(invitations)
|
|
293
|
+
.where(eq(invitations.workspaceId, workspaceId))
|
|
294
|
+
.orderBy(asc(invitations.createdAt), asc(invitations.id));
|
|
295
|
+
zip.addBuffer(jsonBuffer(invitationRows), 'invitations.json');
|
|
296
|
+
|
|
297
|
+
// PINNED keys only (workspace_id = this workspace): keys are USER
|
|
298
|
+
// credentials under the user-scoped model — an unpinned key belongs
|
|
299
|
+
// to its holder, not to any one workspace's export.
|
|
300
|
+
const apiKeyRows = await db
|
|
301
|
+
.select({
|
|
302
|
+
id: apiKeys.id,
|
|
303
|
+
keyId: apiKeys.keyId,
|
|
304
|
+
name: apiKeys.name,
|
|
305
|
+
scopes: apiKeys.scopes,
|
|
306
|
+
createdBy: apiKeys.createdBy,
|
|
307
|
+
createdAt: apiKeys.createdAt,
|
|
308
|
+
lastUsedAt: apiKeys.lastUsedAt,
|
|
309
|
+
revokedAt: apiKeys.revokedAt,
|
|
310
|
+
expiresAt: apiKeys.expiresAt
|
|
311
|
+
})
|
|
312
|
+
.from(apiKeys)
|
|
313
|
+
.where(eq(apiKeys.workspaceId, workspaceId))
|
|
314
|
+
.orderBy(asc(apiKeys.createdAt), asc(apiKeys.id));
|
|
315
|
+
zip.addBuffer(jsonBuffer(apiKeyRows), 'api-keys.json');
|
|
316
|
+
|
|
317
|
+
// Projects and their members, archived projects included (a project is
|
|
318
|
+
// never deleted). What a tool links to a project is the tool's entry.
|
|
319
|
+
const projectRows = await db
|
|
320
|
+
.select({
|
|
321
|
+
id: projects.id,
|
|
322
|
+
name: projects.name,
|
|
323
|
+
description: projects.description,
|
|
324
|
+
metadata: projects.metadata,
|
|
325
|
+
archivedAt: projects.archivedAt,
|
|
326
|
+
createdBy: projects.createdBy,
|
|
327
|
+
createdAt: projects.createdAt,
|
|
328
|
+
updatedAt: projects.updatedAt
|
|
329
|
+
})
|
|
330
|
+
.from(projects)
|
|
331
|
+
.where(eq(projects.workspaceId, workspaceId))
|
|
332
|
+
.orderBy(asc(projects.createdAt), asc(projects.id));
|
|
333
|
+
zip.addBuffer(jsonBuffer(projectRows), 'projects.json');
|
|
334
|
+
|
|
335
|
+
const projectMemberRows = await db
|
|
336
|
+
.select({
|
|
337
|
+
id: projectMembers.id,
|
|
338
|
+
projectId: projectMembers.projectId,
|
|
339
|
+
memberId: projectMembers.memberId,
|
|
340
|
+
userId: workspaceMembers.userId,
|
|
341
|
+
role: projectMembers.role,
|
|
342
|
+
addedBy: projectMembers.addedBy,
|
|
343
|
+
createdAt: projectMembers.createdAt
|
|
344
|
+
})
|
|
345
|
+
.from(projectMembers)
|
|
346
|
+
.innerJoin(projects, eq(projectMembers.projectId, projects.id))
|
|
347
|
+
.innerJoin(workspaceMembers, eq(projectMembers.memberId, workspaceMembers.id))
|
|
348
|
+
.where(eq(projects.workspaceId, workspaceId))
|
|
349
|
+
.orderBy(asc(projectMembers.createdAt), asc(projectMembers.id));
|
|
350
|
+
zip.addBuffer(jsonBuffer(projectMemberRows), 'project_members.json');
|
|
351
|
+
|
|
352
|
+
// The teams (PRDCT-2813): the tool's own and the hub's projections
|
|
353
|
+
// alike (`hubTeamId` tells them apart), their seats, and their places
|
|
354
|
+
// on projects (PRDCT-2794). The same three tables on both editions.
|
|
355
|
+
const teamRows = await db
|
|
356
|
+
.select({
|
|
357
|
+
id: workspaceTeams.id,
|
|
358
|
+
hubTeamId: workspaceTeams.hubTeamId,
|
|
359
|
+
slug: workspaceTeams.slug,
|
|
360
|
+
name: workspaceTeams.name,
|
|
361
|
+
createdBy: workspaceTeams.createdBy,
|
|
362
|
+
createdAt: workspaceTeams.createdAt,
|
|
363
|
+
updatedAt: workspaceTeams.updatedAt
|
|
364
|
+
})
|
|
365
|
+
.from(workspaceTeams)
|
|
366
|
+
.where(eq(workspaceTeams.workspaceId, workspaceId))
|
|
367
|
+
.orderBy(asc(workspaceTeams.createdAt), asc(workspaceTeams.id));
|
|
368
|
+
zip.addBuffer(jsonBuffer(teamRows), 'teams.json');
|
|
369
|
+
|
|
370
|
+
const teamMemberRows = await db
|
|
371
|
+
.select({
|
|
372
|
+
id: workspaceTeamMembers.id,
|
|
373
|
+
teamId: workspaceTeamMembers.teamId,
|
|
374
|
+
memberId: workspaceTeamMembers.memberId,
|
|
375
|
+
userId: workspaceMembers.userId,
|
|
376
|
+
addedBy: workspaceTeamMembers.addedBy,
|
|
377
|
+
createdAt: workspaceTeamMembers.createdAt
|
|
378
|
+
})
|
|
379
|
+
.from(workspaceTeamMembers)
|
|
380
|
+
.innerJoin(workspaceTeams, eq(workspaceTeamMembers.teamId, workspaceTeams.id))
|
|
381
|
+
.innerJoin(workspaceMembers, eq(workspaceTeamMembers.memberId, workspaceMembers.id))
|
|
382
|
+
.where(eq(workspaceTeams.workspaceId, workspaceId))
|
|
383
|
+
.orderBy(asc(workspaceTeamMembers.createdAt), asc(workspaceTeamMembers.id));
|
|
384
|
+
zip.addBuffer(jsonBuffer(teamMemberRows), 'team_members.json');
|
|
385
|
+
|
|
386
|
+
const projectTeamRows = await db
|
|
387
|
+
.select({
|
|
388
|
+
id: projectTeams.id,
|
|
389
|
+
projectId: projectTeams.projectId,
|
|
390
|
+
teamId: projectTeams.teamId,
|
|
391
|
+
role: projectTeams.role,
|
|
392
|
+
addedBy: projectTeams.addedBy,
|
|
393
|
+
createdAt: projectTeams.createdAt
|
|
394
|
+
})
|
|
395
|
+
.from(projectTeams)
|
|
396
|
+
.innerJoin(projects, eq(projectTeams.projectId, projects.id))
|
|
397
|
+
.where(eq(projects.workspaceId, workspaceId))
|
|
398
|
+
.orderBy(asc(projectTeams.createdAt), asc(projectTeams.id));
|
|
399
|
+
zip.addBuffer(jsonBuffer(projectTeamRows), 'project_teams.json');
|
|
400
|
+
|
|
401
|
+
// 3. Audit log as NDJSON, keyset-batched (this table is unbounded).
|
|
402
|
+
zip.addReadStream(Readable.from(auditNdjson(db, workspaceId)), 'audit-log.ndjson');
|
|
403
|
+
|
|
404
|
+
// 4. Files metadata — ALL rows including soft-deleted (deletedAt set;
|
|
405
|
+
// createdBy may be null after an account delete).
|
|
406
|
+
const fileRows = await db
|
|
407
|
+
.select()
|
|
408
|
+
.from(files)
|
|
409
|
+
.where(eq(files.workspaceId, workspaceId))
|
|
410
|
+
.orderBy(asc(files.createdAt), asc(files.id));
|
|
411
|
+
zip.addBuffer(
|
|
412
|
+
jsonBuffer(
|
|
413
|
+
fileRows.map((f) => ({
|
|
414
|
+
id: f.id,
|
|
415
|
+
sha256: f.sha256,
|
|
416
|
+
sizeBytes: f.sizeBytes,
|
|
417
|
+
contentType: f.contentType,
|
|
418
|
+
originalName: f.originalName,
|
|
419
|
+
createdBy: f.createdBy,
|
|
420
|
+
createdAt: f.createdAt.toISOString(),
|
|
421
|
+
deletedAt: f.deletedAt?.toISOString() ?? null
|
|
422
|
+
}))
|
|
423
|
+
),
|
|
424
|
+
'files.json'
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// 4b. The tool's entries, in the tool's order (none without the slot).
|
|
428
|
+
for (const entry of toolEntries) zip.addBuffer(entry.buffer, entry.entryName);
|
|
429
|
+
|
|
430
|
+
// 5. Blobs, strictly one at a time: await each stream's end before
|
|
431
|
+
// opening the next so file descriptors / S3 sockets never pile up
|
|
432
|
+
// (yazl keeps accepting entries while piping until end()).
|
|
433
|
+
const skipped: Array<{ fileId: string; sha256: string; reason: string }> = [];
|
|
434
|
+
for (const f of fileRows) {
|
|
435
|
+
// Destroying yazl's output does NOT stop the pump: the ZipFile only
|
|
436
|
+
// latches errors emitted on itself, so entries added after an abort
|
|
437
|
+
// are still opened and fully drained. Check the signal explicitly or
|
|
438
|
+
// a cancelled multi-GB export keeps consuming disk/S3 bandwidth.
|
|
439
|
+
if (c.req.raw.signal.aborted) throw new Error('client aborted the export download');
|
|
440
|
+
if (f.deletedAt) {
|
|
441
|
+
skipped.push({ fileId: f.id, sha256: f.sha256, reason: 'soft-deleted' });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const key = blobKey(f.workspaceId, f.sha256);
|
|
445
|
+
if (!(await storage.exists(key))) {
|
|
446
|
+
skipped.push({ fileId: f.id, sha256: f.sha256, reason: 'blob-missing' });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const stream = await storage.getStream(key);
|
|
450
|
+
currentBlobStream = stream;
|
|
451
|
+
// compress: false — media/binary blobs don't deflate; JSON entries
|
|
452
|
+
// above keep yazl's default compression.
|
|
453
|
+
zip.addReadStream(stream, `files/${f.id}-${sanitizeEntryName(f.originalName)}`, {
|
|
454
|
+
compress: false
|
|
455
|
+
});
|
|
456
|
+
await finished(stream);
|
|
457
|
+
currentBlobStream = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 6. The skip list last (this is why the manifest carries none).
|
|
461
|
+
zip.addBuffer(jsonBuffer(skipped), 'skipped-blobs.json');
|
|
462
|
+
zip.end();
|
|
463
|
+
})().catch((err: unknown) => {
|
|
464
|
+
// A client cancel is routine, not an operational error.
|
|
465
|
+
if (c.req.raw.signal.aborted) {
|
|
466
|
+
logger.info({ workspaceId }, 'workspace export cancelled by the client');
|
|
467
|
+
} else {
|
|
468
|
+
logger.error({ err, workspaceId }, 'workspace export pump failed — download truncated');
|
|
469
|
+
}
|
|
470
|
+
currentBlobStream?.destroy();
|
|
471
|
+
output.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
475
|
+
return c.body(Readable.toWeb(output) as unknown as ReadableStream, 200, {
|
|
476
|
+
'content-type': 'application/zip',
|
|
477
|
+
'content-disposition': `attachment; filename="export-${instance?.instanceId ?? 'instance'}-${stamp}.zip"`,
|
|
478
|
+
'x-content-type-options': 'nosniff',
|
|
479
|
+
'cache-control': 'no-store'
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
package/src/api/files.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import type { Context } from 'hono';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import type { ReadableStream as WebReadableStream } from 'node:stream/web';
|
|
5
|
+
import type { SQL } from 'drizzle-orm';
|
|
6
|
+
import { fileGetRoute, filesListRoute, fileUploadRoute } from '@antasphere/chassis-contract/routes';
|
|
7
|
+
import type { Principal } from '@antasphere/chassis-contract';
|
|
8
|
+
import type { DbConn, FileRow } from '@antasphere/chassis-db';
|
|
9
|
+
import type { Env } from '../env.js';
|
|
10
|
+
import type { ScopeRoutes } from './scope-routes.js';
|
|
11
|
+
import type { Logger } from '../logger.js';
|
|
12
|
+
import type { PlatformRegistry } from '../platform/registry.js';
|
|
13
|
+
import type { FileService } from '../files/service.js';
|
|
14
|
+
import { FileTooLargeError } from '../files/spool.js';
|
|
15
|
+
import { serveBlob } from '../files/serve.js';
|
|
16
|
+
import type { StorageDriver } from '../storage/driver.js';
|
|
17
|
+
import { isUuid } from '../pagination.js';
|
|
18
|
+
import { requireAuth, requireNonGuest } from '../middleware/auth-context.js';
|
|
19
|
+
|
|
20
|
+
const err = (code: string, message: string) => ({ error: { code, message } });
|
|
21
|
+
|
|
22
|
+
const toWire = (f: FileRow) => ({
|
|
23
|
+
id: f.id,
|
|
24
|
+
sha256: f.sha256,
|
|
25
|
+
sizeBytes: f.sizeBytes,
|
|
26
|
+
contentType: f.contentType,
|
|
27
|
+
originalName: f.originalName,
|
|
28
|
+
createdBy: f.createdBy,
|
|
29
|
+
createdAt: f.createdAt.toISOString()
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export interface FileRouteDeps {
|
|
33
|
+
service: FileService;
|
|
34
|
+
storage: StorageDriver;
|
|
35
|
+
registry: PlatformRegistry;
|
|
36
|
+
env: Pick<Env, 'MAX_FILE_SIZE_MB'>;
|
|
37
|
+
logger: Logger;
|
|
38
|
+
/**
|
|
39
|
+
* ADR 011 blob-delete guard: true when the blob is referenced by a live
|
|
40
|
+
* version manifest of the tool's resource — DELETE answers 409 file_in_use instead
|
|
41
|
+
* of removing it. Runs inside the delete transaction (files module stays
|
|
42
|
+
* tool-agnostic; the wiring point injects the tool's check).
|
|
43
|
+
*/
|
|
44
|
+
blobInUse: (tx: DbConn, workspaceId: string, sha256: string) => Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* ADR 013 per-blob read scope (SL-B1): a WHERE predicate over the `files`
|
|
47
|
+
* row, or `undefined` for the workspace admin/owner operator view. Injected
|
|
48
|
+
* for the same reason as `blobInUse` — the files module stays
|
|
49
|
+
* tool-agnostic and the wiring point supplies the tool's policy.
|
|
50
|
+
*/
|
|
51
|
+
blobReadScope: (principal: Principal) => SQL | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The delete route the tool instantiated (its OpenAPI 409 carries the tool's
|
|
54
|
+
* wording), and the tool's message of that 409 `file_in_use`
|
|
55
|
+
* (`copy.fileInUse`): what references a blob is the tool's to name.
|
|
56
|
+
*/
|
|
57
|
+
fileDeleteRoute: ScopeRoutes['fileDeleteRoute'];
|
|
58
|
+
fileInUse: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function registerFileRoutes(api: OpenAPIHono, deps: FileRouteDeps): void {
|
|
62
|
+
const { service, storage, registry, env, logger } = deps;
|
|
63
|
+
const maxBytes = env.MAX_FILE_SIZE_MB * 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
api.use('/files', requireAuth());
|
|
66
|
+
api.use('/files/*', requireAuth());
|
|
67
|
+
// Guest capability limit (D2, both editions): the generic files surface is
|
|
68
|
+
// a WORKSPACE-level surface (ADR 006) — an inventory of the tenant's blobs
|
|
69
|
+
// with a delete on it. Its READS are now per-resource authorized like every
|
|
70
|
+
// other content read (SL-B1: `deps.blobReadScope`, ADR 013), but the
|
|
71
|
+
// surface as a whole still belongs to the workspace, and an external guest
|
|
72
|
+
// has no business there: they were invited to ONE resource, not to the host
|
|
73
|
+
// tenant's file cabinet. Guests push and pull its bytes through the ADR
|
|
74
|
+
// 013-gated routes of the tool instead (its asset upload and its
|
|
75
|
+
// per-resource asset download).
|
|
76
|
+
api.use('/files', requireNonGuest());
|
|
77
|
+
api.use('/files/*', requireNonGuest());
|
|
78
|
+
|
|
79
|
+
api.openapi(filesListRoute, async (c) => {
|
|
80
|
+
const principal = c.get('principal')!;
|
|
81
|
+
const { cursor, limit } = c.req.valid('query');
|
|
82
|
+
const { files: rows, nextCursor } = await service.list(principal.workspaceId, {
|
|
83
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
84
|
+
limit,
|
|
85
|
+
// ADR 013 (SL-B1): a plain member pages the blobs they uploaded plus
|
|
86
|
+
// those referenced by resources they can read; admins/owners keep the
|
|
87
|
+
// whole-workspace operator view.
|
|
88
|
+
visibility: deps.blobReadScope(principal)
|
|
89
|
+
});
|
|
90
|
+
return c.json({ files: rows.map(toWire), nextCursor }, 200);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
api.openapi(fileUploadRoute, async (c) => {
|
|
94
|
+
const principal = c.get('principal')!;
|
|
95
|
+
const { name } = c.req.valid('query');
|
|
96
|
+
const contentType = c.req.header('content-type') ?? 'application/octet-stream';
|
|
97
|
+
|
|
98
|
+
// The declared-size check and the usage event are the entitlement gate's
|
|
99
|
+
// (the billing rail, §7): a tool declares `files.upload` on this route in
|
|
100
|
+
// its contract and the chassis gate runs it before this handler; the hard
|
|
101
|
+
// cap is still enforced mid-stream below. Nothing is checked or emitted
|
|
102
|
+
// by hand here.
|
|
103
|
+
if (!c.req.raw.body) {
|
|
104
|
+
return c.json(err('empty_body', 'Request body required'), 400);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { file, deduplicated } = await service.upload({
|
|
109
|
+
workspaceId: principal.workspaceId,
|
|
110
|
+
createdBy: principal.userId,
|
|
111
|
+
originalName: name,
|
|
112
|
+
contentType,
|
|
113
|
+
body: Readable.fromWeb(c.req.raw.body as WebReadableStream),
|
|
114
|
+
maxBytes
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
c.set('audit', {
|
|
118
|
+
action: 'file.upload',
|
|
119
|
+
resourceType: 'file',
|
|
120
|
+
resourceId: file.id,
|
|
121
|
+
metadata: { name, sizeBytes: file.sizeBytes, deduplicated }
|
|
122
|
+
});
|
|
123
|
+
registry.events.emit('file.uploaded', {
|
|
124
|
+
workspaceId: principal.workspaceId,
|
|
125
|
+
fileId: file.id,
|
|
126
|
+
sizeBytes: file.sizeBytes
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return c.json({ file: toWire(file), deduplicated }, 201);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (e instanceof FileTooLargeError) {
|
|
132
|
+
return c.json(err('file_too_large', e.message), 413);
|
|
133
|
+
}
|
|
134
|
+
logger.error({ err: e }, 'file upload failed');
|
|
135
|
+
return c.json(err('upload_failed', 'Upload failed'), 400);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
api.openapi(fileGetRoute, async (c) => {
|
|
140
|
+
const principal = c.get('principal')!;
|
|
141
|
+
const { id } = c.req.valid('param');
|
|
142
|
+
// 404, never 403, when the scope excludes the blob (ADR 013): a blob a
|
|
143
|
+
// principal cannot read must not be probeable by id either.
|
|
144
|
+
const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
|
|
145
|
+
if (!file) return c.json(err('not_found', 'File not found'), 404);
|
|
146
|
+
return c.json(toWire(file), 200);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
api.openapi(deps.fileDeleteRoute, async (c) => {
|
|
150
|
+
const principal = c.get('principal')!;
|
|
151
|
+
const { id } = c.req.valid('param');
|
|
152
|
+
// Same scope on the mutation: a member must not be able to delete (or
|
|
153
|
+
// probe) a blob they may not read.
|
|
154
|
+
const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
|
|
155
|
+
if (!file) return c.json(err('not_found', 'File not found'), 404);
|
|
156
|
+
const outcome = await service.delete(file, (tx, row) => deps.blobInUse(tx, row.workspaceId, row.sha256));
|
|
157
|
+
if (outcome === 'in_use') {
|
|
158
|
+
return c.json(err('file_in_use', deps.fileInUse), 409);
|
|
159
|
+
}
|
|
160
|
+
c.set('audit', { action: 'file.delete', resourceType: 'file', resourceId: file.id });
|
|
161
|
+
return c.json(toWire(file), 200);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── Content: streamed, Range-capable. Registered as plain Hono routes (a
|
|
165
|
+
// byte endpoint, not part of the JSON OpenAPI surface); guarded by the
|
|
166
|
+
// same requireAuth + scope machinery as everything else under /files.
|
|
167
|
+
// The streaming machinery itself (ETag/304/Range/safe-serving) is shared
|
|
168
|
+
// with the tool's asset download — files/serve.ts.
|
|
169
|
+
const contentHandler = async (c: Context, headOnly: boolean) => {
|
|
170
|
+
const principal = c.get('principal')!;
|
|
171
|
+
const id = c.req.param('id') ?? '';
|
|
172
|
+
// Plain Hono route (no contract param schema): reject a non-UUID id here
|
|
173
|
+
// or it reaches Postgres' uuid cast and surfaces as a sanitized 500.
|
|
174
|
+
if (!isUuid(id)) return c.json(err('not_found', 'File not found'), 404);
|
|
175
|
+
// The byte route carries the ADR 013 scope too (SL-B1) — this was the
|
|
176
|
+
// whole-tenant read: `workspace_id` alone served ANY resource's content to
|
|
177
|
+
// any member and to any read-scope key.
|
|
178
|
+
const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
|
|
179
|
+
if (!file) return c.json(err('not_found', 'File not found'), 404);
|
|
180
|
+
|
|
181
|
+
return serveBlob(c, {
|
|
182
|
+
storage,
|
|
183
|
+
logger,
|
|
184
|
+
workspaceId: file.workspaceId,
|
|
185
|
+
sha256: file.sha256,
|
|
186
|
+
sizeBytes: file.sizeBytes,
|
|
187
|
+
contentType: file.contentType,
|
|
188
|
+
filename: file.originalName,
|
|
189
|
+
headOnly
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ONE registration for both verbs. Hono rewrites HEAD to GET before routing
|
|
194
|
+
// (verified against the pinned version: a HEAD request runs the GET handler
|
|
195
|
+
// and the HEAD registration never fires), so a separate `api.on('HEAD', …)`
|
|
196
|
+
// is dead code and every HEAD would silently stream the whole body. Read
|
|
197
|
+
// the verb off the request instead — that is the only signal that survives
|
|
198
|
+
// the rewrite. Never add an `api.on('HEAD', …)` route on this app.
|
|
199
|
+
api.get('/files/:id/content', (c) => contentHandler(c, c.req.method === 'HEAD'));
|
|
200
|
+
}
|