@mdbase-dev/connect 0.1.0-beta.31 → 0.1.0-beta.33
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 +112 -34
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-session.d.ts +39 -14
- package/dist/application-session.js +201 -70
- package/dist/async.js +11 -5
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.js +2 -1
- package/dist/collection-client.d.ts +36 -33
- package/dist/collection-client.js +549 -104
- package/dist/connect-authorization-helpers.d.ts +3 -0
- package/dist/connect-authorization-helpers.js +13 -0
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +102 -103
- package/dist/connection-file-transport.d.ts +2 -2
- package/dist/connection-file-transport.js +4 -4
- package/dist/connection-notifications.d.ts +10 -4
- package/dist/connection-notifications.js +41 -23
- package/dist/connection-transport.d.ts +19 -15
- package/dist/connection-transport.js +202 -202
- package/dist/connection-types.d.ts +5 -4
- package/dist/connection.d.ts +47 -46
- package/dist/connection.js +320 -162
- package/dist/crypto-entry.d.ts +7 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.js +4 -4
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-transfer-internals.d.ts +23 -0
- package/dist/file-transfer-internals.js +224 -0
- package/dist/files.d.ts +36 -14
- package/dist/files.js +92 -222
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +24 -19
- package/dist/index.js +10 -18
- package/dist/internal-types.d.ts +9 -1
- package/dist/local-file-transport.d.ts +2 -2
- package/dist/local-file-transport.js +13 -13
- package/dist/mdbase-connect.d.ts +6 -4
- package/dist/mdbase-connect.js +9 -8
- package/dist/notifications.d.ts +3 -2
- package/dist/operation-helpers.d.ts +3 -3
- package/dist/operation-helpers.js +17 -34
- package/dist/operation-types.d.ts +435 -45
- package/dist/outcomes.d.ts +15 -15
- package/dist/outcomes.js +13 -1
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/request-budget.d.ts +27 -0
- package/dist/request-budget.js +95 -0
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +19 -0
- package/dist/session.d.ts +5 -4
- package/dist/session.js +6 -5
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +60 -0
- package/dist/token-response.d.ts +11 -0
- package/dist/token-response.js +77 -0
- package/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- package/package.json +12 -2
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { isConnectProblem } from "@mdbase-dev/connect-protocol";
|
|
2
|
-
import { decryptRelayResponse, encryptRelayRequest, RelayCryptoError
|
|
1
|
+
import { OPERATION_TRANSPORT_PROTOCOL_VERSION, isConnectProblem, mutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { decryptRelayResponse, encryptRelayRequest, RelayCryptoError } from "./crypto.js";
|
|
3
3
|
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
4
4
|
import { ConnectionFileTransport } from "./connection-file-transport.js";
|
|
5
|
+
import { authorityProofHeaders } from "./authority-proof.js";
|
|
6
|
+
import { PendingMutationStore } from "./pending-mutation-store.js";
|
|
5
7
|
import { directFallbackStatus, isMutation, localNetworkPermission, loopbackRequest, operationFingerprint, operationTransportError, sameAuthorization, throwIfCancelled, unknownMutationOutcome } from "./operation-helpers.js";
|
|
6
|
-
import { apiError,
|
|
8
|
+
import { apiError, decodeJsonResponse, oauthErrorCode, parseStored, } from "./runtime-utils.js";
|
|
9
|
+
import { readStoredToken } from "./stored-token.js";
|
|
10
|
+
import { withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
|
|
7
11
|
export class ConnectionTransport {
|
|
8
12
|
serverUrl;
|
|
9
13
|
storage;
|
|
@@ -13,6 +17,8 @@ export class ConnectionTransport {
|
|
|
13
17
|
collectionId;
|
|
14
18
|
internals;
|
|
15
19
|
onChange;
|
|
20
|
+
timeouts;
|
|
21
|
+
pendingMutationStore;
|
|
16
22
|
files;
|
|
17
23
|
refreshPromise = null;
|
|
18
24
|
directStatus;
|
|
@@ -28,12 +34,14 @@ export class ConnectionTransport {
|
|
|
28
34
|
this.collectionId = options.collectionId;
|
|
29
35
|
this.internals = options.internals;
|
|
30
36
|
this.onChange = options.onChange;
|
|
37
|
+
this.timeouts = options.timeouts;
|
|
38
|
+
this.pendingMutationStore = new PendingMutationStore(this.storage, this.internals.pendingMutationKey(this.collectionId));
|
|
31
39
|
this.files = new ConnectionFileTransport({
|
|
32
40
|
keyStore: this.keyStore,
|
|
33
41
|
serverUrl: this.serverUrl,
|
|
34
42
|
loopbackUrl: this.loopbackUrl,
|
|
35
|
-
authorizedToken: () => this.authorizedToken(),
|
|
36
|
-
refreshAuthorization: () => this.refreshAuthorization(),
|
|
43
|
+
authorizedToken: (signal) => this.authorizedToken({ signal, timeoutMs: null }),
|
|
44
|
+
refreshAuthorization: (signal) => this.refreshAuthorization(signal),
|
|
37
45
|
shouldAttemptDirect: (token) => this.shouldAttemptDirect(token),
|
|
38
46
|
onDirectAvailable: () => {
|
|
39
47
|
this.markDirectAvailable();
|
|
@@ -41,7 +49,7 @@ export class ConnectionTransport {
|
|
|
41
49
|
},
|
|
42
50
|
onDirectUnavailable: () => this.markDirectUnavailable(),
|
|
43
51
|
onRelayAvailable: () => this.setRoute("relay"),
|
|
44
|
-
authorityProofHeaders: (token, method, url, body, credential) => this.
|
|
52
|
+
authorityProofHeaders: (token, method, url, body, credential) => authorityProofHeaders(this.keyStore, token, method, url, body, credential)
|
|
45
53
|
});
|
|
46
54
|
this.directStatus = this.directAccessMode === "disabled"
|
|
47
55
|
? "disabled"
|
|
@@ -56,7 +64,10 @@ export class ConnectionTransport {
|
|
|
56
64
|
notifyStorageChanged() {
|
|
57
65
|
this.emitConnection();
|
|
58
66
|
}
|
|
59
|
-
async checkDirectAccess() {
|
|
67
|
+
async checkDirectAccess(options = {}) {
|
|
68
|
+
return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.checkDirectAccessWithinBudget(budget.signal));
|
|
69
|
+
}
|
|
70
|
+
async checkDirectAccessWithinBudget(signal) {
|
|
60
71
|
const token = this.currentToken();
|
|
61
72
|
if (!this.directEligible(token))
|
|
62
73
|
return this.setDirectStatus("disabled");
|
|
@@ -64,22 +75,25 @@ export class ConnectionTransport {
|
|
|
64
75
|
if (permission === "denied")
|
|
65
76
|
return this.setDirectStatus("denied");
|
|
66
77
|
if ((permission === "prompt" || permission === null)
|
|
67
|
-
&& this.storage.getItem(this.directPreferenceKey()) !== "enabled") {
|
|
78
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "enabled") {
|
|
68
79
|
return this.setDirectStatus("permission_required");
|
|
69
80
|
}
|
|
70
|
-
return this.probeDirectAccess();
|
|
81
|
+
return this.probeDirectAccess(signal);
|
|
71
82
|
}
|
|
72
83
|
/** Call from a user gesture to request browser permission for direct local access. */
|
|
73
|
-
async requestDirectAccess() {
|
|
84
|
+
async requestDirectAccess(options = {}) {
|
|
85
|
+
return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.requestDirectAccessWithinBudget(budget.signal));
|
|
86
|
+
}
|
|
87
|
+
async requestDirectAccessWithinBudget(signal) {
|
|
74
88
|
const token = this.currentToken();
|
|
75
89
|
if (!this.directCapable(token))
|
|
76
90
|
return this.setDirectStatus("disabled");
|
|
77
|
-
this.storage.setItem(this.directPreferenceKey(), "enabled");
|
|
91
|
+
this.storage.setItem(this.internals.directPreferenceKey(), "enabled");
|
|
78
92
|
this.directRetryAt = 0;
|
|
79
|
-
return this.probeDirectAccess();
|
|
93
|
+
return this.probeDirectAccess(signal);
|
|
80
94
|
}
|
|
81
95
|
disableDirectAccess() {
|
|
82
|
-
this.storage.setItem(this.directPreferenceKey(), "disabled");
|
|
96
|
+
this.storage.setItem(this.internals.directPreferenceKey(), "disabled");
|
|
83
97
|
this.setDirectStatus("disabled");
|
|
84
98
|
this.setRoute("relay");
|
|
85
99
|
}
|
|
@@ -88,24 +102,39 @@ export class ConnectionTransport {
|
|
|
88
102
|
* authority is remote, directly connected, or relay connected.
|
|
89
103
|
* Authority credentials and routing stay private inside the SDK.
|
|
90
104
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
pendingMutations() {
|
|
106
|
+
return this.storedPendingMutations().map((pending) => ({
|
|
107
|
+
requestId: pending.requestId,
|
|
108
|
+
operation: this.pendingMutationStore.identifier(pending),
|
|
109
|
+
fingerprint: pending.inputFingerprint,
|
|
110
|
+
status: "outcome_unknown",
|
|
111
|
+
createdAt: new Date(pending.createdAt).toISOString()
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
pendingMutation(requestId) {
|
|
115
|
+
const pending = this.storedPendingMutation(requestId);
|
|
116
|
+
return pending ? {
|
|
117
|
+
requestId: pending.requestId,
|
|
118
|
+
operation: this.pendingMutationStore.identifier(pending),
|
|
119
|
+
fingerprint: pending.inputFingerprint,
|
|
120
|
+
status: "outcome_unknown",
|
|
121
|
+
createdAt: new Date(pending.createdAt).toISOString()
|
|
122
|
+
} : null;
|
|
123
|
+
}
|
|
124
|
+
async recoverPendingMutation(requestId, options) {
|
|
125
|
+
const pending = this.storedPendingMutation(requestId);
|
|
103
126
|
if (!pending) {
|
|
104
127
|
throw connectError("no_pending_mutation", "There is no interrupted mutation to resume.");
|
|
105
128
|
}
|
|
106
|
-
return this.
|
|
129
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.performOperationWithinBudget(pending.operation, pending.request?.input ?? {}, { ...options, signal: budget.signal }, pending));
|
|
107
130
|
}
|
|
108
131
|
async performOperation(operation, input, options = {}) {
|
|
132
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.performOperationWithinBudget(operation, input, {
|
|
133
|
+
...options,
|
|
134
|
+
signal: budget.signal
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
async performOperationWithinBudget(operation, input, options, storedPending) {
|
|
109
138
|
throwIfCancelled(options.signal);
|
|
110
139
|
let token = this.currentToken();
|
|
111
140
|
if (!token)
|
|
@@ -121,45 +150,51 @@ export class ConnectionTransport {
|
|
|
121
150
|
}
|
|
122
151
|
let tryDirect = await this.shouldAttemptDirect(token);
|
|
123
152
|
if (!tryDirect) {
|
|
124
|
-
token = await this.authorizedToken();
|
|
153
|
+
token = await this.authorizedToken({ signal: options.signal, timeoutMs: null });
|
|
125
154
|
if (!token)
|
|
126
155
|
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
127
156
|
}
|
|
157
|
+
const pendingRequestId = storedPending?.requestId
|
|
158
|
+
?? (isMutation(operation, input) ? crypto.randomUUID() : undefined);
|
|
128
159
|
let attempt;
|
|
129
160
|
try {
|
|
130
|
-
attempt = await this.sendOperation(token, operation, input, tryDirect, options);
|
|
161
|
+
attempt = await this.sendOperation(token, operation, input, tryDirect, options, pendingRequestId);
|
|
131
162
|
}
|
|
132
163
|
catch (error) {
|
|
133
|
-
throw operationTransportError(error, options.signal,
|
|
164
|
+
throw operationTransportError(error, options.signal, pendingRequestId !== undefined && this.storedPendingMutation(pendingRequestId) !== null
|
|
165
|
+
? pendingRequestId
|
|
166
|
+
: undefined, token.authority ? "hosted_provider_unavailable" : "relay_unavailable");
|
|
134
167
|
}
|
|
135
168
|
let response = attempt.response;
|
|
136
169
|
const staleBinding = response.status === 409
|
|
137
|
-
&& (await response.clone().
|
|
170
|
+
&& (await decodeJsonResponse(response.clone(), "invalid_operation_response", "The collection authority returned an invalid operation response.").catch(() => null))?.error?.code === "encryption_binding_stale";
|
|
138
171
|
if ((response.status === 401 || staleBinding) && token.refreshToken) {
|
|
139
172
|
if (attempt.pendingMutation
|
|
140
173
|
&& (attempt.directDeliveryUncertain
|
|
141
174
|
|| (attempt.encryptedRequest && attempt.resumingMutation))) {
|
|
142
|
-
throw connectError("operation_outcome_unknown", "The direct operation may have completed, but its encrypted grant changed before the response could be recovered. Refresh before making another change.", { operationOutcome: "unknown" });
|
|
175
|
+
throw connectError("operation_outcome_unknown", "The direct operation may have completed, but its encrypted grant changed before the response could be recovered. Refresh before making another change.", { operationOutcome: "unknown", details: { request_id: attempt.requestId } });
|
|
143
176
|
}
|
|
144
177
|
if (attempt.pendingMutation)
|
|
145
|
-
this.clearPendingMutation();
|
|
146
|
-
token = await this.refreshAuthorization();
|
|
178
|
+
this.clearPendingMutation(attempt.requestId);
|
|
179
|
+
token = await this.refreshAuthorization(options.signal);
|
|
147
180
|
tryDirect = await this.shouldAttemptDirect(token);
|
|
148
181
|
try {
|
|
149
|
-
attempt = await this.sendOperation(token, operation, input, tryDirect, options);
|
|
182
|
+
attempt = await this.sendOperation(token, operation, input, tryDirect, options, pendingRequestId);
|
|
150
183
|
}
|
|
151
184
|
catch (error) {
|
|
152
|
-
throw operationTransportError(error, options.signal,
|
|
185
|
+
throw operationTransportError(error, options.signal, pendingRequestId !== undefined && this.storedPendingMutation(pendingRequestId) !== null
|
|
186
|
+
? pendingRequestId
|
|
187
|
+
: undefined, token.authority ? "hosted_provider_unavailable" : "relay_unavailable");
|
|
153
188
|
}
|
|
154
189
|
response = attempt.response;
|
|
155
190
|
}
|
|
156
191
|
let body;
|
|
157
192
|
try {
|
|
158
|
-
body = await response.
|
|
193
|
+
body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned a response that is not valid JSON.");
|
|
159
194
|
}
|
|
160
195
|
catch (cause) {
|
|
161
196
|
if (attempt.pendingMutation)
|
|
162
|
-
throw unknownMutationOutcome(cause);
|
|
197
|
+
throw unknownMutationOutcome(attempt.requestId, cause);
|
|
163
198
|
throw connectError("invalid_operation_response", "The collection authority returned a response that is not valid JSON.", { cause });
|
|
164
199
|
}
|
|
165
200
|
if (!response.ok) {
|
|
@@ -167,10 +202,10 @@ export class ConnectionTransport {
|
|
|
167
202
|
if (attempt.pendingMutation
|
|
168
203
|
&& (attempt.directDeliveryUncertain
|
|
169
204
|
|| (attempt.encryptedRequest && attempt.resumingMutation))) {
|
|
170
|
-
throw unknownMutationOutcome(error);
|
|
205
|
+
throw unknownMutationOutcome(attempt.requestId, error);
|
|
171
206
|
}
|
|
172
207
|
if (attempt.pendingMutation && !attempt.directDeliveryUncertain) {
|
|
173
|
-
this.clearPendingMutation();
|
|
208
|
+
this.clearPendingMutation(attempt.requestId);
|
|
174
209
|
}
|
|
175
210
|
if (error.code === "direct_operation_rejected" && error.status === 403) {
|
|
176
211
|
this.invalidateRejectedAuthorization(token);
|
|
@@ -179,15 +214,24 @@ export class ConnectionTransport {
|
|
|
179
214
|
}
|
|
180
215
|
if (attempt.encryptedRequest) {
|
|
181
216
|
const encryptedResponse = body?.envelope;
|
|
182
|
-
|
|
217
|
+
const pendingCrypto = attempt.pendingMutationRecord;
|
|
218
|
+
const responseEncryption = pendingCrypto?.encryption ?? token.encryption;
|
|
219
|
+
const responseGrantId = pendingCrypto?.grantId ?? token.grantId;
|
|
220
|
+
const responseApplicationId = pendingCrypto?.applicationId ?? token.clientId;
|
|
221
|
+
const responseKeyHandle = pendingCrypto?.keyHandle ?? token.keyHandle;
|
|
222
|
+
if (!encryptedResponse || !responseEncryption || !responseGrantId || !responseKeyHandle) {
|
|
183
223
|
if (attempt.pendingMutation)
|
|
184
|
-
throw unknownMutationOutcome(new Error("Encrypted operation response was missing its envelope."));
|
|
224
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Encrypted operation response was missing its envelope."));
|
|
185
225
|
throw connectError("invalid_encrypted_response", "The relay did not return an encrypted connector response.");
|
|
186
226
|
}
|
|
187
227
|
try {
|
|
188
|
-
const decrypted = await decryptRelayResponse(this.keyStore,
|
|
228
|
+
const decrypted = await decryptRelayResponse(this.keyStore, responseKeyHandle, {
|
|
229
|
+
grantId: responseGrantId,
|
|
230
|
+
applicationId: responseApplicationId,
|
|
231
|
+
encryption: responseEncryption
|
|
232
|
+
}, attempt.encryptedRequest, encryptedResponse);
|
|
189
233
|
if (attempt.pendingMutation)
|
|
190
|
-
this.clearPendingMutation();
|
|
234
|
+
this.clearPendingMutation(attempt.requestId);
|
|
191
235
|
if (!decrypted.ok)
|
|
192
236
|
throw serverConnectError(decrypted.problem.code === "unknown"
|
|
193
237
|
? decrypted.problem.server_code
|
|
@@ -203,65 +247,79 @@ export class ConnectionTransport {
|
|
|
203
247
|
throw error;
|
|
204
248
|
if (error instanceof RelayCryptoError) {
|
|
205
249
|
if (attempt.pendingMutation)
|
|
206
|
-
throw unknownMutationOutcome(error);
|
|
250
|
+
throw unknownMutationOutcome(attempt.requestId, error);
|
|
207
251
|
throw serverConnectError(error.code, error.message);
|
|
208
252
|
}
|
|
209
253
|
throw error;
|
|
210
254
|
}
|
|
211
255
|
}
|
|
212
|
-
if (body?.protocol_version !==
|
|
256
|
+
if (body?.protocol_version !== OPERATION_TRANSPORT_PROTOCOL_VERSION
|
|
257
|
+
|| body?.request_id !== attempt.requestId) {
|
|
213
258
|
if (attempt.pendingMutation)
|
|
214
|
-
throw unknownMutationOutcome(new Error("Operation response protocol or request ID did not match."));
|
|
259
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Operation response protocol or request ID did not match."));
|
|
215
260
|
throw connectError("invalid_operation_response", "The collection authority returned a response for a different protocol request.");
|
|
216
261
|
}
|
|
217
262
|
if (body.ok === false) {
|
|
218
263
|
if (!isConnectProblem(body.problem)) {
|
|
219
264
|
if (attempt.pendingMutation)
|
|
220
|
-
throw unknownMutationOutcome(new Error("Operation rejection did not contain a canonical problem."));
|
|
265
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Operation rejection did not contain a canonical problem."));
|
|
221
266
|
throw connectError("invalid_operation_response", "The collection authority returned an invalid operation problem.");
|
|
222
267
|
}
|
|
223
268
|
if (attempt.pendingMutation)
|
|
224
|
-
this.clearPendingMutation();
|
|
269
|
+
this.clearPendingMutation(attempt.requestId);
|
|
225
270
|
throw new MdbaseConnectError(body.problem);
|
|
226
271
|
}
|
|
227
272
|
if (body.ok !== true || !("result" in body)) {
|
|
228
273
|
if (attempt.pendingMutation)
|
|
229
|
-
throw unknownMutationOutcome(new Error("Successful operation response did not contain a result."));
|
|
274
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Successful operation response did not contain a result."));
|
|
230
275
|
throw connectError("invalid_operation_response", "The collection authority returned an incomplete operation response.");
|
|
231
276
|
}
|
|
232
277
|
if (attempt.pendingMutation)
|
|
233
|
-
this.clearPendingMutation();
|
|
278
|
+
this.clearPendingMutation(attempt.requestId);
|
|
234
279
|
return body.result;
|
|
235
280
|
}
|
|
236
|
-
async performAuthoritySyncRequest(collectionId, replicaId, method, path, input) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|| token.authority.replicaId !== replicaId) {
|
|
241
|
-
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
242
|
-
}
|
|
243
|
-
let response = await this.sendAuthoritySyncRequest(token, method, path, input);
|
|
244
|
-
if (response.status === 401 && token.refreshToken) {
|
|
245
|
-
token = await this.refreshAuthorization();
|
|
246
|
-
if (!token.authority
|
|
281
|
+
async performAuthoritySyncRequest(collectionId, replicaId, method, path, input, options = {}) {
|
|
282
|
+
return withCooperativeRequestBudget(options, this.timeouts.syncMs, async (budget) => {
|
|
283
|
+
let token = await this.authorizedToken({ signal: budget.signal, timeoutMs: null });
|
|
284
|
+
if (!token?.authority
|
|
247
285
|
|| token.collectionId !== collectionId
|
|
248
286
|
|| token.authority.replicaId !== replicaId) {
|
|
249
287
|
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
250
288
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
try {
|
|
290
|
+
let response = await this.sendAuthoritySyncRequest(token, method, path, input, budget.signal);
|
|
291
|
+
if (response.status === 401 && token.refreshToken) {
|
|
292
|
+
token = await this.refreshAuthorization(budget.signal);
|
|
293
|
+
if (!token.authority
|
|
294
|
+
|| token.collectionId !== collectionId
|
|
295
|
+
|| token.authority.replicaId !== replicaId) {
|
|
296
|
+
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
297
|
+
}
|
|
298
|
+
response = await this.sendAuthoritySyncRequest(token, method, path, input, budget.signal);
|
|
299
|
+
}
|
|
300
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid synchronization response.");
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw apiError(body, "sync_failed", "Collection synchronization failed.", response.status);
|
|
303
|
+
}
|
|
304
|
+
return body;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const mutationRequestId = method === "POST" && path === "mutations"
|
|
308
|
+
&& input && typeof input === "object"
|
|
309
|
+
&& typeof input.mutation_id === "string"
|
|
310
|
+
? input.mutation_id
|
|
311
|
+
: undefined;
|
|
312
|
+
throw operationTransportError(error, budget.signal, mutationRequestId, "hosted_provider_unavailable");
|
|
313
|
+
}
|
|
314
|
+
});
|
|
257
315
|
}
|
|
258
|
-
async sendAuthoritySyncRequest(token, method, path, input) {
|
|
316
|
+
async sendAuthoritySyncRequest(token, method, path, input, signal) {
|
|
259
317
|
if (!token.authority) {
|
|
260
318
|
throw connectError("not_remote_authority", "This authorization has no remote authority endpoint.");
|
|
261
319
|
}
|
|
262
320
|
const url = `${token.authority.syncUrl}/${path}`;
|
|
263
321
|
const body = input === undefined ? undefined : JSON.stringify(input);
|
|
264
|
-
const proof = await this.
|
|
322
|
+
const proof = await authorityProofHeaders(this.keyStore, token, method, url, body, token.authority.accessToken);
|
|
265
323
|
return fetch(url, {
|
|
266
324
|
method,
|
|
267
325
|
headers: {
|
|
@@ -269,24 +327,24 @@ export class ConnectionTransport {
|
|
|
269
327
|
...(input === undefined ? {} : { "content-type": "application/json" }),
|
|
270
328
|
...proof
|
|
271
329
|
},
|
|
272
|
-
...(body === undefined ? {} : { body })
|
|
330
|
+
...(body === undefined ? {} : { body }),
|
|
331
|
+
signal
|
|
273
332
|
});
|
|
274
333
|
}
|
|
275
|
-
async sendOperation(token, operation, input, tryDirect, options = {}) {
|
|
334
|
+
async sendOperation(token, operation, input, tryDirect, options = {}, pendingRequestId) {
|
|
276
335
|
let body = input ?? {};
|
|
277
336
|
let encryptedRequest;
|
|
278
337
|
let pendingMutation = false;
|
|
279
338
|
let resumingMutation = false;
|
|
280
|
-
let requestId = crypto.randomUUID();
|
|
339
|
+
let requestId = pendingRequestId ?? crypto.randomUUID();
|
|
281
340
|
let pending = null;
|
|
282
341
|
let inputFingerprint;
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
342
|
+
if (pendingRequestId !== undefined) {
|
|
343
|
+
pending = this.storedPendingMutation(pendingRequestId);
|
|
344
|
+
inputFingerprint = pending?.inputFingerprint
|
|
345
|
+
?? await operationFingerprint(operation, input);
|
|
286
346
|
if (pending) {
|
|
287
347
|
if (pending.collectionId !== token.collectionId
|
|
288
|
-
|| pending.grantId !== token.grantId
|
|
289
|
-
|| pending.keyId !== token.encryption?.key_id
|
|
290
348
|
|| pending.operation !== operation
|
|
291
349
|
|| pending.inputFingerprint !== inputFingerprint) {
|
|
292
350
|
throw connectError("pending_mutation_unresolved", "A previous write still has an unknown outcome. Retry that exact write before making another change.");
|
|
@@ -296,6 +354,13 @@ export class ConnectionTransport {
|
|
|
296
354
|
}
|
|
297
355
|
pendingMutation = true;
|
|
298
356
|
}
|
|
357
|
+
const mutation = pending?.mutation ?? mutationOperationIdentifier(operation, input);
|
|
358
|
+
if (pending?.envelope && (token.authority || !token.encryption)) {
|
|
359
|
+
throw connectError("pending_mutation_unresolved", "The pending write requires its encrypted relay authority. Reconnect that authority before recovery.");
|
|
360
|
+
}
|
|
361
|
+
if (pending?.request && token.encryption && !token.authority) {
|
|
362
|
+
throw connectError("pending_mutation_unresolved", "The pending write belongs to a different authority transport. Reconnect that authority before recovery.");
|
|
363
|
+
}
|
|
299
364
|
if (token.encryption && !token.authority) {
|
|
300
365
|
if (!token.grantId || !token.keyHandle) {
|
|
301
366
|
throw connectError("missing_grant_key", "Reconnect this application to restore encrypted access.");
|
|
@@ -310,16 +375,20 @@ export class ConnectionTransport {
|
|
|
310
375
|
}
|
|
311
376
|
else {
|
|
312
377
|
encryptedRequest = await encryptRelayRequest(this.keyStore, token.keyHandle, { grantId: token.grantId, applicationId: token.clientId, encryption: token.encryption }, operation, input, requestId);
|
|
313
|
-
this.
|
|
378
|
+
this.pendingMutationStore.store({
|
|
314
379
|
collectionId: token.collectionId,
|
|
315
380
|
...(token.grantId ? { grantId: token.grantId } : {}),
|
|
316
381
|
keyId: token.encryption.key_id,
|
|
382
|
+
keyHandle: token.keyHandle,
|
|
383
|
+
applicationId: token.clientId,
|
|
384
|
+
encryption: token.encryption,
|
|
317
385
|
operation,
|
|
386
|
+
mutation: mutation,
|
|
318
387
|
inputFingerprint: inputFingerprint,
|
|
319
388
|
requestId,
|
|
320
389
|
envelope: encryptedRequest,
|
|
321
390
|
createdAt: Date.now()
|
|
322
|
-
})
|
|
391
|
+
});
|
|
323
392
|
}
|
|
324
393
|
}
|
|
325
394
|
else {
|
|
@@ -334,21 +403,23 @@ export class ConnectionTransport {
|
|
|
334
403
|
body = encryptedRequest;
|
|
335
404
|
}
|
|
336
405
|
else {
|
|
337
|
-
const request = {
|
|
338
|
-
protocol_version:
|
|
406
|
+
const request = pending?.request ?? {
|
|
407
|
+
protocol_version: OPERATION_TRANSPORT_PROTOCOL_VERSION,
|
|
339
408
|
request_id: requestId,
|
|
340
409
|
input: body
|
|
341
410
|
};
|
|
342
411
|
body = request;
|
|
343
412
|
if (pendingMutation && !pending) {
|
|
344
|
-
this.
|
|
413
|
+
this.pendingMutationStore.store({
|
|
345
414
|
collectionId: token.collectionId,
|
|
346
415
|
...(token.grantId ? { grantId: token.grantId } : {}),
|
|
347
416
|
operation,
|
|
417
|
+
mutation: mutation,
|
|
348
418
|
inputFingerprint: inputFingerprint,
|
|
349
419
|
requestId,
|
|
420
|
+
request,
|
|
350
421
|
createdAt: Date.now()
|
|
351
|
-
})
|
|
422
|
+
});
|
|
352
423
|
}
|
|
353
424
|
}
|
|
354
425
|
if (tryDirect && encryptedRequest && !token.authority) {
|
|
@@ -370,6 +441,7 @@ export class ConnectionTransport {
|
|
|
370
441
|
requestId,
|
|
371
442
|
encryptedRequest,
|
|
372
443
|
pendingMutation,
|
|
444
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
373
445
|
resumingMutation
|
|
374
446
|
};
|
|
375
447
|
}
|
|
@@ -389,11 +461,11 @@ export class ConnectionTransport {
|
|
|
389
461
|
try {
|
|
390
462
|
relayToken = token.expiresAt > Date.now() + 30_000
|
|
391
463
|
? token
|
|
392
|
-
: await this.refreshAuthorization();
|
|
464
|
+
: await this.refreshAuthorization(options.signal);
|
|
393
465
|
}
|
|
394
466
|
catch (error) {
|
|
395
467
|
if (pendingMutation && (directDeliveryUncertain || resumingMutation))
|
|
396
|
-
throw unknownMutationOutcome(error);
|
|
468
|
+
throw unknownMutationOutcome(requestId, error);
|
|
397
469
|
throw error;
|
|
398
470
|
}
|
|
399
471
|
let response;
|
|
@@ -410,7 +482,7 @@ export class ConnectionTransport {
|
|
|
410
482
|
}
|
|
411
483
|
catch (error) {
|
|
412
484
|
if (pendingMutation && (directDeliveryUncertain || resumingMutation))
|
|
413
|
-
throw unknownMutationOutcome(error);
|
|
485
|
+
throw unknownMutationOutcome(requestId, error);
|
|
414
486
|
throw error;
|
|
415
487
|
}
|
|
416
488
|
if (response.ok)
|
|
@@ -421,6 +493,7 @@ export class ConnectionTransport {
|
|
|
421
493
|
encryptedRequest,
|
|
422
494
|
directDeliveryUncertain,
|
|
423
495
|
pendingMutation,
|
|
496
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
424
497
|
resumingMutation
|
|
425
498
|
};
|
|
426
499
|
}
|
|
@@ -429,7 +502,7 @@ export class ConnectionTransport {
|
|
|
429
502
|
: `${this.serverUrl}/v1/authorities/${encodeURIComponent(token.collectionId)}/operations/${operation}`;
|
|
430
503
|
const operationBody = JSON.stringify(body);
|
|
431
504
|
const proof = token.authority
|
|
432
|
-
? await this.
|
|
505
|
+
? await authorityProofHeaders(this.keyStore, token, "POST", operationUrl, operationBody, token.authority.accessToken)
|
|
433
506
|
: {};
|
|
434
507
|
const response = await fetch(operationUrl, {
|
|
435
508
|
method: "POST",
|
|
@@ -443,7 +516,14 @@ export class ConnectionTransport {
|
|
|
443
516
|
});
|
|
444
517
|
if (response.ok)
|
|
445
518
|
this.setRoute(token.authority ? "remote" : "relay");
|
|
446
|
-
return {
|
|
519
|
+
return {
|
|
520
|
+
response,
|
|
521
|
+
requestId,
|
|
522
|
+
encryptedRequest,
|
|
523
|
+
pendingMutation,
|
|
524
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
525
|
+
resumingMutation
|
|
526
|
+
};
|
|
447
527
|
}
|
|
448
528
|
directCapable(token) {
|
|
449
529
|
if (!token || token.authority || !token.encryption || !token.grantId || !token.keyHandle)
|
|
@@ -463,7 +543,7 @@ export class ConnectionTransport {
|
|
|
463
543
|
directEligible(token) {
|
|
464
544
|
return token !== null
|
|
465
545
|
&& this.directCapable(token)
|
|
466
|
-
&& this.storage.getItem(this.directPreferenceKey()) !== "disabled";
|
|
546
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "disabled";
|
|
467
547
|
}
|
|
468
548
|
async shouldAttemptDirect(token) {
|
|
469
549
|
if (!this.directEligible(token))
|
|
@@ -478,25 +558,26 @@ export class ConnectionTransport {
|
|
|
478
558
|
return false;
|
|
479
559
|
}
|
|
480
560
|
if ((permission === "prompt" || permission === null)
|
|
481
|
-
&& this.storage.getItem(this.directPreferenceKey()) !== "enabled") {
|
|
561
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "enabled") {
|
|
482
562
|
this.setDirectStatus("permission_required");
|
|
483
563
|
return false;
|
|
484
564
|
}
|
|
485
565
|
this.setDirectStatus("checking");
|
|
486
566
|
return true;
|
|
487
567
|
}
|
|
488
|
-
async probeDirectAccess() {
|
|
568
|
+
async probeDirectAccess(signal) {
|
|
489
569
|
this.setDirectStatus("checking");
|
|
490
570
|
try {
|
|
491
571
|
const response = await fetch(`${this.loopbackUrl}/v1/ready`, loopbackRequest({
|
|
492
572
|
method: "GET",
|
|
493
|
-
cache: "no-store"
|
|
573
|
+
cache: "no-store",
|
|
574
|
+
signal
|
|
494
575
|
}));
|
|
495
|
-
const body = await response.
|
|
576
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The connector returned an invalid readiness response.").catch(() => null);
|
|
496
577
|
if (response.ok
|
|
497
578
|
&& body?.service === "mdbase-connect"
|
|
498
579
|
&& body?.loopback_protocol_version === 1
|
|
499
|
-
&& body?.
|
|
580
|
+
&& body?.operation_transport_protocol_version === OPERATION_TRANSPORT_PROTOCOL_VERSION) {
|
|
500
581
|
this.markDirectAvailable();
|
|
501
582
|
return "available";
|
|
502
583
|
}
|
|
@@ -536,7 +617,7 @@ export class ConnectionTransport {
|
|
|
536
617
|
this.onChange();
|
|
537
618
|
}
|
|
538
619
|
invalidateRejectedAuthorization(rejected) {
|
|
539
|
-
const current = parseStored(this.storage.getItem(this.tokenKey()));
|
|
620
|
+
const current = parseStored(this.storage.getItem(this.internals.tokenKey(this.collectionId)));
|
|
540
621
|
if (!current || !sameAuthorization(current, rejected))
|
|
541
622
|
return;
|
|
542
623
|
this.internals.removeToken(this.collectionId, current.keyHandle);
|
|
@@ -545,73 +626,15 @@ export class ConnectionTransport {
|
|
|
545
626
|
this.emitConnection();
|
|
546
627
|
}
|
|
547
628
|
currentToken() {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
this.internals.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (stored)
|
|
556
|
-
invalidate();
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
if (token.version !== 1
|
|
560
|
-
|| typeof token.accessToken !== "string"
|
|
561
|
-
|| token.accessToken.length === 0
|
|
562
|
-
|| typeof token.clientId !== "string"
|
|
563
|
-
|| token.clientId.length === 0
|
|
564
|
-
|| token.collectionId !== this.collectionId
|
|
565
|
-
|| typeof token.collectionName !== "string"
|
|
566
|
-
|| token.collectionName.length === 0
|
|
567
|
-
|| !Array.isArray(token.operations)
|
|
568
|
-
|| token.operations.some((operation) => typeof operation !== "string")
|
|
569
|
-
|| typeof token.expiresAt !== "number"
|
|
570
|
-
|| !Number.isFinite(token.expiresAt)
|
|
571
|
-
|| (token.refreshToken !== undefined
|
|
572
|
-
&& (typeof token.refreshToken !== "string"
|
|
573
|
-
|| token.refreshToken.length === 0))
|
|
574
|
-
|| (token.refreshExpiresAt !== undefined
|
|
575
|
-
&& (typeof token.refreshExpiresAt !== "number"
|
|
576
|
-
|| !Number.isFinite(token.refreshExpiresAt))))
|
|
577
|
-
return invalidate(token.keyHandle);
|
|
578
|
-
if (!parseGrantScope(token.scope)) {
|
|
579
|
-
return invalidate(token.keyHandle);
|
|
580
|
-
}
|
|
581
|
-
if (token.fileCapability && !validFileCapability(token.fileCapability)) {
|
|
582
|
-
return invalidate(token.keyHandle);
|
|
583
|
-
}
|
|
584
|
-
if (token.authority
|
|
585
|
-
&& !validStoredAuthority(token.authority, token.collectionId)) {
|
|
586
|
-
return invalidate(token.keyHandle);
|
|
587
|
-
}
|
|
588
|
-
if (this.internals.relayEncryption === "required") {
|
|
589
|
-
if (token.authority) {
|
|
590
|
-
if (!token.keyHandle || !token.authority.proofPublicKey) {
|
|
591
|
-
return invalidate(token.keyHandle);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
if (!token.grantId
|
|
596
|
-
|| !token.keyHandle
|
|
597
|
-
|| !validStoredEncryption(token.encryption, token.collectionId)) {
|
|
598
|
-
return invalidate(token.keyHandle);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
if (token.expiresAt <= Date.now()
|
|
603
|
-
&& (!token.refreshToken || (token.refreshExpiresAt ?? 0) <= Date.now())) {
|
|
604
|
-
// The cloud bearer and the local grant proof have separate lifetimes. Keep an
|
|
605
|
-
// encrypted local grant usable while the connector still recognizes it; relay
|
|
606
|
-
// use will require reauthorization, and revocation remains enforced locally.
|
|
607
|
-
if (this.directCapable(token))
|
|
608
|
-
return token;
|
|
609
|
-
this.internals.removeToken(this.collectionId, token.keyHandle);
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
return token;
|
|
629
|
+
return readStoredToken({
|
|
630
|
+
stored: this.storage.getItem(this.internals.tokenKey(this.collectionId)),
|
|
631
|
+
collectionId: this.collectionId,
|
|
632
|
+
relayEncryption: this.internals.relayEncryption,
|
|
633
|
+
invalidate: (keyHandle) => this.internals.removeToken(this.collectionId, keyHandle, "invalid_stored_grant"),
|
|
634
|
+
directCapable: (token) => this.directCapable(token)
|
|
635
|
+
});
|
|
613
636
|
}
|
|
614
|
-
async authorizedToken() {
|
|
637
|
+
async authorizedToken(options = {}) {
|
|
615
638
|
const token = this.currentToken();
|
|
616
639
|
if (!token)
|
|
617
640
|
return null;
|
|
@@ -624,17 +647,17 @@ export class ConnectionTransport {
|
|
|
624
647
|
this.internals.removeToken(this.collectionId, token.keyHandle);
|
|
625
648
|
return null;
|
|
626
649
|
}
|
|
627
|
-
return this.refreshAuthorization();
|
|
650
|
+
return this.refreshAuthorization(options.signal);
|
|
628
651
|
}
|
|
629
|
-
refreshAuthorization() {
|
|
652
|
+
refreshAuthorization(signal) {
|
|
630
653
|
if (this.refreshPromise)
|
|
631
654
|
return this.refreshPromise;
|
|
632
|
-
this.refreshPromise = this.performRefresh().finally(() => {
|
|
655
|
+
this.refreshPromise = this.performRefresh(signal).finally(() => {
|
|
633
656
|
this.refreshPromise = null;
|
|
634
657
|
});
|
|
635
658
|
return this.refreshPromise;
|
|
636
659
|
}
|
|
637
|
-
async performRefresh() {
|
|
660
|
+
async performRefresh(signal) {
|
|
638
661
|
const current = this.currentToken();
|
|
639
662
|
if (!current?.refreshToken) {
|
|
640
663
|
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
@@ -650,7 +673,7 @@ export class ConnectionTransport {
|
|
|
650
673
|
client_id: current.clientId
|
|
651
674
|
}).toString();
|
|
652
675
|
const proof = current.authority
|
|
653
|
-
? await this.
|
|
676
|
+
? await authorityProofHeaders(this.keyStore, current, "POST", refreshUrl, refreshBody, current.refreshToken)
|
|
654
677
|
: {};
|
|
655
678
|
const response = await fetch(refreshUrl, {
|
|
656
679
|
method: "POST",
|
|
@@ -658,9 +681,10 @@ export class ConnectionTransport {
|
|
|
658
681
|
"content-type": "application/x-www-form-urlencoded",
|
|
659
682
|
...proof
|
|
660
683
|
},
|
|
661
|
-
body: refreshBody
|
|
684
|
+
body: refreshBody,
|
|
685
|
+
signal
|
|
662
686
|
});
|
|
663
|
-
const body = await response.
|
|
687
|
+
const body = await decodeJsonResponse(response, "invalid_token_response", "The authorization service returned an invalid token response.");
|
|
664
688
|
if (!response.ok) {
|
|
665
689
|
const latest = this.currentToken();
|
|
666
690
|
if (latest?.refreshToken && latest.refreshToken !== attemptedRefreshToken) {
|
|
@@ -676,28 +700,6 @@ export class ConnectionTransport {
|
|
|
676
700
|
}
|
|
677
701
|
return this.storeTokenResponse(body, current.clientId, current.keyHandle);
|
|
678
702
|
}
|
|
679
|
-
async authorityProofHeaders(token, method, url, body, credential) {
|
|
680
|
-
if (!token.authority?.proofPublicKey)
|
|
681
|
-
return {};
|
|
682
|
-
if (!token.keyHandle) {
|
|
683
|
-
throw connectError("missing_grant_key", "Reconnect this application to restore remote authority request signing.");
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
const target = new URL(url);
|
|
687
|
-
return await signAuthorityRequest(this.keyStore, token.keyHandle, token.authority.proofPublicKey, {
|
|
688
|
-
method,
|
|
689
|
-
target: `${target.pathname}${target.search}`,
|
|
690
|
-
body,
|
|
691
|
-
credential
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
if (error instanceof RelayCryptoError) {
|
|
696
|
-
throw serverConnectError(error.code, error.message);
|
|
697
|
-
}
|
|
698
|
-
throw error;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
703
|
storeTokenResponse(body, clientId, keyHandle) {
|
|
702
704
|
if (body.collection_id !== this.collectionId) {
|
|
703
705
|
throw connectError("collection_mismatch", "The refreshed authorization belongs to a different collection.");
|
|
@@ -710,19 +712,17 @@ export class ConnectionTransport {
|
|
|
710
712
|
this.emitConnection();
|
|
711
713
|
return token;
|
|
712
714
|
}
|
|
713
|
-
|
|
714
|
-
return this.
|
|
715
|
-
}
|
|
716
|
-
notificationKey(transport = "web_push") {
|
|
717
|
-
return this.internals.notificationKey(this.collectionId, transport);
|
|
715
|
+
storedPendingMutations() {
|
|
716
|
+
return this.pendingMutationStore.list(this.currentToken()?.collectionId ?? null);
|
|
718
717
|
}
|
|
719
|
-
|
|
720
|
-
return this.
|
|
718
|
+
storedPendingMutation(requestId) {
|
|
719
|
+
return this.pendingMutationStore.find(this.currentToken()?.collectionId ?? null, requestId);
|
|
721
720
|
}
|
|
722
|
-
clearPendingMutation() {
|
|
723
|
-
this.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
721
|
+
clearPendingMutation(requestId) {
|
|
722
|
+
const pending = this.pendingMutationStore.take(requestId);
|
|
723
|
+
const currentKeyHandle = this.currentToken()?.keyHandle;
|
|
724
|
+
if (pending?.keyHandle && pending.keyHandle !== currentKeyHandle) {
|
|
725
|
+
void this.keyStore.delete(pending.keyHandle).catch(() => undefined);
|
|
726
|
+
}
|
|
727
727
|
}
|
|
728
728
|
}
|