@tinycloud/operations 0.3.4-beta.6 → 0.4.0-beta.8

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.
@@ -6965,6 +6965,9 @@ function evaluateOperationAuthority(granted, required, resolveSpace2) {
6965
6965
  missing: canonicalizeCapabilities(missing)
6966
6966
  };
6967
6967
  }
6968
+ function delegationWithinRequest(requested, effective, resolveSpace2) {
6969
+ return requested.length > 0 && evaluateOperationAuthority(requested, canonicalizeCapabilities(effective), resolveSpace2).satisfied;
6970
+ }
6968
6971
  function isExactCapabilityMemberSubset(hinted, planned, resolveSpace2) {
6969
6972
  const canonicalHinted = canonicalizeOperationCapabilities(hinted, resolveSpace2);
6970
6973
  const canonicalPlanned = canonicalizeOperationCapabilities(planned, resolveSpace2);
@@ -7083,16 +7086,23 @@ import {
7083
7086
  chmod,
7084
7087
  link,
7085
7088
  mkdir,
7089
+ open,
7086
7090
  readFile,
7087
7091
  readdir,
7088
7092
  rename,
7089
7093
  rmdir,
7090
7094
  rm,
7091
7095
  stat,
7096
+ unlink,
7092
7097
  writeFile
7093
7098
  } from "fs/promises";
7094
7099
  import { homedir } from "os";
7095
7100
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
7101
+ function processWideContext(key) {
7102
+ var _a2;
7103
+ const registry = globalThis;
7104
+ return registry[_a2 = Symbol.for(key)] ?? (registry[_a2] = new AsyncLocalStorage());
7105
+ }
7096
7106
  function tinycloudHomePath() {
7097
7107
  const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
7098
7108
  return join(home, ".tinycloud");
@@ -7131,6 +7141,9 @@ function profileStoreMetadataPath(profile, store) {
7131
7141
  function profileLockPath(profile) {
7132
7142
  return join(profilePath(profile), ".lock");
7133
7143
  }
7144
+ function profileTurnLockPath(profile) {
7145
+ return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
7146
+ }
7134
7147
  async function readJson(filePath) {
7135
7148
  try {
7136
7149
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -7140,6 +7153,9 @@ async function readJson(filePath) {
7140
7153
  }
7141
7154
  }
7142
7155
  async function writeJsonAtomic(filePath, value) {
7156
+ await replaceJson(filePath, value, false);
7157
+ }
7158
+ async function replaceJson(filePath, value, durable) {
7143
7159
  const directory = dirname(filePath);
7144
7160
  const temporaryPath = join(
7145
7161
  directory,
@@ -7149,13 +7165,25 @@ async function writeJsonAtomic(filePath, value) {
7149
7165
  `;
7150
7166
  await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7151
7167
  try {
7152
- await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7168
+ if (durable) await writeFileDurably(temporaryPath, contents);
7169
+ else await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7153
7170
  await rename(temporaryPath, filePath);
7154
7171
  } catch (error) {
7155
7172
  await rm(temporaryPath, { force: true }).catch(() => void 0);
7156
7173
  throw error;
7157
7174
  }
7158
7175
  }
7176
+ async function writeFileDurably(path, contents) {
7177
+ const handle = await open(path, "wx", PRIVATE_FILE_MODE);
7178
+ try {
7179
+ if (contents) {
7180
+ await handle.writeFile(contents, "utf8");
7181
+ await handle.datasync();
7182
+ }
7183
+ } finally {
7184
+ await handle.close();
7185
+ }
7186
+ }
7159
7187
  async function readProfile(profile) {
7160
7188
  return readJson(profileConfigPath(profile));
7161
7189
  }
@@ -7192,15 +7220,28 @@ async function withProfileLock(profile, action, options = {}) {
7192
7220
  const lockPath = profileLockPath(normalizedProfile);
7193
7221
  const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7194
7222
  if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7195
- const release = await acquireProfileLock(normalizedProfile, options);
7196
- const ownership = { lockPath, active: true };
7223
+ const acquired = await acquireProfileLock(normalizedProfile, options);
7224
+ const ownership = {
7225
+ lockPath,
7226
+ turnSlot: acquired.turnSlot,
7227
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
7228
+ active: true
7229
+ };
7197
7230
  try {
7198
7231
  return await heldProfileLocks.run([...held, ownership], action);
7199
7232
  } finally {
7200
7233
  ownership.active = false;
7201
- await release();
7234
+ await acquired.release();
7235
+ if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
7202
7236
  }
7203
7237
  }
7238
+ async function refuseWriteToDeletedProfile(profile) {
7239
+ const lockPath = profileLockPath(profile);
7240
+ const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
7241
+ if (!ownership?.deletedWhileWaiting) return;
7242
+ if (await exists(profileConfigPath(profile))) return;
7243
+ throw new ProfileDeletedError(profile);
7244
+ }
7204
7245
  async function updateProfileStore(profile, store, update, options = {}) {
7205
7246
  return withProfileLock(
7206
7247
  profile,
@@ -7209,6 +7250,7 @@ async function updateProfileStore(profile, store, update, options = {}) {
7209
7250
  );
7210
7251
  }
7211
7252
  async function updateProfileStoreWhileLocked(profile, store, update) {
7253
+ await refuseWriteToDeletedProfile(profile);
7212
7254
  const current = await readProfileStore(profile, store);
7213
7255
  const next = await update(current.records);
7214
7256
  await writeJsonAtomic(profileStorePath(profile, store), next.records);
@@ -7219,32 +7261,271 @@ async function writeFormatOneMetadata(profile, store) {
7219
7261
  await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
7220
7262
  }
7221
7263
  async function acquireProfileLock(profile, options) {
7222
- const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
7223
- const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;
7264
+ const deadline = {
7265
+ startedAt: Date.now(),
7266
+ timeoutMs: options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,
7267
+ retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
7268
+ };
7224
7269
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7225
- const startedAt = Date.now();
7226
7270
  const lockPath = profileLockPath(profile);
7271
+ const turnDirectory = profileTurnLockPath(profile);
7227
7272
  const directory = profilePath(profile);
7228
7273
  await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7274
+ await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
7229
7275
  for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
7230
7276
  await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
7231
7277
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
7232
7278
  });
7233
7279
  }
7234
- while (true) {
7235
- const token = randomUUID();
7236
- if (await publishProfileLock(profile, lockPath, token)) {
7237
- return async () => {
7238
- await releaseProfileLock(profile, lockPath, token);
7239
- };
7280
+ const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
7281
+ try {
7282
+ const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
7283
+ const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
7284
+ while (true) {
7285
+ const token = randomUUID();
7286
+ if (await publishProfileLock(profile, lockPath, token)) {
7287
+ return {
7288
+ deletedWhileWaiting,
7289
+ turnSlot: turn.slot,
7290
+ release: async () => {
7291
+ try {
7292
+ await releaseProfileLock(profile, lockPath, token);
7293
+ } finally {
7294
+ await releaseTurn(profile, turn);
7295
+ }
7296
+ }
7297
+ };
7298
+ }
7299
+ await signalTestLockContention(profile);
7300
+ if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7301
+ await waitOrTimeOut(profile, deadline);
7302
+ }
7303
+ } catch (error) {
7304
+ await releaseTurn(profile, turn);
7305
+ throw error;
7306
+ }
7307
+ }
7308
+ function checkDeadline(profile, deadline, detail) {
7309
+ if (Date.now() - deadline.startedAt >= deadline.timeoutMs) {
7310
+ throw new ProfileLockTimeoutError(profile, deadline.timeoutMs, detail);
7311
+ }
7312
+ }
7313
+ async function waitOrTimeOut(profile, deadline, detail) {
7314
+ checkDeadline(profile, deadline, detail);
7315
+ await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
7316
+ }
7317
+ async function acquireTurn(profile, directory, deadline) {
7318
+ let firstWaitedTurn;
7319
+ let problem;
7320
+ const damaged = (what) => `The turn lock ${directory} ${what}, so no process can take it. If no tc or MCP process is using profile "${profile}", remove that directory.`;
7321
+ for (let first = true; ; first = false) {
7322
+ if (!first) checkDeadline(profile, deadline, problem);
7323
+ problem = void 0;
7324
+ const latest = await readLatestTurn(directory);
7325
+ if (latest === "missing") {
7326
+ await createTurnDirectory(directory);
7327
+ continue;
7328
+ }
7329
+ if (latest === "empty") {
7330
+ problem = damaged("has no turns");
7331
+ await waitOrTimeOut(profile, deadline, problem);
7332
+ continue;
7333
+ }
7334
+ if (latest === null) continue;
7335
+ if (typeof latest === "number") {
7336
+ problem = damaged(`has an unreadable turn ${latest}`);
7337
+ await waitOrTimeOut(profile, deadline, problem);
7338
+ continue;
7339
+ }
7340
+ firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
7341
+ if (latest.held && latest.done) {
7342
+ const turn = await takeTurn(profile, directory, latest);
7343
+ if (turn !== null) return { turn, firstWaitedTurn };
7344
+ await waitOrTimeOut(profile, deadline);
7345
+ } else if (latest.done) {
7346
+ problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
7347
+ await waitOrTimeOut(profile, deadline, problem);
7348
+ } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
7349
+ await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
7350
+ await settleAbandonedTurn(directory, latest);
7351
+ relinquishedTurns.delete(latest.token);
7352
+ } else {
7353
+ await signalTestLockContention(profile);
7354
+ await waitOrTimeOut(profile, deadline);
7355
+ }
7356
+ }
7357
+ }
7358
+ async function readLatestTurn(directory) {
7359
+ let names;
7360
+ try {
7361
+ names = await readdir(directory);
7362
+ } catch (error) {
7363
+ if (isErrno(error, "ENOENT")) return "missing";
7364
+ throw error;
7365
+ }
7366
+ const slots = names.filter((name) => TURN.test(name)).map(Number);
7367
+ if (slots.length === 0) return "empty";
7368
+ const slot = Math.max(...slots);
7369
+ const turn = await readTurn(directory, slot);
7370
+ return turn === "damaged" ? slot : turn;
7371
+ }
7372
+ async function readTurn(directory, slot) {
7373
+ const path = join(directory, String(slot));
7374
+ let text;
7375
+ try {
7376
+ text = await readFile(join(path, "owner.json"), "utf8");
7377
+ } catch (error) {
7378
+ if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTDIR")) throw error;
7379
+ return await exists(path) ? "damaged" : null;
7380
+ }
7381
+ let owner;
7382
+ try {
7383
+ owner = JSON.parse(text);
7384
+ } catch {
7385
+ return "damaged";
7386
+ }
7387
+ if (owner === null || typeof owner !== "object" || typeof owner.token !== "string" || typeof owner.pid !== "number") {
7388
+ return "damaged";
7389
+ }
7390
+ const done = await exists(join(path, `${owner.token}.done`));
7391
+ const held = await exists(join(path, `${owner.token}.held`));
7392
+ return {
7393
+ slot,
7394
+ token: owner.token,
7395
+ pid: owner.pid,
7396
+ after: typeof owner.after === "string" ? owner.after : null,
7397
+ held,
7398
+ done
7399
+ };
7400
+ }
7401
+ async function isFinishedTurn(directory, slot, token) {
7402
+ const turn = await readTurn(directory, slot);
7403
+ if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
7404
+ return turn !== null && turn.token === token && turn.held && turn.done;
7405
+ }
7406
+ async function takeTurn(profile, directory, latest) {
7407
+ const token = randomUUID();
7408
+ const slot = latest.slot + 1;
7409
+ const path = join(directory, String(slot));
7410
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
7411
+ let published = false;
7412
+ try {
7413
+ published = await publishDirectory(
7414
+ join(directory, `.stage-${process.pid}-${token}`),
7415
+ path,
7416
+ { "owner.json": `${JSON.stringify(owner, null, 2)}
7417
+ ` },
7418
+ () => waitForTestBarrier(TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, profile)
7419
+ );
7420
+ if (published && await isFinishedTurn(directory, latest.slot, latest.token) && await markTurn(path, token, "held")) {
7421
+ return { directory, slot, token };
7422
+ }
7423
+ if (published) await markTurn(path, token, "done");
7424
+ return null;
7425
+ } catch (error) {
7426
+ if (published) settleOwnTurn(directory, slot, token);
7427
+ throw error;
7428
+ }
7429
+ }
7430
+ async function settleAbandonedTurn(directory, abandoned) {
7431
+ const path = join(directory, String(abandoned.slot));
7432
+ if (!abandoned.held && abandoned.after !== null && await isFinishedTurn(directory, abandoned.slot - 1, abandoned.after)) {
7433
+ await markTurn(path, abandoned.token, "held");
7434
+ }
7435
+ await markTurn(path, abandoned.token, "done");
7436
+ }
7437
+ function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
7438
+ relinquishedTurns.add(token);
7439
+ void (async () => {
7440
+ try {
7441
+ const turn = await readTurn(directory, slot);
7442
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
7443
+ relinquishedTurns.delete(token);
7444
+ } catch {
7445
+ if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
7446
+ }
7447
+ })();
7448
+ }
7449
+ async function markTurn(path, token, kind) {
7450
+ for (let attempt = 0; ; attempt++) {
7451
+ try {
7452
+ await writeFile(join(path, `${token}.${kind}`), "", { mode: PRIVATE_FILE_MODE, flag: "wx" });
7453
+ return true;
7454
+ } catch (error) {
7455
+ if (isErrno(error, "EEXIST")) return true;
7456
+ if (isErrno(error, "ENOENT")) return false;
7457
+ if (attempt >= MARKER_RETRY_DELAYS_MS.length) throw error;
7458
+ await sleep(MARKER_RETRY_DELAYS_MS[attempt]);
7459
+ }
7460
+ }
7461
+ }
7462
+ async function createTurnDirectory(directory) {
7463
+ const token = randomUUID();
7464
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
7465
+ await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
7466
+ "0/owner.json": `${JSON.stringify(owner, null, 2)}
7467
+ `,
7468
+ [`0/${token}.held`]: "",
7469
+ [`0/${token}.done`]: ""
7470
+ });
7471
+ }
7472
+ async function publishDirectory(stage, target, files, beforeRename) {
7473
+ try {
7474
+ for (const [name, contents] of Object.entries(files)) {
7475
+ await mkdir(dirname(join(stage, name)), { recursive: true, mode: PRIVATE_DIR_MODE });
7476
+ await writeFileDurably(join(stage, name), contents);
7477
+ }
7478
+ await beforeRename?.();
7479
+ await rename(stage, target);
7480
+ return true;
7481
+ } catch (error) {
7482
+ if (["ENOTEMPTY", "EEXIST", "ENOENT"].some((code) => isErrno(error, code)) || await exists(target)) return false;
7483
+ throw error;
7484
+ } finally {
7485
+ await rm(stage, { recursive: true, force: true }).catch(() => void 0);
7486
+ }
7487
+ }
7488
+ async function releaseTurn(profile, turn) {
7489
+ try {
7490
+ await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
7491
+ } catch {
7492
+ settleOwnTurn(turn.directory, turn.slot, turn.token);
7493
+ return;
7494
+ }
7495
+ await collectTurns(profile, turn).catch(() => void 0);
7496
+ }
7497
+ function isAbandonedTurnEntry(name) {
7498
+ if (name.startsWith(".trash-")) return true;
7499
+ const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
7500
+ return writer !== void 0 && !isProcessAlive(Number(writer));
7501
+ }
7502
+ function isAbandonedTurnDirectoryStage(name) {
7503
+ const writer = /^\.stage-.+-([0-9]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(name)?.[1];
7504
+ return writer !== void 0 && !isProcessAlive(Number(writer));
7505
+ }
7506
+ async function collectTurns(profile, turn) {
7507
+ for (const [directory, abandoned] of [
7508
+ [turn.directory, isAbandonedTurnEntry],
7509
+ [dirname(turn.directory), isAbandonedTurnDirectoryStage]
7510
+ ]) {
7511
+ for (const name of (await readdir(directory)).filter(abandoned)) {
7512
+ await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
7513
+ }
7514
+ }
7515
+ const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
7516
+ for (const slot of older) {
7517
+ const candidate = await readTurn(turn.directory, slot);
7518
+ if (candidate === null || candidate === "damaged" || !candidate.done) continue;
7519
+ if (candidate.held) {
7520
+ if (await exists(join(turn.directory, String(slot - 1)))) continue;
7521
+ const next = await readTurn(turn.directory, slot + 1);
7522
+ if (next === null || next === "damaged" || !next.held || !next.done) continue;
7240
7523
  }
7241
- await signalTestLockContention(profile);
7242
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7243
- const elapsedMs = Date.now() - startedAt;
7244
- if (elapsedMs >= timeoutMs) {
7245
- throw new ProfileLockTimeoutError(profile, timeoutMs);
7524
+ await waitForTestBarrier(TEST_LOCK_TURN_COLLECT_BARRIER_DIR, profile);
7525
+ const trash = join(turn.directory, `.trash-${randomUUID()}`);
7526
+ if (await rename(join(turn.directory, String(slot)), trash).then(() => true, () => false)) {
7527
+ await rm(trash, { recursive: true, force: true }).catch(() => void 0);
7246
7528
  }
7247
- await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7248
7529
  }
7249
7530
  }
7250
7531
  async function publishProfileLock(profile, lockPath, token) {
@@ -7257,12 +7538,18 @@ async function publishProfileLock(profile, lockPath, token) {
7257
7538
  return false;
7258
7539
  }
7259
7540
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7541
+ const ownerPath = join(lockPath, "owner.json");
7260
7542
  const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
7261
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7543
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
7544
+ `;
7262
7545
  try {
7263
- await writeFile(staged, `${JSON.stringify(owner, null, 2)}
7264
- `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7265
- await link(staged, join(lockPath, "owner.json"));
7546
+ await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7547
+ try {
7548
+ await link(staged, ownerPath);
7549
+ } catch (error) {
7550
+ if (!lacksHardLinks(error)) throw error;
7551
+ await writeFile(ownerPath, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7552
+ }
7266
7553
  return true;
7267
7554
  } catch (error) {
7268
7555
  if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
@@ -7272,6 +7559,9 @@ async function publishProfileLock(profile, lockPath, token) {
7272
7559
  await rm(staged, { force: true }).catch(() => void 0);
7273
7560
  }
7274
7561
  }
7562
+ function lacksHardLinks(error) {
7563
+ return typeof error === "object" && error !== null && "code" in error && NO_HARD_LINK_ERRORS.has(String(error.code));
7564
+ }
7275
7565
  async function releaseProfileLock(profile, lockPath, token) {
7276
7566
  const ownerPath = join(lockPath, "owner.json");
7277
7567
  const owner = await readJson(ownerPath).catch(() => null);
@@ -7295,9 +7585,10 @@ async function signalTestLockContention(profile) {
7295
7585
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7296
7586
  }
7297
7587
  async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7588
+ let aged;
7298
7589
  try {
7299
7590
  const { mtimeMs } = await stat(lockPath);
7300
- if (Date.now() - mtimeMs < staleAfterMs) return false;
7591
+ aged = Date.now() - mtimeMs >= staleAfterMs;
7301
7592
  } catch {
7302
7593
  return false;
7303
7594
  }
@@ -7306,7 +7597,16 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7306
7597
  try {
7307
7598
  const entries = await readdir(lockPath);
7308
7599
  if (entries.includes("owner.json")) return false;
7309
- if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7600
+ const onlyOrphanedClaims = entries.length > 0 && entries.every((entry) => RECOVERY_CLAIM.test(entry));
7601
+ if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
7602
+ for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
7603
+ const claimPath = join(lockPath, name);
7604
+ if (await hasLiveRecoveryOwner(claimPath)) {
7605
+ if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
7606
+ return false;
7607
+ }
7608
+ await rm(claimPath, { force: true });
7609
+ }
7310
7610
  for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7311
7611
  await rm(join(lockPath, name), { force: true });
7312
7612
  }
@@ -7317,6 +7617,23 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7317
7617
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
7318
7618
  return true;
7319
7619
  }
7620
+ async function hasLiveRecoveryOwner(claimPath) {
7621
+ let contents;
7622
+ try {
7623
+ contents = await readFile(claimPath, "utf8");
7624
+ } catch (error) {
7625
+ if (isErrno(error, "ENOENT")) return false;
7626
+ throw error;
7627
+ }
7628
+ let owner;
7629
+ try {
7630
+ owner = JSON.parse(contents);
7631
+ } catch {
7632
+ return false;
7633
+ }
7634
+ const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
7635
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
7636
+ }
7320
7637
  function recoveryFenced(since, staleAfterMs) {
7321
7638
  return performance.now() - since >= staleAfterMs / 2;
7322
7639
  }
@@ -7331,37 +7648,57 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7331
7648
  }
7332
7649
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7333
7650
  const ownerPath = join(lockPath, "owner.json");
7334
- const owner = await readJson(ownerPath).catch(() => null);
7335
- if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7336
- if (!isStaleOwner(owner, staleAfterMs)) return false;
7337
- const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7651
+ const observed = await readFile(ownerPath, "utf8").catch(() => null);
7652
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7653
+ if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
7338
7654
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7339
- const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
7655
+ const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
7656
+ let moved = false;
7340
7657
  try {
7341
7658
  await link(ownerPath, claimPath);
7342
- } catch {
7343
- return false;
7659
+ } catch (error) {
7660
+ if (!lacksHardLinks(error)) return false;
7661
+ try {
7662
+ await rename(ownerPath, claimPath);
7663
+ moved = true;
7664
+ } catch {
7665
+ return false;
7666
+ }
7344
7667
  }
7345
- const claimedAt = performance.now();
7346
- await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7347
- const claimed = await readJson(claimPath).catch(() => null);
7348
- const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7668
+ if (moved) await waitForTestBarrier(TEST_LOCK_MOVED_BARRIER_DIR, profile);
7669
+ const sameInstance = await readFile(claimPath, "utf8").then((claimed) => claimed === observed, () => false);
7349
7670
  await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7350
- if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7351
- await rm(claimPath, { force: true });
7352
- await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7671
+ if (!sameInstance) {
7672
+ if (moved) {
7673
+ await rename(claimPath, ownerPath).catch(() => void 0);
7674
+ } else {
7675
+ await unlink(claimPath).catch(() => void 0);
7676
+ }
7353
7677
  return false;
7354
7678
  }
7355
- if (sameInstance) {
7356
- await rm(ownerPath, { force: true });
7357
- await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7679
+ if (!moved) await rm(ownerPath, { force: true });
7680
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7681
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7682
+ for (const name of (await readdir(lockPath).catch(() => [])).filter((entry) => RECOVERY_CLAIM.test(entry))) {
7683
+ if (join(lockPath, name) !== claimPath) await rm(join(lockPath, name), { force: true });
7358
7684
  }
7359
- await rm(claimPath, { force: true });
7360
- await rmdir(lockPath).catch(() => void 0);
7361
- if (!sameInstance) return false;
7685
+ if (await unlink(claimPath).then(() => true, () => false)) await rmdir(lockPath).catch(() => void 0);
7362
7686
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
7363
7687
  return true;
7364
7688
  }
7689
+ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
7690
+ let owner;
7691
+ try {
7692
+ owner = JSON.parse(observed);
7693
+ } catch {
7694
+ owner = null;
7695
+ }
7696
+ if (typeof owner === "object" && owner !== null && "pid" in owner) {
7697
+ return isStaleOwner(owner, staleAfterMs);
7698
+ }
7699
+ const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
7700
+ return Date.now() - modifiedAt >= staleAfterMs;
7701
+ }
7365
7702
  async function waitForTestBarrier(environmentName, profile) {
7366
7703
  if (process.env.NODE_ENV !== "test") return;
7367
7704
  const barrierDirectory = process.env[environmentName];
@@ -7388,6 +7725,15 @@ function isStaleOwner(owner, staleAfterMs) {
7388
7725
  }
7389
7726
  return !isProcessAlive(owner.pid);
7390
7727
  }
7728
+ async function exists(path) {
7729
+ try {
7730
+ await stat(path);
7731
+ return true;
7732
+ } catch (error) {
7733
+ if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR")) return false;
7734
+ throw error;
7735
+ }
7736
+ }
7391
7737
  function isProcessAlive(pid) {
7392
7738
  try {
7393
7739
  process.kill(pid, 0);
@@ -7408,7 +7754,7 @@ function isErrno(error, code) {
7408
7754
  function sleep(milliseconds) {
7409
7755
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
7410
7756
  }
7411
- var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_FENCED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ABANDONED_CLAIM, STAGED_OWNER;
7757
+ var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
7412
7758
  var init_state = __esm({
7413
7759
  "src/state.ts"() {
7414
7760
  "use strict";
@@ -7425,18 +7771,36 @@ var init_state = __esm({
7425
7771
  TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
7426
7772
  TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
7427
7773
  TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
7428
- TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
7429
7774
  TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
7430
- invocationStateRoot = new AsyncLocalStorage();
7431
- heldProfileLocks = new AsyncLocalStorage();
7775
+ TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
7776
+ TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
7777
+ TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
7778
+ TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
7779
+ invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
7780
+ heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
7432
7781
  ProfileLockTimeoutError = class extends Error {
7433
- constructor(profile, timeoutMs) {
7434
- super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
7782
+ /** `detail` names the cause when it is not simply another process holding the lock. */
7783
+ constructor(profile, timeoutMs, detail) {
7784
+ super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.${detail ? ` ${detail}` : ""}`);
7435
7785
  this.code = "PROFILE_LOCK_TIMEOUT";
7436
7786
  this.name = "ProfileLockTimeoutError";
7437
7787
  }
7438
7788
  };
7789
+ ProfileDeletedError = class extends Error {
7790
+ constructor(profile) {
7791
+ super(`Profile "${profile}" was deleted while this change waited for its lock, so the change was not written.`);
7792
+ this.code = "PROFILE_NOT_FOUND";
7793
+ this.name = "ProfileDeletedError";
7794
+ }
7795
+ };
7796
+ TURN = /^(?:0|[1-9][0-9]*)$/;
7797
+ relinquishedTurns = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Set());
7798
+ MARKER_RETRY_DELAYS_MS = [10, 50, 250];
7799
+ SETTLE_RETRY_MS = 1e3;
7800
+ SETTLE_RETRIES = 300;
7801
+ NO_HARD_LINK_ERRORS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EISDIR", "EMLINK"]);
7439
7802
  ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7803
+ RECOVERY_CLAIM = /^\.recover-[0-9a-f-]+\.json$/;
7440
7804
  STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7441
7805
  }
7442
7806
  });
@@ -7645,7 +8009,7 @@ function evaluateArtifactAuthority(granted, required, resolveSpace2) {
7645
8009
  function defaultRequestId(now) {
7646
8010
  return `req_${now.getTime().toString(36)}_${randomUUID2().replace(/-/g, "")}`;
7647
8011
  }
7648
- var THIRTY_DAYS_MS, PermissionEntrySchema, CommandSchema, LegacyPermissionRequestArtifactSchema, PortableDelegationSchema, PermissionRequestArtifactSchema, DelegationImportArtifactSchema;
8012
+ var THIRTY_DAYS_MS, PermissionEntrySchema, CommandSchema, LegacyPermissionRequestArtifactSchema, PortableDelegationSchema, PermissionRequestArtifactSchema, DelegationImportArtifactSchema, DelegationRequestBindingSchema;
7649
8013
  var init_artifacts = __esm({
7650
8014
  "src/artifacts.ts"() {
7651
8015
  "use strict";
@@ -7718,16 +8082,12 @@ var init_artifacts = __esm({
7718
8082
  expiry: z2.string().datetime({ offset: true }).optional(),
7719
8083
  prompted: z2.boolean().optional()
7720
8084
  }).strict();
8085
+ DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
7721
8086
  }
7722
8087
  });
7723
8088
 
7724
8089
  // src/secrets.ts
7725
- import {
7726
- canonicalizeAddress,
7727
- makePkhSpaceId,
7728
- parsePkhDid,
7729
- parseSpaceUri
7730
- } from "@tinycloud/node-sdk";
8090
+ import * as nodeSdk from "@tinycloud/node-sdk";
7731
8091
  function normalizeSecretsGetInput(input) {
7732
8092
  const reference = resolveSecretReference(input);
7733
8093
  return {
@@ -7783,18 +8143,18 @@ function resolveOperationSpace(space, node, ownerSpace) {
7783
8143
  if (resolved !== void 0) return canonicalizeSpaceUri(resolved);
7784
8144
  }
7785
8145
  const did = typeof candidate.did === "string" ? candidate.did : void 0;
7786
- const pkh = did === void 0 ? null : parsePkhDid(did);
8146
+ const pkh = did === void 0 ? null : nodeSdk.parsePkhDid(did);
7787
8147
  if (pkh === null) {
7788
8148
  throw invalidSecretInput();
7789
8149
  }
7790
- return makePkhSpaceId(pkh.address, pkh.chainId, space);
8150
+ return nodeSdk.makePkhSpaceId(pkh.address, pkh.chainId, space);
7791
8151
  }
7792
8152
  function ownedSpaceForName(ownerSpace, name) {
7793
- const parsed = parseSpaceUri(ownerSpace);
8153
+ const parsed = nodeSdk.parseSpaceUri(ownerSpace);
7794
8154
  if (parsed === null) return void 0;
7795
8155
  try {
7796
- const pkh = parsePkhDid(parsed.owner);
7797
- return pkh === null ? void 0 : makePkhSpaceId(pkh.address, pkh.chainId, name);
8156
+ const pkh = nodeSdk.parsePkhDid(parsed.owner);
8157
+ return pkh === null ? void 0 : nodeSdk.makePkhSpaceId(pkh.address, pkh.chainId, name);
7798
8158
  } catch {
7799
8159
  return void 0;
7800
8160
  }
@@ -7809,7 +8169,7 @@ function canonicalizeSpaceUri(space) {
7809
8169
  /^tinycloud:pkh:eip155:(\d+):(0x[0-9a-fA-F]{40}):([^:]+)$/
7810
8170
  );
7811
8171
  if (pkh !== null) {
7812
- return `tinycloud:pkh:eip155:${pkh[1]}:${canonicalizeAddress(pkh[2]).toLowerCase()}:${pkh[3]}`;
8172
+ return `tinycloud:pkh:eip155:${pkh[1]}:${nodeSdk.canonicalizeAddress(pkh[2]).toLowerCase()}:${pkh[3]}`;
7813
8173
  }
7814
8174
  return space;
7815
8175
  }
@@ -7862,7 +8222,7 @@ var init_secrets = __esm({
7862
8222
  });
7863
8223
 
7864
8224
  // src/operations/auth.ts
7865
- import * as nodeSdk from "@tinycloud/node-sdk";
8225
+ import * as nodeSdk2 from "@tinycloud/node-sdk";
7866
8226
  import { z as z3 } from "zod";
7867
8227
  async function requestExactAuthority(context, input) {
7868
8228
  const sessionDid = context.summary.sessionDid;
@@ -8007,7 +8367,7 @@ async function importRequestBoundDelegation(context, input) {
8007
8367
  };
8008
8368
  let activated;
8009
8369
  try {
8010
- activated = await nodeSdk.activateValidatedRuntimeDelegation(
8370
+ activated = await nodeSdk2.activateValidatedRuntimeDelegation(
8011
8371
  context.runtime.node,
8012
8372
  delegation,
8013
8373
  { host }
@@ -8023,11 +8383,7 @@ async function importRequestBoundDelegation(context, input) {
8023
8383
  context.runtime.node,
8024
8384
  context.summary.space
8025
8385
  );
8026
- if (request.requested.length === 0 || !evaluateOperationAuthority(
8027
- request.requested,
8028
- effectivePermissions,
8029
- resolveSpace2
8030
- ).satisfied) {
8386
+ if (!delegationWithinRequest(request.requested, effectivePermissions, resolveSpace2)) {
8031
8387
  return operationFailure(
8032
8388
  "DELEGATION_REJECTED",
8033
8389
  "The delegation exceeds the stored authority request."
@@ -8055,12 +8411,19 @@ async function importRequestBoundDelegation(context, input) {
8055
8411
  )
8056
8412
  };
8057
8413
  }
8058
- const alreadyPresent = records.some((entry) => delegationCid(entry) === activated.cid);
8414
+ const record = {
8415
+ delegation: activated.delegation,
8416
+ permissions: activated.effectivePermissions,
8417
+ authorityRequest: {
8418
+ requestId: request.requestId,
8419
+ requested: request.requested
8420
+ }
8421
+ };
8422
+ const position = records.findIndex((entry) => delegationCid(entry) === activated.cid);
8423
+ const alreadyPresent = position !== -1;
8424
+ const others = records.filter((entry) => delegationCid(entry) !== activated.cid);
8059
8425
  return {
8060
- records: alreadyPresent ? records : [...records, {
8061
- delegation: activated.delegation,
8062
- permissions: activated.effectivePermissions
8063
- }],
8426
+ records: alreadyPresent ? [...others.slice(0, position), record, ...others.slice(position)] : [...records, record],
8064
8427
  result: {
8065
8428
  status: "ok",
8066
8429
  output: {
@@ -9788,6 +10151,173 @@ ORDER BY type, name`;
9788
10151
  }
9789
10152
  });
9790
10153
 
10154
+ // src/delegation-binding.ts
10155
+ import { join as join2 } from "path";
10156
+ import * as nodeSdk3 from "@tinycloud/node-sdk";
10157
+ function bindingMigrationPath(profile) {
10158
+ return join2(profilePath(profile), "delegation-binding-migration.json");
10159
+ }
10160
+ function storedDelegationKind(entry) {
10161
+ if (!isRecord4(entry)) return "refused";
10162
+ const delegation = entry.delegation;
10163
+ if (!isRecord4(delegation)) return "refused";
10164
+ const header = delegation.delegationHeader;
10165
+ if (!isRecord4(header) || Object.keys(header).length !== 1 || !Object.prototype.hasOwnProperty.call(header, "Authorization") || typeof header.Authorization !== "string") {
10166
+ return "refused";
10167
+ }
10168
+ if ("siweProof" in delegation) return "signed-login";
10169
+ if (header.Authorization.includes(".")) return "compact";
10170
+ return "authorityRequest" in entry ? "refused" : "other";
10171
+ }
10172
+ function replayLimit(entry, migrated) {
10173
+ const kind = storedDelegationKind(entry);
10174
+ if (!isRecord4(entry) || kind !== "compact" && kind !== "signed-login") return void 0;
10175
+ const binding = DelegationRequestBindingSchema.safeParse(entry.authorityRequest);
10176
+ if (binding.success) return binding.data.requested;
10177
+ return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
10178
+ }
10179
+ function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
10180
+ return {
10181
+ ...record,
10182
+ authorityRequest: DelegationRequestBindingSchema.parse({
10183
+ requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
10184
+ requested: structuredClone(signed)
10185
+ }),
10186
+ authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
10187
+ };
10188
+ }
10189
+ async function prepareStoredDelegationReplay(profile, node, options) {
10190
+ if (await bindingMigrationRecorded(profile)) return true;
10191
+ if (!options.migrate || !await holdsPersistedSession(profile, node)) return false;
10192
+ try {
10193
+ return await withProfileLock(profile, async () => {
10194
+ if (await bindingMigrationRecorded(profile)) return true;
10195
+ if (!await holdsPersistedSession(profile, node)) return false;
10196
+ const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
10197
+ const bound = /* @__PURE__ */ new Map();
10198
+ const unbound = [];
10199
+ for (const entry of await readAdditionalDelegations(profile)) {
10200
+ if (!isRecord4(entry) || "authorityRequest" in entry || storedDelegationKind(entry) !== "compact") continue;
10201
+ const migrated = await migratedRecord(node, entry, options.host, recordedAt);
10202
+ if (migrated === void 0) unbound.push(String(storedCid(entry)));
10203
+ else bound.set(recordKey(entry), migrated);
10204
+ }
10205
+ if (bound.size > 0) {
10206
+ await updateProfileStore(profile, "additional-delegations", (records) => ({
10207
+ records: records.map((entry) => {
10208
+ const key = !isRecord4(entry) || "authorityRequest" in entry ? void 0 : recordKey(entry);
10209
+ return (key === void 0 ? void 0 : bound.get(key)) ?? entry;
10210
+ }),
10211
+ result: void 0
10212
+ }));
10213
+ }
10214
+ await writeJsonAtomic(bindingMigrationPath(profile), {
10215
+ formatVersion: 1,
10216
+ migratedAt: recordedAt,
10217
+ bound: [...bound.values()].map(storedCid),
10218
+ unbound
10219
+ });
10220
+ return true;
10221
+ }, { timeoutMs: MIGRATION_LOCK_TIMEOUT_MS });
10222
+ } catch {
10223
+ return false;
10224
+ }
10225
+ }
10226
+ async function holdsPersistedSession(profile, node) {
10227
+ try {
10228
+ const persisted = await readSession(profile);
10229
+ return typeof persisted?.verificationMethod === "string" && typeof node.sessionDid === "string" && persisted.verificationMethod.split("#", 1)[0] === node.sessionDid.split("#", 1)[0];
10230
+ } catch {
10231
+ return false;
10232
+ }
10233
+ }
10234
+ async function migratedRecord(node, entry, host, recordedAt) {
10235
+ const delegation = normalizeStoredDelegation(entry);
10236
+ if (delegation === void 0) return void 0;
10237
+ const signed = await signedCapabilities(node, delegation, host);
10238
+ if (signed === void 0) return void 0;
10239
+ try {
10240
+ return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
10241
+ } catch {
10242
+ return void 0;
10243
+ }
10244
+ }
10245
+ async function replayStoredDelegation(node, entry, options) {
10246
+ const delegation = normalizeStoredDelegation(entry);
10247
+ if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
10248
+ const limit = replayLimit(entry, options.migrated);
10249
+ if (limit === void 0) return void 0;
10250
+ const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
10251
+ try {
10252
+ return await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
10253
+ } catch {
10254
+ return void 0;
10255
+ }
10256
+ }
10257
+ async function bindingMigrationRecorded(profile) {
10258
+ try {
10259
+ return await readJson(bindingMigrationPath(profile)) !== null;
10260
+ } catch {
10261
+ return true;
10262
+ }
10263
+ }
10264
+ async function signedCapabilities(node, delegation, host) {
10265
+ let signed;
10266
+ try {
10267
+ await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, {
10268
+ // The transport host is not signed authority; check it at replay instead.
10269
+ host: delegation.host ?? host,
10270
+ authorize: (effective) => {
10271
+ signed = effective;
10272
+ return false;
10273
+ }
10274
+ });
10275
+ } catch {
10276
+ }
10277
+ return signed;
10278
+ }
10279
+ function normalizeStoredDelegation(entry) {
10280
+ if (!isRecord4(entry)) return void 0;
10281
+ const raw = entry.delegation;
10282
+ if (!isRecord4(raw) || !isRecord4(raw.delegationHeader)) return void 0;
10283
+ const expiry = raw.expiry instanceof Date ? raw.expiry : typeof raw.expiry === "string" ? new Date(raw.expiry) : void 0;
10284
+ if (expiry === void 0 || Number.isNaN(expiry.getTime()) || typeof raw.cid !== "string" || typeof raw.spaceId !== "string" || typeof raw.path !== "string" || !Array.isArray(raw.actions) || !raw.actions.every((action) => typeof action === "string") || typeof raw.delegateDID !== "string" || typeof raw.ownerAddress !== "string" || typeof raw.chainId !== "number" || typeof raw.delegationHeader.Authorization !== "string") {
10285
+ return void 0;
10286
+ }
10287
+ return { ...raw, expiry };
10288
+ }
10289
+ function storedCid(entry) {
10290
+ const cid = isRecord4(entry) && isRecord4(entry.delegation) ? entry.delegation.cid : void 0;
10291
+ return typeof cid === "string" ? cid : void 0;
10292
+ }
10293
+ function recordKey(entry) {
10294
+ const delegation = entry.delegation;
10295
+ if (!isRecord4(delegation) || !isRecord4(delegation.delegationHeader)) return void 0;
10296
+ return JSON.stringify([delegation.cid, delegation.delegationHeader.Authorization]);
10297
+ }
10298
+ function isRecord4(value) {
10299
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10300
+ }
10301
+ var REQUEST_ID_PREFIX, BINDING_NOTES, MIGRATION_LOCK_TIMEOUT_MS;
10302
+ var init_delegation_binding = __esm({
10303
+ "src/delegation-binding.ts"() {
10304
+ "use strict";
10305
+ init_artifacts();
10306
+ init_authority();
10307
+ init_state();
10308
+ init_secrets();
10309
+ REQUEST_ID_PREFIX = {
10310
+ migration: "migrated",
10311
+ "unbound-import": "unbound-import"
10312
+ };
10313
+ BINDING_NOTES = {
10314
+ migration: "Stored without a request binding before bindings were required. Bound at migration to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them.",
10315
+ "unbound-import": "Imported by `tc auth import` without a stored request. Bound to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them."
10316
+ };
10317
+ MIGRATION_LOCK_TIMEOUT_MS = 250;
10318
+ }
10319
+ });
10320
+
9791
10321
  // src/operations/status.ts
9792
10322
  import { z as z7 } from "zod";
9793
10323
  function createStatusDefinition(params) {
@@ -9844,7 +10374,7 @@ async function inspectPinnedProfileStatus(context, _input) {
9844
10374
  async function pinnedProfileExists(profile) {
9845
10375
  try {
9846
10376
  const storedProfile = await readProfile(profile);
9847
- return isRecord4(storedProfile);
10377
+ return isRecord5(storedProfile);
9848
10378
  } catch {
9849
10379
  return false;
9850
10380
  }
@@ -9855,16 +10385,18 @@ async function countLiveAdditionalDelegations(profile) {
9855
10385
  if (rawDelegations === null) return 0;
9856
10386
  if (!Array.isArray(rawDelegations)) throw new TypeError("Invalid additional delegation store.");
9857
10387
  const now = Date.now();
10388
+ const migrated = await bindingMigrationRecorded(profile);
9858
10389
  let count = 0;
9859
10390
  for (const entry of rawDelegations) {
10391
+ if (storedDelegationKind(entry) === "refused") continue;
9860
10392
  const expiry = storedDelegationExpiry(entry);
9861
- if (expiry.getTime() > now) count += 1;
10393
+ if (expiry.getTime() > now && replayLimit(entry, migrated) !== void 0) count += 1;
9862
10394
  }
9863
10395
  return count;
9864
10396
  }
9865
10397
  function summarizeSession(session) {
9866
10398
  if (session === null) return { present: false, expired: null, expiresAt: null };
9867
- if (!isRecord4(session)) throw new TypeError("Invalid session store.");
10399
+ if (!isRecord5(session)) throw new TypeError("Invalid session store.");
9868
10400
  const expiry = extractSessionExpiry(session);
9869
10401
  if (expiry === null) return { present: true, expired: null, expiresAt: null };
9870
10402
  return {
@@ -9874,7 +10406,7 @@ function summarizeSession(session) {
9874
10406
  };
9875
10407
  }
9876
10408
  function storedDelegationExpiry(entry) {
9877
- if (!isRecord4(entry) || !isRecord4(entry.delegation) || !("expiry" in entry.delegation)) {
10409
+ if (!isRecord5(entry) || !isRecord5(entry.delegation) || !("expiry" in entry.delegation)) {
9878
10410
  throw new TypeError("Invalid stored delegation.");
9879
10411
  }
9880
10412
  return parseExpiry(entry.delegation.expiry);
@@ -9903,13 +10435,14 @@ function profileNotFound(profile) {
9903
10435
  )
9904
10436
  };
9905
10437
  }
9906
- function isRecord4(value) {
10438
+ function isRecord5(value) {
9907
10439
  return typeof value === "object" && value !== null && !Array.isArray(value);
9908
10440
  }
9909
10441
  var StatusInputSchema, StatusOutputSchema, STATUS_POSTURES, STATUS_EXPOSURE, STATUS_SENSITIVITY, statusOperationDefinitions;
9910
10442
  var init_status = __esm({
9911
10443
  "src/operations/status.ts"() {
9912
10444
  "use strict";
10445
+ init_delegation_binding();
9913
10446
  init_errors();
9914
10447
  init_state();
9915
10448
  StatusInputSchema = z7.object({}).strict();
@@ -9932,7 +10465,9 @@ var init_status = __esm({
9932
10465
  expired: z7.boolean().nullable(),
9933
10466
  expiresAt: z7.string().datetime({ offset: true }).nullable()
9934
10467
  }).strict(),
9935
- liveAdditionalDelegationCount: z7.number().int().nonnegative()
10468
+ liveAdditionalDelegationCount: z7.number().int().nonnegative().describe(
10469
+ "Stored, unexpired delegations the replay rule accepts: compact-UCAN or signed-login records with a request binding, and unbound compact-UCAN records before the profile's one-time binding migration. Each is still validated, and checked against its binding, when a runtime installs it."
10470
+ )
9936
10471
  }).strict();
9937
10472
  STATUS_POSTURES = [
9938
10473
  "owner-openkey",
@@ -10086,9 +10621,9 @@ function isOperatorType(value) {
10086
10621
  return value === "human" || value === "agent";
10087
10622
  }
10088
10623
  function isStoredProfile(value) {
10089
- return isRecord5(value) && isNonEmptyString(value.name) && isNonEmptyString(value.host) && typeof value.chainId === "number" && Number.isFinite(value.chainId) && isNonEmptyString(value.spaceName) && isNonEmptyString(value.did) && isNonEmptyString(value.createdAt) && isOptionalNonEmptyString(value.sessionDid) && isOptionalNonEmptyString(value.ownerDid) && isOptionalNonEmptyString(value.spaceId) && isOptionalPosture(value.posture) && isOptionalOperatorType(value.operatorType) && isOptionalAuthMethod(value.authMethod) && isOptionalNonEmptyString(value.defaultSpace) && isOptionalNonEmptyString(value.privateKey) && isOptionalNonEmptyString(value.address) && isOptionalNonEmptyString(value.openkeyHost);
10624
+ return isRecord6(value) && isNonEmptyString(value.name) && isNonEmptyString(value.host) && typeof value.chainId === "number" && Number.isFinite(value.chainId) && isNonEmptyString(value.spaceName) && isNonEmptyString(value.did) && isNonEmptyString(value.createdAt) && isOptionalNonEmptyString(value.sessionDid) && isOptionalNonEmptyString(value.ownerDid) && isOptionalNonEmptyString(value.spaceId) && isOptionalPosture(value.posture) && isOptionalOperatorType(value.operatorType) && isOptionalAuthMethod(value.authMethod) && isOptionalNonEmptyString(value.defaultSpace) && isOptionalNonEmptyString(value.privateKey) && isOptionalNonEmptyString(value.address) && isOptionalNonEmptyString(value.openkeyHost);
10090
10625
  }
10091
- function isRecord5(value) {
10626
+ function isRecord6(value) {
10092
10627
  return typeof value === "object" && value !== null && !Array.isArray(value);
10093
10628
  }
10094
10629
  function isNonEmptyString(value) {
@@ -10129,7 +10664,7 @@ var runtime_exports = {};
10129
10664
  __export(runtime_exports, {
10130
10665
  createInvocationRuntime: () => createInvocationRuntime
10131
10666
  });
10132
- import * as nodeSdk2 from "@tinycloud/node-sdk";
10667
+ import * as nodeSdk4 from "@tinycloud/node-sdk";
10133
10668
  async function createInvocationRuntime(target, requirement = "authenticated") {
10134
10669
  const resolved = await resolveInvocationProfile(target);
10135
10670
  if (!resolved.ok) {
@@ -10146,14 +10681,12 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
10146
10681
  const profileName = summary.profile;
10147
10682
  try {
10148
10683
  const {
10149
- activateValidatedRuntimeDelegation: activateValidatedRuntimeDelegation2,
10150
10684
  TinyCloudNode: TinyCloudNodeConstructor
10151
- } = nodeSdk2;
10685
+ } = nodeSdk4;
10152
10686
  const explicitPrivateKeyOverride = typeof target.privateKey === "string";
10153
- const [session, key, additionalDelegations] = explicitPrivateKeyOverride ? [null, null, []] : await Promise.all([
10687
+ const [session, key] = explicitPrivateKeyOverride ? [null, null] : await Promise.all([
10154
10688
  readSession(profileName),
10155
- readJson(`${profilePath(profileName)}/key.json`),
10156
- readAdditionalDelegations(profileName)
10689
+ readJson(`${profilePath(profileName)}/key.json`)
10157
10690
  ]);
10158
10691
  const profile = resolved.profile;
10159
10692
  const actualPosture = resolvePosture(profile);
@@ -10200,19 +10733,26 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
10200
10733
  const livePrincipalDid = normalizeDid(node.did);
10201
10734
  const authenticatedSpace = explicitPrivateKeyOverride ? spaceForAuthenticatedPrincipal(livePrincipalDid) : void 0;
10202
10735
  const livePermissions = [...node.getVerifiedSessionCapabilities()];
10736
+ const resolveSpace2 = operationSpaceResolver(node, authenticatedSpace ?? summary.space);
10203
10737
  const seenCids = /* @__PURE__ */ new Set();
10204
- for (const entry of additionalDelegations) {
10205
- const delegation = normalizeStoredDelegation(entry);
10206
- if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) continue;
10207
- try {
10208
- const activated = await activateValidatedRuntimeDelegation2(node, delegation, {
10209
- host: summary.host
10738
+ if (!explicitPrivateKeyOverride) {
10739
+ const activator = node;
10740
+ const migrated = await prepareStoredDelegationReplay(profileName, activator, {
10741
+ host: summary.host,
10742
+ // Only the profile's own restored session; a local sign-in without
10743
+ // one uses a fresh session key the stored records do not address.
10744
+ migrate: activeSession !== void 0
10745
+ });
10746
+ for (const entry of await readAdditionalDelegations(profileName)) {
10747
+ const activated = await replayStoredDelegation(activator, entry, {
10748
+ host: summary.host,
10749
+ migrated,
10750
+ resolveSpace: resolveSpace2
10210
10751
  });
10211
- if (!seenCids.has(activated.cid)) {
10752
+ if (activated !== void 0 && !seenCids.has(activated.cid)) {
10212
10753
  seenCids.add(activated.cid);
10213
10754
  livePermissions.push(...activated.effectivePermissions);
10214
10755
  }
10215
- } catch {
10216
10756
  }
10217
10757
  }
10218
10758
  const runtime = {
@@ -10244,8 +10784,8 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
10244
10784
  function spaceForAuthenticatedPrincipal(principal2) {
10245
10785
  if (principal2 === void 0) return void 0;
10246
10786
  try {
10247
- const pkh = nodeSdk2.parsePkhDid(principal2);
10248
- return pkh === null ? void 0 : nodeSdk2.makePkhSpaceId(pkh.address, pkh.chainId, "secrets");
10787
+ const pkh = nodeSdk4.parsePkhDid(principal2);
10788
+ return pkh === null ? void 0 : nodeSdk4.makePkhSpaceId(pkh.address, pkh.chainId, "secrets");
10249
10789
  } catch {
10250
10790
  return void 0;
10251
10791
  }
@@ -10253,7 +10793,7 @@ function spaceForAuthenticatedPrincipal(principal2) {
10253
10793
  function normalizeSession(session, key) {
10254
10794
  const header = session.delegationHeader;
10255
10795
  const jwk = hasPrivateParameter(session.jwk) ? session.jwk : key;
10256
- if (!isRecord6(header) || typeof header.Authorization !== "string" || typeof session.delegationCid !== "string" || typeof session.spaceId !== "string" || !hasPrivateParameter(jwk) || typeof session.verificationMethod !== "string") {
10796
+ if (!isRecord7(header) || typeof header.Authorization !== "string" || typeof session.delegationCid !== "string" || typeof session.spaceId !== "string" || !hasPrivateParameter(jwk) || typeof session.verificationMethod !== "string") {
10257
10797
  return void 0;
10258
10798
  }
10259
10799
  return {
@@ -10269,27 +10809,14 @@ function normalizeSession(session, key) {
10269
10809
  ...Array.isArray(session.tinycloudHosts) && session.tinycloudHosts.every((host) => typeof host === "string") ? { tinycloudHosts: session.tinycloudHosts } : {}
10270
10810
  };
10271
10811
  }
10272
- function normalizeStoredDelegation(entry) {
10273
- const raw = entry.delegation;
10274
- if (!isRecord6(raw) || !isRecord6(raw.delegationHeader)) return void 0;
10275
- const expiry = parseExpiry2(raw.expiry);
10276
- if (expiry === void 0 || typeof raw.cid !== "string" || typeof raw.spaceId !== "string" || typeof raw.path !== "string" || !Array.isArray(raw.actions) || !raw.actions.every((action) => typeof action === "string") || typeof raw.delegateDID !== "string" || typeof raw.ownerAddress !== "string" || typeof raw.chainId !== "number" || typeof raw.delegationHeader.Authorization !== "string") {
10277
- return void 0;
10278
- }
10279
- return { ...raw, expiry };
10280
- }
10281
- function parseExpiry2(value) {
10282
- const expiry = value instanceof Date ? value : typeof value === "string" ? new Date(value) : void 0;
10283
- return expiry !== void 0 && !Number.isNaN(expiry.getTime()) ? expiry : void 0;
10284
- }
10285
10812
  function hasPrivateParameter(value) {
10286
- return isRecord6(value) && typeof value.d === "string" && value.d.length > 0;
10813
+ return isRecord7(value) && typeof value.d === "string" && value.d.length > 0;
10287
10814
  }
10288
10815
  function normalizeDid(value) {
10289
10816
  if (typeof value !== "string" || value.length === 0) return void 0;
10290
10817
  return value.split("#", 1)[0];
10291
10818
  }
10292
- function isRecord6(value) {
10819
+ function isRecord7(value) {
10293
10820
  return typeof value === "object" && value !== null && !Array.isArray(value);
10294
10821
  }
10295
10822
  function failed(context, error) {
@@ -10308,7 +10835,9 @@ var init_runtime = __esm({
10308
10835
  init_authority();
10309
10836
  init_errors();
10310
10837
  init_profile();
10838
+ init_secrets();
10311
10839
  init_state();
10840
+ init_delegation_binding();
10312
10841
  }
10313
10842
  });
10314
10843