@awiki/dsh-plugin 0.3.4 → 0.3.6

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 (88) hide show
  1. package/README.md +60 -51
  2. package/README.zh.md +51 -43
  3. package/cordis.patch.yml +7 -21
  4. package/lib/client.js +785 -1308
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +715 -1017
  7. package/lib/provider.js +16 -52
  8. package/lib/{sdk-adapter-BeJakxLP.mjs → sdk-adapter--trNlr-T.mjs} +38 -143
  9. package/lib/typert.host.js +139 -414
  10. package/lib/typert.remote-client.d.ts +5 -19
  11. package/lib/typert.remote-client.d.ts.map +1 -1
  12. package/lib/typert.remote-client.js +139 -414
  13. package/lib/types/client/AwikiIdentityAccess.d.ts +5 -6
  14. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  15. package/lib/types/client/AwikiIdentityAccess.js +27 -88
  16. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  17. package/lib/types/client/AwikiMail.d.ts +1 -1
  18. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  19. package/lib/types/client/AwikiMail.js +130 -6
  20. package/lib/types/client/AwikiMail.js.map +1 -1
  21. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  22. package/lib/types/client/AwikiOverlay.js +3 -4
  23. package/lib/types/client/AwikiOverlay.js.map +1 -1
  24. package/lib/types/client/controller.d.ts +11 -20
  25. package/lib/types/client/controller.d.ts.map +1 -1
  26. package/lib/types/client/controller.js +48 -53
  27. package/lib/types/client/controller.js.map +1 -1
  28. package/lib/types/client/index.d.ts.map +1 -1
  29. package/lib/types/client/index.js +2 -8
  30. package/lib/types/client/index.js.map +1 -1
  31. package/lib/types/client/mail-attachment.d.ts +40 -0
  32. package/lib/types/client/mail-attachment.d.ts.map +1 -0
  33. package/lib/types/client/mail-attachment.js +186 -0
  34. package/lib/types/client/mail-attachment.js.map +1 -0
  35. package/lib/types/client/settings-locales.js +8 -8
  36. package/lib/types/client/settings-locales.js.map +1 -1
  37. package/lib/types/client/slots.d.ts +6 -10
  38. package/lib/types/client/slots.d.ts.map +1 -1
  39. package/lib/types/index.d.ts +28 -59
  40. package/lib/types/index.d.ts.map +1 -1
  41. package/lib/types/index.js +417 -800
  42. package/lib/types/index.js.map +1 -1
  43. package/lib/types/listener-state.d.ts +3 -5
  44. package/lib/types/listener-state.d.ts.map +1 -1
  45. package/lib/types/listener-state.js +13 -28
  46. package/lib/types/listener-state.js.map +1 -1
  47. package/lib/types/listener.d.ts +32 -9
  48. package/lib/types/listener.d.ts.map +1 -1
  49. package/lib/types/listener.js +127 -11
  50. package/lib/types/listener.js.map +1 -1
  51. package/lib/types/mail.d.ts +28 -2
  52. package/lib/types/mail.d.ts.map +1 -1
  53. package/lib/types/mail.js +78 -2
  54. package/lib/types/mail.js.map +1 -1
  55. package/lib/types/profile-state.d.ts +17 -0
  56. package/lib/types/profile-state.d.ts.map +1 -0
  57. package/lib/types/profile-state.js +58 -0
  58. package/lib/types/profile-state.js.map +1 -0
  59. package/lib/types/provider-api.d.ts +36 -100
  60. package/lib/types/provider-api.d.ts.map +1 -1
  61. package/lib/types/provider.d.ts +1 -7
  62. package/lib/types/provider.d.ts.map +1 -1
  63. package/lib/types/provider.js +15 -53
  64. package/lib/types/provider.js.map +1 -1
  65. package/lib/types/sdk-adapter.d.ts +5 -30
  66. package/lib/types/sdk-adapter.d.ts.map +1 -1
  67. package/lib/types/sdk-adapter.js +48 -133
  68. package/lib/types/sdk-adapter.js.map +1 -1
  69. package/lib/types/sent-mail-store.d.ts +7 -2
  70. package/lib/types/sent-mail-store.d.ts.map +1 -1
  71. package/lib/types/sent-mail-store.js +125 -23
  72. package/lib/types/sent-mail-store.js.map +1 -1
  73. package/lib/types/tools.d.ts +1 -1
  74. package/lib/types/tools.d.ts.map +1 -1
  75. package/lib/types/tools.js +20 -4
  76. package/lib/types/tools.js.map +1 -1
  77. package/lib/types/types.d.ts +45 -77
  78. package/lib/types/types.d.ts.map +1 -1
  79. package/lib/types/types.js.map +1 -1
  80. package/package.json +4 -6
  81. package/lib/types/client/AwikiDevices.d.ts +0 -28
  82. package/lib/types/client/AwikiDevices.d.ts.map +0 -1
  83. package/lib/types/client/AwikiDevices.js +0 -85
  84. package/lib/types/client/AwikiDevices.js.map +0 -1
  85. package/lib/types/realtime-supervisor.d.ts +0 -43
  86. package/lib/types/realtime-supervisor.d.ts.map +0 -1
  87. package/lib/types/realtime-supervisor.js +0 -166
  88. 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';
@@ -44,7 +45,7 @@ import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "
44
45
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
45
46
  import { downloadedAttachment } from "./sdk-adapter.js";
46
47
  import { registerAwikiTools } from "./tools.js";
47
- import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
48
+ 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
49
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
49
50
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
50
51
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
@@ -54,7 +55,7 @@ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "
54
55
  import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
55
56
  import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
56
57
  import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
57
- import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
58
+ import { resolveAwikiProfileName, resolveAwikiStateRoot, resolveAwikiTenantStateRoot, } from "./profile-state.js";
58
59
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
59
60
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
60
61
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
@@ -75,19 +76,21 @@ export const DEFAULT_SUMMARY_MAX_INPUT_BYTES = 32 * 1024;
75
76
  export const MAX_SUMMARY_MESSAGES = 50;
76
77
  /** Loader schema for the Host deployment configuration. */
77
78
  export const Config = z.object({
78
- userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
79
+ userServiceUrl: z.string(),
79
80
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
80
- messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
81
+ messageServiceUrl: z.string(),
81
82
  mailServiceUrl: z.string(),
82
- messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
83
- messageServiceDid: z.string().default(DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
83
+ messageServicePublicUrl: z.string(),
84
+ messageServiceDid: z.string(),
84
85
  allowedAttachmentOrigins: z.array(z.string()).default([]),
85
86
  allowInsecureLoopbackForTesting: z.boolean().default(false),
86
87
  stateRoot: z.string(),
87
88
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
89
+ mailAttachmentMaxCount: z.number().default(MAIL_ATTACHMENT_SERVICE_MAX_COUNT),
90
+ mailAttachmentMaxBytes: z.number().default(MAIL_ATTACHMENT_SERVICE_MAX_BYTES),
91
+ mailAttachmentTotalMaxBytes: z.number().default(MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES),
88
92
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
89
93
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
90
- realtimeEnabled: z.boolean().default(true),
91
94
  listenerEnabled: z.boolean().default(false),
92
95
  listenerAllowedPeers: z.array(z.string()).default([]),
93
96
  listenerWorkspacePath: z.string(),
@@ -105,6 +108,7 @@ const FAILURE_CODES = new Set([
105
108
  'forbidden',
106
109
  'identity-recovery-required',
107
110
  'conflict',
111
+ 'state-in-use',
108
112
  'rate-limited',
109
113
  'group-membership-required',
110
114
  'group-identity-stale',
@@ -118,17 +122,10 @@ const FAILURE_CODES = new Set([
118
122
  'network',
119
123
  'remote',
120
124
  ]);
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
- ]);
125
+ const LISTENER_RESTART_BASE_DELAY_MS = 1_000;
126
+ const LISTENER_RESTART_MAX_DELAY_MS = 30_000;
127
+ const LISTENER_RESTART_MAX_ATTEMPT = 6;
128
+ const LISTENER_STABLE_RESET_MS = 60_000;
132
129
  const FAILURE_MESSAGES = {
133
130
  'not-registered': 'No AWiki identity is registered for this deployment.',
134
131
  'signed-out': 'This installation is signed out of AWiki.',
@@ -141,6 +138,7 @@ const FAILURE_MESSAGES = {
141
138
  'forbidden': 'The AWiki operation is not permitted.',
142
139
  'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
143
140
  'conflict': 'The AWiki operation conflicts with current state.',
141
+ 'state-in-use': 'AWiki data is already open in another client.',
144
142
  'rate-limited': 'The AWiki service rate-limited the request.',
145
143
  'group-membership-required': 'The active AWiki identity is not a member of this group.',
146
144
  'group-identity-stale': 'The AWiki group identity binding is still recovering.',
@@ -158,67 +156,6 @@ class ProviderUnavailableError extends Error {
158
156
  }
159
157
  class SummaryProviderUnavailableError extends Error {
160
158
  }
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
159
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
223
160
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
224
161
  let url;
@@ -249,30 +186,6 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
249
186
  }
250
187
  return new URL('/api/identity-recovery', parsed).toString();
251
188
  }
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
189
  /** Validate a provider domain without inferring it from an API endpoint. */
277
190
  function serviceDomain(raw, field = 'userServiceDomain') {
278
191
  return normalizeAwikiDomain(raw, field);
@@ -320,22 +233,45 @@ function listenerAllowedPeers(raw, enabled) {
320
233
  return peers;
321
234
  }
322
235
  /** Resolve and validate every deployment choice before publishing the service. */
323
- function resolveConfig(config) {
236
+ function resolveConfig(ctx, config) {
324
237
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
325
238
  const configuredStateRoot = config.stateRoot?.trim();
326
239
  const configuredDshHome = process.env.DSH_HOME?.trim();
327
240
  const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
328
241
  ? join(homedir(), '.dsh')
329
242
  : configuredDshHome;
243
+ const profileName = configuredStateRoot === undefined || configuredStateRoot.length === 0
244
+ ? resolveAwikiProfileName(ctx, dshHome)
245
+ : undefined;
330
246
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
331
- ? join(dshHome, 'awiki', 'im-core')
247
+ ? resolveAwikiStateRoot(ctx, dshHome)
332
248
  : configuredStateRoot;
249
+ const legacySharedStateDetected = profileName !== undefined
250
+ && existsSync(join(dshHome, 'awiki', 'im-core'));
333
251
  if (!isAbsolute(stateRoot))
334
252
  throw new TypeError('awiki: stateRoot must be an absolute path');
335
253
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
336
254
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) {
337
255
  throw new TypeError('awiki: attachmentMaxBytes must be a positive safe integer');
338
256
  }
257
+ const mailAttachmentMaxCount = config.mailAttachmentMaxCount ?? MAIL_ATTACHMENT_SERVICE_MAX_COUNT;
258
+ if (!Number.isSafeInteger(mailAttachmentMaxCount)
259
+ || mailAttachmentMaxCount < 0
260
+ || mailAttachmentMaxCount > MAIL_ATTACHMENT_SERVICE_MAX_COUNT) {
261
+ throw new TypeError('awiki: mailAttachmentMaxCount must be an integer from 0 through 10');
262
+ }
263
+ const mailAttachmentMaxBytes = config.mailAttachmentMaxBytes ?? MAIL_ATTACHMENT_SERVICE_MAX_BYTES;
264
+ if (!Number.isSafeInteger(mailAttachmentMaxBytes)
265
+ || mailAttachmentMaxBytes < 1
266
+ || mailAttachmentMaxBytes > MAIL_ATTACHMENT_SERVICE_MAX_BYTES) {
267
+ throw new TypeError('awiki: mailAttachmentMaxBytes must be a positive safe integer no greater than 10 MiB');
268
+ }
269
+ const mailAttachmentTotalMaxBytes = config.mailAttachmentTotalMaxBytes ?? MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES;
270
+ if (!Number.isSafeInteger(mailAttachmentTotalMaxBytes)
271
+ || mailAttachmentTotalMaxBytes < mailAttachmentMaxBytes
272
+ || mailAttachmentTotalMaxBytes > MAIL_ATTACHMENT_SERVICE_TOTAL_MAX_BYTES) {
273
+ throw new TypeError('awiki: mailAttachmentTotalMaxBytes must cover one attachment and not exceed 18 MiB');
274
+ }
339
275
  const imageAttachmentCacheMaxBytes = config.imageAttachmentCacheMaxBytes ?? DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES;
340
276
  if (!Number.isSafeInteger(imageAttachmentCacheMaxBytes)
341
277
  || imageAttachmentCacheMaxBytes < minimumImageAttachmentCacheMaxBytes(attachmentMaxBytes)) {
@@ -346,10 +282,6 @@ function resolveConfig(config) {
346
282
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
347
283
  }
348
284
  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
285
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
354
286
  if (!isAbsolute(listenerWorkspacePath)) {
355
287
  throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
@@ -364,6 +296,7 @@ function resolveConfig(config) {
364
296
  const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
365
297
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
366
298
  return {
299
+ tenantDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
367
300
  userServiceUrl,
368
301
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
369
302
  messageServiceUrl,
@@ -374,9 +307,12 @@ function resolveConfig(config) {
374
307
  allowInsecureLoopbackForTesting,
375
308
  stateRoot,
376
309
  attachmentMaxBytes,
310
+ mailAttachmentMaxCount,
311
+ mailAttachmentMaxBytes,
312
+ mailAttachmentTotalMaxBytes,
377
313
  imageAttachmentCacheMaxBytes,
378
314
  pollIntervalMs,
379
- realtimeEnabled,
315
+ ...profileName === undefined ? {} : { profileName, legacySharedStateDetected },
380
316
  listenerEnabled,
381
317
  listener: {
382
318
  allowedPeers,
@@ -384,6 +320,42 @@ function resolveConfig(config) {
384
320
  stateRoot,
385
321
  },
386
322
  summaryMaxInputBytes,
323
+ deriveUserServiceUrl: config.userServiceUrl === undefined,
324
+ deriveMessageServiceUrl: config.messageServiceUrl === undefined,
325
+ deriveMailServiceUrl: config.mailServiceUrl === undefined,
326
+ deriveMessageServicePublicUrl: config.messageServicePublicUrl === undefined,
327
+ deriveMessageServiceDid: config.messageServiceDid === undefined,
328
+ deriveAllowedAttachmentOrigins: config.allowedAttachmentOrigins === undefined
329
+ || config.allowedAttachmentOrigins.length === 0,
330
+ };
331
+ }
332
+ /** Resolve every tenant-sensitive endpoint and state path from one selected domain. */
333
+ function activeTenantConfig(config, domain) {
334
+ const tenantOrigin = `https://${domain}`;
335
+ const userServiceUrl = config.deriveUserServiceUrl ? tenantOrigin : config.userServiceUrl;
336
+ const messageServiceUrl = config.deriveMessageServiceUrl ? tenantOrigin : config.messageServiceUrl;
337
+ const mailServiceUrl = config.deriveMailServiceUrl ? userServiceUrl : config.mailServiceUrl;
338
+ const messageServicePublicUrl = config.deriveMessageServicePublicUrl
339
+ ? tenantOrigin
340
+ : config.messageServicePublicUrl;
341
+ const messageServiceDid = config.deriveMessageServiceDid
342
+ ? `did:wba:${domain}`
343
+ : config.messageServiceDid;
344
+ const allowedAttachmentOrigins = config.deriveAllowedAttachmentOrigins
345
+ ? attachmentOrigins(undefined, messageServicePublicUrl, config.allowInsecureLoopbackForTesting)
346
+ : config.allowedAttachmentOrigins;
347
+ return {
348
+ domain,
349
+ userServiceUrl,
350
+ userServiceDomain: domain,
351
+ messageServiceUrl,
352
+ mailServiceUrl,
353
+ messageServicePublicUrl,
354
+ messageServiceDid,
355
+ allowedAttachmentOrigins,
356
+ attachmentMaxBytes: config.attachmentMaxBytes,
357
+ allowInsecureLoopbackForTesting: config.allowInsecureLoopbackForTesting,
358
+ stateRoot: resolveAwikiTenantStateRoot(config.stateRoot, domain, config.userServiceDomain),
387
359
  };
388
360
  }
389
361
  /** Return a public, fixed-message failure without retaining a thrown value. */
@@ -537,8 +509,6 @@ function normalizeRecoveryPrepareRequest(request) {
537
509
  return { ...operation, phone, otp };
538
510
  }
539
511
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
540
- const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
541
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
542
512
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
543
513
  async function readBoundedResponseText(response, maxBytes) {
544
514
  const declaredLength = response.headers.get('content-length');
@@ -767,14 +737,6 @@ let AwikiService = (() => {
767
737
  let _inspectIdentityAccess_decorators;
768
738
  let _sendRegistrationOtp_decorators;
769
739
  let _registerIdentity_decorators;
770
- let _beginDeviceJoin_decorators;
771
- let _getDeviceJoinStatus_decorators;
772
- let _cancelDeviceJoin_decorators;
773
- let _refreshDeviceManagement_decorators;
774
- let _startDeviceJoinVerification_decorators;
775
- let _approveDeviceJoin_decorators;
776
- let _rejectDeviceJoin_decorators;
777
- let _revokeDevice_decorators;
778
740
  let _updateDisplayName_decorators;
779
741
  let _getProfile_decorators;
780
742
  let _updateProfile_decorators;
@@ -808,6 +770,7 @@ let AwikiService = (() => {
808
770
  let _readMail_decorators;
809
771
  let _markMailRead_decorators;
810
772
  let _sendMail_decorators;
773
+ let _downloadMailAttachment_decorators;
811
774
  let _clearLocalData_decorators;
812
775
  return class AwikiService extends _classSuper {
813
776
  static {
@@ -820,14 +783,6 @@ let AwikiService = (() => {
820
783
  _inspectIdentityAccess_decorators = [Remote];
821
784
  _sendRegistrationOtp_decorators = [Remote];
822
785
  _registerIdentity_decorators = [Remote];
823
- _beginDeviceJoin_decorators = [Remote];
824
- _getDeviceJoinStatus_decorators = [Remote];
825
- _cancelDeviceJoin_decorators = [Remote];
826
- _refreshDeviceManagement_decorators = [Remote];
827
- _startDeviceJoinVerification_decorators = [Remote];
828
- _approveDeviceJoin_decorators = [Remote];
829
- _rejectDeviceJoin_decorators = [Remote];
830
- _revokeDevice_decorators = [Remote];
831
786
  _updateDisplayName_decorators = [Remote];
832
787
  _getProfile_decorators = [Remote];
833
788
  _updateProfile_decorators = [Remote];
@@ -861,6 +816,7 @@ let AwikiService = (() => {
861
816
  _readMail_decorators = [Remote];
862
817
  _markMailRead_decorators = [Remote];
863
818
  _sendMail_decorators = [Remote];
819
+ _downloadMailAttachment_decorators = [Remote];
864
820
  _clearLocalData_decorators = [Remote];
865
821
  __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);
866
822
  __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);
@@ -870,14 +826,6 @@ let AwikiService = (() => {
870
826
  __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);
871
827
  __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);
872
828
  __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);
873
- __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);
874
- __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);
875
- __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);
876
- __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);
877
- __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);
878
- __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);
879
- __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);
880
- __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);
881
829
  __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);
882
830
  __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);
883
831
  __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);
@@ -911,16 +859,15 @@ let AwikiService = (() => {
911
859
  __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);
912
860
  __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);
913
861
  __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);
862
+ __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);
914
863
  __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);
915
864
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
916
865
  }
917
- static inject = ['tools'];
866
+ static inject = ['tools', 'settings'];
918
867
  static Config = Config;
919
868
  resolved = __runInitializers(this, _instanceExtraInitializers);
920
- sessionStore;
921
- imageAttachmentCache;
922
- sentMailStore;
923
- conversationPreferenceStore;
869
+ tenantConfigCache;
870
+ tenantStateCache;
924
871
  startupUserServiceDomain;
925
872
  settingsProvider;
926
873
  provider;
@@ -928,12 +875,6 @@ let AwikiService = (() => {
928
875
  sessionMutation = Promise.resolve();
929
876
  sessionRevision = 0;
930
877
  activeIdentityDid;
931
- pendingDeviceJoin;
932
- activeDeviceJoinSessionId;
933
- requestRefs = new Map();
934
- requestSessions = new Map();
935
- deviceRefs = new Map();
936
- deviceIds = new Map();
937
878
  activeSummaryRequests = new Set();
938
879
  summaryProvider;
939
880
  recoveryReconciliationTarget;
@@ -948,29 +889,23 @@ let AwikiService = (() => {
948
889
  constructor(ctx, config) {
949
890
  super(ctx, 'awiki');
950
891
  this.hostContext = ctx;
951
- this.resolved = resolveConfig(config);
892
+ this.resolved = resolveConfig(ctx, config);
952
893
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
953
- this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
954
- this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
955
- this.sentMailStore = new AwikiSentMailStore(this.resolved.stateRoot);
956
- this.conversationPreferenceStore = new AwikiConversationPreferenceStore(this.resolved.stateRoot);
957
894
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
958
- ctx.inject(['settings'], (settingsCtx) => {
959
- const provider = settingsCtx.settings;
960
- const settingsScope = settingsCtx.settings.register(settingsNamespace(AWIKI_SETTINGS_NAMESPACE), AwikiSettingsSchema, {
961
- base: { domain: this.resolved.userServiceDomain },
962
- applies: 'restart',
963
- validate: validateAwikiSettings,
964
- });
965
- this.settingsProvider = provider;
966
- this.startupUserServiceDomain = settingsScope.get().domain;
967
- settingsCtx.effect(() => () => {
968
- if (this.settingsProvider === provider) {
969
- this.settingsProvider = undefined;
970
- this.startupUserServiceDomain = this.resolved.userServiceDomain;
971
- }
972
- }, 'awiki: release settings namespace');
895
+ const provider = ctx.settings;
896
+ const settingsScope = provider.register(settingsNamespace(AWIKI_SETTINGS_NAMESPACE), AwikiSettingsSchema, {
897
+ base: { domain: this.resolved.userServiceDomain },
898
+ applies: 'restart',
899
+ validate: validateAwikiSettings,
973
900
  });
901
+ this.settingsProvider = provider;
902
+ this.startupUserServiceDomain = settingsScope.get().domain;
903
+ ctx.effect(() => () => {
904
+ if (this.settingsProvider === provider) {
905
+ this.settingsProvider = undefined;
906
+ this.startupUserServiceDomain = this.resolved.userServiceDomain;
907
+ }
908
+ }, 'awiki: release settings namespace');
974
909
  ctx.inject(['connection'], (connectionCtx) => {
975
910
  connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
976
911
  });
@@ -978,14 +913,14 @@ let AwikiService = (() => {
978
913
  this.workspaceContext = workspaceCtx;
979
914
  const provider = this.provider;
980
915
  if (provider !== undefined)
981
- this.ensureAgentConsumer(provider);
916
+ void this.startListener(provider);
982
917
  workspaceCtx.effect(() => async () => {
983
918
  if (this.workspaceContext !== workspaceCtx)
984
919
  return;
985
920
  this.workspaceContext = undefined;
986
921
  const current = this.provider;
987
922
  if (current !== undefined)
988
- await this.stopAgentConsumer(current);
923
+ await this.stopListener(current);
989
924
  }, 'awiki: release Workspace listener composition');
990
925
  });
991
926
  registerAwikiTools(ctx, this);
@@ -1008,45 +943,17 @@ let AwikiService = (() => {
1008
943
  registerClientFactory(factory) {
1009
944
  if (this.provider !== undefined)
1010
945
  throw new Error('awiki: a client provider is already registered');
1011
- this.pendingDeviceJoin = undefined;
1012
- this.activeDeviceJoinSessionId = undefined;
1013
- const client = factory({
1014
- userServiceUrl: this.resolved.userServiceUrl,
1015
- userServiceDomain: this.startupUserServiceDomain,
1016
- messageServiceUrl: this.resolved.messageServiceUrl,
1017
- mailServiceUrl: this.resolved.mailServiceUrl,
1018
- messageServicePublicUrl: this.resolved.messageServicePublicUrl,
1019
- messageServiceDid: this.resolved.messageServiceDid,
1020
- allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
1021
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1022
- allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
1023
- stateRoot: this.resolved.stateRoot,
1024
- });
946
+ const client = factory(this.activeTenant());
1025
947
  const provider = {
1026
948
  client,
1027
- realtimeGeneration: 0,
1028
- agentConsumerGeneration: 0,
1029
- localDeviceJoinRequestCountAfterSync: 0,
949
+ listenerRestartAttempt: 0,
950
+ listenerGeneration: 0,
951
+ listenerRecoveryBlocked: false,
1030
952
  };
1031
953
  this.provider = provider;
1032
- this.ensureRealtimeSupervisor(provider);
1033
- this.ensureAgentConsumer(provider);
954
+ void this.startListener(provider);
1034
955
  return () => this.disposeProvider(provider);
1035
956
  }
1036
- /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
1037
- getRealtimeDiagnostics() {
1038
- const provider = this.provider;
1039
- const realtime = provider?.realtimeSupervisor?.diagnostics() ?? {
1040
- connected: false,
1041
- activeSessionCount: 0,
1042
- startCount: 0,
1043
- stopCount: 0,
1044
- };
1045
- return {
1046
- ...realtime,
1047
- localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
1048
- };
1049
- }
1050
957
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
1051
958
  registerRecoveryReconciliationTarget(target) {
1052
959
  if (this.recoveryReconciliationTarget !== undefined) {
@@ -1084,43 +991,58 @@ let AwikiService = (() => {
1084
991
  * Read settings needed by the browser presentation.
1085
992
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
1086
993
  */
1087
- async getConfig() {
1088
- let handleRecoveryPhoneEnabled = false;
1089
- const abort = new AbortController();
1090
- const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1091
- try {
1092
- const response = await fetch(new URL('/user-service/v1/server-info', this.resolved.userServiceUrl), {
1093
- method: 'GET',
1094
- headers: { accept: 'application/json', 'cache-control': 'no-store' },
1095
- cache: 'no-store',
1096
- redirect: 'error',
1097
- signal: abort.signal,
1098
- });
1099
- if (response.ok) {
1100
- const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1101
- const value = text === undefined ? undefined : JSON.parse(text);
1102
- if (typeof value === 'object' && value !== null && value.schema_version === 1) {
1103
- const methods = value.identity?.handle_recovery?.methods;
1104
- handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1105
- && method.id === 'phone'
1106
- && method.enabled === true
1107
- && method.verification?.required === true
1108
- && method.verification?.type === 'sms_otp'));
1109
- }
1110
- }
1111
- }
1112
- catch { }
1113
- finally {
1114
- clearTimeout(timeout);
1115
- }
1116
- return {
994
+ getConfig() {
995
+ return Promise.resolve({
1117
996
  ok: true,
1118
997
  value: {
998
+ tenantDomain: this.activeTenant().domain,
1119
999
  pollIntervalMs: this.resolved.pollIntervalMs,
1120
1000
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1121
- handleRecoveryPhoneEnabled,
1001
+ mailAttachmentMaxCount: this.resolved.mailAttachmentMaxCount,
1002
+ mailAttachmentMaxBytes: this.resolved.mailAttachmentMaxBytes,
1003
+ mailAttachmentTotalMaxBytes: this.resolved.mailAttachmentTotalMaxBytes,
1004
+ ...this.resolved.profileName === undefined ? {} : {
1005
+ profileName: this.resolved.profileName,
1006
+ legacySharedStateDetected: this.resolved.legacySharedStateDetected,
1007
+ },
1122
1008
  },
1123
- };
1009
+ });
1010
+ }
1011
+ activeTenant() {
1012
+ const cached = this.tenantConfigCache;
1013
+ if (cached !== undefined)
1014
+ return cached;
1015
+ const value = Object.freeze(activeTenantConfig(this.resolved, this.startupUserServiceDomain));
1016
+ this.tenantConfigCache = value;
1017
+ return value;
1018
+ }
1019
+ activeTenantState() {
1020
+ const cached = this.tenantStateCache;
1021
+ if (cached !== undefined)
1022
+ return cached;
1023
+ const tenant = this.activeTenant();
1024
+ const value = Object.freeze({
1025
+ domain: tenant.domain,
1026
+ stateRoot: tenant.stateRoot,
1027
+ sessionStore: new AwikiSessionStore(tenant.stateRoot),
1028
+ imageAttachmentCache: new AwikiImageAttachmentCache(tenant.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes),
1029
+ sentMailStore: new AwikiSentMailStore(tenant.stateRoot),
1030
+ conversationPreferenceStore: new AwikiConversationPreferenceStore(tenant.stateRoot),
1031
+ });
1032
+ this.tenantStateCache = value;
1033
+ return value;
1034
+ }
1035
+ get sessionStore() {
1036
+ return this.activeTenantState().sessionStore;
1037
+ }
1038
+ get imageAttachmentCache() {
1039
+ return this.activeTenantState().imageAttachmentCache;
1040
+ }
1041
+ get sentMailStore() {
1042
+ return this.activeTenantState().sentMailStore;
1043
+ }
1044
+ get conversationPreferenceStore() {
1045
+ return this.activeTenantState().conversationPreferenceStore;
1124
1046
  }
1125
1047
  /**
1126
1048
  * Read the deployment's identity status.
@@ -1142,9 +1064,6 @@ let AwikiService = (() => {
1142
1064
  if (!identity.ok)
1143
1065
  return identity;
1144
1066
  this.activeIdentityDid = identity.value?.did;
1145
- const provider = this.provider;
1146
- if (identity.value !== null && provider !== undefined)
1147
- this.ensureProviderRuntime(provider);
1148
1067
  return identity.value === null
1149
1068
  ? { ok: true, value: { status: 'unregistered' } }
1150
1069
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -1171,7 +1090,7 @@ let AwikiService = (() => {
1171
1090
  this.publishSession(session);
1172
1091
  const provider = this.provider;
1173
1092
  if (provider !== undefined)
1174
- await this.stopProviderRuntime(provider);
1093
+ await this.stopListener(provider);
1175
1094
  return { ok: true, value: session };
1176
1095
  }
1177
1096
  catch {
@@ -1196,7 +1115,7 @@ let AwikiService = (() => {
1196
1115
  this.publishSession(session);
1197
1116
  const provider = this.provider;
1198
1117
  if (provider !== undefined)
1199
- this.ensureProviderRuntime(provider);
1118
+ void this.startListener(provider);
1200
1119
  return { ok: true, value: session };
1201
1120
  }
1202
1121
  catch {
@@ -1209,7 +1128,7 @@ let AwikiService = (() => {
1209
1128
  const target = identityAccessTarget(request, this.startupUserServiceDomain);
1210
1129
  if (target === undefined)
1211
1130
  return { ok: false, error: failure('invalid-request') };
1212
- const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.resolved.userServiceUrl);
1131
+ const endpoint = new URL(`/.well-known/handle/${encodeURIComponent(target.localPart)}`, this.activeTenant().userServiceUrl);
1213
1132
  const abort = new AbortController();
1214
1133
  const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1215
1134
  let response;
@@ -1254,14 +1173,8 @@ let AwikiService = (() => {
1254
1173
  return { ok: false, error: failure('remote') };
1255
1174
  }
1256
1175
  }
1257
- async sendRegistrationOtp(request) {
1258
- this.pendingDeviceJoin = undefined;
1259
- return this.run(async (client) => {
1260
- if (await this.selectDeviceJoinSession(client) !== null) {
1261
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1262
- }
1263
- return client.sendRegistrationOtp(request);
1264
- });
1176
+ sendRegistrationOtp(request) {
1177
+ return this.run(client => client.sendRegistrationOtp(request));
1265
1178
  }
1266
1179
  /**
1267
1180
  * Register and persist the deployment's only AWiki identity.
@@ -1269,177 +1182,10 @@ let AwikiService = (() => {
1269
1182
  * @returns The new public identity or a closed failure.
1270
1183
  */
1271
1184
  async registerIdentity(request) {
1272
- this.pendingDeviceJoin = undefined;
1273
- const result = await this.run(async (client) => {
1274
- if (await this.selectDeviceJoinSession(client) !== null) {
1275
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1276
- }
1277
- return client.registerIdentity(request);
1278
- });
1279
- if (!result.ok)
1280
- return result;
1281
- if (result.value.status === 'registered') {
1282
- await this.activateRegisteredIdentity(result.value.identity);
1283
- return { ok: true, value: { status: 'registered', identity: result.value.identity } };
1284
- }
1285
- this.pendingDeviceJoin = {
1286
- continuationId: result.value.continuationId,
1287
- fullHandle: result.value.fullHandle,
1288
- mode: result.value.mode,
1289
- requiresUserPresence: result.value.requiresUserPresence,
1290
- };
1291
- return {
1292
- ok: true,
1293
- value: {
1294
- status: 'join-required',
1295
- fullHandle: result.value.fullHandle,
1296
- mode: result.value.mode,
1297
- requiresUserPresence: result.value.requiresUserPresence,
1298
- },
1299
- };
1300
- }
1301
- /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
1302
- async beginDeviceJoin() {
1303
- const continuation = this.pendingDeviceJoin;
1304
- if (continuation === undefined)
1305
- return { ok: false, error: failure('conflict') };
1306
- if (continuation.mode !== 'ordinary' || continuation.requiresUserPresence) {
1307
- return { ok: false, error: failure('forbidden') };
1308
- }
1309
- this.pendingDeviceJoin = undefined;
1310
- const result = await this.run(client => client.beginDeviceJoin({
1311
- continuationId: continuation.continuationId,
1312
- operationId: `dsh-device-join-${randomUUID()}`,
1313
- userPresenceConfirmed: false,
1314
- }));
1315
- if (!result.ok)
1316
- return result;
1317
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1318
- return this.applyCandidateJoinProgress(result.value);
1319
- }
1320
- /** Restore from Core local_sessions and advance only the exact resumable Join. */
1321
- async getDeviceJoinStatus() {
1322
- const result = await this.run(async (client) => {
1323
- const joinSessionId = await this.selectDeviceJoinSession(client);
1324
- return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
1325
- });
1326
- if (!result.ok)
1327
- return result;
1328
- if (result.value === null)
1329
- return { ok: true, value: null };
1330
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1331
- return this.applyCandidateJoinProgress(result.value);
1332
- }
1333
- async cancelDeviceJoin() {
1334
- const result = await this.run(async (client) => {
1335
- const joinSessionId = await this.selectDeviceJoinSession(client);
1336
- if (joinSessionId === null)
1337
- return null;
1338
- return client.cancelDeviceJoin(joinSessionId);
1339
- });
1340
- if (!result.ok)
1341
- return result;
1342
- this.pendingDeviceJoin = undefined;
1343
- this.activeDeviceJoinSessionId = undefined;
1344
- return { ok: true, value: { completed: true } };
1345
- }
1346
- /** Reliable-sync and project only Host-opaque device/request references. */
1347
- refreshDeviceManagement() {
1348
- return this.run(client => this.deviceManagementSnapshot(client));
1349
- }
1350
- async startDeviceJoinVerification(request) {
1351
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1352
- if (joinSessionId === undefined)
1353
- return { ok: false, error: failure('invalid-request') };
1354
- const result = await this.run(async (client) => {
1355
- await this.requireDeviceManager(client);
1356
- await client.syncDeviceManagement();
1357
- let notice = (await client.listLocalDeviceJoinRequests())
1358
- .find(value => value.joinSessionId === joinSessionId);
1359
- if (notice === undefined) {
1360
- throw Object.assign(new Error('request not found'), { name: 'AwikiSdkError', code: 'not-found' });
1361
- }
1362
- const localProgress = await this.localAdminJoinProgress(client, notice);
1363
- if (localProgress !== undefined)
1364
- return localProgress;
1365
- if (!notice.canStartVerification) {
1366
- throw Object.assign(new Error('request unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1367
- }
1368
- const operationId = `dsh-device-verify-${createHash('sha256').update(joinSessionId).digest('hex').slice(0, 32)}`;
1369
- let started;
1370
- try {
1371
- started = await client.startDeviceJoinVerification({
1372
- joinSessionId,
1373
- operationId,
1374
- challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS,
1375
- });
1376
- }
1377
- catch (error) {
1378
- await client.syncDeviceManagement();
1379
- notice = (await client.listLocalDeviceJoinRequests())
1380
- .find(value => value.joinSessionId === joinSessionId);
1381
- const recovered = notice === undefined ? undefined : await this.localAdminJoinProgress(client, notice);
1382
- if (recovered !== undefined)
1383
- return recovered;
1384
- throw error;
1385
- }
1386
- await client.syncDeviceManagement();
1387
- notice = (await client.listLocalDeviceJoinRequests())
1388
- .find(value => value.joinSessionId === joinSessionId);
1389
- if (notice === undefined)
1390
- return started;
1391
- return (await this.localAdminJoinProgress(client, notice)) ?? started;
1392
- });
1393
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1394
- }
1395
- async approveDeviceJoin(request) {
1396
- if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) {
1397
- return { ok: false, error: failure('invalid-request') };
1398
- }
1399
- const joinSessionId = this.requestSessions.get(request.requestRef);
1400
- if (joinSessionId === undefined || !/^\d{6}$/u.test(request.enteredSas)) {
1401
- return { ok: false, error: failure('invalid-request') };
1402
- }
1403
- const result = await this.run(async (client) => {
1404
- await this.requireDeviceManager(client);
1405
- const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1406
- if (progress.sas === undefined || !constantTimeSasMatches(progress.sas, request.enteredSas)) {
1407
- throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
1408
- }
1409
- const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
1410
- return client.confirmDeviceJoinApproval(prompt.approvalHandle);
1411
- });
1412
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1413
- }
1414
- async rejectDeviceJoin(request) {
1415
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1416
- if (joinSessionId === undefined
1417
- || (request.reason !== 'user_rejected' && request.reason !== 'sas_mismatch')) {
1418
- return { ok: false, error: failure('invalid-request') };
1419
- }
1420
- const result = await this.run(async (client) => {
1421
- await this.requireDeviceManager(client);
1422
- return client.rejectDeviceJoin(joinSessionId, request.reason);
1423
- });
1424
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1425
- }
1426
- async revokeDevice(request) {
1427
- if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) {
1428
- return { ok: false, error: failure('invalid-request') };
1429
- }
1430
- const deviceId = this.deviceIds.get(request.deviceRef);
1431
- if (deviceId === undefined)
1432
- return { ok: false, error: failure('invalid-request') };
1433
- return this.run(async (client) => {
1434
- await this.requireDeviceManager(client);
1435
- const devices = await client.getDeviceRegistry();
1436
- const target = devices.find(device => device.deviceId === deviceId);
1437
- if (target === undefined || target.isCurrent) {
1438
- throw Object.assign(new Error('device unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1439
- }
1440
- await client.revokeDevice(deviceId);
1441
- return this.deviceManagementSnapshot(client);
1442
- });
1185
+ const result = await this.run(client => client.registerIdentity(request));
1186
+ if (result.ok)
1187
+ await this.activateRegisteredIdentity(result.value);
1188
+ return result;
1443
1189
  }
1444
1190
  /**
1445
1191
  * Update the deployment identity's public WNS display name.
@@ -1465,16 +1211,10 @@ let AwikiService = (() => {
1465
1211
  }
1466
1212
  /** Start durable phone recovery for one existing full Handle. */
1467
1213
  sendRecoveryOtp(request) {
1468
- this.pendingDeviceJoin = undefined;
1469
1214
  const normalized = normalizeRecoveryOtpRequest(request);
1470
1215
  if (normalized === undefined)
1471
1216
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1472
- return this.run(async (client) => {
1473
- if (await this.selectDeviceJoinSession(client) !== null) {
1474
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1475
- }
1476
- return client.sendRecoveryOtp(normalized);
1477
- }, { allowSignedOut: true });
1217
+ return this.run(client => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
1478
1218
  }
1479
1219
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
1480
1220
  prepareRecovery(request) {
@@ -1842,7 +1582,7 @@ let AwikiService = (() => {
1842
1582
  }
1843
1583
  /** Return the deployment identity's public mailbox state. */
1844
1584
  getMailAccount() {
1845
- return this.run(client => client.getMailAccount());
1585
+ return this.run(client => client.getMailAccount(), { bindMailOwner: true });
1846
1586
  }
1847
1587
  /** List one bounded mailbox page on explicit browser/tool demand. */
1848
1588
  async listMailInbox(request) {
@@ -1853,11 +1593,11 @@ let AwikiService = (() => {
1853
1593
  catch {
1854
1594
  return { ok: false, error: failure('invalid-request') };
1855
1595
  }
1856
- return this.run(async (client) => {
1596
+ return this.run(async (client, _provider, ownerDid) => {
1857
1597
  if (normalized.folder !== 'sent')
1858
1598
  return client.listMailInbox(normalized);
1859
- return this.sentMailStore.list(await this.ownerDid(client), normalized);
1860
- });
1599
+ return this.sentMailStore.list(ownerDid, normalized);
1600
+ }, { bindMailOwner: true });
1861
1601
  }
1862
1602
  /** Read one bounded plain-text mail message. */
1863
1603
  async readMail(request) {
@@ -1868,45 +1608,116 @@ let AwikiService = (() => {
1868
1608
  catch {
1869
1609
  return { ok: false, error: failure('invalid-request') };
1870
1610
  }
1871
- return this.run(async (client) => {
1611
+ return this.run(async (client, _provider, ownerDid) => {
1872
1612
  if (!isLocalSentMailId(normalized.messageId))
1873
1613
  return client.readMail(normalized);
1874
- const local = await this.sentMailStore.read(await this.ownerDid(client), normalized.messageId);
1614
+ const local = await this.sentMailStore.read(ownerDid, normalized.messageId);
1875
1615
  if (local === undefined) {
1876
1616
  throw Object.assign(new Error('sent mail not found'), { name: 'AwikiSdkError', code: 'not-found' });
1877
1617
  }
1878
1618
  return local;
1879
- });
1619
+ }, { bindMailOwner: true });
1880
1620
  }
1881
1621
  /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1882
1622
  markMailRead(request) {
1883
- return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized));
1623
+ return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized), { bindMailOwner: true });
1884
1624
  }
1885
- /** Send one plain-text mail once. Browser callers require an explicit confirmation. */
1625
+ /** Send one mail once. Browser callers require an explicit confirmation. */
1886
1626
  async sendMail(request) {
1887
1627
  let normalized;
1888
1628
  try {
1889
- normalized = mailSendRequest(request);
1629
+ normalized = mailSendRequest(request, {
1630
+ maxCount: this.resolved.mailAttachmentMaxCount,
1631
+ maxBytes: this.resolved.mailAttachmentMaxBytes,
1632
+ totalMaxBytes: this.resolved.mailAttachmentTotalMaxBytes,
1633
+ });
1890
1634
  }
1891
1635
  catch {
1892
1636
  return { ok: false, error: failure('invalid-request') };
1893
1637
  }
1894
- return this.run(async (client) => {
1895
- const result = await client.sendMail(normalized);
1638
+ return this.run(async (client, _provider, ownerDid, assertActive) => {
1639
+ const result = await client.sendMail({
1640
+ to: [...normalized.to],
1641
+ cc: [...normalized.cc],
1642
+ subject: normalized.subject,
1643
+ bodyText: normalized.bodyText,
1644
+ ...normalized.attachments.length === 0 ? {} : {
1645
+ attachments: normalized.attachments.map(attachment => ({
1646
+ fileName: attachment.fileName,
1647
+ contentType: attachment.contentType,
1648
+ bytes: attachment.bytes,
1649
+ })),
1650
+ },
1651
+ });
1652
+ assertActive();
1896
1653
  if (!result.accepted)
1897
1654
  return result;
1655
+ const settled = normalized.attachments.length > 0
1656
+ && result.messageId === undefined
1657
+ && result.warnings.length < 100
1658
+ ? { ...result, warnings: [...result.warnings, 'Sent attachment download is unavailable because the service returned no message id.'] }
1659
+ : result;
1898
1660
  try {
1899
- const ownerDid = await this.ownerDid(client);
1900
1661
  const account = await client.getMailAccount().catch(() => undefined);
1901
- await this.sentMailStore.append(ownerDid, normalized, result, account);
1902
- return result;
1662
+ assertActive();
1663
+ await this.sentMailStore.append(ownerDid, normalized, settled, account);
1664
+ assertActive();
1665
+ return settled;
1903
1666
  }
1904
1667
  catch {
1905
- return result.warnings.length >= 100
1906
- ? result
1907
- : { ...result, warnings: [...result.warnings, 'Sent history could not be saved locally.'] };
1668
+ assertActive();
1669
+ return settled.warnings.length >= 100
1670
+ ? settled
1671
+ : { ...settled, warnings: [...settled.warnings, 'Sent history could not be saved locally.'] };
1908
1672
  }
1909
- });
1673
+ }, { bindMailOwner: true });
1674
+ }
1675
+ /** Download one mail attachment only after an explicit browser action. */
1676
+ async downloadMailAttachment(request) {
1677
+ let normalized;
1678
+ try {
1679
+ normalized = mailAttachmentDownloadRequest(request);
1680
+ }
1681
+ catch {
1682
+ return { ok: false, error: failure('invalid-request') };
1683
+ }
1684
+ return this.run(async (client, _provider, ownerDid, assertActive) => {
1685
+ let serviceMessageId = normalized.localMessageId;
1686
+ if (isLocalSentMailId(serviceMessageId)) {
1687
+ const resolved = await this.sentMailStore.resolveAttachment(ownerDid, serviceMessageId, normalized.attachmentIndex);
1688
+ if (resolved === undefined) {
1689
+ throw Object.assign(new Error('sent mail attachment is unavailable'), {
1690
+ name: 'AwikiSdkError', code: 'not-found',
1691
+ });
1692
+ }
1693
+ serviceMessageId = resolved;
1694
+ }
1695
+ const value = await client.downloadMailAttachment({
1696
+ messageId: serviceMessageId,
1697
+ attachmentIndex: normalized.attachmentIndex,
1698
+ });
1699
+ assertActive();
1700
+ if (!Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0)
1701
+ throw new TypeError('invalid mail attachment size');
1702
+ if (value.sizeBytes > this.resolved.mailAttachmentMaxBytes) {
1703
+ throw Object.assign(new Error('mail attachment exceeds Host limit'), {
1704
+ name: 'AwikiSdkError', code: 'attachment-too-large',
1705
+ });
1706
+ }
1707
+ if (!(value.bytes instanceof Uint8Array) || value.bytes.byteLength !== value.sizeBytes) {
1708
+ throw new TypeError('invalid mail attachment bytes');
1709
+ }
1710
+ const fileName = mailAttachmentFileName(value.fileName);
1711
+ const contentType = mailAttachmentContentType(value.contentType);
1712
+ const bytes = value.bytes;
1713
+ return {
1714
+ fileName,
1715
+ contentType,
1716
+ sizeBytes: bytes.byteLength,
1717
+ sha256: createHash('sha256').update(bytes).digest('hex'),
1718
+ bytesBase64: Buffer.from(bytes).toString('base64'),
1719
+ };
1720
+ }, { bindMailOwner: true });
1910
1721
  }
1911
1722
  /**
1912
1723
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -1921,11 +1732,11 @@ let AwikiService = (() => {
1921
1732
  return this.mutateSession(async () => {
1922
1733
  const provider = this.provider;
1923
1734
  if (provider !== undefined)
1924
- await this.stopProviderRuntime(provider);
1735
+ await this.stopListener(provider);
1925
1736
  const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
1926
1737
  if (!result.ok) {
1927
1738
  if (provider !== undefined && this.provider === provider)
1928
- this.ensureProviderRuntime(provider);
1739
+ void this.startListener(provider);
1929
1740
  return result;
1930
1741
  }
1931
1742
  try {
@@ -1935,10 +1746,10 @@ let AwikiService = (() => {
1935
1746
  await this.sessionStore.signIn();
1936
1747
  this.signedOut = false;
1937
1748
  this.activeIdentityDid = undefined;
1938
- this.pendingDeviceJoin = undefined;
1939
- this.activeDeviceJoinSessionId = undefined;
1940
1749
  this.invalidateSummaries();
1941
1750
  this.publishSession({ status: 'unregistered' });
1751
+ if (provider !== undefined && this.provider === provider)
1752
+ void this.startListener(provider);
1942
1753
  return result;
1943
1754
  }
1944
1755
  catch {
@@ -1946,7 +1757,7 @@ let AwikiService = (() => {
1946
1757
  }
1947
1758
  });
1948
1759
  }
1949
- /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
1760
+ /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
1950
1761
  async applyRecoveredSession(progress) {
1951
1762
  if (progress.phase !== 'applied')
1952
1763
  return true;
@@ -1965,211 +1776,34 @@ let AwikiService = (() => {
1965
1776
  this.activeIdentityDid = identity.did;
1966
1777
  this.invalidateSummaries();
1967
1778
  }
1968
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
1969
1779
  if (this.provider !== provider)
1970
1780
  return false;
1971
1781
  if (!alreadyActive) {
1972
1782
  const session = { status: 'active', identity };
1973
1783
  this.publishSession(session);
1974
- this.ensureProviderRuntime(provider);
1784
+ await provider.listenerStartup;
1785
+ await this.startListener(provider);
1975
1786
  }
1976
- return reconciled;
1787
+ return true;
1977
1788
  }
1978
1789
  catch {
1979
1790
  return false;
1980
1791
  }
1981
1792
  });
1982
1793
  }
1983
- /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
1984
- async reconcileRecoveredIdentity(provider, operationId) {
1985
- const logger = this.ctx.logger('awiki-recovery');
1986
- let mailboxRestored = false;
1987
- try {
1988
- await provider.client.getMailAccount();
1989
- mailboxRestored = true;
1990
- }
1991
- catch {
1992
- logger.warn('awiki: recovered mailbox reconciliation is pending');
1993
- }
1994
- const target = this.recoveryReconciliationTarget;
1995
- if (target === undefined)
1996
- return mailboxRestored;
1997
- try {
1998
- const authority = await provider.client.issueRecoveryAttestation({ operationId });
1999
- const response = await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
2000
- method: 'POST',
2001
- headers: { 'content-type': 'application/json' },
2002
- body: JSON.stringify({ attestation: authority.attestation }),
2003
- }), request => fetch(request));
2004
- if (!await acceptsRecoveryReconciliation(response)) {
2005
- logger.warn('awiki: recovered model account reconciliation is pending');
2006
- return false;
2007
- }
2008
- return mailboxRestored;
2009
- }
2010
- catch {
2011
- logger.warn('awiki: recovered model account reconciliation is pending');
2012
- return false;
2013
- }
2014
- }
2015
- /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
2016
- async selectDeviceJoinSession(client) {
2017
- const sessions = await client.listLocalDeviceJoinSessions();
2018
- if (this.activeDeviceJoinSessionId !== undefined) {
2019
- const tracked = sessions.find(value => (value.side === 'new_device' && value.joinSessionId === this.activeDeviceJoinSessionId));
2020
- if (tracked !== undefined)
2021
- return tracked.joinSessionId;
2022
- this.activeDeviceJoinSessionId = undefined;
2023
- }
2024
- const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
2025
- if (resumable.length > 1) {
2026
- throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
2027
- }
2028
- const selected = resumable[0];
2029
- this.activeDeviceJoinSessionId = selected?.joinSessionId;
2030
- return selected?.joinSessionId ?? null;
2031
- }
2032
- async applyCandidateJoinProgress(value) {
2033
- const phase = candidateJoinPhase(value);
2034
- if (phase === undefined)
2035
- return { ok: false, error: failure('remote') };
2036
- if (phase === 'cancelled' || phase === 'rejected' || phase === 'expired') {
2037
- this.activeDeviceJoinSessionId = undefined;
2038
- }
2039
- if (phase === 'authorized') {
2040
- if (value.identity === undefined)
2041
- return { ok: false, error: failure('remote') };
2042
- await this.activateRegisteredIdentity(value.identity);
2043
- }
2044
- return {
2045
- ok: true,
2046
- value: {
2047
- phase,
2048
- expiresAt: value.expiresAt,
2049
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2050
- completed: phase === 'authorized',
2051
- },
2052
- };
2053
- }
2054
- publicAdminJoinProgress(requestRef, value) {
2055
- const phase = adminJoinPhase(value);
2056
- if (phase === undefined)
2057
- return { ok: false, error: failure('remote') };
2058
- return {
2059
- ok: true,
2060
- value: {
2061
- requestRef,
2062
- phase,
2063
- expiresAt: value.expiresAt,
2064
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2065
- },
2066
- };
2067
- }
2068
- async localAdminJoinProgress(client, notice) {
2069
- const local = (await client.listLocalDeviceJoinSessions())
2070
- .find(session => session.side === 'admin' && session.joinSessionId === notice.joinSessionId);
2071
- if (local === undefined) {
2072
- return notice.claimedByCurrentDevice
2073
- ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId)
2074
- : undefined;
2075
- }
2076
- if (local.localPhase === 'challenge_prepared') {
2077
- return {
2078
- joinSessionId: notice.joinSessionId,
2079
- localPhase: local.localPhase,
2080
- remoteState: notice.state,
2081
- expiresAt: local.expiresAt,
2082
- };
2083
- }
2084
- return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2085
- }
2086
- async requireDeviceManager(client) {
2087
- const current = await client.getCurrentDeviceSummary();
2088
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2089
- throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2090
- }
2091
- }
2092
- requestRef(joinSessionId) {
2093
- const existing = this.requestRefs.get(joinSessionId);
2094
- if (existing !== undefined)
2095
- return existing;
2096
- const reference = `join-${randomUUID()}`;
2097
- this.requestRefs.set(joinSessionId, reference);
2098
- this.requestSessions.set(reference, joinSessionId);
2099
- return reference;
2100
- }
2101
- deviceRef(deviceId) {
2102
- const existing = this.deviceRefs.get(deviceId);
2103
- if (existing !== undefined)
2104
- return existing;
2105
- const reference = `device-${randomUUID()}`;
2106
- this.deviceRefs.set(deviceId, reference);
2107
- this.deviceIds.set(reference, deviceId);
2108
- return reference;
2109
- }
2110
- async deviceManagementSnapshot(client) {
2111
- await client.syncDeviceManagement();
2112
- const current = await client.getCurrentDeviceSummary();
2113
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2114
- this.requestRefs.clear();
2115
- this.requestSessions.clear();
2116
- this.deviceRefs.clear();
2117
- this.deviceIds.clear();
2118
- return {
2119
- canManage: false,
2120
- ...current.role === undefined ? {} : { role: current.role },
2121
- readiness: current.readiness,
2122
- devices: [],
2123
- requests: [],
2124
- };
2125
- }
2126
- const requests = await client.listLocalDeviceJoinRequests();
2127
- await Promise.all(requests
2128
- .filter(request => request.claimedByCurrentDevice)
2129
- .map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
2130
- const devices = await client.getDeviceRegistry();
2131
- return {
2132
- canManage: true,
2133
- role: 'admin',
2134
- readiness: 'admin_ready',
2135
- devices: devices.map(device => this.publicDevice(device)),
2136
- requests: requests.map(request => ({
2137
- requestRef: this.requestRef(request.joinSessionId),
2138
- candidateKeyFingerprint: request.candidateKeyFingerprint,
2139
- issuedAt: request.issuedAt,
2140
- expiresAt: request.expiresAt,
2141
- state: requestJoinPhase(request),
2142
- claimedByCurrentDevice: request.claimedByCurrentDevice,
2143
- canStartVerification: request.canStartVerification,
2144
- })),
2145
- };
2146
- }
2147
- publicDevice(device) {
2148
- return {
2149
- deviceRef: this.deviceRef(device.deviceId),
2150
- status: device.status,
2151
- role: device.role,
2152
- managementReady: device.managementReady,
2153
- isCurrent: device.isCurrent,
2154
- };
2155
- }
2156
- /** Publish one newly registered identity, then start realtime only in the background. */
1794
+ /** Publish one newly registered identity and start its listener through the existing session path. */
2157
1795
  async activateRegisteredIdentity(identity) {
2158
- this.pendingDeviceJoin = undefined;
2159
- this.activeDeviceJoinSessionId = undefined;
2160
1796
  this.activeIdentityDid = identity.did;
2161
1797
  this.publishSession({ status: 'active', identity });
2162
1798
  const provider = this.provider;
2163
- if (provider !== undefined)
2164
- this.ensureProviderRuntime(provider);
1799
+ if (provider !== undefined) {
1800
+ await provider.listenerStartup;
1801
+ await this.startListener(provider);
1802
+ }
2165
1803
  }
2166
1804
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
2167
1805
  invalidateSummaries() {
2168
1806
  this.sessionRevision += 1;
2169
- this.requestRefs.clear();
2170
- this.requestSessions.clear();
2171
- this.deviceRefs.clear();
2172
- this.deviceIds.clear();
2173
1807
  for (const controller of this.activeSummaryRequests)
2174
1808
  controller.abort();
2175
1809
  this.activeSummaryRequests.clear();
@@ -2180,24 +1814,40 @@ let AwikiService = (() => {
2180
1814
  }
2181
1815
  /** Resolve and cache the owner binding required by private Host-side projections. */
2182
1816
  async ownerDid(client) {
2183
- const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
2184
- const ownerDid = this.activeIdentityDid ?? identity?.did;
1817
+ const identity = await client.getIdentity();
1818
+ const ownerDid = identity?.did;
2185
1819
  if (ownerDid === undefined) {
2186
1820
  throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
2187
1821
  }
1822
+ if (this.activeIdentityDid !== undefined && this.activeIdentityDid !== ownerDid) {
1823
+ throw new ProviderUnavailableError();
1824
+ }
2188
1825
  this.activeIdentityDid = ownerDid;
2189
1826
  return ownerDid;
2190
1827
  }
2191
1828
  /** Invoke the current client and normalize every rejection to a public result. */
2192
1829
  async run(operation, options = {}) {
2193
1830
  try {
1831
+ const sessionRevision = this.sessionRevision;
2194
1832
  if (options.allowSignedOut !== true && await this.isSignedOut()) {
2195
1833
  return { ok: false, error: failure('signed-out') };
2196
1834
  }
2197
1835
  const provider = this.provider;
2198
1836
  if (provider === undefined)
2199
1837
  throw new ProviderUnavailableError();
2200
- const value = await operation(provider.client);
1838
+ const ownerDid = options.bindMailOwner === true
1839
+ ? await this.ownerDid(provider.client)
1840
+ : undefined;
1841
+ const assertActive = () => {
1842
+ if (this.provider !== provider
1843
+ || (options.bindMailOwner === true && (this.sessionRevision !== sessionRevision
1844
+ || this.signedOut === true
1845
+ || this.activeIdentityDid !== ownerDid)))
1846
+ throw new ProviderUnavailableError();
1847
+ };
1848
+ assertActive();
1849
+ const value = await operation(provider.client, provider, ownerDid, assertActive);
1850
+ assertActive();
2201
1851
  if (options.skipAttachmentByteValidation !== true && containsUnexpectedBinary(value, new Set())) {
2202
1852
  return { ok: false, error: failure('remote') };
2203
1853
  }
@@ -2208,7 +1858,7 @@ let AwikiService = (() => {
2208
1858
  }
2209
1859
  }
2210
1860
  /** Validate mail input before entering the provider and preserve fixed public failures. */
2211
- runValidatedMail(validate, operation) {
1861
+ runValidatedMail(validate, operation, options = {}) {
2212
1862
  let request;
2213
1863
  try {
2214
1864
  request = validate();
@@ -2216,7 +1866,7 @@ let AwikiService = (() => {
2216
1866
  catch {
2217
1867
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2218
1868
  }
2219
- return this.run(client => operation(client, request));
1869
+ return this.run(client => operation(client, request), options);
2220
1870
  }
2221
1871
  /** Read and cache the private Host-owned session marker. */
2222
1872
  async isSignedOut() {
@@ -2271,194 +1921,161 @@ let AwikiService = (() => {
2271
1921
  this.sessionMutation = pending.then(() => undefined, () => undefined);
2272
1922
  return pending;
2273
1923
  }
2274
- /** Start identity realtime and the optional Agent consumer without blocking identity success. */
2275
- ensureProviderRuntime(provider) {
2276
- const identityDid = this.activeIdentityDid;
2277
- if (provider.runtimeReplacement !== undefined)
2278
- return;
2279
- if (identityDid !== undefined
2280
- && provider.realtimeIdentityDid !== undefined
2281
- && provider.realtimeIdentityDid !== identityDid) {
2282
- this.replaceProviderRuntime(provider, identityDid);
2283
- return;
2284
- }
2285
- this.ensureRealtimeSupervisor(provider);
2286
- this.ensureAgentConsumer(provider);
2287
- }
2288
- replaceProviderRuntime(provider, identityDid) {
2289
- if (provider.runtimeReplacement !== undefined || this.provider !== provider)
2290
- return;
2291
- const replacement = (async () => {
2292
- await this.stopProviderRuntime(provider);
2293
- if (this.provider !== provider || this.activeIdentityDid !== identityDid)
2294
- return;
2295
- this.ensureRealtimeSupervisor(provider);
2296
- this.ensureAgentConsumer(provider);
2297
- })();
2298
- const observed = replacement
2299
- .catch((error) => {
2300
- this.ctx.logger('awiki-realtime').warn('AWiki identity realtime replacement failed: %s', error instanceof Error ? error.message : 'unknown failure');
2301
- })
2302
- .finally(() => {
2303
- if (provider.runtimeReplacement === observed)
2304
- delete provider.runtimeReplacement;
2305
- if (this.provider === provider)
2306
- this.ensureProviderRuntime(provider);
2307
- });
2308
- provider.runtimeReplacement = observed;
2309
- }
2310
- ensureRealtimeSupervisor(provider) {
2311
- if (!this.resolved.realtimeEnabled
2312
- || provider.realtimeSupervisor !== undefined
2313
- || provider.realtimeStartup !== undefined
2314
- || this.provider !== provider
2315
- || (provider.client.realtime ?? provider.client.listener) === undefined)
2316
- return;
2317
- const generation = provider.realtimeGeneration;
2318
- const source = provider.client.realtime ?? provider.client.listener;
2319
- if (source === undefined)
2320
- return;
2321
- const logger = this.ctx.logger('awiki-realtime');
1924
+ /** Start one exact identity-bound listener, atomically releasing a failed startup. */
1925
+ startListener(provider) {
1926
+ if (!this.resolved.listenerEnabled
1927
+ || provider.listener !== undefined
1928
+ || provider.listenerRecoveryBlocked
1929
+ || this.provider !== provider) {
1930
+ return Promise.resolve();
1931
+ }
1932
+ if (provider.listenerStartup !== undefined)
1933
+ return provider.listenerStartup;
1934
+ if (provider.listenerCleanup !== undefined || provider.listenerRestartTimer !== undefined)
1935
+ return Promise.resolve();
1936
+ const workspaceContext = this.workspaceContext;
1937
+ const source = provider.client.listener;
1938
+ if (workspaceContext === undefined || source === undefined)
1939
+ return Promise.resolve();
1940
+ const generation = provider.listenerGeneration;
1941
+ const logger = this.ctx.logger('awiki-listener');
2322
1942
  const startup = (async () => {
2323
1943
  if (await this.isSignedOut())
2324
1944
  return;
2325
- const identity = await provider.client.getIdentity();
2326
- if (identity === null || !this.realtimeFenceMatches(provider, generation))
1945
+ if (await provider.client.getIdentity() === null)
1946
+ return;
1947
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
2327
1948
  return;
2328
- this.activeIdentityDid = identity.did;
2329
- const supervisor = new IdentityRealtimeSupervisor(source, {
2330
- onSynchronized: cause => this.onRealtimeSynchronized(provider, generation, cause),
1949
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
1950
+ const listener = new AwikiAgentListener(source, agents, {
1951
+ ...this.resolved.listener,
1952
+ stateRoot: this.activeTenant().stateRoot,
2331
1953
  }, logger);
2332
- provider.realtimeSupervisor = supervisor;
2333
- provider.realtimeIdentityDid = identity.did;
2334
- supervisor.start();
2335
- this.ensureAgentConsumer(provider);
2336
- })();
2337
- const observed = startup
2338
- .catch((error) => {
2339
- logger.warn('AWiki realtime startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2340
- })
2341
- .finally(() => {
2342
- if (provider.realtimeStartup === observed)
2343
- delete provider.realtimeStartup;
2344
- });
2345
- provider.realtimeStartup = observed;
2346
- }
2347
- realtimeFenceMatches(provider, generation) {
2348
- return this.provider === provider && provider.realtimeGeneration === generation;
2349
- }
2350
- async onRealtimeSynchronized(provider, generation, cause) {
2351
- if (!this.realtimeFenceMatches(provider, generation))
2352
- return;
2353
- if (cause === 'system_notification' || cause === 'stream_recovery') {
1954
+ provider.listener = listener;
2354
1955
  try {
2355
- provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
1956
+ await listener.start();
2356
1957
  }
2357
- catch {
2358
- this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
1958
+ catch (error) {
1959
+ if (provider.listener === listener)
1960
+ delete provider.listener;
1961
+ try {
1962
+ await listener.dispose();
1963
+ }
1964
+ catch {
1965
+ provider.listenerRecoveryBlocked = true;
1966
+ }
1967
+ if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
1968
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1969
+ }
1970
+ throw error;
2359
1971
  }
2360
- }
2361
- if (!['session_start', 'connection_ready', 'reconnected', 'message', 'message_update'].includes(cause))
2362
- return;
2363
- this.ensureAgentConsumer(provider);
2364
- await provider.agentConsumerStartup;
2365
- if (!this.realtimeFenceMatches(provider, generation))
2366
- return;
2367
- await provider.agentConsumer?.reconcileOnce();
2368
- }
2369
- ensureAgentConsumer(provider) {
2370
- const workspaceContext = this.workspaceContext;
2371
- const source = provider.client.agentInbox ?? provider.client.listener;
2372
- const identityDid = this.activeIdentityDid;
2373
- if (!this.resolved.listenerEnabled
2374
- || workspaceContext === undefined
2375
- || source === undefined
2376
- || identityDid === undefined
2377
- || this.provider !== provider)
2378
- return;
2379
- if (provider.agentConsumer !== undefined && provider.agentConsumerIdentityDid === identityDid)
2380
- return;
2381
- if (provider.agentConsumerStartup !== undefined)
2382
- return;
2383
- const generation = ++provider.agentConsumerGeneration;
2384
- const logger = this.ctx.logger('awiki-listener');
2385
- const startup = (async () => {
2386
- await this.detachAgentConsumer(provider);
2387
- if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
1972
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
1973
+ if (provider.listener === listener)
1974
+ delete provider.listener;
1975
+ try {
1976
+ await listener.dispose();
1977
+ }
1978
+ catch (error) {
1979
+ provider.listenerRecoveryBlocked = true;
1980
+ throw error;
1981
+ }
2388
1982
  return;
2389
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
2390
- const consumer = new AwikiAgentListener(source, agents, {
2391
- ...this.resolved.listener,
2392
- identityScope: identityDid,
2393
- }, logger);
2394
- provider.agentConsumer = consumer;
2395
- provider.agentConsumerIdentityDid = identityDid;
2396
- await consumer.reconcileOnce();
1983
+ }
1984
+ provider.listenerStartedAt = Date.now();
1985
+ void listener.whenTerminated().then((result) => {
1986
+ if (result.kind === 'failed') {
1987
+ this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
1988
+ }
1989
+ });
2397
1990
  })();
2398
1991
  const observed = startup
2399
1992
  .catch((error) => {
2400
- logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1993
+ logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2401
1994
  })
2402
1995
  .finally(() => {
2403
- if (provider.agentConsumerStartup === observed)
2404
- delete provider.agentConsumerStartup;
1996
+ if (provider.listenerStartup === observed)
1997
+ delete provider.listenerStartup;
2405
1998
  });
2406
- provider.agentConsumerStartup = observed;
1999
+ provider.listenerStartup = observed;
2000
+ return observed;
2001
+ }
2002
+ async stopListener(provider) {
2003
+ provider.listenerGeneration += 1;
2004
+ provider.listenerRestartAttempt = 0;
2005
+ delete provider.listenerStartedAt;
2006
+ const timer = provider.listenerRestartTimer;
2007
+ if (timer !== undefined) {
2008
+ delete provider.listenerRestartTimer;
2009
+ clearTimeout(timer);
2010
+ }
2011
+ await provider.listenerStartup;
2012
+ await provider.listenerCleanup;
2013
+ const listener = provider.listener;
2014
+ if (listener === undefined)
2015
+ return;
2016
+ delete provider.listener;
2017
+ try {
2018
+ await listener.dispose();
2019
+ }
2020
+ catch (error) {
2021
+ provider.listenerRecoveryBlocked = true;
2022
+ throw error;
2023
+ }
2407
2024
  }
2408
- agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
2025
+ listenerFenceMatches(provider, workspaceContext, generation) {
2409
2026
  return this.provider === provider
2410
2027
  && this.workspaceContext === workspaceContext
2411
- && this.activeIdentityDid === identityDid
2412
- && provider.agentConsumerGeneration === generation;
2028
+ && provider.listenerGeneration === generation;
2413
2029
  }
2414
- async detachAgentConsumer(provider) {
2415
- const consumer = provider.agentConsumer;
2416
- if (consumer === undefined)
2030
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
2031
+ if (provider.listener !== listener)
2417
2032
  return;
2418
- delete provider.agentConsumer;
2419
- delete provider.agentConsumerIdentityDid;
2420
- const cleanup = consumer.dispose();
2033
+ delete provider.listener;
2034
+ const startedAt = provider.listenerStartedAt;
2035
+ delete provider.listenerStartedAt;
2036
+ if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
2037
+ provider.listenerRestartAttempt = 0;
2038
+ }
2039
+ const logger = this.ctx.logger('awiki-listener');
2040
+ logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
2041
+ let disposed = false;
2042
+ const cleanup = listener.dispose()
2043
+ .then(() => { disposed = true; }, (cleanupError) => {
2044
+ provider.listenerRecoveryBlocked = true;
2045
+ logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
2046
+ });
2421
2047
  const observed = cleanup.finally(() => {
2422
- if (provider.agentConsumerCleanup === observed)
2423
- delete provider.agentConsumerCleanup;
2048
+ if (provider.listenerCleanup === observed)
2049
+ delete provider.listenerCleanup;
2050
+ if (disposed)
2051
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
2424
2052
  });
2425
- provider.agentConsumerCleanup = observed;
2426
- await observed;
2427
- }
2428
- async stopAgentConsumer(provider) {
2429
- provider.agentConsumerGeneration += 1;
2430
- await provider.agentConsumerStartup;
2431
- await provider.agentConsumerCleanup;
2432
- await this.detachAgentConsumer(provider);
2433
- }
2434
- async stopRealtimeSupervisor(provider) {
2435
- provider.realtimeGeneration += 1;
2436
- await provider.realtimeStartup;
2437
- const supervisor = provider.realtimeSupervisor;
2438
- if (supervisor === undefined)
2439
- return;
2440
- delete provider.realtimeSupervisor;
2441
- delete provider.realtimeIdentityDid;
2442
- await supervisor.dispose();
2053
+ provider.listenerCleanup = observed;
2443
2054
  }
2444
- async stopProviderRuntime(provider) {
2445
- await Promise.all([
2446
- this.stopRealtimeSupervisor(provider),
2447
- this.stopAgentConsumer(provider),
2448
- ]);
2055
+ scheduleListenerRestart(provider, workspaceContext, generation) {
2056
+ if (provider.listenerRestartTimer !== undefined
2057
+ || !this.listenerFenceMatches(provider, workspaceContext, generation))
2058
+ return;
2059
+ provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
2060
+ const exponent = provider.listenerRestartAttempt - 1;
2061
+ const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
2062
+ const timer = setTimeout(() => {
2063
+ if (provider.listenerRestartTimer !== timer)
2064
+ return;
2065
+ delete provider.listenerRestartTimer;
2066
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
2067
+ return;
2068
+ void this.startListener(provider);
2069
+ }, delay);
2070
+ provider.listenerRestartTimer = timer;
2449
2071
  }
2450
2072
  /** Clear one exact provider slot before joining its one shared disposal. */
2451
2073
  disposeProvider(provider) {
2452
- if (this.provider === provider) {
2074
+ if (this.provider === provider)
2453
2075
  this.provider = undefined;
2454
- this.pendingDeviceJoin = undefined;
2455
- this.activeDeviceJoinSessionId = undefined;
2456
- this.invalidateSummaries();
2457
- }
2458
2076
  provider.disposal ??= (async () => {
2459
2077
  try {
2460
- await provider.runtimeReplacement;
2461
- await this.stopProviderRuntime(provider);
2078
+ await this.stopListener(provider);
2462
2079
  }
2463
2080
  finally {
2464
2081
  await provider.client.dispose();