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