@phis/contracts 0.2.0 → 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 +175 -67
- package/dist/access.d.ts +63 -0
- package/dist/access.js +78 -0
- package/dist/addon-index.d.ts +5 -0
- package/dist/addon-index.js +5 -0
- package/dist/addon.d.ts +181 -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 +175 -33
- 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 +52 -6
- package/dist/core.js +55 -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 +371 -0
- package/dist/media-storage.js +84 -0
- package/dist/media.d.ts +272 -0
- package/dist/media.js +421 -0
- package/dist/queries.d.ts +280 -48
- package/dist/queries.js +10 -1
- package/dist/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -0
- 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 +53 -0
- package/dist/site-groups.js +54 -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 +64 -4
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
package/dist/schema.d.ts
CHANGED
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
* exactly what this removes. An Add-on that needs one of them is either cut wrong or needs a Core
|
|
11
11
|
* service.
|
|
12
12
|
*/
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const PHIS_ADDON_SCHEMA_DESCRIPTOR_VERSION: 1;
|
|
14
14
|
/**
|
|
15
15
|
* What a column holds, said abstractly so Core can map it.
|
|
16
16
|
*
|
|
17
17
|
* `identity` is a generated key and may appear only in a primary key; the width is Core's business, not
|
|
18
18
|
* the Add-on's.
|
|
19
19
|
*/
|
|
20
|
-
export type
|
|
20
|
+
export type PhisAddonColumnType = "identity" | "smallInteger" | "integer" | "bigInteger" | "text" | "string" | "boolean" | "instant" | "json" | "uuid" | "binary";
|
|
21
21
|
/** The closed vocabulary a default may be drawn from. Anything wider would be an expression. */
|
|
22
|
-
export type
|
|
22
|
+
export type PhisAddonColumnDefault = {
|
|
23
23
|
kind: "literal";
|
|
24
24
|
value: string | number | boolean;
|
|
25
25
|
} | {
|
|
@@ -36,27 +36,45 @@ export type PhiServerAddonColumnDefault = {
|
|
|
36
36
|
* binds the pair rather than the id alone. Core never references a group any other way, and an Add-on
|
|
37
37
|
* that could would be able to name a group belonging to a different Site.
|
|
38
38
|
*/
|
|
39
|
-
export declare const
|
|
39
|
+
export declare const PHIS_ADDON_CORE_REFERENCES: {
|
|
40
40
|
readonly site: "core.site";
|
|
41
41
|
readonly user: "core.user";
|
|
42
42
|
readonly group: "core.group";
|
|
43
|
+
/**
|
|
44
|
+
* A stored file, which is the one Core row an Add-on points at without having written it.
|
|
45
|
+
*
|
|
46
|
+
* Core keeps the facts about an object -- where it is, how big, what type, which digest -- in one
|
|
47
|
+
* place for every upload it has ever handled, whoever asked. An Add-on's asset row names one of them
|
|
48
|
+
* and holds nothing about it, so there is no second copy to fall out of step, and no key an Add-on
|
|
49
|
+
* could pass anywhere.
|
|
50
|
+
*/
|
|
51
|
+
readonly mediaAsset: "core.mediaAsset";
|
|
43
52
|
};
|
|
44
|
-
export type
|
|
53
|
+
export type PhisAddonCoreReference = (typeof PHIS_ADDON_CORE_REFERENCES)[keyof typeof PHIS_ADDON_CORE_REFERENCES];
|
|
45
54
|
/**
|
|
46
55
|
* A delete rule, offered as two values and no third.
|
|
47
56
|
*
|
|
48
57
|
* An Add-on must not be able to refuse a Core deletion: an operator who cannot delete a user because an
|
|
49
58
|
* installed package forbids it is looking at a constraint error from code they may not know they have.
|
|
50
59
|
*/
|
|
51
|
-
export type
|
|
52
|
-
export type
|
|
60
|
+
export type PhisAddonDeleteRule = "cascade" | "setNull";
|
|
61
|
+
export type PhisAddonValueColumnDescriptor = {
|
|
53
62
|
name: string;
|
|
54
|
-
type:
|
|
63
|
+
type: PhisAddonColumnType;
|
|
55
64
|
/** Required for `string`, forbidden otherwise. */
|
|
56
65
|
maxLength?: number;
|
|
57
66
|
nullable?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Unique -- within the Site, when the table is Site-scoped.
|
|
69
|
+
*
|
|
70
|
+
* Core prepends the Site column, because the Add-on cannot: it may not declare `site_id` and so has
|
|
71
|
+
* nowhere to name the pair. Without that, uniqueness would reach across every tenant on the instance,
|
|
72
|
+
* and the second Site to install the Add-on would collide with the first over a value neither of them
|
|
73
|
+
* can see. Instance-wide uniqueness is therefore not expressible on a Site-scoped table, which is the
|
|
74
|
+
* intended answer rather than a missing feature.
|
|
75
|
+
*/
|
|
58
76
|
unique?: boolean;
|
|
59
|
-
default?:
|
|
77
|
+
default?: PhisAddonColumnDefault;
|
|
60
78
|
};
|
|
61
79
|
/**
|
|
62
80
|
* A column that points at something, and therefore has no type of its own.
|
|
@@ -66,18 +84,25 @@ export type PhiServerAddonValueColumnDescriptor = {
|
|
|
66
84
|
* shape a foreign key takes -- there is no separate list of them, and no composite key, because a
|
|
67
85
|
* reference that needs two columns is pointing at something an Add-on should not be reaching into.
|
|
68
86
|
*/
|
|
69
|
-
export type
|
|
87
|
+
export type PhisAddonReferenceColumnDescriptor = {
|
|
70
88
|
name: string;
|
|
71
|
-
/**
|
|
72
|
-
|
|
89
|
+
/**
|
|
90
|
+
* A Core table under its symbolic name, or one of this Add-on's own tables.
|
|
91
|
+
*
|
|
92
|
+
* A reference to a Site-scoped table is bound together with its Site, the way a group reference is:
|
|
93
|
+
* Core writes `(column, site_id)` against the target's `(key, site_id)`, and both tables must be
|
|
94
|
+
* Site-scoped. The id alone would let a row of one Site name a row of another, and nothing about the
|
|
95
|
+
* declaration would show it -- the column would look exactly the same and the constraint would hold.
|
|
96
|
+
*/
|
|
97
|
+
references: PhisAddonCoreReference | {
|
|
73
98
|
table: string;
|
|
74
99
|
};
|
|
75
100
|
nullable?: boolean;
|
|
76
101
|
unique?: boolean;
|
|
77
102
|
/** `setNull` requires a nullable column. */
|
|
78
|
-
onDelete:
|
|
103
|
+
onDelete: PhisAddonDeleteRule;
|
|
79
104
|
};
|
|
80
|
-
export type
|
|
105
|
+
export type PhisAddonColumnDescriptor = PhisAddonValueColumnDescriptor | PhisAddonReferenceColumnDescriptor;
|
|
81
106
|
/**
|
|
82
107
|
* A constraint, in structured form.
|
|
83
108
|
*
|
|
@@ -88,7 +113,7 @@ export type PhiServerAddonColumnDescriptor = PhiServerAddonValueColumnDescriptor
|
|
|
88
113
|
* Regular expressions are absent on purpose. Their syntax differs between database systems, and taking
|
|
89
114
|
* them would buy back the dependency this file exists to remove.
|
|
90
115
|
*/
|
|
91
|
-
export type
|
|
116
|
+
export type PhisAddonTableConstraintDescriptor = {
|
|
92
117
|
kind: "range";
|
|
93
118
|
name: string;
|
|
94
119
|
column: string;
|
|
@@ -119,21 +144,115 @@ export type PhiServerAddonTableConstraintDescriptor = {
|
|
|
119
144
|
columns: string[];
|
|
120
145
|
nonNullCount: number;
|
|
121
146
|
};
|
|
122
|
-
|
|
147
|
+
/**
|
|
148
|
+
* What an index is for, said abstractly so Core can choose the structure.
|
|
149
|
+
*
|
|
150
|
+
* `ordered` is the ordinary one: equality, ranges, and the orderings a paged select runs under.
|
|
151
|
+
* `text` is for substring search, names exactly one text column, and is what makes `contains` more than
|
|
152
|
+
* a scan -- measured on 200,000 rows, 72 ms without it and 0.5 ms with. It cannot be unique, because it
|
|
153
|
+
* indexes fragments rather than values.
|
|
154
|
+
*
|
|
155
|
+
* Absent means `ordered`: an index declared before this field existed had not forgotten it.
|
|
156
|
+
*/
|
|
157
|
+
export type PhisAddonIndexKind = "ordered" | "text";
|
|
158
|
+
export type PhisAddonIndexDescriptor = {
|
|
123
159
|
name: string;
|
|
160
|
+
kind?: PhisAddonIndexKind;
|
|
124
161
|
columns: string[];
|
|
162
|
+
/**
|
|
163
|
+
* Unique, and read exactly as `unique` on a column: within the Site, on a Site-scoped table, with the
|
|
164
|
+
* Site column prepended by Core.
|
|
165
|
+
*
|
|
166
|
+
* A non-unique index is only an access path and is left as it was written. Which order helps depends
|
|
167
|
+
* on the query, and Core does not know it.
|
|
168
|
+
*/
|
|
125
169
|
unique?: boolean;
|
|
126
170
|
};
|
|
127
|
-
|
|
171
|
+
/**
|
|
172
|
+
* A named place on a row where a file belongs.
|
|
173
|
+
*
|
|
174
|
+
* The names are the Add-on's -- `logo`, `package`, `screenshots` -- and the set of them is closed, which
|
|
175
|
+
* is what makes the rest possible. Core can only enforce a size and a type where it knows which slot is
|
|
176
|
+
* being filled, and it can only tell an abandoned object from a live one where the vocabulary is finite.
|
|
177
|
+
* An Add-on that needs no files declares none and gets no table.
|
|
178
|
+
*
|
|
179
|
+
* The object key is Core's, never the Add-on's. Two rows cannot be made to name the same object, a key
|
|
180
|
+
* cannot be spelled that reaches outside the Add-on's prefix, and replacing a file writes a new key
|
|
181
|
+
* rather than overwriting the old -- overwriting destroys the current file before the row that replaces
|
|
182
|
+
* it is committed, and a new key leaves the old one behind for the sweep to collect.
|
|
183
|
+
*
|
|
184
|
+
* Requires `siteScoped`. An object lives in one Site's store, because that is where the Site's Storage
|
|
185
|
+
* Profile points; a row shared across Sites has no store its file could belong to.
|
|
186
|
+
*/
|
|
187
|
+
export type PhisAddonAssetSlotDescriptor = {
|
|
188
|
+
name: string;
|
|
189
|
+
/**
|
|
190
|
+
* `one` is a slot that holds the current file and forgets the previous one; `many` accumulates.
|
|
191
|
+
*
|
|
192
|
+
* Enforced on the finished file only. An upload replacing a `one` slot exists alongside the file it
|
|
193
|
+
* will replace until it is complete, because the alternative is a row with nothing behind it for as
|
|
194
|
+
* long as the transfer takes -- and a transfer that fails would have destroyed the file it never
|
|
195
|
+
* managed to replace.
|
|
196
|
+
*/
|
|
197
|
+
cardinality: "one" | "many";
|
|
198
|
+
/** Refused before a byte is accepted, and not raisable by the Add-on that declared it. */
|
|
199
|
+
maxBytes: number;
|
|
200
|
+
/**
|
|
201
|
+
* The exact types this slot takes, as full media types. Empty is not "anything", it is refused.
|
|
202
|
+
*
|
|
203
|
+
* Stated per slot rather than per Add-on because the answers differ within one package: a logo is a
|
|
204
|
+
* handful of image types and a release artifact is one archive type, and a single list wide enough for
|
|
205
|
+
* both permits an image where the archive goes.
|
|
206
|
+
*/
|
|
207
|
+
contentTypes: string[];
|
|
208
|
+
/**
|
|
209
|
+
* Who may fetch the file once it is there. `public` when absent.
|
|
210
|
+
*
|
|
211
|
+
* Stated per slot for the same reason the types are, and the two cases really do differ: the logo on
|
|
212
|
+
* a listing has to be readable by anyone who can see the listing, and the release artifact behind it
|
|
213
|
+
* must not be. Declaring it is what stops a package from having to choose one answer for both.
|
|
214
|
+
*
|
|
215
|
+
* It is here rather than left to a later setting because an installed package would otherwise carry
|
|
216
|
+
* whatever the default happened to be, and a file that was public once has been public.
|
|
217
|
+
*/
|
|
218
|
+
delivery?: "public" | "authenticated" | "internal";
|
|
219
|
+
/**
|
|
220
|
+
* A column on the owning table that Core keeps the file's delivery path in.
|
|
221
|
+
*
|
|
222
|
+
* Without it, showing a row's picture means asking for its files -- one question per row, so a page
|
|
223
|
+
* of fifty costs fifty. With it the path arrives with the row, in the statement that was being run
|
|
224
|
+
* anyway, and a listing is one query again.
|
|
225
|
+
*
|
|
226
|
+
* The column is Core's: it is created from this declaration, written when a file is settled, rewritten
|
|
227
|
+
* when a file replaces it, cleared when it is taken away, and refused as an assignment in every
|
|
228
|
+
* declared query. An Add-on that could write it could point a row at somebody else's file.
|
|
229
|
+
*
|
|
230
|
+
* The path rather than the Media Asset id, because the path is what gets used and the id alone is not
|
|
231
|
+
* enough to build one: a delivery path carries the cache revision, and reading that back would be the
|
|
232
|
+
* join this exists to avoid.
|
|
233
|
+
*
|
|
234
|
+
* Only on a `one` slot. A slot that accumulates has no single path to mirror, and a column holding the
|
|
235
|
+
* newest of several would be a fact nobody asked for.
|
|
236
|
+
*/
|
|
237
|
+
mirrorColumn?: string;
|
|
238
|
+
};
|
|
239
|
+
export type PhisAddonTableDescriptor = {
|
|
128
240
|
name: string;
|
|
129
241
|
/**
|
|
130
|
-
* Core adds the Site column, its foreign key, and its cascade,
|
|
131
|
-
*
|
|
242
|
+
* Core adds the Site column, its foreign key, and its cascade, puts the Site filter into every query
|
|
243
|
+
* that reads this table, and prepends it to every uniqueness rule the table declares. The Add-on
|
|
244
|
+
* neither writes the column nor is able to forget it.
|
|
132
245
|
*
|
|
133
246
|
* This is more than a reference column: it is the boundary a query is compiled against. A table that
|
|
134
247
|
* merely wants to mention a Site uses a reference column instead.
|
|
248
|
+
*
|
|
249
|
+
* Required, and required as a word rather than as an absence. `false` is a real answer -- a category
|
|
250
|
+
* list, a currency table, a set of country codes belong to the Add-on and to no Site -- but it is an
|
|
251
|
+
* answer somebody has to give. Left optional, the unguarded table is what an author gets for saying
|
|
252
|
+
* nothing, and every other boundary in this file works the other way round: Core supplies it and the
|
|
253
|
+
* Add-on cannot forget it. Here it cannot supply it, so it insists on being told.
|
|
135
254
|
*/
|
|
136
|
-
siteScoped
|
|
255
|
+
siteScoped: boolean;
|
|
137
256
|
/**
|
|
138
257
|
* Core adds the owner column and its foreign key, and answers whether a row belongs to the acting
|
|
139
258
|
* user. The Add-on neither writes the column nor decides what "own" means, which is the point: the
|
|
@@ -142,6 +261,12 @@ export type PhiServerAddonTableDescriptor = {
|
|
|
142
261
|
* The column is nullable and a deleted user leaves it null rather than taking the rows along. A row
|
|
143
262
|
* without an owner therefore belongs to nobody, and no membership level reaches it through ownership.
|
|
144
263
|
*
|
|
264
|
+
* Requires `siteScoped`, for the reason a group does. Everywhere else Core weighs a person it weighs
|
|
265
|
+
* them on a Site -- an authorization is read as a pair, and a disabled membership answers no. Ownership
|
|
266
|
+
* without a Site is the one place that pair would come apart: the same account would reach a row from
|
|
267
|
+
* a Site it was never admitted to, and the comparison that let it through looked at nothing but a user
|
|
268
|
+
* id.
|
|
269
|
+
*
|
|
145
270
|
* This is authority, not history. A table that only wants to record who created a row uses an
|
|
146
271
|
* ordinary `core.user` reference column, which grants nothing.
|
|
147
272
|
*/
|
|
@@ -155,10 +280,19 @@ export type PhiServerAddonTableDescriptor = {
|
|
|
155
280
|
* reference column instead.
|
|
156
281
|
*/
|
|
157
282
|
groupScoped?: boolean;
|
|
158
|
-
columns:
|
|
283
|
+
columns: PhisAddonColumnDescriptor[];
|
|
159
284
|
primaryKey: string[];
|
|
160
|
-
indexes?:
|
|
161
|
-
constraints?:
|
|
285
|
+
indexes?: PhisAddonIndexDescriptor[];
|
|
286
|
+
constraints?: PhisAddonTableConstraintDescriptor[];
|
|
287
|
+
/**
|
|
288
|
+
* The files rows of this table may carry, which Core keeps in a table of its own beside this one.
|
|
289
|
+
*
|
|
290
|
+
* A child table rather than a column per slot, because the slots are the Add-on's to name and their
|
|
291
|
+
* number is the Add-on's to choose -- and because a gallery is a slot with more than one file in it,
|
|
292
|
+
* which no column can hold. The child cascades: removing a row removes the rows describing its files,
|
|
293
|
+
* and the objects they named become collectable.
|
|
294
|
+
*/
|
|
295
|
+
assets?: PhisAddonAssetSlotDescriptor[];
|
|
162
296
|
};
|
|
163
297
|
/**
|
|
164
298
|
* The shape an Add-on's schema should have -- not the steps to reach it.
|
|
@@ -166,8 +300,8 @@ export type PhiServerAddonTableDescriptor = {
|
|
|
166
300
|
* `version` is the schema version the manifest reports, and Core compares this description against the
|
|
167
301
|
* one it recorded to decide what to run.
|
|
168
302
|
*/
|
|
169
|
-
export type
|
|
170
|
-
descriptorVersion: typeof
|
|
303
|
+
export type PhisAddonSchemaDescriptor = {
|
|
304
|
+
descriptorVersion: typeof PHIS_ADDON_SCHEMA_DESCRIPTOR_VERSION;
|
|
171
305
|
version: number;
|
|
172
|
-
tables:
|
|
306
|
+
tables: PhisAddonTableDescriptor[];
|
|
173
307
|
};
|
package/dist/schema.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* exactly what this removes. An Add-on that needs one of them is either cut wrong or needs a Core
|
|
11
11
|
* service.
|
|
12
12
|
*/
|
|
13
|
-
export const
|
|
13
|
+
export const PHIS_ADDON_SCHEMA_DESCRIPTOR_VERSION = 1;
|
|
14
14
|
/**
|
|
15
15
|
* A Core table an Add-on may point at, named symbolically.
|
|
16
16
|
*
|
|
@@ -22,8 +22,17 @@ export const PHI_SERVER_ADDON_SCHEMA_DESCRIPTOR_VERSION = 1;
|
|
|
22
22
|
* binds the pair rather than the id alone. Core never references a group any other way, and an Add-on
|
|
23
23
|
* that could would be able to name a group belonging to a different Site.
|
|
24
24
|
*/
|
|
25
|
-
export const
|
|
25
|
+
export const PHIS_ADDON_CORE_REFERENCES = {
|
|
26
26
|
site: "core.site",
|
|
27
27
|
user: "core.user",
|
|
28
28
|
group: "core.group",
|
|
29
|
+
/**
|
|
30
|
+
* A stored file, which is the one Core row an Add-on points at without having written it.
|
|
31
|
+
*
|
|
32
|
+
* Core keeps the facts about an object -- where it is, how big, what type, which digest -- in one
|
|
33
|
+
* place for every upload it has ever handled, whoever asked. An Add-on's asset row names one of them
|
|
34
|
+
* and holds nothing about it, so there is no second copy to fall out of step, and no key an Add-on
|
|
35
|
+
* could pass anywhere.
|
|
36
|
+
*/
|
|
37
|
+
mediaAsset: "core.mediaAsset",
|
|
29
38
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What phis says about a capability provider, in the words the site UI reads it with.
|
|
3
|
+
*
|
|
4
|
+
* `/api/v1/site/capabilities` reports, per provider, whether what a Site's Modules require is actually
|
|
5
|
+
* being offered. phis decides the verdict; the UI acts on it, deactivating a Module whose provider is
|
|
6
|
+
* not `available` and naming the reason in its diagnostics. A verdict one side can produce and the
|
|
7
|
+
* other cannot recognise is not an error anywhere -- it is a Module that quietly stays off.
|
|
8
|
+
*
|
|
9
|
+
* The state was declared twice, once on each side, in the same six words. That is the arrangement that
|
|
10
|
+
* let the log services drift while both files still compiled, so it lives here now -- and the shape
|
|
11
|
+
* around it followed, because a snapshot phis only built and the UI only read was the same arrangement
|
|
12
|
+
* one level up.
|
|
13
|
+
*
|
|
14
|
+
* A provider is Core or an Add-on. Core is one itself, and the one most Modules depend on, which is
|
|
15
|
+
* why nothing here is named for Add-ons.
|
|
16
|
+
*/
|
|
17
|
+
/** `@scope/name` -- Core is `@phis/server/core`. */
|
|
18
|
+
export type PhiCapabilityProviderId = `@${string}/${string}`;
|
|
19
|
+
/** `@scope/name:v<major>` -- a capability at the interface version it is offered under. */
|
|
20
|
+
export type PhiCapabilityId = `@${string}/${string}:v${number}`;
|
|
21
|
+
export type PhiCapabilityState = "available" | "missing" | "disabled" | "incompatible" | "misconfigured" | "unavailable";
|
|
22
|
+
export type PhiCapabilityDescriptor = {
|
|
23
|
+
id: PhiCapabilityId;
|
|
24
|
+
interfaceDigest: string;
|
|
25
|
+
};
|
|
26
|
+
export type PhiCapabilityProvider = {
|
|
27
|
+
providerId: PhiCapabilityProviderId;
|
|
28
|
+
state: PhiCapabilityState;
|
|
29
|
+
diagnosticCode: string | null;
|
|
30
|
+
capabilities: readonly PhiCapabilityDescriptor[];
|
|
31
|
+
};
|
|
32
|
+
export type PhiCapabilitySnapshot = {
|
|
33
|
+
siteKey: string;
|
|
34
|
+
releaseBuildId: string | null;
|
|
35
|
+
buildManifestDigest: string;
|
|
36
|
+
providers: readonly PhiCapabilityProvider[];
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What phis says about a capability provider, in the words the site UI reads it with.
|
|
3
|
+
*
|
|
4
|
+
* `/api/v1/site/capabilities` reports, per provider, whether what a Site's Modules require is actually
|
|
5
|
+
* being offered. phis decides the verdict; the UI acts on it, deactivating a Module whose provider is
|
|
6
|
+
* not `available` and naming the reason in its diagnostics. A verdict one side can produce and the
|
|
7
|
+
* other cannot recognise is not an error anywhere -- it is a Module that quietly stays off.
|
|
8
|
+
*
|
|
9
|
+
* The state was declared twice, once on each side, in the same six words. That is the arrangement that
|
|
10
|
+
* let the log services drift while both files still compiled, so it lives here now -- and the shape
|
|
11
|
+
* around it followed, because a snapshot phis only built and the UI only read was the same arrangement
|
|
12
|
+
* one level up.
|
|
13
|
+
*
|
|
14
|
+
* A provider is Core or an Add-on. Core is one itself, and the one most Modules depend on, which is
|
|
15
|
+
* why nothing here is named for Add-ons.
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
package/dist/signals.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* of widget shapes that belongs to the UI: phi-server checks that a JSON signal names *a* schema, never
|
|
11
11
|
* which one, so the catalogue is not something the two must agree on.
|
|
12
12
|
*/
|
|
13
|
+
import { type PhiCmsInstanceId } from "./cms.js";
|
|
13
14
|
export declare const PHI_SIGNAL_SCOPES: readonly ["widget", "layout", "region", "page", "area", "site"];
|
|
14
15
|
export type PhiSignalScope = (typeof PHI_SIGNAL_SCOPES)[number];
|
|
15
16
|
export declare const PHI_SIGNAL_ACTIONS: readonly ["activate", "change", "toggle", "start", "stop", "clear", "open", "close", "reload", "flush", "filter", "drop"];
|
|
@@ -34,3 +35,60 @@ export declare function isPhiSignalValueType(value: unknown): value is PhiSignal
|
|
|
34
35
|
* The last separator wins, because a scoped package name contains one too.
|
|
35
36
|
*/
|
|
36
37
|
export declare function isPhiSignalValueSchemaShape(value: unknown): value is PhiSignalValueSchema;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a string can name an npm package.
|
|
40
|
+
*
|
|
41
|
+
* The lexical rule the controller address family starts from, and the one phi-server had copied under
|
|
42
|
+
* a different name to validate the same addresses. It is the same test `@phis/ui` keeps beside its own
|
|
43
|
+
* package name for the grammars that do not travel -- module ids, module-scoped keys. If a second
|
|
44
|
+
* grammar in this package needs it, that is the moment to give it a subpath of its own.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isPhiNpmPackageName(value: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The address families a signal may name, and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* A wiring is stored by the Builder, validated by phi-server on the way in, and delivered by the site
|
|
51
|
+
* UI at runtime. All three read the same string, so all three have to parse it alike -- and until
|
|
52
|
+
* 2026-09-07 two of them did it from separate copies, one of which said in its own comment that it
|
|
53
|
+
* "mirrors the grammar in @phis/ui" because it could not import it.
|
|
54
|
+
*
|
|
55
|
+
* `cms:` addresses a Widget or Layout instance, optionally one control inside it. `region:` addresses a
|
|
56
|
+
* Region of the route's active context. `controller:` addresses one Controller instance in the
|
|
57
|
+
* namespace of the Module that owns it.
|
|
58
|
+
*/
|
|
59
|
+
export type PhiSignalAddress = `cms:${PhiCmsInstanceId}` | `cms:${PhiCmsInstanceId}:${string}` | `region:${string}` | `controller:${string}/${string}:${string}`;
|
|
60
|
+
export type PhiControllerSignalAddress = Extract<PhiSignalAddress, `controller:${string}`>;
|
|
61
|
+
export type PhiSignalSender = PhiSignalAddress | null;
|
|
62
|
+
export type PhiSignalReceiver = PhiSignalAddress | "broadcast" | null;
|
|
63
|
+
export type PhiSignalAddressFamily = "cms" | "region";
|
|
64
|
+
/**
|
|
65
|
+
* The one address the Site scope privileges.
|
|
66
|
+
*
|
|
67
|
+
* Site-scoped signals reach across Areas, so nothing addressable inside one may be named in that scope
|
|
68
|
+
* -- no CMS instance, no Region, no broadcast. The Core Runtime Controller is the exception, and both
|
|
69
|
+
* sides have to name the same string for the rule to mean anything. `@phis/ui` builds it from its own
|
|
70
|
+
* constants and asserts it matches this one.
|
|
71
|
+
*/
|
|
72
|
+
export declare const PHI_CORE_RUNTIME_CONTROLLER_ADDRESS: PhiControllerSignalAddress;
|
|
73
|
+
export declare function isPhiSignalAddressSegment(value: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* A controller lives in the namespace of the Module that owns it, so a plugin key is either a bare
|
|
76
|
+
* package name or that package followed by `/modules/<module>/<namespace>`. Foreign packages use
|
|
77
|
+
* either form; the grammar does not privilege first-party keys.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isPhiControllerPluginKey(value: string): boolean;
|
|
80
|
+
export declare function isPhiControllerSignalAddress(address: unknown): address is PhiControllerSignalAddress;
|
|
81
|
+
/** The instance a `cms:` address names, or null when the string is not one. */
|
|
82
|
+
export declare function readPhiCmsSignalAddressInstanceId(address: string): PhiCmsInstanceId | null;
|
|
83
|
+
export declare function isPhiRegionSignalAddress(address: string): boolean;
|
|
84
|
+
export declare function isPhiSignalAddress(value: unknown): value is PhiSignalAddress;
|
|
85
|
+
export declare function readPhiSignalAddress(value: unknown): PhiSignalAddress | undefined;
|
|
86
|
+
export declare function isPhiSignalReceiver(value: unknown): value is PhiSignalReceiver;
|
|
87
|
+
/**
|
|
88
|
+
* Why a receiver may not be addressed in a scope, said in one sentence, or nothing when it may.
|
|
89
|
+
*
|
|
90
|
+
* The same sentence twice over until now: phi-server refused these on the way in, the site UI refused
|
|
91
|
+
* to deliver them at runtime, and neither knew the other's wording. What the two still do differently
|
|
92
|
+
* is what they do with the answer -- one rejects a write, the other drops a signal.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readPhiSignalReceiverScopeProblem(scope: PhiSignalScope, receiver: PhiSignalReceiver): string | null;
|
package/dist/signals.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* of widget shapes that belongs to the UI: phi-server checks that a JSON signal names *a* schema, never
|
|
11
11
|
* which one, so the catalogue is not something the two must agree on.
|
|
12
12
|
*/
|
|
13
|
+
import { isPhiCmsInstanceId } from "./cms.js";
|
|
13
14
|
export const PHI_SIGNAL_SCOPES = [
|
|
14
15
|
"widget",
|
|
15
16
|
"layout",
|
|
@@ -83,3 +84,120 @@ export function isPhiSignalValueSchemaShape(value) {
|
|
|
83
84
|
const leaf = value.slice(marker + PHI_SIGNAL_VALUE_SCHEMA_NAMESPACE.length + PHI_SIGNAL_VALUE_SCHEMA_SEPARATOR.length * 2);
|
|
84
85
|
return leaf.length > 0 && !leaf.includes(PHI_SIGNAL_VALUE_SCHEMA_SEPARATOR);
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether a string can name an npm package.
|
|
89
|
+
*
|
|
90
|
+
* The lexical rule the controller address family starts from, and the one phi-server had copied under
|
|
91
|
+
* a different name to validate the same addresses. It is the same test `@phis/ui` keeps beside its own
|
|
92
|
+
* package name for the grammars that do not travel -- module ids, module-scoped keys. If a second
|
|
93
|
+
* grammar in this package needs it, that is the moment to give it a subpath of its own.
|
|
94
|
+
*/
|
|
95
|
+
export function isPhiNpmPackageName(value) {
|
|
96
|
+
if (!value || value.includes(":") || value.length > 214) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return value.startsWith("@")
|
|
100
|
+
? /^@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*$/.test(value)
|
|
101
|
+
: /^[a-z0-9][a-z0-9._~-]*$/.test(value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The one address the Site scope privileges.
|
|
105
|
+
*
|
|
106
|
+
* Site-scoped signals reach across Areas, so nothing addressable inside one may be named in that scope
|
|
107
|
+
* -- no CMS instance, no Region, no broadcast. The Core Runtime Controller is the exception, and both
|
|
108
|
+
* sides have to name the same string for the rule to mean anything. `@phis/ui` builds it from its own
|
|
109
|
+
* constants and asserts it matches this one.
|
|
110
|
+
*/
|
|
111
|
+
export const PHI_CORE_RUNTIME_CONTROLLER_ADDRESS = "controller:@phis/ui/modules/core/controller/default:default";
|
|
112
|
+
export function isPhiSignalAddressSegment(value) {
|
|
113
|
+
return value.length > 0 && !value.includes("/") && !value.includes(":");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A controller lives in the namespace of the Module that owns it, so a plugin key is either a bare
|
|
117
|
+
* package name or that package followed by `/modules/<module>/<namespace>`. Foreign packages use
|
|
118
|
+
* either form; the grammar does not privilege first-party keys.
|
|
119
|
+
*/
|
|
120
|
+
export function isPhiControllerPluginKey(value) {
|
|
121
|
+
const marker = value.indexOf("/modules/");
|
|
122
|
+
if (marker < 0) {
|
|
123
|
+
return isPhiNpmPackageName(value);
|
|
124
|
+
}
|
|
125
|
+
const rest = value.slice(marker + "/modules/".length).split("/");
|
|
126
|
+
return (isPhiNpmPackageName(value.slice(0, marker)) &&
|
|
127
|
+
rest.length === 2 &&
|
|
128
|
+
rest.every((part) => isPhiSignalAddressSegment(part)));
|
|
129
|
+
}
|
|
130
|
+
export function isPhiControllerSignalAddress(address) {
|
|
131
|
+
if (typeof address !== "string" || !address.startsWith("controller:")) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const body = address.slice("controller:".length);
|
|
135
|
+
const instanceSeparatorIndex = body.lastIndexOf(":");
|
|
136
|
+
if (instanceSeparatorIndex <= 0 || instanceSeparatorIndex === body.length - 1) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const namespacedType = body.slice(0, instanceSeparatorIndex);
|
|
140
|
+
const instanceKey = body.slice(instanceSeparatorIndex + 1);
|
|
141
|
+
const controllerSeparatorIndex = namespacedType.lastIndexOf("/");
|
|
142
|
+
if (controllerSeparatorIndex <= 0 || controllerSeparatorIndex === namespacedType.length - 1) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return (isPhiControllerPluginKey(namespacedType.slice(0, controllerSeparatorIndex)) &&
|
|
146
|
+
isPhiSignalAddressSegment(namespacedType.slice(controllerSeparatorIndex + 1)) &&
|
|
147
|
+
isPhiSignalAddressSegment(instanceKey));
|
|
148
|
+
}
|
|
149
|
+
/** The instance a `cms:` address names, or null when the string is not one. */
|
|
150
|
+
export function readPhiCmsSignalAddressInstanceId(address) {
|
|
151
|
+
if (!address.startsWith("cms:")) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const parts = address.slice("cms:".length).split(":");
|
|
155
|
+
if ((parts.length !== 1 && parts.length !== 2) ||
|
|
156
|
+
!isPhiCmsInstanceId(parts[0]) ||
|
|
157
|
+
(parts.length === 2 && !isPhiSignalAddressSegment(parts[1]))) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return parts[0];
|
|
161
|
+
}
|
|
162
|
+
export function isPhiRegionSignalAddress(address) {
|
|
163
|
+
return address.startsWith("region:") && isPhiSignalAddressSegment(address.slice("region:".length));
|
|
164
|
+
}
|
|
165
|
+
export function isPhiSignalAddress(value) {
|
|
166
|
+
if (typeof value !== "string") {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const address = value.trim();
|
|
170
|
+
return readPhiCmsSignalAddressInstanceId(address) != null ||
|
|
171
|
+
isPhiRegionSignalAddress(address) ||
|
|
172
|
+
isPhiControllerSignalAddress(address);
|
|
173
|
+
}
|
|
174
|
+
export function readPhiSignalAddress(value) {
|
|
175
|
+
return isPhiSignalAddress(value) ? value.trim() : undefined;
|
|
176
|
+
}
|
|
177
|
+
export function isPhiSignalReceiver(value) {
|
|
178
|
+
return value === null || value === "broadcast" || isPhiSignalAddress(value);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Why a receiver may not be addressed in a scope, said in one sentence, or nothing when it may.
|
|
182
|
+
*
|
|
183
|
+
* The same sentence twice over until now: phi-server refused these on the way in, the site UI refused
|
|
184
|
+
* to deliver them at runtime, and neither knew the other's wording. What the two still do differently
|
|
185
|
+
* is what they do with the answer -- one rejects a write, the other drops a signal.
|
|
186
|
+
*/
|
|
187
|
+
export function readPhiSignalReceiverScopeProblem(scope, receiver) {
|
|
188
|
+
if (scope !== "site" || receiver === null) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (receiver === "broadcast") {
|
|
192
|
+
return "must not broadcast in Site scope";
|
|
193
|
+
}
|
|
194
|
+
if (readPhiCmsSignalAddressInstanceId(receiver) != null) {
|
|
195
|
+
return "must not target a CMS instance in Site scope";
|
|
196
|
+
}
|
|
197
|
+
if (isPhiRegionSignalAddress(receiver)) {
|
|
198
|
+
return "must not target a Region in Site scope";
|
|
199
|
+
}
|
|
200
|
+
return receiver === PHI_CORE_RUNTIME_CONTROLLER_ADDRESS
|
|
201
|
+
? null
|
|
202
|
+
: "may target only the Core Runtime Controller in Site scope";
|
|
203
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The membership ladder of a Site group, as both ends must count it.
|
|
3
|
+
*
|
|
4
|
+
* These numbers cross the wire. A membership row is written by `PATCH .../members/<userId>` carrying
|
|
5
|
+
* `membershipFlags` as an integer, and the administration surface offers the levels to choose from, so
|
|
6
|
+
* the site UI does arithmetic on the same bits phis stores. (The Add-on boundary is the other case:
|
|
7
|
+
* there a level crosses by name, as `PHIS_GROUP_LEVELS`, because an Add-on has no reason to
|
|
8
|
+
* know the bits at all.)
|
|
9
|
+
*
|
|
10
|
+
* They were declared on both sides and had drifted, which cost a level. phis counts four --
|
|
11
|
+
* `GROUPS_AND_STORAGE.md` enforces `Manager => Editor => Author => Member` -- and the UI counted
|
|
12
|
+
* three, calling `1|2` a Contributor and `1|2|4` a Manager. So the UI's Manager was phis's Editor:
|
|
13
|
+
* granting it left out the bit that administers membership and deletes what others made, and the
|
|
14
|
+
* viewer was shown a level they did not have. An actual Manager, at `1|2|4|8`, the UI could not name.
|
|
15
|
+
*
|
|
16
|
+
* The levels are cumulative, and that is the point of the ordering rather than an implementation
|
|
17
|
+
* detail: "may edit only their own" is a narrowing of "may edit", so an Author sits below an Editor
|
|
18
|
+
* on one ladder rather than beside them on another.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PhiGroupMembershipFlags: {
|
|
21
|
+
/** Reads what the group holds. */
|
|
22
|
+
readonly Member: 1;
|
|
23
|
+
/** Creates, and may edit and delete what they created. */
|
|
24
|
+
readonly Author: number;
|
|
25
|
+
/** Reaches what others created. */
|
|
26
|
+
readonly Editor: number;
|
|
27
|
+
/** Deletes what anyone created, and administers the group's membership. */
|
|
28
|
+
readonly Manager: number;
|
|
29
|
+
};
|
|
30
|
+
export type PhiGroupMembershipFlagValue = (typeof PhiGroupMembershipFlags)[keyof typeof PhiGroupMembershipFlags];
|
|
31
|
+
/** Ascending, so a selector offers them in the order they widen. */
|
|
32
|
+
export declare const PHI_GROUP_MEMBERSHIP_LEVELS: readonly [1, number, number, number];
|
|
33
|
+
/**
|
|
34
|
+
* The level a stored value amounts to, highest bit first, or `null` if it is not one.
|
|
35
|
+
*
|
|
36
|
+
* Reading downwards is what makes an unknown higher bit fail closed rather than round up.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readPhiGroupMembershipLevel(value: unknown): PhiGroupMembershipFlagValue | null;
|
|
39
|
+
/**
|
|
40
|
+
* A group a Module needs the Site to have, named rather than created.
|
|
41
|
+
*
|
|
42
|
+
* It travels in a Module's `seed` and is written when the Area carrying that Module is published. The
|
|
43
|
+
* key is the Module's own and never changes: it is what makes a second publish find the group it made
|
|
44
|
+
* the first time instead of making another. `provider_id` is the Module's id and is supplied by the
|
|
45
|
+
* materialization, not by the declaration, so a Module cannot claim a group belonging to another.
|
|
46
|
+
*
|
|
47
|
+
* The name is a starting value. Once the row exists it is the Site's, and administration renames it
|
|
48
|
+
* without the next publish undoing that.
|
|
49
|
+
*/
|
|
50
|
+
export type PhisDeclaredSiteGroup = {
|
|
51
|
+
readonly key: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
};
|