@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/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @phis/contracts
|
|
2
2
|
|
|
3
|
-
The agreements phi-server keeps with what surrounds it.
|
|
4
|
-
|
|
3
|
+
The agreements phi-server keeps with what surrounds it. Each under its own subpath, because they hold
|
|
4
|
+
between different parties and freeze at different moments.
|
|
5
5
|
|
|
6
6
|
```
|
|
7
7
|
@phis/contracts/addon what phi-server and a separately shipped Add-on promise each other
|
|
8
8
|
@phis/contracts/access the authorization vocabulary phi-server and @phis/ui both evaluate
|
|
9
|
-
@phis/contracts/signals the signal vocabulary the UI declares
|
|
9
|
+
@phis/contracts/signals the signal vocabulary and address grammar the UI declares in, phi-server validates
|
|
10
|
+
@phis/contracts/catalog the Module category an Add-on declares, the UI groups by, a market will filter on
|
|
11
|
+
@phis/contracts/cms the CMS node identity both sides derive, and must derive alike
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
There is deliberately no root export. A package you can import from the top invites everything that
|
|
@@ -45,6 +47,24 @@ looked wrong on its own, which is why it went unnoticed.
|
|
|
45
47
|
Each side keeps its own viewer type and passes a projection onto `PhiAccessSubject`, so neither
|
|
46
48
|
package has to adopt the other's shape.
|
|
47
49
|
|
|
50
|
+
## `/cms`
|
|
51
|
+
|
|
52
|
+
The encoding of a CMS node's identity: how a Preset node's id is derived from the Module, the preset and
|
|
53
|
+
the node key, and how a draft node's id is derived from the revision it was made in.
|
|
54
|
+
|
|
55
|
+
This one is not a shape check. It is an arithmetic, and it is here for the opposite of the usual reason:
|
|
56
|
+
not because the two sides ask the same question, but because they must compute the same answer. A Preset
|
|
57
|
+
id is derived rather than allocated, so the site UI can address a node before any row exists and
|
|
58
|
+
phi-server can check that stored wiring points at nodes that are really there.
|
|
59
|
+
|
|
60
|
+
It stood in two copies until 2026-09-07 -- 170 lines in phi-server, 260 in `@phis/ui`, with
|
|
61
|
+
`hashPresetIdentity` identical line for line. Nothing would have announced a divergence. A changed
|
|
62
|
+
constant on one side produces ids that are still well formed and simply name nothing: Pages stop
|
|
63
|
+
resolving, drafts point at nodes that are not there, and both copies look correct on their own.
|
|
64
|
+
|
|
65
|
+
The two conveniences built on top stay with `@phis/ui`: the Page-key convention and the map helper are
|
|
66
|
+
its own, and phi-server has no use for either.
|
|
67
|
+
|
|
48
68
|
## `/signals`
|
|
49
69
|
|
|
50
70
|
The closed vocabularies a widget's wiring is written in -- scopes, actions, value types -- and the shape
|
|
@@ -55,31 +75,102 @@ the way in. Two lists, one meaning; and when they were two lists they drifted. `
|
|
|
55
75
|
`date`, `time` and `length`; phi-server had not. A length control's change signal -- from a widget the
|
|
56
76
|
same release shipped -- was refused on save with "valueType is invalid".
|
|
57
77
|
|
|
78
|
+
The address families are here for a sharper reason than the vocabularies. A wiring is stored by the
|
|
79
|
+
Builder, checked by phi-server on the way in, and delivered by the site UI at runtime, so all three
|
|
80
|
+
parse the same string -- and two of them parsed it from separate copies until 2026-09-07. The server's
|
|
81
|
+
said so in its own comment: it "mirrors the grammar in @phis/ui", because it could not import it. With
|
|
82
|
+
them the rule that says which receivers a scope admits, which had been written out on both sides in
|
|
83
|
+
different words, and the one controller address the Site scope privileges, which is only worth
|
|
84
|
+
anything if both sides name the same string.
|
|
85
|
+
|
|
58
86
|
Which schemas exist stays with the UI. phi-server checks that a JSON signal names *a* schema, never
|
|
59
87
|
which one, and `isPhiSignalValueSchemaShape` is exactly that much: shape, not membership. The UI asks
|
|
60
88
|
the stricter question, because it holds the module registry; phi-server must not, or it would refuse
|
|
61
89
|
every third party's schema for never having heard of it.
|
|
62
90
|
|
|
91
|
+
## `/catalog`
|
|
92
|
+
|
|
93
|
+
What a Module is for, as one closed list: `foundation`, `workspace`, `content`, `commerce`, `people`,
|
|
94
|
+
`operations`, `other`.
|
|
95
|
+
|
|
96
|
+
An Add-on declares it per Module -- per Module, because a package may ship a shop and a report and
|
|
97
|
+
neither answer would be true of the other. The site UI groups the Modules page by it, and a Module may
|
|
98
|
+
read another's category and show it.
|
|
99
|
+
|
|
100
|
+
It is here rather than in the site UI package because of the reader that cannot follow it there. A
|
|
101
|
+
marketplace is a phi-server Add-on: it compiles against this package and nothing else, never against
|
|
102
|
+
React, and it is meant to let a shopper filter on category. That filter is not built yet -- an
|
|
103
|
+
offering's category is still free text -- but it is why this list is a contract.
|
|
104
|
+
|
|
105
|
+
It is not in `/addon` on purpose. `/addon` is the frozen ABI a third party compiles against, and this
|
|
106
|
+
list grows as the product does -- an eighth category would lift the package everybody builds against,
|
|
107
|
+
for a change that concerns none of them. Nor does phi-server ask the membership question: a package
|
|
108
|
+
manifest carries its Modules' categories as plain strings, Core checks the shape, and the strict
|
|
109
|
+
question is asked where a registry is actually held. The same split as `/signals`.
|
|
110
|
+
|
|
111
|
+
The labels an operator reads are not here. Those are label-set keys, translated per site, and they
|
|
112
|
+
belong to the UI; this subpath holds the identifiers the parties spell the same way.
|
|
113
|
+
|
|
114
|
+
Beside the vocabulary sits the shape a Module package declares itself in, under `phis` in its
|
|
115
|
+
package.json: which Modules it carries, each one's category, and the language their titles and
|
|
116
|
+
descriptions are written in. A Module is compiled UI code, so reading that out of it means executing a
|
|
117
|
+
stranger's package -- inside the `phis` CLI, or inside a marketplace taking a submission. Neither
|
|
118
|
+
should, and a registry serves package.json without anybody fetching a tarball.
|
|
119
|
+
|
|
120
|
+
It is not a second manifest. A package carries one product at one version: the Add-on manifest is what
|
|
121
|
+
phi-server is handed when an artifact is installed, this is what a catalogue reads about the half that
|
|
122
|
+
never reaches phi-server. Same package, same version, different readers.
|
|
123
|
+
|
|
124
|
+
The author writes it, as they write every other field in that file. `phis module` can check that the
|
|
125
|
+
entries are present and well formed; whether `events` is the right answer for a given Module is a
|
|
126
|
+
judgement only the author holds. A package that declares nothing, or declares it wrongly, is refused at
|
|
127
|
+
intake rather than listed on a guess.
|
|
128
|
+
|
|
129
|
+
## `/media`
|
|
130
|
+
|
|
131
|
+
The words and numbers a Site and phis both have to read the same way about an Asset: the Space kind's
|
|
132
|
+
wire name, the media kinds, the Folder flags, the presentation flags, the image variant keys, and the
|
|
133
|
+
two closed vocabularies that decide deliverability -- `lifecycle_status` and `delivery_policy`.
|
|
134
|
+
|
|
135
|
+
It is here because the values themselves travel and are then judged on the other side. The site UI
|
|
136
|
+
sends a flag mask as a list filter and an array of flag values when metadata is saved, and phis answers
|
|
137
|
+
the filter with `presentation_flags & mask <> 0` and refuses a value it does not know. An Asset's
|
|
138
|
+
payload carries `lifecycleStatus` and `deliveryPolicy` as the numbers they are in the row, and the UI
|
|
139
|
+
decides from them whether `next/image` may be pointed at the original.
|
|
140
|
+
|
|
141
|
+
What made it worth moving is what the copies did while nobody looked. Both sides kept their own table,
|
|
142
|
+
and phis still carried `Private`, `Archived` and `Restricted` long after lifecycle and delivery had
|
|
143
|
+
become columns of their own -- so the set of values its metadata route accepted was not the set the UI
|
|
144
|
+
offered. Nothing failed loudly; a drifting copy of a number never does.
|
|
145
|
+
|
|
146
|
+
The resolvers come with their vocabulary. `normalizePhiMediaKind` and
|
|
147
|
+
`resolvePhiMediaKindFromContentType` are total functions onto the kind list, and a content type that
|
|
148
|
+
lands on `document` in the browser and on `other` on the server files an upload as one thing and lists
|
|
149
|
+
it as another.
|
|
150
|
+
|
|
151
|
+
What stays outside: how a delivery URL is built, which is each side's own business, and phis's
|
|
152
|
+
`smallint` Space kind, which never reaches a Site -- the wire carries the name instead.
|
|
153
|
+
|
|
63
154
|
## What `/addon` covers
|
|
64
155
|
|
|
65
|
-
- **Manifest** — `
|
|
156
|
+
- **Manifest** — `PhisAddonManifestV1`: identity, version, required core capabilities,
|
|
66
157
|
the routes an Add-on claims, the services it provides.
|
|
67
|
-
- **Runtime** — `
|
|
68
|
-
`
|
|
69
|
-
- **Schema** — `
|
|
158
|
+
- **Runtime** — `PhisAddonRuntimeV1`, `PhisAddonHandler`,
|
|
159
|
+
`PhisAddonRequestContext`: the shape the artifact's default export must have.
|
|
160
|
+
- **Schema** — `PhisAddonSchemaDescriptor` and its column, index, and constraint
|
|
70
161
|
descriptors. Tables are declared, not migrated by hand; phi-server applies them.
|
|
71
162
|
- **Service kinds** — the interfaces an Add-on may provide or consume, currently the media
|
|
72
163
|
storage adapter (`PhisMediaStorageAdapter`, upload plans, object I/O).
|
|
73
|
-
- **What Core offers** — `
|
|
74
|
-
require; `
|
|
75
|
-
`
|
|
164
|
+
- **What Core offers** — `PHIS_CORE_CAPABILITIES`, the capability ids an Add-on may
|
|
165
|
+
require; `PHIS_SERVICE_KINDS`, the service kinds Core owns; and
|
|
166
|
+
`PHIS_SERVICE_INTERFACE_DIGESTS`, the digest of each kind's current interface. Declare
|
|
76
167
|
against these names rather than spelling them out: Core refuses an Add-on whose digest is not
|
|
77
168
|
the one this release offers, and a literal cannot be checked before you ship it.
|
|
78
169
|
|
|
79
170
|
This list is a promise, not a plan. A capability appears here when Core delivers it, never
|
|
80
171
|
before.
|
|
81
172
|
|
|
82
|
-
`
|
|
173
|
+
`PHIS_ADDON_ABI_VERSION` is the single number a server checks an Add-on against.
|
|
83
174
|
|
|
84
175
|
## Admission rule
|
|
85
176
|
|
|
@@ -92,6 +183,15 @@ Into `/access`: only what **phi-server and `@phis/ui` must both evaluate**, beca
|
|
|
92
183
|
in its own process. Not what both happen to use — a date formatter used on both sides is shared
|
|
93
184
|
convenience, not a contract, and belongs to neither.
|
|
94
185
|
|
|
186
|
+
Into `/catalog`: only the vocabularies an Add-on **declares a Module in** and another party then reads
|
|
187
|
+
back — closed lists, identifiers only, never the words an operator sees. A vocabulary only the UI ever
|
|
188
|
+
reads stays with the UI.
|
|
189
|
+
|
|
190
|
+
Into `/media`: only what **crosses the wire as a value and is judged on the other side** — a number
|
|
191
|
+
stored in a column that a request filters on, a name a payload carries. Not a helper both sides happen
|
|
192
|
+
to have written twice: a copy is only a contract when disagreeing about it changes what a request
|
|
193
|
+
returns.
|
|
194
|
+
|
|
95
195
|
The test for either is the same: name the two parties and the sentence they are promising each
|
|
96
196
|
other. If that sentence cannot be written down, it does not go in here.
|
|
97
197
|
|
package/dist/access.d.ts
CHANGED
|
@@ -30,6 +30,56 @@ export declare const PhiBaseRole: {
|
|
|
30
30
|
readonly Supporter: number;
|
|
31
31
|
readonly Accountant: number;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* What a stored email address says about itself, as bit flags.
|
|
35
|
+
*
|
|
36
|
+
* `user_emails` holds several addresses per account and marks one of them primary. That mark is a flag
|
|
37
|
+
* rather than its own column because the schema keeps every other such answer in `flags`, and a lone
|
|
38
|
+
* boolean is the one place a reader has to remember an exception.
|
|
39
|
+
*
|
|
40
|
+
* The position is fixed from the first row, and here more strictly than elsewhere: the unique index that
|
|
41
|
+
* enforces "one primary address per account" is partial on this very bit. Move the bit and the index
|
|
42
|
+
* silently guards a different question.
|
|
43
|
+
*/
|
|
44
|
+
export declare const PhiUserEmailFlag: {
|
|
45
|
+
readonly Primary: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Where a stored email address came from.
|
|
49
|
+
*
|
|
50
|
+
* The question this answers is asked after the fact and cannot be reconstructed: did this person type
|
|
51
|
+
* their address themselves, did an administrator set it, or did an identity provider hand it over? An
|
|
52
|
+
* account that can be created from a command line makes that difference worth keeping.
|
|
53
|
+
*
|
|
54
|
+
* Numbers rather than free text, because the set is Core's and a string column accepts every typo as a
|
|
55
|
+
* new source. Below 20 are Core's own origins; from 20 upward are named providers, and they are named
|
|
56
|
+
* rather than left to the Add-on that connects them: the Add-on may be replaced, Google will not be.
|
|
57
|
+
* A provider without a number is `Provider`, and which one it was stands in `user_auth_identities`.
|
|
58
|
+
*
|
|
59
|
+
* There is deliberately no `unknown`. Every writer knows why it writes, and a zero default would let
|
|
60
|
+
* the one that does not pass unnoticed.
|
|
61
|
+
*/
|
|
62
|
+
export declare const PhiUserEmailSource: {
|
|
63
|
+
readonly Operator: 1;
|
|
64
|
+
readonly Admin: 2;
|
|
65
|
+
readonly Registration: 3;
|
|
66
|
+
readonly Self: 4;
|
|
67
|
+
readonly Directory: 5;
|
|
68
|
+
readonly Provider: 9;
|
|
69
|
+
readonly Google: 20;
|
|
70
|
+
readonly Apple: 21;
|
|
71
|
+
readonly Microsoft: 22;
|
|
72
|
+
readonly GitHub: 23;
|
|
73
|
+
};
|
|
74
|
+
export type PhiUserEmailSourceValue = (typeof PhiUserEmailSource)[keyof typeof PhiUserEmailSource];
|
|
75
|
+
export declare function isPhiUserEmailSource(value: unknown): value is PhiUserEmailSourceValue;
|
|
76
|
+
/**
|
|
77
|
+
* Which source a login provider's key stands for.
|
|
78
|
+
*
|
|
79
|
+
* The mapping lives here because the numbers do: it is this file that says 20 means Google. A key with
|
|
80
|
+
* no number of its own is `Provider` rather than a guess, and `user_auth_identities` keeps the detail.
|
|
81
|
+
*/
|
|
82
|
+
export declare function phiUserEmailSourceForProvider(providerKey: string): PhiUserEmailSourceValue;
|
|
33
83
|
export type PhiRoleProviderId = `@${string}/${string}`;
|
|
34
84
|
export type PhiGroupProviderId = `@${string}/${string}`;
|
|
35
85
|
export type PhiViewerRoleClaim = {
|
package/dist/access.js
CHANGED
|
@@ -30,6 +30,71 @@ export const PhiBaseRole = {
|
|
|
30
30
|
Supporter: 1 << 5,
|
|
31
31
|
Accountant: 1 << 6,
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* What a stored email address says about itself, as bit flags.
|
|
35
|
+
*
|
|
36
|
+
* `user_emails` holds several addresses per account and marks one of them primary. That mark is a flag
|
|
37
|
+
* rather than its own column because the schema keeps every other such answer in `flags`, and a lone
|
|
38
|
+
* boolean is the one place a reader has to remember an exception.
|
|
39
|
+
*
|
|
40
|
+
* The position is fixed from the first row, and here more strictly than elsewhere: the unique index that
|
|
41
|
+
* enforces "one primary address per account" is partial on this very bit. Move the bit and the index
|
|
42
|
+
* silently guards a different question.
|
|
43
|
+
*/
|
|
44
|
+
export const PhiUserEmailFlag = {
|
|
45
|
+
Primary: 1 << 0,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Where a stored email address came from.
|
|
49
|
+
*
|
|
50
|
+
* The question this answers is asked after the fact and cannot be reconstructed: did this person type
|
|
51
|
+
* their address themselves, did an administrator set it, or did an identity provider hand it over? An
|
|
52
|
+
* account that can be created from a command line makes that difference worth keeping.
|
|
53
|
+
*
|
|
54
|
+
* Numbers rather than free text, because the set is Core's and a string column accepts every typo as a
|
|
55
|
+
* new source. Below 20 are Core's own origins; from 20 upward are named providers, and they are named
|
|
56
|
+
* rather than left to the Add-on that connects them: the Add-on may be replaced, Google will not be.
|
|
57
|
+
* A provider without a number is `Provider`, and which one it was stands in `user_auth_identities`.
|
|
58
|
+
*
|
|
59
|
+
* There is deliberately no `unknown`. Every writer knows why it writes, and a zero default would let
|
|
60
|
+
* the one that does not pass unnoticed.
|
|
61
|
+
*/
|
|
62
|
+
export const PhiUserEmailSource = {
|
|
63
|
+
Operator: 1,
|
|
64
|
+
Admin: 2,
|
|
65
|
+
Registration: 3,
|
|
66
|
+
Self: 4,
|
|
67
|
+
Directory: 5,
|
|
68
|
+
Provider: 9,
|
|
69
|
+
Google: 20,
|
|
70
|
+
Apple: 21,
|
|
71
|
+
Microsoft: 22,
|
|
72
|
+
GitHub: 23,
|
|
73
|
+
};
|
|
74
|
+
export function isPhiUserEmailSource(value) {
|
|
75
|
+
return typeof value === "number"
|
|
76
|
+
&& Object.values(PhiUserEmailSource).includes(value);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Which source a login provider's key stands for.
|
|
80
|
+
*
|
|
81
|
+
* The mapping lives here because the numbers do: it is this file that says 20 means Google. A key with
|
|
82
|
+
* no number of its own is `Provider` rather than a guess, and `user_auth_identities` keeps the detail.
|
|
83
|
+
*/
|
|
84
|
+
export function phiUserEmailSourceForProvider(providerKey) {
|
|
85
|
+
switch (providerKey.trim().toLowerCase()) {
|
|
86
|
+
case "google":
|
|
87
|
+
return PhiUserEmailSource.Google;
|
|
88
|
+
case "apple":
|
|
89
|
+
return PhiUserEmailSource.Apple;
|
|
90
|
+
case "microsoft":
|
|
91
|
+
return PhiUserEmailSource.Microsoft;
|
|
92
|
+
case "github":
|
|
93
|
+
return PhiUserEmailSource.GitHub;
|
|
94
|
+
default:
|
|
95
|
+
return PhiUserEmailSource.Provider;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
33
98
|
/**
|
|
34
99
|
* Flags as they may be trusted.
|
|
35
100
|
*
|
package/dist/addon-index.d.ts
CHANGED
package/dist/addon-index.js
CHANGED
package/dist/addon.d.ts
CHANGED
|
@@ -11,35 +11,35 @@
|
|
|
11
11
|
* no node builtins -- the Add-on states, and Core checks. If something here ever does more than hand
|
|
12
12
|
* back a constant, it is in the wrong package.
|
|
13
13
|
*/
|
|
14
|
-
import type {
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
17
|
-
import type {
|
|
18
|
-
export declare const
|
|
19
|
-
export declare const
|
|
20
|
-
export type
|
|
21
|
-
export type
|
|
22
|
-
export type
|
|
14
|
+
import type { PhisAddonCapabilities, PhisAddonJobHandler, PhisSiteAccess } from "./capabilities.js";
|
|
15
|
+
import type { PhisCoreCapabilityId, PhisServiceKind } from "./core.js";
|
|
16
|
+
import type { PhisAddonQueryCatalog } from "./queries.js";
|
|
17
|
+
import type { PhisAddonSchemaDescriptor } from "./schema.js";
|
|
18
|
+
export declare const PHIS_ADDON_MANIFEST_VERSION: 1;
|
|
19
|
+
export declare const PHIS_ADDON_ABI_VERSION: 1;
|
|
20
|
+
export type PhisAddonAuthPolicy = "internal" | "site-user" | "site-admin" | "operator";
|
|
21
|
+
export type PhisAddonHttpMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
22
|
+
export type PhisAddonApiRouteDescriptor = {
|
|
23
23
|
id: string;
|
|
24
|
-
method:
|
|
24
|
+
method: PhisAddonHttpMethod;
|
|
25
25
|
path: string;
|
|
26
26
|
handler: string;
|
|
27
|
-
auth:
|
|
27
|
+
auth: PhisAddonAuthPolicy;
|
|
28
28
|
siteScoped: boolean;
|
|
29
29
|
bodyLimitBytes?: number;
|
|
30
30
|
rateLimitClass?: string;
|
|
31
31
|
timeoutMs?: number;
|
|
32
32
|
};
|
|
33
|
-
export type
|
|
33
|
+
export type PhisAddonHookDescriptor = {
|
|
34
34
|
id: string;
|
|
35
|
-
method:
|
|
35
|
+
method: PhisAddonHttpMethod;
|
|
36
36
|
path: string;
|
|
37
37
|
handler: string;
|
|
38
38
|
bodyLimitBytes: number;
|
|
39
39
|
rateLimitClass: string;
|
|
40
40
|
timeoutMs: number;
|
|
41
41
|
/**
|
|
42
|
-
* Whether this hook is told which Site it was called for, through `x-
|
|
42
|
+
* Whether this hook is told which Site it was called for, through `x-phis-site-key`.
|
|
43
43
|
*
|
|
44
44
|
* A hook is unauthenticated by construction -- that is what makes it reachable by a payment provider
|
|
45
45
|
* or by somebody else's Core. Without a Site it also has no capabilities at all, because every one of
|
|
@@ -50,7 +50,7 @@ export type PhiServerAddonHookDescriptor = {
|
|
|
50
50
|
*/
|
|
51
51
|
siteScoped?: boolean;
|
|
52
52
|
};
|
|
53
|
-
export type
|
|
53
|
+
export type PhisAddonCapabilityDescriptor = {
|
|
54
54
|
id: string;
|
|
55
55
|
interfaceDigest: string;
|
|
56
56
|
};
|
|
@@ -62,14 +62,14 @@ export type PhiServerAddonCapabilityDescriptor = {
|
|
|
62
62
|
* reaches the implementation without a dynamic import. The interface digest is checked against what
|
|
63
63
|
* this release offers, so an Add-on built for an older interface is refused rather than called.
|
|
64
64
|
*/
|
|
65
|
-
export type
|
|
66
|
-
serviceKind:
|
|
65
|
+
export type PhisAddonServiceProviderDescriptor = {
|
|
66
|
+
serviceKind: PhisServiceKind;
|
|
67
67
|
providerKey: string;
|
|
68
68
|
interfaceDigest: string;
|
|
69
69
|
factoryExport: string;
|
|
70
|
-
requiredCoreCapabilities:
|
|
70
|
+
requiredCoreCapabilities: PhisCoreCapabilityId[];
|
|
71
71
|
};
|
|
72
|
-
export type
|
|
72
|
+
export type PhisAddonJobDescriptor = {
|
|
73
73
|
id: string;
|
|
74
74
|
handler: string;
|
|
75
75
|
/**
|
|
@@ -88,7 +88,7 @@ export type PhiServerAddonJobDescriptor = {
|
|
|
88
88
|
* check a value against its type before it is stored -- the same bargain as the schema and query
|
|
89
89
|
* descriptors, one step smaller.
|
|
90
90
|
*/
|
|
91
|
-
export type
|
|
91
|
+
export type PhisAddonSettingDescriptor = {
|
|
92
92
|
name: string;
|
|
93
93
|
type: "text" | "integer" | "boolean";
|
|
94
94
|
/** Shown by the CLI when it lists what an Add-on can be configured with. */
|
|
@@ -107,8 +107,8 @@ export type PhiServerAddonSettingDescriptor = {
|
|
|
107
107
|
* `siteAccess` names one of the Site's own claims; `role` names another role of this same Add-on. A
|
|
108
108
|
* Site Admin may always grant any role of any Add-on, so a graph that reaches nobody still has a way in.
|
|
109
109
|
*/
|
|
110
|
-
export type
|
|
111
|
-
siteAccess:
|
|
110
|
+
export type PhisAddonRoleGrantPolicy = {
|
|
111
|
+
siteAccess: PhisSiteAccess;
|
|
112
112
|
} | {
|
|
113
113
|
role: string;
|
|
114
114
|
};
|
|
@@ -122,23 +122,51 @@ export type PhiServerAddonRoleGrantPolicy = {
|
|
|
122
122
|
* Core never interprets the name. It stores assignments, answers `has()`, and enforces `grantableBy`;
|
|
123
123
|
* what `reviewer` means is the Add-on's business and stays there.
|
|
124
124
|
*/
|
|
125
|
-
export type
|
|
125
|
+
export type PhisAddonGroupWriterDescriptor = {
|
|
126
|
+
/**
|
|
127
|
+
* Who may cause this Add-on to create a group or change who is in one.
|
|
128
|
+
*
|
|
129
|
+
* The same policy shape a role's `grantableBy` uses, evaluated the same way, and with the same floor:
|
|
130
|
+
* a Site Admin always may, so a badly declared condition never locks a Site out of its own groups.
|
|
131
|
+
*/
|
|
132
|
+
allowedBy: PhisAddonRoleGrantPolicy;
|
|
133
|
+
};
|
|
134
|
+
export type PhisAddonRoleDescriptor = {
|
|
135
|
+
name: string;
|
|
136
|
+
description?: string;
|
|
137
|
+
grantableBy: PhisAddonRoleGrantPolicy;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* A group this Add-on needs on every Site that runs it.
|
|
141
|
+
*
|
|
142
|
+
* Groups are Core's vocabulary about shared rows, so an Add-on cannot invent one at runtime and cannot
|
|
143
|
+
* put anybody in it -- the groups capability reads and grants nothing. What it can do is say which ones
|
|
144
|
+
* it needs, and Core creates them per Site when the Site asks for the Add-on. Who is in them stays with
|
|
145
|
+
* whoever administers the Site, which is the point: a package should not be able to hand out standing.
|
|
146
|
+
*
|
|
147
|
+
* The group carries this Add-on's id as its provider, and Core already filters group claims against the
|
|
148
|
+
* available providers -- so an Add-on's group stops granting anything the moment the Add-on is gone,
|
|
149
|
+
* without anybody having to remember to clean up.
|
|
150
|
+
*/
|
|
151
|
+
export type PhisAddonGroupDescriptor = {
|
|
152
|
+
/** Unique within this Add-on. Frozen once a Site has one: memberships point at it by key. */
|
|
153
|
+
key: string;
|
|
154
|
+
/** What an administrator sees in the Site's group list. */
|
|
126
155
|
name: string;
|
|
127
156
|
description?: string;
|
|
128
|
-
grantableBy: PhiServerAddonRoleGrantPolicy;
|
|
129
157
|
};
|
|
130
|
-
export type
|
|
131
|
-
manifestVersion: typeof
|
|
158
|
+
export type PhisAddonManifestV1 = {
|
|
159
|
+
manifestVersion: typeof PHIS_ADDON_MANIFEST_VERSION;
|
|
132
160
|
addonId: string;
|
|
133
161
|
packageName: string;
|
|
134
162
|
packageVersion: string;
|
|
135
163
|
serverAbi: string;
|
|
136
|
-
capabilities:
|
|
137
|
-
serviceProviders:
|
|
138
|
-
requiredCoreCapabilities:
|
|
139
|
-
apiRoutes:
|
|
140
|
-
hooks:
|
|
141
|
-
jobs:
|
|
164
|
+
capabilities: PhisAddonCapabilityDescriptor[];
|
|
165
|
+
serviceProviders: PhisAddonServiceProviderDescriptor[];
|
|
166
|
+
requiredCoreCapabilities: PhisCoreCapabilityId[];
|
|
167
|
+
apiRoutes: PhisAddonApiRouteDescriptor[];
|
|
168
|
+
hooks: PhisAddonHookDescriptor[];
|
|
169
|
+
jobs: PhisAddonJobDescriptor[];
|
|
142
170
|
/**
|
|
143
171
|
* What an operator may configure. Absent means nothing is.
|
|
144
172
|
*
|
|
@@ -148,7 +176,7 @@ export type PhiServerAddonManifestV1 = {
|
|
|
148
176
|
* fields may demand an explicit `null`, because no stored manifest predates them; later ones must
|
|
149
177
|
* define what absence means and accept it.
|
|
150
178
|
*/
|
|
151
|
-
settings?:
|
|
179
|
+
settings?: PhisAddonSettingDescriptor[];
|
|
152
180
|
/**
|
|
153
181
|
* The roles this Add-on defines. Absent means it defines none.
|
|
154
182
|
*
|
|
@@ -156,14 +184,50 @@ export type PhiServerAddonManifestV1 = {
|
|
|
156
184
|
* forgotten it. A name is frozen from the first assignment -- renaming one orphans every grant that
|
|
157
185
|
* points at it, silently, because the grant is stored as text and text does not follow a rename.
|
|
158
186
|
*/
|
|
159
|
-
roles?:
|
|
187
|
+
roles?: PhisAddonRoleDescriptor[];
|
|
188
|
+
/**
|
|
189
|
+
* The groups this Add-on needs. Absent means none.
|
|
190
|
+
*
|
|
191
|
+
* Optional like every field added after the manifest version was in the wild, and absent means the
|
|
192
|
+
* Add-on asks for no group rather than that it forgot to say so.
|
|
193
|
+
*/
|
|
194
|
+
groups?: PhisAddonGroupDescriptor[];
|
|
195
|
+
/**
|
|
196
|
+
* Whether this Add-on may create groups of its own while it runs, and who may cause it to.
|
|
197
|
+
*
|
|
198
|
+
* The declared list above is for the groups a Site always has -- one per Add-on, known when the
|
|
199
|
+
* manifest was written. This is for the ones a Site produces as it runs: one per vendor, per team, per
|
|
200
|
+
* tenant, whose number and names nobody knew in advance.
|
|
201
|
+
*
|
|
202
|
+
* It stays a declaration rather than a capability an Add-on simply has, for the reason `grantableBy`
|
|
203
|
+
* exists: the manifest is covered by a digest the running code cannot rewrite, so the author states
|
|
204
|
+
* who may cause a group, and Core decides whether the condition is met. Without it, "an Add-on may
|
|
205
|
+
* make groups" would mean "an Add-on may make itself an authority container and fill it".
|
|
206
|
+
*
|
|
207
|
+
* What it may do is bounded by ownership either way: a group it creates carries its own id as the
|
|
208
|
+
* provider, and every write here refuses a group carrying anybody else's.
|
|
209
|
+
*/
|
|
210
|
+
groupWriter?: PhisAddonGroupWriterDescriptor;
|
|
211
|
+
/**
|
|
212
|
+
* The Modules this package's other half contributes. Absent means it has no Module half.
|
|
213
|
+
*
|
|
214
|
+
* Each id is `<package name>/<module key>`, and the package name is this manifest's own -- one package
|
|
215
|
+
* carries one product, so there is nothing to resolve. What the field adds is the one thing Core
|
|
216
|
+
* cannot see from an artifact: whether a Module half exists at all. An Add-on that is only an Add-on,
|
|
217
|
+
* like a storage provider, says nothing here and is complete on its own.
|
|
218
|
+
*
|
|
219
|
+
* It is what lets enabling an Add-on for a Site check that the Site actually has the other half. A
|
|
220
|
+
* Site with the Add-on and not the Module has routes with no surface: nothing is broken, but an
|
|
221
|
+
* operator who thinks they switched a feature on finds nothing.
|
|
222
|
+
*/
|
|
223
|
+
modules?: string[];
|
|
160
224
|
/**
|
|
161
225
|
* The tables this Add-on owns, or null when it owns none.
|
|
162
226
|
*
|
|
163
227
|
* There is no list of migration steps beside it. The descriptor is the desired shape; what has been
|
|
164
228
|
* applied is recorded in the database, and Core derives the difference.
|
|
165
229
|
*/
|
|
166
|
-
schema:
|
|
230
|
+
schema: PhisAddonSchemaDescriptor | null;
|
|
167
231
|
/**
|
|
168
232
|
* The statements this Add-on may run against its own tables, or null when it runs none.
|
|
169
233
|
*
|
|
@@ -172,14 +236,14 @@ export type PhiServerAddonManifestV1 = {
|
|
|
172
236
|
*
|
|
173
237
|
* Optional for the reason `settings` is: it arrived after the manifest version was in the wild.
|
|
174
238
|
*/
|
|
175
|
-
queries?:
|
|
239
|
+
queries?: PhisAddonQueryCatalog | null;
|
|
176
240
|
/** The version of `schema`, or 0 when there is none. */
|
|
177
241
|
migrationVersion: number;
|
|
178
242
|
};
|
|
179
|
-
export type
|
|
243
|
+
export type PhisAddonRequestContext = {
|
|
180
244
|
addonId: string;
|
|
181
245
|
requestId: string;
|
|
182
|
-
method:
|
|
246
|
+
method: PhisAddonHttpMethod;
|
|
183
247
|
url: string;
|
|
184
248
|
headers: Headers;
|
|
185
249
|
body: Uint8Array;
|
|
@@ -188,6 +252,15 @@ export type PhiServerAddonRequestContext = {
|
|
|
188
252
|
site: {
|
|
189
253
|
id: number;
|
|
190
254
|
key: string;
|
|
255
|
+
/**
|
|
256
|
+
* Where this Site answers from, as the outside world reaches it.
|
|
257
|
+
*
|
|
258
|
+
* Core's own addresses are Site-relative, which is right for a page of that Site and useless in
|
|
259
|
+
* anything that leaves it: a listing another Core fetches, a link in an email, a picture in a
|
|
260
|
+
* feed. A handler cannot derive this -- a request carries the host it happened to arrive on, and
|
|
261
|
+
* an internal one carries none at all -- so it is the Site's own record that says it.
|
|
262
|
+
*/
|
|
263
|
+
publicBaseUrl: string;
|
|
191
264
|
} | null;
|
|
192
265
|
actor: {
|
|
193
266
|
kind: "operator";
|
|
@@ -205,10 +278,10 @@ export type PhiServerAddonRequestContext = {
|
|
|
205
278
|
* Only what the manifest declared arrives. A capability is not fetched, looked up, or constructed by
|
|
206
279
|
* the Add-on -- it is here or it is not, and it was already decided before the handler ran.
|
|
207
280
|
*/
|
|
208
|
-
capabilities:
|
|
281
|
+
capabilities: PhisAddonCapabilities;
|
|
209
282
|
signal: AbortSignal;
|
|
210
283
|
};
|
|
211
|
-
export type
|
|
284
|
+
export type PhisAddonHandler = (context: PhisAddonRequestContext) => Promise<Response> | Response;
|
|
212
285
|
/**
|
|
213
286
|
* A service factory, as the runtime exports it.
|
|
214
287
|
*
|
|
@@ -216,24 +289,24 @@ export type PhiServerAddonHandler = (context: PhiServerAddonRequestContext) => P
|
|
|
216
289
|
* what makes that agreement checkable. The factory receives schema-validated profile configuration and
|
|
217
290
|
* a restricted context, never a database handle or a general secret accessor.
|
|
218
291
|
*/
|
|
219
|
-
export type
|
|
220
|
-
export type
|
|
221
|
-
manifest:
|
|
222
|
-
apiHandlers?: Readonly<Record<string,
|
|
223
|
-
hookHandlers?: Readonly<Record<string,
|
|
224
|
-
serviceFactories?: Readonly<Record<string,
|
|
225
|
-
jobHandlers?: Readonly<Record<string,
|
|
292
|
+
export type PhisAddonServiceFactory = (config: unknown, context: unknown) => unknown;
|
|
293
|
+
export type PhisAddonRuntimeV1 = {
|
|
294
|
+
manifest: PhisAddonManifestV1;
|
|
295
|
+
apiHandlers?: Readonly<Record<string, PhisAddonHandler>>;
|
|
296
|
+
hookHandlers?: Readonly<Record<string, PhisAddonHandler>>;
|
|
297
|
+
serviceFactories?: Readonly<Record<string, PhisAddonServiceFactory>>;
|
|
298
|
+
jobHandlers?: Readonly<Record<string, PhisAddonJobHandler>>;
|
|
226
299
|
};
|
|
227
|
-
export type
|
|
228
|
-
|
|
300
|
+
export type PhisAddonRuntimeModuleV1 = {
|
|
301
|
+
phisAddon: PhisAddonRuntimeV1;
|
|
229
302
|
};
|
|
230
|
-
export type
|
|
303
|
+
export type PhisAddonBuildEntry = {
|
|
231
304
|
enabled: boolean;
|
|
232
305
|
manifestDigest: string;
|
|
233
|
-
manifest:
|
|
234
|
-
load: () => Promise<
|
|
306
|
+
manifest: PhisAddonManifestV1;
|
|
307
|
+
load: () => Promise<PhisAddonRuntimeModuleV1>;
|
|
235
308
|
};
|
|
236
|
-
export type
|
|
309
|
+
export type PhisAddonBuildManifest = Readonly<Record<string, PhisAddonBuildEntry>>;
|
|
237
310
|
/**
|
|
238
311
|
* One installed Add-on, as the operator's desired state records it.
|
|
239
312
|
*
|
|
@@ -242,7 +315,7 @@ export type PhiServerAddonBuildManifest = Readonly<Record<string, PhiServerAddon
|
|
|
242
315
|
* digests bind the three apart: `manifestDigest` covers the descriptors below, `artifactDigest` covers
|
|
243
316
|
* the file in the install root, and the loader refuses a file that no longer matches.
|
|
244
317
|
*/
|
|
245
|
-
export type
|
|
318
|
+
export type PhisAddonDesiredStateEntry = {
|
|
246
319
|
addonId: string;
|
|
247
320
|
packageName: string;
|
|
248
321
|
packageVersion: string;
|
|
@@ -250,10 +323,10 @@ export type PhiServerAddonDesiredStateEntry = {
|
|
|
250
323
|
/** SHA-256 of the installed artifact, and the cache key its load address carries. */
|
|
251
324
|
artifactDigest: string;
|
|
252
325
|
/** The validated descriptors, as read from the artifact at installation. */
|
|
253
|
-
manifest:
|
|
326
|
+
manifest: PhisAddonManifestV1;
|
|
254
327
|
enabled: boolean;
|
|
255
328
|
};
|
|
256
|
-
export type
|
|
329
|
+
export type PhisAddonDesiredState = {
|
|
257
330
|
formatVersion: 1;
|
|
258
|
-
addons:
|
|
331
|
+
addons: PhisAddonDesiredStateEntry[];
|
|
259
332
|
};
|
package/dist/addon.js
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
* no node builtins -- the Add-on states, and Core checks. If something here ever does more than hand
|
|
12
12
|
* back a constant, it is in the wrong package.
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
14
|
+
export const PHIS_ADDON_MANIFEST_VERSION = 1;
|
|
15
|
+
export const PHIS_ADDON_ABI_VERSION = 1;
|