Files
0pt/src/document-workbench/upload-coordinator.ts
T
admin 92eb78ba0a
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 37s
CI (SIAX Cloud) / quality (pull_request) Successful in 43s
fix(upload): an idempotent retry returns the same verified receipt
A client retry with the same idempotency key replayed the journal and got
'outcome_unknown' even when the first upload had succeeded, because the
verified DOC0 receipt was never persisted. Store document/version/checksum/
stage on the attempt (sql/021) and return it on replay; only a missing receipt
falls back to observation. No second POST, no duplicate document.
2026-11-03 00:35:32 +01:00

108 lines
4.7 KiB
TypeScript

import {
Doc0UploadOutcomeUnknownError,
prepareDoc0UploadIdentity,
type Doc0Inspection,
type Doc0InspectionInput,
type Doc0UploadInput,
type Doc0UploadReceipt,
} from './doc0-upload-client';
import type { StoredUploadReceipt, UploadAttemptIdentity, UploadAttemptState } from './upload-attempt-store';
type Scope = Pick<Doc0UploadInput, 'tenantId' | 'projectId' | 'actorId'>;
type Attempts = {
prepare(input: UploadAttemptIdentity): Promise<{ state: UploadAttemptState; replay: boolean }>;
startDispatch(input: UploadAttemptIdentity): Promise<boolean>;
/** Optional so lightweight tests/implementations keep working. */
recordReceipt?(input: UploadAttemptIdentity, receipt: StoredUploadReceipt): Promise<void>;
getReceipt?(input: UploadAttemptIdentity): Promise<StoredUploadReceipt | undefined>;
};
type Doc0 = {
upload(input: Doc0UploadInput): Promise<Doc0UploadReceipt>;
inspect(input: Doc0InspectionInput): Promise<Doc0Inspection>;
};
export type CoordinatedUpload =
| { status: 'receipt_verified'; receipt: Doc0UploadReceipt }
| { status: 'outcome_unknown'; observation?: Doc0Inspection };
/**
* Internal only: no customer routes are wired. Scope must be derived from an
* authenticated principal and independently authorized by the injected policy.
* The DOC0 journal, RLS database role, storage and project authorization must
* be deployed and verified before this coordinator can serve customer data.
*/
export class DocumentUploadCoordinator {
constructor(
private readonly deps: {
attempts: Attempts;
doc0: Doc0;
authorize: (scope: Scope) => Promise<boolean>;
maxBytes: number;
},
) {
if (!Number.isSafeInteger(deps.maxBytes) || deps.maxBytes <= 0) throw new Error('Invalid upload limit');
}
async execute(input: Doc0UploadInput): Promise<CoordinatedUpload> {
if (!(input.bytes instanceof Uint8Array) || input.bytes.byteLength > this.deps.maxBytes) {
throw new Error('Invalid upload input');
}
// Snapshot both metadata and bytes before the first await: a caller cannot
// mutate the request between the durable fingerprint and the actual POST.
const snapshot: Doc0UploadInput = { ...input, bytes: new Uint8Array(input.bytes) };
const details = prepareDoc0UploadIdentity(snapshot);
const scope = { tenantId: snapshot.tenantId, projectId: snapshot.projectId, actorId: snapshot.actorId };
if ((await this.deps.authorize(scope)) !== true) throw new Error('Document upload not authorized');
const attempt: UploadAttemptIdentity = {
...scope,
attemptId: snapshot.idempotencyKey,
sourceSha256: details.sourceSha256,
sourceSizeBytes: details.sourceSizeBytes,
requestFingerprint: details.requestFingerprint,
};
await this.deps.attempts.prepare(attempt);
// A database error here also stops the operation: the update might have
// committed and must never be followed by an unrecorded network write.
if ((await this.deps.attempts.startDispatch(attempt)) === true) {
try {
const receipt = await this.deps.doc0.upload(snapshot);
// Persist the verified receipt so retrying with the same idempotency
// key returns this exact 201 instead of "outcome unknown". A journal
// write failure must not turn a real upload into an error response.
try {
await this.deps.attempts.recordReceipt?.(attempt, receipt);
} catch {
/* receipt stays unpersisted; a retry falls back to observation */
}
return { status: 'receipt_verified', receipt };
} catch (error) {
// The client marks every potentially committed POST as unknown. Any
// other error propagates, while the one-way dispatch remains recorded.
if (!(error instanceof Doc0UploadOutcomeUnknownError)) throw error;
}
}
// A retry of an already-dispatched attempt: if the first dispatch returned
// a verified receipt, return it again (same document, same checksum).
try {
const stored = await this.deps.attempts.getReceipt?.(attempt);
if (stored) return { status: 'receipt_verified', receipt: stored };
} catch {
/* fall through to observation */
}
// Observation only. Even not_found does not prove that a write cannot
// still arrive; completed_recorded does not prove storage or extraction.
try {
const observation = await this.deps.doc0.inspect({
...scope,
requestId: snapshot.requestId,
idempotencyKey: snapshot.idempotencyKey,
sourceSha256: details.sourceSha256,
sourceSizeBytes: details.sourceSizeBytes,
requestFingerprint: details.requestFingerprint,
});
return { status: 'outcome_unknown', observation };
} catch {
return { status: 'outcome_unknown' };
}
}
}