@tinycloud/operations 0.5.0-beta.0 → 0.5.0-beta.10

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 (51) hide show
  1. package/coverage.json +11 -1
  2. package/dist/artifacts.cjs +235 -43
  3. package/dist/artifacts.cjs.map +1 -1
  4. package/dist/artifacts.d.cts +1 -1
  5. package/dist/artifacts.d.ts +1 -1
  6. package/dist/artifacts.js +236 -43
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/cli-runtime.cjs +1021 -223
  9. package/dist/cli-runtime.cjs.map +1 -1
  10. package/dist/cli-runtime.d.cts +1 -1
  11. package/dist/cli-runtime.d.ts +1 -1
  12. package/dist/cli-runtime.js +1020 -222
  13. package/dist/cli-runtime.js.map +1 -1
  14. package/dist/{contract-DfyrluLB.d.cts → contract-zRklonbR.d.cts} +6 -3
  15. package/dist/{contract-DfyrluLB.d.ts → contract-zRklonbR.d.ts} +6 -3
  16. package/dist/delegation-binding.cjs +242 -43
  17. package/dist/delegation-binding.cjs.map +1 -1
  18. package/dist/delegation-binding.d.cts +15 -3
  19. package/dist/delegation-binding.d.ts +15 -3
  20. package/dist/delegation-binding.js +242 -43
  21. package/dist/delegation-binding.js.map +1 -1
  22. package/dist/index.cjs +1049 -224
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1046 -221
  27. package/dist/index.js.map +1 -1
  28. package/dist/profile.cjs +13 -4
  29. package/dist/profile.cjs.map +1 -1
  30. package/dist/profile.d.cts +14 -2
  31. package/dist/profile.d.ts +14 -2
  32. package/dist/profile.js +13 -4
  33. package/dist/profile.js.map +1 -1
  34. package/dist/recovery.cjs +67 -0
  35. package/dist/recovery.cjs.map +1 -0
  36. package/dist/recovery.d.cts +38 -0
  37. package/dist/recovery.d.ts +38 -0
  38. package/dist/recovery.js +39 -0
  39. package/dist/recovery.js.map +1 -0
  40. package/dist/secret-request.cjs +271 -0
  41. package/dist/secret-request.cjs.map +1 -0
  42. package/dist/secret-request.d.cts +119 -0
  43. package/dist/secret-request.d.ts +119 -0
  44. package/dist/secret-request.js +222 -0
  45. package/dist/secret-request.js.map +1 -0
  46. package/dist/state.cjs +232 -40
  47. package/dist/state.cjs.map +1 -1
  48. package/dist/state.js +233 -40
  49. package/dist/state.js.map +1 -1
  50. package/generated/operations.json +1838 -89
  51. package/package.json +15 -5
@@ -298,6 +298,7 @@ var init_errors2 = __esm({
298
298
  "PROFILE_NOT_FOUND",
299
299
  "PROFILE_POSTURE_NOT_ALLOWED",
300
300
  "PROFILE_OWNER_OPT_IN_REQUIRED",
301
+ "PROFILE_MIGRATION_REQUIRED",
301
302
  "AUTH_REQUIRED",
302
303
  "PERMISSION_DENIED",
303
304
  "SESSION_NOT_FOUND",
@@ -4106,10 +4107,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace2) {
4106
4107
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace2);
4107
4108
  const missing = [];
4108
4109
  for (const requested of canonicalRequired) {
4109
- const covered = canonicalGranted.some(
4110
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
4110
+ const sameScope = canonicalGranted.filter(
4111
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
4111
4112
  );
4112
- if (!covered) missing.push(requested);
4113
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
4113
4114
  }
4114
4115
  return {
4115
4116
  satisfied: missing.length === 0,
@@ -4233,9 +4234,11 @@ var init_authority = __esm({
4233
4234
  // src/state.ts
4234
4235
  import { randomUUID } from "crypto";
4235
4236
  import { AsyncLocalStorage } from "async_hooks";
4237
+ import { readFileSync, readlinkSync } from "fs";
4236
4238
  import {
4237
4239
  chmod,
4238
4240
  link,
4241
+ lstat,
4239
4242
  mkdir,
4240
4243
  open,
4241
4244
  readFile,
@@ -4247,12 +4250,165 @@ import {
4247
4250
  unlink,
4248
4251
  writeFile
4249
4252
  } from "fs/promises";
4253
+ import { connect, createServer } from "net";
4250
4254
  import { homedir } from "os";
4251
4255
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
4252
4256
  function processWideContext(key) {
4253
- var _a3;
4257
+ var _a4;
4254
4258
  const registry = globalThis;
4255
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
4259
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
4260
+ }
4261
+ function processOrigin() {
4262
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
4263
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
4264
+ try {
4265
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
4266
+ const pidNamespace = readlinkSync("/proc/self/ns/pid");
4267
+ const processStat = readFileSync("/proc/self/stat", "utf8");
4268
+ const closingParen = processStat.lastIndexOf(") ");
4269
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
4270
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
4271
+ return cachedProcessOrigin = null;
4272
+ }
4273
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
4274
+ } catch {
4275
+ return cachedProcessOrigin = null;
4276
+ }
4277
+ }
4278
+ function processOwner(liveness) {
4279
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
4280
+ }
4281
+ function socketDirectory(name) {
4282
+ const match = LIVENESS_SOCKET.exec(name);
4283
+ return match ? `${match[1]}-${match[2]}` : null;
4284
+ }
4285
+ async function pinDirectory(directory) {
4286
+ const handle = await open(directory, "r");
4287
+ try {
4288
+ const { dev, ino } = await handle.stat({ bigint: true });
4289
+ return {
4290
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
4291
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
4292
+ close: () => handle.close()
4293
+ };
4294
+ } catch (error) {
4295
+ await handle.close();
4296
+ throw error;
4297
+ }
4298
+ }
4299
+ async function bindLiveness(directory) {
4300
+ if (!processOrigin()) return null;
4301
+ let pinned;
4302
+ try {
4303
+ pinned = await pinDirectory(directory);
4304
+ } catch {
4305
+ return null;
4306
+ }
4307
+ const socket = `.live-${pinned.identity}-${randomUUID()}`;
4308
+ const server = createServer((connection) => connection.destroy());
4309
+ let bound = false;
4310
+ try {
4311
+ await new Promise((resolve2, reject) => {
4312
+ server.once("error", reject);
4313
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
4314
+ server.off("error", reject);
4315
+ resolve2();
4316
+ });
4317
+ });
4318
+ bound = true;
4319
+ server.on("error", () => void 0);
4320
+ server.unref();
4321
+ await rename(pinned.path(`${socket}.new`), pinned.path(socket));
4322
+ const stats = await lstat(pinned.path(socket), { bigint: true });
4323
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
4324
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
4325
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
4326
+ } catch {
4327
+ if (bound) {
4328
+ server.close();
4329
+ for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
4330
+ }
4331
+ return null;
4332
+ } finally {
4333
+ await pinned.close();
4334
+ }
4335
+ }
4336
+ async function ensureLiveness(acquisition) {
4337
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
4338
+ return acquisition.held;
4339
+ }
4340
+ async function closeLiveness(held) {
4341
+ held.server.close();
4342
+ await unlink(held.path).catch(() => void 0);
4343
+ }
4344
+ function parseLiveness(value) {
4345
+ if (typeof value !== "object" || value === null) return null;
4346
+ const { socket, dev, ino } = value;
4347
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
4348
+ return name !== null && name[3] === void 0 && typeof dev === "string" && /^[0-9]+$/.test(dev) && typeof ino === "string" && /^[0-9]+$/.test(ino) ? { socket: name[0], dev, ino } : null;
4349
+ }
4350
+ async function controlIn(pinned, acquisition) {
4351
+ const own = await ensureLiveness(acquisition);
4352
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
4353
+ const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
4354
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
4355
+ }
4356
+ async function tryConnect(path) {
4357
+ return new Promise((resolve2) => {
4358
+ const probe = connect({ path });
4359
+ const settle = (outcome) => {
4360
+ clearTimeout(timer);
4361
+ probe.destroy();
4362
+ resolve2(outcome);
4363
+ };
4364
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
4365
+ probe.once("connect", () => settle("connected"));
4366
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
4367
+ });
4368
+ }
4369
+ async function kernelRefuses(pinned, name, control) {
4370
+ if (process.versions.bun !== void 0) return false;
4371
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
4372
+ }
4373
+ async function probeLiveness(liveness, acquisition) {
4374
+ let pinned;
4375
+ try {
4376
+ pinned = await pinDirectory(acquisition.directory);
4377
+ } catch {
4378
+ return "unknown";
4379
+ }
4380
+ try {
4381
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
4382
+ const control = await controlIn(pinned, acquisition);
4383
+ if (!control) return "unknown";
4384
+ let stats;
4385
+ try {
4386
+ stats = await lstat(pinned.path(liveness.socket), { bigint: true });
4387
+ } catch (error) {
4388
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
4389
+ }
4390
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
4391
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
4392
+ } finally {
4393
+ await pinned.close();
4394
+ }
4395
+ }
4396
+ async function sweepLiveness(acquisition) {
4397
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
4398
+ const pinned = await pinDirectory(acquisition.directory);
4399
+ try {
4400
+ const control = await controlIn(pinned, acquisition);
4401
+ if (!control) return;
4402
+ for (const name of await readdir(pinned.path("."))) {
4403
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
4404
+ const stats = await lstat(pinned.path(name)).catch(() => null);
4405
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
4406
+ await unlink(pinned.path(name)).catch(() => void 0);
4407
+ }
4408
+ }
4409
+ } finally {
4410
+ await pinned.close();
4411
+ }
4256
4412
  }
4257
4413
  function tinycloudHomePath() {
4258
4414
  const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
@@ -4429,7 +4585,18 @@ async function acquireProfileLock(profile, options) {
4429
4585
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
4430
4586
  });
4431
4587
  }
4432
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4588
+ const liveness = { directory: turnDirectory, held: void 0 };
4589
+ const closeAcquisitionLiveness = async () => {
4590
+ if (liveness.held) await closeLiveness(liveness.held);
4591
+ };
4592
+ let taken;
4593
+ try {
4594
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4595
+ } catch (error) {
4596
+ await closeAcquisitionLiveness();
4597
+ throw error;
4598
+ }
4599
+ const { turn, firstWaitedTurn } = taken;
4433
4600
  try {
4434
4601
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
4435
4602
  await rmdir(directory).catch(() => void 0);
@@ -4439,7 +4606,7 @@ async function acquireProfileLock(profile, options) {
4439
4606
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
4440
4607
  while (true) {
4441
4608
  const token = randomUUID();
4442
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4609
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
4443
4610
  return {
4444
4611
  deletedWhileWaiting,
4445
4612
  turnSlot: turn.slot,
@@ -4447,17 +4614,19 @@ async function acquireProfileLock(profile, options) {
4447
4614
  try {
4448
4615
  await releaseProfileLock(profile, lockPath, token);
4449
4616
  } finally {
4450
- await releaseTurn(profile, turn);
4617
+ await releaseTurn(profile, turn, liveness);
4618
+ await closeAcquisitionLiveness();
4451
4619
  }
4452
4620
  }
4453
4621
  };
4454
4622
  }
4455
4623
  await signalTestLockContention(profile);
4456
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4624
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
4457
4625
  await waitOrTimeOut(profile, deadline);
4458
4626
  }
4459
4627
  } catch (error) {
4460
- await releaseTurn(profile, turn);
4628
+ await releaseTurn(profile, turn, liveness);
4629
+ await closeAcquisitionLiveness();
4461
4630
  throw error;
4462
4631
  }
4463
4632
  }
@@ -4470,7 +4639,7 @@ async function waitOrTimeOut(profile, deadline, detail) {
4470
4639
  checkDeadline(profile, deadline, detail);
4471
4640
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
4472
4641
  }
4473
- async function acquireTurn(profile, directory, deadline) {
4642
+ async function acquireTurn(profile, directory, deadline, liveness) {
4474
4643
  let firstWaitedTurn;
4475
4644
  let problem;
4476
4645
  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.`;
@@ -4495,13 +4664,13 @@ async function acquireTurn(profile, directory, deadline) {
4495
4664
  }
4496
4665
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
4497
4666
  if (latest.held && latest.done) {
4498
- const turn = await takeTurn(profile, directory, latest);
4667
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
4499
4668
  if (turn !== null) return { turn, firstWaitedTurn };
4500
4669
  await waitOrTimeOut(profile, deadline);
4501
4670
  } else if (latest.done) {
4502
4671
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
4503
4672
  await waitOrTimeOut(profile, deadline, problem);
4504
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4673
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
4505
4674
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
4506
4675
  await settleAbandonedTurn(directory, latest);
4507
4676
  relinquishedTurns.delete(latest.token);
@@ -4546,6 +4715,7 @@ async function readTurn(directory, slot) {
4546
4715
  const done = await exists(join(path, `${owner.token}.done`));
4547
4716
  const held = await exists(join(path, `${owner.token}.held`));
4548
4717
  return {
4718
+ ...owner,
4549
4719
  slot,
4550
4720
  token: owner.token,
4551
4721
  pid: owner.pid,
@@ -4559,11 +4729,11 @@ async function isFinishedTurn(directory, slot, token) {
4559
4729
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4560
4730
  return turn !== null && turn.token === token && turn.held && turn.done;
4561
4731
  }
4562
- async function takeTurn(profile, directory, latest) {
4732
+ async function takeTurn(profile, directory, latest, liveness) {
4563
4733
  const token = randomUUID();
4564
4734
  const slot = latest.slot + 1;
4565
4735
  const path = join(directory, String(slot));
4566
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4736
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4567
4737
  let published = false;
4568
4738
  try {
4569
4739
  published = await publishDirectory(
@@ -4617,7 +4787,7 @@ async function markTurn(path, token, kind) {
4617
4787
  }
4618
4788
  async function createTurnDirectory(directory) {
4619
4789
  const token = randomUUID();
4620
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4790
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4621
4791
  await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
4622
4792
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4623
4793
  `,
@@ -4641,31 +4811,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4641
4811
  await rm(stage, { recursive: true, force: true }).catch(() => void 0);
4642
4812
  }
4643
4813
  }
4644
- async function releaseTurn(profile, turn) {
4814
+ async function releaseTurn(profile, turn, liveness) {
4645
4815
  try {
4646
4816
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
4647
4817
  } catch {
4648
4818
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4649
4819
  return;
4650
4820
  }
4651
- await collectTurns(profile, turn).catch(() => void 0);
4821
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4652
4822
  }
4653
- function isAbandonedTurnEntry(name) {
4823
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4654
4824
  if (name.startsWith(".trash-")) return true;
4655
4825
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4656
- return writer !== void 0 && !isProcessAlive(Number(writer));
4826
+ if (writer === void 0) return false;
4827
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4828
+ return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
4657
4829
  }
4658
- function isAbandonedTurnDirectoryStage(name) {
4830
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4659
4831
  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];
4660
- return writer !== void 0 && !isProcessAlive(Number(writer));
4832
+ if (writer === void 0) return false;
4833
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4834
+ return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
4835
+ }
4836
+ async function isStagedOwnerGone(recordPath, liveness) {
4837
+ const owner = await readJson(recordPath).catch(() => null);
4838
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4661
4839
  }
4662
- async function collectTurns(profile, turn) {
4840
+ async function collectTurns(profile, turn, liveness) {
4663
4841
  for (const [directory, abandoned] of [
4664
4842
  [turn.directory, isAbandonedTurnEntry],
4665
4843
  [dirname(turn.directory), isAbandonedTurnDirectoryStage]
4666
4844
  ]) {
4667
- for (const name of (await readdir(directory)).filter(abandoned)) {
4668
- await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4845
+ for (const name of await readdir(directory)) {
4846
+ if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4669
4847
  }
4670
4848
  }
4671
4849
  const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -4683,8 +4861,9 @@ async function collectTurns(profile, turn) {
4683
4861
  await rm(trash, { recursive: true, force: true }).catch(() => void 0);
4684
4862
  }
4685
4863
  }
4864
+ await sweepLiveness(liveness);
4686
4865
  }
4687
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4866
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4688
4867
  try {
4689
4868
  await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
4690
4869
  } catch (error) {
@@ -4697,7 +4876,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4697
4876
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4698
4877
  const ownerPath = join(lockPath, "owner.json");
4699
4878
  const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
4700
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4879
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4701
4880
  `;
4702
4881
  try {
4703
4882
  await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4741,7 +4920,7 @@ async function signalTestLockContention(profile) {
4741
4920
  await writeFile(signalPath, `${profile}
4742
4921
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4743
4922
  }
4744
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4923
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4745
4924
  let aged;
4746
4925
  try {
4747
4926
  const { mtimeMs } = await stat(lockPath);
@@ -4758,7 +4937,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4758
4937
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4759
4938
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4760
4939
  const claimPath = join(lockPath, name);
4761
- if (await hasLiveRecoveryOwner(claimPath)) {
4940
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4762
4941
  if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
4763
4942
  return false;
4764
4943
  }
@@ -4774,7 +4953,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4774
4953
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4775
4954
  return true;
4776
4955
  }
4777
- async function hasLiveRecoveryOwner(claimPath) {
4956
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4778
4957
  let contents;
4779
4958
  try {
4780
4959
  contents = await readFile(claimPath, "utf8");
@@ -4788,8 +4967,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4788
4967
  } catch {
4789
4968
  return false;
4790
4969
  }
4791
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4792
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4970
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4971
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4793
4972
  }
4794
4973
  function recoveryFenced(since, staleAfterMs) {
4795
4974
  return performance.now() - since >= staleAfterMs / 2;
@@ -4803,11 +4982,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4803
4982
  if (aged) await rm(path, { force: true }).catch(() => void 0);
4804
4983
  }
4805
4984
  }
4806
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4985
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4807
4986
  const ownerPath = join(lockPath, "owner.json");
4808
4987
  const observed = await readFile(ownerPath, "utf8").catch(() => null);
4809
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4810
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4988
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4989
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4811
4990
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4812
4991
  const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
4813
4992
  let moved = false;
@@ -4843,7 +5022,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4843
5022
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4844
5023
  return true;
4845
5024
  }
4846
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
5025
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4847
5026
  let owner;
4848
5027
  try {
4849
5028
  owner = JSON.parse(observed);
@@ -4851,7 +5030,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4851
5030
  owner = null;
4852
5031
  }
4853
5032
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4854
- return isStaleOwner(owner, staleAfterMs);
5033
+ return isStaleOwner(owner, liveness, staleAfterMs);
4855
5034
  }
4856
5035
  const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4857
5036
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4873,14 +5052,14 @@ async function waitForTestBarrier(environmentName, profile) {
4873
5052
  }
4874
5053
  }
4875
5054
  }
4876
- function isStaleOwner(owner, staleAfterMs) {
5055
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4877
5056
  const now = Date.now();
4878
5057
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4879
5058
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4880
5059
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4881
5060
  return false;
4882
5061
  }
4883
- return !isProcessAlive(owner.pid);
5062
+ return !await isRecordedProcessAlive(owner, liveness);
4884
5063
  }
4885
5064
  async function exists(path) {
4886
5065
  try {
@@ -4891,13 +5070,22 @@ async function exists(path) {
4891
5070
  throw error;
4892
5071
  }
4893
5072
  }
4894
- function isProcessAlive(pid) {
5073
+ async function isRecordedProcessAlive(owner, acquisition) {
5074
+ const pid = owner.pid;
5075
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4895
5076
  try {
4896
5077
  process.kill(pid, 0);
4897
- return true;
4898
5078
  } catch (error) {
4899
5079
  return !isErrno(error, "ESRCH");
4900
5080
  }
5081
+ if (pid !== process.pid || typeof owner.instanceId !== "string" || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(owner.instanceId) || owner.instanceId === processInstanceId) return true;
5082
+ const origin = processOrigin();
5083
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
5084
+ if (owner.pidNamespace === origin.pidNamespace) {
5085
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
5086
+ }
5087
+ const liveness = parseLiveness(owner.liveness);
5088
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4901
5089
  }
4902
5090
  function validateProfileName(profile) {
4903
5091
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
@@ -4911,7 +5099,7 @@ function isErrno(error, code) {
4911
5099
  function sleep(milliseconds) {
4912
5100
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
4913
5101
  }
4914
- 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, _a2, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
5102
+ 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, TEST_LOCK_LIVENESS_BARRIER_DIR, invocationStateRoot, heldProfileLocks, _a2, processInstanceId, cachedProcessOrigin, LIVENESS_SOCKET, LIVENESS_PROBE_TIMEOUT_MS, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a3, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
4915
5103
  var init_state = __esm({
4916
5104
  "src/state.ts"() {
4917
5105
  "use strict";
@@ -4933,8 +5121,12 @@ var init_state = __esm({
4933
5121
  TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
4934
5122
  TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
4935
5123
  TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
5124
+ TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
4936
5125
  invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
4937
5126
  heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
5127
+ processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
5128
+ LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
5129
+ LIVENESS_PROBE_TIMEOUT_MS = 1e3;
4938
5130
  ProfileLockTimeoutError = class extends Error {
4939
5131
  /** `detail` names the cause when it is not simply another process holding the lock. */
4940
5132
  constructor(profile, timeoutMs, detail) {
@@ -4951,7 +5143,7 @@ var init_state = __esm({
4951
5143
  }
4952
5144
  };
4953
5145
  TURN = /^(?:0|[1-9][0-9]*)$/;
4954
- relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
5146
+ relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
4955
5147
  MARKER_RETRY_DELAYS_MS = [10, 50, 250];
4956
5148
  SETTLE_RETRY_MS = 1e3;
4957
5149
  SETTLE_RETRIES = 300;
@@ -5243,6 +5435,45 @@ var init_artifacts = __esm({
5243
5435
  }
5244
5436
  });
5245
5437
 
5438
+ // src/recovery.ts
5439
+ function signInRecovery(profile, posture) {
5440
+ if (!SAFE_PROFILE_TOKEN.test(profile)) return { action: "sign_in" };
5441
+ const template = posture === "local-owner-key" ? SIGN_IN_TEMPLATES.local : posture === "delegate-session" ? SIGN_IN_TEMPLATES.scoped : SIGN_IN_TEMPLATES.openkey;
5442
+ return { action: "sign_in", command: template(profile) };
5443
+ }
5444
+ function migrationRecovery(profile) {
5445
+ return SAFE_PROFILE_TOKEN.test(profile) ? { action: "complete_migration", command: migrationCommand(profile), operationId: MIGRATION_OPERATION_ID } : { action: "complete_migration", operationId: MIGRATION_OPERATION_ID };
5446
+ }
5447
+ function isSafeRecovery(value, action) {
5448
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
5449
+ const record = value;
5450
+ if (record.action !== action) return false;
5451
+ const keys = Object.keys(record);
5452
+ const allowed = action === "sign_in" ? ["action", "command"] : ["action", "command", "operationId"];
5453
+ if (!keys.every((key) => allowed.includes(key))) return false;
5454
+ if (action === "complete_migration" && record.operationId !== MIGRATION_OPERATION_ID) return false;
5455
+ if (!("command" in record)) return true;
5456
+ const command = record.command;
5457
+ if (typeof command !== "string") return false;
5458
+ const profile = /^tc --profile (\S+) /.exec(command)?.[1];
5459
+ if (profile === void 0 || !SAFE_PROFILE_TOKEN.test(profile)) return false;
5460
+ return action === "sign_in" ? Object.values(SIGN_IN_TEMPLATES).some((template) => template(profile) === command) : migrationCommand(profile) === command;
5461
+ }
5462
+ var SAFE_PROFILE_TOKEN, MIGRATION_OPERATION_ID, SIGN_IN_TEMPLATES, migrationCommand;
5463
+ var init_recovery = __esm({
5464
+ "src/recovery.ts"() {
5465
+ "use strict";
5466
+ SAFE_PROFILE_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
5467
+ MIGRATION_OPERATION_ID = "tinycloud.auth.capabilities";
5468
+ SIGN_IN_TEMPLATES = {
5469
+ local: (profile) => `tc --profile ${profile} auth login --method local`,
5470
+ openkey: (profile) => `tc --profile ${profile} auth login --method openkey`,
5471
+ scoped: (profile) => `tc --profile ${profile} auth login --method openkey --paste --manifest <manifest.json>`
5472
+ };
5473
+ migrationCommand = (profile) => `tc --profile ${profile} auth caps`;
5474
+ }
5475
+ });
5476
+
5246
5477
  // src/secrets.ts
5247
5478
  import * as nodeSdk from "@tinycloud/node-sdk";
5248
5479
  function normalizeSecretsGetInput(input) {
@@ -5330,15 +5561,6 @@ function canonicalizeSpaceUri(space) {
5330
5561
  }
5331
5562
  return space;
5332
5563
  }
5333
- function buildSecretSetupUrl(reference) {
5334
- const parameters = [
5335
- ["secret", reference.name],
5336
- ...reference.scope === void 0 ? [] : [["scope", reference.scope]],
5337
- ["space", reference.space]
5338
- ];
5339
- const query = parameters.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
5340
- return `${SECRET_MANAGER_BASE_URL}/app?${query}`;
5341
- }
5342
5564
  function canonicalizeSecretScope(scope) {
5343
5565
  if (scope === void 0 || scope === "") return void 0;
5344
5566
  const trimmed = scope.trim();
@@ -5366,13 +5588,12 @@ function invalidSecretInput() {
5366
5588
  operationError("INPUT_INVALID", "The secret reference is invalid.")
5367
5589
  );
5368
5590
  }
5369
- var DEFAULT_SECRETS_SPACE, SECRET_MANAGER_BASE_URL, SECRET_NAME_RE2, RESERVED_SECRET_SCOPES;
5591
+ var DEFAULT_SECRETS_SPACE, SECRET_NAME_RE2, RESERVED_SECRET_SCOPES;
5370
5592
  var init_secrets = __esm({
5371
5593
  "src/secrets.ts"() {
5372
5594
  "use strict";
5373
5595
  init_errors2();
5374
5596
  DEFAULT_SECRETS_SPACE = "secrets";
5375
- SECRET_MANAGER_BASE_URL = "https://secrets.tinycloud.xyz";
5376
5597
  SECRET_NAME_RE2 = /^[A-Z][A-Z0-9_]*$/;
5377
5598
  RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
5378
5599
  }
@@ -5529,7 +5750,10 @@ async function importRequestBoundDelegation(context, input) {
5529
5750
  delegation,
5530
5751
  { host }
5531
5752
  );
5532
- } catch {
5753
+ } catch (error) {
5754
+ if (typeof error === "object" && error !== null && "reason" in error && error.reason === "expired") {
5755
+ return expiredDelegation();
5756
+ }
5533
5757
  return operationFailure(
5534
5758
  "DELEGATION_REJECTED",
5535
5759
  "The delegation was rejected by the TinyCloud runtime."
@@ -5547,55 +5771,67 @@ async function importRequestBoundDelegation(context, input) {
5547
5771
  );
5548
5772
  }
5549
5773
  try {
5550
- return await updateProfileStore(
5551
- context.summary.profile,
5552
- "additional-delegations",
5553
- async (records) => {
5554
- const state = await importState(context, input.requestId);
5555
- if (!state.ok) return { records, result: state.result };
5556
- if (state.sessionDid !== sessionDid || normalizeHost(state.host) !== normalizeHost(host)) {
5557
- return {
5558
- records,
5559
- result: audienceMismatch(state.sessionDid, sessionDid)
5774
+ return await withProfileLock(context.summary.profile, async () => {
5775
+ const previousRecords = await readAdditionalDelegations(context.summary.profile);
5776
+ const result = await updateProfileStore(
5777
+ context.summary.profile,
5778
+ "additional-delegations",
5779
+ async (records) => {
5780
+ const state = await importState(context, input.requestId);
5781
+ if (!state.ok) return { records, result: state.result };
5782
+ if (state.sessionDid !== sessionDid || normalizeHost(state.host) !== normalizeHost(host)) {
5783
+ return {
5784
+ records,
5785
+ result: audienceMismatch(state.sessionDid, sessionDid)
5786
+ };
5787
+ }
5788
+ if (state.request.profile !== context.summary.profile || !samePrincipal(state.request.sessionDid, sessionDid) || normalizeHost(state.request.host) !== normalizeHost(host) || JSON.stringify(canonicalizeOperationCapabilities(state.request.requested, resolveSpace2)) !== JSON.stringify(canonicalizeOperationCapabilities(request.requested, resolveSpace2))) {
5789
+ return {
5790
+ records,
5791
+ result: operationFailure(
5792
+ "DELEGATION_ARTIFACT_INVALID",
5793
+ "The delegation does not reference a current stored request."
5794
+ )
5795
+ };
5796
+ }
5797
+ const record = {
5798
+ delegation: activated.delegation,
5799
+ permissions: activated.effectivePermissions,
5800
+ authorityRequest: {
5801
+ requestId: request.requestId,
5802
+ requested: request.requested
5803
+ }
5560
5804
  };
5561
- }
5562
- if (state.request.profile !== context.summary.profile || !samePrincipal(state.request.sessionDid, sessionDid) || normalizeHost(state.request.host) !== normalizeHost(host) || JSON.stringify(canonicalizeOperationCapabilities(state.request.requested, resolveSpace2)) !== JSON.stringify(canonicalizeOperationCapabilities(request.requested, resolveSpace2))) {
5805
+ const position = records.findIndex((entry) => delegationCid(entry) === activated.cid);
5806
+ const alreadyPresent = position !== -1;
5807
+ const others = records.filter((entry) => delegationCid(entry) !== activated.cid);
5563
5808
  return {
5564
- records,
5565
- result: operationFailure(
5566
- "DELEGATION_ARTIFACT_INVALID",
5567
- "The delegation does not reference a current stored request."
5568
- )
5809
+ records: alreadyPresent ? [...others.slice(0, position), record, ...others.slice(position)] : [...records, record],
5810
+ result: {
5811
+ status: "ok",
5812
+ output: {
5813
+ cid: activated.cid,
5814
+ effectivePermissions,
5815
+ expiry: activated.expiry.toISOString(),
5816
+ audience: activated.audience,
5817
+ host: activated.host,
5818
+ activated: true,
5819
+ alreadyPresent
5820
+ }
5821
+ }
5569
5822
  };
5570
5823
  }
5571
- const record = {
5572
- delegation: activated.delegation,
5573
- permissions: activated.effectivePermissions,
5574
- authorityRequest: {
5575
- requestId: request.requestId,
5576
- requested: request.requested
5577
- }
5578
- };
5579
- const position = records.findIndex((entry) => delegationCid(entry) === activated.cid);
5580
- const alreadyPresent = position !== -1;
5581
- const others = records.filter((entry) => delegationCid(entry) !== activated.cid);
5582
- return {
5583
- records: alreadyPresent ? [...others.slice(0, position), record, ...others.slice(position)] : [...records, record],
5584
- result: {
5585
- status: "ok",
5586
- output: {
5587
- cid: activated.cid,
5588
- effectivePermissions,
5589
- expiry: activated.expiry.toISOString(),
5590
- audience: activated.audience,
5591
- host: activated.host,
5592
- activated: true,
5593
- alreadyPresent
5594
- }
5595
- }
5596
- };
5824
+ );
5825
+ if (activated.expiry.getTime() <= Date.now()) {
5826
+ await updateProfileStore(
5827
+ context.summary.profile,
5828
+ "additional-delegations",
5829
+ () => ({ records: previousRecords, result: void 0 })
5830
+ );
5831
+ return expiredDelegation();
5597
5832
  }
5598
- );
5833
+ return result;
5834
+ });
5599
5835
  } catch {
5600
5836
  return internalImportFailure();
5601
5837
  }
@@ -5842,8 +6078,33 @@ var init_auth = __esm({
5842
6078
  }
5843
6079
  });
5844
6080
 
5845
- // src/operations/exploration.ts
6081
+ // src/kv-failure.ts
5846
6082
  import { authorizationVerdictOf, validatedCapabilityOf } from "@tinycloud/sdk-services";
6083
+ function kvRefusal(error) {
6084
+ const verdict = authorizationVerdictOf(error);
6085
+ if (verdict === "unauthenticated") {
6086
+ return validatedCapabilityOf(error) === void 0 ? "auth_required" : "permission_denied";
6087
+ }
6088
+ return verdict === "forbidden" ? "permission_denied" : void 0;
6089
+ }
6090
+ function authorizationFailure(error, action) {
6091
+ switch (kvRefusal(error)) {
6092
+ case "auth_required":
6093
+ return { status: "error", error: operationError("AUTH_REQUIRED", `Authentication is required to ${action}.`) };
6094
+ case "permission_denied":
6095
+ return { status: "error", error: operationError("PERMISSION_DENIED", `Permission is denied to ${action}.`) };
6096
+ case void 0:
6097
+ return void 0;
6098
+ }
6099
+ }
6100
+ var init_kv_failure = __esm({
6101
+ "src/kv-failure.ts"() {
6102
+ "use strict";
6103
+ init_errors2();
6104
+ }
6105
+ });
6106
+
6107
+ // src/operations/exploration.ts
5847
6108
  import { z as z4 } from "zod";
5848
6109
  async function planAccountRegistryRead(context, path) {
5849
6110
  return [{
@@ -6211,19 +6472,6 @@ function caughtKvFailure(error, action) {
6211
6472
  if (error instanceof OperationInvocationError) return { status: "error", error: error.operationError };
6212
6473
  return authorizationFailure(error, action) ?? nodeFailure(action);
6213
6474
  }
6214
- function authorizationFailure(error, action) {
6215
- const verdict = authorizationVerdictOf(error);
6216
- if (verdict === "unauthenticated") {
6217
- if (validatedCapabilityOf(error) !== void 0) {
6218
- return { status: "error", error: operationError("PERMISSION_DENIED", `Permission is denied to ${action}.`) };
6219
- }
6220
- return { status: "error", error: operationError("AUTH_REQUIRED", `Authentication is required to ${action}.`) };
6221
- }
6222
- if (verdict === "forbidden") {
6223
- return { status: "error", error: operationError("PERMISSION_DENIED", `Permission is denied to ${action}.`) };
6224
- }
6225
- return void 0;
6226
- }
6227
6475
  function accountRegistryOutput(output) {
6228
6476
  if (Buffer.byteLength(JSON.stringify(output), "utf8") > MAX_ACCOUNT_REGISTRY_BYTES) {
6229
6477
  return {
@@ -6276,6 +6524,7 @@ var MAX_KV_VALUE_BYTES, DEFAULT_KV_LIST_LIMIT, MAX_ACCOUNT_REGISTRY_RECORDS, MAX
6276
6524
  var init_exploration = __esm({
6277
6525
  "src/operations/exploration.ts"() {
6278
6526
  "use strict";
6527
+ init_kv_failure();
6279
6528
  init_errors2();
6280
6529
  init_secrets();
6281
6530
  MAX_KV_VALUE_BYTES = 1024 * 1024;
@@ -6576,6 +6825,191 @@ var init_exploration = __esm({
6576
6825
  }
6577
6826
  });
6578
6827
 
6828
+ // src/secret-request.ts
6829
+ import * as nodeSdk3 from "@tinycloud/node-sdk";
6830
+ function canonicalSecretRequestOwner(did) {
6831
+ const match = typeof did === "string" ? OWNER_RE.exec(did) : null;
6832
+ if (match === null || !Number.isSafeInteger(Number(match[1]))) throw invalidOwner();
6833
+ const address = match[2];
6834
+ let checksummed;
6835
+ try {
6836
+ checksummed = nodeSdk3.canonicalizeAddress(address);
6837
+ } catch {
6838
+ throw invalidOwner();
6839
+ }
6840
+ if (/[A-F]/.test(address.slice(2)) && address !== checksummed) throw invalidOwner();
6841
+ return `did:pkh:eip155:${match[1]}:${checksummed}`;
6842
+ }
6843
+ function buildSecretRequestUrl(input) {
6844
+ const owner = canonicalSecretRequestOwner(input.owner);
6845
+ const names = validSecretNames(input.names);
6846
+ if (input.scope !== void 0) assertSecretRequestScope(input.scope);
6847
+ const requester = input.requester;
6848
+ if (requester !== void 0 && (typeof requester !== "string" || requester.length === 0 || requester.length > MAX_REQUESTER_LENGTH || CONTROL_OR_FORMAT.test(requester))) {
6849
+ throw new SecretRequestInputError("INVALID_REQUESTER", "The requester label must be 1-64 characters with no control or format characters.");
6850
+ }
6851
+ if (input.returnTo !== void 0 && !isAllowedReturnTo(input.returnTo)) {
6852
+ throw new SecretRequestInputError("INVALID_RETURN_TO", "The return address is not an allowed Secret Manager callback.");
6853
+ }
6854
+ if (input.state !== void 0 && !STATE_RE.test(input.state)) {
6855
+ throw new SecretRequestInputError("INVALID_STATE", "The state must be 1-128 characters of A-Z, a-z, 0-9, '.', '_', '~' or '-'.");
6856
+ }
6857
+ const parameters = [
6858
+ ["owner", owner],
6859
+ ...names.map((name) => ["name", name]),
6860
+ ...input.scope === void 0 ? [] : [["scope", input.scope]],
6861
+ ...input.requester === void 0 ? [] : [["requester", input.requester]],
6862
+ ...input.returnTo === void 0 ? [] : [["return_to", input.returnTo]],
6863
+ ...input.state === void 0 ? [] : [["state", input.state]],
6864
+ ["v", "1"]
6865
+ ];
6866
+ const url = `${SECRET_MANAGER_ORIGIN}/request?${parameters.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&")}`;
6867
+ if (url.length > SECRET_REQUEST_MAX_URL_LENGTH) {
6868
+ throw new SecretRequestInputError("REQUEST_LINK_TOO_LONG", `The request link is longer than ${SECRET_REQUEST_MAX_URL_LENGTH} characters.`);
6869
+ }
6870
+ return { url, owner, names, ...input.scope === void 0 ? {} : { scope: input.scope } };
6871
+ }
6872
+ function secretSetupFor(link2) {
6873
+ return {
6874
+ url: link2.url,
6875
+ names: [...link2.names],
6876
+ ...link2.scope === void 0 ? {} : { scope: link2.scope },
6877
+ owner: link2.owner,
6878
+ action: "add_secret"
6879
+ };
6880
+ }
6881
+ function resolveSecretsOwner(profile, requestedOwner) {
6882
+ const resolved = profile.local ? ownerOrUndefined(profile.principalDid) : ownerOrUndefined(profile.ownerDid) ?? (profile.posture === "delegate-session" ? ownerOfSpace(profile.space) : void 0) ?? ownerOrUndefined(profile.principalDid);
6883
+ const requested = requestedOwner === void 0 ? void 0 : ownerOrUndefined(requestedOwner);
6884
+ if (resolved !== void 0) {
6885
+ if (requestedOwner !== void 0 && requested !== resolved) {
6886
+ return {
6887
+ ok: false,
6888
+ code: "OWNER_MISMATCH",
6889
+ message: "The requested owner is not the owner of this identity's secrets."
6890
+ };
6891
+ }
6892
+ return { ok: true, owner: resolved, local: profile.local };
6893
+ }
6894
+ if (requested !== void 0 && !profile.local) return { ok: true, owner: requested, local: false };
6895
+ return {
6896
+ ok: false,
6897
+ code: "OWNER_DID_UNKNOWN",
6898
+ message: "The owner of this identity's secrets is unknown. Pass the owner's did:pkh with --owner."
6899
+ };
6900
+ }
6901
+ function missingSecretRecovery(input) {
6902
+ if (input.local) {
6903
+ return { kind: "put_locally", hint: `Store it with: ${putCommands(input.names, input.scope)}` };
6904
+ }
6905
+ if (input.owner !== void 0) {
6906
+ try {
6907
+ return {
6908
+ kind: "add_secret",
6909
+ setup: secretSetupFor(buildSecretRequestUrl({
6910
+ owner: input.owner,
6911
+ names: input.names,
6912
+ ...input.scope === void 0 ? {} : { scope: input.scope }
6913
+ }))
6914
+ };
6915
+ } catch {
6916
+ }
6917
+ }
6918
+ return {
6919
+ kind: "not_requestable",
6920
+ hint: `No Secret Manager request link can express this secret. The owner can store it with: ${putCommands(input.names, input.scope)}`
6921
+ };
6922
+ }
6923
+ function isValidSecretRequestScope(scope) {
6924
+ return scope.length <= MAX_SCOPE_LENGTH && SCOPE_RE.test(scope) && !RESERVED_SCOPES.includes(scope);
6925
+ }
6926
+ function isValidSecretName(name) {
6927
+ return name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
6928
+ }
6929
+ function validSecretNames(names) {
6930
+ for (const name of names) assertSecretName(name);
6931
+ const unique = dedupe(names);
6932
+ if (unique.length === 0) {
6933
+ throw new SecretRequestInputError("INVALID_SECRET_NAME", "At least one secret name is required.");
6934
+ }
6935
+ if (unique.length > SECRET_REQUEST_MAX_NAMES) {
6936
+ throw new SecretRequestInputError("TOO_MANY_SECRET_NAMES", `At most ${SECRET_REQUEST_MAX_NAMES} secret names can be requested at once.`);
6937
+ }
6938
+ return unique;
6939
+ }
6940
+ function assertSecretName(name) {
6941
+ if (typeof name !== "string" || !isValidSecretName(name)) {
6942
+ throw new SecretRequestInputError("INVALID_SECRET_NAME", "Secret names are 1-64 characters of A-Z, 0-9 and '_', starting with a letter.");
6943
+ }
6944
+ }
6945
+ function assertSecretRequestScope(scope) {
6946
+ if (typeof scope !== "string" || !isValidSecretRequestScope(scope)) {
6947
+ throw new SecretRequestInputError("INVALID_SECRET_SCOPE", "The scope must be a canonical scope of at most 64 characters, not default, global or scoped.");
6948
+ }
6949
+ }
6950
+ function isAllowedReturnTo(returnTo) {
6951
+ if (typeof returnTo !== "string" || returnTo.length > MAX_RETURN_TO_LENGTH) return false;
6952
+ let url;
6953
+ try {
6954
+ url = new URL(returnTo);
6955
+ } catch {
6956
+ return false;
6957
+ }
6958
+ if (url.username !== "" || url.password !== "") return false;
6959
+ return RETURN_TO_ORIGINS.includes(url.origin) || RETURN_TO_PREVIEW.test(url.origin);
6960
+ }
6961
+ function ownerOrUndefined(did) {
6962
+ if (did === void 0) return void 0;
6963
+ try {
6964
+ return canonicalSecretRequestOwner(did);
6965
+ } catch {
6966
+ return void 0;
6967
+ }
6968
+ }
6969
+ function ownerOfSpace(space) {
6970
+ const match = space === void 0 ? null : PKH_SPACE_RE.exec(space);
6971
+ if (match === null) return void 0;
6972
+ return ownerOrUndefined(`did:pkh:eip155:${match[1]}:${match[2].toLowerCase()}`);
6973
+ }
6974
+ function putCommands(names, scope) {
6975
+ return dedupe(names).map((name) => `tc secrets put ${name}${scope === void 0 ? "" : ` --scope ${scope}`} --stdin`).join("; ");
6976
+ }
6977
+ function dedupe(names) {
6978
+ return [...new Set(names)];
6979
+ }
6980
+ function invalidOwner() {
6981
+ return new SecretRequestInputError("INVALID_OWNER", "The owner must be a did:pkh:eip155 account with a valid EIP-55 address.");
6982
+ }
6983
+ var SECRET_MANAGER_ORIGIN, SECRET_REQUEST_MAX_NAMES, SECRET_REQUEST_MAX_URL_LENGTH, OWNER_RE, NAME_RE, MAX_NAME_LENGTH, SCOPE_RE, MAX_SCOPE_LENGTH, RESERVED_SCOPES, MAX_REQUESTER_LENGTH, CONTROL_OR_FORMAT, STATE_RE, MAX_RETURN_TO_LENGTH, RETURN_TO_ORIGINS, RETURN_TO_PREVIEW, PKH_SPACE_RE, SecretRequestInputError;
6984
+ var init_secret_request = __esm({
6985
+ "src/secret-request.ts"() {
6986
+ "use strict";
6987
+ SECRET_MANAGER_ORIGIN = "https://secrets.tinycloud.xyz";
6988
+ SECRET_REQUEST_MAX_NAMES = 10;
6989
+ SECRET_REQUEST_MAX_URL_LENGTH = 8192;
6990
+ OWNER_RE = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40})$/;
6991
+ NAME_RE = /^[A-Z][A-Z0-9_]*$/;
6992
+ MAX_NAME_LENGTH = 64;
6993
+ SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
6994
+ MAX_SCOPE_LENGTH = 64;
6995
+ RESERVED_SCOPES = ["default", "global", "scoped"];
6996
+ MAX_REQUESTER_LENGTH = 64;
6997
+ CONTROL_OR_FORMAT = /[\p{Cc}\p{Cf}]/u;
6998
+ STATE_RE = /^[A-Za-z0-9._~-]{1,128}$/;
6999
+ MAX_RETURN_TO_LENGTH = 2048;
7000
+ RETURN_TO_ORIGINS = ["https://feed.tinycloud.xyz"];
7001
+ RETURN_TO_PREVIEW = /^https:\/\/[a-z0-9-]+\.tinyfeed\.pages\.dev$/;
7002
+ PKH_SPACE_RE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-fA-F]{40}):[^:]+$/;
7003
+ SecretRequestInputError = class extends Error {
7004
+ constructor(code, message) {
7005
+ super(message);
7006
+ this.name = "SecretRequestInputError";
7007
+ this.code = code;
7008
+ }
7009
+ };
7010
+ }
7011
+ });
7012
+
6579
7013
  // src/operations/secrets-get.ts
6580
7014
  import { z as z5 } from "zod";
6581
7015
  async function planSecretGet(context, input) {
@@ -6643,27 +7077,14 @@ async function executeSecretGet(context, input) {
6643
7077
  )
6644
7078
  };
6645
7079
  }
6646
- return classifySecretRead(reference, result);
7080
+ return classifySecretRead(reference, result, context.runtime?.localOwner === true);
6647
7081
  }
6648
- function classifySecretRead(reference, result) {
7082
+ function classifySecretRead(reference, result, localOwner) {
6649
7083
  switch (result.status) {
6650
7084
  case "ok":
6651
7085
  return { status: "ok", output: { value: result.value } };
6652
7086
  case "not_found":
6653
- return {
6654
- status: "setup_required",
6655
- setup: {
6656
- kind: "secret_manager",
6657
- secret: {
6658
- name: reference.name,
6659
- ...reference.scope === void 0 ? {} : { scope: reference.scope },
6660
- space: reference.space
6661
- },
6662
- url: buildSecretSetupUrl(reference),
6663
- message: "Enter this secret in Secret Manager, then retry the operation."
6664
- },
6665
- requiresCallerInput: true
6666
- };
7087
+ return missingSecretOutcome(reference, localOwner);
6667
7088
  case "permission_required":
6668
7089
  return {
6669
7090
  status: "authority_required",
@@ -6708,6 +7129,47 @@ function classifySecretRead(reference, result) {
6708
7129
  error: operationError("SECRET_READ_FAILED", "The secret read failed.")
6709
7130
  };
6710
7131
  }
7132
+ function missingSecretOutcome(reference, localOwner) {
7133
+ const owner = ownerOfSpace(reference.space);
7134
+ const recovery = missingSecretRecovery({
7135
+ owner,
7136
+ local: localOwner,
7137
+ names: [reference.name],
7138
+ ...reference.scope === void 0 ? {} : { scope: reference.scope }
7139
+ });
7140
+ if (recovery.kind === "add_secret") {
7141
+ const { setup } = recovery;
7142
+ return {
7143
+ status: "setup_required",
7144
+ setup: {
7145
+ kind: "secret_manager",
7146
+ action: setup.action,
7147
+ url: setup.url,
7148
+ names: setup.names,
7149
+ ...setup.scope === void 0 ? {} : { scope: setup.scope },
7150
+ owner: setup.owner,
7151
+ secret: {
7152
+ name: reference.name,
7153
+ ...reference.scope === void 0 ? {} : { scope: reference.scope },
7154
+ space: reference.space
7155
+ },
7156
+ message: "Ask the owner to add this secret in Secret Manager, then retry the operation."
7157
+ },
7158
+ requiresCallerInput: true
7159
+ };
7160
+ }
7161
+ return {
7162
+ status: "error",
7163
+ error: operationError("KV_NOT_FOUND", recovery.hint, {
7164
+ details: {
7165
+ names: [reference.name],
7166
+ ...reference.scope === void 0 ? {} : { scope: reference.scope },
7167
+ ...owner === void 0 ? {} : { owner },
7168
+ recoveryKind: recovery.kind
7169
+ }
7170
+ })
7171
+ };
7172
+ }
6711
7173
  function resolveEncryptionNetworkId(node, space) {
6712
7174
  const candidate = node;
6713
7175
  let scoped;
@@ -6734,6 +7196,7 @@ var init_secrets_get = __esm({
6734
7196
  "src/operations/secrets-get.ts"() {
6735
7197
  "use strict";
6736
7198
  init_errors2();
7199
+ init_secret_request();
6737
7200
  init_secrets();
6738
7201
  SecretsGetInputSchema = z5.object({
6739
7202
  name: z5.string(),
@@ -6785,9 +7248,197 @@ var init_secrets_get = __esm({
6785
7248
  }
6786
7249
  });
6787
7250
 
7251
+ // src/operations/secrets-status.ts
7252
+ import { z as z6 } from "zod";
7253
+ import * as nodeSdk4 from "@tinycloud/node-sdk";
7254
+ async function executeSecretsStatus(context, input) {
7255
+ const runtime = context.runtime;
7256
+ const node = runtime?.node;
7257
+ if (runtime === void 0 || typeof node?.kvForSpace !== "function") {
7258
+ return {
7259
+ status: "error",
7260
+ error: operationError("NODE_ERROR", "The TinyCloud node could not check the secrets.", { retryable: true })
7261
+ };
7262
+ }
7263
+ const local = runtime.localOwner === true;
7264
+ const resolution = resolveSecretsOwner({
7265
+ local,
7266
+ posture: context.summary.posture,
7267
+ ...context.summary.ownerDid === void 0 ? {} : { ownerDid: context.summary.ownerDid },
7268
+ ...context.summary.principalDid === void 0 ? {} : { principalDid: context.summary.principalDid },
7269
+ ...context.summary.space === void 0 ? {} : { space: context.summary.space }
7270
+ }, input.owner);
7271
+ if (!resolution.ok) {
7272
+ return {
7273
+ status: "error",
7274
+ error: operationError("INPUT_INVALID", resolution.message, { details: { reason: resolution.code } })
7275
+ };
7276
+ }
7277
+ const owner = resolution.owner;
7278
+ const skipped2 = runtime.skippedGrantReasons ?? [];
7279
+ const pkh = nodeSdk4.parsePkhDid(owner);
7280
+ const kv = node.kvForSpace(nodeSdk4.makePkhSpaceId(pkh.address, pkh.chainId, "secrets"));
7281
+ const outcomes = await Promise.all(input.names.map(async (name) => {
7282
+ const path = input.scope === void 0 ? `vault/secrets/${name}` : `vault/secrets/scoped/${input.scope}/${name}`;
7283
+ try {
7284
+ const result = await kv.get(path, { raw: true, prefix: "", timeout: PROBE_TIMEOUT_MS });
7285
+ if (result.ok) return "present";
7286
+ const error = result.error;
7287
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "KV_NOT_FOUND") {
7288
+ return "pending";
7289
+ }
7290
+ const refusal = kvRefusal(error);
7291
+ if (refusal === "auth_required") return "auth_required";
7292
+ if (refusal === "permission_denied") return "denied";
7293
+ return "unreachable";
7294
+ } catch {
7295
+ return "unreachable";
7296
+ }
7297
+ }));
7298
+ const namesWith = (outcome) => input.names.filter((_, index) => outcomes[index] === outcome);
7299
+ const present = namesWith("present");
7300
+ const pending = namesWith("pending");
7301
+ const denied = namesWith("denied");
7302
+ if (present.length + pending.length < input.names.length) {
7303
+ if (outcomes.includes("auth_required") || skipped2.includes("activation_unauthenticated")) {
7304
+ return {
7305
+ status: "error",
7306
+ error: operationError(
7307
+ "AUTH_REQUIRED",
7308
+ "The node did not accept this profile's stored session. Sign in again, then retry.",
7309
+ { retryable: false, details: { recovery: signInRecovery(context.summary.profile, context.summary.posture) } }
7310
+ )
7311
+ };
7312
+ }
7313
+ if (outcomes.includes("unreachable") || skipped2.includes("activation_unavailable")) {
7314
+ return {
7315
+ status: "error",
7316
+ error: operationError("NODE_UNREACHABLE", "The TinyCloud node could not be reached.", { retryable: true })
7317
+ };
7318
+ }
7319
+ }
7320
+ const recovery = pending.length === 0 ? void 0 : missingSecretRecovery({
7321
+ owner,
7322
+ local,
7323
+ names: pending,
7324
+ ...input.scope === void 0 ? {} : { scope: input.scope }
7325
+ });
7326
+ return {
7327
+ status: "ok",
7328
+ output: {
7329
+ status: denied.length > 0 ? "consent_required" : pending.length > 0 ? "pending" : "ready",
7330
+ owner,
7331
+ names: [...input.names],
7332
+ ...input.scope === void 0 ? {} : { scope: input.scope },
7333
+ present,
7334
+ pending,
7335
+ denied,
7336
+ ...recovery === void 0 ? {} : recovery.kind === "add_secret" ? { setup: recovery.setup } : { hint: recovery.hint }
7337
+ }
7338
+ };
7339
+ }
7340
+ var PROBE_TIMEOUT_MS, SECRETS_STATUS_DEADLINE_MS, SecretsStatusInputSchema, SecretSetupSchema, SecretsStatusOutputSchema, SECRETS_STATUS_POSTURES, SECRETS_STATUS_EXPOSURE, SECRETS_STATUS_SENSITIVITY, secretsStatusOperationDefinition, secretsStatusOperationDefinitions;
7341
+ var init_secrets_status = __esm({
7342
+ "src/operations/secrets-status.ts"() {
7343
+ "use strict";
7344
+ init_errors2();
7345
+ init_kv_failure();
7346
+ init_recovery();
7347
+ init_secret_request();
7348
+ init_secrets();
7349
+ PROBE_TIMEOUT_MS = 15e3;
7350
+ SECRETS_STATUS_DEADLINE_MS = 18e3;
7351
+ SecretsStatusInputSchema = z6.object({
7352
+ names: z6.array(z6.string()),
7353
+ scope: z6.string().optional(),
7354
+ owner: z6.string().optional()
7355
+ }).strict().superRefine((input, context) => {
7356
+ try {
7357
+ validSecretNames(input.names);
7358
+ } catch {
7359
+ context.addIssue({ code: z6.ZodIssueCode.custom, path: ["names"], message: "Invalid secret names." });
7360
+ }
7361
+ try {
7362
+ const scope = canonicalizeSecretScope(input.scope);
7363
+ if (scope !== void 0 && !isValidSecretRequestScope(scope)) throw new Error("invalid scope");
7364
+ } catch {
7365
+ context.addIssue({ code: z6.ZodIssueCode.custom, path: ["scope"], message: "Invalid secret scope." });
7366
+ }
7367
+ if (input.owner !== void 0) {
7368
+ try {
7369
+ canonicalSecretRequestOwner(input.owner);
7370
+ } catch {
7371
+ context.addIssue({ code: z6.ZodIssueCode.custom, path: ["owner"], message: "Invalid owner." });
7372
+ }
7373
+ }
7374
+ }).transform((input) => {
7375
+ const scope = canonicalizeSecretScope(input.scope);
7376
+ return {
7377
+ names: validSecretNames(input.names),
7378
+ ...scope === void 0 ? {} : { scope },
7379
+ ...input.owner === void 0 ? {} : { owner: canonicalSecretRequestOwner(input.owner) }
7380
+ };
7381
+ });
7382
+ SecretSetupSchema = z6.object({
7383
+ url: z6.string().url(),
7384
+ names: z6.array(z6.string()).min(1),
7385
+ scope: z6.string().optional(),
7386
+ owner: z6.string(),
7387
+ action: z6.literal("add_secret")
7388
+ }).strict();
7389
+ SecretsStatusOutputSchema = z6.object({
7390
+ status: z6.enum(["ready", "pending", "consent_required"]),
7391
+ owner: z6.string(),
7392
+ names: z6.array(z6.string()).min(1),
7393
+ scope: z6.string().optional(),
7394
+ present: z6.array(z6.string()),
7395
+ pending: z6.array(z6.string()),
7396
+ denied: z6.array(z6.string()),
7397
+ setup: SecretSetupSchema.optional(),
7398
+ hint: z6.string().optional()
7399
+ }).strict();
7400
+ SECRETS_STATUS_POSTURES = [
7401
+ "owner-openkey",
7402
+ "delegate-session",
7403
+ "local-owner-key"
7404
+ ];
7405
+ SECRETS_STATUS_EXPOSURE = {
7406
+ cli: { status: "required" },
7407
+ mcp: { status: "required" },
7408
+ skill: { status: "required" },
7409
+ docs: { status: "required" }
7410
+ };
7411
+ SECRETS_STATUS_SENSITIVITY = {
7412
+ input: [],
7413
+ output: []
7414
+ };
7415
+ secretsStatusOperationDefinition = {
7416
+ id: "tinycloud.secrets.status",
7417
+ version: 1,
7418
+ title: "Check whether TinyCloud secrets exist",
7419
+ description: "Report, without reading any value, which named secrets exist in the owner's secrets space: ready, pending (with a Secret Manager request link), or consent_required for names this identity may not read. Each name needs tinycloud.kv/get; this operation never requests it. It never signs in: it uses only the profile's stored session, and writes no profile state.",
7420
+ input: SecretsStatusInputSchema,
7421
+ output: SecretsStatusOutputSchema,
7422
+ effects: ["read"],
7423
+ runtime: "restored",
7424
+ postures: SECRETS_STATUS_POSTURES,
7425
+ exposure: SECRETS_STATUS_EXPOSURE,
7426
+ sensitivity: SECRETS_STATUS_SENSITIVITY,
7427
+ deadlineMs: SECRETS_STATUS_DEADLINE_MS,
7428
+ // Never evaluated against the session: a missing grant is reported as
7429
+ // `consent_required`, never saved as an authority request.
7430
+ authority: async () => [],
7431
+ execute: executeSecretsStatus
7432
+ };
7433
+ secretsStatusOperationDefinitions = [
7434
+ secretsStatusOperationDefinition
7435
+ ];
7436
+ }
7437
+ });
7438
+
6788
7439
  // src/operations/sql.ts
6789
7440
  import nodeSqlParser from "node-sql-parser";
6790
- import { z as z6 } from "zod";
7441
+ import { z as z7 } from "zod";
6791
7442
  async function planSqlRead(context, input) {
6792
7443
  const space = resolveSqlSpace(context, input.space);
6793
7444
  return [{
@@ -7105,7 +7756,7 @@ var init_sql = __esm({
7105
7756
  "use strict";
7106
7757
  init_errors2();
7107
7758
  init_secrets();
7108
- init_exploration();
7759
+ init_kv_failure();
7109
7760
  DEFAULT_MAX_ROWS = 100;
7110
7761
  MAX_MAX_ROWS = 1e3;
7111
7762
  DEFAULT_MAX_BYTES = 1024 * 1024;
@@ -7121,29 +7772,29 @@ WHERE type IN ('table', 'view', 'index', 'trigger')
7121
7772
  AND name NOT LIKE 'sqlite_%'
7122
7773
  ORDER BY type, name`;
7123
7774
  sqlParser = new nodeSqlParser.Parser();
7124
- SpaceSchema2 = z6.string().min(1).refine(
7775
+ SpaceSchema2 = z7.string().min(1).refine(
7125
7776
  (space) => (/^[A-Za-z0-9_-]+$/.test(space) || space.startsWith("tinycloud:")) && !space.includes("*"),
7126
7777
  "Invalid TinyCloud space."
7127
7778
  );
7128
- DatabaseSchema = z6.string().min(1).max(256).refine(
7779
+ DatabaseSchema = z7.string().min(1).max(256).refine(
7129
7780
  (database) => database !== "/" && !database.endsWith("/") && !database.includes("*") && !/[\u0000-\u001f\u007f]/.test(database),
7130
7781
  "Invalid SQLite database name."
7131
7782
  );
7132
- SafeNumberSchema = z6.number().finite().refine(
7783
+ SafeNumberSchema = z7.number().finite().refine(
7133
7784
  (value) => !Number.isInteger(value) || Number.isSafeInteger(value),
7134
7785
  "SQLite integer parameters must be JavaScript safe integers."
7135
7786
  );
7136
- Base64Schema = z6.string().max(Math.ceil(MAX_MAX_BYTES / 3) * 4).refine(
7787
+ Base64Schema = z7.string().max(Math.ceil(MAX_MAX_BYTES / 3) * 4).refine(
7137
7788
  isCanonicalBase642,
7138
7789
  "Invalid base64 BLOB value."
7139
7790
  );
7140
- SqlInputValueSchema = z6.union([
7141
- z6.null(),
7791
+ SqlInputValueSchema = z7.union([
7792
+ z7.null(),
7142
7793
  SafeNumberSchema,
7143
- z6.string().max(MAX_MAX_BYTES),
7144
- z6.object({ type: z6.literal("blob"), base64: Base64Schema }).strict()
7794
+ z7.string().max(MAX_MAX_BYTES),
7795
+ z7.object({ type: z7.literal("blob"), base64: Base64Schema }).strict()
7145
7796
  ]);
7146
- SqlParamsSchema = z6.array(SqlInputValueSchema).max(MAX_PARAMETERS).superRefine(
7797
+ SqlParamsSchema = z7.array(SqlInputValueSchema).max(MAX_PARAMETERS).superRefine(
7147
7798
  (params, context) => {
7148
7799
  const totalBytes = params.reduce(
7149
7800
  (total, value) => total + sqlInputValueBytes(value),
@@ -7151,13 +7802,13 @@ ORDER BY type, name`;
7151
7802
  );
7152
7803
  if (totalBytes > MAX_MAX_BYTES) {
7153
7804
  context.addIssue({
7154
- code: z6.ZodIssueCode.custom,
7805
+ code: z7.ZodIssueCode.custom,
7155
7806
  message: "SQLite query parameters exceed the 4 MiB input limit."
7156
7807
  });
7157
7808
  }
7158
7809
  }
7159
7810
  );
7160
- RequiredSqlParamsSchema = z6.array(SqlInputValueSchema).min(1).max(MAX_PARAMETERS).superRefine(
7811
+ RequiredSqlParamsSchema = z7.array(SqlInputValueSchema).min(1).max(MAX_PARAMETERS).superRefine(
7161
7812
  (params, context) => {
7162
7813
  const totalBytes = params.reduce(
7163
7814
  (total, value) => total + sqlInputValueBytes(value),
@@ -7165,88 +7816,88 @@ ORDER BY type, name`;
7165
7816
  );
7166
7817
  if (totalBytes > MAX_MAX_BYTES) {
7167
7818
  context.addIssue({
7168
- code: z6.ZodIssueCode.custom,
7819
+ code: z7.ZodIssueCode.custom,
7169
7820
  message: "SQLite query parameters exceed the 4 MiB input limit."
7170
7821
  });
7171
7822
  }
7172
7823
  }
7173
7824
  );
7174
- SqlOutputValueSchema = z6.union([
7175
- z6.null(),
7825
+ SqlOutputValueSchema = z7.union([
7826
+ z7.null(),
7176
7827
  SafeNumberSchema,
7177
- z6.string(),
7178
- z6.object({
7179
- type: z6.literal("blob"),
7180
- base64: z6.string(),
7181
- byteLength: z6.number().int().nonnegative()
7828
+ z7.string(),
7829
+ z7.object({
7830
+ type: z7.literal("blob"),
7831
+ base64: z7.string(),
7832
+ byteLength: z7.number().int().nonnegative()
7182
7833
  }).strict()
7183
7834
  ]);
7184
- SqlTargetInputSchema = z6.object({
7835
+ SqlTargetInputSchema = z7.object({
7185
7836
  space: SpaceSchema2,
7186
7837
  database: DatabaseSchema
7187
7838
  }).strict();
7188
- SqlQueryInputSchema = z6.object({
7839
+ SqlQueryInputSchema = z7.object({
7189
7840
  space: SpaceSchema2,
7190
7841
  database: DatabaseSchema,
7191
- sql: z6.string().min(1).max(MAX_SQL_LENGTH).superRefine((sql, context) => {
7842
+ sql: z7.string().min(1).max(MAX_SQL_LENGTH).superRefine((sql, context) => {
7192
7843
  if (!isSingleReadQuery(sql)) {
7193
7844
  context.addIssue({
7194
- code: z6.ZodIssueCode.custom,
7845
+ code: z7.ZodIssueCode.custom,
7195
7846
  message: "SQL must parse as exactly one SQLite SELECT statement."
7196
7847
  });
7197
7848
  }
7198
7849
  }),
7199
7850
  params: SqlParamsSchema.optional(),
7200
- maxRows: z6.number().int().positive().max(MAX_MAX_ROWS).optional(),
7201
- maxBytes: z6.number().int().positive().max(MAX_MAX_BYTES).optional()
7851
+ maxRows: z7.number().int().positive().max(MAX_MAX_ROWS).optional(),
7852
+ maxBytes: z7.number().int().positive().max(MAX_MAX_BYTES).optional()
7202
7853
  }).strict();
7203
- SqlExecuteInputSchema = z6.object({
7854
+ SqlExecuteInputSchema = z7.object({
7204
7855
  space: SpaceSchema2,
7205
7856
  database: DatabaseSchema,
7206
- sql: z6.string().min(1).max(MAX_SQL_LENGTH),
7857
+ sql: z7.string().min(1).max(MAX_SQL_LENGTH),
7207
7858
  params: RequiredSqlParamsSchema,
7208
- acknowledgeDatabaseWideAuthority: z6.literal(true).describe(SQL_WRITE_AUTHORITY_NOTICE)
7859
+ acknowledgeDatabaseWideAuthority: z7.literal(true).describe(SQL_WRITE_AUTHORITY_NOTICE)
7209
7860
  }).strict().superRefine((input, context) => {
7210
7861
  if (parseSingleDmlStatement(input.sql, input.params.length) === void 0) {
7211
7862
  context.addIssue({
7212
- code: z6.ZodIssueCode.custom,
7863
+ code: z7.ZodIssueCode.custom,
7213
7864
  path: ["sql"],
7214
7865
  message: "SQL must parse as exactly one parameterized INSERT, UPDATE, or DELETE with one value per positional placeholder."
7215
7866
  });
7216
7867
  }
7217
7868
  });
7218
- SqlLimitsOutputSchema = z6.object({
7219
- maxRows: z6.number().int().positive(),
7220
- maxBytes: z6.number().int().positive(),
7221
- enforcement: z6.literal("node-requested-client-verified")
7869
+ SqlLimitsOutputSchema = z7.object({
7870
+ maxRows: z7.number().int().positive(),
7871
+ maxBytes: z7.number().int().positive(),
7872
+ enforcement: z7.literal("node-requested-client-verified")
7222
7873
  }).strict();
7223
- SqlSchemaInspectOutputSchema = z6.object({
7224
- space: z6.string().min(1),
7225
- database: z6.string().min(1),
7226
- objects: z6.array(z6.object({
7227
- type: z6.enum(["table", "view", "index", "trigger"]),
7228
- name: z6.string().min(1),
7229
- tableName: z6.string().min(1),
7230
- sql: z6.string().optional()
7874
+ SqlSchemaInspectOutputSchema = z7.object({
7875
+ space: z7.string().min(1),
7876
+ database: z7.string().min(1),
7877
+ objects: z7.array(z7.object({
7878
+ type: z7.enum(["table", "view", "index", "trigger"]),
7879
+ name: z7.string().min(1),
7880
+ tableName: z7.string().min(1),
7881
+ sql: z7.string().optional()
7231
7882
  }).strict()),
7232
- count: z6.number().int().nonnegative(),
7883
+ count: z7.number().int().nonnegative(),
7233
7884
  limits: SqlLimitsOutputSchema
7234
7885
  }).strict();
7235
- SqlQueryOutputSchema = z6.object({
7236
- space: z6.string().min(1),
7237
- database: z6.string().min(1),
7238
- columns: z6.array(z6.string()),
7239
- rows: z6.array(z6.array(SqlOutputValueSchema)),
7240
- rowCount: z6.number().int().nonnegative(),
7886
+ SqlQueryOutputSchema = z7.object({
7887
+ space: z7.string().min(1),
7888
+ database: z7.string().min(1),
7889
+ columns: z7.array(z7.string()),
7890
+ rows: z7.array(z7.array(SqlOutputValueSchema)),
7891
+ rowCount: z7.number().int().nonnegative(),
7241
7892
  limits: SqlLimitsOutputSchema
7242
7893
  }).strict();
7243
- SqlExecuteOutputSchema = z6.object({
7244
- space: z6.string().min(1),
7245
- database: z6.string().min(1),
7246
- statementType: z6.enum(["insert", "update", "delete"]),
7247
- changes: z6.number().int().nonnegative(),
7248
- lastInsertRowId: z6.number().int().nullable(),
7249
- authorityNotice: z6.literal(SQL_WRITE_AUTHORITY_NOTICE)
7894
+ SqlExecuteOutputSchema = z7.object({
7895
+ space: z7.string().min(1),
7896
+ database: z7.string().min(1),
7897
+ statementType: z7.enum(["insert", "update", "delete"]),
7898
+ changes: z7.number().int().nonnegative(),
7899
+ lastInsertRowId: z7.number().int().nullable(),
7900
+ authorityNotice: z7.literal(SQL_WRITE_AUTHORITY_NOTICE)
7250
7901
  }).strict();
7251
7902
  AUTHENTICATED_POSTURES2 = [
7252
7903
  "owner-openkey",
@@ -7346,6 +7997,8 @@ var init_contract = __esm({
7346
7997
  "resources_mismatch",
7347
7998
  "unsupported",
7348
7999
  "activation_rejected",
8000
+ "activation_unavailable",
8001
+ "activation_unauthenticated",
7349
8002
  "invalid"
7350
8003
  ];
7351
8004
  DELEGATION_CID_PATTERN = "^bafkr4i[a-z2-7]{52}$";
@@ -7354,7 +8007,7 @@ var init_contract = __esm({
7354
8007
 
7355
8008
  // src/delegation-binding.ts
7356
8009
  import { join as join2 } from "path";
7357
- import * as nodeSdk3 from "@tinycloud/node-sdk";
8010
+ import * as nodeSdk5 from "@tinycloud/node-sdk";
7358
8011
  function bindingMigrationPath(profile) {
7359
8012
  return join2(profilePath(profile), "delegation-binding-migration.json");
7360
8013
  }
@@ -7450,6 +8103,9 @@ function skipped(reason) {
7450
8103
  }
7451
8104
  async function replayProfileDelegations(profile, node, options) {
7452
8105
  await prepareStoredDelegationReplay(profile, node, { host: options.host, migrate: options.migrate });
8106
+ return replayStoredDelegationRecords(profile, node, options);
8107
+ }
8108
+ async function replayStoredDelegationRecords(profile, node, options) {
7453
8109
  const installed = [];
7454
8110
  const warnings = [];
7455
8111
  const seen = /* @__PURE__ */ new Set();
@@ -7490,7 +8146,7 @@ function storedDelegationForAnotherKey(entry, node) {
7490
8146
  const audience = verifiedCompactAudience(node, delegation);
7491
8147
  if (audience === void 0) return false;
7492
8148
  try {
7493
- return nodeSdk3.principalDidEquals(delegation.delegateDID, audience) && !nodeSdk3.principalDidEquals(audience, node.sessionDid);
8149
+ return nodeSdk5.principalDidEquals(delegation.delegateDID, audience) && !nodeSdk5.principalDidEquals(audience, node.sessionDid);
7494
8150
  } catch {
7495
8151
  return false;
7496
8152
  }
@@ -7524,7 +8180,7 @@ async function assess(node, entry, options, mode) {
7524
8180
  let outsideRequest = false;
7525
8181
  let effectivePermissions;
7526
8182
  try {
7527
- const installed = await nodeSdk3.activateValidatedRuntimeDelegation(node, toActivate, {
8183
+ const installed = await nodeSdk5.activateValidatedRuntimeDelegation(node, toActivate, {
7528
8184
  host: options.host,
7529
8185
  authorize: (effective) => {
7530
8186
  verifyCompactProof(toActivate);
@@ -7556,7 +8212,7 @@ function verifyCompactProof(delegation) {
7556
8212
  try {
7557
8213
  parseSignedCompactUcanAttenuation(withoutBearer(authorization), delegation.cid);
7558
8214
  } catch {
7559
- throw new nodeSdk3.RuntimeDelegationRejectedError("proof_invalid", "The compact delegation proof is invalid.");
8215
+ throw new nodeSdk5.RuntimeDelegationRejectedError("proof_invalid", "The compact delegation proof is invalid.");
7560
8216
  }
7561
8217
  }
7562
8218
  function rejectionReason(error) {
@@ -7573,7 +8229,7 @@ async function bindingMigrationRecorded(profile) {
7573
8229
  async function signedCapabilities(node, delegation, host) {
7574
8230
  let signed;
7575
8231
  try {
7576
- await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, {
8232
+ await nodeSdk5.activateValidatedRuntimeDelegation(node, delegation, {
7577
8233
  // The transport host is not signed authority; check it at replay instead.
7578
8234
  host: delegation.host ?? host,
7579
8235
  authorize: (effective) => {
@@ -7636,7 +8292,7 @@ var init_delegation_binding = __esm({
7636
8292
  });
7637
8293
 
7638
8294
  // src/operations/status.ts
7639
- import { z as z7 } from "zod";
8295
+ import { z as z8 } from "zod";
7640
8296
  function createStatusDefinition(params) {
7641
8297
  return {
7642
8298
  id: params.id,
@@ -7761,27 +8417,27 @@ var init_status = __esm({
7761
8417
  init_delegation_binding();
7762
8418
  init_errors2();
7763
8419
  init_state();
7764
- StatusInputSchema = z7.object({}).strict();
7765
- StatusOutputSchema = z7.object({
7766
- profile: z7.string().min(1),
7767
- host: z7.string().min(1),
7768
- posture: z7.enum([
8420
+ StatusInputSchema = z8.object({}).strict();
8421
+ StatusOutputSchema = z8.object({
8422
+ profile: z8.string().min(1),
8423
+ host: z8.string().min(1),
8424
+ posture: z8.enum([
7769
8425
  "owner-openkey",
7770
8426
  "delegate-session",
7771
8427
  "local-owner-key",
7772
8428
  "unauthenticated"
7773
8429
  ]),
7774
- operatorType: z7.enum(["human", "agent"]).optional(),
7775
- principalDid: z7.string().min(1).optional(),
7776
- sessionDid: z7.string().min(1).optional(),
7777
- ownerDid: z7.string().min(1).optional(),
7778
- space: z7.string().min(1).optional(),
7779
- session: z7.object({
7780
- present: z7.boolean(),
7781
- expired: z7.boolean().nullable(),
7782
- expiresAt: z7.string().datetime({ offset: true }).nullable()
8430
+ operatorType: z8.enum(["human", "agent"]).optional(),
8431
+ principalDid: z8.string().min(1).optional(),
8432
+ sessionDid: z8.string().min(1).optional(),
8433
+ ownerDid: z8.string().min(1).optional(),
8434
+ space: z8.string().min(1).optional(),
8435
+ session: z8.object({
8436
+ present: z8.boolean(),
8437
+ expired: z8.boolean().nullable(),
8438
+ expiresAt: z8.string().datetime({ offset: true }).nullable()
7783
8439
  }).strict(),
7784
- liveAdditionalDelegationCount: z7.number().int().nonnegative().describe(
8440
+ liveAdditionalDelegationCount: z8.number().int().nonnegative().describe(
7785
8441
  "Stored, unexpired compact-UCAN or signed-login delegations with a valid request binding. Each still requires local proof validation and node activation before it grants authority."
7786
8442
  )
7787
8443
  }).strict();
@@ -7825,7 +8481,8 @@ function registeredOperationDefinitions() {
7825
8481
  ...authOperationDefinitions,
7826
8482
  ...explorationOperationDefinitions,
7827
8483
  ...sqlOperationDefinitions,
7828
- ...secretsGetOperationDefinitions
8484
+ ...secretsGetOperationDefinitions,
8485
+ ...secretsStatusOperationDefinitions
7829
8486
  ]);
7830
8487
  return operationDefinitions;
7831
8488
  }
@@ -7857,6 +8514,7 @@ var init_registry = __esm({
7857
8514
  init_auth();
7858
8515
  init_exploration();
7859
8516
  init_secrets_get();
8517
+ init_secrets_status();
7860
8518
  init_sql();
7861
8519
  init_status();
7862
8520
  }
@@ -7909,6 +8567,9 @@ function resolvePosture(profile) {
7909
8567
  if (profile.authMethod === "local") return "local-owner-key";
7910
8568
  return "owner-openkey";
7911
8569
  }
8570
+ function isLocalOwnerProfile(profile) {
8571
+ return resolvePosture(profile) === "local-owner-key" || profile.authMethod === "local" || typeof profile.privateKey === "string";
8572
+ }
7912
8573
  function profileNotFound2(profile) {
7913
8574
  return {
7914
8575
  ok: false,
@@ -7980,7 +8641,7 @@ var runtime_exports = {};
7980
8641
  __export(runtime_exports, {
7981
8642
  createInvocationRuntime: () => createInvocationRuntime
7982
8643
  });
7983
- import * as nodeSdk4 from "@tinycloud/node-sdk";
8644
+ import * as nodeSdk6 from "@tinycloud/node-sdk";
7984
8645
  async function createInvocationRuntime(target, requirement = "authenticated") {
7985
8646
  const resolved = await resolveInvocationProfile(target);
7986
8647
  if (!resolved.ok) {
@@ -7994,11 +8655,14 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
7994
8655
  if (requirement === "inspection") {
7995
8656
  return { ok: true, context: { summary } };
7996
8657
  }
8658
+ if (requirement === "restored") {
8659
+ return createRestoredRuntime(target, resolved);
8660
+ }
7997
8661
  const profileName = summary.profile;
7998
8662
  try {
7999
8663
  const {
8000
8664
  TinyCloudNode: TinyCloudNodeConstructor
8001
- } = nodeSdk4;
8665
+ } = nodeSdk6;
8002
8666
  const explicitPrivateKeyOverride = typeof target.privateKey === "string";
8003
8667
  const [session, key] = explicitPrivateKeyOverride ? [null, null] : await Promise.all([
8004
8668
  readSession(profileName),
@@ -8076,7 +8740,9 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
8076
8740
  const runtime = {
8077
8741
  node,
8078
8742
  granted: canonicalizeCapabilities(livePermissions),
8079
- ...warnings.length === 0 ? {} : { warnings }
8743
+ ...warnings.length === 0 ? {} : { warnings },
8744
+ localOwner: explicitPrivateKeyOverride || isLocalOwnerProfile(profile),
8745
+ skippedGrantReasons: warnings.map((warning) => warning.reason)
8080
8746
  };
8081
8747
  return {
8082
8748
  ok: true,
@@ -8100,11 +8766,112 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
8100
8766
  ));
8101
8767
  }
8102
8768
  }
8769
+ async function createRestoredRuntime(target, resolved) {
8770
+ const summary = resolved.context;
8771
+ const profileName = summary.profile;
8772
+ const profile = resolved.profile;
8773
+ if (typeof target.privateKey === "string") {
8774
+ return failed(summary, operationError(
8775
+ "AUTH_REQUIRED",
8776
+ "This operation never signs in; an explicit private key has no stored session.",
8777
+ { details: { reason: "explicit_key" } }
8778
+ ));
8779
+ }
8780
+ try {
8781
+ const actualPosture = resolvePosture(profile);
8782
+ const selectedPosture = profile.authMethod === "local" ? "local-owner-key" : summary.posture;
8783
+ if (actualPosture !== summary.posture || selectedPosture !== summary.posture) {
8784
+ return failed(
8785
+ { ...summary, posture: actualPosture !== summary.posture ? actualPosture : selectedPosture },
8786
+ operationError(
8787
+ "PROFILE_POSTURE_NOT_ALLOWED",
8788
+ "The selected profile changed posture during runtime resolution."
8789
+ )
8790
+ );
8791
+ }
8792
+ const [session, key] = await Promise.all([
8793
+ readSession(profileName),
8794
+ readJson(`${profilePath(profileName)}/key.json`)
8795
+ ]);
8796
+ const activeSession = session === null ? void 0 : normalizeSession(session, key);
8797
+ if (activeSession === void 0) {
8798
+ return failed(summary, operationError(
8799
+ "SESSION_NOT_FOUND",
8800
+ "The selected profile does not have an active session. This operation never signs in.",
8801
+ { details: { recovery: signInRecovery(profileName, summary.posture) } }
8802
+ ));
8803
+ }
8804
+ if (!await bindingMigrationRecorded(profileName)) {
8805
+ const records = await readAdditionalDelegations(profileName);
8806
+ if (records.some(
8807
+ (entry) => storedDelegationKind(entry) === "compact" && !("authorityRequest" in entry)
8808
+ )) {
8809
+ return failed(summary, operationError(
8810
+ "PROFILE_MIGRATION_REQUIRED",
8811
+ `Profile "${profileName}" has a one-time stored-grant upgrade to finish. Run one authenticated command on it (tc --profile <profile> auth caps), then retry.`,
8812
+ { retryable: false, details: { recovery: migrationRecovery(profileName) } }
8813
+ ));
8814
+ }
8815
+ }
8816
+ const { TinyCloudNode: TinyCloudNodeConstructor } = nodeSdk6;
8817
+ const node = new TinyCloudNodeConstructor({
8818
+ host: summary.host,
8819
+ ...typeof profile.privateKey === "string" ? { privateKey: profile.privateKey } : {}
8820
+ });
8821
+ try {
8822
+ await node.restoreSession(activeSession);
8823
+ } catch (error) {
8824
+ if (isRecord7(error) && error.code === "AUTH_EXPIRED") {
8825
+ return failed(summary, operationError(
8826
+ "AUTH_REQUIRED",
8827
+ "The stored session has expired or is no longer valid. Sign in again.",
8828
+ { retryable: false, details: { recovery: signInRecovery(profileName, summary.posture) } }
8829
+ ));
8830
+ }
8831
+ throw error;
8832
+ }
8833
+ const activeSessionDid = normalizeDid(node.sessionDid);
8834
+ const livePrincipalDid = normalizeDid(node.did);
8835
+ const livePermissions = [...node.getVerifiedSessionCapabilities()];
8836
+ const replay = await replayStoredDelegationRecords(
8837
+ profileName,
8838
+ node,
8839
+ { host: summary.host, resolveSpace: operationSpaceResolver(node, summary.space) }
8840
+ );
8841
+ for (const installed of replay.installed) {
8842
+ livePermissions.push(...installed.effectivePermissions);
8843
+ }
8844
+ const runtime = {
8845
+ node,
8846
+ granted: canonicalizeCapabilities(livePermissions),
8847
+ ...replay.warnings.length === 0 ? {} : { warnings: replay.warnings },
8848
+ localOwner: isLocalOwnerProfile(profile),
8849
+ skippedGrantReasons: replay.warnings.map((warning) => warning.reason)
8850
+ };
8851
+ return {
8852
+ ok: true,
8853
+ context: {
8854
+ summary: {
8855
+ ...summary,
8856
+ ...livePrincipalDid === void 0 ? {} : { principalDid: livePrincipalDid },
8857
+ ...activeSessionDid === void 0 ? {} : { sessionDid: activeSessionDid }
8858
+ },
8859
+ runtime
8860
+ }
8861
+ };
8862
+ } catch {
8863
+ return failed(summary, operationError(
8864
+ "NODE_ERROR",
8865
+ "The selected profile runtime could not be initialized.",
8866
+ { retryable: true }
8867
+ ));
8868
+ }
8869
+ }
8103
8870
  function spaceForAuthenticatedPrincipal(principal2) {
8104
8871
  if (principal2 === void 0) return void 0;
8105
8872
  try {
8106
- const pkh = nodeSdk4.parsePkhDid(principal2);
8107
- return pkh === null ? void 0 : nodeSdk4.makePkhSpaceId(pkh.address, pkh.chainId, "secrets");
8873
+ const pkh = nodeSdk6.parsePkhDid(principal2);
8874
+ return pkh === null ? void 0 : nodeSdk6.makePkhSpaceId(pkh.address, pkh.chainId, "secrets");
8108
8875
  } catch {
8109
8876
  return void 0;
8110
8877
  }
@@ -8154,6 +8921,7 @@ var init_runtime = __esm({
8154
8921
  init_authority();
8155
8922
  init_errors2();
8156
8923
  init_profile();
8924
+ init_recovery();
8157
8925
  init_secrets();
8158
8926
  init_state();
8159
8927
  init_delegation_binding();
@@ -8168,11 +8936,14 @@ init_artifacts();
8168
8936
  import { createHash } from "crypto";
8169
8937
  import { isAbsolute as isAbsolute2 } from "path";
8170
8938
  import {
8171
- makePkhSpaceId as makePkhSpaceId3,
8172
- parsePkhDid as parsePkhDid3,
8939
+ makePkhSpaceId as makePkhSpaceId4,
8940
+ parsePkhDid as parsePkhDid4,
8173
8941
  parseSpaceUri as parseSpaceUri2
8174
8942
  } from "@tinycloud/node-sdk";
8175
8943
 
8944
+ // src/redaction.ts
8945
+ init_recovery();
8946
+
8176
8947
  // src/safe-values.ts
8177
8948
  function safeOriginHost(value) {
8178
8949
  if (typeof value !== "string" || value.length === 0) return void 0;
@@ -8249,7 +9020,25 @@ function redactOperationError(_definition, error) {
8249
9020
  }
8250
9021
  var SAFE_ERROR_DETAIL_POLICIES = {
8251
9022
  INPUT_INVALID: {
8252
- issues: (value) => Array.isArray(value) ? safeIssueCodes(value) : void 0
9023
+ issues: (value) => Array.isArray(value) ? safeIssueCodes(value) : void 0,
9024
+ reason: (value) => value === "OWNER_MISMATCH" || value === "OWNER_DID_UNKNOWN" ? value : void 0
9025
+ },
9026
+ AUTH_REQUIRED: {
9027
+ recovery: (value) => isSafeRecovery(value, "sign_in") ? { ...value } : void 0,
9028
+ reason: (value) => value === "explicit_key" ? value : void 0
9029
+ },
9030
+ SESSION_NOT_FOUND: {
9031
+ recovery: (value) => isSafeRecovery(value, "sign_in") ? { ...value } : void 0
9032
+ },
9033
+ PROFILE_MIGRATION_REQUIRED: {
9034
+ recovery: (value) => isSafeRecovery(value, "complete_migration") ? { ...value } : void 0
9035
+ },
9036
+ // `secrets.get` not found for an identity no request link serves.
9037
+ KV_NOT_FOUND: {
9038
+ recoveryKind: (value) => value === "put_locally" || value === "not_requestable" ? value : void 0,
9039
+ names: (value) => Array.isArray(value) && value.length >= 1 && value.length <= 10 && value.every((name) => typeof name === "string" && name.length <= 256 && /^[A-Z][A-Z0-9_]*$/.test(name)) ? [...value] : void 0,
9040
+ scope: (value) => typeof value === "string" && value.length <= 256 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(value) ? value : void 0,
9041
+ owner: (value) => typeof value === "string" && /^did:pkh:eip155:[1-9][0-9]*:0x[0-9a-fA-F]{40}$/.test(value) ? value : void 0
8253
9042
  },
8254
9043
  OPERATION_VERSION_UNSUPPORTED: {
8255
9044
  supportedVersions: (value) => Array.isArray(value) && value.every((item) => Number.isInteger(item)) ? value : void 0
@@ -8533,7 +9322,7 @@ async function prepareInvocation(operationId, operationVersion, invocationTarget
8533
9322
  )
8534
9323
  ) };
8535
9324
  }
8536
- if (target.privateKey === void 0 && definition.runtime === "authenticated" && isOwnerPosture(inspectionResolution.context.summary.posture) && target.allowOwnerProfile !== true) {
9325
+ if (target.privateKey === void 0 && requiresRuntime(definition.runtime) && isOwnerPosture(inspectionResolution.context.summary.posture) && target.allowOwnerProfile !== true) {
8537
9326
  return { status: "error", result: errorResult(
8538
9327
  operation,
8539
9328
  inspectionResolution.context.summary,
@@ -8543,15 +9332,15 @@ async function prepareInvocation(operationId, operationVersion, invocationTarget
8543
9332
  )
8544
9333
  ) };
8545
9334
  }
8546
- const runtimeResolution = definition.runtime === "inspection" ? inspectionResolution : await resolveRuntime(target, "authenticated");
9335
+ const runtimeResolution = definition.runtime === "inspection" ? inspectionResolution : await resolveRuntime(target, definition.runtime);
8547
9336
  if (!runtimeResolution.ok) {
8548
9337
  return { status: "error", result: errorResult(
8549
9338
  operation,
8550
9339
  runtimeResolution.context,
8551
- runtimeResolution.error
9340
+ redactOperationError(definition, runtimeResolution.error)
8552
9341
  ) };
8553
9342
  }
8554
- if (definition.runtime === "authenticated") {
9343
+ if (requiresRuntime(definition.runtime)) {
8555
9344
  const actualPosture = runtimeResolution.context.summary.posture;
8556
9345
  if (!definition.postures.includes(actualPosture)) {
8557
9346
  return { status: "error", result: errorResult(
@@ -8951,8 +9740,8 @@ function canonicalPkhOwner(space) {
8951
9740
  const parsedSpace = parseSpaceUri2(space);
8952
9741
  if (parsedSpace === null) return void 0;
8953
9742
  try {
8954
- const pkh = parsePkhDid3(parsedSpace.owner);
8955
- return pkh === null ? void 0 : parseSpaceUri2(makePkhSpaceId3(pkh.address, pkh.chainId, parsedSpace.name))?.owner;
9743
+ const pkh = parsePkhDid4(parsedSpace.owner);
9744
+ return pkh === null ? void 0 : parseSpaceUri2(makePkhSpaceId4(pkh.address, pkh.chainId, parsedSpace.name))?.owner;
8956
9745
  } catch {
8957
9746
  return void 0;
8958
9747
  }
@@ -9037,6 +9826,15 @@ function internalOperationErrorResult(prepared) {
9037
9826
  function isOwnerPosture(posture) {
9038
9827
  return posture === "owner-openkey" || posture === "local-owner-key";
9039
9828
  }
9829
+ function requiresRuntime(requirement) {
9830
+ switch (requirement) {
9831
+ case "authenticated":
9832
+ case "restored":
9833
+ return true;
9834
+ case "inspection":
9835
+ return false;
9836
+ }
9837
+ }
9040
9838
  function zodIssueDetails(error) {
9041
9839
  return {
9042
9840
  // The canonical error channel carries only schema-owned issue codes.