palaryn 0.1.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 +21 -0
- package/README.md +716 -0
- package/dist/sdk/typescript/src/client.d.ts +71 -0
- package/dist/sdk/typescript/src/client.d.ts.map +1 -0
- package/dist/sdk/typescript/src/client.js +176 -0
- package/dist/sdk/typescript/src/client.js.map +1 -0
- package/dist/sdk/typescript/src/errors.d.ts +50 -0
- package/dist/sdk/typescript/src/errors.d.ts.map +1 -0
- package/dist/sdk/typescript/src/errors.js +103 -0
- package/dist/sdk/typescript/src/errors.js.map +1 -0
- package/dist/sdk/typescript/src/index.d.ts +4 -0
- package/dist/sdk/typescript/src/index.d.ts.map +1 -0
- package/dist/sdk/typescript/src/index.js +15 -0
- package/dist/sdk/typescript/src/index.js.map +1 -0
- package/dist/sdk/typescript/src/types.d.ts +101 -0
- package/dist/sdk/typescript/src/types.d.ts.map +1 -0
- package/dist/sdk/typescript/src/types.js +6 -0
- package/dist/sdk/typescript/src/types.js.map +1 -0
- package/dist/src/admin/index.d.ts +2 -0
- package/dist/src/admin/index.d.ts.map +1 -0
- package/dist/src/admin/index.js +6 -0
- package/dist/src/admin/index.js.map +1 -0
- package/dist/src/admin/routes.d.ts +5 -0
- package/dist/src/admin/routes.d.ts.map +1 -0
- package/dist/src/admin/routes.js +471 -0
- package/dist/src/admin/routes.js.map +1 -0
- package/dist/src/admin/templates.d.ts +51 -0
- package/dist/src/admin/templates.d.ts.map +1 -0
- package/dist/src/admin/templates.js +500 -0
- package/dist/src/admin/templates.js.map +1 -0
- package/dist/src/anomaly/detector.d.ts +141 -0
- package/dist/src/anomaly/detector.d.ts.map +1 -0
- package/dist/src/anomaly/detector.js +554 -0
- package/dist/src/anomaly/detector.js.map +1 -0
- package/dist/src/anomaly/index.d.ts +2 -0
- package/dist/src/anomaly/index.d.ts.map +1 -0
- package/dist/src/anomaly/index.js +7 -0
- package/dist/src/anomaly/index.js.map +1 -0
- package/dist/src/approval/manager.d.ts +147 -0
- package/dist/src/approval/manager.d.ts.map +1 -0
- package/dist/src/approval/manager.js +511 -0
- package/dist/src/approval/manager.js.map +1 -0
- package/dist/src/approval/webhook.d.ts +36 -0
- package/dist/src/approval/webhook.d.ts.map +1 -0
- package/dist/src/approval/webhook.js +135 -0
- package/dist/src/approval/webhook.js.map +1 -0
- package/dist/src/audit/logger.d.ts +70 -0
- package/dist/src/audit/logger.d.ts.map +1 -0
- package/dist/src/audit/logger.js +440 -0
- package/dist/src/audit/logger.js.map +1 -0
- package/dist/src/auth/index.d.ts +6 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +22 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/src/auth/password.d.ts +3 -0
- package/dist/src/auth/password.d.ts.map +1 -0
- package/dist/src/auth/password.js +25 -0
- package/dist/src/auth/password.js.map +1 -0
- package/dist/src/auth/pkce.d.ts +13 -0
- package/dist/src/auth/pkce.d.ts.map +1 -0
- package/dist/src/auth/pkce.js +58 -0
- package/dist/src/auth/pkce.js.map +1 -0
- package/dist/src/auth/providers.d.ts +28 -0
- package/dist/src/auth/providers.d.ts.map +1 -0
- package/dist/src/auth/providers.js +198 -0
- package/dist/src/auth/providers.js.map +1 -0
- package/dist/src/auth/routes.d.ts +14 -0
- package/dist/src/auth/routes.d.ts.map +1 -0
- package/dist/src/auth/routes.js +431 -0
- package/dist/src/auth/routes.js.map +1 -0
- package/dist/src/auth/session.d.ts +24 -0
- package/dist/src/auth/session.d.ts.map +1 -0
- package/dist/src/auth/session.js +105 -0
- package/dist/src/auth/session.js.map +1 -0
- package/dist/src/billing/index.d.ts +7 -0
- package/dist/src/billing/index.d.ts.map +1 -0
- package/dist/src/billing/index.js +14 -0
- package/dist/src/billing/index.js.map +1 -0
- package/dist/src/billing/plan-enforcer.d.ts +44 -0
- package/dist/src/billing/plan-enforcer.d.ts.map +1 -0
- package/dist/src/billing/plan-enforcer.js +110 -0
- package/dist/src/billing/plan-enforcer.js.map +1 -0
- package/dist/src/billing/routes.d.ts +15 -0
- package/dist/src/billing/routes.d.ts.map +1 -0
- package/dist/src/billing/routes.js +193 -0
- package/dist/src/billing/routes.js.map +1 -0
- package/dist/src/billing/stripe-client.d.ts +14 -0
- package/dist/src/billing/stripe-client.d.ts.map +1 -0
- package/dist/src/billing/stripe-client.js +51 -0
- package/dist/src/billing/stripe-client.js.map +1 -0
- package/dist/src/billing/webhook-handler.d.ts +19 -0
- package/dist/src/billing/webhook-handler.d.ts.map +1 -0
- package/dist/src/billing/webhook-handler.js +169 -0
- package/dist/src/billing/webhook-handler.js.map +1 -0
- package/dist/src/billing/webhook-routes.d.ts +5 -0
- package/dist/src/billing/webhook-routes.d.ts.map +1 -0
- package/dist/src/billing/webhook-routes.js +30 -0
- package/dist/src/billing/webhook-routes.js.map +1 -0
- package/dist/src/budget/manager.d.ts +95 -0
- package/dist/src/budget/manager.d.ts.map +1 -0
- package/dist/src/budget/manager.js +547 -0
- package/dist/src/budget/manager.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +38 -0
- package/dist/src/budget/usage-extractor.d.ts.map +1 -0
- package/dist/src/budget/usage-extractor.js +165 -0
- package/dist/src/budget/usage-extractor.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +115 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.d.ts.map +1 -0
- package/dist/src/config/defaults.js +243 -0
- package/dist/src/config/defaults.js.map +1 -0
- package/dist/src/config/validate.d.ts +15 -0
- package/dist/src/config/validate.d.ts.map +1 -0
- package/dist/src/config/validate.js +105 -0
- package/dist/src/config/validate.js.map +1 -0
- package/dist/src/dlp/composite-scanner.d.ts +47 -0
- package/dist/src/dlp/composite-scanner.d.ts.map +1 -0
- package/dist/src/dlp/composite-scanner.js +186 -0
- package/dist/src/dlp/composite-scanner.js.map +1 -0
- package/dist/src/dlp/index.d.ts +10 -0
- package/dist/src/dlp/index.d.ts.map +1 -0
- package/dist/src/dlp/index.js +26 -0
- package/dist/src/dlp/index.js.map +1 -0
- package/dist/src/dlp/interfaces.d.ts +33 -0
- package/dist/src/dlp/interfaces.d.ts.map +1 -0
- package/dist/src/dlp/interfaces.js +3 -0
- package/dist/src/dlp/interfaces.js.map +1 -0
- package/dist/src/dlp/patterns.d.ts +9 -0
- package/dist/src/dlp/patterns.d.ts.map +1 -0
- package/dist/src/dlp/patterns.js +25 -0
- package/dist/src/dlp/patterns.js.map +1 -0
- package/dist/src/dlp/prompt-injection-backend.d.ts +68 -0
- package/dist/src/dlp/prompt-injection-backend.d.ts.map +1 -0
- package/dist/src/dlp/prompt-injection-backend.js +148 -0
- package/dist/src/dlp/prompt-injection-backend.js.map +1 -0
- package/dist/src/dlp/prompt-injection-patterns.d.ts +32 -0
- package/dist/src/dlp/prompt-injection-patterns.d.ts.map +1 -0
- package/dist/src/dlp/prompt-injection-patterns.js +290 -0
- package/dist/src/dlp/prompt-injection-patterns.js.map +1 -0
- package/dist/src/dlp/regex-backend.d.ts +32 -0
- package/dist/src/dlp/regex-backend.d.ts.map +1 -0
- package/dist/src/dlp/regex-backend.js +153 -0
- package/dist/src/dlp/regex-backend.js.map +1 -0
- package/dist/src/dlp/scanner.d.ts +122 -0
- package/dist/src/dlp/scanner.d.ts.map +1 -0
- package/dist/src/dlp/scanner.js +444 -0
- package/dist/src/dlp/scanner.js.map +1 -0
- package/dist/src/dlp/text-normalizer.d.ts +41 -0
- package/dist/src/dlp/text-normalizer.d.ts.map +1 -0
- package/dist/src/dlp/text-normalizer.js +203 -0
- package/dist/src/dlp/text-normalizer.js.map +1 -0
- package/dist/src/dlp/trufflehog-backend.d.ts +64 -0
- package/dist/src/dlp/trufflehog-backend.d.ts.map +1 -0
- package/dist/src/dlp/trufflehog-backend.js +151 -0
- package/dist/src/dlp/trufflehog-backend.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts +25 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -0
- package/dist/src/executor/http-executor.js +333 -0
- package/dist/src/executor/http-executor.js.map +1 -0
- package/dist/src/executor/index.d.ts +6 -0
- package/dist/src/executor/index.d.ts.map +1 -0
- package/dist/src/executor/index.js +12 -0
- package/dist/src/executor/index.js.map +1 -0
- package/dist/src/executor/interfaces.d.ts +11 -0
- package/dist/src/executor/interfaces.d.ts.map +1 -0
- package/dist/src/executor/interfaces.js +3 -0
- package/dist/src/executor/interfaces.js.map +1 -0
- package/dist/src/executor/noop-executor.d.ts +13 -0
- package/dist/src/executor/noop-executor.d.ts.map +1 -0
- package/dist/src/executor/noop-executor.js +21 -0
- package/dist/src/executor/noop-executor.js.map +1 -0
- package/dist/src/executor/registry.d.ts +30 -0
- package/dist/src/executor/registry.d.ts.map +1 -0
- package/dist/src/executor/registry.js +62 -0
- package/dist/src/executor/registry.js.map +1 -0
- package/dist/src/executor/slack-executor.d.ts +24 -0
- package/dist/src/executor/slack-executor.d.ts.map +1 -0
- package/dist/src/executor/slack-executor.js +147 -0
- package/dist/src/executor/slack-executor.js.map +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +74 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts +23 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -0
- package/dist/src/mcp/auth-verifier.js +162 -0
- package/dist/src/mcp/auth-verifier.js.map +1 -0
- package/dist/src/mcp/bridge.d.ts +132 -0
- package/dist/src/mcp/bridge.d.ts.map +1 -0
- package/dist/src/mcp/bridge.js +734 -0
- package/dist/src/mcp/bridge.js.map +1 -0
- package/dist/src/mcp/http-transport.d.ts +32 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -0
- package/dist/src/mcp/http-transport.js +538 -0
- package/dist/src/mcp/http-transport.js.map +1 -0
- package/dist/src/mcp/index.d.ts +10 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +17 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/oauth-pages.d.ts +23 -0
- package/dist/src/mcp/oauth-pages.d.ts.map +1 -0
- package/dist/src/mcp/oauth-pages.js +121 -0
- package/dist/src/mcp/oauth-pages.js.map +1 -0
- package/dist/src/mcp/oauth-postgres-stores.d.ts +55 -0
- package/dist/src/mcp/oauth-postgres-stores.d.ts.map +1 -0
- package/dist/src/mcp/oauth-postgres-stores.js +226 -0
- package/dist/src/mcp/oauth-postgres-stores.js.map +1 -0
- package/dist/src/mcp/oauth-provider.d.ts +95 -0
- package/dist/src/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/src/mcp/oauth-provider.js +360 -0
- package/dist/src/mcp/oauth-provider.js.map +1 -0
- package/dist/src/mcp/oauth-stores.d.ts +62 -0
- package/dist/src/mcp/oauth-stores.d.ts.map +1 -0
- package/dist/src/mcp/oauth-stores.js +154 -0
- package/dist/src/mcp/oauth-stores.js.map +1 -0
- package/dist/src/mcp/server.d.ts +18 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +51 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/metrics/collector.d.ts +106 -0
- package/dist/src/metrics/collector.d.ts.map +1 -0
- package/dist/src/metrics/collector.js +311 -0
- package/dist/src/metrics/collector.js.map +1 -0
- package/dist/src/metrics/index.d.ts +2 -0
- package/dist/src/metrics/index.d.ts.map +1 -0
- package/dist/src/metrics/index.js +6 -0
- package/dist/src/metrics/index.js.map +1 -0
- package/dist/src/middleware/auth.d.ts +77 -0
- package/dist/src/middleware/auth.d.ts.map +1 -0
- package/dist/src/middleware/auth.js +720 -0
- package/dist/src/middleware/auth.js.map +1 -0
- package/dist/src/middleware/session.d.ts +18 -0
- package/dist/src/middleware/session.d.ts.map +1 -0
- package/dist/src/middleware/session.js +67 -0
- package/dist/src/middleware/session.js.map +1 -0
- package/dist/src/middleware/validate.d.ts +3 -0
- package/dist/src/middleware/validate.d.ts.map +1 -0
- package/dist/src/middleware/validate.js +85 -0
- package/dist/src/middleware/validate.js.map +1 -0
- package/dist/src/policy/engine.d.ts +107 -0
- package/dist/src/policy/engine.d.ts.map +1 -0
- package/dist/src/policy/engine.js +646 -0
- package/dist/src/policy/engine.js.map +1 -0
- package/dist/src/policy/index.d.ts +3 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/policy/index.js +8 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/policy/opa-engine.d.ts +176 -0
- package/dist/src/policy/opa-engine.d.ts.map +1 -0
- package/dist/src/policy/opa-engine.js +790 -0
- package/dist/src/policy/opa-engine.js.map +1 -0
- package/dist/src/proxy/forward-proxy.d.ts +30 -0
- package/dist/src/proxy/forward-proxy.d.ts.map +1 -0
- package/dist/src/proxy/forward-proxy.js +580 -0
- package/dist/src/proxy/forward-proxy.js.map +1 -0
- package/dist/src/proxy/index.d.ts +2 -0
- package/dist/src/proxy/index.d.ts.map +1 -0
- package/dist/src/proxy/index.js +8 -0
- package/dist/src/proxy/index.js.map +1 -0
- package/dist/src/ratelimit/limiter.d.ts +45 -0
- package/dist/src/ratelimit/limiter.d.ts.map +1 -0
- package/dist/src/ratelimit/limiter.js +158 -0
- package/dist/src/ratelimit/limiter.js.map +1 -0
- package/dist/src/replay/engine.d.ts +40 -0
- package/dist/src/replay/engine.d.ts.map +1 -0
- package/dist/src/replay/engine.js +106 -0
- package/dist/src/replay/engine.js.map +1 -0
- package/dist/src/replay/index.d.ts +2 -0
- package/dist/src/replay/index.d.ts.map +1 -0
- package/dist/src/replay/index.js +6 -0
- package/dist/src/replay/index.js.map +1 -0
- package/dist/src/saas/index.d.ts +2 -0
- package/dist/src/saas/index.d.ts.map +1 -0
- package/dist/src/saas/index.js +18 -0
- package/dist/src/saas/index.js.map +1 -0
- package/dist/src/saas/routes.d.ts +18 -0
- package/dist/src/saas/routes.d.ts.map +1 -0
- package/dist/src/saas/routes.js +1566 -0
- package/dist/src/saas/routes.js.map +1 -0
- package/dist/src/server/app.d.ts +44 -0
- package/dist/src/server/app.d.ts.map +1 -0
- package/dist/src/server/app.js +854 -0
- package/dist/src/server/app.js.map +1 -0
- package/dist/src/server/errors.d.ts +32 -0
- package/dist/src/server/errors.d.ts.map +1 -0
- package/dist/src/server/errors.js +39 -0
- package/dist/src/server/errors.js.map +1 -0
- package/dist/src/server/gateway.d.ts +165 -0
- package/dist/src/server/gateway.d.ts.map +1 -0
- package/dist/src/server/gateway.js +964 -0
- package/dist/src/server/gateway.js.map +1 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +295 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/logger.d.ts +33 -0
- package/dist/src/server/logger.d.ts.map +1 -0
- package/dist/src/server/logger.js +230 -0
- package/dist/src/server/logger.js.map +1 -0
- package/dist/src/server/stream-proxy.d.ts +32 -0
- package/dist/src/server/stream-proxy.d.ts.map +1 -0
- package/dist/src/server/stream-proxy.js +184 -0
- package/dist/src/server/stream-proxy.js.map +1 -0
- package/dist/src/storage/file-persistence.d.ts +48 -0
- package/dist/src/storage/file-persistence.d.ts.map +1 -0
- package/dist/src/storage/file-persistence.js +280 -0
- package/dist/src/storage/file-persistence.js.map +1 -0
- package/dist/src/storage/index.d.ts +5 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +21 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +237 -0
- package/dist/src/storage/interfaces.d.ts.map +1 -0
- package/dist/src/storage/interfaces.js +3 -0
- package/dist/src/storage/interfaces.js.map +1 -0
- package/dist/src/storage/memory.d.ts +162 -0
- package/dist/src/storage/memory.d.ts.map +1 -0
- package/dist/src/storage/memory.js +603 -0
- package/dist/src/storage/memory.js.map +1 -0
- package/dist/src/storage/postgres.d.ts +267 -0
- package/dist/src/storage/postgres.d.ts.map +1 -0
- package/dist/src/storage/postgres.js +1555 -0
- package/dist/src/storage/postgres.js.map +1 -0
- package/dist/src/storage/redis.d.ts +202 -0
- package/dist/src/storage/redis.d.ts.map +1 -0
- package/dist/src/storage/redis.js +629 -0
- package/dist/src/storage/redis.js.map +1 -0
- package/dist/src/tracing/index.d.ts +2 -0
- package/dist/src/tracing/index.d.ts.map +1 -0
- package/dist/src/tracing/index.js +6 -0
- package/dist/src/tracing/index.js.map +1 -0
- package/dist/src/tracing/provider.d.ts +43 -0
- package/dist/src/tracing/provider.d.ts.map +1 -0
- package/dist/src/tracing/provider.js +74 -0
- package/dist/src/tracing/provider.js.map +1 -0
- package/dist/src/trust/calculator.d.ts +54 -0
- package/dist/src/trust/calculator.d.ts.map +1 -0
- package/dist/src/trust/calculator.js +102 -0
- package/dist/src/trust/calculator.js.map +1 -0
- package/dist/src/trust/index.d.ts +2 -0
- package/dist/src/trust/index.d.ts.map +1 -0
- package/dist/src/trust/index.js +7 -0
- package/dist/src/trust/index.js.map +1 -0
- package/dist/src/types/budget.d.ts +30 -0
- package/dist/src/types/budget.d.ts.map +1 -0
- package/dist/src/types/budget.js +3 -0
- package/dist/src/types/budget.js.map +1 -0
- package/dist/src/types/config.d.ts +176 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/config.js +3 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/events.d.ts +24 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +3 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +24 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/policy.d.ts +60 -0
- package/dist/src/types/policy.d.ts.map +1 -0
- package/dist/src/types/policy.js +3 -0
- package/dist/src/types/policy.js.map +1 -0
- package/dist/src/types/stripe-config.d.ts +12 -0
- package/dist/src/types/stripe-config.d.ts.map +1 -0
- package/dist/src/types/stripe-config.js +3 -0
- package/dist/src/types/stripe-config.js.map +1 -0
- package/dist/src/types/subscription.d.ts +24 -0
- package/dist/src/types/subscription.d.ts.map +1 -0
- package/dist/src/types/subscription.js +38 -0
- package/dist/src/types/subscription.js.map +1 -0
- package/dist/src/types/tool-call.d.ts +42 -0
- package/dist/src/types/tool-call.d.ts.map +1 -0
- package/dist/src/types/tool-call.js +3 -0
- package/dist/src/types/tool-call.js.map +1 -0
- package/dist/src/types/tool-result.d.ts +58 -0
- package/dist/src/types/tool-result.d.ts.map +1 -0
- package/dist/src/types/tool-result.js +3 -0
- package/dist/src/types/tool-result.js.map +1 -0
- package/dist/src/types/user.d.ts +101 -0
- package/dist/src/types/user.d.ts.map +1 -0
- package/dist/src/types/user.js +6 -0
- package/dist/src/types/user.js.map +1 -0
- package/dist/tests/integration/api.test.d.ts +2 -0
- package/dist/tests/integration/api.test.d.ts.map +1 -0
- package/dist/tests/integration/api.test.js +1199 -0
- package/dist/tests/integration/api.test.js.map +1 -0
- package/dist/tests/integration/proxy.test.d.ts +2 -0
- package/dist/tests/integration/proxy.test.d.ts.map +1 -0
- package/dist/tests/integration/proxy.test.js +251 -0
- package/dist/tests/integration/proxy.test.js.map +1 -0
- package/dist/tests/integration/storage.test.d.ts +16 -0
- package/dist/tests/integration/storage.test.d.ts.map +1 -0
- package/dist/tests/integration/storage.test.js +826 -0
- package/dist/tests/integration/storage.test.js.map +1 -0
- package/dist/tests/unit/admin.test.d.ts +2 -0
- package/dist/tests/unit/admin.test.d.ts.map +1 -0
- package/dist/tests/unit/admin.test.js +698 -0
- package/dist/tests/unit/admin.test.js.map +1 -0
- package/dist/tests/unit/anomaly-detector.test.d.ts +2 -0
- package/dist/tests/unit/anomaly-detector.test.d.ts.map +1 -0
- package/dist/tests/unit/anomaly-detector.test.js +903 -0
- package/dist/tests/unit/anomaly-detector.test.js.map +1 -0
- package/dist/tests/unit/approval-manager.test.d.ts +2 -0
- package/dist/tests/unit/approval-manager.test.d.ts.map +1 -0
- package/dist/tests/unit/approval-manager.test.js +528 -0
- package/dist/tests/unit/approval-manager.test.js.map +1 -0
- package/dist/tests/unit/approval-webhook.test.d.ts +2 -0
- package/dist/tests/unit/approval-webhook.test.d.ts.map +1 -0
- package/dist/tests/unit/approval-webhook.test.js +355 -0
- package/dist/tests/unit/approval-webhook.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.d.ts +2 -0
- package/dist/tests/unit/audit-logger.test.d.ts.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +635 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -0
- package/dist/tests/unit/auth-routes.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes.test.js +281 -0
- package/dist/tests/unit/auth-routes.test.js.map +1 -0
- package/dist/tests/unit/auth.test.d.ts +2 -0
- package/dist/tests/unit/auth.test.d.ts.map +1 -0
- package/dist/tests/unit/auth.test.js +1382 -0
- package/dist/tests/unit/auth.test.js.map +1 -0
- package/dist/tests/unit/billing.test.d.ts +2 -0
- package/dist/tests/unit/billing.test.d.ts.map +1 -0
- package/dist/tests/unit/billing.test.js +579 -0
- package/dist/tests/unit/billing.test.js.map +1 -0
- package/dist/tests/unit/budget-manager.test.d.ts +2 -0
- package/dist/tests/unit/budget-manager.test.d.ts.map +1 -0
- package/dist/tests/unit/budget-manager.test.js +778 -0
- package/dist/tests/unit/budget-manager.test.js.map +1 -0
- package/dist/tests/unit/budget-race.test.d.ts +2 -0
- package/dist/tests/unit/budget-race.test.d.ts.map +1 -0
- package/dist/tests/unit/budget-race.test.js +58 -0
- package/dist/tests/unit/budget-race.test.js.map +1 -0
- package/dist/tests/unit/cli.test.d.ts +2 -0
- package/dist/tests/unit/cli.test.d.ts.map +1 -0
- package/dist/tests/unit/cli.test.js +93 -0
- package/dist/tests/unit/cli.test.js.map +1 -0
- package/dist/tests/unit/concurrency.test.d.ts +2 -0
- package/dist/tests/unit/concurrency.test.d.ts.map +1 -0
- package/dist/tests/unit/concurrency.test.js +1270 -0
- package/dist/tests/unit/concurrency.test.js.map +1 -0
- package/dist/tests/unit/config-validate.test.d.ts +2 -0
- package/dist/tests/unit/config-validate.test.d.ts.map +1 -0
- package/dist/tests/unit/config-validate.test.js +230 -0
- package/dist/tests/unit/config-validate.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.d.ts +2 -0
- package/dist/tests/unit/defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/defaults.test.js +364 -0
- package/dist/tests/unit/defaults.test.js.map +1 -0
- package/dist/tests/unit/dlp-backends.test.d.ts +2 -0
- package/dist/tests/unit/dlp-backends.test.d.ts.map +1 -0
- package/dist/tests/unit/dlp-backends.test.js +563 -0
- package/dist/tests/unit/dlp-backends.test.js.map +1 -0
- package/dist/tests/unit/dlp-scanner.test.d.ts +2 -0
- package/dist/tests/unit/dlp-scanner.test.d.ts.map +1 -0
- package/dist/tests/unit/dlp-scanner.test.js +739 -0
- package/dist/tests/unit/dlp-scanner.test.js.map +1 -0
- package/dist/tests/unit/error-responses.test.d.ts +2 -0
- package/dist/tests/unit/error-responses.test.d.ts.map +1 -0
- package/dist/tests/unit/error-responses.test.js +101 -0
- package/dist/tests/unit/error-responses.test.js.map +1 -0
- package/dist/tests/unit/executor-registry.test.d.ts +2 -0
- package/dist/tests/unit/executor-registry.test.d.ts.map +1 -0
- package/dist/tests/unit/executor-registry.test.js +390 -0
- package/dist/tests/unit/executor-registry.test.js.map +1 -0
- package/dist/tests/unit/forward-proxy.test.d.ts +2 -0
- package/dist/tests/unit/forward-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/forward-proxy.test.js +621 -0
- package/dist/tests/unit/forward-proxy.test.js.map +1 -0
- package/dist/tests/unit/gateway-features.test.d.ts +2 -0
- package/dist/tests/unit/gateway-features.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-features.test.js +753 -0
- package/dist/tests/unit/gateway-features.test.js.map +1 -0
- package/dist/tests/unit/http-executor.test.d.ts +2 -0
- package/dist/tests/unit/http-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor.test.js +310 -0
- package/dist/tests/unit/http-executor.test.js.map +1 -0
- package/dist/tests/unit/mcp-bridge.test.d.ts +2 -0
- package/dist/tests/unit/mcp-bridge.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-bridge.test.js +1136 -0
- package/dist/tests/unit/mcp-bridge.test.js.map +1 -0
- package/dist/tests/unit/mcp-http-transport.test.d.ts +2 -0
- package/dist/tests/unit/mcp-http-transport.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-http-transport.test.js +899 -0
- package/dist/tests/unit/mcp-http-transport.test.js.map +1 -0
- package/dist/tests/unit/mcp-oauth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-oauth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-oauth.test.js +759 -0
- package/dist/tests/unit/mcp-oauth.test.js.map +1 -0
- package/dist/tests/unit/mcp-server.test.d.ts +15 -0
- package/dist/tests/unit/mcp-server.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-server.test.js +158 -0
- package/dist/tests/unit/mcp-server.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.d.ts +2 -0
- package/dist/tests/unit/metrics.test.d.ts.map +1 -0
- package/dist/tests/unit/metrics.test.js +208 -0
- package/dist/tests/unit/metrics.test.js.map +1 -0
- package/dist/tests/unit/oauth.test.d.ts +2 -0
- package/dist/tests/unit/oauth.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth.test.js +281 -0
- package/dist/tests/unit/oauth.test.js.map +1 -0
- package/dist/tests/unit/opa-circuit-breaker.test.d.ts +2 -0
- package/dist/tests/unit/opa-circuit-breaker.test.d.ts.map +1 -0
- package/dist/tests/unit/opa-circuit-breaker.test.js +297 -0
- package/dist/tests/unit/opa-circuit-breaker.test.js.map +1 -0
- package/dist/tests/unit/opa-engine.test.d.ts +2 -0
- package/dist/tests/unit/opa-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/opa-engine.test.js +1813 -0
- package/dist/tests/unit/opa-engine.test.js.map +1 -0
- package/dist/tests/unit/pipeline-timing.test.d.ts +2 -0
- package/dist/tests/unit/pipeline-timing.test.d.ts.map +1 -0
- package/dist/tests/unit/pipeline-timing.test.js +528 -0
- package/dist/tests/unit/pipeline-timing.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.d.ts +2 -0
- package/dist/tests/unit/policy-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +1345 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -0
- package/dist/tests/unit/policy-store.test.d.ts +2 -0
- package/dist/tests/unit/policy-store.test.d.ts.map +1 -0
- package/dist/tests/unit/policy-store.test.js +60 -0
- package/dist/tests/unit/policy-store.test.js.map +1 -0
- package/dist/tests/unit/postgres-storage.test.d.ts +2 -0
- package/dist/tests/unit/postgres-storage.test.d.ts.map +1 -0
- package/dist/tests/unit/postgres-storage.test.js +614 -0
- package/dist/tests/unit/postgres-storage.test.js.map +1 -0
- package/dist/tests/unit/prompt-injection-backend.test.d.ts +2 -0
- package/dist/tests/unit/prompt-injection-backend.test.d.ts.map +1 -0
- package/dist/tests/unit/prompt-injection-backend.test.js +621 -0
- package/dist/tests/unit/prompt-injection-backend.test.js.map +1 -0
- package/dist/tests/unit/proxy-hardening.test.d.ts +2 -0
- package/dist/tests/unit/proxy-hardening.test.d.ts.map +1 -0
- package/dist/tests/unit/proxy-hardening.test.js +166 -0
- package/dist/tests/unit/proxy-hardening.test.js.map +1 -0
- package/dist/tests/unit/rate-limiter.test.d.ts +2 -0
- package/dist/tests/unit/rate-limiter.test.d.ts.map +1 -0
- package/dist/tests/unit/rate-limiter.test.js +443 -0
- package/dist/tests/unit/rate-limiter.test.js.map +1 -0
- package/dist/tests/unit/redis-storage.test.d.ts +2 -0
- package/dist/tests/unit/redis-storage.test.d.ts.map +1 -0
- package/dist/tests/unit/redis-storage.test.js +766 -0
- package/dist/tests/unit/redis-storage.test.js.map +1 -0
- package/dist/tests/unit/replay-engine.test.d.ts +2 -0
- package/dist/tests/unit/replay-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/replay-engine.test.js +371 -0
- package/dist/tests/unit/replay-engine.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +1399 -0
- package/dist/tests/unit/saas-routes.test.js.map +1 -0
- package/dist/tests/unit/session.test.d.ts +2 -0
- package/dist/tests/unit/session.test.d.ts.map +1 -0
- package/dist/tests/unit/session.test.js +532 -0
- package/dist/tests/unit/session.test.js.map +1 -0
- package/dist/tests/unit/slack-executor.test.d.ts +2 -0
- package/dist/tests/unit/slack-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/slack-executor.test.js +209 -0
- package/dist/tests/unit/slack-executor.test.js.map +1 -0
- package/dist/tests/unit/storage-hardening.test.d.ts +2 -0
- package/dist/tests/unit/storage-hardening.test.d.ts.map +1 -0
- package/dist/tests/unit/storage-hardening.test.js +165 -0
- package/dist/tests/unit/storage-hardening.test.js.map +1 -0
- package/dist/tests/unit/storage.test.d.ts +2 -0
- package/dist/tests/unit/storage.test.d.ts.map +1 -0
- package/dist/tests/unit/storage.test.js +698 -0
- package/dist/tests/unit/storage.test.js.map +1 -0
- package/dist/tests/unit/text-normalizer.test.d.ts +2 -0
- package/dist/tests/unit/text-normalizer.test.d.ts.map +1 -0
- package/dist/tests/unit/text-normalizer.test.js +229 -0
- package/dist/tests/unit/text-normalizer.test.js.map +1 -0
- package/dist/tests/unit/tracing.test.d.ts +2 -0
- package/dist/tests/unit/tracing.test.d.ts.map +1 -0
- package/dist/tests/unit/tracing.test.js +611 -0
- package/dist/tests/unit/tracing.test.js.map +1 -0
- package/dist/tests/unit/trust-calculator.test.d.ts +2 -0
- package/dist/tests/unit/trust-calculator.test.d.ts.map +1 -0
- package/dist/tests/unit/trust-calculator.test.js +497 -0
- package/dist/tests/unit/trust-calculator.test.js.map +1 -0
- package/dist/tests/unit/ts-sdk.test.d.ts +2 -0
- package/dist/tests/unit/ts-sdk.test.d.ts.map +1 -0
- package/dist/tests/unit/ts-sdk.test.js +421 -0
- package/dist/tests/unit/ts-sdk.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor-llm.test.d.ts +2 -0
- package/dist/tests/unit/usage-extractor-llm.test.d.ts.map +1 -0
- package/dist/tests/unit/usage-extractor-llm.test.js +139 -0
- package/dist/tests/unit/usage-extractor-llm.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.d.ts +2 -0
- package/dist/tests/unit/usage-extractor.test.d.ts.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +271 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -0
- package/dist/tests/unit/user-stores.test.d.ts +2 -0
- package/dist/tests/unit/user-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/user-stores.test.js +687 -0
- package/dist/tests/unit/user-stores.test.js.map +1 -0
- package/dist/tests/unit/validate.test.d.ts +2 -0
- package/dist/tests/unit/validate.test.d.ts.map +1 -0
- package/dist/tests/unit/validate.test.js +545 -0
- package/dist/tests/unit/validate.test.js.map +1 -0
- package/package.json +86 -0
- package/policy-packs/README.md +42 -0
- package/policy-packs/default.yaml +46 -0
- package/policy-packs/dev_fast.yaml +54 -0
- package/policy-packs/prod_strict.yaml +83 -0
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
40
|
+
const auth_1 = require("../../src/middleware/auth");
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Mock jwks-rsa
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const mockGetSigningKey = jest.fn();
|
|
45
|
+
jest.mock('jwks-rsa', () => {
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: {
|
|
49
|
+
JwksClient: jest.fn().mockImplementation(() => ({
|
|
50
|
+
getSigningKey: mockGetSigningKey,
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
JwksClient: jest.fn().mockImplementation(() => ({
|
|
54
|
+
getSigningKey: mockGetSigningKey,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Helpers
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const crypto = __importStar(require("crypto"));
|
|
62
|
+
const TEST_SECRET = 'test-secret-key-for-jwt-testing';
|
|
63
|
+
// RSA key pair for JWKS tests (RS256) — prevents algorithm confusion attacks
|
|
64
|
+
const { privateKey: TEST_JWKS_PRIVATE_KEY, publicKey: TEST_JWKS_PUBLIC_KEY } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } });
|
|
65
|
+
function createMockReq(headers = {}, ip) {
|
|
66
|
+
const addr = ip || '127.0.0.1';
|
|
67
|
+
return {
|
|
68
|
+
headers: { ...headers },
|
|
69
|
+
body: {},
|
|
70
|
+
ip: addr,
|
|
71
|
+
socket: { remoteAddress: addr },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createMockRes() {
|
|
75
|
+
const state = { statusCode: 0, body: null, headers: {} };
|
|
76
|
+
const res = {
|
|
77
|
+
status: jest.fn().mockImplementation((code) => {
|
|
78
|
+
state.statusCode = code;
|
|
79
|
+
return res;
|
|
80
|
+
}),
|
|
81
|
+
json: jest.fn().mockImplementation((data) => {
|
|
82
|
+
state.body = data;
|
|
83
|
+
return res;
|
|
84
|
+
}),
|
|
85
|
+
setHeader: jest.fn().mockImplementation((key, value) => {
|
|
86
|
+
state.headers[key] = value;
|
|
87
|
+
return res;
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
res,
|
|
92
|
+
getStatus: () => state.statusCode,
|
|
93
|
+
getBody: () => state.body,
|
|
94
|
+
getHeaders: () => state.headers,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function baseAuthConfig(overrides) {
|
|
98
|
+
return {
|
|
99
|
+
enabled: true,
|
|
100
|
+
api_keys: {
|
|
101
|
+
'valid-key': { workspace_id: 'ws_1', description: 'Test key' },
|
|
102
|
+
},
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const RBAC_CONFIG = {
|
|
107
|
+
enabled: true,
|
|
108
|
+
roles: {
|
|
109
|
+
admin: {
|
|
110
|
+
description: 'Full admin access',
|
|
111
|
+
permissions: ['admin:full'],
|
|
112
|
+
},
|
|
113
|
+
operator: {
|
|
114
|
+
description: 'Can execute tools and manage approvals',
|
|
115
|
+
permissions: ['tool:execute', 'approval:manage', 'trace:read', 'policy:read'],
|
|
116
|
+
},
|
|
117
|
+
readonly: {
|
|
118
|
+
description: 'Read-only access',
|
|
119
|
+
permissions: ['tool:execute:read', 'trace:read', 'policy:read'],
|
|
120
|
+
},
|
|
121
|
+
agent: {
|
|
122
|
+
description: 'AI agent - can execute tools',
|
|
123
|
+
permissions: ['tool:execute'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
default_role: 'agent',
|
|
127
|
+
};
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
// Tests
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
// Clear brute-force state between tests
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
auth_1.failedAuthAttempts.clear();
|
|
134
|
+
});
|
|
135
|
+
afterAll(() => {
|
|
136
|
+
(0, auth_1.stopBruteForceCleanup)();
|
|
137
|
+
});
|
|
138
|
+
describe('Auth Middleware', () => {
|
|
139
|
+
// -------------------------------------------------------------------------
|
|
140
|
+
// Backward compatibility: auth disabled
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
describe('Auth disabled mode', () => {
|
|
143
|
+
it('should pass through when auth is disabled', async () => {
|
|
144
|
+
const config = baseAuthConfig({ enabled: false });
|
|
145
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
146
|
+
const req = createMockReq();
|
|
147
|
+
const { res } = createMockRes();
|
|
148
|
+
const next = jest.fn();
|
|
149
|
+
await middleware(req, res, next);
|
|
150
|
+
expect(next).toHaveBeenCalled();
|
|
151
|
+
expect(req.workspace_id).toBe('ws_default');
|
|
152
|
+
});
|
|
153
|
+
it('should set auth context with method none when disabled', async () => {
|
|
154
|
+
const config = baseAuthConfig({ enabled: false });
|
|
155
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
156
|
+
const req = createMockReq();
|
|
157
|
+
const { res } = createMockRes();
|
|
158
|
+
const next = jest.fn();
|
|
159
|
+
await middleware(req, res, next);
|
|
160
|
+
const auth = req.auth;
|
|
161
|
+
expect(auth).toBeDefined();
|
|
162
|
+
expect(auth.auth_method).toBe('none');
|
|
163
|
+
expect(auth.workspace_id).toBe('ws_default');
|
|
164
|
+
expect(auth.actor_id).toBe('anonymous');
|
|
165
|
+
expect(auth.roles).toEqual([]);
|
|
166
|
+
expect(auth.permissions).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
it('should not require any headers when auth is disabled', async () => {
|
|
169
|
+
const config = baseAuthConfig({ enabled: false, api_keys: {} });
|
|
170
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
171
|
+
const req = createMockReq();
|
|
172
|
+
const { res } = createMockRes();
|
|
173
|
+
const next = jest.fn();
|
|
174
|
+
await middleware(req, res, next);
|
|
175
|
+
expect(next).toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
// API Key auth
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
describe('API Key auth', () => {
|
|
182
|
+
it('should accept valid API key via X-API-Key header', async () => {
|
|
183
|
+
const config = baseAuthConfig();
|
|
184
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
185
|
+
const req = createMockReq({ 'x-api-key': 'valid-key' });
|
|
186
|
+
const { res } = createMockRes();
|
|
187
|
+
const next = jest.fn();
|
|
188
|
+
await middleware(req, res, next);
|
|
189
|
+
expect(next).toHaveBeenCalled();
|
|
190
|
+
expect(req.workspace_id).toBe('ws_1');
|
|
191
|
+
});
|
|
192
|
+
it('should accept valid API key via Bearer token', async () => {
|
|
193
|
+
const config = baseAuthConfig();
|
|
194
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
195
|
+
const req = createMockReq({ authorization: 'Bearer valid-key' });
|
|
196
|
+
const { res } = createMockRes();
|
|
197
|
+
const next = jest.fn();
|
|
198
|
+
await middleware(req, res, next);
|
|
199
|
+
expect(next).toHaveBeenCalled();
|
|
200
|
+
expect(req.workspace_id).toBe('ws_1');
|
|
201
|
+
});
|
|
202
|
+
it('should return 401 when no key is provided', async () => {
|
|
203
|
+
const config = baseAuthConfig();
|
|
204
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
205
|
+
const req = createMockReq();
|
|
206
|
+
const mock = createMockRes();
|
|
207
|
+
const next = jest.fn();
|
|
208
|
+
await middleware(req, mock.res, next);
|
|
209
|
+
expect(next).not.toHaveBeenCalled();
|
|
210
|
+
expect(mock.getStatus()).toBe(401);
|
|
211
|
+
expect(mock.getBody().error).toContain('Missing API key');
|
|
212
|
+
});
|
|
213
|
+
it('should return 403 for invalid API key', async () => {
|
|
214
|
+
const config = baseAuthConfig();
|
|
215
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
216
|
+
const req = createMockReq({ 'x-api-key': 'bad-key' });
|
|
217
|
+
const mock = createMockRes();
|
|
218
|
+
const next = jest.fn();
|
|
219
|
+
await middleware(req, mock.res, next);
|
|
220
|
+
expect(next).not.toHaveBeenCalled();
|
|
221
|
+
expect(mock.getStatus()).toBe(403);
|
|
222
|
+
expect(mock.getBody().error).toContain('Invalid API key');
|
|
223
|
+
});
|
|
224
|
+
it('should return 403 for revoked API key', async () => {
|
|
225
|
+
const config = baseAuthConfig({
|
|
226
|
+
api_keys: {
|
|
227
|
+
'revoked-key': { workspace_id: 'ws_1', revoked: true },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
231
|
+
const req = createMockReq({ 'x-api-key': 'revoked-key' });
|
|
232
|
+
const mock = createMockRes();
|
|
233
|
+
const next = jest.fn();
|
|
234
|
+
await middleware(req, mock.res, next);
|
|
235
|
+
expect(next).not.toHaveBeenCalled();
|
|
236
|
+
expect(mock.getStatus()).toBe(403);
|
|
237
|
+
expect(mock.getBody().error).toContain('revoked');
|
|
238
|
+
});
|
|
239
|
+
it('should return 403 for expired API key', async () => {
|
|
240
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
|
|
241
|
+
const config = baseAuthConfig({
|
|
242
|
+
api_keys: {
|
|
243
|
+
'expired-key': { workspace_id: 'ws_1', expires_at: pastDate },
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
247
|
+
const req = createMockReq({ 'x-api-key': 'expired-key' });
|
|
248
|
+
const mock = createMockRes();
|
|
249
|
+
const next = jest.fn();
|
|
250
|
+
await middleware(req, mock.res, next);
|
|
251
|
+
expect(next).not.toHaveBeenCalled();
|
|
252
|
+
expect(mock.getStatus()).toBe(403);
|
|
253
|
+
expect(mock.getBody().error).toContain('expired');
|
|
254
|
+
});
|
|
255
|
+
it('should accept non-expired API key', async () => {
|
|
256
|
+
const futureDate = new Date(Date.now() + 86400000).toISOString(); // 1 day from now
|
|
257
|
+
const config = baseAuthConfig({
|
|
258
|
+
api_keys: {
|
|
259
|
+
'fresh-key': { workspace_id: 'ws_2', expires_at: futureDate },
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
263
|
+
const req = createMockReq({ 'x-api-key': 'fresh-key' });
|
|
264
|
+
const { res } = createMockRes();
|
|
265
|
+
const next = jest.fn();
|
|
266
|
+
await middleware(req, res, next);
|
|
267
|
+
expect(next).toHaveBeenCalled();
|
|
268
|
+
expect(req.workspace_id).toBe('ws_2');
|
|
269
|
+
});
|
|
270
|
+
it('should update last_used_at on successful auth', async () => {
|
|
271
|
+
const config = baseAuthConfig({
|
|
272
|
+
api_keys: {
|
|
273
|
+
'track-key': { workspace_id: 'ws_1' },
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
277
|
+
const req = createMockReq({ 'x-api-key': 'track-key' });
|
|
278
|
+
const { res } = createMockRes();
|
|
279
|
+
const next = jest.fn();
|
|
280
|
+
await middleware(req, res, next);
|
|
281
|
+
expect(config.api_keys['track-key'].last_used_at).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
it('should set auth context for API key auth', async () => {
|
|
284
|
+
const config = baseAuthConfig({
|
|
285
|
+
api_keys: {
|
|
286
|
+
'full-key': { workspace_id: 'ws_3', description: 'Full key', roles: ['admin'] },
|
|
287
|
+
},
|
|
288
|
+
rbac: RBAC_CONFIG,
|
|
289
|
+
});
|
|
290
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
291
|
+
const req = createMockReq({ 'x-api-key': 'full-key' });
|
|
292
|
+
const { res } = createMockRes();
|
|
293
|
+
const next = jest.fn();
|
|
294
|
+
await middleware(req, res, next);
|
|
295
|
+
const auth = req.auth;
|
|
296
|
+
expect(auth).toBeDefined();
|
|
297
|
+
expect(auth.auth_method).toBe('api_key');
|
|
298
|
+
expect(auth.workspace_id).toBe('ws_3');
|
|
299
|
+
expect(auth.roles).toEqual(['admin']);
|
|
300
|
+
expect(auth.permissions).toContain('admin:full');
|
|
301
|
+
expect(auth.api_key_id).toBe('full-key');
|
|
302
|
+
});
|
|
303
|
+
it('should maintain backward compat with old-style api_keys config', async () => {
|
|
304
|
+
// Old-style: just { workspace_id, description } - no roles, no expires_at, etc.
|
|
305
|
+
const config = {
|
|
306
|
+
enabled: true,
|
|
307
|
+
api_keys: {
|
|
308
|
+
'old-key': { workspace_id: 'ws_legacy', description: 'Legacy key' },
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
312
|
+
const req = createMockReq({ 'x-api-key': 'old-key' });
|
|
313
|
+
const { res } = createMockRes();
|
|
314
|
+
const next = jest.fn();
|
|
315
|
+
await middleware(req, res, next);
|
|
316
|
+
expect(next).toHaveBeenCalled();
|
|
317
|
+
expect(req.workspace_id).toBe('ws_legacy');
|
|
318
|
+
expect(req.api_key_description).toBe('Legacy key');
|
|
319
|
+
const auth = req.auth;
|
|
320
|
+
expect(auth.auth_method).toBe('api_key');
|
|
321
|
+
expect(auth.roles).toEqual([]);
|
|
322
|
+
expect(auth.permissions).toEqual([]);
|
|
323
|
+
});
|
|
324
|
+
it('should prefer X-API-Key header over Authorization Bearer', async () => {
|
|
325
|
+
const config = baseAuthConfig({
|
|
326
|
+
api_keys: {
|
|
327
|
+
'header-key': { workspace_id: 'ws_header' },
|
|
328
|
+
'bearer-key': { workspace_id: 'ws_bearer' },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
332
|
+
const req = createMockReq({
|
|
333
|
+
'x-api-key': 'header-key',
|
|
334
|
+
authorization: 'Bearer bearer-key',
|
|
335
|
+
});
|
|
336
|
+
const { res } = createMockRes();
|
|
337
|
+
const next = jest.fn();
|
|
338
|
+
await middleware(req, res, next);
|
|
339
|
+
// X-API-Key should take precedence
|
|
340
|
+
expect(req.workspace_id).toBe('ws_header');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
// -------------------------------------------------------------------------
|
|
344
|
+
// Hash-based API key lookup (timing attack prevention)
|
|
345
|
+
// -------------------------------------------------------------------------
|
|
346
|
+
describe('Hash-based API key lookup', () => {
|
|
347
|
+
it('should authenticate valid key using hash-based lookup', async () => {
|
|
348
|
+
const config = baseAuthConfig({
|
|
349
|
+
api_keys: {
|
|
350
|
+
'hash-test-key-abc': { workspace_id: 'ws_hash', description: 'Hash test key' },
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
354
|
+
const req = createMockReq({ 'x-api-key': 'hash-test-key-abc' });
|
|
355
|
+
const { res } = createMockRes();
|
|
356
|
+
const next = jest.fn();
|
|
357
|
+
await middleware(req, res, next);
|
|
358
|
+
expect(next).toHaveBeenCalled();
|
|
359
|
+
expect(req.workspace_id).toBe('ws_hash');
|
|
360
|
+
});
|
|
361
|
+
it('should reject invalid key with hash-based lookup', async () => {
|
|
362
|
+
const config = baseAuthConfig({
|
|
363
|
+
api_keys: {
|
|
364
|
+
'real-key-123': { workspace_id: 'ws_1' },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
368
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key-456' });
|
|
369
|
+
const mock = createMockRes();
|
|
370
|
+
const next = jest.fn();
|
|
371
|
+
await middleware(req, mock.res, next);
|
|
372
|
+
expect(next).not.toHaveBeenCalled();
|
|
373
|
+
expect(mock.getStatus()).toBe(403);
|
|
374
|
+
});
|
|
375
|
+
it('should still detect revoked keys after hash-based lookup', async () => {
|
|
376
|
+
const config = baseAuthConfig({
|
|
377
|
+
api_keys: {
|
|
378
|
+
'revoked-hash-key': { workspace_id: 'ws_1', revoked: true },
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
382
|
+
const req = createMockReq({ 'x-api-key': 'revoked-hash-key' });
|
|
383
|
+
const mock = createMockRes();
|
|
384
|
+
const next = jest.fn();
|
|
385
|
+
await middleware(req, mock.res, next);
|
|
386
|
+
expect(next).not.toHaveBeenCalled();
|
|
387
|
+
expect(mock.getStatus()).toBe(403);
|
|
388
|
+
expect(mock.getBody().error).toContain('revoked');
|
|
389
|
+
});
|
|
390
|
+
it('should still detect expired keys after hash-based lookup', async () => {
|
|
391
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString();
|
|
392
|
+
const config = baseAuthConfig({
|
|
393
|
+
api_keys: {
|
|
394
|
+
'expired-hash-key': { workspace_id: 'ws_1', expires_at: pastDate },
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
398
|
+
const req = createMockReq({ 'x-api-key': 'expired-hash-key' });
|
|
399
|
+
const mock = createMockRes();
|
|
400
|
+
const next = jest.fn();
|
|
401
|
+
await middleware(req, mock.res, next);
|
|
402
|
+
expect(next).not.toHaveBeenCalled();
|
|
403
|
+
expect(mock.getStatus()).toBe(403);
|
|
404
|
+
expect(mock.getBody().error).toContain('expired');
|
|
405
|
+
});
|
|
406
|
+
it('should work with multiple keys using hash-based lookup', async () => {
|
|
407
|
+
const config = baseAuthConfig({
|
|
408
|
+
api_keys: {
|
|
409
|
+
'key-alpha': { workspace_id: 'ws_alpha' },
|
|
410
|
+
'key-beta': { workspace_id: 'ws_beta' },
|
|
411
|
+
'key-gamma': { workspace_id: 'ws_gamma' },
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
415
|
+
// Test each key matches correctly
|
|
416
|
+
for (const [key, expectedWs] of [
|
|
417
|
+
['key-alpha', 'ws_alpha'],
|
|
418
|
+
['key-beta', 'ws_beta'],
|
|
419
|
+
['key-gamma', 'ws_gamma'],
|
|
420
|
+
]) {
|
|
421
|
+
const req = createMockReq({ 'x-api-key': key });
|
|
422
|
+
const { res } = createMockRes();
|
|
423
|
+
const next = jest.fn();
|
|
424
|
+
await middleware(req, res, next);
|
|
425
|
+
expect(next).toHaveBeenCalled();
|
|
426
|
+
expect(req.workspace_id).toBe(expectedWs);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
// -------------------------------------------------------------------------
|
|
431
|
+
// Hash-based API key lookup (timing attack prevention)
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
describe('Hash-based API key lookup', () => {
|
|
434
|
+
it('should authenticate valid key using hash-based lookup', async () => {
|
|
435
|
+
const config = baseAuthConfig({
|
|
436
|
+
api_keys: {
|
|
437
|
+
'hash-test-key-abc': { workspace_id: 'ws_hash', description: 'Hash test key' },
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
441
|
+
const req = createMockReq({ 'x-api-key': 'hash-test-key-abc' });
|
|
442
|
+
const { res } = createMockRes();
|
|
443
|
+
const next = jest.fn();
|
|
444
|
+
await middleware(req, res, next);
|
|
445
|
+
expect(next).toHaveBeenCalled();
|
|
446
|
+
expect(req.workspace_id).toBe('ws_hash');
|
|
447
|
+
});
|
|
448
|
+
it('should reject invalid key with hash-based lookup', async () => {
|
|
449
|
+
const config = baseAuthConfig({
|
|
450
|
+
api_keys: {
|
|
451
|
+
'real-key-123': { workspace_id: 'ws_1' },
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
455
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key-456' });
|
|
456
|
+
const mock = createMockRes();
|
|
457
|
+
const next = jest.fn();
|
|
458
|
+
await middleware(req, mock.res, next);
|
|
459
|
+
expect(next).not.toHaveBeenCalled();
|
|
460
|
+
expect(mock.getStatus()).toBe(403);
|
|
461
|
+
});
|
|
462
|
+
it('should still detect revoked keys after hash-based lookup', async () => {
|
|
463
|
+
const config = baseAuthConfig({
|
|
464
|
+
api_keys: {
|
|
465
|
+
'revoked-hash-key': { workspace_id: 'ws_1', revoked: true },
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
469
|
+
const req = createMockReq({ 'x-api-key': 'revoked-hash-key' });
|
|
470
|
+
const mock = createMockRes();
|
|
471
|
+
const next = jest.fn();
|
|
472
|
+
await middleware(req, mock.res, next);
|
|
473
|
+
expect(next).not.toHaveBeenCalled();
|
|
474
|
+
expect(mock.getStatus()).toBe(403);
|
|
475
|
+
expect(mock.getBody().error).toContain('revoked');
|
|
476
|
+
});
|
|
477
|
+
it('should still detect expired keys after hash-based lookup', async () => {
|
|
478
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString();
|
|
479
|
+
const config = baseAuthConfig({
|
|
480
|
+
api_keys: {
|
|
481
|
+
'expired-hash-key': { workspace_id: 'ws_1', expires_at: pastDate },
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
485
|
+
const req = createMockReq({ 'x-api-key': 'expired-hash-key' });
|
|
486
|
+
const mock = createMockRes();
|
|
487
|
+
const next = jest.fn();
|
|
488
|
+
await middleware(req, mock.res, next);
|
|
489
|
+
expect(next).not.toHaveBeenCalled();
|
|
490
|
+
expect(mock.getStatus()).toBe(403);
|
|
491
|
+
expect(mock.getBody().error).toContain('expired');
|
|
492
|
+
});
|
|
493
|
+
it('should work with multiple keys using hash-based lookup', async () => {
|
|
494
|
+
const config = baseAuthConfig({
|
|
495
|
+
api_keys: {
|
|
496
|
+
'key-alpha': { workspace_id: 'ws_alpha' },
|
|
497
|
+
'key-beta': { workspace_id: 'ws_beta' },
|
|
498
|
+
'key-gamma': { workspace_id: 'ws_gamma' },
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
502
|
+
for (const [key, expectedWs] of [
|
|
503
|
+
['key-alpha', 'ws_alpha'],
|
|
504
|
+
['key-beta', 'ws_beta'],
|
|
505
|
+
['key-gamma', 'ws_gamma'],
|
|
506
|
+
]) {
|
|
507
|
+
const req = createMockReq({ 'x-api-key': key });
|
|
508
|
+
const { res } = createMockRes();
|
|
509
|
+
const next = jest.fn();
|
|
510
|
+
await middleware(req, res, next);
|
|
511
|
+
expect(next).toHaveBeenCalled();
|
|
512
|
+
expect(req.workspace_id).toBe(expectedWs);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
// -------------------------------------------------------------------------
|
|
517
|
+
// JWT auth with static secret
|
|
518
|
+
// -------------------------------------------------------------------------
|
|
519
|
+
describe('JWT auth with static secret', () => {
|
|
520
|
+
function jwtConfig(overrides) {
|
|
521
|
+
return {
|
|
522
|
+
enabled: true,
|
|
523
|
+
api_keys: {},
|
|
524
|
+
jwt: {
|
|
525
|
+
enabled: true,
|
|
526
|
+
secret: TEST_SECRET,
|
|
527
|
+
algorithms: ['HS256'],
|
|
528
|
+
},
|
|
529
|
+
...overrides,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
it('should authenticate with a valid JWT', async () => {
|
|
533
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt', roles: ['operator'] }, TEST_SECRET, { algorithm: 'HS256' });
|
|
534
|
+
const config = jwtConfig({ rbac: RBAC_CONFIG });
|
|
535
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
536
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
537
|
+
const { res } = createMockRes();
|
|
538
|
+
const next = jest.fn();
|
|
539
|
+
await middleware(req, res, next);
|
|
540
|
+
expect(next).toHaveBeenCalled();
|
|
541
|
+
const auth = req.auth;
|
|
542
|
+
expect(auth.auth_method).toBe('jwt');
|
|
543
|
+
expect(auth.workspace_id).toBe('ws_jwt');
|
|
544
|
+
expect(auth.actor_id).toBe('user-1');
|
|
545
|
+
expect(auth.roles).toEqual(['operator']);
|
|
546
|
+
expect(auth.permissions).toContain('tool:execute');
|
|
547
|
+
expect(auth.permissions).toContain('approval:manage');
|
|
548
|
+
});
|
|
549
|
+
it('should reject an expired JWT', async () => {
|
|
550
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt' }, TEST_SECRET, { algorithm: 'HS256', expiresIn: '-1s' });
|
|
551
|
+
const config = jwtConfig();
|
|
552
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
553
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
554
|
+
const mock = createMockRes();
|
|
555
|
+
const next = jest.fn();
|
|
556
|
+
await middleware(req, mock.res, next);
|
|
557
|
+
expect(next).not.toHaveBeenCalled();
|
|
558
|
+
expect(mock.getStatus()).toBe(401);
|
|
559
|
+
expect(mock.getBody().error).toContain('JWT verification failed');
|
|
560
|
+
});
|
|
561
|
+
it('should reject a JWT with invalid signature', async () => {
|
|
562
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt' }, 'wrong-secret', { algorithm: 'HS256' });
|
|
563
|
+
const config = jwtConfig();
|
|
564
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
565
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
566
|
+
const mock = createMockRes();
|
|
567
|
+
const next = jest.fn();
|
|
568
|
+
await middleware(req, mock.res, next);
|
|
569
|
+
expect(next).not.toHaveBeenCalled();
|
|
570
|
+
expect(mock.getStatus()).toBe(401);
|
|
571
|
+
expect(mock.getBody().error).toContain('JWT verification failed');
|
|
572
|
+
});
|
|
573
|
+
it('should use default workspace when workspace claim is missing', async () => {
|
|
574
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-2' }, TEST_SECRET, { algorithm: 'HS256' });
|
|
575
|
+
const config = jwtConfig();
|
|
576
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
577
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
578
|
+
const { res } = createMockRes();
|
|
579
|
+
const next = jest.fn();
|
|
580
|
+
await middleware(req, res, next);
|
|
581
|
+
const auth = req.auth;
|
|
582
|
+
expect(auth.workspace_id).toBe('ws_default');
|
|
583
|
+
});
|
|
584
|
+
it('should use custom claim mappings', async () => {
|
|
585
|
+
const token = jsonwebtoken_1.default.sign({ user_id: 'custom-user', tenant: 'custom-ws', groups: ['admin'] }, TEST_SECRET, { algorithm: 'HS256' });
|
|
586
|
+
const config = jwtConfig({
|
|
587
|
+
jwt: {
|
|
588
|
+
enabled: true,
|
|
589
|
+
secret: TEST_SECRET,
|
|
590
|
+
algorithms: ['HS256'],
|
|
591
|
+
workspace_claim: 'tenant',
|
|
592
|
+
roles_claim: 'groups',
|
|
593
|
+
actor_claim: 'user_id',
|
|
594
|
+
},
|
|
595
|
+
rbac: RBAC_CONFIG,
|
|
596
|
+
});
|
|
597
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
598
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
599
|
+
const { res } = createMockRes();
|
|
600
|
+
const next = jest.fn();
|
|
601
|
+
await middleware(req, res, next);
|
|
602
|
+
const auth = req.auth;
|
|
603
|
+
expect(auth.workspace_id).toBe('custom-ws');
|
|
604
|
+
expect(auth.actor_id).toBe('custom-user');
|
|
605
|
+
expect(auth.roles).toEqual(['admin']);
|
|
606
|
+
});
|
|
607
|
+
it('should validate issuer when configured', async () => {
|
|
608
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt' }, TEST_SECRET, { algorithm: 'HS256', issuer: 'wrong-issuer' });
|
|
609
|
+
const config = jwtConfig({
|
|
610
|
+
jwt: {
|
|
611
|
+
enabled: true,
|
|
612
|
+
secret: TEST_SECRET,
|
|
613
|
+
algorithms: ['HS256'],
|
|
614
|
+
issuer: 'expected-issuer',
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
618
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
619
|
+
const mock = createMockRes();
|
|
620
|
+
const next = jest.fn();
|
|
621
|
+
await middleware(req, mock.res, next);
|
|
622
|
+
expect(next).not.toHaveBeenCalled();
|
|
623
|
+
expect(mock.getStatus()).toBe(401);
|
|
624
|
+
});
|
|
625
|
+
it('should validate audience when configured', async () => {
|
|
626
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt' }, TEST_SECRET, { algorithm: 'HS256', audience: 'wrong-audience' });
|
|
627
|
+
const config = jwtConfig({
|
|
628
|
+
jwt: {
|
|
629
|
+
enabled: true,
|
|
630
|
+
secret: TEST_SECRET,
|
|
631
|
+
algorithms: ['HS256'],
|
|
632
|
+
audience: 'expected-audience',
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
636
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
637
|
+
const mock = createMockRes();
|
|
638
|
+
const next = jest.fn();
|
|
639
|
+
await middleware(req, mock.res, next);
|
|
640
|
+
expect(next).not.toHaveBeenCalled();
|
|
641
|
+
expect(mock.getStatus()).toBe(401);
|
|
642
|
+
});
|
|
643
|
+
it('should set backward compat workspace_id on request', async () => {
|
|
644
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_jwt_compat' }, TEST_SECRET, { algorithm: 'HS256' });
|
|
645
|
+
const config = jwtConfig();
|
|
646
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
647
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
648
|
+
const { res } = createMockRes();
|
|
649
|
+
const next = jest.fn();
|
|
650
|
+
await middleware(req, res, next);
|
|
651
|
+
expect(req.workspace_id).toBe('ws_jwt_compat');
|
|
652
|
+
});
|
|
653
|
+
it('should handle roles claim that is not an array gracefully', async () => {
|
|
654
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'user-1', workspace_id: 'ws_1', roles: 'not-an-array' }, TEST_SECRET, { algorithm: 'HS256' });
|
|
655
|
+
const config = jwtConfig();
|
|
656
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
657
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
658
|
+
const { res } = createMockRes();
|
|
659
|
+
const next = jest.fn();
|
|
660
|
+
await middleware(req, res, next);
|
|
661
|
+
expect(next).toHaveBeenCalled();
|
|
662
|
+
const auth = req.auth;
|
|
663
|
+
expect(auth.roles).toEqual([]);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
// -------------------------------------------------------------------------
|
|
667
|
+
// JWT auth with JWKS
|
|
668
|
+
// -------------------------------------------------------------------------
|
|
669
|
+
describe('JWT auth with JWKS', () => {
|
|
670
|
+
beforeEach(() => {
|
|
671
|
+
mockGetSigningKey.mockReset();
|
|
672
|
+
});
|
|
673
|
+
function jwksConfig(overrides) {
|
|
674
|
+
return {
|
|
675
|
+
enabled: true,
|
|
676
|
+
api_keys: {},
|
|
677
|
+
jwt: {
|
|
678
|
+
enabled: true,
|
|
679
|
+
jwks_uri: 'https://example.com/.well-known/jwks.json',
|
|
680
|
+
algorithms: ['RS256'],
|
|
681
|
+
},
|
|
682
|
+
...overrides,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
it('should authenticate with JWKS-verified JWT', async () => {
|
|
686
|
+
// Create a token signed with the RSA private key (RS256)
|
|
687
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwks-user', workspace_id: 'ws_jwks', roles: ['admin'] }, TEST_JWKS_PRIVATE_KEY, { algorithm: 'RS256', keyid: 'test-kid' });
|
|
688
|
+
// Mock JWKS signing key retrieval — returns the public key
|
|
689
|
+
mockGetSigningKey.mockImplementation((kid, callback) => {
|
|
690
|
+
callback(null, {
|
|
691
|
+
getPublicKey: () => TEST_JWKS_PUBLIC_KEY,
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
const config = jwksConfig({ rbac: RBAC_CONFIG });
|
|
695
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
696
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
697
|
+
const { res } = createMockRes();
|
|
698
|
+
const next = jest.fn();
|
|
699
|
+
await middleware(req, res, next);
|
|
700
|
+
expect(next).toHaveBeenCalled();
|
|
701
|
+
const auth = req.auth;
|
|
702
|
+
expect(auth.auth_method).toBe('jwt');
|
|
703
|
+
expect(auth.workspace_id).toBe('ws_jwks');
|
|
704
|
+
expect(auth.actor_id).toBe('jwks-user');
|
|
705
|
+
});
|
|
706
|
+
it('should reject when JWKS key retrieval fails', async () => {
|
|
707
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwks-user', workspace_id: 'ws_jwks' }, TEST_JWKS_PRIVATE_KEY, { algorithm: 'RS256', keyid: 'test-kid' });
|
|
708
|
+
mockGetSigningKey.mockImplementation((kid, callback) => {
|
|
709
|
+
callback(new Error('Unable to find signing key'));
|
|
710
|
+
});
|
|
711
|
+
const config = jwksConfig();
|
|
712
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
713
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
714
|
+
const mock = createMockRes();
|
|
715
|
+
const next = jest.fn();
|
|
716
|
+
await middleware(req, mock.res, next);
|
|
717
|
+
expect(next).not.toHaveBeenCalled();
|
|
718
|
+
expect(mock.getStatus()).toBe(401);
|
|
719
|
+
expect(mock.getBody().error).toContain('JWT verification failed');
|
|
720
|
+
});
|
|
721
|
+
it('should reject when JWKS returns no key', async () => {
|
|
722
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwks-user', workspace_id: 'ws_jwks' }, TEST_JWKS_PRIVATE_KEY, { algorithm: 'RS256', keyid: 'test-kid' });
|
|
723
|
+
mockGetSigningKey.mockImplementation((kid, callback) => {
|
|
724
|
+
callback(null, undefined);
|
|
725
|
+
});
|
|
726
|
+
const config = jwksConfig();
|
|
727
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
728
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
729
|
+
const mock = createMockRes();
|
|
730
|
+
const next = jest.fn();
|
|
731
|
+
await middleware(req, mock.res, next);
|
|
732
|
+
expect(next).not.toHaveBeenCalled();
|
|
733
|
+
expect(mock.getStatus()).toBe(401);
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
// -------------------------------------------------------------------------
|
|
737
|
+
// API key takes precedence over JWT
|
|
738
|
+
// -------------------------------------------------------------------------
|
|
739
|
+
describe('Multiple auth methods', () => {
|
|
740
|
+
it('should prefer API key over JWT when key matches', async () => {
|
|
741
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwt-user', workspace_id: 'ws_jwt' }, TEST_SECRET, { algorithm: 'HS256' });
|
|
742
|
+
const config = {
|
|
743
|
+
enabled: true,
|
|
744
|
+
api_keys: {
|
|
745
|
+
[token]: { workspace_id: 'ws_apikey', description: 'Token is also a key' },
|
|
746
|
+
},
|
|
747
|
+
jwt: { enabled: true, secret: TEST_SECRET, algorithms: ['HS256'] },
|
|
748
|
+
};
|
|
749
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
750
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
751
|
+
const { res } = createMockRes();
|
|
752
|
+
const next = jest.fn();
|
|
753
|
+
await middleware(req, res, next);
|
|
754
|
+
const auth = req.auth;
|
|
755
|
+
// API key should win
|
|
756
|
+
expect(auth.auth_method).toBe('api_key');
|
|
757
|
+
expect(auth.workspace_id).toBe('ws_apikey');
|
|
758
|
+
});
|
|
759
|
+
it('should fall through to JWT when bearer token is not a valid API key', async () => {
|
|
760
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwt-user', workspace_id: 'ws_jwt' }, TEST_SECRET, { algorithm: 'HS256' });
|
|
761
|
+
const config = {
|
|
762
|
+
enabled: true,
|
|
763
|
+
api_keys: {
|
|
764
|
+
'different-key': { workspace_id: 'ws_apikey' },
|
|
765
|
+
},
|
|
766
|
+
jwt: { enabled: true, secret: TEST_SECRET, algorithms: ['HS256'] },
|
|
767
|
+
};
|
|
768
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
769
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
770
|
+
const { res } = createMockRes();
|
|
771
|
+
const next = jest.fn();
|
|
772
|
+
await middleware(req, res, next);
|
|
773
|
+
const auth = req.auth;
|
|
774
|
+
expect(auth.auth_method).toBe('jwt');
|
|
775
|
+
expect(auth.workspace_id).toBe('ws_jwt');
|
|
776
|
+
});
|
|
777
|
+
it('should return 403 when bearer is not API key and JWT is not configured', async () => {
|
|
778
|
+
const config = {
|
|
779
|
+
enabled: true,
|
|
780
|
+
api_keys: {
|
|
781
|
+
'real-key': { workspace_id: 'ws_1' },
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
785
|
+
const req = createMockReq({ authorization: 'Bearer not-a-key' });
|
|
786
|
+
const mock = createMockRes();
|
|
787
|
+
const next = jest.fn();
|
|
788
|
+
await middleware(req, mock.res, next);
|
|
789
|
+
expect(next).not.toHaveBeenCalled();
|
|
790
|
+
expect(mock.getStatus()).toBe(403);
|
|
791
|
+
});
|
|
792
|
+
it('should return 403 when X-API-Key is not valid and JWT not configured', async () => {
|
|
793
|
+
const config = {
|
|
794
|
+
enabled: true,
|
|
795
|
+
api_keys: {
|
|
796
|
+
'real-key': { workspace_id: 'ws_1' },
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
800
|
+
const req = createMockReq({ 'x-api-key': 'bad-key' });
|
|
801
|
+
const mock = createMockRes();
|
|
802
|
+
const next = jest.fn();
|
|
803
|
+
await middleware(req, mock.res, next);
|
|
804
|
+
expect(next).not.toHaveBeenCalled();
|
|
805
|
+
expect(mock.getStatus()).toBe(403);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
// ===========================================================================
|
|
810
|
+
// resolvePermissions
|
|
811
|
+
// ===========================================================================
|
|
812
|
+
describe('resolvePermissions', () => {
|
|
813
|
+
it('should return empty when RBAC is not configured', () => {
|
|
814
|
+
expect((0, auth_1.resolvePermissions)(['admin'], undefined)).toEqual([]);
|
|
815
|
+
});
|
|
816
|
+
it('should return empty when RBAC is disabled', () => {
|
|
817
|
+
const rbac = { enabled: false, roles: { admin: { permissions: ['admin:full'] } } };
|
|
818
|
+
expect((0, auth_1.resolvePermissions)(['admin'], rbac)).toEqual([]);
|
|
819
|
+
});
|
|
820
|
+
it('should resolve single role to permissions', () => {
|
|
821
|
+
const perms = (0, auth_1.resolvePermissions)(['operator'], RBAC_CONFIG);
|
|
822
|
+
expect(perms).toContain('tool:execute');
|
|
823
|
+
expect(perms).toContain('approval:manage');
|
|
824
|
+
expect(perms).toContain('trace:read');
|
|
825
|
+
expect(perms).toContain('policy:read');
|
|
826
|
+
});
|
|
827
|
+
it('should resolve multiple roles and deduplicate', () => {
|
|
828
|
+
const perms = (0, auth_1.resolvePermissions)(['operator', 'readonly'], RBAC_CONFIG);
|
|
829
|
+
// Both have trace:read - should appear only once
|
|
830
|
+
const traceReadCount = perms.filter(p => p === 'trace:read').length;
|
|
831
|
+
expect(traceReadCount).toBe(1);
|
|
832
|
+
// Should have permissions from both roles
|
|
833
|
+
expect(perms).toContain('tool:execute');
|
|
834
|
+
expect(perms).toContain('tool:execute:read');
|
|
835
|
+
expect(perms).toContain('approval:manage');
|
|
836
|
+
});
|
|
837
|
+
it('should ignore unknown roles', () => {
|
|
838
|
+
const perms = (0, auth_1.resolvePermissions)(['nonexistent'], RBAC_CONFIG);
|
|
839
|
+
expect(perms).toEqual([]);
|
|
840
|
+
});
|
|
841
|
+
it('should handle empty roles array', () => {
|
|
842
|
+
const perms = (0, auth_1.resolvePermissions)([], RBAC_CONFIG);
|
|
843
|
+
expect(perms).toEqual([]);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
// ===========================================================================
|
|
847
|
+
// hasPermission
|
|
848
|
+
// ===========================================================================
|
|
849
|
+
describe('hasPermission', () => {
|
|
850
|
+
it('should return true for exact match', () => {
|
|
851
|
+
expect((0, auth_1.hasPermission)(['tool:execute', 'trace:read'], 'trace:read')).toBe(true);
|
|
852
|
+
});
|
|
853
|
+
it('should return false when permission is missing', () => {
|
|
854
|
+
expect((0, auth_1.hasPermission)(['trace:read'], 'tool:execute')).toBe(false);
|
|
855
|
+
});
|
|
856
|
+
it('should grant everything with admin:full', () => {
|
|
857
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'tool:execute')).toBe(true);
|
|
858
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'approval:manage')).toBe(true);
|
|
859
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'policy:write')).toBe(true);
|
|
860
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'tool:execute:admin')).toBe(true);
|
|
861
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'trace:read')).toBe(true);
|
|
862
|
+
expect((0, auth_1.hasPermission)(['admin:full'], 'custom:permission')).toBe(true);
|
|
863
|
+
});
|
|
864
|
+
it('should grant tool:execute:* sub-permissions from tool:execute', () => {
|
|
865
|
+
const perms = ['tool:execute'];
|
|
866
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute:read')).toBe(true);
|
|
867
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute:write')).toBe(true);
|
|
868
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute:delete')).toBe(true);
|
|
869
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute:admin')).toBe(true);
|
|
870
|
+
});
|
|
871
|
+
it('should NOT grant tool:execute from tool:execute:read', () => {
|
|
872
|
+
const perms = ['tool:execute:read'];
|
|
873
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute')).toBe(false);
|
|
874
|
+
expect((0, auth_1.hasPermission)(perms, 'tool:execute:write')).toBe(false);
|
|
875
|
+
});
|
|
876
|
+
it('should handle empty permissions', () => {
|
|
877
|
+
expect((0, auth_1.hasPermission)([], 'tool:execute')).toBe(false);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
// ===========================================================================
|
|
881
|
+
// RBAC Middleware
|
|
882
|
+
// ===========================================================================
|
|
883
|
+
describe('RBAC Middleware', () => {
|
|
884
|
+
const fullRbacConfig = {
|
|
885
|
+
enabled: true,
|
|
886
|
+
api_keys: {},
|
|
887
|
+
rbac: RBAC_CONFIG,
|
|
888
|
+
};
|
|
889
|
+
it('should pass through when RBAC is not enabled', () => {
|
|
890
|
+
const config = { enabled: true, api_keys: {} };
|
|
891
|
+
const middleware = (0, auth_1.createRBACMiddleware)(config, 'tool:execute');
|
|
892
|
+
const req = createMockReq();
|
|
893
|
+
const { res } = createMockRes();
|
|
894
|
+
const next = jest.fn();
|
|
895
|
+
middleware(req, res, next);
|
|
896
|
+
expect(next).toHaveBeenCalled();
|
|
897
|
+
});
|
|
898
|
+
it('should pass through when RBAC config is missing', () => {
|
|
899
|
+
const config = { enabled: true, api_keys: {} };
|
|
900
|
+
const middleware = (0, auth_1.createRBACMiddleware)(config, 'tool:execute');
|
|
901
|
+
const req = createMockReq();
|
|
902
|
+
const { res } = createMockRes();
|
|
903
|
+
const next = jest.fn();
|
|
904
|
+
middleware(req, res, next);
|
|
905
|
+
expect(next).toHaveBeenCalled();
|
|
906
|
+
});
|
|
907
|
+
it('should return 401 when auth context is missing', () => {
|
|
908
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute');
|
|
909
|
+
const req = createMockReq();
|
|
910
|
+
const mock = createMockRes();
|
|
911
|
+
const next = jest.fn();
|
|
912
|
+
middleware(req, mock.res, next);
|
|
913
|
+
expect(next).not.toHaveBeenCalled();
|
|
914
|
+
expect(mock.getStatus()).toBe(401);
|
|
915
|
+
});
|
|
916
|
+
it('should pass through when auth method is none', () => {
|
|
917
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute');
|
|
918
|
+
const req = createMockReq();
|
|
919
|
+
req.auth = {
|
|
920
|
+
workspace_id: 'ws_default',
|
|
921
|
+
actor_id: 'anonymous',
|
|
922
|
+
roles: [],
|
|
923
|
+
permissions: [],
|
|
924
|
+
auth_method: 'none',
|
|
925
|
+
};
|
|
926
|
+
const { res } = createMockRes();
|
|
927
|
+
const next = jest.fn();
|
|
928
|
+
middleware(req, res, next);
|
|
929
|
+
expect(next).toHaveBeenCalled();
|
|
930
|
+
});
|
|
931
|
+
it('should allow when user has required permission', () => {
|
|
932
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute');
|
|
933
|
+
const req = createMockReq();
|
|
934
|
+
req.auth = {
|
|
935
|
+
workspace_id: 'ws_1',
|
|
936
|
+
actor_id: 'user-1',
|
|
937
|
+
roles: ['operator'],
|
|
938
|
+
permissions: ['tool:execute', 'approval:manage', 'trace:read', 'policy:read'],
|
|
939
|
+
auth_method: 'api_key',
|
|
940
|
+
};
|
|
941
|
+
const { res } = createMockRes();
|
|
942
|
+
const next = jest.fn();
|
|
943
|
+
middleware(req, res, next);
|
|
944
|
+
expect(next).toHaveBeenCalled();
|
|
945
|
+
});
|
|
946
|
+
it('should deny when user lacks required permission', () => {
|
|
947
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'policy:write');
|
|
948
|
+
const req = createMockReq();
|
|
949
|
+
req.auth = {
|
|
950
|
+
workspace_id: 'ws_1',
|
|
951
|
+
actor_id: 'user-1',
|
|
952
|
+
roles: ['readonly'],
|
|
953
|
+
permissions: ['tool:execute:read', 'trace:read', 'policy:read'],
|
|
954
|
+
auth_method: 'api_key',
|
|
955
|
+
};
|
|
956
|
+
const mock = createMockRes();
|
|
957
|
+
const next = jest.fn();
|
|
958
|
+
middleware(req, mock.res, next);
|
|
959
|
+
expect(next).not.toHaveBeenCalled();
|
|
960
|
+
expect(mock.getStatus()).toBe(403);
|
|
961
|
+
expect(mock.getBody().error).toContain('Insufficient permissions');
|
|
962
|
+
expect(mock.getBody().error).toContain('policy:write');
|
|
963
|
+
});
|
|
964
|
+
it('should allow admin:full to bypass any permission check', () => {
|
|
965
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'policy:write');
|
|
966
|
+
const req = createMockReq();
|
|
967
|
+
req.auth = {
|
|
968
|
+
workspace_id: 'ws_1',
|
|
969
|
+
actor_id: 'admin-1',
|
|
970
|
+
roles: ['admin'],
|
|
971
|
+
permissions: ['admin:full'],
|
|
972
|
+
auth_method: 'api_key',
|
|
973
|
+
};
|
|
974
|
+
const { res } = createMockRes();
|
|
975
|
+
const next = jest.fn();
|
|
976
|
+
middleware(req, res, next);
|
|
977
|
+
expect(next).toHaveBeenCalled();
|
|
978
|
+
});
|
|
979
|
+
it('should apply default role when user has no roles', () => {
|
|
980
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute');
|
|
981
|
+
const req = createMockReq();
|
|
982
|
+
req.auth = {
|
|
983
|
+
workspace_id: 'ws_1',
|
|
984
|
+
actor_id: 'user-no-roles',
|
|
985
|
+
roles: [],
|
|
986
|
+
permissions: [],
|
|
987
|
+
auth_method: 'api_key',
|
|
988
|
+
};
|
|
989
|
+
const { res } = createMockRes();
|
|
990
|
+
const next = jest.fn();
|
|
991
|
+
middleware(req, res, next);
|
|
992
|
+
// Default role is 'agent' which has 'tool:execute'
|
|
993
|
+
expect(next).toHaveBeenCalled();
|
|
994
|
+
const auth = req.auth;
|
|
995
|
+
expect(auth.roles).toEqual(['agent']);
|
|
996
|
+
expect(auth.permissions).toContain('tool:execute');
|
|
997
|
+
});
|
|
998
|
+
it('should deny when default role does not have required permission', () => {
|
|
999
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'policy:write');
|
|
1000
|
+
const req = createMockReq();
|
|
1001
|
+
req.auth = {
|
|
1002
|
+
workspace_id: 'ws_1',
|
|
1003
|
+
actor_id: 'user-no-roles',
|
|
1004
|
+
roles: [],
|
|
1005
|
+
permissions: [],
|
|
1006
|
+
auth_method: 'api_key',
|
|
1007
|
+
};
|
|
1008
|
+
const mock = createMockRes();
|
|
1009
|
+
const next = jest.fn();
|
|
1010
|
+
middleware(req, mock.res, next);
|
|
1011
|
+
// Default role 'agent' only has 'tool:execute', not 'policy:write'
|
|
1012
|
+
expect(next).not.toHaveBeenCalled();
|
|
1013
|
+
expect(mock.getStatus()).toBe(403);
|
|
1014
|
+
});
|
|
1015
|
+
it('should handle capability-specific permission (readonly can read)', () => {
|
|
1016
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute:read');
|
|
1017
|
+
const req = createMockReq();
|
|
1018
|
+
req.auth = {
|
|
1019
|
+
workspace_id: 'ws_1',
|
|
1020
|
+
actor_id: 'readonly-user',
|
|
1021
|
+
roles: ['readonly'],
|
|
1022
|
+
permissions: ['tool:execute:read', 'trace:read', 'policy:read'],
|
|
1023
|
+
auth_method: 'api_key',
|
|
1024
|
+
};
|
|
1025
|
+
const { res } = createMockRes();
|
|
1026
|
+
const next = jest.fn();
|
|
1027
|
+
middleware(req, res, next);
|
|
1028
|
+
expect(next).toHaveBeenCalled();
|
|
1029
|
+
});
|
|
1030
|
+
it('should deny capability-specific permission (readonly cannot write)', () => {
|
|
1031
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute:write');
|
|
1032
|
+
const req = createMockReq();
|
|
1033
|
+
req.auth = {
|
|
1034
|
+
workspace_id: 'ws_1',
|
|
1035
|
+
actor_id: 'readonly-user',
|
|
1036
|
+
roles: ['readonly'],
|
|
1037
|
+
permissions: ['tool:execute:read', 'trace:read', 'policy:read'],
|
|
1038
|
+
auth_method: 'api_key',
|
|
1039
|
+
};
|
|
1040
|
+
const mock = createMockRes();
|
|
1041
|
+
const next = jest.fn();
|
|
1042
|
+
middleware(req, mock.res, next);
|
|
1043
|
+
expect(next).not.toHaveBeenCalled();
|
|
1044
|
+
expect(mock.getStatus()).toBe(403);
|
|
1045
|
+
});
|
|
1046
|
+
it('should allow operator to execute any tool capability via tool:execute', () => {
|
|
1047
|
+
// operator has 'tool:execute' which should grant 'tool:execute:write', 'tool:execute:delete', etc.
|
|
1048
|
+
const middleware = (0, auth_1.createRBACMiddleware)(fullRbacConfig, 'tool:execute:delete');
|
|
1049
|
+
const req = createMockReq();
|
|
1050
|
+
req.auth = {
|
|
1051
|
+
workspace_id: 'ws_1',
|
|
1052
|
+
actor_id: 'op-user',
|
|
1053
|
+
roles: ['operator'],
|
|
1054
|
+
permissions: ['tool:execute', 'approval:manage', 'trace:read', 'policy:read'],
|
|
1055
|
+
auth_method: 'api_key',
|
|
1056
|
+
};
|
|
1057
|
+
const { res } = createMockRes();
|
|
1058
|
+
const next = jest.fn();
|
|
1059
|
+
middleware(req, res, next);
|
|
1060
|
+
expect(next).toHaveBeenCalled();
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
// ===========================================================================
|
|
1064
|
+
// Request enrichment (AuthContext on request)
|
|
1065
|
+
// ===========================================================================
|
|
1066
|
+
describe('Request enrichment', () => {
|
|
1067
|
+
it('should set all AuthContext fields for API key auth', async () => {
|
|
1068
|
+
const config = {
|
|
1069
|
+
enabled: true,
|
|
1070
|
+
api_keys: {
|
|
1071
|
+
'enrichment-key': {
|
|
1072
|
+
workspace_id: 'ws_enrich',
|
|
1073
|
+
description: 'Enrichment test',
|
|
1074
|
+
roles: ['operator'],
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
rbac: RBAC_CONFIG,
|
|
1078
|
+
};
|
|
1079
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1080
|
+
const req = createMockReq({ 'x-api-key': 'enrichment-key' });
|
|
1081
|
+
const { res } = createMockRes();
|
|
1082
|
+
const next = jest.fn();
|
|
1083
|
+
await middleware(req, res, next);
|
|
1084
|
+
const auth = req.auth;
|
|
1085
|
+
expect(auth).toEqual({
|
|
1086
|
+
workspace_id: 'ws_enrich',
|
|
1087
|
+
actor_id: expect.stringContaining('apikey:'),
|
|
1088
|
+
roles: ['operator'],
|
|
1089
|
+
permissions: expect.arrayContaining(['tool:execute', 'approval:manage']),
|
|
1090
|
+
auth_method: 'api_key',
|
|
1091
|
+
api_key_id: 'enrichment-key',
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
it('should set all AuthContext fields for JWT auth', async () => {
|
|
1095
|
+
const token = jsonwebtoken_1.default.sign({ sub: 'jwt-actor', workspace_id: 'ws_jwt_enrich', roles: ['admin'] }, TEST_SECRET, { algorithm: 'HS256' });
|
|
1096
|
+
const config = {
|
|
1097
|
+
enabled: true,
|
|
1098
|
+
api_keys: {},
|
|
1099
|
+
jwt: { enabled: true, secret: TEST_SECRET, algorithms: ['HS256'] },
|
|
1100
|
+
rbac: RBAC_CONFIG,
|
|
1101
|
+
};
|
|
1102
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1103
|
+
const req = createMockReq({ authorization: `Bearer ${token}` });
|
|
1104
|
+
const { res } = createMockRes();
|
|
1105
|
+
const next = jest.fn();
|
|
1106
|
+
await middleware(req, res, next);
|
|
1107
|
+
const auth = req.auth;
|
|
1108
|
+
expect(auth).toEqual({
|
|
1109
|
+
workspace_id: 'ws_jwt_enrich',
|
|
1110
|
+
actor_id: 'jwt-actor',
|
|
1111
|
+
roles: ['admin'],
|
|
1112
|
+
permissions: ['admin:full'],
|
|
1113
|
+
auth_method: 'jwt',
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
it('should set both auth and backward-compat workspace_id', async () => {
|
|
1117
|
+
const config = {
|
|
1118
|
+
enabled: true,
|
|
1119
|
+
api_keys: {
|
|
1120
|
+
'compat-key': { workspace_id: 'ws_compat' },
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1124
|
+
const req = createMockReq({ 'x-api-key': 'compat-key' });
|
|
1125
|
+
const { res } = createMockRes();
|
|
1126
|
+
const next = jest.fn();
|
|
1127
|
+
await middleware(req, res, next);
|
|
1128
|
+
expect(req.workspace_id).toBe('ws_compat');
|
|
1129
|
+
expect(req.auth.workspace_id).toBe('ws_compat');
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
// ===========================================================================
|
|
1133
|
+
// Brute-force protection (S8)
|
|
1134
|
+
// ===========================================================================
|
|
1135
|
+
describe('Brute-force protection', () => {
|
|
1136
|
+
it('should allow auth attempts within the threshold', async () => {
|
|
1137
|
+
const config = baseAuthConfig();
|
|
1138
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1139
|
+
const ip = '10.0.0.1';
|
|
1140
|
+
// 4 failed attempts should still be OK
|
|
1141
|
+
for (let i = 0; i < 4; i++) {
|
|
1142
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1143
|
+
const mock = createMockRes();
|
|
1144
|
+
const next = jest.fn();
|
|
1145
|
+
await middleware(req, mock.res, next);
|
|
1146
|
+
expect(mock.getStatus()).toBe(403);
|
|
1147
|
+
}
|
|
1148
|
+
// 5th attempt should still not be locked (lock happens after 5 failures)
|
|
1149
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1150
|
+
const mock = createMockRes();
|
|
1151
|
+
const next = jest.fn();
|
|
1152
|
+
await middleware(req, mock.res, next);
|
|
1153
|
+
// 5th failure triggers lock, but this attempt itself still gets a 403
|
|
1154
|
+
expect(mock.getStatus()).toBe(403);
|
|
1155
|
+
});
|
|
1156
|
+
it('should return 429 after exceeding failed auth threshold', async () => {
|
|
1157
|
+
const config = baseAuthConfig();
|
|
1158
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1159
|
+
const ip = '10.0.0.2';
|
|
1160
|
+
// Generate 5 failed attempts to trigger lockout
|
|
1161
|
+
for (let i = 0; i < 5; i++) {
|
|
1162
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1163
|
+
const mock = createMockRes();
|
|
1164
|
+
const next = jest.fn();
|
|
1165
|
+
await middleware(req, mock.res, next);
|
|
1166
|
+
}
|
|
1167
|
+
// 6th attempt should be locked out
|
|
1168
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1169
|
+
const mock = createMockRes();
|
|
1170
|
+
const next = jest.fn();
|
|
1171
|
+
await middleware(req, mock.res, next);
|
|
1172
|
+
expect(mock.getStatus()).toBe(429);
|
|
1173
|
+
expect(mock.getBody().error).toContain('Too many failed');
|
|
1174
|
+
});
|
|
1175
|
+
it('should include Retry-After header when locked out', async () => {
|
|
1176
|
+
const config = baseAuthConfig();
|
|
1177
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1178
|
+
const ip = '10.0.0.3';
|
|
1179
|
+
// Trigger lockout
|
|
1180
|
+
for (let i = 0; i < 5; i++) {
|
|
1181
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1182
|
+
const mock = createMockRes();
|
|
1183
|
+
await middleware(req, mock.res, jest.fn());
|
|
1184
|
+
}
|
|
1185
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1186
|
+
const mockRes = {
|
|
1187
|
+
status: jest.fn().mockReturnThis(),
|
|
1188
|
+
json: jest.fn().mockReturnThis(),
|
|
1189
|
+
setHeader: jest.fn(),
|
|
1190
|
+
};
|
|
1191
|
+
await middleware(req, mockRes, jest.fn());
|
|
1192
|
+
expect(mockRes.status).toHaveBeenCalledWith(429);
|
|
1193
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('Retry-After', expect.any(String));
|
|
1194
|
+
});
|
|
1195
|
+
it('should clear lockout on successful auth', async () => {
|
|
1196
|
+
const config = baseAuthConfig();
|
|
1197
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1198
|
+
const ip = '10.0.0.4';
|
|
1199
|
+
// Generate some failures (below threshold)
|
|
1200
|
+
for (let i = 0; i < 3; i++) {
|
|
1201
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, ip);
|
|
1202
|
+
const mock = createMockRes();
|
|
1203
|
+
await middleware(req, mock.res, jest.fn());
|
|
1204
|
+
}
|
|
1205
|
+
// Successful auth should clear the count
|
|
1206
|
+
const req = createMockReq({ 'x-api-key': 'valid-key' }, ip);
|
|
1207
|
+
const { res } = createMockRes();
|
|
1208
|
+
const next = jest.fn();
|
|
1209
|
+
await middleware(req, res, next);
|
|
1210
|
+
expect(next).toHaveBeenCalled();
|
|
1211
|
+
// Verify the entry was cleared
|
|
1212
|
+
expect(auth_1.failedAuthAttempts.has(ip)).toBe(false);
|
|
1213
|
+
});
|
|
1214
|
+
it('should track different IPs independently', async () => {
|
|
1215
|
+
const config = baseAuthConfig();
|
|
1216
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1217
|
+
// Lock out IP1
|
|
1218
|
+
for (let i = 0; i < 5; i++) {
|
|
1219
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key' }, '192.168.1.1');
|
|
1220
|
+
const mock = createMockRes();
|
|
1221
|
+
await middleware(req, mock.res, jest.fn());
|
|
1222
|
+
}
|
|
1223
|
+
// IP1 should be locked
|
|
1224
|
+
const req1 = createMockReq({ 'x-api-key': 'wrong-key' }, '192.168.1.1');
|
|
1225
|
+
const mock1 = createMockRes();
|
|
1226
|
+
await middleware(req1, mock1.res, jest.fn());
|
|
1227
|
+
expect(mock1.getStatus()).toBe(429);
|
|
1228
|
+
// IP2 should still work fine
|
|
1229
|
+
const req2 = createMockReq({ 'x-api-key': 'valid-key' }, '192.168.1.2');
|
|
1230
|
+
const { res: res2 } = createMockRes();
|
|
1231
|
+
const next2 = jest.fn();
|
|
1232
|
+
await middleware(req2, res2, next2);
|
|
1233
|
+
expect(next2).toHaveBeenCalled();
|
|
1234
|
+
});
|
|
1235
|
+
it('should use x-forwarded-for header for IP detection when trusted_proxies is set', async () => {
|
|
1236
|
+
const config = baseAuthConfig({ trusted_proxies: ['127.0.0.1'] });
|
|
1237
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1238
|
+
const forwardedIp = '203.0.113.50';
|
|
1239
|
+
// Generate failures for the forwarded IP
|
|
1240
|
+
for (let i = 0; i < 5; i++) {
|
|
1241
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key', 'x-forwarded-for': forwardedIp }, '127.0.0.1');
|
|
1242
|
+
const mock = createMockRes();
|
|
1243
|
+
await middleware(req, mock.res, jest.fn());
|
|
1244
|
+
}
|
|
1245
|
+
// The forwarded IP should be locked
|
|
1246
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key', 'x-forwarded-for': forwardedIp }, '127.0.0.1');
|
|
1247
|
+
const mock = createMockRes();
|
|
1248
|
+
await middleware(req, mock.res, jest.fn());
|
|
1249
|
+
expect(mock.getStatus()).toBe(429);
|
|
1250
|
+
});
|
|
1251
|
+
it('should ignore x-forwarded-for when trusted_proxies is not set', async () => {
|
|
1252
|
+
const config = baseAuthConfig();
|
|
1253
|
+
const middleware = (0, auth_1.createAuthMiddleware)(config);
|
|
1254
|
+
const forwardedIp = '203.0.113.50';
|
|
1255
|
+
// Generate failures via XFF - should use socket IP instead
|
|
1256
|
+
for (let i = 0; i < 5; i++) {
|
|
1257
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key', 'x-forwarded-for': forwardedIp }, '10.0.0.99');
|
|
1258
|
+
const mock = createMockRes();
|
|
1259
|
+
await middleware(req, mock.res, jest.fn());
|
|
1260
|
+
}
|
|
1261
|
+
// The forwarded IP should NOT be locked (XFF ignored)
|
|
1262
|
+
// Instead, the socket IP (10.0.0.99) should be locked
|
|
1263
|
+
const req = createMockReq({ 'x-api-key': 'wrong-key', 'x-forwarded-for': forwardedIp }, '10.0.0.99');
|
|
1264
|
+
const mock = createMockRes();
|
|
1265
|
+
await middleware(req, mock.res, jest.fn());
|
|
1266
|
+
expect(mock.getStatus()).toBe(429);
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
// ===========================================================================
|
|
1270
|
+
// Salted API key hashing (A6)
|
|
1271
|
+
// ===========================================================================
|
|
1272
|
+
describe('Salted API key hashing', () => {
|
|
1273
|
+
it('should generate a 32-char hex salt', () => {
|
|
1274
|
+
const salt = (0, auth_1.generateSalt)();
|
|
1275
|
+
expect(salt).toMatch(/^[0-9a-f]{32}$/);
|
|
1276
|
+
});
|
|
1277
|
+
it('should produce different hashes with different salts', () => {
|
|
1278
|
+
const key = 'test-api-key';
|
|
1279
|
+
const salt1 = (0, auth_1.generateSalt)();
|
|
1280
|
+
const salt2 = (0, auth_1.generateSalt)();
|
|
1281
|
+
const hash1 = (0, auth_1.hashKeyWithSalt)(key, salt1);
|
|
1282
|
+
const hash2 = (0, auth_1.hashKeyWithSalt)(key, salt2);
|
|
1283
|
+
expect(hash1).not.toBe(hash2);
|
|
1284
|
+
});
|
|
1285
|
+
it('should produce consistent hashes with the same salt', () => {
|
|
1286
|
+
const key = 'test-api-key';
|
|
1287
|
+
const salt = 'abcdef0123456789abcdef0123456789';
|
|
1288
|
+
const hash1 = (0, auth_1.hashKeyWithSalt)(key, salt);
|
|
1289
|
+
const hash2 = (0, auth_1.hashKeyWithSalt)(key, salt);
|
|
1290
|
+
expect(hash1).toBe(hash2);
|
|
1291
|
+
});
|
|
1292
|
+
it('should produce a 64-char hex string (SHA-256)', () => {
|
|
1293
|
+
const key = 'test-api-key';
|
|
1294
|
+
const salt = (0, auth_1.generateSalt)();
|
|
1295
|
+
const hash = (0, auth_1.hashKeyWithSalt)(key, salt);
|
|
1296
|
+
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
// ===========================================================================
|
|
1300
|
+
// RBAC default_role escalation (S9)
|
|
1301
|
+
// ===========================================================================
|
|
1302
|
+
describe('RBAC default_role escalation prevention', () => {
|
|
1303
|
+
it('should fall back to readonly when default_role is admin', () => {
|
|
1304
|
+
const rbacConfig = {
|
|
1305
|
+
...RBAC_CONFIG,
|
|
1306
|
+
default_role: 'admin',
|
|
1307
|
+
};
|
|
1308
|
+
const config = {
|
|
1309
|
+
enabled: true,
|
|
1310
|
+
api_keys: {},
|
|
1311
|
+
rbac: rbacConfig,
|
|
1312
|
+
};
|
|
1313
|
+
const middleware = (0, auth_1.createRBACMiddleware)(config, 'tool:execute');
|
|
1314
|
+
const req = createMockReq();
|
|
1315
|
+
req.auth = {
|
|
1316
|
+
workspace_id: 'ws_1',
|
|
1317
|
+
actor_id: 'user-no-roles',
|
|
1318
|
+
roles: [],
|
|
1319
|
+
permissions: [],
|
|
1320
|
+
auth_method: 'api_key',
|
|
1321
|
+
};
|
|
1322
|
+
const { res } = createMockRes();
|
|
1323
|
+
const next = jest.fn();
|
|
1324
|
+
middleware(req, res, next);
|
|
1325
|
+
// Should have fallen back to 'readonly', which does NOT have tool:execute
|
|
1326
|
+
const auth = req.auth;
|
|
1327
|
+
expect(auth.roles).toEqual(['readonly']);
|
|
1328
|
+
expect(auth.permissions).not.toContain('admin:full');
|
|
1329
|
+
});
|
|
1330
|
+
it('should fall back to readonly when default_role is operator', () => {
|
|
1331
|
+
const rbacConfig = {
|
|
1332
|
+
...RBAC_CONFIG,
|
|
1333
|
+
default_role: 'operator',
|
|
1334
|
+
};
|
|
1335
|
+
const config = {
|
|
1336
|
+
enabled: true,
|
|
1337
|
+
api_keys: {},
|
|
1338
|
+
rbac: rbacConfig,
|
|
1339
|
+
};
|
|
1340
|
+
const middleware = (0, auth_1.createRBACMiddleware)(config, 'approval:manage');
|
|
1341
|
+
const req = createMockReq();
|
|
1342
|
+
req.auth = {
|
|
1343
|
+
workspace_id: 'ws_1',
|
|
1344
|
+
actor_id: 'user-no-roles',
|
|
1345
|
+
roles: [],
|
|
1346
|
+
permissions: [],
|
|
1347
|
+
auth_method: 'api_key',
|
|
1348
|
+
};
|
|
1349
|
+
const mock = createMockRes();
|
|
1350
|
+
const next = jest.fn();
|
|
1351
|
+
middleware(req, mock.res, next);
|
|
1352
|
+
// Should fall back to 'readonly', which does not have 'approval:manage'
|
|
1353
|
+
const auth = req.auth;
|
|
1354
|
+
expect(auth.roles).toEqual(['readonly']);
|
|
1355
|
+
expect(auth.permissions).not.toContain('approval:manage');
|
|
1356
|
+
expect(next).not.toHaveBeenCalled();
|
|
1357
|
+
expect(mock.getStatus()).toBe(403);
|
|
1358
|
+
});
|
|
1359
|
+
it('should not modify default_role when it is a safe role', () => {
|
|
1360
|
+
const middleware = (0, auth_1.createRBACMiddleware)({
|
|
1361
|
+
enabled: true,
|
|
1362
|
+
api_keys: {},
|
|
1363
|
+
rbac: RBAC_CONFIG, // default_role: 'agent'
|
|
1364
|
+
}, 'tool:execute');
|
|
1365
|
+
const req = createMockReq();
|
|
1366
|
+
req.auth = {
|
|
1367
|
+
workspace_id: 'ws_1',
|
|
1368
|
+
actor_id: 'user-no-roles',
|
|
1369
|
+
roles: [],
|
|
1370
|
+
permissions: [],
|
|
1371
|
+
auth_method: 'api_key',
|
|
1372
|
+
};
|
|
1373
|
+
const { res } = createMockRes();
|
|
1374
|
+
const next = jest.fn();
|
|
1375
|
+
middleware(req, res, next);
|
|
1376
|
+
const auth = req.auth;
|
|
1377
|
+
expect(auth.roles).toEqual(['agent']);
|
|
1378
|
+
expect(auth.permissions).toContain('tool:execute');
|
|
1379
|
+
expect(next).toHaveBeenCalled();
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
//# sourceMappingURL=auth.test.js.map
|