@phis/contracts 0.3.1 → 0.4.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/core.d.ts +38 -0
- package/dist/core.js +39 -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/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -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 +22 -2
package/dist/capabilities.d.ts
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* at the point where it made the mistake.
|
|
13
13
|
*/
|
|
14
14
|
import type { PhisAssetsCapabilityV1 } from "./assets.js";
|
|
15
|
+
import type { PhisAddonEventId } from "./core.js";
|
|
15
16
|
import type { PhisDataCapabilityV1 } from "./queries.js";
|
|
17
|
+
import type { PhisResourceLinksCapabilityV1 } from "./resource-links.js";
|
|
18
|
+
import type { PhisSupportCapabilityV1 } from "./support.js";
|
|
19
|
+
import type { PhisThreadsCapabilityV1 } from "./threads.js";
|
|
16
20
|
/**
|
|
17
21
|
* The cumulative group levels, by name.
|
|
18
22
|
*
|
|
@@ -94,6 +98,8 @@ export type PhisAddonCapabilities = {
|
|
|
94
98
|
secrets?: PhisSecretsCapabilityV1;
|
|
95
99
|
groups?: PhisGroupsCapabilityV1;
|
|
96
100
|
threads?: PhisThreadsCapabilityV1;
|
|
101
|
+
resourceLinks?: PhisResourceLinksCapabilityV1;
|
|
102
|
+
support?: PhisSupportCapabilityV1;
|
|
97
103
|
settings?: PhisSettingsCapabilityV1;
|
|
98
104
|
roles?: PhisRolesCapabilityV1;
|
|
99
105
|
};
|
|
@@ -128,17 +134,38 @@ export type PhisRolesCapabilityV1 = {
|
|
|
128
134
|
* itself: a release artifact, a generated preview, an export waiting to be fetched.
|
|
129
135
|
*/
|
|
130
136
|
export type PhisStorageCapabilityV1 = {
|
|
137
|
+
/**
|
|
138
|
+
* Both directions are streams, because two of the three things this store is for -- a release
|
|
139
|
+
* artifact and an export waiting to be fetched -- are the things that grow. A buffer would make the
|
|
140
|
+
* Site's largest allowed object the memory one call costs, on a path that only moves bytes.
|
|
141
|
+
*
|
|
142
|
+
* `byteSize` is declared rather than measured. The store's ceiling is booked before a byte is
|
|
143
|
+
* written, so that a write arriving at the same time as another cannot find room that the other
|
|
144
|
+
* already took, and nothing can be booked that has not been stated. Core counts while it writes and
|
|
145
|
+
* refuses a body that turns out not to be the size it claimed, so the declaration is a promise the
|
|
146
|
+
* Add-on is held to rather than one it is trusted on.
|
|
147
|
+
*
|
|
148
|
+
* An Add-on that already holds its bytes wraps them in a one-chunk `ReadableStream` and states
|
|
149
|
+
* `bytes.byteLength`.
|
|
150
|
+
*/
|
|
131
151
|
put(input: {
|
|
132
152
|
key: string;
|
|
133
|
-
body: Uint8Array
|
|
153
|
+
body: ReadableStream<Uint8Array>;
|
|
154
|
+
byteSize: number;
|
|
134
155
|
contentType: string;
|
|
135
156
|
}): Promise<{
|
|
136
157
|
key: string;
|
|
137
158
|
byteSize: number;
|
|
138
159
|
}>;
|
|
139
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Null rather than throwing for an object that is not there.
|
|
162
|
+
*
|
|
163
|
+
* The body arrives as it is read. `byteSize` comes with it so a caller that does need every byte can
|
|
164
|
+
* decide before it starts collecting them.
|
|
165
|
+
*/
|
|
140
166
|
get(key: string): Promise<{
|
|
141
|
-
body: Uint8Array
|
|
167
|
+
body: ReadableStream<Uint8Array>;
|
|
168
|
+
byteSize: number;
|
|
142
169
|
contentType: string;
|
|
143
170
|
} | null>;
|
|
144
171
|
head(key: string): Promise<{
|
|
@@ -323,93 +350,33 @@ export type PhisAddonJobContext = {
|
|
|
323
350
|
signal: AbortSignal;
|
|
324
351
|
};
|
|
325
352
|
/**
|
|
326
|
-
*
|
|
353
|
+
* One event Core hands an Add-on after the fact.
|
|
327
354
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
355
|
+
* A handler runs with no actor -- nobody called it, something happened -- and reaches exactly what the
|
|
356
|
+
* link that caused the delivery reaches. It is the counterpart of a hook: a hook is the outside telling
|
|
357
|
+
* this Site something, an event is this Site telling an Add-on something, and both run as the Add-on
|
|
358
|
+
* rather than as a person.
|
|
332
359
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* it goes -- which is what an integration reporting an outside event needs. How such an integration
|
|
337
|
-
* later appends to the thread it opened is deliberately left unanswered here: it wants a thread the
|
|
338
|
-
* Add-on owns, and ownership of a thread is a thing to design once something real needs it rather than
|
|
339
|
-
* to guess at now.
|
|
360
|
+
* Delivery is bounded by the same link. An Add-on is told about a thread it is synced with and about no
|
|
361
|
+
* other, because a Site-wide feed of everything anybody writes is the listing `threads:v1` refuses,
|
|
362
|
+
* arriving by push instead of by pull.
|
|
340
363
|
*/
|
|
341
|
-
export type
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}): Promise<PhisThreadDetail | null>;
|
|
353
|
-
/**
|
|
354
|
-
* Opens a thread.
|
|
355
|
-
*
|
|
356
|
-
* The one call an Add-on may make with no actor behind it. Recipients are named as user ids, team
|
|
357
|
-
* ids, or both, and every one is checked against this Site before anything is written.
|
|
358
|
-
*/
|
|
359
|
-
create(input: {
|
|
360
|
-
subject?: string | null;
|
|
361
|
-
message: string;
|
|
362
|
-
messageMarkdown?: string | null;
|
|
363
|
-
recipientUserIds?: number[];
|
|
364
|
-
recipientTeamIds?: number[];
|
|
365
|
-
}): Promise<PhisThreadDetail>;
|
|
366
|
-
/** Adds a message to a thread the acting user is in. */
|
|
367
|
-
reply(input: {
|
|
368
|
-
threadId: number;
|
|
369
|
-
message: string;
|
|
370
|
-
messageMarkdown?: string | null;
|
|
371
|
-
}): Promise<PhisThreadDetail | null>;
|
|
372
|
-
/** Brings more people into a thread the acting user is in. Adds only; it never removes. */
|
|
373
|
-
addRecipients(input: {
|
|
374
|
-
threadId: number;
|
|
375
|
-
recipientUserIds?: number[];
|
|
376
|
-
recipientTeamIds?: number[];
|
|
377
|
-
}): Promise<PhisThreadDetail | null>;
|
|
378
|
-
/**
|
|
379
|
-
* Takes a thread out of the active list, for everyone in it.
|
|
380
|
-
*
|
|
381
|
-
* One row carries the status, so this is the thread being closed rather than one reader hiding it.
|
|
382
|
-
*/
|
|
383
|
-
archive(threadId: number): Promise<boolean>;
|
|
384
|
-
/** Marks the thread read for the acting user, up to its newest message or to one named. */
|
|
385
|
-
markRead(input: {
|
|
364
|
+
export type PhisAddonEventContext = {
|
|
365
|
+
addonId: string;
|
|
366
|
+
event: PhisAddonEventId;
|
|
367
|
+
/** This delivery, stable across retries, so a handler can recognise one it already settled. */
|
|
368
|
+
deliveryId: string;
|
|
369
|
+
site: {
|
|
370
|
+
id: number;
|
|
371
|
+
key: string;
|
|
372
|
+
};
|
|
373
|
+
/** The thread the event happened in, and the message where the event is about one. */
|
|
374
|
+
subject: {
|
|
386
375
|
threadId: number;
|
|
387
|
-
messageId
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
id: number;
|
|
392
|
-
subject: string | null;
|
|
393
|
-
status: number;
|
|
394
|
-
type: number;
|
|
395
|
-
createdAt: string;
|
|
396
|
-
updatedAt: string;
|
|
397
|
-
latestMessageAt: string | null;
|
|
398
|
-
unread: boolean;
|
|
399
|
-
};
|
|
400
|
-
export type PhisThreadMessage = {
|
|
401
|
-
id: number;
|
|
402
|
-
bodyText: string;
|
|
403
|
-
bodyMarkdown: string | null;
|
|
404
|
-
author: PhisUserProjection | null;
|
|
405
|
-
createdAt: string;
|
|
406
|
-
};
|
|
407
|
-
export type PhisThreadDetail = {
|
|
408
|
-
thread: PhisThreadSummary;
|
|
409
|
-
messages: PhisThreadMessage[];
|
|
410
|
-
/** Whether older messages remain beyond the window `messages` carries. */
|
|
411
|
-
hasMoreMessages: boolean;
|
|
412
|
-
recipientUserIds: number[];
|
|
413
|
-
recipientTeamIds: number[];
|
|
376
|
+
messageId: number | null;
|
|
377
|
+
};
|
|
378
|
+
capabilities: PhisAddonCapabilities;
|
|
379
|
+
signal: AbortSignal;
|
|
414
380
|
};
|
|
381
|
+
export type PhisAddonEventHandler = (context: PhisAddonEventContext) => Promise<void> | void;
|
|
415
382
|
export type PhisAddonJobHandler = (context: PhisAddonJobContext) => Promise<void> | void;
|
package/dist/core.d.ts
CHANGED
|
@@ -47,6 +47,28 @@ export declare const PHIS_CORE_CAPABILITIES: {
|
|
|
47
47
|
readonly roles: "@phis/server/roles:v1";
|
|
48
48
|
};
|
|
49
49
|
export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof PHIS_CORE_CAPABILITIES];
|
|
50
|
+
/**
|
|
51
|
+
* What Core tells an Add-on about after the fact.
|
|
52
|
+
*
|
|
53
|
+
* An integration that mirrors a conversation cannot wait to be asked: somebody writes a reply here and
|
|
54
|
+
* it has to appear over there, and no request the Add-on serves is involved. Polling would be the
|
|
55
|
+
* alternative, and polling for "did anybody write anything" is the Site-wide read every capability here
|
|
56
|
+
* is shaped to prevent.
|
|
57
|
+
*
|
|
58
|
+
* An event is delivered only to an Add-on linked to the thread it happened in, and a message this same
|
|
59
|
+
* Add-on imported is never announced back to it -- which is what stops a mirror from echoing.
|
|
60
|
+
*
|
|
61
|
+
* The list is short on purpose and grows when an Add-on needs an entry, the way the capability catalogue
|
|
62
|
+
* does. Every event a Site could emit, declared in advance, would be a contract to get wrong.
|
|
63
|
+
*/
|
|
64
|
+
export declare const PHIS_ADDON_EVENTS: {
|
|
65
|
+
readonly threadMessageCreated: "thread.message.created";
|
|
66
|
+
readonly threadMessageUpdated: "thread.message.updated";
|
|
67
|
+
readonly threadStatusChanged: "thread.status.changed";
|
|
68
|
+
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
69
|
+
readonly threadLinkRemoved: "thread.link.removed";
|
|
70
|
+
};
|
|
71
|
+
export type PhisAddonEventId = (typeof PHIS_ADDON_EVENTS)[keyof typeof PHIS_ADDON_EVENTS];
|
|
50
72
|
/**
|
|
51
73
|
* The Core-owned service kinds an Add-on may implement.
|
|
52
74
|
*
|
|
@@ -57,6 +79,15 @@ export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof
|
|
|
57
79
|
export declare const PHIS_SERVICE_KINDS: {
|
|
58
80
|
readonly mediaStorage: "@phis/server/service/media-storage";
|
|
59
81
|
readonly directory: "@phis/server/service/directory";
|
|
82
|
+
/**
|
|
83
|
+
* Turning text in one language into text in another.
|
|
84
|
+
*
|
|
85
|
+
* A kind rather than a capability for the same reason as the two above: Core owns the question and
|
|
86
|
+
* keeps asking it, and what an implementation talks to -- a vendor, a model on the machine, a
|
|
87
|
+
* catalogue it maintains itself -- is its own business. Several may be installed at once; the
|
|
88
|
+
* operator selects which one an installation uses.
|
|
89
|
+
*/
|
|
90
|
+
readonly translation: "@phis/server/service/translation";
|
|
60
91
|
};
|
|
61
92
|
export type PhisServiceKind = (typeof PHIS_SERVICE_KINDS)[keyof typeof PHIS_SERVICE_KINDS];
|
|
62
93
|
/**
|
|
@@ -69,6 +100,13 @@ export type PhisServiceKind = (typeof PHIS_SERVICE_KINDS)[keyof typeof PHIS_SERV
|
|
|
69
100
|
*
|
|
70
101
|
* The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
|
|
71
102
|
* documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
|
|
103
|
+
*
|
|
104
|
+
* **Before the v1 release every version here stays at 1, whatever changes shape.** There is no released
|
|
105
|
+
* Add-on to protect and nothing in the field built against an older interface, so raising a version buys
|
|
106
|
+
* no compatibility and costs the installation: the recorded manifest of every installed Add-on stops
|
|
107
|
+
* matching, `buildPhisCapabilitySnapshot` refuses the desired state, and every request that resolves a
|
|
108
|
+
* viewer answers 500 until each Add-on has been rebundled and upgraded. An interface that changes before
|
|
109
|
+
* v1 is simply the interface, and both sides are rebuilt from one tree.
|
|
72
110
|
*/
|
|
73
111
|
export declare const PHIS_SERVICE_INTERFACE_VERSIONS: Readonly<Record<PhisServiceKind, number>>;
|
|
74
112
|
export declare const PHIS_SERVICE_INTERFACE_DIGESTS: Readonly<Record<PhisServiceKind, string>>;
|
package/dist/core.js
CHANGED
|
@@ -46,6 +46,27 @@ export const PHIS_CORE_CAPABILITIES = {
|
|
|
46
46
|
/** Whether the acting user holds one of this Add-on's own declared roles. */
|
|
47
47
|
roles: "@phis/server/roles:v1",
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* What Core tells an Add-on about after the fact.
|
|
51
|
+
*
|
|
52
|
+
* An integration that mirrors a conversation cannot wait to be asked: somebody writes a reply here and
|
|
53
|
+
* it has to appear over there, and no request the Add-on serves is involved. Polling would be the
|
|
54
|
+
* alternative, and polling for "did anybody write anything" is the Site-wide read every capability here
|
|
55
|
+
* is shaped to prevent.
|
|
56
|
+
*
|
|
57
|
+
* An event is delivered only to an Add-on linked to the thread it happened in, and a message this same
|
|
58
|
+
* Add-on imported is never announced back to it -- which is what stops a mirror from echoing.
|
|
59
|
+
*
|
|
60
|
+
* The list is short on purpose and grows when an Add-on needs an entry, the way the capability catalogue
|
|
61
|
+
* does. Every event a Site could emit, declared in advance, would be a contract to get wrong.
|
|
62
|
+
*/
|
|
63
|
+
export const PHIS_ADDON_EVENTS = {
|
|
64
|
+
threadMessageCreated: "thread.message.created",
|
|
65
|
+
threadMessageUpdated: "thread.message.updated",
|
|
66
|
+
threadStatusChanged: "thread.status.changed",
|
|
67
|
+
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
68
|
+
threadLinkRemoved: "thread.link.removed",
|
|
69
|
+
};
|
|
49
70
|
/**
|
|
50
71
|
* The Core-owned service kinds an Add-on may implement.
|
|
51
72
|
*
|
|
@@ -56,6 +77,15 @@ export const PHIS_CORE_CAPABILITIES = {
|
|
|
56
77
|
export const PHIS_SERVICE_KINDS = {
|
|
57
78
|
mediaStorage: "@phis/server/service/media-storage",
|
|
58
79
|
directory: "@phis/server/service/directory",
|
|
80
|
+
/**
|
|
81
|
+
* Turning text in one language into text in another.
|
|
82
|
+
*
|
|
83
|
+
* A kind rather than a capability for the same reason as the two above: Core owns the question and
|
|
84
|
+
* keeps asking it, and what an implementation talks to -- a vendor, a model on the machine, a
|
|
85
|
+
* catalogue it maintains itself -- is its own business. Several may be installed at once; the
|
|
86
|
+
* operator selects which one an installation uses.
|
|
87
|
+
*/
|
|
88
|
+
translation: "@phis/server/service/translation",
|
|
59
89
|
};
|
|
60
90
|
/**
|
|
61
91
|
* The digest of the interface each service kind currently has.
|
|
@@ -67,12 +97,21 @@ export const PHIS_SERVICE_KINDS = {
|
|
|
67
97
|
*
|
|
68
98
|
* The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
|
|
69
99
|
* documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
|
|
100
|
+
*
|
|
101
|
+
* **Before the v1 release every version here stays at 1, whatever changes shape.** There is no released
|
|
102
|
+
* Add-on to protect and nothing in the field built against an older interface, so raising a version buys
|
|
103
|
+
* no compatibility and costs the installation: the recorded manifest of every installed Add-on stops
|
|
104
|
+
* matching, `buildPhisCapabilitySnapshot` refuses the desired state, and every request that resolves a
|
|
105
|
+
* viewer answers 500 until each Add-on has been rebundled and upgraded. An interface that changes before
|
|
106
|
+
* v1 is simply the interface, and both sides are rebuilt from one tree.
|
|
70
107
|
*/
|
|
71
108
|
export const PHIS_SERVICE_INTERFACE_VERSIONS = {
|
|
72
109
|
[PHIS_SERVICE_KINDS.mediaStorage]: 1,
|
|
73
110
|
[PHIS_SERVICE_KINDS.directory]: 1,
|
|
111
|
+
[PHIS_SERVICE_KINDS.translation]: 1,
|
|
74
112
|
};
|
|
75
113
|
export const PHIS_SERVICE_INTERFACE_DIGESTS = {
|
|
76
114
|
[PHIS_SERVICE_KINDS.mediaStorage]: "45a871522511f7d77841ca8dd0113b423b3cfd4cb921f143cd1d3d116cab6952",
|
|
77
115
|
[PHIS_SERVICE_KINDS.directory]: "2cac7397c7beb1b8e71fc67f5cae3be89182dddf096895570a15deafa1adbfc9",
|
|
116
|
+
[PHIS_SERVICE_KINDS.translation]: "9bae2e7b92ded5cdde9321939a3bb8cd589f6e1ef8bca7bc5f65608313b3207d",
|
|
78
117
|
};
|
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
|
*
|