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,140 @@
1
+ export function maskApiKey(value) {
2
+ const raw = value?.trim() || "";
3
+ if (!raw) {
4
+ return "none";
5
+ }
6
+ if (raw.length <= 10) {
7
+ return `${raw.slice(0, 2)}...${raw.slice(-2)}`;
8
+ }
9
+ return `${raw.slice(0, 4)}...${raw.slice(-4)}`;
10
+ }
11
+ export function formatProxyError(error) {
12
+ if (error instanceof Error) {
13
+ return error.message;
14
+ }
15
+ const proxyError = error ?? {};
16
+ const lines = [
17
+ `${proxyError.code ?? "PROXY_REQUEST_FAILED"}: ${proxyError.message ?? "Unknown proxy error"}`,
18
+ ];
19
+ if (proxyError.request_id) {
20
+ lines.push(`request_id: ${proxyError.request_id}`);
21
+ }
22
+ if (typeof proxyError.upstream_status === "number") {
23
+ lines.push(`upstream_status: ${proxyError.upstream_status}`);
24
+ }
25
+ if (typeof proxyError.retryable === "boolean") {
26
+ lines.push(`retryable: ${String(proxyError.retryable)}`);
27
+ }
28
+ return lines.join("\n");
29
+ }
30
+ export function formatHealthStatus(status) {
31
+ const lines = [
32
+ "Proxy status",
33
+ `ok: ${String(status.ok ?? true)}`,
34
+ `active provider: ${status.activeProviderId ?? "unknown"}`,
35
+ `upstream: ${status.upstream ?? "n/a"}`,
36
+ `fallback: ${status.fallback ?? "n/a"}`,
37
+ ];
38
+ if (status.latestPromptCache) {
39
+ lines.push(`prompt cache: ${status.latestPromptCache.cacheStatus ?? "unknown"} ` +
40
+ `provider=${status.latestPromptCache.providerId ?? "n/a"} ` +
41
+ `hit=${String(status.latestPromptCache.providerCacheHit ?? false)}`);
42
+ }
43
+ for (const line of status.usageSummary ?? []) {
44
+ lines.push(line);
45
+ }
46
+ return lines.join("\n");
47
+ }
48
+ export function formatProviders(payload) {
49
+ const lines = [`Active provider: ${payload.activeProviderId ?? "none"}`, "", "Providers:"];
50
+ for (const provider of payload.providers ?? []) {
51
+ lines.push(`- ${provider.name} (${provider.id}) auth=${provider.authMode ?? "api_key"} ` +
52
+ `apiKey=${provider.hasProviderApiKey ? "yes" : "no"} ` +
53
+ `account=${provider.chatgptAccountId ?? "n/a"}`);
54
+ }
55
+ lines.push("", "Client routes:");
56
+ for (const route of payload.clientRoutes ?? []) {
57
+ lines.push(`- ${route.key}: ${route.providerName ?? route.providerId ?? "unassigned"} ` +
58
+ `model=${route.modelOverride ?? "default"} keys=${route.apiKeys.map(maskApiKey).join(", ") || "none"}`);
59
+ }
60
+ return lines.join("\n");
61
+ }
62
+ export function formatProviderDetails(provider) {
63
+ return [
64
+ `${provider.name} (${provider.id})`,
65
+ `baseUrl: ${provider.baseUrl}`,
66
+ `authMode: ${provider.authMode ?? "api_key"}`,
67
+ `provider keys: ${provider.providerApiKeysCount ?? 0}`,
68
+ `system managed: ${String(provider.capabilities?.systemManaged ?? false)}`,
69
+ `account platform: ${provider.capabilities?.accountPlatform ?? "n/a"}`,
70
+ `account pool required: ${String(provider.capabilities?.accountPoolRequired ?? false)}`,
71
+ `chatgpt account: ${provider.chatgptAccountId ?? "n/a"}`,
72
+ ].join("\n");
73
+ }
74
+ export function formatClientConfigs(payload) {
75
+ const lines = [`Proxy base URL: ${payload.proxyBaseUrl ?? "n/a"}`];
76
+ for (const [client, status] of Object.entries(payload.clients ?? {})) {
77
+ lines.push("");
78
+ lines.push(`${client}:`);
79
+ lines.push(`path: ${status.path ?? "n/a"}`);
80
+ lines.push(`exists: ${String(status.exists ?? false)}`);
81
+ lines.push(`configured: ${String(status.configured ?? false)}`);
82
+ lines.push(`routeApiKey: ${maskApiKey(status.routeApiKey)}`);
83
+ if (status.auth) {
84
+ lines.push(`auth configured: ${String(status.auth.configured ?? false)}`);
85
+ lines.push(`auth apiKey: ${maskApiKey(status.auth.detectedApiKey)}`);
86
+ }
87
+ for (const [key, value] of Object.entries(status.detected ?? {})) {
88
+ lines.push(`${key}: ${value ?? "n/a"}`);
89
+ }
90
+ }
91
+ return lines.join("\n");
92
+ }
93
+ export function formatOauthStatus(payload) {
94
+ const lines = [
95
+ `OAuth enabled: ${String(payload.enabled ?? false)}`,
96
+ `Rotation mode: ${payload.rotationMode ?? "unknown"}`,
97
+ "",
98
+ "Accounts:",
99
+ ];
100
+ for (const account of payload.accounts ?? []) {
101
+ lines.push(`- ${account.email || account.accountId || account.id} id=${account.id} ` +
102
+ `disabled=${String(account.disabled ?? false)} expires=${account.expiresAt ?? "n/a"} ` +
103
+ `refreshed=${account.lastRefreshAt ?? "never"}`);
104
+ }
105
+ if ((payload.accounts?.length ?? 0) === 0) {
106
+ lines.push("- none");
107
+ }
108
+ return lines.join("\n");
109
+ }
110
+ export function formatTestResult(payload) {
111
+ const lines = ["Test response", payload.outputText.trim() || "(empty)"];
112
+ if (payload.requestId) {
113
+ lines.push("", `request_id: ${payload.requestId}`);
114
+ }
115
+ return lines.join("\n");
116
+ }
117
+ export function formatModels(payload) {
118
+ const lines = ["Models:"];
119
+ for (const item of payload.data ?? []) {
120
+ lines.push(`- ${item.id ?? "unknown"} owner=${item.owned_by ?? "n/a"}`);
121
+ }
122
+ if ((payload.data?.length ?? 0) === 0) {
123
+ lines.push("- none");
124
+ }
125
+ return lines.join("\n");
126
+ }
127
+ export function extractResponseText(body) {
128
+ if (!body || typeof body !== "object") {
129
+ return "";
130
+ }
131
+ const candidate = body;
132
+ if (typeof candidate.output_text === "string") {
133
+ return candidate.output_text;
134
+ }
135
+ const parts = candidate.output
136
+ ?.flatMap((item) => item.content ?? [])
137
+ .filter((part) => part?.type === "output_text" && typeof part.text === "string")
138
+ .map((part) => part.text ?? "") ?? [];
139
+ return parts.join("\n").trim();
140
+ }
@@ -0,0 +1,370 @@
1
+ export function assertWorkspaceApiKeyCapacity(args) {
2
+ const entitlement = args.billing.getActiveEntitlementForWorkspace(args.workspaceId);
3
+ const maxApiKeys = entitlement?.maxApiKeys ?? args.maxApiKeysIfNoEntitlement;
4
+ if (!maxApiKeys) {
5
+ throw new Error("No active entitlement was found for this customer workspace.");
6
+ }
7
+ const ignored = new Set(args.ignoredKeyIds ?? []);
8
+ const occupiedKeys = args.customerKeys.listKeysByWorkspace(args.workspaceId).filter((record) => {
9
+ if (ignored.has(record.id)) {
10
+ return false;
11
+ }
12
+ return record.status === "active" || record.status === "suspended";
13
+ });
14
+ if (occupiedKeys.length >= maxApiKeys) {
15
+ throw new Error(`API key limit reached for this workspace (${occupiedKeys.length}/${maxApiKeys}). Revoke or rotate an existing key first.`);
16
+ }
17
+ }
18
+ export async function grantCustomerAccess(args) {
19
+ return provisionCustomerAccess({
20
+ ...args,
21
+ action: "grant",
22
+ });
23
+ }
24
+ export async function renewCustomerAccess(args) {
25
+ return provisionCustomerAccess({
26
+ ...args,
27
+ action: "renew",
28
+ });
29
+ }
30
+ async function provisionCustomerAccess(args) {
31
+ const existingUser = args.identities.getUser(args.telegramUserId);
32
+ args.identities.upsertUser({
33
+ telegramUserId: args.telegramUserId,
34
+ defaultRole: "customer",
35
+ defaultStatus: "active",
36
+ });
37
+ args.identities.setUserStatus(args.telegramUserId, "active");
38
+ if (!existingUser) {
39
+ args.auditLog?.record({
40
+ event: "user.created",
41
+ actor: args.actor,
42
+ subjectType: "telegram_user",
43
+ subjectId: args.telegramUserId,
44
+ metadata: {
45
+ telegramUserId: args.telegramUserId,
46
+ },
47
+ });
48
+ }
49
+ else if (existingUser.status === "pending_approval") {
50
+ args.auditLog?.record({
51
+ event: "user.approved",
52
+ actor: args.actor,
53
+ subjectType: "telegram_user",
54
+ subjectId: args.telegramUserId,
55
+ metadata: {
56
+ telegramUserId: args.telegramUserId,
57
+ priorStatus: existingUser.status,
58
+ reason: `${args.action}_approval`,
59
+ },
60
+ });
61
+ }
62
+ const existingWorkspace = args.workspaces.getDefaultWorkspace(args.telegramUserId);
63
+ const workspace = args.workspaces.ensureDefaultWorkspace({
64
+ ownerTelegramUserId: args.telegramUserId,
65
+ defaultClientRoute: args.defaultClientRoute,
66
+ status: "active",
67
+ });
68
+ if (!existingWorkspace) {
69
+ args.auditLog?.record({
70
+ event: "workspace.created",
71
+ actor: args.actor,
72
+ subjectType: "workspace",
73
+ subjectId: workspace.id,
74
+ metadata: {
75
+ workspaceId: workspace.id,
76
+ telegramUserId: args.telegramUserId,
77
+ clientRoute: workspace.defaultClientRoute,
78
+ },
79
+ });
80
+ }
81
+ else if (existingWorkspace.status === "pending_approval") {
82
+ args.auditLog?.record({
83
+ event: "workspace.approved",
84
+ actor: args.actor,
85
+ subjectType: "workspace",
86
+ subjectId: workspace.id,
87
+ metadata: {
88
+ workspaceId: workspace.id,
89
+ telegramUserId: args.telegramUserId,
90
+ priorStatus: existingWorkspace.status,
91
+ reason: `${args.action}_approval`,
92
+ },
93
+ });
94
+ }
95
+ if (workspace.status !== "active") {
96
+ args.workspaces.setStatus(workspace.id, "active");
97
+ }
98
+ const plan = args.billing.getPlan(args.planId);
99
+ if (!plan) {
100
+ throw new Error(`Plan not found: ${args.planId}`);
101
+ }
102
+ let mode;
103
+ let keyRecord = args.customerKeys.getActiveKeyForUser(args.telegramUserId);
104
+ let apiKey;
105
+ let createdKeyId;
106
+ let keyToRevokeAfterSync;
107
+ if (args.replaceKey || (keyRecord && !args.customerKeys.getApiKeySecret(keyRecord.id))) {
108
+ const latestKey = args.customerKeys.getLatestKeyForUser(args.telegramUserId);
109
+ const ignoredKeyIds = keyRecord
110
+ ? [keyRecord.id]
111
+ : latestKey && latestKey.status !== "revoked"
112
+ ? [latestKey.id]
113
+ : [];
114
+ assertWorkspaceApiKeyCapacity({
115
+ workspaceId: workspace.id,
116
+ billing: args.billing,
117
+ customerKeys: args.customerKeys,
118
+ ignoredKeyIds,
119
+ maxApiKeysIfNoEntitlement: plan.maxApiKeys,
120
+ });
121
+ keyToRevokeAfterSync = keyRecord ?? (latestKey && latestKey.status !== "revoked" ? latestKey : undefined);
122
+ const created = args.customerKeys.createKey({
123
+ workspaceId: workspace.id,
124
+ telegramUserId: args.telegramUserId,
125
+ clientRoute: workspace.defaultClientRoute,
126
+ status: "suspended",
127
+ });
128
+ keyRecord = created.record;
129
+ apiKey = created.apiKey;
130
+ createdKeyId = created.record.id;
131
+ mode = latestKey ? "existing_key_replaced" : "new_key_created";
132
+ args.auditLog?.record({
133
+ event: "api_key.created",
134
+ actor: args.actor,
135
+ subjectType: "customer_api_key",
136
+ subjectId: keyRecord.id,
137
+ metadata: {
138
+ telegramUserId: args.telegramUserId,
139
+ workspaceId: workspace.id,
140
+ clientRoute: keyRecord.clientRoute,
141
+ keyPreview: keyRecord.apiKeyPreview,
142
+ },
143
+ });
144
+ if (latestKey) {
145
+ args.auditLog?.record({
146
+ event: "api_key.rotated",
147
+ actor: args.actor,
148
+ subjectType: "customer_api_key",
149
+ subjectId: keyRecord.id,
150
+ metadata: {
151
+ telegramUserId: args.telegramUserId,
152
+ workspaceId: workspace.id,
153
+ oldKeyId: latestKey.id,
154
+ newKeyId: keyRecord.id,
155
+ newKeyPreview: keyRecord.apiKeyPreview,
156
+ },
157
+ });
158
+ }
159
+ }
160
+ else {
161
+ if (keyRecord) {
162
+ mode = "existing_key_already_active";
163
+ apiKey = args.customerKeys.getApiKeySecret(keyRecord.id);
164
+ }
165
+ else {
166
+ const latestKey = args.customerKeys.getLatestKeyForUser(args.telegramUserId);
167
+ const latestKeySecret = latestKey ? args.customerKeys.getApiKeySecret(latestKey.id) : undefined;
168
+ if (latestKey && latestKey.status !== "revoked" && latestKeySecret) {
169
+ keyRecord = args.customerKeys.setStatus(latestKey.id, "active");
170
+ mode = "existing_key_reactivated";
171
+ if (keyRecord) {
172
+ apiKey = latestKeySecret;
173
+ }
174
+ if (keyRecord) {
175
+ args.auditLog?.record({
176
+ event: "api_key.activated",
177
+ actor: args.actor,
178
+ subjectType: "customer_api_key",
179
+ subjectId: keyRecord.id,
180
+ metadata: {
181
+ telegramUserId: args.telegramUserId,
182
+ workspaceId: workspace.id,
183
+ keyPreview: keyRecord.apiKeyPreview,
184
+ },
185
+ });
186
+ }
187
+ }
188
+ else if (latestKey && latestKey.status !== "revoked") {
189
+ assertWorkspaceApiKeyCapacity({
190
+ workspaceId: workspace.id,
191
+ billing: args.billing,
192
+ customerKeys: args.customerKeys,
193
+ ignoredKeyIds: [latestKey.id],
194
+ maxApiKeysIfNoEntitlement: plan.maxApiKeys,
195
+ });
196
+ keyToRevokeAfterSync = latestKey;
197
+ const created = args.customerKeys.createKey({
198
+ workspaceId: workspace.id,
199
+ telegramUserId: args.telegramUserId,
200
+ clientRoute: workspace.defaultClientRoute,
201
+ status: "suspended",
202
+ });
203
+ keyRecord = created.record;
204
+ apiKey = created.apiKey;
205
+ createdKeyId = created.record.id;
206
+ mode = "existing_key_replaced";
207
+ args.auditLog?.record({
208
+ event: "api_key.created",
209
+ actor: args.actor,
210
+ subjectType: "customer_api_key",
211
+ subjectId: keyRecord.id,
212
+ metadata: {
213
+ telegramUserId: args.telegramUserId,
214
+ workspaceId: workspace.id,
215
+ clientRoute: keyRecord.clientRoute,
216
+ keyPreview: keyRecord.apiKeyPreview,
217
+ reason: "legacy_key_secret_unavailable",
218
+ },
219
+ });
220
+ args.auditLog?.record({
221
+ event: "api_key.rotated",
222
+ actor: args.actor,
223
+ subjectType: "customer_api_key",
224
+ subjectId: keyRecord.id,
225
+ metadata: {
226
+ telegramUserId: args.telegramUserId,
227
+ workspaceId: workspace.id,
228
+ oldKeyId: latestKey.id,
229
+ newKeyId: keyRecord.id,
230
+ newKeyPreview: keyRecord.apiKeyPreview,
231
+ reason: "legacy_key_secret_unavailable",
232
+ },
233
+ });
234
+ }
235
+ else {
236
+ assertWorkspaceApiKeyCapacity({
237
+ workspaceId: workspace.id,
238
+ billing: args.billing,
239
+ customerKeys: args.customerKeys,
240
+ maxApiKeysIfNoEntitlement: plan.maxApiKeys,
241
+ });
242
+ const created = args.customerKeys.createKey({
243
+ workspaceId: workspace.id,
244
+ telegramUserId: args.telegramUserId,
245
+ clientRoute: workspace.defaultClientRoute,
246
+ status: "suspended",
247
+ });
248
+ keyRecord = created.record;
249
+ apiKey = created.apiKey;
250
+ createdKeyId = created.record.id;
251
+ mode = "new_key_created";
252
+ args.auditLog?.record({
253
+ event: "api_key.created",
254
+ actor: args.actor,
255
+ subjectType: "customer_api_key",
256
+ subjectId: keyRecord.id,
257
+ metadata: {
258
+ telegramUserId: args.telegramUserId,
259
+ workspaceId: workspace.id,
260
+ clientRoute: keyRecord.clientRoute,
261
+ keyPreview: keyRecord.apiKeyPreview,
262
+ },
263
+ });
264
+ }
265
+ }
266
+ }
267
+ if (!keyRecord) {
268
+ throw new Error("Customer key could not be created or reactivated");
269
+ }
270
+ if (apiKey && createdKeyId) {
271
+ try {
272
+ await syncNewRouteApiKey(args.proxyClient, workspace.defaultClientRoute, apiKey);
273
+ const activated = args.customerKeys.setStatus(createdKeyId, "active");
274
+ if (activated) {
275
+ keyRecord = activated;
276
+ args.auditLog?.record({
277
+ event: "api_key.activated",
278
+ actor: args.actor,
279
+ subjectType: "customer_api_key",
280
+ subjectId: activated.id,
281
+ metadata: {
282
+ telegramUserId: args.telegramUserId,
283
+ workspaceId: workspace.id,
284
+ keyPreview: activated.apiKeyPreview,
285
+ reason: "proxy_sync_succeeded",
286
+ },
287
+ });
288
+ }
289
+ if (keyToRevokeAfterSync) {
290
+ args.customerKeys.setStatus(keyToRevokeAfterSync.id, "revoked");
291
+ args.auditLog?.record({
292
+ event: "api_key.revoked",
293
+ actor: args.actor,
294
+ subjectType: "customer_api_key",
295
+ subjectId: keyToRevokeAfterSync.id,
296
+ metadata: {
297
+ telegramUserId: args.telegramUserId,
298
+ workspaceId: workspace.id,
299
+ keyPreview: keyToRevokeAfterSync.apiKeyPreview,
300
+ reason: "rotation",
301
+ },
302
+ });
303
+ }
304
+ }
305
+ catch (error) {
306
+ args.customerKeys.setStatus(createdKeyId, "revoked");
307
+ args.auditLog?.record({
308
+ event: "api_key.revoked",
309
+ actor: { type: "system", id: "proxy-sync-rollback" },
310
+ subjectType: "customer_api_key",
311
+ subjectId: createdKeyId,
312
+ metadata: {
313
+ telegramUserId: args.telegramUserId,
314
+ workspaceId: workspace.id,
315
+ keyPreview: keyRecord.apiKeyPreview,
316
+ reason: "proxy_sync_failed",
317
+ },
318
+ });
319
+ throw error;
320
+ }
321
+ }
322
+ const { subscription } = args.billing.grantSubscription({
323
+ workspaceId: workspace.id,
324
+ planId: args.planId,
325
+ days: args.days,
326
+ });
327
+ args.auditLog?.record({
328
+ event: args.action === "grant" ? "subscription.granted" : "subscription.renewed",
329
+ actor: args.actor,
330
+ subjectType: "workspace",
331
+ subjectId: workspace.id,
332
+ metadata: {
333
+ workspaceId: workspace.id,
334
+ telegramUserId: args.telegramUserId,
335
+ planId: args.planId,
336
+ days: args.days,
337
+ subscriptionEndsAt: subscription.currentPeriodEnd,
338
+ },
339
+ });
340
+ return {
341
+ mode,
342
+ workspaceId: workspace.id,
343
+ clientRoute: workspace.defaultClientRoute,
344
+ keyId: keyRecord.id,
345
+ keyPreview: keyRecord.apiKeyPreview,
346
+ apiKey,
347
+ subscriptionEndsAt: subscription.currentPeriodEnd,
348
+ };
349
+ }
350
+ function readClientRouteKeys(payload, clientRoute) {
351
+ const routes = [
352
+ ...(Array.isArray(payload?.clientRoutes) ? payload.clientRoutes : []),
353
+ ...Object.values(payload?.clients ?? {})
354
+ .map((entry) => entry?.route)
355
+ .filter(Boolean),
356
+ ];
357
+ const route = routes.find((entry) => entry?.key === clientRoute);
358
+ return Array.isArray(route?.apiKeys)
359
+ ? route.apiKeys.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
360
+ : [];
361
+ }
362
+ async function syncNewRouteApiKey(proxyClient, clientRoute, apiKey) {
363
+ const clientConfigs = await proxyClient.getClientConfigs();
364
+ const currentKeys = readClientRouteKeys(clientConfigs, clientRoute);
365
+ const nextKeys = [...new Set([...currentKeys, apiKey])];
366
+ await proxyClient.setClientRouteApiKeys({
367
+ client: clientRoute,
368
+ apiKeys: nextKeys,
369
+ });
370
+ }