@the-open-engine/zeroshot 6.27.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/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 +11 -6
- 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,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,14 +40,17 @@
|
|
|
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 && npm run typecheck:hosted-session",
|
|
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
46
|
"typecheck:hosted-session": "tsc --project tsconfig.hosted-session.json",
|
|
47
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",
|
|
48
50
|
"typecheck:cluster": "tsc --project tsconfig.cluster.json",
|
|
49
51
|
"lint:agent-cli-provider": "eslint \"src/agent-cli-provider/**/*.ts\" \"tests/agent-cli-provider/**/*.ts\"",
|
|
50
52
|
"build:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.build.json",
|
|
53
|
+
"build:target": "tsc --project tsconfig.target.build.json",
|
|
51
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",
|
|
52
55
|
"test:agent-cli-provider": "node --test tests/agent-cli-provider/*.test.js",
|
|
53
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",
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
"check:agent-cli-provider": "npm run check:agent-cli-provider:ci",
|
|
56
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",
|
|
57
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",
|
|
58
|
-
"test:cluster-package": "npm run build:agent-cli-provider && npm run build:cluster && node --test tests/cluster/package.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",
|
|
59
62
|
"dev:link": "npm link",
|
|
60
63
|
"lint": "eslint .",
|
|
61
64
|
"lint:fix": "eslint . --fix",
|
|
@@ -77,9 +80,9 @@
|
|
|
77
80
|
"release:preflight": "node scripts/release-preflight.js",
|
|
78
81
|
"release:recover": "node scripts/release-recovery.js",
|
|
79
82
|
"release:assert-published": "node scripts/assert-release-published.js",
|
|
80
|
-
"prepack": "npm run build:cluster",
|
|
83
|
+
"prepack": "npm run build:cluster && npm run build:target",
|
|
81
84
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run check:agent-cli-provider:ci",
|
|
82
|
-
"prepare": "npm run build:agent-cli-provider && husky"
|
|
85
|
+
"prepare": "npm run build:agent-cli-provider && npm run build:target && husky"
|
|
83
86
|
},
|
|
84
87
|
"c8": {
|
|
85
88
|
"reporter": [
|
|
@@ -167,9 +170,11 @@
|
|
|
167
170
|
"node-pty": "^1.1.0",
|
|
168
171
|
"omelette": "^0.4.17",
|
|
169
172
|
"pidusage": "^4.0.1",
|
|
173
|
+
"open": "^10.1.0",
|
|
170
174
|
"proper-lockfile": "^4.1.2"
|
|
171
175
|
},
|
|
172
176
|
"optionalDependencies": {
|
|
177
|
+
"@napi-rs/keyring": "^1.1.6",
|
|
173
178
|
"ws": "^8.18.3"
|
|
174
179
|
},
|
|
175
180
|
"devDependencies": {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
// @ts-expect-error no declaration file for proper-lockfile
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
6
|
+
|
|
7
|
+
const LOCK_STALE_MS = 10_000;
|
|
8
|
+
const LOCK_RETRIES = 100;
|
|
9
|
+
const LOCK_RETRY_MIN_TIMEOUT_MS = 50;
|
|
10
|
+
const LOCK_RETRY_MAX_TIMEOUT_MS = 5_000;
|
|
11
|
+
|
|
12
|
+
export async function acquireTargetLock(targetId: string): Promise<() => Promise<void>> {
|
|
13
|
+
const lockDir = path.join(os.homedir(), '.zeroshot');
|
|
14
|
+
await fs.mkdir(lockDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const lockTarget = path.join(lockDir, `target-${targetId}.lock`);
|
|
17
|
+
try {
|
|
18
|
+
await fs.writeFile(lockTarget, '', { flag: 'wx' });
|
|
19
|
+
} catch (err: unknown) {
|
|
20
|
+
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const release = await lockfile.lock(lockTarget, {
|
|
24
|
+
stale: LOCK_STALE_MS,
|
|
25
|
+
retries: {
|
|
26
|
+
retries: LOCK_RETRIES,
|
|
27
|
+
minTimeout: LOCK_RETRY_MIN_TIMEOUT_MS,
|
|
28
|
+
maxTimeout: LOCK_RETRY_MAX_TIMEOUT_MS,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return async () => {
|
|
33
|
+
await release();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export class CredentialStoreUnavailableError extends Error {
|
|
2
|
+
constructor(message?: string) {
|
|
3
|
+
super(
|
|
4
|
+
message ??
|
|
5
|
+
'OS secure store unavailable. Install libsecret (Linux), or run on macOS/Windows. No plaintext fallback.',
|
|
6
|
+
);
|
|
7
|
+
this.name = 'CredentialStoreUnavailableError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TargetCredentialStore {
|
|
12
|
+
get(service: string, account: string): Promise<string | null>;
|
|
13
|
+
set(service: string, account: string, token: string): Promise<void>;
|
|
14
|
+
delete(service: string, account: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function targetServiceKey(targetId: string): string {
|
|
18
|
+
return `zeroshot-target-${targetId}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TARGET_ACCOUNT = 'refresh-token';
|
|
22
|
+
|
|
23
|
+
export class KeyringCredentialStore implements TargetCredentialStore {
|
|
24
|
+
private readonly Entry: new (service: string, account: string) => {
|
|
25
|
+
getPassword(): string;
|
|
26
|
+
setPassword(password: string): void;
|
|
27
|
+
deletePassword(): void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
private constructor(
|
|
31
|
+
Entry: new (service: string, account: string) => {
|
|
32
|
+
getPassword(): string;
|
|
33
|
+
setPassword(password: string): void;
|
|
34
|
+
deletePassword(): void;
|
|
35
|
+
},
|
|
36
|
+
) {
|
|
37
|
+
this.Entry = Entry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async create(): Promise<KeyringCredentialStore> {
|
|
41
|
+
let keyringModule: { Entry: new (service: string, account: string) => {
|
|
42
|
+
getPassword(): string;
|
|
43
|
+
setPassword(password: string): void;
|
|
44
|
+
deletePassword(): void;
|
|
45
|
+
} };
|
|
46
|
+
try {
|
|
47
|
+
keyringModule = await import('@napi-rs/keyring') as typeof keyringModule;
|
|
48
|
+
} catch {
|
|
49
|
+
throw new CredentialStoreUnavailableError();
|
|
50
|
+
}
|
|
51
|
+
if (!keyringModule.Entry) {
|
|
52
|
+
throw new CredentialStoreUnavailableError();
|
|
53
|
+
}
|
|
54
|
+
return new KeyringCredentialStore(keyringModule.Entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async get(service: string, account: string): Promise<string | null> {
|
|
58
|
+
try {
|
|
59
|
+
const entry = new this.Entry(service, account);
|
|
60
|
+
return entry.getPassword();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async set(service: string, account: string, token: string): Promise<void> {
|
|
67
|
+
const entry = new this.Entry(service, account);
|
|
68
|
+
entry.setPassword(token);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async delete(service: string, account: string): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
const entry = new this.Entry(service, account);
|
|
74
|
+
entry.deletePassword();
|
|
75
|
+
} catch {
|
|
76
|
+
// Already deleted or not present
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class FakeCredentialStore implements TargetCredentialStore {
|
|
82
|
+
private readonly store = new Map<string, string>();
|
|
83
|
+
|
|
84
|
+
private key(service: string, account: string): string {
|
|
85
|
+
return `${service}::${account}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async get(service: string, account: string): Promise<string | null> {
|
|
89
|
+
return this.store.get(this.key(service, account)) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async set(service: string, account: string, token: string): Promise<void> {
|
|
93
|
+
this.store.set(this.key(service, account), token);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async delete(service: string, account: string): Promise<void> {
|
|
97
|
+
this.store.delete(this.key(service, account));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
has(service: string, account: string): boolean {
|
|
101
|
+
return this.store.has(this.key(service, account));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
clear(): void {
|
|
105
|
+
this.store.clear();
|
|
106
|
+
}
|
|
107
|
+
}
|