@mdbase-dev/connect 0.1.0-beta.21 → 0.1.0-beta.22

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.
Files changed (49) hide show
  1. package/README.md +108 -38
  2. package/dist/authorization-url.d.ts +2 -1
  3. package/dist/authorization-url.js +5 -7
  4. package/dist/browser/integrity.json +1 -1
  5. package/dist/browser/mdbase-connect.min.js +3 -3
  6. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  7. package/dist/collection-client.d.ts +34 -31
  8. package/dist/collection-client.js +107 -61
  9. package/dist/connect.d.ts +4 -3
  10. package/dist/connect.js +53 -38
  11. package/dist/connection-file-transport.d.ts +31 -0
  12. package/dist/connection-file-transport.js +42 -0
  13. package/dist/connection-notifications.js +21 -21
  14. package/dist/connection-transport.d.ts +2 -0
  15. package/dist/connection-transport.js +95 -37
  16. package/dist/connection-types.d.ts +2 -1
  17. package/dist/connection.d.ts +49 -45
  18. package/dist/connection.js +73 -33
  19. package/dist/crypto.d.ts +5 -6
  20. package/dist/crypto.js +13 -13
  21. package/dist/errors.d.ts +38 -24
  22. package/dist/errors.js +130 -71
  23. package/dist/file-crypto.d.ts +25 -0
  24. package/dist/file-crypto.js +132 -0
  25. package/dist/file-sha256.d.ts +12 -0
  26. package/dist/file-sha256.js +143 -0
  27. package/dist/file-stream-source.d.ts +17 -0
  28. package/dist/file-stream-source.js +157 -0
  29. package/dist/files.d.ts +91 -0
  30. package/dist/files.js +644 -0
  31. package/dist/hosted-file-request.d.ts +6 -0
  32. package/dist/hosted-file-request.js +63 -0
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +3 -0
  35. package/dist/internal-types.d.ts +3 -1
  36. package/dist/local-file-transport.d.ts +30 -0
  37. package/dist/local-file-transport.js +367 -0
  38. package/dist/notifications.js +5 -5
  39. package/dist/operation-helpers.d.ts +2 -2
  40. package/dist/operation-helpers.js +22 -18
  41. package/dist/operation-types.d.ts +3 -4
  42. package/dist/outcomes.d.ts +51 -0
  43. package/dist/outcomes.js +168 -0
  44. package/dist/runtime-utils.d.ts +6 -1
  45. package/dist/runtime-utils.js +90 -11
  46. package/dist/selection.js +3 -3
  47. package/dist/session.d.ts +9 -8
  48. package/dist/session.js +31 -20
  49. package/package.json +2 -2
@@ -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, unwrapOperation } from "./errors.js";
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
- const scope = this.transport.currentToken()?.scope;
47
- if (!scope) {
48
- throw new MdbaseConnectError("not_authorized", "This collection is no longer authorized for this application.");
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 preview = options.preflight ?? unwrapOperation(await this.preflightRename(input, {
272
- signal: options.signal
273
- }));
274
- assertRenamePreview(input, preview);
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
- emit("completed", false, estimate.totalUnits);
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 preview = options.preflight ?? unwrapOperation(await this.preflightDelete(input, {
309
- signal: options.signal
310
- }));
311
- assertDeletePreview(input, preview);
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
- emit("completed", false, estimate.totalUnits);
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
- async resumePendingMutation(input, options) {
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
- async *watch(options = {}) {
361
- yield* this.collectionClient.watch(options);
400
+ watch(options = {}) {
401
+ return this.collectionClient.watch(options);
362
402
  }
363
- async operation(operation, input, options) {
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 CollectionOperation, type EncryptedRelayOperationRequest, type EncryptedRelayOperationResponse, type GrantEncryption } from "@mdbase-dev/connect-protocol";
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: CollectionOperation, input: unknown, requestId?: string): Promise<EncryptedRelayOperationRequest>;
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
- error: {
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
- let peer;
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 { MdbaseDiagnostic, MdbaseOperationEnvelope } from "@mdbase-dev/connect-protocol";
2
- export type MdbaseRecoveryAction = "retry" | "reauthorize" | "refresh" | "resolve_outcome" | "fix_request" | "none";
3
- export interface MdbaseConnectErrorOptions {
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 code: string;
22
+ readonly problem: ConnectProblem;
23
+ readonly code: ConnectProblem["code"];
14
24
  readonly status?: number;
15
- readonly retryable: boolean;
16
- readonly requiresAuthorization: boolean;
17
- readonly outcomeUnknown: boolean;
18
- readonly recovery: MdbaseRecoveryAction;
19
- readonly details?: unknown;
20
- constructor(code: string, message: string, options?: MdbaseConnectErrorOptions);
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
- /** Return a valid operation result or throw while preserving every diagnostic. */
29
- export declare function unwrapOperation<Result>(envelope: MdbaseOperationEnvelope<Result>): Result;
30
- /** True only when repeating a read/poll is safe without asking the user. */
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
- retryable;
5
- requiresAuthorization;
6
- outcomeUnknown;
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
- const classification = classifyConnectError(code, options.status);
14
- this.status = options.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 class MdbaseOperationValidationError extends Error {
25
- diagnostics;
26
- result;
27
- code = "operation_invalid";
28
- constructor(diagnostics, result) {
29
- super(diagnostics.filter((item) => item.severity === "error").map((item) => item.message).join(" ")
30
- || diagnostics.map((item) => item.message).join(" ")
31
- || "The collection rejected this operation.");
32
- this.diagnostics = diagnostics;
33
- this.result = result;
34
- this.name = "MdbaseOperationValidationError";
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
- /** Return a valid operation result or throw while preserving every diagnostic. */
38
- export function unwrapOperation(envelope) {
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 a read/poll is safe without asking the user. */
131
+ /** True only when repeating an operation is safe without asking the user. */
44
132
  export function isRetryableConnectError(error) {
45
- if (error instanceof MdbaseConnectError)
46
- return error.retryable && !error.outcomeUnknown;
47
- return error instanceof TypeError;
133
+ return error instanceof MdbaseConnectError && error.retryable;
48
134
  }
49
- function classifyConnectError(code, status) {
50
- const authorizationCodes = new Set([
51
- "authorization_expired",
52
- "direct_operation_rejected",
53
- "encryption_required",
54
- "insufficient_access",
55
- "missing_grant_key",
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>;