@trusty-squire/mcp 1.1.13-rc.8 → 1.1.13

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 (162) hide show
  1. package/README.md +139 -62
  2. package/assets/login/vnc-input.js +9 -0
  3. package/assets/login/vnc.html +214 -202
  4. package/assets/screencaps/connect-walkthrough.png +0 -0
  5. package/assets/screencaps/connect-walkthrough.svg +1 -1
  6. package/dist/api-client.d.ts +26 -3
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +19 -31
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/bin.js +2 -3
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bot/browser.d.ts +134 -77
  13. package/dist/bot/browser.d.ts.map +1 -1
  14. package/dist/bot/browser.js +1788 -1543
  15. package/dist/bot/browser.js.map +1 -1
  16. package/dist/bot/compact-observation-v2.d.ts +167 -0
  17. package/dist/bot/compact-observation-v2.d.ts.map +1 -0
  18. package/dist/bot/compact-observation-v2.js +1038 -0
  19. package/dist/bot/compact-observation-v2.js.map +1 -0
  20. package/dist/bot/credential-shape.d.ts +2 -0
  21. package/dist/bot/credential-shape.d.ts.map +1 -1
  22. package/dist/bot/credential-shape.js +21 -8
  23. package/dist/bot/credential-shape.js.map +1 -1
  24. package/dist/bot/element-fingerprint.d.ts +9 -0
  25. package/dist/bot/element-fingerprint.d.ts.map +1 -0
  26. package/dist/bot/element-fingerprint.js +147 -0
  27. package/dist/bot/element-fingerprint.js.map +1 -0
  28. package/dist/bot/email-verification.d.ts +7 -1
  29. package/dist/bot/email-verification.d.ts.map +1 -1
  30. package/dist/bot/email-verification.js +48 -14
  31. package/dist/bot/email-verification.js.map +1 -1
  32. package/dist/bot/google-login.d.ts +28 -52
  33. package/dist/bot/google-login.d.ts.map +1 -1
  34. package/dist/bot/google-login.js +246 -1119
  35. package/dist/bot/google-login.js.map +1 -1
  36. package/dist/bot/install-completion.d.ts +1 -2
  37. package/dist/bot/install-completion.d.ts.map +1 -1
  38. package/dist/bot/install-completion.js +1 -2
  39. package/dist/bot/install-completion.js.map +1 -1
  40. package/dist/bot/login-state.d.ts +13 -7
  41. package/dist/bot/login-state.d.ts.map +1 -1
  42. package/dist/bot/login-state.js +50 -114
  43. package/dist/bot/login-state.js.map +1 -1
  44. package/dist/bot/oauth-providers.d.ts +1 -1
  45. package/dist/bot/oauth-providers.d.ts.map +1 -1
  46. package/dist/bot/oauth-providers.js +3 -3
  47. package/dist/bot/oauth-providers.js.map +1 -1
  48. package/dist/bot/oauth-scope.d.ts +4 -0
  49. package/dist/bot/oauth-scope.d.ts.map +1 -0
  50. package/dist/bot/oauth-scope.js +58 -0
  51. package/dist/bot/oauth-scope.js.map +1 -0
  52. package/dist/bot/operator-browser-watchdog.d.ts +127 -0
  53. package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
  54. package/dist/bot/operator-browser-watchdog.js +438 -0
  55. package/dist/bot/operator-browser-watchdog.js.map +1 -0
  56. package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
  57. package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
  58. package/dist/bot/owner-process-reaper-worker.js +17 -0
  59. package/dist/bot/owner-process-reaper-worker.js.map +1 -0
  60. package/dist/bot/owner-process-reaper.d.ts +103 -0
  61. package/dist/bot/owner-process-reaper.d.ts.map +1 -0
  62. package/dist/bot/owner-process-reaper.js +1058 -0
  63. package/dist/bot/owner-process-reaper.js.map +1 -0
  64. package/dist/bot/pay-operator.d.ts +13 -2
  65. package/dist/bot/pay-operator.d.ts.map +1 -1
  66. package/dist/bot/pay-operator.js +319 -123
  67. package/dist/bot/pay-operator.js.map +1 -1
  68. package/dist/bot/profile.d.ts +10 -1
  69. package/dist/bot/profile.d.ts.map +1 -1
  70. package/dist/bot/profile.js +137 -11
  71. package/dist/bot/profile.js.map +1 -1
  72. package/dist/bot/promote-to-skill.js +1 -1
  73. package/dist/bot/promote-to-skill.js.map +1 -1
  74. package/dist/bot/provision-session.d.ts +60 -164
  75. package/dist/bot/provision-session.d.ts.map +1 -1
  76. package/dist/bot/provision-session.js +2049 -1546
  77. package/dist/bot/provision-session.js.map +1 -1
  78. package/dist/bot/remote-login-display.d.ts +61 -0
  79. package/dist/bot/remote-login-display.d.ts.map +1 -0
  80. package/dist/bot/remote-login-display.js +765 -0
  81. package/dist/bot/remote-login-display.js.map +1 -0
  82. package/dist/bot/session/hosts.d.ts +8 -0
  83. package/dist/bot/session/hosts.d.ts.map +1 -0
  84. package/dist/bot/session/hosts.js +36 -0
  85. package/dist/bot/session/hosts.js.map +1 -0
  86. package/dist/bot/session/lifecycle.d.ts +56 -0
  87. package/dist/bot/session/lifecycle.d.ts.map +1 -0
  88. package/dist/bot/session/lifecycle.js +962 -0
  89. package/dist/bot/session/lifecycle.js.map +1 -0
  90. package/dist/bot/session/model.d.ts +175 -0
  91. package/dist/bot/session/model.d.ts.map +1 -0
  92. package/dist/bot/session/model.js +77 -0
  93. package/dist/bot/session/model.js.map +1 -0
  94. package/dist/install/cli.d.ts +29 -9
  95. package/dist/install/cli.d.ts.map +1 -1
  96. package/dist/install/cli.js +243 -322
  97. package/dist/install/cli.js.map +1 -1
  98. package/dist/install/interactive.d.ts +1 -0
  99. package/dist/install/interactive.d.ts.map +1 -1
  100. package/dist/install/interactive.js +19 -5
  101. package/dist/install/interactive.js.map +1 -1
  102. package/dist/install/ui.d.ts.map +1 -1
  103. package/dist/install/ui.js +3 -15
  104. package/dist/install/ui.js.map +1 -1
  105. package/dist/operator-env.js +1 -1
  106. package/dist/operator-env.js.map +1 -1
  107. package/dist/server-instance-registry.d.ts +103 -0
  108. package/dist/server-instance-registry.d.ts.map +1 -0
  109. package/dist/server-instance-registry.js +392 -0
  110. package/dist/server-instance-registry.js.map +1 -0
  111. package/dist/server.d.ts +15 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +164 -62
  114. package/dist/server.js.map +1 -1
  115. package/dist/session-guard.d.ts +28 -0
  116. package/dist/session-guard.d.ts.map +1 -0
  117. package/dist/session-guard.js +84 -0
  118. package/dist/session-guard.js.map +1 -0
  119. package/dist/session.d.ts +57 -13
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +246 -127
  122. package/dist/session.js.map +1 -1
  123. package/dist/tools/audit-log.d.ts +16 -2
  124. package/dist/tools/audit-log.d.ts.map +1 -1
  125. package/dist/tools/audit-log.js +223 -13
  126. package/dist/tools/audit-log.js.map +1 -1
  127. package/dist/tools/audit-rollup.d.ts +80 -0
  128. package/dist/tools/audit-rollup.d.ts.map +1 -0
  129. package/dist/tools/audit-rollup.js +0 -0
  130. package/dist/tools/audit-rollup.js.map +1 -0
  131. package/dist/tools/fetch-credential.d.ts +37 -0
  132. package/dist/tools/fetch-credential.d.ts.map +1 -0
  133. package/dist/tools/fetch-credential.js +153 -0
  134. package/dist/tools/fetch-credential.js.map +1 -0
  135. package/dist/tools/index.d.ts +3 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +7 -3
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/operate-pay.d.ts +4 -4
  140. package/dist/tools/operate-pay.d.ts.map +1 -1
  141. package/dist/tools/operate-pay.js +163 -96
  142. package/dist/tools/operate-pay.js.map +1 -1
  143. package/dist/tools/provision-drive.d.ts +43 -35
  144. package/dist/tools/provision-drive.d.ts.map +1 -1
  145. package/dist/tools/provision-drive.js +268 -164
  146. package/dist/tools/provision-drive.js.map +1 -1
  147. package/dist/tools/use-credential.d.ts.map +1 -1
  148. package/dist/tools/use-credential.js +3 -2
  149. package/dist/tools/use-credential.js.map +1 -1
  150. package/package.json +4 -6
  151. package/dist/bot/operator-direct-identity.d.ts +0 -26
  152. package/dist/bot/operator-direct-identity.d.ts.map +0 -1
  153. package/dist/bot/operator-direct-identity.js +0 -120
  154. package/dist/bot/operator-direct-identity.js.map +0 -1
  155. package/dist/bot/operator-profile-pool.d.ts +0 -97
  156. package/dist/bot/operator-profile-pool.d.ts.map +0 -1
  157. package/dist/bot/operator-profile-pool.js +0 -855
  158. package/dist/bot/operator-profile-pool.js.map +0 -1
  159. package/dist/bot/xvfb.d.ts +0 -13
  160. package/dist/bot/xvfb.d.ts.map +0 -1
  161. package/dist/bot/xvfb.js +0 -146
  162. package/dist/bot/xvfb.js.map +0 -1
@@ -1,855 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { chmodSync, copyFileSync, cpSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
- import { homedir, hostname, tmpdir } from "node:os";
4
- import { basename, dirname, join, relative, resolve } from "node:path";
5
- import { CHROME_PROFILE_DIR, processBirthIdentity, processBirthIdentityState, profilePathIdentity, profileProcessIdentity, profileProcessIdentityState, signalProfileProcess, } from "./profile.js";
6
- // Fixed per-namespace admission bound. These directories, rather than an
7
- // in-memory counter, are the authority across MCP processes. A third operator
8
- // start in the same namespace may wait at the provision seam, but can never
9
- // create a third active profile while both slots are claimed.
10
- const ACTIVE_SLOT_COUNT = 2;
11
- const UNPUBLISHED_SEED_GENERATION = "unpublished";
12
- const STARTUP_GRACE_MS = 30_000;
13
- const WARM_IDLE_TTL_MS = 6 * 60 * 60 * 1_000;
14
- const WARM_MAX_REUSES = 50;
15
- const WARM_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
16
- const TRANSIENT_PROFILE_NAMES = new Set([
17
- "SingletonLock",
18
- "SingletonSocket",
19
- "SingletonCookie",
20
- "DevToolsActivePort",
21
- ]);
22
- const TRANSIENT_PROFILE_DIRS = new Set(["Cache", "Code Cache", "GPUCache", "Crashpad"]);
23
- const IDENTITY_SEED_FILES = [
24
- "Local State",
25
- "logged-in-providers.json",
26
- "provider-emails.json",
27
- ];
28
- const POOLED_COOKIE_STORES = ["Default/Cookies", "Default/Network/Cookies"];
29
- const SQLITE_SIDECAR_SUFFIXES = ["", "-journal", "-shm", "-wal"];
30
- export const GOOGLE_LOGIN_COOKIE_MARKERS = ["__Secure-1PSID", "SAPISID", "SID"];
31
- export class OperatorProfileAcquisitionInterruptedError extends Error {
32
- reason;
33
- phase;
34
- constructor(reason, phase = "profile") {
35
- super(`operator profile acquisition ${reason === "timeout" ? "timed out" : "was cancelled"}`);
36
- this.reason = reason;
37
- this.phase = phase;
38
- this.name = "OperatorProfileAcquisitionInterruptedError";
39
- }
40
- }
41
- function defaultPoolRoot() {
42
- return process.env.VITEST === "true"
43
- ? join(tmpdir(), `trusty-squire-operator-profiles-test-${process.pid}`)
44
- : join(homedir(), ".trusty-squire", "operator-profiles");
45
- }
46
- function poolRootForSource(sourceProfileDir, rootDir) {
47
- if (rootDir !== undefined)
48
- return resolve(rootDir);
49
- const namespace = createHash("sha256")
50
- .update(profilePathIdentity(sourceProfileDir))
51
- .digest("hex")
52
- .slice(0, 24);
53
- return join(defaultPoolRoot(), "namespaces", namespace);
54
- }
55
- function paths(rootDir) {
56
- const root = resolve(rootDir ?? defaultPoolRoot());
57
- const seed = join(root, "seed");
58
- return {
59
- root,
60
- seed,
61
- generations: join(seed, "generations"),
62
- current: join(seed, "current"),
63
- seedLock: join(seed, ".lock"),
64
- profiles: join(root, "profiles"),
65
- active: join(root, "active"),
66
- activeClaims: join(root, "active-claims"),
67
- warm: join(root, "warm"),
68
- tombstones: join(root, "tombstones"),
69
- };
70
- }
71
- function ensurePrivateDir(path) {
72
- mkdirSync(path, { recursive: true, mode: 0o700 });
73
- chmodSync(path, 0o700);
74
- }
75
- function initializePool(p) {
76
- for (const dir of [
77
- p.root,
78
- p.seed,
79
- p.generations,
80
- p.profiles,
81
- p.active,
82
- p.activeClaims,
83
- p.warm,
84
- p.tombstones,
85
- ]) {
86
- ensurePrivateDir(dir);
87
- }
88
- }
89
- function writePrivateJson(path, value) {
90
- const temporary = `${path}.tmp.${randomUUID()}`;
91
- try {
92
- writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
93
- chmodSync(temporary, 0o600);
94
- renameSync(temporary, path);
95
- }
96
- catch (err) {
97
- rmSync(temporary, { force: true });
98
- throw err;
99
- }
100
- }
101
- function readJson(path) {
102
- try {
103
- return JSON.parse(readFileSync(path, "utf8"));
104
- }
105
- catch {
106
- return null;
107
- }
108
- }
109
- function currentProcessIdentity() {
110
- return processBirthIdentity(process.pid) ?? { pid: process.pid, start_time: "unknown" };
111
- }
112
- function processState(identity) {
113
- return processBirthIdentityState(identity);
114
- }
115
- function readSeedLockOwner(path) {
116
- try {
117
- return lstatSync(path).isDirectory()
118
- ? readJson(join(path, "owner.json"))
119
- : readJson(path);
120
- }
121
- catch {
122
- return null;
123
- }
124
- }
125
- function seedLockState(path) {
126
- // Valid owner records are reclaimed only when their same-host PID/start_time is
127
- // confirmed stale. A matching or cross-host owner is intentionally unreclaimed:
128
- // wall-clock reclaim of a plausibly-live owner cannot be fully race-free without
129
- // a heavier heartbeat or fencing protocol, which was rejected as out of scope.
130
- // It also cannot protect against an already-running old-version orphan like the
131
- // 2026-08-15 incident; identify and kill that process by PID instead. The
132
- // startup grace below applies only when no valid owner record can be read.
133
- const owner = readSeedLockOwner(path);
134
- if (owner !== null) {
135
- if (owner.host !== hostname())
136
- return "unknown";
137
- return processState(owner);
138
- }
139
- try {
140
- return Date.now() - lstatSync(path).mtimeMs >= STARTUP_GRACE_MS ? "stale" : "unknown";
141
- }
142
- catch (err) {
143
- return err.code === "ENOENT" ? "stale" : "unknown";
144
- }
145
- }
146
- function seedLockArtifacts(p, kind) {
147
- const dir = kind === "claim" ? p.seed : p.tombstones;
148
- const prefix = kind === "claim" ? `${basename(p.seedLock)}.claim-` : "seed-lock-";
149
- try {
150
- return readdirSync(dir)
151
- .filter((name) => name.startsWith(prefix))
152
- .map((name) => join(dir, name));
153
- }
154
- catch {
155
- return [];
156
- }
157
- }
158
- function scavengeSeedLockArtifacts(p) {
159
- let retainedTombstone = false;
160
- for (const tombstone of seedLockArtifacts(p, "stale")) {
161
- if (seedLockState(tombstone) === "stale") {
162
- rmSync(tombstone, { recursive: true, force: true });
163
- }
164
- else {
165
- retainedTombstone = true;
166
- }
167
- }
168
- for (const claim of seedLockArtifacts(p, "claim")) {
169
- if (seedLockState(claim) === "stale")
170
- rmSync(claim, { force: true });
171
- }
172
- return retainedTombstone;
173
- }
174
- function quarantineStaleSeedLock(p) {
175
- if (seedLockState(p.seedLock) !== "stale")
176
- return false;
177
- const tombstone = join(p.tombstones, `seed-lock-${randomUUID()}`);
178
- try {
179
- renameSync(p.seedLock, tombstone);
180
- }
181
- catch (err) {
182
- return err.code === "ENOENT";
183
- }
184
- if (seedLockState(tombstone) === "stale") {
185
- rmSync(tombstone, { recursive: true, force: true });
186
- return true;
187
- }
188
- return false;
189
- }
190
- function stripTransientProfileState(root) {
191
- const walk = (dir) => {
192
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
193
- const path = join(dir, entry.name);
194
- if (TRANSIENT_PROFILE_NAMES.has(entry.name) || TRANSIENT_PROFILE_DIRS.has(entry.name)) {
195
- rmSync(path, { recursive: true, force: true });
196
- }
197
- else if (entry.isDirectory()) {
198
- walk(path);
199
- }
200
- }
201
- };
202
- if (existsSync(root))
203
- walk(root);
204
- }
205
- function stripPooledProfileCookies(root) {
206
- for (const relative of POOLED_COOKIE_STORES) {
207
- for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
208
- rmSync(join(root, `${relative}${suffix}`), { force: true });
209
- }
210
- }
211
- }
212
- function copyIdentitySeedFile(sourceRoot, destinationRoot, relative) {
213
- const source = join(sourceRoot, relative);
214
- try {
215
- if (!lstatSync(source).isFile())
216
- return;
217
- }
218
- catch {
219
- return;
220
- }
221
- const destination = join(destinationRoot, relative);
222
- ensurePrivateDir(dirname(destination));
223
- copyFileSync(source, destination);
224
- chmodSync(destination, 0o600);
225
- }
226
- // Pooled clones must never carry Google's session cookies: a clone presents
227
- // already-rotated-away cookie values (Google rotates them on the live
228
- // profile as it's used), and Google's server treats that as credential
229
- // replay and invalidates the whole session family — killing the user's real
230
- // Google session for a casual pooled run. Google identity now only flows
231
- // through the direct, unshared profile (operator-direct-identity.ts, gated
232
- // by operate_start's require_live_identity); the seed and its clones carry
233
- // no cookies at all.
234
- function copyIdentitySeed(sourceProfileDir, destination) {
235
- if (!lstatSync(sourceProfileDir).isDirectory()) {
236
- throw new Error(`operator login source is not a directory: ${sourceProfileDir}`);
237
- }
238
- ensurePrivateDir(destination);
239
- for (const relative of IDENTITY_SEED_FILES) {
240
- copyIdentitySeedFile(sourceProfileDir, destination, relative);
241
- }
242
- }
243
- function assertProfileAcquisitionActive(control, phase = "profile") {
244
- if (control.signal?.aborted === true) {
245
- throw new OperatorProfileAcquisitionInterruptedError("cancelled", phase);
246
- }
247
- if (control.deadline !== undefined && Date.now() >= control.deadline) {
248
- throw new OperatorProfileAcquisitionInterruptedError("timeout", phase);
249
- }
250
- }
251
- async function waitForSeedLockRetry(control) {
252
- assertProfileAcquisitionActive(control, "seed_lock");
253
- const delay = control.deadline === undefined ? 25 : Math.max(0, Math.min(25, control.deadline - Date.now()));
254
- await new Promise((resolveWait) => {
255
- let settled = false;
256
- let timer;
257
- const finish = () => {
258
- if (settled)
259
- return;
260
- settled = true;
261
- if (timer !== undefined)
262
- clearTimeout(timer);
263
- control.signal?.removeEventListener("abort", finish);
264
- resolveWait();
265
- };
266
- timer = setTimeout(finish, delay);
267
- control.signal?.addEventListener("abort", finish, { once: true });
268
- if (control.signal?.aborted === true)
269
- finish();
270
- });
271
- assertProfileAcquisitionActive(control, "seed_lock");
272
- }
273
- async function withSeedLock(p, fn, control = {}) {
274
- const token = randomUUID();
275
- const owner = {
276
- host: hostname(),
277
- ...currentProcessIdentity(),
278
- token,
279
- };
280
- for (;;) {
281
- assertProfileAcquisitionActive(control, "seed_lock");
282
- if (scavengeSeedLockArtifacts(p)) {
283
- await waitForSeedLockRetry(control);
284
- continue;
285
- }
286
- const claim = `${p.seedLock}.claim-${randomUUID()}`;
287
- writeFileSync(claim, `${JSON.stringify(owner)}\n`, { mode: 0o600, flag: "wx" });
288
- try {
289
- linkSync(claim, p.seedLock);
290
- }
291
- catch (err) {
292
- rmSync(claim, { force: true });
293
- if (err.code !== "EEXIST")
294
- throw err;
295
- if (quarantineStaleSeedLock(p))
296
- continue;
297
- await waitForSeedLockRetry(control);
298
- continue;
299
- }
300
- try {
301
- rmSync(claim, { force: true });
302
- }
303
- catch (err) {
304
- if (readSeedLockOwner(p.seedLock)?.token === token) {
305
- rmSync(p.seedLock, { recursive: true, force: true });
306
- }
307
- throw err;
308
- }
309
- break;
310
- }
311
- try {
312
- assertProfileAcquisitionActive(control, "seed_lock");
313
- return await fn();
314
- }
315
- finally {
316
- if (readSeedLockOwner(p.seedLock)?.token === token) {
317
- rmSync(p.seedLock, { recursive: true, force: true });
318
- }
319
- for (const tombstone of seedLockArtifacts(p, "stale")) {
320
- if (readSeedLockOwner(tombstone)?.token === token) {
321
- rmSync(tombstone, { recursive: true, force: true });
322
- }
323
- }
324
- }
325
- }
326
- function currentGeneration(p) {
327
- try {
328
- const target = readlinkSync(p.current);
329
- const generation = basename(target);
330
- return existsSync(join(p.generations, generation, "user-data")) ? generation : null;
331
- }
332
- catch {
333
- return null;
334
- }
335
- }
336
- async function publishSeedLocked(p, sourceProfileDir) {
337
- const generation = randomUUID();
338
- const staging = join(p.generations, `.${generation}.tmp`);
339
- const destination = join(p.generations, generation);
340
- try {
341
- ensurePrivateDir(staging);
342
- copyIdentitySeed(sourceProfileDir, join(staging, "user-data"));
343
- renameSync(staging, destination);
344
- const nextLink = join(p.seed, `.current-${generation}`);
345
- symlinkSync(join("generations", generation), nextLink, "dir");
346
- renameSync(nextLink, p.current);
347
- for (const entry of readdirSync(p.generations, { withFileTypes: true })) {
348
- if (entry.name !== generation) {
349
- rmSync(join(p.generations, entry.name), { recursive: true, force: true });
350
- }
351
- }
352
- return generation;
353
- }
354
- catch (err) {
355
- rmSync(staging, { recursive: true, force: true });
356
- throw err;
357
- }
358
- }
359
- export function assertOperatorProfileRuntimeSupported(platform = process.platform) {
360
- if (platform !== "linux") {
361
- throw new Error("operator profile leases require Linux process identity");
362
- }
363
- }
364
- export function canPublishOperatorProfileSeed(proof, platform = process.platform) {
365
- return (platform === "linux" &&
366
- proof.provider === "google" &&
367
- proof.loginStatus === "completed" &&
368
- proof.closeState === "closed");
369
- }
370
- export async function publishOperatorProfileSeed(sourceProfileDir = CHROME_PROFILE_DIR, opts) {
371
- if (!canPublishOperatorProfileSeed(opts.proof)) {
372
- throw new Error("operator profile seed publication requires explicit completed Google login provenance and verified Linux Chrome closure");
373
- }
374
- const resolvedSource = profilePathIdentity(sourceProfileDir);
375
- const p = paths(poolRootForSource(resolvedSource, opts.rootDir));
376
- initializePool(p);
377
- return await withSeedLock(p, () => publishSeedLocked(p, resolvedSource));
378
- }
379
- function profileDir(p, profileId) {
380
- return join(p.profiles, profileId, "user-data");
381
- }
382
- function readLease(claimDir) {
383
- const lease = readJson(join(claimDir, "lease.json"));
384
- if (lease?.version !== 1 ||
385
- typeof lease.lease_token !== "string" ||
386
- typeof lease.profile_id !== "string" ||
387
- !/^[0-9a-f-]{36}$/.test(lease.profile_id) ||
388
- typeof lease.seed_generation !== "string" ||
389
- typeof lease.created_at !== "number" ||
390
- typeof lease.reuse_count !== "number" ||
391
- (lease.disposition !== undefined && lease.disposition !== "destroy_required")) {
392
- return null;
393
- }
394
- return lease;
395
- }
396
- function removeProfile(p, lease) {
397
- rmSync(join(p.profiles, lease.profile_id), { recursive: true, force: true });
398
- }
399
- function quarantine(p, publicPath, label) {
400
- const privatePath = join(p.tombstones, `${label}-${randomUUID()}`);
401
- try {
402
- renameSync(publicPath, privatePath);
403
- return privatePath;
404
- }
405
- catch (err) {
406
- if (["ENOENT", "EEXIST", "ENOTEMPTY"].includes(err.code ?? "")) {
407
- return null;
408
- }
409
- throw err;
410
- }
411
- }
412
- function removeSlotContainer(p, path) {
413
- let privateClaim = null;
414
- try {
415
- if (lstatSync(path).isSymbolicLink()) {
416
- privateClaim = resolve(dirname(path), readlinkSync(path));
417
- }
418
- }
419
- catch {
420
- return;
421
- }
422
- rmSync(path, { recursive: true, force: true });
423
- if (privateClaim !== null && dirname(privateClaim) === p.activeClaims) {
424
- rmSync(privateClaim, { recursive: true, force: true });
425
- }
426
- }
427
- function reserveActiveSlot(p, index, sessionId, beforePublish) {
428
- const ownerToken = randomUUID();
429
- const privateClaim = join(p.activeClaims, randomUUID());
430
- const candidate = join(p.active, `slot-${index}`);
431
- ensurePrivateDir(privateClaim);
432
- let published = false;
433
- try {
434
- const identity = currentProcessIdentity();
435
- writePrivateJson(join(privateClaim, "owner.json"), {
436
- host: hostname(),
437
- ...identity,
438
- token: ownerToken,
439
- session_id: sessionId,
440
- });
441
- beforePublish?.();
442
- try {
443
- symlinkSync(relative(p.active, privateClaim), candidate, "dir");
444
- }
445
- catch (err) {
446
- if (err.code === "EEXIST")
447
- return null;
448
- throw err;
449
- }
450
- published = true;
451
- if (readJson(join(candidate, "owner.json"))?.token !== ownerToken) {
452
- throw new Error("operator active-slot ownership was lost during reservation");
453
- }
454
- return { slotDir: candidate, ownerToken };
455
- }
456
- finally {
457
- if (!published)
458
- rmSync(privateClaim, { recursive: true, force: true });
459
- }
460
- }
461
- function quarantineOwnedActiveSlot(p, slotDir, ownerToken, label) {
462
- if (readJson(join(slotDir, "owner.json"))?.token !== ownerToken)
463
- return null;
464
- const tombstone = quarantine(p, slotDir, label);
465
- if (tombstone === null)
466
- return null;
467
- if (readJson(join(tombstone, "owner.json"))?.token !== ownerToken) {
468
- // The renamed directory is private, so retaining an ambiguity is safe.
469
- return null;
470
- }
471
- return tombstone;
472
- }
473
- // removeProfile deletes a real, potentially large per-session Chrome profile
474
- // (cache/history/IndexedDB accumulated over real browsing) — a disk-bound
475
- // rmSync that can be genuinely slow. onRemovable, when supplied, lets the
476
- // caller defer that physical delete until after the seed lock (which this
477
- // function normally runs under, via scavengeQuarantinedActive /
478
- // scavengeActiveSlots) has been released, so an unrelated concurrent
479
- // operate_start never queues behind someone else's cleanup.
480
- function reapQuarantinedActive(p, tombstone, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess, onRemovable) {
481
- const owner = readJson(join(tombstone, "owner.json"));
482
- if (owner !== null && (owner.host !== hostname() || processState(owner) !== "stale")) {
483
- const slotIndex = /^active-(\d+)-/.exec(basename(tombstone))?.[1];
484
- if (slotIndex === undefined)
485
- return;
486
- const publicSlot = join(p.active, `slot-${slotIndex}`);
487
- try {
488
- renameSync(tombstone, publicSlot);
489
- }
490
- catch {
491
- // Another owner already reserved capacity. Keep this lease private and
492
- // leak-safe for a later manual inspection rather than killing ambiguity.
493
- }
494
- return;
495
- }
496
- const claim = join(tombstone, "claim");
497
- const lease = readLease(claim);
498
- if (lease === null) {
499
- removeSlotContainer(p, tombstone);
500
- return;
501
- }
502
- const dir = profileDir(p, lease.profile_id);
503
- const worker = lease.worker;
504
- if (worker === undefined)
505
- return;
506
- const state = workerState(worker, dir);
507
- if (state === "unknown")
508
- return;
509
- if (state === "matching") {
510
- if (!signalWorker(worker, dir, "SIGKILL"))
511
- return;
512
- return;
513
- }
514
- if (onRemovable) {
515
- onRemovable({ lease, finish: () => removeSlotContainer(p, tombstone) });
516
- }
517
- else {
518
- removeProfile(p, lease);
519
- removeSlotContainer(p, tombstone);
520
- }
521
- }
522
- function scavengeDestroyRequired(p, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess) {
523
- for (const entry of readdirSync(p.tombstones)) {
524
- if (!entry.startsWith("destroy-required-"))
525
- continue;
526
- const tombstone = join(p.tombstones, entry);
527
- const lease = readLease(join(tombstone, "claim"));
528
- if (lease?.disposition !== "destroy_required" || lease.worker === undefined)
529
- continue;
530
- const dir = profileDir(p, lease.profile_id);
531
- const state = workerState(lease.worker, dir);
532
- if (state === "unknown")
533
- continue;
534
- if (state === "matching") {
535
- signalWorker(lease.worker, dir, "SIGKILL");
536
- continue;
537
- }
538
- removeProfile(p, lease);
539
- removeSlotContainer(p, tombstone);
540
- }
541
- }
542
- function scavengeQuarantinedActive(p, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess, onRemovable) {
543
- for (const entry of readdirSync(p.tombstones)) {
544
- if (!/^active-\d+-/.test(entry))
545
- continue;
546
- reapQuarantinedActive(p, join(p.tombstones, entry), workerState, signalWorker, onRemovable);
547
- }
548
- }
549
- function scavengeActiveSlots(p, now, onRemovable) {
550
- for (let index = 0; index < ACTIVE_SLOT_COUNT; index += 1) {
551
- const slot = join(p.active, `slot-${index}`);
552
- if (!existsSync(slot))
553
- continue;
554
- const owner = readJson(join(slot, "owner.json"));
555
- if (owner === null) {
556
- if (now - statSync(slot).mtimeMs < STARTUP_GRACE_MS)
557
- continue;
558
- }
559
- else if (owner.host !== hostname() || processState(owner) !== "stale") {
560
- continue;
561
- }
562
- // The public slot is only a candidate until this rename wins. No process is
563
- // inspected again, signalled, or deleted while the lease remains claimable.
564
- const tombstone = quarantine(p, slot, `active-${index}`);
565
- if (tombstone !== null)
566
- reapQuarantinedActive(p, tombstone, undefined, undefined, onRemovable);
567
- }
568
- }
569
- // onRemovable defers the physical delete of an expired warm profile's real
570
- // (potentially large) directory to after the caller releases the seed lock —
571
- // see reapQuarantinedActive's comment for why.
572
- function scavengeWarm(p, generation, now, onRemovable) {
573
- const removeQuarantinedWarm = (tombstone) => {
574
- const lease = readLease(tombstone);
575
- if (lease === null) {
576
- rmSync(tombstone, { recursive: true, force: true });
577
- }
578
- else if (onRemovable) {
579
- onRemovable({
580
- lease,
581
- finish: () => rmSync(tombstone, { recursive: true, force: true }),
582
- });
583
- }
584
- else {
585
- removeProfile(p, lease);
586
- rmSync(tombstone, { recursive: true, force: true });
587
- }
588
- };
589
- for (const entry of readdirSync(p.tombstones)) {
590
- if (entry.startsWith("warm-"))
591
- removeQuarantinedWarm(join(p.tombstones, entry));
592
- }
593
- const warm = join(p.warm, "slot-0");
594
- if (!existsSync(warm))
595
- return;
596
- const lease = readLease(warm);
597
- const expired = lease === null ||
598
- lease.worker !== undefined ||
599
- lease.seed_generation !== generation ||
600
- lease.returned_at === undefined ||
601
- now - lease.returned_at >= WARM_IDLE_TTL_MS ||
602
- now - lease.created_at >= WARM_MAX_AGE_MS ||
603
- lease.reuse_count >= WARM_MAX_REUSES;
604
- if (!expired)
605
- return;
606
- const tombstone = quarantine(p, warm, "warm");
607
- if (tombstone === null)
608
- return;
609
- removeQuarantinedWarm(tombstone);
610
- }
611
- function flushDeferredProfileRemovals(p, removals) {
612
- for (const removal of removals) {
613
- try {
614
- removeProfile(p, removal.lease);
615
- removal.finish();
616
- }
617
- catch { }
618
- }
619
- }
620
- export class OperatorProfileLease {
621
- p;
622
- slotDir;
623
- claimDir;
624
- ownerToken;
625
- descriptor;
626
- now;
627
- profileDir;
628
- profileId;
629
- seedGeneration;
630
- finished = false;
631
- constructor(p, slotDir, claimDir, ownerToken, descriptor, now) {
632
- this.p = p;
633
- this.slotDir = slotDir;
634
- this.claimDir = claimDir;
635
- this.ownerToken = ownerToken;
636
- this.descriptor = descriptor;
637
- this.now = now;
638
- this.profileId = descriptor.profile_id;
639
- this.seedGeneration = descriptor.seed_generation;
640
- this.profileDir = profileDir(p, descriptor.profile_id);
641
- }
642
- bindWorker(worker) {
643
- if (this.finished)
644
- throw new Error("operator profile lease is already released");
645
- if (worker.host !== hostname() ||
646
- profilePathIdentity(worker.user_data_dir) !== profilePathIdentity(this.profileDir)) {
647
- throw new Error("operator worker identity does not match its leased profile");
648
- }
649
- this.descriptor = { ...this.descriptor, worker };
650
- writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
651
- }
652
- async returnWarm(closeState) {
653
- if (this.finished)
654
- return;
655
- if (this.descriptor.disposition === "destroy_required") {
656
- if (closeState === "closed")
657
- await this.destroy();
658
- else
659
- await this.retain(true);
660
- return;
661
- }
662
- if (closeState !== "closed") {
663
- await this.retain();
664
- return;
665
- }
666
- const withoutWorker = { ...this.descriptor };
667
- delete withoutWorker.worker;
668
- this.descriptor = {
669
- ...withoutWorker,
670
- returned_at: this.now(),
671
- reuse_count: this.descriptor.reuse_count + 1,
672
- };
673
- writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
674
- const current = currentGeneration(this.p);
675
- const warm = join(this.p.warm, "slot-0");
676
- let pooled = current === this.descriptor.seed_generation;
677
- if (pooled) {
678
- try {
679
- renameSync(this.claimDir, warm);
680
- }
681
- catch (err) {
682
- if (!["EEXIST", "ENOTEMPTY"].includes(err.code ?? ""))
683
- throw err;
684
- pooled = false;
685
- }
686
- }
687
- if (!pooled) {
688
- this.discardOwnedSlot();
689
- this.finished = true;
690
- return;
691
- }
692
- this.releaseSlot();
693
- this.finished = true;
694
- }
695
- async destroy() {
696
- if (this.finished)
697
- return;
698
- this.descriptor = { ...this.descriptor, disposition: "destroy_required" };
699
- writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
700
- this.discardOwnedSlot("destroy-required");
701
- this.finished = true;
702
- }
703
- async retain(destroyRequired = false) {
704
- if (this.finished)
705
- return;
706
- if (destroyRequired || this.descriptor.disposition === "destroy_required") {
707
- this.descriptor = { ...this.descriptor, disposition: "destroy_required" };
708
- writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
709
- }
710
- quarantineOwnedActiveSlot(this.p, this.slotDir, this.ownerToken, this.descriptor.disposition === "destroy_required" ? "destroy-required" : "unverified-close");
711
- this.finished = true;
712
- }
713
- discardOwnedSlot(label = "owned-active") {
714
- const tombstone = quarantineOwnedActiveSlot(this.p, this.slotDir, this.ownerToken, label);
715
- if (tombstone === null)
716
- return;
717
- const quarantinedLease = readLease(join(tombstone, "claim"));
718
- if (quarantinedLease?.lease_token !== this.descriptor.lease_token) {
719
- // Ownership changed or the descriptor became ambiguous. The private
720
- // tombstone is no longer claimable, so retain it without deleting data.
721
- return;
722
- }
723
- removeProfile(this.p, quarantinedLease);
724
- removeSlotContainer(this.p, tombstone);
725
- }
726
- releaseSlot() {
727
- const owner = readJson(join(this.slotDir, "owner.json"));
728
- if (owner?.token === this.ownerToken)
729
- removeSlotContainer(this.p, this.slotDir);
730
- }
731
- }
732
- export async function acquireOperatorProfile(sessionId, opts = {}) {
733
- assertOperatorProfileRuntimeSupported();
734
- const sourceProfileDir = profilePathIdentity(opts.sourceProfileDir ?? CHROME_PROFILE_DIR);
735
- const p = paths(poolRootForSource(sourceProfileDir, opts.rootDir));
736
- const now = opts.now ?? Date.now;
737
- initializePool(p);
738
- scavengeDestroyRequired(p);
739
- // Collected by scavengeQuarantinedActive/scavengeActiveSlots instead of
740
- // physically removed inline, so the (potentially slow, disk-bound) removal
741
- // of a real per-session profile directory happens after the seed lock
742
- // below is released, never while it's held.
743
- const reclaimedFromReservation = [];
744
- const reservation = await withSeedLock(p, () => {
745
- scavengeQuarantinedActive(p, undefined, undefined, (removal) => reclaimedFromReservation.push(removal));
746
- scavengeActiveSlots(p, now(), (removal) => reclaimedFromReservation.push(removal));
747
- for (let index = 0; index < ACTIVE_SLOT_COUNT; index += 1) {
748
- const claimed = reserveActiveSlot(p, index, sessionId);
749
- if (claimed !== null)
750
- return claimed;
751
- }
752
- return null;
753
- }, opts);
754
- flushDeferredProfileRemovals(p, reclaimedFromReservation);
755
- if (reservation === null) {
756
- throw new Error("operate_start capacity reached: 2 operator sessions are active; finish one and retry");
757
- }
758
- const { slotDir, ownerToken } = reservation;
759
- const claimDir = join(slotDir, "claim");
760
- let allocatedProfileId = null;
761
- // Same deferred-removal reasoning as reclaimedFromReservation above, for
762
- // the expired-warm-profile case surfaced while cloning/reusing.
763
- const reclaimedFromWarm = [];
764
- try {
765
- assertProfileAcquisitionActive(opts);
766
- const acquiredDescriptor = await withSeedLock(p, () => {
767
- const generation = currentGeneration(p);
768
- scavengeWarm(p, generation ?? UNPUBLISHED_SEED_GENERATION, now(), (removal) => reclaimedFromWarm.push(removal));
769
- const warm = join(p.warm, "slot-0");
770
- let descriptor = null;
771
- try {
772
- renameSync(warm, claimDir);
773
- descriptor = readLease(claimDir);
774
- }
775
- catch (err) {
776
- if (err.code !== "ENOENT")
777
- throw err;
778
- }
779
- if (descriptor !== null) {
780
- stripPooledProfileCookies(profileDir(p, descriptor.profile_id));
781
- const activeDescriptor = { ...descriptor };
782
- delete activeDescriptor.returned_at;
783
- delete activeDescriptor.worker;
784
- writePrivateJson(join(claimDir, "lease.json"), activeDescriptor);
785
- return activeDescriptor;
786
- }
787
- rmSync(claimDir, { recursive: true, force: true });
788
- const profileId = randomUUID();
789
- allocatedProfileId = profileId;
790
- const profileRoot = join(p.profiles, profileId);
791
- ensurePrivateDir(profileRoot);
792
- const userDataDir = join(profileRoot, "user-data");
793
- if (generation === null) {
794
- ensurePrivateDir(userDataDir);
795
- }
796
- else {
797
- cpSync(join(p.generations, generation, "user-data"), userDataDir, {
798
- recursive: true,
799
- force: false,
800
- errorOnExist: true,
801
- });
802
- stripTransientProfileState(userDataDir);
803
- }
804
- stripPooledProfileCookies(userDataDir);
805
- ensurePrivateDir(claimDir);
806
- const coldDescriptor = {
807
- version: 1,
808
- lease_token: randomUUID(),
809
- profile_id: profileId,
810
- seed_generation: generation ?? UNPUBLISHED_SEED_GENERATION,
811
- created_at: now(),
812
- reuse_count: 0,
813
- };
814
- writePrivateJson(join(claimDir, "lease.json"), coldDescriptor);
815
- return coldDescriptor;
816
- }, opts);
817
- assertProfileAcquisitionActive(opts);
818
- return new OperatorProfileLease(p, slotDir, claimDir, ownerToken, acquiredDescriptor, now);
819
- }
820
- catch (err) {
821
- const tombstone = quarantineOwnedActiveSlot(p, slotDir, ownerToken, "failed-active");
822
- if (tombstone !== null) {
823
- const lease = readLease(join(tombstone, "claim"));
824
- let descriptorWasSafe = false;
825
- if (lease !== null) {
826
- removeProfile(p, lease);
827
- descriptorWasSafe = true;
828
- }
829
- else if (allocatedProfileId !== null) {
830
- rmSync(join(p.profiles, allocatedProfileId), { recursive: true, force: true });
831
- descriptorWasSafe = true;
832
- }
833
- if (descriptorWasSafe)
834
- removeSlotContainer(p, tombstone);
835
- }
836
- throw err;
837
- }
838
- finally {
839
- flushDeferredProfileRemovals(p, reclaimedFromWarm);
840
- }
841
- }
842
- export function localWorkerIdentity(pid, userDataDir) {
843
- return profileProcessIdentity(pid, userDataDir);
844
- }
845
- export const operatorProfilePoolTest = {
846
- paths,
847
- poolRootForSource,
848
- currentGeneration,
849
- withSeedLock,
850
- reserveActiveSlot,
851
- scavengeQuarantinedActive,
852
- scavengeDestroyRequired,
853
- resetDefaultPool: () => rmSync(defaultPoolRoot(), { recursive: true, force: true }),
854
- };
855
- //# sourceMappingURL=operator-profile-pool.js.map