@mdbase-dev/connect 0.1.0-beta.21 → 0.1.0-beta.23
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 +108 -38
- package/dist/authorization-url.d.ts +2 -1
- package/dist/authorization-url.js +5 -7
- 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/collection-client.d.ts +34 -31
- package/dist/collection-client.js +107 -61
- package/dist/connect.d.ts +4 -3
- package/dist/connect.js +53 -38
- package/dist/connection-file-transport.d.ts +31 -0
- package/dist/connection-file-transport.js +42 -0
- package/dist/connection-notifications.js +21 -21
- package/dist/connection-transport.d.ts +2 -0
- package/dist/connection-transport.js +95 -37
- package/dist/connection-types.d.ts +2 -1
- package/dist/connection.d.ts +49 -45
- package/dist/connection.js +73 -33
- package/dist/crypto.d.ts +5 -6
- package/dist/crypto.js +13 -13
- package/dist/errors.d.ts +38 -24
- package/dist/errors.js +130 -71
- 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/files.d.ts +91 -0
- package/dist/files.js +644 -0
- package/dist/hosted-file-request.d.ts +6 -0
- package/dist/hosted-file-request.js +63 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/internal-types.d.ts +3 -1
- package/dist/local-file-transport.d.ts +30 -0
- package/dist/local-file-transport.js +367 -0
- package/dist/notifications.js +5 -5
- package/dist/operation-helpers.d.ts +2 -2
- package/dist/operation-helpers.js +22 -18
- package/dist/operation-types.d.ts +3 -4
- package/dist/outcomes.d.ts +51 -0
- package/dist/outcomes.js +168 -0
- package/dist/runtime-utils.d.ts +6 -1
- package/dist/runtime-utils.js +90 -11
- package/dist/selection.js +3 -3
- package/dist/session.d.ts +9 -8
- package/dist/session.js +31 -20
- package/package.json +2 -2
package/dist/connection.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { MdbaseCollectionClient } from "./collection-client.js";
|
|
2
2
|
import { ConnectionNotifications } from "./connection-notifications.js";
|
|
3
3
|
import { ConnectionTransport } from "./connection-transport.js";
|
|
4
|
-
import { MdbaseConnectError
|
|
4
|
+
import { MdbaseConnectError } from "./errors.js";
|
|
5
|
+
import { MdbaseFileClient } from "./files.js";
|
|
5
6
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
6
7
|
import { assertDeletePreview, assertRenamePreview, deleteEstimate, isCancellation, renameEstimate, throwIfCancelled, uniqueOperations } from "./operation-helpers.js";
|
|
8
|
+
import { AUTHORIZATION_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, DIRECT_ACCESS_PROBLEM_CODES, NOTIFICATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
|
|
7
9
|
export class MdbaseConnection {
|
|
8
10
|
internals;
|
|
9
11
|
collectionId;
|
|
10
12
|
collectionClient;
|
|
11
13
|
notifications;
|
|
12
14
|
transport;
|
|
15
|
+
files;
|
|
13
16
|
connectionListeners = new Set();
|
|
14
17
|
constructor(internals, collectionId) {
|
|
15
18
|
this.internals = internals;
|
|
@@ -24,6 +27,12 @@ export class MdbaseConnection {
|
|
|
24
27
|
internals,
|
|
25
28
|
onChange: () => this.emitConnection()
|
|
26
29
|
});
|
|
30
|
+
this.files = new MdbaseFileClient(() => this.fileCapability, (method, path, input, signal) => this.transport.files.control(method, path, input, signal), {
|
|
31
|
+
uploadChunk: (session, chunkIndex, bytes, signal) => this.transport.files.uploadChunk(session, chunkIndex, bytes, signal),
|
|
32
|
+
downloadChunk: (session, chunkIndex, signal) => this.transport.files.downloadChunk(session, chunkIndex, signal)
|
|
33
|
+
}, {
|
|
34
|
+
downloadPart: (session, partIndex, expectedLength, signal) => this.transport.files.downloadHostedPart(session, partIndex, expectedLength, signal)
|
|
35
|
+
});
|
|
27
36
|
this.collectionClient = new MdbaseCollectionClient({
|
|
28
37
|
operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
|
|
29
38
|
});
|
|
@@ -31,7 +40,7 @@ export class MdbaseConnection {
|
|
|
31
40
|
serverUrl: internals.serverUrl,
|
|
32
41
|
storage: internals.storage,
|
|
33
42
|
authorizedToken: () => this.transport.authorizedToken(),
|
|
34
|
-
register: () => this.register(),
|
|
43
|
+
register: () => this.internals.register(),
|
|
35
44
|
notificationKey: (transport) => internals.notificationKey(collectionId, transport)
|
|
36
45
|
});
|
|
37
46
|
}
|
|
@@ -43,11 +52,10 @@ export class MdbaseConnection {
|
|
|
43
52
|
return [...(this.transport.currentToken()?.operations ?? [])];
|
|
44
53
|
}
|
|
45
54
|
get scope() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return scope;
|
|
55
|
+
return this.transport.currentToken()?.scope ?? null;
|
|
56
|
+
}
|
|
57
|
+
get fileCapability() {
|
|
58
|
+
return this.transport.currentToken()?.fileCapability ?? null;
|
|
51
59
|
}
|
|
52
60
|
get directAccess() {
|
|
53
61
|
return this.transport.directAccess;
|
|
@@ -56,7 +64,7 @@ export class MdbaseConnection {
|
|
|
56
64
|
return this.transport.route;
|
|
57
65
|
}
|
|
58
66
|
register() {
|
|
59
|
-
return this.internals.register();
|
|
67
|
+
return captureConnectOutcome(() => this.internals.register(), REGISTRATION_PROBLEM_CODES);
|
|
60
68
|
}
|
|
61
69
|
info() {
|
|
62
70
|
const token = this.transport.currentToken();
|
|
@@ -65,6 +73,7 @@ export class MdbaseConnection {
|
|
|
65
73
|
displayName: token.collectionName,
|
|
66
74
|
operations: [...token.operations],
|
|
67
75
|
scope: token.scope,
|
|
76
|
+
...(token.fileCapability ? { fileCapability: token.fileCapability } : {}),
|
|
68
77
|
route: this.transport.route,
|
|
69
78
|
directAccess: this.transport.directAccess
|
|
70
79
|
} : null;
|
|
@@ -85,15 +94,16 @@ export class MdbaseConnection {
|
|
|
85
94
|
return this.authorizationCapabilities(requiredOperations).sufficient;
|
|
86
95
|
}
|
|
87
96
|
authorize(options = {}) {
|
|
88
|
-
return this.internals.authorize({
|
|
97
|
+
return captureConnectOutcome(() => this.internals.authorize({
|
|
89
98
|
...options,
|
|
90
99
|
target: { kind: "collection", collectionId: this.collectionId }
|
|
91
|
-
});
|
|
100
|
+
}), AUTHORIZATION_PROBLEM_CODES);
|
|
92
101
|
}
|
|
93
102
|
async requestOperations(requiredOperations, options = {}) {
|
|
94
103
|
const capabilities = this.authorizationCapabilities(requiredOperations);
|
|
95
|
-
if (capabilities.sufficient)
|
|
96
|
-
return { kind: "unchanged", connection: this };
|
|
104
|
+
if (capabilities.sufficient) {
|
|
105
|
+
return connectSuccess({ kind: "unchanged", connection: this });
|
|
106
|
+
}
|
|
97
107
|
return this.authorize({
|
|
98
108
|
...options,
|
|
99
109
|
operations: uniqueOperations([
|
|
@@ -111,11 +121,11 @@ export class MdbaseConnection {
|
|
|
111
121
|
this.transport.notifyStorageChanged();
|
|
112
122
|
}
|
|
113
123
|
checkDirectAccess() {
|
|
114
|
-
return this.transport.checkDirectAccess();
|
|
124
|
+
return captureConnectOutcome(() => this.transport.checkDirectAccess(), DIRECT_ACCESS_PROBLEM_CODES);
|
|
115
125
|
}
|
|
116
126
|
/** Call from a user gesture to request browser permission for direct local access. */
|
|
117
127
|
requestDirectAccess() {
|
|
118
|
-
return this.transport.requestDirectAccess();
|
|
128
|
+
return captureConnectOutcome(() => this.transport.requestDirectAccess(), DIRECT_ACCESS_PROBLEM_CODES);
|
|
119
129
|
}
|
|
120
130
|
disableDirectAccess() {
|
|
121
131
|
this.transport.disableDirectAccess();
|
|
@@ -180,16 +190,16 @@ export class MdbaseConnection {
|
|
|
180
190
|
* only an opaque cursor and static presentation copy.
|
|
181
191
|
*/
|
|
182
192
|
registerNotifications(options) {
|
|
183
|
-
return this.notifications.registerNotifications(options);
|
|
193
|
+
return captureConnectOutcome(() => this.notifications.registerNotifications(options), NOTIFICATION_PROBLEM_CODES);
|
|
184
194
|
}
|
|
185
195
|
registerNativeNotifications(options) {
|
|
186
|
-
return this.notifications.registerNativeNotifications(options);
|
|
196
|
+
return captureConnectOutcome(() => this.notifications.registerNativeNotifications(options), NOTIFICATION_PROBLEM_CODES);
|
|
187
197
|
}
|
|
188
198
|
unregisterNativeNotifications() {
|
|
189
|
-
return this.notifications.unregisterNativeNotifications();
|
|
199
|
+
return captureConnectOutcome(() => this.notifications.unregisterNativeNotifications(), NOTIFICATION_PROBLEM_CODES);
|
|
190
200
|
}
|
|
191
201
|
unregisterNotifications(serviceWorker) {
|
|
192
|
-
return this.notifications.unregisterNotifications(serviceWorker);
|
|
202
|
+
return captureConnectOutcome(() => this.notifications.unregisterNotifications(serviceWorker), NOTIFICATION_PROBLEM_CODES);
|
|
193
203
|
}
|
|
194
204
|
forget() {
|
|
195
205
|
const token = this.transport.currentToken();
|
|
@@ -268,22 +278,37 @@ export class MdbaseConnection {
|
|
|
268
278
|
try {
|
|
269
279
|
throwIfCancelled(options.signal);
|
|
270
280
|
emit("preflighting", true);
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
const previewOutcome = options.preflight
|
|
282
|
+
? connectSuccess(options.preflight)
|
|
283
|
+
: await this.preflightRename(input, { signal: options.signal });
|
|
284
|
+
if (!previewOutcome.ok)
|
|
285
|
+
return previewOutcome;
|
|
286
|
+
const preview = previewOutcome.value;
|
|
287
|
+
try {
|
|
288
|
+
assertRenamePreview(input, preview);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error instanceof MdbaseConnectError) {
|
|
292
|
+
return connectFailure(error.problem);
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
275
296
|
estimate = renameEstimate(input, preview);
|
|
276
297
|
emit("ready", true);
|
|
277
298
|
throwIfCancelled(options.signal);
|
|
278
299
|
const cancellable = this.transport.hasResumableMutationTransport();
|
|
279
300
|
emit("applying", cancellable);
|
|
280
301
|
const result = await this.rename(input, cancellable ? { signal: options.signal } : undefined);
|
|
281
|
-
|
|
302
|
+
if (result.ok)
|
|
303
|
+
emit("completed", false, estimate.totalUnits);
|
|
282
304
|
return result;
|
|
283
305
|
}
|
|
284
306
|
catch (error) {
|
|
285
307
|
if (isCancellation(error, options.signal))
|
|
286
308
|
emit("cancelled", false);
|
|
309
|
+
if (error instanceof MdbaseConnectError) {
|
|
310
|
+
return connectFailure(error.problem);
|
|
311
|
+
}
|
|
287
312
|
throw error;
|
|
288
313
|
}
|
|
289
314
|
}
|
|
@@ -305,30 +330,45 @@ export class MdbaseConnection {
|
|
|
305
330
|
try {
|
|
306
331
|
throwIfCancelled(options.signal);
|
|
307
332
|
emit("preflighting", true);
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
333
|
+
const previewOutcome = options.preflight
|
|
334
|
+
? connectSuccess(options.preflight)
|
|
335
|
+
: await this.preflightDelete(input, { signal: options.signal });
|
|
336
|
+
if (!previewOutcome.ok)
|
|
337
|
+
return previewOutcome;
|
|
338
|
+
const preview = previewOutcome.value;
|
|
339
|
+
try {
|
|
340
|
+
assertDeletePreview(input, preview);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof MdbaseConnectError) {
|
|
344
|
+
return connectFailure(error.problem);
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
312
348
|
estimate = deleteEstimate(preview);
|
|
313
349
|
emit("ready", true);
|
|
314
350
|
throwIfCancelled(options.signal);
|
|
315
351
|
const cancellable = this.transport.hasResumableMutationTransport();
|
|
316
352
|
emit("applying", cancellable);
|
|
317
353
|
const result = await this.delete(input, cancellable ? { signal: options.signal } : undefined);
|
|
318
|
-
|
|
354
|
+
if (result.ok)
|
|
355
|
+
emit("completed", false, estimate.totalUnits);
|
|
319
356
|
return result;
|
|
320
357
|
}
|
|
321
358
|
catch (error) {
|
|
322
359
|
if (isCancellation(error, options.signal))
|
|
323
360
|
emit("cancelled", false);
|
|
361
|
+
if (error instanceof MdbaseConnectError) {
|
|
362
|
+
return connectFailure(error.problem);
|
|
363
|
+
}
|
|
324
364
|
throw error;
|
|
325
365
|
}
|
|
326
366
|
}
|
|
327
367
|
pendingMutation() {
|
|
328
368
|
return this.transport.pendingMutation();
|
|
329
369
|
}
|
|
330
|
-
|
|
331
|
-
return this.transport.resumePendingMutation(input, options);
|
|
370
|
+
resumePendingMutation(input, options) {
|
|
371
|
+
return captureConnectOutcome(() => this.transport.resumePendingMutation(input, options), COLLECTION_MUTATION_PROBLEM_CODES);
|
|
332
372
|
}
|
|
333
373
|
validate(input = {}) {
|
|
334
374
|
return this.collectionClient.validate(input);
|
|
@@ -357,10 +397,10 @@ export class MdbaseConnection {
|
|
|
357
397
|
reconcileTimers(input) {
|
|
358
398
|
return this.collectionClient.reconcileTimers(input);
|
|
359
399
|
}
|
|
360
|
-
|
|
361
|
-
|
|
400
|
+
watch(options = {}) {
|
|
401
|
+
return this.collectionClient.watch(options);
|
|
362
402
|
}
|
|
363
|
-
|
|
403
|
+
operation(operation, input, options) {
|
|
364
404
|
return this.collectionClient.operation(operation, input, options);
|
|
365
405
|
}
|
|
366
406
|
emitConnection() {
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ConnectProblem, type EncryptedRelayOperation, type EncryptedRelayOperationRequest, type EncryptedRelayOperationResponse, type GrantEncryption } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
export interface GrantKeyRecord {
|
|
3
3
|
handle: string;
|
|
4
4
|
agreementPrivateKey: CryptoKey;
|
|
@@ -46,19 +46,18 @@ export interface AuthorityProofInput {
|
|
|
46
46
|
/** Sign one remote-authority or token-refresh request with the approved grant key. */
|
|
47
47
|
export declare function signAuthorityRequest(store: GrantKeyStore, handle: string, expectedPublicKey: string, input: AuthorityProofInput): Promise<Record<string, string>>;
|
|
48
48
|
export declare function authorityProofMessage(input: Required<Pick<AuthorityProofInput, "method" | "target" | "credential" | "timestamp" | "nonce">> & Pick<AuthorityProofInput, "body">): Promise<string>;
|
|
49
|
-
export declare function encryptRelayRequest(store: GrantKeyStore, handle: string, binding: RelayBinding, operation:
|
|
49
|
+
export declare function encryptRelayRequest(store: GrantKeyStore, handle: string, binding: RelayBinding, operation: EncryptedRelayOperation, input: unknown, requestId?: string): Promise<EncryptedRelayOperationRequest>;
|
|
50
50
|
export declare function decryptRelayResponse<Result>(store: GrantKeyStore, handle: string, binding: RelayBinding, request: EncryptedRelayOperationRequest, response: EncryptedRelayOperationResponse): Promise<{
|
|
51
51
|
ok: true;
|
|
52
52
|
result: Result;
|
|
53
53
|
} | {
|
|
54
54
|
ok: false;
|
|
55
|
-
|
|
56
|
-
code: string;
|
|
57
|
-
message: string;
|
|
58
|
-
};
|
|
55
|
+
problem: ConnectProblem;
|
|
59
56
|
}>;
|
|
60
57
|
export declare class RelayCryptoError extends Error {
|
|
61
58
|
readonly code: string;
|
|
62
59
|
constructor(code: string, message: string);
|
|
63
60
|
}
|
|
61
|
+
/** @internal Shared ECDH primitive used by the independently keyed file data plane. */
|
|
62
|
+
export declare function deriveP256SharedSecret(privateKey: CryptoKey, peerPublicKey: string): Promise<ArrayBuffer>;
|
|
64
63
|
export declare function validateGrantEncryption(encryption: GrantEncryption): void;
|
package/dist/crypto.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ENCRYPTED_RELAY_PROTOCOL_VERSION, AUTHORITY_PROOF_DOMAIN, AUTHORITY_PROOF_HEADERS, AUTHORITY_PROOF_VERSION, RELAY_ENCRYPTION_SUITE } from "@mdbase-dev/connect-protocol";
|
|
1
|
+
import { ENCRYPTED_RELAY_PROTOCOL_VERSION, AUTHORITY_PROOF_DOMAIN, AUTHORITY_PROOF_HEADERS, AUTHORITY_PROOF_VERSION, RELAY_ENCRYPTION_SUITE, isConnectProblem } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
const DATABASE_NAME = "mdbase-connect-keys";
|
|
3
3
|
const STORE_NAME = "grant-keys";
|
|
4
4
|
const MAX_U64 = 18446744073709551615n;
|
|
@@ -198,9 +198,7 @@ export async function decryptRelayResponse(store, handle, binding, request, resp
|
|
|
198
198
|
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
|
|
199
199
|
if (value?.ok === true && "result" in value)
|
|
200
200
|
return value;
|
|
201
|
-
if (value?.ok === false
|
|
202
|
-
&& typeof value.error?.code === "string"
|
|
203
|
-
&& typeof value.error?.message === "string")
|
|
201
|
+
if (value?.ok === false && isConnectProblem(value.problem))
|
|
204
202
|
return value;
|
|
205
203
|
throw new Error("invalid response body");
|
|
206
204
|
}
|
|
@@ -249,15 +247,7 @@ async function requireSigningKey(store, handle, expectedPublicKey) {
|
|
|
249
247
|
return record;
|
|
250
248
|
}
|
|
251
249
|
async function deriveDirectionalKey(privateKey, binding, direction) {
|
|
252
|
-
|
|
253
|
-
let shared;
|
|
254
|
-
try {
|
|
255
|
-
peer = await crypto.subtle.importKey("raw", toArrayBuffer(p256PublicKey(binding.encryption.connector_agreement_public_key)), { name: "ECDH", namedCurve: "P-256" }, false, []);
|
|
256
|
-
shared = await crypto.subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
throw new RelayCryptoError("invalid_public_key", "The connector relay public key is invalid.");
|
|
260
|
-
}
|
|
250
|
+
const shared = await deriveP256SharedSecret(privateKey, binding.encryption.connector_agreement_public_key);
|
|
261
251
|
const material = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
|
|
262
252
|
const contextBytes = new TextEncoder().encode(context(binding));
|
|
263
253
|
const salt = await crypto.subtle.digest("SHA-256", contextBytes);
|
|
@@ -268,6 +258,16 @@ async function deriveDirectionalKey(privateKey, binding, direction) {
|
|
|
268
258
|
info: new TextEncoder().encode(direction === "request" ? REQUEST_INFO : RESPONSE_INFO)
|
|
269
259
|
}, material, { name: "AES-GCM", length: 256 }, false, direction === "request" ? ["encrypt"] : ["decrypt"]);
|
|
270
260
|
}
|
|
261
|
+
/** @internal Shared ECDH primitive used by the independently keyed file data plane. */
|
|
262
|
+
export async function deriveP256SharedSecret(privateKey, peerPublicKey) {
|
|
263
|
+
try {
|
|
264
|
+
const peer = await crypto.subtle.importKey("raw", toArrayBuffer(p256PublicKey(peerPublicKey)), { name: "ECDH", namedCurve: "P-256" }, false, []);
|
|
265
|
+
return await crypto.subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
throw new RelayCryptoError("invalid_public_key", "The connector relay public key is invalid.");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
271
|
function context(binding) {
|
|
272
272
|
const value = binding.encryption;
|
|
273
273
|
return [
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type ConnectOperationOutcome, type ConnectProblem, type ConnectProblemCode, type ConnectProblemDetailsByCode, type KnownConnectProblem, type MdbaseOperationEnvelope } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
interface ProblemOptions {
|
|
3
|
+
operationOutcome?: ConnectOperationOutcome;
|
|
4
|
+
traceId?: string;
|
|
4
5
|
status?: number;
|
|
5
|
-
retryable?: boolean;
|
|
6
|
-
requiresAuthorization?: boolean;
|
|
7
|
-
outcomeUnknown?: boolean;
|
|
8
|
-
recovery?: MdbaseRecoveryAction;
|
|
9
|
-
details?: unknown;
|
|
10
6
|
cause?: unknown;
|
|
11
7
|
}
|
|
8
|
+
type ProblemOptionsFor<Code extends ConnectProblemCode> = ConnectProblemDetailsByCode[Code] extends undefined ? ProblemOptions & {
|
|
9
|
+
details?: never;
|
|
10
|
+
} : {} extends ConnectProblemDetailsByCode[Code] ? ProblemOptions & {
|
|
11
|
+
details?: ConnectProblemDetailsByCode[Code];
|
|
12
|
+
} : ProblemOptions & {
|
|
13
|
+
details: ConnectProblemDetailsByCode[Code];
|
|
14
|
+
};
|
|
15
|
+
type ProblemArguments<Code extends ConnectProblemCode> = ConnectProblemDetailsByCode[Code] extends undefined ? [options?: ProblemOptionsFor<Code>] : {} extends ConnectProblemDetailsByCode[Code] ? [options?: ProblemOptionsFor<Code>] : [options: ProblemOptionsFor<Code>];
|
|
16
|
+
export interface ConnectErrorContext {
|
|
17
|
+
status?: number;
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** Internal carrier used while adapting throwing transports into public outcomes. */
|
|
12
21
|
export declare class MdbaseConnectError extends Error {
|
|
13
|
-
readonly
|
|
22
|
+
readonly problem: ConnectProblem;
|
|
23
|
+
readonly code: ConnectProblem["code"];
|
|
14
24
|
readonly status?: number;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
export declare class MdbaseOperationValidationError<Result = unknown> extends Error {
|
|
23
|
-
readonly diagnostics: MdbaseDiagnostic[];
|
|
24
|
-
readonly result: Result;
|
|
25
|
-
readonly code = "operation_invalid";
|
|
26
|
-
constructor(diagnostics: MdbaseDiagnostic[], result: Result);
|
|
25
|
+
constructor(problem: ConnectProblem, context?: ConnectErrorContext);
|
|
26
|
+
get retryable(): boolean;
|
|
27
|
+
get requiresAuthorization(): boolean;
|
|
28
|
+
get outcomeUnknown(): boolean;
|
|
29
|
+
get recovery(): "choose_collection" | "contact_support" | "fix_request" | "none" | "reauthorize" | "refresh" | "repair_collection" | "resolve_conflict" | "resolve_outcome" | "resume_connector_access" | "retry" | "upgrade_application" | "upgrade_collection" | "upgrade_connector";
|
|
30
|
+
get details(): unknown;
|
|
27
31
|
}
|
|
28
|
-
|
|
29
|
-
export declare function
|
|
30
|
-
|
|
32
|
+
export declare function connectProblem<Code extends ConnectProblemCode>(code: Code, message: string, ...[options]: ProblemArguments<Code>): KnownConnectProblem<Code>;
|
|
33
|
+
export declare function unknownConnectProblem(serverCode: string, message: string, options?: ProblemOptions & {
|
|
34
|
+
details?: unknown;
|
|
35
|
+
}): ConnectProblem;
|
|
36
|
+
export declare function connectError<Code extends ConnectProblemCode>(code: Code, message: string, ...args: ProblemArguments<Code>): MdbaseConnectError;
|
|
37
|
+
export declare function serverConnectError(serverCode: string, message: string, options?: ProblemOptions & {
|
|
38
|
+
details?: unknown;
|
|
39
|
+
status?: number;
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
}): MdbaseConnectError;
|
|
42
|
+
export declare function operationProblem<Result>(envelope: MdbaseOperationEnvelope<Result>): KnownConnectProblem<"collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported" | "operation_invalid">;
|
|
43
|
+
/** True only when repeating an operation is safe without asking the user. */
|
|
31
44
|
export declare function isRetryableConnectError(error: unknown): boolean;
|
|
45
|
+
export {};
|
package/dist/errors.js
CHANGED
|
@@ -1,83 +1,142 @@
|
|
|
1
|
+
import { CONNECT_PROBLEM_CATALOG, CONNECT_PROBLEM_VERSION, normalizeConnectProblem } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
/** Internal carrier used while adapting throwing transports into public outcomes. */
|
|
1
3
|
export class MdbaseConnectError extends Error {
|
|
4
|
+
problem;
|
|
2
5
|
code;
|
|
3
6
|
status;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
recovery;
|
|
8
|
-
details;
|
|
9
|
-
constructor(code, message, options = {}) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.code = code;
|
|
7
|
+
constructor(problem, context = {}) {
|
|
8
|
+
super(problem.message, context.cause === undefined ? undefined : { cause: context.cause });
|
|
9
|
+
this.problem = problem;
|
|
12
10
|
this.name = "MdbaseConnectError";
|
|
13
|
-
|
|
14
|
-
this.status =
|
|
15
|
-
this.retryable = options.retryable ?? classification.retryable;
|
|
16
|
-
this.requiresAuthorization = options.requiresAuthorization ?? classification.requiresAuthorization;
|
|
17
|
-
this.outcomeUnknown = options.outcomeUnknown ?? classification.outcomeUnknown;
|
|
18
|
-
this.recovery = options.recovery ?? classification.recovery;
|
|
19
|
-
this.details = options.details;
|
|
20
|
-
if (options.cause !== undefined)
|
|
21
|
-
Object.defineProperty(this, "cause", { value: options.cause, configurable: true });
|
|
11
|
+
this.code = problem.code;
|
|
12
|
+
this.status = context.status;
|
|
22
13
|
}
|
|
14
|
+
get retryable() {
|
|
15
|
+
return this.problem.recovery === "retry"
|
|
16
|
+
&& this.problem.operation_outcome !== "unknown";
|
|
17
|
+
}
|
|
18
|
+
get requiresAuthorization() {
|
|
19
|
+
return this.problem.category === "authorization";
|
|
20
|
+
}
|
|
21
|
+
get outcomeUnknown() {
|
|
22
|
+
return this.problem.operation_outcome === "unknown";
|
|
23
|
+
}
|
|
24
|
+
get recovery() {
|
|
25
|
+
return this.problem.recovery;
|
|
26
|
+
}
|
|
27
|
+
get details() {
|
|
28
|
+
return this.problem.details;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function connectProblem(code, message, ...[options = {}]) {
|
|
32
|
+
const definition = CONNECT_PROBLEM_CATALOG[code];
|
|
33
|
+
return {
|
|
34
|
+
problem_version: CONNECT_PROBLEM_VERSION,
|
|
35
|
+
code,
|
|
36
|
+
category: definition.category,
|
|
37
|
+
recovery: definition.recovery,
|
|
38
|
+
message,
|
|
39
|
+
...(options.details === undefined ? {} : { details: options.details }),
|
|
40
|
+
...(options.operationOutcome === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { operation_outcome: options.operationOutcome }),
|
|
43
|
+
...(options.traceId === undefined ? {} : { trace_id: options.traceId })
|
|
44
|
+
};
|
|
23
45
|
}
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
export function unknownConnectProblem(serverCode, message, options = {}) {
|
|
47
|
+
return normalizeConnectProblem(serverCode, message, {
|
|
48
|
+
details: options.details,
|
|
49
|
+
operation_outcome: options.operationOutcome,
|
|
50
|
+
trace_id: options.traceId
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function connectError(code, message, ...args) {
|
|
54
|
+
const options = args[0];
|
|
55
|
+
return new MdbaseConnectError(connectProblem(code, message, ...args), {
|
|
56
|
+
status: options?.status,
|
|
57
|
+
cause: options?.cause
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function serverConnectError(serverCode, message, options = {}) {
|
|
61
|
+
const { status, cause, ...problemOptions } = options;
|
|
62
|
+
return new MdbaseConnectError(normalizeConnectProblem(serverCode, message, {
|
|
63
|
+
details: problemOptions.details,
|
|
64
|
+
operation_outcome: problemOptions.operationOutcome,
|
|
65
|
+
trace_id: problemOptions.traceId
|
|
66
|
+
}), { status, cause });
|
|
67
|
+
}
|
|
68
|
+
export function operationProblem(envelope) {
|
|
69
|
+
const code = diagnosticProblemCode(envelope.diagnostics);
|
|
70
|
+
const message = diagnosticMessage(envelope.diagnostics);
|
|
71
|
+
if (code === "collection_version_unsupported") {
|
|
72
|
+
const versions = diagnosticVersions(envelope.diagnostics);
|
|
73
|
+
return connectProblem(code, message, {
|
|
74
|
+
operationOutcome: "rejected",
|
|
75
|
+
details: versions
|
|
76
|
+
});
|
|
35
77
|
}
|
|
78
|
+
if (code !== "operation_invalid") {
|
|
79
|
+
return connectProblem(code, message, {
|
|
80
|
+
operationOutcome: "rejected",
|
|
81
|
+
details: { diagnostics: envelope.diagnostics }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return connectProblem(code, message, {
|
|
85
|
+
operationOutcome: "rejected",
|
|
86
|
+
details: {
|
|
87
|
+
diagnostics: envelope.diagnostics,
|
|
88
|
+
partial_result: envelope.result
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function diagnosticProblemCode(diagnostics) {
|
|
93
|
+
const codes = new Set(diagnostics
|
|
94
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
95
|
+
.map((diagnostic) => diagnostic.code));
|
|
96
|
+
if (["migration_required", "migration_lossy", "unsupported_version"]
|
|
97
|
+
.some((code) => codes.has(code)))
|
|
98
|
+
return "collection_version_unsupported";
|
|
99
|
+
if (["invalid_config", "invalid_configuration", "missing_config", "missing_spec_version"]
|
|
100
|
+
.some((code) => codes.has(code)))
|
|
101
|
+
return "collection_configuration_invalid";
|
|
102
|
+
if ([
|
|
103
|
+
"duplicate_type",
|
|
104
|
+
"invalid_contract_definition",
|
|
105
|
+
"invalid_data_contract",
|
|
106
|
+
"invalid_type_definition",
|
|
107
|
+
"invalid_type_path"
|
|
108
|
+
].some((code) => codes.has(code)))
|
|
109
|
+
return "collection_type_registry_invalid";
|
|
110
|
+
if (["collection_invalid", "collection_open_failed"]
|
|
111
|
+
.some((code) => codes.has(code)))
|
|
112
|
+
return "collection_invalid";
|
|
113
|
+
return "operation_invalid";
|
|
114
|
+
}
|
|
115
|
+
function diagnosticVersions(diagnostics) {
|
|
116
|
+
const source = diagnostics
|
|
117
|
+
.map((diagnostic) => diagnostic.details)
|
|
118
|
+
.find((details) => isRecord(details));
|
|
119
|
+
return {
|
|
120
|
+
current_version: typeof source?.current_version === "string"
|
|
121
|
+
? source.current_version
|
|
122
|
+
: "0.2.x",
|
|
123
|
+
required_version: typeof source?.required_version === "string"
|
|
124
|
+
? source.required_version
|
|
125
|
+
: "0.3.0"
|
|
126
|
+
};
|
|
36
127
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!envelope.valid)
|
|
40
|
-
throw new MdbaseOperationValidationError(envelope.diagnostics, envelope.result);
|
|
41
|
-
return envelope.result;
|
|
128
|
+
function isRecord(value) {
|
|
129
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
130
|
}
|
|
43
|
-
/** True only when repeating
|
|
131
|
+
/** True only when repeating an operation is safe without asking the user. */
|
|
44
132
|
export function isRetryableConnectError(error) {
|
|
45
|
-
|
|
46
|
-
return error.retryable && !error.outcomeUnknown;
|
|
47
|
-
return error instanceof TypeError;
|
|
133
|
+
return error instanceof MdbaseConnectError && error.retryable;
|
|
48
134
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"not_authorized",
|
|
57
|
-
"relay_authorization_expired"
|
|
58
|
-
]);
|
|
59
|
-
const outcomeUnknown = code === "direct_outcome_unknown" || code === "pending_mutation_unresolved";
|
|
60
|
-
const requiresAuthorization = authorizationCodes.has(code) || status === 401;
|
|
61
|
-
const retryableCodes = new Set([
|
|
62
|
-
"connector_offline",
|
|
63
|
-
"discovery_failed",
|
|
64
|
-
"relay_unavailable",
|
|
65
|
-
"sync_failed",
|
|
66
|
-
"temporarily_unavailable",
|
|
67
|
-
"timeout"
|
|
68
|
-
]);
|
|
69
|
-
const retryableStatus = status === 408 || status === 425 || status === 429 || (status !== undefined && status >= 500);
|
|
70
|
-
const retryable = !outcomeUnknown && !requiresAuthorization && (retryableCodes.has(code) || retryableStatus);
|
|
71
|
-
const recovery = outcomeUnknown
|
|
72
|
-
? "resolve_outcome"
|
|
73
|
-
: requiresAuthorization
|
|
74
|
-
? "reauthorize"
|
|
75
|
-
: code === "change_cursor_reset"
|
|
76
|
-
? "refresh"
|
|
77
|
-
: retryable
|
|
78
|
-
? "retry"
|
|
79
|
-
: status !== undefined && status >= 400 && status < 500
|
|
80
|
-
? "fix_request"
|
|
81
|
-
: "none";
|
|
82
|
-
return { retryable, requiresAuthorization, outcomeUnknown, recovery };
|
|
135
|
+
function diagnosticMessage(diagnostics) {
|
|
136
|
+
return diagnostics
|
|
137
|
+
.filter((item) => item.severity === "error")
|
|
138
|
+
.map((item) => item.message)
|
|
139
|
+
.join(" ")
|
|
140
|
+
|| diagnostics.map((item) => item.message).join(" ")
|
|
141
|
+
|| "The collection rejected this operation.";
|
|
83
142
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type FileFrameHeader, type FileFrameKind, type FileTransferDirection } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type GrantKeyStore, type RelayBinding } from "./crypto.js";
|
|
3
|
+
export interface FileTransferBinding extends RelayBinding {
|
|
4
|
+
authorityId: string;
|
|
5
|
+
transferId: string;
|
|
6
|
+
direction: FileTransferDirection;
|
|
7
|
+
}
|
|
8
|
+
export declare class FileTransferCryptoError extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
constructor(code: string, message: string);
|
|
11
|
+
}
|
|
12
|
+
/** Grant-bound, direction-separated cipher for one resumable file transfer. */
|
|
13
|
+
export declare class GrantFileTransferCipher {
|
|
14
|
+
private readonly binding;
|
|
15
|
+
private readonly key;
|
|
16
|
+
private constructor();
|
|
17
|
+
static open(store: GrantKeyStore, handle: string, binding: FileTransferBinding): Promise<GrantFileTransferCipher>;
|
|
18
|
+
/** @internal Enables non-browser key-agreement adapters and compatibility tests. */
|
|
19
|
+
static fromSharedSecret(sharedSecret: BufferSource, binding: FileTransferBinding): Promise<GrantFileTransferCipher>;
|
|
20
|
+
encryptChunk(kind: FileFrameKind, header: FileFrameHeader, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
21
|
+
decryptChunk(encoded: Uint8Array): Promise<Uint8Array>;
|
|
22
|
+
private validateHeader;
|
|
23
|
+
}
|
|
24
|
+
/** @internal Deterministic HKDF profile shared with connector authorities. */
|
|
25
|
+
export declare function deriveFileTransferKey(sharedSecret: BufferSource, binding: FileTransferBinding): Promise<CryptoKey>;
|