@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.
- package/README.md +60 -51
- package/README.zh.md +51 -43
- package/cordis.patch.yml +7 -21
- package/lib/client.js +785 -1308
- package/lib/client.js.map +1 -1
- package/lib/index.js +715 -1017
- package/lib/provider.js +16 -52
- package/lib/{sdk-adapter-BeJakxLP.mjs → sdk-adapter--trNlr-T.mjs} +38 -143
- package/lib/typert.host.js +139 -414
- package/lib/typert.remote-client.d.ts +5 -19
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +139 -414
- package/lib/types/client/AwikiIdentityAccess.d.ts +5 -6
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +27 -88
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiMail.d.ts +1 -1
- package/lib/types/client/AwikiMail.d.ts.map +1 -1
- package/lib/types/client/AwikiMail.js +130 -6
- package/lib/types/client/AwikiMail.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +3 -4
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/controller.d.ts +11 -20
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +48 -53
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +2 -8
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/mail-attachment.d.ts +40 -0
- package/lib/types/client/mail-attachment.d.ts.map +1 -0
- package/lib/types/client/mail-attachment.js +186 -0
- package/lib/types/client/mail-attachment.js.map +1 -0
- package/lib/types/client/settings-locales.js +8 -8
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +6 -10
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/index.d.ts +28 -59
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +417 -800
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener-state.d.ts +3 -5
- package/lib/types/listener-state.d.ts.map +1 -1
- package/lib/types/listener-state.js +13 -28
- package/lib/types/listener-state.js.map +1 -1
- package/lib/types/listener.d.ts +32 -9
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +127 -11
- package/lib/types/listener.js.map +1 -1
- package/lib/types/mail.d.ts +28 -2
- package/lib/types/mail.d.ts.map +1 -1
- package/lib/types/mail.js +78 -2
- package/lib/types/mail.js.map +1 -1
- package/lib/types/profile-state.d.ts +17 -0
- package/lib/types/profile-state.d.ts.map +1 -0
- package/lib/types/profile-state.js +58 -0
- package/lib/types/profile-state.js.map +1 -0
- package/lib/types/provider-api.d.ts +36 -100
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts +1 -7
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +15 -53
- package/lib/types/provider.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts +5 -30
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +48 -133
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/sent-mail-store.d.ts +7 -2
- package/lib/types/sent-mail-store.d.ts.map +1 -1
- package/lib/types/sent-mail-store.js +125 -23
- package/lib/types/sent-mail-store.js.map +1 -1
- package/lib/types/tools.d.ts +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/tools.js +20 -4
- package/lib/types/tools.js.map +1 -1
- package/lib/types/types.d.ts +45 -77
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +4 -6
- package/lib/types/client/AwikiDevices.d.ts +0 -28
- package/lib/types/client/AwikiDevices.d.ts.map +0 -1
- package/lib/types/client/AwikiDevices.js +0 -85
- package/lib/types/client/AwikiDevices.js.map +0 -1
- package/lib/types/realtime-supervisor.d.ts +0 -43
- package/lib/types/realtime-supervisor.d.ts.map +0 -1
- package/lib/types/realtime-supervisor.js +0 -166
- package/lib/types/realtime-supervisor.js.map +0 -1
package/lib/types/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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()
|
|
79
|
+
userServiceUrl: z.string(),
|
|
79
80
|
userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
|
|
80
|
-
messageServiceUrl: z.string()
|
|
81
|
+
messageServiceUrl: z.string(),
|
|
81
82
|
mailServiceUrl: z.string(),
|
|
82
|
-
messageServicePublicUrl: z.string()
|
|
83
|
-
messageServiceDid: z.string()
|
|
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
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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.
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
949
|
+
listenerRestartAttempt: 0,
|
|
950
|
+
listenerGeneration: 0,
|
|
951
|
+
listenerRecoveryBlocked: false,
|
|
1030
952
|
};
|
|
1031
953
|
this.provider = provider;
|
|
1032
|
-
this.
|
|
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
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1258
|
-
this.
|
|
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.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
1902
|
-
|
|
1662
|
+
assertActive();
|
|
1663
|
+
await this.sentMailStore.append(ownerDid, normalized, settled, account);
|
|
1664
|
+
assertActive();
|
|
1665
|
+
return settled;
|
|
1903
1666
|
}
|
|
1904
1667
|
catch {
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1784
|
+
await provider.listenerStartup;
|
|
1785
|
+
await this.startListener(provider);
|
|
1975
1786
|
}
|
|
1976
|
-
return
|
|
1787
|
+
return true;
|
|
1977
1788
|
}
|
|
1978
1789
|
catch {
|
|
1979
1790
|
return false;
|
|
1980
1791
|
}
|
|
1981
1792
|
});
|
|
1982
1793
|
}
|
|
1983
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
2184
|
-
const ownerDid =
|
|
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
|
|
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
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
return;
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
this.
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
const
|
|
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
|
-
|
|
2326
|
-
|
|
1945
|
+
if (await provider.client.getIdentity() === null)
|
|
1946
|
+
return;
|
|
1947
|
+
if (!this.listenerFenceMatches(provider, workspaceContext, generation))
|
|
2327
1948
|
return;
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2330
|
-
|
|
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.
|
|
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
|
-
|
|
1956
|
+
await listener.start();
|
|
2356
1957
|
}
|
|
2357
|
-
catch {
|
|
2358
|
-
|
|
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
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
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
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
|
1993
|
+
logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
2401
1994
|
})
|
|
2402
1995
|
.finally(() => {
|
|
2403
|
-
if (provider.
|
|
2404
|
-
delete provider.
|
|
1996
|
+
if (provider.listenerStartup === observed)
|
|
1997
|
+
delete provider.listenerStartup;
|
|
2405
1998
|
});
|
|
2406
|
-
provider.
|
|
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
|
-
|
|
2025
|
+
listenerFenceMatches(provider, workspaceContext, generation) {
|
|
2409
2026
|
return this.provider === provider
|
|
2410
2027
|
&& this.workspaceContext === workspaceContext
|
|
2411
|
-
&&
|
|
2412
|
-
&& provider.agentConsumerGeneration === generation;
|
|
2028
|
+
&& provider.listenerGeneration === generation;
|
|
2413
2029
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
if (consumer === undefined)
|
|
2030
|
+
releaseFailedListener(provider, listener, workspaceContext, generation, error) {
|
|
2031
|
+
if (provider.listener !== listener)
|
|
2417
2032
|
return;
|
|
2418
|
-
delete provider.
|
|
2419
|
-
|
|
2420
|
-
|
|
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.
|
|
2423
|
-
delete provider.
|
|
2048
|
+
if (provider.listenerCleanup === observed)
|
|
2049
|
+
delete provider.listenerCleanup;
|
|
2050
|
+
if (disposed)
|
|
2051
|
+
this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
2424
2052
|
});
|
|
2425
|
-
provider.
|
|
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
|
-
|
|
2445
|
-
|
|
2446
|
-
this.
|
|
2447
|
-
|
|
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
|
|
2461
|
-
await this.stopProviderRuntime(provider);
|
|
2078
|
+
await this.stopListener(provider);
|
|
2462
2079
|
}
|
|
2463
2080
|
finally {
|
|
2464
2081
|
await provider.client.dispose();
|