opc-agent 1.2.0 → 1.2.1

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 (155) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/CONTRIBUTING.md +75 -75
  3. package/README.md +429 -429
  4. package/README.zh-CN.md +415 -415
  5. package/dist/channels/web.js +256 -256
  6. package/dist/core/knowledge.d.ts +0 -5
  7. package/dist/core/knowledge.js +2 -39
  8. package/dist/core/streaming.d.ts +56 -0
  9. package/dist/core/streaming.js +160 -0
  10. package/dist/deploy/hermes.js +22 -22
  11. package/dist/deploy/openclaw.js +31 -31
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.js +7 -1
  14. package/dist/providers/index.d.ts +1 -1
  15. package/dist/providers/index.js +14 -158
  16. package/dist/schema/oad.d.ts +3 -3
  17. package/dist/templates/code-reviewer.js +5 -5
  18. package/dist/templates/customer-service.js +2 -2
  19. package/dist/templates/data-analyst.js +5 -5
  20. package/dist/templates/knowledge-base.js +2 -2
  21. package/dist/templates/sales-assistant.js +4 -4
  22. package/dist/templates/teacher.js +6 -6
  23. package/dist/tools/gateway.d.ts +28 -0
  24. package/dist/tools/gateway.js +177 -0
  25. package/docs/.vitepress/config.ts +103 -103
  26. package/docs/api/cli.md +48 -48
  27. package/docs/api/oad-schema.md +64 -64
  28. package/docs/api/sdk.md +80 -80
  29. package/docs/guide/concepts.md +51 -51
  30. package/docs/guide/configuration.md +79 -79
  31. package/docs/guide/deployment.md +42 -42
  32. package/docs/guide/getting-started.md +44 -44
  33. package/docs/guide/templates.md +28 -28
  34. package/docs/guide/testing.md +84 -84
  35. package/docs/index.md +27 -27
  36. package/docs/zh/api/cli.md +54 -54
  37. package/docs/zh/api/oad-schema.md +87 -87
  38. package/docs/zh/api/sdk.md +102 -102
  39. package/docs/zh/guide/concepts.md +104 -104
  40. package/docs/zh/guide/configuration.md +135 -135
  41. package/docs/zh/guide/deployment.md +81 -81
  42. package/docs/zh/guide/getting-started.md +82 -82
  43. package/docs/zh/guide/templates.md +84 -84
  44. package/docs/zh/guide/testing.md +88 -88
  45. package/docs/zh/index.md +27 -27
  46. package/examples/customer-service-demo/README.md +90 -90
  47. package/examples/customer-service-demo/oad.yaml +107 -107
  48. package/package.json +50 -50
  49. package/src/analytics/index.ts +66 -66
  50. package/src/channels/discord.ts +192 -192
  51. package/src/channels/email.ts +177 -177
  52. package/src/channels/feishu.ts +236 -236
  53. package/src/channels/index.ts +15 -15
  54. package/src/channels/slack.ts +160 -160
  55. package/src/channels/telegram.ts +90 -90
  56. package/src/channels/voice.ts +106 -106
  57. package/src/channels/webhook.ts +199 -199
  58. package/src/channels/websocket.ts +87 -87
  59. package/src/channels/wechat.ts +149 -149
  60. package/src/core/a2a.ts +143 -143
  61. package/src/core/agent.ts +152 -152
  62. package/src/core/analytics-engine.ts +186 -186
  63. package/src/core/auth.ts +57 -57
  64. package/src/core/cache.ts +141 -141
  65. package/src/core/compose.ts +77 -77
  66. package/src/core/config.ts +14 -14
  67. package/src/core/errors.ts +148 -148
  68. package/src/core/hitl.ts +138 -138
  69. package/src/core/logger.ts +57 -57
  70. package/src/core/orchestrator.ts +215 -215
  71. package/src/core/performance.ts +187 -187
  72. package/src/core/rate-limiter.ts +128 -128
  73. package/src/core/room.ts +109 -109
  74. package/src/core/runtime.ts +152 -152
  75. package/src/core/sandbox.ts +101 -101
  76. package/src/core/security.ts +171 -171
  77. package/src/core/streaming.ts +195 -0
  78. package/src/core/types.ts +68 -68
  79. package/src/core/versioning.ts +106 -106
  80. package/src/core/watch.ts +178 -178
  81. package/src/core/workflow.ts +235 -235
  82. package/src/deploy/hermes.ts +156 -156
  83. package/src/deploy/openclaw.ts +200 -200
  84. package/src/dtv/data.ts +29 -29
  85. package/src/dtv/trust.ts +43 -43
  86. package/src/dtv/value.ts +47 -47
  87. package/src/i18n/index.ts +216 -216
  88. package/src/index.ts +6 -0
  89. package/src/marketplace/index.ts +223 -223
  90. package/src/memory/deepbrain.ts +108 -108
  91. package/src/memory/index.ts +34 -34
  92. package/src/plugins/index.ts +208 -208
  93. package/src/schema/oad.ts +155 -155
  94. package/src/skills/base.ts +16 -16
  95. package/src/skills/document.ts +100 -100
  96. package/src/skills/http.ts +35 -35
  97. package/src/skills/index.ts +27 -27
  98. package/src/skills/scheduler.ts +80 -80
  99. package/src/skills/webhook-trigger.ts +59 -59
  100. package/src/templates/code-reviewer.ts +34 -34
  101. package/src/templates/customer-service.ts +80 -80
  102. package/src/templates/data-analyst.ts +70 -70
  103. package/src/templates/executive-assistant.ts +71 -71
  104. package/src/templates/financial-advisor.ts +60 -60
  105. package/src/templates/knowledge-base.ts +31 -31
  106. package/src/templates/legal-assistant.ts +71 -71
  107. package/src/templates/sales-assistant.ts +79 -79
  108. package/src/templates/teacher.ts +79 -79
  109. package/src/testing/index.ts +181 -181
  110. package/src/tools/calculator.ts +73 -73
  111. package/src/tools/datetime.ts +149 -149
  112. package/src/tools/gateway.ts +220 -0
  113. package/src/tools/json-transform.ts +187 -187
  114. package/src/tools/mcp.ts +76 -76
  115. package/src/tools/text-analysis.ts +116 -116
  116. package/templates/Dockerfile +15 -15
  117. package/templates/code-reviewer/README.md +27 -27
  118. package/templates/code-reviewer/oad.yaml +41 -41
  119. package/templates/customer-service/README.md +22 -22
  120. package/templates/customer-service/oad.yaml +36 -36
  121. package/templates/docker-compose.yml +21 -21
  122. package/templates/ecommerce-assistant/README.md +45 -45
  123. package/templates/ecommerce-assistant/oad.yaml +47 -47
  124. package/templates/knowledge-base/README.md +28 -28
  125. package/templates/knowledge-base/oad.yaml +38 -38
  126. package/templates/sales-assistant/README.md +26 -26
  127. package/templates/sales-assistant/oad.yaml +43 -43
  128. package/templates/tech-support/README.md +43 -43
  129. package/templates/tech-support/oad.yaml +45 -45
  130. package/tests/a2a.test.ts +66 -66
  131. package/tests/agent.test.ts +72 -72
  132. package/tests/analytics.test.ts +50 -50
  133. package/tests/channel.test.ts +39 -39
  134. package/tests/e2e.test.ts +134 -134
  135. package/tests/errors.test.ts +83 -83
  136. package/tests/gateway.test.ts +71 -0
  137. package/tests/hitl.test.ts +71 -71
  138. package/tests/i18n.test.ts +41 -41
  139. package/tests/mcp.test.ts +54 -54
  140. package/tests/oad.test.ts +68 -68
  141. package/tests/performance.test.ts +115 -115
  142. package/tests/plugin.test.ts +74 -74
  143. package/tests/room.test.ts +106 -106
  144. package/tests/runtime.test.ts +42 -42
  145. package/tests/sandbox.test.ts +46 -46
  146. package/tests/security.test.ts +60 -60
  147. package/tests/streaming.test.ts +109 -0
  148. package/tests/templates.test.ts +77 -77
  149. package/tests/v070.test.ts +76 -76
  150. package/tests/versioning.test.ts +75 -75
  151. package/tests/voice.test.ts +61 -61
  152. package/tests/webhook.test.ts +29 -29
  153. package/tests/workflow.test.ts +143 -143
  154. package/tsconfig.json +19 -19
  155. package/vitest.config.ts +9 -9
@@ -1,186 +1,186 @@
1
- /**
2
- * Analytics Engine - Persistent analytics with JSON file storage.
3
- * Tracks every message, LLM call, tool use, and error with timestamps.
4
- */
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
-
8
- export interface AnalyticsEvent {
9
- type: 'message' | 'llm_call' | 'tool_use' | 'error';
10
- timestamp: number;
11
- data: Record<string, any>;
12
- }
13
-
14
- export interface AnalyticsStats {
15
- totalMessages: number;
16
- totalLLMCalls: number;
17
- totalToolUses: number;
18
- totalErrors: number;
19
- avgResponseTimeMs: number;
20
- totalTokens: { input: number; output: number; total: number };
21
- topSkills: { name: string; count: number }[];
22
- topErrors: { message: string; count: number }[];
23
- messagesPerDay: Record<string, number>;
24
- period: { from: number; to: number };
25
- }
26
-
27
- export class AnalyticsEngine {
28
- private dataDir: string;
29
- private eventsFile: string;
30
- private events: AnalyticsEvent[] = [];
31
-
32
- constructor(dataDir: string = '.') {
33
- this.dataDir = path.resolve(dataDir, 'data');
34
- this.eventsFile = path.join(this.dataDir, 'analytics.json');
35
- this.load();
36
- }
37
-
38
- private load(): void {
39
- try {
40
- if (fs.existsSync(this.eventsFile)) {
41
- const raw = fs.readFileSync(this.eventsFile, 'utf-8');
42
- this.events = JSON.parse(raw);
43
- }
44
- } catch {
45
- this.events = [];
46
- }
47
- }
48
-
49
- private save(): void {
50
- if (!fs.existsSync(this.dataDir)) {
51
- fs.mkdirSync(this.dataDir, { recursive: true });
52
- }
53
- // Keep last 10000 events to prevent unbounded growth
54
- if (this.events.length > 10000) {
55
- this.events = this.events.slice(-10000);
56
- }
57
- fs.writeFileSync(this.eventsFile, JSON.stringify(this.events, null, 2));
58
- }
59
-
60
- track(type: AnalyticsEvent['type'], data: Record<string, any>): void {
61
- this.events.push({ type, timestamp: Date.now(), data });
62
- this.save();
63
- }
64
-
65
- trackMessage(userId: string, responseTimeMs: number, tokensIn: number, tokensOut: number): void {
66
- this.track('message', { userId, responseTimeMs, tokensIn, tokensOut });
67
- }
68
-
69
- trackLLMCall(provider: string, model: string, tokensIn: number, tokensOut: number, latencyMs: number): void {
70
- this.track('llm_call', { provider, model, tokensIn, tokensOut, latencyMs });
71
- }
72
-
73
- trackToolUse(toolName: string, success: boolean, latencyMs: number): void {
74
- this.track('tool_use', { toolName, success, latencyMs });
75
- }
76
-
77
- trackError(error: string, context?: string): void {
78
- this.track('error', { error, context });
79
- }
80
-
81
- getStats(fromTs?: number, toTs?: number): AnalyticsStats {
82
- const now = Date.now();
83
- const from = fromTs ?? 0;
84
- const to = toTs ?? now;
85
- const filtered = this.events.filter(e => e.timestamp >= from && e.timestamp <= to);
86
-
87
- const messages = filtered.filter(e => e.type === 'message');
88
- const llmCalls = filtered.filter(e => e.type === 'llm_call');
89
- const toolUses = filtered.filter(e => e.type === 'tool_use');
90
- const errors = filtered.filter(e => e.type === 'error');
91
-
92
- // Avg response time
93
- const totalResponseTime = messages.reduce((sum, e) => sum + (e.data.responseTimeMs ?? 0), 0);
94
- const avgResponseTimeMs = messages.length > 0 ? Math.round(totalResponseTime / messages.length) : 0;
95
-
96
- // Total tokens
97
- const totalTokensIn = llmCalls.reduce((sum, e) => sum + (e.data.tokensIn ?? 0), 0);
98
- const totalTokensOut = llmCalls.reduce((sum, e) => sum + (e.data.tokensOut ?? 0), 0);
99
-
100
- // Top skills (from tool_use)
101
- const skillCounts: Record<string, number> = {};
102
- for (const e of toolUses) {
103
- const name = e.data.toolName ?? 'unknown';
104
- skillCounts[name] = (skillCounts[name] ?? 0) + 1;
105
- }
106
- const topSkills = Object.entries(skillCounts)
107
- .sort((a, b) => b[1] - a[1])
108
- .slice(0, 10)
109
- .map(([name, count]) => ({ name, count }));
110
-
111
- // Top errors
112
- const errorCounts: Record<string, number> = {};
113
- for (const e of errors) {
114
- const msg = e.data.error ?? 'unknown';
115
- errorCounts[msg] = (errorCounts[msg] ?? 0) + 1;
116
- }
117
- const topErrors = Object.entries(errorCounts)
118
- .sort((a, b) => b[1] - a[1])
119
- .slice(0, 10)
120
- .map(([message, count]) => ({ message, count }));
121
-
122
- // Messages per day
123
- const messagesPerDay: Record<string, number> = {};
124
- for (const e of messages) {
125
- const day = new Date(e.timestamp).toISOString().slice(0, 10);
126
- messagesPerDay[day] = (messagesPerDay[day] ?? 0) + 1;
127
- }
128
-
129
- return {
130
- totalMessages: messages.length,
131
- totalLLMCalls: llmCalls.length,
132
- totalToolUses: toolUses.length,
133
- totalErrors: errors.length,
134
- avgResponseTimeMs,
135
- totalTokens: { input: totalTokensIn, output: totalTokensOut, total: totalTokensIn + totalTokensOut },
136
- topSkills,
137
- topErrors,
138
- messagesPerDay,
139
- period: { from, to },
140
- };
141
- }
142
-
143
- getRecentEvents(limit: number = 50): AnalyticsEvent[] {
144
- return this.events.slice(-limit);
145
- }
146
-
147
- clear(): void {
148
- this.events = [];
149
- this.save();
150
- }
151
-
152
- /**
153
- * Format stats for CLI display.
154
- */
155
- static formatStats(stats: AnalyticsStats): string {
156
- const lines: string[] = [];
157
- lines.push('');
158
- lines.push('══════════════════════════════════════════');
159
- lines.push(' OPC Agent Analytics');
160
- lines.push('══════════════════════════════════════════');
161
- lines.push('');
162
- lines.push(` 📨 Messages: ${stats.totalMessages}`);
163
- lines.push(` 🤖 LLM Calls: ${stats.totalLLMCalls}`);
164
- lines.push(` 🔧 Tool Uses: ${stats.totalToolUses}`);
165
- lines.push(` ❌ Errors: ${stats.totalErrors}`);
166
- lines.push(` ⏱ Avg Response: ${stats.avgResponseTimeMs}ms`);
167
- lines.push(` 🪙 Tokens: ${stats.totalTokens.total} (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`);
168
- lines.push('');
169
- if (stats.topSkills.length > 0) {
170
- lines.push(' Top Skills:');
171
- for (const s of stats.topSkills.slice(0, 5)) {
172
- lines.push(` • ${s.name}: ${s.count}`);
173
- }
174
- lines.push('');
175
- }
176
- if (stats.topErrors.length > 0) {
177
- lines.push(' Top Errors:');
178
- for (const e of stats.topErrors.slice(0, 3)) {
179
- lines.push(` • ${e.message}: ${e.count}`);
180
- }
181
- lines.push('');
182
- }
183
- lines.push('──────────────────────────────────────────');
184
- return lines.join('\n');
185
- }
186
- }
1
+ /**
2
+ * Analytics Engine - Persistent analytics with JSON file storage.
3
+ * Tracks every message, LLM call, tool use, and error with timestamps.
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+
8
+ export interface AnalyticsEvent {
9
+ type: 'message' | 'llm_call' | 'tool_use' | 'error';
10
+ timestamp: number;
11
+ data: Record<string, any>;
12
+ }
13
+
14
+ export interface AnalyticsStats {
15
+ totalMessages: number;
16
+ totalLLMCalls: number;
17
+ totalToolUses: number;
18
+ totalErrors: number;
19
+ avgResponseTimeMs: number;
20
+ totalTokens: { input: number; output: number; total: number };
21
+ topSkills: { name: string; count: number }[];
22
+ topErrors: { message: string; count: number }[];
23
+ messagesPerDay: Record<string, number>;
24
+ period: { from: number; to: number };
25
+ }
26
+
27
+ export class AnalyticsEngine {
28
+ private dataDir: string;
29
+ private eventsFile: string;
30
+ private events: AnalyticsEvent[] = [];
31
+
32
+ constructor(dataDir: string = '.') {
33
+ this.dataDir = path.resolve(dataDir, 'data');
34
+ this.eventsFile = path.join(this.dataDir, 'analytics.json');
35
+ this.load();
36
+ }
37
+
38
+ private load(): void {
39
+ try {
40
+ if (fs.existsSync(this.eventsFile)) {
41
+ const raw = fs.readFileSync(this.eventsFile, 'utf-8');
42
+ this.events = JSON.parse(raw);
43
+ }
44
+ } catch {
45
+ this.events = [];
46
+ }
47
+ }
48
+
49
+ private save(): void {
50
+ if (!fs.existsSync(this.dataDir)) {
51
+ fs.mkdirSync(this.dataDir, { recursive: true });
52
+ }
53
+ // Keep last 10000 events to prevent unbounded growth
54
+ if (this.events.length > 10000) {
55
+ this.events = this.events.slice(-10000);
56
+ }
57
+ fs.writeFileSync(this.eventsFile, JSON.stringify(this.events, null, 2));
58
+ }
59
+
60
+ track(type: AnalyticsEvent['type'], data: Record<string, any>): void {
61
+ this.events.push({ type, timestamp: Date.now(), data });
62
+ this.save();
63
+ }
64
+
65
+ trackMessage(userId: string, responseTimeMs: number, tokensIn: number, tokensOut: number): void {
66
+ this.track('message', { userId, responseTimeMs, tokensIn, tokensOut });
67
+ }
68
+
69
+ trackLLMCall(provider: string, model: string, tokensIn: number, tokensOut: number, latencyMs: number): void {
70
+ this.track('llm_call', { provider, model, tokensIn, tokensOut, latencyMs });
71
+ }
72
+
73
+ trackToolUse(toolName: string, success: boolean, latencyMs: number): void {
74
+ this.track('tool_use', { toolName, success, latencyMs });
75
+ }
76
+
77
+ trackError(error: string, context?: string): void {
78
+ this.track('error', { error, context });
79
+ }
80
+
81
+ getStats(fromTs?: number, toTs?: number): AnalyticsStats {
82
+ const now = Date.now();
83
+ const from = fromTs ?? 0;
84
+ const to = toTs ?? now;
85
+ const filtered = this.events.filter(e => e.timestamp >= from && e.timestamp <= to);
86
+
87
+ const messages = filtered.filter(e => e.type === 'message');
88
+ const llmCalls = filtered.filter(e => e.type === 'llm_call');
89
+ const toolUses = filtered.filter(e => e.type === 'tool_use');
90
+ const errors = filtered.filter(e => e.type === 'error');
91
+
92
+ // Avg response time
93
+ const totalResponseTime = messages.reduce((sum, e) => sum + (e.data.responseTimeMs ?? 0), 0);
94
+ const avgResponseTimeMs = messages.length > 0 ? Math.round(totalResponseTime / messages.length) : 0;
95
+
96
+ // Total tokens
97
+ const totalTokensIn = llmCalls.reduce((sum, e) => sum + (e.data.tokensIn ?? 0), 0);
98
+ const totalTokensOut = llmCalls.reduce((sum, e) => sum + (e.data.tokensOut ?? 0), 0);
99
+
100
+ // Top skills (from tool_use)
101
+ const skillCounts: Record<string, number> = {};
102
+ for (const e of toolUses) {
103
+ const name = e.data.toolName ?? 'unknown';
104
+ skillCounts[name] = (skillCounts[name] ?? 0) + 1;
105
+ }
106
+ const topSkills = Object.entries(skillCounts)
107
+ .sort((a, b) => b[1] - a[1])
108
+ .slice(0, 10)
109
+ .map(([name, count]) => ({ name, count }));
110
+
111
+ // Top errors
112
+ const errorCounts: Record<string, number> = {};
113
+ for (const e of errors) {
114
+ const msg = e.data.error ?? 'unknown';
115
+ errorCounts[msg] = (errorCounts[msg] ?? 0) + 1;
116
+ }
117
+ const topErrors = Object.entries(errorCounts)
118
+ .sort((a, b) => b[1] - a[1])
119
+ .slice(0, 10)
120
+ .map(([message, count]) => ({ message, count }));
121
+
122
+ // Messages per day
123
+ const messagesPerDay: Record<string, number> = {};
124
+ for (const e of messages) {
125
+ const day = new Date(e.timestamp).toISOString().slice(0, 10);
126
+ messagesPerDay[day] = (messagesPerDay[day] ?? 0) + 1;
127
+ }
128
+
129
+ return {
130
+ totalMessages: messages.length,
131
+ totalLLMCalls: llmCalls.length,
132
+ totalToolUses: toolUses.length,
133
+ totalErrors: errors.length,
134
+ avgResponseTimeMs,
135
+ totalTokens: { input: totalTokensIn, output: totalTokensOut, total: totalTokensIn + totalTokensOut },
136
+ topSkills,
137
+ topErrors,
138
+ messagesPerDay,
139
+ period: { from, to },
140
+ };
141
+ }
142
+
143
+ getRecentEvents(limit: number = 50): AnalyticsEvent[] {
144
+ return this.events.slice(-limit);
145
+ }
146
+
147
+ clear(): void {
148
+ this.events = [];
149
+ this.save();
150
+ }
151
+
152
+ /**
153
+ * Format stats for CLI display.
154
+ */
155
+ static formatStats(stats: AnalyticsStats): string {
156
+ const lines: string[] = [];
157
+ lines.push('');
158
+ lines.push('══════════════════════════════════════════');
159
+ lines.push(' OPC Agent Analytics');
160
+ lines.push('══════════════════════════════════════════');
161
+ lines.push('');
162
+ lines.push(` 📨 Messages: ${stats.totalMessages}`);
163
+ lines.push(` 🤖 LLM Calls: ${stats.totalLLMCalls}`);
164
+ lines.push(` 🔧 Tool Uses: ${stats.totalToolUses}`);
165
+ lines.push(` ❌ Errors: ${stats.totalErrors}`);
166
+ lines.push(` ⏱ Avg Response: ${stats.avgResponseTimeMs}ms`);
167
+ lines.push(` 🪙 Tokens: ${stats.totalTokens.total} (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`);
168
+ lines.push('');
169
+ if (stats.topSkills.length > 0) {
170
+ lines.push(' Top Skills:');
171
+ for (const s of stats.topSkills.slice(0, 5)) {
172
+ lines.push(` • ${s.name}: ${s.count}`);
173
+ }
174
+ lines.push('');
175
+ }
176
+ if (stats.topErrors.length > 0) {
177
+ lines.push(' Top Errors:');
178
+ for (const e of stats.topErrors.slice(0, 3)) {
179
+ lines.push(` • ${e.message}: ${e.count}`);
180
+ }
181
+ lines.push('');
182
+ }
183
+ lines.push('──────────────────────────────────────────');
184
+ return lines.join('\n');
185
+ }
186
+ }
package/src/core/auth.ts CHANGED
@@ -1,57 +1,57 @@
1
- import type { Request, Response, NextFunction } from 'express';
2
-
3
- export interface AuthConfig {
4
- apiKeys: string[];
5
- sessionIsolation?: boolean;
6
- }
7
-
8
- export interface AuthSession {
9
- apiKey: string;
10
- userId: string;
11
- createdAt: number;
12
- }
13
-
14
- const sessions = new Map<string, AuthSession>();
15
-
16
- export function createAuthMiddleware(config: AuthConfig) {
17
- return (req: Request, res: Response, next: NextFunction): void => {
18
- // Skip auth for non-API routes and health/metrics
19
- if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
20
- next();
21
- return;
22
- }
23
-
24
- const apiKey = req.headers['x-api-key'] as string
25
- ?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
26
- ?? (req.query as any).apiKey;
27
-
28
- if (!apiKey || !config.apiKeys.includes(apiKey)) {
29
- res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
30
- return;
31
- }
32
-
33
- // Derive userId from API key for session isolation
34
- const userId = `user_${hashKey(apiKey)}`;
35
- if (!sessions.has(apiKey)) {
36
- sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
37
- }
38
-
39
- // Attach user info to request
40
- (req as any).userId = userId;
41
- (req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
42
-
43
- next();
44
- };
45
- }
46
-
47
- function hashKey(key: string): string {
48
- let h = 0;
49
- for (let i = 0; i < key.length; i++) {
50
- h = ((h << 5) - h + key.charCodeAt(i)) | 0;
51
- }
52
- return Math.abs(h).toString(36);
53
- }
54
-
55
- export function getActiveSessions(): AuthSession[] {
56
- return Array.from(sessions.values());
57
- }
1
+ import type { Request, Response, NextFunction } from 'express';
2
+
3
+ export interface AuthConfig {
4
+ apiKeys: string[];
5
+ sessionIsolation?: boolean;
6
+ }
7
+
8
+ export interface AuthSession {
9
+ apiKey: string;
10
+ userId: string;
11
+ createdAt: number;
12
+ }
13
+
14
+ const sessions = new Map<string, AuthSession>();
15
+
16
+ export function createAuthMiddleware(config: AuthConfig) {
17
+ return (req: Request, res: Response, next: NextFunction): void => {
18
+ // Skip auth for non-API routes and health/metrics
19
+ if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
20
+ next();
21
+ return;
22
+ }
23
+
24
+ const apiKey = req.headers['x-api-key'] as string
25
+ ?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
26
+ ?? (req.query as any).apiKey;
27
+
28
+ if (!apiKey || !config.apiKeys.includes(apiKey)) {
29
+ res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
30
+ return;
31
+ }
32
+
33
+ // Derive userId from API key for session isolation
34
+ const userId = `user_${hashKey(apiKey)}`;
35
+ if (!sessions.has(apiKey)) {
36
+ sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
37
+ }
38
+
39
+ // Attach user info to request
40
+ (req as any).userId = userId;
41
+ (req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
42
+
43
+ next();
44
+ };
45
+ }
46
+
47
+ function hashKey(key: string): string {
48
+ let h = 0;
49
+ for (let i = 0; i < key.length; i++) {
50
+ h = ((h << 5) - h + key.charCodeAt(i)) | 0;
51
+ }
52
+ return Math.abs(h).toString(36);
53
+ }
54
+
55
+ export function getActiveSessions(): AuthSession[] {
56
+ return Array.from(sessions.values());
57
+ }