@phis/contracts 0.2.0 → 0.3.1
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/README.md +111 -11
- package/dist/access.d.ts +50 -0
- package/dist/access.js +65 -0
- package/dist/addon-index.d.ts +1 -0
- package/dist/addon-index.js +1 -0
- package/dist/addon.d.ts +130 -57
- package/dist/addon.js +2 -2
- package/dist/assets.d.ts +165 -0
- package/dist/assets.js +1 -0
- package/dist/capabilities.d.ts +205 -30
- package/dist/capabilities.js +2 -2
- package/dist/catalog.d.ts +104 -0
- package/dist/catalog.js +116 -0
- package/dist/cms.d.ts +120 -0
- package/dist/cms.js +270 -0
- package/dist/core.d.ts +14 -6
- package/dist/core.js +16 -8
- package/dist/http.d.ts +33 -0
- package/dist/http.js +33 -0
- package/dist/locale.d.ts +14 -0
- package/dist/locale.js +14 -0
- package/dist/logging.d.ts +49 -0
- package/dist/logging.js +17 -0
- package/dist/media-storage.d.ts +129 -0
- package/dist/media-storage.js +56 -0
- package/dist/media.d.ts +228 -0
- package/dist/media.js +362 -0
- package/dist/queries.d.ts +280 -48
- package/dist/queries.js +10 -1
- package/dist/schema.d.ts +160 -26
- package/dist/schema.js +11 -2
- package/dist/server-capabilities.d.ts +37 -0
- package/dist/server-capabilities.js +17 -0
- package/dist/signals.d.ts +58 -0
- package/dist/signals.js +118 -0
- package/dist/site-groups.d.ts +38 -0
- package/dist/site-groups.js +54 -0
- package/package.json +43 -3
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
package/dist/http.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request headers phis and the site UI must spell the same way.
|
|
3
|
+
*
|
|
4
|
+
* These five names are a wire protocol between processes that deploy independently. phis reads them in
|
|
5
|
+
* its guards and route handlers; the site UI writes them in its proxies, gateways and browser widgets.
|
|
6
|
+
* A disagreement is not a type error and not a failing test -- it is a 403 from `guardApiV1` or a 400
|
|
7
|
+
* from `requireSiteFromExplicitKeyRequest`, at runtime, in whichever direction the mismatch happens to
|
|
8
|
+
* point.
|
|
9
|
+
*
|
|
10
|
+
* They were string literals in both repositories before this file existed, eighty-odd of them, and the
|
|
11
|
+
* one place that had bothered to name a constant named it in only one of the two. Renaming the prefix
|
|
12
|
+
* from `x-phi-` to `x-phis-` is what made that visible; the point of collecting them here is that the
|
|
13
|
+
* next such change is one edit rather than a search.
|
|
14
|
+
*/
|
|
15
|
+
/** Carries the public or internal API token on every `/api/v1/*` request. */
|
|
16
|
+
export const PHIS_TOKEN_HEADER = "x-phis-token";
|
|
17
|
+
/**
|
|
18
|
+
* Names the Site a request is scoped to, and is the only thing that does.
|
|
19
|
+
*
|
|
20
|
+
* There is deliberately no host, origin or referer fallback: those are caller-controlled, so a request
|
|
21
|
+
* without this header could otherwise claim any Site whose hostname it spoofed.
|
|
22
|
+
*/
|
|
23
|
+
export const PHIS_SITE_KEY_HEADER = "x-phis-site-key";
|
|
24
|
+
/** Names the CMS area a media request acts in, which decides what the actor may reach. */
|
|
25
|
+
export const PHIS_AREA_HEADER = "x-phis-area";
|
|
26
|
+
/**
|
|
27
|
+
* Hand the original request path and query from the site proxy to the server helpers behind it.
|
|
28
|
+
*
|
|
29
|
+
* Both are set and read inside the site runtime and never reach phis; they exist because a Server
|
|
30
|
+
* Component cannot otherwise see the URL the proxy rewrote.
|
|
31
|
+
*/
|
|
32
|
+
export const PHIS_REQUEST_PATH_HEADER = "x-phis-request-path";
|
|
33
|
+
export const PHIS_REQUEST_SEARCH_HEADER = "x-phis-request-search";
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language this software is written in.
|
|
3
|
+
*
|
|
4
|
+
* Not a default and not a preference: it is the locale every label, every email template and every
|
|
5
|
+
* global message is authored in, and therefore the one a translation starts from. `TRANSLATIONS.md`
|
|
6
|
+
* fixes it -- global messages in `phis.tr_msg` always use English as their canonical source, while a
|
|
7
|
+
* Site's own messages use the immutable `sourceLocale` chosen when the Site was created.
|
|
8
|
+
*
|
|
9
|
+
* It was called DEFAULT_LOCALE on both sides, which is the name of a different thing: a Site's default
|
|
10
|
+
* locale is configuration, stored per Site in `sites.default_locale`, and an installation may set it to
|
|
11
|
+
* anything. Under one name the two were indistinguishable at the call site, and a reader could not tell
|
|
12
|
+
* whether a line stated a fact about the source text or made an assumption about somebody's Site.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PHI_CANONICAL_SOURCE_LOCALE = "en";
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language this software is written in.
|
|
3
|
+
*
|
|
4
|
+
* Not a default and not a preference: it is the locale every label, every email template and every
|
|
5
|
+
* global message is authored in, and therefore the one a translation starts from. `TRANSLATIONS.md`
|
|
6
|
+
* fixes it -- global messages in `phis.tr_msg` always use English as their canonical source, while a
|
|
7
|
+
* Site's own messages use the immutable `sourceLocale` chosen when the Site was created.
|
|
8
|
+
*
|
|
9
|
+
* It was called DEFAULT_LOCALE on both sides, which is the name of a different thing: a Site's default
|
|
10
|
+
* locale is configuration, stored per Site in `sites.default_locale`, and an installation may set it to
|
|
11
|
+
* anything. Under one name the two were indistinguishable at the call site, and a reader could not tell
|
|
12
|
+
* whether a line stated a fact about the source text or made an assumption about somebody's Site.
|
|
13
|
+
*/
|
|
14
|
+
export const PHI_CANONICAL_SOURCE_LOCALE = "en";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log vocabulary phis and the site UI must both know.
|
|
3
|
+
*
|
|
4
|
+
* Two processes write these records and one of them reads all of them: a Site runtime and this server
|
|
5
|
+
* both emit structured lines to journald, and the admin log surface in phis parses them back --
|
|
6
|
+
* filtering by service, splitting by level, and projecting exactly the fields named below. The writer
|
|
7
|
+
* and the reader are therefore two ends of one format, and neither can defer to the other.
|
|
8
|
+
*
|
|
9
|
+
* They did not agree before this file existed. `service` was a closed list in both places, and the two
|
|
10
|
+
* lists drifted: phis admitted "phi-ui" while the UI wrote "phi-shared". Nothing failed. The records
|
|
11
|
+
* simply carried a name the reader never offered as a filter, so the events were there and not
|
|
12
|
+
* findable -- the same failure mode, and the same cure, as the role flags in `access.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Only the vocabulary lives here. How a line is serialized is not shared: phis guards against cycles
|
|
15
|
+
* and BigInt because an Add-on hands it arbitrary values, and that is its own concern.
|
|
16
|
+
*/
|
|
17
|
+
/** Which process wrote the record. The reader offers exactly these as filters. */
|
|
18
|
+
export type PhiLogService = "phis" | "ui" | "site" | "cli";
|
|
19
|
+
export type PhiLogLevel = "debug" | "info" | "warn" | "error";
|
|
20
|
+
/** What a logger carries across every record it writes. */
|
|
21
|
+
export type PhiLoggerContext = {
|
|
22
|
+
service: PhiLogService;
|
|
23
|
+
siteKey?: string | null;
|
|
24
|
+
area?: string | null;
|
|
25
|
+
requestId?: string | null;
|
|
26
|
+
userId?: number | string | null;
|
|
27
|
+
actorRole?: string | null;
|
|
28
|
+
pluginKey?: string | null;
|
|
29
|
+
method?: string | null;
|
|
30
|
+
path?: string | null;
|
|
31
|
+
targetType?: string | null;
|
|
32
|
+
targetId?: number | string | null;
|
|
33
|
+
};
|
|
34
|
+
/** What one record adds to that context. */
|
|
35
|
+
export type PhiLogEvent = {
|
|
36
|
+
message?: string;
|
|
37
|
+
status?: number | null;
|
|
38
|
+
durationMs?: number | null;
|
|
39
|
+
error?: unknown;
|
|
40
|
+
meta?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
export interface PhiLogger {
|
|
43
|
+
readonly context: Readonly<PhiLoggerContext>;
|
|
44
|
+
child(context: Partial<PhiLoggerContext>): PhiLogger;
|
|
45
|
+
debug(event: string, data?: PhiLogEvent): void;
|
|
46
|
+
info(event: string, data?: PhiLogEvent): void;
|
|
47
|
+
warn(event: string, data?: PhiLogEvent): void;
|
|
48
|
+
error(event: string, data?: PhiLogEvent): void;
|
|
49
|
+
}
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log vocabulary phis and the site UI must both know.
|
|
3
|
+
*
|
|
4
|
+
* Two processes write these records and one of them reads all of them: a Site runtime and this server
|
|
5
|
+
* both emit structured lines to journald, and the admin log surface in phis parses them back --
|
|
6
|
+
* filtering by service, splitting by level, and projecting exactly the fields named below. The writer
|
|
7
|
+
* and the reader are therefore two ends of one format, and neither can defer to the other.
|
|
8
|
+
*
|
|
9
|
+
* They did not agree before this file existed. `service` was a closed list in both places, and the two
|
|
10
|
+
* lists drifted: phis admitted "phi-ui" while the UI wrote "phi-shared". Nothing failed. The records
|
|
11
|
+
* simply carried a name the reader never offered as a filter, so the events were there and not
|
|
12
|
+
* findable -- the same failure mode, and the same cure, as the role flags in `access.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Only the vocabulary lives here. How a line is serialized is not shared: phis guards against cycles
|
|
15
|
+
* and BigInt because an Add-on hands it arbitrary values, and that is its own concern.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/media-storage.d.ts
CHANGED
|
@@ -23,8 +23,32 @@ export type PhisMediaObjectStreamInput = {
|
|
|
23
23
|
body: ReadableStream<Uint8Array>;
|
|
24
24
|
contentType: string;
|
|
25
25
|
metadata?: Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Told that nobody is waiting for this write any more.
|
|
28
|
+
*
|
|
29
|
+
* A body that stops arriving does not, on its own, end the write: the Client is gone or silent while
|
|
30
|
+
* the Provider sits waiting for a byte that will never come. Core aborts the read either way, so an
|
|
31
|
+
* Adapter that ignores this still ends -- with a torn object it must clean up. Honouring it is how an
|
|
32
|
+
* Adapter stops paying for the write, and for a remote Provider that is a request in flight.
|
|
33
|
+
*/
|
|
34
|
+
signal?: AbortSignal;
|
|
26
35
|
};
|
|
27
36
|
export type PhisMediaObjectHead = {
|
|
37
|
+
/**
|
|
38
|
+
* What this Provider can attest about the object's content, if anything.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately not `etag`. An ETag is an HTTP cache validator and each Provider fills it with what it
|
|
41
|
+
* likes -- a real SHA-256 for Core's own storage, an MD5 for a single-request S3 PUT, and a digest of
|
|
42
|
+
* part digests above that, which hashes no bytes anyone uploaded. Reading one as a content hash is
|
|
43
|
+
* how an MD5 ends up recorded as a SHA-256.
|
|
44
|
+
*
|
|
45
|
+
* Null is the correct answer wherever the Provider cannot say. A missing digest is a fact Core can
|
|
46
|
+
* record; a wrong one invites a comparison that quietly finds nothing.
|
|
47
|
+
*/
|
|
48
|
+
checksum?: {
|
|
49
|
+
algorithm: PhisMediaChecksumAlgorithm;
|
|
50
|
+
value: string;
|
|
51
|
+
} | null;
|
|
28
52
|
storageKey: string;
|
|
29
53
|
byteSize: number;
|
|
30
54
|
contentType: string;
|
|
@@ -72,13 +96,118 @@ export type PhiMediaUploadPlanInput = {
|
|
|
72
96
|
* Core owns its route table, so the address is handed to the adapter rather than assembled by it.
|
|
73
97
|
*/
|
|
74
98
|
proxyUploadUrl: string;
|
|
99
|
+
/**
|
|
100
|
+
* What the client says it is about to send, and under which algorithm, as lowercase hex.
|
|
101
|
+
*
|
|
102
|
+
* 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 and name the header the client has to send, so a body
|
|
104
|
+
* that does not match is refused there and never becomes an object. That refusal is what makes the
|
|
105
|
+
* figure trustworthy -- Core records the client's number because a wrong one would not have got this
|
|
106
|
+
* far, not because the client is believed.
|
|
107
|
+
*
|
|
108
|
+
* The algorithm travels with the value because the adapter does not get to pick one. It was settled
|
|
109
|
+
* for this Profile when it was probed, and a plan that quietly used another would produce a digest
|
|
110
|
+
* that is correct and yet incomparable with every digest recorded before it.
|
|
111
|
+
*
|
|
112
|
+
* Absent means the digest will be established some other way, which is Core hashing the body as it
|
|
113
|
+
* streams through. An adapter must never invent one.
|
|
114
|
+
*/
|
|
115
|
+
checksum?: {
|
|
116
|
+
algorithm: PhisMediaChecksumAlgorithm;
|
|
117
|
+
value: string;
|
|
118
|
+
};
|
|
75
119
|
};
|
|
76
120
|
export type PhiMediaUploadCompletionInput = {
|
|
77
121
|
storageKey: string;
|
|
78
122
|
/** Whatever the plan's issuer asked the Client to bring back. Core passes it through unread. */
|
|
79
123
|
completion?: unknown;
|
|
80
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* What one endpoint turned out to be able to do, established by trying rather than by asking.
|
|
127
|
+
*
|
|
128
|
+
* A Provider is one adapter over many backends: the S3 adapter speaks to AWS, to MinIO and to Garage,
|
|
129
|
+
* and they do not agree on everything. So the capability belongs to the configured endpoint and not to
|
|
130
|
+
* the code, and no field of the configuration would tell Core which one it is looking at.
|
|
131
|
+
*
|
|
132
|
+
* Establishing it once, when an operator sets a Profile up, also moves a whole class of failure
|
|
133
|
+
* forward: today a wrong endpoint, a missing permission or a bucket that does not exist is discovered
|
|
134
|
+
* by whoever uploads first, and reaches them as their error rather than as a misconfiguration.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* The digests this system knows how to record, named so what is stored says what it is.
|
|
138
|
+
*
|
|
139
|
+
* One column holding "a checksum" is how an MD5 ends up in a field called SHA-256 and nobody notices:
|
|
140
|
+
* an `ETag` is whatever a Provider decided to put there, and Providers disagree. Every recorded digest
|
|
141
|
+
* therefore carries the algorithm that produced it.
|
|
142
|
+
*/
|
|
143
|
+
export declare const PHIS_MEDIA_CHECKSUM_ALGORITHMS: readonly ["sha256", "sha512", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5"];
|
|
144
|
+
export type PhisMediaChecksumAlgorithm = (typeof PHIS_MEDIA_CHECKSUM_ALGORITHMS)[number];
|
|
145
|
+
/**
|
|
146
|
+
* The digests strong enough to answer "is this the same file", rather than only "did it arrive intact".
|
|
147
|
+
*
|
|
148
|
+
* The distinction is not fussiness. CRC is linear: given a stored object, a second file colliding with
|
|
149
|
+
* it is algebra rather than computation, and MD5 collisions are a download. Both remain perfectly good
|
|
150
|
+
* evidence that a transfer was not corrupted, which is the other thing a checksum is for.
|
|
151
|
+
*
|
|
152
|
+
* What rides on this is duplicate detection, and a collision there is a refused upload rather than a
|
|
153
|
+
* substituted file -- so the cost of trusting a weak digest is that someone who may already write to a
|
|
154
|
+
* Space can stop one file from entering it. Small, but not nothing, and free to avoid.
|
|
155
|
+
*/
|
|
156
|
+
export declare const PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS: readonly PhisMediaChecksumAlgorithm[];
|
|
157
|
+
export declare function isPhisMediaIdentityChecksum(algorithm: string | null | undefined): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Which of the digests an endpoint verifies a Profile will record.
|
|
160
|
+
*
|
|
161
|
+
* Chosen once, when the Profile is probed, and never per upload: duplicate detection compares digests,
|
|
162
|
+
* and two are only comparable under the same algorithm. Deterministic on purpose -- the same measured
|
|
163
|
+
* endpoint always yields the same answer, so re-probing an unchanged endpoint does not silently start a
|
|
164
|
+
* second cohort of objects that can never be told to match the first.
|
|
165
|
+
*
|
|
166
|
+
* Only identity-grade digests are eligible. A CRC an endpoint also verifies still proves a transfer
|
|
167
|
+
* intact, and Core records it when that is what arrived; it is simply not what a Profile asks for.
|
|
168
|
+
*/
|
|
169
|
+
export declare function choosePhisMediaChecksumAlgorithm(probe: {
|
|
170
|
+
verifiedAlgorithms: readonly PhisMediaChecksumAlgorithm[];
|
|
171
|
+
} | null | undefined): PhisMediaChecksumAlgorithm | null;
|
|
172
|
+
export type PhisMediaStorageProbe = {
|
|
173
|
+
/** Whether a small object could be written, read back, and removed again. */
|
|
174
|
+
writable: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Which digests the endpoint was found to check against the bytes it is given.
|
|
177
|
+
*
|
|
178
|
+
* An algorithm is listed only when a correct digest was accepted *and* a deliberately wrong one was
|
|
179
|
+
* refused. Accepting alone proves nothing: an endpoint that ignores the header accepts everything,
|
|
180
|
+
* and a checksum nobody verifies is a claim by the uploader wearing the shape of proof.
|
|
181
|
+
*
|
|
182
|
+
* What it buys is a direct upload that still yields a trustworthy digest -- the bytes go from the
|
|
183
|
+
* client to the storage without passing through Core, and the storage refuses them if they are not
|
|
184
|
+
* what the client said they were. Which of the listed algorithms is used is settled once, per
|
|
185
|
+
* Profile, rather than per upload: what is recorded has to stay comparable with what was recorded
|
|
186
|
+
* before it, and an algorithm that changes underneath splits one set of objects into two that can
|
|
187
|
+
* never be told to be the same.
|
|
188
|
+
*/
|
|
189
|
+
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
|
+
/** What did not answer, in an operator's terms. Empty when everything did. */
|
|
200
|
+
findings: string[];
|
|
201
|
+
};
|
|
81
202
|
export interface PhisMediaStorageAdapter {
|
|
203
|
+
/**
|
|
204
|
+
* Tries what this endpoint can do, and leaves nothing behind.
|
|
205
|
+
*
|
|
206
|
+
* Runs when a Profile is configured, not per upload. It writes a small object under a key of its own
|
|
207
|
+
* and removes it again; a Provider that cannot be probed answers with `writable: false` and a finding
|
|
208
|
+
* rather than throwing, because a Profile that cannot be reached is an answer too.
|
|
209
|
+
*/
|
|
210
|
+
probeCapabilities(): Promise<PhisMediaStorageProbe>;
|
|
82
211
|
putObject(input: PhisMediaObjectInput): Promise<PhisMediaObjectHead>;
|
|
83
212
|
putObjectStream(input: PhisMediaObjectStreamInput): Promise<PhisMediaObjectHead>;
|
|
84
213
|
getObject(storageKey: string): Promise<Buffer | null>;
|
package/dist/media-storage.js
CHANGED
|
@@ -7,3 +7,59 @@
|
|
|
7
7
|
* and answers about objects.
|
|
8
8
|
*/
|
|
9
9
|
export const PHI_LOCAL_MEDIA_STORAGE_PROVIDER_ID = "@phis/server/storage-local";
|
|
10
|
+
/**
|
|
11
|
+
* What one endpoint turned out to be able to do, established by trying rather than by asking.
|
|
12
|
+
*
|
|
13
|
+
* A Provider is one adapter over many backends: the S3 adapter speaks to AWS, to MinIO and to Garage,
|
|
14
|
+
* and they do not agree on everything. So the capability belongs to the configured endpoint and not to
|
|
15
|
+
* the code, and no field of the configuration would tell Core which one it is looking at.
|
|
16
|
+
*
|
|
17
|
+
* Establishing it once, when an operator sets a Profile up, also moves a whole class of failure
|
|
18
|
+
* forward: today a wrong endpoint, a missing permission or a bucket that does not exist is discovered
|
|
19
|
+
* by whoever uploads first, and reaches them as their error rather than as a misconfiguration.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The digests this system knows how to record, named so what is stored says what it is.
|
|
23
|
+
*
|
|
24
|
+
* One column holding "a checksum" is how an MD5 ends up in a field called SHA-256 and nobody notices:
|
|
25
|
+
* an `ETag` is whatever a Provider decided to put there, and Providers disagree. Every recorded digest
|
|
26
|
+
* therefore carries the algorithm that produced it.
|
|
27
|
+
*/
|
|
28
|
+
export const PHIS_MEDIA_CHECKSUM_ALGORITHMS = [
|
|
29
|
+
"sha256", "sha512", "xxhash128", "crc64nvme", "crc32c", "crc32", "md5",
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* The digests strong enough to answer "is this the same file", rather than only "did it arrive intact".
|
|
33
|
+
*
|
|
34
|
+
* The distinction is not fussiness. CRC is linear: given a stored object, a second file colliding with
|
|
35
|
+
* it is algebra rather than computation, and MD5 collisions are a download. Both remain perfectly good
|
|
36
|
+
* evidence that a transfer was not corrupted, which is the other thing a checksum is for.
|
|
37
|
+
*
|
|
38
|
+
* What rides on this is duplicate detection, and a collision there is a refused upload rather than a
|
|
39
|
+
* substituted file -- so the cost of trusting a weak digest is that someone who may already write to a
|
|
40
|
+
* Space can stop one file from entering it. Small, but not nothing, and free to avoid.
|
|
41
|
+
*/
|
|
42
|
+
export const PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS = ["sha256", "sha512"];
|
|
43
|
+
export function isPhisMediaIdentityChecksum(algorithm) {
|
|
44
|
+
return PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS.includes(algorithm);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Which of the digests an endpoint verifies a Profile will record.
|
|
48
|
+
*
|
|
49
|
+
* Chosen once, when the Profile is probed, and never per upload: duplicate detection compares digests,
|
|
50
|
+
* and two are only comparable under the same algorithm. Deterministic on purpose -- the same measured
|
|
51
|
+
* endpoint always yields the same answer, so re-probing an unchanged endpoint does not silently start a
|
|
52
|
+
* second cohort of objects that can never be told to match the first.
|
|
53
|
+
*
|
|
54
|
+
* Only identity-grade digests are eligible. A CRC an endpoint also verifies still proves a transfer
|
|
55
|
+
* intact, and Core records it when that is what arrived; it is simply not what a Profile asks for.
|
|
56
|
+
*/
|
|
57
|
+
export function choosePhisMediaChecksumAlgorithm(probe) {
|
|
58
|
+
if (!probe)
|
|
59
|
+
return null;
|
|
60
|
+
for (const candidate of PHIS_MEDIA_IDENTITY_CHECKSUM_ALGORITHMS) {
|
|
61
|
+
if (probe.verifiedAlgorithms.includes(candidate))
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a Media Space names its kind on the wire.
|
|
3
|
+
*
|
|
4
|
+
* Only the name is shared. phis stores the kind as a smallint and that number is its own business --
|
|
5
|
+
* the site UI never sees it, so putting it here would place a storage detail in a contract.
|
|
6
|
+
*
|
|
7
|
+
* What made this worth writing down is that the translation between the two was incomplete: phis knew
|
|
8
|
+
* four kinds and mapped three, falling back to `"site"` for the rest, while the UI declared its own
|
|
9
|
+
* union of the same three. An Add-on Space therefore reached the administration surface calling itself
|
|
10
|
+
* a Site Space, in the one view that shows Add-on Spaces at all. With the names here, the map on the
|
|
11
|
+
* server side is a total function from its own enum onto this one, and a kind that is added and not
|
|
12
|
+
* mapped stops compiling.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PHI_MEDIA_SPACE_KINDS: readonly ["site", "user", "group", "addon"];
|
|
15
|
+
export type PhiMediaSpaceKind = (typeof PHI_MEDIA_SPACE_KINDS)[number];
|
|
16
|
+
/**
|
|
17
|
+
* The kinds a Module may declare a need for.
|
|
18
|
+
*
|
|
19
|
+
* The Site Space always exists and is governed by the Core Media role matrix, so declaring it could
|
|
20
|
+
* only repeat what is already true. An Add-on Space belongs to its Add-on and comes with installing
|
|
21
|
+
* it, rather than being asked for by a Module.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PHI_DECLARABLE_MEDIA_SPACE_KINDS: readonly ["user", "group"];
|
|
24
|
+
export type PhiDeclarableMediaSpaceKind = (typeof PHI_DECLARABLE_MEDIA_SPACE_KINDS)[number];
|
|
25
|
+
export declare function isPhiDeclarableMediaSpaceKind(value: unknown): value is PhiDeclarableMediaSpaceKind;
|
|
26
|
+
/**
|
|
27
|
+
* The display properties an Asset may carry, as bits of `media_assets.presentation_flags`.
|
|
28
|
+
*
|
|
29
|
+
* Shared because the number itself travels: the site UI sends a mask as a list filter and an array of
|
|
30
|
+
* these values when metadata is saved, and phis both answers the filter with `flags & mask <> 0` and
|
|
31
|
+
* refuses a value it does not know. Two sides that must agree on a number is what this package is for,
|
|
32
|
+
* and the copy that lived on each side had already drifted -- phis still carried `Private`, `Archived`
|
|
33
|
+
* and `Restricted` long after lifecycle and delivery had become columns of their own.
|
|
34
|
+
*
|
|
35
|
+
* Display only. Access is `delivery_policy` and `lifecycle_status`, never a bit in here.
|
|
36
|
+
*
|
|
37
|
+
* Bits 1 and 2 are free, and before v1 a bit may be given a new meaning rather than kept vacant.
|
|
38
|
+
*/
|
|
39
|
+
export declare const PhiMediaAssetFlags: {
|
|
40
|
+
/** Offered as a background. What an Asset is for; where it came from is `meta.source`. */
|
|
41
|
+
readonly Background: number;
|
|
42
|
+
readonly Featured: number;
|
|
43
|
+
readonly Locked: number;
|
|
44
|
+
readonly Mask: number;
|
|
45
|
+
};
|
|
46
|
+
export type PhiMediaAssetFlag = (typeof PhiMediaAssetFlags)[keyof typeof PhiMediaAssetFlags];
|
|
47
|
+
/**
|
|
48
|
+
* The rest of the Media vocabulary both sides speak.
|
|
49
|
+
*
|
|
50
|
+
* Kind names travel as `kind` in the list contract and are stored on the Asset; Folder flags travel in
|
|
51
|
+
* the Folder payload; a variant key is a number in a delivery URL and the name phis generates the
|
|
52
|
+
* rendition under. None of it is one side's private business, and all of it stood twice -- once here in
|
|
53
|
+
* spirit, once in each repository in fact -- until a bit value drifted and showed what that costs.
|
|
54
|
+
*
|
|
55
|
+
* The two resolvers come along because a vocabulary without its total function is only half a contract:
|
|
56
|
+
* a content type must land on the same kind in the browser and on the server, or an upload is filed as
|
|
57
|
+
* one thing and listed as another.
|
|
58
|
+
*/
|
|
59
|
+
export declare const PhiMediaFolderFlags: {
|
|
60
|
+
readonly Collapsed: number;
|
|
61
|
+
readonly Hidden: number;
|
|
62
|
+
readonly Locked: number;
|
|
63
|
+
};
|
|
64
|
+
export declare const PhiMediaKind: {
|
|
65
|
+
readonly Image: "image";
|
|
66
|
+
readonly Video: "video";
|
|
67
|
+
readonly Audio: "audio";
|
|
68
|
+
readonly Pdf: "pdf";
|
|
69
|
+
readonly Markdown: "markdown";
|
|
70
|
+
readonly Document: "document";
|
|
71
|
+
readonly Archive: "archive";
|
|
72
|
+
readonly Font: "font";
|
|
73
|
+
readonly Binary: "binary";
|
|
74
|
+
readonly Other: "other";
|
|
75
|
+
};
|
|
76
|
+
export declare const PhiImageAssetVariantKey: {
|
|
77
|
+
readonly Thumbnail: 0;
|
|
78
|
+
readonly Preview: 1;
|
|
79
|
+
readonly Banner: 2;
|
|
80
|
+
readonly Header: 3;
|
|
81
|
+
readonly Card: 4;
|
|
82
|
+
readonly Hero: 5;
|
|
83
|
+
readonly Avatar: 6;
|
|
84
|
+
readonly Logo: 7;
|
|
85
|
+
readonly Landscape: 8;
|
|
86
|
+
readonly Portrait: 9;
|
|
87
|
+
};
|
|
88
|
+
export declare const PhiImageAssetVariantKeyName: {
|
|
89
|
+
readonly 0: "thumbnail";
|
|
90
|
+
readonly 1: "preview";
|
|
91
|
+
readonly 2: "banner";
|
|
92
|
+
readonly 3: "header";
|
|
93
|
+
readonly 4: "card";
|
|
94
|
+
readonly 5: "hero";
|
|
95
|
+
readonly 6: "avatar";
|
|
96
|
+
readonly 7: "logo";
|
|
97
|
+
readonly 8: "landscape";
|
|
98
|
+
readonly 9: "portrait";
|
|
99
|
+
};
|
|
100
|
+
export type PhiImageAssetVariantSpec = {
|
|
101
|
+
width: number;
|
|
102
|
+
height: number;
|
|
103
|
+
fit: "cover" | "contain";
|
|
104
|
+
quality: number;
|
|
105
|
+
format: "webp";
|
|
106
|
+
};
|
|
107
|
+
export declare function normalizePhiMediaKind(kind: string | null | undefined): "binary" | "other" | "image" | "video" | "audio" | "pdf" | "markdown" | "document" | "archive" | "font";
|
|
108
|
+
export declare function resolvePhiMediaKindFromContentType(contentType: string | null | undefined): "binary" | "other" | "image" | "video" | "audio" | "pdf" | "markdown" | "document" | "archive" | "font";
|
|
109
|
+
export declare function normalizePhiImageAssetVariantKey(key: unknown): 0 | 1 | 2 | 3 | 4 | 5 | 6 | 9 | 7 | 8 | null;
|
|
110
|
+
export declare function resolvePhiImageAssetVariantKeyName(key: number | null | undefined): string | null;
|
|
111
|
+
export declare function resolvePhiImageAssetVariantSpec(key: number | null | undefined): PhiImageAssetVariantSpec | null;
|
|
112
|
+
/**
|
|
113
|
+
* The two closed vocabularies that decide whether an Asset may be delivered at all.
|
|
114
|
+
*
|
|
115
|
+
* The numbers themselves reach the site UI: an Asset's detail payload carries `lifecycleStatus` and
|
|
116
|
+
* `deliveryPolicy` as they stand in the row, and the UI decides from them whether `next/image` may be
|
|
117
|
+
* pointed at the original. phis evaluates `delivery_policy` before any physical lookup, so a value that
|
|
118
|
+
* means one thing on one side and another on the other would be a visibility bug, not a display bug.
|
|
119
|
+
*
|
|
120
|
+
* Unlike the Space kind next to them in phis, which stays a storage detail because the wire carries its
|
|
121
|
+
* name instead of its number.
|
|
122
|
+
*/
|
|
123
|
+
export declare const PhiMediaLifecycleStatus: {
|
|
124
|
+
readonly Pending: 1;
|
|
125
|
+
readonly Ready: 2;
|
|
126
|
+
readonly Archived: 3;
|
|
127
|
+
readonly Quarantined: 4;
|
|
128
|
+
readonly Failed: 5;
|
|
129
|
+
};
|
|
130
|
+
export declare const PhiMediaDeliveryPolicy: {
|
|
131
|
+
readonly Public: 1;
|
|
132
|
+
readonly Authenticated: 2;
|
|
133
|
+
readonly User: 3;
|
|
134
|
+
readonly Group: 4;
|
|
135
|
+
readonly Internal: 5;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* What a typeface says about its own proportions.
|
|
139
|
+
*
|
|
140
|
+
* Shared because the numbers cross: phis reads them out of the uploaded file once and keeps them on the
|
|
141
|
+
* Asset, and the site UI turns them into the `size-adjust` and `*-override` descriptors of a fallback
|
|
142
|
+
* `@font-face`. Without that face a browser sets the page in a local substitute and reflows when the
|
|
143
|
+
* real file lands, which is the one thing a viewer actually sees.
|
|
144
|
+
*
|
|
145
|
+
* Everything is in font units on the `unitsPerEm` grid, so a reader divides rather than assumes 1000.
|
|
146
|
+
*/
|
|
147
|
+
export type PhiFontMetrics = {
|
|
148
|
+
/** The container the numbers were read from. */
|
|
149
|
+
format: "sfnt" | "woff" | "woff2";
|
|
150
|
+
familyName: string | null;
|
|
151
|
+
unitsPerEm: number;
|
|
152
|
+
ascent: number;
|
|
153
|
+
descent: number;
|
|
154
|
+
lineGap: number;
|
|
155
|
+
capHeight: number | null;
|
|
156
|
+
xHeight: number | null;
|
|
157
|
+
/**
|
|
158
|
+
* How wide running text sets, as an advance width.
|
|
159
|
+
*
|
|
160
|
+
* `glyphs` is the frequency-weighted advance of the characters prose is made of. `os2` is the font's
|
|
161
|
+
* own `xAvgCharWidth`, which averages every glyph in the file and is therefore wrong for a Latin page
|
|
162
|
+
* in a family that also carries Greek -- the source travels so a reader can tell the two apart.
|
|
163
|
+
*/
|
|
164
|
+
xWidthAvg: number | null;
|
|
165
|
+
xWidthAvgSource: "glyphs" | "os2" | null;
|
|
166
|
+
/**
|
|
167
|
+
* What the file classifies itself as, and `null` where it classifies itself as nothing.
|
|
168
|
+
*
|
|
169
|
+
* Plenty of good fonts leave both PANOSE and the `OS/2` family class empty, so the absence is a fact
|
|
170
|
+
* about the file rather than a reason to guess: whoever picks the local substitute decides instead.
|
|
171
|
+
*/
|
|
172
|
+
category: "serif" | "sans-serif" | "monospace" | null;
|
|
173
|
+
/**
|
|
174
|
+
* Which unicode cuts the typeface has characters in, read out of its `cmap` at upload.
|
|
175
|
+
*
|
|
176
|
+
* Absent on a font uploaded before cuts existed; such a font is delivered whole.
|
|
177
|
+
*/
|
|
178
|
+
coverage?: PhiFontCoverage | null;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* The cuts a typeface can be delivered in.
|
|
182
|
+
*
|
|
183
|
+
* The number stands in a delivery URL, so it is shared; the named cuts follow the unicode ranges Google
|
|
184
|
+
* Fonts divides its families along, so a face declared here splits where fonts are commonly split.
|
|
185
|
+
* `Rest` is not a range but everything a typeface has outside all of them -- its own characters, listed
|
|
186
|
+
* per font in `PhiFontCoverage.restRanges`, so a page never loses a glyph and never fetches a cut for a
|
|
187
|
+
* character the font does not have.
|
|
188
|
+
*/
|
|
189
|
+
export declare const PhiFontSubsetKey: {
|
|
190
|
+
readonly Latin: 0;
|
|
191
|
+
readonly LatinExt: 1;
|
|
192
|
+
readonly Cyrillic: 2;
|
|
193
|
+
readonly CyrillicExt: 3;
|
|
194
|
+
readonly Greek: 4;
|
|
195
|
+
readonly GreekExt: 5;
|
|
196
|
+
readonly Vietnamese: 6;
|
|
197
|
+
readonly Math: 7;
|
|
198
|
+
readonly Symbols: 8;
|
|
199
|
+
readonly Rest: 15;
|
|
200
|
+
};
|
|
201
|
+
export type PhiFontSubsetKeyValue = (typeof PhiFontSubsetKey)[keyof typeof PhiFontSubsetKey];
|
|
202
|
+
/** An inclusive codepoint range. */
|
|
203
|
+
export type PhiUnicodeRange = readonly [start: number, end: number];
|
|
204
|
+
/**
|
|
205
|
+
* Raised whenever the ranges below or the way a cut is produced changes, so every stored cut is made
|
|
206
|
+
* again rather than served under a rule that no longer describes it.
|
|
207
|
+
*/
|
|
208
|
+
export declare const PHI_FONT_SUBSET_VERSION = 1;
|
|
209
|
+
/** The named cuts, as Google Fonts' CSS2 API states them for a Latin, Cyrillic and Greek family. */
|
|
210
|
+
export declare const PHI_FONT_SUBSET_RANGES: Readonly<Record<Exclude<PhiFontSubsetKeyValue, 15>, readonly PhiUnicodeRange[]>>;
|
|
211
|
+
/** What a typeface has characters in: the named cuts, and its own characters outside all of them. */
|
|
212
|
+
export type PhiFontCoverage = {
|
|
213
|
+
subsets: PhiFontSubsetKeyValue[];
|
|
214
|
+
restRanges: PhiUnicodeRange[];
|
|
215
|
+
};
|
|
216
|
+
export declare function isPhiCodepointInRanges(codepoint: number, ranges: readonly PhiUnicodeRange[]): boolean;
|
|
217
|
+
/** A `unicode-range` descriptor value. */
|
|
218
|
+
export declare function formatPhiUnicodeRange(ranges: readonly PhiUnicodeRange[]): string;
|
|
219
|
+
/** The ranges a cut covers for one typeface: the shared list for a named cut, the font's own for `Rest`. */
|
|
220
|
+
export declare function resolvePhiFontSubsetRanges(key: PhiFontSubsetKeyValue, coverage: PhiFontCoverage): readonly PhiUnicodeRange[];
|
|
221
|
+
export declare function isPhiFontSubsetKey(value: unknown): value is PhiFontSubsetKeyValue;
|
|
222
|
+
/**
|
|
223
|
+
* The cuts to preload for a page in `locale`, `Latin` first, restricted to what the font covers.
|
|
224
|
+
*
|
|
225
|
+
* `locale` is a BCP 47 tag; its script subtag overrides the language's usual one, so `sr-Latn` preloads
|
|
226
|
+
* `LatinExt` where `sr` preloads `Cyrillic`.
|
|
227
|
+
*/
|
|
228
|
+
export declare function resolvePhiFontPreloadSubsetKeys(locale: string | null | undefined, coverage: PhiFontCoverage): PhiFontSubsetKeyValue[];
|