@mdbase-dev/connect 0.1.0-beta.55 → 0.1.0-beta.56
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 +1 -1
- package/dist/authority-sync-request.d.ts +3 -0
- package/dist/authority-sync-request.js +20 -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/connection-transport-internals.d.ts +22 -0
- package/dist/connection-transport-internals.js +1 -0
- package/dist/connection-transport.d.ts +2 -22
- package/dist/connection-transport.js +21 -21
- package/dist/fresh-encrypted-request.d.ts +4 -0
- package/dist/fresh-encrypted-request.js +28 -0
- package/dist/local-file-transport.d.ts +1 -0
- package/dist/local-file-transport.js +28 -5
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-fZeP3Z5L7qaOmLPO0ST72DyQgwJCq0XaamP4LFUHOTWxA2MTeF4eerxdIfULZkEq
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GrantKeyStore } from "./crypto.js";
|
|
2
|
+
import type { StoredToken } from "./internal-types.js";
|
|
3
|
+
import type { ResolvedConnectTimeouts } from "./request-budget.js";
|
|
4
|
+
export interface ConnectionTransportInternals {
|
|
5
|
+
readonly relayEncryption: "required" | "disabled";
|
|
6
|
+
removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant", discardPending?: boolean): void;
|
|
7
|
+
storeTokenResponse(body: any, clientId: string, keyHandle?: string): StoredToken;
|
|
8
|
+
tokenKey(collectionId: string): string;
|
|
9
|
+
pendingMutationKey(collectionId: string): string;
|
|
10
|
+
directPreferenceKey(): string;
|
|
11
|
+
}
|
|
12
|
+
export interface ConnectionTransportOptions {
|
|
13
|
+
serverUrl: string;
|
|
14
|
+
storage: Storage;
|
|
15
|
+
keyStore: GrantKeyStore;
|
|
16
|
+
directAccessMode: "auto" | "disabled";
|
|
17
|
+
loopbackUrl: string;
|
|
18
|
+
collectionId: string;
|
|
19
|
+
internals: ConnectionTransportInternals;
|
|
20
|
+
onChange(): void;
|
|
21
|
+
timeouts: ResolvedConnectTimeouts;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
import type { CollectionOperation } from "@mdbase-dev/connect-protocol";
|
|
2
|
-
import { type GrantKeyStore } from "./crypto.js";
|
|
3
2
|
import type { StoredToken } from "./internal-types.js";
|
|
4
3
|
import type { DirectAccessStatus, MdbaseConnectionRoute } from "./connection-types.js";
|
|
5
4
|
import type { ConnectRequestOptions, PendingMutationSummary } from "./operation-types.js";
|
|
6
5
|
import { ConnectionFileTransport } from "./connection-file-transport.js";
|
|
7
|
-
import {
|
|
8
|
-
export
|
|
9
|
-
readonly relayEncryption: "required" | "disabled";
|
|
10
|
-
removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant", discardPending?: boolean): void;
|
|
11
|
-
storeTokenResponse(body: any, clientId: string, keyHandle?: string): StoredToken;
|
|
12
|
-
tokenKey(collectionId: string): string;
|
|
13
|
-
pendingMutationKey(collectionId: string): string;
|
|
14
|
-
directPreferenceKey(): string;
|
|
15
|
-
}
|
|
16
|
-
export interface ConnectionTransportOptions {
|
|
17
|
-
serverUrl: string;
|
|
18
|
-
storage: Storage;
|
|
19
|
-
keyStore: GrantKeyStore;
|
|
20
|
-
directAccessMode: "auto" | "disabled";
|
|
21
|
-
loopbackUrl: string;
|
|
22
|
-
collectionId: string;
|
|
23
|
-
internals: ConnectionTransportInternals;
|
|
24
|
-
onChange(): void;
|
|
25
|
-
timeouts: ResolvedConnectTimeouts;
|
|
26
|
-
}
|
|
6
|
+
import type { ConnectionTransportOptions } from "./connection-transport-internals.js";
|
|
7
|
+
export type { ConnectionTransportInternals, ConnectionTransportOptions } from "./connection-transport-internals.js";
|
|
27
8
|
export declare class ConnectionTransport {
|
|
28
9
|
private readonly serverUrl;
|
|
29
10
|
private readonly storage;
|
|
@@ -62,7 +43,6 @@ export declare class ConnectionTransport {
|
|
|
62
43
|
performOperation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<Result>;
|
|
63
44
|
private performOperationWithinBudget;
|
|
64
45
|
performAuthoritySyncRequest<Result>(collectionId: string, replicaId: string, method: "GET" | "POST", path: string, input?: unknown, options?: ConnectRequestOptions): Promise<Result>;
|
|
65
|
-
private sendAuthoritySyncRequest;
|
|
66
46
|
private sendOperation;
|
|
67
47
|
private directCapable;
|
|
68
48
|
hasResumableMutationTransport(): boolean;
|
|
@@ -8,6 +8,8 @@ import { directFallbackStatus, isMutation, localNetworkPermission, loopbackReque
|
|
|
8
8
|
import { apiError, decodeJsonResponse, oauthErrorCode, parseStored, } from "./runtime-utils.js";
|
|
9
9
|
import { readStoredToken } from "./stored-token.js";
|
|
10
10
|
import { withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
|
|
11
|
+
import { sendAuthoritySyncRequest } from "./authority-sync-request.js";
|
|
12
|
+
import { freshEncryptedRequest } from "./fresh-encrypted-request.js";
|
|
11
13
|
export class ConnectionTransport {
|
|
12
14
|
serverUrl;
|
|
13
15
|
storage;
|
|
@@ -134,7 +136,7 @@ export class ConnectionTransport {
|
|
|
134
136
|
signal: budget.signal
|
|
135
137
|
}));
|
|
136
138
|
}
|
|
137
|
-
async performOperationWithinBudget(operation, input, options, storedPending) {
|
|
139
|
+
async performOperationWithinBudget(operation, input, options, storedPending, freshReadRetried = false) {
|
|
138
140
|
throwIfCancelled(options.signal);
|
|
139
141
|
let token = this.currentToken();
|
|
140
142
|
if (!token)
|
|
@@ -199,6 +201,11 @@ export class ConnectionTransport {
|
|
|
199
201
|
}
|
|
200
202
|
if (!response.ok) {
|
|
201
203
|
const error = apiError(body, "operation_failed", "Collection operation failed.", response.status);
|
|
204
|
+
if (error.code === "fresh_request_required"
|
|
205
|
+
&& !attempt.pendingMutation
|
|
206
|
+
&& !freshReadRetried) {
|
|
207
|
+
return this.performOperationWithinBudget(operation, input, options, undefined, true);
|
|
208
|
+
}
|
|
202
209
|
if (attempt.pendingMutation
|
|
203
210
|
&& (attempt.directDeliveryUncertain
|
|
204
211
|
|| (attempt.encryptedRequest && attempt.resumingMutation))) {
|
|
@@ -230,6 +237,12 @@ export class ConnectionTransport {
|
|
|
230
237
|
applicationId: responseApplicationId,
|
|
231
238
|
encryption: responseEncryption
|
|
232
239
|
}, attempt.encryptedRequest, encryptedResponse);
|
|
240
|
+
if (!decrypted.ok
|
|
241
|
+
&& decrypted.problem.code === "fresh_request_required"
|
|
242
|
+
&& !attempt.pendingMutation
|
|
243
|
+
&& !freshReadRetried) {
|
|
244
|
+
return this.performOperationWithinBudget(operation, input, options, undefined, true);
|
|
245
|
+
}
|
|
233
246
|
if (attempt.pendingMutation)
|
|
234
247
|
this.clearPendingMutation(attempt.requestId);
|
|
235
248
|
if (!decrypted.ok)
|
|
@@ -287,7 +300,7 @@ export class ConnectionTransport {
|
|
|
287
300
|
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
288
301
|
}
|
|
289
302
|
try {
|
|
290
|
-
let response = await this.
|
|
303
|
+
let response = await sendAuthoritySyncRequest(this.keyStore, token, method, path, input, budget.signal);
|
|
291
304
|
if (response.status === 401 && token.refreshToken) {
|
|
292
305
|
token = await this.refreshAuthorization(budget.signal);
|
|
293
306
|
if (!token.authority
|
|
@@ -295,7 +308,7 @@ export class ConnectionTransport {
|
|
|
295
308
|
|| token.authority.replicaId !== replicaId) {
|
|
296
309
|
throw connectError("authority_authorization_changed", "Reconnect this collection authority before synchronizing.");
|
|
297
310
|
}
|
|
298
|
-
response = await this.
|
|
311
|
+
response = await sendAuthoritySyncRequest(this.keyStore, token, method, path, input, budget.signal);
|
|
299
312
|
}
|
|
300
313
|
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid synchronization response.");
|
|
301
314
|
if (!response.ok) {
|
|
@@ -313,24 +326,6 @@ export class ConnectionTransport {
|
|
|
313
326
|
}
|
|
314
327
|
});
|
|
315
328
|
}
|
|
316
|
-
async sendAuthoritySyncRequest(token, method, path, input, signal) {
|
|
317
|
-
if (!token.authority) {
|
|
318
|
-
throw connectError("not_remote_authority", "This authorization has no remote authority endpoint.");
|
|
319
|
-
}
|
|
320
|
-
const url = `${token.authority.syncUrl}/${path}`;
|
|
321
|
-
const body = input === undefined ? undefined : JSON.stringify(input);
|
|
322
|
-
const proof = await authorityProofHeaders(this.keyStore, token, method, url, body, token.authority.accessToken);
|
|
323
|
-
return fetch(url, {
|
|
324
|
-
method,
|
|
325
|
-
headers: {
|
|
326
|
-
authorization: `Bearer ${token.authority.accessToken}`,
|
|
327
|
-
...(input === undefined ? {} : { "content-type": "application/json" }),
|
|
328
|
-
...proof
|
|
329
|
-
},
|
|
330
|
-
...(body === undefined ? {} : { body }),
|
|
331
|
-
signal
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
329
|
async sendOperation(token, operation, input, tryDirect, options = {}, pendingRequestId) {
|
|
335
330
|
let body = input ?? {};
|
|
336
331
|
let encryptedRequest;
|
|
@@ -400,6 +395,7 @@ export class ConnectionTransport {
|
|
|
400
395
|
throw serverConnectError(error.code, error.message);
|
|
401
396
|
throw error;
|
|
402
397
|
}
|
|
398
|
+
requestId = encryptedRequest.request_id;
|
|
403
399
|
body = encryptedRequest;
|
|
404
400
|
}
|
|
405
401
|
else {
|
|
@@ -469,6 +465,10 @@ export class ConnectionTransport {
|
|
|
469
465
|
throw error;
|
|
470
466
|
}
|
|
471
467
|
let response;
|
|
468
|
+
if (!pendingMutation) {
|
|
469
|
+
encryptedRequest = await freshEncryptedRequest(this.keyStore, relayToken, operation, input);
|
|
470
|
+
requestId = encryptedRequest.request_id;
|
|
471
|
+
}
|
|
472
472
|
try {
|
|
473
473
|
response = await fetch(`${this.serverUrl}/v1/authorities/${encodeURIComponent(relayToken.collectionId)}/operations/${operation}`, {
|
|
474
474
|
method: "POST",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CollectionOperation, EncryptedRelayOperationRequest } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type GrantKeyStore } from "./crypto.js";
|
|
3
|
+
import type { StoredToken } from "./internal-types.js";
|
|
4
|
+
export declare function freshEncryptedRequest(keyStore: GrantKeyStore, token: StoredToken, operation: CollectionOperation, input: unknown): Promise<EncryptedRelayOperationRequest>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { encryptRelayRequest, RelayCryptoError } from "./crypto.js";
|
|
2
|
+
import { connectError, serverConnectError } from "./errors.js";
|
|
3
|
+
export async function freshEncryptedRequest(keyStore, token, operation, input) {
|
|
4
|
+
if (!token.encryption || !token.grantId || !token.keyHandle) {
|
|
5
|
+
throw connectError("authorization_binding_incompatible", "The refreshed authorization no longer supports encrypted connector operations.", {
|
|
6
|
+
details: {
|
|
7
|
+
contract: "grant_encryption",
|
|
8
|
+
required: [1],
|
|
9
|
+
supported: token.encryption ? [token.encryption.protocol_version] : [],
|
|
10
|
+
peer: "connector",
|
|
11
|
+
operation
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return await encryptRelayRequest(keyStore, token.keyHandle, {
|
|
17
|
+
grantId: token.grantId,
|
|
18
|
+
applicationId: token.clientId,
|
|
19
|
+
encryption: token.encryption
|
|
20
|
+
}, operation, input);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof RelayCryptoError) {
|
|
24
|
+
throw serverConnectError(error.code, error.message);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -17,6 +17,7 @@ export declare class LocalFileTransport {
|
|
|
17
17
|
private readonly options;
|
|
18
18
|
constructor(options: LocalFileTransportOptions);
|
|
19
19
|
control<Result>(token: StoredToken, method: "GET" | "POST" | "DELETE", path: string, input: unknown, signal?: AbortSignal): Promise<Result>;
|
|
20
|
+
private controlAttempt;
|
|
20
21
|
private decryptControlResponse;
|
|
21
22
|
uploadChunk(session: FileTransferSession, chunkIndex: number, bytes: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
22
23
|
downloadChunk(session: FileTransferSession, chunkIndex: number, signal?: AbortSignal): Promise<Uint8Array>;
|
|
@@ -11,11 +11,15 @@ export class LocalFileTransport {
|
|
|
11
11
|
this.options = options;
|
|
12
12
|
}
|
|
13
13
|
async control(token, method, path, input, signal) {
|
|
14
|
+
return this.controlAttempt(token, method, path, input, signal, false);
|
|
15
|
+
}
|
|
16
|
+
async controlAttempt(token, method, path, input, signal, freshReadRetried) {
|
|
14
17
|
requireEncryption(token);
|
|
15
18
|
const controlInput = localFileControlInput(method, path, input);
|
|
16
19
|
let encryptedRequest = await this.encryptControl(token, controlInput);
|
|
17
20
|
let response;
|
|
18
|
-
|
|
21
|
+
const triedDirect = await this.options.shouldAttemptDirect(token);
|
|
22
|
+
if (triedDirect) {
|
|
19
23
|
try {
|
|
20
24
|
response = await fetch(`${this.options.loopbackUrl}/v1/files/control`, loopbackRequest({
|
|
21
25
|
method: "POST",
|
|
@@ -32,11 +36,18 @@ export class LocalFileTransport {
|
|
|
32
36
|
if (response && !directFallbackStatus(response.status)) {
|
|
33
37
|
if (response.ok)
|
|
34
38
|
this.options.onDirectAvailable();
|
|
35
|
-
return this.decryptControlResponse(token, encryptedRequest, response);
|
|
39
|
+
return this.decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried);
|
|
36
40
|
}
|
|
37
41
|
if (response)
|
|
38
42
|
this.options.onDirectUnavailable();
|
|
39
43
|
}
|
|
44
|
+
if (triedDirect && method === "GET") {
|
|
45
|
+
token = token.expiresAt > Date.now() + 30_000
|
|
46
|
+
? token
|
|
47
|
+
: await this.options.refreshAuthorization(signal);
|
|
48
|
+
requireLocalFileToken(token);
|
|
49
|
+
encryptedRequest = await this.encryptControl(token, controlInput);
|
|
50
|
+
}
|
|
40
51
|
response = await this.sendRelayControl(token, encryptedRequest, signal);
|
|
41
52
|
if (await refreshableBindingFailure(response, token)) {
|
|
42
53
|
token = await this.options.refreshAuthorization(signal);
|
|
@@ -46,18 +57,30 @@ export class LocalFileTransport {
|
|
|
46
57
|
}
|
|
47
58
|
if (response.ok)
|
|
48
59
|
this.options.onRelayAvailable();
|
|
49
|
-
return this.decryptControlResponse(token, encryptedRequest, response);
|
|
60
|
+
return this.decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried);
|
|
50
61
|
}
|
|
51
|
-
async decryptControlResponse(token, encryptedRequest, response) {
|
|
62
|
+
async decryptControlResponse(token, method, path, input, signal, encryptedRequest, response, freshReadRetried) {
|
|
52
63
|
const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file control response.");
|
|
53
64
|
if (!response.ok) {
|
|
54
|
-
|
|
65
|
+
const error = apiError(body, "operation_failed", "Collection file request failed.", response.status);
|
|
66
|
+
if (error.code === "fresh_request_required"
|
|
67
|
+
&& method === "GET"
|
|
68
|
+
&& !freshReadRetried) {
|
|
69
|
+
return this.controlAttempt(token, method, path, input, signal, true);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
55
72
|
}
|
|
56
73
|
const encryptedResponse = body?.envelope;
|
|
57
74
|
if (!encryptedResponse) {
|
|
58
75
|
throw connectError("invalid_encrypted_response", "The connector omitted its encrypted file response.");
|
|
59
76
|
}
|
|
60
77
|
const decrypted = await decryptRelayResponse(this.options.keyStore, token.keyHandle, relayBinding(token), encryptedRequest, encryptedResponse);
|
|
78
|
+
if (!decrypted.ok
|
|
79
|
+
&& decrypted.problem.code === "fresh_request_required"
|
|
80
|
+
&& method === "GET"
|
|
81
|
+
&& !freshReadRetried) {
|
|
82
|
+
return this.controlAttempt(token, method, path, input, signal, true);
|
|
83
|
+
}
|
|
61
84
|
if (!decrypted.ok)
|
|
62
85
|
throwEncryptedProblem(decrypted.problem);
|
|
63
86
|
return decrypted.result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mdbase-dev/connect",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.56",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@mdbase-dev/connect-protocol": "0.1.0-beta.
|
|
36
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.56"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|