@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,432 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RateLimiterMemory,
|
|
3
|
+
RateLimiterRedis,
|
|
4
|
+
RateLimiterRes,
|
|
5
|
+
type RateLimiterAbstract
|
|
6
|
+
} from 'rate-limiter-flexible';
|
|
7
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
8
|
+
import { getConnInfo } from '@hono/node-server/conninfo';
|
|
9
|
+
import type { EntitlementService, Principal } from '@antasphere/chassis-contract';
|
|
10
|
+
import type { Env } from '../env.js';
|
|
11
|
+
import type { Logger } from '../logger.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Auth-surface rate limits (ours, not Better Auth's built-in): login, OTP
|
|
15
|
+
* sends, invitation acceptance, setup attempts, API-key verification
|
|
16
|
+
* failures. Memory backend per replica by default; Redis when REDIS_URL is
|
|
17
|
+
* set so limits hold across replicas (Profile B).
|
|
18
|
+
*/
|
|
19
|
+
/** Workspace-creation attempts one person gets per hour; an address gets ten times that. */
|
|
20
|
+
export const WORKSPACE_CREATE_PER_PERSON = 60;
|
|
21
|
+
|
|
22
|
+
export interface RateLimiters {
|
|
23
|
+
login: RateLimiterAbstract;
|
|
24
|
+
otp: RateLimiterAbstract;
|
|
25
|
+
invitationAccept: RateLimiterAbstract;
|
|
26
|
+
setup: RateLimiterAbstract;
|
|
27
|
+
apiKeyFailures: RateLimiterAbstract;
|
|
28
|
+
/**
|
|
29
|
+
* CLI connect (`POST /sso/cli-connect`, cloud): per client address, sized by
|
|
30
|
+
* CLI_CONNECT_IP_LIMIT_PER_15_MIN for a room behind one address (PRDCT-2958,
|
|
31
|
+
* the lane E verifier's F1: on the login wall, 10 per 15 minutes, the 11th
|
|
32
|
+
* `<tool> login` of a venue was refused). The exchange token it presents is
|
|
33
|
+
* one-time and minted by the hub for one person, behind the hub's own walls.
|
|
34
|
+
*/
|
|
35
|
+
cliConnect: RateLimiterAbstract;
|
|
36
|
+
/** RFC 7591 dynamic client registration — unauthenticated by design, so tight. */
|
|
37
|
+
oauthRegister: RateLimiterAbstract;
|
|
38
|
+
/** OAuth token endpoint — a full connector dance is ~8 requests, so generous. */
|
|
39
|
+
oauthToken: RateLimiterAbstract;
|
|
40
|
+
/** The bundled /mcp endpoint — MCP clients are chatty, so generous per IP. */
|
|
41
|
+
mcp: RateLimiterAbstract;
|
|
42
|
+
/** Password reset requests — per IP AND per email, tight (abuse + enumeration). */
|
|
43
|
+
passwordReset: RateLimiterAbstract;
|
|
44
|
+
/** Full-workspace export — expensive streams, keyed per IP AND per user. */
|
|
45
|
+
workspaceExport: RateLimiterAbstract;
|
|
46
|
+
/** Break-glass superadmin recovery — a rare operator action, tight per IP. */
|
|
47
|
+
breakGlass: RateLimiterAbstract;
|
|
48
|
+
/**
|
|
49
|
+
* Workspace creation, the PER-PERSON bucket (key `u:<userId>`) — a rare
|
|
50
|
+
* human act; on cloud every attempt is a call to the hub, and the cloud
|
|
51
|
+
* zero-membership session resolves no principal, so the general quota
|
|
52
|
+
* never sees it. Spent by the handler (api/workspaces.ts) once the caller
|
|
53
|
+
* is IDENTIFIED — never by an unauthenticated request, never by a method
|
|
54
|
+
* that is not the POST.
|
|
55
|
+
*/
|
|
56
|
+
workspaceCreate: RateLimiterAbstract;
|
|
57
|
+
/**
|
|
58
|
+
* Workspace creation, the PER-ADDRESS bucket — ten people's worth of the
|
|
59
|
+
* per-person one, spent only by identified callers the per-person bucket
|
|
60
|
+
* let through. One colleague can therefore take at most a tenth of it:
|
|
61
|
+
* an office behind one NAT address is never locked out by one person,
|
|
62
|
+
* while a farm of accounts behind one address is still bounded.
|
|
63
|
+
*/
|
|
64
|
+
workspaceCreateAddress: RateLimiterAbstract;
|
|
65
|
+
/** The unauthenticated OpenAPI document — cheap now that it is a boot-time buffer, but still anonymous. */
|
|
66
|
+
openapiDoc: RateLimiterAbstract;
|
|
67
|
+
/**
|
|
68
|
+
* The demo pass redeem (demo sign-in, self-hosted only): an anonymous
|
|
69
|
+
* presentation of a sign-in-equivalent secret, so a guessing wall PER
|
|
70
|
+
* ADDRESS and nothing else — never keyed on the pass, whose legitimate
|
|
71
|
+
* audience it would cap. Mounted only while DEMO_SIGN_IN is on.
|
|
72
|
+
*/
|
|
73
|
+
demoRedeem: RateLimiterAbstract;
|
|
74
|
+
/**
|
|
75
|
+
* Shared factory riding the same backend (Redis when REDIS_URL is set,
|
|
76
|
+
* memory otherwise) for buckets sized at runtime — the per-principal
|
|
77
|
+
* request quota creates one limiter per distinct quota tier through this.
|
|
78
|
+
*/
|
|
79
|
+
make: (prefix: string, points: number, durationSec: number) => RateLimiterAbstract;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A tool's own bucket: the three arguments the shared `make` factory is
|
|
84
|
+
* called with. The store prefix is observable (`rl:<prefix>` in Redis), so a
|
|
85
|
+
* declaration is the bucket's identity, not only its size.
|
|
86
|
+
*/
|
|
87
|
+
export interface BucketDeclaration {
|
|
88
|
+
prefix: string;
|
|
89
|
+
points: number;
|
|
90
|
+
durationSec: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The chassis buckets plus the TOOL's own (`toolBuckets`, name → declaration),
|
|
95
|
+
* all built by the same `make`: one store (Redis when REDIS_URL is set, memory
|
|
96
|
+
* otherwise), one key-prefix scheme. A tool bucket can never take a chassis
|
|
97
|
+
* bucket's name — the type refuses it.
|
|
98
|
+
*/
|
|
99
|
+
export async function createRateLimiters<TBucket extends string = never>(
|
|
100
|
+
env: Pick<Env, 'REDIS_URL'> & Partial<Pick<Env, 'CLI_CONNECT_IP_LIMIT_PER_15_MIN'>>,
|
|
101
|
+
logger: Logger,
|
|
102
|
+
toolBuckets?: { [K in TBucket]: K extends keyof RateLimiters ? never : BucketDeclaration }
|
|
103
|
+
): Promise<RateLimiters & Record<TBucket, RateLimiterAbstract>> {
|
|
104
|
+
let make: (prefix: string, points: number, durationSec: number) => RateLimiterAbstract;
|
|
105
|
+
|
|
106
|
+
if (env.REDIS_URL) {
|
|
107
|
+
const { Redis } = await import('ioredis');
|
|
108
|
+
const client = new Redis(env.REDIS_URL, { enableOfflineQueue: false });
|
|
109
|
+
client.on('error', (err: Error) => logger.error({ err }, 'redis rate-limit store error'));
|
|
110
|
+
make = (prefix, points, duration) =>
|
|
111
|
+
new RateLimiterRedis({
|
|
112
|
+
storeClient: client,
|
|
113
|
+
keyPrefix: `rl:${prefix}`,
|
|
114
|
+
points,
|
|
115
|
+
duration,
|
|
116
|
+
// If Redis blips, fail open to memory rather than dropping requests.
|
|
117
|
+
insuranceLimiter: new RateLimiterMemory({ points, duration })
|
|
118
|
+
});
|
|
119
|
+
logger.info('rate limiting backed by redis');
|
|
120
|
+
} else {
|
|
121
|
+
make = (prefix, points, duration) =>
|
|
122
|
+
new RateLimiterMemory({ keyPrefix: `rl:${prefix}`, points, duration });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const chassis = {
|
|
126
|
+
login: make('login', 10, 15 * 60),
|
|
127
|
+
otp: make('otp', 5, 10 * 60),
|
|
128
|
+
invitationAccept: make('inv-accept', 10, 60 * 60),
|
|
129
|
+
setup: make('setup', 5, 60 * 60),
|
|
130
|
+
apiKeyFailures: make('key-fail', 20, 15 * 60),
|
|
131
|
+
cliConnect: make('cli-connect', Math.max(1, env.CLI_CONNECT_IP_LIMIT_PER_15_MIN ?? 300), 15 * 60),
|
|
132
|
+
oauthRegister: make('oauth-dcr', 10, 60 * 60),
|
|
133
|
+
oauthToken: make('oauth-token', 60, 60),
|
|
134
|
+
// Per ADDRESS: an event's teams' agents share one venue address (2026-10-01: ten times 120).
|
|
135
|
+
mcp: make('mcp', 1200, 60),
|
|
136
|
+
passwordReset: make('pw-reset', 5, 10 * 60),
|
|
137
|
+
workspaceExport: make('ws-export', 5, 600),
|
|
138
|
+
breakGlass: make('break-glass', 10, 60 * 60),
|
|
139
|
+
workspaceCreate: make('ws-create', WORKSPACE_CREATE_PER_PERSON, 60 * 60),
|
|
140
|
+
workspaceCreateAddress: make('ws-create-ip', WORKSPACE_CREATE_PER_PERSON * 10, 60 * 60),
|
|
141
|
+
openapiDoc: make('openapi', 60, 60),
|
|
142
|
+
demoRedeem: make('demo-redeem', 20, 15 * 60)
|
|
143
|
+
} satisfies Omit<RateLimiters, 'make'>;
|
|
144
|
+
// The tool's buckets are built after the chassis ones, in declaration order.
|
|
145
|
+
const tool = {} as Record<TBucket, RateLimiterAbstract>;
|
|
146
|
+
for (const [name, d] of Object.entries(toolBuckets ?? {}) as Array<[TBucket, BucketDeclaration]>) {
|
|
147
|
+
tool[name] = make(d.prefix, d.points, d.durationSec);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { ...chassis, ...tool, make };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The client-identity function: what keys a per-IP bucket and the audit rows. */
|
|
154
|
+
export type ClientIpFn = (c: Context) => string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Client identity for buckets and audit. x-forwarded-for is only believed
|
|
158
|
+
* when TRUST_PROXY says a proxy we control sets it; otherwise the socket
|
|
159
|
+
* address wins and spoofed headers are ignored. We read the RIGHTMOST hop —
|
|
160
|
+
* the one appended (or set) by the trusted proxy itself; the leftmost is
|
|
161
|
+
* client-claimed and spoofable whenever the proxy appends rather than
|
|
162
|
+
* overwrites. Note the compose caveat: without a proxy, Docker NAT can
|
|
163
|
+
* collapse external clients onto one address — per-IP limits are only as
|
|
164
|
+
* granular as what the socket sees.
|
|
165
|
+
*/
|
|
166
|
+
export function makeClientIp(trustProxy: boolean): ClientIpFn {
|
|
167
|
+
return (c) => {
|
|
168
|
+
if (trustProxy) {
|
|
169
|
+
const hops = c.req.header('x-forwarded-for')?.split(',');
|
|
170
|
+
const xff = hops?.[hops.length - 1]?.trim();
|
|
171
|
+
if (xff) return xff;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return getConnInfo(c).remote.address ?? 'unknown';
|
|
175
|
+
} catch {
|
|
176
|
+
// No socket (tests via app.request()) — one shared bucket.
|
|
177
|
+
return 'unknown';
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 429 with the standard wire shape when the bucket is empty. */
|
|
183
|
+
/**
|
|
184
|
+
* WHEN a request costs a point.
|
|
185
|
+
*
|
|
186
|
+
* - `arrival` (default): consume before the handler runs. Correct for
|
|
187
|
+
* everything whose cost IS the arrival — sending mail, accepting an
|
|
188
|
+
* invitation, registering an OAuth client, running setup.
|
|
189
|
+
* - `failure`: check the bucket before the handler (an exhausted bucket
|
|
190
|
+
* still 429s immediately) but consume only when the handler answered a
|
|
191
|
+
* client error. Correct for CREDENTIAL-VERIFICATION walls: on arrival, the
|
|
192
|
+
* limiter cannot yet know whether the caller is the account owner, so an
|
|
193
|
+
* email-keyed bucket consumed up front lets anyone who knows an address
|
|
194
|
+
* spend that account's whole budget and lock its owner out — including on
|
|
195
|
+
* the requests the owner themselves gets right. Consuming on failure keeps
|
|
196
|
+
* the brute-force wall exactly as tight (a guess is a failure) while a
|
|
197
|
+
* legitimate sign-in costs nothing.
|
|
198
|
+
*/
|
|
199
|
+
export interface RateLimitOptions {
|
|
200
|
+
consumeOn?: 'arrival' | 'failure';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const RATE_LIMITED_BODY = {
|
|
204
|
+
error: { code: 'rate_limited', message: 'Too many requests, slow down' }
|
|
205
|
+
} as const;
|
|
206
|
+
|
|
207
|
+
export function rateLimit(
|
|
208
|
+
limiter: RateLimiterAbstract,
|
|
209
|
+
clientIp: ClientIpFn,
|
|
210
|
+
extraKeys?: (c: Context) => Promise<string[]>,
|
|
211
|
+
options: RateLimitOptions = {}
|
|
212
|
+
): MiddlewareHandler {
|
|
213
|
+
const consumeOn = options.consumeOn ?? 'arrival';
|
|
214
|
+
return async (c, next) => {
|
|
215
|
+
const keys = [clientIp(c), ...(extraKeys ? await extraKeys(c) : [])];
|
|
216
|
+
|
|
217
|
+
if (consumeOn === 'arrival') {
|
|
218
|
+
try {
|
|
219
|
+
for (const key of keys) {
|
|
220
|
+
await limiter.consume(key);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
return c.json(RATE_LIMITED_BODY, 429);
|
|
224
|
+
}
|
|
225
|
+
return next();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Failure-only: read the buckets (no consume) and refuse an exhausted one
|
|
229
|
+
// BEFORE the handler, so a drained wall still costs an attacker nothing to
|
|
230
|
+
// hit and no credential check runs. Backend errors fail open, matching
|
|
231
|
+
// every other limiter here.
|
|
232
|
+
for (const key of keys) {
|
|
233
|
+
const state = await limiter.get(key).catch(() => null);
|
|
234
|
+
if (state && state.remainingPoints <= 0 && state.msBeforeNext > 0) {
|
|
235
|
+
return c.json(RATE_LIMITED_BODY, 429);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await next();
|
|
239
|
+
// 4xx/5xx = the credential was not accepted. Better Auth answers 401 for a
|
|
240
|
+
// bad password, 400 for a malformed attempt, 403 for a refused origin —
|
|
241
|
+
// all of them are attempts that must cost. A 429 is not: it is a wall
|
|
242
|
+
// behind this one (the library's own 3-per-10-seconds on `/sign-in*`)
|
|
243
|
+
// saying "slow down", no credential was judged. Counting it let a burst of
|
|
244
|
+
// ordinary sign-ins from one address (a room behind one public IP) spend
|
|
245
|
+
// this 15-minute wall in seconds (PRDCT-2830, PRDCT-2848).
|
|
246
|
+
if (c.res.status >= 400 && c.res.status !== 429) {
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
await limiter.consume(key).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Per-email key for OTP sends and password login (per IP AND per email): the
|
|
256
|
+
* email key keeps per-account protection intact when NAT or a rotating
|
|
257
|
+
* attacker collapses/expands the IP dimension.
|
|
258
|
+
*/
|
|
259
|
+
export async function emailKeyOf(c: Context): Promise<string[]> {
|
|
260
|
+
try {
|
|
261
|
+
const body = (await c.req.raw.clone().json()) as { email?: string };
|
|
262
|
+
if (body?.email && typeof body.email === 'string') {
|
|
263
|
+
return [`email:${body.email.toLowerCase().trim()}`];
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// non-JSON body — the IP key alone applies
|
|
267
|
+
}
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── General per-principal request quota (I3) ────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/** Outcome of one quota consume — everything the HTTP layer needs for headers. */
|
|
274
|
+
export interface QuotaDecision {
|
|
275
|
+
ok: boolean;
|
|
276
|
+
/** The sustained per-minute limit (what RateLimit-Limit reports). */
|
|
277
|
+
limit: number;
|
|
278
|
+
remaining: number;
|
|
279
|
+
/** Seconds until the sustained window resets. */
|
|
280
|
+
resetSeconds: number;
|
|
281
|
+
/** Present when ok=false: seconds the client should wait before retrying. */
|
|
282
|
+
retryAfterSeconds?: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface RequestQuotaService {
|
|
286
|
+
/** Consume one request for this principal; null = quota disabled (no headers). */
|
|
287
|
+
consume(principal: Principal): Promise<QuotaDecision | null>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** RateLimit draft headers + the X- legacy mirror; reset values are delta seconds. */
|
|
291
|
+
export function quotaHeaderEntries(d: QuotaDecision): Array<[string, string]> {
|
|
292
|
+
const remaining = String(Math.max(0, d.remaining));
|
|
293
|
+
const reset = String(Math.max(0, d.resetSeconds));
|
|
294
|
+
const limit = String(d.limit);
|
|
295
|
+
return [
|
|
296
|
+
['RateLimit-Limit', limit],
|
|
297
|
+
['RateLimit-Remaining', remaining],
|
|
298
|
+
['RateLimit-Reset', reset],
|
|
299
|
+
['X-RateLimit-Limit', limit],
|
|
300
|
+
['X-RateLimit-Remaining', remaining],
|
|
301
|
+
['X-RateLimit-Reset', reset]
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Bucket key for the general API quota, derived ONLY from the authenticated
|
|
307
|
+
* principal — never from a client-supplied header, so a caller can neither
|
|
308
|
+
* rotate its own bucket nor fill another principal's. Two API keys of the
|
|
309
|
+
* same user are separate buckets (a leaked key can be throttled/revoked
|
|
310
|
+
* without starving the owner's other integrations); the same user's session
|
|
311
|
+
* is a third.
|
|
312
|
+
*/
|
|
313
|
+
export function principalBucketKey(p: Principal): string {
|
|
314
|
+
if (p.via === 'api_key') return `key:${p.apiKeyId ?? p.userId}`;
|
|
315
|
+
if (p.via === 'oauth') return `oauth:${p.userId}`;
|
|
316
|
+
return `session:${p.userId}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface RequestQuotaDeps {
|
|
320
|
+
entitlements: EntitlementService;
|
|
321
|
+
/** The shared limiter factory from createRateLimiters — Redis-backed when configured. */
|
|
322
|
+
make: RateLimiters['make'];
|
|
323
|
+
logger: Logger;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Per-principal request quota over the shared rate-limit backend. Two buckets
|
|
328
|
+
* per principal: a sustained per-minute window (the quota agents budget
|
|
329
|
+
* against — headers describe this one) and a 1-second spike cap that smooths
|
|
330
|
+
* thundering herds without burning sustained points on rejected spikes.
|
|
331
|
+
*
|
|
332
|
+
* Limits come from EntitlementService.getRequestQuota so a plan-aware edition
|
|
333
|
+
* varies them per principal; limiter instances are created lazily per
|
|
334
|
+
* distinct quota size (a product has a handful of plans, not thousands) and
|
|
335
|
+
* the size is part of the store prefix, so a plan change moves the principal
|
|
336
|
+
* to a fresh bucket instead of misreading the old counter.
|
|
337
|
+
*
|
|
338
|
+
* Failure posture matches the auth-surface limiters: Redis outages fall back
|
|
339
|
+
* to the per-replica insurance limiter inside `make`; any other backend or
|
|
340
|
+
* entitlement failure logs and FAILS OPEN (availability over throttling — the
|
|
341
|
+
* quota is a cost control, not an authz boundary; authz stays fail-closed).
|
|
342
|
+
*/
|
|
343
|
+
export function createRequestQuota({ entitlements, make, logger }: RequestQuotaDeps): RequestQuotaService {
|
|
344
|
+
const sustained = new Map<number, RateLimiterAbstract>();
|
|
345
|
+
const burst = new Map<number, RateLimiterAbstract>();
|
|
346
|
+
const limiterFor = (
|
|
347
|
+
cache: Map<number, RateLimiterAbstract>,
|
|
348
|
+
prefix: string,
|
|
349
|
+
points: number,
|
|
350
|
+
durationSec: number
|
|
351
|
+
): RateLimiterAbstract => {
|
|
352
|
+
let limiter = cache.get(points);
|
|
353
|
+
if (!limiter) {
|
|
354
|
+
limiter = make(`${prefix}:${points}`, points, durationSec);
|
|
355
|
+
cache.set(points, limiter);
|
|
356
|
+
}
|
|
357
|
+
return limiter;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
async consume(principal) {
|
|
362
|
+
let perMinute: number;
|
|
363
|
+
let burstPerSecond: number;
|
|
364
|
+
try {
|
|
365
|
+
({ perMinute, burstPerSecond } = await entitlements.getRequestQuota(principal));
|
|
366
|
+
} catch (err) {
|
|
367
|
+
logger.error({ err }, 'request quota lookup failed — allowing request');
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
// Guard a product's custom (non-TS-checked) EntitlementService returning
|
|
371
|
+
// a non-numeric quota: a bare `undefined` would slip past `<= 0` and hand
|
|
372
|
+
// rate-limiter-flexible `points: undefined` (a surprise 4/min). Fail open.
|
|
373
|
+
if (!Number.isFinite(perMinute) || !Number.isFinite(burstPerSecond)) {
|
|
374
|
+
logger.error(
|
|
375
|
+
{ perMinute, burstPerSecond },
|
|
376
|
+
'request quota returned a non-numeric value — allowing request'
|
|
377
|
+
);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (perMinute <= 0) return null; // 0 = unlimited, a conscious operator opt-out
|
|
381
|
+
|
|
382
|
+
const key = principalBucketKey(principal);
|
|
383
|
+
const sustainedLimiter = limiterFor(sustained, 'apiq', perMinute, 60);
|
|
384
|
+
|
|
385
|
+
// Spike cap first: a rejected burst must not burn sustained points —
|
|
386
|
+
// the 1 s bucket self-heals and the minute budget stays intact.
|
|
387
|
+
if (burstPerSecond > 0) {
|
|
388
|
+
try {
|
|
389
|
+
await limiterFor(burst, 'apiq-burst', burstPerSecond, 1).consume(key);
|
|
390
|
+
} catch (rejection) {
|
|
391
|
+
if (!(rejection instanceof RateLimiterRes)) {
|
|
392
|
+
logger.error({ err: rejection }, 'rate-limit backend failure — allowing request');
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
// Report the sustained bucket's state (read-only) so headers stay
|
|
396
|
+
// meaningful; the retry hint comes from the 1 s burst window.
|
|
397
|
+
const state = await sustainedLimiter.get(key).catch(() => null);
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
limit: perMinute,
|
|
401
|
+
remaining: state ? Math.max(0, state.remainingPoints) : perMinute,
|
|
402
|
+
resetSeconds: Math.ceil((state?.msBeforeNext ?? 0) / 1000),
|
|
403
|
+
retryAfterSeconds: Math.max(1, Math.ceil(rejection.msBeforeNext / 1000))
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const res = await sustainedLimiter.consume(key);
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
limit: perMinute,
|
|
413
|
+
remaining: res.remainingPoints,
|
|
414
|
+
resetSeconds: Math.ceil(res.msBeforeNext / 1000)
|
|
415
|
+
};
|
|
416
|
+
} catch (rejection) {
|
|
417
|
+
if (!(rejection instanceof RateLimiterRes)) {
|
|
418
|
+
logger.error({ err: rejection }, 'rate-limit backend failure — allowing request');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const retry = Math.max(1, Math.ceil(rejection.msBeforeNext / 1000));
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
limit: perMinute,
|
|
425
|
+
remaining: 0,
|
|
426
|
+
resetSeconds: retry,
|
|
427
|
+
retryAfterSeconds: retry
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import type { Logger } from '../logger.js';
|
|
4
|
+
import { routeLabel } from '../route-label.js';
|
|
5
|
+
|
|
6
|
+
declare module 'hono' {
|
|
7
|
+
interface ContextVariableMap {
|
|
8
|
+
requestId: string;
|
|
9
|
+
logger: Logger;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* First middleware in the chain: binds a request id (propagated from
|
|
15
|
+
* x-request-id when a proxy already assigned one) and a child logger carrying
|
|
16
|
+
* it, and writes the completion log line with latency. Everything downstream
|
|
17
|
+
* (traces, audit entries) reuses this id, which is what makes one request
|
|
18
|
+
* followable across log, trace, and audit.
|
|
19
|
+
*/
|
|
20
|
+
/** Propagated ids must be sane: bounded length, log-safe charset. */
|
|
21
|
+
const REQUEST_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
22
|
+
|
|
23
|
+
export function requestId(base: Logger): MiddlewareHandler {
|
|
24
|
+
return async (c, next) => {
|
|
25
|
+
const incoming = c.req.header('x-request-id');
|
|
26
|
+
const id = incoming && REQUEST_ID_RE.test(incoming) ? incoming : randomUUID();
|
|
27
|
+
const log = base.child({ requestId: id });
|
|
28
|
+
c.set('requestId', id);
|
|
29
|
+
c.set('logger', log);
|
|
30
|
+
c.header('x-request-id', id);
|
|
31
|
+
const start = performance.now();
|
|
32
|
+
try {
|
|
33
|
+
await next();
|
|
34
|
+
} finally {
|
|
35
|
+
const latencyMs = Math.round((performance.now() - start) * 100) / 100;
|
|
36
|
+
log.info(
|
|
37
|
+
{
|
|
38
|
+
method: c.req.method,
|
|
39
|
+
// The MATCHED ROUTE PATTERN, never the raw URL path — see
|
|
40
|
+
// route-label.ts. On THIS repo that is not hypothetical: the public
|
|
41
|
+
// viewer is `/v/:secret`, so logging c.req.path wrote every live
|
|
42
|
+
// share-link capability into the log stream, where redaction (which
|
|
43
|
+
// keys off object properties) can do nothing about one opaque
|
|
44
|
+
// string. Read after next() so routing has resolved.
|
|
45
|
+
path: routeLabel(c),
|
|
46
|
+
status: c.error ? 500 : c.res.status,
|
|
47
|
+
latencyMs,
|
|
48
|
+
...(c.error ? { failed: true } : {})
|
|
49
|
+
},
|
|
50
|
+
'request'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed endpoint allowlist for machine principals (API keys now, OAuth
|
|
3
|
+
* bearer tokens in M6). Ported from a predecessor template's oauth-scopes.ts.
|
|
4
|
+
*
|
|
5
|
+
* Machine principals are deliberately MORE restricted than the user they
|
|
6
|
+
* belong to: they can only reach endpoints listed here, each behind its
|
|
7
|
+
* scope. Anything not listed returns null and the gate 403s — new endpoints
|
|
8
|
+
* are unreachable to keys/tokens until someone consciously opens them.
|
|
9
|
+
* Session principals are role-gated instead and never pass through this.
|
|
10
|
+
*
|
|
11
|
+
* Pure logic (no I/O) so it stays unit-testable. The chassis owns the
|
|
12
|
+
* MECHANISM and the rules of its own endpoints; the scope NAMES and the rules
|
|
13
|
+
* of the tool's endpoints are the tool's (`createScopeAllowlist`).
|
|
14
|
+
*/
|
|
15
|
+
export function looksLikeJwt(token: string): boolean {
|
|
16
|
+
return token.split('.').length === 3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One tool rule: the scope a machine principal needs for this request, or
|
|
21
|
+
* null when the rule does not speak about it (the next rule is asked; no rule
|
|
22
|
+
* speaking = denied).
|
|
23
|
+
*/
|
|
24
|
+
export type ScopeRule<S extends string> = (path: string, method: string, isRead: boolean) => S | null;
|
|
25
|
+
|
|
26
|
+
export interface ScopeAllowlistOptions<S extends string> {
|
|
27
|
+
/** The tool's scope behind the chassis READ surfaces (`/me`, the files reads). */
|
|
28
|
+
read: S;
|
|
29
|
+
/** The tool's scope behind the chassis MUTATIONS (files writes, the CLI key self-revoke). */
|
|
30
|
+
write: S;
|
|
31
|
+
/** The tool's dedicated opt-in scope for the full-workspace export. */
|
|
32
|
+
dataExport: S;
|
|
33
|
+
/** The tool's own rules, asked in order AFTER the chassis rules. */
|
|
34
|
+
rules: ReadonlyArray<ScopeRule<S>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the allowlist function: the chassis rules FIRST, then the tool's
|
|
39
|
+
* rules in order, and `null` (the gate's 403 `endpoint_not_allowed`) when
|
|
40
|
+
* nothing matched — fail-closed by construction.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* The chassis project routes a machine principal may reach, EXACT shapes only:
|
|
44
|
+
* reads under the read scope, writes under the write scope. Anything else
|
|
45
|
+
* under `/projects` answers null HERE, so a tool's own route on a project (its
|
|
46
|
+
* link to the project, say) is the tool's rule to open, and an unknown shape
|
|
47
|
+
* stays closed.
|
|
48
|
+
*/
|
|
49
|
+
const PROJECT_ID = '[0-9a-fA-F-]{36}';
|
|
50
|
+
const PROJECT_RE = new RegExp(`^/api/v1/projects/${PROJECT_ID}$`);
|
|
51
|
+
const PROJECT_ARCHIVE_RE = new RegExp(`^/api/v1/projects/${PROJECT_ID}/(?:archive|unarchive)$`);
|
|
52
|
+
const PROJECT_MEMBERS_RE = new RegExp(`^/api/v1/projects/${PROJECT_ID}/members$`);
|
|
53
|
+
const PROJECT_MEMBER_RE = new RegExp(`^/api/v1/projects/${PROJECT_ID}/members/[^/]+$`);
|
|
54
|
+
const PROJECT_TEAM_RE = new RegExp(`^/api/v1/projects/${PROJECT_ID}/teams/${PROJECT_ID}$`);
|
|
55
|
+
|
|
56
|
+
function projectAccess(path: string, method: string): 'read' | 'write' | null {
|
|
57
|
+
if (path === '/api/v1/projects') return method === 'GET' ? 'read' : method === 'POST' ? 'write' : null;
|
|
58
|
+
if (PROJECT_RE.test(path)) return method === 'GET' ? 'read' : method === 'PATCH' ? 'write' : null;
|
|
59
|
+
if (PROJECT_ARCHIVE_RE.test(path)) return method === 'POST' ? 'write' : null;
|
|
60
|
+
if (PROJECT_MEMBERS_RE.test(path)) return method === 'GET' ? 'read' : method === 'POST' ? 'write' : null;
|
|
61
|
+
if (PROJECT_MEMBER_RE.test(path)) return method === 'PATCH' || method === 'DELETE' ? 'write' : null;
|
|
62
|
+
if (PROJECT_TEAM_RE.test(path)) return method === 'PATCH' || method === 'DELETE' ? 'write' : null;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The chassis team routes a machine principal may reach (PRDCT-2813), EXACT
|
|
68
|
+
* shapes only, the projects' pattern: the reads under the read scope, the
|
|
69
|
+
* writes under the write scope, the handler's owner-or-admin check staying
|
|
70
|
+
* the authority (and the hub-managed gate the refusal in a hub-origin
|
|
71
|
+
* workspace). A person is named by their user id in the path, never by a
|
|
72
|
+
* membership id.
|
|
73
|
+
*/
|
|
74
|
+
const TEAM_RE = new RegExp(`^/api/v1/teams/${PROJECT_ID}$`);
|
|
75
|
+
const TEAM_MEMBERS_RE = new RegExp(`^/api/v1/teams/${PROJECT_ID}/members$`);
|
|
76
|
+
const TEAM_MEMBER_RE = new RegExp(`^/api/v1/teams/${PROJECT_ID}/members/[^/]+$`);
|
|
77
|
+
|
|
78
|
+
function teamAccess(path: string, method: string): 'read' | 'write' | null {
|
|
79
|
+
if (path === '/api/v1/teams') return method === 'GET' ? 'read' : method === 'POST' ? 'write' : null;
|
|
80
|
+
if (TEAM_RE.test(path)) {
|
|
81
|
+
return method === 'GET' ? 'read' : method === 'PATCH' || method === 'DELETE' ? 'write' : null;
|
|
82
|
+
}
|
|
83
|
+
if (TEAM_MEMBERS_RE.test(path)) return method === 'GET' ? 'read' : method === 'POST' ? 'write' : null;
|
|
84
|
+
if (TEAM_MEMBER_RE.test(path)) return method === 'DELETE' ? 'write' : null;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createScopeAllowlist<S extends string>({
|
|
89
|
+
read,
|
|
90
|
+
write,
|
|
91
|
+
dataExport,
|
|
92
|
+
rules
|
|
93
|
+
}: ScopeAllowlistOptions<S>): (path: string, method: string) => S | null {
|
|
94
|
+
return function requiredScopeFor(path: string, method: string): S | null {
|
|
95
|
+
const isRead = method === 'GET' || method === 'HEAD';
|
|
96
|
+
if (path === '/api/v1/me' && isRead) return read;
|
|
97
|
+
// Files: the substrate machine callers actually need (agents pushing and
|
|
98
|
+
// pulling artifacts). Reads → the read scope, mutations → the write scope.
|
|
99
|
+
if (path === '/api/v1/files' || path.startsWith('/api/v1/files/')) {
|
|
100
|
+
return isRead ? read : write;
|
|
101
|
+
}
|
|
102
|
+
// CLI logout self-revoke (api/cli-auth.ts): the ONE /cli/auth route open
|
|
103
|
+
// to machines — DELETE revokes exactly the PRESENTING key. Safe to open
|
|
104
|
+
// because the route names no key (no id parameter): the handler acts only
|
|
105
|
+
// on principal.apiKeyId, resolved from the presented secret, and refuses
|
|
106
|
+
// every non-key principal — so the widest possible effect is a credential
|
|
107
|
+
// revoking ITSELF. The write scope because revocation is a mutation
|
|
108
|
+
// and every CLI-minted key carries it (mirrors the hub's account:write
|
|
109
|
+
// entry). Exact path + method match: any OTHER method on the path, and
|
|
110
|
+
// the PUBLIC mint routes (/cli/auth/request, /cli/auth/complete — which
|
|
111
|
+
// never reach this gate anyway), stay fail-closed.
|
|
112
|
+
if (path === '/api/v1/cli/auth/key' && method === 'DELETE') return write;
|
|
113
|
+
// The person's default workspace (api/workspaces.ts, PRDCT-2815): a
|
|
114
|
+
// setting of the PERSON a credential stands for, which the CLI sets.
|
|
115
|
+
// The route names no workspace and acts on the caller's own membership
|
|
116
|
+
// rows only; a key pinned to one workspace is refused by the handler.
|
|
117
|
+
// Exact path + method: nothing else under /me opens with it.
|
|
118
|
+
if (path === '/api/v1/me/default-workspace' && method === 'PUT') return write;
|
|
119
|
+
// Full-workspace export: a dedicated opt-in scope, NEVER the read scope — any
|
|
120
|
+
// admin read key would otherwise be a whole-tenant exfiltration tool.
|
|
121
|
+
// (Account deletion, DELETE /members/{id}, stays deliberately UNLISTED:
|
|
122
|
+
// machines 403 fail-closed; deleting people is a session-only act.
|
|
123
|
+
// Break-glass, /admin/break-glass/*, is likewise deliberately UNLISTED:
|
|
124
|
+
// superadmin recovery is a human session act — a key or token whose owner
|
|
125
|
+
// is on SUPERADMIN_EMAILS still 403s here, fail-closed. Never list it.)
|
|
126
|
+
if (path === '/api/v1/workspace/export' && isRead) return dataExport;
|
|
127
|
+
// Projects: each route listed by its exact shape and method (above).
|
|
128
|
+
const project = projectAccess(path, method);
|
|
129
|
+
if (project !== null) return project === 'read' ? read : write;
|
|
130
|
+
// Teams: the same discipline (above).
|
|
131
|
+
const team = teamAccess(path, method);
|
|
132
|
+
if (team !== null) return team === 'read' ? read : write;
|
|
133
|
+
for (const rule of rules) {
|
|
134
|
+
const needed = rule(path, method, isRead);
|
|
135
|
+
if (needed !== null) return needed;
|
|
136
|
+
}
|
|
137
|
+
// Deliberately UNLISTED chassis surfaces (fail-closed 403 for
|
|
138
|
+
// keys/tokens), like break-glass:
|
|
139
|
+
// - /sso/logout (cloud) — single logout is a BROWSER act: a machine
|
|
140
|
+
// credential must never be able to end its user's sessions. Never
|
|
141
|
+
// list it.
|
|
142
|
+
// - POST /workspaces — creating a workspace (on cloud: an organization at
|
|
143
|
+
// the hub, as the user) is a HUMAN act from the dashboard. A key or a
|
|
144
|
+
// token must never be able to mint tenants for its holder; the handler
|
|
145
|
+
// re-checks `via === 'session'` on top (api/workspaces.ts). Never list it.
|
|
146
|
+
// - /me/onboarding/dismiss (cloud) — the first-run welcome is a browser
|
|
147
|
+
// concern; machines carry no banner to dismiss.
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
}
|