@awiki/dsh-plugin 0.3.7 → 0.3.11-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +112 -73
  2. package/README.zh.md +91 -62
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/cordis.patch.yml +6 -5
  5. package/lib/builtin-tenants.json +22 -0
  6. package/lib/builtin-tenants.sha256 +1 -0
  7. package/lib/client.js +9281 -2379
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +3553 -1138
  10. package/lib/model-proxy-contract.js +19 -11
  11. package/lib/provider.js +73 -17
  12. package/lib/{sdk-adapter--trNlr-T.mjs → sdk-adapter-CYcJH7sM.mjs} +374 -101
  13. package/lib/typert.host.js +1595 -168
  14. package/lib/typert.remote-client.d.ts +43 -7
  15. package/lib/typert.remote-client.d.ts.map +1 -1
  16. package/lib/typert.remote-client.js +1595 -168
  17. package/lib/types/builtin-tenant-config.d.ts +19 -0
  18. package/lib/types/builtin-tenant-config.d.ts.map +1 -0
  19. package/lib/types/builtin-tenant-config.js +57 -0
  20. package/lib/types/builtin-tenant-config.js.map +1 -0
  21. package/lib/types/client/AwikiDeviceJoinReminder.d.ts +15 -0
  22. package/lib/types/client/AwikiDeviceJoinReminder.d.ts.map +1 -0
  23. package/lib/types/client/AwikiDeviceJoinReminder.js +74 -0
  24. package/lib/types/client/AwikiDeviceJoinReminder.js.map +1 -0
  25. package/lib/types/client/AwikiDevices.d.ts +33 -0
  26. package/lib/types/client/AwikiDevices.d.ts.map +1 -0
  27. package/lib/types/client/AwikiDevices.js +141 -0
  28. package/lib/types/client/AwikiDevices.js.map +1 -0
  29. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
  30. package/lib/types/client/AwikiGroupDetails.js +16 -13
  31. package/lib/types/client/AwikiGroupDetails.js.map +1 -1
  32. package/lib/types/client/AwikiIdentityAccess.d.ts +17 -8
  33. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  34. package/lib/types/client/AwikiIdentityAccess.js +269 -37
  35. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  36. package/lib/types/client/AwikiIntegrationSettings.d.ts +21 -0
  37. package/lib/types/client/AwikiIntegrationSettings.d.ts.map +1 -0
  38. package/lib/types/client/AwikiIntegrationSettings.js +105 -0
  39. package/lib/types/client/AwikiIntegrationSettings.js.map +1 -0
  40. package/lib/types/client/AwikiMail.d.ts +1 -1
  41. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  42. package/lib/types/client/AwikiMail.js +17 -137
  43. package/lib/types/client/AwikiMail.js.map +1 -1
  44. package/lib/types/client/AwikiOverlay.d.ts +0 -1
  45. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  46. package/lib/types/client/AwikiOverlay.js +26 -28
  47. package/lib/types/client/AwikiOverlay.js.map +1 -1
  48. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
  49. package/lib/types/client/AwikiProfileCard.js +17 -18
  50. package/lib/types/client/AwikiProfileCard.js.map +1 -1
  51. package/lib/types/client/AwikiRecoveryForm.d.ts +6 -1
  52. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  53. package/lib/types/client/AwikiRecoveryForm.js +74 -30
  54. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  55. package/lib/types/client/AwikiSettingsSection.d.ts +28 -13
  56. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  57. package/lib/types/client/AwikiSettingsSection.js +93 -61
  58. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  59. package/lib/types/client/AwikiUpdates.d.ts +10 -0
  60. package/lib/types/client/AwikiUpdates.d.ts.map +1 -0
  61. package/lib/types/client/AwikiUpdates.js +57 -0
  62. package/lib/types/client/AwikiUpdates.js.map +1 -0
  63. package/lib/types/client/awiki-client-bridge.d.ts +2 -2
  64. package/lib/types/client/awiki-client-bridge.d.ts.map +1 -1
  65. package/lib/types/client/awiki-client-bridge.js +4 -1
  66. package/lib/types/client/awiki-client-bridge.js.map +1 -1
  67. package/lib/types/client/controller.d.ts +92 -44
  68. package/lib/types/client/controller.d.ts.map +1 -1
  69. package/lib/types/client/controller.js +761 -334
  70. package/lib/types/client/controller.js.map +1 -1
  71. package/lib/types/client/drafts.d.ts +31 -0
  72. package/lib/types/client/drafts.d.ts.map +1 -0
  73. package/lib/types/client/drafts.js +94 -0
  74. package/lib/types/client/drafts.js.map +1 -0
  75. package/lib/types/client/image-cache.d.ts +2 -2
  76. package/lib/types/client/image-cache.d.ts.map +1 -1
  77. package/lib/types/client/image-cache.js +15 -2
  78. package/lib/types/client/image-cache.js.map +1 -1
  79. package/lib/types/client/index.d.ts +1 -1
  80. package/lib/types/client/index.d.ts.map +1 -1
  81. package/lib/types/client/index.js +64 -19
  82. package/lib/types/client/index.js.map +1 -1
  83. package/lib/types/client/integration-operation.d.ts +9 -0
  84. package/lib/types/client/integration-operation.d.ts.map +1 -0
  85. package/lib/types/client/integration-operation.js +54 -0
  86. package/lib/types/client/integration-operation.js.map +1 -0
  87. package/lib/types/client/mail-list-cache.d.ts +2 -0
  88. package/lib/types/client/mail-list-cache.d.ts.map +1 -1
  89. package/lib/types/client/mail-list-cache.js +18 -0
  90. package/lib/types/client/mail-list-cache.js.map +1 -1
  91. package/lib/types/client/otp-cooldown.d.ts +7 -0
  92. package/lib/types/client/otp-cooldown.d.ts.map +1 -0
  93. package/lib/types/client/otp-cooldown.js +29 -0
  94. package/lib/types/client/otp-cooldown.js.map +1 -0
  95. package/lib/types/client/settings-controller.d.ts +38 -2
  96. package/lib/types/client/settings-controller.d.ts.map +1 -1
  97. package/lib/types/client/settings-controller.js +206 -10
  98. package/lib/types/client/settings-controller.js.map +1 -1
  99. package/lib/types/client/settings-locales.d.ts +1 -1
  100. package/lib/types/client/settings-locales.d.ts.map +1 -1
  101. package/lib/types/client/settings-locales.js +192 -14
  102. package/lib/types/client/settings-locales.js.map +1 -1
  103. package/lib/types/client/slots.d.ts +21 -10
  104. package/lib/types/client/slots.d.ts.map +1 -1
  105. package/lib/types/client/slots.js +0 -1
  106. package/lib/types/client/slots.js.map +1 -1
  107. package/lib/types/client/store.d.ts +1 -1
  108. package/lib/types/client/store.d.ts.map +1 -1
  109. package/lib/types/client/store.js +1 -1
  110. package/lib/types/client/store.js.map +1 -1
  111. package/lib/types/conversation-preferences.d.ts.map +1 -1
  112. package/lib/types/conversation-preferences.js +0 -17
  113. package/lib/types/conversation-preferences.js.map +1 -1
  114. package/lib/types/desktop-distribution.d.ts +25 -0
  115. package/lib/types/desktop-distribution.d.ts.map +1 -0
  116. package/lib/types/desktop-distribution.js +57 -0
  117. package/lib/types/desktop-distribution.js.map +1 -0
  118. package/lib/types/domain.d.ts +1 -1
  119. package/lib/types/domain.d.ts.map +1 -1
  120. package/lib/types/domain.js +2 -1
  121. package/lib/types/domain.js.map +1 -1
  122. package/lib/types/index.d.ts +165 -41
  123. package/lib/types/index.d.ts.map +1 -1
  124. package/lib/types/index.js +1594 -535
  125. package/lib/types/index.js.map +1 -1
  126. package/lib/types/integration-client.d.ts +17 -0
  127. package/lib/types/integration-client.d.ts.map +1 -0
  128. package/lib/types/integration-client.js +233 -0
  129. package/lib/types/integration-client.js.map +1 -0
  130. package/lib/types/legacy-sent-mail-cache.d.ts +4 -0
  131. package/lib/types/legacy-sent-mail-cache.d.ts.map +1 -0
  132. package/lib/types/legacy-sent-mail-cache.js +29 -0
  133. package/lib/types/legacy-sent-mail-cache.js.map +1 -0
  134. package/lib/types/listener-state.d.ts +5 -3
  135. package/lib/types/listener-state.d.ts.map +1 -1
  136. package/lib/types/listener-state.js +28 -13
  137. package/lib/types/listener-state.js.map +1 -1
  138. package/lib/types/listener.d.ts +9 -32
  139. package/lib/types/listener.d.ts.map +1 -1
  140. package/lib/types/listener.js +13 -128
  141. package/lib/types/listener.js.map +1 -1
  142. package/lib/types/mail-list-client.d.ts +12 -0
  143. package/lib/types/mail-list-client.d.ts.map +1 -0
  144. package/lib/types/mail-list-client.js +254 -0
  145. package/lib/types/mail-list-client.js.map +1 -0
  146. package/lib/types/mail-recovery-observability.d.ts +8 -0
  147. package/lib/types/mail-recovery-observability.d.ts.map +1 -0
  148. package/lib/types/mail-recovery-observability.js +119 -0
  149. package/lib/types/mail-recovery-observability.js.map +1 -0
  150. package/lib/types/mail.d.ts +2 -28
  151. package/lib/types/mail.d.ts.map +1 -1
  152. package/lib/types/mail.js +2 -79
  153. package/lib/types/mail.js.map +1 -1
  154. package/lib/types/model-proxy-contract.d.ts +8 -8
  155. package/lib/types/model-proxy-contract.d.ts.map +1 -1
  156. package/lib/types/model-proxy-contract.js +15 -8
  157. package/lib/types/model-proxy-contract.js.map +1 -1
  158. package/lib/types/package-version.generated.d.ts +4 -0
  159. package/lib/types/package-version.generated.d.ts.map +1 -0
  160. package/lib/types/package-version.generated.js +4 -0
  161. package/lib/types/package-version.generated.js.map +1 -0
  162. package/lib/types/profile-state.d.ts +2 -13
  163. package/lib/types/profile-state.d.ts.map +1 -1
  164. package/lib/types/profile-state.js +3 -20
  165. package/lib/types/profile-state.js.map +1 -1
  166. package/lib/types/provider-api.d.ts +134 -43
  167. package/lib/types/provider-api.d.ts.map +1 -1
  168. package/lib/types/provider.d.ts +8 -2
  169. package/lib/types/provider.d.ts.map +1 -1
  170. package/lib/types/provider.js +76 -16
  171. package/lib/types/provider.js.map +1 -1
  172. package/lib/types/realtime-supervisor.d.ts +60 -0
  173. package/lib/types/realtime-supervisor.d.ts.map +1 -0
  174. package/lib/types/realtime-supervisor.js +255 -0
  175. package/lib/types/realtime-supervisor.js.map +1 -0
  176. package/lib/types/registration-policy.d.ts +7 -0
  177. package/lib/types/registration-policy.d.ts.map +1 -0
  178. package/lib/types/registration-policy.js +21 -0
  179. package/lib/types/registration-policy.js.map +1 -0
  180. package/lib/types/sdk-adapter.d.ts +62 -11
  181. package/lib/types/sdk-adapter.d.ts.map +1 -1
  182. package/lib/types/sdk-adapter.js +285 -123
  183. package/lib/types/sdk-adapter.js.map +1 -1
  184. package/lib/types/settings-rpc-contract.d.ts +63 -5
  185. package/lib/types/settings-rpc-contract.d.ts.map +1 -1
  186. package/lib/types/settings-rpc-contract.js +108 -5
  187. package/lib/types/settings-rpc-contract.js.map +1 -1
  188. package/lib/types/settings-rpc.d.ts +14 -1
  189. package/lib/types/settings-rpc.d.ts.map +1 -1
  190. package/lib/types/settings-rpc.js +132 -3
  191. package/lib/types/settings-rpc.js.map +1 -1
  192. package/lib/types/settings-transport.d.ts +7 -0
  193. package/lib/types/settings-transport.d.ts.map +1 -0
  194. package/lib/types/settings-transport.js +52 -0
  195. package/lib/types/settings-transport.js.map +1 -0
  196. package/lib/types/tenant-registry.d.ts +74 -0
  197. package/lib/types/tenant-registry.d.ts.map +1 -0
  198. package/lib/types/tenant-registry.js +440 -0
  199. package/lib/types/tenant-registry.js.map +1 -0
  200. package/lib/types/tools.d.ts +1 -1
  201. package/lib/types/tools.d.ts.map +1 -1
  202. package/lib/types/tools.js +4 -20
  203. package/lib/types/tools.js.map +1 -1
  204. package/lib/types/types.d.ts +249 -66
  205. package/lib/types/types.d.ts.map +1 -1
  206. package/lib/types/types.js.map +1 -1
  207. package/lib/types/update-policy.d.ts +51 -0
  208. package/lib/types/update-policy.d.ts.map +1 -0
  209. package/lib/types/update-policy.js +379 -0
  210. package/lib/types/update-policy.js.map +1 -0
  211. package/lib/types/version.d.ts +4 -0
  212. package/lib/types/version.d.ts.map +1 -0
  213. package/lib/types/version.js +44 -0
  214. package/lib/types/version.js.map +1 -0
  215. package/package.json +88 -61
  216. package/lib/types/base64.d.ts +0 -4
  217. package/lib/types/base64.d.ts.map +0 -1
  218. package/lib/types/base64.js +0 -28
  219. package/lib/types/base64.js.map +0 -1
  220. package/lib/types/client/mail-attachment.d.ts +0 -40
  221. package/lib/types/client/mail-attachment.d.ts.map +0 -1
  222. package/lib/types/client/mail-attachment.js +0 -187
  223. package/lib/types/client/mail-attachment.js.map +0 -1
  224. package/lib/types/sent-mail-store.d.ts +0 -27
  225. package/lib/types/sent-mail-store.d.ts.map +0 -1
  226. package/lib/types/sent-mail-store.js +0 -348
  227. 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,31 +32,43 @@ 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
+ export { registerAwikiLoopbackRpc } from "./settings-transport.js";
36
+ import { registerAwikiSettingsTransport } from "./settings-transport.js";
37
+ import { registrationHandleLocalPart, shortHandleInviteRequired } from "./registration-policy.js";
38
+ import { decodeDesktopDistribution } from "./desktop-distribution.js";
39
+ /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
36
40
  import { Context } from '@deepseek-ai/cordis';
37
- import { createHash } from 'node:crypto';
38
- import { existsSync } from 'node:fs';
41
+ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
39
42
  import { homedir } from 'node:os';
40
43
  import { isAbsolute, join } from 'node:path';
41
44
  import z from '@deepseek-ai/schemastery';
42
45
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
43
- import { settingsNamespace } from '@deepseek-ai/dsh-settings';
46
+ import {} from '@deepseek-ai/dsh-settings';
44
47
  import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
45
48
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
46
- import { downloadedAttachment } from "./sdk-adapter.js";
47
- import { standardBase64Syntax } from "./base64.js";
49
+ import { AwikiIntegrationClient } from "./integration-client.js";
50
+ import { clearLegacySentMailCache } from "./legacy-sent-mail-cache.js";
51
+ import { AwikiMailListClient } from "./mail-list-client.js";
52
+ import { AwikiSdkError, downloadedAttachment } from "./sdk-adapter.js";
53
+ import { failedMailRecoveryObservability, fencedMailRecoveryObservability, successfulMailRecoveryObservability, } from "./mail-recovery-observability.js";
48
54
  import { registerAwikiTools } from "./tools.js";
49
- import { MAIL_ATTACHMENT_SERVICE_MAX_BYTES, MAIL_ATTACHMENT_SERVICE_MAX_COUNT, MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES, mailAttachmentDownloadRequest, mailAttachmentContentType, mailAttachmentFileName, mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
55
+ import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
50
56
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
51
57
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
58
+ import { AWIKI_DEFAULT_BUILTIN_TENANT, } from "./builtin-tenant-config.js";
52
59
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
53
60
  import { createAwikiSettingsRpcHandler } from "./settings-rpc.js";
54
61
  import { AwikiSessionStore } from "./session.js";
55
62
  import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "./attachment-cache.js";
56
- import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
57
63
  import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
58
64
  import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
59
- import { resolveAwikiProfileName, resolveAwikiStateRoot, resolveAwikiTenantStateRoot, } from "./profile-state.js";
65
+ import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
66
+ import { resolveAwikiStateRoot } from "./profile-state.js";
67
+ import { AwikiTenantRegistry, } from "./tenant-registry.js";
68
+ import { checkAwikiUpdatePolicy, readAwikiUpdatePolicyStatus, DSH_AWIKI_VERSION, } from "./update-policy.js";
60
69
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
70
+ export { AWIKI_CHINA_TENANT_ID, AWIKI_GLOBAL_TENANT_ID, AWIKI_OFFICIAL_CATALOG_VERSION, AWIKI_TENANT_REGISTRY_SCHEMA_VERSION, } from "./tenant-registry.js";
71
+ export { DSH_AWIKI_MODEL_PROXY_VERSION, DSH_AWIKI_VERSION, compareVersions as compareAwikiPluginVersions, } from "./update-policy.js";
61
72
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
62
73
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
63
74
  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";
@@ -67,37 +78,37 @@ export const DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
67
78
  export const DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
68
79
  /** Default browser polling interval while the AWiki drawer is open. */
69
80
  export const DEFAULT_POLL_INTERVAL_MS = 3_000;
70
- /** Default AWiki production service origin. */
71
- export const DEFAULT_AWIKI_SERVICE_URL = 'https://awiki.ai';
72
- /** Default authoritative AWiki message-service DID. */
73
- export const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = 'did:wba:awiki.ai';
81
+ /** Package-configured primary AWiki service origin. */
82
+ export const DEFAULT_AWIKI_SERVICE_URL = AWIKI_DEFAULT_BUILTIN_TENANT.backendOrigin;
83
+ /** Package-configured primary authoritative AWiki message-service DID. */
84
+ export const DEFAULT_AWIKI_MESSAGE_SERVICE_DID = `did:wba:${AWIKI_DEFAULT_BUILTIN_TENANT.didHost}`;
74
85
  /** Host-owned model input cap after message minimization. */
75
86
  export const DEFAULT_SUMMARY_MAX_INPUT_BYTES = 32 * 1024;
76
87
  /** Hard limit for one user-triggered conversation summary. */
77
88
  export const MAX_SUMMARY_MESSAGES = 50;
78
89
  /** Loader schema for the Host deployment configuration. */
79
90
  export const Config = z.object({
80
- userServiceUrl: z.string(),
91
+ userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
81
92
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
82
- messageServiceUrl: z.string(),
93
+ legacyTenantSlot: z.string(),
94
+ messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
83
95
  mailServiceUrl: z.string(),
84
- messageServicePublicUrl: z.string(),
85
- messageServiceDid: z.string(),
96
+ messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
97
+ messageServiceDid: z.string().default(DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
86
98
  allowedAttachmentOrigins: z.array(z.string()).default([]),
87
99
  allowInsecureLoopbackForTesting: z.boolean().default(false),
88
100
  stateRoot: z.string(),
89
101
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
90
- mailAttachmentMaxCount: z.number().default(MAIL_ATTACHMENT_SERVICE_MAX_COUNT),
91
- mailAttachmentMaxBytes: z.number().default(MAIL_ATTACHMENT_SERVICE_MAX_BYTES),
92
- mailAttachmentTotalMaxBytes: z.number().default(MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES),
93
102
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
94
103
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
104
+ realtimeEnabled: z.boolean().default(true),
95
105
  listenerEnabled: z.boolean().default(false),
96
106
  listenerAllowedPeers: z.array(z.string()).default([]),
97
107
  listenerWorkspacePath: z.string(),
98
108
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES),
99
109
  });
100
110
  const FAILURE_CODES = new Set([
111
+ 'client-version-unsupported',
101
112
  'not-registered',
102
113
  'signed-out',
103
114
  'already-registered',
@@ -105,11 +116,12 @@ const FAILURE_CODES = new Set([
105
116
  'invalid-otp',
106
117
  'challenge-expired',
107
118
  'handle-unavailable',
119
+ 'short-handle-invite-required',
108
120
  'not-found',
109
121
  'forbidden',
122
+ 'device-rejoin-required',
110
123
  'identity-recovery-required',
111
124
  'conflict',
112
- 'state-in-use',
113
125
  'rate-limited',
114
126
  'group-membership-required',
115
127
  'group-identity-stale',
@@ -123,11 +135,18 @@ const FAILURE_CODES = new Set([
123
135
  'network',
124
136
  'remote',
125
137
  ]);
126
- const LISTENER_RESTART_BASE_DELAY_MS = 1_000;
127
- const LISTENER_RESTART_MAX_DELAY_MS = 30_000;
128
- const LISTENER_RESTART_MAX_ATTEMPT = 6;
129
- const LISTENER_STABLE_RESET_MS = 60_000;
138
+ const DEVICE_JOIN_APPROVAL_CONFIRMATION = 'APPROVE';
139
+ const DEVICE_REVOKE_CONFIRMATION = 'REVOKE';
140
+ const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
141
+ const RESUMABLE_JOIN_PHASES = new Set([
142
+ 'pending',
143
+ 'challenge_prepared',
144
+ 'response_prepared',
145
+ 'response_verified',
146
+ 'approval_prepared',
147
+ ]);
130
148
  const FAILURE_MESSAGES = {
149
+ 'client-version-unsupported': 'This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.',
131
150
  'not-registered': 'No AWiki identity is registered for this deployment.',
132
151
  'signed-out': 'This installation is signed out of AWiki.',
133
152
  'already-registered': 'This deployment already has an AWiki identity.',
@@ -135,11 +154,12 @@ const FAILURE_MESSAGES = {
135
154
  'invalid-otp': 'The AWiki verification code is invalid.',
136
155
  'challenge-expired': 'The AWiki verification challenge expired.',
137
156
  'handle-unavailable': 'The requested AWiki handle is unavailable.',
157
+ 'short-handle-invite-required': 'A new one- through four-character AWiki handle requires an invitation.',
138
158
  'not-found': 'The requested AWiki resource was not found.',
139
159
  'forbidden': 'The AWiki operation is not permitted.',
160
+ 'device-rejoin-required': 'This device credential was revoked and must join the Handle again.',
140
161
  'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
141
162
  'conflict': 'The AWiki operation conflicts with current state.',
142
- 'state-in-use': 'AWiki data is already open in another client.',
143
163
  'rate-limited': 'The AWiki service rate-limited the request.',
144
164
  'group-membership-required': 'The active AWiki identity is not a member of this group.',
145
165
  'group-identity-stale': 'The AWiki group identity binding is still recovering.',
@@ -157,6 +177,67 @@ class ProviderUnavailableError extends Error {
157
177
  }
158
178
  class SummaryProviderUnavailableError extends Error {
159
179
  }
180
+ function candidateJoinPhase(value) {
181
+ if (value.remoteState === 'rejected')
182
+ return 'rejected';
183
+ if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
184
+ return 'cancelled';
185
+ if (value.remoteState === 'expired' || value.localPhase === 'expired')
186
+ return 'expired';
187
+ if (value.localPhase === 'authorized'
188
+ && value.remoteState === 'consumed'
189
+ && value.completed
190
+ && value.identity !== undefined)
191
+ return 'authorized';
192
+ if (value.sas !== undefined) {
193
+ return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
194
+ ? 'sas-ready'
195
+ : undefined;
196
+ }
197
+ if (value.remoteState === 'challenge_sent'
198
+ || value.remoteState === 'response_verified'
199
+ || value.remoteState === 'consumed'
200
+ || value.localPhase !== 'pending')
201
+ return 'verifying';
202
+ return value.remoteState === 'pending' ? 'pending' : undefined;
203
+ }
204
+ function adminJoinPhase(value) {
205
+ if (value.remoteState === 'rejected')
206
+ return 'rejected';
207
+ if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
208
+ return 'cancelled';
209
+ if (value.remoteState === 'expired' || value.localPhase === 'expired')
210
+ return 'expired';
211
+ if (value.localPhase === 'authorized' && value.remoteState === 'consumed')
212
+ return 'authorized';
213
+ if (value.sas !== undefined) {
214
+ return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
215
+ ? 'sas-ready'
216
+ : undefined;
217
+ }
218
+ if (value.remoteState === 'challenge_sent'
219
+ || value.remoteState === 'response_verified'
220
+ || value.remoteState === 'consumed'
221
+ || value.localPhase !== 'pending')
222
+ return 'verifying';
223
+ return value.remoteState === 'pending' ? 'pending' : undefined;
224
+ }
225
+ function requestJoinPhase(value) {
226
+ switch (value.state) {
227
+ case 'pending': return 'pending';
228
+ case 'challenge_sent':
229
+ case 'response_verified': return 'verifying';
230
+ case 'consumed': return 'authorized';
231
+ case 'cancelled': return 'cancelled';
232
+ case 'rejected': return 'rejected';
233
+ case 'expired': return 'expired';
234
+ }
235
+ }
236
+ function constantTimeSasMatches(expected, actual) {
237
+ if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual))
238
+ return false;
239
+ return timingSafeEqual(Buffer.from(expected, 'ascii'), Buffer.from(actual, 'ascii'));
240
+ }
160
241
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
161
242
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
162
243
  let url;
@@ -175,17 +256,25 @@ function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
175
256
  }
176
257
  return raw;
177
258
  }
178
- /** Resolve the only supported post-recovery endpoint without accepting URL-carried state. */
179
- function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting) {
180
- if (target?.kind !== 'model-proxy-v1' || typeof target.baseURL !== 'string') {
181
- throw new TypeError('awiki: recovery reconciliation target is invalid');
259
+ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting) {
260
+ if (typeof services !== 'object' || services === null || Array.isArray(services))
261
+ return undefined;
262
+ const candidate = services[name];
263
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
264
+ return undefined;
265
+ const value = candidate;
266
+ if (value.enabled !== true || typeof value.base_url !== 'string')
267
+ return undefined;
268
+ try {
269
+ const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
270
+ const parsed = new URL(baseUrl);
271
+ if (parsed.search !== '')
272
+ return undefined;
273
+ return baseUrl;
182
274
  }
183
- const baseURL = serviceUrl('recoveryReconciliationTarget.baseURL', target.baseURL, allowInsecureLoopbackForTesting);
184
- const parsed = new URL(baseURL);
185
- if (parsed.search !== '') {
186
- throw new TypeError('awiki: recovery reconciliation target must not contain a query');
275
+ catch {
276
+ return undefined;
187
277
  }
188
- return new URL('/api/identity-recovery', parsed).toString();
189
278
  }
190
279
  /** Validate a provider domain without inferring it from an API endpoint. */
191
280
  function serviceDomain(raw, field = 'userServiceDomain') {
@@ -241,38 +330,15 @@ function resolveConfig(ctx, config) {
241
330
  const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
242
331
  ? join(homedir(), '.dsh')
243
332
  : configuredDshHome;
244
- const profileName = configuredStateRoot === undefined || configuredStateRoot.length === 0
245
- ? resolveAwikiProfileName(ctx, dshHome)
246
- : undefined;
247
333
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
248
334
  ? resolveAwikiStateRoot(ctx, dshHome)
249
335
  : configuredStateRoot;
250
- const legacySharedStateDetected = profileName !== undefined
251
- && existsSync(join(dshHome, 'awiki', 'im-core'));
252
336
  if (!isAbsolute(stateRoot))
253
337
  throw new TypeError('awiki: stateRoot must be an absolute path');
254
338
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
255
339
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) {
256
340
  throw new TypeError('awiki: attachmentMaxBytes must be a positive safe integer');
257
341
  }
258
- const mailAttachmentMaxCount = config.mailAttachmentMaxCount ?? MAIL_ATTACHMENT_SERVICE_MAX_COUNT;
259
- if (!Number.isSafeInteger(mailAttachmentMaxCount)
260
- || mailAttachmentMaxCount < 0
261
- || mailAttachmentMaxCount > MAIL_ATTACHMENT_SERVICE_MAX_COUNT) {
262
- throw new TypeError('awiki: mailAttachmentMaxCount must be an integer from 0 through 10');
263
- }
264
- const mailAttachmentMaxBytes = config.mailAttachmentMaxBytes ?? MAIL_ATTACHMENT_SERVICE_MAX_BYTES;
265
- if (!Number.isSafeInteger(mailAttachmentMaxBytes)
266
- || mailAttachmentMaxBytes < 1
267
- || mailAttachmentMaxBytes > MAIL_ATTACHMENT_SERVICE_MAX_BYTES) {
268
- throw new TypeError('awiki: mailAttachmentMaxBytes must be a positive safe integer no greater than 10 MiB');
269
- }
270
- const mailAttachmentTotalMaxBytes = config.mailAttachmentTotalMaxBytes ?? MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES;
271
- if (!Number.isSafeInteger(mailAttachmentTotalMaxBytes)
272
- || mailAttachmentTotalMaxBytes < mailAttachmentMaxBytes
273
- || mailAttachmentTotalMaxBytes > MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES) {
274
- throw new TypeError('awiki: mailAttachmentTotalMaxBytes must cover one attachment and not exceed 18 MiB');
275
- }
276
342
  const imageAttachmentCacheMaxBytes = config.imageAttachmentCacheMaxBytes ?? DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES;
277
343
  if (!Number.isSafeInteger(imageAttachmentCacheMaxBytes)
278
344
  || imageAttachmentCacheMaxBytes < minimumImageAttachmentCacheMaxBytes(attachmentMaxBytes)) {
@@ -283,6 +349,10 @@ function resolveConfig(ctx, config) {
283
349
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
284
350
  }
285
351
  const listenerEnabled = config.listenerEnabled ?? false;
352
+ const realtimeEnabled = config.realtimeEnabled ?? true;
353
+ if (listenerEnabled && !realtimeEnabled) {
354
+ throw new TypeError('awiki: listenerEnabled requires realtimeEnabled');
355
+ }
286
356
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
287
357
  if (!isAbsolute(listenerWorkspacePath)) {
288
358
  throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
@@ -296,10 +366,16 @@ function resolveConfig(ctx, config) {
296
366
  const messageServiceUrl = serviceUrl('messageServiceUrl', config.messageServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
297
367
  const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
298
368
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
369
+ const legacyTenantSlot = config.legacyTenantSlot?.trim();
370
+ if (legacyTenantSlot !== undefined
371
+ && legacyTenantSlot !== 'primary'
372
+ && legacyTenantSlot !== 'secondary') {
373
+ throw new TypeError('awiki: legacyTenantSlot must be primary or secondary');
374
+ }
299
375
  return {
300
- tenantDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
301
376
  userServiceUrl,
302
377
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
378
+ ...legacyTenantSlot === undefined ? {} : { legacyTenantSlot },
303
379
  messageServiceUrl,
304
380
  mailServiceUrl,
305
381
  messageServicePublicUrl,
@@ -308,12 +384,9 @@ function resolveConfig(ctx, config) {
308
384
  allowInsecureLoopbackForTesting,
309
385
  stateRoot,
310
386
  attachmentMaxBytes,
311
- mailAttachmentMaxCount,
312
- mailAttachmentMaxBytes,
313
- mailAttachmentTotalMaxBytes,
314
387
  imageAttachmentCacheMaxBytes,
315
388
  pollIntervalMs,
316
- ...profileName === undefined ? {} : { profileName, legacySharedStateDetected },
389
+ realtimeEnabled,
317
390
  listenerEnabled,
318
391
  listener: {
319
392
  allowedPeers,
@@ -321,48 +394,21 @@ function resolveConfig(ctx, config) {
321
394
  stateRoot,
322
395
  },
323
396
  summaryMaxInputBytes,
324
- deriveUserServiceUrl: config.userServiceUrl === undefined,
325
- deriveMessageServiceUrl: config.messageServiceUrl === undefined,
326
- deriveMailServiceUrl: config.mailServiceUrl === undefined,
327
- deriveMessageServicePublicUrl: config.messageServicePublicUrl === undefined,
328
- deriveMessageServiceDid: config.messageServiceDid === undefined,
329
- deriveAllowedAttachmentOrigins: config.allowedAttachmentOrigins === undefined
330
- || config.allowedAttachmentOrigins.length === 0,
331
- };
332
- }
333
- /** Resolve every tenant-sensitive endpoint and state path from one selected domain. */
334
- function activeTenantConfig(config, domain) {
335
- const tenantOrigin = `https://${domain}`;
336
- const userServiceUrl = config.deriveUserServiceUrl ? tenantOrigin : config.userServiceUrl;
337
- const messageServiceUrl = config.deriveMessageServiceUrl ? tenantOrigin : config.messageServiceUrl;
338
- const mailServiceUrl = config.deriveMailServiceUrl ? userServiceUrl : config.mailServiceUrl;
339
- const messageServicePublicUrl = config.deriveMessageServicePublicUrl
340
- ? tenantOrigin
341
- : config.messageServicePublicUrl;
342
- const messageServiceDid = config.deriveMessageServiceDid
343
- ? `did:wba:${domain}`
344
- : config.messageServiceDid;
345
- const allowedAttachmentOrigins = config.deriveAllowedAttachmentOrigins
346
- ? attachmentOrigins(undefined, messageServicePublicUrl, config.allowInsecureLoopbackForTesting)
347
- : config.allowedAttachmentOrigins;
348
- return {
349
- domain,
350
- userServiceUrl,
351
- userServiceDomain: domain,
352
- messageServiceUrl,
353
- mailServiceUrl,
354
- messageServicePublicUrl,
355
- messageServiceDid,
356
- allowedAttachmentOrigins,
357
- attachmentMaxBytes: config.attachmentMaxBytes,
358
- allowInsecureLoopbackForTesting: config.allowInsecureLoopbackForTesting,
359
- stateRoot: resolveAwikiTenantStateRoot(config.stateRoot, domain, config.userServiceDomain),
360
397
  };
361
398
  }
362
399
  /** Return a public, fixed-message failure without retaining a thrown value. */
363
400
  function failure(code) {
364
401
  return { code, message: FAILURE_MESSAGES[code] };
365
402
  }
403
+ function integrationUnavailable() {
404
+ return {
405
+ ok: false,
406
+ error: {
407
+ code: 'unavailable',
408
+ message: 'Guest messaging is not available for the active AWiki tenant.',
409
+ },
410
+ };
411
+ }
366
412
  /** Normalize SDK and provider failures without returning remote bodies, credentials, or causes. */
367
413
  function normalizeFailure(error) {
368
414
  if (error instanceof ProviderUnavailableError) {
@@ -428,38 +474,6 @@ function normalizeGroupDid(value) {
428
474
  return undefined;
429
475
  return value;
430
476
  }
431
- /** Rebuild the browser-safe recovery DTO instead of trusting provider object shape. */
432
- function publicGroupRebindRecoverySummary(value) {
433
- if (typeof value !== 'object' || value === null)
434
- return undefined;
435
- const candidate = value;
436
- const counts = [candidate.processed, candidate.completed, candidate.pending, candidate.blocked];
437
- if (counts.some(count => !Number.isSafeInteger(count) || count < 0 || count > 0xffff_ffff)) {
438
- return undefined;
439
- }
440
- if (!Array.isArray(candidate.items) || candidate.items.length > 500)
441
- return undefined;
442
- const seen = new Set();
443
- const items = [];
444
- for (const value of candidate.items) {
445
- if (typeof value !== 'object' || value === null)
446
- return undefined;
447
- const item = value;
448
- const groupDid = normalizeGroupDid(item.groupDid);
449
- if (groupDid === undefined || seen.has(groupDid) || (item.status !== 'pending' && item.status !== 'blocked')) {
450
- return undefined;
451
- }
452
- seen.add(groupDid);
453
- items.push({ groupDid, status: item.status });
454
- }
455
- return {
456
- processed: candidate.processed,
457
- completed: candidate.completed,
458
- pending: candidate.pending,
459
- blocked: candidate.blocked,
460
- items,
461
- };
462
- }
463
477
  function normalizeProfileRequest(request) {
464
478
  if (typeof request?.displayName !== 'string' || typeof request.bio !== 'string' || !Array.isArray(request.tags))
465
479
  return undefined;
@@ -510,6 +524,14 @@ function normalizeRecoveryPrepareRequest(request) {
510
524
  return { ...operation, phone, otp };
511
525
  }
512
526
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
527
+ const DID_URI = /^did:[a-z0-9]+:(?:[A-Za-z0-9._-]|%[0-9A-Fa-f]{2})+(?::(?:[A-Za-z0-9._-]|%[0-9A-Fa-f]{2})+)*$/u;
528
+ const DID_URI_MAX_CHARACTERS = 2_048;
529
+ const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
530
+ function isBoundedDidUri(value) {
531
+ return typeof value === 'string'
532
+ && value.length <= DID_URI_MAX_CHARACTERS
533
+ && DID_URI.test(value);
534
+ }
513
535
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
514
536
  async function readBoundedResponseText(response, maxBytes) {
515
537
  const declaredLength = response.headers.get('content-length');
@@ -559,7 +581,7 @@ function identityAccessTarget(request, configuredDomain) {
559
581
  const dot = handle.indexOf('.');
560
582
  const localPart = (dot < 0 ? handle : handle.slice(0, dot)).trim();
561
583
  const domain = (dot < 0 ? configuredDomain : handle.slice(dot + 1)).trim().replace(/\.$/u, '');
562
- if (localPart.length === 0 || domain !== configuredDomain)
584
+ if (registrationHandleLocalPart(localPart) !== localPart || domain !== configuredDomain)
563
585
  return undefined;
564
586
  return { localPart, fullHandle: `${localPart}.${configuredDomain}` };
565
587
  }
@@ -716,7 +738,7 @@ function decodeAttachment(bytesBase64, maxBytes) {
716
738
  const maxEncoded = Math.ceil(maxBytes / 3) * 4;
717
739
  if (bytesBase64.length > maxEncoded)
718
740
  return { ok: false, error: failure('attachment-too-large') };
719
- if (bytesBase64.length % 4 !== 0 || !standardBase64Syntax(bytesBase64)) {
741
+ if (bytesBase64.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(bytesBase64)) {
720
742
  return { ok: false, error: failure('invalid-request') };
721
743
  }
722
744
  const bytes = Uint8Array.from(Buffer.from(bytesBase64, 'base64'));
@@ -731,6 +753,12 @@ let AwikiService = (() => {
731
753
  let _classSuper = TypertRemoteService;
732
754
  let _instanceExtraInitializers = [];
733
755
  let _getConfig_decorators;
756
+ let _getIntegration_decorators;
757
+ let _createIntegration_decorators;
758
+ let _updateIntegration_decorators;
759
+ let _rotateIntegrationId_decorators;
760
+ let _closeIntegration_decorators;
761
+ let _reopenIntegration_decorators;
734
762
  let _getIdentity_decorators;
735
763
  let _getSession_decorators;
736
764
  let _logout_decorators;
@@ -738,6 +766,18 @@ let AwikiService = (() => {
738
766
  let _inspectIdentityAccess_decorators;
739
767
  let _sendRegistrationOtp_decorators;
740
768
  let _registerIdentity_decorators;
769
+ let _retireDeviceIdentityForRejoin_decorators;
770
+ let _beginDeviceJoin_decorators;
771
+ let _getIdentityAccessState_decorators;
772
+ let _getDeviceJoinStatus_decorators;
773
+ let _cancelDeviceJoin_decorators;
774
+ let _refreshDeviceManagement_decorators;
775
+ let _startDeviceJoinVerification_decorators;
776
+ let _approveDeviceJoin_decorators;
777
+ let _rejectDeviceJoin_decorators;
778
+ let _revokeDevice_decorators;
779
+ let _prepareRootTransfer_decorators;
780
+ let _confirmRootTransfer_decorators;
741
781
  let _updateDisplayName_decorators;
742
782
  let _getProfile_decorators;
743
783
  let _updateProfile_decorators;
@@ -746,16 +786,17 @@ let AwikiService = (() => {
746
786
  let _activateRecovery_decorators;
747
787
  let _getRecoveryStatus_decorators;
748
788
  let _resumeRecovery_decorators;
789
+ let _enterRecoveredSession_decorators;
749
790
  let _discardRecovery_decorators;
750
791
  let _resolvePeer_decorators;
751
792
  let _createGroup_decorators;
752
793
  let _getGroup_decorators;
753
794
  let _joinGroup_decorators;
754
795
  let _leaveGroup_decorators;
796
+ let _getDisplayProfiles_decorators;
755
797
  let _listGroupMembers_decorators;
756
798
  let _addGroupMember_decorators;
757
799
  let _removeGroupMember_decorators;
758
- let _resumeGroupRebindRecovery_decorators;
759
800
  let _getConversationPreferences_decorators;
760
801
  let _updateConversationPreference_decorators;
761
802
  let _listConversations_decorators;
@@ -771,12 +812,17 @@ let AwikiService = (() => {
771
812
  let _readMail_decorators;
772
813
  let _markMailRead_decorators;
773
814
  let _sendMail_decorators;
774
- let _downloadMailAttachment_decorators;
775
815
  let _clearLocalData_decorators;
776
816
  return class AwikiService extends _classSuper {
777
817
  static {
778
818
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
779
819
  _getConfig_decorators = [Remote];
820
+ _getIntegration_decorators = [Remote];
821
+ _createIntegration_decorators = [Remote];
822
+ _updateIntegration_decorators = [Remote];
823
+ _rotateIntegrationId_decorators = [Remote];
824
+ _closeIntegration_decorators = [Remote];
825
+ _reopenIntegration_decorators = [Remote];
780
826
  _getIdentity_decorators = [Remote];
781
827
  _getSession_decorators = [Remote];
782
828
  _logout_decorators = [Remote];
@@ -784,6 +830,18 @@ let AwikiService = (() => {
784
830
  _inspectIdentityAccess_decorators = [Remote];
785
831
  _sendRegistrationOtp_decorators = [Remote];
786
832
  _registerIdentity_decorators = [Remote];
833
+ _retireDeviceIdentityForRejoin_decorators = [Remote];
834
+ _beginDeviceJoin_decorators = [Remote];
835
+ _getIdentityAccessState_decorators = [Remote];
836
+ _getDeviceJoinStatus_decorators = [Remote];
837
+ _cancelDeviceJoin_decorators = [Remote];
838
+ _refreshDeviceManagement_decorators = [Remote];
839
+ _startDeviceJoinVerification_decorators = [Remote];
840
+ _approveDeviceJoin_decorators = [Remote];
841
+ _rejectDeviceJoin_decorators = [Remote];
842
+ _revokeDevice_decorators = [Remote];
843
+ _prepareRootTransfer_decorators = [Remote];
844
+ _confirmRootTransfer_decorators = [Remote];
787
845
  _updateDisplayName_decorators = [Remote];
788
846
  _getProfile_decorators = [Remote];
789
847
  _updateProfile_decorators = [Remote];
@@ -792,16 +850,17 @@ let AwikiService = (() => {
792
850
  _activateRecovery_decorators = [Remote];
793
851
  _getRecoveryStatus_decorators = [Remote];
794
852
  _resumeRecovery_decorators = [Remote];
853
+ _enterRecoveredSession_decorators = [Remote];
795
854
  _discardRecovery_decorators = [Remote];
796
855
  _resolvePeer_decorators = [Remote];
797
856
  _createGroup_decorators = [Remote];
798
857
  _getGroup_decorators = [Remote];
799
858
  _joinGroup_decorators = [Remote];
800
859
  _leaveGroup_decorators = [Remote];
860
+ _getDisplayProfiles_decorators = [Remote];
801
861
  _listGroupMembers_decorators = [Remote];
802
862
  _addGroupMember_decorators = [Remote];
803
863
  _removeGroupMember_decorators = [Remote];
804
- _resumeGroupRebindRecovery_decorators = [Remote];
805
864
  _getConversationPreferences_decorators = [Remote];
806
865
  _updateConversationPreference_decorators = [Remote];
807
866
  _listConversations_decorators = [Remote];
@@ -817,9 +876,14 @@ let AwikiService = (() => {
817
876
  _readMail_decorators = [Remote];
818
877
  _markMailRead_decorators = [Remote];
819
878
  _sendMail_decorators = [Remote];
820
- _downloadMailAttachment_decorators = [Remote];
821
879
  _clearLocalData_decorators = [Remote];
822
880
  __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);
881
+ __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);
882
+ __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);
883
+ __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);
884
+ __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);
885
+ __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);
886
+ __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);
823
887
  __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);
824
888
  __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);
825
889
  __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);
@@ -827,6 +891,18 @@ let AwikiService = (() => {
827
891
  __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);
828
892
  __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);
829
893
  __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);
894
+ __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);
895
+ __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);
896
+ __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);
897
+ __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);
898
+ __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);
899
+ __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);
900
+ __esDecorate(this, null, _startDeviceJoinVerification_decorators, { kind: "method", name: "startDeviceJoinVerification", static: false, private: false, access: { has: obj => "startDeviceJoinVerification" in obj, get: obj => obj.startDeviceJoinVerification }, metadata: _metadata }, null, _instanceExtraInitializers);
901
+ __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);
902
+ __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);
903
+ __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);
904
+ __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);
905
+ __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);
830
906
  __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);
831
907
  __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);
832
908
  __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);
@@ -835,16 +911,17 @@ let AwikiService = (() => {
835
911
  __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);
836
912
  __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);
837
913
  __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);
914
+ __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);
838
915
  __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);
839
916
  __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);
840
917
  __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);
841
918
  __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);
842
919
  __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);
843
920
  __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);
921
+ __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);
844
922
  __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);
845
923
  __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);
846
924
  __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);
847
- __esDecorate(this, null, _resumeGroupRebindRecovery_decorators, { kind: "method", name: "resumeGroupRebindRecovery", static: false, private: false, access: { has: obj => "resumeGroupRebindRecovery" in obj, get: obj => obj.resumeGroupRebindRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
848
925
  __esDecorate(this, null, _getConversationPreferences_decorators, { kind: "method", name: "getConversationPreferences", static: false, private: false, access: { has: obj => "getConversationPreferences" in obj, get: obj => obj.getConversationPreferences }, metadata: _metadata }, null, _instanceExtraInitializers);
849
926
  __esDecorate(this, null, _updateConversationPreference_decorators, { kind: "method", name: "updateConversationPreference", static: false, private: false, access: { has: obj => "updateConversationPreference" in obj, get: obj => obj.updateConversationPreference }, metadata: _metadata }, null, _instanceExtraInitializers);
850
927
  __esDecorate(this, null, _listConversations_decorators, { kind: "method", name: "listConversations", static: false, private: false, access: { has: obj => "listConversations" in obj, get: obj => obj.listConversations }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -860,28 +937,49 @@ let AwikiService = (() => {
860
937
  __esDecorate(this, null, _readMail_decorators, { kind: "method", name: "readMail", static: false, private: false, access: { has: obj => "readMail" in obj, get: obj => obj.readMail }, metadata: _metadata }, null, _instanceExtraInitializers);
861
938
  __esDecorate(this, null, _markMailRead_decorators, { kind: "method", name: "markMailRead", static: false, private: false, access: { has: obj => "markMailRead" in obj, get: obj => obj.markMailRead }, metadata: _metadata }, null, _instanceExtraInitializers);
862
939
  __esDecorate(this, null, _sendMail_decorators, { kind: "method", name: "sendMail", static: false, private: false, access: { has: obj => "sendMail" in obj, get: obj => obj.sendMail }, metadata: _metadata }, null, _instanceExtraInitializers);
863
- __esDecorate(this, null, _downloadMailAttachment_decorators, { kind: "method", name: "downloadMailAttachment", static: false, private: false, access: { has: obj => "downloadMailAttachment" in obj, get: obj => obj.downloadMailAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
864
940
  __esDecorate(this, null, _clearLocalData_decorators, { kind: "method", name: "clearLocalData", static: false, private: false, access: { has: obj => "clearLocalData" in obj, get: obj => obj.clearLocalData }, metadata: _metadata }, null, _instanceExtraInitializers);
865
941
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
866
942
  }
867
- static inject = ['tools', 'settings'];
943
+ static inject = ['tools'];
868
944
  static Config = Config;
869
945
  resolved = __runInitializers(this, _instanceExtraInitializers);
870
- tenantConfigCache;
871
- tenantStateCache;
946
+ sessionStore;
947
+ imageAttachmentCache;
948
+ conversationPreferenceStore;
872
949
  startupUserServiceDomain;
873
950
  settingsProvider;
951
+ tenantRegistry;
952
+ activeTenant;
953
+ activeClientOptions;
954
+ clientFactory;
874
955
  provider;
956
+ runtimeGeneration = 0;
957
+ tenantSwitching = false;
958
+ tenantParticipants = new Set();
959
+ activeCapabilities;
960
+ updatePolicyStatus;
961
+ updatePolicyRequest;
962
+ updatePolicyTask;
963
+ updatePolicyTaskKey;
964
+ desktopDistribution;
875
965
  signedOut;
876
966
  sessionMutation = Promise.resolve();
877
967
  sessionRevision = 0;
878
968
  activeIdentityDid;
969
+ pendingDeviceJoin;
970
+ activeDeviceJoinSessionId;
971
+ requestRefs = new Map();
972
+ requestSessions = new Map();
973
+ deviceRefs = new Map();
974
+ deviceIds = new Map();
975
+ rootTransfers = new Map();
879
976
  activeSummaryRequests = new Set();
880
977
  summaryProvider;
881
- recoveryReconciliationTarget;
882
978
  hostContext;
883
979
  /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
884
980
  externalHttpAuth;
981
+ integrationClient;
982
+ mailListClient;
885
983
  workspaceContext;
886
984
  /**
887
985
  * @param ctx - owning Host context.
@@ -893,39 +991,72 @@ let AwikiService = (() => {
893
991
  this.resolved = resolveConfig(ctx, config);
894
992
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
895
993
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
896
- const provider = ctx.settings;
897
- const settingsScope = provider.register(settingsNamespace(AWIKI_SETTINGS_NAMESPACE), AwikiSettingsSchema, {
898
- base: { domain: this.resolved.userServiceDomain },
899
- applies: 'restart',
900
- validate: validateAwikiSettings,
994
+ ctx.inject(['settings'], (settingsCtx) => {
995
+ const provider = settingsCtx.settings;
996
+ const settingsScope = settingsCtx.settings.register(AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, {
997
+ base: { domain: this.resolved.userServiceDomain },
998
+ applies: 'restart',
999
+ validate: validateAwikiSettings,
1000
+ });
1001
+ this.settingsProvider = provider;
1002
+ this.startupUserServiceDomain = settingsScope.get().domain;
1003
+ settingsCtx.effect(() => () => {
1004
+ if (this.settingsProvider === provider) {
1005
+ this.settingsProvider = undefined;
1006
+ this.startupUserServiceDomain = this.resolved.userServiceDomain;
1007
+ }
1008
+ }, 'awiki: release settings namespace');
901
1009
  });
902
- this.settingsProvider = provider;
903
- this.startupUserServiceDomain = settingsScope.get().domain;
904
- ctx.effect(() => () => {
905
- if (this.settingsProvider === provider) {
906
- this.settingsProvider = undefined;
907
- this.startupUserServiceDomain = this.resolved.userServiceDomain;
908
- }
909
- }, 'awiki: release settings namespace');
910
1010
  ctx.inject(['connection'], (connectionCtx) => {
911
- connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
1011
+ registerAwikiSettingsTransport(connectionCtx.connection, createAwikiSettingsRpcHandler(() => this.settingsProvider, {
1012
+ describe: () => this.getTenantRegistryView(),
1013
+ create: (displayName, domain) => this.createCustomTenant(displayName, domain),
1014
+ rename: (tenantId, displayName) => this.renameCustomTenant(tenantId, displayName),
1015
+ switch: tenantId => this.switchTenant(tenantId),
1016
+ archive: tenantId => this.archiveCustomTenant(tenantId),
1017
+ describeUpdate: () => this.getUpdatePolicyStatus(),
1018
+ refreshUpdate: () => this.refreshUpdatePolicy(),
1019
+ describeDesktopUpdate: () => this.getDesktopUpdate(),
1020
+ refreshDesktopUpdate: () => this.refreshDesktopUpdate(),
1021
+ }));
912
1022
  });
913
1023
  ctx.inject(['workspaceRegistry'], (workspaceCtx) => {
914
1024
  this.workspaceContext = workspaceCtx;
915
1025
  const provider = this.provider;
916
1026
  if (provider !== undefined)
917
- void this.startListener(provider);
1027
+ this.ensureAgentConsumer(provider);
918
1028
  workspaceCtx.effect(() => async () => {
919
1029
  if (this.workspaceContext !== workspaceCtx)
920
1030
  return;
921
1031
  this.workspaceContext = undefined;
922
1032
  const current = this.provider;
923
1033
  if (current !== undefined)
924
- await this.stopListener(current);
1034
+ await this.stopAgentConsumer(current);
925
1035
  }, 'awiki: release Workspace listener composition');
926
1036
  });
1037
+ ctx.inject(['desktopDistribution'], desktopCtx => {
1038
+ const service = desktopCtx.get('desktopDistribution');
1039
+ if (typeof service !== 'object' || service === null
1040
+ || !('getSnapshot' in service) || typeof service.getSnapshot !== 'function'
1041
+ || !('check' in service) || typeof service.check !== 'function')
1042
+ return;
1043
+ const candidate = service;
1044
+ if (decodeDesktopDistribution(candidate.getSnapshot()) === undefined)
1045
+ return;
1046
+ this.desktopDistribution = candidate;
1047
+ desktopCtx.effect(() => () => {
1048
+ if (this.desktopDistribution === candidate)
1049
+ this.desktopDistribution = undefined;
1050
+ });
1051
+ });
1052
+ ctx.effect(() => {
1053
+ const timer = setInterval(() => { void this.refreshUpdatePolicy().catch(() => undefined); }, 6 * 60 * 60 * 1000);
1054
+ timer.unref();
1055
+ return () => { clearInterval(timer); };
1056
+ });
927
1057
  registerAwikiTools(ctx, this);
928
1058
  ctx.effect(() => async () => {
1059
+ this.updatePolicyRequest?.abort();
929
1060
  const provider = this.provider;
930
1061
  if (provider !== undefined)
931
1062
  await this.disposeProvider(provider);
@@ -942,36 +1073,351 @@ let AwikiService = (() => {
942
1073
  * @returns asynchronous disposer for the exact registered client.
943
1074
  */
944
1075
  registerClientFactory(factory) {
945
- if (this.provider !== undefined)
1076
+ if (this.clientFactory !== undefined || this.provider !== undefined) {
946
1077
  throw new Error('awiki: a client provider is already registered');
947
- const client = factory(this.activeTenant());
1078
+ }
1079
+ this.clientFactory = factory;
1080
+ try {
1081
+ this.openTenantProvider(this.ensureTenantRegistry().active(), factory);
1082
+ }
1083
+ catch (error) {
1084
+ this.clientFactory = undefined;
1085
+ throw error;
1086
+ }
1087
+ let active = true;
1088
+ return async () => {
1089
+ if (!active)
1090
+ return;
1091
+ active = false;
1092
+ this.clientFactory = undefined;
1093
+ const current = this.provider;
1094
+ if (current !== undefined)
1095
+ await this.disposeProvider(current);
1096
+ };
1097
+ }
1098
+ ensureTenantRegistry() {
1099
+ if (this.tenantRegistry !== undefined)
1100
+ return this.tenantRegistry;
1101
+ const domain = this.startupUserServiceDomain;
1102
+ const configuredOrigin = `https://${this.resolved.userServiceDomain}`;
1103
+ const tenantOrigin = `https://${domain}`;
1104
+ const derive = (value) => value.replace(/\/$/u, '') === configuredOrigin ? tenantOrigin : value;
1105
+ const seed = {
1106
+ domain,
1107
+ configured: domain !== DEFAULT_AWIKI_DOMAIN
1108
+ || this.resolved.userServiceUrl.replace(/\/$/u, '') !== configuredOrigin
1109
+ || this.resolved.messageServiceUrl.replace(/\/$/u, '') !== configuredOrigin,
1110
+ ...this.resolved.legacyTenantSlot === undefined
1111
+ ? {}
1112
+ : { legacyTenantSlot: this.resolved.legacyTenantSlot },
1113
+ userServiceUrl: derive(this.resolved.userServiceUrl),
1114
+ messageServiceUrl: derive(this.resolved.messageServiceUrl),
1115
+ mailServiceUrl: derive(this.resolved.mailServiceUrl),
1116
+ messageServicePublicUrl: derive(this.resolved.messageServicePublicUrl),
1117
+ messageServiceDid: this.resolved.messageServiceDid === `did:wba:${this.resolved.userServiceDomain}`
1118
+ ? `did:wba:${domain}`
1119
+ : this.resolved.messageServiceDid,
1120
+ };
1121
+ this.tenantRegistry = AwikiTenantRegistry.open(this.resolved.stateRoot, seed);
1122
+ return this.tenantRegistry;
1123
+ }
1124
+ optionsForTenant(tenant) {
1125
+ const endpoints = tenant.endpoints;
1126
+ const messageServicePublicOrigin = new URL(endpoints.messageServicePublicUrl).origin;
1127
+ return {
1128
+ ...endpoints,
1129
+ userServiceDomain: tenant.didHost,
1130
+ allowedAttachmentOrigins: tenant.storageLayout === 'legacy-base'
1131
+ ? this.resolved.allowedAttachmentOrigins
1132
+ : [messageServicePublicOrigin],
1133
+ attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1134
+ allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
1135
+ stateRoot: this.ensureTenantRegistry().stateRoot(tenant),
1136
+ };
1137
+ }
1138
+ ensureTenantOptions() {
1139
+ if (this.activeClientOptions !== undefined)
1140
+ return this.activeClientOptions;
1141
+ const tenant = this.ensureTenantRegistry().active();
1142
+ const options = this.optionsForTenant(tenant);
1143
+ this.bindTenantState(tenant, options);
1144
+ return options;
1145
+ }
1146
+ bindTenantState(tenant, options) {
1147
+ this.updatePolicyRequest?.abort();
1148
+ this.updatePolicyStatus = undefined;
1149
+ this.updatePolicyTask = undefined;
1150
+ this.activeTenant = tenant;
1151
+ this.activeClientOptions = options;
1152
+ this.startupUserServiceDomain = tenant.didHost;
1153
+ this.sessionStore = new AwikiSessionStore(options.stateRoot);
1154
+ this.imageAttachmentCache = new AwikiImageAttachmentCache(options.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
1155
+ this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
1156
+ this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
1157
+ this.integrationClient = undefined;
1158
+ this.activeCapabilities = {
1159
+ tenantId: tenant.tenantId,
1160
+ generation: this.runtimeGeneration,
1161
+ online: false,
1162
+ handleRecoveryPhoneEnabled: false,
1163
+ };
1164
+ this.signedOut = undefined;
1165
+ this.activeIdentityDid = undefined;
1166
+ this.pendingDeviceJoin = undefined;
1167
+ this.activeDeviceJoinSessionId = undefined;
1168
+ }
1169
+ openTenantProvider(tenant, factory, startRuntime = true) {
1170
+ const options = this.optionsForTenant(tenant);
1171
+ const client = factory(options);
948
1172
  const provider = {
949
1173
  client,
950
- listenerRestartAttempt: 0,
951
- listenerGeneration: 0,
952
- listenerRecoveryBlocked: false,
1174
+ realtimeGeneration: 0,
1175
+ agentConsumerGeneration: 0,
1176
+ localDeviceJoinRequestCountAfterSync: 0,
953
1177
  };
1178
+ this.bindTenantState(tenant, options);
954
1179
  this.provider = provider;
955
- void this.startListener(provider);
956
- return () => this.disposeProvider(provider);
1180
+ void this.refreshUpdatePolicy().catch(() => undefined);
1181
+ if (startRuntime)
1182
+ this.ensureProviderRuntime(provider);
1183
+ return provider;
1184
+ }
1185
+ /** Read the Host-owned catalog for trusted loopback settings surfaces. */
1186
+ getTenantRegistryView() {
1187
+ return this.ensureTenantRegistry().snapshot(this.tenantSwitching);
1188
+ }
1189
+ /** Browser-safe, same-process update state for Desktop and loopback settings. */
1190
+ getUpdatePolicyStatus() {
1191
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
1192
+ const modelProxyVersion = this.currentModelProxyVersion();
1193
+ return this.updatePolicyStatus ?? readAwikiUpdatePolicyStatus({
1194
+ tenant, generation: this.runtimeGeneration, stateRoot: this.resolved.stateRoot,
1195
+ currentPluginVersion: DSH_AWIKI_VERSION,
1196
+ ...modelProxyVersion === undefined ? {} : { currentModelProxyVersion: modelProxyVersion },
1197
+ });
957
1198
  }
958
- /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
959
- registerRecoveryReconciliationTarget(target) {
960
- if (this.recoveryReconciliationTarget !== undefined) {
961
- throw new Error('awiki: a recovery reconciliation target is already registered');
962
- }
963
- const registered = Object.freeze({
964
- endpoint: recoveryReconciliationEndpoint(target, this.resolved.allowInsecureLoopbackForTesting),
1199
+ /** Only the Desktop provider owns its release source; tenants cannot override it. */
1200
+ getDesktopUpdate() {
1201
+ return decodeDesktopDistribution(this.desktopDistribution?.getSnapshot());
1202
+ }
1203
+ async refreshDesktopUpdate() {
1204
+ const provider = this.desktopDistribution;
1205
+ if (provider === undefined)
1206
+ return undefined;
1207
+ const value = await provider.check();
1208
+ return provider === this.desktopDistribution ? decodeDesktopDistribution(value) : this.getDesktopUpdate();
1209
+ }
1210
+ /** Refresh only the active generation; late results from old tenants are discarded. */
1211
+ refreshUpdatePolicy() {
1212
+ const key = JSON.stringify([this.runtimeGeneration, this.currentModelProxyVersion()]);
1213
+ if (this.updatePolicyTask !== undefined && this.updatePolicyTaskKey === key)
1214
+ return this.updatePolicyTask;
1215
+ const task = this.performUpdatePolicyRefresh().finally(() => {
1216
+ if (this.updatePolicyTask === task)
1217
+ this.updatePolicyTask = undefined;
1218
+ });
1219
+ this.updatePolicyTask = task;
1220
+ this.updatePolicyTaskKey = key;
1221
+ return task;
1222
+ }
1223
+ async performUpdatePolicyRefresh() {
1224
+ const tenant = this.activeTenant ?? this.ensureTenantRegistry().active();
1225
+ const generation = this.runtimeGeneration;
1226
+ this.updatePolicyRequest?.abort();
1227
+ const request = new AbortController();
1228
+ this.updatePolicyRequest = request;
1229
+ const currentModelProxyVersion = this.currentModelProxyVersion();
1230
+ const status = await checkAwikiUpdatePolicy({
1231
+ tenant,
1232
+ generation,
1233
+ stateRoot: this.resolved.stateRoot,
1234
+ currentPluginVersion: DSH_AWIKI_VERSION,
1235
+ ...currentModelProxyVersion === undefined ? {} : { currentModelProxyVersion },
1236
+ allowInsecureLoopback: this.resolved.allowInsecureLoopbackForTesting,
1237
+ signal: request.signal,
965
1238
  });
966
- this.recoveryReconciliationTarget = registered;
1239
+ if (this.updatePolicyRequest === request
1240
+ && this.runtimeGeneration === generation
1241
+ && this.activeTenant?.tenantId === tenant.tenantId) {
1242
+ const wasRestricted = this.getUpdatePolicyStatus().restricted;
1243
+ this.updatePolicyStatus = status;
1244
+ this.hostContext.emit('awiki/update-policy', status);
1245
+ if (status.restricted) {
1246
+ this.invalidateSummaries();
1247
+ const provider = this.provider;
1248
+ if (provider !== undefined) {
1249
+ try {
1250
+ await this.stopProviderRuntime(provider);
1251
+ }
1252
+ catch (error) {
1253
+ this.ctx.logger('awiki-update').warn('AWiki restricted-version runtime shutdown failed: %s', error instanceof Error ? error.message : 'unknown failure');
1254
+ }
1255
+ }
1256
+ }
1257
+ else if (wasRestricted && this.provider !== undefined && !this.tenantSwitching) {
1258
+ this.ensureProviderRuntime(this.provider);
1259
+ }
1260
+ }
1261
+ return this.getUpdatePolicyStatus();
1262
+ }
1263
+ currentModelProxyVersion() {
1264
+ for (const participant of this.tenantParticipants) {
1265
+ if (participant.component?.product === 'dsh-awiki-model-proxy')
1266
+ return participant.component.version;
1267
+ }
1268
+ return undefined;
1269
+ }
1270
+ createCustomTenant(displayName, domain) {
1271
+ this.assertVersionSupported();
1272
+ this.ensureTenantRegistry().createCustom(displayName, domain);
1273
+ return this.getTenantRegistryView();
1274
+ }
1275
+ renameCustomTenant(tenantId, displayName) {
1276
+ this.assertVersionSupported();
1277
+ this.ensureTenantRegistry().renameCustom(tenantId, displayName);
1278
+ return this.getTenantRegistryView();
1279
+ }
1280
+ archiveCustomTenant(tenantId) {
1281
+ this.assertVersionSupported();
1282
+ this.ensureTenantRegistry().archiveCustom(tenantId);
1283
+ return this.getTenantRegistryView();
1284
+ }
1285
+ /** Register one same-process resource owner in every transactional tenant change. */
1286
+ registerTenantLifecycleParticipant(participant) {
1287
+ if (this.tenantParticipants.has(participant))
1288
+ throw new Error('awiki: tenant lifecycle participant is already registered');
1289
+ this.tenantParticipants.add(participant);
1290
+ if (participant.component !== undefined)
1291
+ this.updatePolicyStatus = undefined;
1292
+ void this.refreshUpdatePolicy().catch(() => undefined);
967
1293
  let active = true;
968
1294
  return () => {
969
1295
  if (!active)
970
1296
  return;
971
1297
  active = false;
972
- if (this.recoveryReconciliationTarget === registered) {
973
- this.recoveryReconciliationTarget = undefined;
1298
+ this.tenantParticipants.delete(participant);
1299
+ if (participant.component !== undefined)
1300
+ this.updatePolicyStatus = undefined;
1301
+ void this.refreshUpdatePolicy().catch(() => undefined);
1302
+ };
1303
+ }
1304
+ /** Replace the complete Core runtime and commit the active tenant only after opening succeeds. */
1305
+ async switchTenant(tenantId) {
1306
+ if (this.tenantSwitching)
1307
+ throw new Error('awiki: a tenant switch is already in progress');
1308
+ const registry = this.ensureTenantRegistry();
1309
+ const previousTenant = registry.active();
1310
+ if (previousTenant.tenantId === tenantId)
1311
+ return this.getTenantRegistryView();
1312
+ const target = registry.find(tenantId);
1313
+ if (target === undefined || target.lifecycle === 'archived')
1314
+ throw new Error('awiki: target tenant is unavailable');
1315
+ const factory = this.clientFactory;
1316
+ const previousProvider = this.provider;
1317
+ if (factory === undefined || previousProvider === undefined)
1318
+ throw new Error('awiki: client provider is unavailable');
1319
+ this.tenantSwitching = true;
1320
+ this.updatePolicyRequest?.abort();
1321
+ this.updatePolicyTask = undefined;
1322
+ const context = {
1323
+ from: previousTenant,
1324
+ to: target,
1325
+ generation: ++this.runtimeGeneration,
1326
+ };
1327
+ const prepared = [];
1328
+ let targetOpened = false;
1329
+ let committed = false;
1330
+ try {
1331
+ await this.sessionMutation;
1332
+ this.invalidateSummaries();
1333
+ for (const participant of this.tenantParticipants) {
1334
+ await participant.prepareSwitch(context);
1335
+ prepared.push(participant);
974
1336
  }
1337
+ await this.disposeProvider(previousProvider);
1338
+ const targetProvider = this.openTenantProvider(target, factory, false);
1339
+ targetOpened = true;
1340
+ // The factory returns an adapter immediately; its native Core opens asynchronously.
1341
+ // A local identity read proves both open and Registry loading before committing the switch.
1342
+ await targetProvider.client.getIdentity();
1343
+ await Promise.all([this.discoverTenantCapabilities(3_000), this.refreshUpdatePolicy()]);
1344
+ registry.commitActive(target.tenantId);
1345
+ committed = true;
1346
+ for (const participant of prepared)
1347
+ await participant.commitSwitch?.(context);
1348
+ this.tenantSwitching = false;
1349
+ this.ensureProviderRuntime(targetProvider);
1350
+ const view = this.getTenantRegistryView();
1351
+ this.hostContext.emit('awiki/tenant', view);
1352
+ return view;
1353
+ }
1354
+ catch (error) {
1355
+ const rollbackFailures = [];
1356
+ if (targetOpened) {
1357
+ const current = this.provider;
1358
+ if (current !== undefined) {
1359
+ try {
1360
+ await this.disposeProvider(current);
1361
+ }
1362
+ catch (rollbackError) {
1363
+ rollbackFailures.push(rollbackError);
1364
+ }
1365
+ }
1366
+ }
1367
+ if (committed) {
1368
+ try {
1369
+ registry.commitActive(previousTenant.tenantId);
1370
+ }
1371
+ catch (rollbackError) {
1372
+ rollbackFailures.push(rollbackError);
1373
+ }
1374
+ }
1375
+ if (this.provider === undefined) {
1376
+ try {
1377
+ const restored = this.openTenantProvider(previousTenant, factory, false);
1378
+ await restored.client.getIdentity();
1379
+ this.ensureProviderRuntime(restored);
1380
+ }
1381
+ catch (rollbackError) {
1382
+ rollbackFailures.push(rollbackError);
1383
+ }
1384
+ }
1385
+ for (const participant of prepared.reverse()) {
1386
+ try {
1387
+ await participant.rollbackSwitch?.(context);
1388
+ }
1389
+ catch (rollbackError) {
1390
+ rollbackFailures.push(rollbackError);
1391
+ }
1392
+ }
1393
+ if (rollbackFailures.length > 0) {
1394
+ throw new AggregateError([error, ...rollbackFailures], 'awiki: tenant switch failed and rollback was incomplete');
1395
+ }
1396
+ this.tenantSwitching = false;
1397
+ if (this.provider !== undefined)
1398
+ this.ensureProviderRuntime(this.provider);
1399
+ throw new Error('awiki: tenant switch failed; the previous tenant was restored', { cause: error });
1400
+ }
1401
+ finally {
1402
+ this.tenantSwitching = false;
1403
+ }
1404
+ }
1405
+ /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
1406
+ getRealtimeDiagnostics() {
1407
+ const provider = this.provider;
1408
+ const realtime = provider?.realtimeSupervisor?.diagnostics() ?? {
1409
+ connected: false,
1410
+ activeSessionCount: 0,
1411
+ startCount: 0,
1412
+ stopCount: 0,
1413
+ maxActiveSessionCount: 0,
1414
+ generation: 0,
1415
+ retryCount: 0,
1416
+ lifecyclePhase: 'idle',
1417
+ };
1418
+ return {
1419
+ ...realtime,
1420
+ localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
975
1421
  };
976
1422
  }
977
1423
  /** Register one replaceable conversation-summary provider for this deployment. */
@@ -992,58 +1438,145 @@ let AwikiService = (() => {
992
1438
  * Read settings needed by the browser presentation.
993
1439
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
994
1440
  */
995
- getConfig() {
996
- return Promise.resolve({
1441
+ async getConfig() {
1442
+ this.ensureTenantOptions();
1443
+ const capabilities = await this.discoverTenantCapabilities(10_000);
1444
+ if (this.tenantSwitching
1445
+ || capabilities.generation !== this.runtimeGeneration
1446
+ || capabilities.tenantId !== this.activeTenant?.tenantId) {
1447
+ return { ok: false, error: failure('conflict') };
1448
+ }
1449
+ return {
997
1450
  ok: true,
998
1451
  value: {
999
- tenantDomain: this.activeTenant().domain,
1000
1452
  pollIntervalMs: this.resolved.pollIntervalMs,
1001
1453
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1002
- mailAttachmentMaxCount: this.resolved.mailAttachmentMaxCount,
1003
- mailAttachmentMaxBytes: this.resolved.mailAttachmentMaxBytes,
1004
- mailAttachmentTotalMaxBytes: this.resolved.mailAttachmentTotalMaxBytes,
1005
- ...this.resolved.profileName === undefined ? {} : {
1006
- profileName: this.resolved.profileName,
1007
- legacySharedStateDetected: this.resolved.legacySharedStateDetected,
1454
+ handleRecoveryPhoneEnabled: capabilities.handleRecoveryPhoneEnabled,
1455
+ ...capabilities.guestGatewayBaseUrl === undefined
1456
+ ? {}
1457
+ : { integrationGuideUrl: new URL('/guest/guide/integration', capabilities.guestGatewayBaseUrl).toString() },
1458
+ tenantId: capabilities.tenantId,
1459
+ tenantGeneration: capabilities.generation,
1460
+ tenantOnline: capabilities.online,
1461
+ services: {
1462
+ modelProxy: { enabled: capabilities.modelProxyBaseUrl !== undefined },
1463
+ guestGateway: { enabled: capabilities.guestGatewayBaseUrl !== undefined },
1008
1464
  },
1009
1465
  },
1010
- });
1011
- }
1012
- activeTenant() {
1013
- const cached = this.tenantConfigCache;
1014
- if (cached !== undefined)
1015
- return cached;
1016
- const value = Object.freeze(activeTenantConfig(this.resolved, this.startupUserServiceDomain));
1017
- this.tenantConfigCache = value;
1018
- return value;
1019
- }
1020
- activeTenantState() {
1021
- const cached = this.tenantStateCache;
1022
- if (cached !== undefined)
1023
- return cached;
1024
- const tenant = this.activeTenant();
1025
- const value = Object.freeze({
1026
- domain: tenant.domain,
1027
- stateRoot: tenant.stateRoot,
1028
- sessionStore: new AwikiSessionStore(tenant.stateRoot),
1029
- imageAttachmentCache: new AwikiImageAttachmentCache(tenant.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes),
1030
- sentMailStore: new AwikiSentMailStore(tenant.stateRoot),
1031
- conversationPreferenceStore: new AwikiConversationPreferenceStore(tenant.stateRoot),
1032
- });
1033
- this.tenantStateCache = value;
1034
- return value;
1035
- }
1036
- get sessionStore() {
1037
- return this.activeTenantState().sessionStore;
1038
- }
1039
- get imageAttachmentCache() {
1040
- return this.activeTenantState().imageAttachmentCache;
1466
+ };
1041
1467
  }
1042
- get sentMailStore() {
1043
- return this.activeTenantState().sentMailStore;
1468
+ /** Trusted same-process capability binding for optional tenant participants. */
1469
+ getTenantCapabilities() {
1470
+ const capabilities = this.activeCapabilities;
1471
+ if (capabilities === undefined)
1472
+ throw new Error('awiki: tenant capabilities are unavailable');
1473
+ return { ...capabilities };
1474
+ }
1475
+ /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
1476
+ refreshTenantCapabilities() {
1477
+ this.ensureTenantOptions();
1478
+ return this.discoverTenantCapabilities(10_000);
1479
+ }
1480
+ async discoverTenantCapabilities(timeoutMs) {
1481
+ const options = this.activeClientOptions;
1482
+ const tenant = this.activeTenant;
1483
+ if (options === undefined || tenant === undefined)
1484
+ throw new ProviderUnavailableError();
1485
+ const generation = this.runtimeGeneration;
1486
+ const fallback = {
1487
+ tenantId: tenant.tenantId,
1488
+ generation,
1489
+ online: false,
1490
+ handleRecoveryPhoneEnabled: false,
1491
+ };
1492
+ const publish = (capabilities) => {
1493
+ if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
1494
+ this.activeCapabilities = capabilities;
1495
+ this.integrationClient = capabilities.guestGatewayBaseUrl === undefined
1496
+ ? undefined
1497
+ : new AwikiIntegrationClient(capabilities.guestGatewayBaseUrl, this.externalHttpAuth);
1498
+ }
1499
+ return capabilities;
1500
+ };
1501
+ const abort = new AbortController();
1502
+ const timeout = setTimeout(() => { abort.abort(); }, timeoutMs);
1503
+ try {
1504
+ const endpoint = new URL('/user-service/v1/server-info', options.userServiceUrl);
1505
+ endpoint.searchParams.set('client_platform', 'dsh');
1506
+ const response = await fetch(endpoint, {
1507
+ method: 'GET',
1508
+ headers: { accept: 'application/json', 'cache-control': 'no-store' },
1509
+ cache: 'no-store',
1510
+ redirect: 'error',
1511
+ signal: abort.signal,
1512
+ });
1513
+ if (!response.ok)
1514
+ return publish(fallback);
1515
+ const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1516
+ const value = text === undefined ? undefined : JSON.parse(text);
1517
+ if (typeof value !== 'object' || value === null || value.schema_version !== 1)
1518
+ return publish(fallback);
1519
+ const methods = value.identity?.handle_recovery?.methods;
1520
+ const handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1521
+ && method.id === 'phone'
1522
+ && method.enabled === true
1523
+ && method.verification?.required === true
1524
+ && method.verification?.type === 'sms_otp'));
1525
+ const services = value.services;
1526
+ const modelProxyBaseUrl = publishedServiceBaseUrl(services, 'model_proxy', this.resolved.allowInsecureLoopbackForTesting);
1527
+ const guestGatewayBaseUrl = publishedServiceBaseUrl(services, 'guest_gateway', false);
1528
+ const capabilities = {
1529
+ tenantId: tenant.tenantId,
1530
+ generation,
1531
+ online: true,
1532
+ handleRecoveryPhoneEnabled,
1533
+ ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1534
+ ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1535
+ };
1536
+ return publish(capabilities);
1537
+ }
1538
+ catch {
1539
+ return publish(fallback);
1540
+ }
1541
+ finally {
1542
+ clearTimeout(timeout);
1543
+ }
1044
1544
  }
1045
- get conversationPreferenceStore() {
1046
- return this.activeTenantState().conversationPreferenceStore;
1545
+ /** Read the Integration owned by the active full Handle through the fixed Host client. */
1546
+ getIntegration() {
1547
+ if (this.getUpdatePolicyStatus().restricted)
1548
+ return Promise.resolve(integrationUnavailable());
1549
+ return this.integrationClient?.read() ?? Promise.resolve(integrationUnavailable());
1550
+ }
1551
+ /** Create the active full Handle's only Integration. */
1552
+ createIntegration(request) {
1553
+ if (this.getUpdatePolicyStatus().restricted)
1554
+ return Promise.resolve(integrationUnavailable());
1555
+ return this.integrationClient?.create(request) ?? Promise.resolve(integrationUnavailable());
1556
+ }
1557
+ /** Update editable Integration fields with optimistic concurrency. */
1558
+ updateIntegration(request) {
1559
+ if (this.getUpdatePolicyStatus().restricted)
1560
+ return Promise.resolve(integrationUnavailable());
1561
+ return this.integrationClient?.update(request) ?? Promise.resolve(integrationUnavailable());
1562
+ }
1563
+ /** Atomically replace the current public Integration id. */
1564
+ rotateIntegrationId(request) {
1565
+ if (this.getUpdatePolicyStatus().restricted)
1566
+ return Promise.resolve(integrationUnavailable());
1567
+ return this.integrationClient?.rotate(request) ?? Promise.resolve(integrationUnavailable());
1568
+ }
1569
+ /** Close the Integration and revoke its current public id. */
1570
+ closeIntegration(request) {
1571
+ if (this.getUpdatePolicyStatus().restricted)
1572
+ return Promise.resolve(integrationUnavailable());
1573
+ return this.integrationClient?.close(request) ?? Promise.resolve(integrationUnavailable());
1574
+ }
1575
+ /** Revalidate one closed Integration and issue a new public id. */
1576
+ reopenIntegration(request) {
1577
+ if (this.getUpdatePolicyStatus().restricted)
1578
+ return Promise.resolve(integrationUnavailable());
1579
+ return this.integrationClient?.reopen(request) ?? Promise.resolve(integrationUnavailable());
1047
1580
  }
1048
1581
  /**
1049
1582
  * Read the deployment's identity status.
@@ -1065,6 +1598,9 @@ let AwikiService = (() => {
1065
1598
  if (!identity.ok)
1066
1599
  return identity;
1067
1600
  this.activeIdentityDid = identity.value?.did;
1601
+ const provider = this.provider;
1602
+ if (identity.value !== null && provider !== undefined)
1603
+ this.ensureProviderRuntime(provider);
1068
1604
  return identity.value === null
1069
1605
  ? { ok: true, value: { status: 'unregistered' } }
1070
1606
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -1074,6 +1610,8 @@ let AwikiService = (() => {
1074
1610
  if (request?.confirmation !== AWIKI_LOGOUT_CONFIRMATION) {
1075
1611
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1076
1612
  }
1613
+ // Fence pending post-Recovery work before the serialized sign-out reaches the head of the queue.
1614
+ this.sessionRevision += 1;
1077
1615
  return this.mutateSession(async () => {
1078
1616
  if (await this.isSignedOut())
1079
1617
  return { ok: true, value: { status: 'signed-out' } };
@@ -1091,7 +1629,7 @@ let AwikiService = (() => {
1091
1629
  this.publishSession(session);
1092
1630
  const provider = this.provider;
1093
1631
  if (provider !== undefined)
1094
- await this.stopListener(provider);
1632
+ await this.stopProviderRuntime(provider);
1095
1633
  return { ok: true, value: session };
1096
1634
  }
1097
1635
  catch {
@@ -1116,7 +1654,7 @@ let AwikiService = (() => {
1116
1654
  this.publishSession(session);
1117
1655
  const provider = this.provider;
1118
1656
  if (provider !== undefined)
1119
- void this.startListener(provider);
1657
+ this.ensureProviderRuntime(provider);
1120
1658
  return { ok: true, value: session };
1121
1659
  }
1122
1660
  catch {
@@ -1126,10 +1664,13 @@ let AwikiService = (() => {
1126
1664
  }
1127
1665
  /** Classify one configured-domain Handle before selecting the registration or recovery OTP purpose. */
1128
1666
  async inspectIdentityAccess(request) {
1129
- const target = identityAccessTarget(request, this.startupUserServiceDomain);
1667
+ const options = this.activeClientOptions;
1668
+ if (options === undefined)
1669
+ return { ok: false, error: normalizeFailure(new ProviderUnavailableError()) };
1670
+ const target = identityAccessTarget(request, options.userServiceDomain);
1130
1671
  if (target === undefined)
1131
1672
  return { ok: false, error: failure('invalid-request') };
1132
- const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.activeTenant().userServiceUrl);
1673
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, options.userServiceUrl);
1133
1674
  const abort = new AbortController();
1134
1675
  const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1135
1676
  let response;
@@ -1151,7 +1692,7 @@ let AwikiService = (() => {
1151
1692
  if (response.status === 404) {
1152
1693
  return { ok: true, value: { status: 'available', fullHandle: target.fullHandle } };
1153
1694
  }
1154
- if (!response.ok)
1695
+ if (!response.ok && response.status !== 410)
1155
1696
  return { ok: false, error: failure('remote') };
1156
1697
  try {
1157
1698
  const text = await readBoundedResponseText(response, IDENTITY_ACCESS_RESPONSE_MAX_BYTES);
@@ -1162,10 +1703,10 @@ let AwikiService = (() => {
1162
1703
  return { ok: false, error: failure('remote') };
1163
1704
  const binding = value;
1164
1705
  if (binding.handle !== target.fullHandle
1165
- || typeof binding.did !== 'string'
1166
- || !binding.did.startsWith('did:')
1706
+ || !isBoundedDidUri(binding.did)
1167
1707
  || typeof binding.status !== 'string'
1168
- || binding.status.length === 0) {
1708
+ || binding.status.length === 0
1709
+ || (response.status === 410 && binding.status !== 'revoked')) {
1169
1710
  return { ok: false, error: failure('remote') };
1170
1711
  }
1171
1712
  return { ok: true, value: { status: 'existing', fullHandle: target.fullHandle } };
@@ -1174,8 +1715,21 @@ let AwikiService = (() => {
1174
1715
  return { ok: false, error: failure('remote') };
1175
1716
  }
1176
1717
  }
1177
- sendRegistrationOtp(request) {
1178
- return this.run(client => client.sendRegistrationOtp(request));
1718
+ async sendRegistrationOtp(request) {
1719
+ if (shortHandleInviteRequired(request.handle)) {
1720
+ const inspection = await this.inspectIdentityAccess({ handle: request.handle });
1721
+ if (!inspection.ok)
1722
+ return inspection;
1723
+ if (inspection.value.status === 'available')
1724
+ return { ok: false, error: failure('short-handle-invite-required') };
1725
+ }
1726
+ this.pendingDeviceJoin = undefined;
1727
+ return this.run(async (client) => {
1728
+ if (await this.selectDeviceJoinSession(client) !== null) {
1729
+ throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1730
+ }
1731
+ return client.sendRegistrationOtp(request);
1732
+ });
1179
1733
  }
1180
1734
  /**
1181
1735
  * Register and persist the deployment's only AWiki identity.
@@ -1183,10 +1737,313 @@ let AwikiService = (() => {
1183
1737
  * @returns The new public identity or a closed failure.
1184
1738
  */
1185
1739
  async registerIdentity(request) {
1186
- const result = await this.run(client => client.registerIdentity(request));
1187
- if (result.ok)
1188
- await this.activateRegisteredIdentity(result.value);
1189
- return result;
1740
+ if (shortHandleInviteRequired(request.handle)) {
1741
+ const inspection = await this.inspectIdentityAccess({ handle: request.handle });
1742
+ if (!inspection.ok)
1743
+ return inspection;
1744
+ if (inspection.value.status === 'available')
1745
+ return { ok: false, error: failure('short-handle-invite-required') };
1746
+ }
1747
+ this.pendingDeviceJoin = undefined;
1748
+ const result = await this.run(async (client) => {
1749
+ if (await this.selectDeviceJoinSession(client) !== null) {
1750
+ throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1751
+ }
1752
+ return client.registerIdentity(request);
1753
+ });
1754
+ if (!result.ok)
1755
+ return result;
1756
+ if (result.value.status === 'registered') {
1757
+ await this.activateRegisteredIdentity(result.value.identity);
1758
+ return { ok: true, value: { status: 'registered', identity: result.value.identity } };
1759
+ }
1760
+ this.pendingDeviceJoin = {
1761
+ continuationId: result.value.continuationId,
1762
+ fullHandle: result.value.fullHandle,
1763
+ mode: result.value.mode,
1764
+ requiresUserPresence: result.value.requiresUserPresence,
1765
+ };
1766
+ return {
1767
+ ok: true,
1768
+ value: {
1769
+ status: 'join-required',
1770
+ fullHandle: result.value.fullHandle,
1771
+ mode: result.value.mode,
1772
+ requiresUserPresence: result.value.requiresUserPresence,
1773
+ },
1774
+ };
1775
+ }
1776
+ /** Retire only this revoked device credential; ordinary local messages remain for the rejoined identity. */
1777
+ retireDeviceIdentityForRejoin() {
1778
+ return this.mutateSession(async () => {
1779
+ const provider = this.provider;
1780
+ if (provider !== undefined)
1781
+ await this.stopProviderRuntime(provider);
1782
+ const result = await this.run(client => client.retireDefaultIdentityForRejoin(), { allowSignedOut: true });
1783
+ if (!result.ok) {
1784
+ if (provider !== undefined && this.provider === provider)
1785
+ this.ensureProviderRuntime(provider);
1786
+ return result;
1787
+ }
1788
+ try {
1789
+ await this.sessionStore.signIn();
1790
+ }
1791
+ catch {
1792
+ try {
1793
+ await this.sessionStore.signIn();
1794
+ }
1795
+ catch {
1796
+ this.ctx.logger('awiki').warn('AWiki local session marker could not be refreshed after device retirement');
1797
+ }
1798
+ }
1799
+ this.signedOut = false;
1800
+ this.activeIdentityDid = undefined;
1801
+ this.pendingDeviceJoin = undefined;
1802
+ this.activeDeviceJoinSessionId = undefined;
1803
+ this.invalidateSummaries();
1804
+ this.publishSession({ status: 'unregistered' });
1805
+ return { ok: true, value: { completed: true } };
1806
+ });
1807
+ }
1808
+ /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
1809
+ async beginDeviceJoin() {
1810
+ const continuation = this.pendingDeviceJoin;
1811
+ if (continuation === undefined)
1812
+ return { ok: false, error: failure('conflict') };
1813
+ if ((continuation.mode === 'ordinary' && continuation.requiresUserPresence)
1814
+ || (continuation.mode === 'handle-recovery-rebind' && !continuation.requiresUserPresence)) {
1815
+ return { ok: false, error: failure('forbidden') };
1816
+ }
1817
+ const result = await this.run(async (client) => {
1818
+ let userPresenceConfirmed = false;
1819
+ if (continuation.mode === 'handle-recovery-rebind') {
1820
+ if (!client.trustedUserPresenceSupported) {
1821
+ throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
1822
+ }
1823
+ userPresenceConfirmed = await client.confirmUserPresence('Rejoin your recovered AWiki identity on this device');
1824
+ if (!userPresenceConfirmed) {
1825
+ throw Object.assign(new Error('user presence denied'), { name: 'AwikiSdkError', code: 'forbidden' });
1826
+ }
1827
+ }
1828
+ this.pendingDeviceJoin = undefined;
1829
+ return client.beginDeviceJoin({
1830
+ continuationId: continuation.continuationId,
1831
+ operationId: `dsh-device-join-${randomUUID()}`,
1832
+ userPresenceConfirmed,
1833
+ });
1834
+ });
1835
+ if (!result.ok)
1836
+ return result;
1837
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
1838
+ return this.applyCandidateJoinProgress(result.value);
1839
+ }
1840
+ /** Read local workflow authority before offering a new login. No remote mutations. */
1841
+ getIdentityAccessState() {
1842
+ return this.run(async (client) => {
1843
+ const joining = await this.selectDeviceJoinSession(client) !== null;
1844
+ const recoveries = await client.listPendingRecoveries();
1845
+ const domain = this.activeClientOptions?.userServiceDomain;
1846
+ const choice = this.pendingDeviceJoin;
1847
+ return {
1848
+ choice: joining || choice === undefined ? null : {
1849
+ status: 'join-required',
1850
+ fullHandle: choice.fullHandle,
1851
+ mode: choice.mode, requiresUserPresence: choice.requiresUserPresence,
1852
+ },
1853
+ joining,
1854
+ recoveries: recoveries.filter(value => domain !== undefined && value.fullHandle.slice(value.fullHandle.indexOf('.') + 1) === domain),
1855
+ };
1856
+ }, { allowSignedOut: true });
1857
+ }
1858
+ /** Restore from Core local_sessions and advance only the exact resumable Join. */
1859
+ async getDeviceJoinStatus() {
1860
+ const result = await this.run(async (client) => {
1861
+ const joinSessionId = await this.selectDeviceJoinSession(client);
1862
+ return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
1863
+ });
1864
+ if (!result.ok)
1865
+ return result;
1866
+ if (result.value === null)
1867
+ return { ok: true, value: null };
1868
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
1869
+ return this.applyCandidateJoinProgress(result.value);
1870
+ }
1871
+ async cancelDeviceJoin() {
1872
+ const result = await this.run(async (client) => {
1873
+ const joinSessionId = await this.selectDeviceJoinSession(client);
1874
+ if (joinSessionId === null)
1875
+ return null;
1876
+ return client.cancelDeviceJoin(joinSessionId);
1877
+ });
1878
+ if (!result.ok)
1879
+ return result;
1880
+ this.pendingDeviceJoin = undefined;
1881
+ this.activeDeviceJoinSessionId = undefined;
1882
+ return { ok: true, value: { completed: true } };
1883
+ }
1884
+ /** Reliable-sync and project only Host-opaque device/request references. */
1885
+ refreshDeviceManagement() {
1886
+ return this.run(client => this.deviceManagementSnapshot(client));
1887
+ }
1888
+ async startDeviceJoinVerification(request) {
1889
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
1890
+ if (joinSessionId === undefined)
1891
+ return { ok: false, error: failure('invalid-request') };
1892
+ const result = await this.run(async (client) => {
1893
+ await this.requireDeviceManager(client);
1894
+ await client.syncDeviceManagement();
1895
+ let notice = (await client.listLocalDeviceJoinRequests())
1896
+ .find(value => value.joinSessionId === joinSessionId);
1897
+ if (notice === undefined) {
1898
+ throw Object.assign(new Error('request not found'), { name: 'AwikiSdkError', code: 'not-found' });
1899
+ }
1900
+ const localProgress = await this.localAdminJoinProgress(client, notice);
1901
+ if (localProgress !== undefined)
1902
+ return localProgress;
1903
+ if (!notice.canStartVerification) {
1904
+ throw Object.assign(new Error('request unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1905
+ }
1906
+ const operationId = `dsh-device-verify-${createHash('sha256').update(joinSessionId).digest('hex').slice(0, 32)}`;
1907
+ let started;
1908
+ try {
1909
+ started = await client.startDeviceJoinVerification({
1910
+ joinSessionId,
1911
+ operationId,
1912
+ challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS,
1913
+ });
1914
+ }
1915
+ catch (error) {
1916
+ await client.syncDeviceManagement();
1917
+ notice = (await client.listLocalDeviceJoinRequests())
1918
+ .find(value => value.joinSessionId === joinSessionId);
1919
+ const recovered = notice === undefined ? undefined : await this.localAdminJoinProgress(client, notice);
1920
+ if (recovered !== undefined)
1921
+ return recovered;
1922
+ throw error;
1923
+ }
1924
+ await client.syncDeviceManagement();
1925
+ notice = (await client.listLocalDeviceJoinRequests())
1926
+ .find(value => value.joinSessionId === joinSessionId);
1927
+ if (notice === undefined)
1928
+ return started;
1929
+ return (await this.localAdminJoinProgress(client, notice)) ?? started;
1930
+ });
1931
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1932
+ }
1933
+ async approveDeviceJoin(request) {
1934
+ if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) {
1935
+ return { ok: false, error: failure('invalid-request') };
1936
+ }
1937
+ const joinSessionId = this.requestSessions.get(request.requestRef);
1938
+ if (joinSessionId === undefined || !/^\d{6}$/u.test(request.enteredSas)) {
1939
+ return { ok: false, error: failure('invalid-request') };
1940
+ }
1941
+ const result = await this.run(async (client) => {
1942
+ await this.requireDeviceManager(client);
1943
+ const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1944
+ if (progress.sas === undefined || !constantTimeSasMatches(progress.sas, request.enteredSas)) {
1945
+ throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
1946
+ }
1947
+ const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
1948
+ try {
1949
+ return await client.confirmDeviceJoinApproval(prompt.approvalHandle);
1950
+ }
1951
+ catch (error) {
1952
+ // Approval is a remote mutation: its response may fail after Core has
1953
+ // already committed both the provider document and the local terminal
1954
+ // Join state. Re-read the authoritative terminal state before exposing
1955
+ // a false failure to the administrator.
1956
+ try {
1957
+ await client.syncDeviceManagement();
1958
+ }
1959
+ catch { }
1960
+ try {
1961
+ const recovered = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1962
+ if (adminJoinPhase(recovered) === 'authorized')
1963
+ return recovered;
1964
+ }
1965
+ catch { }
1966
+ throw error;
1967
+ }
1968
+ });
1969
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1970
+ }
1971
+ async rejectDeviceJoin(request) {
1972
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
1973
+ if (joinSessionId === undefined
1974
+ || (request.reason !== 'user_rejected' && request.reason !== 'sas_mismatch')) {
1975
+ return { ok: false, error: failure('invalid-request') };
1976
+ }
1977
+ const result = await this.run(async (client) => {
1978
+ await this.requireDeviceManager(client);
1979
+ return client.rejectDeviceJoin(joinSessionId, request.reason);
1980
+ });
1981
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1982
+ }
1983
+ async revokeDevice(request) {
1984
+ if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) {
1985
+ return { ok: false, error: failure('invalid-request') };
1986
+ }
1987
+ const deviceId = this.deviceIds.get(request.deviceRef);
1988
+ if (deviceId === undefined)
1989
+ return { ok: false, error: failure('invalid-request') };
1990
+ return this.run(async (client) => {
1991
+ await this.requireDeviceManager(client);
1992
+ const devices = await client.getDeviceRegistry();
1993
+ const target = devices.find(device => device.deviceId === deviceId);
1994
+ if (target === undefined || target.isCurrent) {
1995
+ throw Object.assign(new Error('device unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1996
+ }
1997
+ await client.revokeDevice(deviceId);
1998
+ return this.deviceManagementSnapshot(client);
1999
+ });
2000
+ }
2001
+ /** Prepare a short-lived Core authorization without exposing it to Browser. */
2002
+ async prepareRootTransfer(request) {
2003
+ const deviceId = this.deviceIds.get(request?.deviceRef);
2004
+ if (deviceId === undefined)
2005
+ return { ok: false, error: failure('invalid-request') };
2006
+ return this.run(async (client) => {
2007
+ const did = await this.requireRootTransferRecipient(client, deviceId);
2008
+ const preparation = await client.prepareRootKeyTransfer(deviceId);
2009
+ if (preparation.recipient.did !== did || preparation.recipient.deviceId !== deviceId) {
2010
+ throw Object.assign(new Error('root transfer recipient mismatch'), { name: 'AwikiSdkError', code: 'remote' });
2011
+ }
2012
+ for (const [reference, pending] of this.rootTransfers) {
2013
+ if (pending.deviceId === deviceId)
2014
+ this.rootTransfers.delete(reference);
2015
+ }
2016
+ const transferRef = `root-transfer-${randomUUID()}`;
2017
+ this.rootTransfers.set(transferRef, {
2018
+ authorizationHandle: preparation.authorizationHandle,
2019
+ deviceId,
2020
+ deviceRef: request.deviceRef,
2021
+ did,
2022
+ });
2023
+ return { transferRef, deviceRef: request.deviceRef, expiresAt: preparation.expiresAt };
2024
+ });
2025
+ }
2026
+ /** Authenticate locally, recheck fresh context, then consume one exact Core authorization. */
2027
+ async confirmRootTransfer(request) {
2028
+ const pending = this.rootTransfers.get(request?.transferRef);
2029
+ if (pending === undefined)
2030
+ return { ok: false, error: failure('invalid-request') };
2031
+ this.rootTransfers.delete(request.transferRef);
2032
+ return this.run(async (client) => {
2033
+ const confirmed = await client.confirmUserPresence('Grant AWiki device management access');
2034
+ if (!confirmed) {
2035
+ throw Object.assign(new Error('user presence denied'), { name: 'AwikiSdkError', code: 'forbidden' });
2036
+ }
2037
+ const did = await this.requireRootTransferRecipient(client, pending.deviceId);
2038
+ if (did !== pending.did) {
2039
+ throw Object.assign(new Error('root transfer identity changed'), { name: 'AwikiSdkError', code: 'forbidden' });
2040
+ }
2041
+ const sent = await client.confirmAndSendRootKeyTransfer(pending.authorizationHandle);
2042
+ if (sent.recipientDeviceId !== pending.deviceId) {
2043
+ throw Object.assign(new Error('root transfer receipt mismatch'), { name: 'AwikiSdkError', code: 'remote' });
2044
+ }
2045
+ return { deviceRef: pending.deviceRef, acceptedAt: sent.acceptedAt };
2046
+ });
1190
2047
  }
1191
2048
  /**
1192
2049
  * Update the deployment identity's public WNS display name.
@@ -1212,10 +2069,16 @@ let AwikiService = (() => {
1212
2069
  }
1213
2070
  /** Start durable phone recovery for one existing full Handle. */
1214
2071
  sendRecoveryOtp(request) {
2072
+ this.pendingDeviceJoin = undefined;
1215
2073
  const normalized = normalizeRecoveryOtpRequest(request);
1216
2074
  if (normalized === undefined)
1217
2075
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1218
- return this.run(client => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
2076
+ return this.run(async (client) => {
2077
+ if (await this.selectDeviceJoinSession(client) !== null) {
2078
+ throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
2079
+ }
2080
+ return client.sendRecoveryOtp(normalized);
2081
+ }, { allowSignedOut: true });
1219
2082
  }
1220
2083
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
1221
2084
  prepareRecovery(request) {
@@ -1229,33 +2092,35 @@ let AwikiService = (() => {
1229
2092
  const normalized = normalizeRecoveryOperation(request);
1230
2093
  if (normalized === undefined)
1231
2094
  return { ok: false, error: failure('invalid-request') };
1232
- const result = await this.run(client => client.activateRecovery(normalized), { allowSignedOut: true });
1233
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1234
- return { ok: false, error: failure('remote') };
1235
- }
1236
- return result;
2095
+ return this.run(client => client.activateRecovery(normalized), { allowSignedOut: true });
1237
2096
  }
1238
2097
  /** Read durable recovery state before deciding whether a retry is valid. */
1239
2098
  async getRecoveryStatus(request) {
1240
2099
  const normalized = normalizeRecoveryOperation(request);
1241
2100
  if (normalized === undefined)
1242
2101
  return { ok: false, error: failure('invalid-request') };
1243
- const result = await this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
1244
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1245
- return { ok: false, error: failure('remote') };
1246
- }
1247
- return result;
2102
+ return this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
1248
2103
  }
1249
2104
  /** Resume only the exact Core-owned operation selected by the browser. */
1250
2105
  async resumeRecovery(request) {
1251
2106
  const normalized = normalizeRecoveryOperation(request);
1252
2107
  if (normalized === undefined)
1253
2108
  return { ok: false, error: failure('invalid-request') };
1254
- const result = await this.run(client => client.resumeRecovery(normalized), { allowSignedOut: true });
1255
- if (result.ok && !await this.applyRecoveredSession(result.value)) {
1256
- return { ok: false, error: failure('remote') };
2109
+ return this.run(client => client.resumeRecovery(normalized), { allowSignedOut: true });
2110
+ }
2111
+ /** Explicitly enter an applied identity; reads and late Core completions never sign in. */
2112
+ async enterRecoveredSession(request) {
2113
+ const normalized = normalizeRecoveryOperation(request);
2114
+ if (normalized === undefined)
2115
+ return { ok: false, error: failure('invalid-request') };
2116
+ const requestGeneration = this.sessionRevision;
2117
+ const result = await this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
2118
+ if (!result.ok)
2119
+ return result;
2120
+ if (result.value.phase !== 'applied' || !result.value.allowedActions?.includes('activate_identity')) {
2121
+ return { ok: false, error: failure('conflict') };
1257
2122
  }
1258
- return result;
2123
+ return this.applyRecoveredSession(result.value, requestGeneration);
1259
2124
  }
1260
2125
  /** Discard only a pre-attempt operation; Core rejects post-attempt deletion. */
1261
2126
  async discardRecovery(request) {
@@ -1322,6 +2187,12 @@ let AwikiService = (() => {
1322
2187
  return result.ok ? { ok: true, value: { completed: true } } : result;
1323
2188
  }
1324
2189
  /** Read one authoritative versioned member page. */
2190
+ getDisplayProfiles(peers) {
2191
+ if (!Array.isArray(peers) || peers.length > 100 || peers.some(peer => typeof peer !== 'string' || !peer.startsWith('did:'))) {
2192
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
2193
+ }
2194
+ return this.run(client => client.getDisplayProfiles(peers));
2195
+ }
1325
2196
  listGroupMembers(request) {
1326
2197
  const groupDid = normalizeGroupDid(request?.groupDid);
1327
2198
  if (groupDid === undefined
@@ -1353,16 +2224,6 @@ let AwikiService = (() => {
1353
2224
  }
1354
2225
  return this.run(client => client.removeGroupMember(groupDid, member));
1355
2226
  }
1356
- /** Resume durable group membership convergence after account sync restores old groups. */
1357
- async resumeGroupRebindRecovery() {
1358
- const result = await this.run(client => client.resumeGroupRebindRecovery());
1359
- if (!result.ok)
1360
- return result;
1361
- const summary = publicGroupRebindRecoverySummary(result.value);
1362
- return summary === undefined
1363
- ? { ok: false, error: failure('remote') }
1364
- : { ok: true, value: summary };
1365
- }
1366
2227
  /** Read presentation-only roster preferences for the active identity. */
1367
2228
  getConversationPreferences() {
1368
2229
  return this.run(async (client) => this.conversationPreferenceStore.get(await this.ownerDid(client)));
@@ -1583,7 +2444,7 @@ let AwikiService = (() => {
1583
2444
  }
1584
2445
  /** Return the deployment identity's public mailbox state. */
1585
2446
  getMailAccount() {
1586
- return this.run(client => client.getMailAccount(), { bindMailOwner: true });
2447
+ return this.run(client => client.getMailAccount());
1587
2448
  }
1588
2449
  /** List one bounded mailbox page on explicit browser/tool demand. */
1589
2450
  async listMailInbox(request) {
@@ -1594,11 +2455,15 @@ let AwikiService = (() => {
1594
2455
  catch {
1595
2456
  return { ok: false, error: failure('invalid-request') };
1596
2457
  }
1597
- return this.run(async (client, _provider, ownerDid) => {
1598
- if (normalized.folder !== 'sent')
1599
- return client.listMailInbox(normalized);
1600
- return this.sentMailStore.list(ownerDid, normalized);
1601
- }, { bindMailOwner: true });
2458
+ const requestGeneration = this.sessionRevision;
2459
+ return this.run(async (client) => {
2460
+ const page = normalized.folder === 'sent'
2461
+ ? await this.mailListClient.listOutbound(normalized)
2462
+ : await client.listMailInbox(normalized);
2463
+ if (this.sessionRevision !== requestGeneration)
2464
+ throw new AwikiSdkError('network');
2465
+ return page;
2466
+ });
1602
2467
  }
1603
2468
  /** Read one bounded plain-text mail message. */
1604
2469
  async readMail(request) {
@@ -1609,116 +2474,22 @@ let AwikiService = (() => {
1609
2474
  catch {
1610
2475
  return { ok: false, error: failure('invalid-request') };
1611
2476
  }
1612
- return this.run(async (client, _provider, ownerDid) => {
1613
- if (!isLocalSentMailId(normalized.messageId))
1614
- return client.readMail(normalized);
1615
- const local = await this.sentMailStore.read(ownerDid, normalized.messageId);
1616
- if (local === undefined) {
1617
- throw Object.assign(new Error('sent mail not found'), { name: 'AwikiSdkError', code: 'not-found' });
1618
- }
1619
- return local;
1620
- }, { bindMailOwner: true });
2477
+ return this.run(client => client.readMail(normalized));
1621
2478
  }
1622
2479
  /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1623
2480
  markMailRead(request) {
1624
- return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized), { bindMailOwner: true });
2481
+ return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized));
1625
2482
  }
1626
- /** Send one mail once. Browser callers require an explicit confirmation. */
2483
+ /** Send one plain-text mail once. Browser callers require an explicit confirmation. */
1627
2484
  async sendMail(request) {
1628
2485
  let normalized;
1629
2486
  try {
1630
- normalized = mailSendRequest(request, {
1631
- maxCount: this.resolved.mailAttachmentMaxCount,
1632
- maxBytes: this.resolved.mailAttachmentMaxBytes,
1633
- totalMaxBytes: this.resolved.mailAttachmentTotalMaxBytes,
1634
- });
2487
+ normalized = mailSendRequest(request);
1635
2488
  }
1636
2489
  catch {
1637
2490
  return { ok: false, error: failure('invalid-request') };
1638
2491
  }
1639
- return this.run(async (client, _provider, ownerDid, assertActive) => {
1640
- const result = await client.sendMail({
1641
- to: [...normalized.to],
1642
- cc: [...normalized.cc],
1643
- subject: normalized.subject,
1644
- bodyText: normalized.bodyText,
1645
- ...normalized.attachments.length === 0 ? {} : {
1646
- attachments: normalized.attachments.map(attachment => ({
1647
- fileName: attachment.fileName,
1648
- contentType: attachment.contentType,
1649
- bytes: attachment.bytes,
1650
- })),
1651
- },
1652
- });
1653
- assertActive();
1654
- if (!result.accepted)
1655
- return result;
1656
- const settled = normalized.attachments.length > 0
1657
- && result.messageId === undefined
1658
- && result.warnings.length < 100
1659
- ? { ...result, warnings: [...result.warnings, 'Sent attachment download is unavailable because the service returned no message id.'] }
1660
- : result;
1661
- try {
1662
- const account = await client.getMailAccount().catch(() => undefined);
1663
- assertActive();
1664
- await this.sentMailStore.append(ownerDid, normalized, settled, account);
1665
- assertActive();
1666
- return settled;
1667
- }
1668
- catch {
1669
- assertActive();
1670
- return settled.warnings.length >= 100
1671
- ? settled
1672
- : { ...settled, warnings: [...settled.warnings, 'Sent history could not be saved locally.'] };
1673
- }
1674
- }, { bindMailOwner: true });
1675
- }
1676
- /** Download one mail attachment only after an explicit browser action. */
1677
- async downloadMailAttachment(request) {
1678
- let normalized;
1679
- try {
1680
- normalized = mailAttachmentDownloadRequest(request);
1681
- }
1682
- catch {
1683
- return { ok: false, error: failure('invalid-request') };
1684
- }
1685
- return this.run(async (client, _provider, ownerDid, assertActive) => {
1686
- let serviceMessageId = normalized.localMessageId;
1687
- if (isLocalSentMailId(serviceMessageId)) {
1688
- const resolved = await this.sentMailStore.resolveAttachment(ownerDid, serviceMessageId, normalized.attachmentIndex);
1689
- if (resolved === undefined) {
1690
- throw Object.assign(new Error('sent mail attachment is unavailable'), {
1691
- name: 'AwikiSdkError', code: 'not-found',
1692
- });
1693
- }
1694
- serviceMessageId = resolved;
1695
- }
1696
- const value = await client.downloadMailAttachment({
1697
- messageId: serviceMessageId,
1698
- attachmentIndex: normalized.attachmentIndex,
1699
- });
1700
- assertActive();
1701
- if (!Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0)
1702
- throw new TypeError('invalid mail attachment size');
1703
- if (value.sizeBytes > this.resolved.mailAttachmentMaxBytes) {
1704
- throw Object.assign(new Error('mail attachment exceeds Host limit'), {
1705
- name: 'AwikiSdkError', code: 'attachment-too-large',
1706
- });
1707
- }
1708
- if (!(value.bytes instanceof Uint8Array) || value.bytes.byteLength !== value.sizeBytes) {
1709
- throw new TypeError('invalid mail attachment bytes');
1710
- }
1711
- const fileName = mailAttachmentFileName(value.fileName);
1712
- const contentType = mailAttachmentContentType(value.contentType);
1713
- const bytes = value.bytes;
1714
- return {
1715
- fileName,
1716
- contentType,
1717
- sizeBytes: bytes.byteLength,
1718
- sha256: createHash('sha256').update(bytes).digest('hex'),
1719
- bytesBase64: Buffer.from(bytes).toString('base64'),
1720
- };
1721
- }, { bindMailOwner: true });
2492
+ return this.run(client => client.sendMail(normalized));
1722
2493
  }
1723
2494
  /**
1724
2495
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -1730,27 +2501,35 @@ let AwikiService = (() => {
1730
2501
  if (request?.confirmation !== AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION) {
1731
2502
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1732
2503
  }
2504
+ // Fence pending post-Recovery work before the serialized clear reaches the head of the queue.
2505
+ this.sessionRevision += 1;
1733
2506
  return this.mutateSession(async () => {
1734
2507
  const provider = this.provider;
1735
2508
  if (provider !== undefined)
1736
- await this.stopListener(provider);
1737
- const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2509
+ await this.stopProviderRuntime(provider);
2510
+ let result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2511
+ // Core's clear is local and idempotent. A provider mutation that was still
2512
+ // settling can surface once as the deliberately opaque `remote` failure.
2513
+ // Retry only that class once after the runtime has already been stopped.
2514
+ if (!result.ok && result.error.code === 'remote') {
2515
+ result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
2516
+ }
1738
2517
  if (!result.ok) {
1739
2518
  if (provider !== undefined && this.provider === provider)
1740
- void this.startListener(provider);
2519
+ this.ensureProviderRuntime(provider);
1741
2520
  return result;
1742
2521
  }
1743
2522
  try {
1744
2523
  await this.imageAttachmentCache.clear();
1745
- await this.sentMailStore.clear();
2524
+ await clearLegacySentMailCache(this.activeClientOptions?.stateRoot ?? this.resolved.stateRoot);
1746
2525
  await this.conversationPreferenceStore.clear();
1747
2526
  await this.sessionStore.signIn();
1748
2527
  this.signedOut = false;
1749
2528
  this.activeIdentityDid = undefined;
2529
+ this.pendingDeviceJoin = undefined;
2530
+ this.activeDeviceJoinSessionId = undefined;
1750
2531
  this.invalidateSummaries();
1751
2532
  this.publishSession({ status: 'unregistered' });
1752
- if (provider !== undefined && this.provider === provider)
1753
- void this.startListener(provider);
1754
2533
  return result;
1755
2534
  }
1756
2535
  catch {
@@ -1758,53 +2537,291 @@ let AwikiService = (() => {
1758
2537
  }
1759
2538
  });
1760
2539
  }
1761
- /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
1762
- async applyRecoveredSession(progress) {
2540
+ /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
2541
+ async applyRecoveredSession(progress, requestGeneration) {
1763
2542
  if (progress.phase !== 'applied')
1764
- return true;
2543
+ return { ok: true, value: progress };
1765
2544
  return this.mutateSession(async () => {
1766
2545
  const provider = this.provider;
1767
2546
  if (provider === undefined)
1768
- return false;
2547
+ return { ok: false, error: failure('remote') };
2548
+ if (this.sessionRevision !== requestGeneration) {
2549
+ return {
2550
+ ok: false,
2551
+ error: {
2552
+ ...failure('remote'),
2553
+ mailRecoveryObservability: fencedMailRecoveryObservability(this.activeIdentityDid === progress.currentDid, 'replaced'),
2554
+ },
2555
+ };
2556
+ }
1769
2557
  try {
1770
2558
  const identity = await provider.client.getIdentity();
1771
- if (identity === null || identity.did !== progress.currentDid)
1772
- return false;
2559
+ const currentPrincipalMatchesRecovery = identity !== null && identity.did === progress.currentDid;
2560
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2561
+ return {
2562
+ ok: false,
2563
+ error: {
2564
+ ...failure('remote'),
2565
+ mailRecoveryObservability: fencedMailRecoveryObservability(currentPrincipalMatchesRecovery, 'replaced'),
2566
+ },
2567
+ };
2568
+ }
2569
+ if (!currentPrincipalMatchesRecovery) {
2570
+ return {
2571
+ ok: false,
2572
+ error: {
2573
+ ...failure('remote'),
2574
+ mailRecoveryObservability: fencedMailRecoveryObservability(false, 'current'),
2575
+ },
2576
+ };
2577
+ }
1773
2578
  const alreadyActive = this.signedOut === false && this.activeIdentityDid === identity.did;
1774
2579
  if (!alreadyActive) {
1775
2580
  await this.sessionStore.signIn();
2581
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2582
+ return {
2583
+ ok: false,
2584
+ error: {
2585
+ ...failure('remote'),
2586
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, 'replaced'),
2587
+ },
2588
+ };
2589
+ }
1776
2590
  this.signedOut = false;
1777
2591
  this.activeIdentityDid = identity.did;
1778
2592
  this.invalidateSummaries();
2593
+ requestGeneration = this.sessionRevision;
2594
+ }
2595
+ const mailRecoveryObservability = await this.reconcileRecoveredMailbox(provider);
2596
+ if (this.provider !== provider || this.sessionRevision !== requestGeneration) {
2597
+ return {
2598
+ ok: false,
2599
+ error: {
2600
+ ...failure('remote'),
2601
+ mailRecoveryObservability: fencedMailRecoveryObservability(true, 'replaced'),
2602
+ },
2603
+ };
1779
2604
  }
1780
- if (this.provider !== provider)
1781
- return false;
1782
2605
  if (!alreadyActive) {
1783
2606
  const session = { status: 'active', identity };
1784
2607
  this.publishSession(session);
1785
- await provider.listenerStartup;
1786
- await this.startListener(provider);
2608
+ this.ensureProviderRuntime(provider);
1787
2609
  }
1788
- return true;
2610
+ return { ok: true, value: { ...progress, mailRecoveryObservability } };
1789
2611
  }
1790
2612
  catch {
1791
- return false;
2613
+ return { ok: false, error: failure('remote') };
1792
2614
  }
1793
2615
  });
1794
2616
  }
1795
- /** Publish one newly registered identity and start its listener through the existing session path. */
2617
+ /** Rebind Mail first-use ownership and retain only its closed, secret-free classification. */
2618
+ async reconcileRecoveredMailbox(provider) {
2619
+ try {
2620
+ await provider.client.getMailAccount();
2621
+ return successfulMailRecoveryObservability();
2622
+ }
2623
+ catch (error) {
2624
+ return failedMailRecoveryObservability(error);
2625
+ }
2626
+ }
2627
+ /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
2628
+ async selectDeviceJoinSession(client) {
2629
+ const sessions = await client.listLocalDeviceJoinSessions();
2630
+ const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
2631
+ if (this.activeDeviceJoinSessionId !== undefined) {
2632
+ const tracked = resumable.find(value => value.joinSessionId === this.activeDeviceJoinSessionId);
2633
+ if (tracked !== undefined)
2634
+ return tracked.joinSessionId;
2635
+ this.activeDeviceJoinSessionId = undefined;
2636
+ }
2637
+ if (resumable.length > 1) {
2638
+ throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
2639
+ }
2640
+ const selected = resumable[0];
2641
+ this.activeDeviceJoinSessionId = selected?.joinSessionId;
2642
+ return selected?.joinSessionId ?? null;
2643
+ }
2644
+ async applyCandidateJoinProgress(value) {
2645
+ const phase = candidateJoinPhase(value);
2646
+ if (phase === undefined)
2647
+ return { ok: false, error: failure('remote') };
2648
+ if (phase === 'cancelled' || phase === 'rejected' || phase === 'expired') {
2649
+ this.activeDeviceJoinSessionId = undefined;
2650
+ }
2651
+ if (phase === 'authorized') {
2652
+ if (value.identity === undefined)
2653
+ return { ok: false, error: failure('remote') };
2654
+ await this.activateRegisteredIdentity(value.identity);
2655
+ }
2656
+ return {
2657
+ ok: true,
2658
+ value: {
2659
+ phase,
2660
+ expiresAt: value.expiresAt,
2661
+ ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2662
+ completed: phase === 'authorized',
2663
+ },
2664
+ };
2665
+ }
2666
+ publicAdminJoinProgress(requestRef, value) {
2667
+ const phase = adminJoinPhase(value);
2668
+ if (phase === undefined)
2669
+ return { ok: false, error: failure('remote') };
2670
+ return {
2671
+ ok: true,
2672
+ value: {
2673
+ requestRef,
2674
+ phase,
2675
+ expiresAt: value.expiresAt,
2676
+ ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2677
+ },
2678
+ };
2679
+ }
2680
+ async localAdminJoinProgress(client, notice) {
2681
+ const local = (await client.listLocalDeviceJoinSessions())
2682
+ .find(session => session.side === 'admin' && session.joinSessionId === notice.joinSessionId);
2683
+ if (local === undefined) {
2684
+ return notice.claimedByCurrentDevice
2685
+ ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId)
2686
+ : undefined;
2687
+ }
2688
+ if (local.localPhase === 'challenge_prepared') {
2689
+ return {
2690
+ joinSessionId: notice.joinSessionId,
2691
+ localPhase: local.localPhase,
2692
+ remoteState: notice.state,
2693
+ expiresAt: local.expiresAt,
2694
+ };
2695
+ }
2696
+ return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2697
+ }
2698
+ async requireDeviceManager(client) {
2699
+ const current = await client.getCurrentDeviceSummary();
2700
+ if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2701
+ throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2702
+ }
2703
+ }
2704
+ async requireRootTransferRecipient(client, deviceId) {
2705
+ if (!client.trustedUserPresenceSupported) {
2706
+ throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
2707
+ }
2708
+ await this.requireDeviceManager(client);
2709
+ await client.syncDeviceManagement();
2710
+ const identity = await client.getIdentity();
2711
+ if (identity === null) {
2712
+ throw Object.assign(new Error('identity required'), { name: 'AwikiSdkError', code: 'not-registered' });
2713
+ }
2714
+ const target = (await client.getDeviceRegistry()).find(device => device.deviceId === deviceId);
2715
+ if (target === undefined
2716
+ || target.isCurrent
2717
+ || target.status !== 'active'
2718
+ || target.role !== 'member'
2719
+ || target.managementReady) {
2720
+ throw Object.assign(new Error('root transfer recipient unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
2721
+ }
2722
+ return identity.did;
2723
+ }
2724
+ requestRef(joinSessionId) {
2725
+ const existing = this.requestRefs.get(joinSessionId);
2726
+ if (existing !== undefined)
2727
+ return existing;
2728
+ const reference = `join-${randomUUID()}`;
2729
+ this.requestRefs.set(joinSessionId, reference);
2730
+ this.requestSessions.set(reference, joinSessionId);
2731
+ return reference;
2732
+ }
2733
+ deviceRef(deviceId) {
2734
+ const existing = this.deviceRefs.get(deviceId);
2735
+ if (existing !== undefined)
2736
+ return existing;
2737
+ const reference = `device-${randomUUID()}`;
2738
+ this.deviceRefs.set(deviceId, reference);
2739
+ this.deviceIds.set(reference, deviceId);
2740
+ return reference;
2741
+ }
2742
+ async deviceManagementSnapshot(client) {
2743
+ await client.syncDeviceManagement();
2744
+ const current = await client.getCurrentDeviceSummary();
2745
+ if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2746
+ this.requestRefs.clear();
2747
+ this.requestSessions.clear();
2748
+ this.deviceRefs.clear();
2749
+ this.deviceIds.clear();
2750
+ this.rootTransfers.clear();
2751
+ return {
2752
+ canManage: false,
2753
+ rootTransferSupported: client.trustedUserPresenceSupported,
2754
+ ...current.role === undefined ? {} : { role: current.role },
2755
+ readiness: current.readiness,
2756
+ devices: [],
2757
+ requests: [],
2758
+ };
2759
+ }
2760
+ const requests = await client.listLocalDeviceJoinRequests();
2761
+ await Promise.all(requests
2762
+ .filter(request => request.claimedByCurrentDevice)
2763
+ .map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
2764
+ const devices = await client.getDeviceRegistry();
2765
+ const identity = await client.getIdentity();
2766
+ const joinedAtByDeviceId = new Map();
2767
+ for (const request of requests) {
2768
+ const previous = joinedAtByDeviceId.get(request.protocolDeviceId);
2769
+ if (previous === undefined || request.issuedAt > previous) {
2770
+ joinedAtByDeviceId.set(request.protocolDeviceId, request.issuedAt);
2771
+ }
2772
+ }
2773
+ const initialDeviceJoinedAt = identity === null
2774
+ ? undefined
2775
+ : new Date(identity.registeredAt).toISOString();
2776
+ return {
2777
+ canManage: true,
2778
+ rootTransferSupported: client.trustedUserPresenceSupported,
2779
+ role: 'admin',
2780
+ readiness: 'admin_ready',
2781
+ devices: devices
2782
+ .filter(device => device.status === 'active')
2783
+ .map(device => this.publicDevice(device, joinedAtByDeviceId.get(device.deviceId) ?? (device.isCurrent ? initialDeviceJoinedAt : undefined))),
2784
+ requests: requests.map(request => ({
2785
+ requestRef: this.requestRef(request.joinSessionId),
2786
+ candidateKeyFingerprint: request.candidateKeyFingerprint,
2787
+ issuedAt: request.issuedAt,
2788
+ expiresAt: request.expiresAt,
2789
+ state: requestJoinPhase(request),
2790
+ claimedByCurrentDevice: request.claimedByCurrentDevice,
2791
+ canStartVerification: request.canStartVerification,
2792
+ })),
2793
+ };
2794
+ }
2795
+ publicDevice(device, joinedAt) {
2796
+ const fingerprint = createHash('sha256').update(device.deviceId).digest('hex').slice(0, 8).toUpperCase();
2797
+ return {
2798
+ deviceRef: this.deviceRef(device.deviceId),
2799
+ displayId: `${fingerprint.slice(0, 4)}-${fingerprint.slice(4)}`,
2800
+ ...joinedAt === undefined ? {} : { joinedAt },
2801
+ status: device.status,
2802
+ role: device.role,
2803
+ managementReady: device.managementReady,
2804
+ isCurrent: device.isCurrent,
2805
+ };
2806
+ }
2807
+ /** Publish one newly registered identity, then start realtime only in the background. */
1796
2808
  async activateRegisteredIdentity(identity) {
2809
+ this.pendingDeviceJoin = undefined;
2810
+ this.activeDeviceJoinSessionId = undefined;
1797
2811
  this.activeIdentityDid = identity.did;
1798
2812
  this.publishSession({ status: 'active', identity });
1799
2813
  const provider = this.provider;
1800
- if (provider !== undefined) {
1801
- await provider.listenerStartup;
1802
- await this.startListener(provider);
1803
- }
2814
+ if (provider !== undefined)
2815
+ this.ensureProviderRuntime(provider);
1804
2816
  }
1805
2817
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
1806
2818
  invalidateSummaries() {
1807
2819
  this.sessionRevision += 1;
2820
+ this.requestRefs.clear();
2821
+ this.requestSessions.clear();
2822
+ this.deviceRefs.clear();
2823
+ this.deviceIds.clear();
2824
+ this.rootTransfers.clear();
1808
2825
  for (const controller of this.activeSummaryRequests)
1809
2826
  controller.abort();
1810
2827
  this.activeSummaryRequests.clear();
@@ -1815,40 +2832,31 @@ let AwikiService = (() => {
1815
2832
  }
1816
2833
  /** Resolve and cache the owner binding required by private Host-side projections. */
1817
2834
  async ownerDid(client) {
1818
- const identity = await client.getIdentity();
1819
- const ownerDid = identity?.did;
2835
+ const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
2836
+ const ownerDid = this.activeIdentityDid ?? identity?.did;
1820
2837
  if (ownerDid === undefined) {
1821
2838
  throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
1822
2839
  }
1823
- if (this.activeIdentityDid !== undefined && this.activeIdentityDid !== ownerDid) {
1824
- throw new ProviderUnavailableError();
1825
- }
1826
2840
  this.activeIdentityDid = ownerDid;
1827
2841
  return ownerDid;
1828
2842
  }
1829
2843
  /** Invoke the current client and normalize every rejection to a public result. */
1830
2844
  async run(operation, options = {}) {
1831
2845
  try {
1832
- const sessionRevision = this.sessionRevision;
2846
+ const generation = this.runtimeGeneration;
2847
+ if (this.getUpdatePolicyStatus().restricted) {
2848
+ return { ok: false, error: failure('client-version-unsupported') };
2849
+ }
1833
2850
  if (options.allowSignedOut !== true && await this.isSignedOut()) {
1834
2851
  return { ok: false, error: failure('signed-out') };
1835
2852
  }
1836
2853
  const provider = this.provider;
1837
2854
  if (provider === undefined)
1838
2855
  throw new ProviderUnavailableError();
1839
- const ownerDid = options.bindMailOwner === true
1840
- ? await this.ownerDid(provider.client)
1841
- : undefined;
1842
- const assertActive = () => {
1843
- if (this.provider !== provider
1844
- || (options.bindMailOwner === true && (this.sessionRevision !== sessionRevision
1845
- || this.signedOut === true
1846
- || this.activeIdentityDid !== ownerDid)))
1847
- throw new ProviderUnavailableError();
1848
- };
1849
- assertActive();
1850
- const value = await operation(provider.client, provider, ownerDid, assertActive);
1851
- assertActive();
2856
+ const value = await operation(provider.client);
2857
+ if (this.runtimeGeneration !== generation || this.provider !== provider) {
2858
+ return { ok: false, error: failure('conflict') };
2859
+ }
1852
2860
  if (options.skipAttachmentByteValidation !== true && containsUnexpectedBinary(value, new Set())) {
1853
2861
  return { ok: false, error: failure('remote') };
1854
2862
  }
@@ -1858,8 +2866,13 @@ let AwikiService = (() => {
1858
2866
  return { ok: false, error: normalizeFailure(error) };
1859
2867
  }
1860
2868
  }
2869
+ assertVersionSupported() {
2870
+ if (this.getUpdatePolicyStatus().restricted) {
2871
+ throw new Error('awiki: this plugin version must be updated before using the active tenant');
2872
+ }
2873
+ }
1861
2874
  /** Validate mail input before entering the provider and preserve fixed public failures. */
1862
- runValidatedMail(validate, operation, options = {}) {
2875
+ runValidatedMail(validate, operation) {
1863
2876
  let request;
1864
2877
  try {
1865
2878
  request = validate();
@@ -1867,7 +2880,7 @@ let AwikiService = (() => {
1867
2880
  catch {
1868
2881
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1869
2882
  }
1870
- return this.run(client => operation(client, request), options);
2883
+ return this.run(client => operation(client, request));
1871
2884
  }
1872
2885
  /** Read and cache the private Host-owned session marker. */
1873
2886
  async isSignedOut() {
@@ -1918,165 +2931,211 @@ let AwikiService = (() => {
1918
2931
  }
1919
2932
  /** Serialize sign-in, sign-out, and destructive clear transitions. */
1920
2933
  mutateSession(operation) {
1921
- const pending = this.sessionMutation.then(operation, operation);
2934
+ const generation = this.runtimeGeneration;
2935
+ if (this.tenantSwitching)
2936
+ return Promise.resolve({ ok: false, error: failure('conflict') });
2937
+ const invoke = () => this.tenantSwitching || generation !== this.runtimeGeneration
2938
+ ? Promise.resolve({ ok: false, error: failure('conflict') })
2939
+ : operation();
2940
+ const pending = this.sessionMutation.then(invoke, invoke);
1922
2941
  this.sessionMutation = pending.then(() => undefined, () => undefined);
1923
2942
  return pending;
1924
2943
  }
1925
- /** Start one exact identity-bound listener, atomically releasing a failed startup. */
1926
- startListener(provider) {
1927
- if (!this.resolved.listenerEnabled
1928
- || provider.listener !== undefined
1929
- || provider.listenerRecoveryBlocked
1930
- || this.provider !== provider) {
1931
- return Promise.resolve();
1932
- }
1933
- if (provider.listenerStartup !== undefined)
1934
- return provider.listenerStartup;
1935
- if (provider.listenerCleanup !== undefined || provider.listenerRestartTimer !== undefined)
1936
- return Promise.resolve();
1937
- const workspaceContext = this.workspaceContext;
1938
- const source = provider.client.listener;
1939
- if (workspaceContext === undefined || source === undefined)
1940
- return Promise.resolve();
1941
- const generation = provider.listenerGeneration;
1942
- const logger = this.ctx.logger('awiki-listener');
2944
+ /** Start identity realtime and the optional Agent consumer without blocking identity success. */
2945
+ ensureProviderRuntime(provider) {
2946
+ if (this.tenantSwitching)
2947
+ return;
2948
+ const identityDid = this.activeIdentityDid;
2949
+ if (provider.runtimeReplacement !== undefined)
2950
+ return;
2951
+ if (identityDid !== undefined
2952
+ && provider.realtimeIdentityDid !== undefined
2953
+ && provider.realtimeIdentityDid !== identityDid) {
2954
+ this.replaceProviderRuntime(provider, identityDid);
2955
+ return;
2956
+ }
2957
+ this.ensureRealtimeSupervisor(provider);
2958
+ this.ensureAgentConsumer(provider);
2959
+ }
2960
+ replaceProviderRuntime(provider, identityDid) {
2961
+ if (provider.runtimeReplacement !== undefined || this.provider !== provider)
2962
+ return;
2963
+ const replacement = (async () => {
2964
+ await this.stopProviderRuntime(provider);
2965
+ if (this.provider !== provider || this.activeIdentityDid !== identityDid)
2966
+ return;
2967
+ this.ensureRealtimeSupervisor(provider);
2968
+ this.ensureAgentConsumer(provider);
2969
+ })();
2970
+ const observed = replacement
2971
+ .catch((error) => {
2972
+ this.ctx.logger('awiki-realtime').warn('AWiki identity realtime replacement failed: %s', error instanceof Error ? error.message : 'unknown failure');
2973
+ })
2974
+ .finally(() => {
2975
+ if (provider.runtimeReplacement === observed)
2976
+ delete provider.runtimeReplacement;
2977
+ if (this.provider === provider)
2978
+ this.ensureProviderRuntime(provider);
2979
+ });
2980
+ provider.runtimeReplacement = observed;
2981
+ }
2982
+ ensureRealtimeSupervisor(provider) {
2983
+ if (this.getUpdatePolicyStatus().restricted)
2984
+ return;
2985
+ if (!this.resolved.realtimeEnabled
2986
+ || provider.realtimeSupervisor !== undefined
2987
+ || provider.realtimeStartup !== undefined
2988
+ || this.provider !== provider
2989
+ || (provider.client.realtime ?? provider.client.listener) === undefined)
2990
+ return;
2991
+ const generation = provider.realtimeGeneration;
2992
+ const source = provider.client.realtime ?? provider.client.listener;
2993
+ if (source === undefined)
2994
+ return;
2995
+ const logger = this.ctx.logger('awiki-realtime');
1943
2996
  const startup = (async () => {
1944
2997
  if (await this.isSignedOut())
1945
2998
  return;
1946
- if (await provider.client.getIdentity() === null)
1947
- return;
1948
- if (!this.listenerFenceMatches(provider, workspaceContext, generation))
2999
+ const identity = await provider.client.getIdentity();
3000
+ if (identity === null || !this.realtimeFenceMatches(provider, generation))
1949
3001
  return;
1950
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
1951
- const listener = new AwikiAgentListener(source, agents, {
1952
- ...this.resolved.listener,
1953
- stateRoot: this.activeTenant().stateRoot,
3002
+ this.activeIdentityDid = identity.did;
3003
+ const supervisor = new IdentityRealtimeSupervisor(source, {
3004
+ onSynchronized: cause => this.onRealtimeSynchronized(provider, generation, cause),
1954
3005
  }, logger);
1955
- provider.listener = listener;
3006
+ provider.realtimeSupervisor = supervisor;
3007
+ provider.realtimeIdentityDid = identity.did;
3008
+ supervisor.start();
3009
+ this.ensureAgentConsumer(provider);
3010
+ })();
3011
+ const observed = startup
3012
+ .catch((error) => {
3013
+ logger.warn('AWiki realtime startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
3014
+ })
3015
+ .finally(() => {
3016
+ if (provider.realtimeStartup === observed)
3017
+ delete provider.realtimeStartup;
3018
+ });
3019
+ provider.realtimeStartup = observed;
3020
+ }
3021
+ realtimeFenceMatches(provider, generation) {
3022
+ return this.provider === provider && provider.realtimeGeneration === generation;
3023
+ }
3024
+ async onRealtimeSynchronized(provider, generation, cause) {
3025
+ if (!this.realtimeFenceMatches(provider, generation))
3026
+ return;
3027
+ if (cause === 'system_notification' || cause === 'stream_recovery') {
1956
3028
  try {
1957
- await listener.start();
3029
+ provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
1958
3030
  }
1959
- catch (error) {
1960
- if (provider.listener === listener)
1961
- delete provider.listener;
1962
- try {
1963
- await listener.dispose();
1964
- }
1965
- catch {
1966
- provider.listenerRecoveryBlocked = true;
1967
- }
1968
- if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
1969
- this.scheduleListenerRestart(provider, workspaceContext, generation);
1970
- }
1971
- throw error;
3031
+ catch {
3032
+ this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
1972
3033
  }
1973
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
1974
- if (provider.listener === listener)
1975
- delete provider.listener;
1976
- try {
1977
- await listener.dispose();
1978
- }
1979
- catch (error) {
1980
- provider.listenerRecoveryBlocked = true;
1981
- throw error;
1982
- }
3034
+ }
3035
+ if (!['session_start', 'connection_ready', 'reconnected', 'message', 'message_update'].includes(cause))
3036
+ return;
3037
+ this.ensureAgentConsumer(provider);
3038
+ await provider.agentConsumerStartup;
3039
+ if (!this.realtimeFenceMatches(provider, generation))
3040
+ return;
3041
+ await provider.agentConsumer?.reconcileOnce();
3042
+ }
3043
+ ensureAgentConsumer(provider) {
3044
+ if (this.getUpdatePolicyStatus().restricted)
3045
+ return;
3046
+ const workspaceContext = this.workspaceContext;
3047
+ const source = provider.client.agentInbox ?? provider.client.listener;
3048
+ const identityDid = this.activeIdentityDid;
3049
+ if (!this.resolved.listenerEnabled
3050
+ || workspaceContext === undefined
3051
+ || source === undefined
3052
+ || identityDid === undefined
3053
+ || this.provider !== provider)
3054
+ return;
3055
+ if (provider.agentConsumer !== undefined && provider.agentConsumerIdentityDid === identityDid)
3056
+ return;
3057
+ if (provider.agentConsumerStartup !== undefined)
3058
+ return;
3059
+ const generation = ++provider.agentConsumerGeneration;
3060
+ const logger = this.ctx.logger('awiki-listener');
3061
+ const startup = (async () => {
3062
+ await this.detachAgentConsumer(provider);
3063
+ if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
1983
3064
  return;
1984
- }
1985
- provider.listenerStartedAt = Date.now();
1986
- void listener.whenTerminated().then((result) => {
1987
- if (result.kind === 'failed') {
1988
- this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
1989
- }
1990
- });
3065
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
3066
+ const consumer = new AwikiAgentListener(source, agents, {
3067
+ ...this.resolved.listener,
3068
+ stateRoot: this.activeClientOptions?.stateRoot ?? this.resolved.listener.stateRoot,
3069
+ identityScope: identityDid,
3070
+ }, logger);
3071
+ provider.agentConsumer = consumer;
3072
+ provider.agentConsumerIdentityDid = identityDid;
3073
+ await consumer.reconcileOnce();
1991
3074
  })();
1992
3075
  const observed = startup
1993
3076
  .catch((error) => {
1994
- logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
3077
+ logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1995
3078
  })
1996
3079
  .finally(() => {
1997
- if (provider.listenerStartup === observed)
1998
- delete provider.listenerStartup;
3080
+ if (provider.agentConsumerStartup === observed)
3081
+ delete provider.agentConsumerStartup;
1999
3082
  });
2000
- provider.listenerStartup = observed;
2001
- return observed;
2002
- }
2003
- async stopListener(provider) {
2004
- provider.listenerGeneration += 1;
2005
- provider.listenerRestartAttempt = 0;
2006
- delete provider.listenerStartedAt;
2007
- const timer = provider.listenerRestartTimer;
2008
- if (timer !== undefined) {
2009
- delete provider.listenerRestartTimer;
2010
- clearTimeout(timer);
2011
- }
2012
- await provider.listenerStartup;
2013
- await provider.listenerCleanup;
2014
- const listener = provider.listener;
2015
- if (listener === undefined)
2016
- return;
2017
- delete provider.listener;
2018
- try {
2019
- await listener.dispose();
2020
- }
2021
- catch (error) {
2022
- provider.listenerRecoveryBlocked = true;
2023
- throw error;
2024
- }
3083
+ provider.agentConsumerStartup = observed;
2025
3084
  }
2026
- listenerFenceMatches(provider, workspaceContext, generation) {
3085
+ agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
2027
3086
  return this.provider === provider
2028
3087
  && this.workspaceContext === workspaceContext
2029
- && provider.listenerGeneration === generation;
3088
+ && this.activeIdentityDid === identityDid
3089
+ && provider.agentConsumerGeneration === generation;
2030
3090
  }
2031
- releaseFailedListener(provider, listener, workspaceContext, generation, error) {
2032
- if (provider.listener !== listener)
3091
+ async detachAgentConsumer(provider) {
3092
+ const consumer = provider.agentConsumer;
3093
+ if (consumer === undefined)
2033
3094
  return;
2034
- delete provider.listener;
2035
- const startedAt = provider.listenerStartedAt;
2036
- delete provider.listenerStartedAt;
2037
- if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
2038
- provider.listenerRestartAttempt = 0;
2039
- }
2040
- const logger = this.ctx.logger('awiki-listener');
2041
- logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
2042
- let disposed = false;
2043
- const cleanup = listener.dispose()
2044
- .then(() => { disposed = true; }, (cleanupError) => {
2045
- provider.listenerRecoveryBlocked = true;
2046
- logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
2047
- });
3095
+ delete provider.agentConsumer;
3096
+ delete provider.agentConsumerIdentityDid;
3097
+ const cleanup = consumer.dispose();
2048
3098
  const observed = cleanup.finally(() => {
2049
- if (provider.listenerCleanup === observed)
2050
- delete provider.listenerCleanup;
2051
- if (disposed)
2052
- this.scheduleListenerRestart(provider, workspaceContext, generation);
3099
+ if (provider.agentConsumerCleanup === observed)
3100
+ delete provider.agentConsumerCleanup;
2053
3101
  });
2054
- provider.listenerCleanup = observed;
2055
- }
2056
- scheduleListenerRestart(provider, workspaceContext, generation) {
2057
- if (provider.listenerRestartTimer !== undefined
2058
- || !this.listenerFenceMatches(provider, workspaceContext, generation))
3102
+ provider.agentConsumerCleanup = observed;
3103
+ await observed;
3104
+ }
3105
+ async stopAgentConsumer(provider) {
3106
+ provider.agentConsumerGeneration += 1;
3107
+ await provider.agentConsumerStartup;
3108
+ await provider.agentConsumerCleanup;
3109
+ await this.detachAgentConsumer(provider);
3110
+ }
3111
+ async stopRealtimeSupervisor(provider) {
3112
+ provider.realtimeGeneration += 1;
3113
+ await provider.realtimeStartup;
3114
+ const supervisor = provider.realtimeSupervisor;
3115
+ if (supervisor === undefined)
2059
3116
  return;
2060
- provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
2061
- const exponent = provider.listenerRestartAttempt - 1;
2062
- const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
2063
- const timer = setTimeout(() => {
2064
- if (provider.listenerRestartTimer !== timer)
2065
- return;
2066
- delete provider.listenerRestartTimer;
2067
- if (!this.listenerFenceMatches(provider, workspaceContext, generation))
2068
- return;
2069
- void this.startListener(provider);
2070
- }, delay);
2071
- provider.listenerRestartTimer = timer;
3117
+ delete provider.realtimeSupervisor;
3118
+ delete provider.realtimeIdentityDid;
3119
+ await supervisor.dispose();
3120
+ }
3121
+ async stopProviderRuntime(provider) {
3122
+ await Promise.all([
3123
+ this.stopRealtimeSupervisor(provider),
3124
+ this.stopAgentConsumer(provider),
3125
+ ]);
2072
3126
  }
2073
3127
  /** Clear one exact provider slot before joining its one shared disposal. */
2074
3128
  disposeProvider(provider) {
2075
- if (this.provider === provider)
3129
+ if (this.provider === provider) {
2076
3130
  this.provider = undefined;
3131
+ this.pendingDeviceJoin = undefined;
3132
+ this.activeDeviceJoinSessionId = undefined;
3133
+ this.invalidateSummaries();
3134
+ }
2077
3135
  provider.disposal ??= (async () => {
2078
3136
  try {
2079
- await this.stopListener(provider);
3137
+ await provider.runtimeReplacement;
3138
+ await this.stopProviderRuntime(provider);
2080
3139
  }
2081
3140
  finally {
2082
3141
  await provider.client.dispose();