@phis/contracts 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- @phis/addon-contract
1
+ @phis/contracts
2
2
  Copyright 2026 Phisys Ltd
3
3
 
4
4
  This product includes software developed by Phisys Ltd.
package/README.md CHANGED
@@ -1,202 +1,210 @@
1
1
  # @phis/contracts
2
2
 
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.
3
+ The agreements phis (`@phis/server`) keeps with what surrounds it: the site UI (`@phis/ui`), Sites, and
4
+ separately shipped Add-ons. Each lives under its own subpath, because they hold between different parties
5
+ and change at different moments.
5
6
 
6
7
  ```
7
- @phis/contracts/addon what phi-server and a separately shipped Add-on promise each other
8
- @phis/contracts/access the authorization vocabulary phi-server and @phis/ui both evaluate
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
8
+ @phis/contracts/addon what phis and a separately shipped Add-on promise each other
9
+ @phis/contracts/access the authorization vocabulary phis and @phis/ui both evaluate
10
+ @phis/contracts/signals the signal vocabulary and address grammar the UI declares in and phis validates
11
+ @phis/contracts/catalog the Module categories an Add-on declares, the UI groups by and a market filters on
12
+ @phis/contracts/cms the CMS node identity both sides derive, and must derive alike
13
+ @phis/contracts/media the Asset vocabularies both sides store, filter on and judge
14
+ @phis/contracts/http the request headers phis and the site UI must spell the same way
15
+ @phis/contracts/locale the locale this software is authored in
16
+ @phis/contracts/logging the structured log vocabulary a Site writes and phis reads back
17
+ @phis/contracts/server-capabilities the capability snapshot phis reports and the UI acts on
18
+ @phis/contracts/site-groups the membership levels of a Site group, as both ends count them
19
+ @phis/contracts/user-state the shapes a Module may keep a person's decisions in, and the key grammar
12
20
  ```
13
21
 
14
- There is deliberately no root export. A package you can import from the top invites everything that
15
- two of our packages happen to share; a subpath makes you say which agreement you mean.
22
+ ## Before the v1 release, interface versions stay at 1
16
23
 
17
- ## `/addon`
24
+ `PHIS_SERVICE_INTERFACE_VERSIONS` and `PHIS_SERVICE_INTERFACE_DIGESTS` in `@phis/contracts/addon` are not
25
+ raised while phis is pre-v1, however much an interface changes. The mechanism they drive is real and
26
+ deliberate -- Core refuses an Add-on whose recorded manifest names a digest this release does not offer --
27
+ and that is exactly why raising a version now is the wrong move: there is no released Add-on to protect, so
28
+ it buys no compatibility, and it takes the installation down. The recorded manifest in
29
+ `config/phis-addons.json` stops matching, the capability snapshot refuses the desired state, and every
30
+ request that resolves a viewer answers 500 until each installed Add-on has been rebundled and upgraded.
18
31
 
19
- The types and constants a phi-server Add-on declares or implements. Types and constants only — no
20
- runtime code, no `server-only`, no React, no Next.
32
+ An interface that changes before v1 is simply the interface. Both sides are rebuilt from one tree, and the
33
+ version is raised once, when there is something in the field that a wrong assumption would break.
21
34
 
22
- An Add-on is compiled to a single ESM artifact and installed into a running phi-server through the
23
- `phis` CLI. It is never built into the server. This subpath is the only thing an Add-on compiles
24
- against; it must not depend on phi-server itself.
35
+ There is deliberately no root export. A package you can import from the top invites everything two of
36
+ our packages happen to share; a subpath makes you say which agreement you mean.
25
37
 
26
38
  ```sh
27
- npm install --save-dev @phis/contracts
39
+ npm install @phis/contracts
28
40
  ```
29
41
 
30
- For an Add-on author it is a build-time dependency: everything `/addon` exports is erased at compile
31
- time, apart from a handful of constants.
42
+ ## `/addon`
32
43
 
33
- ## `/access`
44
+ The types and constants an Add-on declares or implements. Types and constants only: no runtime code, no
45
+ `server-only`, no React, no Next.
46
+
47
+ An Add-on is compiled to a single ESM artifact and installed into a running phis through the `phis` CLI;
48
+ it is never built into the server. This subpath is the only thing an Add-on compiles against, and it must
49
+ not depend on phis itself. For an Add-on author it is a build-time dependency: everything it exports is
50
+ erased at compile time apart from a handful of constants.
51
+
52
+ What it covers:
53
+
54
+ - **Manifest** -- `PhisAddonManifestV1`: identity, version, required Core capabilities, the routes an
55
+ Add-on claims, the services it provides.
56
+ - **Runtime** -- `PhisAddonRuntimeV1`, `PhisAddonHandler`, `PhisAddonRequestContext`. The artifact
57
+ exports it under the name `phisAddon` (`PhisAddonRuntimeModuleV1`).
58
+ - **Schema** -- `PhisAddonSchemaDescriptor` and its column, index and constraint descriptors. Tables are
59
+ declared, not migrated by hand; phis applies them.
60
+ - **Service kinds** -- the Core-owned kinds an Add-on may supply an implementation of: Media Storage
61
+ (`PhisMediaStorageAdapter`, upload plans, object I/O) and Directory. `PHIS_SERVICE_KINDS` names them
62
+ and `PHIS_SERVICE_INTERFACE_DIGESTS` holds the digest of each kind's current interface; Core refuses an
63
+ Add-on built against a different digest at install.
64
+ - **Core capabilities** -- `PHIS_CORE_CAPABILITIES`, the capability ids an Add-on may require. Declare
65
+ against these constants rather than spelling the ids out. The list is the vocabulary and may reserve a
66
+ name before Core delivers it (`resource-links`, `support`); an Add-on that requires a capability this
67
+ release does not deliver is refused rather than handed a missing object.
68
+
69
+ `PHIS_ADDON_ABI_VERSION` is the single number phis checks an Add-on against.
70
+
71
+ How an Add-on is laid out, compiled, installed, activated and upgraded is described in phis's
72
+ [`ADDON_HOWTO.md`](https://github.com/phis-dev/phis-server/blob/main/ADDON_HOWTO.md); the rules behind it
73
+ are in [`SERVER_ADDONS.md`](https://github.com/phis-dev/phis-server/blob/main/SERVER_ADDONS.md).
34
74
 
35
- Claim shapes, policy shapes, and the evaluator that decides them. Unlike `/addon`, this one **is**
36
- runtime code, and it is a real dependency of both packages that use it.
75
+ ## `/access`
37
76
 
38
- Two processes decide the same question. phi-server decides it in its guards and API routes; the site
39
- decides it while rendering — per navigation entry, per tree node, and in the browser, where
40
- phi-server is not reachable without a round trip. Neither can defer to the other, so both evaluate,
41
- and one compiled source is the only way both can agree.
77
+ Claim shapes, policy shapes, and the evaluator that decides them. Unlike `/addon`, this is runtime code
78
+ and a real dependency of both packages that use it.
42
79
 
43
- They did not agree before this subpath existed. A stored claim with `flags: -1` admitted everything
44
- on the server and nothing in the UI: one side normalised the value, the other did not. Neither copy
45
- looked wrong on its own, which is why it went unnoticed.
80
+ Two processes decide the same question. phis decides it in its guards and API routes; a Site decides it
81
+ while rendering -- per navigation entry, per tree node, and in the browser, where phis is not reachable
82
+ without a round trip. Neither can defer to the other, so both evaluate, and one compiled source is the
83
+ only way both reach the same answer: a stored claim normalised on one side and not on the other admits
84
+ on one and refuses on the other, and neither copy looks wrong on its own.
46
85
 
47
- Each side keeps its own viewer type and passes a projection onto `PhiAccessSubject`, so neither
48
- package has to adopt the other's shape.
86
+ Each side keeps its own viewer type and passes a projection onto `PhiAccessSubject`, so neither package
87
+ adopts the other's shape.
49
88
 
50
89
  ## `/cms`
51
90
 
52
91
  The encoding of a CMS node's identity: how a Preset node's id is derived from the Module, the preset and
53
92
  the node key, and how a draft node's id is derived from the revision it was made in.
54
93
 
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.
94
+ It is arithmetic both sides must compute alike. A Preset id is derived rather than allocated, so the site
95
+ UI can address a node before any row exists and phis can check that stored wiring points at nodes that
96
+ really exist. A changed constant on one side produces ids that are still well formed and simply name
97
+ nothing. The Page-key convention and the map helper built on top stay with `@phis/ui`.
59
98
 
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.
99
+ ## `/signals`
64
100
 
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.
101
+ The closed vocabularies a Widget's wiring is written in -- scopes, actions, value types -- the shape of a
102
+ value schema name, and the address families.
67
103
 
68
- ## `/signals`
104
+ A Widget declares what it emits and listens for, the Builder stores that, phis validates it on the way
105
+ in, and the Site delivers it at runtime, so all of them parse the same strings. The rule that says which
106
+ receivers a scope admits, and the controller address the Site scope privileges, live here for the same
107
+ reason.
69
108
 
70
- The closed vocabularies a widget's wiring is written in -- scopes, actions, value types -- and the shape
71
- of a value schema name.
109
+ Which schemas exist stays with the UI. phis checks that a JSON signal names *a* schema, never which one:
110
+ `isPhiSignalValueSchemaShape` checks the shape, not membership. The UI asks the stricter question because
111
+ it holds the Module registry; phis must not, or it would refuse every third party's schema.
72
112
 
73
- A widget declares what it emits and listens for, the Builder stores that, and phi-server validates it on
74
- the way in. Two lists, one meaning; and when they were two lists they drifted. `@phis/ui` had grown
75
- `date`, `time` and `length`; phi-server had not. A length control's change signal -- from a widget the
76
- same release shipped -- was refused on save with "valueType is invalid".
113
+ ## `/catalog`
77
114
 
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.
115
+ What a Module is for, as one closed list: `foundation`, `workspace`, `content`, `media`, `commerce`,
116
+ `identity`, `communication`, `events`, `analytics`, `integration`, `operations`, `other`
117
+ (`PHI_RUNTIME_MODULE_CATEGORIES`).
85
118
 
86
- Which schemas exist stays with the UI. phi-server checks that a JSON signal names *a* schema, never
87
- which one, and `isPhiSignalValueSchemaShape` is exactly that much: shape, not membership. The UI asks
88
- the stricter question, because it holds the module registry; phi-server must not, or it would refuse
89
- every third party's schema for never having heard of it.
119
+ An Add-on declares it per Module, because a package may ship a shop and a report and neither answer would
120
+ be true of the other. The site UI groups the Modules page by it, and a marketplace Add-on -- which
121
+ compiles against this package and nothing else -- filters on it.
90
122
 
91
- ## `/catalog`
123
+ It is not in `/addon` on purpose: `/addon` is the ABI a third party compiles against, and this list grows
124
+ with the product. phis does not ask the membership question either; a manifest carries categories as
125
+ plain strings, Core checks the shape, and the strict question is asked where a registry is held. The
126
+ labels an operator reads are label-set keys owned by the UI; this subpath holds identifiers only.
92
127
 
93
- What a Module is for, as one closed list: `foundation`, `workspace`, `content`, `commerce`, `people`,
94
- `operations`, `other`.
128
+ Beside the vocabulary sits the shape a Module package declares itself in, under `phis` in its
129
+ `package.json`: which Modules it carries, each one's category, and the language their titles and
130
+ descriptions are written in. Reading that from `package.json` means nobody has to execute a stranger's
131
+ package to list it. The author writes it; `phis module` checks that the entries are present and well
132
+ formed, and a package that declares nothing is refused at intake.
95
133
 
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.
134
+ ## `/media`
99
135
 
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.
136
+ The words and numbers a Site and phis must read the same way about an Asset: the Space kind's wire name,
137
+ the media kinds, the Folder flags, the presentation flags, the image variant keys, and the two closed
138
+ vocabularies that decide deliverability -- `lifecycle_status` and `delivery_policy`.
104
139
 
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`.
140
+ These values travel and are judged on the other side. The site UI sends a flag mask as a list filter and
141
+ flag values when metadata is saved, and phis filters with `presentation_flags & mask <> 0` and refuses a
142
+ value it does not know. An Asset payload carries `lifecycleStatus` and `deliveryPolicy` as stored, and
143
+ the UI decides from them whether `next/image` may be pointed at the original.
110
144
 
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.
145
+ The resolvers come with their vocabulary: `normalizePhiMediaKind` and `resolvePhiMediaKindFromContentType`
146
+ map onto the kind list, so an upload is filed and listed as the same kind on both sides. How a delivery
147
+ URL is built stays with each side, and phis's numeric Space kind never reaches a Site.
113
148
 
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.
149
+ ## `/http`
119
150
 
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.
151
+ The request header names phis reads and the site UI writes: `PHIS_TOKEN_HEADER` (`x-phis-token`),
152
+ `PHIS_SITE_KEY_HEADER`, `PHIS_AREA_HEADER`, `PHIS_REQUEST_PATH_HEADER` and `PHIS_REQUEST_SEARCH_HEADER`.
153
+ A disagreement here is not a type error but a 403 or 400 at runtime.
123
154
 
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.
155
+ ## `/locale`
128
156
 
129
- ## `/media`
157
+ `PHI_CANONICAL_SOURCE_LOCALE`: the locale every label, email template and global message is authored in,
158
+ and therefore the one a translation starts from. It is not a Site's default locale, which is per-Site
159
+ configuration.
130
160
 
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
-
154
- ## What `/addon` covers
155
-
156
- - **Manifest** — `PhisAddonManifestV1`: identity, version, required core capabilities,
157
- the routes an Add-on claims, the services it provides.
158
- - **Runtime** — `PhisAddonRuntimeV1`, `PhisAddonHandler`,
159
- `PhisAddonRequestContext`: the shape the artifact's default export must have.
160
- - **Schema** — `PhisAddonSchemaDescriptor` and its column, index, and constraint
161
- descriptors. Tables are declared, not migrated by hand; phi-server applies them.
162
- - **Service kinds** — the interfaces an Add-on may provide or consume, currently the media
163
- storage adapter (`PhisMediaStorageAdapter`, upload plans, object I/O).
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
167
- against these names rather than spelling them out: Core refuses an Add-on whose digest is not
168
- the one this release offers, and a literal cannot be checked before you ship it.
169
-
170
- This list is a promise, not a plan. A capability appears here when Core delivers it, never
171
- before.
172
-
173
- `PHIS_ADDON_ABI_VERSION` is the single number a server checks an Add-on against.
161
+ ## `/logging`
174
162
 
175
- ## Admission rule
163
+ The structured log vocabulary: `PhiLogService` (`phis`, `ui`, `site`, `cli`), `PhiLogLevel`,
164
+ `PhiLoggerContext`, `PhiLogEvent` and the `PhiLogger` interface. A Site and phis both write these records
165
+ to journald, and the log surface in phis parses them back, filtering by exactly these services and
166
+ fields. How a line is serialised is each writer's own concern.
176
167
 
177
- Per subpath, and both halves are narrow on purpose.
168
+ ## `/server-capabilities`
178
169
 
179
- Into `/addon`: only what **phi-server and a separately shipped Add-on must agree on**. Declarations,
180
- never implementations.
170
+ The snapshot `/api/v1/site/capabilities` reports: per provider (Core or an Add-on), the capabilities it
171
+ offers and its state (`PhiCapabilitySnapshot`, `PhiCapabilityProvider`, `PhiCapabilityState`). phis
172
+ decides the verdict; the site UI acts on it, deactivating a Module whose provider is not `available` and
173
+ naming the reason in its diagnostics.
181
174
 
182
- Into `/access`: only what **phi-server and `@phis/ui` must both evaluate**, because each decides it
183
- in its own process. Not what both happen to use — a date formatter used on both sides is shared
184
- convenience, not a contract, and belongs to neither.
175
+ ## `/site-groups`
185
176
 
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.
177
+ The membership ladder of a Site group: `PhiGroupMembershipFlags` (`Member`, `Author`, `Editor`,
178
+ `Manager`, cumulative bits), `PHI_GROUP_MEMBERSHIP_LEVELS` and `readPhiGroupMembershipLevel`. The site UI
179
+ sends `membershipFlags` as an integer and offers the levels to choose from, so both ends must count the
180
+ same bits. The Add-on boundary names levels instead (`PHIS_GROUP_LEVELS` in `/addon`).
181
+
182
+ ## `/user-state`
183
+
184
+ What a Module may keep per Site about a person who decided something: the closed shape set
185
+ (`PHIS_USER_STATE_SHAPES` -- `flag`, `marker`, `value`, `set`), the `<package>/<key>` grammar, the
186
+ declaration a Module makes (`PhisDeclarableUserStateKey`), and `readPhisUserStateWrite`, which both ends
187
+ call so a refused write is refused the same way in both. phis stores it and is the only party that
188
+ validates; the site UI declares the keys and reads them while rendering. Core's own account facts stay
189
+ bit flags on the membership and account rows and are not this.
190
+
191
+ ## Admission rule
189
192
 
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.
193
+ Per subpath, and narrow on purpose. Name the two parties and the sentence they promise each other; if that
194
+ sentence cannot be written down, it does not go in here.
194
195
 
195
- The test for either is the same: name the two parties and the sentence they are promising each
196
- other. If that sentence cannot be written down, it does not go in here.
196
+ - `/addon`: only what phis and a separately shipped Add-on must agree on. Declarations, never
197
+ implementations.
198
+ - `/access`: only what phis and `@phis/ui` must both evaluate, each in its own process. A helper both
199
+ happen to use is convenience, not a contract.
200
+ - `/catalog`: only vocabularies an Add-on declares a Module in and another party reads back -- closed
201
+ lists of identifiers, never the words an operator sees.
202
+ - `/media`, `/http`, `/logging`, `/site-groups`, `/server-capabilities`: only what crosses the wire as a
203
+ value and is judged on the other side. A copy is a contract when disagreeing about it changes what a
204
+ request returns.
205
+ - `/user-state`: only the shapes and the key grammar both ends check a write against. What a Module keeps
206
+ under its keys is the Module's business and never named here.
197
207
 
198
- ## Documentation
208
+ ## License
199
209
 
200
- The full walkthrough — how an Add-on is laid out, compiled, installed, activated, and
201
- upgraded — lives in phi-server's `ADDON_HOWTO.md`, with the design rationale in
202
- `SERVER_ADDONS.md`.
210
+ Apache-2.0. See `LICENSE` and `NOTICE`.
package/dist/access.d.ts CHANGED
@@ -29,6 +29,19 @@ export declare const PhiBaseRole: {
29
29
  readonly Developer: number;
30
30
  readonly Supporter: number;
31
31
  readonly Accountant: number;
32
+ /**
33
+ * Works here, and needs nothing else.
34
+ *
35
+ * It grants no capability at all, which is what it is for: being Staff decides who may be written to
36
+ * and who may open a thread with a colleague, and until this bit existed the only way to say it was to
37
+ * hand out a role that also grants something -- a developer bit to somebody who never touches a log.
38
+ *
39
+ * Staff is whoever holds *any* Core role, so every role above implies it and this is the one to give
40
+ * somebody who holds none of them. Roles an Add-on defines never make anybody Staff: they are
41
+ * provider-local and say what a person may do inside that Add-on, which is how somebody outside the
42
+ * company can hold one and remain outside it.
43
+ */
44
+ readonly Staff: number;
32
45
  };
33
46
  /**
34
47
  * What a stored email address says about itself, as bit flags.
package/dist/access.js CHANGED
@@ -29,6 +29,19 @@ export const PhiBaseRole = {
29
29
  Developer: 1 << 4,
30
30
  Supporter: 1 << 5,
31
31
  Accountant: 1 << 6,
32
+ /**
33
+ * Works here, and needs nothing else.
34
+ *
35
+ * It grants no capability at all, which is what it is for: being Staff decides who may be written to
36
+ * and who may open a thread with a colleague, and until this bit existed the only way to say it was to
37
+ * hand out a role that also grants something -- a developer bit to somebody who never touches a log.
38
+ *
39
+ * Staff is whoever holds *any* Core role, so every role above implies it and this is the one to give
40
+ * somebody who holds none of them. Roles an Add-on defines never make anybody Staff: they are
41
+ * provider-local and say what a person may do inside that Add-on, which is how somebody outside the
42
+ * company can hold one and remain outside it.
43
+ */
44
+ Staff: 1 << 7,
32
45
  };
33
46
  /**
34
47
  * What a stored email address says about itself, as bit flags.
@@ -4,4 +4,8 @@ export * from "./capabilities.js";
4
4
  export * from "./core.js";
5
5
  export * from "./media-storage.js";
6
6
  export * from "./queries.js";
7
+ export * from "./resource-links.js";
7
8
  export * from "./schema.js";
9
+ export * from "./support.js";
10
+ export * from "./threads.js";
11
+ export * from "./translation.js";
@@ -4,4 +4,8 @@ export * from "./capabilities.js";
4
4
  export * from "./core.js";
5
5
  export * from "./media-storage.js";
6
6
  export * from "./queries.js";
7
+ export * from "./resource-links.js";
7
8
  export * from "./schema.js";
9
+ export * from "./support.js";
10
+ export * from "./threads.js";
11
+ export * from "./translation.js";
package/dist/addon.d.ts CHANGED
@@ -11,8 +11,8 @@
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 { PhisAddonCapabilities, PhisAddonJobHandler, PhisSiteAccess } from "./capabilities.js";
15
- import type { PhisCoreCapabilityId, PhisServiceKind } from "./core.js";
14
+ import type { PhisAddonCapabilities, PhisAddonEventHandler, PhisAddonJobHandler, PhisSiteAccess } from "./capabilities.js";
15
+ import type { PhisAddonEventId, PhisCoreCapabilityId, PhisServiceKind } from "./core.js";
16
16
  import type { PhisAddonQueryCatalog } from "./queries.js";
17
17
  import type { PhisAddonSchemaDescriptor } from "./schema.js";
18
18
  export declare const PHIS_ADDON_MANIFEST_VERSION: 1;
@@ -30,6 +30,28 @@ export type PhisAddonApiRouteDescriptor = {
30
30
  rateLimitClass?: string;
31
31
  timeoutMs?: number;
32
32
  };
33
+ /**
34
+ * How Core establishes that a hook request is from who it claims to be.
35
+ *
36
+ * A hook is unauthenticated by construction, and until Core could check a signature itself, "verified"
37
+ * was a thing each handler asserted about itself -- so the rule that a request with no actor may not
38
+ * write had to hold for every hook, including the ones that had in fact proved their caller. Stating the
39
+ * check here moves it in front of the handler: Core verifies over the raw body, and a handler that runs
40
+ * has already been vouched for.
41
+ *
42
+ * `secret` names one of this Add-on's own declared secrets, so the value an operator set is never in the
43
+ * manifest. `deliveryIdHeader` names the header the provider identifies a delivery with; where it is
44
+ * given, Core refuses a delivery it has already accepted, which is what makes a webhook retried after a
45
+ * timeout harmless.
46
+ */
47
+ export type PhisAddonHookVerificationDescriptor = {
48
+ scheme: "hmac-sha256";
49
+ header: string;
50
+ /** What the header value is prefixed with, where it is -- GitHub sends `sha256=`. */
51
+ prefix?: string;
52
+ secret: string;
53
+ deliveryIdHeader?: string;
54
+ };
33
55
  export type PhisAddonHookDescriptor = {
34
56
  id: string;
35
57
  method: PhisAddonHttpMethod;
@@ -38,6 +60,14 @@ export type PhisAddonHookDescriptor = {
38
60
  bodyLimitBytes: number;
39
61
  rateLimitClass: string;
40
62
  timeoutMs: number;
63
+ /**
64
+ * How this hook's caller is checked, or absent where it cannot be.
65
+ *
66
+ * Absent is not a lesser variant of present: an unverified hook writes nothing at all -- not through
67
+ * `data:v1`, not by opening a thread -- because nothing distinguishes its caller from anybody who
68
+ * found the address. A hook that needs to write declares how it can be believed.
69
+ */
70
+ verification?: PhisAddonHookVerificationDescriptor;
41
71
  /**
42
72
  * Whether this hook is told which Site it was called for, through `x-phis-site-key`.
43
73
  *
@@ -155,6 +185,18 @@ export type PhisAddonGroupDescriptor = {
155
185
  name: string;
156
186
  description?: string;
157
187
  };
188
+ /**
189
+ * One event this Add-on wants to be told about.
190
+ *
191
+ * Declared like a route: Core reaches the handler by the name the runtime exports it under, so nothing
192
+ * registers itself and what an Add-on listens to is readable before it runs. Several handlers may name
193
+ * the same event; each is delivered separately and retried separately.
194
+ */
195
+ export type PhisAddonEventHandlerDescriptor = {
196
+ id: string;
197
+ event: PhisAddonEventId;
198
+ handler: string;
199
+ };
158
200
  export type PhisAddonManifestV1 = {
159
201
  manifestVersion: typeof PHIS_ADDON_MANIFEST_VERSION;
160
202
  addonId: string;
@@ -167,6 +209,14 @@ export type PhisAddonManifestV1 = {
167
209
  apiRoutes: PhisAddonApiRouteDescriptor[];
168
210
  hooks: PhisAddonHookDescriptor[];
169
211
  jobs: PhisAddonJobDescriptor[];
212
+ /**
213
+ * The events this Add-on wants delivered. Absent means none.
214
+ *
215
+ * Optional like every field added after this manifest version was in the wild. Declaring one is not
216
+ * receiving it: an event reaches this Add-on only for a thread it holds a synced link to, so an
217
+ * Add-on that declares them and links nothing is told nothing.
218
+ */
219
+ eventHandlers?: PhisAddonEventHandlerDescriptor[];
170
220
  /**
171
221
  * What an operator may configure. Absent means nothing is.
172
222
  *
@@ -296,6 +346,7 @@ export type PhisAddonRuntimeV1 = {
296
346
  hookHandlers?: Readonly<Record<string, PhisAddonHandler>>;
297
347
  serviceFactories?: Readonly<Record<string, PhisAddonServiceFactory>>;
298
348
  jobHandlers?: Readonly<Record<string, PhisAddonJobHandler>>;
349
+ eventHandlers?: Readonly<Record<string, PhisAddonEventHandler>>;
299
350
  };
300
351
  export type PhisAddonRuntimeModuleV1 = {
301
352
  phisAddon: PhisAddonRuntimeV1;