@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.
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +102 -0
- package/dist/access.d.ts +115 -0
- package/dist/access.js +157 -0
- package/dist/addon-index.d.ts +6 -0
- package/dist/addon-index.js +6 -0
- package/dist/addon.d.ts +259 -0
- package/dist/addon.js +15 -0
- package/dist/capabilities.d.ts +240 -0
- package/dist/capabilities.js +37 -0
- package/dist/core.d.ts +66 -0
- package/dist/core.js +70 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/media-storage.d.ts +160 -0
- package/dist/media-storage.js +9 -0
- package/dist/queries.d.ts +171 -0
- package/dist/queries.js +16 -0
- package/dist/schema.d.ts +173 -0
- package/dist/schema.js +29 -0
- package/dist/signals.d.ts +36 -0
- package/dist/signals.js +85 -0
- package/package.json +52 -0
package/dist/addon.d.ts
ADDED
|
@@ -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>>;
|