@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,1165 @@
|
|
|
1
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import { matchedRoutes } from 'hono/route';
|
|
3
|
+
import { createEmailVerificationToken } from 'better-auth/api';
|
|
4
|
+
import { jwtVerify } from 'jose';
|
|
5
|
+
import { registerOpenApiDoc } from './index.js';
|
|
6
|
+
import { ulid } from 'ulid';
|
|
7
|
+
import { and, asc, desc, eq } from 'drizzle-orm';
|
|
8
|
+
import { ACTIVE_WORKSPACE_HEADER } from '@antasphere/chassis-contract';
|
|
9
|
+
import { instanceRoute, setupRoute } from '@antasphere/chassis-contract/routes';
|
|
10
|
+
import { account, instanceSettings, user as userTable, userOnboarding, workspaceMembers, workspaces } from '@antasphere/chassis-db';
|
|
11
|
+
import { hubConfig } from '../env.js';
|
|
12
|
+
import { DEMO_SESSION_REFUSED_API_ROUTES, demoSessionApiRefusalMessage, demoSessionAuthRefusal } from '../identity/index.js';
|
|
13
|
+
import { DemoPassService } from '../identity/demo-pass.js';
|
|
14
|
+
import { auditMiddleware } from '../audit/index.js';
|
|
15
|
+
import { isDeferringGate, registerEntitlementGate } from '../entitlements/index.js';
|
|
16
|
+
import { constantTimeEquals } from '../util/index.js';
|
|
17
|
+
import { isSecureSetupOrigin } from '../util/index.js';
|
|
18
|
+
import { authBodyGuard } from '../middleware/index.js';
|
|
19
|
+
import { admitPrincipal, authContext, resolveSessionPrincipal } from '../middleware/index.js';
|
|
20
|
+
import { idempotency } from '../middleware/index.js';
|
|
21
|
+
import { crossSiteGuard } from '../middleware/index.js';
|
|
22
|
+
import { jsonDepthLimit } from '../middleware/index.js';
|
|
23
|
+
import { deferrableBodyCap } from '../middleware/body-cap.js';
|
|
24
|
+
import { noStoreAuthenticated } from '../middleware/index.js';
|
|
25
|
+
import { oauthPublicEndpoints } from '../middleware/index.js';
|
|
26
|
+
import { createRequestQuota, emailKeyOf, makeClientIp, rateLimit } from '../middleware/index.js';
|
|
27
|
+
import { HUB_SSO_PROVIDER_ID } from '../identity/index.js';
|
|
28
|
+
import { registerBreakGlassRoutes } from './index.js';
|
|
29
|
+
import { registerCliAuthRoutes } from './index.js';
|
|
30
|
+
import { registerOnboardingRoutes } from './index.js';
|
|
31
|
+
import { registerWorkspaceRoutes, workspaceCreationPolicy, workspaceCreationRefusal, workspaceCreateWallClosed } from './index.js';
|
|
32
|
+
import { registerSsoConnectRoutes } from './index.js';
|
|
33
|
+
import { registerSsoLogoutRoutes } from './index.js';
|
|
34
|
+
import { registerMemberRoutes } from './index.js';
|
|
35
|
+
import { demoSessionJudge, demoSignInOn, registerDemoPassRoutes } from './index.js';
|
|
36
|
+
import { registerProjectRoutes } from './index.js';
|
|
37
|
+
import { registerTeamRoutes } from './index.js';
|
|
38
|
+
import { registerApiKeyRoutes } from './index.js';
|
|
39
|
+
import { registerInvitationRoutes } from './index.js';
|
|
40
|
+
import { registerAuditRoutes } from './index.js';
|
|
41
|
+
import { registerFileRoutes } from './index.js';
|
|
42
|
+
import { registerExportRoutes } from './index.js';
|
|
43
|
+
/** Inline error body matching the wire shape; keeps openapi handlers typed. */
|
|
44
|
+
const err = (code, message) => ({ error: { code, message } });
|
|
45
|
+
/** Where the sign-in library is mounted: the full path a middleware on `api` sees starts with it. */
|
|
46
|
+
const AUTH_MOUNT = '/api/v1/auth';
|
|
47
|
+
/**
|
|
48
|
+
* PRDCT-1437 door 1: neutralize the change-email consume's account-existence
|
|
49
|
+
* oracle by rewriting the REQUEST before Better Auth reads the token.
|
|
50
|
+
*
|
|
51
|
+
* The consume of a `change-email-verification` link updates the target's email
|
|
52
|
+
* to the new address; when that address already belongs to ANOTHER user the
|
|
53
|
+
* update dies at the `user.email` UNIQUE constraint as a raw 500, while a free
|
|
54
|
+
* address answers a 302 — a mint-then-consume "does this email exist anywhere
|
|
55
|
+
* on the instance" for any workspace owner, and a robustness bug. Rather than
|
|
56
|
+
* reimplement the consume (LESSONS.md M6: never hand-roll the Better Auth
|
|
57
|
+
* flow), on a collision we re-sign the token so its target is the member's
|
|
58
|
+
* CURRENT address — a no-op change that runs Better Auth's ENTIRE success path
|
|
59
|
+
* (its own `callbackURL` origin check, the session mint, the set-cookie, the
|
|
60
|
+
* 302-or-JSON, the audit hook) with no unique violation. Redirect, cookie, and
|
|
61
|
+
* origin-check verdict are then byte-identical to the free case BY
|
|
62
|
+
* CONSTRUCTION, and no `callbackURL` handling is added here, so no open-redirect
|
|
63
|
+
* surface is introduced (the route's own originCheck stays the only judge).
|
|
64
|
+
*
|
|
65
|
+
* The one residual tell — the success JSON echoes the resulting email in its
|
|
66
|
+
* `user` record — is reachable only WITHOUT a `callbackURL` (a real consume
|
|
67
|
+
* always carries one and redirects) and is stripped to `user:null` by the
|
|
68
|
+
* wrapper below. The honest, inherent residual (email uniqueness is
|
|
69
|
+
* instance-global, so the change's outcome is readable one step later on the
|
|
70
|
+
* roster) is documented in ADR 013's amendment. It runs in a before-hook only
|
|
71
|
+
* as a query mutation, which the endpoint's own parsed query never sees, so it
|
|
72
|
+
* MUST rewrite the request here. Re-verify the token shape and the
|
|
73
|
+
* `change-email-verification` requestType on ANY Better Auth bump.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Constrain a verify-email redirect Location to the instance's own origin
|
|
77
|
+
* (Major 3, PRDCT-1437 hardening). DEFENCE-IN-DEPTH, not a hole being closed:
|
|
78
|
+
* on the pinned Better Auth, `/verify-email` DOES run its own callbackURL
|
|
79
|
+
* origin check in production (the `originCheck` factory has no GET skip; only
|
|
80
|
+
* `originCheckMiddleware` does) — but the test harness disables that guard
|
|
81
|
+
* (`NODE_ENV=test` sets `skipOriginCheck`), and a future bump could regress
|
|
82
|
+
* it, so we constrain the redirect ourselves and the no-op collision rewrite
|
|
83
|
+
* routes the taken branch through the same constraint. A same-origin (or
|
|
84
|
+
* public-origin) target is returned
|
|
85
|
+
* as a relative path; anything off-origin is neutralized to `/`. Legitimate
|
|
86
|
+
* consumes send a relative `callbackURL` and are untouched; applied to every
|
|
87
|
+
* response the wrapper sees, so the taken and free branches stay identical.
|
|
88
|
+
*/
|
|
89
|
+
function safeRedirectLocation(location, requestUrl, publicBaseUrl) {
|
|
90
|
+
let resolved;
|
|
91
|
+
let servingOrigin;
|
|
92
|
+
try {
|
|
93
|
+
servingOrigin = new URL(requestUrl).origin;
|
|
94
|
+
resolved = new URL(location, requestUrl);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return '/';
|
|
98
|
+
}
|
|
99
|
+
let publicOrigin = null;
|
|
100
|
+
try {
|
|
101
|
+
publicOrigin = new URL(publicBaseUrl).origin;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
publicOrigin = null;
|
|
105
|
+
}
|
|
106
|
+
if (resolved.origin === servingOrigin || resolved.origin === publicOrigin) {
|
|
107
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
108
|
+
}
|
|
109
|
+
return '/';
|
|
110
|
+
}
|
|
111
|
+
async function rewriteChangeEmailCollision(auth, authSecret, request) {
|
|
112
|
+
const url = new URL(request.url);
|
|
113
|
+
const token = url.searchParams.get('token');
|
|
114
|
+
if (!token)
|
|
115
|
+
return request;
|
|
116
|
+
let payload;
|
|
117
|
+
try {
|
|
118
|
+
const verified = await jwtVerify(token, new TextEncoder().encode(authSecret), {
|
|
119
|
+
algorithms: ['HS256']
|
|
120
|
+
});
|
|
121
|
+
payload = verified.payload;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return request; // a bad token is Better Auth's to reject (uniform error redirect)
|
|
125
|
+
}
|
|
126
|
+
const email = payload.email;
|
|
127
|
+
const updateTo = payload.updateTo;
|
|
128
|
+
if (payload.requestType !== 'change-email-verification' ||
|
|
129
|
+
typeof email !== 'string' ||
|
|
130
|
+
typeof updateTo !== 'string' ||
|
|
131
|
+
email === updateTo) {
|
|
132
|
+
return request;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const authCtx = await auth.$context;
|
|
136
|
+
const target = await authCtx.internalAdapter.findUserByEmail(email);
|
|
137
|
+
if (!target)
|
|
138
|
+
return request; // user_not_found — uniform on both branches
|
|
139
|
+
// A signed-in NON-target redirectOnErrors identically on both branches, so
|
|
140
|
+
// only the state that actually reaches the update needs neutralizing.
|
|
141
|
+
const session = await auth.api.getSession({ headers: request.headers }).catch(() => null);
|
|
142
|
+
if (session && session.user.email !== email)
|
|
143
|
+
return request;
|
|
144
|
+
const holder = await authCtx.internalAdapter.findUserByEmail(updateTo);
|
|
145
|
+
if (!holder || holder.user.id === target.user.id)
|
|
146
|
+
return request; // no collision
|
|
147
|
+
// Collision: re-sign as a no-op (change email → same email). Better Auth
|
|
148
|
+
// then updates the row to its own value (no 23505) and runs the full
|
|
149
|
+
// success path off the rewritten request.
|
|
150
|
+
const noopToken = await createEmailVerificationToken(authCtx.secret, email, email, 3600, {
|
|
151
|
+
requestType: 'change-email-verification'
|
|
152
|
+
});
|
|
153
|
+
url.searchParams.set('token', noopToken);
|
|
154
|
+
return new Request(url.toString(), request);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// A lookup failure must not turn into a distinguishing error — let Better
|
|
158
|
+
// Auth answer, and the wrapper's 5xx→uniform mapping backstops it.
|
|
159
|
+
return request;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Control-flow marker: the singleton claim lost (instance already set up). */
|
|
163
|
+
class SetupAlreadyDone extends Error {
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The versioned API, mounted at /api/v1. Every route is defined by a contract
|
|
167
|
+
* in the tool's contract package (`<tool contract>/routes`); the OpenAPI document is generated from those
|
|
168
|
+
* contracts and served at /api/v1/openapi.json.
|
|
169
|
+
*/
|
|
170
|
+
export function createApiApp(deps) {
|
|
171
|
+
const { db, env, auth, registry, logger, apiKeys: apiKeyService, audit, email, limiters, tool } = deps;
|
|
172
|
+
const { meRoute, apiKeysListRoute, apiKeyCreateRoute, apiKeyRevokeRoute, cliAuthCompleteRoute } = tool.scopes.contractRoutes;
|
|
173
|
+
const { ssoCliConnectRoute } = tool.scopes.contractRoutes;
|
|
174
|
+
// The registry as the chassis routers read it (the generic events only):
|
|
175
|
+
// the same seams, the same bus, seen through its chassis view.
|
|
176
|
+
const chassisRegistry = { ...registry, events: registry.events.chassisView() };
|
|
177
|
+
// What every positional hook of the tool receives.
|
|
178
|
+
const hookContext = {
|
|
179
|
+
db,
|
|
180
|
+
env,
|
|
181
|
+
auth,
|
|
182
|
+
registry,
|
|
183
|
+
logger,
|
|
184
|
+
audit,
|
|
185
|
+
email,
|
|
186
|
+
limiters,
|
|
187
|
+
storage: deps.storage,
|
|
188
|
+
fileService: deps.fileService,
|
|
189
|
+
authSecret: deps.authSecret,
|
|
190
|
+
hubSso: deps.hubSso
|
|
191
|
+
};
|
|
192
|
+
// P7 (internal/federation.md): on EDITION=cloud, membership of a hub-origin
|
|
193
|
+
// (projected) workspace is managed at the hub — this is the pointer the
|
|
194
|
+
// membership-mutation gates and /me carry. Same single `hubConfig` switch
|
|
195
|
+
// as every cloud seam: null on oss, so oss wires no gate and /me answers
|
|
196
|
+
// hubManageUrl null.
|
|
197
|
+
const hub = hubConfig(env);
|
|
198
|
+
const hubManaged = hub ? { manageUrl: hub.issuerUrl } : undefined;
|
|
199
|
+
// Demo sign-in (the demo pass spec): DEMO_SIGN_IN on the self-hosted
|
|
200
|
+
// edition only. Off, no demo route, wall or discovery key exists.
|
|
201
|
+
const demoSignIn = demoSignInOn(env, hub, logger);
|
|
202
|
+
const api = new OpenAPIHono({
|
|
203
|
+
// Validation failures use the same wire shape as every other error.
|
|
204
|
+
defaultHook: (result, c) => {
|
|
205
|
+
if (!result.success) {
|
|
206
|
+
return c.json({
|
|
207
|
+
error: {
|
|
208
|
+
code: 'validation_error',
|
|
209
|
+
message: 'Request validation failed',
|
|
210
|
+
details: result.error.issues
|
|
211
|
+
}
|
|
212
|
+
}, 400);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// NOTE (release-gate fix): malformed/empty JSON bodies used to 500 here —
|
|
217
|
+
// hono's json validator throws HTTPException(400) BEFORE zod runs, so the
|
|
218
|
+
// defaultHook above never fires. Hono routes thrown errors straight to the
|
|
219
|
+
// top-level errorHandler from the throwing frame (an upstream middleware
|
|
220
|
+
// try/catch never sees them), so the mapping to the 400 wire shape lives
|
|
221
|
+
// in app.ts app.onError — the one seam that covers every body route.
|
|
222
|
+
// ── Public OAuth endpoints first: wildcard CORS + OPTIONS 204 + no-store
|
|
223
|
+
// on token responses. Before the rate limits so preflights cost nothing.
|
|
224
|
+
api.use('/auth/*', oauthPublicEndpoints());
|
|
225
|
+
// ── Cross-site (CSRF) gate, before ANY other work: an unsafe method driven
|
|
226
|
+
// from another origin is refused before a body is buffered, before a
|
|
227
|
+
// limiter bucket is touched, and before a credential resolves. Sessions are
|
|
228
|
+
// ambient credentials — every business route below (the tool's resources,
|
|
229
|
+
// its tokens and collaborators, api-keys) would otherwise be drivable
|
|
230
|
+
// from any page the user happens to have open.
|
|
231
|
+
//
|
|
232
|
+
// SCOPED TO /api/v1 ON PURPOSE. The public viewer (`/v/:secret`) is mounted
|
|
233
|
+
// on the root app and is DELIBERATELY embeddable cross-origin (ADR 021 +
|
|
234
|
+
// /embed.js), including its password-form POST — mounting this guard there
|
|
235
|
+
// would break every legitimate embed. The viewer authenticates on the share
|
|
236
|
+
// secret and its own scoped cookie, not on the dashboard session, so it is
|
|
237
|
+
// not the ambient-credential surface this closes.
|
|
238
|
+
api.use('*', crossSiteGuard({
|
|
239
|
+
publicBaseUrl: env.PUBLIC_BASE_URL,
|
|
240
|
+
// The tool's never-trusted origins (e.g. a viewer origin, PRDCT-1352:
|
|
241
|
+
// the origin author-controlled script runs on): never a trust
|
|
242
|
+
// grant here, even if it is the serving origin.
|
|
243
|
+
deniedOrigins: deps.untrustedOrigins,
|
|
244
|
+
// `/api/v1/viewer/*` is the share-token annotation API, and it is a
|
|
245
|
+
// DELIBERATE wildcard-CORS surface (viewer/annotations-api.ts): it is
|
|
246
|
+
// called by the overlay client running inside the sandboxed viewer
|
|
247
|
+
// iframe, whose origin is the opaque `null`. Nothing there is
|
|
248
|
+
// cookie-authenticated — the share token in the path is the credential —
|
|
249
|
+
// so it is not the ambient-credential class this guard closes, and
|
|
250
|
+
// refusing `Origin: null` would break annotations outright.
|
|
251
|
+
isExempt: (path) => tool.api.csrfExempt?.(path) ?? false
|
|
252
|
+
}));
|
|
253
|
+
// ── Authenticated responses are never cached. Registered ABOVE the
|
|
254
|
+
// credential middleware so it wraps every route under /api/v1, and it reads
|
|
255
|
+
// the principal AFTER the inner chain has resolved it. Routes that set their
|
|
256
|
+
// own Cache-Control (public discovery, the OpenAPI document) keep it.
|
|
257
|
+
api.use('*', noStoreAuthenticated());
|
|
258
|
+
// The public viewer-token annotation surface (Phase 5): same posture — the
|
|
259
|
+
// overlay calls cross-origin from the sandboxed opaque origin (Origin:
|
|
260
|
+
// null), token-authed, never cookie-authed, so wildcard CORS is safe.
|
|
261
|
+
tool.api.early?.(api, hookContext);
|
|
262
|
+
// ── Body size caps, path-routed. 1 MiB is generous for every JSON/auth
|
|
263
|
+
// body. Exceptions:
|
|
264
|
+
// - /files: streamed uploads with their own mid-stream cap
|
|
265
|
+
// (MAX_FILE_SIZE_MB) plus a Content-Length entitlement check;
|
|
266
|
+
// - the tool's multipart asset uploads (its `bodyLimit` slot) — capped at
|
|
267
|
+
// MAX_FILE_SIZE_MB (+1 MiB multipart framing headroom) so an unbounded
|
|
268
|
+
// body can never balloon the buffering parse;
|
|
269
|
+
// - the rest of the tool's routes, which are JSON, but commit manifests are legal up to
|
|
270
|
+
// 5000 entries × 1 KiB paths — a 16 MiB cap fits any contract-valid
|
|
271
|
+
// manifest while still bounding abuse.
|
|
272
|
+
const jsonBodyCap = {
|
|
273
|
+
maxBytes: 1024 * 1024,
|
|
274
|
+
onError: (c) => c.json(err('payload_too_large', 'Request body exceeds the 1 MiB limit'), 413)
|
|
275
|
+
};
|
|
276
|
+
// The tool's own caps, declared ONCE (the slot's factory runs here, never
|
|
277
|
+
// per request) and built into middleware per cap object below.
|
|
278
|
+
const toolBodyLimit = tool.api.bodyLimit?.(hookContext);
|
|
279
|
+
// - the viewer's form file upload (PRDCT-2403): one raw streamed file per
|
|
280
|
+
// request, capped MID-STREAM by the form-upload ceiling in its handler.
|
|
281
|
+
//
|
|
282
|
+
// A declared Content-Length over a cap on a route whose gate judges a
|
|
283
|
+
// PLAN LIMIT is not refused here (PRDCT-2632): the entitlement gate must
|
|
284
|
+
// see the declared size first, so a metered account meets 403
|
|
285
|
+
// `plan_required` with its upgrade link at any size and the cap's 413
|
|
286
|
+
// answers only when the plan allows the size. `deferrableBodyCap`
|
|
287
|
+
// (middleware/body-cap.ts) parks the refusal on the context and DROPS the
|
|
288
|
+
// request's body, keyed on the gate itself being on the matched route;
|
|
289
|
+
// the gate fires it. Every cap goes through it, the tool's and the
|
|
290
|
+
// default JSON one alike, so a future declared-limit route left under
|
|
291
|
+
// the default cap is never shadowed either. An undeclared body is still
|
|
292
|
+
// counted and cut by the cap.
|
|
293
|
+
const deferring = (c) => matchedRoutes(c).some((route) => isDeferringGate(route.handler));
|
|
294
|
+
const capMiddleware = new WeakMap();
|
|
295
|
+
const capped = (cap) => {
|
|
296
|
+
let built = capMiddleware.get(cap);
|
|
297
|
+
if (!built) {
|
|
298
|
+
built = deferrableBodyCap(cap, deferring);
|
|
299
|
+
capMiddleware.set(cap, built);
|
|
300
|
+
}
|
|
301
|
+
return built;
|
|
302
|
+
};
|
|
303
|
+
api.use('*', (c, next) => {
|
|
304
|
+
const path = c.req.path;
|
|
305
|
+
if (path.startsWith('/api/v1/files'))
|
|
306
|
+
return next();
|
|
307
|
+
const verdict = toolBodyLimit?.(path);
|
|
308
|
+
if (verdict === 'exempt')
|
|
309
|
+
return next();
|
|
310
|
+
return capped(verdict ?? jsonBodyCap)(c, next);
|
|
311
|
+
});
|
|
312
|
+
// ── JSON nesting cap, right after the size cap (so the scan is bounded by
|
|
313
|
+
// it): `JSON.parse` accepts any depth but `JSON.stringify` is recursive, so
|
|
314
|
+
// a small body of nothing but `[` turns any echo/audit/log of it into a
|
|
315
|
+
// RangeError 500 inside the shipped node:22-alpine image.
|
|
316
|
+
// The form file upload is exempt: its body is a FILE, never parsed, and a
|
|
317
|
+
// respondent's `.json` sent with a JSON content type would otherwise be
|
|
318
|
+
// cloned into memory whole (up to the upload ceiling) by the scan.
|
|
319
|
+
const depthLimit = jsonDepthLimit();
|
|
320
|
+
api.use('*', (c, next) => (tool.api.jsonDepthExempt?.(c.req.path) ? next() : depthLimit(c, next)));
|
|
321
|
+
// ── Auth-surface rate limits: registered FIRST so they run before auth
|
|
322
|
+
// resolution — abusive traffic is rejected before it costs a DB query.
|
|
323
|
+
const clientIp = makeClientIp(env.TRUST_PROXY);
|
|
324
|
+
// Login keys by IP AND email: Docker NAT (or a rotating attacker) can
|
|
325
|
+
// collapse/expand the IP dimension, so per-account protection must not
|
|
326
|
+
// depend on it. emailKeyOf returns [] for bodyless social sign-in.
|
|
327
|
+
//
|
|
328
|
+
// consumeOn: 'failure' — see rate-limit.ts. The email key is derived from
|
|
329
|
+
// the REQUEST BODY, i.e. from an address anyone can type, so consuming it on
|
|
330
|
+
// arrival made the wall an account-lockout tool: ten POSTs naming a victim
|
|
331
|
+
// emptied that account's bucket for the window. Consuming only when the
|
|
332
|
+
// credential was actually refused keeps the brute-force budget identical and
|
|
333
|
+
// makes the owner's own successful sign-ins free.
|
|
334
|
+
//
|
|
335
|
+
// The federated start (`/auth/sign-in/oauth2`, the silent start toward the
|
|
336
|
+
// hub) presents no credential: it only answers the hub's authorize URL, and
|
|
337
|
+
// the hub checks the person with its own wall. It stays off this one, so a
|
|
338
|
+
// wall drained by wrong passwords behind a shared address does not also
|
|
339
|
+
// close the hub sign-in to everyone behind it (PRDCT-2830).
|
|
340
|
+
const loginWall = rateLimit(limiters.login, clientIp, emailKeyOf, { consumeOn: 'failure' });
|
|
341
|
+
api.use('/auth/sign-in/*', (c, next) => c.req.path.endsWith('/auth/sign-in/oauth2') ? next() : loginWall(c, next));
|
|
342
|
+
api.use('/auth/email-otp/*', rateLimit(limiters.otp, clientIp, emailKeyOf));
|
|
343
|
+
api.use('/auth/sign-up/*', rateLimit(limiters.login, clientIp));
|
|
344
|
+
// Password reset: request keys by IP AND email (tight); reset + change key
|
|
345
|
+
// by IP (brute-forcing a token / the current password). The deprecated
|
|
346
|
+
// emailOTP forget-password alias shares the reset wall.
|
|
347
|
+
api.use('/auth/request-password-reset', rateLimit(limiters.passwordReset, clientIp, emailKeyOf));
|
|
348
|
+
api.use('/auth/reset-password', rateLimit(limiters.passwordReset, clientIp));
|
|
349
|
+
api.use('/auth/reset-password/*', rateLimit(limiters.passwordReset, clientIp));
|
|
350
|
+
api.use('/auth/forget-password/*', rateLimit(limiters.passwordReset, clientIp, emailKeyOf));
|
|
351
|
+
// Email change mints sign-in-equivalent tokens and sends mail — same tight
|
|
352
|
+
// wall as the reset surface (per IP; the route is session-gated anyway).
|
|
353
|
+
api.use('/auth/change-email', rateLimit(limiters.passwordReset, clientIp));
|
|
354
|
+
api.use('/auth/change-password', rateLimit(limiters.login, clientIp));
|
|
355
|
+
// The 2FA surface: verify-totp / verify-backup-code guess a 6-digit code or
|
|
356
|
+
// a backup code behind the 10-minute pending-sign-in cookie; enable/disable
|
|
357
|
+
// present the current password. Same brute-force wall as login (per IP).
|
|
358
|
+
api.use('/auth/two-factor/*', rateLimit(limiters.login, clientIp));
|
|
359
|
+
// Account deletion presents the current password — same brute-force wall.
|
|
360
|
+
api.use('/auth/delete-user', rateLimit(limiters.login, clientIp));
|
|
361
|
+
api.use('/auth/oauth2/register', rateLimit(limiters.oauthRegister, clientIp));
|
|
362
|
+
api.use('/auth/oauth2/token', rateLimit(limiters.oauthToken, clientIp));
|
|
363
|
+
api.use('/setup', rateLimit(limiters.setup, clientIp));
|
|
364
|
+
api.use('/invitations/accept', rateLimit(limiters.invitationAccept, clientIp));
|
|
365
|
+
api.use('/invitations/lookup', rateLimit(limiters.invitationAccept, clientIp));
|
|
366
|
+
// Collaborator claims are invitation acceptances in per-resource clothing —
|
|
367
|
+
// the same public token-redemption surface, the same wall.
|
|
368
|
+
const rateLimitContext = { ...hookContext, clientIp };
|
|
369
|
+
tool.api.rateLimits?.(api, rateLimitContext);
|
|
370
|
+
// Break-glass: a rare superadmin recovery action — a tight per-IP wall
|
|
371
|
+
// bounds allowlist probing before the handlers' own session checks run.
|
|
372
|
+
api.use('/admin/break-glass/*', rateLimit(limiters.breakGlass, clientIp));
|
|
373
|
+
// CLI email-OTP sign-in (api/cli-auth.ts): request = an OTP send (the OTP
|
|
374
|
+
// wall, per IP + email); complete = a credential guess (the login wall,
|
|
375
|
+
// per IP + email) on top of better-auth's own 3-attempts-per-code limit.
|
|
376
|
+
api.use('/cli/auth/request', rateLimit(limiters.otp, clientIp, emailKeyOf));
|
|
377
|
+
api.use('/cli/auth/complete', rateLimit(limiters.login, clientIp, emailKeyOf, { consumeOn: 'failure' }));
|
|
378
|
+
// The demo pass redeem (identity/demo-pass-plugin.ts): a secret presented
|
|
379
|
+
// anonymously, so every arrival costs, per address only. In front of the
|
|
380
|
+
// sign-in handler, and only where the endpoint exists.
|
|
381
|
+
if (demoSignIn) {
|
|
382
|
+
// Only a REFUSED redeem costs: the wall is there against guessing, and a
|
|
383
|
+
// person playing a demonstration opens many valid links in a row from one
|
|
384
|
+
// address (found on the pair: the 21st valid click in fifteen minutes was
|
|
385
|
+
// refused). A drained wall still refuses everything until it refills.
|
|
386
|
+
api.use('/auth/demo/redeem', rateLimit(limiters.demoRedeem, clientIp, undefined, { consumeOn: 'failure' }));
|
|
387
|
+
}
|
|
388
|
+
// The OpenAPI document is unauthenticated (PUBLIC_API_PATHS) so it never
|
|
389
|
+
// reaches the per-principal quota; the buffer is generated once at boot
|
|
390
|
+
// (openapi-doc.ts) and this wall is the defence in depth on top.
|
|
391
|
+
api.use('/openapi.json', rateLimit(limiters.openapiDoc, clientIp));
|
|
392
|
+
// CLI cross-tool connect (cloud only, api/sso-connect.ts): presenting an
|
|
393
|
+
// exchange token is a credential presentation, walled per client address
|
|
394
|
+
// on its own bucket sized for a room (CLI_CONNECT_IP_LIMIT_PER_15_MIN): on
|
|
395
|
+
// the login wall (10 per 15 minutes) the 11th `<tool> login` behind one
|
|
396
|
+
// venue address was refused (PRDCT-2958, verifier F1). The token itself is
|
|
397
|
+
// one-time and the hub's walls bound each person. Conditional like the
|
|
398
|
+
// route itself: oss mounts zero hub surface.
|
|
399
|
+
if (deps.hubSso) {
|
|
400
|
+
api.use('/sso/cli-connect', rateLimit(limiters.cliConnect, clientIp));
|
|
401
|
+
}
|
|
402
|
+
// ── JSON pre-validation in front of the Better Auth mount (AF-1 + the
|
|
403
|
+
// update-user half of SL-B4, PRDCT-1358): Better Auth answers its own 500
|
|
404
|
+
// on a malformed/empty JSON body and on a NUL that reaches Postgres —
|
|
405
|
+
// responses app.onError never sees, so the app-level mappings cannot help.
|
|
406
|
+
// Registered here, after the size and depth caps, so the guard's body read
|
|
407
|
+
// is bounded.
|
|
408
|
+
api.use('/auth/*', authBodyGuard());
|
|
409
|
+
// Demo sign-in, the second door of "a session a pass opened lives only
|
|
410
|
+
// while its pass does": the sign-in library's mount runs BEFORE the
|
|
411
|
+
// credential resolver, and it is where a session authorizes an OAuth client
|
|
412
|
+
// and answers get-session. A dead pass's session is deleted here first, so
|
|
413
|
+
// the library sees none. A live pass's session is a visit: the library
|
|
414
|
+
// paths that would leave a credential, a grant or an account change behind
|
|
415
|
+
// it are refused here (identity/demo-pass-rules.ts
|
|
416
|
+
// `DEMO_SESSION_REFUSED_AUTH_PATHS`). And the library's own session deletes
|
|
417
|
+
// (a pass session's sign-out, a person's own revoke of one) delete the
|
|
418
|
+
// session row only: those sessions are ended the pass's way first
|
|
419
|
+
// (`endSessions`), so their OAuth tokens and link rows go with them.
|
|
420
|
+
// The revoking caller is verified by the library: a forged or dead cookie
|
|
421
|
+
// ends nothing.
|
|
422
|
+
if (demoSignIn) {
|
|
423
|
+
const judge = demoSessionJudge(db);
|
|
424
|
+
const demoPassService = new DemoPassService(db);
|
|
425
|
+
api.use('/auth/*', async (c, next) => {
|
|
426
|
+
if (!c.req.header('cookie'))
|
|
427
|
+
return next();
|
|
428
|
+
const verdict = await judge(c.req.raw.headers);
|
|
429
|
+
const authPath = c.req.path.startsWith(AUTH_MOUNT) ? c.req.path.slice(AUTH_MOUNT.length) : c.req.path;
|
|
430
|
+
if (verdict && verdict !== 'ended') {
|
|
431
|
+
if (demoSessionAuthRefusal(authPath)) {
|
|
432
|
+
return c.json(err('demo_session', 'A session opened by a demo link cannot do this: sign in with your password'), 403);
|
|
433
|
+
}
|
|
434
|
+
// A pass session that signs itself out ends the pass's way; the
|
|
435
|
+
// library then answers as usual.
|
|
436
|
+
if (authPath === '/sign-out')
|
|
437
|
+
await demoPassService.endSessions([verdict.sessionId]);
|
|
438
|
+
}
|
|
439
|
+
else if (authPath === '/revoke-session' ||
|
|
440
|
+
authPath === '/revoke-sessions' ||
|
|
441
|
+
authPath === '/revoke-other-sessions') {
|
|
442
|
+
// A person's OWN session revoking a pass session (a pass session is
|
|
443
|
+
// refused these paths above). The caller is verified by the library
|
|
444
|
+
// (`getSession`, the cookie's signature checked): a forged or dead
|
|
445
|
+
// cookie ends nothing. The body is read from a clone: the library
|
|
446
|
+
// gets the request untouched. The exact path compare loses nothing:
|
|
447
|
+
// the library's router answers 404 to any other spelling.
|
|
448
|
+
const verified = await auth.api.getSession({ headers: c.req.raw.headers }).catch(() => null);
|
|
449
|
+
if (verified?.session?.id && verified?.user?.id) {
|
|
450
|
+
let bodyToken = null;
|
|
451
|
+
try {
|
|
452
|
+
const body = await c.req.raw.clone().json();
|
|
453
|
+
if (body && typeof body === 'object' && typeof body.token === 'string') {
|
|
454
|
+
bodyToken = body.token;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// No JSON body: nothing names a session (the library answers as it does).
|
|
459
|
+
}
|
|
460
|
+
const passSessions = await demoPassService.passSessionsRevokedBy(authPath, { sessionId: verified.session.id, userId: verified.user.id }, bodyToken);
|
|
461
|
+
if (passSessions.length > 0)
|
|
462
|
+
await demoPassService.endSessions(passSessions);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return next();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Better Auth owns /api/v1/auth/* (mounted before the credential middleware
|
|
469
|
+
// — it IS the credential machinery). On cloud the whole mount runs inside
|
|
470
|
+
// the hub-SSO login scope: an AsyncLocalStorage span carrying the verified
|
|
471
|
+
// hub assertion from the SSO callback's getUserInfo to its after-hook —
|
|
472
|
+
// request-scoped, so concurrent logins can never cross-wire (ADR 015).
|
|
473
|
+
const { hubSso } = deps;
|
|
474
|
+
// The change-email consume, wrapped (PRDCT-1437). The request is rewritten
|
|
475
|
+
// on a collision (rewriteChangeEmailCollision above), then two response
|
|
476
|
+
// normalizations keep the taken and free cases indistinguishable:
|
|
477
|
+
//
|
|
478
|
+
// - JSON success body: the change-email success echoes the resulting email
|
|
479
|
+
// in its `user` record — the one observable the no-op rewrite leaves
|
|
480
|
+
// differing between the taken and free cases. It is reachable ONLY without
|
|
481
|
+
// a `callbackURL` (a real consume always carries one and redirects, never
|
|
482
|
+
// hitting this JSON), so stripping `user` to null for every change-email
|
|
483
|
+
// consume touches no legitimate flow. Only the change/legacy branches
|
|
484
|
+
// return a non-null user; the plain verify-email success is already null.
|
|
485
|
+
// - a residual 5xx: the rewrite closes the DECIDABLE collision, but two
|
|
486
|
+
// concurrent mints claiming one FRESH address (no pre-existing account —
|
|
487
|
+
// so NOT an existence probe) can still lose at the DB. Map it to the same
|
|
488
|
+
// benign JSON so it never surfaces a raw 500. No redirect is issued here —
|
|
489
|
+
// every redirect stays Better Auth's, judged by its own originCheck, so
|
|
490
|
+
// this wrapper adds no open-redirect surface.
|
|
491
|
+
//
|
|
492
|
+
// Registered BEFORE the wildcard mount so the exact route wins.
|
|
493
|
+
api.on(['GET'], '/auth/verify-email', async (c) => {
|
|
494
|
+
const req = await rewriteChangeEmailCollision(auth, deps.authSecret, c.req.raw);
|
|
495
|
+
const res = await (hubSso ? hubSso.runWithLoginScope(() => auth.handler(req)) : auth.handler(req));
|
|
496
|
+
if (res.status === 200 && (res.headers.get('content-type') ?? '').includes('application/json')) {
|
|
497
|
+
const body = (await res
|
|
498
|
+
.clone()
|
|
499
|
+
.json()
|
|
500
|
+
.catch(() => null));
|
|
501
|
+
if (body && body.status === true && body.user != null) {
|
|
502
|
+
const headers = new Headers(res.headers);
|
|
503
|
+
headers.delete('content-length');
|
|
504
|
+
return new Response(JSON.stringify({ status: true, user: null }), { status: 200, headers });
|
|
505
|
+
}
|
|
506
|
+
return res;
|
|
507
|
+
}
|
|
508
|
+
// A 3xx: constrain the Location to this origin (Major 3). Better Auth's
|
|
509
|
+
// GET verify-email skips originCheck (no Origin header on a navigation) and
|
|
510
|
+
// redirects to any callbackURL — an open redirect for anyone holding a
|
|
511
|
+
// verify token, applied to BOTH branches so it stays uniform.
|
|
512
|
+
if (res.status >= 300 && res.status < 400) {
|
|
513
|
+
const location = res.headers.get('location');
|
|
514
|
+
if (location) {
|
|
515
|
+
const safe = safeRedirectLocation(location, c.req.url, env.PUBLIC_BASE_URL);
|
|
516
|
+
if (safe !== location) {
|
|
517
|
+
const headers = new Headers(res.headers);
|
|
518
|
+
headers.set('location', safe);
|
|
519
|
+
return new Response(null, { status: res.status, headers });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return res;
|
|
523
|
+
}
|
|
524
|
+
if (res.status < 500)
|
|
525
|
+
return res;
|
|
526
|
+
logger.warn({ status: res.status }, 'verify-email answered 5xx — mapped to the uniform non-revealing response');
|
|
527
|
+
return c.json({ status: true, user: null }, 200);
|
|
528
|
+
});
|
|
529
|
+
api.on(['GET', 'POST'], '/auth/*', (c) => hubSso ? hubSso.runWithLoginScope(() => auth.handler(c.req.raw)) : auth.handler(c.req.raw));
|
|
530
|
+
api.use('*', authContext({
|
|
531
|
+
registry: chassisRegistry,
|
|
532
|
+
isApiKeyToken: apiKeyService.isToken,
|
|
533
|
+
resolveApiKey: (token, requested) => apiKeyService.resolve(token, requested),
|
|
534
|
+
resolveOauthJwt: (token, requested) => deps.oauthJwt.resolve(token, requested),
|
|
535
|
+
keyFailureLimiter: limiters.apiKeyFailures,
|
|
536
|
+
clientIp,
|
|
537
|
+
// General per-principal quota (I3): runs inside authContext, after the
|
|
538
|
+
// credential resolves (the bucket key IS the principal identity) and
|
|
539
|
+
// before the scope gate. Public paths and /auth/* never reach it (no
|
|
540
|
+
// principal), so the auth-surface limiters above are not doubled.
|
|
541
|
+
// Limits ride the EntitlementService seam; buckets ride the same
|
|
542
|
+
// Redis-or-memory backend as every other limiter.
|
|
543
|
+
requestQuota: createRequestQuota({
|
|
544
|
+
entitlements: registry.entitlements,
|
|
545
|
+
make: limiters.make,
|
|
546
|
+
logger
|
|
547
|
+
}),
|
|
548
|
+
// Cloud edition's post-resolution veto (internal/federation.md P4);
|
|
549
|
+
// undefined on oss.
|
|
550
|
+
principalGate: deps.principalGate,
|
|
551
|
+
// Demo sign-in: a session a demo pass opened lives only while its pass
|
|
552
|
+
// does, and its audit rows carry the pass's id. Absent while the switch is off.
|
|
553
|
+
...(demoSignIn ? { demoSession: demoSessionJudge(db) } : {}),
|
|
554
|
+
// The composed fail-closed allowlist: chassis rules, then the tool's rules.
|
|
555
|
+
requiredScopeFor: tool.scopes.requiredScopeFor,
|
|
556
|
+
guestForbiddenMessage: tool.copy.guestForbidden
|
|
557
|
+
}));
|
|
558
|
+
// A session a demo link opened is a visit (identity/demo-pass-rules.ts
|
|
559
|
+
// `DEMO_SESSION_REFUSED_API_ROUTES`): the routes that mint a credential or
|
|
560
|
+
// make something the person keeps refuse it here, at one place, right after
|
|
561
|
+
// the credential resolves and before anything claims an idempotency key.
|
|
562
|
+
// `demoPassId` is only ever set while the switch is on. The tool's own
|
|
563
|
+
// routes of the kind (its `demoSessionRefusedRoutes` slot) join the list here.
|
|
564
|
+
for (const rule of [...DEMO_SESSION_REFUSED_API_ROUTES, ...(tool.demoSessionRefusedRoutes ?? [])]) {
|
|
565
|
+
const gate = async (c, next) => {
|
|
566
|
+
if (c.get('demoPassId')) {
|
|
567
|
+
return c.json(err('demo_session', demoSessionApiRefusalMessage(rule.does)), 403);
|
|
568
|
+
}
|
|
569
|
+
return next();
|
|
570
|
+
};
|
|
571
|
+
if (rule.method === 'ALL')
|
|
572
|
+
api.use(rule.path, gate);
|
|
573
|
+
else
|
|
574
|
+
api.on(rule.method, rule.path, gate);
|
|
575
|
+
}
|
|
576
|
+
// Idempotency sits strictly BETWEEN authContext (it needs the resolved
|
|
577
|
+
// principal to scope claims) and auditMiddleware (a replayed short-circuit
|
|
578
|
+
// never reaches the audit layer, so a retry cannot land a second audit row).
|
|
579
|
+
api.use('*', idempotency({
|
|
580
|
+
db,
|
|
581
|
+
authSecret: deps.authSecret,
|
|
582
|
+
...(tool.api.idempotencyTargets ? { toolTargets: tool.api.idempotencyTargets } : {})
|
|
583
|
+
}));
|
|
584
|
+
api.use('*', auditMiddleware(audit, clientIp, tool.api.auditExempt ? { exempt: tool.api.auditExempt } : {}));
|
|
585
|
+
// Instance id for usage-event sources, cached after first read.
|
|
586
|
+
let cachedInstanceId = null;
|
|
587
|
+
const instanceId = async () => {
|
|
588
|
+
if (cachedInstanceId)
|
|
589
|
+
return cachedInstanceId;
|
|
590
|
+
const [row] = await db.select({ id: instanceSettings.instanceId }).from(instanceSettings).limit(1);
|
|
591
|
+
cachedInstanceId = row?.id ?? 'unsetup';
|
|
592
|
+
return cachedInstanceId;
|
|
593
|
+
};
|
|
594
|
+
// ── The entitlement gate (the billing rail, §7) — AFTER the scope gate
|
|
595
|
+
// (inside authContext), the idempotency claim and the audit middleware,
|
|
596
|
+
// BEFORE every handler: one middleware per declared route, so a route
|
|
597
|
+
// that declares a meter, a limit or a feature is checked here and its
|
|
598
|
+
// usage event emitted here, and no handler checks or emits by hand. The
|
|
599
|
+
// dashboard, the CLI and the MCP tools all pass through it.
|
|
600
|
+
registerEntitlementGate(api, {
|
|
601
|
+
declarations: deps.entitlements.routes,
|
|
602
|
+
tool: deps.entitlements,
|
|
603
|
+
entitlements: registry.entitlements,
|
|
604
|
+
usage: registry.usage,
|
|
605
|
+
cloud: deps.entitlementCloud,
|
|
606
|
+
source: async () => ({ instanceId: await instanceId(), edition: env.EDITION, version: env.APP_VERSION }),
|
|
607
|
+
logger
|
|
608
|
+
});
|
|
609
|
+
// ── GET /instance — unauthenticated discovery ────────────────────────────
|
|
610
|
+
api.openapi(instanceRoute, async (c) => {
|
|
611
|
+
const [row] = await db.select().from(instanceSettings).limit(1);
|
|
612
|
+
c.header('Cache-Control', 'public, max-age=60');
|
|
613
|
+
return c.json({
|
|
614
|
+
name: row?.name ?? tool.identity.displayName,
|
|
615
|
+
instanceId: row?.instanceId ?? null,
|
|
616
|
+
edition: env.EDITION,
|
|
617
|
+
version: env.APP_VERSION,
|
|
618
|
+
apiVersion: 'v1',
|
|
619
|
+
setupRequired: !row,
|
|
620
|
+
auth: {
|
|
621
|
+
...registry.identity.describe(),
|
|
622
|
+
// Cloud only (SL-1): the hint-cookie contract the dashboard's
|
|
623
|
+
// silent auto-connect reads — spread-gated on the same single
|
|
624
|
+
// `hub` switch as every cloud seam, so the oss discovery bytes
|
|
625
|
+
// never carry the key.
|
|
626
|
+
...(hub
|
|
627
|
+
? { sso: { hintCookieName: hub.hintCookieName, hintCookieDomain: hub.hintCookieDomain } }
|
|
628
|
+
: {})
|
|
629
|
+
},
|
|
630
|
+
features: { mcp: true, oauth: true, files: true },
|
|
631
|
+
// Present only while demo links sign people in here (the banner's cue).
|
|
632
|
+
...(demoSignIn ? { demoSignIn: true } : {}),
|
|
633
|
+
// Present only on a cloud instance whose operator restricted the tool
|
|
634
|
+
// (PRDCT-2947): organizations are opened by Antasphere, not created here.
|
|
635
|
+
...(hub && env.TOOL_RESTRICTED ? { restricted: true } : {}),
|
|
636
|
+
// What this version declares for the billing rail (§7): the hub seeds
|
|
637
|
+
// from it, staff read it, a client reads the limit it is held to.
|
|
638
|
+
entitlements: {
|
|
639
|
+
actions: deps.entitlements.actions,
|
|
640
|
+
limits: deps.entitlements.limits,
|
|
641
|
+
features: deps.entitlements.features
|
|
642
|
+
}
|
|
643
|
+
}, 200);
|
|
644
|
+
});
|
|
645
|
+
// ── POST /setup — one-shot first-boot wizard ─────────────────────────────
|
|
646
|
+
api.openapi(setupRoute, async (c) => {
|
|
647
|
+
const body = c.req.valid('json');
|
|
648
|
+
// BEFORE the token check: an instance reachable only over plaintext on a
|
|
649
|
+
// non-loopback origin must not accept the owner password at all, whether
|
|
650
|
+
// or not the caller holds the token (setup-transport.ts).
|
|
651
|
+
if (!env.ALLOW_INSECURE_SETUP && !isSecureSetupOrigin(env.PUBLIC_BASE_URL)) {
|
|
652
|
+
logger.warn({ publicBaseUrl: env.PUBLIC_BASE_URL }, 'refused setup over plaintext on a non-loopback origin');
|
|
653
|
+
return c.json(err('insecure_transport', 'Setup over plaintext HTTP on a non-loopback origin is refused. Put TLS in front, reach the instance through an SSH tunnel, or set ALLOW_INSECURE_SETUP=true.'), 403);
|
|
654
|
+
}
|
|
655
|
+
const [existing] = await db.select({ id: instanceSettings.id }).from(instanceSettings).limit(1);
|
|
656
|
+
if (existing) {
|
|
657
|
+
return c.json(err('already_setup', 'This instance has already been set up'), 410);
|
|
658
|
+
}
|
|
659
|
+
// ALWAYS required (PRDCT-1347): the claim decides who owns the instance.
|
|
660
|
+
// `deps.setupToken` is SETUP_TOKEN or the boot-generated token; it is
|
|
661
|
+
// null only for a set-up instance, which the 410 above already answered
|
|
662
|
+
// — so a null here is a claim with no credential to compare against and
|
|
663
|
+
// fails closed rather than falling through to a free claim.
|
|
664
|
+
if (!deps.setupToken || !constantTimeEquals(body.setupToken ?? '', deps.setupToken)) {
|
|
665
|
+
return c.json(err('invalid_setup_token', 'A valid setup token is required'), 403);
|
|
666
|
+
}
|
|
667
|
+
// Create the owner user through Better Auth so credential hashing and
|
|
668
|
+
// account rows follow its rules exactly. Better Auth cannot join our
|
|
669
|
+
// transaction, so this happens first; a setup that later loses the claim
|
|
670
|
+
// race leaves an orphaned user who can sign in but reaches nothing (401
|
|
671
|
+
// on every route — no membership).
|
|
672
|
+
let ownerUserId;
|
|
673
|
+
try {
|
|
674
|
+
const created = await auth.api.signUpEmail({
|
|
675
|
+
body: { email: body.owner.email, password: body.owner.password, name: body.owner.name }
|
|
676
|
+
});
|
|
677
|
+
ownerUserId = created.user.id;
|
|
678
|
+
}
|
|
679
|
+
catch (cause) {
|
|
680
|
+
// Retry after a crashed earlier attempt: the user may already exist.
|
|
681
|
+
// Reuse it ONLY if the presented credentials actually sign in — never
|
|
682
|
+
// attach a workspace to an account the caller cannot prove they own.
|
|
683
|
+
try {
|
|
684
|
+
const signedIn = await auth.api.signInEmail({
|
|
685
|
+
body: { email: body.owner.email, password: body.owner.password }
|
|
686
|
+
});
|
|
687
|
+
ownerUserId = signedIn.user.id;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
logger.error({ err: cause }, 'setup: owner creation failed');
|
|
691
|
+
return c.json(err('owner_creation_failed', 'Could not create the owner user'), 400);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// The operator drove the wizard (holding the setup token when one is
|
|
695
|
+
// set) — their address needs no mailbox dance, and on the cloud edition
|
|
696
|
+
// it MUST be verified (D9, internal/federation.md): under hub-only login the
|
|
697
|
+
// operator's only entrance is the hub trusted-link, and Better Auth
|
|
698
|
+
// refuses to link a trusted provider onto an UNVERIFIED local email
|
|
699
|
+
// (requireLocalEmailVerified stays on). An unverified operator bricks a
|
|
700
|
+
// cloud instance at bootstrap. The hub's own setup does the same.
|
|
701
|
+
await db.update(userTable).set({ emailVerified: true }).where(eq(userTable.id, ownerUserId));
|
|
702
|
+
// Claim the singleton and — on the OSS edition — create the FIRST
|
|
703
|
+
// workspace + owner membership in ONE transaction: either the instance
|
|
704
|
+
// is fully set up or nothing persisted. The workspace goes through the
|
|
705
|
+
// registry's WorkspaceService (ADR 014) — the same path product flows
|
|
706
|
+
// use for every LATER workspace. On CLOUD (`hub` set — the same single
|
|
707
|
+
// switch as every cloud seam) setup creates NO workspace: every cloud
|
|
708
|
+
// workspace is a hub-org projection (user-scoped federation), so the
|
|
709
|
+
// operator bootstrap mints a verified, break-glass capable USER only —
|
|
710
|
+
// the singleton claim, the edition stamp, and the D9 emailVerified
|
|
711
|
+
// force above all stay.
|
|
712
|
+
const instanceId = ulid();
|
|
713
|
+
let workspaceId;
|
|
714
|
+
try {
|
|
715
|
+
workspaceId = await db.transaction(async (tx) => {
|
|
716
|
+
const claimed = await tx
|
|
717
|
+
.insert(instanceSettings)
|
|
718
|
+
// The edition stamp is what the R7 boot guard compares against
|
|
719
|
+
// (boot.ts, internal/federation.md): setup records the edition this
|
|
720
|
+
// instance was born under.
|
|
721
|
+
.values({
|
|
722
|
+
id: 'instance',
|
|
723
|
+
instanceId,
|
|
724
|
+
name: body.instanceName,
|
|
725
|
+
edition: env.EDITION,
|
|
726
|
+
// The durable operator record (CLOUD-3): survives the orphan purge.
|
|
727
|
+
operatorUserId: ownerUserId
|
|
728
|
+
})
|
|
729
|
+
.onConflictDoNothing()
|
|
730
|
+
.returning({ id: instanceSettings.id });
|
|
731
|
+
if (claimed.length === 0)
|
|
732
|
+
throw new SetupAlreadyDone();
|
|
733
|
+
if (hub)
|
|
734
|
+
return null;
|
|
735
|
+
const created = await registry.workspaces.create(body.instanceName, ownerUserId, tx);
|
|
736
|
+
return created.workspaceId;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
catch (cause) {
|
|
740
|
+
if (cause instanceof SetupAlreadyDone) {
|
|
741
|
+
return c.json(err('already_setup', 'This instance has already been set up'), 410);
|
|
742
|
+
}
|
|
743
|
+
logger.error({ err: cause }, 'setup: transaction failed (nothing persisted)');
|
|
744
|
+
return c.json(err('internal', 'Setup failed; the instance remains uninitialized — retry'), 500);
|
|
745
|
+
}
|
|
746
|
+
chassisRegistry.events.emit('setup.completed', { workspaceId, instanceId });
|
|
747
|
+
logger.info({ workspaceId, instanceId }, 'first-boot setup completed');
|
|
748
|
+
// The generated token has done its one job; a leftover copy in the data
|
|
749
|
+
// volume is a secret with no purpose (it would ride in every backup).
|
|
750
|
+
await deps.clearGeneratedSetupToken().catch((cause) => {
|
|
751
|
+
logger.warn({ err: cause }, 'setup: could not remove the generated setup-token file');
|
|
752
|
+
});
|
|
753
|
+
// The genesis privileged action: audited directly (no principal in
|
|
754
|
+
// context yet — the middleware exempts /setup).
|
|
755
|
+
await audit.write({
|
|
756
|
+
workspaceId,
|
|
757
|
+
principal: null,
|
|
758
|
+
action: 'instance.setup',
|
|
759
|
+
resourceType: 'instance',
|
|
760
|
+
resourceId: instanceId,
|
|
761
|
+
requestId: c.get('requestId'),
|
|
762
|
+
metadata: { ownerUserId, ownerEmail: body.owner.email }
|
|
763
|
+
});
|
|
764
|
+
return c.json({ instanceId, workspaceId, ownerUserId }, 201);
|
|
765
|
+
});
|
|
766
|
+
/**
|
|
767
|
+
* SL-6 /me extras — CLOUD + SESSION callers only (both spread-gated
|
|
768
|
+
* below; machine credentials and every oss response never carry the
|
|
769
|
+
* keys):
|
|
770
|
+
*
|
|
771
|
+
* - `firstRunPending`: tool-local, retry-safe banner truth —
|
|
772
|
+
* `NOT EXISTS (user_onboarding row WHERE dismissed_at IS NOT NULL)`.
|
|
773
|
+
* A missing row (lost first-login insert) still shows the welcome;
|
|
774
|
+
* only an explicit dismissal (or the deploy backfill) hides it.
|
|
775
|
+
* - `ssoOnly`: the hint-watch discriminator — the user holds an
|
|
776
|
+
* `antasphere` account row AND no local credential (password) row.
|
|
777
|
+
* 'credential' is better-auth's password-account providerId (pinned
|
|
778
|
+
* 1.6.15; re-verify on bump). A break-glass-capable operator always
|
|
779
|
+
* has a credential row (setup mints it), so they are NEVER ssoOnly
|
|
780
|
+
* and the dashboard's hint-watch can never sign them out.
|
|
781
|
+
* - `hubDenied`: the organizations the person's last definitive hub
|
|
782
|
+
* list named as not opening this tool to them (a hint for the
|
|
783
|
+
* refusal page; the hub is the truth), and `hubNoAccessUrl`, the
|
|
784
|
+
* hub's page that lists whom to ask, ready-made with this tool's
|
|
785
|
+
* client id.
|
|
786
|
+
*/
|
|
787
|
+
// `/me.canCreateWorkspace` and POST /workspaces judge through the SAME
|
|
788
|
+
// rule (api/workspaces.ts), so the flag never promises what the route
|
|
789
|
+
// refuses. Never throws into /me: a failed read answers false.
|
|
790
|
+
const creationPolicy = workspaceCreationPolicy({
|
|
791
|
+
maxPerUser: env.MAX_WORKSPACES_PER_USER,
|
|
792
|
+
restricted: env.TOOL_RESTRICTED,
|
|
793
|
+
cloud: deps.workspaceCloud
|
|
794
|
+
});
|
|
795
|
+
const creationWall = { person: limiters.workspaceCreate, address: limiters.workspaceCreateAddress };
|
|
796
|
+
const canCreateWorkspace = async (userId, via, address) => {
|
|
797
|
+
try {
|
|
798
|
+
if ((await workspaceCreationRefusal(db, creationPolicy, { userId, via })) !== null)
|
|
799
|
+
return false;
|
|
800
|
+
// Honest about the rate wall too (read-only, nothing is spent): a
|
|
801
|
+
// person the route would answer 429 is not offered the entry.
|
|
802
|
+
return !(await workspaceCreateWallClosed(creationWall, address, userId));
|
|
803
|
+
}
|
|
804
|
+
catch (cause) {
|
|
805
|
+
logger.warn({ err: cause }, '/me: canCreateWorkspace could not be computed — answering false');
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const CREDENTIAL_PROVIDER_ID = 'credential';
|
|
810
|
+
// Cloud only (null on oss): the hub's no-access url, built once here where
|
|
811
|
+
// the hub is known and handed to the session extras by both callers.
|
|
812
|
+
const hubNoAccessUrl = hub
|
|
813
|
+
? `${hub.issuerUrl.replace(/\/+$/, '')}/no-access?client_id=${encodeURIComponent(hub.clientId)}`
|
|
814
|
+
: null;
|
|
815
|
+
const cloudSessionExtras = async (userId, noAccessUrl) => {
|
|
816
|
+
const [onboardingRows, accountRows] = await Promise.all([
|
|
817
|
+
db
|
|
818
|
+
.select({ dismissedAt: userOnboarding.dismissedAt })
|
|
819
|
+
.from(userOnboarding)
|
|
820
|
+
.where(eq(userOnboarding.userId, userId))
|
|
821
|
+
.limit(1),
|
|
822
|
+
db.select({ providerId: account.providerId }).from(account).where(eq(account.userId, userId))
|
|
823
|
+
]);
|
|
824
|
+
const providers = new Set(accountRows.map((r) => r.providerId));
|
|
825
|
+
return {
|
|
826
|
+
firstRunPending: onboardingRows[0]?.dismissedAt == null,
|
|
827
|
+
ssoOnly: providers.has(HUB_SSO_PROVIDER_ID) && !providers.has(CREDENTIAL_PROVIDER_ID),
|
|
828
|
+
hubDenied: deps.hubDeniedOrgs?.(userId) ?? [],
|
|
829
|
+
hubNoAccessUrl: noAccessUrl
|
|
830
|
+
};
|
|
831
|
+
};
|
|
832
|
+
// The zero-state re-admission: one cached reconcile pass, then the
|
|
833
|
+
// session is resolved again (the selection rule picks the workspace) and
|
|
834
|
+
// passed through the live gate exactly as authContext would. Any verdict
|
|
835
|
+
// other than a resolved, admitted principal keeps the zero state.
|
|
836
|
+
const readmitAfterReconcile = async (c, userId) => {
|
|
837
|
+
try {
|
|
838
|
+
await deps.reconcileForZeroState?.(userId);
|
|
839
|
+
const resolved = await resolveSessionPrincipal(c, chassisRegistry);
|
|
840
|
+
if (!resolved || resolved.userId !== userId)
|
|
841
|
+
return null;
|
|
842
|
+
const admitted = await admitPrincipal(c, resolved, deps.principalGate);
|
|
843
|
+
return admitted.ok ? admitted.principal : null;
|
|
844
|
+
}
|
|
845
|
+
catch (cause) {
|
|
846
|
+
logger.warn({ err: cause, userId }, '/me: zero-state re-admission failed — answering the zero state');
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
// ── GET /me — whoami across all credential paths ─────────────────────────
|
|
851
|
+
// No blanket requireAuth: the ZERO-MEMBERSHIP session state is route-local
|
|
852
|
+
// (user-scoped federation). A LIVE session whose user holds no active
|
|
853
|
+
// membership — the cloud operator before break-glass, a hub user whose
|
|
854
|
+
// last org was removed — resolves to a null principal (principal
|
|
855
|
+
// resolution requires a membership) but must NOT read as "signed out":
|
|
856
|
+
// /me answers 200 with `workspaces: [], workspace: null` so the dashboard
|
|
857
|
+
// renders the no-organization zero state instead of a login bounce.
|
|
858
|
+
// Machine credentials keep their 401: an invalid/zero-membership key or
|
|
859
|
+
// bearer dies inside authContext before this handler ever runs, so the
|
|
860
|
+
// session lookup below can only ever fire for cookie-authenticated
|
|
861
|
+
// requests.
|
|
862
|
+
api.openapi(meRoute, async (c) => {
|
|
863
|
+
let principal = c.get('principal');
|
|
864
|
+
let zeroStateUser = null;
|
|
865
|
+
if (!principal) {
|
|
866
|
+
// The zero state exists ONLY for the selector-less default request: a
|
|
867
|
+
// session that NAMED a workspace and failed its membership check must
|
|
868
|
+
// keep the fail-closed 401 (no oracle about the workspace, and the
|
|
869
|
+
// dashboard's stale-selection self-heal keys on that failure). With
|
|
870
|
+
// no selector, a live session resolving to null principal means
|
|
871
|
+
// exactly "zero active memberships" (the shared resolveMembership
|
|
872
|
+
// default rule).
|
|
873
|
+
if (c.req.header(ACTIVE_WORKSPACE_HEADER)?.trim()) {
|
|
874
|
+
return c.json(err('unauthenticated', 'Authentication required'), 401);
|
|
875
|
+
}
|
|
876
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
877
|
+
if (!session?.user) {
|
|
878
|
+
return c.json(err('unauthenticated', 'Authentication required'), 401);
|
|
879
|
+
}
|
|
880
|
+
// Re-admission (cloud): the live gate never runs for a principal-less
|
|
881
|
+
// session, so the zero-state read runs the reconciler's pass itself
|
|
882
|
+
// (cached ~10 s, single-flight, retry-throttled — a polling refusal
|
|
883
|
+
// page is not a hub call per request). When the pass re-projected a
|
|
884
|
+
// membership, the session resolves again through the SAME resolver
|
|
885
|
+
// and gate authContext uses, and the answer is the normal shape.
|
|
886
|
+
if (deps.reconcileForZeroState) {
|
|
887
|
+
principal = await readmitAfterReconcile(c, session.user.id);
|
|
888
|
+
}
|
|
889
|
+
if (!principal)
|
|
890
|
+
zeroStateUser = session.user;
|
|
891
|
+
}
|
|
892
|
+
if (!principal) {
|
|
893
|
+
const user = zeroStateUser;
|
|
894
|
+
if (!user)
|
|
895
|
+
return c.json(err('unauthenticated', 'Authentication required'), 401);
|
|
896
|
+
return c.json({
|
|
897
|
+
user: { id: user.id, email: user.email, name: user.name },
|
|
898
|
+
workspace: null,
|
|
899
|
+
role: null,
|
|
900
|
+
origin: null,
|
|
901
|
+
via: 'session',
|
|
902
|
+
scopes: null,
|
|
903
|
+
apiKeyExpiresAt: null,
|
|
904
|
+
workspaces: [],
|
|
905
|
+
activeWorkspaceId: null,
|
|
906
|
+
// The zero state's CTA target on cloud: organizations are created
|
|
907
|
+
// at the hub, never locally (internal/federation.md).
|
|
908
|
+
hubManageUrl: hubManaged?.manageUrl ?? null,
|
|
909
|
+
// oss: always false here — a zero-membership user is not an active
|
|
910
|
+
// member of this instance. cloud: true with a live hub link (their
|
|
911
|
+
// next organization is created from this very state).
|
|
912
|
+
canCreateWorkspace: hub ? await canCreateWorkspace(user.id, 'session', clientIp(c)) : false,
|
|
913
|
+
// Cloud + session extras (SL-6): the zero state is session-only
|
|
914
|
+
// by construction, so only the edition gate applies here.
|
|
915
|
+
...(hubNoAccessUrl !== null ? await cloudSessionExtras(user.id, hubNoAccessUrl) : {})
|
|
916
|
+
}, 200);
|
|
917
|
+
}
|
|
918
|
+
// Workspaces this credential can name (user-scoped credential model):
|
|
919
|
+
// EVERY credential kind lists ALL of the user's active memberships — a
|
|
920
|
+
// credential is the user, and the org is a per-request parameter. The
|
|
921
|
+
// order mirrors the selection rule (default first, then oldest), but
|
|
922
|
+
// clients must read the `default` flag explicitly, never infer it from
|
|
923
|
+
// the index. (A PINNED key still lists everything: the pin restricts
|
|
924
|
+
// which workspace the key can REACH, not what its holder may see —
|
|
925
|
+
// /me answers about the user behind the credential.)
|
|
926
|
+
const memberships = await db
|
|
927
|
+
.select({
|
|
928
|
+
id: workspaces.id,
|
|
929
|
+
name: workspaces.name,
|
|
930
|
+
role: workspaceMembers.role,
|
|
931
|
+
centralAccountId: workspaces.centralAccountId,
|
|
932
|
+
hubStatus: workspaces.hubStatus,
|
|
933
|
+
isDefault: workspaceMembers.isDefault,
|
|
934
|
+
lookTheme: workspaces.lookTheme,
|
|
935
|
+
lookPattern: workspaces.lookPattern,
|
|
936
|
+
lookField: workspaces.lookField,
|
|
937
|
+
lookGrain: workspaces.lookGrain
|
|
938
|
+
})
|
|
939
|
+
.from(workspaceMembers)
|
|
940
|
+
.innerJoin(workspaces, eq(workspaceMembers.workspaceId, workspaces.id))
|
|
941
|
+
.where(and(eq(workspaceMembers.userId, principal.userId), eq(workspaceMembers.isActive, true)))
|
|
942
|
+
.orderBy(desc(workspaceMembers.isDefault), asc(workspaceMembers.createdAt), asc(workspaceMembers.id));
|
|
943
|
+
// hubOrigin is the BOOLEAN projection flag (P7); the hub org id rides
|
|
944
|
+
// beside it since PRDCT-2947 (the person's own organization ids, which
|
|
945
|
+
// `--org <name>` resolves against). Map explicitly, never spread the row.
|
|
946
|
+
const wireWorkspaces = memberships.map((m) => ({
|
|
947
|
+
id: m.id,
|
|
948
|
+
name: m.name,
|
|
949
|
+
role: m.role,
|
|
950
|
+
hubOrigin: m.centralAccountId !== null,
|
|
951
|
+
centralAccountId: m.centralAccountId,
|
|
952
|
+
look: { theme: m.lookTheme, pattern: m.lookPattern, field: m.lookField, grain: m.lookGrain },
|
|
953
|
+
suspended: m.hubStatus === 'suspended',
|
|
954
|
+
default: m.isDefault
|
|
955
|
+
}));
|
|
956
|
+
const active = wireWorkspaces.find((m) => m.id === principal.workspaceId);
|
|
957
|
+
return c.json({
|
|
958
|
+
user: { id: principal.userId, email: principal.email, name: principal.name },
|
|
959
|
+
workspace: {
|
|
960
|
+
id: principal.workspaceId,
|
|
961
|
+
name: active?.name ?? '',
|
|
962
|
+
// The ACTIVE workspace's flag comes from the principal itself —
|
|
963
|
+
// accountRef IS the request workspace's centralAccountId, read
|
|
964
|
+
// live by every credential resolver.
|
|
965
|
+
hubOrigin: Boolean(principal.accountRef),
|
|
966
|
+
look: active?.look ?? { theme: null, pattern: null, field: null, grain: null }
|
|
967
|
+
},
|
|
968
|
+
role: principal.role,
|
|
969
|
+
origin: principal.origin,
|
|
970
|
+
via: principal.via,
|
|
971
|
+
scopes: principal.scopes ? [...principal.scopes] : null,
|
|
972
|
+
apiKeyExpiresAt: principal.apiKeyExpiresAt ?? null,
|
|
973
|
+
workspaces: wireWorkspaces,
|
|
974
|
+
activeWorkspaceId: principal.workspaceId,
|
|
975
|
+
hubManageUrl: hubManaged && principal.accountRef ? hubManaged.manageUrl : null,
|
|
976
|
+
// A session a demo link opened creates no workspace (POST /workspaces
|
|
977
|
+
// refuses it before the rule), so it is not offered one.
|
|
978
|
+
canCreateWorkspace: c.get('demoPassId')
|
|
979
|
+
? false
|
|
980
|
+
: await canCreateWorkspace(principal.userId, principal.via, clientIp(c)),
|
|
981
|
+
// Cloud + SESSION only (SL-6): machine credentials never carry the
|
|
982
|
+
// onboarding/hint-watch keys — the banner and the auto-sign-out are
|
|
983
|
+
// browser concerns.
|
|
984
|
+
...(hubNoAccessUrl !== null && principal.via === 'session'
|
|
985
|
+
? await cloudSessionExtras(principal.userId, hubNoAccessUrl)
|
|
986
|
+
: {})
|
|
987
|
+
}, 200);
|
|
988
|
+
});
|
|
989
|
+
// First-run onboarding dismiss (SL-6): cloud-only — an oss boot leaves
|
|
990
|
+
// POST /me/onboarding/dismiss to the JSON 404 terminator. Session-only
|
|
991
|
+
// (unlisted in the machine scope allowlist; route-local session check).
|
|
992
|
+
if (hub) {
|
|
993
|
+
registerOnboardingRoutes(api, { db, auth });
|
|
994
|
+
}
|
|
995
|
+
// Workspace creation (PRDCT-2444 / PRDCT-2443): one route, both editions.
|
|
996
|
+
// Session-only — unlisted in the machine scope allowlist, re-checked in
|
|
997
|
+
// the handler; self-audited into the NEW workspace (audit-exempt path).
|
|
998
|
+
registerWorkspaceRoutes(api, {
|
|
999
|
+
db,
|
|
1000
|
+
auth,
|
|
1001
|
+
audit,
|
|
1002
|
+
registry: chassisRegistry,
|
|
1003
|
+
logger,
|
|
1004
|
+
clientIp,
|
|
1005
|
+
wall: creationWall,
|
|
1006
|
+
maxPerUser: env.MAX_WORKSPACES_PER_USER,
|
|
1007
|
+
restricted: env.TOOL_RESTRICTED,
|
|
1008
|
+
cloud: deps.workspaceCloud,
|
|
1009
|
+
guestForbiddenMessage: tool.copy.guestForbidden,
|
|
1010
|
+
displayName: tool.identity.displayName
|
|
1011
|
+
});
|
|
1012
|
+
// ── Platform modules ─────────────────────────────────────────────────────
|
|
1013
|
+
// Break-glass first: it self-authenticates (superadmin sessions may carry
|
|
1014
|
+
// NO membership, so principal can be null) and is deliberately UNLISTED in
|
|
1015
|
+
// the machine scope allowlist — keys/tokens 403 fail-closed above.
|
|
1016
|
+
registerBreakGlassRoutes(api, {
|
|
1017
|
+
db,
|
|
1018
|
+
auth,
|
|
1019
|
+
audit,
|
|
1020
|
+
logger,
|
|
1021
|
+
superadminEmails: env.SUPERADMIN_EMAILS
|
|
1022
|
+
});
|
|
1023
|
+
// CLI email-OTP → API-key mint: PUBLIC pre-auth routes (listed in
|
|
1024
|
+
// PUBLIC_API_PATHS) riding the emailOTP plugin; rate-limited above. Also
|
|
1025
|
+
// registers DELETE /cli/auth/key — the authenticated self-revoke (CLI
|
|
1026
|
+
// logout), NOT public, open to machines under the tool's write scope. On
|
|
1027
|
+
// cloud (hubSso present) the mint pair refuses 403 cli_otp_disabled — the
|
|
1028
|
+
// D1 hub-only entrance closure; the self-revoke stays open.
|
|
1029
|
+
registerCliAuthRoutes(api, {
|
|
1030
|
+
db,
|
|
1031
|
+
auth,
|
|
1032
|
+
email,
|
|
1033
|
+
apiKeys: apiKeyService,
|
|
1034
|
+
audit,
|
|
1035
|
+
logger,
|
|
1036
|
+
hubSso,
|
|
1037
|
+
cliKeyScopes: tool.scopes.cliKey,
|
|
1038
|
+
routes: { cliAuthCompleteRoute },
|
|
1039
|
+
displayName: tool.identity.displayName
|
|
1040
|
+
});
|
|
1041
|
+
// CLI cross-tool connect (internal/federation.md P5): PUBLIC exchange of a
|
|
1042
|
+
// hub-minted 120 s JWT (+ its H3 offline grant) for a USER-scoped API
|
|
1043
|
+
// key. Registered ONLY on cloud — an oss boot leaves the path to the JSON
|
|
1044
|
+
// 404 terminator below, so the self-host edition provably carries zero
|
|
1045
|
+
// hub surface here.
|
|
1046
|
+
if (hubSso && deps.hubGrant) {
|
|
1047
|
+
registerSsoConnectRoutes(api, {
|
|
1048
|
+
db,
|
|
1049
|
+
auth,
|
|
1050
|
+
hubSso,
|
|
1051
|
+
grant: deps.hubGrant,
|
|
1052
|
+
apiKeys: apiKeyService,
|
|
1053
|
+
audit,
|
|
1054
|
+
logger,
|
|
1055
|
+
cliKeyScopes: tool.scopes.cliKey,
|
|
1056
|
+
routes: { ssoCliConnectRoute }
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
// Single logout (SL-2): cloud-only like /sso/cli-connect — an oss boot
|
|
1060
|
+
// leaves POST /sso/logout to the JSON 404 terminator. Deliberately
|
|
1061
|
+
// UNLISTED in the machine scope allowlist: sessions only (incl.
|
|
1062
|
+
// zero-membership — the handler resolves the session route-locally).
|
|
1063
|
+
if (hubSso && deps.hubLogout && hub) {
|
|
1064
|
+
registerSsoLogoutRoutes(api, {
|
|
1065
|
+
auth,
|
|
1066
|
+
logout: deps.hubLogout,
|
|
1067
|
+
hint: {
|
|
1068
|
+
name: hub.hintCookieName,
|
|
1069
|
+
domain: hub.hintCookieDomain,
|
|
1070
|
+
secure: env.PUBLIC_BASE_URL.startsWith('https://')
|
|
1071
|
+
},
|
|
1072
|
+
logger
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
registerMemberRoutes(api, {
|
|
1076
|
+
db,
|
|
1077
|
+
auth,
|
|
1078
|
+
publicBaseUrl: env.PUBLIC_BASE_URL,
|
|
1079
|
+
accountDeletion: deps.accountDeletion,
|
|
1080
|
+
hubManaged,
|
|
1081
|
+
guestTargetMessage: tool.copy.guestTarget,
|
|
1082
|
+
onMembershipRemoval: tool.membershipRemoval
|
|
1083
|
+
});
|
|
1084
|
+
// Teams: the tool's own on self-hosted (and in a cloud-local workspace), the
|
|
1085
|
+
// hub's projection in a hub-origin workspace, where the members' gate refuses the writes.
|
|
1086
|
+
registerTeamRoutes(api, { db, hubManaged });
|
|
1087
|
+
if (demoSignIn) {
|
|
1088
|
+
registerDemoPassRoutes(api, {
|
|
1089
|
+
db,
|
|
1090
|
+
publicBaseUrl: env.PUBLIC_BASE_URL,
|
|
1091
|
+
emailDomains: env.DEMO_SIGN_IN_EMAIL_DOMAINS,
|
|
1092
|
+
guestTargetMessage: tool.copy.guestTarget
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
registerApiKeyRoutes(api, db, apiKeyService, { apiKeysListRoute, apiKeyCreateRoute, apiKeyRevokeRoute });
|
|
1096
|
+
registerInvitationRoutes(api, {
|
|
1097
|
+
db,
|
|
1098
|
+
env,
|
|
1099
|
+
auth,
|
|
1100
|
+
email,
|
|
1101
|
+
audit,
|
|
1102
|
+
registry: chassisRegistry,
|
|
1103
|
+
logger,
|
|
1104
|
+
mailBrand: { name: tool.identity.displayName, tagline: tool.copy.mail.tagline },
|
|
1105
|
+
inviteMail: { pitch: tool.copy.mail.invitePitch, preheader: tool.copy.mail.invitePreheader },
|
|
1106
|
+
hubManaged,
|
|
1107
|
+
// CLOUD-5: no local-password accounts minted through invitations on cloud.
|
|
1108
|
+
ssoOnly: Boolean(deps.hubSso),
|
|
1109
|
+
...(demoSignIn ? { demoSession: demoSessionJudge(db) } : {})
|
|
1110
|
+
});
|
|
1111
|
+
// Projects: a subgroup of the workspace with its own members. Deliberately
|
|
1112
|
+
// handed NO `hubManaged`: project membership is the tool's own on both
|
|
1113
|
+
// editions, picked among the workspace's existing members.
|
|
1114
|
+
registerProjectRoutes(api, { db });
|
|
1115
|
+
registerAuditRoutes(api, db);
|
|
1116
|
+
registerExportRoutes(api, {
|
|
1117
|
+
db,
|
|
1118
|
+
env,
|
|
1119
|
+
storage: deps.storage,
|
|
1120
|
+
audit,
|
|
1121
|
+
limiters,
|
|
1122
|
+
clientIp,
|
|
1123
|
+
logger,
|
|
1124
|
+
workspaceExportRoute: tool.scopes.contractRoutes.workspaceExportRoute,
|
|
1125
|
+
// The tool's export entries, bound to its domain once, here. No slot: no field.
|
|
1126
|
+
...(tool.api.exportEntries ? { exportEntries: tool.api.exportEntries(deps.domain) } : {})
|
|
1127
|
+
});
|
|
1128
|
+
registerFileRoutes(api, {
|
|
1129
|
+
service: deps.fileService,
|
|
1130
|
+
storage: deps.storage,
|
|
1131
|
+
registry: chassisRegistry,
|
|
1132
|
+
env,
|
|
1133
|
+
logger,
|
|
1134
|
+
// The tool's blob policy. ADR 011 sharp edge closed: a blob referenced
|
|
1135
|
+
// by a live version manifest of the tool's resource is not deletable
|
|
1136
|
+
// through the generic files surface; SL-B1: the generic files surface
|
|
1137
|
+
// authorizes per RESOURCE, not per workspace — the ADR 013 policy expressed
|
|
1138
|
+
// as a WHERE predicate.
|
|
1139
|
+
...tool.api.filePolicy(deps.domain),
|
|
1140
|
+
fileDeleteRoute: tool.scopes.contractRoutes.fileDeleteRoute,
|
|
1141
|
+
fileInUse: tool.copy.fileInUse
|
|
1142
|
+
});
|
|
1143
|
+
// The tool's own routes: AFTER the files routes, BEFORE the OpenAPI
|
|
1144
|
+
// document and the JSON 404. The tool keeps its own internal order.
|
|
1145
|
+
const routesContext = { ...rateLimitContext, instanceId };
|
|
1146
|
+
tool.api.routes?.(api, routesContext, deps.domain);
|
|
1147
|
+
// Generated ONCE here, at the end of route registration — never per
|
|
1148
|
+
// request (api/openapi-doc.ts explains why that mattered).
|
|
1149
|
+
registerOpenApiDoc(api, {
|
|
1150
|
+
openapi: '3.1.0',
|
|
1151
|
+
info: {
|
|
1152
|
+
title: 'Platform API',
|
|
1153
|
+
version: env.APP_VERSION
|
|
1154
|
+
},
|
|
1155
|
+
servers: [{ url: '/api/v1' }]
|
|
1156
|
+
});
|
|
1157
|
+
// ── JSON 404 terminator — registered LAST ────────────────────────────────
|
|
1158
|
+
// An unmatched /api/v1 path answers the API wire shape instead of falling
|
|
1159
|
+
// through to the SPA catch-all (which would hand a browser session the
|
|
1160
|
+
// dashboard HTML with a 200). Machine principals never reach this: the
|
|
1161
|
+
// fail-closed scope gate already 403s every unlisted endpoint above.
|
|
1162
|
+
api.all('*', (c) => c.json(err('not_found', 'Not found'), 404));
|
|
1163
|
+
return api;
|
|
1164
|
+
}
|
|
1165
|
+
//# sourceMappingURL=create-api.js.map
|