@ai-workbench/connect 0.0.0-stage → 0.7.6
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/NOTICE.md +18 -0
- package/README.md +26 -2
- package/client/index.cjs +124 -0
- package/cordis.patch.yml +3 -0
- package/helper/auth-status.cjs +18 -0
- package/helper/claude-callback.cjs +29 -0
- package/helper/main.cjs +19 -0
- package/helper/projects.cjs +57 -0
- package/helper/remote-command.cjs +15 -0
- package/helper/remote-login.cjs +25 -0
- package/helper/window.cjs +52 -0
- package/lib/api.d.ts +196 -0
- package/lib/api.js +1 -0
- package/lib/archive.d.ts +1 -0
- package/lib/archive.js +63 -0
- package/lib/auth-status.d.ts +3 -0
- package/lib/auth-status.js +18 -0
- package/lib/cache.d.ts +21 -0
- package/lib/cache.js +156 -0
- package/lib/component-download.d.ts +32 -0
- package/lib/component-download.js +76 -0
- package/lib/component-source.d.ts +52 -0
- package/lib/component-source.js +146 -0
- package/lib/components.d.ts +10 -0
- package/lib/components.js +154 -0
- package/lib/connect.d.ts +10 -0
- package/lib/connect.js +246 -0
- package/lib/cordis.d.ts +15 -0
- package/lib/cordis.js +1 -0
- package/lib/defaults.d.ts +30 -0
- package/lib/defaults.js +4 -0
- package/lib/electron.d.ts +8 -0
- package/lib/electron.js +83 -0
- package/lib/enrollment.d.ts +3 -0
- package/lib/enrollment.js +78 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.js +55 -0
- package/lib/integration.d.ts +6 -0
- package/lib/integration.js +56 -0
- package/lib/login.d.ts +35 -0
- package/lib/login.js +81 -0
- package/lib/native-import-picker.d.ts +11 -0
- package/lib/native-import-picker.js +49 -0
- package/lib/needs.d.ts +9 -0
- package/lib/needs.js +52 -0
- package/lib/network.d.ts +26 -0
- package/lib/network.js +226 -0
- package/lib/physical-fs.d.ts +3 -0
- package/lib/physical-fs.js +7 -0
- package/lib/ports.d.ts +43 -0
- package/lib/ports.js +1 -0
- package/lib/private-files.d.ts +3 -0
- package/lib/private-files.js +41 -0
- package/lib/processes.d.ts +5 -0
- package/lib/processes.js +147 -0
- package/lib/profile-activation.d.ts +32 -0
- package/lib/profile-activation.js +118 -0
- package/lib/profile-enrollment.d.ts +21 -0
- package/lib/profile-enrollment.js +275 -0
- package/lib/profile-runtime.d.ts +6 -0
- package/lib/profile-runtime.js +47 -0
- package/lib/profile-settings.d.ts +2 -0
- package/lib/profile-settings.js +13 -0
- package/lib/projects.d.ts +4 -0
- package/lib/projects.js +41 -0
- package/lib/provider.d.ts +73 -0
- package/lib/provider.js +39 -0
- package/lib/remote-login.d.ts +2 -0
- package/lib/remote-login.js +9 -0
- package/lib/ssh.d.ts +10 -0
- package/lib/ssh.js +78 -0
- package/lib/startup.d.ts +18 -0
- package/lib/startup.js +211 -0
- package/lib/workspace-bindings.d.ts +32 -0
- package/lib/workspace-bindings.js +328 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/node_modules/argparse/LICENSE +254 -0
- package/node_modules/argparse/README.md +84 -0
- package/node_modules/argparse/argparse.js +3707 -0
- package/node_modules/argparse/lib/sub.js +67 -0
- package/node_modules/argparse/lib/textwrap.js +440 -0
- package/node_modules/argparse/package.json +31 -0
- package/node_modules/js-yaml/LICENSE +21 -0
- package/node_modules/js-yaml/README.md +233 -0
- package/node_modules/js-yaml/bin/js-yaml.js +117 -0
- package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
- package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
- package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
- package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
- package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
- package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
- package/node_modules/js-yaml/index.js +44 -0
- package/node_modules/js-yaml/lib/common.js +50 -0
- package/node_modules/js-yaml/lib/dumper.js +937 -0
- package/node_modules/js-yaml/lib/exception.js +51 -0
- package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
- package/node_modules/js-yaml/lib/loader.js +1803 -0
- package/node_modules/js-yaml/lib/schema/core.js +9 -0
- package/node_modules/js-yaml/lib/schema/default.js +20 -0
- package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
- package/node_modules/js-yaml/lib/schema/json.js +17 -0
- package/node_modules/js-yaml/lib/schema.js +109 -0
- package/node_modules/js-yaml/lib/snippet.js +96 -0
- package/node_modules/js-yaml/lib/type/binary.js +122 -0
- package/node_modules/js-yaml/lib/type/bool.js +35 -0
- package/node_modules/js-yaml/lib/type/float.js +99 -0
- package/node_modules/js-yaml/lib/type/int.js +142 -0
- package/node_modules/js-yaml/lib/type/map.js +8 -0
- package/node_modules/js-yaml/lib/type/merge.js +12 -0
- package/node_modules/js-yaml/lib/type/null.js +35 -0
- package/node_modules/js-yaml/lib/type/omap.js +45 -0
- package/node_modules/js-yaml/lib/type/pairs.js +50 -0
- package/node_modules/js-yaml/lib/type/seq.js +8 -0
- package/node_modules/js-yaml/lib/type/set.js +29 -0
- package/node_modules/js-yaml/lib/type/str.js +8 -0
- package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
- package/node_modules/js-yaml/lib/type.js +66 -0
- package/node_modules/js-yaml/package.json +83 -0
- package/node_modules/pend/LICENSE +23 -0
- package/node_modules/pend/README.md +41 -0
- package/node_modules/pend/index.js +55 -0
- package/node_modules/pend/package.json +18 -0
- package/node_modules/pend/test.js +137 -0
- package/node_modules/yauzl/LICENSE +21 -0
- package/node_modules/yauzl/README.md +1015 -0
- package/node_modules/yauzl/crc32.js +45 -0
- package/node_modules/yauzl/fd-slicer.js +188 -0
- package/node_modules/yauzl/index.js +1096 -0
- package/node_modules/yauzl/package.json +40 -0
- package/package.json +89 -4
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { lstat, realpath, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
4
|
+
import { readEnrollment } from "./enrollment.js";
|
|
5
|
+
import { privateDirectory, privateFile, readPrivate } from "./private-files.js";
|
|
6
|
+
import { ok, fail, need, NeedError } from "./needs.js";
|
|
7
|
+
// Only metadata about authkey is examined here. Its contents belong exclusively to N.
|
|
8
|
+
export function createProfileEnrollment(options) {
|
|
9
|
+
const instanceId = randomUUID(), listeners = new Set(), abort = new AbortController();
|
|
10
|
+
let state = { instanceId, revision: 0, phase: 'empty' }, record, loaded = false, disposed = false;
|
|
11
|
+
const pending = new Set();
|
|
12
|
+
let disposal;
|
|
13
|
+
let loadFlight, flight;
|
|
14
|
+
const dataDir = join(options.profileDir, 'ai-workbench-connect'), file = join(dataDir, 'enrollment.json'), trustDir = join(dataDir, 'trust');
|
|
15
|
+
const snapshot = () => structuredClone(state);
|
|
16
|
+
const publish = (phase, reason) => { if (disposed)
|
|
17
|
+
return; state = { instanceId, revision: state.revision + 1, phase, ...(reason ? { need: typeof reason === 'string' ? need(reason) : structuredClone(reason) } : {}) }; options.onState?.(snapshot()); for (const fn of listeners)
|
|
18
|
+
fn(snapshot()); };
|
|
19
|
+
const validOptions = () => isAbsolute(options.profileDir) && Number.isSafeInteger(options.metadataMaxBytes) && options.metadataMaxBytes > 0 && options.metadataMaxBytes <= 2147483647;
|
|
20
|
+
const canonical = async (path) => { if (!isAbsolute(path) || await realpath(path) !== resolve(path))
|
|
21
|
+
throw Error('NON_CANONICAL_DIRECTORY'); };
|
|
22
|
+
const sourceFile = async (path) => { await privateFile(path); const stat = await lstat(path); if ((stat.mode & 0o777) !== 0o600 || stat.nlink !== 1)
|
|
23
|
+
throw Error('SOURCE_FILE_INVALID'); return stat; };
|
|
24
|
+
async function load() {
|
|
25
|
+
if (disposed)
|
|
26
|
+
return fail('DISPOSED');
|
|
27
|
+
if (loaded)
|
|
28
|
+
return ok(undefined);
|
|
29
|
+
if (loadFlight)
|
|
30
|
+
return loadFlight;
|
|
31
|
+
loadFlight = (async () => {
|
|
32
|
+
try {
|
|
33
|
+
if (!validOptions())
|
|
34
|
+
return fail('SETTINGS_REQUIRED');
|
|
35
|
+
await canonical(options.profileDir);
|
|
36
|
+
try {
|
|
37
|
+
await lstat(dataDir);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
if (disposed)
|
|
42
|
+
return fail('DISPOSED');
|
|
43
|
+
loaded = true;
|
|
44
|
+
return ok(undefined);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
await privateDirectory(dataDir);
|
|
49
|
+
await canonical(dataDir);
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(await readPrivate(file, options.metadataMaxBytes));
|
|
52
|
+
if (value.schemaVersion !== 1 || typeof value.requestId !== 'string' || !value.requestId || typeof value.source !== 'string' || !/^[a-f0-9]{64}$/.test(value.digest) || !['host', 'username', 'projectRoot'].every(k => typeof value[k] === 'string') || !Number.isInteger(value.port))
|
|
53
|
+
throw Error('REGISTRATION_INVALID');
|
|
54
|
+
record = value;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (disposed)
|
|
61
|
+
return fail('DISPOSED');
|
|
62
|
+
loaded = true;
|
|
63
|
+
if (record)
|
|
64
|
+
publish('imported');
|
|
65
|
+
return ok(undefined);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
publish('blocked', 'ENROLLMENT_INVALID');
|
|
69
|
+
return fail('ENROLLMENT_INVALID');
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
const result = await loadFlight;
|
|
73
|
+
if (!result.ok)
|
|
74
|
+
loadFlight = undefined;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
async function validate(source) {
|
|
78
|
+
try {
|
|
79
|
+
await canonical(source);
|
|
80
|
+
const d = await lstat(source);
|
|
81
|
+
if (!d.isDirectory() || d.uid !== process.getuid() || (d.mode & 0o777) !== 0o700)
|
|
82
|
+
return fail('ENROLLMENT_INVALID');
|
|
83
|
+
await sourceFile(join(source, 'enroll.json'));
|
|
84
|
+
const metadata = await readPrivate(join(source, 'enroll.json'), options.metadataMaxBytes);
|
|
85
|
+
const rule = await sourceFile(join(source, 'ssh-rule.json'));
|
|
86
|
+
if (rule.size > options.metadataMaxBytes)
|
|
87
|
+
return fail('ENROLLMENT_INVALID');
|
|
88
|
+
const value = JSON.parse(metadata), keyName = value.authKeyDelivery?.path;
|
|
89
|
+
if (typeof keyName !== 'string' || !(/^[a-zA-Z0-9._-]+$/).test(keyName) || ['.', '..'].includes(keyName))
|
|
90
|
+
return fail('ENROLLMENT_INVALID');
|
|
91
|
+
const key = join(source, keyName);
|
|
92
|
+
try {
|
|
93
|
+
if ((await sourceFile(key)).size === 0)
|
|
94
|
+
return fail('ENROLLMENT_REQUIRED');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT' || !record || !options.activate)
|
|
98
|
+
return fail(error.code === 'ENOENT' ? 'ENROLLMENT_REQUIRED' : 'ENROLLMENT_INVALID');
|
|
99
|
+
}
|
|
100
|
+
// Validate the source digest before replacing our trust cache on an existing registration.
|
|
101
|
+
const digest = createHash('sha256').update(metadata).digest('hex');
|
|
102
|
+
if (record && (record.source !== source || record.digest !== digest))
|
|
103
|
+
return fail('ENROLLMENT_INVALID');
|
|
104
|
+
if (disposed)
|
|
105
|
+
return fail('DISPOSED');
|
|
106
|
+
await privateDirectory(dataDir);
|
|
107
|
+
await canonical(dataDir);
|
|
108
|
+
if (disposed)
|
|
109
|
+
return fail('DISPOSED');
|
|
110
|
+
const normalized = await readEnrollment(source, trustDir, options.metadataMaxBytes);
|
|
111
|
+
if (!normalized.ok)
|
|
112
|
+
return normalized;
|
|
113
|
+
if (await readPrivate(join(source, 'enroll.json'), options.metadataMaxBytes) !== metadata)
|
|
114
|
+
return fail('ENROLLMENT_INVALID');
|
|
115
|
+
await canonical(source);
|
|
116
|
+
const { host, username, port, projectRoot } = normalized.value;
|
|
117
|
+
return ok({ enrollment: normalized.value, registration: { schemaVersion: 1, requestId: record?.requestId ?? randomUUID(), source, digest, host, username, port, projectRoot } });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return fail('ENROLLMENT_INVALID');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
getState: snapshot,
|
|
125
|
+
onState(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; },
|
|
126
|
+
read() {
|
|
127
|
+
const promise = (async () => {
|
|
128
|
+
if (disposed)
|
|
129
|
+
return fail('DISPOSED');
|
|
130
|
+
const ready = await load();
|
|
131
|
+
if (disposed)
|
|
132
|
+
return fail('DISPOSED');
|
|
133
|
+
if (!ready.ok)
|
|
134
|
+
return ready;
|
|
135
|
+
if (!record)
|
|
136
|
+
return fail('ENROLLMENT_REQUIRED');
|
|
137
|
+
if (!options.activate)
|
|
138
|
+
return fail('DEPENDENCY_REQUIRED');
|
|
139
|
+
const result = await validate(record.source);
|
|
140
|
+
if (disposed)
|
|
141
|
+
return fail('DISPOSED');
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
publish('blocked', result.error.code);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
if (!options.activate)
|
|
147
|
+
return fail('DEPENDENCY_REQUIRED');
|
|
148
|
+
const activated = await options.activate(result.value.enrollment, abort.signal);
|
|
149
|
+
if (disposed)
|
|
150
|
+
return fail('DISPOSED');
|
|
151
|
+
if (!activated.ok) {
|
|
152
|
+
publish('blocked', activated.error.code);
|
|
153
|
+
return activated;
|
|
154
|
+
}
|
|
155
|
+
if (state.phase !== 'imported' || state.need)
|
|
156
|
+
publish('imported');
|
|
157
|
+
return ok(result.value.enrollment);
|
|
158
|
+
})();
|
|
159
|
+
pending.add(promise);
|
|
160
|
+
void promise.then(() => pending.delete(promise), () => pending.delete(promise));
|
|
161
|
+
return promise;
|
|
162
|
+
},
|
|
163
|
+
importEnrollment() {
|
|
164
|
+
if (disposed)
|
|
165
|
+
return Promise.resolve(fail('DISPOSED'));
|
|
166
|
+
if (flight)
|
|
167
|
+
return flight;
|
|
168
|
+
const run = async () => {
|
|
169
|
+
const ready = await load();
|
|
170
|
+
if (disposed)
|
|
171
|
+
return fail('DISPOSED');
|
|
172
|
+
if (!ready.ok)
|
|
173
|
+
return ready;
|
|
174
|
+
const blocked = options.blockedByNetwork?.();
|
|
175
|
+
if (blocked) {
|
|
176
|
+
publish('blocked', blocked.code);
|
|
177
|
+
return fail(blocked.code);
|
|
178
|
+
}
|
|
179
|
+
if (options.active?.())
|
|
180
|
+
return fail('ENROLLMENT_INVALID');
|
|
181
|
+
const previous = record ? 'imported' : 'empty';
|
|
182
|
+
publish('importing');
|
|
183
|
+
let temp;
|
|
184
|
+
try {
|
|
185
|
+
const source = await options.pick(abort.signal);
|
|
186
|
+
if (disposed)
|
|
187
|
+
return fail('DISPOSED');
|
|
188
|
+
if (source === null) {
|
|
189
|
+
publish(previous);
|
|
190
|
+
return ok({ outcome: 'cancelled', state: snapshot() });
|
|
191
|
+
}
|
|
192
|
+
const blocked = options.blockedByNetwork?.();
|
|
193
|
+
if (blocked) {
|
|
194
|
+
publish('blocked', blocked.code);
|
|
195
|
+
return fail(blocked.code);
|
|
196
|
+
}
|
|
197
|
+
if (options.active?.()) {
|
|
198
|
+
publish('blocked', 'ENROLLMENT_INVALID');
|
|
199
|
+
return fail('ENROLLMENT_INVALID');
|
|
200
|
+
}
|
|
201
|
+
const result = await validate(source);
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
publish('blocked', result.error.code);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
if (disposed)
|
|
207
|
+
return fail('DISPOSED');
|
|
208
|
+
const bytes = JSON.stringify(result.value.registration) + '\n';
|
|
209
|
+
if (Buffer.byteLength(bytes) > options.metadataMaxBytes) {
|
|
210
|
+
publish('blocked', 'ENROLLMENT_INVALID');
|
|
211
|
+
return fail('ENROLLMENT_INVALID');
|
|
212
|
+
}
|
|
213
|
+
temp = join(dataDir, '.enrollment-' + randomUUID());
|
|
214
|
+
const handle = await open(temp, 'wx', 0o600);
|
|
215
|
+
try {
|
|
216
|
+
if (disposed)
|
|
217
|
+
return fail('DISPOSED');
|
|
218
|
+
await handle.writeFile(bytes);
|
|
219
|
+
if (disposed)
|
|
220
|
+
return fail('DISPOSED');
|
|
221
|
+
await handle.sync();
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await handle.close();
|
|
225
|
+
}
|
|
226
|
+
if (disposed)
|
|
227
|
+
return fail('DISPOSED');
|
|
228
|
+
await rename(temp, file);
|
|
229
|
+
temp = undefined;
|
|
230
|
+
const directory = await open(dataDir, 'r');
|
|
231
|
+
try {
|
|
232
|
+
await directory.sync();
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await directory.close();
|
|
236
|
+
}
|
|
237
|
+
if (disposed)
|
|
238
|
+
return fail('DISPOSED');
|
|
239
|
+
record = result.value.registration;
|
|
240
|
+
if (!options.activate) {
|
|
241
|
+
publish('blocked', 'DEPENDENCY_REQUIRED');
|
|
242
|
+
return fail('DEPENDENCY_REQUIRED');
|
|
243
|
+
}
|
|
244
|
+
const activated = await options.activate(result.value.enrollment, abort.signal);
|
|
245
|
+
if (disposed)
|
|
246
|
+
return fail('DISPOSED');
|
|
247
|
+
if (!activated.ok) {
|
|
248
|
+
publish('blocked', activated.error.code);
|
|
249
|
+
return activated;
|
|
250
|
+
}
|
|
251
|
+
publish('imported');
|
|
252
|
+
return ok({ outcome: 'imported', state: snapshot() });
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (disposed)
|
|
256
|
+
return fail('DISPOSED');
|
|
257
|
+
const reason = error instanceof NeedError ? structuredClone(error.need) : need('ENROLLMENT_INVALID');
|
|
258
|
+
publish('blocked', reason);
|
|
259
|
+
return { ok: false, error: reason };
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (temp)
|
|
263
|
+
await rm(temp, { force: true });
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const promise = run();
|
|
267
|
+
flight = promise;
|
|
268
|
+
void promise.finally(() => { if (flight === promise)
|
|
269
|
+
flight = undefined; }).catch(() => { });
|
|
270
|
+
return promise;
|
|
271
|
+
},
|
|
272
|
+
dispose() { if (disposal)
|
|
273
|
+
return disposal; disposed = true; abort.abort(); listeners.clear(); disposal = Promise.allSettled([...pending, ...(flight ? [flight] : []), ...(loadFlight ? [loadFlight] : [])]).then(() => { }); return disposal; },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { ConnectApi } from './api.ts';
|
|
3
|
+
import type { Ports } from './ports.ts';
|
|
4
|
+
import type { Config } from './index.ts';
|
|
5
|
+
import type { NetworkProvider } from './provider.ts';
|
|
6
|
+
export declare function attachProfileRuntime(ctx: Context, api: ConnectApi, ports: Ports, config: Config, network?: NetworkProvider): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mapNetworkNeed } from "./provider.js";
|
|
2
|
+
import { createProfileEnrollment } from "./profile-enrollment.js";
|
|
3
|
+
import { createWorkspaceBindings } from "./workspace-bindings.js";
|
|
4
|
+
import { NeedError, need, fail } from "./needs.js";
|
|
5
|
+
import { createNativeImportPicker } from "./native-import-picker.js";
|
|
6
|
+
import { L_DEFAULTS } from "./defaults.js";
|
|
7
|
+
import { createProfileActivation } from "./profile-activation.js";
|
|
8
|
+
// Composition only: consume official services; never provide replacements or alter default entries.
|
|
9
|
+
export function attachProfileRuntime(ctx, api, ports, config, network) {
|
|
10
|
+
const profile = ctx.get('profileContext');
|
|
11
|
+
if (!profile || typeof profile.dir !== 'string')
|
|
12
|
+
return;
|
|
13
|
+
const activation = createProfileActivation({ profileDir: profile.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes, network, settings: config.network });
|
|
14
|
+
const manager = createProfileEnrollment({ profileDir: profile.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes,
|
|
15
|
+
pick: createNativeImportPicker(() => ctx.get('directoryPicker')),
|
|
16
|
+
blockedByNetwork: () => { const error = network?.getState().error; return error && ['ENROLLMENT_RESULT_UNKNOWN', 'ENROLLMENT_REJECTED', 'LOCAL_STATE_INVALID', 'ALREADY_RUNNING'].includes(error.code) ? mapNetworkNeed(error) : undefined; },
|
|
17
|
+
activate: activation.activate, onState: state => ctx.emit('ai-workbench/enrollment-state', state) });
|
|
18
|
+
ports.enrollment = { read: () => manager.read() };
|
|
19
|
+
api.getEnrollmentState = manager.getState;
|
|
20
|
+
api.onEnrollmentState = manager.onState;
|
|
21
|
+
api.importEnrollment = async () => { const result = await manager.importEnrollment(); if (result.ok && result.value.outcome === 'imported')
|
|
22
|
+
await api.refreshLoginPrompts(); return result; };
|
|
23
|
+
ctx.effect(() => () => manager.dispose());
|
|
24
|
+
let workspaceRevision = 0;
|
|
25
|
+
let lastWorkspaceError;
|
|
26
|
+
ctx.provide('aiWorkbenchWorkspaceDiagnostics', { getLatest: () => lastWorkspaceError });
|
|
27
|
+
ctx.inject(['workspaceRegistry', 'sessions', 'sessionPersistence'], native => {
|
|
28
|
+
const registry = native.get('workspaceRegistry');
|
|
29
|
+
const bindings = createWorkspaceBindings({ profileDir: profile.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes, api, registry,
|
|
30
|
+
readHeader: async (id) => {
|
|
31
|
+
const live = native.get('sessions');
|
|
32
|
+
const session = live?.get(id);
|
|
33
|
+
if (session)
|
|
34
|
+
return session.header;
|
|
35
|
+
const persistence = native.get('sessionPersistence');
|
|
36
|
+
if (!persistence)
|
|
37
|
+
throw new NeedError(need('DEPENDENCY_REQUIRED'));
|
|
38
|
+
return (await persistence.list()).find(s => s.header.id === id)?.header;
|
|
39
|
+
}, nextRevision: () => ++workspaceRevision, onState: state => ctx.emit('ai-workbench/workspace-binding-state', state), diagnostic: error => { lastWorkspaceError = error; ctx.emit('ai-workbench/workspace-diagnostic', error); } });
|
|
40
|
+
native.effect(() => api.onConnectionState(bindings.observeConnection));
|
|
41
|
+
api.getWorkspaceBindingState = bindings.getWorkspaceBindingState;
|
|
42
|
+
api.onWorkspaceBindingState = bindings.onWorkspaceBindingState;
|
|
43
|
+
api.acknowledgeWorkspaceNotice = bindings.acknowledgeWorkspaceNotice;
|
|
44
|
+
api.resolveSessionProject = bindings.resolveSessionProject;
|
|
45
|
+
native.effect(() => () => { api.resolveSessionProject = async () => fail('DEPENDENCY_REQUIRED'); api.acknowledgeWorkspaceNotice = async () => fail('DEPENDENCY_REQUIRED'); return bindings.dispose(); });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isAbsolute, join } from 'node:path';
|
|
2
|
+
import { COMPONENT_SOURCES, preset } from "./component-source.js";
|
|
3
|
+
import { L_DEFAULTS, ELECTRON_PIN } from "./defaults.js";
|
|
4
|
+
// Owner approved technical values, 2026-10-20 settings decision, part two.
|
|
5
|
+
// 组件直接来自自己的官方地址;OAuth网页仍通过N主机出口。
|
|
6
|
+
export function profileSettings(config, profileDir, platform = process.platform, arch = process.arch) {
|
|
7
|
+
if (typeof profileDir !== 'string' || !isAbsolute(profileDir))
|
|
8
|
+
return config;
|
|
9
|
+
const runtime = platform === 'darwin' && arch === 'arm64' ? { ...ELECTRON_PIN, source: COMPONENT_SOURCES.npmmirror.source } : {};
|
|
10
|
+
const chosen = config.helper?.componentDownloadSource;
|
|
11
|
+
const selected = chosen === undefined ? {} : { source: preset(chosen) ? COMPONENT_SOURCES[chosen].source : '' };
|
|
12
|
+
return { ...config, commandOutputMaxBytes: config.commandOutputMaxBytes === undefined ? L_DEFAULTS.commandOutputMaxBytes : config.commandOutputMaxBytes, helper: { ...runtime, componentDownloadMaxRedirects: L_DEFAULTS.componentDownloadMaxRedirects, componentObservationIntervalMs: L_DEFAULTS.componentObservationIntervalMs, maxDownloadBytes: L_DEFAULTS.maxDownloadBytes, maxExtractedBytes: L_DEFAULTS.maxExtractedBytes, loginOutputMaxBytes: L_DEFAULTS.loginOutputMaxBytes, ipcMaxBytes: L_DEFAULTS.ipcMaxBytes, remoteKillGraceMs: L_DEFAULTS.remoteKillGraceMs, ...config.helper, ...selected, cacheDir: config.helper?.cacheDir === undefined ? join(profileDir, 'ai-workbench-connect', 'login-component-cache') : config.helper.cacheDir } };
|
|
13
|
+
}
|
package/lib/projects.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { runRemote } from "./ssh.js";
|
|
3
|
+
import { fail, ok } from "./needs.js";
|
|
4
|
+
export function isProjectName(name) { return typeof name === 'string' && name !== '' && name !== '.' && name !== '..' && !/[\\/\x00-\x1f\x7f]/.test(name); }
|
|
5
|
+
const codes = ['PROJECT_ROOT_INVALID', 'PROJECT_EXISTS', 'PROJECT_INIT_FAILED', 'PROJECT_CREATE_FAILED', 'PROJECT_LIST_FAILED', 'PROJECT_NAME_INVALID'];
|
|
6
|
+
const reasons = new Set(['invalid-name', 'symlink', 'not-directory', 'permissions', 'not-git', 'invalid-git', 'outside-root']);
|
|
7
|
+
function validProject(value, root) {
|
|
8
|
+
return typeof value === 'object' && value !== null && 'name' in value && typeof value.name === 'string' && isProjectName(value.name) && 'path' in value && value.path === root + '/' + value.name;
|
|
9
|
+
}
|
|
10
|
+
export function createProjects(runner) {
|
|
11
|
+
const request = async (t, operation, signal, name) => {
|
|
12
|
+
if (operation === 'create' && !isProjectName(name))
|
|
13
|
+
return fail('PROJECT_NAME_INVALID');
|
|
14
|
+
try {
|
|
15
|
+
const source = await readFile(new URL('../helper/projects.cjs', import.meta.url), 'utf8');
|
|
16
|
+
const command = 'node -e ' + "'" + (source + '\nmain();').replaceAll("'", "'\\''") + "'";
|
|
17
|
+
const raw = await runRemote(runner, t, command, signal, JSON.stringify({ protocolVersion: 1, operation, root: t.projectRoot, ...(operation === 'create' ? { name } : {}) }));
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
return fail('CONNECTION_LOST');
|
|
20
|
+
if (raw.exitCode !== 0)
|
|
21
|
+
return fail(operation === 'list' ? 'PROJECT_LIST_FAILED' : 'PROJECT_CREATE_FAILED');
|
|
22
|
+
const result = JSON.parse(raw.stdout);
|
|
23
|
+
if (result.protocolVersion !== 1 || typeof result.ok !== 'boolean')
|
|
24
|
+
return fail('PROJECT_PROTOCOL_INVALID');
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
const code = codes.find(code => code === result.error?.code);
|
|
27
|
+
return fail(code ?? 'PROJECT_PROTOCOL_INVALID');
|
|
28
|
+
}
|
|
29
|
+
if (operation === 'create')
|
|
30
|
+
return validProject(result.value, t.projectRoot) && result.value.name === name ? ok(result.value) : fail('PROJECT_PROTOCOL_INVALID');
|
|
31
|
+
const list = result.value;
|
|
32
|
+
if (!list || !Array.isArray(list.projects) || !Array.isArray(list.unavailable) || !list.projects.every((p) => validProject(p, t.projectRoot)) || !list.unavailable.every((p) => p && typeof p.name === 'string' && reasons.has(p.reason)))
|
|
33
|
+
return fail('PROJECT_PROTOCOL_INVALID');
|
|
34
|
+
return ok(list);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return fail(operation === 'list' ? 'PROJECT_LIST_FAILED' : 'PROJECT_CREATE_FAILED');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return { list: (t, s) => request(t, 'list', s), create: (t, n, s) => request(t, 'create', s, n) };
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Ports } from './ports.ts';
|
|
2
|
+
import type { ProxyLease } from './login.ts';
|
|
3
|
+
interface NNeed {
|
|
4
|
+
code: string;
|
|
5
|
+
}
|
|
6
|
+
type NResult<T> = {
|
|
7
|
+
ok: true;
|
|
8
|
+
value: T;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: NNeed;
|
|
12
|
+
};
|
|
13
|
+
export interface ActivationSettings {
|
|
14
|
+
configFile: string;
|
|
15
|
+
helperLogFile: string;
|
|
16
|
+
startupObservationSeconds: number;
|
|
17
|
+
shutdownObservationSeconds: number;
|
|
18
|
+
controlResponseMaxBytes: number;
|
|
19
|
+
downloadMaxRedirects: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ActivationState {
|
|
22
|
+
instanceId: string;
|
|
23
|
+
revision: number;
|
|
24
|
+
phase: 'unconfigured' | 'activating' | 'active' | 'blocked' | 'stopped';
|
|
25
|
+
receipt: {
|
|
26
|
+
apiVersion: 1;
|
|
27
|
+
session: 'key' | 'identity';
|
|
28
|
+
key: 'not-required' | 'unattempted' | 'attempted' | 'confirmed' | 'unknown';
|
|
29
|
+
identity: 'unconfirmed' | 'resumable';
|
|
30
|
+
cleanup: 'not-required' | 'pending' | 'complete';
|
|
31
|
+
} | null;
|
|
32
|
+
error: NNeed | null;
|
|
33
|
+
}
|
|
34
|
+
export interface NetworkProvider {
|
|
35
|
+
activate?(settings: ActivationSettings, signal: AbortSignal): Promise<NResult<ActivationState>>;
|
|
36
|
+
getActivationState?(): ActivationState;
|
|
37
|
+
onActivationState?(listener: (state: ActivationState) => void): () => void;
|
|
38
|
+
getState(): {
|
|
39
|
+
phase: string;
|
|
40
|
+
error?: NNeed;
|
|
41
|
+
status?: {
|
|
42
|
+
host: {
|
|
43
|
+
lastSeenAt: string | null;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
connect(target: {
|
|
48
|
+
host: string;
|
|
49
|
+
username: string;
|
|
50
|
+
port: number;
|
|
51
|
+
knownHostsFile: string;
|
|
52
|
+
}, signal: AbortSignal): Promise<NResult<{
|
|
53
|
+
proxyCommand: string;
|
|
54
|
+
}>>;
|
|
55
|
+
onState(listener: (event: {
|
|
56
|
+
kind: 'lost' | 'restored';
|
|
57
|
+
}) => void): () => void;
|
|
58
|
+
openLoginProxy(signal: AbortSignal): Promise<NResult<{
|
|
59
|
+
credentials: {
|
|
60
|
+
url: string;
|
|
61
|
+
username: string;
|
|
62
|
+
password: string;
|
|
63
|
+
};
|
|
64
|
+
release(): Promise<void>;
|
|
65
|
+
}>>;
|
|
66
|
+
download(source: string, signal: AbortSignal): Promise<NResult<Response>>;
|
|
67
|
+
}
|
|
68
|
+
export declare function mapNetworkNeed(value: NNeed): import("./api.ts").ConnectNeed;
|
|
69
|
+
export declare function adaptNetwork(provider: NetworkProvider): NonNullable<Ports['network']> & {
|
|
70
|
+
acquireProxy(signal: AbortSignal): Promise<ProxyLease>;
|
|
71
|
+
download(source: string, signal: AbortSignal): Promise<Response>;
|
|
72
|
+
};
|
|
73
|
+
export {};
|
package/lib/provider.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NeedError, need } from "./needs.js";
|
|
2
|
+
export function mapNetworkNeed(value) {
|
|
3
|
+
const code = { NEED_ENROLLMENT: 'ENROLLMENT_REQUIRED', ENROLLMENT_REJECTED: 'ENROLLMENT_REJECTED', ENROLLMENT_RESULT_UNKNOWN: 'ENROLLMENT_RESULT_UNKNOWN', LOCAL_STATE_INVALID: 'ENROLLMENT_INVALID', HOST_OFFLINE: 'HOST_OFFLINE', HOST_TRUST_REQUIRED: 'HOST_TRUST_REQUIRED', HOST_KEY_MISMATCH: 'HOST_KEY_MISMATCH', ENROLLMENT_MISMATCH: 'ENROLLMENT_INVALID', CONFIG_INVALID: 'SETTINGS_REQUIRED', SETTINGS_REQUIRED: 'SETTINGS_REQUIRED', SSH_FAILED: 'SSH_FAILED', NETWORK_INTERRUPTED: 'CONNECTION_LOST', CONTROL_UNAVAILABLE: 'CONNECTION_LOST', NETWORK_NOT_READY: 'CONNECTION_LOST', DISPOSED: 'DISPOSED' }[value.code] ?? 'DEPENDENCY_REQUIRED';
|
|
4
|
+
const direct = ['ACTIVATION_CONFLICT', 'ACTIVATION_CANCELLED', 'ENROLLMENT_CLEANUP_UNBOUND', 'ENROLLMENT_CLEANUP_FAILED', 'ENROLLMENT_MISMATCH', 'LOCAL_STATE_INVALID', 'ALREADY_RUNNING', 'ENROLLMENT_RECEIPT_REQUIRED', 'CONTROL_PROTOCOL_INVALID', 'HELPER_HANDOFF_FAILED', 'HELPER_SHUTDOWN_FORCED'];
|
|
5
|
+
const result = need(direct.includes(value.code) ? value.code : code);
|
|
6
|
+
result.officialReason = /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'NETWORK_NEED_INVALID';
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
export function adaptNetwork(provider) {
|
|
10
|
+
return {
|
|
11
|
+
async connect(e, signal) {
|
|
12
|
+
const result = await provider.connect({ host: e.host, username: e.username, port: e.port, knownHostsFile: e.knownHostsFile }, signal);
|
|
13
|
+
if (result.ok)
|
|
14
|
+
return result;
|
|
15
|
+
const error = mapNetworkNeed(result.error), lastSeen = provider.getState().status?.host.lastSeenAt;
|
|
16
|
+
if (error.code === 'HOST_OFFLINE' && typeof lastSeen === 'string' && !Number.isNaN(Date.parse(lastSeen)))
|
|
17
|
+
error.lastSeen = lastSeen;
|
|
18
|
+
return { ok: false, error };
|
|
19
|
+
},
|
|
20
|
+
onState(listener) { return provider.onState(event => { if (event.kind === 'lost' || event.kind === 'restored')
|
|
21
|
+
listener({ kind: event.kind }); }); },
|
|
22
|
+
async acquireProxy(signal) {
|
|
23
|
+
const result = await provider.openLoginProxy(signal);
|
|
24
|
+
if (!result.ok)
|
|
25
|
+
throw new NeedError(mapNetworkNeed(result.error));
|
|
26
|
+
const source = result.value, credentials = source.credentials;
|
|
27
|
+
let release;
|
|
28
|
+
return { url: credentials.url, get username() { return credentials.username; }, get password() { return credentials.password; }, release: () => release ??= (async () => { try {
|
|
29
|
+
await source.release();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
credentials.username = '';
|
|
33
|
+
credentials.password = '';
|
|
34
|
+
} })() };
|
|
35
|
+
},
|
|
36
|
+
async download(source, signal) { const result = await provider.download(source, signal); if (!result.ok)
|
|
37
|
+
throw new NeedError(mapNetworkNeed(result.error)); return result.value; },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export function remoteLoginCommand(provider, killGraceMs, probeIsolate = false) {
|
|
3
|
+
if (provider !== 'claude' && provider !== 'codex')
|
|
4
|
+
throw Error('LOGIN_PROVIDER_INVALID');
|
|
5
|
+
if (!Number.isSafeInteger(killGraceMs) || killGraceMs < 0 || killGraceMs > 2147483647)
|
|
6
|
+
throw Error('REMOTE_KILL_GRACE_REQUIRED');
|
|
7
|
+
const source = readFileSync(new URL('../helper/remote-login.cjs', import.meta.url), 'utf8');
|
|
8
|
+
return "node -e '" + source.replaceAll("'", "'\\''") + "' -- " + provider + ' ' + killGraceMs + (probeIsolate ? ' --probe-isolate' : '');
|
|
9
|
+
}
|
package/lib/ssh.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SshTarget } from './api.ts';
|
|
2
|
+
import type { CommandResult, Ports } from './ports.ts';
|
|
3
|
+
export declare function shellQuote(s: string): string;
|
|
4
|
+
export declare function proxyCommand(binary: string, runtimeDir: string): string;
|
|
5
|
+
export declare function sshArgs(t: SshTarget): string[];
|
|
6
|
+
export interface Runner {
|
|
7
|
+
run(cmd: string, args: string[], signal: AbortSignal, input?: string): Promise<CommandResult>;
|
|
8
|
+
}
|
|
9
|
+
export declare function runRemote(runner: Runner, t: SshTarget, command: string, signal: AbortSignal, input?: string): Promise<CommandResult>;
|
|
10
|
+
export declare function createSsh(runner: Runner, maxOutputBytes?: number): NonNullable<Ports['ssh']>;
|
package/lib/ssh.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { fail, ok } from "./needs.js";
|
|
4
|
+
export function shellQuote(s) {
|
|
5
|
+
if (/[\x00-\x1f\x7f%]/.test(s))
|
|
6
|
+
throw new Error('INVALID_PROXY_PATH');
|
|
7
|
+
return "'" + s.replaceAll("'", "'\\''") + "'";
|
|
8
|
+
}
|
|
9
|
+
export function proxyCommand(binary, runtimeDir) {
|
|
10
|
+
if (!binary.startsWith('/') || !runtimeDir.startsWith('/'))
|
|
11
|
+
throw new Error('ABSOLUTE_PROXY_PATH_REQUIRED');
|
|
12
|
+
return `${shellQuote(binary)} ssh-proxy --runtime-dir ${shellQuote(runtimeDir)} %h %p`;
|
|
13
|
+
}
|
|
14
|
+
export function sshArgs(t) {
|
|
15
|
+
return ['-F', '/dev/null', '-T', '-o', 'IdentityAgent=none', '-o', 'PubkeyAuthentication=no', '-o', 'PasswordAuthentication=no', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=none', '-o', 'UpdateHostKeys=no', '-o', 'CanonicalizeHostname=no', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'GlobalKnownHostsFile=/dev/null', '-o', 'UserKnownHostsFile=' + JSON.stringify(t.knownHostsFile.replaceAll('%', '%%')), '-o', 'ProxyCommand=' + t.proxyCommand, '-p', String(t.port), '-l', t.username, t.host];
|
|
16
|
+
}
|
|
17
|
+
export async function runRemote(runner, t, command, signal, input) {
|
|
18
|
+
const nonce = randomUUID(), source = await readFile(new URL('../helper/remote-command.cjs', import.meta.url), 'utf8');
|
|
19
|
+
const remote = 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "'";
|
|
20
|
+
const raw = await runner.run('ssh', [...sshArgs(t), remote], signal, JSON.stringify({ protocolVersion: 1, nonce, command, ...input === undefined ? {} : { input } }));
|
|
21
|
+
if (signal.aborted || raw.exitCode !== 0 || raw.stderr !== '')
|
|
22
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
23
|
+
const lines = raw.stdout.split('\n');
|
|
24
|
+
if (lines.pop() !== '')
|
|
25
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
26
|
+
let started = false, ended = false, exitCode = null;
|
|
27
|
+
const out = [], err = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const v = JSON.parse(line);
|
|
30
|
+
if (!v || typeof v !== 'object' || v.protocolVersion !== 1 || v.nonce !== nonce || ended)
|
|
31
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
32
|
+
const keys = Object.keys(v).sort().join(',');
|
|
33
|
+
if (v.type === 'start' && !started && keys === 'nonce,protocolVersion,type') {
|
|
34
|
+
started = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!started)
|
|
38
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
39
|
+
if ((v.type === 'stdout' || v.type === 'stderr') && keys === 'data,nonce,protocolVersion,type' && typeof v.data === 'string') {
|
|
40
|
+
const bytes = Buffer.from(v.data, 'base64');
|
|
41
|
+
if (bytes.toString('base64') !== v.data)
|
|
42
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
43
|
+
(v.type === 'stdout' ? out : err).push(bytes);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (v.type === 'end' && keys === 'exitCode,nonce,protocolVersion,signal,type' && Number.isInteger(v.exitCode) && v.exitCode >= 0 && v.exitCode <= 255 && v.signal === null) {
|
|
47
|
+
ended = true;
|
|
48
|
+
exitCode = v.exitCode;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
52
|
+
}
|
|
53
|
+
if (!ended || exitCode === null)
|
|
54
|
+
throw Error('REMOTE_RESULT_INVALID');
|
|
55
|
+
return { exitCode, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8') };
|
|
56
|
+
}
|
|
57
|
+
export function createSsh(runner, maxOutputBytes) {
|
|
58
|
+
return {
|
|
59
|
+
verify: async (t, signal) => { try {
|
|
60
|
+
const r = await runRemote(runner, t, 'id -un', signal);
|
|
61
|
+
return r.exitCode === 0 && r.stdout.trim() === t.username ? ok(undefined) : fail('SSH_FAILED');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return fail('SSH_FAILED');
|
|
65
|
+
} },
|
|
66
|
+
status: async (t, p, signal) => {
|
|
67
|
+
try {
|
|
68
|
+
if (!['codex', 'claude'].includes(p) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
|
|
69
|
+
return fail('AUTH_STATUS_INVALID');
|
|
70
|
+
const source = await readFile(new URL('../helper/auth-status.cjs', import.meta.url), 'utf8');
|
|
71
|
+
return ok(await runRemote(runner, t, 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "' -- " + p + ' ' + maxOutputBytes, signal));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return fail('AUTH_STATUS_INVALID');
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/lib/startup.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConnectApi, ConnectionState, LoginPromptState, LoginPromptResult, Provider, NeedCode, Result } from './api.ts';
|
|
2
|
+
export type StartupState = LoginPromptState;
|
|
3
|
+
export declare function employeeMessage(code?: NeedCode): string;
|
|
4
|
+
type Operations = Pick<ConnectApi, 'connect' | 'checkLogin' | 'login' | 'getState'>;
|
|
5
|
+
export declare function createStartup(api: Operations, providers: Provider[], emit?: (s: LoginPromptState) => void, result?: (r: LoginPromptResult) => void, instanceId?: string): {
|
|
6
|
+
getState: () => LoginPromptState;
|
|
7
|
+
check: () => Promise<void>;
|
|
8
|
+
subscribe(listener: (s: LoginPromptState) => void): () => void;
|
|
9
|
+
request(provider: Provider, intent: "automatic" | "user"): Promise<Result<LoginPromptState>>;
|
|
10
|
+
respond: (requestId: string, action: "authorize" | "defer") => Promise<Result<LoginPromptState>>;
|
|
11
|
+
defer(provider: Provider): void;
|
|
12
|
+
authorize(provider: Provider): Promise<void>;
|
|
13
|
+
connectionChanged(next: ConnectionState): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
dispose(): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export type Startup = ReturnType<typeof createStartup>;
|
|
18
|
+
export {};
|