dsh-plugin-subscription-usage 0.0.0-stage → 0.2.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 (169) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +50 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +9 -0
  5. package/lib/auth/claude-code-creds.d.ts +23 -0
  6. package/lib/auth/claude-code-creds.js +172 -0
  7. package/lib/auth/device-flow.d.ts +55 -0
  8. package/lib/auth/device-flow.js +177 -0
  9. package/lib/auth/jwt.d.ts +10 -0
  10. package/lib/auth/jwt.js +25 -0
  11. package/lib/auth/oauth-flow.d.ts +91 -0
  12. package/lib/auth/oauth-flow.js +227 -0
  13. package/lib/auth/pkce.d.ts +31 -0
  14. package/lib/auth/pkce.js +35 -0
  15. package/lib/auth/rpc.d.ts +233 -0
  16. package/lib/auth/rpc.js +501 -0
  17. package/lib/auth/store.d.ts +198 -0
  18. package/lib/auth/store.js +473 -0
  19. package/lib/client/Footer.d.ts +3 -0
  20. package/lib/client/Footer.js +73 -0
  21. package/lib/client/Gauge.d.ts +28 -0
  22. package/lib/client/Gauge.js +139 -0
  23. package/lib/client/ImageGallery.d.ts +54 -0
  24. package/lib/client/ImageGallery.js +112 -0
  25. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  26. package/lib/client/ImageGenerateToolview.js +130 -0
  27. package/lib/client/Notice.d.ts +15 -0
  28. package/lib/client/Notice.js +93 -0
  29. package/lib/client/ProviderCard.d.ts +35 -0
  30. package/lib/client/ProviderCard.js +240 -0
  31. package/lib/client/SpeedSelect.d.ts +72 -0
  32. package/lib/client/SpeedSelect.js +214 -0
  33. package/lib/client/Usage.d.ts +7 -0
  34. package/lib/client/Usage.js +83 -0
  35. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  36. package/lib/client/VideoGenerateToolview.js +172 -0
  37. package/lib/client/context.d.ts +16 -0
  38. package/lib/client/context.js +11 -0
  39. package/lib/client/fast-command.d.ts +2 -0
  40. package/lib/client/fast-command.js +4 -0
  41. package/lib/client/hooks.d.ts +2 -0
  42. package/lib/client/hooks.js +10 -0
  43. package/lib/client/index.d.ts +3 -0
  44. package/lib/client/index.js +113 -0
  45. package/lib/client/locales.d.ts +139 -0
  46. package/lib/client/locales.js +267 -0
  47. package/lib/client/meter.d.ts +91 -0
  48. package/lib/client/meter.js +196 -0
  49. package/lib/client/prefs.d.ts +51 -0
  50. package/lib/client/prefs.js +71 -0
  51. package/lib/client/rpc.d.ts +161 -0
  52. package/lib/client/rpc.js +31 -0
  53. package/lib/client/store.d.ts +67 -0
  54. package/lib/client/store.js +129 -0
  55. package/lib/client/styles.d.ts +7 -0
  56. package/lib/client/styles.js +176 -0
  57. package/lib/client.js +3933 -0
  58. package/lib/client.js.map +1 -0
  59. package/lib/compat.d.ts +55 -0
  60. package/lib/compat.js +20 -0
  61. package/lib/http.d.ts +134 -0
  62. package/lib/http.js +431 -0
  63. package/lib/index.d.ts +121 -0
  64. package/lib/index.js +13399 -0
  65. package/lib/json-text.d.ts +2 -0
  66. package/lib/json-text.js +4 -0
  67. package/lib/model-defaults.d.ts +23 -0
  68. package/lib/model-defaults.js +238 -0
  69. package/lib/model-speeds.d.ts +10 -0
  70. package/lib/model-speeds.js +48 -0
  71. package/lib/models-directory.d.ts +17 -0
  72. package/lib/models-directory.js +25 -0
  73. package/lib/provider-settings.d.ts +55 -0
  74. package/lib/provider-settings.js +215 -0
  75. package/lib/providers/account-preferences.d.ts +46 -0
  76. package/lib/providers/account-preferences.js +188 -0
  77. package/lib/providers/accounts.d.ts +113 -0
  78. package/lib/providers/accounts.js +142 -0
  79. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  80. package/lib/providers/antigravity-oauth-client.js +7 -0
  81. package/lib/providers/antigravity.d.ts +119 -0
  82. package/lib/providers/antigravity.js +552 -0
  83. package/lib/providers/catalog-refresh.d.ts +4 -0
  84. package/lib/providers/catalog-refresh.js +30 -0
  85. package/lib/providers/catalog-store.d.ts +47 -0
  86. package/lib/providers/catalog-store.js +283 -0
  87. package/lib/providers/claude-cli-version.d.ts +15 -0
  88. package/lib/providers/claude-cli-version.js +24 -0
  89. package/lib/providers/claude.d.ts +212 -0
  90. package/lib/providers/claude.js +726 -0
  91. package/lib/providers/codex-client-version.d.ts +8 -0
  92. package/lib/providers/codex-client-version.js +17 -0
  93. package/lib/providers/codex-search.d.ts +29 -0
  94. package/lib/providers/codex-search.js +198 -0
  95. package/lib/providers/codex.d.ts +253 -0
  96. package/lib/providers/codex.js +930 -0
  97. package/lib/providers/common.d.ts +415 -0
  98. package/lib/providers/common.js +679 -0
  99. package/lib/providers/copilot.d.ts +365 -0
  100. package/lib/providers/copilot.js +956 -0
  101. package/lib/providers/grok.d.ts +214 -0
  102. package/lib/providers/grok.js +750 -0
  103. package/lib/providers/image-pool.d.ts +30 -0
  104. package/lib/providers/image-pool.js +89 -0
  105. package/lib/providers/npm-cli-version.d.ts +56 -0
  106. package/lib/providers/npm-cli-version.js +98 -0
  107. package/lib/providers/pool-family.d.ts +56 -0
  108. package/lib/providers/pool-family.js +45 -0
  109. package/lib/providers/pool-health.d.ts +91 -0
  110. package/lib/providers/pool-health.js +164 -0
  111. package/lib/providers/pool-usage.d.ts +106 -0
  112. package/lib/providers/pool-usage.js +302 -0
  113. package/lib/providers/pool.d.ts +115 -0
  114. package/lib/providers/pool.js +461 -0
  115. package/lib/providers/rate-limit.d.ts +192 -0
  116. package/lib/providers/rate-limit.js +338 -0
  117. package/lib/providers/replay.d.ts +4 -0
  118. package/lib/providers/replay.js +37 -0
  119. package/lib/providers/reset-redemption.d.ts +20 -0
  120. package/lib/providers/reset-redemption.js +70 -0
  121. package/lib/tools/image-generate.d.ts +128 -0
  122. package/lib/tools/image-generate.js +438 -0
  123. package/lib/tools/registration.d.ts +16 -0
  124. package/lib/tools/registration.js +25 -0
  125. package/lib/tools/usage.d.ts +62 -0
  126. package/lib/tools/usage.js +89 -0
  127. package/lib/tools/video-generate.d.ts +89 -0
  128. package/lib/tools/video-generate.js +257 -0
  129. package/lib/tools/x-search.d.ts +58 -0
  130. package/lib/tools/x-search.js +197 -0
  131. package/lib/translate/anthropic.d.ts +180 -0
  132. package/lib/translate/anthropic.js +534 -0
  133. package/lib/translate/antigravity-images.d.ts +11 -0
  134. package/lib/translate/antigravity-images.js +62 -0
  135. package/lib/translate/antigravity-schema.d.ts +4 -0
  136. package/lib/translate/antigravity-schema.js +145 -0
  137. package/lib/translate/antigravity-thinking.d.ts +5 -0
  138. package/lib/translate/antigravity-thinking.js +44 -0
  139. package/lib/translate/antigravity.d.ts +120 -0
  140. package/lib/translate/antigravity.js +412 -0
  141. package/lib/translate/chat-completions.d.ts +120 -0
  142. package/lib/translate/chat-completions.js +382 -0
  143. package/lib/translate/resolved.d.ts +98 -0
  144. package/lib/translate/resolved.js +186 -0
  145. package/lib/translate/responses.d.ts +192 -0
  146. package/lib/translate/responses.js +443 -0
  147. package/lib/translate/sse.d.ts +21 -0
  148. package/lib/translate/sse.js +62 -0
  149. package/lib/usage/headers.d.ts +21 -0
  150. package/lib/usage/headers.js +64 -0
  151. package/lib/usage/pace.d.ts +53 -0
  152. package/lib/usage/pace.js +48 -0
  153. package/lib/usage/service.d.ts +64 -0
  154. package/lib/usage/service.js +104 -0
  155. package/lib/usage-providers/common.d.ts +23 -0
  156. package/lib/usage-providers/common.js +28 -0
  157. package/lib/usage-providers/index.d.ts +42 -0
  158. package/lib/usage-providers/index.js +71 -0
  159. package/lib/usage-providers/keys.d.ts +11 -0
  160. package/lib/usage-providers/keys.js +52 -0
  161. package/lib/usage-providers/kimi.d.ts +4 -0
  162. package/lib/usage-providers/kimi.js +52 -0
  163. package/lib/usage-providers/ollama-cloud.d.ts +4 -0
  164. package/lib/usage-providers/ollama-cloud.js +42 -0
  165. package/lib/usage-providers/zai.d.ts +5 -0
  166. package/lib/usage-providers/zai.js +53 -0
  167. package/locale/en.json +6 -0
  168. package/locale/zh.json +6 -0
  169. package/package.json +115 -4
@@ -0,0 +1,501 @@
1
+ /**
2
+ * The `subscriptions-auth` host RPC endpoints the web Settings page drives.
3
+ * They are mounted as exact POST Fetch routes on the shared `/api` channel
4
+ * (`/api/subscriptions-auth.<endpoint>`) and registered only when a host
5
+ * `connection` service exists (the web profile); headless compositions load
6
+ * the plugin without it. All business outcomes are returned as RpcResult
7
+ * values; handlers never throw.
8
+ */
9
+ import { AttachmentId } from '@deepseek-ai/dsh-attachment';
10
+ import { PROVIDER_IDS, PROVIDER_SHORT_NAMES } from './store.js';
11
+ import { isKeyProviderId, KEY_ACCOUNT, KEY_PROVIDER_IDS } from '../usage-providers/index.js';
12
+ /**
13
+ * Endpoint-name prefix under the shared `/api` channel: endpoint `status`
14
+ * is served at `/api/subscriptions-auth.status`. The browser half calls
15
+ * `rpc.call('/api', 'subscriptions-auth.status', payload)`.
16
+ */
17
+ export const SUBSCRIPTIONS_AUTH_PREFIX = 'subscriptions-auth.';
18
+ /**
19
+ * Every endpoint {@link dispatch} answers; each gets one exact Fetch route.
20
+ * Kept in one place so the route table and the switch cannot drift apart.
21
+ */
22
+ export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
23
+ 'providerSettings', 'setProviderSettings',
24
+ 'status', 'login', 'manual', 'cancel', 'logout', 'setDefault', 'usage',
25
+ 'image', 'video', 'prepareReset', 'consumeReset',
26
+ 'speed', 'setSpeed',
27
+ 'proxyGet', 'proxySet', 'proxyTest',
28
+ 'keysSet', 'keysRemove',
29
+ 'modelDefaults', 'setModelDefault',
30
+ ];
31
+ /** Media types the attachment store accepts (ImageMediaType). */
32
+ const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
33
+ /** Bare MP4 file names the `video` endpoint accepts (no path separators). */
34
+ const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
35
+ /** Payload carried no usable provider id — an RPC client bug, not a server failure. */
36
+ export class BadRequest extends Error {
37
+ }
38
+ function readEnvelope(body) {
39
+ if (typeof body !== 'object' || body === null)
40
+ return undefined;
41
+ const record = body;
42
+ if (record.type !== 'client-request' || typeof record.rpcId !== 'string' || typeof record.method !== 'string')
43
+ return undefined;
44
+ return { type: 'client-request', rpcId: record.rpcId, method: record.method, payload: record.payload };
45
+ }
46
+ function serverResponse(rpcId, result) {
47
+ return Response.json({ type: 'server-response', rpcId, result });
48
+ }
49
+ /**
50
+ * Wrap one endpoint's RPC handler as an exact Fetch route: decode the
51
+ * `client-request` envelope the browser rpc caller posts, run the handler,
52
+ * and answer with the matching `server-response` envelope — the same wire
53
+ * contract the dedicated-channel bridge used to apply, reproduced here so
54
+ * `rpc.call('/api', 'subscriptions-auth.<endpoint>', payload)` keeps working
55
+ * unchanged on the browser side.
56
+ */
57
+ function fetchRouteFor(endpoint, handler) {
58
+ const method = `${SUBSCRIPTIONS_AUTH_PREFIX}${endpoint}`;
59
+ return {
60
+ path: `/api/${method}`,
61
+ methods: ['POST'],
62
+ requestBody: 'buffered',
63
+ fetch: async (request) => {
64
+ if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
65
+ return new Response('content type must be application/json', { status: 415 });
66
+ }
67
+ let body;
68
+ try {
69
+ body = await request.json();
70
+ }
71
+ catch {
72
+ return new Response('body is not JSON', { status: 400 });
73
+ }
74
+ const envelope = readEnvelope(body);
75
+ if (envelope === undefined) {
76
+ const rawId = body?.rpcId;
77
+ return serverResponse(typeof rawId === 'string' ? rawId : 'invalid-request', {
78
+ ok: false,
79
+ error: { code: 'gateway/bad-request', message: 'invalid client-request message', details: { issues: [] } },
80
+ });
81
+ }
82
+ if (envelope.method !== method) {
83
+ return serverResponse(envelope.rpcId, {
84
+ ok: false,
85
+ error: {
86
+ code: 'gateway/bad-request',
87
+ message: `method ${JSON.stringify(envelope.method)} does not match endpoint ${JSON.stringify(method)}`,
88
+ details: { issues: [] },
89
+ },
90
+ });
91
+ }
92
+ return serverResponse(envelope.rpcId, await handler(endpoint, envelope.payload, request.signal));
93
+ },
94
+ };
95
+ }
96
+ function ok(value) {
97
+ return { ok: true, value };
98
+ }
99
+ function failure(error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ if (error instanceof BadRequest) {
102
+ // The issues array is zod-shaped upstream; this channel validates by hand.
103
+ return { ok: false, error: { code: 'bad-request', message, details: { issues: [] } } };
104
+ }
105
+ return { ok: false, error: { code: 'internal', message, details: {} } };
106
+ }
107
+ function readProvider(payload) {
108
+ if (typeof payload !== 'object' || payload === null)
109
+ throw new BadRequest('payload must be an object');
110
+ const provider = payload.provider;
111
+ if (typeof provider !== 'string' || !PROVIDER_IDS.includes(provider)) {
112
+ throw new BadRequest(`payload.provider must be one of ${PROVIDER_IDS.join(', ')}`);
113
+ }
114
+ return provider;
115
+ }
116
+ /** An OAuth provider or a key provider, for endpoints both kinds share. */
117
+ function readUsageProvider(payload) {
118
+ if (typeof payload === 'object' && payload !== null && isKeyProviderId(payload.provider)) {
119
+ return payload.provider;
120
+ }
121
+ return readProvider(payload);
122
+ }
123
+ function readKeyProvider(payload) {
124
+ const provider = typeof payload === 'object' && payload !== null ? payload.provider : undefined;
125
+ if (!isKeyProviderId(provider))
126
+ throw new BadRequest(`payload.provider must be one of ${KEY_PROVIDER_IDS.join(', ')}`);
127
+ return provider;
128
+ }
129
+ function readString(payload, field) {
130
+ const value = payload[field];
131
+ if (typeof value !== 'string' || value.length === 0) {
132
+ throw new BadRequest(`payload.${field} must be a non-empty string`);
133
+ }
134
+ return value;
135
+ }
136
+ /** Validate the `setModelDefault` endpoint's payload. */
137
+ function readModelDefaultInput(payload) {
138
+ const provider = readProvider(payload);
139
+ const model = readString(payload, 'model');
140
+ const record = payload;
141
+ let effort;
142
+ if (record.effort !== undefined) {
143
+ if (typeof record.effort !== 'string' || record.effort.length === 0) {
144
+ throw new BadRequest('payload.effort must be a non-empty string when present');
145
+ }
146
+ effort = record.effort;
147
+ }
148
+ return {
149
+ provider,
150
+ model,
151
+ ...(effort === undefined ? {} : { effort }),
152
+ };
153
+ }
154
+ /** Validate the optional Claude login method. */
155
+ function readLoginMethod(payload, provider) {
156
+ const method = payload.method;
157
+ if (method === undefined)
158
+ return undefined;
159
+ if (provider !== 'claude')
160
+ throw new BadRequest('payload.method is only valid for claude');
161
+ if (method !== 'oauth' && method !== 'keychain') {
162
+ throw new BadRequest('payload.method must be "oauth" or "keychain"');
163
+ }
164
+ return method;
165
+ }
166
+ /** Validate the `setSpeed` endpoint's tier. */
167
+ function readSpeedTier(payload) {
168
+ const tier = payload.tier;
169
+ if (tier !== 'standard' && tier !== 'fast') {
170
+ throw new BadRequest('payload.tier must be "standard" or "fast"');
171
+ }
172
+ return tier;
173
+ }
174
+ /** Validate the `image` endpoint's payload into a full attachment reference. */
175
+ function readImageRef(payload) {
176
+ if (typeof payload !== 'object' || payload === null)
177
+ throw new BadRequest('payload must be an object');
178
+ const record = payload;
179
+ const attachmentId = record.attachmentId;
180
+ if (typeof attachmentId !== 'string' || attachmentId.length === 0) {
181
+ throw new BadRequest('payload.attachmentId must be a non-empty string');
182
+ }
183
+ const mediaType = record.mediaType;
184
+ if (typeof mediaType !== 'string' || !IMAGE_MEDIA_TYPES.includes(mediaType)) {
185
+ throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
186
+ }
187
+ for (const field of ['bytes', 'width', 'height']) {
188
+ const value = record[field];
189
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
190
+ throw new BadRequest(`payload.${field} must be a positive integer`);
191
+ }
192
+ }
193
+ const name = record.name;
194
+ if (name !== undefined && typeof name !== 'string') {
195
+ throw new BadRequest('payload.name must be a string when present');
196
+ }
197
+ return {
198
+ attachmentId: AttachmentId(attachmentId),
199
+ mediaType: mediaType,
200
+ bytes: record.bytes,
201
+ width: record.width,
202
+ height: record.height,
203
+ ...name === undefined ? {} : { name: name },
204
+ };
205
+ }
206
+ /**
207
+ * Validate the `video` endpoint's payload into a bare file name. Rejecting
208
+ * anything with a path separator (the pattern allows none) pins every read
209
+ * inside the plugin's videos directory.
210
+ */
211
+ function readVideoName(payload) {
212
+ if (typeof payload !== 'object' || payload === null)
213
+ throw new BadRequest('payload must be an object');
214
+ const name = payload.name;
215
+ if (typeof name !== 'string' || !VIDEO_NAME_PATTERN.test(name)) {
216
+ throw new BadRequest('payload.name must be a bare .mp4 file name');
217
+ }
218
+ return name;
219
+ }
220
+ /** Validate the usage endpoint's optional read reason. */
221
+ function readReason(payload) {
222
+ const reason = payload.reason;
223
+ if (reason === undefined)
224
+ return undefined;
225
+ if (reason !== 'poll' && reason !== 'open' && reason !== 'manual') {
226
+ throw new BadRequest('payload.reason must be "poll", "open" or "manual" when present');
227
+ }
228
+ return reason;
229
+ }
230
+ /** Validate the status endpoint's optional session id. */
231
+ function readOptionalSessionId(payload) {
232
+ if (payload === null || typeof payload !== 'object')
233
+ return undefined;
234
+ const sessionId = payload.sessionId;
235
+ if (sessionId === undefined)
236
+ return undefined;
237
+ if (typeof sessionId !== 'string' || sessionId.length === 0)
238
+ throw new BadRequest('payload.sessionId must be a non-empty string when present');
239
+ return sessionId;
240
+ }
241
+ /** Validate the usage/model catalog endpoints' optional force flag. */
242
+ function readForce(payload) {
243
+ if (typeof payload !== 'object' || payload === null)
244
+ return false;
245
+ const force = payload.force;
246
+ if (force === undefined)
247
+ return false;
248
+ if (typeof force !== 'boolean')
249
+ throw new BadRequest('payload.force must be a boolean when present');
250
+ return force;
251
+ }
252
+ /** Validate the session id both speed endpoints carry. */
253
+ function readSessionId(payload) {
254
+ if (typeof payload !== 'object' || payload === null)
255
+ throw new BadRequest('payload must be an object');
256
+ return readString(payload, 'sessionId');
257
+ }
258
+ /** Validate a `proxySet` payload into a shape `ProxyInput` accepts. */
259
+ function readProxyInput(payload) {
260
+ if (typeof payload !== 'object' || payload === null)
261
+ throw new BadRequest('payload must be an object');
262
+ const record = payload;
263
+ if (typeof record.enabled !== 'boolean')
264
+ throw new BadRequest('payload.enabled must be a boolean');
265
+ if (typeof record.url !== 'string')
266
+ throw new BadRequest('payload.url must be a string');
267
+ let username;
268
+ if (record.username !== undefined) {
269
+ if (typeof record.username !== 'string')
270
+ throw new BadRequest('payload.username must be a string when present');
271
+ username = record.username;
272
+ }
273
+ let password;
274
+ if (record.password !== undefined) {
275
+ if (record.password !== null && typeof record.password !== 'string') {
276
+ throw new BadRequest('payload.password must be a string or null when present');
277
+ }
278
+ password = record.password;
279
+ }
280
+ let bypass;
281
+ if (record.bypass !== undefined) {
282
+ if (!Array.isArray(record.bypass) || record.bypass.some(entry => typeof entry !== 'string')) {
283
+ throw new BadRequest('payload.bypass must be an array of strings when present');
284
+ }
285
+ bypass = record.bypass;
286
+ }
287
+ return {
288
+ enabled: record.enabled,
289
+ url: record.url,
290
+ ...username === undefined ? {} : { username },
291
+ ...password === undefined ? {} : { password },
292
+ ...bypass === undefined ? {} : { bypass },
293
+ };
294
+ }
295
+ /** Validate a `proxyTest` payload (the destination URL and an optional draft). */
296
+ function readProxyTestPayload(payload) {
297
+ if (typeof payload !== 'object' || payload === null)
298
+ return {};
299
+ const record = payload;
300
+ const url = record.url;
301
+ if (url === undefined && record.proxy === undefined)
302
+ return {};
303
+ if (url !== undefined && (typeof url !== 'string' || url.length === 0)) {
304
+ throw new BadRequest('payload.url must be a non-empty string when present');
305
+ }
306
+ let proxy;
307
+ if (record.proxy !== undefined) {
308
+ if (typeof record.proxy !== 'object' || record.proxy === null) {
309
+ throw new BadRequest('payload.proxy must be an object when present');
310
+ }
311
+ const draftRecord = record.proxy;
312
+ if (typeof draftRecord.url !== 'string' || draftRecord.url.length === 0) {
313
+ throw new BadRequest('payload.proxy.url must be a non-empty string');
314
+ }
315
+ let username;
316
+ if (draftRecord.username !== undefined) {
317
+ if (typeof draftRecord.username !== 'string') {
318
+ throw new BadRequest('payload.proxy.username must be a string when present');
319
+ }
320
+ username = draftRecord.username;
321
+ }
322
+ let password;
323
+ if (draftRecord.password !== undefined) {
324
+ if (typeof draftRecord.password !== 'string') {
325
+ throw new BadRequest('payload.proxy.password must be a string when present');
326
+ }
327
+ password = draftRecord.password;
328
+ }
329
+ proxy = {
330
+ url: draftRecord.url,
331
+ ...username === undefined ? {} : { username },
332
+ ...password === undefined ? {} : { password },
333
+ };
334
+ }
335
+ return {
336
+ ...url === undefined ? {} : { url },
337
+ ...proxy === undefined ? {} : { proxy },
338
+ };
339
+ }
340
+ async function dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal, providerSettings) {
341
+ switch (endpoint) {
342
+ case 'providerSettings':
343
+ if (!providerSettings)
344
+ throw new BadRequest('provider settings are unavailable');
345
+ return ok(await providerSettings.get(readProvider(payload), readForce(payload)));
346
+ case 'setProviderSettings': {
347
+ if (!providerSettings)
348
+ throw new BadRequest('provider settings are unavailable');
349
+ const provider = readProvider(payload);
350
+ await providerSettings.set(provider, payload.settings);
351
+ return ok({ ok: true });
352
+ }
353
+ case 'status': {
354
+ const sessionId = readOptionalSessionId(payload);
355
+ // One provider's failure (a corrupt store entry, a broken flow) must not
356
+ // blind the whole page: it degrades to an error detail on that provider
357
+ // while the others still report their real status.
358
+ const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider, sessionId).catch((error) => ({
359
+ busy: false,
360
+ accounts: [],
361
+ detail: error instanceof Error ? error.message : String(error),
362
+ }))]));
363
+ const sizes = await Promise.all(PROVIDER_IDS.map(provider => providerSettings?.catalogSize?.(provider).catch(() => undefined)));
364
+ const keyViews = await controller.keyProviders?.() ?? [];
365
+ // A key provider with a stored key reports like a one-account provider, so the Client treats it as any metered provider.
366
+ const keyEntries = keyViews.filter(view => view.configured).map(view => [view.id, {
367
+ busy: false, shortName: view.name, metered: true,
368
+ accounts: [{ key: KEY_ACCOUNT, account: `…${view.tail ?? ''}`, isDefault: true }],
369
+ }]);
370
+ return ok({ usageRevision: controller.usageRevision?.() ?? 0, keyProviders: keyViews, providers: Object.fromEntries([...keyEntries, ...entries.map(([provider, status], index) => {
371
+ const displayName = providerSettings?.displayName?.(provider);
372
+ const visibleModels = providerSettings?.visibleModels?.(provider);
373
+ const catalogSize = sizes[index];
374
+ return [provider, { ...status, shortName: PROVIDER_SHORT_NAMES[provider], metered: controller.hasUsage?.(provider) ?? false,
375
+ ...(displayName ? { displayName } : {}),
376
+ ...(visibleModels === undefined ? {} : { visibleModels }),
377
+ ...(catalogSize === undefined ? {} : { catalogSize }) }];
378
+ })]) });
379
+ }
380
+ case 'login': {
381
+ const provider = readProvider(payload);
382
+ return ok(await controller.login(provider, readLoginMethod(payload, provider)));
383
+ }
384
+ case 'manual': {
385
+ const provider = readProvider(payload);
386
+ await controller.manual(provider, readString(payload, 'input'));
387
+ return ok({ ok: true });
388
+ }
389
+ case 'cancel':
390
+ await controller.cancel(readProvider(payload));
391
+ return ok({ ok: true });
392
+ case 'logout': {
393
+ const provider = readProvider(payload);
394
+ await controller.logout(provider, readString(payload, 'account'));
395
+ return ok({ ok: true });
396
+ }
397
+ case 'setDefault': {
398
+ const provider = readProvider(payload);
399
+ await controller.setDefault(provider, readString(payload, 'account'));
400
+ return ok({ ok: true });
401
+ }
402
+ case 'usage': {
403
+ const provider = readUsageProvider(payload);
404
+ return ok(await controller.usage(provider, readString(payload, 'account'), signal, readForce(payload), readReason(payload)));
405
+ }
406
+ case 'keysSet': {
407
+ if (!controller.setKey)
408
+ throw new BadRequest('API key usage is unavailable');
409
+ await controller.setKey(readKeyProvider(payload), readString(payload, 'key'));
410
+ return ok({ ok: true });
411
+ }
412
+ case 'keysRemove': {
413
+ if (!controller.removeKey)
414
+ throw new BadRequest('API key usage is unavailable');
415
+ await controller.removeKey(readKeyProvider(payload));
416
+ return ok({ ok: true });
417
+ }
418
+ case 'prepareReset':
419
+ if (!controller.prepareReset)
420
+ throw new BadRequest('Reset redemption unavailable');
421
+ return ok(await controller.prepareReset(readString(payload, 'account'), signal));
422
+ case 'consumeReset':
423
+ if (!controller.consumeReset)
424
+ throw new BadRequest('Reset redemption unavailable');
425
+ await controller.consumeReset(readString(payload, 'account'), readString(payload, 'ticket'), signal);
426
+ return ok({ ok: true });
427
+ case 'image':
428
+ return ok(await controller.readImage(readImageRef(payload), signal));
429
+ case 'video':
430
+ return ok(await controller.readVideo(readVideoName(payload), signal));
431
+ case 'speed':
432
+ return ok(await speed.speed(readSessionId(payload), payload.model === undefined ? undefined : readString(payload, 'model')));
433
+ case 'setSpeed':
434
+ await speed.setSpeed(readSessionId(payload), readSpeedTier(payload), payload.model === undefined ? undefined : readString(payload, 'model'));
435
+ return ok({ ok: true });
436
+ case 'proxyGet':
437
+ if (proxy === undefined)
438
+ throw new BadRequest('proxy configuration is unavailable');
439
+ return ok(await proxy.get());
440
+ case 'proxySet':
441
+ if (proxy === undefined)
442
+ throw new BadRequest('proxy configuration is unavailable');
443
+ return ok(await proxy.set(readProxyInput(payload)));
444
+ case 'proxyTest':
445
+ if (proxy === undefined)
446
+ throw new BadRequest('proxy configuration is unavailable');
447
+ return ok(await proxy.test(readProxyTestPayload(payload)));
448
+ case 'modelDefaults': {
449
+ if (modelDefaults === undefined)
450
+ throw new BadRequest('model defaults are unavailable');
451
+ return ok(await modelDefaults.catalog(readForce(payload)));
452
+ }
453
+ case 'setModelDefault':
454
+ if (modelDefaults === undefined)
455
+ throw new BadRequest('model defaults are unavailable');
456
+ {
457
+ const input = readModelDefaultInput(payload);
458
+ await modelDefaults.set(input.provider, input.model, input.effort);
459
+ }
460
+ return ok({ ok: true });
461
+ default:
462
+ throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
463
+ }
464
+ }
465
+ /**
466
+ * Register the `/subscriptions-auth` RPC channel when a host connection exists.
467
+ * @param ctx - the plugin context (headless profiles have no `connection`).
468
+ * @param controller - the auth operations backing the endpoints.
469
+ * @param speed - the per-session speed-tier state backing the Speed toggle.
470
+ * @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
471
+ * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
472
+ */
473
+ export function registerAuthRpc(ctx, controller, speed, proxy = undefined, modelDefaults = undefined, providerSettings = undefined) {
474
+ // `connection` is not in this plugin's inject list (headless compositions
475
+ // lack it), so its startup order is unconstrained: defer registration until
476
+ // the service exists instead of probing once at apply time.
477
+ //
478
+ // Exact Fetch routes under `/api` rather than a dedicated `rpc.handle`
479
+ // channel: since dsh 0.1.5 the connection plugin no longer injects
480
+ // `webServer` itself, and `rpc.handle` resolves `webServer` through the
481
+ // connection plugin's own fiber, so every dedicated channel registration
482
+ // throws `cannot get property "webServer" without inject`. The `/api`
483
+ // route is mounted by the connection plugin (with its own webServer scope)
484
+ // and applies the same trust fence and browser authentication, so exact
485
+ // routes below it work on both dsh lines.
486
+ ctx.inject(['connection'], (ctx) => {
487
+ const connection = ctx.get('connection');
488
+ const handler = async (endpoint, payload, signal) => {
489
+ try {
490
+ return await dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal, providerSettings);
491
+ }
492
+ catch (error) {
493
+ return failure(error);
494
+ }
495
+ };
496
+ const register = connection.fetch.register;
497
+ for (const endpoint of SUBSCRIPTIONS_AUTH_ENDPOINTS) {
498
+ ctx.effect(() => register(fetchRouteFor(endpoint, handler)), `dsh-plugin-subscription-usage: /api/${SUBSCRIPTIONS_AUTH_PREFIX}${endpoint} route`);
499
+ }
500
+ });
501
+ }