The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
294 lines
8.3 KiB
TypeScript
294 lines
8.3 KiB
TypeScript
import type { Result } from "../../result.ts";
|
|
import type { FileByteSource } from "../browser-file-storage/file.ts";
|
|
import type {
|
|
BrowserDataFailure,
|
|
BrowserDataResult,
|
|
TransferProgress,
|
|
} from "../browser-file-storage/shared.ts";
|
|
|
|
export const RESUMABLE_UPLOAD_PROTOCOL =
|
|
"PRESIGNED_MULTIPART_V1" as const;
|
|
export type ResumableUploadProtocol =
|
|
typeof RESUMABLE_UPLOAD_PROTOCOL;
|
|
|
|
/**
|
|
* Multipart uploads use a bounded part manifest instead of a whole-file
|
|
* ArrayBuffer. The digest is SHA-256 over the canonical ordered part metadata
|
|
* and SHA-256 part digests.
|
|
*/
|
|
export type UploadFileFingerprint = Readonly<{
|
|
algorithm: "SHA-256-PARTS-V1";
|
|
digestHex: string;
|
|
byteLength: number;
|
|
partSizeBytes: number;
|
|
partCount: number;
|
|
}>;
|
|
|
|
export type UploadPartDescriptor = Readonly<{
|
|
partNumber: number;
|
|
offset: number;
|
|
byteLength: number;
|
|
checksumSha256: string;
|
|
}>;
|
|
|
|
/**
|
|
* A non-authorizing server acknowledgement. It must be opaque, contain no PII,
|
|
* URL or credential, and be accepted only with the exact descriptor binding.
|
|
*/
|
|
export type UploadPartReceipt = UploadPartDescriptor &
|
|
Readonly<{
|
|
receiptToken: string;
|
|
}>;
|
|
|
|
export type PartitionDeleteOutcome =
|
|
| Readonly<{ state: "DELETED"; effect: "APPLIED" }>
|
|
| Readonly<{
|
|
state: "PENDING";
|
|
effect: "UNKNOWN";
|
|
reason: "BLOCKED_DEADLINE";
|
|
}>;
|
|
|
|
export interface UploadRangeReader {
|
|
readonly byteLength: number;
|
|
readRange(input: Readonly<{
|
|
offset: number;
|
|
length: number;
|
|
signal: AbortSignal;
|
|
}>): Promise<BrowserDataResult<Uint8Array>>;
|
|
}
|
|
|
|
/**
|
|
* FILE_BYTE_SOURCE supports existing FileByteSource implementations. It must
|
|
* be replayable for the fingerprint pass and transfer pass. RANGE_READER is
|
|
* preferred for concurrent uploads and OPFS/file-vault range adapters.
|
|
*/
|
|
export type ResumableUploadSource =
|
|
| Readonly<{
|
|
kind: "FILE_BYTE_SOURCE";
|
|
bytes: FileByteSource;
|
|
}>
|
|
| Readonly<{
|
|
kind: "RANGE_READER";
|
|
reader: UploadRangeReader;
|
|
}>;
|
|
|
|
export type ResumableUploadRequest = Readonly<{
|
|
/** Opaque, caller-stable operation key. It must not contain a file name. */
|
|
uploadKey: string;
|
|
/** Registry-approved backend purpose identifier, not user-provided text. */
|
|
purpose: string;
|
|
mediaType: string;
|
|
source: ResumableUploadSource;
|
|
signal: AbortSignal;
|
|
onProgress?: (progress: TransferProgress) => void;
|
|
}>;
|
|
|
|
export type QuarantinedUpload = Readonly<{
|
|
state: "QUARANTINED";
|
|
resourceId: string;
|
|
byteLength: number;
|
|
replayed: boolean;
|
|
}>;
|
|
|
|
export type UploadAbortOutcome = Readonly<{
|
|
state: "ABORTED" | "ORPHANED" | "ALREADY_COMPLETED" | "NOT_FOUND";
|
|
}>;
|
|
|
|
export interface ResumableUploadPort {
|
|
upload(
|
|
request: ResumableUploadRequest,
|
|
): Promise<BrowserDataResult<QuarantinedUpload>>;
|
|
abort(request: Readonly<{
|
|
uploadKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<BrowserDataResult<UploadAbortOutcome>>;
|
|
}
|
|
|
|
/**
|
|
* Retry-After is transport metadata used only by the runtime. It is bounded
|
|
* before sleeping and is removed from the application-facing failure.
|
|
*/
|
|
export type UploadProviderFailure = BrowserDataFailure &
|
|
Readonly<{
|
|
retryAfterMs?: number;
|
|
}>;
|
|
|
|
export type UploadProviderResult<Value> = Result<
|
|
Value,
|
|
UploadProviderFailure
|
|
>;
|
|
|
|
export type UploadSession = Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
partSizeBytes: number;
|
|
partCount: number;
|
|
maxConcurrency: number;
|
|
expiresAtEpochMs: number;
|
|
}>;
|
|
|
|
export type ActiveUploadStatus = Readonly<{
|
|
state: "ACTIVE";
|
|
session: UploadSession;
|
|
acceptedParts: readonly UploadPartReceipt[];
|
|
}>;
|
|
|
|
export type UploadSessionStatus =
|
|
| ActiveUploadStatus
|
|
| Readonly<{
|
|
state: "QUARANTINED";
|
|
session: UploadSession;
|
|
resourceId: string;
|
|
}>
|
|
| Readonly<{
|
|
state: "ABORTED" | "EXPIRED" | "NOT_FOUND";
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
}>;
|
|
|
|
/**
|
|
* The capability value is intentionally generic. The presigned-transfer
|
|
* adapter owns its URL/method/header contract; this port neither duplicates
|
|
* that type nor permits it to enter a durable checkpoint.
|
|
*/
|
|
export type UploadPartCapability<Capability> = Readonly<{
|
|
capability: Capability;
|
|
uploadBindingSha256: string;
|
|
expiresAtEpochMs: number;
|
|
}>;
|
|
|
|
export interface ResumableUploadControlPlane<Capability> {
|
|
createSession(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
uploadKey: string;
|
|
purpose: string;
|
|
mediaType: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
requestedPartSizeBytes: number;
|
|
requestedMaxConcurrency: number;
|
|
idempotencyKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<UploadSession>>;
|
|
|
|
getStatus(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<UploadSessionStatus>>;
|
|
|
|
issuePartCapability(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
uploadBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
mediaType: string;
|
|
part: UploadPartDescriptor;
|
|
idempotencyKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<UploadPartCapability<Capability>>>;
|
|
|
|
complete(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
orderedParts: readonly UploadPartReceipt[];
|
|
idempotencyKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<Readonly<{
|
|
state: "QUARANTINED";
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
resourceId: string;
|
|
}>>>;
|
|
|
|
abort(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
idempotencyKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<Readonly<{
|
|
state: "ABORTED" | "NOT_FOUND" | "EXPIRED" | "ALREADY_COMPLETED";
|
|
}>>>;
|
|
}
|
|
|
|
export interface UploadPartExecutor<Capability> {
|
|
uploadPart(input: Readonly<{
|
|
protocol: ResumableUploadProtocol;
|
|
capability: Capability;
|
|
sessionId: string;
|
|
requestBindingSha256: string;
|
|
uploadBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
mediaType: string;
|
|
part: UploadPartDescriptor;
|
|
bytes: Uint8Array;
|
|
idempotencyKey: string;
|
|
signal: AbortSignal;
|
|
}>): Promise<UploadProviderResult<UploadPartReceipt>>;
|
|
}
|
|
|
|
/**
|
|
* Durable, non-secret recovery state. Implementations must reject any unknown
|
|
* property so a signed URL, authorization header or user metadata cannot be
|
|
* smuggled into persistence.
|
|
*/
|
|
export type ResumableUploadCheckpoint = Readonly<{
|
|
schemaVersion: 1;
|
|
protocol: ResumableUploadProtocol;
|
|
revision: number;
|
|
state: "ACTIVE" | "ABORT_PENDING";
|
|
uploadKey: string;
|
|
requestBindingSha256: string;
|
|
fingerprint: UploadFileFingerprint;
|
|
sessionId: string;
|
|
sessionExpiresAtEpochMs: number;
|
|
sessionMaxConcurrency: number;
|
|
acceptedParts: readonly UploadPartReceipt[];
|
|
updatedAtEpochMs: number;
|
|
}>;
|
|
|
|
export interface ResumableUploadCheckpointStore {
|
|
read(
|
|
uploadKey: string,
|
|
signal?: AbortSignal,
|
|
): Promise<BrowserDataResult<ResumableUploadCheckpoint | null>>;
|
|
compareAndSwap(input: Readonly<{
|
|
expectedRevision: number | null;
|
|
checkpoint: ResumableUploadCheckpoint;
|
|
signal?: AbortSignal;
|
|
}>): Promise<BrowserDataResult<ResumableUploadCheckpoint>>;
|
|
remove(input: Readonly<{
|
|
uploadKey: string;
|
|
expectedRevision: number;
|
|
signal?: AbortSignal;
|
|
}>): Promise<BrowserDataResult<void>>;
|
|
close(): void;
|
|
}
|
|
|
|
export interface ResumableUploadCheckpointAdmin {
|
|
/**
|
|
* Account/logout lifecycle operation for this already-bound opaque partition.
|
|
* The adapter closes its connection before deletion and bounds blocked waits.
|
|
*/
|
|
/**
|
|
* BT-UP-03. An IndexedDB `deleteDatabase()` request cannot be cancelled once
|
|
* dispatched, so a blocked deadline is not evidence that nothing happened.
|
|
* `PENDING` reports the effect honestly as `UNKNOWN`; only pre-dispatch
|
|
* problems are ordinary failures.
|
|
*/
|
|
deletePartition(
|
|
signal?: AbortSignal,
|
|
): Promise<BrowserDataResult<PartitionDeleteOutcome>>;
|
|
}
|