@ai-workbench/connect 0.0.0-stage → 0.7.5

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.
Files changed (129) hide show
  1. package/NOTICE.md +18 -0
  2. package/README.md +24 -2
  3. package/client/index.cjs +123 -0
  4. package/cordis.patch.yml +3 -0
  5. package/helper/auth-status.cjs +18 -0
  6. package/helper/claude-callback.cjs +29 -0
  7. package/helper/main.cjs +19 -0
  8. package/helper/projects.cjs +57 -0
  9. package/helper/remote-command.cjs +15 -0
  10. package/helper/remote-login.cjs +25 -0
  11. package/helper/window.cjs +52 -0
  12. package/lib/api.d.ts +196 -0
  13. package/lib/api.js +1 -0
  14. package/lib/archive.d.ts +1 -0
  15. package/lib/archive.js +63 -0
  16. package/lib/auth-status.d.ts +3 -0
  17. package/lib/auth-status.js +18 -0
  18. package/lib/cache.d.ts +21 -0
  19. package/lib/cache.js +156 -0
  20. package/lib/component-download.d.ts +25 -0
  21. package/lib/component-download.js +73 -0
  22. package/lib/components.d.ts +9 -0
  23. package/lib/components.js +121 -0
  24. package/lib/connect.d.ts +10 -0
  25. package/lib/connect.js +246 -0
  26. package/lib/cordis.d.ts +15 -0
  27. package/lib/cordis.js +1 -0
  28. package/lib/defaults.d.ts +29 -0
  29. package/lib/defaults.js +4 -0
  30. package/lib/electron.d.ts +8 -0
  31. package/lib/electron.js +83 -0
  32. package/lib/enrollment.d.ts +3 -0
  33. package/lib/enrollment.js +78 -0
  34. package/lib/index.d.ts +40 -0
  35. package/lib/index.js +48 -0
  36. package/lib/integration.d.ts +5 -0
  37. package/lib/integration.js +47 -0
  38. package/lib/login.d.ts +35 -0
  39. package/lib/login.js +81 -0
  40. package/lib/native-import-picker.d.ts +11 -0
  41. package/lib/native-import-picker.js +49 -0
  42. package/lib/needs.d.ts +9 -0
  43. package/lib/needs.js +52 -0
  44. package/lib/network.d.ts +26 -0
  45. package/lib/network.js +226 -0
  46. package/lib/physical-fs.d.ts +3 -0
  47. package/lib/physical-fs.js +7 -0
  48. package/lib/ports.d.ts +43 -0
  49. package/lib/ports.js +1 -0
  50. package/lib/private-files.d.ts +3 -0
  51. package/lib/private-files.js +41 -0
  52. package/lib/processes.d.ts +5 -0
  53. package/lib/processes.js +147 -0
  54. package/lib/profile-activation.d.ts +32 -0
  55. package/lib/profile-activation.js +118 -0
  56. package/lib/profile-enrollment.d.ts +21 -0
  57. package/lib/profile-enrollment.js +275 -0
  58. package/lib/profile-runtime.d.ts +6 -0
  59. package/lib/profile-runtime.js +47 -0
  60. package/lib/profile-settings.d.ts +2 -0
  61. package/lib/profile-settings.js +10 -0
  62. package/lib/projects.d.ts +4 -0
  63. package/lib/projects.js +41 -0
  64. package/lib/provider.d.ts +73 -0
  65. package/lib/provider.js +39 -0
  66. package/lib/remote-login.d.ts +2 -0
  67. package/lib/remote-login.js +9 -0
  68. package/lib/ssh.d.ts +10 -0
  69. package/lib/ssh.js +78 -0
  70. package/lib/startup.d.ts +18 -0
  71. package/lib/startup.js +211 -0
  72. package/lib/workspace-bindings.d.ts +32 -0
  73. package/lib/workspace-bindings.js +328 -0
  74. package/locale/en.json +6 -0
  75. package/locale/zh.json +6 -0
  76. package/node_modules/argparse/LICENSE +254 -0
  77. package/node_modules/argparse/README.md +84 -0
  78. package/node_modules/argparse/argparse.js +3707 -0
  79. package/node_modules/argparse/lib/sub.js +67 -0
  80. package/node_modules/argparse/lib/textwrap.js +440 -0
  81. package/node_modules/argparse/package.json +31 -0
  82. package/node_modules/js-yaml/LICENSE +21 -0
  83. package/node_modules/js-yaml/README.md +233 -0
  84. package/node_modules/js-yaml/bin/js-yaml.js +117 -0
  85. package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
  86. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
  87. package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
  88. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
  89. package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
  90. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
  91. package/node_modules/js-yaml/index.js +44 -0
  92. package/node_modules/js-yaml/lib/common.js +50 -0
  93. package/node_modules/js-yaml/lib/dumper.js +937 -0
  94. package/node_modules/js-yaml/lib/exception.js +51 -0
  95. package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
  96. package/node_modules/js-yaml/lib/loader.js +1803 -0
  97. package/node_modules/js-yaml/lib/schema/core.js +9 -0
  98. package/node_modules/js-yaml/lib/schema/default.js +20 -0
  99. package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
  100. package/node_modules/js-yaml/lib/schema/json.js +17 -0
  101. package/node_modules/js-yaml/lib/schema.js +109 -0
  102. package/node_modules/js-yaml/lib/snippet.js +96 -0
  103. package/node_modules/js-yaml/lib/type/binary.js +122 -0
  104. package/node_modules/js-yaml/lib/type/bool.js +35 -0
  105. package/node_modules/js-yaml/lib/type/float.js +99 -0
  106. package/node_modules/js-yaml/lib/type/int.js +142 -0
  107. package/node_modules/js-yaml/lib/type/map.js +8 -0
  108. package/node_modules/js-yaml/lib/type/merge.js +12 -0
  109. package/node_modules/js-yaml/lib/type/null.js +35 -0
  110. package/node_modules/js-yaml/lib/type/omap.js +45 -0
  111. package/node_modules/js-yaml/lib/type/pairs.js +50 -0
  112. package/node_modules/js-yaml/lib/type/seq.js +8 -0
  113. package/node_modules/js-yaml/lib/type/set.js +29 -0
  114. package/node_modules/js-yaml/lib/type/str.js +8 -0
  115. package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
  116. package/node_modules/js-yaml/lib/type.js +66 -0
  117. package/node_modules/js-yaml/package.json +83 -0
  118. package/node_modules/pend/LICENSE +23 -0
  119. package/node_modules/pend/README.md +41 -0
  120. package/node_modules/pend/index.js +55 -0
  121. package/node_modules/pend/package.json +18 -0
  122. package/node_modules/pend/test.js +137 -0
  123. package/node_modules/yauzl/LICENSE +21 -0
  124. package/node_modules/yauzl/README.md +1015 -0
  125. package/node_modules/yauzl/crc32.js +45 -0
  126. package/node_modules/yauzl/fd-slicer.js +188 -0
  127. package/node_modules/yauzl/index.js +1096 -0
  128. package/node_modules/yauzl/package.json +40 -0
  129. package/package.json +89 -4
@@ -0,0 +1,147 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+ import { extractLoginUrl } from "./login.js";
4
+ import { fail, ok } from "./needs.js";
5
+ function limit(n) { if (!Number.isSafeInteger(n) || n < 1)
6
+ throw new Error('OUTPUT_SETTING_REQUIRED'); }
7
+ function start(command, args, signal) {
8
+ if (signal.aborted)
9
+ throw new Error('CANCELLED');
10
+ return spawn(command, args, { shell: false, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined } });
11
+ }
12
+ function reap(child, closed) {
13
+ if (child.exitCode === null && child.signalCode === null)
14
+ child.kill('SIGTERM');
15
+ return closed.then(() => { });
16
+ }
17
+ export function createRunner(maxBytes) {
18
+ limit(maxBytes);
19
+ return { run: async (command, args, signal, input) => {
20
+ const child = start(command, args, signal);
21
+ let stdout = '', stderr = '', size = 0, failed = false;
22
+ child.stdin.on('error', () => { failed = true; child.kill('SIGTERM'); });
23
+ child.stdin.end(input);
24
+ const finished = new Promise((resolve, reject) => {
25
+ child.once('error', () => { failed = true; });
26
+ for (const [stream, channel] of [[child.stdout, 'out'], [child.stderr, 'err']]) {
27
+ const decoder = new StringDecoder('utf8');
28
+ stream.on('data', (chunk) => { size += chunk.length; if (size > maxBytes) {
29
+ failed = true;
30
+ child.kill('SIGTERM');
31
+ return;
32
+ } if (channel === 'out')
33
+ stdout += decoder.write(chunk);
34
+ else
35
+ stderr += decoder.write(chunk); });
36
+ }
37
+ child.once('close', code => failed || signal.aborted ? reject(new Error('COMMAND_FAILED')) : resolve({ exitCode: code, stdout, stderr }));
38
+ });
39
+ const abort = () => child.kill('SIGTERM');
40
+ signal.addEventListener('abort', abort, { once: true });
41
+ try {
42
+ if (signal.aborted)
43
+ abort();
44
+ return await finished;
45
+ }
46
+ finally {
47
+ signal.removeEventListener('abort', abort);
48
+ }
49
+ } };
50
+ }
51
+ export async function createRemoteLogin(command, args, provider, maxBytes, signal, protocol) {
52
+ limit(maxBytes);
53
+ const child = start(command, args, signal);
54
+ let size = 0, invalid = false, urlSent = false, owned = false;
55
+ let terminal;
56
+ let ownResolve = () => { }, ownReject = () => { };
57
+ const ownership = new Promise((yes, no) => { ownResolve = yes; ownReject = () => no(new Error('REMOTE_LOGIN_NOT_STARTED')); });
58
+ void ownership.catch(() => { });
59
+ let found = () => { };
60
+ const url = new Promise(r => found = r);
61
+ const pending = new Map();
62
+ for (const stream of [child.stdout, child.stderr]) {
63
+ const record = { decoder: new StringDecoder('utf8'), line: '' };
64
+ pending.set(stream, record);
65
+ stream.on('data', (chunk) => {
66
+ size += chunk.length;
67
+ if (size > maxBytes) {
68
+ invalid = true;
69
+ child.kill('SIGTERM');
70
+ return;
71
+ }
72
+ record.line += record.decoder.write(chunk);
73
+ const lines = record.line.split(/\r?\n/);
74
+ record.line = lines.pop();
75
+ for (const line of lines) {
76
+ if (protocol && line.startsWith('AW_LOGIN_PROCESS ')) {
77
+ try {
78
+ const v = JSON.parse(line.slice(17));
79
+ if (stream === child.stdout && !owned && Number.isSafeInteger(v.pid) && v.pid > 0 && v.group === v.pid && v.provider === provider) {
80
+ owned = true;
81
+ ownResolve(v);
82
+ }
83
+ else
84
+ throw Error();
85
+ }
86
+ catch {
87
+ invalid = true;
88
+ child.kill('SIGTERM');
89
+ }
90
+ }
91
+ if (protocol && line.startsWith('AW_LOGIN_RESULT ')) {
92
+ try {
93
+ const v = JSON.parse(line.slice(16));
94
+ if (stream !== child.stdout || terminal || !owned || v.protocolVersion !== 1 || v.provider !== provider || Object.keys(v).sort().join(',') !== 'exitCode,protocolVersion,provider,stopping' || !Number.isInteger(v.exitCode) || v.exitCode < 0 || v.exitCode > 255 || typeof v.stopping !== 'boolean')
95
+ throw Error();
96
+ terminal = v;
97
+ }
98
+ catch {
99
+ invalid = true;
100
+ child.kill('SIGTERM');
101
+ }
102
+ }
103
+ const u = extractLoginUrl(provider, line);
104
+ if (u && !urlSent) {
105
+ urlSent = true;
106
+ found(ok(u));
107
+ }
108
+ }
109
+ });
110
+ }
111
+ child.once('error', () => { invalid = true; });
112
+ const completion = new Promise(resolve => child.once('close', code => {
113
+ for (const record of pending.values()) {
114
+ if (protocol && /^AW_LOGIN_(RESULT|PROCESS)/.test(record.line))
115
+ invalid = true;
116
+ const u = extractLoginUrl(provider, record.line + record.decoder.end());
117
+ if (u && !urlSent) {
118
+ urlSent = true;
119
+ found(ok(u));
120
+ }
121
+ record.line = '';
122
+ }
123
+ ownReject();
124
+ if (!urlSent)
125
+ found(fail('LOGIN_FAILED'));
126
+ signal.removeEventListener('abort', abort);
127
+ resolve(!invalid && !signal.aborted && code === 0 && (!protocol || (owned && terminal?.exitCode === 0 && !terminal.stopping)) ? ok(undefined) : fail('LOGIN_FAILED'));
128
+ }));
129
+ const abort = () => child.kill('SIGTERM');
130
+ signal.addEventListener('abort', abort, { once: true });
131
+ if (signal.aborted)
132
+ abort();
133
+ child.stdin.on('error', () => { invalid = true; });
134
+ return { url, completion, ...(protocol ? { ownership } : {}),
135
+ writeCode: async (code) => {
136
+ if (signal.aborted || child.exitCode !== null || child.signalCode !== null || child.stdin.writableEnded || /[\x00-\x20\x7f]/.test(code) || !code)
137
+ throw new Error('LOGIN_CODE_REJECTED');
138
+ await new Promise((yes, no) => { const failed = () => no(new Error('LOGIN_STDIN_FAILED')); child.stdin.once('error', failed); const done = () => { child.stdin.removeListener('error', failed); yes(); }; if (protocol)
139
+ child.stdin.write(JSON.stringify({ type: 'code', value: code }) + '\n', done);
140
+ else
141
+ child.stdin.end(code + '\n', done); });
142
+ },
143
+ close: () => { if (protocol && child.exitCode === null && child.signalCode === null && !child.stdin.writableEnded) {
144
+ child.stdin.end(JSON.stringify({ type: 'close' }) + '\n');
145
+ return completion.then(() => { });
146
+ } return reap(child, completion); }, };
147
+ }
@@ -0,0 +1,32 @@
1
+ import type { Enrollment } from './ports.ts';
2
+ import type { Result } from './api.ts';
3
+ import type { NetworkProvider } from './provider.ts';
4
+ export interface AssemblySettings {
5
+ configFile?: string;
6
+ helperLogFile?: string;
7
+ stateDir?: string;
8
+ runtimeDir?: string;
9
+ nodeName?: string;
10
+ proxyListen?: string;
11
+ logUpload?: boolean;
12
+ statusRefreshIntervalSeconds?: number;
13
+ dialTimeoutSeconds?: number;
14
+ headerTimeoutSeconds?: number;
15
+ enrollmentObservationSeconds?: number;
16
+ httpMaxHeaderBytes?: number;
17
+ httpMaxResponseHeaderBytes?: number;
18
+ startupObservationSeconds?: number;
19
+ shutdownObservationSeconds?: number;
20
+ controlResponseMaxBytes?: number;
21
+ downloadMaxRedirects?: number;
22
+ }
23
+ interface Options {
24
+ profileDir: string;
25
+ metadataMaxBytes: number;
26
+ network?: NetworkProvider;
27
+ settings?: AssemblySettings;
28
+ }
29
+ export declare function createProfileActivation(options: Options): {
30
+ activate(e: Enrollment, signal: AbortSignal): Promise<Result<void>>;
31
+ };
32
+ export {};
@@ -0,0 +1,118 @@
1
+ import { open, rename, rm, realpath } from 'node:fs/promises';
2
+ import { join, isAbsolute } from 'node:path';
3
+ import { hostname, homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { ok, fail } from "./needs.js";
6
+ import { mapNetworkNeed } from "./provider.js";
7
+ import { privateDirectory, readPrivate } from "./private-files.js";
8
+ import { NETWORK_DEFAULTS } from "./defaults.js";
9
+ // Own handoff configuration only. Never reads N's identity files, journal or key contents.
10
+ export function createProfileActivation(options) {
11
+ const root = join(options.profileDir, 'ai-workbench-connect'), recordFile = join(root, 'network-activation.json');
12
+ let flight, flightTarget;
13
+ async function save(file, value) { const bytes = JSON.stringify(value) + '\n'; if (Buffer.byteLength(bytes) > options.metadataMaxBytes)
14
+ throw Error('ACTIVATION_METADATA_LIMIT'); const temp = file + '.' + randomUUID(); try {
15
+ const h = await open(temp, 'wx', 0o600);
16
+ try {
17
+ await h.writeFile(bytes);
18
+ await h.sync();
19
+ }
20
+ finally {
21
+ await h.close();
22
+ }
23
+ await rename(temp, file);
24
+ const d = await open(join(file, '..'), 'r');
25
+ try {
26
+ await d.sync();
27
+ }
28
+ finally {
29
+ await d.close();
30
+ }
31
+ }
32
+ finally {
33
+ await rm(temp, { force: true });
34
+ } }
35
+ const receiptValid = (s) => s?.phase === 'active' && s.receipt?.apiVersion === 1 && s.receipt.session === 'identity' && s.receipt.identity === 'resumable' && ((s.receipt.key === 'confirmed' && s.receipt.cleanup === 'complete') || (s.receipt.key === 'not-required' && s.receipt.cleanup === 'not-required'));
36
+ async function run(e, signal) {
37
+ const n = options.network;
38
+ if (!n?.activate || !n.getActivationState || !n.onActivationState)
39
+ return fail('DEPENDENCY_REQUIRED');
40
+ if (signal.aborted)
41
+ return fail('ACTIVATION_CANCELLED');
42
+ try {
43
+ if (!isAbsolute(options.profileDir) || await realpath(options.profileDir) !== options.profileDir)
44
+ return fail('ENROLLMENT_INVALID');
45
+ await privateDirectory(root);
46
+ const configDir = join(root, 'network-config'), logDir = join(root, 'network-logs');
47
+ await privateDirectory(configDir);
48
+ await privateDirectory(logDir);
49
+ for (const p of [root, configDir, logDir])
50
+ if (await realpath(p) !== p)
51
+ return fail('ENROLLMENT_INVALID');
52
+ const s = { ...NETWORK_DEFAULTS, ...options.settings };
53
+ const settings = { configFile: s.configFile ?? join(configDir, 'config.json'), helperLogFile: s.helperLogFile ?? join(logDir, 'network.log'), startupObservationSeconds: s.startupObservationSeconds, shutdownObservationSeconds: s.shutdownObservationSeconds, controlResponseMaxBytes: s.controlResponseMaxBytes, downloadMaxRedirects: s.downloadMaxRedirects };
54
+ const target = { host: e.host, username: e.username, port: e.port, knownHostsFile: e.knownHostsFile };
55
+ const expected = { apiVersion: 1, stateDir: s.stateDir ?? join(homedir(), 'Library', 'Application Support', 'ai-workbench', 'network'), runtimeDir: s.runtimeDir ?? '/tmp/aw-net-' + process.getuid(), nodeName: s.nodeName ?? `aw-${e.username}-${hostname()}-${process.getuid()}`, host: { name: e.host, sshPort: e.port, employee: e.username, knownHostsFile: e.knownHostsFile }, proxyListen: s.proxyListen, logUpload: s.logUpload, statusRefreshIntervalSeconds: s.statusRefreshIntervalSeconds, dialTimeoutSeconds: s.dialTimeoutSeconds, headerTimeoutSeconds: s.headerTimeoutSeconds, enrollmentObservationSeconds: s.enrollmentObservationSeconds, httpMaxHeaderBytes: s.httpMaxHeaderBytes, httpMaxResponseHeaderBytes: s.httpMaxResponseHeaderBytes };
56
+ const canonical = (v) => JSON.stringify(v, (_, x) => x && typeof x === 'object' && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort(([a], [b]) => a.localeCompare(b))) : x);
57
+ const registration = { schemaVersion: 1, settings, target };
58
+ let stored = false;
59
+ try {
60
+ const r = JSON.parse(await readPrivate(recordFile, options.metadataMaxBytes));
61
+ if (canonical(r) !== canonical(registration))
62
+ return fail('ENROLLMENT_MISMATCH');
63
+ stored = true;
64
+ }
65
+ catch (error) {
66
+ if (error.code !== 'ENOENT')
67
+ throw error;
68
+ }
69
+ try {
70
+ const config = JSON.parse(await readPrivate(settings.configFile, options.metadataMaxBytes));
71
+ const key = config.authKeyFile;
72
+ delete config.authKeyFile;
73
+ if (canonical(config) !== canonical(expected) || (key !== undefined && key !== e.authKeyFile))
74
+ return fail('ENROLLMENT_MISMATCH');
75
+ if (!stored)
76
+ return fail('LOCAL_STATE_INVALID');
77
+ }
78
+ catch (error) {
79
+ if (error.code !== 'ENOENT')
80
+ throw error;
81
+ if (stored)
82
+ return fail('LOCAL_STATE_INVALID');
83
+ if (!e.authKeyFile)
84
+ return fail('ENROLLMENT_REQUIRED');
85
+ await save(settings.configFile, { ...expected, authKeyFile: e.authKeyFile });
86
+ }
87
+ if (!stored)
88
+ await save(recordFile, registration);
89
+ // Settings and target references are durable before activation; thereafter N owns config cleanup.
90
+ const before = n.getActivationState();
91
+ let latest = before;
92
+ const off = n.onActivationState(next => { if (next.instanceId === before.instanceId && next.revision >= latest.revision)
93
+ latest = next; });
94
+ try {
95
+ const result = await n.activate(settings, signal);
96
+ if (!result.ok)
97
+ return { ok: false, error: mapNetworkNeed(result.error) };
98
+ const current = n.getActivationState();
99
+ if (result.value.instanceId !== before.instanceId || current.instanceId !== before.instanceId || result.value.revision < latest.revision || current.revision < result.value.revision)
100
+ return fail('CONTROL_PROTOCOL_INVALID');
101
+ if (!receiptValid(result.value) || !receiptValid(current))
102
+ return fail('ENROLLMENT_RECEIPT_REQUIRED');
103
+ return ok(undefined);
104
+ }
105
+ finally {
106
+ off();
107
+ }
108
+ }
109
+ catch {
110
+ return fail('ENROLLMENT_INVALID');
111
+ }
112
+ }
113
+ return { activate(e, signal) { const target = JSON.stringify([e.host, e.username, e.port, e.knownHostsFile, e.projectRoot]); if (flight)
114
+ return target === flightTarget ? flight : Promise.resolve(fail('ACTIVATION_CONFLICT')); flightTarget = target; const p = run(e, signal); flight = p; void p.finally(() => { if (flight === p) {
115
+ flight = undefined;
116
+ flightTarget = undefined;
117
+ } }).catch(() => { }); return p; } };
118
+ }
@@ -0,0 +1,21 @@
1
+ import type { EnrollmentState, ConnectNeed, Result } from './api.ts';
2
+ import type { Enrollment } from './ports.ts';
3
+ export interface ProfileEnrollmentOptions {
4
+ profileDir: string;
5
+ metadataMaxBytes: number;
6
+ pick: (signal: AbortSignal) => Promise<string | null>;
7
+ activate?: (enrollment: Enrollment, signal: AbortSignal) => Promise<Result<void>>;
8
+ active?: () => boolean;
9
+ blockedByNetwork?: () => Pick<ConnectNeed, 'code'> | undefined;
10
+ onState?: (state: EnrollmentState) => void;
11
+ }
12
+ export declare function createProfileEnrollment(options: ProfileEnrollmentOptions): {
13
+ getState: () => EnrollmentState;
14
+ onState(fn: (s: EnrollmentState) => void): () => void;
15
+ read(): Promise<Result<Enrollment>>;
16
+ importEnrollment(): Promise<Result<{
17
+ outcome: "imported" | "cancelled";
18
+ state: EnrollmentState;
19
+ }>>;
20
+ dispose(): Promise<void>;
21
+ };
@@ -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,2 @@
1
+ import type { Config } from './index.ts';
2
+ export declare function profileSettings(config: Config, profileDir?: string, platform?: string, arch?: string): Config;