opc-agent 1.3.1 → 1.3.2

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