@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
@@ -1,7 +1,7 @@
1
1
  import { PermissionEntry, ValidatedRuntimeDelegation, RuntimeDelegationActivator, PortableDelegation } from '@tinycloud/node-sdk';
2
2
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.cjs';
3
- import { S as StoredGrantSkipReason, b as OperationWarning } from './contract-DfyrluLB.cjs';
4
- export { c as STORED_GRANT_SKIP_REASONS } from './contract-DfyrluLB.cjs';
3
+ import { S as StoredGrantSkipReason, b as OperationWarning } from './contract-zRklonbR.cjs';
4
+ export { c as STORED_GRANT_SKIP_REASONS } from './contract-zRklonbR.cjs';
5
5
  import '@tinycloud/sdk-core';
6
6
 
7
7
  /** Creates the one owner-aware resolver shared by operation matching paths. */
@@ -130,6 +130,18 @@ declare function replayProfileDelegations(profile: string, node: RuntimeDelegati
130
130
  installed: ValidatedRuntimeDelegation[];
131
131
  warnings: OperationWarning[];
132
132
  }>;
133
+ /**
134
+ * Replays the profile's stored records as they are, under the replay rule,
135
+ * without migrating: no lock, no store or marker write. Runtimes that must
136
+ * never write profile state (the `"restored"` runtime) call only this.
137
+ */
138
+ declare function replayStoredDelegationRecords(profile: string, node: RuntimeDelegationActivator, options: {
139
+ readonly host: string;
140
+ readonly resolveSpace: OperationSpaceResolver;
141
+ }): Promise<{
142
+ installed: ValidatedRuntimeDelegation[];
143
+ warnings: OperationWarning[];
144
+ }>;
133
145
  /** Return an audience only after the exact compact bytes, CID and signature verify. */
134
146
  declare function verifiedCompactAudience(node: RuntimeDelegationActivator, delegation: PortableDelegation): string | undefined;
135
147
  /** Only a verified signed audience and matching transport audience can identify a foreign grant. */
@@ -171,4 +183,4 @@ declare function inspectStoredDelegation(entry: unknown, options?: {
171
183
  /** An unreadable marker counts as migrated: a damaged marker never reopens migration. */
172
184
  declare function bindingMigrationRecorded(profile: string): Promise<boolean>;
173
185
 
174
- export { BINDING_NOTES, type BindingSource, OperationWarning, type StoredDelegationInspection, type StoredDelegationReplay, type StoredDelegationSource, StoredGrantSkipReason, activateUnboundImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, inspectStoredDelegation, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayProfileDelegations, replayStoredDelegation, storedDelegationForAnotherKey, storedDelegationKind, storedDelegationSource, verifiedCompactAudience, verifiedStoredCid, withoutBearer };
186
+ export { BINDING_NOTES, type BindingSource, OperationWarning, type StoredDelegationInspection, type StoredDelegationReplay, type StoredDelegationSource, StoredGrantSkipReason, activateUnboundImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, inspectStoredDelegation, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayProfileDelegations, replayStoredDelegation, replayStoredDelegationRecords, storedDelegationForAnotherKey, storedDelegationKind, storedDelegationSource, verifiedCompactAudience, verifiedStoredCid, withoutBearer };
@@ -1,7 +1,7 @@
1
1
  import { PermissionEntry, ValidatedRuntimeDelegation, RuntimeDelegationActivator, PortableDelegation } from '@tinycloud/node-sdk';
2
2
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.js';
3
- import { S as StoredGrantSkipReason, b as OperationWarning } from './contract-DfyrluLB.js';
4
- export { c as STORED_GRANT_SKIP_REASONS } from './contract-DfyrluLB.js';
3
+ import { S as StoredGrantSkipReason, b as OperationWarning } from './contract-zRklonbR.js';
4
+ export { c as STORED_GRANT_SKIP_REASONS } from './contract-zRklonbR.js';
5
5
  import '@tinycloud/sdk-core';
6
6
 
7
7
  /** Creates the one owner-aware resolver shared by operation matching paths. */
@@ -130,6 +130,18 @@ declare function replayProfileDelegations(profile: string, node: RuntimeDelegati
130
130
  installed: ValidatedRuntimeDelegation[];
131
131
  warnings: OperationWarning[];
132
132
  }>;
133
+ /**
134
+ * Replays the profile's stored records as they are, under the replay rule,
135
+ * without migrating: no lock, no store or marker write. Runtimes that must
136
+ * never write profile state (the `"restored"` runtime) call only this.
137
+ */
138
+ declare function replayStoredDelegationRecords(profile: string, node: RuntimeDelegationActivator, options: {
139
+ readonly host: string;
140
+ readonly resolveSpace: OperationSpaceResolver;
141
+ }): Promise<{
142
+ installed: ValidatedRuntimeDelegation[];
143
+ warnings: OperationWarning[];
144
+ }>;
133
145
  /** Return an audience only after the exact compact bytes, CID and signature verify. */
134
146
  declare function verifiedCompactAudience(node: RuntimeDelegationActivator, delegation: PortableDelegation): string | undefined;
135
147
  /** Only a verified signed audience and matching transport audience can identify a foreign grant. */
@@ -171,4 +183,4 @@ declare function inspectStoredDelegation(entry: unknown, options?: {
171
183
  /** An unreadable marker counts as migrated: a damaged marker never reopens migration. */
172
184
  declare function bindingMigrationRecorded(profile: string): Promise<boolean>;
173
185
 
174
- export { BINDING_NOTES, type BindingSource, OperationWarning, type StoredDelegationInspection, type StoredDelegationReplay, type StoredDelegationSource, StoredGrantSkipReason, activateUnboundImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, inspectStoredDelegation, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayProfileDelegations, replayStoredDelegation, storedDelegationForAnotherKey, storedDelegationKind, storedDelegationSource, verifiedCompactAudience, verifiedStoredCid, withoutBearer };
186
+ export { BINDING_NOTES, type BindingSource, OperationWarning, type StoredDelegationInspection, type StoredDelegationReplay, type StoredDelegationSource, StoredGrantSkipReason, activateUnboundImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, inspectStoredDelegation, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayProfileDelegations, replayStoredDelegation, replayStoredDelegationRecords, storedDelegationForAnotherKey, storedDelegationKind, storedDelegationSource, verifiedCompactAudience, verifiedStoredCid, withoutBearer };
@@ -3624,10 +3624,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace) {
3624
3624
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
3625
3625
  const missing = [];
3626
3626
  for (const requested of canonicalRequired) {
3627
- const covered = canonicalGranted.some(
3628
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
3627
+ const sameScope = canonicalGranted.filter(
3628
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
3629
3629
  );
3630
- if (!covered) missing.push(requested);
3630
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
3631
3631
  }
3632
3632
  return {
3633
3633
  satisfied: missing.length === 0,
@@ -3704,9 +3704,11 @@ function canonicalizeCaveats(caveats) {
3704
3704
  // src/state.ts
3705
3705
  import { randomUUID } from "crypto";
3706
3706
  import { AsyncLocalStorage } from "async_hooks";
3707
+ import { readFileSync, readlinkSync } from "fs";
3707
3708
  import {
3708
3709
  chmod,
3709
3710
  link,
3711
+ lstat,
3710
3712
  mkdir,
3711
3713
  open,
3712
3714
  readFile,
@@ -3718,6 +3720,7 @@ import {
3718
3720
  unlink,
3719
3721
  writeFile
3720
3722
  } from "fs/promises";
3723
+ import { connect, createServer } from "net";
3721
3724
  import { homedir } from "os";
3722
3725
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
3723
3726
  var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
@@ -3737,13 +3740,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
3737
3740
  var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
3738
3741
  var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
3739
3742
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
3743
+ var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
3740
3744
  function processWideContext(key) {
3741
- var _a3;
3745
+ var _a4;
3742
3746
  const registry = globalThis;
3743
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
3747
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
3744
3748
  }
3745
3749
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
3746
3750
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
3751
+ var _a2;
3752
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
3753
+ var cachedProcessOrigin;
3754
+ function processOrigin() {
3755
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
3756
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
3757
+ try {
3758
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
3759
+ const pidNamespace = readlinkSync("/proc/self/ns/pid");
3760
+ const processStat = readFileSync("/proc/self/stat", "utf8");
3761
+ const closingParen = processStat.lastIndexOf(") ");
3762
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
3763
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
3764
+ return cachedProcessOrigin = null;
3765
+ }
3766
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
3767
+ } catch {
3768
+ return cachedProcessOrigin = null;
3769
+ }
3770
+ }
3771
+ function processOwner(liveness) {
3772
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
3773
+ }
3774
+ var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
3775
+ var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
3776
+ function socketDirectory(name) {
3777
+ const match = LIVENESS_SOCKET.exec(name);
3778
+ return match ? `${match[1]}-${match[2]}` : null;
3779
+ }
3780
+ async function pinDirectory(directory) {
3781
+ const handle = await open(directory, "r");
3782
+ try {
3783
+ const { dev, ino } = await handle.stat({ bigint: true });
3784
+ return {
3785
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
3786
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
3787
+ close: () => handle.close()
3788
+ };
3789
+ } catch (error) {
3790
+ await handle.close();
3791
+ throw error;
3792
+ }
3793
+ }
3794
+ async function bindLiveness(directory) {
3795
+ if (!processOrigin()) return null;
3796
+ let pinned;
3797
+ try {
3798
+ pinned = await pinDirectory(directory);
3799
+ } catch {
3800
+ return null;
3801
+ }
3802
+ const socket = `.live-${pinned.identity}-${randomUUID()}`;
3803
+ const server = createServer((connection) => connection.destroy());
3804
+ let bound = false;
3805
+ try {
3806
+ await new Promise((resolve2, reject) => {
3807
+ server.once("error", reject);
3808
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
3809
+ server.off("error", reject);
3810
+ resolve2();
3811
+ });
3812
+ });
3813
+ bound = true;
3814
+ server.on("error", () => void 0);
3815
+ server.unref();
3816
+ await rename(pinned.path(`${socket}.new`), pinned.path(socket));
3817
+ const stats = await lstat(pinned.path(socket), { bigint: true });
3818
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
3819
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
3820
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
3821
+ } catch {
3822
+ if (bound) {
3823
+ server.close();
3824
+ for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
3825
+ }
3826
+ return null;
3827
+ } finally {
3828
+ await pinned.close();
3829
+ }
3830
+ }
3831
+ async function ensureLiveness(acquisition) {
3832
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
3833
+ return acquisition.held;
3834
+ }
3835
+ async function closeLiveness(held) {
3836
+ held.server.close();
3837
+ await unlink(held.path).catch(() => void 0);
3838
+ }
3839
+ function parseLiveness(value) {
3840
+ if (typeof value !== "object" || value === null) return null;
3841
+ const { socket, dev, ino } = value;
3842
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
3843
+ 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;
3844
+ }
3845
+ async function controlIn(pinned, acquisition) {
3846
+ const own = await ensureLiveness(acquisition);
3847
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
3848
+ const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
3849
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
3850
+ }
3851
+ async function tryConnect(path) {
3852
+ return new Promise((resolve2) => {
3853
+ const probe = connect({ path });
3854
+ const settle = (outcome) => {
3855
+ clearTimeout(timer);
3856
+ probe.destroy();
3857
+ resolve2(outcome);
3858
+ };
3859
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
3860
+ probe.once("connect", () => settle("connected"));
3861
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
3862
+ });
3863
+ }
3864
+ async function kernelRefuses(pinned, name, control) {
3865
+ if (process.versions.bun !== void 0) return false;
3866
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
3867
+ }
3868
+ async function probeLiveness(liveness, acquisition) {
3869
+ let pinned;
3870
+ try {
3871
+ pinned = await pinDirectory(acquisition.directory);
3872
+ } catch {
3873
+ return "unknown";
3874
+ }
3875
+ try {
3876
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
3877
+ const control = await controlIn(pinned, acquisition);
3878
+ if (!control) return "unknown";
3879
+ let stats;
3880
+ try {
3881
+ stats = await lstat(pinned.path(liveness.socket), { bigint: true });
3882
+ } catch (error) {
3883
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
3884
+ }
3885
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
3886
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
3887
+ } finally {
3888
+ await pinned.close();
3889
+ }
3890
+ }
3891
+ async function sweepLiveness(acquisition) {
3892
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
3893
+ const pinned = await pinDirectory(acquisition.directory);
3894
+ try {
3895
+ const control = await controlIn(pinned, acquisition);
3896
+ if (!control) return;
3897
+ for (const name of await readdir(pinned.path("."))) {
3898
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
3899
+ const stats = await lstat(pinned.path(name)).catch(() => null);
3900
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
3901
+ await unlink(pinned.path(name)).catch(() => void 0);
3902
+ }
3903
+ }
3904
+ } finally {
3905
+ await pinned.close();
3906
+ }
3907
+ }
3747
3908
  var ProfileLockTimeoutError = class extends Error {
3748
3909
  /** `detail` names the cause when it is not simply another process holding the lock. */
3749
3910
  constructor(profile, timeoutMs, detail) {
@@ -3915,7 +4076,18 @@ async function acquireProfileLock(profile, options) {
3915
4076
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
3916
4077
  });
3917
4078
  }
3918
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4079
+ const liveness = { directory: turnDirectory, held: void 0 };
4080
+ const closeAcquisitionLiveness = async () => {
4081
+ if (liveness.held) await closeLiveness(liveness.held);
4082
+ };
4083
+ let taken;
4084
+ try {
4085
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4086
+ } catch (error) {
4087
+ await closeAcquisitionLiveness();
4088
+ throw error;
4089
+ }
4090
+ const { turn, firstWaitedTurn } = taken;
3919
4091
  try {
3920
4092
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
3921
4093
  await rmdir(directory).catch(() => void 0);
@@ -3925,7 +4097,7 @@ async function acquireProfileLock(profile, options) {
3925
4097
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
3926
4098
  while (true) {
3927
4099
  const token = randomUUID();
3928
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4100
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
3929
4101
  return {
3930
4102
  deletedWhileWaiting,
3931
4103
  turnSlot: turn.slot,
@@ -3933,17 +4105,19 @@ async function acquireProfileLock(profile, options) {
3933
4105
  try {
3934
4106
  await releaseProfileLock(profile, lockPath, token);
3935
4107
  } finally {
3936
- await releaseTurn(profile, turn);
4108
+ await releaseTurn(profile, turn, liveness);
4109
+ await closeAcquisitionLiveness();
3937
4110
  }
3938
4111
  }
3939
4112
  };
3940
4113
  }
3941
4114
  await signalTestLockContention(profile);
3942
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4115
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
3943
4116
  await waitOrTimeOut(profile, deadline);
3944
4117
  }
3945
4118
  } catch (error) {
3946
- await releaseTurn(profile, turn);
4119
+ await releaseTurn(profile, turn, liveness);
4120
+ await closeAcquisitionLiveness();
3947
4121
  throw error;
3948
4122
  }
3949
4123
  }
@@ -3957,12 +4131,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
3957
4131
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
3958
4132
  }
3959
4133
  var TURN = /^(?:0|[1-9][0-9]*)$/;
3960
- var _a2;
3961
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
4134
+ var _a3;
4135
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
3962
4136
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
3963
4137
  var SETTLE_RETRY_MS = 1e3;
3964
4138
  var SETTLE_RETRIES = 300;
3965
- async function acquireTurn(profile, directory, deadline) {
4139
+ async function acquireTurn(profile, directory, deadline, liveness) {
3966
4140
  let firstWaitedTurn;
3967
4141
  let problem;
3968
4142
  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.`;
@@ -3987,13 +4161,13 @@ async function acquireTurn(profile, directory, deadline) {
3987
4161
  }
3988
4162
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
3989
4163
  if (latest.held && latest.done) {
3990
- const turn = await takeTurn(profile, directory, latest);
4164
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
3991
4165
  if (turn !== null) return { turn, firstWaitedTurn };
3992
4166
  await waitOrTimeOut(profile, deadline);
3993
4167
  } else if (latest.done) {
3994
4168
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
3995
4169
  await waitOrTimeOut(profile, deadline, problem);
3996
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4170
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
3997
4171
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
3998
4172
  await settleAbandonedTurn(directory, latest);
3999
4173
  relinquishedTurns.delete(latest.token);
@@ -4038,6 +4212,7 @@ async function readTurn(directory, slot) {
4038
4212
  const done = await exists(join(path, `${owner.token}.done`));
4039
4213
  const held = await exists(join(path, `${owner.token}.held`));
4040
4214
  return {
4215
+ ...owner,
4041
4216
  slot,
4042
4217
  token: owner.token,
4043
4218
  pid: owner.pid,
@@ -4051,11 +4226,11 @@ async function isFinishedTurn(directory, slot, token) {
4051
4226
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4052
4227
  return turn !== null && turn.token === token && turn.held && turn.done;
4053
4228
  }
4054
- async function takeTurn(profile, directory, latest) {
4229
+ async function takeTurn(profile, directory, latest, liveness) {
4055
4230
  const token = randomUUID();
4056
4231
  const slot = latest.slot + 1;
4057
4232
  const path = join(directory, String(slot));
4058
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4233
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4059
4234
  let published = false;
4060
4235
  try {
4061
4236
  published = await publishDirectory(
@@ -4109,7 +4284,7 @@ async function markTurn(path, token, kind) {
4109
4284
  }
4110
4285
  async function createTurnDirectory(directory) {
4111
4286
  const token = randomUUID();
4112
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4287
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4113
4288
  await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
4114
4289
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4115
4290
  `,
@@ -4133,31 +4308,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4133
4308
  await rm(stage, { recursive: true, force: true }).catch(() => void 0);
4134
4309
  }
4135
4310
  }
4136
- async function releaseTurn(profile, turn) {
4311
+ async function releaseTurn(profile, turn, liveness) {
4137
4312
  try {
4138
4313
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
4139
4314
  } catch {
4140
4315
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4141
4316
  return;
4142
4317
  }
4143
- await collectTurns(profile, turn).catch(() => void 0);
4318
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4144
4319
  }
4145
- function isAbandonedTurnEntry(name) {
4320
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4146
4321
  if (name.startsWith(".trash-")) return true;
4147
4322
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4148
- return writer !== void 0 && !isProcessAlive(Number(writer));
4323
+ if (writer === void 0) return false;
4324
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4325
+ return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
4149
4326
  }
4150
- function isAbandonedTurnDirectoryStage(name) {
4327
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4151
4328
  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];
4152
- return writer !== void 0 && !isProcessAlive(Number(writer));
4329
+ if (writer === void 0) return false;
4330
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4331
+ return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
4332
+ }
4333
+ async function isStagedOwnerGone(recordPath, liveness) {
4334
+ const owner = await readJson(recordPath).catch(() => null);
4335
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4153
4336
  }
4154
- async function collectTurns(profile, turn) {
4337
+ async function collectTurns(profile, turn, liveness) {
4155
4338
  for (const [directory, abandoned] of [
4156
4339
  [turn.directory, isAbandonedTurnEntry],
4157
4340
  [dirname(turn.directory), isAbandonedTurnDirectoryStage]
4158
4341
  ]) {
4159
- for (const name of (await readdir(directory)).filter(abandoned)) {
4160
- await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4342
+ for (const name of await readdir(directory)) {
4343
+ if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
4161
4344
  }
4162
4345
  }
4163
4346
  const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -4175,8 +4358,9 @@ async function collectTurns(profile, turn) {
4175
4358
  await rm(trash, { recursive: true, force: true }).catch(() => void 0);
4176
4359
  }
4177
4360
  }
4361
+ await sweepLiveness(liveness);
4178
4362
  }
4179
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4363
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4180
4364
  try {
4181
4365
  await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
4182
4366
  } catch (error) {
@@ -4189,7 +4373,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4189
4373
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4190
4374
  const ownerPath = join(lockPath, "owner.json");
4191
4375
  const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
4192
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4376
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4193
4377
  `;
4194
4378
  try {
4195
4379
  await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4234,7 +4418,7 @@ async function signalTestLockContention(profile) {
4234
4418
  await writeFile(signalPath, `${profile}
4235
4419
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4236
4420
  }
4237
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4421
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4238
4422
  let aged;
4239
4423
  try {
4240
4424
  const { mtimeMs } = await stat(lockPath);
@@ -4251,7 +4435,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4251
4435
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4252
4436
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4253
4437
  const claimPath = join(lockPath, name);
4254
- if (await hasLiveRecoveryOwner(claimPath)) {
4438
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4255
4439
  if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
4256
4440
  return false;
4257
4441
  }
@@ -4267,7 +4451,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4267
4451
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4268
4452
  return true;
4269
4453
  }
4270
- async function hasLiveRecoveryOwner(claimPath) {
4454
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4271
4455
  let contents;
4272
4456
  try {
4273
4457
  contents = await readFile(claimPath, "utf8");
@@ -4281,8 +4465,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4281
4465
  } catch {
4282
4466
  return false;
4283
4467
  }
4284
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4285
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4468
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4469
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4286
4470
  }
4287
4471
  function recoveryFenced(since, staleAfterMs) {
4288
4472
  return performance.now() - since >= staleAfterMs / 2;
@@ -4299,11 +4483,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4299
4483
  if (aged) await rm(path, { force: true }).catch(() => void 0);
4300
4484
  }
4301
4485
  }
4302
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4486
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4303
4487
  const ownerPath = join(lockPath, "owner.json");
4304
4488
  const observed = await readFile(ownerPath, "utf8").catch(() => null);
4305
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4306
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4489
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4490
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4307
4491
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4308
4492
  const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
4309
4493
  let moved = false;
@@ -4339,7 +4523,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4339
4523
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4340
4524
  return true;
4341
4525
  }
4342
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4526
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4343
4527
  let owner;
4344
4528
  try {
4345
4529
  owner = JSON.parse(observed);
@@ -4347,7 +4531,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4347
4531
  owner = null;
4348
4532
  }
4349
4533
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4350
- return isStaleOwner(owner, staleAfterMs);
4534
+ return isStaleOwner(owner, liveness, staleAfterMs);
4351
4535
  }
4352
4536
  const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4353
4537
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4369,14 +4553,14 @@ async function waitForTestBarrier(environmentName, profile) {
4369
4553
  }
4370
4554
  }
4371
4555
  }
4372
- function isStaleOwner(owner, staleAfterMs) {
4556
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4373
4557
  const now = Date.now();
4374
4558
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4375
4559
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4376
4560
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4377
4561
  return false;
4378
4562
  }
4379
- return !isProcessAlive(owner.pid);
4563
+ return !await isRecordedProcessAlive(owner, liveness);
4380
4564
  }
4381
4565
  async function exists(path) {
4382
4566
  try {
@@ -4387,13 +4571,22 @@ async function exists(path) {
4387
4571
  throw error;
4388
4572
  }
4389
4573
  }
4390
- function isProcessAlive(pid) {
4574
+ async function isRecordedProcessAlive(owner, acquisition) {
4575
+ const pid = owner.pid;
4576
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4391
4577
  try {
4392
4578
  process.kill(pid, 0);
4393
- return true;
4394
4579
  } catch (error) {
4395
4580
  return !isErrno(error, "ESRCH");
4396
4581
  }
4582
+ 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;
4583
+ const origin = processOrigin();
4584
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
4585
+ if (owner.pidNamespace === origin.pidNamespace) {
4586
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
4587
+ }
4588
+ const liveness = parseLiveness(owner.liveness);
4589
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4397
4590
  }
4398
4591
  function validateProfileName(profile) {
4399
4592
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
@@ -4493,6 +4686,8 @@ var STORED_GRANT_SKIP_REASONS = [
4493
4686
  "resources_mismatch",
4494
4687
  "unsupported",
4495
4688
  "activation_rejected",
4689
+ "activation_unavailable",
4690
+ "activation_unauthenticated",
4496
4691
  "invalid"
4497
4692
  ];
4498
4693
  var DELEGATION_CID_PATTERN = "^bafkr4i[a-z2-7]{52}$";
@@ -4726,6 +4921,9 @@ function skipped(reason) {
4726
4921
  }
4727
4922
  async function replayProfileDelegations(profile, node, options) {
4728
4923
  await prepareStoredDelegationReplay(profile, node, { host: options.host, migrate: options.migrate });
4924
+ return replayStoredDelegationRecords(profile, node, options);
4925
+ }
4926
+ async function replayStoredDelegationRecords(profile, node, options) {
4729
4927
  const installed = [];
4730
4928
  const warnings = [];
4731
4929
  const seen = /* @__PURE__ */ new Set();
@@ -4916,6 +5114,7 @@ export {
4916
5114
  replayLimit,
4917
5115
  replayProfileDelegations,
4918
5116
  replayStoredDelegation,
5117
+ replayStoredDelegationRecords,
4919
5118
  storedDelegationForAnotherKey,
4920
5119
  storedDelegationKind,
4921
5120
  storedDelegationSource,