@tearleads/client-sdk 0.1.26 → 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.
@@ -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, blobChunkPlaintextByteLength, bytesToHex, CONTENT_RECORD_ENCRYPTION_SUITE, computeBlobChunkCount, computeBlobEncryptedByteLength, computeContentRecordNonceDomainHash, createAesGcmIv, createIncrementalSha256, deriveBlobChunkIv, encodeBlobEnvelopeV2Header, joinBlobPartBytes, normalizeBlobChunkSize, } from "../../../../internal/crypto/index.js";
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 planPartByteLength(context, index) {
112
- assertPartIndex(index, context.partCount);
113
- const plaintextLength = blobChunkPlaintextByteLength({
107
+ function createBlobEncryptionPlan(input) {
108
+ const { context, sha256, snapshot, source } = input;
109
+ const byteLength = computeBlobEncryptedByteLength({
114
110
  chunkCount: context.partCount,
115
- chunkIndex: index,
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: computeBlobEncryptedByteLength({
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
- getPartByteLength(index) {
138
- return planPartByteLength(context, index);
139
- },
140
- async encryptPart(index) {
141
- assertPartIndex(index, context.partCount);
142
- const plaintext = await readVerifiedBlobSourceChunk({
143
- chunkIndex: index,
144
- snapshot,
145
- source,
146
- });
147
- const ciphertext = await encryptBlobChunk(context, index, plaintext);
148
- return joinBlobPartBytes(index === 0 ? context.headerBytes : null, ciphertext);
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 of plaintext per independently encrypted multipart chunk. */
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 of plaintext per independently encrypted multipart chunk. */\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"]}
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"]}
@@ -306,7 +306,7 @@ export declare const uploadMultipartBlobPartBytesOperation: Omit<{
306
306
  }>>;
307
307
  };
308
308
  readonly runtimeRefinements: readonly [{
309
- readonly description: "Declared multipart part byte length is at most 104857600";
309
+ readonly description: "Declared multipart part byte length is at most 33554432";
310
310
  readonly id: "blobs.multipartPart.declaredByteLengthMax";
311
311
  }];
312
312
  }, "failureResponses" | "failureStatuses"> & {
@@ -1,2 +1,7 @@
1
- /** Protocol ceiling for one buffered multipart blob part. */
1
+ /**
2
+ * Protocol ceiling for one buffered multipart blob part. The API buffers a
3
+ * part before uploading it, and the Workers isolate that serves uploads has
4
+ * 128 MB of memory, so the ceiling leaves room for the copy the digest check
5
+ * makes.
6
+ */
2
7
  export declare const MAX_MULTIPART_BLOB_PART_BYTES: number;
@@ -1,3 +1,8 @@
1
- /** Protocol ceiling for one buffered multipart blob part. */
2
- export const MAX_MULTIPART_BLOB_PART_BYTES = 100 * 1024 * 1024;
1
+ /**
2
+ * Protocol ceiling for one buffered multipart blob part. The API buffers a
3
+ * part before uploading it, and the Workers isolate that serves uploads has
4
+ * 128 MB of memory, so the ceiling leaves room for the copy the digest check
5
+ * makes.
6
+ */
7
+ export const MAX_MULTIPART_BLOB_PART_BYTES = 32 * 1024 * 1024;
3
8
  //# sourceMappingURL=blob.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"blob.js","sourceRoot":"","sources":["blob.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC","sourcesContent":["/** Protocol ceiling for one buffered multipart blob part. */\nexport const MAX_MULTIPART_BLOB_PART_BYTES = 100 * 1024 * 1024;\n"]}
1
+ {"version":3,"file":"blob.js","sourceRoot":"","sources":["blob.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC","sourcesContent":["/**\n * Protocol ceiling for one buffered multipart blob part. The API buffers a\n * part before uploading it, and the Workers isolate that serves uploads has\n * 128 MB of memory, so the ceiling leaves room for the copy the digest check\n * makes.\n */\nexport const MAX_MULTIPART_BLOB_PART_BYTES = 32 * 1024 * 1024;\n"]}
@@ -74,7 +74,7 @@ export declare const MAX_INLINE_CONTAINER_REKEYS = 16;
74
74
  * exceeding it: beyond the cap the remainder repairs lazily.
75
75
  *
76
76
  * A sealed keyring is 64 bytes per retained epoch, so a full batch stays far
77
- * inside the API's 100 MiB request limit unless its containers average tens of
77
+ * inside the API's 32 MiB request limit unless its containers average tens of
78
78
  * thousands of rotations each, which `MAX_CONTAINER_KEY_EPOCH` already treats
79
79
  * as a runaway rather than a use.
80
80
  */
@@ -66,7 +66,7 @@ export const MAX_INLINE_CONTAINER_REKEYS = 16;
66
66
  * exceeding it: beyond the cap the remainder repairs lazily.
67
67
  *
68
68
  * A sealed keyring is 64 bytes per retained epoch, so a full batch stays far
69
- * inside the API's 100 MiB request limit unless its containers average tens of
69
+ * inside the API's 32 MiB request limit unless its containers average tens of
70
70
  * thousands of rotations each, which `MAX_CONTAINER_KEY_EPOCH` already treats
71
71
  * as a runaway rather than a use.
72
72
  */
@@ -1 +1 @@
1
- {"version":3,"file":"containerKekKeyringWire.js","sourceRoot":"","sources":["containerKekKeyringWire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAC7C,MAAM,CAAC,MAAM,yCAAyC,GAAG,CAAC,CAAC;AAC3D,MAAM,CAAC,MAAM,wCAAwC,GAAG,EAAE,CAAC;AAC3D,MAAM,CAAC,MAAM,gDAAgD,GAAG,EAAE,CAAC;AAEnE,MAAM,UAAU,8BAA8B,CAAC,QAAgB;IAC7D,OAAO,CACL,yCAAyC;QACzC,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,wCAAwC;QACzD,gDAAgD,CACjD,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,MAAM,8CAA8C,GAAG,YAAY,CACxE,8BAA8B,CAAC,uBAAuB,CAAC,CACxD,CAAC;AAEF,MAAM,wBAAwB,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;AAClD,MAAM,gCAAgC,GAAG,0BAA0B,CACjE,CAAC,CAAC,MAAM,CAAS,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,EACxC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAC7B,CAAC;AAEF,MAAM,CAAC,MAAM,mCAAmC,GAAG,gBAAgB,CAAC;IAClE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC/B,EAAE,EAAE,mBAAmB,CAAC,wBAAwB,CAAC;IACjD,MAAM,EAAE,mBAAmB,CAAC,8CAA8C,CAAC;IAC3E,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,OAAO,EAAE,gCAAgC;CAC1C,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,+BAA+B,CAC7C,KAAc;IAEd,OAAO,mCAAmC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,EAAE,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC","sourcesContent":["import { z } from \"zod\";\nimport { registerJsonSchemaFragment } from \"../jsonSchema\";\nimport { boundedStringSchema, loosePlainObject } from \"../schema\";\n\n/**\n * Shared wire arithmetic for sealed container KEK keyrings.\n *\n * These numbers define the exact-length equality every layer enforces: the\n * server rejects a rotation whose sealed keyring ciphertext differs from\n * `sealedContainerKekKeyringBytes(keyEpoch)`, and response guards reject any\n * blob over the `MAX_CONTAINER_KEY_EPOCH` ceiling before cryptographic work.\n * `@tearleads/crypto` consumes these same constants for seal/open, so the\n * equation has one definition. The overhead constant is the AES-GCM tag.\n */\nexport const MAX_CONTAINER_KEY_EPOCH = 65536;\nexport const SEALED_CONTAINER_KEK_KEYRING_HEADER_BYTES = 8;\nexport const SEALED_CONTAINER_KEK_KEYRING_ENTRY_BYTES = 64;\nexport const SEALED_CONTAINER_KEK_KEYRING_AEAD_OVERHEAD_BYTES = 16;\n\nexport function sealedContainerKekKeyringBytes(keyEpoch: number): number {\n return (\n SEALED_CONTAINER_KEK_KEYRING_HEADER_BYTES +\n (keyEpoch - 1) * SEALED_CONTAINER_KEK_KEYRING_ENTRY_BYTES +\n SEALED_CONTAINER_KEK_KEYRING_AEAD_OVERHEAD_BYTES\n );\n}\n\nfunction base64Length(bytes: number): number {\n return Math.ceil(bytes / 3) * 4;\n}\n\n/** Parse ceiling: the base64 length of a keyring sealed at the epoch cap. */\nexport const MAX_SEALED_CONTAINER_KEK_KEYRING_BASE64_LENGTH = base64Length(\n sealedContainerKekKeyringBytes(MAX_CONTAINER_KEY_EPOCH),\n);\n\nconst AES_GCM_IV_BASE64_LENGTH = base64Length(12);\nconst ContainerKekKeyringVersionSchema = registerJsonSchemaFragment(\n z.custom<number>((value) => value === 1),\n { const: 1, type: \"number\" },\n);\n\nexport const ContainerKekKeyringWireRecordSchema = loosePlainObject({\n containerId: z.string(),\n containerKeyEpochId: z.string(),\n iv: boundedStringSchema(AES_GCM_IV_BASE64_LENGTH),\n sealed: boundedStringSchema(MAX_SEALED_CONTAINER_KEK_KEYRING_BASE64_LENGTH),\n sealingSuite: z.string(),\n version: ContainerKekKeyringVersionSchema,\n});\n\n/**\n * Structural bound applied before any allocation-heavy or cryptographic work.\n * Content verification (exact length for the epoch, AEAD, per-entry\n * commitments) happens in `@tearleads/crypto`; this guard only bounds a\n * hostile payload.\n */\nexport function isContainerKekKeyringWireRecord(\n value: unknown,\n): value is Record<string, unknown> {\n return ContainerKekKeyringWireRecordSchema.safeParse(value).success;\n}\n\n/**\n * Maximum epochs one kek-log page may serve. Recovery walks the log from the\n * newest page backward, so the page bound — not the container's lifetime\n * rotation count — determines a response's size.\n */\nexport const CONTAINER_KEK_LOG_PAGE_LIMIT = 256;\n\n/**\n * Maximum signed container rekeys one document/blob write may carry inline.\n * Each rotation ships a keyring, which is O(its epoch) bytes, so an unbounded\n * batch is an unbounded request body.\n */\nexport const MAX_INLINE_CONTAINER_REKEYS = 16;\n\n/**\n * Maximum descendant rekeys one container rotation may carry. A rotation must\n * re-key, in its own transaction, every descendant that sits above a directly\n * granted container, or a writer granted only further down would wait on\n * another device's write. The set is empty for almost every rotation, since\n * child containers carry no direct grant unless shared, so this bounds the\n * body rather than shaping ordinary traffic. A rotation is never refused for\n * exceeding it: beyond the cap the remainder repairs lazily.\n *\n * A sealed keyring is 64 bytes per retained epoch, so a full batch stays far\n * inside the API's 100 MiB request limit unless its containers average tens of\n * thousands of rotations each, which `MAX_CONTAINER_KEY_EPOCH` already treats\n * as a runaway rather than a use.\n */\nexport const MAX_ROTATION_CONTAINER_REKEYS = 64;\n\n/**\n * Maximum recipient envelopes one kek-log epoch may serve, applied PER EPOCH\n * so no epoch can be starved by another's width — a starved epoch is\n * indistinguishable from an unaddressed one and would surface as a false\n * `no-addressed-envelope` recovery failure.\n *\n * Recovery needs ONE openable anchor per epoch, not every envelope, and the\n * ranking puts the requester's own direct wrap first, so this cap never costs\n * a usable anchor. Together with the epoch page limit it bounds the response\n * at page x cap rows regardless of how many recipients a container has\n * accumulated.\n */\nexport const CONTAINER_KEK_WRAPS_PER_EPOCH_LIMIT = 16;\n\n/**\n * Maximum authorized principals a single kek-log read may scope wraps to.\n * A requester's principal set has no intrinsic ceiling and each entry adds a\n * clause, and bind parameters, to the statement.\n *\n * The requester's direct user envelope and their parent-container envelopes\n * are scoped outside this cap and rank ahead of principal wraps, so the bound\n * never costs the anchors openable without principal-policy state. Opening a\n * principal wrap needs that state regardless (see issue #1941).\n */\nexport const CONTAINER_KEK_LOG_PRINCIPAL_SCOPE_LIMIT = 64;\n"]}
1
+ {"version":3,"file":"containerKekKeyringWire.js","sourceRoot":"","sources":["containerKekKeyringWire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAC7C,MAAM,CAAC,MAAM,yCAAyC,GAAG,CAAC,CAAC;AAC3D,MAAM,CAAC,MAAM,wCAAwC,GAAG,EAAE,CAAC;AAC3D,MAAM,CAAC,MAAM,gDAAgD,GAAG,EAAE,CAAC;AAEnE,MAAM,UAAU,8BAA8B,CAAC,QAAgB;IAC7D,OAAO,CACL,yCAAyC;QACzC,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,wCAAwC;QACzD,gDAAgD,CACjD,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,MAAM,8CAA8C,GAAG,YAAY,CACxE,8BAA8B,CAAC,uBAAuB,CAAC,CACxD,CAAC;AAEF,MAAM,wBAAwB,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;AAClD,MAAM,gCAAgC,GAAG,0BAA0B,CACjE,CAAC,CAAC,MAAM,CAAS,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,EACxC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAC7B,CAAC;AAEF,MAAM,CAAC,MAAM,mCAAmC,GAAG,gBAAgB,CAAC;IAClE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC/B,EAAE,EAAE,mBAAmB,CAAC,wBAAwB,CAAC;IACjD,MAAM,EAAE,mBAAmB,CAAC,8CAA8C,CAAC;IAC3E,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,OAAO,EAAE,gCAAgC;CAC1C,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,+BAA+B,CAC7C,KAAc;IAEd,OAAO,mCAAmC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,EAAE,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC","sourcesContent":["import { z } from \"zod\";\nimport { registerJsonSchemaFragment } from \"../jsonSchema\";\nimport { boundedStringSchema, loosePlainObject } from \"../schema\";\n\n/**\n * Shared wire arithmetic for sealed container KEK keyrings.\n *\n * These numbers define the exact-length equality every layer enforces: the\n * server rejects a rotation whose sealed keyring ciphertext differs from\n * `sealedContainerKekKeyringBytes(keyEpoch)`, and response guards reject any\n * blob over the `MAX_CONTAINER_KEY_EPOCH` ceiling before cryptographic work.\n * `@tearleads/crypto` consumes these same constants for seal/open, so the\n * equation has one definition. The overhead constant is the AES-GCM tag.\n */\nexport const MAX_CONTAINER_KEY_EPOCH = 65536;\nexport const SEALED_CONTAINER_KEK_KEYRING_HEADER_BYTES = 8;\nexport const SEALED_CONTAINER_KEK_KEYRING_ENTRY_BYTES = 64;\nexport const SEALED_CONTAINER_KEK_KEYRING_AEAD_OVERHEAD_BYTES = 16;\n\nexport function sealedContainerKekKeyringBytes(keyEpoch: number): number {\n return (\n SEALED_CONTAINER_KEK_KEYRING_HEADER_BYTES +\n (keyEpoch - 1) * SEALED_CONTAINER_KEK_KEYRING_ENTRY_BYTES +\n SEALED_CONTAINER_KEK_KEYRING_AEAD_OVERHEAD_BYTES\n );\n}\n\nfunction base64Length(bytes: number): number {\n return Math.ceil(bytes / 3) * 4;\n}\n\n/** Parse ceiling: the base64 length of a keyring sealed at the epoch cap. */\nexport const MAX_SEALED_CONTAINER_KEK_KEYRING_BASE64_LENGTH = base64Length(\n sealedContainerKekKeyringBytes(MAX_CONTAINER_KEY_EPOCH),\n);\n\nconst AES_GCM_IV_BASE64_LENGTH = base64Length(12);\nconst ContainerKekKeyringVersionSchema = registerJsonSchemaFragment(\n z.custom<number>((value) => value === 1),\n { const: 1, type: \"number\" },\n);\n\nexport const ContainerKekKeyringWireRecordSchema = loosePlainObject({\n containerId: z.string(),\n containerKeyEpochId: z.string(),\n iv: boundedStringSchema(AES_GCM_IV_BASE64_LENGTH),\n sealed: boundedStringSchema(MAX_SEALED_CONTAINER_KEK_KEYRING_BASE64_LENGTH),\n sealingSuite: z.string(),\n version: ContainerKekKeyringVersionSchema,\n});\n\n/**\n * Structural bound applied before any allocation-heavy or cryptographic work.\n * Content verification (exact length for the epoch, AEAD, per-entry\n * commitments) happens in `@tearleads/crypto`; this guard only bounds a\n * hostile payload.\n */\nexport function isContainerKekKeyringWireRecord(\n value: unknown,\n): value is Record<string, unknown> {\n return ContainerKekKeyringWireRecordSchema.safeParse(value).success;\n}\n\n/**\n * Maximum epochs one kek-log page may serve. Recovery walks the log from the\n * newest page backward, so the page bound — not the container's lifetime\n * rotation count — determines a response's size.\n */\nexport const CONTAINER_KEK_LOG_PAGE_LIMIT = 256;\n\n/**\n * Maximum signed container rekeys one document/blob write may carry inline.\n * Each rotation ships a keyring, which is O(its epoch) bytes, so an unbounded\n * batch is an unbounded request body.\n */\nexport const MAX_INLINE_CONTAINER_REKEYS = 16;\n\n/**\n * Maximum descendant rekeys one container rotation may carry. A rotation must\n * re-key, in its own transaction, every descendant that sits above a directly\n * granted container, or a writer granted only further down would wait on\n * another device's write. The set is empty for almost every rotation, since\n * child containers carry no direct grant unless shared, so this bounds the\n * body rather than shaping ordinary traffic. A rotation is never refused for\n * exceeding it: beyond the cap the remainder repairs lazily.\n *\n * A sealed keyring is 64 bytes per retained epoch, so a full batch stays far\n * inside the API's 32 MiB request limit unless its containers average tens of\n * thousands of rotations each, which `MAX_CONTAINER_KEY_EPOCH` already treats\n * as a runaway rather than a use.\n */\nexport const MAX_ROTATION_CONTAINER_REKEYS = 64;\n\n/**\n * Maximum recipient envelopes one kek-log epoch may serve, applied PER EPOCH\n * so no epoch can be starved by another's width — a starved epoch is\n * indistinguishable from an unaddressed one and would surface as a false\n * `no-addressed-envelope` recovery failure.\n *\n * Recovery needs ONE openable anchor per epoch, not every envelope, and the\n * ranking puts the requester's own direct wrap first, so this cap never costs\n * a usable anchor. Together with the epoch page limit it bounds the response\n * at page x cap rows regardless of how many recipients a container has\n * accumulated.\n */\nexport const CONTAINER_KEK_WRAPS_PER_EPOCH_LIMIT = 16;\n\n/**\n * Maximum authorized principals a single kek-log read may scope wraps to.\n * A requester's principal set has no intrinsic ceiling and each entry adds a\n * clause, and bind parameters, to the statement.\n *\n * The requester's direct user envelope and their parent-container envelopes\n * are scoped outside this cap and rank ahead of principal wraps, so the bound\n * never costs the anchors openable without principal-policy state. Opening a\n * principal wrap needs that state regardless (see issue #1941).\n */\nexport const CONTAINER_KEK_LOG_PRINCIPAL_SCOPE_LIMIT = 64;\n"]}
@@ -1,10 +1,12 @@
1
1
  import { MAX_CONTAINER_PATH_LENGTH } from "./containerLimits.js";
2
2
  export const MAX_DOCUMENT_SYNC_REQUEST_BYTES = 16 * 1024 * 1024;
3
3
  // A response page must be able to carry the largest accepted atomic rotation
4
- // baseline (the 100 MiB route ceiling) plus its signed metadata and key-bundle
5
- // envelope. Ordinary sync updates remain constrained by the smaller request
6
- // ceiling above.
7
- export const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES = 128 * 1024 * 1024;
4
+ // baseline (the 32 MiB route ceiling) plus its signed metadata and key-bundle
5
+ // envelope, and no more: the API materializes a page and then serializes it,
6
+ // so the page and its JSON copy must both fit the 128 MB Workers isolate.
7
+ // Ordinary sync updates remain constrained by the smaller request ceiling
8
+ // above.
9
+ export const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES = 48 * 1024 * 1024;
8
10
  export const MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES = 16 * 1024 * 1024;
9
11
  export const MAX_DOCUMENT_SYNC_RESPONSE_UPDATE_PAGE_BYTES = MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES -
10
12
  MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES;
@@ -1 +1 @@
1
- {"version":3,"file":"documentSyncLimits.js","sourceRoot":"","sources":["documentSyncLimits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhE,6EAA6E;AAC7E,+EAA+E;AAC/E,4EAA4E;AAC5E,iBAAiB;AACjB,MAAM,CAAC,MAAM,qCAAqC,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AACvE,MAAM,CAAC,MAAM,yCAAyC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAC1E,MAAM,CAAC,MAAM,4CAA4C,GACvD,qCAAqC;IACrC,yCAAyC,CAAC;AAC5C,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC;AAC1D,MAAM,CAAC,MAAM,oCAAoC,GAAG,GAAG,CAAC;AAExD,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,CAAC;AACrD,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qCAAqC,GAAG,EAAE,CAAC;AACxD,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,kBAAkB;AAClB,MAAM,CAAC,MAAM,yCAAyC,GACpD,qCAAqC,GAAG,yBAAyB,CAAC;AACpE,8EAA8E;AAC9E,8EAA8E;AAC9E,qEAAqE;AACrE,MAAM,CAAC,MAAM,qCAAqC,GAAG,EAAE,CAAC","sourcesContent":["import { MAX_CONTAINER_PATH_LENGTH } from \"./containerLimits\";\n\nexport const MAX_DOCUMENT_SYNC_REQUEST_BYTES = 16 * 1024 * 1024;\n\n// A response page must be able to carry the largest accepted atomic rotation\n// baseline (the 100 MiB route ceiling) plus its signed metadata and key-bundle\n// envelope. Ordinary sync updates remain constrained by the smaller request\n// ceiling above.\nexport const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES = 128 * 1024 * 1024;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES = 16 * 1024 * 1024;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_UPDATE_PAGE_BYTES =\n MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES -\n MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_UPDATES = 64;\nexport const MAX_DOCUMENT_SYNC_PULL_CURSOR_LENGTH = 512;\n\nexport const MAX_DOCUMENT_SYNC_OUTGOING_UPDATES = 64;\n// Individual updates may use the request's remaining budget. The SDK applies\n// the exact serialized-byte ceiling after encryption and trims the batch; a\n// smaller estimate here would permanently strand an otherwise valid update.\nexport const MAX_DOCUMENT_SYNC_AUTHORIZATION_PATHS = 64;\n// Derive the aggregate ceiling from the structural limits so every valid link\n// set remains syncable after later container moves deepen its paths. This is\n// still a finite verification bound; the serialized request has a separate\n// 16 MiB ceiling.\nexport const MAX_DOCUMENT_SYNC_AUTHORIZATION_PATH_REFS =\n MAX_DOCUMENT_SYNC_AUTHORIZATION_PATHS * MAX_CONTAINER_PATH_LENGTH;\n// Flag-day product maximum for every document content-key bundle. Create/link\n// request validation and the client link preflight keep a greenfield database\n// from ever producing a document that cannot fit this sync contract.\nexport const MAX_DOCUMENT_SYNC_CONTENT_KEY_TARGETS = 64;\n"]}
1
+ {"version":3,"file":"documentSyncLimits.js","sourceRoot":"","sources":["documentSyncLimits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhE,6EAA6E;AAC7E,8EAA8E;AAC9E,6EAA6E;AAC7E,0EAA0E;AAC1E,0EAA0E;AAC1E,SAAS;AACT,MAAM,CAAC,MAAM,qCAAqC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACtE,MAAM,CAAC,MAAM,yCAAyC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAC1E,MAAM,CAAC,MAAM,4CAA4C,GACvD,qCAAqC;IACrC,yCAAyC,CAAC;AAC5C,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC;AAC1D,MAAM,CAAC,MAAM,oCAAoC,GAAG,GAAG,CAAC;AAExD,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,CAAC;AACrD,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qCAAqC,GAAG,EAAE,CAAC;AACxD,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,kBAAkB;AAClB,MAAM,CAAC,MAAM,yCAAyC,GACpD,qCAAqC,GAAG,yBAAyB,CAAC;AACpE,8EAA8E;AAC9E,8EAA8E;AAC9E,qEAAqE;AACrE,MAAM,CAAC,MAAM,qCAAqC,GAAG,EAAE,CAAC","sourcesContent":["import { MAX_CONTAINER_PATH_LENGTH } from \"./containerLimits\";\n\nexport const MAX_DOCUMENT_SYNC_REQUEST_BYTES = 16 * 1024 * 1024;\n\n// A response page must be able to carry the largest accepted atomic rotation\n// baseline (the 32 MiB route ceiling) plus its signed metadata and key-bundle\n// envelope, and no more: the API materializes a page and then serializes it,\n// so the page and its JSON copy must both fit the 128 MB Workers isolate.\n// Ordinary sync updates remain constrained by the smaller request ceiling\n// above.\nexport const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES = 48 * 1024 * 1024;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES = 16 * 1024 * 1024;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_UPDATE_PAGE_BYTES =\n MAX_DOCUMENT_SYNC_RESPONSE_PAGE_BYTES -\n MAX_DOCUMENT_SYNC_RESPONSE_ENVELOPE_BYTES;\nexport const MAX_DOCUMENT_SYNC_RESPONSE_PAGE_UPDATES = 64;\nexport const MAX_DOCUMENT_SYNC_PULL_CURSOR_LENGTH = 512;\n\nexport const MAX_DOCUMENT_SYNC_OUTGOING_UPDATES = 64;\n// Individual updates may use the request's remaining budget. The SDK applies\n// the exact serialized-byte ceiling after encryption and trims the batch; a\n// smaller estimate here would permanently strand an otherwise valid update.\nexport const MAX_DOCUMENT_SYNC_AUTHORIZATION_PATHS = 64;\n// Derive the aggregate ceiling from the structural limits so every valid link\n// set remains syncable after later container moves deepen its paths. This is\n// still a finite verification bound; the serialized request has a separate\n// 16 MiB ceiling.\nexport const MAX_DOCUMENT_SYNC_AUTHORIZATION_PATH_REFS =\n MAX_DOCUMENT_SYNC_AUTHORIZATION_PATHS * MAX_CONTAINER_PATH_LENGTH;\n// Flag-day product maximum for every document content-key bundle. Create/link\n// request validation and the client link preflight keep a greenfield database\n// from ever producing a document that cannot fit this sync contract.\nexport const MAX_DOCUMENT_SYNC_CONTENT_KEY_TARGETS = 64;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tearleads/client-sdk",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "React-free Tearleads client runtime: local SQLite persistence, keys, sync, encrypted blobs, and domain workflows.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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()} fixed 5 MiB chunks.`);
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"]}