@byline/core 6.12.0 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@types/extraction-types.d.ts +423 -0
- package/dist/@types/extraction-types.js +8 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/extraction/artifact.d.ts +46 -0
- package/dist/extraction/artifact.js +121 -0
- package/dist/extraction/artifact.test.node.d.ts +8 -0
- package/dist/extraction/artifact.test.node.js +194 -0
- package/dist/extraction/errors.d.ts +47 -0
- package/dist/extraction/errors.js +57 -0
- package/dist/extraction/errors.test.node.d.ts +8 -0
- package/dist/extraction/errors.test.node.js +68 -0
- package/dist/extraction/evidence.d.ts +22 -0
- package/dist/extraction/evidence.js +54 -0
- package/dist/extraction/evidence.test.node.d.ts +8 -0
- package/dist/extraction/evidence.test.node.js +138 -0
- package/dist/extraction/index.d.ts +14 -0
- package/dist/extraction/index.js +17 -0
- package/dist/extraction/internal.d.ts +30 -0
- package/dist/extraction/internal.js +93 -0
- package/dist/extraction/outcomes.d.ts +20 -0
- package/dist/extraction/outcomes.js +30 -0
- package/dist/extraction/outcomes.test.node.d.ts +8 -0
- package/dist/extraction/outcomes.test.node.js +55 -0
- package/dist/extraction/page-map.d.ts +40 -0
- package/dist/extraction/page-map.js +190 -0
- package/dist/extraction/page-map.test.node.d.ts +8 -0
- package/dist/extraction/page-map.test.node.js +371 -0
- package/dist/extraction/producer.d.ts +62 -0
- package/dist/extraction/producer.js +76 -0
- package/dist/extraction/producer.test.node.d.ts +8 -0
- package/dist/extraction/producer.test.node.js +230 -0
- package/dist/extraction/root-graph-guard.test.node.d.ts +8 -0
- package/dist/extraction/root-graph-guard.test.node.js +293 -0
- package/dist/extraction/work.d.ts +55 -0
- package/dist/extraction/work.js +102 -0
- package/dist/extraction/work.test.node.d.ts +8 -0
- package/dist/extraction/work.test.node.js +409 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/storage/stored-files-server.d.ts +1 -0
- package/dist/storage/stored-files-server.js +9 -24
- package/dist/storage/stored-files-server.test.node.js +6 -0
- package/dist/storage/verified-read.d.ts +131 -0
- package/dist/storage/verified-read.js +328 -0
- package/dist/storage/verified-read.test.node.d.ts +8 -0
- package/dist/storage/verified-read.test.node.js +645 -0
- package/package.json +6 -6
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { PreconditionFallbackReason, SourceIdentityProblem, SourceUnavailableReason, SourceVerificationEvidence } from '../@types/extraction-types.js';
|
|
9
|
+
import type { StoredFileValue } from '../@types/field-data-types.js';
|
|
10
|
+
import type { IStorageProvider, StorageValidator } from '../@types/storage-types.js';
|
|
11
|
+
/**
|
|
12
|
+
* The file name prefix of a spooled body. A process crash between spooling and
|
|
13
|
+
* `dispose` leaves such a file in the temporary directory, and the prefix lets an
|
|
14
|
+
* operator find and remove it.
|
|
15
|
+
*
|
|
16
|
+
* @provisional
|
|
17
|
+
*/
|
|
18
|
+
export declare const VERIFIED_SOURCE_SPOOL_PREFIX = "byline-verified-source-";
|
|
19
|
+
/**
|
|
20
|
+
* Verified source bytes. The caller disposes of the body in a `finally` block.
|
|
21
|
+
*
|
|
22
|
+
* @provisional
|
|
23
|
+
*/
|
|
24
|
+
export interface VerifiedSourceBody {
|
|
25
|
+
readonly size: number;
|
|
26
|
+
/** A fresh web stream over the verified bytes; may be opened more than once. */
|
|
27
|
+
stream(): ReadableStream<Uint8Array>;
|
|
28
|
+
/** A copy of the verified bytes in memory. Avoid for large files. */
|
|
29
|
+
bytes(): Promise<Uint8Array>;
|
|
30
|
+
/** Deletes any spool file. Idempotent. */
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The outcome of a source read. It is returned rather than thrown, so a caller handles
|
|
35
|
+
* each case explicitly. A precondition that prevented reading yields no `observedHash`.
|
|
36
|
+
*
|
|
37
|
+
* @provisional
|
|
38
|
+
*/
|
|
39
|
+
export type VerifiedSourceRead = {
|
|
40
|
+
status: 'verified';
|
|
41
|
+
evidence: SourceVerificationEvidence;
|
|
42
|
+
body: VerifiedSourceBody;
|
|
43
|
+
} | {
|
|
44
|
+
status: 'identity-problem';
|
|
45
|
+
problem: SourceIdentityProblem;
|
|
46
|
+
storagePath: string;
|
|
47
|
+
/** Absent for `missing-hash`. */
|
|
48
|
+
expectedHash?: string;
|
|
49
|
+
/** Only for `hash-mismatch`. */
|
|
50
|
+
observedHash?: string;
|
|
51
|
+
/** Only for `hash-mismatch`. */
|
|
52
|
+
observedValidator?: StorageValidator;
|
|
53
|
+
preconditionFallback?: PreconditionFallbackReason;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'unavailable';
|
|
56
|
+
reason: SourceUnavailableReason;
|
|
57
|
+
cause?: unknown;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The outcome of a change probe.
|
|
61
|
+
*
|
|
62
|
+
* `unprobed` is reduced assurance, not `unchanged`. With `head-unsupported`, a caller
|
|
63
|
+
* may reuse a verified artifact without a probe, and only separate verify runs detect a
|
|
64
|
+
* same-path replacement. With `no-evidence`, reuse is not permitted.
|
|
65
|
+
*
|
|
66
|
+
* @provisional
|
|
67
|
+
*/
|
|
68
|
+
export type SourceProbe = {
|
|
69
|
+
status: 'unchanged';
|
|
70
|
+
validator: StorageValidator;
|
|
71
|
+
}
|
|
72
|
+
/** Read with `knownChanged`; the hash decides. */
|
|
73
|
+
| {
|
|
74
|
+
status: 'changed';
|
|
75
|
+
validator: StorageValidator;
|
|
76
|
+
} | {
|
|
77
|
+
status: 'missing-object';
|
|
78
|
+
} | {
|
|
79
|
+
status: 'unprobed';
|
|
80
|
+
reason: 'head-unsupported' | 'no-evidence';
|
|
81
|
+
} | {
|
|
82
|
+
status: 'unavailable';
|
|
83
|
+
cause: unknown;
|
|
84
|
+
};
|
|
85
|
+
/** Destroys a stream if it can be destroyed. */
|
|
86
|
+
export declare function destroyStream(stream: NodeJS.ReadableStream, reason?: unknown): void;
|
|
87
|
+
/**
|
|
88
|
+
* Reads a stream to its end, handing each chunk to `onChunk` in order. An abort destroys
|
|
89
|
+
* the stream, which rejects the read, and an abort that fires as the stream ends rejects
|
|
90
|
+
* too. The chunk may be reused by the stream, so `onChunk` copies anything it keeps.
|
|
91
|
+
*/
|
|
92
|
+
export declare function consumeStream(stream: NodeJS.ReadableStream, options: {
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
onChunk: (chunk: Buffer) => void | Promise<void>;
|
|
95
|
+
}): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Reads a stored file and verifies it against the reference's `fileHash`. The body is
|
|
98
|
+
* read to its end and hashed before any caller sees a byte, so only verified bytes can
|
|
99
|
+
* reach an extractor.
|
|
100
|
+
*
|
|
101
|
+
* - Makes at most two reads. A `changed`, `unsupported-kind` or `unestablishable`
|
|
102
|
+
* precondition leads to exactly one unconditional read, because a changed token does
|
|
103
|
+
* not prove changed content and the hash decides.
|
|
104
|
+
* - The evidence validator is the one returned by this read, never one from a probe or
|
|
105
|
+
* from the saved value.
|
|
106
|
+
* - Never writes to storage, never changes the saved value, and never accepts different
|
|
107
|
+
* bytes. Force is a work decision, not a verification bypass.
|
|
108
|
+
*
|
|
109
|
+
* Throws only for a malformed `memoryThreshold`, which is a caller bug.
|
|
110
|
+
*
|
|
111
|
+
* @provisional
|
|
112
|
+
*/
|
|
113
|
+
export declare function readVerifiedSource(storage: IStorageProvider, source: Pick<StoredFileValue, 'storagePath' | 'storageProvider' | 'fileHash' | 'storageValidator'>, options?: {
|
|
114
|
+
/** Skip the expectation: a probe already showed a different validator. */
|
|
115
|
+
knownChanged?: boolean;
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
/** Bytes kept in memory before spooling to a temporary file. Default 8 MiB. */
|
|
118
|
+
memoryThreshold?: number;
|
|
119
|
+
}): Promise<VerifiedSourceRead>;
|
|
120
|
+
/**
|
|
121
|
+
* Compares the stored object's current validator with the **evidence's** validator,
|
|
122
|
+
* never the reference's saved one, so a matching copied value cannot stand in for
|
|
123
|
+
* verification. A different validator, or one of a different kind, is `changed`: the
|
|
124
|
+
* caller reads with `knownChanged`, and the hash decides.
|
|
125
|
+
*
|
|
126
|
+
* An unchanged probe does not establish a first baseline, and does not prove the object
|
|
127
|
+
* cannot change afterwards.
|
|
128
|
+
*
|
|
129
|
+
* @provisional
|
|
130
|
+
*/
|
|
131
|
+
export declare function probeVerifiedSource(storage: IStorageProvider, storagePath: string, evidence: SourceVerificationEvidence | null): Promise<SourceProbe>;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
// Server-only: published through `@byline/core/stored-files`. It hashes and spools with
|
|
9
|
+
// Node built-ins, so it must stay out of the browser-safe main entry.
|
|
10
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
11
|
+
import { createReadStream } from 'node:fs';
|
|
12
|
+
import { open, readFile, rm } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { Readable } from 'node:stream';
|
|
16
|
+
import { isSourceVerificationEvidence } from '../extraction/evidence.js';
|
|
17
|
+
import { ERR_STORAGE, ErrorCodes } from '../lib/errors.js';
|
|
18
|
+
import { isSameStorageValidator } from './content-hash.js';
|
|
19
|
+
/**
|
|
20
|
+
* The file name prefix of a spooled body. A process crash between spooling and
|
|
21
|
+
* `dispose` leaves such a file in the temporary directory, and the prefix lets an
|
|
22
|
+
* operator find and remove it.
|
|
23
|
+
*
|
|
24
|
+
* @provisional
|
|
25
|
+
*/
|
|
26
|
+
export const VERIFIED_SOURCE_SPOOL_PREFIX = 'byline-verified-source-';
|
|
27
|
+
const DEFAULT_MEMORY_THRESHOLD = 8 * 1024 * 1024;
|
|
28
|
+
/** Destroys a stream if it can be destroyed. */
|
|
29
|
+
export function destroyStream(stream, reason) {
|
|
30
|
+
const destroyable = stream;
|
|
31
|
+
destroyable.destroy?.(reason instanceof Error ? reason : undefined);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads a stream to its end, handing each chunk to `onChunk` in order. An abort destroys
|
|
35
|
+
* the stream, which rejects the read, and an abort that fires as the stream ends rejects
|
|
36
|
+
* too. The chunk may be reused by the stream, so `onChunk` copies anything it keeps.
|
|
37
|
+
*/
|
|
38
|
+
export async function consumeStream(stream, options) {
|
|
39
|
+
const { signal, onChunk } = options;
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
// An already-aborted signal never fires 'abort'; release the stream here.
|
|
42
|
+
// Destroy without an error: nothing listens to this stream yet, so an
|
|
43
|
+
// error argument would surface as an unhandled 'error' event.
|
|
44
|
+
destroyStream(stream);
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
}
|
|
47
|
+
const onAbort = () => destroyStream(stream, signal?.reason);
|
|
48
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
49
|
+
try {
|
|
50
|
+
for await (const chunk of stream) {
|
|
51
|
+
await onChunk(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
signal?.removeEventListener('abort', onAbort);
|
|
56
|
+
}
|
|
57
|
+
// Destroying a stream that has already ended does not reject the iteration above.
|
|
58
|
+
signal?.throwIfAborted();
|
|
59
|
+
}
|
|
60
|
+
function preconditionReason(error) {
|
|
61
|
+
const candidate = error;
|
|
62
|
+
if (candidate?.code !== ErrorCodes.STORAGE_PRECONDITION)
|
|
63
|
+
return null;
|
|
64
|
+
const reason = candidate.details?.reason;
|
|
65
|
+
// An unrecognised reason means the expectation could not be checked.
|
|
66
|
+
return reason === 'changed' || reason === 'unsupported-kind' || reason === 'missing'
|
|
67
|
+
? reason
|
|
68
|
+
: 'unestablishable';
|
|
69
|
+
}
|
|
70
|
+
/** Holds bytes in memory up to a threshold, then in a private file. */
|
|
71
|
+
class BodySink {
|
|
72
|
+
memoryThreshold;
|
|
73
|
+
chunks = [];
|
|
74
|
+
handle;
|
|
75
|
+
path;
|
|
76
|
+
size = 0;
|
|
77
|
+
constructor(memoryThreshold) {
|
|
78
|
+
this.memoryThreshold = memoryThreshold;
|
|
79
|
+
}
|
|
80
|
+
async write(chunk) {
|
|
81
|
+
this.size += chunk.length;
|
|
82
|
+
if (this.handle === undefined && this.size <= this.memoryThreshold) {
|
|
83
|
+
this.chunks.push(chunk);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (this.handle === undefined) {
|
|
87
|
+
this.path = join(tmpdir(), `${VERIFIED_SOURCE_SPOOL_PREFIX}${randomBytes(16).toString('hex')}`);
|
|
88
|
+
// `wx` refuses an existing path; the mode keeps the bytes private.
|
|
89
|
+
this.handle = await open(this.path, 'wx', 0o600);
|
|
90
|
+
for (const buffered of this.chunks)
|
|
91
|
+
await this.handle.writeFile(buffered);
|
|
92
|
+
this.chunks = [];
|
|
93
|
+
}
|
|
94
|
+
await this.handle.writeFile(chunk);
|
|
95
|
+
}
|
|
96
|
+
async finish() {
|
|
97
|
+
if (this.handle !== undefined) {
|
|
98
|
+
await this.handle.close();
|
|
99
|
+
this.handle = undefined;
|
|
100
|
+
}
|
|
101
|
+
return this.path === undefined
|
|
102
|
+
? memoryBody(Buffer.concat(this.chunks))
|
|
103
|
+
: spoolBody(this.path, this.size);
|
|
104
|
+
}
|
|
105
|
+
/** Removes everything. Never throws: the read's own failure is what matters. */
|
|
106
|
+
async discard() {
|
|
107
|
+
this.chunks = [];
|
|
108
|
+
await this.handle?.close().catch(() => { });
|
|
109
|
+
this.handle = undefined;
|
|
110
|
+
if (this.path !== undefined)
|
|
111
|
+
await rm(this.path, { force: true }).catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function assertNotDisposed(disposed) {
|
|
115
|
+
if (disposed)
|
|
116
|
+
throw new Error('This verified source body has been disposed.');
|
|
117
|
+
}
|
|
118
|
+
function memoryBody(buffer) {
|
|
119
|
+
let disposed = false;
|
|
120
|
+
return {
|
|
121
|
+
size: buffer.length,
|
|
122
|
+
stream() {
|
|
123
|
+
assertNotDisposed(disposed);
|
|
124
|
+
const copy = new Uint8Array(buffer);
|
|
125
|
+
return new ReadableStream({
|
|
126
|
+
start(controller) {
|
|
127
|
+
if (copy.length > 0)
|
|
128
|
+
controller.enqueue(copy);
|
|
129
|
+
controller.close();
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
async bytes() {
|
|
134
|
+
assertNotDisposed(disposed);
|
|
135
|
+
return new Uint8Array(buffer);
|
|
136
|
+
},
|
|
137
|
+
async dispose() {
|
|
138
|
+
disposed = true;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function spoolBody(path, size) {
|
|
143
|
+
let disposal;
|
|
144
|
+
return {
|
|
145
|
+
size,
|
|
146
|
+
stream() {
|
|
147
|
+
assertNotDisposed(disposal !== undefined);
|
|
148
|
+
return Readable.toWeb(createReadStream(path));
|
|
149
|
+
},
|
|
150
|
+
async bytes() {
|
|
151
|
+
assertNotDisposed(disposal !== undefined);
|
|
152
|
+
return new Uint8Array(await readFile(path));
|
|
153
|
+
},
|
|
154
|
+
dispose() {
|
|
155
|
+
disposal ??= rm(path, { force: true });
|
|
156
|
+
return disposal;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reads a stored file and verifies it against the reference's `fileHash`. The body is
|
|
162
|
+
* read to its end and hashed before any caller sees a byte, so only verified bytes can
|
|
163
|
+
* reach an extractor.
|
|
164
|
+
*
|
|
165
|
+
* - Makes at most two reads. A `changed`, `unsupported-kind` or `unestablishable`
|
|
166
|
+
* precondition leads to exactly one unconditional read, because a changed token does
|
|
167
|
+
* not prove changed content and the hash decides.
|
|
168
|
+
* - The evidence validator is the one returned by this read, never one from a probe or
|
|
169
|
+
* from the saved value.
|
|
170
|
+
* - Never writes to storage, never changes the saved value, and never accepts different
|
|
171
|
+
* bytes. Force is a work decision, not a verification bypass.
|
|
172
|
+
*
|
|
173
|
+
* Throws only for a malformed `memoryThreshold`, which is a caller bug.
|
|
174
|
+
*
|
|
175
|
+
* @provisional
|
|
176
|
+
*/
|
|
177
|
+
export async function readVerifiedSource(storage, source, options) {
|
|
178
|
+
const memoryThreshold = options?.memoryThreshold ?? DEFAULT_MEMORY_THRESHOLD;
|
|
179
|
+
if (!Number.isSafeInteger(memoryThreshold) || memoryThreshold < 0) {
|
|
180
|
+
throw new TypeError('memoryThreshold must be a non-negative integer');
|
|
181
|
+
}
|
|
182
|
+
const { storagePath, fileHash } = source;
|
|
183
|
+
const signal = options?.signal;
|
|
184
|
+
if (fileHash === undefined || fileHash === null || fileHash === '') {
|
|
185
|
+
return { status: 'identity-problem', problem: 'missing-hash', storagePath };
|
|
186
|
+
}
|
|
187
|
+
if (source.storageProvider !== storage.providerName) {
|
|
188
|
+
return { status: 'unavailable', reason: 'provider-mismatch' };
|
|
189
|
+
}
|
|
190
|
+
if (storage.download === undefined) {
|
|
191
|
+
return { status: 'unavailable', reason: 'download-unsupported' };
|
|
192
|
+
}
|
|
193
|
+
const failure = (cause) => ({
|
|
194
|
+
status: 'unavailable',
|
|
195
|
+
reason: signal?.aborted === true ? 'aborted' : 'read-failed',
|
|
196
|
+
cause,
|
|
197
|
+
});
|
|
198
|
+
if (signal?.aborted)
|
|
199
|
+
return failure(signal.reason);
|
|
200
|
+
const expect = options?.knownChanged === true ? undefined : (source.storageValidator ?? undefined);
|
|
201
|
+
let fallback;
|
|
202
|
+
const missingObject = () => ({
|
|
203
|
+
status: 'identity-problem',
|
|
204
|
+
problem: 'missing-object',
|
|
205
|
+
storagePath,
|
|
206
|
+
expectedHash: fileHash,
|
|
207
|
+
...(fallback !== undefined && { preconditionFallback: fallback }),
|
|
208
|
+
});
|
|
209
|
+
let read;
|
|
210
|
+
try {
|
|
211
|
+
read = await storage.download(storagePath, {
|
|
212
|
+
...(expect !== undefined && { expect }),
|
|
213
|
+
...(signal !== undefined && { signal }),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const reason = preconditionReason(error);
|
|
218
|
+
if (reason === null)
|
|
219
|
+
return failure(error);
|
|
220
|
+
if (reason === 'missing')
|
|
221
|
+
return missingObject();
|
|
222
|
+
fallback = reason;
|
|
223
|
+
try {
|
|
224
|
+
read = await storage.download(storagePath, signal !== undefined ? { signal } : {});
|
|
225
|
+
}
|
|
226
|
+
catch (fallbackError) {
|
|
227
|
+
// There is never a third read. Only `missing` keeps a specific meaning here.
|
|
228
|
+
return preconditionReason(fallbackError) === 'missing'
|
|
229
|
+
? missingObject()
|
|
230
|
+
: failure(fallbackError);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (read === null)
|
|
234
|
+
return missingObject();
|
|
235
|
+
const sink = new BodySink(memoryThreshold);
|
|
236
|
+
const hash = createHash('sha256');
|
|
237
|
+
try {
|
|
238
|
+
await consumeStream(read.body, {
|
|
239
|
+
...(signal !== undefined && { signal }),
|
|
240
|
+
onChunk: async (chunk) => {
|
|
241
|
+
// A stream may reuse its buffer, so the body must own the bytes it hashed.
|
|
242
|
+
const owned = Buffer.from(chunk);
|
|
243
|
+
hash.update(owned);
|
|
244
|
+
await sink.write(owned);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (read.size !== undefined && read.size !== sink.size) {
|
|
248
|
+
throw ERR_STORAGE({
|
|
249
|
+
message: `The body ended at ${sink.size} bytes, but the provider reported ${read.size}.`,
|
|
250
|
+
details: { reason: 'size-mismatch', actualSize: sink.size, declaredSize: read.size },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
destroyStream(read.body);
|
|
256
|
+
await sink.discard();
|
|
257
|
+
return failure(error);
|
|
258
|
+
}
|
|
259
|
+
const observedHash = hash.digest('hex');
|
|
260
|
+
if (observedHash !== fileHash) {
|
|
261
|
+
await sink.discard();
|
|
262
|
+
return {
|
|
263
|
+
status: 'identity-problem',
|
|
264
|
+
problem: 'hash-mismatch',
|
|
265
|
+
storagePath,
|
|
266
|
+
expectedHash: fileHash,
|
|
267
|
+
observedHash,
|
|
268
|
+
observedValidator: read.validator,
|
|
269
|
+
...(fallback !== undefined && { preconditionFallback: fallback }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const evidence = {
|
|
273
|
+
expectedHash: fileHash,
|
|
274
|
+
contentHash: observedHash,
|
|
275
|
+
validator: read.validator,
|
|
276
|
+
verifiedAt: new Date().toISOString(),
|
|
277
|
+
size: sink.size,
|
|
278
|
+
readBasis: fallback !== undefined ? 'fallback' : expect !== undefined ? 'expected' : 'unconditional',
|
|
279
|
+
...(fallback !== undefined && { preconditionFallback: fallback }),
|
|
280
|
+
};
|
|
281
|
+
if (!isSourceVerificationEvidence(evidence, { expectedHash: fileHash })) {
|
|
282
|
+
await sink.discard();
|
|
283
|
+
return failure(ERR_STORAGE({
|
|
284
|
+
message: `Storage provider '${storage.providerName}' returned an unusable validator.`,
|
|
285
|
+
details: { reason: 'validator-unavailable', storagePath },
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const body = await sink.finish();
|
|
290
|
+
if (signal?.aborted) {
|
|
291
|
+
await body.dispose();
|
|
292
|
+
return failure(signal.reason);
|
|
293
|
+
}
|
|
294
|
+
return { status: 'verified', evidence, body };
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
await sink.discard();
|
|
298
|
+
return failure(error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Compares the stored object's current validator with the **evidence's** validator,
|
|
303
|
+
* never the reference's saved one, so a matching copied value cannot stand in for
|
|
304
|
+
* verification. A different validator, or one of a different kind, is `changed`: the
|
|
305
|
+
* caller reads with `knownChanged`, and the hash decides.
|
|
306
|
+
*
|
|
307
|
+
* An unchanged probe does not establish a first baseline, and does not prove the object
|
|
308
|
+
* cannot change afterwards.
|
|
309
|
+
*
|
|
310
|
+
* @provisional
|
|
311
|
+
*/
|
|
312
|
+
export async function probeVerifiedSource(storage, storagePath, evidence) {
|
|
313
|
+
if (!isSourceVerificationEvidence(evidence))
|
|
314
|
+
return { status: 'unprobed', reason: 'no-evidence' };
|
|
315
|
+
if (storage.head === undefined)
|
|
316
|
+
return { status: 'unprobed', reason: 'head-unsupported' };
|
|
317
|
+
try {
|
|
318
|
+
const head = await storage.head(storagePath);
|
|
319
|
+
if (head === null)
|
|
320
|
+
return { status: 'missing-object' };
|
|
321
|
+
return isSameStorageValidator(head.validator, evidence.validator)
|
|
322
|
+
? { status: 'unchanged', validator: head.validator }
|
|
323
|
+
: { status: 'changed', validator: head.validator };
|
|
324
|
+
}
|
|
325
|
+
catch (cause) {
|
|
326
|
+
return { status: 'unavailable', cause };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|