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

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 (99) hide show
  1. package/README.md +60 -58
  2. package/README.zh.md +51 -45
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/cordis.patch.yml +8 -7
  5. package/lib/client.js +1119 -1340
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +809 -1085
  8. package/lib/provider.js +17 -70
  9. package/lib/{sdk-adapter-FGaOi8It.mjs → sdk-adapter--trNlr-T.mjs} +84 -168
  10. package/lib/typert.host.js +178 -406
  11. package/lib/typert.remote-client.d.ts +7 -19
  12. package/lib/typert.remote-client.d.ts.map +1 -1
  13. package/lib/typert.remote-client.js +178 -406
  14. package/lib/types/base64.d.ts +4 -0
  15. package/lib/types/base64.d.ts.map +1 -0
  16. package/lib/types/base64.js +28 -0
  17. package/lib/types/base64.js.map +1 -0
  18. package/lib/types/client/AwikiIdentityAccess.d.ts +5 -6
  19. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  20. package/lib/types/client/AwikiIdentityAccess.js +27 -88
  21. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  22. package/lib/types/client/AwikiMail.d.ts +1 -1
  23. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  24. package/lib/types/client/AwikiMail.js +130 -6
  25. package/lib/types/client/AwikiMail.js.map +1 -1
  26. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  27. package/lib/types/client/AwikiOverlay.js +12 -5
  28. package/lib/types/client/AwikiOverlay.js.map +1 -1
  29. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  30. package/lib/types/client/AwikiRecoveryForm.js +1 -1
  31. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  32. package/lib/types/client/controller.d.ts +34 -20
  33. package/lib/types/client/controller.d.ts.map +1 -1
  34. package/lib/types/client/controller.js +203 -56
  35. package/lib/types/client/controller.js.map +1 -1
  36. package/lib/types/client/index.d.ts.map +1 -1
  37. package/lib/types/client/index.js +4 -8
  38. package/lib/types/client/index.js.map +1 -1
  39. package/lib/types/client/mail-attachment.d.ts +40 -0
  40. package/lib/types/client/mail-attachment.d.ts.map +1 -0
  41. package/lib/types/client/mail-attachment.js +187 -0
  42. package/lib/types/client/mail-attachment.js.map +1 -0
  43. package/lib/types/client/settings-locales.js +8 -8
  44. package/lib/types/client/settings-locales.js.map +1 -1
  45. package/lib/types/client/slots.d.ts +10 -10
  46. package/lib/types/client/slots.d.ts.map +1 -1
  47. package/lib/types/conversation-preferences.d.ts.map +1 -1
  48. package/lib/types/conversation-preferences.js +17 -0
  49. package/lib/types/conversation-preferences.js.map +1 -1
  50. package/lib/types/index.d.ts +30 -59
  51. package/lib/types/index.d.ts.map +1 -1
  52. package/lib/types/index.js +464 -805
  53. package/lib/types/index.js.map +1 -1
  54. package/lib/types/listener-state.d.ts +3 -5
  55. package/lib/types/listener-state.d.ts.map +1 -1
  56. package/lib/types/listener-state.js +13 -28
  57. package/lib/types/listener-state.js.map +1 -1
  58. package/lib/types/listener.d.ts +32 -9
  59. package/lib/types/listener.d.ts.map +1 -1
  60. package/lib/types/listener.js +127 -11
  61. package/lib/types/listener.js.map +1 -1
  62. package/lib/types/mail.d.ts +28 -2
  63. package/lib/types/mail.d.ts.map +1 -1
  64. package/lib/types/mail.js +79 -2
  65. package/lib/types/mail.js.map +1 -1
  66. package/lib/types/profile-state.d.ts +17 -0
  67. package/lib/types/profile-state.d.ts.map +1 -0
  68. package/lib/types/profile-state.js +58 -0
  69. package/lib/types/profile-state.js.map +1 -0
  70. package/lib/types/provider-api.d.ts +38 -102
  71. package/lib/types/provider-api.d.ts.map +1 -1
  72. package/lib/types/provider.d.ts +2 -8
  73. package/lib/types/provider.d.ts.map +1 -1
  74. package/lib/types/provider.js +16 -73
  75. package/lib/types/provider.js.map +1 -1
  76. package/lib/types/sdk-adapter.d.ts +7 -32
  77. package/lib/types/sdk-adapter.d.ts.map +1 -1
  78. package/lib/types/sdk-adapter.js +104 -163
  79. package/lib/types/sdk-adapter.js.map +1 -1
  80. package/lib/types/sent-mail-store.d.ts +7 -2
  81. package/lib/types/sent-mail-store.d.ts.map +1 -1
  82. package/lib/types/sent-mail-store.js +125 -23
  83. package/lib/types/sent-mail-store.js.map +1 -1
  84. package/lib/types/tools.d.ts +1 -1
  85. package/lib/types/tools.d.ts.map +1 -1
  86. package/lib/types/tools.js +20 -4
  87. package/lib/types/tools.js.map +1 -1
  88. package/lib/types/types.d.ts +66 -77
  89. package/lib/types/types.d.ts.map +1 -1
  90. package/lib/types/types.js.map +1 -1
  91. package/package.json +15 -20
  92. package/lib/types/client/AwikiDevices.d.ts +0 -28
  93. package/lib/types/client/AwikiDevices.d.ts.map +0 -1
  94. package/lib/types/client/AwikiDevices.js +0 -85
  95. package/lib/types/client/AwikiDevices.js.map +0 -1
  96. package/lib/types/realtime-supervisor.d.ts +0 -54
  97. package/lib/types/realtime-supervisor.d.ts.map +0 -1
  98. package/lib/types/realtime-supervisor.js +0 -242
  99. package/lib/types/realtime-supervisor.js.map +0 -1
@@ -34,7 +34,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
34
34
  done = true;
35
35
  };
36
36
  import { Context } from '@deepseek-ai/cordis';
37
- import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
37
+ import { createHash } from 'node:crypto';
38
+ import { existsSync } from 'node:fs';
38
39
  import { homedir } from 'node:os';
39
40
  import { isAbsolute, join } from 'node:path';
40
41
  import z from '@deepseek-ai/schemastery';
@@ -43,8 +44,9 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
43
44
  import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
44
45
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
45
46
  import { downloadedAttachment } from "./sdk-adapter.js";
47
+ import { standardBase64Syntax } from "./base64.js";
46
48
  import { registerAwikiTools } from "./tools.js";
47
- import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.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";
48
50
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
49
51
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
50
52
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
@@ -54,7 +56,7 @@ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "
54
56
  import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
55
57
  import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
56
58
  import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
57
- import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
59
+ import { resolveAwikiProfileName, resolveAwikiStateRoot, resolveAwikiTenantStateRoot, } from "./profile-state.js";
58
60
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
59
61
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
60
62
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
@@ -75,19 +77,21 @@ export const DEFAULT_SUMMARY_MAX_INPUT_BYTES = 32 * 1024;
75
77
  export const MAX_SUMMARY_MESSAGES = 50;
76
78
  /** Loader schema for the Host deployment configuration. */
77
79
  export const Config = z.object({
78
- userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
80
+ userServiceUrl: z.string(),
79
81
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
80
- messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
82
+ messageServiceUrl: z.string(),
81
83
  mailServiceUrl: z.string(),
82
- messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
83
- messageServiceDid: z.string().default(DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
84
+ messageServicePublicUrl: z.string(),
85
+ messageServiceDid: z.string(),
84
86
  allowedAttachmentOrigins: z.array(z.string()).default([]),
85
87
  allowInsecureLoopbackForTesting: z.boolean().default(false),
86
88
  stateRoot: z.string(),
87
89
  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),
88
93
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
89
94
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
90
- realtimeEnabled: z.boolean().default(true),
91
95
  listenerEnabled: z.boolean().default(false),
92
96
  listenerAllowedPeers: z.array(z.string()).default([]),
93
97
  listenerWorkspacePath: z.string(),
@@ -105,6 +109,7 @@ const FAILURE_CODES = new Set([
105
109
  'forbidden',
106
110
  'identity-recovery-required',
107
111
  'conflict',
112
+ 'state-in-use',
108
113
  'rate-limited',
109
114
  'group-membership-required',
110
115
  'group-identity-stale',
@@ -118,17 +123,10 @@ const FAILURE_CODES = new Set([
118
123
  'network',
119
124
  'remote',
120
125
  ]);
121
- const DEVICE_JOIN_APPROVAL_CONFIRMATION = 'APPROVE';
122
- const DEVICE_REVOKE_CONFIRMATION = 'REVOKE';
123
- const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
124
- const RESUMABLE_JOIN_PHASES = new Set([
125
- 'pending',
126
- 'challenge_prepared',
127
- 'response_prepared',
128
- 'response_verified',
129
- 'approval_prepared',
130
- 'authorized',
131
- ]);
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;
132
130
  const FAILURE_MESSAGES = {
133
131
  'not-registered': 'No AWiki identity is registered for this deployment.',
134
132
  'signed-out': 'This installation is signed out of AWiki.',
@@ -141,6 +139,7 @@ const FAILURE_MESSAGES = {
141
139
  'forbidden': 'The AWiki operation is not permitted.',
142
140
  'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
143
141
  'conflict': 'The AWiki operation conflicts with current state.',
142
+ 'state-in-use': 'AWiki data is already open in another client.',
144
143
  'rate-limited': 'The AWiki service rate-limited the request.',
145
144
  'group-membership-required': 'The active AWiki identity is not a member of this group.',
146
145
  'group-identity-stale': 'The AWiki group identity binding is still recovering.',
@@ -158,67 +157,6 @@ class ProviderUnavailableError extends Error {
158
157
  }
159
158
  class SummaryProviderUnavailableError extends Error {
160
159
  }
161
- function candidateJoinPhase(value) {
162
- if (value.remoteState === 'rejected')
163
- return 'rejected';
164
- if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
165
- return 'cancelled';
166
- if (value.remoteState === 'expired' || value.localPhase === 'expired')
167
- return 'expired';
168
- if (value.localPhase === 'authorized'
169
- && value.remoteState === 'consumed'
170
- && value.completed
171
- && value.identity !== undefined)
172
- return 'authorized';
173
- if (value.sas !== undefined) {
174
- return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
175
- ? 'sas-ready'
176
- : undefined;
177
- }
178
- if (value.remoteState === 'challenge_sent'
179
- || value.remoteState === 'response_verified'
180
- || value.remoteState === 'consumed'
181
- || value.localPhase !== 'pending')
182
- return 'verifying';
183
- return value.remoteState === 'pending' ? 'pending' : undefined;
184
- }
185
- function adminJoinPhase(value) {
186
- if (value.remoteState === 'rejected')
187
- return 'rejected';
188
- if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
189
- return 'cancelled';
190
- if (value.remoteState === 'expired' || value.localPhase === 'expired')
191
- return 'expired';
192
- if (value.localPhase === 'authorized' && value.remoteState === 'consumed')
193
- return 'authorized';
194
- if (value.sas !== undefined) {
195
- return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
196
- ? 'sas-ready'
197
- : undefined;
198
- }
199
- if (value.remoteState === 'challenge_sent'
200
- || value.remoteState === 'response_verified'
201
- || value.remoteState === 'consumed'
202
- || value.localPhase !== 'pending')
203
- return 'verifying';
204
- return value.remoteState === 'pending' ? 'pending' : undefined;
205
- }
206
- function requestJoinPhase(value) {
207
- switch (value.state) {
208
- case 'pending': return 'pending';
209
- case 'challenge_sent':
210
- case 'response_verified': return 'verifying';
211
- case 'consumed': return 'authorized';
212
- case 'cancelled': return 'cancelled';
213
- case 'rejected': return 'rejected';
214
- case 'expired': return 'expired';
215
- }
216
- }
217
- function constantTimeSasMatches(expected, actual) {
218
- if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual))
219
- return false;
220
- return timingSafeEqual(Buffer.from(expected, 'ascii'), Buffer.from(actual, 'ascii'));
221
- }
222
160
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
223
161
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
224
162
  let url;
@@ -249,30 +187,6 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
249
187
  }
250
188
  return new URL('/api/identity-recovery', parsed).toString();
251
189
  }
252
- /** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
253
- async function acceptsRecoveryReconciliation(response) {
254
- if (!response.ok)
255
- return false;
256
- let value;
257
- try {
258
- const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
259
- if (text === undefined)
260
- return false;
261
- value = JSON.parse(text);
262
- }
263
- catch {
264
- return false;
265
- }
266
- if (typeof value !== 'object' || value === null || Array.isArray(value))
267
- return false;
268
- const result = value;
269
- const keys = Object.keys(result).sort();
270
- return keys.length === 2
271
- && keys[0] === 'idempotent'
272
- && keys[1] === 'restored'
273
- && result.restored === true
274
- && typeof result.idempotent === 'boolean';
275
- }
276
190
  /** Validate a provider domain without inferring it from an API endpoint. */
277
191
  function serviceDomain(raw, field = 'userServiceDomain') {
278
192
  return normalizeAwikiDomain(raw, field);
@@ -320,22 +234,45 @@ function listenerAllowedPeers(raw, enabled) {
320
234
  return peers;
321
235
  }
322
236
  /** Resolve and validate every deployment choice before publishing the service. */
323
- function resolveConfig(config) {
237
+ function resolveConfig(ctx, config) {
324
238
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
325
239
  const configuredStateRoot = config.stateRoot?.trim();
326
240
  const configuredDshHome = process.env.DSH_HOME?.trim();
327
241
  const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
328
242
  ? join(homedir(), '.dsh')
329
243
  : configuredDshHome;
244
+ const profileName = configuredStateRoot === undefined || configuredStateRoot.length === 0
245
+ ? resolveAwikiProfileName(ctx, dshHome)
246
+ : undefined;
330
247
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
331
- ? join(dshHome, 'awiki', 'im-core')
248
+ ? resolveAwikiStateRoot(ctx, dshHome)
332
249
  : configuredStateRoot;
250
+ const legacySharedStateDetected = profileName !== undefined
251
+ && existsSync(join(dshHome, 'awiki', 'im-core'));
333
252
  if (!isAbsolute(stateRoot))
334
253
  throw new TypeError('awiki: stateRoot must be an absolute path');
335
254
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
336
255
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) {
337
256
  throw new TypeError('awiki: attachmentMaxBytes must be a positive safe integer');
338
257
  }
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
+ }
339
276
  const imageAttachmentCacheMaxBytes = config.imageAttachmentCacheMaxBytes ?? DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES;
340
277
  if (!Number.isSafeInteger(imageAttachmentCacheMaxBytes)
341
278
  || imageAttachmentCacheMaxBytes < minimumImageAttachmentCacheMaxBytes(attachmentMaxBytes)) {
@@ -346,10 +283,6 @@ function resolveConfig(config) {
346
283
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
347
284
  }
348
285
  const listenerEnabled = config.listenerEnabled ?? false;
349
- const realtimeEnabled = config.realtimeEnabled ?? true;
350
- if (listenerEnabled && !realtimeEnabled) {
351
- throw new TypeError('awiki: listenerEnabled requires realtimeEnabled');
352
- }
353
286
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
354
287
  if (!isAbsolute(listenerWorkspacePath)) {
355
288
  throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
@@ -364,6 +297,7 @@ function resolveConfig(config) {
364
297
  const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
365
298
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
366
299
  return {
300
+ tenantDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
367
301
  userServiceUrl,
368
302
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
369
303
  messageServiceUrl,
@@ -374,9 +308,12 @@ function resolveConfig(config) {
374
308
  allowInsecureLoopbackForTesting,
375
309
  stateRoot,
376
310
  attachmentMaxBytes,
311
+ mailAttachmentMaxCount,
312
+ mailAttachmentMaxBytes,
313
+ mailAttachmentTotalMaxBytes,
377
314
  imageAttachmentCacheMaxBytes,
378
315
  pollIntervalMs,
379
- realtimeEnabled,
316
+ ...profileName === undefined ? {} : { profileName, legacySharedStateDetected },
380
317
  listenerEnabled,
381
318
  listener: {
382
319
  allowedPeers,
@@ -384,6 +321,42 @@ function resolveConfig(config) {
384
321
  stateRoot,
385
322
  },
386
323
  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),
387
360
  };
388
361
  }
389
362
  /** Return a public, fixed-message failure without retaining a thrown value. */
@@ -455,6 +428,38 @@ function normalizeGroupDid(value) {
455
428
  return undefined;
456
429
  return value;
457
430
  }
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
+ }
458
463
  function normalizeProfileRequest(request) {
459
464
  if (typeof request?.displayName !== 'string' || typeof request.bio !== 'string' || !Array.isArray(request.tags))
460
465
  return undefined;
@@ -505,8 +510,6 @@ function normalizeRecoveryPrepareRequest(request) {
505
510
  return { ...operation, phone, otp };
506
511
  }
507
512
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
508
- const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
509
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
510
513
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
511
514
  async function readBoundedResponseText(response, maxBytes) {
512
515
  const declaredLength = response.headers.get('content-length');
@@ -713,7 +716,7 @@ function decodeAttachment(bytesBase64, maxBytes) {
713
716
  const maxEncoded = Math.ceil(maxBytes / 3) * 4;
714
717
  if (bytesBase64.length > maxEncoded)
715
718
  return { ok: false, error: failure('attachment-too-large') };
716
- if (bytesBase64.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(bytesBase64)) {
719
+ if (bytesBase64.length % 4 !== 0 || !standardBase64Syntax(bytesBase64)) {
717
720
  return { ok: false, error: failure('invalid-request') };
718
721
  }
719
722
  const bytes = Uint8Array.from(Buffer.from(bytesBase64, 'base64'));
@@ -735,14 +738,6 @@ let AwikiService = (() => {
735
738
  let _inspectIdentityAccess_decorators;
736
739
  let _sendRegistrationOtp_decorators;
737
740
  let _registerIdentity_decorators;
738
- let _beginDeviceJoin_decorators;
739
- let _getDeviceJoinStatus_decorators;
740
- let _cancelDeviceJoin_decorators;
741
- let _refreshDeviceManagement_decorators;
742
- let _startDeviceJoinVerification_decorators;
743
- let _approveDeviceJoin_decorators;
744
- let _rejectDeviceJoin_decorators;
745
- let _revokeDevice_decorators;
746
741
  let _updateDisplayName_decorators;
747
742
  let _getProfile_decorators;
748
743
  let _updateProfile_decorators;
@@ -760,6 +755,7 @@ let AwikiService = (() => {
760
755
  let _listGroupMembers_decorators;
761
756
  let _addGroupMember_decorators;
762
757
  let _removeGroupMember_decorators;
758
+ let _resumeGroupRebindRecovery_decorators;
763
759
  let _getConversationPreferences_decorators;
764
760
  let _updateConversationPreference_decorators;
765
761
  let _listConversations_decorators;
@@ -775,6 +771,7 @@ let AwikiService = (() => {
775
771
  let _readMail_decorators;
776
772
  let _markMailRead_decorators;
777
773
  let _sendMail_decorators;
774
+ let _downloadMailAttachment_decorators;
778
775
  let _clearLocalData_decorators;
779
776
  return class AwikiService extends _classSuper {
780
777
  static {
@@ -787,14 +784,6 @@ let AwikiService = (() => {
787
784
  _inspectIdentityAccess_decorators = [Remote];
788
785
  _sendRegistrationOtp_decorators = [Remote];
789
786
  _registerIdentity_decorators = [Remote];
790
- _beginDeviceJoin_decorators = [Remote];
791
- _getDeviceJoinStatus_decorators = [Remote];
792
- _cancelDeviceJoin_decorators = [Remote];
793
- _refreshDeviceManagement_decorators = [Remote];
794
- _startDeviceJoinVerification_decorators = [Remote];
795
- _approveDeviceJoin_decorators = [Remote];
796
- _rejectDeviceJoin_decorators = [Remote];
797
- _revokeDevice_decorators = [Remote];
798
787
  _updateDisplayName_decorators = [Remote];
799
788
  _getProfile_decorators = [Remote];
800
789
  _updateProfile_decorators = [Remote];
@@ -812,6 +801,7 @@ let AwikiService = (() => {
812
801
  _listGroupMembers_decorators = [Remote];
813
802
  _addGroupMember_decorators = [Remote];
814
803
  _removeGroupMember_decorators = [Remote];
804
+ _resumeGroupRebindRecovery_decorators = [Remote];
815
805
  _getConversationPreferences_decorators = [Remote];
816
806
  _updateConversationPreference_decorators = [Remote];
817
807
  _listConversations_decorators = [Remote];
@@ -827,6 +817,7 @@ let AwikiService = (() => {
827
817
  _readMail_decorators = [Remote];
828
818
  _markMailRead_decorators = [Remote];
829
819
  _sendMail_decorators = [Remote];
820
+ _downloadMailAttachment_decorators = [Remote];
830
821
  _clearLocalData_decorators = [Remote];
831
822
  __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);
832
823
  __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);
@@ -836,14 +827,6 @@ let AwikiService = (() => {
836
827
  __esDecorate(this, null, _inspectIdentityAccess_decorators, { kind: "method", name: "inspectIdentityAccess", static: false, private: false, access: { has: obj => "inspectIdentityAccess" in obj, get: obj => obj.inspectIdentityAccess }, metadata: _metadata }, null, _instanceExtraInitializers);
837
828
  __esDecorate(this, null, _sendRegistrationOtp_decorators, { kind: "method", name: "sendRegistrationOtp", static: false, private: false, access: { has: obj => "sendRegistrationOtp" in obj, get: obj => obj.sendRegistrationOtp }, metadata: _metadata }, null, _instanceExtraInitializers);
838
829
  __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);
839
- __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);
840
- __esDecorate(this, null, _getDeviceJoinStatus_decorators, { kind: "method", name: "getDeviceJoinStatus", static: false, private: false, access: { has: obj => "getDeviceJoinStatus" in obj, get: obj => obj.getDeviceJoinStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
841
- __esDecorate(this, null, _cancelDeviceJoin_decorators, { kind: "method", name: "cancelDeviceJoin", static: false, private: false, access: { has: obj => "cancelDeviceJoin" in obj, get: obj => obj.cancelDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
842
- __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);
843
- __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);
844
- __esDecorate(this, null, _approveDeviceJoin_decorators, { kind: "method", name: "approveDeviceJoin", static: false, private: false, access: { has: obj => "approveDeviceJoin" in obj, get: obj => obj.approveDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
845
- __esDecorate(this, null, _rejectDeviceJoin_decorators, { kind: "method", name: "rejectDeviceJoin", static: false, private: false, access: { has: obj => "rejectDeviceJoin" in obj, get: obj => obj.rejectDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
846
- __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);
847
830
  __esDecorate(this, null, _updateDisplayName_decorators, { kind: "method", name: "updateDisplayName", static: false, private: false, access: { has: obj => "updateDisplayName" in obj, get: obj => obj.updateDisplayName }, metadata: _metadata }, null, _instanceExtraInitializers);
848
831
  __esDecorate(this, null, _getProfile_decorators, { kind: "method", name: "getProfile", static: false, private: false, access: { has: obj => "getProfile" in obj, get: obj => obj.getProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
849
832
  __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);
@@ -861,6 +844,7 @@ let AwikiService = (() => {
861
844
  __esDecorate(this, null, _listGroupMembers_decorators, { kind: "method", name: "listGroupMembers", static: false, private: false, access: { has: obj => "listGroupMembers" in obj, get: obj => obj.listGroupMembers }, metadata: _metadata }, null, _instanceExtraInitializers);
862
845
  __esDecorate(this, null, _addGroupMember_decorators, { kind: "method", name: "addGroupMember", static: false, private: false, access: { has: obj => "addGroupMember" in obj, get: obj => obj.addGroupMember }, metadata: _metadata }, null, _instanceExtraInitializers);
863
846
  __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);
864
848
  __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);
865
849
  __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);
866
850
  __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);
@@ -876,16 +860,15 @@ let AwikiService = (() => {
876
860
  __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);
877
861
  __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);
878
862
  __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);
879
864
  __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);
880
865
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
881
866
  }
882
- static inject = ['tools'];
867
+ static inject = ['tools', 'settings'];
883
868
  static Config = Config;
884
869
  resolved = __runInitializers(this, _instanceExtraInitializers);
885
- sessionStore;
886
- imageAttachmentCache;
887
- sentMailStore;
888
- conversationPreferenceStore;
870
+ tenantConfigCache;
871
+ tenantStateCache;
889
872
  startupUserServiceDomain;
890
873
  settingsProvider;
891
874
  provider;
@@ -893,12 +876,6 @@ let AwikiService = (() => {
893
876
  sessionMutation = Promise.resolve();
894
877
  sessionRevision = 0;
895
878
  activeIdentityDid;
896
- pendingDeviceJoin;
897
- activeDeviceJoinSessionId;
898
- requestRefs = new Map();
899
- requestSessions = new Map();
900
- deviceRefs = new Map();
901
- deviceIds = new Map();
902
879
  activeSummaryRequests = new Set();
903
880
  summaryProvider;
904
881
  recoveryReconciliationTarget;
@@ -913,29 +890,23 @@ let AwikiService = (() => {
913
890
  constructor(ctx, config) {
914
891
  super(ctx, 'awiki');
915
892
  this.hostContext = ctx;
916
- this.resolved = resolveConfig(config);
893
+ this.resolved = resolveConfig(ctx, config);
917
894
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
918
- this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
919
- this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
920
- this.sentMailStore = new AwikiSentMailStore(this.resolved.stateRoot);
921
- this.conversationPreferenceStore = new AwikiConversationPreferenceStore(this.resolved.stateRoot);
922
895
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
923
- ctx.inject(['settings'], (settingsCtx) => {
924
- const provider = settingsCtx.settings;
925
- const settingsScope = settingsCtx.settings.register(settingsNamespace(AWIKI_SETTINGS_NAMESPACE), AwikiSettingsSchema, {
926
- base: { domain: this.resolved.userServiceDomain },
927
- applies: 'restart',
928
- validate: validateAwikiSettings,
929
- });
930
- this.settingsProvider = provider;
931
- this.startupUserServiceDomain = settingsScope.get().domain;
932
- settingsCtx.effect(() => () => {
933
- if (this.settingsProvider === provider) {
934
- this.settingsProvider = undefined;
935
- this.startupUserServiceDomain = this.resolved.userServiceDomain;
936
- }
937
- }, 'awiki: release settings namespace');
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,
938
901
  });
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');
939
910
  ctx.inject(['connection'], (connectionCtx) => {
940
911
  connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
941
912
  });
@@ -943,14 +914,14 @@ let AwikiService = (() => {
943
914
  this.workspaceContext = workspaceCtx;
944
915
  const provider = this.provider;
945
916
  if (provider !== undefined)
946
- this.ensureAgentConsumer(provider);
917
+ void this.startListener(provider);
947
918
  workspaceCtx.effect(() => async () => {
948
919
  if (this.workspaceContext !== workspaceCtx)
949
920
  return;
950
921
  this.workspaceContext = undefined;
951
922
  const current = this.provider;
952
923
  if (current !== undefined)
953
- await this.stopAgentConsumer(current);
924
+ await this.stopListener(current);
954
925
  }, 'awiki: release Workspace listener composition');
955
926
  });
956
927
  registerAwikiTools(ctx, this);
@@ -973,49 +944,17 @@ let AwikiService = (() => {
973
944
  registerClientFactory(factory) {
974
945
  if (this.provider !== undefined)
975
946
  throw new Error('awiki: a client provider is already registered');
976
- this.pendingDeviceJoin = undefined;
977
- this.activeDeviceJoinSessionId = undefined;
978
- const client = factory({
979
- userServiceUrl: this.resolved.userServiceUrl,
980
- userServiceDomain: this.startupUserServiceDomain,
981
- messageServiceUrl: this.resolved.messageServiceUrl,
982
- mailServiceUrl: this.resolved.mailServiceUrl,
983
- messageServicePublicUrl: this.resolved.messageServicePublicUrl,
984
- messageServiceDid: this.resolved.messageServiceDid,
985
- allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
986
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
987
- allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
988
- stateRoot: this.resolved.stateRoot,
989
- });
947
+ const client = factory(this.activeTenant());
990
948
  const provider = {
991
949
  client,
992
- realtimeGeneration: 0,
993
- agentConsumerGeneration: 0,
994
- localDeviceJoinRequestCountAfterSync: 0,
950
+ listenerRestartAttempt: 0,
951
+ listenerGeneration: 0,
952
+ listenerRecoveryBlocked: false,
995
953
  };
996
954
  this.provider = provider;
997
- this.ensureRealtimeSupervisor(provider);
998
- this.ensureAgentConsumer(provider);
955
+ void this.startListener(provider);
999
956
  return () => this.disposeProvider(provider);
1000
957
  }
1001
- /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
1002
- getRealtimeDiagnostics() {
1003
- const provider = this.provider;
1004
- const realtime = provider?.realtimeSupervisor?.diagnostics() ?? {
1005
- connected: false,
1006
- activeSessionCount: 0,
1007
- startCount: 0,
1008
- stopCount: 0,
1009
- maxActiveSessionCount: 0,
1010
- generation: 0,
1011
- retryCount: 0,
1012
- lifecyclePhase: 'idle',
1013
- };
1014
- return {
1015
- ...realtime,
1016
- localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
1017
- };
1018
- }
1019
958
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
1020
959
  registerRecoveryReconciliationTarget(target) {
1021
960
  if (this.recoveryReconciliationTarget !== undefined) {
@@ -1053,43 +992,58 @@ let AwikiService = (() => {
1053
992
  * Read settings needed by the browser presentation.
1054
993
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
1055
994
  */
1056
- async getConfig() {
1057
- let handleRecoveryPhoneEnabled = false;
1058
- const abort = new AbortController();
1059
- const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1060
- try {
1061
- const response = await fetch(new URL('/user-service/v1/server-info', this.resolved.userServiceUrl), {
1062
- method: 'GET',
1063
- headers: { accept: 'application/json', 'cache-control': 'no-store' },
1064
- cache: 'no-store',
1065
- redirect: 'error',
1066
- signal: abort.signal,
1067
- });
1068
- if (response.ok) {
1069
- const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1070
- const value = text === undefined ? undefined : JSON.parse(text);
1071
- if (typeof value === 'object' && value !== null && value.schema_version === 1) {
1072
- const methods = value.identity?.handle_recovery?.methods;
1073
- handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1074
- && method.id === 'phone'
1075
- && method.enabled === true
1076
- && method.verification?.required === true
1077
- && method.verification?.type === 'sms_otp'));
1078
- }
1079
- }
1080
- }
1081
- catch { }
1082
- finally {
1083
- clearTimeout(timeout);
1084
- }
1085
- return {
995
+ getConfig() {
996
+ return Promise.resolve({
1086
997
  ok: true,
1087
998
  value: {
999
+ tenantDomain: this.activeTenant().domain,
1088
1000
  pollIntervalMs: this.resolved.pollIntervalMs,
1089
1001
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1090
- handleRecoveryPhoneEnabled,
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,
1008
+ },
1091
1009
  },
1092
- };
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;
1041
+ }
1042
+ get sentMailStore() {
1043
+ return this.activeTenantState().sentMailStore;
1044
+ }
1045
+ get conversationPreferenceStore() {
1046
+ return this.activeTenantState().conversationPreferenceStore;
1093
1047
  }
1094
1048
  /**
1095
1049
  * Read the deployment's identity status.
@@ -1111,9 +1065,6 @@ let AwikiService = (() => {
1111
1065
  if (!identity.ok)
1112
1066
  return identity;
1113
1067
  this.activeIdentityDid = identity.value?.did;
1114
- const provider = this.provider;
1115
- if (identity.value !== null && provider !== undefined)
1116
- this.ensureProviderRuntime(provider);
1117
1068
  return identity.value === null
1118
1069
  ? { ok: true, value: { status: 'unregistered' } }
1119
1070
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -1140,7 +1091,7 @@ let AwikiService = (() => {
1140
1091
  this.publishSession(session);
1141
1092
  const provider = this.provider;
1142
1093
  if (provider !== undefined)
1143
- await this.stopProviderRuntime(provider);
1094
+ await this.stopListener(provider);
1144
1095
  return { ok: true, value: session };
1145
1096
  }
1146
1097
  catch {
@@ -1165,7 +1116,7 @@ let AwikiService = (() => {
1165
1116
  this.publishSession(session);
1166
1117
  const provider = this.provider;
1167
1118
  if (provider !== undefined)
1168
- this.ensureProviderRuntime(provider);
1119
+ void this.startListener(provider);
1169
1120
  return { ok: true, value: session };
1170
1121
  }
1171
1122
  catch {
@@ -1178,7 +1129,7 @@ let AwikiService = (() => {
1178
1129
  const target = identityAccessTarget(request, this.startupUserServiceDomain);
1179
1130
  if (target === undefined)
1180
1131
  return { ok: false, error: failure('invalid-request') };
1181
- const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.resolved.userServiceUrl);
1132
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.activeTenant().userServiceUrl);
1182
1133
  const abort = new AbortController();
1183
1134
  const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1184
1135
  let response;
@@ -1223,14 +1174,8 @@ let AwikiService = (() => {
1223
1174
  return { ok: false, error: failure('remote') };
1224
1175
  }
1225
1176
  }
1226
- async sendRegistrationOtp(request) {
1227
- this.pendingDeviceJoin = undefined;
1228
- return this.run(async (client) => {
1229
- if (await this.selectDeviceJoinSession(client) !== null) {
1230
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1231
- }
1232
- return client.sendRegistrationOtp(request);
1233
- });
1177
+ sendRegistrationOtp(request) {
1178
+ return this.run(client => client.sendRegistrationOtp(request));
1234
1179
  }
1235
1180
  /**
1236
1181
  * Register and persist the deployment's only AWiki identity.
@@ -1238,177 +1183,10 @@ let AwikiService = (() => {
1238
1183
  * @returns The new public identity or a closed failure.
1239
1184
  */
1240
1185
  async registerIdentity(request) {
1241
- this.pendingDeviceJoin = undefined;
1242
- const result = await this.run(async (client) => {
1243
- if (await this.selectDeviceJoinSession(client) !== null) {
1244
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1245
- }
1246
- return client.registerIdentity(request);
1247
- });
1248
- if (!result.ok)
1249
- return result;
1250
- if (result.value.status === 'registered') {
1251
- await this.activateRegisteredIdentity(result.value.identity);
1252
- return { ok: true, value: { status: 'registered', identity: result.value.identity } };
1253
- }
1254
- this.pendingDeviceJoin = {
1255
- continuationId: result.value.continuationId,
1256
- fullHandle: result.value.fullHandle,
1257
- mode: result.value.mode,
1258
- requiresUserPresence: result.value.requiresUserPresence,
1259
- };
1260
- return {
1261
- ok: true,
1262
- value: {
1263
- status: 'join-required',
1264
- fullHandle: result.value.fullHandle,
1265
- mode: result.value.mode,
1266
- requiresUserPresence: result.value.requiresUserPresence,
1267
- },
1268
- };
1269
- }
1270
- /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
1271
- async beginDeviceJoin() {
1272
- const continuation = this.pendingDeviceJoin;
1273
- if (continuation === undefined)
1274
- return { ok: false, error: failure('conflict') };
1275
- if (continuation.mode !== 'ordinary' || continuation.requiresUserPresence) {
1276
- return { ok: false, error: failure('forbidden') };
1277
- }
1278
- this.pendingDeviceJoin = undefined;
1279
- const result = await this.run(client => client.beginDeviceJoin({
1280
- continuationId: continuation.continuationId,
1281
- operationId: `dsh-device-join-${randomUUID()}`,
1282
- userPresenceConfirmed: false,
1283
- }));
1284
- if (!result.ok)
1285
- return result;
1286
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1287
- return this.applyCandidateJoinProgress(result.value);
1288
- }
1289
- /** Restore from Core local_sessions and advance only the exact resumable Join. */
1290
- async getDeviceJoinStatus() {
1291
- const result = await this.run(async (client) => {
1292
- const joinSessionId = await this.selectDeviceJoinSession(client);
1293
- return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
1294
- });
1295
- if (!result.ok)
1296
- return result;
1297
- if (result.value === null)
1298
- return { ok: true, value: null };
1299
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1300
- return this.applyCandidateJoinProgress(result.value);
1301
- }
1302
- async cancelDeviceJoin() {
1303
- const result = await this.run(async (client) => {
1304
- const joinSessionId = await this.selectDeviceJoinSession(client);
1305
- if (joinSessionId === null)
1306
- return null;
1307
- return client.cancelDeviceJoin(joinSessionId);
1308
- });
1309
- if (!result.ok)
1310
- return result;
1311
- this.pendingDeviceJoin = undefined;
1312
- this.activeDeviceJoinSessionId = undefined;
1313
- return { ok: true, value: { completed: true } };
1314
- }
1315
- /** Reliable-sync and project only Host-opaque device/request references. */
1316
- refreshDeviceManagement() {
1317
- return this.run(client => this.deviceManagementSnapshot(client));
1318
- }
1319
- async startDeviceJoinVerification(request) {
1320
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1321
- if (joinSessionId === undefined)
1322
- return { ok: false, error: failure('invalid-request') };
1323
- const result = await this.run(async (client) => {
1324
- await this.requireDeviceManager(client);
1325
- await client.syncDeviceManagement();
1326
- let notice = (await client.listLocalDeviceJoinRequests())
1327
- .find(value => value.joinSessionId === joinSessionId);
1328
- if (notice === undefined) {
1329
- throw Object.assign(new Error('request not found'), { name: 'AwikiSdkError', code: 'not-found' });
1330
- }
1331
- const localProgress = await this.localAdminJoinProgress(client, notice);
1332
- if (localProgress !== undefined)
1333
- return localProgress;
1334
- if (!notice.canStartVerification) {
1335
- throw Object.assign(new Error('request unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1336
- }
1337
- const operationId = `dsh-device-verify-${createHash('sha256').update(joinSessionId).digest('hex').slice(0, 32)}`;
1338
- let started;
1339
- try {
1340
- started = await client.startDeviceJoinVerification({
1341
- joinSessionId,
1342
- operationId,
1343
- challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS,
1344
- });
1345
- }
1346
- catch (error) {
1347
- await client.syncDeviceManagement();
1348
- notice = (await client.listLocalDeviceJoinRequests())
1349
- .find(value => value.joinSessionId === joinSessionId);
1350
- const recovered = notice === undefined ? undefined : await this.localAdminJoinProgress(client, notice);
1351
- if (recovered !== undefined)
1352
- return recovered;
1353
- throw error;
1354
- }
1355
- await client.syncDeviceManagement();
1356
- notice = (await client.listLocalDeviceJoinRequests())
1357
- .find(value => value.joinSessionId === joinSessionId);
1358
- if (notice === undefined)
1359
- return started;
1360
- return (await this.localAdminJoinProgress(client, notice)) ?? started;
1361
- });
1362
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1363
- }
1364
- async approveDeviceJoin(request) {
1365
- if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) {
1366
- return { ok: false, error: failure('invalid-request') };
1367
- }
1368
- const joinSessionId = this.requestSessions.get(request.requestRef);
1369
- if (joinSessionId === undefined || !/^\d{6}$/u.test(request.enteredSas)) {
1370
- return { ok: false, error: failure('invalid-request') };
1371
- }
1372
- const result = await this.run(async (client) => {
1373
- await this.requireDeviceManager(client);
1374
- const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1375
- if (progress.sas === undefined || !constantTimeSasMatches(progress.sas, request.enteredSas)) {
1376
- throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
1377
- }
1378
- const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
1379
- return client.confirmDeviceJoinApproval(prompt.approvalHandle);
1380
- });
1381
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1382
- }
1383
- async rejectDeviceJoin(request) {
1384
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1385
- if (joinSessionId === undefined
1386
- || (request.reason !== 'user_rejected' && request.reason !== 'sas_mismatch')) {
1387
- return { ok: false, error: failure('invalid-request') };
1388
- }
1389
- const result = await this.run(async (client) => {
1390
- await this.requireDeviceManager(client);
1391
- return client.rejectDeviceJoin(joinSessionId, request.reason);
1392
- });
1393
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1394
- }
1395
- async revokeDevice(request) {
1396
- if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) {
1397
- return { ok: false, error: failure('invalid-request') };
1398
- }
1399
- const deviceId = this.deviceIds.get(request.deviceRef);
1400
- if (deviceId === undefined)
1401
- return { ok: false, error: failure('invalid-request') };
1402
- return this.run(async (client) => {
1403
- await this.requireDeviceManager(client);
1404
- const devices = await client.getDeviceRegistry();
1405
- const target = devices.find(device => device.deviceId === deviceId);
1406
- if (target === undefined || target.isCurrent) {
1407
- throw Object.assign(new Error('device unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1408
- }
1409
- await client.revokeDevice(deviceId);
1410
- return this.deviceManagementSnapshot(client);
1411
- });
1186
+ const result = await this.run(client => client.registerIdentity(request));
1187
+ if (result.ok)
1188
+ await this.activateRegisteredIdentity(result.value);
1189
+ return result;
1412
1190
  }
1413
1191
  /**
1414
1192
  * Update the deployment identity's public WNS display name.
@@ -1434,16 +1212,10 @@ let AwikiService = (() => {
1434
1212
  }
1435
1213
  /** Start durable phone recovery for one existing full Handle. */
1436
1214
  sendRecoveryOtp(request) {
1437
- this.pendingDeviceJoin = undefined;
1438
1215
  const normalized = normalizeRecoveryOtpRequest(request);
1439
1216
  if (normalized === undefined)
1440
1217
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1441
- return this.run(async (client) => {
1442
- if (await this.selectDeviceJoinSession(client) !== null) {
1443
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1444
- }
1445
- return client.sendRecoveryOtp(normalized);
1446
- }, { allowSignedOut: true });
1218
+ return this.run(client => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
1447
1219
  }
1448
1220
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
1449
1221
  prepareRecovery(request) {
@@ -1581,6 +1353,16 @@ let AwikiService = (() => {
1581
1353
  }
1582
1354
  return this.run(client => client.removeGroupMember(groupDid, member));
1583
1355
  }
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
+ }
1584
1366
  /** Read presentation-only roster preferences for the active identity. */
1585
1367
  getConversationPreferences() {
1586
1368
  return this.run(async (client) => this.conversationPreferenceStore.get(await this.ownerDid(client)));
@@ -1801,7 +1583,7 @@ let AwikiService = (() => {
1801
1583
  }
1802
1584
  /** Return the deployment identity's public mailbox state. */
1803
1585
  getMailAccount() {
1804
- return this.run(client => client.getMailAccount());
1586
+ return this.run(client => client.getMailAccount(), { bindMailOwner: true });
1805
1587
  }
1806
1588
  /** List one bounded mailbox page on explicit browser/tool demand. */
1807
1589
  async listMailInbox(request) {
@@ -1812,11 +1594,11 @@ let AwikiService = (() => {
1812
1594
  catch {
1813
1595
  return { ok: false, error: failure('invalid-request') };
1814
1596
  }
1815
- return this.run(async (client) => {
1597
+ return this.run(async (client, _provider, ownerDid) => {
1816
1598
  if (normalized.folder !== 'sent')
1817
1599
  return client.listMailInbox(normalized);
1818
- return this.sentMailStore.list(await this.ownerDid(client), normalized);
1819
- });
1600
+ return this.sentMailStore.list(ownerDid, normalized);
1601
+ }, { bindMailOwner: true });
1820
1602
  }
1821
1603
  /** Read one bounded plain-text mail message. */
1822
1604
  async readMail(request) {
@@ -1827,45 +1609,116 @@ let AwikiService = (() => {
1827
1609
  catch {
1828
1610
  return { ok: false, error: failure('invalid-request') };
1829
1611
  }
1830
- return this.run(async (client) => {
1612
+ return this.run(async (client, _provider, ownerDid) => {
1831
1613
  if (!isLocalSentMailId(normalized.messageId))
1832
1614
  return client.readMail(normalized);
1833
- const local = await this.sentMailStore.read(await this.ownerDid(client), normalized.messageId);
1615
+ const local = await this.sentMailStore.read(ownerDid, normalized.messageId);
1834
1616
  if (local === undefined) {
1835
1617
  throw Object.assign(new Error('sent mail not found'), { name: 'AwikiSdkError', code: 'not-found' });
1836
1618
  }
1837
1619
  return local;
1838
- });
1620
+ }, { bindMailOwner: true });
1839
1621
  }
1840
1622
  /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1841
1623
  markMailRead(request) {
1842
- return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized));
1624
+ return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized), { bindMailOwner: true });
1843
1625
  }
1844
- /** Send one plain-text mail once. Browser callers require an explicit confirmation. */
1626
+ /** Send one mail once. Browser callers require an explicit confirmation. */
1845
1627
  async sendMail(request) {
1846
1628
  let normalized;
1847
1629
  try {
1848
- normalized = mailSendRequest(request);
1630
+ normalized = mailSendRequest(request, {
1631
+ maxCount: this.resolved.mailAttachmentMaxCount,
1632
+ maxBytes: this.resolved.mailAttachmentMaxBytes,
1633
+ totalMaxBytes: this.resolved.mailAttachmentTotalMaxBytes,
1634
+ });
1849
1635
  }
1850
1636
  catch {
1851
1637
  return { ok: false, error: failure('invalid-request') };
1852
1638
  }
1853
- return this.run(async (client) => {
1854
- const result = await client.sendMail(normalized);
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();
1855
1654
  if (!result.accepted)
1856
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;
1857
1661
  try {
1858
- const ownerDid = await this.ownerDid(client);
1859
1662
  const account = await client.getMailAccount().catch(() => undefined);
1860
- await this.sentMailStore.append(ownerDid, normalized, result, account);
1861
- return result;
1663
+ assertActive();
1664
+ await this.sentMailStore.append(ownerDid, normalized, settled, account);
1665
+ assertActive();
1666
+ return settled;
1862
1667
  }
1863
1668
  catch {
1864
- return result.warnings.length >= 100
1865
- ? result
1866
- : { ...result, warnings: [...result.warnings, 'Sent history could not be saved locally.'] };
1669
+ assertActive();
1670
+ return settled.warnings.length >= 100
1671
+ ? settled
1672
+ : { ...settled, warnings: [...settled.warnings, 'Sent history could not be saved locally.'] };
1867
1673
  }
1868
- });
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 });
1869
1722
  }
1870
1723
  /**
1871
1724
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -1880,11 +1733,11 @@ let AwikiService = (() => {
1880
1733
  return this.mutateSession(async () => {
1881
1734
  const provider = this.provider;
1882
1735
  if (provider !== undefined)
1883
- await this.stopProviderRuntime(provider);
1736
+ await this.stopListener(provider);
1884
1737
  const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
1885
1738
  if (!result.ok) {
1886
1739
  if (provider !== undefined && this.provider === provider)
1887
- this.ensureProviderRuntime(provider);
1740
+ void this.startListener(provider);
1888
1741
  return result;
1889
1742
  }
1890
1743
  try {
@@ -1894,10 +1747,10 @@ let AwikiService = (() => {
1894
1747
  await this.sessionStore.signIn();
1895
1748
  this.signedOut = false;
1896
1749
  this.activeIdentityDid = undefined;
1897
- this.pendingDeviceJoin = undefined;
1898
- this.activeDeviceJoinSessionId = undefined;
1899
1750
  this.invalidateSummaries();
1900
1751
  this.publishSession({ status: 'unregistered' });
1752
+ if (provider !== undefined && this.provider === provider)
1753
+ void this.startListener(provider);
1901
1754
  return result;
1902
1755
  }
1903
1756
  catch {
@@ -1905,7 +1758,7 @@ let AwikiService = (() => {
1905
1758
  }
1906
1759
  });
1907
1760
  }
1908
- /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
1761
+ /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
1909
1762
  async applyRecoveredSession(progress) {
1910
1763
  if (progress.phase !== 'applied')
1911
1764
  return true;
@@ -1924,211 +1777,34 @@ let AwikiService = (() => {
1924
1777
  this.activeIdentityDid = identity.did;
1925
1778
  this.invalidateSummaries();
1926
1779
  }
1927
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
1928
1780
  if (this.provider !== provider)
1929
1781
  return false;
1930
1782
  if (!alreadyActive) {
1931
1783
  const session = { status: 'active', identity };
1932
1784
  this.publishSession(session);
1933
- this.ensureProviderRuntime(provider);
1785
+ await provider.listenerStartup;
1786
+ await this.startListener(provider);
1934
1787
  }
1935
- return reconciled;
1788
+ return true;
1936
1789
  }
1937
1790
  catch {
1938
1791
  return false;
1939
1792
  }
1940
1793
  });
1941
1794
  }
1942
- /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
1943
- async reconcileRecoveredIdentity(provider, operationId) {
1944
- const logger = this.ctx.logger('awiki-recovery');
1945
- let mailboxRestored = false;
1946
- try {
1947
- await provider.client.getMailAccount();
1948
- mailboxRestored = true;
1949
- }
1950
- catch {
1951
- logger.warn('awiki: recovered mailbox reconciliation is pending');
1952
- }
1953
- const target = this.recoveryReconciliationTarget;
1954
- if (target === undefined)
1955
- return mailboxRestored;
1956
- try {
1957
- const authority = await provider.client.issueRecoveryAttestation({ operationId });
1958
- const response = await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
1959
- method: 'POST',
1960
- headers: { 'content-type': 'application/json' },
1961
- body: JSON.stringify({ attestation: authority.attestation }),
1962
- }), request => fetch(request));
1963
- if (!await acceptsRecoveryReconciliation(response)) {
1964
- logger.warn('awiki: recovered model account reconciliation is pending');
1965
- return false;
1966
- }
1967
- return mailboxRestored;
1968
- }
1969
- catch {
1970
- logger.warn('awiki: recovered model account reconciliation is pending');
1971
- return false;
1972
- }
1973
- }
1974
- /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
1975
- async selectDeviceJoinSession(client) {
1976
- const sessions = await client.listLocalDeviceJoinSessions();
1977
- if (this.activeDeviceJoinSessionId !== undefined) {
1978
- const tracked = sessions.find(value => (value.side === 'new_device' && value.joinSessionId === this.activeDeviceJoinSessionId));
1979
- if (tracked !== undefined)
1980
- return tracked.joinSessionId;
1981
- this.activeDeviceJoinSessionId = undefined;
1982
- }
1983
- const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
1984
- if (resumable.length > 1) {
1985
- throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
1986
- }
1987
- const selected = resumable[0];
1988
- this.activeDeviceJoinSessionId = selected?.joinSessionId;
1989
- return selected?.joinSessionId ?? null;
1990
- }
1991
- async applyCandidateJoinProgress(value) {
1992
- const phase = candidateJoinPhase(value);
1993
- if (phase === undefined)
1994
- return { ok: false, error: failure('remote') };
1995
- if (phase === 'cancelled' || phase === 'rejected' || phase === 'expired') {
1996
- this.activeDeviceJoinSessionId = undefined;
1997
- }
1998
- if (phase === 'authorized') {
1999
- if (value.identity === undefined)
2000
- return { ok: false, error: failure('remote') };
2001
- await this.activateRegisteredIdentity(value.identity);
2002
- }
2003
- return {
2004
- ok: true,
2005
- value: {
2006
- phase,
2007
- expiresAt: value.expiresAt,
2008
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2009
- completed: phase === 'authorized',
2010
- },
2011
- };
2012
- }
2013
- publicAdminJoinProgress(requestRef, value) {
2014
- const phase = adminJoinPhase(value);
2015
- if (phase === undefined)
2016
- return { ok: false, error: failure('remote') };
2017
- return {
2018
- ok: true,
2019
- value: {
2020
- requestRef,
2021
- phase,
2022
- expiresAt: value.expiresAt,
2023
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2024
- },
2025
- };
2026
- }
2027
- async localAdminJoinProgress(client, notice) {
2028
- const local = (await client.listLocalDeviceJoinSessions())
2029
- .find(session => session.side === 'admin' && session.joinSessionId === notice.joinSessionId);
2030
- if (local === undefined) {
2031
- return notice.claimedByCurrentDevice
2032
- ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId)
2033
- : undefined;
2034
- }
2035
- if (local.localPhase === 'challenge_prepared') {
2036
- return {
2037
- joinSessionId: notice.joinSessionId,
2038
- localPhase: local.localPhase,
2039
- remoteState: notice.state,
2040
- expiresAt: local.expiresAt,
2041
- };
2042
- }
2043
- return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2044
- }
2045
- async requireDeviceManager(client) {
2046
- const current = await client.getCurrentDeviceSummary();
2047
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2048
- throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2049
- }
2050
- }
2051
- requestRef(joinSessionId) {
2052
- const existing = this.requestRefs.get(joinSessionId);
2053
- if (existing !== undefined)
2054
- return existing;
2055
- const reference = `join-${randomUUID()}`;
2056
- this.requestRefs.set(joinSessionId, reference);
2057
- this.requestSessions.set(reference, joinSessionId);
2058
- return reference;
2059
- }
2060
- deviceRef(deviceId) {
2061
- const existing = this.deviceRefs.get(deviceId);
2062
- if (existing !== undefined)
2063
- return existing;
2064
- const reference = `device-${randomUUID()}`;
2065
- this.deviceRefs.set(deviceId, reference);
2066
- this.deviceIds.set(reference, deviceId);
2067
- return reference;
2068
- }
2069
- async deviceManagementSnapshot(client) {
2070
- await client.syncDeviceManagement();
2071
- const current = await client.getCurrentDeviceSummary();
2072
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2073
- this.requestRefs.clear();
2074
- this.requestSessions.clear();
2075
- this.deviceRefs.clear();
2076
- this.deviceIds.clear();
2077
- return {
2078
- canManage: false,
2079
- ...current.role === undefined ? {} : { role: current.role },
2080
- readiness: current.readiness,
2081
- devices: [],
2082
- requests: [],
2083
- };
2084
- }
2085
- const requests = await client.listLocalDeviceJoinRequests();
2086
- await Promise.all(requests
2087
- .filter(request => request.claimedByCurrentDevice)
2088
- .map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
2089
- const devices = await client.getDeviceRegistry();
2090
- return {
2091
- canManage: true,
2092
- role: 'admin',
2093
- readiness: 'admin_ready',
2094
- devices: devices.map(device => this.publicDevice(device)),
2095
- requests: requests.map(request => ({
2096
- requestRef: this.requestRef(request.joinSessionId),
2097
- candidateKeyFingerprint: request.candidateKeyFingerprint,
2098
- issuedAt: request.issuedAt,
2099
- expiresAt: request.expiresAt,
2100
- state: requestJoinPhase(request),
2101
- claimedByCurrentDevice: request.claimedByCurrentDevice,
2102
- canStartVerification: request.canStartVerification,
2103
- })),
2104
- };
2105
- }
2106
- publicDevice(device) {
2107
- return {
2108
- deviceRef: this.deviceRef(device.deviceId),
2109
- status: device.status,
2110
- role: device.role,
2111
- managementReady: device.managementReady,
2112
- isCurrent: device.isCurrent,
2113
- };
2114
- }
2115
- /** Publish one newly registered identity, then start realtime only in the background. */
1795
+ /** Publish one newly registered identity and start its listener through the existing session path. */
2116
1796
  async activateRegisteredIdentity(identity) {
2117
- this.pendingDeviceJoin = undefined;
2118
- this.activeDeviceJoinSessionId = undefined;
2119
1797
  this.activeIdentityDid = identity.did;
2120
1798
  this.publishSession({ status: 'active', identity });
2121
1799
  const provider = this.provider;
2122
- if (provider !== undefined)
2123
- this.ensureProviderRuntime(provider);
1800
+ if (provider !== undefined) {
1801
+ await provider.listenerStartup;
1802
+ await this.startListener(provider);
1803
+ }
2124
1804
  }
2125
1805
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
2126
1806
  invalidateSummaries() {
2127
1807
  this.sessionRevision += 1;
2128
- this.requestRefs.clear();
2129
- this.requestSessions.clear();
2130
- this.deviceRefs.clear();
2131
- this.deviceIds.clear();
2132
1808
  for (const controller of this.activeSummaryRequests)
2133
1809
  controller.abort();
2134
1810
  this.activeSummaryRequests.clear();
@@ -2139,24 +1815,40 @@ let AwikiService = (() => {
2139
1815
  }
2140
1816
  /** Resolve and cache the owner binding required by private Host-side projections. */
2141
1817
  async ownerDid(client) {
2142
- const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
2143
- const ownerDid = this.activeIdentityDid ?? identity?.did;
1818
+ const identity = await client.getIdentity();
1819
+ const ownerDid = identity?.did;
2144
1820
  if (ownerDid === undefined) {
2145
1821
  throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
2146
1822
  }
1823
+ if (this.activeIdentityDid !== undefined && this.activeIdentityDid !== ownerDid) {
1824
+ throw new ProviderUnavailableError();
1825
+ }
2147
1826
  this.activeIdentityDid = ownerDid;
2148
1827
  return ownerDid;
2149
1828
  }
2150
1829
  /** Invoke the current client and normalize every rejection to a public result. */
2151
1830
  async run(operation, options = {}) {
2152
1831
  try {
1832
+ const sessionRevision = this.sessionRevision;
2153
1833
  if (options.allowSignedOut !== true && await this.isSignedOut()) {
2154
1834
  return { ok: false, error: failure('signed-out') };
2155
1835
  }
2156
1836
  const provider = this.provider;
2157
1837
  if (provider === undefined)
2158
1838
  throw new ProviderUnavailableError();
2159
- const value = await operation(provider.client);
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();
2160
1852
  if (options.skipAttachmentByteValidation !== true && containsUnexpectedBinary(value, new Set())) {
2161
1853
  return { ok: false, error: failure('remote') };
2162
1854
  }
@@ -2167,7 +1859,7 @@ let AwikiService = (() => {
2167
1859
  }
2168
1860
  }
2169
1861
  /** Validate mail input before entering the provider and preserve fixed public failures. */
2170
- runValidatedMail(validate, operation) {
1862
+ runValidatedMail(validate, operation, options = {}) {
2171
1863
  let request;
2172
1864
  try {
2173
1865
  request = validate();
@@ -2175,7 +1867,7 @@ let AwikiService = (() => {
2175
1867
  catch {
2176
1868
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2177
1869
  }
2178
- return this.run(client => operation(client, request));
1870
+ return this.run(client => operation(client, request), options);
2179
1871
  }
2180
1872
  /** Read and cache the private Host-owned session marker. */
2181
1873
  async isSignedOut() {
@@ -2230,194 +1922,161 @@ let AwikiService = (() => {
2230
1922
  this.sessionMutation = pending.then(() => undefined, () => undefined);
2231
1923
  return pending;
2232
1924
  }
2233
- /** Start identity realtime and the optional Agent consumer without blocking identity success. */
2234
- ensureProviderRuntime(provider) {
2235
- const identityDid = this.activeIdentityDid;
2236
- if (provider.runtimeReplacement !== undefined)
2237
- return;
2238
- if (identityDid !== undefined
2239
- && provider.realtimeIdentityDid !== undefined
2240
- && provider.realtimeIdentityDid !== identityDid) {
2241
- this.replaceProviderRuntime(provider, identityDid);
2242
- return;
2243
- }
2244
- this.ensureRealtimeSupervisor(provider);
2245
- this.ensureAgentConsumer(provider);
2246
- }
2247
- replaceProviderRuntime(provider, identityDid) {
2248
- if (provider.runtimeReplacement !== undefined || this.provider !== provider)
2249
- return;
2250
- const replacement = (async () => {
2251
- await this.stopProviderRuntime(provider);
2252
- if (this.provider !== provider || this.activeIdentityDid !== identityDid)
2253
- return;
2254
- this.ensureRealtimeSupervisor(provider);
2255
- this.ensureAgentConsumer(provider);
2256
- })();
2257
- const observed = replacement
2258
- .catch((error) => {
2259
- this.ctx.logger('awiki-realtime').warn('AWiki identity realtime replacement failed: %s', error instanceof Error ? error.message : 'unknown failure');
2260
- })
2261
- .finally(() => {
2262
- if (provider.runtimeReplacement === observed)
2263
- delete provider.runtimeReplacement;
2264
- if (this.provider === provider)
2265
- this.ensureProviderRuntime(provider);
2266
- });
2267
- provider.runtimeReplacement = observed;
2268
- }
2269
- ensureRealtimeSupervisor(provider) {
2270
- if (!this.resolved.realtimeEnabled
2271
- || provider.realtimeSupervisor !== undefined
2272
- || provider.realtimeStartup !== undefined
2273
- || this.provider !== provider
2274
- || (provider.client.realtime ?? provider.client.listener) === undefined)
2275
- return;
2276
- const generation = provider.realtimeGeneration;
2277
- const source = provider.client.realtime ?? provider.client.listener;
2278
- if (source === undefined)
2279
- return;
2280
- const logger = this.ctx.logger('awiki-realtime');
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');
2281
1943
  const startup = (async () => {
2282
1944
  if (await this.isSignedOut())
2283
1945
  return;
2284
- const identity = await provider.client.getIdentity();
2285
- if (identity === null || !this.realtimeFenceMatches(provider, generation))
1946
+ if (await provider.client.getIdentity() === null)
1947
+ return;
1948
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
2286
1949
  return;
2287
- this.activeIdentityDid = identity.did;
2288
- const supervisor = new IdentityRealtimeSupervisor(source, {
2289
- onSynchronized: cause => this.onRealtimeSynchronized(provider, generation, cause),
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,
2290
1954
  }, logger);
2291
- provider.realtimeSupervisor = supervisor;
2292
- provider.realtimeIdentityDid = identity.did;
2293
- supervisor.start();
2294
- this.ensureAgentConsumer(provider);
2295
- })();
2296
- const observed = startup
2297
- .catch((error) => {
2298
- logger.warn('AWiki realtime startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2299
- })
2300
- .finally(() => {
2301
- if (provider.realtimeStartup === observed)
2302
- delete provider.realtimeStartup;
2303
- });
2304
- provider.realtimeStartup = observed;
2305
- }
2306
- realtimeFenceMatches(provider, generation) {
2307
- return this.provider === provider && provider.realtimeGeneration === generation;
2308
- }
2309
- async onRealtimeSynchronized(provider, generation, cause) {
2310
- if (!this.realtimeFenceMatches(provider, generation))
2311
- return;
2312
- if (cause === 'system_notification' || cause === 'stream_recovery') {
1955
+ provider.listener = listener;
2313
1956
  try {
2314
- provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
1957
+ await listener.start();
2315
1958
  }
2316
- catch {
2317
- this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
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;
2318
1972
  }
2319
- }
2320
- if (!['session_start', 'connection_ready', 'reconnected', 'message', 'message_update'].includes(cause))
2321
- return;
2322
- this.ensureAgentConsumer(provider);
2323
- await provider.agentConsumerStartup;
2324
- if (!this.realtimeFenceMatches(provider, generation))
2325
- return;
2326
- await provider.agentConsumer?.reconcileOnce();
2327
- }
2328
- ensureAgentConsumer(provider) {
2329
- const workspaceContext = this.workspaceContext;
2330
- const source = provider.client.agentInbox ?? provider.client.listener;
2331
- const identityDid = this.activeIdentityDid;
2332
- if (!this.resolved.listenerEnabled
2333
- || workspaceContext === undefined
2334
- || source === undefined
2335
- || identityDid === undefined
2336
- || this.provider !== provider)
2337
- return;
2338
- if (provider.agentConsumer !== undefined && provider.agentConsumerIdentityDid === identityDid)
2339
- return;
2340
- if (provider.agentConsumerStartup !== undefined)
2341
- return;
2342
- const generation = ++provider.agentConsumerGeneration;
2343
- const logger = this.ctx.logger('awiki-listener');
2344
- const startup = (async () => {
2345
- await this.detachAgentConsumer(provider);
2346
- if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
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
+ }
2347
1983
  return;
2348
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
2349
- const consumer = new AwikiAgentListener(source, agents, {
2350
- ...this.resolved.listener,
2351
- identityScope: identityDid,
2352
- }, logger);
2353
- provider.agentConsumer = consumer;
2354
- provider.agentConsumerIdentityDid = identityDid;
2355
- await consumer.reconcileOnce();
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
+ });
2356
1991
  })();
2357
1992
  const observed = startup
2358
1993
  .catch((error) => {
2359
- logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1994
+ logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2360
1995
  })
2361
1996
  .finally(() => {
2362
- if (provider.agentConsumerStartup === observed)
2363
- delete provider.agentConsumerStartup;
1997
+ if (provider.listenerStartup === observed)
1998
+ delete provider.listenerStartup;
2364
1999
  });
2365
- provider.agentConsumerStartup = observed;
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
+ }
2366
2025
  }
2367
- agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
2026
+ listenerFenceMatches(provider, workspaceContext, generation) {
2368
2027
  return this.provider === provider
2369
2028
  && this.workspaceContext === workspaceContext
2370
- && this.activeIdentityDid === identityDid
2371
- && provider.agentConsumerGeneration === generation;
2029
+ && provider.listenerGeneration === generation;
2372
2030
  }
2373
- async detachAgentConsumer(provider) {
2374
- const consumer = provider.agentConsumer;
2375
- if (consumer === undefined)
2031
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
2032
+ if (provider.listener !== listener)
2376
2033
  return;
2377
- delete provider.agentConsumer;
2378
- delete provider.agentConsumerIdentityDid;
2379
- const cleanup = consumer.dispose();
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
+ });
2380
2048
  const observed = cleanup.finally(() => {
2381
- if (provider.agentConsumerCleanup === observed)
2382
- delete provider.agentConsumerCleanup;
2049
+ if (provider.listenerCleanup === observed)
2050
+ delete provider.listenerCleanup;
2051
+ if (disposed)
2052
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
2383
2053
  });
2384
- provider.agentConsumerCleanup = observed;
2385
- await observed;
2386
- }
2387
- async stopAgentConsumer(provider) {
2388
- provider.agentConsumerGeneration += 1;
2389
- await provider.agentConsumerStartup;
2390
- await provider.agentConsumerCleanup;
2391
- await this.detachAgentConsumer(provider);
2392
- }
2393
- async stopRealtimeSupervisor(provider) {
2394
- provider.realtimeGeneration += 1;
2395
- await provider.realtimeStartup;
2396
- const supervisor = provider.realtimeSupervisor;
2397
- if (supervisor === undefined)
2398
- return;
2399
- delete provider.realtimeSupervisor;
2400
- delete provider.realtimeIdentityDid;
2401
- await supervisor.dispose();
2054
+ provider.listenerCleanup = observed;
2402
2055
  }
2403
- async stopProviderRuntime(provider) {
2404
- await Promise.all([
2405
- this.stopRealtimeSupervisor(provider),
2406
- this.stopAgentConsumer(provider),
2407
- ]);
2056
+ scheduleListenerRestart(provider, workspaceContext, generation) {
2057
+ if (provider.listenerRestartTimer !== undefined
2058
+ || !this.listenerFenceMatches(provider, workspaceContext, generation))
2059
+ 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;
2408
2072
  }
2409
2073
  /** Clear one exact provider slot before joining its one shared disposal. */
2410
2074
  disposeProvider(provider) {
2411
- if (this.provider === provider) {
2075
+ if (this.provider === provider)
2412
2076
  this.provider = undefined;
2413
- this.pendingDeviceJoin = undefined;
2414
- this.activeDeviceJoinSessionId = undefined;
2415
- this.invalidateSummaries();
2416
- }
2417
2077
  provider.disposal ??= (async () => {
2418
2078
  try {
2419
- await provider.runtimeReplacement;
2420
- await this.stopProviderRuntime(provider);
2079
+ await this.stopListener(provider);
2421
2080
  }
2422
2081
  finally {
2423
2082
  await provider.client.dispose();