@tearleads/client-sdk 0.1.27 → 0.1.28
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/data/documents/blob/shared/chunkedBlobCrypto.js +20 -36
- package/data/documents/blob/shared/chunkedBlobCrypto.js.map +1 -1
- package/data/documents/blob/shared/encryptedUploadParts.js +42 -0
- package/data/documents/blob/shared/encryptedUploadParts.js.map +1 -0
- package/data/documents/blob/shared/types.d.ts +1 -1
- package/data/documents/blob/shared/types.js.map +1 -1
- package/package.json +1 -1
- package/workflows/blobs/multipartUpload.js +1 -1
- package/workflows/blobs/multipartUpload.js.map +1 -1
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { AES_256_KEY_BYTES, AES_GCM_TAG_BYTES, assertAesGcmIv, BLOB_CHUNK_SIZE_BYTES, BLOB_ENCRYPTED_BYTES_FORMAT, BLOB_ENCRYPTED_BYTES_VERSION,
|
|
1
|
+
import { AES_256_KEY_BYTES, AES_GCM_TAG_BYTES, assertAesGcmIv, BLOB_CHUNK_SIZE_BYTES, BLOB_ENCRYPTED_BYTES_FORMAT, BLOB_ENCRYPTED_BYTES_VERSION, bytesToHex, CONTENT_RECORD_ENCRYPTION_SUITE, computeBlobChunkCount, computeBlobEncryptedByteLength, computeContentRecordNonceDomainHash, createAesGcmIv, createIncrementalSha256, deriveBlobChunkIv, encodeBlobEnvelopeV2Header, normalizeBlobChunkSize, } from "../../../../internal/crypto/index.js";
|
|
2
2
|
import { bytesToBase64 } from "../../../../internal/encoding/index.js";
|
|
3
3
|
import { importContentKeyMaterial } from "../../shared/contentRecordKeys.js";
|
|
4
4
|
import { blobContentMetadataHash, contentRecordAdditionalDataBytes, deriveBlobBaseIv, deriveBlobContentRecordKey, } from "./blobRecordCrypto.js";
|
|
5
5
|
import { inspectBlobSource, readVerifiedBlobSourceChunk, } from "./blobSourceSnapshot.js";
|
|
6
|
+
import { createEncryptedUploadParts } from "./encryptedUploadParts.js";
|
|
6
7
|
export const DEFAULT_BLOB_CHUNK_SIZE_BYTES = BLOB_CHUNK_SIZE_BYTES;
|
|
7
|
-
function assertPartIndex(index, partCount) {
|
|
8
|
-
if (!Number.isInteger(index) || index < 0 || index >= partCount) {
|
|
9
|
-
throw new Error("Blob encryption part index is out of range");
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
8
|
async function createBlobEncryptionContext(input, snapshot) {
|
|
13
9
|
const chunkSize = normalizeBlobChunkSize(input.chunkSize ?? DEFAULT_BLOB_CHUNK_SIZE_BYTES);
|
|
14
10
|
const plaintextByteLength = input.source.byteLength;
|
|
@@ -108,45 +104,33 @@ async function runEncryptionHashPass(source, snapshot, context) {
|
|
|
108
104
|
}
|
|
109
105
|
return bytesToHex(objectHash.digest());
|
|
110
106
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
const
|
|
107
|
+
function createBlobEncryptionPlan(input) {
|
|
108
|
+
const { context, sha256, snapshot, source } = input;
|
|
109
|
+
const byteLength = computeBlobEncryptedByteLength({
|
|
114
110
|
chunkCount: context.partCount,
|
|
115
|
-
|
|
116
|
-
chunkSize: context.chunkSize,
|
|
111
|
+
headerByteLength: context.headerBytes.byteLength,
|
|
117
112
|
plaintextByteLength: context.plaintextByteLength,
|
|
118
113
|
});
|
|
119
|
-
return (plaintextLength +
|
|
120
|
-
AES_GCM_TAG_BYTES +
|
|
121
|
-
(index === 0 ? context.headerBytes.byteLength : 0));
|
|
122
|
-
}
|
|
123
|
-
function createBlobEncryptionPlan(input) {
|
|
124
|
-
const { context, sha256, snapshot, source } = input;
|
|
125
114
|
return {
|
|
126
|
-
byteLength
|
|
127
|
-
chunkCount: context.partCount,
|
|
128
|
-
headerByteLength: context.headerBytes.byteLength,
|
|
129
|
-
plaintextByteLength: context.plaintextByteLength,
|
|
130
|
-
}),
|
|
115
|
+
byteLength,
|
|
131
116
|
chunkSize: context.chunkSize,
|
|
132
117
|
metadataHash: context.metadataHash,
|
|
133
|
-
partCount: context.partCount,
|
|
134
118
|
plaintextByteLength: context.plaintextByteLength,
|
|
135
119
|
plaintextSha256: snapshot.sha256,
|
|
136
120
|
sha256,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
121
|
+
...createEncryptedUploadParts({
|
|
122
|
+
byteLength,
|
|
123
|
+
chunkSize: context.chunkSize + AES_GCM_TAG_BYTES,
|
|
124
|
+
header: context.headerBytes,
|
|
125
|
+
async encryptChunk(chunkIndex) {
|
|
126
|
+
const plaintext = await readVerifiedBlobSourceChunk({
|
|
127
|
+
chunkIndex,
|
|
128
|
+
snapshot,
|
|
129
|
+
source,
|
|
130
|
+
});
|
|
131
|
+
return encryptBlobChunk(context, chunkIndex, plaintext);
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
150
134
|
};
|
|
151
135
|
}
|
|
152
136
|
export async function prepareBlobEncryption(input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunkedBlobCrypto.js","sourceRoot":"","sources":["chunkedBlobCrypto.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AES_256_KEY_BYTES,\n AES_GCM_TAG_BYTES,\n assertAesGcmIv,\n BLOB_CHUNK_SIZE_BYTES,\n BLOB_ENCRYPTED_BYTES_FORMAT,\n BLOB_ENCRYPTED_BYTES_VERSION,\n blobChunkPlaintextByteLength,\n bytesToHex,\n CONTENT_RECORD_ENCRYPTION_SUITE,\n computeBlobChunkCount,\n computeBlobEncryptedByteLength,\n computeContentRecordNonceDomainHash,\n createAesGcmIv,\n createIncrementalSha256,\n deriveBlobChunkIv,\n encodeBlobEnvelopeV2Header,\n joinBlobPartBytes,\n normalizeBlobChunkSize,\n} from \"@tearleads/crypto\";\nimport { bytesToBase64 } from \"@tearleads/encoding\";\nimport type { BlobContentKeyBundleRequest } from \"@tearleads/validators/request\";\nimport type { BlobByteSource, BlobBytes } from \"../../../blobContracts\";\nimport { importContentKeyMaterial } from \"../../shared/contentRecordKeys\";\nimport {\n blobContentMetadataHash,\n contentRecordAdditionalDataBytes,\n deriveBlobBaseIv,\n deriveBlobContentRecordKey,\n} from \"./blobRecordCrypto\";\nimport {\n type BlobSourceSnapshot,\n inspectBlobSource,\n readVerifiedBlobSourceChunk,\n} from \"./blobSourceSnapshot\";\nimport type { BlobEncryptionPlan } from \"./types\";\n\nexport const DEFAULT_BLOB_CHUNK_SIZE_BYTES = BLOB_CHUNK_SIZE_BYTES;\n\ninterface PrepareBlobEncryptionInput {\n readonly blobId: string;\n readonly chunkSize?: number | undefined;\n readonly contentKey: Uint8Array;\n readonly contentKeyBundle: BlobContentKeyBundleRequest;\n readonly nonceSeed?: Uint8Array | undefined;\n readonly organizationId: string;\n readonly source: BlobByteSource;\n readonly sourceSnapshot?: BlobSourceSnapshot | undefined;\n}\n\nfunction assertPartIndex(index: number, partCount: number): void {\n if (!Number.isInteger(index) || index < 0 || index >= partCount) {\n throw new Error(\"Blob encryption part index is out of range\");\n }\n}\n\ninterface BlobEncryptionContext {\n readonly baseIv: BlobBytes;\n readonly blobId: string;\n readonly chunkSize: number;\n readonly contentKeyEpoch: number;\n readonly contentRecordId: string;\n readonly headerBytes: BlobBytes;\n readonly metadataHash: string;\n readonly nonceDomainHash: string;\n readonly organizationId: string;\n readonly partCount: number;\n readonly plaintextByteLength: number;\n readonly recordKey: CryptoKey;\n}\n\nasync function createBlobEncryptionContext(\n input: PrepareBlobEncryptionInput,\n snapshot: BlobSourceSnapshot,\n): Promise<BlobEncryptionContext> {\n const chunkSize = normalizeBlobChunkSize(\n input.chunkSize ?? DEFAULT_BLOB_CHUNK_SIZE_BYTES,\n );\n const plaintextByteLength = input.source.byteLength;\n const partCount = computeBlobChunkCount(plaintextByteLength, chunkSize);\n const contentRecordId = input.blobId;\n const { contentKeyEpoch } = input.contentKeyBundle;\n const nonceSeed = new Uint8Array(input.nonceSeed ?? createAesGcmIv());\n const baseIv = await deriveBlobBaseIv({\n chunkSize,\n nonceSeed,\n plaintextSha256: snapshot.sha256,\n });\n\n const [nonceDomainHash, metadataHash] = await Promise.all([\n computeContentRecordNonceDomainHash({\n version: 1,\n organizationId: input.organizationId,\n objectKind: \"blob\",\n objectId: input.blobId,\n contentKeyEpoch,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n contentRecordId,\n }),\n blobContentMetadataHash({\n blobId: input.blobId,\n byteLength: plaintextByteLength,\n chunkCount: partCount,\n chunkSize,\n contentKeyEpoch,\n }),\n ]);\n const contentKeyMaterial = await importContentKeyMaterial(input.contentKey);\n const recordKey = await deriveBlobContentRecordKey({\n blobId: input.blobId,\n contentKeyEpoch,\n contentKeyMaterial,\n contentRecordId,\n organizationId: input.organizationId,\n usage: \"encrypt\",\n });\n return {\n baseIv,\n blobId: input.blobId,\n chunkSize,\n contentKeyEpoch,\n contentRecordId,\n headerBytes: encodeBlobEnvelopeV2Header({\n blobId: input.blobId,\n byteLength: plaintextByteLength,\n chunkCount: partCount,\n chunkSize,\n contentKeyEpoch,\n contentRecordId,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n format: BLOB_ENCRYPTED_BYTES_FORMAT,\n iv: bytesToBase64(baseIv),\n metadataHash,\n nonceDomainHash,\n version: BLOB_ENCRYPTED_BYTES_VERSION,\n }),\n metadataHash,\n nonceDomainHash,\n organizationId: input.organizationId,\n partCount,\n plaintextByteLength,\n recordKey,\n };\n}\n\nasync function encryptBlobChunk(\n context: BlobEncryptionContext,\n chunkIndex: number,\n plaintext: BlobBytes,\n): Promise<BlobBytes> {\n return new Uint8Array(\n await crypto.subtle.encrypt(\n {\n name: \"AES-GCM\",\n iv: deriveBlobChunkIv(context.baseIv, chunkIndex),\n additionalData: contentRecordAdditionalDataBytes({\n blobId: context.blobId,\n chunkCount: context.partCount,\n chunkIndex,\n chunkPlaintextByteLength: plaintext.byteLength,\n chunkSize: context.chunkSize,\n contentKeyEpoch: context.contentKeyEpoch,\n contentRecordId: context.contentRecordId,\n metadataHash: context.metadataHash,\n nonceDomainHash: context.nonceDomainHash,\n organizationId: context.organizationId,\n plaintextByteLength: context.plaintextByteLength,\n }),\n },\n context.recordKey,\n plaintext,\n ),\n );\n}\n\nasync function runEncryptionHashPass(\n source: BlobByteSource,\n snapshot: BlobSourceSnapshot,\n context: BlobEncryptionContext,\n): Promise<string> {\n const objectHash = createIncrementalSha256();\n objectHash.update(context.headerBytes);\n for (let chunkIndex = 0; chunkIndex < context.partCount; chunkIndex += 1) {\n const plaintext = await readVerifiedBlobSourceChunk({\n chunkIndex,\n snapshot,\n source,\n });\n objectHash.update(await encryptBlobChunk(context, chunkIndex, plaintext));\n }\n return bytesToHex(objectHash.digest());\n}\n\nfunction planPartByteLength(\n context: BlobEncryptionContext,\n index: number,\n): number {\n assertPartIndex(index, context.partCount);\n const plaintextLength = blobChunkPlaintextByteLength({\n chunkCount: context.partCount,\n chunkIndex: index,\n chunkSize: context.chunkSize,\n plaintextByteLength: context.plaintextByteLength,\n });\n return (\n plaintextLength +\n AES_GCM_TAG_BYTES +\n (index === 0 ? context.headerBytes.byteLength : 0)\n );\n}\n\nfunction createBlobEncryptionPlan(input: {\n readonly context: BlobEncryptionContext;\n readonly sha256: string;\n readonly snapshot: BlobSourceSnapshot;\n readonly source: BlobByteSource;\n}): BlobEncryptionPlan {\n const { context, sha256, snapshot, source } = input;\n return {\n byteLength: computeBlobEncryptedByteLength({\n chunkCount: context.partCount,\n headerByteLength: context.headerBytes.byteLength,\n plaintextByteLength: context.plaintextByteLength,\n }),\n chunkSize: context.chunkSize,\n metadataHash: context.metadataHash,\n partCount: context.partCount,\n plaintextByteLength: context.plaintextByteLength,\n plaintextSha256: snapshot.sha256,\n sha256,\n getPartByteLength(index) {\n return planPartByteLength(context, index);\n },\n async encryptPart(index) {\n assertPartIndex(index, context.partCount);\n const plaintext = await readVerifiedBlobSourceChunk({\n chunkIndex: index,\n snapshot,\n source,\n });\n const ciphertext = await encryptBlobChunk(context, index, plaintext);\n return joinBlobPartBytes(\n index === 0 ? context.headerBytes : null,\n ciphertext,\n );\n },\n };\n}\n\nexport async function prepareBlobEncryption(\n input: PrepareBlobEncryptionInput,\n): Promise<BlobEncryptionPlan> {\n if (input.contentKey.byteLength !== AES_256_KEY_BYTES) {\n throw new Error(\"Blob content key must be 32 bytes\");\n }\n if (input.nonceSeed) {\n assertAesGcmIv(input.nonceSeed, \"Blob nonce seed is invalid\");\n }\n const chunkSize = normalizeBlobChunkSize(\n input.chunkSize ?? DEFAULT_BLOB_CHUNK_SIZE_BYTES,\n );\n const snapshot =\n input.sourceSnapshot ?? (await inspectBlobSource(input.source, chunkSize));\n const context = await createBlobEncryptionContext(input, snapshot);\n if (\n snapshot.byteLength !== context.plaintextByteLength ||\n snapshot.chunkSize !== context.chunkSize\n ) {\n throw new Error(\"Blob source snapshot does not match encryption layout\");\n }\n const sha256 = await runEncryptionHashPass(input.source, snapshot, context);\n return createBlobEncryptionPlan({\n context,\n sha256,\n snapshot,\n source: input.source,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chunkedBlobCrypto.js","sourceRoot":"","sources":["chunkedBlobCrypto.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AES_256_KEY_BYTES,\n AES_GCM_TAG_BYTES,\n assertAesGcmIv,\n BLOB_CHUNK_SIZE_BYTES,\n BLOB_ENCRYPTED_BYTES_FORMAT,\n BLOB_ENCRYPTED_BYTES_VERSION,\n bytesToHex,\n CONTENT_RECORD_ENCRYPTION_SUITE,\n computeBlobChunkCount,\n computeBlobEncryptedByteLength,\n computeContentRecordNonceDomainHash,\n createAesGcmIv,\n createIncrementalSha256,\n deriveBlobChunkIv,\n encodeBlobEnvelopeV2Header,\n normalizeBlobChunkSize,\n} from \"@tearleads/crypto\";\nimport { bytesToBase64 } from \"@tearleads/encoding\";\nimport type { BlobContentKeyBundleRequest } from \"@tearleads/validators/request\";\nimport type { BlobByteSource, BlobBytes } from \"../../../blobContracts\";\nimport { importContentKeyMaterial } from \"../../shared/contentRecordKeys\";\nimport {\n blobContentMetadataHash,\n contentRecordAdditionalDataBytes,\n deriveBlobBaseIv,\n deriveBlobContentRecordKey,\n} from \"./blobRecordCrypto\";\nimport {\n type BlobSourceSnapshot,\n inspectBlobSource,\n readVerifiedBlobSourceChunk,\n} from \"./blobSourceSnapshot\";\nimport { createEncryptedUploadParts } from \"./encryptedUploadParts\";\nimport type { BlobEncryptionPlan } from \"./types\";\n\nexport const DEFAULT_BLOB_CHUNK_SIZE_BYTES = BLOB_CHUNK_SIZE_BYTES;\n\ninterface PrepareBlobEncryptionInput {\n readonly blobId: string;\n readonly chunkSize?: number | undefined;\n readonly contentKey: Uint8Array;\n readonly contentKeyBundle: BlobContentKeyBundleRequest;\n readonly nonceSeed?: Uint8Array | undefined;\n readonly organizationId: string;\n readonly source: BlobByteSource;\n readonly sourceSnapshot?: BlobSourceSnapshot | undefined;\n}\n\ninterface BlobEncryptionContext {\n readonly baseIv: BlobBytes;\n readonly blobId: string;\n readonly chunkSize: number;\n readonly contentKeyEpoch: number;\n readonly contentRecordId: string;\n readonly headerBytes: BlobBytes;\n readonly metadataHash: string;\n readonly nonceDomainHash: string;\n readonly organizationId: string;\n readonly partCount: number;\n readonly plaintextByteLength: number;\n readonly recordKey: CryptoKey;\n}\n\nasync function createBlobEncryptionContext(\n input: PrepareBlobEncryptionInput,\n snapshot: BlobSourceSnapshot,\n): Promise<BlobEncryptionContext> {\n const chunkSize = normalizeBlobChunkSize(\n input.chunkSize ?? DEFAULT_BLOB_CHUNK_SIZE_BYTES,\n );\n const plaintextByteLength = input.source.byteLength;\n const partCount = computeBlobChunkCount(plaintextByteLength, chunkSize);\n const contentRecordId = input.blobId;\n const { contentKeyEpoch } = input.contentKeyBundle;\n const nonceSeed = new Uint8Array(input.nonceSeed ?? createAesGcmIv());\n const baseIv = await deriveBlobBaseIv({\n chunkSize,\n nonceSeed,\n plaintextSha256: snapshot.sha256,\n });\n\n const [nonceDomainHash, metadataHash] = await Promise.all([\n computeContentRecordNonceDomainHash({\n version: 1,\n organizationId: input.organizationId,\n objectKind: \"blob\",\n objectId: input.blobId,\n contentKeyEpoch,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n contentRecordId,\n }),\n blobContentMetadataHash({\n blobId: input.blobId,\n byteLength: plaintextByteLength,\n chunkCount: partCount,\n chunkSize,\n contentKeyEpoch,\n }),\n ]);\n const contentKeyMaterial = await importContentKeyMaterial(input.contentKey);\n const recordKey = await deriveBlobContentRecordKey({\n blobId: input.blobId,\n contentKeyEpoch,\n contentKeyMaterial,\n contentRecordId,\n organizationId: input.organizationId,\n usage: \"encrypt\",\n });\n return {\n baseIv,\n blobId: input.blobId,\n chunkSize,\n contentKeyEpoch,\n contentRecordId,\n headerBytes: encodeBlobEnvelopeV2Header({\n blobId: input.blobId,\n byteLength: plaintextByteLength,\n chunkCount: partCount,\n chunkSize,\n contentKeyEpoch,\n contentRecordId,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n format: BLOB_ENCRYPTED_BYTES_FORMAT,\n iv: bytesToBase64(baseIv),\n metadataHash,\n nonceDomainHash,\n version: BLOB_ENCRYPTED_BYTES_VERSION,\n }),\n metadataHash,\n nonceDomainHash,\n organizationId: input.organizationId,\n partCount,\n plaintextByteLength,\n recordKey,\n };\n}\n\nasync function encryptBlobChunk(\n context: BlobEncryptionContext,\n chunkIndex: number,\n plaintext: BlobBytes,\n): Promise<BlobBytes> {\n return new Uint8Array(\n await crypto.subtle.encrypt(\n {\n name: \"AES-GCM\",\n iv: deriveBlobChunkIv(context.baseIv, chunkIndex),\n additionalData: contentRecordAdditionalDataBytes({\n blobId: context.blobId,\n chunkCount: context.partCount,\n chunkIndex,\n chunkPlaintextByteLength: plaintext.byteLength,\n chunkSize: context.chunkSize,\n contentKeyEpoch: context.contentKeyEpoch,\n contentRecordId: context.contentRecordId,\n metadataHash: context.metadataHash,\n nonceDomainHash: context.nonceDomainHash,\n organizationId: context.organizationId,\n plaintextByteLength: context.plaintextByteLength,\n }),\n },\n context.recordKey,\n plaintext,\n ),\n );\n}\n\nasync function runEncryptionHashPass(\n source: BlobByteSource,\n snapshot: BlobSourceSnapshot,\n context: BlobEncryptionContext,\n): Promise<string> {\n const objectHash = createIncrementalSha256();\n objectHash.update(context.headerBytes);\n for (let chunkIndex = 0; chunkIndex < context.partCount; chunkIndex += 1) {\n const plaintext = await readVerifiedBlobSourceChunk({\n chunkIndex,\n snapshot,\n source,\n });\n objectHash.update(await encryptBlobChunk(context, chunkIndex, plaintext));\n }\n return bytesToHex(objectHash.digest());\n}\n\nfunction createBlobEncryptionPlan(input: {\n readonly context: BlobEncryptionContext;\n readonly sha256: string;\n readonly snapshot: BlobSourceSnapshot;\n readonly source: BlobByteSource;\n}): BlobEncryptionPlan {\n const { context, sha256, snapshot, source } = input;\n const byteLength = computeBlobEncryptedByteLength({\n chunkCount: context.partCount,\n headerByteLength: context.headerBytes.byteLength,\n plaintextByteLength: context.plaintextByteLength,\n });\n return {\n byteLength,\n chunkSize: context.chunkSize,\n metadataHash: context.metadataHash,\n plaintextByteLength: context.plaintextByteLength,\n plaintextSha256: snapshot.sha256,\n sha256,\n ...createEncryptedUploadParts({\n byteLength,\n chunkSize: context.chunkSize + AES_GCM_TAG_BYTES,\n header: context.headerBytes,\n async encryptChunk(chunkIndex) {\n const plaintext = await readVerifiedBlobSourceChunk({\n chunkIndex,\n snapshot,\n source,\n });\n return encryptBlobChunk(context, chunkIndex, plaintext);\n },\n }),\n };\n}\n\nexport async function prepareBlobEncryption(\n input: PrepareBlobEncryptionInput,\n): Promise<BlobEncryptionPlan> {\n if (input.contentKey.byteLength !== AES_256_KEY_BYTES) {\n throw new Error(\"Blob content key must be 32 bytes\");\n }\n if (input.nonceSeed) {\n assertAesGcmIv(input.nonceSeed, \"Blob nonce seed is invalid\");\n }\n const chunkSize = normalizeBlobChunkSize(\n input.chunkSize ?? DEFAULT_BLOB_CHUNK_SIZE_BYTES,\n );\n const snapshot =\n input.sourceSnapshot ?? (await inspectBlobSource(input.source, chunkSize));\n const context = await createBlobEncryptionContext(input, snapshot);\n if (\n snapshot.byteLength !== context.plaintextByteLength ||\n snapshot.chunkSize !== context.chunkSize\n ) {\n throw new Error(\"Blob source snapshot does not match encryption layout\");\n }\n const sha256 = await runEncryptionHashPass(input.source, snapshot, context);\n return createBlobEncryptionPlan({\n context,\n sha256,\n snapshot,\n source: input.source,\n });\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Repartition the envelope without changing its encryption or buffering the file. */
|
|
2
|
+
export function createEncryptedUploadParts(input) {
|
|
3
|
+
// Including the header in the fixed transport size keeps even a maximum-size
|
|
4
|
+
// envelope within 10,000 parts. R2 requires equal sizes for all nonfinal parts.
|
|
5
|
+
const partSize = input.chunkSize + input.header.byteLength;
|
|
6
|
+
const partCount = Math.ceil(input.byteLength / partSize);
|
|
7
|
+
function getPartByteLength(index) {
|
|
8
|
+
if (!Number.isInteger(index) || index < 0 || index >= partCount) {
|
|
9
|
+
throw new Error("Blob encryption part index is out of range");
|
|
10
|
+
}
|
|
11
|
+
return Math.min(partSize, input.byteLength - index * partSize);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
partCount,
|
|
15
|
+
getPartByteLength,
|
|
16
|
+
async encryptPart(index) {
|
|
17
|
+
const output = new Uint8Array(getPartByteLength(index));
|
|
18
|
+
const start = index * partSize;
|
|
19
|
+
const end = start + output.byteLength;
|
|
20
|
+
if (start < input.header.byteLength) {
|
|
21
|
+
output.set(input.header.subarray(start, Math.min(end, input.header.byteLength)));
|
|
22
|
+
}
|
|
23
|
+
let position = Math.max(start, input.header.byteLength);
|
|
24
|
+
while (position < end) {
|
|
25
|
+
const chunkIndex = Math.floor((position - input.header.byteLength) / input.chunkSize);
|
|
26
|
+
const chunkStart = input.header.byteLength + chunkIndex * input.chunkSize;
|
|
27
|
+
// Adjacent transport parts can re-read and re-encrypt a shared chunk.
|
|
28
|
+
// Independent calls keep memory bounded during concurrent upload/resume;
|
|
29
|
+
// the tradeoff is up to two encryptions per chunk in this upload pass.
|
|
30
|
+
const chunk = await input.encryptChunk(chunkIndex);
|
|
31
|
+
const offset = position - chunkStart;
|
|
32
|
+
const length = Math.min(chunk.byteLength - offset, end - position);
|
|
33
|
+
if (length <= 0)
|
|
34
|
+
throw new Error("Encrypted blob chunk is shorter than its layout");
|
|
35
|
+
output.set(chunk.subarray(offset, offset + length), position - start);
|
|
36
|
+
position += length;
|
|
37
|
+
}
|
|
38
|
+
return output;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=encryptedUploadParts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryptedUploadParts.js","sourceRoot":"","sources":["encryptedUploadParts.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,MAAM,UAAU,0BAA0B,CAAC,KAK1C;IACC,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC;IAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC;IACzD,SAAS,iBAAiB,CAAC,KAAa;QACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC,CAAC;IACjE,CAAC;IACD,OAAO;QACL,SAAS;QACT,iBAAiB;QACjB,KAAK,CAAC,WAAW,CAAC,KAAa;YAC7B,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;YACxD,MAAM,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;YAC/B,MAAM,GAAG,GAAG,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC;YACtC,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;gBACpC,MAAM,CAAC,GAAG,CACR,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CACrE,CAAC;YACJ,CAAC;YACD,IAAI,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACxD,OAAO,QAAQ,GAAG,GAAG,EAAE,CAAC;gBACtB,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAC3B,CAAC,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,SAAS,CACvD,CAAC;gBACF,MAAM,UAAU,GACd,KAAK,CAAC,MAAM,CAAC,UAAU,GAAG,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC;gBACzD,sEAAsE;gBACtE,yEAAyE;gBACzE,uEAAuE;gBACvE,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;gBACnD,MAAM,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;gBACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,MAAM,EAAE,GAAG,GAAG,QAAQ,CAAC,CAAC;gBACnE,IAAI,MAAM,IAAI,CAAC;oBACb,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;gBACrE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAC;gBACtE,QAAQ,IAAI,MAAM,CAAC;YACrB,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { BlobBytes } from \"../../../blobContracts\";\n\n/** Repartition the envelope without changing its encryption or buffering the file. */\nexport function createEncryptedUploadParts(input: {\n readonly byteLength: number;\n readonly chunkSize: number;\n readonly header: BlobBytes;\n readonly encryptChunk: (index: number) => Promise<BlobBytes>;\n}) {\n // Including the header in the fixed transport size keeps even a maximum-size\n // envelope within 10,000 parts. R2 requires equal sizes for all nonfinal parts.\n const partSize = input.chunkSize + input.header.byteLength;\n const partCount = Math.ceil(input.byteLength / partSize);\n function getPartByteLength(index: number): number {\n if (!Number.isInteger(index) || index < 0 || index >= partCount) {\n throw new Error(\"Blob encryption part index is out of range\");\n }\n return Math.min(partSize, input.byteLength - index * partSize);\n }\n return {\n partCount,\n getPartByteLength,\n async encryptPart(index: number): Promise<BlobBytes> {\n const output = new Uint8Array(getPartByteLength(index));\n const start = index * partSize;\n const end = start + output.byteLength;\n if (start < input.header.byteLength) {\n output.set(\n input.header.subarray(start, Math.min(end, input.header.byteLength)),\n );\n }\n let position = Math.max(start, input.header.byteLength);\n while (position < end) {\n const chunkIndex = Math.floor(\n (position - input.header.byteLength) / input.chunkSize,\n );\n const chunkStart =\n input.header.byteLength + chunkIndex * input.chunkSize;\n // Adjacent transport parts can re-read and re-encrypt a shared chunk.\n // Independent calls keep memory bounded during concurrent upload/resume;\n // the tradeoff is up to two encryptions per chunk in this upload pass.\n const chunk = await input.encryptChunk(chunkIndex);\n const offset = position - chunkStart;\n const length = Math.min(chunk.byteLength - offset, end - position);\n if (length <= 0)\n throw new Error(\"Encrypted blob chunk is shorter than its layout\");\n output.set(chunk.subarray(offset, offset + length), position - start);\n position += length;\n }\n return output;\n },\n };\n}\n"]}
|
|
@@ -126,7 +126,7 @@ export interface UploadDocumentAttachmentInput {
|
|
|
126
126
|
writerProjection?: DocumentWriterProjectionResponse | undefined;
|
|
127
127
|
}
|
|
128
128
|
export interface MultipartBlobUploadOptions {
|
|
129
|
-
/** Exactly 5 MiB
|
|
129
|
+
/** Exactly 5 MiB per plaintext encryption chunk; transport parts include envelope overhead. */
|
|
130
130
|
readonly partSize: number;
|
|
131
131
|
readonly resumeStageId?: string | undefined;
|
|
132
132
|
readonly uploadConcurrency?: number | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAmCA,MAAM,CAAC,MAAM,mCAAmC,GAC9C,wCAAwC,CAAC;AAC3C,MAAM,CAAC,MAAM,8BAA8B,GACzC,mCAAmC,CAAC;AACtC,MAAM,CAAC,MAAM,wCAAwC,GACnD,wCAAwC,CAAC;AAC3C,MAAM,CAAC,MAAM,gCAAgC,GAC3C,qCAAqC,CAAC;AACxC,MAAM,CAAC,MAAM,6BAA6B,GACxC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,yCAAyC,CAAC,CAAC;AACtE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,WAAW,EAAE,CAAC","sourcesContent":["import type {\n RequestFailureKind,\n UploadMultipartBlobPartBytesRequest,\n} from \"@tearleads/api-client\";\nimport type {\n BlobContentKeyTarget,\n BlobEnvelopeHeaderRecord,\n BlobEnvelopeRecord,\n WriteHeader,\n} from \"@tearleads/crypto\";\nimport type {\n BlobAttachmentBindRequest,\n BlobAttachmentDetachRequest,\n BlobContentKeyBundleRequest,\n CompleteMultipartBlobStageRequest,\n InitiateMultipartBlobStageRequest,\n} from \"@tearleads/validators/request\";\nimport type {\n BlobAttachmentBindResponse,\n BlobAttachmentDetachResponse,\n BlobAttachmentSummary,\n CompleteMultipartBlobStageResponse,\n DocumentWriterProjectionResponse,\n InitiateMultipartBlobStageResponse,\n MultipartBlobStageStatusResponse,\n UploadMultipartBlobPartResponse,\n} from \"@tearleads/validators/response\";\nimport type { BlobByteSourceInput, BlobBytes } from \"../../../blobContracts\";\nimport type {\n ProjectionUserKeyResolver,\n ReferencedPrincipalPolicyWarmer,\n} from \"../../../keyingProjectionVerification\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport type { DocumentCreateAuthor } from \"../../shared/types\";\n\nexport const BLOB_CONTENT_RECORD_KEY_INFO_DOMAIN =\n \"tearleads.blob.content-record-key-info\";\nexport const BLOB_CONTENT_RECORD_AAD_DOMAIN =\n \"tearleads.blob.content-record-aad\";\nexport const BLOB_CONTENT_RECORD_METADATA_HASH_DOMAIN =\n \"tearleads.blob.content-record-metadata\";\nexport const BLOB_CONTENT_RECORD_NONCE_DOMAIN =\n \"tearleads.blob.content-record-nonce\";\nexport const BLOB_CONTENT_RECORD_HKDF_SALT: Uint8Array<ArrayBuffer> =\n new TextEncoder().encode(\"tearleads.blob.content-record-hkdf-salt\");\nexport const TEXT_ENCODER = new TextEncoder();\n\nexport interface BlobAttachmentRequestOptions {\n readonly expectedPaymentRequiredOrganizationId?: string | undefined;\n}\n\nexport interface BlobAttachmentApi {\n bindBlobAttachment(\n blobId: string,\n input: BlobAttachmentBindRequest,\n options?: BlobAttachmentRequestOptions,\n ): Promise<BlobAttachmentBindResponse | null>;\n clearWriterProjectionCaches?(): void;\n evictDocumentWriterProjection?(documentId: string): void;\n getRequestFailure?(input: {\n method: \"DELETE\" | \"GET\" | \"POST\" | \"PUT\";\n path: string;\n }): {\n readonly code?: string | undefined;\n readonly kind: RequestFailureKind;\n readonly message: string;\n readonly status: number | null;\n } | null;\n completeMultipartBlobStage(\n stageId: string,\n input: CompleteMultipartBlobStageRequest,\n ): Promise<CompleteMultipartBlobStageResponse | null>;\n getDocumentWriterProjection(\n documentId: string,\n ): Promise<DocumentWriterProjectionResponse | null>;\n getMultipartBlobStage(\n stageId: string,\n ): Promise<MultipartBlobStageStatusResponse | null>;\n initiateMultipartBlobStage(\n input: InitiateMultipartBlobStageRequest,\n ): Promise<InitiateMultipartBlobStageResponse | null>;\n uploadMultipartBlobPartBytes(\n stageId: string,\n partNumber: number,\n input: UploadMultipartBlobPartBytesRequest,\n ): Promise<UploadMultipartBlobPartResponse | null>;\n}\n\nexport interface BlobAttachmentDetachApi {\n detachBlobAttachment(\n blobId: string,\n bindingId: string,\n input: BlobAttachmentDetachRequest,\n options?: BlobAttachmentRequestOptions,\n ): Promise<BlobAttachmentDetachResponse | null>;\n getDocumentWriterProjection(\n documentId: string,\n ): Promise<DocumentWriterProjectionResponse | null>;\n}\n\nexport type { BlobContentKeyTarget };\n\n/**\n * Aliases of the crypto parser's own types. The parser is shared with the API,\n * so restating these shapes here would let the two drift apart silently.\n */\nexport type BlobEncryptedBytesRecord = BlobEnvelopeRecord;\n\n/**\n * The envelope as the header establishes it. Streaming readers hold this\n * before any ciphertext is read, so it has no `chunks`: an empty array there\n * would read as an empty object rather than an unread one.\n */\nexport type BlobEncryptedBytesHeader = BlobEnvelopeHeaderRecord;\n\nexport type BlobEncryptedChunk = BlobEnvelopeRecord[\"chunks\"][number];\n\nexport interface BlobSourceSnapshot {\n readonly byteLength: number;\n readonly chunkSha256: readonly string[];\n readonly chunkSize: number;\n readonly sha256: string;\n}\n\nexport interface BlobEncryptionPlan {\n readonly byteLength: number;\n readonly chunkSize: number;\n readonly metadataHash: string;\n readonly partCount: number;\n readonly plaintextByteLength: number;\n readonly plaintextSha256: string;\n readonly sha256: string;\n encryptPart(index: number): Promise<BlobBytes>;\n getPartByteLength(index: number): number;\n}\n\nexport interface DocumentManifestIdentity {\n documentId: string;\n manifestHash: string;\n organizationId: string;\n}\n\nexport interface BlobAttachmentMaterial {\n blobAccessManifestHash: string;\n contentKeyBundle: BlobContentKeyBundleRequest;\n encrypted: BlobEncryptionPlan;\n manifestIdentity: DocumentManifestIdentity;\n targetHash: string;\n targets: BlobContentKeyTarget[];\n writerProjection: DocumentWriterProjectionResponse;\n}\n\n/**\n * Progress for an in-flight multipart blob upload.\n */\nexport interface MultipartUploadProgress {\n readonly bytesTotal: number;\n readonly bytesUploaded: number;\n readonly partsCompleted: number;\n readonly partsTotal: number;\n}\n\nexport type MultipartUploadProgressListener = (\n progress: MultipartUploadProgress,\n) => void;\n\n/**\n * Fires once a multipart stage has been opened (or resumed), with its id and\n * plaintext chunk size. Callers persist both so a later attempt can reproduce\n * the same encrypted parts and resume instead of orphaning the partial upload.\n */\nexport type MultipartStageResolvedListener = (input: {\n readonly partSize: number;\n readonly stageId: string;\n}) => void | Promise<void>;\n\nexport interface UploadDocumentAttachmentInput {\n apiClient: BlobAttachmentApi;\n author: DocumentCreateAuthor;\n blobId?: string | undefined;\n bindingId?: string | undefined;\n bytes: BlobByteSourceInput;\n contentKey?: Uint8Array | undefined;\n contentKeyEpoch?: number | undefined;\n documentId: string;\n eventId?: string | undefined;\n execSql: ExecSql;\n expectedBindingId: string | null;\n isRemoteSyncBlocked?: ((organizationId: string) => boolean) | undefined;\n // A persisted nonce seed. The plaintext digest derives the envelope IV from\n // it, then each chunk derives a unique IV. Same-source retries reproduce the\n // encrypted parts; changed bytes cannot reuse their AES-GCM nonces.\n nonceSeed?: Uint8Array | undefined;\n multipart?: MultipartBlobUploadOptions | undefined;\n onMultipartProgress?: MultipartUploadProgressListener | undefined;\n onStageResolved?: MultipartStageResolvedListener | undefined;\n resolveProjectionUserKey: ProjectionUserKeyResolver;\n signedAt?: string | undefined;\n slotId: string;\n targetSecretKey: Uint8Array;\n warmReferencedPrincipalPolicies?: ReferencedPrincipalPolicyWarmer | undefined;\n writerProjection?: DocumentWriterProjectionResponse | undefined;\n}\n\nexport interface MultipartBlobUploadOptions {\n /** Exactly 5 MiB
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAmCA,MAAM,CAAC,MAAM,mCAAmC,GAC9C,wCAAwC,CAAC;AAC3C,MAAM,CAAC,MAAM,8BAA8B,GACzC,mCAAmC,CAAC;AACtC,MAAM,CAAC,MAAM,wCAAwC,GACnD,wCAAwC,CAAC;AAC3C,MAAM,CAAC,MAAM,gCAAgC,GAC3C,qCAAqC,CAAC;AACxC,MAAM,CAAC,MAAM,6BAA6B,GACxC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,yCAAyC,CAAC,CAAC;AACtE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,WAAW,EAAE,CAAC","sourcesContent":["import type {\n RequestFailureKind,\n UploadMultipartBlobPartBytesRequest,\n} from \"@tearleads/api-client\";\nimport type {\n BlobContentKeyTarget,\n BlobEnvelopeHeaderRecord,\n BlobEnvelopeRecord,\n WriteHeader,\n} from \"@tearleads/crypto\";\nimport type {\n BlobAttachmentBindRequest,\n BlobAttachmentDetachRequest,\n BlobContentKeyBundleRequest,\n CompleteMultipartBlobStageRequest,\n InitiateMultipartBlobStageRequest,\n} from \"@tearleads/validators/request\";\nimport type {\n BlobAttachmentBindResponse,\n BlobAttachmentDetachResponse,\n BlobAttachmentSummary,\n CompleteMultipartBlobStageResponse,\n DocumentWriterProjectionResponse,\n InitiateMultipartBlobStageResponse,\n MultipartBlobStageStatusResponse,\n UploadMultipartBlobPartResponse,\n} from \"@tearleads/validators/response\";\nimport type { BlobByteSourceInput, BlobBytes } from \"../../../blobContracts\";\nimport type {\n ProjectionUserKeyResolver,\n ReferencedPrincipalPolicyWarmer,\n} from \"../../../keyingProjectionVerification\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport type { DocumentCreateAuthor } from \"../../shared/types\";\n\nexport const BLOB_CONTENT_RECORD_KEY_INFO_DOMAIN =\n \"tearleads.blob.content-record-key-info\";\nexport const BLOB_CONTENT_RECORD_AAD_DOMAIN =\n \"tearleads.blob.content-record-aad\";\nexport const BLOB_CONTENT_RECORD_METADATA_HASH_DOMAIN =\n \"tearleads.blob.content-record-metadata\";\nexport const BLOB_CONTENT_RECORD_NONCE_DOMAIN =\n \"tearleads.blob.content-record-nonce\";\nexport const BLOB_CONTENT_RECORD_HKDF_SALT: Uint8Array<ArrayBuffer> =\n new TextEncoder().encode(\"tearleads.blob.content-record-hkdf-salt\");\nexport const TEXT_ENCODER = new TextEncoder();\n\nexport interface BlobAttachmentRequestOptions {\n readonly expectedPaymentRequiredOrganizationId?: string | undefined;\n}\n\nexport interface BlobAttachmentApi {\n bindBlobAttachment(\n blobId: string,\n input: BlobAttachmentBindRequest,\n options?: BlobAttachmentRequestOptions,\n ): Promise<BlobAttachmentBindResponse | null>;\n clearWriterProjectionCaches?(): void;\n evictDocumentWriterProjection?(documentId: string): void;\n getRequestFailure?(input: {\n method: \"DELETE\" | \"GET\" | \"POST\" | \"PUT\";\n path: string;\n }): {\n readonly code?: string | undefined;\n readonly kind: RequestFailureKind;\n readonly message: string;\n readonly status: number | null;\n } | null;\n completeMultipartBlobStage(\n stageId: string,\n input: CompleteMultipartBlobStageRequest,\n ): Promise<CompleteMultipartBlobStageResponse | null>;\n getDocumentWriterProjection(\n documentId: string,\n ): Promise<DocumentWriterProjectionResponse | null>;\n getMultipartBlobStage(\n stageId: string,\n ): Promise<MultipartBlobStageStatusResponse | null>;\n initiateMultipartBlobStage(\n input: InitiateMultipartBlobStageRequest,\n ): Promise<InitiateMultipartBlobStageResponse | null>;\n uploadMultipartBlobPartBytes(\n stageId: string,\n partNumber: number,\n input: UploadMultipartBlobPartBytesRequest,\n ): Promise<UploadMultipartBlobPartResponse | null>;\n}\n\nexport interface BlobAttachmentDetachApi {\n detachBlobAttachment(\n blobId: string,\n bindingId: string,\n input: BlobAttachmentDetachRequest,\n options?: BlobAttachmentRequestOptions,\n ): Promise<BlobAttachmentDetachResponse | null>;\n getDocumentWriterProjection(\n documentId: string,\n ): Promise<DocumentWriterProjectionResponse | null>;\n}\n\nexport type { BlobContentKeyTarget };\n\n/**\n * Aliases of the crypto parser's own types. The parser is shared with the API,\n * so restating these shapes here would let the two drift apart silently.\n */\nexport type BlobEncryptedBytesRecord = BlobEnvelopeRecord;\n\n/**\n * The envelope as the header establishes it. Streaming readers hold this\n * before any ciphertext is read, so it has no `chunks`: an empty array there\n * would read as an empty object rather than an unread one.\n */\nexport type BlobEncryptedBytesHeader = BlobEnvelopeHeaderRecord;\n\nexport type BlobEncryptedChunk = BlobEnvelopeRecord[\"chunks\"][number];\n\nexport interface BlobSourceSnapshot {\n readonly byteLength: number;\n readonly chunkSha256: readonly string[];\n readonly chunkSize: number;\n readonly sha256: string;\n}\n\nexport interface BlobEncryptionPlan {\n readonly byteLength: number;\n readonly chunkSize: number;\n readonly metadataHash: string;\n readonly partCount: number;\n readonly plaintextByteLength: number;\n readonly plaintextSha256: string;\n readonly sha256: string;\n encryptPart(index: number): Promise<BlobBytes>;\n getPartByteLength(index: number): number;\n}\n\nexport interface DocumentManifestIdentity {\n documentId: string;\n manifestHash: string;\n organizationId: string;\n}\n\nexport interface BlobAttachmentMaterial {\n blobAccessManifestHash: string;\n contentKeyBundle: BlobContentKeyBundleRequest;\n encrypted: BlobEncryptionPlan;\n manifestIdentity: DocumentManifestIdentity;\n targetHash: string;\n targets: BlobContentKeyTarget[];\n writerProjection: DocumentWriterProjectionResponse;\n}\n\n/**\n * Progress for an in-flight multipart blob upload.\n */\nexport interface MultipartUploadProgress {\n readonly bytesTotal: number;\n readonly bytesUploaded: number;\n readonly partsCompleted: number;\n readonly partsTotal: number;\n}\n\nexport type MultipartUploadProgressListener = (\n progress: MultipartUploadProgress,\n) => void;\n\n/**\n * Fires once a multipart stage has been opened (or resumed), with its id and\n * plaintext chunk size. Callers persist both so a later attempt can reproduce\n * the same encrypted parts and resume instead of orphaning the partial upload.\n */\nexport type MultipartStageResolvedListener = (input: {\n readonly partSize: number;\n readonly stageId: string;\n}) => void | Promise<void>;\n\nexport interface UploadDocumentAttachmentInput {\n apiClient: BlobAttachmentApi;\n author: DocumentCreateAuthor;\n blobId?: string | undefined;\n bindingId?: string | undefined;\n bytes: BlobByteSourceInput;\n contentKey?: Uint8Array | undefined;\n contentKeyEpoch?: number | undefined;\n documentId: string;\n eventId?: string | undefined;\n execSql: ExecSql;\n expectedBindingId: string | null;\n isRemoteSyncBlocked?: ((organizationId: string) => boolean) | undefined;\n // A persisted nonce seed. The plaintext digest derives the envelope IV from\n // it, then each chunk derives a unique IV. Same-source retries reproduce the\n // encrypted parts; changed bytes cannot reuse their AES-GCM nonces.\n nonceSeed?: Uint8Array | undefined;\n multipart?: MultipartBlobUploadOptions | undefined;\n onMultipartProgress?: MultipartUploadProgressListener | undefined;\n onStageResolved?: MultipartStageResolvedListener | undefined;\n resolveProjectionUserKey: ProjectionUserKeyResolver;\n signedAt?: string | undefined;\n slotId: string;\n targetSecretKey: Uint8Array;\n warmReferencedPrincipalPolicies?: ReferencedPrincipalPolicyWarmer | undefined;\n writerProjection?: DocumentWriterProjectionResponse | undefined;\n}\n\nexport interface MultipartBlobUploadOptions {\n /** Exactly 5 MiB per plaintext encryption chunk; transport parts include envelope overhead. */\n readonly partSize: number;\n // The id of a previously opened stage to resume. When set, the upload refetches\n // that stage and skips parts the server already has instead of opening a new\n // one; if the stage is gone (expired), it falls back to a fresh stage.\n readonly resumeStageId?: string | undefined;\n readonly uploadConcurrency?: number | undefined;\n}\n\nexport interface UploadDocumentAttachmentResult {\n blobId: string;\n bindingId: string;\n request: BlobAttachmentBindRequest;\n response: BlobAttachmentBindResponse;\n sha256: string;\n byteLength: number;\n writeHeader: WriteHeader;\n writeHeaderHash: string;\n writerProjection: DocumentWriterProjectionResponse;\n}\n\nexport interface DetachDocumentAttachmentInput {\n apiClient: BlobAttachmentDetachApi;\n author: DocumentCreateAuthor;\n bindingId: string;\n blobId: string;\n documentId: string;\n eventId?: string | undefined;\n execSql: ExecSql;\n isRemoteSyncBlocked?: ((organizationId: string) => boolean) | undefined;\n resolveProjectionUserKey: ProjectionUserKeyResolver;\n signedAt?: string | undefined;\n slotId: string;\n stillCurrent?: (() => boolean) | undefined;\n warmReferencedPrincipalPolicies?: ReferencedPrincipalPolicyWarmer | undefined;\n writerProjection?: DocumentWriterProjectionResponse | undefined;\n}\n\nexport interface DetachDocumentAttachmentResult {\n request: BlobAttachmentDetachRequest;\n response: BlobAttachmentDetachResponse;\n writerProjection: DocumentWriterProjectionResponse;\n}\n\nexport interface DecryptDocumentAttachmentBlobInput {\n binding: BlobAttachmentSummary;\n encryptedBytes: BlobBytes;\n expectedDocumentId: string;\n expectedSlotId: string;\n execSql: ExecSql;\n resolveProjectionUserKey: ProjectionUserKeyResolver;\n targetSecretKey: Uint8Array;\n stillCurrent?: (() => boolean) | undefined;\n warmReferencedPrincipalPolicies?: ReferencedPrincipalPolicyWarmer | undefined;\n writerProjection: DocumentWriterProjectionResponse;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -196,7 +196,7 @@ async function resolveMultipartStageStatus(input) {
|
|
|
196
196
|
}
|
|
197
197
|
function assertMultipartPlanLimits(encryption) {
|
|
198
198
|
if (encryption.partCount > MAX_BLOB_CHUNK_COUNT) {
|
|
199
|
-
throw new Error(`Multipart blob upload would require ${encryption.partCount.toLocaleString()} parts; the maximum is ${MAX_BLOB_CHUNK_COUNT.toLocaleString()}
|
|
199
|
+
throw new Error(`Multipart blob upload would require ${encryption.partCount.toLocaleString()} parts; the maximum is ${MAX_BLOB_CHUNK_COUNT.toLocaleString()} parts.`);
|
|
200
200
|
}
|
|
201
201
|
for (let index = 0; index < encryption.partCount; index += 1) {
|
|
202
202
|
if (encryption.getPartByteLength(index) > MAX_MULTIPART_PART_BYTES) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipartUpload.js","sourceRoot":"","sources":["multipartUpload.ts"],"names":[],"mappings":"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","sourcesContent":["import { bytesToHex, MAX_BLOB_CHUNK_COUNT } from \"@tearleads/crypto\";\nimport type {\n InitiateMultipartBlobStageResponse,\n MultipartBlobStagePart,\n MultipartBlobStageStatusResponse,\n} from \"@tearleads/validators/response\";\nimport type { BlobEncryptionPlan } from \"../../data/documents/blob/shared/crypto\";\nimport type {\n BlobAttachmentApi,\n MultipartStageResolvedListener,\n MultipartUploadProgressListener,\n UploadDocumentAttachmentInput,\n} from \"../../data/documents/blob/shared/types\";\nimport {\n assertMultipartStageReplaceable,\n multipartApiFailureMessage,\n multipartStagePath,\n} from \"./multipartStageFailures\";\n\nconst DEFAULT_MULTIPART_UPLOAD_CONCURRENCY = 4;\nconst MAX_MULTIPART_UPLOAD_CONCURRENCY = 4;\nconst MAX_MULTIPART_PART_BYTES = 5 * 1024 * 1024 * 1024;\n\ninterface MultipartPartCommit {\n readonly etag: string;\n readonly partNumber: number;\n}\n\ninterface MultipartPartUploadTask {\n readonly byteLength: number;\n readonly partIndex: number;\n readonly partNumber: number;\n}\n\nfunction pathSegment(value: number | string): string {\n return encodeURIComponent(String(value));\n}\n\nfunction multipartPartPath(input: {\n readonly partNumber: number;\n readonly stageId: string;\n}): string {\n const basePath = `${multipartStagePath(input.stageId)}/parts/${pathSegment(input.partNumber)}`;\n return `${basePath}/bytes`;\n}\n\nasync function sha256BytesHex(bytes: Uint8Array<ArrayBuffer>): Promise<string> {\n return bytesToHex(\n new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes)),\n );\n}\n\nfunction normalizeMultipartUploadConcurrency(\n value: number | undefined,\n): number {\n if (value === undefined) {\n return DEFAULT_MULTIPART_UPLOAD_CONCURRENCY;\n }\n if (\n !Number.isInteger(value) ||\n value <= 0 ||\n value > MAX_MULTIPART_UPLOAD_CONCURRENCY\n ) {\n throw new Error(\n `Multipart blob upload concurrency must be between 1 and ${MAX_MULTIPART_UPLOAD_CONCURRENCY}`,\n );\n }\n\n return value;\n}\n\nfunction isMultipartPartCommit(\n part: MultipartPartCommit | undefined,\n): part is MultipartPartCommit {\n return part !== undefined;\n}\n\nasync function uploadMultipartPartTasks(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly completeParts: (MultipartPartCommit | undefined)[];\n readonly concurrency: number;\n readonly encryption: BlobEncryptionPlan;\n readonly onPartUploaded?: (task: MultipartPartUploadTask) => void;\n readonly stageId: string;\n readonly tasks: readonly MultipartPartUploadTask[];\n readonly uploadId: string;\n}): Promise<void> {\n let failed = false;\n let firstFailure: { readonly error: unknown } | undefined;\n let nextTaskIndex = 0;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const task = input.tasks[nextTaskIndex];\n nextTaskIndex += 1;\n if (!task) {\n return;\n }\n\n try {\n const uploaded = await uploadMultipartPartBytes({\n apiClient: input.apiClient,\n encryption: input.encryption,\n stageId: input.stageId,\n task,\n uploadId: input.uploadId,\n });\n if (!uploaded) {\n failed = true;\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob part ${task.partNumber} upload failed for stage ${input.stageId}.`,\n request: {\n method: \"PUT\",\n path: multipartPartPath({\n partNumber: task.partNumber,\n stageId: input.stageId,\n }),\n },\n }),\n );\n }\n\n input.completeParts[task.partIndex] = {\n etag: uploaded.part.etag,\n partNumber: task.partNumber,\n };\n input.onPartUploaded?.(task);\n } catch (error) {\n // Stop sibling workers from pulling new tasks and firing more uploads\n // once any part rejects. Keep this worker alive long enough for the\n // shared join below to drain uploads siblings already started.\n failed = true;\n firstFailure ??= { error };\n return;\n }\n }\n }\n\n const workerCount = Math.min(input.concurrency, input.tasks.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n if (firstFailure) {\n throw firstFailure.error;\n }\n}\n\nasync function uploadMultipartPartBytes(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly encryption: BlobEncryptionPlan;\n readonly stageId: string;\n readonly task: MultipartPartUploadTask;\n readonly uploadId: string;\n}): ReturnType<BlobAttachmentApi[\"uploadMultipartBlobPartBytes\"]> {\n const encryptedPartBytes = await input.encryption.encryptPart(\n input.task.partIndex,\n );\n if (encryptedPartBytes.byteLength !== input.task.byteLength) {\n throw new Error(\n `Encrypted blob part ${input.task.partNumber} byte length mismatch: expected ${input.task.byteLength.toLocaleString()}, received ${encryptedPartBytes.byteLength.toLocaleString()}.`,\n );\n }\n\n const uploaded = await input.apiClient.uploadMultipartBlobPartBytes(\n input.stageId,\n input.task.partNumber,\n {\n byteLength: encryptedPartBytes.byteLength,\n encryptedBytes: encryptedPartBytes,\n sha256: await sha256BytesHex(encryptedPartBytes),\n uploadId: input.uploadId,\n },\n );\n if (\n uploaded &&\n (uploaded.stageId !== input.stageId ||\n uploaded.uploadId !== input.uploadId ||\n uploaded.part.partNumber !== input.task.partNumber ||\n uploaded.part.byteLength !== input.task.byteLength)\n ) {\n throw new Error(\n `Multipart blob part ${input.task.partNumber} response does not match its request.`,\n );\n }\n return uploaded;\n}\n\nfunction uploadedPartsByPartNumber(\n parts: readonly MultipartBlobStagePart[],\n): ReadonlyMap<number, MultipartBlobStagePart> {\n return new Map(parts.map((part) => [part.partNumber, part]));\n}\n\nfunction isMultipartStageStatus(\n stage: InitiateMultipartBlobStageResponse | MultipartBlobStageStatusResponse,\n): stage is MultipartBlobStageStatusResponse {\n return \"completed\" in stage && typeof stage.completed === \"boolean\";\n}\n\ninterface MultipartUploadProgressState {\n bytesTotal: number;\n bytesUploaded: number;\n partsCompleted: number;\n partsTotal: number;\n}\n\ninterface MultipartPartPlan {\n completeParts: (MultipartPartCommit | undefined)[];\n progress: MultipartUploadProgressState;\n uploadTasks: MultipartPartUploadTask[];\n}\n\n/**\n * Plans encrypted part indices without materializing their bytes, reusing any\n * already-staged parts and seeding cumulative progress for resumed uploads.\n */\nfunction planMultipartParts(input: {\n encryption: BlobEncryptionPlan;\n uploadedParts: ReadonlyMap<number, MultipartBlobStagePart>;\n}): MultipartPartPlan {\n const { encryption, uploadedParts } = input;\n const completeParts: (MultipartPartCommit | undefined)[] = new Array(\n encryption.partCount,\n );\n const uploadTasks: MultipartPartUploadTask[] = [];\n const progress: MultipartUploadProgressState = {\n bytesTotal: 0,\n bytesUploaded: 0,\n partsCompleted: 0,\n partsTotal: encryption.partCount,\n };\n\n for (let partIndex = 0; partIndex < encryption.partCount; partIndex += 1) {\n const partNumber = partIndex + 1;\n const byteLength = encryption.getPartByteLength(partIndex);\n progress.bytesTotal += byteLength;\n const uploadedPart = uploadedParts.get(partNumber);\n if (uploadedPart?.byteLength === byteLength) {\n completeParts[partIndex] = {\n etag: uploadedPart.etag,\n partNumber,\n };\n progress.partsCompleted += 1;\n progress.bytesUploaded += byteLength;\n continue;\n }\n\n uploadTasks.push({\n byteLength,\n partIndex,\n partNumber,\n });\n }\n\n return { completeParts, progress, uploadTasks };\n}\n\nasync function resolveMultipartStageStatus(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly byteLength: number;\n readonly multipart: NonNullable<UploadDocumentAttachmentInput[\"multipart\"]>;\n readonly organizationId: string;\n readonly onStageUnavailable?:\n | ((stageId: string) => Promise<void>)\n | undefined;\n readonly sha256: string;\n}): Promise<MultipartBlobStageStatusResponse> {\n const resumeStageId = input.multipart.resumeStageId;\n if (resumeStageId) {\n const resumed = await input.apiClient.getMultipartBlobStage(resumeStageId);\n if (!resumed) {\n await assertMultipartStageReplaceable({\n apiClient: input.apiClient,\n onStageUnavailable: input.onStageUnavailable,\n stageId: resumeStageId,\n });\n }\n if (resumed && resumed.stageId !== resumeStageId) {\n throw new Error(\n `Multipart blob resume response does not match stage ${resumeStageId}.`,\n );\n }\n if (resumed) {\n // A successful lookup is authoritative: a metadata mismatch indicates\n // inconsistent persisted state or a bad server response, not proof that\n // the existing stage is safe to abandon.\n if (\n resumed.organizationId !== input.organizationId ||\n resumed.byteLength !== input.byteLength ||\n resumed.sha256 !== input.sha256\n ) {\n throw new Error(\n `Multipart blob resume stage ${resumeStageId} does not match the requested organization or bytes.`,\n );\n }\n return resumed;\n }\n }\n\n const stage = await input.apiClient.initiateMultipartBlobStage({\n organizationId: input.organizationId,\n byteLength: input.byteLength,\n sha256: input.sha256,\n });\n if (!stage) {\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob stage initiation failed for ${input.byteLength.toLocaleString()} bytes.`,\n request: { method: \"POST\", path: \"/blobs/stages/multipart\" },\n }),\n );\n }\n if (\n stage.organizationId !== input.organizationId ||\n stage.byteLength !== input.byteLength ||\n stage.sha256 !== input.sha256\n ) {\n throw new Error(\n \"Multipart blob stage initiation response does not match its request.\",\n );\n }\n\n return isMultipartStageStatus(stage)\n ? stage\n : {\n ...stage,\n completed: false,\n };\n}\n\nfunction assertMultipartPlanLimits(encryption: BlobEncryptionPlan): void {\n if (encryption.partCount > MAX_BLOB_CHUNK_COUNT) {\n throw new Error(\n `Multipart blob upload would require ${encryption.partCount.toLocaleString()} parts; the maximum is ${MAX_BLOB_CHUNK_COUNT.toLocaleString()} fixed 5 MiB chunks.`,\n );\n }\n for (let index = 0; index < encryption.partCount; index += 1) {\n if (encryption.getPartByteLength(index) > MAX_MULTIPART_PART_BYTES) {\n throw new Error(\n `Encrypted blob part ${index + 1} exceeds the 5 GiB multipart part limit.`,\n );\n }\n }\n}\n\nexport async function stageMultipartBlobAttachment(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly encryption: BlobEncryptionPlan;\n readonly multipart: NonNullable<UploadDocumentAttachmentInput[\"multipart\"]>;\n readonly organizationId: string;\n readonly onMultipartProgress?: MultipartUploadProgressListener | undefined;\n readonly onStageResolved?: MultipartStageResolvedListener | undefined;\n readonly onStageUnavailable?:\n | ((stageId: string) => Promise<void>)\n | undefined;\n}): Promise<string | null> {\n if (input.multipart.partSize !== input.encryption.chunkSize) {\n throw new Error(\n \"Multipart part size must match the encryption chunk size exactly.\",\n );\n }\n assertMultipartPlanLimits(input.encryption);\n const status = await resolveMultipartStageStatus({\n apiClient: input.apiClient,\n byteLength: input.encryption.byteLength,\n multipart: input.multipart,\n organizationId: input.organizationId,\n onStageUnavailable: input.onStageUnavailable,\n sha256: input.encryption.sha256,\n });\n // Surface the stage id before any parts upload so the caller can persist it;\n // an upload interrupted mid-flight can then resume this same stage.\n await input.onStageResolved?.({\n partSize: input.encryption.chunkSize,\n stageId: status.stageId,\n });\n if (status.completed) {\n input.onMultipartProgress?.({\n bytesTotal: input.encryption.byteLength,\n bytesUploaded: input.encryption.byteLength,\n partsCompleted: input.encryption.partCount,\n partsTotal: input.encryption.partCount,\n });\n return status.stageId;\n }\n\n const { completeParts, progress, uploadTasks } = planMultipartParts({\n encryption: input.encryption,\n uploadedParts: uploadedPartsByPartNumber(status.uploadedParts),\n });\n\n const reportMultipartProgress = () => {\n input.onMultipartProgress?.({\n bytesTotal: progress.bytesTotal,\n bytesUploaded: progress.bytesUploaded,\n partsCompleted: progress.partsCompleted,\n partsTotal: progress.partsTotal,\n });\n };\n reportMultipartProgress();\n\n await uploadMultipartPartTasks({\n apiClient: input.apiClient,\n completeParts,\n concurrency: normalizeMultipartUploadConcurrency(\n input.multipart.uploadConcurrency,\n ),\n encryption: input.encryption,\n onPartUploaded: (task) => {\n progress.partsCompleted += 1;\n progress.bytesUploaded += task.byteLength;\n reportMultipartProgress();\n },\n stageId: status.stageId,\n tasks: uploadTasks,\n uploadId: status.uploadId,\n });\n const committedParts = completeParts.filter(isMultipartPartCommit);\n if (committedParts.length !== input.encryption.partCount) {\n throw new Error(\n `Multipart blob upload incomplete for stage ${status.stageId}: committed ${committedParts.length.toLocaleString()} of ${input.encryption.partCount.toLocaleString()} parts.`,\n );\n }\n\n const completed = await input.apiClient.completeMultipartBlobStage(\n status.stageId,\n {\n parts: committedParts,\n uploadId: status.uploadId,\n },\n );\n if (!completed) {\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob stage completion failed for stage ${status.stageId} with ${committedParts.length.toLocaleString()} committed parts.`,\n request: {\n method: \"POST\",\n path: `${multipartStagePath(status.stageId)}/complete`,\n },\n }),\n );\n }\n if (\n completed.organizationId !== input.organizationId ||\n completed.stageId !== status.stageId ||\n completed.byteLength !== input.encryption.byteLength ||\n completed.sha256 !== input.encryption.sha256\n ) {\n throw new Error(\n `Multipart blob stage ${status.stageId} completion response does not match its request.`,\n );\n }\n\n return completed.stageId;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"multipartUpload.js","sourceRoot":"","sources":["multipartUpload.ts"],"names":[],"mappings":"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","sourcesContent":["import { bytesToHex, MAX_BLOB_CHUNK_COUNT } from \"@tearleads/crypto\";\nimport type {\n InitiateMultipartBlobStageResponse,\n MultipartBlobStagePart,\n MultipartBlobStageStatusResponse,\n} from \"@tearleads/validators/response\";\nimport type { BlobEncryptionPlan } from \"../../data/documents/blob/shared/crypto\";\nimport type {\n BlobAttachmentApi,\n MultipartStageResolvedListener,\n MultipartUploadProgressListener,\n UploadDocumentAttachmentInput,\n} from \"../../data/documents/blob/shared/types\";\nimport {\n assertMultipartStageReplaceable,\n multipartApiFailureMessage,\n multipartStagePath,\n} from \"./multipartStageFailures\";\n\nconst DEFAULT_MULTIPART_UPLOAD_CONCURRENCY = 4;\nconst MAX_MULTIPART_UPLOAD_CONCURRENCY = 4;\nconst MAX_MULTIPART_PART_BYTES = 5 * 1024 * 1024 * 1024;\n\ninterface MultipartPartCommit {\n readonly etag: string;\n readonly partNumber: number;\n}\n\ninterface MultipartPartUploadTask {\n readonly byteLength: number;\n readonly partIndex: number;\n readonly partNumber: number;\n}\n\nfunction pathSegment(value: number | string): string {\n return encodeURIComponent(String(value));\n}\n\nfunction multipartPartPath(input: {\n readonly partNumber: number;\n readonly stageId: string;\n}): string {\n const basePath = `${multipartStagePath(input.stageId)}/parts/${pathSegment(input.partNumber)}`;\n return `${basePath}/bytes`;\n}\n\nasync function sha256BytesHex(bytes: Uint8Array<ArrayBuffer>): Promise<string> {\n return bytesToHex(\n new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes)),\n );\n}\n\nfunction normalizeMultipartUploadConcurrency(\n value: number | undefined,\n): number {\n if (value === undefined) {\n return DEFAULT_MULTIPART_UPLOAD_CONCURRENCY;\n }\n if (\n !Number.isInteger(value) ||\n value <= 0 ||\n value > MAX_MULTIPART_UPLOAD_CONCURRENCY\n ) {\n throw new Error(\n `Multipart blob upload concurrency must be between 1 and ${MAX_MULTIPART_UPLOAD_CONCURRENCY}`,\n );\n }\n\n return value;\n}\n\nfunction isMultipartPartCommit(\n part: MultipartPartCommit | undefined,\n): part is MultipartPartCommit {\n return part !== undefined;\n}\n\nasync function uploadMultipartPartTasks(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly completeParts: (MultipartPartCommit | undefined)[];\n readonly concurrency: number;\n readonly encryption: BlobEncryptionPlan;\n readonly onPartUploaded?: (task: MultipartPartUploadTask) => void;\n readonly stageId: string;\n readonly tasks: readonly MultipartPartUploadTask[];\n readonly uploadId: string;\n}): Promise<void> {\n let failed = false;\n let firstFailure: { readonly error: unknown } | undefined;\n let nextTaskIndex = 0;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const task = input.tasks[nextTaskIndex];\n nextTaskIndex += 1;\n if (!task) {\n return;\n }\n\n try {\n const uploaded = await uploadMultipartPartBytes({\n apiClient: input.apiClient,\n encryption: input.encryption,\n stageId: input.stageId,\n task,\n uploadId: input.uploadId,\n });\n if (!uploaded) {\n failed = true;\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob part ${task.partNumber} upload failed for stage ${input.stageId}.`,\n request: {\n method: \"PUT\",\n path: multipartPartPath({\n partNumber: task.partNumber,\n stageId: input.stageId,\n }),\n },\n }),\n );\n }\n\n input.completeParts[task.partIndex] = {\n etag: uploaded.part.etag,\n partNumber: task.partNumber,\n };\n input.onPartUploaded?.(task);\n } catch (error) {\n // Stop sibling workers from pulling new tasks and firing more uploads\n // once any part rejects. Keep this worker alive long enough for the\n // shared join below to drain uploads siblings already started.\n failed = true;\n firstFailure ??= { error };\n return;\n }\n }\n }\n\n const workerCount = Math.min(input.concurrency, input.tasks.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n if (firstFailure) {\n throw firstFailure.error;\n }\n}\n\nasync function uploadMultipartPartBytes(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly encryption: BlobEncryptionPlan;\n readonly stageId: string;\n readonly task: MultipartPartUploadTask;\n readonly uploadId: string;\n}): ReturnType<BlobAttachmentApi[\"uploadMultipartBlobPartBytes\"]> {\n const encryptedPartBytes = await input.encryption.encryptPart(\n input.task.partIndex,\n );\n if (encryptedPartBytes.byteLength !== input.task.byteLength) {\n throw new Error(\n `Encrypted blob part ${input.task.partNumber} byte length mismatch: expected ${input.task.byteLength.toLocaleString()}, received ${encryptedPartBytes.byteLength.toLocaleString()}.`,\n );\n }\n\n const uploaded = await input.apiClient.uploadMultipartBlobPartBytes(\n input.stageId,\n input.task.partNumber,\n {\n byteLength: encryptedPartBytes.byteLength,\n encryptedBytes: encryptedPartBytes,\n sha256: await sha256BytesHex(encryptedPartBytes),\n uploadId: input.uploadId,\n },\n );\n if (\n uploaded &&\n (uploaded.stageId !== input.stageId ||\n uploaded.uploadId !== input.uploadId ||\n uploaded.part.partNumber !== input.task.partNumber ||\n uploaded.part.byteLength !== input.task.byteLength)\n ) {\n throw new Error(\n `Multipart blob part ${input.task.partNumber} response does not match its request.`,\n );\n }\n return uploaded;\n}\n\nfunction uploadedPartsByPartNumber(\n parts: readonly MultipartBlobStagePart[],\n): ReadonlyMap<number, MultipartBlobStagePart> {\n return new Map(parts.map((part) => [part.partNumber, part]));\n}\n\nfunction isMultipartStageStatus(\n stage: InitiateMultipartBlobStageResponse | MultipartBlobStageStatusResponse,\n): stage is MultipartBlobStageStatusResponse {\n return \"completed\" in stage && typeof stage.completed === \"boolean\";\n}\n\ninterface MultipartUploadProgressState {\n bytesTotal: number;\n bytesUploaded: number;\n partsCompleted: number;\n partsTotal: number;\n}\n\ninterface MultipartPartPlan {\n completeParts: (MultipartPartCommit | undefined)[];\n progress: MultipartUploadProgressState;\n uploadTasks: MultipartPartUploadTask[];\n}\n\n/**\n * Plans encrypted part indices without materializing their bytes, reusing any\n * already-staged parts and seeding cumulative progress for resumed uploads.\n */\nfunction planMultipartParts(input: {\n encryption: BlobEncryptionPlan;\n uploadedParts: ReadonlyMap<number, MultipartBlobStagePart>;\n}): MultipartPartPlan {\n const { encryption, uploadedParts } = input;\n const completeParts: (MultipartPartCommit | undefined)[] = new Array(\n encryption.partCount,\n );\n const uploadTasks: MultipartPartUploadTask[] = [];\n const progress: MultipartUploadProgressState = {\n bytesTotal: 0,\n bytesUploaded: 0,\n partsCompleted: 0,\n partsTotal: encryption.partCount,\n };\n\n for (let partIndex = 0; partIndex < encryption.partCount; partIndex += 1) {\n const partNumber = partIndex + 1;\n const byteLength = encryption.getPartByteLength(partIndex);\n progress.bytesTotal += byteLength;\n const uploadedPart = uploadedParts.get(partNumber);\n if (uploadedPart?.byteLength === byteLength) {\n completeParts[partIndex] = {\n etag: uploadedPart.etag,\n partNumber,\n };\n progress.partsCompleted += 1;\n progress.bytesUploaded += byteLength;\n continue;\n }\n\n uploadTasks.push({\n byteLength,\n partIndex,\n partNumber,\n });\n }\n\n return { completeParts, progress, uploadTasks };\n}\n\nasync function resolveMultipartStageStatus(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly byteLength: number;\n readonly multipart: NonNullable<UploadDocumentAttachmentInput[\"multipart\"]>;\n readonly organizationId: string;\n readonly onStageUnavailable?:\n | ((stageId: string) => Promise<void>)\n | undefined;\n readonly sha256: string;\n}): Promise<MultipartBlobStageStatusResponse> {\n const resumeStageId = input.multipart.resumeStageId;\n if (resumeStageId) {\n const resumed = await input.apiClient.getMultipartBlobStage(resumeStageId);\n if (!resumed) {\n await assertMultipartStageReplaceable({\n apiClient: input.apiClient,\n onStageUnavailable: input.onStageUnavailable,\n stageId: resumeStageId,\n });\n }\n if (resumed && resumed.stageId !== resumeStageId) {\n throw new Error(\n `Multipart blob resume response does not match stage ${resumeStageId}.`,\n );\n }\n if (resumed) {\n // A successful lookup is authoritative: a metadata mismatch indicates\n // inconsistent persisted state or a bad server response, not proof that\n // the existing stage is safe to abandon.\n if (\n resumed.organizationId !== input.organizationId ||\n resumed.byteLength !== input.byteLength ||\n resumed.sha256 !== input.sha256\n ) {\n throw new Error(\n `Multipart blob resume stage ${resumeStageId} does not match the requested organization or bytes.`,\n );\n }\n return resumed;\n }\n }\n\n const stage = await input.apiClient.initiateMultipartBlobStage({\n organizationId: input.organizationId,\n byteLength: input.byteLength,\n sha256: input.sha256,\n });\n if (!stage) {\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob stage initiation failed for ${input.byteLength.toLocaleString()} bytes.`,\n request: { method: \"POST\", path: \"/blobs/stages/multipart\" },\n }),\n );\n }\n if (\n stage.organizationId !== input.organizationId ||\n stage.byteLength !== input.byteLength ||\n stage.sha256 !== input.sha256\n ) {\n throw new Error(\n \"Multipart blob stage initiation response does not match its request.\",\n );\n }\n\n return isMultipartStageStatus(stage)\n ? stage\n : {\n ...stage,\n completed: false,\n };\n}\n\nfunction assertMultipartPlanLimits(encryption: BlobEncryptionPlan): void {\n if (encryption.partCount > MAX_BLOB_CHUNK_COUNT) {\n throw new Error(\n `Multipart blob upload would require ${encryption.partCount.toLocaleString()} parts; the maximum is ${MAX_BLOB_CHUNK_COUNT.toLocaleString()} parts.`,\n );\n }\n for (let index = 0; index < encryption.partCount; index += 1) {\n if (encryption.getPartByteLength(index) > MAX_MULTIPART_PART_BYTES) {\n throw new Error(\n `Encrypted blob part ${index + 1} exceeds the 5 GiB multipart part limit.`,\n );\n }\n }\n}\n\nexport async function stageMultipartBlobAttachment(input: {\n readonly apiClient: BlobAttachmentApi;\n readonly encryption: BlobEncryptionPlan;\n readonly multipart: NonNullable<UploadDocumentAttachmentInput[\"multipart\"]>;\n readonly organizationId: string;\n readonly onMultipartProgress?: MultipartUploadProgressListener | undefined;\n readonly onStageResolved?: MultipartStageResolvedListener | undefined;\n readonly onStageUnavailable?:\n | ((stageId: string) => Promise<void>)\n | undefined;\n}): Promise<string | null> {\n if (input.multipart.partSize !== input.encryption.chunkSize) {\n throw new Error(\n \"Multipart part size must match the encryption chunk size exactly.\",\n );\n }\n assertMultipartPlanLimits(input.encryption);\n const status = await resolveMultipartStageStatus({\n apiClient: input.apiClient,\n byteLength: input.encryption.byteLength,\n multipart: input.multipart,\n organizationId: input.organizationId,\n onStageUnavailable: input.onStageUnavailable,\n sha256: input.encryption.sha256,\n });\n // Surface the stage id before any parts upload so the caller can persist it;\n // an upload interrupted mid-flight can then resume this same stage.\n await input.onStageResolved?.({\n partSize: input.encryption.chunkSize,\n stageId: status.stageId,\n });\n if (status.completed) {\n input.onMultipartProgress?.({\n bytesTotal: input.encryption.byteLength,\n bytesUploaded: input.encryption.byteLength,\n partsCompleted: input.encryption.partCount,\n partsTotal: input.encryption.partCount,\n });\n return status.stageId;\n }\n\n const { completeParts, progress, uploadTasks } = planMultipartParts({\n encryption: input.encryption,\n uploadedParts: uploadedPartsByPartNumber(status.uploadedParts),\n });\n\n const reportMultipartProgress = () => {\n input.onMultipartProgress?.({\n bytesTotal: progress.bytesTotal,\n bytesUploaded: progress.bytesUploaded,\n partsCompleted: progress.partsCompleted,\n partsTotal: progress.partsTotal,\n });\n };\n reportMultipartProgress();\n\n await uploadMultipartPartTasks({\n apiClient: input.apiClient,\n completeParts,\n concurrency: normalizeMultipartUploadConcurrency(\n input.multipart.uploadConcurrency,\n ),\n encryption: input.encryption,\n onPartUploaded: (task) => {\n progress.partsCompleted += 1;\n progress.bytesUploaded += task.byteLength;\n reportMultipartProgress();\n },\n stageId: status.stageId,\n tasks: uploadTasks,\n uploadId: status.uploadId,\n });\n const committedParts = completeParts.filter(isMultipartPartCommit);\n if (committedParts.length !== input.encryption.partCount) {\n throw new Error(\n `Multipart blob upload incomplete for stage ${status.stageId}: committed ${committedParts.length.toLocaleString()} of ${input.encryption.partCount.toLocaleString()} parts.`,\n );\n }\n\n const completed = await input.apiClient.completeMultipartBlobStage(\n status.stageId,\n {\n parts: committedParts,\n uploadId: status.uploadId,\n },\n );\n if (!completed) {\n throw new Error(\n multipartApiFailureMessage({\n apiClient: input.apiClient,\n fallback: `Multipart blob stage completion failed for stage ${status.stageId} with ${committedParts.length.toLocaleString()} committed parts.`,\n request: {\n method: \"POST\",\n path: `${multipartStagePath(status.stageId)}/complete`,\n },\n }),\n );\n }\n if (\n completed.organizationId !== input.organizationId ||\n completed.stageId !== status.stageId ||\n completed.byteLength !== input.encryption.byteLength ||\n completed.sha256 !== input.encryption.sha256\n ) {\n throw new Error(\n `Multipart blob stage ${status.stageId} completion response does not match its request.`,\n );\n }\n\n return completed.stageId;\n}\n"]}
|