@phis/contracts 0.2.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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * What a phi-server Add-on declares or implements.
3
+ *
4
+ * This package exists so an Add-on has something to build against that is not phi-server itself.
5
+ * phi-server is a private application without an export surface; an Add-on published to a registry
6
+ * could not depend on it, and a type import into the application's internals would bind an Add-on to a
7
+ * file path that can move without anybody noticing.
8
+ *
9
+ * The admission rule is strict, and it is the only thing keeping this from becoming a junk drawer:
10
+ * nothing enters unless at least one Add-on must declare or implement it. No validation, no helpers,
11
+ * no node builtins -- the Add-on states, and Core checks. If something here ever does more than hand
12
+ * back a constant, it is in the wrong package.
13
+ */
14
+ import type { PhiServerAddonCapabilities, PhiServerAddonJobHandler, PhiServerSiteAccess } from "./capabilities.js";
15
+ import type { PhiServerCoreCapabilityId, PhiServerServiceKind } from "./core.js";
16
+ import type { PhiServerAddonQueryCatalog } from "./queries.js";
17
+ import type { PhiServerAddonSchemaDescriptor } from "./schema.js";
18
+ export declare const PHI_SERVER_ADDON_MANIFEST_VERSION: 1;
19
+ export declare const PHI_SERVER_ADDON_ABI_VERSION: 1;
20
+ export type PhiServerAddonAuthPolicy = "internal" | "site-user" | "site-admin" | "operator";
21
+ export type PhiServerAddonHttpMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
22
+ export type PhiServerAddonApiRouteDescriptor = {
23
+ id: string;
24
+ method: PhiServerAddonHttpMethod;
25
+ path: string;
26
+ handler: string;
27
+ auth: PhiServerAddonAuthPolicy;
28
+ siteScoped: boolean;
29
+ bodyLimitBytes?: number;
30
+ rateLimitClass?: string;
31
+ timeoutMs?: number;
32
+ };
33
+ export type PhiServerAddonHookDescriptor = {
34
+ id: string;
35
+ method: PhiServerAddonHttpMethod;
36
+ path: string;
37
+ handler: string;
38
+ bodyLimitBytes: number;
39
+ rateLimitClass: string;
40
+ timeoutMs: number;
41
+ /**
42
+ * Whether this hook is told which Site it was called for, through `x-phi-site-key`.
43
+ *
44
+ * A hook is unauthenticated by construction -- that is what makes it reachable by a payment provider
45
+ * or by somebody else's Core. Without a Site it also has no capabilities at all, because every one of
46
+ * them is Site-scoped, so a hook could until now verify a signature and touch no data. Stating the
47
+ * Site is what makes a public read possible; it grants nothing else, and the caller is still nobody.
48
+ *
49
+ * Optional, and absent means false: a hook written before this field existed had not forgotten it.
50
+ */
51
+ siteScoped?: boolean;
52
+ };
53
+ export type PhiServerAddonCapabilityDescriptor = {
54
+ id: string;
55
+ interfaceDigest: string;
56
+ };
57
+ /**
58
+ * One implementation of a Core-owned service kind.
59
+ *
60
+ * `providerKey` is what a Storage Profile row stores, so it must stay stable across releases of the
61
+ * Add-on; `factoryExport` names the entry in the runtime's `serviceFactories` map, which is how Core
62
+ * reaches the implementation without a dynamic import. The interface digest is checked against what
63
+ * this release offers, so an Add-on built for an older interface is refused rather than called.
64
+ */
65
+ export type PhiServerAddonServiceProviderDescriptor = {
66
+ serviceKind: PhiServerServiceKind;
67
+ providerKey: string;
68
+ interfaceDigest: string;
69
+ factoryExport: string;
70
+ requiredCoreCapabilities: PhiServerCoreCapabilityId[];
71
+ };
72
+ export type PhiServerAddonJobDescriptor = {
73
+ id: string;
74
+ handler: string;
75
+ /**
76
+ * How often this job expects to run, for the operator to read -- never a schedule Core keeps.
77
+ *
78
+ * phi-server serves from several instances, so an in-process timer would fire once per instance for
79
+ * one intended run. The operator schedules `phis addon job run`, which is one runner by construction,
80
+ * and this field tells them what the Add-on had in mind.
81
+ */
82
+ suggestedInterval?: "hourly" | "daily" | "weekly";
83
+ };
84
+ /**
85
+ * One setting an operator may configure for this Add-on.
86
+ *
87
+ * Declared so `phis addon config` can list what is configurable, refuse a name nobody declared, and
88
+ * check a value against its type before it is stored -- the same bargain as the schema and query
89
+ * descriptors, one step smaller.
90
+ */
91
+ export type PhiServerAddonSettingDescriptor = {
92
+ name: string;
93
+ type: "text" | "integer" | "boolean";
94
+ /** Shown by the CLI when it lists what an Add-on can be configured with. */
95
+ description?: string;
96
+ /** Answered when the operator has set nothing. Absent means the Add-on must handle null. */
97
+ default?: string | number | boolean;
98
+ };
99
+ /**
100
+ * Who may hand out one of this Add-on's roles.
101
+ *
102
+ * Declared rather than coded, which is the whole point: the Add-on's author decides the policy, and
103
+ * Core enforces it. A running Add-on cannot rewrite what is in a manifest the digest covers, so "the
104
+ * Add-on decides who may grant" and "an Add-on cannot widen what it was granted" stop contradicting
105
+ * each other.
106
+ *
107
+ * `siteAccess` names one of the Site's own claims; `role` names another role of this same Add-on. A
108
+ * Site Admin may always grant any role of any Add-on, so a graph that reaches nobody still has a way in.
109
+ */
110
+ export type PhiServerAddonRoleGrantPolicy = {
111
+ siteAccess: PhiServerSiteAccess;
112
+ } | {
113
+ role: string;
114
+ };
115
+ /**
116
+ * One role this Add-on defines, in its own vocabulary.
117
+ *
118
+ * A role answers "may this person do this at all", where there is no row yet to judge -- may they submit
119
+ * an offering, open the agent workspace, approve somebody else's work. What may be done to a particular
120
+ * row stays the row's question: ownership for their own, the group ladder for shared ones.
121
+ *
122
+ * Core never interprets the name. It stores assignments, answers `has()`, and enforces `grantableBy`;
123
+ * what `reviewer` means is the Add-on's business and stays there.
124
+ */
125
+ export type PhiServerAddonRoleDescriptor = {
126
+ name: string;
127
+ description?: string;
128
+ grantableBy: PhiServerAddonRoleGrantPolicy;
129
+ };
130
+ export type PhiServerAddonManifestV1 = {
131
+ manifestVersion: typeof PHI_SERVER_ADDON_MANIFEST_VERSION;
132
+ addonId: string;
133
+ packageName: string;
134
+ packageVersion: string;
135
+ serverAbi: string;
136
+ capabilities: PhiServerAddonCapabilityDescriptor[];
137
+ serviceProviders: PhiServerAddonServiceProviderDescriptor[];
138
+ requiredCoreCapabilities: PhiServerCoreCapabilityId[];
139
+ apiRoutes: PhiServerAddonApiRouteDescriptor[];
140
+ hooks: PhiServerAddonHookDescriptor[];
141
+ jobs: PhiServerAddonJobDescriptor[];
142
+ /**
143
+ * What an operator may configure. Absent means nothing is.
144
+ *
145
+ * Optional, like every field added after this manifest version was already in the wild. A required
146
+ * addition would make an installed Add-on's stored manifest unreadable the moment Core is upgraded --
147
+ * and its author had not forgotten anything, the field did not exist when they built it. The initial
148
+ * fields may demand an explicit `null`, because no stored manifest predates them; later ones must
149
+ * define what absence means and accept it.
150
+ */
151
+ settings?: PhiServerAddonSettingDescriptor[];
152
+ /**
153
+ * The roles this Add-on defines. Absent means it defines none.
154
+ *
155
+ * Optional for the same reason `settings` is: a manifest built before the field existed had not
156
+ * forgotten it. A name is frozen from the first assignment -- renaming one orphans every grant that
157
+ * points at it, silently, because the grant is stored as text and text does not follow a rename.
158
+ */
159
+ roles?: PhiServerAddonRoleDescriptor[];
160
+ /**
161
+ * The tables this Add-on owns, or null when it owns none.
162
+ *
163
+ * There is no list of migration steps beside it. The descriptor is the desired shape; what has been
164
+ * applied is recorded in the database, and Core derives the difference.
165
+ */
166
+ schema: PhiServerAddonSchemaDescriptor | null;
167
+ /**
168
+ * The statements this Add-on may run against its own tables, or null when it runs none.
169
+ *
170
+ * Declared for the same reason the tables are: Core derives the SQL once, so what an installed
171
+ * Add-on can do to its data is readable before it does it, and nothing is assembled per request.
172
+ *
173
+ * Optional for the reason `settings` is: it arrived after the manifest version was in the wild.
174
+ */
175
+ queries?: PhiServerAddonQueryCatalog | null;
176
+ /** The version of `schema`, or 0 when there is none. */
177
+ migrationVersion: number;
178
+ };
179
+ export type PhiServerAddonRequestContext = {
180
+ addonId: string;
181
+ requestId: string;
182
+ method: PhiServerAddonHttpMethod;
183
+ url: string;
184
+ headers: Headers;
185
+ body: Uint8Array;
186
+ path: string;
187
+ pathParams: Readonly<Record<string, string>>;
188
+ site: {
189
+ id: number;
190
+ key: string;
191
+ } | null;
192
+ actor: {
193
+ kind: "operator";
194
+ id: string;
195
+ } | {
196
+ kind: "user";
197
+ id: number;
198
+ } | {
199
+ kind: "internal";
200
+ id: null;
201
+ } | null;
202
+ /**
203
+ * What Core hands this request, bound to the Site and actor above.
204
+ *
205
+ * Only what the manifest declared arrives. A capability is not fetched, looked up, or constructed by
206
+ * the Add-on -- it is here or it is not, and it was already decided before the handler ran.
207
+ */
208
+ capabilities: PhiServerAddonCapabilities;
209
+ signal: AbortSignal;
210
+ };
211
+ export type PhiServerAddonHandler = (context: PhiServerAddonRequestContext) => Promise<Response> | Response;
212
+ /**
213
+ * A service factory, as the runtime exports it.
214
+ *
215
+ * Core states the shape of `config` and `context` per service kind; the manifest's interface digest is
216
+ * what makes that agreement checkable. The factory receives schema-validated profile configuration and
217
+ * a restricted context, never a database handle or a general secret accessor.
218
+ */
219
+ export type PhiServerAddonServiceFactory = (config: unknown, context: unknown) => unknown;
220
+ export type PhiServerAddonRuntimeV1 = {
221
+ manifest: PhiServerAddonManifestV1;
222
+ apiHandlers?: Readonly<Record<string, PhiServerAddonHandler>>;
223
+ hookHandlers?: Readonly<Record<string, PhiServerAddonHandler>>;
224
+ serviceFactories?: Readonly<Record<string, PhiServerAddonServiceFactory>>;
225
+ jobHandlers?: Readonly<Record<string, PhiServerAddonJobHandler>>;
226
+ };
227
+ export type PhiServerAddonRuntimeModuleV1 = {
228
+ phiServerAddon: PhiServerAddonRuntimeV1;
229
+ };
230
+ export type PhiServerAddonBuildEntry = {
231
+ enabled: boolean;
232
+ manifestDigest: string;
233
+ manifest: PhiServerAddonManifestV1;
234
+ load: () => Promise<PhiServerAddonRuntimeModuleV1>;
235
+ };
236
+ export type PhiServerAddonBuildManifest = Readonly<Record<string, PhiServerAddonBuildEntry>>;
237
+ /**
238
+ * One installed Add-on, as the operator's desired state records it.
239
+ *
240
+ * The entry carries the descriptors as well as the digests. Code and description then travel together
241
+ * as one per-instance unit, and the database is left to say only whether the Add-on may run. The two
242
+ * digests bind the three apart: `manifestDigest` covers the descriptors below, `artifactDigest` covers
243
+ * the file in the install root, and the loader refuses a file that no longer matches.
244
+ */
245
+ export type PhiServerAddonDesiredStateEntry = {
246
+ addonId: string;
247
+ packageName: string;
248
+ packageVersion: string;
249
+ manifestDigest: string;
250
+ /** SHA-256 of the installed artifact, and the cache key its load address carries. */
251
+ artifactDigest: string;
252
+ /** The validated descriptors, as read from the artifact at installation. */
253
+ manifest: PhiServerAddonManifestV1;
254
+ enabled: boolean;
255
+ };
256
+ export type PhiServerAddonDesiredState = {
257
+ formatVersion: 1;
258
+ addons: PhiServerAddonDesiredStateEntry[];
259
+ };
package/dist/addon.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What a phi-server Add-on declares or implements.
3
+ *
4
+ * This package exists so an Add-on has something to build against that is not phi-server itself.
5
+ * phi-server is a private application without an export surface; an Add-on published to a registry
6
+ * could not depend on it, and a type import into the application's internals would bind an Add-on to a
7
+ * file path that can move without anybody noticing.
8
+ *
9
+ * The admission rule is strict, and it is the only thing keeping this from becoming a junk drawer:
10
+ * nothing enters unless at least one Add-on must declare or implement it. No validation, no helpers,
11
+ * no node builtins -- the Add-on states, and Core checks. If something here ever does more than hand
12
+ * back a constant, it is in the wrong package.
13
+ */
14
+ export const PHI_SERVER_ADDON_MANIFEST_VERSION = 1;
15
+ export const PHI_SERVER_ADDON_ABI_VERSION = 1;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * What Core hands an Add-on, as opposed to what it checks about one.
3
+ *
4
+ * A capability reaches an Add-on already bound to the request it belongs to: the Site and the acting
5
+ * user are closed over, not passed in. There is therefore no argument through which an Add-on could
6
+ * name another Site or ask about somebody else -- the restriction is the shape of the thing rather
7
+ * than a check inside it. `PhisMediaStorageServiceContext` binds a Provider to its own secret in the
8
+ * same way and for the same reason.
9
+ *
10
+ * Only the capabilities an Add-on's manifest declared, and this release offers, are present. The rest
11
+ * are absent rather than throwing: an Add-on that reaches for one it did not declare finds `undefined`
12
+ * at the point where it made the mistake.
13
+ */
14
+ import type { PhiServerDataCapabilityV1 } from "./queries.js";
15
+ /**
16
+ * The cumulative group levels, by name.
17
+ *
18
+ * Names rather than numbers cross this boundary. The bits are Core's business, an Add-on has no reason
19
+ * to do arithmetic on them, and a value that never leaves Core is one an Add-on cannot come to depend
20
+ * on. Each level does everything the one before it does and one thing more.
21
+ */
22
+ export declare const PHI_SERVER_GROUP_LEVELS: readonly ["member", "author", "editor", "manager"];
23
+ export type PhiServerGroupLevel = (typeof PHI_SERVER_GROUP_LEVELS)[number];
24
+ /**
25
+ * What an actor wants to do to one row.
26
+ *
27
+ * Creating is absent on purpose. There is no row to ask about yet, and whether an Add-on may write at
28
+ * all is settled before its handler runs.
29
+ */
30
+ export type PhiServerRowIntent = "read" | "update" | "delete";
31
+ /**
32
+ * A Site capability an Add-on may ask after, named symbolically.
33
+ *
34
+ * An Add-on never states a role flag. Which capabilities are askable is then a decision, and Core is
35
+ * free to change what a role means without an installed Add-on holding a stale bitmask.
36
+ */
37
+ export declare const PHI_SERVER_SITE_ACCESS: {
38
+ readonly siteAdmin: "site-admin";
39
+ readonly developerTools: "developer-tools";
40
+ readonly structureAuthoring: "structure-authoring";
41
+ readonly contentEditing: "content-editing";
42
+ readonly publishing: "publishing";
43
+ readonly siteMedia: "site-media";
44
+ readonly support: "support";
45
+ readonly accounting: "accounting";
46
+ };
47
+ export type PhiServerSiteAccess = (typeof PHI_SERVER_SITE_ACCESS)[keyof typeof PHI_SERVER_SITE_ACCESS];
48
+ /**
49
+ * The authorization capability: `@phis/server/authorization:v1`.
50
+ *
51
+ * It answers questions and grants nothing. There is no operation here that changes a role, a
52
+ * membership, or an owner, and there must not be one -- a single capability declaration would then be
53
+ * permission to rewrite the authorization model rather than permission to consult it.
54
+ *
55
+ * `mayActOnRow` is the reason this exists. The scope columns on an Add-on's tables are Core's:
56
+ * `ownerScoped` and `groupScoped` mean Core wrote `owner_user_id` and `group_id`, so Core can read
57
+ * them back and apply the ladder itself. The Add-on names a row and an intent; it never reproduces the
58
+ * rule, which is what keeps one rule from becoming one implementation per package.
59
+ *
60
+ * A table that is neither owner- nor group-scoped has nothing for this to decide and always answers
61
+ * false: absence of a scope is not absence of a restriction.
62
+ */
63
+ export type PhiServerAuthorizationCapabilityV1 = {
64
+ /**
65
+ * Whether the acting user may do this to one row of one of this Add-on's own tables.
66
+ *
67
+ * `table` is a table of this Add-on's schema under the name its descriptor gave it; another
68
+ * Add-on's table is not addressable, because the schema is derived from the caller rather than
69
+ * taken from the argument. A row that does not exist answers false rather than raising -- "may I"
70
+ * about something absent has no other honest answer.
71
+ */
72
+ mayActOnRow(input: {
73
+ table: string;
74
+ id: number | string;
75
+ intent: PhiServerRowIntent;
76
+ }): Promise<boolean>;
77
+ /** The level the acting user effectively holds in one group, or null when they are not in it. */
78
+ groupLevel(groupId: number): Promise<PhiServerGroupLevel | null>;
79
+ /** Whether the acting user holds one Site capability, with the Core Admin override applied. */
80
+ hasSiteAccess(access: PhiServerSiteAccess): Promise<boolean>;
81
+ };
82
+ /**
83
+ * The capabilities a handler may find on its context, keyed as the Core catalog keys them.
84
+ *
85
+ * Optional throughout: presence follows the manifest, and a capability this release does not offer
86
+ * stops the Add-on at the dispatcher rather than arriving as a broken object.
87
+ */
88
+ export type PhiServerAddonCapabilities = {
89
+ authorization?: PhiServerAuthorizationCapabilityV1;
90
+ data?: PhiServerDataCapabilityV1;
91
+ storage?: PhiServerStorageCapabilityV1;
92
+ secrets?: PhiServerSecretsCapabilityV1;
93
+ groups?: PhiServerGroupsCapabilityV1;
94
+ settings?: PhiServerSettingsCapabilityV1;
95
+ roles?: PhiServerRolesCapabilityV1;
96
+ };
97
+ /**
98
+ * The roles capability: `@phis/server/roles:v1`.
99
+ *
100
+ * Answers whether the acting user holds one of the roles this Add-on declared, and nothing about who
101
+ * they are. There is no writer, for the reason `settings:v1` and `secrets:v1` have none: an Add-on able
102
+ * to grant its own roles could grant itself whatever those roles guard.
103
+ *
104
+ * Only declared roles are answered. A grant stored against a name this release no longer declares is not
105
+ * handed over, the same way a stored setting nobody declared is not -- so an Add-on never sees a role it
106
+ * does not know, whatever has accumulated across versions.
107
+ */
108
+ export type PhiServerRolesCapabilityV1 = {
109
+ /** Whether the acting user holds this role here. False when nobody is acting. */
110
+ has(role: string): Promise<boolean>;
111
+ /** Every declared role the acting user holds, in declaration order. */
112
+ all(): Promise<readonly string[]>;
113
+ };
114
+ /**
115
+ * The storage capability: `@phis/server/storage:v1`.
116
+ *
117
+ * A private object store, bound to this Add-on and this Site. Keys are the Add-on's own and relative:
118
+ * Core puts them under a prefix it derives, and never hands the physical key back. An Add-on therefore
119
+ * cannot store an address, and cannot reach another Add-on's objects or another Site's by constructing
120
+ * one -- the same rule the install root follows, where a stored string never becomes an import
121
+ * specifier.
122
+ *
123
+ * This is not the Media library. Assets that a person uploads, browses, and references belong in a
124
+ * Media Space with its folders, quotas and delivery policies. This is for what an Add-on keeps for
125
+ * itself: a release artifact, a generated preview, an export waiting to be fetched.
126
+ */
127
+ export type PhiServerStorageCapabilityV1 = {
128
+ put(input: {
129
+ key: string;
130
+ body: Uint8Array;
131
+ contentType: string;
132
+ }): Promise<{
133
+ key: string;
134
+ byteSize: number;
135
+ }>;
136
+ /** Null rather than throwing for an object that is not there. */
137
+ get(key: string): Promise<{
138
+ body: Uint8Array;
139
+ contentType: string;
140
+ } | null>;
141
+ head(key: string): Promise<{
142
+ key: string;
143
+ byteSize: number;
144
+ contentType: string;
145
+ } | null>;
146
+ /** Whether an object was removed. Removing what is not there is not an error. */
147
+ remove(key: string): Promise<boolean>;
148
+ /** The Add-on's own keys under a prefix, relative as they were given. */
149
+ list(prefix?: string): Promise<string[]>;
150
+ };
151
+ /**
152
+ * The secrets capability: `@phis/server/secrets:v1`.
153
+ *
154
+ * An Add-on reads the secrets an operator set for it and no others. The owner is bound here, so there
155
+ * is no argument through which another Add-on's reference could be named -- the resolver enforces the
156
+ * same rule a second time, because the two are different kinds of mistake.
157
+ *
158
+ * There is no writer. An operator sets a secret with `phis secret set`; a package that could write its
159
+ * own would be a package that could plant one.
160
+ */
161
+ export type PhiServerSecretsCapabilityV1 = {
162
+ /** The value, or null when the operator has not configured it. */
163
+ read(name: string): Promise<string | null>;
164
+ };
165
+ /**
166
+ * What an Add-on may learn about a person.
167
+ *
168
+ * Deliberately the same projection one group member gets of another: a display name, a company only
169
+ * where the group discloses it, and never an address. An Add-on renders "listed by" and nothing more.
170
+ */
171
+ export type PhiServerUserProjection = {
172
+ userId: number;
173
+ displayName: string | null;
174
+ companyName: string | null;
175
+ };
176
+ /**
177
+ * The groups capability: `@phis/server/groups:v1`.
178
+ *
179
+ * Group facts. What follows from them -- may this actor do this -- is `authorization:v1`.
180
+ *
181
+ * Every answer is bounded by the acting user: an Add-on sees the groups its actor is in and the people
182
+ * its actor could already see in them. It cannot become a directory of a Site's users, because there is
183
+ * no question here that reaches past the actor's own membership.
184
+ */
185
+ export type PhiServerGroupsCapabilityV1 = {
186
+ /** The groups the acting user belongs to on this Site. */
187
+ myGroups(): Promise<Array<{
188
+ id: number;
189
+ key: string;
190
+ name: string;
191
+ }>>;
192
+ /** The members of one group the acting user is in, projected as a member sees them. */
193
+ members(groupId: number): Promise<PhiServerUserProjection[]>;
194
+ /**
195
+ * One person, if the acting user shares a group with them.
196
+ *
197
+ * Null otherwise, and null is the whole point: it is what stops a row's owner id from becoming a
198
+ * lookup into everybody.
199
+ */
200
+ user(userId: number): Promise<PhiServerUserProjection | null>;
201
+ };
202
+ /**
203
+ * The settings capability: `@phis/server/settings:v1`.
204
+ *
205
+ * What an operator configures and an Add-on reads: a commission rate, a moderation switch, an endpoint.
206
+ * Distinct from `secrets:v1` on purpose -- a percentage is not a credential, and asking the secret store
207
+ * to hold one would make "what is configured here" unreadable to the operator who configured it.
208
+ *
209
+ * There is no writer. Settings are the operator's, set with `phis addon config set`, and an Add-on that
210
+ * could write its own would be able to widen what it was given.
211
+ *
212
+ * A setting the operator has not set answers with the declared default, so an Add-on has one shape to
213
+ * handle rather than two, and `null` means genuinely unset with no default to fall back to.
214
+ */
215
+ export type PhiServerSettingsCapabilityV1 = {
216
+ read(name: string): Promise<string | number | boolean | null>;
217
+ all(): Promise<Readonly<Record<string, string | number | boolean | null>>>;
218
+ };
219
+ /**
220
+ * What a job receives.
221
+ *
222
+ * No request, no headers, and above all no actor: a job is the Add-on acting on its own data, not a
223
+ * person acting through it. That is why the row-level ladder does not apply inside one, and why a job
224
+ * is reachable only from `phis addon job run` and never from a route -- an Add-on that could start its
225
+ * own job from a request would have built itself a way around the ladder.
226
+ */
227
+ export type PhiServerAddonJobContext = {
228
+ addonId: string;
229
+ jobId: string;
230
+ /** This run, for correlating what the job logs with what the operator started. */
231
+ runId: string;
232
+ /** The Site this run is for. A job always runs against exactly one. */
233
+ site: {
234
+ id: number;
235
+ key: string;
236
+ };
237
+ capabilities: PhiServerAddonCapabilities;
238
+ signal: AbortSignal;
239
+ };
240
+ export type PhiServerAddonJobHandler = (context: PhiServerAddonJobContext) => Promise<void> | void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * What Core hands an Add-on, as opposed to what it checks about one.
3
+ *
4
+ * A capability reaches an Add-on already bound to the request it belongs to: the Site and the acting
5
+ * user are closed over, not passed in. There is therefore no argument through which an Add-on could
6
+ * name another Site or ask about somebody else -- the restriction is the shape of the thing rather
7
+ * than a check inside it. `PhisMediaStorageServiceContext` binds a Provider to its own secret in the
8
+ * same way and for the same reason.
9
+ *
10
+ * Only the capabilities an Add-on's manifest declared, and this release offers, are present. The rest
11
+ * are absent rather than throwing: an Add-on that reaches for one it did not declare finds `undefined`
12
+ * at the point where it made the mistake.
13
+ */
14
+ /**
15
+ * The cumulative group levels, by name.
16
+ *
17
+ * Names rather than numbers cross this boundary. The bits are Core's business, an Add-on has no reason
18
+ * to do arithmetic on them, and a value that never leaves Core is one an Add-on cannot come to depend
19
+ * on. Each level does everything the one before it does and one thing more.
20
+ */
21
+ export const PHI_SERVER_GROUP_LEVELS = ["member", "author", "editor", "manager"];
22
+ /**
23
+ * A Site capability an Add-on may ask after, named symbolically.
24
+ *
25
+ * An Add-on never states a role flag. Which capabilities are askable is then a decision, and Core is
26
+ * free to change what a role means without an installed Add-on holding a stale bitmask.
27
+ */
28
+ export const PHI_SERVER_SITE_ACCESS = {
29
+ siteAdmin: "site-admin",
30
+ developerTools: "developer-tools",
31
+ structureAuthoring: "structure-authoring",
32
+ contentEditing: "content-editing",
33
+ publishing: "publishing",
34
+ siteMedia: "site-media",
35
+ support: "support",
36
+ accounting: "accounting",
37
+ };
package/dist/core.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What Core offers, named once so both sides can spell it the same way.
3
+ *
4
+ * An Add-on declares which Core capabilities it needs and which Core-owned service kinds it
5
+ * implements. Those names are an agreement between two artifacts that ship separately, which is
6
+ * exactly what this package is for -- and until they lived here, an Add-on author had to read them out
7
+ * of phi-server, a private application they never receive. The S3 Storage Add-on carried the media
8
+ * storage digest as a copied hex literal for that reason.
9
+ *
10
+ * This list is a promise, not a plan. A capability appears here when Core delivers it, never before:
11
+ * an Add-on that finds a name here must be able to rely on it. `images:v1` is therefore absent.
12
+ *
13
+ * Still no node builtins. The digests are stated rather than derived, so a manifest stays readable
14
+ * without hashing anything -- Core recomputes them and checks, which is where that belongs.
15
+ */
16
+ /** The Core capabilities an Add-on may require. */
17
+ export declare const PHI_SERVER_CORE_CAPABILITIES: {
18
+ readonly authentication: "@phis/server/authentication:v1";
19
+ /**
20
+ * "May this actor", in all three of its sources: does the row belong to them, what level do they
21
+ * hold in its group, and what may they do on this Site at all. It is spelled out rather than
22
+ * shortened to `auth` because the shortening would sit one letter away from `authentication` and
23
+ * mean the opposite half of the same word.
24
+ */
25
+ readonly authorization: "@phis/server/authorization:v1";
26
+ /** Running the queries the manifest declared against this Add-on's own tables. */
27
+ readonly data: "@phis/server/data:v1";
28
+ readonly threads: "@phis/server/threads:v1";
29
+ readonly resourceLinks: "@phis/server/resource-links:v1";
30
+ readonly support: "@phis/server/support:v1";
31
+ /** Group facts -- which groups exist, who is in them. What follows from them is `authorization`. */
32
+ readonly groups: "@phis/server/groups:v1";
33
+ readonly storage: "@phis/server/storage:v1";
34
+ /** Reading the secrets an operator set for this Add-on, and no others. */
35
+ readonly secrets: "@phis/server/secrets:v1";
36
+ /** Reading what an operator configured for this Add-on on this Site. */
37
+ readonly settings: "@phis/server/settings:v1";
38
+ /** Whether the acting user holds one of this Add-on's own declared roles. */
39
+ readonly roles: "@phis/server/roles:v1";
40
+ };
41
+ export type PhiServerCoreCapabilityId = (typeof PHI_SERVER_CORE_CAPABILITIES)[keyof typeof PHI_SERVER_CORE_CAPABILITIES];
42
+ /**
43
+ * The Core-owned service kinds an Add-on may implement.
44
+ *
45
+ * A service kind is not a capability. A capability is something Core offers an Add-on; a service kind
46
+ * is something Core already owns and an Add-on supplies an implementation of -- S3 for Media Storage,
47
+ * LDAP for a Directory. Core keeps selecting the configured implementation itself.
48
+ */
49
+ export declare const PHI_SERVER_SERVICE_KINDS: {
50
+ readonly mediaStorage: "@phis/server/service/media-storage";
51
+ readonly directory: "@phis/server/service/directory";
52
+ };
53
+ export type PhiServerServiceKind = (typeof PHI_SERVER_SERVICE_KINDS)[keyof typeof PHI_SERVER_SERVICE_KINDS];
54
+ /**
55
+ * The digest of the interface each service kind currently has.
56
+ *
57
+ * An Add-on puts the digest of the interface it was built against into its manifest. Core compares it
58
+ * with what this release offers and refuses the Add-on when they differ -- at install, with the value
59
+ * quoted back, rather than at the first call. Whoever changes the shape of an interface raises its
60
+ * version and replaces the digest here, and every Add-on built against the old one is refused.
61
+ *
62
+ * The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
63
+ * documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
64
+ */
65
+ export declare const PHI_SERVER_SERVICE_INTERFACE_VERSIONS: Readonly<Record<PhiServerServiceKind, number>>;
66
+ export declare const PHI_SERVER_SERVICE_INTERFACE_DIGESTS: Readonly<Record<PhiServerServiceKind, string>>;