dsh-plugin-subscription-oauth 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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -2
  3. package/cordis.patch.yml +13 -0
  4. package/lib/auth/device-flow.d.ts +112 -0
  5. package/lib/auth/device-flow.js +319 -0
  6. package/lib/auth/device-id.d.ts +31 -0
  7. package/lib/auth/device-id.js +58 -0
  8. package/lib/auth/flow.d.ts +33 -0
  9. package/lib/auth/flow.js +12 -0
  10. package/lib/auth/glm-flow.d.ts +99 -0
  11. package/lib/auth/glm-flow.js +194 -0
  12. package/lib/auth/headers.d.ts +61 -0
  13. package/lib/auth/headers.js +106 -0
  14. package/lib/auth/pkce-flow.d.ts +78 -0
  15. package/lib/auth/pkce-flow.js +205 -0
  16. package/lib/auth/provider-controller.d.ts +241 -0
  17. package/lib/auth/provider-controller.js +502 -0
  18. package/lib/auth/registry.d.ts +91 -0
  19. package/lib/auth/registry.js +149 -0
  20. package/lib/auth/rpc.d.ts +31 -0
  21. package/lib/auth/rpc.js +200 -0
  22. package/lib/auth/store.d.ts +99 -0
  23. package/lib/auth/store.js +213 -0
  24. package/lib/auth/tokens.d.ts +76 -0
  25. package/lib/auth/tokens.js +103 -0
  26. package/lib/client/SubscriptionsSection.d.ts +34 -0
  27. package/lib/client/SubscriptionsSection.js +742 -0
  28. package/lib/client/index.d.ts +36 -0
  29. package/lib/client/index.js +47 -0
  30. package/lib/client/locales.d.ts +187 -0
  31. package/lib/client/locales.js +185 -0
  32. package/lib/client/styles.d.ts +18 -0
  33. package/lib/client/styles.js +257 -0
  34. package/lib/client/subscriptions-rpc.d.ts +24 -0
  35. package/lib/client/subscriptions-rpc.js +44 -0
  36. package/lib/client.js +2078 -0
  37. package/lib/client.js.map +1 -0
  38. package/lib/constants.d.ts +79 -0
  39. package/lib/constants.js +91 -0
  40. package/lib/errors.d.ts +28 -0
  41. package/lib/errors.js +34 -0
  42. package/lib/http.d.ts +24 -0
  43. package/lib/http.js +98 -0
  44. package/lib/index.d.ts +57 -0
  45. package/lib/index.js +185 -0
  46. package/lib/protocol.d.ts +316 -0
  47. package/lib/protocol.js +29 -0
  48. package/lib/providers/antigravity-catalog.d.ts +53 -0
  49. package/lib/providers/antigravity-catalog.js +269 -0
  50. package/lib/providers/antigravity.d.ts +331 -0
  51. package/lib/providers/antigravity.js +821 -0
  52. package/lib/providers/auto.d.ts +151 -0
  53. package/lib/providers/auto.js +351 -0
  54. package/lib/providers/catalog.d.ts +69 -0
  55. package/lib/providers/catalog.js +78 -0
  56. package/lib/providers/common.d.ts +51 -0
  57. package/lib/providers/common.js +169 -0
  58. package/lib/providers/glm-provider.d.ts +41 -0
  59. package/lib/providers/glm-provider.js +165 -0
  60. package/lib/providers/glm.d.ts +159 -0
  61. package/lib/providers/glm.js +445 -0
  62. package/lib/providers/grok.d.ts +224 -0
  63. package/lib/providers/grok.js +700 -0
  64. package/lib/providers/kimi-provider.d.ts +45 -0
  65. package/lib/providers/kimi-provider.js +148 -0
  66. package/lib/providers/kimi.d.ts +151 -0
  67. package/lib/providers/kimi.js +400 -0
  68. package/lib/providers/responses-stream.d.ts +58 -0
  69. package/lib/providers/responses-stream.js +115 -0
  70. package/lib/providers/usage.d.ts +65 -0
  71. package/lib/providers/usage.js +309 -0
  72. package/lib/settings.d.ts +122 -0
  73. package/lib/settings.js +333 -0
  74. package/lib/translate/antigravity-schema.d.ts +24 -0
  75. package/lib/translate/antigravity-schema.js +174 -0
  76. package/lib/translate/antigravity.d.ts +184 -0
  77. package/lib/translate/antigravity.js +419 -0
  78. package/lib/translate/responses.d.ts +156 -0
  79. package/lib/translate/responses.js +395 -0
  80. package/lib/translate/sse.d.ts +27 -0
  81. package/lib/translate/sse.js +82 -0
  82. package/package.json +126 -4
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Plan and allowance reads.
3
+ *
4
+ * Both providers state what a subscription still has left on their own
5
+ * endpoints, in their own shapes. This module normalises those shapes into the
6
+ * one {@link UsageRead} the Settings page renders, and nothing here ever throws
7
+ * for a merely unsupported or unavailable endpoint: a provider that states no
8
+ * usage simply yields `undefined`.
9
+ *
10
+ * @module dsh-plugin-subscription-oauth/providers/usage
11
+ */
12
+ import { attributedHeaders } from '../auth/headers.js';
13
+ import { proxiedFetch } from '../http.js';
14
+ /** True for a plain object usable as a JSON record. */
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ /** A finite number, from either a number or a numeric string. */
19
+ function toNumber(value) {
20
+ if (typeof value === 'number' && Number.isFinite(value))
21
+ return value;
22
+ if (typeof value === 'string' && value.trim().length > 0) {
23
+ const parsed = Number(value);
24
+ if (Number.isFinite(parsed))
25
+ return parsed;
26
+ }
27
+ return undefined;
28
+ }
29
+ /** A percentage clamped to 0–100, ignoring nonsense. */
30
+ function toPercent(value) {
31
+ const parsed = toNumber(value);
32
+ if (parsed === undefined || parsed < 0)
33
+ return undefined;
34
+ return Math.min(100, parsed);
35
+ }
36
+ /**
37
+ * An instant from an RFC 3339 string or a numeric stamp.
38
+ *
39
+ * A bare number below 1e12 is read as seconds: every provider here states epoch
40
+ * milliseconds or an ISO string, and a seconds value would otherwise land in 1970.
41
+ */
42
+ function toInstant(value) {
43
+ if (typeof value === 'string' && value.length > 0) {
44
+ const parsed = Date.parse(value);
45
+ return Number.isFinite(parsed) ? parsed : undefined;
46
+ }
47
+ const numeric = toNumber(value);
48
+ if (numeric === undefined || numeric <= 0)
49
+ return undefined;
50
+ return numeric < 1e12 ? numeric * 1000 : numeric;
51
+ }
52
+ /** The first present reset instant among a row's usual field names. */
53
+ function resetInstant(row) {
54
+ for (const key of ['resetsAt', 'resets_at', 'resetAt', 'reset_at', 'end', 'endsAt', 'expiresAt']) {
55
+ const instant = toInstant(row[key]);
56
+ if (instant !== undefined)
57
+ return instant;
58
+ }
59
+ const relative = toNumber(row.retry_after_seconds ?? row.retry_after);
60
+ if (relative !== undefined && relative > 0)
61
+ return Date.now() + relative * 1000;
62
+ return undefined;
63
+ }
64
+ /** Read a `{limit, used|remaining, ...}` row, or a direct utilisation field. */
65
+ function readRow(value, resetFallback) {
66
+ if (!isRecord(value))
67
+ return undefined;
68
+ const resetsAt = resetInstant(value) ?? (resetFallback === undefined ? undefined : resetInstant(resetFallback));
69
+ const limit = toNumber(value.limit);
70
+ const usedDirect = toNumber(value.used);
71
+ if (limit !== undefined && limit > 0 && usedDirect !== undefined) {
72
+ return {
73
+ usedPercent: Math.min(100, (usedDirect / limit) * 100),
74
+ used: usedDirect,
75
+ limit,
76
+ ...resetsAt === undefined ? {} : { resetsAt },
77
+ };
78
+ }
79
+ if (limit !== undefined && limit > 0) {
80
+ const remaining = toNumber(value.remaining);
81
+ if (remaining !== undefined) {
82
+ const used = limit - remaining;
83
+ return {
84
+ usedPercent: Math.min(100, (used / limit) * 100),
85
+ used,
86
+ limit,
87
+ ...resetsAt === undefined ? {} : { resetsAt },
88
+ };
89
+ }
90
+ }
91
+ const percent = toPercent(value.utilization ?? value.percent ?? value.usedPercent ?? value.used_percent);
92
+ if (percent === undefined)
93
+ return undefined;
94
+ return {
95
+ usedPercent: percent,
96
+ ...usedDirect === undefined ? {} : { used: usedDirect },
97
+ ...limit === undefined ? {} : { limit },
98
+ ...resetsAt === undefined ? {} : { resetsAt },
99
+ };
100
+ }
101
+ /** Turn one row into a wire window. */
102
+ function toWindow(label, row) {
103
+ return {
104
+ label,
105
+ usedPercent: row.usedPercent,
106
+ ...row.used === undefined ? {} : { used: row.used },
107
+ ...row.limit === undefined ? {} : { limit: row.limit },
108
+ ...row.resetsAt === undefined ? {} : { resetsAt: row.resetsAt },
109
+ };
110
+ }
111
+ /** The `/usages` endpoint on the Kimi Code base. */
112
+ export const KIMI_USAGE_PATH = '/usages';
113
+ /** The Grok Build CLI chat proxy's billing endpoint. */
114
+ export const GROK_BILLING_URL = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits';
115
+ /** Subscription tier encoded in a Grok access token's `tier` claim. */
116
+ const GROK_TIER_NAMES = {
117
+ 0: 'Free',
118
+ 1: 'SuperGrok',
119
+ 2: 'X Basic',
120
+ 3: 'X Premium',
121
+ 4: 'X Premium+',
122
+ 5: 'SuperGrok Heavy',
123
+ 6: 'SuperGrok Lite',
124
+ 7: 'SuperGrok Plus',
125
+ };
126
+ /** Unwrap a Kimi usage envelope that nests the payload under `data`. */
127
+ function unwrapKimiUsage(value) {
128
+ if (!isRecord(value))
129
+ return undefined;
130
+ const nested = value.data;
131
+ if (!isRecord(nested))
132
+ return value;
133
+ const usable = (field) => field !== undefined && field !== null;
134
+ const outer = usable(value.usage) || usable(value.limits) || usable(value.totalQuota);
135
+ const inner = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota);
136
+ return !outer && inner ? nested : value;
137
+ }
138
+ /** The label a Kimi limit row carries, lowercased. */
139
+ function kimiLimitLabel(item, detail) {
140
+ return [item.name, item.title, item.scope, detail.name, detail.title]
141
+ .filter((value) => typeof value === 'string')
142
+ .join(' ')
143
+ .toLowerCase();
144
+ }
145
+ /** Whether a Kimi limit row describes the rolling five-hour window. */
146
+ function isFiveHourLimit(item, detail, window) {
147
+ const duration = toNumber(window.duration ?? item.duration ?? detail.duration);
148
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
149
+ if (unit.includes('MINUTE') && duration === 300)
150
+ return true;
151
+ if (unit.includes('HOUR') && duration === 5)
152
+ return true;
153
+ return /(^|\b)5\s*(?:h|hour)/.test(kimiLimitLabel(item, detail));
154
+ }
155
+ /** Whether a Kimi limit row describes the weekly window. */
156
+ function isWeeklyLimit(item, detail, window) {
157
+ const duration = toNumber(window.duration ?? item.duration ?? detail.duration);
158
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
159
+ if (unit.includes('DAY') && duration === 7)
160
+ return true;
161
+ if (unit.includes('HOUR') && duration === 168)
162
+ return true;
163
+ return /weekly|7\s*(?:d|day)/.test(kimiLimitLabel(item, detail));
164
+ }
165
+ /**
166
+ * Read the Kimi Code plan's rolling windows.
167
+ *
168
+ * `GET <base>/usages` states a five-hour window and a weekly window, either of
169
+ * which may be absent depending on the plan. The payload also carries a
170
+ * whole-subscription `totalQuota`, which is deliberately not published as a third
171
+ * meter: what a Kimi Code plan is metered against is the rolling and weekly
172
+ * windows, and a bare credit total next to them reads as a second, contradictory
173
+ * allowance.
174
+ *
175
+ * @param input - endpoint base, bearer, and an injectable fetch.
176
+ * @returns the windows, or undefined when the endpoint states none.
177
+ */
178
+ export async function readKimiUsage(input) {
179
+ const fetchFn = input.fetchFn ?? proxiedFetch;
180
+ let response;
181
+ try {
182
+ response = await fetchFn(`${input.apiBase}${KIMI_USAGE_PATH}`, {
183
+ headers: attributedHeaders({ 'authorization': `Bearer ${input.accessToken}`, 'accept': 'application/json' }),
184
+ });
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ if (!response.ok)
190
+ return undefined;
191
+ const payload = await response.json().catch(() => undefined);
192
+ const body = unwrapKimiUsage(payload);
193
+ if (body === undefined)
194
+ return undefined;
195
+ // The whole-subscription row is the weekly window; a `limits` entry may
196
+ // restate it (or add the rolling five-hour one) with its own reset.
197
+ let weekly = readRow(body.usage);
198
+ let fiveHour;
199
+ if (Array.isArray(body.limits)) {
200
+ for (const raw of body.limits) {
201
+ if (!isRecord(raw))
202
+ continue;
203
+ const detail = isRecord(raw.detail) ? raw.detail : raw;
204
+ const window = isRecord(raw.window) ? raw.window : {};
205
+ if (fiveHour === undefined && isFiveHourLimit(raw, detail, window)) {
206
+ fiveHour = readRow(detail, window);
207
+ }
208
+ if (weekly === undefined && isWeeklyLimit(raw, detail, window)) {
209
+ weekly = readRow(detail, window);
210
+ }
211
+ if (fiveHour !== undefined && weekly !== undefined)
212
+ break;
213
+ }
214
+ }
215
+ const windows = [];
216
+ if (fiveHour !== undefined)
217
+ windows.push(toWindow('5h', fiveHour));
218
+ if (weekly !== undefined)
219
+ windows.push(toWindow('weekly', weekly));
220
+ if (windows.length === 0)
221
+ return undefined;
222
+ const plan = typeof body.plan === 'string' && body.plan.length > 0 ? body.plan : undefined;
223
+ return { windows, ...plan === undefined ? {} : { plan } };
224
+ }
225
+ /** Read the subscription tier a Grok access token carries. */
226
+ export function grokTierName(accessToken) {
227
+ const parts = accessToken.split('.');
228
+ if (parts.length < 2)
229
+ return undefined;
230
+ try {
231
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
232
+ if (!isRecord(payload))
233
+ return undefined;
234
+ const tier = payload.tier;
235
+ if (typeof tier !== 'number' || !Number.isInteger(tier))
236
+ return undefined;
237
+ return GROK_TIER_NAMES[tier] ?? String(tier);
238
+ }
239
+ catch {
240
+ return undefined;
241
+ }
242
+ }
243
+ /**
244
+ * Read the Grok (X Premium) plan's metered allowance.
245
+ *
246
+ * The CLI's own chat proxy states included-credit usage as a ready-made
247
+ * percentage plus the current period; an older shape states cent-valued totals
248
+ * from which the percentage is derived.
249
+ *
250
+ * @param input - bearer, and an injectable fetch.
251
+ * @returns the window, or undefined when the endpoint states none.
252
+ */
253
+ export async function readGrokUsage(input) {
254
+ const fetchFn = input.fetchFn ?? proxiedFetch;
255
+ let response;
256
+ try {
257
+ response = await fetchFn(GROK_BILLING_URL, {
258
+ headers: attributedHeaders({
259
+ 'authorization': `Bearer ${input.accessToken}`,
260
+ // The proxy only honors bearer tokens presented as the Grok CLI.
261
+ 'x-xai-token-auth': 'xai-grok-cli',
262
+ 'accept': 'application/json',
263
+ }),
264
+ });
265
+ }
266
+ catch {
267
+ return undefined;
268
+ }
269
+ if (!response.ok)
270
+ return undefined;
271
+ const payload = await response.json().catch(() => undefined);
272
+ if (!isRecord(payload))
273
+ return undefined;
274
+ const config = isRecord(payload.config) ? payload.config : {};
275
+ const windows = [];
276
+ const period = isRecord(config.currentPeriod) ? config.currentPeriod : undefined;
277
+ const percent = toPercent(config.creditUsagePercent);
278
+ if (percent !== undefined || period !== undefined) {
279
+ const resetsAt = period === undefined ? undefined : toInstant(period.end);
280
+ windows.push({
281
+ label: period?.type === 'USAGE_PERIOD_TYPE_WEEKLY' ? 'weekly' : 'credits',
282
+ usedPercent: percent ?? 0,
283
+ ...resetsAt === undefined ? {} : { resetsAt },
284
+ });
285
+ }
286
+ else {
287
+ const limit = toNumber(isRecord(config.monthlyLimit) ? config.monthlyLimit.val : undefined);
288
+ if (limit !== undefined && limit > 0) {
289
+ const used = toNumber(isRecord(config.used) ? config.used.val : undefined) ?? 0;
290
+ const resetsAt = toInstant(config.billingPeriodEnd);
291
+ windows.push({
292
+ label: 'credits',
293
+ usedPercent: Math.min(100, (used / limit) * 100),
294
+ used,
295
+ limit,
296
+ ...resetsAt === undefined ? {} : { resetsAt },
297
+ });
298
+ }
299
+ }
300
+ if (windows.length === 0)
301
+ return undefined;
302
+ // The upstream response rarely carries the tier, so the access token's own
303
+ // claim is the working fallback.
304
+ const stated = typeof payload.subscriptionTier === 'string' && payload.subscriptionTier.length > 0
305
+ ? payload.subscriptionTier
306
+ : undefined;
307
+ const plan = stated ?? grokTierName(input.accessToken);
308
+ return { windows, ...plan === undefined ? {} : { plan } };
309
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Model-visibility and model-order preferences.
3
+ *
4
+ * Which models appear in the session model picker, and in which order, is a user
5
+ * choice, so it lives on the node half beside the adapter that answers the
6
+ * picker's catalog query — not in the browser. The file is a small JSON document
7
+ * keyed by provider; a missing or corrupt section means "everything visible, in
8
+ * the curated order", which is the shipped default.
9
+ *
10
+ * The visibility set is a HIDDEN list rather than a visible list: a model that a
11
+ * later release adds to the catalog must appear without the user re-selecting it.
12
+ * The order is a partial rank: ids it does not mention keep their catalog
13
+ * position, so a new model lands without disturbing the user's arrangement.
14
+ *
15
+ * @module dsh-plugin-subscription-oauth/settings
16
+ */
17
+ /**
18
+ * Provider whose earlier, flat preference shape is migrated on load.
19
+ *
20
+ * Before this plugin served more than one route the document was
21
+ * `{ hidden: string[] }`; that list belonged to Kimi Code.
22
+ */
23
+ export declare const LEGACY_FLAT_PROVIDER = "kimi";
24
+ /**
25
+ * The `auto` provider's arrangement.
26
+ *
27
+ * `auto` is a provider of its own, so its list is not a route's catalog: it is
28
+ * the union of every enabled model, keyed by `provider/model` because two routes
29
+ * may serve the same id.
30
+ */
31
+ export interface AutoPreferences {
32
+ /** `provider/model` keys, most preferred first. */
33
+ order: readonly string[];
34
+ /** Keys `auto` must not use, whatever their own route offers. */
35
+ off: readonly string[];
36
+ }
37
+ /**
38
+ * Owns the model preference document.
39
+ *
40
+ * Reads are cached; every write updates the cache and the file together, so the
41
+ * picker's next catalog query sees the change without a restart.
42
+ */
43
+ export declare class ModelPreferences {
44
+ private readonly file;
45
+ private hidden;
46
+ private order;
47
+ private auto;
48
+ private extra;
49
+ private loaded;
50
+ /** The in-flight first read, so concurrent callers share one. */
51
+ private loading;
52
+ constructor(file: string);
53
+ /**
54
+ * Load the document once; `reload` forces a re-read.
55
+ *
56
+ * Callers read several sections at once (`hidden`, `order`, `extra`), so the load
57
+ * is single-flight: marking the object loaded before the file had been read let a
58
+ * concurrent first call return an empty snapshot, which silently dropped the
59
+ * user's stored choices for whichever read lost the race.
60
+ */
61
+ private ensureLoaded;
62
+ /** Read the file into memory and record that the state is loaded. */
63
+ private read;
64
+ /** Re-read the file, discarding in-memory state. */
65
+ reload(): Promise<void>;
66
+ /** The ids one provider's picker must not offer. */
67
+ hiddenIds(provider: string): Promise<ReadonlySet<string>>;
68
+ /** The user's model rank for one provider, most preferred first. */
69
+ orderOf(provider: string): Promise<readonly string[]>;
70
+ /** The ids the user added by hand for one provider. */
71
+ extraIds(provider: string): Promise<readonly string[]>;
72
+ /**
73
+ * Add one model id by hand.
74
+ *
75
+ * The point of the button is a model the catalog does not know yet, so nothing
76
+ * here validates the id against an endpoint — only that it is a plausible id.
77
+ *
78
+ * @param provider - provider route key.
79
+ * @param id - the model id the endpoint accepts.
80
+ * @returns whether the stored list actually changed.
81
+ */
82
+ addExtra(provider: string, id: string): Promise<boolean>;
83
+ /**
84
+ * Forget one hand-added id.
85
+ * @param provider - provider route key.
86
+ * @param id - the model id to drop.
87
+ * @returns whether the stored list actually changed.
88
+ */
89
+ removeExtra(provider: string, id: string): Promise<boolean>;
90
+ /** The `auto` provider's arrangement. */
91
+ autoPreferences(): Promise<AutoPreferences>;
92
+ /**
93
+ * Replace the `auto` provider's order.
94
+ * @param keys - `provider/model` keys, most preferred first.
95
+ * @returns whether the stored order actually changed.
96
+ */
97
+ setAutoOrder(keys: readonly string[]): Promise<boolean>;
98
+ /**
99
+ * Record whether `auto` may use one model.
100
+ * @param key - `provider/model` key.
101
+ * @param enabled - whether `auto` may fall back to it.
102
+ * @returns whether the stored state actually changed.
103
+ */
104
+ setAutoEnabled(key: string, enabled: boolean): Promise<boolean>;
105
+ /**
106
+ * Record one model's visibility.
107
+ * @param provider - provider route key.
108
+ * @param id - model id as the catalog reports it.
109
+ * @param visible - whether the picker should offer it.
110
+ * @returns whether the stored state actually changed.
111
+ */
112
+ setVisible(provider: string, id: string, visible: boolean): Promise<boolean>;
113
+ /**
114
+ * Replace one provider's model rank.
115
+ * @param provider - provider route key.
116
+ * @param ids - the complete rank, most preferred first.
117
+ * @returns whether the stored order actually changed.
118
+ */
119
+ setOrder(provider: string, ids: readonly string[]): Promise<boolean>;
120
+ /** Write the document atomically with owner-only permissions. */
121
+ private persist;
122
+ }