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,1566 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createSaaSRouter = createSaaSRouter;
|
|
37
|
+
const express_1 = require("express");
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const crypto_1 = require("crypto");
|
|
40
|
+
const memory_1 = require("../storage/memory");
|
|
41
|
+
const calculator_1 = require("../trust/calculator");
|
|
42
|
+
const engine_1 = require("../replay/engine");
|
|
43
|
+
const plan_enforcer_1 = require("../billing/plan-enforcer");
|
|
44
|
+
/** Extract a route param as string (Express 5 returns string | string[]). */
|
|
45
|
+
function param(req, name) {
|
|
46
|
+
const val = req.params[name];
|
|
47
|
+
return Array.isArray(val) ? val[0] : val;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Require session authentication for all SaaS routes.
|
|
51
|
+
*/
|
|
52
|
+
function requireSession(req, res) {
|
|
53
|
+
if (!req.sessionUser) {
|
|
54
|
+
res.status(401).json({ error: 'Session authentication required' });
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function createSaaSRouter(deps) {
|
|
60
|
+
const router = (0, express_1.Router)();
|
|
61
|
+
const { config, userStore, workspaceStore, workspaceMemberStore, userApiKeyStore, sessionStore, gateway } = deps;
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// User Profile
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
router.get('/user/profile', (req, res) => {
|
|
66
|
+
if (!requireSession(req, res))
|
|
67
|
+
return;
|
|
68
|
+
const user = req.sessionUser;
|
|
69
|
+
res.json({
|
|
70
|
+
id: user.id,
|
|
71
|
+
email: user.email,
|
|
72
|
+
display_name: user.display_name,
|
|
73
|
+
avatar_url: user.avatar_url,
|
|
74
|
+
status: user.status,
|
|
75
|
+
onboarding_completed: user.onboarding_completed,
|
|
76
|
+
created_at: user.created_at,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
router.put('/user/profile', (req, res) => {
|
|
80
|
+
if (!requireSession(req, res))
|
|
81
|
+
return;
|
|
82
|
+
const user = req.sessionUser;
|
|
83
|
+
const { display_name } = req.body;
|
|
84
|
+
if (!display_name || typeof display_name !== 'string' || display_name.trim().length === 0) {
|
|
85
|
+
res.status(400).json({ error: 'display_name is required' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (display_name.length > 100) {
|
|
89
|
+
res.status(400).json({ error: 'display_name must be 100 characters or less' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const updated = userStore.update(user.id, {
|
|
93
|
+
display_name: display_name.trim(),
|
|
94
|
+
updated_at: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
res.json(updated);
|
|
97
|
+
});
|
|
98
|
+
router.put('/user/onboarding', (req, res) => {
|
|
99
|
+
if (!requireSession(req, res))
|
|
100
|
+
return;
|
|
101
|
+
const user = req.sessionUser;
|
|
102
|
+
const updated = userStore.update(user.id, {
|
|
103
|
+
onboarding_completed: true,
|
|
104
|
+
updated_at: new Date().toISOString(),
|
|
105
|
+
});
|
|
106
|
+
res.json(updated);
|
|
107
|
+
});
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Workspaces
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
router.get('/workspaces', (req, res) => {
|
|
112
|
+
if (!requireSession(req, res))
|
|
113
|
+
return;
|
|
114
|
+
const user = req.sessionUser;
|
|
115
|
+
const memberships = workspaceMemberStore.getByUser(user.id);
|
|
116
|
+
const workspaces = memberships.map(m => {
|
|
117
|
+
const ws = workspaceStore.getById(m.workspace_id);
|
|
118
|
+
return ws ? { ...ws, role: m.role } : null;
|
|
119
|
+
}).filter(Boolean);
|
|
120
|
+
res.json({ workspaces });
|
|
121
|
+
});
|
|
122
|
+
router.post('/workspaces', (req, res) => {
|
|
123
|
+
if (!requireSession(req, res))
|
|
124
|
+
return;
|
|
125
|
+
const user = req.sessionUser;
|
|
126
|
+
const { name, slug } = req.body;
|
|
127
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
128
|
+
res.status(400).json({ error: 'name is required' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Enforce workspace limit based on user's highest plan
|
|
132
|
+
const userWorkspaces = workspaceMemberStore.getByUser(user.id)
|
|
133
|
+
.filter(m => m.role === 'owner')
|
|
134
|
+
.map(m => workspaceStore.getById(m.workspace_id))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
// Determine the highest plan the user owns
|
|
137
|
+
const planPriority = { free: 0, pro: 1, business: 2, enterprise: 3 };
|
|
138
|
+
let highestPlan = 'free';
|
|
139
|
+
for (const ws of userWorkspaces) {
|
|
140
|
+
const p = (ws.plan || 'free');
|
|
141
|
+
if ((planPriority[p] || 0) > (planPriority[highestPlan] || 0)) {
|
|
142
|
+
highestPlan = p;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const wsLimitCheck = plan_enforcer_1.PlanEnforcer.checkWorkspaceLimit(highestPlan, userWorkspaces.length);
|
|
146
|
+
if (!wsLimitCheck.allowed) {
|
|
147
|
+
res.status(403).json({ error: wsLimitCheck.reason });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Generate slug from name if not provided
|
|
151
|
+
const wsSlug = (slug || name).toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').slice(0, 63);
|
|
152
|
+
if (workspaceStore.getBySlug(wsSlug)) {
|
|
153
|
+
res.status(409).json({ error: 'Workspace slug already taken' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
const workspaceId = (0, crypto_1.randomUUID)();
|
|
158
|
+
workspaceStore.create({
|
|
159
|
+
id: workspaceId,
|
|
160
|
+
name: name.trim(),
|
|
161
|
+
slug: wsSlug,
|
|
162
|
+
owner_user_id: user.id,
|
|
163
|
+
plan: 'free',
|
|
164
|
+
settings: {},
|
|
165
|
+
created_at: now,
|
|
166
|
+
updated_at: now,
|
|
167
|
+
});
|
|
168
|
+
workspaceMemberStore.create({
|
|
169
|
+
id: (0, crypto_1.randomUUID)(),
|
|
170
|
+
workspace_id: workspaceId,
|
|
171
|
+
user_id: user.id,
|
|
172
|
+
role: 'owner',
|
|
173
|
+
joined_at: now,
|
|
174
|
+
});
|
|
175
|
+
// Update session to point to this workspace
|
|
176
|
+
const sessionData = req.sessionData;
|
|
177
|
+
if (sessionData) {
|
|
178
|
+
sessionStore.update(sessionData.id, { workspace_id: workspaceId });
|
|
179
|
+
}
|
|
180
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
181
|
+
res.status(201).json(workspace);
|
|
182
|
+
});
|
|
183
|
+
router.get('/workspaces/:id', (req, res) => {
|
|
184
|
+
if (!requireSession(req, res))
|
|
185
|
+
return;
|
|
186
|
+
const user = req.sessionUser;
|
|
187
|
+
const workspaceId = param(req, 'id');
|
|
188
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
189
|
+
if (!membership) {
|
|
190
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
194
|
+
if (!workspace) {
|
|
195
|
+
res.status(404).json({ error: 'Workspace not found' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
res.json({ ...workspace, role: membership.role });
|
|
199
|
+
});
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Workspace Stats (Dashboard)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
router.get('/workspaces/:id/stats', (req, res) => {
|
|
204
|
+
if (!requireSession(req, res))
|
|
205
|
+
return;
|
|
206
|
+
const user = req.sessionUser;
|
|
207
|
+
const workspaceId = param(req, 'id');
|
|
208
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
209
|
+
if (!membership) {
|
|
210
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Gather stats from the gateway's audit store
|
|
214
|
+
// Match events by workspace UUID or slug (Android clients send slug as workspace_id)
|
|
215
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
216
|
+
const wsSlug = workspace?.slug;
|
|
217
|
+
const allEvents = gateway.getAuditLogger().getAllEvents();
|
|
218
|
+
const wsEvents = allEvents.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
|
|
219
|
+
const recentEvents = wsEvents.filter(e => {
|
|
220
|
+
const age = Date.now() - new Date(e.timestamp).getTime();
|
|
221
|
+
return age < 86400000; // Last 24 hours
|
|
222
|
+
});
|
|
223
|
+
const members = workspaceMemberStore.getByWorkspace(workspaceId);
|
|
224
|
+
const apiKeys = userApiKeyStore.getByWorkspace(workspaceId);
|
|
225
|
+
res.json({
|
|
226
|
+
total_requests: wsEvents.length,
|
|
227
|
+
requests_24h: recentEvents.length,
|
|
228
|
+
members: members.length,
|
|
229
|
+
api_keys: apiKeys.filter(k => !k.revoked).length,
|
|
230
|
+
recent_events: recentEvents.slice(-10).reverse(),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Events (filterable, sortable, paginated)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
router.get('/workspaces/:id/events', (req, res) => {
|
|
237
|
+
if (!requireSession(req, res))
|
|
238
|
+
return;
|
|
239
|
+
const user = req.sessionUser;
|
|
240
|
+
const workspaceId = param(req, 'id');
|
|
241
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
242
|
+
if (!membership) {
|
|
243
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
247
|
+
const wsSlug = workspace?.slug;
|
|
248
|
+
const allEvents = gateway.getAuditLogger().getAllEvents();
|
|
249
|
+
// Filter to this workspace (strict match — no ws_default fallback)
|
|
250
|
+
let events = allEvents.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
|
|
251
|
+
// Query params
|
|
252
|
+
const eventType = req.query.event_type;
|
|
253
|
+
const toolName = req.query.tool;
|
|
254
|
+
const actorId = req.query.actor;
|
|
255
|
+
const status = req.query.status;
|
|
256
|
+
const search = req.query.q;
|
|
257
|
+
const since = req.query.since;
|
|
258
|
+
const sort = req.query.sort || 'desc';
|
|
259
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
260
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
261
|
+
// Apply filters
|
|
262
|
+
if (eventType) {
|
|
263
|
+
const types = eventType.split(',');
|
|
264
|
+
events = events.filter(e => types.includes(e.event_type));
|
|
265
|
+
}
|
|
266
|
+
if (toolName) {
|
|
267
|
+
events = events.filter(e => e.tool_name?.includes(toolName));
|
|
268
|
+
}
|
|
269
|
+
if (actorId) {
|
|
270
|
+
events = events.filter(e => e.actor_id?.includes(actorId));
|
|
271
|
+
}
|
|
272
|
+
if (status) {
|
|
273
|
+
const statuses = status.split(',');
|
|
274
|
+
events = events.filter(e => {
|
|
275
|
+
const s = e.metadata?.status || e.metadata?.decision || '';
|
|
276
|
+
return statuses.some(st => s.includes(st));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (search) {
|
|
280
|
+
const q = search.toLowerCase();
|
|
281
|
+
events = events.filter(e => e.tool_name?.toLowerCase().includes(q) ||
|
|
282
|
+
e.actor_id?.toLowerCase().includes(q) ||
|
|
283
|
+
e.task_id?.toLowerCase().includes(q) ||
|
|
284
|
+
e.event_type?.toLowerCase().includes(q));
|
|
285
|
+
}
|
|
286
|
+
if (since) {
|
|
287
|
+
events = events.filter(e => e.timestamp >= since);
|
|
288
|
+
}
|
|
289
|
+
// Sort
|
|
290
|
+
events.sort((a, b) => {
|
|
291
|
+
const cmp = a.timestamp.localeCompare(b.timestamp);
|
|
292
|
+
return sort === 'asc' ? cmp : -cmp;
|
|
293
|
+
});
|
|
294
|
+
const total = events.length;
|
|
295
|
+
const paged = events.slice(offset, offset + limit);
|
|
296
|
+
res.json({ events: paged, total, offset, limit });
|
|
297
|
+
});
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// API Keys
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
router.get('/workspaces/:id/api-keys', (req, res) => {
|
|
302
|
+
if (!requireSession(req, res))
|
|
303
|
+
return;
|
|
304
|
+
const user = req.sessionUser;
|
|
305
|
+
const workspaceId = param(req, 'id');
|
|
306
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
307
|
+
if (!membership) {
|
|
308
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const keys = userApiKeyStore.getByWorkspace(workspaceId);
|
|
312
|
+
// Never return the hash, only metadata
|
|
313
|
+
res.json({
|
|
314
|
+
api_keys: keys.map(k => ({
|
|
315
|
+
id: k.id,
|
|
316
|
+
key_prefix: k.key_prefix,
|
|
317
|
+
name: k.name,
|
|
318
|
+
roles: k.roles,
|
|
319
|
+
tags: k.tags || [],
|
|
320
|
+
revoked: k.revoked,
|
|
321
|
+
last_used_at: k.last_used_at,
|
|
322
|
+
created_at: k.created_at,
|
|
323
|
+
})),
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
router.post('/workspaces/:id/api-keys', (req, res) => {
|
|
327
|
+
if (!requireSession(req, res))
|
|
328
|
+
return;
|
|
329
|
+
const user = req.sessionUser;
|
|
330
|
+
const workspaceId = param(req, 'id');
|
|
331
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
332
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
333
|
+
res.status(403).json({ error: 'Only workspace owners and admins can create API keys' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const { name, roles, tags } = req.body;
|
|
337
|
+
if (!name || typeof name !== 'string') {
|
|
338
|
+
res.status(400).json({ error: 'name is required' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Validate tags
|
|
342
|
+
const parsedTags = Array.isArray(tags)
|
|
343
|
+
? tags.filter((t) => typeof t === 'string' && t.length > 0).slice(0, 20)
|
|
344
|
+
: [];
|
|
345
|
+
// Enforce API key limit based on workspace plan
|
|
346
|
+
const wsForKeyLimit = workspaceStore.getById(workspaceId);
|
|
347
|
+
if (wsForKeyLimit) {
|
|
348
|
+
const plan = (wsForKeyLimit.plan || 'free');
|
|
349
|
+
const currentKeys = userApiKeyStore.getByWorkspace(workspaceId).filter(k => !k.revoked);
|
|
350
|
+
const keyLimitCheck = plan_enforcer_1.PlanEnforcer.checkApiKeyLimit(plan, currentKeys.length);
|
|
351
|
+
if (!keyLimitCheck.allowed) {
|
|
352
|
+
res.status(403).json({ error: keyLimitCheck.reason });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Generate a random API key (pn_<32 random hex chars>) with salted hash
|
|
357
|
+
const rawKey = `pn_${crypto.randomBytes(32).toString('hex')}`;
|
|
358
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
359
|
+
const hash = crypto.createHash('sha256').update(salt + rawKey).digest('hex');
|
|
360
|
+
const keyHash = `${salt}:${hash}`; // salted format: "salt:hash"
|
|
361
|
+
const keyPrefix = rawKey.slice(0, 11); // "pn_" + first 8 hex
|
|
362
|
+
const apiKey = {
|
|
363
|
+
id: (0, crypto_1.randomUUID)(),
|
|
364
|
+
key_hash: keyHash,
|
|
365
|
+
key_prefix: keyPrefix,
|
|
366
|
+
user_id: user.id,
|
|
367
|
+
workspace_id: workspaceId,
|
|
368
|
+
name: name.trim(),
|
|
369
|
+
roles: Array.isArray(roles) ? roles : ['agent'],
|
|
370
|
+
tags: parsedTags,
|
|
371
|
+
revoked: false,
|
|
372
|
+
created_at: new Date().toISOString(),
|
|
373
|
+
};
|
|
374
|
+
userApiKeyStore.create(apiKey);
|
|
375
|
+
// Return the plaintext key only once
|
|
376
|
+
res.status(201).json({
|
|
377
|
+
id: apiKey.id,
|
|
378
|
+
key: rawKey,
|
|
379
|
+
key_prefix: keyPrefix,
|
|
380
|
+
name: apiKey.name,
|
|
381
|
+
roles: apiKey.roles,
|
|
382
|
+
tags: apiKey.tags,
|
|
383
|
+
workspace_id: workspaceId,
|
|
384
|
+
created_at: apiKey.created_at,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
router.delete('/workspaces/:id/api-keys/:keyId', (req, res) => {
|
|
388
|
+
if (!requireSession(req, res))
|
|
389
|
+
return;
|
|
390
|
+
const user = req.sessionUser;
|
|
391
|
+
const workspaceId = param(req, 'id');
|
|
392
|
+
const keyId = param(req, 'keyId');
|
|
393
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
394
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
395
|
+
res.status(403).json({ error: 'Only workspace owners and admins can revoke API keys' });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const key = userApiKeyStore.getById(keyId);
|
|
399
|
+
if (!key || key.workspace_id !== workspaceId) {
|
|
400
|
+
res.status(404).json({ error: 'API key not found' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
userApiKeyStore.update(keyId, { revoked: true });
|
|
404
|
+
res.json({ status: 'ok', id: keyId });
|
|
405
|
+
});
|
|
406
|
+
router.patch('/workspaces/:id/api-keys/:keyId', (req, res) => {
|
|
407
|
+
if (!requireSession(req, res))
|
|
408
|
+
return;
|
|
409
|
+
const user = req.sessionUser;
|
|
410
|
+
const workspaceId = param(req, 'id');
|
|
411
|
+
const keyId = param(req, 'keyId');
|
|
412
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
413
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
414
|
+
res.status(403).json({ error: 'Only workspace owners and admins can update API keys' });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const key = userApiKeyStore.getById(keyId);
|
|
418
|
+
if (!key || key.workspace_id !== workspaceId) {
|
|
419
|
+
res.status(404).json({ error: 'API key not found' });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const updates = {};
|
|
423
|
+
if (req.body.name !== undefined && typeof req.body.name === 'string') {
|
|
424
|
+
updates.name = req.body.name.trim();
|
|
425
|
+
}
|
|
426
|
+
if (Array.isArray(req.body.tags)) {
|
|
427
|
+
updates.tags = req.body.tags.filter((t) => typeof t === 'string' && t.length > 0).slice(0, 20);
|
|
428
|
+
}
|
|
429
|
+
if (Object.keys(updates).length === 0) {
|
|
430
|
+
res.status(400).json({ error: 'No valid fields to update (supported: name, tags)' });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const updated = userApiKeyStore.update(keyId, updates);
|
|
434
|
+
if (!updated) {
|
|
435
|
+
res.status(500).json({ error: 'Failed to update API key' });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
res.json({
|
|
439
|
+
id: updated.id,
|
|
440
|
+
key_prefix: updated.key_prefix,
|
|
441
|
+
name: updated.name,
|
|
442
|
+
roles: updated.roles,
|
|
443
|
+
tags: updated.tags || [],
|
|
444
|
+
revoked: updated.revoked,
|
|
445
|
+
last_used_at: updated.last_used_at,
|
|
446
|
+
created_at: updated.created_at,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Traces
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
router.get('/workspaces/:id/traces', (req, res) => {
|
|
453
|
+
if (!requireSession(req, res))
|
|
454
|
+
return;
|
|
455
|
+
const user = req.sessionUser;
|
|
456
|
+
const workspaceId = param(req, 'id');
|
|
457
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
458
|
+
if (!membership) {
|
|
459
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
463
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
464
|
+
const statusFilter = req.query.status;
|
|
465
|
+
const toolFilter = req.query.tool;
|
|
466
|
+
const eventTypeFilter = req.query.event_type;
|
|
467
|
+
const allEvents = gateway.getAuditLogger().getAllEvents();
|
|
468
|
+
// Match events by workspace UUID or slug (Android clients send slug as workspace_id)
|
|
469
|
+
const wsForTraces = workspaceStore.getById(workspaceId);
|
|
470
|
+
const slugForTraces = wsForTraces?.slug;
|
|
471
|
+
let wsEvents = allEvents
|
|
472
|
+
.filter(e => e.workspace_id === workspaceId || (slugForTraces && e.workspace_id === slugForTraces));
|
|
473
|
+
// Apply server-side filters
|
|
474
|
+
if (statusFilter) {
|
|
475
|
+
const sf = statusFilter.toLowerCase();
|
|
476
|
+
wsEvents = wsEvents.filter(e => {
|
|
477
|
+
const decision = (e.metadata?.decision || '').toLowerCase();
|
|
478
|
+
const status = (e.metadata?.status || '').toLowerCase();
|
|
479
|
+
return decision.includes(sf) || status.includes(sf) || e.event_type.toLowerCase().includes(sf);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (toolFilter) {
|
|
483
|
+
const tf = toolFilter.toLowerCase();
|
|
484
|
+
wsEvents = wsEvents.filter(e => (e.tool_name || '').toLowerCase().includes(tf));
|
|
485
|
+
}
|
|
486
|
+
if (eventTypeFilter) {
|
|
487
|
+
const ef = eventTypeFilter.toLowerCase();
|
|
488
|
+
wsEvents = wsEvents.filter(e => e.event_type.toLowerCase().includes(ef));
|
|
489
|
+
}
|
|
490
|
+
wsEvents.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
491
|
+
res.json({
|
|
492
|
+
traces: wsEvents.slice(offset, offset + limit),
|
|
493
|
+
total: wsEvents.length,
|
|
494
|
+
limit,
|
|
495
|
+
offset,
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Budgets
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
router.get('/workspaces/:id/budgets', (req, res) => {
|
|
502
|
+
if (!requireSession(req, res))
|
|
503
|
+
return;
|
|
504
|
+
const user = req.sessionUser;
|
|
505
|
+
const workspaceId = param(req, 'id');
|
|
506
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
507
|
+
if (!membership) {
|
|
508
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const spending = gateway.getBudgetManager().getSpendingSummary();
|
|
512
|
+
// Use workspace-specific budget config if available, otherwise global
|
|
513
|
+
const budgetConfigStore = gateway.getBudgetConfigStore();
|
|
514
|
+
const wsConfig = budgetConfigStore?.getByWorkspaceId(workspaceId);
|
|
515
|
+
const effectiveConfig = wsConfig ? { ...config.budget, ...wsConfig } : config.budget;
|
|
516
|
+
const is_custom = !!wsConfig;
|
|
517
|
+
res.json({
|
|
518
|
+
workspace_id: workspaceId,
|
|
519
|
+
config: effectiveConfig,
|
|
520
|
+
spending,
|
|
521
|
+
is_custom,
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Approvals (proxy to gateway's ApprovalManager)
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
router.get('/workspaces/:id/approvals', (req, res) => {
|
|
528
|
+
if (!requireSession(req, res))
|
|
529
|
+
return;
|
|
530
|
+
const user = req.sessionUser;
|
|
531
|
+
const workspaceId = param(req, 'id');
|
|
532
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
533
|
+
if (!membership) {
|
|
534
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const pending = gateway.getPendingApprovals(workspaceId);
|
|
538
|
+
// Strip the JWT token — frontend only sees approval metadata
|
|
539
|
+
const approvals = pending.map(a => ({
|
|
540
|
+
approval_id: a.approval_id,
|
|
541
|
+
tool_call_id: a.tool_call_id,
|
|
542
|
+
task_id: a.task_id,
|
|
543
|
+
workspace_id: a.workspace_id,
|
|
544
|
+
actor_id: a.actor_id,
|
|
545
|
+
tool_name: a.tool_name,
|
|
546
|
+
tool_capability: a.tool_capability,
|
|
547
|
+
args_summary: a.args_summary,
|
|
548
|
+
scope: a.scope,
|
|
549
|
+
reason: a.reason,
|
|
550
|
+
status: a.status,
|
|
551
|
+
created_at: a.created_at,
|
|
552
|
+
expires_at: a.expires_at,
|
|
553
|
+
}));
|
|
554
|
+
res.json({ approvals });
|
|
555
|
+
});
|
|
556
|
+
router.post('/workspaces/:id/approvals/:approvalId/approve', async (req, res) => {
|
|
557
|
+
if (!requireSession(req, res))
|
|
558
|
+
return;
|
|
559
|
+
const user = req.sessionUser;
|
|
560
|
+
const workspaceId = param(req, 'id');
|
|
561
|
+
const approvalId = param(req, 'approvalId');
|
|
562
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
563
|
+
if (!membership) {
|
|
564
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Verify approval exists and belongs to this workspace
|
|
568
|
+
const approval = gateway.getApprovalManager().getApproval(approvalId);
|
|
569
|
+
if (!approval) {
|
|
570
|
+
res.status(404).json({ error: 'Approval not found' });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (approval.workspace_id !== workspaceId) {
|
|
574
|
+
res.status(404).json({ error: 'Approval not found' });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
await gateway.getApprovalManager().resolveById(approvalId, user.id, true);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
const message = err instanceof Error ? err.message : 'Approval failed';
|
|
582
|
+
res.status(400).json({ error: message });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
res.json({ status: 'approved', approval_id: approvalId });
|
|
586
|
+
});
|
|
587
|
+
router.post('/workspaces/:id/approvals/:approvalId/deny', async (req, res) => {
|
|
588
|
+
if (!requireSession(req, res))
|
|
589
|
+
return;
|
|
590
|
+
const user = req.sessionUser;
|
|
591
|
+
const workspaceId = param(req, 'id');
|
|
592
|
+
const approvalId = param(req, 'approvalId');
|
|
593
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
594
|
+
if (!membership) {
|
|
595
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const approvalRecord = gateway.getApprovalManager().getApproval(approvalId);
|
|
599
|
+
if (!approvalRecord) {
|
|
600
|
+
res.status(404).json({ error: 'Approval not found' });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (approvalRecord.workspace_id !== workspaceId) {
|
|
604
|
+
res.status(404).json({ error: 'Approval not found' });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const reason = req.body?.reason || 'Denied via SaaS dashboard';
|
|
608
|
+
try {
|
|
609
|
+
await gateway.getApprovalManager().resolveById(approvalId, user.id, false, reason);
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
const message = err instanceof Error ? err.message : 'Denial failed';
|
|
613
|
+
res.status(400).json({ error: message });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
res.json({ status: 'denied', approval_id: approvalId });
|
|
617
|
+
});
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Policies (workspace-aware CRUD)
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
// Ensure a PolicyStore is available on the gateway
|
|
622
|
+
if (!gateway.getPolicyStore()) {
|
|
623
|
+
gateway.setStores({ policyStore: deps.policyStore || new memory_1.InMemoryPolicyStore() });
|
|
624
|
+
}
|
|
625
|
+
const policyStore = gateway.getPolicyStore();
|
|
626
|
+
router.get('/workspaces/:id/policies', (req, res) => {
|
|
627
|
+
if (!requireSession(req, res))
|
|
628
|
+
return;
|
|
629
|
+
const user = req.sessionUser;
|
|
630
|
+
const workspaceId = param(req, 'id');
|
|
631
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
632
|
+
if (!membership) {
|
|
633
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const { policy, is_custom } = gateway.getWorkspacePolicy(workspaceId);
|
|
637
|
+
res.json({ policy, is_custom });
|
|
638
|
+
});
|
|
639
|
+
router.put('/workspaces/:id/policies', (req, res) => {
|
|
640
|
+
if (!requireSession(req, res))
|
|
641
|
+
return;
|
|
642
|
+
const user = req.sessionUser;
|
|
643
|
+
const workspaceId = param(req, 'id');
|
|
644
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
645
|
+
if (!membership) {
|
|
646
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
650
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify policies' });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const body = req.body;
|
|
654
|
+
const validation = gateway.validatePolicy(body);
|
|
655
|
+
if (!validation.valid) {
|
|
656
|
+
res.status(400).json({ valid: false, errors: validation.errors });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
policyStore.set(workspaceId, body);
|
|
660
|
+
gateway.getAuditLogger().log({
|
|
661
|
+
event_type: 'POLICY_UPDATED',
|
|
662
|
+
tool_call_id: '',
|
|
663
|
+
task_id: '',
|
|
664
|
+
workspace_id: workspaceId,
|
|
665
|
+
actor_id: user.id,
|
|
666
|
+
tool_name: '',
|
|
667
|
+
metadata: { policy_name: body.name, policy_version: body.version, updated_by: user.id },
|
|
668
|
+
});
|
|
669
|
+
res.json({ policy: body, is_custom: true });
|
|
670
|
+
});
|
|
671
|
+
router.delete('/workspaces/:id/policies', (req, res) => {
|
|
672
|
+
if (!requireSession(req, res))
|
|
673
|
+
return;
|
|
674
|
+
const user = req.sessionUser;
|
|
675
|
+
const workspaceId = param(req, 'id');
|
|
676
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
677
|
+
if (!membership) {
|
|
678
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
682
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify policies' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
policyStore.delete(workspaceId);
|
|
686
|
+
gateway.getAuditLogger().log({
|
|
687
|
+
event_type: 'POLICY_RESET',
|
|
688
|
+
tool_call_id: '',
|
|
689
|
+
task_id: '',
|
|
690
|
+
workspace_id: workspaceId,
|
|
691
|
+
actor_id: user.id,
|
|
692
|
+
tool_name: '',
|
|
693
|
+
metadata: { reset_by: user.id },
|
|
694
|
+
});
|
|
695
|
+
const policy = gateway.getCurrentPolicy();
|
|
696
|
+
res.json({ status: 'reset', policy, is_custom: false });
|
|
697
|
+
});
|
|
698
|
+
router.post('/workspaces/:id/policies/validate', (req, res) => {
|
|
699
|
+
if (!requireSession(req, res))
|
|
700
|
+
return;
|
|
701
|
+
const user = req.sessionUser;
|
|
702
|
+
const workspaceId = param(req, 'id');
|
|
703
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
704
|
+
if (!membership) {
|
|
705
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const body = req.body;
|
|
709
|
+
const validation = gateway.validatePolicy(body);
|
|
710
|
+
res.json(validation);
|
|
711
|
+
});
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
// Policy Rule Generation (LLM-powered)
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
router.post('/workspaces/:id/policies/generate-rule', async (req, res) => {
|
|
716
|
+
if (!requireSession(req, res))
|
|
717
|
+
return;
|
|
718
|
+
const user = req.sessionUser;
|
|
719
|
+
const workspaceId = param(req, 'id');
|
|
720
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
721
|
+
if (!membership) {
|
|
722
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const apiKey = process.env.PALARYN_LLM_API_KEY;
|
|
726
|
+
if (!apiKey) {
|
|
727
|
+
res.status(503).json({ error: 'AI rule generation is not configured. Set PALARYN_LLM_API_KEY environment variable.' });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const { description } = req.body;
|
|
731
|
+
if (!description || typeof description !== 'string' || description.trim().length === 0) {
|
|
732
|
+
res.status(400).json({ error: 'description is required' });
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const trimmed = description.trim().slice(0, 500);
|
|
736
|
+
const systemPrompt = `You are a policy rule generator for Palaryn, an AI agent gateway. Given a natural language description, generate a single JSON PolicyRule object.
|
|
737
|
+
|
|
738
|
+
The PolicyRule schema:
|
|
739
|
+
{
|
|
740
|
+
"name": string, // Short kebab-case name for the rule
|
|
741
|
+
"description": string, // Human-readable description
|
|
742
|
+
"effect": "ALLOW" | "DENY" | "REQUIRE_APPROVAL" | "TRANSFORM",
|
|
743
|
+
"priority": number, // Lower = higher precedence (1-100)
|
|
744
|
+
"conditions": {
|
|
745
|
+
"tools": string[], // Tool names: "http.request", "slack.post_message", etc.
|
|
746
|
+
"tool_match": string, // Regex pattern for tool name matching
|
|
747
|
+
"capabilities": string[], // "read", "write", "delete", "admin"
|
|
748
|
+
"actors": string[], // Actor/agent IDs
|
|
749
|
+
"actor_types": string[], // "agent", "user", "system"
|
|
750
|
+
"domains": string[], // URL domains: "api.github.com", "*.googleapis.com"
|
|
751
|
+
"domain_blocklist": string[],// Blocked domains
|
|
752
|
+
"methods": string[], // HTTP methods: "GET", "POST", "PUT", "DELETE", "PATCH"
|
|
753
|
+
"labels": string[], // Context labels
|
|
754
|
+
"platforms": string[], // Source platforms: "langgraph", "claude_code", "n8n", "custom"
|
|
755
|
+
"workspace_ids": string[] // Specific workspace IDs
|
|
756
|
+
},
|
|
757
|
+
"approval": { // Only when effect is REQUIRE_APPROVAL
|
|
758
|
+
"scope": string,
|
|
759
|
+
"ttl_seconds": number,
|
|
760
|
+
"reason": string
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
Rules:
|
|
765
|
+
- Only include condition fields that are relevant to the description. Omit empty arrays.
|
|
766
|
+
- Use "DENY" for blocking, "ALLOW" for permitting, "REQUIRE_APPROVAL" when human approval is needed.
|
|
767
|
+
- For domain wildcards use "*.example.com" format.
|
|
768
|
+
- IMPORTANT: Always pair capabilities with matching methods. read → ["GET","HEAD","OPTIONS"], write → ["POST","PUT","PATCH"], delete → ["DELETE"], admin → all methods.
|
|
769
|
+
- When the description says "read-only", set capabilities to ["read"] AND methods to ["GET","HEAD","OPTIONS"].
|
|
770
|
+
- Return ONLY the JSON object, no markdown fences, no explanation.
|
|
771
|
+
|
|
772
|
+
Examples:
|
|
773
|
+
Input: "Allow GET requests to GitHub and Google APIs"
|
|
774
|
+
Output: {"name":"allow-get-github-google","description":"Allow GET requests to GitHub and Google APIs","effect":"ALLOW","priority":10,"conditions":{"tools":["http.request"],"capabilities":["read"],"methods":["GET"],"domains":["api.github.com","*.googleapis.com"]}}
|
|
775
|
+
|
|
776
|
+
Input: "Block all delete operations"
|
|
777
|
+
Output: {"name":"block-delete-ops","description":"Block all delete operations","effect":"DENY","priority":5,"conditions":{"capabilities":["delete"],"methods":["DELETE"]}}
|
|
778
|
+
|
|
779
|
+
Input: "Allow read-only access"
|
|
780
|
+
Output: {"name":"allow-read-only","description":"Allow read-only access","effect":"ALLOW","priority":10,"conditions":{"capabilities":["read"],"methods":["GET","HEAD","OPTIONS"]}}
|
|
781
|
+
|
|
782
|
+
Input: "Require approval for write operations to Slack"
|
|
783
|
+
Output: {"name":"approve-slack-writes","description":"Require approval for write operations to Slack","effect":"REQUIRE_APPROVAL","priority":15,"conditions":{"capabilities":["write"],"methods":["POST","PUT","PATCH"],"domains":["api.slack.com"]},"approval":{"scope":"team_lead","ttl_seconds":3600,"reason":"Write operations to Slack require approval"}}`;
|
|
784
|
+
try {
|
|
785
|
+
const controller = new AbortController();
|
|
786
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
787
|
+
const llmRes = await fetch('https://api.anthropic.com/v1/messages', {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: {
|
|
790
|
+
'Content-Type': 'application/json',
|
|
791
|
+
'x-api-key': apiKey,
|
|
792
|
+
'anthropic-version': '2023-06-01',
|
|
793
|
+
},
|
|
794
|
+
body: JSON.stringify({
|
|
795
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
796
|
+
max_tokens: 1024,
|
|
797
|
+
system: systemPrompt,
|
|
798
|
+
messages: [{ role: 'user', content: trimmed }],
|
|
799
|
+
}),
|
|
800
|
+
signal: controller.signal,
|
|
801
|
+
});
|
|
802
|
+
clearTimeout(timeout);
|
|
803
|
+
if (!llmRes.ok) {
|
|
804
|
+
const errBody = await llmRes.text();
|
|
805
|
+
console.error('[generate-rule] LLM API error:', llmRes.status, errBody);
|
|
806
|
+
res.status(502).json({ error: 'Failed to generate rule. LLM API returned an error.' });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const llmData = await llmRes.json();
|
|
810
|
+
const text = llmData.content?.[0]?.text || '';
|
|
811
|
+
// Strip markdown code fences if present
|
|
812
|
+
const cleaned = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
|
|
813
|
+
let rule;
|
|
814
|
+
try {
|
|
815
|
+
rule = JSON.parse(cleaned);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
console.error('[generate-rule] Failed to parse LLM output:', cleaned);
|
|
819
|
+
res.status(500).json({ error: 'Failed to parse generated rule. Please try rephrasing your description.' });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
// Normalize effect to uppercase
|
|
823
|
+
if (rule.effect && typeof rule.effect === 'string') {
|
|
824
|
+
rule.effect = rule.effect.toUpperCase();
|
|
825
|
+
}
|
|
826
|
+
// Ensure required fields exist
|
|
827
|
+
if (!rule.name)
|
|
828
|
+
rule.name = 'generated-rule';
|
|
829
|
+
if (!rule.conditions)
|
|
830
|
+
rule.conditions = {};
|
|
831
|
+
if (!rule.effect)
|
|
832
|
+
rule.effect = 'DENY';
|
|
833
|
+
// Strip unknown top-level fields
|
|
834
|
+
const validKeys = ['name', 'description', 'effect', 'priority', 'conditions', 'transformations', 'approval'];
|
|
835
|
+
for (const key of Object.keys(rule)) {
|
|
836
|
+
if (!validKeys.includes(key))
|
|
837
|
+
delete rule[key];
|
|
838
|
+
}
|
|
839
|
+
res.json({ rule });
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
if (err.name === 'AbortError') {
|
|
843
|
+
res.status(504).json({ error: 'Rule generation timed out. Please try again.' });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
console.error('[generate-rule] Unexpected error:', err);
|
|
847
|
+
res.status(500).json({ error: 'Failed to generate rule. Please try again.' });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
// Security (DLP detections dashboard)
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
router.get('/workspaces/:id/security', (req, res) => {
|
|
854
|
+
if (!requireSession(req, res))
|
|
855
|
+
return;
|
|
856
|
+
const user = req.sessionUser;
|
|
857
|
+
const workspaceId = param(req, 'id');
|
|
858
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
859
|
+
if (!membership) {
|
|
860
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
864
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
865
|
+
const allDlpEvents = gateway.getAuditLogger().getEventsByType('DLP_SCANNED');
|
|
866
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
867
|
+
const wsSlug = workspace?.slug;
|
|
868
|
+
const wsEvents = allDlpEvents
|
|
869
|
+
.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug))
|
|
870
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
871
|
+
// Compute severity stats
|
|
872
|
+
let high = 0, medium = 0, low = 0;
|
|
873
|
+
const patternSet = new Set();
|
|
874
|
+
for (const e of wsEvents) {
|
|
875
|
+
const severity = e.metadata?.severity;
|
|
876
|
+
if (severity === 'high')
|
|
877
|
+
high++;
|
|
878
|
+
else if (severity === 'medium')
|
|
879
|
+
medium++;
|
|
880
|
+
else
|
|
881
|
+
low++;
|
|
882
|
+
const detected = e.metadata?.detected;
|
|
883
|
+
if (Array.isArray(detected)) {
|
|
884
|
+
for (const p of detected)
|
|
885
|
+
patternSet.add(p);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
res.json({
|
|
889
|
+
events: wsEvents.slice(offset, offset + limit),
|
|
890
|
+
total: wsEvents.length,
|
|
891
|
+
limit,
|
|
892
|
+
offset,
|
|
893
|
+
stats: {
|
|
894
|
+
total: wsEvents.length,
|
|
895
|
+
high,
|
|
896
|
+
medium,
|
|
897
|
+
low,
|
|
898
|
+
unique_patterns: patternSet.size,
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
// Workspace Switch
|
|
904
|
+
// ---------------------------------------------------------------------------
|
|
905
|
+
router.post('/workspaces/:id/switch', (req, res) => {
|
|
906
|
+
if (!requireSession(req, res))
|
|
907
|
+
return;
|
|
908
|
+
const user = req.sessionUser;
|
|
909
|
+
const workspaceId = param(req, 'id');
|
|
910
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
911
|
+
if (!membership) {
|
|
912
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
916
|
+
if (!workspace) {
|
|
917
|
+
res.status(404).json({ error: 'Workspace not found' });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// Update session to point to this workspace
|
|
921
|
+
const sessionData = req.sessionData;
|
|
922
|
+
if (sessionData) {
|
|
923
|
+
sessionStore.update(sessionData.id, { workspace_id: workspaceId });
|
|
924
|
+
}
|
|
925
|
+
res.json({ ...workspace, role: membership.role });
|
|
926
|
+
});
|
|
927
|
+
// ---------------------------------------------------------------------------
|
|
928
|
+
// Members
|
|
929
|
+
// ---------------------------------------------------------------------------
|
|
930
|
+
router.get('/workspaces/:id/members', (req, res) => {
|
|
931
|
+
if (!requireSession(req, res))
|
|
932
|
+
return;
|
|
933
|
+
const user = req.sessionUser;
|
|
934
|
+
const workspaceId = param(req, 'id');
|
|
935
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
936
|
+
if (!membership) {
|
|
937
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const members = workspaceMemberStore.getByWorkspace(workspaceId);
|
|
941
|
+
const enriched = members.map(m => {
|
|
942
|
+
const u = userStore.getById(m.user_id);
|
|
943
|
+
return {
|
|
944
|
+
id: m.id,
|
|
945
|
+
user_id: m.user_id,
|
|
946
|
+
role: m.role,
|
|
947
|
+
joined_at: m.joined_at,
|
|
948
|
+
email: u?.email || '',
|
|
949
|
+
display_name: u?.display_name || '',
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
res.json({ members: enriched, viewer_role: membership.role });
|
|
953
|
+
});
|
|
954
|
+
router.put('/workspaces/:id/members/:memberId', (req, res) => {
|
|
955
|
+
if (!requireSession(req, res))
|
|
956
|
+
return;
|
|
957
|
+
const user = req.sessionUser;
|
|
958
|
+
const workspaceId = param(req, 'id');
|
|
959
|
+
const memberId = param(req, 'memberId');
|
|
960
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
961
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
962
|
+
res.status(403).json({ error: 'Only workspace owners and admins can change roles' });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const target = workspaceMemberStore.getById(memberId);
|
|
966
|
+
if (!target || target.workspace_id !== workspaceId) {
|
|
967
|
+
res.status(404).json({ error: 'Member not found' });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
// Cannot change own role
|
|
971
|
+
if (target.user_id === user.id) {
|
|
972
|
+
res.status(400).json({ error: 'Cannot change your own role' });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const { role } = req.body;
|
|
976
|
+
const validRoles = ['owner', 'admin', 'member', 'viewer'];
|
|
977
|
+
if (!role || !validRoles.includes(role)) {
|
|
978
|
+
res.status(400).json({ error: `role must be one of: ${validRoles.join(', ')}` });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const updated = workspaceMemberStore.update(memberId, { role });
|
|
982
|
+
res.json(updated);
|
|
983
|
+
});
|
|
984
|
+
router.delete('/workspaces/:id/members/:memberId', (req, res) => {
|
|
985
|
+
if (!requireSession(req, res))
|
|
986
|
+
return;
|
|
987
|
+
const user = req.sessionUser;
|
|
988
|
+
const workspaceId = param(req, 'id');
|
|
989
|
+
const memberId = param(req, 'memberId');
|
|
990
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
991
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
992
|
+
res.status(403).json({ error: 'Only workspace owners and admins can remove members' });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const target = workspaceMemberStore.getById(memberId);
|
|
996
|
+
if (!target || target.workspace_id !== workspaceId) {
|
|
997
|
+
res.status(404).json({ error: 'Member not found' });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
// Cannot remove yourself
|
|
1001
|
+
if (target.user_id === user.id) {
|
|
1002
|
+
res.status(400).json({ error: 'Cannot remove yourself' });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
// Prevent removing the last owner
|
|
1006
|
+
if (target.role === 'owner') {
|
|
1007
|
+
const owners = workspaceMemberStore.getByWorkspace(workspaceId).filter(m => m.role === 'owner');
|
|
1008
|
+
if (owners.length <= 1) {
|
|
1009
|
+
res.status(400).json({ error: 'Cannot remove the last owner' });
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
workspaceMemberStore.delete(memberId);
|
|
1014
|
+
res.json({ status: 'ok', id: memberId });
|
|
1015
|
+
});
|
|
1016
|
+
router.post('/workspaces/:id/members', (req, res) => {
|
|
1017
|
+
if (!requireSession(req, res))
|
|
1018
|
+
return;
|
|
1019
|
+
const user = req.sessionUser;
|
|
1020
|
+
const workspaceId = param(req, 'id');
|
|
1021
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1022
|
+
if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) {
|
|
1023
|
+
res.status(403).json({ error: 'Only workspace owners and admins can add members' });
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const { email, role } = req.body;
|
|
1027
|
+
if (!email || typeof email !== 'string') {
|
|
1028
|
+
res.status(400).json({ error: 'email is required' });
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// Enforce member limit based on workspace plan
|
|
1032
|
+
const wsForLimit = workspaceStore.getById(workspaceId);
|
|
1033
|
+
if (wsForLimit) {
|
|
1034
|
+
const plan = (wsForLimit.plan || 'free');
|
|
1035
|
+
const currentMembers = workspaceMemberStore.getByWorkspace(workspaceId);
|
|
1036
|
+
const memberLimitCheck = plan_enforcer_1.PlanEnforcer.checkMemberLimit(plan, currentMembers.length);
|
|
1037
|
+
if (!memberLimitCheck.allowed) {
|
|
1038
|
+
res.status(403).json({ error: memberLimitCheck.reason });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const validRoles = ['admin', 'member', 'viewer'];
|
|
1043
|
+
const memberRole = validRoles.includes(role) ? role : 'member';
|
|
1044
|
+
const targetUser = userStore.getByEmail(email.trim().toLowerCase());
|
|
1045
|
+
if (!targetUser) {
|
|
1046
|
+
res.status(404).json({ error: 'User not found' });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
// Check if already a member
|
|
1050
|
+
const existing = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, targetUser.id);
|
|
1051
|
+
if (existing) {
|
|
1052
|
+
res.status(409).json({ error: 'User is already a member of this workspace' });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const newMember = {
|
|
1056
|
+
id: (0, crypto_1.randomUUID)(),
|
|
1057
|
+
workspace_id: workspaceId,
|
|
1058
|
+
user_id: targetUser.id,
|
|
1059
|
+
role: memberRole,
|
|
1060
|
+
joined_at: new Date().toISOString(),
|
|
1061
|
+
};
|
|
1062
|
+
workspaceMemberStore.create(newMember);
|
|
1063
|
+
const enriched = {
|
|
1064
|
+
id: newMember.id,
|
|
1065
|
+
user_id: targetUser.id,
|
|
1066
|
+
role: newMember.role,
|
|
1067
|
+
joined_at: newMember.joined_at,
|
|
1068
|
+
email: targetUser.email,
|
|
1069
|
+
display_name: targetUser.display_name,
|
|
1070
|
+
};
|
|
1071
|
+
res.status(201).json(enriched);
|
|
1072
|
+
});
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
// Trace Detail (single task)
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
router.get('/workspaces/:id/traces/:taskId', (req, res) => {
|
|
1077
|
+
if (!requireSession(req, res))
|
|
1078
|
+
return;
|
|
1079
|
+
const user = req.sessionUser;
|
|
1080
|
+
const workspaceId = param(req, 'id');
|
|
1081
|
+
const taskId = param(req, 'taskId');
|
|
1082
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1083
|
+
if (!membership) {
|
|
1084
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const events = gateway.getTaskTrace(taskId);
|
|
1088
|
+
// Filter to events belonging to this workspace
|
|
1089
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
1090
|
+
const wsSlug = workspace?.slug;
|
|
1091
|
+
const filteredEvents = events.filter(e => e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug));
|
|
1092
|
+
// Cap at 200 events to prevent UI overload
|
|
1093
|
+
res.json({
|
|
1094
|
+
task_id: taskId,
|
|
1095
|
+
events: filteredEvents.slice(0, 200),
|
|
1096
|
+
total: filteredEvents.length,
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
// Dashboard Stats (aggregated metrics for dashboard widgets)
|
|
1101
|
+
// ---------------------------------------------------------------------------
|
|
1102
|
+
router.get('/workspaces/:id/dashboard/stats', (req, res) => {
|
|
1103
|
+
if (!requireSession(req, res))
|
|
1104
|
+
return;
|
|
1105
|
+
const user = req.sessionUser;
|
|
1106
|
+
const workspaceId = param(req, 'id');
|
|
1107
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1108
|
+
if (!membership) {
|
|
1109
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
1113
|
+
const wsSlug = workspace?.slug;
|
|
1114
|
+
// Use the audit logger's getEventStats for aggregated metrics
|
|
1115
|
+
// Pass both workspace UUID and slug to match events (no ws_default — strict isolation)
|
|
1116
|
+
const stats = gateway.getAuditLogger().getEventStats(workspaceId, 24);
|
|
1117
|
+
const slugStats = wsSlug ? gateway.getAuditLogger().getEventStats(wsSlug, 24) : null;
|
|
1118
|
+
// Merge stats from UUID and slug matches only
|
|
1119
|
+
const allStats = [stats, slugStats].filter((s) => s !== null);
|
|
1120
|
+
const mergeSum = (fn) => allStats.reduce((acc, s) => acc + fn(s), 0);
|
|
1121
|
+
const metrics = {
|
|
1122
|
+
requests_per_minute: Math.round(mergeSum(s => s.requests_per_minute) * 10) / 10,
|
|
1123
|
+
blocked_24h: mergeSum(s => s.blocked_count),
|
|
1124
|
+
avg_latency_ms: Math.round(stats.avg_duration_ms || slugStats?.avg_duration_ms || 0),
|
|
1125
|
+
active_agents: mergeSum(s => s.active_agents),
|
|
1126
|
+
pending_approvals: gateway.getPendingApprovals(workspaceId).length,
|
|
1127
|
+
budget_burn_percent: 0,
|
|
1128
|
+
};
|
|
1129
|
+
// Compute budget burn
|
|
1130
|
+
const spending = gateway.getBudgetManager().getSpendingSummary();
|
|
1131
|
+
const budgetConfig = config.budget;
|
|
1132
|
+
if (budgetConfig.workspace_monthly_budget_usd && budgetConfig.workspace_monthly_budget_usd > 0) {
|
|
1133
|
+
metrics.budget_burn_percent = Math.round((spending.workspace_monthly_total / budgetConfig.workspace_monthly_budget_usd) * 100);
|
|
1134
|
+
}
|
|
1135
|
+
// Shield score: compute from policy breakdown (across all matching workspace IDs)
|
|
1136
|
+
const totalDecisions = allStats.reduce((acc, s) => acc + Object.values(s.policy_breakdown).reduce((sum, n) => sum + n, 0), 0);
|
|
1137
|
+
const allowCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['allow'] || 0), 0);
|
|
1138
|
+
const transformCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['transform'] || 0), 0);
|
|
1139
|
+
const approvalCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['require_approval'] || 0), 0);
|
|
1140
|
+
const denyCount = allStats.reduce((acc, s) => acc + (s.policy_breakdown['deny'] || 0), 0);
|
|
1141
|
+
const shield_score = {
|
|
1142
|
+
score: totalDecisions > 0 ? Math.round(((allowCount + transformCount) / totalDecisions) * 100) : 100,
|
|
1143
|
+
breakdown: {
|
|
1144
|
+
allowed_percent: totalDecisions > 0 ? Math.round((allowCount / totalDecisions) * 100) : 100,
|
|
1145
|
+
transformed_percent: totalDecisions > 0 ? Math.round((transformCount / totalDecisions) * 100) : 0,
|
|
1146
|
+
approval_percent: totalDecisions > 0 ? Math.round((approvalCount / totalDecisions) * 100) : 0,
|
|
1147
|
+
blocked_percent: totalDecisions > 0 ? Math.round((denyCount / totalDecisions) * 100) : 0,
|
|
1148
|
+
},
|
|
1149
|
+
};
|
|
1150
|
+
// Pipeline throughput from stats (merged across all matching workspace IDs)
|
|
1151
|
+
const pipeline_throughput = stats.pipeline_throughput.map(stage => ({
|
|
1152
|
+
...stage,
|
|
1153
|
+
passed: allStats.reduce((acc, s) => acc + (s.pipeline_throughput.find(t => t.stage === stage.stage)?.passed || 0), 0),
|
|
1154
|
+
failed: allStats.reduce((acc, s) => acc + (s.pipeline_throughput.find(t => t.stage === stage.stage)?.failed || 0), 0),
|
|
1155
|
+
}));
|
|
1156
|
+
res.json({
|
|
1157
|
+
metrics,
|
|
1158
|
+
shield_score,
|
|
1159
|
+
pipeline_throughput,
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
// ---------------------------------------------------------------------------
|
|
1163
|
+
// Anomaly Baseline (for anomaly radar widget)
|
|
1164
|
+
// ---------------------------------------------------------------------------
|
|
1165
|
+
router.get('/workspaces/:id/anomalies/baseline', (req, res) => {
|
|
1166
|
+
if (!requireSession(req, res))
|
|
1167
|
+
return;
|
|
1168
|
+
const user = req.sessionUser;
|
|
1169
|
+
const workspaceId = param(req, 'id');
|
|
1170
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1171
|
+
if (!membership) {
|
|
1172
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const detector = gateway.getAnomalyDetector();
|
|
1176
|
+
if (!detector) {
|
|
1177
|
+
res.json({ current: {}, baseline: {}, alerts: [] });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const report = detector.getBaselineReport();
|
|
1181
|
+
res.json(report);
|
|
1182
|
+
});
|
|
1183
|
+
// ---------------------------------------------------------------------------
|
|
1184
|
+
// Agent Trust Score
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
router.get('/workspaces/:id/agents/:actorId/trust', (req, res) => {
|
|
1187
|
+
if (!requireSession(req, res))
|
|
1188
|
+
return;
|
|
1189
|
+
const user = req.sessionUser;
|
|
1190
|
+
const workspaceId = param(req, 'id');
|
|
1191
|
+
const actorId = param(req, 'actorId');
|
|
1192
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1193
|
+
if (!membership) {
|
|
1194
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const detector = gateway.getAnomalyDetector();
|
|
1198
|
+
if (!detector) {
|
|
1199
|
+
res.json({ actor_id: actorId, score: 100, risk_level: 'low', breakdown: {}, calculated_at: new Date().toISOString() });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const calculator = new calculator_1.TrustScoreCalculator(detector, gateway.getAuditLogger(), gateway.getBudgetManager());
|
|
1203
|
+
res.json(calculator.calculate(actorId));
|
|
1204
|
+
});
|
|
1205
|
+
router.get('/workspaces/:id/agents/trust-leaderboard', (req, res) => {
|
|
1206
|
+
if (!requireSession(req, res))
|
|
1207
|
+
return;
|
|
1208
|
+
const user = req.sessionUser;
|
|
1209
|
+
const workspaceId = param(req, 'id');
|
|
1210
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1211
|
+
if (!membership) {
|
|
1212
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
// Get distinct actor IDs from workspace events
|
|
1216
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
1217
|
+
const wsSlug = workspace?.slug;
|
|
1218
|
+
const allEvents = gateway.getAuditLogger().getAllEvents();
|
|
1219
|
+
const actorIds = new Set();
|
|
1220
|
+
for (const e of allEvents) {
|
|
1221
|
+
if (e.workspace_id === workspaceId || (wsSlug && e.workspace_id === wsSlug)) {
|
|
1222
|
+
if (e.actor_id)
|
|
1223
|
+
actorIds.add(e.actor_id);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const detector = gateway.getAnomalyDetector();
|
|
1227
|
+
if (!detector || actorIds.size === 0) {
|
|
1228
|
+
res.json({ agents: [] });
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const calculator = new calculator_1.TrustScoreCalculator(detector, gateway.getAuditLogger(), gateway.getBudgetManager());
|
|
1232
|
+
const leaderboard = calculator.getLeaderboard([...actorIds]);
|
|
1233
|
+
res.json({ agents: leaderboard });
|
|
1234
|
+
});
|
|
1235
|
+
// ---------------------------------------------------------------------------
|
|
1236
|
+
// Session Replay
|
|
1237
|
+
// ---------------------------------------------------------------------------
|
|
1238
|
+
router.post('/workspaces/:id/replay', (req, res) => {
|
|
1239
|
+
if (!requireSession(req, res))
|
|
1240
|
+
return;
|
|
1241
|
+
const user = req.sessionUser;
|
|
1242
|
+
const workspaceId = param(req, 'id');
|
|
1243
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1244
|
+
if (!membership) {
|
|
1245
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const { task_id, policy_pack_path } = req.body;
|
|
1249
|
+
if (!task_id || typeof task_id !== 'string') {
|
|
1250
|
+
res.status(400).json({ error: 'task_id is required' });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (!policy_pack_path || typeof policy_pack_path !== 'string') {
|
|
1254
|
+
res.status(400).json({ error: 'policy_pack_path is required' });
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const engine = new engine_1.SessionReplayEngine(gateway.getAuditLogger());
|
|
1258
|
+
const result = engine.replay(task_id, policy_pack_path);
|
|
1259
|
+
res.json(result);
|
|
1260
|
+
});
|
|
1261
|
+
// ---------------------------------------------------------------------------
|
|
1262
|
+
// LLM Usage (model/provider breakdown)
|
|
1263
|
+
// ---------------------------------------------------------------------------
|
|
1264
|
+
router.get('/workspaces/:id/llm-usage', (req, res) => {
|
|
1265
|
+
if (!requireSession(req, res))
|
|
1266
|
+
return;
|
|
1267
|
+
const user = req.sessionUser;
|
|
1268
|
+
const workspaceId = param(req, 'id');
|
|
1269
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1270
|
+
if (!membership) {
|
|
1271
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const range = req.query.range || '24h';
|
|
1275
|
+
const rangeMs = {
|
|
1276
|
+
'1h': 3600000,
|
|
1277
|
+
'6h': 21600000,
|
|
1278
|
+
'24h': 86400000,
|
|
1279
|
+
'7d': 604800000,
|
|
1280
|
+
'30d': 2592000000,
|
|
1281
|
+
};
|
|
1282
|
+
const windowMs = rangeMs[range] || rangeMs['24h'];
|
|
1283
|
+
const cutoff = Date.now() - windowMs;
|
|
1284
|
+
const workspace = workspaceStore.getById(workspaceId);
|
|
1285
|
+
const wsSlug = workspace?.slug;
|
|
1286
|
+
const allEvents = gateway.getAuditLogger().getAllEvents();
|
|
1287
|
+
// Filter events that have model metadata and belong to this workspace
|
|
1288
|
+
const wsEvents = allEvents.filter(e => {
|
|
1289
|
+
if (e.workspace_id !== workspaceId && (!wsSlug || e.workspace_id !== wsSlug))
|
|
1290
|
+
return false;
|
|
1291
|
+
if (new Date(e.timestamp).getTime() < cutoff)
|
|
1292
|
+
return false;
|
|
1293
|
+
return e.metadata?.model;
|
|
1294
|
+
});
|
|
1295
|
+
// Aggregate by model
|
|
1296
|
+
const modelMap = new Map();
|
|
1297
|
+
let totalRequests = 0;
|
|
1298
|
+
let totalCostUsd = 0;
|
|
1299
|
+
let totalTokens = 0;
|
|
1300
|
+
let totalLatencyMs = 0;
|
|
1301
|
+
for (const e of wsEvents) {
|
|
1302
|
+
const model = e.metadata.model;
|
|
1303
|
+
const provider = e.metadata.provider || 'unknown';
|
|
1304
|
+
const inputTokens = e.metadata.input_tokens || 0;
|
|
1305
|
+
const outputTokens = e.metadata.output_tokens || 0;
|
|
1306
|
+
const costUsd = e.metadata.cost_usd || 0;
|
|
1307
|
+
const durationMs = e.metadata.duration_ms || 0;
|
|
1308
|
+
let entry = modelMap.get(model);
|
|
1309
|
+
if (!entry) {
|
|
1310
|
+
entry = { model, provider, requests: 0, input_tokens: 0, output_tokens: 0, cost_usd: 0, total_latency_ms: 0 };
|
|
1311
|
+
modelMap.set(model, entry);
|
|
1312
|
+
}
|
|
1313
|
+
entry.requests++;
|
|
1314
|
+
entry.input_tokens += inputTokens;
|
|
1315
|
+
entry.output_tokens += outputTokens;
|
|
1316
|
+
entry.cost_usd += costUsd;
|
|
1317
|
+
entry.total_latency_ms += durationMs;
|
|
1318
|
+
totalRequests++;
|
|
1319
|
+
totalCostUsd += costUsd;
|
|
1320
|
+
totalTokens += inputTokens + outputTokens;
|
|
1321
|
+
totalLatencyMs += durationMs;
|
|
1322
|
+
}
|
|
1323
|
+
const byModel = Array.from(modelMap.values()).map(m => ({
|
|
1324
|
+
model: m.model,
|
|
1325
|
+
provider: m.provider,
|
|
1326
|
+
requests: m.requests,
|
|
1327
|
+
input_tokens: m.input_tokens,
|
|
1328
|
+
output_tokens: m.output_tokens,
|
|
1329
|
+
cost_usd: m.cost_usd,
|
|
1330
|
+
avg_latency_ms: m.requests > 0 ? Math.round(m.total_latency_ms / m.requests) : 0,
|
|
1331
|
+
})).sort((a, b) => b.requests - a.requests);
|
|
1332
|
+
// Build time series (bucket events into intervals)
|
|
1333
|
+
const bucketCount = Math.min(24, Math.max(6, Math.floor(windowMs / 3600000)));
|
|
1334
|
+
const bucketMs = windowMs / bucketCount;
|
|
1335
|
+
const timeSeries = [];
|
|
1336
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
1337
|
+
const bucketStart = cutoff + i * bucketMs;
|
|
1338
|
+
const bucketEnd = bucketStart + bucketMs;
|
|
1339
|
+
let requests = 0;
|
|
1340
|
+
let cost = 0;
|
|
1341
|
+
let tokens = 0;
|
|
1342
|
+
for (const e of wsEvents) {
|
|
1343
|
+
const t = new Date(e.timestamp).getTime();
|
|
1344
|
+
if (t >= bucketStart && t < bucketEnd) {
|
|
1345
|
+
requests++;
|
|
1346
|
+
cost += e.metadata.cost_usd || 0;
|
|
1347
|
+
tokens += (e.metadata.input_tokens || 0) + (e.metadata.output_tokens || 0);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
timeSeries.push({
|
|
1351
|
+
timestamp: new Date(bucketStart).toISOString(),
|
|
1352
|
+
requests,
|
|
1353
|
+
cost_usd: cost,
|
|
1354
|
+
tokens,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
res.json({
|
|
1358
|
+
summary: {
|
|
1359
|
+
total_requests: totalRequests,
|
|
1360
|
+
total_cost_usd: totalCostUsd,
|
|
1361
|
+
total_tokens: totalTokens,
|
|
1362
|
+
avg_latency_ms: totalRequests > 0 ? Math.round(totalLatencyMs / totalRequests) : 0,
|
|
1363
|
+
},
|
|
1364
|
+
by_model: byModel,
|
|
1365
|
+
time_series: timeSeries,
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
// ---------------------------------------------------------------------------
|
|
1369
|
+
// Per-Workspace Rate Limit Configuration
|
|
1370
|
+
// ---------------------------------------------------------------------------
|
|
1371
|
+
// Ensure stores are available on the gateway
|
|
1372
|
+
if (!gateway.getRateLimitConfigStore()) {
|
|
1373
|
+
gateway.setStores({ rateLimitConfigStore: deps.rateLimitConfigStore || new memory_1.InMemoryRateLimitConfigStore() });
|
|
1374
|
+
}
|
|
1375
|
+
const rateLimitConfigStore = gateway.getRateLimitConfigStore();
|
|
1376
|
+
if (!gateway.getBudgetConfigStore()) {
|
|
1377
|
+
gateway.setStores({ budgetConfigStore: deps.budgetConfigStore || new memory_1.InMemoryBudgetConfigStore() });
|
|
1378
|
+
}
|
|
1379
|
+
const budgetConfigStore = gateway.getBudgetConfigStore();
|
|
1380
|
+
router.get('/workspaces/:id/rate-limits', (req, res) => {
|
|
1381
|
+
if (!requireSession(req, res))
|
|
1382
|
+
return;
|
|
1383
|
+
const user = req.sessionUser;
|
|
1384
|
+
const workspaceId = param(req, 'id');
|
|
1385
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1386
|
+
if (!membership) {
|
|
1387
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const wsConfig = rateLimitConfigStore.getByWorkspaceId(workspaceId);
|
|
1391
|
+
const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
|
|
1392
|
+
const effectiveConfig = wsConfig ? { ...globalConfig, ...wsConfig } : globalConfig;
|
|
1393
|
+
res.json({ config: effectiveConfig, is_custom: !!wsConfig });
|
|
1394
|
+
});
|
|
1395
|
+
router.put('/workspaces/:id/rate-limits', (req, res) => {
|
|
1396
|
+
if (!requireSession(req, res))
|
|
1397
|
+
return;
|
|
1398
|
+
const user = req.sessionUser;
|
|
1399
|
+
const workspaceId = param(req, 'id');
|
|
1400
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1401
|
+
if (!membership) {
|
|
1402
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
1406
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify rate limits' });
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const body = req.body;
|
|
1410
|
+
// Validate: all values must be positive numbers if present
|
|
1411
|
+
const errors = [];
|
|
1412
|
+
if (body.actor_max_per_window !== undefined && (typeof body.actor_max_per_window !== 'number' || body.actor_max_per_window <= 0)) {
|
|
1413
|
+
errors.push('actor_max_per_window must be a positive number');
|
|
1414
|
+
}
|
|
1415
|
+
if (body.workspace_max_per_window !== undefined && (typeof body.workspace_max_per_window !== 'number' || body.workspace_max_per_window <= 0)) {
|
|
1416
|
+
errors.push('workspace_max_per_window must be a positive number');
|
|
1417
|
+
}
|
|
1418
|
+
if (body.window_ms !== undefined && (typeof body.window_ms !== 'number' || body.window_ms <= 0)) {
|
|
1419
|
+
errors.push('window_ms must be a positive number');
|
|
1420
|
+
}
|
|
1421
|
+
if (errors.length > 0) {
|
|
1422
|
+
res.status(400).json({ error: 'Validation failed', errors });
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const clean = {};
|
|
1426
|
+
if (body.actor_max_per_window !== undefined)
|
|
1427
|
+
clean.actor_max_per_window = body.actor_max_per_window;
|
|
1428
|
+
if (body.workspace_max_per_window !== undefined)
|
|
1429
|
+
clean.workspace_max_per_window = body.workspace_max_per_window;
|
|
1430
|
+
if (body.window_ms !== undefined)
|
|
1431
|
+
clean.window_ms = body.window_ms;
|
|
1432
|
+
rateLimitConfigStore.set(workspaceId, clean);
|
|
1433
|
+
gateway.getAuditLogger().log({
|
|
1434
|
+
event_type: 'RATE_LIMIT_CONFIG_UPDATED',
|
|
1435
|
+
tool_call_id: '',
|
|
1436
|
+
task_id: '',
|
|
1437
|
+
workspace_id: workspaceId,
|
|
1438
|
+
actor_id: user.id,
|
|
1439
|
+
tool_name: '',
|
|
1440
|
+
metadata: { config: clean, updated_by: user.id },
|
|
1441
|
+
});
|
|
1442
|
+
const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
|
|
1443
|
+
res.json({ config: { ...globalConfig, ...clean }, is_custom: true });
|
|
1444
|
+
});
|
|
1445
|
+
router.delete('/workspaces/:id/rate-limits', (req, res) => {
|
|
1446
|
+
if (!requireSession(req, res))
|
|
1447
|
+
return;
|
|
1448
|
+
const user = req.sessionUser;
|
|
1449
|
+
const workspaceId = param(req, 'id');
|
|
1450
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1451
|
+
if (!membership) {
|
|
1452
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
1456
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify rate limits' });
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
rateLimitConfigStore.delete(workspaceId);
|
|
1460
|
+
gateway.getAuditLogger().log({
|
|
1461
|
+
event_type: 'RATE_LIMIT_CONFIG_RESET',
|
|
1462
|
+
tool_call_id: '',
|
|
1463
|
+
task_id: '',
|
|
1464
|
+
workspace_id: workspaceId,
|
|
1465
|
+
actor_id: user.id,
|
|
1466
|
+
tool_name: '',
|
|
1467
|
+
metadata: { reset_by: user.id },
|
|
1468
|
+
});
|
|
1469
|
+
const globalConfig = config.rate_limit || { enabled: false, actor_max_per_window: 100, workspace_max_per_window: 500, window_ms: 60000 };
|
|
1470
|
+
res.json({ status: 'reset', config: globalConfig, is_custom: false });
|
|
1471
|
+
});
|
|
1472
|
+
// ---------------------------------------------------------------------------
|
|
1473
|
+
// Per-Workspace Budget Configuration
|
|
1474
|
+
// ---------------------------------------------------------------------------
|
|
1475
|
+
router.get('/workspaces/:id/budget-config', (req, res) => {
|
|
1476
|
+
if (!requireSession(req, res))
|
|
1477
|
+
return;
|
|
1478
|
+
const user = req.sessionUser;
|
|
1479
|
+
const workspaceId = param(req, 'id');
|
|
1480
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1481
|
+
if (!membership) {
|
|
1482
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const wsConfig = budgetConfigStore.getByWorkspaceId(workspaceId);
|
|
1486
|
+
const effectiveConfig = wsConfig ? { ...config.budget, ...wsConfig } : config.budget;
|
|
1487
|
+
res.json({ config: effectiveConfig, is_custom: !!wsConfig });
|
|
1488
|
+
});
|
|
1489
|
+
router.put('/workspaces/:id/budget-config', (req, res) => {
|
|
1490
|
+
if (!requireSession(req, res))
|
|
1491
|
+
return;
|
|
1492
|
+
const user = req.sessionUser;
|
|
1493
|
+
const workspaceId = param(req, 'id');
|
|
1494
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1495
|
+
if (!membership) {
|
|
1496
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
1500
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify budget config' });
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const body = req.body;
|
|
1504
|
+
// Validate: all values must be positive numbers if present
|
|
1505
|
+
const errors = [];
|
|
1506
|
+
const numFields = [
|
|
1507
|
+
'task_budget_usd', 'user_daily_budget_usd', 'user_monthly_budget_usd',
|
|
1508
|
+
'workspace_daily_budget_usd', 'workspace_monthly_budget_usd',
|
|
1509
|
+
'max_steps_per_task', 'max_retries_per_call', 'max_wall_clock_ms',
|
|
1510
|
+
];
|
|
1511
|
+
for (const field of numFields) {
|
|
1512
|
+
const val = body[field];
|
|
1513
|
+
if (val !== undefined && (typeof val !== 'number' || val <= 0)) {
|
|
1514
|
+
errors.push(`${field} must be a positive number`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (errors.length > 0) {
|
|
1518
|
+
res.status(400).json({ error: 'Validation failed', errors });
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const clean = {};
|
|
1522
|
+
for (const field of numFields) {
|
|
1523
|
+
if (body[field] !== undefined)
|
|
1524
|
+
clean[field] = body[field];
|
|
1525
|
+
}
|
|
1526
|
+
budgetConfigStore.set(workspaceId, clean);
|
|
1527
|
+
gateway.getAuditLogger().log({
|
|
1528
|
+
event_type: 'BUDGET_CONFIG_UPDATED',
|
|
1529
|
+
tool_call_id: '',
|
|
1530
|
+
task_id: '',
|
|
1531
|
+
workspace_id: workspaceId,
|
|
1532
|
+
actor_id: user.id,
|
|
1533
|
+
tool_name: '',
|
|
1534
|
+
metadata: { config: clean, updated_by: user.id },
|
|
1535
|
+
});
|
|
1536
|
+
res.json({ config: { ...config.budget, ...clean }, is_custom: true });
|
|
1537
|
+
});
|
|
1538
|
+
router.delete('/workspaces/:id/budget-config', (req, res) => {
|
|
1539
|
+
if (!requireSession(req, res))
|
|
1540
|
+
return;
|
|
1541
|
+
const user = req.sessionUser;
|
|
1542
|
+
const workspaceId = param(req, 'id');
|
|
1543
|
+
const membership = workspaceMemberStore.getByWorkspaceAndUser(workspaceId, user.id);
|
|
1544
|
+
if (!membership) {
|
|
1545
|
+
res.status(403).json({ error: 'Not a member of this workspace' });
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (!['owner', 'admin'].includes(membership.role)) {
|
|
1549
|
+
res.status(403).json({ error: 'Only workspace owners and admins can modify budget config' });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
budgetConfigStore.delete(workspaceId);
|
|
1553
|
+
gateway.getAuditLogger().log({
|
|
1554
|
+
event_type: 'BUDGET_CONFIG_RESET',
|
|
1555
|
+
tool_call_id: '',
|
|
1556
|
+
task_id: '',
|
|
1557
|
+
workspace_id: workspaceId,
|
|
1558
|
+
actor_id: user.id,
|
|
1559
|
+
tool_name: '',
|
|
1560
|
+
metadata: { reset_by: user.id },
|
|
1561
|
+
});
|
|
1562
|
+
res.json({ status: 'reset', config: config.budget, is_custom: false });
|
|
1563
|
+
});
|
|
1564
|
+
return router;
|
|
1565
|
+
}
|
|
1566
|
+
//# sourceMappingURL=routes.js.map
|