@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.
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
package/dist/connect.js CHANGED
@@ -3,21 +3,22 @@ import { abortableDelay } from "./async.js";
3
3
  import { randomBase64Url } from "./base64.js";
4
4
  import { MdbaseConnection } from "./connection.js";
5
5
  import { IndexedDbGrantKeyStore, MemoryGrantKeyStore, validateGrantEncryption } from "./crypto.js";
6
- import { MdbaseConnectError } from "./errors.js";
6
+ import { connectError, serverConnectError } from "./errors.js";
7
7
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
8
8
  import { uniqueOperations } from "./operation-helpers.js";
9
+ import { AUTHORIZATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome } from "./outcomes.js";
9
10
  import { MdbaseSession } from "./session.js";
10
- import { MemoryStorage, apiError, canonicalLoopbackUrl, createPkce, defaultCallbackUrl, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
11
+ import { MemoryStorage, apiError, canonicalLoopbackUrl, connectFetch, createPkce, defaultCallbackUrl, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validFileCapability, validAuthorityTokenResponse } from "./runtime-utils.js";
11
12
  export class MdbaseConnect {
12
13
  internals;
13
14
  constructor(options) {
14
15
  this.internals = new MdbaseConnectInternals(options);
15
16
  }
16
17
  register() {
17
- return this.internals.register();
18
+ return captureConnectOutcome(() => this.internals.register(), REGISTRATION_PROBLEM_CODES);
18
19
  }
19
20
  authorize(options = {}) {
20
- return this.internals.authorize(options);
21
+ return captureConnectOutcome(() => this.internals.authorize(options), AUTHORIZATION_PROBLEM_CODES);
21
22
  }
22
23
  createSession(options) {
23
24
  return new MdbaseSession(this, options);
@@ -25,8 +26,8 @@ export class MdbaseConnect {
25
26
  environment() {
26
27
  return this.internals.environment();
27
28
  }
28
- completeAuthorization(callbackUrl = defaultCallbackUrl()) {
29
- return this.internals.completeAuthorization(callbackUrl);
29
+ completeAuthorization(callbackUrl) {
30
+ return captureConnectOutcome(() => this.internals.completeAuthorization(callbackUrl ?? defaultCallbackUrl()), AUTHORIZATION_PROBLEM_CODES);
30
31
  }
31
32
  connections() {
32
33
  return this.internals.connections();
@@ -109,11 +110,11 @@ class MdbaseConnectInternals {
109
110
  async register() {
110
111
  if (this.application)
111
112
  return this.application;
112
- const response = await fetch(`${this.serverUrl}/v1/apps/register`, {
113
+ const response = await connectFetch(`${this.serverUrl}/v1/apps/register`, {
113
114
  method: "POST",
114
115
  headers: { "content-type": "application/json" },
115
116
  body: JSON.stringify({ manifest: await this.loadManifest() })
116
- });
117
+ }, "temporarily_unavailable", "Application registration is temporarily unavailable.");
117
118
  const body = await response.json();
118
119
  if (!response.ok)
119
120
  throw apiError(body, "discovery_failed", "Application discovery failed.", response.status);
@@ -131,21 +132,21 @@ class MdbaseConnectInternals {
131
132
  });
132
133
  }
133
134
  catch (cause) {
134
- throw new MdbaseConnectError("manifest_load_failed", "The bundled application declaration could not be loaded.", { cause });
135
+ throw connectError("manifest_load_failed", "The bundled application declaration could not be loaded.", { cause });
135
136
  }
136
137
  if (!response.ok) {
137
- throw new MdbaseConnectError("manifest_load_failed", `The bundled application declaration returned HTTP ${response.status}.`, { status: response.status });
138
+ throw connectError("manifest_load_failed", `The bundled application declaration returned HTTP ${response.status}.`, { status: response.status });
138
139
  }
139
140
  try {
140
141
  return await response.json();
141
142
  }
142
143
  catch (cause) {
143
- throw new MdbaseConnectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", { cause });
144
+ throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", { cause });
144
145
  }
145
146
  }
146
147
  async authorize(options = {}) {
147
148
  if (typeof location === "undefined" && !this.navigate && !options.openVerification) {
148
- throw new MdbaseConnectError("browser_required", "Authorization navigation requires a browser environment.");
149
+ throw connectError("browser_required", "Authorization navigation requires a browser environment.");
149
150
  }
150
151
  const portableDeclared = typeof this.manifest !== "string"
151
152
  && this.manifest.distribution === "portable";
@@ -213,7 +214,7 @@ class MdbaseConnectInternals {
213
214
  async authorizePortable(application, options, popup) {
214
215
  if (this.relayEncryption !== "required") {
215
216
  popup?.close();
216
- throw new MdbaseConnectError("encryption_required", "Downloaded applications require encrypted relay authorization.");
217
+ throw connectError("encryption_required", "Downloaded applications require encrypted relay authorization.");
217
218
  }
218
219
  const { verifier, challenge } = await createPkce();
219
220
  const keyHandle = `grant:${application.id}:${randomBase64Url(24)}`;
@@ -242,7 +243,7 @@ class MdbaseConnectInternals {
242
243
  catch (cause) {
243
244
  popup?.close();
244
245
  await this.keyStore.delete(keyHandle);
245
- throw new MdbaseConnectError("device_authorization_failed", "Downloaded application authorization could not be started.", { cause });
246
+ throw connectError("device_authorization_failed", "Downloaded application authorization could not be started.", { cause });
246
247
  }
247
248
  if (!response.ok) {
248
249
  popup?.close();
@@ -272,14 +273,22 @@ class MdbaseConnectInternals {
272
273
  }
273
274
  else {
274
275
  await this.keyStore.delete(keyHandle);
275
- throw new MdbaseConnectError("approval_window_blocked", "The approval window was blocked. Show the provided verification link and code, then try again.", { details: authorization });
276
+ throw connectError("approval_window_blocked", "The approval window was blocked. Show the provided verification link and code, then try again.", {
277
+ details: {
278
+ user_code: authorization.userCode,
279
+ verification_uri: authorization.verificationUri,
280
+ verification_uri_complete: authorization.verificationUriComplete,
281
+ expires_at: authorization.expiresAt,
282
+ interval_seconds: authorization.intervalSeconds
283
+ }
284
+ });
276
285
  }
277
286
  let intervalSeconds = authorization.intervalSeconds;
278
287
  try {
279
288
  while (Date.now() < authorization.expiresAt) {
280
289
  await abortableDelay(intervalSeconds * 1_000, options.signal);
281
290
  if (options.signal?.aborted) {
282
- throw new MdbaseConnectError("authorization_cancelled", "Downloaded application authorization was cancelled.");
291
+ throw connectError("authorization_cancelled", "Downloaded application authorization was cancelled.");
283
292
  }
284
293
  let tokenResponse;
285
294
  try {
@@ -297,7 +306,7 @@ class MdbaseConnectInternals {
297
306
  }
298
307
  catch (cause) {
299
308
  if (options.signal?.aborted) {
300
- throw new MdbaseConnectError("authorization_cancelled", "Downloaded application authorization was cancelled.", { cause });
309
+ throw connectError("authorization_cancelled", "Downloaded application authorization was cancelled.", { cause });
301
310
  }
302
311
  continue;
303
312
  }
@@ -319,22 +328,22 @@ class MdbaseConnectInternals {
319
328
  && tokenBody.encryption.application_agreement_public_key
320
329
  === grantKey.agreementPublicKey;
321
330
  if (tokenBody.application_origin !== "null") {
322
- throw new MdbaseConnectError("invalid_token_response", "Authorization did not bind the portable grant to its opaque application origin.");
331
+ throw connectError("invalid_token_response", "Authorization did not bind the portable grant to its opaque application origin.");
323
332
  }
324
333
  if (tokenBody.authority
325
334
  && (!authority
326
335
  || tokenBody.encryption != null
327
336
  || tokenBody.authority.proof_public_key !== grantKey.signingPublicKey)) {
328
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned a remote authority capability that is not bound to this portable grant key.");
337
+ throw connectError("invalid_token_response", "Authorization returned a remote authority capability that is not bound to this portable grant key.");
329
338
  }
330
339
  if (!tokenBody.authority && !localEncryption) {
331
- throw new MdbaseConnectError("encryption_required", "Authorization did not establish the expected key-bound local portable grant.");
340
+ throw connectError("encryption_required", "Authorization did not establish the expected key-bound local portable grant.");
332
341
  }
333
342
  const token = this.storeTokenResponse(tokenBody, application.id, keyHandle);
334
343
  if (options.target?.kind === "collection"
335
344
  && options.target.collectionId !== token.collectionId) {
336
345
  this.removeToken(token.collectionId, token.keyHandle);
337
- throw new MdbaseConnectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
346
+ throw connectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
338
347
  }
339
348
  popup?.close();
340
349
  return {
@@ -343,7 +352,7 @@ class MdbaseConnectInternals {
343
352
  ...(options.returnTo ? { returnTo: options.returnTo } : {})
344
353
  };
345
354
  }
346
- throw new MdbaseConnectError("expired_token", "The downloaded application authorization code expired.");
355
+ throw connectError("expired_token", "The downloaded application authorization code expired.");
347
356
  }
348
357
  catch (error) {
349
358
  popup?.close();
@@ -354,7 +363,7 @@ class MdbaseConnectInternals {
354
363
  completeAuthorization(callbackUrl) {
355
364
  const state = new URL(callbackUrl).searchParams.get("state");
356
365
  if (!state) {
357
- return Promise.reject(new MdbaseConnectError("invalid_callback", "Authorization callback is missing its state."));
366
+ return Promise.reject(connectError("invalid_callback", "Authorization callback is missing its state."));
358
367
  }
359
368
  const existing = this.completionPromises.get(state);
360
369
  if (existing)
@@ -373,18 +382,18 @@ class MdbaseConnectInternals {
373
382
  const pendingKey = this.pendingKey(state);
374
383
  const pending = parseStored(this.storage.getItem(pendingKey));
375
384
  if (!pending || pending.version !== 1 || state !== pending.state) {
376
- throw new MdbaseConnectError("invalid_callback", "Authorization callback is missing or does not match this browser session.");
385
+ throw connectError("invalid_callback", "Authorization callback is missing or does not match this browser session.");
377
386
  }
378
387
  if (callback.searchParams.has("error")) {
379
388
  if (pending.keyHandle)
380
389
  await this.keyStore.delete(pending.keyHandle);
381
390
  this.storage.removeItem(pendingKey);
382
- throw new MdbaseConnectError(callback.searchParams.get("error") ?? "access_denied", callback.searchParams.get("error_description") ?? "Collection access was not approved.", { details: pending.returnTo ? { returnTo: pending.returnTo } : undefined });
391
+ throw serverConnectError(callback.searchParams.get("error") ?? "access_denied", callback.searchParams.get("error_description") ?? "Collection access was not approved.", { details: pending.returnTo ? { return_to: pending.returnTo } : undefined });
383
392
  }
384
393
  if (!code) {
385
- throw new MdbaseConnectError("invalid_callback", "Authorization callback is missing its code.");
394
+ throw connectError("invalid_callback", "Authorization callback is missing its code.");
386
395
  }
387
- const response = await fetch(`${this.serverUrl}/oauth/token`, {
396
+ const response = await connectFetch(`${this.serverUrl}/oauth/token`, {
388
397
  method: "POST",
389
398
  headers: { "content-type": "application/x-www-form-urlencoded" },
390
399
  body: new URLSearchParams({
@@ -394,7 +403,7 @@ class MdbaseConnectInternals {
394
403
  redirect_uri: pending.redirectUri,
395
404
  code_verifier: pending.verifier
396
405
  })
397
- });
406
+ }, "temporarily_unavailable", "Authorization completion is temporarily unavailable.");
398
407
  const body = await response.json();
399
408
  if (!response.ok)
400
409
  throw apiError(body, "token_exchange_failed", "Authorization could not be completed.", response.status);
@@ -405,7 +414,7 @@ class MdbaseConnectInternals {
405
414
  if (pending.keyHandle)
406
415
  await this.keyStore.delete(pending.keyHandle);
407
416
  this.storage.removeItem(pendingKey);
408
- throw new MdbaseConnectError("encryption_required", "Authorization did not establish the required encrypted relay grant.");
417
+ throw connectError("encryption_required", "Authorization did not establish the required encrypted relay grant.");
409
418
  }
410
419
  if (body.authority?.proof_public_key) {
411
420
  if (!pending.keyHandle
@@ -414,7 +423,7 @@ class MdbaseConnectInternals {
414
423
  if (pending.keyHandle)
415
424
  await this.keyStore.delete(pending.keyHandle);
416
425
  this.storage.removeItem(pendingKey);
417
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned a remote authority capability bound to another application key.");
426
+ throw connectError("invalid_token_response", "Authorization returned a remote authority capability bound to another application key.");
418
427
  }
419
428
  }
420
429
  else if (body.authority && pending.keyHandle) {
@@ -424,7 +433,7 @@ class MdbaseConnectInternals {
424
433
  if (pending.collectionId && pending.collectionId !== token.collectionId) {
425
434
  this.removeToken(token.collectionId, token.keyHandle);
426
435
  this.storage.removeItem(pendingKey);
427
- throw new MdbaseConnectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
436
+ throw connectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
428
437
  }
429
438
  this.storage.removeItem(pendingKey);
430
439
  return {
@@ -462,29 +471,33 @@ class MdbaseConnectInternals {
462
471
  storeTokenResponse(body, clientId, keyHandle) {
463
472
  const collectionId = body.collection_id;
464
473
  if (typeof collectionId !== "string") {
465
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned no collection ID.");
474
+ throw connectError("invalid_token_response", "Authorization returned no collection ID.");
466
475
  }
467
476
  const scope = parseGrantScope(body.scope);
468
477
  if (!scope) {
469
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned no valid collection scope.");
478
+ throw connectError("invalid_token_response", "Authorization returned no valid collection scope.");
470
479
  }
471
480
  if (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
472
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
481
+ throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
473
482
  }
474
483
  if (body.authority && body.encryption) {
475
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned conflicting collection transports.");
484
+ throw connectError("invalid_token_response", "Authorization returned conflicting collection transports.");
485
+ }
486
+ if (body.file_capability !== null && body.file_capability !== undefined
487
+ && !validFileCapability(body.file_capability)) {
488
+ throw connectError("invalid_token_response", "Authorization returned an invalid file capability.");
476
489
  }
477
490
  if (body.encryption) {
478
491
  try {
479
492
  validateGrantEncryption(body.encryption);
480
493
  }
481
494
  catch {
482
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
495
+ throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
483
496
  }
484
497
  if (body.encryption.collection_id !== collectionId
485
498
  || typeof body.grant_id !== "string"
486
499
  || body.grant_id.length === 0) {
487
- throw new MdbaseConnectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
500
+ throw connectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
488
501
  }
489
502
  }
490
503
  const previous = parseStored(this.storage.getItem(this.tokenKey(collectionId)));
@@ -498,7 +511,7 @@ class MdbaseConnectInternals {
498
511
  !== body.encryption.connector_agreement_public_key
499
512
  || previous.encryption.application_agreement_public_key
500
513
  !== body.encryption.application_agreement_public_key)) {
501
- throw new MdbaseConnectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.", { requiresAuthorization: true, recovery: "reauthorize" });
514
+ throw connectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.");
502
515
  }
503
516
  if (previous?.keyHandle && previous.keyHandle !== keyHandle) {
504
517
  void this.keyStore.delete(previous.keyHandle).catch(() => undefined);
@@ -518,12 +531,14 @@ class MdbaseConnectInternals {
518
531
  : undefined,
519
532
  grantId: body.grant_id,
520
533
  encryption: body.encryption ?? undefined,
534
+ fileCapability: body.file_capability ?? undefined,
521
535
  applicationOrigin: body.application_origin ?? this.defaultApplicationOrigin(),
522
536
  keyHandle,
523
537
  savedAt: Date.now(),
524
538
  authority: body.authority ? {
525
539
  operationsUrl: body.authority.operations_url,
526
540
  syncUrl: body.authority.sync_url,
541
+ filesUrl: body.authority.files_url,
527
542
  replicaId: body.authority.replica_id,
528
543
  accessToken: body.authority.access_token,
529
544
  proofPublicKey: body.authority.proof_public_key
@@ -0,0 +1,31 @@
1
+ import type { FileTransferSession } from "@mdbase-dev/connect-protocol";
2
+ import type { GrantKeyStore } from "./crypto.js";
3
+ import type { StoredToken } from "./internal-types.js";
4
+ type ProofHeaders = (token: StoredToken, method: "GET" | "POST" | "DELETE", url: string, body: string | undefined, credential: string) => Promise<Record<string, string>>;
5
+ export interface ConnectionFileTransportOptions {
6
+ keyStore: GrantKeyStore;
7
+ serverUrl: string;
8
+ loopbackUrl: string;
9
+ authorizedToken(): Promise<StoredToken | null>;
10
+ refreshAuthorization(): Promise<StoredToken>;
11
+ shouldAttemptDirect(token: StoredToken): Promise<boolean>;
12
+ onDirectAvailable(): void;
13
+ onDirectUnavailable(): void;
14
+ onRelayAvailable(): void;
15
+ authorityProofHeaders: ProofHeaders;
16
+ }
17
+ /**
18
+ * Owns every transport concern specific to files. ConnectionTransport only
19
+ * supplies authorization, routing, and proof-signing policy.
20
+ */
21
+ export declare class ConnectionFileTransport {
22
+ private readonly options;
23
+ private readonly local;
24
+ constructor(options: ConnectionFileTransportOptions);
25
+ control<Result>(method: "GET" | "POST" | "DELETE", path?: string, input?: unknown, signal?: AbortSignal): Promise<Result>;
26
+ uploadChunk(session: FileTransferSession, chunkIndex: number, bytes: Uint8Array, signal?: AbortSignal): Promise<void>;
27
+ downloadChunk(session: FileTransferSession, chunkIndex: number, signal?: AbortSignal): Promise<Uint8Array>;
28
+ downloadHostedPart(session: FileTransferSession, partIndex: number, expectedLength: number, signal?: AbortSignal): Promise<ReadableStream<Uint8Array>>;
29
+ private requireFileToken;
30
+ }
31
+ export {};
@@ -0,0 +1,42 @@
1
+ import { connectError } from "./errors.js";
2
+ import { performHostedFilePartRequest, performHostedFileRequest } from "./hosted-file-request.js";
3
+ import { LocalFileTransport } from "./local-file-transport.js";
4
+ /**
5
+ * Owns every transport concern specific to files. ConnectionTransport only
6
+ * supplies authorization, routing, and proof-signing policy.
7
+ */
8
+ export class ConnectionFileTransport {
9
+ options;
10
+ local;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.local = new LocalFileTransport(options);
14
+ }
15
+ async control(method, path = "", input, signal) {
16
+ const token = await this.requireFileToken();
17
+ if (!token.authority) {
18
+ return this.local.control(token, method, path, input, signal);
19
+ }
20
+ return performHostedFileRequest(token, method, path, input, signal, this.options.refreshAuthorization, this.options.authorityProofHeaders);
21
+ }
22
+ uploadChunk(session, chunkIndex, bytes, signal) {
23
+ return this.local.uploadChunk(session, chunkIndex, bytes, signal);
24
+ }
25
+ downloadChunk(session, chunkIndex, signal) {
26
+ return this.local.downloadChunk(session, chunkIndex, signal);
27
+ }
28
+ async downloadHostedPart(session, partIndex, expectedLength, signal) {
29
+ const token = await this.requireFileToken();
30
+ if (!token.authority) {
31
+ throw connectError("not_remote_authority", "Hosted file delivery requires a remote authority endpoint.");
32
+ }
33
+ return performHostedFilePartRequest(token, `downloads/${encodeURIComponent(session.transfer_id)}/parts/${partIndex}`, expectedLength, signal, this.options.refreshAuthorization, this.options.authorityProofHeaders);
34
+ }
35
+ async requireFileToken() {
36
+ const token = await this.options.authorizedToken();
37
+ if (!token?.fileCapability) {
38
+ throw connectError("not_authorized", "This connection has no file capability.");
39
+ }
40
+ return token;
41
+ }
42
+ }
@@ -1,5 +1,5 @@
1
- import { MdbaseConnectError } from "./errors.js";
2
- import { apiError, parseStored } from "./runtime-utils.js";
1
+ import { connectError } from "./errors.js";
2
+ import { apiError, connectFetch, parseStored } from "./runtime-utils.js";
3
3
  import { base64UrlBytes, randomBase64Url } from "./base64.js";
4
4
  export class ConnectionNotifications {
5
5
  context;
@@ -9,19 +9,19 @@ export class ConnectionNotifications {
9
9
  async registerNotifications(options) {
10
10
  const token = await this.context.authorizedToken();
11
11
  if (!token) {
12
- throw new MdbaseConnectError("not_authorized", "Connect this application before enabling notifications.");
12
+ throw connectError("not_authorized", "Connect this application before enabling notifications.");
13
13
  }
14
14
  const application = await this.context.register();
15
15
  const declared = application.notifications?.criteria.map((criterion) => criterion.id) ?? [];
16
16
  const criteria = [...new Set(options.criteria ?? declared)];
17
17
  const undeclared = criteria.find((criterion) => !declared.includes(criterion));
18
18
  if (undeclared) {
19
- throw new MdbaseConnectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
19
+ throw connectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
20
20
  }
21
21
  if (criteria.length === 0) {
22
- throw new MdbaseConnectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
22
+ throw connectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
23
23
  }
24
- const keyResponse = await fetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`);
24
+ const keyResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`, undefined, "notifications_unavailable", "Push notifications are unavailable.");
25
25
  const keyBody = await keyResponse.json();
26
26
  if (!keyResponse.ok) {
27
27
  throw apiError(keyBody, "notifications_unavailable", "Push notifications are unavailable.", keyResponse.status);
@@ -35,13 +35,13 @@ export class ConnectionNotifications {
35
35
  }
36
36
  const serialized = pushSubscription.toJSON();
37
37
  if (!serialized.endpoint || !serialized.keys?.p256dh || !serialized.keys.auth) {
38
- throw new MdbaseConnectError("invalid_push_subscription", "The browser returned an incomplete push subscription.");
38
+ throw connectError("invalid_push_subscription", "The browser returned an incomplete push subscription.");
39
39
  }
40
40
  const previous = parseStored(this.context.storage.getItem(this.context.notificationKey()));
41
41
  const installationId = options.installationId
42
42
  ?? previous?.installationId
43
43
  ?? randomBase64Url(24);
44
- const channelResponse = await fetch(`${this.context.serverUrl}/v1/notifications/channels`, {
44
+ const channelResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels`, {
45
45
  method: "POST",
46
46
  headers: {
47
47
  authorization: `Bearer ${token.accessToken}`,
@@ -56,7 +56,7 @@ export class ConnectionNotifications {
56
56
  keys: serialized.keys
57
57
  }
58
58
  })
59
- });
59
+ }, "notification_registration_failed", "Could not register push notifications.");
60
60
  const channelBody = await channelResponse.json();
61
61
  if (!channelResponse.ok) {
62
62
  throw apiError(channelBody, "notification_registration_failed", "Could not register push notifications.", channelResponse.status);
@@ -84,27 +84,27 @@ export class ConnectionNotifications {
84
84
  async registerNativeNotifications(options) {
85
85
  const token = await this.context.authorizedToken();
86
86
  if (!token) {
87
- throw new MdbaseConnectError("not_authorized", "Connect this application before enabling notifications.");
87
+ throw connectError("not_authorized", "Connect this application before enabling notifications.");
88
88
  }
89
89
  const application = await this.context.register();
90
90
  if (application.notifications?.native_delivery?.mode !== "managed_fcm") {
91
- throw new MdbaseConnectError("managed_fcm_not_declared", "This application does not declare Connect-managed native notifications.");
91
+ throw connectError("managed_fcm_not_declared", "This application does not declare Connect-managed native notifications.");
92
92
  }
93
93
  const declared = application.notifications.criteria.map((criterion) => criterion.id);
94
94
  const criteria = [...new Set(options.criteria ?? declared)];
95
95
  const undeclared = criteria.find((criterion) => !declared.includes(criterion));
96
96
  if (undeclared) {
97
- throw new MdbaseConnectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
97
+ throw connectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
98
98
  }
99
99
  if (criteria.length === 0) {
100
- throw new MdbaseConnectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
100
+ throw connectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
101
101
  }
102
102
  const storageKey = this.context.notificationKey("fcm");
103
103
  const previous = parseStored(this.context.storage.getItem(storageKey));
104
104
  const installationId = options.installationId
105
105
  ?? previous?.installationId
106
106
  ?? randomBase64Url(24);
107
- const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels`, {
107
+ const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels`, {
108
108
  method: "POST",
109
109
  headers: {
110
110
  authorization: `Bearer ${token.accessToken}`,
@@ -116,7 +116,7 @@ export class ConnectionNotifications {
116
116
  transport: "fcm",
117
117
  token: options.token
118
118
  })
119
- });
119
+ }, "notification_registration_failed", "Could not register native notifications.");
120
120
  const body = await response.json();
121
121
  if (!response.ok) {
122
122
  throw apiError(body, "notification_registration_failed", "Could not register native notifications.", response.status);
@@ -139,7 +139,7 @@ export class ConnectionNotifications {
139
139
  const registration = parseStored(this.context.storage.getItem(storageKey));
140
140
  const token = await this.context.authorizedToken();
141
141
  if (registration?.channelId && !token) {
142
- throw new MdbaseConnectError("not_authorized", "Reconnect this application before disabling native notifications.");
142
+ throw connectError("not_authorized", "Reconnect this application before disabling native notifications.");
143
143
  }
144
144
  if (registration?.channelId && token) {
145
145
  await this.deleteNotificationChannel(registration.channelId, token.accessToken);
@@ -150,13 +150,13 @@ export class ConnectionNotifications {
150
150
  const registration = parseStored(this.context.storage.getItem(this.context.notificationKey()));
151
151
  const token = await this.context.authorizedToken();
152
152
  if (registration?.channelId && !token) {
153
- throw new MdbaseConnectError("not_authorized", "Reconnect this application before disabling push notifications.");
153
+ throw connectError("not_authorized", "Reconnect this application before disabling push notifications.");
154
154
  }
155
155
  if (registration?.channelId && token) {
156
- const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels/${registration.channelId}`, {
156
+ const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${registration.channelId}`, {
157
157
  method: "DELETE",
158
158
  headers: { authorization: `Bearer ${token.accessToken}` }
159
- });
159
+ }, "notification_unregistration_failed", "Could not unregister push notifications.");
160
160
  if (!response.ok && response.status !== 404) {
161
161
  const body = await response.json();
162
162
  throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
@@ -167,10 +167,10 @@ export class ConnectionNotifications {
167
167
  await subscription?.unsubscribe();
168
168
  }
169
169
  async deleteNotificationChannel(channelId, accessToken) {
170
- const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels/${channelId}`, {
170
+ const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${channelId}`, {
171
171
  method: "DELETE",
172
172
  headers: { authorization: `Bearer ${accessToken}` }
173
- });
173
+ }, "notification_unregistration_failed", "Could not unregister push notifications.");
174
174
  if (!response.ok && response.status !== 404) {
175
175
  const body = await response.json();
176
176
  throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
@@ -3,6 +3,7 @@ import { type GrantKeyStore } from "./crypto.js";
3
3
  import type { StoredToken } from "./internal-types.js";
4
4
  import type { DirectAccessStatus, MdbaseConnectionRoute } from "./connection-types.js";
5
5
  import type { OperationRequestOptions, PendingMutationSummary } from "./operation-types.js";
6
+ import { ConnectionFileTransport } from "./connection-file-transport.js";
6
7
  export interface ConnectionTransportInternals {
7
8
  readonly relayEncryption: "required" | "disabled";
8
9
  removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant"): void;
@@ -31,6 +32,7 @@ export declare class ConnectionTransport {
31
32
  private readonly collectionId;
32
33
  private readonly internals;
33
34
  private readonly onChange;
35
+ readonly files: ConnectionFileTransport;
34
36
  private refreshPromise;
35
37
  private directStatus;
36
38
  private currentRoute;