@tinycloud/operations 0.5.0-beta.1 → 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 +961 -178
  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 +960 -177
  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 +989 -179
  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 +986 -176
  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,4 +1,4 @@
1
- declare const OPERATION_ERROR_CODES: readonly ["INPUT_INVALID", "OPERATION_NOT_FOUND", "OPERATION_VERSION_UNSUPPORTED", "PROFILE_NOT_FOUND", "PROFILE_POSTURE_NOT_ALLOWED", "PROFILE_OWNER_OPT_IN_REQUIRED", "AUTH_REQUIRED", "PERMISSION_DENIED", "SESSION_NOT_FOUND", "PERMISSION_HINT_INVALID", "DELEGATION_ARTIFACT_INVALID", "DELEGATION_EXPIRED", "DELEGATION_AUDIENCE_MISMATCH", "DELEGATION_HOST_MISMATCH", "DELEGATION_REJECTED", "ENCRYPTION_NETWORK_UNRESOLVED", "NODE_UNREACHABLE", "NODE_ERROR", "STORAGE_QUOTA_EXCEEDED", "KV_NOT_FOUND", "KV_PRECONDITION_FAILED", "KV_CONFLICT", "KV_RESPONSE_TOO_LARGE", "SQL_QUERY_INVALID", "SQL_EXECUTION_FAILED", "SQL_RESULT_LIMIT_EXCEEDED", "SQL_VALUE_UNSAFE", "SECRET_READ_FAILED", "SECRET_DECRYPT_FAILED", "OUTPUT_INVALID", "INTERNAL_ERROR"];
1
+ declare const OPERATION_ERROR_CODES: readonly ["INPUT_INVALID", "OPERATION_NOT_FOUND", "OPERATION_VERSION_UNSUPPORTED", "PROFILE_NOT_FOUND", "PROFILE_POSTURE_NOT_ALLOWED", "PROFILE_OWNER_OPT_IN_REQUIRED", "PROFILE_MIGRATION_REQUIRED", "AUTH_REQUIRED", "PERMISSION_DENIED", "SESSION_NOT_FOUND", "PERMISSION_HINT_INVALID", "DELEGATION_ARTIFACT_INVALID", "DELEGATION_EXPIRED", "DELEGATION_AUDIENCE_MISMATCH", "DELEGATION_HOST_MISMATCH", "DELEGATION_REJECTED", "ENCRYPTION_NETWORK_UNRESOLVED", "NODE_UNREACHABLE", "NODE_ERROR", "STORAGE_QUOTA_EXCEEDED", "KV_NOT_FOUND", "KV_PRECONDITION_FAILED", "KV_CONFLICT", "KV_RESPONSE_TOO_LARGE", "SQL_QUERY_INVALID", "SQL_EXECUTION_FAILED", "SQL_RESULT_LIMIT_EXCEEDED", "SQL_VALUE_UNSAFE", "SECRET_READ_FAILED", "SECRET_DECRYPT_FAILED", "OUTPUT_INVALID", "INTERNAL_ERROR"];
2
2
  type OperationErrorCode = (typeof OPERATION_ERROR_CODES)[number];
3
3
  interface OperationError {
4
4
  readonly code: OperationErrorCode;
@@ -29,9 +29,12 @@ interface OperationContextSummary {
29
29
  * Why a stored grant was not used. A fixed vocabulary: diagnostics never
30
30
  * carry exception text, node responses, URLs or other stored transport data.
31
31
  * `unbound`: the record has no valid request binding where one is required;
32
- * `outside_request`: its signed authority exceeds its binding.
32
+ * `outside_request`: its signed authority exceeds its binding;
33
+ * `activation_unavailable`: the node could not be reached, timed out, was
34
+ * rate limited or failed (transport, 408, 429, 5xx) while activating it;
35
+ * `activation_unauthenticated`: the node answered 401 while activating it.
33
36
  */
34
- declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "invalid"];
37
+ declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "activation_unavailable", "activation_unauthenticated", "invalid"];
35
38
  type StoredGrantSkipReason = typeof STORED_GRANT_SKIP_REASONS[number];
36
39
  /**
37
40
  * A non-fatal diagnostic attached to a result. `STORED_GRANT_SKIPPED`: a
@@ -1,4 +1,4 @@
1
- declare const OPERATION_ERROR_CODES: readonly ["INPUT_INVALID", "OPERATION_NOT_FOUND", "OPERATION_VERSION_UNSUPPORTED", "PROFILE_NOT_FOUND", "PROFILE_POSTURE_NOT_ALLOWED", "PROFILE_OWNER_OPT_IN_REQUIRED", "AUTH_REQUIRED", "PERMISSION_DENIED", "SESSION_NOT_FOUND", "PERMISSION_HINT_INVALID", "DELEGATION_ARTIFACT_INVALID", "DELEGATION_EXPIRED", "DELEGATION_AUDIENCE_MISMATCH", "DELEGATION_HOST_MISMATCH", "DELEGATION_REJECTED", "ENCRYPTION_NETWORK_UNRESOLVED", "NODE_UNREACHABLE", "NODE_ERROR", "STORAGE_QUOTA_EXCEEDED", "KV_NOT_FOUND", "KV_PRECONDITION_FAILED", "KV_CONFLICT", "KV_RESPONSE_TOO_LARGE", "SQL_QUERY_INVALID", "SQL_EXECUTION_FAILED", "SQL_RESULT_LIMIT_EXCEEDED", "SQL_VALUE_UNSAFE", "SECRET_READ_FAILED", "SECRET_DECRYPT_FAILED", "OUTPUT_INVALID", "INTERNAL_ERROR"];
1
+ declare const OPERATION_ERROR_CODES: readonly ["INPUT_INVALID", "OPERATION_NOT_FOUND", "OPERATION_VERSION_UNSUPPORTED", "PROFILE_NOT_FOUND", "PROFILE_POSTURE_NOT_ALLOWED", "PROFILE_OWNER_OPT_IN_REQUIRED", "PROFILE_MIGRATION_REQUIRED", "AUTH_REQUIRED", "PERMISSION_DENIED", "SESSION_NOT_FOUND", "PERMISSION_HINT_INVALID", "DELEGATION_ARTIFACT_INVALID", "DELEGATION_EXPIRED", "DELEGATION_AUDIENCE_MISMATCH", "DELEGATION_HOST_MISMATCH", "DELEGATION_REJECTED", "ENCRYPTION_NETWORK_UNRESOLVED", "NODE_UNREACHABLE", "NODE_ERROR", "STORAGE_QUOTA_EXCEEDED", "KV_NOT_FOUND", "KV_PRECONDITION_FAILED", "KV_CONFLICT", "KV_RESPONSE_TOO_LARGE", "SQL_QUERY_INVALID", "SQL_EXECUTION_FAILED", "SQL_RESULT_LIMIT_EXCEEDED", "SQL_VALUE_UNSAFE", "SECRET_READ_FAILED", "SECRET_DECRYPT_FAILED", "OUTPUT_INVALID", "INTERNAL_ERROR"];
2
2
  type OperationErrorCode = (typeof OPERATION_ERROR_CODES)[number];
3
3
  interface OperationError {
4
4
  readonly code: OperationErrorCode;
@@ -29,9 +29,12 @@ interface OperationContextSummary {
29
29
  * Why a stored grant was not used. A fixed vocabulary: diagnostics never
30
30
  * carry exception text, node responses, URLs or other stored transport data.
31
31
  * `unbound`: the record has no valid request binding where one is required;
32
- * `outside_request`: its signed authority exceeds its binding.
32
+ * `outside_request`: its signed authority exceeds its binding;
33
+ * `activation_unavailable`: the node could not be reached, timed out, was
34
+ * rate limited or failed (transport, 408, 429, 5xx) while activating it;
35
+ * `activation_unauthenticated`: the node answered 401 while activating it.
33
36
  */
34
- declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "invalid"];
37
+ declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "activation_unavailable", "activation_unauthenticated", "invalid"];
35
38
  type StoredGrantSkipReason = typeof STORED_GRANT_SKIP_REASONS[number];
36
39
  /**
37
40
  * A non-fatal diagnostic attached to a result. `STORED_GRANT_SKIPPED`: a
@@ -730,6 +730,7 @@ __export(delegation_binding_exports, {
730
730
  replayLimit: () => replayLimit,
731
731
  replayProfileDelegations: () => replayProfileDelegations,
732
732
  replayStoredDelegation: () => replayStoredDelegation,
733
+ replayStoredDelegationRecords: () => replayStoredDelegationRecords,
733
734
  storedDelegationForAnotherKey: () => storedDelegationForAnotherKey,
734
735
  storedDelegationKind: () => storedDelegationKind,
735
736
  storedDelegationSource: () => storedDelegationSource,
@@ -3653,10 +3654,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace) {
3653
3654
  const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
3654
3655
  const missing = [];
3655
3656
  for (const requested of canonicalRequired) {
3656
- const covered = canonicalGranted.some(
3657
- (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
3657
+ const sameScope = canonicalGranted.filter(
3658
+ (candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
3658
3659
  );
3659
- if (!covered) missing.push(requested);
3660
+ if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
3660
3661
  }
3661
3662
  return {
3662
3663
  satisfied: missing.length === 0,
@@ -3733,7 +3734,9 @@ function canonicalizeCaveats(caveats) {
3733
3734
  // src/state.ts
3734
3735
  var import_node_crypto = require("crypto");
3735
3736
  var import_node_async_hooks = require("async_hooks");
3737
+ var import_node_fs = require("fs");
3736
3738
  var import_promises = require("fs/promises");
3739
+ var import_node_net = require("net");
3737
3740
  var import_node_os = require("os");
3738
3741
  var import_node_path = require("path");
3739
3742
  var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
@@ -3753,13 +3756,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
3753
3756
  var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
3754
3757
  var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
3755
3758
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
3759
+ var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
3756
3760
  function processWideContext(key) {
3757
- var _a3;
3761
+ var _a4;
3758
3762
  const registry = globalThis;
3759
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new import_node_async_hooks.AsyncLocalStorage());
3763
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
3760
3764
  }
3761
3765
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
3762
3766
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
3767
+ var _a2;
3768
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
3769
+ var cachedProcessOrigin;
3770
+ function processOrigin() {
3771
+ if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
3772
+ if (process.platform !== "linux") return cachedProcessOrigin = null;
3773
+ try {
3774
+ const bootId = (0, import_node_fs.readFileSync)("/proc/sys/kernel/random/boot_id", "utf8").trim();
3775
+ const pidNamespace = (0, import_node_fs.readlinkSync)("/proc/self/ns/pid");
3776
+ const processStat = (0, import_node_fs.readFileSync)("/proc/self/stat", "utf8");
3777
+ const closingParen = processStat.lastIndexOf(") ");
3778
+ const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
3779
+ if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
3780
+ return cachedProcessOrigin = null;
3781
+ }
3782
+ return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
3783
+ } catch {
3784
+ return cachedProcessOrigin = null;
3785
+ }
3786
+ }
3787
+ function processOwner(liveness) {
3788
+ return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
3789
+ }
3790
+ var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
3791
+ var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
3792
+ function socketDirectory(name) {
3793
+ const match = LIVENESS_SOCKET.exec(name);
3794
+ return match ? `${match[1]}-${match[2]}` : null;
3795
+ }
3796
+ async function pinDirectory(directory) {
3797
+ const handle = await (0, import_promises.open)(directory, "r");
3798
+ try {
3799
+ const { dev, ino } = await handle.stat({ bigint: true });
3800
+ return {
3801
+ identity: `${dev.toString(16)}-${ino.toString(16)}`,
3802
+ path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
3803
+ close: () => handle.close()
3804
+ };
3805
+ } catch (error) {
3806
+ await handle.close();
3807
+ throw error;
3808
+ }
3809
+ }
3810
+ async function bindLiveness(directory) {
3811
+ if (!processOrigin()) return null;
3812
+ let pinned;
3813
+ try {
3814
+ pinned = await pinDirectory(directory);
3815
+ } catch {
3816
+ return null;
3817
+ }
3818
+ const socket = `.live-${pinned.identity}-${(0, import_node_crypto.randomUUID)()}`;
3819
+ const server = (0, import_node_net.createServer)((connection) => connection.destroy());
3820
+ let bound = false;
3821
+ try {
3822
+ await new Promise((resolve2, reject) => {
3823
+ server.once("error", reject);
3824
+ server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
3825
+ server.off("error", reject);
3826
+ resolve2();
3827
+ });
3828
+ });
3829
+ bound = true;
3830
+ server.on("error", () => void 0);
3831
+ server.unref();
3832
+ await (0, import_promises.rename)(pinned.path(`${socket}.new`), pinned.path(socket));
3833
+ const stats = await (0, import_promises.lstat)(pinned.path(socket), { bigint: true });
3834
+ if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
3835
+ await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, (0, import_node_path.basename)(directory));
3836
+ return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: (0, import_node_path.join)(directory, socket), server };
3837
+ } catch {
3838
+ if (bound) {
3839
+ server.close();
3840
+ for (const name of [`${socket}.new`, socket]) await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
3841
+ }
3842
+ return null;
3843
+ } finally {
3844
+ await pinned.close();
3845
+ }
3846
+ }
3847
+ async function ensureLiveness(acquisition) {
3848
+ if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
3849
+ return acquisition.held;
3850
+ }
3851
+ async function closeLiveness(held) {
3852
+ held.server.close();
3853
+ await (0, import_promises.unlink)(held.path).catch(() => void 0);
3854
+ }
3855
+ function parseLiveness(value) {
3856
+ if (typeof value !== "object" || value === null) return null;
3857
+ const { socket, dev, ino } = value;
3858
+ const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
3859
+ 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;
3860
+ }
3861
+ async function controlIn(pinned, acquisition) {
3862
+ const own = await ensureLiveness(acquisition);
3863
+ if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
3864
+ const stats = await (0, import_promises.lstat)(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
3865
+ return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
3866
+ }
3867
+ async function tryConnect(path) {
3868
+ return new Promise((resolve2) => {
3869
+ const probe = (0, import_node_net.connect)({ path });
3870
+ const settle = (outcome) => {
3871
+ clearTimeout(timer);
3872
+ probe.destroy();
3873
+ resolve2(outcome);
3874
+ };
3875
+ const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
3876
+ probe.once("connect", () => settle("connected"));
3877
+ probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
3878
+ });
3879
+ }
3880
+ async function kernelRefuses(pinned, name, control) {
3881
+ if (process.versions.bun !== void 0) return false;
3882
+ return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
3883
+ }
3884
+ async function probeLiveness(liveness, acquisition) {
3885
+ let pinned;
3886
+ try {
3887
+ pinned = await pinDirectory(acquisition.directory);
3888
+ } catch {
3889
+ return "unknown";
3890
+ }
3891
+ try {
3892
+ if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
3893
+ const control = await controlIn(pinned, acquisition);
3894
+ if (!control) return "unknown";
3895
+ let stats;
3896
+ try {
3897
+ stats = await (0, import_promises.lstat)(pinned.path(liveness.socket), { bigint: true });
3898
+ } catch (error) {
3899
+ return isErrno(error, "ENOENT") ? "gone" : "unknown";
3900
+ }
3901
+ if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
3902
+ return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
3903
+ } finally {
3904
+ await pinned.close();
3905
+ }
3906
+ }
3907
+ async function sweepLiveness(acquisition) {
3908
+ if (process.versions.bun !== void 0 || !acquisition.held) return;
3909
+ const pinned = await pinDirectory(acquisition.directory);
3910
+ try {
3911
+ const control = await controlIn(pinned, acquisition);
3912
+ if (!control) return;
3913
+ for (const name of await (0, import_promises.readdir)(pinned.path("."))) {
3914
+ if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
3915
+ const stats = await (0, import_promises.lstat)(pinned.path(name)).catch(() => null);
3916
+ if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
3917
+ await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
3918
+ }
3919
+ }
3920
+ } finally {
3921
+ await pinned.close();
3922
+ }
3923
+ }
3763
3924
  var ProfileLockTimeoutError = class extends Error {
3764
3925
  /** `detail` names the cause when it is not simply another process holding the lock. */
3765
3926
  constructor(profile, timeoutMs, detail) {
@@ -3931,7 +4092,18 @@ async function acquireProfileLock(profile, options) {
3931
4092
  if (path !== directory || !isErrno(error, "ENOENT")) throw error;
3932
4093
  });
3933
4094
  }
3934
- const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
4095
+ const liveness = { directory: turnDirectory, held: void 0 };
4096
+ const closeAcquisitionLiveness = async () => {
4097
+ if (liveness.held) await closeLiveness(liveness.held);
4098
+ };
4099
+ let taken;
4100
+ try {
4101
+ taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
4102
+ } catch (error) {
4103
+ await closeAcquisitionLiveness();
4104
+ throw error;
4105
+ }
4106
+ const { turn, firstWaitedTurn } = taken;
3935
4107
  try {
3936
4108
  if (options.requireProfile && !await exists(profileConfigPath(profile))) {
3937
4109
  await (0, import_promises.rmdir)(directory).catch(() => void 0);
@@ -3941,7 +4113,7 @@ async function acquireProfileLock(profile, options) {
3941
4113
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
3942
4114
  while (true) {
3943
4115
  const token = (0, import_node_crypto.randomUUID)();
3944
- if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
4116
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
3945
4117
  return {
3946
4118
  deletedWhileWaiting,
3947
4119
  turnSlot: turn.slot,
@@ -3949,17 +4121,19 @@ async function acquireProfileLock(profile, options) {
3949
4121
  try {
3950
4122
  await releaseProfileLock(profile, lockPath, token);
3951
4123
  } finally {
3952
- await releaseTurn(profile, turn);
4124
+ await releaseTurn(profile, turn, liveness);
4125
+ await closeAcquisitionLiveness();
3953
4126
  }
3954
4127
  }
3955
4128
  };
3956
4129
  }
3957
4130
  await signalTestLockContention(profile);
3958
- if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
4131
+ if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
3959
4132
  await waitOrTimeOut(profile, deadline);
3960
4133
  }
3961
4134
  } catch (error) {
3962
- await releaseTurn(profile, turn);
4135
+ await releaseTurn(profile, turn, liveness);
4136
+ await closeAcquisitionLiveness();
3963
4137
  throw error;
3964
4138
  }
3965
4139
  }
@@ -3973,12 +4147,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
3973
4147
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
3974
4148
  }
3975
4149
  var TURN = /^(?:0|[1-9][0-9]*)$/;
3976
- var _a2;
3977
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
4150
+ var _a3;
4151
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
3978
4152
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
3979
4153
  var SETTLE_RETRY_MS = 1e3;
3980
4154
  var SETTLE_RETRIES = 300;
3981
- async function acquireTurn(profile, directory, deadline) {
4155
+ async function acquireTurn(profile, directory, deadline, liveness) {
3982
4156
  let firstWaitedTurn;
3983
4157
  let problem;
3984
4158
  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.`;
@@ -4003,13 +4177,13 @@ async function acquireTurn(profile, directory, deadline) {
4003
4177
  }
4004
4178
  firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
4005
4179
  if (latest.held && latest.done) {
4006
- const turn = await takeTurn(profile, directory, latest);
4180
+ const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
4007
4181
  if (turn !== null) return { turn, firstWaitedTurn };
4008
4182
  await waitOrTimeOut(profile, deadline);
4009
4183
  } else if (latest.done) {
4010
4184
  problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
4011
4185
  await waitOrTimeOut(profile, deadline, problem);
4012
- } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
4186
+ } else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
4013
4187
  await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
4014
4188
  await settleAbandonedTurn(directory, latest);
4015
4189
  relinquishedTurns.delete(latest.token);
@@ -4054,6 +4228,7 @@ async function readTurn(directory, slot) {
4054
4228
  const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
4055
4229
  const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
4056
4230
  return {
4231
+ ...owner,
4057
4232
  slot,
4058
4233
  token: owner.token,
4059
4234
  pid: owner.pid,
@@ -4067,11 +4242,11 @@ async function isFinishedTurn(directory, slot, token) {
4067
4242
  if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
4068
4243
  return turn !== null && turn.token === token && turn.held && turn.done;
4069
4244
  }
4070
- async function takeTurn(profile, directory, latest) {
4245
+ async function takeTurn(profile, directory, latest, liveness) {
4071
4246
  const token = (0, import_node_crypto.randomUUID)();
4072
4247
  const slot = latest.slot + 1;
4073
4248
  const path = (0, import_node_path.join)(directory, String(slot));
4074
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
4249
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
4075
4250
  let published = false;
4076
4251
  try {
4077
4252
  published = await publishDirectory(
@@ -4125,7 +4300,7 @@ async function markTurn(path, token, kind) {
4125
4300
  }
4126
4301
  async function createTurnDirectory(directory) {
4127
4302
  const token = (0, import_node_crypto.randomUUID)();
4128
- const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
4303
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
4129
4304
  await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
4130
4305
  "0/owner.json": `${JSON.stringify(owner, null, 2)}
4131
4306
  `,
@@ -4149,31 +4324,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
4149
4324
  await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
4150
4325
  }
4151
4326
  }
4152
- async function releaseTurn(profile, turn) {
4327
+ async function releaseTurn(profile, turn, liveness) {
4153
4328
  try {
4154
4329
  await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
4155
4330
  } catch {
4156
4331
  settleOwnTurn(turn.directory, turn.slot, turn.token);
4157
4332
  return;
4158
4333
  }
4159
- await collectTurns(profile, turn).catch(() => void 0);
4334
+ await collectTurns(profile, turn, liveness).catch(() => void 0);
4160
4335
  }
4161
- function isAbandonedTurnEntry(name) {
4336
+ async function isAbandonedTurnEntry(directory, name, liveness) {
4162
4337
  if (name.startsWith(".trash-")) return true;
4163
4338
  const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
4164
- return writer !== void 0 && !isProcessAlive(Number(writer));
4339
+ if (writer === void 0) return false;
4340
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4341
+ return name.startsWith(".stage-") && await isStagedOwnerGone((0, import_node_path.join)(directory, name, "owner.json"), liveness);
4165
4342
  }
4166
- function isAbandonedTurnDirectoryStage(name) {
4343
+ async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
4167
4344
  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];
4168
- return writer !== void 0 && !isProcessAlive(Number(writer));
4345
+ if (writer === void 0) return false;
4346
+ if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
4347
+ return isStagedOwnerGone((0, import_node_path.join)(directory, name, "0", "owner.json"), liveness);
4348
+ }
4349
+ async function isStagedOwnerGone(recordPath, liveness) {
4350
+ const owner = await readJson(recordPath).catch(() => null);
4351
+ return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
4169
4352
  }
4170
- async function collectTurns(profile, turn) {
4353
+ async function collectTurns(profile, turn, liveness) {
4171
4354
  for (const [directory, abandoned] of [
4172
4355
  [turn.directory, isAbandonedTurnEntry],
4173
4356
  [(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
4174
4357
  ]) {
4175
- for (const name of (await (0, import_promises.readdir)(directory)).filter(abandoned)) {
4176
- await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
4358
+ for (const name of await (0, import_promises.readdir)(directory)) {
4359
+ if (await abandoned(directory, name, liveness)) await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
4177
4360
  }
4178
4361
  }
4179
4362
  const older = (await (0, import_promises.readdir)(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
@@ -4191,8 +4374,9 @@ async function collectTurns(profile, turn) {
4191
4374
  await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
4192
4375
  }
4193
4376
  }
4377
+ await sweepLiveness(liveness);
4194
4378
  }
4195
- async function publishProfileLock(profile, lockPath, token, requireProfile) {
4379
+ async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
4196
4380
  try {
4197
4381
  await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
4198
4382
  } catch (error) {
@@ -4205,7 +4389,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
4205
4389
  await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
4206
4390
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4207
4391
  const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
4208
- const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
4392
+ const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
4209
4393
  `;
4210
4394
  try {
4211
4395
  await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
@@ -4250,7 +4434,7 @@ async function signalTestLockContention(profile) {
4250
4434
  await (0, import_promises.writeFile)(signalPath, `${profile}
4251
4435
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
4252
4436
  }
4253
- async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4437
+ async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
4254
4438
  let aged;
4255
4439
  try {
4256
4440
  const { mtimeMs } = await (0, import_promises.stat)(lockPath);
@@ -4267,7 +4451,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4267
4451
  if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
4268
4452
  for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
4269
4453
  const claimPath = (0, import_node_path.join)(lockPath, name);
4270
- if (await hasLiveRecoveryOwner(claimPath)) {
4454
+ if (await hasLiveRecoveryOwner(claimPath, liveness)) {
4271
4455
  if (!await exists((0, import_node_path.join)(lockPath, "owner.json"))) await (0, import_promises.rename)(claimPath, (0, import_node_path.join)(lockPath, "owner.json"));
4272
4456
  return false;
4273
4457
  }
@@ -4283,7 +4467,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
4283
4467
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4284
4468
  return true;
4285
4469
  }
4286
- async function hasLiveRecoveryOwner(claimPath) {
4470
+ async function hasLiveRecoveryOwner(claimPath, liveness) {
4287
4471
  let contents;
4288
4472
  try {
4289
4473
  contents = await (0, import_promises.readFile)(claimPath, "utf8");
@@ -4297,8 +4481,8 @@ async function hasLiveRecoveryOwner(claimPath) {
4297
4481
  } catch {
4298
4482
  return false;
4299
4483
  }
4300
- const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
4301
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
4484
+ const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
4485
+ return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
4302
4486
  }
4303
4487
  function recoveryFenced(since, staleAfterMs) {
4304
4488
  return performance.now() - since >= staleAfterMs / 2;
@@ -4315,11 +4499,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
4315
4499
  if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
4316
4500
  }
4317
4501
  }
4318
- async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4502
+ async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
4319
4503
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
4320
4504
  const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
4321
- if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
4322
- if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
4505
+ if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
4506
+ if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
4323
4507
  await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
4324
4508
  const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
4325
4509
  let moved = false;
@@ -4355,7 +4539,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
4355
4539
  await removeAgedOwnerFiles(lockPath, staleAfterMs);
4356
4540
  return true;
4357
4541
  }
4358
- async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4542
+ async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
4359
4543
  let owner;
4360
4544
  try {
4361
4545
  owner = JSON.parse(observed);
@@ -4363,7 +4547,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
4363
4547
  owner = null;
4364
4548
  }
4365
4549
  if (typeof owner === "object" && owner !== null && "pid" in owner) {
4366
- return isStaleOwner(owner, staleAfterMs);
4550
+ return isStaleOwner(owner, liveness, staleAfterMs);
4367
4551
  }
4368
4552
  const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
4369
4553
  return Date.now() - modifiedAt >= staleAfterMs;
@@ -4385,14 +4569,14 @@ async function waitForTestBarrier(environmentName, profile) {
4385
4569
  }
4386
4570
  }
4387
4571
  }
4388
- function isStaleOwner(owner, staleAfterMs) {
4572
+ async function isStaleOwner(owner, liveness, staleAfterMs) {
4389
4573
  const now = Date.now();
4390
4574
  const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
4391
4575
  if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
4392
4576
  if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
4393
4577
  return false;
4394
4578
  }
4395
- return !isProcessAlive(owner.pid);
4579
+ return !await isRecordedProcessAlive(owner, liveness);
4396
4580
  }
4397
4581
  async function exists(path) {
4398
4582
  try {
@@ -4403,13 +4587,22 @@ async function exists(path) {
4403
4587
  throw error;
4404
4588
  }
4405
4589
  }
4406
- function isProcessAlive(pid) {
4590
+ async function isRecordedProcessAlive(owner, acquisition) {
4591
+ const pid = owner.pid;
4592
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
4407
4593
  try {
4408
4594
  process.kill(pid, 0);
4409
- return true;
4410
4595
  } catch (error) {
4411
4596
  return !isErrno(error, "ESRCH");
4412
4597
  }
4598
+ 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;
4599
+ const origin = processOrigin();
4600
+ if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
4601
+ if (owner.pidNamespace === origin.pidNamespace) {
4602
+ return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
4603
+ }
4604
+ const liveness = parseLiveness(owner.liveness);
4605
+ return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
4413
4606
  }
4414
4607
  function validateProfileName(profile) {
4415
4608
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
@@ -4509,6 +4702,8 @@ var STORED_GRANT_SKIP_REASONS = [
4509
4702
  "resources_mismatch",
4510
4703
  "unsupported",
4511
4704
  "activation_rejected",
4705
+ "activation_unavailable",
4706
+ "activation_unauthenticated",
4512
4707
  "invalid"
4513
4708
  ];
4514
4709
  var DELEGATION_CID_PATTERN = "^bafkr4i[a-z2-7]{52}$";
@@ -4742,6 +4937,9 @@ function skipped(reason) {
4742
4937
  }
4743
4938
  async function replayProfileDelegations(profile, node, options) {
4744
4939
  await prepareStoredDelegationReplay(profile, node, { host: options.host, migrate: options.migrate });
4940
+ return replayStoredDelegationRecords(profile, node, options);
4941
+ }
4942
+ async function replayStoredDelegationRecords(profile, node, options) {
4745
4943
  const installed = [];
4746
4944
  const warnings = [];
4747
4945
  const seen = /* @__PURE__ */ new Set();
@@ -4933,6 +5131,7 @@ function isRecord(value) {
4933
5131
  replayLimit,
4934
5132
  replayProfileDelegations,
4935
5133
  replayStoredDelegation,
5134
+ replayStoredDelegationRecords,
4936
5135
  storedDelegationForAnotherKey,
4937
5136
  storedDelegationKind,
4938
5137
  storedDelegationSource,