@moxxy/plugin-oauth 0.0.33
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/dist/adapters/openai-device-flow.d.ts +34 -0
- package/dist/adapters/openai-device-flow.d.ts.map +1 -0
- package/dist/adapters/openai-device-flow.js +136 -0
- package/dist/adapters/openai-device-flow.js.map +1 -0
- package/dist/adapters/rfc8628-device-flow.d.ts +20 -0
- package/dist/adapters/rfc8628-device-flow.d.ts.map +1 -0
- package/dist/adapters/rfc8628-device-flow.js +54 -0
- package/dist/adapters/rfc8628-device-flow.js.map +1 -0
- package/dist/credential-lock.d.ts +60 -0
- package/dist/credential-lock.d.ts.map +1 -0
- package/dist/credential-lock.js +149 -0
- package/dist/credential-lock.js.map +1 -0
- package/dist/ensure-fresh.d.ts +63 -0
- package/dist/ensure-fresh.d.ts.map +1 -0
- package/dist/ensure-fresh.js +122 -0
- package/dist/ensure-fresh.js.map +1 -0
- package/dist/flow.d.ts +6 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +5 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/browser-flow.d.ts +23 -0
- package/dist/oauth/browser-flow.d.ts.map +1 -0
- package/dist/oauth/browser-flow.js +82 -0
- package/dist/oauth/browser-flow.js.map +1 -0
- package/dist/oauth/callback-server.d.ts +10 -0
- package/dist/oauth/callback-server.d.ts.map +1 -0
- package/dist/oauth/callback-server.js +158 -0
- package/dist/oauth/callback-server.js.map +1 -0
- package/dist/oauth/device-flow-shared.d.ts +39 -0
- package/dist/oauth/device-flow-shared.d.ts.map +1 -0
- package/dist/oauth/device-flow-shared.js +101 -0
- package/dist/oauth/device-flow-shared.js.map +1 -0
- package/dist/oauth/device-flow.d.ts +29 -0
- package/dist/oauth/device-flow.d.ts.map +1 -0
- package/dist/oauth/device-flow.js +74 -0
- package/dist/oauth/device-flow.js.map +1 -0
- package/dist/oauth/poll-until.d.ts +41 -0
- package/dist/oauth/poll-until.d.ts.map +1 -0
- package/dist/oauth/poll-until.js +70 -0
- package/dist/oauth/poll-until.js.map +1 -0
- package/dist/oauth/token-exchange.d.ts +28 -0
- package/dist/oauth/token-exchange.d.ts.map +1 -0
- package/dist/oauth/token-exchange.js +142 -0
- package/dist/oauth/token-exchange.js.map +1 -0
- package/dist/oauth/types.d.ts +90 -0
- package/dist/oauth/types.d.ts.map +1 -0
- package/dist/oauth/types.js +2 -0
- package/dist/oauth/types.js.map +1 -0
- package/dist/open-browser.d.ts +32 -0
- package/dist/open-browser.d.ts.map +1 -0
- package/dist/open-browser.js +83 -0
- package/dist/open-browser.js.map +1 -0
- package/dist/pkce.d.ts +20 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +34 -0
- package/dist/pkce.js.map +1 -0
- package/dist/profile.d.ts +129 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +11 -0
- package/dist/profile.js.map +1 -0
- package/dist/run-login.d.ts +9 -0
- package/dist/run-login.d.ts.map +1 -0
- package/dist/run-login.js +93 -0
- package/dist/run-login.js.map +1 -0
- package/dist/storage.d.ts +46 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +167 -0
- package/dist/storage.js.map +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +283 -0
- package/dist/tools.js.map +1 -0
- package/package.json +73 -0
- package/skills/google-oauth.md +313 -0
- package/skills/oauth-flow.md +132 -0
- package/src/adapters/openai-device-flow.test.ts +191 -0
- package/src/adapters/openai-device-flow.ts +179 -0
- package/src/adapters/rfc8628-device-flow.ts +75 -0
- package/src/credential-lock.ts +183 -0
- package/src/discovery.test.ts +35 -0
- package/src/ensure-fresh.test.ts +279 -0
- package/src/ensure-fresh.ts +178 -0
- package/src/flow.ts +9 -0
- package/src/index.ts +153 -0
- package/src/oauth/browser-flow.ts +96 -0
- package/src/oauth/callback-server.test.ts +90 -0
- package/src/oauth/callback-server.ts +208 -0
- package/src/oauth/device-flow-shared.test.ts +142 -0
- package/src/oauth/device-flow-shared.ts +129 -0
- package/src/oauth/device-flow.test.ts +102 -0
- package/src/oauth/device-flow.ts +82 -0
- package/src/oauth/poll-until.test.ts +63 -0
- package/src/oauth/poll-until.ts +100 -0
- package/src/oauth/token-exchange.test.ts +224 -0
- package/src/oauth/token-exchange.ts +169 -0
- package/src/oauth/types.ts +92 -0
- package/src/oauth.test.ts +185 -0
- package/src/open-browser-spawn.test.ts +69 -0
- package/src/open-browser.test.ts +30 -0
- package/src/open-browser.ts +84 -0
- package/src/pkce.ts +37 -0
- package/src/profile.test.ts +331 -0
- package/src/profile.ts +135 -0
- package/src/run-login.ts +123 -0
- package/src/storage.test.ts +191 -0
- package/src/storage.ts +208 -0
- package/src/tools.test.ts +239 -0
- package/src/tools.ts +332 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow-shared.d.ts","sourceRoot":"","sources":["../../src/oauth/device-flow-shared.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAyD/B;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACtD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC5B,WAAW,CAAC,QAAQ,CAAC,CA8BvB"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { classifyHttpStatus, MoxxyError } from '@moxxy/sdk';
|
|
2
|
+
import { parseTokenResponse } from './token-exchange.js';
|
|
3
|
+
/**
|
|
4
|
+
* POST the RFC 8628 device-authorization request and parse the response. The
|
|
5
|
+
* single home for the request + field parsing + error handling shared by the
|
|
6
|
+
* legacy `runDeviceCodeFlow` and the `rfc8628DeviceFlow` adapter.
|
|
7
|
+
*/
|
|
8
|
+
export async function requestDeviceAuthorization(args) {
|
|
9
|
+
const body = new URLSearchParams();
|
|
10
|
+
body.set('client_id', args.clientId);
|
|
11
|
+
body.set('scope', args.scopes.join(' '));
|
|
12
|
+
const res = await fetch(args.deviceUrl, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
15
|
+
body: body.toString(),
|
|
16
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const text = await res.text().catch(() => '');
|
|
20
|
+
throw (classifyHttpStatus(res.status, { url: args.deviceUrl, body: text }) ??
|
|
21
|
+
new MoxxyError({
|
|
22
|
+
code: 'AUTH_INVALID',
|
|
23
|
+
message: `device-code request failed (HTTP ${res.status}): ${text.slice(0, 300)}`,
|
|
24
|
+
context: { status: res.status, url: args.deviceUrl },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
// A 200 with a non-JSON/truncated body (proxy/captive-portal error page)
|
|
28
|
+
// would make res.json() reject with a raw SyntaxError that escapes the
|
|
29
|
+
// MoxxyError boundary; convert it to a typed PROVIDER_UNKNOWN_RESPONSE.
|
|
30
|
+
const json = (await res.json().catch(() => {
|
|
31
|
+
throw new MoxxyError({
|
|
32
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
33
|
+
message: 'device-authorization endpoint returned a non-JSON success body',
|
|
34
|
+
context: { url: args.deviceUrl },
|
|
35
|
+
});
|
|
36
|
+
}));
|
|
37
|
+
const deviceCode = typeof json.device_code === 'string' ? json.device_code : null;
|
|
38
|
+
const userCode = typeof json.user_code === 'string' ? json.user_code : null;
|
|
39
|
+
const verificationUri = typeof json.verification_uri === 'string'
|
|
40
|
+
? json.verification_uri
|
|
41
|
+
: typeof json.verification_url === 'string'
|
|
42
|
+
? json.verification_url
|
|
43
|
+
: null;
|
|
44
|
+
const verificationUriComplete = typeof json.verification_uri_complete === 'string' ? json.verification_uri_complete : undefined;
|
|
45
|
+
const expiresInSec = typeof json.expires_in === 'number' ? json.expires_in : 600;
|
|
46
|
+
const intervalSec = typeof json.interval === 'number' ? json.interval : 5;
|
|
47
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
48
|
+
throw new MoxxyError({
|
|
49
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
50
|
+
message: `device-code response missing required fields: ${JSON.stringify(json).slice(0, 200)}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
deviceCode,
|
|
55
|
+
userCode,
|
|
56
|
+
verificationUri,
|
|
57
|
+
...(verificationUriComplete ? { verificationUriComplete } : {}),
|
|
58
|
+
expiresInSec,
|
|
59
|
+
intervalSec,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Classify a device-flow token-poll response per RFC 8628 §3.5: success →
|
|
64
|
+
* `done`; `authorization_pending` → keep waiting; `slow_down` → +5s and wait;
|
|
65
|
+
* `access_denied` / `expired_token` / other → throw a MoxxyError. Shared by both
|
|
66
|
+
* poll loops; each caller builds its own request body (the legacy flow appends
|
|
67
|
+
* client_id/client_secret) and hands the response + parsed JSON here.
|
|
68
|
+
*/
|
|
69
|
+
export function classifyDeviceTokenResponse(res, json, state) {
|
|
70
|
+
if (res.ok && typeof json.access_token === 'string') {
|
|
71
|
+
return { done: parseTokenResponse(json) };
|
|
72
|
+
}
|
|
73
|
+
const err = typeof json.error === 'string' ? json.error : `HTTP ${res.status}`;
|
|
74
|
+
if (err === 'authorization_pending')
|
|
75
|
+
return { pending: true };
|
|
76
|
+
if (err === 'slow_down') {
|
|
77
|
+
state.intervalMs += 5000;
|
|
78
|
+
return { pending: true };
|
|
79
|
+
}
|
|
80
|
+
if (err === 'access_denied') {
|
|
81
|
+
throw new MoxxyError({
|
|
82
|
+
code: 'OAUTH_FLOW_DENIED',
|
|
83
|
+
message: 'You declined the device authorization.',
|
|
84
|
+
hint: 'Re-run the login command and approve the consent screen on your browser device.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (err === 'expired_token') {
|
|
88
|
+
throw new MoxxyError({
|
|
89
|
+
code: 'OAUTH_FLOW_TIMEOUT',
|
|
90
|
+
message: 'The device code expired before you finished signing in.',
|
|
91
|
+
hint: 'Re-run the login command — a new code will be generated.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const desc = typeof json.error_description === 'string' ? json.error_description : '';
|
|
95
|
+
throw new MoxxyError({
|
|
96
|
+
code: 'AUTH_INVALID',
|
|
97
|
+
message: `OAuth device flow failed: ${err}${desc ? ` — ${desc}` : ''}.`,
|
|
98
|
+
context: { provider_error: String(err), ...(desc ? { description: desc } : {}) },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=device-flow-shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow-shared.js","sourceRoot":"","sources":["../../src/oauth/device-flow-shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DeviceFlowOptions, TokenSet } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the RFC 8628 device-authorization flow. Suitable for headless
|
|
4
|
+
* environments (SSH session, CI, kiosk, no display): the user opens
|
|
5
|
+
* the verification URL on any device, types the short user_code,
|
|
6
|
+
* approves the scopes, and the local process discovers the grant by
|
|
7
|
+
* polling the token endpoint.
|
|
8
|
+
*
|
|
9
|
+
* Phases:
|
|
10
|
+
* 1. POST `client_id` + `scope` to deviceUrl → returns user_code,
|
|
11
|
+
* verification_uri, device_code, expires_in, interval.
|
|
12
|
+
* 2. `onPrompt` fires once with the user-facing pieces — the caller
|
|
13
|
+
* surfaces them in whatever UI it has.
|
|
14
|
+
* 3. Poll tokenUrl every `interval` seconds with
|
|
15
|
+
* grant_type=urn:ietf:params:oauth:grant-type:device_code +
|
|
16
|
+
* device_code. The provider replies:
|
|
17
|
+
* - authorization_pending → keep polling.
|
|
18
|
+
* - slow_down → bump interval by 5s and keep polling.
|
|
19
|
+
* - access_denied → user clicked deny; throw.
|
|
20
|
+
* - expired_token → device_code expired; throw.
|
|
21
|
+
* - access_token, ... → success; return TokenSet.
|
|
22
|
+
*
|
|
23
|
+
* The device-authorization request + the poll-response classification are
|
|
24
|
+
* shared with the {@link rfc8628DeviceFlow} adapter via `device-flow-shared`.
|
|
25
|
+
* This flow additionally sends `client_id` (+ optional `client_secret`) on the
|
|
26
|
+
* poll, which is why it builds its own poll body.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runDeviceCodeFlow(opts: DeviceFlowOptions): Promise<TokenSet>;
|
|
29
|
+
//# sourceMappingURL=device-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/oauth/device-flow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU9D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuBlF"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { pollUntil } from './poll-until.js';
|
|
2
|
+
import { classifyDeviceTokenResponse, requestDeviceAuthorization } from './device-flow-shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Small margin added to the provider's interval before the first/each poll, so
|
|
5
|
+
* a server that enforces the interval strictly doesn't answer edge polls with
|
|
6
|
+
* `slow_down`. Mirrors `run-login.ts`'s margin so both device-flow drivers
|
|
7
|
+
* behave identically.
|
|
8
|
+
*/
|
|
9
|
+
const DEVICE_POLL_SAFETY_MARGIN_MS = 3000;
|
|
10
|
+
/**
|
|
11
|
+
* Run the RFC 8628 device-authorization flow. Suitable for headless
|
|
12
|
+
* environments (SSH session, CI, kiosk, no display): the user opens
|
|
13
|
+
* the verification URL on any device, types the short user_code,
|
|
14
|
+
* approves the scopes, and the local process discovers the grant by
|
|
15
|
+
* polling the token endpoint.
|
|
16
|
+
*
|
|
17
|
+
* Phases:
|
|
18
|
+
* 1. POST `client_id` + `scope` to deviceUrl → returns user_code,
|
|
19
|
+
* verification_uri, device_code, expires_in, interval.
|
|
20
|
+
* 2. `onPrompt` fires once with the user-facing pieces — the caller
|
|
21
|
+
* surfaces them in whatever UI it has.
|
|
22
|
+
* 3. Poll tokenUrl every `interval` seconds with
|
|
23
|
+
* grant_type=urn:ietf:params:oauth:grant-type:device_code +
|
|
24
|
+
* device_code. The provider replies:
|
|
25
|
+
* - authorization_pending → keep polling.
|
|
26
|
+
* - slow_down → bump interval by 5s and keep polling.
|
|
27
|
+
* - access_denied → user clicked deny; throw.
|
|
28
|
+
* - expired_token → device_code expired; throw.
|
|
29
|
+
* - access_token, ... → success; return TokenSet.
|
|
30
|
+
*
|
|
31
|
+
* The device-authorization request + the poll-response classification are
|
|
32
|
+
* shared with the {@link rfc8628DeviceFlow} adapter via `device-flow-shared`.
|
|
33
|
+
* This flow additionally sends `client_id` (+ optional `client_secret`) on the
|
|
34
|
+
* poll, which is why it builds its own poll body.
|
|
35
|
+
*/
|
|
36
|
+
export async function runDeviceCodeFlow(opts) {
|
|
37
|
+
const auth = await requestDeviceAuthorization({
|
|
38
|
+
deviceUrl: opts.deviceUrl,
|
|
39
|
+
clientId: opts.clientId,
|
|
40
|
+
scopes: opts.scopes,
|
|
41
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
42
|
+
});
|
|
43
|
+
opts.onPrompt({
|
|
44
|
+
userCode: auth.userCode,
|
|
45
|
+
verificationUri: auth.verificationUri,
|
|
46
|
+
...(auth.verificationUriComplete ? { verificationUriComplete: auth.verificationUriComplete } : {}),
|
|
47
|
+
expiresIn: auth.expiresInSec,
|
|
48
|
+
interval: auth.intervalSec,
|
|
49
|
+
});
|
|
50
|
+
return pollUntil((state) => pollOnce(opts, auth.deviceCode, state), {
|
|
51
|
+
intervalMs: auth.intervalSec * 1000 + DEVICE_POLL_SAFETY_MARGIN_MS,
|
|
52
|
+
timeoutMs: Math.min(opts.timeoutMs ?? auth.expiresInSec * 1000, auth.expiresInSec * 1000),
|
|
53
|
+
label: 'OAuth device flow',
|
|
54
|
+
leadingWait: true,
|
|
55
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function pollOnce(opts, deviceCode, state) {
|
|
59
|
+
const body = new URLSearchParams();
|
|
60
|
+
body.set('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
|
|
61
|
+
body.set('device_code', deviceCode);
|
|
62
|
+
body.set('client_id', opts.clientId);
|
|
63
|
+
if (opts.clientSecret)
|
|
64
|
+
body.set('client_secret', opts.clientSecret);
|
|
65
|
+
const pollRes = await fetch(opts.tokenUrl, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
68
|
+
body: body.toString(),
|
|
69
|
+
...(state.signal ? { signal: state.signal } : {}),
|
|
70
|
+
});
|
|
71
|
+
const pollJson = (await pollRes.json().catch(() => ({})));
|
|
72
|
+
return classifyDeviceTokenResponse(pollRes, pollJson, state);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=device-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/oauth/device-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared polling primitive for OAuth-style "ask again later" modes. Handles
|
|
3
|
+
* the gnarly bits — deadline math, abort-responsive sleep, interval bumps
|
|
4
|
+
* for `slow_down`-style backpressure — so each device-flow dialect only has
|
|
5
|
+
* to encode its HTTP shape, not the timing harness.
|
|
6
|
+
*
|
|
7
|
+
* Consumed by both `runDeviceCodeFlow` (RFC 8628) and the Codex device flow
|
|
8
|
+
* (non-standard OpenAI endpoints). The polling fn returns `{done}` to finish
|
|
9
|
+
* or `{pending}` to keep going, and may mutate `state.intervalMs` mid-flight
|
|
10
|
+
* to apply a `slow_down` bump.
|
|
11
|
+
*/
|
|
12
|
+
export interface PollState {
|
|
13
|
+
/** Mutable so the polling fn can bump on `slow_down`. */
|
|
14
|
+
intervalMs: number;
|
|
15
|
+
/**
|
|
16
|
+
* The flow's abort signal, threaded through so a polling fn can pass it
|
|
17
|
+
* into its in-flight fetch and cancel a hung request — not just the
|
|
18
|
+
* inter-poll sleep. Undefined when the caller supplied no signal.
|
|
19
|
+
*/
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
export type PollOutcome<T> = {
|
|
23
|
+
done: T;
|
|
24
|
+
} | {
|
|
25
|
+
pending: true;
|
|
26
|
+
};
|
|
27
|
+
export interface PollUntilOpts {
|
|
28
|
+
readonly intervalMs: number;
|
|
29
|
+
readonly timeoutMs: number;
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
/**
|
|
32
|
+
* Wait BEFORE the first call. RFC 8628 says clients SHOULD wait `interval`
|
|
33
|
+
* before the first poll; some flows (e.g. Codex) poll immediately. Default
|
|
34
|
+
* true to match the more conservative RFC behavior.
|
|
35
|
+
*/
|
|
36
|
+
readonly leadingWait?: boolean;
|
|
37
|
+
/** Used in timeout / abort error messages. */
|
|
38
|
+
readonly label?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function pollUntil<T>(fn: (state: PollState) => Promise<PollOutcome<T>>, opts: PollUntilOpts): Promise<T>;
|
|
41
|
+
//# sourceMappingURL=poll-until.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-until.d.ts","sourceRoot":"","sources":["../../src/oauth/poll-until.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EACjD,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,CAAC,CAAC,CA0BZ"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared polling primitive for OAuth-style "ask again later" modes. Handles
|
|
3
|
+
* the gnarly bits — deadline math, abort-responsive sleep, interval bumps
|
|
4
|
+
* for `slow_down`-style backpressure — so each device-flow dialect only has
|
|
5
|
+
* to encode its HTTP shape, not the timing harness.
|
|
6
|
+
*
|
|
7
|
+
* Consumed by both `runDeviceCodeFlow` (RFC 8628) and the Codex device flow
|
|
8
|
+
* (non-standard OpenAI endpoints). The polling fn returns `{done}` to finish
|
|
9
|
+
* or `{pending}` to keep going, and may mutate `state.intervalMs` mid-flight
|
|
10
|
+
* to apply a `slow_down` bump.
|
|
11
|
+
*/
|
|
12
|
+
import { MoxxyError } from '@moxxy/sdk';
|
|
13
|
+
export async function pollUntil(fn, opts) {
|
|
14
|
+
const state = {
|
|
15
|
+
intervalMs: opts.intervalMs,
|
|
16
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
17
|
+
};
|
|
18
|
+
const leadingWait = opts.leadingWait ?? true;
|
|
19
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
20
|
+
const label = opts.label ?? 'poll';
|
|
21
|
+
let first = true;
|
|
22
|
+
while (Date.now() < deadline) {
|
|
23
|
+
if (opts.signal?.aborted)
|
|
24
|
+
throw abortedError(label);
|
|
25
|
+
if (!first || leadingWait) {
|
|
26
|
+
const remaining = deadline - Date.now();
|
|
27
|
+
if (remaining <= 0)
|
|
28
|
+
break;
|
|
29
|
+
await sleep(Math.min(state.intervalMs, remaining), opts.signal, label);
|
|
30
|
+
}
|
|
31
|
+
first = false;
|
|
32
|
+
const result = await fn(state);
|
|
33
|
+
if ('done' in result)
|
|
34
|
+
return result.done;
|
|
35
|
+
}
|
|
36
|
+
throw new MoxxyError({
|
|
37
|
+
code: 'OAUTH_FLOW_TIMEOUT',
|
|
38
|
+
message: `${label} timed out waiting for completion`,
|
|
39
|
+
context: { label, timeout_ms: opts.timeoutMs },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function abortedError(label) {
|
|
43
|
+
return new MoxxyError({
|
|
44
|
+
code: 'NETWORK_ABORTED',
|
|
45
|
+
message: `${label} aborted`,
|
|
46
|
+
context: { label },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function sleep(ms, signal, label) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (signal?.aborted)
|
|
52
|
+
return reject(abortedError(label));
|
|
53
|
+
// `{ once: true }` only auto-removes a listener that actually FIRES; on the
|
|
54
|
+
// timer-resolved path it would linger. pollUntil calls sleep() once per
|
|
55
|
+
// poll cycle against the SAME long-lived signal, so an unremoved listener
|
|
56
|
+
// accumulates one-per-iteration (~120 over a 10-min device flow), tripping
|
|
57
|
+
// MaxListenersExceededWarning and retaining each closure. Remove on BOTH
|
|
58
|
+
// exits so it's exactly one add / one remove per sleep.
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
clearTimeout(t);
|
|
61
|
+
reject(abortedError(label));
|
|
62
|
+
};
|
|
63
|
+
const t = setTimeout(() => {
|
|
64
|
+
signal?.removeEventListener('abort', onAbort);
|
|
65
|
+
resolve();
|
|
66
|
+
}, ms);
|
|
67
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=poll-until.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-until.js","sourceRoot":"","sources":["../../src/oauth/poll-until.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA6BxC,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,EAAiD,EACjD,IAAmB;IAEnB,MAAM,KAAK,GAAc;QACvB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAC;IACF,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;IAEnC,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO;YAAE,MAAM,YAAY,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK,IAAI,WAAW,EAAE,CAAC;YAC1B,MAAM,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACxC,IAAI,SAAS,IAAI,CAAC;gBAAE,MAAM;YAC1B,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACzE,CAAC;QACD,KAAK,GAAG,KAAK,CAAC;QACd,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,MAAM,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAC3C,CAAC;IACD,MAAM,IAAI,UAAU,CAAC;QACnB,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,GAAG,KAAK,mCAAmC;QACpD,OAAO,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,UAAU,CAAC;QACpB,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,GAAG,KAAK,UAAU;QAC3B,OAAO,EAAE,EAAE,KAAK,EAAE;KACnB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,KAAK,CAAC,EAAU,EAAE,MAA+B,EAAE,KAAa;IACvE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO;YAAE,OAAO,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QACxD,4EAA4E;QAC5E,wEAAwE;QACxE,0EAA0E;QAC1E,2EAA2E;QAC3E,yEAAyE;QACzE,wDAAwD;QACxD,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,YAAY,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC;QACF,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE;YACxB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TokenSet } from './types.js';
|
|
2
|
+
interface ExchangeCodeInput {
|
|
3
|
+
readonly tokenUrl: string;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly redirectUri: string;
|
|
6
|
+
readonly clientId: string;
|
|
7
|
+
readonly clientSecret?: string;
|
|
8
|
+
readonly codeVerifier: string;
|
|
9
|
+
/** Abort the exchange fetch when the caller's flow is cancelled. */
|
|
10
|
+
readonly signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export declare function exchangeCodeForToken(input: ExchangeCodeInput, fetchImpl?: typeof fetch): Promise<TokenSet>;
|
|
13
|
+
/**
|
|
14
|
+
* Refresh an access token using a stored refresh_token. Same token
|
|
15
|
+
* endpoint, different grant_type. Returns a new TokenSet — note that
|
|
16
|
+
* providers MAY or MAY NOT rotate the refresh_token (Google does
|
|
17
|
+
* not; Auth0 with rotation does). Caller should preserve the prior
|
|
18
|
+
* refresh_token if the response omits one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function refreshAccessToken(input: {
|
|
21
|
+
readonly tokenUrl: string;
|
|
22
|
+
readonly clientId: string;
|
|
23
|
+
readonly clientSecret?: string;
|
|
24
|
+
readonly refreshToken: string;
|
|
25
|
+
}, fetchImpl?: typeof fetch): Promise<TokenSet>;
|
|
26
|
+
export declare function parseTokenResponse(json: Record<string, unknown>): TokenSet;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=token-exchange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-exchange.d.ts","sourceRoot":"","sources":["../../src/oauth/token-exchange.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,UAAU,iBAAiB;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,iBAAiB,EACxB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CA2BnB;AA2DD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE;IACL,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,EACD,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,QAAQ,CAAC,CA2BnB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAqB1E"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { classifyHttpStatus, MoxxyError } from '@moxxy/sdk';
|
|
2
|
+
export async function exchangeCodeForToken(input, fetchImpl = fetch) {
|
|
3
|
+
const body = new URLSearchParams();
|
|
4
|
+
body.set('grant_type', 'authorization_code');
|
|
5
|
+
body.set('code', input.code);
|
|
6
|
+
body.set('redirect_uri', input.redirectUri);
|
|
7
|
+
body.set('client_id', input.clientId);
|
|
8
|
+
body.set('code_verifier', input.codeVerifier);
|
|
9
|
+
if (input.clientSecret)
|
|
10
|
+
body.set('client_secret', input.clientSecret);
|
|
11
|
+
const res = await fetchImpl(input.tokenUrl, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
14
|
+
body: body.toString(),
|
|
15
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const text = await res.text().catch(() => '');
|
|
19
|
+
throw (classifyHttpStatus(res.status, { url: input.tokenUrl, body: text }) ??
|
|
20
|
+
new MoxxyError({
|
|
21
|
+
code: 'AUTH_INVALID',
|
|
22
|
+
message: `Token exchange failed (HTTP ${res.status})${errorSummary(text)}`,
|
|
23
|
+
context: { status: res.status, url: input.tokenUrl, ...bodyContext(text) },
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
const json = await parseJsonBody(res, input.tokenUrl);
|
|
27
|
+
return parseTokenResponse(json);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a human-message suffix from a token-endpoint error body. Token
|
|
31
|
+
* endpoints return `{error, error_description}` (RFC 6749 §5.2); prefer those
|
|
32
|
+
* structured fields verbatim. For an opaque/HTML body (proxy/captive-portal
|
|
33
|
+
* error page) — provider/attacker-influenced text that flows to logs and can
|
|
34
|
+
* reach the model — emit nothing in the human message; the bounded raw body
|
|
35
|
+
* still lands in `context` via {@link bodyContext}.
|
|
36
|
+
*/
|
|
37
|
+
function errorSummary(body) {
|
|
38
|
+
const fields = parseOauthError(body);
|
|
39
|
+
if (!fields)
|
|
40
|
+
return '';
|
|
41
|
+
const { error, error_description } = fields;
|
|
42
|
+
const detail = error_description ?? error;
|
|
43
|
+
return detail ? `: ${detail.slice(0, 300)}` : '';
|
|
44
|
+
}
|
|
45
|
+
/** Truncated raw body for the error context only (never the human message). */
|
|
46
|
+
function bodyContext(body) {
|
|
47
|
+
return body ? { body: body.slice(0, 200) } : {};
|
|
48
|
+
}
|
|
49
|
+
function parseOauthError(body) {
|
|
50
|
+
if (!body)
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(body);
|
|
54
|
+
if (!parsed || typeof parsed !== 'object')
|
|
55
|
+
return null;
|
|
56
|
+
const obj = parsed;
|
|
57
|
+
const error = typeof obj.error === 'string' ? obj.error : undefined;
|
|
58
|
+
const error_description = typeof obj.error_description === 'string' ? obj.error_description : undefined;
|
|
59
|
+
if (!error && !error_description)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
...(error !== undefined ? { error } : {}),
|
|
63
|
+
...(error_description !== undefined ? { error_description } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null; // opaque / HTML body — don't reflect it into the message
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse a 2xx token-endpoint body as JSON. A provider (or an intercepting
|
|
72
|
+
* proxy / captive portal / load-balancer error page) can return HTTP 200 with
|
|
73
|
+
* a non-JSON or truncated body; an unguarded `res.json()` would reject with a
|
|
74
|
+
* native SyntaxError that escapes the MoxxyError boundary (no code/hint, and
|
|
75
|
+
* `isAuthRejection` can't classify it). Convert it to a typed error instead.
|
|
76
|
+
*/
|
|
77
|
+
async function parseJsonBody(res, url) {
|
|
78
|
+
return (await res.json().catch(() => {
|
|
79
|
+
throw new MoxxyError({
|
|
80
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
81
|
+
message: 'token endpoint returned a non-JSON success body',
|
|
82
|
+
context: { url },
|
|
83
|
+
});
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh an access token using a stored refresh_token. Same token
|
|
88
|
+
* endpoint, different grant_type. Returns a new TokenSet — note that
|
|
89
|
+
* providers MAY or MAY NOT rotate the refresh_token (Google does
|
|
90
|
+
* not; Auth0 with rotation does). Caller should preserve the prior
|
|
91
|
+
* refresh_token if the response omits one.
|
|
92
|
+
*/
|
|
93
|
+
export async function refreshAccessToken(input, fetchImpl = fetch) {
|
|
94
|
+
const body = new URLSearchParams();
|
|
95
|
+
body.set('grant_type', 'refresh_token');
|
|
96
|
+
body.set('refresh_token', input.refreshToken);
|
|
97
|
+
body.set('client_id', input.clientId);
|
|
98
|
+
if (input.clientSecret)
|
|
99
|
+
body.set('client_secret', input.clientSecret);
|
|
100
|
+
const res = await fetchImpl(input.tokenUrl, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
103
|
+
body: body.toString(),
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = await res.text().catch(() => '');
|
|
107
|
+
// 401/403 here typically means the refresh_token was revoked/expired —
|
|
108
|
+
// classifyHttpStatus maps those to AUTH_INVALID/AUTH_DENIED. Surface a
|
|
109
|
+
// refresh-specific fallback for the unmapped statuses.
|
|
110
|
+
throw (classifyHttpStatus(res.status, { url: input.tokenUrl, body: text }) ??
|
|
111
|
+
new MoxxyError({
|
|
112
|
+
code: 'AUTH_EXPIRED',
|
|
113
|
+
message: `Token refresh failed (HTTP ${res.status})${errorSummary(text)}`,
|
|
114
|
+
context: { status: res.status, url: input.tokenUrl, ...bodyContext(text) },
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
const json = await parseJsonBody(res, input.tokenUrl);
|
|
118
|
+
return parseTokenResponse(json);
|
|
119
|
+
}
|
|
120
|
+
export function parseTokenResponse(json) {
|
|
121
|
+
const access = typeof json.access_token === 'string' ? json.access_token : null;
|
|
122
|
+
if (!access) {
|
|
123
|
+
throw new MoxxyError({
|
|
124
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
125
|
+
message: `token response missing access_token: ${JSON.stringify(json).slice(0, 200)}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const refresh = typeof json.refresh_token === 'string' ? json.refresh_token : undefined;
|
|
129
|
+
const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : null;
|
|
130
|
+
const scope = typeof json.scope === 'string' ? json.scope : undefined;
|
|
131
|
+
const tokenType = typeof json.token_type === 'string' ? json.token_type : 'Bearer';
|
|
132
|
+
const idToken = typeof json.id_token === 'string' ? json.id_token : undefined;
|
|
133
|
+
return {
|
|
134
|
+
accessToken: access,
|
|
135
|
+
...(refresh !== undefined ? { refreshToken: refresh } : {}),
|
|
136
|
+
...(expiresIn != null ? { expiresAt: Date.now() + expiresIn * 1000 } : {}),
|
|
137
|
+
...(scope !== undefined ? { scope } : {}),
|
|
138
|
+
tokenType,
|
|
139
|
+
...(idToken !== undefined ? { idToken } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=token-exchange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-exchange.js","sourceRoot":"","sources":["../../src/oauth/token-exchange.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface OAuthFlowOptions {
|
|
2
|
+
/** Provider's authorization endpoint, e.g. https://accounts.google.com/o/oauth2/v2/auth */
|
|
3
|
+
readonly authUrl: string;
|
|
4
|
+
/** Provider's token endpoint, e.g. https://oauth2.googleapis.com/token */
|
|
5
|
+
readonly tokenUrl: string;
|
|
6
|
+
readonly clientId: string;
|
|
7
|
+
/** Confidential clients only; loopback flows usually omit. */
|
|
8
|
+
readonly clientSecret?: string;
|
|
9
|
+
readonly scopes: ReadonlyArray<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Loopback redirect port. The redirect URI MUST be registered with
|
|
12
|
+
* the provider exactly (most providers require an exact match) — pick
|
|
13
|
+
* a value and tell the user to register it. Default 8765 to keep the
|
|
14
|
+
* Google Cloud Console setup deterministic across sessions.
|
|
15
|
+
*/
|
|
16
|
+
readonly redirectPort?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Loopback redirect path. Default `/callback`. Some providers reject
|
|
19
|
+
* paths other than `/`; tweak per provider. The full registered
|
|
20
|
+
* redirect URI must be `http://localhost:<port><path>` exactly.
|
|
21
|
+
*/
|
|
22
|
+
readonly redirectPath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Provider-specific extra query parameters on the auth URL. Common
|
|
25
|
+
* cases: `access_type=offline` + `prompt=consent` for Google (forces
|
|
26
|
+
* the issuance of a refresh_token); `audience=...` for Auth0.
|
|
27
|
+
*/
|
|
28
|
+
readonly extraAuthParams?: Readonly<Record<string, string>>;
|
|
29
|
+
/** How long to wait for the callback before giving up. Default 5min. */
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Abort signal — when fired, shuts the local server and rejects the
|
|
33
|
+
* flow. Wire from `ctx.signal` so a turn cancel kills a pending auth.
|
|
34
|
+
*/
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
/**
|
|
37
|
+
* When true, do NOT auto-open the browser. The auth URL still
|
|
38
|
+
* gets handed to `onAuthUrl`; the caller is expected to print it
|
|
39
|
+
* for the user to visit on a host where the loopback callback is
|
|
40
|
+
* reachable (same machine, SSH tunnel, port-forward).
|
|
41
|
+
*/
|
|
42
|
+
readonly noOpen?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Fires with the built auth URL just before the browser-open step.
|
|
45
|
+
* Use to log / display the URL so the user sees it even when
|
|
46
|
+
* auto-open fails or `noOpen` is set.
|
|
47
|
+
*/
|
|
48
|
+
readonly onAuthUrl?: (url: string) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface DeviceFlowOptions {
|
|
51
|
+
/** Provider's device-authorization endpoint (RFC 8628 §3.1). */
|
|
52
|
+
readonly deviceUrl: string;
|
|
53
|
+
readonly tokenUrl: string;
|
|
54
|
+
readonly clientId: string;
|
|
55
|
+
readonly clientSecret?: string;
|
|
56
|
+
readonly scopes: ReadonlyArray<string>;
|
|
57
|
+
/** Hard cap; the device-code's own expires_in usually drives the timer. */
|
|
58
|
+
readonly timeoutMs?: number;
|
|
59
|
+
readonly signal?: AbortSignal;
|
|
60
|
+
/**
|
|
61
|
+
* Fired ONCE with the user-facing prompt info as soon as the
|
|
62
|
+
* device endpoint returns. Channels should surface this prominently
|
|
63
|
+
* — the whole flow stalls until the user finishes on the URL.
|
|
64
|
+
*/
|
|
65
|
+
readonly onPrompt: (info: DevicePrompt) => void;
|
|
66
|
+
}
|
|
67
|
+
export interface DevicePrompt {
|
|
68
|
+
/** Short code the user types into the verification page. */
|
|
69
|
+
readonly userCode: string;
|
|
70
|
+
/** URL the user opens on any device. */
|
|
71
|
+
readonly verificationUri: string;
|
|
72
|
+
/** Some providers return a URL that already includes the user_code. */
|
|
73
|
+
readonly verificationUriComplete?: string;
|
|
74
|
+
/** Seconds until the device_code expires (from the provider). */
|
|
75
|
+
readonly expiresIn: number;
|
|
76
|
+
/** Poll interval the provider wants us to use, in seconds. */
|
|
77
|
+
readonly interval: number;
|
|
78
|
+
}
|
|
79
|
+
export interface TokenSet {
|
|
80
|
+
readonly accessToken: string;
|
|
81
|
+
readonly refreshToken?: string;
|
|
82
|
+
/** Epoch ms when the access_token expires. */
|
|
83
|
+
readonly expiresAt?: number;
|
|
84
|
+
/** Granted scopes — provider may grant less than requested. */
|
|
85
|
+
readonly scope?: string;
|
|
86
|
+
readonly tokenType: string;
|
|
87
|
+
/** OIDC id_token if the provider returned one (Google does for `openid` scope). */
|
|
88
|
+
readonly idToken?: string;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/oauth/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,uEAAuE;IACvE,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/oauth/types.ts"],"names":[],"mappings":""}
|