@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
@@ -1,21 +1,18 @@
1
- import { MAX_ERROR_BODY_BYTES, MAX_RESPONSE_BYTES, MAX_RETRY_ELAPSED_MS, IDEMPOTENCY_KEY_PATTERN } from './bounds.ts';
1
+ import { IDEMPOTENCY_KEY_PATTERN, MAX_RESPONSE_BYTES } from './bounds.js';
2
2
  import {
3
+ TargetAdapterError,
3
4
  TargetAuthError,
4
- TargetConflictError,
5
- TargetNotFoundError,
6
5
  TargetProtocolError,
7
- TargetRateLimitError,
8
6
  TargetTransportError,
9
- } from './errors.ts';
10
- import { TargetAdapterError } from './errors.ts';
11
- import { DefaultRetryPolicy, parseRetryAfter } from './retry.ts';
7
+ } from './errors.js';
8
+ import { DefaultRetryPolicy } from './retry.js';
12
9
  import {
13
10
  assertCapsule,
14
11
  assertCapsuleAccess,
15
12
  assertCapsuleLimits,
16
13
  assertCapsuleListPage,
17
- } from './response-validation.ts';
18
- import type { TargetAdapter } from './target-adapter.ts';
14
+ } from './response-validation.js';
15
+ import type { TargetAdapter, CreateTargetAdapterOptions } from './adapter-types.js';
19
16
  import type {
20
17
  AllocateRequest,
21
18
  Capsule,
@@ -24,363 +21,279 @@ import type {
24
21
  CapsuleListPage,
25
22
  Clock,
26
23
  HttpTransport,
24
+ ListRequest,
27
25
  RetryPolicy,
28
26
  TargetAccessTokenProvider,
29
- TargetDiscovery,
30
- } from './types.ts';
31
-
32
- interface ZeroCloudV1Options {
33
- readonly discovery: TargetDiscovery;
34
- readonly organization: string;
35
- readonly tokenProvider: TargetAccessTokenProvider;
36
- readonly transport?: HttpTransport;
37
- readonly clock?: Clock;
38
- readonly retryPolicy?: RetryPolicy;
39
- }
27
+ } from './types.js';
28
+ import type { TargetDiscoveryDescriptor } from '../target/discovery.js';
29
+ import { readBoundedResponseJson } from '../target/bounded-response.js';
30
+ import { throwCapsuleServerError } from './capsule-error-response.js';
31
+ import { validateAccessUrl } from './access-url.js';
32
+ import { withTargetRetry } from './retry-executor.js';
33
+ import {
34
+ requestUrl,
35
+ type AdapterRequest,
36
+ type ExecuteArguments,
37
+ } from './adapter-request.js';
40
38
 
41
39
  const DEFAULT_TRANSPORT: HttpTransport = {
42
- fetch(url: string, init: RequestInit & { redirect: 'error' }): Promise<Response> {
40
+ fetch(url, init) {
43
41
  return globalThis.fetch(url, init);
44
42
  },
45
43
  };
46
-
47
44
  const DEFAULT_CLOCK: Clock = { now: () => Date.now() };
48
45
 
49
46
  function throwIfAborted(signal?: AbortSignal): void {
50
- if (signal?.aborted) {
51
- throw signal.reason === undefined
52
- ? new DOMException('The operation was aborted', 'AbortError')
53
- : signal.reason;
54
- }
47
+ if (signal?.aborted)
48
+ throw signal.reason ?? new globalThis.DOMException('The operation was aborted', 'AbortError');
55
49
  }
56
50
 
57
- async function waitForRetryDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
58
- throwIfAborted(signal);
59
- if (delayMs <= 0) return;
60
-
61
- await new Promise<void>((resolve, reject) => {
62
- const onAbort = (): void => {
63
- clearTimeout(timer);
64
- reject(
65
- signal!.reason === undefined
66
- ? new DOMException('The operation was aborted', 'AbortError')
67
- : signal!.reason,
68
- );
69
- };
70
- const timer = setTimeout(() => {
71
- signal?.removeEventListener('abort', onAbort);
72
- resolve();
73
- }, delayMs);
74
- signal?.addEventListener('abort', onAbort, { once: true });
75
- });
51
+ function validOpaque(value: string, field: string): void {
52
+ if (value.length === 0 || value.length > 1024)
53
+ throw new TargetProtocolError(`${field} is invalid`);
76
54
  }
77
55
 
78
- function originOf(url: string): string {
79
- try {
80
- const u = new URL(url);
81
- return u.origin;
82
- } catch {
83
- throw new TargetProtocolError(`Invalid URL: ${url}`);
84
- }
56
+ function jsonRequest(body: unknown): string {
57
+ return JSON.stringify(body);
85
58
  }
86
59
 
60
+
87
61
  export class ZeroCloudV1TargetAdapter implements TargetAdapter {
88
- private readonly discovery: TargetDiscovery;
89
- private readonly organization: string;
90
- private readonly tokenProvider: TargetAccessTokenProvider;
91
- private readonly transport: HttpTransport;
92
- private readonly clock: Clock;
93
- private readonly retryPolicy: RetryPolicy;
94
- private readonly expectedOrigin: string;
62
+ readonly #descriptor: TargetDiscoveryDescriptor;
63
+ readonly #organizationId: string;
64
+ readonly #tokenProvider: TargetAccessTokenProvider;
65
+ readonly #transport: HttpTransport;
66
+ readonly #clock: Clock;
67
+ readonly #retryPolicy: RetryPolicy;
68
+
69
+ constructor(options: CreateTargetAdapterOptions) {
70
+ this.#descriptor = options.descriptor;
71
+ this.#organizationId = options.organization.id;
72
+ validOpaque(this.#organizationId, 'organization id');
73
+ this.#tokenProvider = options.tokenProvider;
74
+ this.#transport = options.transport ?? DEFAULT_TRANSPORT;
75
+ this.#clock = options.clock ?? DEFAULT_CLOCK;
76
+ this.#retryPolicy = options.retryPolicy ?? new DefaultRetryPolicy();
77
+ }
95
78
 
96
- constructor(opts: ZeroCloudV1Options) {
97
- this.discovery = opts.discovery;
98
- this.organization = opts.organization;
99
- this.tokenProvider = opts.tokenProvider;
100
- this.transport = opts.transport ?? DEFAULT_TRANSPORT;
101
- this.clock = opts.clock ?? DEFAULT_CLOCK;
102
- this.retryPolicy = opts.retryPolicy ?? new DefaultRetryPolicy();
103
- this.expectedOrigin = originOf(opts.discovery.capsuleV1);
79
+ get credentialInstall(): TargetAdapter['credentialInstall'] {
80
+ const descriptor = this.#descriptor.credentialInstall;
81
+ return descriptor === null
82
+ ? Object.freeze({ supported: false as const })
83
+ : Object.freeze({ supported: true as const, descriptor });
104
84
  }
105
85
 
106
- async allocate(req: AllocateRequest, signal?: AbortSignal): Promise<Capsule> {
107
- if (!IDEMPOTENCY_KEY_PATTERN.test(req.idempotencyKey)) {
108
- throw new TargetProtocolError(
109
- `Invalid idempotency key: must match ${IDEMPOTENCY_KEY_PATTERN}`,
86
+ allocate(request: AllocateRequest, signal?: AbortSignal): Promise<Capsule> {
87
+ try {
88
+ if (!IDEMPOTENCY_KEY_PATTERN.test(request.idempotencyKey))
89
+ throw new TargetProtocolError('Idempotency key is invalid');
90
+ if (request.label !== undefined && (request.label.length < 1 || request.label.length > 100)) {
91
+ throw new TargetProtocolError('Capsule label is outside the supported bounds');
92
+ }
93
+ if (request.size !== undefined && !this.#descriptor.sizes.catalog.includes(request.size)) {
94
+ throw new TargetProtocolError('Capsule size is not advertised by the target');
95
+ }
96
+ const body = {
97
+ ...(request.label === undefined ? {} : { label: request.label }),
98
+ ...(request.size === undefined ? {} : { size: request.size }),
99
+ };
100
+ return this.#execute(
101
+ 'allocate',
102
+ 'POST',
103
+ this.#descriptor.capsule.routes.allocate,
104
+ { org_id: this.#organizationId },
105
+ 201,
106
+ assertCapsule,
107
+ signal,
108
+ {
109
+ body: jsonRequest(body),
110
+ headers: { 'Idempotency-Key': request.idempotencyKey },
111
+ }
110
112
  );
113
+ } catch (error) {
114
+ return Promise.reject(error);
111
115
  }
112
-
113
- const body = JSON.stringify({ profile: req.profile, organization: this.organization });
114
-
115
- return this._withRetry(async () => {
116
- const resp = await this._request('POST', '/capsules', {
117
- signal,
118
- body,
119
- headers: { 'Idempotency-Key': req.idempotencyKey },
120
- });
121
- const json = await this._readJson(resp);
122
- return assertCapsule(json);
123
- }, signal);
124
116
  }
125
117
 
126
- async list(cursor?: string, signal?: AbortSignal): Promise<CapsuleListPage> {
127
- return this._withRetry(async () => {
128
- const params = new URLSearchParams({ organization: this.organization });
129
- if (cursor) params.set('cursor', cursor);
130
- const resp = await this._request('GET', `/capsules?${params.toString()}`, { signal });
131
- const json = await this._readJson(resp);
132
- const page = assertCapsuleListPage(json);
133
- if (page.cursor !== undefined && page.cursor === cursor) {
134
- throw new TargetProtocolError(
135
- `Pagination loop detected: server returned the same cursor "${cursor}"`,
136
- );
137
- }
138
- return page;
139
- }, signal);
118
+ async list(request: ListRequest = {}, signal?: AbortSignal): Promise<CapsuleListPage> {
119
+ if (request.cursor !== undefined) validOpaque(request.cursor, 'cursor');
120
+ if (
121
+ request.limit !== undefined &&
122
+ (!Number.isSafeInteger(request.limit) ||
123
+ request.limit < 1 ||
124
+ request.limit > this.#descriptor.pagination.maxPageSize)
125
+ ) {
126
+ throw new TargetProtocolError('Pagination limit is outside the advertised bounds');
127
+ }
128
+ const page = await this.#execute(
129
+ 'list',
130
+ 'GET',
131
+ this.#descriptor.capsule.routes.list,
132
+ {
133
+ org_id: this.#organizationId,
134
+ cursor: request.cursor,
135
+ limit: request.limit,
136
+ },
137
+ 200,
138
+ assertCapsuleListPage,
139
+ signal
140
+ );
141
+ if (request.cursor !== undefined && page.nextCursor === request.cursor)
142
+ throw new TargetProtocolError('Target returned a pagination loop');
143
+ return page;
140
144
  }
141
145
 
142
- async inspect(capsuleId: string, signal?: AbortSignal): Promise<Capsule> {
143
- return this._withRetry(async () => {
144
- const resp = await this._request('GET', `/capsules/${encodeURIComponent(capsuleId)}`, {
145
- signal,
146
- });
147
- const json = await this._readJson(resp);
148
- return assertCapsule(json);
149
- }, signal);
146
+ inspect(capsuleId: string, signal?: AbortSignal): Promise<Capsule> {
147
+ validOpaque(capsuleId, 'capsule id');
148
+ return this.#execute(
149
+ 'inspect',
150
+ 'GET',
151
+ this.#descriptor.capsule.routes.inspect,
152
+ { org_id: this.#organizationId, capsule_id: capsuleId },
153
+ 200,
154
+ assertCapsule,
155
+ signal
156
+ );
150
157
  }
151
158
 
152
- async terminate(capsuleId: string, signal?: AbortSignal): Promise<void> {
153
- await this._withRetry(async () => {
154
- const resp = await this._request(
155
- 'DELETE',
156
- `/capsules/${encodeURIComponent(capsuleId)}`,
157
- { signal },
158
- );
159
- if (resp.status !== 204) {
160
- const json = await this._readJson(resp);
161
- throw new TargetProtocolError(`Unexpected terminate response: ${JSON.stringify(json)}`);
162
- }
163
- }, signal);
159
+ terminate(capsuleId: string, signal?: AbortSignal): Promise<Capsule> {
160
+ validOpaque(capsuleId, 'capsule id');
161
+ return this.#execute(
162
+ 'terminate',
163
+ 'DELETE',
164
+ this.#descriptor.capsule.routes.terminate,
165
+ { org_id: this.#organizationId, capsule_id: capsuleId },
166
+ 202,
167
+ assertCapsule,
168
+ signal
169
+ );
164
170
  }
165
171
 
166
- async limits(signal?: AbortSignal): Promise<CapsuleLimits> {
167
- return this._withRetry(async () => {
168
- const params = new URLSearchParams({ organization: this.organization });
169
- const resp = await this._request('GET', `/limits?${params.toString()}`, { signal });
170
- const json = await this._readJson(resp);
171
- return assertCapsuleLimits(json);
172
- }, signal);
172
+ limits(signal?: AbortSignal): Promise<CapsuleLimits> {
173
+ return this.#execute(
174
+ 'limits',
175
+ 'GET',
176
+ this.#descriptor.capsule.routes.limits,
177
+ { org_id: this.#organizationId },
178
+ 200,
179
+ assertCapsuleLimits,
180
+ signal
181
+ );
173
182
  }
174
183
 
175
184
  async access(capsuleId: string, signal?: AbortSignal): Promise<CapsuleAccess> {
176
- return this._withRetry(async () => {
177
- const resp = await this._request(
178
- 'POST',
179
- `/capsules/${encodeURIComponent(capsuleId)}/access`,
180
- { signal },
181
- );
182
- const json = await this._readJson(resp);
183
- return assertCapsuleAccess(json);
184
- }, signal);
185
+ validOpaque(capsuleId, 'capsule id');
186
+ const result = await this.#execute(
187
+ 'access',
188
+ 'POST',
189
+ this.#descriptor.capsule.routes.access,
190
+ { capsule_id: capsuleId },
191
+ 200,
192
+ assertCapsuleAccess,
193
+ signal,
194
+ {
195
+ body: jsonRequest({ protocol: 'openengine.cluster/v1' }),
196
+ }
197
+ );
198
+ validateAccessUrl(result.websocketUrl, capsuleId, this.#descriptor);
199
+ return result;
185
200
  }
186
201
 
187
- private async _request(
188
- method: string,
189
- path: string,
190
- opts: { signal?: AbortSignal | undefined; body?: string | undefined; headers?: Record<string, string> | undefined },
191
- ): Promise<Response> {
192
- const url = `${this.discovery.capsuleV1}${path}`;
193
-
194
- const responseOrigin = originOf(url);
195
- if (responseOrigin !== this.expectedOrigin) {
196
- throw new TargetProtocolError(
197
- `Origin mismatch: expected ${this.expectedOrigin}, got ${responseOrigin}`,
198
- );
199
- }
200
-
201
- let token: string;
202
- try {
203
- token = await this.tokenProvider.getAccessToken(opts.signal);
204
- } catch (err) {
205
- throw new TargetTransportError('Failed to acquire access token', err);
206
- }
207
-
208
- const headers: Record<string, string> = {
209
- 'Authorization': `Bearer ${token}`,
210
- 'Content-Type': 'application/json',
211
- 'Accept': 'application/json',
212
- ...opts.headers,
213
- };
202
+ #execute<T>(...args: ExecuteArguments<T>): Promise<T> {
203
+ return Promise.resolve().then(() => this.#executeExpanded(args));
204
+ }
214
205
 
215
- let response: Response;
216
- const fetchInit: RequestInit & { redirect: 'error' } = {
206
+ #executeExpanded<T>(args: ExecuteArguments<T>): Promise<T> {
207
+ const [
208
+ operation,
217
209
  method,
218
- headers,
219
- redirect: 'error',
220
- };
221
- if (opts.body !== undefined) fetchInit.body = opts.body;
222
- if (opts.signal !== undefined) fetchInit.signal = opts.signal;
223
-
210
+ template,
211
+ values,
212
+ expectedStatus,
213
+ validate,
214
+ signal,
215
+ request = {},
216
+ ] = args;
217
+ let path: string;
224
218
  try {
225
- response = await this.transport.fetch(url, fetchInit);
226
- } catch (err) {
227
- if (err instanceof TargetAdapterError) throw err;
228
- const msg = err instanceof Error ? err.message : String(err);
229
- if (msg.includes('redirect')) {
230
- throw new TargetProtocolError(`Redirect rejected for ${method} ${path}`, err);
231
- }
232
- throw new TargetTransportError(`Network error during ${method} ${path}`, err);
233
- }
234
-
235
- if (response.status < 200 || response.status >= 300) {
236
- await this._mapStatusError(response, method, path, opts.headers);
219
+ path = template.expand(values);
220
+ } catch {
221
+ throw new TargetProtocolError('Capsule route expansion is unsafe');
237
222
  }
238
-
239
- return response;
223
+ return withTargetRetry(
224
+ operation,
225
+ async () => {
226
+ const response = await this.#request(method, path, signal, request);
227
+ if (response.status >= 300 && response.status < 400) {
228
+ await response.body?.cancel().catch(() => undefined);
229
+ throw new TargetProtocolError('Capsule redirects are forbidden');
230
+ }
231
+ if (response.status !== expectedStatus) {
232
+ if (response.status >= 200 && response.status < 300) {
233
+ await response.body?.cancel().catch(() => undefined);
234
+ throw new TargetProtocolError('Target returned an unexpected success status');
235
+ }
236
+ await throwCapsuleServerError(
237
+ response,
238
+ (errorResponse) => this.#readJson(errorResponse),
239
+ this.#clock,
240
+ );
241
+ }
242
+ return validate(await this.#readJson(response));
243
+ },
244
+ signal,
245
+ { clock: this.#clock, policy: this.#retryPolicy },
246
+ );
240
247
  }
241
248
 
242
- private async _mapStatusError(
243
- response: Response,
249
+ async #request(
244
250
  method: string,
245
251
  path: string,
246
- headers?: Record<string, string>,
247
- ): Promise<never> {
248
- const status = response.status;
249
- const errorBody = await this._readErrorBody(response);
250
- const context = `${status} ${method} ${path}: ${errorBody}`;
251
-
252
- if (status === 401 || status === 403) throw new TargetAuthError(context);
253
- if (status === 404) throw new TargetNotFoundError(context);
254
- if (status === 409) {
255
- const idempotencyKey = headers?.['Idempotency-Key'] ?? 'unknown';
256
- throw new TargetConflictError(idempotencyKey, context);
257
- }
258
- if (status === 429) {
259
- const retryAfterMs = parseRetryAfter(response.headers.get('Retry-After'), this.clock);
260
- throw new TargetRateLimitError(context, retryAfterMs ?? undefined);
261
- }
262
- if (status >= 500) throw new TargetTransportError(context);
263
- throw new TargetProtocolError(`Unexpected status ${context}`);
264
- }
265
-
266
- private async _readJson(response: Response): Promise<unknown> {
267
- const contentLength = response.headers.get('Content-Length');
268
- if (contentLength !== null) {
269
- const len = parseInt(contentLength, 10);
270
- if (Number.isFinite(len) && len > MAX_RESPONSE_BYTES) {
271
- throw new TargetProtocolError(
272
- `Response too large: ${len} bytes exceeds limit of ${MAX_RESPONSE_BYTES}`,
273
- );
274
- }
275
- }
276
-
277
- let text: string;
278
- try {
279
- const reader = response.body?.getReader();
280
- if (!reader) {
281
- text = await response.text();
282
- } else {
283
- const chunks: Uint8Array[] = [];
284
- let totalBytes = 0;
285
- while (true) {
286
- const { done, value } = await reader.read();
287
- if (done) break;
288
- totalBytes += value.byteLength;
289
- if (totalBytes > MAX_RESPONSE_BYTES) {
290
- reader.cancel();
291
- throw new TargetProtocolError(
292
- `Response body exceeds limit of ${MAX_RESPONSE_BYTES} bytes`,
293
- );
294
- }
295
- chunks.push(value);
296
- }
297
- const combined = new Uint8Array(totalBytes);
298
- let offset = 0;
299
- for (const chunk of chunks) {
300
- combined.set(chunk, offset);
301
- offset += chunk.byteLength;
302
- }
303
- text = new TextDecoder().decode(combined);
304
- }
305
- } catch (err) {
306
- if (err instanceof TargetProtocolError) throw err;
307
- throw new TargetProtocolError('Failed to read response body', err);
308
- }
309
-
252
+ signal: AbortSignal | undefined,
253
+ request: AdapterRequest,
254
+ ): Promise<Response> {
255
+ throwIfAborted(signal);
256
+ const url = requestUrl(path, this.#descriptor);
257
+ let token: string;
310
258
  try {
311
- return JSON.parse(text);
312
- } catch (err) {
313
- throw new TargetProtocolError('Invalid JSON in response body', err);
259
+ token = await this.#tokenProvider.getAccessToken(signal);
260
+ } catch {
261
+ throw new TargetAuthError('Target access authorization failed');
314
262
  }
315
- }
316
-
317
- private async _readErrorBody(response: Response): Promise<string> {
318
- const reader = response.body?.getReader();
319
- if (!reader) return '';
320
-
321
- const chunks: Uint8Array[] = [];
322
- let totalBytes = 0;
263
+ const init: RequestInit & { redirect: 'manual' } = {
264
+ method,
265
+ headers: {
266
+ Accept: 'application/json',
267
+ Authorization: `Bearer ${token}`,
268
+ ...(request.body === undefined ? {} : { 'Content-Type': 'application/json' }),
269
+ ...request.headers,
270
+ },
271
+ redirect: 'manual',
272
+ };
273
+ if (request.body !== undefined) init.body = request.body;
274
+ if (signal !== undefined) init.signal = signal;
323
275
  try {
324
- while (totalBytes < MAX_ERROR_BODY_BYTES) {
325
- const { done, value } = await reader.read();
326
- if (done) break;
327
-
328
- const remaining = MAX_ERROR_BODY_BYTES - totalBytes;
329
- const retained = value.byteLength > remaining ? value.slice(0, remaining) : value;
330
- chunks.push(retained);
331
- totalBytes += retained.byteLength;
332
- if (retained.byteLength !== value.byteLength || totalBytes === MAX_ERROR_BODY_BYTES) {
333
- void reader.cancel().catch(() => undefined);
334
- break;
335
- }
276
+ const response = await this.#transport.fetch(url.href, init);
277
+ if (response.url && new globalThis.URL(response.url).href !== url.href) {
278
+ await response.body?.cancel().catch(() => undefined);
279
+ throw new TargetProtocolError('Capsule response changed target route');
336
280
  }
337
- } catch {
338
- void reader.cancel().catch(() => undefined);
339
- return '<unreadable>';
340
- }
341
-
342
- const combined = new Uint8Array(totalBytes);
343
- let offset = 0;
344
- for (const chunk of chunks) {
345
- combined.set(chunk, offset);
346
- offset += chunk.byteLength;
281
+ return response;
282
+ } catch (error) {
283
+ if (error instanceof TargetAdapterError) throw error;
284
+ throwIfAborted(signal);
285
+ throw new TargetTransportError('Capsule transport failed');
347
286
  }
348
- return new TextDecoder().decode(combined);
349
287
  }
350
288
 
351
- private async _withRetry<T>(
352
- fn: () => Promise<T>,
353
- signal?: AbortSignal,
354
- ): Promise<T> {
355
- const startTime = this.clock.now();
356
- let attempt = 0;
357
-
358
- while (true) {
359
- throwIfAborted(signal);
360
- try {
361
- return await fn();
362
- } catch (err) {
363
- throwIfAborted(signal);
364
- if (!(err instanceof TargetAdapterError)) throw err;
365
- if (err instanceof TargetAuthError) throw err;
366
-
367
- attempt++;
368
- const elapsed = this.clock.now() - startTime;
369
- const decision = this.retryPolicy.shouldRetry(attempt, elapsed, err);
370
- const remaining = MAX_RETRY_ELAPSED_MS - elapsed;
371
-
372
- if (
373
- !decision.retry ||
374
- !Number.isFinite(decision.delayMs) ||
375
- decision.delayMs < 0 ||
376
- decision.delayMs >= remaining
377
- ) {
378
- throw err;
379
- }
380
289
 
381
- await waitForRetryDelay(decision.delayMs, signal);
382
- if (this.clock.now() - startTime >= MAX_RETRY_ELAPSED_MS) throw err;
383
- }
384
- }
290
+ #readJson(response: Response): Promise<unknown> {
291
+ return readBoundedResponseJson(response, MAX_RESPONSE_BYTES, (kind) =>
292
+ new TargetProtocolError(
293
+ kind === 'size'
294
+ ? 'Capsule response exceeds the size limit'
295
+ : 'Capsule response is not valid UTF-8 JSON',
296
+ ),
297
+ );
385
298
  }
386
299
  }