@xopcai/xopc 0.0.27 → 0.0.29
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/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/adapters/onboard-cli.d.ts +7 -0
- package/dist/extensions/weixin/src/adapters/onboard-cli.js +61 -0
- package/dist/extensions/weixin/src/adapters/onboard-cli.js.map +1 -0
- package/dist/extensions/weixin/src/cli/qr-login.d.ts +5 -0
- package/dist/extensions/weixin/src/cli/qr-login.js +1 -1
- package/dist/extensions/weixin/src/cli/qr-login.js.map +1 -1
- package/dist/extensions/weixin/src/index.js +1 -1
- package/dist/extensions/weixin/src/plugin.d.ts +1 -0
- package/dist/extensions/weixin/src/plugin.js +2 -0
- package/dist/extensions/weixin/src/plugin.js.map +1 -1
- package/dist/gateway/static/root/assets/agents-CkgFSiCY.js +216 -0
- package/dist/gateway/static/root/assets/agents-CkgFSiCY.js.map +1 -0
- package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js → apps-page-Bmq19MS-.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-CBBh_Ww8.js.map → apps-page-Bmq19MS-.js.map} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js +9 -0
- package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js.map +1 -0
- package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js +2 -0
- package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js.map +1 -0
- package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js → cron-utils-N1PqD2DB.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-utils-08gdQfl9.js.map → cron-utils-N1PqD2DB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-C1MrygQH.js → dist--p2HQ2QF.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-C1MrygQH.js.map → dist--p2HQ2QF.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js → extension-debug-page-DwHCB_6T.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-DN3HKUGS.js.map → extension-debug-page-DwHCB_6T.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js → extension-page-BsYwQIex.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-CoFDHZtZ.js.map → extension-page-BsYwQIex.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js → extension-settings-page-nsisEgjB.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-BcPCu_Go.js.map → extension-settings-page-nsisEgjB.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-CR8zUHGR.js +4734 -0
- package/dist/gateway/static/root/assets/{index-PfkB8N37.js.map → index-CR8zUHGR.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-Dnfha4O2.css +1 -0
- package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js +2 -0
- package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js.map +1 -0
- package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js +2 -0
- package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js.map +1 -0
- package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js +2 -0
- package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js.map +1 -0
- package/dist/gateway/static/root/assets/skills-page-Clg8deH0.js +3 -0
- package/dist/gateway/static/root/assets/{skills-page-BmBDCEbY.js.map → skills-page-Clg8deH0.js.map} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/agent/lifecycle/hook-handler.d.ts +2 -0
- package/dist/src/agent/lifecycle/hook-handler.js +24 -0
- package/dist/src/agent/lifecycle/hook-handler.js.map +1 -1
- package/dist/src/agent/messaging/command-handler.js +10 -2
- package/dist/src/agent/messaging/command-handler.js.map +1 -1
- package/dist/src/agent/service/process-direct-streaming.js +77 -20
- package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
- package/dist/src/agent/service.d.ts +15 -0
- package/dist/src/agent/service.js +21 -1
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/channels/weixin/index.js +1 -1
- package/dist/src/cli/agent-chat-log-level-preset.d.ts +8 -0
- package/dist/src/cli/agent-chat-log-level-preset.js +25 -0
- package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -0
- package/dist/src/cli/commands/agent/interactive.js +4 -2
- package/dist/src/cli/commands/agent/interactive.js.map +1 -1
- package/dist/src/cli/commands/agent/stream-renderer.d.ts +14 -0
- package/dist/src/cli/commands/agent/stream-renderer.js +99 -0
- package/dist/src/cli/commands/agent/stream-renderer.js.map +1 -0
- package/dist/src/cli/commands/agent.js +2 -2
- package/dist/src/cli/commands/agent.js.map +1 -1
- package/dist/src/cli/commands/onboard.js +77 -93
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/tui.d.ts +1 -0
- package/dist/src/cli/commands/tui.js +40 -0
- package/dist/src/cli/commands/tui.js.map +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +7 -3
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/config/schema.d.ts +6 -0
- package/dist/src/config/schema.js +11 -3
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/extensions/hooks.js +5 -1
- package/dist/src/extensions/hooks.js.map +1 -1
- package/dist/src/extensions/loader.d.ts +1 -0
- package/dist/src/extensions/loader.js +3 -1
- package/dist/src/extensions/loader.js.map +1 -1
- package/dist/src/extensions/sdk/index.d.ts +1 -1
- package/dist/src/extensions/sdk/index.js.map +1 -1
- package/dist/src/extensions/types/core.d.ts +8 -0
- package/dist/src/extensions/types/hooks.d.ts +16 -1
- package/dist/src/extensions/types/hooks.js +1 -0
- package/dist/src/extensions/types/hooks.js.map +1 -1
- package/dist/src/gateway/agents-admin.d.ts +19 -1
- package/dist/src/gateway/agents-admin.js +164 -3
- package/dist/src/gateway/agents-admin.js.map +1 -1
- package/dist/src/gateway/auth.d.ts +17 -3
- package/dist/src/gateway/auth.js +35 -16
- package/dist/src/gateway/auth.js.map +1 -1
- package/dist/src/gateway/hono/app.js +31 -1
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +1 -1
- package/dist/src/gateway/hono/middleware/auth.js +4 -3
- package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
- package/dist/src/gateway/hono/middleware/scopes.d.ts +15 -0
- package/dist/src/gateway/hono/middleware/scopes.js +41 -0
- package/dist/src/gateway/hono/middleware/scopes.js.map +1 -0
- package/dist/src/gateway/hono/routes/agents.js +59 -5
- package/dist/src/gateway/hono/routes/agents.js.map +1 -1
- package/dist/src/gateway/hono/routes/config.js +2 -2
- package/dist/src/gateway/hono/routes/config.js.map +1 -1
- package/dist/src/gateway/hono/routes/public-gateway.js +1 -0
- package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
- package/dist/src/gateway/hono/routes/sessions.js +17 -0
- package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
- package/dist/src/gateway/security/audit.d.ts +18 -0
- package/dist/src/gateway/security/audit.js +68 -0
- package/dist/src/gateway/security/audit.js.map +1 -0
- package/dist/src/gateway/security/csp.d.ts +19 -0
- package/dist/src/gateway/security/csp.js +52 -0
- package/dist/src/gateway/security/csp.js.map +1 -0
- package/dist/src/gateway/security/dangerous-tools.d.ts +20 -0
- package/dist/src/gateway/security/dangerous-tools.js +46 -0
- package/dist/src/gateway/security/dangerous-tools.js.map +1 -0
- package/dist/src/gateway/security/flood-guard.d.ts +28 -0
- package/dist/src/gateway/security/flood-guard.js +42 -0
- package/dist/src/gateway/security/flood-guard.js.map +1 -0
- package/dist/src/gateway/security/index.d.ts +9 -0
- package/dist/src/gateway/security/index.js +10 -0
- package/dist/src/gateway/security/known-weak-secrets.d.ts +10 -0
- package/dist/src/gateway/security/known-weak-secrets.js +36 -0
- package/dist/src/gateway/security/known-weak-secrets.js.map +1 -0
- package/dist/src/gateway/security/operator-scopes.d.ts +37 -0
- package/dist/src/gateway/security/operator-scopes.js +137 -0
- package/dist/src/gateway/security/operator-scopes.js.map +1 -0
- package/dist/src/gateway/security/origin-check.d.ts +21 -0
- package/dist/src/gateway/security/origin-check.js +56 -0
- package/dist/src/gateway/security/origin-check.js.map +1 -0
- package/dist/src/gateway/security/preauth-connection-budget.d.ts +17 -0
- package/dist/src/gateway/security/preauth-connection-budget.js +49 -0
- package/dist/src/gateway/security/preauth-connection-budget.js.map +1 -0
- package/dist/src/gateway/security/secret-equal.d.ts +8 -0
- package/dist/src/gateway/security/secret-equal.js +30 -0
- package/dist/src/gateway/security/secret-equal.js.map +1 -0
- package/dist/src/gateway/service.d.ts +3 -1
- package/dist/src/gateway/service.js +40 -4
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/session/client-history.d.ts +21 -0
- package/dist/src/session/client-history.js +89 -0
- package/dist/src/session/client-history.js.map +1 -0
- package/dist/src/session/index.d.ts +1 -0
- package/dist/src/session/index.js +2 -1
- package/dist/src/session/manager.d.ts +2 -0
- package/dist/src/session/manager.js +5 -0
- package/dist/src/session/manager.js.map +1 -1
- package/dist/src/session/thinking-resolve.js +1 -1
- package/dist/src/session/thinking-resolve.js.map +1 -1
- package/dist/src/tui/backends/embedded-backend.d.ts +42 -0
- package/dist/src/tui/backends/embedded-backend.js +173 -0
- package/dist/src/tui/backends/embedded-backend.js.map +1 -0
- package/dist/src/tui/backends/gateway-sse-backend.d.ts +53 -0
- package/dist/src/tui/backends/gateway-sse-backend.js +256 -0
- package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -0
- package/dist/src/tui/chat-history.d.ts +4 -0
- package/dist/src/tui/chat-history.js +29 -0
- package/dist/src/tui/chat-history.js.map +1 -0
- package/dist/src/tui/components/assistant-message.d.ts +6 -0
- package/dist/src/tui/components/assistant-message.js +19 -0
- package/dist/src/tui/components/assistant-message.js.map +1 -0
- package/dist/src/tui/components/chat-log.d.ts +21 -0
- package/dist/src/tui/components/chat-log.js +113 -0
- package/dist/src/tui/components/chat-log.js.map +1 -0
- package/dist/src/tui/components/custom-editor.d.ts +14 -0
- package/dist/src/tui/components/custom-editor.js +50 -0
- package/dist/src/tui/components/custom-editor.js.map +1 -0
- package/dist/src/tui/components/fuzzy-filter.d.ts +17 -0
- package/dist/src/tui/components/fuzzy-filter.js +85 -0
- package/dist/src/tui/components/fuzzy-filter.js.map +1 -0
- package/dist/src/tui/components/searchable-select-list.d.ts +39 -0
- package/dist/src/tui/components/searchable-select-list.js +257 -0
- package/dist/src/tui/components/searchable-select-list.js.map +1 -0
- package/dist/src/tui/components/tool-execution.d.ts +16 -0
- package/dist/src/tui/components/tool-execution.js +76 -0
- package/dist/src/tui/components/tool-execution.js.map +1 -0
- package/dist/src/tui/components/user-message.d.ts +6 -0
- package/dist/src/tui/components/user-message.js +22 -0
- package/dist/src/tui/components/user-message.js.map +1 -0
- package/dist/src/tui/sse-consumer.d.ts +15 -0
- package/dist/src/tui/sse-consumer.js +75 -0
- package/dist/src/tui/sse-consumer.js.map +1 -0
- package/dist/src/tui/stream-assembler.d.ts +22 -0
- package/dist/src/tui/stream-assembler.js +63 -0
- package/dist/src/tui/stream-assembler.js.map +1 -0
- package/dist/src/tui/theme.d.ts +73 -0
- package/dist/src/tui/theme.js +157 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/tui-agent-events.d.ts +7 -0
- package/dist/src/tui/tui-agent-events.js +103 -0
- package/dist/src/tui/tui-agent-events.js.map +1 -0
- package/dist/src/tui/tui-backend.d.ts +80 -0
- package/dist/src/tui/tui-backend.js +1 -0
- package/dist/src/tui/tui-commands.d.ts +23 -0
- package/dist/src/tui/tui-commands.js +165 -0
- package/dist/src/tui/tui-commands.js.map +1 -0
- package/dist/src/tui/tui-lifecycle.d.ts +26 -0
- package/dist/src/tui/tui-lifecycle.js +57 -0
- package/dist/src/tui/tui-lifecycle.js.map +1 -0
- package/dist/src/tui/tui-local-shell.d.ts +28 -0
- package/dist/src/tui/tui-local-shell.js +147 -0
- package/dist/src/tui/tui-local-shell.js.map +1 -0
- package/dist/src/tui/tui-overlays.d.ts +8 -0
- package/dist/src/tui/tui-overlays.js +22 -0
- package/dist/src/tui/tui-overlays.js.map +1 -0
- package/dist/src/tui/tui-picker-overlay.d.ts +26 -0
- package/dist/src/tui/tui-picker-overlay.js +69 -0
- package/dist/src/tui/tui-picker-overlay.js.map +1 -0
- package/dist/src/tui/tui-stdio-filter.d.ts +17 -0
- package/dist/src/tui/tui-stdio-filter.js +96 -0
- package/dist/src/tui/tui-stdio-filter.js.map +1 -0
- package/dist/src/tui/tui-submit.d.ts +25 -0
- package/dist/src/tui/tui-submit.js +102 -0
- package/dist/src/tui/tui-submit.js.map +1 -0
- package/dist/src/tui/tui-suspend.d.ts +10 -0
- package/dist/src/tui/tui-suspend.js +18 -0
- package/dist/src/tui/tui-suspend.js.map +1 -0
- package/dist/src/tui/tui-types.d.ts +86 -0
- package/dist/src/tui/tui-types.js +21 -0
- package/dist/src/tui/tui-types.js.map +1 -0
- package/dist/src/tui/tui.d.ts +5 -0
- package/dist/src/tui/tui.js +389 -0
- package/dist/src/tui/tui.js.map +1 -0
- package/package.json +5 -3
- package/dist/gateway/static/root/assets/agents-w8_jzuiX.js +0 -216
- package/dist/gateway/static/root/assets/agents-w8_jzuiX.js.map +0 -1
- package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js +0 -9
- package/dist/gateway/static/root/assets/channels-settings-DUKRPC7C.js.map +0 -1
- package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js +0 -2
- package/dist/gateway/static/root/assets/cron-page-S18t1yG-.js.map +0 -1
- package/dist/gateway/static/root/assets/index-OT4cGzon.css +0 -1
- package/dist/gateway/static/root/assets/index-PfkB8N37.js +0 -4734
- package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-DoWe1GWy.js.map +0 -1
- package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js +0 -2
- package/dist/gateway/static/root/assets/sessions-page-2uOYwEwd.js.map +0 -1
- package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js +0 -2
- package/dist/gateway/static/root/assets/settings-page-fQWswCuq.js.map +0 -1
- package/dist/gateway/static/root/assets/skills-page-BmBDCEbY.js +0 -3
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
//#region src/gateway/security/operator-scopes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Operator scope system for fine-grained gateway API authorization.
|
|
4
|
+
*
|
|
5
|
+
* Each gateway API method maps to one or more required scopes.
|
|
6
|
+
* Clients declare their scopes at connection time; the gateway enforces
|
|
7
|
+
* that the declared scopes cover the method being invoked.
|
|
8
|
+
*/
|
|
9
|
+
const ADMIN_SCOPE = "operator.admin";
|
|
10
|
+
const READ_SCOPE = "operator.read";
|
|
11
|
+
const WRITE_SCOPE = "operator.write";
|
|
12
|
+
const KNOWN_OPERATOR_SCOPES = new Set([
|
|
13
|
+
ADMIN_SCOPE,
|
|
14
|
+
READ_SCOPE,
|
|
15
|
+
WRITE_SCOPE
|
|
16
|
+
]);
|
|
17
|
+
function isOperatorScope(value) {
|
|
18
|
+
return typeof value === "string" && KNOWN_OPERATOR_SCOPES.has(value);
|
|
19
|
+
}
|
|
20
|
+
/** Default scopes granted to authenticated CLI / direct connections. */
|
|
21
|
+
const DEFAULT_OPERATOR_SCOPES = [
|
|
22
|
+
ADMIN_SCOPE,
|
|
23
|
+
READ_SCOPE,
|
|
24
|
+
WRITE_SCOPE
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Route-level scope requirements.
|
|
28
|
+
* Maps HTTP route path prefixes to required minimum scope.
|
|
29
|
+
*/
|
|
30
|
+
const ROUTE_SCOPE_REQUIREMENTS = [
|
|
31
|
+
{
|
|
32
|
+
prefix: "/api/config",
|
|
33
|
+
scope: ADMIN_SCOPE
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
prefix: "/api/update",
|
|
37
|
+
scope: ADMIN_SCOPE
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
prefix: "/api/extensions/registry",
|
|
41
|
+
scope: ADMIN_SCOPE
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
prefix: "/api/agent",
|
|
45
|
+
scope: WRITE_SCOPE
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
prefix: "/api/sessions",
|
|
49
|
+
scope: WRITE_SCOPE
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
prefix: "/api/cron",
|
|
53
|
+
scope: WRITE_SCOPE
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
prefix: "/api/channels",
|
|
57
|
+
scope: WRITE_SCOPE
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
prefix: "/api/agents",
|
|
61
|
+
scope: WRITE_SCOPE
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
prefix: "/api/workspace",
|
|
65
|
+
scope: WRITE_SCOPE
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
prefix: "/api/skills",
|
|
69
|
+
scope: WRITE_SCOPE
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
prefix: "/api/commands",
|
|
73
|
+
scope: WRITE_SCOPE
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
prefix: "/api/host-fs",
|
|
77
|
+
scope: WRITE_SCOPE
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
prefix: "/api/status",
|
|
81
|
+
scope: READ_SCOPE
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
prefix: "/api/models",
|
|
85
|
+
scope: READ_SCOPE
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
prefix: "/api/logs",
|
|
89
|
+
scope: READ_SCOPE
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
prefix: "/api/doctor",
|
|
93
|
+
scope: READ_SCOPE
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
prefix: "/api/events",
|
|
97
|
+
scope: READ_SCOPE
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
/** Scope hierarchy: admin > write > read. */
|
|
101
|
+
const SCOPE_HIERARCHY = {
|
|
102
|
+
[ADMIN_SCOPE]: 3,
|
|
103
|
+
[WRITE_SCOPE]: 2,
|
|
104
|
+
[READ_SCOPE]: 1
|
|
105
|
+
};
|
|
106
|
+
function scopeSatisfies(granted, required) {
|
|
107
|
+
return SCOPE_HIERARCHY[granted] >= SCOPE_HIERARCHY[required];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check whether a set of granted scopes satisfies the required scope for a route.
|
|
111
|
+
*/
|
|
112
|
+
function authorizeRouteScope(routePath, grantedScopes) {
|
|
113
|
+
const routeRequirement = ROUTE_SCOPE_REQUIREMENTS.find((entry) => routePath.startsWith(entry.prefix));
|
|
114
|
+
if (!routeRequirement) return authorizeScope(READ_SCOPE, grantedScopes);
|
|
115
|
+
return authorizeScope(routeRequirement.scope, grantedScopes);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check whether any of the granted scopes satisfies the required scope.
|
|
119
|
+
*/
|
|
120
|
+
function authorizeScope(requiredScope, grantedScopes) {
|
|
121
|
+
if (grantedScopes.some((granted) => scopeSatisfies(granted, requiredScope))) return { allowed: true };
|
|
122
|
+
return {
|
|
123
|
+
allowed: false,
|
|
124
|
+
requiredScope
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse scopes from a comma-separated header value.
|
|
129
|
+
*/
|
|
130
|
+
function parseScopesHeader(headerValue) {
|
|
131
|
+
if (!headerValue?.trim()) return [...DEFAULT_OPERATOR_SCOPES];
|
|
132
|
+
return headerValue.split(",").map((scope) => scope.trim()).filter(isOperatorScope);
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { ADMIN_SCOPE, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, READ_SCOPE, WRITE_SCOPE, authorizeRouteScope, authorizeScope, isOperatorScope, parseScopesHeader };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=operator-scopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operator-scopes.js","names":[],"sources":["../../../../src/gateway/security/operator-scopes.ts"],"sourcesContent":["/**\n * Operator scope system for fine-grained gateway API authorization.\n *\n * Each gateway API method maps to one or more required scopes.\n * Clients declare their scopes at connection time; the gateway enforces\n * that the declared scopes cover the method being invoked.\n */\n\nexport const ADMIN_SCOPE = 'operator.admin' as const;\nexport const READ_SCOPE = 'operator.read' as const;\nexport const WRITE_SCOPE = 'operator.write' as const;\n\nexport type OperatorScope =\n | typeof ADMIN_SCOPE\n | typeof READ_SCOPE\n | typeof WRITE_SCOPE;\n\nconst KNOWN_OPERATOR_SCOPE_VALUES: readonly OperatorScope[] = [\n ADMIN_SCOPE,\n READ_SCOPE,\n WRITE_SCOPE,\n];\n\nexport const KNOWN_OPERATOR_SCOPES: ReadonlySet<OperatorScope> = new Set(\n KNOWN_OPERATOR_SCOPE_VALUES,\n);\n\nexport function isOperatorScope(value: unknown): value is OperatorScope {\n return typeof value === 'string' && KNOWN_OPERATOR_SCOPES.has(value as OperatorScope);\n}\n\n/** Default scopes granted to authenticated CLI / direct connections. */\nexport const DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [\n ADMIN_SCOPE,\n READ_SCOPE,\n WRITE_SCOPE,\n];\n\n/**\n * Route-level scope requirements.\n * Maps HTTP route path prefixes to required minimum scope.\n */\nconst ROUTE_SCOPE_REQUIREMENTS: ReadonlyArray<{ prefix: string; scope: OperatorScope }> = [\n // Admin routes\n { prefix: '/api/config', scope: ADMIN_SCOPE },\n { prefix: '/api/update', scope: ADMIN_SCOPE },\n { prefix: '/api/extensions/registry', scope: ADMIN_SCOPE },\n\n // Write routes\n { prefix: '/api/agent', scope: WRITE_SCOPE },\n { prefix: '/api/sessions', scope: WRITE_SCOPE },\n { prefix: '/api/cron', scope: WRITE_SCOPE },\n { prefix: '/api/channels', scope: WRITE_SCOPE },\n { prefix: '/api/agents', scope: WRITE_SCOPE },\n { prefix: '/api/workspace', scope: WRITE_SCOPE },\n { prefix: '/api/skills', scope: WRITE_SCOPE },\n { prefix: '/api/commands', scope: WRITE_SCOPE },\n { prefix: '/api/host-fs', scope: WRITE_SCOPE },\n\n // Read routes\n { prefix: '/api/status', scope: READ_SCOPE },\n { prefix: '/api/models', scope: READ_SCOPE },\n { prefix: '/api/logs', scope: READ_SCOPE },\n { prefix: '/api/doctor', scope: READ_SCOPE },\n { prefix: '/api/events', scope: READ_SCOPE },\n];\n\n/** Scope hierarchy: admin > write > read. */\nconst SCOPE_HIERARCHY: Record<OperatorScope, number> = {\n [ADMIN_SCOPE]: 3,\n [WRITE_SCOPE]: 2,\n [READ_SCOPE]: 1,\n};\n\nfunction scopeSatisfies(granted: OperatorScope, required: OperatorScope): boolean {\n return SCOPE_HIERARCHY[granted] >= SCOPE_HIERARCHY[required];\n}\n\n/**\n * Check whether a set of granted scopes satisfies the required scope for a route.\n */\nexport function authorizeRouteScope(\n routePath: string,\n grantedScopes: readonly OperatorScope[],\n): { allowed: true } | { allowed: false; requiredScope: OperatorScope } {\n const routeRequirement = ROUTE_SCOPE_REQUIREMENTS.find(\n (entry) => routePath.startsWith(entry.prefix),\n );\n\n if (!routeRequirement) {\n // Routes without explicit scope requirements default to read\n return authorizeScope(READ_SCOPE, grantedScopes);\n }\n\n return authorizeScope(routeRequirement.scope, grantedScopes);\n}\n\n/**\n * Check whether any of the granted scopes satisfies the required scope.\n */\nexport function authorizeScope(\n requiredScope: OperatorScope,\n grantedScopes: readonly OperatorScope[],\n): { allowed: true } | { allowed: false; requiredScope: OperatorScope } {\n const hasSufficientScope = grantedScopes.some(\n (granted) => scopeSatisfies(granted, requiredScope),\n );\n\n if (hasSufficientScope) {\n return { allowed: true };\n }\n return { allowed: false, requiredScope };\n}\n\n/**\n * Parse scopes from a comma-separated header value.\n */\nexport function parseScopesHeader(headerValue: string | undefined): OperatorScope[] {\n if (!headerValue?.trim()) {\n return [...DEFAULT_OPERATOR_SCOPES];\n }\n return headerValue\n .split(',')\n .map((scope) => scope.trim())\n .filter(isOperatorScope);\n}\n"],"mappings":";;;;;;;;AAQA,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,cAAc;AAa3B,MAAa,wBAAoD,IAAI,IACnE;CANA;CACA;CACA;CAIA,CACD;AAED,SAAgB,gBAAgB,OAAwC;AACtE,QAAO,OAAO,UAAU,YAAY,sBAAsB,IAAI,MAAuB;;;AAIvF,MAAa,0BAA2C;CACtD;CACA;CACA;CACD;;;;;AAMD,MAAM,2BAAoF;CAExF;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAA4B,OAAO;EAAa;CAG1D;EAAE,QAAQ;EAAc,OAAO;EAAa;CAC5C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAa,OAAO;EAAa;CAC3C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAkB,OAAO;EAAa;CAChD;EAAE,QAAQ;EAAe,OAAO;EAAa;CAC7C;EAAE,QAAQ;EAAiB,OAAO;EAAa;CAC/C;EAAE,QAAQ;EAAgB,OAAO;EAAa;CAG9C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAa,OAAO;EAAY;CAC1C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC5C;EAAE,QAAQ;EAAe,OAAO;EAAY;CAC7C;;AAGD,MAAM,kBAAiD;EACpD,cAAc;EACd,cAAc;EACd,aAAa;CACf;AAED,SAAS,eAAe,SAAwB,UAAkC;AAChF,QAAO,gBAAgB,YAAY,gBAAgB;;;;;AAMrD,SAAgB,oBACd,WACA,eACsE;CACtE,MAAM,mBAAmB,yBAAyB,MAC/C,UAAU,UAAU,WAAW,MAAM,OAAO,CAC9C;AAED,KAAI,CAAC,iBAEH,QAAO,eAAe,YAAY,cAAc;AAGlD,QAAO,eAAe,iBAAiB,OAAO,cAAc;;;;;AAM9D,SAAgB,eACd,eACA,eACsE;AAKtE,KAJ2B,cAAc,MACtC,YAAY,eAAe,SAAS,cAAc,CAG/B,CACpB,QAAO,EAAE,SAAS,MAAM;AAE1B,QAAO;EAAE,SAAS;EAAO;EAAe;;;;;AAM1C,SAAgB,kBAAkB,aAAkD;AAClF,KAAI,CAAC,aAAa,MAAM,CACtB,QAAO,CAAC,GAAG,wBAAwB;AAErC,QAAO,YACJ,MAAM,IAAI,CACV,KAAK,UAAU,MAAM,MAAM,CAAC,CAC5B,OAAO,gBAAgB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Origin checking for CSRF protection on HTTP and WebSocket requests.
|
|
3
|
+
*
|
|
4
|
+
* Validates that browser-initiated requests come from an allowed origin.
|
|
5
|
+
* Non-browser requests (no Origin header) are handled by other auth layers.
|
|
6
|
+
*/
|
|
7
|
+
type OriginCheckResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback';
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function checkBrowserOrigin(params: {
|
|
15
|
+
requestHost?: string;
|
|
16
|
+
origin?: string;
|
|
17
|
+
allowedOrigins?: string[];
|
|
18
|
+
allowHostHeaderOriginFallback?: boolean;
|
|
19
|
+
isLocalClient?: boolean;
|
|
20
|
+
}): OriginCheckResult;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/gateway/security/origin-check.ts
|
|
2
|
+
const LOOPBACK_HOSTNAMES = new Set([
|
|
3
|
+
"localhost",
|
|
4
|
+
"127.0.0.1",
|
|
5
|
+
"::1",
|
|
6
|
+
"[::1]"
|
|
7
|
+
]);
|
|
8
|
+
function isLoopbackHost(hostname) {
|
|
9
|
+
return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
function parseOriginHost(originRaw) {
|
|
12
|
+
const trimmed = (originRaw ?? "").trim();
|
|
13
|
+
if (!trimmed || trimmed === "null") return null;
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(trimmed);
|
|
16
|
+
return {
|
|
17
|
+
origin: url.origin.toLowerCase(),
|
|
18
|
+
host: url.host.toLowerCase(),
|
|
19
|
+
hostname: url.hostname.toLowerCase()
|
|
20
|
+
};
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function normalizeHostHeader(hostRaw) {
|
|
26
|
+
return (hostRaw ?? "").trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function checkBrowserOrigin(params) {
|
|
29
|
+
const parsedOrigin = parseOriginHost(params.origin);
|
|
30
|
+
if (!parsedOrigin) return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: "origin missing or invalid"
|
|
33
|
+
};
|
|
34
|
+
const allowlist = new Set((params.allowedOrigins ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
35
|
+
if (allowlist.has("*") || allowlist.has(parsedOrigin.origin)) return {
|
|
36
|
+
ok: true,
|
|
37
|
+
matchedBy: "allowlist"
|
|
38
|
+
};
|
|
39
|
+
const requestHost = normalizeHostHeader(params.requestHost);
|
|
40
|
+
if (params.allowHostHeaderOriginFallback === true && requestHost && parsedOrigin.host === requestHost) return {
|
|
41
|
+
ok: true,
|
|
42
|
+
matchedBy: "host-header-fallback"
|
|
43
|
+
};
|
|
44
|
+
if (params.isLocalClient && isLoopbackHost(parsedOrigin.hostname)) return {
|
|
45
|
+
ok: true,
|
|
46
|
+
matchedBy: "local-loopback"
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: "origin not allowed"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { checkBrowserOrigin };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=origin-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origin-check.js","names":[],"sources":["../../../../src/gateway/security/origin-check.ts"],"sourcesContent":["/**\n * Browser Origin checking for CSRF protection on HTTP and WebSocket requests.\n *\n * Validates that browser-initiated requests come from an allowed origin.\n * Non-browser requests (no Origin header) are handled by other auth layers.\n */\n\ntype OriginCheckResult =\n | { ok: true; matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback' }\n | { ok: false; reason: string };\n\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);\n\nfunction isLoopbackHost(hostname: string): boolean {\n return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());\n}\n\nfunction parseOriginHost(originRaw?: string): { origin: string; host: string; hostname: string } | null {\n const trimmed = (originRaw ?? '').trim();\n if (!trimmed || trimmed === 'null') {\n return null;\n }\n try {\n const url = new URL(trimmed);\n return {\n origin: url.origin.toLowerCase(),\n host: url.host.toLowerCase(),\n hostname: url.hostname.toLowerCase(),\n };\n } catch {\n return null;\n }\n}\n\nfunction normalizeHostHeader(hostRaw?: string): string {\n const trimmed = (hostRaw ?? '').trim().toLowerCase();\n // Strip port if present for comparison\n return trimmed;\n}\n\nexport function checkBrowserOrigin(params: {\n requestHost?: string;\n origin?: string;\n allowedOrigins?: string[];\n allowHostHeaderOriginFallback?: boolean;\n isLocalClient?: boolean;\n}): OriginCheckResult {\n const parsedOrigin = parseOriginHost(params.origin);\n if (!parsedOrigin) {\n return { ok: false, reason: 'origin missing or invalid' };\n }\n\n const allowlist = new Set(\n (params.allowedOrigins ?? [])\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean),\n );\n if (allowlist.has('*') || allowlist.has(parsedOrigin.origin)) {\n return { ok: true, matchedBy: 'allowlist' };\n }\n\n const requestHost = normalizeHostHeader(params.requestHost);\n if (\n params.allowHostHeaderOriginFallback === true &&\n requestHost &&\n parsedOrigin.host === requestHost\n ) {\n return { ok: true, matchedBy: 'host-header-fallback' };\n }\n\n // Dev fallback only for genuinely local socket clients, not Host-header claims.\n if (params.isLocalClient && isLoopbackHost(parsedOrigin.hostname)) {\n return { ok: true, matchedBy: 'local-loopback' };\n }\n\n return { ok: false, reason: 'origin not allowed' };\n}\n"],"mappings":";AAWA,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;CAAQ,CAAC;AAE9E,SAAS,eAAe,UAA2B;AACjD,QAAO,mBAAmB,IAAI,SAAS,aAAa,CAAC;;AAGvD,SAAS,gBAAgB,WAA+E;CACtG,MAAM,WAAW,aAAa,IAAI,MAAM;AACxC,KAAI,CAAC,WAAW,YAAY,OAC1B,QAAO;AAET,KAAI;EACF,MAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,SAAO;GACL,QAAQ,IAAI,OAAO,aAAa;GAChC,MAAM,IAAI,KAAK,aAAa;GAC5B,UAAU,IAAI,SAAS,aAAa;GACrC;SACK;AACN,SAAO;;;AAIX,SAAS,oBAAoB,SAA0B;AAGrD,SAFiB,WAAW,IAAI,MAAM,CAAC,aAEzB;;AAGhB,SAAgB,mBAAmB,QAMb;CACpB,MAAM,eAAe,gBAAgB,OAAO,OAAO;AACnD,KAAI,CAAC,aACH,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA6B;CAG3D,MAAM,YAAY,IAAI,KACnB,OAAO,kBAAkB,EAAE,EACzB,KAAK,UAAU,MAAM,MAAM,CAAC,aAAa,CAAC,CAC1C,OAAO,QAAQ,CACnB;AACD,KAAI,UAAU,IAAI,IAAI,IAAI,UAAU,IAAI,aAAa,OAAO,CAC1D,QAAO;EAAE,IAAI;EAAM,WAAW;EAAa;CAG7C,MAAM,cAAc,oBAAoB,OAAO,YAAY;AAC3D,KACE,OAAO,kCAAkC,QACzC,eACA,aAAa,SAAS,YAEtB,QAAO;EAAE,IAAI;EAAM,WAAW;EAAwB;AAIxD,KAAI,OAAO,iBAAiB,eAAe,aAAa,SAAS,CAC/D,QAAO;EAAE,IAAI;EAAM,WAAW;EAAkB;AAGlD,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAsB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-authentication connection budget per IP address.
|
|
3
|
+
*
|
|
4
|
+
* Limits the number of concurrent unauthenticated connections from a single
|
|
5
|
+
* IP to prevent connection-flood DoS attacks. Once a connection authenticates
|
|
6
|
+
* successfully, its slot is released.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMaxPreauthConnectionsPerIp(env?: NodeJS.ProcessEnv): number;
|
|
9
|
+
export type PreauthConnectionBudget = {
|
|
10
|
+
/** Try to acquire a slot for the given client IP. Returns false if the budget is exhausted. */
|
|
11
|
+
acquire(clientIp: string | undefined): boolean;
|
|
12
|
+
/** Release a slot when the connection authenticates or closes. */
|
|
13
|
+
release(clientIp: string | undefined): void;
|
|
14
|
+
/** Current number of tracked IPs. */
|
|
15
|
+
size(): number;
|
|
16
|
+
};
|
|
17
|
+
export declare function createPreauthConnectionBudget(limit?: number): PreauthConnectionBudget;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/gateway/security/preauth-connection-budget.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pre-authentication connection budget per IP address.
|
|
4
|
+
*
|
|
5
|
+
* Limits the number of concurrent unauthenticated connections from a single
|
|
6
|
+
* IP to prevent connection-flood DoS attacks. Once a connection authenticates
|
|
7
|
+
* successfully, its slot is released.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP = 32;
|
|
10
|
+
const UNKNOWN_CLIENT_IP_BUDGET_KEY = "__xopc_unknown_client_ip__";
|
|
11
|
+
function getMaxPreauthConnectionsPerIp(env = process.env) {
|
|
12
|
+
const configured = env.XOPC_MAX_PREAUTH_CONNECTIONS_PER_IP;
|
|
13
|
+
if (!configured) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
|
|
14
|
+
const parsed = Number(configured);
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
|
|
16
|
+
return Math.max(1, Math.floor(parsed));
|
|
17
|
+
}
|
|
18
|
+
function createPreauthConnectionBudget(limit = getMaxPreauthConnectionsPerIp()) {
|
|
19
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20
|
+
function normalizeBudgetKey(clientIp) {
|
|
21
|
+
return clientIp?.trim() || UNKNOWN_CLIENT_IP_BUDGET_KEY;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
acquire(clientIp) {
|
|
25
|
+
const ip = normalizeBudgetKey(clientIp);
|
|
26
|
+
const next = (counts.get(ip) ?? 0) + 1;
|
|
27
|
+
if (next > limit) return false;
|
|
28
|
+
counts.set(ip, next);
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
release(clientIp) {
|
|
32
|
+
const ip = normalizeBudgetKey(clientIp);
|
|
33
|
+
const current = counts.get(ip);
|
|
34
|
+
if (current === void 0) return;
|
|
35
|
+
if (current <= 1) {
|
|
36
|
+
counts.delete(ip);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
counts.set(ip, current - 1);
|
|
40
|
+
},
|
|
41
|
+
size() {
|
|
42
|
+
return counts.size;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=preauth-connection-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preauth-connection-budget.js","names":[],"sources":["../../../../src/gateway/security/preauth-connection-budget.ts"],"sourcesContent":["/**\n * Pre-authentication connection budget per IP address.\n *\n * Limits the number of concurrent unauthenticated connections from a single\n * IP to prevent connection-flood DoS attacks. Once a connection authenticates\n * successfully, its slot is released.\n */\n\nconst DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP = 32;\nconst UNKNOWN_CLIENT_IP_BUDGET_KEY = '__xopc_unknown_client_ip__';\n\nexport function getMaxPreauthConnectionsPerIp(env: NodeJS.ProcessEnv = process.env): number {\n const configured = env.XOPC_MAX_PREAUTH_CONNECTIONS_PER_IP;\n if (!configured) {\n return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;\n }\n const parsed = Number(configured);\n if (!Number.isFinite(parsed) || parsed < 1) {\n return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;\n }\n return Math.max(1, Math.floor(parsed));\n}\n\nexport type PreauthConnectionBudget = {\n /** Try to acquire a slot for the given client IP. Returns false if the budget is exhausted. */\n acquire(clientIp: string | undefined): boolean;\n /** Release a slot when the connection authenticates or closes. */\n release(clientIp: string | undefined): void;\n /** Current number of tracked IPs. */\n size(): number;\n};\n\nexport function createPreauthConnectionBudget(\n limit = getMaxPreauthConnectionsPerIp(),\n): PreauthConnectionBudget {\n const counts = new Map<string, number>();\n\n function normalizeBudgetKey(clientIp: string | undefined): string {\n const ip = clientIp?.trim();\n // Keep unresolved IPs capped under a shared fallback bucket\n // instead of failing open.\n return ip || UNKNOWN_CLIENT_IP_BUDGET_KEY;\n }\n\n return {\n acquire(clientIp) {\n const ip = normalizeBudgetKey(clientIp);\n const next = (counts.get(ip) ?? 0) + 1;\n if (next > limit) {\n return false;\n }\n counts.set(ip, next);\n return true;\n },\n\n release(clientIp) {\n const ip = normalizeBudgetKey(clientIp);\n const current = counts.get(ip);\n if (current === undefined) {\n return;\n }\n if (current <= 1) {\n counts.delete(ip);\n return;\n }\n counts.set(ip, current - 1);\n },\n\n size() {\n return counts.size;\n },\n };\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,yCAAyC;AAC/C,MAAM,+BAA+B;AAErC,SAAgB,8BAA8B,MAAyB,QAAQ,KAAa;CAC1F,MAAM,aAAa,IAAI;AACvB,KAAI,CAAC,WACH,QAAO;CAET,MAAM,SAAS,OAAO,WAAW;AACjC,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC,QAAO;AAET,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC;;AAYxC,SAAgB,8BACd,QAAQ,+BAA+B,EACd;CACzB,MAAM,yBAAS,IAAI,KAAqB;CAExC,SAAS,mBAAmB,UAAsC;AAIhE,SAHW,UAAU,MAAM,IAGd;;AAGf,QAAO;EACL,QAAQ,UAAU;GAChB,MAAM,KAAK,mBAAmB,SAAS;GACvC,MAAM,QAAQ,OAAO,IAAI,GAAG,IAAI,KAAK;AACrC,OAAI,OAAO,MACT,QAAO;AAET,UAAO,IAAI,IAAI,KAAK;AACpB,UAAO;;EAGT,QAAQ,UAAU;GAChB,MAAM,KAAK,mBAAmB,SAAS;GACvC,MAAM,UAAU,OAAO,IAAI,GAAG;AAC9B,OAAI,YAAY,KAAA,EACd;AAEF,OAAI,WAAW,GAAG;AAChB,WAAO,OAAO,GAAG;AACjB;;AAEF,UAAO,IAAI,IAAI,UAAU,EAAE;;EAG7B,OAAO;AACL,UAAO,OAAO;;EAEjB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constant-time string comparison to prevent timing attacks.
|
|
3
|
+
*
|
|
4
|
+
* Uses `crypto.timingSafeEqual` with padding so both buffers always have
|
|
5
|
+
* the same byte length. The actual length is checked separately to reject
|
|
6
|
+
* mismatches without leaking position information.
|
|
7
|
+
*/
|
|
8
|
+
export declare function safeEqualSecret(provided: string | undefined | null, expected: string | undefined | null): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
//#region src/gateway/security/secret-equal.ts
|
|
3
|
+
/**
|
|
4
|
+
* Pad a Buffer to the target length by allocating a zeroed buffer and copying.
|
|
5
|
+
*/
|
|
6
|
+
function padSecretBytes(bytes, length) {
|
|
7
|
+
if (bytes.length === length) return bytes;
|
|
8
|
+
const padded = Buffer.alloc(length);
|
|
9
|
+
bytes.copy(padded);
|
|
10
|
+
return padded;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Constant-time string comparison to prevent timing attacks.
|
|
14
|
+
*
|
|
15
|
+
* Uses `crypto.timingSafeEqual` with padding so both buffers always have
|
|
16
|
+
* the same byte length. The actual length is checked separately to reject
|
|
17
|
+
* mismatches without leaking position information.
|
|
18
|
+
*/
|
|
19
|
+
function safeEqualSecret(provided, expected) {
|
|
20
|
+
if (typeof provided !== "string" || typeof expected !== "string") return false;
|
|
21
|
+
const providedBytes = Buffer.from(provided, "utf8");
|
|
22
|
+
const expectedBytes = Buffer.from(expected, "utf8");
|
|
23
|
+
const byteLength = Math.max(providedBytes.length, expectedBytes.length);
|
|
24
|
+
if (byteLength === 0) return true;
|
|
25
|
+
return timingSafeEqual(padSecretBytes(providedBytes, byteLength), padSecretBytes(expectedBytes, byteLength)) && providedBytes.length === expectedBytes.length;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { safeEqualSecret };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=secret-equal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-equal.js","names":[],"sources":["../../../../src/gateway/security/secret-equal.ts"],"sourcesContent":["import { timingSafeEqual } from 'node:crypto';\n\n/**\n * Pad a Buffer to the target length by allocating a zeroed buffer and copying.\n */\nfunction padSecretBytes(bytes: Buffer, length: number): Buffer {\n if (bytes.length === length) {\n return bytes;\n }\n const padded = Buffer.alloc(length);\n bytes.copy(padded);\n return padded;\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n *\n * Uses `crypto.timingSafeEqual` with padding so both buffers always have\n * the same byte length. The actual length is checked separately to reject\n * mismatches without leaking position information.\n */\nexport function safeEqualSecret(\n provided: string | undefined | null,\n expected: string | undefined | null,\n): boolean {\n if (typeof provided !== 'string' || typeof expected !== 'string') {\n return false;\n }\n const providedBytes = Buffer.from(provided, 'utf8');\n const expectedBytes = Buffer.from(expected, 'utf8');\n const byteLength = Math.max(providedBytes.length, expectedBytes.length);\n if (byteLength === 0) {\n return true;\n }\n return (\n timingSafeEqual(\n padSecretBytes(providedBytes, byteLength),\n padSecretBytes(expectedBytes, byteLength),\n ) && providedBytes.length === expectedBytes.length\n );\n}\n"],"mappings":";;;;;AAKA,SAAS,eAAe,OAAe,QAAwB;AAC7D,KAAI,MAAM,WAAW,OACnB,QAAO;CAET,MAAM,SAAS,OAAO,MAAM,OAAO;AACnC,OAAM,KAAK,OAAO;AAClB,QAAO;;;;;;;;;AAUT,SAAgB,gBACd,UACA,UACS;AACT,KAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,QAAO;CAET,MAAM,gBAAgB,OAAO,KAAK,UAAU,OAAO;CACnD,MAAM,gBAAgB,OAAO,KAAK,UAAU,OAAO;CACnD,MAAM,aAAa,KAAK,IAAI,cAAc,QAAQ,cAAc,OAAO;AACvE,KAAI,eAAe,EACjB,QAAO;AAET,QACE,gBACE,eAAe,eAAe,WAAW,EACzC,eAAe,eAAe,WAAW,CAC1C,IAAI,cAAc,WAAW,cAAc"}
|
|
@@ -170,6 +170,8 @@ export declare class GatewayService {
|
|
|
170
170
|
}, unknown>;
|
|
171
171
|
/** Abort an in-flight webchat agent run (matches `runId` from SSE `status`). */
|
|
172
172
|
abortAgentRun(runId: string): boolean;
|
|
173
|
+
/** Background drain for extension-initiated webchat turns (`scheduleWebchatContinuation`). */
|
|
174
|
+
private drainScheduledWebchatContinuation;
|
|
173
175
|
/**
|
|
174
176
|
* Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).
|
|
175
177
|
* `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).
|
|
@@ -429,7 +431,7 @@ export declare class GatewayService {
|
|
|
429
431
|
/**
|
|
430
432
|
* Get current auth mode.
|
|
431
433
|
*/
|
|
432
|
-
getAuthMode(): 'none' | 'token';
|
|
434
|
+
getAuthMode(): 'none' | 'token' | 'password';
|
|
433
435
|
/**
|
|
434
436
|
* Get current auth token (for CLI server integration).
|
|
435
437
|
* Returns undefined if mode is 'none'.
|
|
@@ -35,6 +35,8 @@ import { computeBundledExtensionExtensionsPatch } from "../extensions/bundled-ex
|
|
|
35
35
|
import { HeartbeatService, heartbeatRunnerConfigFromConfig } from "./heartbeat/service.js";
|
|
36
36
|
import "./heartbeat/index.js";
|
|
37
37
|
import { assertGatewayAuthConfigured, extractToken, resolveGatewayAuth, validateToken } from "./auth.js";
|
|
38
|
+
import { assertGatewayAuthNotKnownWeak } from "./security/known-weak-secrets.js";
|
|
39
|
+
import { auditGatewayConfig } from "./security/audit.js";
|
|
38
40
|
import { AgentRunRelay } from "./agent-run-relay.js";
|
|
39
41
|
import { ClarifyBridge } from "./clarify-bridge.js";
|
|
40
42
|
import { downloadSkillZipBuffer, fetchMarketplacePackageDetail, listSkillPackages, resolveSkillZipDownloadUrl, resolveSkillsStoreBaseUrl, skillIdForMarketplaceInstall } from "../agent/skills/skills-store-client.js";
|
|
@@ -86,6 +88,12 @@ var GatewayService = class {
|
|
|
86
88
|
this.config = loadConfig(this.configPath);
|
|
87
89
|
this.auth = resolveGatewayAuth({ authConfig: this.config.gateway?.auth });
|
|
88
90
|
assertGatewayAuthConfigured(this.auth);
|
|
91
|
+
assertGatewayAuthNotKnownWeak(this.auth);
|
|
92
|
+
auditGatewayConfig({
|
|
93
|
+
auth: this.auth,
|
|
94
|
+
host: this.config.gateway?.host,
|
|
95
|
+
corsOrigins: this.config.gateway?.corsOrigins
|
|
96
|
+
});
|
|
89
97
|
if (this.auth.mode === "token") {
|
|
90
98
|
const tokenPreview = this.auth.token ? `${this.auth.token.slice(0, 4)}***` : "none";
|
|
91
99
|
log.info({
|
|
@@ -199,7 +207,12 @@ var GatewayService = class {
|
|
|
199
207
|
this.channelManager.enableOutboundPersistence(resolveAgentDir(this.config, getDefaultAgentId(this.config)));
|
|
200
208
|
if (this.extensionLoader) this.extensionLoader.setRuntimeContext({
|
|
201
209
|
bus: this.bus,
|
|
202
|
-
sessionManager: this.sessionManager
|
|
210
|
+
sessionManager: this.sessionManager,
|
|
211
|
+
scheduleWebchatContinuation: (sessionKey, continuationMessage) => {
|
|
212
|
+
queueMicrotask(() => {
|
|
213
|
+
this.drainScheduledWebchatContinuation(sessionKey, continuationMessage);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
203
216
|
});
|
|
204
217
|
await this.loadExtensionsAndRegisterChannels();
|
|
205
218
|
await this.channelManager.initialize();
|
|
@@ -609,7 +622,8 @@ var GatewayService = class {
|
|
|
609
622
|
peerKind: "direct",
|
|
610
623
|
peerId: chatId
|
|
611
624
|
});
|
|
612
|
-
const
|
|
625
|
+
const timezone = this.agentService.resolveUserTimezoneForSession(sessionKey);
|
|
626
|
+
const stampedMessage = message.trimStart().startsWith("/") ? message : prependEnvelopeTimestamp(message, timezone);
|
|
613
627
|
const prepared = await this.agentService.prepareInboundAttachments(sessionKey, cappedAttachments);
|
|
614
628
|
try {
|
|
615
629
|
await this._saveUserMessage(sessionKey, message, prepared);
|
|
@@ -623,6 +637,7 @@ var GatewayService = class {
|
|
|
623
637
|
if (!runAbort) throw new Error("run abort controller missing for webchat");
|
|
624
638
|
const mergedSignal = runOptions?.signal ? AbortSignal.any([runOptions.signal, runAbort.signal]) : runAbort.signal;
|
|
625
639
|
this.activeWebchatRunBySession.set(sessionKey, runId);
|
|
640
|
+
let streamError;
|
|
626
641
|
try {
|
|
627
642
|
this.emit("agent.stream", {
|
|
628
643
|
sessionKey,
|
|
@@ -651,9 +666,10 @@ var GatewayService = class {
|
|
|
651
666
|
};
|
|
652
667
|
} catch (error) {
|
|
653
668
|
log.error({ error }, "Agent processing failed");
|
|
669
|
+
streamError = error instanceof Error ? error.message : "Unknown error";
|
|
654
670
|
const errorEvent = {
|
|
655
671
|
type: "error",
|
|
656
|
-
content: `Error: ${
|
|
672
|
+
content: `Error: ${streamError}`
|
|
657
673
|
};
|
|
658
674
|
this.runRelay.publish(runId, errorEvent);
|
|
659
675
|
this.emit("agent.stream", {
|
|
@@ -664,11 +680,19 @@ var GatewayService = class {
|
|
|
664
680
|
yield errorEvent;
|
|
665
681
|
return {
|
|
666
682
|
status: "error",
|
|
667
|
-
summary:
|
|
683
|
+
summary: streamError
|
|
668
684
|
};
|
|
669
685
|
} finally {
|
|
670
686
|
this.activeWebchatRunBySession.delete(sessionKey);
|
|
671
687
|
this.runAbortControllers.delete(runId);
|
|
688
|
+
const assistantPlainText = this.agentService.getLastAssistantPlainText(sessionKey);
|
|
689
|
+
await this.agentService.emitWebchatTurnComplete({
|
|
690
|
+
sessionKey,
|
|
691
|
+
inboundUserText: message,
|
|
692
|
+
assistantPlainText,
|
|
693
|
+
aborted: mergedSignal.aborted,
|
|
694
|
+
...streamError !== void 0 ? { streamError } : {}
|
|
695
|
+
});
|
|
672
696
|
}
|
|
673
697
|
}
|
|
674
698
|
const correlationMeta = inboundCorrelationMetadataFromAsyncLogContext();
|
|
@@ -706,6 +730,18 @@ var GatewayService = class {
|
|
|
706
730
|
c.abort();
|
|
707
731
|
return true;
|
|
708
732
|
}
|
|
733
|
+
/** Background drain for extension-initiated webchat turns (`scheduleWebchatContinuation`). */
|
|
734
|
+
async drainScheduledWebchatContinuation(sessionKey, message) {
|
|
735
|
+
try {
|
|
736
|
+
const gen = this.runAgent(message, "webchat", sessionKey, void 0, void 0, void 0);
|
|
737
|
+
for await (const _ of gen);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
log.warn({
|
|
740
|
+
err,
|
|
741
|
+
sessionKey
|
|
742
|
+
}, "Scheduled webchat continuation failed");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
709
745
|
/**
|
|
710
746
|
* Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).
|
|
711
747
|
* `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).
|