@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,26 +1,48 @@
1
- import { ClusterConfigError, connectInitialized } from '../cluster/index.js';
2
- import type { ServerCapabilities, GraphProfile } from '../cluster/index.js';
3
- import type { ConnectOptions } from '../cluster/index.js';
4
- import type { AccessResponse, HostedSessionInit, InitializedSession } from './types.js';
1
+ import {
2
+ ClusterConfigError,
3
+ ClusterUpgradeError,
4
+ connectInitialized,
5
+ type ServerCapabilities,
6
+ type GraphProfile,
7
+ } from '../cluster/index.js';
8
+ import type {
9
+ HostedAccess,
10
+ HostedSessionInit,
11
+ HostedWatch,
12
+ HostedWatchOptions,
13
+ InitializedSession,
14
+ } from './types.js';
15
+ import { normalizedAuthority, validateHostedAccess } from './authority.js';
16
+ import { HostedAuthenticationError } from './errors.js';
17
+ import { ReconnectingHostedWatch } from './reconnecting-watch.js';
18
+ export {
19
+ HostedAuthenticationError,
20
+ HostedAuthorizationError,
21
+ HostedTransportUncertainError,
22
+ } from './errors.js';
23
+
5
24
 
6
25
  function combineSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
7
- const defined = signals.filter((s): s is AbortSignal => s !== undefined);
26
+ const defined = signals.filter((signal): signal is AbortSignal => signal !== undefined);
8
27
  if (defined.length === 0) return undefined;
9
28
  if (defined.length === 1) return defined[0];
10
29
  return AbortSignal.any(defined);
11
30
  }
12
31
 
13
32
  export class HostedSessionCoordinator {
14
- readonly #getAccess: (signal?: AbortSignal) => Promise<AccessResponse>;
15
- readonly #connectOptions: Omit<ConnectOptions, 'headers' | 'signal'> | undefined;
33
+ readonly #init: HostedSessionInit;
34
+ readonly #targetAuthority: string;
16
35
  readonly #clock: { now(): number };
17
36
  readonly #closeController = new AbortController();
37
+ readonly #sessions = new Set<InitializedSession>();
18
38
  #referenceCapabilities: ServerCapabilities | undefined;
19
39
  #closed = false;
20
40
 
21
41
  constructor(init: HostedSessionInit) {
22
- this.#getAccess = init.getAccess;
23
- this.#connectOptions = init.connectOptions;
42
+ if (init.capsuleId.length === 0)
43
+ throw new ClusterConfigError('capsuleId must not be empty', 'INVALID_CAPSULE_ID');
44
+ this.#init = init;
45
+ this.#targetAuthority = normalizedAuthority(init.targetAuthority);
24
46
  this.#clock = init.clock ?? Date;
25
47
  }
26
48
 
@@ -34,72 +56,107 @@ export class HostedSessionCoordinator {
34
56
  async replace(signal?: AbortSignal): Promise<InitializedSession> {
35
57
  this.#requireNotClosed();
36
58
  const session = await this.#createSession(signal);
37
- this.#verifyCapabilities(session.initializeResult.capabilities, session);
38
- return session;
59
+ try {
60
+ if (this.#referenceCapabilities !== undefined) {
61
+ this.#verifyCapabilities(
62
+ this.#referenceCapabilities,
63
+ session.initializeResult.capabilities
64
+ );
65
+ }
66
+ return session;
67
+ } catch (error) {
68
+ await session.connection.close();
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ async watch(options: HostedWatchOptions): Promise<HostedWatch> {
74
+ const session = await this.open(options.signal);
75
+ try {
76
+ const subscription = await session.client.watch(
77
+ options.params,
78
+ options.signal === undefined ? {} : { signal: options.signal }
79
+ );
80
+ const hosted = new ReconnectingHostedWatch(
81
+ (replacementSignal) => this.replace(replacementSignal),
82
+ session,
83
+ subscription,
84
+ options.signal,
85
+ );
86
+ options.signal?.addEventListener(
87
+ 'abort',
88
+ () => {
89
+ void hosted.cancel().catch(() => undefined);
90
+ },
91
+ { once: true }
92
+ );
93
+ if (options.signal?.aborted) {
94
+ await hosted.cancel();
95
+ throw options.signal.reason ?? new DOMException('hosted watch aborted', 'AbortError');
96
+ }
97
+ return hosted;
98
+ } catch (error) {
99
+ await session.connection.close();
100
+ throw error;
101
+ }
39
102
  }
40
103
 
41
- renewalDeadline(access: AccessResponse, receivedAt: number): number {
104
+ renewalDeadline(access: HostedAccess, receivedAt: number): number {
42
105
  const expiresAt = Date.parse(access.expiresAt);
43
- if (Number.isNaN(expiresAt)) {
44
- throw new ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
106
+ if (!Number.isFinite(expiresAt) || expiresAt <= receivedAt) {
107
+ throw new ClusterConfigError(
108
+ 'access expiry must be a future RFC 3339 timestamp',
109
+ 'INVALID_ACCESS_EXPIRY'
110
+ );
45
111
  }
46
112
  const lifetime = expiresAt - receivedAt;
47
- return Math.min(expiresAt - 30_000, receivedAt + 0.8 * lifetime);
113
+ const lead = Math.max(5_000, Math.min(60_000, Math.floor(lifetime * 0.1)));
114
+ return Math.max(receivedAt, expiresAt - lead);
48
115
  }
49
116
 
50
117
  async close(): Promise<void> {
118
+ if (this.#closed) return;
51
119
  this.#closed = true;
52
- this.#closeController.abort();
120
+ this.#closeController.abort(new DOMException('coordinator closed', 'AbortError'));
121
+ const sessions = [...this.#sessions];
122
+ await Promise.all(sessions.map((session) => session.connection.close()));
123
+ this.#sessions.clear();
53
124
  }
54
125
 
55
126
  async #createSession(signal?: AbortSignal): Promise<InitializedSession> {
56
127
  const combined = combineSignals([signal, this.#closeController.signal]);
57
- const access = await this.#getAccess(combined);
58
- const expiresAt = Date.parse(access.expiresAt);
59
- if (Number.isNaN(expiresAt)) {
60
- throw new ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
61
- }
62
- if (expiresAt <= this.#clock.now()) {
63
- throw new ClusterConfigError('access token is already expired', 'ACCESS_EXPIRED');
64
- }
65
-
66
- let endpoint: URL;
128
+ const access = await this.#init.adapter.access(this.#init.capsuleId, combined);
129
+ validateHostedAccess(access, this.#targetAuthority);
130
+ const receivedAt = this.#clock.now();
131
+ this.renewalDeadline(access, receivedAt);
67
132
  try {
68
- endpoint = new URL(access.endpoint);
69
- } catch {
70
- throw new ClusterConfigError('hosted access endpoint is invalid', 'INVALID_ENDPOINT');
71
- }
72
- if (endpoint.protocol !== 'wss:') {
73
- throw new ClusterConfigError('hosted access endpoint must use wss', 'INSECURE_ENDPOINT');
133
+ const session = await connectInitialized(access.websocketUrl, {
134
+ ...this.#init.connectOptions,
135
+ headers: { Authorization: `Bearer ${access.accessToken}` },
136
+ ...(combined === undefined ? {} : { signal: combined }),
137
+ });
138
+ this.#sessions.add(session);
139
+ void session.connection.closed.then(() => this.#sessions.delete(session));
140
+ return session;
141
+ } catch (error) {
142
+ if (error instanceof ClusterUpgradeError && error.status === 401)
143
+ throw new HostedAuthenticationError();
144
+ throw error;
74
145
  }
75
-
76
- return connectInitialized(endpoint.href, {
77
- ...this.#connectOptions,
78
- headers: { Authorization: `Bearer ${access.token}` },
79
- ...(combined !== undefined ? { signal: combined } : {}),
80
- });
81
146
  }
82
147
 
83
- #verifyCapabilities(incoming: ServerCapabilities, session: InitializedSession): void {
84
- if (!this.#referenceCapabilities) return;
85
- const ref = this.#referenceCapabilities;
86
- const mismatches: string[] = [];
87
-
88
- if (ref.graphProfiles) {
89
- const incomingProfiles = new Set<GraphProfile>(incoming.graphProfiles ?? []);
90
- for (const profile of ref.graphProfiles) {
91
- if (!incomingProfiles.has(profile)) mismatches.push(`missing graphProfile: ${profile}`);
92
- }
93
- }
94
- if (ref.logs && !incoming.logs) mismatches.push('missing capability: logs');
95
- if (ref.agentAttach && !incoming.agentAttach)
96
- mismatches.push('missing capability: agentAttach');
97
148
 
98
- if (mismatches.length > 0) {
99
- void session.connection.close();
149
+ #verifyCapabilities(reference: ServerCapabilities, incoming: ServerCapabilities): void {
150
+ const referenceProfiles = new Set<GraphProfile>(reference.graphProfiles ?? []);
151
+ const incomingProfiles = new Set<GraphProfile>(incoming.graphProfiles ?? []);
152
+ if (
153
+ (reference.logs && !incoming.logs) ||
154
+ (reference.agentAttach && !incoming.agentAttach) ||
155
+ [...referenceProfiles].some((profile) => !incomingProfiles.has(profile))
156
+ ) {
100
157
  throw new ClusterConfigError(
101
- `replacement capabilities incompatible: ${mismatches.join(', ')}`,
102
- 'INCOMPATIBLE_CAPABILITIES'
158
+ 'replacement connection removed required server capabilities',
159
+ 'CAPABILITY_REGRESSION'
103
160
  );
104
161
  }
105
162
  }
@@ -0,0 +1,21 @@
1
+ export class HostedAuthenticationError extends Error {
2
+ constructor() {
3
+ super('Hosted target authentication failed');
4
+ this.name = 'HostedAuthenticationError';
5
+ }
6
+ }
7
+
8
+ export class HostedAuthorizationError extends Error {
9
+ constructor() {
10
+ super('Hosted target authorization was revoked');
11
+ this.name = 'HostedAuthorizationError';
12
+ }
13
+ }
14
+
15
+ export class HostedTransportUncertainError extends Error {
16
+ readonly executionRetryAuthorized = false;
17
+ constructor() {
18
+ super('Hosted session transport closed with uncertain execution state');
19
+ this.name = 'HostedTransportUncertainError';
20
+ }
21
+ }
@@ -1,2 +1,14 @@
1
- export { HostedSessionCoordinator } from './coordinator.js';
2
- export type { AccessResponse, HostedSessionInit, InitializedSession } from './types.js';
1
+ export {
2
+ HostedSessionCoordinator,
3
+ HostedAuthenticationError,
4
+ HostedAuthorizationError,
5
+ HostedTransportUncertainError,
6
+ } from './coordinator.js';
7
+ export type {
8
+ HostedAccess,
9
+ HostedAccessAdapter,
10
+ HostedSessionInit,
11
+ HostedWatch,
12
+ HostedWatchOptions,
13
+ InitializedSession,
14
+ } from './types.js';
@@ -0,0 +1,148 @@
1
+ import type {
2
+ WatchSubscription,
3
+ WatchSubscriptionItem,
4
+ } from '../cluster/index.js';
5
+ import {
6
+ HostedAuthorizationError,
7
+ HostedTransportUncertainError,
8
+ } from './errors.js';
9
+ import type { HostedWatch, InitializedSession } from './types.js';
10
+
11
+ type ReplaceSession = (signal: AbortSignal) => Promise<InitializedSession>;
12
+
13
+ export class ReconnectingHostedWatch implements HostedWatch {
14
+ readonly #replace: ReplaceSession;
15
+ readonly #ownedController = new AbortController();
16
+ readonly #signal: AbortSignal;
17
+ #session: InitializedSession;
18
+ #subscription: WatchSubscription;
19
+ #tail: Promise<void> = Promise.resolve();
20
+ #replacementPromise: Promise<InitializedSession> | undefined;
21
+ #cancelPromise: Promise<void> | undefined;
22
+ #reconnected = false;
23
+ #cancelled = false;
24
+
25
+ constructor(
26
+ replace: ReplaceSession,
27
+ session: InitializedSession,
28
+ subscription: WatchSubscription,
29
+ signal?: AbortSignal,
30
+ ) {
31
+ this.#replace = replace;
32
+ this.#session = session;
33
+ this.#subscription = subscription;
34
+ this.#signal = signal === undefined
35
+ ? this.#ownedController.signal
36
+ : AbortSignal.any([signal, this.#ownedController.signal]);
37
+ }
38
+
39
+ [Symbol.asyncIterator](): this {
40
+ return this;
41
+ }
42
+
43
+ next(): Promise<IteratorResult<WatchSubscriptionItem>> {
44
+ const result = this.#tail.then(() => this.#readNext());
45
+ this.#tail = result.then(() => undefined, () => undefined);
46
+ return result;
47
+ }
48
+
49
+ async #readNext(): Promise<IteratorResult<WatchSubscriptionItem>> {
50
+ while (!this.#cancelled) {
51
+ const item = await this.#subscription.stream.next();
52
+ if (this.#cancelled) return { done: true, value: undefined };
53
+ if (!item.done) return item;
54
+ return this.#handleClosed(item);
55
+ }
56
+ return { done: true, value: undefined };
57
+ }
58
+
59
+ async #handleClosed(
60
+ item: IteratorResult<WatchSubscriptionItem>,
61
+ ): Promise<IteratorResult<WatchSubscriptionItem>> {
62
+ if (this.#session.connection.state === 'OPEN') {
63
+ await this.#session.connection.close();
64
+ return item;
65
+ }
66
+ const { code } = await this.#session.connection.closed;
67
+ if (code === 4401) return this.#reconnect();
68
+ if (code === 4403) throw new HostedAuthorizationError();
69
+ if (code === 1000) return item;
70
+ throw new HostedTransportUncertainError();
71
+ }
72
+
73
+ async #reconnect(): Promise<IteratorResult<WatchSubscriptionItem>> {
74
+ if (this.#reconnected) throw new HostedTransportUncertainError();
75
+ this.#reconnected = true;
76
+ const replacementPromise = this.#replace(this.#signal);
77
+ this.#replacementPromise = replacementPromise;
78
+ let replacement: InitializedSession;
79
+ try {
80
+ replacement = await replacementPromise;
81
+ } catch (error) {
82
+ if (this.#cancelled) return { done: true, value: undefined };
83
+ throw error;
84
+ } finally {
85
+ if (this.#replacementPromise === replacementPromise) {
86
+ this.#replacementPromise = undefined;
87
+ }
88
+ }
89
+ return this.#installReplacement(replacement);
90
+ }
91
+
92
+ async #installReplacement(
93
+ replacement: InitializedSession,
94
+ ): Promise<IteratorResult<WatchSubscriptionItem>> {
95
+ if (this.#cancelled) {
96
+ await replacement.connection.close();
97
+ return { done: true, value: undefined };
98
+ }
99
+ try {
100
+ const subscription = await this.#subscription.stream.reconnect(replacement.connection);
101
+ if (this.#cancelled) {
102
+ await subscription.stream.cancel();
103
+ await replacement.connection.close();
104
+ return { done: true, value: undefined };
105
+ }
106
+ this.#subscription = subscription;
107
+ this.#session = replacement;
108
+ return this.#readNext();
109
+ } catch (error) {
110
+ await replacement.connection.close();
111
+ if (this.#cancelled) return { done: true, value: undefined };
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ async return(): Promise<IteratorResult<WatchSubscriptionItem>> {
117
+ await this.cancel();
118
+ return { done: true, value: undefined };
119
+ }
120
+
121
+ async throw(error?: unknown): Promise<IteratorResult<WatchSubscriptionItem>> {
122
+ await this.cancel();
123
+ throw error;
124
+ }
125
+
126
+ cancel(): Promise<void> {
127
+ this.#cancelPromise ??= this.#cancelOwned();
128
+ return this.#cancelPromise;
129
+ }
130
+
131
+ async #cancelOwned(): Promise<void> {
132
+ this.#cancelled = true;
133
+ this.#ownedController.abort(new DOMException('hosted watch cancelled', 'AbortError'));
134
+ const replacement = this.#replacementPromise;
135
+ const outcomes = await Promise.allSettled([
136
+ this.#subscription.stream.cancel(),
137
+ this.#session.connection.close(),
138
+ ...(replacement === undefined
139
+ ? []
140
+ : [replacement.then(() => undefined, () => undefined)]),
141
+ ]);
142
+ await this.#tail;
143
+ const failure = outcomes.find(
144
+ (outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected',
145
+ );
146
+ if (failure !== undefined) throw failure.reason;
147
+ }
148
+ }
@@ -1,15 +1,20 @@
1
- import type { Connection } from '../cluster/index.js';
2
- import type { ClusterClient, ConnectOptions } from '../cluster/index.js';
3
- import type { InitializeResult } from '../cluster/index.js';
4
-
5
- export interface AccessResponse {
6
- readonly endpoint: string;
7
- readonly token: string;
1
+ import type { Connection, ClusterClient, ConnectOptions, InitializeResult, WatchParams, WatchSubscriptionItem } from '../cluster/index.js';
2
+ export interface HostedAccess {
3
+ readonly protocol: 'openengine.cluster/v1';
4
+ readonly websocketUrl: string;
5
+ readonly accessToken: string;
6
+ readonly tokenType: 'Bearer';
8
7
  readonly expiresAt: string;
9
8
  }
10
9
 
10
+ export interface HostedAccessAdapter {
11
+ access(capsuleId: string, signal?: AbortSignal): Promise<HostedAccess>;
12
+ }
13
+
11
14
  export interface HostedSessionInit {
12
- readonly getAccess: (signal?: AbortSignal) => Promise<AccessResponse>;
15
+ readonly adapter: HostedAccessAdapter;
16
+ readonly capsuleId: string;
17
+ readonly targetAuthority: string;
13
18
  readonly connectOptions?: Omit<ConnectOptions, 'headers' | 'signal'>;
14
19
  readonly clock?: { now(): number };
15
20
  }
@@ -19,3 +24,12 @@ export interface InitializedSession {
19
24
  readonly client: ClusterClient;
20
25
  readonly initializeResult: InitializeResult;
21
26
  }
27
+
28
+ export interface HostedWatch extends AsyncIterator<WatchSubscriptionItem>, AsyncIterable<WatchSubscriptionItem> {
29
+ cancel(): Promise<void>;
30
+ }
31
+
32
+ export interface HostedWatchOptions {
33
+ readonly params: WatchParams;
34
+ readonly signal?: AbortSignal;
35
+ }
@@ -0,0 +1,46 @@
1
+ import type { TargetDiscoveryDescriptor } from '../target/discovery.js';
2
+ import { TargetProtocolError } from './errors.js';
3
+
4
+ function hasForbiddenCodePoint(value: string): boolean {
5
+ for (const character of value) {
6
+ const codePoint = character.codePointAt(0)!;
7
+ if (codePoint <= 0x20 || codePoint === 0x7f) return true;
8
+ }
9
+ return false;
10
+ }
11
+
12
+ function parseAccessUrl(value: string): URL {
13
+ try {
14
+ if (hasForbiddenCodePoint(value)) throw new TypeError('unsafe URL code point');
15
+ const url = new globalThis.URL(value);
16
+ if (url.href !== value) throw new TypeError('non-canonical URL');
17
+ return url;
18
+ } catch {
19
+ throw new TargetProtocolError('Capsule access WebSocket URL is invalid');
20
+ }
21
+ }
22
+
23
+ function matchesAccessRoute(url: URL, target: URL, expectedPath: string): boolean {
24
+ const expectedProtocol = target.protocol === 'http:' ? 'ws:' : 'wss:';
25
+ return url.protocol === expectedProtocol &&
26
+ url.host === target.host &&
27
+ !url.username &&
28
+ !url.password &&
29
+ !url.search &&
30
+ !url.hash &&
31
+ url.pathname === expectedPath;
32
+ }
33
+
34
+ export function validateAccessUrl(
35
+ value: string,
36
+ capsuleId: string,
37
+ descriptor: TargetDiscoveryDescriptor,
38
+ ): void {
39
+ const url = parseAccessUrl(value);
40
+ const expectedPath = descriptor.transport.websocketRouteTemplate.expand({
41
+ capsule_id: capsuleId,
42
+ });
43
+ if (!matchesAccessRoute(url, new globalThis.URL(descriptor.origin), expectedPath)) {
44
+ throw new TargetProtocolError('Capsule access WebSocket URL does not match discovery');
45
+ }
46
+ }
@@ -0,0 +1,28 @@
1
+ import type { RouteTemplate, TargetDiscoveryDescriptor } from '../target/discovery.js';
2
+ import { TargetProtocolError } from './errors.js';
3
+ import type { TargetOperation } from './retry-executor.js';
4
+
5
+ export type AdapterRequest = {
6
+ readonly body?: string;
7
+ readonly headers?: Readonly<Record<string, string>>;
8
+ };
9
+
10
+ export type ExecuteArguments<T> = [
11
+ operation: TargetOperation,
12
+ method: string,
13
+ template: RouteTemplate,
14
+ values: Readonly<Record<string, string | number | undefined>>,
15
+ expectedStatus: number,
16
+ validate: (body: unknown) => T,
17
+ signal?: AbortSignal,
18
+ request?: AdapterRequest,
19
+ ];
20
+
21
+ export function requestUrl(path: string, descriptor: TargetDiscoveryDescriptor): URL {
22
+ const url = new globalThis.URL(path, descriptor.capsule.baseUrl);
23
+ if (url.origin !== descriptor.origin || url.hash ||
24
+ `${url.pathname}${url.search}` !== path) {
25
+ throw new TargetProtocolError('Capsule route changed during URL canonicalization');
26
+ }
27
+ return url;
28
+ }
@@ -0,0 +1,39 @@
1
+ import type { TargetDiscoveryDescriptor } from '../target/discovery.js';
2
+ import type {
3
+ AllocateRequest,
4
+ Capsule,
5
+ CapsuleAccess,
6
+ CapsuleLimits,
7
+ CapsuleListPage,
8
+ Clock,
9
+ HttpTransport,
10
+ ListRequest,
11
+ RetryPolicy,
12
+ TargetAccessTokenProvider,
13
+ } from './types.js';
14
+
15
+ export type CredentialInstallCapability =
16
+ | { readonly supported: false }
17
+ | {
18
+ readonly supported: true;
19
+ readonly descriptor: NonNullable<TargetDiscoveryDescriptor['credentialInstall']>;
20
+ };
21
+
22
+ export interface TargetAdapter {
23
+ allocate(req: AllocateRequest, signal?: AbortSignal): Promise<Capsule>;
24
+ list(req?: ListRequest, signal?: AbortSignal): Promise<CapsuleListPage>;
25
+ inspect(capsuleId: string, signal?: AbortSignal): Promise<Capsule>;
26
+ terminate(capsuleId: string, signal?: AbortSignal): Promise<Capsule>;
27
+ limits(signal?: AbortSignal): Promise<CapsuleLimits>;
28
+ access(capsuleId: string, signal?: AbortSignal): Promise<CapsuleAccess>;
29
+ readonly credentialInstall: CredentialInstallCapability;
30
+ }
31
+
32
+ export interface CreateTargetAdapterOptions {
33
+ readonly descriptor: TargetDiscoveryDescriptor;
34
+ readonly organization: { readonly id: string };
35
+ readonly tokenProvider: TargetAccessTokenProvider;
36
+ readonly transport?: HttpTransport;
37
+ readonly clock?: Clock;
38
+ readonly retryPolicy?: RetryPolicy;
39
+ }
@@ -1,3 +1,4 @@
1
+ /** Shared wire limits for every hosted-target response and retry path. */
1
2
  export const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
2
3
  export const MAX_PAGINATION_PAGES = 100;
3
4
  export const MAX_RETRY_ATTEMPTS = 3;
@@ -0,0 +1,107 @@
1
+ import { TargetProtocolError, TargetServerError } from './errors.js';
2
+ import { parseRetryAfter } from './retry.js';
3
+ import type { Clock } from './types.js';
4
+
5
+ const ERROR_CODES = Object.freeze({
6
+ unauthorized: { statuses: [401], retryable: false },
7
+ invalid_request: { statuses: [400], retryable: false },
8
+ not_found: { statuses: [404], retryable: false },
9
+ forbidden: { statuses: [402, 403], retryable: false },
10
+ idempotency_conflict: { statuses: [409], retryable: false },
11
+ run_conflict: { statuses: [409], retryable: true },
12
+ rate_limited: { statuses: [429], retryable: true },
13
+ temporarily_unavailable: { statuses: [503], retryable: true },
14
+ internal_error: { statuses: [500], retryable: false },
15
+ });
16
+ type ErrorCode = keyof typeof ERROR_CODES;
17
+
18
+ type CapsuleErrorBody = {
19
+ readonly code: ErrorCode;
20
+ readonly capsuleId: string | null;
21
+ readonly retryable: boolean;
22
+ };
23
+
24
+ function isErrorCode(value: unknown): value is ErrorCode {
25
+ return typeof value === 'string' && value in ERROR_CODES;
26
+ }
27
+
28
+ function isRecordBody(body: unknown): body is Record<string, unknown> {
29
+ return body !== null && typeof body === 'object' && !Array.isArray(body);
30
+ }
31
+
32
+ function hasExactErrorFields(value: Record<string, unknown>): boolean {
33
+ const fields = Object.keys(value);
34
+ const exactFields = ['code', 'message', 'capsule_id', 'retryable'];
35
+ return fields.length === exactFields.length &&
36
+ exactFields.every((field) => field in value);
37
+ }
38
+
39
+ function hasValidErrorValues(value: Record<string, unknown>): value is Record<string, unknown> & {
40
+ readonly code: ErrorCode;
41
+ readonly capsule_id: string | null;
42
+ readonly retryable: boolean;
43
+ } {
44
+ return isErrorCode(value.code) &&
45
+ typeof value.message === 'string' &&
46
+ (value.capsule_id === null || typeof value.capsule_id === 'string') &&
47
+ typeof value.retryable === 'boolean';
48
+ }
49
+
50
+ function parseErrorBody(body: unknown): CapsuleErrorBody {
51
+ if (!isRecordBody(body) || !hasExactErrorFields(body) || !hasValidErrorValues(body)) {
52
+ throw new TargetProtocolError('Capsule error response is malformed');
53
+ }
54
+ const value = body;
55
+ return {
56
+ code: value.code,
57
+ capsuleId: value.capsule_id,
58
+ retryable: value.retryable,
59
+ };
60
+ }
61
+
62
+ function validateChallenge(response: Response): void {
63
+ if (response.status !== 401) return;
64
+ if (response.headers.get('WWW-Authenticate') !== 'Bearer error="invalid_token"') {
65
+ throw new TargetProtocolError('Capsule authentication challenge is malformed');
66
+ }
67
+ }
68
+
69
+ function validatedRetryAfter(
70
+ response: Response,
71
+ retryable: boolean,
72
+ clock: Clock,
73
+ ): number | undefined {
74
+ const header = response.headers.get('Retry-After');
75
+ const retryAfter = parseRetryAfter(header, clock) ?? undefined;
76
+ if (header !== null && retryAfter === undefined) {
77
+ throw new TargetProtocolError('Capsule Retry-After header is malformed');
78
+ }
79
+ if (retryable && retryAfter === undefined) {
80
+ throw new TargetProtocolError('Retryable capsule error omitted Retry-After');
81
+ }
82
+ if (!retryable && retryAfter !== undefined) {
83
+ throw new TargetProtocolError('Permanent capsule error advertised Retry-After');
84
+ }
85
+ return retryAfter;
86
+ }
87
+
88
+ export async function throwCapsuleServerError(
89
+ response: Response,
90
+ readJson: (response: Response) => Promise<unknown>,
91
+ clock: Clock,
92
+ ): Promise<never> {
93
+ const value = parseErrorBody(await readJson(response));
94
+ const contract = ERROR_CODES[value.code];
95
+ if (!contract.statuses.includes(response.status) || contract.retryable !== value.retryable) {
96
+ throw new TargetProtocolError('Capsule error response contradicts its status contract');
97
+ }
98
+ validateChallenge(response);
99
+ const retryAfter = validatedRetryAfter(response, contract.retryable, clock);
100
+ throw new TargetServerError(
101
+ response.status,
102
+ value.code,
103
+ contract.retryable,
104
+ value.capsuleId,
105
+ retryAfter,
106
+ );
107
+ }