fix: stabilize DOC0 upload fingerprint metadata
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 28s
masterplan-lock / check (pull_request) Successful in 32s
CI (SIAX Cloud) / quality (push) Successful in 51s
CI (SIAX Cloud) / quality (pull_request) Successful in 50s
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 28s
masterplan-lock / check (pull_request) Successful in 32s
CI (SIAX Cloud) / quality (push) Successful in 51s
CI (SIAX Cloud) / quality (pull_request) Successful in 50s
This commit is contained in:
@@ -51,6 +51,45 @@ export type Doc0Inspection =
|
||||
| { status: 'not_found' | 'in_progress' | 'reconciliation_required' }
|
||||
| { status: 'completed_recorded'; documentId: string; versionId: string };
|
||||
|
||||
/**
|
||||
* DOC0 fingerprints the multipart parser's filename and MIME, not the values
|
||||
* supplied to FormData. Use an ASCII wire name and a restricted, normalized
|
||||
* MIME so their parser interpretation is deterministic. The original name is
|
||||
* preserved separately as DOC0's displayName.
|
||||
*
|
||||
* Internal contract only: callers must retain this identity before dispatch,
|
||||
* authorize its scope, and keep the same input bytes/metadata until dispatch.
|
||||
*/
|
||||
export function prepareDoc0UploadIdentity(input: Doc0UploadInput): {
|
||||
sourceSha256: string;
|
||||
sourceSizeBytes: number;
|
||||
requestFingerprint: string;
|
||||
wireFilename: string;
|
||||
wireMimeType: string;
|
||||
} {
|
||||
if (
|
||||
![input.tenantId, input.projectId, input.actorId, input.requestId, input.idempotencyKey, input.filename, input.mimeType]
|
||||
.every((value) => typeof value === 'string' && value.trim()) ||
|
||||
![input.tenantId, input.actorId, input.requestId, input.idempotencyKey]
|
||||
.every((value) => /^[\x21-\x7e]{1,256}$/.test(value)) ||
|
||||
!['public', 'internal', 'confidential', 'restricted', 'personal-data', 'financial', 'legal'].includes(input.classification) ||
|
||||
!(input.bytes instanceof Uint8Array) || input.bytes.byteLength === 0
|
||||
) throw new Error('Invalid DOC0 upload input');
|
||||
const wireMimeType = input.mimeType.toLowerCase();
|
||||
if (!/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(wireMimeType)) {
|
||||
throw new Error('Invalid DOC0 upload MIME type');
|
||||
}
|
||||
const extension = /\.([a-z0-9]{1,16})$/i.exec(input.filename)?.[1]?.toLowerCase() ?? 'bin';
|
||||
const wireFilename = `source.${extension}`;
|
||||
const sourceSha256 = createHash('sha256').update(input.bytes).digest('hex');
|
||||
const sourceSizeBytes = input.bytes.byteLength;
|
||||
const requestFingerprint = createHash('sha256').update(JSON.stringify([
|
||||
'doc0-upload-v1', input.tenantId, input.projectId, input.actorId, sourceSha256,
|
||||
sourceSizeBytes, wireFilename, wireMimeType, input.filename, 'unknown', input.classification, [], null,
|
||||
])).digest('hex');
|
||||
return { sourceSha256, sourceSizeBytes, requestFingerprint, wireFilename, wireMimeType };
|
||||
}
|
||||
|
||||
const MAX_RECEIPT_BYTES = 128 * 1024;
|
||||
|
||||
async function readBoundedJson(response: Response): Promise<unknown> {
|
||||
@@ -108,26 +147,9 @@ export class Doc0UploadClient {
|
||||
}
|
||||
|
||||
async upload(input: Doc0UploadInput): Promise<Doc0UploadReceipt> {
|
||||
if (
|
||||
![
|
||||
input.tenantId,
|
||||
input.projectId,
|
||||
input.actorId,
|
||||
input.requestId,
|
||||
input.idempotencyKey,
|
||||
input.filename,
|
||||
input.mimeType,
|
||||
].every((value) => typeof value === 'string' && value.trim()) ||
|
||||
!['public', 'internal', 'confidential', 'restricted', 'personal-data', 'financial', 'legal'].includes(
|
||||
input.classification,
|
||||
) ||
|
||||
!(input.bytes instanceof Uint8Array) ||
|
||||
input.bytes.byteLength === 0 ||
|
||||
input.bytes.byteLength > this.options.maxBytes
|
||||
) {
|
||||
throw new Error('Invalid DOC0 upload input');
|
||||
}
|
||||
const expectedHash = createHash('sha256').update(input.bytes).digest('hex');
|
||||
const identity = prepareDoc0UploadIdentity(input);
|
||||
if (identity.sourceSizeBytes > this.options.maxBytes) throw new Error('Invalid DOC0 upload input');
|
||||
const expectedHash = identity.sourceSha256;
|
||||
const body = new FormData();
|
||||
body.set(
|
||||
'input',
|
||||
@@ -138,7 +160,7 @@ export class Doc0UploadClient {
|
||||
classification: input.classification,
|
||||
}),
|
||||
);
|
||||
body.set('file', new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }), input.filename);
|
||||
body.set('file', new Blob([new Uint8Array(input.bytes)], { type: identity.wireMimeType }), identity.wireFilename);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await this.fetchImpl(`${this.baseUrl}/v1/doc0/documents`, {
|
||||
|
||||
Reference in New Issue
Block a user