@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/openai-device-flow.d.ts +34 -0
  3. package/dist/adapters/openai-device-flow.d.ts.map +1 -0
  4. package/dist/adapters/openai-device-flow.js +136 -0
  5. package/dist/adapters/openai-device-flow.js.map +1 -0
  6. package/dist/adapters/rfc8628-device-flow.d.ts +20 -0
  7. package/dist/adapters/rfc8628-device-flow.d.ts.map +1 -0
  8. package/dist/adapters/rfc8628-device-flow.js +54 -0
  9. package/dist/adapters/rfc8628-device-flow.js.map +1 -0
  10. package/dist/credential-lock.d.ts +60 -0
  11. package/dist/credential-lock.d.ts.map +1 -0
  12. package/dist/credential-lock.js +149 -0
  13. package/dist/credential-lock.js.map +1 -0
  14. package/dist/ensure-fresh.d.ts +63 -0
  15. package/dist/ensure-fresh.d.ts.map +1 -0
  16. package/dist/ensure-fresh.js +122 -0
  17. package/dist/ensure-fresh.js.map +1 -0
  18. package/dist/flow.d.ts +6 -0
  19. package/dist/flow.d.ts.map +1 -0
  20. package/dist/flow.js +5 -0
  21. package/dist/flow.js.map +1 -0
  22. package/dist/index.d.ts +43 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +82 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/oauth/browser-flow.d.ts +23 -0
  27. package/dist/oauth/browser-flow.d.ts.map +1 -0
  28. package/dist/oauth/browser-flow.js +82 -0
  29. package/dist/oauth/browser-flow.js.map +1 -0
  30. package/dist/oauth/callback-server.d.ts +10 -0
  31. package/dist/oauth/callback-server.d.ts.map +1 -0
  32. package/dist/oauth/callback-server.js +158 -0
  33. package/dist/oauth/callback-server.js.map +1 -0
  34. package/dist/oauth/device-flow-shared.d.ts +39 -0
  35. package/dist/oauth/device-flow-shared.d.ts.map +1 -0
  36. package/dist/oauth/device-flow-shared.js +101 -0
  37. package/dist/oauth/device-flow-shared.js.map +1 -0
  38. package/dist/oauth/device-flow.d.ts +29 -0
  39. package/dist/oauth/device-flow.d.ts.map +1 -0
  40. package/dist/oauth/device-flow.js +74 -0
  41. package/dist/oauth/device-flow.js.map +1 -0
  42. package/dist/oauth/poll-until.d.ts +41 -0
  43. package/dist/oauth/poll-until.d.ts.map +1 -0
  44. package/dist/oauth/poll-until.js +70 -0
  45. package/dist/oauth/poll-until.js.map +1 -0
  46. package/dist/oauth/token-exchange.d.ts +28 -0
  47. package/dist/oauth/token-exchange.d.ts.map +1 -0
  48. package/dist/oauth/token-exchange.js +142 -0
  49. package/dist/oauth/token-exchange.js.map +1 -0
  50. package/dist/oauth/types.d.ts +90 -0
  51. package/dist/oauth/types.d.ts.map +1 -0
  52. package/dist/oauth/types.js +2 -0
  53. package/dist/oauth/types.js.map +1 -0
  54. package/dist/open-browser.d.ts +32 -0
  55. package/dist/open-browser.d.ts.map +1 -0
  56. package/dist/open-browser.js +83 -0
  57. package/dist/open-browser.js.map +1 -0
  58. package/dist/pkce.d.ts +20 -0
  59. package/dist/pkce.d.ts.map +1 -0
  60. package/dist/pkce.js +34 -0
  61. package/dist/pkce.js.map +1 -0
  62. package/dist/profile.d.ts +129 -0
  63. package/dist/profile.d.ts.map +1 -0
  64. package/dist/profile.js +11 -0
  65. package/dist/profile.js.map +1 -0
  66. package/dist/run-login.d.ts +9 -0
  67. package/dist/run-login.d.ts.map +1 -0
  68. package/dist/run-login.js +93 -0
  69. package/dist/run-login.js.map +1 -0
  70. package/dist/storage.d.ts +46 -0
  71. package/dist/storage.d.ts.map +1 -0
  72. package/dist/storage.js +167 -0
  73. package/dist/storage.js.map +1 -0
  74. package/dist/tools.d.ts +8 -0
  75. package/dist/tools.d.ts.map +1 -0
  76. package/dist/tools.js +283 -0
  77. package/dist/tools.js.map +1 -0
  78. package/package.json +73 -0
  79. package/skills/google-oauth.md +313 -0
  80. package/skills/oauth-flow.md +132 -0
  81. package/src/adapters/openai-device-flow.test.ts +191 -0
  82. package/src/adapters/openai-device-flow.ts +179 -0
  83. package/src/adapters/rfc8628-device-flow.ts +75 -0
  84. package/src/credential-lock.ts +183 -0
  85. package/src/discovery.test.ts +35 -0
  86. package/src/ensure-fresh.test.ts +279 -0
  87. package/src/ensure-fresh.ts +178 -0
  88. package/src/flow.ts +9 -0
  89. package/src/index.ts +153 -0
  90. package/src/oauth/browser-flow.ts +96 -0
  91. package/src/oauth/callback-server.test.ts +90 -0
  92. package/src/oauth/callback-server.ts +208 -0
  93. package/src/oauth/device-flow-shared.test.ts +142 -0
  94. package/src/oauth/device-flow-shared.ts +129 -0
  95. package/src/oauth/device-flow.test.ts +102 -0
  96. package/src/oauth/device-flow.ts +82 -0
  97. package/src/oauth/poll-until.test.ts +63 -0
  98. package/src/oauth/poll-until.ts +100 -0
  99. package/src/oauth/token-exchange.test.ts +224 -0
  100. package/src/oauth/token-exchange.ts +169 -0
  101. package/src/oauth/types.ts +92 -0
  102. package/src/oauth.test.ts +185 -0
  103. package/src/open-browser-spawn.test.ts +69 -0
  104. package/src/open-browser.test.ts +30 -0
  105. package/src/open-browser.ts +84 -0
  106. package/src/pkce.ts +37 -0
  107. package/src/profile.test.ts +331 -0
  108. package/src/profile.ts +135 -0
  109. package/src/run-login.ts +123 -0
  110. package/src/storage.test.ts +191 -0
  111. package/src/storage.ts +208 -0
  112. package/src/tools.test.ts +239 -0
  113. package/src/tools.ts +332 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moxxy (moxxy.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Device-authorization adapter for OpenAI's non-standard flow (used by
3
+ * Codex/ChatGPT, opencode, codex-rs). Not RFC 8628 — speaks a JSON dialect:
4
+ *
5
+ * 1. POST `${issuer}/api/accounts/deviceauth/usercode` JSON `{client_id}`
6
+ * → `{device_auth_id, user_code, interval}`.
7
+ * 2. Surface `user_code` + a product-specific verification URI
8
+ * (`verificationUri`) to the user.
9
+ * 3. Poll `${issuer}/api/accounts/deviceauth/token` JSON
10
+ * `{device_auth_id, user_code}`. 403/404 ⇒ keep polling. 200 returns
11
+ * `{authorization_code, code_verifier}`.
12
+ * 4. Exchange that `authorization_code` + `code_verifier` for real tokens
13
+ * via the standard `/oauth/token` endpoint with
14
+ * `redirect_uri=${issuer}/deviceauth/callback` (not an actual redirect
15
+ * target — just a registered value the server expects to see).
16
+ *
17
+ * Reusable for any OpenAI-issued client; product-specific bits are the
18
+ * verification URI and the `client_id`.
19
+ */
20
+ import type { DeviceFlowAdapter } from '../profile.js';
21
+ export interface OpenaiDeviceFlowOpts {
22
+ /** Auth issuer base URL, e.g. `https://auth.openai.com`. */
23
+ readonly issuer: string;
24
+ /** Standard OAuth token endpoint — `parseTokenResponse` consumes its reply. */
25
+ readonly tokenUrl: string;
26
+ /**
27
+ * URL the user opens to enter the device code. Product-specific —
28
+ * Codex uses `${issuer}/codex/device`. The init endpoint does NOT
29
+ * return one, so the caller supplies it.
30
+ */
31
+ readonly verificationUri: string;
32
+ }
33
+ export declare function openaiDeviceFlow(opts: OpenaiDeviceFlowOpts): DeviceFlowAdapter;
34
+ //# sourceMappingURL=openai-device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-device-flow.d.ts","sourceRoot":"","sources":["../../src/adapters/openai-device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EACV,iBAAiB,EAGlB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAQD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,iBAAiB,CAiI9E"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Device-authorization adapter for OpenAI's non-standard flow (used by
3
+ * Codex/ChatGPT, opencode, codex-rs). Not RFC 8628 — speaks a JSON dialect:
4
+ *
5
+ * 1. POST `${issuer}/api/accounts/deviceauth/usercode` JSON `{client_id}`
6
+ * → `{device_auth_id, user_code, interval}`.
7
+ * 2. Surface `user_code` + a product-specific verification URI
8
+ * (`verificationUri`) to the user.
9
+ * 3. Poll `${issuer}/api/accounts/deviceauth/token` JSON
10
+ * `{device_auth_id, user_code}`. 403/404 ⇒ keep polling. 200 returns
11
+ * `{authorization_code, code_verifier}`.
12
+ * 4. Exchange that `authorization_code` + `code_verifier` for real tokens
13
+ * via the standard `/oauth/token` endpoint with
14
+ * `redirect_uri=${issuer}/deviceauth/callback` (not an actual redirect
15
+ * target — just a registered value the server expects to see).
16
+ *
17
+ * Reusable for any OpenAI-issued client; product-specific bits are the
18
+ * verification URI and the `client_id`.
19
+ */
20
+ import { classifyHttpStatus, MoxxyError } from '@moxxy/sdk';
21
+ import { exchangeCodeForToken } from '../oauth/token-exchange.js';
22
+ export function openaiDeviceFlow(opts) {
23
+ const initUrl = `${opts.issuer}/api/accounts/deviceauth/usercode`;
24
+ const pollUrl = `${opts.issuer}/api/accounts/deviceauth/token`;
25
+ const exchangeRedirectUri = `${opts.issuer}/deviceauth/callback`;
26
+ return {
27
+ async start(args) {
28
+ const res = await fetch(initUrl, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ client_id: args.clientId }),
32
+ ...(args.signal ? { signal: args.signal } : {}),
33
+ });
34
+ if (!res.ok) {
35
+ const text = await res.text().catch(() => '');
36
+ throw (classifyHttpStatus(res.status, { url: initUrl, body: text || res.statusText }) ??
37
+ new MoxxyError({
38
+ code: 'AUTH_INVALID',
39
+ message: `device auth init failed: ${res.status} ${text || res.statusText}`,
40
+ context: { status: res.status, url: initUrl },
41
+ }));
42
+ }
43
+ const data = (await res.json().catch(() => {
44
+ throw new MoxxyError({
45
+ code: 'PROVIDER_UNKNOWN_RESPONSE',
46
+ message: 'device-auth init returned a non-JSON success body',
47
+ context: { url: initUrl },
48
+ });
49
+ }));
50
+ // Required-field validation: a 200 missing device_auth_id/user_code would
51
+ // otherwise produce a poll body of `{ device_auth_id: undefined }` that
52
+ // 403s forever until timeout. Reject clearly instead.
53
+ if (typeof data.device_auth_id !== 'string' || typeof data.user_code !== 'string') {
54
+ throw new MoxxyError({
55
+ code: 'PROVIDER_UNKNOWN_RESPONSE',
56
+ message: 'device-auth init response missing device_auth_id or user_code',
57
+ context: { url: initUrl },
58
+ });
59
+ }
60
+ // Coerce-then-validate: a malformed server value (e.g. interval:"" or a
61
+ // non-numeric string) parses to NaN, which would poison the poll timing
62
+ // (`setTimeout(_, NaN)` busy-loops; a NaN deadline aborts the flow). Only
63
+ // accept a finite number; otherwise fall back to the RFC-style defaults.
64
+ const rawInterval = typeof data.interval === 'string' ? parseInt(data.interval, 10) : data.interval;
65
+ const intervalSec = Math.max(Number.isFinite(rawInterval) ? rawInterval : 5, 1);
66
+ const rawExpiresIn = typeof data.expires_in === 'string' ? parseInt(data.expires_in, 10) : data.expires_in;
67
+ const expiresInSec = Number.isFinite(rawExpiresIn) ? rawExpiresIn : 600;
68
+ return {
69
+ userCode: data.user_code,
70
+ verificationUri: opts.verificationUri,
71
+ intervalMs: intervalSec * 1000,
72
+ expiresInMs: expiresInSec * 1000,
73
+ providerData: {
74
+ deviceAuthId: data.device_auth_id,
75
+ userCode: data.user_code,
76
+ clientId: args.clientId,
77
+ },
78
+ };
79
+ },
80
+ async poll(init, state) {
81
+ const { deviceAuthId, userCode, clientId } = init.providerData;
82
+ const res = await fetch(pollUrl, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ device_auth_id: deviceAuthId, user_code: userCode }),
86
+ ...(state.signal ? { signal: state.signal } : {}),
87
+ });
88
+ if (res.ok) {
89
+ const data = (await res.json().catch(() => {
90
+ throw new MoxxyError({
91
+ code: 'PROVIDER_UNKNOWN_RESPONSE',
92
+ message: 'device-auth poll returned a non-JSON success body',
93
+ context: { url: pollUrl },
94
+ });
95
+ }));
96
+ // Required-field validation: without it a 200 omitting these would call
97
+ // exchangeCodeForToken with undefined, which URLSearchParams coerces to
98
+ // the literal 'undefined' — a wasted exchange that fails opaquely.
99
+ if (typeof data.authorization_code !== 'string' || typeof data.code_verifier !== 'string') {
100
+ throw new MoxxyError({
101
+ code: 'PROVIDER_UNKNOWN_RESPONSE',
102
+ message: 'device-auth poll response missing authorization_code or code_verifier',
103
+ context: { url: pollUrl },
104
+ });
105
+ }
106
+ // Two-step: poll returns a server-side authorization_code we
107
+ // exchange via the standard token endpoint. The redirect_uri
108
+ // here is the issuer's device callback — a registered value,
109
+ // not a URI we listen on. Route through the shared exchange
110
+ // helper so this dialect can't drift from browser-flow.
111
+ return {
112
+ done: await exchangeCodeForToken({
113
+ tokenUrl: opts.tokenUrl,
114
+ code: data.authorization_code,
115
+ redirectUri: exchangeRedirectUri,
116
+ clientId,
117
+ codeVerifier: data.code_verifier,
118
+ ...(state.signal ? { signal: state.signal } : {}),
119
+ }),
120
+ };
121
+ }
122
+ // OpenAI's "still waiting" signal — 403 or 404 with no further detail.
123
+ if (res.status === 403 || res.status === 404) {
124
+ return { pending: true };
125
+ }
126
+ const text = await res.text().catch(() => '');
127
+ throw (classifyHttpStatus(res.status, { url: pollUrl, body: text || res.statusText }) ??
128
+ new MoxxyError({
129
+ code: 'AUTH_INVALID',
130
+ message: `Device auth poll failed: ${res.status} ${text || res.statusText}`,
131
+ context: { status: res.status, url: pollUrl },
132
+ }));
133
+ },
134
+ };
135
+ }
136
+ //# sourceMappingURL=openai-device-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-device-flow.js","sourceRoot":"","sources":["../../src/adapters/openai-device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AA2BlE,MAAM,UAAU,gBAAgB,CAAC,IAA0B;IACzD,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,mCAAmC,CAAC;IAClE,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,gCAAgC,CAAC;IAC/D,MAAM,mBAAmB,GAAG,GAAG,IAAI,CAAC,MAAM,sBAAsB,CAAC;IAEjE,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,IAAyB;YACnC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE;gBAC/B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChD,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC9C,MAAM,CACJ,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;oBAC9E,IAAI,UAAU,CAAC;wBACb,IAAI,EAAE,cAAc;wBACpB,OAAO,EAAE,4BAA4B,GAAG,CAAC,MAAM,IAAI,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE;wBAC3E,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE;qBAC9C,CAAC,CACH,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBACxC,MAAM,IAAI,UAAU,CAAC;oBACnB,IAAI,EAAE,2BAA2B;oBACjC,OAAO,EAAE,mDAAmD;oBAC5D,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;iBAC1B,CAAC,CAAC;YACL,CAAC,CAAC,CAKD,CAAC;YACF,0EAA0E;YAC1E,wEAAwE;YACxE,sDAAsD;YACtD,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;gBAClF,MAAM,IAAI,UAAU,CAAC;oBACnB,IAAI,EAAE,2BAA2B;oBACjC,OAAO,EAAE,+DAA+D;oBACxE,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;iBAC1B,CAAC,CAAC;YACL,CAAC;YACD,wEAAwE;YACxE,wEAAwE;YACxE,0EAA0E;YAC1E,yEAAyE;YACzE,MAAM,WAAW,GACf,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;YAClF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAE,WAAsB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC5F,MAAM,YAAY,GAChB,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;YACxF,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,YAAuB,CAAC,CAAC,CAAC,GAAG,CAAC;YACpF,OAAO;gBACL,QAAQ,EAAE,IAAI,CAAC,SAAS;gBACxB,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,UAAU,EAAE,WAAW,GAAG,IAAI;gBAC9B,WAAW,EAAE,YAAY,GAAG,IAAI;gBAChC,YAAY,EAAE;oBACZ,YAAY,EAAE,IAAI,CAAC,cAAc;oBACjC,QAAQ,EAAE,IAAI,CAAC,SAAS;oBACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;iBACI;aAC9B,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAoB,EAAE,KAAgB;YAC/C,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,YAAiC,CAAC;YACpF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE;gBAC/B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC3E,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClD,CAAC,CAAC;YACH,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;gBACX,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;oBACxC,MAAM,IAAI,UAAU,CAAC;wBACnB,IAAI,EAAE,2BAA2B;wBACjC,OAAO,EAAE,mDAAmD;wBAC5D,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;qBAC1B,CAAC,CAAC;gBACL,CAAC,CAAC,CAGD,CAAC;gBACF,wEAAwE;gBACxE,wEAAwE;gBACxE,mEAAmE;gBACnE,IAAI,OAAO,IAAI,CAAC,kBAAkB,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;oBAC1F,MAAM,IAAI,UAAU,CAAC;wBACnB,IAAI,EAAE,2BAA2B;wBACjC,OAAO,EAAE,uEAAuE;wBAChF,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;qBAC1B,CAAC,CAAC;gBACL,CAAC;gBACD,6DAA6D;gBAC7D,6DAA6D;gBAC7D,6DAA6D;gBAC7D,4DAA4D;gBAC5D,wDAAwD;gBACxD,OAAO;oBACL,IAAI,EAAE,MAAM,oBAAoB,CAAC;wBAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,IAAI,EAAE,IAAI,CAAC,kBAAkB;wBAC7B,WAAW,EAAE,mBAAmB;wBAChC,QAAQ;wBACR,YAAY,EAAE,IAAI,CAAC,aAAa;wBAChC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAClD,CAAC;iBACH,CAAC;YACJ,CAAC;YACD,uEAAuE;YACvE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,CACJ,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC9E,IAAI,UAAU,CAAC;oBACb,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,4BAA4B,GAAG,CAAC,MAAM,IAAI,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE;oBAC3E,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE;iBAC9C,CAAC,CACH,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Standards-compliant device-authorization adapter per RFC 8628.
3
+ *
4
+ * Phases:
5
+ * 1. POST `client_id` + `scope` to `deviceUrl` (form-encoded).
6
+ * 2. Surface `user_code` + `verification_uri` to the user.
7
+ * 3. Poll `tokenUrl` every `interval` with
8
+ * `grant_type=urn:ietf:params:oauth:grant-type:device_code` + `device_code`.
9
+ * 4. Handle `authorization_pending` / `slow_down` / fatal codes per spec.
10
+ *
11
+ * The device-authorization request + poll-response classification are shared
12
+ * with the legacy `runDeviceCodeFlow` via `oauth/device-flow-shared`.
13
+ */
14
+ import type { DeviceFlowAdapter } from '../profile.js';
15
+ export interface Rfc8628AdapterOpts {
16
+ readonly deviceUrl: string;
17
+ readonly tokenUrl: string;
18
+ }
19
+ export declare function rfc8628DeviceFlow(opts: Rfc8628AdapterOpts): DeviceFlowAdapter;
20
+ //# sourceMappingURL=rfc8628-device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfc8628-device-flow.d.ts","sourceRoot":"","sources":["../../src/adapters/rfc8628-device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,KAAK,EACV,iBAAiB,EAGlB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAMD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,CAuC7E"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Standards-compliant device-authorization adapter per RFC 8628.
3
+ *
4
+ * Phases:
5
+ * 1. POST `client_id` + `scope` to `deviceUrl` (form-encoded).
6
+ * 2. Surface `user_code` + `verification_uri` to the user.
7
+ * 3. Poll `tokenUrl` every `interval` with
8
+ * `grant_type=urn:ietf:params:oauth:grant-type:device_code` + `device_code`.
9
+ * 4. Handle `authorization_pending` / `slow_down` / fatal codes per spec.
10
+ *
11
+ * The device-authorization request + poll-response classification are shared
12
+ * with the legacy `runDeviceCodeFlow` via `oauth/device-flow-shared`.
13
+ */
14
+ import { classifyDeviceTokenResponse, requestDeviceAuthorization, } from '../oauth/device-flow-shared.js';
15
+ export function rfc8628DeviceFlow(opts) {
16
+ return {
17
+ async start(args) {
18
+ const auth = await requestDeviceAuthorization({
19
+ deviceUrl: opts.deviceUrl,
20
+ clientId: args.clientId,
21
+ scopes: args.scopes,
22
+ ...(args.signal ? { signal: args.signal } : {}),
23
+ });
24
+ return {
25
+ userCode: auth.userCode,
26
+ verificationUri: auth.verificationUri,
27
+ ...(auth.verificationUriComplete
28
+ ? { verificationUriComplete: auth.verificationUriComplete }
29
+ : {}),
30
+ intervalMs: auth.intervalSec * 1000,
31
+ expiresInMs: auth.expiresInSec * 1000,
32
+ providerData: { deviceCode: auth.deviceCode },
33
+ };
34
+ },
35
+ async poll(init, state) {
36
+ const { deviceCode } = init.providerData;
37
+ const body = new URLSearchParams();
38
+ body.set('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
39
+ body.set('device_code', deviceCode);
40
+ const res = await fetch(opts.tokenUrl, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/x-www-form-urlencoded',
44
+ Accept: 'application/json',
45
+ },
46
+ body: body.toString(),
47
+ ...(state.signal ? { signal: state.signal } : {}),
48
+ });
49
+ const json = (await res.json().catch(() => ({})));
50
+ return classifyDeviceTokenResponse(res, json, state);
51
+ },
52
+ };
53
+ }
54
+ //# sourceMappingURL=rfc8628-device-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfc8628-device-flow.js","sourceRoot":"","sources":["../../src/adapters/rfc8628-device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AAkBxC,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,IAAyB;YACnC,MAAM,IAAI,GAAG,MAAM,0BAA0B,CAAC;gBAC5C,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChD,CAAC,CAAC;YACH,OAAO;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,GAAG,CAAC,IAAI,CAAC,uBAAuB;oBAC9B,CAAC,CAAC,EAAE,uBAAuB,EAAE,IAAI,CAAC,uBAAuB,EAAE;oBAC3D,CAAC,CAAC,EAAE,CAAC;gBACP,UAAU,EAAE,IAAI,CAAC,WAAW,GAAG,IAAI;gBACnC,WAAW,EAAE,IAAI,CAAC,YAAY,GAAG,IAAI;gBACrC,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAyB;aACrE,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAoB,EAAE,KAAgB;YAC/C,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,YAA4B,CAAC;YACzD,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,8CAA8C,CAAC,CAAC;YACvE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;YACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE;gBACrC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,mCAAmC;oBACnD,MAAM,EAAE,kBAAkB;iBAC3B;gBACD,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;gBACrB,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClD,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAA4B,CAAC;YAC7E,OAAO,2BAA2B,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Per-credential refresh serialization.
3
+ *
4
+ * OAuth providers with ROTATING single-use refresh tokens (Anthropic's Claude
5
+ * subscription, OpenAI's Codex/ChatGPT plan) invalidate the previous
6
+ * refresh_token on every refresh. Two concurrent refreshes with the same
7
+ * stored token therefore race: one wins, the other burns a now-dead token and
8
+ * logs the user out. `withCredentialLock` serializes "refresh + persist"
9
+ * critical sections at two levels:
10
+ *
11
+ * 1. In-process: a per-key `Mutex` (promise chain) so concurrent consumers
12
+ * in one process (e.g. the chat provider and the whisper-stt transcriber
13
+ * sharing the codex credential) coalesce into a single refresh — the
14
+ * followers re-read the vault under the lock and reuse the winner's
15
+ * rotated tokens.
16
+ * 2. Cross-process: a best-effort O_EXCL lockfile under
17
+ * `<moxxy home>/locks/` with stale-lock takeover, so a TUI and a desktop
18
+ * runner refreshing the same credential queue up instead of racing.
19
+ *
20
+ * The file lock is deliberately best-effort: if the lock directory is
21
+ * unusable, or a live-but-slow holder keeps it past `waitMs`, we proceed
22
+ * WITHOUT the lock rather than deadlocking auth — the vault's read-merge-write
23
+ * persistence and the callers' invalid_grant re-read-retry recovery keep the
24
+ * losing side recoverable even then.
25
+ */
26
+ export interface CredentialLockOptions {
27
+ /** Directory holding the lock files. Default `<moxxy home>/locks`. */
28
+ readonly dir?: string;
29
+ /**
30
+ * Take over a lock file whose mtime is older than this (holder crashed
31
+ * without releasing). Default 60s — comfortably above a worst-case retried
32
+ * token refresh.
33
+ */
34
+ readonly staleMs?: number;
35
+ /** Poll interval while waiting on a held lock. Default 150ms. */
36
+ readonly pollMs?: number;
37
+ /**
38
+ * Max time to wait for the file lock before proceeding WITHOUT it
39
+ * (best effort — never deadlock auth on a wedged lock). Default 30s.
40
+ */
41
+ readonly waitMs?: number;
42
+ }
43
+ /**
44
+ * Run `fn` while holding the per-credential lock (in-process mutex +
45
+ * best-effort cross-process lockfile). Callers should RE-READ their stored
46
+ * credential inside `fn` — a queued waiter usually finds the winner's
47
+ * freshly rotated tokens and can skip its own refresh entirely.
48
+ */
49
+ export declare function withCredentialLock<T>(key: string, fn: () => Promise<T>, opts?: CredentialLockOptions): Promise<T>;
50
+ /**
51
+ * True for deterministic credential rejections from a token endpoint, as
52
+ * opposed to transient network/5xx failures. Covers the AUTH_* codes (401/403
53
+ * and refresh-specific failures) plus PROVIDER_BAD_REQUEST — `invalid_grant`
54
+ * is canonically an HTTP 400 (RFC 6749 §5.2), which `classifyHttpStatus` maps
55
+ * there. Used to decide whether a failed refresh is worth retrying with a
56
+ * fresher refresh_token re-read from the vault (another process may have
57
+ * rotated ours away).
58
+ */
59
+ export declare function isAuthRejection(err: unknown): boolean;
60
+ //# sourceMappingURL=credential-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-lock.d.ts","sourceRoot":"","sources":["../src/credential-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAYD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAQrD"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Per-credential refresh serialization.
3
+ *
4
+ * OAuth providers with ROTATING single-use refresh tokens (Anthropic's Claude
5
+ * subscription, OpenAI's Codex/ChatGPT plan) invalidate the previous
6
+ * refresh_token on every refresh. Two concurrent refreshes with the same
7
+ * stored token therefore race: one wins, the other burns a now-dead token and
8
+ * logs the user out. `withCredentialLock` serializes "refresh + persist"
9
+ * critical sections at two levels:
10
+ *
11
+ * 1. In-process: a per-key `Mutex` (promise chain) so concurrent consumers
12
+ * in one process (e.g. the chat provider and the whisper-stt transcriber
13
+ * sharing the codex credential) coalesce into a single refresh — the
14
+ * followers re-read the vault under the lock and reuse the winner's
15
+ * rotated tokens.
16
+ * 2. Cross-process: a best-effort O_EXCL lockfile under
17
+ * `<moxxy home>/locks/` with stale-lock takeover, so a TUI and a desktop
18
+ * runner refreshing the same credential queue up instead of racing.
19
+ *
20
+ * The file lock is deliberately best-effort: if the lock directory is
21
+ * unusable, or a live-but-slow holder keeps it past `waitMs`, we proceed
22
+ * WITHOUT the lock rather than deadlocking auth — the vault's read-merge-write
23
+ * persistence and the callers' invalid_grant re-read-retry recovery keep the
24
+ * losing side recoverable even then.
25
+ */
26
+ import { mkdir, open, rename, rm, stat } from 'node:fs/promises';
27
+ import { randomBytes } from 'node:crypto';
28
+ import { dirname, join } from 'node:path';
29
+ import { MoxxyError, createMutex } from '@moxxy/sdk';
30
+ import { moxxyPath } from '@moxxy/sdk/server';
31
+ const DEFAULT_STALE_MS = 60_000;
32
+ const DEFAULT_POLL_MS = 150;
33
+ const DEFAULT_WAIT_MS = 30_000;
34
+ /**
35
+ * In-process locks, keyed by sanitized credential key. Module-level so every
36
+ * consumer of this package instance (providers, stt, tools) shares them.
37
+ */
38
+ const inProcessLocks = new Map();
39
+ /**
40
+ * Run `fn` while holding the per-credential lock (in-process mutex +
41
+ * best-effort cross-process lockfile). Callers should RE-READ their stored
42
+ * credential inside `fn` — a queued waiter usually finds the winner's
43
+ * freshly rotated tokens and can skip its own refresh entirely.
44
+ */
45
+ export async function withCredentialLock(key, fn, opts = {}) {
46
+ const safe = sanitizeKey(key);
47
+ let mutex = inProcessLocks.get(safe);
48
+ if (!mutex) {
49
+ mutex = createMutex();
50
+ inProcessLocks.set(safe, mutex);
51
+ }
52
+ return mutex.run(async () => {
53
+ const release = await acquireFileLock(join(opts.dir ?? moxxyPath('locks'), `${safe}.lock`), opts.staleMs ?? DEFAULT_STALE_MS, opts.pollMs ?? DEFAULT_POLL_MS, opts.waitMs ?? DEFAULT_WAIT_MS);
54
+ try {
55
+ return await fn();
56
+ }
57
+ finally {
58
+ await release();
59
+ }
60
+ });
61
+ }
62
+ /**
63
+ * True for deterministic credential rejections from a token endpoint, as
64
+ * opposed to transient network/5xx failures. Covers the AUTH_* codes (401/403
65
+ * and refresh-specific failures) plus PROVIDER_BAD_REQUEST — `invalid_grant`
66
+ * is canonically an HTTP 400 (RFC 6749 §5.2), which `classifyHttpStatus` maps
67
+ * there. Used to decide whether a failed refresh is worth retrying with a
68
+ * fresher refresh_token re-read from the vault (another process may have
69
+ * rotated ours away).
70
+ */
71
+ export function isAuthRejection(err) {
72
+ return (err instanceof MoxxyError &&
73
+ (err.code === 'AUTH_INVALID' ||
74
+ err.code === 'AUTH_DENIED' ||
75
+ err.code === 'AUTH_EXPIRED' ||
76
+ err.code === 'PROVIDER_BAD_REQUEST'));
77
+ }
78
+ function sanitizeKey(key) {
79
+ return key.replace(/[^a-zA-Z0-9._-]+/g, '_');
80
+ }
81
+ /**
82
+ * O_EXCL ('wx') lockfile acquisition with stale takeover. Returns a release
83
+ * function. Never throws: an unusable lock dir degrades to running unlocked.
84
+ */
85
+ async function acquireFileLock(lockPath, staleMs, pollMs, waitMs) {
86
+ const deadline = Date.now() + waitMs;
87
+ try {
88
+ await mkdir(dirname(lockPath), { recursive: true });
89
+ }
90
+ catch {
91
+ return async () => { };
92
+ }
93
+ for (;;) {
94
+ try {
95
+ const handle = await open(lockPath, 'wx');
96
+ try {
97
+ await handle.writeFile(`${process.pid} ${new Date().toISOString()}\n`, 'utf8');
98
+ }
99
+ finally {
100
+ await handle.close().catch(() => { });
101
+ }
102
+ return async () => {
103
+ await rm(lockPath, { force: true }).catch(() => { });
104
+ };
105
+ }
106
+ catch (err) {
107
+ if (err.code !== 'EEXIST') {
108
+ // Read-only FS, permissions, … — degrade to unlocked rather than
109
+ // blocking the user's auth on infrastructure trouble.
110
+ return async () => { };
111
+ }
112
+ }
113
+ // Lock held — take over if the holder looks dead, else wait and re-try.
114
+ // Takeover is made atomic via rename() to dodge the rm()+open() TOCTOU: a
115
+ // naive rm() lets two processes both judge the same lock stale and both
116
+ // delete it, and one could unlink a holder's freshly-recreated lock. POSIX
117
+ // rename is atomic and single-winner — only one racer's rename of the SAME
118
+ // inode succeeds; the loser's rename fails (ENOENT/the file moved) and it
119
+ // spins to re-acquire instead of clobbering the winner. We rename to a
120
+ // unique temp then rm that temp, so the original path is freed for the
121
+ // winner's `open('wx')` on the next spin. The file lock stays best-effort
122
+ // (see the module header): any failure degrades to the unlocked-but-
123
+ // recoverable path guarded by the in-process mutex + invalid_grant retry.
124
+ try {
125
+ const st = await stat(lockPath);
126
+ if (Date.now() - st.mtimeMs > staleMs) {
127
+ const tmp = `${lockPath}.stale-${process.pid}-${randomBytes(6).toString('hex')}`;
128
+ try {
129
+ await rename(lockPath, tmp);
130
+ }
131
+ catch {
132
+ continue; // lost the takeover race (someone moved/recreated it) — respin
133
+ }
134
+ await rm(tmp, { force: true }).catch(() => { });
135
+ continue;
136
+ }
137
+ }
138
+ catch {
139
+ continue; // released between open() and stat() — grab it on the next spin
140
+ }
141
+ if (Date.now() >= deadline)
142
+ return async () => { };
143
+ await sleep(pollMs);
144
+ }
145
+ }
146
+ function sleep(ms) {
147
+ return new Promise((resolve) => setTimeout(resolve, ms));
148
+ }
149
+ //# sourceMappingURL=credential-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-lock.js","sourceRoot":"","sources":["../src/credential-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pre-request token freshness helper. Reads stored creds, refreshes when
3
+ * within `skewMs` of expiry (or forced), persists the rotated tokens BEFORE
4
+ * returning so a crash mid-flight can't strand a single-use refresh_token.
5
+ *
6
+ * The refresh + persist critical section runs under the per-credential lock
7
+ * (`withCredentialLock`): concurrent consumers — in this process or another
8
+ * moxxy process — coalesce into a single IdP refresh, with the followers
9
+ * re-reading the winner's rotated tokens from the vault instead of burning
10
+ * the (single-use, rotating) refresh_token a second time.
11
+ *
12
+ * Returns the live TokenSet + extras the provider can use to build headers
13
+ * (e.g. ChatGPT-Account-Id). Throws when no credential is stored or the
14
+ * refresh fails permanently.
15
+ */
16
+ import { MoxxyError } from '@moxxy/sdk';
17
+ import { type OAuthVault, type StoredCreds } from './storage.js';
18
+ import type { TokenSet } from './oauth/types.js';
19
+ import type { OAuthProviderProfile } from './profile.js';
20
+ /**
21
+ * Single refresh-and-persist critical section shared by `ensureFreshTokens`
22
+ * (provider profiles) and `oauth_get_token` (raw stored creds). Encodes the
23
+ * security-critical sequence ONCE — refresh-token guard, rotation-race
24
+ * recovery, refresh_token preservation (RFC 6749 §6), atomic re-persist —
25
+ * and lets each caller plug in its own provider key, error wording, and
26
+ * extras handling via a {@link RefreshSpec}. MUST run inside
27
+ * `withCredentialLock`: it is a writer to the `oauth/<provider>/*` keys.
28
+ */
29
+ export interface RefreshSpec {
30
+ /** Vault namespace + lock key for this credential. */
31
+ readonly provider: string;
32
+ /** Raised when the stored creds carry no refresh_token to spend. */
33
+ noRefreshTokenError(): MoxxyError;
34
+ /**
35
+ * Wrap a non-network refresh failure (after `classifyNetworkError` declined
36
+ * it). Return a MoxxyError to surface a friendly message, or the original
37
+ * error to re-throw it verbatim.
38
+ */
39
+ wrapRefreshFailure(err: unknown): unknown;
40
+ /**
41
+ * Compute the extras to persist alongside the refreshed tokens, given the
42
+ * merged token set and the previously-stored extras. Return `undefined` to
43
+ * omit the extras key entirely (matching `storeTokenSet`'s present-only
44
+ * write semantics).
45
+ */
46
+ resolveExtras(merged: TokenSet, storedExtras: Readonly<Record<string, string>>): Record<string, string> | undefined;
47
+ }
48
+ export declare function refreshAndStore(vault: OAuthVault, spec: RefreshSpec, stored: StoredCreds, retried?: boolean): Promise<{
49
+ tokens: TokenSet;
50
+ extras: Readonly<Record<string, string>>;
51
+ }>;
52
+ export interface EnsureFreshOptions {
53
+ /** Force a refresh even if the access token hasn't expired. */
54
+ readonly force?: boolean;
55
+ /** Refresh when within this many ms of expiry. Default 60_000. */
56
+ readonly skewMs?: number;
57
+ }
58
+ export interface EnsureFreshResult {
59
+ readonly tokens: TokenSet;
60
+ readonly extras: Readonly<Record<string, string>>;
61
+ }
62
+ export declare function ensureFreshTokens(profile: OAuthProviderProfile, vault: OAuthVault, opts?: EnsureFreshOptions): Promise<EnsureFreshResult>;
63
+ //# sourceMappingURL=ensure-fresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-fresh.d.ts","sourceRoot":"","sources":["../src/ensure-fresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAwB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAA6C,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAG5G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,mBAAmB,IAAI,UAAU,CAAC;IAClC;;;;OAIG;IACH,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1C;;;;;OAKG;IACH,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CACrH;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,WAAW,EACnB,OAAO,UAAQ,GACd,OAAO,CAAC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC,CA4CzE;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,oBAAoB,EAC7B,KAAK,EAAE,UAAU,EACjB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAyB5B"}