@xopcai/xopc 0.0.26 → 0.0.28

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.
Files changed (152) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/extensions/weixin/src/adapters/onboard-cli.d.ts +7 -0
  3. package/dist/extensions/weixin/src/adapters/onboard-cli.js +61 -0
  4. package/dist/extensions/weixin/src/adapters/onboard-cli.js.map +1 -0
  5. package/dist/extensions/weixin/src/cli/qr-login.d.ts +5 -0
  6. package/dist/extensions/weixin/src/cli/qr-login.js +1 -1
  7. package/dist/extensions/weixin/src/cli/qr-login.js.map +1 -1
  8. package/dist/extensions/weixin/src/index.js +1 -1
  9. package/dist/extensions/weixin/src/plugin.d.ts +1 -0
  10. package/dist/extensions/weixin/src/plugin.js +2 -0
  11. package/dist/extensions/weixin/src/plugin.js.map +1 -1
  12. package/dist/gateway/static/root/assets/{agents-Clv9i1Kb.js → agents-DplaQYS2.js} +2 -2
  13. package/dist/gateway/static/root/assets/{agents-Clv9i1Kb.js.map → agents-DplaQYS2.js.map} +1 -1
  14. package/dist/gateway/static/root/assets/{apps-page-DqclV-PP.js → apps-page-Co95hLOJ.js} +2 -2
  15. package/dist/gateway/static/root/assets/{apps-page-DqclV-PP.js.map → apps-page-Co95hLOJ.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/{channels-settings-CLyTYjrz.js → channels-settings-CkfSST0k.js} +2 -2
  17. package/dist/gateway/static/root/assets/{channels-settings-CLyTYjrz.js.map → channels-settings-CkfSST0k.js.map} +1 -1
  18. package/dist/gateway/static/root/assets/{cron-page-CU8lutMt.js → cron-page-D9q6KqL8.js} +2 -2
  19. package/dist/gateway/static/root/assets/{cron-page-CU8lutMt.js.map → cron-page-D9q6KqL8.js.map} +1 -1
  20. package/dist/gateway/static/root/assets/{cron-utils-_UjiWax6.js → cron-utils-BmzF4m1y.js} +2 -2
  21. package/dist/gateway/static/root/assets/{cron-utils-_UjiWax6.js.map → cron-utils-BmzF4m1y.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/{dist-Xqb4IGWC.js → dist-Dn-ufXyc.js} +2 -2
  23. package/dist/gateway/static/root/assets/{dist-Xqb4IGWC.js.map → dist-Dn-ufXyc.js.map} +1 -1
  24. package/dist/gateway/static/root/assets/{extension-debug-page-CtTUkAmw.js → extension-debug-page-BZ8xQ74_.js} +2 -2
  25. package/dist/gateway/static/root/assets/{extension-debug-page-CtTUkAmw.js.map → extension-debug-page-BZ8xQ74_.js.map} +1 -1
  26. package/dist/gateway/static/root/assets/{extension-page-C-aQU8qR.js → extension-page-BlNgKxwW.js} +2 -2
  27. package/dist/gateway/static/root/assets/{extension-page-C-aQU8qR.js.map → extension-page-BlNgKxwW.js.map} +1 -1
  28. package/dist/gateway/static/root/assets/{extension-settings-page-b0y9aY-q.js → extension-settings-page-CWTdW_oY.js} +2 -2
  29. package/dist/gateway/static/root/assets/{extension-settings-page-b0y9aY-q.js.map → extension-settings-page-CWTdW_oY.js.map} +1 -1
  30. package/dist/gateway/static/root/assets/index-OT4cGzon.css +1 -0
  31. package/dist/gateway/static/root/assets/{index-Gr2HWo-G.js → index-lV8FGWlt.js} +94 -94
  32. package/dist/gateway/static/root/assets/{index-Gr2HWo-G.js.map → index-lV8FGWlt.js.map} +1 -1
  33. package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js +2 -0
  34. package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js.map +1 -0
  35. package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js +2 -0
  36. package/dist/gateway/static/root/assets/{sessions-page-Cryg-36Z.js.map → sessions-page-CdmjxDEM.js.map} +1 -1
  37. package/dist/gateway/static/root/assets/{settings-page-DFNKT9yg.js → settings-page-DU2XLf5s.js} +2 -2
  38. package/dist/gateway/static/root/assets/{settings-page-DFNKT9yg.js.map → settings-page-DU2XLf5s.js.map} +1 -1
  39. package/dist/gateway/static/root/assets/{skills-page-D4gfh0Ih.js → skills-page-lb7vYtlP.js} +2 -2
  40. package/dist/gateway/static/root/assets/{skills-page-D4gfh0Ih.js.map → skills-page-lb7vYtlP.js.map} +1 -1
  41. package/dist/gateway/static/root/index.html +2 -2
  42. package/dist/package.js +1 -1
  43. package/dist/src/channels/index.js +2 -2
  44. package/dist/src/channels/manager.js +2 -2
  45. package/dist/src/channels/weixin/index.js +1 -1
  46. package/dist/src/cli/agent-chat-log-level-preset.d.ts +7 -0
  47. package/dist/src/cli/agent-chat-log-level-preset.js +22 -0
  48. package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -0
  49. package/dist/src/cli/commands/agent/interactive.js +4 -2
  50. package/dist/src/cli/commands/agent/interactive.js.map +1 -1
  51. package/dist/src/cli/commands/agent/stream-renderer.d.ts +14 -0
  52. package/dist/src/cli/commands/agent/stream-renderer.js +99 -0
  53. package/dist/src/cli/commands/agent/stream-renderer.js.map +1 -0
  54. package/dist/src/cli/commands/agent.js +2 -2
  55. package/dist/src/cli/commands/agent.js.map +1 -1
  56. package/dist/src/cli/commands/onboard.js +77 -93
  57. package/dist/src/cli/commands/onboard.js.map +1 -1
  58. package/dist/src/cli/commands/tui.d.ts +1 -0
  59. package/dist/src/cli/commands/tui.js +40 -0
  60. package/dist/src/cli/commands/tui.js.map +1 -0
  61. package/dist/src/cli/index.d.ts +2 -0
  62. package/dist/src/cli/index.js +3 -0
  63. package/dist/src/cli/index.js.map +1 -1
  64. package/dist/src/config/schema.d.ts +6 -0
  65. package/dist/src/config/schema.js +6 -1
  66. package/dist/src/config/schema.js.map +1 -1
  67. package/dist/src/gateway/auth.d.ts +17 -3
  68. package/dist/src/gateway/auth.js +35 -16
  69. package/dist/src/gateway/auth.js.map +1 -1
  70. package/dist/src/gateway/hono/app.js +30 -1
  71. package/dist/src/gateway/hono/app.js.map +1 -1
  72. package/dist/src/gateway/hono/lib/config-payload.d.ts +1 -1
  73. package/dist/src/gateway/hono/middleware/auth.js +4 -3
  74. package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
  75. package/dist/src/gateway/hono/middleware/scopes.d.ts +15 -0
  76. package/dist/src/gateway/hono/middleware/scopes.js +41 -0
  77. package/dist/src/gateway/hono/middleware/scopes.js.map +1 -0
  78. package/dist/src/gateway/security/audit.d.ts +18 -0
  79. package/dist/src/gateway/security/audit.js +68 -0
  80. package/dist/src/gateway/security/audit.js.map +1 -0
  81. package/dist/src/gateway/security/csp.d.ts +19 -0
  82. package/dist/src/gateway/security/csp.js +52 -0
  83. package/dist/src/gateway/security/csp.js.map +1 -0
  84. package/dist/src/gateway/security/dangerous-tools.d.ts +20 -0
  85. package/dist/src/gateway/security/dangerous-tools.js +46 -0
  86. package/dist/src/gateway/security/dangerous-tools.js.map +1 -0
  87. package/dist/src/gateway/security/flood-guard.d.ts +28 -0
  88. package/dist/src/gateway/security/flood-guard.js +42 -0
  89. package/dist/src/gateway/security/flood-guard.js.map +1 -0
  90. package/dist/src/gateway/security/index.d.ts +9 -0
  91. package/dist/src/gateway/security/index.js +10 -0
  92. package/dist/src/gateway/security/known-weak-secrets.d.ts +10 -0
  93. package/dist/src/gateway/security/known-weak-secrets.js +36 -0
  94. package/dist/src/gateway/security/known-weak-secrets.js.map +1 -0
  95. package/dist/src/gateway/security/operator-scopes.d.ts +37 -0
  96. package/dist/src/gateway/security/operator-scopes.js +137 -0
  97. package/dist/src/gateway/security/operator-scopes.js.map +1 -0
  98. package/dist/src/gateway/security/origin-check.d.ts +21 -0
  99. package/dist/src/gateway/security/origin-check.js +56 -0
  100. package/dist/src/gateway/security/origin-check.js.map +1 -0
  101. package/dist/src/gateway/security/preauth-connection-budget.d.ts +17 -0
  102. package/dist/src/gateway/security/preauth-connection-budget.js +49 -0
  103. package/dist/src/gateway/security/preauth-connection-budget.js.map +1 -0
  104. package/dist/src/gateway/security/secret-equal.d.ts +8 -0
  105. package/dist/src/gateway/security/secret-equal.js +30 -0
  106. package/dist/src/gateway/security/secret-equal.js.map +1 -0
  107. package/dist/src/gateway/service.d.ts +1 -1
  108. package/dist/src/gateway/service.js +11 -2
  109. package/dist/src/gateway/service.js.map +1 -1
  110. package/dist/src/tui/backends/embedded-backend.d.ts +42 -0
  111. package/dist/src/tui/backends/embedded-backend.js +160 -0
  112. package/dist/src/tui/backends/embedded-backend.js.map +1 -0
  113. package/dist/src/tui/backends/gateway-sse-backend.d.ts +49 -0
  114. package/dist/src/tui/backends/gateway-sse-backend.js +226 -0
  115. package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -0
  116. package/dist/src/tui/components/assistant-message.d.ts +6 -0
  117. package/dist/src/tui/components/assistant-message.js +19 -0
  118. package/dist/src/tui/components/assistant-message.js.map +1 -0
  119. package/dist/src/tui/components/chat-log.d.ts +19 -0
  120. package/dist/src/tui/components/chat-log.js +99 -0
  121. package/dist/src/tui/components/chat-log.js.map +1 -0
  122. package/dist/src/tui/components/custom-editor.d.ts +13 -0
  123. package/dist/src/tui/components/custom-editor.js +44 -0
  124. package/dist/src/tui/components/custom-editor.js.map +1 -0
  125. package/dist/src/tui/components/tool-execution.d.ts +16 -0
  126. package/dist/src/tui/components/tool-execution.js +76 -0
  127. package/dist/src/tui/components/tool-execution.js.map +1 -0
  128. package/dist/src/tui/components/user-message.d.ts +6 -0
  129. package/dist/src/tui/components/user-message.js +22 -0
  130. package/dist/src/tui/components/user-message.js.map +1 -0
  131. package/dist/src/tui/sse-consumer.d.ts +15 -0
  132. package/dist/src/tui/sse-consumer.js +75 -0
  133. package/dist/src/tui/sse-consumer.js.map +1 -0
  134. package/dist/src/tui/stream-assembler.d.ts +22 -0
  135. package/dist/src/tui/stream-assembler.js +63 -0
  136. package/dist/src/tui/stream-assembler.js.map +1 -0
  137. package/dist/src/tui/theme.d.ts +71 -0
  138. package/dist/src/tui/theme.js +151 -0
  139. package/dist/src/tui/theme.js.map +1 -0
  140. package/dist/src/tui/tui-backend.d.ts +84 -0
  141. package/dist/src/tui/tui-backend.js +1 -0
  142. package/dist/src/tui/tui-types.d.ts +85 -0
  143. package/dist/src/tui/tui-types.js +21 -0
  144. package/dist/src/tui/tui-types.js.map +1 -0
  145. package/dist/src/tui/tui.d.ts +3 -0
  146. package/dist/src/tui/tui.js +526 -0
  147. package/dist/src/tui/tui.js.map +1 -0
  148. package/package.json +9 -3
  149. package/dist/gateway/static/root/assets/index-DhSFfSNN.css +0 -1
  150. package/dist/gateway/static/root/assets/logs-page-DRI33XK4.js +0 -2
  151. package/dist/gateway/static/root/assets/logs-page-DRI33XK4.js.map +0 -1
  152. package/dist/gateway/static/root/assets/sessions-page-Cryg-36Z.js +0 -2
@@ -0,0 +1,9 @@
1
+ export { safeEqualSecret } from './secret-equal.js';
2
+ export { assertGatewayAuthNotKnownWeak, KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, } from './known-weak-secrets.js';
3
+ export { checkBrowserOrigin } from './origin-check.js';
4
+ export { computeInlineScriptHashes, buildGatewayConsoleCspHeader } from './csp.js';
5
+ export { ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE, KNOWN_OPERATOR_SCOPES, DEFAULT_OPERATOR_SCOPES, isOperatorScope, authorizeRouteScope, authorizeScope, parseScopesHeader, type OperatorScope, } from './operator-scopes.js';
6
+ export { DEFAULT_GATEWAY_HTTP_TOOL_DENY, isDangerousHttpTool, filterDangerousHttpTools, } from './dangerous-tools.js';
7
+ export { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp, type PreauthConnectionBudget, } from './preauth-connection-budget.js';
8
+ export { UnauthorizedFloodGuard, type FloodGuardOptions, type FloodGuardDecision, } from './flood-guard.js';
9
+ export { auditGatewayConfig, type SecurityAuditFinding, } from './audit.js';
@@ -0,0 +1,10 @@
1
+ import { safeEqualSecret } from "./secret-equal.js";
2
+ import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, assertGatewayAuthNotKnownWeak } from "./known-weak-secrets.js";
3
+ import { auditGatewayConfig } from "./audit.js";
4
+ import { buildGatewayConsoleCspHeader, computeInlineScriptHashes } from "./csp.js";
5
+ import { checkBrowserOrigin } from "./origin-check.js";
6
+ import { ADMIN_SCOPE, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, READ_SCOPE, WRITE_SCOPE, authorizeRouteScope, authorizeScope, isOperatorScope, parseScopesHeader } from "./operator-scopes.js";
7
+ import { DEFAULT_GATEWAY_HTTP_TOOL_DENY, filterDangerousHttpTools, isDangerousHttpTool } from "./dangerous-tools.js";
8
+ import { UnauthorizedFloodGuard } from "./flood-guard.js";
9
+ import { createPreauthConnectionBudget, getMaxPreauthConnectionsPerIp } from "./preauth-connection-budget.js";
10
+ export { ADMIN_SCOPE, DEFAULT_GATEWAY_HTTP_TOOL_DENY, DEFAULT_OPERATOR_SCOPES, KNOWN_OPERATOR_SCOPES, KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, READ_SCOPE, UnauthorizedFloodGuard, WRITE_SCOPE, assertGatewayAuthNotKnownWeak, auditGatewayConfig, authorizeRouteScope, authorizeScope, buildGatewayConsoleCspHeader, checkBrowserOrigin, computeInlineScriptHashes, createPreauthConnectionBudget, filterDangerousHttpTools, getMaxPreauthConnectionsPerIp, isDangerousHttpTool, isOperatorScope, parseScopesHeader, safeEqualSecret };
@@ -0,0 +1,10 @@
1
+ import type { ResolvedGatewayAuth } from '../auth.js';
2
+ /**
3
+ * Placeholder credentials that have shipped in `.env.example` or been used as
4
+ * copy-paste examples in onboarding docs. If any of these becomes the resolved
5
+ * gateway credential, reject it at startup. The operator almost certainly
6
+ * copied an example file verbatim without replacing the sentinel, which would
7
+ * otherwise leave the gateway protected by a publicly-known credential.
8
+ */
9
+ export declare const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS: readonly ["change-me-to-a-long-random-token", "change-me-now", "your-secret-token-here", "test-token", "my-token", "token", "secret", "password", "123456", "abc123"];
10
+ export declare function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void;
@@ -0,0 +1,36 @@
1
+ //#region src/gateway/security/known-weak-secrets.ts
2
+ /**
3
+ * Placeholder credentials that have shipped in `.env.example` or been used as
4
+ * copy-paste examples in onboarding docs. If any of these becomes the resolved
5
+ * gateway credential, reject it at startup. The operator almost certainly
6
+ * copied an example file verbatim without replacing the sentinel, which would
7
+ * otherwise leave the gateway protected by a publicly-known credential.
8
+ */
9
+ const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [
10
+ "change-me-to-a-long-random-token",
11
+ "change-me-now",
12
+ "your-secret-token-here",
13
+ "test-token",
14
+ "my-token",
15
+ "token",
16
+ "secret",
17
+ "password",
18
+ "123456",
19
+ "abc123"
20
+ ];
21
+ const KNOWN_WEAK_GATEWAY_TOKENS = new Set(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS);
22
+ /**
23
+ * Minimum acceptable token length. Short tokens are vulnerable to brute-force
24
+ * even with rate limiting.
25
+ */
26
+ const MIN_TOKEN_LENGTH = 16;
27
+ function assertGatewayAuthNotKnownWeak(auth) {
28
+ if (auth.mode !== "token" || !auth.token) return;
29
+ const token = auth.token.trim();
30
+ if (KNOWN_WEAK_GATEWAY_TOKENS.has(token)) throw new Error("Invalid config: gateway auth token is set to a published example placeholder from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) and set XOPC_GATEWAY_TOKEN or gateway.auth.token before starting the gateway.");
31
+ if (token.length < MIN_TOKEN_LENGTH) throw new Error(`Invalid config: gateway auth token is too short (${token.length} chars, minimum ${MIN_TOKEN_LENGTH}). Use a strong random token (e.g. \`openssl rand -hex 32\`).`);
32
+ }
33
+ //#endregion
34
+ export { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, assertGatewayAuthNotKnownWeak };
35
+
36
+ //# sourceMappingURL=known-weak-secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-weak-secrets.js","names":[],"sources":["../../../../src/gateway/security/known-weak-secrets.ts"],"sourcesContent":["import type { ResolvedGatewayAuth } from '../auth.js';\n\n/**\n * Placeholder credentials that have shipped in `.env.example` or been used as\n * copy-paste examples in onboarding docs. If any of these becomes the resolved\n * gateway credential, reject it at startup. The operator almost certainly\n * copied an example file verbatim without replacing the sentinel, which would\n * otherwise leave the gateway protected by a publicly-known credential.\n */\n\nexport const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [\n 'change-me-to-a-long-random-token',\n 'change-me-now',\n 'your-secret-token-here',\n 'test-token',\n 'my-token',\n 'token',\n 'secret',\n 'password',\n '123456',\n 'abc123',\n] as const;\n\nconst KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet<string> = new Set(\n KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,\n);\n\n/**\n * Minimum acceptable token length. Short tokens are vulnerable to brute-force\n * even with rate limiting.\n */\nconst MIN_TOKEN_LENGTH = 16;\n\nexport function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {\n if (auth.mode !== 'token' || !auth.token) {\n return;\n }\n\n const token = auth.token.trim();\n\n if (KNOWN_WEAK_GATEWAY_TOKENS.has(token)) {\n throw new Error(\n 'Invalid config: gateway auth token is set to a published example placeholder ' +\n 'from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) ' +\n 'and set XOPC_GATEWAY_TOKEN or gateway.auth.token before starting the gateway.',\n );\n }\n\n if (token.length < MIN_TOKEN_LENGTH) {\n throw new Error(\n `Invalid config: gateway auth token is too short (${token.length} chars, minimum ${MIN_TOKEN_LENGTH}). ` +\n 'Use a strong random token (e.g. `openssl rand -hex 32`).',\n );\n }\n}\n"],"mappings":";;;;;;;;AAUA,MAAa,wCAAwC;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,4BAAiD,IAAI,IACzD,sCACD;;;;;AAMD,MAAM,mBAAmB;AAEzB,SAAgB,8BAA8B,MAAiC;AAC7E,KAAI,KAAK,SAAS,WAAW,CAAC,KAAK,MACjC;CAGF,MAAM,QAAQ,KAAK,MAAM,MAAM;AAE/B,KAAI,0BAA0B,IAAI,MAAM,CACtC,OAAM,IAAI,MACR,6OAGD;AAGH,KAAI,MAAM,SAAS,iBACjB,OAAM,IAAI,MACR,oDAAoD,MAAM,OAAO,kBAAkB,iBAAiB,+DAErG"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Operator scope system for fine-grained gateway API authorization.
3
+ *
4
+ * Each gateway API method maps to one or more required scopes.
5
+ * Clients declare their scopes at connection time; the gateway enforces
6
+ * that the declared scopes cover the method being invoked.
7
+ */
8
+ export declare const ADMIN_SCOPE: "operator.admin";
9
+ export declare const READ_SCOPE: "operator.read";
10
+ export declare const WRITE_SCOPE: "operator.write";
11
+ export type OperatorScope = typeof ADMIN_SCOPE | typeof READ_SCOPE | typeof WRITE_SCOPE;
12
+ export declare const KNOWN_OPERATOR_SCOPES: ReadonlySet<OperatorScope>;
13
+ export declare function isOperatorScope(value: unknown): value is OperatorScope;
14
+ /** Default scopes granted to authenticated CLI / direct connections. */
15
+ export declare const DEFAULT_OPERATOR_SCOPES: OperatorScope[];
16
+ /**
17
+ * Check whether a set of granted scopes satisfies the required scope for a route.
18
+ */
19
+ export declare function authorizeRouteScope(routePath: string, grantedScopes: readonly OperatorScope[]): {
20
+ allowed: true;
21
+ } | {
22
+ allowed: false;
23
+ requiredScope: OperatorScope;
24
+ };
25
+ /**
26
+ * Check whether any of the granted scopes satisfies the required scope.
27
+ */
28
+ export declare function authorizeScope(requiredScope: OperatorScope, grantedScopes: readonly OperatorScope[]): {
29
+ allowed: true;
30
+ } | {
31
+ allowed: false;
32
+ requiredScope: OperatorScope;
33
+ };
34
+ /**
35
+ * Parse scopes from a comma-separated header value.
36
+ */
37
+ export declare function parseScopesHeader(headerValue: string | undefined): OperatorScope[];
@@ -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"}
@@ -429,7 +429,7 @@ export declare class GatewayService {
429
429
  /**
430
430
  * Get current auth mode.
431
431
  */
432
- getAuthMode(): 'none' | 'token';
432
+ getAuthMode(): 'none' | 'token' | 'password';
433
433
  /**
434
434
  * Get current auth token (for CLI server integration).
435
435
  * Returns undefined if mode is 'none'.
@@ -26,15 +26,17 @@ import { CHAT_CHANNEL_ORDER } from "../channels/registry.js";
26
26
  import { ExtensionLoader } from "../extensions/loader.js";
27
27
  import "../extensions/index.js";
28
28
  import { AgentService } from "../agent/service.js";
29
- import { ChannelManager } from "../channels/manager.js";
30
29
  import { ConfigHotReloader } from "../config/reload.js";
31
30
  import "../config/index.js";
31
+ import { ChannelManager } from "../channels/manager.js";
32
32
  import { CronService } from "../cron/service.js";
33
33
  import "../cron/index.js";
34
34
  import { computeBundledExtensionExtensionsPatch } from "../extensions/bundled-extension-activation.js";
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({
@@ -609,7 +617,8 @@ var GatewayService = class {
609
617
  peerKind: "direct",
610
618
  peerId: chatId
611
619
  });
612
- const stampedMessage = prependEnvelopeTimestamp(message, this.agentService.resolveUserTimezoneForSession(sessionKey));
620
+ const timezone = this.agentService.resolveUserTimezoneForSession(sessionKey);
621
+ const stampedMessage = message.trimStart().startsWith("/") ? message : prependEnvelopeTimestamp(message, timezone);
613
622
  const prepared = await this.agentService.prepareInboundAttachments(sessionKey, cappedAttachments);
614
623
  try {
615
624
  await this._saveUserMessage(sessionKey, message, prepared);