Files
tech-log-frontend/src/application/ports/browser-file-storage/file.ts
T

251 lines
6.9 KiB
TypeScript

import type {
BrowserDataResult,
TransferProgress,
} from "./shared.ts";
import type { AuthorizedDownloadCapability } from "../browser-transfer/authorized-download.ts";
/**
* Application-owned browser-file contracts.
*
* Native File, Blob, FileList, FileSystemHandle, Response and ReadableStream
* intentionally do not cross this boundary. A transient object URL may cross
* only through the presentation-local PreviewLease below; it must never enter
* domain state, persistence, diagnostics or a general application cache.
*/
declare const localFileRefBrand: unique symbol;
declare const fileVerificationReceiptBrand: unique symbol;
declare const filePolicyKeyBrand: unique symbol;
declare const filePolicyIntentionBrand: unique symbol;
declare const browserManagedCapabilityReceiptBrand: unique symbol;
export type LocalFileRef = string & {
readonly [localFileRefBrand]: "LocalFileRef";
};
export type FileVerificationReceipt = string & {
readonly [fileVerificationReceiptBrand]: "FileVerificationReceipt";
};
/**
* Registry-issued, non-semantic identifiers. A feature receives a frozen
* reference from its composition root; presentation must not construct policy
* definitions or select another feature's registered policy.
*/
export type FilePolicyKey = string & {
readonly [filePolicyKeyBrand]: "FilePolicyKey";
};
export type FilePolicyIntention = string & {
readonly [filePolicyIntentionBrand]: "FilePolicyIntention";
};
export type FilePolicyReference = Readonly<{
policyKey: FilePolicyKey;
intention: FilePolicyIntention;
}>;
export type FileSelectionSource =
| "NATIVE_INPUT"
| "SYSTEM_PICKER"
| "DROP";
export type FileSelectionLimitReduction = Readonly<{
maxCount?: number;
maxFileBytes?: number;
maxTotalBytes?: number;
}>;
export type FileCandidate = Readonly<{
ref: LocalFileRef;
/**
* Untrusted, potentially personal display metadata. It must never be used as
* a resource identifier or diagnostics attribute.
*/
displayName: string;
sizeBytes: number;
reportedMediaType: string | null;
lastModifiedEpochMs: number | null;
source: FileSelectionSource;
}>;
export type FileSelectionOutcome =
| Readonly<{ kind: "SELECTED"; files: readonly FileCandidate[] }>
| Readonly<{ kind: "DISMISSED" }>;
export type FilePickerSupport = Readonly<{
nativeInput: true;
systemOpenPicker: boolean;
systemSavePicker: boolean;
}>;
export interface FilePickerPort {
readonly support: FilePickerSupport;
/**
* Must be invoked as the first browser action in a trusted user activation.
* A dismissed picker is a successful DISMISSED outcome, not an error.
*/
select(input: {
policy: FilePolicyReference;
limits?: FileSelectionLimitReduction;
signal?: AbortSignal;
}): Promise<BrowserDataResult<FileSelectionOutcome>>;
release(ref: LocalFileRef): void;
}
export type FileSignatureResult =
| "MATCHED"
| "MISMATCHED"
| "UNKNOWN";
export type FileInspection = Readonly<{
byteLength: number;
reportedMediaType: string | null;
detectedMediaType: string | null;
normalizedExtension: string | null;
signature: FileSignatureResult;
/**
* Issued only for a matched signature and bound inside the transient vault
* to this file snapshot and inspection policy.
*/
verificationReceipt: FileVerificationReceipt | null;
}>;
/**
* File-capability byte stream with a closed failure channel. Implementations
* must convert native exceptions to BrowserDataResult and never throw a raw
* DOMException across the application boundary.
*/
export interface FileByteSource {
readonly byteLength: number | null;
stream(
signal: AbortSignal,
): AsyncIterable<BrowserDataResult<Uint8Array>>;
}
export interface FileContentPort {
inspect(input: {
ref: LocalFileRef;
policy: FilePolicyReference;
maxInspectionBytes?: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<FileInspection>>;
readRange(input: {
ref: LocalFileRef;
offset: number;
length: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<Uint8Array>>;
openSource(input: {
ref: LocalFileRef;
signal: AbortSignal;
}): Promise<BrowserDataResult<FileByteSource>>;
release(ref: LocalFileRef): void;
}
export type PreviewLease = Readonly<{
url: string;
mediaType: string;
release(): void;
}>;
export interface TransientPreviewPort {
create(input: {
ref: LocalFileRef;
verificationReceipt: FileVerificationReceipt;
policy: FilePolicyReference;
maxPreviewBytes?: number;
signal: AbortSignal;
}): Promise<BrowserDataResult<PreviewLease>>;
dispose(): void;
}
export type BrowserManagedDownloadCapabilityReceipt = string & {
readonly [browserManagedCapabilityReceiptBrand]:
"BrowserManagedDownloadCapabilityReceipt";
};
export type DownloadSource =
| Readonly<{
kind: "BROWSER_MANAGED_RESOURCE";
resourceId: string;
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
}>
| Readonly<{
kind: "AUTHORIZED_STREAM_RESOURCE";
resourceId: string;
/**
* Exact provider-issued handle. Raw href/query/header values are never
* caller inputs and an equal-looking fabricated handle must be rejected.
*/
capability: AuthorizedDownloadCapability;
}>
| Readonly<{
kind: "GENERATED";
bytes: FileByteSource;
expectedSha256?: string;
}>;
export type DownloadStrategy =
| "BROWSER_MANAGED"
| "PROMPT_AND_STREAM"
| "BOUNDED_OBJECT_URL";
export type DownloadOutcome =
| Readonly<{
kind: "BROWSER_HANDOFF";
transferId: string;
}>
| Readonly<{
kind: "SAVED";
transferId: string;
bytesWritten: number;
integrity: "VERIFIED" | "NOT_PROVIDED";
}>
| Readonly<{ kind: "DISMISSED" }>;
export interface DownloadDeliveryPort {
deliver(input: {
policy: FilePolicyReference;
source: DownloadSource;
suggestedFileName: string;
/**
* Optional reductions of the composition-owned policy ceiling. These
* values can never raise the registered or absolute runtime limits.
*/
maxTransferBytes?: number;
maxBufferedBytes?: number;
signal: AbortSignal;
onProgress(progress: TransferProgress): void;
}): Promise<BrowserDataResult<DownloadOutcome>>;
}
/**
* Synchronously resolved, server-enforced handoff capability. The endpoint
* behind href must bind and enforce every field, including expiry and the
* optional digest; the browser adapter cannot observe navigation bytes.
*/
export type BrowserManagedDownloadCapability = Readonly<{
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
href: string;
resourceId: string;
mediaType: string;
safeExtension: string;
maxBytes: number;
expectedSha256?: string;
expiresAtEpochMs: number;
}>;
export interface BrowserManagedDownloadCapabilityResolver {
resolve(input: Readonly<{
resourceId: string;
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
}>): BrowserDataResult<BrowserManagedDownloadCapability>;
}