@phis/contracts 0.3.1 → 0.5.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/NOTICE +1 -1
- package/README.md +159 -151
- package/dist/access.d.ts +13 -0
- package/dist/access.js +13 -0
- package/dist/addon-index.d.ts +4 -0
- package/dist/addon-index.js +4 -0
- package/dist/addon.d.ts +53 -2
- package/dist/capabilities.d.ts +54 -87
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +14 -2
- package/dist/cms.d.ts +11 -0
- package/dist/cms.js +20 -0
- package/dist/conditions.d.ts +96 -0
- package/dist/conditions.js +163 -0
- package/dist/controls.d.ts +83 -0
- package/dist/controls.js +133 -0
- package/dist/core.d.ts +38 -0
- package/dist/core.js +39 -0
- package/dist/forms.d.ts +496 -0
- package/dist/forms.js +508 -0
- package/dist/http.d.ts +10 -1
- package/dist/http.js +10 -1
- package/dist/layout.d.ts +22 -0
- package/dist/layout.js +30 -0
- package/dist/media-storage.d.ts +256 -14
- package/dist/media-storage.js +30 -2
- package/dist/media.d.ts +47 -3
- package/dist/media.js +59 -0
- package/dist/record.d.ts +8 -0
- package/dist/record.js +24 -0
- package/dist/references.d.ts +130 -0
- package/dist/references.js +208 -0
- package/dist/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -0
- package/dist/signals.d.ts +1 -0
- package/dist/signals.js +3 -0
- package/dist/site-groups.d.ts +15 -0
- package/dist/support.d.ts +390 -0
- package/dist/support.js +148 -0
- package/dist/threads.d.ts +363 -0
- package/dist/threads.js +146 -0
- package/dist/translation.d.ts +125 -0
- package/dist/translation.js +15 -0
- package/dist/user-state.d.ts +132 -0
- package/dist/user-state.js +92 -0
- package/package.json +47 -2
package/dist/media-storage.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ export type PhisMediaObjectStreamInput = {
|
|
|
33
33
|
*/
|
|
34
34
|
signal?: AbortSignal;
|
|
35
35
|
};
|
|
36
|
+
/** Which bytes of an object somebody wants: the first one, and how many. */
|
|
37
|
+
export type PhisMediaObjectRange = {
|
|
38
|
+
/** The first byte, counted from zero and inside the object. */
|
|
39
|
+
offset: number;
|
|
40
|
+
/** How many bytes are wanted. An implementation may serve fewer, and then says so. */
|
|
41
|
+
length: number;
|
|
42
|
+
};
|
|
43
|
+
export type PhisMediaObjectRangeStream = {
|
|
44
|
+
body: ReadableStream<Uint8Array>;
|
|
45
|
+
/** What was served, which is what a `Content-Range` may claim -- never merely what was asked for. */
|
|
46
|
+
range: PhisMediaObjectRange;
|
|
47
|
+
/** The object's whole size, so a `Content-Range` can name the total it is a part of. */
|
|
48
|
+
byteSize: number;
|
|
49
|
+
};
|
|
36
50
|
export type PhisMediaObjectHead = {
|
|
37
51
|
/**
|
|
38
52
|
* What this Provider can attest about the object's content, if anything.
|
|
@@ -82,6 +96,34 @@ export type PhiMediaUploadPlan = {
|
|
|
82
96
|
method: "PUT";
|
|
83
97
|
headers: Record<string, string>;
|
|
84
98
|
expiresAt: string;
|
|
99
|
+
} | {
|
|
100
|
+
/**
|
|
101
|
+
* The body in several requests, assembled by the Provider.
|
|
102
|
+
*
|
|
103
|
+
* What this buys is not a larger ceiling but a resumable one. A single request that fails at ninety
|
|
104
|
+
* per cent has to be sent again from the first byte, and above a few hundred megabytes on an
|
|
105
|
+
* ordinary connection that is the usual outcome rather than the unlucky one. Each part here fails
|
|
106
|
+
* and repeats on its own.
|
|
107
|
+
*
|
|
108
|
+
* The parts are in order and each covers `partSizeBytes`, except the last, which covers what is
|
|
109
|
+
* left. That is the whole addressing scheme: part *n* is the bytes from `(n - 1) * partSizeBytes`,
|
|
110
|
+
* and a Client that can slice a file needs nothing else.
|
|
111
|
+
*
|
|
112
|
+
* `uploadId` travels out and comes back. The Client returns it, with each part's entity tag, as the
|
|
113
|
+
* `completion` that `completeUpload` assembles from -- and Core keeps its own copy, because the one
|
|
114
|
+
* thing a Client cannot be asked to do is report an upload it has stopped existing to report.
|
|
115
|
+
*/
|
|
116
|
+
kind: "multipart-put";
|
|
117
|
+
method: "PUT";
|
|
118
|
+
uploadId: string;
|
|
119
|
+
partSizeBytes: number;
|
|
120
|
+
parts: readonly {
|
|
121
|
+
/** One-based and contiguous, the way every part API counts. */
|
|
122
|
+
partNumber: number;
|
|
123
|
+
url: string;
|
|
124
|
+
headers?: Record<string, string>;
|
|
125
|
+
}[];
|
|
126
|
+
expiresAt: string;
|
|
85
127
|
};
|
|
86
128
|
export type PhiMediaUploadPlanKindKey = PhiMediaUploadPlan["kind"];
|
|
87
129
|
export type PhiMediaUploadPlanInput = {
|
|
@@ -100,10 +142,13 @@ export type PhiMediaUploadPlanInput = {
|
|
|
100
142
|
* What the client says it is about to send, and under which algorithm, as lowercase hex.
|
|
101
143
|
*
|
|
102
144
|
* Present only for a Profile whose probe found the endpoint verifies that algorithm: an adapter that
|
|
103
|
-
* receives it must sign it into the request
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
145
|
+
* receives it must sign it into the request, so a body that does not match is refused there and never
|
|
146
|
+
* becomes an object. That refusal is what makes the figure trustworthy -- Core records the client's
|
|
147
|
+
* number because a wrong one would not have got this far, not because the client is believed.
|
|
148
|
+
*
|
|
149
|
+
* Signed, and not also named as a header for the Client to repeat. Signing a request that has no body
|
|
150
|
+
* yet puts the digest in the query string, where the signature covers it; a checksum header outside the
|
|
151
|
+
* signed set is refused by a strict endpoint, so naming it costs the upload instead of securing it.
|
|
107
152
|
*
|
|
108
153
|
* The algorithm travels with the value because the adapter does not get to pick one. It was settled
|
|
109
154
|
* for this Profile when it was probed, and a plan that quietly used another would produce a digest
|
|
@@ -116,6 +161,113 @@ export type PhiMediaUploadPlanInput = {
|
|
|
116
161
|
algorithm: PhisMediaChecksumAlgorithm;
|
|
117
162
|
value: string;
|
|
118
163
|
};
|
|
164
|
+
/**
|
|
165
|
+
* One digest per part, in part order, as lowercase hex, for a body that is to arrive in parts.
|
|
166
|
+
*
|
|
167
|
+
* The same bargain as `checksum` and for the same reason, one level down: each part's digest is signed
|
|
168
|
+
* into that part's request, so a part whose bytes do not match is refused and never joins the object.
|
|
169
|
+
* Present only together with `mayAssembleFromParts`, and only where the Profile's base algorithm is
|
|
170
|
+
* SHA-256 -- these are SHA-256 digests and an adapter must not offer them under another name.
|
|
171
|
+
*
|
|
172
|
+
* The divisions are `resolvePhiMediaUploadPartSizeBytes` over the same `sizeBytes`, which is how the
|
|
173
|
+
* Client could compute them before this plan existed. An adapter that receives a count not matching that
|
|
174
|
+
* rule must refuse the plan rather than address the parts it was given: the mismatch means the two sides
|
|
175
|
+
* divided the body differently, and every digest after the first would be signed against other bytes.
|
|
176
|
+
*
|
|
177
|
+
* `checksum` alongside this carries the composite these add up to, which is what Core records. The
|
|
178
|
+
* adapter does not need it and must not recompute it.
|
|
179
|
+
*/
|
|
180
|
+
partChecksums?: readonly string[];
|
|
181
|
+
/**
|
|
182
|
+
* Whether this Profile may answer with an object assembled from parts.
|
|
183
|
+
*
|
|
184
|
+
* Core's decision and not the Provider's, for the same reason `checksum` above is: a Provider does not
|
|
185
|
+
* get to grade its own endpoint. What Core weighs is the size -- a single request that fails at ninety
|
|
186
|
+
* per cent is sent again from the first byte, and above a few hundred megabytes that is the usual
|
|
187
|
+
* outcome -- against what the object will be able to attest afterwards.
|
|
188
|
+
*
|
|
189
|
+
* It is a permission and not a capability: the Provider may be perfectly able and still not be asked.
|
|
190
|
+
*
|
|
191
|
+
* Absent means no. A Provider that cannot assemble parts ignores it either way.
|
|
192
|
+
*/
|
|
193
|
+
mayAssembleFromParts?: boolean;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* What is needed to carry on delivering an assembly that was already opened.
|
|
197
|
+
*
|
|
198
|
+
* The case is a page that went away mid-upload -- a reload, a crash, a tab somebody closed. The parts that
|
|
199
|
+
* did arrive are at the Provider, paid for and perfectly good, and until now the only thing that could
|
|
200
|
+
* happen to them was to be aborted: the plan was gone with the page, and asking for a new one opens a
|
|
201
|
+
* second assembly the first one's parts do not belong to. Which is why this is not `createUploadPlan` with
|
|
202
|
+
* an extra field. `createUploadPlan` *opens* an upload; this one joins the one that is open.
|
|
203
|
+
*
|
|
204
|
+
* It is deliberately not "list the parts and let Core sign what is missing": which parts an assembly holds
|
|
205
|
+
* and which addresses those parts have are the same Provider's business, and splitting them would put Core
|
|
206
|
+
* in the position of deciding what is missing from a listing it cannot interpret.
|
|
207
|
+
*
|
|
208
|
+
* `partChecksums` carries **every** part's digest, in part order, exactly as `createUploadPlan` received it
|
|
209
|
+
* -- the implementation signs the ones it is about to ask for. Anything else would make Core work out the
|
|
210
|
+
* gap before it knows what the gap is.
|
|
211
|
+
*/
|
|
212
|
+
export type PhiMediaUploadResumeInput = {
|
|
213
|
+
storageKey: string;
|
|
214
|
+
/** The Provider's own name for the assembly, which the session kept because the Client could not. */
|
|
215
|
+
uploadId: string;
|
|
216
|
+
contentType: string;
|
|
217
|
+
sizeBytes: number;
|
|
218
|
+
expiresAt: string;
|
|
219
|
+
/** One digest per part, in part order, as lowercase hex. The same bargain as on a fresh plan. */
|
|
220
|
+
partChecksums?: readonly string[];
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* An assembly as it stands: what is still to be sent, and what is already there.
|
|
224
|
+
*
|
|
225
|
+
* The two halves travel together because a completion has to name every part. The Client brings back the
|
|
226
|
+
* tags of the parts it sent, and these are the tags of the parts an earlier page sent -- nobody else has
|
|
227
|
+
* them, and the Client that had them is the thing that went away.
|
|
228
|
+
*/
|
|
229
|
+
export type PhiMediaUploadResumption = {
|
|
230
|
+
/** Addresses for the parts the storage does not hold yet. Empty where the body is complete. */
|
|
231
|
+
plan: Extract<PhiMediaUploadPlan, {
|
|
232
|
+
kind: "multipart-put";
|
|
233
|
+
}>;
|
|
234
|
+
/** The parts it does hold, in part order. */
|
|
235
|
+
uploaded: readonly {
|
|
236
|
+
partNumber: number;
|
|
237
|
+
/** As the storage reports it, quoting and all: a completion has to name it exactly. */
|
|
238
|
+
eTag: string;
|
|
239
|
+
sizeBytes: number;
|
|
240
|
+
}[];
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* What a storage is asked to clean up on its own, and how long it must wait first.
|
|
244
|
+
*
|
|
245
|
+
* Both figures follow from how long an upload session may live, which is Core's to know. They are days
|
|
246
|
+
* because that is the only unit an object lifecycle rule takes, and they are more than one day because such
|
|
247
|
+
* a rule is evaluated asynchronously -- a rule of "one day" can fire on an upload that still has hours to
|
|
248
|
+
* run, and aborting a body somebody is in the middle of sending is worse than paying for it a while longer.
|
|
249
|
+
*
|
|
250
|
+
* `stagingKeyPrefixes` are Core's key space, without whatever prefix a Provider adds of its own; the
|
|
251
|
+
* Provider prepends that the same way it does for an object key.
|
|
252
|
+
*
|
|
253
|
+
* **An empty prefix list means no expiry rule at all, and an implementation must treat it that way.** An
|
|
254
|
+
* expiry rule matching everything would delete every object in the storage after two days, which is the one
|
|
255
|
+
* mistake in this area that cannot be undone -- so "expire nothing" and "expire everything" must never be
|
|
256
|
+
* the same argument.
|
|
257
|
+
*/
|
|
258
|
+
export type PhisMediaLifecyclePolicy = {
|
|
259
|
+
/**
|
|
260
|
+
* Days after an upload was started before the storage may abort what was never completed.
|
|
261
|
+
*
|
|
262
|
+
* This is the whole reason the rule exists. A Client that reports its own failure is handled without any
|
|
263
|
+
* of this, and one that crashes, loses its connection or has its tab killed reports nothing -- and what it
|
|
264
|
+
* leaves behind is parts that are paid for and cannot be listed.
|
|
265
|
+
*/
|
|
266
|
+
abortIncompleteMultipartUploadAfterDays: number;
|
|
267
|
+
/** Key prefixes, in Core's key space, under which a body is staged before it becomes an Asset. */
|
|
268
|
+
stagingKeyPrefixes: readonly string[];
|
|
269
|
+
/** Days a staged object may sit under those prefixes before the storage removes it. */
|
|
270
|
+
expireStagedObjectsAfterDays: number;
|
|
119
271
|
};
|
|
120
272
|
export type PhiMediaUploadCompletionInput = {
|
|
121
273
|
storageKey: string;
|
|
@@ -140,8 +292,9 @@ export type PhiMediaUploadCompletionInput = {
|
|
|
140
292
|
* an `ETag` is whatever a Provider decided to put there, and Providers disagree. Every recorded digest
|
|
141
293
|
* therefore carries the algorithm that produced it.
|
|
142
294
|
*/
|
|
143
|
-
export declare const PHIS_MEDIA_CHECKSUM_ALGORITHMS: readonly ["sha256", "sha512", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5"];
|
|
295
|
+
export declare const PHIS_MEDIA_CHECKSUM_ALGORITHMS: readonly ["sha256", "sha512", "sha256-composite", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5"];
|
|
144
296
|
export type PhisMediaChecksumAlgorithm = (typeof PHIS_MEDIA_CHECKSUM_ALGORITHMS)[number];
|
|
297
|
+
export { PHIS_MEDIA_UPLOAD_MAX_PART_COUNT, PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES, formatPhiMediaCompositeChecksum, parsePhiMediaCompositeChecksum, resolvePhiMediaUploadPartCount, resolvePhiMediaUploadPartSizeBytes, } from "./media.js";
|
|
145
298
|
/**
|
|
146
299
|
* The digests strong enough to answer "is this the same file", rather than only "did it arrive intact".
|
|
147
300
|
*
|
|
@@ -154,6 +307,19 @@ export type PhisMediaChecksumAlgorithm = (typeof PHIS_MEDIA_CHECKSUM_ALGORITHMS)
|
|
|
154
307
|
* Space can stop one file from entering it. Small, but not nothing, and free to avoid.
|
|
155
308
|
*/
|
|
156
309
|
export declare const PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS: readonly PhisMediaChecksumAlgorithm[];
|
|
310
|
+
/**
|
|
311
|
+
* Why `sha256-composite` is identity-grade although it hashes digests rather than bytes.
|
|
312
|
+
*
|
|
313
|
+
* It is `SHA256` over the concatenated part digests of a body divided by a fixed rule. For the question
|
|
314
|
+
* duplicate detection asks -- are these the same bytes -- it is as hard to forge as SHA-256 itself: a
|
|
315
|
+
* second file matching one requires a SHA-256 collision in some part. What it cannot do is compare across
|
|
316
|
+
* divisions, which is why the division is written into the value and why the rule above is frozen.
|
|
317
|
+
*
|
|
318
|
+
* It exists because a browser cannot produce the other kind for a large file. Web Crypto has no
|
|
319
|
+
* incremental digest, so a whole-object SHA-256 means holding the entire body in memory; a part digest is
|
|
320
|
+
* one part, and the composite over those is a few kilobytes. The choice is not between this and a
|
|
321
|
+
* whole-object digest, it is between this and no digest at all.
|
|
322
|
+
*/
|
|
157
323
|
export declare function isPhisMediaIdentityChecksum(algorithm: string | null | undefined): boolean;
|
|
158
324
|
/**
|
|
159
325
|
* Which of the digests an endpoint verifies a Profile will record.
|
|
@@ -165,6 +331,10 @@ export declare function isPhisMediaIdentityChecksum(algorithm: string | null | u
|
|
|
165
331
|
*
|
|
166
332
|
* Only identity-grade digests are eligible. A CRC an endpoint also verifies still proves a transfer
|
|
167
333
|
* intact, and Core records it when that is what arrived; it is simply not what a Profile asks for.
|
|
334
|
+
*
|
|
335
|
+
* `sha256-composite` is not eligible either, for a different reason: it is not something an endpoint
|
|
336
|
+
* verifies, it is the form a SHA-256 takes when a body arrived in parts. A Profile records a base
|
|
337
|
+
* algorithm, and an object assembled from parts under a `sha256` Profile carries the composite of it.
|
|
168
338
|
*/
|
|
169
339
|
export declare function choosePhisMediaChecksumAlgorithm(probe: {
|
|
170
340
|
verifiedAlgorithms: readonly PhisMediaChecksumAlgorithm[];
|
|
@@ -187,15 +357,6 @@ export type PhisMediaStorageProbe = {
|
|
|
187
357
|
* never be told to be the same.
|
|
188
358
|
*/
|
|
189
359
|
verifiedAlgorithms: PhisMediaChecksumAlgorithm[];
|
|
190
|
-
/**
|
|
191
|
-
* Whether a digest survives an object arriving in parts.
|
|
192
|
-
*
|
|
193
|
-
* A checksum signed into one PUT covers that PUT. Above the single-request limit an object is
|
|
194
|
-
* assembled from parts, and the usual answer is a digest of the part digests -- a number that hashes
|
|
195
|
-
* no bytes anyone uploaded. An endpoint that computes over the whole assembled object instead keeps
|
|
196
|
-
* the guarantee at any size; one that does not confines it to what fits in one request.
|
|
197
|
-
*/
|
|
198
|
-
verifiesWholeMultipartObject: boolean;
|
|
199
360
|
/** What did not answer, in an operator's terms. Empty when everything did. */
|
|
200
361
|
findings: string[];
|
|
201
362
|
};
|
|
@@ -210,7 +371,46 @@ export interface PhisMediaStorageAdapter {
|
|
|
210
371
|
probeCapabilities(): Promise<PhisMediaStorageProbe>;
|
|
211
372
|
putObject(input: PhisMediaObjectInput): Promise<PhisMediaObjectHead>;
|
|
212
373
|
putObjectStream(input: PhisMediaObjectStreamInput): Promise<PhisMediaObjectHead>;
|
|
374
|
+
/**
|
|
375
|
+
* The whole object at once, for code that has to look at every byte.
|
|
376
|
+
*
|
|
377
|
+
* Image processing, font subsetting and content sniffing are what this is for: they hold the bytes
|
|
378
|
+
* because they read them, and no stream would change that. Serving an object to somebody is not one
|
|
379
|
+
* of those cases -- see `getObjectStream`.
|
|
380
|
+
*/
|
|
213
381
|
getObject(storageKey: string): Promise<Buffer | null>;
|
|
382
|
+
/**
|
|
383
|
+
* The object as it arrives, for code that only passes it on.
|
|
384
|
+
*
|
|
385
|
+
* Delivery reads nothing: it copies bytes from the Provider to whoever asked, and holding the whole
|
|
386
|
+
* object to do that makes the largest object a Site accepts the amount of memory one download costs.
|
|
387
|
+
* A Provider that stores remotely hands back its own response body, and the Local adapter a file read
|
|
388
|
+
* stream, so nothing between the two ever holds more than what is in flight.
|
|
389
|
+
*
|
|
390
|
+
* `null` means the object is not there, the same answer `getObject` gives.
|
|
391
|
+
*/
|
|
392
|
+
getObjectStream(storageKey: string): Promise<ReadableStream<Uint8Array> | null>;
|
|
393
|
+
/**
|
|
394
|
+
* Part of an object as it arrives, for serving a `Range` request.
|
|
395
|
+
*
|
|
396
|
+
* What this is for is seeking. A browser asked for a video cannot start in the middle without it, and
|
|
397
|
+
* Safari commonly declines to play at all where a server answers `200` to a `Range` -- so delivery
|
|
398
|
+
* without this is delivery of files people download, not of media people scrub through.
|
|
399
|
+
*
|
|
400
|
+
* It reports the range it **actually** produced rather than echoing the one it was given, and that is
|
|
401
|
+
* the point of the shape. A `Content-Range` is a claim about bytes; a Provider that quietly ignored the
|
|
402
|
+
* request and returned the whole object would otherwise turn that claim into a lie, and the caller
|
|
403
|
+
* would frame a complete body as a partial one. Comparing what was asked with what came back is what
|
|
404
|
+
* lets delivery answer `200` in that case instead.
|
|
405
|
+
*
|
|
406
|
+
* `offset` is the first byte, `length` is how many are wanted. An implementation clamps `length` to the
|
|
407
|
+
* end of the object and says so in what it returns; the caller has already established that the offset
|
|
408
|
+
* is inside the object, because it knows the size and a range beyond it is a `416` rather than a read.
|
|
409
|
+
* A `length` of zero -- asked for, or left after clamping -- is an empty body and not an error.
|
|
410
|
+
*
|
|
411
|
+
* `null` means the object is not there, the same answer `getObjectStream` gives.
|
|
412
|
+
*/
|
|
413
|
+
getObjectRangeStream(storageKey: string, range: PhisMediaObjectRange): Promise<PhisMediaObjectRangeStream | null>;
|
|
214
414
|
/**
|
|
215
415
|
* The first bytes of an object, for deciding what it actually is without moving it.
|
|
216
416
|
*
|
|
@@ -237,6 +437,29 @@ export interface PhisMediaStorageAdapter {
|
|
|
237
437
|
listPrefix(prefix: string): Promise<PhisMediaObjectHead[]>;
|
|
238
438
|
/** States how the Client is to deliver this body. */
|
|
239
439
|
createUploadPlan(input: PhiMediaUploadPlanInput): Promise<PhiMediaUploadPlan>;
|
|
440
|
+
/**
|
|
441
|
+
* Says how what is left of an already opened assembly is to be delivered.
|
|
442
|
+
*
|
|
443
|
+
* This is what makes a large upload survive the page it was started from. A part that fails repeats
|
|
444
|
+
* alone already, but a reload used to cost every part that had arrived -- the plan lived in the tab and
|
|
445
|
+
* the parts were aborted the moment the page said it was leaving. The parts are the expensive thing;
|
|
446
|
+
* they are at the Provider and nothing is wrong with them.
|
|
447
|
+
*
|
|
448
|
+
* The Provider answers, not Core, because only the Provider knows what it is holding. A Client's account
|
|
449
|
+
* of how far it got is a claim about a session that has stopped existing, and a Core-side tally would be
|
|
450
|
+
* a second record of the same fact that can disagree with the storage.
|
|
451
|
+
*
|
|
452
|
+
* `null` means there is nothing to carry on: the assembly was aborted, or a lifecycle rule has reached
|
|
453
|
+
* it, or it was never this storage's. It also means that for a Provider that never assembles anything --
|
|
454
|
+
* a no-op answer and not a shortcoming, the same way `applyCorsPolicy` is a no-op where no browser ever
|
|
455
|
+
* addresses the storage.
|
|
456
|
+
*
|
|
457
|
+
* An implementation must refuse -- `null` -- where what it holds was divided differently from what it is
|
|
458
|
+
* being asked to sign: a part of the wrong size means the object would assemble into something other
|
|
459
|
+
* than the body Core recorded a digest for, and finishing it would put a correct digest on the wrong
|
|
460
|
+
* bytes. Starting over is the honest outcome there.
|
|
461
|
+
*/
|
|
462
|
+
resumeUploadPlan(input: PhiMediaUploadResumeInput): Promise<PhiMediaUploadResumption | null>;
|
|
240
463
|
/**
|
|
241
464
|
* Settles the staged object once the Client says it is done, and answers what actually landed.
|
|
242
465
|
*
|
|
@@ -256,6 +479,25 @@ export interface PhisMediaStorageAdapter {
|
|
|
256
479
|
* to check, because a Provider that cannot say no to a body it never receives has nothing to say.
|
|
257
480
|
*/
|
|
258
481
|
applyCorsPolicy(origins: readonly string[]): Promise<void>;
|
|
482
|
+
/**
|
|
483
|
+
* States what the storage is to clean up by itself, because Core cannot see it and never will.
|
|
484
|
+
*
|
|
485
|
+
* The reason this is not a sweeper is that the two things it names are invisible to the side that would
|
|
486
|
+
* have to sweep them. An unfinished multipart upload is billed from the moment it opens and appears in no
|
|
487
|
+
* object listing at all: only the storage knows it exists, and only the Client that has gone away could
|
|
488
|
+
* have named it. A staged object whose session expired is findable, but reaching it means opening
|
|
489
|
+
* Providers on behalf of sessions a request has no business touching -- which is why expiry deliberately
|
|
490
|
+
* releases the reservation and leaves storage alone.
|
|
491
|
+
*
|
|
492
|
+
* So the rule is written once, when a Profile is written, exactly as the CORS rule is. What Core knows is
|
|
493
|
+
* how long an upload may take and which keys are staging; what the storage knows is how to express "stop
|
|
494
|
+
* paying for this".
|
|
495
|
+
*
|
|
496
|
+
* Replaced whole, never merged, for the same reason CORS is: a rule has to state what is true now rather
|
|
497
|
+
* than accumulate what once was. A Provider with no such notion, such as the Local one, implements it as
|
|
498
|
+
* a no-op -- its files are Core's own and an operator command is what removes them.
|
|
499
|
+
*/
|
|
500
|
+
applyLifecyclePolicy(policy: PhisMediaLifecyclePolicy): Promise<void>;
|
|
259
501
|
/**
|
|
260
502
|
* What makes two Storage Profiles the same physical storage.
|
|
261
503
|
*
|
package/dist/media-storage.js
CHANGED
|
@@ -26,8 +26,17 @@ export const PHI_LOCAL_MEDIA_STORAGE_PROVIDER_ID = "@phis/server/storage-local";
|
|
|
26
26
|
* therefore carries the algorithm that produced it.
|
|
27
27
|
*/
|
|
28
28
|
export const PHIS_MEDIA_CHECKSUM_ALGORITHMS = [
|
|
29
|
-
"sha256", "sha512", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5",
|
|
29
|
+
"sha256", "sha512", "sha256-composite", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5",
|
|
30
30
|
];
|
|
31
|
+
/*
|
|
32
|
+
* The division and the composite's recorded form live in `./media.ts`, and are re-exported here.
|
|
33
|
+
*
|
|
34
|
+
* They are the one part of this contract a browser has to compute as well: the Client hashes the parts
|
|
35
|
+
* before a plan exists, because a part digest can only be signed into a part request if it is known when
|
|
36
|
+
* that request is signed. So the rule cannot sit behind the Add-on surface, and it must not be written
|
|
37
|
+
* twice -- two copies of a division are two divisions the day one of them is edited.
|
|
38
|
+
*/
|
|
39
|
+
export { PHIS_MEDIA_UPLOAD_MAX_PART_COUNT, PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES, formatPhiMediaCompositeChecksum, parsePhiMediaCompositeChecksum, resolvePhiMediaUploadPartCount, resolvePhiMediaUploadPartSizeBytes, } from "./media.js";
|
|
31
40
|
/**
|
|
32
41
|
* The digests strong enough to answer "is this the same file", rather than only "did it arrive intact".
|
|
33
42
|
*
|
|
@@ -39,7 +48,20 @@ export const PHIS_MEDIA_CHECKSUM_ALGORITHMS = [
|
|
|
39
48
|
* substituted file -- so the cost of trusting a weak digest is that someone who may already write to a
|
|
40
49
|
* Space can stop one file from entering it. Small, but not nothing, and free to avoid.
|
|
41
50
|
*/
|
|
42
|
-
export const PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS = ["sha256", "sha512"];
|
|
51
|
+
export const PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS = ["sha256", "sha512", "sha256-composite"];
|
|
52
|
+
/**
|
|
53
|
+
* Why `sha256-composite` is identity-grade although it hashes digests rather than bytes.
|
|
54
|
+
*
|
|
55
|
+
* It is `SHA256` over the concatenated part digests of a body divided by a fixed rule. For the question
|
|
56
|
+
* duplicate detection asks -- are these the same bytes -- it is as hard to forge as SHA-256 itself: a
|
|
57
|
+
* second file matching one requires a SHA-256 collision in some part. What it cannot do is compare across
|
|
58
|
+
* divisions, which is why the division is written into the value and why the rule above is frozen.
|
|
59
|
+
*
|
|
60
|
+
* It exists because a browser cannot produce the other kind for a large file. Web Crypto has no
|
|
61
|
+
* incremental digest, so a whole-object SHA-256 means holding the entire body in memory; a part digest is
|
|
62
|
+
* one part, and the composite over those is a few kilobytes. The choice is not between this and a
|
|
63
|
+
* whole-object digest, it is between this and no digest at all.
|
|
64
|
+
*/
|
|
43
65
|
export function isPhisMediaIdentityChecksum(algorithm) {
|
|
44
66
|
return PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS.includes(algorithm);
|
|
45
67
|
}
|
|
@@ -53,11 +75,17 @@ export function isPhisMediaIdentityChecksum(algorithm) {
|
|
|
53
75
|
*
|
|
54
76
|
* Only identity-grade digests are eligible. A CRC an endpoint also verifies still proves a transfer
|
|
55
77
|
* intact, and Core records it when that is what arrived; it is simply not what a Profile asks for.
|
|
78
|
+
*
|
|
79
|
+
* `sha256-composite` is not eligible either, for a different reason: it is not something an endpoint
|
|
80
|
+
* verifies, it is the form a SHA-256 takes when a body arrived in parts. A Profile records a base
|
|
81
|
+
* algorithm, and an object assembled from parts under a `sha256` Profile carries the composite of it.
|
|
56
82
|
*/
|
|
57
83
|
export function choosePhisMediaChecksumAlgorithm(probe) {
|
|
58
84
|
if (!probe)
|
|
59
85
|
return null;
|
|
60
86
|
for (const candidate of PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS) {
|
|
87
|
+
if (candidate === "sha256-composite")
|
|
88
|
+
continue;
|
|
61
89
|
if (probe.verifiedAlgorithms.includes(candidate))
|
|
62
90
|
return candidate;
|
|
63
91
|
}
|
package/dist/media.d.ts
CHANGED
|
@@ -104,9 +104,9 @@ export type PhiImageAssetVariantSpec = {
|
|
|
104
104
|
quality: number;
|
|
105
105
|
format: "webp";
|
|
106
106
|
};
|
|
107
|
-
export declare function normalizePhiMediaKind(kind: string | null | undefined): "
|
|
108
|
-
export declare function resolvePhiMediaKindFromContentType(contentType: string | null | undefined): "
|
|
109
|
-
export declare function normalizePhiImageAssetVariantKey(key: unknown): 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|
|
107
|
+
export declare function normalizePhiMediaKind(kind: string | null | undefined): "image" | "video" | "audio" | "pdf" | "markdown" | "document" | "archive" | "font" | "binary" | "other";
|
|
108
|
+
export declare function resolvePhiMediaKindFromContentType(contentType: string | null | undefined): "image" | "video" | "audio" | "pdf" | "markdown" | "document" | "archive" | "font" | "binary" | "other";
|
|
109
|
+
export declare function normalizePhiImageAssetVariantKey(key: unknown): 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 8 | null;
|
|
110
110
|
export declare function resolvePhiImageAssetVariantKeyName(key: number | null | undefined): string | null;
|
|
111
111
|
export declare function resolvePhiImageAssetVariantSpec(key: number | null | undefined): PhiImageAssetVariantSpec | null;
|
|
112
112
|
/**
|
|
@@ -226,3 +226,47 @@ export declare function isPhiFontSubsetKey(value: unknown): value is PhiFontSubs
|
|
|
226
226
|
* `LatinExt` where `sr` preloads `Cyrillic`.
|
|
227
227
|
*/
|
|
228
228
|
export declare function resolvePhiFontPreloadSubsetKeys(locale: string | null | undefined, coverage: PhiFontCoverage): PhiFontSubsetKeyValue[];
|
|
229
|
+
/**
|
|
230
|
+
* How a body above one part is divided, as a rule rather than as a Provider's private arithmetic.
|
|
231
|
+
*
|
|
232
|
+
* It lives here because two parties have to agree on it and neither can be told by the other in time. The
|
|
233
|
+
* Provider needs the part size to sign one request per part; the Client needs it *before* the plan exists,
|
|
234
|
+
* because a digest can only be signed into a request if it is known when the request is signed -- so the
|
|
235
|
+
* Client has to hash the same divisions the Provider is about to address. A rule both compute from the
|
|
236
|
+
* byte size is the only arrangement where that holds without an extra round trip.
|
|
237
|
+
*
|
|
238
|
+
* **Frozen once objects exist under it.** A `sha256-composite` value is a digest of part digests, so it
|
|
239
|
+
* answers "the same bytes divided the same way". Change the division and the same file yields a different
|
|
240
|
+
* value, which splits duplicate detection into cohorts that can never match across the change. The value
|
|
241
|
+
* carries the part size for exactly this reason: a changed rule produces non-matches, never false matches.
|
|
242
|
+
* Changing it is therefore a decision about the whole corpus, not a tuning knob.
|
|
243
|
+
*
|
|
244
|
+
* `PHIS_MEDIA_UPLOAD_MAX_PART_COUNT` is well under S3's ten thousand on purpose. Every part is signed up
|
|
245
|
+
* front, so the count is also the size of the plan a Client receives and Core holds in memory.
|
|
246
|
+
*/
|
|
247
|
+
export declare const PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES: number;
|
|
248
|
+
export declare const PHIS_MEDIA_UPLOAD_MAX_PART_COUNT = 1000;
|
|
249
|
+
/**
|
|
250
|
+
* The part size for one body, and the same answer wherever it is asked.
|
|
251
|
+
*
|
|
252
|
+
* Sixteen mebibytes until that would need more parts than are allowed, and from there the smallest whole
|
|
253
|
+
* mebibyte that fits the body into the allowance. Whole mebibytes keep the number short where it is
|
|
254
|
+
* recorded and keep the arithmetic exact in a browser, where sizes are doubles.
|
|
255
|
+
*/
|
|
256
|
+
export declare function resolvePhiMediaUploadPartSizeBytes(sizeBytes: number): number;
|
|
257
|
+
/** How many parts one body is divided into under `resolvePhiMediaUploadPartSizeBytes`. */
|
|
258
|
+
export declare function resolvePhiMediaUploadPartCount(sizeBytes: number): number;
|
|
259
|
+
/**
|
|
260
|
+
* The recorded form of a `sha256-composite` digest: the digest, the part count, the part size.
|
|
261
|
+
*
|
|
262
|
+
* Duplicate detection compares these values as strings, so everything that decides whether two of them
|
|
263
|
+
* mean the same thing has to be inside the string. The digest alone would not: the same bytes divided
|
|
264
|
+
* differently hash differently, and a value that did not say how it was divided would look like a plain
|
|
265
|
+
* SHA-256 that simply failed to match. Naming the division makes a mismatch legible instead of puzzling.
|
|
266
|
+
*/
|
|
267
|
+
export declare function formatPhiMediaCompositeChecksum(digestHex: string, partCount: number, partSizeBytes: number): string;
|
|
268
|
+
export declare function parsePhiMediaCompositeChecksum(value: string): {
|
|
269
|
+
digest: string;
|
|
270
|
+
partCount: number;
|
|
271
|
+
partSizeBytes: number;
|
|
272
|
+
} | null;
|
package/dist/media.js
CHANGED
|
@@ -360,3 +360,62 @@ export function resolvePhiFontPreloadSubsetKeys(locale, coverage) {
|
|
|
360
360
|
: PHI_FONT_SUBSET_PRELOAD_BY_LANGUAGE[language] ?? [];
|
|
361
361
|
return [PhiFontSubsetKey.Latin, ...extra].filter((key) => coverage.subsets.includes(key));
|
|
362
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* How a body above one part is divided, as a rule rather than as a Provider's private arithmetic.
|
|
365
|
+
*
|
|
366
|
+
* It lives here because two parties have to agree on it and neither can be told by the other in time. The
|
|
367
|
+
* Provider needs the part size to sign one request per part; the Client needs it *before* the plan exists,
|
|
368
|
+
* because a digest can only be signed into a request if it is known when the request is signed -- so the
|
|
369
|
+
* Client has to hash the same divisions the Provider is about to address. A rule both compute from the
|
|
370
|
+
* byte size is the only arrangement where that holds without an extra round trip.
|
|
371
|
+
*
|
|
372
|
+
* **Frozen once objects exist under it.** A `sha256-composite` value is a digest of part digests, so it
|
|
373
|
+
* answers "the same bytes divided the same way". Change the division and the same file yields a different
|
|
374
|
+
* value, which splits duplicate detection into cohorts that can never match across the change. The value
|
|
375
|
+
* carries the part size for exactly this reason: a changed rule produces non-matches, never false matches.
|
|
376
|
+
* Changing it is therefore a decision about the whole corpus, not a tuning knob.
|
|
377
|
+
*
|
|
378
|
+
* `PHIS_MEDIA_UPLOAD_MAX_PART_COUNT` is well under S3's ten thousand on purpose. Every part is signed up
|
|
379
|
+
* front, so the count is also the size of the plan a Client receives and Core holds in memory.
|
|
380
|
+
*/
|
|
381
|
+
export const PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
382
|
+
export const PHIS_MEDIA_UPLOAD_MAX_PART_COUNT = 1000;
|
|
383
|
+
/**
|
|
384
|
+
* The part size for one body, and the same answer wherever it is asked.
|
|
385
|
+
*
|
|
386
|
+
* Sixteen mebibytes until that would need more parts than are allowed, and from there the smallest whole
|
|
387
|
+
* mebibyte that fits the body into the allowance. Whole mebibytes keep the number short where it is
|
|
388
|
+
* recorded and keep the arithmetic exact in a browser, where sizes are doubles.
|
|
389
|
+
*/
|
|
390
|
+
export function resolvePhiMediaUploadPartSizeBytes(sizeBytes) {
|
|
391
|
+
const mebibyte = 1024 * 1024;
|
|
392
|
+
if (sizeBytes <= PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES * PHIS_MEDIA_UPLOAD_MAX_PART_COUNT) {
|
|
393
|
+
return PHIS_MEDIA_UPLOAD_PART_SIZE_BYTES;
|
|
394
|
+
}
|
|
395
|
+
return Math.ceil(Math.ceil(sizeBytes / PHIS_MEDIA_UPLOAD_MAX_PART_COUNT) / mebibyte) * mebibyte;
|
|
396
|
+
}
|
|
397
|
+
/** How many parts one body is divided into under `resolvePhiMediaUploadPartSizeBytes`. */
|
|
398
|
+
export function resolvePhiMediaUploadPartCount(sizeBytes) {
|
|
399
|
+
return Math.max(1, Math.ceil(sizeBytes / resolvePhiMediaUploadPartSizeBytes(sizeBytes)));
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The recorded form of a `sha256-composite` digest: the digest, the part count, the part size.
|
|
403
|
+
*
|
|
404
|
+
* Duplicate detection compares these values as strings, so everything that decides whether two of them
|
|
405
|
+
* mean the same thing has to be inside the string. The digest alone would not: the same bytes divided
|
|
406
|
+
* differently hash differently, and a value that did not say how it was divided would look like a plain
|
|
407
|
+
* SHA-256 that simply failed to match. Naming the division makes a mismatch legible instead of puzzling.
|
|
408
|
+
*/
|
|
409
|
+
export function formatPhiMediaCompositeChecksum(digestHex, partCount, partSizeBytes) {
|
|
410
|
+
return `${digestHex.toLowerCase()}-${partCount}-${partSizeBytes}`;
|
|
411
|
+
}
|
|
412
|
+
export function parsePhiMediaCompositeChecksum(value) {
|
|
413
|
+
const match = /^([0-9a-f]{64})-(\d{1,5})-(\d{1,12})$/u.exec(value.trim().toLowerCase());
|
|
414
|
+
if (!match)
|
|
415
|
+
return null;
|
|
416
|
+
const partCount = Number(match[2]);
|
|
417
|
+
const partSizeBytes = Number(match[3]);
|
|
418
|
+
if (partCount < 1 || partSizeBytes < 1)
|
|
419
|
+
return null;
|
|
420
|
+
return { digest: match[1], partCount, partSizeBytes };
|
|
421
|
+
}
|
package/dist/record.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function isPhiRecord(value: unknown): value is Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* The value a dotted path names inside a record: `"author.name"` reads `input.author.name`.
|
|
4
|
+
*
|
|
5
|
+
* Empty segments are skipped, so `""` names the input itself, and only records are walked -- an array
|
|
6
|
+
* or a primitive on the way ends the walk with `undefined` rather than being indexed into.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readPhiDotPath(input: unknown, path: string): unknown;
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared by the parsers of this package and not exported from it: each consumer keeps its own, and
|
|
3
|
+
* the one question they all answer is settled the same way -- an array is not a keyed record.
|
|
4
|
+
*/
|
|
5
|
+
export function isPhiRecord(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The value a dotted path names inside a record: `"author.name"` reads `input.author.name`.
|
|
10
|
+
*
|
|
11
|
+
* Empty segments are skipped, so `""` names the input itself, and only records are walked -- an array
|
|
12
|
+
* or a primitive on the way ends the walk with `undefined` rather than being indexed into.
|
|
13
|
+
*/
|
|
14
|
+
export function readPhiDotPath(input, path) {
|
|
15
|
+
let current = input;
|
|
16
|
+
for (const segment of path.split(".")) {
|
|
17
|
+
if (!segment)
|
|
18
|
+
continue;
|
|
19
|
+
if (!isPhiRecord(current))
|
|
20
|
+
return undefined;
|
|
21
|
+
current = current[segment];
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|