@the-open-engine/zeroshot 6.26.0 → 6.28.0
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/cli/index.js +211 -2
- package/lib/cluster/client.cjs +30 -5
- package/lib/cluster/client.d.ts +11 -1
- package/lib/cluster/client.mjs +29 -5
- package/lib/cluster/connection.cjs +38 -2
- package/lib/cluster/connection.d.ts +3 -0
- package/lib/cluster/connection.mjs +37 -1
- package/lib/cluster/index.cjs +3 -1
- package/lib/cluster/index.d.ts +3 -3
- package/lib/cluster/index.mjs +2 -2
- package/lib/hosted-session/coordinator.cjs +101 -0
- package/lib/hosted-session/coordinator.d.ts +9 -0
- package/lib/hosted-session/coordinator.mjs +97 -0
- package/lib/hosted-session/index.cjs +5 -0
- package/lib/hosted-session/index.d.ts +2 -0
- package/lib/hosted-session/index.mjs +1 -0
- package/lib/hosted-session/types.cjs +2 -0
- package/lib/hosted-session/types.d.ts +20 -0
- package/lib/hosted-session/types.mjs +1 -0
- package/lib/target/credential-lock.d.ts +1 -0
- package/lib/target/credential-lock.js +38 -0
- package/lib/target/credential-store.d.ts +27 -0
- package/lib/target/credential-store.js +113 -0
- package/lib/target/device-flow.d.ts +38 -0
- package/lib/target/device-flow.js +104 -0
- package/lib/target/discovery.d.ts +11 -0
- package/lib/target/discovery.js +120 -0
- package/lib/target/index.d.ts +6 -0
- package/lib/target/index.js +38 -0
- package/lib/target/target-registry.d.ts +45 -0
- package/lib/target/target-registry.js +132 -0
- package/lib/target/target-session.d.ts +39 -0
- package/lib/target/target-session.js +162 -0
- package/package.json +20 -8
- package/scripts/build-cluster.js +21 -7
- package/src/cluster/client.ts +45 -5
- package/src/cluster/connection.ts +32 -1
- package/src/cluster/index.ts +4 -2
- package/src/cluster/ws.d.ts +1 -0
- package/src/hosted-session/coordinator.ts +110 -0
- package/src/hosted-session/index.ts +2 -0
- package/src/hosted-session/types.ts +21 -0
- package/src/target/credential-lock.ts +35 -0
- package/src/target/credential-store.ts +107 -0
- package/src/target/device-flow.ts +157 -0
- package/src/target/discovery.ts +149 -0
- package/src/target/index.ts +55 -0
- package/src/target/target-registry.ts +174 -0
- package/src/target/target-session.ts +249 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TargetDiscoveryError = void 0;
|
|
4
|
+
exports.discoverTargetSessionEndpoints = discoverTargetSessionEndpoints;
|
|
5
|
+
const DISCOVERY_PATH = '/.well-known/openengine-hosted-target';
|
|
6
|
+
const MAX_DISCOVERY_BYTES = 64 * 1024;
|
|
7
|
+
class TargetDiscoveryError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(`Target discovery failed: ${message}`);
|
|
10
|
+
this.name = 'TargetDiscoveryError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.TargetDiscoveryError = TargetDiscoveryError;
|
|
14
|
+
function record(value, field) {
|
|
15
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
|
+
throw new TargetDiscoveryError(`${field} must be an object`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function stringField(source, field) {
|
|
21
|
+
const value = source[field];
|
|
22
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
23
|
+
throw new TargetDiscoveryError(`${field} must be a non-empty string`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function safeEndpoint(value, field, serviceOrigin) {
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
throw new TargetDiscoveryError(`${field} must be an absolute URL`);
|
|
30
|
+
}
|
|
31
|
+
let endpoint;
|
|
32
|
+
try {
|
|
33
|
+
endpoint = new URL(value);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new TargetDiscoveryError(`${field} must be an absolute URL`);
|
|
37
|
+
}
|
|
38
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
39
|
+
throw new TargetDiscoveryError(`${field} contains forbidden URL components`);
|
|
40
|
+
}
|
|
41
|
+
if (endpoint.origin !== serviceOrigin) {
|
|
42
|
+
throw new TargetDiscoveryError(`${field} must remain on the target origin`);
|
|
43
|
+
}
|
|
44
|
+
return endpoint.href;
|
|
45
|
+
}
|
|
46
|
+
async function readBoundedJson(response) {
|
|
47
|
+
const declaredLength = response.headers.get('content-length');
|
|
48
|
+
if (declaredLength !== null && Number(declaredLength) > MAX_DISCOVERY_BYTES) {
|
|
49
|
+
throw new TargetDiscoveryError('response exceeds the size limit');
|
|
50
|
+
}
|
|
51
|
+
if (!response.body)
|
|
52
|
+
return response.json();
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done)
|
|
59
|
+
break;
|
|
60
|
+
total += value.byteLength;
|
|
61
|
+
if (total > MAX_DISCOVERY_BYTES) {
|
|
62
|
+
await reader.cancel();
|
|
63
|
+
throw new TargetDiscoveryError('response exceeds the size limit');
|
|
64
|
+
}
|
|
65
|
+
chunks.push(value);
|
|
66
|
+
}
|
|
67
|
+
const bytes = new Uint8Array(total);
|
|
68
|
+
let offset = 0;
|
|
69
|
+
for (const chunk of chunks) {
|
|
70
|
+
bytes.set(chunk, offset);
|
|
71
|
+
offset += chunk.byteLength;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new TargetDiscoveryError('response is not valid JSON');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function fetchDocument(http, url) {
|
|
81
|
+
const response = await http.fetch(url, {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: { Accept: 'application/json' },
|
|
84
|
+
redirect: 'error',
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new TargetDiscoveryError(`request failed with status ${response.status}`);
|
|
88
|
+
}
|
|
89
|
+
return record(await readBoundedJson(response), 'response');
|
|
90
|
+
}
|
|
91
|
+
async function discoverTargetSessionEndpoints(targetUrl, http) {
|
|
92
|
+
const target = new URL(targetUrl);
|
|
93
|
+
const discovery = await fetchDocument(http, new URL(DISCOVERY_PATH, target).href);
|
|
94
|
+
if (discovery.kind !== 'openengine.hosted-target/v1') {
|
|
95
|
+
throw new TargetDiscoveryError('unsupported hosted-target version');
|
|
96
|
+
}
|
|
97
|
+
if (discovery.organization_binding !== 'device_approval') {
|
|
98
|
+
throw new TargetDiscoveryError('unsupported organization binding');
|
|
99
|
+
}
|
|
100
|
+
const oauth = record(discovery.oauth, 'oauth');
|
|
101
|
+
const metadataUrl = safeEndpoint(oauth.metadata_url, 'oauth.metadata_url', target.origin);
|
|
102
|
+
const deviceEndpoint = safeEndpoint(oauth.device_authorization_endpoint, 'oauth.device_authorization_endpoint', target.origin);
|
|
103
|
+
const tokenEndpoint = safeEndpoint(oauth.token_endpoint, 'oauth.token_endpoint', target.origin);
|
|
104
|
+
const clientId = stringField(oauth, 'client_id');
|
|
105
|
+
const metadata = await fetchDocument(http, metadataUrl);
|
|
106
|
+
const metadataDeviceEndpoint = safeEndpoint(metadata.device_authorization_endpoint, 'device_authorization_endpoint', target.origin);
|
|
107
|
+
const metadataTokenEndpoint = safeEndpoint(metadata.token_endpoint, 'token_endpoint', target.origin);
|
|
108
|
+
if (metadataDeviceEndpoint !== deviceEndpoint || metadataTokenEndpoint !== tokenEndpoint) {
|
|
109
|
+
throw new TargetDiscoveryError('OAuth metadata does not match hosted-target discovery');
|
|
110
|
+
}
|
|
111
|
+
const revocationEndpoint = metadata.revocation_endpoint === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: safeEndpoint(metadata.revocation_endpoint, 'revocation_endpoint', target.origin);
|
|
114
|
+
return {
|
|
115
|
+
deviceAuthorizationEndpoint: deviceEndpoint,
|
|
116
|
+
tokenEndpoint,
|
|
117
|
+
...(revocationEndpoint === undefined ? {} : { revocationEndpoint }),
|
|
118
|
+
clientId,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CredentialStoreUnavailableError, KeyringCredentialStore, FakeCredentialStore, targetServiceKey, TARGET_ACCOUNT, type TargetCredentialStore, } from './credential-store.ts';
|
|
2
|
+
export { acquireTargetLock } from './credential-lock.ts';
|
|
3
|
+
export { requestDeviceCode, pollForToken, DeviceFlowDeniedError, DeviceFlowExpiredError, UnboundSessionError, type DeviceCodeResponse, type TokenResponse, type HttpTransport, type Clock, } from './device-flow.ts';
|
|
4
|
+
export { addTarget, removeTarget, getTarget, listTargets, updateTargetOrganization, validateTargetName, normalizeAndValidateUrl, TargetNameInvalidError, TargetNameExistsError, TargetNotFoundError, TargetUrlInvalidError, type TargetRecord, type SettingsPort, } from './target-registry.ts';
|
|
5
|
+
export { targetLogin, refreshAccessToken, getAccessTokenProvider, revokeAndCleanup, LoginRequiredError, type BrowserOpener, type TargetSessionDeps, type TargetAccessTokenProvider, } from './target-session.ts';
|
|
6
|
+
export { discoverTargetSessionEndpoints, TargetDiscoveryError, type TargetSessionEndpoints, } from './discovery.ts';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TargetDiscoveryError = exports.discoverTargetSessionEndpoints = exports.LoginRequiredError = exports.revokeAndCleanup = exports.getAccessTokenProvider = exports.refreshAccessToken = exports.targetLogin = exports.TargetUrlInvalidError = exports.TargetNotFoundError = exports.TargetNameExistsError = exports.TargetNameInvalidError = exports.normalizeAndValidateUrl = exports.validateTargetName = exports.updateTargetOrganization = exports.listTargets = exports.getTarget = exports.removeTarget = exports.addTarget = exports.UnboundSessionError = exports.DeviceFlowExpiredError = exports.DeviceFlowDeniedError = exports.pollForToken = exports.requestDeviceCode = exports.acquireTargetLock = exports.TARGET_ACCOUNT = exports.targetServiceKey = exports.FakeCredentialStore = exports.KeyringCredentialStore = exports.CredentialStoreUnavailableError = void 0;
|
|
4
|
+
var credential_store_ts_1 = require("./credential-store.js");
|
|
5
|
+
Object.defineProperty(exports, "CredentialStoreUnavailableError", { enumerable: true, get: function () { return credential_store_ts_1.CredentialStoreUnavailableError; } });
|
|
6
|
+
Object.defineProperty(exports, "KeyringCredentialStore", { enumerable: true, get: function () { return credential_store_ts_1.KeyringCredentialStore; } });
|
|
7
|
+
Object.defineProperty(exports, "FakeCredentialStore", { enumerable: true, get: function () { return credential_store_ts_1.FakeCredentialStore; } });
|
|
8
|
+
Object.defineProperty(exports, "targetServiceKey", { enumerable: true, get: function () { return credential_store_ts_1.targetServiceKey; } });
|
|
9
|
+
Object.defineProperty(exports, "TARGET_ACCOUNT", { enumerable: true, get: function () { return credential_store_ts_1.TARGET_ACCOUNT; } });
|
|
10
|
+
var credential_lock_ts_1 = require("./credential-lock.js");
|
|
11
|
+
Object.defineProperty(exports, "acquireTargetLock", { enumerable: true, get: function () { return credential_lock_ts_1.acquireTargetLock; } });
|
|
12
|
+
var device_flow_ts_1 = require("./device-flow.js");
|
|
13
|
+
Object.defineProperty(exports, "requestDeviceCode", { enumerable: true, get: function () { return device_flow_ts_1.requestDeviceCode; } });
|
|
14
|
+
Object.defineProperty(exports, "pollForToken", { enumerable: true, get: function () { return device_flow_ts_1.pollForToken; } });
|
|
15
|
+
Object.defineProperty(exports, "DeviceFlowDeniedError", { enumerable: true, get: function () { return device_flow_ts_1.DeviceFlowDeniedError; } });
|
|
16
|
+
Object.defineProperty(exports, "DeviceFlowExpiredError", { enumerable: true, get: function () { return device_flow_ts_1.DeviceFlowExpiredError; } });
|
|
17
|
+
Object.defineProperty(exports, "UnboundSessionError", { enumerable: true, get: function () { return device_flow_ts_1.UnboundSessionError; } });
|
|
18
|
+
var target_registry_ts_1 = require("./target-registry.js");
|
|
19
|
+
Object.defineProperty(exports, "addTarget", { enumerable: true, get: function () { return target_registry_ts_1.addTarget; } });
|
|
20
|
+
Object.defineProperty(exports, "removeTarget", { enumerable: true, get: function () { return target_registry_ts_1.removeTarget; } });
|
|
21
|
+
Object.defineProperty(exports, "getTarget", { enumerable: true, get: function () { return target_registry_ts_1.getTarget; } });
|
|
22
|
+
Object.defineProperty(exports, "listTargets", { enumerable: true, get: function () { return target_registry_ts_1.listTargets; } });
|
|
23
|
+
Object.defineProperty(exports, "updateTargetOrganization", { enumerable: true, get: function () { return target_registry_ts_1.updateTargetOrganization; } });
|
|
24
|
+
Object.defineProperty(exports, "validateTargetName", { enumerable: true, get: function () { return target_registry_ts_1.validateTargetName; } });
|
|
25
|
+
Object.defineProperty(exports, "normalizeAndValidateUrl", { enumerable: true, get: function () { return target_registry_ts_1.normalizeAndValidateUrl; } });
|
|
26
|
+
Object.defineProperty(exports, "TargetNameInvalidError", { enumerable: true, get: function () { return target_registry_ts_1.TargetNameInvalidError; } });
|
|
27
|
+
Object.defineProperty(exports, "TargetNameExistsError", { enumerable: true, get: function () { return target_registry_ts_1.TargetNameExistsError; } });
|
|
28
|
+
Object.defineProperty(exports, "TargetNotFoundError", { enumerable: true, get: function () { return target_registry_ts_1.TargetNotFoundError; } });
|
|
29
|
+
Object.defineProperty(exports, "TargetUrlInvalidError", { enumerable: true, get: function () { return target_registry_ts_1.TargetUrlInvalidError; } });
|
|
30
|
+
var target_session_ts_1 = require("./target-session.js");
|
|
31
|
+
Object.defineProperty(exports, "targetLogin", { enumerable: true, get: function () { return target_session_ts_1.targetLogin; } });
|
|
32
|
+
Object.defineProperty(exports, "refreshAccessToken", { enumerable: true, get: function () { return target_session_ts_1.refreshAccessToken; } });
|
|
33
|
+
Object.defineProperty(exports, "getAccessTokenProvider", { enumerable: true, get: function () { return target_session_ts_1.getAccessTokenProvider; } });
|
|
34
|
+
Object.defineProperty(exports, "revokeAndCleanup", { enumerable: true, get: function () { return target_session_ts_1.revokeAndCleanup; } });
|
|
35
|
+
Object.defineProperty(exports, "LoginRequiredError", { enumerable: true, get: function () { return target_session_ts_1.LoginRequiredError; } });
|
|
36
|
+
var discovery_ts_1 = require("./discovery.js");
|
|
37
|
+
Object.defineProperty(exports, "discoverTargetSessionEndpoints", { enumerable: true, get: function () { return discovery_ts_1.discoverTargetSessionEndpoints; } });
|
|
38
|
+
Object.defineProperty(exports, "TargetDiscoveryError", { enumerable: true, get: function () { return discovery_ts_1.TargetDiscoveryError; } });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface TargetRecord {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly adapterVersion: string;
|
|
5
|
+
readonly deviceToken: string;
|
|
6
|
+
readonly organization?: {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
};
|
|
10
|
+
readonly createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class TargetNameInvalidError extends Error {
|
|
13
|
+
constructor(name: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class TargetNameExistsError extends Error {
|
|
16
|
+
constructor(name: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class TargetNotFoundError extends Error {
|
|
19
|
+
constructor(name: string);
|
|
20
|
+
}
|
|
21
|
+
export declare class TargetUrlInvalidError extends Error {
|
|
22
|
+
constructor(url: string, reason: string);
|
|
23
|
+
}
|
|
24
|
+
export declare function validateTargetName(name: string): void;
|
|
25
|
+
export declare function normalizeAndValidateUrl(rawUrl: string): string;
|
|
26
|
+
interface SettingsWithTargets {
|
|
27
|
+
_targets?: Record<string, TargetRecord>;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface SettingsPort {
|
|
31
|
+
load(): SettingsWithTargets;
|
|
32
|
+
mutate(mutator: (settings: SettingsWithTargets) => void): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function addTarget(name: string, rawUrl: string, settings: SettingsPort): TargetRecord;
|
|
35
|
+
export declare function removeTarget(name: string, settings: SettingsPort): TargetRecord;
|
|
36
|
+
export declare function getTarget(name: string, settings: SettingsPort): TargetRecord | null;
|
|
37
|
+
export declare function listTargets(settings: SettingsPort): Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
record: TargetRecord;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function updateTargetOrganization(name: string, organization: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}, settings: SettingsPort): void;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TargetUrlInvalidError = exports.TargetNotFoundError = exports.TargetNameExistsError = exports.TargetNameInvalidError = void 0;
|
|
7
|
+
exports.validateTargetName = validateTargetName;
|
|
8
|
+
exports.normalizeAndValidateUrl = normalizeAndValidateUrl;
|
|
9
|
+
exports.addTarget = addTarget;
|
|
10
|
+
exports.removeTarget = removeTarget;
|
|
11
|
+
exports.getTarget = getTarget;
|
|
12
|
+
exports.listTargets = listTargets;
|
|
13
|
+
exports.updateTargetOrganization = updateTargetOrganization;
|
|
14
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
|
+
class TargetNameInvalidError extends Error {
|
|
16
|
+
constructor(name) {
|
|
17
|
+
super(`Invalid target name "${name}". Must be 1-64 characters, alphanumeric and hyphens only.`);
|
|
18
|
+
this.name = 'TargetNameInvalidError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.TargetNameInvalidError = TargetNameInvalidError;
|
|
22
|
+
class TargetNameExistsError extends Error {
|
|
23
|
+
constructor(name) {
|
|
24
|
+
super(`Target "${name}" already exists. Remove it first or choose a different name.`);
|
|
25
|
+
this.name = 'TargetNameExistsError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.TargetNameExistsError = TargetNameExistsError;
|
|
29
|
+
class TargetNotFoundError extends Error {
|
|
30
|
+
constructor(name) {
|
|
31
|
+
super(`Target "${name}" not found.`);
|
|
32
|
+
this.name = 'TargetNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.TargetNotFoundError = TargetNotFoundError;
|
|
36
|
+
class TargetUrlInvalidError extends Error {
|
|
37
|
+
constructor(url, reason) {
|
|
38
|
+
super(`Invalid target URL "${url}": ${reason}`);
|
|
39
|
+
this.name = 'TargetUrlInvalidError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.TargetUrlInvalidError = TargetUrlInvalidError;
|
|
43
|
+
const TARGET_NAME_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$/;
|
|
44
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
45
|
+
function validateTargetName(name) {
|
|
46
|
+
if (!TARGET_NAME_PATTERN.test(name) || name.length > 64) {
|
|
47
|
+
throw new TargetNameInvalidError(name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeAndValidateUrl(rawUrl) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(rawUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new TargetUrlInvalidError(rawUrl, 'not a valid URL');
|
|
57
|
+
}
|
|
58
|
+
if (parsed.username || parsed.password) {
|
|
59
|
+
throw new TargetUrlInvalidError(rawUrl, 'URL must not contain userinfo');
|
|
60
|
+
}
|
|
61
|
+
if (parsed.search || parsed.hash) {
|
|
62
|
+
throw new TargetUrlInvalidError(rawUrl, 'URL must not contain query or fragment');
|
|
63
|
+
}
|
|
64
|
+
const isLoopback = LOOPBACK_HOSTS.has(parsed.hostname);
|
|
65
|
+
if (parsed.protocol !== 'https:' && !isLoopback) {
|
|
66
|
+
throw new TargetUrlInvalidError(rawUrl, 'HTTPS required for non-loopback targets');
|
|
67
|
+
}
|
|
68
|
+
let normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
69
|
+
if (normalized.endsWith('/') && normalized.length > 1) {
|
|
70
|
+
normalized = normalized.slice(0, -1);
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
function addTarget(name, rawUrl, settings) {
|
|
75
|
+
validateTargetName(name);
|
|
76
|
+
const url = normalizeAndValidateUrl(rawUrl);
|
|
77
|
+
const existing = settings.load();
|
|
78
|
+
if (existing._targets?.[name]) {
|
|
79
|
+
throw new TargetNameExistsError(name);
|
|
80
|
+
}
|
|
81
|
+
const record = {
|
|
82
|
+
id: node_crypto_1.default.randomUUID(),
|
|
83
|
+
url,
|
|
84
|
+
adapterVersion: 'v1',
|
|
85
|
+
deviceToken: node_crypto_1.default.randomUUID(),
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
settings.mutate((s) => {
|
|
89
|
+
if (!s._targets) {
|
|
90
|
+
s._targets = {};
|
|
91
|
+
}
|
|
92
|
+
s._targets[name] = record;
|
|
93
|
+
});
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function removeTarget(name, settings) {
|
|
97
|
+
const existing = settings.load();
|
|
98
|
+
const record = existing._targets?.[name];
|
|
99
|
+
if (!record) {
|
|
100
|
+
throw new TargetNotFoundError(name);
|
|
101
|
+
}
|
|
102
|
+
settings.mutate((s) => {
|
|
103
|
+
if (s._targets) {
|
|
104
|
+
delete s._targets[name];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
function getTarget(name, settings) {
|
|
110
|
+
const existing = settings.load();
|
|
111
|
+
return existing._targets?.[name] ?? null;
|
|
112
|
+
}
|
|
113
|
+
function listTargets(settings) {
|
|
114
|
+
const existing = settings.load();
|
|
115
|
+
const targets = existing._targets ?? {};
|
|
116
|
+
return Object.entries(targets).map(([name, record]) => ({ name, record }));
|
|
117
|
+
}
|
|
118
|
+
function updateTargetOrganization(name, organization, settings) {
|
|
119
|
+
const existing = settings.load();
|
|
120
|
+
if (!existing._targets?.[name]) {
|
|
121
|
+
throw new TargetNotFoundError(name);
|
|
122
|
+
}
|
|
123
|
+
settings.mutate((s) => {
|
|
124
|
+
const target = s._targets?.[name];
|
|
125
|
+
if (target) {
|
|
126
|
+
s._targets[name] = {
|
|
127
|
+
...target,
|
|
128
|
+
organization,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TargetCredentialStore } from './credential-store.ts';
|
|
2
|
+
import type { TargetRecord, SettingsPort } from './target-registry.ts';
|
|
3
|
+
import { type HttpTransport, type Clock } from './device-flow.ts';
|
|
4
|
+
export declare class LoginRequiredError extends Error {
|
|
5
|
+
readonly targetName: string;
|
|
6
|
+
constructor(targetName: string);
|
|
7
|
+
}
|
|
8
|
+
export interface BrowserOpener {
|
|
9
|
+
open(url: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface TargetSessionDeps {
|
|
12
|
+
readonly http: HttpTransport;
|
|
13
|
+
readonly clock: Clock;
|
|
14
|
+
readonly browserOpener: BrowserOpener;
|
|
15
|
+
readonly stderr: {
|
|
16
|
+
write(s: string): void;
|
|
17
|
+
};
|
|
18
|
+
readonly discoveryEndpoints: {
|
|
19
|
+
readonly deviceAuthorizationEndpoint: string;
|
|
20
|
+
readonly tokenEndpoint: string;
|
|
21
|
+
readonly revocationEndpoint?: string;
|
|
22
|
+
readonly clientId: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function targetLogin(targetName: string, target: TargetRecord, credentialStore: TargetCredentialStore, acquireLock: () => Promise<() => Promise<void>>, settings: SettingsPort, deps: TargetSessionDeps): Promise<{
|
|
26
|
+
organization: {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
export declare function refreshAccessToken(targetName: string, target: TargetRecord, credentialStore: TargetCredentialStore, acquireLock: () => Promise<() => Promise<void>>, deps: Pick<TargetSessionDeps, 'http' | 'discoveryEndpoints'>): Promise<{
|
|
32
|
+
accessToken: string;
|
|
33
|
+
expiresIn: number;
|
|
34
|
+
}>;
|
|
35
|
+
export interface TargetAccessTokenProvider {
|
|
36
|
+
getAccessToken(signal?: AbortSignal): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
export declare function getAccessTokenProvider(targetName: string, target: TargetRecord, credentialStore: TargetCredentialStore, acquireLock: () => Promise<() => Promise<void>>, deps: Pick<TargetSessionDeps, 'http' | 'discoveryEndpoints'>, clock?: Clock): TargetAccessTokenProvider;
|
|
39
|
+
export declare function revokeAndCleanup(target: TargetRecord, credentialStore: TargetCredentialStore, acquireLock: () => Promise<() => Promise<void>>, deps: Pick<TargetSessionDeps, 'http' | 'discoveryEndpoints'>, force: boolean): Promise<void>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoginRequiredError = void 0;
|
|
4
|
+
exports.targetLogin = targetLogin;
|
|
5
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
6
|
+
exports.getAccessTokenProvider = getAccessTokenProvider;
|
|
7
|
+
exports.revokeAndCleanup = revokeAndCleanup;
|
|
8
|
+
const device_flow_ts_1 = require("./device-flow.js");
|
|
9
|
+
const credential_store_ts_1 = require("./credential-store.js");
|
|
10
|
+
const target_registry_ts_1 = require("./target-registry.js");
|
|
11
|
+
class LoginRequiredError extends Error {
|
|
12
|
+
targetName;
|
|
13
|
+
constructor(targetName) {
|
|
14
|
+
super(`Login required. Run: zeroshot target login ${targetName}`);
|
|
15
|
+
this.name = 'LoginRequiredError';
|
|
16
|
+
this.targetName = targetName;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.LoginRequiredError = LoginRequiredError;
|
|
20
|
+
async function targetLogin(targetName, target, credentialStore, acquireLock, settings, deps) {
|
|
21
|
+
const { http, clock, browserOpener, stderr, discoveryEndpoints } = deps;
|
|
22
|
+
const { deviceAuthorizationEndpoint, tokenEndpoint, clientId } = discoveryEndpoints;
|
|
23
|
+
const codeResponse = await (0, device_flow_ts_1.requestDeviceCode)(deviceAuthorizationEndpoint, clientId, http);
|
|
24
|
+
stderr.write(`\nOpen this URL to authorize:\n ${codeResponse.verification_uri}\n\nEnter code: ${codeResponse.user_code}\n\n`);
|
|
25
|
+
if (codeResponse.verification_uri_complete) {
|
|
26
|
+
try {
|
|
27
|
+
await browserOpener.open(codeResponse.verification_uri_complete);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Browser open is best-effort
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const tokenResponse = await (0, device_flow_ts_1.pollForToken)(tokenEndpoint, clientId, codeResponse.device_code, codeResponse.interval, codeResponse.expires_in, http, clock);
|
|
34
|
+
if (!tokenResponse.organization) {
|
|
35
|
+
throw new device_flow_ts_1.UnboundSessionError(codeResponse.verification_uri);
|
|
36
|
+
}
|
|
37
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
38
|
+
const release = await acquireLock();
|
|
39
|
+
try {
|
|
40
|
+
await credentialStore.set(serviceKey, credential_store_ts_1.TARGET_ACCOUNT, tokenResponse.refresh_token);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await release();
|
|
44
|
+
}
|
|
45
|
+
(0, target_registry_ts_1.updateTargetOrganization)(targetName, tokenResponse.organization, settings);
|
|
46
|
+
return { organization: tokenResponse.organization };
|
|
47
|
+
}
|
|
48
|
+
async function refreshAccessToken(targetName, target, credentialStore, acquireLock, deps) {
|
|
49
|
+
const { http, discoveryEndpoints } = deps;
|
|
50
|
+
const { tokenEndpoint, revocationEndpoint, clientId } = discoveryEndpoints;
|
|
51
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
52
|
+
const release = await acquireLock();
|
|
53
|
+
try {
|
|
54
|
+
const currentRefreshToken = await credentialStore.get(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
55
|
+
if (!currentRefreshToken) {
|
|
56
|
+
throw new LoginRequiredError(targetName);
|
|
57
|
+
}
|
|
58
|
+
const body = new URLSearchParams({
|
|
59
|
+
grant_type: 'refresh_token',
|
|
60
|
+
refresh_token: currentRefreshToken,
|
|
61
|
+
client_id: clientId,
|
|
62
|
+
});
|
|
63
|
+
let tokenResponse;
|
|
64
|
+
const response = await http.fetch(tokenEndpoint, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
67
|
+
body: body.toString(),
|
|
68
|
+
redirect: 'error',
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorBody = (await response.json().catch(() => ({ error: 'unknown' })));
|
|
72
|
+
if (errorBody.error === 'invalid_grant') {
|
|
73
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
74
|
+
throw new LoginRequiredError(targetName);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Token refresh failed (${response.status}): ${errorBody.error}`);
|
|
77
|
+
}
|
|
78
|
+
tokenResponse = (await response.json());
|
|
79
|
+
try {
|
|
80
|
+
await credentialStore.set(serviceKey, credential_store_ts_1.TARGET_ACCOUNT, tokenResponse.refresh_token);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
await bestEffortRevoke(tokenResponse.refresh_token, revocationEndpoint, clientId, http);
|
|
84
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
85
|
+
throw new LoginRequiredError(targetName);
|
|
86
|
+
}
|
|
87
|
+
return { accessToken: tokenResponse.access_token, expiresIn: tokenResponse.expires_in };
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function bestEffortRevoke(token, revocationEndpoint, clientId, http) {
|
|
94
|
+
if (!revocationEndpoint)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
const body = new URLSearchParams({
|
|
98
|
+
token,
|
|
99
|
+
client_id: clientId,
|
|
100
|
+
token_type_hint: 'refresh_token',
|
|
101
|
+
});
|
|
102
|
+
await http.fetch(revocationEndpoint, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
105
|
+
body: body.toString(),
|
|
106
|
+
redirect: 'error',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getAccessTokenProvider(targetName, target, credentialStore, acquireLock, deps, clock = { now: () => Date.now() }) {
|
|
114
|
+
let cachedToken = null;
|
|
115
|
+
let expiresAt = 0;
|
|
116
|
+
return {
|
|
117
|
+
async getAccessToken(_signal) {
|
|
118
|
+
if (cachedToken && clock.now() < expiresAt - 30_000) {
|
|
119
|
+
return cachedToken;
|
|
120
|
+
}
|
|
121
|
+
const result = await refreshAccessToken(targetName, target, credentialStore, acquireLock, deps);
|
|
122
|
+
cachedToken = result.accessToken;
|
|
123
|
+
expiresAt = clock.now() + result.expiresIn * 1000;
|
|
124
|
+
return result.accessToken;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function revokeAndCleanup(target, credentialStore, acquireLock, deps, force) {
|
|
129
|
+
const { http, discoveryEndpoints } = deps;
|
|
130
|
+
const { revocationEndpoint, clientId } = discoveryEndpoints;
|
|
131
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
132
|
+
const release = await acquireLock();
|
|
133
|
+
try {
|
|
134
|
+
const refreshToken = await credentialStore.get(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
135
|
+
if (refreshToken && revocationEndpoint) {
|
|
136
|
+
const body = new URLSearchParams({
|
|
137
|
+
token: refreshToken,
|
|
138
|
+
client_id: clientId,
|
|
139
|
+
token_type_hint: 'refresh_token',
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
const response = await http.fetch(revocationEndpoint, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
145
|
+
body: body.toString(),
|
|
146
|
+
redirect: 'error',
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok && !force) {
|
|
149
|
+
throw new Error(`Remote revocation failed (${response.status}). Use --force to remove anyway.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (!force)
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await release();
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.28.0",
|
|
4
4
|
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"pretest": "npm run build:agent-cli-provider",
|
|
32
|
+
"pretest": "npm run build:agent-cli-provider && npm run build:target",
|
|
33
33
|
"test": "node tests/run-tests.js",
|
|
34
34
|
"test:unit": "node tests/run-tests.js",
|
|
35
35
|
"test:e2e": "mocha 'tests/e2e/**/*.test.js' --timeout 120000",
|
|
@@ -40,21 +40,25 @@
|
|
|
40
40
|
"test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
|
|
41
41
|
"postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
|
|
42
42
|
"start": "node cli/index.js",
|
|
43
|
-
"typecheck": "tsc --noEmit && npm run typecheck:cluster && npm run typecheck:hosted-target",
|
|
43
|
+
"typecheck": "tsc --noEmit && npm run typecheck:cluster && npm run typecheck:hosted-target && npm run typecheck:hosted-session && npm run typecheck:target",
|
|
44
44
|
"typecheck:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.json",
|
|
45
45
|
"typecheck:hosted-target": "tsc --project tsconfig.hosted-target.json",
|
|
46
|
+
"typecheck:hosted-session": "tsc --project tsconfig.hosted-session.json",
|
|
46
47
|
"test:hosted-target": "node --test tests/hosted-target/*.test.ts",
|
|
48
|
+
"test:target": "npm run build:agent-cli-provider && npm run build:target && node --test tests/target/*.test.ts",
|
|
49
|
+
"typecheck:target": "tsc --project tsconfig.target.json",
|
|
47
50
|
"typecheck:cluster": "tsc --project tsconfig.cluster.json",
|
|
48
51
|
"lint:agent-cli-provider": "eslint \"src/agent-cli-provider/**/*.ts\" \"tests/agent-cli-provider/**/*.ts\"",
|
|
49
52
|
"build:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.build.json",
|
|
50
|
-
"build:
|
|
53
|
+
"build:target": "tsc --project tsconfig.target.build.json",
|
|
54
|
+
"build:cluster": "npm run protocol:generate && tsc --project tsconfig.cluster.cjs.json && tsc --project tsconfig.cluster.esm.json && tsc --project tsconfig.hosted-session.cjs.json && tsc --project tsconfig.hosted-session.esm.json && node scripts/build-cluster.js",
|
|
51
55
|
"test:agent-cli-provider": "node --test tests/agent-cli-provider/*.test.js",
|
|
52
56
|
"test:omp": "npm run build:agent-cli-provider && node --test tests/agent-cli-provider/omp-*.test.js && node tests/run-tests.js tests/omp-rpc-watcher.test.js tests/omp-isolation-capability.test.js tests/omp-docker-auth.test.js tests/omp-docker-fresh-only.test.js",
|
|
53
57
|
"test:providers:live": "npm run build:agent-cli-provider && node scripts/live-provider-smoke.js",
|
|
54
58
|
"check:agent-cli-provider": "npm run check:agent-cli-provider:ci",
|
|
55
59
|
"check:agent-cli-provider:ci": "npm run typecheck:agent-cli-provider && npm run lint:agent-cli-provider && npm run build:agent-cli-provider && npm run test:agent-cli-provider",
|
|
56
|
-
"test:cluster-client": "npm run build:cluster && node --test tests/cluster/client.test.js tests/cluster/parity.test.js tests/cluster/architecture.test.js tests/cluster/verifier-regressions.test.js tests/cluster/request-validation.test.js",
|
|
57
|
-
"test:cluster-package": "npm run build:cluster && node --test tests/cluster/package.test.js",
|
|
60
|
+
"test:cluster-client": "npm run build:cluster && node --test tests/cluster/client.test.js tests/cluster/parity.test.js tests/cluster/architecture.test.js tests/cluster/verifier-regressions.test.js tests/cluster/request-validation.test.js tests/hosted-session/coordinator.test.js",
|
|
61
|
+
"test:cluster-package": "npm run build:agent-cli-provider && npm run build:cluster && npm run build:target && node --test tests/cluster/package.test.js",
|
|
58
62
|
"dev:link": "npm link",
|
|
59
63
|
"lint": "eslint .",
|
|
60
64
|
"lint:fix": "eslint . --fix",
|
|
@@ -76,9 +80,9 @@
|
|
|
76
80
|
"release:preflight": "node scripts/release-preflight.js",
|
|
77
81
|
"release:recover": "node scripts/release-recovery.js",
|
|
78
82
|
"release:assert-published": "node scripts/assert-release-published.js",
|
|
79
|
-
"prepack": "npm run build:cluster",
|
|
83
|
+
"prepack": "npm run build:cluster && npm run build:target",
|
|
80
84
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run check:agent-cli-provider:ci",
|
|
81
|
-
"prepare": "npm run build:agent-cli-provider && husky"
|
|
85
|
+
"prepare": "npm run build:agent-cli-provider && npm run build:target && husky"
|
|
82
86
|
},
|
|
83
87
|
"c8": {
|
|
84
88
|
"reporter": [
|
|
@@ -132,6 +136,12 @@
|
|
|
132
136
|
"require": "./lib/cluster/index.cjs",
|
|
133
137
|
"default": "./lib/cluster/index.cjs"
|
|
134
138
|
},
|
|
139
|
+
"./hosted-session": {
|
|
140
|
+
"types": "./lib/hosted-session/index.d.ts",
|
|
141
|
+
"import": "./lib/hosted-session/index.mjs",
|
|
142
|
+
"require": "./lib/hosted-session/index.cjs",
|
|
143
|
+
"default": "./lib/hosted-session/index.cjs"
|
|
144
|
+
},
|
|
135
145
|
"./package.json": "./package.json",
|
|
136
146
|
"./*": "./*"
|
|
137
147
|
},
|
|
@@ -160,9 +170,11 @@
|
|
|
160
170
|
"node-pty": "^1.1.0",
|
|
161
171
|
"omelette": "^0.4.17",
|
|
162
172
|
"pidusage": "^4.0.1",
|
|
173
|
+
"open": "^10.1.0",
|
|
163
174
|
"proper-lockfile": "^4.1.2"
|
|
164
175
|
},
|
|
165
176
|
"optionalDependencies": {
|
|
177
|
+
"@napi-rs/keyring": "^1.1.6",
|
|
166
178
|
"ws": "^8.18.3"
|
|
167
179
|
},
|
|
168
180
|
"devDependencies": {
|