@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,68 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { describeHubClockSkew, hubClockSkewMs } from '../../src/entitlements/clock-skew.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The hub clock probe (PRDCT-2644): one GET of the discovery document, the
|
|
6
|
+
* `Date` header read against the local clock; null on anything unreadable,
|
|
7
|
+
* never a throw; a sentence only past the hub's five-minute forward window.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ISSUER = 'https://hub.test/';
|
|
11
|
+
const NOW = Date.parse('2026-09-23T12:00:00.000Z');
|
|
12
|
+
const MIN = 60_000;
|
|
13
|
+
|
|
14
|
+
function answering(headers: Record<string, string>) {
|
|
15
|
+
const calls: Array<{ url: string; method: string | undefined }> = [];
|
|
16
|
+
const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
17
|
+
calls.push({ url: String(input), method: init?.method });
|
|
18
|
+
return new Response('{}', { status: 200, headers });
|
|
19
|
+
}) as typeof fetch;
|
|
20
|
+
return { calls, fetchImpl };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('hubClockSkewMs', () => {
|
|
24
|
+
it('a hub seven minutes behind reads as +7 minutes, and the sentence says ahead', async () => {
|
|
25
|
+
const f = answering({ date: new Date(NOW - 7 * MIN).toUTCString() });
|
|
26
|
+
const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
|
|
27
|
+
expect(skew).toBe(7 * MIN);
|
|
28
|
+
expect(f.calls).toEqual([{ url: 'https://hub.test/.well-known/openid-configuration', method: 'GET' }]);
|
|
29
|
+
const sentence = describeHubClockSkew(skew!);
|
|
30
|
+
expect(sentence).toBe(
|
|
31
|
+
"this instance's clock is 7 minutes ahead of the hub's; usage events it stamps would be refused by the hub's window"
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('a hub seven minutes ahead reads as −7 minutes, and the sentence says behind', async () => {
|
|
36
|
+
const f = answering({ date: new Date(NOW + 7 * MIN).toUTCString() });
|
|
37
|
+
const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
|
|
38
|
+
expect(skew).toBe(-7 * MIN);
|
|
39
|
+
expect(describeHubClockSkew(skew!)).toMatch(/7 minutes behind the hub's/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('within five minutes either way there is no sentence (the bound is inclusive)', async () => {
|
|
43
|
+
const f = answering({ date: new Date(NOW - 3 * MIN).toUTCString() });
|
|
44
|
+
const skew = await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW });
|
|
45
|
+
expect(skew).toBe(3 * MIN);
|
|
46
|
+
expect(describeHubClockSkew(skew!)).toBeNull();
|
|
47
|
+
expect(describeHubClockSkew(5 * MIN)).toBeNull();
|
|
48
|
+
expect(describeHubClockSkew(-5 * MIN)).toBeNull();
|
|
49
|
+
expect(describeHubClockSkew(5 * MIN + 1)).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('no Date header reads as null', async () => {
|
|
53
|
+
const f = answering({});
|
|
54
|
+
expect(await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW })).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('an unparseable Date header reads as null', async () => {
|
|
58
|
+
const f = answering({ date: 'not a date' });
|
|
59
|
+
expect(await hubClockSkewMs(ISSUER, { fetchImpl: f.fetchImpl, now: () => NOW })).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('a fetch that throws reads as null, never a throw', async () => {
|
|
63
|
+
const fetchImpl = (async () => {
|
|
64
|
+
throw new TypeError('fetch failed');
|
|
65
|
+
}) as unknown as typeof fetch;
|
|
66
|
+
await expect(hubClockSkewMs(ISSUER, { fetchImpl, now: () => NOW })).resolves.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { crossSiteGuard } from '@antasphere/chassis-server/middleware';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BROW-2: nothing under /api/v1 checked Origin or Sec-Fetch metadata, so every
|
|
7
|
+
* session-authenticated business route was drivable from any page the victim
|
|
8
|
+
* had open. Better Auth's own check covers only its `/sign-in` paths.
|
|
9
|
+
*/
|
|
10
|
+
function app(): Hono {
|
|
11
|
+
const api = new Hono();
|
|
12
|
+
api.use('*', crossSiteGuard({ publicBaseUrl: 'https://app.example.com' }));
|
|
13
|
+
api.get('/orgs', (c) => c.json({ read: true }));
|
|
14
|
+
api.post('/orgs', (c) => c.json({ created: true }, 201));
|
|
15
|
+
api.delete('/orgs/:id', (c) => c.json({ deleted: true }));
|
|
16
|
+
api.post('/auth/oauth2/token', (c) => c.json({ token: 'x' }));
|
|
17
|
+
api.post('/auth/sign-in/email', (c) => c.json({ signed: true }));
|
|
18
|
+
const root = new Hono();
|
|
19
|
+
root.route('/api/v1', api);
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const post = (headers: Record<string, string>) => app().request('/api/v1/orgs', { method: 'POST', headers });
|
|
24
|
+
|
|
25
|
+
describe('crossSiteGuard', () => {
|
|
26
|
+
it('rejects a cross-site credentialed POST before the handler', async () => {
|
|
27
|
+
const res = await post({
|
|
28
|
+
'sec-fetch-site': 'cross-site',
|
|
29
|
+
origin: 'https://evil.example',
|
|
30
|
+
cookie: 'session=abc'
|
|
31
|
+
});
|
|
32
|
+
expect(res.status).toBe(403);
|
|
33
|
+
expect(await res.json()).toMatchObject({ error: { code: 'cross_site_forbidden' } });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects an untrusted Origin even when Sec-Fetch metadata is absent (older browsers)', async () => {
|
|
37
|
+
const res = await post({ origin: 'https://evil.example', cookie: 'session=abc' });
|
|
38
|
+
expect(res.status).toBe(403);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects an opaque `Origin: null` (sandboxed iframe / redirected form post)', async () => {
|
|
42
|
+
const res = await post({ origin: 'null', cookie: 'session=abc' });
|
|
43
|
+
expect(res.status).toBe(403);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('rejects a same-site-but-different-origin POST from a sibling subdomain', async () => {
|
|
47
|
+
// Sec-Fetch-Site says `same-site`, so the cross-site arm does not fire —
|
|
48
|
+
// the Origin arm must. A host-only session cookie IS sent on these.
|
|
49
|
+
const res = await post({
|
|
50
|
+
'sec-fetch-site': 'same-site',
|
|
51
|
+
origin: 'https://marketing.example.com',
|
|
52
|
+
cookie: 'session=abc'
|
|
53
|
+
});
|
|
54
|
+
expect(res.status).toBe(403);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('covers every unsafe method, not just POST', async () => {
|
|
58
|
+
const res = await app().request('/api/v1/orgs/1', {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
headers: { 'sec-fetch-site': 'cross-site' }
|
|
61
|
+
});
|
|
62
|
+
expect(res.status).toBe(403);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('allows the dashboard: same-origin POST with the app Origin', async () => {
|
|
66
|
+
const res = await app().request('https://app.example.com/api/v1/orgs', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'sec-fetch-site': 'same-origin',
|
|
70
|
+
origin: 'https://app.example.com',
|
|
71
|
+
cookie: 'session=abc'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
expect(res.status).toBe(201);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('allows the serving origin even when it is not PUBLIC_BASE_URL (dev proxy, previews)', async () => {
|
|
78
|
+
const res = await app().request('http://localhost:5173/api/v1/orgs', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { origin: 'http://localhost:5173', 'sec-fetch-site': 'same-origin' }
|
|
81
|
+
});
|
|
82
|
+
expect(res.status).toBe(201);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('leaves non-browser clients alone (no Origin, no Sec-Fetch)', async () => {
|
|
86
|
+
const res = await post({ 'content-type': 'application/json' });
|
|
87
|
+
expect(res.status).toBe(201);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('never blocks bearer-authenticated calls — those are not ambient credentials', async () => {
|
|
91
|
+
const res = await post({
|
|
92
|
+
authorization: 'Bearer pk_live_something',
|
|
93
|
+
origin: 'https://some-mcp-client.example',
|
|
94
|
+
'sec-fetch-site': 'cross-site'
|
|
95
|
+
});
|
|
96
|
+
expect(res.status).toBe(201);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('exempts the deliberate wildcard-CORS OAuth endpoints', async () => {
|
|
100
|
+
const res = await app().request('/api/v1/auth/oauth2/token', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { origin: 'https://claude.ai', 'sec-fetch-site': 'cross-site' }
|
|
103
|
+
});
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does NOT exempt the cookie-bearing auth endpoints', async () => {
|
|
108
|
+
const res = await app().request('/api/v1/auth/sign-in/email', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { origin: 'https://evil.example', 'sec-fetch-site': 'cross-site' }
|
|
111
|
+
});
|
|
112
|
+
expect(res.status).toBe(403);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('leaves safe methods readable — CORS already stops the attacker seeing them', async () => {
|
|
116
|
+
const res = await app().request('/api/v1/orgs', {
|
|
117
|
+
headers: { 'sec-fetch-site': 'cross-site', origin: 'https://evil.example' }
|
|
118
|
+
});
|
|
119
|
+
expect(res.status).toBe(200);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('deniedOrigins — the viewer origin (PRDCT-1352)', () => {
|
|
123
|
+
const VIEWER = 'https://decks.example.net';
|
|
124
|
+
function gated(): Hono {
|
|
125
|
+
const api = new Hono();
|
|
126
|
+
api.use(
|
|
127
|
+
'*',
|
|
128
|
+
crossSiteGuard({
|
|
129
|
+
publicBaseUrl: 'https://app.example.com',
|
|
130
|
+
// Even listed as trusted, denial wins: an operator cannot re-open it by accident.
|
|
131
|
+
extraTrustedOrigins: [VIEWER],
|
|
132
|
+
deniedOrigins: [VIEWER]
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
api.post('/orgs', (c) => c.json({ created: true }, 201));
|
|
136
|
+
const root = new Hono();
|
|
137
|
+
root.route('/api/v1', api);
|
|
138
|
+
return root;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
it('refuses the viewer origin even when it IS the serving origin', async () => {
|
|
142
|
+
const res = await gated().request(`${VIEWER}/api/v1/orgs`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { origin: VIEWER, 'sec-fetch-site': 'same-origin', cookie: 'session=abc' }
|
|
145
|
+
});
|
|
146
|
+
expect(res.status).toBe(403);
|
|
147
|
+
expect(await res.json()).toMatchObject({ error: { code: 'cross_site_forbidden' } });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('refuses the viewer origin arriving at the app origin', async () => {
|
|
151
|
+
const res = await gated().request('https://app.example.com/api/v1/orgs', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { origin: VIEWER, 'sec-fetch-site': 'cross-site', cookie: 'session=abc' }
|
|
154
|
+
});
|
|
155
|
+
expect(res.status).toBe(403);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('still lets the app origin through', async () => {
|
|
159
|
+
const res = await gated().request('https://app.example.com/api/v1/orgs', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { origin: 'https://app.example.com', 'sec-fetch-site': 'same-origin' }
|
|
162
|
+
});
|
|
163
|
+
expect(res.status).toBe(201);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { declaredCredits } from '@antasphere/chassis-contract';
|
|
3
|
+
|
|
4
|
+
/** The price an action declares is what a test can compute from the declaration (PRDCT-2627). */
|
|
5
|
+
describe('declaredCredits', () => {
|
|
6
|
+
it('prices a quantity from the declaration: credits per `per` units, exact', () => {
|
|
7
|
+
const MB = 1024 * 1024;
|
|
8
|
+
const upload = { creditsPerUnit: 5, unit: 'bytes', per: MB };
|
|
9
|
+
expect(declaredCredits(upload, 20 * MB)).toBe(100);
|
|
10
|
+
expect(declaredCredits(upload, 43)).toBeCloseTo(0.000205, 6);
|
|
11
|
+
expect(declaredCredits({ creditsPerUnit: 50 }, 1)).toBe(50);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
DEMO_PASS_DEFAULT_MINUTES,
|
|
4
|
+
DEMO_PASS_MAX_MINUTES,
|
|
5
|
+
DEMO_SESSION_REFUSED_AUTH_PATHS,
|
|
6
|
+
demoSessionAuthRefusal,
|
|
7
|
+
isDemoAddress,
|
|
8
|
+
isDemoSignInHost,
|
|
9
|
+
isSafeDemoPath,
|
|
10
|
+
parseDemoEmailDomains,
|
|
11
|
+
parseDemoHosts
|
|
12
|
+
} from '@antasphere/chassis-server/identity';
|
|
13
|
+
import { INSECURE_SETUP_ORIGINS, SECURE_SETUP_ORIGINS } from '@antasphere/chassis-server/testing';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The demo pass's pure rules (the demo pass spec, sections 1 to 3). A pass is
|
|
17
|
+
* a sign-in-equivalent link, so every one of these is a boundary: the host
|
|
18
|
+
* the switch may be on, the addresses a pass may open, the page it lands on.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const NONE: ReadonlySet<string> = new Set();
|
|
22
|
+
|
|
23
|
+
describe('the lifetime constants', () => {
|
|
24
|
+
it('default to one day and stop at one week', () => {
|
|
25
|
+
expect(DEMO_PASS_DEFAULT_MINUTES).toBe(1440);
|
|
26
|
+
expect(DEMO_PASS_MAX_MINUTES).toBe(10080);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('parseDemoHosts / parseDemoEmailDomains', () => {
|
|
31
|
+
it('are empty for unset and blank input', () => {
|
|
32
|
+
expect(parseDemoHosts(undefined).size).toBe(0);
|
|
33
|
+
expect(parseDemoHosts('').size).toBe(0);
|
|
34
|
+
expect(parseDemoHosts(' ').size).toBe(0);
|
|
35
|
+
expect(parseDemoEmailDomains(undefined).size).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('trim, lowercase and dedupe the entries', () => {
|
|
39
|
+
const set = parseDemoHosts(' Demo.Example.io ,staging.acme.test, demo.example.io');
|
|
40
|
+
expect([...set].sort()).toEqual(['demo.example.io', 'staging.acme.test']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.each([
|
|
44
|
+
['a scheme', 'https://demo.example.io', /scheme/],
|
|
45
|
+
['a path', 'demo.example.io/app', /path/],
|
|
46
|
+
['a port', 'demo.example.io:8443', /port/],
|
|
47
|
+
['a space', 'demo example.io', /space/],
|
|
48
|
+
['a wildcard', '*.example.io', /wildcard/],
|
|
49
|
+
['an empty label', 'demo..example.io', /empty label/],
|
|
50
|
+
['a leading dot', '.example.io', /empty label/],
|
|
51
|
+
['a trailing dot', 'example.io.', /empty label/],
|
|
52
|
+
['a character no hostname carries', 'demo_host.example.io', /not a hostname/]
|
|
53
|
+
])('refuse an entry with %s, naming the variable and the entry', (_what, entry, reason) => {
|
|
54
|
+
expect(() => parseDemoHosts(`ok.example.io,${entry}`)).toThrow(reason);
|
|
55
|
+
expect(() => parseDemoHosts(entry)).toThrow(/DEMO_SIGN_IN_HOSTS/);
|
|
56
|
+
expect(() => parseDemoEmailDomains(entry)).toThrow(/DEMO_SIGN_IN_EMAIL_DOMAINS/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('refuse a list of separators only rather than arm an empty one', () => {
|
|
60
|
+
expect(() => parseDemoHosts(',,')).toThrow(/at least one hostname/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('isDemoSignInHost: loopback, or listed exactly', () => {
|
|
65
|
+
// The loopback half of the setup-origin table: every plaintext origin the
|
|
66
|
+
// setup wizard accepts is loopback by construction (the https ones are
|
|
67
|
+
// accepted there for their transport, which says nothing about the host).
|
|
68
|
+
const LOOPBACK = SECURE_SETUP_ORIGINS.filter((url) => url.startsWith('http://'));
|
|
69
|
+
const PUBLIC = [
|
|
70
|
+
...SECURE_SETUP_ORIGINS.filter((url) => url.startsWith('https://')),
|
|
71
|
+
...INSECURE_SETUP_ORIGINS
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
it.each(LOOPBACK)('accepts %s with an empty list', (url) => {
|
|
75
|
+
expect(isDemoSignInHost(url, NONE)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each(PUBLIC)('REFUSES %s with an empty list (look-alike hosts included)', (url) => {
|
|
79
|
+
expect(isDemoSignInHost(url, NONE)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts a public host once it is listed, and only that exact host', () => {
|
|
83
|
+
const hosts = parseDemoHosts('platform.example.com');
|
|
84
|
+
expect(isDemoSignInHost('https://platform.example.com', hosts)).toBe(true);
|
|
85
|
+
expect(isDemoSignInHost('https://PLATFORM.example.com/base', hosts)).toBe(true);
|
|
86
|
+
expect(isDemoSignInHost('https://app.platform.example.com', hosts)).toBe(false);
|
|
87
|
+
expect(isDemoSignInHost('https://platform.example.com.evil.test', hosts)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('fails closed on anything it cannot parse', () => {
|
|
91
|
+
expect(isDemoSignInHost('not a url', NONE)).toBe(false);
|
|
92
|
+
expect(isDemoSignInHost('', parseDemoHosts('localhost'))).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('isDemoAddress: reserved example and test domains, or an operator-listed one', () => {
|
|
97
|
+
it.each([
|
|
98
|
+
'a@example.com',
|
|
99
|
+
'a@example.net',
|
|
100
|
+
'a@example.org',
|
|
101
|
+
'a@sub.example.org',
|
|
102
|
+
'a@team.test',
|
|
103
|
+
'a@x.localhost',
|
|
104
|
+
'a@shop.example',
|
|
105
|
+
'a@nowhere.invalid',
|
|
106
|
+
'A@EXAMPLE.COM',
|
|
107
|
+
'a@Team.Test'
|
|
108
|
+
])('accepts %s', (email) => {
|
|
109
|
+
expect(isDemoAddress(email, NONE)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.each([
|
|
113
|
+
'a@notexample.com',
|
|
114
|
+
'a@example.com.evil.io',
|
|
115
|
+
'a@gmail.com',
|
|
116
|
+
'a@testing.io',
|
|
117
|
+
'a@test.io',
|
|
118
|
+
'"a@example.com"@gmail.com',
|
|
119
|
+
'a@example.com@gmail.com',
|
|
120
|
+
'a@.test',
|
|
121
|
+
'a@example..com',
|
|
122
|
+
'a@',
|
|
123
|
+
'@example.com',
|
|
124
|
+
'no-at-sign.example.com'
|
|
125
|
+
])('refuses %s', (email) => {
|
|
126
|
+
expect(isDemoAddress(email, NONE)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('judges the part after the LAST @', () => {
|
|
130
|
+
expect(isDemoAddress('"a@gmail.com"@example.com', NONE)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('adds the operator domains and their subdomains, on a label boundary', () => {
|
|
134
|
+
const extra = parseDemoEmailDomains('demo.acme.io');
|
|
135
|
+
expect(isDemoAddress('a@demo.acme.io', extra)).toBe(true);
|
|
136
|
+
expect(isDemoAddress('a@eu.demo.acme.io', extra)).toBe(true);
|
|
137
|
+
expect(isDemoAddress('A@DEMO.ACME.IO', extra)).toBe(true);
|
|
138
|
+
expect(isDemoAddress('a@acme.io', extra)).toBe(false);
|
|
139
|
+
expect(isDemoAddress('a@xdemo.acme.io', extra)).toBe(false);
|
|
140
|
+
expect(isDemoAddress('a@demo.acme.io.evil.test.com', extra)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('isSafeDemoPath: one leading slash, no fragment, no control character', () => {
|
|
145
|
+
it.each(['/', '/decks/1?x=1', '/settings/members', '/a/b?c=d&e=%2F'])('accepts %s', (path) => {
|
|
146
|
+
expect(isSafeDemoPath(path)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('accepts a path of exactly 2048 characters', () => {
|
|
150
|
+
expect(isSafeDemoPath('/' + 'a'.repeat(2047))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it.each([
|
|
154
|
+
['empty', ''],
|
|
155
|
+
['protocol-relative', '//evil'],
|
|
156
|
+
['backslash-relative', '/\\evil'],
|
|
157
|
+
['relative', 'decks'],
|
|
158
|
+
['absolute URL', 'https://evil.test/'],
|
|
159
|
+
['a fragment', '/a#b'],
|
|
160
|
+
['a space', '/a b'],
|
|
161
|
+
['a control character', '/a\u0001b'],
|
|
162
|
+
['a newline', '/a\nb'],
|
|
163
|
+
['DEL', '/a\u007fb'],
|
|
164
|
+
['2049 characters', '/' + 'a'.repeat(2048)]
|
|
165
|
+
])('refuses %s', (_what, path) => {
|
|
166
|
+
expect(isSafeDemoPath(path)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('demoSessionAuthRefusal: what a pass’s session may not do at the sign-in library', () => {
|
|
171
|
+
it('refuses every listed path', () => {
|
|
172
|
+
expect(DEMO_SESSION_REFUSED_AUTH_PATHS.length).toBe(27);
|
|
173
|
+
for (const path of DEMO_SESSION_REFUSED_AUTH_PATHS) {
|
|
174
|
+
expect(demoSessionAuthRefusal(path), path).toBe(true);
|
|
175
|
+
}
|
|
176
|
+
// A session's dynamic client registration stores the client under the person.
|
|
177
|
+
expect(demoSessionAuthRefusal('/oauth2/register')).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('refuses them whatever the case, and with one trailing slash', () => {
|
|
181
|
+
expect(demoSessionAuthRefusal('/Change-Password')).toBe(true);
|
|
182
|
+
expect(demoSessionAuthRefusal('/OAUTH2/AUTHORIZE')).toBe(true);
|
|
183
|
+
expect(demoSessionAuthRefusal('/change-password/')).toBe(true);
|
|
184
|
+
expect(demoSessionAuthRefusal('/Two-Factor/Enable/')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('allows reading the session, signing out, signing in and redeeming', () => {
|
|
188
|
+
for (const path of ['/get-session', '/sign-out', '/sign-in/email', '/demo/redeem']) {
|
|
189
|
+
expect(demoSessionAuthRefusal(path), path).toBe(false);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createEmailDriver, type EmailMessage } from '@antasphere/chassis-server/email';
|
|
3
|
+
import type { Logger } from '@antasphere/chassis-server/logger';
|
|
4
|
+
|
|
5
|
+
const logger = { info: () => {}, warn: () => {}, error: () => {} } as unknown as Logger;
|
|
6
|
+
|
|
7
|
+
/** The factory takes a Pick of Env, so the tests build exactly that shape. */
|
|
8
|
+
type FactoryEnv = Parameters<typeof createEmailDriver>[0];
|
|
9
|
+
const env = (over: Partial<FactoryEnv> = {}): FactoryEnv => ({ EMAIL_DRIVER: 'none', ...over }) as FactoryEnv;
|
|
10
|
+
|
|
11
|
+
describe('createEmailDriver', () => {
|
|
12
|
+
it('defaults to the none driver, which never delivers', () => {
|
|
13
|
+
const driver = createEmailDriver(env(), logger);
|
|
14
|
+
expect(driver.name).toBe('none');
|
|
15
|
+
expect(driver.delivers).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('builds each delivering driver when its credential and sender are present', () => {
|
|
19
|
+
const from = 'Things <noreply@example.com>';
|
|
20
|
+
expect(
|
|
21
|
+
createEmailDriver(env({ EMAIL_DRIVER: 'smtp', SMTP_URL: 'smtp://h:25', EMAIL_FROM: from }), logger).name
|
|
22
|
+
).toBe('smtp');
|
|
23
|
+
expect(
|
|
24
|
+
createEmailDriver(env({ EMAIL_DRIVER: 'resend', RESEND_API_KEY: 're_x', EMAIL_FROM: from }), logger)
|
|
25
|
+
.name
|
|
26
|
+
).toBe('resend');
|
|
27
|
+
const brevo = createEmailDriver(
|
|
28
|
+
env({ EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-x', EMAIL_FROM: from }),
|
|
29
|
+
logger
|
|
30
|
+
);
|
|
31
|
+
expect(brevo.name).toBe('brevo');
|
|
32
|
+
expect(brevo.delivers).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// A driver that boots without its credential would fail at the first send,
|
|
36
|
+
// which is the flow a user is waiting on; it must fail at boot instead.
|
|
37
|
+
it('refuses a delivering driver that is missing its credential or its sender', () => {
|
|
38
|
+
const from = 'Things <noreply@example.com>';
|
|
39
|
+
expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'brevo', EMAIL_FROM: from }), logger)).toThrow(
|
|
40
|
+
'EMAIL_DRIVER=brevo requires BREVO_API_KEY'
|
|
41
|
+
);
|
|
42
|
+
expect(() =>
|
|
43
|
+
createEmailDriver(env({ EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-x' }), logger)
|
|
44
|
+
).toThrow('EMAIL_DRIVER=brevo requires EMAIL_FROM');
|
|
45
|
+
expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'resend', EMAIL_FROM: from }), logger)).toThrow(
|
|
46
|
+
'EMAIL_DRIVER=resend requires RESEND_API_KEY'
|
|
47
|
+
);
|
|
48
|
+
expect(() => createEmailDriver(env({ EMAIL_DRIVER: 'smtp', EMAIL_FROM: from }), logger)).toThrow(
|
|
49
|
+
'EMAIL_DRIVER=smtp requires SMTP_URL'
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('the brevo driver on the wire', () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const send = async (
|
|
60
|
+
from: string,
|
|
61
|
+
response: Response,
|
|
62
|
+
message: EmailMessage = { to: 'her@example.com', subject: 'Subject', html: '<p>Body</p>' }
|
|
63
|
+
) => {
|
|
64
|
+
const fetchMock = vi.fn(async () => response);
|
|
65
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
66
|
+
const driver = createEmailDriver(
|
|
67
|
+
{ EMAIL_DRIVER: 'brevo', BREVO_API_KEY: 'xkeysib-secret', EMAIL_FROM: from } as FactoryEnv,
|
|
68
|
+
logger
|
|
69
|
+
);
|
|
70
|
+
const result = await driver.send(message).then(
|
|
71
|
+
() => null,
|
|
72
|
+
(error: Error) => error
|
|
73
|
+
);
|
|
74
|
+
return { result, fetchMock };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
it('posts the message to Brevo with the key in the api-key header', async () => {
|
|
78
|
+
const { result, fetchMock } = await send(
|
|
79
|
+
'Things <noreply@example.com>',
|
|
80
|
+
new Response('{}', { status: 201 })
|
|
81
|
+
);
|
|
82
|
+
expect(result).toBeNull();
|
|
83
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
84
|
+
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
85
|
+
expect(url).toBe('https://api.brevo.com/v3/smtp/email');
|
|
86
|
+
expect(init.method).toBe('POST');
|
|
87
|
+
expect((init.headers as Record<string, string>)['api-key']).toBe('xkeysib-secret');
|
|
88
|
+
const body = JSON.parse(init.body as string);
|
|
89
|
+
// `Name <addr>` is split into Brevo's own sender shape.
|
|
90
|
+
expect(body.sender).toEqual({ name: 'Things', email: 'noreply@example.com' });
|
|
91
|
+
expect(body.to).toEqual([{ email: 'her@example.com' }]);
|
|
92
|
+
expect(body.subject).toBe('Subject');
|
|
93
|
+
expect(body.htmlContent).toBe('<p>Body</p>');
|
|
94
|
+
// An absent text part is omitted, never sent as undefined/null.
|
|
95
|
+
expect('textContent' in body).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('passes a bare sender address through and carries the text part when there is one', async () => {
|
|
99
|
+
const { fetchMock } = await send('noreply@example.com', new Response('{}', { status: 201 }), {
|
|
100
|
+
to: 'her@example.com',
|
|
101
|
+
subject: 'Subject',
|
|
102
|
+
html: '<p>Body</p>',
|
|
103
|
+
text: 'Body'
|
|
104
|
+
});
|
|
105
|
+
const body = JSON.parse((fetchMock.mock.calls[0] as unknown as [string, RequestInit])[1].body as string);
|
|
106
|
+
expect(body.sender).toEqual({ email: 'noreply@example.com' });
|
|
107
|
+
expect(body.textContent).toBe('Body');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// The response body names the reason and may echo the request, so it must not
|
|
111
|
+
// reach the thrown message a caller could relay onward.
|
|
112
|
+
it('maps a refusal to its cause without leaking the response body', async () => {
|
|
113
|
+
const leak = 'unverified sender her@example.com and xkeysib-secret';
|
|
114
|
+
const cases: [number, string][] = [
|
|
115
|
+
[401, 'brevo: authentication failed'],
|
|
116
|
+
[429, 'brevo: rate limited'],
|
|
117
|
+
[503, 'brevo: temporarily unavailable'],
|
|
118
|
+
[400, 'brevo: refused the message (400)']
|
|
119
|
+
];
|
|
120
|
+
for (const [status, expected] of cases) {
|
|
121
|
+
const { result } = await send('noreply@example.com', new Response(leak, { status }));
|
|
122
|
+
expect(result?.message).toBe(expected);
|
|
123
|
+
expect(result?.message).not.toContain('xkeysib-secret');
|
|
124
|
+
expect(result?.message).not.toContain('her@example.com');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|