responses-proxy 0.1.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 (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,318 @@
1
+ import { createHash, randomBytes, randomInt } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import BetterSqlite3 from "better-sqlite3";
5
+ export class DashboardAuthRepository {
6
+ db;
7
+ constructor(db) {
8
+ this.db = db;
9
+ }
10
+ static create(dbFile) {
11
+ mkdirSync(path.dirname(dbFile), { recursive: true });
12
+ const db = new BetterSqlite3(dbFile);
13
+ ensureDashboardAuthSchema(db);
14
+ return new DashboardAuthRepository(db);
15
+ }
16
+ createChallenge(input) {
17
+ const now = input.now ?? new Date();
18
+ const createdAt = now.toISOString();
19
+ const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
20
+ const id = randomBytes(16).toString("hex");
21
+ const otp = randomInt(0, 1_000_000).toString().padStart(6, "0");
22
+ this.db
23
+ .prepare(`INSERT INTO dashboard_auth_challenges (
24
+ id,
25
+ telegram_user_id,
26
+ otp_hash,
27
+ expires_at,
28
+ created_at
29
+ ) VALUES (?, ?, ?, ?, ?)`)
30
+ .run(id, input.telegramUserId, hashOtp(id, otp), expiresAt, createdAt);
31
+ return { id, telegramUserId: input.telegramUserId, otp, expiresAt };
32
+ }
33
+ createApprovalChallenge(input) {
34
+ const now = input.now ?? new Date();
35
+ const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
36
+ const id = randomBytes(16).toString("hex");
37
+ const displayCode = randomInt(10, 100).toString().padStart(2, "0");
38
+ const pollToken = randomBytes(24).toString("base64url");
39
+ const telegramUserIds = Array.from(new Set(input.telegramUserIds.filter(Boolean)));
40
+ if (telegramUserIds.length === 0) {
41
+ throw new Error("Cannot create dashboard approval challenge without Telegram admins.");
42
+ }
43
+ const insert = this.db.prepare(`INSERT INTO dashboard_auth_challenges (
44
+ id,
45
+ challenge_group_id,
46
+ telegram_user_id,
47
+ otp_hash,
48
+ poll_token_hash,
49
+ expires_at,
50
+ created_at
51
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
52
+ const createdAt = now.toISOString();
53
+ for (const telegramUserId of telegramUserIds) {
54
+ const rowId = randomBytes(16).toString("hex");
55
+ insert.run(rowId, id, telegramUserId, hashOtp(rowId, displayCode), hashPollToken(pollToken), expiresAt, createdAt);
56
+ }
57
+ return { id, displayCode, pollToken, expiresAt };
58
+ }
59
+ getApprovalChallengeStatus(input) {
60
+ const now = input.now ?? new Date();
61
+ const row = this.getApprovalChallengeRow(input.challengeId, input.pollToken);
62
+ if (!row) {
63
+ return { ok: false, reason: "invalid" };
64
+ }
65
+ if (row.approved_at) {
66
+ return {
67
+ ok: true,
68
+ status: "approved",
69
+ challengeId: input.challengeId,
70
+ expiresAt: row.expires_at,
71
+ telegramUserId: row.approved_by_telegram_user_id ?? row.telegram_user_id,
72
+ };
73
+ }
74
+ if (row.rejected_at) {
75
+ return { ok: true, status: "rejected", challengeId: input.challengeId, expiresAt: row.expires_at };
76
+ }
77
+ if (new Date(row.expires_at).getTime() < now.getTime()) {
78
+ return { ok: true, status: "expired", challengeId: input.challengeId, expiresAt: row.expires_at };
79
+ }
80
+ if (row.consumed_at) {
81
+ return { ok: true, status: "consumed", challengeId: input.challengeId, expiresAt: row.expires_at };
82
+ }
83
+ return { ok: true, status: "pending", challengeId: input.challengeId, expiresAt: row.expires_at };
84
+ }
85
+ resolveApprovalChoice(input) {
86
+ const now = input.now ?? new Date();
87
+ const row = this.getAdminChallengeRow(input.challengeId, input.telegramUserId);
88
+ if (!row) {
89
+ return { ok: false, reason: "invalid" };
90
+ }
91
+ if (row.consumed_at) {
92
+ return { ok: false, reason: "consumed" };
93
+ }
94
+ if (new Date(row.expires_at).getTime() < now.getTime()) {
95
+ return { ok: false, reason: "expired" };
96
+ }
97
+ if (row.approved_at || row.rejected_at) {
98
+ return { ok: false, reason: "consumed" };
99
+ }
100
+ if (hashOtp(row.id, input.selectedCode) === row.otp_hash) {
101
+ this.db
102
+ .prepare(`UPDATE dashboard_auth_challenges
103
+ SET approved_at = ?, approved_by_telegram_user_id = ?
104
+ WHERE challenge_group_id = ?`)
105
+ .run(now.toISOString(), input.telegramUserId, input.challengeId);
106
+ return { ok: true, status: "approved" };
107
+ }
108
+ this.db
109
+ .prepare(`UPDATE dashboard_auth_challenges
110
+ SET rejected_at = ?, rejected_by_telegram_user_id = ?
111
+ WHERE challenge_group_id = ?`)
112
+ .run(now.toISOString(), input.telegramUserId, input.challengeId);
113
+ return { ok: true, status: "rejected" };
114
+ }
115
+ consumeApprovedChallenge(input) {
116
+ const now = input.now ?? new Date();
117
+ const row = this.getApprovalChallengeRow(input.challengeId, input.pollToken);
118
+ if (!row) {
119
+ return { ok: false, reason: "invalid" };
120
+ }
121
+ if (row.rejected_at) {
122
+ return { ok: false, reason: "rejected" };
123
+ }
124
+ if (new Date(row.expires_at).getTime() < now.getTime()) {
125
+ return { ok: false, reason: "expired" };
126
+ }
127
+ if (!row.approved_at) {
128
+ return { ok: false, reason: "pending" };
129
+ }
130
+ if (row.consumed_at) {
131
+ return { ok: false, reason: "consumed" };
132
+ }
133
+ this.db
134
+ .prepare(`UPDATE dashboard_auth_challenges
135
+ SET consumed_at = ?
136
+ WHERE challenge_group_id = ?`)
137
+ .run(now.toISOString(), input.challengeId);
138
+ return {
139
+ ok: true,
140
+ telegramUserId: row.approved_by_telegram_user_id ?? row.telegram_user_id,
141
+ expiresAt: row.expires_at,
142
+ };
143
+ }
144
+ consumeChallenge(input) {
145
+ return this.consumeChallengeForUsers({ telegramUserIds: [input.telegramUserId], otp: input.otp, now: input.now });
146
+ }
147
+ consumeChallengeForUsers(input) {
148
+ const now = input.now ?? new Date();
149
+ const userIds = Array.from(new Set(input.telegramUserIds.filter(Boolean)));
150
+ if (userIds.length === 0) {
151
+ return { ok: false, reason: "invalid" };
152
+ }
153
+ const placeholders = userIds.map(() => "?").join(", ");
154
+ const rows = this.db
155
+ .prepare(`SELECT * FROM dashboard_auth_challenges
156
+ WHERE telegram_user_id IN (${placeholders})
157
+ AND consumed_at IS NULL
158
+ ORDER BY created_at DESC
159
+ LIMIT 20`)
160
+ .all(...userIds);
161
+ for (const row of rows) {
162
+ if (hashOtp(row.id, input.otp) !== row.otp_hash) {
163
+ continue;
164
+ }
165
+ if (new Date(row.expires_at).getTime() < now.getTime()) {
166
+ return { ok: false, reason: "expired" };
167
+ }
168
+ this.db
169
+ .prepare(`UPDATE dashboard_auth_challenges
170
+ SET consumed_at = ?
171
+ WHERE id = ?`)
172
+ .run(now.toISOString(), row.id);
173
+ return { ok: true, challengeId: row.id, telegramUserId: row.telegram_user_id };
174
+ }
175
+ return { ok: false, reason: "invalid" };
176
+ }
177
+ createSession(input) {
178
+ const now = input.now ?? new Date();
179
+ const timestamp = now.toISOString();
180
+ const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
181
+ const id = randomBytes(16).toString("hex");
182
+ const token = randomBytes(32).toString("base64url");
183
+ this.db
184
+ .prepare(`INSERT INTO dashboard_auth_sessions (
185
+ id,
186
+ session_hash,
187
+ telegram_user_id,
188
+ role,
189
+ expires_at,
190
+ created_at,
191
+ updated_at
192
+ ) VALUES (?, ?, ?, 'admin', ?, ?, ?)`)
193
+ .run(id, hashSessionToken(token), input.telegramUserId, expiresAt, timestamp, timestamp);
194
+ return {
195
+ token,
196
+ session: {
197
+ id,
198
+ telegramUserId: input.telegramUserId,
199
+ role: "admin",
200
+ expiresAt,
201
+ },
202
+ };
203
+ }
204
+ getSessionByToken(token, now = new Date()) {
205
+ if (!token) {
206
+ return undefined;
207
+ }
208
+ const row = this.db
209
+ .prepare(`SELECT * FROM dashboard_auth_sessions
210
+ WHERE session_hash = ?
211
+ AND revoked_at IS NULL
212
+ LIMIT 1`)
213
+ .get(hashSessionToken(token));
214
+ if (!row || new Date(row.expires_at).getTime() < now.getTime()) {
215
+ return undefined;
216
+ }
217
+ return {
218
+ id: row.id,
219
+ telegramUserId: row.telegram_user_id,
220
+ role: "admin",
221
+ expiresAt: row.expires_at,
222
+ };
223
+ }
224
+ revokeSessionByToken(token, now = new Date()) {
225
+ if (!token) {
226
+ return false;
227
+ }
228
+ const timestamp = now.toISOString();
229
+ const result = this.db
230
+ .prepare(`UPDATE dashboard_auth_sessions
231
+ SET revoked_at = ?, updated_at = ?
232
+ WHERE session_hash = ?
233
+ AND revoked_at IS NULL`)
234
+ .run(timestamp, timestamp, hashSessionToken(token));
235
+ return result.changes > 0;
236
+ }
237
+ getApprovalChallengeRow(challengeId, pollToken) {
238
+ return this.db
239
+ .prepare(`SELECT * FROM dashboard_auth_challenges
240
+ WHERE challenge_group_id = ?
241
+ AND poll_token_hash = ?
242
+ ORDER BY created_at ASC
243
+ LIMIT 1`)
244
+ .get(challengeId, hashPollToken(pollToken));
245
+ }
246
+ getAdminChallengeRow(challengeId, telegramUserId) {
247
+ return this.db
248
+ .prepare(`SELECT * FROM dashboard_auth_challenges
249
+ WHERE challenge_group_id = ?
250
+ AND telegram_user_id = ?
251
+ ORDER BY created_at ASC
252
+ LIMIT 1`)
253
+ .get(challengeId, telegramUserId);
254
+ }
255
+ }
256
+ function ensureDashboardAuthSchema(db) {
257
+ db.exec(`
258
+ CREATE TABLE IF NOT EXISTS dashboard_auth_challenges (
259
+ id TEXT PRIMARY KEY,
260
+ challenge_group_id TEXT,
261
+ telegram_user_id TEXT NOT NULL,
262
+ otp_hash TEXT NOT NULL,
263
+ poll_token_hash TEXT,
264
+ expires_at TEXT NOT NULL,
265
+ consumed_at TEXT,
266
+ approved_at TEXT,
267
+ approved_by_telegram_user_id TEXT,
268
+ rejected_at TEXT,
269
+ rejected_by_telegram_user_id TEXT,
270
+ created_at TEXT NOT NULL
271
+ );
272
+
273
+ CREATE INDEX IF NOT EXISTS idx_dashboard_auth_challenges_user
274
+ ON dashboard_auth_challenges(telegram_user_id, consumed_at, created_at);
275
+
276
+ CREATE TABLE IF NOT EXISTS dashboard_auth_sessions (
277
+ id TEXT PRIMARY KEY,
278
+ session_hash TEXT NOT NULL UNIQUE,
279
+ telegram_user_id TEXT NOT NULL,
280
+ role TEXT NOT NULL,
281
+ expires_at TEXT NOT NULL,
282
+ revoked_at TEXT,
283
+ created_at TEXT NOT NULL,
284
+ updated_at TEXT NOT NULL
285
+ );
286
+
287
+ CREATE INDEX IF NOT EXISTS idx_dashboard_auth_sessions_hash
288
+ ON dashboard_auth_sessions(session_hash, revoked_at, expires_at);
289
+ `);
290
+ ensureColumn(db, "dashboard_auth_challenges", "challenge_group_id", "TEXT");
291
+ ensureColumn(db, "dashboard_auth_challenges", "poll_token_hash", "TEXT");
292
+ ensureColumn(db, "dashboard_auth_challenges", "approved_at", "TEXT");
293
+ ensureColumn(db, "dashboard_auth_challenges", "approved_by_telegram_user_id", "TEXT");
294
+ ensureColumn(db, "dashboard_auth_challenges", "rejected_at", "TEXT");
295
+ ensureColumn(db, "dashboard_auth_challenges", "rejected_by_telegram_user_id", "TEXT");
296
+ db.exec(`
297
+ CREATE INDEX IF NOT EXISTS idx_dashboard_auth_challenges_group
298
+ ON dashboard_auth_challenges(challenge_group_id, telegram_user_id, created_at);
299
+ `);
300
+ }
301
+ function hashOtp(challengeId, otp) {
302
+ return createHash("sha256").update(`${challengeId}:${otp}`).digest("hex");
303
+ }
304
+ function hashSessionToken(token) {
305
+ return createHash("sha256").update(token).digest("hex");
306
+ }
307
+ function hashPollToken(token) {
308
+ return createHash("sha256").update(token).digest("hex");
309
+ }
310
+ function ensureColumn(db, tableName, columnName, columnDefinition) {
311
+ const columns = db
312
+ .prepare(`PRAGMA table_info(${tableName})`)
313
+ .all();
314
+ if (columns.some((column) => column.name === columnName)) {
315
+ return;
316
+ }
317
+ db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition}`);
318
+ }
@@ -0,0 +1,133 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { DashboardAuthRepository } from "./dashboard-auth.js";
7
+ import { registerDashboardLoginCallbacks } from "./telegram-bot/dashboard-login.js";
8
+ test("dashboard approval challenge resolves and consumes once", async () => {
9
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-dashboard-auth-"));
10
+ const dbFile = path.join(tempDir, "auth.sqlite");
11
+ try {
12
+ const repo = DashboardAuthRepository.create(dbFile);
13
+ const challenge = repo.createApprovalChallenge({ telegramUserIds: ["1", "2"], ttlMs: 60_000 });
14
+ assert.match(challenge.displayCode, /^\d{2}$/);
15
+ const initial = repo.getApprovalChallengeStatus({ challengeId: challenge.id, pollToken: challenge.pollToken });
16
+ assert.equal(initial.ok, true);
17
+ assert.equal(initial.status, "pending");
18
+ const resolved = repo.resolveApprovalChoice({
19
+ challengeId: challenge.id,
20
+ telegramUserId: "1",
21
+ selectedCode: challenge.displayCode,
22
+ });
23
+ assert.equal(resolved.ok, true);
24
+ assert.equal(resolved.status, "approved");
25
+ const approved = repo.getApprovalChallengeStatus({ challengeId: challenge.id, pollToken: challenge.pollToken });
26
+ assert.equal(approved.ok, true);
27
+ assert.equal(approved.status, "approved");
28
+ const consumed = repo.consumeApprovedChallenge({ challengeId: challenge.id, pollToken: challenge.pollToken });
29
+ assert.equal(consumed.ok, true);
30
+ assert.equal(consumed.telegramUserId, "1");
31
+ const second = repo.consumeApprovedChallenge({ challengeId: challenge.id, pollToken: challenge.pollToken });
32
+ assert.equal(second.ok, false);
33
+ if (!second.ok) {
34
+ assert.equal(second.reason, "consumed");
35
+ }
36
+ }
37
+ finally {
38
+ rmSync(tempDir, { recursive: true, force: true });
39
+ }
40
+ });
41
+ test("dashboard login callback approves or rejects selected code", async () => {
42
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-dashboard-login-"));
43
+ const dbFile = path.join(tempDir, "auth.sqlite");
44
+ try {
45
+ const repo = DashboardAuthRepository.create(dbFile);
46
+ const challenge = repo.createApprovalChallenge({ telegramUserIds: ["1"], ttlMs: 60_000 });
47
+ const handler = createCallbackHarness();
48
+ registerDashboardLoginCallbacks(handler.bot, {
49
+ telegramBotToken: "token",
50
+ allowedUserIds: new Set(),
51
+ allowedChatIds: new Set(),
52
+ ownerUserIds: new Set(["1"]),
53
+ adminUserIds: new Set(["1"]),
54
+ botMode: "polling",
55
+ proxyAdminBaseUrl: "http://127.0.0.1:8318",
56
+ defaultModel: "gpt-5.5",
57
+ publicSignupEnabled: false,
58
+ requireAdminApproval: true,
59
+ defaultCustomerRoute: "customers",
60
+ publicResponsesBaseUrl: "http://127.0.0.1:8318/v1",
61
+ proxyRequestTimeoutMs: 30_000,
62
+ sessionDbPath: dbFile,
63
+ sessionTtlMs: 900_000,
64
+ rateLimitWindowMs: 60_000,
65
+ rateLimitMaxRequests: 12,
66
+ logLevel: "info",
67
+ }, repo);
68
+ const approved = handler.find(`v1:dashauth:${challenge.id}:${challenge.displayCode}`);
69
+ const approveCtx = createCallbackContext({ fromId: 1, chatId: 1, data: `v1:dashauth:${challenge.id}:${challenge.displayCode}` });
70
+ approveCtx.match = approved.match;
71
+ await approved.handler(approveCtx);
72
+ assert.equal(approveCtx.answeredCallbacks[0]?.text, "Dashboard login approved.");
73
+ assert.match(approveCtx.editedReplies[0]?.text ?? "", /approved/i);
74
+ const rejectedChallenge = repo.createApprovalChallenge({ telegramUserIds: ["1"], ttlMs: 60_000 });
75
+ const rejected = handler.find(`v1:dashauth:${rejectedChallenge.id}:00`);
76
+ const rejectCtx = createCallbackContext({ fromId: 1, chatId: 1, data: `v1:dashauth:${rejectedChallenge.id}:00` });
77
+ rejectCtx.match = rejected.match;
78
+ await rejected.handler(rejectCtx);
79
+ assert.equal(rejectCtx.answeredCallbacks[0]?.show_alert, true);
80
+ assert.match(rejectCtx.answeredCallbacks[0]?.text ?? "", /Wrong code/i);
81
+ }
82
+ finally {
83
+ rmSync(tempDir, { recursive: true, force: true });
84
+ }
85
+ });
86
+ function createCallbackHarness() {
87
+ const callbackHandlers = [];
88
+ return {
89
+ bot: {
90
+ callbackQuery(pattern, handler) {
91
+ callbackHandlers.push({ pattern, handler });
92
+ },
93
+ },
94
+ find(data) {
95
+ for (const entry of callbackHandlers) {
96
+ if (typeof entry.pattern === "string") {
97
+ if (entry.pattern === data) {
98
+ return { handler: entry.handler, match: [data] };
99
+ }
100
+ continue;
101
+ }
102
+ const match = data.match(entry.pattern);
103
+ if (match) {
104
+ return { handler: entry.handler, match };
105
+ }
106
+ }
107
+ assert.fail(`No callback handler for ${data}`);
108
+ },
109
+ };
110
+ }
111
+ function createCallbackContext(input) {
112
+ const answeredCallbacks = [];
113
+ const editedReplies = [];
114
+ return {
115
+ from: { id: input.fromId },
116
+ chat: { id: input.chatId, type: "private" },
117
+ callbackQuery: { data: input.data, message: { message_id: 1, chat: { id: input.chatId, type: "private" } } },
118
+ answeredCallbacks,
119
+ editedReplies,
120
+ answerCallbackQuery(payload) {
121
+ answeredCallbacks.push(payload ?? {});
122
+ return Promise.resolve();
123
+ },
124
+ editMessageText(text) {
125
+ editedReplies.push({ text });
126
+ return Promise.resolve();
127
+ },
128
+ reply(text) {
129
+ editedReplies.push({ text });
130
+ return Promise.resolve();
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,235 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { createServer } from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import test from "node:test";
8
+ import { DashboardAuthRepository } from "./dashboard-auth.js";
9
+ const repoRoot = process.cwd();
10
+ const distClientIndexPath = path.join(repoRoot, "dist", "client", "index.html");
11
+ const distClientIndexHtml = readFileSync(distClientIndexPath, "utf8");
12
+ const builtReactAssetPath = extractBuiltReactAssetPath(distClientIndexHtml);
13
+ test("dashboard serving smoke coverage", { concurrency: false }, async (t) => {
14
+ await t.test("react mode serves dashboard, preserves backend routes, and logs mode", async () => {
15
+ const server = await startDashboardServer();
16
+ try {
17
+ assert.match(server.output, /Dashboard UI: react \(serving .*dist\/client\)/);
18
+ const root = await fetchText(`${server.baseUrl}/`);
19
+ assert.equal(root.response.status, 200);
20
+ assert.equal(root.response.headers.get("cache-control"), "no-cache");
21
+ assert.match(root.text, /Responses Proxy React Shell|<div id="root"><\/div>/);
22
+ const favicon = await fetchText(`${server.baseUrl}/favicon.svg`);
23
+ assert.equal(favicon.response.status, 200);
24
+ assert.equal(favicon.response.headers.get("cache-control"), "no-cache");
25
+ assert.match(favicon.response.headers.get("content-type") ?? "", /image\/svg\+xml/);
26
+ const health = await fetchJson(`${server.baseUrl}/health`);
27
+ assert.equal(health.response.status, 200);
28
+ assert.match(health.response.headers.get("content-type") ?? "", /application\/json/);
29
+ assert.equal(health.body.ok, true);
30
+ const protectedBeforeLogin = await fetchJson(`${server.baseUrl}/api/providers`);
31
+ assert.equal(protectedBeforeLogin.response.status, 401);
32
+ const approvalRequest = await fetchJson(`${server.baseUrl}/api/dashboard-auth/request-approval`, {
33
+ method: "POST",
34
+ });
35
+ assert.equal(approvalRequest.response.status, 200);
36
+ assert.equal(typeof approvalRequest.body.debugApprovalCode, "string");
37
+ assert.match(String(approvalRequest.body.displayCode), /^\d{2}$/);
38
+ const dashboardAuth = DashboardAuthRepository.create(server.customerDbFile);
39
+ const resolved = dashboardAuth.resolveApprovalChoice({
40
+ challengeId: approvalRequest.body.challengeId,
41
+ telegramUserId: "1",
42
+ selectedCode: approvalRequest.body.debugApprovalCode,
43
+ });
44
+ assert.equal(resolved.ok, true);
45
+ const poll = await fetchJson(`${server.baseUrl}/api/dashboard-auth/approval-status?challengeId=${encodeURIComponent(approvalRequest.body.challengeId)}&pollToken=${encodeURIComponent(approvalRequest.body.pollToken)}`);
46
+ assert.equal(poll.response.status, 200);
47
+ assert.equal(poll.body.status, "approved");
48
+ const approvedSession = poll.body.session;
49
+ assert.equal(typeof approvedSession?.telegramUserId, "string");
50
+ const session = DashboardAuthRepository.create(server.customerDbFile).createSession({
51
+ telegramUserId: approvedSession?.telegramUserId,
52
+ ttlMs: 60_000,
53
+ });
54
+ const sessionCookie = `responses_proxy_dashboard_session=${encodeURIComponent(session.token)}`;
55
+ const providers = await fetchJson(`${server.baseUrl}/api/providers`, {
56
+ headers: { Cookie: sessionCookie },
57
+ });
58
+ assert.equal(providers.response.status, 200);
59
+ assert.match(providers.response.headers.get("content-type") ?? "", /application\/json/);
60
+ assert.equal(typeof providers.body, "object");
61
+ assert.doesNotMatch(JSON.stringify(providers.body), /Responses Proxy React Shell/);
62
+ const authCallback = await fetchText(`${server.baseUrl}/auth/chatgpt/callback`);
63
+ assert.notEqual(authCallback.response.status, 404);
64
+ assert.doesNotMatch(authCallback.text, /Responses Proxy React Shell/);
65
+ assert.match(authCallback.text, /ChatGPT OAuth/);
66
+ const v1Models = await fetchText(`${server.baseUrl}/v1/models`);
67
+ assert.notEqual(v1Models.response.status, 200);
68
+ assert.doesNotMatch(v1Models.text, /Responses Proxy React Shell/);
69
+ assert.match(v1Models.response.headers.get("content-type") ?? "", /application\/json/);
70
+ }
71
+ finally {
72
+ await server.stop();
73
+ }
74
+ });
75
+ await t.test("legacy dashboard routes are removed", async () => {
76
+ const server = await startDashboardServer();
77
+ try {
78
+ const legacy = await fetchText(`${server.baseUrl}/legacy`);
79
+ assert.equal(legacy.response.status, 404);
80
+ assert.match(legacy.response.headers.get("content-type") ?? "", /application\/json/);
81
+ const legacyAsset = await fetchText(`${server.baseUrl}/legacy/app.js`);
82
+ assert.equal(legacyAsset.response.status, 404);
83
+ assert.match(legacyAsset.response.headers.get("content-type") ?? "", /application\/json/);
84
+ const rootAsset = await fetchText(`${server.baseUrl}/app.js`);
85
+ assert.equal(rootAsset.response.status, 404);
86
+ assert.match(rootAsset.response.headers.get("content-type") ?? "", /application\/json/);
87
+ const health = await fetchJson(`${server.baseUrl}/health`);
88
+ assert.equal(health.response.status, 200);
89
+ assert.equal(health.body.ok, true);
90
+ }
91
+ finally {
92
+ await server.stop();
93
+ }
94
+ });
95
+ await t.test("static assets use expected cache policy and block traversal", async () => {
96
+ const server = await startDashboardServer();
97
+ try {
98
+ const reactAsset = await fetchText(`${server.baseUrl}${builtReactAssetPath}`);
99
+ assert.equal(reactAsset.response.status, 200);
100
+ assert.equal(reactAsset.response.headers.get("cache-control"), "public, max-age=31536000, immutable");
101
+ assert.match(reactAsset.response.headers.get("content-type") ?? "", /javascript|text\/css/);
102
+ const legacyAsset = await fetchText(`${server.baseUrl}/legacy/app.js`);
103
+ assert.equal(legacyAsset.response.status, 404);
104
+ const traversal = await fetchText(`${server.baseUrl}/assets/..%2F..%2Fserver.js`);
105
+ assert.equal(traversal.response.status, 404);
106
+ assert.match(traversal.response.headers.get("content-type") ?? "", /application\/json/);
107
+ }
108
+ finally {
109
+ await server.stop();
110
+ }
111
+ });
112
+ });
113
+ function extractBuiltReactAssetPath(indexHtml) {
114
+ const match = indexHtml.match(/"(\/assets\/[^"]+\.(?:js|css))"/);
115
+ assert.ok(match?.[1], "Could not find built React asset reference in dist/client/index.html");
116
+ return match[1];
117
+ }
118
+ async function fetchText(url, init) {
119
+ const response = await fetch(url, init);
120
+ const text = await response.text();
121
+ return { response, text };
122
+ }
123
+ async function fetchJson(url, init) {
124
+ const response = await fetch(url, init);
125
+ const body = (await response.json().catch(() => ({})));
126
+ return { response, body };
127
+ }
128
+ async function startDashboardServer(extraEnv = {}) {
129
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-dashboard-serving-"));
130
+ const port = await getFreePort();
131
+ const child = spawn(process.execPath, ["--import", "tsx", "src/server.ts"], {
132
+ cwd: repoRoot,
133
+ env: {
134
+ ...process.env,
135
+ PORT: String(port),
136
+ HOST: "127.0.0.1",
137
+ UPSTREAM_BASE_URL: "https://api.openai.com",
138
+ APP_DB_PATH: path.join(tempDir, "app.sqlite"),
139
+ CUSTOMER_KEY_DB_PATH: path.join(tempDir, "customer.sqlite"),
140
+ SESSION_LOG_DIR: path.join(tempDir, "sessions"),
141
+ PROVIDER_USAGE_CHECK_ENABLED: "false",
142
+ CHATGPT_OAUTH_ENABLED: "false",
143
+ TELEGRAM_BOT_TOKEN: "test-dashboard-bot-token",
144
+ TELEGRAM_OWNER_USER_IDS: "1",
145
+ TELEGRAM_ADMIN_USER_IDS: "1",
146
+ LOG_LEVEL: "info",
147
+ ...extraEnv,
148
+ },
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ });
151
+ let output = "";
152
+ child.stdout.on("data", (chunk) => {
153
+ output += chunk.toString();
154
+ });
155
+ child.stderr.on("data", (chunk) => {
156
+ output += chunk.toString();
157
+ });
158
+ try {
159
+ await waitForServer(`http://127.0.0.1:${port}/health`, child);
160
+ }
161
+ catch (error) {
162
+ child.kill("SIGTERM");
163
+ rmSync(tempDir, { recursive: true, force: true });
164
+ throw new Error(`Server failed to start.\n${output}\n${String(error)}`);
165
+ }
166
+ return {
167
+ baseUrl: `http://127.0.0.1:${port}`,
168
+ customerDbFile: path.join(tempDir, "customer.sqlite"),
169
+ get output() {
170
+ return output;
171
+ },
172
+ async stop() {
173
+ await stopChild(child);
174
+ rmSync(tempDir, { recursive: true, force: true });
175
+ },
176
+ };
177
+ }
178
+ async function waitForServer(url, child, timeoutMs = 15_000) {
179
+ const startedAt = Date.now();
180
+ while (Date.now() - startedAt < timeoutMs) {
181
+ if (child.exitCode !== null) {
182
+ throw new Error(`server exited early with code ${child.exitCode}`);
183
+ }
184
+ try {
185
+ const response = await fetch(url);
186
+ if (response.ok) {
187
+ return;
188
+ }
189
+ }
190
+ catch {
191
+ // Retry until the timeout expires.
192
+ }
193
+ await new Promise((resolve) => setTimeout(resolve, 100));
194
+ }
195
+ throw new Error(`server did not become ready within ${timeoutMs}ms`);
196
+ }
197
+ async function stopChild(child) {
198
+ if (child.exitCode !== null) {
199
+ return;
200
+ }
201
+ child.kill("SIGTERM");
202
+ await new Promise((resolve) => {
203
+ const timer = setTimeout(() => {
204
+ if (child.exitCode === null) {
205
+ child.kill("SIGKILL");
206
+ }
207
+ }, 3_000);
208
+ child.once("exit", () => {
209
+ clearTimeout(timer);
210
+ resolve();
211
+ });
212
+ });
213
+ }
214
+ async function getFreePort() {
215
+ return await new Promise((resolve, reject) => {
216
+ const server = createServer();
217
+ server.listen(0, "127.0.0.1", () => {
218
+ const address = server.address();
219
+ if (!address || typeof address === "string") {
220
+ server.close();
221
+ reject(new Error("Could not resolve a free port"));
222
+ return;
223
+ }
224
+ const { port } = address;
225
+ server.close((error) => {
226
+ if (error) {
227
+ reject(error);
228
+ return;
229
+ }
230
+ resolve(port);
231
+ });
232
+ });
233
+ server.on("error", reject);
234
+ });
235
+ }