@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.
Files changed (51) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +175 -67
  3. package/dist/access.d.ts +63 -0
  4. package/dist/access.js +78 -0
  5. package/dist/addon-index.d.ts +5 -0
  6. package/dist/addon-index.js +5 -0
  7. package/dist/addon.d.ts +181 -57
  8. package/dist/addon.js +2 -2
  9. package/dist/assets.d.ts +165 -0
  10. package/dist/assets.js +1 -0
  11. package/dist/capabilities.d.ts +175 -33
  12. package/dist/capabilities.js +2 -2
  13. package/dist/catalog.d.ts +104 -0
  14. package/dist/catalog.js +116 -0
  15. package/dist/cms.d.ts +120 -0
  16. package/dist/cms.js +270 -0
  17. package/dist/core.d.ts +52 -6
  18. package/dist/core.js +55 -8
  19. package/dist/http.d.ts +33 -0
  20. package/dist/http.js +33 -0
  21. package/dist/locale.d.ts +14 -0
  22. package/dist/locale.js +14 -0
  23. package/dist/logging.d.ts +49 -0
  24. package/dist/logging.js +17 -0
  25. package/dist/media-storage.d.ts +371 -0
  26. package/dist/media-storage.js +84 -0
  27. package/dist/media.d.ts +272 -0
  28. package/dist/media.js +421 -0
  29. package/dist/queries.d.ts +280 -48
  30. package/dist/queries.js +10 -1
  31. package/dist/resource-links.d.ts +94 -0
  32. package/dist/resource-links.js +27 -0
  33. package/dist/schema.d.ts +160 -26
  34. package/dist/schema.js +11 -2
  35. package/dist/server-capabilities.d.ts +37 -0
  36. package/dist/server-capabilities.js +17 -0
  37. package/dist/signals.d.ts +58 -0
  38. package/dist/signals.js +118 -0
  39. package/dist/site-groups.d.ts +53 -0
  40. package/dist/site-groups.js +54 -0
  41. package/dist/support.d.ts +390 -0
  42. package/dist/support.js +148 -0
  43. package/dist/threads.d.ts +363 -0
  44. package/dist/threads.js +146 -0
  45. package/dist/translation.d.ts +125 -0
  46. package/dist/translation.js +15 -0
  47. package/dist/user-state.d.ts +132 -0
  48. package/dist/user-state.js +92 -0
  49. package/package.json +64 -4
  50. package/dist/index.d.ts +0 -6
  51. package/dist/index.js +0 -6
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,102 +1,210 @@
1
1
  # @phis/contracts
2
2
 
3
- The agreements phi-server keeps with what surrounds it. Two of them, under two subpaths, because
4
- they hold 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 the UI declares against and phi-server validates
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
10
20
  ```
11
21
 
12
- There is deliberately no root export. A package you can import from the top invites everything that
13
- 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
14
23
 
15
- ## `/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.
16
31
 
17
- The types and constants a phi-server Add-on declares or implements. Types and constants only — no
18
- 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.
19
34
 
20
- An Add-on is compiled to a single ESM artifact and installed into a running phi-server through the
21
- `phis` CLI. It is never built into the server. This subpath is the only thing an Add-on compiles
22
- 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.
23
37
 
24
38
  ```sh
25
- npm install --save-dev @phis/contracts
39
+ npm install @phis/contracts
26
40
  ```
27
41
 
28
- For an Add-on author it is a build-time dependency: everything `/addon` exports is erased at compile
29
- time, apart from a handful of constants.
42
+ ## `/addon`
43
+
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).
30
74
 
31
75
  ## `/access`
32
76
 
33
- Claim shapes, policy shapes, and the evaluator that decides them. Unlike `/addon`, this one **is**
34
- runtime code, and it is a real dependency of both packages that use it.
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.
35
79
 
36
- Two processes decide the same question. phi-server decides it in its guards and API routes; the site
37
- decides it while rendering — per navigation entry, per tree node, and in the browser, where
38
- phi-server is not reachable without a round trip. Neither can defer to the other, so both evaluate,
39
- and one compiled source is the only way both can agree.
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.
40
85
 
41
- They did not agree before this subpath existed. A stored claim with `flags: -1` admitted everything
42
- on the server and nothing in the UI: one side normalised the value, the other did not. Neither copy
43
- looked wrong on its own, which is why it went unnoticed.
86
+ Each side keeps its own viewer type and passes a projection onto `PhiAccessSubject`, so neither package
87
+ adopts the other's shape.
44
88
 
45
- Each side keeps its own viewer type and passes a projection onto `PhiAccessSubject`, so neither
46
- package has to adopt the other's shape.
89
+ ## `/cms`
90
+
91
+ The encoding of a CMS node's identity: how a Preset node's id is derived from the Module, the preset and
92
+ the node key, and how a draft node's id is derived from the revision it was made in.
93
+
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`.
47
98
 
48
99
  ## `/signals`
49
100
 
50
- The closed vocabularies a widget's wiring is written in -- scopes, actions, value types -- and the shape
51
- of a value schema name.
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.
52
103
 
53
- A widget declares what it emits and listens for, the Builder stores that, and phi-server validates it on
54
- the way in. Two lists, one meaning; and when they were two lists they drifted. `@phis/ui` had grown
55
- `date`, `time` and `length`; phi-server had not. A length control's change signal -- from a widget the
56
- same release shipped -- was refused on save with "valueType is invalid".
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.
57
108
 
58
- Which schemas exist stays with the UI. phi-server checks that a JSON signal names *a* schema, never
59
- which one, and `isPhiSignalValueSchemaShape` is exactly that much: shape, not membership. The UI asks
60
- the stricter question, because it holds the module registry; phi-server must not, or it would refuse
61
- every third party's schema for never having heard of it.
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.
62
112
 
63
- ## What `/addon` covers
113
+ ## `/catalog`
64
114
 
65
- - **Manifest** — `PhiServerAddonManifestV1`: identity, version, required core capabilities,
66
- the routes an Add-on claims, the services it provides.
67
- - **Runtime** — `PhiServerAddonRuntimeV1`, `PhiServerAddonHandler`,
68
- `PhiServerAddonRequestContext`: the shape the artifact's default export must have.
69
- - **Schema** — `PhiServerAddonSchemaDescriptor` and its column, index, and constraint
70
- descriptors. Tables are declared, not migrated by hand; phi-server applies them.
71
- - **Service kinds** — the interfaces an Add-on may provide or consume, currently the media
72
- storage adapter (`PhisMediaStorageAdapter`, upload plans, object I/O).
73
- - **What Core offers** — `PHI_SERVER_CORE_CAPABILITIES`, the capability ids an Add-on may
74
- require; `PHI_SERVER_SERVICE_KINDS`, the service kinds Core owns; and
75
- `PHI_SERVER_SERVICE_INTERFACE_DIGESTS`, the digest of each kind's current interface. Declare
76
- against these names rather than spelling them out: Core refuses an Add-on whose digest is not
77
- the one this release offers, and a literal cannot be checked before you ship it.
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`).
78
118
 
79
- This list is a promise, not a plan. A capability appears here when Core delivers it, never
80
- before.
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.
81
122
 
82
- `PHI_SERVER_ADDON_ABI_VERSION` is the single number a server checks an Add-on against.
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.
83
127
 
84
- ## Admission rule
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.
133
+
134
+ ## `/media`
135
+
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`.
139
+
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.
144
+
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.
148
+
149
+ ## `/http`
85
150
 
86
- Per subpath, and both halves are narrow on purpose.
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.
87
154
 
88
- Into `/addon`: only what **phi-server and a separately shipped Add-on must agree on**. Declarations,
89
- never implementations.
155
+ ## `/locale`
156
+
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.
160
+
161
+ ## `/logging`
162
+
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.
167
+
168
+ ## `/server-capabilities`
169
+
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.
174
+
175
+ ## `/site-groups`
176
+
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
90
192
 
91
- Into `/access`: only what **phi-server and `@phis/ui` must both evaluate**, because each decides it
92
- in its own process. Not what both happen to use — a date formatter used on both sides is shared
93
- convenience, not a contract, and belongs to neither.
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.
94
195
 
95
- The test for either is the same: name the two parties and the sentence they are promising each
96
- 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.
97
207
 
98
- ## Documentation
208
+ ## License
99
209
 
100
- The full walkthrough — how an Add-on is laid out, compiled, installed, activated, and
101
- upgraded — lives in phi-server's `ADDON_HOWTO.md`, with the design rationale in
102
- `SERVER_ADDONS.md`.
210
+ Apache-2.0. See `LICENSE` and `NOTICE`.
package/dist/access.d.ts CHANGED
@@ -29,7 +29,70 @@ 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
  };
46
+ /**
47
+ * What a stored email address says about itself, as bit flags.
48
+ *
49
+ * `user_emails` holds several addresses per account and marks one of them primary. That mark is a flag
50
+ * rather than its own column because the schema keeps every other such answer in `flags`, and a lone
51
+ * boolean is the one place a reader has to remember an exception.
52
+ *
53
+ * The position is fixed from the first row, and here more strictly than elsewhere: the unique index that
54
+ * enforces "one primary address per account" is partial on this very bit. Move the bit and the index
55
+ * silently guards a different question.
56
+ */
57
+ export declare const PhiUserEmailFlag: {
58
+ readonly Primary: number;
59
+ };
60
+ /**
61
+ * Where a stored email address came from.
62
+ *
63
+ * The question this answers is asked after the fact and cannot be reconstructed: did this person type
64
+ * their address themselves, did an administrator set it, or did an identity provider hand it over? An
65
+ * account that can be created from a command line makes that difference worth keeping.
66
+ *
67
+ * Numbers rather than free text, because the set is Core's and a string column accepts every typo as a
68
+ * new source. Below 20 are Core's own origins; from 20 upward are named providers, and they are named
69
+ * rather than left to the Add-on that connects them: the Add-on may be replaced, Google will not be.
70
+ * A provider without a number is `Provider`, and which one it was stands in `user_auth_identities`.
71
+ *
72
+ * There is deliberately no `unknown`. Every writer knows why it writes, and a zero default would let
73
+ * the one that does not pass unnoticed.
74
+ */
75
+ export declare const PhiUserEmailSource: {
76
+ readonly Operator: 1;
77
+ readonly Admin: 2;
78
+ readonly Registration: 3;
79
+ readonly Self: 4;
80
+ readonly Directory: 5;
81
+ readonly Provider: 9;
82
+ readonly Google: 20;
83
+ readonly Apple: 21;
84
+ readonly Microsoft: 22;
85
+ readonly GitHub: 23;
86
+ };
87
+ export type PhiUserEmailSourceValue = (typeof PhiUserEmailSource)[keyof typeof PhiUserEmailSource];
88
+ export declare function isPhiUserEmailSource(value: unknown): value is PhiUserEmailSourceValue;
89
+ /**
90
+ * Which source a login provider's key stands for.
91
+ *
92
+ * The mapping lives here because the numbers do: it is this file that says 20 means Google. A key with
93
+ * no number of its own is `Provider` rather than a guess, and `user_auth_identities` keeps the detail.
94
+ */
95
+ export declare function phiUserEmailSourceForProvider(providerKey: string): PhiUserEmailSourceValue;
33
96
  export type PhiRoleProviderId = `@${string}/${string}`;
34
97
  export type PhiGroupProviderId = `@${string}/${string}`;
35
98
  export type PhiViewerRoleClaim = {
package/dist/access.js CHANGED
@@ -29,7 +29,85 @@ 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
  };
46
+ /**
47
+ * What a stored email address says about itself, as bit flags.
48
+ *
49
+ * `user_emails` holds several addresses per account and marks one of them primary. That mark is a flag
50
+ * rather than its own column because the schema keeps every other such answer in `flags`, and a lone
51
+ * boolean is the one place a reader has to remember an exception.
52
+ *
53
+ * The position is fixed from the first row, and here more strictly than elsewhere: the unique index that
54
+ * enforces "one primary address per account" is partial on this very bit. Move the bit and the index
55
+ * silently guards a different question.
56
+ */
57
+ export const PhiUserEmailFlag = {
58
+ Primary: 1 << 0,
59
+ };
60
+ /**
61
+ * Where a stored email address came from.
62
+ *
63
+ * The question this answers is asked after the fact and cannot be reconstructed: did this person type
64
+ * their address themselves, did an administrator set it, or did an identity provider hand it over? An
65
+ * account that can be created from a command line makes that difference worth keeping.
66
+ *
67
+ * Numbers rather than free text, because the set is Core's and a string column accepts every typo as a
68
+ * new source. Below 20 are Core's own origins; from 20 upward are named providers, and they are named
69
+ * rather than left to the Add-on that connects them: the Add-on may be replaced, Google will not be.
70
+ * A provider without a number is `Provider`, and which one it was stands in `user_auth_identities`.
71
+ *
72
+ * There is deliberately no `unknown`. Every writer knows why it writes, and a zero default would let
73
+ * the one that does not pass unnoticed.
74
+ */
75
+ export const PhiUserEmailSource = {
76
+ Operator: 1,
77
+ Admin: 2,
78
+ Registration: 3,
79
+ Self: 4,
80
+ Directory: 5,
81
+ Provider: 9,
82
+ Google: 20,
83
+ Apple: 21,
84
+ Microsoft: 22,
85
+ GitHub: 23,
86
+ };
87
+ export function isPhiUserEmailSource(value) {
88
+ return typeof value === "number"
89
+ && Object.values(PhiUserEmailSource).includes(value);
90
+ }
91
+ /**
92
+ * Which source a login provider's key stands for.
93
+ *
94
+ * The mapping lives here because the numbers do: it is this file that says 20 means Google. A key with
95
+ * no number of its own is `Provider` rather than a guess, and `user_auth_identities` keeps the detail.
96
+ */
97
+ export function phiUserEmailSourceForProvider(providerKey) {
98
+ switch (providerKey.trim().toLowerCase()) {
99
+ case "google":
100
+ return PhiUserEmailSource.Google;
101
+ case "apple":
102
+ return PhiUserEmailSource.Apple;
103
+ case "microsoft":
104
+ return PhiUserEmailSource.Microsoft;
105
+ case "github":
106
+ return PhiUserEmailSource.GitHub;
107
+ default:
108
+ return PhiUserEmailSource.Provider;
109
+ }
110
+ }
33
111
  /**
34
112
  * Flags as they may be trusted.
35
113
  *
@@ -1,6 +1,11 @@
1
1
  export * from "./addon.js";
2
+ export * from "./assets.js";
2
3
  export * from "./capabilities.js";
3
4
  export * from "./core.js";
4
5
  export * from "./media-storage.js";
5
6
  export * from "./queries.js";
7
+ export * from "./resource-links.js";
6
8
  export * from "./schema.js";
9
+ export * from "./support.js";
10
+ export * from "./threads.js";
11
+ export * from "./translation.js";
@@ -1,6 +1,11 @@
1
1
  export * from "./addon.js";
2
+ export * from "./assets.js";
2
3
  export * from "./capabilities.js";
3
4
  export * from "./core.js";
4
5
  export * from "./media-storage.js";
5
6
  export * from "./queries.js";
7
+ export * from "./resource-links.js";
6
8
  export * from "./schema.js";
9
+ export * from "./support.js";
10
+ export * from "./threads.js";
11
+ export * from "./translation.js";