@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,457 @@
|
|
|
1
|
+
import { decodeFileFrame, FILE_PROTOCOL_VERSION, FILE_TRANSFER_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { decryptRelayResponse, encryptRelayRequest } from "./crypto.js";
|
|
3
|
+
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
4
|
+
import { GrantFileTransferCipher } from "./file-crypto.js";
|
|
5
|
+
import { GrantKeyLeaseSet, retainCurrentGrantToken } from "./grant-key-leases.js";
|
|
6
|
+
import { directFallbackStatus, loopbackRequest } from "./operation-helpers.js";
|
|
7
|
+
import { apiError, decodeJsonResponse } from "./runtime-utils.js";
|
|
8
|
+
import { isConnectorBusy, isExplicitConnectorBusyResponse, MAX_CONNECTOR_BUSY_RETRIES, waitForConnectorAvailability } from "./transient-retry.js";
|
|
9
|
+
/** Keeps grant-bound file crypto and loopback framing out of the operation transport. */
|
|
10
|
+
export class LocalFileTransport {
|
|
11
|
+
options;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
async control(token, method, path, input, signal) {
|
|
16
|
+
const leases = this.grantKeyLeases(token.collectionId);
|
|
17
|
+
try {
|
|
18
|
+
token = await this.retainEntryToken(token, leases, signal);
|
|
19
|
+
return await this.controlAttempt(token, method, path, input, signal, false, 0, leases);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
leases.release();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async controlAttempt(token, method, path, input, signal, freshReadRetried, connectorBusyRetries, leases) {
|
|
26
|
+
requireEncryption(token);
|
|
27
|
+
const controlInput = localFileControlInput(method, path, input);
|
|
28
|
+
let encryptedRequest = await this.encryptControl(token, controlInput);
|
|
29
|
+
let response;
|
|
30
|
+
const triedDirect = await this.options.shouldAttemptDirect(token);
|
|
31
|
+
if (triedDirect) {
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(`${this.options.loopbackUrl}/v1/files/control`, loopbackRequest({
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "content-type": "application/mdbase-connect+json" },
|
|
36
|
+
body: JSON.stringify(encryptedRequest),
|
|
37
|
+
signal
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
throw error;
|
|
43
|
+
this.options.onDirectUnavailable();
|
|
44
|
+
}
|
|
45
|
+
if (response && (!directFallbackStatus(response.status)
|
|
46
|
+
|| await isExplicitConnectorBusyResponse(response))) {
|
|
47
|
+
if (response.ok)
|
|
48
|
+
this.options.onDirectAvailable();
|
|
49
|
+
return this.decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried, connectorBusyRetries, leases);
|
|
50
|
+
}
|
|
51
|
+
if (response)
|
|
52
|
+
this.options.onDirectUnavailable();
|
|
53
|
+
}
|
|
54
|
+
if (triedDirect && method === "GET") {
|
|
55
|
+
token = token.expiresAt > Date.now() + 30_000
|
|
56
|
+
? token
|
|
57
|
+
: await this.options.refreshAuthorization(signal);
|
|
58
|
+
token = await this.retainEntryToken(token, leases, signal);
|
|
59
|
+
requireLocalFileToken(token);
|
|
60
|
+
encryptedRequest = await this.encryptControl(token, controlInput);
|
|
61
|
+
}
|
|
62
|
+
response = await this.sendRelayControl(token, encryptedRequest, signal);
|
|
63
|
+
if (await refreshableBindingFailure(response, token)) {
|
|
64
|
+
token = await this.options.refreshAuthorization(signal);
|
|
65
|
+
token = await this.retainEntryToken(token, leases, signal);
|
|
66
|
+
requireLocalFileToken(token);
|
|
67
|
+
encryptedRequest = await this.encryptControl(token, controlInput);
|
|
68
|
+
response = await this.sendRelayControl(token, encryptedRequest, signal);
|
|
69
|
+
}
|
|
70
|
+
if (response.ok)
|
|
71
|
+
this.options.onRelayAvailable();
|
|
72
|
+
return this.decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried, connectorBusyRetries, leases);
|
|
73
|
+
}
|
|
74
|
+
async decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried, connectorBusyRetries, leases) {
|
|
75
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file control response.");
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const error = apiError(body, "operation_failed", "Collection file request failed.", response.status);
|
|
78
|
+
if (isConnectorBusy(error)
|
|
79
|
+
&& connectorBusyRetries < MAX_CONNECTOR_BUSY_RETRIES) {
|
|
80
|
+
await waitForConnectorAvailability(connectorBusyRetries, signal);
|
|
81
|
+
return this.controlAttempt(token, method, path, input, signal, freshReadRetried, connectorBusyRetries + 1, leases);
|
|
82
|
+
}
|
|
83
|
+
if (error.code === "fresh_request_required"
|
|
84
|
+
&& method === "GET"
|
|
85
|
+
&& !freshReadRetried) {
|
|
86
|
+
return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const encryptedResponse = body?.envelope;
|
|
91
|
+
if (!encryptedResponse) {
|
|
92
|
+
throw connectError("invalid_encrypted_response", "The connector omitted its encrypted file response.");
|
|
93
|
+
}
|
|
94
|
+
const decrypted = await decryptRelayResponse(this.options.keyStore, token.keyHandle, relayBinding(token), encryptedRequest, encryptedResponse);
|
|
95
|
+
if (!decrypted.ok
|
|
96
|
+
&& decrypted.problem.code === "fresh_request_required"
|
|
97
|
+
&& method === "GET"
|
|
98
|
+
&& !freshReadRetried) {
|
|
99
|
+
return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
|
|
100
|
+
}
|
|
101
|
+
if (!decrypted.ok) {
|
|
102
|
+
const error = encryptedProblem(decrypted.problem);
|
|
103
|
+
if (isConnectorBusy(error)
|
|
104
|
+
&& connectorBusyRetries < MAX_CONNECTOR_BUSY_RETRIES) {
|
|
105
|
+
await waitForConnectorAvailability(connectorBusyRetries, signal);
|
|
106
|
+
return this.controlAttempt(token, method, path, input, signal, freshReadRetried, connectorBusyRetries + 1, leases);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
return decrypted.result;
|
|
111
|
+
}
|
|
112
|
+
async uploadChunk(session, chunkIndex, bytes, signal, authorizedToken) {
|
|
113
|
+
const token = authorizedToken ?? await this.localFileToken(session, "upload", signal);
|
|
114
|
+
const leases = this.grantKeyLeases(token.collectionId);
|
|
115
|
+
try {
|
|
116
|
+
const current = await this.retainEntryToken(token, leases, signal);
|
|
117
|
+
validateLocalFileToken(current, session, "upload");
|
|
118
|
+
return await this.uploadChunkWithLease(current, leases, session, chunkIndex, bytes, signal);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
leases.release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async uploadChunkWithLease(token, leases, session, chunkIndex, bytes, signal) {
|
|
125
|
+
let deliveryToken = token;
|
|
126
|
+
let encoded = await encryptedUploadChunk(this.options.keyStore, deliveryToken, session, chunkIndex, bytes);
|
|
127
|
+
let response;
|
|
128
|
+
if (await this.options.shouldAttemptDirect(deliveryToken)) {
|
|
129
|
+
try {
|
|
130
|
+
response = await fetch(`${this.options.loopbackUrl}/v1/files/upload`, loopbackRequest(fileUploadRequest(encoded, signal)));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
throw error;
|
|
135
|
+
this.options.onDirectUnavailable();
|
|
136
|
+
}
|
|
137
|
+
if (response && (!directFallbackStatus(response.status)
|
|
138
|
+
|| await isExplicitConnectorBusyResponse(response))) {
|
|
139
|
+
if (response.ok)
|
|
140
|
+
this.options.onDirectAvailable();
|
|
141
|
+
return requireSuccessfulChunkResponse(response, "Encrypted file chunk upload failed.");
|
|
142
|
+
}
|
|
143
|
+
if (response)
|
|
144
|
+
this.options.onDirectUnavailable();
|
|
145
|
+
}
|
|
146
|
+
response = await this.sendRelayUpload(deliveryToken, encoded, signal);
|
|
147
|
+
if (response.status === 401 && deliveryToken.refreshToken) {
|
|
148
|
+
deliveryToken = await this.options.refreshAuthorization(signal);
|
|
149
|
+
deliveryToken = await this.retainEntryToken(deliveryToken, leases, signal);
|
|
150
|
+
requireLocalFileToken(deliveryToken);
|
|
151
|
+
encoded = await encryptedUploadChunk(this.options.keyStore, deliveryToken, session, chunkIndex, bytes);
|
|
152
|
+
response = await this.sendRelayUpload(deliveryToken, encoded, signal);
|
|
153
|
+
}
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file upload response.");
|
|
156
|
+
throw apiError(body, "operation_failed", "Encrypted file chunk upload failed.", response.status);
|
|
157
|
+
}
|
|
158
|
+
this.options.onRelayAvailable();
|
|
159
|
+
}
|
|
160
|
+
async downloadChunk(session, chunkIndex, signal, authorizedToken) {
|
|
161
|
+
let token = authorizedToken ?? await this.localFileToken(session, "download", signal);
|
|
162
|
+
const leases = this.grantKeyLeases(token.collectionId);
|
|
163
|
+
try {
|
|
164
|
+
token = await this.retainEntryToken(token, leases, signal);
|
|
165
|
+
validateLocalFileToken(token, session, "download");
|
|
166
|
+
return await this.downloadChunkWithLease(token, leases, session, chunkIndex, signal);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
leases.release();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async downloadChunkWithLease(token, leases, session, chunkIndex, signal) {
|
|
173
|
+
let response;
|
|
174
|
+
if (await this.options.shouldAttemptDirect(token)) {
|
|
175
|
+
try {
|
|
176
|
+
response = await fetch(`${this.options.loopbackUrl}/v1/files/download/${encodeURIComponent(token.grantId)}/${encodeURIComponent(session.transfer_id)}/${chunkIndex}`, loopbackRequest({ method: "GET", signal }));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (signal?.aborted)
|
|
180
|
+
throw error;
|
|
181
|
+
this.options.onDirectUnavailable();
|
|
182
|
+
}
|
|
183
|
+
if (response && (!directFallbackStatus(response.status)
|
|
184
|
+
|| await isExplicitConnectorBusyResponse(response))) {
|
|
185
|
+
if (response.ok)
|
|
186
|
+
this.options.onDirectAvailable();
|
|
187
|
+
return this.decryptDownloadResponse(token, session, chunkIndex, response);
|
|
188
|
+
}
|
|
189
|
+
if (response)
|
|
190
|
+
this.options.onDirectUnavailable();
|
|
191
|
+
}
|
|
192
|
+
response = await this.sendRelayDownload(token, session, chunkIndex, signal);
|
|
193
|
+
if (response.status === 401 && token.refreshToken) {
|
|
194
|
+
token = await this.options.refreshAuthorization(signal);
|
|
195
|
+
token = await this.retainEntryToken(token, leases, signal);
|
|
196
|
+
requireLocalFileToken(token);
|
|
197
|
+
response = await this.sendRelayDownload(token, session, chunkIndex, signal);
|
|
198
|
+
}
|
|
199
|
+
if (response.ok)
|
|
200
|
+
this.options.onRelayAvailable();
|
|
201
|
+
return this.decryptDownloadResponse(token, session, chunkIndex, response);
|
|
202
|
+
}
|
|
203
|
+
async decryptDownloadResponse(token, session, chunkIndex, response) {
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file download response.");
|
|
206
|
+
throw apiError(body, "operation_failed", "Encrypted file chunk download failed.", response.status);
|
|
207
|
+
}
|
|
208
|
+
if (response.headers.get("content-type")?.split(";", 1)[0]
|
|
209
|
+
!== "application/mdbase-connect-file") {
|
|
210
|
+
throw connectError("invalid_operation_response", "The connector returned an invalid file response media type.");
|
|
211
|
+
}
|
|
212
|
+
const encoded = new Uint8Array(await response.arrayBuffer());
|
|
213
|
+
let plaintext;
|
|
214
|
+
try {
|
|
215
|
+
validateDownloadFrame(session, chunkIndex, encoded);
|
|
216
|
+
plaintext = await (await fileCipher(this.options.keyStore, token, session))
|
|
217
|
+
.decryptChunk(encoded);
|
|
218
|
+
}
|
|
219
|
+
catch (cause) {
|
|
220
|
+
if (cause instanceof MdbaseConnectError)
|
|
221
|
+
throw cause;
|
|
222
|
+
throw connectError("invalid_operation_response", "The connector returned an invalid encrypted file frame.", { cause });
|
|
223
|
+
}
|
|
224
|
+
if (plaintext.byteLength !== expectedFramedChunkLength(session, chunkIndex)) {
|
|
225
|
+
throw connectError("invalid_operation_response", "The connector returned a truncated file chunk.");
|
|
226
|
+
}
|
|
227
|
+
return plaintext;
|
|
228
|
+
}
|
|
229
|
+
encryptControl(token, input) {
|
|
230
|
+
requireEncryption(token);
|
|
231
|
+
return encryptRelayRequest(this.options.keyStore, token.keyHandle, relayBinding(token), "file_control", input);
|
|
232
|
+
}
|
|
233
|
+
sendRelayControl(token, encryptedRequest, signal) {
|
|
234
|
+
return fetch(`${this.options.serverUrl}/v1/authorities/${encodeURIComponent(token.collectionId)}/files/control`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
238
|
+
"content-type": "application/json"
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(encryptedRequest),
|
|
241
|
+
signal
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
sendRelayUpload(token, encoded, signal) {
|
|
245
|
+
return fetch(`${this.options.serverUrl}/v1/authorities/${encodeURIComponent(token.collectionId)}/files/upload`, {
|
|
246
|
+
...fileUploadRequest(encoded, signal),
|
|
247
|
+
headers: {
|
|
248
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
249
|
+
"content-type": "application/mdbase-connect-file"
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
sendRelayDownload(token, session, chunkIndex, signal) {
|
|
254
|
+
return fetch(`${this.options.serverUrl}/v1/authorities/${encodeURIComponent(token.collectionId)}/files/download/${encodeURIComponent(session.transfer_id)}/${chunkIndex}`, {
|
|
255
|
+
method: "GET",
|
|
256
|
+
headers: { authorization: `Bearer ${token.accessToken}` },
|
|
257
|
+
signal
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async localFileToken(session, direction, signal) {
|
|
261
|
+
const token = await this.options.authorizedToken(signal);
|
|
262
|
+
validateLocalFileToken(token, session, direction);
|
|
263
|
+
return token;
|
|
264
|
+
}
|
|
265
|
+
grantKeyLeases(collectionId) {
|
|
266
|
+
return new GrantKeyLeaseSet(collectionId, this.options.acquireGrantKeyLease
|
|
267
|
+
?? (() => Promise.resolve(() => undefined)));
|
|
268
|
+
}
|
|
269
|
+
async retainEntryToken(snapshot, leases, signal) {
|
|
270
|
+
if (!this.options.currentToken) {
|
|
271
|
+
await leases.retain(snapshot, signal);
|
|
272
|
+
return snapshot;
|
|
273
|
+
}
|
|
274
|
+
const current = await retainCurrentGrantToken(this.options.currentToken, leases, signal);
|
|
275
|
+
if (current)
|
|
276
|
+
return current;
|
|
277
|
+
throw connectError("not_authorized", "Reconnect this application to continue.");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function validateLocalFileToken(token, session, direction) {
|
|
281
|
+
if (!token || token.authority || !token.fileCapability || !token.encryption
|
|
282
|
+
|| !token.grantId || !token.keyHandle
|
|
283
|
+
|| session.direction !== direction
|
|
284
|
+
|| session.protection !== "grant_aead_v1") {
|
|
285
|
+
throw connectError("not_authorized", "This connection cannot use encrypted local file delivery.");
|
|
286
|
+
}
|
|
287
|
+
framedChunkSize(session);
|
|
288
|
+
}
|
|
289
|
+
function requireLocalFileToken(token) {
|
|
290
|
+
if (token.authority || !token.fileCapability) {
|
|
291
|
+
throw connectError("not_authorized", "This connection cannot use encrypted local file delivery.");
|
|
292
|
+
}
|
|
293
|
+
requireEncryption(token);
|
|
294
|
+
}
|
|
295
|
+
async function encryptedUploadChunk(keyStore, token, session, chunkIndex, bytes) {
|
|
296
|
+
const header = fileFrameHeader(token, session, chunkIndex, bytes.byteLength);
|
|
297
|
+
return (await fileCipher(keyStore, token, session))
|
|
298
|
+
.encryptChunk("upload_chunk", header, bytes);
|
|
299
|
+
}
|
|
300
|
+
function fileUploadRequest(encoded, signal) {
|
|
301
|
+
return {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: { "content-type": "application/mdbase-connect-file" },
|
|
304
|
+
body: Uint8Array.from(encoded).buffer,
|
|
305
|
+
signal
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function requireSuccessfulChunkResponse(response, message) {
|
|
309
|
+
if (response.ok)
|
|
310
|
+
return;
|
|
311
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file transfer response.");
|
|
312
|
+
throw apiError(body, "operation_failed", message, response.status);
|
|
313
|
+
}
|
|
314
|
+
async function refreshableBindingFailure(response, token) {
|
|
315
|
+
if (!token.refreshToken)
|
|
316
|
+
return false;
|
|
317
|
+
if (response.status === 401)
|
|
318
|
+
return true;
|
|
319
|
+
if (response.status !== 409)
|
|
320
|
+
return false;
|
|
321
|
+
const body = await decodeJsonResponse(response.clone(), "invalid_operation_response", "The collection authority returned an invalid file response.").catch(() => null);
|
|
322
|
+
return body?.error?.code === "encryption_binding_stale";
|
|
323
|
+
}
|
|
324
|
+
function requireEncryption(token) {
|
|
325
|
+
if (!token.encryption || !token.grantId || !token.keyHandle) {
|
|
326
|
+
throw connectError("missing_grant_key", "Reconnect this application to restore encrypted file access.");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function relayBinding(token) {
|
|
330
|
+
requireEncryption(token);
|
|
331
|
+
return {
|
|
332
|
+
grantId: token.grantId,
|
|
333
|
+
applicationId: token.clientId,
|
|
334
|
+
encryption: token.encryption
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function fileCipher(keyStore, token, session) {
|
|
338
|
+
requireEncryption(token);
|
|
339
|
+
return GrantFileTransferCipher.open(keyStore, token.keyHandle, {
|
|
340
|
+
...relayBinding(token),
|
|
341
|
+
authorityId: token.encryption.connector_id,
|
|
342
|
+
transferId: session.transfer_id,
|
|
343
|
+
direction: session.direction
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function fileFrameHeader(token, session, chunkIndex, plaintextLength) {
|
|
347
|
+
requireEncryption(token);
|
|
348
|
+
const chunkSize = framedChunkSize(session);
|
|
349
|
+
const expectedLength = expectedFramedChunkLength(session, chunkIndex);
|
|
350
|
+
if (plaintextLength !== expectedLength) {
|
|
351
|
+
throw connectError("invalid_request", "The file chunk has the wrong byte length.");
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
protocol_version: FILE_TRANSFER_PROTOCOL_VERSION,
|
|
355
|
+
protection: "grant_aead_v1",
|
|
356
|
+
grant_id: token.grantId,
|
|
357
|
+
authority_id: token.encryption.connector_id,
|
|
358
|
+
collection_id: token.collectionId,
|
|
359
|
+
transfer_id: session.transfer_id,
|
|
360
|
+
direction: session.direction,
|
|
361
|
+
chunk_size: chunkSize,
|
|
362
|
+
chunk_index: chunkIndex,
|
|
363
|
+
offset: chunkIndex * chunkSize,
|
|
364
|
+
plaintext_length: plaintextLength,
|
|
365
|
+
total_size: session.total_size,
|
|
366
|
+
scope_epoch: token.encryption.scope_epoch,
|
|
367
|
+
key_id: token.encryption.key_id
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function validateDownloadFrame(session, chunkIndex, encoded) {
|
|
371
|
+
const frame = decodeFileFrame(encoded);
|
|
372
|
+
const expectedLength = expectedFramedChunkLength(session, chunkIndex);
|
|
373
|
+
const chunkSize = framedChunkSize(session);
|
|
374
|
+
if (frame.kind !== "download_chunk"
|
|
375
|
+
|| frame.header.chunk_index !== chunkIndex
|
|
376
|
+
|| frame.header.chunk_size !== chunkSize
|
|
377
|
+
|| frame.header.offset !== chunkIndex * chunkSize
|
|
378
|
+
|| frame.header.plaintext_length !== expectedLength
|
|
379
|
+
|| frame.header.total_size !== session.total_size) {
|
|
380
|
+
throw connectError("invalid_operation_response", "The connector returned a file frame for a different chunk.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function encryptedProblem(problem) {
|
|
384
|
+
return serverConnectError(problem.code === "unknown" ? problem.server_code : problem.code, problem.message, {
|
|
385
|
+
details: problem.details,
|
|
386
|
+
operationOutcome: problem.operation_outcome ?? "rejected",
|
|
387
|
+
traceId: problem.trace_id
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
function localFileControlInput(method, path, input) {
|
|
391
|
+
if (method === "GET" && (path === "" || path.startsWith("?"))) {
|
|
392
|
+
const query = new URLSearchParams(path.startsWith("?") ? path.slice(1) : path);
|
|
393
|
+
const limit = query.get("limit");
|
|
394
|
+
return {
|
|
395
|
+
protocol_version: FILE_PROTOCOL_VERSION,
|
|
396
|
+
type: "list_files",
|
|
397
|
+
...(query.get("folder") ? { folder: query.get("folder") } : {}),
|
|
398
|
+
...(query.get("after") ? { after: query.get("after") } : {}),
|
|
399
|
+
...(limit ? { limit: Number(limit) } : {})
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const segments = path.split("/");
|
|
403
|
+
if (method === "GET" && segments.length === 2 && segments[0] === "transfers") {
|
|
404
|
+
return transferControl("get_file_transfer_status", segments[1]);
|
|
405
|
+
}
|
|
406
|
+
if (method === "DELETE" && segments.length === 2 && segments[0] === "transfers") {
|
|
407
|
+
return transferControl("abort_file_transfer", segments[1]);
|
|
408
|
+
}
|
|
409
|
+
if (method === "POST" && isRecord(input)) {
|
|
410
|
+
const expectedType = path === "uploads"
|
|
411
|
+
? "open_file_upload"
|
|
412
|
+
: path === "downloads"
|
|
413
|
+
? "open_file_download"
|
|
414
|
+
: segments.length === 3 && segments[0] === "uploads" && segments[2] === "commit"
|
|
415
|
+
? "commit_file_upload"
|
|
416
|
+
: segments.length === 2 && segments[1] === "move"
|
|
417
|
+
&& input.file_id === decodeURIComponent(segments[0])
|
|
418
|
+
? "move_file"
|
|
419
|
+
: segments.length === 2 && segments[1] === "delete"
|
|
420
|
+
&& input.file_id === decodeURIComponent(segments[0])
|
|
421
|
+
? "delete_file"
|
|
422
|
+
: null;
|
|
423
|
+
if (expectedType && input.type === expectedType)
|
|
424
|
+
return { ...input };
|
|
425
|
+
}
|
|
426
|
+
throw connectError("invalid_request", "The file control request path is invalid.");
|
|
427
|
+
}
|
|
428
|
+
function transferControl(type, encodedId) {
|
|
429
|
+
return {
|
|
430
|
+
protocol_version: FILE_PROTOCOL_VERSION,
|
|
431
|
+
type,
|
|
432
|
+
transfer_id: decodeURIComponent(encodedId)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function framedChunkSize(session) {
|
|
436
|
+
if (session.strategy.kind !== "framed_chunks"
|
|
437
|
+
|| !Number.isSafeInteger(session.strategy.chunk_size)
|
|
438
|
+
|| session.strategy.chunk_size <= 0) {
|
|
439
|
+
throw connectError("invalid_operation_response", "The connector returned an invalid framed transfer strategy.");
|
|
440
|
+
}
|
|
441
|
+
return session.strategy.chunk_size;
|
|
442
|
+
}
|
|
443
|
+
function expectedFramedChunkLength(session, chunkIndex) {
|
|
444
|
+
const chunkSize = framedChunkSize(session);
|
|
445
|
+
if (!Number.isSafeInteger(chunkIndex) || chunkIndex < 0
|
|
446
|
+
|| !Number.isSafeInteger(session.total_size) || session.total_size < 0) {
|
|
447
|
+
throw connectError("invalid_request", "The file chunk index is invalid.");
|
|
448
|
+
}
|
|
449
|
+
const offset = chunkIndex * chunkSize;
|
|
450
|
+
if (!Number.isSafeInteger(offset) || offset >= session.total_size) {
|
|
451
|
+
throw connectError("invalid_request", "The file chunk index is outside the transfer.");
|
|
452
|
+
}
|
|
453
|
+
return Math.min(chunkSize, session.total_size - offset);
|
|
454
|
+
}
|
|
455
|
+
function isRecord(value) {
|
|
456
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
457
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { MdbaseConnectOptions } from "./connect-options.js";
|
|
3
|
+
import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthorizationResult, type MdbaseAuthorizeOptions, type MdbaseConnectEnvironment } from "./connection.js";
|
|
4
|
+
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
5
|
+
import { type AuthorizationProblemCode, type ConnectOutcome, type RegistrationProblemCode } from "./outcomes.js";
|
|
6
|
+
import { MdbaseApplicationSession, type MdbaseApplicationSessionOptions } from "./application-session.js";
|
|
7
|
+
import type { MdbaseUnavailableReason } from "./session.js";
|
|
8
|
+
import type { Application } from "./internal-types.js";
|
|
9
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
10
|
+
export declare class MdbaseConnect<Frontmatter extends JsonObject = JsonObject> {
|
|
11
|
+
private readonly internals;
|
|
12
|
+
constructor(options: MdbaseConnectOptions);
|
|
13
|
+
register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
|
|
14
|
+
authorize(options?: MdbaseAuthorizeOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, AuthorizationProblemCode>>;
|
|
15
|
+
/** The ordinary application lifecycle entry point. */
|
|
16
|
+
application(options: MdbaseApplicationSessionOptions): MdbaseApplicationSession<Frontmatter>;
|
|
17
|
+
manifest(options?: ConnectRequestOptions): Promise<ConnectOutcome<import("./application-contract.js").MdbaseApplicationManifest, RegistrationProblemCode>>;
|
|
18
|
+
environment(): MdbaseConnectEnvironment;
|
|
19
|
+
completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
|
|
20
|
+
connections(): MdbaseConnectionInfo[];
|
|
21
|
+
connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
|
|
22
|
+
unavailableReason(collectionId: string): MdbaseUnavailableReason | null;
|
|
23
|
+
connectionApplicationId(collectionId: string): string | null;
|
|
24
|
+
onConnectionsChange(listener: (connections: MdbaseConnectionInfo[]) => void): () => void;
|
|
25
|
+
forgetAll(): void;
|
|
26
|
+
/** Forget a public TOFU pin after independently verifying a replaced connector. */
|
|
27
|
+
forgetConnectorIdentity(connectorId: string): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MdbaseConnectInternals } from "./connect.js";
|
|
2
|
+
import { defaultCallbackUrl } from "./runtime-utils.js";
|
|
3
|
+
import { AUTHORIZATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome } from "./outcomes.js";
|
|
4
|
+
import { MdbaseApplicationSession } from "./application-session.js";
|
|
5
|
+
export class MdbaseConnect {
|
|
6
|
+
internals;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.internals = new MdbaseConnectInternals(options);
|
|
9
|
+
}
|
|
10
|
+
register(options) {
|
|
11
|
+
return captureConnectOutcome(() => this.internals.register(options), REGISTRATION_PROBLEM_CODES);
|
|
12
|
+
}
|
|
13
|
+
authorize(options = {}) {
|
|
14
|
+
return captureConnectOutcome(() => this.internals.authorize(options), AUTHORIZATION_PROBLEM_CODES);
|
|
15
|
+
}
|
|
16
|
+
/** The ordinary application lifecycle entry point. */
|
|
17
|
+
application(options) {
|
|
18
|
+
return new MdbaseApplicationSession(this, options, this.internals.timeouts);
|
|
19
|
+
}
|
|
20
|
+
manifest(options) {
|
|
21
|
+
return captureConnectOutcome(() => this.internals.manifestDeclaration(options), REGISTRATION_PROBLEM_CODES);
|
|
22
|
+
}
|
|
23
|
+
environment() {
|
|
24
|
+
return this.internals.environment();
|
|
25
|
+
}
|
|
26
|
+
completeAuthorization(callbackUrl, options) {
|
|
27
|
+
return captureConnectOutcome(() => this.internals.completeAuthorization(callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl), options), AUTHORIZATION_PROBLEM_CODES);
|
|
28
|
+
}
|
|
29
|
+
connections() {
|
|
30
|
+
return this.internals.connections();
|
|
31
|
+
}
|
|
32
|
+
connection(collectionId) {
|
|
33
|
+
return this.internals.connection(collectionId);
|
|
34
|
+
}
|
|
35
|
+
unavailableReason(collectionId) {
|
|
36
|
+
return this.internals.unavailableReason(collectionId);
|
|
37
|
+
}
|
|
38
|
+
connectionApplicationId(collectionId) {
|
|
39
|
+
return this.internals.connectionApplicationId(collectionId);
|
|
40
|
+
}
|
|
41
|
+
onConnectionsChange(listener) {
|
|
42
|
+
return this.internals.onConnectionsChange(listener);
|
|
43
|
+
}
|
|
44
|
+
forgetAll() {
|
|
45
|
+
for (const connection of this.connections()) {
|
|
46
|
+
this.connection(connection.collectionId)?.forget();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Forget a public TOFU pin after independently verifying a replaced connector. */
|
|
50
|
+
forgetConnectorIdentity(connectorId) {
|
|
51
|
+
this.internals.forgetConnectorIdentity(connectorId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
2
|
+
export interface MdbaseNotificationRegistrationOptions extends ConnectRequestOptions {
|
|
3
|
+
serviceWorker: ServiceWorkerRegistration;
|
|
4
|
+
/** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
|
|
5
|
+
criteria?: string[];
|
|
6
|
+
/** Stable per-installation ID. The SDK persists one when omitted. */
|
|
7
|
+
installationId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MdbaseNotificationRegistration {
|
|
10
|
+
channelId: string;
|
|
11
|
+
installationId: string;
|
|
12
|
+
criteria: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface MdbaseNativeNotificationRegistrationOptions extends ConnectRequestOptions {
|
|
15
|
+
/** Current FCM registration token. Refresh by calling this method again. */
|
|
16
|
+
token: string;
|
|
17
|
+
/** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
|
|
18
|
+
criteria?: string[];
|
|
19
|
+
/** Stable per-installation ID. The SDK persists one when omitted. */
|
|
20
|
+
installationId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MdbaseNativeNotificationRegistration extends MdbaseNotificationRegistration {
|
|
23
|
+
transport: "fcm";
|
|
24
|
+
}
|
|
25
|
+
export interface MdbaseNativeNotificationData {
|
|
26
|
+
type: "mdbase.notification";
|
|
27
|
+
version: 1;
|
|
28
|
+
signal_id: string;
|
|
29
|
+
criterion_id: string;
|
|
30
|
+
cursor: string;
|
|
31
|
+
}
|
|
32
|
+
export interface MdbasePushPayload {
|
|
33
|
+
type: "mdbase.notification";
|
|
34
|
+
version: 1;
|
|
35
|
+
signal_id: string;
|
|
36
|
+
criterion_id: string;
|
|
37
|
+
cursor: string;
|
|
38
|
+
presentation: {
|
|
39
|
+
title: string;
|
|
40
|
+
body?: string;
|
|
41
|
+
tag?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function parseMdbasePushPayload(value: unknown): MdbasePushPayload;
|
|
45
|
+
/** Parse the string-valued data attached to an APNs/FCM notification. */
|
|
46
|
+
export declare function parseMdbaseNativeNotificationData(value: unknown): MdbaseNativeNotificationData;
|
|
47
|
+
/** Display a validated mdbase push from a service worker `push` handler. */
|
|
48
|
+
export declare function showMdbasePushNotification(registration: Pick<ServiceWorkerRegistration, "showNotification">, value: unknown): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { connectError } from "./errors.js";
|
|
2
|
+
export function parseMdbasePushPayload(value) {
|
|
3
|
+
if (!value || typeof value !== "object") {
|
|
4
|
+
throw connectError("invalid_push_payload", "The push payload is not an object.");
|
|
5
|
+
}
|
|
6
|
+
const payload = value;
|
|
7
|
+
if (payload.type !== "mdbase.notification"
|
|
8
|
+
|| payload.version !== 1
|
|
9
|
+
|| typeof payload.signal_id !== "string"
|
|
10
|
+
|| typeof payload.criterion_id !== "string"
|
|
11
|
+
|| typeof payload.cursor !== "string"
|
|
12
|
+
|| !payload.presentation
|
|
13
|
+
|| typeof payload.presentation.title !== "string") {
|
|
14
|
+
throw connectError("invalid_push_payload", "The push payload is not an mdbase notification.");
|
|
15
|
+
}
|
|
16
|
+
return payload;
|
|
17
|
+
}
|
|
18
|
+
/** Parse the string-valued data attached to an APNs/FCM notification. */
|
|
19
|
+
export function parseMdbaseNativeNotificationData(value) {
|
|
20
|
+
if (!value || typeof value !== "object") {
|
|
21
|
+
throw connectError("invalid_push_payload", "The native notification data is not an object.");
|
|
22
|
+
}
|
|
23
|
+
const data = value;
|
|
24
|
+
if (data.type !== "mdbase.notification"
|
|
25
|
+
|| (data.version !== 1 && data.version !== "1")
|
|
26
|
+
|| typeof data.signal_id !== "string"
|
|
27
|
+
|| typeof data.criterion_id !== "string"
|
|
28
|
+
|| typeof data.cursor !== "string") {
|
|
29
|
+
throw connectError("invalid_push_payload", "The native notification data is not an mdbase notification.");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
type: "mdbase.notification",
|
|
33
|
+
version: 1,
|
|
34
|
+
signal_id: data.signal_id,
|
|
35
|
+
criterion_id: data.criterion_id,
|
|
36
|
+
cursor: data.cursor
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Display a validated mdbase push from a service worker `push` handler. */
|
|
40
|
+
export function showMdbasePushNotification(registration, value) {
|
|
41
|
+
const payload = parseMdbasePushPayload(value);
|
|
42
|
+
return registration.showNotification(payload.presentation.title, {
|
|
43
|
+
...(payload.presentation.body ? { body: payload.presentation.body } : {}),
|
|
44
|
+
...(payload.presentation.tag ? { tag: payload.presentation.tag } : {}),
|
|
45
|
+
data: {
|
|
46
|
+
type: payload.type,
|
|
47
|
+
signal_id: payload.signal_id,
|
|
48
|
+
criterion_id: payload.criterion_id,
|
|
49
|
+
cursor: payload.cursor
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CollectionOperation, type ConnectProblem, type EncryptedRelayOperationRequest } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { MdbaseConnectError } from "./errors.js";
|
|
3
|
+
import type { StoredToken } from "./internal-types.js";
|
|
4
|
+
import type { DeleteInput, DeletePreflightResult, MutationEstimate, RenameInput, RenamePreflightResult } from "./operation-types.js";
|
|
5
|
+
export type LoopbackRequestInit = RequestInit & {
|
|
6
|
+
targetAddressSpace?: "loopback";
|
|
7
|
+
};
|
|
8
|
+
export declare function loopbackRequest(init: RequestInit): LoopbackRequestInit;
|
|
9
|
+
export declare function localNetworkPermission(): Promise<PermissionState | null>;
|
|
10
|
+
export declare function directFallbackStatus(status: number): boolean;
|
|
11
|
+
export declare function isMutation(operation: CollectionOperation, input?: unknown): boolean;
|
|
12
|
+
export declare function withOperationDeadline(request: EncryptedRelayOperationRequest, deadlineUnixMs?: number): EncryptedRelayOperationRequest;
|
|
13
|
+
export declare function uniqueOperations(operations: CollectionOperation[]): CollectionOperation[];
|
|
14
|
+
export declare function sameAuthorization(left: StoredToken, right: StoredToken): boolean;
|
|
15
|
+
export declare function throwIfCancelled(signal?: AbortSignal): void;
|
|
16
|
+
export declare function operationTransportError(error: unknown, signal: AbortSignal | undefined, pendingRequestId: string | undefined, unavailableCode: "hosted_provider_unavailable" | "relay_unavailable"): Error;
|
|
17
|
+
export declare function encryptedOperationError(problem: ConnectProblem): MdbaseConnectError;
|
|
18
|
+
export declare function fetchOperationRequest(url: string, accessToken: string, proof: Record<string, string>, body: string, signal?: AbortSignal): Promise<Response>;
|
|
19
|
+
export declare function isCancellation(error: unknown, signal?: AbortSignal): boolean;
|
|
20
|
+
export declare function assertRenamePreview(input: RenameInput, preview: RenamePreflightResult): void;
|
|
21
|
+
export declare function assertDeletePreview(input: DeleteInput, preview: DeletePreflightResult): void;
|
|
22
|
+
export declare function renameEstimate(input: RenameInput, preview: RenamePreflightResult): MutationEstimate;
|
|
23
|
+
export declare function deleteEstimate(preview: DeletePreflightResult): MutationEstimate;
|
|
24
|
+
export declare function operationFingerprint(operation: CollectionOperation, input: unknown): Promise<string>;
|
|
25
|
+
export declare function canonicalJson(value: unknown): string;
|
|
26
|
+
export declare function sortJson(value: unknown): unknown;
|
|
27
|
+
export declare function unknownMutationOutcome(requestId: string, cause: unknown): MdbaseConnectError;
|