@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.
- package/README.md +3 -3
- package/cli/index.js +20 -224
- package/docker/zeroshot-cluster/Dockerfile +2 -3
- package/lib/cluster/client.cjs +30 -7
- package/lib/cluster/client.d.cts +52 -0
- package/lib/cluster/client.d.mts +52 -0
- package/lib/cluster/client.d.ts +4 -6
- package/lib/cluster/client.mjs +32 -9
- package/lib/cluster/connection-state.cjs +25 -0
- package/lib/cluster/connection-state.d.cts +13 -0
- package/lib/cluster/connection-state.d.mts +13 -0
- package/lib/cluster/connection-state.d.ts +13 -0
- package/lib/cluster/connection-state.mjs +21 -0
- package/lib/cluster/connection-types.cjs +2 -0
- package/lib/cluster/connection-types.d.cts +33 -0
- package/lib/cluster/connection-types.d.mts +33 -0
- package/lib/cluster/connection-types.d.ts +33 -0
- package/lib/cluster/connection-types.mjs +1 -0
- package/lib/cluster/connection.cjs +18 -46
- package/lib/cluster/connection.d.cts +26 -0
- package/lib/cluster/connection.d.mts +26 -0
- package/lib/cluster/connection.d.ts +7 -25
- package/lib/cluster/connection.mjs +12 -42
- package/lib/cluster/errors.cjs +9 -1
- package/lib/cluster/errors.d.cts +29 -0
- package/lib/cluster/errors.d.mts +29 -0
- package/lib/cluster/errors.d.ts +4 -0
- package/lib/cluster/errors.mjs +7 -0
- package/lib/cluster/frames.d.cts +12 -0
- package/lib/cluster/frames.d.mts +12 -0
- package/lib/cluster/generated/protocol-schema.d.cts +1 -0
- package/lib/cluster/generated/protocol-schema.d.mts +1 -0
- package/lib/cluster/generated/protocol.d.cts +781 -0
- package/lib/cluster/generated/protocol.d.mts +781 -0
- package/lib/cluster/index.cjs +2 -2
- package/lib/cluster/index.d.cts +13 -0
- package/lib/cluster/index.d.mts +13 -0
- package/lib/cluster/index.d.ts +3 -3
- package/lib/cluster/index.mjs +2 -2
- package/lib/cluster/json-source.cjs +1 -0
- package/lib/cluster/json-source.d.cts +5 -0
- package/lib/cluster/json-source.d.mts +5 -0
- package/lib/cluster/json-source.d.ts +1 -0
- package/lib/cluster/json-source.mjs +1 -0
- package/lib/cluster/payload-value.cjs +1 -0
- package/lib/cluster/payload-value.d.cts +8 -0
- package/lib/cluster/payload-value.d.mts +8 -0
- package/lib/cluster/payload-value.d.ts +1 -0
- package/lib/cluster/payload-value.mjs +1 -0
- package/lib/cluster/queue.d.cts +15 -0
- package/lib/cluster/queue.d.mts +15 -0
- package/lib/cluster/socket.cjs +8 -0
- package/lib/cluster/socket.d.cts +13 -0
- package/lib/cluster/socket.d.mts +13 -0
- package/lib/cluster/socket.d.ts +2 -0
- package/lib/cluster/socket.mjs +7 -0
- package/lib/cluster/subscriptions.d.cts +73 -0
- package/lib/cluster/subscriptions.d.mts +73 -0
- package/lib/cluster/subscriptions.d.ts +5 -1
- package/lib/cluster/validators.cjs +1 -0
- package/lib/cluster/validators.d.cts +7 -0
- package/lib/cluster/validators.d.mts +7 -0
- package/lib/cluster/validators.d.ts +1 -0
- package/lib/cluster/validators.mjs +1 -0
- package/lib/cluster-worker/engine-adapter.js +1 -11
- package/lib/cluster-worker/engine-input.js +14 -0
- package/lib/hosted-session/authority.cjs +61 -0
- package/lib/hosted-session/authority.d.cts +3 -0
- package/lib/hosted-session/authority.d.mts +3 -0
- package/lib/hosted-session/authority.d.ts +3 -0
- package/lib/hosted-session/authority.mjs +57 -0
- package/lib/hosted-session/coordinator.cjs +78 -51
- package/lib/hosted-session/coordinator.d.cts +11 -0
- package/lib/hosted-session/coordinator.d.mts +11 -0
- package/lib/hosted-session/coordinator.d.ts +4 -2
- package/lib/hosted-session/coordinator.mjs +75 -51
- package/lib/hosted-session/errors.cjs +25 -0
- package/lib/hosted-session/errors.d.cts +10 -0
- package/lib/hosted-session/errors.d.mts +10 -0
- package/lib/hosted-session/errors.d.ts +10 -0
- package/lib/hosted-session/errors.mjs +19 -0
- package/lib/hosted-session/index.cjs +4 -1
- package/lib/hosted-session/index.d.cts +2 -0
- package/lib/hosted-session/index.d.mts +2 -0
- package/lib/hosted-session/index.d.ts +2 -2
- package/lib/hosted-session/index.mjs +1 -1
- package/lib/hosted-session/reconnecting-watch.cjs +131 -0
- package/lib/hosted-session/reconnecting-watch.d.cts +13 -0
- package/lib/hosted-session/reconnecting-watch.d.mts +13 -0
- package/lib/hosted-session/reconnecting-watch.d.ts +13 -0
- package/lib/hosted-session/reconnecting-watch.mjs +127 -0
- package/lib/hosted-session/types.d.cts +32 -0
- package/lib/hosted-session/types.d.mts +32 -0
- package/lib/hosted-session/types.d.ts +19 -7
- package/lib/hosted-target/access-url.cjs +44 -0
- package/lib/hosted-target/access-url.d.cts +2 -0
- package/lib/hosted-target/access-url.d.mts +2 -0
- package/lib/hosted-target/access-url.d.ts +2 -0
- package/lib/hosted-target/access-url.mjs +41 -0
- package/lib/hosted-target/adapter-request.cjs +12 -0
- package/lib/hosted-target/adapter-request.d.cts +17 -0
- package/lib/hosted-target/adapter-request.d.mts +17 -0
- package/lib/hosted-target/adapter-request.d.ts +17 -0
- package/lib/hosted-target/adapter-request.mjs +9 -0
- package/lib/hosted-target/adapter-types.cjs +2 -0
- package/lib/hosted-target/adapter-types.d.cts +27 -0
- package/lib/hosted-target/adapter-types.d.mts +27 -0
- package/lib/hosted-target/adapter-types.d.ts +27 -0
- package/lib/hosted-target/adapter-types.mjs +1 -0
- package/lib/hosted-target/bounds.cjs +10 -0
- package/lib/hosted-target/bounds.d.cts +7 -0
- package/lib/hosted-target/bounds.d.mts +7 -0
- package/lib/hosted-target/bounds.d.ts +7 -0
- package/lib/hosted-target/bounds.mjs +7 -0
- package/lib/hosted-target/capsule-error-response.cjs +76 -0
- package/lib/hosted-target/capsule-error-response.d.cts +2 -0
- package/lib/hosted-target/capsule-error-response.d.mts +2 -0
- package/lib/hosted-target/capsule-error-response.d.ts +2 -0
- package/lib/hosted-target/capsule-error-response.mjs +73 -0
- package/lib/hosted-target/errors.cjs +88 -0
- package/lib/hosted-target/errors.d.cts +36 -0
- package/lib/hosted-target/errors.d.mts +36 -0
- package/lib/hosted-target/errors.d.ts +36 -0
- package/lib/hosted-target/errors.mjs +75 -0
- package/lib/hosted-target/index.cjs +16 -0
- package/lib/hosted-target/index.d.cts +3 -0
- package/lib/hosted-target/index.d.mts +3 -0
- package/lib/hosted-target/index.d.ts +3 -0
- package/lib/hosted-target/index.mjs +2 -0
- package/lib/hosted-target/response-validation.cjs +169 -0
- package/lib/hosted-target/response-validation.d.cts +5 -0
- package/lib/hosted-target/response-validation.d.mts +5 -0
- package/lib/hosted-target/response-validation.d.ts +5 -0
- package/lib/hosted-target/response-validation.mjs +163 -0
- package/lib/hosted-target/retry-executor.cjs +51 -0
- package/lib/hosted-target/retry-executor.d.cts +8 -0
- package/lib/hosted-target/retry-executor.d.mts +8 -0
- package/lib/hosted-target/retry-executor.d.ts +8 -0
- package/lib/hosted-target/retry-executor.mjs +48 -0
- package/lib/hosted-target/retry.cjs +78 -0
- package/lib/hosted-target/retry.d.cts +9 -0
- package/lib/hosted-target/retry.d.mts +9 -0
- package/lib/hosted-target/retry.d.ts +9 -0
- package/lib/hosted-target/retry.mjs +73 -0
- package/lib/hosted-target/target-adapter.cjs +10 -0
- package/lib/hosted-target/target-adapter.d.cts +3 -0
- package/lib/hosted-target/target-adapter.d.mts +3 -0
- package/lib/hosted-target/target-adapter.d.ts +3 -0
- package/lib/hosted-target/target-adapter.mjs +7 -0
- package/lib/hosted-target/types.cjs +4 -0
- package/lib/hosted-target/types.d.cts +50 -0
- package/lib/hosted-target/types.d.mts +50 -0
- package/lib/hosted-target/types.d.ts +50 -0
- package/lib/hosted-target/types.mjs +1 -0
- package/lib/hosted-target/zero-cloud-v1-adapter.cjs +185 -0
- package/lib/hosted-target/zero-cloud-v1-adapter.d.cts +13 -0
- package/lib/hosted-target/zero-cloud-v1-adapter.d.mts +13 -0
- package/lib/hosted-target/zero-cloud-v1-adapter.d.ts +13 -0
- package/lib/hosted-target/zero-cloud-v1-adapter.mjs +181 -0
- package/lib/target/bounded-response.cjs +51 -0
- package/lib/target/bounded-response.d.cts +1 -0
- package/lib/target/bounded-response.d.mts +1 -0
- package/lib/target/bounded-response.d.ts +1 -0
- package/lib/target/bounded-response.js +51 -0
- package/lib/target/bounded-response.mjs +48 -0
- package/lib/target/credential-lock.js +1 -3
- package/lib/target/credential-store.js +9 -4
- package/lib/target/device-flow.cjs +213 -0
- package/lib/target/device-flow.d.cts +54 -0
- package/lib/target/device-flow.d.mts +54 -0
- package/lib/target/device-flow.d.ts +26 -10
- package/lib/target/device-flow.js +162 -53
- package/lib/target/device-flow.mjs +203 -0
- package/lib/target/discovery-errors.cjs +10 -0
- package/lib/target/discovery-errors.d.cts +3 -0
- package/lib/target/discovery-errors.d.mts +3 -0
- package/lib/target/discovery-errors.d.ts +3 -0
- package/lib/target/discovery-errors.js +10 -0
- package/lib/target/discovery-errors.mjs +6 -0
- package/lib/target/discovery-sections.cjs +191 -0
- package/lib/target/discovery-sections.d.cts +52 -0
- package/lib/target/discovery-sections.d.mts +52 -0
- package/lib/target/discovery-sections.d.ts +52 -0
- package/lib/target/discovery-sections.js +191 -0
- package/lib/target/discovery-sections.mjs +179 -0
- package/lib/target/discovery-validation.cjs +109 -0
- package/lib/target/discovery-validation.d.cts +28 -0
- package/lib/target/discovery-validation.d.mts +28 -0
- package/lib/target/discovery-validation.d.ts +28 -0
- package/lib/target/discovery-validation.js +109 -0
- package/lib/target/discovery-validation.mjs +99 -0
- package/lib/target/discovery.cjs +112 -0
- package/lib/target/discovery.d.cts +74 -0
- package/lib/target/discovery.d.mts +74 -0
- package/lib/target/discovery.d.ts +68 -5
- package/lib/target/discovery.js +93 -101
- package/lib/target/discovery.mjs +105 -0
- package/lib/target/index.d.ts +6 -6
- package/lib/target/index.js +36 -36
- package/lib/target/oauth-http.d.ts +2 -0
- package/lib/target/oauth-http.js +25 -0
- package/lib/target/refresh-exchange.d.ts +11 -0
- package/lib/target/refresh-exchange.js +24 -0
- package/lib/target/refresh-revocation.d.ts +8 -0
- package/lib/target/refresh-revocation.js +19 -0
- package/lib/target/route-template.cjs +85 -0
- package/lib/target/route-template.d.cts +7 -0
- package/lib/target/route-template.d.mts +7 -0
- package/lib/target/route-template.d.ts +7 -0
- package/lib/target/route-template.js +85 -0
- package/lib/target/route-template.mjs +81 -0
- package/lib/target/session-errors.d.ts +4 -0
- package/lib/target/session-errors.js +12 -0
- package/lib/target/session-verification.d.ts +10 -0
- package/lib/target/session-verification.js +42 -0
- package/lib/target/target-registry.d.ts +7 -3
- package/lib/target/target-registry.js +41 -10
- package/lib/target/target-session-manager.d.ts +48 -0
- package/lib/target/target-session-manager.js +226 -0
- package/lib/target/target-session.d.ts +39 -31
- package/lib/target/target-session.js +63 -150
- package/lib/target/token-response.cjs +51 -0
- package/lib/target/token-response.d.cts +7 -0
- package/lib/target/token-response.d.mts +7 -0
- package/lib/target/token-response.d.ts +7 -0
- package/lib/target/token-response.js +51 -0
- package/lib/target/token-response.mjs +48 -0
- package/package.json +16 -6
- package/scripts/audit-production-dependencies.js +150 -0
- package/scripts/build-cluster.js +23 -3
- package/scripts/opcore-agent-gate.js +159 -0
- package/scripts/opcore-agent-tool-overlays.js +154 -0
- package/scripts/opcore-introduced-check.js +288 -0
- package/src/cluster/client.ts +42 -15
- package/src/cluster/connection-state.ts +33 -0
- package/src/cluster/connection-types.ts +28 -0
- package/src/cluster/connection.ts +37 -68
- package/src/cluster/errors.ts +5 -0
- package/src/cluster/index.ts +3 -1
- package/src/cluster/json-source.ts +1 -0
- package/src/cluster/payload-value.ts +1 -0
- package/src/cluster/socket.ts +13 -0
- package/src/cluster/subscriptions.ts +7 -2
- package/src/cluster/validators.ts +1 -0
- package/src/cluster/ws.d.ts +1 -1
- package/src/hosted-session/authority.ts +77 -0
- package/src/hosted-session/coordinator.ts +113 -56
- package/src/hosted-session/errors.ts +21 -0
- package/src/hosted-session/index.ts +14 -2
- package/src/hosted-session/reconnecting-watch.ts +148 -0
- package/src/hosted-session/types.ts +22 -8
- package/src/hosted-target/access-url.ts +46 -0
- package/src/hosted-target/adapter-request.ts +28 -0
- package/src/hosted-target/adapter-types.ts +39 -0
- package/src/hosted-target/bounds.ts +1 -0
- package/src/hosted-target/capsule-error-response.ts +107 -0
- package/src/hosted-target/errors.ts +36 -35
- package/src/hosted-target/index.ts +10 -23
- package/src/hosted-target/response-validation.ts +170 -62
- package/src/hosted-target/retry-executor.ts +62 -0
- package/src/hosted-target/retry.ts +47 -11
- package/src/hosted-target/target-adapter.ts +12 -8
- package/src/hosted-target/types.ts +19 -18
- package/src/hosted-target/zero-cloud-v1-adapter.ts +229 -316
- package/src/target/bounded-response.ts +68 -0
- package/src/target/credential-lock.ts +1 -3
- package/src/target/credential-store.ts +14 -10
- package/src/target/device-flow.ts +206 -71
- package/src/target/discovery-errors.ts +6 -0
- package/src/target/discovery-sections.ts +251 -0
- package/src/target/discovery-validation.ts +144 -0
- package/src/target/discovery.ts +163 -115
- package/src/target/index.ts +15 -11
- package/src/target/oauth-http.ts +27 -0
- package/src/target/refresh-exchange.ts +36 -0
- package/src/target/refresh-revocation.ts +29 -0
- package/src/target/route-template.ts +105 -0
- package/src/target/session-errors.ts +9 -0
- package/src/target/session-verification.ts +51 -0
- package/src/target/target-registry.ts +60 -27
- package/src/target/target-session-manager.ts +285 -0
- package/src/target/target-session.ts +105 -222
- package/src/target/token-response.ts +62 -0
|
@@ -1,26 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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((
|
|
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 #
|
|
15
|
-
readonly #
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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:
|
|
104
|
+
renewalDeadline(access: HostedAccess, receivedAt: number): number {
|
|
42
105
|
const expiresAt = Date.parse(access.expiresAt);
|
|
43
|
-
if (Number.
|
|
44
|
-
throw new ClusterConfigError(
|
|
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
|
-
|
|
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.#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
'
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
readonly
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|