@awiki/dsh-plugin 0.3.3 → 0.3.4
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 +37 -31
- package/README.zh.md +30 -27
- package/cordis.patch.yml +17 -0
- package/lib/client.js +1269 -272
- package/lib/client.js.map +1 -1
- package/lib/index.js +874 -314
- package/lib/provider.js +52 -16
- package/lib/{sdk-adapter-CDLTgHJA.mjs → sdk-adapter-BeJakxLP.mjs} +142 -4
- package/lib/typert.host.js +419 -89
- package/lib/typert.remote-client.d.ts +19 -3
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +419 -89
- package/lib/types/client/AwikiDevices.d.ts +28 -0
- package/lib/types/client/AwikiDevices.d.ts.map +1 -0
- package/lib/types/client/AwikiDevices.js +85 -0
- package/lib/types/client/AwikiDevices.js.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts +6 -5
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +88 -27
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +4 -3
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/controller.d.ts +20 -5
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +38 -7
- 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 +8 -0
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/slots.d.ts +10 -2
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/index.d.ts +51 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +675 -174
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener-state.d.ts +5 -3
- package/lib/types/listener-state.d.ts.map +1 -1
- package/lib/types/listener-state.js +28 -13
- package/lib/types/listener-state.js.map +1 -1
- package/lib/types/listener.d.ts +9 -32
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +11 -127
- package/lib/types/listener.js.map +1 -1
- package/lib/types/provider-api.d.ts +96 -4
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts +7 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +53 -15
- package/lib/types/provider.js.map +1 -1
- package/lib/types/realtime-supervisor.d.ts +43 -0
- package/lib/types/realtime-supervisor.d.ts.map +1 -0
- package/lib/types/realtime-supervisor.js +166 -0
- package/lib/types/realtime-supervisor.js.map +1 -0
- package/lib/types/sdk-adapter.d.ts +28 -2
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +132 -3
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/types.d.ts +76 -6
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +18 -18
- package/lib/types/profile-state.d.ts +0 -11
- package/lib/types/profile-state.d.ts.map +0 -1
- package/lib/types/profile-state.js +0 -48
- package/lib/types/profile-state.js.map +0 -1
package/lib/types/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
34
34
|
done = true;
|
|
35
35
|
};
|
|
36
36
|
import { Context } from '@deepseek-ai/cordis';
|
|
37
|
-
import {
|
|
37
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
38
38
|
import { homedir } from 'node:os';
|
|
39
39
|
import { isAbsolute, join } from 'node:path';
|
|
40
40
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -54,7 +54,7 @@ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "
|
|
|
54
54
|
import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
|
|
55
55
|
import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
|
|
56
56
|
import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
|
|
57
|
-
import {
|
|
57
|
+
import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
|
|
58
58
|
export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
|
|
59
59
|
export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
|
|
60
60
|
export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
|
|
@@ -87,6 +87,7 @@ export const Config = z.object({
|
|
|
87
87
|
attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
|
|
88
88
|
imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
|
|
89
89
|
pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
|
|
90
|
+
realtimeEnabled: z.boolean().default(true),
|
|
90
91
|
listenerEnabled: z.boolean().default(false),
|
|
91
92
|
listenerAllowedPeers: z.array(z.string()).default([]),
|
|
92
93
|
listenerWorkspacePath: z.string(),
|
|
@@ -104,7 +105,6 @@ const FAILURE_CODES = new Set([
|
|
|
104
105
|
'forbidden',
|
|
105
106
|
'identity-recovery-required',
|
|
106
107
|
'conflict',
|
|
107
|
-
'state-in-use',
|
|
108
108
|
'rate-limited',
|
|
109
109
|
'group-membership-required',
|
|
110
110
|
'group-identity-stale',
|
|
@@ -118,10 +118,17 @@ const FAILURE_CODES = new Set([
|
|
|
118
118
|
'network',
|
|
119
119
|
'remote',
|
|
120
120
|
]);
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
121
|
+
const DEVICE_JOIN_APPROVAL_CONFIRMATION = 'APPROVE';
|
|
122
|
+
const DEVICE_REVOKE_CONFIRMATION = 'REVOKE';
|
|
123
|
+
const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
|
|
124
|
+
const RESUMABLE_JOIN_PHASES = new Set([
|
|
125
|
+
'pending',
|
|
126
|
+
'challenge_prepared',
|
|
127
|
+
'response_prepared',
|
|
128
|
+
'response_verified',
|
|
129
|
+
'approval_prepared',
|
|
130
|
+
'authorized',
|
|
131
|
+
]);
|
|
125
132
|
const FAILURE_MESSAGES = {
|
|
126
133
|
'not-registered': 'No AWiki identity is registered for this deployment.',
|
|
127
134
|
'signed-out': 'This installation is signed out of AWiki.',
|
|
@@ -134,7 +141,6 @@ const FAILURE_MESSAGES = {
|
|
|
134
141
|
'forbidden': 'The AWiki operation is not permitted.',
|
|
135
142
|
'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
|
|
136
143
|
'conflict': 'The AWiki operation conflicts with current state.',
|
|
137
|
-
'state-in-use': 'AWiki data is already open in another client.',
|
|
138
144
|
'rate-limited': 'The AWiki service rate-limited the request.',
|
|
139
145
|
'group-membership-required': 'The active AWiki identity is not a member of this group.',
|
|
140
146
|
'group-identity-stale': 'The AWiki group identity binding is still recovering.',
|
|
@@ -152,6 +158,67 @@ class ProviderUnavailableError extends Error {
|
|
|
152
158
|
}
|
|
153
159
|
class SummaryProviderUnavailableError extends Error {
|
|
154
160
|
}
|
|
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
|
+
}
|
|
155
222
|
/** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
|
|
156
223
|
function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
|
|
157
224
|
let url;
|
|
@@ -253,21 +320,16 @@ function listenerAllowedPeers(raw, enabled) {
|
|
|
253
320
|
return peers;
|
|
254
321
|
}
|
|
255
322
|
/** Resolve and validate every deployment choice before publishing the service. */
|
|
256
|
-
function resolveConfig(
|
|
323
|
+
function resolveConfig(config) {
|
|
257
324
|
const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
|
|
258
325
|
const configuredStateRoot = config.stateRoot?.trim();
|
|
259
326
|
const configuredDshHome = process.env.DSH_HOME?.trim();
|
|
260
327
|
const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
|
|
261
328
|
? join(homedir(), '.dsh')
|
|
262
329
|
: configuredDshHome;
|
|
263
|
-
const profileName = configuredStateRoot === undefined || configuredStateRoot.length === 0
|
|
264
|
-
? resolveAwikiProfileName(ctx, dshHome)
|
|
265
|
-
: undefined;
|
|
266
330
|
const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
|
|
267
|
-
?
|
|
331
|
+
? join(dshHome, 'awiki', 'im-core')
|
|
268
332
|
: configuredStateRoot;
|
|
269
|
-
const legacySharedStateDetected = profileName !== undefined
|
|
270
|
-
&& existsSync(join(dshHome, 'awiki', 'im-core'));
|
|
271
333
|
if (!isAbsolute(stateRoot))
|
|
272
334
|
throw new TypeError('awiki: stateRoot must be an absolute path');
|
|
273
335
|
const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
@@ -284,6 +346,10 @@ function resolveConfig(ctx, config) {
|
|
|
284
346
|
throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
|
|
285
347
|
}
|
|
286
348
|
const listenerEnabled = config.listenerEnabled ?? false;
|
|
349
|
+
const realtimeEnabled = config.realtimeEnabled ?? true;
|
|
350
|
+
if (listenerEnabled && !realtimeEnabled) {
|
|
351
|
+
throw new TypeError('awiki: listenerEnabled requires realtimeEnabled');
|
|
352
|
+
}
|
|
287
353
|
const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
|
|
288
354
|
if (!isAbsolute(listenerWorkspacePath)) {
|
|
289
355
|
throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
|
|
@@ -310,7 +376,7 @@ function resolveConfig(ctx, config) {
|
|
|
310
376
|
attachmentMaxBytes,
|
|
311
377
|
imageAttachmentCacheMaxBytes,
|
|
312
378
|
pollIntervalMs,
|
|
313
|
-
|
|
379
|
+
realtimeEnabled,
|
|
314
380
|
listenerEnabled,
|
|
315
381
|
listener: {
|
|
316
382
|
allowedPeers,
|
|
@@ -471,6 +537,7 @@ function normalizeRecoveryPrepareRequest(request) {
|
|
|
471
537
|
return { ...operation, phone, otp };
|
|
472
538
|
}
|
|
473
539
|
const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
540
|
+
const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
474
541
|
const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
|
|
475
542
|
/** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
|
|
476
543
|
async function readBoundedResponseText(response, maxBytes) {
|
|
@@ -700,6 +767,14 @@ let AwikiService = (() => {
|
|
|
700
767
|
let _inspectIdentityAccess_decorators;
|
|
701
768
|
let _sendRegistrationOtp_decorators;
|
|
702
769
|
let _registerIdentity_decorators;
|
|
770
|
+
let _beginDeviceJoin_decorators;
|
|
771
|
+
let _getDeviceJoinStatus_decorators;
|
|
772
|
+
let _cancelDeviceJoin_decorators;
|
|
773
|
+
let _refreshDeviceManagement_decorators;
|
|
774
|
+
let _startDeviceJoinVerification_decorators;
|
|
775
|
+
let _approveDeviceJoin_decorators;
|
|
776
|
+
let _rejectDeviceJoin_decorators;
|
|
777
|
+
let _revokeDevice_decorators;
|
|
703
778
|
let _updateDisplayName_decorators;
|
|
704
779
|
let _getProfile_decorators;
|
|
705
780
|
let _updateProfile_decorators;
|
|
@@ -745,6 +820,14 @@ let AwikiService = (() => {
|
|
|
745
820
|
_inspectIdentityAccess_decorators = [Remote];
|
|
746
821
|
_sendRegistrationOtp_decorators = [Remote];
|
|
747
822
|
_registerIdentity_decorators = [Remote];
|
|
823
|
+
_beginDeviceJoin_decorators = [Remote];
|
|
824
|
+
_getDeviceJoinStatus_decorators = [Remote];
|
|
825
|
+
_cancelDeviceJoin_decorators = [Remote];
|
|
826
|
+
_refreshDeviceManagement_decorators = [Remote];
|
|
827
|
+
_startDeviceJoinVerification_decorators = [Remote];
|
|
828
|
+
_approveDeviceJoin_decorators = [Remote];
|
|
829
|
+
_rejectDeviceJoin_decorators = [Remote];
|
|
830
|
+
_revokeDevice_decorators = [Remote];
|
|
748
831
|
_updateDisplayName_decorators = [Remote];
|
|
749
832
|
_getProfile_decorators = [Remote];
|
|
750
833
|
_updateProfile_decorators = [Remote];
|
|
@@ -787,6 +870,14 @@ let AwikiService = (() => {
|
|
|
787
870
|
__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);
|
|
788
871
|
__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);
|
|
789
872
|
__esDecorate(this, null, _registerIdentity_decorators, { kind: "method", name: "registerIdentity", static: false, private: false, access: { has: obj => "registerIdentity" in obj, get: obj => obj.registerIdentity }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
873
|
+
__esDecorate(this, null, _beginDeviceJoin_decorators, { kind: "method", name: "beginDeviceJoin", static: false, private: false, access: { has: obj => "beginDeviceJoin" in obj, get: obj => obj.beginDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
874
|
+
__esDecorate(this, null, _getDeviceJoinStatus_decorators, { kind: "method", name: "getDeviceJoinStatus", static: false, private: false, access: { has: obj => "getDeviceJoinStatus" in obj, get: obj => obj.getDeviceJoinStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
875
|
+
__esDecorate(this, null, _cancelDeviceJoin_decorators, { kind: "method", name: "cancelDeviceJoin", static: false, private: false, access: { has: obj => "cancelDeviceJoin" in obj, get: obj => obj.cancelDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
876
|
+
__esDecorate(this, null, _refreshDeviceManagement_decorators, { kind: "method", name: "refreshDeviceManagement", static: false, private: false, access: { has: obj => "refreshDeviceManagement" in obj, get: obj => obj.refreshDeviceManagement }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
877
|
+
__esDecorate(this, null, _startDeviceJoinVerification_decorators, { kind: "method", name: "startDeviceJoinVerification", static: false, private: false, access: { has: obj => "startDeviceJoinVerification" in obj, get: obj => obj.startDeviceJoinVerification }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
878
|
+
__esDecorate(this, null, _approveDeviceJoin_decorators, { kind: "method", name: "approveDeviceJoin", static: false, private: false, access: { has: obj => "approveDeviceJoin" in obj, get: obj => obj.approveDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
879
|
+
__esDecorate(this, null, _rejectDeviceJoin_decorators, { kind: "method", name: "rejectDeviceJoin", static: false, private: false, access: { has: obj => "rejectDeviceJoin" in obj, get: obj => obj.rejectDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
880
|
+
__esDecorate(this, null, _revokeDevice_decorators, { kind: "method", name: "revokeDevice", static: false, private: false, access: { has: obj => "revokeDevice" in obj, get: obj => obj.revokeDevice }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
790
881
|
__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);
|
|
791
882
|
__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);
|
|
792
883
|
__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);
|
|
@@ -837,6 +928,12 @@ let AwikiService = (() => {
|
|
|
837
928
|
sessionMutation = Promise.resolve();
|
|
838
929
|
sessionRevision = 0;
|
|
839
930
|
activeIdentityDid;
|
|
931
|
+
pendingDeviceJoin;
|
|
932
|
+
activeDeviceJoinSessionId;
|
|
933
|
+
requestRefs = new Map();
|
|
934
|
+
requestSessions = new Map();
|
|
935
|
+
deviceRefs = new Map();
|
|
936
|
+
deviceIds = new Map();
|
|
840
937
|
activeSummaryRequests = new Set();
|
|
841
938
|
summaryProvider;
|
|
842
939
|
recoveryReconciliationTarget;
|
|
@@ -851,7 +948,7 @@ let AwikiService = (() => {
|
|
|
851
948
|
constructor(ctx, config) {
|
|
852
949
|
super(ctx, 'awiki');
|
|
853
950
|
this.hostContext = ctx;
|
|
854
|
-
this.resolved = resolveConfig(
|
|
951
|
+
this.resolved = resolveConfig(config);
|
|
855
952
|
this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
|
|
856
953
|
this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
|
|
857
954
|
this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
|
|
@@ -881,14 +978,14 @@ let AwikiService = (() => {
|
|
|
881
978
|
this.workspaceContext = workspaceCtx;
|
|
882
979
|
const provider = this.provider;
|
|
883
980
|
if (provider !== undefined)
|
|
884
|
-
|
|
981
|
+
this.ensureAgentConsumer(provider);
|
|
885
982
|
workspaceCtx.effect(() => async () => {
|
|
886
983
|
if (this.workspaceContext !== workspaceCtx)
|
|
887
984
|
return;
|
|
888
985
|
this.workspaceContext = undefined;
|
|
889
986
|
const current = this.provider;
|
|
890
987
|
if (current !== undefined)
|
|
891
|
-
await this.
|
|
988
|
+
await this.stopAgentConsumer(current);
|
|
892
989
|
}, 'awiki: release Workspace listener composition');
|
|
893
990
|
});
|
|
894
991
|
registerAwikiTools(ctx, this);
|
|
@@ -911,6 +1008,8 @@ let AwikiService = (() => {
|
|
|
911
1008
|
registerClientFactory(factory) {
|
|
912
1009
|
if (this.provider !== undefined)
|
|
913
1010
|
throw new Error('awiki: a client provider is already registered');
|
|
1011
|
+
this.pendingDeviceJoin = undefined;
|
|
1012
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
914
1013
|
const client = factory({
|
|
915
1014
|
userServiceUrl: this.resolved.userServiceUrl,
|
|
916
1015
|
userServiceDomain: this.startupUserServiceDomain,
|
|
@@ -925,14 +1024,29 @@ let AwikiService = (() => {
|
|
|
925
1024
|
});
|
|
926
1025
|
const provider = {
|
|
927
1026
|
client,
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1027
|
+
realtimeGeneration: 0,
|
|
1028
|
+
agentConsumerGeneration: 0,
|
|
1029
|
+
localDeviceJoinRequestCountAfterSync: 0,
|
|
931
1030
|
};
|
|
932
1031
|
this.provider = provider;
|
|
933
|
-
|
|
1032
|
+
this.ensureRealtimeSupervisor(provider);
|
|
1033
|
+
this.ensureAgentConsumer(provider);
|
|
934
1034
|
return () => this.disposeProvider(provider);
|
|
935
1035
|
}
|
|
1036
|
+
/** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
|
|
1037
|
+
getRealtimeDiagnostics() {
|
|
1038
|
+
const provider = this.provider;
|
|
1039
|
+
const realtime = provider?.realtimeSupervisor?.diagnostics() ?? {
|
|
1040
|
+
connected: false,
|
|
1041
|
+
activeSessionCount: 0,
|
|
1042
|
+
startCount: 0,
|
|
1043
|
+
stopCount: 0,
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
...realtime,
|
|
1047
|
+
localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
936
1050
|
/** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
|
|
937
1051
|
registerRecoveryReconciliationTarget(target) {
|
|
938
1052
|
if (this.recoveryReconciliationTarget !== undefined) {
|
|
@@ -970,18 +1084,43 @@ let AwikiService = (() => {
|
|
|
970
1084
|
* Read settings needed by the browser presentation.
|
|
971
1085
|
* @returns Browser-safe polling configuration without SDK endpoints or state paths.
|
|
972
1086
|
*/
|
|
973
|
-
getConfig() {
|
|
974
|
-
|
|
1087
|
+
async getConfig() {
|
|
1088
|
+
let handleRecoveryPhoneEnabled = false;
|
|
1089
|
+
const abort = new AbortController();
|
|
1090
|
+
const timeout = setTimeout(() => { abort.abort(); }, 10_000);
|
|
1091
|
+
try {
|
|
1092
|
+
const response = await fetch(new URL('/user-service/v1/server-info', this.resolved.userServiceUrl), {
|
|
1093
|
+
method: 'GET',
|
|
1094
|
+
headers: { accept: 'application/json', 'cache-control': 'no-store' },
|
|
1095
|
+
cache: 'no-store',
|
|
1096
|
+
redirect: 'error',
|
|
1097
|
+
signal: abort.signal,
|
|
1098
|
+
});
|
|
1099
|
+
if (response.ok) {
|
|
1100
|
+
const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
|
|
1101
|
+
const value = text === undefined ? undefined : JSON.parse(text);
|
|
1102
|
+
if (typeof value === 'object' && value !== null && value.schema_version === 1) {
|
|
1103
|
+
const methods = value.identity?.handle_recovery?.methods;
|
|
1104
|
+
handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
|
|
1105
|
+
&& method.id === 'phone'
|
|
1106
|
+
&& method.enabled === true
|
|
1107
|
+
&& method.verification?.required === true
|
|
1108
|
+
&& method.verification?.type === 'sms_otp'));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch { }
|
|
1113
|
+
finally {
|
|
1114
|
+
clearTimeout(timeout);
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
975
1117
|
ok: true,
|
|
976
1118
|
value: {
|
|
977
1119
|
pollIntervalMs: this.resolved.pollIntervalMs,
|
|
978
1120
|
attachmentMaxBytes: this.resolved.attachmentMaxBytes,
|
|
979
|
-
|
|
980
|
-
profileName: this.resolved.profileName,
|
|
981
|
-
legacySharedStateDetected: this.resolved.legacySharedStateDetected,
|
|
982
|
-
},
|
|
1121
|
+
handleRecoveryPhoneEnabled,
|
|
983
1122
|
},
|
|
984
|
-
}
|
|
1123
|
+
};
|
|
985
1124
|
}
|
|
986
1125
|
/**
|
|
987
1126
|
* Read the deployment's identity status.
|
|
@@ -1003,6 +1142,9 @@ let AwikiService = (() => {
|
|
|
1003
1142
|
if (!identity.ok)
|
|
1004
1143
|
return identity;
|
|
1005
1144
|
this.activeIdentityDid = identity.value?.did;
|
|
1145
|
+
const provider = this.provider;
|
|
1146
|
+
if (identity.value !== null && provider !== undefined)
|
|
1147
|
+
this.ensureProviderRuntime(provider);
|
|
1006
1148
|
return identity.value === null
|
|
1007
1149
|
? { ok: true, value: { status: 'unregistered' } }
|
|
1008
1150
|
: { ok: true, value: { status: 'active', identity: identity.value } };
|
|
@@ -1029,7 +1171,7 @@ let AwikiService = (() => {
|
|
|
1029
1171
|
this.publishSession(session);
|
|
1030
1172
|
const provider = this.provider;
|
|
1031
1173
|
if (provider !== undefined)
|
|
1032
|
-
await this.
|
|
1174
|
+
await this.stopProviderRuntime(provider);
|
|
1033
1175
|
return { ok: true, value: session };
|
|
1034
1176
|
}
|
|
1035
1177
|
catch {
|
|
@@ -1054,7 +1196,7 @@ let AwikiService = (() => {
|
|
|
1054
1196
|
this.publishSession(session);
|
|
1055
1197
|
const provider = this.provider;
|
|
1056
1198
|
if (provider !== undefined)
|
|
1057
|
-
|
|
1199
|
+
this.ensureProviderRuntime(provider);
|
|
1058
1200
|
return { ok: true, value: session };
|
|
1059
1201
|
}
|
|
1060
1202
|
catch {
|
|
@@ -1112,8 +1254,14 @@ let AwikiService = (() => {
|
|
|
1112
1254
|
return { ok: false, error: failure('remote') };
|
|
1113
1255
|
}
|
|
1114
1256
|
}
|
|
1115
|
-
sendRegistrationOtp(request) {
|
|
1116
|
-
|
|
1257
|
+
async sendRegistrationOtp(request) {
|
|
1258
|
+
this.pendingDeviceJoin = undefined;
|
|
1259
|
+
return this.run(async (client) => {
|
|
1260
|
+
if (await this.selectDeviceJoinSession(client) !== null) {
|
|
1261
|
+
throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
|
|
1262
|
+
}
|
|
1263
|
+
return client.sendRegistrationOtp(request);
|
|
1264
|
+
});
|
|
1117
1265
|
}
|
|
1118
1266
|
/**
|
|
1119
1267
|
* Register and persist the deployment's only AWiki identity.
|
|
@@ -1121,10 +1269,177 @@ let AwikiService = (() => {
|
|
|
1121
1269
|
* @returns The new public identity or a closed failure.
|
|
1122
1270
|
*/
|
|
1123
1271
|
async registerIdentity(request) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
await this.
|
|
1127
|
-
|
|
1272
|
+
this.pendingDeviceJoin = undefined;
|
|
1273
|
+
const result = await this.run(async (client) => {
|
|
1274
|
+
if (await this.selectDeviceJoinSession(client) !== null) {
|
|
1275
|
+
throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
|
|
1276
|
+
}
|
|
1277
|
+
return client.registerIdentity(request);
|
|
1278
|
+
});
|
|
1279
|
+
if (!result.ok)
|
|
1280
|
+
return result;
|
|
1281
|
+
if (result.value.status === 'registered') {
|
|
1282
|
+
await this.activateRegisteredIdentity(result.value.identity);
|
|
1283
|
+
return { ok: true, value: { status: 'registered', identity: result.value.identity } };
|
|
1284
|
+
}
|
|
1285
|
+
this.pendingDeviceJoin = {
|
|
1286
|
+
continuationId: result.value.continuationId,
|
|
1287
|
+
fullHandle: result.value.fullHandle,
|
|
1288
|
+
mode: result.value.mode,
|
|
1289
|
+
requiresUserPresence: result.value.requiresUserPresence,
|
|
1290
|
+
};
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
value: {
|
|
1294
|
+
status: 'join-required',
|
|
1295
|
+
fullHandle: result.value.fullHandle,
|
|
1296
|
+
mode: result.value.mode,
|
|
1297
|
+
requiresUserPresence: result.value.requiresUserPresence,
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
/** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
|
|
1302
|
+
async beginDeviceJoin() {
|
|
1303
|
+
const continuation = this.pendingDeviceJoin;
|
|
1304
|
+
if (continuation === undefined)
|
|
1305
|
+
return { ok: false, error: failure('conflict') };
|
|
1306
|
+
if (continuation.mode !== 'ordinary' || continuation.requiresUserPresence) {
|
|
1307
|
+
return { ok: false, error: failure('forbidden') };
|
|
1308
|
+
}
|
|
1309
|
+
this.pendingDeviceJoin = undefined;
|
|
1310
|
+
const result = await this.run(client => client.beginDeviceJoin({
|
|
1311
|
+
continuationId: continuation.continuationId,
|
|
1312
|
+
operationId: `dsh-device-join-${randomUUID()}`,
|
|
1313
|
+
userPresenceConfirmed: false,
|
|
1314
|
+
}));
|
|
1315
|
+
if (!result.ok)
|
|
1316
|
+
return result;
|
|
1317
|
+
this.activeDeviceJoinSessionId = result.value.joinSessionId;
|
|
1318
|
+
return this.applyCandidateJoinProgress(result.value);
|
|
1319
|
+
}
|
|
1320
|
+
/** Restore from Core local_sessions and advance only the exact resumable Join. */
|
|
1321
|
+
async getDeviceJoinStatus() {
|
|
1322
|
+
const result = await this.run(async (client) => {
|
|
1323
|
+
const joinSessionId = await this.selectDeviceJoinSession(client);
|
|
1324
|
+
return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
|
|
1325
|
+
});
|
|
1326
|
+
if (!result.ok)
|
|
1327
|
+
return result;
|
|
1328
|
+
if (result.value === null)
|
|
1329
|
+
return { ok: true, value: null };
|
|
1330
|
+
this.activeDeviceJoinSessionId = result.value.joinSessionId;
|
|
1331
|
+
return this.applyCandidateJoinProgress(result.value);
|
|
1332
|
+
}
|
|
1333
|
+
async cancelDeviceJoin() {
|
|
1334
|
+
const result = await this.run(async (client) => {
|
|
1335
|
+
const joinSessionId = await this.selectDeviceJoinSession(client);
|
|
1336
|
+
if (joinSessionId === null)
|
|
1337
|
+
return null;
|
|
1338
|
+
return client.cancelDeviceJoin(joinSessionId);
|
|
1339
|
+
});
|
|
1340
|
+
if (!result.ok)
|
|
1341
|
+
return result;
|
|
1342
|
+
this.pendingDeviceJoin = undefined;
|
|
1343
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
1344
|
+
return { ok: true, value: { completed: true } };
|
|
1345
|
+
}
|
|
1346
|
+
/** Reliable-sync and project only Host-opaque device/request references. */
|
|
1347
|
+
refreshDeviceManagement() {
|
|
1348
|
+
return this.run(client => this.deviceManagementSnapshot(client));
|
|
1349
|
+
}
|
|
1350
|
+
async startDeviceJoinVerification(request) {
|
|
1351
|
+
const joinSessionId = this.requestSessions.get(request?.requestRef);
|
|
1352
|
+
if (joinSessionId === undefined)
|
|
1353
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1354
|
+
const result = await this.run(async (client) => {
|
|
1355
|
+
await this.requireDeviceManager(client);
|
|
1356
|
+
await client.syncDeviceManagement();
|
|
1357
|
+
let notice = (await client.listLocalDeviceJoinRequests())
|
|
1358
|
+
.find(value => value.joinSessionId === joinSessionId);
|
|
1359
|
+
if (notice === undefined) {
|
|
1360
|
+
throw Object.assign(new Error('request not found'), { name: 'AwikiSdkError', code: 'not-found' });
|
|
1361
|
+
}
|
|
1362
|
+
const localProgress = await this.localAdminJoinProgress(client, notice);
|
|
1363
|
+
if (localProgress !== undefined)
|
|
1364
|
+
return localProgress;
|
|
1365
|
+
if (!notice.canStartVerification) {
|
|
1366
|
+
throw Object.assign(new Error('request unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
|
|
1367
|
+
}
|
|
1368
|
+
const operationId = `dsh-device-verify-${createHash('sha256').update(joinSessionId).digest('hex').slice(0, 32)}`;
|
|
1369
|
+
let started;
|
|
1370
|
+
try {
|
|
1371
|
+
started = await client.startDeviceJoinVerification({
|
|
1372
|
+
joinSessionId,
|
|
1373
|
+
operationId,
|
|
1374
|
+
challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
catch (error) {
|
|
1378
|
+
await client.syncDeviceManagement();
|
|
1379
|
+
notice = (await client.listLocalDeviceJoinRequests())
|
|
1380
|
+
.find(value => value.joinSessionId === joinSessionId);
|
|
1381
|
+
const recovered = notice === undefined ? undefined : await this.localAdminJoinProgress(client, notice);
|
|
1382
|
+
if (recovered !== undefined)
|
|
1383
|
+
return recovered;
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
await client.syncDeviceManagement();
|
|
1387
|
+
notice = (await client.listLocalDeviceJoinRequests())
|
|
1388
|
+
.find(value => value.joinSessionId === joinSessionId);
|
|
1389
|
+
if (notice === undefined)
|
|
1390
|
+
return started;
|
|
1391
|
+
return (await this.localAdminJoinProgress(client, notice)) ?? started;
|
|
1392
|
+
});
|
|
1393
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
1394
|
+
}
|
|
1395
|
+
async approveDeviceJoin(request) {
|
|
1396
|
+
if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) {
|
|
1397
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1398
|
+
}
|
|
1399
|
+
const joinSessionId = this.requestSessions.get(request.requestRef);
|
|
1400
|
+
if (joinSessionId === undefined || !/^\d{6}$/u.test(request.enteredSas)) {
|
|
1401
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1402
|
+
}
|
|
1403
|
+
const result = await this.run(async (client) => {
|
|
1404
|
+
await this.requireDeviceManager(client);
|
|
1405
|
+
const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
|
|
1406
|
+
if (progress.sas === undefined || !constantTimeSasMatches(progress.sas, request.enteredSas)) {
|
|
1407
|
+
throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
|
|
1408
|
+
}
|
|
1409
|
+
const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
|
|
1410
|
+
return client.confirmDeviceJoinApproval(prompt.approvalHandle);
|
|
1411
|
+
});
|
|
1412
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
1413
|
+
}
|
|
1414
|
+
async rejectDeviceJoin(request) {
|
|
1415
|
+
const joinSessionId = this.requestSessions.get(request?.requestRef);
|
|
1416
|
+
if (joinSessionId === undefined
|
|
1417
|
+
|| (request.reason !== 'user_rejected' && request.reason !== 'sas_mismatch')) {
|
|
1418
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1419
|
+
}
|
|
1420
|
+
const result = await this.run(async (client) => {
|
|
1421
|
+
await this.requireDeviceManager(client);
|
|
1422
|
+
return client.rejectDeviceJoin(joinSessionId, request.reason);
|
|
1423
|
+
});
|
|
1424
|
+
return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
|
|
1425
|
+
}
|
|
1426
|
+
async revokeDevice(request) {
|
|
1427
|
+
if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) {
|
|
1428
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1429
|
+
}
|
|
1430
|
+
const deviceId = this.deviceIds.get(request.deviceRef);
|
|
1431
|
+
if (deviceId === undefined)
|
|
1432
|
+
return { ok: false, error: failure('invalid-request') };
|
|
1433
|
+
return this.run(async (client) => {
|
|
1434
|
+
await this.requireDeviceManager(client);
|
|
1435
|
+
const devices = await client.getDeviceRegistry();
|
|
1436
|
+
const target = devices.find(device => device.deviceId === deviceId);
|
|
1437
|
+
if (target === undefined || target.isCurrent) {
|
|
1438
|
+
throw Object.assign(new Error('device unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
|
|
1439
|
+
}
|
|
1440
|
+
await client.revokeDevice(deviceId);
|
|
1441
|
+
return this.deviceManagementSnapshot(client);
|
|
1442
|
+
});
|
|
1128
1443
|
}
|
|
1129
1444
|
/**
|
|
1130
1445
|
* Update the deployment identity's public WNS display name.
|
|
@@ -1150,10 +1465,16 @@ let AwikiService = (() => {
|
|
|
1150
1465
|
}
|
|
1151
1466
|
/** Start durable phone recovery for one existing full Handle. */
|
|
1152
1467
|
sendRecoveryOtp(request) {
|
|
1468
|
+
this.pendingDeviceJoin = undefined;
|
|
1153
1469
|
const normalized = normalizeRecoveryOtpRequest(request);
|
|
1154
1470
|
if (normalized === undefined)
|
|
1155
1471
|
return Promise.resolve({ ok: false, error: failure('invalid-request') });
|
|
1156
|
-
return this.run(client =>
|
|
1472
|
+
return this.run(async (client) => {
|
|
1473
|
+
if (await this.selectDeviceJoinSession(client) !== null) {
|
|
1474
|
+
throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
|
|
1475
|
+
}
|
|
1476
|
+
return client.sendRecoveryOtp(normalized);
|
|
1477
|
+
}, { allowSignedOut: true });
|
|
1157
1478
|
}
|
|
1158
1479
|
/** Verify a recovery OTP and freeze its exact intent before the remote commit. */
|
|
1159
1480
|
prepareRecovery(request) {
|
|
@@ -1600,11 +1921,11 @@ let AwikiService = (() => {
|
|
|
1600
1921
|
return this.mutateSession(async () => {
|
|
1601
1922
|
const provider = this.provider;
|
|
1602
1923
|
if (provider !== undefined)
|
|
1603
|
-
await this.
|
|
1924
|
+
await this.stopProviderRuntime(provider);
|
|
1604
1925
|
const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
|
|
1605
1926
|
if (!result.ok) {
|
|
1606
1927
|
if (provider !== undefined && this.provider === provider)
|
|
1607
|
-
|
|
1928
|
+
this.ensureProviderRuntime(provider);
|
|
1608
1929
|
return result;
|
|
1609
1930
|
}
|
|
1610
1931
|
try {
|
|
@@ -1614,10 +1935,10 @@ let AwikiService = (() => {
|
|
|
1614
1935
|
await this.sessionStore.signIn();
|
|
1615
1936
|
this.signedOut = false;
|
|
1616
1937
|
this.activeIdentityDid = undefined;
|
|
1938
|
+
this.pendingDeviceJoin = undefined;
|
|
1939
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
1617
1940
|
this.invalidateSummaries();
|
|
1618
1941
|
this.publishSession({ status: 'unregistered' });
|
|
1619
|
-
if (provider !== undefined && this.provider === provider)
|
|
1620
|
-
void this.startListener(provider);
|
|
1621
1942
|
return result;
|
|
1622
1943
|
}
|
|
1623
1944
|
catch {
|
|
@@ -1650,8 +1971,7 @@ let AwikiService = (() => {
|
|
|
1650
1971
|
if (!alreadyActive) {
|
|
1651
1972
|
const session = { status: 'active', identity };
|
|
1652
1973
|
this.publishSession(session);
|
|
1653
|
-
|
|
1654
|
-
await this.startListener(provider);
|
|
1974
|
+
this.ensureProviderRuntime(provider);
|
|
1655
1975
|
}
|
|
1656
1976
|
return reconciled;
|
|
1657
1977
|
}
|
|
@@ -1692,19 +2012,164 @@ let AwikiService = (() => {
|
|
|
1692
2012
|
return false;
|
|
1693
2013
|
}
|
|
1694
2014
|
}
|
|
1695
|
-
/**
|
|
2015
|
+
/** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
|
|
2016
|
+
async selectDeviceJoinSession(client) {
|
|
2017
|
+
const sessions = await client.listLocalDeviceJoinSessions();
|
|
2018
|
+
if (this.activeDeviceJoinSessionId !== undefined) {
|
|
2019
|
+
const tracked = sessions.find(value => (value.side === 'new_device' && value.joinSessionId === this.activeDeviceJoinSessionId));
|
|
2020
|
+
if (tracked !== undefined)
|
|
2021
|
+
return tracked.joinSessionId;
|
|
2022
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
2023
|
+
}
|
|
2024
|
+
const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
|
|
2025
|
+
if (resumable.length > 1) {
|
|
2026
|
+
throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
|
|
2027
|
+
}
|
|
2028
|
+
const selected = resumable[0];
|
|
2029
|
+
this.activeDeviceJoinSessionId = selected?.joinSessionId;
|
|
2030
|
+
return selected?.joinSessionId ?? null;
|
|
2031
|
+
}
|
|
2032
|
+
async applyCandidateJoinProgress(value) {
|
|
2033
|
+
const phase = candidateJoinPhase(value);
|
|
2034
|
+
if (phase === undefined)
|
|
2035
|
+
return { ok: false, error: failure('remote') };
|
|
2036
|
+
if (phase === 'cancelled' || phase === 'rejected' || phase === 'expired') {
|
|
2037
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
2038
|
+
}
|
|
2039
|
+
if (phase === 'authorized') {
|
|
2040
|
+
if (value.identity === undefined)
|
|
2041
|
+
return { ok: false, error: failure('remote') };
|
|
2042
|
+
await this.activateRegisteredIdentity(value.identity);
|
|
2043
|
+
}
|
|
2044
|
+
return {
|
|
2045
|
+
ok: true,
|
|
2046
|
+
value: {
|
|
2047
|
+
phase,
|
|
2048
|
+
expiresAt: value.expiresAt,
|
|
2049
|
+
...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
|
|
2050
|
+
completed: phase === 'authorized',
|
|
2051
|
+
},
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
publicAdminJoinProgress(requestRef, value) {
|
|
2055
|
+
const phase = adminJoinPhase(value);
|
|
2056
|
+
if (phase === undefined)
|
|
2057
|
+
return { ok: false, error: failure('remote') };
|
|
2058
|
+
return {
|
|
2059
|
+
ok: true,
|
|
2060
|
+
value: {
|
|
2061
|
+
requestRef,
|
|
2062
|
+
phase,
|
|
2063
|
+
expiresAt: value.expiresAt,
|
|
2064
|
+
...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
|
|
2065
|
+
},
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
async localAdminJoinProgress(client, notice) {
|
|
2069
|
+
const local = (await client.listLocalDeviceJoinSessions())
|
|
2070
|
+
.find(session => session.side === 'admin' && session.joinSessionId === notice.joinSessionId);
|
|
2071
|
+
if (local === undefined) {
|
|
2072
|
+
return notice.claimedByCurrentDevice
|
|
2073
|
+
? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId)
|
|
2074
|
+
: undefined;
|
|
2075
|
+
}
|
|
2076
|
+
if (local.localPhase === 'challenge_prepared') {
|
|
2077
|
+
return {
|
|
2078
|
+
joinSessionId: notice.joinSessionId,
|
|
2079
|
+
localPhase: local.localPhase,
|
|
2080
|
+
remoteState: notice.state,
|
|
2081
|
+
expiresAt: local.expiresAt,
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
|
|
2085
|
+
}
|
|
2086
|
+
async requireDeviceManager(client) {
|
|
2087
|
+
const current = await client.getCurrentDeviceSummary();
|
|
2088
|
+
if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
|
|
2089
|
+
throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
requestRef(joinSessionId) {
|
|
2093
|
+
const existing = this.requestRefs.get(joinSessionId);
|
|
2094
|
+
if (existing !== undefined)
|
|
2095
|
+
return existing;
|
|
2096
|
+
const reference = `join-${randomUUID()}`;
|
|
2097
|
+
this.requestRefs.set(joinSessionId, reference);
|
|
2098
|
+
this.requestSessions.set(reference, joinSessionId);
|
|
2099
|
+
return reference;
|
|
2100
|
+
}
|
|
2101
|
+
deviceRef(deviceId) {
|
|
2102
|
+
const existing = this.deviceRefs.get(deviceId);
|
|
2103
|
+
if (existing !== undefined)
|
|
2104
|
+
return existing;
|
|
2105
|
+
const reference = `device-${randomUUID()}`;
|
|
2106
|
+
this.deviceRefs.set(deviceId, reference);
|
|
2107
|
+
this.deviceIds.set(reference, deviceId);
|
|
2108
|
+
return reference;
|
|
2109
|
+
}
|
|
2110
|
+
async deviceManagementSnapshot(client) {
|
|
2111
|
+
await client.syncDeviceManagement();
|
|
2112
|
+
const current = await client.getCurrentDeviceSummary();
|
|
2113
|
+
if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
|
|
2114
|
+
this.requestRefs.clear();
|
|
2115
|
+
this.requestSessions.clear();
|
|
2116
|
+
this.deviceRefs.clear();
|
|
2117
|
+
this.deviceIds.clear();
|
|
2118
|
+
return {
|
|
2119
|
+
canManage: false,
|
|
2120
|
+
...current.role === undefined ? {} : { role: current.role },
|
|
2121
|
+
readiness: current.readiness,
|
|
2122
|
+
devices: [],
|
|
2123
|
+
requests: [],
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
const requests = await client.listLocalDeviceJoinRequests();
|
|
2127
|
+
await Promise.all(requests
|
|
2128
|
+
.filter(request => request.claimedByCurrentDevice)
|
|
2129
|
+
.map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
|
|
2130
|
+
const devices = await client.getDeviceRegistry();
|
|
2131
|
+
return {
|
|
2132
|
+
canManage: true,
|
|
2133
|
+
role: 'admin',
|
|
2134
|
+
readiness: 'admin_ready',
|
|
2135
|
+
devices: devices.map(device => this.publicDevice(device)),
|
|
2136
|
+
requests: requests.map(request => ({
|
|
2137
|
+
requestRef: this.requestRef(request.joinSessionId),
|
|
2138
|
+
candidateKeyFingerprint: request.candidateKeyFingerprint,
|
|
2139
|
+
issuedAt: request.issuedAt,
|
|
2140
|
+
expiresAt: request.expiresAt,
|
|
2141
|
+
state: requestJoinPhase(request),
|
|
2142
|
+
claimedByCurrentDevice: request.claimedByCurrentDevice,
|
|
2143
|
+
canStartVerification: request.canStartVerification,
|
|
2144
|
+
})),
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
publicDevice(device) {
|
|
2148
|
+
return {
|
|
2149
|
+
deviceRef: this.deviceRef(device.deviceId),
|
|
2150
|
+
status: device.status,
|
|
2151
|
+
role: device.role,
|
|
2152
|
+
managementReady: device.managementReady,
|
|
2153
|
+
isCurrent: device.isCurrent,
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
/** Publish one newly registered identity, then start realtime only in the background. */
|
|
1696
2157
|
async activateRegisteredIdentity(identity) {
|
|
2158
|
+
this.pendingDeviceJoin = undefined;
|
|
2159
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
1697
2160
|
this.activeIdentityDid = identity.did;
|
|
1698
2161
|
this.publishSession({ status: 'active', identity });
|
|
1699
2162
|
const provider = this.provider;
|
|
1700
|
-
if (provider !== undefined)
|
|
1701
|
-
|
|
1702
|
-
await this.startListener(provider);
|
|
1703
|
-
}
|
|
2163
|
+
if (provider !== undefined)
|
|
2164
|
+
this.ensureProviderRuntime(provider);
|
|
1704
2165
|
}
|
|
1705
2166
|
/** Invalidate cached session work and cancel every model request still owned by the old session. */
|
|
1706
2167
|
invalidateSummaries() {
|
|
1707
2168
|
this.sessionRevision += 1;
|
|
2169
|
+
this.requestRefs.clear();
|
|
2170
|
+
this.requestSessions.clear();
|
|
2171
|
+
this.deviceRefs.clear();
|
|
2172
|
+
this.deviceIds.clear();
|
|
1708
2173
|
for (const controller of this.activeSummaryRequests)
|
|
1709
2174
|
controller.abort();
|
|
1710
2175
|
this.activeSummaryRequests.clear();
|
|
@@ -1806,158 +2271,194 @@ let AwikiService = (() => {
|
|
|
1806
2271
|
this.sessionMutation = pending.then(() => undefined, () => undefined);
|
|
1807
2272
|
return pending;
|
|
1808
2273
|
}
|
|
1809
|
-
/** Start
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
return
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
const
|
|
2274
|
+
/** Start identity realtime and the optional Agent consumer without blocking identity success. */
|
|
2275
|
+
ensureProviderRuntime(provider) {
|
|
2276
|
+
const identityDid = this.activeIdentityDid;
|
|
2277
|
+
if (provider.runtimeReplacement !== undefined)
|
|
2278
|
+
return;
|
|
2279
|
+
if (identityDid !== undefined
|
|
2280
|
+
&& provider.realtimeIdentityDid !== undefined
|
|
2281
|
+
&& provider.realtimeIdentityDid !== identityDid) {
|
|
2282
|
+
this.replaceProviderRuntime(provider, identityDid);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
this.ensureRealtimeSupervisor(provider);
|
|
2286
|
+
this.ensureAgentConsumer(provider);
|
|
2287
|
+
}
|
|
2288
|
+
replaceProviderRuntime(provider, identityDid) {
|
|
2289
|
+
if (provider.runtimeReplacement !== undefined || this.provider !== provider)
|
|
2290
|
+
return;
|
|
2291
|
+
const replacement = (async () => {
|
|
2292
|
+
await this.stopProviderRuntime(provider);
|
|
2293
|
+
if (this.provider !== provider || this.activeIdentityDid !== identityDid)
|
|
2294
|
+
return;
|
|
2295
|
+
this.ensureRealtimeSupervisor(provider);
|
|
2296
|
+
this.ensureAgentConsumer(provider);
|
|
2297
|
+
})();
|
|
2298
|
+
const observed = replacement
|
|
2299
|
+
.catch((error) => {
|
|
2300
|
+
this.ctx.logger('awiki-realtime').warn('AWiki identity realtime replacement failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
2301
|
+
})
|
|
2302
|
+
.finally(() => {
|
|
2303
|
+
if (provider.runtimeReplacement === observed)
|
|
2304
|
+
delete provider.runtimeReplacement;
|
|
2305
|
+
if (this.provider === provider)
|
|
2306
|
+
this.ensureProviderRuntime(provider);
|
|
2307
|
+
});
|
|
2308
|
+
provider.runtimeReplacement = observed;
|
|
2309
|
+
}
|
|
2310
|
+
ensureRealtimeSupervisor(provider) {
|
|
2311
|
+
if (!this.resolved.realtimeEnabled
|
|
2312
|
+
|| provider.realtimeSupervisor !== undefined
|
|
2313
|
+
|| provider.realtimeStartup !== undefined
|
|
2314
|
+
|| this.provider !== provider
|
|
2315
|
+
|| (provider.client.realtime ?? provider.client.listener) === undefined)
|
|
2316
|
+
return;
|
|
2317
|
+
const generation = provider.realtimeGeneration;
|
|
2318
|
+
const source = provider.client.realtime ?? provider.client.listener;
|
|
2319
|
+
if (source === undefined)
|
|
2320
|
+
return;
|
|
2321
|
+
const logger = this.ctx.logger('awiki-realtime');
|
|
1827
2322
|
const startup = (async () => {
|
|
1828
2323
|
if (await this.isSignedOut())
|
|
1829
2324
|
return;
|
|
1830
|
-
|
|
2325
|
+
const identity = await provider.client.getIdentity();
|
|
2326
|
+
if (identity === null || !this.realtimeFenceMatches(provider, generation))
|
|
1831
2327
|
return;
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
provider.
|
|
2328
|
+
this.activeIdentityDid = identity.did;
|
|
2329
|
+
const supervisor = new IdentityRealtimeSupervisor(source, {
|
|
2330
|
+
onSynchronized: cause => this.onRealtimeSynchronized(provider, generation, cause),
|
|
2331
|
+
}, logger);
|
|
2332
|
+
provider.realtimeSupervisor = supervisor;
|
|
2333
|
+
provider.realtimeIdentityDid = identity.did;
|
|
2334
|
+
supervisor.start();
|
|
2335
|
+
this.ensureAgentConsumer(provider);
|
|
2336
|
+
})();
|
|
2337
|
+
const observed = startup
|
|
2338
|
+
.catch((error) => {
|
|
2339
|
+
logger.warn('AWiki realtime startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
2340
|
+
})
|
|
2341
|
+
.finally(() => {
|
|
2342
|
+
if (provider.realtimeStartup === observed)
|
|
2343
|
+
delete provider.realtimeStartup;
|
|
2344
|
+
});
|
|
2345
|
+
provider.realtimeStartup = observed;
|
|
2346
|
+
}
|
|
2347
|
+
realtimeFenceMatches(provider, generation) {
|
|
2348
|
+
return this.provider === provider && provider.realtimeGeneration === generation;
|
|
2349
|
+
}
|
|
2350
|
+
async onRealtimeSynchronized(provider, generation, cause) {
|
|
2351
|
+
if (!this.realtimeFenceMatches(provider, generation))
|
|
2352
|
+
return;
|
|
2353
|
+
if (cause === 'system_notification' || cause === 'stream_recovery') {
|
|
1837
2354
|
try {
|
|
1838
|
-
await
|
|
2355
|
+
provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
|
|
1839
2356
|
}
|
|
1840
|
-
catch
|
|
1841
|
-
|
|
1842
|
-
delete provider.listener;
|
|
1843
|
-
try {
|
|
1844
|
-
await listener.dispose();
|
|
1845
|
-
}
|
|
1846
|
-
catch {
|
|
1847
|
-
provider.listenerRecoveryBlocked = true;
|
|
1848
|
-
}
|
|
1849
|
-
if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
|
|
1850
|
-
this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
1851
|
-
}
|
|
1852
|
-
throw error;
|
|
2357
|
+
catch {
|
|
2358
|
+
this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
|
|
1853
2359
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2360
|
+
}
|
|
2361
|
+
if (!['session_start', 'connection_ready', 'reconnected', 'message', 'message_update'].includes(cause))
|
|
2362
|
+
return;
|
|
2363
|
+
this.ensureAgentConsumer(provider);
|
|
2364
|
+
await provider.agentConsumerStartup;
|
|
2365
|
+
if (!this.realtimeFenceMatches(provider, generation))
|
|
2366
|
+
return;
|
|
2367
|
+
await provider.agentConsumer?.reconcileOnce();
|
|
2368
|
+
}
|
|
2369
|
+
ensureAgentConsumer(provider) {
|
|
2370
|
+
const workspaceContext = this.workspaceContext;
|
|
2371
|
+
const source = provider.client.agentInbox ?? provider.client.listener;
|
|
2372
|
+
const identityDid = this.activeIdentityDid;
|
|
2373
|
+
if (!this.resolved.listenerEnabled
|
|
2374
|
+
|| workspaceContext === undefined
|
|
2375
|
+
|| source === undefined
|
|
2376
|
+
|| identityDid === undefined
|
|
2377
|
+
|| this.provider !== provider)
|
|
2378
|
+
return;
|
|
2379
|
+
if (provider.agentConsumer !== undefined && provider.agentConsumerIdentityDid === identityDid)
|
|
2380
|
+
return;
|
|
2381
|
+
if (provider.agentConsumerStartup !== undefined)
|
|
2382
|
+
return;
|
|
2383
|
+
const generation = ++provider.agentConsumerGeneration;
|
|
2384
|
+
const logger = this.ctx.logger('awiki-listener');
|
|
2385
|
+
const startup = (async () => {
|
|
2386
|
+
await this.detachAgentConsumer(provider);
|
|
2387
|
+
if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
|
|
1864
2388
|
return;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2389
|
+
const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
|
|
2390
|
+
const consumer = new AwikiAgentListener(source, agents, {
|
|
2391
|
+
...this.resolved.listener,
|
|
2392
|
+
identityScope: identityDid,
|
|
2393
|
+
}, logger);
|
|
2394
|
+
provider.agentConsumer = consumer;
|
|
2395
|
+
provider.agentConsumerIdentityDid = identityDid;
|
|
2396
|
+
await consumer.reconcileOnce();
|
|
1872
2397
|
})();
|
|
1873
2398
|
const observed = startup
|
|
1874
2399
|
.catch((error) => {
|
|
1875
|
-
logger.warn('AWiki
|
|
2400
|
+
logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
1876
2401
|
})
|
|
1877
2402
|
.finally(() => {
|
|
1878
|
-
if (provider.
|
|
1879
|
-
delete provider.
|
|
2403
|
+
if (provider.agentConsumerStartup === observed)
|
|
2404
|
+
delete provider.agentConsumerStartup;
|
|
1880
2405
|
});
|
|
1881
|
-
provider.
|
|
1882
|
-
return observed;
|
|
1883
|
-
}
|
|
1884
|
-
async stopListener(provider) {
|
|
1885
|
-
provider.listenerGeneration += 1;
|
|
1886
|
-
provider.listenerRestartAttempt = 0;
|
|
1887
|
-
delete provider.listenerStartedAt;
|
|
1888
|
-
const timer = provider.listenerRestartTimer;
|
|
1889
|
-
if (timer !== undefined) {
|
|
1890
|
-
delete provider.listenerRestartTimer;
|
|
1891
|
-
clearTimeout(timer);
|
|
1892
|
-
}
|
|
1893
|
-
await provider.listenerStartup;
|
|
1894
|
-
await provider.listenerCleanup;
|
|
1895
|
-
const listener = provider.listener;
|
|
1896
|
-
if (listener === undefined)
|
|
1897
|
-
return;
|
|
1898
|
-
delete provider.listener;
|
|
1899
|
-
try {
|
|
1900
|
-
await listener.dispose();
|
|
1901
|
-
}
|
|
1902
|
-
catch (error) {
|
|
1903
|
-
provider.listenerRecoveryBlocked = true;
|
|
1904
|
-
throw error;
|
|
1905
|
-
}
|
|
2406
|
+
provider.agentConsumerStartup = observed;
|
|
1906
2407
|
}
|
|
1907
|
-
|
|
2408
|
+
agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
|
|
1908
2409
|
return this.provider === provider
|
|
1909
2410
|
&& this.workspaceContext === workspaceContext
|
|
1910
|
-
&&
|
|
2411
|
+
&& this.activeIdentityDid === identityDid
|
|
2412
|
+
&& provider.agentConsumerGeneration === generation;
|
|
1911
2413
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
2414
|
+
async detachAgentConsumer(provider) {
|
|
2415
|
+
const consumer = provider.agentConsumer;
|
|
2416
|
+
if (consumer === undefined)
|
|
1914
2417
|
return;
|
|
1915
|
-
delete provider.
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
|
|
1919
|
-
provider.listenerRestartAttempt = 0;
|
|
1920
|
-
}
|
|
1921
|
-
const logger = this.ctx.logger('awiki-listener');
|
|
1922
|
-
logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
1923
|
-
let disposed = false;
|
|
1924
|
-
const cleanup = listener.dispose()
|
|
1925
|
-
.then(() => { disposed = true; }, (cleanupError) => {
|
|
1926
|
-
provider.listenerRecoveryBlocked = true;
|
|
1927
|
-
logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
|
|
1928
|
-
});
|
|
2418
|
+
delete provider.agentConsumer;
|
|
2419
|
+
delete provider.agentConsumerIdentityDid;
|
|
2420
|
+
const cleanup = consumer.dispose();
|
|
1929
2421
|
const observed = cleanup.finally(() => {
|
|
1930
|
-
if (provider.
|
|
1931
|
-
delete provider.
|
|
1932
|
-
if (disposed)
|
|
1933
|
-
this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
2422
|
+
if (provider.agentConsumerCleanup === observed)
|
|
2423
|
+
delete provider.agentConsumerCleanup;
|
|
1934
2424
|
});
|
|
1935
|
-
provider.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2425
|
+
provider.agentConsumerCleanup = observed;
|
|
2426
|
+
await observed;
|
|
2427
|
+
}
|
|
2428
|
+
async stopAgentConsumer(provider) {
|
|
2429
|
+
provider.agentConsumerGeneration += 1;
|
|
2430
|
+
await provider.agentConsumerStartup;
|
|
2431
|
+
await provider.agentConsumerCleanup;
|
|
2432
|
+
await this.detachAgentConsumer(provider);
|
|
2433
|
+
}
|
|
2434
|
+
async stopRealtimeSupervisor(provider) {
|
|
2435
|
+
provider.realtimeGeneration += 1;
|
|
2436
|
+
await provider.realtimeStartup;
|
|
2437
|
+
const supervisor = provider.realtimeSupervisor;
|
|
2438
|
+
if (supervisor === undefined)
|
|
1940
2439
|
return;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
void this.startListener(provider);
|
|
1951
|
-
}, delay);
|
|
1952
|
-
provider.listenerRestartTimer = timer;
|
|
2440
|
+
delete provider.realtimeSupervisor;
|
|
2441
|
+
delete provider.realtimeIdentityDid;
|
|
2442
|
+
await supervisor.dispose();
|
|
2443
|
+
}
|
|
2444
|
+
async stopProviderRuntime(provider) {
|
|
2445
|
+
await Promise.all([
|
|
2446
|
+
this.stopRealtimeSupervisor(provider),
|
|
2447
|
+
this.stopAgentConsumer(provider),
|
|
2448
|
+
]);
|
|
1953
2449
|
}
|
|
1954
2450
|
/** Clear one exact provider slot before joining its one shared disposal. */
|
|
1955
2451
|
disposeProvider(provider) {
|
|
1956
|
-
if (this.provider === provider)
|
|
2452
|
+
if (this.provider === provider) {
|
|
1957
2453
|
this.provider = undefined;
|
|
2454
|
+
this.pendingDeviceJoin = undefined;
|
|
2455
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
2456
|
+
this.invalidateSummaries();
|
|
2457
|
+
}
|
|
1958
2458
|
provider.disposal ??= (async () => {
|
|
1959
2459
|
try {
|
|
1960
|
-
await
|
|
2460
|
+
await provider.runtimeReplacement;
|
|
2461
|
+
await this.stopProviderRuntime(provider);
|
|
1961
2462
|
}
|
|
1962
2463
|
finally {
|
|
1963
2464
|
await provider.client.dispose();
|