@awiki/dsh-plugin 0.3.2 → 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 +36 -22
- package/README.zh.md +29 -19
- package/cordis.patch.yml +17 -0
- package/lib/client.js +1273 -216
- package/lib/client.js.map +1 -1
- package/lib/index.js +874 -266
- package/lib/provider.js +52 -16
- package/lib/{sdk-adapter-C0gdpg1u.mjs → sdk-adapter-BeJakxLP.mjs} +141 -3
- package/lib/typert.host.js +376 -44
- 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 +376 -44
- 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 -3
- 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 -3
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +38 -3
- 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 +50 -9
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +672 -157
- 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 +131 -2
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/types.d.ts +75 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +5 -3
package/lib/types/index.js
CHANGED
|
@@ -34,6 +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 { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
37
38
|
import { homedir } from 'node:os';
|
|
38
39
|
import { isAbsolute, join } from 'node:path';
|
|
39
40
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -53,6 +54,7 @@ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "
|
|
|
53
54
|
import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
|
|
54
55
|
import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
|
|
55
56
|
import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
|
|
57
|
+
import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
|
|
56
58
|
export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
|
|
57
59
|
export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
|
|
58
60
|
export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
|
|
@@ -85,6 +87,7 @@ export const Config = z.object({
|
|
|
85
87
|
attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
|
|
86
88
|
imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
|
|
87
89
|
pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
|
|
90
|
+
realtimeEnabled: z.boolean().default(true),
|
|
88
91
|
listenerEnabled: z.boolean().default(false),
|
|
89
92
|
listenerAllowedPeers: z.array(z.string()).default([]),
|
|
90
93
|
listenerWorkspacePath: z.string(),
|
|
@@ -115,10 +118,17 @@ const FAILURE_CODES = new Set([
|
|
|
115
118
|
'network',
|
|
116
119
|
'remote',
|
|
117
120
|
]);
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
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
|
+
]);
|
|
122
132
|
const FAILURE_MESSAGES = {
|
|
123
133
|
'not-registered': 'No AWiki identity is registered for this deployment.',
|
|
124
134
|
'signed-out': 'This installation is signed out of AWiki.',
|
|
@@ -148,6 +158,67 @@ class ProviderUnavailableError extends Error {
|
|
|
148
158
|
}
|
|
149
159
|
class SummaryProviderUnavailableError extends Error {
|
|
150
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
|
+
}
|
|
151
222
|
/** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
|
|
152
223
|
function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
|
|
153
224
|
let url;
|
|
@@ -275,6 +346,10 @@ function resolveConfig(config) {
|
|
|
275
346
|
throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
|
|
276
347
|
}
|
|
277
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
|
+
}
|
|
278
353
|
const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
|
|
279
354
|
if (!isAbsolute(listenerWorkspacePath)) {
|
|
280
355
|
throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
|
|
@@ -301,6 +376,7 @@ function resolveConfig(config) {
|
|
|
301
376
|
attachmentMaxBytes,
|
|
302
377
|
imageAttachmentCacheMaxBytes,
|
|
303
378
|
pollIntervalMs,
|
|
379
|
+
realtimeEnabled,
|
|
304
380
|
listenerEnabled,
|
|
305
381
|
listener: {
|
|
306
382
|
allowedPeers,
|
|
@@ -461,6 +537,7 @@ function normalizeRecoveryPrepareRequest(request) {
|
|
|
461
537
|
return { ...operation, phone, otp };
|
|
462
538
|
}
|
|
463
539
|
const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
540
|
+
const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
464
541
|
const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
|
|
465
542
|
/** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
|
|
466
543
|
async function readBoundedResponseText(response, maxBytes) {
|
|
@@ -690,6 +767,14 @@ let AwikiService = (() => {
|
|
|
690
767
|
let _inspectIdentityAccess_decorators;
|
|
691
768
|
let _sendRegistrationOtp_decorators;
|
|
692
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;
|
|
693
778
|
let _updateDisplayName_decorators;
|
|
694
779
|
let _getProfile_decorators;
|
|
695
780
|
let _updateProfile_decorators;
|
|
@@ -735,6 +820,14 @@ let AwikiService = (() => {
|
|
|
735
820
|
_inspectIdentityAccess_decorators = [Remote];
|
|
736
821
|
_sendRegistrationOtp_decorators = [Remote];
|
|
737
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];
|
|
738
831
|
_updateDisplayName_decorators = [Remote];
|
|
739
832
|
_getProfile_decorators = [Remote];
|
|
740
833
|
_updateProfile_decorators = [Remote];
|
|
@@ -777,6 +870,14 @@ let AwikiService = (() => {
|
|
|
777
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);
|
|
778
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);
|
|
779
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);
|
|
780
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);
|
|
781
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);
|
|
782
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);
|
|
@@ -827,6 +928,12 @@ let AwikiService = (() => {
|
|
|
827
928
|
sessionMutation = Promise.resolve();
|
|
828
929
|
sessionRevision = 0;
|
|
829
930
|
activeIdentityDid;
|
|
931
|
+
pendingDeviceJoin;
|
|
932
|
+
activeDeviceJoinSessionId;
|
|
933
|
+
requestRefs = new Map();
|
|
934
|
+
requestSessions = new Map();
|
|
935
|
+
deviceRefs = new Map();
|
|
936
|
+
deviceIds = new Map();
|
|
830
937
|
activeSummaryRequests = new Set();
|
|
831
938
|
summaryProvider;
|
|
832
939
|
recoveryReconciliationTarget;
|
|
@@ -871,14 +978,14 @@ let AwikiService = (() => {
|
|
|
871
978
|
this.workspaceContext = workspaceCtx;
|
|
872
979
|
const provider = this.provider;
|
|
873
980
|
if (provider !== undefined)
|
|
874
|
-
|
|
981
|
+
this.ensureAgentConsumer(provider);
|
|
875
982
|
workspaceCtx.effect(() => async () => {
|
|
876
983
|
if (this.workspaceContext !== workspaceCtx)
|
|
877
984
|
return;
|
|
878
985
|
this.workspaceContext = undefined;
|
|
879
986
|
const current = this.provider;
|
|
880
987
|
if (current !== undefined)
|
|
881
|
-
await this.
|
|
988
|
+
await this.stopAgentConsumer(current);
|
|
882
989
|
}, 'awiki: release Workspace listener composition');
|
|
883
990
|
});
|
|
884
991
|
registerAwikiTools(ctx, this);
|
|
@@ -901,6 +1008,8 @@ let AwikiService = (() => {
|
|
|
901
1008
|
registerClientFactory(factory) {
|
|
902
1009
|
if (this.provider !== undefined)
|
|
903
1010
|
throw new Error('awiki: a client provider is already registered');
|
|
1011
|
+
this.pendingDeviceJoin = undefined;
|
|
1012
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
904
1013
|
const client = factory({
|
|
905
1014
|
userServiceUrl: this.resolved.userServiceUrl,
|
|
906
1015
|
userServiceDomain: this.startupUserServiceDomain,
|
|
@@ -915,14 +1024,29 @@ let AwikiService = (() => {
|
|
|
915
1024
|
});
|
|
916
1025
|
const provider = {
|
|
917
1026
|
client,
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1027
|
+
realtimeGeneration: 0,
|
|
1028
|
+
agentConsumerGeneration: 0,
|
|
1029
|
+
localDeviceJoinRequestCountAfterSync: 0,
|
|
921
1030
|
};
|
|
922
1031
|
this.provider = provider;
|
|
923
|
-
|
|
1032
|
+
this.ensureRealtimeSupervisor(provider);
|
|
1033
|
+
this.ensureAgentConsumer(provider);
|
|
924
1034
|
return () => this.disposeProvider(provider);
|
|
925
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
|
+
}
|
|
926
1050
|
/** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
|
|
927
1051
|
registerRecoveryReconciliationTarget(target) {
|
|
928
1052
|
if (this.recoveryReconciliationTarget !== undefined) {
|
|
@@ -960,14 +1084,43 @@ let AwikiService = (() => {
|
|
|
960
1084
|
* Read settings needed by the browser presentation.
|
|
961
1085
|
* @returns Browser-safe polling configuration without SDK endpoints or state paths.
|
|
962
1086
|
*/
|
|
963
|
-
getConfig() {
|
|
964
|
-
|
|
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 {
|
|
965
1117
|
ok: true,
|
|
966
1118
|
value: {
|
|
967
1119
|
pollIntervalMs: this.resolved.pollIntervalMs,
|
|
968
1120
|
attachmentMaxBytes: this.resolved.attachmentMaxBytes,
|
|
1121
|
+
handleRecoveryPhoneEnabled,
|
|
969
1122
|
},
|
|
970
|
-
}
|
|
1123
|
+
};
|
|
971
1124
|
}
|
|
972
1125
|
/**
|
|
973
1126
|
* Read the deployment's identity status.
|
|
@@ -989,6 +1142,9 @@ let AwikiService = (() => {
|
|
|
989
1142
|
if (!identity.ok)
|
|
990
1143
|
return identity;
|
|
991
1144
|
this.activeIdentityDid = identity.value?.did;
|
|
1145
|
+
const provider = this.provider;
|
|
1146
|
+
if (identity.value !== null && provider !== undefined)
|
|
1147
|
+
this.ensureProviderRuntime(provider);
|
|
992
1148
|
return identity.value === null
|
|
993
1149
|
? { ok: true, value: { status: 'unregistered' } }
|
|
994
1150
|
: { ok: true, value: { status: 'active', identity: identity.value } };
|
|
@@ -1015,7 +1171,7 @@ let AwikiService = (() => {
|
|
|
1015
1171
|
this.publishSession(session);
|
|
1016
1172
|
const provider = this.provider;
|
|
1017
1173
|
if (provider !== undefined)
|
|
1018
|
-
await this.
|
|
1174
|
+
await this.stopProviderRuntime(provider);
|
|
1019
1175
|
return { ok: true, value: session };
|
|
1020
1176
|
}
|
|
1021
1177
|
catch {
|
|
@@ -1040,7 +1196,7 @@ let AwikiService = (() => {
|
|
|
1040
1196
|
this.publishSession(session);
|
|
1041
1197
|
const provider = this.provider;
|
|
1042
1198
|
if (provider !== undefined)
|
|
1043
|
-
|
|
1199
|
+
this.ensureProviderRuntime(provider);
|
|
1044
1200
|
return { ok: true, value: session };
|
|
1045
1201
|
}
|
|
1046
1202
|
catch {
|
|
@@ -1098,8 +1254,14 @@ let AwikiService = (() => {
|
|
|
1098
1254
|
return { ok: false, error: failure('remote') };
|
|
1099
1255
|
}
|
|
1100
1256
|
}
|
|
1101
|
-
sendRegistrationOtp(request) {
|
|
1102
|
-
|
|
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
|
+
});
|
|
1103
1265
|
}
|
|
1104
1266
|
/**
|
|
1105
1267
|
* Register and persist the deployment's only AWiki identity.
|
|
@@ -1107,10 +1269,177 @@ let AwikiService = (() => {
|
|
|
1107
1269
|
* @returns The new public identity or a closed failure.
|
|
1108
1270
|
*/
|
|
1109
1271
|
async registerIdentity(request) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
await this.
|
|
1113
|
-
|
|
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
|
+
});
|
|
1114
1443
|
}
|
|
1115
1444
|
/**
|
|
1116
1445
|
* Update the deployment identity's public WNS display name.
|
|
@@ -1136,10 +1465,16 @@ let AwikiService = (() => {
|
|
|
1136
1465
|
}
|
|
1137
1466
|
/** Start durable phone recovery for one existing full Handle. */
|
|
1138
1467
|
sendRecoveryOtp(request) {
|
|
1468
|
+
this.pendingDeviceJoin = undefined;
|
|
1139
1469
|
const normalized = normalizeRecoveryOtpRequest(request);
|
|
1140
1470
|
if (normalized === undefined)
|
|
1141
1471
|
return Promise.resolve({ ok: false, error: failure('invalid-request') });
|
|
1142
|
-
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 });
|
|
1143
1478
|
}
|
|
1144
1479
|
/** Verify a recovery OTP and freeze its exact intent before the remote commit. */
|
|
1145
1480
|
prepareRecovery(request) {
|
|
@@ -1586,11 +1921,11 @@ let AwikiService = (() => {
|
|
|
1586
1921
|
return this.mutateSession(async () => {
|
|
1587
1922
|
const provider = this.provider;
|
|
1588
1923
|
if (provider !== undefined)
|
|
1589
|
-
await this.
|
|
1924
|
+
await this.stopProviderRuntime(provider);
|
|
1590
1925
|
const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
|
|
1591
1926
|
if (!result.ok) {
|
|
1592
1927
|
if (provider !== undefined && this.provider === provider)
|
|
1593
|
-
|
|
1928
|
+
this.ensureProviderRuntime(provider);
|
|
1594
1929
|
return result;
|
|
1595
1930
|
}
|
|
1596
1931
|
try {
|
|
@@ -1600,10 +1935,10 @@ let AwikiService = (() => {
|
|
|
1600
1935
|
await this.sessionStore.signIn();
|
|
1601
1936
|
this.signedOut = false;
|
|
1602
1937
|
this.activeIdentityDid = undefined;
|
|
1938
|
+
this.pendingDeviceJoin = undefined;
|
|
1939
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
1603
1940
|
this.invalidateSummaries();
|
|
1604
1941
|
this.publishSession({ status: 'unregistered' });
|
|
1605
|
-
if (provider !== undefined && this.provider === provider)
|
|
1606
|
-
void this.startListener(provider);
|
|
1607
1942
|
return result;
|
|
1608
1943
|
}
|
|
1609
1944
|
catch {
|
|
@@ -1636,8 +1971,7 @@ let AwikiService = (() => {
|
|
|
1636
1971
|
if (!alreadyActive) {
|
|
1637
1972
|
const session = { status: 'active', identity };
|
|
1638
1973
|
this.publishSession(session);
|
|
1639
|
-
|
|
1640
|
-
await this.startListener(provider);
|
|
1974
|
+
this.ensureProviderRuntime(provider);
|
|
1641
1975
|
}
|
|
1642
1976
|
return reconciled;
|
|
1643
1977
|
}
|
|
@@ -1678,19 +2012,164 @@ let AwikiService = (() => {
|
|
|
1678
2012
|
return false;
|
|
1679
2013
|
}
|
|
1680
2014
|
}
|
|
1681
|
-
/**
|
|
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. */
|
|
1682
2157
|
async activateRegisteredIdentity(identity) {
|
|
2158
|
+
this.pendingDeviceJoin = undefined;
|
|
2159
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
1683
2160
|
this.activeIdentityDid = identity.did;
|
|
1684
2161
|
this.publishSession({ status: 'active', identity });
|
|
1685
2162
|
const provider = this.provider;
|
|
1686
|
-
if (provider !== undefined)
|
|
1687
|
-
|
|
1688
|
-
await this.startListener(provider);
|
|
1689
|
-
}
|
|
2163
|
+
if (provider !== undefined)
|
|
2164
|
+
this.ensureProviderRuntime(provider);
|
|
1690
2165
|
}
|
|
1691
2166
|
/** Invalidate cached session work and cancel every model request still owned by the old session. */
|
|
1692
2167
|
invalidateSummaries() {
|
|
1693
2168
|
this.sessionRevision += 1;
|
|
2169
|
+
this.requestRefs.clear();
|
|
2170
|
+
this.requestSessions.clear();
|
|
2171
|
+
this.deviceRefs.clear();
|
|
2172
|
+
this.deviceIds.clear();
|
|
1694
2173
|
for (const controller of this.activeSummaryRequests)
|
|
1695
2174
|
controller.abort();
|
|
1696
2175
|
this.activeSummaryRequests.clear();
|
|
@@ -1792,158 +2271,194 @@ let AwikiService = (() => {
|
|
|
1792
2271
|
this.sessionMutation = pending.then(() => undefined, () => undefined);
|
|
1793
2272
|
return pending;
|
|
1794
2273
|
}
|
|
1795
|
-
/** Start
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
return
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
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');
|
|
1813
2322
|
const startup = (async () => {
|
|
1814
2323
|
if (await this.isSignedOut())
|
|
1815
2324
|
return;
|
|
1816
|
-
|
|
2325
|
+
const identity = await provider.client.getIdentity();
|
|
2326
|
+
if (identity === null || !this.realtimeFenceMatches(provider, generation))
|
|
1817
2327
|
return;
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
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') {
|
|
1823
2354
|
try {
|
|
1824
|
-
await
|
|
2355
|
+
provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
|
|
1825
2356
|
}
|
|
1826
|
-
catch
|
|
1827
|
-
|
|
1828
|
-
delete provider.listener;
|
|
1829
|
-
try {
|
|
1830
|
-
await listener.dispose();
|
|
1831
|
-
}
|
|
1832
|
-
catch {
|
|
1833
|
-
provider.listenerRecoveryBlocked = true;
|
|
1834
|
-
}
|
|
1835
|
-
if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
|
|
1836
|
-
this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
1837
|
-
}
|
|
1838
|
-
throw error;
|
|
2357
|
+
catch {
|
|
2358
|
+
this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
|
|
1839
2359
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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))
|
|
1850
2388
|
return;
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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();
|
|
1858
2397
|
})();
|
|
1859
2398
|
const observed = startup
|
|
1860
2399
|
.catch((error) => {
|
|
1861
|
-
logger.warn('AWiki
|
|
2400
|
+
logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
1862
2401
|
})
|
|
1863
2402
|
.finally(() => {
|
|
1864
|
-
if (provider.
|
|
1865
|
-
delete provider.
|
|
2403
|
+
if (provider.agentConsumerStartup === observed)
|
|
2404
|
+
delete provider.agentConsumerStartup;
|
|
1866
2405
|
});
|
|
1867
|
-
provider.
|
|
1868
|
-
return observed;
|
|
1869
|
-
}
|
|
1870
|
-
async stopListener(provider) {
|
|
1871
|
-
provider.listenerGeneration += 1;
|
|
1872
|
-
provider.listenerRestartAttempt = 0;
|
|
1873
|
-
delete provider.listenerStartedAt;
|
|
1874
|
-
const timer = provider.listenerRestartTimer;
|
|
1875
|
-
if (timer !== undefined) {
|
|
1876
|
-
delete provider.listenerRestartTimer;
|
|
1877
|
-
clearTimeout(timer);
|
|
1878
|
-
}
|
|
1879
|
-
await provider.listenerStartup;
|
|
1880
|
-
await provider.listenerCleanup;
|
|
1881
|
-
const listener = provider.listener;
|
|
1882
|
-
if (listener === undefined)
|
|
1883
|
-
return;
|
|
1884
|
-
delete provider.listener;
|
|
1885
|
-
try {
|
|
1886
|
-
await listener.dispose();
|
|
1887
|
-
}
|
|
1888
|
-
catch (error) {
|
|
1889
|
-
provider.listenerRecoveryBlocked = true;
|
|
1890
|
-
throw error;
|
|
1891
|
-
}
|
|
2406
|
+
provider.agentConsumerStartup = observed;
|
|
1892
2407
|
}
|
|
1893
|
-
|
|
2408
|
+
agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
|
|
1894
2409
|
return this.provider === provider
|
|
1895
2410
|
&& this.workspaceContext === workspaceContext
|
|
1896
|
-
&&
|
|
2411
|
+
&& this.activeIdentityDid === identityDid
|
|
2412
|
+
&& provider.agentConsumerGeneration === generation;
|
|
1897
2413
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
2414
|
+
async detachAgentConsumer(provider) {
|
|
2415
|
+
const consumer = provider.agentConsumer;
|
|
2416
|
+
if (consumer === undefined)
|
|
1900
2417
|
return;
|
|
1901
|
-
delete provider.
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
|
|
1905
|
-
provider.listenerRestartAttempt = 0;
|
|
1906
|
-
}
|
|
1907
|
-
const logger = this.ctx.logger('awiki-listener');
|
|
1908
|
-
logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
|
|
1909
|
-
let disposed = false;
|
|
1910
|
-
const cleanup = listener.dispose()
|
|
1911
|
-
.then(() => { disposed = true; }, (cleanupError) => {
|
|
1912
|
-
provider.listenerRecoveryBlocked = true;
|
|
1913
|
-
logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
|
|
1914
|
-
});
|
|
2418
|
+
delete provider.agentConsumer;
|
|
2419
|
+
delete provider.agentConsumerIdentityDid;
|
|
2420
|
+
const cleanup = consumer.dispose();
|
|
1915
2421
|
const observed = cleanup.finally(() => {
|
|
1916
|
-
if (provider.
|
|
1917
|
-
delete provider.
|
|
1918
|
-
if (disposed)
|
|
1919
|
-
this.scheduleListenerRestart(provider, workspaceContext, generation);
|
|
2422
|
+
if (provider.agentConsumerCleanup === observed)
|
|
2423
|
+
delete provider.agentConsumerCleanup;
|
|
1920
2424
|
});
|
|
1921
|
-
provider.
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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)
|
|
1926
2439
|
return;
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
void this.startListener(provider);
|
|
1937
|
-
}, delay);
|
|
1938
|
-
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
|
+
]);
|
|
1939
2449
|
}
|
|
1940
2450
|
/** Clear one exact provider slot before joining its one shared disposal. */
|
|
1941
2451
|
disposeProvider(provider) {
|
|
1942
|
-
if (this.provider === provider)
|
|
2452
|
+
if (this.provider === provider) {
|
|
1943
2453
|
this.provider = undefined;
|
|
2454
|
+
this.pendingDeviceJoin = undefined;
|
|
2455
|
+
this.activeDeviceJoinSessionId = undefined;
|
|
2456
|
+
this.invalidateSummaries();
|
|
2457
|
+
}
|
|
1944
2458
|
provider.disposal ??= (async () => {
|
|
1945
2459
|
try {
|
|
1946
|
-
await
|
|
2460
|
+
await provider.runtimeReplacement;
|
|
2461
|
+
await this.stopProviderRuntime(provider);
|
|
1947
2462
|
}
|
|
1948
2463
|
finally {
|
|
1949
2464
|
await provider.client.dispose();
|