@deepseek-ai/dsh-deepseek-account-platform 0.1.7-alpha.1

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.
@@ -0,0 +1,488 @@
1
+ /** Platform PKCE account provider; browser approval never bypasses local cancellation. */
2
+ import { randomBytes, randomUUID, createHash, timingSafeEqual } from 'node:crypto';
3
+ import { arch, platform, release } from 'node:os';
4
+ import { finished } from 'node:stream/promises';
5
+ import { Service } from '@deepseek-ai/cordis';
6
+ import Schema from '@deepseek-ai/schemastery';
7
+ import { z } from 'zod';
8
+ import { DeepSeekAccount, desktopClientHeaders, mergePlatformCookies } from '@deepseek-ai/dsh-deepseek-account';
9
+ import { credentialKey } from '@deepseek-ai/dsh-credentials';
10
+ import { profile, readAccountDetail } from "./details.js";
11
+ import { revokeAccount } from "./logout.js";
12
+ import { PlatformAuthError, platformHeaders, platformOrigin, browserUrl, requestPlatform, initialization, exchange, loginOrigin } from "./protocol.js";
13
+ const KEY = credentialKey('deepseek-account-platform', 'default');
14
+ const DEVICE = credentialKey('deepseek-account-platform', 'device');
15
+ const grant = z.object({ version: z.literal(1), token: z.string().min(1), issuer: z.url() });
16
+ const device = z.object({ id: z.uuid() });
17
+ /** Validated deployment choices. */
18
+ export const Config = Schema.object({
19
+ platformOrigin: Schema.string().default('https://platform.deepseek.com'),
20
+ desktopPlatform: Schema.union([Schema.const('darwin'), Schema.const('win32'), Schema.const(null)]).default(null),
21
+ embeddedPageDist: Schema.string().default(''),
22
+ inferenceOrigin: Schema.string().default('https://api.deepseek.com'),
23
+ allowLoopbackHttp: Schema.boolean().default(false),
24
+ rewriteBrowserOrigin: Schema.boolean().default(false),
25
+ requestHeaders: Schema.dict(Schema.string().role('secret')).default({}),
26
+ accountRequestHeaders: Schema.dict(Schema.string().role('secret')).default({}),
27
+ requestTimeoutMs: Schema.number().min(1).max(120_000).default(30_000),
28
+ logoutMaxRetries: Schema.number().min(0).max(5).step(1).default(5),
29
+ logoutRetryDelayMs: Schema.number().min(1).max(60_000).default(1_000),
30
+ attemptTimeoutMs: Schema.number().min(1).max(3_600_000).default(600_000),
31
+ });
32
+ /** The platform implementation owns login state and its opaque stored grant. */
33
+ export class PlatformAccount extends DeepSeekAccount {
34
+ static inject = ['credentials', 'authorization'];
35
+ static Config = Config;
36
+ origin;
37
+ embeddedPageDist;
38
+ inferenceOrigin;
39
+ rewriteBrowserOrigin;
40
+ clientHeaders;
41
+ requestHeaders;
42
+ accountRequestHeaders;
43
+ requestTimeout;
44
+ attemptTimeout;
45
+ logoutPolicy;
46
+ logoutLifetime = new AbortController();
47
+ revocations = new Set();
48
+ attempt;
49
+ listeners = new Set();
50
+ lastProfile;
51
+ detailsLifetime = new AbortController();
52
+ closed = false;
53
+ removing;
54
+ /** @param ctx - Host with authorization and credentials services. @param config - deployment options. */
55
+ constructor(ctx, config = {}) {
56
+ super(ctx);
57
+ const resolved = Config(config);
58
+ this.embeddedPageDist = resolved.embeddedPageDist;
59
+ this.origin = platformOrigin(resolved.platformOrigin, resolved.allowLoopbackHttp);
60
+ const inference = new URL(resolved.inferenceOrigin);
61
+ if (!['http:', 'https:'].includes(inference.protocol) || inference.username || inference.password
62
+ || inference.pathname !== '/' || inference.search || inference.hash) {
63
+ throw new Error('account: inferenceOrigin must be an HTTP(S) origin without credentials, path, query or fragment');
64
+ }
65
+ this.inferenceOrigin = inference.origin;
66
+ this.rewriteBrowserOrigin = resolved.rewriteBrowserOrigin;
67
+ this.clientHeaders = desktopClientHeaders(resolved.desktopPlatform);
68
+ this.requestHeaders = platformHeaders(resolved.requestHeaders);
69
+ const accountHeaders = platformHeaders(resolved.accountRequestHeaders);
70
+ this.accountRequestHeaders = { ...this.requestHeaders, ...accountHeaders };
71
+ if (accountHeaders.cookie !== undefined) {
72
+ this.accountRequestHeaders.cookie = mergePlatformCookies(this.requestHeaders.cookie ?? '', accountHeaders.cookie);
73
+ }
74
+ this.requestTimeout = resolved.requestTimeoutMs;
75
+ this.attemptTimeout = resolved.attemptTimeoutMs;
76
+ this.logoutPolicy = {
77
+ maxRetries: resolved.logoutMaxRetries, delayMs: resolved.logoutRetryDelayMs, requestTimeoutMs: this.requestTimeout,
78
+ };
79
+ ctx.authorization.registerFlow({
80
+ key: KEY, label: 'DeepSeek', methods: [{ id: 'browser', label: 'DeepSeek' }],
81
+ run: (session) => {
82
+ const attempt = this.attempt;
83
+ if (attempt === undefined)
84
+ return Promise.reject(new PlatformAuthError('protocol'));
85
+ attempt.running = this.run(session, attempt);
86
+ return attempt.running;
87
+ },
88
+ });
89
+ ctx.on('credentials/record-updated', (key) => {
90
+ if (key !== KEY)
91
+ return;
92
+ this.invalidateDetails();
93
+ this.changed();
94
+ });
95
+ ctx.effect(() => async () => {
96
+ this.closed = true;
97
+ this.logoutLifetime.abort();
98
+ this.invalidateDetails();
99
+ const active = this.attempt;
100
+ if (active !== undefined) {
101
+ if (active.view.phase !== 'committing')
102
+ active.controller.abort();
103
+ await active.done;
104
+ }
105
+ await this.removing;
106
+ await Promise.all(this.revocations);
107
+ this.changed();
108
+ }, 'account: active attempt lifetime');
109
+ }
110
+ async [Service.init]() {
111
+ const record = await this.ctx.credentials.readRecord(KEY);
112
+ if (record === undefined)
113
+ return;
114
+ if (record.kind !== 'grant')
115
+ throw new PlatformAuthError('storage');
116
+ const parsed = grant.safeParse(record.payload);
117
+ if (!parsed.success)
118
+ throw new PlatformAuthError('storage');
119
+ if (parsed.data.issuer === this.origin)
120
+ return;
121
+ await this.ctx.credentials.deleteRecord(KEY);
122
+ console.info('[deepseek-account] stored grant discarded', { reason: 'issuer-mismatch' });
123
+ }
124
+ async getState() {
125
+ const record = await this.ctx.credentials.readRecord(KEY);
126
+ if (record !== undefined && (record.kind !== 'grant' || !grant.safeParse(record.payload).success)) {
127
+ throw new PlatformAuthError('storage');
128
+ }
129
+ return {
130
+ status: record === undefined ? 'signed-out' : 'credential-stored', attempt: this.attempt?.view ?? null,
131
+ links: { usageUrl: new URL('/usage', this.origin).href, topUpUrl: new URL('/top_up', this.origin).href },
132
+ };
133
+ }
134
+ async getProfile() {
135
+ const lifetime = this.detailsLifetime;
136
+ const result = await this.getDetail('profile');
137
+ if (this.detailsLifetime !== lifetime)
138
+ return null;
139
+ if (result?.status === 'ready')
140
+ this.lastProfile = result;
141
+ return result?.status === 'failed' ? this.lastProfile ?? result : result;
142
+ }
143
+ getBalance() { return this.getDetail('balance'); }
144
+ async getDetail(field) {
145
+ const lifetime = this.detailsLifetime;
146
+ const stored = await this.readCurrentGrant(lifetime);
147
+ if (stored === null || lifetime.signal.aborted)
148
+ return null;
149
+ if (field === 'profile' && this.attempt?.initialProfile?.token === stored.token) {
150
+ const initial = this.attempt.initialProfile.value;
151
+ delete this.attempt.initialProfile;
152
+ return initial;
153
+ }
154
+ const details = await readAccountDetail(field, this.origin, stored.token, AbortSignal.any([lifetime.signal, AbortSignal.timeout(this.requestTimeout)]), { ...this.accountRequestHeaders, ...this.clientHeaders });
155
+ return this.detailsLifetime !== lifetime ? null : details;
156
+ }
157
+ async getPlatformSession() {
158
+ const lifetime = this.detailsLifetime;
159
+ const stored = await this.readCurrentGrant(lifetime);
160
+ if (stored === null || lifetime.signal.aborted)
161
+ return null;
162
+ const requestHeaders = { ...this.accountRequestHeaders, ...this.clientHeaders };
163
+ return { origin: this.origin, token: stored.token,
164
+ ...(this.embeddedPageDist ? { embeddedPageDist: this.embeddedPageDist } : {}),
165
+ ...(Object.keys(requestHeaders).length ? { requestHeaders } : {}) };
166
+ }
167
+ async readCurrentGrant(lifetime) {
168
+ if (this.closed)
169
+ return null;
170
+ const record = await this.ctx.credentials.readRecord(KEY);
171
+ if (record === undefined || lifetime.signal.aborted)
172
+ return null;
173
+ if (record.kind !== 'grant')
174
+ throw new PlatformAuthError('storage');
175
+ const parsed = grant.safeParse(record.payload);
176
+ if (!parsed.success)
177
+ throw new PlatformAuthError('storage');
178
+ // A private origin override must never forward a grant issued by another environment.
179
+ if (parsed.data.issuer !== this.origin)
180
+ throw new PlatformAuthError('protocol');
181
+ return parsed.data;
182
+ }
183
+ async resolveToken(url) {
184
+ const destination = new URL(url);
185
+ if (destination.origin !== this.inferenceOrigin || destination.username || destination.password)
186
+ return undefined;
187
+ const record = await this.ctx.credentials.readRecord(KEY);
188
+ if (record === undefined)
189
+ return undefined;
190
+ if (record.kind !== 'grant')
191
+ throw new PlatformAuthError('storage');
192
+ const result = grant.safeParse(record.payload);
193
+ if (!result.success)
194
+ throw new PlatformAuthError('storage');
195
+ // Development grants cannot authenticate production model or file requests.
196
+ const issuer = new URL(result.data.issuer);
197
+ if (result.data.token.startsWith('dsh_mock_'))
198
+ return undefined;
199
+ if (this.inferenceOrigin === 'https://api.deepseek.com') {
200
+ if (['localhost', '127.0.0.1', '[::1]'].includes(issuer.hostname))
201
+ return undefined;
202
+ }
203
+ else if (issuer.origin !== this.origin)
204
+ return undefined;
205
+ return result.data.token;
206
+ }
207
+ async startSignIn(locale, callbackOrigin, loginSource) {
208
+ if (this.removing !== undefined)
209
+ await this.removing;
210
+ const origin = loginOrigin(callbackOrigin);
211
+ if (this.closed)
212
+ throw new PlatformAuthError('protocol');
213
+ if (this.attempt !== undefined && ['initializing', 'waiting-browser', 'exchanging', 'committing'].includes(this.attempt.view.phase)) {
214
+ return this.getState();
215
+ }
216
+ const previous = this.attempt;
217
+ if (previous !== undefined) {
218
+ await previous.done;
219
+ // Disposal can close the provider while the previous attempt settles.
220
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
221
+ if (this.closed)
222
+ throw new PlatformAuthError('protocol');
223
+ if (this.attempt !== previous)
224
+ return this.getState();
225
+ }
226
+ const attempt = {
227
+ origin, loginSource,
228
+ locale: locale.toLowerCase().split(/[-_]/)[0] === 'zh' ? 'zh_CN' : 'en_US',
229
+ view: { id: randomUUID(), phase: 'initializing' },
230
+ controller: new AbortController(), done: Promise.resolve(), running: Promise.resolve(),
231
+ };
232
+ this.attempt = attempt;
233
+ attempt.done = this.ctx.authorization.begin({
234
+ key: KEY, signal: attempt.controller.signal,
235
+ interaction: { notify: () => undefined, prompt: () => Promise.reject(new PlatformAuthError('protocol')) },
236
+ }).then((outcome) => {
237
+ this.update(attempt, { phase: outcome.status === 'authorized' ? 'succeeded' : 'cancelled' });
238
+ if (outcome.status === 'authorized' && attempt.completionUrl !== undefined) {
239
+ attempt.callback?.writeHead(302, { location: attempt.completionUrl, 'cache-control': 'no-store' }).end();
240
+ }
241
+ else
242
+ attempt.callback?.writeHead(204, { 'cache-control': 'no-store' }).end();
243
+ }).catch((error) => {
244
+ const code = error instanceof PlatformAuthError ? error.code : 'protocol';
245
+ console.info('[deepseek-account] sign-in failed', { errorCode: code });
246
+ this.update(attempt, { phase: code === 'expired' ? 'expired' : 'failed', errorCode: code });
247
+ this.finishFailedCallback(attempt);
248
+ }).then(async () => {
249
+ // begin() may report cancellation before the HTTP work observes its signal.
250
+ await attempt.running.catch(() => undefined);
251
+ if (attempt.callback !== undefined) {
252
+ await finished(attempt.callback, { cleanup: true }).catch(() => undefined);
253
+ }
254
+ await attempt.disposeCallback?.();
255
+ });
256
+ this.changed();
257
+ return this.getState();
258
+ }
259
+ async cancelSignIn(id) {
260
+ const attempt = this.attempt;
261
+ if (attempt?.view.id === id) {
262
+ if (attempt.view.phase !== 'committing') {
263
+ attempt.controller.abort();
264
+ this.ctx.authorization.cancel(KEY);
265
+ }
266
+ await attempt.done;
267
+ }
268
+ return this.getState();
269
+ }
270
+ signOut() {
271
+ this.removing ??= (async () => {
272
+ if (this.closed)
273
+ throw new PlatformAuthError('protocol');
274
+ if (this.attempt !== undefined)
275
+ await this.cancelSignIn(this.attempt.view.id);
276
+ const record = await this.ctx.credentials.readRecord(KEY);
277
+ if (record !== undefined) {
278
+ if (record.kind !== 'grant')
279
+ throw new PlatformAuthError('storage');
280
+ const parsed = grant.safeParse(record.payload);
281
+ if (!parsed.success)
282
+ throw new PlatformAuthError('storage');
283
+ if (parsed.data.issuer !== this.origin)
284
+ throw new PlatformAuthError('protocol');
285
+ await this.ctx.credentials.deleteRecord(KEY);
286
+ this.revoke(parsed.data.token);
287
+ }
288
+ this.attempt = undefined;
289
+ this.changed();
290
+ return this.getState();
291
+ })().finally(() => { this.removing = undefined; });
292
+ return this.removing;
293
+ }
294
+ async *watch(signal) {
295
+ let dirty = true;
296
+ let wake;
297
+ const changed = () => { dirty = true; wake?.(); };
298
+ this.listeners.add(changed);
299
+ signal.addEventListener('abort', changed, { once: true });
300
+ try {
301
+ while (!this.closed && !signal.aborted) {
302
+ if (dirty) {
303
+ dirty = false;
304
+ yield await this.getState();
305
+ continue;
306
+ }
307
+ await new Promise((resolve) => { wake = resolve; });
308
+ }
309
+ }
310
+ finally {
311
+ this.listeners.delete(changed);
312
+ signal.removeEventListener('abort', changed);
313
+ }
314
+ }
315
+ revoke(token) {
316
+ if (this.closed)
317
+ return;
318
+ const revocation = revokeAccount(this.origin, token, this.logoutPolicy, this.logoutLifetime.signal, { ...this.requestHeaders, ...this.clientHeaders }).finally(() => { this.revocations.delete(revocation); });
319
+ this.revocations.add(revocation);
320
+ }
321
+ invalidateDetails() {
322
+ this.lastProfile = undefined;
323
+ this.detailsLifetime.abort();
324
+ this.detailsLifetime = new AbortController();
325
+ }
326
+ changed() { for (const listener of this.listeners)
327
+ listener(); }
328
+ update(attempt, value) {
329
+ const { authorizeUrl: _url, ...rest } = attempt.view;
330
+ attempt.view = { ...rest, ...value };
331
+ this.changed();
332
+ }
333
+ async run(session, attempt) {
334
+ const webServer = this.ctx.get('webServer');
335
+ if (webServer === undefined)
336
+ throw new PlatformAuthError('protocol');
337
+ const verifier = randomBytes(32).toString('base64url');
338
+ const state = randomBytes(32).toString('base64url');
339
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
340
+ let authorizeId;
341
+ const code = Promise.withResolvers();
342
+ // Callback requests may precede initialization settlement; the rejection is observed immediately.
343
+ void code.promise.catch(() => undefined);
344
+ const deadline = new AbortController();
345
+ let expiresAt = Date.now() + this.attemptTimeout;
346
+ const signal = AbortSignal.any([session.signal, deadline.signal]);
347
+ let timer = setTimeout(() => { deadline.abort(); }, this.attemptTimeout);
348
+ const abort = () => { code.reject(new PlatformAuthError('expired')); };
349
+ signal.addEventListener('abort', abort, { once: true });
350
+ try {
351
+ attempt.disposeCallback = this.ctx.effect(() => webServer.register({
352
+ kind: 'exact', path: '/oauth/callback', handler: (req, res) => {
353
+ let url;
354
+ try {
355
+ url = new URL(req.url ?? '/', 'http://127.0.0.1');
356
+ }
357
+ catch {
358
+ res.writeHead(400, { 'cache-control': 'no-store' }).end();
359
+ return;
360
+ }
361
+ const receivedCode = url.searchParams.get('code');
362
+ const receivedState = url.searchParams.get('state') ?? '';
363
+ const validState = Buffer.byteLength(receivedState) === Buffer.byteLength(state)
364
+ && timingSafeEqual(Buffer.from(receivedState), Buffer.from(state));
365
+ if (req.method !== 'GET' || url.pathname !== '/oauth/callback' || !validState || !receivedCode
366
+ || url.searchParams.getAll('state').length !== 1 || url.searchParams.getAll('code').length !== 1) {
367
+ res.writeHead(400, { 'cache-control': 'no-store' }).end();
368
+ return;
369
+ }
370
+ if (signal.aborted || attempt.callback !== undefined || attempt.view.phase !== 'waiting-browser') {
371
+ res.writeHead(410, { 'cache-control': 'no-store' }).end();
372
+ return;
373
+ }
374
+ attempt.callback = res;
375
+ code.resolve(receivedCode);
376
+ },
377
+ }), 'account: browser callback');
378
+ signal.throwIfAborted();
379
+ const redirectUri = `${attempt.origin}/oauth/callback`;
380
+ const init = initialization.safeParse(await this.request('auth_init', {
381
+ code_challenge: challenge, code_challenge_method: 'S256', state, redirect_uri: redirectUri, locale: attempt.locale,
382
+ login_source: attempt.loginSource,
383
+ }, signal), { reportInput: true });
384
+ if (!init.success)
385
+ this.rejectPayload('auth_init', init.error);
386
+ const authorizeUrl = browserUrl(init.data.authorize_url, this.origin, '/dsh/authorize', this.rewriteBrowserOrigin);
387
+ authorizeId = init.data.authorize_id;
388
+ signal.throwIfAborted();
389
+ const now = Date.now();
390
+ expiresAt = Math.min(expiresAt, now + init.data.expires_in * 1000);
391
+ const remaining = expiresAt - now;
392
+ if (remaining <= 0) {
393
+ deadline.abort();
394
+ signal.throwIfAborted();
395
+ }
396
+ clearTimeout(timer);
397
+ timer = setTimeout(() => { deadline.abort(); }, remaining);
398
+ this.update(attempt, { phase: 'waiting-browser', authorizeUrl, expiresAt });
399
+ const receivedCode = await code.promise;
400
+ signal.throwIfAborted();
401
+ this.update(attempt, { phase: 'exchanging' });
402
+ const deviceRecord = await this.ctx.credentials.modifyRecord(DEVICE, current => Promise.resolve(current === undefined
403
+ ? { kind: 'grant', payload: { id: randomUUID() } } : undefined));
404
+ if (deviceRecord?.kind !== 'grant')
405
+ throw new PlatformAuthError('storage');
406
+ const identity = device.parse(deviceRecord.payload);
407
+ const result = exchange.safeParse(await this.request('auth_exchange', {
408
+ code: receivedCode, code_verifier: verifier, redirect_uri: redirectUri,
409
+ device_id: identity.id, device_model: `${platform()}-${arch()}`, os_version: `${platform()} ${release()}`,
410
+ }, signal), { reportInput: true });
411
+ if (!result.success)
412
+ this.rejectPayload('auth_exchange', result.error);
413
+ const completionUrl = new URL(browserUrl(result.data.authorized_url, this.origin, '/dsh/authorized', this.rewriteBrowserOrigin));
414
+ completionUrl.searchParams.set('login_source', attempt.loginSource);
415
+ attempt.completionUrl = completionUrl.href;
416
+ if (Date.now() >= expiresAt)
417
+ deadline.abort();
418
+ signal.throwIfAborted();
419
+ if (result.data.user != null) {
420
+ try {
421
+ attempt.initialProfile = { token: result.data.token, value: { status: 'ready', value: profile(result.data.user) } };
422
+ }
423
+ catch {
424
+ // Invalid optional user data falls back to current without discarding a valid authorization.
425
+ }
426
+ }
427
+ this.update(attempt, { phase: 'committing' });
428
+ clearTimeout(timer);
429
+ try {
430
+ await session.commit({ kind: 'grant', payload: { version: 1, token: result.data.token, issuer: this.origin } });
431
+ }
432
+ catch {
433
+ throw new PlatformAuthError('storage');
434
+ }
435
+ }
436
+ catch (error) {
437
+ if (deadline.signal.aborted)
438
+ throw new PlatformAuthError('expired');
439
+ throw error;
440
+ }
441
+ finally {
442
+ if (signal.aborted && authorizeId !== undefined)
443
+ this.cancelRequest(authorizeId, verifier);
444
+ clearTimeout(timer);
445
+ signal.removeEventListener('abort', abort);
446
+ // begin() settles the browser response and removes only this attempt’s route.
447
+ }
448
+ }
449
+ rejectPayload(stage, error) {
450
+ console.info('[deepseek-account] payload rejected', {
451
+ stage, issues: error.issues.map(issue => ({
452
+ path: issue.path, code: issue.code,
453
+ receivedType: issue.input === null ? 'null' : Array.isArray(issue.input) ? 'array' : typeof issue.input,
454
+ })),
455
+ });
456
+ throw new PlatformAuthError('protocol');
457
+ }
458
+ cancelRequest(authorizeId, verifier) {
459
+ if (this.closed)
460
+ return;
461
+ const cancellation = this.request('auth_cancel', { authorize_id: authorizeId, code_verifier: verifier }, this.logoutLifetime.signal).then(() => undefined, () => {
462
+ // Remote cancellation failures never reverse local cancellation or expose verifier diagnostics.
463
+ }).finally(() => { this.revocations.delete(cancellation); });
464
+ this.revocations.add(cancellation);
465
+ }
466
+ request(method, body, signal) {
467
+ return requestPlatform(this.origin, method, body, AbortSignal.any([signal, AbortSignal.timeout(this.requestTimeout)]), { ...this.requestHeaders, ...this.clientHeaders });
468
+ }
469
+ finishFailedCallback(attempt) {
470
+ if (attempt.loginSource === 'web') {
471
+ const nonce = randomBytes(16).toString('base64url');
472
+ const message = attempt.locale === 'zh_CN' ? '登录失败,请关闭此标签页并在原页面重试。'
473
+ : 'Sign-in failed. Close this tab and try again in the original tab.';
474
+ attempt.callback?.writeHead(200, {
475
+ 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store',
476
+ 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; frame-ancestors 'none'`,
477
+ 'referrer-policy': 'no-referrer',
478
+ }).end(`<!doctype html><html lang="${attempt.locale === 'zh_CN' ? 'zh-CN' : 'en'}"><meta charset="utf-8"><title>${message}</title>`
479
+ + `<body><p>${message}</p><script nonce="${nonce}">window.close()</script></body></html>`);
480
+ }
481
+ else {
482
+ // Native account subscribers focus the login window; retain the browser’s Platform document.
483
+ attempt.callback?.writeHead(204, { 'cache-control': 'no-store' }).end();
484
+ }
485
+ }
486
+ }
487
+ export default PlatformAccount;
488
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,17 @@
1
+ /** Resolved logout retry settings owned by the account provider. */
2
+ export interface LogoutRetryPolicy {
3
+ readonly maxRetries: number;
4
+ readonly delayMs: number;
5
+ readonly requestTimeoutMs: number;
6
+ }
7
+ /**
8
+ * Revoke one captured grant without changing local account state.
9
+ * @param origin - Issuer verified before local grant removal.
10
+ * @param token - Removed grant, never replaced with a later login's token.
11
+ * @param policy - Request deadline and exponential retry limits.
12
+ * @param signal - Provider lifetime; shutdown aborts requests and delays.
13
+ * @param headers - Validated issuer-only deployment headers.
14
+ * @returns After success, exhaustion, or cancellation; remote failures stay in the background.
15
+ */
16
+ export declare function revokeAccount(origin: string, token: string, policy: LogoutRetryPolicy, signal: AbortSignal, headers: Record<string, string>): Promise<void>;
17
+ //# sourceMappingURL=logout.d.ts.map
@@ -0,0 +1,34 @@
1
+ /** Bounded, memory-only revocation of a token already removed from local credentials. */
2
+ import { setTimeout } from 'node:timers/promises';
3
+ import { logoutAccount } from "./protocol.js";
4
+ /**
5
+ * Revoke one captured grant without changing local account state.
6
+ * @param origin - Issuer verified before local grant removal.
7
+ * @param token - Removed grant, never replaced with a later login's token.
8
+ * @param policy - Request deadline and exponential retry limits.
9
+ * @param signal - Provider lifetime; shutdown aborts requests and delays.
10
+ * @param headers - Validated issuer-only deployment headers.
11
+ * @returns After success, exhaustion, or cancellation; remote failures stay in the background.
12
+ */
13
+ export async function revokeAccount(origin, token, policy, signal, headers) {
14
+ for (let attempt = 0; attempt <= policy.maxRetries; attempt++) {
15
+ if (signal.aborted)
16
+ return;
17
+ if (attempt > 0) {
18
+ try {
19
+ await setTimeout(policy.delayMs * 2 ** (attempt - 1), undefined, { signal, ref: false });
20
+ }
21
+ catch {
22
+ return;
23
+ } // The validated delay rejects only when the provider lifetime ends.
24
+ }
25
+ try {
26
+ await logoutAccount(origin, token, AbortSignal.any([signal, AbortSignal.timeout(policy.requestTimeoutMs)]), headers);
27
+ return;
28
+ }
29
+ catch {
30
+ // HTTP, business, and transport failures consume one attempt without restoring credentials.
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1,78 @@
1
+ /** Validated platform HTTP messages and restricted browser destinations. */
2
+ import { z } from 'zod';
3
+ /** Protocol errors expose a stable code, never a response body or authorization URL. */
4
+ export declare class PlatformAuthError extends Error {
5
+ readonly code: 'network' | 'protocol' | 'expired' | 'storage';
6
+ /** @param code - safe error classification. */
7
+ constructor(code: 'network' | 'protocol' | 'expired' | 'storage');
8
+ }
9
+ /**
10
+ * Accept HTTPS platform endpoints, or explicitly configured loopback development HTTP.
11
+ * @param value - configured origin.
12
+ * @param allowLoopbackHttp - development-only opt-in.
13
+ * @returns normalized origin.
14
+ */
15
+ export declare function platformOrigin(value: string, allowLoopbackHttp: boolean): string;
16
+ /**
17
+ * Validate platform-owned browser destinations without forwarding arbitrary URLs.
18
+ * @param value - returned browser URL.
19
+ * @param origin - configured platform origin.
20
+ * @param path - fixed authorize or completion path.
21
+ * @param rewriteOrigin - map validated browser pages to the configured development origin.
22
+ * @returns normalized URL on the configured origin.
23
+ */
24
+ export declare function browserUrl(value: string, origin: string, path: string, rewriteOrigin?: boolean): string;
25
+ /**
26
+ * Validate Host-only deployment headers without exposing their values in diagnostics.
27
+ * @param values - configured headers for the Platform origin.
28
+ * @returns normalized headers; authorization, routing and framing remain provider-owned.
29
+ */
30
+ export declare function platformHeaders(values: Record<string, string>): Record<string, string>;
31
+ /** Successful initialization response. */
32
+ export declare const initialization: z.ZodObject<{
33
+ authorize_url: z.ZodURL;
34
+ authorize_id: z.ZodString;
35
+ expires_in: z.ZodNumber;
36
+ }, z.core.$strip>;
37
+ /** Successful code exchange response. */
38
+ export declare const exchange: z.ZodObject<{
39
+ token: z.ZodString;
40
+ authorized_url: z.ZodURL;
41
+ user: z.ZodOptional<z.ZodUnknown>;
42
+ }, z.core.$strip>;
43
+ /**
44
+ * Read one bounded platform response with stable, non-secret diagnostics.
45
+ * @param origin - validated platform origin.
46
+ * @param method - platform endpoint suffix.
47
+ * @param body - protocol request, never logged.
48
+ * @param signal - attempt cancellation and timeout.
49
+ * @param headers - validated deployment headers for this origin.
50
+ * @returns successful business payload, validated by its caller.
51
+ */
52
+ export declare function requestPlatform(origin: string, method: string, body: unknown, signal: AbortSignal, headers: Record<string, string>): Promise<unknown>;
53
+ /**
54
+ * Fetch a fixed Platform account endpoint with the grant kept in Host request headers.
55
+ * @param origin - configured and grant-matched origin.
56
+ * @param path - account endpoint.
57
+ * @param token - account grant.
58
+ * @param signal - credential lifetime and request timeout.
59
+ * @param headers - validated deployment headers for this origin.
60
+ * @returns successful business payload.
61
+ */
62
+ export declare function requestAccount(origin: string, path: '/auth-api/v0/users/current' | '/api/v0/users/get_user_summary', token: string, signal: AbortSignal, headers: Record<string, string>): Promise<unknown>;
63
+ /**
64
+ * End the Platform session using its existing logout endpoint.
65
+ * @param origin - configured origin matching the grant issuer.
66
+ * @param token - stored account token.
67
+ * @param signal - logout request deadline.
68
+ * @param headers - validated deployment headers for this origin.
69
+ * @returns after Platform confirms logout.
70
+ */
71
+ export declare function logoutAccount(origin: string, token: string, signal: AbortSignal, headers: Record<string, string>): Promise<void>;
72
+ /**
73
+ * Accept a browser-accessible loopback HTTP origin for local or SSH-forwarded login.
74
+ * @param value - loopback HTTP origin with an explicit port supplied by the authenticated initiating client.
75
+ * @returns normalized origin; remote domains and path-based proxies are unsupported.
76
+ */
77
+ export declare function loginOrigin(value: string): string;
78
+ //# sourceMappingURL=protocol.d.ts.map