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