@awiki/dsh-plugin 0.3.7 → 0.3.10

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