@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,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HostedSessionCoordinator = void 0;
|
|
4
|
+
const index_js_1 = require("../cluster/index.cjs");
|
|
5
|
+
function combineSignals(signals) {
|
|
6
|
+
const defined = signals.filter((s) => s !== undefined);
|
|
7
|
+
if (defined.length === 0)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (defined.length === 1)
|
|
10
|
+
return defined[0];
|
|
11
|
+
return AbortSignal.any(defined);
|
|
12
|
+
}
|
|
13
|
+
class HostedSessionCoordinator {
|
|
14
|
+
#getAccess;
|
|
15
|
+
#connectOptions;
|
|
16
|
+
#clock;
|
|
17
|
+
#closeController = new AbortController();
|
|
18
|
+
#referenceCapabilities;
|
|
19
|
+
#closed = false;
|
|
20
|
+
constructor(init) {
|
|
21
|
+
this.#getAccess = init.getAccess;
|
|
22
|
+
this.#connectOptions = init.connectOptions;
|
|
23
|
+
this.#clock = init.clock ?? Date;
|
|
24
|
+
}
|
|
25
|
+
async open(signal) {
|
|
26
|
+
this.#requireNotClosed();
|
|
27
|
+
const session = await this.#createSession(signal);
|
|
28
|
+
this.#referenceCapabilities = session.initializeResult.capabilities;
|
|
29
|
+
return session;
|
|
30
|
+
}
|
|
31
|
+
async replace(signal) {
|
|
32
|
+
this.#requireNotClosed();
|
|
33
|
+
const session = await this.#createSession(signal);
|
|
34
|
+
this.#verifyCapabilities(session.initializeResult.capabilities, session);
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
renewalDeadline(access, receivedAt) {
|
|
38
|
+
const expiresAt = Date.parse(access.expiresAt);
|
|
39
|
+
if (Number.isNaN(expiresAt)) {
|
|
40
|
+
throw new index_js_1.ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
|
|
41
|
+
}
|
|
42
|
+
const lifetime = expiresAt - receivedAt;
|
|
43
|
+
return Math.min(expiresAt - 30_000, receivedAt + 0.8 * lifetime);
|
|
44
|
+
}
|
|
45
|
+
async close() {
|
|
46
|
+
this.#closed = true;
|
|
47
|
+
this.#closeController.abort();
|
|
48
|
+
}
|
|
49
|
+
async #createSession(signal) {
|
|
50
|
+
const combined = combineSignals([signal, this.#closeController.signal]);
|
|
51
|
+
const access = await this.#getAccess(combined);
|
|
52
|
+
const expiresAt = Date.parse(access.expiresAt);
|
|
53
|
+
if (Number.isNaN(expiresAt)) {
|
|
54
|
+
throw new index_js_1.ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
|
|
55
|
+
}
|
|
56
|
+
if (expiresAt <= this.#clock.now()) {
|
|
57
|
+
throw new index_js_1.ClusterConfigError('access token is already expired', 'ACCESS_EXPIRED');
|
|
58
|
+
}
|
|
59
|
+
let endpoint;
|
|
60
|
+
try {
|
|
61
|
+
endpoint = new URL(access.endpoint);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new index_js_1.ClusterConfigError('hosted access endpoint is invalid', 'INVALID_ENDPOINT');
|
|
65
|
+
}
|
|
66
|
+
if (endpoint.protocol !== 'wss:') {
|
|
67
|
+
throw new index_js_1.ClusterConfigError('hosted access endpoint must use wss', 'INSECURE_ENDPOINT');
|
|
68
|
+
}
|
|
69
|
+
return (0, index_js_1.connectInitialized)(endpoint.href, {
|
|
70
|
+
...this.#connectOptions,
|
|
71
|
+
headers: { Authorization: `Bearer ${access.token}` },
|
|
72
|
+
...(combined !== undefined ? { signal: combined } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
#verifyCapabilities(incoming, session) {
|
|
76
|
+
if (!this.#referenceCapabilities)
|
|
77
|
+
return;
|
|
78
|
+
const ref = this.#referenceCapabilities;
|
|
79
|
+
const mismatches = [];
|
|
80
|
+
if (ref.graphProfiles) {
|
|
81
|
+
const incomingProfiles = new Set(incoming.graphProfiles ?? []);
|
|
82
|
+
for (const profile of ref.graphProfiles) {
|
|
83
|
+
if (!incomingProfiles.has(profile))
|
|
84
|
+
mismatches.push(`missing graphProfile: ${profile}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (ref.logs && !incoming.logs)
|
|
88
|
+
mismatches.push('missing capability: logs');
|
|
89
|
+
if (ref.agentAttach && !incoming.agentAttach)
|
|
90
|
+
mismatches.push('missing capability: agentAttach');
|
|
91
|
+
if (mismatches.length > 0) {
|
|
92
|
+
void session.connection.close();
|
|
93
|
+
throw new index_js_1.ClusterConfigError(`replacement capabilities incompatible: ${mismatches.join(', ')}`, 'INCOMPATIBLE_CAPABILITIES');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
#requireNotClosed() {
|
|
97
|
+
if (this.#closed)
|
|
98
|
+
throw new index_js_1.ClusterConfigError('coordinator is closed', 'COORDINATOR_CLOSED');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.HostedSessionCoordinator = HostedSessionCoordinator;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AccessResponse, HostedSessionInit, InitializedSession } from './types.js';
|
|
2
|
+
export declare class HostedSessionCoordinator {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(init: HostedSessionInit);
|
|
5
|
+
open(signal?: AbortSignal): Promise<InitializedSession>;
|
|
6
|
+
replace(signal?: AbortSignal): Promise<InitializedSession>;
|
|
7
|
+
renewalDeadline(access: AccessResponse, receivedAt: number): number;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ClusterConfigError, connectInitialized } from '../cluster/index.mjs';
|
|
2
|
+
function combineSignals(signals) {
|
|
3
|
+
const defined = signals.filter((s) => s !== undefined);
|
|
4
|
+
if (defined.length === 0)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (defined.length === 1)
|
|
7
|
+
return defined[0];
|
|
8
|
+
return AbortSignal.any(defined);
|
|
9
|
+
}
|
|
10
|
+
export class HostedSessionCoordinator {
|
|
11
|
+
#getAccess;
|
|
12
|
+
#connectOptions;
|
|
13
|
+
#clock;
|
|
14
|
+
#closeController = new AbortController();
|
|
15
|
+
#referenceCapabilities;
|
|
16
|
+
#closed = false;
|
|
17
|
+
constructor(init) {
|
|
18
|
+
this.#getAccess = init.getAccess;
|
|
19
|
+
this.#connectOptions = init.connectOptions;
|
|
20
|
+
this.#clock = init.clock ?? Date;
|
|
21
|
+
}
|
|
22
|
+
async open(signal) {
|
|
23
|
+
this.#requireNotClosed();
|
|
24
|
+
const session = await this.#createSession(signal);
|
|
25
|
+
this.#referenceCapabilities = session.initializeResult.capabilities;
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
async replace(signal) {
|
|
29
|
+
this.#requireNotClosed();
|
|
30
|
+
const session = await this.#createSession(signal);
|
|
31
|
+
this.#verifyCapabilities(session.initializeResult.capabilities, session);
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
renewalDeadline(access, receivedAt) {
|
|
35
|
+
const expiresAt = Date.parse(access.expiresAt);
|
|
36
|
+
if (Number.isNaN(expiresAt)) {
|
|
37
|
+
throw new ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
|
|
38
|
+
}
|
|
39
|
+
const lifetime = expiresAt - receivedAt;
|
|
40
|
+
return Math.min(expiresAt - 30_000, receivedAt + 0.8 * lifetime);
|
|
41
|
+
}
|
|
42
|
+
async close() {
|
|
43
|
+
this.#closed = true;
|
|
44
|
+
this.#closeController.abort();
|
|
45
|
+
}
|
|
46
|
+
async #createSession(signal) {
|
|
47
|
+
const combined = combineSignals([signal, this.#closeController.signal]);
|
|
48
|
+
const access = await this.#getAccess(combined);
|
|
49
|
+
const expiresAt = Date.parse(access.expiresAt);
|
|
50
|
+
if (Number.isNaN(expiresAt)) {
|
|
51
|
+
throw new ClusterConfigError(`invalid expiresAt: ${access.expiresAt}`, 'INVALID_EXPIRY');
|
|
52
|
+
}
|
|
53
|
+
if (expiresAt <= this.#clock.now()) {
|
|
54
|
+
throw new ClusterConfigError('access token is already expired', 'ACCESS_EXPIRED');
|
|
55
|
+
}
|
|
56
|
+
let endpoint;
|
|
57
|
+
try {
|
|
58
|
+
endpoint = new URL(access.endpoint);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new ClusterConfigError('hosted access endpoint is invalid', 'INVALID_ENDPOINT');
|
|
62
|
+
}
|
|
63
|
+
if (endpoint.protocol !== 'wss:') {
|
|
64
|
+
throw new ClusterConfigError('hosted access endpoint must use wss', 'INSECURE_ENDPOINT');
|
|
65
|
+
}
|
|
66
|
+
return connectInitialized(endpoint.href, {
|
|
67
|
+
...this.#connectOptions,
|
|
68
|
+
headers: { Authorization: `Bearer ${access.token}` },
|
|
69
|
+
...(combined !== undefined ? { signal: combined } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
#verifyCapabilities(incoming, session) {
|
|
73
|
+
if (!this.#referenceCapabilities)
|
|
74
|
+
return;
|
|
75
|
+
const ref = this.#referenceCapabilities;
|
|
76
|
+
const mismatches = [];
|
|
77
|
+
if (ref.graphProfiles) {
|
|
78
|
+
const incomingProfiles = new Set(incoming.graphProfiles ?? []);
|
|
79
|
+
for (const profile of ref.graphProfiles) {
|
|
80
|
+
if (!incomingProfiles.has(profile))
|
|
81
|
+
mismatches.push(`missing graphProfile: ${profile}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (ref.logs && !incoming.logs)
|
|
85
|
+
mismatches.push('missing capability: logs');
|
|
86
|
+
if (ref.agentAttach && !incoming.agentAttach)
|
|
87
|
+
mismatches.push('missing capability: agentAttach');
|
|
88
|
+
if (mismatches.length > 0) {
|
|
89
|
+
void session.connection.close();
|
|
90
|
+
throw new ClusterConfigError(`replacement capabilities incompatible: ${mismatches.join(', ')}`, 'INCOMPATIBLE_CAPABILITIES');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
#requireNotClosed() {
|
|
94
|
+
if (this.#closed)
|
|
95
|
+
throw new ClusterConfigError('coordinator is closed', 'COORDINATOR_CLOSED');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HostedSessionCoordinator = void 0;
|
|
4
|
+
var coordinator_js_1 = require("./coordinator.cjs");
|
|
5
|
+
Object.defineProperty(exports, "HostedSessionCoordinator", { enumerable: true, get: function () { return coordinator_js_1.HostedSessionCoordinator; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HostedSessionCoordinator } from './coordinator.mjs';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Connection } from '../cluster/index.js';
|
|
2
|
+
import type { ClusterClient, ConnectOptions } from '../cluster/index.js';
|
|
3
|
+
import type { InitializeResult } from '../cluster/index.js';
|
|
4
|
+
export interface AccessResponse {
|
|
5
|
+
readonly endpoint: string;
|
|
6
|
+
readonly token: string;
|
|
7
|
+
readonly expiresAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HostedSessionInit {
|
|
10
|
+
readonly getAccess: (signal?: AbortSignal) => Promise<AccessResponse>;
|
|
11
|
+
readonly connectOptions?: Omit<ConnectOptions, 'headers' | 'signal'>;
|
|
12
|
+
readonly clock?: {
|
|
13
|
+
now(): number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface InitializedSession {
|
|
17
|
+
readonly connection: Connection;
|
|
18
|
+
readonly client: ClusterClient;
|
|
19
|
+
readonly initializeResult: InitializeResult;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function acquireTargetLock(targetId: string): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
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.acquireTargetLock = acquireTargetLock;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
// @ts-expect-error no declaration file for proper-lockfile
|
|
11
|
+
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
12
|
+
const LOCK_STALE_MS = 10_000;
|
|
13
|
+
const LOCK_RETRIES = 100;
|
|
14
|
+
const LOCK_RETRY_MIN_TIMEOUT_MS = 50;
|
|
15
|
+
const LOCK_RETRY_MAX_TIMEOUT_MS = 5_000;
|
|
16
|
+
async function acquireTargetLock(targetId) {
|
|
17
|
+
const lockDir = node_path_1.default.join(node_os_1.default.homedir(), '.zeroshot');
|
|
18
|
+
await node_fs_1.promises.mkdir(lockDir, { recursive: true });
|
|
19
|
+
const lockTarget = node_path_1.default.join(lockDir, `target-${targetId}.lock`);
|
|
20
|
+
try {
|
|
21
|
+
await node_fs_1.promises.writeFile(lockTarget, '', { flag: 'wx' });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code !== 'EEXIST')
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
const release = await proper_lockfile_1.default.lock(lockTarget, {
|
|
28
|
+
stale: LOCK_STALE_MS,
|
|
29
|
+
retries: {
|
|
30
|
+
retries: LOCK_RETRIES,
|
|
31
|
+
minTimeout: LOCK_RETRY_MIN_TIMEOUT_MS,
|
|
32
|
+
maxTimeout: LOCK_RETRY_MAX_TIMEOUT_MS,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return async () => {
|
|
36
|
+
await release();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class CredentialStoreUnavailableError extends Error {
|
|
2
|
+
constructor(message?: string);
|
|
3
|
+
}
|
|
4
|
+
export interface TargetCredentialStore {
|
|
5
|
+
get(service: string, account: string): Promise<string | null>;
|
|
6
|
+
set(service: string, account: string, token: string): Promise<void>;
|
|
7
|
+
delete(service: string, account: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function targetServiceKey(targetId: string): string;
|
|
10
|
+
export declare const TARGET_ACCOUNT = "refresh-token";
|
|
11
|
+
export declare class KeyringCredentialStore implements TargetCredentialStore {
|
|
12
|
+
private readonly Entry;
|
|
13
|
+
private constructor();
|
|
14
|
+
static create(): Promise<KeyringCredentialStore>;
|
|
15
|
+
get(service: string, account: string): Promise<string | null>;
|
|
16
|
+
set(service: string, account: string, token: string): Promise<void>;
|
|
17
|
+
delete(service: string, account: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare class FakeCredentialStore implements TargetCredentialStore {
|
|
20
|
+
private readonly store;
|
|
21
|
+
private key;
|
|
22
|
+
get(service: string, account: string): Promise<string | null>;
|
|
23
|
+
set(service: string, account: string, token: string): Promise<void>;
|
|
24
|
+
delete(service: string, account: string): Promise<void>;
|
|
25
|
+
has(service: string, account: string): boolean;
|
|
26
|
+
clear(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FakeCredentialStore = exports.KeyringCredentialStore = exports.TARGET_ACCOUNT = exports.CredentialStoreUnavailableError = void 0;
|
|
37
|
+
exports.targetServiceKey = targetServiceKey;
|
|
38
|
+
class CredentialStoreUnavailableError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message ??
|
|
41
|
+
'OS secure store unavailable. Install libsecret (Linux), or run on macOS/Windows. No plaintext fallback.');
|
|
42
|
+
this.name = 'CredentialStoreUnavailableError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.CredentialStoreUnavailableError = CredentialStoreUnavailableError;
|
|
46
|
+
function targetServiceKey(targetId) {
|
|
47
|
+
return `zeroshot-target-${targetId}`;
|
|
48
|
+
}
|
|
49
|
+
exports.TARGET_ACCOUNT = 'refresh-token';
|
|
50
|
+
class KeyringCredentialStore {
|
|
51
|
+
Entry;
|
|
52
|
+
constructor(Entry) {
|
|
53
|
+
this.Entry = Entry;
|
|
54
|
+
}
|
|
55
|
+
static async create() {
|
|
56
|
+
let keyringModule;
|
|
57
|
+
try {
|
|
58
|
+
keyringModule = await Promise.resolve().then(() => __importStar(require('@napi-rs/keyring')));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new CredentialStoreUnavailableError();
|
|
62
|
+
}
|
|
63
|
+
if (!keyringModule.Entry) {
|
|
64
|
+
throw new CredentialStoreUnavailableError();
|
|
65
|
+
}
|
|
66
|
+
return new KeyringCredentialStore(keyringModule.Entry);
|
|
67
|
+
}
|
|
68
|
+
async get(service, account) {
|
|
69
|
+
try {
|
|
70
|
+
const entry = new this.Entry(service, account);
|
|
71
|
+
return entry.getPassword();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async set(service, account, token) {
|
|
78
|
+
const entry = new this.Entry(service, account);
|
|
79
|
+
entry.setPassword(token);
|
|
80
|
+
}
|
|
81
|
+
async delete(service, account) {
|
|
82
|
+
try {
|
|
83
|
+
const entry = new this.Entry(service, account);
|
|
84
|
+
entry.deletePassword();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Already deleted or not present
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.KeyringCredentialStore = KeyringCredentialStore;
|
|
92
|
+
class FakeCredentialStore {
|
|
93
|
+
store = new Map();
|
|
94
|
+
key(service, account) {
|
|
95
|
+
return `${service}::${account}`;
|
|
96
|
+
}
|
|
97
|
+
async get(service, account) {
|
|
98
|
+
return this.store.get(this.key(service, account)) ?? null;
|
|
99
|
+
}
|
|
100
|
+
async set(service, account, token) {
|
|
101
|
+
this.store.set(this.key(service, account), token);
|
|
102
|
+
}
|
|
103
|
+
async delete(service, account) {
|
|
104
|
+
this.store.delete(this.key(service, account));
|
|
105
|
+
}
|
|
106
|
+
has(service, account) {
|
|
107
|
+
return this.store.has(this.key(service, account));
|
|
108
|
+
}
|
|
109
|
+
clear() {
|
|
110
|
+
this.store.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.FakeCredentialStore = FakeCredentialStore;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface DeviceCodeResponse {
|
|
2
|
+
readonly device_code: string;
|
|
3
|
+
readonly user_code: string;
|
|
4
|
+
readonly verification_uri: string;
|
|
5
|
+
readonly verification_uri_complete?: string;
|
|
6
|
+
readonly expires_in: number;
|
|
7
|
+
readonly interval: number;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenResponse {
|
|
10
|
+
readonly access_token: string;
|
|
11
|
+
readonly refresh_token: string;
|
|
12
|
+
readonly token_type: string;
|
|
13
|
+
readonly expires_in: number;
|
|
14
|
+
readonly organization?: {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface HttpTransport {
|
|
20
|
+
fetch(url: string, init: RequestInit & {
|
|
21
|
+
redirect: 'error';
|
|
22
|
+
}): Promise<Response>;
|
|
23
|
+
}
|
|
24
|
+
export interface Clock {
|
|
25
|
+
now(): number;
|
|
26
|
+
}
|
|
27
|
+
export declare class DeviceFlowDeniedError extends Error {
|
|
28
|
+
constructor();
|
|
29
|
+
}
|
|
30
|
+
export declare class DeviceFlowExpiredError extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
33
|
+
export declare class UnboundSessionError extends Error {
|
|
34
|
+
readonly verificationUri: string;
|
|
35
|
+
constructor(verificationUri: string);
|
|
36
|
+
}
|
|
37
|
+
export declare function requestDeviceCode(deviceAuthorizationEndpoint: string, clientId: string, http: HttpTransport, signal?: AbortSignal): Promise<DeviceCodeResponse>;
|
|
38
|
+
export declare function pollForToken(tokenEndpoint: string, clientId: string, deviceCode: string, interval: number, expiresIn: number, http: HttpTransport, clock?: Clock, signal?: AbortSignal): Promise<TokenResponse>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnboundSessionError = exports.DeviceFlowExpiredError = exports.DeviceFlowDeniedError = void 0;
|
|
4
|
+
exports.requestDeviceCode = requestDeviceCode;
|
|
5
|
+
exports.pollForToken = pollForToken;
|
|
6
|
+
class DeviceFlowDeniedError extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Device authorization denied by user');
|
|
9
|
+
this.name = 'DeviceFlowDeniedError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.DeviceFlowDeniedError = DeviceFlowDeniedError;
|
|
13
|
+
class DeviceFlowExpiredError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('Device authorization code expired');
|
|
16
|
+
this.name = 'DeviceFlowExpiredError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.DeviceFlowExpiredError = DeviceFlowExpiredError;
|
|
20
|
+
class UnboundSessionError extends Error {
|
|
21
|
+
verificationUri;
|
|
22
|
+
constructor(verificationUri) {
|
|
23
|
+
super(`Session not bound to an organization. Re-approve at ${verificationUri} and select an organization.`);
|
|
24
|
+
this.name = 'UnboundSessionError';
|
|
25
|
+
this.verificationUri = verificationUri;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.UnboundSessionError = UnboundSessionError;
|
|
29
|
+
const DEFAULT_CLOCK = { now: () => Date.now() };
|
|
30
|
+
function sleep(ms, signal) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const timer = setTimeout(resolve, ms);
|
|
37
|
+
signal?.addEventListener('abort', () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
40
|
+
}, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function requestDeviceCode(deviceAuthorizationEndpoint, clientId, http, signal) {
|
|
44
|
+
const body = new URLSearchParams({
|
|
45
|
+
client_id: clientId,
|
|
46
|
+
scope: 'openid',
|
|
47
|
+
});
|
|
48
|
+
const init = {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
51
|
+
body: body.toString(),
|
|
52
|
+
redirect: 'error',
|
|
53
|
+
};
|
|
54
|
+
if (signal)
|
|
55
|
+
init.signal = signal;
|
|
56
|
+
const response = await http.fetch(deviceAuthorizationEndpoint, init);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
throw new Error(`Device code request failed (${response.status}): ${text}`);
|
|
60
|
+
}
|
|
61
|
+
return (await response.json());
|
|
62
|
+
}
|
|
63
|
+
async function pollForToken(tokenEndpoint, clientId, deviceCode, interval, expiresIn, http, clock = DEFAULT_CLOCK, signal) {
|
|
64
|
+
const deadline = clock.now() + expiresIn * 1000;
|
|
65
|
+
let currentInterval = interval;
|
|
66
|
+
while (clock.now() < deadline) {
|
|
67
|
+
if (signal?.aborted) {
|
|
68
|
+
throw signal.reason ?? new DOMException('Aborted', 'AbortError');
|
|
69
|
+
}
|
|
70
|
+
await sleep(currentInterval * 1000, signal);
|
|
71
|
+
const body = new URLSearchParams({
|
|
72
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
73
|
+
device_code: deviceCode,
|
|
74
|
+
client_id: clientId,
|
|
75
|
+
});
|
|
76
|
+
const init = {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
79
|
+
body: body.toString(),
|
|
80
|
+
redirect: 'error',
|
|
81
|
+
};
|
|
82
|
+
if (signal)
|
|
83
|
+
init.signal = signal;
|
|
84
|
+
const response = await http.fetch(tokenEndpoint, init);
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
return (await response.json());
|
|
87
|
+
}
|
|
88
|
+
const errorBody = (await response.json());
|
|
89
|
+
switch (errorBody.error) {
|
|
90
|
+
case 'authorization_pending':
|
|
91
|
+
continue;
|
|
92
|
+
case 'slow_down':
|
|
93
|
+
currentInterval += 5;
|
|
94
|
+
continue;
|
|
95
|
+
case 'access_denied':
|
|
96
|
+
throw new DeviceFlowDeniedError();
|
|
97
|
+
case 'expired_token':
|
|
98
|
+
throw new DeviceFlowExpiredError();
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Token endpoint error: ${errorBody.error}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new DeviceFlowExpiredError();
|
|
104
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HttpTransport } from './device-flow.ts';
|
|
2
|
+
export interface TargetSessionEndpoints {
|
|
3
|
+
readonly deviceAuthorizationEndpoint: string;
|
|
4
|
+
readonly tokenEndpoint: string;
|
|
5
|
+
readonly revocationEndpoint?: string;
|
|
6
|
+
readonly clientId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class TargetDiscoveryError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function discoverTargetSessionEndpoints(targetUrl: string, http: HttpTransport): Promise<TargetSessionEndpoints>;
|