@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,394 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
3
|
+
import { desc, eq } from 'drizzle-orm';
|
|
4
|
+
import { auditLog } from '@antasphere/chassis-db';
|
|
5
|
+
import {
|
|
6
|
+
createDatabase,
|
|
7
|
+
createTestApp,
|
|
8
|
+
extractCookie,
|
|
9
|
+
readJson,
|
|
10
|
+
RecordingEmailDriver,
|
|
11
|
+
startPostgres,
|
|
12
|
+
type TestApp
|
|
13
|
+
} from './helpers.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Email change (M6):
|
|
17
|
+
* - with a delivering driver, self-serve change is open: the UNVERIFIED user
|
|
18
|
+
* (the template's common case) gets the single-leg flow (one verification
|
|
19
|
+
* mail to the NEW address); a VERIFIED user gets the two-leg flow
|
|
20
|
+
* (confirmation to OLD, then verification to NEW);
|
|
21
|
+
* - a duplicate target email answers an identical no-op 200 (no enumeration);
|
|
22
|
+
* - with no driver the endpoint stays closed (400) and the admin-generated
|
|
23
|
+
* change link is the SMTP-free path — sign-in-equivalent, single-use, and
|
|
24
|
+
* authorization-gated;
|
|
25
|
+
* - discovery reports auth.emailChange per the driver;
|
|
26
|
+
* - /auth/change-email sits behind the passwordReset rate-limit wall.
|
|
27
|
+
*
|
|
28
|
+
* Change-email tokens are STATELESS HS256 JWTs (never stored), so the
|
|
29
|
+
* delivering-driver tests capture links from a RecordingEmailDriver.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const OWNER = { email: 'owner@ec.test', name: 'Ec Owner', password: 'ec-owner-password-123' };
|
|
33
|
+
|
|
34
|
+
let container: StartedPostgreSqlContainer;
|
|
35
|
+
|
|
36
|
+
const json = (body: unknown) => ({
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
body: JSON.stringify(body)
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** POST helper with cookie + a per-test XFF ip (isolates rate-limit buckets). */
|
|
43
|
+
const withCookie = (cookie: string, body: unknown, ip: string) => ({
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'content-type': 'application/json', cookie, 'x-forwarded-for': ip },
|
|
46
|
+
body: JSON.stringify(body)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function signIn(app: TestApp, email: string, password: string): Promise<Response> {
|
|
50
|
+
return app.app.request('/api/v1/auth/sign-in/email', json({ email, password }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Invite + accept a user; returns nothing (sign in to use them). */
|
|
54
|
+
async function addUser(
|
|
55
|
+
app: TestApp,
|
|
56
|
+
ownerCookie: string,
|
|
57
|
+
user: { email: string; name: string; password: string }
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const inv = await readJson(
|
|
60
|
+
await app.app.request('/api/v1/invitations', {
|
|
61
|
+
...json({ email: user.email, role: 'member' }),
|
|
62
|
+
headers: { 'content-type': 'application/json', cookie: ownerCookie }
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
const token = (inv.acceptUrl as string).split('/invite/')[1]!;
|
|
66
|
+
const accepted = await app.app.request(
|
|
67
|
+
'/api/v1/invitations/accept',
|
|
68
|
+
json({ token, name: user.name, password: user.password })
|
|
69
|
+
);
|
|
70
|
+
expect(accepted.status).toBe(200);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function userRow(app: TestApp, email: string): Promise<{ email: string; verified: boolean } | null> {
|
|
74
|
+
const { rows } = await app.db.pool.query<{ email: string; email_verified: boolean }>(
|
|
75
|
+
`SELECT email, email_verified FROM "user" WHERE email = $1`,
|
|
76
|
+
[email]
|
|
77
|
+
);
|
|
78
|
+
return rows[0] ? { email: rows[0].email, verified: rows[0].email_verified } : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The first http(s) URL in a recorded mail's text body. */
|
|
82
|
+
function linkOf(mail: { text?: string }): string {
|
|
83
|
+
const m = /https?:\/\/\S+/.exec(mail.text ?? '');
|
|
84
|
+
if (!m) throw new Error(`no URL in mail text: ${mail.text}`);
|
|
85
|
+
return m[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
container = await startPostgres();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterAll(async () => {
|
|
93
|
+
await container?.stop();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('with a delivering email driver (recording)', () => {
|
|
97
|
+
let app: TestApp;
|
|
98
|
+
let mailer: RecordingEmailDriver;
|
|
99
|
+
let ownerCookie: string;
|
|
100
|
+
|
|
101
|
+
const MEMBER_A = { email: 'membera@ec.test', name: 'Ec MemberA', password: 'ec-membera-password-1' };
|
|
102
|
+
const MEMBER_B = { email: 'memberb@ec.test', name: 'Ec MemberB', password: 'ec-memberb-password-1' };
|
|
103
|
+
const OWNER_NEW_EMAIL = 'owner2@ec.test';
|
|
104
|
+
|
|
105
|
+
beforeAll(async () => {
|
|
106
|
+
mailer = new RecordingEmailDriver();
|
|
107
|
+
app = await createTestApp(await createDatabase(container, 'ec_rec'), {}, { email: mailer });
|
|
108
|
+
await app.app.request(
|
|
109
|
+
'/api/v1/setup',
|
|
110
|
+
json({ setupToken: 'integration-test-setup-token', instanceName: 'Ec', owner: OWNER })
|
|
111
|
+
);
|
|
112
|
+
ownerCookie = extractCookie(await signIn(app, OWNER.email, OWNER.password));
|
|
113
|
+
await addUser(app, ownerCookie, MEMBER_A);
|
|
114
|
+
await addUser(app, ownerCookie, MEMBER_B);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterAll(async () => {
|
|
118
|
+
await app.stop();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('reports emailChange: true in discovery', async () => {
|
|
122
|
+
const info = await readJson(await app.app.request('/api/v1/instance'));
|
|
123
|
+
expect(info.auth.emailChange).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('unverified user (the common case): single leg — ONE mail to the NEW address', async () => {
|
|
127
|
+
// Setup now mints the operator verified (D9 — the cloud trusted-link
|
|
128
|
+
// bootstrap); an unverified user WITH a session is a manufactured state
|
|
129
|
+
// here, standing in for the pre-D9 common case.
|
|
130
|
+
await app.db.pool.query(`UPDATE "user" SET email_verified = false WHERE email = $1`, [OWNER.email]);
|
|
131
|
+
mailer.sent.length = 0;
|
|
132
|
+
|
|
133
|
+
const res = await app.app.request(
|
|
134
|
+
'/api/v1/auth/change-email',
|
|
135
|
+
withCookie(ownerCookie, { newEmail: OWNER_NEW_EMAIL, callbackURL: '/account' }, '10.1.0.1')
|
|
136
|
+
);
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
expect((await readJson(res)).status).toBe(true);
|
|
139
|
+
|
|
140
|
+
// Exactly one mail, to the NEW address (no confirmation leg for unverified).
|
|
141
|
+
expect(mailer.sent).toHaveLength(1);
|
|
142
|
+
expect(mailer.sent[0]!.to).toBe(OWNER_NEW_EMAIL);
|
|
143
|
+
expect(mailer.sent[0]!.subject).toContain('Verify');
|
|
144
|
+
|
|
145
|
+
// Nothing changed yet — the link does the change.
|
|
146
|
+
expect((await userRow(app, OWNER.email))?.verified).toBe(false);
|
|
147
|
+
|
|
148
|
+
// Consume the link (browser flow: still signed in). It redirects to the
|
|
149
|
+
// callbackURL and flips email + emailVerified.
|
|
150
|
+
const verify = await app.app.request(linkOf(mailer.sent[0]!), { headers: { cookie: ownerCookie } });
|
|
151
|
+
expect(verify.status).toBeGreaterThanOrEqual(300);
|
|
152
|
+
expect(verify.status).toBeLessThan(400);
|
|
153
|
+
expect(verify.headers.get('location')).toContain('/account');
|
|
154
|
+
|
|
155
|
+
expect(await userRow(app, OWNER.email)).toBeNull();
|
|
156
|
+
expect(await userRow(app, OWNER_NEW_EMAIL)).toEqual({ email: OWNER_NEW_EMAIL, verified: true });
|
|
157
|
+
|
|
158
|
+
// Old sessions survive the change (nothing is revoked).
|
|
159
|
+
const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: ownerCookie } }));
|
|
160
|
+
expect(me.user.email).toBe(OWNER_NEW_EMAIL);
|
|
161
|
+
|
|
162
|
+
// Sign-in works with the new email (same password), not the old one.
|
|
163
|
+
expect((await signIn(app, OWNER_NEW_EMAIL, OWNER.password)).status).toBe(200);
|
|
164
|
+
expect((await signIn(app, OWNER.email, OWNER.password)).status).toBe(401);
|
|
165
|
+
|
|
166
|
+
// Both credential events were audited (request + landing).
|
|
167
|
+
for (const action of ['user.email_change_request', 'user.email_change']) {
|
|
168
|
+
const [row] = await app.db.db
|
|
169
|
+
.select()
|
|
170
|
+
.from(auditLog)
|
|
171
|
+
.where(eq(auditLog.action, action))
|
|
172
|
+
.orderBy(desc(auditLog.id))
|
|
173
|
+
.limit(1);
|
|
174
|
+
expect(row, `audit row ${action}`).toBeTruthy();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('verified user: two legs — confirm at the OLD address, verify at the NEW', async () => {
|
|
179
|
+
await app.db.pool.query(`UPDATE "user" SET email_verified = true WHERE email = $1`, [MEMBER_A.email]);
|
|
180
|
+
const cookie = extractCookie(await signIn(app, MEMBER_A.email, MEMBER_A.password));
|
|
181
|
+
const newEmail = 'membera2@ec.test';
|
|
182
|
+
mailer.sent.length = 0;
|
|
183
|
+
|
|
184
|
+
const res = await app.app.request(
|
|
185
|
+
'/api/v1/auth/change-email',
|
|
186
|
+
withCookie(cookie, { newEmail, callbackURL: '/account' }, '10.1.0.2')
|
|
187
|
+
);
|
|
188
|
+
expect(res.status).toBe(200);
|
|
189
|
+
|
|
190
|
+
// Leg 1: confirmation to the OLD address.
|
|
191
|
+
expect(mailer.sent).toHaveLength(1);
|
|
192
|
+
expect(mailer.sent[0]!.to).toBe(MEMBER_A.email);
|
|
193
|
+
expect(mailer.sent[0]!.subject).toContain('Confirm');
|
|
194
|
+
|
|
195
|
+
// Consuming it does not change the email yet — it sends leg 2 to the NEW.
|
|
196
|
+
const confirm = await app.app.request(linkOf(mailer.sent[0]!), { headers: { cookie } });
|
|
197
|
+
expect(confirm.status).toBeGreaterThanOrEqual(300);
|
|
198
|
+
expect(confirm.status).toBeLessThan(400);
|
|
199
|
+
expect((await userRow(app, MEMBER_A.email))?.email).toBe(MEMBER_A.email);
|
|
200
|
+
expect(mailer.sent).toHaveLength(2);
|
|
201
|
+
expect(mailer.sent[1]!.to).toBe(newEmail);
|
|
202
|
+
expect(mailer.sent[1]!.subject).toContain('Verify');
|
|
203
|
+
|
|
204
|
+
// Leg 2 lands the change.
|
|
205
|
+
const verify = await app.app.request(linkOf(mailer.sent[1]!), { headers: { cookie } });
|
|
206
|
+
expect(verify.status).toBeGreaterThanOrEqual(300);
|
|
207
|
+
expect(verify.status).toBeLessThan(400);
|
|
208
|
+
expect(await userRow(app, MEMBER_A.email)).toBeNull();
|
|
209
|
+
expect(await userRow(app, newEmail)).toEqual({ email: newEmail, verified: true });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('does not reveal whether an email exists: duplicate target is a mail-free no-op 200', async () => {
|
|
213
|
+
const cookie = extractCookie(await signIn(app, MEMBER_B.email, MEMBER_B.password));
|
|
214
|
+
mailer.sent.length = 0;
|
|
215
|
+
|
|
216
|
+
const res = await app.app.request(
|
|
217
|
+
'/api/v1/auth/change-email',
|
|
218
|
+
withCookie(cookie, { newEmail: OWNER_NEW_EMAIL, callbackURL: '/account' }, '10.1.0.3')
|
|
219
|
+
);
|
|
220
|
+
expect(res.status).toBe(200);
|
|
221
|
+
expect((await readJson(res)).status).toBe(true);
|
|
222
|
+
expect(mailer.sent).toHaveLength(0);
|
|
223
|
+
expect((await userRow(app, MEMBER_B.email))?.email).toBe(MEMBER_B.email);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('rate-limits /auth/change-email on the 6th call', async () => {
|
|
227
|
+
const cookie = extractCookie(await signIn(app, MEMBER_B.email, MEMBER_B.password));
|
|
228
|
+
for (let i = 1; i <= 5; i++) {
|
|
229
|
+
// Duplicate-target no-ops: nothing changes, nothing is mailed.
|
|
230
|
+
const r = await app.app.request(
|
|
231
|
+
'/api/v1/auth/change-email',
|
|
232
|
+
withCookie(cookie, { newEmail: OWNER_NEW_EMAIL, callbackURL: '/account' }, '10.1.0.99')
|
|
233
|
+
);
|
|
234
|
+
expect(r.status, `call ${i}`).toBe(200);
|
|
235
|
+
}
|
|
236
|
+
const sixth = await app.app.request(
|
|
237
|
+
'/api/v1/auth/change-email',
|
|
238
|
+
withCookie(cookie, { newEmail: OWNER_NEW_EMAIL, callbackURL: '/account' }, '10.1.0.99')
|
|
239
|
+
);
|
|
240
|
+
expect(sixth.status).toBe(429);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('no email driver (default): the admin change link', () => {
|
|
245
|
+
let app: TestApp;
|
|
246
|
+
let ownerCookie: string;
|
|
247
|
+
let memberId: string;
|
|
248
|
+
let ownerId: string;
|
|
249
|
+
|
|
250
|
+
const MEMBER = { email: 'member@ecadm.test', name: 'Adm Member', password: 'adm-member-password-1' };
|
|
251
|
+
const ADMIN = { email: 'admin@ecadm.test', name: 'Adm Admin', password: 'adm-admin-password-12' };
|
|
252
|
+
const MEMBER_NEW_EMAIL = 'member-new@ecadm.test';
|
|
253
|
+
|
|
254
|
+
beforeAll(async () => {
|
|
255
|
+
app = await createTestApp(await createDatabase(container, 'ec_none'));
|
|
256
|
+
await app.app.request(
|
|
257
|
+
'/api/v1/setup',
|
|
258
|
+
json({ setupToken: 'integration-test-setup-token', instanceName: 'EcAdm', owner: OWNER })
|
|
259
|
+
);
|
|
260
|
+
ownerCookie = extractCookie(await signIn(app, OWNER.email, OWNER.password));
|
|
261
|
+
await addUser(app, ownerCookie, MEMBER);
|
|
262
|
+
await addUser(app, ownerCookie, ADMIN);
|
|
263
|
+
|
|
264
|
+
const { members } = await readJson(
|
|
265
|
+
await app.app.request('/api/v1/members', { headers: { cookie: ownerCookie } })
|
|
266
|
+
);
|
|
267
|
+
memberId = members.find((m: { email: string }) => m.email === MEMBER.email).id;
|
|
268
|
+
ownerId = members.find((m: { email: string }) => m.email === OWNER.email).id;
|
|
269
|
+
const adminMemberId = members.find((m: { email: string }) => m.email === ADMIN.email).id;
|
|
270
|
+
await app.app.request(`/api/v1/members/${adminMemberId}`, {
|
|
271
|
+
...json({ role: 'admin' }),
|
|
272
|
+
method: 'PATCH',
|
|
273
|
+
headers: { 'content-type': 'application/json', cookie: ownerCookie }
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
afterAll(async () => {
|
|
278
|
+
await app.stop();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('reports emailChange: false in discovery and closes self-serve change', async () => {
|
|
282
|
+
const info = await readJson(await app.app.request('/api/v1/instance'));
|
|
283
|
+
expect(info.auth.emailChange).toBe(false);
|
|
284
|
+
|
|
285
|
+
const res = await app.app.request(
|
|
286
|
+
'/api/v1/auth/change-email',
|
|
287
|
+
withCookie(ownerCookie, { newEmail: 'else@ecadm.test' }, '10.2.0.1')
|
|
288
|
+
);
|
|
289
|
+
expect(res.status).toBe(400);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('admin mints a single-use link that changes the email AND signs the member in', async () => {
|
|
293
|
+
const minted = await app.app.request(`/api/v1/members/${memberId}/change-email-link`, {
|
|
294
|
+
...json({ newEmail: MEMBER_NEW_EMAIL }),
|
|
295
|
+
headers: { 'content-type': 'application/json', cookie: ownerCookie }
|
|
296
|
+
});
|
|
297
|
+
expect(minted.status).toBe(200);
|
|
298
|
+
const link = await readJson(minted);
|
|
299
|
+
expect(link.newEmail).toBe(MEMBER_NEW_EMAIL);
|
|
300
|
+
expect(link.verifyUrl).toContain('/api/v1/auth/verify-email?token=');
|
|
301
|
+
expect(new Date(link.expiresAt).getTime()).toBeGreaterThan(Date.now());
|
|
302
|
+
|
|
303
|
+
// The mint is audited.
|
|
304
|
+
const [audit] = await app.db.db
|
|
305
|
+
.select()
|
|
306
|
+
.from(auditLog)
|
|
307
|
+
.where(eq(auditLog.action, 'member.change_email_link'))
|
|
308
|
+
.orderBy(desc(auditLog.id))
|
|
309
|
+
.limit(1);
|
|
310
|
+
expect(audit).toBeTruthy();
|
|
311
|
+
|
|
312
|
+
// Consumed LOGGED OUT: it redirects to /account and creates a session
|
|
313
|
+
// for the member — the sign-in-equivalent property the dialog warns about.
|
|
314
|
+
const verify = await app.app.request(link.verifyUrl);
|
|
315
|
+
expect(verify.status).toBeGreaterThanOrEqual(300);
|
|
316
|
+
expect(verify.status).toBeLessThan(400);
|
|
317
|
+
expect(verify.headers.get('location')).toContain('/account');
|
|
318
|
+
const sessionCookie = extractCookie(verify);
|
|
319
|
+
const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: sessionCookie } }));
|
|
320
|
+
expect(me.user.email).toBe(MEMBER_NEW_EMAIL);
|
|
321
|
+
|
|
322
|
+
const row = await app.db.pool.query<{ email_verified: boolean }>(
|
|
323
|
+
`SELECT email_verified FROM "user" WHERE email = $1`,
|
|
324
|
+
[MEMBER_NEW_EMAIL]
|
|
325
|
+
);
|
|
326
|
+
expect(row.rows[0]!.email_verified).toBe(true);
|
|
327
|
+
|
|
328
|
+
// Single-use in effect: the old email no longer resolves, so a replay
|
|
329
|
+
// redirects with an error instead of changing anything.
|
|
330
|
+
const reuse = await app.app.request(link.verifyUrl);
|
|
331
|
+
expect(reuse.status).toBeGreaterThanOrEqual(300);
|
|
332
|
+
expect(reuse.status).toBeLessThan(400);
|
|
333
|
+
expect(reuse.headers.get('location')).toContain('error=');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('refuses minting to non-admins, admin-on-owner, unknown members, and taken emails', async () => {
|
|
337
|
+
// A plain member cannot mint at all (the explicit 3-segment gate).
|
|
338
|
+
const memberCookie = extractCookie(await signIn(app, MEMBER_NEW_EMAIL, MEMBER.password));
|
|
339
|
+
const asMember = await app.app.request(`/api/v1/members/${memberId}/change-email-link`, {
|
|
340
|
+
...json({ newEmail: 'whatever@ecadm.test' }),
|
|
341
|
+
headers: { 'content-type': 'application/json', cookie: memberCookie }
|
|
342
|
+
});
|
|
343
|
+
expect(asMember.status).toBe(403);
|
|
344
|
+
|
|
345
|
+
// An admin (non-owner) cannot mint for an owner.
|
|
346
|
+
const adminCookie = extractCookie(await signIn(app, ADMIN.email, ADMIN.password));
|
|
347
|
+
const asAdmin = await app.app.request(`/api/v1/members/${ownerId}/change-email-link`, {
|
|
348
|
+
...json({ newEmail: 'whatever@ecadm.test' }),
|
|
349
|
+
headers: { 'content-type': 'application/json', cookie: adminCookie }
|
|
350
|
+
});
|
|
351
|
+
expect(asAdmin.status).toBe(403);
|
|
352
|
+
|
|
353
|
+
// Unknown member id → 404.
|
|
354
|
+
const unknown = await app.app.request(
|
|
355
|
+
`/api/v1/members/00000000-0000-0000-0000-000000000000/change-email-link`,
|
|
356
|
+
{
|
|
357
|
+
...json({ newEmail: 'whatever@ecadm.test' }),
|
|
358
|
+
headers: { 'content-type': 'application/json', cookie: ownerCookie }
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
expect(unknown.status).toBe(404);
|
|
362
|
+
|
|
363
|
+
// An email that already belongs to a user → 409 email_taken.
|
|
364
|
+
const taken = await app.app.request(`/api/v1/members/${memberId}/change-email-link`, {
|
|
365
|
+
...json({ newEmail: ADMIN.email.toUpperCase() }),
|
|
366
|
+
headers: { 'content-type': 'application/json', cookie: ownerCookie }
|
|
367
|
+
});
|
|
368
|
+
expect(taken.status).toBe(409);
|
|
369
|
+
expect((await readJson(taken)).error.code).toBe('email_taken');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('with EMAIL_DRIVER configured (env path)', () => {
|
|
374
|
+
let app: TestApp;
|
|
375
|
+
|
|
376
|
+
beforeAll(async () => {
|
|
377
|
+
// Dead SMTP port — delivery would fail, but discovery only reflects the
|
|
378
|
+
// configured driver.
|
|
379
|
+
app = await createTestApp(await createDatabase(container, 'ec_smtp'), {
|
|
380
|
+
EMAIL_DRIVER: 'smtp',
|
|
381
|
+
SMTP_URL: 'smtp://127.0.0.1:1',
|
|
382
|
+
EMAIL_FROM: 'Ec <noreply@ec.test>'
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
afterAll(async () => {
|
|
387
|
+
await app.stop();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('flips discovery to emailChange: true', async () => {
|
|
391
|
+
const info = await readJson(await app.app.request('/api/v1/instance'));
|
|
392
|
+
expect(info.auth.emailChange).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
3
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createDb, schema } from '@antasphere/chassis-db';
|
|
8
|
+
import { defineChassisContract } from '@antasphere/chassis-contract';
|
|
9
|
+
import { defineChassisRoutes } from '@antasphere/chassis-contract/routes';
|
|
10
|
+
import { createPlatform, type BootResult, type ToolDefinition } from '../../src/index.js';
|
|
11
|
+
import { createScopeAllowlist } from '../../src/middleware/index.js';
|
|
12
|
+
import { endPool } from '../../src/testing/index.js';
|
|
13
|
+
import { THINGS_COPY, THINGS_IDENTITY, THINGS_ROUTES_COPY } from '../host/identity.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The chassis boots with a MINIMAL tool: no env extension, no services, no
|
|
17
|
+
* jobs, no buckets, no routes, no public app, no MCP tools — only its own scope
|
|
18
|
+
* vocabulary. This is the proof that the composition has no hidden need of the
|
|
19
|
+
* deck slots.
|
|
20
|
+
*
|
|
21
|
+
* The migrations are a FIXTURE read by path, not an import: the chassis's
|
|
22
|
+
* own history (`test/host/drizzle`, generated from the chassis schema by
|
|
23
|
+
* `pnpm db:generate` in this package), the chassis tables and nothing else.
|
|
24
|
+
*/
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const MIGRATIONS_FIXTURE = join(here, '../host/drizzle');
|
|
27
|
+
|
|
28
|
+
const SETUP_TOKEN = 'empty-tool-setup-token';
|
|
29
|
+
const OWNER = { email: 'owner@things.test', name: 'Things Owner', password: 'things-owner-password-1' };
|
|
30
|
+
|
|
31
|
+
const contract = defineChassisContract({ scopes: ['things:read', 'things:write', 'things:export'] });
|
|
32
|
+
type ThingsScope = 'things:read' | 'things:write' | 'things:export';
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
35
|
+
type NoEnv = {};
|
|
36
|
+
type NoDomain = Record<string, never>;
|
|
37
|
+
|
|
38
|
+
const emptyTool: ToolDefinition<NoEnv, NoDomain> = {
|
|
39
|
+
identity: THINGS_IDENTITY,
|
|
40
|
+
runtime: {
|
|
41
|
+
version: '0.0.0-test',
|
|
42
|
+
findMigrationsDir: () => MIGRATIONS_FIXTURE,
|
|
43
|
+
publicDir: join(here, 'no-dashboard-build')
|
|
44
|
+
},
|
|
45
|
+
db: (url) => createDb(url, schema),
|
|
46
|
+
scopes: {
|
|
47
|
+
oauth: ['openid', 'profile', 'email', 'offline_access', 'things:read', 'things:write', 'things:export'],
|
|
48
|
+
cliKey: ['things:read', 'things:write'],
|
|
49
|
+
requiredScopeFor: createScopeAllowlist<ThingsScope>({
|
|
50
|
+
read: 'things:read',
|
|
51
|
+
write: 'things:write',
|
|
52
|
+
dataExport: 'things:export',
|
|
53
|
+
rules: []
|
|
54
|
+
}),
|
|
55
|
+
contractRoutes: defineChassisRoutes(contract, THINGS_IDENTITY, THINGS_ROUTES_COPY)
|
|
56
|
+
},
|
|
57
|
+
services: () => ({}),
|
|
58
|
+
api: {
|
|
59
|
+
// A tool with no domain references no blob; its files are its uploaders' and its operators'.
|
|
60
|
+
filePolicy: () => ({ blobInUse: async () => false, blobReadScope: () => undefined })
|
|
61
|
+
},
|
|
62
|
+
mcp: {
|
|
63
|
+
registerTools: () => {},
|
|
64
|
+
instructions: (info) => `MCP endpoint of the "${info.instanceName}" instance.`,
|
|
65
|
+
errorHints: {},
|
|
66
|
+
scopes: { read: 'things:read', write: 'things:write' }
|
|
67
|
+
},
|
|
68
|
+
copy: THINGS_COPY
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let container: StartedPostgreSqlContainer;
|
|
72
|
+
let booted: BootResult<NoEnv, NoDomain>;
|
|
73
|
+
let cookie: string;
|
|
74
|
+
|
|
75
|
+
const json = (body: unknown, headers: Record<string, string> = {}) => ({
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
78
|
+
body: JSON.stringify(body)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function extractCookie(res: Response): string {
|
|
82
|
+
const setCookie = res.headers.get('set-cookie');
|
|
83
|
+
if (!setCookie) throw new Error('no set-cookie header in response');
|
|
84
|
+
return setCookie
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((part) => part.split(';')[0]?.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join('; ');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
container = await new PostgreSqlContainer('pgvector/pgvector:pg17').start();
|
|
93
|
+
booted = await createPlatform(emptyTool).boot({
|
|
94
|
+
DATABASE_URL: container.getConnectionUri(),
|
|
95
|
+
DATA_DIR: await mkdtemp(join(tmpdir(), 'chassis-empty-tool-')),
|
|
96
|
+
AUTH_SECRET: 'integration-test-secret-0123456789abcdef0123456789abcdef',
|
|
97
|
+
SETUP_TOKEN,
|
|
98
|
+
NODE_ENV: 'test',
|
|
99
|
+
LOG_LEVEL: 'error',
|
|
100
|
+
PUBLIC_BASE_URL: 'http://localhost:3000'
|
|
101
|
+
});
|
|
102
|
+
}, 240_000);
|
|
103
|
+
|
|
104
|
+
afterAll(async () => {
|
|
105
|
+
await booted?.jobs.stop().catch(() => {});
|
|
106
|
+
if (booted) await endPool(booted.db.pool); // the sockets closed, before the container stops (PRDCT-2547)
|
|
107
|
+
await container?.stop();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('the chassis with an empty tool', () => {
|
|
111
|
+
it('boots to ready and answers the probes', async () => {
|
|
112
|
+
expect(booted.state.ready).toBe(true);
|
|
113
|
+
expect(booted.tool).toEqual({});
|
|
114
|
+
expect((await booted.app.request('/healthz')).status).toBe(200);
|
|
115
|
+
expect((await booted.app.request('/readyz')).status).toBe(200);
|
|
116
|
+
const instance = (await (await booted.app.request('/api/v1/instance')).json()) as {
|
|
117
|
+
name: string;
|
|
118
|
+
setupRequired: boolean;
|
|
119
|
+
};
|
|
120
|
+
expect(instance).toMatchObject({ name: 'Things', setupRequired: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('setup claims the instance, sign-in works, /me answers', async () => {
|
|
124
|
+
const setup = await booted.app.request(
|
|
125
|
+
'/api/v1/setup',
|
|
126
|
+
json({ setupToken: SETUP_TOKEN, instanceName: 'Things Instance', owner: OWNER })
|
|
127
|
+
);
|
|
128
|
+
expect(setup.status).toBe(201);
|
|
129
|
+
const signIn = await booted.app.request(
|
|
130
|
+
'/api/v1/auth/sign-in/email',
|
|
131
|
+
json({ email: OWNER.email, password: OWNER.password })
|
|
132
|
+
);
|
|
133
|
+
expect(signIn.status).toBe(200);
|
|
134
|
+
cookie = extractCookie(signIn);
|
|
135
|
+
const me = await booted.app.request('/api/v1/me', { headers: { cookie } });
|
|
136
|
+
expect(me.status).toBe(200);
|
|
137
|
+
expect(await me.json()).toMatchObject({ user: { email: OWNER.email }, role: 'owner', via: 'session' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('a key with the tool scope reaches /me and is refused on an unlisted route', async () => {
|
|
141
|
+
const minted = await booted.app.request(
|
|
142
|
+
'/api/v1/api-keys',
|
|
143
|
+
json({ name: 'things-ro', scopes: ['things:read'] }, { cookie })
|
|
144
|
+
);
|
|
145
|
+
expect(minted.status).toBe(201);
|
|
146
|
+
const { key } = (await minted.json()) as { key: string };
|
|
147
|
+
const authorization = `Bearer ${key}`;
|
|
148
|
+
|
|
149
|
+
const me = await booted.app.request('/api/v1/me', { headers: { authorization } });
|
|
150
|
+
expect(me.status).toBe(200);
|
|
151
|
+
expect(await me.json()).toMatchObject({ via: 'api_key', scopes: ['things:read'] });
|
|
152
|
+
|
|
153
|
+
const members = await booted.app.request('/api/v1/members', { headers: { authorization } });
|
|
154
|
+
expect(members.status).toBe(403);
|
|
155
|
+
// A scope of another vocabulary is not mintable here.
|
|
156
|
+
const foreign = await booted.app.request(
|
|
157
|
+
'/api/v1/api-keys',
|
|
158
|
+
json({ name: 'foreign', scopes: ['gadgets:read'] }, { cookie })
|
|
159
|
+
);
|
|
160
|
+
expect(foreign.status).toBe(400);
|
|
161
|
+
|
|
162
|
+
// /mcp lists exactly the generic tools: the chassis registers `<toolPrefix>whoami`
|
|
163
|
+
// itself, third, so the name its other tools point at exists for every tool,
|
|
164
|
+
// then the eleven project tools and the two team reads (projects are a chassis concept, so a tool
|
|
165
|
+
// that declares none of its own still serves them).
|
|
166
|
+
const rpc = (body: unknown) =>
|
|
167
|
+
booted.app.request('/mcp', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'content-type': 'application/json',
|
|
171
|
+
accept: 'application/json, text/event-stream',
|
|
172
|
+
authorization
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(body)
|
|
175
|
+
});
|
|
176
|
+
const mcp = await rpc({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} });
|
|
177
|
+
expect(mcp.status).toBe(200);
|
|
178
|
+
const listed = (await mcp.json()) as { result: { tools: Array<{ name: string }> } };
|
|
179
|
+
expect(listed.result.tools.map((t) => t.name)).toEqual([
|
|
180
|
+
'get_me',
|
|
181
|
+
'list_files',
|
|
182
|
+
'things_whoami',
|
|
183
|
+
'things_list_projects',
|
|
184
|
+
'things_get_project',
|
|
185
|
+
'things_list_project_members',
|
|
186
|
+
'things_create_project',
|
|
187
|
+
'things_update_project',
|
|
188
|
+
'things_archive_project',
|
|
189
|
+
'things_add_project_member',
|
|
190
|
+
'things_set_project_member_role',
|
|
191
|
+
'things_remove_project_member',
|
|
192
|
+
'things_set_project_team_role',
|
|
193
|
+
'things_remove_project_team',
|
|
194
|
+
'things_list_teams',
|
|
195
|
+
'things_list_team_members'
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
// …and it answers `/me`, as the key it was called with.
|
|
199
|
+
const called = await rpc({
|
|
200
|
+
jsonrpc: '2.0',
|
|
201
|
+
id: 2,
|
|
202
|
+
method: 'tools/call',
|
|
203
|
+
params: { name: 'things_whoami', arguments: {} }
|
|
204
|
+
});
|
|
205
|
+
expect(called.status).toBe(200);
|
|
206
|
+
const answer = (await called.json()) as {
|
|
207
|
+
result: { isError?: boolean; content: Array<{ type: string; text: string }> };
|
|
208
|
+
};
|
|
209
|
+
expect(answer.result.isError).toBeFalsy();
|
|
210
|
+
expect(JSON.parse(answer.result.content[0]!.text)).toMatchObject({
|
|
211
|
+
user: { email: OWNER.email },
|
|
212
|
+
via: 'api_key',
|
|
213
|
+
scopes: ['things:read']
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('a key WITHOUT the read scope gets the friendly scope sentence on both identity tools', async () => {
|
|
218
|
+
// Such a key exists: the mint takes any subset of the tool's vocabulary.
|
|
219
|
+
const minted = await booted.app.request(
|
|
220
|
+
'/api/v1/api-keys',
|
|
221
|
+
json({ name: 'things-wo', scopes: ['things:write'] }, { cookie })
|
|
222
|
+
);
|
|
223
|
+
expect(minted.status).toBe(201);
|
|
224
|
+
const { key } = (await minted.json()) as { key: string };
|
|
225
|
+
const authorization = `Bearer ${key}`;
|
|
226
|
+
|
|
227
|
+
// The enforcement point is the API's allowlist, and it holds by itself…
|
|
228
|
+
const me = await booted.app.request('/api/v1/me', { headers: { authorization } });
|
|
229
|
+
expect(me.status).toBe(403);
|
|
230
|
+
|
|
231
|
+
// …the MCP pre-check is what turns that raw 403 into a sentence the model can act on.
|
|
232
|
+
for (const name of ['things_whoami', 'get_me']) {
|
|
233
|
+
const called = await booted.app.request('/mcp', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: {
|
|
236
|
+
'content-type': 'application/json',
|
|
237
|
+
accept: 'application/json, text/event-stream',
|
|
238
|
+
authorization
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: {} } })
|
|
241
|
+
});
|
|
242
|
+
expect(called.status, name).toBe(200);
|
|
243
|
+
const answer = (await called.json()) as {
|
|
244
|
+
result: { isError?: boolean; content: Array<{ type: string; text: string }> };
|
|
245
|
+
};
|
|
246
|
+
expect(answer.result.isError, name).toBe(true);
|
|
247
|
+
expect(answer.result.content[0]!.text, name).toBe(
|
|
248
|
+
'Missing scope "things:read": this connection was not granted permission to read data. ' +
|
|
249
|
+
'Reconnect the MCP server and approve the permission on the consent screen.'
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('the OpenAPI document carries no deck path and the root app no deck surface', async () => {
|
|
255
|
+
const doc = (await (await booted.app.request('/api/v1/openapi.json')).json()) as {
|
|
256
|
+
paths: Record<string, unknown>;
|
|
257
|
+
};
|
|
258
|
+
const paths = Object.keys(doc.paths);
|
|
259
|
+
expect(paths).toContain('/me');
|
|
260
|
+
expect(paths).toContain('/files');
|
|
261
|
+
expect(paths.filter((p) => /presentations|collaborators|annotations|viewer/.test(p))).toEqual([]);
|
|
262
|
+
// Unmatched API paths answer the JSON 404 terminator; there is no public viewer.
|
|
263
|
+
expect((await booted.app.request('/api/v1/presentations', { headers: { cookie } })).status).toBe(404);
|
|
264
|
+
const placeholder = await booted.app.request('/v/some-secret');
|
|
265
|
+
expect(await placeholder.text()).toContain('No dashboard build found');
|
|
266
|
+
});
|
|
267
|
+
});
|