@awiki/dsh-plugin 0.3.5 → 0.3.7-dsh-test.20260831.1
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/README.md +45 -32
- package/README.zh.md +33 -28
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/cordis.patch.yml +3 -1
- package/lib/client.js +1348 -637
- package/lib/client.js.map +1 -1
- package/lib/index.js +989 -393
- package/lib/provider.js +57 -16
- package/lib/{sdk-adapter-CDLTgHJA.mjs → sdk-adapter-FGaOi8It.mjs} +167 -50
- package/lib/typert.host.js +411 -128
- package/lib/typert.remote-client.d.ts +19 -5
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +411 -128
- package/lib/types/client/AwikiDevices.d.ts +28 -0
- package/lib/types/client/AwikiDevices.d.ts.map +1 -0
- package/lib/types/client/AwikiDevices.js +85 -0
- package/lib/types/client/AwikiDevices.js.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts +6 -5
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +88 -27
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +5 -12
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.js +1 -1
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
- package/lib/types/client/controller.d.ts +20 -28
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +41 -162
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +8 -2
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/slots.d.ts +10 -6
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/conversation-preferences.d.ts.map +1 -1
- package/lib/types/conversation-preferences.js +0 -17
- package/lib/types/conversation-preferences.js.map +1 -1
- package/lib/types/index.d.ts +54 -13
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +739 -221
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener-state.d.ts +5 -3
- package/lib/types/listener-state.d.ts.map +1 -1
- package/lib/types/listener-state.js +28 -13
- package/lib/types/listener-state.js.map +1 -1
- package/lib/types/listener.d.ts +9 -32
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +11 -127
- package/lib/types/listener.js.map +1 -1
- package/lib/types/provider-api.d.ts +99 -7
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts +7 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +58 -15
- package/lib/types/provider.js.map +1 -1
- package/lib/types/realtime-supervisor.d.ts +54 -0
- package/lib/types/realtime-supervisor.d.ts.map +1 -0
- package/lib/types/realtime-supervisor.js +242 -0
- package/lib/types/realtime-supervisor.js.map +1 -0
- package/lib/types/sdk-adapter.d.ts +31 -5
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +162 -59
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/types.d.ts +76 -27
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +5 -2
- package/lib/types/profile-state.d.ts +0 -11
- package/lib/types/profile-state.d.ts.map +0 -1
- package/lib/types/profile-state.js +0 -48
- package/lib/types/profile-state.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { n as downloadedAttachment } from "./sdk-adapter-
|
|
1
|
+
import { n as downloadedAttachment } from "./sdk-adapter-FGaOi8It.mjs";
|
|
2
2
|
import "@deepseek-ai/cordis";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
7
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
8
8
|
import { SettingsConflictError, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
10
|
import { chmod, lstat, mkdir, readFile, readdir, rename, rm, unlink, utimes, writeFile } from "node:fs/promises";
|
|
11
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
12
11
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
13
12
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
13
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
14
|
//#region lib/types/types.js
|
|
17
15
|
/** Client-safe AWiki service and Remote data types. */
|
|
18
16
|
/** Exact browser acknowledgement required before locally signing out. */
|
|
@@ -1180,7 +1178,6 @@ const MAX_IDENTIFIER_CHARACTERS = 2048;
|
|
|
1180
1178
|
const MAX_TITLE_CHARACTERS = 1024;
|
|
1181
1179
|
const MAX_PREVIEW_CHARACTERS = 4096;
|
|
1182
1180
|
const MAX_HANDLE_CHARACTERS = 512;
|
|
1183
|
-
const MAX_RECOVERY_SIGNATURE_CHARACTERS = 128;
|
|
1184
1181
|
function invalidState() {
|
|
1185
1182
|
throw new TypeError("awiki: conversation preferences are invalid");
|
|
1186
1183
|
}
|
|
@@ -1266,22 +1263,17 @@ function preferences(value, expectedOwnerDid) {
|
|
|
1266
1263
|
if (seen.has(hidden.conversation.id)) invalidState();
|
|
1267
1264
|
seen.add(hidden.conversation.id);
|
|
1268
1265
|
}
|
|
1269
|
-
const dismissedGroupRecoverySignature = optionalString(value.dismissedGroupRecoverySignature, MAX_RECOVERY_SIGNATURE_CHARACTERS);
|
|
1270
1266
|
return {
|
|
1271
1267
|
version: STORE_VERSION,
|
|
1272
1268
|
ownerDid: String(expectedOwnerDid),
|
|
1273
|
-
hiddenConversations
|
|
1274
|
-
...dismissedGroupRecoverySignature === void 0 ? {} : { dismissedGroupRecoverySignature }
|
|
1269
|
+
hiddenConversations
|
|
1275
1270
|
};
|
|
1276
1271
|
}
|
|
1277
1272
|
function publicPreferences(value) {
|
|
1278
|
-
return {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
})),
|
|
1283
|
-
...value.dismissedGroupRecoverySignature === void 0 ? {} : { dismissedGroupRecoverySignature: value.dismissedGroupRecoverySignature }
|
|
1284
|
-
};
|
|
1273
|
+
return { hiddenConversations: value.hiddenConversations.map((hidden) => ({
|
|
1274
|
+
conversation: { ...hidden.conversation },
|
|
1275
|
+
hiddenAt: hidden.hiddenAt
|
|
1276
|
+
})) };
|
|
1285
1277
|
}
|
|
1286
1278
|
function emptyPreferences(ownerDid) {
|
|
1287
1279
|
return {
|
|
@@ -1302,10 +1294,6 @@ function normalizeConversationPreferenceMutation(value) {
|
|
|
1302
1294
|
action: "restore",
|
|
1303
1295
|
conversationId: boundedString$1(value.conversationId, MAX_IDENTIFIER_CHARACTERS)
|
|
1304
1296
|
};
|
|
1305
|
-
if (value.action === "dismiss-group-recovery") return {
|
|
1306
|
-
action: "dismiss-group-recovery",
|
|
1307
|
-
signature: boundedString$1(value.signature, MAX_RECOVERY_SIGNATURE_CHARACTERS)
|
|
1308
|
-
};
|
|
1309
1297
|
} catch {
|
|
1310
1298
|
return;
|
|
1311
1299
|
}
|
|
@@ -1351,14 +1339,6 @@ var AwikiConversationPreferenceStore = class {
|
|
|
1351
1339
|
};
|
|
1352
1340
|
break;
|
|
1353
1341
|
}
|
|
1354
|
-
case "dismiss-group-recovery": {
|
|
1355
|
-
const signature = normalized.signature;
|
|
1356
|
-
next = {
|
|
1357
|
-
...current,
|
|
1358
|
-
dismissedGroupRecoverySignature: signature
|
|
1359
|
-
};
|
|
1360
|
-
break;
|
|
1361
|
-
}
|
|
1362
1342
|
default: invalidState();
|
|
1363
1343
|
}
|
|
1364
1344
|
await this.write(ownerDid, next);
|
|
@@ -1450,12 +1430,13 @@ var AwikiConversationPreferenceStore = class {
|
|
|
1450
1430
|
};
|
|
1451
1431
|
//#endregion
|
|
1452
1432
|
//#region lib/types/listener-state.js
|
|
1453
|
-
const STATE_VERSION =
|
|
1433
|
+
const STATE_VERSION = 2;
|
|
1454
1434
|
const MAX_STATE_BYTES = 1048576;
|
|
1455
1435
|
const MAX_CONVERSATIONS = 1e3;
|
|
1456
|
-
function emptyState() {
|
|
1436
|
+
function emptyState(identityScopeHash) {
|
|
1457
1437
|
return {
|
|
1458
1438
|
version: STATE_VERSION,
|
|
1439
|
+
identityScopeHash,
|
|
1459
1440
|
conversations: {}
|
|
1460
1441
|
};
|
|
1461
1442
|
}
|
|
@@ -1467,8 +1448,10 @@ function boundedString(value, field) {
|
|
|
1467
1448
|
return value;
|
|
1468
1449
|
}
|
|
1469
1450
|
/** Validate and detach the complete state file before the listener trusts any route. */
|
|
1470
|
-
function parseState(value) {
|
|
1471
|
-
if (
|
|
1451
|
+
function parseState(value, identityScopeHash) {
|
|
1452
|
+
if (isRecord(value) && value.version === 1) return emptyState(identityScopeHash);
|
|
1453
|
+
if (isRecord(value) && value.version === STATE_VERSION && typeof value.identityScopeHash === "string" && value.identityScopeHash !== identityScopeHash) return emptyState(identityScopeHash);
|
|
1454
|
+
if (!isRecord(value) || value.version !== STATE_VERSION || value.identityScopeHash !== identityScopeHash || !isRecord(value.conversations)) throw new TypeError("awiki: listener state is invalid");
|
|
1472
1455
|
const entries = Object.entries(value.conversations);
|
|
1473
1456
|
if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
|
|
1474
1457
|
const conversations = {};
|
|
@@ -1491,6 +1474,7 @@ function parseState(value) {
|
|
|
1491
1474
|
}
|
|
1492
1475
|
return {
|
|
1493
1476
|
version: STATE_VERSION,
|
|
1477
|
+
identityScopeHash,
|
|
1494
1478
|
conversations
|
|
1495
1479
|
};
|
|
1496
1480
|
}
|
|
@@ -1501,28 +1485,31 @@ function isMissing(error) {
|
|
|
1501
1485
|
var AwikiListenerStateStore = class {
|
|
1502
1486
|
hostDirectory;
|
|
1503
1487
|
statePath;
|
|
1504
|
-
|
|
1488
|
+
identityScopeHash;
|
|
1489
|
+
constructor(stateRoot, identityScope) {
|
|
1490
|
+
if (identityScope.length === 0 || identityScope.length > 2048) throw new TypeError("awiki: listener identity scope is invalid");
|
|
1491
|
+
this.identityScopeHash = createHash("sha256").update(identityScope).digest("hex");
|
|
1505
1492
|
this.hostDirectory = join(stateRoot, ".host");
|
|
1506
1493
|
this.statePath = join(this.hostDirectory, "listener-state.json");
|
|
1507
1494
|
}
|
|
1508
|
-
/** Load the current
|
|
1495
|
+
/** Load the current identity scope or reset unscoped v1 state on first use. */
|
|
1509
1496
|
async load() {
|
|
1510
|
-
if (!await this.hasPrivateHostDirectory()) return emptyState();
|
|
1497
|
+
if (!await this.hasPrivateHostDirectory()) return emptyState(this.identityScopeHash);
|
|
1511
1498
|
try {
|
|
1512
1499
|
const metadata = await lstat(this.statePath);
|
|
1513
1500
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
|
|
1514
1501
|
const text = await readFile(this.statePath, "utf8");
|
|
1515
1502
|
if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
|
|
1516
|
-
return parseState(JSON.parse(text));
|
|
1503
|
+
return parseState(JSON.parse(text), this.identityScopeHash);
|
|
1517
1504
|
} catch (error) {
|
|
1518
|
-
if (isMissing(error)) return emptyState();
|
|
1505
|
+
if (isMissing(error)) return emptyState(this.identityScopeHash);
|
|
1519
1506
|
if (error instanceof SyntaxError) throw new TypeError("awiki: listener state file is invalid");
|
|
1520
1507
|
throw error;
|
|
1521
1508
|
}
|
|
1522
1509
|
}
|
|
1523
1510
|
/** Replace the state atomically without ever writing message or Agent text. */
|
|
1524
1511
|
async save(state) {
|
|
1525
|
-
const snapshot = parseState(state);
|
|
1512
|
+
const snapshot = parseState(state, this.identityScopeHash);
|
|
1526
1513
|
const text = `${JSON.stringify(snapshot)}\n`;
|
|
1527
1514
|
if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is too large");
|
|
1528
1515
|
await mkdir(this.hostDirectory, {
|
|
@@ -1744,11 +1731,12 @@ function incomingFromPeer(message, conversation) {
|
|
|
1744
1731
|
}
|
|
1745
1732
|
function copyState(state) {
|
|
1746
1733
|
return {
|
|
1747
|
-
version:
|
|
1734
|
+
version: 2,
|
|
1735
|
+
identityScopeHash: state.identityScopeHash,
|
|
1748
1736
|
conversations: Object.fromEntries(Object.entries(state.conversations).map(([id, route]) => [id, { ...route }]))
|
|
1749
1737
|
};
|
|
1750
1738
|
}
|
|
1751
|
-
/**
|
|
1739
|
+
/** Consume authorized Direct text only after the identity supervisor commits synchronization. */
|
|
1752
1740
|
var AwikiAgentListener = class {
|
|
1753
1741
|
awiki;
|
|
1754
1742
|
agents;
|
|
@@ -1757,34 +1745,23 @@ var AwikiAgentListener = class {
|
|
|
1757
1745
|
store;
|
|
1758
1746
|
logger;
|
|
1759
1747
|
stateReady;
|
|
1760
|
-
state
|
|
1761
|
-
version: 1,
|
|
1762
|
-
conversations: {}
|
|
1763
|
-
};
|
|
1748
|
+
state;
|
|
1764
1749
|
stateMutation = Promise.resolve();
|
|
1765
1750
|
syncMutation = Promise.resolve();
|
|
1766
1751
|
scheduledMessageIds = /* @__PURE__ */ new Set();
|
|
1767
1752
|
conversationQueues = /* @__PURE__ */ new Map();
|
|
1768
|
-
lifecycle;
|
|
1769
|
-
started;
|
|
1770
|
-
resolveStarted;
|
|
1771
|
-
rejectStarted;
|
|
1772
|
-
lifecycleGeneration = 0;
|
|
1773
|
-
streamGeneration = 0;
|
|
1774
|
-
activeRealtime;
|
|
1775
1753
|
stopped = false;
|
|
1776
|
-
termination;
|
|
1777
|
-
resolveTermination;
|
|
1778
|
-
terminationSettled = false;
|
|
1779
1754
|
constructor(awiki, agents, config, logger, store) {
|
|
1780
1755
|
this.awiki = awiki;
|
|
1781
1756
|
this.agents = agents;
|
|
1782
1757
|
this.config = config;
|
|
1783
|
-
this.termination = new Promise((resolve) => {
|
|
1784
|
-
this.resolveTermination = resolve;
|
|
1785
|
-
});
|
|
1786
1758
|
this.allowedPeers = new Set(config.allowedPeers.map((peer) => peer.startsWith("did:") ? peer : peer.toLowerCase()));
|
|
1787
|
-
this.store = store ?? new AwikiListenerStateStore(config.stateRoot);
|
|
1759
|
+
this.store = store ?? new AwikiListenerStateStore(config.stateRoot, config.identityScope);
|
|
1760
|
+
this.state = {
|
|
1761
|
+
version: 2,
|
|
1762
|
+
identityScopeHash: this.store.identityScopeHash,
|
|
1763
|
+
conversations: {}
|
|
1764
|
+
};
|
|
1788
1765
|
this.logger = logger ?? {
|
|
1789
1766
|
debug() {},
|
|
1790
1767
|
info() {},
|
|
@@ -1796,38 +1773,11 @@ var AwikiAgentListener = class {
|
|
|
1796
1773
|
this.state = state;
|
|
1797
1774
|
});
|
|
1798
1775
|
}
|
|
1799
|
-
/**
|
|
1800
|
-
|
|
1801
|
-
if (this.started !== void 0) return this.started;
|
|
1802
|
-
this.started = new Promise((resolve, reject) => {
|
|
1803
|
-
this.resolveStarted = resolve;
|
|
1804
|
-
this.rejectStarted = reject;
|
|
1805
|
-
});
|
|
1806
|
-
const generation = ++this.lifecycleGeneration;
|
|
1807
|
-
this.lifecycle = this.run(generation).then(() => {
|
|
1808
|
-
this.finishTermination({ kind: "stopped" });
|
|
1809
|
-
}, (error) => {
|
|
1810
|
-
this.rejectStarted?.(error);
|
|
1811
|
-
this.rejectStarted = void 0;
|
|
1812
|
-
this.finishTermination({
|
|
1813
|
-
kind: "failed",
|
|
1814
|
-
error
|
|
1815
|
-
});
|
|
1816
|
-
if (!this.stopped) this.logger.warn("AWiki realtime listener stopped: %s", error instanceof Error ? error.message : "unknown failure");
|
|
1817
|
-
});
|
|
1818
|
-
return this.started;
|
|
1819
|
-
}
|
|
1820
|
-
/** Resolve once with either orderly shutdown or the exact terminal lifecycle failure. */
|
|
1821
|
-
whenTerminated() {
|
|
1822
|
-
return this.termination;
|
|
1823
|
-
}
|
|
1824
|
-
/** Deterministic canonical sync plus committed-history reconciliation for tests and recovery. */
|
|
1825
|
-
async synchronizeOnce(reason) {
|
|
1776
|
+
/** Reconcile only committed history; this consumer cannot start WSS or advance sync. */
|
|
1777
|
+
async reconcileOnce() {
|
|
1826
1778
|
await this.enqueueSync(async () => {
|
|
1827
1779
|
await this.stateReady;
|
|
1828
1780
|
if (this.stopped) return;
|
|
1829
|
-
await this.awiki.syncNow(reason);
|
|
1830
|
-
if (this.stopped) return;
|
|
1831
1781
|
await this.reconcileCommittedHistory();
|
|
1832
1782
|
});
|
|
1833
1783
|
}
|
|
@@ -1837,84 +1787,12 @@ var AwikiAgentListener = class {
|
|
|
1837
1787
|
while (this.conversationQueues.size > 0) await Promise.allSettled([...this.conversationQueues.values()]);
|
|
1838
1788
|
await this.stateMutation;
|
|
1839
1789
|
}
|
|
1840
|
-
/**
|
|
1790
|
+
/** Fence late work, drain committed messages, then release listener-owned Agents. */
|
|
1841
1791
|
async dispose() {
|
|
1842
1792
|
if (this.stopped) return;
|
|
1843
1793
|
this.stopped = true;
|
|
1844
|
-
this.lifecycleGeneration += 1;
|
|
1845
|
-
this.streamGeneration += 1;
|
|
1846
|
-
const active = this.activeRealtime;
|
|
1847
|
-
this.activeRealtime = void 0;
|
|
1848
|
-
await active?.session.stop().catch(() => void 0);
|
|
1849
|
-
await this.lifecycle;
|
|
1850
|
-
this.resolveStarted?.();
|
|
1851
|
-
this.resolveStarted = void 0;
|
|
1852
|
-
this.rejectStarted = void 0;
|
|
1853
1794
|
await this.whenIdle();
|
|
1854
1795
|
await this.agents.dispose();
|
|
1855
|
-
this.finishTermination({ kind: "stopped" });
|
|
1856
|
-
}
|
|
1857
|
-
finishTermination(result) {
|
|
1858
|
-
if (this.terminationSettled) return;
|
|
1859
|
-
this.terminationSettled = true;
|
|
1860
|
-
this.resolveTermination(result);
|
|
1861
|
-
}
|
|
1862
|
-
currentLifecycle(generation) {
|
|
1863
|
-
return !this.stopped && this.lifecycleGeneration === generation;
|
|
1864
|
-
}
|
|
1865
|
-
async run(lifecycleGeneration) {
|
|
1866
|
-
await this.synchronize("session_start", lifecycleGeneration);
|
|
1867
|
-
if (!this.currentLifecycle(lifecycleGeneration)) return;
|
|
1868
|
-
let active = await this.openRealtime(lifecycleGeneration);
|
|
1869
|
-
if (active === void 0) return;
|
|
1870
|
-
this.resolveStarted?.();
|
|
1871
|
-
this.resolveStarted = void 0;
|
|
1872
|
-
this.rejectStarted = void 0;
|
|
1873
|
-
try {
|
|
1874
|
-
while (this.currentLifecycle(lifecycleGeneration)) {
|
|
1875
|
-
const event = await active.session.nextEvent();
|
|
1876
|
-
if (!this.currentLifecycle(lifecycleGeneration) || this.activeRealtime?.generation !== active.generation) return;
|
|
1877
|
-
if (event === null) {
|
|
1878
|
-
this.activeRealtime = void 0;
|
|
1879
|
-
this.streamGeneration += 1;
|
|
1880
|
-
await active.session.stop();
|
|
1881
|
-
if (!this.currentLifecycle(lifecycleGeneration)) return;
|
|
1882
|
-
await this.synchronize("websocket_reconnect", lifecycleGeneration);
|
|
1883
|
-
if (!this.currentLifecycle(lifecycleGeneration)) return;
|
|
1884
|
-
const replacement = await this.openRealtime(lifecycleGeneration);
|
|
1885
|
-
if (replacement === void 0) return;
|
|
1886
|
-
active = replacement;
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
if (event.kind !== "sync_required") continue;
|
|
1890
|
-
await this.synchronize(event.cause === "reconnected" ? "websocket_reconnect" : "websocket_hint", lifecycleGeneration);
|
|
1891
|
-
}
|
|
1892
|
-
} finally {
|
|
1893
|
-
if (this.activeRealtime?.generation === active.generation) this.activeRealtime = void 0;
|
|
1894
|
-
await active.session.stop().catch(() => void 0);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
async openRealtime(lifecycleGeneration) {
|
|
1898
|
-
const session = await this.awiki.startRealtime();
|
|
1899
|
-
if (!this.currentLifecycle(lifecycleGeneration)) {
|
|
1900
|
-
await session.stop().catch(() => void 0);
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
const active = {
|
|
1904
|
-
generation: ++this.streamGeneration,
|
|
1905
|
-
session
|
|
1906
|
-
};
|
|
1907
|
-
this.activeRealtime = active;
|
|
1908
|
-
return active;
|
|
1909
|
-
}
|
|
1910
|
-
async synchronize(reason, lifecycleGeneration) {
|
|
1911
|
-
await this.enqueueSync(async () => {
|
|
1912
|
-
await this.stateReady;
|
|
1913
|
-
if (!this.currentLifecycle(lifecycleGeneration)) return;
|
|
1914
|
-
await this.awiki.syncNow(reason);
|
|
1915
|
-
if (!this.currentLifecycle(lifecycleGeneration)) return;
|
|
1916
|
-
await this.reconcileCommittedHistory();
|
|
1917
|
-
});
|
|
1918
1796
|
}
|
|
1919
1797
|
enqueueSync(operation) {
|
|
1920
1798
|
const pending = this.syncMutation.then(operation, operation);
|
|
@@ -2081,7 +1959,8 @@ var AwikiAgentListener = class {
|
|
|
2081
1959
|
...update.lastProcessedMessageId === void 0 ? current?.lastProcessedMessageId === void 0 ? {} : { lastProcessedMessageId: current.lastProcessedMessageId } : { lastProcessedMessageId: update.lastProcessedMessageId }
|
|
2082
1960
|
};
|
|
2083
1961
|
this.state = {
|
|
2084
|
-
version:
|
|
1962
|
+
version: 2,
|
|
1963
|
+
identityScopeHash: this.state.identityScopeHash,
|
|
2085
1964
|
conversations: {
|
|
2086
1965
|
...this.state.conversations,
|
|
2087
1966
|
[conversation.id]: route
|
|
@@ -2094,39 +1973,215 @@ var AwikiAgentListener = class {
|
|
|
2094
1973
|
}
|
|
2095
1974
|
};
|
|
2096
1975
|
//#endregion
|
|
2097
|
-
//#region lib/types/
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
1976
|
+
//#region lib/types/realtime-supervisor.js
|
|
1977
|
+
const RETRY_BASE_DELAY_MS = 1e3;
|
|
1978
|
+
const RETRY_MAX_DELAY_MS = 3e4;
|
|
1979
|
+
const SYNC_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
1980
|
+
"sync.retry.transport_unavailable",
|
|
1981
|
+
"sync.retry.service_unavailable",
|
|
1982
|
+
"sync.retry.local_state_unavailable",
|
|
1983
|
+
"sync.retry.local_state.actor_closed",
|
|
1984
|
+
"sync.retry.local_state.database_busy",
|
|
1985
|
+
"sync.retry.local_state.constraint_failed",
|
|
1986
|
+
"sync.retry.local_state.schema_unavailable",
|
|
1987
|
+
"sync.retry.local_state.storage_unavailable",
|
|
1988
|
+
"sync.retry.local_state.codec_unavailable",
|
|
1989
|
+
"sync.retry.local_state.other",
|
|
1990
|
+
"sync.retryable_failure",
|
|
1991
|
+
"sync.recovery_required",
|
|
1992
|
+
"sync.auth_revoked",
|
|
1993
|
+
"sync.blocked",
|
|
1994
|
+
"sync.unexpected_status"
|
|
1995
|
+
]);
|
|
1996
|
+
function syncReason(cause) {
|
|
1997
|
+
if (cause === "session_start") return "session_start";
|
|
1998
|
+
return cause === "reconnected" ? "websocket_reconnect" : "websocket_hint";
|
|
2102
1999
|
}
|
|
2103
|
-
|
|
2104
|
-
* Resolve the active DSH profile without guessing it from argv or process type.
|
|
2105
|
-
* Desktop's generation-scoped service is authoritative. Ordinary DSH has no
|
|
2106
|
-
* corresponding service, so its Loader root is accepted only when it is the
|
|
2107
|
-
* exact `$DSH_HOME/profiles/<name>` directory.
|
|
2108
|
-
*/
|
|
2109
|
-
function resolveAwikiProfileName(ctx, dshHome) {
|
|
2110
|
-
const desktopProfiles = ctx.get("desktopProfiles");
|
|
2111
|
-
if (desktopProfiles !== void 0) return assertProfileName(desktopProfiles.current?.name, "desktopProfiles.current");
|
|
2112
|
-
let profileDir;
|
|
2000
|
+
function syncFailureCode(error) {
|
|
2113
2001
|
try {
|
|
2114
|
-
if (
|
|
2115
|
-
const
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2002
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
2003
|
+
const value = error.realtimeFailureCode;
|
|
2004
|
+
if (typeof value !== "string") return void 0;
|
|
2005
|
+
return SYNC_FAILURE_CODES.has(value) ? value : void 0;
|
|
2118
2006
|
} catch {
|
|
2119
2007
|
return;
|
|
2120
2008
|
}
|
|
2121
|
-
const profilesDir = resolve(dshHome, "profiles");
|
|
2122
|
-
if (dirname(profileDir) !== profilesDir) return void 0;
|
|
2123
|
-
return assertProfileName(basename(profileDir), "Loader profile directory");
|
|
2124
|
-
}
|
|
2125
|
-
/** Resolve the profile-local default while preserving the legacy fallback. */
|
|
2126
|
-
function resolveAwikiStateRoot(ctx, dshHome) {
|
|
2127
|
-
const profileName = resolveAwikiProfileName(ctx, dshHome);
|
|
2128
|
-
return profileName === void 0 ? join(dshHome, "awiki", "im-core") : join(dshHome, "awiki", profileName, "im-core");
|
|
2129
2009
|
}
|
|
2010
|
+
/** Own the deployment identity's only WSS without knowing Workspace or Agent policy. */
|
|
2011
|
+
var IdentityRealtimeSupervisor = class {
|
|
2012
|
+
realtime;
|
|
2013
|
+
config;
|
|
2014
|
+
logger;
|
|
2015
|
+
lifecycle;
|
|
2016
|
+
generation = 0;
|
|
2017
|
+
activeSession;
|
|
2018
|
+
stoppedSessions = /* @__PURE__ */ new WeakSet();
|
|
2019
|
+
stopped = false;
|
|
2020
|
+
connected = false;
|
|
2021
|
+
startCount = 0;
|
|
2022
|
+
stopCount = 0;
|
|
2023
|
+
maxActiveSessionCount = 0;
|
|
2024
|
+
retryCount = 0;
|
|
2025
|
+
lifecyclePhase = "idle";
|
|
2026
|
+
lastFailureCode;
|
|
2027
|
+
lastConnectedAtMs;
|
|
2028
|
+
lastCommittedSyncCause;
|
|
2029
|
+
retryTimer;
|
|
2030
|
+
resolveRetry;
|
|
2031
|
+
constructor(realtime, config = {}, logger = {
|
|
2032
|
+
debug() {},
|
|
2033
|
+
info() {},
|
|
2034
|
+
warn() {},
|
|
2035
|
+
error() {},
|
|
2036
|
+
name: "awiki-realtime"
|
|
2037
|
+
}) {
|
|
2038
|
+
this.realtime = realtime;
|
|
2039
|
+
this.config = config;
|
|
2040
|
+
this.logger = logger;
|
|
2041
|
+
}
|
|
2042
|
+
/** Start in the background; identity activation must never await connectivity. */
|
|
2043
|
+
start() {
|
|
2044
|
+
if (this.lifecycle !== void 0 || this.stopped) return;
|
|
2045
|
+
const generation = ++this.generation;
|
|
2046
|
+
this.lifecycle = this.run(generation).catch((error) => {
|
|
2047
|
+
if (!this.stopped) this.logger.warn("AWiki realtime supervisor stopped unexpectedly: %s", error instanceof Error ? error.message : "unknown failure");
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
diagnostics() {
|
|
2051
|
+
return {
|
|
2052
|
+
connected: this.connected,
|
|
2053
|
+
activeSessionCount: this.activeSession === void 0 ? 0 : 1,
|
|
2054
|
+
startCount: this.startCount,
|
|
2055
|
+
stopCount: this.stopCount,
|
|
2056
|
+
maxActiveSessionCount: this.maxActiveSessionCount,
|
|
2057
|
+
generation: this.generation,
|
|
2058
|
+
retryCount: this.retryCount,
|
|
2059
|
+
lifecyclePhase: this.lifecyclePhase,
|
|
2060
|
+
...this.lastFailureCode === void 0 ? {} : { lastFailureCode: this.lastFailureCode },
|
|
2061
|
+
...this.lastConnectedAtMs === void 0 ? {} : { lastConnectedAtMs: this.lastConnectedAtMs },
|
|
2062
|
+
...this.lastCommittedSyncCause === void 0 ? {} : { lastCommittedSyncCause: this.lastCommittedSyncCause }
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
/** Fence late events, wake retry sleep, stop the exact session, and join the lifecycle. */
|
|
2066
|
+
async dispose() {
|
|
2067
|
+
if (this.stopped) return;
|
|
2068
|
+
this.stopped = true;
|
|
2069
|
+
this.generation += 1;
|
|
2070
|
+
this.connected = false;
|
|
2071
|
+
this.lifecyclePhase = "stopping";
|
|
2072
|
+
this.wakeRetry();
|
|
2073
|
+
const session = this.activeSession;
|
|
2074
|
+
if (session !== void 0) await this.stopSession(session).catch(() => void 0);
|
|
2075
|
+
await this.lifecycle;
|
|
2076
|
+
this.lifecyclePhase = "stopped";
|
|
2077
|
+
}
|
|
2078
|
+
current(generation) {
|
|
2079
|
+
return !this.stopped && this.generation === generation;
|
|
2080
|
+
}
|
|
2081
|
+
async run(generation) {
|
|
2082
|
+
let cause = "session_start";
|
|
2083
|
+
let failures = 0;
|
|
2084
|
+
while (this.current(generation)) {
|
|
2085
|
+
let session;
|
|
2086
|
+
try {
|
|
2087
|
+
this.lifecyclePhase = cause === "session_start" ? "initial_sync" : "reconnect_sync";
|
|
2088
|
+
await this.synchronize(cause, generation);
|
|
2089
|
+
if (!this.current(generation)) return;
|
|
2090
|
+
this.lifecyclePhase = "starting";
|
|
2091
|
+
session = await this.realtime.startRealtime();
|
|
2092
|
+
if (!this.current(generation)) {
|
|
2093
|
+
await this.stopSession(session).catch(() => void 0);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
this.activeSession = session;
|
|
2097
|
+
this.maxActiveSessionCount = 1;
|
|
2098
|
+
this.startCount += 1;
|
|
2099
|
+
this.connected = (await session.getStatus().catch(() => ({ connected: false }))).connected;
|
|
2100
|
+
this.lifecyclePhase = "connected";
|
|
2101
|
+
if (this.connected) this.lastConnectedAtMs = Date.now();
|
|
2102
|
+
this.lastFailureCode = void 0;
|
|
2103
|
+
failures = 0;
|
|
2104
|
+
this.retryCount = 0;
|
|
2105
|
+
while (this.current(generation) && this.activeSession === session) {
|
|
2106
|
+
const event = await session.nextEvent();
|
|
2107
|
+
if (!this.current(generation) || this.activeSession !== session) return;
|
|
2108
|
+
if (event === null) {
|
|
2109
|
+
await this.stopSession(session);
|
|
2110
|
+
cause = "reconnected";
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
this.observeConnection(event);
|
|
2114
|
+
if (event.kind !== "sync_required") continue;
|
|
2115
|
+
await this.synchronize(event.cause, generation);
|
|
2116
|
+
}
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
const failedPhase = this.lifecyclePhase;
|
|
2119
|
+
const safeSyncFailureCode = syncFailureCode(error);
|
|
2120
|
+
this.connected = false;
|
|
2121
|
+
if (session !== void 0) try {
|
|
2122
|
+
await this.stopSession(session);
|
|
2123
|
+
} catch {
|
|
2124
|
+
this.lastFailureCode = "stop_failed";
|
|
2125
|
+
}
|
|
2126
|
+
if (!this.current(generation)) return;
|
|
2127
|
+
failures += 1;
|
|
2128
|
+
this.retryCount = failures;
|
|
2129
|
+
if (this.lastFailureCode !== "stop_failed") this.lastFailureCode = failedPhase === "initial_sync" || failedPhase === "reconnect_sync" ? safeSyncFailureCode ?? "sync_failed" : failedPhase === "starting" ? "start_failed" : "session_failed";
|
|
2130
|
+
this.lifecyclePhase = "backoff";
|
|
2131
|
+
this.logger.warn("AWiki realtime lifecycle failed; retrying: %s", error instanceof Error ? error.message : "unknown failure");
|
|
2132
|
+
await this.waitForRetry(failures, generation);
|
|
2133
|
+
cause = cause === "session_start" ? "session_start" : "reconnected";
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
async synchronize(cause, generation) {
|
|
2138
|
+
await this.realtime.syncNow(syncReason(cause));
|
|
2139
|
+
if (!this.current(generation)) return;
|
|
2140
|
+
this.lastCommittedSyncCause = cause;
|
|
2141
|
+
try {
|
|
2142
|
+
await this.config.onSynchronized?.(cause);
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
this.logger.warn("AWiki realtime post-sync consumer failed: %s", error instanceof Error ? error.message : "unknown failure");
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
observeConnection(event) {
|
|
2148
|
+
if (event.kind !== "connection_state_changed") return;
|
|
2149
|
+
this.connected = event.state === "connected";
|
|
2150
|
+
if (this.connected) {
|
|
2151
|
+
this.lifecyclePhase = "connected";
|
|
2152
|
+
this.lastConnectedAtMs = Date.now();
|
|
2153
|
+
this.lastFailureCode = void 0;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async stopSession(session) {
|
|
2157
|
+
if (this.stoppedSessions.has(session)) return;
|
|
2158
|
+
this.stoppedSessions.add(session);
|
|
2159
|
+
if (this.activeSession === session) this.activeSession = void 0;
|
|
2160
|
+
this.connected = false;
|
|
2161
|
+
this.lifecyclePhase = "stopping";
|
|
2162
|
+
this.stopCount += 1;
|
|
2163
|
+
await session.stop();
|
|
2164
|
+
}
|
|
2165
|
+
waitForRetry(failures, generation) {
|
|
2166
|
+
if (!this.current(generation)) return Promise.resolve();
|
|
2167
|
+
const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.min(failures - 1, 10), RETRY_MAX_DELAY_MS);
|
|
2168
|
+
return new Promise((resolve) => {
|
|
2169
|
+
const finish = () => {
|
|
2170
|
+
if (this.retryTimer === timer) this.retryTimer = void 0;
|
|
2171
|
+
if (this.resolveRetry === finish) this.resolveRetry = void 0;
|
|
2172
|
+
resolve();
|
|
2173
|
+
};
|
|
2174
|
+
const timer = setTimeout(finish, delay);
|
|
2175
|
+
this.retryTimer = timer;
|
|
2176
|
+
this.resolveRetry = finish;
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
wakeRetry() {
|
|
2180
|
+
const timer = this.retryTimer;
|
|
2181
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2182
|
+
this.resolveRetry?.();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2130
2185
|
//#endregion
|
|
2131
2186
|
//#region lib/types/index.js
|
|
2132
2187
|
/** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
|
|
@@ -2198,6 +2253,7 @@ const Config = z.object({
|
|
|
2198
2253
|
attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
|
|
2199
2254
|
imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
|
|
2200
2255
|
pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
|
|
2256
|
+
realtimeEnabled: z.boolean().default(true),
|
|
2201
2257
|
listenerEnabled: z.boolean().default(false),
|
|
2202
2258
|
listenerAllowedPeers: z.array(z.string()).default([]),
|
|
2203
2259
|
listenerWorkspacePath: z.string(),
|
|
@@ -2215,7 +2271,6 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
|
2215
2271
|
"forbidden",
|
|
2216
2272
|
"identity-recovery-required",
|
|
2217
2273
|
"conflict",
|
|
2218
|
-
"state-in-use",
|
|
2219
2274
|
"rate-limited",
|
|
2220
2275
|
"group-membership-required",
|
|
2221
2276
|
"group-identity-stale",
|
|
@@ -2229,10 +2284,17 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
|
2229
2284
|
"network",
|
|
2230
2285
|
"remote"
|
|
2231
2286
|
]);
|
|
2232
|
-
const
|
|
2233
|
-
const
|
|
2234
|
-
const
|
|
2235
|
-
const
|
|
2287
|
+
const DEVICE_JOIN_APPROVAL_CONFIRMATION = "APPROVE";
|
|
2288
|
+
const DEVICE_REVOKE_CONFIRMATION = "REVOKE";
|
|
2289
|
+
const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
|
|
2290
|
+
const RESUMABLE_JOIN_PHASES = /* @__PURE__ */ new Set([
|
|
2291
|
+
"pending",
|
|
2292
|
+
"challenge_prepared",
|
|
2293
|
+
"response_prepared",
|
|
2294
|
+
"response_verified",
|
|
2295
|
+
"approval_prepared",
|
|
2296
|
+
"authorized"
|
|
2297
|
+
]);
|
|
2236
2298
|
const FAILURE_MESSAGES = {
|
|
2237
2299
|
"not-registered": "No AWiki identity is registered for this deployment.",
|
|
2238
2300
|
"signed-out": "This installation is signed out of AWiki.",
|
|
@@ -2245,7 +2307,6 @@ const FAILURE_MESSAGES = {
|
|
|
2245
2307
|
"forbidden": "The AWiki operation is not permitted.",
|
|
2246
2308
|
"identity-recovery-required": "The local AWiki identity must be recovered before it can be used again.",
|
|
2247
2309
|
"conflict": "The AWiki operation conflicts with current state.",
|
|
2248
|
-
"state-in-use": "AWiki data is already open in another client.",
|
|
2249
2310
|
"rate-limited": "The AWiki service rate-limited the request.",
|
|
2250
2311
|
"group-membership-required": "The active AWiki identity is not a member of this group.",
|
|
2251
2312
|
"group-identity-stale": "The AWiki group identity binding is still recovering.",
|
|
@@ -2261,6 +2322,39 @@ const FAILURE_MESSAGES = {
|
|
|
2261
2322
|
};
|
|
2262
2323
|
var ProviderUnavailableError = class extends Error {};
|
|
2263
2324
|
var SummaryProviderUnavailableError = class extends Error {};
|
|
2325
|
+
function candidateJoinPhase(value) {
|
|
2326
|
+
if (value.remoteState === "rejected") return "rejected";
|
|
2327
|
+
if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
|
|
2328
|
+
if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
|
|
2329
|
+
if (value.localPhase === "authorized" && value.remoteState === "consumed" && value.completed && value.identity !== void 0) return "authorized";
|
|
2330
|
+
if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
|
|
2331
|
+
if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
|
|
2332
|
+
return value.remoteState === "pending" ? "pending" : void 0;
|
|
2333
|
+
}
|
|
2334
|
+
function adminJoinPhase(value) {
|
|
2335
|
+
if (value.remoteState === "rejected") return "rejected";
|
|
2336
|
+
if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
|
|
2337
|
+
if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
|
|
2338
|
+
if (value.localPhase === "authorized" && value.remoteState === "consumed") return "authorized";
|
|
2339
|
+
if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
|
|
2340
|
+
if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
|
|
2341
|
+
return value.remoteState === "pending" ? "pending" : void 0;
|
|
2342
|
+
}
|
|
2343
|
+
function requestJoinPhase(value) {
|
|
2344
|
+
switch (value.state) {
|
|
2345
|
+
case "pending": return "pending";
|
|
2346
|
+
case "challenge_sent":
|
|
2347
|
+
case "response_verified": return "verifying";
|
|
2348
|
+
case "consumed": return "authorized";
|
|
2349
|
+
case "cancelled": return "cancelled";
|
|
2350
|
+
case "rejected": return "rejected";
|
|
2351
|
+
case "expired": return "expired";
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function constantTimeSasMatches(expected, actual) {
|
|
2355
|
+
if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual)) return false;
|
|
2356
|
+
return timingSafeEqual(Buffer.from(expected, "ascii"), Buffer.from(actual, "ascii"));
|
|
2357
|
+
}
|
|
2264
2358
|
/** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
|
|
2265
2359
|
function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
|
|
2266
2360
|
let url;
|
|
@@ -2282,6 +2376,22 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
|
|
|
2282
2376
|
if (parsed.search !== "") throw new TypeError("awiki: recovery reconciliation target must not contain a query");
|
|
2283
2377
|
return new URL("/api/identity-recovery", parsed).toString();
|
|
2284
2378
|
}
|
|
2379
|
+
/** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
|
|
2380
|
+
async function acceptsRecoveryReconciliation(response) {
|
|
2381
|
+
if (!response.ok) return false;
|
|
2382
|
+
let value;
|
|
2383
|
+
try {
|
|
2384
|
+
const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
|
|
2385
|
+
if (text === void 0) return false;
|
|
2386
|
+
value = JSON.parse(text);
|
|
2387
|
+
} catch {
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2391
|
+
const result = value;
|
|
2392
|
+
const keys = Object.keys(result).sort();
|
|
2393
|
+
return keys.length === 2 && keys[0] === "idempotent" && keys[1] === "restored" && result.restored === true && typeof result.idempotent === "boolean";
|
|
2394
|
+
}
|
|
2285
2395
|
/** Validate a provider domain without inferring it from an API endpoint. */
|
|
2286
2396
|
function serviceDomain(raw, field = "userServiceDomain") {
|
|
2287
2397
|
return normalizeAwikiDomain(raw, field);
|
|
@@ -2315,14 +2425,12 @@ function listenerAllowedPeers(raw, enabled) {
|
|
|
2315
2425
|
return peers;
|
|
2316
2426
|
}
|
|
2317
2427
|
/** Resolve and validate every deployment choice before publishing the service. */
|
|
2318
|
-
function resolveConfig(
|
|
2428
|
+
function resolveConfig(config) {
|
|
2319
2429
|
const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
|
|
2320
2430
|
const configuredStateRoot = config.stateRoot?.trim();
|
|
2321
2431
|
const configuredDshHome = process.env.DSH_HOME?.trim();
|
|
2322
2432
|
const dshHome = configuredDshHome === void 0 || configuredDshHome.length === 0 ? join(homedir(), ".dsh") : configuredDshHome;
|
|
2323
|
-
const
|
|
2324
|
-
const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiStateRoot(ctx, dshHome) : configuredStateRoot;
|
|
2325
|
-
const legacySharedStateDetected = profileName !== void 0 && existsSync(join(dshHome, "awiki", "im-core"));
|
|
2433
|
+
const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? join(dshHome, "awiki", "im-core") : configuredStateRoot;
|
|
2326
2434
|
if (!isAbsolute(stateRoot)) throw new TypeError("awiki: stateRoot must be an absolute path");
|
|
2327
2435
|
const attachmentMaxBytes = config.attachmentMaxBytes ?? 10485760;
|
|
2328
2436
|
if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) throw new TypeError("awiki: attachmentMaxBytes must be a positive safe integer");
|
|
@@ -2331,6 +2439,8 @@ function resolveConfig(ctx, config) {
|
|
|
2331
2439
|
const pollIntervalMs = config.pollIntervalMs ?? 3e3;
|
|
2332
2440
|
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1e3 || pollIntervalMs > 6e4) throw new TypeError("awiki: pollIntervalMs must be a safe integer from 1000 through 60000");
|
|
2333
2441
|
const listenerEnabled = config.listenerEnabled ?? false;
|
|
2442
|
+
const realtimeEnabled = config.realtimeEnabled ?? true;
|
|
2443
|
+
if (listenerEnabled && !realtimeEnabled) throw new TypeError("awiki: listenerEnabled requires realtimeEnabled");
|
|
2334
2444
|
const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, "workspaces", "awiki");
|
|
2335
2445
|
if (!isAbsolute(listenerWorkspacePath)) throw new TypeError("awiki: listenerWorkspacePath must be an absolute path");
|
|
2336
2446
|
const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
|
|
@@ -2353,10 +2463,7 @@ function resolveConfig(ctx, config) {
|
|
|
2353
2463
|
attachmentMaxBytes,
|
|
2354
2464
|
imageAttachmentCacheMaxBytes,
|
|
2355
2465
|
pollIntervalMs,
|
|
2356
|
-
|
|
2357
|
-
profileName,
|
|
2358
|
-
legacySharedStateDetected
|
|
2359
|
-
},
|
|
2466
|
+
realtimeEnabled,
|
|
2360
2467
|
listenerEnabled,
|
|
2361
2468
|
listener: {
|
|
2362
2469
|
allowedPeers,
|
|
@@ -2426,38 +2533,6 @@ function normalizeGroupDid(value) {
|
|
|
2426
2533
|
if (typeof value !== "string" || !value.startsWith("did:") || value.length > 2048) return void 0;
|
|
2427
2534
|
return value;
|
|
2428
2535
|
}
|
|
2429
|
-
/** Rebuild the browser-safe recovery DTO instead of trusting provider object shape. */
|
|
2430
|
-
function publicGroupRebindRecoverySummary(value) {
|
|
2431
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
2432
|
-
const candidate = value;
|
|
2433
|
-
if ([
|
|
2434
|
-
candidate.processed,
|
|
2435
|
-
candidate.completed,
|
|
2436
|
-
candidate.pending,
|
|
2437
|
-
candidate.blocked
|
|
2438
|
-
].some((count) => !Number.isSafeInteger(count) || count < 0 || count > 4294967295)) return;
|
|
2439
|
-
if (!Array.isArray(candidate.items) || candidate.items.length > 500) return void 0;
|
|
2440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2441
|
-
const items = [];
|
|
2442
|
-
for (const value of candidate.items) {
|
|
2443
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
2444
|
-
const item = value;
|
|
2445
|
-
const groupDid = normalizeGroupDid(item.groupDid);
|
|
2446
|
-
if (groupDid === void 0 || seen.has(groupDid) || item.status !== "pending" && item.status !== "blocked") return;
|
|
2447
|
-
seen.add(groupDid);
|
|
2448
|
-
items.push({
|
|
2449
|
-
groupDid,
|
|
2450
|
-
status: item.status
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
return {
|
|
2454
|
-
processed: candidate.processed,
|
|
2455
|
-
completed: candidate.completed,
|
|
2456
|
-
pending: candidate.pending,
|
|
2457
|
-
blocked: candidate.blocked,
|
|
2458
|
-
items
|
|
2459
|
-
};
|
|
2460
|
-
}
|
|
2461
2536
|
function normalizeProfileRequest(request) {
|
|
2462
2537
|
if (typeof request?.displayName !== "string" || typeof request.bio !== "string" || !Array.isArray(request.tags)) return void 0;
|
|
2463
2538
|
const displayName = request.displayName.trim();
|
|
@@ -2507,6 +2582,8 @@ function normalizeRecoveryPrepareRequest(request) {
|
|
|
2507
2582
|
};
|
|
2508
2583
|
}
|
|
2509
2584
|
const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
|
|
2585
|
+
const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
|
|
2586
|
+
const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
|
|
2510
2587
|
/** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
|
|
2511
2588
|
async function readBoundedResponseText(response, maxBytes) {
|
|
2512
2589
|
const declaredLength = response.headers.get("content-length");
|
|
@@ -2724,6 +2801,14 @@ let AwikiService = (() => {
|
|
|
2724
2801
|
let _inspectIdentityAccess_decorators;
|
|
2725
2802
|
let _sendRegistrationOtp_decorators;
|
|
2726
2803
|
let _registerIdentity_decorators;
|
|
2804
|
+
let _beginDeviceJoin_decorators;
|
|
2805
|
+
let _getDeviceJoinStatus_decorators;
|
|
2806
|
+
let _cancelDeviceJoin_decorators;
|
|
2807
|
+
let _refreshDeviceManagement_decorators;
|
|
2808
|
+
let _startDeviceJoinVerification_decorators;
|
|
2809
|
+
let _approveDeviceJoin_decorators;
|
|
2810
|
+
let _rejectDeviceJoin_decorators;
|
|
2811
|
+
let _revokeDevice_decorators;
|
|
2727
2812
|
let _updateDisplayName_decorators;
|
|
2728
2813
|
let _getProfile_decorators;
|
|
2729
2814
|
let _updateProfile_decorators;
|
|
@@ -2741,7 +2826,6 @@ let AwikiService = (() => {
|
|
|
2741
2826
|
let _listGroupMembers_decorators;
|
|
2742
2827
|
let _addGroupMember_decorators;
|
|
2743
2828
|
let _removeGroupMember_decorators;
|
|
2744
|
-
let _resumeGroupRebindRecovery_decorators;
|
|
2745
2829
|
let _getConversationPreferences_decorators;
|
|
2746
2830
|
let _updateConversationPreference_decorators;
|
|
2747
2831
|
let _listConversations_decorators;
|
|
@@ -2769,6 +2853,14 @@ let AwikiService = (() => {
|
|
|
2769
2853
|
_inspectIdentityAccess_decorators = [Remote];
|
|
2770
2854
|
_sendRegistrationOtp_decorators = [Remote];
|
|
2771
2855
|
_registerIdentity_decorators = [Remote];
|
|
2856
|
+
_beginDeviceJoin_decorators = [Remote];
|
|
2857
|
+
_getDeviceJoinStatus_decorators = [Remote];
|
|
2858
|
+
_cancelDeviceJoin_decorators = [Remote];
|
|
2859
|
+
_refreshDeviceManagement_decorators = [Remote];
|
|
2860
|
+
_startDeviceJoinVerification_decorators = [Remote];
|
|
2861
|
+
_approveDeviceJoin_decorators = [Remote];
|
|
2862
|
+
_rejectDeviceJoin_decorators = [Remote];
|
|
2863
|
+
_revokeDevice_decorators = [Remote];
|
|
2772
2864
|
_updateDisplayName_decorators = [Remote];
|
|
2773
2865
|
_getProfile_decorators = [Remote];
|
|
2774
2866
|
_updateProfile_decorators = [Remote];
|
|
@@ -2786,7 +2878,6 @@ let AwikiService = (() => {
|
|
|
2786
2878
|
_listGroupMembers_decorators = [Remote];
|
|
2787
2879
|
_addGroupMember_decorators = [Remote];
|
|
2788
2880
|
_removeGroupMember_decorators = [Remote];
|
|
2789
|
-
_resumeGroupRebindRecovery_decorators = [Remote];
|
|
2790
2881
|
_getConversationPreferences_decorators = [Remote];
|
|
2791
2882
|
_updateConversationPreference_decorators = [Remote];
|
|
2792
2883
|
_listConversations_decorators = [Remote];
|
|
@@ -2891,6 +2982,94 @@ let AwikiService = (() => {
|
|
|
2891
2982
|
},
|
|
2892
2983
|
metadata: _metadata
|
|
2893
2984
|
}, null, _instanceExtraInitializers);
|
|
2985
|
+
__esDecorate(this, null, _beginDeviceJoin_decorators, {
|
|
2986
|
+
kind: "method",
|
|
2987
|
+
name: "beginDeviceJoin",
|
|
2988
|
+
static: false,
|
|
2989
|
+
private: false,
|
|
2990
|
+
access: {
|
|
2991
|
+
has: (obj) => "beginDeviceJoin" in obj,
|
|
2992
|
+
get: (obj) => obj.beginDeviceJoin
|
|
2993
|
+
},
|
|
2994
|
+
metadata: _metadata
|
|
2995
|
+
}, null, _instanceExtraInitializers);
|
|
2996
|
+
__esDecorate(this, null, _getDeviceJoinStatus_decorators, {
|
|
2997
|
+
kind: "method",
|
|
2998
|
+
name: "getDeviceJoinStatus",
|
|
2999
|
+
static: false,
|
|
3000
|
+
private: false,
|
|
3001
|
+
access: {
|
|
3002
|
+
has: (obj) => "getDeviceJoinStatus" in obj,
|
|
3003
|
+
get: (obj) => obj.getDeviceJoinStatus
|
|
3004
|
+
},
|
|
3005
|
+
metadata: _metadata
|
|
3006
|
+
}, null, _instanceExtraInitializers);
|
|
3007
|
+
__esDecorate(this, null, _cancelDeviceJoin_decorators, {
|
|
3008
|
+
kind: "method",
|
|
3009
|
+
name: "cancelDeviceJoin",
|
|
3010
|
+
static: false,
|
|
3011
|
+
private: false,
|
|
3012
|
+
access: {
|
|
3013
|
+
has: (obj) => "cancelDeviceJoin" in obj,
|
|
3014
|
+
get: (obj) => obj.cancelDeviceJoin
|
|
3015
|
+
},
|
|
3016
|
+
metadata: _metadata
|
|
3017
|
+
}, null, _instanceExtraInitializers);
|
|
3018
|
+
__esDecorate(this, null, _refreshDeviceManagement_decorators, {
|
|
3019
|
+
kind: "method",
|
|
3020
|
+
name: "refreshDeviceManagement",
|
|
3021
|
+
static: false,
|
|
3022
|
+
private: false,
|
|
3023
|
+
access: {
|
|
3024
|
+
has: (obj) => "refreshDeviceManagement" in obj,
|
|
3025
|
+
get: (obj) => obj.refreshDeviceManagement
|
|
3026
|
+
},
|
|
3027
|
+
metadata: _metadata
|
|
3028
|
+
}, null, _instanceExtraInitializers);
|
|
3029
|
+
__esDecorate(this, null, _startDeviceJoinVerification_decorators, {
|
|
3030
|
+
kind: "method",
|
|
3031
|
+
name: "startDeviceJoinVerification",
|
|
3032
|
+
static: false,
|
|
3033
|
+
private: false,
|
|
3034
|
+
access: {
|
|
3035
|
+
has: (obj) => "startDeviceJoinVerification" in obj,
|
|
3036
|
+
get: (obj) => obj.startDeviceJoinVerification
|
|
3037
|
+
},
|
|
3038
|
+
metadata: _metadata
|
|
3039
|
+
}, null, _instanceExtraInitializers);
|
|
3040
|
+
__esDecorate(this, null, _approveDeviceJoin_decorators, {
|
|
3041
|
+
kind: "method",
|
|
3042
|
+
name: "approveDeviceJoin",
|
|
3043
|
+
static: false,
|
|
3044
|
+
private: false,
|
|
3045
|
+
access: {
|
|
3046
|
+
has: (obj) => "approveDeviceJoin" in obj,
|
|
3047
|
+
get: (obj) => obj.approveDeviceJoin
|
|
3048
|
+
},
|
|
3049
|
+
metadata: _metadata
|
|
3050
|
+
}, null, _instanceExtraInitializers);
|
|
3051
|
+
__esDecorate(this, null, _rejectDeviceJoin_decorators, {
|
|
3052
|
+
kind: "method",
|
|
3053
|
+
name: "rejectDeviceJoin",
|
|
3054
|
+
static: false,
|
|
3055
|
+
private: false,
|
|
3056
|
+
access: {
|
|
3057
|
+
has: (obj) => "rejectDeviceJoin" in obj,
|
|
3058
|
+
get: (obj) => obj.rejectDeviceJoin
|
|
3059
|
+
},
|
|
3060
|
+
metadata: _metadata
|
|
3061
|
+
}, null, _instanceExtraInitializers);
|
|
3062
|
+
__esDecorate(this, null, _revokeDevice_decorators, {
|
|
3063
|
+
kind: "method",
|
|
3064
|
+
name: "revokeDevice",
|
|
3065
|
+
static: false,
|
|
3066
|
+
private: false,
|
|
3067
|
+
access: {
|
|
3068
|
+
has: (obj) => "revokeDevice" in obj,
|
|
3069
|
+
get: (obj) => obj.revokeDevice
|
|
3070
|
+
},
|
|
3071
|
+
metadata: _metadata
|
|
3072
|
+
}, null, _instanceExtraInitializers);
|
|
2894
3073
|
__esDecorate(this, null, _updateDisplayName_decorators, {
|
|
2895
3074
|
kind: "method",
|
|
2896
3075
|
name: "updateDisplayName",
|
|
@@ -3078,17 +3257,6 @@ let AwikiService = (() => {
|
|
|
3078
3257
|
},
|
|
3079
3258
|
metadata: _metadata
|
|
3080
3259
|
}, null, _instanceExtraInitializers);
|
|
3081
|
-
__esDecorate(this, null, _resumeGroupRebindRecovery_decorators, {
|
|
3082
|
-
kind: "method",
|
|
3083
|
-
name: "resumeGroupRebindRecovery",
|
|
3084
|
-
static: false,
|
|
3085
|
-
private: false,
|
|
3086
|
-
access: {
|
|
3087
|
-
has: (obj) => "resumeGroupRebindRecovery" in obj,
|
|
3088
|
-
get: (obj) => obj.resumeGroupRebindRecovery
|
|
3089
|
-
},
|
|
3090
|
-
metadata: _metadata
|
|
3091
|
-
}, null, _instanceExtraInitializers);
|
|
3092
3260
|
__esDecorate(this, null, _getConversationPreferences_decorators, {
|
|
3093
3261
|
kind: "method",
|
|
3094
3262
|
name: "getConversationPreferences",
|
|
@@ -3286,6 +3454,12 @@ let AwikiService = (() => {
|
|
|
3286
3454
|
sessionMutation = Promise.resolve();
|
|
3287
3455
|
sessionRevision = 0;
|
|
3288
3456
|
activeIdentityDid;
|
|
3457
|
+
pendingDeviceJoin;
|
|
3458
|
+
activeDeviceJoinSessionId;
|
|
3459
|
+
requestRefs = /* @__PURE__ */ new Map();
|
|
3460
|
+
requestSessions = /* @__PURE__ */ new Map();
|
|
3461
|
+
deviceRefs = /* @__PURE__ */ new Map();
|
|
3462
|
+
deviceIds = /* @__PURE__ */ new Map();
|
|
3289
3463
|
activeSummaryRequests = /* @__PURE__ */ new Set();
|
|
3290
3464
|
summaryProvider;
|
|
3291
3465
|
recoveryReconciliationTarget;
|
|
@@ -3300,7 +3474,7 @@ let AwikiService = (() => {
|
|
|
3300
3474
|
constructor(ctx, config) {
|
|
3301
3475
|
super(ctx, "awiki");
|
|
3302
3476
|
this.hostContext = ctx;
|
|
3303
|
-
this.resolved = resolveConfig(
|
|
3477
|
+
this.resolved = resolveConfig(config);
|
|
3304
3478
|
this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
|
|
3305
3479
|
this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
|
|
3306
3480
|
this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
|
|
@@ -3329,12 +3503,12 @@ let AwikiService = (() => {
|
|
|
3329
3503
|
ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
|
|
3330
3504
|
this.workspaceContext = workspaceCtx;
|
|
3331
3505
|
const provider = this.provider;
|
|
3332
|
-
if (provider !== void 0) this.
|
|
3506
|
+
if (provider !== void 0) this.ensureAgentConsumer(provider);
|
|
3333
3507
|
workspaceCtx.effect(() => async () => {
|
|
3334
3508
|
if (this.workspaceContext !== workspaceCtx) return;
|
|
3335
3509
|
this.workspaceContext = void 0;
|
|
3336
3510
|
const current = this.provider;
|
|
3337
|
-
if (current !== void 0) await this.
|
|
3511
|
+
if (current !== void 0) await this.stopAgentConsumer(current);
|
|
3338
3512
|
}, "awiki: release Workspace listener composition");
|
|
3339
3513
|
});
|
|
3340
3514
|
registerAwikiTools(ctx, this);
|
|
@@ -3355,6 +3529,8 @@ let AwikiService = (() => {
|
|
|
3355
3529
|
*/
|
|
3356
3530
|
registerClientFactory(factory) {
|
|
3357
3531
|
if (this.provider !== void 0) throw new Error("awiki: a client provider is already registered");
|
|
3532
|
+
this.pendingDeviceJoin = void 0;
|
|
3533
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
3358
3534
|
const provider = {
|
|
3359
3535
|
client: factory({
|
|
3360
3536
|
userServiceUrl: this.resolved.userServiceUrl,
|
|
@@ -3368,14 +3544,32 @@ let AwikiService = (() => {
|
|
|
3368
3544
|
allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
|
|
3369
3545
|
stateRoot: this.resolved.stateRoot
|
|
3370
3546
|
}),
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3547
|
+
realtimeGeneration: 0,
|
|
3548
|
+
agentConsumerGeneration: 0,
|
|
3549
|
+
localDeviceJoinRequestCountAfterSync: 0
|
|
3374
3550
|
};
|
|
3375
3551
|
this.provider = provider;
|
|
3376
|
-
this.
|
|
3552
|
+
this.ensureRealtimeSupervisor(provider);
|
|
3553
|
+
this.ensureAgentConsumer(provider);
|
|
3377
3554
|
return () => this.disposeProvider(provider);
|
|
3378
3555
|
}
|
|
3556
|
+
/** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
|
|
3557
|
+
getRealtimeDiagnostics() {
|
|
3558
|
+
const provider = this.provider;
|
|
3559
|
+
return {
|
|
3560
|
+
...provider?.realtimeSupervisor?.diagnostics() ?? {
|
|
3561
|
+
connected: false,
|
|
3562
|
+
activeSessionCount: 0,
|
|
3563
|
+
startCount: 0,
|
|
3564
|
+
stopCount: 0,
|
|
3565
|
+
maxActiveSessionCount: 0,
|
|
3566
|
+
generation: 0,
|
|
3567
|
+
retryCount: 0,
|
|
3568
|
+
lifecyclePhase: "idle"
|
|
3569
|
+
},
|
|
3570
|
+
localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3379
3573
|
/** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
|
|
3380
3574
|
registerRecoveryReconciliationTarget(target) {
|
|
3381
3575
|
if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
|
|
@@ -3403,18 +3597,42 @@ let AwikiService = (() => {
|
|
|
3403
3597
|
* Read settings needed by the browser presentation.
|
|
3404
3598
|
* @returns Browser-safe polling configuration without SDK endpoints or state paths.
|
|
3405
3599
|
*/
|
|
3406
|
-
getConfig() {
|
|
3407
|
-
|
|
3600
|
+
async getConfig() {
|
|
3601
|
+
let handleRecoveryPhoneEnabled = false;
|
|
3602
|
+
const abort = new AbortController();
|
|
3603
|
+
const timeout = setTimeout(() => {
|
|
3604
|
+
abort.abort();
|
|
3605
|
+
}, 1e4);
|
|
3606
|
+
try {
|
|
3607
|
+
const response = await fetch(new URL("/user-service/v1/server-info", this.resolved.userServiceUrl), {
|
|
3608
|
+
method: "GET",
|
|
3609
|
+
headers: {
|
|
3610
|
+
accept: "application/json",
|
|
3611
|
+
"cache-control": "no-store"
|
|
3612
|
+
},
|
|
3613
|
+
cache: "no-store",
|
|
3614
|
+
redirect: "error",
|
|
3615
|
+
signal: abort.signal
|
|
3616
|
+
});
|
|
3617
|
+
if (response.ok) {
|
|
3618
|
+
const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
|
|
3619
|
+
const value = text === void 0 ? void 0 : JSON.parse(text);
|
|
3620
|
+
if (typeof value === "object" && value !== null && value.schema_version === 1) {
|
|
3621
|
+
const methods = value.identity?.handle_recovery?.methods;
|
|
3622
|
+
handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => typeof method === "object" && method !== null && method.id === "phone" && method.enabled === true && method.verification?.required === true && method.verification?.type === "sms_otp");
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
} catch {} finally {
|
|
3626
|
+
clearTimeout(timeout);
|
|
3627
|
+
}
|
|
3628
|
+
return {
|
|
3408
3629
|
ok: true,
|
|
3409
3630
|
value: {
|
|
3410
3631
|
pollIntervalMs: this.resolved.pollIntervalMs,
|
|
3411
3632
|
attachmentMaxBytes: this.resolved.attachmentMaxBytes,
|
|
3412
|
-
|
|
3413
|
-
profileName: this.resolved.profileName,
|
|
3414
|
-
legacySharedStateDetected: this.resolved.legacySharedStateDetected
|
|
3415
|
-
}
|
|
3633
|
+
handleRecoveryPhoneEnabled
|
|
3416
3634
|
}
|
|
3417
|
-
}
|
|
3635
|
+
};
|
|
3418
3636
|
}
|
|
3419
3637
|
/**
|
|
3420
3638
|
* Read the deployment's identity status.
|
|
@@ -3437,6 +3655,8 @@ let AwikiService = (() => {
|
|
|
3437
3655
|
const identity = await this.run((client) => client.getIdentity(), { allowSignedOut: true });
|
|
3438
3656
|
if (!identity.ok) return identity;
|
|
3439
3657
|
this.activeIdentityDid = identity.value?.did;
|
|
3658
|
+
const provider = this.provider;
|
|
3659
|
+
if (identity.value !== null && provider !== void 0) this.ensureProviderRuntime(provider);
|
|
3440
3660
|
return identity.value === null ? {
|
|
3441
3661
|
ok: true,
|
|
3442
3662
|
value: { status: "unregistered" }
|
|
@@ -3473,7 +3693,7 @@ let AwikiService = (() => {
|
|
|
3473
3693
|
const session = { status: "signed-out" };
|
|
3474
3694
|
this.publishSession(session);
|
|
3475
3695
|
const provider = this.provider;
|
|
3476
|
-
if (provider !== void 0) await this.
|
|
3696
|
+
if (provider !== void 0) await this.stopProviderRuntime(provider);
|
|
3477
3697
|
return {
|
|
3478
3698
|
ok: true,
|
|
3479
3699
|
value: session
|
|
@@ -3506,7 +3726,7 @@ let AwikiService = (() => {
|
|
|
3506
3726
|
};
|
|
3507
3727
|
this.publishSession(session);
|
|
3508
3728
|
const provider = this.provider;
|
|
3509
|
-
if (provider !== void 0) this.
|
|
3729
|
+
if (provider !== void 0) this.ensureProviderRuntime(provider);
|
|
3510
3730
|
return {
|
|
3511
3731
|
ok: true,
|
|
3512
3732
|
value: session
|
|
@@ -3592,8 +3812,15 @@ let AwikiService = (() => {
|
|
|
3592
3812
|
};
|
|
3593
3813
|
}
|
|
3594
3814
|
}
|
|
3595
|
-
sendRegistrationOtp(request) {
|
|
3596
|
-
|
|
3815
|
+
async sendRegistrationOtp(request) {
|
|
3816
|
+
this.pendingDeviceJoin = void 0;
|
|
3817
|
+
return this.run(async (client) => {
|
|
3818
|
+
if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
|
|
3819
|
+
name: "AwikiSdkError",
|
|
3820
|
+
code: "conflict"
|
|
3821
|
+
});
|
|
3822
|
+
return client.sendRegistrationOtp(request);
|
|
3823
|
+
});
|
|
3597
3824
|
}
|
|
3598
3825
|
/**
|
|
3599
3826
|
* Register and persist the deployment's only AWiki identity.
|
|
@@ -3601,9 +3828,190 @@ let AwikiService = (() => {
|
|
|
3601
3828
|
* @returns The new public identity or a closed failure.
|
|
3602
3829
|
*/
|
|
3603
3830
|
async registerIdentity(request) {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3831
|
+
this.pendingDeviceJoin = void 0;
|
|
3832
|
+
const result = await this.run(async (client) => {
|
|
3833
|
+
if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
|
|
3834
|
+
name: "AwikiSdkError",
|
|
3835
|
+
code: "conflict"
|
|
3836
|
+
});
|
|
3837
|
+
return client.registerIdentity(request);
|
|
3838
|
+
});
|
|
3839
|
+
if (!result.ok) return result;
|
|
3840
|
+
if (result.value.status === "registered") {
|
|
3841
|
+
await this.activateRegisteredIdentity(result.value.identity);
|
|
3842
|
+
return {
|
|
3843
|
+
ok: true,
|
|
3844
|
+
value: {
|
|
3845
|
+
status: "registered",
|
|
3846
|
+
identity: result.value.identity
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
this.pendingDeviceJoin = {
|
|
3851
|
+
continuationId: result.value.continuationId,
|
|
3852
|
+
fullHandle: result.value.fullHandle,
|
|
3853
|
+
mode: result.value.mode,
|
|
3854
|
+
requiresUserPresence: result.value.requiresUserPresence
|
|
3855
|
+
};
|
|
3856
|
+
return {
|
|
3857
|
+
ok: true,
|
|
3858
|
+
value: {
|
|
3859
|
+
status: "join-required",
|
|
3860
|
+
fullHandle: result.value.fullHandle,
|
|
3861
|
+
mode: result.value.mode,
|
|
3862
|
+
requiresUserPresence: result.value.requiresUserPresence
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
/** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
|
|
3867
|
+
async beginDeviceJoin() {
|
|
3868
|
+
const continuation = this.pendingDeviceJoin;
|
|
3869
|
+
if (continuation === void 0) return {
|
|
3870
|
+
ok: false,
|
|
3871
|
+
error: failure("conflict")
|
|
3872
|
+
};
|
|
3873
|
+
if (continuation.mode !== "ordinary" || continuation.requiresUserPresence) return {
|
|
3874
|
+
ok: false,
|
|
3875
|
+
error: failure("forbidden")
|
|
3876
|
+
};
|
|
3877
|
+
this.pendingDeviceJoin = void 0;
|
|
3878
|
+
const result = await this.run((client) => client.beginDeviceJoin({
|
|
3879
|
+
continuationId: continuation.continuationId,
|
|
3880
|
+
operationId: `dsh-device-join-${randomUUID()}`,
|
|
3881
|
+
userPresenceConfirmed: false
|
|
3882
|
+
}));
|
|
3883
|
+
if (!result.ok) return result;
|
|
3884
|
+
this.activeDeviceJoinSessionId = result.value.joinSessionId;
|
|
3885
|
+
return this.applyCandidateJoinProgress(result.value);
|
|
3886
|
+
}
|
|
3887
|
+
/** Restore from Core local_sessions and advance only the exact resumable Join. */
|
|
3888
|
+
async getDeviceJoinStatus() {
|
|
3889
|
+
const result = await this.run(async (client) => {
|
|
3890
|
+
const joinSessionId = await this.selectDeviceJoinSession(client);
|
|
3891
|
+
return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
|
|
3892
|
+
});
|
|
3893
|
+
if (!result.ok) return result;
|
|
3894
|
+
if (result.value === null) return {
|
|
3895
|
+
ok: true,
|
|
3896
|
+
value: null
|
|
3897
|
+
};
|
|
3898
|
+
this.activeDeviceJoinSessionId = result.value.joinSessionId;
|
|
3899
|
+
return this.applyCandidateJoinProgress(result.value);
|
|
3900
|
+
}
|
|
3901
|
+
async cancelDeviceJoin() {
|
|
3902
|
+
const result = await this.run(async (client) => {
|
|
3903
|
+
const joinSessionId = await this.selectDeviceJoinSession(client);
|
|
3904
|
+
if (joinSessionId === null) return null;
|
|
3905
|
+
return client.cancelDeviceJoin(joinSessionId);
|
|
3906
|
+
});
|
|
3907
|
+
if (!result.ok) return result;
|
|
3908
|
+
this.pendingDeviceJoin = void 0;
|
|
3909
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
3910
|
+
return {
|
|
3911
|
+
ok: true,
|
|
3912
|
+
value: { completed: true }
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
/** Reliable-sync and project only Host-opaque device/request references. */
|
|
3916
|
+
refreshDeviceManagement() {
|
|
3917
|
+
return this.run((client) => this.deviceManagementSnapshot(client));
|
|
3918
|
+
}
|
|
3919
|
+
async startDeviceJoinVerification(request) {
|
|
3920
|
+
const joinSessionId = this.requestSessions.get(request?.requestRef);
|
|
3921
|
+
if (joinSessionId === void 0) return {
|
|
3922
|
+
ok: false,
|
|
3923
|
+
error: failure("invalid-request")
|
|
3924
|
+
};
|
|
3925
|
+
const result = await this.run(async (client) => {
|
|
3926
|
+
await this.requireDeviceManager(client);
|
|
3927
|
+
await client.syncDeviceManagement();
|
|
3928
|
+
let notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
|
|
3929
|
+
if (notice === void 0) throw Object.assign(/* @__PURE__ */ new Error("request not found"), {
|
|
3930
|
+
name: "AwikiSdkError",
|
|
3931
|
+
code: "not-found"
|
|
3932
|
+
});
|
|
3933
|
+
const localProgress = await this.localAdminJoinProgress(client, notice);
|
|
3934
|
+
if (localProgress !== void 0) return localProgress;
|
|
3935
|
+
if (!notice.canStartVerification) throw Object.assign(/* @__PURE__ */ new Error("request unavailable"), {
|
|
3936
|
+
name: "AwikiSdkError",
|
|
3937
|
+
code: "forbidden"
|
|
3938
|
+
});
|
|
3939
|
+
const operationId = `dsh-device-verify-${createHash("sha256").update(joinSessionId).digest("hex").slice(0, 32)}`;
|
|
3940
|
+
let started;
|
|
3941
|
+
try {
|
|
3942
|
+
started = await client.startDeviceJoinVerification({
|
|
3943
|
+
joinSessionId,
|
|
3944
|
+
operationId,
|
|
3945
|
+
challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS
|
|
3946
|
+
});
|
|
3947
|
+
} catch (error) {
|
|
3948
|
+
await client.syncDeviceManagement();
|
|
3949
|
+
notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
|
|
3950
|
+
const recovered = notice === void 0 ? void 0 : await this.localAdminJoinProgress(client, notice);
|
|
3951
|
+
if (recovered !== void 0) return recovered;
|
|
3952
|
+
throw error;
|
|
3953
|
+
}
|
|
3954
|
+
await client.syncDeviceManagement();
|
|
3955
|
+
notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
|
|
3956
|
+
if (notice === void 0) return started;
|
|
3957
|
+
return await this.localAdminJoinProgress(client, notice) ?? started;
|
|
3958
|
+
});
|
|
3959
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
3960
|
+
}
|
|
3961
|
+
async approveDeviceJoin(request) {
|
|
3962
|
+
if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) return {
|
|
3963
|
+
ok: false,
|
|
3964
|
+
error: failure("invalid-request")
|
|
3965
|
+
};
|
|
3966
|
+
const joinSessionId = this.requestSessions.get(request.requestRef);
|
|
3967
|
+
if (joinSessionId === void 0 || !/^\d{6}$/u.test(request.enteredSas)) return {
|
|
3968
|
+
ok: false,
|
|
3969
|
+
error: failure("invalid-request")
|
|
3970
|
+
};
|
|
3971
|
+
const result = await this.run(async (client) => {
|
|
3972
|
+
await this.requireDeviceManager(client);
|
|
3973
|
+
const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
|
|
3974
|
+
if (progress.sas === void 0 || !constantTimeSasMatches(progress.sas, request.enteredSas)) throw Object.assign(/* @__PURE__ */ new Error("sas mismatch"), {
|
|
3975
|
+
name: "AwikiSdkError",
|
|
3976
|
+
code: "forbidden"
|
|
3977
|
+
});
|
|
3978
|
+
const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
|
|
3979
|
+
return client.confirmDeviceJoinApproval(prompt.approvalHandle);
|
|
3980
|
+
});
|
|
3981
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
3982
|
+
}
|
|
3983
|
+
async rejectDeviceJoin(request) {
|
|
3984
|
+
const joinSessionId = this.requestSessions.get(request?.requestRef);
|
|
3985
|
+
if (joinSessionId === void 0 || request.reason !== "user_rejected" && request.reason !== "sas_mismatch") return {
|
|
3986
|
+
ok: false,
|
|
3987
|
+
error: failure("invalid-request")
|
|
3988
|
+
};
|
|
3989
|
+
const result = await this.run(async (client) => {
|
|
3990
|
+
await this.requireDeviceManager(client);
|
|
3991
|
+
return client.rejectDeviceJoin(joinSessionId, request.reason);
|
|
3992
|
+
});
|
|
3993
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
3994
|
+
}
|
|
3995
|
+
async revokeDevice(request) {
|
|
3996
|
+
if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) return {
|
|
3997
|
+
ok: false,
|
|
3998
|
+
error: failure("invalid-request")
|
|
3999
|
+
};
|
|
4000
|
+
const deviceId = this.deviceIds.get(request.deviceRef);
|
|
4001
|
+
if (deviceId === void 0) return {
|
|
4002
|
+
ok: false,
|
|
4003
|
+
error: failure("invalid-request")
|
|
4004
|
+
};
|
|
4005
|
+
return this.run(async (client) => {
|
|
4006
|
+
await this.requireDeviceManager(client);
|
|
4007
|
+
const target = (await client.getDeviceRegistry()).find((device) => device.deviceId === deviceId);
|
|
4008
|
+
if (target === void 0 || target.isCurrent) throw Object.assign(/* @__PURE__ */ new Error("device unavailable"), {
|
|
4009
|
+
name: "AwikiSdkError",
|
|
4010
|
+
code: "forbidden"
|
|
4011
|
+
});
|
|
4012
|
+
await client.revokeDevice(deviceId);
|
|
4013
|
+
return this.deviceManagementSnapshot(client);
|
|
4014
|
+
});
|
|
3607
4015
|
}
|
|
3608
4016
|
/**
|
|
3609
4017
|
* Update the deployment identity's public WNS display name.
|
|
@@ -3630,12 +4038,19 @@ let AwikiService = (() => {
|
|
|
3630
4038
|
}
|
|
3631
4039
|
/** Start durable phone recovery for one existing full Handle. */
|
|
3632
4040
|
sendRecoveryOtp(request) {
|
|
4041
|
+
this.pendingDeviceJoin = void 0;
|
|
3633
4042
|
const normalized = normalizeRecoveryOtpRequest(request);
|
|
3634
4043
|
if (normalized === void 0) return Promise.resolve({
|
|
3635
4044
|
ok: false,
|
|
3636
4045
|
error: failure("invalid-request")
|
|
3637
4046
|
});
|
|
3638
|
-
return this.run((client) =>
|
|
4047
|
+
return this.run(async (client) => {
|
|
4048
|
+
if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
|
|
4049
|
+
name: "AwikiSdkError",
|
|
4050
|
+
code: "conflict"
|
|
4051
|
+
});
|
|
4052
|
+
return client.sendRecoveryOtp(normalized);
|
|
4053
|
+
}, { allowSignedOut: true });
|
|
3639
4054
|
}
|
|
3640
4055
|
/** Verify a recovery OTP and freeze its exact intent before the remote commit. */
|
|
3641
4056
|
prepareRecovery(request) {
|
|
@@ -3805,19 +4220,6 @@ let AwikiService = (() => {
|
|
|
3805
4220
|
});
|
|
3806
4221
|
return this.run((client) => client.removeGroupMember(groupDid, member));
|
|
3807
4222
|
}
|
|
3808
|
-
/** Resume durable group membership convergence after account sync restores old groups. */
|
|
3809
|
-
async resumeGroupRebindRecovery() {
|
|
3810
|
-
const result = await this.run((client) => client.resumeGroupRebindRecovery());
|
|
3811
|
-
if (!result.ok) return result;
|
|
3812
|
-
const summary = publicGroupRebindRecoverySummary(result.value);
|
|
3813
|
-
return summary === void 0 ? {
|
|
3814
|
-
ok: false,
|
|
3815
|
-
error: failure("remote")
|
|
3816
|
-
} : {
|
|
3817
|
-
ok: true,
|
|
3818
|
-
value: summary
|
|
3819
|
-
};
|
|
3820
|
-
}
|
|
3821
4223
|
/** Read presentation-only roster preferences for the active identity. */
|
|
3822
4224
|
getConversationPreferences() {
|
|
3823
4225
|
return this.run(async (client) => this.conversationPreferenceStore.get(await this.ownerDid(client)));
|
|
@@ -4147,10 +4549,10 @@ let AwikiService = (() => {
|
|
|
4147
4549
|
});
|
|
4148
4550
|
return this.mutateSession(async () => {
|
|
4149
4551
|
const provider = this.provider;
|
|
4150
|
-
if (provider !== void 0) await this.
|
|
4552
|
+
if (provider !== void 0) await this.stopProviderRuntime(provider);
|
|
4151
4553
|
const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
|
|
4152
4554
|
if (!result.ok) {
|
|
4153
|
-
if (provider !== void 0 && this.provider === provider) this.
|
|
4555
|
+
if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
|
|
4154
4556
|
return result;
|
|
4155
4557
|
}
|
|
4156
4558
|
try {
|
|
@@ -4160,9 +4562,10 @@ let AwikiService = (() => {
|
|
|
4160
4562
|
await this.sessionStore.signIn();
|
|
4161
4563
|
this.signedOut = false;
|
|
4162
4564
|
this.activeIdentityDid = void 0;
|
|
4565
|
+
this.pendingDeviceJoin = void 0;
|
|
4566
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
4163
4567
|
this.invalidateSummaries();
|
|
4164
4568
|
this.publishSession({ status: "unregistered" });
|
|
4165
|
-
if (provider !== void 0 && this.provider === provider) this.startListener(provider);
|
|
4166
4569
|
return result;
|
|
4167
4570
|
} catch {
|
|
4168
4571
|
return {
|
|
@@ -4172,7 +4575,7 @@ let AwikiService = (() => {
|
|
|
4172
4575
|
}
|
|
4173
4576
|
});
|
|
4174
4577
|
}
|
|
4175
|
-
/** Re-enter after Core
|
|
4578
|
+
/** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
|
|
4176
4579
|
async applyRecoveredSession(progress) {
|
|
4177
4580
|
if (progress.phase !== "applied") return true;
|
|
4178
4581
|
return this.mutateSession(async () => {
|
|
@@ -4188,6 +4591,7 @@ let AwikiService = (() => {
|
|
|
4188
4591
|
this.activeIdentityDid = identity.did;
|
|
4189
4592
|
this.invalidateSummaries();
|
|
4190
4593
|
}
|
|
4594
|
+
const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
|
|
4191
4595
|
if (this.provider !== provider) return false;
|
|
4192
4596
|
if (!alreadyActive) {
|
|
4193
4597
|
const session = {
|
|
@@ -4195,31 +4599,195 @@ let AwikiService = (() => {
|
|
|
4195
4599
|
identity
|
|
4196
4600
|
};
|
|
4197
4601
|
this.publishSession(session);
|
|
4198
|
-
|
|
4199
|
-
await this.startListener(provider);
|
|
4602
|
+
this.ensureProviderRuntime(provider);
|
|
4200
4603
|
}
|
|
4201
|
-
return
|
|
4604
|
+
return reconciled;
|
|
4202
4605
|
} catch {
|
|
4203
4606
|
return false;
|
|
4204
4607
|
}
|
|
4205
4608
|
});
|
|
4206
4609
|
}
|
|
4207
|
-
/**
|
|
4610
|
+
/** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
|
|
4611
|
+
async reconcileRecoveredIdentity(provider, operationId) {
|
|
4612
|
+
const logger = this.ctx.logger("awiki-recovery");
|
|
4613
|
+
let mailboxRestored = false;
|
|
4614
|
+
try {
|
|
4615
|
+
await provider.client.getMailAccount();
|
|
4616
|
+
mailboxRestored = true;
|
|
4617
|
+
} catch {
|
|
4618
|
+
logger.warn("awiki: recovered mailbox reconciliation is pending");
|
|
4619
|
+
}
|
|
4620
|
+
const target = this.recoveryReconciliationTarget;
|
|
4621
|
+
if (target === void 0) return mailboxRestored;
|
|
4622
|
+
try {
|
|
4623
|
+
const authority = await provider.client.issueRecoveryAttestation({ operationId });
|
|
4624
|
+
if (!await acceptsRecoveryReconciliation(await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
|
|
4625
|
+
method: "POST",
|
|
4626
|
+
headers: { "content-type": "application/json" },
|
|
4627
|
+
body: JSON.stringify({ attestation: authority.attestation })
|
|
4628
|
+
}), (request) => fetch(request)))) {
|
|
4629
|
+
logger.warn("awiki: recovered model account reconciliation is pending");
|
|
4630
|
+
return false;
|
|
4631
|
+
}
|
|
4632
|
+
return mailboxRestored;
|
|
4633
|
+
} catch {
|
|
4634
|
+
logger.warn("awiki: recovered model account reconciliation is pending");
|
|
4635
|
+
return false;
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
/** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
|
|
4639
|
+
async selectDeviceJoinSession(client) {
|
|
4640
|
+
const sessions = await client.listLocalDeviceJoinSessions();
|
|
4641
|
+
if (this.activeDeviceJoinSessionId !== void 0) {
|
|
4642
|
+
const tracked = sessions.find((value) => value.side === "new_device" && value.joinSessionId === this.activeDeviceJoinSessionId);
|
|
4643
|
+
if (tracked !== void 0) return tracked.joinSessionId;
|
|
4644
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
4645
|
+
}
|
|
4646
|
+
const resumable = sessions.filter((value) => value.side === "new_device" && RESUMABLE_JOIN_PHASES.has(value.localPhase));
|
|
4647
|
+
if (resumable.length > 1) throw Object.assign(/* @__PURE__ */ new Error("multiple local joins"), {
|
|
4648
|
+
name: "AwikiSdkError",
|
|
4649
|
+
code: "conflict"
|
|
4650
|
+
});
|
|
4651
|
+
const selected = resumable[0];
|
|
4652
|
+
this.activeDeviceJoinSessionId = selected?.joinSessionId;
|
|
4653
|
+
return selected?.joinSessionId ?? null;
|
|
4654
|
+
}
|
|
4655
|
+
async applyCandidateJoinProgress(value) {
|
|
4656
|
+
const phase = candidateJoinPhase(value);
|
|
4657
|
+
if (phase === void 0) return {
|
|
4658
|
+
ok: false,
|
|
4659
|
+
error: failure("remote")
|
|
4660
|
+
};
|
|
4661
|
+
if (phase === "cancelled" || phase === "rejected" || phase === "expired") this.activeDeviceJoinSessionId = void 0;
|
|
4662
|
+
if (phase === "authorized") {
|
|
4663
|
+
if (value.identity === void 0) return {
|
|
4664
|
+
ok: false,
|
|
4665
|
+
error: failure("remote")
|
|
4666
|
+
};
|
|
4667
|
+
await this.activateRegisteredIdentity(value.identity);
|
|
4668
|
+
}
|
|
4669
|
+
return {
|
|
4670
|
+
ok: true,
|
|
4671
|
+
value: {
|
|
4672
|
+
phase,
|
|
4673
|
+
expiresAt: value.expiresAt,
|
|
4674
|
+
...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {},
|
|
4675
|
+
completed: phase === "authorized"
|
|
4676
|
+
}
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
publicAdminJoinProgress(requestRef, value) {
|
|
4680
|
+
const phase = adminJoinPhase(value);
|
|
4681
|
+
if (phase === void 0) return {
|
|
4682
|
+
ok: false,
|
|
4683
|
+
error: failure("remote")
|
|
4684
|
+
};
|
|
4685
|
+
return {
|
|
4686
|
+
ok: true,
|
|
4687
|
+
value: {
|
|
4688
|
+
requestRef,
|
|
4689
|
+
phase,
|
|
4690
|
+
expiresAt: value.expiresAt,
|
|
4691
|
+
...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {}
|
|
4692
|
+
}
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
async localAdminJoinProgress(client, notice) {
|
|
4696
|
+
const local = (await client.listLocalDeviceJoinSessions()).find((session) => session.side === "admin" && session.joinSessionId === notice.joinSessionId);
|
|
4697
|
+
if (local === void 0) return notice.claimedByCurrentDevice ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId) : void 0;
|
|
4698
|
+
if (local.localPhase === "challenge_prepared") return {
|
|
4699
|
+
joinSessionId: notice.joinSessionId,
|
|
4700
|
+
localPhase: local.localPhase,
|
|
4701
|
+
remoteState: notice.state,
|
|
4702
|
+
expiresAt: local.expiresAt
|
|
4703
|
+
};
|
|
4704
|
+
return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
|
|
4705
|
+
}
|
|
4706
|
+
async requireDeviceManager(client) {
|
|
4707
|
+
const current = await client.getCurrentDeviceSummary();
|
|
4708
|
+
if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") throw Object.assign(/* @__PURE__ */ new Error("device manager required"), {
|
|
4709
|
+
name: "AwikiSdkError",
|
|
4710
|
+
code: "forbidden"
|
|
4711
|
+
});
|
|
4712
|
+
}
|
|
4713
|
+
requestRef(joinSessionId) {
|
|
4714
|
+
const existing = this.requestRefs.get(joinSessionId);
|
|
4715
|
+
if (existing !== void 0) return existing;
|
|
4716
|
+
const reference = `join-${randomUUID()}`;
|
|
4717
|
+
this.requestRefs.set(joinSessionId, reference);
|
|
4718
|
+
this.requestSessions.set(reference, joinSessionId);
|
|
4719
|
+
return reference;
|
|
4720
|
+
}
|
|
4721
|
+
deviceRef(deviceId) {
|
|
4722
|
+
const existing = this.deviceRefs.get(deviceId);
|
|
4723
|
+
if (existing !== void 0) return existing;
|
|
4724
|
+
const reference = `device-${randomUUID()}`;
|
|
4725
|
+
this.deviceRefs.set(deviceId, reference);
|
|
4726
|
+
this.deviceIds.set(reference, deviceId);
|
|
4727
|
+
return reference;
|
|
4728
|
+
}
|
|
4729
|
+
async deviceManagementSnapshot(client) {
|
|
4730
|
+
await client.syncDeviceManagement();
|
|
4731
|
+
const current = await client.getCurrentDeviceSummary();
|
|
4732
|
+
if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") {
|
|
4733
|
+
this.requestRefs.clear();
|
|
4734
|
+
this.requestSessions.clear();
|
|
4735
|
+
this.deviceRefs.clear();
|
|
4736
|
+
this.deviceIds.clear();
|
|
4737
|
+
return {
|
|
4738
|
+
canManage: false,
|
|
4739
|
+
...current.role === void 0 ? {} : { role: current.role },
|
|
4740
|
+
readiness: current.readiness,
|
|
4741
|
+
devices: [],
|
|
4742
|
+
requests: []
|
|
4743
|
+
};
|
|
4744
|
+
}
|
|
4745
|
+
const requests = await client.listLocalDeviceJoinRequests();
|
|
4746
|
+
await Promise.all(requests.filter((request) => request.claimedByCurrentDevice).map((request) => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => void 0)));
|
|
4747
|
+
return {
|
|
4748
|
+
canManage: true,
|
|
4749
|
+
role: "admin",
|
|
4750
|
+
readiness: "admin_ready",
|
|
4751
|
+
devices: (await client.getDeviceRegistry()).map((device) => this.publicDevice(device)),
|
|
4752
|
+
requests: requests.map((request) => ({
|
|
4753
|
+
requestRef: this.requestRef(request.joinSessionId),
|
|
4754
|
+
candidateKeyFingerprint: request.candidateKeyFingerprint,
|
|
4755
|
+
issuedAt: request.issuedAt,
|
|
4756
|
+
expiresAt: request.expiresAt,
|
|
4757
|
+
state: requestJoinPhase(request),
|
|
4758
|
+
claimedByCurrentDevice: request.claimedByCurrentDevice,
|
|
4759
|
+
canStartVerification: request.canStartVerification
|
|
4760
|
+
}))
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
publicDevice(device) {
|
|
4764
|
+
return {
|
|
4765
|
+
deviceRef: this.deviceRef(device.deviceId),
|
|
4766
|
+
status: device.status,
|
|
4767
|
+
role: device.role,
|
|
4768
|
+
managementReady: device.managementReady,
|
|
4769
|
+
isCurrent: device.isCurrent
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
/** Publish one newly registered identity, then start realtime only in the background. */
|
|
4208
4773
|
async activateRegisteredIdentity(identity) {
|
|
4774
|
+
this.pendingDeviceJoin = void 0;
|
|
4775
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
4209
4776
|
this.activeIdentityDid = identity.did;
|
|
4210
4777
|
this.publishSession({
|
|
4211
4778
|
status: "active",
|
|
4212
4779
|
identity
|
|
4213
4780
|
});
|
|
4214
4781
|
const provider = this.provider;
|
|
4215
|
-
if (provider !== void 0)
|
|
4216
|
-
await provider.listenerStartup;
|
|
4217
|
-
await this.startListener(provider);
|
|
4218
|
-
}
|
|
4782
|
+
if (provider !== void 0) this.ensureProviderRuntime(provider);
|
|
4219
4783
|
}
|
|
4220
4784
|
/** Invalidate cached session work and cancel every model request still owned by the old session. */
|
|
4221
4785
|
invalidateSummaries() {
|
|
4222
4786
|
this.sessionRevision += 1;
|
|
4787
|
+
this.requestRefs.clear();
|
|
4788
|
+
this.requestSessions.clear();
|
|
4789
|
+
this.deviceRefs.clear();
|
|
4790
|
+
this.deviceIds.clear();
|
|
4223
4791
|
for (const controller of this.activeSummaryRequests) controller.abort();
|
|
4224
4792
|
this.activeSummaryRequests.clear();
|
|
4225
4793
|
}
|
|
@@ -4319,120 +4887,148 @@ let AwikiService = (() => {
|
|
|
4319
4887
|
this.sessionMutation = pending.then(() => void 0, () => void 0);
|
|
4320
4888
|
return pending;
|
|
4321
4889
|
}
|
|
4322
|
-
/** Start
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
if (provider.
|
|
4326
|
-
if (
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4890
|
+
/** Start identity realtime and the optional Agent consumer without blocking identity success. */
|
|
4891
|
+
ensureProviderRuntime(provider) {
|
|
4892
|
+
const identityDid = this.activeIdentityDid;
|
|
4893
|
+
if (provider.runtimeReplacement !== void 0) return;
|
|
4894
|
+
if (identityDid !== void 0 && provider.realtimeIdentityDid !== void 0 && provider.realtimeIdentityDid !== identityDid) {
|
|
4895
|
+
this.replaceProviderRuntime(provider, identityDid);
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
this.ensureRealtimeSupervisor(provider);
|
|
4899
|
+
this.ensureAgentConsumer(provider);
|
|
4900
|
+
}
|
|
4901
|
+
replaceProviderRuntime(provider, identityDid) {
|
|
4902
|
+
if (provider.runtimeReplacement !== void 0 || this.provider !== provider) return;
|
|
4903
|
+
const observed = (async () => {
|
|
4904
|
+
await this.stopProviderRuntime(provider);
|
|
4905
|
+
if (this.provider !== provider || this.activeIdentityDid !== identityDid) return;
|
|
4906
|
+
this.ensureRealtimeSupervisor(provider);
|
|
4907
|
+
this.ensureAgentConsumer(provider);
|
|
4908
|
+
})().catch((error) => {
|
|
4909
|
+
this.ctx.logger("awiki-realtime").warn("AWiki identity realtime replacement failed: %s", error instanceof Error ? error.message : "unknown failure");
|
|
4910
|
+
}).finally(() => {
|
|
4911
|
+
if (provider.runtimeReplacement === observed) delete provider.runtimeReplacement;
|
|
4912
|
+
if (this.provider === provider) this.ensureProviderRuntime(provider);
|
|
4913
|
+
});
|
|
4914
|
+
provider.runtimeReplacement = observed;
|
|
4915
|
+
}
|
|
4916
|
+
ensureRealtimeSupervisor(provider) {
|
|
4917
|
+
if (!this.resolved.realtimeEnabled || provider.realtimeSupervisor !== void 0 || provider.realtimeStartup !== void 0 || this.provider !== provider || (provider.client.realtime ?? provider.client.listener) === void 0) return;
|
|
4918
|
+
const generation = provider.realtimeGeneration;
|
|
4919
|
+
const source = provider.client.realtime ?? provider.client.listener;
|
|
4920
|
+
if (source === void 0) return;
|
|
4921
|
+
const logger = this.ctx.logger("awiki-realtime");
|
|
4332
4922
|
const observed = (async () => {
|
|
4333
4923
|
if (await this.isSignedOut()) return;
|
|
4334
|
-
|
|
4335
|
-
if (!this.
|
|
4336
|
-
|
|
4337
|
-
const
|
|
4338
|
-
provider.
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
if (provider.listener === listener) delete provider.listener;
|
|
4343
|
-
try {
|
|
4344
|
-
await listener.dispose();
|
|
4345
|
-
} catch {
|
|
4346
|
-
provider.listenerRecoveryBlocked = true;
|
|
4347
|
-
}
|
|
4348
|
-
if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
4349
|
-
throw error;
|
|
4350
|
-
}
|
|
4351
|
-
if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
|
|
4352
|
-
if (provider.listener === listener) delete provider.listener;
|
|
4353
|
-
try {
|
|
4354
|
-
await listener.dispose();
|
|
4355
|
-
} catch (error) {
|
|
4356
|
-
provider.listenerRecoveryBlocked = true;
|
|
4357
|
-
throw error;
|
|
4358
|
-
}
|
|
4359
|
-
return;
|
|
4360
|
-
}
|
|
4361
|
-
provider.listenerStartedAt = Date.now();
|
|
4362
|
-
listener.whenTerminated().then((result) => {
|
|
4363
|
-
if (result.kind === "failed") this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
|
|
4364
|
-
});
|
|
4924
|
+
const identity = await provider.client.getIdentity();
|
|
4925
|
+
if (identity === null || !this.realtimeFenceMatches(provider, generation)) return;
|
|
4926
|
+
this.activeIdentityDid = identity.did;
|
|
4927
|
+
const supervisor = new IdentityRealtimeSupervisor(source, { onSynchronized: (cause) => this.onRealtimeSynchronized(provider, generation, cause) }, logger);
|
|
4928
|
+
provider.realtimeSupervisor = supervisor;
|
|
4929
|
+
provider.realtimeIdentityDid = identity.did;
|
|
4930
|
+
supervisor.start();
|
|
4931
|
+
this.ensureAgentConsumer(provider);
|
|
4365
4932
|
})().catch((error) => {
|
|
4366
|
-
logger.warn("AWiki
|
|
4933
|
+
logger.warn("AWiki realtime startup failed: %s", error instanceof Error ? error.message : "unknown failure");
|
|
4367
4934
|
}).finally(() => {
|
|
4368
|
-
if (provider.
|
|
4935
|
+
if (provider.realtimeStartup === observed) delete provider.realtimeStartup;
|
|
4369
4936
|
});
|
|
4370
|
-
provider.
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
provider.listenerGeneration += 1;
|
|
4375
|
-
provider.listenerRestartAttempt = 0;
|
|
4376
|
-
delete provider.listenerStartedAt;
|
|
4377
|
-
const timer = provider.listenerRestartTimer;
|
|
4378
|
-
if (timer !== void 0) {
|
|
4379
|
-
delete provider.listenerRestartTimer;
|
|
4380
|
-
clearTimeout(timer);
|
|
4381
|
-
}
|
|
4382
|
-
await provider.listenerStartup;
|
|
4383
|
-
await provider.listenerCleanup;
|
|
4384
|
-
const listener = provider.listener;
|
|
4385
|
-
if (listener === void 0) return;
|
|
4386
|
-
delete provider.listener;
|
|
4387
|
-
try {
|
|
4388
|
-
await listener.dispose();
|
|
4389
|
-
} catch (error) {
|
|
4390
|
-
provider.listenerRecoveryBlocked = true;
|
|
4391
|
-
throw error;
|
|
4392
|
-
}
|
|
4937
|
+
provider.realtimeStartup = observed;
|
|
4938
|
+
}
|
|
4939
|
+
realtimeFenceMatches(provider, generation) {
|
|
4940
|
+
return this.provider === provider && provider.realtimeGeneration === generation;
|
|
4393
4941
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4942
|
+
async onRealtimeSynchronized(provider, generation, cause) {
|
|
4943
|
+
if (!this.realtimeFenceMatches(provider, generation)) return;
|
|
4944
|
+
if (cause === "system_notification" || cause === "stream_recovery") try {
|
|
4945
|
+
provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
|
|
4946
|
+
} catch {
|
|
4947
|
+
this.ctx.logger("awiki-realtime").debug("AWiki realtime could not observe local device Join requests");
|
|
4948
|
+
}
|
|
4949
|
+
if (![
|
|
4950
|
+
"session_start",
|
|
4951
|
+
"connection_ready",
|
|
4952
|
+
"reconnected",
|
|
4953
|
+
"message",
|
|
4954
|
+
"message_update"
|
|
4955
|
+
].includes(cause)) return;
|
|
4956
|
+
this.ensureAgentConsumer(provider);
|
|
4957
|
+
await provider.agentConsumerStartup;
|
|
4958
|
+
if (!this.realtimeFenceMatches(provider, generation)) return;
|
|
4959
|
+
await provider.agentConsumer?.reconcileOnce();
|
|
4396
4960
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
const
|
|
4401
|
-
|
|
4402
|
-
if (
|
|
4961
|
+
ensureAgentConsumer(provider) {
|
|
4962
|
+
const workspaceContext = this.workspaceContext;
|
|
4963
|
+
const source = provider.client.agentInbox ?? provider.client.listener;
|
|
4964
|
+
const identityDid = this.activeIdentityDid;
|
|
4965
|
+
if (!this.resolved.listenerEnabled || workspaceContext === void 0 || source === void 0 || identityDid === void 0 || this.provider !== provider) return;
|
|
4966
|
+
if (provider.agentConsumer !== void 0 && provider.agentConsumerIdentityDid === identityDid) return;
|
|
4967
|
+
if (provider.agentConsumerStartup !== void 0) return;
|
|
4968
|
+
const generation = ++provider.agentConsumerGeneration;
|
|
4403
4969
|
const logger = this.ctx.logger("awiki-listener");
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4970
|
+
const observed = (async () => {
|
|
4971
|
+
await this.detachAgentConsumer(provider);
|
|
4972
|
+
if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation)) return;
|
|
4973
|
+
const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
|
|
4974
|
+
const consumer = new AwikiAgentListener(source, agents, {
|
|
4975
|
+
...this.resolved.listener,
|
|
4976
|
+
identityScope: identityDid
|
|
4977
|
+
}, logger);
|
|
4978
|
+
provider.agentConsumer = consumer;
|
|
4979
|
+
provider.agentConsumerIdentityDid = identityDid;
|
|
4980
|
+
await consumer.reconcileOnce();
|
|
4981
|
+
})().catch((error) => {
|
|
4982
|
+
logger.warn("AWiki Agent consumer startup failed: %s", error instanceof Error ? error.message : "unknown failure");
|
|
4411
4983
|
}).finally(() => {
|
|
4412
|
-
if (provider.
|
|
4413
|
-
|
|
4984
|
+
if (provider.agentConsumerStartup === observed) delete provider.agentConsumerStartup;
|
|
4985
|
+
});
|
|
4986
|
+
provider.agentConsumerStartup = observed;
|
|
4987
|
+
}
|
|
4988
|
+
agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
|
|
4989
|
+
return this.provider === provider && this.workspaceContext === workspaceContext && this.activeIdentityDid === identityDid && provider.agentConsumerGeneration === generation;
|
|
4990
|
+
}
|
|
4991
|
+
async detachAgentConsumer(provider) {
|
|
4992
|
+
const consumer = provider.agentConsumer;
|
|
4993
|
+
if (consumer === void 0) return;
|
|
4994
|
+
delete provider.agentConsumer;
|
|
4995
|
+
delete provider.agentConsumerIdentityDid;
|
|
4996
|
+
const observed = consumer.dispose().finally(() => {
|
|
4997
|
+
if (provider.agentConsumerCleanup === observed) delete provider.agentConsumerCleanup;
|
|
4414
4998
|
});
|
|
4415
|
-
provider.
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
provider.
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4999
|
+
provider.agentConsumerCleanup = observed;
|
|
5000
|
+
await observed;
|
|
5001
|
+
}
|
|
5002
|
+
async stopAgentConsumer(provider) {
|
|
5003
|
+
provider.agentConsumerGeneration += 1;
|
|
5004
|
+
await provider.agentConsumerStartup;
|
|
5005
|
+
await provider.agentConsumerCleanup;
|
|
5006
|
+
await this.detachAgentConsumer(provider);
|
|
5007
|
+
}
|
|
5008
|
+
async stopRealtimeSupervisor(provider) {
|
|
5009
|
+
provider.realtimeGeneration += 1;
|
|
5010
|
+
await provider.realtimeStartup;
|
|
5011
|
+
const supervisor = provider.realtimeSupervisor;
|
|
5012
|
+
if (supervisor === void 0) return;
|
|
5013
|
+
delete provider.realtimeSupervisor;
|
|
5014
|
+
delete provider.realtimeIdentityDid;
|
|
5015
|
+
await supervisor.dispose();
|
|
5016
|
+
}
|
|
5017
|
+
async stopProviderRuntime(provider) {
|
|
5018
|
+
await Promise.all([this.stopRealtimeSupervisor(provider), this.stopAgentConsumer(provider)]);
|
|
4429
5019
|
}
|
|
4430
5020
|
/** Clear one exact provider slot before joining its one shared disposal. */
|
|
4431
5021
|
disposeProvider(provider) {
|
|
4432
|
-
if (this.provider === provider)
|
|
5022
|
+
if (this.provider === provider) {
|
|
5023
|
+
this.provider = void 0;
|
|
5024
|
+
this.pendingDeviceJoin = void 0;
|
|
5025
|
+
this.activeDeviceJoinSessionId = void 0;
|
|
5026
|
+
this.invalidateSummaries();
|
|
5027
|
+
}
|
|
4433
5028
|
provider.disposal ??= (async () => {
|
|
4434
5029
|
try {
|
|
4435
|
-
await
|
|
5030
|
+
await provider.runtimeReplacement;
|
|
5031
|
+
await this.stopProviderRuntime(provider);
|
|
4436
5032
|
} finally {
|
|
4437
5033
|
await provider.client.dispose();
|
|
4438
5034
|
}
|