@mymehq/sdk 4.1.1 → 4.3.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/dist/auth/index.d.ts +60 -2
- package/dist/auth/index.js +126 -2
- package/package.json +2 -2
package/dist/auth/index.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ interface TokenProvider {
|
|
|
35
35
|
onSignOut(handler: () => void): () => void;
|
|
36
36
|
/** Force a sign-out — clears local storage, fires onSignOut handlers. */
|
|
37
37
|
signOut(): Promise<void>;
|
|
38
|
+
/** Force a token refresh and return the new access token. Same single-flight
|
|
39
|
+
* semantics as `getAccessToken()`. Consumers calling on a 401 response
|
|
40
|
+
* should use this rather than waiting for the next `getAccessToken()` to
|
|
41
|
+
* hit the proactive window. */
|
|
42
|
+
refresh(): Promise<string>;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -99,7 +104,7 @@ declare class MymeAuth {
|
|
|
99
104
|
* Typed OAuth error surface. Mirrors the wire `error` codes from
|
|
100
105
|
* RFC 6749 §5.2 and the rotation-replay extension.
|
|
101
106
|
*/
|
|
102
|
-
type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "invalid_scope" | "invalid_token" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "access_denied" | "insufficient_scope" | "token_reuse_detected" | "server_error" | "temporarily_unavailable";
|
|
107
|
+
type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "invalid_scope" | "invalid_token" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "access_denied" | "insufficient_scope" | "token_reuse_detected" | "server_error" | "temporarily_unavailable" | "authorization_pending" | "slow_down" | "expired_token";
|
|
103
108
|
declare class OAuthError extends Error {
|
|
104
109
|
readonly code: OAuthErrorCode;
|
|
105
110
|
readonly status: number;
|
|
@@ -126,4 +131,57 @@ declare function computeCodeChallenge(verifier: string): Promise<string>;
|
|
|
126
131
|
/** Generate a random opaque state value for CSRF protection on the redirect. */
|
|
127
132
|
declare function generateState(byteLength?: number): string;
|
|
128
133
|
|
|
129
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Device Authorization Grant flow (RFC 8628) — for headless / CLI / native
|
|
136
|
+
* apps that can't host a browser callback.
|
|
137
|
+
*
|
|
138
|
+
* const handle = await startDeviceFlow({ issuer, clientId, scopes });
|
|
139
|
+
* console.log(`Visit ${handle.verification_uri} and enter ${handle.user_code}`);
|
|
140
|
+
* const provider = await handle.pollForToken();
|
|
141
|
+
*
|
|
142
|
+
* `pollForToken()` blocks until the user approves on the server (returning
|
|
143
|
+
* a `TokenProvider`), the user denies (throws OAuthError "access_denied"),
|
|
144
|
+
* or the device_code expires (throws OAuthError "expired_token"). Adheres
|
|
145
|
+
* to RFC 8628 polling rules: respects `interval`, doubles on `slow_down`.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
interface StartDeviceFlowConfig {
|
|
149
|
+
/** Myme server URL — protocol + host (and port). */
|
|
150
|
+
issuer: string;
|
|
151
|
+
/** OAuth client id, registered via POST /auth/clients. */
|
|
152
|
+
clientId: string;
|
|
153
|
+
/** Scopes to request (e.g. ["core.note:read"]). */
|
|
154
|
+
scopes: string[];
|
|
155
|
+
/** Token storage for persisting the resulting access/refresh tokens.
|
|
156
|
+
* Defaults to localStorage in browser, in-memory in Node. CLI consumers
|
|
157
|
+
* typically pass a filesystem-backed implementation. */
|
|
158
|
+
storage?: TokenStorage;
|
|
159
|
+
/** Override for the global fetch (testing). */
|
|
160
|
+
fetch?: typeof globalThis.fetch;
|
|
161
|
+
}
|
|
162
|
+
interface DeviceFlowHandle {
|
|
163
|
+
/** Short, human-readable code the user types on the verification page
|
|
164
|
+
* (XXXX-XXXX shape). */
|
|
165
|
+
user_code: string;
|
|
166
|
+
/** URL the user should visit. */
|
|
167
|
+
verification_uri: string;
|
|
168
|
+
/** Same URL with `?user_code=…` appended; some clients can render this
|
|
169
|
+
* as a deep link / QR code so the user doesn't have to type. */
|
|
170
|
+
verification_uri_complete: string;
|
|
171
|
+
/** Seconds the device_code remains valid. */
|
|
172
|
+
expires_in: number;
|
|
173
|
+
/** Minimum seconds between polls. */
|
|
174
|
+
interval: number;
|
|
175
|
+
/** Block until the user approves on the server. Resolves with a
|
|
176
|
+
* TokenProvider that can be passed straight into `MymeClient`.
|
|
177
|
+
* Rejects with OAuthError on denial / expiry / fatal error. */
|
|
178
|
+
pollForToken(options?: {
|
|
179
|
+
signal?: AbortSignal;
|
|
180
|
+
}): Promise<TokenProvider>;
|
|
181
|
+
}
|
|
182
|
+
/** Initiate a Device Authorization Grant flow. The returned handle
|
|
183
|
+
* carries the user-facing code + URL plus a `pollForToken()` that
|
|
184
|
+
* blocks until approval. */
|
|
185
|
+
declare function startDeviceFlow(config: StartDeviceFlowConfig): Promise<DeviceFlowHandle>;
|
|
186
|
+
|
|
187
|
+
export { type DeviceFlowHandle, InMemoryTokenStorage, LocalStorageTokenStorage, MymeAuth, type MymeAuthConfig, OAuthError, type OAuthErrorCode, type StartDeviceFlowConfig, type TokenProvider, type TokenStorage, computeCodeChallenge, defaultTokenStorage, generateCodeVerifier, generateState, startDeviceFlow };
|
package/dist/auth/index.js
CHANGED
|
@@ -128,7 +128,9 @@ var StoredTokenProvider = class {
|
|
|
128
128
|
}
|
|
129
129
|
return this.refresh();
|
|
130
130
|
}
|
|
131
|
-
/** Single-flight refresh — concurrent callers await the same promise.
|
|
131
|
+
/** Single-flight refresh — concurrent callers await the same promise.
|
|
132
|
+
* Public so consumers can force a refresh on 401 responses (RFC 6750
|
|
133
|
+
* invalid_token) without waiting for the proactive-refresh window. */
|
|
132
134
|
async refresh() {
|
|
133
135
|
if (this.inflightRefresh) return this.inflightRefresh;
|
|
134
136
|
if (!this.cache) {
|
|
@@ -319,6 +321,127 @@ var MymeAuth = class {
|
|
|
319
321
|
await this.storage.delete(this.pendingKey);
|
|
320
322
|
}
|
|
321
323
|
};
|
|
324
|
+
|
|
325
|
+
// src/auth/device-flow.ts
|
|
326
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
327
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
328
|
+
var SLOW_DOWN_BUMP_MS = 5e3;
|
|
329
|
+
async function startDeviceFlow(config) {
|
|
330
|
+
const issuer = config.issuer.replace(/\/+$/, "");
|
|
331
|
+
const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
332
|
+
const storage = config.storage ?? defaultTokenStorage();
|
|
333
|
+
const initiateRes = await fetchImpl(`${issuer}/auth/device`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: { "content-type": "application/json" },
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
client_id: config.clientId,
|
|
338
|
+
scope: config.scopes.join(" ")
|
|
339
|
+
})
|
|
340
|
+
});
|
|
341
|
+
if (!initiateRes.ok) {
|
|
342
|
+
const body = await safeJson(initiateRes);
|
|
343
|
+
const errCode = extractErrorCode(body);
|
|
344
|
+
throw new OAuthError(
|
|
345
|
+
errCode,
|
|
346
|
+
`device_authorization initiate failed: ${String(initiateRes.status)}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const initiated = await initiateRes.json();
|
|
350
|
+
let pollIntervalMs = typeof initiated.interval === "number" ? initiated.interval * 1e3 : DEFAULT_POLL_INTERVAL_MS;
|
|
351
|
+
const handle = {
|
|
352
|
+
user_code: initiated.user_code,
|
|
353
|
+
verification_uri: initiated.verification_uri,
|
|
354
|
+
verification_uri_complete: initiated.verification_uri_complete,
|
|
355
|
+
expires_in: initiated.expires_in,
|
|
356
|
+
interval: initiated.interval,
|
|
357
|
+
async pollForToken(options) {
|
|
358
|
+
const deadline = Date.now() + initiated.expires_in * 1e3;
|
|
359
|
+
while (Date.now() < deadline) {
|
|
360
|
+
if (options?.signal?.aborted) {
|
|
361
|
+
throw new OAuthError(
|
|
362
|
+
"invalid_request",
|
|
363
|
+
"Device flow aborted by caller"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
await sleep(pollIntervalMs, options?.signal);
|
|
367
|
+
const res = await fetchImpl(`${issuer}/auth/device/token`, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
370
|
+
body: new URLSearchParams({
|
|
371
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
372
|
+
device_code: initiated.device_code,
|
|
373
|
+
client_id: config.clientId
|
|
374
|
+
}).toString()
|
|
375
|
+
});
|
|
376
|
+
if (res.ok) {
|
|
377
|
+
const tokens = await res.json();
|
|
378
|
+
const storageKey = `myme.auth.tokens:${new URL(issuer).origin}:${config.clientId}`;
|
|
379
|
+
const provider = new StoredTokenProvider({
|
|
380
|
+
issuer,
|
|
381
|
+
clientId: config.clientId,
|
|
382
|
+
storage,
|
|
383
|
+
storageKey,
|
|
384
|
+
fetch: fetchImpl
|
|
385
|
+
});
|
|
386
|
+
await provider.persist({
|
|
387
|
+
access_token: tokens.access_token,
|
|
388
|
+
refresh_token: tokens.refresh_token,
|
|
389
|
+
access_expires_at: Date.now() + tokens.expires_in * 1e3,
|
|
390
|
+
scope: tokens.scope
|
|
391
|
+
});
|
|
392
|
+
return provider;
|
|
393
|
+
}
|
|
394
|
+
const body = await safeJson(res);
|
|
395
|
+
const code = body.error ?? "invalid_grant";
|
|
396
|
+
if (code === "authorization_pending") continue;
|
|
397
|
+
if (code === "slow_down") {
|
|
398
|
+
pollIntervalMs += SLOW_DOWN_BUMP_MS;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
throw new OAuthError(
|
|
402
|
+
code,
|
|
403
|
+
body.error_description ?? "Device flow failed"
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
throw new OAuthError("expired_token", "Device flow expired");
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return handle;
|
|
410
|
+
}
|
|
411
|
+
async function safeJson(res) {
|
|
412
|
+
try {
|
|
413
|
+
return await res.json();
|
|
414
|
+
} catch {
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function extractErrorCode(body) {
|
|
419
|
+
const err = body.error;
|
|
420
|
+
if (typeof err === "string") return err;
|
|
421
|
+
if (typeof err === "object" && err !== null) {
|
|
422
|
+
const code = err.code;
|
|
423
|
+
if (typeof code === "string") return code;
|
|
424
|
+
}
|
|
425
|
+
return "invalid_request";
|
|
426
|
+
}
|
|
427
|
+
function sleep(ms, signal) {
|
|
428
|
+
return new Promise((resolve, reject) => {
|
|
429
|
+
if (signal?.aborted) {
|
|
430
|
+
reject(new Error("aborted"));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const t = setTimeout(() => {
|
|
434
|
+
signal?.removeEventListener("abort", onAbort);
|
|
435
|
+
resolve();
|
|
436
|
+
}, ms);
|
|
437
|
+
const onAbort = () => {
|
|
438
|
+
clearTimeout(t);
|
|
439
|
+
signal?.removeEventListener("abort", onAbort);
|
|
440
|
+
reject(new Error("aborted"));
|
|
441
|
+
};
|
|
442
|
+
signal?.addEventListener("abort", onAbort);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
322
445
|
export {
|
|
323
446
|
InMemoryTokenStorage,
|
|
324
447
|
LocalStorageTokenStorage,
|
|
@@ -327,5 +450,6 @@ export {
|
|
|
327
450
|
computeCodeChallenge,
|
|
328
451
|
defaultTokenStorage,
|
|
329
452
|
generateCodeVerifier,
|
|
330
|
-
generateState
|
|
453
|
+
generateState,
|
|
454
|
+
startDeviceFlow
|
|
331
455
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mymehq/sdk",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@mymehq/shared": "4.
|
|
23
|
+
"@mymehq/shared": "4.3.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^22.0.0",
|