@awiki/dsh-plugin 0.3.7-dsh-test.20260831.2 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +66 -26
  2. package/README.zh.md +57 -23
  3. package/cordis.patch.yml +7 -5
  4. package/lib/builtin-tenants.json +22 -0
  5. package/lib/builtin-tenants.sha256 +1 -0
  6. package/lib/client.js +7123 -1635
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +2534 -474
  9. package/lib/model-proxy-contract.js +11 -3
  10. package/lib/provider.js +6 -3
  11. package/lib/{sdk-adapter-FGaOi8It.mjs → sdk-adapter-CYcJH7sM.mjs} +210 -21
  12. package/lib/typert.host.js +1305 -106
  13. package/lib/typert.remote-client.d.ts +25 -1
  14. package/lib/typert.remote-client.d.ts.map +1 -1
  15. package/lib/typert.remote-client.js +1305 -106
  16. package/lib/types/builtin-tenant-config.d.ts +19 -0
  17. package/lib/types/builtin-tenant-config.d.ts.map +1 -0
  18. package/lib/types/builtin-tenant-config.js +57 -0
  19. package/lib/types/builtin-tenant-config.js.map +1 -0
  20. package/lib/types/client/AwikiDeviceJoinReminder.d.ts +15 -0
  21. package/lib/types/client/AwikiDeviceJoinReminder.d.ts.map +1 -0
  22. package/lib/types/client/AwikiDeviceJoinReminder.js +74 -0
  23. package/lib/types/client/AwikiDeviceJoinReminder.js.map +1 -0
  24. package/lib/types/client/AwikiDevices.d.ts +8 -3
  25. package/lib/types/client/AwikiDevices.d.ts.map +1 -1
  26. package/lib/types/client/AwikiDevices.js +64 -8
  27. package/lib/types/client/AwikiDevices.js.map +1 -1
  28. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
  29. package/lib/types/client/AwikiGroupDetails.js +16 -13
  30. package/lib/types/client/AwikiGroupDetails.js.map +1 -1
  31. package/lib/types/client/AwikiIdentityAccess.d.ts +10 -4
  32. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  33. package/lib/types/client/AwikiIdentityAccess.js +139 -36
  34. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  35. package/lib/types/client/AwikiIntegrationSettings.d.ts +21 -0
  36. package/lib/types/client/AwikiIntegrationSettings.d.ts.map +1 -0
  37. package/lib/types/client/AwikiIntegrationSettings.js +105 -0
  38. package/lib/types/client/AwikiIntegrationSettings.js.map +1 -0
  39. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  40. package/lib/types/client/AwikiMail.js +12 -8
  41. package/lib/types/client/AwikiMail.js.map +1 -1
  42. package/lib/types/client/AwikiOverlay.d.ts +0 -1
  43. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  44. package/lib/types/client/AwikiOverlay.js +26 -21
  45. package/lib/types/client/AwikiOverlay.js.map +1 -1
  46. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
  47. package/lib/types/client/AwikiProfileCard.js +17 -18
  48. package/lib/types/client/AwikiProfileCard.js.map +1 -1
  49. package/lib/types/client/AwikiRecoveryForm.d.ts +5 -1
  50. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  51. package/lib/types/client/AwikiRecoveryForm.js +73 -30
  52. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  53. package/lib/types/client/AwikiSettingsSection.d.ts +28 -13
  54. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  55. package/lib/types/client/AwikiSettingsSection.js +93 -61
  56. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  57. package/lib/types/client/AwikiUpdates.d.ts +10 -0
  58. package/lib/types/client/AwikiUpdates.d.ts.map +1 -0
  59. package/lib/types/client/AwikiUpdates.js +57 -0
  60. package/lib/types/client/AwikiUpdates.js.map +1 -0
  61. package/lib/types/client/awiki-client-bridge.d.ts +1 -1
  62. package/lib/types/client/awiki-client-bridge.d.ts.map +1 -1
  63. package/lib/types/client/awiki-client-bridge.js +4 -1
  64. package/lib/types/client/awiki-client-bridge.js.map +1 -1
  65. package/lib/types/client/controller.d.ts +65 -11
  66. package/lib/types/client/controller.d.ts.map +1 -1
  67. package/lib/types/client/controller.js +480 -127
  68. package/lib/types/client/controller.js.map +1 -1
  69. package/lib/types/client/drafts.d.ts +31 -0
  70. package/lib/types/client/drafts.d.ts.map +1 -0
  71. package/lib/types/client/drafts.js +94 -0
  72. package/lib/types/client/drafts.js.map +1 -0
  73. package/lib/types/client/image-cache.d.ts +2 -2
  74. package/lib/types/client/image-cache.d.ts.map +1 -1
  75. package/lib/types/client/image-cache.js +15 -2
  76. package/lib/types/client/image-cache.js.map +1 -1
  77. package/lib/types/client/index.d.ts.map +1 -1
  78. package/lib/types/client/index.js +55 -15
  79. package/lib/types/client/index.js.map +1 -1
  80. package/lib/types/client/integration-operation.d.ts +9 -0
  81. package/lib/types/client/integration-operation.d.ts.map +1 -0
  82. package/lib/types/client/integration-operation.js +54 -0
  83. package/lib/types/client/integration-operation.js.map +1 -0
  84. package/lib/types/client/mail-list-cache.d.ts +2 -0
  85. package/lib/types/client/mail-list-cache.d.ts.map +1 -1
  86. package/lib/types/client/mail-list-cache.js +18 -0
  87. package/lib/types/client/mail-list-cache.js.map +1 -1
  88. package/lib/types/client/otp-cooldown.d.ts +6 -0
  89. package/lib/types/client/otp-cooldown.d.ts.map +1 -0
  90. package/lib/types/client/otp-cooldown.js +18 -0
  91. package/lib/types/client/otp-cooldown.js.map +1 -0
  92. package/lib/types/client/settings-controller.d.ts +34 -0
  93. package/lib/types/client/settings-controller.d.ts.map +1 -1
  94. package/lib/types/client/settings-controller.js +182 -2
  95. package/lib/types/client/settings-controller.js.map +1 -1
  96. package/lib/types/client/settings-locales.d.ts +1 -1
  97. package/lib/types/client/settings-locales.d.ts.map +1 -1
  98. package/lib/types/client/settings-locales.js +184 -6
  99. package/lib/types/client/settings-locales.js.map +1 -1
  100. package/lib/types/client/slots.d.ts +11 -1
  101. package/lib/types/client/slots.d.ts.map +1 -1
  102. package/lib/types/client/slots.js +0 -1
  103. package/lib/types/client/slots.js.map +1 -1
  104. package/lib/types/desktop-distribution.d.ts +25 -0
  105. package/lib/types/desktop-distribution.d.ts.map +1 -0
  106. package/lib/types/desktop-distribution.js +57 -0
  107. package/lib/types/desktop-distribution.js.map +1 -0
  108. package/lib/types/domain.d.ts +1 -1
  109. package/lib/types/domain.d.ts.map +1 -1
  110. package/lib/types/domain.js +2 -1
  111. package/lib/types/domain.js.map +1 -1
  112. package/lib/types/index.d.ts +112 -18
  113. package/lib/types/index.d.ts.map +1 -1
  114. package/lib/types/index.js +897 -205
  115. package/lib/types/index.js.map +1 -1
  116. package/lib/types/integration-client.d.ts +17 -0
  117. package/lib/types/integration-client.d.ts.map +1 -0
  118. package/lib/types/integration-client.js +233 -0
  119. package/lib/types/integration-client.js.map +1 -0
  120. package/lib/types/legacy-sent-mail-cache.d.ts +4 -0
  121. package/lib/types/legacy-sent-mail-cache.d.ts.map +1 -0
  122. package/lib/types/legacy-sent-mail-cache.js +29 -0
  123. package/lib/types/legacy-sent-mail-cache.js.map +1 -0
  124. package/lib/types/mail-list-client.d.ts +12 -0
  125. package/lib/types/mail-list-client.d.ts.map +1 -0
  126. package/lib/types/mail-list-client.js +254 -0
  127. package/lib/types/mail-list-client.js.map +1 -0
  128. package/lib/types/mail-recovery-observability.d.ts +8 -0
  129. package/lib/types/mail-recovery-observability.d.ts.map +1 -0
  130. package/lib/types/mail-recovery-observability.js +119 -0
  131. package/lib/types/mail-recovery-observability.js.map +1 -0
  132. package/lib/types/model-proxy-contract.d.ts.map +1 -1
  133. package/lib/types/model-proxy-contract.js +7 -0
  134. package/lib/types/model-proxy-contract.js.map +1 -1
  135. package/lib/types/package-version.generated.d.ts +4 -0
  136. package/lib/types/package-version.generated.d.ts.map +1 -0
  137. package/lib/types/package-version.generated.js +4 -0
  138. package/lib/types/package-version.generated.js.map +1 -0
  139. package/lib/types/profile-state.d.ts +6 -0
  140. package/lib/types/profile-state.d.ts.map +1 -0
  141. package/lib/types/profile-state.js +41 -0
  142. package/lib/types/profile-state.js.map +1 -0
  143. package/lib/types/provider-api.d.ts +36 -9
  144. package/lib/types/provider-api.d.ts.map +1 -1
  145. package/lib/types/provider.d.ts.map +1 -1
  146. package/lib/types/provider.js +5 -2
  147. package/lib/types/provider.js.map +1 -1
  148. package/lib/types/realtime-supervisor.d.ts +6 -0
  149. package/lib/types/realtime-supervisor.d.ts.map +1 -1
  150. package/lib/types/realtime-supervisor.js +14 -1
  151. package/lib/types/realtime-supervisor.js.map +1 -1
  152. package/lib/types/sdk-adapter.d.ts +32 -6
  153. package/lib/types/sdk-adapter.d.ts.map +1 -1
  154. package/lib/types/sdk-adapter.js +127 -24
  155. package/lib/types/sdk-adapter.js.map +1 -1
  156. package/lib/types/settings-rpc-contract.d.ts +58 -0
  157. package/lib/types/settings-rpc-contract.d.ts.map +1 -1
  158. package/lib/types/settings-rpc-contract.js +103 -0
  159. package/lib/types/settings-rpc-contract.js.map +1 -1
  160. package/lib/types/settings-rpc.d.ts +14 -1
  161. package/lib/types/settings-rpc.d.ts.map +1 -1
  162. package/lib/types/settings-rpc.js +130 -1
  163. package/lib/types/settings-rpc.js.map +1 -1
  164. package/lib/types/tenant-registry.d.ts +74 -0
  165. package/lib/types/tenant-registry.d.ts.map +1 -0
  166. package/lib/types/tenant-registry.js +440 -0
  167. package/lib/types/tenant-registry.js.map +1 -0
  168. package/lib/types/types.d.ts +173 -1
  169. package/lib/types/types.d.ts.map +1 -1
  170. package/lib/types/types.js.map +1 -1
  171. package/lib/types/update-policy.d.ts +51 -0
  172. package/lib/types/update-policy.d.ts.map +1 -0
  173. package/lib/types/update-policy.js +379 -0
  174. package/lib/types/update-policy.js.map +1 -0
  175. package/lib/types/version.d.ts +4 -0
  176. package/lib/types/version.d.ts.map +1 -0
  177. package/lib/types/version.js +44 -0
  178. package/lib/types/version.js.map +1 -0
  179. package/package.json +34 -21
  180. package/lib/types/sent-mail-store.d.ts +0 -22
  181. package/lib/types/sent-mail-store.d.ts.map +0 -1
  182. package/lib/types/sent-mail-store.js +0 -246
  183. package/lib/types/sent-mail-store.js.map +0 -1
package/lib/index.js CHANGED
@@ -1,16 +1,107 @@
1
- import { n as downloadedAttachment } from "./sdk-adapter-FGaOi8It.mjs";
1
+ import { a as fencedMailRecoveryObservability, i as failedMailRecoveryObservability, o as successfulMailRecoveryObservability, r as downloadedAttachment, t as AwikiSdkError } from "./sdk-adapter-CYcJH7sM.mjs";
2
2
  import "@deepseek-ai/cordis";
3
3
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
- import { isAbsolute, join } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, resolve } 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
- import { defineTool } from "@deepseek-ai/dsh-tools";
10
9
  import { chmod, lstat, mkdir, readFile, readdir, rename, rm, unlink, utimes, writeFile } from "node:fs/promises";
10
+ import { defineTool } from "@deepseek-ai/dsh-tools";
11
11
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
12
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
13
13
  import { SessionId } from "@deepseek-ai/dsh-session";
14
+ import { fileURLToPath } from "node:url";
15
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
16
+ import { satisfies } from "semver";
17
+ //#region lib/types/version.js
18
+ /** Shared strict version comparison for update presentation and Host gates. */
19
+ function assertVersion(value) {
20
+ if (value.length > 256 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(value)) throw new Error("invalid semantic version");
21
+ }
22
+ function compareVersions(left, right) {
23
+ assertVersion(left);
24
+ assertVersion(right);
25
+ const parse = (value) => {
26
+ const withoutBuild = value.split("+", 1)[0];
27
+ const separator = withoutBuild.indexOf("-");
28
+ const core = (separator < 0 ? withoutBuild : withoutBuild.slice(0, separator)).split(".").map(BigInt);
29
+ return separator < 0 ? { core } : {
30
+ core,
31
+ prerelease: withoutBuild.slice(separator + 1).split(".")
32
+ };
33
+ };
34
+ const a = parse(left);
35
+ const b = parse(right);
36
+ for (let index = 0; index < 3; index += 1) {
37
+ const difference = a.core[index] - b.core[index];
38
+ if (difference !== 0n) return difference < 0n ? -1 : 1;
39
+ }
40
+ if (a.prerelease === void 0 || b.prerelease === void 0) return a.prerelease === b.prerelease ? 0 : a.prerelease === void 0 ? 1 : -1;
41
+ for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index += 1) {
42
+ const leftPart = a.prerelease[index];
43
+ const rightPart = b.prerelease[index];
44
+ if (leftPart === rightPart) continue;
45
+ const leftNumeric = /^\d+$/u.test(leftPart);
46
+ if (leftNumeric !== /^\d+$/u.test(rightPart)) return leftNumeric ? -1 : 1;
47
+ if (leftNumeric) return BigInt(leftPart) < BigInt(rightPart) ? -1 : 1;
48
+ return leftPart < rightPart ? -1 : 1;
49
+ }
50
+ if (a.prerelease.length !== b.prerelease.length) return a.prerelease.length < b.prerelease.length ? -1 : 1;
51
+ return 0;
52
+ }
53
+ //#endregion
54
+ //#region lib/types/desktop-distribution.js
55
+ function decodeDesktopDistribution(value) {
56
+ if (typeof value !== "object" || value === null) return void 0;
57
+ const view = value;
58
+ if (view.schemaVersion !== 1 || view.distributionId !== "awiki-dsh-desktop" || typeof view.currentVersion !== "string" || view.channel !== "stable" && view.channel !== "prerelease" || typeof view.downloadPageUrl !== "string" || ![
59
+ "unchecked",
60
+ "checking",
61
+ "ready",
62
+ "failed"
63
+ ].includes(String(view.state)) || typeof view.updateAvailable !== "boolean" || typeof view.usedCache !== "boolean" || view.latestVersion !== void 0 && typeof view.latestVersion !== "string" || view.checkedAt !== void 0 && typeof view.checkedAt !== "string" || view.noRelease !== void 0 && typeof view.noRelease !== "boolean") return void 0;
64
+ try {
65
+ assertVersion(view.currentVersion);
66
+ if (view.latestVersion !== void 0) assertVersion(view.latestVersion);
67
+ if (view.checkedAt !== void 0 && !Number.isFinite(Date.parse(view.checkedAt))) return void 0;
68
+ const url = new URL(view.downloadPageUrl);
69
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return void 0;
70
+ } catch {
71
+ return;
72
+ }
73
+ let bundledVersions;
74
+ if (view.bundledVersions !== void 0) {
75
+ if (typeof view.bundledVersions !== "object" || view.bundledVersions === null) return void 0;
76
+ const bundled = view.bundledVersions;
77
+ if (typeof bundled.plugin !== "string" || bundled.modelProxy !== void 0 && typeof bundled.modelProxy !== "string") return void 0;
78
+ try {
79
+ assertVersion(bundled.plugin);
80
+ if (bundled.modelProxy !== void 0) assertVersion(bundled.modelProxy);
81
+ } catch {
82
+ return;
83
+ }
84
+ bundledVersions = {
85
+ plugin: bundled.plugin,
86
+ ...typeof bundled.modelProxy === "string" ? { modelProxy: bundled.modelProxy } : {}
87
+ };
88
+ }
89
+ return {
90
+ schemaVersion: 1,
91
+ distributionId: "awiki-dsh-desktop",
92
+ currentVersion: view.currentVersion,
93
+ channel: view.channel,
94
+ downloadPageUrl: view.downloadPageUrl,
95
+ state: view.state,
96
+ updateAvailable: view.updateAvailable,
97
+ usedCache: view.usedCache,
98
+ ...typeof view.latestVersion === "string" ? { latestVersion: view.latestVersion } : {},
99
+ ...typeof view.checkedAt === "string" ? { checkedAt: view.checkedAt } : {},
100
+ ...typeof view.noRelease === "boolean" ? { noRelease: view.noRelease } : {},
101
+ ...bundledVersions === void 0 ? {} : { bundledVersions }
102
+ };
103
+ }
104
+ //#endregion
14
105
  //#region lib/types/types.js
15
106
  /** Client-safe AWiki service and Remote data types. */
16
107
  /** Exact browser acknowledgement required before locally signing out. */
@@ -190,6 +281,511 @@ function responseMetadata(response) {
190
281
  };
191
282
  }
192
283
  //#endregion
284
+ //#region lib/types/integration-client.js
285
+ /** Host-only fixed-scope client for Guest Integration management. */
286
+ const MAX_REQUEST_BYTES = 65536;
287
+ const MAX_RESPONSE_BYTES$1 = 1048576;
288
+ const REQUEST_TIMEOUT_MS = 1e4;
289
+ const INTEGRATION_STATUSES = ["active", "closed"];
290
+ const TARGET_AVAILABILITIES = [
291
+ "eligible",
292
+ "group_not_found",
293
+ "owner_not_active",
294
+ "owner_mismatch",
295
+ "not_open_join",
296
+ "unsupported_security_profile",
297
+ "member_send_disabled",
298
+ "attachments_disabled",
299
+ "group_full",
300
+ "validation_unavailable"
301
+ ];
302
+ const OPERATIONS = {
303
+ read: {
304
+ method: "GET",
305
+ path: "/guest/api/v1/management/integration"
306
+ },
307
+ create: {
308
+ method: "POST",
309
+ path: "/guest/api/v1/management/integration"
310
+ },
311
+ update: {
312
+ method: "PATCH",
313
+ path: "/guest/api/v1/management/integration"
314
+ },
315
+ rotate: {
316
+ method: "POST",
317
+ path: "/guest/api/v1/management/integration/rotate-id"
318
+ },
319
+ close: {
320
+ method: "POST",
321
+ path: "/guest/api/v1/management/integration/close"
322
+ },
323
+ reopen: {
324
+ method: "POST",
325
+ path: "/guest/api/v1/management/integration/reopen"
326
+ }
327
+ };
328
+ /** Browser-safe management input translated to the Gateway's snake-case contract. */
329
+ function fields(request) {
330
+ return {
331
+ product_name: request.productName,
332
+ description: request.description,
333
+ contact_enabled: request.contactEnabled,
334
+ contact_description: request.contactDescription,
335
+ group_targets: request.groupTargets.map((target) => ({
336
+ ...target.id === void 0 ? {} : { id: target.id },
337
+ group_did: target.groupDid,
338
+ description: target.description
339
+ }))
340
+ };
341
+ }
342
+ function view$1(raw) {
343
+ if (typeof raw !== "object" || raw === null) throw new TypeError("invalid Integration response");
344
+ const value = raw;
345
+ const owner = value.owner;
346
+ const groups = value.group_targets;
347
+ if (typeof owner !== "object" || owner === null || !Array.isArray(groups)) throw new TypeError("invalid Integration response");
348
+ return {
349
+ id: requiredString$1(value.id),
350
+ publicId: nullableString(value.public_id),
351
+ integrationUrl: nullableString(value.integration_url),
352
+ owner: {
353
+ tenantId: requiredString$1(owner.tenant_id),
354
+ handle: requiredString$1(owner.handle),
355
+ currentDid: requiredString$1(owner.current_did),
356
+ displayName: requiredString$1(owner.display_name)
357
+ },
358
+ productName: requiredString$1(value.product_name),
359
+ description: requiredString$1(value.description),
360
+ contactEnabled: requiredBoolean(value.contact_enabled),
361
+ contactDescription: requiredString$1(value.contact_description),
362
+ groupTargets: groups.map((group) => {
363
+ if (typeof group !== "object" || group === null) throw new TypeError("invalid Integration group");
364
+ const target = group;
365
+ return {
366
+ id: requiredString$1(target.id),
367
+ groupDid: requiredString$1(target.group_did),
368
+ displayName: requiredString$1(target.display_name),
369
+ avatarUrl: nullableString(target.avatar_url),
370
+ description: requiredString$1(target.description),
371
+ availability: requiredEnum(target.availability, TARGET_AVAILABILITIES)
372
+ };
373
+ }),
374
+ status: requiredEnum(value.status, INTEGRATION_STATUSES),
375
+ revision: requiredInteger(value.revision)
376
+ };
377
+ }
378
+ function requiredString$1(value) {
379
+ if (typeof value !== "string") throw new TypeError("invalid Integration response");
380
+ return value;
381
+ }
382
+ function nullableString(value) {
383
+ if (value === null) return null;
384
+ return requiredString$1(value);
385
+ }
386
+ function requiredBoolean(value) {
387
+ if (typeof value !== "boolean") throw new TypeError("invalid Integration response");
388
+ return value;
389
+ }
390
+ function requiredInteger(value) {
391
+ if (!Number.isSafeInteger(value)) throw new TypeError("invalid Integration response");
392
+ return value;
393
+ }
394
+ function requiredEnum(value, allowed) {
395
+ if (typeof value !== "string" || !allowed.includes(value)) throw new TypeError("invalid Integration response");
396
+ return value;
397
+ }
398
+ function failure$1(status, raw) {
399
+ const body = typeof raw === "object" && raw !== null ? raw : {};
400
+ const nested = typeof body.error === "object" && body.error !== null ? body.error : body;
401
+ const candidate = typeof nested.code === "string" ? nested.code : "";
402
+ const code = status === 404 ? "not-found" : status === 409 ? "conflict" : status === 400 ? "invalid-request" : status === 401 ? "unauthorized" : status === 403 ? "forbidden" : status === 429 ? "rate-limited" : status >= 500 ? "unavailable" : [
403
+ "not-found",
404
+ "conflict",
405
+ "invalid-request",
406
+ "forbidden",
407
+ "unauthorized",
408
+ "rate-limited",
409
+ "unavailable"
410
+ ].includes(candidate) ? candidate : "remote";
411
+ return {
412
+ code,
413
+ message: integrationFailureMessage(code)
414
+ };
415
+ }
416
+ function integrationFailureMessage(code) {
417
+ switch (code) {
418
+ case "not-found": return "尚未创建 Integration。";
419
+ case "conflict": return "Integration 已在其他位置更新,请重新加载后再试。";
420
+ case "invalid-request": return "Integration 信息不完整或格式不正确。";
421
+ case "forbidden": return "当前 AWiki 身份无权管理这个 Integration。";
422
+ case "unauthorized": return "请先在 AWiki 正式版中登录。";
423
+ case "rate-limited": return "操作过于频繁,请稍后重试。";
424
+ case "unavailable": return "临时消息服务暂时不可用。";
425
+ case "network": return "无法连接临时消息服务。";
426
+ default: return "临时消息服务返回了无法识别的结果。";
427
+ }
428
+ }
429
+ async function readBounded$1(response) {
430
+ const reader = response.body?.getReader();
431
+ if (reader === void 0) return null;
432
+ const chunks = [];
433
+ let size = 0;
434
+ try {
435
+ while (true) {
436
+ const result = await reader.read();
437
+ if (result.done) break;
438
+ size += result.value.byteLength;
439
+ if (size > MAX_RESPONSE_BYTES$1) throw new TypeError("Integration response too large");
440
+ chunks.push(result.value);
441
+ }
442
+ } finally {
443
+ reader.releaseLock();
444
+ }
445
+ const bytes = new Uint8Array(size);
446
+ let offset = 0;
447
+ for (const chunk of chunks) {
448
+ bytes.set(chunk, offset);
449
+ offset += chunk.byteLength;
450
+ }
451
+ if (bytes.length === 0) return null;
452
+ return JSON.parse(new TextDecoder().decode(bytes));
453
+ }
454
+ /** Fixed-origin client; callers cannot control path, method, headers, or transport. */
455
+ var AwikiIntegrationClient = class {
456
+ origin;
457
+ auth;
458
+ constructor(origin, auth) {
459
+ this.origin = origin;
460
+ this.auth = auth;
461
+ }
462
+ read() {
463
+ return this.execute("read");
464
+ }
465
+ create(request) {
466
+ return this.execute("create", fields(request), request.idempotencyKey);
467
+ }
468
+ update(request) {
469
+ return this.execute("update", {
470
+ expected_revision: request.expectedRevision,
471
+ ...fields(request)
472
+ }, request.idempotencyKey);
473
+ }
474
+ rotate(request) {
475
+ return this.execute("rotate", { expected_revision: request.expectedRevision }, request.idempotencyKey);
476
+ }
477
+ close(request) {
478
+ return this.execute("close", { expected_revision: request.expectedRevision }, request.idempotencyKey);
479
+ }
480
+ reopen(request) {
481
+ return this.execute("reopen", {
482
+ expected_revision: request.expectedRevision,
483
+ ...fields(request)
484
+ }, request.idempotencyKey);
485
+ }
486
+ async execute(operation, body, idempotencyKey) {
487
+ const spec = OPERATIONS[operation];
488
+ const encoded = body === void 0 ? void 0 : JSON.stringify(body);
489
+ if (encoded !== void 0 && Buffer.byteLength(encoded, "utf8") > MAX_REQUEST_BYTES) return {
490
+ ok: false,
491
+ error: {
492
+ code: "invalid-request",
493
+ message: integrationFailureMessage("invalid-request")
494
+ }
495
+ };
496
+ const headers = new Headers({ accept: "application/json" });
497
+ if (encoded !== void 0) headers.set("content-type", "application/json");
498
+ if (idempotencyKey !== void 0) headers.set("idempotency-key", idempotencyKey);
499
+ const abort = new AbortController();
500
+ const timeout = setTimeout(() => {
501
+ abort.abort();
502
+ }, REQUEST_TIMEOUT_MS);
503
+ try {
504
+ const request = new Request(new URL(spec.path, this.origin), {
505
+ method: spec.method,
506
+ headers,
507
+ ...encoded === void 0 ? {} : { body: encoded },
508
+ redirect: "error",
509
+ signal: abort.signal
510
+ });
511
+ const response = await this.auth.dispatch(request, (signed) => fetch(signed));
512
+ const raw = await readBounded$1(response);
513
+ if (!response.ok) return {
514
+ ok: false,
515
+ error: failure$1(response.status, raw)
516
+ };
517
+ return {
518
+ ok: true,
519
+ value: view$1(raw)
520
+ };
521
+ } catch (error) {
522
+ if (error instanceof AwikiExternalHttpAuthError && (error.code === "not-registered" || error.code === "signed-out")) return {
523
+ ok: false,
524
+ error: {
525
+ code: "unauthorized",
526
+ message: integrationFailureMessage("unauthorized")
527
+ }
528
+ };
529
+ return {
530
+ ok: false,
531
+ error: {
532
+ code: "network",
533
+ message: integrationFailureMessage("network")
534
+ }
535
+ };
536
+ } finally {
537
+ clearTimeout(timeout);
538
+ }
539
+ }
540
+ };
541
+ //#endregion
542
+ //#region lib/types/legacy-sent-mail-cache.js
543
+ /** Exact cleanup for the removed Host-local sent-mail authority. */
544
+ function isMissing$4(error) {
545
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
546
+ }
547
+ /** Clear only the legacy cache directory during the existing destructive local-data flow. */
548
+ async function clearLegacySentMailCache(stateRoot) {
549
+ const hostDirectory = join(stateRoot, ".host");
550
+ const directory = join(hostDirectory, "sent-mail-v1");
551
+ try {
552
+ const host = await lstat(hostDirectory);
553
+ if (!host.isDirectory() || host.isSymbolicLink()) throw new TypeError("awiki: legacy sent-mail cache parent is invalid");
554
+ const target = await lstat(directory);
555
+ if (target.isDirectory() && !target.isSymbolicLink()) await rm(directory, {
556
+ recursive: true,
557
+ force: true
558
+ });
559
+ else await unlink(directory);
560
+ } catch (error) {
561
+ if (!isMissing$4(error)) throw error;
562
+ }
563
+ }
564
+ //#endregion
565
+ //#region lib/types/mail-list-client.js
566
+ /** Host-only server-authoritative outbound Mail query. */
567
+ const MAIL_LIST_PATH = "/mail/rpc";
568
+ const MAIL_LIST_PAGE_SIZE = 100;
569
+ const MAIL_LIST_TIMEOUT_MS = 1e4;
570
+ const MAX_RESPONSE_BYTES = 1048576;
571
+ const MAX_UINT32 = 4294967295;
572
+ function invalidResponse() {
573
+ throw new AwikiSdkError("remote");
574
+ }
575
+ function uint32(value) {
576
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_UINT32) invalidResponse();
577
+ return value;
578
+ }
579
+ function requiredString(value, maxBytes) {
580
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value || /[\u0000-\u001f\u007f-\u009f]/u.test(value) || Buffer.byteLength(value, "utf8") > maxBytes) invalidResponse();
581
+ return value;
582
+ }
583
+ function addresses(value) {
584
+ if (value === null || value === void 0 || value === "") return [];
585
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[;,]/u) : invalidResponse();
586
+ if (raw.length > 100) invalidResponse();
587
+ return raw.map((candidate) => {
588
+ const address = requiredString(typeof candidate === "string" ? candidate.trim() : candidate, 320);
589
+ if (!address.includes("@") || /\s/u.test(address)) invalidResponse();
590
+ return address;
591
+ });
592
+ }
593
+ function timestamp(value) {
594
+ const copied = requiredString(value, 64);
595
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/u.exec(copied);
596
+ if (match === null) invalidResponse();
597
+ const year = Number(match[1]);
598
+ const month = Number(match[2]);
599
+ const day = Number(match[3]);
600
+ const hour = Number(match[4]);
601
+ const minute = Number(match[5]);
602
+ const second = Number(match[6]);
603
+ const zone = match[8];
604
+ const monthDays = [
605
+ 31,
606
+ year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
607
+ 31,
608
+ 30,
609
+ 31,
610
+ 30,
611
+ 31,
612
+ 31,
613
+ 30,
614
+ 31,
615
+ 30,
616
+ 31
617
+ ];
618
+ if (year === 0 || month < 1 || month > 12 || day < 1 || day > monthDays[month - 1] || hour > 23 || minute > 59 || second > 59) invalidResponse();
619
+ if (zone !== void 0 && zone !== "Z") {
620
+ const offsetHour = Number(zone.slice(1, 3));
621
+ const offsetMinute = Number(zone.slice(4, 6));
622
+ if (offsetHour > 23 || offsetMinute > 59) invalidResponse();
623
+ }
624
+ return zone === void 0 ? `${copied}Z` : copied;
625
+ }
626
+ function boundedText$1(value, maxBytes, fallback) {
627
+ const source = typeof value === "string" && value.trim() !== "" ? value : fallback;
628
+ if (/\u0000/u.test(source)) invalidResponse();
629
+ if (Buffer.byteLength(source, "utf8") <= maxBytes) return {
630
+ value: source,
631
+ truncated: false
632
+ };
633
+ const characters = Array.from(source);
634
+ while (characters.length > 0 && Buffer.byteLength(characters.join(""), "utf8") > maxBytes) characters.pop();
635
+ return {
636
+ value: characters.join(""),
637
+ truncated: true
638
+ };
639
+ }
640
+ function summary(raw) {
641
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) invalidResponse();
642
+ const value = raw;
643
+ if (value.direction !== "outbound" || typeof value.has_attachments !== "boolean") invalidResponse();
644
+ const subject = boundedText$1(value.subject, 1024, "(no subject)");
645
+ return {
646
+ id: requiredString(value.id, 2048),
647
+ folder: "sent",
648
+ from: addresses(value.from_addr),
649
+ to: addresses(value.to_addr),
650
+ cc: addresses(value.cc_addr),
651
+ subject: subject.value,
652
+ subjectTruncated: subject.truncated,
653
+ previewTruncated: false,
654
+ sentAt: timestamp(value.created_at),
655
+ unread: false,
656
+ hasAttachments: value.has_attachments
657
+ };
658
+ }
659
+ async function readBounded(response) {
660
+ const declared = response.headers.get("content-length");
661
+ if (declared !== null && /^\d+$/u.test(declared) && Number(declared) > MAX_RESPONSE_BYTES) invalidResponse();
662
+ const reader = response.body?.getReader();
663
+ if (reader === void 0) invalidResponse();
664
+ const chunks = [];
665
+ let size = 0;
666
+ try {
667
+ while (true) {
668
+ const result = await reader.read();
669
+ if (result.done) break;
670
+ size += result.value.byteLength;
671
+ if (size > MAX_RESPONSE_BYTES) invalidResponse();
672
+ chunks.push(result.value);
673
+ }
674
+ } finally {
675
+ reader.releaseLock();
676
+ }
677
+ const bytes = new Uint8Array(size);
678
+ let offset = 0;
679
+ for (const chunk of chunks) {
680
+ bytes.set(chunk, offset);
681
+ offset += chunk.byteLength;
682
+ }
683
+ try {
684
+ return JSON.parse(new TextDecoder().decode(bytes));
685
+ } catch {
686
+ invalidResponse();
687
+ }
688
+ }
689
+ function rpcFailure(code) {
690
+ if (code === -32e3) throw new AwikiSdkError("not-registered");
691
+ if (code === -32001) throw new AwikiSdkError("forbidden");
692
+ if (code === -32002) throw new AwikiSdkError("not-found");
693
+ if (code === -32003) throw new AwikiSdkError("conflict");
694
+ if (code === -32004 || code === -32600 || code === -32602) throw new AwikiSdkError("invalid-request");
695
+ if (code === -32005) throw new AwikiSdkError("rate-limited");
696
+ invalidResponse();
697
+ }
698
+ function responseFailure(status) {
699
+ if (status === 401) throw new AwikiSdkError("not-registered");
700
+ if (status === 403) throw new AwikiSdkError("forbidden");
701
+ if (status === 429) throw new AwikiSdkError("rate-limited");
702
+ if (status >= 500) throw new AwikiSdkError("network");
703
+ invalidResponse();
704
+ }
705
+ /** Fixed Mail Service client; callers cannot select method, direction, headers, or transport. */
706
+ var AwikiMailListClient = class {
707
+ origin;
708
+ auth;
709
+ constructor(origin, auth) {
710
+ this.origin = origin;
711
+ this.auth = auth;
712
+ }
713
+ async listOutbound(request) {
714
+ const offset = request.offset ?? 0;
715
+ const limit = request.limit ?? 20;
716
+ const firstPageNumber = Math.floor(offset / MAIL_LIST_PAGE_SIZE) + 1;
717
+ const first = await this.page(firstPageNumber);
718
+ const pageOffset = offset % MAIL_LIST_PAGE_SIZE;
719
+ let items = first.items.slice(pageOffset, pageOffset + limit);
720
+ let total = first.total;
721
+ if (items.length < limit && offset + items.length < total) {
722
+ const next = await this.page(firstPageNumber + 1);
723
+ total = next.total;
724
+ items = [...items, ...next.items.slice(0, limit - items.length)];
725
+ }
726
+ const nextOffset = offset + items.length;
727
+ const hasMore = nextOffset < total;
728
+ return {
729
+ items,
730
+ ...hasMore ? { nextOffset } : {},
731
+ hasMore
732
+ };
733
+ }
734
+ async page(page) {
735
+ const abort = new AbortController();
736
+ const timeout = setTimeout(() => {
737
+ abort.abort();
738
+ }, MAIL_LIST_TIMEOUT_MS);
739
+ try {
740
+ const request = new Request(new URL(MAIL_LIST_PATH, this.origin), {
741
+ method: "POST",
742
+ headers: {
743
+ accept: "application/json",
744
+ "content-type": "application/json"
745
+ },
746
+ body: JSON.stringify({
747
+ jsonrpc: "2.0",
748
+ id: 1,
749
+ method: "mail.list",
750
+ params: {
751
+ direction: "outbound",
752
+ page,
753
+ page_size: MAIL_LIST_PAGE_SIZE
754
+ }
755
+ }),
756
+ redirect: "error",
757
+ signal: abort.signal
758
+ });
759
+ const response = await this.auth.dispatch(request, (signed) => fetch(signed));
760
+ if (!response.ok) responseFailure(response.status);
761
+ const raw = await readBounded(response);
762
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) invalidResponse();
763
+ const envelope = raw;
764
+ if (envelope.jsonrpc !== "2.0" || envelope.id !== 1) invalidResponse();
765
+ if (envelope.error !== null && envelope.error !== void 0) {
766
+ if (typeof envelope.error !== "object" || Array.isArray(envelope.error)) invalidResponse();
767
+ rpcFailure(envelope.error.code);
768
+ }
769
+ if (typeof envelope.result !== "object" || envelope.result === null || Array.isArray(envelope.result)) invalidResponse();
770
+ const result = envelope.result;
771
+ if (!Array.isArray(result.items) || result.items.length > MAIL_LIST_PAGE_SIZE) invalidResponse();
772
+ return {
773
+ total: uint32(result.total),
774
+ items: result.items.map(summary)
775
+ };
776
+ } catch (error) {
777
+ if (error instanceof AwikiSdkError) throw error;
778
+ if (error instanceof AwikiExternalHttpAuthError) {
779
+ if (error.code === "not-registered") throw new AwikiSdkError("not-registered");
780
+ if (error.code === "signed-out") throw new AwikiSdkError("signed-out");
781
+ }
782
+ throw new AwikiSdkError("network");
783
+ } finally {
784
+ clearTimeout(timeout);
785
+ }
786
+ }
787
+ };
788
+ //#endregion
193
789
  //#region lib/types/tools.js
194
790
  /** Model-facing AWiki read and approved-send tools. */
195
791
  /** Model tool that reads the public deployment identity. */
@@ -555,10 +1151,55 @@ function mailSendRequest(request) {
555
1151
  };
556
1152
  }
557
1153
  //#endregion
1154
+ //#region lib/types/builtin-tenant-config.js
1155
+ /** Build-injected two-slot tenant catalog with a source-tree default for tests. */
1156
+ function record(value) {
1157
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1158
+ }
1159
+ function definition(value) {
1160
+ if (!record(value) || !record(value.display_name) || typeof value.display_name["zh-CN"] !== "string" || value.display_name["zh-CN"].trim() === "" || typeof value.display_name.en !== "string" || value.display_name.en.trim() === "" || typeof value.backend_origin !== "string" || typeof value.did_host !== "string") throw new Error("awiki: invalid built-in tenant definition");
1161
+ const origin = new URL(value.backend_origin);
1162
+ const loopback = origin.protocol === "http:" && [
1163
+ "localhost",
1164
+ "127.0.0.1",
1165
+ "[::1]"
1166
+ ].includes(origin.hostname);
1167
+ if (origin.protocol !== "https:" && !loopback || origin.username !== "" || origin.password !== "" || origin.pathname !== "/" || origin.search !== "" || origin.hash !== "" || !loopback && origin.port !== "") throw new Error("awiki: built-in tenant origin must be an HTTPS origin");
1168
+ const didHost = value.did_host.trim().toLowerCase().replace(/\.$/u, "");
1169
+ if (origin.hostname !== didHost) throw new Error("awiki: built-in tenant DID host must match its origin");
1170
+ return {
1171
+ displayName: {
1172
+ "zh-CN": value.display_name["zh-CN"].trim(),
1173
+ en: value.display_name.en.trim()
1174
+ },
1175
+ backendOrigin: origin.origin,
1176
+ didHost
1177
+ };
1178
+ }
1179
+ function decodeBuiltinTenantConfig(value) {
1180
+ if (!record(value) || value.schema_version !== 1 || value.default_slot !== "primary" && value.default_slot !== "secondary" || !record(value.tenants) || Object.keys(value.tenants).sort().join(",") !== "primary,secondary") throw new Error("awiki: invalid built-in tenant config");
1181
+ const primary = definition(value.tenants.primary);
1182
+ const secondary = definition(value.tenants.secondary);
1183
+ if (primary.backendOrigin === secondary.backendOrigin || primary.didHost === secondary.didHost) throw new Error("awiki: built-in tenant slots must use distinct endpoints");
1184
+ return {
1185
+ schemaVersion: 1,
1186
+ defaultSlot: value.default_slot,
1187
+ tenants: {
1188
+ primary,
1189
+ secondary
1190
+ }
1191
+ };
1192
+ }
1193
+ function rawConfig() {
1194
+ return JSON.parse("{\n \"schema_version\": 1,\n \"default_slot\": \"primary\",\n \"tenants\": {\n \"primary\": {\n \"display_name\": {\n \"zh-CN\": \"AWiki 中国(上海)\",\n \"en\": \"AWiki China (Shanghai)\"\n },\n \"backend_origin\": \"https://awiki.me\",\n \"did_host\": \"awiki.me\"\n },\n \"secondary\": {\n \"display_name\": {\n \"zh-CN\": \"AWiki 全球(硅谷)\",\n \"en\": \"AWiki Global (Silicon Valley)\"\n },\n \"backend_origin\": \"https://awiki.ai\",\n \"did_host\": \"awiki.ai\"\n }\n }\n}\n");
1195
+ }
1196
+ const AWIKI_BUILTIN_TENANT_CONFIG = decodeBuiltinTenantConfig(rawConfig());
1197
+ const AWIKI_DEFAULT_BUILTIN_TENANT = AWIKI_BUILTIN_TENANT_CONFIG.tenants[AWIKI_BUILTIN_TENANT_CONFIG.defaultSlot];
1198
+ //#endregion
558
1199
  //#region lib/types/domain.js
559
1200
  /** Client-safe AWiki Handle provider domain constants and validation. */
560
1201
  /** Default Handle provider domain for new AWiki deployments. */
561
- const DEFAULT_AWIKI_DOMAIN = "awiki.ai";
1202
+ const DEFAULT_AWIKI_DOMAIN = AWIKI_DEFAULT_BUILTIN_TENANT.didHost;
562
1203
  /** Field carrying the Handle provider domain in the AWiki settings namespace. */
563
1204
  const AWIKI_DOMAIN_FIELD = "domain";
564
1205
  /** Settings namespace owned by the AWiki plugin. */
@@ -587,20 +1228,29 @@ const AWIKI_SETTINGS_RPC_CHANNEL = "/awiki-settings";
587
1228
  const AWIKI_SETTINGS_RPC_ENDPOINTS = {
588
1229
  describe: "describe",
589
1230
  setDomain: "set-domain",
590
- resetDomain: "reset-domain"
1231
+ resetDomain: "reset-domain",
1232
+ describeTenants: "describe-tenants",
1233
+ createTenant: "create-tenant",
1234
+ renameTenant: "rename-tenant",
1235
+ switchTenant: "switch-tenant",
1236
+ archiveTenant: "archive-tenant",
1237
+ describeUpdatePolicy: "describe-update-policy",
1238
+ refreshUpdatePolicy: "refresh-update-policy",
1239
+ describeDesktopUpdate: "describe-desktop-update",
1240
+ refreshDesktopUpdate: "refresh-desktop-update"
591
1241
  };
592
1242
  //#endregion
593
1243
  //#region lib/types/settings-rpc.js
594
1244
  /** Loopback-only Host transport for AWiki's durable plugin settings. */
595
- function isRecord$2(value) {
1245
+ function isRecord$4(value) {
596
1246
  return typeof value === "object" && value !== null && !Array.isArray(value);
597
1247
  }
598
1248
  function expectedRevision(payload) {
599
- if (!isRecord$2(payload) || !Number.isSafeInteger(payload.expectedRevision) || payload.expectedRevision < 0) return;
1249
+ if (!isRecord$4(payload) || !Number.isSafeInteger(payload.expectedRevision) || payload.expectedRevision < 0) return;
600
1250
  return payload.expectedRevision;
601
1251
  }
602
1252
  function sanitizeLayer(value) {
603
- if (!isRecord$2(value)) return void 0;
1253
+ if (!isRecord$4(value)) return void 0;
604
1254
  if (!Object.hasOwn(value, "domain")) return {};
605
1255
  if (typeof value["domain"] !== "string") return void 0;
606
1256
  const domain = normalizeAwikiDomain(value[AWIKI_DOMAIN_FIELD]);
@@ -609,7 +1259,7 @@ function sanitizeLayer(value) {
609
1259
  }
610
1260
  function view(provider) {
611
1261
  const descriptor = provider.describe({ redactSecrets: true }).find((candidate) => candidate.ns === AWIKI_SETTINGS_NAMESPACE);
612
- if (descriptor === void 0 || !isRecord$2(descriptor.value) || typeof descriptor.value.domain !== "string") return;
1262
+ if (descriptor === void 0 || !isRecord$4(descriptor.value) || typeof descriptor.value.domain !== "string") return;
613
1263
  const domain = normalizeAwikiDomain(descriptor.value.domain);
614
1264
  if (domain !== descriptor.value.domain) return void 0;
615
1265
  const base = descriptor.base === void 0 ? void 0 : sanitizeLayer(descriptor.base);
@@ -653,14 +1303,138 @@ function cancelled() {
653
1303
  }
654
1304
  };
655
1305
  }
1306
+ function publicTenantView(value) {
1307
+ return {
1308
+ schemaVersion: value.schemaVersion,
1309
+ officialCatalogVersion: value.officialCatalogVersion,
1310
+ generation: value.generation,
1311
+ activeTenantId: value.activeTenantId,
1312
+ tenants: value.tenants.map((tenant) => {
1313
+ const slot = tenant.tenantId === "builtin-primary" ? "primary" : tenant.tenantId === "builtin-secondary" ? "secondary" : void 0;
1314
+ return {
1315
+ tenantId: tenant.tenantId,
1316
+ storageScopeId: tenant.storageScopeId,
1317
+ kind: tenant.kind,
1318
+ displayName: tenant.displayName,
1319
+ ...slot === void 0 ? {} : { displayNames: AWIKI_BUILTIN_TENANT_CONFIG.tenants[slot].displayName },
1320
+ backendBaseUrl: tenant.backendBaseUrl,
1321
+ didHost: tenant.didHost,
1322
+ lifecycle: tenant.lifecycle,
1323
+ storageLayout: tenant.storageLayout
1324
+ };
1325
+ }),
1326
+ switching: value.switching,
1327
+ ...value.diagnostic === void 0 ? {} : { diagnostic: value.diagnostic }
1328
+ };
1329
+ }
1330
+ function tenantRejected(error) {
1331
+ return {
1332
+ ok: false,
1333
+ error: {
1334
+ code: "settings-rejected",
1335
+ message: error instanceof Error && error.message.startsWith("awiki:") ? error.message : "The Host rejected the AWiki tenant change.",
1336
+ details: { ns: AWIKI_SETTINGS_NAMESPACE }
1337
+ }
1338
+ };
1339
+ }
656
1340
  /** Build a handler whose provider lookup remains correct across Cordis reinjection. */
657
- function createAwikiSettingsRpcHandler(getProvider) {
1341
+ function createAwikiSettingsRpcHandler(getProvider, tenantManagement) {
658
1342
  return async (endpoint, payload, signal) => {
659
1343
  if (signal.aborted) return cancelled();
1344
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.describeDesktopUpdate || endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.refreshDesktopUpdate) {
1345
+ if (!isRecord$4(payload)) return badRequest();
1346
+ try {
1347
+ return {
1348
+ ok: true,
1349
+ value: (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.refreshDesktopUpdate ? await tenantManagement?.refreshDesktopUpdate?.() : tenantManagement?.describeDesktopUpdate?.()) ?? null
1350
+ };
1351
+ } catch {
1352
+ return unavailable();
1353
+ }
1354
+ }
1355
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.describeTenants) {
1356
+ if (!isRecord$4(payload) || tenantManagement === void 0) return unavailable();
1357
+ try {
1358
+ return {
1359
+ ok: true,
1360
+ value: publicTenantView(tenantManagement.describe())
1361
+ };
1362
+ } catch (error) {
1363
+ return tenantRejected(error);
1364
+ }
1365
+ }
1366
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.createTenant) {
1367
+ if (!isRecord$4(payload) || typeof payload.displayName !== "string" || typeof payload.domain !== "string" || tenantManagement === void 0) return badRequest();
1368
+ try {
1369
+ return {
1370
+ ok: true,
1371
+ value: publicTenantView(tenantManagement.create(payload.displayName, payload.domain))
1372
+ };
1373
+ } catch (error) {
1374
+ return tenantRejected(error);
1375
+ }
1376
+ }
1377
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.renameTenant) {
1378
+ if (!isRecord$4(payload) || typeof payload.tenantId !== "string" || typeof payload.displayName !== "string" || tenantManagement === void 0) return badRequest();
1379
+ try {
1380
+ return {
1381
+ ok: true,
1382
+ value: publicTenantView(tenantManagement.rename(payload.tenantId, payload.displayName))
1383
+ };
1384
+ } catch (error) {
1385
+ return tenantRejected(error);
1386
+ }
1387
+ }
1388
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.switchTenant) {
1389
+ if (!isRecord$4(payload) || typeof payload.tenantId !== "string" || tenantManagement === void 0) return badRequest();
1390
+ try {
1391
+ return {
1392
+ ok: true,
1393
+ value: publicTenantView(await tenantManagement.switch(payload.tenantId))
1394
+ };
1395
+ } catch (error) {
1396
+ return tenantRejected(error);
1397
+ }
1398
+ }
1399
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.archiveTenant) {
1400
+ if (!isRecord$4(payload) || typeof payload.tenantId !== "string" || tenantManagement === void 0) return badRequest();
1401
+ try {
1402
+ return {
1403
+ ok: true,
1404
+ value: publicTenantView(tenantManagement.archive(payload.tenantId))
1405
+ };
1406
+ } catch (error) {
1407
+ return tenantRejected(error);
1408
+ }
1409
+ }
1410
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.describeUpdatePolicy) {
1411
+ const describeUpdate = tenantManagement?.describeUpdate;
1412
+ if (!isRecord$4(payload) || describeUpdate === void 0) return unavailable();
1413
+ try {
1414
+ return {
1415
+ ok: true,
1416
+ value: describeUpdate.call(tenantManagement)
1417
+ };
1418
+ } catch (error) {
1419
+ return tenantRejected(error);
1420
+ }
1421
+ }
1422
+ if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.refreshUpdatePolicy) {
1423
+ const refreshUpdate = tenantManagement?.refreshUpdate;
1424
+ if (!isRecord$4(payload) || refreshUpdate === void 0) return unavailable();
1425
+ try {
1426
+ return {
1427
+ ok: true,
1428
+ value: await refreshUpdate.call(tenantManagement)
1429
+ };
1430
+ } catch (error) {
1431
+ return tenantRejected(error);
1432
+ }
1433
+ }
660
1434
  const provider = getProvider();
661
1435
  if (provider === void 0) return unavailable();
662
1436
  if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.describe) {
663
- if (!isRecord$2(payload)) return badRequest();
1437
+ if (!isRecord$4(payload)) return badRequest();
664
1438
  const current = view(provider);
665
1439
  return current === void 0 ? unavailable() : {
666
1440
  ok: true,
@@ -671,7 +1445,7 @@ function createAwikiSettingsRpcHandler(getProvider) {
671
1445
  if (revision === void 0) return badRequest();
672
1446
  let operation;
673
1447
  if (endpoint === AWIKI_SETTINGS_RPC_ENDPOINTS.setDomain) {
674
- if (!isRecord$2(payload) || typeof payload.domain !== "string") return badRequest();
1448
+ if (!isRecord$4(payload) || typeof payload.domain !== "string") return badRequest();
675
1449
  let domain;
676
1450
  try {
677
1451
  domain = normalizeAwikiDomain(payload.domain);
@@ -740,7 +1514,7 @@ var AwikiSessionStore = class {
740
1514
  if (await readFile(this.markerPath, "utf8") !== SIGNED_OUT_MARKER) throw new TypeError("awiki: local session marker is invalid");
741
1515
  return true;
742
1516
  } catch (error) {
743
- if (isMissing$4(error)) return false;
1517
+ if (isMissing$3(error)) return false;
744
1518
  throw error;
745
1519
  }
746
1520
  }
@@ -758,7 +1532,7 @@ var AwikiSessionStore = class {
758
1532
  mode: 384
759
1533
  });
760
1534
  } catch (error) {
761
- if (!isFileExists$3(error) || !await this.isSignedOut()) throw error;
1535
+ if (!isFileExists$2(error) || !await this.isSignedOut()) throw error;
762
1536
  }
763
1537
  await chmod(this.markerPath, 384);
764
1538
  }
@@ -767,7 +1541,7 @@ var AwikiSessionStore = class {
767
1541
  try {
768
1542
  await unlink(this.markerPath);
769
1543
  } catch (error) {
770
- if (!isMissing$4(error)) throw error;
1544
+ if (!isMissing$3(error)) throw error;
771
1545
  }
772
1546
  }
773
1547
  async hasPrivateHostDirectory() {
@@ -776,15 +1550,15 @@ var AwikiSessionStore = class {
776
1550
  if (!directory.isDirectory() || directory.isSymbolicLink()) throw new TypeError("awiki: local session directory is invalid");
777
1551
  return true;
778
1552
  } catch (error) {
779
- if (isMissing$4(error)) return false;
1553
+ if (isMissing$3(error)) return false;
780
1554
  throw error;
781
1555
  }
782
1556
  }
783
1557
  };
784
- function isMissing$4(error) {
1558
+ function isMissing$3(error) {
785
1559
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
786
1560
  }
787
- function isFileExists$3(error) {
1561
+ function isFileExists$2(error) {
788
1562
  return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
789
1563
  }
790
1564
  //#endregion
@@ -877,7 +1651,7 @@ var AwikiImageAttachmentCache = class {
877
1651
  force: true
878
1652
  });
879
1653
  } catch (error) {
880
- if (!isMissing$3(error)) throw error;
1654
+ if (!isMissing$2(error)) throw error;
881
1655
  }
882
1656
  }
883
1657
  decode(input, ownerDid, request) {
@@ -903,7 +1677,7 @@ var AwikiImageAttachmentCache = class {
903
1677
  if (!await this.hasPrivateDirectory(this.hostDirectory)) throw new TypeError("awiki: local Host directory is invalid");
904
1678
  await chmod(this.hostDirectory, 448);
905
1679
  await mkdir(this.directory, { mode: 448 }).catch((error) => {
906
- if (!isFileExists$2(error)) throw error;
1680
+ if (!isFileExists$1(error)) throw error;
907
1681
  });
908
1682
  if (!await this.hasPrivateDirectory(this.directory)) throw new TypeError("awiki: local image cache directory is invalid");
909
1683
  await chmod(this.directory, 448);
@@ -941,233 +1715,17 @@ var AwikiImageAttachmentCache = class {
941
1715
  if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new TypeError("awiki: local image cache path is invalid");
942
1716
  return true;
943
1717
  } catch (error) {
944
- if (isMissing$3(error)) return false;
1718
+ if (isMissing$2(error)) return false;
945
1719
  throw error;
946
1720
  }
947
1721
  }
948
1722
  };
949
- function isMissing$3(error) {
950
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
951
- }
952
- function isFileExists$2(error) {
953
- return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
954
- }
955
- //#endregion
956
- //#region lib/types/sent-mail-store.js
957
- /** Private, owner-bound persistence for mail successfully sent by this installation. */
958
- const STORE_VERSION$1 = 1;
959
- const STORE_DIRECTORY$1 = "sent-mail-v1";
960
- const MAX_RECORDS = 200;
961
- const MAX_STORE_BYTES$1 = 16777216;
962
- const PREVIEW_CHARACTERS = 160;
963
- const LOCAL_SENT_MAIL_ID_PREFIX = "awiki-sent-v1:";
964
- function invalidState$1() {
965
- throw new TypeError("awiki: local sent-mail history is invalid");
966
- }
967
1723
  function isMissing$2(error) {
968
- return typeof error === "object" && error !== null && error.code === "ENOENT";
1724
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
969
1725
  }
970
1726
  function isFileExists$1(error) {
971
- return typeof error === "object" && error !== null && error.code === "EEXIST";
972
- }
973
- function validToken(value, maxCharacters) {
974
- return typeof value === "string" && value.length > 0 && value.trim() === value && Array.from(value).length <= maxCharacters && !/[\u0000-\u001f\u007f-\u009f]/u.test(value);
975
- }
976
- function validAddress(value) {
977
- return typeof value === "string" && Array.from(value).length >= 3 && Array.from(value).length <= 320 && value.includes("@") && !/[\s\u0000-\u001f\u007f-\u009f]/u.test(value);
978
- }
979
- function decodeRecord(input) {
980
- if (typeof input !== "object" || input === null || Array.isArray(input)) invalidState$1();
981
- const value = input;
982
- if (typeof value.id !== "string" || !/^awiki-sent-v1:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value.id) || value.serviceMessageId !== void 0 && !validToken(value.serviceMessageId, 2048) || value.from !== void 0 && !validAddress(value.from) || typeof value.sentAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value.sentAt) || !Number.isFinite(Date.parse(value.sentAt))) invalidState$1();
983
- let request;
984
- try {
985
- request = mailSendRequest({
986
- to: value.to,
987
- cc: value.cc,
988
- subject: value.subject,
989
- bodyText: value.bodyText
990
- });
991
- } catch {
992
- invalidState$1();
993
- }
994
- return {
995
- id: value.id,
996
- ...value.serviceMessageId === void 0 ? {} : { serviceMessageId: value.serviceMessageId },
997
- ...value.from === void 0 ? {} : { from: value.from },
998
- to: [...request.to],
999
- cc: [...request.cc ?? []],
1000
- subject: request.subject,
1001
- bodyText: request.bodyText,
1002
- sentAt: value.sentAt
1003
- };
1004
- }
1005
- function message(record) {
1006
- const characters = Array.from(record.bodyText);
1007
- const preview = characters.slice(0, PREVIEW_CHARACTERS).join("");
1008
- return {
1009
- summary: {
1010
- id: record.id,
1011
- folder: "sent",
1012
- from: record.from === void 0 ? [] : [record.from],
1013
- to: [...record.to],
1014
- cc: [...record.cc],
1015
- subject: record.subject,
1016
- subjectTruncated: false,
1017
- preview,
1018
- previewTruncated: characters.length > PREVIEW_CHARACTERS,
1019
- sentAt: record.sentAt,
1020
- unread: false,
1021
- hasAttachments: false,
1022
- attachmentCount: 0
1023
- },
1024
- bodyText: record.bodyText,
1025
- bodyTruncated: false,
1026
- hasHtmlBody: false,
1027
- attachments: []
1028
- };
1029
- }
1030
- function isLocalSentMailId(messageId) {
1031
- return String(messageId).startsWith(LOCAL_SENT_MAIL_ID_PREFIX);
1727
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
1032
1728
  }
1033
- /** Atomic, bounded history of sends accepted by the Mail Service. */
1034
- var AwikiSentMailStore = class {
1035
- hostDirectory;
1036
- directory;
1037
- mutation = Promise.resolve();
1038
- constructor(stateRoot) {
1039
- this.hostDirectory = join(stateRoot, ".host");
1040
- this.directory = join(this.hostDirectory, STORE_DIRECTORY$1);
1041
- }
1042
- async list(ownerDid, request) {
1043
- const records = request.unreadOnly === true ? [] : await this.load(ownerDid);
1044
- const offset = request.offset ?? 0;
1045
- const limit = request.limit ?? 20;
1046
- const page = records.slice(offset, offset + limit);
1047
- const nextOffset = offset + page.length;
1048
- const hasMore = nextOffset < records.length;
1049
- return {
1050
- items: page.map((record) => message(record).summary),
1051
- ...hasMore ? { nextOffset } : {},
1052
- hasMore
1053
- };
1054
- }
1055
- async read(ownerDid, messageId) {
1056
- if (!isLocalSentMailId(messageId)) return void 0;
1057
- const record = (await this.load(ownerDid)).find((candidate) => candidate.id === String(messageId));
1058
- return record === void 0 ? void 0 : message(record);
1059
- }
1060
- append(ownerDid, request, result, account) {
1061
- const normalized = mailSendRequest(request);
1062
- const record = {
1063
- id: `${LOCAL_SENT_MAIL_ID_PREFIX}${randomUUID()}`,
1064
- ...result.messageId === void 0 ? {} : { serviceMessageId: String(result.messageId) },
1065
- ...account?.mailboxAddress === void 0 ? {} : { from: account.mailboxAddress },
1066
- to: [...normalized.to],
1067
- cc: [...normalized.cc ?? []],
1068
- subject: normalized.subject,
1069
- bodyText: normalized.bodyText,
1070
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
1071
- };
1072
- const append = async () => {
1073
- const records = await this.load(ownerDid);
1074
- await this.write(ownerDid, [record, ...records].slice(0, MAX_RECORDS));
1075
- };
1076
- const pending = this.mutation.then(append, append);
1077
- this.mutation = pending.catch(() => void 0);
1078
- return pending;
1079
- }
1080
- async clear() {
1081
- await this.mutation;
1082
- try {
1083
- if (!await this.hasDirectory(this.hostDirectory)) return;
1084
- const metadata = await lstat(this.directory);
1085
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
1086
- await unlink(this.directory);
1087
- return;
1088
- }
1089
- await rm(this.directory, {
1090
- recursive: true,
1091
- force: true
1092
- });
1093
- } catch (error) {
1094
- if (!isMissing$2(error)) throw error;
1095
- }
1096
- }
1097
- async load(ownerDid) {
1098
- if (!await this.hasDirectory(this.hostDirectory) || !await this.hasDirectory(this.directory)) return [];
1099
- const path = this.path(ownerDid);
1100
- let text;
1101
- try {
1102
- const metadata = await lstat(path);
1103
- if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STORE_BYTES$1) invalidState$1();
1104
- text = await readFile(path, "utf8");
1105
- } catch (error) {
1106
- if (isMissing$2(error)) return [];
1107
- throw error;
1108
- }
1109
- let parsed;
1110
- try {
1111
- parsed = JSON.parse(text);
1112
- } catch {
1113
- invalidState$1();
1114
- }
1115
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) invalidState$1();
1116
- const value = parsed;
1117
- if (value.version !== STORE_VERSION$1 || value.ownerDid !== String(ownerDid) || !Array.isArray(value.records) || value.records.length > MAX_RECORDS) invalidState$1();
1118
- return value.records.map(decodeRecord);
1119
- }
1120
- async write(ownerDid, records) {
1121
- await this.ensureDirectory();
1122
- const path = this.path(ownerDid);
1123
- const temporary = join(this.directory, `.${this.key(ownerDid)}.${randomUUID()}.tmp`);
1124
- const text = JSON.stringify({
1125
- version: STORE_VERSION$1,
1126
- ownerDid: String(ownerDid),
1127
- records
1128
- });
1129
- if (Buffer.byteLength(text, "utf8") > MAX_STORE_BYTES$1) invalidState$1();
1130
- try {
1131
- await writeFile(temporary, text, {
1132
- flag: "wx",
1133
- mode: 384
1134
- });
1135
- await rename(temporary, path);
1136
- await chmod(path, 384);
1137
- } finally {
1138
- await unlink(temporary).catch(() => void 0);
1139
- }
1140
- }
1141
- async ensureDirectory() {
1142
- await mkdir(this.hostDirectory, {
1143
- recursive: true,
1144
- mode: 448
1145
- });
1146
- if (!await this.hasDirectory(this.hostDirectory)) invalidState$1();
1147
- await chmod(this.hostDirectory, 448);
1148
- await mkdir(this.directory, { mode: 448 }).catch((error) => {
1149
- if (!isFileExists$1(error)) throw error;
1150
- });
1151
- if (!await this.hasDirectory(this.directory)) invalidState$1();
1152
- await chmod(this.directory, 448);
1153
- }
1154
- async hasDirectory(path) {
1155
- try {
1156
- const metadata = await lstat(path);
1157
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) invalidState$1();
1158
- return true;
1159
- } catch (error) {
1160
- if (isMissing$2(error)) return false;
1161
- throw error;
1162
- }
1163
- }
1164
- path(ownerDid) {
1165
- return join(this.directory, `${this.key(ownerDid)}.json`);
1166
- }
1167
- key(ownerDid) {
1168
- return createHash("sha256").update(String(ownerDid)).digest("hex");
1169
- }
1170
- };
1171
1729
  //#endregion
1172
1730
  //#region lib/types/conversation-preferences.js
1173
1731
  const STORE_VERSION = 1;
@@ -1187,7 +1745,7 @@ function isMissing$1(error) {
1187
1745
  function isFileExists(error) {
1188
1746
  return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
1189
1747
  }
1190
- function isRecord$1(value) {
1748
+ function isRecord$3(value) {
1191
1749
  return typeof value === "object" && value !== null && !Array.isArray(value);
1192
1750
  }
1193
1751
  function boundedString$1(value, maximum) {
@@ -1210,7 +1768,7 @@ function optionalTimestamp(value) {
1210
1768
  }
1211
1769
  /** Rebuild a bounded display-only snapshot before it reaches private persistence. */
1212
1770
  function conversation(value) {
1213
- if (!isRecord$1(value)) invalidState();
1771
+ if (!isRecord$3(value)) invalidState();
1214
1772
  const kind = value.kind;
1215
1773
  const id = boundedString$1(value.id, MAX_IDENTIFIER_CHARACTERS);
1216
1774
  const title = boundedString$1(value.title, MAX_TITLE_CHARACTERS);
@@ -1249,14 +1807,14 @@ function conversation(value) {
1249
1807
  invalidState();
1250
1808
  }
1251
1809
  function hiddenPreference(value) {
1252
- if (!isRecord$1(value) || !Number.isSafeInteger(value.hiddenAt) || value.hiddenAt < 0) invalidState();
1810
+ if (!isRecord$3(value) || !Number.isSafeInteger(value.hiddenAt) || value.hiddenAt < 0) invalidState();
1253
1811
  return {
1254
1812
  conversation: conversation(value.conversation),
1255
1813
  hiddenAt: value.hiddenAt
1256
1814
  };
1257
1815
  }
1258
1816
  function preferences(value, expectedOwnerDid) {
1259
- if (!isRecord$1(value) || value.version !== STORE_VERSION || value.ownerDid !== String(expectedOwnerDid) || !Array.isArray(value.hiddenConversations) || value.hiddenConversations.length > MAX_HIDDEN_CONVERSATIONS) invalidState();
1817
+ if (!isRecord$3(value) || value.version !== STORE_VERSION || value.ownerDid !== String(expectedOwnerDid) || !Array.isArray(value.hiddenConversations) || value.hiddenConversations.length > MAX_HIDDEN_CONVERSATIONS) invalidState();
1260
1818
  const hiddenConversations = value.hiddenConversations.map(hiddenPreference);
1261
1819
  const seen = /* @__PURE__ */ new Set();
1262
1820
  for (const hidden of hiddenConversations) {
@@ -1285,7 +1843,7 @@ function emptyPreferences(ownerDid) {
1285
1843
  /** Validate one browser mutation before entering private Host persistence. */
1286
1844
  function normalizeConversationPreferenceMutation(value) {
1287
1845
  try {
1288
- if (!isRecord$1(value)) return void 0;
1846
+ if (!isRecord$3(value)) return void 0;
1289
1847
  if (value.action === "hide") return {
1290
1848
  action: "hide",
1291
1849
  conversation: conversation(value.conversation)
@@ -1440,7 +1998,7 @@ function emptyState(identityScopeHash) {
1440
1998
  conversations: {}
1441
1999
  };
1442
2000
  }
1443
- function isRecord(value) {
2001
+ function isRecord$2(value) {
1444
2002
  return typeof value === "object" && value !== null && !Array.isArray(value);
1445
2003
  }
1446
2004
  function boundedString(value, field) {
@@ -1449,15 +2007,15 @@ function boundedString(value, field) {
1449
2007
  }
1450
2008
  /** Validate and detach the complete state file before the listener trusts any route. */
1451
2009
  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");
2010
+ if (isRecord$2(value) && value.version === 1) return emptyState(identityScopeHash);
2011
+ if (isRecord$2(value) && value.version === STATE_VERSION && typeof value.identityScopeHash === "string" && value.identityScopeHash !== identityScopeHash) return emptyState(identityScopeHash);
2012
+ if (!isRecord$2(value) || value.version !== STATE_VERSION || value.identityScopeHash !== identityScopeHash || !isRecord$2(value.conversations)) throw new TypeError("awiki: listener state is invalid");
1455
2013
  const entries = Object.entries(value.conversations);
1456
2014
  if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
1457
2015
  const conversations = {};
1458
2016
  for (const [conversationId, raw] of entries) {
1459
2017
  const id = boundedString(conversationId, "conversation id");
1460
- if (!isRecord(raw)) throw new TypeError("awiki: listener conversation state is invalid");
2018
+ if (!isRecord$2(raw)) throw new TypeError("awiki: listener conversation state is invalid");
1461
2019
  if (Object.keys(raw).some((key) => ![
1462
2020
  "peerDid",
1463
2021
  "sessionId",
@@ -2026,6 +2584,9 @@ var IdentityRealtimeSupervisor = class {
2026
2584
  lastFailureCode;
2027
2585
  lastConnectedAtMs;
2028
2586
  lastCommittedSyncCause;
2587
+ lastSyncPagesFetched;
2588
+ lastSyncMessagesHydrated;
2589
+ lastSyncOlderHistoryExcluded;
2029
2590
  retryTimer;
2030
2591
  resolveRetry;
2031
2592
  constructor(realtime, config = {}, logger = {
@@ -2059,7 +2620,10 @@ var IdentityRealtimeSupervisor = class {
2059
2620
  lifecyclePhase: this.lifecyclePhase,
2060
2621
  ...this.lastFailureCode === void 0 ? {} : { lastFailureCode: this.lastFailureCode },
2061
2622
  ...this.lastConnectedAtMs === void 0 ? {} : { lastConnectedAtMs: this.lastConnectedAtMs },
2062
- ...this.lastCommittedSyncCause === void 0 ? {} : { lastCommittedSyncCause: this.lastCommittedSyncCause }
2623
+ ...this.lastCommittedSyncCause === void 0 ? {} : { lastCommittedSyncCause: this.lastCommittedSyncCause },
2624
+ ...this.lastSyncPagesFetched === void 0 ? {} : { lastSyncPagesFetched: this.lastSyncPagesFetched },
2625
+ ...this.lastSyncMessagesHydrated === void 0 ? {} : { lastSyncMessagesHydrated: this.lastSyncMessagesHydrated },
2626
+ ...this.lastSyncOlderHistoryExcluded === void 0 ? {} : { lastSyncOlderHistoryExcluded: this.lastSyncOlderHistoryExcluded }
2063
2627
  };
2064
2628
  }
2065
2629
  /** Fence late events, wake retry sleep, stop the exact session, and join the lifecycle. */
@@ -2134,57 +2698,815 @@ var IdentityRealtimeSupervisor = class {
2134
2698
  }
2135
2699
  }
2136
2700
  }
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);
2701
+ async synchronize(cause, generation) {
2702
+ const result = await this.realtime.syncNow(syncReason(cause));
2703
+ if (!this.current(generation)) return;
2704
+ this.lastCommittedSyncCause = cause;
2705
+ this.lastSyncPagesFetched = result.pagesFetched;
2706
+ this.lastSyncMessagesHydrated = result.messagesHydrated;
2707
+ this.lastSyncOlderHistoryExcluded = result.olderHistoryExcluded;
2708
+ try {
2709
+ await this.config.onSynchronized?.(cause);
2710
+ } catch (error) {
2711
+ this.logger.warn("AWiki realtime post-sync consumer failed: %s", error instanceof Error ? error.message : "unknown failure");
2712
+ }
2713
+ }
2714
+ observeConnection(event) {
2715
+ if (event.kind !== "connection_state_changed") return;
2716
+ this.connected = event.state === "connected";
2717
+ if (this.connected) {
2718
+ this.lifecyclePhase = "connected";
2719
+ this.lastConnectedAtMs = Date.now();
2720
+ this.lastFailureCode = void 0;
2721
+ }
2722
+ }
2723
+ async stopSession(session) {
2724
+ if (this.stoppedSessions.has(session)) return;
2725
+ this.stoppedSessions.add(session);
2726
+ if (this.activeSession === session) this.activeSession = void 0;
2727
+ this.connected = false;
2728
+ this.lifecyclePhase = "stopping";
2729
+ this.stopCount += 1;
2730
+ await session.stop();
2731
+ }
2732
+ waitForRetry(failures, generation) {
2733
+ if (!this.current(generation)) return Promise.resolve();
2734
+ const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.min(failures - 1, 10), RETRY_MAX_DELAY_MS);
2735
+ return new Promise((resolve) => {
2736
+ const finish = () => {
2737
+ if (this.retryTimer === timer) this.retryTimer = void 0;
2738
+ if (this.resolveRetry === finish) this.resolveRetry = void 0;
2739
+ resolve();
2740
+ };
2741
+ const timer = setTimeout(finish, delay);
2742
+ this.retryTimer = timer;
2743
+ this.resolveRetry = finish;
2744
+ });
2745
+ }
2746
+ wakeRetry() {
2747
+ const timer = this.retryTimer;
2748
+ if (timer !== void 0) clearTimeout(timer);
2749
+ this.resolveRetry?.();
2750
+ }
2751
+ };
2752
+ //#endregion
2753
+ //#region lib/types/profile-state.js
2754
+ function assertProfileName(value, source) {
2755
+ if (typeof value !== "string" || value.length === 0 || value.includes("/") || value.includes("\\") || value === "." || value === ".." || value === "node_modules") throw new TypeError(`awiki: ${source} supplied an invalid profile name`);
2756
+ return value;
2757
+ }
2758
+ /** Resolve only profile identities supplied by an authoritative DSH service or Loader directory. */
2759
+ function resolveAwikiProfileName(ctx, dshHome) {
2760
+ const desktopProfiles = ctx.get("desktopProfiles");
2761
+ if (desktopProfiles !== void 0) return assertProfileName(desktopProfiles.current?.name, "desktopProfiles.current");
2762
+ let profileDir;
2763
+ try {
2764
+ if (ctx.baseUrl === void 0) return void 0;
2765
+ const baseUrl = new URL(ctx.baseUrl);
2766
+ if (baseUrl.protocol !== "file:") return void 0;
2767
+ profileDir = resolve(fileURLToPath(baseUrl));
2768
+ } catch {
2769
+ return;
2770
+ }
2771
+ const profilesDir = resolve(dshHome, "profiles");
2772
+ if (dirname(profileDir) !== profilesDir) return void 0;
2773
+ return assertProfileName(basename(profileDir), "Loader profile directory");
2774
+ }
2775
+ /** Preserve the historical fallback while isolating desktop/CLI profiles at the Host boundary. */
2776
+ function resolveAwikiStateRoot(ctx, dshHome) {
2777
+ const profileName = resolveAwikiProfileName(ctx, dshHome);
2778
+ return profileName === void 0 ? join(dshHome, "awiki", "im-core") : join(dshHome, "awiki", profileName, "im-core");
2779
+ }
2780
+ //#endregion
2781
+ //#region lib/types/tenant-registry.js
2782
+ /** Host-owned durable AWiki tenant catalog and storage-scope migration. */
2783
+ const AWIKI_TENANT_REGISTRY_SCHEMA_VERSION = 1;
2784
+ const AWIKI_OFFICIAL_CATALOG_VERSION = 2;
2785
+ const AWIKI_PRIMARY_TENANT_ID = "builtin-primary";
2786
+ const AWIKI_SECONDARY_TENANT_ID = "builtin-secondary";
2787
+ /** Compatibility aliases for existing callers; the persisted IDs are generic slots. */
2788
+ const AWIKI_CHINA_TENANT_ID = AWIKI_PRIMARY_TENANT_ID;
2789
+ const AWIKI_GLOBAL_TENANT_ID = AWIKI_SECONDARY_TENANT_ID;
2790
+ const OFFICIALS = ["primary", "secondary"].map((slot) => {
2791
+ const configured = AWIKI_BUILTIN_TENANT_CONFIG.tenants[slot];
2792
+ return {
2793
+ slot,
2794
+ tenantId: slot === "primary" ? AWIKI_PRIMARY_TENANT_ID : AWIKI_SECONDARY_TENANT_ID,
2795
+ legacyTenantId: slot === "primary" ? "official-china" : "official-global",
2796
+ storageScopeId: `builtin-${slot}-v1`,
2797
+ displayName: configured.displayName["zh-CN"],
2798
+ backendOrigin: configured.backendOrigin,
2799
+ didHost: configured.didHost
2800
+ };
2801
+ });
2802
+ /** Immutable snapshot of the DSH endpoint defaults shipped before tenant registries existed. */
2803
+ const PRE_REGISTRY_DEFAULT_DOMAIN = "awiki.info";
2804
+ const PRE_REGISTRY_DEFAULT_ENDPOINTS = endpointsForOrigin(`https://${PRE_REGISTRY_DEFAULT_DOMAIN}`, PRE_REGISTRY_DEFAULT_DOMAIN);
2805
+ function registryFilePath(baseStateRoot) {
2806
+ return join(dirname(baseStateRoot), `${basename(baseStateRoot)}.tenant-registry.json`);
2807
+ }
2808
+ function endpointsForOrigin(origin, domain) {
2809
+ return {
2810
+ userServiceUrl: origin,
2811
+ messageServiceUrl: origin,
2812
+ mailServiceUrl: origin,
2813
+ messageServicePublicUrl: origin,
2814
+ messageServiceDid: `did:wba:${domain}`
2815
+ };
2816
+ }
2817
+ function officialProfile(entry) {
2818
+ return {
2819
+ tenantId: entry.tenantId,
2820
+ storageScopeId: entry.storageScopeId,
2821
+ kind: "built_in",
2822
+ displayName: entry.displayName,
2823
+ backendBaseUrl: entry.backendOrigin,
2824
+ didHost: entry.didHost,
2825
+ lifecycle: "inactive",
2826
+ storageLayout: "scope-v1",
2827
+ endpoints: endpointsForOrigin(entry.backendOrigin, entry.didHost)
2828
+ };
2829
+ }
2830
+ function isRecord$1(value) {
2831
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2832
+ }
2833
+ function safeString(value) {
2834
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2835
+ }
2836
+ function decodeEndpoints(value) {
2837
+ if (!isRecord$1(value)) return void 0;
2838
+ const userServiceUrl = safeString(value.userServiceUrl);
2839
+ const messageServiceUrl = safeString(value.messageServiceUrl);
2840
+ const mailServiceUrl = safeString(value.mailServiceUrl);
2841
+ const messageServicePublicUrl = safeString(value.messageServicePublicUrl);
2842
+ const messageServiceDid = safeString(value.messageServiceDid);
2843
+ if (userServiceUrl === void 0 || messageServiceUrl === void 0 || mailServiceUrl === void 0 || messageServicePublicUrl === void 0 || messageServiceDid === void 0) return void 0;
2844
+ return {
2845
+ userServiceUrl,
2846
+ messageServiceUrl,
2847
+ mailServiceUrl,
2848
+ messageServicePublicUrl,
2849
+ messageServiceDid
2850
+ };
2851
+ }
2852
+ function decodeTenant(value) {
2853
+ if (!isRecord$1(value)) return void 0;
2854
+ const tenantId = safeString(value.tenantId);
2855
+ const storageScopeId = safeString(value.storageScopeId);
2856
+ const displayName = safeString(value.displayName);
2857
+ const backendBaseUrl = safeString(value.backendBaseUrl);
2858
+ const didHost = safeString(value.didHost);
2859
+ const endpoints = decodeEndpoints(value.endpoints);
2860
+ if (tenantId === void 0 || storageScopeId === void 0 || displayName === void 0 || backendBaseUrl === void 0 || didHost === void 0 || endpoints === void 0 || value.kind !== "built_in" && value.kind !== "custom" || value.lifecycle !== "active" && value.lifecycle !== "inactive" && value.lifecycle !== "archived" || value.storageLayout !== "scope-v1" && value.storageLayout !== "legacy-base" && value.storageLayout !== "domain-v1") return;
2861
+ return {
2862
+ tenantId,
2863
+ storageScopeId,
2864
+ kind: value.kind,
2865
+ displayName,
2866
+ backendBaseUrl,
2867
+ didHost,
2868
+ lifecycle: value.lifecycle,
2869
+ storageLayout: value.storageLayout,
2870
+ endpoints
2871
+ };
2872
+ }
2873
+ function decodeDocument(value) {
2874
+ if (!isRecord$1(value) || value.schemaVersion !== 1 || !Number.isSafeInteger(value.officialCatalogVersion) || value.officialCatalogVersion < 1 || value.officialCatalogVersion > 2 || !Number.isSafeInteger(value.generation) || value.generation < 0 || typeof value.activeTenantId !== "string" || !Array.isArray(value.tenants)) return void 0;
2875
+ const tenants = value.tenants.map(decodeTenant);
2876
+ if (tenants.some((tenant) => tenant === void 0)) return void 0;
2877
+ const complete = tenants;
2878
+ if (new Set(complete.map((tenant) => tenant.tenantId)).size !== complete.length || new Set(complete.map((tenant) => tenant.storageScopeId)).size !== complete.length || !complete.some((tenant) => tenant.tenantId === value.activeTenantId && tenant.lifecycle === "active") || complete.filter((tenant) => tenant.lifecycle === "active").length !== 1) return void 0;
2879
+ return {
2880
+ schemaVersion: 1,
2881
+ officialCatalogVersion: 2,
2882
+ generation: value.generation,
2883
+ activeTenantId: value.activeTenantId,
2884
+ tenants: complete
2885
+ };
2886
+ }
2887
+ function directoryHasData(path) {
2888
+ try {
2889
+ return statSync(path).isDirectory() && readdirSync(path).length > 0;
2890
+ } catch {
2891
+ return false;
2892
+ }
2893
+ }
2894
+ function sameOfficialEndpoint(tenant, entry) {
2895
+ const origin = entry.backendOrigin;
2896
+ return tenant.didHost === entry.didHost || tenant.backendBaseUrl.replace(/\/$/u, "") === origin || tenant.endpoints.userServiceUrl.replace(/\/$/u, "") === origin;
2897
+ }
2898
+ function preservedTenantId(tenants, tenant) {
2899
+ const base = `custom-${tenant.storageScopeId}`;
2900
+ if (!tenants.some((candidate) => candidate.tenantId === base)) return base;
2901
+ let suffix = 2;
2902
+ while (tenants.some((candidate) => candidate.tenantId === `${base}-${suffix}`)) suffix += 1;
2903
+ return `${base}-${suffix}`;
2904
+ }
2905
+ function activate(document, tenantId, generation) {
2906
+ return {
2907
+ ...document,
2908
+ generation,
2909
+ activeTenantId: tenantId,
2910
+ tenants: document.tenants.map((tenant) => ({
2911
+ ...tenant,
2912
+ lifecycle: tenant.tenantId === tenantId ? "active" : tenant.lifecycle === "active" ? "inactive" : tenant.lifecycle
2913
+ }))
2914
+ };
2915
+ }
2916
+ function reconcileOfficials(document) {
2917
+ const tenants = [...document.tenants];
2918
+ let activeTenantId = document.activeTenantId;
2919
+ for (const entry of OFFICIALS) {
2920
+ let exactIndex = tenants.findIndex((tenant) => tenant.tenantId === entry.tenantId);
2921
+ if (exactIndex >= 0) {
2922
+ const current = tenants[exactIndex];
2923
+ if (sameOfficialEndpoint(current, entry)) {
2924
+ tenants[exactIndex] = {
2925
+ ...officialProfile(entry),
2926
+ storageScopeId: current.storageScopeId,
2927
+ storageLayout: current.storageLayout,
2928
+ lifecycle: current.lifecycle
2929
+ };
2930
+ continue;
2931
+ }
2932
+ const preservedId = preservedTenantId(tenants, current);
2933
+ tenants[exactIndex] = {
2934
+ ...current,
2935
+ tenantId: preservedId,
2936
+ kind: "custom"
2937
+ };
2938
+ if (activeTenantId === current.tenantId) activeTenantId = preservedId;
2939
+ exactIndex = -1;
2940
+ }
2941
+ const legacyIndex = tenants.findIndex((tenant) => tenant.tenantId === entry.legacyTenantId);
2942
+ if (legacyIndex >= 0) {
2943
+ const current = tenants[legacyIndex];
2944
+ if (sameOfficialEndpoint(current, entry)) {
2945
+ tenants[legacyIndex] = {
2946
+ ...officialProfile(entry),
2947
+ storageScopeId: current.storageScopeId,
2948
+ storageLayout: current.storageLayout,
2949
+ lifecycle: current.lifecycle
2950
+ };
2951
+ if (activeTenantId === current.tenantId) activeTenantId = entry.tenantId;
2952
+ continue;
2953
+ }
2954
+ tenants[legacyIndex] = {
2955
+ ...current,
2956
+ kind: "custom"
2957
+ };
2958
+ }
2959
+ const endpointIndex = tenants.findIndex((tenant) => sameOfficialEndpoint(tenant, entry));
2960
+ if (endpointIndex >= 0) {
2961
+ const current = tenants[endpointIndex];
2962
+ tenants[endpointIndex] = {
2963
+ ...officialProfile(entry),
2964
+ storageScopeId: current.storageScopeId,
2965
+ storageLayout: current.storageLayout,
2966
+ lifecycle: current.lifecycle
2967
+ };
2968
+ if (activeTenantId === current.tenantId) activeTenantId = entry.tenantId;
2969
+ } else tenants.push(officialProfile(entry));
2970
+ }
2971
+ return activate({
2972
+ ...document,
2973
+ officialCatalogVersion: 2,
2974
+ activeTenantId,
2975
+ tenants
2976
+ }, activeTenantId, document.generation);
2977
+ }
2978
+ function cloneDocument(document) {
2979
+ return structuredClone(document);
2980
+ }
2981
+ /** Durable registry. All mutations are synchronous, backup-aware, and atomic. */
2982
+ var AwikiTenantRegistry = class AwikiTenantRegistry {
2983
+ baseStateRoot;
2984
+ filePath;
2985
+ backupPath;
2986
+ diagnosticPath;
2987
+ document;
2988
+ persistent;
2989
+ constructor(baseStateRoot, document, persistent) {
2990
+ this.baseStateRoot = baseStateRoot;
2991
+ this.filePath = registryFilePath(baseStateRoot);
2992
+ this.backupPath = `${this.filePath}.pre-v1.bak`;
2993
+ this.diagnosticPath = `${this.filePath}.migration-error.txt`;
2994
+ this.document = document;
2995
+ this.persistent = persistent;
2996
+ }
2997
+ static open(baseStateRoot, seed) {
2998
+ const filePath = registryFilePath(baseStateRoot);
2999
+ if (existsSync(filePath)) try {
3000
+ const decoded = decodeDocument(JSON.parse(readFileSync(filePath, "utf8")));
3001
+ if (decoded === void 0) throw new Error("registry schema or invariants are invalid");
3002
+ const registry = new AwikiTenantRegistry(baseStateRoot, reconcileOfficials(decoded), true);
3003
+ if (JSON.stringify(decoded) !== JSON.stringify(registry.document)) registry.persist(true);
3004
+ return registry;
2143
3005
  } catch (error) {
2144
- this.logger.warn("AWiki realtime post-sync consumer failed: %s", error instanceof Error ? error.message : "unknown failure");
3006
+ const diagnostic = `AWiki tenant registry was not migrated: ${error instanceof Error ? error.message : "unknown error"}\n`;
3007
+ mkdirSync(dirname(filePath), { recursive: true });
3008
+ writeFileSync(`${filePath}.migration-error.txt`, diagnostic, {
3009
+ encoding: "utf8",
3010
+ mode: 384
3011
+ });
3012
+ return new AwikiTenantRegistry(baseStateRoot, legacyDocument(seed, !seed.configured), false);
2145
3013
  }
3014
+ const hasLegacyData = directoryHasData(baseStateRoot);
3015
+ const document = seed.configured || hasLegacyData ? legacyDocument(seed, hasLegacyData && !seed.configured) : freshDocument();
3016
+ const registry = new AwikiTenantRegistry(baseStateRoot, document, true);
3017
+ registry.persist(false);
3018
+ return registry;
2146
3019
  }
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;
3020
+ snapshot(switching = false) {
3021
+ return {
3022
+ ...cloneDocument(this.document),
3023
+ switching,
3024
+ ...this.persistent ? {} : { diagnostic: this.diagnosticPath }
3025
+ };
3026
+ }
3027
+ active() {
3028
+ const tenant = this.document.tenants.find((candidate) => candidate.tenantId === this.document.activeTenantId);
3029
+ if (tenant === void 0) throw new Error("awiki: active tenant is unavailable");
3030
+ return structuredClone(tenant);
3031
+ }
3032
+ find(tenantId) {
3033
+ const tenant = this.document.tenants.find((candidate) => candidate.tenantId === tenantId);
3034
+ return tenant === void 0 ? void 0 : structuredClone(tenant);
3035
+ }
3036
+ stateRoot(tenant) {
3037
+ if (tenant.storageLayout === "legacy-base") return this.baseStateRoot;
3038
+ if (tenant.storageLayout === "domain-v1") return join(dirname(this.baseStateRoot), "tenants", tenant.didHost, basename(this.baseStateRoot));
3039
+ return join(dirname(this.baseStateRoot), "tenant-scopes", tenant.storageScopeId, basename(this.baseStateRoot));
3040
+ }
3041
+ createCustom(displayName, rawDomain) {
3042
+ const domain = normalizeAwikiDomain(rawDomain);
3043
+ const name = displayName.trim();
3044
+ if (name.length === 0 || Array.from(name).length > 80) throw new TypeError("awiki: tenant name is required and must not exceed 80 characters");
3045
+ if (this.document.tenants.some((tenant) => tenant.didHost === domain || tenant.backendBaseUrl === `https://${domain}`)) throw new Error("awiki: a tenant with this endpoint already exists");
3046
+ const tenant = {
3047
+ tenantId: randomUUID(),
3048
+ storageScopeId: randomUUID(),
3049
+ kind: "custom",
3050
+ displayName: name,
3051
+ backendBaseUrl: `https://${domain}`,
3052
+ didHost: domain,
3053
+ lifecycle: "inactive",
3054
+ storageLayout: "scope-v1",
3055
+ endpoints: endpointsForOrigin(`https://${domain}`, domain)
3056
+ };
3057
+ this.document = {
3058
+ ...this.document,
3059
+ tenants: [...this.document.tenants, tenant]
3060
+ };
3061
+ this.persist(true);
3062
+ return structuredClone(tenant);
3063
+ }
3064
+ renameCustom(tenantId, displayName) {
3065
+ const name = displayName.trim();
3066
+ if (name.length === 0 || Array.from(name).length > 80) throw new TypeError("awiki: tenant name is required and must not exceed 80 characters");
3067
+ const current = this.find(tenantId);
3068
+ if (current === void 0) throw new Error("awiki: tenant does not exist");
3069
+ if (current.kind === "built_in") throw new Error("awiki: official tenants cannot be modified");
3070
+ const updated = {
3071
+ ...current,
3072
+ displayName: name
3073
+ };
3074
+ this.document = {
3075
+ ...this.document,
3076
+ tenants: this.document.tenants.map((tenant) => tenant.tenantId === tenantId ? updated : tenant)
3077
+ };
3078
+ this.persist(true);
3079
+ return structuredClone(updated);
3080
+ }
3081
+ archiveCustom(tenantId) {
3082
+ const current = this.find(tenantId);
3083
+ if (current === void 0) throw new Error("awiki: tenant does not exist");
3084
+ if (current.kind === "built_in") throw new Error("awiki: official tenants cannot be archived");
3085
+ if (current.lifecycle === "active") throw new Error("awiki: switch away before archiving this tenant");
3086
+ this.document = {
3087
+ ...this.document,
3088
+ tenants: this.document.tenants.map((tenant) => tenant.tenantId === tenantId ? {
3089
+ ...tenant,
3090
+ lifecycle: "archived"
3091
+ } : tenant)
3092
+ };
3093
+ this.persist(true);
3094
+ }
3095
+ commitActive(tenantId) {
3096
+ const target = this.document.tenants.find((tenant) => tenant.tenantId === tenantId);
3097
+ if (target === void 0 || target.lifecycle === "archived") throw new Error("awiki: target tenant is unavailable");
3098
+ const previous = this.document;
3099
+ this.document = activate(previous, tenantId, previous.generation + 1);
3100
+ try {
3101
+ this.persist(true);
3102
+ } catch (error) {
3103
+ this.document = previous;
3104
+ throw error;
2154
3105
  }
3106
+ return cloneDocument(this.document);
2155
3107
  }
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();
3108
+ persist(backup) {
3109
+ if (!this.persistent) throw new Error("awiki: tenant registry is read-only until its migration diagnostic is resolved");
3110
+ mkdirSync(dirname(this.filePath), { recursive: true });
3111
+ if (backup && existsSync(this.filePath) && !existsSync(this.backupPath)) copyFileSync(this.filePath, this.backupPath);
3112
+ const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
3113
+ writeFileSync(temporary, `${JSON.stringify(this.document, null, 2)}\n`, {
3114
+ encoding: "utf8",
3115
+ mode: 384
3116
+ });
3117
+ renameSync(temporary, this.filePath);
2164
3118
  }
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();
3119
+ };
3120
+ function freshDocument() {
3121
+ const tenants = OFFICIALS.map(officialProfile);
3122
+ const activeTenantId = AWIKI_BUILTIN_TENANT_CONFIG.defaultSlot === "primary" ? AWIKI_PRIMARY_TENANT_ID : AWIKI_SECONDARY_TENANT_ID;
3123
+ return activate({
3124
+ schemaVersion: 1,
3125
+ officialCatalogVersion: 2,
3126
+ generation: 0,
3127
+ activeTenantId,
3128
+ tenants
3129
+ }, activeTenantId, 0);
3130
+ }
3131
+ function legacyDocument(seed, usePreRegistryDefault) {
3132
+ const selectedSlot = seed.legacyTenantSlot;
3133
+ const selectedEntry = selectedSlot === void 0 ? void 0 : OFFICIALS.find((candidate) => candidate.slot === selectedSlot);
3134
+ const historicalSeed = usePreRegistryDefault && selectedEntry === void 0 ? {
3135
+ ...seed,
3136
+ domain: PRE_REGISTRY_DEFAULT_DOMAIN,
3137
+ ...PRE_REGISTRY_DEFAULT_ENDPOINTS
3138
+ } : seed;
3139
+ const domain = selectedEntry?.didHost ?? normalizeAwikiDomain(historicalSeed.domain);
3140
+ const entry = selectedEntry ?? OFFICIALS.find((candidate) => candidate.didHost === domain);
3141
+ const endpoints = selectedEntry === void 0 ? {
3142
+ userServiceUrl: historicalSeed.userServiceUrl,
3143
+ messageServiceUrl: historicalSeed.messageServiceUrl,
3144
+ mailServiceUrl: historicalSeed.mailServiceUrl,
3145
+ messageServicePublicUrl: historicalSeed.messageServicePublicUrl,
3146
+ messageServiceDid: historicalSeed.messageServiceDid
3147
+ } : endpointsForOrigin(selectedEntry.backendOrigin, selectedEntry.didHost);
3148
+ const tenant = entry === void 0 ? {
3149
+ tenantId: randomUUID(),
3150
+ storageScopeId: randomUUID(),
3151
+ kind: "custom",
3152
+ displayName: domain,
3153
+ backendBaseUrl: historicalSeed.userServiceUrl,
3154
+ didHost: domain,
3155
+ lifecycle: "active",
3156
+ storageLayout: "legacy-base",
3157
+ endpoints
3158
+ } : {
3159
+ ...officialProfile(entry),
3160
+ storageScopeId: randomUUID(),
3161
+ lifecycle: "active",
3162
+ storageLayout: "legacy-base",
3163
+ endpoints
3164
+ };
3165
+ return reconcileOfficials({
3166
+ schemaVersion: 1,
3167
+ officialCatalogVersion: 2,
3168
+ generation: 0,
3169
+ activeTenantId: tenant.tenantId,
3170
+ tenants: [tenant]
3171
+ });
3172
+ }
3173
+ //#endregion
3174
+ //#region lib/types/package-version.generated.js
3175
+ /** Generated from package.json by scripts/sync-package-versions.mjs. */
3176
+ const DSH_AWIKI_PACKAGE_VERSION = "0.3.10";
3177
+ const DSH_AWIKI_MODEL_PROXY_PACKAGE_VERSION = "0.1.6";
3178
+ //#endregion
3179
+ //#region lib/types/update-policy.js
3180
+ /** Tenant-scoped DSH AWiki plugin update policy and verified cache. */
3181
+ const DSH_AWIKI_VERSION = DSH_AWIKI_PACKAGE_VERSION;
3182
+ const DSH_AWIKI_MODEL_PROXY_VERSION = DSH_AWIKI_MODEL_PROXY_PACKAGE_VERSION;
3183
+ const PRODUCT = "dsh-awiki";
3184
+ const CHANNEL = "stable";
3185
+ const MAX_POLICY_BYTES = 1048576;
3186
+ /** Load only this tenant's verified cache before its business runtime starts. */
3187
+ function readAwikiUpdatePolicyStatus(options) {
3188
+ const origin = new URL(options.tenant.backendBaseUrl).origin;
3189
+ const base = {
3190
+ tenantId: options.tenant.tenantId,
3191
+ policyOrigin: origin,
3192
+ tenantGeneration: options.generation,
3193
+ currentPluginVersion: options.currentPluginVersion ?? DSH_AWIKI_VERSION,
3194
+ ...options.currentModelProxyVersion === void 0 ? {} : { currentModelProxyVersion: options.currentModelProxyVersion }
3195
+ };
3196
+ const cached = readCache(policyCachePath(options.stateRoot, options.tenant.tenantId, origin), origin);
3197
+ return cached === void 0 ? {
3198
+ ...base,
3199
+ checkState: "unchecked",
3200
+ offline: false,
3201
+ usedCache: false,
3202
+ policyUnavailable: true,
3203
+ restricted: false,
3204
+ modelProxyRestricted: false
3205
+ } : statusFromPolicy(base, cached.policy, cached.checkedAt, false, true);
3206
+ }
3207
+ async function checkAwikiUpdatePolicy(options) {
3208
+ const origin = new URL(options.tenant.backendBaseUrl).origin;
3209
+ assertPolicyOrigin(origin, options.allowInsecureLoopback === true);
3210
+ const cachePath = policyCachePath(options.stateRoot, options.tenant.tenantId, origin);
3211
+ const cached = readCache(cachePath, origin);
3212
+ const currentPluginVersion = options.currentPluginVersion ?? DSH_AWIKI_VERSION;
3213
+ const base = {
3214
+ tenantId: options.tenant.tenantId,
3215
+ policyOrigin: origin,
3216
+ tenantGeneration: options.generation,
3217
+ currentPluginVersion,
3218
+ ...options.currentModelProxyVersion === void 0 ? {} : { currentModelProxyVersion: options.currentModelProxyVersion }
3219
+ };
3220
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? 15e3);
3221
+ const signal = options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout]);
3222
+ const bounded = async (operation) => {
3223
+ signal.throwIfAborted();
3224
+ let abort;
3225
+ const cancelled = new Promise((_, reject) => {
3226
+ abort = () => {
3227
+ reject(signal.reason);
2173
3228
  };
2174
- const timer = setTimeout(finish, delay);
2175
- this.retryTimer = timer;
2176
- this.resolveRetry = finish;
3229
+ signal.addEventListener("abort", abort, { once: true });
2177
3230
  });
3231
+ try {
3232
+ return await Promise.race([operation, cancelled]);
3233
+ } finally {
3234
+ signal.removeEventListener("abort", abort);
3235
+ }
3236
+ };
3237
+ try {
3238
+ signal.throwIfAborted();
3239
+ const endpoint = new URL("/user-service/v1/server-info", origin);
3240
+ endpoint.searchParams.set("client_platform", "dsh");
3241
+ const response = await bounded((options.fetcher ?? fetch)(endpoint, {
3242
+ method: "GET",
3243
+ headers: {
3244
+ accept: "application/json",
3245
+ "cache-control": "no-store"
3246
+ },
3247
+ cache: "no-store",
3248
+ redirect: "error",
3249
+ signal
3250
+ }));
3251
+ signal.throwIfAborted();
3252
+ if (response.status === 404 && options.tenant.kind === "custom") {
3253
+ rmSync(cachePath, { force: true });
3254
+ return {
3255
+ ...base,
3256
+ offline: false,
3257
+ usedCache: false,
3258
+ policyUnavailable: true,
3259
+ checkState: "unavailable",
3260
+ restricted: false,
3261
+ modelProxyRestricted: false
3262
+ };
3263
+ }
3264
+ if (response.redirected || response.url !== "" && new URL(response.url).origin !== origin) throw new Error("update policy response crossed its tenant origin");
3265
+ if (!response.ok) throw new Error(`policy status ${response.status}`);
3266
+ const bytes = await bounded(readPolicyBody(response));
3267
+ signal.throwIfAborted();
3268
+ if (bytes.byteLength > MAX_POLICY_BYTES) throw new Error("policy response exceeds 1 MiB");
3269
+ const policy = decodeServerInfoPolicy(JSON.parse(Buffer.from(bytes).toString("utf8")), origin, cached?.policy.policy_revision);
3270
+ if (policy === void 0) {
3271
+ rmSync(cachePath, { force: true });
3272
+ return {
3273
+ ...base,
3274
+ offline: false,
3275
+ usedCache: false,
3276
+ policyUnavailable: true,
3277
+ checkState: "unavailable",
3278
+ restricted: false,
3279
+ modelProxyRestricted: false,
3280
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
3281
+ };
3282
+ }
3283
+ if (cached !== void 0 && policy.policy_revision < cached.policy.policy_revision) throw new Error("policy revision moved backwards");
3284
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
3285
+ try {
3286
+ writeCache(cachePath, {
3287
+ checkedAt,
3288
+ policy
3289
+ });
3290
+ } catch {}
3291
+ return statusFromPolicy(base, policy, checkedAt, false, false);
3292
+ } catch (error) {
3293
+ if (options.signal?.aborted === true) throw error;
3294
+ if (cached !== void 0) return {
3295
+ ...statusFromPolicy(base, cached.policy, cached.checkedAt, true, true),
3296
+ checkState: "failed"
3297
+ };
3298
+ return {
3299
+ ...base,
3300
+ offline: true,
3301
+ usedCache: false,
3302
+ policyUnavailable: true,
3303
+ checkState: "failed",
3304
+ restricted: false,
3305
+ modelProxyRestricted: false
3306
+ };
2178
3307
  }
2179
- wakeRetry() {
2180
- const timer = this.retryTimer;
2181
- if (timer !== void 0) clearTimeout(timer);
2182
- this.resolveRetry?.();
3308
+ }
3309
+ async function readPolicyBody(response) {
3310
+ const declaredLength = Number(response.headers.get("content-length"));
3311
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_POLICY_BYTES) throw new Error("policy response exceeds 1 MiB");
3312
+ if (response.body === null) throw new Error("empty policy response");
3313
+ const reader = response.body.getReader();
3314
+ const chunks = [];
3315
+ let length = 0;
3316
+ try {
3317
+ while (true) {
3318
+ const { value, done } = await reader.read();
3319
+ if (done) break;
3320
+ length += value.byteLength;
3321
+ if (length > MAX_POLICY_BYTES) {
3322
+ await reader.cancel();
3323
+ throw new Error("policy response exceeds 1 MiB");
3324
+ }
3325
+ chunks.push(value);
3326
+ }
3327
+ } finally {
3328
+ reader.releaseLock();
2183
3329
  }
2184
- };
3330
+ return Buffer.concat(chunks, length);
3331
+ }
3332
+ function statusFromPolicy(base, policy, checkedAt, offline, usedCache) {
3333
+ const plugin = policy.packages.plugin;
3334
+ const modelProxy = policy.packages.model_proxy;
3335
+ const updateAvailable = compareVersions(base.currentPluginVersion, plugin.recommended_version) < 0 || modelProxy !== void 0 && base.currentModelProxyVersion !== void 0 && compareVersions(base.currentModelProxyVersion, modelProxy.recommended_version) < 0;
3336
+ const command = upgradeCommand(base, plugin, modelProxy);
3337
+ return {
3338
+ ...base,
3339
+ checkState: "ready",
3340
+ updateAvailable,
3341
+ ...command === void 0 ? {} : { upgradeCommand: command },
3342
+ policyRevision: policy.policy_revision,
3343
+ recommendedPluginVersion: plugin.recommended_version,
3344
+ minimumPluginVersion: plugin.min_supported_version,
3345
+ ...modelProxy === void 0 ? {} : {
3346
+ recommendedModelProxyVersion: modelProxy.recommended_version,
3347
+ minimumModelProxyVersion: modelProxy.min_supported_version
3348
+ },
3349
+ releaseNotesUrl: policy.release_notes_url,
3350
+ ...plugin.installable === false ? {} : { pluginTarget: publicTarget(plugin) },
3351
+ ...modelProxy === void 0 || modelProxy.installable === false ? {} : { modelProxyTarget: publicTarget(modelProxy) },
3352
+ offline,
3353
+ usedCache,
3354
+ policyUnavailable: false,
3355
+ restricted: compareVersions(base.currentPluginVersion, plugin.min_supported_version) < 0,
3356
+ modelProxyRestricted: modelProxy !== void 0 && base.currentModelProxyVersion !== void 0 && compareVersions(base.currentModelProxyVersion, modelProxy.min_supported_version) < 0,
3357
+ checkedAt
3358
+ };
3359
+ }
3360
+ /** Exact published targets only; never downgrade a newer installed component. */
3361
+ function upgradeCommand(current, plugin, proxy) {
3362
+ const pluginUpgrade = compareVersions(current.currentPluginVersion, plugin.recommended_version) < 0;
3363
+ const proxyUpgrade = proxy !== void 0 && current.currentModelProxyVersion !== void 0 && compareVersions(current.currentModelProxyVersion, proxy.recommended_version) < 0;
3364
+ if (!pluginUpgrade && !proxyUpgrade || plugin.installable === false || current.currentModelProxyVersion !== void 0 && proxy?.installable === false) return void 0;
3365
+ if (current.currentModelProxyVersion !== void 0 && (proxy === void 0 || compareVersions(current.currentModelProxyVersion, proxy.recommended_version) > 0)) return void 0;
3366
+ const pluginVersion = pluginUpgrade ? plugin.recommended_version : current.currentPluginVersion;
3367
+ if (current.currentModelProxyVersion !== void 0 && proxy?.requires_plugin !== void 0 && !satisfies(pluginVersion, proxy.requires_plugin, { includePrerelease: true })) return void 0;
3368
+ return `dsh plugin add ${[pluginUpgrade ? `@awiki/dsh-plugin@${pluginVersion}` : void 0, proxyUpgrade ? `@awiki/dsh-model-proxy@${proxy.recommended_version}` : void 0].filter(Boolean).join(" ")}`;
3369
+ }
3370
+ function publicTarget(value) {
3371
+ return {
3372
+ name: value.name,
3373
+ recommendedVersion: value.recommended_version,
3374
+ minimumVersion: value.min_supported_version,
3375
+ integrity: value.integrity,
3376
+ ...value.repository === void 0 ? {} : { repository: value.repository },
3377
+ ...value.requires_plugin === void 0 ? {} : { requiresPlugin: value.requires_plugin }
3378
+ };
3379
+ }
3380
+ function decodePolicy(value, origin) {
3381
+ if (!isRecord(value) || value.product !== PRODUCT || value.channel !== CHANNEL || value.policy_origin !== origin || !Number.isSafeInteger(value.policy_revision) || value.policy_revision < 1 || typeof value.published_at !== "string" || Number.isNaN(Date.parse(value.published_at)) || typeof value.release_notes_url !== "string" || !isRecord(value.packages)) throw new Error("invalid update policy");
3382
+ const releaseNotes = value.release_notes_url === "" ? void 0 : new URL(value.release_notes_url);
3383
+ if (releaseNotes !== void 0) assertPolicyOrigin(releaseNotes.origin, origin.startsWith("http://"));
3384
+ const plugin = decodePackage(value.packages.plugin, "@awiki/dsh-plugin");
3385
+ const modelProxy = value.packages.model_proxy === void 0 ? void 0 : decodePackage(value.packages.model_proxy, "@awiki/dsh-model-proxy");
3386
+ if (compareVersions(plugin.min_supported_version, plugin.recommended_version) > 0 || modelProxy !== void 0 && compareVersions(modelProxy.min_supported_version, modelProxy.recommended_version) > 0) throw new Error("minimum version exceeds recommended version");
3387
+ return {
3388
+ product: PRODUCT,
3389
+ channel: CHANNEL,
3390
+ policy_origin: origin,
3391
+ policy_revision: value.policy_revision,
3392
+ published_at: value.published_at,
3393
+ release_notes_url: releaseNotes?.toString() ?? "",
3394
+ packages: {
3395
+ plugin,
3396
+ ...modelProxy === void 0 ? {} : { model_proxy: modelProxy }
3397
+ }
3398
+ };
3399
+ }
3400
+ function decodeServerInfoPolicy(value, origin, minimumRevision = 0) {
3401
+ if (!isRecord(value) || value.schema_version !== 1) throw new Error("invalid server-info");
3402
+ const releases = value.client_versions;
3403
+ if (!isRecord(releases) || releases.schema_version !== 1 || releases.channel !== CHANNEL || releases.policy_origin !== origin || !Number.isSafeInteger(releases.policy_revision) || releases.policy_revision < 1 || typeof releases.published_at !== "string" || Number.isNaN(Date.parse(releases.published_at)) || !isRecord(releases.products) || !isRecord(releases.products.dsh)) throw new Error("invalid client version policy");
3404
+ if (releases.policy_revision < minimumRevision) throw new Error("policy revision moved backwards");
3405
+ const product = releases.products.dsh;
3406
+ if (product.enabled === false) {
3407
+ if (product.compatibility === void 0 || product.compatibility === null) return void 0;
3408
+ if (!isRecord(product.compatibility)) throw new Error("invalid DSH compatibility");
3409
+ const component = (value, name) => {
3410
+ if (!isRecord(value)) throw new Error("invalid DSH component compatibility");
3411
+ return {
3412
+ name,
3413
+ recommended_version: value.recommended_version,
3414
+ min_supported_version: value.minimum_supported_version,
3415
+ installable: false
3416
+ };
3417
+ };
3418
+ return decodePolicy({
3419
+ product: PRODUCT,
3420
+ channel: CHANNEL,
3421
+ policy_origin: origin,
3422
+ policy_revision: releases.policy_revision,
3423
+ published_at: releases.published_at,
3424
+ release_notes_url: product.release_notes_url ?? "",
3425
+ packages: {
3426
+ plugin: component(product.compatibility.plugin, "@awiki/dsh-plugin"),
3427
+ ...product.compatibility.model_proxy == null ? {} : { model_proxy: component(product.compatibility.model_proxy, "@awiki/dsh-model-proxy") }
3428
+ }
3429
+ }, origin);
3430
+ }
3431
+ if (product.enabled !== true || typeof product.release_notes_url !== "string" || !isRecord(product.plugin)) throw new Error("invalid DSH client version policy");
3432
+ const plugin = decodeServerPackage(product.plugin, "@awiki/dsh-plugin");
3433
+ const modelProxy = isRecord(product.model_proxy) && product.model_proxy.enabled === true ? decodeServerPackage(product.model_proxy, "@awiki/dsh-model-proxy") : void 0;
3434
+ return decodePolicy({
3435
+ product: PRODUCT,
3436
+ channel: CHANNEL,
3437
+ policy_origin: releases.policy_origin,
3438
+ policy_revision: releases.policy_revision,
3439
+ published_at: releases.published_at,
3440
+ release_notes_url: product.release_notes_url,
3441
+ packages: {
3442
+ plugin,
3443
+ ...modelProxy === void 0 ? {} : { model_proxy: modelProxy }
3444
+ }
3445
+ }, origin);
3446
+ }
3447
+ function decodeServerPackage(value, expectedName) {
3448
+ return decodePackage({
3449
+ name: value.package_name,
3450
+ recommended_version: value.recommended_version,
3451
+ min_supported_version: value.minimum_supported_version,
3452
+ integrity: value.integrity,
3453
+ repository: value.repository === null ? void 0 : value.repository,
3454
+ requires_plugin: value.requires_plugin === null ? void 0 : value.requires_plugin
3455
+ }, expectedName);
3456
+ }
3457
+ function decodePackage(value, expectedName) {
3458
+ if (!isRecord(value) || value.name !== expectedName || typeof value.recommended_version !== "string" || typeof value.min_supported_version !== "string" || value.installable !== false && (typeof value.integrity !== "string" || !/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(value.integrity)) || value.repository !== void 0 && typeof value.repository !== "string" || value.requires_plugin !== void 0 && typeof value.requires_plugin !== "string") throw new Error("invalid update package target");
3459
+ assertVersion(value.recommended_version);
3460
+ assertVersion(value.min_supported_version);
3461
+ return {
3462
+ name: value.name,
3463
+ recommended_version: value.recommended_version,
3464
+ min_supported_version: value.min_supported_version,
3465
+ ...value.installable === false ? { installable: false } : { integrity: value.integrity },
3466
+ ...value.repository === void 0 ? {} : { repository: value.repository },
3467
+ ...value.requires_plugin === void 0 ? {} : { requires_plugin: value.requires_plugin }
3468
+ };
3469
+ }
3470
+ function policyCachePath(stateRoot, tenantId, origin) {
3471
+ const key = createHash("sha256").update(`${tenantId}\n${origin}\n${PRODUCT}\n${CHANNEL}`, "utf8").digest("hex");
3472
+ return join(stateRoot, "update-policy", `${key}.json`);
3473
+ }
3474
+ function readCache(path, origin) {
3475
+ try {
3476
+ const value = JSON.parse(readFileSync(path, "utf8"));
3477
+ if (!isRecord(value) || typeof value.checkedAt !== "string") return void 0;
3478
+ return {
3479
+ checkedAt: value.checkedAt,
3480
+ policy: decodePolicy(value.policy, origin)
3481
+ };
3482
+ } catch {
3483
+ return;
3484
+ }
3485
+ }
3486
+ function writeCache(path, value) {
3487
+ mkdirSync(dirname(path), {
3488
+ recursive: true,
3489
+ mode: 448
3490
+ });
3491
+ const temporary = `${path}.tmp-${process.pid}`;
3492
+ writeFileSync(temporary, `${JSON.stringify(value, void 0, 2)}\n`, { mode: 384 });
3493
+ renameSync(temporary, path);
3494
+ }
3495
+ function assertPolicyOrigin(origin, allowLoopback) {
3496
+ const url = new URL(origin);
3497
+ if (url.protocol === "https:") return;
3498
+ if (allowLoopback && url.protocol === "http:" && [
3499
+ "localhost",
3500
+ "127.0.0.1",
3501
+ "[::1]"
3502
+ ].includes(url.hostname)) return;
3503
+ throw new Error("update policy origin must use HTTPS");
3504
+ }
3505
+ function isRecord(value) {
3506
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3507
+ }
2185
3508
  //#endregion
2186
3509
  //#region lib/types/index.js
2187
- /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
2188
3510
  var __runInitializers = function(thisArg, initializers, value) {
2189
3511
  var useValue = arguments.length > 2;
2190
3512
  for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
@@ -2231,10 +3553,10 @@ const DEFAULT_ATTACHMENT_MAX_BYTES = 10485760;
2231
3553
  const DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 67108864;
2232
3554
  /** Default browser polling interval while the AWiki drawer is open. */
2233
3555
  const DEFAULT_POLL_INTERVAL_MS = 3e3;
2234
- /** Default AWiki production service origin. */
2235
- const DEFAULT_AWIKI_SERVICE_URL = "https://awiki.ai";
2236
- /** Default authoritative AWiki message-service DID. */
2237
- const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = "did:wba:awiki.ai";
3556
+ /** Package-configured primary AWiki service origin. */
3557
+ const DEFAULT_AWIKI_SERVICE_URL = AWIKI_DEFAULT_BUILTIN_TENANT.backendOrigin;
3558
+ /** Package-configured primary authoritative AWiki message-service DID. */
3559
+ const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = `did:wba:${AWIKI_DEFAULT_BUILTIN_TENANT.didHost}`;
2238
3560
  /** Host-owned model input cap after message minimization. */
2239
3561
  const DEFAULT_SUMMARY_MAX_INPUT_BYTES = 32768;
2240
3562
  /** Hard limit for one user-triggered conversation summary. */
@@ -2243,6 +3565,7 @@ const MAX_SUMMARY_MESSAGES = 50;
2243
3565
  const Config = z.object({
2244
3566
  userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
2245
3567
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
3568
+ legacyTenantSlot: z.string(),
2246
3569
  messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
2247
3570
  mailServiceUrl: z.string(),
2248
3571
  messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
@@ -2260,6 +3583,7 @@ const Config = z.object({
2260
3583
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES)
2261
3584
  });
2262
3585
  const FAILURE_CODES = /* @__PURE__ */ new Set([
3586
+ "client-version-unsupported",
2263
3587
  "not-registered",
2264
3588
  "signed-out",
2265
3589
  "already-registered",
@@ -2269,6 +3593,7 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2269
3593
  "handle-unavailable",
2270
3594
  "not-found",
2271
3595
  "forbidden",
3596
+ "device-rejoin-required",
2272
3597
  "identity-recovery-required",
2273
3598
  "conflict",
2274
3599
  "rate-limited",
@@ -2292,10 +3617,10 @@ const RESUMABLE_JOIN_PHASES = /* @__PURE__ */ new Set([
2292
3617
  "challenge_prepared",
2293
3618
  "response_prepared",
2294
3619
  "response_verified",
2295
- "approval_prepared",
2296
- "authorized"
3620
+ "approval_prepared"
2297
3621
  ]);
2298
3622
  const FAILURE_MESSAGES = {
3623
+ "client-version-unsupported": "This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.",
2299
3624
  "not-registered": "No AWiki identity is registered for this deployment.",
2300
3625
  "signed-out": "This installation is signed out of AWiki.",
2301
3626
  "already-registered": "This deployment already has an AWiki identity.",
@@ -2305,6 +3630,7 @@ const FAILURE_MESSAGES = {
2305
3630
  "handle-unavailable": "The requested AWiki handle is unavailable.",
2306
3631
  "not-found": "The requested AWiki resource was not found.",
2307
3632
  "forbidden": "The AWiki operation is not permitted.",
3633
+ "device-rejoin-required": "This device credential was revoked and must join the Handle again.",
2308
3634
  "identity-recovery-required": "The local AWiki identity must be recovered before it can be used again.",
2309
3635
  "conflict": "The AWiki operation conflicts with current state.",
2310
3636
  "rate-limited": "The AWiki service rate-limited the request.",
@@ -2368,29 +3694,19 @@ function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
2368
3694
  if (url.username !== "" || url.password !== "" || url.hash !== "") throw new TypeError(`awiki: ${field} must not contain credentials or a URL fragment`);
2369
3695
  return raw;
2370
3696
  }
2371
- /** Resolve the only supported post-recovery endpoint without accepting URL-carried state. */
2372
- function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting) {
2373
- if (target?.kind !== "model-proxy-v1" || typeof target.baseURL !== "string") throw new TypeError("awiki: recovery reconciliation target is invalid");
2374
- const baseURL = serviceUrl("recoveryReconciliationTarget.baseURL", target.baseURL, allowInsecureLoopbackForTesting);
2375
- const parsed = new URL(baseURL);
2376
- if (parsed.search !== "") throw new TypeError("awiki: recovery reconciliation target must not contain a query");
2377
- return new URL("/api/identity-recovery", parsed).toString();
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;
3697
+ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting) {
3698
+ if (typeof services !== "object" || services === null || Array.isArray(services)) return void 0;
3699
+ const candidate = services[name];
3700
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
3701
+ const value = candidate;
3702
+ if (value.enabled !== true || typeof value.base_url !== "string") return void 0;
2383
3703
  try {
2384
- const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
2385
- if (text === void 0) return false;
2386
- value = JSON.parse(text);
3704
+ const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
3705
+ if (new URL(baseUrl).search !== "") return void 0;
3706
+ return baseUrl;
2387
3707
  } catch {
2388
- return false;
3708
+ return;
2389
3709
  }
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
3710
  }
2395
3711
  /** Validate a provider domain without inferring it from an API endpoint. */
2396
3712
  function serviceDomain(raw, field = "userServiceDomain") {
@@ -2425,12 +3741,12 @@ function listenerAllowedPeers(raw, enabled) {
2425
3741
  return peers;
2426
3742
  }
2427
3743
  /** Resolve and validate every deployment choice before publishing the service. */
2428
- function resolveConfig(config) {
3744
+ function resolveConfig(ctx, config) {
2429
3745
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
2430
3746
  const configuredStateRoot = config.stateRoot?.trim();
2431
3747
  const configuredDshHome = process.env.DSH_HOME?.trim();
2432
3748
  const dshHome = configuredDshHome === void 0 || configuredDshHome.length === 0 ? join(homedir(), ".dsh") : configuredDshHome;
2433
- const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? join(dshHome, "awiki", "im-core") : configuredStateRoot;
3749
+ const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiStateRoot(ctx, dshHome) : configuredStateRoot;
2434
3750
  if (!isAbsolute(stateRoot)) throw new TypeError("awiki: stateRoot must be an absolute path");
2435
3751
  const attachmentMaxBytes = config.attachmentMaxBytes ?? 10485760;
2436
3752
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) throw new TypeError("awiki: attachmentMaxBytes must be a positive safe integer");
@@ -2446,17 +3762,20 @@ function resolveConfig(config) {
2446
3762
  const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
2447
3763
  const summaryMaxInputBytes = config.summaryMaxInputBytes ?? 32768;
2448
3764
  if (!Number.isSafeInteger(summaryMaxInputBytes) || summaryMaxInputBytes < 1024) throw new TypeError("awiki: summaryMaxInputBytes must be a safe integer of at least 1024");
2449
- const userServiceUrl = serviceUrl("userServiceUrl", config.userServiceUrl ?? "https://awiki.ai", allowInsecureLoopbackForTesting);
2450
- const messageServiceUrl = serviceUrl("messageServiceUrl", config.messageServiceUrl ?? "https://awiki.ai", allowInsecureLoopbackForTesting);
3765
+ const userServiceUrl = serviceUrl("userServiceUrl", config.userServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
3766
+ const messageServiceUrl = serviceUrl("messageServiceUrl", config.messageServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
2451
3767
  const mailServiceUrl = serviceUrl("mailServiceUrl", config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
2452
- const messageServicePublicUrl = serviceUrl("messageServicePublicUrl", config.messageServicePublicUrl ?? "https://awiki.ai", allowInsecureLoopbackForTesting);
3768
+ const messageServicePublicUrl = serviceUrl("messageServicePublicUrl", config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
3769
+ const legacyTenantSlot = config.legacyTenantSlot?.trim();
3770
+ if (legacyTenantSlot !== void 0 && legacyTenantSlot !== "primary" && legacyTenantSlot !== "secondary") throw new TypeError("awiki: legacyTenantSlot must be primary or secondary");
2453
3771
  return {
2454
3772
  userServiceUrl,
2455
- userServiceDomain: serviceDomain(config.userServiceDomain ?? "awiki.ai"),
3773
+ userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
3774
+ ...legacyTenantSlot === void 0 ? {} : { legacyTenantSlot },
2456
3775
  messageServiceUrl,
2457
3776
  mailServiceUrl,
2458
3777
  messageServicePublicUrl,
2459
- messageServiceDid: serviceDid(config.messageServiceDid ?? "did:wba:awiki.ai"),
3778
+ messageServiceDid: serviceDid(config.messageServiceDid ?? DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
2460
3779
  allowedAttachmentOrigins: attachmentOrigins(config.allowedAttachmentOrigins, messageServicePublicUrl, allowInsecureLoopbackForTesting),
2461
3780
  allowInsecureLoopbackForTesting,
2462
3781
  stateRoot,
@@ -2480,6 +3799,15 @@ function failure(code) {
2480
3799
  message: FAILURE_MESSAGES[code]
2481
3800
  };
2482
3801
  }
3802
+ function integrationUnavailable() {
3803
+ return {
3804
+ ok: false,
3805
+ error: {
3806
+ code: "unavailable",
3807
+ message: "Guest messaging is not available for the active AWiki tenant."
3808
+ }
3809
+ };
3810
+ }
2483
3811
  /** Normalize SDK and provider failures without returning remote bodies, credentials, or causes. */
2484
3812
  function normalizeFailure(error) {
2485
3813
  if (error instanceof ProviderUnavailableError) return {
@@ -2583,7 +3911,6 @@ function normalizeRecoveryPrepareRequest(request) {
2583
3911
  }
2584
3912
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
2585
3913
  const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
2586
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
2587
3914
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
2588
3915
  async function readBoundedResponseText(response, maxBytes) {
2589
3916
  const declaredLength = response.headers.get("content-length");
@@ -2794,6 +4121,12 @@ let AwikiService = (() => {
2794
4121
  let _classSuper = TypertRemoteService;
2795
4122
  let _instanceExtraInitializers = [];
2796
4123
  let _getConfig_decorators;
4124
+ let _getIntegration_decorators;
4125
+ let _createIntegration_decorators;
4126
+ let _updateIntegration_decorators;
4127
+ let _rotateIntegrationId_decorators;
4128
+ let _closeIntegration_decorators;
4129
+ let _reopenIntegration_decorators;
2797
4130
  let _getIdentity_decorators;
2798
4131
  let _getSession_decorators;
2799
4132
  let _logout_decorators;
@@ -2801,7 +4134,9 @@ let AwikiService = (() => {
2801
4134
  let _inspectIdentityAccess_decorators;
2802
4135
  let _sendRegistrationOtp_decorators;
2803
4136
  let _registerIdentity_decorators;
4137
+ let _retireDeviceIdentityForRejoin_decorators;
2804
4138
  let _beginDeviceJoin_decorators;
4139
+ let _getIdentityAccessState_decorators;
2805
4140
  let _getDeviceJoinStatus_decorators;
2806
4141
  let _cancelDeviceJoin_decorators;
2807
4142
  let _refreshDeviceManagement_decorators;
@@ -2809,6 +4144,8 @@ let AwikiService = (() => {
2809
4144
  let _approveDeviceJoin_decorators;
2810
4145
  let _rejectDeviceJoin_decorators;
2811
4146
  let _revokeDevice_decorators;
4147
+ let _prepareRootTransfer_decorators;
4148
+ let _confirmRootTransfer_decorators;
2812
4149
  let _updateDisplayName_decorators;
2813
4150
  let _getProfile_decorators;
2814
4151
  let _updateProfile_decorators;
@@ -2817,12 +4154,14 @@ let AwikiService = (() => {
2817
4154
  let _activateRecovery_decorators;
2818
4155
  let _getRecoveryStatus_decorators;
2819
4156
  let _resumeRecovery_decorators;
4157
+ let _enterRecoveredSession_decorators;
2820
4158
  let _discardRecovery_decorators;
2821
4159
  let _resolvePeer_decorators;
2822
4160
  let _createGroup_decorators;
2823
4161
  let _getGroup_decorators;
2824
4162
  let _joinGroup_decorators;
2825
4163
  let _leaveGroup_decorators;
4164
+ let _getDisplayProfiles_decorators;
2826
4165
  let _listGroupMembers_decorators;
2827
4166
  let _addGroupMember_decorators;
2828
4167
  let _removeGroupMember_decorators;
@@ -2846,6 +4185,12 @@ let AwikiService = (() => {
2846
4185
  static {
2847
4186
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
2848
4187
  _getConfig_decorators = [Remote];
4188
+ _getIntegration_decorators = [Remote];
4189
+ _createIntegration_decorators = [Remote];
4190
+ _updateIntegration_decorators = [Remote];
4191
+ _rotateIntegrationId_decorators = [Remote];
4192
+ _closeIntegration_decorators = [Remote];
4193
+ _reopenIntegration_decorators = [Remote];
2849
4194
  _getIdentity_decorators = [Remote];
2850
4195
  _getSession_decorators = [Remote];
2851
4196
  _logout_decorators = [Remote];
@@ -2853,7 +4198,9 @@ let AwikiService = (() => {
2853
4198
  _inspectIdentityAccess_decorators = [Remote];
2854
4199
  _sendRegistrationOtp_decorators = [Remote];
2855
4200
  _registerIdentity_decorators = [Remote];
4201
+ _retireDeviceIdentityForRejoin_decorators = [Remote];
2856
4202
  _beginDeviceJoin_decorators = [Remote];
4203
+ _getIdentityAccessState_decorators = [Remote];
2857
4204
  _getDeviceJoinStatus_decorators = [Remote];
2858
4205
  _cancelDeviceJoin_decorators = [Remote];
2859
4206
  _refreshDeviceManagement_decorators = [Remote];
@@ -2861,6 +4208,8 @@ let AwikiService = (() => {
2861
4208
  _approveDeviceJoin_decorators = [Remote];
2862
4209
  _rejectDeviceJoin_decorators = [Remote];
2863
4210
  _revokeDevice_decorators = [Remote];
4211
+ _prepareRootTransfer_decorators = [Remote];
4212
+ _confirmRootTransfer_decorators = [Remote];
2864
4213
  _updateDisplayName_decorators = [Remote];
2865
4214
  _getProfile_decorators = [Remote];
2866
4215
  _updateProfile_decorators = [Remote];
@@ -2869,12 +4218,14 @@ let AwikiService = (() => {
2869
4218
  _activateRecovery_decorators = [Remote];
2870
4219
  _getRecoveryStatus_decorators = [Remote];
2871
4220
  _resumeRecovery_decorators = [Remote];
4221
+ _enterRecoveredSession_decorators = [Remote];
2872
4222
  _discardRecovery_decorators = [Remote];
2873
4223
  _resolvePeer_decorators = [Remote];
2874
4224
  _createGroup_decorators = [Remote];
2875
4225
  _getGroup_decorators = [Remote];
2876
4226
  _joinGroup_decorators = [Remote];
2877
4227
  _leaveGroup_decorators = [Remote];
4228
+ _getDisplayProfiles_decorators = [Remote];
2878
4229
  _listGroupMembers_decorators = [Remote];
2879
4230
  _addGroupMember_decorators = [Remote];
2880
4231
  _removeGroupMember_decorators = [Remote];
@@ -2905,6 +4256,72 @@ let AwikiService = (() => {
2905
4256
  },
2906
4257
  metadata: _metadata
2907
4258
  }, null, _instanceExtraInitializers);
4259
+ __esDecorate(this, null, _getIntegration_decorators, {
4260
+ kind: "method",
4261
+ name: "getIntegration",
4262
+ static: false,
4263
+ private: false,
4264
+ access: {
4265
+ has: (obj) => "getIntegration" in obj,
4266
+ get: (obj) => obj.getIntegration
4267
+ },
4268
+ metadata: _metadata
4269
+ }, null, _instanceExtraInitializers);
4270
+ __esDecorate(this, null, _createIntegration_decorators, {
4271
+ kind: "method",
4272
+ name: "createIntegration",
4273
+ static: false,
4274
+ private: false,
4275
+ access: {
4276
+ has: (obj) => "createIntegration" in obj,
4277
+ get: (obj) => obj.createIntegration
4278
+ },
4279
+ metadata: _metadata
4280
+ }, null, _instanceExtraInitializers);
4281
+ __esDecorate(this, null, _updateIntegration_decorators, {
4282
+ kind: "method",
4283
+ name: "updateIntegration",
4284
+ static: false,
4285
+ private: false,
4286
+ access: {
4287
+ has: (obj) => "updateIntegration" in obj,
4288
+ get: (obj) => obj.updateIntegration
4289
+ },
4290
+ metadata: _metadata
4291
+ }, null, _instanceExtraInitializers);
4292
+ __esDecorate(this, null, _rotateIntegrationId_decorators, {
4293
+ kind: "method",
4294
+ name: "rotateIntegrationId",
4295
+ static: false,
4296
+ private: false,
4297
+ access: {
4298
+ has: (obj) => "rotateIntegrationId" in obj,
4299
+ get: (obj) => obj.rotateIntegrationId
4300
+ },
4301
+ metadata: _metadata
4302
+ }, null, _instanceExtraInitializers);
4303
+ __esDecorate(this, null, _closeIntegration_decorators, {
4304
+ kind: "method",
4305
+ name: "closeIntegration",
4306
+ static: false,
4307
+ private: false,
4308
+ access: {
4309
+ has: (obj) => "closeIntegration" in obj,
4310
+ get: (obj) => obj.closeIntegration
4311
+ },
4312
+ metadata: _metadata
4313
+ }, null, _instanceExtraInitializers);
4314
+ __esDecorate(this, null, _reopenIntegration_decorators, {
4315
+ kind: "method",
4316
+ name: "reopenIntegration",
4317
+ static: false,
4318
+ private: false,
4319
+ access: {
4320
+ has: (obj) => "reopenIntegration" in obj,
4321
+ get: (obj) => obj.reopenIntegration
4322
+ },
4323
+ metadata: _metadata
4324
+ }, null, _instanceExtraInitializers);
2908
4325
  __esDecorate(this, null, _getIdentity_decorators, {
2909
4326
  kind: "method",
2910
4327
  name: "getIdentity",
@@ -2982,6 +4399,17 @@ let AwikiService = (() => {
2982
4399
  },
2983
4400
  metadata: _metadata
2984
4401
  }, null, _instanceExtraInitializers);
4402
+ __esDecorate(this, null, _retireDeviceIdentityForRejoin_decorators, {
4403
+ kind: "method",
4404
+ name: "retireDeviceIdentityForRejoin",
4405
+ static: false,
4406
+ private: false,
4407
+ access: {
4408
+ has: (obj) => "retireDeviceIdentityForRejoin" in obj,
4409
+ get: (obj) => obj.retireDeviceIdentityForRejoin
4410
+ },
4411
+ metadata: _metadata
4412
+ }, null, _instanceExtraInitializers);
2985
4413
  __esDecorate(this, null, _beginDeviceJoin_decorators, {
2986
4414
  kind: "method",
2987
4415
  name: "beginDeviceJoin",
@@ -2993,6 +4421,17 @@ let AwikiService = (() => {
2993
4421
  },
2994
4422
  metadata: _metadata
2995
4423
  }, null, _instanceExtraInitializers);
4424
+ __esDecorate(this, null, _getIdentityAccessState_decorators, {
4425
+ kind: "method",
4426
+ name: "getIdentityAccessState",
4427
+ static: false,
4428
+ private: false,
4429
+ access: {
4430
+ has: (obj) => "getIdentityAccessState" in obj,
4431
+ get: (obj) => obj.getIdentityAccessState
4432
+ },
4433
+ metadata: _metadata
4434
+ }, null, _instanceExtraInitializers);
2996
4435
  __esDecorate(this, null, _getDeviceJoinStatus_decorators, {
2997
4436
  kind: "method",
2998
4437
  name: "getDeviceJoinStatus",
@@ -3070,6 +4509,28 @@ let AwikiService = (() => {
3070
4509
  },
3071
4510
  metadata: _metadata
3072
4511
  }, null, _instanceExtraInitializers);
4512
+ __esDecorate(this, null, _prepareRootTransfer_decorators, {
4513
+ kind: "method",
4514
+ name: "prepareRootTransfer",
4515
+ static: false,
4516
+ private: false,
4517
+ access: {
4518
+ has: (obj) => "prepareRootTransfer" in obj,
4519
+ get: (obj) => obj.prepareRootTransfer
4520
+ },
4521
+ metadata: _metadata
4522
+ }, null, _instanceExtraInitializers);
4523
+ __esDecorate(this, null, _confirmRootTransfer_decorators, {
4524
+ kind: "method",
4525
+ name: "confirmRootTransfer",
4526
+ static: false,
4527
+ private: false,
4528
+ access: {
4529
+ has: (obj) => "confirmRootTransfer" in obj,
4530
+ get: (obj) => obj.confirmRootTransfer
4531
+ },
4532
+ metadata: _metadata
4533
+ }, null, _instanceExtraInitializers);
3073
4534
  __esDecorate(this, null, _updateDisplayName_decorators, {
3074
4535
  kind: "method",
3075
4536
  name: "updateDisplayName",
@@ -3158,6 +4619,17 @@ let AwikiService = (() => {
3158
4619
  },
3159
4620
  metadata: _metadata
3160
4621
  }, null, _instanceExtraInitializers);
4622
+ __esDecorate(this, null, _enterRecoveredSession_decorators, {
4623
+ kind: "method",
4624
+ name: "enterRecoveredSession",
4625
+ static: false,
4626
+ private: false,
4627
+ access: {
4628
+ has: (obj) => "enterRecoveredSession" in obj,
4629
+ get: (obj) => obj.enterRecoveredSession
4630
+ },
4631
+ metadata: _metadata
4632
+ }, null, _instanceExtraInitializers);
3161
4633
  __esDecorate(this, null, _discardRecovery_decorators, {
3162
4634
  kind: "method",
3163
4635
  name: "discardRecovery",
@@ -3219,8 +4691,19 @@ let AwikiService = (() => {
3219
4691
  static: false,
3220
4692
  private: false,
3221
4693
  access: {
3222
- has: (obj) => "leaveGroup" in obj,
3223
- get: (obj) => obj.leaveGroup
4694
+ has: (obj) => "leaveGroup" in obj,
4695
+ get: (obj) => obj.leaveGroup
4696
+ },
4697
+ metadata: _metadata
4698
+ }, null, _instanceExtraInitializers);
4699
+ __esDecorate(this, null, _getDisplayProfiles_decorators, {
4700
+ kind: "method",
4701
+ name: "getDisplayProfiles",
4702
+ static: false,
4703
+ private: false,
4704
+ access: {
4705
+ has: (obj) => "getDisplayProfiles" in obj,
4706
+ get: (obj) => obj.getDisplayProfiles
3224
4707
  },
3225
4708
  metadata: _metadata
3226
4709
  }, null, _instanceExtraInitializers);
@@ -3445,11 +4928,23 @@ let AwikiService = (() => {
3445
4928
  resolved = __runInitializers(this, _instanceExtraInitializers);
3446
4929
  sessionStore;
3447
4930
  imageAttachmentCache;
3448
- sentMailStore;
3449
4931
  conversationPreferenceStore;
3450
4932
  startupUserServiceDomain;
3451
4933
  settingsProvider;
4934
+ tenantRegistry;
4935
+ activeTenant;
4936
+ activeClientOptions;
4937
+ clientFactory;
3452
4938
  provider;
4939
+ runtimeGeneration = 0;
4940
+ tenantSwitching = false;
4941
+ tenantParticipants = /* @__PURE__ */ new Set();
4942
+ activeCapabilities;
4943
+ updatePolicyStatus;
4944
+ updatePolicyRequest;
4945
+ updatePolicyTask;
4946
+ updatePolicyTaskKey;
4947
+ desktopDistribution;
3453
4948
  signedOut;
3454
4949
  sessionMutation = Promise.resolve();
3455
4950
  sessionRevision = 0;
@@ -3460,12 +4955,14 @@ let AwikiService = (() => {
3460
4955
  requestSessions = /* @__PURE__ */ new Map();
3461
4956
  deviceRefs = /* @__PURE__ */ new Map();
3462
4957
  deviceIds = /* @__PURE__ */ new Map();
4958
+ rootTransfers = /* @__PURE__ */ new Map();
3463
4959
  activeSummaryRequests = /* @__PURE__ */ new Set();
3464
4960
  summaryProvider;
3465
- recoveryReconciliationTarget;
3466
4961
  hostContext;
3467
4962
  /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
3468
4963
  externalHttpAuth;
4964
+ integrationClient;
4965
+ mailListClient;
3469
4966
  workspaceContext;
3470
4967
  /**
3471
4968
  * @param ctx - owning Host context.
@@ -3474,12 +4971,8 @@ let AwikiService = (() => {
3474
4971
  constructor(ctx, config) {
3475
4972
  super(ctx, "awiki");
3476
4973
  this.hostContext = ctx;
3477
- this.resolved = resolveConfig(config);
4974
+ this.resolved = resolveConfig(ctx, config);
3478
4975
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
3479
- this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
3480
- this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
3481
- this.sentMailStore = new AwikiSentMailStore(this.resolved.stateRoot);
3482
- this.conversationPreferenceStore = new AwikiConversationPreferenceStore(this.resolved.stateRoot);
3483
4976
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
3484
4977
  ctx.inject(["settings"], (settingsCtx) => {
3485
4978
  const provider = settingsCtx.settings;
@@ -3498,7 +4991,17 @@ let AwikiService = (() => {
3498
4991
  }, "awiki: release settings namespace");
3499
4992
  });
3500
4993
  ctx.inject(["connection"], (connectionCtx) => {
3501
- connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: "loopback" });
4994
+ connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider, {
4995
+ describe: () => this.getTenantRegistryView(),
4996
+ create: (displayName, domain) => this.createCustomTenant(displayName, domain),
4997
+ rename: (tenantId, displayName) => this.renameCustomTenant(tenantId, displayName),
4998
+ switch: (tenantId) => this.switchTenant(tenantId),
4999
+ archive: (tenantId) => this.archiveCustomTenant(tenantId),
5000
+ describeUpdate: () => this.getUpdatePolicyStatus(),
5001
+ refreshUpdate: () => this.refreshUpdatePolicy(),
5002
+ describeDesktopUpdate: () => this.getDesktopUpdate(),
5003
+ refreshDesktopUpdate: () => this.refreshDesktopUpdate()
5004
+ }), { authority: "loopback" });
3502
5005
  });
3503
5006
  ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
3504
5007
  this.workspaceContext = workspaceCtx;
@@ -3511,8 +5014,28 @@ let AwikiService = (() => {
3511
5014
  if (current !== void 0) await this.stopAgentConsumer(current);
3512
5015
  }, "awiki: release Workspace listener composition");
3513
5016
  });
5017
+ ctx.inject(["desktopDistribution"], (desktopCtx) => {
5018
+ const service = desktopCtx.get("desktopDistribution");
5019
+ if (typeof service !== "object" || service === null || !("getSnapshot" in service) || typeof service.getSnapshot !== "function" || !("check" in service) || typeof service.check !== "function") return;
5020
+ const candidate = service;
5021
+ if (decodeDesktopDistribution(candidate.getSnapshot()) === void 0) return;
5022
+ this.desktopDistribution = candidate;
5023
+ desktopCtx.effect(() => () => {
5024
+ if (this.desktopDistribution === candidate) this.desktopDistribution = void 0;
5025
+ });
5026
+ });
5027
+ ctx.effect(() => {
5028
+ const timer = setInterval(() => {
5029
+ this.refreshUpdatePolicy().catch(() => void 0);
5030
+ }, 216e5);
5031
+ timer.unref();
5032
+ return () => {
5033
+ clearInterval(timer);
5034
+ };
5035
+ });
3514
5036
  registerAwikiTools(ctx, this);
3515
5037
  ctx.effect(() => async () => {
5038
+ this.updatePolicyRequest?.abort();
3516
5039
  const provider = this.provider;
3517
5040
  if (provider !== void 0) await this.disposeProvider(provider);
3518
5041
  }, "awiki: dispose current client provider");
@@ -3528,30 +5051,276 @@ let AwikiService = (() => {
3528
5051
  * @returns asynchronous disposer for the exact registered client.
3529
5052
  */
3530
5053
  registerClientFactory(factory) {
3531
- if (this.provider !== void 0) throw new Error("awiki: a client provider is already registered");
5054
+ if (this.clientFactory !== void 0 || this.provider !== void 0) throw new Error("awiki: a client provider is already registered");
5055
+ this.clientFactory = factory;
5056
+ try {
5057
+ this.openTenantProvider(this.ensureTenantRegistry().active(), factory);
5058
+ } catch (error) {
5059
+ this.clientFactory = void 0;
5060
+ throw error;
5061
+ }
5062
+ let active = true;
5063
+ return async () => {
5064
+ if (!active) return;
5065
+ active = false;
5066
+ this.clientFactory = void 0;
5067
+ const current = this.provider;
5068
+ if (current !== void 0) await this.disposeProvider(current);
5069
+ };
5070
+ }
5071
+ ensureTenantRegistry() {
5072
+ if (this.tenantRegistry !== void 0) return this.tenantRegistry;
5073
+ const domain = this.startupUserServiceDomain;
5074
+ const configuredOrigin = `https://${this.resolved.userServiceDomain}`;
5075
+ const tenantOrigin = `https://${domain}`;
5076
+ const derive = (value) => value.replace(/\/$/u, "") === configuredOrigin ? tenantOrigin : value;
5077
+ const seed = {
5078
+ domain,
5079
+ configured: domain !== DEFAULT_AWIKI_DOMAIN || this.resolved.userServiceUrl.replace(/\/$/u, "") !== configuredOrigin || this.resolved.messageServiceUrl.replace(/\/$/u, "") !== configuredOrigin,
5080
+ ...this.resolved.legacyTenantSlot === void 0 ? {} : { legacyTenantSlot: this.resolved.legacyTenantSlot },
5081
+ userServiceUrl: derive(this.resolved.userServiceUrl),
5082
+ messageServiceUrl: derive(this.resolved.messageServiceUrl),
5083
+ mailServiceUrl: derive(this.resolved.mailServiceUrl),
5084
+ messageServicePublicUrl: derive(this.resolved.messageServicePublicUrl),
5085
+ messageServiceDid: this.resolved.messageServiceDid === `did:wba:${this.resolved.userServiceDomain}` ? `did:wba:${domain}` : this.resolved.messageServiceDid
5086
+ };
5087
+ this.tenantRegistry = AwikiTenantRegistry.open(this.resolved.stateRoot, seed);
5088
+ return this.tenantRegistry;
5089
+ }
5090
+ optionsForTenant(tenant) {
5091
+ const endpoints = tenant.endpoints;
5092
+ const messageServicePublicOrigin = new URL(endpoints.messageServicePublicUrl).origin;
5093
+ return {
5094
+ ...endpoints,
5095
+ userServiceDomain: tenant.didHost,
5096
+ allowedAttachmentOrigins: tenant.storageLayout === "legacy-base" ? this.resolved.allowedAttachmentOrigins : [messageServicePublicOrigin],
5097
+ attachmentMaxBytes: this.resolved.attachmentMaxBytes,
5098
+ allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
5099
+ stateRoot: this.ensureTenantRegistry().stateRoot(tenant)
5100
+ };
5101
+ }
5102
+ ensureTenantOptions() {
5103
+ if (this.activeClientOptions !== void 0) return this.activeClientOptions;
5104
+ const tenant = this.ensureTenantRegistry().active();
5105
+ const options = this.optionsForTenant(tenant);
5106
+ this.bindTenantState(tenant, options);
5107
+ return options;
5108
+ }
5109
+ bindTenantState(tenant, options) {
5110
+ this.updatePolicyRequest?.abort();
5111
+ this.updatePolicyStatus = void 0;
5112
+ this.updatePolicyTask = void 0;
5113
+ this.activeTenant = tenant;
5114
+ this.activeClientOptions = options;
5115
+ this.startupUserServiceDomain = tenant.didHost;
5116
+ this.sessionStore = new AwikiSessionStore(options.stateRoot);
5117
+ this.imageAttachmentCache = new AwikiImageAttachmentCache(options.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
5118
+ this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
5119
+ this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
5120
+ this.integrationClient = void 0;
5121
+ this.activeCapabilities = {
5122
+ tenantId: tenant.tenantId,
5123
+ generation: this.runtimeGeneration,
5124
+ online: false,
5125
+ handleRecoveryPhoneEnabled: false
5126
+ };
5127
+ this.signedOut = void 0;
5128
+ this.activeIdentityDid = void 0;
3532
5129
  this.pendingDeviceJoin = void 0;
3533
5130
  this.activeDeviceJoinSessionId = void 0;
5131
+ }
5132
+ openTenantProvider(tenant, factory, startRuntime = true) {
5133
+ const options = this.optionsForTenant(tenant);
3534
5134
  const provider = {
3535
- client: factory({
3536
- userServiceUrl: this.resolved.userServiceUrl,
3537
- userServiceDomain: this.startupUserServiceDomain,
3538
- messageServiceUrl: this.resolved.messageServiceUrl,
3539
- mailServiceUrl: this.resolved.mailServiceUrl,
3540
- messageServicePublicUrl: this.resolved.messageServicePublicUrl,
3541
- messageServiceDid: this.resolved.messageServiceDid,
3542
- allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
3543
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3544
- allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
3545
- stateRoot: this.resolved.stateRoot
3546
- }),
5135
+ client: factory(options),
3547
5136
  realtimeGeneration: 0,
3548
5137
  agentConsumerGeneration: 0,
3549
5138
  localDeviceJoinRequestCountAfterSync: 0
3550
5139
  };
5140
+ this.bindTenantState(tenant, options);
3551
5141
  this.provider = provider;
3552
- this.ensureRealtimeSupervisor(provider);
3553
- this.ensureAgentConsumer(provider);
3554
- return () => this.disposeProvider(provider);
5142
+ this.refreshUpdatePolicy().catch(() => void 0);
5143
+ if (startRuntime) this.ensureProviderRuntime(provider);
5144
+ return provider;
5145
+ }
5146
+ /** Read the Host-owned catalog for trusted loopback settings surfaces. */
5147
+ getTenantRegistryView() {
5148
+ return this.ensureTenantRegistry().snapshot(this.tenantSwitching);
5149
+ }
5150
+ /** Browser-safe, same-process update state for Desktop and loopback settings. */
5151
+ getUpdatePolicyStatus() {
5152
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
5153
+ const modelProxyVersion = this.currentModelProxyVersion();
5154
+ return this.updatePolicyStatus ?? readAwikiUpdatePolicyStatus({
5155
+ tenant,
5156
+ generation: this.runtimeGeneration,
5157
+ stateRoot: this.resolved.stateRoot,
5158
+ currentPluginVersion: DSH_AWIKI_VERSION,
5159
+ ...modelProxyVersion === void 0 ? {} : { currentModelProxyVersion: modelProxyVersion }
5160
+ });
5161
+ }
5162
+ /** Only the Desktop provider owns its release source; tenants cannot override it. */
5163
+ getDesktopUpdate() {
5164
+ return decodeDesktopDistribution(this.desktopDistribution?.getSnapshot());
5165
+ }
5166
+ async refreshDesktopUpdate() {
5167
+ const provider = this.desktopDistribution;
5168
+ if (provider === void 0) return void 0;
5169
+ const value = await provider.check();
5170
+ return provider === this.desktopDistribution ? decodeDesktopDistribution(value) : this.getDesktopUpdate();
5171
+ }
5172
+ /** Refresh only the active generation; late results from old tenants are discarded. */
5173
+ refreshUpdatePolicy() {
5174
+ const key = JSON.stringify([this.runtimeGeneration, this.currentModelProxyVersion()]);
5175
+ if (this.updatePolicyTask !== void 0 && this.updatePolicyTaskKey === key) return this.updatePolicyTask;
5176
+ const task = this.performUpdatePolicyRefresh().finally(() => {
5177
+ if (this.updatePolicyTask === task) this.updatePolicyTask = void 0;
5178
+ });
5179
+ this.updatePolicyTask = task;
5180
+ this.updatePolicyTaskKey = key;
5181
+ return task;
5182
+ }
5183
+ async performUpdatePolicyRefresh() {
5184
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
5185
+ const generation = this.runtimeGeneration;
5186
+ this.updatePolicyRequest?.abort();
5187
+ const request = new AbortController();
5188
+ this.updatePolicyRequest = request;
5189
+ const currentModelProxyVersion = this.currentModelProxyVersion();
5190
+ const status = await checkAwikiUpdatePolicy({
5191
+ tenant,
5192
+ generation,
5193
+ stateRoot: this.resolved.stateRoot,
5194
+ currentPluginVersion: DSH_AWIKI_VERSION,
5195
+ ...currentModelProxyVersion === void 0 ? {} : { currentModelProxyVersion },
5196
+ allowInsecureLoopback: this.resolved.allowInsecureLoopbackForTesting,
5197
+ signal: request.signal
5198
+ });
5199
+ if (this.updatePolicyRequest === request && this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
5200
+ const wasRestricted = this.getUpdatePolicyStatus().restricted;
5201
+ this.updatePolicyStatus = status;
5202
+ this.hostContext.emit("awiki/update-policy", status);
5203
+ if (status.restricted) {
5204
+ this.invalidateSummaries();
5205
+ const provider = this.provider;
5206
+ if (provider !== void 0) try {
5207
+ await this.stopProviderRuntime(provider);
5208
+ } catch (error) {
5209
+ this.ctx.logger("awiki-update").warn("AWiki restricted-version runtime shutdown failed: %s", error instanceof Error ? error.message : "unknown failure");
5210
+ }
5211
+ } else if (wasRestricted && this.provider !== void 0 && !this.tenantSwitching) this.ensureProviderRuntime(this.provider);
5212
+ }
5213
+ return this.getUpdatePolicyStatus();
5214
+ }
5215
+ currentModelProxyVersion() {
5216
+ for (const participant of this.tenantParticipants) if (participant.component?.product === "dsh-awiki-model-proxy") return participant.component.version;
5217
+ }
5218
+ createCustomTenant(displayName, domain) {
5219
+ this.assertVersionSupported();
5220
+ this.ensureTenantRegistry().createCustom(displayName, domain);
5221
+ return this.getTenantRegistryView();
5222
+ }
5223
+ renameCustomTenant(tenantId, displayName) {
5224
+ this.assertVersionSupported();
5225
+ this.ensureTenantRegistry().renameCustom(tenantId, displayName);
5226
+ return this.getTenantRegistryView();
5227
+ }
5228
+ archiveCustomTenant(tenantId) {
5229
+ this.assertVersionSupported();
5230
+ this.ensureTenantRegistry().archiveCustom(tenantId);
5231
+ return this.getTenantRegistryView();
5232
+ }
5233
+ /** Register one same-process resource owner in every transactional tenant change. */
5234
+ registerTenantLifecycleParticipant(participant) {
5235
+ if (this.tenantParticipants.has(participant)) throw new Error("awiki: tenant lifecycle participant is already registered");
5236
+ this.tenantParticipants.add(participant);
5237
+ if (participant.component !== void 0) this.updatePolicyStatus = void 0;
5238
+ this.refreshUpdatePolicy().catch(() => void 0);
5239
+ let active = true;
5240
+ return () => {
5241
+ if (!active) return;
5242
+ active = false;
5243
+ this.tenantParticipants.delete(participant);
5244
+ if (participant.component !== void 0) this.updatePolicyStatus = void 0;
5245
+ this.refreshUpdatePolicy().catch(() => void 0);
5246
+ };
5247
+ }
5248
+ /** Replace the complete Core runtime and commit the active tenant only after opening succeeds. */
5249
+ async switchTenant(tenantId) {
5250
+ if (this.tenantSwitching) throw new Error("awiki: a tenant switch is already in progress");
5251
+ const registry = this.ensureTenantRegistry();
5252
+ const previousTenant = registry.active();
5253
+ if (previousTenant.tenantId === tenantId) return this.getTenantRegistryView();
5254
+ const target = registry.find(tenantId);
5255
+ if (target === void 0 || target.lifecycle === "archived") throw new Error("awiki: target tenant is unavailable");
5256
+ const factory = this.clientFactory;
5257
+ const previousProvider = this.provider;
5258
+ if (factory === void 0 || previousProvider === void 0) throw new Error("awiki: client provider is unavailable");
5259
+ this.tenantSwitching = true;
5260
+ this.updatePolicyRequest?.abort();
5261
+ this.updatePolicyTask = void 0;
5262
+ const context = {
5263
+ from: previousTenant,
5264
+ to: target,
5265
+ generation: ++this.runtimeGeneration
5266
+ };
5267
+ const prepared = [];
5268
+ let targetOpened = false;
5269
+ let committed = false;
5270
+ try {
5271
+ await this.sessionMutation;
5272
+ this.invalidateSummaries();
5273
+ for (const participant of this.tenantParticipants) {
5274
+ await participant.prepareSwitch(context);
5275
+ prepared.push(participant);
5276
+ }
5277
+ await this.disposeProvider(previousProvider);
5278
+ const targetProvider = this.openTenantProvider(target, factory, false);
5279
+ targetOpened = true;
5280
+ await targetProvider.client.getIdentity();
5281
+ await Promise.all([this.discoverTenantCapabilities(3e3), this.refreshUpdatePolicy()]);
5282
+ registry.commitActive(target.tenantId);
5283
+ committed = true;
5284
+ for (const participant of prepared) await participant.commitSwitch?.(context);
5285
+ this.tenantSwitching = false;
5286
+ this.ensureProviderRuntime(targetProvider);
5287
+ const view = this.getTenantRegistryView();
5288
+ this.hostContext.emit("awiki/tenant", view);
5289
+ return view;
5290
+ } catch (error) {
5291
+ const rollbackFailures = [];
5292
+ if (targetOpened) {
5293
+ const current = this.provider;
5294
+ if (current !== void 0) try {
5295
+ await this.disposeProvider(current);
5296
+ } catch (rollbackError) {
5297
+ rollbackFailures.push(rollbackError);
5298
+ }
5299
+ }
5300
+ if (committed) try {
5301
+ registry.commitActive(previousTenant.tenantId);
5302
+ } catch (rollbackError) {
5303
+ rollbackFailures.push(rollbackError);
5304
+ }
5305
+ if (this.provider === void 0) try {
5306
+ const restored = this.openTenantProvider(previousTenant, factory, false);
5307
+ await restored.client.getIdentity();
5308
+ this.ensureProviderRuntime(restored);
5309
+ } catch (rollbackError) {
5310
+ rollbackFailures.push(rollbackError);
5311
+ }
5312
+ for (const participant of prepared.reverse()) try {
5313
+ await participant.rollbackSwitch?.(context);
5314
+ } catch (rollbackError) {
5315
+ rollbackFailures.push(rollbackError);
5316
+ }
5317
+ if (rollbackFailures.length > 0) throw new AggregateError([error, ...rollbackFailures], "awiki: tenant switch failed and rollback was incomplete");
5318
+ this.tenantSwitching = false;
5319
+ if (this.provider !== void 0) this.ensureProviderRuntime(this.provider);
5320
+ throw new Error("awiki: tenant switch failed; the previous tenant was restored", { cause: error });
5321
+ } finally {
5322
+ this.tenantSwitching = false;
5323
+ }
3555
5324
  }
3556
5325
  /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
3557
5326
  getRealtimeDiagnostics() {
@@ -3570,18 +5339,6 @@ let AwikiService = (() => {
3570
5339
  localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0
3571
5340
  };
3572
5341
  }
3573
- /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
3574
- registerRecoveryReconciliationTarget(target) {
3575
- if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
3576
- const registered = Object.freeze({ endpoint: recoveryReconciliationEndpoint(target, this.resolved.allowInsecureLoopbackForTesting) });
3577
- this.recoveryReconciliationTarget = registered;
3578
- let active = true;
3579
- return () => {
3580
- if (!active) return;
3581
- active = false;
3582
- if (this.recoveryReconciliationTarget === registered) this.recoveryReconciliationTarget = void 0;
3583
- };
3584
- }
3585
5342
  /** Register one replaceable conversation-summary provider for this deployment. */
3586
5343
  registerSummaryProvider(provider) {
3587
5344
  if (this.summaryProvider !== void 0) throw new Error("awiki: a summary provider is already registered");
@@ -3598,13 +5355,66 @@ let AwikiService = (() => {
3598
5355
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
3599
5356
  */
3600
5357
  async getConfig() {
3601
- let handleRecoveryPhoneEnabled = false;
5358
+ this.ensureTenantOptions();
5359
+ const capabilities = await this.discoverTenantCapabilities(1e4);
5360
+ if (this.tenantSwitching || capabilities.generation !== this.runtimeGeneration || capabilities.tenantId !== this.activeTenant?.tenantId) return {
5361
+ ok: false,
5362
+ error: failure("conflict")
5363
+ };
5364
+ return {
5365
+ ok: true,
5366
+ value: {
5367
+ pollIntervalMs: this.resolved.pollIntervalMs,
5368
+ attachmentMaxBytes: this.resolved.attachmentMaxBytes,
5369
+ handleRecoveryPhoneEnabled: capabilities.handleRecoveryPhoneEnabled,
5370
+ ...capabilities.guestGatewayBaseUrl === void 0 ? {} : { integrationGuideUrl: new URL("/guest/guide/integration", capabilities.guestGatewayBaseUrl).toString() },
5371
+ tenantId: capabilities.tenantId,
5372
+ tenantGeneration: capabilities.generation,
5373
+ tenantOnline: capabilities.online,
5374
+ services: {
5375
+ modelProxy: { enabled: capabilities.modelProxyBaseUrl !== void 0 },
5376
+ guestGateway: { enabled: capabilities.guestGatewayBaseUrl !== void 0 }
5377
+ }
5378
+ }
5379
+ };
5380
+ }
5381
+ /** Trusted same-process capability binding for optional tenant participants. */
5382
+ getTenantCapabilities() {
5383
+ const capabilities = this.activeCapabilities;
5384
+ if (capabilities === void 0) throw new Error("awiki: tenant capabilities are unavailable");
5385
+ return { ...capabilities };
5386
+ }
5387
+ /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
5388
+ refreshTenantCapabilities() {
5389
+ this.ensureTenantOptions();
5390
+ return this.discoverTenantCapabilities(1e4);
5391
+ }
5392
+ async discoverTenantCapabilities(timeoutMs) {
5393
+ const options = this.activeClientOptions;
5394
+ const tenant = this.activeTenant;
5395
+ if (options === void 0 || tenant === void 0) throw new ProviderUnavailableError();
5396
+ const generation = this.runtimeGeneration;
5397
+ const fallback = {
5398
+ tenantId: tenant.tenantId,
5399
+ generation,
5400
+ online: false,
5401
+ handleRecoveryPhoneEnabled: false
5402
+ };
5403
+ const publish = (capabilities) => {
5404
+ if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
5405
+ this.activeCapabilities = capabilities;
5406
+ this.integrationClient = capabilities.guestGatewayBaseUrl === void 0 ? void 0 : new AwikiIntegrationClient(capabilities.guestGatewayBaseUrl, this.externalHttpAuth);
5407
+ }
5408
+ return capabilities;
5409
+ };
3602
5410
  const abort = new AbortController();
3603
5411
  const timeout = setTimeout(() => {
3604
5412
  abort.abort();
3605
- }, 1e4);
5413
+ }, timeoutMs);
3606
5414
  try {
3607
- const response = await fetch(new URL("/user-service/v1/server-info", this.resolved.userServiceUrl), {
5415
+ const endpoint = new URL("/user-service/v1/server-info", options.userServiceUrl);
5416
+ endpoint.searchParams.set("client_platform", "dsh");
5417
+ const response = await fetch(endpoint, {
3608
5418
  method: "GET",
3609
5419
  headers: {
3610
5420
  accept: "application/json",
@@ -3614,25 +5424,58 @@ let AwikiService = (() => {
3614
5424
  redirect: "error",
3615
5425
  signal: abort.signal
3616
5426
  });
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 {
5427
+ if (!response.ok) return publish(fallback);
5428
+ const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
5429
+ const value = text === void 0 ? void 0 : JSON.parse(text);
5430
+ if (typeof value !== "object" || value === null || value.schema_version !== 1) return publish(fallback);
5431
+ const methods = value.identity?.handle_recovery?.methods;
5432
+ const 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");
5433
+ const services = value.services;
5434
+ const modelProxyBaseUrl = publishedServiceBaseUrl(services, "model_proxy", this.resolved.allowInsecureLoopbackForTesting);
5435
+ const guestGatewayBaseUrl = publishedServiceBaseUrl(services, "guest_gateway", false);
5436
+ return publish({
5437
+ tenantId: tenant.tenantId,
5438
+ generation,
5439
+ online: true,
5440
+ handleRecoveryPhoneEnabled,
5441
+ ...modelProxyBaseUrl === void 0 ? {} : { modelProxyBaseUrl },
5442
+ ...guestGatewayBaseUrl === void 0 ? {} : { guestGatewayBaseUrl }
5443
+ });
5444
+ } catch {
5445
+ return publish(fallback);
5446
+ } finally {
3626
5447
  clearTimeout(timeout);
3627
5448
  }
3628
- return {
3629
- ok: true,
3630
- value: {
3631
- pollIntervalMs: this.resolved.pollIntervalMs,
3632
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3633
- handleRecoveryPhoneEnabled
3634
- }
3635
- };
5449
+ }
5450
+ /** Read the Integration owned by the active full Handle through the fixed Host client. */
5451
+ getIntegration() {
5452
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5453
+ return this.integrationClient?.read() ?? Promise.resolve(integrationUnavailable());
5454
+ }
5455
+ /** Create the active full Handle's only Integration. */
5456
+ createIntegration(request) {
5457
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5458
+ return this.integrationClient?.create(request) ?? Promise.resolve(integrationUnavailable());
5459
+ }
5460
+ /** Update editable Integration fields with optimistic concurrency. */
5461
+ updateIntegration(request) {
5462
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5463
+ return this.integrationClient?.update(request) ?? Promise.resolve(integrationUnavailable());
5464
+ }
5465
+ /** Atomically replace the current public Integration id. */
5466
+ rotateIntegrationId(request) {
5467
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5468
+ return this.integrationClient?.rotate(request) ?? Promise.resolve(integrationUnavailable());
5469
+ }
5470
+ /** Close the Integration and revoke its current public id. */
5471
+ closeIntegration(request) {
5472
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5473
+ return this.integrationClient?.close(request) ?? Promise.resolve(integrationUnavailable());
5474
+ }
5475
+ /** Revalidate one closed Integration and issue a new public id. */
5476
+ reopenIntegration(request) {
5477
+ if (this.getUpdatePolicyStatus().restricted) return Promise.resolve(integrationUnavailable());
5478
+ return this.integrationClient?.reopen(request) ?? Promise.resolve(integrationUnavailable());
3636
5479
  }
3637
5480
  /**
3638
5481
  * Read the deployment's identity status.
@@ -3674,6 +5517,7 @@ let AwikiService = (() => {
3674
5517
  ok: false,
3675
5518
  error: failure("invalid-request")
3676
5519
  });
5520
+ this.sessionRevision += 1;
3677
5521
  return this.mutateSession(async () => {
3678
5522
  if (await this.isSignedOut()) return {
3679
5523
  ok: true,
@@ -3741,12 +5585,17 @@ let AwikiService = (() => {
3741
5585
  }
3742
5586
  /** Classify one configured-domain Handle before selecting the registration or recovery OTP purpose. */
3743
5587
  async inspectIdentityAccess(request) {
3744
- const target = identityAccessTarget(request, this.startupUserServiceDomain);
5588
+ const options = this.activeClientOptions;
5589
+ if (options === void 0) return {
5590
+ ok: false,
5591
+ error: normalizeFailure(new ProviderUnavailableError())
5592
+ };
5593
+ const target = identityAccessTarget(request, options.userServiceDomain);
3745
5594
  if (target === void 0) return {
3746
5595
  ok: false,
3747
5596
  error: failure("invalid-request")
3748
5597
  };
3749
- const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.resolved.userServiceUrl);
5598
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, options.userServiceUrl);
3750
5599
  const abort = new AbortController();
3751
5600
  const timeout = setTimeout(() => {
3752
5601
  abort.abort();
@@ -3863,6 +5712,37 @@ let AwikiService = (() => {
3863
5712
  }
3864
5713
  };
3865
5714
  }
5715
+ /** Retire only this revoked device credential; ordinary local messages remain for the rejoined identity. */
5716
+ retireDeviceIdentityForRejoin() {
5717
+ return this.mutateSession(async () => {
5718
+ const provider = this.provider;
5719
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
5720
+ const result = await this.run((client) => client.retireDefaultIdentityForRejoin(), { allowSignedOut: true });
5721
+ if (!result.ok) {
5722
+ if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
5723
+ return result;
5724
+ }
5725
+ try {
5726
+ await this.sessionStore.signIn();
5727
+ } catch {
5728
+ try {
5729
+ await this.sessionStore.signIn();
5730
+ } catch {
5731
+ this.ctx.logger("awiki").warn("AWiki local session marker could not be refreshed after device retirement");
5732
+ }
5733
+ }
5734
+ this.signedOut = false;
5735
+ this.activeIdentityDid = void 0;
5736
+ this.pendingDeviceJoin = void 0;
5737
+ this.activeDeviceJoinSessionId = void 0;
5738
+ this.invalidateSummaries();
5739
+ this.publishSession({ status: "unregistered" });
5740
+ return {
5741
+ ok: true,
5742
+ value: { completed: true }
5743
+ };
5744
+ });
5745
+ }
3866
5746
  /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
3867
5747
  async beginDeviceJoin() {
3868
5748
  const continuation = this.pendingDeviceJoin;
@@ -3870,20 +5750,53 @@ let AwikiService = (() => {
3870
5750
  ok: false,
3871
5751
  error: failure("conflict")
3872
5752
  };
3873
- if (continuation.mode !== "ordinary" || continuation.requiresUserPresence) return {
5753
+ if (continuation.mode === "ordinary" && continuation.requiresUserPresence || continuation.mode === "handle-recovery-rebind" && !continuation.requiresUserPresence) return {
3874
5754
  ok: false,
3875
5755
  error: failure("forbidden")
3876
5756
  };
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
- }));
5757
+ const result = await this.run(async (client) => {
5758
+ let userPresenceConfirmed = false;
5759
+ if (continuation.mode === "handle-recovery-rebind") {
5760
+ if (!client.trustedUserPresenceSupported) throw Object.assign(/* @__PURE__ */ new Error("trusted user presence unsupported"), {
5761
+ name: "AwikiSdkError",
5762
+ code: "forbidden"
5763
+ });
5764
+ userPresenceConfirmed = await client.confirmUserPresence("Rejoin your recovered AWiki identity on this device");
5765
+ if (!userPresenceConfirmed) throw Object.assign(/* @__PURE__ */ new Error("user presence denied"), {
5766
+ name: "AwikiSdkError",
5767
+ code: "forbidden"
5768
+ });
5769
+ }
5770
+ this.pendingDeviceJoin = void 0;
5771
+ return client.beginDeviceJoin({
5772
+ continuationId: continuation.continuationId,
5773
+ operationId: `dsh-device-join-${randomUUID()}`,
5774
+ userPresenceConfirmed
5775
+ });
5776
+ });
3883
5777
  if (!result.ok) return result;
3884
5778
  this.activeDeviceJoinSessionId = result.value.joinSessionId;
3885
5779
  return this.applyCandidateJoinProgress(result.value);
3886
5780
  }
5781
+ /** Read local workflow authority before offering a new login. No remote mutations. */
5782
+ getIdentityAccessState() {
5783
+ return this.run(async (client) => {
5784
+ const joining = await this.selectDeviceJoinSession(client) !== null;
5785
+ const recoveries = await client.listPendingRecoveries();
5786
+ const domain = this.activeClientOptions?.userServiceDomain;
5787
+ const choice = this.pendingDeviceJoin;
5788
+ return {
5789
+ choice: joining || choice === void 0 ? null : {
5790
+ status: "join-required",
5791
+ fullHandle: choice.fullHandle,
5792
+ mode: choice.mode,
5793
+ requiresUserPresence: choice.requiresUserPresence
5794
+ },
5795
+ joining,
5796
+ recoveries: recoveries.filter((value) => domain !== void 0 && value.fullHandle.slice(value.fullHandle.indexOf(".") + 1) === domain)
5797
+ };
5798
+ }, { allowSignedOut: true });
5799
+ }
3887
5800
  /** Restore from Core local_sessions and advance only the exact resumable Join. */
3888
5801
  async getDeviceJoinStatus() {
3889
5802
  const result = await this.run(async (client) => {
@@ -3976,7 +5889,18 @@ let AwikiService = (() => {
3976
5889
  code: "forbidden"
3977
5890
  });
3978
5891
  const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
3979
- return client.confirmDeviceJoinApproval(prompt.approvalHandle);
5892
+ try {
5893
+ return await client.confirmDeviceJoinApproval(prompt.approvalHandle);
5894
+ } catch (error) {
5895
+ try {
5896
+ await client.syncDeviceManagement();
5897
+ } catch {}
5898
+ try {
5899
+ const recovered = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
5900
+ if (adminJoinPhase(recovered) === "authorized") return recovered;
5901
+ } catch {}
5902
+ throw error;
5903
+ }
3980
5904
  });
3981
5905
  return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3982
5906
  }
@@ -4013,6 +5937,63 @@ let AwikiService = (() => {
4013
5937
  return this.deviceManagementSnapshot(client);
4014
5938
  });
4015
5939
  }
5940
+ /** Prepare a short-lived Core authorization without exposing it to Browser. */
5941
+ async prepareRootTransfer(request) {
5942
+ const deviceId = this.deviceIds.get(request?.deviceRef);
5943
+ if (deviceId === void 0) return {
5944
+ ok: false,
5945
+ error: failure("invalid-request")
5946
+ };
5947
+ return this.run(async (client) => {
5948
+ const did = await this.requireRootTransferRecipient(client, deviceId);
5949
+ const preparation = await client.prepareRootKeyTransfer(deviceId);
5950
+ if (preparation.recipient.did !== did || preparation.recipient.deviceId !== deviceId) throw Object.assign(/* @__PURE__ */ new Error("root transfer recipient mismatch"), {
5951
+ name: "AwikiSdkError",
5952
+ code: "remote"
5953
+ });
5954
+ for (const [reference, pending] of this.rootTransfers) if (pending.deviceId === deviceId) this.rootTransfers.delete(reference);
5955
+ const transferRef = `root-transfer-${randomUUID()}`;
5956
+ this.rootTransfers.set(transferRef, {
5957
+ authorizationHandle: preparation.authorizationHandle,
5958
+ deviceId,
5959
+ deviceRef: request.deviceRef,
5960
+ did
5961
+ });
5962
+ return {
5963
+ transferRef,
5964
+ deviceRef: request.deviceRef,
5965
+ expiresAt: preparation.expiresAt
5966
+ };
5967
+ });
5968
+ }
5969
+ /** Authenticate locally, recheck fresh context, then consume one exact Core authorization. */
5970
+ async confirmRootTransfer(request) {
5971
+ const pending = this.rootTransfers.get(request?.transferRef);
5972
+ if (pending === void 0) return {
5973
+ ok: false,
5974
+ error: failure("invalid-request")
5975
+ };
5976
+ this.rootTransfers.delete(request.transferRef);
5977
+ return this.run(async (client) => {
5978
+ if (!await client.confirmUserPresence("Grant AWiki device management access")) throw Object.assign(/* @__PURE__ */ new Error("user presence denied"), {
5979
+ name: "AwikiSdkError",
5980
+ code: "forbidden"
5981
+ });
5982
+ if (await this.requireRootTransferRecipient(client, pending.deviceId) !== pending.did) throw Object.assign(/* @__PURE__ */ new Error("root transfer identity changed"), {
5983
+ name: "AwikiSdkError",
5984
+ code: "forbidden"
5985
+ });
5986
+ const sent = await client.confirmAndSendRootKeyTransfer(pending.authorizationHandle);
5987
+ if (sent.recipientDeviceId !== pending.deviceId) throw Object.assign(/* @__PURE__ */ new Error("root transfer receipt mismatch"), {
5988
+ name: "AwikiSdkError",
5989
+ code: "remote"
5990
+ });
5991
+ return {
5992
+ deviceRef: pending.deviceRef,
5993
+ acceptedAt: sent.acceptedAt
5994
+ };
5995
+ });
5996
+ }
4016
5997
  /**
4017
5998
  * Update the deployment identity's public WNS display name.
4018
5999
  * @param request - replacement display name selected by the user.
@@ -4068,12 +6049,7 @@ let AwikiService = (() => {
4068
6049
  ok: false,
4069
6050
  error: failure("invalid-request")
4070
6051
  };
4071
- const result = await this.run((client) => client.activateRecovery(normalized), { allowSignedOut: true });
4072
- if (result.ok && !await this.applyRecoveredSession(result.value)) return {
4073
- ok: false,
4074
- error: failure("remote")
4075
- };
4076
- return result;
6052
+ return this.run((client) => client.activateRecovery(normalized), { allowSignedOut: true });
4077
6053
  }
4078
6054
  /** Read durable recovery state before deciding whether a retry is valid. */
4079
6055
  async getRecoveryStatus(request) {
@@ -4082,12 +6058,7 @@ let AwikiService = (() => {
4082
6058
  ok: false,
4083
6059
  error: failure("invalid-request")
4084
6060
  };
4085
- const result = await this.run((client) => client.getRecoveryStatus(normalized), { allowSignedOut: true });
4086
- if (result.ok && !await this.applyRecoveredSession(result.value)) return {
4087
- ok: false,
4088
- error: failure("remote")
4089
- };
4090
- return result;
6061
+ return this.run((client) => client.getRecoveryStatus(normalized), { allowSignedOut: true });
4091
6062
  }
4092
6063
  /** Resume only the exact Core-owned operation selected by the browser. */
4093
6064
  async resumeRecovery(request) {
@@ -4096,12 +6067,23 @@ let AwikiService = (() => {
4096
6067
  ok: false,
4097
6068
  error: failure("invalid-request")
4098
6069
  };
4099
- const result = await this.run((client) => client.resumeRecovery(normalized), { allowSignedOut: true });
4100
- if (result.ok && !await this.applyRecoveredSession(result.value)) return {
6070
+ return this.run((client) => client.resumeRecovery(normalized), { allowSignedOut: true });
6071
+ }
6072
+ /** Explicitly enter an applied identity; reads and late Core completions never sign in. */
6073
+ async enterRecoveredSession(request) {
6074
+ const normalized = normalizeRecoveryOperation(request);
6075
+ if (normalized === void 0) return {
4101
6076
  ok: false,
4102
- error: failure("remote")
6077
+ error: failure("invalid-request")
4103
6078
  };
4104
- return result;
6079
+ const requestGeneration = this.sessionRevision;
6080
+ const result = await this.run((client) => client.getRecoveryStatus(normalized), { allowSignedOut: true });
6081
+ if (!result.ok) return result;
6082
+ if (result.value.phase !== "applied" || !result.value.allowedActions?.includes("activate_identity")) return {
6083
+ ok: false,
6084
+ error: failure("conflict")
6085
+ };
6086
+ return this.applyRecoveredSession(result.value, requestGeneration);
4105
6087
  }
4106
6088
  /** Discard only a pre-attempt operation; Core rejects post-attempt deletion. */
4107
6089
  async discardRecovery(request) {
@@ -4188,6 +6170,13 @@ let AwikiService = (() => {
4188
6170
  } : result;
4189
6171
  }
4190
6172
  /** Read one authoritative versioned member page. */
6173
+ getDisplayProfiles(peers) {
6174
+ if (!Array.isArray(peers) || peers.length > 100 || peers.some((peer) => typeof peer !== "string" || !peer.startsWith("did:"))) return Promise.resolve({
6175
+ ok: false,
6176
+ error: failure("invalid-request")
6177
+ });
6178
+ return this.run((client) => client.getDisplayProfiles(peers));
6179
+ }
4191
6180
  listGroupMembers(request) {
4192
6181
  const groupDid = normalizeGroupDid(request?.groupDid);
4193
6182
  if (groupDid === void 0 || request.cursor !== void 0 && (typeof request.cursor !== "string" || request.cursor.length > 4096) || request.limit !== void 0 && (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 100)) return Promise.resolve({
@@ -4479,9 +6468,11 @@ let AwikiService = (() => {
4479
6468
  error: failure("invalid-request")
4480
6469
  };
4481
6470
  }
6471
+ const requestGeneration = this.sessionRevision;
4482
6472
  return this.run(async (client) => {
4483
- if (normalized.folder !== "sent") return client.listMailInbox(normalized);
4484
- return this.sentMailStore.list(await this.ownerDid(client), normalized);
6473
+ const page = normalized.folder === "sent" ? await this.mailListClient.listOutbound(normalized) : await client.listMailInbox(normalized);
6474
+ if (this.sessionRevision !== requestGeneration) throw new AwikiSdkError("network");
6475
+ return page;
4485
6476
  });
4486
6477
  }
4487
6478
  /** Read one bounded plain-text mail message. */
@@ -4495,15 +6486,7 @@ let AwikiService = (() => {
4495
6486
  error: failure("invalid-request")
4496
6487
  };
4497
6488
  }
4498
- return this.run(async (client) => {
4499
- if (!isLocalSentMailId(normalized.messageId)) return client.readMail(normalized);
4500
- const local = await this.sentMailStore.read(await this.ownerDid(client), normalized.messageId);
4501
- if (local === void 0) throw Object.assign(/* @__PURE__ */ new Error("sent mail not found"), {
4502
- name: "AwikiSdkError",
4503
- code: "not-found"
4504
- });
4505
- return local;
4506
- });
6489
+ return this.run((client) => client.readMail(normalized));
4507
6490
  }
4508
6491
  /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
4509
6492
  markMailRead(request) {
@@ -4520,21 +6503,7 @@ let AwikiService = (() => {
4520
6503
  error: failure("invalid-request")
4521
6504
  };
4522
6505
  }
4523
- return this.run(async (client) => {
4524
- const result = await client.sendMail(normalized);
4525
- if (!result.accepted) return result;
4526
- try {
4527
- const ownerDid = await this.ownerDid(client);
4528
- const account = await client.getMailAccount().catch(() => void 0);
4529
- await this.sentMailStore.append(ownerDid, normalized, result, account);
4530
- return result;
4531
- } catch {
4532
- return result.warnings.length >= 100 ? result : {
4533
- ...result,
4534
- warnings: [...result.warnings, "Sent history could not be saved locally."]
4535
- };
4536
- }
4537
- });
6506
+ return this.run((client) => client.sendMail(normalized));
4538
6507
  }
4539
6508
  /**
4540
6509
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -4547,17 +6516,19 @@ let AwikiService = (() => {
4547
6516
  ok: false,
4548
6517
  error: failure("invalid-request")
4549
6518
  });
6519
+ this.sessionRevision += 1;
4550
6520
  return this.mutateSession(async () => {
4551
6521
  const provider = this.provider;
4552
6522
  if (provider !== void 0) await this.stopProviderRuntime(provider);
4553
- const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
6523
+ let result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
6524
+ if (!result.ok && result.error.code === "remote") result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
4554
6525
  if (!result.ok) {
4555
6526
  if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
4556
6527
  return result;
4557
6528
  }
4558
6529
  try {
4559
6530
  await this.imageAttachmentCache.clear();
4560
- await this.sentMailStore.clear();
6531
+ await clearLegacySentMailCache(this.activeClientOptions?.stateRoot ?? this.resolved.stateRoot);
4561
6532
  await this.conversationPreferenceStore.clear();
4562
6533
  await this.sessionStore.signIn();
4563
6534
  this.signedOut = false;
@@ -4576,23 +6547,64 @@ let AwikiService = (() => {
4576
6547
  });
4577
6548
  }
4578
6549
  /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
4579
- async applyRecoveredSession(progress) {
4580
- if (progress.phase !== "applied") return true;
6550
+ async applyRecoveredSession(progress, requestGeneration) {
6551
+ if (progress.phase !== "applied") return {
6552
+ ok: true,
6553
+ value: progress
6554
+ };
4581
6555
  return this.mutateSession(async () => {
4582
6556
  const provider = this.provider;
4583
- if (provider === void 0) return false;
6557
+ if (provider === void 0) return {
6558
+ ok: false,
6559
+ error: failure("remote")
6560
+ };
6561
+ if (this.sessionRevision !== requestGeneration) return {
6562
+ ok: false,
6563
+ error: {
6564
+ ...failure("remote"),
6565
+ mailRecoveryObservability: fencedMailRecoveryObservability(this.activeIdentityDid === progress.currentDid, "replaced")
6566
+ }
6567
+ };
4584
6568
  try {
4585
6569
  const identity = await provider.client.getIdentity();
4586
- if (identity === null || identity.did !== progress.currentDid) return false;
6570
+ const currentPrincipalMatchesRecovery = identity !== null && identity.did === progress.currentDid;
6571
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) return {
6572
+ ok: false,
6573
+ error: {
6574
+ ...failure("remote"),
6575
+ mailRecoveryObservability: fencedMailRecoveryObservability(currentPrincipalMatchesRecovery, "replaced")
6576
+ }
6577
+ };
6578
+ if (!currentPrincipalMatchesRecovery) return {
6579
+ ok: false,
6580
+ error: {
6581
+ ...failure("remote"),
6582
+ mailRecoveryObservability: fencedMailRecoveryObservability(false, "current")
6583
+ }
6584
+ };
4587
6585
  const alreadyActive = this.signedOut === false && this.activeIdentityDid === identity.did;
4588
6586
  if (!alreadyActive) {
4589
6587
  await this.sessionStore.signIn();
6588
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) return {
6589
+ ok: false,
6590
+ error: {
6591
+ ...failure("remote"),
6592
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, "replaced")
6593
+ }
6594
+ };
4590
6595
  this.signedOut = false;
4591
6596
  this.activeIdentityDid = identity.did;
4592
6597
  this.invalidateSummaries();
6598
+ requestGeneration = this.sessionRevision;
4593
6599
  }
4594
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
4595
- if (this.provider !== provider) return false;
6600
+ const mailRecoveryObservability = await this.reconcileRecoveredMailbox(provider);
6601
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) return {
6602
+ ok: false,
6603
+ error: {
6604
+ ...failure("remote"),
6605
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, "replaced")
6606
+ }
6607
+ };
4596
6608
  if (!alreadyActive) {
4597
6609
  const session = {
4598
6610
  status: "active",
@@ -4601,49 +6613,38 @@ let AwikiService = (() => {
4601
6613
  this.publishSession(session);
4602
6614
  this.ensureProviderRuntime(provider);
4603
6615
  }
4604
- return reconciled;
6616
+ return {
6617
+ ok: true,
6618
+ value: {
6619
+ ...progress,
6620
+ mailRecoveryObservability
6621
+ }
6622
+ };
4605
6623
  } catch {
4606
- return false;
6624
+ return {
6625
+ ok: false,
6626
+ error: failure("remote")
6627
+ };
4607
6628
  }
4608
6629
  });
4609
6630
  }
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;
6631
+ /** Rebind Mail first-use ownership and retain only its closed, secret-free classification. */
6632
+ async reconcileRecoveredMailbox(provider) {
4614
6633
  try {
4615
6634
  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;
6635
+ return successfulMailRecoveryObservability();
6636
+ } catch (error) {
6637
+ return failedMailRecoveryObservability(error);
4636
6638
  }
4637
6639
  }
4638
6640
  /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
4639
6641
  async selectDeviceJoinSession(client) {
4640
- const sessions = await client.listLocalDeviceJoinSessions();
6642
+ const resumable = (await client.listLocalDeviceJoinSessions()).filter((value) => value.side === "new_device" && RESUMABLE_JOIN_PHASES.has(value.localPhase));
4641
6643
  if (this.activeDeviceJoinSessionId !== void 0) {
4642
- const tracked = sessions.find((value) => value.side === "new_device" && value.joinSessionId === this.activeDeviceJoinSessionId);
6644
+ const tracked = resumable.find((value) => value.joinSessionId === this.activeDeviceJoinSessionId);
4643
6645
  if (tracked !== void 0) return tracked.joinSessionId;
4644
6646
  this.activeDeviceJoinSessionId = void 0;
4645
6647
  }
4646
- const resumable = sessions.filter((value) => value.side === "new_device" && RESUMABLE_JOIN_PHASES.has(value.localPhase));
4647
6648
  if (resumable.length > 1) throw Object.assign(/* @__PURE__ */ new Error("multiple local joins"), {
4648
6649
  name: "AwikiSdkError",
4649
6650
  code: "conflict"
@@ -4710,6 +6711,25 @@ let AwikiService = (() => {
4710
6711
  code: "forbidden"
4711
6712
  });
4712
6713
  }
6714
+ async requireRootTransferRecipient(client, deviceId) {
6715
+ if (!client.trustedUserPresenceSupported) throw Object.assign(/* @__PURE__ */ new Error("trusted user presence unsupported"), {
6716
+ name: "AwikiSdkError",
6717
+ code: "forbidden"
6718
+ });
6719
+ await this.requireDeviceManager(client);
6720
+ await client.syncDeviceManagement();
6721
+ const identity = await client.getIdentity();
6722
+ if (identity === null) throw Object.assign(/* @__PURE__ */ new Error("identity required"), {
6723
+ name: "AwikiSdkError",
6724
+ code: "not-registered"
6725
+ });
6726
+ const target = (await client.getDeviceRegistry()).find((device) => device.deviceId === deviceId);
6727
+ if (target === void 0 || target.isCurrent || target.status !== "active" || target.role !== "member" || target.managementReady) throw Object.assign(/* @__PURE__ */ new Error("root transfer recipient unavailable"), {
6728
+ name: "AwikiSdkError",
6729
+ code: "forbidden"
6730
+ });
6731
+ return identity.did;
6732
+ }
4713
6733
  requestRef(joinSessionId) {
4714
6734
  const existing = this.requestRefs.get(joinSessionId);
4715
6735
  if (existing !== void 0) return existing;
@@ -4734,8 +6754,10 @@ let AwikiService = (() => {
4734
6754
  this.requestSessions.clear();
4735
6755
  this.deviceRefs.clear();
4736
6756
  this.deviceIds.clear();
6757
+ this.rootTransfers.clear();
4737
6758
  return {
4738
6759
  canManage: false,
6760
+ rootTransferSupported: client.trustedUserPresenceSupported,
4739
6761
  ...current.role === void 0 ? {} : { role: current.role },
4740
6762
  readiness: current.readiness,
4741
6763
  devices: [],
@@ -4744,11 +6766,20 @@ let AwikiService = (() => {
4744
6766
  }
4745
6767
  const requests = await client.listLocalDeviceJoinRequests();
4746
6768
  await Promise.all(requests.filter((request) => request.claimedByCurrentDevice).map((request) => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => void 0)));
6769
+ const devices = await client.getDeviceRegistry();
6770
+ const identity = await client.getIdentity();
6771
+ const joinedAtByDeviceId = /* @__PURE__ */ new Map();
6772
+ for (const request of requests) {
6773
+ const previous = joinedAtByDeviceId.get(request.protocolDeviceId);
6774
+ if (previous === void 0 || request.issuedAt > previous) joinedAtByDeviceId.set(request.protocolDeviceId, request.issuedAt);
6775
+ }
6776
+ const initialDeviceJoinedAt = identity === null ? void 0 : new Date(identity.registeredAt).toISOString();
4747
6777
  return {
4748
6778
  canManage: true,
6779
+ rootTransferSupported: client.trustedUserPresenceSupported,
4749
6780
  role: "admin",
4750
6781
  readiness: "admin_ready",
4751
- devices: (await client.getDeviceRegistry()).map((device) => this.publicDevice(device)),
6782
+ devices: devices.filter((device) => device.status === "active").map((device) => this.publicDevice(device, joinedAtByDeviceId.get(device.deviceId) ?? (device.isCurrent ? initialDeviceJoinedAt : void 0))),
4752
6783
  requests: requests.map((request) => ({
4753
6784
  requestRef: this.requestRef(request.joinSessionId),
4754
6785
  candidateKeyFingerprint: request.candidateKeyFingerprint,
@@ -4760,9 +6791,12 @@ let AwikiService = (() => {
4760
6791
  }))
4761
6792
  };
4762
6793
  }
4763
- publicDevice(device) {
6794
+ publicDevice(device, joinedAt) {
6795
+ const fingerprint = createHash("sha256").update(device.deviceId).digest("hex").slice(0, 8).toUpperCase();
4764
6796
  return {
4765
6797
  deviceRef: this.deviceRef(device.deviceId),
6798
+ displayId: `${fingerprint.slice(0, 4)}-${fingerprint.slice(4)}`,
6799
+ ...joinedAt === void 0 ? {} : { joinedAt },
4766
6800
  status: device.status,
4767
6801
  role: device.role,
4768
6802
  managementReady: device.managementReady,
@@ -4788,6 +6822,7 @@ let AwikiService = (() => {
4788
6822
  this.requestSessions.clear();
4789
6823
  this.deviceRefs.clear();
4790
6824
  this.deviceIds.clear();
6825
+ this.rootTransfers.clear();
4791
6826
  for (const controller of this.activeSummaryRequests) controller.abort();
4792
6827
  this.activeSummaryRequests.clear();
4793
6828
  }
@@ -4809,6 +6844,11 @@ let AwikiService = (() => {
4809
6844
  /** Invoke the current client and normalize every rejection to a public result. */
4810
6845
  async run(operation, options = {}) {
4811
6846
  try {
6847
+ const generation = this.runtimeGeneration;
6848
+ if (this.getUpdatePolicyStatus().restricted) return {
6849
+ ok: false,
6850
+ error: failure("client-version-unsupported")
6851
+ };
4812
6852
  if (options.allowSignedOut !== true && await this.isSignedOut()) return {
4813
6853
  ok: false,
4814
6854
  error: failure("signed-out")
@@ -4816,6 +6856,10 @@ let AwikiService = (() => {
4816
6856
  const provider = this.provider;
4817
6857
  if (provider === void 0) throw new ProviderUnavailableError();
4818
6858
  const value = await operation(provider.client);
6859
+ if (this.runtimeGeneration !== generation || this.provider !== provider) return {
6860
+ ok: false,
6861
+ error: failure("conflict")
6862
+ };
4819
6863
  if (options.skipAttachmentByteValidation !== true && containsUnexpectedBinary(value, /* @__PURE__ */ new Set())) return {
4820
6864
  ok: false,
4821
6865
  error: failure("remote")
@@ -4831,6 +6875,9 @@ let AwikiService = (() => {
4831
6875
  };
4832
6876
  }
4833
6877
  }
6878
+ assertVersionSupported() {
6879
+ if (this.getUpdatePolicyStatus().restricted) throw new Error("awiki: this plugin version must be updated before using the active tenant");
6880
+ }
4834
6881
  /** Validate mail input before entering the provider and preserve fixed public failures. */
4835
6882
  runValidatedMail(validate, operation) {
4836
6883
  let request;
@@ -4883,12 +6930,22 @@ let AwikiService = (() => {
4883
6930
  }
4884
6931
  /** Serialize sign-in, sign-out, and destructive clear transitions. */
4885
6932
  mutateSession(operation) {
4886
- const pending = this.sessionMutation.then(operation, operation);
6933
+ const generation = this.runtimeGeneration;
6934
+ if (this.tenantSwitching) return Promise.resolve({
6935
+ ok: false,
6936
+ error: failure("conflict")
6937
+ });
6938
+ const invoke = () => this.tenantSwitching || generation !== this.runtimeGeneration ? Promise.resolve({
6939
+ ok: false,
6940
+ error: failure("conflict")
6941
+ }) : operation();
6942
+ const pending = this.sessionMutation.then(invoke, invoke);
4887
6943
  this.sessionMutation = pending.then(() => void 0, () => void 0);
4888
6944
  return pending;
4889
6945
  }
4890
6946
  /** Start identity realtime and the optional Agent consumer without blocking identity success. */
4891
6947
  ensureProviderRuntime(provider) {
6948
+ if (this.tenantSwitching) return;
4892
6949
  const identityDid = this.activeIdentityDid;
4893
6950
  if (provider.runtimeReplacement !== void 0) return;
4894
6951
  if (identityDid !== void 0 && provider.realtimeIdentityDid !== void 0 && provider.realtimeIdentityDid !== identityDid) {
@@ -4914,6 +6971,7 @@ let AwikiService = (() => {
4914
6971
  provider.runtimeReplacement = observed;
4915
6972
  }
4916
6973
  ensureRealtimeSupervisor(provider) {
6974
+ if (this.getUpdatePolicyStatus().restricted) return;
4917
6975
  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
6976
  const generation = provider.realtimeGeneration;
4919
6977
  const source = provider.client.realtime ?? provider.client.listener;
@@ -4959,6 +7017,7 @@ let AwikiService = (() => {
4959
7017
  await provider.agentConsumer?.reconcileOnce();
4960
7018
  }
4961
7019
  ensureAgentConsumer(provider) {
7020
+ if (this.getUpdatePolicyStatus().restricted) return;
4962
7021
  const workspaceContext = this.workspaceContext;
4963
7022
  const source = provider.client.agentInbox ?? provider.client.listener;
4964
7023
  const identityDid = this.activeIdentityDid;
@@ -4973,6 +7032,7 @@ let AwikiService = (() => {
4973
7032
  const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4974
7033
  const consumer = new AwikiAgentListener(source, agents, {
4975
7034
  ...this.resolved.listener,
7035
+ stateRoot: this.activeClientOptions?.stateRoot ?? this.resolved.listener.stateRoot,
4976
7036
  identityScope: identityDid
4977
7037
  }, logger);
4978
7038
  provider.agentConsumer = consumer;
@@ -5047,4 +7107,4 @@ function containsUnexpectedBinary(value, seen) {
5047
7107
  return false;
5048
7108
  }
5049
7109
  //#endregion
5050
- export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_DOMAIN_FIELD, AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_LOGOUT_CONFIRMATION, AWIKI_MAIL_ACCOUNT_TOOL, AWIKI_MAIL_INBOX_TOOL, AWIKI_MAIL_MARK_READ_TOOL, AWIKI_MAIL_READ_TOOL, AWIKI_MAIL_SEND_TOOL, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, AWIKI_SETTINGS_NAMESPACE, AwikiExternalHttpAuthError, AwikiService, AwikiService as default, AwikiSettingsSchema, Config, DEFAULT_ATTACHMENT_MAX_BYTES, DEFAULT_AWIKI_DOMAIN, DEFAULT_AWIKI_MESSAGE_SERVICE_DID, DEFAULT_AWIKI_SERVICE_URL, DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SUMMARY_MAX_INPUT_BYTES, MAX_SUMMARY_MESSAGES, normalizeAwikiDomain, validateAwikiSettings };
7110
+ export { AWIKI_CHINA_TENANT_ID, AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_DOMAIN_FIELD, AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AWIKI_GLOBAL_TENANT_ID, AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_LOGOUT_CONFIRMATION, AWIKI_MAIL_ACCOUNT_TOOL, AWIKI_MAIL_INBOX_TOOL, AWIKI_MAIL_MARK_READ_TOOL, AWIKI_MAIL_READ_TOOL, AWIKI_MAIL_SEND_TOOL, AWIKI_OFFICIAL_CATALOG_VERSION, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, AWIKI_SETTINGS_NAMESPACE, AWIKI_TENANT_REGISTRY_SCHEMA_VERSION, AwikiExternalHttpAuthError, AwikiService, AwikiService as default, AwikiSettingsSchema, Config, DEFAULT_ATTACHMENT_MAX_BYTES, DEFAULT_AWIKI_DOMAIN, DEFAULT_AWIKI_MESSAGE_SERVICE_DID, DEFAULT_AWIKI_SERVICE_URL, DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SUMMARY_MAX_INPUT_BYTES, DSH_AWIKI_MODEL_PROXY_VERSION, DSH_AWIKI_VERSION, MAX_SUMMARY_MESSAGES, compareVersions as compareAwikiPluginVersions, normalizeAwikiDomain, validateAwikiSettings };