opc-agent 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CONTRIBUTING.md +75 -75
  2. package/README.md +235 -358
  3. package/README.zh-CN.md +415 -415
  4. package/dist/channels/web.js +256 -256
  5. package/dist/core/knowledge.d.ts +5 -0
  6. package/dist/core/knowledge.js +39 -2
  7. package/dist/deploy/hermes.js +22 -22
  8. package/dist/deploy/openclaw.js +31 -31
  9. package/dist/index.d.ts +0 -4
  10. package/dist/index.js +1 -7
  11. package/dist/providers/index.d.ts +1 -1
  12. package/dist/providers/index.js +158 -14
  13. package/dist/schema/oad.d.ts +3 -3
  14. package/dist/templates/code-reviewer.js +5 -5
  15. package/dist/templates/customer-service.js +2 -2
  16. package/dist/templates/data-analyst.js +5 -5
  17. package/dist/templates/knowledge-base.js +2 -2
  18. package/dist/templates/sales-assistant.js +4 -4
  19. package/dist/templates/teacher.js +6 -6
  20. package/docs/.vitepress/config.ts +103 -103
  21. package/docs/api/cli.md +48 -48
  22. package/docs/api/oad-schema.md +64 -64
  23. package/docs/api/sdk.md +80 -80
  24. package/docs/guide/concepts.md +51 -51
  25. package/docs/guide/configuration.md +79 -79
  26. package/docs/guide/deployment.md +42 -42
  27. package/docs/guide/getting-started.md +44 -44
  28. package/docs/guide/templates.md +28 -28
  29. package/docs/guide/testing.md +84 -84
  30. package/docs/index.md +27 -27
  31. package/docs/zh/api/cli.md +54 -54
  32. package/docs/zh/api/oad-schema.md +87 -87
  33. package/docs/zh/api/sdk.md +102 -102
  34. package/docs/zh/guide/concepts.md +104 -104
  35. package/docs/zh/guide/configuration.md +135 -135
  36. package/docs/zh/guide/deployment.md +81 -81
  37. package/docs/zh/guide/getting-started.md +82 -82
  38. package/docs/zh/guide/templates.md +84 -84
  39. package/docs/zh/guide/testing.md +88 -88
  40. package/docs/zh/index.md +27 -27
  41. package/examples/customer-service-demo/README.md +90 -90
  42. package/examples/customer-service-demo/oad.yaml +107 -107
  43. package/package.json +50 -50
  44. package/src/analytics/index.ts +66 -66
  45. package/src/channels/discord.ts +192 -192
  46. package/src/channels/email.ts +177 -177
  47. package/src/channels/feishu.ts +236 -236
  48. package/src/channels/index.ts +15 -15
  49. package/src/channels/slack.ts +160 -160
  50. package/src/channels/telegram.ts +90 -90
  51. package/src/channels/voice.ts +106 -106
  52. package/src/channels/webhook.ts +199 -199
  53. package/src/channels/websocket.ts +87 -87
  54. package/src/channels/wechat.ts +149 -149
  55. package/src/cli.ts +119 -1
  56. package/src/core/a2a.ts +143 -143
  57. package/src/core/agent.ts +152 -152
  58. package/src/core/analytics-engine.ts +186 -186
  59. package/src/core/auth.ts +57 -57
  60. package/src/core/cache.ts +141 -141
  61. package/src/core/compose.ts +77 -77
  62. package/src/core/config.ts +14 -14
  63. package/src/core/errors.ts +148 -148
  64. package/src/core/hitl.ts +138 -138
  65. package/src/core/logger.ts +57 -57
  66. package/src/core/orchestrator.ts +215 -215
  67. package/src/core/performance.ts +187 -187
  68. package/src/core/rate-limiter.ts +128 -128
  69. package/src/core/room.ts +109 -109
  70. package/src/core/runtime.ts +152 -152
  71. package/src/core/sandbox.ts +101 -101
  72. package/src/core/security.ts +171 -171
  73. package/src/core/types.ts +68 -68
  74. package/src/core/versioning.ts +106 -106
  75. package/src/core/watch.ts +178 -178
  76. package/src/core/workflow.ts +235 -235
  77. package/src/deploy/hermes.ts +156 -156
  78. package/src/deploy/openclaw.ts +200 -200
  79. package/src/i18n/index.ts +216 -216
  80. package/src/index.ts +6 -2
  81. package/src/memory/deepbrain.ts +108 -108
  82. package/src/memory/index.ts +34 -34
  83. package/src/plugins/index.ts +208 -208
  84. package/src/schema/oad.ts +154 -155
  85. package/src/skills/base.ts +16 -16
  86. package/src/skills/document.ts +100 -100
  87. package/src/skills/http.ts +35 -35
  88. package/src/skills/index.ts +27 -27
  89. package/src/skills/scheduler.ts +80 -80
  90. package/src/skills/webhook-trigger.ts +59 -59
  91. package/src/templates/code-reviewer.ts +30 -34
  92. package/src/templates/customer-service.ts +76 -80
  93. package/src/templates/data-analyst.ts +66 -70
  94. package/src/templates/executive-assistant.ts +71 -71
  95. package/src/templates/financial-advisor.ts +60 -60
  96. package/src/templates/knowledge-base.ts +27 -31
  97. package/src/templates/legal-assistant.ts +71 -71
  98. package/src/templates/sales-assistant.ts +75 -79
  99. package/src/templates/teacher.ts +75 -79
  100. package/src/testing/index.ts +181 -181
  101. package/src/tools/calculator.ts +73 -73
  102. package/src/tools/datetime.ts +149 -149
  103. package/src/tools/json-transform.ts +187 -187
  104. package/src/tools/mcp.ts +76 -76
  105. package/src/tools/text-analysis.ts +116 -116
  106. package/src/traces/index.ts +132 -0
  107. package/templates/Dockerfile +15 -15
  108. package/templates/code-reviewer/README.md +27 -27
  109. package/templates/code-reviewer/oad.yaml +41 -41
  110. package/templates/customer-service/README.md +22 -22
  111. package/templates/customer-service/oad.yaml +36 -36
  112. package/templates/docker-compose.yml +21 -21
  113. package/templates/ecommerce-assistant/README.md +45 -45
  114. package/templates/ecommerce-assistant/oad.yaml +47 -47
  115. package/templates/knowledge-base/README.md +28 -28
  116. package/templates/knowledge-base/oad.yaml +38 -38
  117. package/templates/sales-assistant/README.md +26 -26
  118. package/templates/sales-assistant/oad.yaml +43 -43
  119. package/templates/tech-support/README.md +43 -43
  120. package/templates/tech-support/oad.yaml +45 -45
  121. package/tests/a2a.test.ts +66 -66
  122. package/tests/agent.test.ts +72 -72
  123. package/tests/analytics.test.ts +50 -50
  124. package/tests/channel.test.ts +39 -39
  125. package/tests/e2e.test.ts +134 -134
  126. package/tests/errors.test.ts +83 -83
  127. package/tests/hitl.test.ts +71 -71
  128. package/tests/i18n.test.ts +41 -41
  129. package/tests/mcp.test.ts +54 -54
  130. package/tests/oad.test.ts +68 -68
  131. package/tests/performance.test.ts +115 -115
  132. package/tests/plugin.test.ts +74 -74
  133. package/tests/room.test.ts +106 -106
  134. package/tests/runtime.test.ts +42 -42
  135. package/tests/sandbox.test.ts +46 -46
  136. package/tests/security.test.ts +60 -60
  137. package/tests/templates.test.ts +77 -77
  138. package/tests/v070.test.ts +76 -76
  139. package/tests/versioning.test.ts +75 -75
  140. package/tests/voice.test.ts +61 -61
  141. package/tests/webhook.test.ts +29 -29
  142. package/tests/workflow.test.ts +143 -143
  143. package/tsconfig.json +19 -19
  144. package/vitest.config.ts +9 -9
  145. package/dist/core/streaming.d.ts +0 -56
  146. package/dist/core/streaming.js +0 -160
  147. package/dist/tools/gateway.d.ts +0 -28
  148. package/dist/tools/gateway.js +0 -177
  149. package/src/dtv/data.ts +0 -29
  150. package/src/dtv/trust.ts +0 -43
  151. package/src/dtv/value.ts +0 -47
  152. package/src/marketplace/index.ts +0 -223
@@ -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
+ }