dsh-subscriptions 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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/cordis.patch.yml +12 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +209 -0
- package/lib/auth/rpc.js +386 -0
- package/lib/auth/store.d.ts +196 -0
- package/lib/auth/store.js +468 -0
- package/lib/client/CursorCard.d.ts +9 -0
- package/lib/client/CursorCard.js +113 -0
- package/lib/client/ExternalUsageCards.d.ts +9 -0
- package/lib/client/ExternalUsageCards.js +72 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +133 -0
- package/lib/client/ProviderAccountManager.d.ts +13 -0
- package/lib/client/ProviderAccountManager.js +147 -0
- package/lib/client/ProviderModelEditor.d.ts +39 -0
- package/lib/client/ProviderModelEditor.js +107 -0
- package/lib/client/SpeedSelect.d.ts +71 -0
- package/lib/client/SpeedSelect.js +177 -0
- package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
- package/lib/client/SubscriptionUsageBadge.js +543 -0
- package/lib/client/SubscriptionsSection.d.ts +140 -0
- package/lib/client/SubscriptionsSection.js +544 -0
- package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
- package/lib/client/UsageBadgeDisplaySetting.js +36 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +175 -0
- package/lib/client/account-preferences.d.ts +24 -0
- package/lib/client/account-preferences.js +21 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/host-contract.d.ts +90 -0
- package/lib/client/host-contract.js +101 -0
- package/lib/client/host-icons.d.ts +24 -0
- package/lib/client/host-icons.js +21 -0
- package/lib/client/index.d.ts +27 -0
- package/lib/client/index.js +136 -0
- package/lib/client/locales.d.ts +297 -0
- package/lib/client/locales.js +295 -0
- package/lib/client/subscription-card-styles.d.ts +3 -0
- package/lib/client/subscription-card-styles.js +53 -0
- package/lib/client/subscriptions-rpc.d.ts +14 -0
- package/lib/client/subscriptions-rpc.js +34 -0
- package/lib/client/usage-badge-preferences.d.ts +11 -0
- package/lib/client/usage-badge-preferences.js +41 -0
- package/lib/client.js +4472 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +15 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.js +14018 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/provider-settings.d.ts +39 -0
- package/lib/provider-settings.js +151 -0
- package/lib/providers/account-preferences.d.ts +41 -0
- package/lib/providers/account-preferences.js +153 -0
- package/lib/providers/accounts.d.ts +112 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +110 -0
- package/lib/providers/antigravity.js +516 -0
- package/lib/providers/catalog-store.d.ts +48 -0
- package/lib/providers/catalog-store.js +243 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +181 -0
- package/lib/providers/claude.js +680 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +229 -0
- package/lib/providers/codex.js +821 -0
- package/lib/providers/common.d.ts +389 -0
- package/lib/providers/common.js +656 -0
- package/lib/providers/copilot.d.ts +344 -0
- package/lib/providers/copilot.js +888 -0
- package/lib/providers/cursor-adapter.d.ts +15 -0
- package/lib/providers/cursor-adapter.js +39 -0
- package/lib/providers/cursor-auth.d.ts +43 -0
- package/lib/providers/cursor-auth.js +169 -0
- package/lib/providers/cursor-model-settings.d.ts +15 -0
- package/lib/providers/cursor-model-settings.js +51 -0
- package/lib/providers/cursor-usage.d.ts +9 -0
- package/lib/providers/cursor-usage.js +112 -0
- package/lib/providers/external-usage-controller.d.ts +19 -0
- package/lib/providers/external-usage-controller.js +32 -0
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +82 -0
- package/lib/providers/grok.d.ts +205 -0
- package/lib/providers/grok.js +713 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +150 -0
- package/lib/providers/pool-usage.d.ts +86 -0
- package/lib/providers/pool-usage.js +223 -0
- package/lib/providers/pool.d.ts +108 -0
- package/lib/providers/pool.js +368 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +15 -0
- package/lib/tools/registration.js +24 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +161 -0
- package/lib/translate/anthropic.js +515 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +114 -0
- package/lib/translate/antigravity.js +366 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +80 -0
- package/lib/translate/resolved.js +118 -0
- package/lib/translate/responses.d.ts +179 -0
- package/lib/translate/responses.js +408 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +56 -0
- package/package.json +114 -0
- package/vendor/cursor/LICENSE +21 -0
- package/vendor/cursor/index.d.ts +18 -0
- package/vendor/cursor/index.js +3574 -0
- package/vendor/cursor/proto.js +254 -0
package/lib/auth/rpc.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
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 } from './store.js';
|
|
11
|
+
import { EXTERNAL_USAGE_SOURCES } from '../providers/external-usage-controller.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
|
+
'externalStatus', 'externalUsage',
|
|
26
|
+
'cursorStatus', 'cursorLogin', 'cursorCancel', 'cursorLogout', 'cursorUsage', 'cursorModels',
|
|
27
|
+
'cursorSettings', 'cursorSetSettings',
|
|
28
|
+
'image', 'video',
|
|
29
|
+
'speed', 'setSpeed',
|
|
30
|
+
'modelDefaults', 'setModelDefault',
|
|
31
|
+
];
|
|
32
|
+
/** Media types the attachment store accepts (ImageMediaType). */
|
|
33
|
+
const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
34
|
+
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
35
|
+
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
36
|
+
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
37
|
+
export class BadRequest extends Error {
|
|
38
|
+
}
|
|
39
|
+
function readEnvelope(body) {
|
|
40
|
+
if (typeof body !== 'object' || body === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
const record = body;
|
|
43
|
+
if (record.type !== 'client-request' || typeof record.rpcId !== 'string' || typeof record.method !== 'string')
|
|
44
|
+
return undefined;
|
|
45
|
+
return { type: 'client-request', rpcId: record.rpcId, method: record.method, payload: record.payload };
|
|
46
|
+
}
|
|
47
|
+
function serverResponse(rpcId, result) {
|
|
48
|
+
return Response.json({ type: 'server-response', rpcId, result });
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrap one endpoint's RPC handler as an exact Fetch route: decode the
|
|
52
|
+
* `client-request` envelope the browser rpc caller posts, run the handler,
|
|
53
|
+
* and answer with the matching `server-response` envelope — the same wire
|
|
54
|
+
* contract the dedicated-channel bridge used to apply, reproduced here so
|
|
55
|
+
* `rpc.call('/api', 'subscriptions-auth.<endpoint>', payload)` keeps working
|
|
56
|
+
* unchanged on the browser side.
|
|
57
|
+
*/
|
|
58
|
+
function fetchRouteFor(endpoint, handler) {
|
|
59
|
+
const method = `${SUBSCRIPTIONS_AUTH_PREFIX}${endpoint}`;
|
|
60
|
+
return {
|
|
61
|
+
path: `/api/${method}`,
|
|
62
|
+
methods: ['POST'],
|
|
63
|
+
requestBody: 'buffered',
|
|
64
|
+
fetch: async (request) => {
|
|
65
|
+
if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
66
|
+
return new Response('content type must be application/json', { status: 415 });
|
|
67
|
+
}
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = await request.json();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return new Response('body is not JSON', { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
const envelope = readEnvelope(body);
|
|
76
|
+
if (envelope === undefined) {
|
|
77
|
+
const rawId = body?.rpcId;
|
|
78
|
+
return serverResponse(typeof rawId === 'string' ? rawId : 'invalid-request', {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: { code: 'gateway/bad-request', message: 'invalid client-request message', details: { issues: [] } },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (envelope.method !== method) {
|
|
84
|
+
return serverResponse(envelope.rpcId, {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: {
|
|
87
|
+
code: 'gateway/bad-request',
|
|
88
|
+
message: `method ${JSON.stringify(envelope.method)} does not match endpoint ${JSON.stringify(method)}`,
|
|
89
|
+
details: { issues: [] },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return serverResponse(envelope.rpcId, await handler(endpoint, envelope.payload, request.signal));
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function ok(value) {
|
|
98
|
+
return { ok: true, value };
|
|
99
|
+
}
|
|
100
|
+
function failure(error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
if (error instanceof BadRequest) {
|
|
103
|
+
// The issues array is zod-shaped upstream; this channel validates by hand.
|
|
104
|
+
return { ok: false, error: { code: 'bad-request', message, details: { issues: [] } } };
|
|
105
|
+
}
|
|
106
|
+
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
107
|
+
}
|
|
108
|
+
function readProvider(payload) {
|
|
109
|
+
if (typeof payload !== 'object' || payload === null)
|
|
110
|
+
throw new BadRequest('payload must be an object');
|
|
111
|
+
const provider = payload.provider;
|
|
112
|
+
if (typeof provider !== 'string' || !PROVIDER_IDS.includes(provider)) {
|
|
113
|
+
throw new BadRequest(`payload.provider must be one of ${PROVIDER_IDS.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
return provider;
|
|
116
|
+
}
|
|
117
|
+
function readString(payload, field) {
|
|
118
|
+
const value = payload[field];
|
|
119
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
120
|
+
throw new BadRequest(`payload.${field} must be a non-empty string`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
/** Validate the `setModelDefault` endpoint's payload. */
|
|
125
|
+
function readModelDefaultInput(payload) {
|
|
126
|
+
const provider = readProvider(payload);
|
|
127
|
+
const model = readString(payload, 'model');
|
|
128
|
+
const record = payload;
|
|
129
|
+
let effort;
|
|
130
|
+
if (record.effort !== undefined) {
|
|
131
|
+
if (typeof record.effort !== 'string' || record.effort.length === 0) {
|
|
132
|
+
throw new BadRequest('payload.effort must be a non-empty string when present');
|
|
133
|
+
}
|
|
134
|
+
effort = record.effort;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
provider,
|
|
138
|
+
model,
|
|
139
|
+
...(effort === undefined ? {} : { effort }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Validate the optional Claude login method. */
|
|
143
|
+
function readLoginMethod(payload, provider) {
|
|
144
|
+
const method = payload.method;
|
|
145
|
+
if (method === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (provider !== 'claude')
|
|
148
|
+
throw new BadRequest('payload.method is only valid for claude');
|
|
149
|
+
if (method !== 'oauth' && method !== 'keychain') {
|
|
150
|
+
throw new BadRequest('payload.method must be "oauth" or "keychain"');
|
|
151
|
+
}
|
|
152
|
+
return method;
|
|
153
|
+
}
|
|
154
|
+
/** Validate the `setSpeed` endpoint's tier. */
|
|
155
|
+
function readSpeedTier(payload) {
|
|
156
|
+
const tier = payload.tier;
|
|
157
|
+
if (tier !== 'standard' && tier !== 'fast') {
|
|
158
|
+
throw new BadRequest('payload.tier must be "standard" or "fast"');
|
|
159
|
+
}
|
|
160
|
+
return tier;
|
|
161
|
+
}
|
|
162
|
+
/** Validate the `image` endpoint's payload into a full attachment reference. */
|
|
163
|
+
function readImageRef(payload) {
|
|
164
|
+
if (typeof payload !== 'object' || payload === null)
|
|
165
|
+
throw new BadRequest('payload must be an object');
|
|
166
|
+
const record = payload;
|
|
167
|
+
const attachmentId = record.attachmentId;
|
|
168
|
+
if (typeof attachmentId !== 'string' || attachmentId.length === 0) {
|
|
169
|
+
throw new BadRequest('payload.attachmentId must be a non-empty string');
|
|
170
|
+
}
|
|
171
|
+
const mediaType = record.mediaType;
|
|
172
|
+
if (typeof mediaType !== 'string' || !IMAGE_MEDIA_TYPES.includes(mediaType)) {
|
|
173
|
+
throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
for (const field of ['bytes', 'width', 'height']) {
|
|
176
|
+
const value = record[field];
|
|
177
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
178
|
+
throw new BadRequest(`payload.${field} must be a positive integer`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const name = record.name;
|
|
182
|
+
if (name !== undefined && typeof name !== 'string') {
|
|
183
|
+
throw new BadRequest('payload.name must be a string when present');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
attachmentId: AttachmentId(attachmentId),
|
|
187
|
+
mediaType: mediaType,
|
|
188
|
+
bytes: record.bytes,
|
|
189
|
+
width: record.width,
|
|
190
|
+
height: record.height,
|
|
191
|
+
...name === undefined ? {} : { name: name },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Validate the `video` endpoint's payload into a bare file name. Rejecting
|
|
196
|
+
* anything with a path separator (the pattern allows none) pins every read
|
|
197
|
+
* inside the plugin's videos directory.
|
|
198
|
+
*/
|
|
199
|
+
function readVideoName(payload) {
|
|
200
|
+
if (typeof payload !== 'object' || payload === null)
|
|
201
|
+
throw new BadRequest('payload must be an object');
|
|
202
|
+
const name = payload.name;
|
|
203
|
+
if (typeof name !== 'string' || !VIDEO_NAME_PATTERN.test(name)) {
|
|
204
|
+
throw new BadRequest('payload.name must be a bare .mp4 file name');
|
|
205
|
+
}
|
|
206
|
+
return name;
|
|
207
|
+
}
|
|
208
|
+
/** Validate the usage/model catalog endpoints' optional force flag. */
|
|
209
|
+
function readForce(payload) {
|
|
210
|
+
if (typeof payload !== 'object' || payload === null)
|
|
211
|
+
return false;
|
|
212
|
+
const force = payload.force;
|
|
213
|
+
if (force === undefined)
|
|
214
|
+
return false;
|
|
215
|
+
if (typeof force !== 'boolean')
|
|
216
|
+
throw new BadRequest('payload.force must be a boolean when present');
|
|
217
|
+
return force;
|
|
218
|
+
}
|
|
219
|
+
/** Validate the session id both speed endpoints carry. */
|
|
220
|
+
function readSessionId(payload) {
|
|
221
|
+
if (typeof payload !== 'object' || payload === null)
|
|
222
|
+
throw new BadRequest('payload must be an object');
|
|
223
|
+
return readString(payload, 'sessionId');
|
|
224
|
+
}
|
|
225
|
+
async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor) {
|
|
226
|
+
switch (endpoint) {
|
|
227
|
+
case 'providerSettings':
|
|
228
|
+
if (!providerSettings)
|
|
229
|
+
throw new BadRequest('provider settings are unavailable');
|
|
230
|
+
return ok(await providerSettings.get(readProvider(payload), readForce(payload)));
|
|
231
|
+
case 'setProviderSettings': {
|
|
232
|
+
if (!providerSettings)
|
|
233
|
+
throw new BadRequest('provider settings are unavailable');
|
|
234
|
+
const provider = readProvider(payload);
|
|
235
|
+
await providerSettings.set(provider, payload.settings);
|
|
236
|
+
return ok({ ok: true });
|
|
237
|
+
}
|
|
238
|
+
case 'status': {
|
|
239
|
+
// One provider's failure (a corrupt store entry, a broken flow) must not
|
|
240
|
+
// blind the whole page: it degrades to an error detail on that provider
|
|
241
|
+
// while the others still report their real status.
|
|
242
|
+
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider).catch((error) => ({
|
|
243
|
+
busy: false,
|
|
244
|
+
accounts: [],
|
|
245
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
246
|
+
}))]));
|
|
247
|
+
return ok({ providers: Object.fromEntries(entries) });
|
|
248
|
+
}
|
|
249
|
+
case 'login': {
|
|
250
|
+
const provider = readProvider(payload);
|
|
251
|
+
return ok(await controller.login(provider, readLoginMethod(payload, provider)));
|
|
252
|
+
}
|
|
253
|
+
case 'manual': {
|
|
254
|
+
const provider = readProvider(payload);
|
|
255
|
+
await controller.manual(provider, readString(payload, 'input'));
|
|
256
|
+
return ok({ ok: true });
|
|
257
|
+
}
|
|
258
|
+
case 'cancel':
|
|
259
|
+
await controller.cancel(readProvider(payload));
|
|
260
|
+
return ok({ ok: true });
|
|
261
|
+
case 'logout': {
|
|
262
|
+
const provider = readProvider(payload);
|
|
263
|
+
await controller.logout(provider, readString(payload, 'account'));
|
|
264
|
+
return ok({ ok: true });
|
|
265
|
+
}
|
|
266
|
+
case 'setDefault': {
|
|
267
|
+
const provider = readProvider(payload);
|
|
268
|
+
await controller.setDefault(provider, readString(payload, 'account'));
|
|
269
|
+
return ok({ ok: true });
|
|
270
|
+
}
|
|
271
|
+
case 'usage': {
|
|
272
|
+
const provider = readProvider(payload);
|
|
273
|
+
return ok(await controller.usage(provider, readString(payload, 'account'), signal, readForce(payload)));
|
|
274
|
+
}
|
|
275
|
+
case 'externalStatus':
|
|
276
|
+
if (!externalUsage)
|
|
277
|
+
throw new BadRequest('external usage is unavailable');
|
|
278
|
+
return ok(await externalUsage.status());
|
|
279
|
+
case 'externalUsage': {
|
|
280
|
+
if (!externalUsage)
|
|
281
|
+
throw new BadRequest('external usage is unavailable');
|
|
282
|
+
const source = readString(payload, 'source');
|
|
283
|
+
if (!EXTERNAL_USAGE_SOURCES.includes(source)) {
|
|
284
|
+
throw new BadRequest(`payload.source must be one of ${EXTERNAL_USAGE_SOURCES.join(', ')}`);
|
|
285
|
+
}
|
|
286
|
+
return ok(await externalUsage.usage(source, signal));
|
|
287
|
+
}
|
|
288
|
+
case 'cursorStatus':
|
|
289
|
+
if (!cursor)
|
|
290
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
291
|
+
return ok(await cursor.status());
|
|
292
|
+
case 'cursorLogin':
|
|
293
|
+
if (!cursor)
|
|
294
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
295
|
+
return ok(await cursor.login());
|
|
296
|
+
case 'cursorCancel':
|
|
297
|
+
if (!cursor)
|
|
298
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
299
|
+
await cursor.cancel();
|
|
300
|
+
return ok({ ok: true });
|
|
301
|
+
case 'cursorLogout':
|
|
302
|
+
if (!cursor)
|
|
303
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
304
|
+
await cursor.logout();
|
|
305
|
+
return ok({ ok: true });
|
|
306
|
+
case 'cursorUsage':
|
|
307
|
+
if (!cursor)
|
|
308
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
309
|
+
return ok(await cursor.usage(signal));
|
|
310
|
+
case 'cursorModels':
|
|
311
|
+
if (!cursor)
|
|
312
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
313
|
+
return ok(await cursor.models(readForce(payload), signal));
|
|
314
|
+
case 'cursorSettings':
|
|
315
|
+
if (!cursor)
|
|
316
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
317
|
+
return ok(await cursor.settings(readForce(payload), signal));
|
|
318
|
+
case 'cursorSetSettings':
|
|
319
|
+
if (!cursor)
|
|
320
|
+
throw new BadRequest('Cursor account is unavailable');
|
|
321
|
+
if (payload === null || typeof payload !== 'object')
|
|
322
|
+
throw new BadRequest('payload must be an object');
|
|
323
|
+
await cursor.setSettings(payload.settings);
|
|
324
|
+
return ok({ ok: true });
|
|
325
|
+
case 'image':
|
|
326
|
+
return ok(await controller.readImage(readImageRef(payload), signal));
|
|
327
|
+
case 'video':
|
|
328
|
+
return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
329
|
+
case 'speed':
|
|
330
|
+
return ok(await speed.speed(readSessionId(payload)));
|
|
331
|
+
case 'setSpeed':
|
|
332
|
+
await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
|
|
333
|
+
return ok({ ok: true });
|
|
334
|
+
case 'modelDefaults': {
|
|
335
|
+
if (modelDefaults === undefined)
|
|
336
|
+
throw new BadRequest('model defaults are unavailable');
|
|
337
|
+
return ok(await modelDefaults.catalog(readForce(payload)));
|
|
338
|
+
}
|
|
339
|
+
case 'setModelDefault':
|
|
340
|
+
if (modelDefaults === undefined)
|
|
341
|
+
throw new BadRequest('model defaults are unavailable');
|
|
342
|
+
{
|
|
343
|
+
const input = readModelDefaultInput(payload);
|
|
344
|
+
await modelDefaults.set(input.provider, input.model, input.effort);
|
|
345
|
+
}
|
|
346
|
+
return ok({ ok: true });
|
|
347
|
+
default:
|
|
348
|
+
throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Register the `/subscriptions-auth` RPC channel when a host connection exists.
|
|
353
|
+
* @param ctx - the plugin context (headless profiles have no `connection`).
|
|
354
|
+
* @param controller - the auth operations backing the endpoints.
|
|
355
|
+
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
356
|
+
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
357
|
+
*/
|
|
358
|
+
export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined) {
|
|
359
|
+
// `connection` is not in this plugin's inject list (headless compositions
|
|
360
|
+
// lack it), so its startup order is unconstrained: defer registration until
|
|
361
|
+
// the service exists instead of probing once at apply time.
|
|
362
|
+
//
|
|
363
|
+
// Exact Fetch routes under `/api` rather than a dedicated `rpc.handle`
|
|
364
|
+
// channel: since dsh 0.1.5 the connection plugin no longer injects
|
|
365
|
+
// `webServer` itself, and `rpc.handle` resolves `webServer` through the
|
|
366
|
+
// connection plugin's own fiber, so every dedicated channel registration
|
|
367
|
+
// throws `cannot get property "webServer" without inject`. The `/api`
|
|
368
|
+
// route is mounted by the connection plugin (with its own webServer scope)
|
|
369
|
+
// and applies the same trust fence and browser authentication, so exact
|
|
370
|
+
// routes below it work on both dsh lines.
|
|
371
|
+
ctx.inject(['connection'], (ctx) => {
|
|
372
|
+
const connection = ctx.get('connection');
|
|
373
|
+
const handler = async (endpoint, payload, signal) => {
|
|
374
|
+
try {
|
|
375
|
+
return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
return failure(error);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const register = connection.fetch.register;
|
|
382
|
+
for (const endpoint of SUBSCRIPTIONS_AUTH_ENDPOINTS) {
|
|
383
|
+
ctx.effect(() => register(fetchRouteFor(endpoint, handler)), `dsh-subscriptions: /api/${SUBSCRIPTIONS_AUTH_PREFIX}${endpoint} route`);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk OAuth session store at `~/.dsh/plugins/subscriptions/auth.json`.
|
|
3
|
+
*
|
|
4
|
+
* The file is a JSON object keyed by provider id, each entry holding that
|
|
5
|
+
* provider's ACCOUNTS: a map of account key → session plus the default
|
|
6
|
+
* account's key. Writes are atomic (tmp file + rename) with mode 0600
|
|
7
|
+
* because they carry bearer tokens. Session shapes live here (not in the
|
|
8
|
+
* provider modules) because this file owns the durable format.
|
|
9
|
+
*
|
|
10
|
+
* Backward compatibility: entries written by single-account versions hold
|
|
11
|
+
* the session fields directly (no `accounts` wrapper); reads migrate them
|
|
12
|
+
* in memory, and the next write persists the new shape — existing logins
|
|
13
|
+
* survive the upgrade untouched.
|
|
14
|
+
*/
|
|
15
|
+
/** Provider routes this plugin can serve. */
|
|
16
|
+
export type ProviderId = 'codex' | 'claude' | 'grok' | 'copilot' | 'antigravity';
|
|
17
|
+
/** Every provider route, in display order. */
|
|
18
|
+
export declare const PROVIDER_IDS: readonly ProviderId[];
|
|
19
|
+
/** Stored ChatGPT/Codex subscription session. */
|
|
20
|
+
export interface CodexSession {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
/** Epoch milliseconds at which the access token expires. */
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
/** `chatgpt_account_id` claim from the id token; sent as the `chatgpt-account-id` header. */
|
|
26
|
+
accountId: string;
|
|
27
|
+
idToken?: string;
|
|
28
|
+
/** User email from the id token, when the token carried it. */
|
|
29
|
+
emailAddress?: string;
|
|
30
|
+
/** `chatgpt_plan_type` claim from the id token (e.g. `plus`, `pro`), when present. */
|
|
31
|
+
planType?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Stored Claude Pro/Max subscription session. */
|
|
34
|
+
export interface ClaudeSession {
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken: string;
|
|
37
|
+
/** Epoch milliseconds at which the access token expires. */
|
|
38
|
+
expiresAt: number;
|
|
39
|
+
/** Scope string the tokens were issued with; echoed on refresh. */
|
|
40
|
+
scopes: string;
|
|
41
|
+
emailAddress?: string;
|
|
42
|
+
subscriptionType?: string;
|
|
43
|
+
/**
|
|
44
|
+
* True when this account was imported from Claude Code's own credential
|
|
45
|
+
* store (Keychain/file): only bound accounts sync refreshes back to it.
|
|
46
|
+
*/
|
|
47
|
+
keychainBound?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Stored Grok (X Premium / xAI) subscription session. */
|
|
50
|
+
export interface GrokSession {
|
|
51
|
+
accessToken: string;
|
|
52
|
+
refreshToken: string;
|
|
53
|
+
/** Epoch milliseconds at which the access token expires. */
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
/** Token endpoint from OIDC discovery; retained for refreshes. */
|
|
56
|
+
tokenEndpoint: string;
|
|
57
|
+
scopes?: string;
|
|
58
|
+
/** Display account: email, username, or subject claim from the id token. */
|
|
59
|
+
account?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Stored GitHub Copilot subscription session. Two token generations are at
|
|
63
|
+
* play: the long-lived GitHub OAuth token from the device flow is kept in
|
|
64
|
+
* `refreshToken`, and `accessToken` carries the short-lived (~30 minutes)
|
|
65
|
+
* Copilot API token exchanged from it. A "refresh" is therefore a fresh
|
|
66
|
+
* exchange against `copilot_internal/v2/token`, not an OAuth grant.
|
|
67
|
+
*/
|
|
68
|
+
export interface CopilotSession {
|
|
69
|
+
/** Copilot API token; sent as the bearer on api.githubcopilot.com. */
|
|
70
|
+
accessToken: string;
|
|
71
|
+
/** Long-lived GitHub OAuth token from the device flow. */
|
|
72
|
+
refreshToken: string;
|
|
73
|
+
/** Epoch milliseconds at which the Copilot API token expires. */
|
|
74
|
+
expiresAt: number;
|
|
75
|
+
/** GitHub login name, for the status display. */
|
|
76
|
+
account?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Stored Google OAuth session for the Antigravity v1internal API. */
|
|
79
|
+
export interface AntigravitySession {
|
|
80
|
+
accessToken: string;
|
|
81
|
+
refreshToken: string;
|
|
82
|
+
/** Epoch milliseconds at which the Google access token expires. */
|
|
83
|
+
expiresAt: number;
|
|
84
|
+
/** Cloud AI Companion project required by Antigravity request envelopes. */
|
|
85
|
+
projectId: string;
|
|
86
|
+
/** Google account email, for the status display. */
|
|
87
|
+
account?: string;
|
|
88
|
+
/** Subscription tier observed during project discovery. */
|
|
89
|
+
plan?: string;
|
|
90
|
+
/** Granted Google OAuth scopes, when the token endpoint returned them. */
|
|
91
|
+
scopes?: string;
|
|
92
|
+
}
|
|
93
|
+
/** One provider's accounts: account key → session, plus the default account. */
|
|
94
|
+
export interface ProviderAccounts<S> {
|
|
95
|
+
/** Key of the account direct (non-pool) routes serve; the first login wins. */
|
|
96
|
+
default?: string;
|
|
97
|
+
accounts: Record<string, S>;
|
|
98
|
+
/** Old keys remain bound to their migrated account, never a workspace sibling. */
|
|
99
|
+
aliases?: Record<string, string>;
|
|
100
|
+
}
|
|
101
|
+
/** The durable store shape: per provider, its accounts. */
|
|
102
|
+
export interface SessionMap {
|
|
103
|
+
codex?: ProviderAccounts<CodexSession>;
|
|
104
|
+
claude?: ProviderAccounts<ClaudeSession>;
|
|
105
|
+
grok?: ProviderAccounts<GrokSession>;
|
|
106
|
+
copilot?: ProviderAccounts<CopilotSession>;
|
|
107
|
+
antigravity?: ProviderAccounts<AntigravitySession>;
|
|
108
|
+
}
|
|
109
|
+
/** Any stored session, for provider-agnostic plumbing. */
|
|
110
|
+
export type StoredSession = CodexSession | ClaudeSession | GrokSession | CopilotSession | AntigravitySession;
|
|
111
|
+
/** The session type one provider stores. */
|
|
112
|
+
export type SessionOf<K extends ProviderId> = NonNullable<SessionMap[K]>['accounts'][string];
|
|
113
|
+
/** One account entry as returned by {@link listAccounts} (default first). */
|
|
114
|
+
export interface AccountEntry<S> {
|
|
115
|
+
key: string;
|
|
116
|
+
session: S;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The stable identity of one session's account: Codex keys on workspace AND
|
|
120
|
+
* user (email fallback, workspace-only for unidentified legacy sessions),
|
|
121
|
+
* the others on their display identity, falling
|
|
122
|
+
* back to a refresh-token hash for sessions stored before identity fields
|
|
123
|
+
* existed. Logging the same account in again lands on the same key, so a
|
|
124
|
+
* re-login updates in place instead of duplicating. (The hash fallback can
|
|
125
|
+
* miss that dedup once for a legacy session re-logged with a now-known
|
|
126
|
+
* identity — the duplicate is visible on the Settings page and can simply
|
|
127
|
+
* be logged out.)
|
|
128
|
+
* @param provider - the provider route.
|
|
129
|
+
* @param session - the session to key.
|
|
130
|
+
* @returns the account map key.
|
|
131
|
+
*/
|
|
132
|
+
export declare function accountKeyOf(provider: ProviderId, session: StoredSession): string;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a persisted legacy account key to its canonical account identity.
|
|
135
|
+
* For Codex, a reference that matches no key or alias also resolves through
|
|
136
|
+
* a unique login email or workspace ID (see {@link resolveCodexReference}).
|
|
137
|
+
*/
|
|
138
|
+
export declare function resolveAccountKey(provider: ProviderId, account: string, path?: string): Promise<string>;
|
|
139
|
+
/**
|
|
140
|
+
* Absolute path of the auth store file.
|
|
141
|
+
* @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function authFilePath(): string;
|
|
144
|
+
/**
|
|
145
|
+
* Read the whole store. A missing file is an empty store; malformed JSON or a
|
|
146
|
+
* malformed entry throws, because silently discarding tokens would strand the
|
|
147
|
+
* user without a diagnosis. Single-account entries are migrated in memory;
|
|
148
|
+
* the next write persists the new shape.
|
|
149
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
150
|
+
* @returns the parsed session map.
|
|
151
|
+
*/
|
|
152
|
+
export declare function loadStore(path?: string): Promise<SessionMap>;
|
|
153
|
+
/**
|
|
154
|
+
* List one provider's accounts, default first.
|
|
155
|
+
* @param provider - the provider route.
|
|
156
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
157
|
+
* @returns the account entries in stable order (empty when logged out).
|
|
158
|
+
*/
|
|
159
|
+
export declare function listAccounts<K extends ProviderId>(provider: K, path?: string): Promise<AccountEntry<SessionOf<K>>[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Read one account's session.
|
|
162
|
+
* @param provider - the provider route.
|
|
163
|
+
* @param account - the account key; defaults to the provider's default account.
|
|
164
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
165
|
+
* @returns the stored session, or `undefined` when absent.
|
|
166
|
+
*/
|
|
167
|
+
export declare function getAccountSession<K extends ProviderId>(provider: K, account?: string, path?: string): Promise<SessionOf<K> | undefined>;
|
|
168
|
+
/**
|
|
169
|
+
* Write one account's session, preserving the others. The first account of a
|
|
170
|
+
* provider becomes its default.
|
|
171
|
+
*
|
|
172
|
+
* The session is validated before it lands: a corrupt entry written here
|
|
173
|
+
* would fail every later read of the whole store (one bad entry breaks all
|
|
174
|
+
* providers' status), so the write path must be as strict as the read path.
|
|
175
|
+
* @param provider - the provider route.
|
|
176
|
+
* @param account - the account key (see {@link accountKeyOf}).
|
|
177
|
+
* @param session - the fresh session from a login or refresh.
|
|
178
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
179
|
+
* @throws when the session is missing accessToken/refreshToken/expiresAt.
|
|
180
|
+
*/
|
|
181
|
+
export declare function saveAccountSession<K extends ProviderId>(provider: K, account: string, session: SessionOf<K>, path?: string): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* Delete one account's session (logout). Deleting the default moves the badge
|
|
184
|
+
* to the next remaining account.
|
|
185
|
+
* @param provider - the provider route.
|
|
186
|
+
* @param account - the account key.
|
|
187
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
188
|
+
*/
|
|
189
|
+
export declare function deleteAccountSession(provider: ProviderId, account: string, path?: string): Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Pin the account direct (non-pool) routes serve.
|
|
192
|
+
* @param provider - the provider route.
|
|
193
|
+
* @param account - the account key; must exist.
|
|
194
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
195
|
+
*/
|
|
196
|
+
export declare function setDefaultAccount(provider: ProviderId, account: string, path?: string): Promise<void>;
|