@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
@@ -1,4 +1,3 @@
1
- /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
2
1
  var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
2
  var useValue = arguments.length > 2;
4
3
  for (var i = 0; i < initializers.length; i++) {
@@ -33,6 +32,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
33
32
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
33
  done = true;
35
34
  };
35
+ import { decodeDesktopDistribution } from "./desktop-distribution.js";
36
+ /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
36
37
  import { Context } from '@deepseek-ai/cordis';
37
38
  import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
38
39
  import { homedir } from 'node:os';
@@ -42,20 +43,29 @@ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
42
43
  import { settingsNamespace } from '@deepseek-ai/dsh-settings';
43
44
  import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
44
45
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
45
- import { downloadedAttachment } from "./sdk-adapter.js";
46
+ import { AwikiIntegrationClient } from "./integration-client.js";
47
+ import { clearLegacySentMailCache } from "./legacy-sent-mail-cache.js";
48
+ import { AwikiMailListClient } from "./mail-list-client.js";
49
+ import { AwikiSdkError, downloadedAttachment } from "./sdk-adapter.js";
50
+ import { failedMailRecoveryObservability, fencedMailRecoveryObservability, successfulMailRecoveryObservability, } from "./mail-recovery-observability.js";
46
51
  import { registerAwikiTools } from "./tools.js";
47
52
  import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
48
53
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
49
54
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
55
+ import { AWIKI_DEFAULT_BUILTIN_TENANT, } from "./builtin-tenant-config.js";
50
56
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
51
57
  import { createAwikiSettingsRpcHandler } from "./settings-rpc.js";
52
58
  import { AwikiSessionStore } from "./session.js";
53
59
  import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "./attachment-cache.js";
54
- import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
55
60
  import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
56
61
  import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
57
62
  import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
63
+ import { resolveAwikiStateRoot } from "./profile-state.js";
64
+ import { AwikiTenantRegistry, } from "./tenant-registry.js";
65
+ import { checkAwikiUpdatePolicy, readAwikiUpdatePolicyStatus, DSH_AWIKI_VERSION, } from "./update-policy.js";
58
66
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
67
+ export { AWIKI_CHINA_TENANT_ID, AWIKI_GLOBAL_TENANT_ID, AWIKI_OFFICIAL_CATALOG_VERSION, AWIKI_TENANT_REGISTRY_SCHEMA_VERSION, } from "./tenant-registry.js";
68
+ export { DSH_AWIKI_MODEL_PROXY_VERSION, DSH_AWIKI_VERSION, compareVersions as compareAwikiPluginVersions, } from "./update-policy.js";
59
69
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
60
70
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
61
71
  export { AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, 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, } from "./tools.js";
@@ -65,10 +75,10 @@ export const DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
65
75
  export const DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
66
76
  /** Default browser polling interval while the AWiki drawer is open. */
67
77
  export const DEFAULT_POLL_INTERVAL_MS = 3_000;
68
- /** Default AWiki production service origin. */
69
- export const DEFAULT_AWIKI_SERVICE_URL = 'https://awiki.ai';
70
- /** Default authoritative AWiki message-service DID. */
71
- export const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = 'did:wba:awiki.ai';
78
+ /** Package-configured primary AWiki service origin. */
79
+ export const DEFAULT_AWIKI_SERVICE_URL = AWIKI_DEFAULT_BUILTIN_TENANT.backendOrigin;
80
+ /** Package-configured primary authoritative AWiki message-service DID. */
81
+ export const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = `did:wba:${AWIKI_DEFAULT_BUILTIN_TENANT.didHost}`;
72
82
  /** Host-owned model input cap after message minimization. */
73
83
  export const DEFAULT_SUMMARY_MAX_INPUT_BYTES = 32 * 1024;
74
84
  /** Hard limit for one user-triggered conversation summary. */
@@ -77,6 +87,7 @@ export const MAX_SUMMARY_MESSAGES = 50;
77
87
  export const Config = z.object({
78
88
  userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
79
89
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
90
+ legacyTenantSlot: z.string(),
80
91
  messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
81
92
  mailServiceUrl: z.string(),
82
93
  messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
@@ -94,6 +105,7 @@ export const Config = z.object({
94
105
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES),
95
106
  });
96
107
  const FAILURE_CODES = new Set([
108
+ 'client-version-unsupported',
97
109
  'not-registered',
98
110
  'signed-out',
99
111
  'already-registered',
@@ -103,6 +115,7 @@ const FAILURE_CODES = new Set([
103
115
  'handle-unavailable',
104
116
  'not-found',
105
117
  'forbidden',
118
+ 'device-rejoin-required',
106
119
  'identity-recovery-required',
107
120
  'conflict',
108
121
  'rate-limited',
@@ -127,9 +140,9 @@ const RESUMABLE_JOIN_PHASES = new Set([
127
140
  'response_prepared',
128
141
  'response_verified',
129
142
  'approval_prepared',
130
- 'authorized',
131
143
  ]);
132
144
  const FAILURE_MESSAGES = {
145
+ 'client-version-unsupported': 'This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.',
133
146
  'not-registered': 'No AWiki identity is registered for this deployment.',
134
147
  'signed-out': 'This installation is signed out of AWiki.',
135
148
  'already-registered': 'This deployment already has an AWiki identity.',
@@ -139,6 +152,7 @@ const FAILURE_MESSAGES = {
139
152
  'handle-unavailable': 'The requested AWiki handle is unavailable.',
140
153
  'not-found': 'The requested AWiki resource was not found.',
141
154
  'forbidden': 'The AWiki operation is not permitted.',
155
+ 'device-rejoin-required': 'This device credential was revoked and must join the Handle again.',
142
156
  'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
143
157
  'conflict': 'The AWiki operation conflicts with current state.',
144
158
  'rate-limited': 'The AWiki service rate-limited the request.',
@@ -237,41 +251,25 @@ function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
237
251
  }
238
252
  return raw;
239
253
  }
240
- /** Resolve the only supported post-recovery endpoint without accepting URL-carried state. */
241
- function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting) {
242
- if (target?.kind !== 'model-proxy-v1' || typeof target.baseURL !== 'string') {
243
- throw new TypeError('awiki: recovery reconciliation target is invalid');
244
- }
245
- const baseURL = serviceUrl('recoveryReconciliationTarget.baseURL', target.baseURL, allowInsecureLoopbackForTesting);
246
- const parsed = new URL(baseURL);
247
- if (parsed.search !== '') {
248
- throw new TypeError('awiki: recovery reconciliation target must not contain a query');
249
- }
250
- return new URL('/api/identity-recovery', parsed).toString();
251
- }
252
- /** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
253
- async function acceptsRecoveryReconciliation(response) {
254
- if (!response.ok)
255
- return false;
256
- let value;
254
+ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting) {
255
+ if (typeof services !== 'object' || services === null || Array.isArray(services))
256
+ return undefined;
257
+ const candidate = services[name];
258
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
259
+ return undefined;
260
+ const value = candidate;
261
+ if (value.enabled !== true || typeof value.base_url !== 'string')
262
+ return undefined;
257
263
  try {
258
- const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
259
- if (text === undefined)
260
- return false;
261
- value = JSON.parse(text);
264
+ const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
265
+ const parsed = new URL(baseUrl);
266
+ if (parsed.search !== '')
267
+ return undefined;
268
+ return baseUrl;
262
269
  }
263
270
  catch {
264
- return false;
271
+ return undefined;
265
272
  }
266
- if (typeof value !== 'object' || value === null || Array.isArray(value))
267
- return false;
268
- const result = value;
269
- const keys = Object.keys(result).sort();
270
- return keys.length === 2
271
- && keys[0] === 'idempotent'
272
- && keys[1] === 'restored'
273
- && result.restored === true
274
- && typeof result.idempotent === 'boolean';
275
273
  }
276
274
  /** Validate a provider domain without inferring it from an API endpoint. */
277
275
  function serviceDomain(raw, field = 'userServiceDomain') {
@@ -320,7 +318,7 @@ function listenerAllowedPeers(raw, enabled) {
320
318
  return peers;
321
319
  }
322
320
  /** Resolve and validate every deployment choice before publishing the service. */
323
- function resolveConfig(config) {
321
+ function resolveConfig(ctx, config) {
324
322
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
325
323
  const configuredStateRoot = config.stateRoot?.trim();
326
324
  const configuredDshHome = process.env.DSH_HOME?.trim();
@@ -328,7 +326,7 @@ function resolveConfig(config) {
328
326
  ? join(homedir(), '.dsh')
329
327
  : configuredDshHome;
330
328
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
331
- ? join(dshHome, 'awiki', 'im-core')
329
+ ? resolveAwikiStateRoot(ctx, dshHome)
332
330
  : configuredStateRoot;
333
331
  if (!isAbsolute(stateRoot))
334
332
  throw new TypeError('awiki: stateRoot must be an absolute path');
@@ -363,9 +361,16 @@ function resolveConfig(config) {
363
361
  const messageServiceUrl = serviceUrl('messageServiceUrl', config.messageServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
364
362
  const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
365
363
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
364
+ const legacyTenantSlot = config.legacyTenantSlot?.trim();
365
+ if (legacyTenantSlot !== undefined
366
+ && legacyTenantSlot !== 'primary'
367
+ && legacyTenantSlot !== 'secondary') {
368
+ throw new TypeError('awiki: legacyTenantSlot must be primary or secondary');
369
+ }
366
370
  return {
367
371
  userServiceUrl,
368
372
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
373
+ ...legacyTenantSlot === undefined ? {} : { legacyTenantSlot },
369
374
  messageServiceUrl,
370
375
  mailServiceUrl,
371
376
  messageServicePublicUrl,
@@ -390,6 +395,15 @@ function resolveConfig(config) {
390
395
  function failure(code) {
391
396
  return { code, message: FAILURE_MESSAGES[code] };
392
397
  }
398
+ function integrationUnavailable() {
399
+ return {
400
+ ok: false,
401
+ error: {
402
+ code: 'unavailable',
403
+ message: 'Guest messaging is not available for the active AWiki tenant.',
404
+ },
405
+ };
406
+ }
393
407
  /** Normalize SDK and provider failures without returning remote bodies, credentials, or causes. */
394
408
  function normalizeFailure(error) {
395
409
  if (error instanceof ProviderUnavailableError) {
@@ -506,7 +520,6 @@ function normalizeRecoveryPrepareRequest(request) {
506
520
  }
507
521
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
508
522
  const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
509
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
510
523
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
511
524
  async function readBoundedResponseText(response, maxBytes) {
512
525
  const declaredLength = response.headers.get('content-length');
@@ -728,6 +741,12 @@ let AwikiService = (() => {
728
741
  let _classSuper = TypertRemoteService;
729
742
  let _instanceExtraInitializers = [];
730
743
  let _getConfig_decorators;
744
+ let _getIntegration_decorators;
745
+ let _createIntegration_decorators;
746
+ let _updateIntegration_decorators;
747
+ let _rotateIntegrationId_decorators;
748
+ let _closeIntegration_decorators;
749
+ let _reopenIntegration_decorators;
731
750
  let _getIdentity_decorators;
732
751
  let _getSession_decorators;
733
752
  let _logout_decorators;
@@ -735,7 +754,9 @@ let AwikiService = (() => {
735
754
  let _inspectIdentityAccess_decorators;
736
755
  let _sendRegistrationOtp_decorators;
737
756
  let _registerIdentity_decorators;
757
+ let _retireDeviceIdentityForRejoin_decorators;
738
758
  let _beginDeviceJoin_decorators;
759
+ let _getIdentityAccessState_decorators;
739
760
  let _getDeviceJoinStatus_decorators;
740
761
  let _cancelDeviceJoin_decorators;
741
762
  let _refreshDeviceManagement_decorators;
@@ -743,6 +764,8 @@ let AwikiService = (() => {
743
764
  let _approveDeviceJoin_decorators;
744
765
  let _rejectDeviceJoin_decorators;
745
766
  let _revokeDevice_decorators;
767
+ let _prepareRootTransfer_decorators;
768
+ let _confirmRootTransfer_decorators;
746
769
  let _updateDisplayName_decorators;
747
770
  let _getProfile_decorators;
748
771
  let _updateProfile_decorators;
@@ -751,12 +774,14 @@ let AwikiService = (() => {
751
774
  let _activateRecovery_decorators;
752
775
  let _getRecoveryStatus_decorators;
753
776
  let _resumeRecovery_decorators;
777
+ let _enterRecoveredSession_decorators;
754
778
  let _discardRecovery_decorators;
755
779
  let _resolvePeer_decorators;
756
780
  let _createGroup_decorators;
757
781
  let _getGroup_decorators;
758
782
  let _joinGroup_decorators;
759
783
  let _leaveGroup_decorators;
784
+ let _getDisplayProfiles_decorators;
760
785
  let _listGroupMembers_decorators;
761
786
  let _addGroupMember_decorators;
762
787
  let _removeGroupMember_decorators;
@@ -780,6 +805,12 @@ let AwikiService = (() => {
780
805
  static {
781
806
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
782
807
  _getConfig_decorators = [Remote];
808
+ _getIntegration_decorators = [Remote];
809
+ _createIntegration_decorators = [Remote];
810
+ _updateIntegration_decorators = [Remote];
811
+ _rotateIntegrationId_decorators = [Remote];
812
+ _closeIntegration_decorators = [Remote];
813
+ _reopenIntegration_decorators = [Remote];
783
814
  _getIdentity_decorators = [Remote];
784
815
  _getSession_decorators = [Remote];
785
816
  _logout_decorators = [Remote];
@@ -787,7 +818,9 @@ let AwikiService = (() => {
787
818
  _inspectIdentityAccess_decorators = [Remote];
788
819
  _sendRegistrationOtp_decorators = [Remote];
789
820
  _registerIdentity_decorators = [Remote];
821
+ _retireDeviceIdentityForRejoin_decorators = [Remote];
790
822
  _beginDeviceJoin_decorators = [Remote];
823
+ _getIdentityAccessState_decorators = [Remote];
791
824
  _getDeviceJoinStatus_decorators = [Remote];
792
825
  _cancelDeviceJoin_decorators = [Remote];
793
826
  _refreshDeviceManagement_decorators = [Remote];
@@ -795,6 +828,8 @@ let AwikiService = (() => {
795
828
  _approveDeviceJoin_decorators = [Remote];
796
829
  _rejectDeviceJoin_decorators = [Remote];
797
830
  _revokeDevice_decorators = [Remote];
831
+ _prepareRootTransfer_decorators = [Remote];
832
+ _confirmRootTransfer_decorators = [Remote];
798
833
  _updateDisplayName_decorators = [Remote];
799
834
  _getProfile_decorators = [Remote];
800
835
  _updateProfile_decorators = [Remote];
@@ -803,12 +838,14 @@ let AwikiService = (() => {
803
838
  _activateRecovery_decorators = [Remote];
804
839
  _getRecoveryStatus_decorators = [Remote];
805
840
  _resumeRecovery_decorators = [Remote];
841
+ _enterRecoveredSession_decorators = [Remote];
806
842
  _discardRecovery_decorators = [Remote];
807
843
  _resolvePeer_decorators = [Remote];
808
844
  _createGroup_decorators = [Remote];
809
845
  _getGroup_decorators = [Remote];
810
846
  _joinGroup_decorators = [Remote];
811
847
  _leaveGroup_decorators = [Remote];
848
+ _getDisplayProfiles_decorators = [Remote];
812
849
  _listGroupMembers_decorators = [Remote];
813
850
  _addGroupMember_decorators = [Remote];
814
851
  _removeGroupMember_decorators = [Remote];
@@ -829,6 +866,12 @@ let AwikiService = (() => {
829
866
  _sendMail_decorators = [Remote];
830
867
  _clearLocalData_decorators = [Remote];
831
868
  __esDecorate(this, null, _getConfig_decorators, { kind: "method", name: "getConfig", static: false, private: false, access: { has: obj => "getConfig" in obj, get: obj => obj.getConfig }, metadata: _metadata }, null, _instanceExtraInitializers);
869
+ __esDecorate(this, null, _getIntegration_decorators, { kind: "method", name: "getIntegration", static: false, private: false, access: { has: obj => "getIntegration" in obj, get: obj => obj.getIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
870
+ __esDecorate(this, null, _createIntegration_decorators, { kind: "method", name: "createIntegration", static: false, private: false, access: { has: obj => "createIntegration" in obj, get: obj => obj.createIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
871
+ __esDecorate(this, null, _updateIntegration_decorators, { kind: "method", name: "updateIntegration", static: false, private: false, access: { has: obj => "updateIntegration" in obj, get: obj => obj.updateIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
872
+ __esDecorate(this, null, _rotateIntegrationId_decorators, { kind: "method", name: "rotateIntegrationId", static: false, private: false, access: { has: obj => "rotateIntegrationId" in obj, get: obj => obj.rotateIntegrationId }, metadata: _metadata }, null, _instanceExtraInitializers);
873
+ __esDecorate(this, null, _closeIntegration_decorators, { kind: "method", name: "closeIntegration", static: false, private: false, access: { has: obj => "closeIntegration" in obj, get: obj => obj.closeIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
874
+ __esDecorate(this, null, _reopenIntegration_decorators, { kind: "method", name: "reopenIntegration", static: false, private: false, access: { has: obj => "reopenIntegration" in obj, get: obj => obj.reopenIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
832
875
  __esDecorate(this, null, _getIdentity_decorators, { kind: "method", name: "getIdentity", static: false, private: false, access: { has: obj => "getIdentity" in obj, get: obj => obj.getIdentity }, metadata: _metadata }, null, _instanceExtraInitializers);
833
876
  __esDecorate(this, null, _getSession_decorators, { kind: "method", name: "getSession", static: false, private: false, access: { has: obj => "getSession" in obj, get: obj => obj.getSession }, metadata: _metadata }, null, _instanceExtraInitializers);
834
877
  __esDecorate(this, null, _logout_decorators, { kind: "method", name: "logout", static: false, private: false, access: { has: obj => "logout" in obj, get: obj => obj.logout }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -836,7 +879,9 @@ let AwikiService = (() => {
836
879
  __esDecorate(this, null, _inspectIdentityAccess_decorators, { kind: "method", name: "inspectIdentityAccess", static: false, private: false, access: { has: obj => "inspectIdentityAccess" in obj, get: obj => obj.inspectIdentityAccess }, metadata: _metadata }, null, _instanceExtraInitializers);
837
880
  __esDecorate(this, null, _sendRegistrationOtp_decorators, { kind: "method", name: "sendRegistrationOtp", static: false, private: false, access: { has: obj => "sendRegistrationOtp" in obj, get: obj => obj.sendRegistrationOtp }, metadata: _metadata }, null, _instanceExtraInitializers);
838
881
  __esDecorate(this, null, _registerIdentity_decorators, { kind: "method", name: "registerIdentity", static: false, private: false, access: { has: obj => "registerIdentity" in obj, get: obj => obj.registerIdentity }, metadata: _metadata }, null, _instanceExtraInitializers);
882
+ __esDecorate(this, null, _retireDeviceIdentityForRejoin_decorators, { kind: "method", name: "retireDeviceIdentityForRejoin", static: false, private: false, access: { has: obj => "retireDeviceIdentityForRejoin" in obj, get: obj => obj.retireDeviceIdentityForRejoin }, metadata: _metadata }, null, _instanceExtraInitializers);
839
883
  __esDecorate(this, null, _beginDeviceJoin_decorators, { kind: "method", name: "beginDeviceJoin", static: false, private: false, access: { has: obj => "beginDeviceJoin" in obj, get: obj => obj.beginDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
884
+ __esDecorate(this, null, _getIdentityAccessState_decorators, { kind: "method", name: "getIdentityAccessState", static: false, private: false, access: { has: obj => "getIdentityAccessState" in obj, get: obj => obj.getIdentityAccessState }, metadata: _metadata }, null, _instanceExtraInitializers);
840
885
  __esDecorate(this, null, _getDeviceJoinStatus_decorators, { kind: "method", name: "getDeviceJoinStatus", static: false, private: false, access: { has: obj => "getDeviceJoinStatus" in obj, get: obj => obj.getDeviceJoinStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
841
886
  __esDecorate(this, null, _cancelDeviceJoin_decorators, { kind: "method", name: "cancelDeviceJoin", static: false, private: false, access: { has: obj => "cancelDeviceJoin" in obj, get: obj => obj.cancelDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
842
887
  __esDecorate(this, null, _refreshDeviceManagement_decorators, { kind: "method", name: "refreshDeviceManagement", static: false, private: false, access: { has: obj => "refreshDeviceManagement" in obj, get: obj => obj.refreshDeviceManagement }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -844,6 +889,8 @@ let AwikiService = (() => {
844
889
  __esDecorate(this, null, _approveDeviceJoin_decorators, { kind: "method", name: "approveDeviceJoin", static: false, private: false, access: { has: obj => "approveDeviceJoin" in obj, get: obj => obj.approveDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
845
890
  __esDecorate(this, null, _rejectDeviceJoin_decorators, { kind: "method", name: "rejectDeviceJoin", static: false, private: false, access: { has: obj => "rejectDeviceJoin" in obj, get: obj => obj.rejectDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
846
891
  __esDecorate(this, null, _revokeDevice_decorators, { kind: "method", name: "revokeDevice", static: false, private: false, access: { has: obj => "revokeDevice" in obj, get: obj => obj.revokeDevice }, metadata: _metadata }, null, _instanceExtraInitializers);
892
+ __esDecorate(this, null, _prepareRootTransfer_decorators, { kind: "method", name: "prepareRootTransfer", static: false, private: false, access: { has: obj => "prepareRootTransfer" in obj, get: obj => obj.prepareRootTransfer }, metadata: _metadata }, null, _instanceExtraInitializers);
893
+ __esDecorate(this, null, _confirmRootTransfer_decorators, { kind: "method", name: "confirmRootTransfer", static: false, private: false, access: { has: obj => "confirmRootTransfer" in obj, get: obj => obj.confirmRootTransfer }, metadata: _metadata }, null, _instanceExtraInitializers);
847
894
  __esDecorate(this, null, _updateDisplayName_decorators, { kind: "method", name: "updateDisplayName", static: false, private: false, access: { has: obj => "updateDisplayName" in obj, get: obj => obj.updateDisplayName }, metadata: _metadata }, null, _instanceExtraInitializers);
848
895
  __esDecorate(this, null, _getProfile_decorators, { kind: "method", name: "getProfile", static: false, private: false, access: { has: obj => "getProfile" in obj, get: obj => obj.getProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
849
896
  __esDecorate(this, null, _updateProfile_decorators, { kind: "method", name: "updateProfile", static: false, private: false, access: { has: obj => "updateProfile" in obj, get: obj => obj.updateProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -852,12 +899,14 @@ let AwikiService = (() => {
852
899
  __esDecorate(this, null, _activateRecovery_decorators, { kind: "method", name: "activateRecovery", static: false, private: false, access: { has: obj => "activateRecovery" in obj, get: obj => obj.activateRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
853
900
  __esDecorate(this, null, _getRecoveryStatus_decorators, { kind: "method", name: "getRecoveryStatus", static: false, private: false, access: { has: obj => "getRecoveryStatus" in obj, get: obj => obj.getRecoveryStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
854
901
  __esDecorate(this, null, _resumeRecovery_decorators, { kind: "method", name: "resumeRecovery", static: false, private: false, access: { has: obj => "resumeRecovery" in obj, get: obj => obj.resumeRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
902
+ __esDecorate(this, null, _enterRecoveredSession_decorators, { kind: "method", name: "enterRecoveredSession", static: false, private: false, access: { has: obj => "enterRecoveredSession" in obj, get: obj => obj.enterRecoveredSession }, metadata: _metadata }, null, _instanceExtraInitializers);
855
903
  __esDecorate(this, null, _discardRecovery_decorators, { kind: "method", name: "discardRecovery", static: false, private: false, access: { has: obj => "discardRecovery" in obj, get: obj => obj.discardRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
856
904
  __esDecorate(this, null, _resolvePeer_decorators, { kind: "method", name: "resolvePeer", static: false, private: false, access: { has: obj => "resolvePeer" in obj, get: obj => obj.resolvePeer }, metadata: _metadata }, null, _instanceExtraInitializers);
857
905
  __esDecorate(this, null, _createGroup_decorators, { kind: "method", name: "createGroup", static: false, private: false, access: { has: obj => "createGroup" in obj, get: obj => obj.createGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
858
906
  __esDecorate(this, null, _getGroup_decorators, { kind: "method", name: "getGroup", static: false, private: false, access: { has: obj => "getGroup" in obj, get: obj => obj.getGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
859
907
  __esDecorate(this, null, _joinGroup_decorators, { kind: "method", name: "joinGroup", static: false, private: false, access: { has: obj => "joinGroup" in obj, get: obj => obj.joinGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
860
908
  __esDecorate(this, null, _leaveGroup_decorators, { kind: "method", name: "leaveGroup", static: false, private: false, access: { has: obj => "leaveGroup" in obj, get: obj => obj.leaveGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
909
+ __esDecorate(this, null, _getDisplayProfiles_decorators, { kind: "method", name: "getDisplayProfiles", static: false, private: false, access: { has: obj => "getDisplayProfiles" in obj, get: obj => obj.getDisplayProfiles }, metadata: _metadata }, null, _instanceExtraInitializers);
861
910
  __esDecorate(this, null, _listGroupMembers_decorators, { kind: "method", name: "listGroupMembers", static: false, private: false, access: { has: obj => "listGroupMembers" in obj, get: obj => obj.listGroupMembers }, metadata: _metadata }, null, _instanceExtraInitializers);
862
911
  __esDecorate(this, null, _addGroupMember_decorators, { kind: "method", name: "addGroupMember", static: false, private: false, access: { has: obj => "addGroupMember" in obj, get: obj => obj.addGroupMember }, metadata: _metadata }, null, _instanceExtraInitializers);
863
912
  __esDecorate(this, null, _removeGroupMember_decorators, { kind: "method", name: "removeGroupMember", static: false, private: false, access: { has: obj => "removeGroupMember" in obj, get: obj => obj.removeGroupMember }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -884,11 +933,23 @@ let AwikiService = (() => {
884
933
  resolved = __runInitializers(this, _instanceExtraInitializers);
885
934
  sessionStore;
886
935
  imageAttachmentCache;
887
- sentMailStore;
888
936
  conversationPreferenceStore;
889
937
  startupUserServiceDomain;
890
938
  settingsProvider;
939
+ tenantRegistry;
940
+ activeTenant;
941
+ activeClientOptions;
942
+ clientFactory;
891
943
  provider;
944
+ runtimeGeneration = 0;
945
+ tenantSwitching = false;
946
+ tenantParticipants = new Set();
947
+ activeCapabilities;
948
+ updatePolicyStatus;
949
+ updatePolicyRequest;
950
+ updatePolicyTask;
951
+ updatePolicyTaskKey;
952
+ desktopDistribution;
892
953
  signedOut;
893
954
  sessionMutation = Promise.resolve();
894
955
  sessionRevision = 0;
@@ -899,12 +960,14 @@ let AwikiService = (() => {
899
960
  requestSessions = new Map();
900
961
  deviceRefs = new Map();
901
962
  deviceIds = new Map();
963
+ rootTransfers = new Map();
902
964
  activeSummaryRequests = new Set();
903
965
  summaryProvider;
904
- recoveryReconciliationTarget;
905
966
  hostContext;
906
967
  /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
907
968
  externalHttpAuth;
969
+ integrationClient;
970
+ mailListClient;
908
971
  workspaceContext;
909
972
  /**
910
973
  * @param ctx - owning Host context.
@@ -913,12 +976,8 @@ let AwikiService = (() => {
913
976
  constructor(ctx, config) {
914
977
  super(ctx, 'awiki');
915
978
  this.hostContext = ctx;
916
- this.resolved = resolveConfig(config);
979
+ this.resolved = resolveConfig(ctx, config);
917
980
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
918
- this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
919
- this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
920
- this.sentMailStore = new AwikiSentMailStore(this.resolved.stateRoot);
921
- this.conversationPreferenceStore = new AwikiConversationPreferenceStore(this.resolved.stateRoot);
922
981
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
923
982
  ctx.inject(['settings'], (settingsCtx) => {
924
983
  const provider = settingsCtx.settings;
@@ -937,7 +996,17 @@ let AwikiService = (() => {
937
996
  }, 'awiki: release settings namespace');
938
997
  });
939
998
  ctx.inject(['connection'], (connectionCtx) => {
940
- connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
999
+ connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider, {
1000
+ describe: () => this.getTenantRegistryView(),
1001
+ create: (displayName, domain) => this.createCustomTenant(displayName, domain),
1002
+ rename: (tenantId, displayName) => this.renameCustomTenant(tenantId, displayName),
1003
+ switch: tenantId => this.switchTenant(tenantId),
1004
+ archive: tenantId => this.archiveCustomTenant(tenantId),
1005
+ describeUpdate: () => this.getUpdatePolicyStatus(),
1006
+ refreshUpdate: () => this.refreshUpdatePolicy(),
1007
+ describeDesktopUpdate: () => this.getDesktopUpdate(),
1008
+ refreshDesktopUpdate: () => this.refreshDesktopUpdate(),
1009
+ }), { authority: 'loopback' });
941
1010
  });
942
1011
  ctx.inject(['workspaceRegistry'], (workspaceCtx) => {
943
1012
  this.workspaceContext = workspaceCtx;
@@ -953,8 +1022,29 @@ let AwikiService = (() => {
953
1022
  await this.stopAgentConsumer(current);
954
1023
  }, 'awiki: release Workspace listener composition');
955
1024
  });
1025
+ ctx.inject(['desktopDistribution'], desktopCtx => {
1026
+ const service = desktopCtx.get('desktopDistribution');
1027
+ if (typeof service !== 'object' || service === null
1028
+ || !('getSnapshot' in service) || typeof service.getSnapshot !== 'function'
1029
+ || !('check' in service) || typeof service.check !== 'function')
1030
+ return;
1031
+ const candidate = service;
1032
+ if (decodeDesktopDistribution(candidate.getSnapshot()) === undefined)
1033
+ return;
1034
+ this.desktopDistribution = candidate;
1035
+ desktopCtx.effect(() => () => {
1036
+ if (this.desktopDistribution === candidate)
1037
+ this.desktopDistribution = undefined;
1038
+ });
1039
+ });
1040
+ ctx.effect(() => {
1041
+ const timer = setInterval(() => { void this.refreshUpdatePolicy().catch(() => undefined); }, 6 * 60 * 60 * 1000);
1042
+ timer.unref();
1043
+ return () => { clearInterval(timer); };
1044
+ });
956
1045
  registerAwikiTools(ctx, this);
957
1046
  ctx.effect(() => async () => {
1047
+ this.updatePolicyRequest?.abort();
958
1048
  const provider = this.provider;
959
1049
  if (provider !== undefined)
960
1050
  await this.disposeProvider(provider);
@@ -971,32 +1061,334 @@ let AwikiService = (() => {
971
1061
  * @returns asynchronous disposer for the exact registered client.
972
1062
  */
973
1063
  registerClientFactory(factory) {
974
- if (this.provider !== undefined)
1064
+ if (this.clientFactory !== undefined || this.provider !== undefined) {
975
1065
  throw new Error('awiki: a client provider is already registered');
976
- this.pendingDeviceJoin = undefined;
977
- this.activeDeviceJoinSessionId = undefined;
978
- const client = factory({
979
- userServiceUrl: this.resolved.userServiceUrl,
980
- userServiceDomain: this.startupUserServiceDomain,
981
- messageServiceUrl: this.resolved.messageServiceUrl,
982
- mailServiceUrl: this.resolved.mailServiceUrl,
983
- messageServicePublicUrl: this.resolved.messageServicePublicUrl,
984
- messageServiceDid: this.resolved.messageServiceDid,
985
- allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
1066
+ }
1067
+ this.clientFactory = factory;
1068
+ try {
1069
+ this.openTenantProvider(this.ensureTenantRegistry().active(), factory);
1070
+ }
1071
+ catch (error) {
1072
+ this.clientFactory = undefined;
1073
+ throw error;
1074
+ }
1075
+ let active = true;
1076
+ return async () => {
1077
+ if (!active)
1078
+ return;
1079
+ active = false;
1080
+ this.clientFactory = undefined;
1081
+ const current = this.provider;
1082
+ if (current !== undefined)
1083
+ await this.disposeProvider(current);
1084
+ };
1085
+ }
1086
+ ensureTenantRegistry() {
1087
+ if (this.tenantRegistry !== undefined)
1088
+ return this.tenantRegistry;
1089
+ const domain = this.startupUserServiceDomain;
1090
+ const configuredOrigin = `https://${this.resolved.userServiceDomain}`;
1091
+ const tenantOrigin = `https://${domain}`;
1092
+ const derive = (value) => value.replace(/\/$/u, '') === configuredOrigin ? tenantOrigin : value;
1093
+ const seed = {
1094
+ domain,
1095
+ configured: domain !== DEFAULT_AWIKI_DOMAIN
1096
+ || this.resolved.userServiceUrl.replace(/\/$/u, '') !== configuredOrigin
1097
+ || this.resolved.messageServiceUrl.replace(/\/$/u, '') !== configuredOrigin,
1098
+ ...this.resolved.legacyTenantSlot === undefined
1099
+ ? {}
1100
+ : { legacyTenantSlot: this.resolved.legacyTenantSlot },
1101
+ userServiceUrl: derive(this.resolved.userServiceUrl),
1102
+ messageServiceUrl: derive(this.resolved.messageServiceUrl),
1103
+ mailServiceUrl: derive(this.resolved.mailServiceUrl),
1104
+ messageServicePublicUrl: derive(this.resolved.messageServicePublicUrl),
1105
+ messageServiceDid: this.resolved.messageServiceDid === `did:wba:${this.resolved.userServiceDomain}`
1106
+ ? `did:wba:${domain}`
1107
+ : this.resolved.messageServiceDid,
1108
+ };
1109
+ this.tenantRegistry = AwikiTenantRegistry.open(this.resolved.stateRoot, seed);
1110
+ return this.tenantRegistry;
1111
+ }
1112
+ optionsForTenant(tenant) {
1113
+ const endpoints = tenant.endpoints;
1114
+ const messageServicePublicOrigin = new URL(endpoints.messageServicePublicUrl).origin;
1115
+ return {
1116
+ ...endpoints,
1117
+ userServiceDomain: tenant.didHost,
1118
+ allowedAttachmentOrigins: tenant.storageLayout === 'legacy-base'
1119
+ ? this.resolved.allowedAttachmentOrigins
1120
+ : [messageServicePublicOrigin],
986
1121
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
987
1122
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
988
- stateRoot: this.resolved.stateRoot,
989
- });
1123
+ stateRoot: this.ensureTenantRegistry().stateRoot(tenant),
1124
+ };
1125
+ }
1126
+ ensureTenantOptions() {
1127
+ if (this.activeClientOptions !== undefined)
1128
+ return this.activeClientOptions;
1129
+ const tenant = this.ensureTenantRegistry().active();
1130
+ const options = this.optionsForTenant(tenant);
1131
+ this.bindTenantState(tenant, options);
1132
+ return options;
1133
+ }
1134
+ bindTenantState(tenant, options) {
1135
+ this.updatePolicyRequest?.abort();
1136
+ this.updatePolicyStatus = undefined;
1137
+ this.updatePolicyTask = undefined;
1138
+ this.activeTenant = tenant;
1139
+ this.activeClientOptions = options;
1140
+ this.startupUserServiceDomain = tenant.didHost;
1141
+ this.sessionStore = new AwikiSessionStore(options.stateRoot);
1142
+ this.imageAttachmentCache = new AwikiImageAttachmentCache(options.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
1143
+ this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
1144
+ this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
1145
+ this.integrationClient = undefined;
1146
+ this.activeCapabilities = {
1147
+ tenantId: tenant.tenantId,
1148
+ generation: this.runtimeGeneration,
1149
+ online: false,
1150
+ handleRecoveryPhoneEnabled: false,
1151
+ };
1152
+ this.signedOut = undefined;
1153
+ this.activeIdentityDid = undefined;
1154
+ this.pendingDeviceJoin = undefined;
1155
+ this.activeDeviceJoinSessionId = undefined;
1156
+ }
1157
+ openTenantProvider(tenant, factory, startRuntime = true) {
1158
+ const options = this.optionsForTenant(tenant);
1159
+ const client = factory(options);
990
1160
  const provider = {
991
1161
  client,
992
1162
  realtimeGeneration: 0,
993
1163
  agentConsumerGeneration: 0,
994
1164
  localDeviceJoinRequestCountAfterSync: 0,
995
1165
  };
1166
+ this.bindTenantState(tenant, options);
996
1167
  this.provider = provider;
997
- this.ensureRealtimeSupervisor(provider);
998
- this.ensureAgentConsumer(provider);
999
- return () => this.disposeProvider(provider);
1168
+ void this.refreshUpdatePolicy().catch(() => undefined);
1169
+ if (startRuntime)
1170
+ this.ensureProviderRuntime(provider);
1171
+ return provider;
1172
+ }
1173
+ /** Read the Host-owned catalog for trusted loopback settings surfaces. */
1174
+ getTenantRegistryView() {
1175
+ return this.ensureTenantRegistry().snapshot(this.tenantSwitching);
1176
+ }
1177
+ /** Browser-safe, same-process update state for Desktop and loopback settings. */
1178
+ getUpdatePolicyStatus() {
1179
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
1180
+ const modelProxyVersion = this.currentModelProxyVersion();
1181
+ return this.updatePolicyStatus ?? readAwikiUpdatePolicyStatus({
1182
+ tenant, generation: this.runtimeGeneration, stateRoot: this.resolved.stateRoot,
1183
+ currentPluginVersion: DSH_AWIKI_VERSION,
1184
+ ...modelProxyVersion === undefined ? {} : { currentModelProxyVersion: modelProxyVersion },
1185
+ });
1186
+ }
1187
+ /** Only the Desktop provider owns its release source; tenants cannot override it. */
1188
+ getDesktopUpdate() {
1189
+ return decodeDesktopDistribution(this.desktopDistribution?.getSnapshot());
1190
+ }
1191
+ async refreshDesktopUpdate() {
1192
+ const provider = this.desktopDistribution;
1193
+ if (provider === undefined)
1194
+ return undefined;
1195
+ const value = await provider.check();
1196
+ return provider === this.desktopDistribution ? decodeDesktopDistribution(value) : this.getDesktopUpdate();
1197
+ }
1198
+ /** Refresh only the active generation; late results from old tenants are discarded. */
1199
+ refreshUpdatePolicy() {
1200
+ const key = JSON.stringify([this.runtimeGeneration, this.currentModelProxyVersion()]);
1201
+ if (this.updatePolicyTask !== undefined && this.updatePolicyTaskKey === key)
1202
+ return this.updatePolicyTask;
1203
+ const task = this.performUpdatePolicyRefresh().finally(() => {
1204
+ if (this.updatePolicyTask === task)
1205
+ this.updatePolicyTask = undefined;
1206
+ });
1207
+ this.updatePolicyTask = task;
1208
+ this.updatePolicyTaskKey = key;
1209
+ return task;
1210
+ }
1211
+ async performUpdatePolicyRefresh() {
1212
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
1213
+ const generation = this.runtimeGeneration;
1214
+ this.updatePolicyRequest?.abort();
1215
+ const request = new AbortController();
1216
+ this.updatePolicyRequest = request;
1217
+ const currentModelProxyVersion = this.currentModelProxyVersion();
1218
+ const status = await checkAwikiUpdatePolicy({
1219
+ tenant,
1220
+ generation,
1221
+ stateRoot: this.resolved.stateRoot,
1222
+ currentPluginVersion: DSH_AWIKI_VERSION,
1223
+ ...currentModelProxyVersion === undefined ? {} : { currentModelProxyVersion },
1224
+ allowInsecureLoopback: this.resolved.allowInsecureLoopbackForTesting,
1225
+ signal: request.signal,
1226
+ });
1227
+ if (this.updatePolicyRequest === request
1228
+ && this.runtimeGeneration === generation
1229
+ && this.activeTenant?.tenantId === tenant.tenantId) {
1230
+ const wasRestricted = this.getUpdatePolicyStatus().restricted;
1231
+ this.updatePolicyStatus = status;
1232
+ this.hostContext.emit('awiki/update-policy', status);
1233
+ if (status.restricted) {
1234
+ this.invalidateSummaries();
1235
+ const provider = this.provider;
1236
+ if (provider !== undefined) {
1237
+ try {
1238
+ await this.stopProviderRuntime(provider);
1239
+ }
1240
+ catch (error) {
1241
+ this.ctx.logger('awiki-update').warn('AWiki restricted-version runtime shutdown failed: %s', error instanceof Error ? error.message : 'unknown failure');
1242
+ }
1243
+ }
1244
+ }
1245
+ else if (wasRestricted && this.provider !== undefined && !this.tenantSwitching) {
1246
+ this.ensureProviderRuntime(this.provider);
1247
+ }
1248
+ }
1249
+ return this.getUpdatePolicyStatus();
1250
+ }
1251
+ currentModelProxyVersion() {
1252
+ for (const participant of this.tenantParticipants) {
1253
+ if (participant.component?.product === 'dsh-awiki-model-proxy')
1254
+ return participant.component.version;
1255
+ }
1256
+ return undefined;
1257
+ }
1258
+ createCustomTenant(displayName, domain) {
1259
+ this.assertVersionSupported();
1260
+ this.ensureTenantRegistry().createCustom(displayName, domain);
1261
+ return this.getTenantRegistryView();
1262
+ }
1263
+ renameCustomTenant(tenantId, displayName) {
1264
+ this.assertVersionSupported();
1265
+ this.ensureTenantRegistry().renameCustom(tenantId, displayName);
1266
+ return this.getTenantRegistryView();
1267
+ }
1268
+ archiveCustomTenant(tenantId) {
1269
+ this.assertVersionSupported();
1270
+ this.ensureTenantRegistry().archiveCustom(tenantId);
1271
+ return this.getTenantRegistryView();
1272
+ }
1273
+ /** Register one same-process resource owner in every transactional tenant change. */
1274
+ registerTenantLifecycleParticipant(participant) {
1275
+ if (this.tenantParticipants.has(participant))
1276
+ throw new Error('awiki: tenant lifecycle participant is already registered');
1277
+ this.tenantParticipants.add(participant);
1278
+ if (participant.component !== undefined)
1279
+ this.updatePolicyStatus = undefined;
1280
+ void this.refreshUpdatePolicy().catch(() => undefined);
1281
+ let active = true;
1282
+ return () => {
1283
+ if (!active)
1284
+ return;
1285
+ active = false;
1286
+ this.tenantParticipants.delete(participant);
1287
+ if (participant.component !== undefined)
1288
+ this.updatePolicyStatus = undefined;
1289
+ void this.refreshUpdatePolicy().catch(() => undefined);
1290
+ };
1291
+ }
1292
+ /** Replace the complete Core runtime and commit the active tenant only after opening succeeds. */
1293
+ async switchTenant(tenantId) {
1294
+ if (this.tenantSwitching)
1295
+ throw new Error('awiki: a tenant switch is already in progress');
1296
+ const registry = this.ensureTenantRegistry();
1297
+ const previousTenant = registry.active();
1298
+ if (previousTenant.tenantId === tenantId)
1299
+ return this.getTenantRegistryView();
1300
+ const target = registry.find(tenantId);
1301
+ if (target === undefined || target.lifecycle === 'archived')
1302
+ throw new Error('awiki: target tenant is unavailable');
1303
+ const factory = this.clientFactory;
1304
+ const previousProvider = this.provider;
1305
+ if (factory === undefined || previousProvider === undefined)
1306
+ throw new Error('awiki: client provider is unavailable');
1307
+ this.tenantSwitching = true;
1308
+ this.updatePolicyRequest?.abort();
1309
+ this.updatePolicyTask = undefined;
1310
+ const context = {
1311
+ from: previousTenant,
1312
+ to: target,
1313
+ generation: ++this.runtimeGeneration,
1314
+ };
1315
+ const prepared = [];
1316
+ let targetOpened = false;
1317
+ let committed = false;
1318
+ try {
1319
+ await this.sessionMutation;
1320
+ this.invalidateSummaries();
1321
+ for (const participant of this.tenantParticipants) {
1322
+ await participant.prepareSwitch(context);
1323
+ prepared.push(participant);
1324
+ }
1325
+ await this.disposeProvider(previousProvider);
1326
+ const targetProvider = this.openTenantProvider(target, factory, false);
1327
+ targetOpened = true;
1328
+ // The factory returns an adapter immediately; its native Core opens asynchronously.
1329
+ // A local identity read proves both open and Registry loading before committing the switch.
1330
+ await targetProvider.client.getIdentity();
1331
+ await Promise.all([this.discoverTenantCapabilities(3_000), this.refreshUpdatePolicy()]);
1332
+ registry.commitActive(target.tenantId);
1333
+ committed = true;
1334
+ for (const participant of prepared)
1335
+ await participant.commitSwitch?.(context);
1336
+ this.tenantSwitching = false;
1337
+ this.ensureProviderRuntime(targetProvider);
1338
+ const view = this.getTenantRegistryView();
1339
+ this.hostContext.emit('awiki/tenant', view);
1340
+ return view;
1341
+ }
1342
+ catch (error) {
1343
+ const rollbackFailures = [];
1344
+ if (targetOpened) {
1345
+ const current = this.provider;
1346
+ if (current !== undefined) {
1347
+ try {
1348
+ await this.disposeProvider(current);
1349
+ }
1350
+ catch (rollbackError) {
1351
+ rollbackFailures.push(rollbackError);
1352
+ }
1353
+ }
1354
+ }
1355
+ if (committed) {
1356
+ try {
1357
+ registry.commitActive(previousTenant.tenantId);
1358
+ }
1359
+ catch (rollbackError) {
1360
+ rollbackFailures.push(rollbackError);
1361
+ }
1362
+ }
1363
+ if (this.provider === undefined) {
1364
+ try {
1365
+ const restored = this.openTenantProvider(previousTenant, factory, false);
1366
+ await restored.client.getIdentity();
1367
+ this.ensureProviderRuntime(restored);
1368
+ }
1369
+ catch (rollbackError) {
1370
+ rollbackFailures.push(rollbackError);
1371
+ }
1372
+ }
1373
+ for (const participant of prepared.reverse()) {
1374
+ try {
1375
+ await participant.rollbackSwitch?.(context);
1376
+ }
1377
+ catch (rollbackError) {
1378
+ rollbackFailures.push(rollbackError);
1379
+ }
1380
+ }
1381
+ if (rollbackFailures.length > 0) {
1382
+ throw new AggregateError([error, ...rollbackFailures], 'awiki: tenant switch failed and rollback was incomplete');
1383
+ }
1384
+ this.tenantSwitching = false;
1385
+ if (this.provider !== undefined)
1386
+ this.ensureProviderRuntime(this.provider);
1387
+ throw new Error('awiki: tenant switch failed; the previous tenant was restored', { cause: error });
1388
+ }
1389
+ finally {
1390
+ this.tenantSwitching = false;
1391
+ }
1000
1392
  }
1001
1393
  /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
1002
1394
  getRealtimeDiagnostics() {
@@ -1016,25 +1408,6 @@ let AwikiService = (() => {
1016
1408
  localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
1017
1409
  };
1018
1410
  }
1019
- /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
1020
- registerRecoveryReconciliationTarget(target) {
1021
- if (this.recoveryReconciliationTarget !== undefined) {
1022
- throw new Error('awiki: a recovery reconciliation target is already registered');
1023
- }
1024
- const registered = Object.freeze({
1025
- endpoint: recoveryReconciliationEndpoint(target, this.resolved.allowInsecureLoopbackForTesting),
1026
- });
1027
- this.recoveryReconciliationTarget = registered;
1028
- let active = true;
1029
- return () => {
1030
- if (!active)
1031
- return;
1032
- active = false;
1033
- if (this.recoveryReconciliationTarget === registered) {
1034
- this.recoveryReconciliationTarget = undefined;
1035
- }
1036
- };
1037
- }
1038
1411
  /** Register one replaceable conversation-summary provider for this deployment. */
1039
1412
  registerSummaryProvider(provider) {
1040
1413
  if (this.summaryProvider !== undefined)
@@ -1054,42 +1427,144 @@ let AwikiService = (() => {
1054
1427
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
1055
1428
  */
1056
1429
  async getConfig() {
1057
- let handleRecoveryPhoneEnabled = false;
1430
+ this.ensureTenantOptions();
1431
+ const capabilities = await this.discoverTenantCapabilities(10_000);
1432
+ if (this.tenantSwitching
1433
+ || capabilities.generation !== this.runtimeGeneration
1434
+ || capabilities.tenantId !== this.activeTenant?.tenantId) {
1435
+ return { ok: false, error: failure('conflict') };
1436
+ }
1437
+ return {
1438
+ ok: true,
1439
+ value: {
1440
+ pollIntervalMs: this.resolved.pollIntervalMs,
1441
+ attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1442
+ handleRecoveryPhoneEnabled: capabilities.handleRecoveryPhoneEnabled,
1443
+ ...capabilities.guestGatewayBaseUrl === undefined
1444
+ ? {}
1445
+ : { integrationGuideUrl: new URL('/guest/guide/integration', capabilities.guestGatewayBaseUrl).toString() },
1446
+ tenantId: capabilities.tenantId,
1447
+ tenantGeneration: capabilities.generation,
1448
+ tenantOnline: capabilities.online,
1449
+ services: {
1450
+ modelProxy: { enabled: capabilities.modelProxyBaseUrl !== undefined },
1451
+ guestGateway: { enabled: capabilities.guestGatewayBaseUrl !== undefined },
1452
+ },
1453
+ },
1454
+ };
1455
+ }
1456
+ /** Trusted same-process capability binding for optional tenant participants. */
1457
+ getTenantCapabilities() {
1458
+ const capabilities = this.activeCapabilities;
1459
+ if (capabilities === undefined)
1460
+ throw new Error('awiki: tenant capabilities are unavailable');
1461
+ return { ...capabilities };
1462
+ }
1463
+ /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
1464
+ refreshTenantCapabilities() {
1465
+ this.ensureTenantOptions();
1466
+ return this.discoverTenantCapabilities(10_000);
1467
+ }
1468
+ async discoverTenantCapabilities(timeoutMs) {
1469
+ const options = this.activeClientOptions;
1470
+ const tenant = this.activeTenant;
1471
+ if (options === undefined || tenant === undefined)
1472
+ throw new ProviderUnavailableError();
1473
+ const generation = this.runtimeGeneration;
1474
+ const fallback = {
1475
+ tenantId: tenant.tenantId,
1476
+ generation,
1477
+ online: false,
1478
+ handleRecoveryPhoneEnabled: false,
1479
+ };
1480
+ const publish = (capabilities) => {
1481
+ if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
1482
+ this.activeCapabilities = capabilities;
1483
+ this.integrationClient = capabilities.guestGatewayBaseUrl === undefined
1484
+ ? undefined
1485
+ : new AwikiIntegrationClient(capabilities.guestGatewayBaseUrl, this.externalHttpAuth);
1486
+ }
1487
+ return capabilities;
1488
+ };
1058
1489
  const abort = new AbortController();
1059
- const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1490
+ const timeout = setTimeout(() => { abort.abort(); }, timeoutMs);
1060
1491
  try {
1061
- const response = await fetch(new URL('/user-service/v1/server-info', this.resolved.userServiceUrl), {
1492
+ const endpoint = new URL('/user-service/v1/server-info', options.userServiceUrl);
1493
+ endpoint.searchParams.set('client_platform', 'dsh');
1494
+ const response = await fetch(endpoint, {
1062
1495
  method: 'GET',
1063
1496
  headers: { accept: 'application/json', 'cache-control': 'no-store' },
1064
1497
  cache: 'no-store',
1065
1498
  redirect: 'error',
1066
1499
  signal: abort.signal,
1067
1500
  });
1068
- if (response.ok) {
1069
- const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1070
- const value = text === undefined ? undefined : JSON.parse(text);
1071
- if (typeof value === 'object' && value !== null && value.schema_version === 1) {
1072
- const methods = value.identity?.handle_recovery?.methods;
1073
- handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1074
- && method.id === 'phone'
1075
- && method.enabled === true
1076
- && method.verification?.required === true
1077
- && method.verification?.type === 'sms_otp'));
1078
- }
1079
- }
1501
+ if (!response.ok)
1502
+ return publish(fallback);
1503
+ const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1504
+ const value = text === undefined ? undefined : JSON.parse(text);
1505
+ if (typeof value !== 'object' || value === null || value.schema_version !== 1)
1506
+ return publish(fallback);
1507
+ const methods = value.identity?.handle_recovery?.methods;
1508
+ const handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1509
+ && method.id === 'phone'
1510
+ && method.enabled === true
1511
+ && method.verification?.required === true
1512
+ && method.verification?.type === 'sms_otp'));
1513
+ const services = value.services;
1514
+ const modelProxyBaseUrl = publishedServiceBaseUrl(services, 'model_proxy', this.resolved.allowInsecureLoopbackForTesting);
1515
+ const guestGatewayBaseUrl = publishedServiceBaseUrl(services, 'guest_gateway', false);
1516
+ const capabilities = {
1517
+ tenantId: tenant.tenantId,
1518
+ generation,
1519
+ online: true,
1520
+ handleRecoveryPhoneEnabled,
1521
+ ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1522
+ ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1523
+ };
1524
+ return publish(capabilities);
1525
+ }
1526
+ catch {
1527
+ return publish(fallback);
1080
1528
  }
1081
- catch { }
1082
1529
  finally {
1083
1530
  clearTimeout(timeout);
1084
1531
  }
1085
- return {
1086
- ok: true,
1087
- value: {
1088
- pollIntervalMs: this.resolved.pollIntervalMs,
1089
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1090
- handleRecoveryPhoneEnabled,
1091
- },
1092
- };
1532
+ }
1533
+ /** Read the Integration owned by the active full Handle through the fixed Host client. */
1534
+ getIntegration() {
1535
+ if (this.getUpdatePolicyStatus().restricted)
1536
+ return Promise.resolve(integrationUnavailable());
1537
+ return this.integrationClient?.read() ?? Promise.resolve(integrationUnavailable());
1538
+ }
1539
+ /** Create the active full Handle's only Integration. */
1540
+ createIntegration(request) {
1541
+ if (this.getUpdatePolicyStatus().restricted)
1542
+ return Promise.resolve(integrationUnavailable());
1543
+ return this.integrationClient?.create(request) ?? Promise.resolve(integrationUnavailable());
1544
+ }
1545
+ /** Update editable Integration fields with optimistic concurrency. */
1546
+ updateIntegration(request) {
1547
+ if (this.getUpdatePolicyStatus().restricted)
1548
+ return Promise.resolve(integrationUnavailable());
1549
+ return this.integrationClient?.update(request) ?? Promise.resolve(integrationUnavailable());
1550
+ }
1551
+ /** Atomically replace the current public Integration id. */
1552
+ rotateIntegrationId(request) {
1553
+ if (this.getUpdatePolicyStatus().restricted)
1554
+ return Promise.resolve(integrationUnavailable());
1555
+ return this.integrationClient?.rotate(request) ?? Promise.resolve(integrationUnavailable());
1556
+ }
1557
+ /** Close the Integration and revoke its current public id. */
1558
+ closeIntegration(request) {
1559
+ if (this.getUpdatePolicyStatus().restricted)
1560
+ return Promise.resolve(integrationUnavailable());
1561
+ return this.integrationClient?.close(request) ?? Promise.resolve(integrationUnavailable());
1562
+ }
1563
+ /** Revalidate one closed Integration and issue a new public id. */
1564
+ reopenIntegration(request) {
1565
+ if (this.getUpdatePolicyStatus().restricted)
1566
+ return Promise.resolve(integrationUnavailable());
1567
+ return this.integrationClient?.reopen(request) ?? Promise.resolve(integrationUnavailable());
1093
1568
  }
1094
1569
  /**
1095
1570
  * Read the deployment's identity status.
@@ -1123,6 +1598,8 @@ let AwikiService = (() => {
1123
1598
  if (request?.confirmation !== AWIKI_LOGOUT_CONFIRMATION) {
1124
1599
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1125
1600
  }
1601
+ // Fence pending post-Recovery work before the serialized sign-out reaches the head of the queue.
1602
+ this.sessionRevision += 1;
1126
1603
  return this.mutateSession(async () => {
1127
1604
  if (await this.isSignedOut())
1128
1605
  return { ok: true, value: { status: 'signed-out' } };
@@ -1175,10 +1652,13 @@ let AwikiService = (() => {
1175
1652
  }
1176
1653
  /** Classify one configured-domain Handle before selecting the registration or recovery OTP purpose. */
1177
1654
  async inspectIdentityAccess(request) {
1178
- const target = identityAccessTarget(request, this.startupUserServiceDomain);
1655
+ const options = this.activeClientOptions;
1656
+ if (options === undefined)
1657
+ return { ok: false, error: normalizeFailure(new ProviderUnavailableError()) };
1658
+ const target = identityAccessTarget(request, options.userServiceDomain);
1179
1659
  if (target === undefined)
1180
1660
  return { ok: false, error: failure('invalid-request') };
1181
- const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.resolved.userServiceUrl);
1661
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, options.userServiceUrl);
1182
1662
  const abort = new AbortController();
1183
1663
  const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1184
1664
  let response;
@@ -1267,25 +1747,88 @@ let AwikiService = (() => {
1267
1747
  },
1268
1748
  };
1269
1749
  }
1750
+ /** Retire only this revoked device credential; ordinary local messages remain for the rejoined identity. */
1751
+ retireDeviceIdentityForRejoin() {
1752
+ return this.mutateSession(async () => {
1753
+ const provider = this.provider;
1754
+ if (provider !== undefined)
1755
+ await this.stopProviderRuntime(provider);
1756
+ const result = await this.run(client => client.retireDefaultIdentityForRejoin(), { allowSignedOut: true });
1757
+ if (!result.ok) {
1758
+ if (provider !== undefined && this.provider === provider)
1759
+ this.ensureProviderRuntime(provider);
1760
+ return result;
1761
+ }
1762
+ try {
1763
+ await this.sessionStore.signIn();
1764
+ }
1765
+ catch {
1766
+ try {
1767
+ await this.sessionStore.signIn();
1768
+ }
1769
+ catch {
1770
+ this.ctx.logger('awiki').warn('AWiki local session marker could not be refreshed after device retirement');
1771
+ }
1772
+ }
1773
+ this.signedOut = false;
1774
+ this.activeIdentityDid = undefined;
1775
+ this.pendingDeviceJoin = undefined;
1776
+ this.activeDeviceJoinSessionId = undefined;
1777
+ this.invalidateSummaries();
1778
+ this.publishSession({ status: 'unregistered' });
1779
+ return { ok: true, value: { completed: true } };
1780
+ });
1781
+ }
1270
1782
  /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
1271
1783
  async beginDeviceJoin() {
1272
1784
  const continuation = this.pendingDeviceJoin;
1273
1785
  if (continuation === undefined)
1274
1786
  return { ok: false, error: failure('conflict') };
1275
- if (continuation.mode !== 'ordinary' || continuation.requiresUserPresence) {
1787
+ if ((continuation.mode === 'ordinary' && continuation.requiresUserPresence)
1788
+ || (continuation.mode === 'handle-recovery-rebind' && !continuation.requiresUserPresence)) {
1276
1789
  return { ok: false, error: failure('forbidden') };
1277
1790
  }
1278
- this.pendingDeviceJoin = undefined;
1279
- const result = await this.run(client => client.beginDeviceJoin({
1280
- continuationId: continuation.continuationId,
1281
- operationId: `dsh-device-join-${randomUUID()}`,
1282
- userPresenceConfirmed: false,
1283
- }));
1791
+ const result = await this.run(async (client) => {
1792
+ let userPresenceConfirmed = false;
1793
+ if (continuation.mode === 'handle-recovery-rebind') {
1794
+ if (!client.trustedUserPresenceSupported) {
1795
+ throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
1796
+ }
1797
+ userPresenceConfirmed = await client.confirmUserPresence('Rejoin your recovered AWiki identity on this device');
1798
+ if (!userPresenceConfirmed) {
1799
+ throw Object.assign(new Error('user presence denied'), { name: 'AwikiSdkError', code: 'forbidden' });
1800
+ }
1801
+ }
1802
+ this.pendingDeviceJoin = undefined;
1803
+ return client.beginDeviceJoin({
1804
+ continuationId: continuation.continuationId,
1805
+ operationId: `dsh-device-join-${randomUUID()}`,
1806
+ userPresenceConfirmed,
1807
+ });
1808
+ });
1284
1809
  if (!result.ok)
1285
1810
  return result;
1286
1811
  this.activeDeviceJoinSessionId = result.value.joinSessionId;
1287
1812
  return this.applyCandidateJoinProgress(result.value);
1288
1813
  }
1814
+ /** Read local workflow authority before offering a new login. No remote mutations. */
1815
+ getIdentityAccessState() {
1816
+ return this.run(async (client) => {
1817
+ const joining = await this.selectDeviceJoinSession(client) !== null;
1818
+ const recoveries = await client.listPendingRecoveries();
1819
+ const domain = this.activeClientOptions?.userServiceDomain;
1820
+ const choice = this.pendingDeviceJoin;
1821
+ return {
1822
+ choice: joining || choice === undefined ? null : {
1823
+ status: 'join-required',
1824
+ fullHandle: choice.fullHandle,
1825
+ mode: choice.mode, requiresUserPresence: choice.requiresUserPresence,
1826
+ },
1827
+ joining,
1828
+ recoveries: recoveries.filter(value => domain !== undefined && value.fullHandle.slice(value.fullHandle.indexOf('.') + 1) === domain),
1829
+ };
1830
+ }, { allowSignedOut: true });
1831
+ }
1289
1832
  /** Restore from Core local_sessions and advance only the exact resumable Join. */
1290
1833
  async getDeviceJoinStatus() {
1291
1834
  const result = await this.run(async (client) => {
@@ -1376,7 +1919,26 @@ let AwikiService = (() => {
1376
1919
  throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
1377
1920
  }
1378
1921
  const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
1379
- return client.confirmDeviceJoinApproval(prompt.approvalHandle);
1922
+ try {
1923
+ return await client.confirmDeviceJoinApproval(prompt.approvalHandle);
1924
+ }
1925
+ catch (error) {
1926
+ // Approval is a remote mutation: its response may fail after Core has
1927
+ // already committed both the provider document and the local terminal
1928
+ // Join state. Re-read the authoritative terminal state before exposing
1929
+ // a false failure to the administrator.
1930
+ try {
1931
+ await client.syncDeviceManagement();
1932
+ }
1933
+ catch { }
1934
+ try {
1935
+ const recovered = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1936
+ if (adminJoinPhase(recovered) === 'authorized')
1937
+ return recovered;
1938
+ }
1939
+ catch { }
1940
+ throw error;
1941
+ }
1380
1942
  });
1381
1943
  return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1382
1944
  }
@@ -1410,6 +1972,53 @@ let AwikiService = (() => {
1410
1972
  return this.deviceManagementSnapshot(client);
1411
1973
  });
1412
1974
  }
1975
+ /** Prepare a short-lived Core authorization without exposing it to Browser. */
1976
+ async prepareRootTransfer(request) {
1977
+ const deviceId = this.deviceIds.get(request?.deviceRef);
1978
+ if (deviceId === undefined)
1979
+ return { ok: false, error: failure('invalid-request') };
1980
+ return this.run(async (client) => {
1981
+ const did = await this.requireRootTransferRecipient(client, deviceId);
1982
+ const preparation = await client.prepareRootKeyTransfer(deviceId);
1983
+ if (preparation.recipient.did !== did || preparation.recipient.deviceId !== deviceId) {
1984
+ throw Object.assign(new Error('root transfer recipient mismatch'), { name: 'AwikiSdkError', code: 'remote' });
1985
+ }
1986
+ for (const [reference, pending] of this.rootTransfers) {
1987
+ if (pending.deviceId === deviceId)
1988
+ this.rootTransfers.delete(reference);
1989
+ }
1990
+ const transferRef = `root-transfer-${randomUUID()}`;
1991
+ this.rootTransfers.set(transferRef, {
1992
+ authorizationHandle: preparation.authorizationHandle,
1993
+ deviceId,
1994
+ deviceRef: request.deviceRef,
1995
+ did,
1996
+ });
1997
+ return { transferRef, deviceRef: request.deviceRef, expiresAt: preparation.expiresAt };
1998
+ });
1999
+ }
2000
+ /** Authenticate locally, recheck fresh context, then consume one exact Core authorization. */
2001
+ async confirmRootTransfer(request) {
2002
+ const pending = this.rootTransfers.get(request?.transferRef);
2003
+ if (pending === undefined)
2004
+ return { ok: false, error: failure('invalid-request') };
2005
+ this.rootTransfers.delete(request.transferRef);
2006
+ return this.run(async (client) => {
2007
+ const confirmed = await client.confirmUserPresence('Grant AWiki device management access');
2008
+ if (!confirmed) {
2009
+ throw Object.assign(new Error('user presence denied'), { name: 'AwikiSdkError', code: 'forbidden' });
2010
+ }
2011
+ const did = await this.requireRootTransferRecipient(client, pending.deviceId);
2012
+ if (did !== pending.did) {
2013
+ throw Object.assign(new Error('root transfer identity changed'), { name: 'AwikiSdkError', code: 'forbidden' });
2014
+ }
2015
+ const sent = await client.confirmAndSendRootKeyTransfer(pending.authorizationHandle);
2016
+ if (sent.recipientDeviceId !== pending.deviceId) {
2017
+ throw Object.assign(new Error('root transfer receipt mismatch'), { name: 'AwikiSdkError', code: 'remote' });
2018
+ }
2019
+ return { deviceRef: pending.deviceRef, acceptedAt: sent.acceptedAt };
2020
+ });
2021
+ }
1413
2022
  /**
1414
2023
  * Update the deployment identity's public WNS display name.
1415
2024
  * @param request - replacement display name selected by the user.
@@ -1457,33 +2066,35 @@ let AwikiService = (() => {
1457
2066
  const normalized = normalizeRecoveryOperation(request);
1458
2067
  if (normalized === undefined)
1459
2068
  return { ok: false, error: failure('invalid-request') };
1460
- const result = await this.run(client => client.activateRecovery(normalized), { allowSignedOut: true });
1461
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1462
- return { ok: false, error: failure('remote') };
1463
- }
1464
- return result;
2069
+ return this.run(client => client.activateRecovery(normalized), { allowSignedOut: true });
1465
2070
  }
1466
2071
  /** Read durable recovery state before deciding whether a retry is valid. */
1467
2072
  async getRecoveryStatus(request) {
1468
2073
  const normalized = normalizeRecoveryOperation(request);
1469
2074
  if (normalized === undefined)
1470
2075
  return { ok: false, error: failure('invalid-request') };
1471
- const result = await this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
1472
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1473
- return { ok: false, error: failure('remote') };
1474
- }
1475
- return result;
2076
+ return this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
1476
2077
  }
1477
2078
  /** Resume only the exact Core-owned operation selected by the browser. */
1478
2079
  async resumeRecovery(request) {
1479
2080
  const normalized = normalizeRecoveryOperation(request);
1480
2081
  if (normalized === undefined)
1481
2082
  return { ok: false, error: failure('invalid-request') };
1482
- const result = await this.run(client => client.resumeRecovery(normalized), { allowSignedOut: true });
1483
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1484
- return { ok: false, error: failure('remote') };
2083
+ return this.run(client => client.resumeRecovery(normalized), { allowSignedOut: true });
2084
+ }
2085
+ /** Explicitly enter an applied identity; reads and late Core completions never sign in. */
2086
+ async enterRecoveredSession(request) {
2087
+ const normalized = normalizeRecoveryOperation(request);
2088
+ if (normalized === undefined)
2089
+ return { ok: false, error: failure('invalid-request') };
2090
+ const requestGeneration = this.sessionRevision;
2091
+ const result = await this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
2092
+ if (!result.ok)
2093
+ return result;
2094
+ if (result.value.phase !== 'applied' || !result.value.allowedActions?.includes('activate_identity')) {
2095
+ return { ok: false, error: failure('conflict') };
1485
2096
  }
1486
- return result;
2097
+ return this.applyRecoveredSession(result.value, requestGeneration);
1487
2098
  }
1488
2099
  /** Discard only a pre-attempt operation; Core rejects post-attempt deletion. */
1489
2100
  async discardRecovery(request) {
@@ -1550,6 +2161,12 @@ let AwikiService = (() => {
1550
2161
  return result.ok ? { ok: true, value: { completed: true } } : result;
1551
2162
  }
1552
2163
  /** Read one authoritative versioned member page. */
2164
+ getDisplayProfiles(peers) {
2165
+ if (!Array.isArray(peers) || peers.length > 100 || peers.some(peer => typeof peer !== 'string' || !peer.startsWith('did:'))) {
2166
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
2167
+ }
2168
+ return this.run(client => client.getDisplayProfiles(peers));
2169
+ }
1553
2170
  listGroupMembers(request) {
1554
2171
  const groupDid = normalizeGroupDid(request?.groupDid);
1555
2172
  if (groupDid === undefined
@@ -1812,10 +2429,14 @@ let AwikiService = (() => {
1812
2429
  catch {
1813
2430
  return { ok: false, error: failure('invalid-request') };
1814
2431
  }
2432
+ const requestGeneration = this.sessionRevision;
1815
2433
  return this.run(async (client) => {
1816
- if (normalized.folder !== 'sent')
1817
- return client.listMailInbox(normalized);
1818
- return this.sentMailStore.list(await this.ownerDid(client), normalized);
2434
+ const page = normalized.folder === 'sent'
2435
+ ? await this.mailListClient.listOutbound(normalized)
2436
+ : await client.listMailInbox(normalized);
2437
+ if (this.sessionRevision !== requestGeneration)
2438
+ throw new AwikiSdkError('network');
2439
+ return page;
1819
2440
  });
1820
2441
  }
1821
2442
  /** Read one bounded plain-text mail message. */
@@ -1827,15 +2448,7 @@ let AwikiService = (() => {
1827
2448
  catch {
1828
2449
  return { ok: false, error: failure('invalid-request') };
1829
2450
  }
1830
- return this.run(async (client) => {
1831
- if (!isLocalSentMailId(normalized.messageId))
1832
- return client.readMail(normalized);
1833
- const local = await this.sentMailStore.read(await this.ownerDid(client), normalized.messageId);
1834
- if (local === undefined) {
1835
- throw Object.assign(new Error('sent mail not found'), { name: 'AwikiSdkError', code: 'not-found' });
1836
- }
1837
- return local;
1838
- });
2451
+ return this.run(client => client.readMail(normalized));
1839
2452
  }
1840
2453
  /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1841
2454
  markMailRead(request) {
@@ -1850,22 +2463,7 @@ let AwikiService = (() => {
1850
2463
  catch {
1851
2464
  return { ok: false, error: failure('invalid-request') };
1852
2465
  }
1853
- return this.run(async (client) => {
1854
- const result = await client.sendMail(normalized);
1855
- if (!result.accepted)
1856
- return result;
1857
- try {
1858
- const ownerDid = await this.ownerDid(client);
1859
- const account = await client.getMailAccount().catch(() => undefined);
1860
- await this.sentMailStore.append(ownerDid, normalized, result, account);
1861
- return result;
1862
- }
1863
- catch {
1864
- return result.warnings.length >= 100
1865
- ? result
1866
- : { ...result, warnings: [...result.warnings, 'Sent history could not be saved locally.'] };
1867
- }
1868
- });
2466
+ return this.run(client => client.sendMail(normalized));
1869
2467
  }
1870
2468
  /**
1871
2469
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -1877,11 +2475,19 @@ let AwikiService = (() => {
1877
2475
  if (request?.confirmation !== AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION) {
1878
2476
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1879
2477
  }
2478
+ // Fence pending post-Recovery work before the serialized clear reaches the head of the queue.
2479
+ this.sessionRevision += 1;
1880
2480
  return this.mutateSession(async () => {
1881
2481
  const provider = this.provider;
1882
2482
  if (provider !== undefined)
1883
2483
  await this.stopProviderRuntime(provider);
1884
- const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2484
+ let result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2485
+ // Core's clear is local and idempotent. A provider mutation that was still
2486
+ // settling can surface once as the deliberately opaque `remote` failure.
2487
+ // Retry only that class once after the runtime has already been stopped.
2488
+ if (!result.ok && result.error.code === 'remote') {
2489
+ result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2490
+ }
1885
2491
  if (!result.ok) {
1886
2492
  if (provider !== undefined && this.provider === provider)
1887
2493
  this.ensureProviderRuntime(provider);
@@ -1889,7 +2495,7 @@ let AwikiService = (() => {
1889
2495
  }
1890
2496
  try {
1891
2497
  await this.imageAttachmentCache.clear();
1892
- await this.sentMailStore.clear();
2498
+ await clearLegacySentMailCache(this.activeClientOptions?.stateRoot ?? this.resolved.stateRoot);
1893
2499
  await this.conversationPreferenceStore.clear();
1894
2500
  await this.sessionStore.signIn();
1895
2501
  this.signedOut = false;
@@ -1906,81 +2512,102 @@ let AwikiService = (() => {
1906
2512
  });
1907
2513
  }
1908
2514
  /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
1909
- async applyRecoveredSession(progress) {
2515
+ async applyRecoveredSession(progress, requestGeneration) {
1910
2516
  if (progress.phase !== 'applied')
1911
- return true;
2517
+ return { ok: true, value: progress };
1912
2518
  return this.mutateSession(async () => {
1913
2519
  const provider = this.provider;
1914
2520
  if (provider === undefined)
1915
- return false;
2521
+ return { ok: false, error: failure('remote') };
2522
+ if (this.sessionRevision !== requestGeneration) {
2523
+ return {
2524
+ ok: false,
2525
+ error: {
2526
+ ...failure('remote'),
2527
+ mailRecoveryObservability: fencedMailRecoveryObservability(this.activeIdentityDid === progress.currentDid, 'replaced'),
2528
+ },
2529
+ };
2530
+ }
1916
2531
  try {
1917
2532
  const identity = await provider.client.getIdentity();
1918
- if (identity === null || identity.did !== progress.currentDid)
1919
- return false;
2533
+ const currentPrincipalMatchesRecovery = identity !== null && identity.did === progress.currentDid;
2534
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2535
+ return {
2536
+ ok: false,
2537
+ error: {
2538
+ ...failure('remote'),
2539
+ mailRecoveryObservability: fencedMailRecoveryObservability(currentPrincipalMatchesRecovery, 'replaced'),
2540
+ },
2541
+ };
2542
+ }
2543
+ if (!currentPrincipalMatchesRecovery) {
2544
+ return {
2545
+ ok: false,
2546
+ error: {
2547
+ ...failure('remote'),
2548
+ mailRecoveryObservability: fencedMailRecoveryObservability(false, 'current'),
2549
+ },
2550
+ };
2551
+ }
1920
2552
  const alreadyActive = this.signedOut === false && this.activeIdentityDid === identity.did;
1921
2553
  if (!alreadyActive) {
1922
2554
  await this.sessionStore.signIn();
2555
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2556
+ return {
2557
+ ok: false,
2558
+ error: {
2559
+ ...failure('remote'),
2560
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, 'replaced'),
2561
+ },
2562
+ };
2563
+ }
1923
2564
  this.signedOut = false;
1924
2565
  this.activeIdentityDid = identity.did;
1925
2566
  this.invalidateSummaries();
2567
+ requestGeneration = this.sessionRevision;
2568
+ }
2569
+ const mailRecoveryObservability = await this.reconcileRecoveredMailbox(provider);
2570
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2571
+ return {
2572
+ ok: false,
2573
+ error: {
2574
+ ...failure('remote'),
2575
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, 'replaced'),
2576
+ },
2577
+ };
1926
2578
  }
1927
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
1928
- if (this.provider !== provider)
1929
- return false;
1930
2579
  if (!alreadyActive) {
1931
2580
  const session = { status: 'active', identity };
1932
2581
  this.publishSession(session);
1933
2582
  this.ensureProviderRuntime(provider);
1934
2583
  }
1935
- return reconciled;
2584
+ return { ok: true, value: { ...progress, mailRecoveryObservability } };
1936
2585
  }
1937
2586
  catch {
1938
- return false;
2587
+ return { ok: false, error: failure('remote') };
1939
2588
  }
1940
2589
  });
1941
2590
  }
1942
- /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
1943
- async reconcileRecoveredIdentity(provider, operationId) {
1944
- const logger = this.ctx.logger('awiki-recovery');
1945
- let mailboxRestored = false;
2591
+ /** Rebind Mail first-use ownership and retain only its closed, secret-free classification. */
2592
+ async reconcileRecoveredMailbox(provider) {
1946
2593
  try {
1947
2594
  await provider.client.getMailAccount();
1948
- mailboxRestored = true;
2595
+ return successfulMailRecoveryObservability();
1949
2596
  }
1950
- catch {
1951
- logger.warn('awiki: recovered mailbox reconciliation is pending');
1952
- }
1953
- const target = this.recoveryReconciliationTarget;
1954
- if (target === undefined)
1955
- return mailboxRestored;
1956
- try {
1957
- const authority = await provider.client.issueRecoveryAttestation({ operationId });
1958
- const response = await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
1959
- method: 'POST',
1960
- headers: { 'content-type': 'application/json' },
1961
- body: JSON.stringify({ attestation: authority.attestation }),
1962
- }), request => fetch(request));
1963
- if (!await acceptsRecoveryReconciliation(response)) {
1964
- logger.warn('awiki: recovered model account reconciliation is pending');
1965
- return false;
1966
- }
1967
- return mailboxRestored;
1968
- }
1969
- catch {
1970
- logger.warn('awiki: recovered model account reconciliation is pending');
1971
- return false;
2597
+ catch (error) {
2598
+ return failedMailRecoveryObservability(error);
1972
2599
  }
1973
2600
  }
1974
2601
  /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
1975
2602
  async selectDeviceJoinSession(client) {
1976
2603
  const sessions = await client.listLocalDeviceJoinSessions();
2604
+ const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
1977
2605
  if (this.activeDeviceJoinSessionId !== undefined) {
1978
- const tracked = sessions.find(value => (value.side === 'new_device' && value.joinSessionId === this.activeDeviceJoinSessionId));
2606
+ const tracked = resumable.find(value => value.joinSessionId === this.activeDeviceJoinSessionId);
1979
2607
  if (tracked !== undefined)
1980
2608
  return tracked.joinSessionId;
1981
2609
  this.activeDeviceJoinSessionId = undefined;
1982
2610
  }
1983
- const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
1984
2611
  if (resumable.length > 1) {
1985
2612
  throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
1986
2613
  }
@@ -2048,6 +2675,26 @@ let AwikiService = (() => {
2048
2675
  throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2049
2676
  }
2050
2677
  }
2678
+ async requireRootTransferRecipient(client, deviceId) {
2679
+ if (!client.trustedUserPresenceSupported) {
2680
+ throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
2681
+ }
2682
+ await this.requireDeviceManager(client);
2683
+ await client.syncDeviceManagement();
2684
+ const identity = await client.getIdentity();
2685
+ if (identity === null) {
2686
+ throw Object.assign(new Error('identity required'), { name: 'AwikiSdkError', code: 'not-registered' });
2687
+ }
2688
+ const target = (await client.getDeviceRegistry()).find(device => device.deviceId === deviceId);
2689
+ if (target === undefined
2690
+ || target.isCurrent
2691
+ || target.status !== 'active'
2692
+ || target.role !== 'member'
2693
+ || target.managementReady) {
2694
+ throw Object.assign(new Error('root transfer recipient unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
2695
+ }
2696
+ return identity.did;
2697
+ }
2051
2698
  requestRef(joinSessionId) {
2052
2699
  const existing = this.requestRefs.get(joinSessionId);
2053
2700
  if (existing !== undefined)
@@ -2074,8 +2721,10 @@ let AwikiService = (() => {
2074
2721
  this.requestSessions.clear();
2075
2722
  this.deviceRefs.clear();
2076
2723
  this.deviceIds.clear();
2724
+ this.rootTransfers.clear();
2077
2725
  return {
2078
2726
  canManage: false,
2727
+ rootTransferSupported: client.trustedUserPresenceSupported,
2079
2728
  ...current.role === undefined ? {} : { role: current.role },
2080
2729
  readiness: current.readiness,
2081
2730
  devices: [],
@@ -2087,11 +2736,25 @@ let AwikiService = (() => {
2087
2736
  .filter(request => request.claimedByCurrentDevice)
2088
2737
  .map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
2089
2738
  const devices = await client.getDeviceRegistry();
2739
+ const identity = await client.getIdentity();
2740
+ const joinedAtByDeviceId = new Map();
2741
+ for (const request of requests) {
2742
+ const previous = joinedAtByDeviceId.get(request.protocolDeviceId);
2743
+ if (previous === undefined || request.issuedAt > previous) {
2744
+ joinedAtByDeviceId.set(request.protocolDeviceId, request.issuedAt);
2745
+ }
2746
+ }
2747
+ const initialDeviceJoinedAt = identity === null
2748
+ ? undefined
2749
+ : new Date(identity.registeredAt).toISOString();
2090
2750
  return {
2091
2751
  canManage: true,
2752
+ rootTransferSupported: client.trustedUserPresenceSupported,
2092
2753
  role: 'admin',
2093
2754
  readiness: 'admin_ready',
2094
- devices: devices.map(device => this.publicDevice(device)),
2755
+ devices: devices
2756
+ .filter(device => device.status === 'active')
2757
+ .map(device => this.publicDevice(device, joinedAtByDeviceId.get(device.deviceId) ?? (device.isCurrent ? initialDeviceJoinedAt : undefined))),
2095
2758
  requests: requests.map(request => ({
2096
2759
  requestRef: this.requestRef(request.joinSessionId),
2097
2760
  candidateKeyFingerprint: request.candidateKeyFingerprint,
@@ -2103,9 +2766,12 @@ let AwikiService = (() => {
2103
2766
  })),
2104
2767
  };
2105
2768
  }
2106
- publicDevice(device) {
2769
+ publicDevice(device, joinedAt) {
2770
+ const fingerprint = createHash('sha256').update(device.deviceId).digest('hex').slice(0, 8).toUpperCase();
2107
2771
  return {
2108
2772
  deviceRef: this.deviceRef(device.deviceId),
2773
+ displayId: `${fingerprint.slice(0, 4)}-${fingerprint.slice(4)}`,
2774
+ ...joinedAt === undefined ? {} : { joinedAt },
2109
2775
  status: device.status,
2110
2776
  role: device.role,
2111
2777
  managementReady: device.managementReady,
@@ -2129,6 +2795,7 @@ let AwikiService = (() => {
2129
2795
  this.requestSessions.clear();
2130
2796
  this.deviceRefs.clear();
2131
2797
  this.deviceIds.clear();
2798
+ this.rootTransfers.clear();
2132
2799
  for (const controller of this.activeSummaryRequests)
2133
2800
  controller.abort();
2134
2801
  this.activeSummaryRequests.clear();
@@ -2150,6 +2817,10 @@ let AwikiService = (() => {
2150
2817
  /** Invoke the current client and normalize every rejection to a public result. */
2151
2818
  async run(operation, options = {}) {
2152
2819
  try {
2820
+ const generation = this.runtimeGeneration;
2821
+ if (this.getUpdatePolicyStatus().restricted) {
2822
+ return { ok: false, error: failure('client-version-unsupported') };
2823
+ }
2153
2824
  if (options.allowSignedOut !== true && await this.isSignedOut()) {
2154
2825
  return { ok: false, error: failure('signed-out') };
2155
2826
  }
@@ -2157,6 +2828,9 @@ let AwikiService = (() => {
2157
2828
  if (provider === undefined)
2158
2829
  throw new ProviderUnavailableError();
2159
2830
  const value = await operation(provider.client);
2831
+ if (this.runtimeGeneration !== generation || this.provider !== provider) {
2832
+ return { ok: false, error: failure('conflict') };
2833
+ }
2160
2834
  if (options.skipAttachmentByteValidation !== true && containsUnexpectedBinary(value, new Set())) {
2161
2835
  return { ok: false, error: failure('remote') };
2162
2836
  }
@@ -2166,6 +2840,11 @@ let AwikiService = (() => {
2166
2840
  return { ok: false, error: normalizeFailure(error) };
2167
2841
  }
2168
2842
  }
2843
+ assertVersionSupported() {
2844
+ if (this.getUpdatePolicyStatus().restricted) {
2845
+ throw new Error('awiki: this plugin version must be updated before using the active tenant');
2846
+ }
2847
+ }
2169
2848
  /** Validate mail input before entering the provider and preserve fixed public failures. */
2170
2849
  runValidatedMail(validate, operation) {
2171
2850
  let request;
@@ -2226,12 +2905,20 @@ let AwikiService = (() => {
2226
2905
  }
2227
2906
  /** Serialize sign-in, sign-out, and destructive clear transitions. */
2228
2907
  mutateSession(operation) {
2229
- const pending = this.sessionMutation.then(operation, operation);
2908
+ const generation = this.runtimeGeneration;
2909
+ if (this.tenantSwitching)
2910
+ return Promise.resolve({ ok: false, error: failure('conflict') });
2911
+ const invoke = () => this.tenantSwitching || generation !== this.runtimeGeneration
2912
+ ? Promise.resolve({ ok: false, error: failure('conflict') })
2913
+ : operation();
2914
+ const pending = this.sessionMutation.then(invoke, invoke);
2230
2915
  this.sessionMutation = pending.then(() => undefined, () => undefined);
2231
2916
  return pending;
2232
2917
  }
2233
2918
  /** Start identity realtime and the optional Agent consumer without blocking identity success. */
2234
2919
  ensureProviderRuntime(provider) {
2920
+ if (this.tenantSwitching)
2921
+ return;
2235
2922
  const identityDid = this.activeIdentityDid;
2236
2923
  if (provider.runtimeReplacement !== undefined)
2237
2924
  return;
@@ -2267,6 +2954,8 @@ let AwikiService = (() => {
2267
2954
  provider.runtimeReplacement = observed;
2268
2955
  }
2269
2956
  ensureRealtimeSupervisor(provider) {
2957
+ if (this.getUpdatePolicyStatus().restricted)
2958
+ return;
2270
2959
  if (!this.resolved.realtimeEnabled
2271
2960
  || provider.realtimeSupervisor !== undefined
2272
2961
  || provider.realtimeStartup !== undefined
@@ -2326,6 +3015,8 @@ let AwikiService = (() => {
2326
3015
  await provider.agentConsumer?.reconcileOnce();
2327
3016
  }
2328
3017
  ensureAgentConsumer(provider) {
3018
+ if (this.getUpdatePolicyStatus().restricted)
3019
+ return;
2329
3020
  const workspaceContext = this.workspaceContext;
2330
3021
  const source = provider.client.agentInbox ?? provider.client.listener;
2331
3022
  const identityDid = this.activeIdentityDid;
@@ -2348,6 +3039,7 @@ let AwikiService = (() => {
2348
3039
  const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
2349
3040
  const consumer = new AwikiAgentListener(source, agents, {
2350
3041
  ...this.resolved.listener,
3042
+ stateRoot: this.activeClientOptions?.stateRoot ?? this.resolved.listener.stateRoot,
2351
3043
  identityScope: identityDid,
2352
3044
  }, logger);
2353
3045
  provider.agentConsumer = consumer;