@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.
@@ -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
- export { InMemoryTokenStorage, LocalStorageTokenStorage, MymeAuth, type MymeAuthConfig, OAuthError, type OAuthErrorCode, type TokenProvider, type TokenStorage, computeCodeChallenge, defaultTokenStorage, generateCodeVerifier, generateState };
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 };
@@ -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.1.1",
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.1.1"
23
+ "@mymehq/shared": "4.3.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.0.0",