@mdbase-dev/connect 0.1.0-beta.32 → 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.
@@ -0,0 +1,13 @@
1
+ import { MdbaseConnectError, connectError } from "./errors.js";
2
+ export function declarationIdFromFamilyIdentity(familyIdentity) {
3
+ const prefix = "bundle:";
4
+ if (!familyIdentity.startsWith(prefix) || familyIdentity.length === prefix.length) {
5
+ throw new Error("The registered application has no valid declaration identity.");
6
+ }
7
+ return familyIdentity.slice(prefix.length);
8
+ }
9
+ export function authorizationAbort(signal, message, cause) {
10
+ if (signal.reason instanceof MdbaseConnectError)
11
+ return signal.reason;
12
+ return connectError("authorization_cancelled", message, { cause });
13
+ }
package/dist/connect.js CHANGED
@@ -3,8 +3,9 @@ import { abortableDelay } from "./async.js";
3
3
  import { randomBase64Url } from "./base64.js";
4
4
  import { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationIdentity, applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
5
5
  import { MdbaseConnection } from "./connection.js";
6
+ import { authorizationAbort, declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
6
7
  import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
7
- import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
8
+ import { connectError, serverConnectError } from "./errors.js";
8
9
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
9
10
  import { uniqueOperations } from "./operation-helpers.js";
10
11
  import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
@@ -197,6 +198,7 @@ export class MdbaseConnectInternals {
197
198
  protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
198
199
  authorization_id: authorizationId,
199
200
  application_id: application.id,
201
+ application_declaration_id: declarationIdFromFamilyIdentity(application.family_identity),
200
202
  application_manifest_digest: application.manifest_digest,
201
203
  application_installation_id: await applicationInstallationId(installation),
202
204
  installation_signing_public_key: installation.signingPublicKey,
@@ -299,6 +301,7 @@ export class MdbaseConnectInternals {
299
301
  protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
300
302
  authorization_id: authorizationId,
301
303
  application_id: application.id,
304
+ application_declaration_id: declarationIdFromFamilyIdentity(application.family_identity),
302
305
  application_manifest_digest: application.manifest_digest,
303
306
  application_installation_id: await applicationInstallationId(installation),
304
307
  installation_signing_public_key: installation.signingPublicKey,
@@ -714,8 +717,3 @@ export class MdbaseConnectInternals {
714
717
  listener(connections);
715
718
  }
716
719
  }
717
- function authorizationAbort(signal, message, cause) {
718
- if (signal.reason instanceof MdbaseConnectError)
719
- return signal.reason;
720
- return connectError("authorization_cancelled", message, { cause });
721
- }
@@ -1,11 +1,11 @@
1
- import type { CollectionChangesPage, CollectionDescription, CollectionOperation, CollectionTypeDocument, CreateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, FileCapability, GrantScope, JsonObject, ReadViewSourceInput, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, ApplyTypePackInput, AssessTypePackInput, TypePackApplyResult, TypePackAssessment, UpdateViewSourceInput } from "@mdbase-dev/connect-protocol";
1
+ import type { CollectionOperation, CollectionTypeDocument, FileCapability, GrantScope, JsonObject } from "@mdbase-dev/connect-protocol";
2
2
  import { type ConnectionTransportInternals } from "./connection-transport.js";
3
3
  import type { DirectAccessStatus, MdbaseAuthorizationCapabilities, MdbaseConnectionInfo, MdbaseConnectionRoute, MdbaseSyncConnection } from "./connection-types.js";
4
4
  import type { GrantKeyStore } from "./crypto.js";
5
5
  import { MdbaseFileClient } from "./files.js";
6
6
  import { type Application } from "./internal-types.js";
7
7
  import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.js";
8
- import type { ChangesInput, CreateInput, CreateTypeInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, UpdateInput, UpdateTypeInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
8
+ import type { ChangesInput, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, ApplyCollectionSetupInput, ApplyTypePackInput, AssessCollectionSetupInput, AssessTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryAllOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
9
9
  import { type AuthorizationProblemCode, type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome, type DirectAccessProblemCode, type NotificationProblemCode, type RegistrationProblemCode } from "./outcomes.js";
10
10
  import type { MdbaseDeviceAuthorization } from "./authorization-types.js";
11
11
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
@@ -109,7 +109,7 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
109
109
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
110
110
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
111
111
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
112
- queryAll(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
112
+ queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
113
113
  listViews(options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewList, CollectionReadProblemCode>>;
114
114
  executeView(input: ExecuteViewInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;
115
115
  readViewSource(input: ReadViewSourceInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewSourceDocument, CollectionReadProblemCode>>;
@@ -133,10 +133,12 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
133
133
  updateType(input: UpdateTypeInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
134
134
  assessTypePack(input: AssessTypePackInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<TypePackAssessment, CollectionTypeProblemCode>>;
135
135
  applyTypePack(input: ApplyTypePackInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<TypePackApplyResult, CollectionTypeProblemCode>>;
136
+ assessCollectionSetup(input: AssessCollectionSetupInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionSetupAssessment, CollectionTypeProblemCode>>;
137
+ applyCollectionSetup(input: ApplyCollectionSetupInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionSetupApplyResult, CollectionTypeProblemCode>>;
136
138
  listTimers(namespace: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseTimerList, CollectionReadProblemCode>>;
137
139
  putTimer(input: {
138
140
  namespace: string;
139
- criterion_id: string;
141
+ criterionId: string;
140
142
  timer: MdbaseDesiredTimer;
141
143
  }, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseTimer, CollectionMutationProblemCode>>;
142
144
  cancelTimer(input: {
@@ -150,10 +152,9 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
150
152
  }, CollectionMutationProblemCode>>;
151
153
  reconcileTimers(input: {
152
154
  namespace: string;
153
- criterion_id: string;
155
+ criterionId: string;
154
156
  timers: MdbaseDesiredTimer[];
155
157
  }, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseTimerReconciliation, CollectionMutationProblemCode>>;
156
158
  watch(input?: WatchInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseWatchSubscription, CollectionChangesProblemCode>>;
157
- operation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<ConnectOutcome<Result>>;
158
159
  private emitConnection;
159
160
  }
@@ -6,7 +6,7 @@ import { MdbaseFileClient } from "./files.js";
6
6
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
7
7
  import { assertDeletePreview, assertRenamePreview, deleteEstimate, isCancellation, renameEstimate, throwIfCancelled, uniqueOperations } from "./operation-helpers.js";
8
8
  import { AUTHORIZATION_PROBLEM_CODES, COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, DIRECT_ACCESS_PROBLEM_CODES, NOTIFICATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
9
- import { withRequestBudget } from "./request-budget.js";
9
+ import { withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
10
10
  export class MdbaseConnection {
11
11
  internals;
12
12
  collectionId;
@@ -37,7 +37,7 @@ export class MdbaseConnection {
37
37
  }, internals.timeouts);
38
38
  this.collectionClient = new MdbaseCollectionClient({
39
39
  operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
40
- });
40
+ }, internals.timeouts.requestMs);
41
41
  this.notifications = new ConnectionNotifications({
42
42
  serverUrl: internals.serverUrl,
43
43
  storage: internals.storage,
@@ -267,108 +267,116 @@ export class MdbaseConnection {
267
267
  return this.collectionClient.preflightRename(input, options);
268
268
  }
269
269
  async renameWithProgress(input, options = {}) {
270
- const started = Date.now();
271
- const resumed = false;
272
- let estimate;
273
- const emit = (state, cancellable, completedUnits = 0) => {
274
- options.onProgress?.({
275
- operation: "rename",
276
- state,
277
- elapsedMs: Date.now() - started,
278
- cancellable,
279
- resumed,
280
- completedUnits,
281
- ...(estimate ? { estimate } : {})
282
- });
283
- };
284
- try {
285
- throwIfCancelled(options.signal);
286
- emit("preflighting", true);
287
- const previewOutcome = options.preflight
288
- ? connectSuccess(options.preflight)
289
- : await this.preflightRename(input, { signal: options.signal });
290
- if (!previewOutcome.ok)
291
- return previewOutcome;
292
- const preview = previewOutcome.value;
270
+ return withCooperativeRequestBudget(options, this.internals.timeouts.requestMs, async (budget) => {
271
+ const started = Date.now();
272
+ const resumed = false;
273
+ let estimate;
274
+ const emit = (state, cancellable, completedUnits = 0) => {
275
+ options.onProgress?.({
276
+ operation: "rename",
277
+ state,
278
+ elapsedMs: Date.now() - started,
279
+ cancellable,
280
+ resumed,
281
+ completedUnits,
282
+ ...(estimate ? { estimate } : {})
283
+ });
284
+ };
293
285
  try {
294
- assertRenamePreview(input, preview);
286
+ throwIfCancelled(budget.signal);
287
+ emit("preflighting", true);
288
+ const previewOutcome = options.preflight
289
+ ? connectSuccess(options.preflight)
290
+ : await this.preflightRename(input, { signal: budget.signal, timeoutMs: null });
291
+ if (!previewOutcome.ok)
292
+ return previewOutcome;
293
+ const preview = previewOutcome.value;
294
+ try {
295
+ assertRenamePreview(input, preview);
296
+ }
297
+ catch (error) {
298
+ if (error instanceof MdbaseConnectError) {
299
+ return connectFailure(error.problem);
300
+ }
301
+ throw error;
302
+ }
303
+ estimate = renameEstimate(input, preview);
304
+ emit("ready", true);
305
+ throwIfCancelled(budget.signal);
306
+ const cancellable = this.transport.hasResumableMutationTransport();
307
+ emit("applying", cancellable);
308
+ const result = await this.rename(input, cancellable
309
+ ? { signal: budget.signal, timeoutMs: null }
310
+ : { timeoutMs: remainingTimeout(budget.remainingMs()) });
311
+ if (result.ok)
312
+ emit("completed", false, estimate.totalUnits);
313
+ return result;
295
314
  }
296
315
  catch (error) {
316
+ if (isCancellation(error, budget.signal))
317
+ emit("cancelled", false);
297
318
  if (error instanceof MdbaseConnectError) {
298
319
  return connectFailure(error.problem);
299
320
  }
300
321
  throw error;
301
322
  }
302
- estimate = renameEstimate(input, preview);
303
- emit("ready", true);
304
- throwIfCancelled(options.signal);
305
- const cancellable = this.transport.hasResumableMutationTransport();
306
- emit("applying", cancellable);
307
- const result = await this.rename(input, cancellable ? { signal: options.signal } : undefined);
308
- if (result.ok)
309
- emit("completed", false, estimate.totalUnits);
310
- return result;
311
- }
312
- catch (error) {
313
- if (isCancellation(error, options.signal))
314
- emit("cancelled", false);
315
- if (error instanceof MdbaseConnectError) {
316
- return connectFailure(error.problem);
317
- }
318
- throw error;
319
- }
323
+ });
320
324
  }
321
325
  async deleteWithProgress(input, options = {}) {
322
- const started = Date.now();
323
- const resumed = false;
324
- let estimate;
325
- const emit = (state, cancellable, completedUnits = 0) => {
326
- options.onProgress?.({
327
- operation: "delete",
328
- state,
329
- elapsedMs: Date.now() - started,
330
- cancellable,
331
- resumed,
332
- completedUnits,
333
- ...(estimate ? { estimate } : {})
334
- });
335
- };
336
- try {
337
- throwIfCancelled(options.signal);
338
- emit("preflighting", true);
339
- const previewOutcome = options.preflight
340
- ? connectSuccess(options.preflight)
341
- : await this.preflightDelete(input, { signal: options.signal });
342
- if (!previewOutcome.ok)
343
- return previewOutcome;
344
- const preview = previewOutcome.value;
326
+ return withCooperativeRequestBudget(options, this.internals.timeouts.requestMs, async (budget) => {
327
+ const started = Date.now();
328
+ const resumed = false;
329
+ let estimate;
330
+ const emit = (state, cancellable, completedUnits = 0) => {
331
+ options.onProgress?.({
332
+ operation: "delete",
333
+ state,
334
+ elapsedMs: Date.now() - started,
335
+ cancellable,
336
+ resumed,
337
+ completedUnits,
338
+ ...(estimate ? { estimate } : {})
339
+ });
340
+ };
345
341
  try {
346
- assertDeletePreview(input, preview);
342
+ throwIfCancelled(budget.signal);
343
+ emit("preflighting", true);
344
+ const previewOutcome = options.preflight
345
+ ? connectSuccess(options.preflight)
346
+ : await this.preflightDelete(input, { signal: budget.signal, timeoutMs: null });
347
+ if (!previewOutcome.ok)
348
+ return previewOutcome;
349
+ const preview = previewOutcome.value;
350
+ try {
351
+ assertDeletePreview(input, preview);
352
+ }
353
+ catch (error) {
354
+ if (error instanceof MdbaseConnectError) {
355
+ return connectFailure(error.problem);
356
+ }
357
+ throw error;
358
+ }
359
+ estimate = deleteEstimate(preview);
360
+ emit("ready", true);
361
+ throwIfCancelled(budget.signal);
362
+ const cancellable = this.transport.hasResumableMutationTransport();
363
+ emit("applying", cancellable);
364
+ const result = await this.delete(input, cancellable
365
+ ? { signal: budget.signal, timeoutMs: null }
366
+ : { timeoutMs: remainingTimeout(budget.remainingMs()) });
367
+ if (result.ok)
368
+ emit("completed", false, estimate.totalUnits);
369
+ return result;
347
370
  }
348
371
  catch (error) {
372
+ if (isCancellation(error, budget.signal))
373
+ emit("cancelled", false);
349
374
  if (error instanceof MdbaseConnectError) {
350
375
  return connectFailure(error.problem);
351
376
  }
352
377
  throw error;
353
378
  }
354
- estimate = deleteEstimate(preview);
355
- emit("ready", true);
356
- throwIfCancelled(options.signal);
357
- const cancellable = this.transport.hasResumableMutationTransport();
358
- emit("applying", cancellable);
359
- const result = await this.delete(input, cancellable ? { signal: options.signal } : undefined);
360
- if (result.ok)
361
- emit("completed", false, estimate.totalUnits);
362
- return result;
363
- }
364
- catch (error) {
365
- if (isCancellation(error, options.signal))
366
- emit("cancelled", false);
367
- if (error instanceof MdbaseConnectError) {
368
- return connectFailure(error.problem);
369
- }
370
- throw error;
371
- }
379
+ });
372
380
  }
373
381
  pendingMutations() {
374
382
  return this.transport.pendingMutations().map((summary) => ({
@@ -413,6 +421,12 @@ export class MdbaseConnection {
413
421
  applyTypePack(input, options) {
414
422
  return this.collectionClient.applyTypePack(input, options);
415
423
  }
424
+ assessCollectionSetup(input, options) {
425
+ return this.collectionClient.assessCollectionSetup(input, options);
426
+ }
427
+ applyCollectionSetup(input, options) {
428
+ return this.collectionClient.applyCollectionSetup(input, options);
429
+ }
416
430
  listTimers(namespace, options) {
417
431
  return this.collectionClient.listTimers(namespace, options);
418
432
  }
@@ -436,9 +450,6 @@ export class MdbaseConnection {
436
450
  return new CollectionWatchSubscription(this.collectionClient, initial.value.cursor, input, input.cursor === undefined ? [] : initial.value.events, this.internals.timeouts.watchStartMs);
437
451
  }), COLLECTION_CHANGES_PROBLEM_CODES);
438
452
  }
439
- operation(operation, input, options) {
440
- return this.collectionClient.operation(operation, input, options);
441
- }
442
453
  emitConnection() {
443
454
  const connection = this.info();
444
455
  for (const listener of this.connectionListeners)
@@ -559,3 +570,6 @@ function syncRequestOptions(options, defaultTimeoutMs) {
559
570
  timeoutMs: options?.timeoutMs === undefined ? defaultTimeoutMs : options.timeoutMs
560
571
  };
561
572
  }
573
+ function remainingTimeout(remainingMs) {
574
+ return remainingMs === null ? null : Math.max(1, Math.ceil(remainingMs));
575
+ }
@@ -1,4 +1,7 @@
1
1
  /** Opt-in cryptographic storage and transport primitives. */
2
- export * from "./crypto.js";
3
- export * from "./file-crypto.js";
4
- export * from "./application-identity.js";
2
+ export { IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, signAuthorityRequest, authorityProofMessage, encryptRelayRequest, decryptRelayResponse, deriveP256SharedSecret, validateGrantEncryption } from "./crypto.js";
3
+ export type { GrantKeyRecord, GrantKeyStore, RelayBinding, AuthorityProofInput } from "./crypto.js";
4
+ export { FileTransferCryptoError, GrantFileTransferCipher, deriveFileTransferKey } from "./file-crypto.js";
5
+ export type { FileTransferBinding } from "./file-crypto.js";
6
+ export { IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, ApplicationIdentityStoreError, applicationInstallationId, signApplicationAuthorization, applicationIdentity } from "./application-identity.js";
7
+ export type { ApplicationIdentity, ApplicationIdentityStore } from "./application-identity.js";
@@ -1,4 +1,4 @@
1
1
  /** Opt-in cryptographic storage and transport primitives. */
2
- export * from "./crypto.js";
3
- export * from "./file-crypto.js";
4
- export * from "./application-identity.js";
2
+ export { IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, signAuthorityRequest, authorityProofMessage, encryptRelayRequest, decryptRelayResponse, deriveP256SharedSecret, validateGrantEncryption } from "./crypto.js";
3
+ export { FileTransferCryptoError, GrantFileTransferCipher, deriveFileTransferKey } from "./file-crypto.js";
4
+ export { IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, ApplicationIdentityStoreError, applicationInstallationId, signApplicationAuthorization, applicationIdentity } from "./application-identity.js";
@@ -1,4 +1,4 @@
1
- import type { FileTransferSession, PreparedFilePart } from "@mdbase-dev/connect-protocol";
1
+ import type { FileTransferSession, FileTransferStatus, PreparedFilePart } from "@mdbase-dev/connect-protocol";
2
2
  import { MdbaseConnectError } from "./errors.js";
3
3
  export declare const MAX_OBJECT_ATTEMPTS = 3;
4
4
  export declare const MAX_BUFFERED_DOWNLOAD_BYTES: number;
@@ -13,6 +13,7 @@ export declare function uploadBodyBytes(body: UploadPartBody): Promise<Uint8Arra
13
13
  export declare function validConcurrency(value?: number): number;
14
14
  export declare function validPageSize(value: number): number;
15
15
  export declare function requireTransferSession(session: FileTransferSession, transferId: string, direction: "upload" | "download"): void;
16
+ export declare function requireTransferStatus(status: FileTransferStatus, session: FileTransferSession): void;
16
17
  export declare function chunkLength(totalSize: number, partSize: number, index: number): number;
17
18
  export declare function retryChunk<Result>(work: () => Promise<Result>, signal?: AbortSignal): Promise<Result>;
18
19
  export declare function requirePreparedPart(part: PreparedFilePart, transferId: string, partIndex: number, offset: number, contentLength: number, method: "PUT"): void;
@@ -90,6 +90,44 @@ export function requireTransferSession(session, transferId, direction) {
90
90
  throw connectError("invalid_operation_response", "The authority returned invalid uploaded part receipts.");
91
91
  }
92
92
  }
93
+ export function requireTransferStatus(status, session) {
94
+ const partSize = session.strategy.kind === "framed_chunks"
95
+ ? session.strategy.chunk_size
96
+ : session.strategy.kind === "object_put"
97
+ ? Math.max(1, session.total_size)
98
+ : session.strategy.part_size;
99
+ const partCount = session.strategy.kind === "object_put"
100
+ ? 1
101
+ : Math.ceil(session.total_size / partSize);
102
+ if (status?.protocol_version !== 1
103
+ || status.type !== "file_transfer_status"
104
+ || status.transfer_id !== session.transfer_id
105
+ || !["open", "committed", "aborted", "expired"].includes(status.state)
106
+ || !Array.isArray(status.received)
107
+ || new Set(status.received).size !== status.received.length
108
+ || status.received.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= partCount)
109
+ || !Number.isSafeInteger(status.received_bytes)
110
+ || status.received_bytes < 0
111
+ || status.received_bytes > session.total_size
112
+ || status.received.reduce((total, index) => total + chunkLength(session.total_size, partSize, index), 0) !== status.received_bytes) {
113
+ throw connectError("invalid_operation_response", "The authority returned invalid transfer status.");
114
+ }
115
+ const uploadedParts = status.uploaded_parts;
116
+ if (!Array.isArray(uploadedParts)
117
+ || uploadedParts.some((part, index) => !Number.isSafeInteger(part?.part_number)
118
+ || part.part_number < 1
119
+ || part.part_number > partCount
120
+ || typeof part.etag !== "string"
121
+ || part.etag.length === 0
122
+ || part.etag.length > 255
123
+ || (index > 0 && uploadedParts[index - 1].part_number >= part.part_number))
124
+ || (session.strategy.kind === "object_multipart"
125
+ ? uploadedParts.length !== status.received.length
126
+ || uploadedParts.some((part, index) => part.part_number - 1 !== status.received[index])
127
+ : uploadedParts.length !== 0)) {
128
+ throw connectError("invalid_operation_response", "The authority returned invalid uploaded part status.");
129
+ }
130
+ }
93
131
  function validTransferStrategy(session) {
94
132
  if (session.strategy.kind === "framed_chunks") {
95
133
  return session.protection === "grant_aead_v1"
package/dist/files.d.ts CHANGED
@@ -1,8 +1,26 @@
1
- import type { CollectionFileDescriptor, DeleteFileReceipt, FileCapability, FileTransferSession } from "@mdbase-dev/connect-protocol";
1
+ import type { FileCapability, FileTransferSession } from "@mdbase-dev/connect-protocol";
2
2
  import { type FileSource } from "./file-transfer-internals.js";
3
3
  import type { ConnectRequestOptions } from "./operation-types.js";
4
4
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
5
5
  export type MdbaseFileSource = FileSource;
6
+ export interface CollectionFileDescriptor {
7
+ fileId: string;
8
+ path: string;
9
+ revision: string;
10
+ contentDigest: `sha256:${string}`;
11
+ size: number;
12
+ mediaType?: string;
13
+ mediaClass: import("@mdbase-dev/connect-protocol").FileMediaClass;
14
+ modifiedAt: string;
15
+ }
16
+ export interface MdbaseFileDeleteReceipt {
17
+ protocolVersion: 1;
18
+ type: "file_deleted";
19
+ mutationId: string;
20
+ fileId: string;
21
+ previousPath: string;
22
+ revision: string;
23
+ }
6
24
  export interface MdbaseFileStreamSource {
7
25
  /** Exact plaintext byte length. */
8
26
  size: number;
@@ -84,7 +102,7 @@ export declare class MdbaseFileClient {
84
102
  downloadBytes(file: CollectionFileDescriptor, options?: MdbaseFileDownloadOptions): Promise<Uint8Array>;
85
103
  move(file: CollectionFileDescriptor, path: string, options?: MdbaseFileMoveOptions): Promise<CollectionFileDescriptor>;
86
104
  private moveWithinBudget;
87
- delete(file: CollectionFileDescriptor, options?: MdbaseFileDeleteOptions): Promise<DeleteFileReceipt>;
105
+ delete(file: CollectionFileDescriptor, options?: MdbaseFileDeleteOptions): Promise<MdbaseFileDeleteReceipt>;
88
106
  private deleteWithinBudget;
89
107
  private uploadPart;
90
108
  private tryReplayUploadCommit;
package/dist/files.js CHANGED
@@ -2,7 +2,7 @@ import { FILE_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
2
  import { MdbaseConnectError, connectError } from "./errors.js";
3
3
  import { IncrementalSha256 } from "./file-sha256.js";
4
4
  import { BinaryPartReader } from "./file-stream-source.js";
5
- import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency, validPageSize } from "./file-transfer-internals.js";
5
+ import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency, validPageSize } from "./file-transfer-internals.js";
6
6
  import { createRequestBudget, resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
7
7
  /** Ergonomic facade over resumable, authority-specific file delivery. */
8
8
  export class MdbaseFileClient {
@@ -38,7 +38,7 @@ export class MdbaseFileClient {
38
38
  }
39
39
  for (const file of page.files) {
40
40
  throwIfAborted(signal);
41
- yield file;
41
+ yield clientFileDescriptor(file);
42
42
  }
43
43
  after = page.next;
44
44
  } while (after);
@@ -130,16 +130,21 @@ export class MdbaseFileClient {
130
130
  const partCount = session.strategy.kind === "object_put"
131
131
  ? 1
132
132
  : Math.ceil(size / partSize);
133
+ const status = await this.request("GET", `transfers/${encodeURIComponent(transferId)}`, undefined, options.signal);
134
+ requireTransferStatus(status, session);
135
+ if (status.state !== "open" && status.state !== "committed") {
136
+ throw connectError("invalid_operation_response", `The authority cannot resume a ${status.state} file transfer.`);
137
+ }
133
138
  prepareSource?.(partSize);
134
- if (!framed && session.received.length === partCount) {
135
- const replay = await this.tryReplayUploadCommit(transferId, session.strategy.kind === "object_multipart" ? session.uploaded_parts : undefined, options.signal);
139
+ if (!framed && status.received.length === partCount) {
140
+ const replay = await this.tryReplayUploadCommit(transferId, session.strategy.kind === "object_multipart" ? status.uploaded_parts : undefined, options.signal);
136
141
  if (replay) {
137
142
  committed = true;
138
- return replay.file;
143
+ return clientFileDescriptor(replay.file);
139
144
  }
140
145
  }
141
- const received = new Set(session.received);
142
- const uploadedParts = new Map((session.uploaded_parts ?? []).map((part) => [part.part_number - 1, part]));
146
+ const received = new Set(status.received);
147
+ const uploadedParts = new Map(status.uploaded_parts.map((part) => [part.part_number - 1, part]));
143
148
  let transferredBytes = [...received].reduce((total, index) => total + chunkLength(size, partSize, index), 0);
144
149
  const parts = await mapConcurrent(partCount, concurrency, async (partIndex) => {
145
150
  const offset = partIndex * partSize;
@@ -174,7 +179,7 @@ export class MdbaseFileClient {
174
179
  throw connectError("invalid_operation_response", "The authority returned an invalid file receipt.");
175
180
  }
176
181
  committed = true;
177
- return receipt.file;
182
+ return clientFileDescriptor(receipt.file);
178
183
  }
179
184
  catch (error) {
180
185
  throw normalizeFileError(error);
@@ -202,7 +207,7 @@ export class MdbaseFileClient {
202
207
  const copy = new Uint8Array(chunk.byteLength);
203
208
  copy.set(chunk);
204
209
  return copy.buffer;
205
- }), { type: file.media_type ?? "" });
210
+ }), { type: file.mediaType ?? "" });
206
211
  }
207
212
  /** Download and verify a file with network backpressure and bounded buffering. */
208
213
  async downloadStream(file, options = {}) {
@@ -218,7 +223,7 @@ export class MdbaseFileClient {
218
223
  protocol_version: FILE_PROTOCOL_VERSION,
219
224
  type: "open_file_download",
220
225
  transfer_id: transferId,
221
- file_id: file.file_id,
226
+ file_id: file.fileId,
222
227
  revision: file.revision
223
228
  }, startupSignal);
224
229
  requireTransferSession(session, transferId, "download");
@@ -257,7 +262,7 @@ export class MdbaseFileClient {
257
262
  await this.abort(transferId);
258
263
  };
259
264
  const verify = () => {
260
- if (receivedBytes !== file.size || `sha256:${hash.digestHex()}` !== file.content_digest) {
265
+ if (receivedBytes !== file.size || `sha256:${hash.digestHex()}` !== file.contentDigest) {
261
266
  throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
262
267
  }
263
268
  };
@@ -357,11 +362,11 @@ export class MdbaseFileClient {
357
362
  const mutationId = options.mutationId ?? crypto.randomUUID();
358
363
  let receipt;
359
364
  try {
360
- receipt = await this.request("POST", `${encodeURIComponent(file.file_id)}/move`, {
365
+ receipt = await this.request("POST", `${encodeURIComponent(file.fileId)}/move`, {
361
366
  protocol_version: FILE_PROTOCOL_VERSION,
362
367
  type: "move_file",
363
368
  mutation_id: mutationId,
364
- file_id: file.file_id,
369
+ file_id: file.fileId,
365
370
  if_revision: options.ifRevision ?? file.revision,
366
371
  from_path: file.path,
367
372
  path,
@@ -376,11 +381,11 @@ export class MdbaseFileClient {
376
381
  if (receipt.protocol_version !== 1
377
382
  || receipt.type !== "file_moved"
378
383
  || receipt.mutation_id !== mutationId
379
- || receipt.file.file_id !== file.file_id
384
+ || receipt.file.file_id !== file.fileId
380
385
  || receipt.file.path !== path) {
381
386
  throw connectError("invalid_operation_response", "The authority returned an invalid file move receipt.");
382
387
  }
383
- return receipt.file;
388
+ return clientFileDescriptor(receipt.file);
384
389
  }
385
390
  async delete(file, options = {}) {
386
391
  return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.deleteWithinBudget(file, { ...options, signal: budget.signal, timeoutMs: null }));
@@ -390,11 +395,11 @@ export class MdbaseFileClient {
390
395
  const mutationId = options.mutationId ?? crypto.randomUUID();
391
396
  let receipt;
392
397
  try {
393
- receipt = await this.request("POST", `${encodeURIComponent(file.file_id)}/delete`, {
398
+ receipt = await this.request("POST", `${encodeURIComponent(file.fileId)}/delete`, {
394
399
  protocol_version: FILE_PROTOCOL_VERSION,
395
400
  type: "delete_file",
396
401
  mutation_id: mutationId,
397
- file_id: file.file_id,
402
+ file_id: file.fileId,
398
403
  if_revision: options.ifRevision ?? file.revision,
399
404
  path: file.path
400
405
  }, options.signal);
@@ -405,11 +410,18 @@ export class MdbaseFileClient {
405
410
  if (receipt.protocol_version !== 1
406
411
  || receipt.type !== "file_deleted"
407
412
  || receipt.mutation_id !== mutationId
408
- || receipt.file_id !== file.file_id
413
+ || receipt.file_id !== file.fileId
409
414
  || receipt.previous_path !== file.path) {
410
415
  throw connectError("invalid_operation_response", "The authority returned an invalid file delete receipt.");
411
416
  }
412
- return receipt;
417
+ return {
418
+ protocolVersion: receipt.protocol_version,
419
+ type: receipt.type,
420
+ mutationId: receipt.mutation_id,
421
+ fileId: receipt.file_id,
422
+ previousPath: receipt.previous_path,
423
+ revision: receipt.revision
424
+ };
413
425
  }
414
426
  async uploadPart(transferId, partIndex, offset, body, contentLength, requireEtag, signal) {
415
427
  for (let attempt = 1; attempt <= MAX_OBJECT_ATTEMPTS; attempt += 1) {
@@ -488,3 +500,15 @@ export class MdbaseFileClient {
488
500
  await this.request("DELETE", `transfers/${encodeURIComponent(transferId)}`).catch(() => undefined);
489
501
  }
490
502
  }
503
+ function clientFileDescriptor(file) {
504
+ return {
505
+ fileId: file.file_id,
506
+ path: file.path,
507
+ revision: file.revision,
508
+ contentDigest: file.content_digest,
509
+ size: file.size,
510
+ ...(file.media_type ? { mediaType: file.media_type } : {}),
511
+ mediaClass: file.media_class,
512
+ modifiedAt: file.modified_at
513
+ };
514
+ }