@the-open-engine/zeroshot 6.28.0 → 6.29.1

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 (283) hide show
  1. package/README.md +3 -3
  2. package/cli/index.js +20 -224
  3. package/docker/zeroshot-cluster/Dockerfile +2 -3
  4. package/lib/cluster/client.cjs +30 -7
  5. package/lib/cluster/client.d.cts +52 -0
  6. package/lib/cluster/client.d.mts +52 -0
  7. package/lib/cluster/client.d.ts +4 -6
  8. package/lib/cluster/client.mjs +32 -9
  9. package/lib/cluster/connection-state.cjs +25 -0
  10. package/lib/cluster/connection-state.d.cts +13 -0
  11. package/lib/cluster/connection-state.d.mts +13 -0
  12. package/lib/cluster/connection-state.d.ts +13 -0
  13. package/lib/cluster/connection-state.mjs +21 -0
  14. package/lib/cluster/connection-types.cjs +2 -0
  15. package/lib/cluster/connection-types.d.cts +33 -0
  16. package/lib/cluster/connection-types.d.mts +33 -0
  17. package/lib/cluster/connection-types.d.ts +33 -0
  18. package/lib/cluster/connection-types.mjs +1 -0
  19. package/lib/cluster/connection.cjs +18 -46
  20. package/lib/cluster/connection.d.cts +26 -0
  21. package/lib/cluster/connection.d.mts +26 -0
  22. package/lib/cluster/connection.d.ts +7 -25
  23. package/lib/cluster/connection.mjs +12 -42
  24. package/lib/cluster/errors.cjs +9 -1
  25. package/lib/cluster/errors.d.cts +29 -0
  26. package/lib/cluster/errors.d.mts +29 -0
  27. package/lib/cluster/errors.d.ts +4 -0
  28. package/lib/cluster/errors.mjs +7 -0
  29. package/lib/cluster/frames.d.cts +12 -0
  30. package/lib/cluster/frames.d.mts +12 -0
  31. package/lib/cluster/generated/protocol-schema.d.cts +1 -0
  32. package/lib/cluster/generated/protocol-schema.d.mts +1 -0
  33. package/lib/cluster/generated/protocol.d.cts +781 -0
  34. package/lib/cluster/generated/protocol.d.mts +781 -0
  35. package/lib/cluster/index.cjs +2 -2
  36. package/lib/cluster/index.d.cts +13 -0
  37. package/lib/cluster/index.d.mts +13 -0
  38. package/lib/cluster/index.d.ts +3 -3
  39. package/lib/cluster/index.mjs +2 -2
  40. package/lib/cluster/json-source.cjs +1 -0
  41. package/lib/cluster/json-source.d.cts +5 -0
  42. package/lib/cluster/json-source.d.mts +5 -0
  43. package/lib/cluster/json-source.d.ts +1 -0
  44. package/lib/cluster/json-source.mjs +1 -0
  45. package/lib/cluster/payload-value.cjs +1 -0
  46. package/lib/cluster/payload-value.d.cts +8 -0
  47. package/lib/cluster/payload-value.d.mts +8 -0
  48. package/lib/cluster/payload-value.d.ts +1 -0
  49. package/lib/cluster/payload-value.mjs +1 -0
  50. package/lib/cluster/queue.d.cts +15 -0
  51. package/lib/cluster/queue.d.mts +15 -0
  52. package/lib/cluster/socket.cjs +8 -0
  53. package/lib/cluster/socket.d.cts +13 -0
  54. package/lib/cluster/socket.d.mts +13 -0
  55. package/lib/cluster/socket.d.ts +2 -0
  56. package/lib/cluster/socket.mjs +7 -0
  57. package/lib/cluster/subscriptions.d.cts +73 -0
  58. package/lib/cluster/subscriptions.d.mts +73 -0
  59. package/lib/cluster/subscriptions.d.ts +5 -1
  60. package/lib/cluster/validators.cjs +1 -0
  61. package/lib/cluster/validators.d.cts +7 -0
  62. package/lib/cluster/validators.d.mts +7 -0
  63. package/lib/cluster/validators.d.ts +1 -0
  64. package/lib/cluster/validators.mjs +1 -0
  65. package/lib/cluster-worker/engine-adapter.js +1 -11
  66. package/lib/cluster-worker/engine-input.js +14 -0
  67. package/lib/hosted-session/authority.cjs +61 -0
  68. package/lib/hosted-session/authority.d.cts +3 -0
  69. package/lib/hosted-session/authority.d.mts +3 -0
  70. package/lib/hosted-session/authority.d.ts +3 -0
  71. package/lib/hosted-session/authority.mjs +57 -0
  72. package/lib/hosted-session/coordinator.cjs +78 -51
  73. package/lib/hosted-session/coordinator.d.cts +11 -0
  74. package/lib/hosted-session/coordinator.d.mts +11 -0
  75. package/lib/hosted-session/coordinator.d.ts +4 -2
  76. package/lib/hosted-session/coordinator.mjs +75 -51
  77. package/lib/hosted-session/errors.cjs +25 -0
  78. package/lib/hosted-session/errors.d.cts +10 -0
  79. package/lib/hosted-session/errors.d.mts +10 -0
  80. package/lib/hosted-session/errors.d.ts +10 -0
  81. package/lib/hosted-session/errors.mjs +19 -0
  82. package/lib/hosted-session/index.cjs +4 -1
  83. package/lib/hosted-session/index.d.cts +2 -0
  84. package/lib/hosted-session/index.d.mts +2 -0
  85. package/lib/hosted-session/index.d.ts +2 -2
  86. package/lib/hosted-session/index.mjs +1 -1
  87. package/lib/hosted-session/reconnecting-watch.cjs +131 -0
  88. package/lib/hosted-session/reconnecting-watch.d.cts +13 -0
  89. package/lib/hosted-session/reconnecting-watch.d.mts +13 -0
  90. package/lib/hosted-session/reconnecting-watch.d.ts +13 -0
  91. package/lib/hosted-session/reconnecting-watch.mjs +127 -0
  92. package/lib/hosted-session/types.d.cts +32 -0
  93. package/lib/hosted-session/types.d.mts +32 -0
  94. package/lib/hosted-session/types.d.ts +19 -7
  95. package/lib/hosted-target/access-url.cjs +44 -0
  96. package/lib/hosted-target/access-url.d.cts +2 -0
  97. package/lib/hosted-target/access-url.d.mts +2 -0
  98. package/lib/hosted-target/access-url.d.ts +2 -0
  99. package/lib/hosted-target/access-url.mjs +41 -0
  100. package/lib/hosted-target/adapter-request.cjs +12 -0
  101. package/lib/hosted-target/adapter-request.d.cts +17 -0
  102. package/lib/hosted-target/adapter-request.d.mts +17 -0
  103. package/lib/hosted-target/adapter-request.d.ts +17 -0
  104. package/lib/hosted-target/adapter-request.mjs +9 -0
  105. package/lib/hosted-target/adapter-types.cjs +2 -0
  106. package/lib/hosted-target/adapter-types.d.cts +27 -0
  107. package/lib/hosted-target/adapter-types.d.mts +27 -0
  108. package/lib/hosted-target/adapter-types.d.ts +27 -0
  109. package/lib/hosted-target/adapter-types.mjs +1 -0
  110. package/lib/hosted-target/bounds.cjs +10 -0
  111. package/lib/hosted-target/bounds.d.cts +7 -0
  112. package/lib/hosted-target/bounds.d.mts +7 -0
  113. package/lib/hosted-target/bounds.d.ts +7 -0
  114. package/lib/hosted-target/bounds.mjs +7 -0
  115. package/lib/hosted-target/capsule-error-response.cjs +76 -0
  116. package/lib/hosted-target/capsule-error-response.d.cts +2 -0
  117. package/lib/hosted-target/capsule-error-response.d.mts +2 -0
  118. package/lib/hosted-target/capsule-error-response.d.ts +2 -0
  119. package/lib/hosted-target/capsule-error-response.mjs +73 -0
  120. package/lib/hosted-target/errors.cjs +88 -0
  121. package/lib/hosted-target/errors.d.cts +36 -0
  122. package/lib/hosted-target/errors.d.mts +36 -0
  123. package/lib/hosted-target/errors.d.ts +36 -0
  124. package/lib/hosted-target/errors.mjs +75 -0
  125. package/lib/hosted-target/index.cjs +16 -0
  126. package/lib/hosted-target/index.d.cts +3 -0
  127. package/lib/hosted-target/index.d.mts +3 -0
  128. package/lib/hosted-target/index.d.ts +3 -0
  129. package/lib/hosted-target/index.mjs +2 -0
  130. package/lib/hosted-target/response-validation.cjs +169 -0
  131. package/lib/hosted-target/response-validation.d.cts +5 -0
  132. package/lib/hosted-target/response-validation.d.mts +5 -0
  133. package/lib/hosted-target/response-validation.d.ts +5 -0
  134. package/lib/hosted-target/response-validation.mjs +163 -0
  135. package/lib/hosted-target/retry-executor.cjs +51 -0
  136. package/lib/hosted-target/retry-executor.d.cts +8 -0
  137. package/lib/hosted-target/retry-executor.d.mts +8 -0
  138. package/lib/hosted-target/retry-executor.d.ts +8 -0
  139. package/lib/hosted-target/retry-executor.mjs +48 -0
  140. package/lib/hosted-target/retry.cjs +78 -0
  141. package/lib/hosted-target/retry.d.cts +9 -0
  142. package/lib/hosted-target/retry.d.mts +9 -0
  143. package/lib/hosted-target/retry.d.ts +9 -0
  144. package/lib/hosted-target/retry.mjs +73 -0
  145. package/lib/hosted-target/target-adapter.cjs +10 -0
  146. package/lib/hosted-target/target-adapter.d.cts +3 -0
  147. package/lib/hosted-target/target-adapter.d.mts +3 -0
  148. package/lib/hosted-target/target-adapter.d.ts +3 -0
  149. package/lib/hosted-target/target-adapter.mjs +7 -0
  150. package/lib/hosted-target/types.cjs +4 -0
  151. package/lib/hosted-target/types.d.cts +50 -0
  152. package/lib/hosted-target/types.d.mts +50 -0
  153. package/lib/hosted-target/types.d.ts +50 -0
  154. package/lib/hosted-target/types.mjs +1 -0
  155. package/lib/hosted-target/zero-cloud-v1-adapter.cjs +185 -0
  156. package/lib/hosted-target/zero-cloud-v1-adapter.d.cts +13 -0
  157. package/lib/hosted-target/zero-cloud-v1-adapter.d.mts +13 -0
  158. package/lib/hosted-target/zero-cloud-v1-adapter.d.ts +13 -0
  159. package/lib/hosted-target/zero-cloud-v1-adapter.mjs +181 -0
  160. package/lib/target/bounded-response.cjs +51 -0
  161. package/lib/target/bounded-response.d.cts +1 -0
  162. package/lib/target/bounded-response.d.mts +1 -0
  163. package/lib/target/bounded-response.d.ts +1 -0
  164. package/lib/target/bounded-response.js +51 -0
  165. package/lib/target/bounded-response.mjs +48 -0
  166. package/lib/target/credential-lock.js +1 -3
  167. package/lib/target/credential-store.js +9 -4
  168. package/lib/target/device-flow.cjs +213 -0
  169. package/lib/target/device-flow.d.cts +54 -0
  170. package/lib/target/device-flow.d.mts +54 -0
  171. package/lib/target/device-flow.d.ts +26 -10
  172. package/lib/target/device-flow.js +162 -53
  173. package/lib/target/device-flow.mjs +203 -0
  174. package/lib/target/discovery-errors.cjs +10 -0
  175. package/lib/target/discovery-errors.d.cts +3 -0
  176. package/lib/target/discovery-errors.d.mts +3 -0
  177. package/lib/target/discovery-errors.d.ts +3 -0
  178. package/lib/target/discovery-errors.js +10 -0
  179. package/lib/target/discovery-errors.mjs +6 -0
  180. package/lib/target/discovery-sections.cjs +191 -0
  181. package/lib/target/discovery-sections.d.cts +52 -0
  182. package/lib/target/discovery-sections.d.mts +52 -0
  183. package/lib/target/discovery-sections.d.ts +52 -0
  184. package/lib/target/discovery-sections.js +191 -0
  185. package/lib/target/discovery-sections.mjs +179 -0
  186. package/lib/target/discovery-validation.cjs +109 -0
  187. package/lib/target/discovery-validation.d.cts +28 -0
  188. package/lib/target/discovery-validation.d.mts +28 -0
  189. package/lib/target/discovery-validation.d.ts +28 -0
  190. package/lib/target/discovery-validation.js +109 -0
  191. package/lib/target/discovery-validation.mjs +99 -0
  192. package/lib/target/discovery.cjs +112 -0
  193. package/lib/target/discovery.d.cts +74 -0
  194. package/lib/target/discovery.d.mts +74 -0
  195. package/lib/target/discovery.d.ts +68 -5
  196. package/lib/target/discovery.js +93 -101
  197. package/lib/target/discovery.mjs +105 -0
  198. package/lib/target/index.d.ts +6 -6
  199. package/lib/target/index.js +36 -36
  200. package/lib/target/oauth-http.d.ts +2 -0
  201. package/lib/target/oauth-http.js +25 -0
  202. package/lib/target/refresh-exchange.d.ts +11 -0
  203. package/lib/target/refresh-exchange.js +24 -0
  204. package/lib/target/refresh-revocation.d.ts +8 -0
  205. package/lib/target/refresh-revocation.js +19 -0
  206. package/lib/target/route-template.cjs +85 -0
  207. package/lib/target/route-template.d.cts +7 -0
  208. package/lib/target/route-template.d.mts +7 -0
  209. package/lib/target/route-template.d.ts +7 -0
  210. package/lib/target/route-template.js +85 -0
  211. package/lib/target/route-template.mjs +81 -0
  212. package/lib/target/session-errors.d.ts +4 -0
  213. package/lib/target/session-errors.js +12 -0
  214. package/lib/target/session-verification.d.ts +10 -0
  215. package/lib/target/session-verification.js +42 -0
  216. package/lib/target/target-registry.d.ts +7 -3
  217. package/lib/target/target-registry.js +41 -10
  218. package/lib/target/target-session-manager.d.ts +48 -0
  219. package/lib/target/target-session-manager.js +226 -0
  220. package/lib/target/target-session.d.ts +39 -31
  221. package/lib/target/target-session.js +63 -150
  222. package/lib/target/token-response.cjs +51 -0
  223. package/lib/target/token-response.d.cts +7 -0
  224. package/lib/target/token-response.d.mts +7 -0
  225. package/lib/target/token-response.d.ts +7 -0
  226. package/lib/target/token-response.js +51 -0
  227. package/lib/target/token-response.mjs +48 -0
  228. package/package.json +16 -6
  229. package/scripts/audit-production-dependencies.js +150 -0
  230. package/scripts/build-cluster.js +23 -3
  231. package/scripts/opcore-agent-gate.js +159 -0
  232. package/scripts/opcore-agent-tool-overlays.js +154 -0
  233. package/scripts/opcore-introduced-check.js +288 -0
  234. package/src/cluster/client.ts +42 -15
  235. package/src/cluster/connection-state.ts +33 -0
  236. package/src/cluster/connection-types.ts +28 -0
  237. package/src/cluster/connection.ts +37 -68
  238. package/src/cluster/errors.ts +5 -0
  239. package/src/cluster/index.ts +3 -1
  240. package/src/cluster/json-source.ts +1 -0
  241. package/src/cluster/payload-value.ts +1 -0
  242. package/src/cluster/socket.ts +13 -0
  243. package/src/cluster/subscriptions.ts +7 -2
  244. package/src/cluster/validators.ts +1 -0
  245. package/src/cluster/ws.d.ts +1 -1
  246. package/src/hosted-session/authority.ts +77 -0
  247. package/src/hosted-session/coordinator.ts +113 -56
  248. package/src/hosted-session/errors.ts +21 -0
  249. package/src/hosted-session/index.ts +14 -2
  250. package/src/hosted-session/reconnecting-watch.ts +148 -0
  251. package/src/hosted-session/types.ts +22 -8
  252. package/src/hosted-target/access-url.ts +46 -0
  253. package/src/hosted-target/adapter-request.ts +28 -0
  254. package/src/hosted-target/adapter-types.ts +39 -0
  255. package/src/hosted-target/bounds.ts +1 -0
  256. package/src/hosted-target/capsule-error-response.ts +107 -0
  257. package/src/hosted-target/errors.ts +36 -35
  258. package/src/hosted-target/index.ts +10 -23
  259. package/src/hosted-target/response-validation.ts +170 -62
  260. package/src/hosted-target/retry-executor.ts +62 -0
  261. package/src/hosted-target/retry.ts +47 -11
  262. package/src/hosted-target/target-adapter.ts +12 -8
  263. package/src/hosted-target/types.ts +19 -18
  264. package/src/hosted-target/zero-cloud-v1-adapter.ts +229 -316
  265. package/src/target/bounded-response.ts +68 -0
  266. package/src/target/credential-lock.ts +1 -3
  267. package/src/target/credential-store.ts +14 -10
  268. package/src/target/device-flow.ts +206 -71
  269. package/src/target/discovery-errors.ts +6 -0
  270. package/src/target/discovery-sections.ts +251 -0
  271. package/src/target/discovery-validation.ts +144 -0
  272. package/src/target/discovery.ts +163 -115
  273. package/src/target/index.ts +15 -11
  274. package/src/target/oauth-http.ts +27 -0
  275. package/src/target/refresh-exchange.ts +36 -0
  276. package/src/target/refresh-revocation.ts +29 -0
  277. package/src/target/route-template.ts +105 -0
  278. package/src/target/session-errors.ts +9 -0
  279. package/src/target/session-verification.ts +51 -0
  280. package/src/target/target-registry.ts +60 -27
  281. package/src/target/target-session-manager.ts +285 -0
  282. package/src/target/target-session.ts +105 -222
  283. package/src/target/token-response.ts +62 -0
@@ -0,0 +1,68 @@
1
+ async function declaredResponseBytes(
2
+ response: Response,
3
+ maxBytes: number,
4
+ error: (kind: 'size' | 'json') => Error,
5
+ ): Promise<number> {
6
+ const declared = response.headers.get('content-length');
7
+ if (declared === null) return 0;
8
+ if (!/^\d+$/.test(declared) || Number(declared) > maxBytes) {
9
+ await response.body?.cancel().catch(() => undefined);
10
+ throw error('size');
11
+ }
12
+ return Number(declared);
13
+ }
14
+
15
+ function growBuffer(
16
+ bytes: Uint8Array,
17
+ required: number,
18
+ used: number,
19
+ maxBytes: number,
20
+ ): Uint8Array {
21
+ const capacity = Math.min(
22
+ maxBytes,
23
+ Math.max(required, Math.max(bytes.byteLength * 2, 8 * 1024)),
24
+ );
25
+ const grown = new Uint8Array(capacity);
26
+ grown.set(bytes.subarray(0, used));
27
+ return grown;
28
+ }
29
+
30
+ function decodeJson(
31
+ bytes: Uint8Array,
32
+ error: (kind: 'size' | 'json') => Error,
33
+ ): unknown {
34
+ try {
35
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
36
+ } catch {
37
+ throw error('json');
38
+ }
39
+ }
40
+
41
+ export async function readBoundedResponseJson(
42
+ response: Response,
43
+ maxBytes: number,
44
+ error: (kind: 'size' | 'json') => Error,
45
+ ): Promise<unknown> {
46
+ const declaredBytes = await declaredResponseBytes(response, maxBytes, error);
47
+ const reader = response.body?.getReader();
48
+ if (reader === undefined) return decodeJson(new Uint8Array(), error);
49
+ let bytes: Uint8Array = new Uint8Array(
50
+ Math.min(maxBytes, Math.max(declaredBytes, 8 * 1024)),
51
+ );
52
+ let total = 0;
53
+ while (true) {
54
+ const { done, value } = await reader.read();
55
+ if (done) break;
56
+ const nextTotal = total + value.byteLength;
57
+ if (nextTotal > maxBytes) {
58
+ await reader.cancel().catch(() => undefined);
59
+ throw error('size');
60
+ }
61
+ if (nextTotal > bytes.byteLength) {
62
+ bytes = growBuffer(bytes, nextTotal, total, maxBytes);
63
+ }
64
+ bytes.set(value, total);
65
+ total = nextTotal;
66
+ }
67
+ return decodeJson(bytes.subarray(0, total), error);
68
+ }
@@ -29,7 +29,5 @@ export async function acquireTargetLock(targetId: string): Promise<() => Promise
29
29
  },
30
30
  });
31
31
 
32
- return async () => {
33
- await release();
34
- };
32
+ return release;
35
33
  }
@@ -22,16 +22,16 @@ export const TARGET_ACCOUNT = 'refresh-token';
22
22
 
23
23
  export class KeyringCredentialStore implements TargetCredentialStore {
24
24
  private readonly Entry: new (service: string, account: string) => {
25
- getPassword(): string;
25
+ getPassword(): string | null;
26
26
  setPassword(password: string): void;
27
- deletePassword(): void;
27
+ deletePassword(): boolean;
28
28
  };
29
29
 
30
30
  private constructor(
31
31
  Entry: new (service: string, account: string) => {
32
- getPassword(): string;
32
+ getPassword(): string | null;
33
33
  setPassword(password: string): void;
34
- deletePassword(): void;
34
+ deletePassword(): boolean;
35
35
  },
36
36
  ) {
37
37
  this.Entry = Entry;
@@ -39,9 +39,9 @@ export class KeyringCredentialStore implements TargetCredentialStore {
39
39
 
40
40
  static async create(): Promise<KeyringCredentialStore> {
41
41
  let keyringModule: { Entry: new (service: string, account: string) => {
42
- getPassword(): string;
42
+ getPassword(): string | null;
43
43
  setPassword(password: string): void;
44
- deletePassword(): void;
44
+ deletePassword(): boolean;
45
45
  } };
46
46
  try {
47
47
  keyringModule = await import('@napi-rs/keyring') as typeof keyringModule;
@@ -59,13 +59,17 @@ export class KeyringCredentialStore implements TargetCredentialStore {
59
59
  const entry = new this.Entry(service, account);
60
60
  return entry.getPassword();
61
61
  } catch {
62
- return null;
62
+ throw new CredentialStoreUnavailableError('The secure credential store could not be read.');
63
63
  }
64
64
  }
65
65
 
66
66
  async set(service: string, account: string, token: string): Promise<void> {
67
- const entry = new this.Entry(service, account);
68
- entry.setPassword(token);
67
+ try {
68
+ const entry = new this.Entry(service, account);
69
+ entry.setPassword(token);
70
+ } catch {
71
+ throw new CredentialStoreUnavailableError('The secure credential store could not be updated.');
72
+ }
69
73
  }
70
74
 
71
75
  async delete(service: string, account: string): Promise<void> {
@@ -73,7 +77,7 @@ export class KeyringCredentialStore implements TargetCredentialStore {
73
77
  const entry = new this.Entry(service, account);
74
78
  entry.deletePassword();
75
79
  } catch {
76
- // Already deleted or not present
80
+ throw new CredentialStoreUnavailableError('The secure credential store could not be cleared.');
77
81
  }
78
82
  }
79
83
  }
@@ -1,3 +1,6 @@
1
+ import { readBoundedResponseJson } from './bounded-response.js';
2
+ import { parseTokenResponse, type TokenResponse } from './token-response.js';
3
+ export { parseTokenResponse, type TokenResponse } from './token-response.js';
1
4
  export interface DeviceCodeResponse {
2
5
  readonly device_code: string;
3
6
  readonly user_code: string;
@@ -7,12 +10,36 @@ export interface DeviceCodeResponse {
7
10
  readonly interval: number;
8
11
  }
9
12
 
10
- export interface TokenResponse {
11
- readonly access_token: string;
12
- readonly refresh_token: string;
13
- readonly token_type: string;
14
- readonly expires_in: number;
15
- readonly organization?: { readonly id: string; readonly name: string };
13
+
14
+ export interface DeviceExchangeContext {
15
+ readonly grantType: string;
16
+ readonly deviceToken: string;
17
+ readonly deviceLabel: string;
18
+ readonly audience: string;
19
+ }
20
+
21
+ export interface DeviceIdentity {
22
+ readonly token: string;
23
+ readonly label: string;
24
+ }
25
+
26
+ function exchangeFields(
27
+ exchange: DeviceExchangeContext | DeviceIdentity | undefined,
28
+ ): Readonly<Record<string, string>> {
29
+ if (exchange === undefined) return {};
30
+ if ('grantType' in exchange) {
31
+ return {
32
+ grant_type: exchange.grantType,
33
+ device_token: exchange.deviceToken,
34
+ device_label: exchange.deviceLabel,
35
+ audience: exchange.audience,
36
+ };
37
+ }
38
+ return {
39
+ device_token: exchange.token,
40
+ device_label: exchange.label,
41
+ audience: 'admin',
42
+ };
16
43
  }
17
44
 
18
45
  export interface HttpTransport {
@@ -49,6 +76,124 @@ export class UnboundSessionError extends Error {
49
76
  }
50
77
 
51
78
  const DEFAULT_CLOCK: Clock = { now: () => Date.now() };
79
+ const MAX_OAUTH_RESPONSE_BYTES = 64 * 1024;
80
+ const MAX_DEVICE_CODE_BYTES = 16 * 1024;
81
+ const LOOPBACK_HOSTS: Readonly<Record<string, true>> = Object.freeze({
82
+ '127.0.0.1': true,
83
+ '::1': true,
84
+ '[::1]': true,
85
+ });
86
+ const MAX_URI_BYTES = 4 * 1024;
87
+ export function oauthFormRequest(
88
+ body: URLSearchParams,
89
+ signal?: AbortSignal,
90
+ ): RequestInit & { redirect: 'error' } {
91
+ const init: RequestInit & { redirect: 'error' } = {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
94
+ body: body.toString(),
95
+ redirect: 'error',
96
+ };
97
+ if (signal !== undefined) init.signal = signal;
98
+ return init;
99
+ }
100
+
101
+ async function requireResponseRoute(response: Response, expectedUrl: string): Promise<void> {
102
+ if (!response.url || new URL(response.url).href === expectedUrl) return;
103
+ await response.body?.cancel().catch(() => undefined);
104
+ throw new Error('OAuth response changed target route or authority');
105
+ }
106
+
107
+
108
+ async function readBoundedJson(response: Response): Promise<unknown> {
109
+ return readBoundedResponseJson(response, MAX_OAUTH_RESPONSE_BYTES, (kind) =>
110
+ new Error(kind === 'size' ? 'OAuth response exceeds the size limit' : 'OAuth response is malformed'),
111
+ );
112
+ }
113
+
114
+ function boundedString(value: unknown, maxBytes: number): value is string {
115
+ return (
116
+ typeof value === 'string' &&
117
+ value.length > 0 &&
118
+ new TextEncoder().encode(value).byteLength <= maxBytes
119
+ );
120
+ }
121
+
122
+ function safeVerificationAuthority(url: URL, allowQuery: boolean): boolean {
123
+ const literalLoopback = LOOPBACK_HOSTS[url.hostname] === true;
124
+ return !url.username && !url.password && !url.hash &&
125
+ (allowQuery || !url.search) &&
126
+ (url.protocol === 'https:' || (url.protocol === 'http:' && literalLoopback));
127
+ }
128
+
129
+ function verificationUrl(value: unknown, allowQuery: boolean): value is string {
130
+ if (
131
+ !boundedString(value, MAX_URI_BYTES) ||
132
+ /[\u0000-\u0020\u007f]|\s/u.test(value)
133
+ ) {
134
+ return false;
135
+ }
136
+ try {
137
+ const url = new URL(value);
138
+ return url.href === value && safeVerificationAuthority(url, allowQuery);
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function deviceFieldsValid(device: Record<string, unknown>): boolean {
144
+ const fields = Object.keys(device);
145
+ const required = ['device_code', 'user_code', 'verification_uri', 'expires_in', 'interval'];
146
+ return fields.every((field) =>
147
+ required.includes(field) || field === 'verification_uri_complete',
148
+ ) && required.every((field) => field in device);
149
+ }
150
+
151
+ function deviceStringsValid(device: Record<string, unknown>): boolean {
152
+ return boundedString(device.device_code, MAX_DEVICE_CODE_BYTES) &&
153
+ boundedString(device.user_code, 256) &&
154
+ verificationUrl(device.verification_uri, false) &&
155
+ (device.verification_uri_complete === undefined ||
156
+ verificationUrl(device.verification_uri_complete, true));
157
+ }
158
+
159
+ function deviceTimingValid(device: Record<string, unknown>): boolean {
160
+ return Number.isSafeInteger(device.expires_in) &&
161
+ (device.expires_in as number) >= 1 &&
162
+ (device.expires_in as number) <= 86_400 &&
163
+ Number.isSafeInteger(device.interval) &&
164
+ (device.interval as number) >= 0 &&
165
+ (device.interval as number) <= 300;
166
+ }
167
+
168
+ function browserAuthorityMatches(device: Record<string, unknown>): boolean {
169
+ return device.verification_uri_complete === undefined ||
170
+ new URL(device.verification_uri_complete as string).origin ===
171
+ new URL(device.verification_uri as string).origin;
172
+ }
173
+
174
+
175
+ function parseDeviceCodeResponse(value: unknown): DeviceCodeResponse {
176
+ const device = object(value, 'Device code');
177
+ if (!deviceFieldsValid(device) || !deviceStringsValid(device) ||
178
+ !deviceTimingValid(device) || !browserAuthorityMatches(device)) {
179
+ throw new Error('Device code response is malformed');
180
+ }
181
+ return Object.freeze(device) as unknown as DeviceCodeResponse;
182
+ }
183
+
184
+ function parseOAuthError(value: unknown): string {
185
+ const error = object(value, 'OAuth error');
186
+ if (Object.keys(error).length !== 1 || typeof error.error !== 'string') {
187
+ throw new Error('OAuth error response is malformed');
188
+ }
189
+ return error.error;
190
+ }
191
+ function object(value: unknown, field: string): Record<string, unknown> {
192
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
193
+ throw new Error(`${field} response is malformed`);
194
+ }
195
+ return value as Record<string, unknown>;
196
+ }
52
197
 
53
198
  function sleep(ms: number, signal?: AbortSignal): Promise<void> {
54
199
  return new Promise((resolve, reject) => {
@@ -74,84 +219,74 @@ export async function requestDeviceCode(
74
219
  http: HttpTransport,
75
220
  signal?: AbortSignal,
76
221
  ): Promise<DeviceCodeResponse> {
77
- const body = new URLSearchParams({
78
- client_id: clientId,
79
- scope: 'openid',
80
- });
81
-
82
- const init: RequestInit & { redirect: 'error' } = {
83
- method: 'POST',
84
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
85
- body: body.toString(),
86
- redirect: 'error',
87
- };
88
- if (signal) init.signal = signal;
222
+ const body = new URLSearchParams({ client_id: clientId });
89
223
 
90
- const response = await http.fetch(deviceAuthorizationEndpoint, init);
224
+ const response = await http.fetch(
225
+ deviceAuthorizationEndpoint,
226
+ oauthFormRequest(body, signal),
227
+ );
228
+ await requireResponseRoute(response, deviceAuthorizationEndpoint);
91
229
 
92
230
  if (!response.ok) {
93
- const text = await response.text();
94
- throw new Error(`Device code request failed (${response.status}): ${text}`);
231
+ parseOAuthError(await readBoundedJson(response));
232
+ throw new Error(`Device code request failed (${response.status})`);
95
233
  }
96
234
 
97
- return (await response.json()) as DeviceCodeResponse;
235
+ return parseDeviceCodeResponse(await readBoundedJson(response));
98
236
  }
99
237
 
100
- export async function pollForToken(
101
- tokenEndpoint: string,
102
- clientId: string,
103
- deviceCode: string,
104
- interval: number,
105
- expiresIn: number,
106
- http: HttpTransport,
107
- clock: Clock = DEFAULT_CLOCK,
108
- signal?: AbortSignal,
109
- ): Promise<TokenResponse> {
110
- const deadline = clock.now() + expiresIn * 1000;
111
- let currentInterval = interval;
112
-
113
- while (clock.now() < deadline) {
114
- if (signal?.aborted) {
115
- throw signal.reason ?? new DOMException('Aborted', 'AbortError');
116
- }
117
-
118
- await sleep(currentInterval * 1000, signal);
238
+ export interface PollForTokenRequest {
239
+ readonly tokenEndpoint: string;
240
+ readonly clientId: string;
241
+ readonly deviceCode: string;
242
+ readonly interval: number;
243
+ readonly expiresIn: number;
244
+ readonly http: HttpTransport;
245
+ readonly clock?: Clock;
246
+ readonly signal?: AbortSignal;
247
+ readonly exchange?: DeviceExchangeContext | DeviceIdentity;
248
+ }
249
+ type PollResult = TokenResponse | 'authorization_pending' | 'slow_down';
119
250
 
120
- const body = new URLSearchParams({
121
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
122
- device_code: deviceCode,
123
- client_id: clientId,
124
- });
251
+ async function parsePollResponse(response: Response): Promise<PollResult> {
252
+ if (response.ok) return parseTokenResponse(await readBoundedJson(response));
253
+ const error = parseOAuthError(await readBoundedJson(response));
254
+ if (error === 'authorization_pending' || error === 'slow_down') return error;
255
+ if (error === 'access_denied') throw new DeviceFlowDeniedError();
256
+ if (error === 'expired_token') throw new DeviceFlowExpiredError();
257
+ throw new Error('Token endpoint returned an unsupported OAuth error');
258
+ }
125
259
 
126
- const init: RequestInit & { redirect: 'error' } = {
127
- method: 'POST',
128
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
129
- body: body.toString(),
130
- redirect: 'error',
131
- };
132
- if (signal) init.signal = signal;
260
+ function pollBody(request: PollForTokenRequest): URLSearchParams {
261
+ return new URLSearchParams({
262
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
263
+ device_code: request.deviceCode,
264
+ client_id: request.clientId,
265
+ ...exchangeFields(request.exchange),
266
+ });
267
+ }
133
268
 
134
- const response = await http.fetch(tokenEndpoint, init);
269
+ export async function pollForToken(request: PollForTokenRequest): Promise<TokenResponse> {
270
+ const clock = request.clock ?? DEFAULT_CLOCK;
271
+ const deadline = clock.now() + request.expiresIn * 1000;
272
+ let currentInterval = request.interval;
135
273
 
136
- if (response.ok) {
137
- return (await response.json()) as TokenResponse;
274
+ while (clock.now() < deadline) {
275
+ if (request.signal?.aborted) {
276
+ throw request.signal.reason ?? new DOMException('Aborted', 'AbortError');
138
277
  }
139
-
140
- const errorBody = (await response.json()) as { error: string };
141
- switch (errorBody.error) {
142
- case 'authorization_pending':
143
- continue;
144
- case 'slow_down':
145
- currentInterval += 5;
146
- continue;
147
- case 'access_denied':
148
- throw new DeviceFlowDeniedError();
149
- case 'expired_token':
150
- throw new DeviceFlowExpiredError();
151
- default:
152
- throw new Error(`Token endpoint error: ${errorBody.error}`);
278
+ await sleep(currentInterval * 1000, request.signal);
279
+ const response = await request.http.fetch(
280
+ request.tokenEndpoint,
281
+ oauthFormRequest(pollBody(request), request.signal),
282
+ );
283
+ const result = await parsePollResponse(response);
284
+ if (result === 'authorization_pending') continue;
285
+ if (result === 'slow_down') {
286
+ currentInterval += 5;
287
+ continue;
153
288
  }
289
+ return result;
154
290
  }
155
-
156
291
  throw new DeviceFlowExpiredError();
157
292
  }
@@ -0,0 +1,6 @@
1
+ export class TargetDiscoveryError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'TargetDiscoveryError';
5
+ }
6
+ }