@mdbase-dev/connect 0.1.0-beta.100
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/LICENSE +21 -0
- package/README.md +696 -0
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-contract.d.ts +22 -0
- package/dist/application-contract.js +75 -0
- package/dist/application-identity.d.ts +34 -0
- package/dist/application-identity.js +148 -0
- package/dist/application-session.d.ts +167 -0
- package/dist/application-session.js +722 -0
- package/dist/async.d.ts +1 -0
- package/dist/async.js +19 -0
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/authority-sync-request.d.ts +3 -0
- package/dist/authority-sync-request.js +20 -0
- package/dist/authorization-refresh.d.ts +14 -0
- package/dist/authorization-refresh.js +41 -0
- package/dist/authorization-types.d.ts +7 -0
- package/dist/authorization-types.js +1 -0
- package/dist/authorization-url.d.ts +5 -0
- package/dist/authorization-url.js +34 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.js +21 -0
- package/dist/browser/integrity.json +4 -0
- package/dist/browser/mdbase-connect.min.js +3 -0
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.d.ts +24 -0
- package/dist/capabilities.js +91 -0
- package/dist/collection-client.d.ts +66 -0
- package/dist/collection-client.js +686 -0
- package/dist/connect-authorization-helpers.d.ts +4 -0
- package/dist/connect-authorization-helpers.js +25 -0
- package/dist/connect-options.d.ts +35 -0
- package/dist/connect-options.js +1 -0
- package/dist/connect.d.ts +66 -0
- package/dist/connect.js +711 -0
- package/dist/connection-file-transport.d.ts +34 -0
- package/dist/connection-file-transport.js +85 -0
- package/dist/connection-index.d.ts +3 -0
- package/dist/connection-index.js +20 -0
- package/dist/connection-notifications.d.ts +31 -0
- package/dist/connection-notifications.js +197 -0
- package/dist/connection-transport-internals.d.ts +24 -0
- package/dist/connection-transport-internals.js +1 -0
- package/dist/connection-transport.d.ts +64 -0
- package/dist/connection-transport.js +725 -0
- package/dist/connection-types.d.ts +38 -0
- package/dist/connection-types.js +1 -0
- package/dist/connection.d.ts +175 -0
- package/dist/connection.js +606 -0
- package/dist/crypto-entry.d.ts +7 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.d.ts +63 -0
- package/dist/crypto.js +409 -0
- package/dist/direct-access.d.ts +3 -0
- package/dist/direct-access.js +29 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +142 -0
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-crypto.d.ts +25 -0
- package/dist/file-crypto.js +132 -0
- package/dist/file-sha256.d.ts +12 -0
- package/dist/file-sha256.js +143 -0
- package/dist/file-stream-source.d.ts +17 -0
- package/dist/file-stream-source.js +157 -0
- package/dist/file-transfer-internals.d.ts +23 -0
- package/dist/file-transfer-internals.js +228 -0
- package/dist/files.d.ts +115 -0
- package/dist/files.js +563 -0
- package/dist/fresh-encrypted-request.d.ts +4 -0
- package/dist/fresh-encrypted-request.js +28 -0
- package/dist/grant-key-leases.d.ts +24 -0
- package/dist/grant-key-leases.js +144 -0
- package/dist/hosted-file-request.d.ts +6 -0
- package/dist/hosted-file-request.js +63 -0
- package/dist/hosted-operation-response.d.ts +5 -0
- package/dist/hosted-operation-response.js +26 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +10 -0
- package/dist/internal-types.d.ts +100 -0
- package/dist/internal-types.js +1 -0
- package/dist/local-file-transport.d.ts +37 -0
- package/dist/local-file-transport.js +457 -0
- package/dist/mdbase-connect.d.ts +28 -0
- package/dist/mdbase-connect.js +53 -0
- package/dist/notifications.d.ts +48 -0
- package/dist/notifications.js +52 -0
- package/dist/operation-helpers.d.ts +27 -0
- package/dist/operation-helpers.js +144 -0
- package/dist/operation-types.d.ts +707 -0
- package/dist/operation-types.js +1 -0
- package/dist/outcomes.d.ts +51 -0
- package/dist/outcomes.js +194 -0
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/pending-recovery-transports.d.ts +5 -0
- package/dist/pending-recovery-transports.js +52 -0
- package/dist/popup-authorization.d.ts +31 -0
- package/dist/popup-authorization.js +100 -0
- package/dist/query-pagination.d.ts +6 -0
- package/dist/query-pagination.js +119 -0
- package/dist/request-budget.d.ts +34 -0
- package/dist/request-budget.js +104 -0
- package/dist/request-coordinator.d.ts +29 -0
- package/dist/request-coordinator.js +213 -0
- package/dist/retired-grant-keys.d.ts +9 -0
- package/dist/retired-grant-keys.js +56 -0
- package/dist/runtime-utils.d.ts +52 -0
- package/dist/runtime-utils.js +368 -0
- package/dist/saved-view-pagination.d.ts +6 -0
- package/dist/saved-view-pagination.js +76 -0
- package/dist/selection.d.ts +48 -0
- package/dist/selection.js +112 -0
- package/dist/session.d.ts +114 -0
- package/dist/session.js +569 -0
- package/dist/shared-operation.d.ts +7 -0
- package/dist/shared-operation.js +41 -0
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +64 -0
- package/dist/structured-readiness.d.ts +77 -0
- package/dist/structured-readiness.js +126 -0
- package/dist/token-response.d.ts +12 -0
- package/dist/token-response.js +81 -0
- package/dist/transient-retry.d.ts +22 -0
- package/dist/transient-retry.js +59 -0
- package/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- package/package.json +51 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { OPERATION_TRANSPORT_PROTOCOL_VERSION, isConnectProblem, mutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { decryptRelayResponse, encryptRelayRequest, RelayCryptoError } from "./crypto.js";
|
|
3
|
+
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
4
|
+
import { ConnectionFileTransport } from "./connection-file-transport.js";
|
|
5
|
+
import { authorityProofHeaders } from "./authority-proof.js";
|
|
6
|
+
import { performAuthorizationRefresh } from "./authorization-refresh.js";
|
|
7
|
+
import { PendingMutationStore } from "./pending-mutation-store.js";
|
|
8
|
+
import { directFallbackStatus, encryptedOperationError, fetchOperationRequest, isMutation, localNetworkPermission, loopbackRequest, operationFingerprint, operationTransportError, sameAuthorization, throwIfCancelled, unknownMutationOutcome, withOperationDeadline } from "./operation-helpers.js";
|
|
9
|
+
import { apiError, decodeJsonResponse, parseStored, } from "./runtime-utils.js";
|
|
10
|
+
import { readStoredToken } from "./stored-token.js";
|
|
11
|
+
import { requestOptionsWithinBudget, withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
|
|
12
|
+
import { sendAuthoritySyncRequest } from "./authority-sync-request.js";
|
|
13
|
+
import { freshEncryptedRequest } from "./fresh-encrypted-request.js";
|
|
14
|
+
import { GrantKeyLeaseSet, retainCurrentGrantToken } from "./grant-key-leases.js";
|
|
15
|
+
import { SharedOperationMap } from "./shared-operation.js";
|
|
16
|
+
import { isExplicitConnectorBusyResponse, retryExplicitConnectorBusy } from "./transient-retry.js";
|
|
17
|
+
import { probeLoopbackAccess, tokenSupportsDirectAccess } from "./direct-access.js";
|
|
18
|
+
import { decodeHostedOperationResponse } from "./hosted-operation-response.js";
|
|
19
|
+
export class ConnectionTransport {
|
|
20
|
+
serverUrl;
|
|
21
|
+
storage;
|
|
22
|
+
keyStore;
|
|
23
|
+
directAccessMode;
|
|
24
|
+
loopbackUrl;
|
|
25
|
+
collectionId;
|
|
26
|
+
internals;
|
|
27
|
+
onChange;
|
|
28
|
+
timeouts;
|
|
29
|
+
pendingMutationStore;
|
|
30
|
+
files;
|
|
31
|
+
refreshOperations = new SharedOperationMap();
|
|
32
|
+
directStatus;
|
|
33
|
+
currentRoute = "relay";
|
|
34
|
+
directFailures = 0;
|
|
35
|
+
directRetryAt = 0;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.serverUrl = options.serverUrl;
|
|
38
|
+
this.storage = options.storage;
|
|
39
|
+
this.keyStore = options.keyStore;
|
|
40
|
+
this.directAccessMode = options.directAccessMode;
|
|
41
|
+
this.loopbackUrl = options.loopbackUrl;
|
|
42
|
+
this.collectionId = options.collectionId;
|
|
43
|
+
this.internals = options.internals;
|
|
44
|
+
this.onChange = options.onChange;
|
|
45
|
+
this.timeouts = options.timeouts;
|
|
46
|
+
this.pendingMutationStore = new PendingMutationStore(this.storage, this.internals.pendingMutationKey(this.collectionId));
|
|
47
|
+
this.files = new ConnectionFileTransport({
|
|
48
|
+
keyStore: this.keyStore,
|
|
49
|
+
serverUrl: this.serverUrl,
|
|
50
|
+
loopbackUrl: this.loopbackUrl,
|
|
51
|
+
collectionId: this.collectionId,
|
|
52
|
+
currentToken: () => this.currentToken(),
|
|
53
|
+
authorizedToken: (signal) => this.authorizedToken({ signal, timeoutMs: null }),
|
|
54
|
+
refreshAuthorization: (signal) => this.refreshAuthorization({ signal, timeoutMs: null }),
|
|
55
|
+
shouldAttemptDirect: (token) => this.shouldAttemptDirect(token),
|
|
56
|
+
onDirectAvailable: () => {
|
|
57
|
+
this.markDirectAvailable();
|
|
58
|
+
this.setRoute("direct");
|
|
59
|
+
},
|
|
60
|
+
onDirectUnavailable: () => this.markDirectUnavailable(),
|
|
61
|
+
onRelayAvailable: () => this.setRoute("relay"),
|
|
62
|
+
acquireGrantKeyLease: (collectionId, keyHandle, signal) => this.internals.acquireGrantKeyLease(collectionId, keyHandle, signal),
|
|
63
|
+
authorityProofHeaders: (token, method, url, body, credential) => authorityProofHeaders(this.keyStore, token, method, url, body, credential)
|
|
64
|
+
});
|
|
65
|
+
this.directStatus = this.directAccessMode === "disabled"
|
|
66
|
+
? "disabled"
|
|
67
|
+
: "unavailable";
|
|
68
|
+
}
|
|
69
|
+
get directAccess() {
|
|
70
|
+
return this.currentToken()?.authority ? "disabled" : this.directStatus;
|
|
71
|
+
}
|
|
72
|
+
get route() {
|
|
73
|
+
return this.currentToken()?.authority ? "remote" : this.currentRoute;
|
|
74
|
+
}
|
|
75
|
+
notifyStorageChanged() { this.emitConnection(); }
|
|
76
|
+
async checkDirectAccess(options = {}) {
|
|
77
|
+
return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.checkDirectAccessWithinBudget(budget.signal));
|
|
78
|
+
}
|
|
79
|
+
async checkDirectAccessWithinBudget(signal) {
|
|
80
|
+
const token = this.currentToken();
|
|
81
|
+
if (!this.directEligible(token))
|
|
82
|
+
return this.setDirectStatus("disabled");
|
|
83
|
+
const permission = await localNetworkPermission();
|
|
84
|
+
if (permission === "denied")
|
|
85
|
+
return this.setDirectStatus("denied");
|
|
86
|
+
if ((permission === "prompt" || permission === null)
|
|
87
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "enabled") {
|
|
88
|
+
return this.setDirectStatus("permission_required");
|
|
89
|
+
}
|
|
90
|
+
return this.probeDirectAccess(signal);
|
|
91
|
+
}
|
|
92
|
+
/** Call from a user gesture to request browser permission for direct local access. */
|
|
93
|
+
async requestDirectAccess(options = {}) {
|
|
94
|
+
return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.requestDirectAccessWithinBudget(budget.signal));
|
|
95
|
+
}
|
|
96
|
+
async requestDirectAccessWithinBudget(signal) {
|
|
97
|
+
const token = this.currentToken();
|
|
98
|
+
if (!this.directCapable(token))
|
|
99
|
+
return this.setDirectStatus("disabled");
|
|
100
|
+
this.storage.setItem(this.internals.directPreferenceKey(), "enabled");
|
|
101
|
+
this.directRetryAt = 0;
|
|
102
|
+
return this.probeDirectAccess(signal);
|
|
103
|
+
}
|
|
104
|
+
disableDirectAccess() {
|
|
105
|
+
this.storage.setItem(this.internals.directPreferenceKey(), "disabled");
|
|
106
|
+
this.setDirectStatus("disabled");
|
|
107
|
+
this.setRoute("relay");
|
|
108
|
+
}
|
|
109
|
+
/** Return pending writes without exposing authority transport credentials. */
|
|
110
|
+
pendingMutations() {
|
|
111
|
+
return this.storedPendingMutations().map((pending) => ({
|
|
112
|
+
requestId: pending.requestId,
|
|
113
|
+
operation: this.pendingMutationStore.identifier(pending),
|
|
114
|
+
fingerprint: pending.inputFingerprint,
|
|
115
|
+
status: "outcome_unknown",
|
|
116
|
+
createdAt: new Date(pending.createdAt).toISOString()
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
pendingMutation(requestId) {
|
|
120
|
+
const pending = this.storedPendingMutation(requestId);
|
|
121
|
+
return pending ? {
|
|
122
|
+
requestId: pending.requestId,
|
|
123
|
+
operation: this.pendingMutationStore.identifier(pending),
|
|
124
|
+
fingerprint: pending.inputFingerprint,
|
|
125
|
+
status: "outcome_unknown",
|
|
126
|
+
createdAt: new Date(pending.createdAt).toISOString()
|
|
127
|
+
} : null;
|
|
128
|
+
}
|
|
129
|
+
async recoverPendingMutation(requestId, options) {
|
|
130
|
+
const pending = this.storedPendingMutation(requestId);
|
|
131
|
+
if (!pending) {
|
|
132
|
+
throw connectError("no_pending_mutation", "There is no interrupted mutation to resume.");
|
|
133
|
+
}
|
|
134
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.performOperationWithinBudget(pending.operation, pending.request?.input ?? {}, requestOptionsWithinBudget(options ?? {}, budget), pending));
|
|
135
|
+
}
|
|
136
|
+
async performOperation(operation, input, options = {}) {
|
|
137
|
+
return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.performOperationWithinBudget(operation, input, requestOptionsWithinBudget(options, budget)));
|
|
138
|
+
}
|
|
139
|
+
async performOperationWithinBudget(operation, input, options, storedPending, freshReadRetried = false, connectorBusyRetries = 0, knownRejectedMutationRetry = false) {
|
|
140
|
+
throwIfCancelled(options.signal);
|
|
141
|
+
const leases = this.grantKeyLeases();
|
|
142
|
+
try {
|
|
143
|
+
const token = await retainCurrentGrantToken(() => this.currentToken(), leases, options.signal);
|
|
144
|
+
if (!token)
|
|
145
|
+
throw connectError("not_authorized", "Connect this application before accessing a collection.");
|
|
146
|
+
await leases.retain(storedPending?.keyHandle, options.signal);
|
|
147
|
+
return await this.performLeasedOperation(token, operation, input, options, leases, storedPending, freshReadRetried, connectorBusyRetries, knownRejectedMutationRetry);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
leases.release();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async performLeasedOperation(token, operation, input, options, leases, storedPending, freshReadRetried = false, connectorBusyRetries = 0, knownRejectedMutationRetry = false) {
|
|
154
|
+
if (!token.operations.includes(operation)) {
|
|
155
|
+
throw connectError("insufficient_access", `This connection does not allow ${operation}.`, {
|
|
156
|
+
details: {
|
|
157
|
+
required_operations: [operation],
|
|
158
|
+
granted_operations: [...token.operations],
|
|
159
|
+
missing_operations: [operation]
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
let tryDirect = await this.shouldAttemptDirect(token);
|
|
164
|
+
if (!tryDirect) {
|
|
165
|
+
const authorized = await this.authorizedToken({ signal: options.signal, timeoutMs: null });
|
|
166
|
+
if (!authorized)
|
|
167
|
+
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
168
|
+
const current = await retainCurrentGrantToken(() => this.currentToken(), leases, options.signal);
|
|
169
|
+
if (!current)
|
|
170
|
+
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
171
|
+
token = current;
|
|
172
|
+
}
|
|
173
|
+
const pendingRequestId = storedPending?.requestId
|
|
174
|
+
?? (isMutation(operation, input) ? crypto.randomUUID() : undefined);
|
|
175
|
+
let attempt;
|
|
176
|
+
try {
|
|
177
|
+
attempt = await this.sendOperation(token, operation, input, tryDirect, leases, options, pendingRequestId, knownRejectedMutationRetry);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
throw operationTransportError(error, options.signal, pendingRequestId !== undefined && this.storedPendingMutation(pendingRequestId) !== null
|
|
181
|
+
? pendingRequestId
|
|
182
|
+
: undefined, token.authority ? "hosted_provider_unavailable" : "relay_unavailable");
|
|
183
|
+
}
|
|
184
|
+
let response = attempt.response;
|
|
185
|
+
const staleBinding = response.status === 409
|
|
186
|
+
&& (await decodeJsonResponse(response.clone(), "invalid_operation_response", "The collection authority returned an invalid operation response.").catch(() => null))?.error?.code === "encryption_binding_stale";
|
|
187
|
+
if ((response.status === 401 || staleBinding) && token.refreshToken) {
|
|
188
|
+
if (attempt.pendingMutation
|
|
189
|
+
&& (attempt.directDeliveryUncertain
|
|
190
|
+
|| (attempt.encryptedRequest && attempt.resumingMutation))) {
|
|
191
|
+
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 } });
|
|
192
|
+
}
|
|
193
|
+
if (attempt.pendingMutation)
|
|
194
|
+
this.clearPendingMutation(attempt.requestId);
|
|
195
|
+
token = await this.refreshAuthorization({ signal: options.signal, timeoutMs: null });
|
|
196
|
+
const current = await retainCurrentGrantToken(() => this.currentToken(), leases, options.signal);
|
|
197
|
+
if (!current)
|
|
198
|
+
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
199
|
+
token = current;
|
|
200
|
+
tryDirect = await this.shouldAttemptDirect(token);
|
|
201
|
+
try {
|
|
202
|
+
attempt = await this.sendOperation(token, operation, input, tryDirect, leases, options, pendingRequestId, knownRejectedMutationRetry);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw operationTransportError(error, options.signal, pendingRequestId !== undefined && this.storedPendingMutation(pendingRequestId) !== null
|
|
206
|
+
? pendingRequestId
|
|
207
|
+
: undefined, token.authority ? "hosted_provider_unavailable" : "relay_unavailable");
|
|
208
|
+
}
|
|
209
|
+
response = attempt.response;
|
|
210
|
+
}
|
|
211
|
+
const retryBusy = (error) => retryExplicitConnectorBusy({
|
|
212
|
+
error,
|
|
213
|
+
attempt,
|
|
214
|
+
completedRetries: connectorBusyRetries,
|
|
215
|
+
signal: options.signal,
|
|
216
|
+
knownRejectedMutationRetry,
|
|
217
|
+
clearPending: (requestId) => this.clearPendingMutation(requestId),
|
|
218
|
+
retry: (pending, knownRejected) => this.performOperationWithinBudget(operation, input, options, pending, freshReadRetried, connectorBusyRetries + 1, knownRejected)
|
|
219
|
+
});
|
|
220
|
+
let decoded;
|
|
221
|
+
try {
|
|
222
|
+
decoded = await decodeHostedOperationResponse(response, token.authority !== undefined);
|
|
223
|
+
}
|
|
224
|
+
catch (cause) {
|
|
225
|
+
if (attempt.pendingMutation)
|
|
226
|
+
throw unknownMutationOutcome(attempt.requestId, cause);
|
|
227
|
+
throw cause;
|
|
228
|
+
}
|
|
229
|
+
const body = decoded.body;
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const error = decoded.httpError
|
|
232
|
+
?? apiError(body, "operation_failed", "Collection operation failed.", response.status);
|
|
233
|
+
const recovery = await retryBusy(error);
|
|
234
|
+
if (recovery.retried)
|
|
235
|
+
return recovery.result;
|
|
236
|
+
if (error.code === "fresh_request_required"
|
|
237
|
+
&& !attempt.pendingMutation
|
|
238
|
+
&& !freshReadRetried) {
|
|
239
|
+
return this.performOperationWithinBudget(operation, input, options, undefined, true);
|
|
240
|
+
}
|
|
241
|
+
if (attempt.pendingMutation && error.outcomeUnknown)
|
|
242
|
+
throw error;
|
|
243
|
+
if (attempt.pendingMutation
|
|
244
|
+
&& (attempt.directDeliveryUncertain
|
|
245
|
+
|| (attempt.encryptedRequest && attempt.resumingMutation))) {
|
|
246
|
+
throw unknownMutationOutcome(attempt.requestId, error);
|
|
247
|
+
}
|
|
248
|
+
if (attempt.pendingMutation && !attempt.directDeliveryUncertain) {
|
|
249
|
+
this.clearPendingMutation(attempt.requestId);
|
|
250
|
+
}
|
|
251
|
+
if (error.code === "direct_operation_rejected" && error.status === 403) {
|
|
252
|
+
this.invalidateRejectedAuthorization(token);
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
if (attempt.encryptedRequest) {
|
|
257
|
+
const encryptedResponse = body?.envelope;
|
|
258
|
+
const pendingCrypto = attempt.pendingMutationRecord;
|
|
259
|
+
const responseEncryption = pendingCrypto?.encryption ?? token.encryption;
|
|
260
|
+
const responseGrantId = pendingCrypto?.grantId ?? token.grantId;
|
|
261
|
+
const responseApplicationId = pendingCrypto?.applicationId ?? token.clientId;
|
|
262
|
+
const responseKeyHandle = pendingCrypto?.keyHandle ?? token.keyHandle;
|
|
263
|
+
if (!encryptedResponse || !responseEncryption || !responseGrantId || !responseKeyHandle) {
|
|
264
|
+
if (attempt.pendingMutation)
|
|
265
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Encrypted operation response was missing its envelope."));
|
|
266
|
+
throw connectError("invalid_encrypted_response", "The relay did not return an encrypted connector response.");
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const decrypted = await decryptRelayResponse(this.keyStore, responseKeyHandle, {
|
|
270
|
+
grantId: responseGrantId,
|
|
271
|
+
applicationId: responseApplicationId,
|
|
272
|
+
encryption: responseEncryption
|
|
273
|
+
}, attempt.encryptedRequest, encryptedResponse);
|
|
274
|
+
if (!decrypted.ok
|
|
275
|
+
&& decrypted.problem.code === "fresh_request_required"
|
|
276
|
+
&& !attempt.pendingMutation
|
|
277
|
+
&& !freshReadRetried) {
|
|
278
|
+
return this.performOperationWithinBudget(operation, input, options, undefined, true);
|
|
279
|
+
}
|
|
280
|
+
if (!decrypted.ok) {
|
|
281
|
+
const error = encryptedOperationError(decrypted.problem);
|
|
282
|
+
if (attempt.pendingMutation && !error.outcomeUnknown)
|
|
283
|
+
this.clearPendingMutation(attempt.requestId);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
if (attempt.pendingMutation)
|
|
287
|
+
this.clearPendingMutation(attempt.requestId);
|
|
288
|
+
return decrypted.result;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error instanceof MdbaseConnectError)
|
|
292
|
+
throw error;
|
|
293
|
+
if (error instanceof RelayCryptoError) {
|
|
294
|
+
if (attempt.pendingMutation)
|
|
295
|
+
throw unknownMutationOutcome(attempt.requestId, error);
|
|
296
|
+
throw serverConnectError(error.code, error.message);
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const expectedProtocol = attempt.pendingMutationRecord?.request?.protocol_version
|
|
302
|
+
?? OPERATION_TRANSPORT_PROTOCOL_VERSION;
|
|
303
|
+
if (body?.protocol_version !== expectedProtocol
|
|
304
|
+
|| body?.request_id !== attempt.requestId) {
|
|
305
|
+
if (attempt.pendingMutation)
|
|
306
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Operation response protocol or request ID did not match."));
|
|
307
|
+
throw connectError("invalid_operation_response", "The collection authority returned a response for a different protocol request.");
|
|
308
|
+
}
|
|
309
|
+
if (body.ok === false) {
|
|
310
|
+
if (!isConnectProblem(body.problem)) {
|
|
311
|
+
if (attempt.pendingMutation)
|
|
312
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Operation rejection did not contain a canonical problem."));
|
|
313
|
+
throw connectError("invalid_operation_response", "The collection authority returned an invalid operation problem.");
|
|
314
|
+
}
|
|
315
|
+
const error = new MdbaseConnectError(body.problem);
|
|
316
|
+
const recovery = await retryBusy(error);
|
|
317
|
+
if (recovery.retried)
|
|
318
|
+
return recovery.result;
|
|
319
|
+
if (attempt.pendingMutation && !error.outcomeUnknown)
|
|
320
|
+
this.clearPendingMutation(attempt.requestId);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
if (body.ok !== true || !("result" in body)) {
|
|
324
|
+
if (attempt.pendingMutation)
|
|
325
|
+
throw unknownMutationOutcome(attempt.requestId, new Error("Successful operation response did not contain a result."));
|
|
326
|
+
throw connectError("invalid_operation_response", "The collection authority returned an incomplete operation response.");
|
|
327
|
+
}
|
|
328
|
+
if (attempt.pendingMutation)
|
|
329
|
+
this.clearPendingMutation(attempt.requestId);
|
|
330
|
+
return body.result;
|
|
331
|
+
}
|
|
332
|
+
async performAuthoritySyncRequest(collectionId, replicaId, method, path, input, options = {}) {
|
|
333
|
+
return withCooperativeRequestBudget(options, this.timeouts.syncMs, async (budget) => {
|
|
334
|
+
const leases = this.grantKeyLeases();
|
|
335
|
+
try {
|
|
336
|
+
let token = await retainCurrentGrantToken(() => this.currentToken(), leases, budget.signal);
|
|
337
|
+
if (token)
|
|
338
|
+
await this.authorizedToken({ signal: budget.signal, timeoutMs: null });
|
|
339
|
+
token = await retainCurrentGrantToken(() => this.currentToken(), leases, budget.signal);
|
|
340
|
+
requireSyncAuthorization(token, collectionId, replicaId);
|
|
341
|
+
let response = await sendAuthoritySyncRequest(this.keyStore, token, method, path, input, budget.signal);
|
|
342
|
+
if (response.status === 401 && token.refreshToken) {
|
|
343
|
+
token = await this.refreshAuthorization({ signal: budget.signal, timeoutMs: null });
|
|
344
|
+
token = await retainCurrentGrantToken(() => this.currentToken(), leases, budget.signal);
|
|
345
|
+
requireSyncAuthorization(token, collectionId, replicaId);
|
|
346
|
+
response = await sendAuthoritySyncRequest(this.keyStore, token, method, path, input, budget.signal);
|
|
347
|
+
}
|
|
348
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid synchronization response.");
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
throw apiError(body, "sync_failed", "Collection synchronization failed.", response.status);
|
|
351
|
+
}
|
|
352
|
+
return body;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const mutationRequestId = method === "POST" && path === "mutations"
|
|
356
|
+
&& input && typeof input === "object"
|
|
357
|
+
&& typeof input.mutation_id === "string"
|
|
358
|
+
? input.mutation_id
|
|
359
|
+
: undefined;
|
|
360
|
+
throw operationTransportError(error, budget.signal, mutationRequestId, "hosted_provider_unavailable");
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
leases.release();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async sendOperation(token, operation, input, tryDirect, leases, options = {}, pendingRequestId, knownRejectedMutationRetry = false) {
|
|
368
|
+
let body = input ?? {};
|
|
369
|
+
let encryptedRequest;
|
|
370
|
+
let pendingMutation = false;
|
|
371
|
+
let resumingMutation = false;
|
|
372
|
+
let requestId = pendingRequestId ?? crypto.randomUUID();
|
|
373
|
+
let pending = null;
|
|
374
|
+
let inputFingerprint;
|
|
375
|
+
if (pendingRequestId !== undefined) {
|
|
376
|
+
pending = this.storedPendingMutation(pendingRequestId);
|
|
377
|
+
inputFingerprint = pending?.inputFingerprint
|
|
378
|
+
?? await operationFingerprint(operation, input);
|
|
379
|
+
if (pending) {
|
|
380
|
+
if (pending.collectionId !== token.collectionId
|
|
381
|
+
|| pending.operation !== operation
|
|
382
|
+
|| pending.inputFingerprint !== inputFingerprint) {
|
|
383
|
+
throw connectError("pending_mutation_unresolved", "A previous write still has an unknown outcome. Retry that exact write before making another change.");
|
|
384
|
+
}
|
|
385
|
+
requestId = pending.requestId;
|
|
386
|
+
resumingMutation = !knownRejectedMutationRetry;
|
|
387
|
+
}
|
|
388
|
+
pendingMutation = true;
|
|
389
|
+
}
|
|
390
|
+
const mutation = pending?.mutation ?? mutationOperationIdentifier(operation, input);
|
|
391
|
+
if (pending?.envelope && (token.authority || !token.encryption)) {
|
|
392
|
+
throw connectError("pending_mutation_unresolved", "The pending write requires its encrypted relay authority. Reconnect that authority before recovery.");
|
|
393
|
+
}
|
|
394
|
+
if (pending?.request && token.encryption && !token.authority) {
|
|
395
|
+
throw connectError("pending_mutation_unresolved", "The pending write belongs to a different authority transport. Reconnect that authority before recovery.");
|
|
396
|
+
}
|
|
397
|
+
if (token.encryption && !token.authority) {
|
|
398
|
+
if (!token.grantId || !token.keyHandle) {
|
|
399
|
+
throw connectError("missing_grant_key", "Reconnect this application to restore encrypted access.");
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
if (pendingMutation) {
|
|
403
|
+
if (pending) {
|
|
404
|
+
if (!pending.envelope) {
|
|
405
|
+
throw connectError("pending_mutation_unresolved", "The pending write belongs to a different transport. Reconnect before retrying it.");
|
|
406
|
+
}
|
|
407
|
+
// Preserve the durable identity and ciphertext, but refresh the
|
|
408
|
+
// unauthenticated scheduling deadline for this recovery attempt.
|
|
409
|
+
encryptedRequest = withOperationDeadline(pending.envelope, options.deadlineUnixMs);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
encryptedRequest = await encryptRelayRequest(this.keyStore, token.keyHandle, { grantId: token.grantId, applicationId: token.clientId, encryption: token.encryption }, operation, input, requestId, options.deadlineUnixMs);
|
|
413
|
+
this.pendingMutationStore.store({
|
|
414
|
+
collectionId: token.collectionId,
|
|
415
|
+
...(token.grantId ? { grantId: token.grantId } : {}),
|
|
416
|
+
keyId: token.encryption.key_id,
|
|
417
|
+
keyHandle: token.keyHandle,
|
|
418
|
+
applicationId: token.clientId,
|
|
419
|
+
encryption: token.encryption,
|
|
420
|
+
operation,
|
|
421
|
+
mutation: mutation,
|
|
422
|
+
inputFingerprint: inputFingerprint,
|
|
423
|
+
requestId,
|
|
424
|
+
envelope: encryptedRequest,
|
|
425
|
+
createdAt: Date.now()
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
encryptedRequest = await encryptRelayRequest(this.keyStore, token.keyHandle, { grantId: token.grantId, applicationId: token.clientId, encryption: token.encryption }, operation, input, undefined, options.deadlineUnixMs);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
if (error instanceof RelayCryptoError)
|
|
435
|
+
throw serverConnectError(error.code, error.message);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
requestId = encryptedRequest.request_id;
|
|
439
|
+
body = encryptedRequest;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const request = pending?.request ?? {
|
|
443
|
+
protocol_version: OPERATION_TRANSPORT_PROTOCOL_VERSION,
|
|
444
|
+
request_id: requestId,
|
|
445
|
+
input: body
|
|
446
|
+
};
|
|
447
|
+
body = request;
|
|
448
|
+
if (pendingMutation && !pending) {
|
|
449
|
+
this.pendingMutationStore.store({
|
|
450
|
+
collectionId: token.collectionId,
|
|
451
|
+
...(token.grantId ? { grantId: token.grantId } : {}),
|
|
452
|
+
operation,
|
|
453
|
+
mutation: mutation,
|
|
454
|
+
inputFingerprint: inputFingerprint,
|
|
455
|
+
requestId,
|
|
456
|
+
request,
|
|
457
|
+
createdAt: Date.now()
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (tryDirect && encryptedRequest && !token.authority) {
|
|
462
|
+
let directDeliveryUncertain = false;
|
|
463
|
+
try {
|
|
464
|
+
const response = await fetch(`${this.loopbackUrl}/v1/operations`, loopbackRequest({
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: { "content-type": "application/mdbase-connect+json" },
|
|
467
|
+
body: JSON.stringify(encryptedRequest),
|
|
468
|
+
signal: options.signal
|
|
469
|
+
}));
|
|
470
|
+
if (!directFallbackStatus(response.status)
|
|
471
|
+
|| await isExplicitConnectorBusyResponse(response)) {
|
|
472
|
+
if (response.ok) {
|
|
473
|
+
this.markDirectAvailable();
|
|
474
|
+
this.setRoute("direct");
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
response,
|
|
478
|
+
requestId,
|
|
479
|
+
encryptedRequest,
|
|
480
|
+
pendingMutation,
|
|
481
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
482
|
+
resumingMutation
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
directDeliveryUncertain = response.status >= 500;
|
|
486
|
+
this.markDirectUnavailable();
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (options.signal?.aborted) {
|
|
490
|
+
if (pendingMutation)
|
|
491
|
+
throw unknownMutationOutcome(requestId, error);
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
directDeliveryUncertain = true;
|
|
495
|
+
if ((await localNetworkPermission()) === "denied")
|
|
496
|
+
this.setDirectStatus("denied");
|
|
497
|
+
else
|
|
498
|
+
this.markDirectUnavailable();
|
|
499
|
+
}
|
|
500
|
+
let relayToken;
|
|
501
|
+
try {
|
|
502
|
+
if (token.expiresAt <= Date.now() + 30_000) {
|
|
503
|
+
await this.refreshAuthorization({ signal: options.signal, timeoutMs: null });
|
|
504
|
+
}
|
|
505
|
+
const current = await retainCurrentGrantToken(() => this.currentToken(), leases, options.signal);
|
|
506
|
+
if (!current)
|
|
507
|
+
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
508
|
+
relayToken = current;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (pendingMutation && (directDeliveryUncertain || resumingMutation))
|
|
512
|
+
throw unknownMutationOutcome(requestId, error);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
let response;
|
|
516
|
+
if (!pendingMutation) {
|
|
517
|
+
encryptedRequest = await freshEncryptedRequest(this.keyStore, relayToken, operation, input, options.deadlineUnixMs);
|
|
518
|
+
requestId = encryptedRequest.request_id;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
response = await fetch(`${this.serverUrl}/v1/authorities/${encodeURIComponent(relayToken.collectionId)}/operations/${operation}`, {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: {
|
|
524
|
+
authorization: `Bearer ${relayToken.accessToken}`,
|
|
525
|
+
"content-type": "application/json"
|
|
526
|
+
},
|
|
527
|
+
body: JSON.stringify(encryptedRequest),
|
|
528
|
+
signal: options.signal
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (pendingMutation)
|
|
533
|
+
throw unknownMutationOutcome(requestId, error);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
if (response.ok)
|
|
537
|
+
this.setRoute("relay");
|
|
538
|
+
return {
|
|
539
|
+
response,
|
|
540
|
+
requestId,
|
|
541
|
+
encryptedRequest,
|
|
542
|
+
directDeliveryUncertain,
|
|
543
|
+
pendingMutation,
|
|
544
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
545
|
+
resumingMutation
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const operationUrl = token.authority
|
|
549
|
+
? `${token.authority.operationsUrl}/${operation}`
|
|
550
|
+
: `${this.serverUrl}/v1/authorities/${encodeURIComponent(token.collectionId)}/operations/${operation}`;
|
|
551
|
+
const operationBody = JSON.stringify(body);
|
|
552
|
+
const proof = token.authority
|
|
553
|
+
? await authorityProofHeaders(this.keyStore, token, "POST", operationUrl, operationBody, token.authority.accessToken)
|
|
554
|
+
: {};
|
|
555
|
+
let response;
|
|
556
|
+
try {
|
|
557
|
+
response = await fetchOperationRequest(operationUrl, token.authority?.accessToken ?? token.accessToken, proof, operationBody, options.signal);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (pendingMutation)
|
|
561
|
+
throw unknownMutationOutcome(requestId, error);
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
if (response.ok)
|
|
565
|
+
this.setRoute(token.authority ? "remote" : "relay");
|
|
566
|
+
return {
|
|
567
|
+
response,
|
|
568
|
+
requestId,
|
|
569
|
+
encryptedRequest,
|
|
570
|
+
pendingMutation,
|
|
571
|
+
...(pendingMutation ? { pendingMutationRecord: pending ?? this.storedPendingMutation(requestId) ?? undefined } : {}),
|
|
572
|
+
resumingMutation
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
directCapable(token) {
|
|
576
|
+
return tokenSupportsDirectAccess(token, this.directAccessMode);
|
|
577
|
+
}
|
|
578
|
+
hasResumableMutationTransport() { return this.currentToken() !== null; }
|
|
579
|
+
directEligible(token) {
|
|
580
|
+
return token !== null
|
|
581
|
+
&& this.directCapable(token)
|
|
582
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "disabled";
|
|
583
|
+
}
|
|
584
|
+
async shouldAttemptDirect(token) {
|
|
585
|
+
if (!this.directEligible(token))
|
|
586
|
+
return false;
|
|
587
|
+
if (this.directStatus === "available")
|
|
588
|
+
return true;
|
|
589
|
+
if (this.directStatus === "unavailable" && Date.now() < this.directRetryAt)
|
|
590
|
+
return false;
|
|
591
|
+
const permission = await localNetworkPermission();
|
|
592
|
+
if (permission === "denied") {
|
|
593
|
+
this.setDirectStatus("denied");
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if ((permission === "prompt" || permission === null)
|
|
597
|
+
&& this.storage.getItem(this.internals.directPreferenceKey()) !== "enabled") {
|
|
598
|
+
this.setDirectStatus("permission_required");
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
this.setDirectStatus("checking");
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
async probeDirectAccess(signal) {
|
|
605
|
+
this.setDirectStatus("checking");
|
|
606
|
+
if (await probeLoopbackAccess(this.loopbackUrl, OPERATION_TRANSPORT_PROTOCOL_VERSION, signal)) {
|
|
607
|
+
this.markDirectAvailable();
|
|
608
|
+
return "available";
|
|
609
|
+
}
|
|
610
|
+
if ((await localNetworkPermission()) === "denied")
|
|
611
|
+
return this.setDirectStatus("denied");
|
|
612
|
+
this.markDirectUnavailable();
|
|
613
|
+
return "unavailable";
|
|
614
|
+
}
|
|
615
|
+
markDirectAvailable() {
|
|
616
|
+
this.directFailures = 0;
|
|
617
|
+
this.directRetryAt = 0;
|
|
618
|
+
this.setDirectStatus("available");
|
|
619
|
+
}
|
|
620
|
+
markDirectUnavailable() {
|
|
621
|
+
this.directFailures += 1;
|
|
622
|
+
this.directRetryAt = Date.now() + Math.min(60_000, 1_000 * 2 ** (this.directFailures - 1));
|
|
623
|
+
this.setDirectStatus("unavailable");
|
|
624
|
+
}
|
|
625
|
+
setDirectStatus(status) {
|
|
626
|
+
if (this.directStatus !== status) {
|
|
627
|
+
this.directStatus = status;
|
|
628
|
+
this.emitConnection();
|
|
629
|
+
}
|
|
630
|
+
return status;
|
|
631
|
+
}
|
|
632
|
+
setRoute(route) {
|
|
633
|
+
if (this.currentRoute !== route) {
|
|
634
|
+
this.currentRoute = route;
|
|
635
|
+
this.emitConnection();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
emitConnection() { this.onChange(); }
|
|
639
|
+
invalidateRejectedAuthorization(rejected) {
|
|
640
|
+
const current = parseStored(this.storage.getItem(this.internals.tokenKey(this.collectionId)));
|
|
641
|
+
if (!current || !sameAuthorization(current, rejected))
|
|
642
|
+
return;
|
|
643
|
+
this.internals.removeToken(this.collectionId, current.keyHandle);
|
|
644
|
+
this.currentRoute = "relay";
|
|
645
|
+
this.directStatus = this.directAccessMode === "disabled" ? "disabled" : "unavailable";
|
|
646
|
+
this.emitConnection();
|
|
647
|
+
}
|
|
648
|
+
currentToken() {
|
|
649
|
+
return readStoredToken({
|
|
650
|
+
stored: this.storage.getItem(this.internals.tokenKey(this.collectionId)),
|
|
651
|
+
collectionId: this.collectionId,
|
|
652
|
+
relayEncryption: this.internals.relayEncryption,
|
|
653
|
+
invalidate: (keyHandle, reason = "invalid_stored_grant") => this.internals.removeToken(this.collectionId, keyHandle, reason),
|
|
654
|
+
directCapable: (token) => this.directCapable(token)
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async authorizedToken(options = {}) {
|
|
658
|
+
const token = this.currentToken();
|
|
659
|
+
if (!token)
|
|
660
|
+
return null;
|
|
661
|
+
if (token.expiresAt > Date.now() + 30_000)
|
|
662
|
+
return token;
|
|
663
|
+
if (!token.refreshToken || (token.refreshExpiresAt ?? 0) <= Date.now()) {
|
|
664
|
+
if (this.directCapable(token)) {
|
|
665
|
+
throw connectError("relay_authorization_expired", "Direct access is still available on this computer, but using the relay requires reconnecting this application.");
|
|
666
|
+
}
|
|
667
|
+
this.internals.removeToken(this.collectionId, token.keyHandle);
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
return this.refreshAuthorization(options);
|
|
671
|
+
}
|
|
672
|
+
refreshAuthorization(options = {}) {
|
|
673
|
+
return this.refreshOperations.run("refresh", options, this.timeouts.requestMs, (signal) => this.performRefresh(signal));
|
|
674
|
+
}
|
|
675
|
+
async performRefresh(signal) {
|
|
676
|
+
const leases = this.grantKeyLeases();
|
|
677
|
+
try {
|
|
678
|
+
const current = await retainCurrentGrantToken(() => this.currentToken(), leases, signal);
|
|
679
|
+
return await performAuthorizationRefresh({
|
|
680
|
+
current, serverUrl: this.serverUrl, keyStore: this.keyStore, signal,
|
|
681
|
+
currentToken: () => this.currentToken(),
|
|
682
|
+
directCapable: (token) => this.directCapable(token),
|
|
683
|
+
removeToken: (keyHandle) => this.internals.removeToken(this.collectionId, keyHandle),
|
|
684
|
+
storeTokenResponse: (body, clientId, keyHandle) => this.storeTokenResponse(body, clientId, keyHandle)
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
leases.release();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
storeTokenResponse(body, clientId, keyHandle) {
|
|
692
|
+
if (body.collection_id !== this.collectionId) {
|
|
693
|
+
throw connectError("collection_mismatch", "The refreshed authorization belongs to a different collection.");
|
|
694
|
+
}
|
|
695
|
+
const token = this.internals.storeTokenResponse(body, clientId, keyHandle);
|
|
696
|
+
this.currentRoute = token.authority ? "remote" : "relay";
|
|
697
|
+
this.directStatus = token.authority || this.directAccessMode === "disabled"
|
|
698
|
+
? "disabled"
|
|
699
|
+
: "unavailable";
|
|
700
|
+
this.emitConnection();
|
|
701
|
+
return token;
|
|
702
|
+
}
|
|
703
|
+
storedPendingMutations() {
|
|
704
|
+
return this.pendingMutationStore.list(this.currentToken()?.collectionId ?? null);
|
|
705
|
+
}
|
|
706
|
+
storedPendingMutation(requestId) {
|
|
707
|
+
return this.pendingMutationStore.find(this.currentToken()?.collectionId ?? null, requestId);
|
|
708
|
+
}
|
|
709
|
+
clearPendingMutation(requestId) {
|
|
710
|
+
const pending = this.pendingMutationStore.take(requestId);
|
|
711
|
+
if (pending?.keyHandle) {
|
|
712
|
+
this.internals.deleteGrantKeyWhenUnused(this.collectionId, pending.keyHandle);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
grantKeyLeases() {
|
|
716
|
+
return new GrantKeyLeaseSet(this.collectionId, (collectionId, keyHandle, signal) => this.internals.acquireGrantKeyLease(collectionId, keyHandle, signal));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function requireSyncAuthorization(token, collectionId, replicaId) {
|
|
720
|
+
if (token?.authority
|
|
721
|
+
&& token.collectionId === collectionId
|
|
722
|
+
&& token.authority.replicaId === replicaId)
|
|
723
|
+
return;
|
|
724
|
+
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
725
|
+
}
|