@ours.network/cli 2.0.3 → 2.0.4

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  bootWrapper,
3
3
  wrapperBooted
4
- } from "./chunk-ZCCCTR27.js";
4
+ } from "./chunk-TSYICL3I.js";
5
5
  import "./chunk-FXKFSNKS.js";
6
6
  export {
7
7
  bootWrapper,
@@ -99,7 +99,7 @@ function childEnvironment(selection) {
99
99
  async function serveDaemon(selection, managed = false) {
100
100
  if (selection.endpoint !== void 0) throw new Error("--endpoint selects a running daemon and cannot be used with daemon serve");
101
101
  Object.assign(process.env, childEnvironment(selection));
102
- const { startDaemon } = await import("./daemon-3C5SDTHJ.js");
102
+ const { startDaemon } = await import("./daemon-LTJHJDWZ.js");
103
103
  const handle = await startDaemon();
104
104
  if (managed) {
105
105
  const response = await fetch(`http://127.0.0.1:${handle.port}/state-dir`);
@@ -89,7 +89,7 @@ function createStartupProgressReporter(stateDir, opts = {}) {
89
89
  }
90
90
 
91
91
  // ../../src/runtime/env.ts
92
- var VERSION = true ? "3.0.3" : "0.0.0-dev";
92
+ var VERSION = true ? "3.0.4" : "0.0.0-dev";
93
93
  var CONFIG = loadConfig();
94
94
  var STATE_DIR = CONFIG.stateDir;
95
95
  var BROKER_URL = CONFIG.brokerUrl;
@@ -802,6 +802,7 @@ function setWrapper(w) {
802
802
  wrapper = w;
803
803
  }
804
804
  var identities = /* @__PURE__ */ new Map();
805
+ var quarantinedIdentities = /* @__PURE__ */ new Map();
805
806
  var registrar = null;
806
807
  var registrarAdBlob = null;
807
808
  function setRegistrar(id) {
@@ -894,13 +895,13 @@ var leases = /* @__PURE__ */ new Map();
894
895
  var tombstones = /* @__PURE__ */ new Set();
895
896
  var sessionHeaders = /* @__PURE__ */ new Map();
896
897
  var outboundRemovalInFlight = /* @__PURE__ */ new Set();
897
- function pidAlive(pid) {
898
- if (!Number.isInteger(pid) || pid <= 0) return false;
898
+ function pidDefinitelyDead(pid) {
899
+ if (!Number.isInteger(pid) || pid <= 1) return false;
899
900
  try {
900
901
  process.kill(pid, 0);
901
- return true;
902
+ return false;
902
903
  } catch (err) {
903
- return err.code === "EPERM";
904
+ return err.code === "ESRCH";
904
905
  }
905
906
  }
906
907
  function leaseByToken(token) {
@@ -1902,6 +1903,85 @@ async function pinRegistrar(id) {
1902
1903
  });
1903
1904
  }
1904
1905
 
1906
+ // ../../src/identity/migration.ts
1907
+ function wasPublished(id) {
1908
+ return Object.values(readBook()).some((entry) => entry.container_id === id.cid);
1909
+ }
1910
+ async function finishRestoredActivation(id) {
1911
+ wrapper.expose_packet(id.cid);
1912
+ identities.set(id.name, id);
1913
+ quarantinedIdentities.delete(id.name);
1914
+ log(`[${id.name}] EXPOSED (routing + broker registration) \u2014 hierarchy reconciled after restore`);
1915
+ try {
1916
+ await contactRestoreSweep(id);
1917
+ } catch (err) {
1918
+ log(`[${id.name}] post-activation contact restore sweep failed:`, String(err));
1919
+ }
1920
+ try {
1921
+ refreshUnread(id);
1922
+ } catch (err) {
1923
+ log(`[${id.name}] post-activation unread refresh failed:`, String(err));
1924
+ }
1925
+ return id;
1926
+ }
1927
+ function quarantine(id, status) {
1928
+ const exposeLocal = wasPublished(id);
1929
+ if (exposeLocal) unpublishFromBook(id);
1930
+ identities.delete(id.name);
1931
+ quarantinedIdentities.set(id.name, { identity: id, status, exposeLocal });
1932
+ log(`[${id.name}] QUARANTINED (${status}) \u2014 management-only, unexposed and unbindable`);
1933
+ return id;
1934
+ }
1935
+ async function reconcileRestoredIdentity(id) {
1936
+ if (rootName === id.name) return finishRestoredActivation(id);
1937
+ const hostRoot = rootName ? identities.get(rootName) : void 0;
1938
+ let info;
1939
+ try {
1940
+ info = describeIdentity(id);
1941
+ } catch (err) {
1942
+ log(`[${id.name}] hierarchy classification failed:`, String(err));
1943
+ return quarantine(id, "migration-failed");
1944
+ }
1945
+ if (hostRoot) {
1946
+ if (info.hasCert && info.roleId !== "" && info.rootCid !== hostRoot.cid) {
1947
+ log(`[${id.name}] preserved imported delegation from root ${info.rootCid.slice(0, 12)}\u2026`);
1948
+ return finishRestoredActivation(id);
1949
+ }
1950
+ try {
1951
+ await delegateRole(hostRoot, id);
1952
+ return finishRestoredActivation(id);
1953
+ } catch (err) {
1954
+ log(`[${id.name}] root delegation during restore failed:`, String(err));
1955
+ return quarantine(id, "migration-failed");
1956
+ }
1957
+ }
1958
+ if (info.hasCert && info.roleId !== "") return finishRestoredActivation(id);
1959
+ return quarantine(id, "awaiting-root");
1960
+ }
1961
+ async function adoptQuarantinedIdentities(root) {
1962
+ const adopted = [];
1963
+ const failed = [];
1964
+ for (const [name, held] of [...quarantinedIdentities]) {
1965
+ try {
1966
+ await delegateRole(root, held.identity);
1967
+ if (held.exposeLocal) await publishToBook(held.identity);
1968
+ await finishRestoredActivation(held.identity);
1969
+ quarantinedIdentities.delete(name);
1970
+ adopted.push(name);
1971
+ } catch (err) {
1972
+ try {
1973
+ unpublishFromBook(held.identity);
1974
+ } catch {
1975
+ }
1976
+ held.status = "migration-failed";
1977
+ quarantinedIdentities.set(name, held);
1978
+ log(`[${name}] quarantine adoption failed under root "${root.name}":`, String(err));
1979
+ failed.push(name);
1980
+ }
1981
+ }
1982
+ return { adopted, failed };
1983
+ }
1984
+
1905
1985
  // ../../src/identity/provision.ts
1906
1986
  function createPacket(name, seed, dir, track = true, signingSecret, deferExposure = false) {
1907
1987
  const config = new PacketWrapperConfigurator();
@@ -2086,12 +2166,9 @@ async function restoreIdentity(name) {
2086
2166
  } catch (err) {
2087
2167
  tearDownUnexposed("history_open", "FAILED (SQLite history unavailable)", err);
2088
2168
  }
2089
- wrapper.expose_packet(id.cid);
2090
- identities.set(name, id);
2091
- log(`[${name}] EXPOSED (routing + broker registration) \u2014 import phase complete`);
2092
- await contactRestoreSweep(id);
2093
- refreshUnread(id);
2094
- return id;
2169
+ const restoredTemp = readTempMetaFile(id.dir);
2170
+ if (restoredTemp) id.temp = restoredTemp;
2171
+ return reconcileRestoredIdentity(id);
2095
2172
  }
2096
2173
 
2097
2174
  // ../../src/state.ts
@@ -2345,8 +2422,11 @@ async function establishRoot(id) {
2345
2422
  return { adopted, failed };
2346
2423
  }
2347
2424
 
2348
- // ../../src/identity/lifecycle.ts
2425
+ // ../../src/identity/reaper.ts
2349
2426
  import { join as join9 } from "node:path";
2427
+ import * as fs12 from "node:fs";
2428
+
2429
+ // ../../src/identity/lifecycle.ts
2350
2430
  import * as fs11 from "node:fs";
2351
2431
 
2352
2432
  // ../../src/render/adapt-to-json.ts
@@ -2724,31 +2804,46 @@ function closeTemporaryIdentity(id, cause) {
2724
2804
  })();
2725
2805
  return t.closing;
2726
2806
  }
2727
- function sweepStaleTempIdentities() {
2728
- for (const id of [...identities.values()]) {
2807
+
2808
+ // ../../src/identity/reaper.ts
2809
+ function sessionReaperIntervalMs(raw = process.env.OURS_SESSION_REAPER_INTERVAL_MS) {
2810
+ if (raw === void 0 || raw.trim() === "") return 5e3;
2811
+ const parsed = Number(raw);
2812
+ if (!Number.isFinite(parsed)) return 5e3;
2813
+ return Math.max(100, Math.trunc(parsed));
2814
+ }
2815
+ async function reapStaleTemporaryIdentities() {
2816
+ const all = [
2817
+ ...identities.values(),
2818
+ ...[...quarantinedIdentities.values()].map((held) => held.identity)
2819
+ ];
2820
+ const closes = [];
2821
+ for (const id of all) {
2729
2822
  const t = id.temp;
2730
- if (!t || t.closing) continue;
2731
- if (pidAlive(t.owner.pid)) continue;
2823
+ if (!t || t.closing || !pidDefinitelyDead(t.owner.pid)) continue;
2732
2824
  const lease = leases.get(id.name);
2733
- if (lease && pidAlive(lease.pid)) continue;
2734
- void closeTemporaryIdentity(id, `stale lease \u2014 owner pid ${t.owner.pid} is dead`).catch(
2735
- (err) => log(`[${id.name}] stale-temp reclaim failed:`, String(err))
2825
+ if (lease && !pidDefinitelyDead(lease.pid)) continue;
2826
+ closes.push(
2827
+ closeTemporaryIdentity(id, `stale lease \u2014 owner pid ${t.owner.pid} is dead`).then(() => {
2828
+ quarantinedIdentities.delete(id.name);
2829
+ }).catch((err) => log(`[${id.name}] stale-temp reclaim failed:`, String(err)))
2736
2830
  );
2737
2831
  }
2832
+ await Promise.all(closes);
2738
2833
  }
2739
- function sweepOrphanTempDirs() {
2740
- if (!fs11.existsSync(STATE_DIR)) return;
2741
- for (const d of fs11.readdirSync(STATE_DIR, { withFileTypes: true })) {
2742
- if (!d.isDirectory() || identities.has(d.name)) continue;
2834
+ async function reapOrphanTemporaryDirectories() {
2835
+ if (!fs12.existsSync(STATE_DIR)) return;
2836
+ for (const d of fs12.readdirSync(STATE_DIR, { withFileTypes: true })) {
2837
+ if (!d.isDirectory() || identities.has(d.name) || quarantinedIdentities.has(d.name)) continue;
2743
2838
  const dir = join9(STATE_DIR, d.name);
2744
2839
  const meta = readTempMetaFile(dir);
2745
- if (!meta || pidAlive(meta.owner.pid)) continue;
2840
+ if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
2746
2841
  try {
2747
- fs11.rmSync(dir, { recursive: true, force: true });
2842
+ fs12.rmSync(dir, { recursive: true, force: true });
2748
2843
  reservedNames.delete(d.name);
2749
- log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} dead, no live packet)`);
2844
+ log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
2750
2845
  } catch (err) {
2751
- log(`[${d.name}] failed to remove orphaned temporary-identity dir:`, String(err));
2846
+ throw new Error(`[${d.name}] failed to remove orphaned temporary-identity dir: ${String(err)}`);
2752
2847
  }
2753
2848
  }
2754
2849
  }
@@ -2859,7 +2954,9 @@ async function bootWrapper() {
2859
2954
  log("failed to start the contact-book registrar (local contact book disabled):", String(err));
2860
2955
  }
2861
2956
  tightenIdentityPerms();
2862
- const names = listPersistedNames();
2957
+ setRootName(readRootMarker());
2958
+ const persistedNames = listPersistedNames();
2959
+ const names = rootName && persistedNames.includes(rootName) ? [rootName, ...persistedNames.filter((name) => name !== rootName)] : persistedNames;
2863
2960
  const fakeRestoreCount = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_COUNT || "") || 0);
2864
2961
  const fakeRestoreMs = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_MS || "") || 0);
2865
2962
  const restoreTotal = names.length === 0 && fakeRestoreCount > 0 ? fakeRestoreCount : names.length;
@@ -2945,8 +3042,8 @@ async function bootWrapper() {
2945
3042
  if (refreshedRoles.length > 0) log(`refreshed ${refreshedRoles.length} role delegation cert(s) against the live AD on boot`);
2946
3043
  }
2947
3044
  persistBindings();
2948
- sweepOrphanTempDirs();
2949
- sweepStaleTempIdentities();
3045
+ await reapOrphanTemporaryDirectories();
3046
+ await reapStaleTemporaryIdentities();
2950
3047
  }
2951
3048
 
2952
3049
  export {
@@ -2962,6 +3059,8 @@ export {
2962
3059
  requireAuth,
2963
3060
  validateName,
2964
3061
  consumeOutboundHistoryFailure,
3062
+ openHistory,
3063
+ closeHistory,
2965
3064
  listIncomingMessages,
2966
3065
  takeUnreadMessages,
2967
3066
  listIncomingFiles,
@@ -2972,8 +3071,12 @@ export {
2972
3071
  listFileHistory,
2973
3072
  getFileHistoryItem,
2974
3073
  FILE_SELECTION_CAP,
3074
+ wrapper,
2975
3075
  identities,
3076
+ quarantinedIdentities,
2976
3077
  registrar,
3078
+ identityDir,
3079
+ keyPath,
2977
3080
  hashLeaseToken,
2978
3081
  writeTempMetaFile,
2979
3082
  isSelectableWireId,
@@ -2982,7 +3085,7 @@ export {
2982
3085
  tombstones,
2983
3086
  sessionHeaders,
2984
3087
  outboundRemovalInFlight,
2985
- pidAlive,
3088
+ pidDefinitelyDead,
2986
3089
  leaseByToken,
2987
3090
  persistBindings,
2988
3091
  resolveBound,
@@ -3000,11 +3103,16 @@ export {
3000
3103
  e2eRecoverySweep,
3001
3104
  readBook,
3002
3105
  exportAdBlob,
3106
+ exportSigningSecret,
3003
3107
  publishToBook,
3004
3108
  unpublishFromBook,
3109
+ pinRegistrar,
3110
+ adoptQuarantinedIdentities,
3111
+ createPacket,
3005
3112
  reservedNames,
3006
3113
  provisionIdentity,
3007
3114
  saveState,
3115
+ saveStateFailClosed,
3008
3116
  withScope,
3009
3117
  withScopeAsync,
3010
3118
  readonlyTx,
@@ -3025,7 +3133,8 @@ export {
3025
3133
  decodeWireBin,
3026
3134
  deleteIdentityCompletely,
3027
3135
  closeTemporaryIdentity,
3028
- sweepStaleTempIdentities,
3136
+ sessionReaperIntervalMs,
3137
+ reapStaleTemporaryIdentities,
3029
3138
  envelopeDispatch,
3030
3139
  PROTOCOL_VERSION,
3031
3140
  clusterSweep,
@@ -204,7 +204,7 @@ async function releaseHeld(lock) {
204
204
  async function bootKernel() {
205
205
  const { lock, acquired } = await ensureLock();
206
206
  try {
207
- const { bootWrapper } = await import("./boot-BUGOBQGZ.js");
207
+ const { bootWrapper } = await import("./boot-HNJ7ZR5L.js");
208
208
  await bootWrapper();
209
209
  } catch (error) {
210
210
  if (acquired) await releaseHeld(lock);
@@ -214,7 +214,7 @@ async function bootKernel() {
214
214
  async function startDaemon(opts = {}) {
215
215
  const { lock, acquired } = await ensureLock();
216
216
  try {
217
- const runtime = await import("./server-TVV2EZZ6.js");
217
+ const runtime = await import("./server-35QOYLY4.js");
218
218
  const { onRuntimeLoaded, ...daemonOptions } = opts;
219
219
  onRuntimeLoaded?.();
220
220
  const handle = await runtime.startDaemon({
package/dist/lifecycle.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  startDaemonManaged,
11
11
  stopDaemonManaged,
12
12
  writeManagedRecord
13
- } from "./chunk-UIWEBE5K.js";
13
+ } from "./chunk-DZH2YEXT.js";
14
14
  import "./chunk-6K2JBKHI.js";
15
15
  export {
16
16
  DAEMON_LOG_FILE,
package/dist/main.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  serveDaemon,
45
45
  startDaemonManaged,
46
46
  stopDaemonManaged
47
- } from "./chunk-UIWEBE5K.js";
47
+ } from "./chunk-DZH2YEXT.js";
48
48
  import {
49
49
  attachClient,
50
50
  defaultConfigPath,
@@ -55,7 +55,7 @@ import {
55
55
  import { existsSync, readFileSync } from "node:fs";
56
56
  import { homedir } from "node:os";
57
57
  import { join, resolve } from "node:path";
58
- var CLI_VERSION = false ? "0.0.0-dev" : "2.0.3";
58
+ var CLI_VERSION = false ? "0.0.0-dev" : "2.0.4";
59
59
  var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
60
60
  var COMMON_BOOLEANS = /* @__PURE__ */ new Set(["--json", "--yes", "--help"]);
61
61
  var DAEMON_READINESS_TIMEOUT_ENV = "OURS_DAEMON_READINESS_TIMEOUT_MS";
@@ -8,15 +8,18 @@ import {
8
8
  PROTOCOL_VERSION,
9
9
  STATE_DIR,
10
10
  VERSION,
11
+ adoptQuarantinedIdentities,
11
12
  appendNotifyLog,
12
13
  bindSession,
13
14
  bootWrapper,
14
15
  capabilityReconcileSweep,
15
16
  clearNotifyHook,
17
+ closeHistory,
16
18
  closeTemporaryIdentity,
17
19
  clusterSweep,
18
20
  consumeOutboundHistoryFailure,
19
21
  contactRestoreSweep,
22
+ createPacket,
20
23
  decodeWireBin,
21
24
  delegateRole,
22
25
  deleteIdentityCompletely,
@@ -26,13 +29,16 @@ import {
26
29
  envelopeDispatch,
27
30
  establishRoot,
28
31
  exportAdBlob,
32
+ exportSigningSecret,
29
33
  findIdentityFile,
30
34
  getFileHistoryItem,
31
35
  getMessageHistoryItem,
32
36
  getMessageHistorySummary,
33
37
  hashLeaseToken,
34
38
  identities,
39
+ identityDir,
35
40
  isSelectableWireId,
41
+ keyPath,
36
42
  leaseByToken,
37
43
  leases,
38
44
  listFileHistory,
@@ -41,13 +47,17 @@ import {
41
47
  listMessageHistory,
42
48
  log,
43
49
  mutatingTx,
50
+ openHistory,
44
51
  outboundRemovalInFlight,
45
52
  persistBindings,
46
- pidAlive,
53
+ pidDefinitelyDead,
54
+ pinRegistrar,
47
55
  provisionIdentity,
48
56
  publishToBook,
57
+ quarantinedIdentities,
49
58
  readBook,
50
59
  readonlyTx,
60
+ reapStaleTemporaryIdentities,
51
61
  refreshUnread,
52
62
  registrar,
53
63
  renderContactRoots,
@@ -60,14 +70,15 @@ import {
60
70
  resolveBound,
61
71
  rootName,
62
72
  saveState,
73
+ saveStateFailClosed,
63
74
  scheduleCapabilityReconcile,
64
75
  serveNotifications,
65
76
  sessionHeaders,
77
+ sessionReaperIntervalMs,
66
78
  setNotifyHook,
67
79
  startupProgress,
68
80
  structuredVoiceOutcome,
69
81
  sttStatus,
70
- sweepStaleTempIdentities,
71
82
  takeUnreadFiles,
72
83
  takeUnreadMessages,
73
84
  tombstones,
@@ -78,8 +89,9 @@ import {
78
89
  voiceDeliveryLine,
79
90
  withScope,
80
91
  withScopeAsync,
92
+ wrapper,
81
93
  writeTempMetaFile
82
- } from "./chunk-ZCCCTR27.js";
94
+ } from "./chunk-TSYICL3I.js";
83
95
  import {
84
96
  buildIdentityFile,
85
97
  writeIdentityFile
@@ -88,11 +100,157 @@ import {
88
100
  // ../../src/http/server.ts
89
101
  import { createServer as createHttpServer } from "node:http";
90
102
  import { randomUUID } from "node:crypto";
91
- import * as fs2 from "node:fs";
103
+ import * as fs3 from "node:fs";
92
104
 
93
105
  // ../../src/protocol.ts
94
106
  var OURS_COMPAT_VERSION = 1;
95
107
 
108
+ // ../../src/errors.ts
109
+ var OursError = class extends Error {
110
+ // `details` is PURELY ADDITIVE and optional: every existing construction and
111
+ // every `e.code` check keeps working untouched. It exists because a refusal a
112
+ // caller is expected to ACT on (trim N bytes, shorten the filename) cannot be
113
+ // delivered as prose alone — see errFileTooLarge.
114
+ constructor(code, message, details) {
115
+ super(message);
116
+ this.code = code;
117
+ this.details = details;
118
+ this.name = "OursError";
119
+ }
120
+ };
121
+ var errNameTaken = (tool, name) => new OursError("NAME_TAKEN", `${tool} failed: an identity named "${name}" already exists.`);
122
+ var errNoLeaseHeaders = () => new OursError(
123
+ "NO_LEASE_HEADERS",
124
+ "create_temporary_identity failed: this client is not connected through the ours connector (no lease token / client pid headers), so there is no session lease to own the identity. Launch ours via the connector (`ours-mcp proxy`)."
125
+ );
126
+ var errRandomNameExhausted = () => new OursError(
127
+ "RANDOM_NAME_EXHAUSTED",
128
+ "create_temporary_identity failed: could not pick a free random name after 5 attempts \u2014 retry."
129
+ );
130
+ var errRootExists = (rootName2) => new OursError(
131
+ "ROOT_EXISTS",
132
+ `create_root_identity failed: a root identity already exists ("${rootName2}") \u2014 one root per host. Nothing to do.`
133
+ );
134
+ var errNotBoundNoName = () => new OursError("NOT_BOUND_NO_NAME", "close_temporary_identity failed: no identity is bound to this session and no name was given.");
135
+ var errNotTemporary = (name) => new OursError(
136
+ "NOT_TEMPORARY",
137
+ `close_temporary_identity failed: "${name}" is a permanent identity. Use remove_identity if you really mean to delete it.`
138
+ );
139
+ var errTempOwnedElsewhereClose = (name, pid) => new OursError(
140
+ "TEMP_OWNED_ELSEWHERE",
141
+ `close_temporary_identity failed: "${name}" is owned by another LIVE session (owner pid ${pid}) \u2014 one session cannot delete another's temporary identity.`
142
+ );
143
+ var errTempClosedWithError = (name, deleteError, remote) => new OursError("DELETE_PARTIAL", `Temporary identity "${name}" closed with an error: ${deleteError}. ${remote}`);
144
+ var errPathNotAbsolute = (path) => new OursError("PATH_NOT_ABSOLUTE", `define_local_identity_file failed: path must be absolute (got "${path}").`);
145
+ var errNoSuchIdentity = (name) => new OursError("NO_SUCH_IDENTITY", `choose_identity failed: no identity named "${name}". Create it with create_identity.`);
146
+ var errNoLeaseToken = () => new OursError(
147
+ "NO_LEASE_TOKEN",
148
+ "choose_identity failed: this client is not connected through the ours connector (no lease token header). Launch ours via the connector (`ours-mcp proxy`)."
149
+ );
150
+ var errTempClosingChoose = (name) => new OursError("TEMP_CLOSING", `choose_identity failed: temporary identity "${name}" is closing \u2014 its state is being deleted.`);
151
+ var errTempOwnedLive = (name, pid) => new OursError(
152
+ "TEMP_OWNED_ELSEWHERE",
153
+ `choose_identity failed: "${name}" is a TEMPORARY identity owned by another live session (owner pid ${pid}). Ownership is exclusive and cannot be overridden \u2014 not even with force. Create your own with create_temporary_identity.`
154
+ );
155
+ var errTempStale = (name, pid) => new OursError(
156
+ "TEMP_STALE",
157
+ `choose_identity failed: temporary identity "${name}" is STALE \u2014 its owning session (pid ${pid}) is gone and it is pending automatic cleanup. It cannot be adopted by another session; use close_temporary_identity to reclaim (clean it up) now.`
158
+ );
159
+ var errBoundElsewhere = (name) => new OursError(
160
+ "BOUND_ELSEWHERE",
161
+ `choose_identity declined: "${name}" is currently bound to another live session. Do not retry with force=true on your own \u2014 tell the user it is in use elsewhere and ask whether to forcibly rebind it here; only retry with force=true after they explicitly confirm.`
162
+ );
163
+ var errTempOwnedElsewhereRemove = (name, pid) => new OursError(
164
+ "TEMP_OWNED_ELSEWHERE",
165
+ `remove_identity failed: "${name}" is a TEMPORARY identity owned by another LIVE session (owner pid ${pid}) \u2014 one session cannot delete another's temporary identity.`
166
+ );
167
+ var errIdentityClosedWithError = (name, deleteError, remote) => new OursError("DELETE_PARTIAL", `Temporary identity "${name}" closed with an error: ${deleteError}.${remote}`);
168
+ var errRootHasRoles = (name, roles) => new OursError(
169
+ "ROOT_HAS_ROLES",
170
+ `remove_identity failed: "${name}" is the root identity and still has ${roles.length} role(s): ${roles.join(", ")}. Remove the roles first.`
171
+ );
172
+ var errIdentityPartiallyRemoved = (name, fail) => new OursError("DELETE_PARTIAL", `Identity "${name}" removed from memory, but ${fail}`);
173
+ var errPublicInviteNamed = () => new OursError(
174
+ "PUBLIC_INVITE_NAMED",
175
+ "generate_invite failed: a public invite cannot pre-assign a contact name \u2014 every redeemer would be registered under it. Omit `name` for a public invite."
176
+ );
177
+ var errNoPolicyArgs = () => new OursError("NO_POLICY_ARGS", "set_local_book_policy: pass expose and/or auto_accept.");
178
+ var errSendFileArgs = () => new OursError("SEND_ARGS", "send_file: provide exactly one of `path` or `data_base64`.");
179
+ var errSendFileFilenameRequired = () => new OursError("SEND_ARGS", "send_file: `filename` is required with `data_base64`.");
180
+ var errFileUnreadable = (detail) => new OursError("FILE_UNREADABLE", `send_file: cannot read file: ${detail}`);
181
+ var errSendFileUploadArgs = () => new OursError("SEND_ARGS", "send_file: provide exactly one of `path`, `data_base64` or `upload_id`.");
182
+ var errUploadNotFound = (uploadId) => new OursError(
183
+ "FILE_UNREADABLE",
184
+ `send_file: no staged upload "${uploadId}" for the bound identity. Upload the bytes with POST /files/upload first. A staged upload is consumed by the first send that names it \u2014 including a send that failed \u2014 so a retry needs a fresh upload.`
185
+ );
186
+ var errFileTooLarge = (i) => new OursError(
187
+ "FILE_TOO_LARGE",
188
+ `send_file: "${i.filename}" is ${i.bytes} bytes. This send's envelope would serialize to ${i.envelopeBytes} bytes, over the transport's ${i.limit}-byte budget \u2014 the largest payload it can carry with this filename, MIME and reply reference is ${i.maxPayloadBytes} bytes. Nothing was sent.`,
189
+ i
190
+ );
191
+ var errNoRoot = () => new OursError("NO_ROOT", "No root identity exists on this host \u2014 create one with create_root_identity first.");
192
+ var errInvalidSelection = (cap) => new OursError("INVALID_SELECTION", `get_files failed: wire_ids must contain 1-${cap} items.`);
193
+ var errMalformedId = () => new OursError("MALFORMED_ID", "get_files failed: every selected wire_id must be exactly 64 hexadecimal characters.");
194
+ var errDuplicateId = () => new OursError("DUPLICATE_ID", "get_files failed: wire_ids must not contain duplicates.");
195
+ var errUnknownOrStaleId = () => new OursError(
196
+ "UNKNOWN_OR_STALE_ID",
197
+ "get_files failed: one or more selected wire_ids is unknown, stale, or no longer unread; no files were retrieved."
198
+ );
199
+ var errTool = (tool, detail, code = "TX_FAILED") => new OursError(code, `${tool} failed: ${detail}`);
200
+
201
+ // ../../src/identity/release.ts
202
+ async function releaseLeaseByToken(token) {
203
+ const tokenHash = hashLeaseToken(token);
204
+ const released = [];
205
+ for (const [name, lease] of [...leases]) {
206
+ if (lease.token !== token) continue;
207
+ leases.delete(name);
208
+ released.push(name);
209
+ }
210
+ const candidates = [
211
+ ...identities.values(),
212
+ ...[...quarantinedIdentities.values()].map((held) => held.identity)
213
+ ];
214
+ const owned = candidates.filter(
215
+ (id) => id.temp?.owner.tokenHash === tokenHash
216
+ );
217
+ const settled = await Promise.allSettled(
218
+ owned.map((id) => closeTemporaryIdentity(id, "owning session released its lease"))
219
+ );
220
+ const closed = [];
221
+ let attempted = 0;
222
+ let notified = 0;
223
+ let failed = 0;
224
+ const localFailures = [];
225
+ for (let i = 0; i < settled.length; i++) {
226
+ const item = settled[i];
227
+ const name = owned[i].name;
228
+ if (item.status === "rejected") {
229
+ localFailures.push({ name, error: String(item.reason) });
230
+ continue;
231
+ }
232
+ const result2 = item.value;
233
+ quarantinedIdentities.delete(name);
234
+ closed.push(name);
235
+ attempted += result2.attempted;
236
+ notified += result2.notified;
237
+ failed += result2.failed;
238
+ if (result2.deleteError) localFailures.push({ name, error: result2.deleteError });
239
+ }
240
+ tombstones.delete(token);
241
+ persistBindings();
242
+ log(`lease released by token \u2026${token.slice(-6)} (${closed.length} temporary identity close(s) awaited)`);
243
+ const result = { released, closed, attempted, notified, failed };
244
+ if (localFailures.length > 0) {
245
+ throw new OursError(
246
+ "DELETE_PARTIAL",
247
+ `Lease release completed with ${localFailures.length} local cleanup failure(s).`,
248
+ { result, localFailures }
249
+ );
250
+ }
251
+ return result;
252
+ }
253
+
96
254
  // ../../src/gc.ts
97
255
  var gcTimer = null;
98
256
  var gcRunning = false;
@@ -350,101 +508,86 @@ function checkFileEnvelope(i) {
350
508
  };
351
509
  }
352
510
 
353
- // ../../src/errors.ts
354
- var OursError = class extends Error {
355
- // `details` is PURELY ADDITIVE and optional: every existing construction and
356
- // every `e.code` check keeps working untouched. It exists because a refusal a
357
- // caller is expected to ACT on (trim N bytes, shorten the filename) cannot be
358
- // delivered as prose alone — see errFileTooLarge.
359
- constructor(code, message, details) {
360
- super(message);
361
- this.code = code;
362
- this.details = details;
363
- this.name = "OursError";
511
+ // ../../src/api/identity.ts
512
+ import { randomBytes as randomBytes2 } from "node:crypto";
513
+ import { isAbsolute } from "node:path";
514
+
515
+ // ../../src/identity/staged.ts
516
+ import * as fs2 from "node:fs";
517
+ import { randomBytes } from "node:crypto";
518
+ async function provisionIdentityUnexposed(name, opts) {
519
+ if (reservedNames.has(name) || identities.has(name)) {
520
+ throw new Error(`identity name "${name}" is reserved or already present`);
364
521
  }
365
- };
366
- var errNameTaken = (tool, name) => new OursError("NAME_TAKEN", `${tool} failed: an identity named "${name}" already exists.`);
367
- var errNoLeaseHeaders = () => new OursError(
368
- "NO_LEASE_HEADERS",
369
- "create_temporary_identity failed: this client is not connected through the ours connector (no lease token / client pid headers), so there is no session lease to own the identity. Launch ours via the connector (`ours-mcp proxy`)."
370
- );
371
- var errRandomNameExhausted = () => new OursError(
372
- "RANDOM_NAME_EXHAUSTED",
373
- "create_temporary_identity failed: could not pick a free random name after 5 attempts \u2014 retry."
374
- );
375
- var errRootExists = (rootName2) => new OursError(
376
- "ROOT_EXISTS",
377
- `create_root_identity failed: a root identity already exists ("${rootName2}") \u2014 one root per host. Nothing to do.`
378
- );
379
- var errNotBoundNoName = () => new OursError("NOT_BOUND_NO_NAME", "close_temporary_identity failed: no identity is bound to this session and no name was given.");
380
- var errNotTemporary = (name) => new OursError(
381
- "NOT_TEMPORARY",
382
- `close_temporary_identity failed: "${name}" is a permanent identity. Use remove_identity if you really mean to delete it.`
383
- );
384
- var errTempOwnedElsewhereClose = (name, pid) => new OursError(
385
- "TEMP_OWNED_ELSEWHERE",
386
- `close_temporary_identity failed: "${name}" is owned by another LIVE session (owner pid ${pid}) \u2014 one session cannot delete another's temporary identity.`
387
- );
388
- var errTempClosedWithError = (name, deleteError, remote) => new OursError("DELETE_PARTIAL", `Temporary identity "${name}" closed with an error: ${deleteError}. ${remote}`);
389
- var errPathNotAbsolute = (path) => new OursError("PATH_NOT_ABSOLUTE", `define_local_identity_file failed: path must be absolute (got "${path}").`);
390
- var errNoSuchIdentity = (name) => new OursError("NO_SUCH_IDENTITY", `choose_identity failed: no identity named "${name}". Create it with create_identity.`);
391
- var errNoLeaseToken = () => new OursError(
392
- "NO_LEASE_TOKEN",
393
- "choose_identity failed: this client is not connected through the ours connector (no lease token header). Launch ours via the connector (`ours-mcp proxy`)."
394
- );
395
- var errTempClosingChoose = (name) => new OursError("TEMP_CLOSING", `choose_identity failed: temporary identity "${name}" is closing \u2014 its state is being deleted.`);
396
- var errTempOwnedLive = (name, pid) => new OursError(
397
- "TEMP_OWNED_ELSEWHERE",
398
- `choose_identity failed: "${name}" is a TEMPORARY identity owned by another live session (owner pid ${pid}). Ownership is exclusive and cannot be overridden \u2014 not even with force. Create your own with create_temporary_identity.`
399
- );
400
- var errTempStale = (name, pid) => new OursError(
401
- "TEMP_STALE",
402
- `choose_identity failed: temporary identity "${name}" is STALE \u2014 its owning session (pid ${pid}) is gone and it is pending automatic cleanup. It cannot be adopted by another session; use close_temporary_identity to reclaim (clean it up) now.`
403
- );
404
- var errBoundElsewhere = (name) => new OursError(
405
- "BOUND_ELSEWHERE",
406
- `choose_identity declined: "${name}" is currently bound to another live session. Do not retry with force=true on your own \u2014 tell the user it is in use elsewhere and ask whether to forcibly rebind it here; only retry with force=true after they explicitly confirm.`
407
- );
408
- var errTempOwnedElsewhereRemove = (name, pid) => new OursError(
409
- "TEMP_OWNED_ELSEWHERE",
410
- `remove_identity failed: "${name}" is a TEMPORARY identity owned by another LIVE session (owner pid ${pid}) \u2014 one session cannot delete another's temporary identity.`
411
- );
412
- var errIdentityClosedWithError = (name, deleteError, remote) => new OursError("DELETE_PARTIAL", `Temporary identity "${name}" closed with an error: ${deleteError}.${remote}`);
413
- var errRootHasRoles = (name, roles) => new OursError(
414
- "ROOT_HAS_ROLES",
415
- `remove_identity failed: "${name}" is the root identity and still has ${roles.length} role(s): ${roles.join(", ")}. Remove the roles first.`
416
- );
417
- var errIdentityPartiallyRemoved = (name, fail) => new OursError("DELETE_PARTIAL", `Identity "${name}" removed from memory, but ${fail}`);
418
- var errPublicInviteNamed = () => new OursError(
419
- "PUBLIC_INVITE_NAMED",
420
- "generate_invite failed: a public invite cannot pre-assign a contact name \u2014 every redeemer would be registered under it. Omit `name` for a public invite."
421
- );
422
- var errNoPolicyArgs = () => new OursError("NO_POLICY_ARGS", "set_local_book_policy: pass expose and/or auto_accept.");
423
- var errSendFileArgs = () => new OursError("SEND_ARGS", "send_file: provide exactly one of `path` or `data_base64`.");
424
- var errSendFileFilenameRequired = () => new OursError("SEND_ARGS", "send_file: `filename` is required with `data_base64`.");
425
- var errFileUnreadable = (detail) => new OursError("FILE_UNREADABLE", `send_file: cannot read file: ${detail}`);
426
- var errSendFileUploadArgs = () => new OursError("SEND_ARGS", "send_file: provide exactly one of `path`, `data_base64` or `upload_id`.");
427
- var errUploadNotFound = (uploadId) => new OursError(
428
- "FILE_UNREADABLE",
429
- `send_file: no staged upload "${uploadId}" for the bound identity. Upload the bytes with POST /files/upload first. A staged upload is consumed by the first send that names it \u2014 including a send that failed \u2014 so a retry needs a fresh upload.`
430
- );
431
- var errFileTooLarge = (i) => new OursError(
432
- "FILE_TOO_LARGE",
433
- `send_file: "${i.filename}" is ${i.bytes} bytes. This send's envelope would serialize to ${i.envelopeBytes} bytes, over the transport's ${i.limit}-byte budget \u2014 the largest payload it can carry with this filename, MIME and reply reference is ${i.maxPayloadBytes} bytes. Nothing was sent.`,
434
- i
435
- );
436
- var errInvalidSelection = (cap) => new OursError("INVALID_SELECTION", `get_files failed: wire_ids must contain 1-${cap} items.`);
437
- var errMalformedId = () => new OursError("MALFORMED_ID", "get_files failed: every selected wire_id must be exactly 64 hexadecimal characters.");
438
- var errDuplicateId = () => new OursError("DUPLICATE_ID", "get_files failed: wire_ids must not contain duplicates.");
439
- var errUnknownOrStaleId = () => new OursError(
440
- "UNKNOWN_OR_STALE_ID",
441
- "get_files failed: one or more selected wire_ids is unknown, stale, or no longer unread; no files were retrieved."
442
- );
443
- var errTool = (tool, detail, code = "TX_FAILED") => new OursError(code, `${tool} failed: ${detail}`);
522
+ reservedNames.add(name);
523
+ const dir = identityDir(name);
524
+ let id;
525
+ try {
526
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
527
+ let tempMeta;
528
+ if (opts.temp) {
529
+ tempMeta = {
530
+ owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid },
531
+ createdAt: Date.now()
532
+ };
533
+ writeTempMetaFile(dir, tempMeta);
534
+ }
535
+ id = await createPacket(name, randomBytes(24).toString("hex"), dir, false, void 0, true);
536
+ openHistory(id);
537
+ if (tempMeta) id.temp = tempMeta;
538
+ fs2.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
539
+ await withScopeAsync(async (lt) => {
540
+ await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
541
+ });
542
+ await pinRegistrar(id);
543
+ if (!opts.localAutoAccept) {
544
+ await withScopeAsync(async (lt) => {
545
+ await mutatingTx(id, "::actor::set_local_policy", { auto_accept: false }, lt);
546
+ });
547
+ }
548
+ saveStateFailClosed(id);
549
+ log(`[${name}] provisioned UNEXPOSED pending hierarchy reconciliation`);
550
+ return id;
551
+ } catch (err) {
552
+ rollbackUnexposedIdentity(id, name, dir);
553
+ throw err;
554
+ }
555
+ }
556
+ async function activateIdentity(id, exposeLocal) {
557
+ wrapper.expose_packet(id.cid);
558
+ identities.set(id.name, id);
559
+ try {
560
+ if (exposeLocal) await publishToBook(id);
561
+ log(`[${id.name}] EXPOSED (routing + broker registration) \u2014 hierarchy reconciled`);
562
+ } catch (err) {
563
+ rollbackUnexposedIdentity(id, id.name, id.dir);
564
+ throw err;
565
+ }
566
+ }
567
+ function rollbackUnexposedIdentity(id, name, dir) {
568
+ if (id) {
569
+ try {
570
+ closeHistory(id);
571
+ } catch {
572
+ }
573
+ try {
574
+ unpublishFromBook(id);
575
+ } catch {
576
+ }
577
+ try {
578
+ wrapper.remove_packet(id.cid);
579
+ } catch {
580
+ }
581
+ }
582
+ identities.delete(name);
583
+ try {
584
+ fs2.rmSync(dir, { recursive: true, force: true });
585
+ } catch {
586
+ }
587
+ reservedNames.delete(name);
588
+ }
444
589
 
445
590
  // ../../src/api/identity.ts
446
- import { randomBytes } from "node:crypto";
447
- import { isAbsolute } from "node:path";
448
591
  function describeOrNull(id) {
449
592
  try {
450
593
  return describeIdentity(id);
@@ -479,7 +622,7 @@ async function createIdentity(ctx, a) {
479
622
  const { name, bio, exposeLocal, localAutoAccept } = a;
480
623
  const bad = validateName(name);
481
624
  if (bad) throw errTool("create_identity", bad, "NAME_INVALID");
482
- if (identities.has(name)) throw errNameTaken("create_identity", name);
625
+ if (identities.has(name) || quarantinedIdentities.has(name)) throw errNameTaken("create_identity", name);
483
626
  try {
484
627
  const id = await provisionIdentity(name, { exposeLocal, localAutoAccept });
485
628
  await setBio(id, bio);
@@ -496,8 +639,9 @@ async function createIdentity(ctx, a) {
496
639
  } else {
497
640
  hierarchy = "root";
498
641
  const res = await establishRoot(id);
499
- adopted = res.adopted;
500
- failed = res.failed;
642
+ const held = await adoptQuarantinedIdentities(id);
643
+ adopted = [...res.adopted, ...held.adopted];
644
+ failed = [...res.failed, ...held.failed];
501
645
  }
502
646
  bindSession(ctx.sessionId(), name);
503
647
  return { info: identityInfo(id), hierarchy, underRoot, adopted, failed, exposedLocal: exposeLocal, localAutoAccept };
@@ -519,22 +663,34 @@ async function createTemporaryIdentity(ctx, a) {
519
663
  }
520
664
  } else {
521
665
  for (let i = 0; i < 5 && chosen === void 0; i++) {
522
- const cand = `tmp-${randomBytes(5).toString("hex")}`;
666
+ const cand = `tmp-${randomBytes2(5).toString("hex")}`;
523
667
  if (!identities.has(cand) && !reservedNames.has(cand)) chosen = cand;
524
668
  }
525
669
  if (chosen === void 0) throw errRandomNameExhausted();
526
670
  }
527
671
  const name = chosen;
672
+ const root = rootName ? identities.get(rootName) : void 0;
673
+ if (!root) throw errNoRoot();
674
+ let id;
528
675
  try {
529
- const id = await provisionIdentity(name, {
530
- exposeLocal,
676
+ id = await provisionIdentityUnexposed(name, {
531
677
  localAutoAccept,
532
678
  temp: { tokenHash: hashLeaseToken(token), pid }
533
679
  });
534
680
  await setBio(id, bio);
681
+ await delegateRole(root, id);
682
+ await activateIdentity(id, exposeLocal);
535
683
  bindSession(ctx.sessionId(), name);
536
- return { info: identityInfo(id), ownerPid: pid, exposedLocal: exposeLocal, localAutoAccept };
684
+ return {
685
+ info: identityInfo(id),
686
+ ownerPid: pid,
687
+ hierarchy: "role",
688
+ underRoot: root.name,
689
+ exposedLocal: exposeLocal,
690
+ localAutoAccept
691
+ };
537
692
  } catch (err) {
693
+ if (id) rollbackUnexposedIdentity(id, name, id.dir);
538
694
  throw errTool("create_temporary_identity", String(err), "PROVISION_FAILED");
539
695
  }
540
696
  }
@@ -552,7 +708,7 @@ async function closeTemporaryIdentityOp(ctx, a) {
552
708
  if (!id.temp) throw errNotTemporary(id.name);
553
709
  const owner = id.temp.owner;
554
710
  const isOwner = token !== void 0 && owner.tokenHash === hashLeaseToken(token);
555
- if (!isOwner && !id.temp.closing && pidAlive(owner.pid)) {
711
+ if (!isOwner && !id.temp.closing && !pidDefinitelyDead(owner.pid)) {
556
712
  throw errTempOwnedElsewhereClose(id.name, owner.pid);
557
713
  }
558
714
  try {
@@ -578,7 +734,7 @@ async function createRootIdentity(ctx, a) {
578
734
  if (skipIfRootExists && existingRootName && identities.has(existingRootName)) {
579
735
  throw errRootExists(existingRootName);
580
736
  }
581
- if (identities.has(name)) throw errNameTaken("create_root_identity", name);
737
+ if (identities.has(name) || quarantinedIdentities.has(name)) throw errNameTaken("create_root_identity", name);
582
738
  try {
583
739
  const id = await provisionIdentity(name, { exposeLocal, localAutoAccept });
584
740
  await setBio(id, bio);
@@ -598,13 +754,14 @@ async function createRootIdentity(ctx, a) {
598
754
  };
599
755
  }
600
756
  const { adopted, failed } = await establishRoot(id);
757
+ const held = await adoptQuarantinedIdentities(id);
601
758
  bindSession(ctx.sessionId(), name);
602
759
  return {
603
760
  info: identityInfo(id),
604
761
  hierarchy: "root",
605
762
  underRoot: null,
606
- adopted,
607
- failed,
763
+ adopted: [...adopted, ...held.adopted],
764
+ failed: [...failed, ...held.failed],
608
765
  exposedLocal: exposeLocal,
609
766
  localAutoAccept
610
767
  };
@@ -633,7 +790,7 @@ async function chooseIdentity(ctx, a) {
633
790
  if (target.temp.closing) throw errTempClosingChoose(name);
634
791
  const owner = target.temp.owner;
635
792
  if (owner.tokenHash !== hashLeaseToken(token)) {
636
- if (pidAlive(owner.pid)) throw errTempOwnedLive(name, owner.pid);
793
+ if (!pidDefinitelyDead(owner.pid)) throw errTempOwnedLive(name, owner.pid);
637
794
  throw errTempStale(name, owner.pid);
638
795
  }
639
796
  const hdrPid = ctx.clientPid();
@@ -648,7 +805,7 @@ async function chooseIdentity(ctx, a) {
648
805
  }
649
806
  const existing = leases.get(name);
650
807
  if (existing && existing.token !== token) {
651
- if (!pidAlive(existing.pid)) {
808
+ if (pidDefinitelyDead(existing.pid)) {
652
809
  log(`auto-reclaiming "${name}" from dead client pid ${existing.pid}`);
653
810
  leases.delete(name);
654
811
  } else if (!force) {
@@ -664,13 +821,13 @@ async function chooseIdentity(ctx, a) {
664
821
  return { name, cid: identities.get(name).cid, switchedFrom };
665
822
  }
666
823
  async function listIdentities(ctx) {
667
- if (identities.size === 0) return [];
824
+ if (identities.size === 0 && quarantinedIdentities.size === 0) return [];
668
825
  const myToken = ctx.leaseToken();
669
826
  const sessionOf = (name) => {
670
827
  const lease = leases.get(name);
671
828
  if (!lease) return null;
672
829
  if (lease.token === myToken) return "mine";
673
- if (!pidAlive(lease.pid)) return null;
830
+ if (pidDefinitelyDead(lease.pid)) return null;
674
831
  return "other-live";
675
832
  };
676
833
  const tempOf = (id) => {
@@ -680,7 +837,7 @@ async function listIdentities(ctx) {
680
837
  if (myToken !== void 0 && id.temp.owner.tokenHash === hashLeaseToken(myToken)) {
681
838
  return { state: "mine", ownerPid };
682
839
  }
683
- return pidAlive(ownerPid) ? { state: "other-live", ownerPid } : { state: "stale", ownerPid };
840
+ return pidDefinitelyDead(ownerPid) ? { state: "stale", ownerPid } : { state: "other-live", ownerPid };
684
841
  };
685
842
  const row = (id, kind) => ({
686
843
  name: id.name,
@@ -697,13 +854,10 @@ async function listIdentities(ctx) {
697
854
  if (id.name === root.name) continue;
698
855
  if (describeIdentity(id).roleId !== "") rows.push(row(id, "role"));
699
856
  }
700
- for (const id of identities.values()) {
701
- if (id.name === root.name || describeIdentity(id).roleId !== "") continue;
702
- rows.push(row(id, "flat"));
703
- }
704
857
  } else {
705
- for (const id of identities.values()) rows.push(row(id, "flat"));
858
+ for (const id of identities.values()) rows.push(row(id, "role"));
706
859
  }
860
+ for (const [name, held] of quarantinedIdentities) rows.push({ name, status: held.status });
707
861
  return rows;
708
862
  }
709
863
  async function currentIdentity(ctx) {
@@ -724,16 +878,18 @@ async function currentIdentity(ctx) {
724
878
  }
725
879
  async function removeIdentity(ctx, a) {
726
880
  const { name } = a;
727
- const id = identities.get(name);
881
+ const held = quarantinedIdentities.get(name);
882
+ const id = identities.get(name) ?? held?.identity;
728
883
  if (!id) throw errTool("remove_identity", `no identity named "${name}".`, "NO_SUCH_IDENTITY");
729
884
  if (id.temp) {
730
885
  const token = ctx.leaseToken();
731
886
  const owner = id.temp.owner;
732
- if ((token === void 0 || owner.tokenHash !== hashLeaseToken(token)) && !id.temp.closing && pidAlive(owner.pid)) {
887
+ if ((token === void 0 || owner.tokenHash !== hashLeaseToken(token)) && !id.temp.closing && !pidDefinitelyDead(owner.pid)) {
733
888
  throw errTempOwnedElsewhereRemove(name, owner.pid);
734
889
  }
735
890
  try {
736
891
  const res = await closeTemporaryIdentity(id, "remove_identity");
892
+ quarantinedIdentities.delete(name);
737
893
  if (res.deleteError) {
738
894
  const remote = res.attempted === 0 ? "" : ` Remove-me notices: ${res.notified}/${res.attempted} queued, ${res.failed} not sent (best effort).`;
739
895
  throw errIdentityClosedWithError(name, res.deleteError, remote);
@@ -752,27 +908,13 @@ async function removeIdentity(ctx, a) {
752
908
  }
753
909
  const fail = deleteIdentityCompletely(id);
754
910
  if (fail) throw errIdentityPartiallyRemoved(name, fail);
911
+ quarantinedIdentities.delete(name);
755
912
  return { name, kind: "permanent" };
756
913
  }
757
914
  async function releaseLease(ctx) {
758
915
  const token = ctx.leaseToken();
759
916
  if (!token) throw errNoLeaseToken();
760
- const released = [];
761
- for (const [n, l] of [...leases]) {
762
- if (l.token !== token) continue;
763
- leases.delete(n);
764
- released.push(n);
765
- const rid = identities.get(n);
766
- if (rid?.temp && rid.temp.owner.tokenHash === hashLeaseToken(token) && !rid.temp.closing) {
767
- void closeTemporaryIdentity(rid, "owning session released its lease").catch(
768
- (err) => log(`[${n}] close on lease release failed:`, String(err))
769
- );
770
- }
771
- }
772
- tombstones.delete(token);
773
- persistBindings();
774
- log(`lease released by token \u2026${token.slice(-6)}`);
775
- return { released };
917
+ return releaseLeaseByToken(token);
776
918
  }
777
919
 
778
920
  // ../../src/api/contacts.ts
@@ -1621,7 +1763,13 @@ async function serveApiOperation(req, res, pathname, readBody2) {
1621
1763
  sendJson(res, 200, await handler(session.ctx, body));
1622
1764
  } catch (err) {
1623
1765
  if (err instanceof OursError) {
1624
- sendJson(res, 400, { error: { code: err.code, message: err.message } });
1766
+ sendJson(res, 400, {
1767
+ error: {
1768
+ code: err.code,
1769
+ message: err.message,
1770
+ ...err.details ? { details: err.details } : {}
1771
+ }
1772
+ });
1625
1773
  return;
1626
1774
  }
1627
1775
  log("api handler error:", name, String(err));
@@ -1776,7 +1924,7 @@ async function startHttpDaemon(opts) {
1776
1924
  for (const sid of [...serversBySession.keys()]) {
1777
1925
  if (sid === "stdio") continue;
1778
1926
  const pid = sessionHeaders.get(sid)?.pid;
1779
- if (pid === void 0 || pid <= 1 || pidAlive(pid)) continue;
1927
+ if (pid === void 0 || !pidDefinitelyDead(pid)) continue;
1780
1928
  const inf = inflight.get(sid);
1781
1929
  if (inf && inf.n > 0 && Date.now() - inf.since < STUCK_MS) continue;
1782
1930
  const srv = serversBySession.get(sid);
@@ -1792,11 +1940,16 @@ async function startHttpDaemon(opts) {
1792
1940
  }
1793
1941
  return reaped;
1794
1942
  };
1943
+ let reaperRunning = false;
1795
1944
  const sessionReaper = setInterval(() => {
1945
+ if (reaperRunning) return;
1946
+ reaperRunning = true;
1796
1947
  reapDeadSessions();
1797
- sweepStaleTempIdentities();
1798
1948
  sweepStaleUploads();
1799
- }, 6e4);
1949
+ void reapStaleTemporaryIdentities().finally(() => {
1950
+ reaperRunning = false;
1951
+ });
1952
+ }, sessionReaperIntervalMs());
1800
1953
  sessionReaper.unref?.();
1801
1954
  const REQ_META_MAX = 1e3;
1802
1955
  const REQ_META_TTL_MS = 10 * 6e4;
@@ -1863,8 +2016,8 @@ async function startHttpDaemon(opts) {
1863
2016
  res.end(JSON.stringify({
1864
2017
  identities: [...identities.values()].map((i) => ({
1865
2018
  name: i.name,
1866
- ...i.temp ? { temporary: true, stale: !pidAlive(i.temp.owner.pid) } : {}
1867
- }))
2019
+ ...i.temp ? { temporary: true, stale: pidDefinitelyDead(i.temp.owner.pid) } : {}
2020
+ })).concat([...quarantinedIdentities.entries()].map(([name, held]) => ({ name, status: held.status })))
1868
2021
  }));
1869
2022
  return;
1870
2023
  }
@@ -1884,6 +2037,11 @@ async function startHttpDaemon(opts) {
1884
2037
  res.end(JSON.stringify({ error: "invalid identity name" }));
1885
2038
  return;
1886
2039
  }
2040
+ if (!identities.has(name)) {
2041
+ res.writeHead(404, { "Content-Type": "application/json" });
2042
+ res.end(JSON.stringify({ error: "no active identity with that name" }));
2043
+ return;
2044
+ }
1887
2045
  await serveNotifications(req, res, name, url.searchParams.get("since"), url.searchParams.get("kinds"));
1888
2046
  return;
1889
2047
  }
@@ -1939,9 +2097,9 @@ async function startHttpDaemon(opts) {
1939
2097
  return;
1940
2098
  }
1941
2099
  try {
1942
- const stat = fs2.statSync(filePath);
2100
+ const stat = fs3.statSync(filePath);
1943
2101
  res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": String(stat.size) });
1944
- const stream = fs2.createReadStream(filePath);
2102
+ const stream = fs3.createReadStream(filePath);
1945
2103
  stream.on("error", () => {
1946
2104
  try {
1947
2105
  res.destroy();
@@ -2043,19 +2201,7 @@ async function startHttpDaemon(opts) {
2043
2201
  }
2044
2202
  const token = sessionHeaders.get(sessionId)?.token ?? req.headers["x-ours-lease-token"];
2045
2203
  if (token) {
2046
- for (const [n, l] of [...leases]) {
2047
- if (l.token !== token) continue;
2048
- leases.delete(n);
2049
- const rid = identities.get(n);
2050
- if (rid?.temp && rid.temp.owner.tokenHash === hashLeaseToken(token) && !rid.temp.closing) {
2051
- void closeTemporaryIdentity(rid, "owning session released its lease").catch(
2052
- (err) => log(`[${n}] close on lease release failed:`, String(err))
2053
- );
2054
- }
2055
- }
2056
- tombstones.delete(token);
2057
- persistBindings();
2058
- log(`lease released by token \u2026${token.slice(-6)}`);
2204
+ await releaseLeaseByToken(token);
2059
2205
  }
2060
2206
  await transports[sessionId].handleRequest(req, res);
2061
2207
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Transport-neutral operator CLI for the ours shared daemon",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",