Overview
Provider-neutral encrypted large-object transfer for AbsoluteJS. It splits a known-length source into bounded, independently authenticated records, writes only ciphertext to an untrusted store, and downloads into a transactional sink.
@absolutejs/secure-transferv0.3.0betaPlatform & InfraProvider-neutral encrypted, record-oriented large-object transfer for AbsoluteJS.
bun add @absolutejs/secure-transferProvider-neutral encrypted large-object transfer for AbsoluteJS. It splits a known-length source into bounded, independently authenticated records, writes only ciphertext to an untrusted store, and downloads into a transactional sink.
downloadRange() authenticates every complete encrypted record covering the requested interval, then passes only the selected plaintext bytes to a transactional range sink. The range is [start, endExclusive) and must be non-empty and within the descriptor's declared plaintext size.
This proves the authenticity and position of the requested records against the descriptor. It intentionally does not fetch or prove the current availability of records outside the range.
Configure a trusted SecureTransferRevocationStore, then create the durable tombstone before attempting ciphertext cleanup:
Recipients must authenticate the E2EE sender, authorize that device to revoke the attachment, strictly decode the notice, and only then call applyRevocation(). The notice is bound to the exact descriptor by a SHA-256 hash. Downloads consult trusted policy state before and throughout retrieval and fail closed when that store errors. Keep tombstones at least through the descriptor expiry; ciphertextRemoved: false means cleanup must be retried even though cooperating clients already block the transfer.
Revocation is not retroactive cryptographic erasure. A bearer capability, ciphertext, or plaintext already copied by a recipient cannot be recalled. After an MLS member removal, use a fresh capability for replacement content and deliver its descriptor only in the new epoch; removal protects future epoch traffic, not secrets the former member already received. This follows the epoch and member-removal model in RFC 9420. Where a deployment uses cryptographic erase for storage cleanup, follow the key sanitization program in NIST SP 800-88 Rev. 2; that still does not sanitize independently held recipient copies.
An MLS removal changes who receives future epoch secrets, but it does not change an attachment capability already delivered in an earlier epoch. For retained attachments that a removed device must no longer fetch, upload a new encrypted copy with a fresh capability and send the replacement only in the new epoch:
The secure-messaging send persists advanced MLS state and its retryable outbox entry before returning, even when delivery is queued. Only then should the sender activate supersession and remove old ciphertext. A recipient strictly decodes the same payload and passes the message's authenticated context to activateReplacement(). Activation verifies the old descriptor hash, fresh transfer ID and capability, attachment, conversation, sender, purpose, and exact epoch. Its persistReplacement callback runs before the old tombstone is installed and must be durable, idempotent, and protect the bearer descriptor.
If activation crashes after descriptor persistence but before supersession, retry the same payload. This temporarily leaves the old transfer usable instead of stranding the replacement. Expiry sweeps clean abandoned new ciphertext when a message can never be durably queued. Rate-limit rotations and prioritize only attachments that remain useful; membership churn must not become an unbounded re-encryption denial of service.
Configure a SecureTransferReceiptProtector and SecureTransferProtectedReceiptStore, then persist the initial protected receipt before reading the source:
Receipts contain the transfer's bearer decryption capability. Core passes only protected opaque bytes to receipt storage and binds protection to receiptId. Use an authenticated protector backed by a key that is separate from object storage credentials. Never implement the protector as plaintext or reversible encoding.
Receipt stores must implement atomic lease acquisition and compare-and-swap updates. Core checkpoints phase: "sealing" before invoking record encryption. If a crash occurs after ciphertext storage, resume authenticates that ciphertext against the source before advancing. If encryption might have happened but no ciphertext is durable, SecureTransferResumeUnsafeError requires a new transfer and capability rather than risking nonce reuse. Receipt adapters should implement SecureTransferProtectedReceiptLifecycleStore; run sweepExpiredReceipts() with its returned cursor until truncated is false.
The descriptor contains the decryption capability and sensitive metadata. It is plaintext until the caller protects it with @absolutejs/secure-messaging or an E2EE envelope. Never place it in object metadata, logs, URLs, push payloads, or a normal chat message.
Storage receives opaque transfer IDs, record indexes, ciphertext sizes, and
expiry. It does not receive filenames, media types, conversation IDs, or keys.
Every record is bound to the transfer, attachment, conversation, sender,
position, total count, expected plaintext size, final-record marker, and expiry.
Record creation is create-only. A collision must never overwrite ciphertext.
Missing, reordered, substituted, duplicated, truncated, and extended records
fail authentication or descriptor validation.
Downloads target a staging sink. commit() occurs only after every record is
authenticated; failure calls abort() so partial plaintext is not mistaken for a complete file.
Range downloads preserve the same staging rule but authenticate only records
intersecting the requested byte interval.
Revocation stores are trusted authorization state and should use credentials
and retention controls distinct from untrusted ciphertext storage.
Version 0.2.0 provides upload, strict descriptor, receipt, and revocation encoding, full and byte-range authenticated download, resumable crash recovery, transactional sinks, honest future-fetch revocation, cleanup, and provider/store contracts. Concrete local and S3/R2 storage adapters live in secure-transfer-adapters. Version 0.3.0 adds epoch-bound fresh-capability replacement and staged supersession after MLS membership changes.
Outcomes
Provider-neutral encrypted large-object transfer for AbsoluteJS. It splits a known-length source into bounded, independently authenticated records, writes only ciphertext to an untrusted store, and downloads into a transactional sink.
downloadRange() authenticates every complete encrypted record covering the requested interval, then passes only the selected plaintext bytes to a transactional range sink. The range is [start, endExclusive) and must be non-empty and within the descriptor's declared plaintext size.
Configure a trusted SecureTransferRevocationStore, then create the durable tombstone before attempting ciphertext cleanup:
Hardening checklist
Follow in order
# @absolutejs/secure-transfer
const transfer = createSecureTransferClient({
cryptoProvider,
store,
policy: {
maximumAttachmentBytes: 1024 ** 4,
maximumDescriptorBytes: 16 * 1024,
maximumFutureSkewMs: 300_000,
maximumMetadataBytes: 4 * 1024,
maximumRecordPlaintextBytes: 1024 * 1024,
maximumRecords: 1_048_576,
maximumTtlMs: 7 * 24 * 60 * 60 * 1000,
},
});
const descriptor = await transfer.upload({
attachmentId: crypto.randomUUID(),
body: file.stream(),
byteLength: file.size,
contentType: file.type,
conversationId,
expiresAt: Date.now() + 86_400_000,
fileName: file.name,
senderDeviceId,
});
await messaging.send({
conversationId,
id: crypto.randomUUID(),
plaintext: encodeSecureTransferDescriptor(descriptor),
purpose: "secure-transfer.descriptor",
ttlMs: 86_400_000,
});downloadRange() authenticates every complete encrypted record covering the requested interval, then passes only the selected plaintext bytes to a transactional range sink. The range is [start, endExclusive) and must be non-empty and within the descriptor's declared plaintext size.
await transfer.downloadRange(
descriptor,
{ start: 1_048_576, endExclusive: 2_097_152 },
rangeSink,
);Configure a trusted SecureTransferRevocationStore, then create the durable tombstone before attempting ciphertext cleanup:
const { revocation, ciphertextRemoved } = await transfer.revoke({
descriptor,
reason: "member-removed",
revokerDeviceId,
});
await messaging.send({
conversationId,
id: crypto.randomUUID(),
plaintext: encodeSecureTransferRevocation(revocation),
purpose: "secure-transfer.revocation",
ttlMs,
});Supported entry points declared by this project’s package manifest. Internal dist paths are not part of the package contract.
Public package entry point declared in package.json.
Scripts declared by this project’s package manifest.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
const createSecureTransferClient: (options: SecureTransferClientOptions) => SecureTransferClient;@absolutejs/secure-transfer