@mdbase-dev/connect 0.1.0-beta.31 → 0.1.0-beta.33

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 (68) hide show
  1. package/README.md +112 -34
  2. package/dist/advanced.d.ts +9 -0
  3. package/dist/advanced.js +6 -0
  4. package/dist/application-session.d.ts +39 -14
  5. package/dist/application-session.js +201 -70
  6. package/dist/async.js +11 -5
  7. package/dist/authority-proof.d.ts +3 -0
  8. package/dist/authority-proof.js +24 -0
  9. package/dist/browser/integrity.json +1 -1
  10. package/dist/browser/mdbase-connect.min.js +3 -3
  11. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  12. package/dist/browser.d.ts +3 -0
  13. package/dist/browser.js +5 -0
  14. package/dist/capabilities.js +2 -1
  15. package/dist/collection-client.d.ts +36 -33
  16. package/dist/collection-client.js +549 -104
  17. package/dist/connect-authorization-helpers.d.ts +3 -0
  18. package/dist/connect-authorization-helpers.js +13 -0
  19. package/dist/connect-options.d.ts +12 -4
  20. package/dist/connect.d.ts +12 -4
  21. package/dist/connect.js +102 -103
  22. package/dist/connection-file-transport.d.ts +2 -2
  23. package/dist/connection-file-transport.js +4 -4
  24. package/dist/connection-notifications.d.ts +10 -4
  25. package/dist/connection-notifications.js +41 -23
  26. package/dist/connection-transport.d.ts +19 -15
  27. package/dist/connection-transport.js +202 -202
  28. package/dist/connection-types.d.ts +5 -4
  29. package/dist/connection.d.ts +47 -46
  30. package/dist/connection.js +320 -162
  31. package/dist/crypto-entry.d.ts +7 -0
  32. package/dist/crypto-entry.js +4 -0
  33. package/dist/crypto.js +4 -4
  34. package/dist/external-store.d.ts +10 -0
  35. package/dist/external-store.js +10 -0
  36. package/dist/file-transfer-internals.d.ts +23 -0
  37. package/dist/file-transfer-internals.js +224 -0
  38. package/dist/files.d.ts +36 -14
  39. package/dist/files.js +92 -222
  40. package/dist/hosted-file-request.js +3 -3
  41. package/dist/index.d.ts +24 -19
  42. package/dist/index.js +10 -18
  43. package/dist/internal-types.d.ts +9 -1
  44. package/dist/local-file-transport.d.ts +2 -2
  45. package/dist/local-file-transport.js +13 -13
  46. package/dist/mdbase-connect.d.ts +6 -4
  47. package/dist/mdbase-connect.js +9 -8
  48. package/dist/notifications.d.ts +3 -2
  49. package/dist/operation-helpers.d.ts +3 -3
  50. package/dist/operation-helpers.js +17 -34
  51. package/dist/operation-types.d.ts +435 -45
  52. package/dist/outcomes.d.ts +15 -15
  53. package/dist/outcomes.js +13 -1
  54. package/dist/pending-mutation-store.d.ts +13 -0
  55. package/dist/pending-mutation-store.js +64 -0
  56. package/dist/request-budget.d.ts +27 -0
  57. package/dist/request-budget.js +95 -0
  58. package/dist/runtime-utils.d.ts +2 -0
  59. package/dist/runtime-utils.js +19 -0
  60. package/dist/session.d.ts +5 -4
  61. package/dist/session.js +6 -5
  62. package/dist/stored-token.d.ts +10 -0
  63. package/dist/stored-token.js +60 -0
  64. package/dist/token-response.d.ts +11 -0
  65. package/dist/token-response.js +77 -0
  66. package/dist/watch-policy.d.ts +8 -0
  67. package/dist/watch-policy.js +12 -0
  68. package/package.json +12 -2
@@ -3,7 +3,7 @@ import { decryptRelayResponse, encryptRelayRequest } from "./crypto.js";
3
3
  import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
4
4
  import { GrantFileTransferCipher } from "./file-crypto.js";
5
5
  import { directFallbackStatus, loopbackRequest } from "./operation-helpers.js";
6
- import { apiError } from "./runtime-utils.js";
6
+ import { apiError, decodeJsonResponse } from "./runtime-utils.js";
7
7
  /** Keeps grant-bound file crypto and loopback framing out of the operation transport. */
8
8
  export class LocalFileTransport {
9
9
  options;
@@ -39,7 +39,7 @@ export class LocalFileTransport {
39
39
  }
40
40
  response = await this.sendRelayControl(token, encryptedRequest, signal);
41
41
  if (await refreshableBindingFailure(response, token)) {
42
- token = await this.options.refreshAuthorization();
42
+ token = await this.options.refreshAuthorization(signal);
43
43
  requireLocalFileToken(token);
44
44
  encryptedRequest = await this.encryptControl(token, controlInput);
45
45
  response = await this.sendRelayControl(token, encryptedRequest, signal);
@@ -49,7 +49,7 @@ export class LocalFileTransport {
49
49
  return this.decryptControlResponse(token, encryptedRequest, response);
50
50
  }
51
51
  async decryptControlResponse(token, encryptedRequest, response) {
52
- const body = await response.json().catch(() => ({}));
52
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file control response.");
53
53
  if (!response.ok) {
54
54
  throw apiError(body, "operation_failed", "Collection file request failed.", response.status);
55
55
  }
@@ -63,7 +63,7 @@ export class LocalFileTransport {
63
63
  return decrypted.result;
64
64
  }
65
65
  async uploadChunk(session, chunkIndex, bytes, signal) {
66
- const token = await this.localFileToken(session, "upload");
66
+ const token = await this.localFileToken(session, "upload", signal);
67
67
  let deliveryToken = token;
68
68
  let encoded = await encryptedUploadChunk(this.options.keyStore, deliveryToken, session, chunkIndex, bytes);
69
69
  let response;
@@ -86,19 +86,19 @@ export class LocalFileTransport {
86
86
  }
87
87
  response = await this.sendRelayUpload(deliveryToken, encoded, signal);
88
88
  if (response.status === 401 && deliveryToken.refreshToken) {
89
- deliveryToken = await this.options.refreshAuthorization();
89
+ deliveryToken = await this.options.refreshAuthorization(signal);
90
90
  requireLocalFileToken(deliveryToken);
91
91
  encoded = await encryptedUploadChunk(this.options.keyStore, deliveryToken, session, chunkIndex, bytes);
92
92
  response = await this.sendRelayUpload(deliveryToken, encoded, signal);
93
93
  }
94
94
  if (!response.ok) {
95
- const body = await response.json().catch(() => ({}));
95
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file upload response.");
96
96
  throw apiError(body, "operation_failed", "Encrypted file chunk upload failed.", response.status);
97
97
  }
98
98
  this.options.onRelayAvailable();
99
99
  }
100
100
  async downloadChunk(session, chunkIndex, signal) {
101
- let token = await this.localFileToken(session, "download");
101
+ let token = await this.localFileToken(session, "download", signal);
102
102
  let response;
103
103
  if (await this.options.shouldAttemptDirect(token)) {
104
104
  try {
@@ -119,7 +119,7 @@ export class LocalFileTransport {
119
119
  }
120
120
  response = await this.sendRelayDownload(token, session, chunkIndex, signal);
121
121
  if (response.status === 401 && token.refreshToken) {
122
- token = await this.options.refreshAuthorization();
122
+ token = await this.options.refreshAuthorization(signal);
123
123
  requireLocalFileToken(token);
124
124
  response = await this.sendRelayDownload(token, session, chunkIndex, signal);
125
125
  }
@@ -129,7 +129,7 @@ export class LocalFileTransport {
129
129
  }
130
130
  async decryptDownloadResponse(token, session, chunkIndex, response) {
131
131
  if (!response.ok) {
132
- const body = await response.json().catch(() => ({}));
132
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file download response.");
133
133
  throw apiError(body, "operation_failed", "Encrypted file chunk download failed.", response.status);
134
134
  }
135
135
  if (response.headers.get("content-type")?.split(";", 1)[0]
@@ -184,8 +184,8 @@ export class LocalFileTransport {
184
184
  signal
185
185
  });
186
186
  }
187
- async localFileToken(session, direction) {
188
- const token = await this.options.authorizedToken();
187
+ async localFileToken(session, direction, signal) {
188
+ const token = await this.options.authorizedToken(signal);
189
189
  if (!token || token.authority || !token.fileCapability || !token.encryption
190
190
  || !token.grantId || !token.keyHandle
191
191
  || session.direction !== direction
@@ -218,7 +218,7 @@ function fileUploadRequest(encoded, signal) {
218
218
  async function requireSuccessfulChunkResponse(response, message) {
219
219
  if (response.ok)
220
220
  return;
221
- const body = await response.json().catch(() => ({}));
221
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "The collection authority returned an invalid file transfer response.");
222
222
  throw apiError(body, "operation_failed", message, response.status);
223
223
  }
224
224
  async function refreshableBindingFailure(response, token) {
@@ -228,7 +228,7 @@ async function refreshableBindingFailure(response, token) {
228
228
  return true;
229
229
  if (response.status !== 409)
230
230
  return false;
231
- const body = await response.clone().json().catch(() => null);
231
+ const body = await decodeJsonResponse(response.clone(), "invalid_operation_response", "The collection authority returned an invalid file response.").catch(() => null);
232
232
  return body?.error?.code === "encryption_binding_stale";
233
233
  }
234
234
  function requireEncryption(token) {
@@ -6,15 +6,17 @@ import { type AuthorizationProblemCode, type ConnectOutcome, type RegistrationPr
6
6
  import { MdbaseApplicationSession, type MdbaseApplicationSessionOptions } from "./application-session.js";
7
7
  import type { MdbaseUnavailableReason } from "./session.js";
8
8
  import type { Application } from "./internal-types.js";
9
+ import type { ConnectRequestOptions } from "./operation-types.js";
9
10
  export declare class MdbaseConnect<Frontmatter extends JsonObject = JsonObject> {
10
11
  private readonly internals;
11
12
  constructor(options: MdbaseConnectOptions);
12
- register(): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
13
+ register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
13
14
  authorize(options?: MdbaseAuthorizeOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, AuthorizationProblemCode>>;
14
- createApplicationSession(options: MdbaseApplicationSessionOptions): MdbaseApplicationSession<Frontmatter>;
15
- manifest(): Promise<ConnectOutcome<import("@mdbase-dev/connect-protocol").MdbaseAppManifest, RegistrationProblemCode>>;
15
+ /** The ordinary application lifecycle entry point. */
16
+ application(options: MdbaseApplicationSessionOptions): MdbaseApplicationSession<Frontmatter>;
17
+ manifest(options?: ConnectRequestOptions): Promise<ConnectOutcome<import("@mdbase-dev/connect-protocol").MdbaseAppManifest, RegistrationProblemCode>>;
16
18
  environment(): MdbaseConnectEnvironment;
17
- completeAuthorization(callbackUrl?: string): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
19
+ completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
18
20
  connections(): MdbaseConnectionInfo[];
19
21
  connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
20
22
  unavailableReason(collectionId: string): MdbaseUnavailableReason | null;
@@ -7,23 +7,24 @@ export class MdbaseConnect {
7
7
  constructor(options) {
8
8
  this.internals = new MdbaseConnectInternals(options);
9
9
  }
10
- register() {
11
- return captureConnectOutcome(() => this.internals.register(), REGISTRATION_PROBLEM_CODES);
10
+ register(options) {
11
+ return captureConnectOutcome(() => this.internals.register(options), REGISTRATION_PROBLEM_CODES);
12
12
  }
13
13
  authorize(options = {}) {
14
14
  return captureConnectOutcome(() => this.internals.authorize(options), AUTHORIZATION_PROBLEM_CODES);
15
15
  }
16
- createApplicationSession(options) {
17
- return new MdbaseApplicationSession(this, options);
16
+ /** The ordinary application lifecycle entry point. */
17
+ application(options) {
18
+ return new MdbaseApplicationSession(this, options, this.internals.timeouts);
18
19
  }
19
- manifest() {
20
- return captureConnectOutcome(() => this.internals.manifestDeclaration(), REGISTRATION_PROBLEM_CODES);
20
+ manifest(options) {
21
+ return captureConnectOutcome(() => this.internals.manifestDeclaration(options), REGISTRATION_PROBLEM_CODES);
21
22
  }
22
23
  environment() {
23
24
  return this.internals.environment();
24
25
  }
25
- completeAuthorization(callbackUrl) {
26
- return captureConnectOutcome(() => this.internals.completeAuthorization(callbackUrl ?? defaultCallbackUrl()), AUTHORIZATION_PROBLEM_CODES);
26
+ completeAuthorization(callbackUrl, options) {
27
+ return captureConnectOutcome(() => this.internals.completeAuthorization(callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl), options), AUTHORIZATION_PROBLEM_CODES);
27
28
  }
28
29
  connections() {
29
30
  return this.internals.connections();
@@ -1,4 +1,5 @@
1
- export interface MdbaseNotificationRegistrationOptions {
1
+ import type { ConnectRequestOptions } from "./operation-types.js";
2
+ export interface MdbaseNotificationRegistrationOptions extends ConnectRequestOptions {
2
3
  serviceWorker: ServiceWorkerRegistration;
3
4
  /** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
4
5
  criteria?: string[];
@@ -10,7 +11,7 @@ export interface MdbaseNotificationRegistration {
10
11
  installationId: string;
11
12
  criteria: string[];
12
13
  }
13
- export interface MdbaseNativeNotificationRegistrationOptions {
14
+ export interface MdbaseNativeNotificationRegistrationOptions extends ConnectRequestOptions {
14
15
  /** Current FCM registration token. Refresh by calling this method again. */
15
16
  token: string;
16
17
  /** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
@@ -1,4 +1,4 @@
1
- import type { CollectionOperation } from "@mdbase-dev/connect-protocol";
1
+ import { type CollectionOperation } from "@mdbase-dev/connect-protocol";
2
2
  import { MdbaseConnectError } from "./errors.js";
3
3
  import type { StoredToken } from "./internal-types.js";
4
4
  import type { DeleteInput, DeletePreflightResult, MutationEstimate, RenameInput, RenamePreflightResult } from "./operation-types.js";
@@ -12,7 +12,7 @@ export declare function isMutation(operation: CollectionOperation, input?: unkno
12
12
  export declare function uniqueOperations(operations: CollectionOperation[]): CollectionOperation[];
13
13
  export declare function sameAuthorization(left: StoredToken, right: StoredToken): boolean;
14
14
  export declare function throwIfCancelled(signal?: AbortSignal): void;
15
- export declare function operationTransportError(error: unknown, signal: AbortSignal | undefined, outcomeUnknown: boolean, unavailableCode: "hosted_provider_unavailable" | "relay_unavailable"): Error;
15
+ export declare function operationTransportError(error: unknown, signal: AbortSignal | undefined, pendingRequestId: string | undefined, unavailableCode: "hosted_provider_unavailable" | "relay_unavailable"): Error;
16
16
  export declare function isCancellation(error: unknown, signal?: AbortSignal): boolean;
17
17
  export declare function assertRenamePreview(input: RenameInput, preview: RenamePreflightResult): void;
18
18
  export declare function assertDeletePreview(input: DeleteInput, preview: DeletePreflightResult): void;
@@ -21,4 +21,4 @@ export declare function deleteEstimate(preview: DeletePreflightResult): Mutation
21
21
  export declare function operationFingerprint(operation: CollectionOperation, input: unknown): Promise<string>;
22
22
  export declare function canonicalJson(value: unknown): string;
23
23
  export declare function sortJson(value: unknown): unknown;
24
- export declare function unknownMutationOutcome(cause: unknown): MdbaseConnectError;
24
+ export declare function unknownMutationOutcome(requestId: string, cause: unknown): MdbaseConnectError;
@@ -1,5 +1,5 @@
1
+ import { isMutatingOperation, mutationFingerprint } from "@mdbase-dev/connect-protocol";
1
2
  import { MdbaseConnectError, connectError } from "./errors.js";
2
- import { bytesToBase64Url } from "./base64.js";
3
3
  export function loopbackRequest(init) {
4
4
  return { ...init, credentials: "omit", targetAddressSpace: "loopback" };
5
5
  }
@@ -20,24 +20,7 @@ export function directFallbackStatus(status) {
20
20
  return status === 404 || status === 405 || status === 426 || status >= 500;
21
21
  }
22
22
  export function isMutation(operation, input) {
23
- if (input && typeof input === "object" && !Array.isArray(input)
24
- && input.dry_run === true)
25
- return false;
26
- return (operation === "sync"
27
- && input !== null
28
- && typeof input === "object"
29
- && !Array.isArray(input)
30
- && input.action === "mutate")
31
- || operation === "create"
32
- || operation === "update"
33
- || operation === "delete"
34
- || operation === "rename"
35
- || operation === "create_type"
36
- || operation === "update_type"
37
- || operation === "apply_type_pack"
38
- || operation === "put_timer"
39
- || operation === "cancel_timer"
40
- || operation === "reconcile_timers";
23
+ return isMutatingOperation(operation, input);
41
24
  }
42
25
  export function uniqueOperations(operations) {
43
26
  return [...new Set(operations)];
@@ -55,19 +38,21 @@ export function throwIfCancelled(signal) {
55
38
  return;
56
39
  throw connectError("operation_cancelled", "The operation was cancelled before it changed the collection.", { operationOutcome: "not_sent", cause: signal.reason });
57
40
  }
58
- export function operationTransportError(error, signal, outcomeUnknown, unavailableCode) {
41
+ export function operationTransportError(error, signal, pendingRequestId, unavailableCode) {
59
42
  // Problems raised before transport dispatch already describe the
60
43
  // authoritative outcome. Do not overwrite them merely because an older
61
44
  // pending mutation also exists in storage.
62
45
  if (error instanceof MdbaseConnectError)
63
46
  return error;
64
47
  if (signal?.aborted) {
65
- if (outcomeUnknown)
66
- return unknownMutationOutcome(error);
48
+ if (pendingRequestId)
49
+ return unknownMutationOutcome(pendingRequestId, error);
50
+ if (signal.reason instanceof MdbaseConnectError)
51
+ return signal.reason;
67
52
  return connectError("operation_cancelled", "The operation was cancelled before it changed the collection.", { operationOutcome: "not_sent", cause: error });
68
53
  }
69
- if (outcomeUnknown) {
70
- return unknownMutationOutcome(error);
54
+ if (pendingRequestId) {
55
+ return unknownMutationOutcome(pendingRequestId, error);
71
56
  }
72
57
  if (error instanceof TypeError) {
73
58
  return connectError(unavailableCode, unavailableCode === "hosted_provider_unavailable"
@@ -81,21 +66,21 @@ export function isCancellation(error, signal) {
81
66
  || (error instanceof MdbaseConnectError && error.code === "operation_cancelled");
82
67
  }
83
68
  export function assertRenamePreview(input, preview) {
84
- if (preview.dry_run !== true || preview.would_rename !== true
69
+ if (preview.dryRun !== true || preview.wouldRename !== true
85
70
  || preview.from !== input.from || preview.to !== input.to) {
86
71
  throw connectError("invalid_preflight", "The rename preview does not match this mutation. Run the preview again.");
87
72
  }
88
73
  }
89
74
  export function assertDeletePreview(input, preview) {
90
- if (preview.dry_run !== true || preview.would_delete !== true || preview.path !== input.path) {
75
+ if (preview.dryRun !== true || preview.wouldDelete !== true || preview.path !== input.path) {
91
76
  throw connectError("invalid_preflight", "The delete preview does not match this mutation. Run the preview again.");
92
77
  }
93
78
  }
94
79
  export function renameEstimate(input, preview) {
95
- if (input.update_refs === false) {
80
+ if (input.updateRefs === false) {
96
81
  return { affectedRecords: 0, totalUnits: 1, warnings: 0 };
97
82
  }
98
- const references = preview.references_affected ?? [];
83
+ const references = preview.referencesAffected ?? [];
99
84
  return {
100
85
  affectedRecords: new Set(references.map((reference) => reference.path)).size,
101
86
  totalUnits: 1 + references.length,
@@ -104,15 +89,13 @@ export function renameEstimate(input, preview) {
104
89
  }
105
90
  export function deleteEstimate(preview) {
106
91
  return {
107
- affectedRecords: new Set((preview.broken_links ?? []).map((reference) => reference.path)).size,
92
+ affectedRecords: new Set((preview.brokenLinks ?? []).map((reference) => reference.path)).size,
108
93
  totalUnits: 1,
109
94
  warnings: 0
110
95
  };
111
96
  }
112
97
  export async function operationFingerprint(operation, input) {
113
- const encoded = new TextEncoder().encode(`${operation}\0${canonicalJson(input ?? {})}`);
114
- const digest = await crypto.subtle.digest("SHA-256", encoded);
115
- return bytesToBase64Url(new Uint8Array(digest));
98
+ return mutationFingerprint(operation, input ?? {});
116
99
  }
117
100
  export function canonicalJson(value) {
118
101
  return JSON.stringify(sortJson(value));
@@ -128,6 +111,6 @@ export function sortJson(value) {
128
111
  }
129
112
  return value;
130
113
  }
131
- export function unknownMutationOutcome(cause) {
132
- return connectError("operation_outcome_unknown", "The write may have completed, but mdbase could not recover its authoritative result. Retry the exact same write to recover safely.", { operationOutcome: "unknown", cause });
114
+ export function unknownMutationOutcome(requestId, cause) {
115
+ return connectError("operation_outcome_unknown", "The write may have completed, but mdbase could not recover its authoritative result. Retry the exact same write to recover safely.", { operationOutcome: "unknown", details: { request_id: requestId }, cause });
133
116
  }