@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.
- package/dist/{boot-BUGOBQGZ.js → boot-HNJ7ZR5L.js} +1 -1
- package/dist/{chunk-UIWEBE5K.js → chunk-DZH2YEXT.js} +1 -1
- package/dist/{chunk-ZCCCTR27.js → chunk-TSYICL3I.js} +141 -32
- package/dist/{daemon-3C5SDTHJ.js → daemon-LTJHJDWZ.js} +2 -2
- package/dist/lifecycle.js +1 -1
- package/dist/main.js +2 -2
- package/dist/mufl_code/351F16D444E6DAA252D4A8D829D17204360D9015FEF670ACE68FB4747BB3B14E.muflo +0 -0
- package/dist/{server-TVV2EZZ6.js → server-35QOYLY4.js} +302 -156
- package/package.json +1 -1
|
@@ -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-
|
|
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.
|
|
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
|
|
898
|
-
if (!Number.isInteger(pid) || pid <=
|
|
898
|
+
function pidDefinitelyDead(pid) {
|
|
899
|
+
if (!Number.isInteger(pid) || pid <= 1) return false;
|
|
899
900
|
try {
|
|
900
901
|
process.kill(pid, 0);
|
|
901
|
-
return
|
|
902
|
+
return false;
|
|
902
903
|
} catch (err) {
|
|
903
|
-
return err.code === "
|
|
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
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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/
|
|
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
|
-
|
|
2728
|
-
|
|
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 &&
|
|
2734
|
-
|
|
2735
|
-
(
|
|
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
|
|
2740
|
-
if (!
|
|
2741
|
-
for (const d of
|
|
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 ||
|
|
2840
|
+
if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
|
|
2746
2841
|
try {
|
|
2747
|
-
|
|
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
|
|
2844
|
+
log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
|
|
2750
2845
|
} catch (err) {
|
|
2751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2949
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
package/dist/main.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
serveDaemon,
|
|
45
45
|
startDaemonManaged,
|
|
46
46
|
stopDaemonManaged
|
|
47
|
-
} from "./chunk-
|
|
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.
|
|
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";
|
package/dist/mufl_code/351F16D444E6DAA252D4A8D829D17204360D9015FEF670ACE68FB4747BB3B14E.muflo
CHANGED
|
Binary file
|
|
@@ -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
|
-
|
|
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-
|
|
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
|
|
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/
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
);
|
|
431
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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-${
|
|
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
|
-
|
|
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 {
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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, "
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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, {
|
|
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 ||
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
2100
|
+
const stat = fs3.statSync(filePath);
|
|
1943
2101
|
res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": String(stat.size) });
|
|
1944
|
-
const stream =
|
|
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
|
-
|
|
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 {
|