@awiki/dsh-plugin 0.2.4 → 0.3.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +131 -22
  2. package/README.zh.md +110 -20
  3. package/THIRD_PARTY_NOTICES.md +8 -1
  4. package/cordis.patch.yml +5 -0
  5. package/lib/client.js +12534 -2647
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +2431 -92
  8. package/lib/model-proxy-contract.js +96 -0
  9. package/lib/provider.js +4 -1
  10. package/lib/sdk-adapter-DLhLrLI6.mjs +886 -0
  11. package/lib/typert.host.js +1269 -534
  12. package/lib/typert.remote-client.d.ts +47 -1
  13. package/lib/typert.remote-client.d.ts.map +1 -1
  14. package/lib/typert.remote-client.js +1254 -141
  15. package/lib/types/attachment-cache.d.ts +28 -0
  16. package/lib/types/attachment-cache.d.ts.map +1 -0
  17. package/lib/types/attachment-cache.js +194 -0
  18. package/lib/types/attachment-cache.js.map +1 -0
  19. package/lib/types/client/AwikiGroupDetails.d.ts +16 -0
  20. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -0
  21. package/lib/types/client/AwikiGroupDetails.js +108 -0
  22. package/lib/types/client/AwikiGroupDetails.js.map +1 -0
  23. package/lib/types/client/AwikiIdentityAccess.d.ts +21 -0
  24. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -0
  25. package/lib/types/client/AwikiIdentityAccess.js +148 -0
  26. package/lib/types/client/AwikiIdentityAccess.js.map +1 -0
  27. package/lib/types/client/AwikiIdentityPage.d.ts +11 -0
  28. package/lib/types/client/AwikiIdentityPage.d.ts.map +1 -0
  29. package/lib/types/client/AwikiIdentityPage.js +8 -0
  30. package/lib/types/client/AwikiIdentityPage.js.map +1 -0
  31. package/lib/types/client/AwikiMail.d.ts +15 -0
  32. package/lib/types/client/AwikiMail.d.ts.map +1 -0
  33. package/lib/types/client/AwikiMail.js +380 -0
  34. package/lib/types/client/AwikiMail.js.map +1 -0
  35. package/lib/types/client/AwikiOnboarding.d.ts +22 -0
  36. package/lib/types/client/AwikiOnboarding.d.ts.map +1 -0
  37. package/lib/types/client/AwikiOnboarding.js +104 -0
  38. package/lib/types/client/AwikiOnboarding.js.map +1 -0
  39. package/lib/types/client/AwikiOverlay.d.ts +10 -0
  40. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  41. package/lib/types/client/AwikiOverlay.js +370 -127
  42. package/lib/types/client/AwikiOverlay.js.map +1 -1
  43. package/lib/types/client/AwikiProfileCard.d.ts +9 -0
  44. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -0
  45. package/lib/types/client/AwikiProfileCard.js +90 -0
  46. package/lib/types/client/AwikiProfileCard.js.map +1 -0
  47. package/lib/types/client/AwikiRecoveryForm.d.ts +24 -0
  48. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -0
  49. package/lib/types/client/AwikiRecoveryForm.js +148 -0
  50. package/lib/types/client/AwikiRecoveryForm.js.map +1 -0
  51. package/lib/types/client/AwikiSettingsSection.d.ts +18 -4
  52. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  53. package/lib/types/client/AwikiSettingsSection.js +277 -23
  54. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  55. package/lib/types/client/MentionText.d.ts +7 -0
  56. package/lib/types/client/MentionText.d.ts.map +1 -0
  57. package/lib/types/client/MentionText.js +10 -0
  58. package/lib/types/client/MentionText.js.map +1 -0
  59. package/lib/types/client/RechargeComingSoonDialog.d.ts +11 -0
  60. package/lib/types/client/RechargeComingSoonDialog.d.ts.map +1 -0
  61. package/lib/types/client/RechargeComingSoonDialog.js +8 -0
  62. package/lib/types/client/RechargeComingSoonDialog.js.map +1 -0
  63. package/lib/types/client/controller.d.ts +174 -4
  64. package/lib/types/client/controller.d.ts.map +1 -1
  65. package/lib/types/client/controller.js +975 -79
  66. package/lib/types/client/controller.js.map +1 -1
  67. package/lib/types/client/image-cache.d.ts +20 -0
  68. package/lib/types/client/image-cache.d.ts.map +1 -0
  69. package/lib/types/client/image-cache.js +177 -0
  70. package/lib/types/client/image-cache.js.map +1 -0
  71. package/lib/types/client/index.d.ts +2 -0
  72. package/lib/types/client/index.d.ts.map +1 -1
  73. package/lib/types/client/index.js +102 -42
  74. package/lib/types/client/index.js.map +1 -1
  75. package/lib/types/client/mail-list-cache.d.ts +15 -0
  76. package/lib/types/client/mail-list-cache.d.ts.map +1 -0
  77. package/lib/types/client/mail-list-cache.js +212 -0
  78. package/lib/types/client/mail-list-cache.js.map +1 -0
  79. package/lib/types/client/mentions.d.ts +51 -0
  80. package/lib/types/client/mentions.d.ts.map +1 -0
  81. package/lib/types/client/mentions.js +165 -0
  82. package/lib/types/client/mentions.js.map +1 -0
  83. package/lib/types/client/model-availability-controller.d.ts +27 -0
  84. package/lib/types/client/model-availability-controller.d.ts.map +1 -0
  85. package/lib/types/client/model-availability-controller.js +111 -0
  86. package/lib/types/client/model-availability-controller.js.map +1 -0
  87. package/lib/types/client/model-proxy-controller.d.ts +45 -0
  88. package/lib/types/client/model-proxy-controller.d.ts.map +1 -0
  89. package/lib/types/client/model-proxy-controller.js +306 -0
  90. package/lib/types/client/model-proxy-controller.js.map +1 -0
  91. package/lib/types/client/recharge-availability.d.ts +6 -0
  92. package/lib/types/client/recharge-availability.d.ts.map +1 -0
  93. package/lib/types/client/recharge-availability.js +6 -0
  94. package/lib/types/client/recharge-availability.js.map +1 -0
  95. package/lib/types/client/settings-locales.d.ts +1 -1
  96. package/lib/types/client/settings-locales.d.ts.map +1 -1
  97. package/lib/types/client/settings-locales.js +186 -8
  98. package/lib/types/client/settings-locales.js.map +1 -1
  99. package/lib/types/client/slots.d.ts +34 -2
  100. package/lib/types/client/slots.d.ts.map +1 -1
  101. package/lib/types/index.d.ts +95 -7
  102. package/lib/types/index.d.ts.map +1 -1
  103. package/lib/types/index.js +968 -37
  104. package/lib/types/index.js.map +1 -1
  105. package/lib/types/listener-state.d.ts +23 -0
  106. package/lib/types/listener-state.d.ts.map +1 -0
  107. package/lib/types/listener-state.js +124 -0
  108. package/lib/types/listener-state.js.map +1 -0
  109. package/lib/types/listener.d.ts +96 -0
  110. package/lib/types/listener.d.ts.map +1 -0
  111. package/lib/types/listener.js +575 -0
  112. package/lib/types/listener.js.map +1 -0
  113. package/lib/types/mail.d.ts +8 -0
  114. package/lib/types/mail.d.ts.map +1 -0
  115. package/lib/types/mail.js +75 -0
  116. package/lib/types/mail.js.map +1 -0
  117. package/lib/types/model-proxy-contract.d.ts +67 -0
  118. package/lib/types/model-proxy-contract.d.ts.map +1 -0
  119. package/lib/types/model-proxy-contract.js +135 -0
  120. package/lib/types/model-proxy-contract.js.map +1 -0
  121. package/lib/types/provider-api.d.ts +106 -2
  122. package/lib/types/provider-api.d.ts.map +1 -1
  123. package/lib/types/provider.d.ts.map +1 -1
  124. package/lib/types/provider.js +3 -0
  125. package/lib/types/provider.js.map +1 -1
  126. package/lib/types/sdk-adapter.d.ts +43 -2
  127. package/lib/types/sdk-adapter.d.ts.map +1 -1
  128. package/lib/types/sdk-adapter.js +622 -24
  129. package/lib/types/sdk-adapter.js.map +1 -1
  130. package/lib/types/sent-mail-store.d.ts +22 -0
  131. package/lib/types/sent-mail-store.d.ts.map +1 -0
  132. package/lib/types/sent-mail-store.js +246 -0
  133. package/lib/types/sent-mail-store.js.map +1 -0
  134. package/lib/types/tools.d.ts +10 -0
  135. package/lib/types/tools.d.ts.map +1 -1
  136. package/lib/types/tools.js +98 -0
  137. package/lib/types/tools.js.map +1 -1
  138. package/lib/types/types.d.ts +254 -2
  139. package/lib/types/types.d.ts.map +1 -1
  140. package/lib/types/types.js.map +1 -1
  141. package/package.json +31 -17
  142. package/lib/sdk-adapter-DK6RqV-c.mjs +0 -355
@@ -35,7 +35,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
35
35
  };
36
36
  import { Context } from '@deepseek-ai/cordis';
37
37
  import { homedir } from 'node:os';
38
- import { join } from 'node:path';
38
+ import { isAbsolute, join } from 'node:path';
39
39
  import z from '@deepseek-ai/schemastery';
40
40
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
41
41
  import { settingsNamespace } from '@deepseek-ai/dsh-settings';
@@ -43,17 +43,23 @@ import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "
43
43
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
44
44
  import { downloadedAttachment } from "./sdk-adapter.js";
45
45
  import { registerAwikiTools } from "./tools.js";
46
+ import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
46
47
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
47
48
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
48
49
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
49
50
  import { createAwikiSettingsRpcHandler } from "./settings-rpc.js";
50
51
  import { AwikiSessionStore } from "./session.js";
52
+ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "./attachment-cache.js";
53
+ import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
54
+ import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
51
55
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
52
56
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
53
57
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
54
- export { AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, } from "./tools.js";
58
+ 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";
55
59
  /** Default maximum attachment size: 10 MiB. */
56
60
  export const DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
61
+ /** Default private on-disk budget for verified image previews: 64 MiB. */
62
+ export const DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
57
63
  /** Default browser polling interval while the AWiki drawer is open. */
58
64
  export const DEFAULT_POLL_INTERVAL_MS = 3_000;
59
65
  /** Default AWiki production service origin. */
@@ -69,13 +75,18 @@ export const Config = z.object({
69
75
  userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
70
76
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
71
77
  messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
78
+ mailServiceUrl: z.string(),
72
79
  messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
73
80
  messageServiceDid: z.string().default(DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
74
81
  allowedAttachmentOrigins: z.array(z.string()).default([]),
75
82
  allowInsecureLoopbackForTesting: z.boolean().default(false),
76
83
  stateRoot: z.string(),
77
84
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
85
+ imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
78
86
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
87
+ listenerEnabled: z.boolean().default(false),
88
+ listenerAllowedPeers: z.array(z.string()).default([]),
89
+ listenerWorkspacePath: z.string(),
79
90
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES),
80
91
  });
81
92
  const FAILURE_CODES = new Set([
@@ -90,15 +101,22 @@ const FAILURE_CODES = new Set([
90
101
  'forbidden',
91
102
  'conflict',
92
103
  'rate-limited',
104
+ 'group-membership-required',
105
+ 'group-identity-stale',
93
106
  'attachment-too-large',
94
107
  'summary-unavailable',
95
108
  'summary-timeout',
96
109
  'summary-cancelled',
97
110
  'summary-invalid-output',
98
111
  'summary-failed',
112
+ 'delivery-unknown',
99
113
  'network',
100
114
  'remote',
101
115
  ]);
116
+ const LISTENER_RESTART_BASE_DELAY_MS = 1_000;
117
+ const LISTENER_RESTART_MAX_DELAY_MS = 30_000;
118
+ const LISTENER_RESTART_MAX_ATTEMPT = 6;
119
+ const LISTENER_STABLE_RESET_MS = 60_000;
102
120
  const FAILURE_MESSAGES = {
103
121
  'not-registered': 'No AWiki identity is registered for this deployment.',
104
122
  'signed-out': 'This installation is signed out of AWiki.',
@@ -111,12 +129,15 @@ const FAILURE_MESSAGES = {
111
129
  'forbidden': 'The AWiki operation is not permitted.',
112
130
  'conflict': 'The AWiki operation conflicts with current state.',
113
131
  'rate-limited': 'The AWiki service rate-limited the request.',
132
+ 'group-membership-required': 'The active AWiki identity is not a member of this group.',
133
+ 'group-identity-stale': 'The AWiki group identity binding is still recovering.',
114
134
  'attachment-too-large': 'The attachment exceeds this deployment\'s size limit.',
115
135
  'summary-unavailable': 'AI summary is unavailable. Check the current default model configuration.',
116
136
  'summary-timeout': 'AI summary timed out. Try again.',
117
137
  'summary-cancelled': 'AI summary was cancelled. Try again.',
118
138
  'summary-invalid-output': 'The model returned an invalid summary. Try again.',
119
139
  'summary-failed': 'AI summary could not be generated. Try again.',
140
+ 'delivery-unknown': 'Mail delivery could not be confirmed. Inspect the mailbox before retrying.',
120
141
  'network': 'The AWiki service could not be reached.',
121
142
  'remote': 'The AWiki service rejected the operation.',
122
143
  };
@@ -170,42 +191,83 @@ function attachmentOrigins(raw, messageServicePublicUrl, allowInsecureLoopbackFo
170
191
  }
171
192
  return origins;
172
193
  }
194
+ /** Resolve the explicit listener allowlist without accepting wildcards or ambiguous whitespace. */
195
+ function listenerAllowedPeers(raw, enabled) {
196
+ const peers = (raw ?? []).map((peer) => {
197
+ if (peer !== peer.trim() || peer.length === 0 || peer.length > 2_048 || /[\u0000-\u001f\u007f]/u.test(peer)) {
198
+ throw new TypeError('awiki: listenerAllowedPeers entries must be non-empty exact Handles or DIDs');
199
+ }
200
+ if (peer === '*')
201
+ throw new TypeError('awiki: listenerAllowedPeers does not accept wildcards');
202
+ return peer.startsWith('did:') ? peer : peer.toLowerCase();
203
+ });
204
+ if (peers.length > 100 || new Set(peers).size !== peers.length) {
205
+ throw new TypeError('awiki: listenerAllowedPeers must contain at most 100 unique entries');
206
+ }
207
+ if (enabled && peers.length === 0) {
208
+ throw new TypeError('awiki: listenerAllowedPeers must contain at least one Handle or DID when listenerEnabled is true');
209
+ }
210
+ return peers;
211
+ }
173
212
  /** Resolve and validate every deployment choice before publishing the service. */
174
213
  function resolveConfig(config) {
175
214
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
176
215
  const configuredStateRoot = config.stateRoot?.trim();
177
216
  const configuredDshHome = process.env.DSH_HOME?.trim();
217
+ const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
218
+ ? join(homedir(), '.dsh')
219
+ : configuredDshHome;
178
220
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
179
- ? join(configuredDshHome === undefined || configuredDshHome.length === 0 ? join(homedir(), '.dsh') : configuredDshHome, 'awiki', 'im-core')
221
+ ? join(dshHome, 'awiki', 'im-core')
180
222
  : configuredStateRoot;
181
- if (stateRoot.length === 0)
182
- throw new TypeError('awiki: stateRoot must be non-empty');
223
+ if (!isAbsolute(stateRoot))
224
+ throw new TypeError('awiki: stateRoot must be an absolute path');
183
225
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
184
226
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) {
185
227
  throw new TypeError('awiki: attachmentMaxBytes must be a positive safe integer');
186
228
  }
229
+ const imageAttachmentCacheMaxBytes = config.imageAttachmentCacheMaxBytes ?? DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES;
230
+ if (!Number.isSafeInteger(imageAttachmentCacheMaxBytes)
231
+ || imageAttachmentCacheMaxBytes < minimumImageAttachmentCacheMaxBytes(attachmentMaxBytes)) {
232
+ throw new TypeError('awiki: imageAttachmentCacheMaxBytes cannot retain one maximum-sized attachment');
233
+ }
187
234
  const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
188
235
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1_000 || pollIntervalMs > 60_000) {
189
236
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
190
237
  }
238
+ const listenerEnabled = config.listenerEnabled ?? false;
239
+ const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
240
+ if (!isAbsolute(listenerWorkspacePath)) {
241
+ throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
242
+ }
243
+ const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
191
244
  const summaryMaxInputBytes = config.summaryMaxInputBytes ?? DEFAULT_SUMMARY_MAX_INPUT_BYTES;
192
245
  if (!Number.isSafeInteger(summaryMaxInputBytes) || summaryMaxInputBytes < 1_024) {
193
246
  throw new TypeError('awiki: summaryMaxInputBytes must be a safe integer of at least 1024');
194
247
  }
195
248
  const userServiceUrl = serviceUrl('userServiceUrl', config.userServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
196
249
  const messageServiceUrl = serviceUrl('messageServiceUrl', config.messageServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
250
+ const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
197
251
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
198
252
  return {
199
253
  userServiceUrl,
200
254
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
201
255
  messageServiceUrl,
256
+ mailServiceUrl,
202
257
  messageServicePublicUrl,
203
258
  messageServiceDid: serviceDid(config.messageServiceDid ?? DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
204
259
  allowedAttachmentOrigins: attachmentOrigins(config.allowedAttachmentOrigins, messageServicePublicUrl, allowInsecureLoopbackForTesting),
205
260
  allowInsecureLoopbackForTesting,
206
261
  stateRoot,
207
262
  attachmentMaxBytes,
263
+ imageAttachmentCacheMaxBytes,
208
264
  pollIntervalMs,
265
+ listenerEnabled,
266
+ listener: {
267
+ allowedPeers,
268
+ workspacePath: listenerWorkspacePath,
269
+ stateRoot,
270
+ },
209
271
  summaryMaxInputBytes,
210
272
  };
211
273
  }
@@ -234,6 +296,208 @@ function normalizeFailure(error) {
234
296
  }
235
297
  return failure('remote');
236
298
  }
299
+ const MAX_GROUP_NAME_CHARACTERS = 100;
300
+ const MAX_GROUP_INITIAL_MEMBERS = 50;
301
+ const MAX_GROUP_MEMBER_CHARACTERS = 512;
302
+ const MAX_PROFILE_DISPLAY_NAME_CHARACTERS = 50;
303
+ const MAX_PROFILE_BIO_CHARACTERS = 100;
304
+ const MAX_PROFILE_TAGS = 5;
305
+ const MAX_PROFILE_TAG_CHARACTERS = 30;
306
+ const MAX_MESSAGE_CHARACTERS = 20_000;
307
+ function normalizeCreateGroupRequest(request) {
308
+ if (typeof request?.name !== 'string' || !Array.isArray(request.members))
309
+ return undefined;
310
+ const name = request.name.trim();
311
+ if (name.length === 0 || Array.from(name).length > MAX_GROUP_NAME_CHARACTERS)
312
+ return undefined;
313
+ if (request.members.length > MAX_GROUP_INITIAL_MEMBERS)
314
+ return undefined;
315
+ const members = [];
316
+ const seen = new Set();
317
+ for (const raw of request.members) {
318
+ if (typeof raw !== 'string')
319
+ return undefined;
320
+ const member = raw.trim().replace(/^@+/u, '');
321
+ if (member.length === 0 || Array.from(member).length > MAX_GROUP_MEMBER_CHARACTERS)
322
+ return undefined;
323
+ if (seen.has(member))
324
+ continue;
325
+ seen.add(member);
326
+ members.push(member);
327
+ }
328
+ return { name, members };
329
+ }
330
+ function normalizeMember(value) {
331
+ if (typeof value !== 'string')
332
+ return undefined;
333
+ const member = value.trim().replace(/^@+/u, '');
334
+ return member.length > 0 && Array.from(member).length <= MAX_GROUP_MEMBER_CHARACTERS
335
+ ? member
336
+ : undefined;
337
+ }
338
+ function normalizeGroupDid(value) {
339
+ if (typeof value !== 'string' || !value.startsWith('did:') || value.length > 2_048)
340
+ return undefined;
341
+ return value;
342
+ }
343
+ /** Rebuild the browser-safe recovery DTO instead of trusting provider object shape. */
344
+ function publicGroupRebindRecoverySummary(value) {
345
+ if (typeof value !== 'object' || value === null)
346
+ return undefined;
347
+ const candidate = value;
348
+ const counts = [candidate.processed, candidate.completed, candidate.pending, candidate.blocked];
349
+ if (counts.some(count => !Number.isSafeInteger(count) || count < 0 || count > 0xffff_ffff)) {
350
+ return undefined;
351
+ }
352
+ return {
353
+ processed: candidate.processed,
354
+ completed: candidate.completed,
355
+ pending: candidate.pending,
356
+ blocked: candidate.blocked,
357
+ };
358
+ }
359
+ function normalizeProfileRequest(request) {
360
+ if (typeof request?.displayName !== 'string' || typeof request.bio !== 'string' || !Array.isArray(request.tags))
361
+ return undefined;
362
+ const displayName = request.displayName.trim();
363
+ const bio = request.bio.trim();
364
+ if (displayName.length === 0
365
+ || Array.from(displayName).length > MAX_PROFILE_DISPLAY_NAME_CHARACTERS
366
+ || Array.from(bio).length > MAX_PROFILE_BIO_CHARACTERS
367
+ || request.tags.length > MAX_PROFILE_TAGS)
368
+ return undefined;
369
+ const tags = [];
370
+ const seen = new Set();
371
+ for (const raw of request.tags) {
372
+ if (typeof raw !== 'string')
373
+ return undefined;
374
+ const tag = raw.trim();
375
+ const key = tag.toLocaleLowerCase();
376
+ if (tag.length === 0 || Array.from(tag).length > MAX_PROFILE_TAG_CHARACTERS || seen.has(key))
377
+ return undefined;
378
+ seen.add(key);
379
+ tags.push(tag);
380
+ }
381
+ return { displayName, bio, tags };
382
+ }
383
+ function normalizeRecoveryOperation(request) {
384
+ if (typeof request?.operationId !== 'string')
385
+ return undefined;
386
+ const operationId = request.operationId.trim();
387
+ return operationId.length > 0 && operationId.length <= 512 ? { operationId } : undefined;
388
+ }
389
+ function normalizeRecoveryOtpRequest(request) {
390
+ if (typeof request?.fullHandle !== 'string' || typeof request.phone !== 'string')
391
+ return undefined;
392
+ const fullHandle = request.fullHandle.trim().replace(/^@+/u, '');
393
+ const phone = request.phone.trim();
394
+ if (fullHandle.length === 0 || fullHandle.length > 512 || phone.length < 5 || phone.length > 32)
395
+ return undefined;
396
+ return { fullHandle, phone };
397
+ }
398
+ function normalizeRecoveryPrepareRequest(request) {
399
+ const operation = normalizeRecoveryOperation(request);
400
+ if (operation === undefined || typeof request.phone !== 'string' || typeof request.otp !== 'string')
401
+ return undefined;
402
+ const phone = request.phone.trim();
403
+ const otp = request.otp.trim();
404
+ if (phone.length < 5 || phone.length > 32 || !/^\d{4,12}$/u.test(otp))
405
+ return undefined;
406
+ return { ...operation, phone, otp };
407
+ }
408
+ const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
409
+ /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
410
+ async function readBoundedResponseText(response, maxBytes) {
411
+ const declaredLength = response.headers.get('content-length');
412
+ if (declaredLength !== null && /^\d+$/u.test(declaredLength) && Number(declaredLength) > maxBytes)
413
+ return undefined;
414
+ if (response.body === null)
415
+ return '';
416
+ const reader = response.body.getReader();
417
+ const chunks = [];
418
+ let length = 0;
419
+ try {
420
+ while (true) {
421
+ const result = await reader.read();
422
+ if (result.done)
423
+ break;
424
+ length += result.value.byteLength;
425
+ if (length > maxBytes) {
426
+ await reader.cancel();
427
+ return undefined;
428
+ }
429
+ chunks.push(Uint8Array.from(result.value));
430
+ }
431
+ }
432
+ catch {
433
+ return undefined;
434
+ }
435
+ finally {
436
+ reader.releaseLock();
437
+ }
438
+ const bytes = new Uint8Array(length);
439
+ let offset = 0;
440
+ for (const chunk of chunks) {
441
+ bytes.set(chunk, offset);
442
+ offset += chunk.byteLength;
443
+ }
444
+ return Buffer.from(bytes).toString('utf8');
445
+ }
446
+ /** Resolve one configured-domain Handle without widening registration authority. */
447
+ function identityAccessTarget(request, configuredDomain) {
448
+ if (typeof request?.handle !== 'string')
449
+ return undefined;
450
+ const raw = request.handle.trim();
451
+ if (raw.length === 0 || raw.length > 255 || /[\u0000-\u001f\u007f]/u.test(raw))
452
+ return undefined;
453
+ const lowered = raw.replace(/[A-Z]/gu, character => character.toLowerCase());
454
+ const handle = lowered.startsWith('wba://') ? lowered.slice('wba://'.length) : lowered;
455
+ const dot = handle.indexOf('.');
456
+ const localPart = (dot < 0 ? handle : handle.slice(0, dot)).trim();
457
+ const domain = (dot < 0 ? configuredDomain : handle.slice(dot + 1)).trim().replace(/\.$/u, '');
458
+ if (localPart.length === 0 || domain !== configuredDomain)
459
+ return undefined;
460
+ return { localPart, fullHandle: `${localPart}.${configuredDomain}` };
461
+ }
462
+ function normalizeSendTextRequest(request) {
463
+ if (typeof request?.text !== 'string' || typeof request.idempotencyKey !== 'string'
464
+ || typeof request.target !== 'object' || request.target === null)
465
+ return undefined;
466
+ const text = request.text;
467
+ const codePoints = Array.from(text);
468
+ if (text.trim().length === 0 || codePoints.length > MAX_MESSAGE_CHARACTERS
469
+ || request.idempotencyKey.length === 0 || request.idempotencyKey.length > 512)
470
+ return undefined;
471
+ if (request.target.kind === 'direct') {
472
+ if (typeof request.target.peer !== 'string' || request.mentions !== undefined)
473
+ return undefined;
474
+ }
475
+ else if (request.target.kind === 'group') {
476
+ if (typeof request.target.group !== 'string')
477
+ return undefined;
478
+ }
479
+ else
480
+ return undefined;
481
+ if (request.mentions === undefined)
482
+ return request;
483
+ if (!Array.isArray(request.mentions) || request.mentions.length === 0 || request.mentions.length > 100)
484
+ return undefined;
485
+ const ids = new Set();
486
+ let previousEnd = 0;
487
+ const mentions = [...request.mentions].sort((left, right) => left.start - right.start || left.end - right.end);
488
+ for (const mention of mentions) {
489
+ if (typeof mention !== 'object' || mention === null
490
+ || typeof mention.id !== 'string' || mention.id.trim() === '' || ids.has(mention.id)
491
+ || !Number.isSafeInteger(mention.start) || !Number.isSafeInteger(mention.end)
492
+ || mention.start < previousEnd || mention.start < 0 || mention.end <= mention.start || mention.end > codePoints.length
493
+ || typeof mention.did !== 'string' || !mention.did.startsWith('did:')
494
+ || (mention.displayName !== undefined && typeof mention.displayName !== 'string'))
495
+ return undefined;
496
+ ids.add(mention.id);
497
+ previousEnd = mention.end;
498
+ }
499
+ return { ...request, mentions };
500
+ }
237
501
  /** Normalize summary-provider failures without returning prompts, model output, routes, or causes. */
238
502
  function normalizeSummaryFailure(error) {
239
503
  if (error instanceof SummaryProviderUnavailableError)
@@ -367,17 +631,40 @@ let AwikiService = (() => {
367
631
  let _getSession_decorators;
368
632
  let _logout_decorators;
369
633
  let _login_decorators;
634
+ let _inspectIdentityAccess_decorators;
370
635
  let _sendRegistrationOtp_decorators;
371
636
  let _registerIdentity_decorators;
372
637
  let _updateDisplayName_decorators;
638
+ let _getProfile_decorators;
639
+ let _updateProfile_decorators;
640
+ let _sendRecoveryOtp_decorators;
641
+ let _prepareRecovery_decorators;
642
+ let _activateRecovery_decorators;
643
+ let _getRecoveryStatus_decorators;
644
+ let _resumeRecovery_decorators;
645
+ let _discardRecovery_decorators;
373
646
  let _resolvePeer_decorators;
647
+ let _createGroup_decorators;
648
+ let _getGroup_decorators;
649
+ let _joinGroup_decorators;
650
+ let _leaveGroup_decorators;
651
+ let _listGroupMembers_decorators;
652
+ let _addGroupMember_decorators;
653
+ let _removeGroupMember_decorators;
654
+ let _resumeGroupRebindRecovery_decorators;
374
655
  let _listConversations_decorators;
375
656
  let _getHistory_decorators;
657
+ let _getLocalHistory_decorators;
376
658
  let _summarizeConversation_decorators;
377
659
  let _markConversationRead_decorators;
378
660
  let _sendText_decorators;
379
661
  let _sendAttachment_decorators;
380
662
  let _downloadAttachment_decorators;
663
+ let _getMailAccount_decorators;
664
+ let _listMailInbox_decorators;
665
+ let _readMail_decorators;
666
+ let _markMailRead_decorators;
667
+ let _sendMail_decorators;
381
668
  let _clearLocalData_decorators;
382
669
  return class AwikiService extends _classSuper {
383
670
  static {
@@ -387,34 +674,80 @@ let AwikiService = (() => {
387
674
  _getSession_decorators = [Remote];
388
675
  _logout_decorators = [Remote];
389
676
  _login_decorators = [Remote];
677
+ _inspectIdentityAccess_decorators = [Remote];
390
678
  _sendRegistrationOtp_decorators = [Remote];
391
679
  _registerIdentity_decorators = [Remote];
392
680
  _updateDisplayName_decorators = [Remote];
681
+ _getProfile_decorators = [Remote];
682
+ _updateProfile_decorators = [Remote];
683
+ _sendRecoveryOtp_decorators = [Remote];
684
+ _prepareRecovery_decorators = [Remote];
685
+ _activateRecovery_decorators = [Remote];
686
+ _getRecoveryStatus_decorators = [Remote];
687
+ _resumeRecovery_decorators = [Remote];
688
+ _discardRecovery_decorators = [Remote];
393
689
  _resolvePeer_decorators = [Remote];
690
+ _createGroup_decorators = [Remote];
691
+ _getGroup_decorators = [Remote];
692
+ _joinGroup_decorators = [Remote];
693
+ _leaveGroup_decorators = [Remote];
694
+ _listGroupMembers_decorators = [Remote];
695
+ _addGroupMember_decorators = [Remote];
696
+ _removeGroupMember_decorators = [Remote];
697
+ _resumeGroupRebindRecovery_decorators = [Remote];
394
698
  _listConversations_decorators = [Remote];
395
699
  _getHistory_decorators = [Remote];
700
+ _getLocalHistory_decorators = [Remote];
396
701
  _summarizeConversation_decorators = [Remote];
397
702
  _markConversationRead_decorators = [Remote];
398
703
  _sendText_decorators = [Remote];
399
704
  _sendAttachment_decorators = [Remote];
400
705
  _downloadAttachment_decorators = [Remote];
706
+ _getMailAccount_decorators = [Remote];
707
+ _listMailInbox_decorators = [Remote];
708
+ _readMail_decorators = [Remote];
709
+ _markMailRead_decorators = [Remote];
710
+ _sendMail_decorators = [Remote];
401
711
  _clearLocalData_decorators = [Remote];
402
712
  __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);
403
713
  __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);
404
714
  __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);
405
715
  __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);
406
716
  __esDecorate(this, null, _login_decorators, { kind: "method", name: "login", static: false, private: false, access: { has: obj => "login" in obj, get: obj => obj.login }, metadata: _metadata }, null, _instanceExtraInitializers);
717
+ __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);
407
718
  __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);
408
719
  __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);
409
720
  __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);
721
+ __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);
722
+ __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);
723
+ __esDecorate(this, null, _sendRecoveryOtp_decorators, { kind: "method", name: "sendRecoveryOtp", static: false, private: false, access: { has: obj => "sendRecoveryOtp" in obj, get: obj => obj.sendRecoveryOtp }, metadata: _metadata }, null, _instanceExtraInitializers);
724
+ __esDecorate(this, null, _prepareRecovery_decorators, { kind: "method", name: "prepareRecovery", static: false, private: false, access: { has: obj => "prepareRecovery" in obj, get: obj => obj.prepareRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
725
+ __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);
726
+ __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);
727
+ __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);
728
+ __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);
410
729
  __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);
730
+ __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);
731
+ __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);
732
+ __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);
733
+ __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);
734
+ __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);
735
+ __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);
736
+ __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);
737
+ __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);
411
738
  __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);
412
739
  __esDecorate(this, null, _getHistory_decorators, { kind: "method", name: "getHistory", static: false, private: false, access: { has: obj => "getHistory" in obj, get: obj => obj.getHistory }, metadata: _metadata }, null, _instanceExtraInitializers);
740
+ __esDecorate(this, null, _getLocalHistory_decorators, { kind: "method", name: "getLocalHistory", static: false, private: false, access: { has: obj => "getLocalHistory" in obj, get: obj => obj.getLocalHistory }, metadata: _metadata }, null, _instanceExtraInitializers);
413
741
  __esDecorate(this, null, _summarizeConversation_decorators, { kind: "method", name: "summarizeConversation", static: false, private: false, access: { has: obj => "summarizeConversation" in obj, get: obj => obj.summarizeConversation }, metadata: _metadata }, null, _instanceExtraInitializers);
414
742
  __esDecorate(this, null, _markConversationRead_decorators, { kind: "method", name: "markConversationRead", static: false, private: false, access: { has: obj => "markConversationRead" in obj, get: obj => obj.markConversationRead }, metadata: _metadata }, null, _instanceExtraInitializers);
415
743
  __esDecorate(this, null, _sendText_decorators, { kind: "method", name: "sendText", static: false, private: false, access: { has: obj => "sendText" in obj, get: obj => obj.sendText }, metadata: _metadata }, null, _instanceExtraInitializers);
416
744
  __esDecorate(this, null, _sendAttachment_decorators, { kind: "method", name: "sendAttachment", static: false, private: false, access: { has: obj => "sendAttachment" in obj, get: obj => obj.sendAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
417
745
  __esDecorate(this, null, _downloadAttachment_decorators, { kind: "method", name: "downloadAttachment", static: false, private: false, access: { has: obj => "downloadAttachment" in obj, get: obj => obj.downloadAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
746
+ __esDecorate(this, null, _getMailAccount_decorators, { kind: "method", name: "getMailAccount", static: false, private: false, access: { has: obj => "getMailAccount" in obj, get: obj => obj.getMailAccount }, metadata: _metadata }, null, _instanceExtraInitializers);
747
+ __esDecorate(this, null, _listMailInbox_decorators, { kind: "method", name: "listMailInbox", static: false, private: false, access: { has: obj => "listMailInbox" in obj, get: obj => obj.listMailInbox }, metadata: _metadata }, null, _instanceExtraInitializers);
748
+ __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);
749
+ __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);
750
+ __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);
418
751
  __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);
419
752
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
420
753
  }
@@ -422,25 +755,33 @@ let AwikiService = (() => {
422
755
  static Config = Config;
423
756
  resolved = __runInitializers(this, _instanceExtraInitializers);
424
757
  sessionStore;
758
+ imageAttachmentCache;
759
+ sentMailStore;
425
760
  startupUserServiceDomain;
426
761
  settingsProvider;
427
762
  provider;
428
763
  signedOut;
429
764
  sessionMutation = Promise.resolve();
430
765
  sessionRevision = 0;
766
+ activeIdentityDid;
431
767
  activeSummaryRequests = new Set();
432
768
  summaryProvider;
769
+ hostContext;
433
770
  /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
434
771
  externalHttpAuth;
772
+ workspaceContext;
435
773
  /**
436
774
  * @param ctx - owning Host context.
437
775
  * @param config - service endpoints, SDK state path, and public limits.
438
776
  */
439
777
  constructor(ctx, config) {
440
778
  super(ctx, 'awiki');
779
+ this.hostContext = ctx;
441
780
  this.resolved = resolveConfig(config);
442
781
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
443
782
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
783
+ this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
784
+ this.sentMailStore = new AwikiSentMailStore(this.resolved.stateRoot);
444
785
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
445
786
  ctx.inject(['settings'], (settingsCtx) => {
446
787
  const provider = settingsCtx.settings;
@@ -461,6 +802,20 @@ let AwikiService = (() => {
461
802
  ctx.inject(['connection'], (connectionCtx) => {
462
803
  connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
463
804
  });
805
+ ctx.inject(['workspaceRegistry'], (workspaceCtx) => {
806
+ this.workspaceContext = workspaceCtx;
807
+ const provider = this.provider;
808
+ if (provider !== undefined)
809
+ void this.startListener(provider);
810
+ workspaceCtx.effect(() => async () => {
811
+ if (this.workspaceContext !== workspaceCtx)
812
+ return;
813
+ this.workspaceContext = undefined;
814
+ const current = this.provider;
815
+ if (current !== undefined)
816
+ await this.stopListener(current);
817
+ }, 'awiki: release Workspace listener composition');
818
+ });
464
819
  registerAwikiTools(ctx, this);
465
820
  ctx.effect(() => async () => {
466
821
  const provider = this.provider;
@@ -485,6 +840,7 @@ let AwikiService = (() => {
485
840
  userServiceUrl: this.resolved.userServiceUrl,
486
841
  userServiceDomain: this.startupUserServiceDomain,
487
842
  messageServiceUrl: this.resolved.messageServiceUrl,
843
+ mailServiceUrl: this.resolved.mailServiceUrl,
488
844
  messageServicePublicUrl: this.resolved.messageServicePublicUrl,
489
845
  messageServiceDid: this.resolved.messageServiceDid,
490
846
  allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
@@ -492,8 +848,14 @@ let AwikiService = (() => {
492
848
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
493
849
  stateRoot: this.resolved.stateRoot,
494
850
  });
495
- const provider = { client };
851
+ const provider = {
852
+ client,
853
+ listenerRestartAttempt: 0,
854
+ listenerGeneration: 0,
855
+ listenerRecoveryBlocked: false,
856
+ };
496
857
  this.provider = provider;
858
+ void this.startListener(provider);
497
859
  return () => this.disposeProvider(provider);
498
860
  }
499
861
  /** Register one replaceable conversation-summary provider for this deployment. */
@@ -527,16 +889,22 @@ let AwikiService = (() => {
527
889
  * Read the deployment's identity status.
528
890
  * @returns The public deployment identity or `null`.
529
891
  */
530
- getIdentity() {
531
- return this.run(client => client.getIdentity());
892
+ async getIdentity() {
893
+ const result = await this.run(client => client.getIdentity());
894
+ if (result.ok)
895
+ this.activeIdentityDid = result.value?.did;
896
+ return result;
532
897
  }
533
898
  /** Return the local registration and sign-in state without exposing secrets. */
534
899
  async getSession() {
535
- if (await this.isSignedOut())
900
+ if (await this.isSignedOut()) {
901
+ this.activeIdentityDid = undefined;
536
902
  return { ok: true, value: { status: 'signed-out' } };
903
+ }
537
904
  const identity = await this.run(client => client.getIdentity(), { allowSignedOut: true });
538
905
  if (!identity.ok)
539
906
  return identity;
907
+ this.activeIdentityDid = identity.value?.did;
540
908
  return identity.value === null
541
909
  ? { ok: true, value: { status: 'unregistered' } }
542
910
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -557,8 +925,14 @@ let AwikiService = (() => {
557
925
  try {
558
926
  await this.sessionStore.signOut();
559
927
  this.signedOut = true;
928
+ this.activeIdentityDid = undefined;
560
929
  this.invalidateSummaries();
561
- return { ok: true, value: { status: 'signed-out' } };
930
+ const session = { status: 'signed-out' };
931
+ this.publishSession(session);
932
+ const provider = this.provider;
933
+ if (provider !== undefined)
934
+ await this.stopListener(provider);
935
+ return { ok: true, value: session };
562
936
  }
563
937
  catch {
564
938
  return { ok: false, error: failure('remote') };
@@ -576,19 +950,70 @@ let AwikiService = (() => {
576
950
  try {
577
951
  await this.sessionStore.signIn();
578
952
  this.signedOut = false;
953
+ this.activeIdentityDid = identity.value.did;
579
954
  this.invalidateSummaries();
580
- return { ok: true, value: { status: 'active', identity: identity.value } };
955
+ const session = { status: 'active', identity: identity.value };
956
+ this.publishSession(session);
957
+ const provider = this.provider;
958
+ if (provider !== undefined)
959
+ void this.startListener(provider);
960
+ return { ok: true, value: session };
581
961
  }
582
962
  catch {
583
963
  return { ok: false, error: failure('remote') };
584
964
  }
585
965
  });
586
966
  }
587
- /**
588
- * Send one Legacy registration verification code.
589
- * @param request - Handle and phone used for the registration challenge.
590
- * @returns Public retry timing or a closed failure.
591
- */
967
+ /** Classify one configured-domain Handle before selecting the registration or recovery OTP purpose. */
968
+ async inspectIdentityAccess(request) {
969
+ const target = identityAccessTarget(request, this.startupUserServiceDomain);
970
+ if (target === undefined)
971
+ return { ok: false, error: failure('invalid-request') };
972
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.resolved.userServiceUrl);
973
+ const abort = new AbortController();
974
+ const timeout = setTimeout(() => { abort.abort(); }, 10_000);
975
+ let response;
976
+ try {
977
+ response = await fetch(endpoint, {
978
+ method: 'GET',
979
+ headers: { accept: 'application/json', 'cache-control': 'no-store' },
980
+ cache: 'no-store',
981
+ redirect: 'error',
982
+ signal: abort.signal,
983
+ });
984
+ }
985
+ catch {
986
+ return { ok: false, error: failure('network') };
987
+ }
988
+ finally {
989
+ clearTimeout(timeout);
990
+ }
991
+ if (response.status === 404) {
992
+ return { ok: true, value: { status: 'available', fullHandle: target.fullHandle } };
993
+ }
994
+ if (!response.ok)
995
+ return { ok: false, error: failure('remote') };
996
+ try {
997
+ const text = await readBoundedResponseText(response, IDENTITY_ACCESS_RESPONSE_MAX_BYTES);
998
+ if (text === undefined)
999
+ return { ok: false, error: failure('remote') };
1000
+ const value = JSON.parse(text);
1001
+ if (typeof value !== 'object' || value === null)
1002
+ return { ok: false, error: failure('remote') };
1003
+ const binding = value;
1004
+ if (binding.handle !== target.fullHandle
1005
+ || typeof binding.did !== 'string'
1006
+ || !binding.did.startsWith('did:')
1007
+ || typeof binding.status !== 'string'
1008
+ || binding.status.length === 0) {
1009
+ return { ok: false, error: failure('remote') };
1010
+ }
1011
+ return { ok: true, value: { status: 'existing', fullHandle: target.fullHandle } };
1012
+ }
1013
+ catch {
1014
+ return { ok: false, error: failure('remote') };
1015
+ }
1016
+ }
592
1017
  sendRegistrationOtp(request) {
593
1018
  return this.run(client => client.sendRegistrationOtp(request));
594
1019
  }
@@ -597,16 +1022,88 @@ let AwikiService = (() => {
597
1022
  * @param request - Handle, phone, and verification code for registration.
598
1023
  * @returns The new public identity or a closed failure.
599
1024
  */
600
- registerIdentity(request) {
601
- return this.run(client => client.registerIdentity(request));
1025
+ async registerIdentity(request) {
1026
+ const result = await this.run(client => client.registerIdentity(request));
1027
+ if (result.ok)
1028
+ await this.activateRegisteredIdentity(result.value);
1029
+ return result;
602
1030
  }
603
1031
  /**
604
1032
  * Update the deployment identity's public WNS display name.
605
1033
  * @param request - replacement display name selected by the user.
606
1034
  * @returns The updated public identity or a closed failure.
607
1035
  */
608
- updateDisplayName(request) {
609
- return this.run(client => client.updateDisplayName(request));
1036
+ async updateDisplayName(request) {
1037
+ const result = await this.run(client => client.updateDisplayName(request));
1038
+ if (result.ok)
1039
+ this.activeIdentityDid = result.value.did;
1040
+ return result;
1041
+ }
1042
+ /** Return the public editable profile for the active identity. */
1043
+ getProfile() {
1044
+ return this.run(client => client.getProfile());
1045
+ }
1046
+ /** Update Display Name, bio, and tags after applying product limits in the Host. */
1047
+ updateProfile(request) {
1048
+ const normalized = normalizeProfileRequest(request);
1049
+ if (normalized === undefined)
1050
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1051
+ return this.run(client => client.updateProfile(normalized));
1052
+ }
1053
+ /** Start durable phone recovery for one existing full Handle. */
1054
+ sendRecoveryOtp(request) {
1055
+ const normalized = normalizeRecoveryOtpRequest(request);
1056
+ if (normalized === undefined)
1057
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1058
+ return this.run(client => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
1059
+ }
1060
+ /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
1061
+ prepareRecovery(request) {
1062
+ const normalized = normalizeRecoveryPrepareRequest(request);
1063
+ if (normalized === undefined)
1064
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1065
+ return this.run(client => client.prepareRecovery(normalized), { allowSignedOut: true });
1066
+ }
1067
+ /** Attempt one prepared recovery commit; uncertain outcomes remain durable in Core. */
1068
+ async activateRecovery(request) {
1069
+ const normalized = normalizeRecoveryOperation(request);
1070
+ if (normalized === undefined)
1071
+ return { ok: false, error: failure('invalid-request') };
1072
+ const result = await this.run(client => client.activateRecovery(normalized), { allowSignedOut: true });
1073
+ if (result.ok && !await this.applyRecoveredSession(result.value)) {
1074
+ return { ok: false, error: failure('remote') };
1075
+ }
1076
+ return result;
1077
+ }
1078
+ /** Read durable recovery state before deciding whether a retry is valid. */
1079
+ async getRecoveryStatus(request) {
1080
+ const normalized = normalizeRecoveryOperation(request);
1081
+ if (normalized === undefined)
1082
+ return { ok: false, error: failure('invalid-request') };
1083
+ const result = await this.run(client => client.getRecoveryStatus(normalized), { allowSignedOut: true });
1084
+ if (result.ok && !await this.applyRecoveredSession(result.value)) {
1085
+ return { ok: false, error: failure('remote') };
1086
+ }
1087
+ return result;
1088
+ }
1089
+ /** Resume only the exact Core-owned operation selected by the browser. */
1090
+ async resumeRecovery(request) {
1091
+ const normalized = normalizeRecoveryOperation(request);
1092
+ if (normalized === undefined)
1093
+ return { ok: false, error: failure('invalid-request') };
1094
+ const result = await this.run(client => client.resumeRecovery(normalized), { allowSignedOut: true });
1095
+ if (result.ok && !await this.applyRecoveredSession(result.value)) {
1096
+ return { ok: false, error: failure('remote') };
1097
+ }
1098
+ return result;
1099
+ }
1100
+ /** Discard only a pre-attempt operation; Core rejects post-attempt deletion. */
1101
+ async discardRecovery(request) {
1102
+ const normalized = normalizeRecoveryOperation(request);
1103
+ if (normalized === undefined)
1104
+ return { ok: false, error: failure('invalid-request') };
1105
+ const result = await this.run(client => client.discardRecovery(normalized), { allowSignedOut: true });
1106
+ return result.ok ? { ok: true, value: { completed: true } } : result;
610
1107
  }
611
1108
  /**
612
1109
  * Resolve one Handle or DID before the browser opens a direct chat.
@@ -616,6 +1113,96 @@ let AwikiService = (() => {
616
1113
  resolvePeer(request) {
617
1114
  return this.run(client => client.resolvePeer(request.peer));
618
1115
  }
1116
+ /**
1117
+ * Create one group, then settle every initial-member invitation without hiding a created group.
1118
+ * @param request - bounded group name and initial Handle/DID values.
1119
+ * @returns The created conversation and per-member outcomes.
1120
+ */
1121
+ async createGroup(request) {
1122
+ const normalized = normalizeCreateGroupRequest(request);
1123
+ if (normalized === undefined)
1124
+ return { ok: false, error: failure('invalid-request') };
1125
+ const created = await this.run(client => client.createGroup(normalized.name));
1126
+ if (!created.ok)
1127
+ return created;
1128
+ const addedMembers = [];
1129
+ const failedMembers = [];
1130
+ for (const member of normalized.members) {
1131
+ const result = await this.run(client => client.addGroupMember(created.value.groupDid, member));
1132
+ if (result.ok)
1133
+ addedMembers.push(result.value);
1134
+ else
1135
+ failedMembers.push({ member, error: result.error });
1136
+ }
1137
+ return {
1138
+ ok: true,
1139
+ value: { conversation: created.value, addedMembers, failedMembers },
1140
+ };
1141
+ }
1142
+ /** Return one authoritative group snapshot for permission-aware UI. */
1143
+ getGroup(request) {
1144
+ const groupDid = normalizeGroupDid(request?.groupDid);
1145
+ if (groupDid === undefined)
1146
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1147
+ return this.run(client => client.getGroup(groupDid));
1148
+ }
1149
+ /** Join one open group and return its authoritative membership state. */
1150
+ joinGroup(request) {
1151
+ const groupDid = normalizeGroupDid(request?.groupDid);
1152
+ if (groupDid === undefined)
1153
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1154
+ return this.run(client => client.joinGroup(groupDid));
1155
+ }
1156
+ /** Leave one group. Core rejects owner leave and unsupported security profiles. */
1157
+ async leaveGroup(request) {
1158
+ const groupDid = normalizeGroupDid(request?.groupDid);
1159
+ if (groupDid === undefined)
1160
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1161
+ const result = await this.run(client => client.leaveGroup(groupDid));
1162
+ return result.ok ? { ok: true, value: { completed: true } } : result;
1163
+ }
1164
+ /** Read one authoritative versioned member page. */
1165
+ listGroupMembers(request) {
1166
+ const groupDid = normalizeGroupDid(request?.groupDid);
1167
+ if (groupDid === undefined
1168
+ || (request.cursor !== undefined && (typeof request.cursor !== 'string' || request.cursor.length > 4_096))
1169
+ || (request.limit !== undefined && (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 100))) {
1170
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1171
+ }
1172
+ return this.run(client => client.listGroupMembers({
1173
+ groupDid,
1174
+ ...request.cursor === undefined ? {} : { cursor: request.cursor },
1175
+ ...request.limit === undefined ? {} : { limit: request.limit },
1176
+ }));
1177
+ }
1178
+ /** Invite one ordinary member after group creation. */
1179
+ addGroupMember(request) {
1180
+ const groupDid = normalizeGroupDid(request?.groupDid);
1181
+ const member = normalizeMember(request?.member);
1182
+ if (groupDid === undefined || member === undefined || (request.role !== undefined && request.role !== 'member')) {
1183
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1184
+ }
1185
+ return this.run(client => client.addGroupMember(groupDid, member));
1186
+ }
1187
+ /** Remove one member. The authoritative Core role check remains decisive. */
1188
+ removeGroupMember(request) {
1189
+ const groupDid = normalizeGroupDid(request?.groupDid);
1190
+ const member = normalizeMember(request?.member);
1191
+ if (groupDid === undefined || member === undefined) {
1192
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1193
+ }
1194
+ return this.run(client => client.removeGroupMember(groupDid, member));
1195
+ }
1196
+ /** Resume durable group membership convergence after account sync restores old groups. */
1197
+ async resumeGroupRebindRecovery() {
1198
+ const result = await this.run(client => client.resumeGroupRebindRecovery());
1199
+ if (!result.ok)
1200
+ return result;
1201
+ const summary = publicGroupRebindRecoverySummary(result.value);
1202
+ return summary === undefined
1203
+ ? { ok: false, error: failure('remote') }
1204
+ : { ok: true, value: summary };
1205
+ }
619
1206
  /**
620
1207
  * List direct and existing group conversations.
621
1208
  * @param request - Optional opaque cursor and page limit.
@@ -632,6 +1219,10 @@ let AwikiService = (() => {
632
1219
  getHistory(request) {
633
1220
  return this.run(client => client.getHistory(request));
634
1221
  }
1222
+ /** Read one committed local conversation page without sync, history, or Directory RPC. */
1223
+ getLocalHistory(request) {
1224
+ return this.run(client => client.getLocalHistory(request));
1225
+ }
635
1226
  /**
636
1227
  * Read real AWiki history, enforce range and byte caps, then invoke the configured model once.
637
1228
  * @param request - selected conversation and its unread snapshot at open time.
@@ -736,7 +1327,10 @@ let AwikiService = (() => {
736
1327
  * @returns The accepted public message or a closed failure.
737
1328
  */
738
1329
  sendText(request) {
739
- return this.run(client => client.sendText(request));
1330
+ const normalized = normalizeSendTextRequest(request);
1331
+ if (normalized === undefined)
1332
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1333
+ return this.run(client => client.sendText(normalized));
740
1334
  }
741
1335
  /**
742
1336
  * Upload and send one attachment after Host validation.
@@ -747,16 +1341,29 @@ let AwikiService = (() => {
747
1341
  const decoded = decodeAttachment(request.bytesBase64, this.resolved.attachmentMaxBytes);
748
1342
  if (!decoded.ok)
749
1343
  return decoded;
750
- return this.run(client => client.sendAttachment({
751
- target: request.target,
752
- attachment: {
753
- fileName: request.fileName,
754
- mimeType: request.mimeType,
755
- bytes: decoded.value,
756
- },
757
- ...request.caption === undefined ? {} : { caption: request.caption },
758
- idempotencyKey: request.idempotencyKey,
759
- }), { skipAttachmentByteValidation: true });
1344
+ return this.run(async (client) => {
1345
+ const message = await client.sendAttachment({
1346
+ target: request.target,
1347
+ attachment: {
1348
+ fileName: request.fileName,
1349
+ mimeType: request.mimeType,
1350
+ bytes: decoded.value,
1351
+ },
1352
+ ...request.caption === undefined ? {} : { caption: request.caption },
1353
+ idempotencyKey: request.idempotencyKey,
1354
+ });
1355
+ if (message.content.kind === 'attachment') {
1356
+ const ownerDid = this.activeIdentityDid ?? (await client.getIdentity().catch(() => null))?.did;
1357
+ if (ownerDid !== undefined) {
1358
+ this.activeIdentityDid = ownerDid;
1359
+ await this.imageAttachmentCache.write(ownerDid, message.id, {
1360
+ attachment: message.content.attachment,
1361
+ bytes: decoded.value,
1362
+ }).catch(() => undefined);
1363
+ }
1364
+ }
1365
+ return message;
1366
+ }, { skipAttachmentByteValidation: true });
760
1367
  }
761
1368
  /**
762
1369
  * Download and encode one provider-verified attachment.
@@ -764,16 +1371,112 @@ let AwikiService = (() => {
764
1371
  * @returns Verified public metadata and canonical Base64 bytes, or a closed failure.
765
1372
  */
766
1373
  async downloadAttachment(request) {
767
- const result = await this.run(client => client.downloadAttachment(request), { skipAttachmentByteValidation: true });
1374
+ try {
1375
+ if (await this.isSignedOut())
1376
+ return { ok: false, error: failure('signed-out') };
1377
+ }
1378
+ catch {
1379
+ return { ok: false, error: failure('remote') };
1380
+ }
1381
+ if (this.provider !== undefined && this.activeIdentityDid !== undefined) {
1382
+ const cached = await this.imageAttachmentCache.read(this.activeIdentityDid, request);
1383
+ if (cached !== undefined)
1384
+ return this.publicDownloadedAttachment(cached);
1385
+ }
1386
+ const result = await this.run(async (client) => {
1387
+ const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
1388
+ const ownerDid = this.activeIdentityDid ?? identity?.did;
1389
+ if (ownerDid === undefined)
1390
+ throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
1391
+ this.activeIdentityDid = ownerDid;
1392
+ const cached = await this.imageAttachmentCache.read(ownerDid, request);
1393
+ if (cached !== undefined)
1394
+ return cached;
1395
+ const downloaded = await client.downloadAttachment(request);
1396
+ await this.imageAttachmentCache.write(ownerDid, request.messageId, downloaded).catch(() => undefined);
1397
+ return downloaded;
1398
+ }, { skipAttachmentByteValidation: true });
768
1399
  if (!result.ok)
769
1400
  return result;
770
- if (result.value.bytes.byteLength > this.resolved.attachmentMaxBytes) {
1401
+ return this.publicDownloadedAttachment(result.value);
1402
+ }
1403
+ /** Revalidate cached/provider bytes before crossing the browser Remote boundary. */
1404
+ publicDownloadedAttachment(value) {
1405
+ if (value.bytes.byteLength > this.resolved.attachmentMaxBytes) {
771
1406
  return { ok: false, error: failure('attachment-too-large') };
772
1407
  }
773
- if (result.value.bytes.byteLength !== result.value.attachment.size) {
1408
+ if (value.bytes.byteLength !== value.attachment.size) {
774
1409
  return { ok: false, error: failure('remote') };
775
1410
  }
776
- return { ok: true, value: downloadedAttachment(result.value) };
1411
+ return { ok: true, value: downloadedAttachment(value) };
1412
+ }
1413
+ /** Return the deployment identity's public mailbox state. */
1414
+ getMailAccount() {
1415
+ return this.run(client => client.getMailAccount());
1416
+ }
1417
+ /** List one bounded mailbox page on explicit browser/tool demand. */
1418
+ async listMailInbox(request) {
1419
+ let normalized;
1420
+ try {
1421
+ normalized = mailInboxRequest(request ?? {});
1422
+ }
1423
+ catch {
1424
+ return { ok: false, error: failure('invalid-request') };
1425
+ }
1426
+ return this.run(async (client) => {
1427
+ if (normalized.folder !== 'sent')
1428
+ return client.listMailInbox(normalized);
1429
+ return this.sentMailStore.list(await this.ownerDid(client), normalized);
1430
+ });
1431
+ }
1432
+ /** Read one bounded plain-text mail message. */
1433
+ async readMail(request) {
1434
+ let normalized;
1435
+ try {
1436
+ normalized = mailReadRequest(request);
1437
+ }
1438
+ catch {
1439
+ return { ok: false, error: failure('invalid-request') };
1440
+ }
1441
+ return this.run(async (client) => {
1442
+ if (!isLocalSentMailId(normalized.messageId))
1443
+ return client.readMail(normalized);
1444
+ const local = await this.sentMailStore.read(await this.ownerDid(client), normalized.messageId);
1445
+ if (local === undefined) {
1446
+ throw Object.assign(new Error('sent mail not found'), { name: 'AwikiSdkError', code: 'not-found' });
1447
+ }
1448
+ return local;
1449
+ });
1450
+ }
1451
+ /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1452
+ markMailRead(request) {
1453
+ return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized));
1454
+ }
1455
+ /** Send one plain-text mail once. Browser callers require an explicit confirmation. */
1456
+ async sendMail(request) {
1457
+ let normalized;
1458
+ try {
1459
+ normalized = mailSendRequest(request);
1460
+ }
1461
+ catch {
1462
+ return { ok: false, error: failure('invalid-request') };
1463
+ }
1464
+ return this.run(async (client) => {
1465
+ const result = await client.sendMail(normalized);
1466
+ if (!result.accepted)
1467
+ return result;
1468
+ try {
1469
+ const ownerDid = await this.ownerDid(client);
1470
+ const account = await client.getMailAccount().catch(() => undefined);
1471
+ await this.sentMailStore.append(ownerDid, normalized, result, account);
1472
+ return result;
1473
+ }
1474
+ catch {
1475
+ return result.warnings.length >= 100
1476
+ ? result
1477
+ : { ...result, warnings: [...result.warnings, 'Sent history could not be saved locally.'] };
1478
+ }
1479
+ });
777
1480
  }
778
1481
  /**
779
1482
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -786,13 +1489,25 @@ let AwikiService = (() => {
786
1489
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
787
1490
  }
788
1491
  return this.mutateSession(async () => {
1492
+ const provider = this.provider;
1493
+ if (provider !== undefined)
1494
+ await this.stopListener(provider);
789
1495
  const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
790
- if (!result.ok)
1496
+ if (!result.ok) {
1497
+ if (provider !== undefined && this.provider === provider)
1498
+ void this.startListener(provider);
791
1499
  return result;
1500
+ }
792
1501
  try {
1502
+ await this.imageAttachmentCache.clear();
1503
+ await this.sentMailStore.clear();
793
1504
  await this.sessionStore.signIn();
794
1505
  this.signedOut = false;
1506
+ this.activeIdentityDid = undefined;
795
1507
  this.invalidateSummaries();
1508
+ this.publishSession({ status: 'unregistered' });
1509
+ if (provider !== undefined && this.provider === provider)
1510
+ void this.startListener(provider);
796
1511
  return result;
797
1512
  }
798
1513
  catch {
@@ -800,6 +1515,45 @@ let AwikiService = (() => {
800
1515
  }
801
1516
  });
802
1517
  }
1518
+ /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
1519
+ async applyRecoveredSession(progress) {
1520
+ if (progress.phase !== 'applied')
1521
+ return true;
1522
+ return this.mutateSession(async () => {
1523
+ const provider = this.provider;
1524
+ if (provider === undefined)
1525
+ return false;
1526
+ try {
1527
+ const identity = await provider.client.getIdentity();
1528
+ if (identity === null || identity.did !== progress.currentDid)
1529
+ return false;
1530
+ if (this.signedOut === false && this.activeIdentityDid === identity.did)
1531
+ return true;
1532
+ await this.sessionStore.signIn();
1533
+ this.signedOut = false;
1534
+ this.activeIdentityDid = identity.did;
1535
+ this.invalidateSummaries();
1536
+ const session = { status: 'active', identity };
1537
+ this.publishSession(session);
1538
+ await provider.listenerStartup;
1539
+ await this.startListener(provider);
1540
+ return true;
1541
+ }
1542
+ catch {
1543
+ return false;
1544
+ }
1545
+ });
1546
+ }
1547
+ /** Publish one newly registered identity and start its listener through the existing session path. */
1548
+ async activateRegisteredIdentity(identity) {
1549
+ this.activeIdentityDid = identity.did;
1550
+ this.publishSession({ status: 'active', identity });
1551
+ const provider = this.provider;
1552
+ if (provider !== undefined) {
1553
+ await provider.listenerStartup;
1554
+ await this.startListener(provider);
1555
+ }
1556
+ }
803
1557
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
804
1558
  invalidateSummaries() {
805
1559
  this.sessionRevision += 1;
@@ -807,6 +1561,20 @@ let AwikiService = (() => {
807
1561
  controller.abort();
808
1562
  this.activeSummaryRequests.clear();
809
1563
  }
1564
+ /** Publish a committed session transition to same-process Host consumers. */
1565
+ publishSession(session) {
1566
+ this.hostContext.emit('awiki/session', session);
1567
+ }
1568
+ /** Resolve and cache the owner binding required by private Host-side projections. */
1569
+ async ownerDid(client) {
1570
+ const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
1571
+ const ownerDid = this.activeIdentityDid ?? identity?.did;
1572
+ if (ownerDid === undefined) {
1573
+ throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
1574
+ }
1575
+ this.activeIdentityDid = ownerDid;
1576
+ return ownerDid;
1577
+ }
810
1578
  /** Invoke the current client and normalize every rejection to a public result. */
811
1579
  async run(operation, options = {}) {
812
1580
  try {
@@ -826,6 +1594,17 @@ let AwikiService = (() => {
826
1594
  return { ok: false, error: normalizeFailure(error) };
827
1595
  }
828
1596
  }
1597
+ /** Validate mail input before entering the provider and preserve fixed public failures. */
1598
+ runValidatedMail(validate, operation) {
1599
+ let request;
1600
+ try {
1601
+ request = validate();
1602
+ }
1603
+ catch {
1604
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1605
+ }
1606
+ return this.run(client => operation(client, request));
1607
+ }
829
1608
  /** Read and cache the private Host-owned session marker. */
830
1609
  async isSignedOut() {
831
1610
  this.signedOut ??= await this.sessionStore.isSignedOut();
@@ -879,11 +1658,163 @@ let AwikiService = (() => {
879
1658
  this.sessionMutation = pending.then(() => undefined, () => undefined);
880
1659
  return pending;
881
1660
  }
1661
+ /** Start one exact identity-bound listener, atomically releasing a failed startup. */
1662
+ startListener(provider) {
1663
+ if (!this.resolved.listenerEnabled
1664
+ || provider.listener !== undefined
1665
+ || provider.listenerRecoveryBlocked
1666
+ || this.provider !== provider) {
1667
+ return Promise.resolve();
1668
+ }
1669
+ if (provider.listenerStartup !== undefined)
1670
+ return provider.listenerStartup;
1671
+ if (provider.listenerCleanup !== undefined || provider.listenerRestartTimer !== undefined)
1672
+ return Promise.resolve();
1673
+ const workspaceContext = this.workspaceContext;
1674
+ const source = provider.client.listener;
1675
+ if (workspaceContext === undefined || source === undefined)
1676
+ return Promise.resolve();
1677
+ const generation = provider.listenerGeneration;
1678
+ const logger = this.ctx.logger('awiki-listener');
1679
+ const startup = (async () => {
1680
+ if (await this.isSignedOut())
1681
+ return;
1682
+ if (await provider.client.getIdentity() === null)
1683
+ return;
1684
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1685
+ return;
1686
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
1687
+ const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
1688
+ provider.listener = listener;
1689
+ try {
1690
+ await listener.start();
1691
+ }
1692
+ catch (error) {
1693
+ if (provider.listener === listener)
1694
+ delete provider.listener;
1695
+ try {
1696
+ await listener.dispose();
1697
+ }
1698
+ catch {
1699
+ provider.listenerRecoveryBlocked = true;
1700
+ }
1701
+ if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
1702
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1703
+ }
1704
+ throw error;
1705
+ }
1706
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
1707
+ if (provider.listener === listener)
1708
+ delete provider.listener;
1709
+ try {
1710
+ await listener.dispose();
1711
+ }
1712
+ catch (error) {
1713
+ provider.listenerRecoveryBlocked = true;
1714
+ throw error;
1715
+ }
1716
+ return;
1717
+ }
1718
+ provider.listenerStartedAt = Date.now();
1719
+ void listener.whenTerminated().then((result) => {
1720
+ if (result.kind === 'failed') {
1721
+ this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
1722
+ }
1723
+ });
1724
+ })();
1725
+ const observed = startup
1726
+ .catch((error) => {
1727
+ logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1728
+ })
1729
+ .finally(() => {
1730
+ if (provider.listenerStartup === observed)
1731
+ delete provider.listenerStartup;
1732
+ });
1733
+ provider.listenerStartup = observed;
1734
+ return observed;
1735
+ }
1736
+ async stopListener(provider) {
1737
+ provider.listenerGeneration += 1;
1738
+ provider.listenerRestartAttempt = 0;
1739
+ delete provider.listenerStartedAt;
1740
+ const timer = provider.listenerRestartTimer;
1741
+ if (timer !== undefined) {
1742
+ delete provider.listenerRestartTimer;
1743
+ clearTimeout(timer);
1744
+ }
1745
+ await provider.listenerStartup;
1746
+ await provider.listenerCleanup;
1747
+ const listener = provider.listener;
1748
+ if (listener === undefined)
1749
+ return;
1750
+ delete provider.listener;
1751
+ try {
1752
+ await listener.dispose();
1753
+ }
1754
+ catch (error) {
1755
+ provider.listenerRecoveryBlocked = true;
1756
+ throw error;
1757
+ }
1758
+ }
1759
+ listenerFenceMatches(provider, workspaceContext, generation) {
1760
+ return this.provider === provider
1761
+ && this.workspaceContext === workspaceContext
1762
+ && provider.listenerGeneration === generation;
1763
+ }
1764
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
1765
+ if (provider.listener !== listener)
1766
+ return;
1767
+ delete provider.listener;
1768
+ const startedAt = provider.listenerStartedAt;
1769
+ delete provider.listenerStartedAt;
1770
+ if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
1771
+ provider.listenerRestartAttempt = 0;
1772
+ }
1773
+ const logger = this.ctx.logger('awiki-listener');
1774
+ logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
1775
+ let disposed = false;
1776
+ const cleanup = listener.dispose()
1777
+ .then(() => { disposed = true; }, (cleanupError) => {
1778
+ provider.listenerRecoveryBlocked = true;
1779
+ logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
1780
+ });
1781
+ const observed = cleanup.finally(() => {
1782
+ if (provider.listenerCleanup === observed)
1783
+ delete provider.listenerCleanup;
1784
+ if (disposed)
1785
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1786
+ });
1787
+ provider.listenerCleanup = observed;
1788
+ }
1789
+ scheduleListenerRestart(provider, workspaceContext, generation) {
1790
+ if (provider.listenerRestartTimer !== undefined
1791
+ || !this.listenerFenceMatches(provider, workspaceContext, generation))
1792
+ return;
1793
+ provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
1794
+ const exponent = provider.listenerRestartAttempt - 1;
1795
+ const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
1796
+ const timer = setTimeout(() => {
1797
+ if (provider.listenerRestartTimer !== timer)
1798
+ return;
1799
+ delete provider.listenerRestartTimer;
1800
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1801
+ return;
1802
+ void this.startListener(provider);
1803
+ }, delay);
1804
+ provider.listenerRestartTimer = timer;
1805
+ }
882
1806
  /** Clear one exact provider slot before joining its one shared disposal. */
883
1807
  disposeProvider(provider) {
884
1808
  if (this.provider === provider)
885
1809
  this.provider = undefined;
886
- provider.disposal ??= provider.client.dispose();
1810
+ provider.disposal ??= (async () => {
1811
+ try {
1812
+ await this.stopListener(provider);
1813
+ }
1814
+ finally {
1815
+ await provider.client.dispose();
1816
+ }
1817
+ })();
887
1818
  return provider.disposal;
888
1819
  }
889
1820
  };