@finueva/drive 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @finueva/drive
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 77546b2: Use portable S3 Content-MD5 transport with mandatory independent server-side whole-file and part SHA-256 verification for all uploads. Descriptor requests require `contentMd5`; there is no legacy transport or profile selector. `completeUpload` and `uploadFile` may return HTTP 202 with a `verifying` result; callers can inspect read-only progress and retry completion with the original idempotency key, or explicitly abort the retained upload. No automatic unbounded polling is performed.
8
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5f0a6b1: Add a separate server-entry-point workspace provisioning client with explicit environment and entitlement validation, bounded service credential transport, and strict replay response checks.
14
+
3
15
  ## 0.5.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -6,16 +6,18 @@ The package is ESM-only. Node.js 22 is the exact minimum; package CI qualifies N
6
6
 
7
7
  ## Exports
8
8
 
9
- | Import | Runtime |
10
- | ----------------------- | --------------------------------- |
11
- | `@finueva/drive` | Browser client, errors, types |
12
- | `@finueva/drive/client` | Browser client |
13
- | `@finueva/drive/server` | Request-scoped Worker/Node client |
14
- | `@finueva/drive/core` | Error values |
15
- | `@finueva/drive/types` | Public operation types |
9
+ | Import | Runtime |
10
+ | ----------------------- | ------------------------------------ |
11
+ | `@finueva/drive` | Browser client, errors, types |
12
+ | `@finueva/drive/client` | Browser client |
13
+ | `@finueva/drive/server` | Worker/Node user and service clients |
14
+ | `@finueva/drive/core` | Error values |
15
+ | `@finueva/drive/types` | Public operation types |
16
16
 
17
17
  ## Browser
18
18
 
19
+ Approved ecosystem servers can separately use `createDriveProvisioningClient` from `/server` to call `provisionWorkspace(command)`. Configure an exact environment and `getProvisioningCredential` provider using Auth service credentials, not User credentials. Commands require a stable provisioning ID, exact workspace reference, and explicit versioned storage entitlement. The client does not infer quotas, follow redirects, or retry. See [Workspace Provisioning](../../apps/developer/docs/sdk/provisioning.md) for ownership and recovery boundaries.
20
+
19
21
  ```ts
20
22
  import { createDriveClient } from "@finueva/drive";
21
23
 
@@ -42,6 +44,7 @@ const matches = await drive.searchWorkspaceItems({
42
44
  limit: 50,
43
45
  });
44
46
 
47
+ const completionIdempotencyKey = crypto.randomUUID();
45
48
  const uploaded = await drive.uploadFile({
46
49
  workspaceId: workspace.workspaceId,
47
50
  parentItemId: workspace.rootItemId,
@@ -49,7 +52,7 @@ const uploaded = await drive.uploadFile({
49
52
  displayFilename: file.name,
50
53
  mediaType: file.type || "application/octet-stream",
51
54
  idempotencyKey: crypto.randomUUID(),
52
- completionIdempotencyKey: crypto.randomUUID(),
55
+ completionIdempotencyKey,
53
56
  onProgress: ({ phase, confirmedByteLength, totalByteLength }) => {
54
57
  renderProgress(phase, confirmedByteLength, totalByteLength);
55
58
  },
@@ -124,15 +127,15 @@ Successful calls return `{ data, status, requestId, validators }`. A successful
124
127
 
125
128
  `createDownload` returns a short-lived archive descriptor for the current or an explicit immutable version, one visible `restore-pending` result for cold content, or a no-body `304` operation result whose validators retain the source ETag. Full, closed, open, and suffix ranges use the selected archive object's strong ETag; malformed or multiple ranges are served as full representations, while valid non-overlapping ranges fail with `range_not_satisfiable`. `downloadItem` follows a descriptor with no Drive credential, ambient browser credentials, or referrer and returns the provider body as a backpressured `Response`; it preserves the same `304` and cold-restore results without a provider request. It validates exact status, ETag, identity encoding, length, and `Content-Range`, cancels malformed upstream bodies, and keeps caller cancellation/deadline authority active until the stream closes or is cancelled. It never returns Blob, ArrayBuffer, text, or base64 whole-file convenience values.
126
129
 
127
- The six low-level upload methods map directly to the Native API. `createUpload` accepts `callerDeclaredContentDigest`, an ordinary whole-file `sha256:{64 lowercase hex}` value calculated or supplied by the caller. It is untrusted metadata, not provider verification evidence. `getUploadStatus` returns one bounded confirmed-part page and the current lifecycle/revision. Descriptor issuance accepts one base64 SHA-256 part checksum, `recordUploadPart` records exact provider ETag/checksum evidence and revision, `completeUpload` returns only a server-verified `ready` result, and `abortUpload` returns bodyless `204` success.
130
+ The six low-level upload methods map directly to the Native API. `createUpload` accepts a caller-declared whole-file `sha256:{64 lowercase hex}` digest. Portable S3 is the only supported upload transport, with no profile selector. Descriptor requests require `contentMd5` (canonical base64 MD5), and return a required `content-md5` header. Part SHA-256 declarations and opaque provider ETags remain separate callback fields; MD5 is transport error detection, not content identity.
128
131
 
129
- Provider part checksums and the provider's multipart composite checksum are verified server-side before publication. They are deliberately distinct from `callerDeclaredContentDigest`: multipart composite SHA-256 is not the ordinary SHA-256 of the complete file. Every low-level control obtains one credential, sends one request, validates exact IDs, checksums, ETags, and safe-integer bounds at runtime, and never retries automatically. `upload_recovery_required` is preserved as a canonical `DriveError` code.
132
+ Publication independently verifies archive bytes against both whole-file and part SHA-256 declarations. `getUploadStatus` exposes bounded confirmed-part pages and read-only verification progress once verification is admitted. `completeUpload` and `uploadFile` return `DriveOperationResult<UploadCompletion, 200 | 202>`: inspect `data.state` for `ready` versus `verifying`. A `202` is not publication. Retain its `uploadId` and the original completion idempotency key, inspect status, then call `completeUpload` again with that key when appropriate. The SDK does not start an unbounded polling loop. To cancel after a pending result, call `abortUpload({ workspaceId, uploadId })`, which returns bodyless `204`.
130
133
 
131
134
  `uploadFile` is the browser-safe direct-byte path for a Web `Blob` or `File`. It accepts zero-byte input, hashes the empty input correctly, accepts the server-mediated zero-byte mode, skips part transfer, and completes normally. Nonempty input is incrementally hashed in slices no larger than 32 MiB and is never materialized as a complete `ArrayBuffer`, Blob copy, base64 string, or SDK request body. The fixed policy permits at most 10,000 parts and 312.5 GiB.
132
135
 
133
- Each nonempty slice is sent directly to the exact descriptor URL. The request includes only descriptor-required headers, uses `credentials: "omit"`, `cache: "no-store"`, `redirect: "error"`, and `referrerPolicy: "no-referrer"`, and shares the configured cancellation/deadline bound. The archive CORS policy must expose both `ETag` and `x-amz-checksum-sha256`; missing, malformed, or mismatched values fail closed. Drive authorization, cookies, default headers, and file metadata never enter the archive request.
136
+ Each nonempty slice is sent directly to the exact descriptor URL. The SDK computes MD5 with the maintained Noble public API on the same bounded slice as SHA-256. Requests include only descriptor-required headers and use `credentials: "omit"`, `cache: "no-store"`, `redirect: "error"`, and `referrerPolicy: "no-referrer"`. Archive CORS must allow `content-md5` and expose `ETag`; no provider SHA response header is required, and ETags are never interpreted as hashes. If a Content-MD5 response header is exposed, it must match the request. Drive credentials, cookies and file metadata never enter the archive request.
134
137
 
135
- Repeating `uploadFile` with the same creation and completion idempotency keys explicitly resumes the retained session. After create/replay, the SDK reads status in pages of 100, bounded to 100 pages and 10,000 parts; validates session identity, mode, policy, lifecycle, revision, and pagination; re-hashes the supplied source; compares every confirmed part checksum; skips exact confirmed parts; and continues from the current revision. A matching `ready` status returns its exact ready projection. `completing` and `provider-completed` replay completion. Recovery-required and aborted states fail without pretending to resume.
138
+ Repeating `uploadFile` with the same creation and completion keys explicitly resumes the retained session when replay remains available. Status reads are bounded to 100 pages of 100 parts. The SDK checks session identity, policy, lifecycle, revisions and checksums, skips confirmed parts and never re-uploads a part merely because its callback response was lost. `completing`, `provider-completed` and `verifying` resume completion; failed/aborted/recovery-required states do not pretend to resume. After receiving pending, using its upload ID with low-level status/complete avoids re-hashing the source. Both normal and empty uploads may return pending, and progress observers can receive the `verifying` phase.
136
139
 
137
140
  There is no automatic network or provider retry. If a part-record callback response may have been lost, the SDK performs one bounded status reconciliation before either accepting the confirmed part or recording the same provider evidence once more; it never blindly re-uploads the bytes. If a completion response may have been lost, one status reconciliation returns success only when `ready` is proven. Other transient failures preserve the session for an explicit repeat until server expiry.
138
141
 
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as UpdateItemInput, A as ListItemVersionsInput, C as FolderPage, D as GetUploadStatusInput, E as GetItemVersionInput, F as ManagedItemVersionPage, I as ManagedItemVersionResource, J as RestoreItemInput, K as ReadyUpload, L as ManagedItemVersionRestoreResult, M as ManagedDownloadRestorePending, N as ManagedItemMetadata, O as ListFolderChildrenInput, P as ManagedItemVersion, Q as TrashItemInput, R as ManagedLifecycleItem, S as FolderItem, T as GetItemInput, W as ProtectionClassification, Y as RestoreItemVersionInput, Z as SearchWorkspaceItemsInput, at as UploadFileProgressCallback, c as CurrentWorkspace, ct as UploadSession, d as DriveClient, dt as WorkspaceItemSearchPage, f as DriveClientConfig, it as UploadFileProgress, j as ManagedDownloadDescriptor, l as DeleteItemInput, lt as UploadStatus, m as DriveCredentialProvider, n as CompleteUploadInput, nt as UploadFileInput, o as CreateUploadInput, ot as UploadPartDescriptor, q as RecordUploadPartInput, r as CreateDownloadInput, rt as UploadFilePhase, s as CreateUploadPartDescriptorInput, st as UploadPartResult, t as AbortUploadInput, tt as UploadConfirmedPart, u as DownloadItemResult, ut as WorkspaceItemSearchItem, w as GetCurrentWorkspaceInput } from "./contract.js";
1
+ import { $ as TrashItemInput, A as ListItemVersionsInput, C as FolderPage, D as GetUploadStatusInput, E as GetItemVersionInput, F as ManagedItemVersionPage, G as ProtectionClassification, I as ManagedItemVersionResource, J as RecordUploadPartInput, L as ManagedItemVersionRestoreResult, M as ManagedDownloadRestorePending, N as ManagedItemMetadata, O as ListFolderChildrenInput, P as ManagedItemVersion, Q as SearchWorkspaceItemsInput, R as ManagedLifecycleItem, S as FolderItem, T as GetItemInput, X as RestoreItemVersionInput, Y as RestoreItemInput, at as UploadFilePhase, c as CurrentWorkspace, ct as UploadPartDescriptor, d as DriveClient, dt as UploadStatus, et as UpdateItemInput, f as DriveClientConfig, ft as WorkspaceItemSearchItem, it as UploadFileInput, j as ManagedDownloadDescriptor, l as DeleteItemInput, lt as UploadPartResult, m as DriveCredentialProvider, n as CompleteUploadInput, nt as UploadCompletion, o as CreateUploadInput, ot as UploadFileProgress, pt as WorkspaceItemSearchPage, q as ReadyUpload, r as CreateDownloadInput, rt as UploadConfirmedPart, s as CreateUploadPartDescriptorInput, st as UploadFileProgressCallback, t as AbortUploadInput, u as DownloadItemResult, ut as UploadSession, w as GetCurrentWorkspaceInput, z as PendingUpload } from "./contract.js";
2
2
  //#region src/client.d.ts
3
3
  declare function createDriveClient(config: DriveClientConfig): DriveClient;
4
4
  //#endregion
5
- export { type AbortUploadInput, type CompleteUploadInput, type CreateDownloadInput, type CreateUploadInput, type CreateUploadPartDescriptorInput, type CurrentWorkspace, type DeleteItemInput, type DownloadItemResult, type DriveClient, type DriveClientConfig, type DriveCredentialProvider, type FolderItem, type FolderPage, type GetCurrentWorkspaceInput, type GetItemInput, type GetItemVersionInput, type GetUploadStatusInput, type ListFolderChildrenInput, type ListItemVersionsInput, type ManagedDownloadDescriptor, type ManagedDownloadRestorePending, type ManagedItemMetadata, type ManagedItemVersion, type ManagedItemVersionPage, type ManagedItemVersionResource, type ManagedItemVersionRestoreResult, type ManagedLifecycleItem, type ProtectionClassification, type ReadyUpload, type RecordUploadPartInput, type RestoreItemInput, type RestoreItemVersionInput, type SearchWorkspaceItemsInput, type TrashItemInput, type UpdateItemInput, type UploadConfirmedPart, type UploadFileInput, type UploadFilePhase, type UploadFileProgress, type UploadFileProgressCallback, type UploadPartDescriptor, type UploadPartResult, type UploadSession, type UploadStatus, type WorkspaceItemSearchItem, type WorkspaceItemSearchPage, createDriveClient };
5
+ export { type AbortUploadInput, type CompleteUploadInput, type CreateDownloadInput, type CreateUploadInput, type CreateUploadPartDescriptorInput, type CurrentWorkspace, type DeleteItemInput, type DownloadItemResult, type DriveClient, type DriveClientConfig, type DriveCredentialProvider, type FolderItem, type FolderPage, type GetCurrentWorkspaceInput, type GetItemInput, type GetItemVersionInput, type GetUploadStatusInput, type ListFolderChildrenInput, type ListItemVersionsInput, type ManagedDownloadDescriptor, type ManagedDownloadRestorePending, type ManagedItemMetadata, type ManagedItemVersion, type ManagedItemVersionPage, type ManagedItemVersionResource, type ManagedItemVersionRestoreResult, type ManagedLifecycleItem, type PendingUpload, type ProtectionClassification, type ReadyUpload, type RecordUploadPartInput, type RestoreItemInput, type RestoreItemVersionInput, type SearchWorkspaceItemsInput, type TrashItemInput, type UpdateItemInput, type UploadCompletion, type UploadConfirmedPart, type UploadFileInput, type UploadFilePhase, type UploadFileProgress, type UploadFileProgressCallback, type UploadPartDescriptor, type UploadPartResult, type UploadSession, type UploadStatus, type WorkspaceItemSearchItem, type WorkspaceItemSearchPage, createDriveClient };
package/dist/client.js CHANGED
@@ -584,12 +584,14 @@ function isUploadStatus(value, afterPartNumber, limit) {
584
584
  "nextAfterPartNumber",
585
585
  "expiresAtEpochMilliseconds",
586
586
  "ready"
587
- ]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID$1.test(value.versionId) || typeof value.uploadId !== "string" || !UPLOAD_ID$1.test(value.uploadId) || value.mode !== "presigned-multipart" && value.mode !== "server-mediated-zero-byte" || ![
587
+ ], ["verification"]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID$1.test(value.versionId) || typeof value.uploadId !== "string" || !UPLOAD_ID$1.test(value.uploadId) || value.mode !== "presigned-multipart" && value.mode !== "server-mediated-zero-byte" || ![
588
588
  "initiating",
589
589
  "recovery-required",
590
590
  "uploading",
591
591
  "completing",
592
592
  "provider-completed",
593
+ "verifying",
594
+ "failed",
593
595
  "ready",
594
596
  "aborting",
595
597
  "aborted"
@@ -599,18 +601,38 @@ function isUploadStatus(value, afterPartNumber, limit) {
599
601
  "maxParts"
600
602
  ]) || value.partPolicy.partByteLength !== UPLOAD_PART_BYTE_LENGTH$2 || !isSafeInteger$1(value.partPolicy.partCount) || value.partPolicy.partCount > 1e4 || value.partPolicy.maxParts !== 1e4 || (value.mode === "server-mediated-zero-byte" ? value.partPolicy.partCount !== 0 : value.partPolicy.partCount < 1) || !Array.isArray(value.confirmedParts) || value.confirmedParts.length > limit || !isSafeInteger$1(value.expiresAtEpochMilliseconds)) return false;
601
603
  let previousPartNumber = afterPartNumber;
604
+ if (value.verification !== void 0) {
605
+ const progress = value.verification;
606
+ if (!isExactRecord(progress, [
607
+ "state",
608
+ "verifiedByteLength",
609
+ "totalByteLength",
610
+ "revision"
611
+ ]) || ![
612
+ "pending",
613
+ "verified",
614
+ "failed"
615
+ ].includes(String(progress.state)) || !isSafeInteger$1(progress.verifiedByteLength) || !isSafeInteger$1(progress.totalByteLength) || progress.totalByteLength > MAX_UPLOAD_BYTE_LENGTH$1 || progress.verifiedByteLength > progress.totalByteLength || !isSafeInteger$1(progress.revision) || progress.state === "verified" && progress.verifiedByteLength !== progress.totalByteLength) return false;
616
+ Object.freeze(progress);
617
+ }
618
+ if (["verifying", "failed"].includes(String(value.lifecycle)) && value.verification === void 0) return false;
602
619
  for (const part of value.confirmedParts) {
603
620
  if (!isExactRecord(part, ["partNumber", "checksumSha256"]) || !isSafeInteger$1(part.partNumber) || part.partNumber <= previousPartNumber || part.partNumber > value.partPolicy.partCount || !isPartChecksum$1(part.checksumSha256)) return false;
604
621
  previousPartNumber = part.partNumber;
605
622
  }
606
- if (!(value.nextAfterPartNumber === null || isSafeInteger$1(value.nextAfterPartNumber) && value.confirmedParts.length === limit && value.nextAfterPartNumber === previousPartNumber) || value.mode === "server-mediated-zero-byte" && (value.confirmedParts.length !== 0 || value.nextAfterPartNumber !== null) || value.lifecycle === "ready" !== (value.ready !== null) || value.ready !== null && (!isReadyUpload(value.ready, { uploadId: value.uploadId }) || value.ready.itemId !== value.itemId || value.ready.versionId !== value.versionId)) return false;
623
+ if (!(value.nextAfterPartNumber === null || isSafeInteger$1(value.nextAfterPartNumber) && value.confirmedParts.length === limit && value.nextAfterPartNumber === previousPartNumber) || value.mode === "server-mediated-zero-byte" && (value.confirmedParts.length !== 0 || value.nextAfterPartNumber !== null) || value.lifecycle === "ready" !== (value.ready !== null) || value.ready !== null && (!isReadyUpload(value.ready, { uploadId: value.uploadId }) || value.ready.itemId !== value.itemId || value.ready.versionId !== value.versionId || !isExactRecord(value.verification, [
624
+ "state",
625
+ "verifiedByteLength",
626
+ "totalByteLength",
627
+ "revision"
628
+ ]) || value.verification.state !== "verified" || value.verification.totalByteLength !== value.ready.byteLength)) return false;
607
629
  Object.freeze(value.partPolicy);
608
630
  for (const part of value.confirmedParts) Object.freeze(part);
609
631
  Object.freeze(value.confirmedParts);
610
632
  Object.freeze(value);
611
633
  return true;
612
634
  }
613
- function isUploadPartDescriptor(value, expectedPartNumber, checksumSha256, allowInsecureHttp) {
635
+ function isUploadPartDescriptor(value, expectedPartNumber, allowInsecureHttp, contentMd5) {
614
636
  if (!isExactRecord(value, [
615
637
  "url",
616
638
  "method",
@@ -618,7 +640,7 @@ function isUploadPartDescriptor(value, expectedPartNumber, checksumSha256, allow
618
640
  "partNumber",
619
641
  "expectedByteLength",
620
642
  "expiresAtEpochMilliseconds"
621
- ]) || !isSignedTransferUrl(value.url, allowInsecureHttp) || value.method !== "PUT" || !isExactRecord(value.headers, ["x-amz-checksum-sha256"]) || value.headers["x-amz-checksum-sha256"] !== checksumSha256 || value.partNumber !== expectedPartNumber || !isSafeInteger$1(value.expectedByteLength) || value.expectedByteLength < 1 || value.expectedByteLength > UPLOAD_PART_BYTE_LENGTH$2 || !isSafeInteger$1(value.expiresAtEpochMilliseconds) || value.expiresAtEpochMilliseconds <= Date.now()) return false;
643
+ ]) || !isSignedTransferUrl(value.url, allowInsecureHttp) || value.method !== "PUT" || !contentMd5 || !isExactRecord(value.headers, ["content-md5"]) || value.headers["content-md5"] !== contentMd5 || value.partNumber !== expectedPartNumber || !isSafeInteger$1(value.expectedByteLength) || value.expectedByteLength < 1 || value.expectedByteLength > UPLOAD_PART_BYTE_LENGTH$2 || !isSafeInteger$1(value.expiresAtEpochMilliseconds) || value.expiresAtEpochMilliseconds <= Date.now()) return false;
622
644
  Object.freeze(value.headers);
623
645
  Object.freeze(value);
624
646
  return true;
@@ -642,6 +664,19 @@ function isReadyUpload(value, expected) {
642
664
  Object.freeze(value);
643
665
  return true;
644
666
  }
667
+ function isPendingUpload(value, uploadId) {
668
+ if (!isExactRecord(value, [
669
+ "itemId",
670
+ "versionId",
671
+ "uploadId",
672
+ "state",
673
+ "verifiedByteLength",
674
+ "totalByteLength",
675
+ "verificationRevision"
676
+ ]) || typeof value.itemId !== "string" || !ITEM_ID$1.test(value.itemId) || typeof value.versionId !== "string" || !VERSION_ID$1.test(value.versionId) || value.uploadId !== uploadId || value.state !== "verifying" || !isSafeInteger$1(value.verifiedByteLength) || !isSafeInteger$1(value.totalByteLength) || value.totalByteLength > MAX_UPLOAD_BYTE_LENGTH$1 || value.verifiedByteLength > value.totalByteLength || !isSafeInteger$1(value.verificationRevision)) return false;
677
+ Object.freeze(value);
678
+ return true;
679
+ }
645
680
  function isPartChecksum$1(value) {
646
681
  return typeof value === "string" && /^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/.test(value);
647
682
  }
@@ -781,9 +816,9 @@ function isSafeCapabilityUrl(value) {
781
816
  return false;
782
817
  }
783
818
  }
784
- function isExactRecord(value, keys) {
819
+ function isExactRecord(value, keys, optional = []) {
785
820
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
786
- return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
821
+ return Object.keys(value).every((key) => keys.includes(key) || optional.includes(key)) && keys.every((key) => Object.hasOwn(value, key));
787
822
  }
788
823
  //#endregion
789
824
  //#region src/internal/client/request.ts
@@ -1112,14 +1147,17 @@ function validateCreateUploadPartDescriptorInput(value) {
1112
1147
  "workspaceId",
1113
1148
  "uploadId",
1114
1149
  "partNumber",
1115
- "checksumSha256"
1150
+ "checksumSha256",
1151
+ "contentMd5"
1116
1152
  ], ["signal"]);
1117
1153
  const upload = validateUploadPath(input.workspaceId, input.uploadId, input.signal);
1118
1154
  if (!isPartNumber(input.partNumber) || !isPartChecksum(input.checksumSha256)) throw new DriveError("invalid_input");
1155
+ if (typeof input.contentMd5 !== "string" || !/^[A-Za-z0-9+/]{21}[AQgw]==$/.test(input.contentMd5)) throw new DriveError("invalid_input");
1119
1156
  return {
1120
1157
  ...upload,
1121
1158
  partNumber: input.partNumber,
1122
- checksumSha256: input.checksumSha256
1159
+ checksumSha256: input.checksumSha256,
1160
+ contentMd5: input.contentMd5
1123
1161
  };
1124
1162
  }
1125
1163
  function validateRecordUploadPartInput(value) {
@@ -2437,7 +2475,7 @@ async function getUploadStatus(runtime, input) {
2437
2475
  }
2438
2476
  } }),
2439
2477
  ...requestOptions
2440
- }), [200], (value) => isUploadStatus(value, afterPartNumber, limit), false, STATUS_ERROR_CODES);
2478
+ }), [200], (value) => isUploadStatus(value, afterPartNumber, limit) && value.uploadId === validated.uploadId, false, STATUS_ERROR_CODES);
2441
2479
  }
2442
2480
  async function createUploadPartDescriptor(runtime, input) {
2443
2481
  const validated = validateCreateUploadPartDescriptorInput(input);
@@ -2449,11 +2487,12 @@ async function createUploadPartDescriptor(runtime, input) {
2449
2487
  } },
2450
2488
  body: {
2451
2489
  partNumber: validated.partNumber,
2452
- checksumSha256: validated.checksumSha256
2490
+ checksumSha256: validated.checksumSha256,
2491
+ contentMd5: validated.contentMd5
2453
2492
  }
2454
2493
  }),
2455
2494
  ...requestOptions
2456
- }), [200], (value) => isUploadPartDescriptor(value, validated.partNumber, validated.checksumSha256, new URL(runtime.baseUrl).protocol === "http:"), false, TRANSFER_ERROR_CODES);
2495
+ }), [200], (value) => isUploadPartDescriptor(value, validated.partNumber, new URL(runtime.baseUrl).protocol === "http:", validated.contentMd5), false, TRANSFER_ERROR_CODES);
2457
2496
  }
2458
2497
  async function recordUploadPart(runtime, input) {
2459
2498
  const validated = validateRecordUploadPartInput(input);
@@ -2485,7 +2524,7 @@ async function completeUpload(runtime, input) {
2485
2524
  header: { "Idempotency-Key": validated.idempotencyKey }
2486
2525
  } }),
2487
2526
  ...requestOptions
2488
- }), [200], (value) => isReadyUpload(value, { uploadId: validated.uploadId }), false, COMPLETE_UPLOAD_ERROR_CODES);
2527
+ }), [200, 202], (value, status) => status === 202 ? isPendingUpload(value, validated.uploadId) : isReadyUpload(value, { uploadId: validated.uploadId }), false, COMPLETE_UPLOAD_ERROR_CODES);
2489
2528
  }
2490
2529
  async function abortUpload(runtime, input) {
2491
2530
  const validated = validateAbortUploadInput(input);
@@ -2549,6 +2588,9 @@ function createView(arr) {
2549
2588
  function rotr(word, shift) {
2550
2589
  return word << 32 - shift | word >>> shift;
2551
2590
  }
2591
+ function rotl(word, shift) {
2592
+ return word << shift | word >>> 32 - shift >>> 0;
2593
+ }
2552
2594
  const hasHexBuiltin = (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")();
2553
2595
  const hexes = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
2554
2596
  function bytesToHex(bytes) {
@@ -2860,6 +2902,115 @@ var _SHA256 = class extends SHA2_32B {
2860
2902
  };
2861
2903
  const sha256 = createHasher(() => new _SHA256(), oidNist(1));
2862
2904
  //#endregion
2905
+ //#region ../../node_modules/.pnpm/@noble+hashes@2.3.0/node_modules/@noble/hashes/legacy.js
2906
+ const SHA1_IV = Uint32Array.from([
2907
+ 1732584193,
2908
+ 4023233417,
2909
+ 2562383102,
2910
+ 271733878,
2911
+ 3285377520
2912
+ ]);
2913
+ const p32 = Math.pow(2, 32);
2914
+ const K = Array.from({ length: 64 }, (_, i) => Math.floor(p32 * Math.abs(Math.sin(i + 1))));
2915
+ const MD5_IV = SHA1_IV.slice(0, 4);
2916
+ const MD5_W = new Uint32Array(16);
2917
+ const MD5_SHIFTS = (() => {
2918
+ const S = [
2919
+ [
2920
+ 7,
2921
+ 12,
2922
+ 17,
2923
+ 22
2924
+ ],
2925
+ [
2926
+ 5,
2927
+ 9,
2928
+ 14,
2929
+ 20
2930
+ ],
2931
+ [
2932
+ 4,
2933
+ 11,
2934
+ 16,
2935
+ 23
2936
+ ],
2937
+ [
2938
+ 6,
2939
+ 10,
2940
+ 15,
2941
+ 21
2942
+ ]
2943
+ ];
2944
+ return Uint8Array.from({ length: 64 }, (_, i) => S[Math.floor(i / 16)][i % 4]);
2945
+ })();
2946
+ var _MD5 = class extends HashMD {
2947
+ A = MD5_IV[0] | 0;
2948
+ B = MD5_IV[1] | 0;
2949
+ C = MD5_IV[2] | 0;
2950
+ D = MD5_IV[3] | 0;
2951
+ constructor() {
2952
+ super(64, 16, 8, true);
2953
+ }
2954
+ get() {
2955
+ const { A, B, C, D } = this;
2956
+ return [
2957
+ A,
2958
+ B,
2959
+ C,
2960
+ D
2961
+ ];
2962
+ }
2963
+ set(A, B, C, D) {
2964
+ this.A = A | 0;
2965
+ this.B = B | 0;
2966
+ this.C = C | 0;
2967
+ this.D = D | 0;
2968
+ }
2969
+ _cloneInto(to) {
2970
+ (to ||= new this.constructor()).set(...this.get());
2971
+ return this._cloneIntoMeta(to);
2972
+ }
2973
+ process(view, offset) {
2974
+ for (let i = 0; i < 16; i++, offset += 4) MD5_W[i] = view.getUint32(offset, true);
2975
+ let { A, B, C, D } = this;
2976
+ for (let i = 0; i < 64; i++) {
2977
+ let F, g;
2978
+ if (i < 16) {
2979
+ F = Chi(B, C, D);
2980
+ g = i;
2981
+ } else if (i < 32) {
2982
+ F = Chi(D, B, C);
2983
+ g = (5 * i + 1) % 16;
2984
+ } else if (i < 48) {
2985
+ F = B ^ C ^ D;
2986
+ g = (3 * i + 5) % 16;
2987
+ } else {
2988
+ F = C ^ (B | ~D);
2989
+ g = 7 * i % 16;
2990
+ }
2991
+ F = F + A + K[i] + MD5_W[g];
2992
+ A = D;
2993
+ D = C;
2994
+ C = B;
2995
+ B = B + rotl(F, MD5_SHIFTS[i]);
2996
+ }
2997
+ A = A + this.A | 0;
2998
+ B = B + this.B | 0;
2999
+ C = C + this.C | 0;
3000
+ D = D + this.D | 0;
3001
+ this.set(A, B, C, D);
3002
+ }
3003
+ roundClean() {
3004
+ clean(MD5_W);
3005
+ }
3006
+ destroy() {
3007
+ this.destroyed = true;
3008
+ this.set(0, 0, 0, 0);
3009
+ clean(this.buffer);
3010
+ }
3011
+ };
3012
+ const md5 = createHasher(() => new _MD5());
3013
+ //#endregion
2863
3014
  //#region src/internal/client/upload.ts
2864
3015
  const UPLOAD_PART_BYTE_LENGTH = 32 * 1024 * 1024;
2865
3016
  const STATUS_PAGE_LIMIT = 100;
@@ -2907,6 +3058,7 @@ async function uploadFileInternal(runtime, input, controls) {
2907
3058
  uploadId: session.uploadId,
2908
3059
  partNumber,
2909
3060
  checksumSha256,
3061
+ contentMd5: digests.partMd5s[index],
2910
3062
  ...validated.signal === void 0 ? {} : { signal: validated.signal }
2911
3063
  });
2912
3064
  const descriptor = descriptorResult.data;
@@ -2914,14 +3066,16 @@ async function uploadFileInternal(runtime, input, controls) {
2914
3066
  status: descriptorResult.status,
2915
3067
  requestId: descriptorResult.requestId
2916
3068
  });
2917
- const etag = await putPart(runtime, descriptor, slicePart(validated.source, start, end, expectedByteLength), checksumSha256, validated.signal);
3069
+ const etag = await putPart(runtime, descriptor, slicePart(validated.source, start, end, expectedByteLength), validated.signal);
2918
3070
  revision = await recordPartWithReconciliation(controls, validated, session, partNumber, revision, expectedByteLength, etag, checksumSha256);
2919
3071
  confirmedByteLength += expectedByteLength;
2920
3072
  confirmedPartCount += 1;
2921
3073
  emitProgress(validated.onProgress, "uploading", confirmedByteLength, validated.byteLength, confirmedPartCount, totalPartCount);
2922
3074
  }
2923
3075
  emitProgress(validated.onProgress, "completing", confirmedByteLength, validated.byteLength, confirmedPartCount, totalPartCount);
2924
- return await completeWithReconciliation(controls, validated, session, digests);
3076
+ const completed = await completeWithReconciliation(controls, validated, session, digests);
3077
+ if (completed.data.state === "verifying") emitProgress(validated.onProgress, "verifying", confirmedByteLength, validated.byteLength, confirmedPartCount, totalPartCount);
3078
+ return completed;
2925
3079
  } catch (error) {
2926
3080
  if (validated.signal?.aborted && session) await bestEffortAbort(controls, validated.workspaceId, session.uploadId);
2927
3081
  if (error instanceof DriveError) throw error;
@@ -2977,6 +3131,7 @@ async function readResumeState(controls, input, session, digests) {
2977
3131
  if (session.mode === "presigned-multipart" && [
2978
3132
  "completing",
2979
3133
  "provider-completed",
3134
+ "verifying",
2980
3135
  "ready"
2981
3136
  ].includes(baseline.lifecycle) && confirmedParts.size !== session.partCount) throw new DriveError("invalid_response");
2982
3137
  failForTerminalStatus(baseline.lifecycle, baselineRequestId);
@@ -3033,7 +3188,8 @@ async function completeWithReconciliation(controls, input, session, digests) {
3033
3188
  idempotencyKey: input.completionIdempotencyKey,
3034
3189
  ...input.signal === void 0 ? {} : { signal: input.signal }
3035
3190
  });
3036
- assertReadyMatchesInput(completed.data, session, input, digests, completed.requestId);
3191
+ if (completed.data.state === "ready") assertReadyMatchesInput(completed.data, session, input, digests, completed.requestId);
3192
+ else if (completed.data.itemId !== session.itemId || completed.data.versionId !== session.versionId || completed.data.uploadId !== session.uploadId || completed.data.totalByteLength !== input.byteLength) throw new DriveError("invalid_response");
3037
3193
  return completed;
3038
3194
  } catch (error) {
3039
3195
  if (!isAmbiguousControlFailure(error) || input.signal?.aborted) throw error;
@@ -3054,6 +3210,7 @@ async function completeWithReconciliation(controls, input, session, digests) {
3054
3210
  async function hashSource(source, byteLength, partCount, signal) {
3055
3211
  const fullHash = sha256.create();
3056
3212
  const partChecksums = [];
3213
+ const partMd5s = [];
3057
3214
  for (let index = 0; index < partCount; index += 1) {
3058
3215
  throwIfCallerAborted(signal);
3059
3216
  const start = index * UPLOAD_PART_BYTE_LENGTH;
@@ -3070,13 +3227,15 @@ async function hashSource(source, byteLength, partCount, signal) {
3070
3227
  const bytes = new Uint8Array(buffer);
3071
3228
  fullHash.update(bytes);
3072
3229
  partChecksums.push(toBase64(sha256(bytes)));
3230
+ partMd5s.push(toBase64(md5(bytes)));
3073
3231
  }
3074
3232
  return Object.freeze({
3075
3233
  callerDeclaredContentDigest: `sha256:${bytesToHex(fullHash.digest())}`,
3076
- partChecksums: Object.freeze(partChecksums)
3234
+ partChecksums: Object.freeze(partChecksums),
3235
+ partMd5s: Object.freeze(partMd5s)
3077
3236
  });
3078
3237
  }
3079
- async function putPart(runtime, descriptor, part, checksumSha256, callerSignal) {
3238
+ async function putPart(runtime, descriptor, part, callerSignal) {
3080
3239
  const remainingMilliseconds = descriptor.expiresAtEpochMilliseconds - Date.now();
3081
3240
  if (remainingMilliseconds < 1) throw new DriveError("invalid_response");
3082
3241
  const operation = createOperationSignal(callerSignal, Math.min(runtime.timeoutMilliseconds, remainingMilliseconds));
@@ -3094,8 +3253,9 @@ async function putPart(runtime, descriptor, part, checksumSha256, callerSignal)
3094
3253
  }), operation.signal);
3095
3254
  if (!(response instanceof Response)) throw new DriveError("invalid_response");
3096
3255
  const etag = response.headers.get("etag");
3097
- const responseChecksum = response.headers.get("x-amz-checksum-sha256");
3098
- if (response.status < 200 || response.status > 299 || !isEtag(etag) || responseChecksum !== checksumSha256) throw new DriveError("invalid_response", { status: response.status });
3256
+ const md5Response = response.headers.get("content-md5");
3257
+ const checksumMatches = md5Response === null || md5Response === descriptor.headers["content-md5"];
3258
+ if (response.status < 200 || response.status > 299 || !isEtag(etag) || !checksumMatches) throw new DriveError("invalid_response", { status: response.status });
3099
3259
  return etag;
3100
3260
  } catch (error) {
3101
3261
  if (operation.didTimeout()) throw new DriveError("timeout");
@@ -3131,7 +3291,7 @@ function failForTerminalStatus(lifecycle, requestId) {
3131
3291
  status: 409,
3132
3292
  ...requestId === void 0 ? {} : { requestId }
3133
3293
  });
3134
- if (lifecycle === "aborting" || lifecycle === "aborted") throw new DriveError("precondition_failed", {
3294
+ if (lifecycle === "aborting" || lifecycle === "aborted" || lifecycle === "failed") throw new DriveError("precondition_failed", {
3135
3295
  status: 409,
3136
3296
  ...requestId === void 0 ? {} : { requestId }
3137
3297
  });