@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,19 +1,19 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import type { TargetDiscoveryDescriptor } from './discovery.js';
|
|
2
3
|
|
|
3
4
|
export interface TargetRecord {
|
|
4
5
|
readonly id: string;
|
|
5
6
|
readonly url: string;
|
|
6
7
|
readonly adapterVersion: string;
|
|
7
8
|
readonly deviceToken: string;
|
|
8
|
-
readonly organization?: { readonly id: string; readonly name
|
|
9
|
+
readonly organization?: { readonly id: string; readonly name?: string };
|
|
10
|
+
readonly refreshInvalidated?: true;
|
|
9
11
|
readonly createdAt: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class TargetNameInvalidError extends Error {
|
|
13
15
|
constructor(name: string) {
|
|
14
|
-
super(
|
|
15
|
-
`Invalid target name "${name}". Must be 1-64 characters, alphanumeric and hyphens only.`,
|
|
16
|
-
);
|
|
16
|
+
super(`Invalid target name "${name}". Must be 1-64 characters, alphanumeric and hyphens only.`);
|
|
17
17
|
this.name = 'TargetNameInvalidError';
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -40,7 +40,11 @@ export class TargetUrlInvalidError extends Error {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const TARGET_NAME_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$/;
|
|
43
|
-
const LOOPBACK_HOSTS
|
|
43
|
+
const LOOPBACK_HOSTS: Readonly<Record<string, true>> = Object.freeze({
|
|
44
|
+
'127.0.0.1': true,
|
|
45
|
+
'::1': true,
|
|
46
|
+
'[::1]': true,
|
|
47
|
+
});
|
|
44
48
|
|
|
45
49
|
export function validateTargetName(name: string): void {
|
|
46
50
|
if (!TARGET_NAME_PATTERN.test(name) || name.length > 64) {
|
|
@@ -49,6 +53,9 @@ export function validateTargetName(name: string): void {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export function normalizeAndValidateUrl(rawUrl: string): string {
|
|
56
|
+
if (/[\u0000-\u0020\u007f]|\s/u.test(rawUrl)) {
|
|
57
|
+
throw new TargetUrlInvalidError(rawUrl, 'URL contains forbidden whitespace or controls');
|
|
58
|
+
}
|
|
52
59
|
let parsed: URL;
|
|
53
60
|
try {
|
|
54
61
|
parsed = new URL(rawUrl);
|
|
@@ -64,16 +71,20 @@ export function normalizeAndValidateUrl(rawUrl: string): string {
|
|
|
64
71
|
throw new TargetUrlInvalidError(rawUrl, 'URL must not contain query or fragment');
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
const isLoopback = LOOPBACK_HOSTS
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
const isLoopback = LOOPBACK_HOSTS[parsed.hostname] === true;
|
|
75
|
+
const protocolAllowed =
|
|
76
|
+
parsed.protocol === 'https:' || (parsed.protocol === 'http:' && isLoopback);
|
|
77
|
+
if (!protocolAllowed) {
|
|
78
|
+
throw new TargetUrlInvalidError(
|
|
79
|
+
rawUrl,
|
|
80
|
+
'HTTPS required except for literal loopback HTTP targets'
|
|
81
|
+
);
|
|
70
82
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (normalized.endsWith('/') && normalized.length > 1) {
|
|
74
|
-
normalized = normalized.slice(0, -1);
|
|
83
|
+
if (parsed.pathname !== '/') {
|
|
84
|
+
throw new TargetUrlInvalidError(rawUrl, 'URL must contain only an origin');
|
|
75
85
|
}
|
|
76
|
-
|
|
86
|
+
|
|
87
|
+
return parsed.origin;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
interface SettingsWithTargets {
|
|
@@ -90,6 +101,7 @@ export function addTarget(
|
|
|
90
101
|
name: string,
|
|
91
102
|
rawUrl: string,
|
|
92
103
|
settings: SettingsPort,
|
|
104
|
+
descriptor: TargetDiscoveryDescriptor
|
|
93
105
|
): TargetRecord {
|
|
94
106
|
validateTargetName(name);
|
|
95
107
|
const url = normalizeAndValidateUrl(rawUrl);
|
|
@@ -98,11 +110,14 @@ export function addTarget(
|
|
|
98
110
|
if (existing._targets?.[name]) {
|
|
99
111
|
throw new TargetNameExistsError(name);
|
|
100
112
|
}
|
|
113
|
+
if (!descriptor || descriptor.origin !== url || descriptor.adapter.majorVersion !== 1) {
|
|
114
|
+
throw new TargetUrlInvalidError(rawUrl, 'validated discovery does not match the target origin');
|
|
115
|
+
}
|
|
101
116
|
|
|
102
117
|
const record: TargetRecord = {
|
|
103
118
|
id: crypto.randomUUID(),
|
|
104
119
|
url,
|
|
105
|
-
adapterVersion:
|
|
120
|
+
adapterVersion: `v${descriptor.adapter.majorVersion}`,
|
|
106
121
|
deviceToken: crypto.randomUUID(),
|
|
107
122
|
createdAt: new Date().toISOString(),
|
|
108
123
|
};
|
|
@@ -117,10 +132,7 @@ export function addTarget(
|
|
|
117
132
|
return record;
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
export function removeTarget(
|
|
121
|
-
name: string,
|
|
122
|
-
settings: SettingsPort,
|
|
123
|
-
): TargetRecord {
|
|
135
|
+
export function removeTarget(name: string, settings: SettingsPort): TargetRecord {
|
|
124
136
|
const existing = settings.load();
|
|
125
137
|
const record = existing._targets?.[name];
|
|
126
138
|
if (!record) {
|
|
@@ -136,17 +148,12 @@ export function removeTarget(
|
|
|
136
148
|
return record;
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
export function getTarget(
|
|
140
|
-
name: string,
|
|
141
|
-
settings: SettingsPort,
|
|
142
|
-
): TargetRecord | null {
|
|
151
|
+
export function getTarget(name: string, settings: SettingsPort): TargetRecord | null {
|
|
143
152
|
const existing = settings.load();
|
|
144
153
|
return existing._targets?.[name] ?? null;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
export function listTargets(
|
|
148
|
-
settings: SettingsPort,
|
|
149
|
-
): Array<{ name: string; record: TargetRecord }> {
|
|
156
|
+
export function listTargets(settings: SettingsPort): Array<{ name: string; record: TargetRecord }> {
|
|
150
157
|
const existing = settings.load();
|
|
151
158
|
const targets = existing._targets ?? {};
|
|
152
159
|
return Object.entries(targets).map(([name, record]) => ({ name, record }));
|
|
@@ -154,8 +161,8 @@ export function listTargets(
|
|
|
154
161
|
|
|
155
162
|
export function updateTargetOrganization(
|
|
156
163
|
name: string,
|
|
157
|
-
organization: { id: string; name
|
|
158
|
-
settings: SettingsPort
|
|
164
|
+
organization: { id: string; name?: string },
|
|
165
|
+
settings: SettingsPort
|
|
159
166
|
): void {
|
|
160
167
|
const existing = settings.load();
|
|
161
168
|
if (!existing._targets?.[name]) {
|
|
@@ -172,3 +179,29 @@ export function updateTargetOrganization(
|
|
|
172
179
|
}
|
|
173
180
|
});
|
|
174
181
|
}
|
|
182
|
+
|
|
183
|
+
export function targetRefreshIsInvalidated(
|
|
184
|
+
name: string,
|
|
185
|
+
settings: SettingsPort,
|
|
186
|
+
): boolean {
|
|
187
|
+
return settings.load()._targets?.[name]?.refreshInvalidated === true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function setTargetRefreshInvalidated(
|
|
191
|
+
name: string,
|
|
192
|
+
invalidated: boolean,
|
|
193
|
+
settings: SettingsPort,
|
|
194
|
+
): void {
|
|
195
|
+
settings.mutate((state) => {
|
|
196
|
+
const targets = state._targets;
|
|
197
|
+
const target = targets?.[name];
|
|
198
|
+
if (targets === undefined || target === undefined) throw new TargetNotFoundError(name);
|
|
199
|
+
if (invalidated) {
|
|
200
|
+
targets[name] = { ...target, refreshInvalidated: true };
|
|
201
|
+
} else {
|
|
202
|
+
const active = { ...target };
|
|
203
|
+
Reflect.deleteProperty(active, 'refreshInvalidated');
|
|
204
|
+
targets[name] = active;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { TargetCredentialStore } from './credential-store.js';
|
|
2
|
+
import { exchangeRefreshToken } from './refresh-exchange.js';
|
|
3
|
+
import type { TargetRecord, SettingsPort } from './target-registry.js';
|
|
4
|
+
import {
|
|
5
|
+
requestDeviceCode,
|
|
6
|
+
pollForToken,
|
|
7
|
+
type Clock,
|
|
8
|
+
type HttpTransport,
|
|
9
|
+
} from './device-flow.js';
|
|
10
|
+
import { targetServiceKey, TARGET_ACCOUNT } from './credential-store.js';
|
|
11
|
+
import {
|
|
12
|
+
setTargetRefreshInvalidated,
|
|
13
|
+
targetRefreshIsInvalidated,
|
|
14
|
+
updateTargetOrganization,
|
|
15
|
+
} from './target-registry.js';
|
|
16
|
+
import type { TargetSessionEndpoints } from './discovery.js';
|
|
17
|
+
import { LoginRequiredError } from './session-errors.js';
|
|
18
|
+
export { LoginRequiredError } from './session-errors.js';
|
|
19
|
+
import { readVerifiedOrganization } from './session-verification.js';
|
|
20
|
+
import { revokeRefreshToken } from './refresh-revocation.js';
|
|
21
|
+
|
|
22
|
+
const DEVICE_LABEL = 'zeroshot-cli';
|
|
23
|
+
const TOKEN_EXPIRY_SKEW_MS = 30_000;
|
|
24
|
+
const AUDIENCE_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
25
|
+
const INVALID_REFRESH_FAMILY = 'zeroshot.invalidated-refresh-family/v1';
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export interface BrowserOpener {
|
|
29
|
+
open(url: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TargetSessionDeps {
|
|
33
|
+
readonly http: HttpTransport;
|
|
34
|
+
readonly clock: Clock;
|
|
35
|
+
readonly browserOpener: BrowserOpener;
|
|
36
|
+
readonly stderr: { write(s: string): void };
|
|
37
|
+
readonly discoveryEndpoints: TargetSessionEndpoints;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TargetSessionManagerInit {
|
|
41
|
+
readonly targetName: string;
|
|
42
|
+
readonly target: TargetRecord;
|
|
43
|
+
readonly credentialStore: TargetCredentialStore;
|
|
44
|
+
readonly acquireLock: () => Promise<() => Promise<void>>;
|
|
45
|
+
readonly settings: SettingsPort;
|
|
46
|
+
readonly deps: TargetSessionDeps;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CachedAccess {
|
|
50
|
+
readonly token: string;
|
|
51
|
+
readonly expiresAt: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function abortReason(signal?: AbortSignal): unknown {
|
|
55
|
+
return signal?.reason ?? new DOMException('Aborted', 'AbortError');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireAudience(audience: string): void {
|
|
59
|
+
if (!AUDIENCE_PATTERN.test(audience)) throw new Error('Requested target audience is invalid');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sole owner of one target's rotating refresh family and audience-scoped access cache.
|
|
64
|
+
* The cross-process lock covers durable reread, exchange, replacement, and invalidation.
|
|
65
|
+
*/
|
|
66
|
+
export class TargetSessionManager {
|
|
67
|
+
readonly #targetName: string;
|
|
68
|
+
readonly #target: TargetRecord;
|
|
69
|
+
readonly #store: TargetCredentialStore;
|
|
70
|
+
readonly #acquireLock: () => Promise<() => Promise<void>>;
|
|
71
|
+
readonly #settings: SettingsPort;
|
|
72
|
+
readonly #deps: TargetSessionDeps;
|
|
73
|
+
readonly #access = new Map<string, CachedAccess>();
|
|
74
|
+
|
|
75
|
+
constructor(init: TargetSessionManagerInit) {
|
|
76
|
+
this.#targetName = init.targetName;
|
|
77
|
+
this.#target = init.target;
|
|
78
|
+
this.#store = init.credentialStore;
|
|
79
|
+
this.#acquireLock = init.acquireLock;
|
|
80
|
+
this.#settings = init.settings;
|
|
81
|
+
this.#deps = init.deps;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#cachedAccess(audience: string): { accessToken: string; expiresIn: number } | null {
|
|
85
|
+
const cached = this.#access.get(audience);
|
|
86
|
+
if (
|
|
87
|
+
cached === undefined ||
|
|
88
|
+
this.#deps.clock.now() >= cached.expiresAt - TOKEN_EXPIRY_SKEW_MS
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
accessToken: cached.token,
|
|
94
|
+
expiresIn: Math.max(
|
|
95
|
+
0,
|
|
96
|
+
Math.floor((cached.expiresAt - this.#deps.clock.now()) / 1_000),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async login(signal?: AbortSignal): Promise<{ organization: { id: string } }> {
|
|
102
|
+
const endpoints = this.#deps.discoveryEndpoints;
|
|
103
|
+
const code = await requestDeviceCode(
|
|
104
|
+
endpoints.deviceAuthorizationEndpoint,
|
|
105
|
+
endpoints.clientId,
|
|
106
|
+
this.#deps.http,
|
|
107
|
+
signal
|
|
108
|
+
);
|
|
109
|
+
this.#deps.stderr.write(
|
|
110
|
+
`\nOpen this URL to authorize:\n ${code.verification_uri}\n\nEnter code: ${code.user_code}\n\n`
|
|
111
|
+
);
|
|
112
|
+
if (code.verification_uri_complete) {
|
|
113
|
+
try {
|
|
114
|
+
await this.#deps.browserOpener.open(code.verification_uri_complete);
|
|
115
|
+
} catch {
|
|
116
|
+
// Opening a browser is explicitly optional; polling remains authoritative.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const release = await this.#acquireLock();
|
|
121
|
+
try {
|
|
122
|
+
const storedPrevious = await this.#store.get(this.#serviceKey(), TARGET_ACCOUNT);
|
|
123
|
+
const previous = storedPrevious === INVALID_REFRESH_FAMILY ? null : storedPrevious;
|
|
124
|
+
|
|
125
|
+
const token = await pollForToken({
|
|
126
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
127
|
+
clientId: endpoints.clientId,
|
|
128
|
+
deviceCode: code.device_code,
|
|
129
|
+
interval: code.interval,
|
|
130
|
+
expiresIn: code.expires_in,
|
|
131
|
+
http: this.#deps.http,
|
|
132
|
+
clock: this.#deps.clock,
|
|
133
|
+
...(signal === undefined ? {} : { signal }),
|
|
134
|
+
exchange: {
|
|
135
|
+
grantType: endpoints.deviceGrantType,
|
|
136
|
+
deviceToken: this.#target.deviceToken,
|
|
137
|
+
deviceLabel: DEVICE_LABEL,
|
|
138
|
+
audience: endpoints.audience,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const organization = await readVerifiedOrganization(
|
|
142
|
+
token.access_token,
|
|
143
|
+
signal,
|
|
144
|
+
this.#deps,
|
|
145
|
+
).catch(
|
|
146
|
+
async () => {
|
|
147
|
+
await this.#bestEffortRevoke(token.refresh_token);
|
|
148
|
+
throw new LoginRequiredError(this.#targetName);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
try {
|
|
152
|
+
await this.#store.set(this.#serviceKey(), TARGET_ACCOUNT, token.refresh_token);
|
|
153
|
+
setTargetRefreshInvalidated(this.#targetName, false, this.#settings);
|
|
154
|
+
this.#access.set(endpoints.audience, {
|
|
155
|
+
token: token.access_token,
|
|
156
|
+
expiresAt: this.#deps.clock.now() + token.expires_in * 1_000,
|
|
157
|
+
});
|
|
158
|
+
updateTargetOrganization(this.#targetName, organization, this.#settings);
|
|
159
|
+
if (previous && previous !== token.refresh_token) {
|
|
160
|
+
const revoked = await revokeRefreshToken(previous, this.#deps);
|
|
161
|
+
if (!revoked.ok) throw new Error('Prior refresh family revocation failed');
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
await this.#invalidateFamily(token.refresh_token);
|
|
165
|
+
throw new LoginRequiredError(this.#targetName);
|
|
166
|
+
}
|
|
167
|
+
return { organization };
|
|
168
|
+
} finally {
|
|
169
|
+
await release();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getAccessTokenWithLifetime(
|
|
174
|
+
audience: string,
|
|
175
|
+
signal?: AbortSignal,
|
|
176
|
+
): Promise<{ accessToken: string; expiresIn: number }> {
|
|
177
|
+
requireAudience(audience);
|
|
178
|
+
const cached = this.#cachedAccess(audience);
|
|
179
|
+
if (cached !== null) return cached;
|
|
180
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
181
|
+
|
|
182
|
+
const release = await this.#acquireLock();
|
|
183
|
+
try {
|
|
184
|
+
const afterLock = this.#cachedAccess(audience);
|
|
185
|
+
if (afterLock !== null) return afterLock;
|
|
186
|
+
if (targetRefreshIsInvalidated(this.#targetName, this.#settings)) {
|
|
187
|
+
throw new LoginRequiredError(this.#targetName);
|
|
188
|
+
}
|
|
189
|
+
const refreshToken = await this.#store.get(this.#serviceKey(), TARGET_ACCOUNT);
|
|
190
|
+
if (!refreshToken || refreshToken === INVALID_REFRESH_FAMILY) {
|
|
191
|
+
throw new LoginRequiredError(this.#targetName);
|
|
192
|
+
}
|
|
193
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
194
|
+
setTargetRefreshInvalidated(this.#targetName, true, this.#settings);
|
|
195
|
+
await this.#store.set(this.#serviceKey(), TARGET_ACCOUNT, INVALID_REFRESH_FAMILY);
|
|
196
|
+
|
|
197
|
+
let replacement: Awaited<ReturnType<typeof exchangeRefreshToken>> | undefined;
|
|
198
|
+
try {
|
|
199
|
+
replacement = await exchangeRefreshToken({
|
|
200
|
+
tokenEndpoint: this.#deps.discoveryEndpoints.tokenEndpoint,
|
|
201
|
+
clientId: this.#deps.discoveryEndpoints.clientId,
|
|
202
|
+
refreshToken,
|
|
203
|
+
audience,
|
|
204
|
+
http: this.#deps.http,
|
|
205
|
+
...(signal === undefined ? {} : { signal }),
|
|
206
|
+
});
|
|
207
|
+
await this.#store.set(this.#serviceKey(), TARGET_ACCOUNT, replacement.refresh_token);
|
|
208
|
+
setTargetRefreshInvalidated(this.#targetName, false, this.#settings);
|
|
209
|
+
this.#access.clear();
|
|
210
|
+
this.#access.set(audience, {
|
|
211
|
+
token: replacement.access_token,
|
|
212
|
+
expiresAt: this.#deps.clock.now() + replacement.expires_in * 1_000,
|
|
213
|
+
});
|
|
214
|
+
return { accessToken: replacement.access_token, expiresIn: replacement.expires_in };
|
|
215
|
+
} catch {
|
|
216
|
+
await this.#invalidateFamily(replacement?.refresh_token);
|
|
217
|
+
throw new LoginRequiredError(this.#targetName);
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
await release();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getAccessToken(audience: string, signal?: AbortSignal): Promise<string> {
|
|
225
|
+
return (await this.getAccessTokenWithLifetime(audience, signal)).accessToken;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
tokenProvider(audience: string): { getAccessToken(signal?: AbortSignal): Promise<string> } {
|
|
229
|
+
requireAudience(audience);
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
getAccessToken: (signal?: AbortSignal) => this.getAccessToken(audience, signal),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async revoke(force: boolean): Promise<void> {
|
|
236
|
+
const release = await this.#acquireLock();
|
|
237
|
+
try {
|
|
238
|
+
const token = await this.#store.get(this.#serviceKey(), TARGET_ACCOUNT);
|
|
239
|
+
if (token && token !== INVALID_REFRESH_FAMILY) {
|
|
240
|
+
try {
|
|
241
|
+
const response = await revokeRefreshToken(token, this.#deps);
|
|
242
|
+
if (!response.ok && !force)
|
|
243
|
+
throw new Error('Remote revocation failed. Use --force to remove anyway.');
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (!force) throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
this.#access.clear();
|
|
249
|
+
await this.#store.delete(this.#serviceKey(), TARGET_ACCOUNT);
|
|
250
|
+
setTargetRefreshInvalidated(this.#targetName, false, this.#settings);
|
|
251
|
+
} finally {
|
|
252
|
+
await release();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
clearMemory(): void {
|
|
257
|
+
this.#access.clear();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async #invalidateFamily(replacement?: string): Promise<void> {
|
|
262
|
+
this.#access.clear();
|
|
263
|
+
setTargetRefreshInvalidated(this.#targetName, true, this.#settings);
|
|
264
|
+
try {
|
|
265
|
+
await this.#store.set(this.#serviceKey(), TARGET_ACCOUNT, INVALID_REFRESH_FAMILY);
|
|
266
|
+
} catch {
|
|
267
|
+
// A direct delete remains safe if overwriting the stale family was unavailable.
|
|
268
|
+
}
|
|
269
|
+
if (replacement) await this.#bestEffortRevoke(replacement);
|
|
270
|
+
await this.#store.delete(this.#serviceKey(), TARGET_ACCOUNT);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async #bestEffortRevoke(token: string): Promise<void> {
|
|
274
|
+
try {
|
|
275
|
+
await revokeRefreshToken(token, this.#deps);
|
|
276
|
+
} catch {
|
|
277
|
+
// Revocation is compensating cleanup; callers still receive login-required.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
#serviceKey(): string {
|
|
283
|
+
return targetServiceKey(this.#target.id);
|
|
284
|
+
}
|
|
285
|
+
}
|