@phis/contracts 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -11
- package/dist/access.d.ts +50 -0
- package/dist/access.js +65 -0
- package/dist/addon-index.d.ts +1 -0
- package/dist/addon-index.js +1 -0
- package/dist/addon.d.ts +130 -57
- package/dist/addon.js +2 -2
- package/dist/assets.d.ts +165 -0
- package/dist/assets.js +1 -0
- package/dist/capabilities.d.ts +205 -30
- package/dist/capabilities.js +2 -2
- package/dist/catalog.d.ts +104 -0
- package/dist/catalog.js +116 -0
- package/dist/cms.d.ts +120 -0
- package/dist/cms.js +270 -0
- package/dist/core.d.ts +14 -6
- package/dist/core.js +16 -8
- package/dist/http.d.ts +33 -0
- package/dist/http.js +33 -0
- package/dist/locale.d.ts +14 -0
- package/dist/locale.js +14 -0
- package/dist/logging.d.ts +49 -0
- package/dist/logging.js +17 -0
- package/dist/media-storage.d.ts +129 -0
- package/dist/media-storage.js +56 -0
- package/dist/media.d.ts +228 -0
- package/dist/media.js +362 -0
- package/dist/queries.d.ts +280 -48
- package/dist/queries.js +10 -1
- package/dist/schema.d.ts +160 -26
- package/dist/schema.js +11 -2
- package/dist/server-capabilities.d.ts +37 -0
- package/dist/server-capabilities.js +17 -0
- package/dist/signals.d.ts +58 -0
- package/dist/signals.js +118 -0
- package/dist/site-groups.d.ts +38 -0
- package/dist/site-groups.js +54 -0
- package/package.json +43 -3
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { PhiMediaUploadPlan } from "./media-storage.js";
|
|
2
|
+
/**
|
|
3
|
+
* Files on a row: `@phis/server/assets:v1`.
|
|
4
|
+
*
|
|
5
|
+
* The slots are declared in the schema descriptor -- a name, a cardinality, a size ceiling, the media
|
|
6
|
+
* types it takes, who may fetch it -- and this is how they are filled. An Add-on says which row and
|
|
7
|
+
* which slot; where the object goes, what it is called and whether it arrived are Core's answers.
|
|
8
|
+
*
|
|
9
|
+
* There is one upload lifecycle in Core and this is not a second one. Core reserves, receives, measures
|
|
10
|
+
* and hashes without knowing who asked; the purpose stated here is read back at the last step, where
|
|
11
|
+
* the finished object becomes a row in the Add-on's table.
|
|
12
|
+
*
|
|
13
|
+
* **No method takes an object key and none returns one.** A key an Add-on chooses is a key Core cannot
|
|
14
|
+
* recognise later, and an object Core cannot recognise is one it can neither deliver nor account for.
|
|
15
|
+
* What comes back instead is a Media Asset id, which the delivery Core already has takes.
|
|
16
|
+
*/
|
|
17
|
+
export type PhisAddonAssetView = {
|
|
18
|
+
id: string;
|
|
19
|
+
slot: string;
|
|
20
|
+
ownerId: string;
|
|
21
|
+
/** The file itself, as Core records it. */
|
|
22
|
+
mediaAssetId: number;
|
|
23
|
+
/**
|
|
24
|
+
* Where the file is served from, Site-relative, as Core addresses its own Media.
|
|
25
|
+
*
|
|
26
|
+
* Given rather than left to be composed, because composing it means knowing two things an Add-on has
|
|
27
|
+
* no way to be told: the route, and the cache revision it carries. A path built without the revision
|
|
28
|
+
* still delivers the file and never gets an immutable cache -- every viewer revalidates every time,
|
|
29
|
+
* and nothing about the result looks wrong enough to investigate.
|
|
30
|
+
*
|
|
31
|
+
* Read at the moment it is used, never stored. The revision rotates whenever the file's delivery
|
|
32
|
+
* changes, and a path kept from an earlier read would then be the stale one it exists to retire.
|
|
33
|
+
*
|
|
34
|
+
* What the path answers with is the slot's `delivery`: `public` to anyone, `authenticated` to a
|
|
35
|
+
* session, `internal` only to Core's own caller. The address is not the permission.
|
|
36
|
+
*/
|
|
37
|
+
contentPath: string;
|
|
38
|
+
byteSize: number;
|
|
39
|
+
contentType: string;
|
|
40
|
+
/**
|
|
41
|
+
* The digest that was established, with the algorithm that produced it -- or null where none was.
|
|
42
|
+
*
|
|
43
|
+
* Established means one of two things: the endpoint verified what the client stated and would have
|
|
44
|
+
* refused anything else, or Core received the body and hashed it in flight. Null is a truthful "not
|
|
45
|
+
* established" -- an `etag` is an HTTP header and not a content hash, and a wrong digest here would
|
|
46
|
+
* be worse than none, because it invites a comparison.
|
|
47
|
+
*
|
|
48
|
+
* The algorithm is part of the answer and not an assumption. Which one an endpoint can attest to
|
|
49
|
+
* differs between Providers, so a bare value would be a number whose meaning the reader has to guess.
|
|
50
|
+
*/
|
|
51
|
+
checksum: {
|
|
52
|
+
algorithm: string;
|
|
53
|
+
value: string;
|
|
54
|
+
} | null;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A place made for a file, and how to deliver it.
|
|
59
|
+
*
|
|
60
|
+
* The plan is the Provider's answer, not Core's: an endpoint that can take a body directly says so, and
|
|
61
|
+
* one that cannot names a Core route to stream through. An Add-on passes it to its client unread.
|
|
62
|
+
*/
|
|
63
|
+
export type PhisAddonAssetReservationView = {
|
|
64
|
+
/** What the transfer is carried out against, and what `finalize` is called with. */
|
|
65
|
+
token: string;
|
|
66
|
+
plan: PhiMediaUploadPlan;
|
|
67
|
+
expiresAt: string;
|
|
68
|
+
};
|
|
69
|
+
export type PhisAssetsCapabilityV1 = {
|
|
70
|
+
/**
|
|
71
|
+
* Reserves a place for a file, before any of it is sent.
|
|
72
|
+
*
|
|
73
|
+
* The size is stated now and held to later, and the space is charged now rather than at the end. A
|
|
74
|
+
* ceiling decided at the end would bound the bookkeeping and not the store: ten clients could each
|
|
75
|
+
* write a gigabyte and have five of them refused afterwards, with the bytes already paid for.
|
|
76
|
+
*
|
|
77
|
+
* `sha256` is the client's statement of what it is about to send. Where the Storage Profile was found
|
|
78
|
+
* to verify one, it is signed into the plan and the endpoint refuses anything else -- so a wrong
|
|
79
|
+
* digest never becomes an object, and Core can record the figure as true without having seen a byte.
|
|
80
|
+
* Where it was not, the body comes through Core and Core hashes it instead.
|
|
81
|
+
*
|
|
82
|
+
* Refused where the slot is not declared, where the type is not one it takes, where the size is over
|
|
83
|
+
* the slot's ceiling or the Site's, where the Space is full, and where the acting user may not change
|
|
84
|
+
* the row. Attaching a file to a row changes it, so it is the row's authority that decides.
|
|
85
|
+
*/
|
|
86
|
+
begin(input: {
|
|
87
|
+
table: string;
|
|
88
|
+
ownerId: string | number;
|
|
89
|
+
slot: string;
|
|
90
|
+
contentType: string;
|
|
91
|
+
sizeBytes: number;
|
|
92
|
+
sha256?: string;
|
|
93
|
+
/** What the file was called where it came from. Core makes one up when there is nothing to use. */
|
|
94
|
+
filename?: string;
|
|
95
|
+
}): Promise<PhisAddonAssetReservationView>;
|
|
96
|
+
/**
|
|
97
|
+
* Settles a reservation once the bytes are there, and makes the file a row.
|
|
98
|
+
*
|
|
99
|
+
* Core looks at what actually landed rather than believing the report: nothing there is not a file,
|
|
100
|
+
* and a different size than was reserved is not a file either. On a single-valued slot the file being
|
|
101
|
+
* replaced goes in the same transaction -- there is no moment in which the row holds two.
|
|
102
|
+
*
|
|
103
|
+
* Repeating it is safe. A client whose answer was lost cannot tell a finished upload from a failed
|
|
104
|
+
* one, so a second call answers with the same file rather than with an error.
|
|
105
|
+
*/
|
|
106
|
+
finalize(input: {
|
|
107
|
+
table: string;
|
|
108
|
+
token: string;
|
|
109
|
+
/** Whatever the plan's issuer asked the client to bring back. Core passes it through unread. */
|
|
110
|
+
completion?: unknown;
|
|
111
|
+
}): Promise<PhisAddonAssetView>;
|
|
112
|
+
/** Gives an unredeemed reservation up. An abandoned one expires on its own; this is the tidy path. */
|
|
113
|
+
abandon(input: {
|
|
114
|
+
table: string;
|
|
115
|
+
token: string;
|
|
116
|
+
}): Promise<boolean>;
|
|
117
|
+
/**
|
|
118
|
+
* The files of a row. A reservation in flight is not among them, because it is not a file yet.
|
|
119
|
+
*
|
|
120
|
+
* Refused where the acting user may not read the row. Asking what a row carries is reading the row,
|
|
121
|
+
* and a slot's `delivery` does not answer it: a file whose id, name and size come back to anyone has
|
|
122
|
+
* already given up most of what `internal` was keeping.
|
|
123
|
+
*/
|
|
124
|
+
list(input: {
|
|
125
|
+
table: string;
|
|
126
|
+
ownerId: string | number;
|
|
127
|
+
slot?: string;
|
|
128
|
+
}): Promise<PhisAddonAssetView[]>;
|
|
129
|
+
/**
|
|
130
|
+
* Takes a file out of its slot, and the file with it.
|
|
131
|
+
*
|
|
132
|
+
* Refused where the acting user may not change the row, which is the same authority `begin` is
|
|
133
|
+
* measured against -- taking a file away changes the row exactly as putting one there does. A file
|
|
134
|
+
* that is not there answers `false` rather than refusing, because a refusal would say it exists.
|
|
135
|
+
*/
|
|
136
|
+
remove(input: {
|
|
137
|
+
table: string;
|
|
138
|
+
id: string;
|
|
139
|
+
}): Promise<boolean>;
|
|
140
|
+
/**
|
|
141
|
+
* What this Add-on's files take up on this Site, and what they are allowed to.
|
|
142
|
+
*
|
|
143
|
+
* Here for the reason `storage:v1` has the same call: the ceiling is the operator's and Core enforces
|
|
144
|
+
* it whether or not anybody read this first, so this exists to let an Add-on say *why*. A refusal an
|
|
145
|
+
* interface can explain before a person picks a file beats one that arrives after they waited for the
|
|
146
|
+
* upload, and a vendor who can see they are at their limit does not report it as a fault.
|
|
147
|
+
*
|
|
148
|
+
* The two figures are separate because both are spent. `bytes` is what is stored; `reservedBytes` is
|
|
149
|
+
* held for uploads in flight and is as unavailable as stored bytes until they finish or lapse. Room
|
|
150
|
+
* for one more file is `quotaBytes - (bytes + reservedBytes)`, which is the comparison `begin` makes.
|
|
151
|
+
*
|
|
152
|
+
* `quotaBytes` is the effective ceiling after inheritance -- the Space's own override where there is
|
|
153
|
+
* one, the Site's default for Add-on Spaces otherwise -- so it is the figure that will actually be
|
|
154
|
+
* applied and not a raw column. `null` means no ceiling. An explicit `0` is a revocation, not an
|
|
155
|
+
* absence.
|
|
156
|
+
*
|
|
157
|
+
* No object count, unlike `storage:v1`. Core keeps a running total of bytes and none of files, and
|
|
158
|
+
* counting them on demand would be the unbounded read the ceiling exists to prevent.
|
|
159
|
+
*/
|
|
160
|
+
usage(): Promise<{
|
|
161
|
+
bytes: number;
|
|
162
|
+
reservedBytes: number;
|
|
163
|
+
quotaBytes: number | null;
|
|
164
|
+
}>;
|
|
165
|
+
};
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* are absent rather than throwing: an Add-on that reaches for one it did not declare finds `undefined`
|
|
12
12
|
* at the point where it made the mistake.
|
|
13
13
|
*/
|
|
14
|
-
import type {
|
|
14
|
+
import type { PhisAssetsCapabilityV1 } from "./assets.js";
|
|
15
|
+
import type { PhisDataCapabilityV1 } from "./queries.js";
|
|
15
16
|
/**
|
|
16
17
|
* The cumulative group levels, by name.
|
|
17
18
|
*
|
|
@@ -19,22 +20,22 @@ import type { PhiServerDataCapabilityV1 } from "./queries.js";
|
|
|
19
20
|
* to do arithmetic on them, and a value that never leaves Core is one an Add-on cannot come to depend
|
|
20
21
|
* on. Each level does everything the one before it does and one thing more.
|
|
21
22
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
export type
|
|
23
|
+
export declare const PHIS_GROUP_LEVELS: readonly ["member", "author", "editor", "manager"];
|
|
24
|
+
export type PhisGroupLevel = (typeof PHIS_GROUP_LEVELS)[number];
|
|
24
25
|
/**
|
|
25
26
|
* What an actor wants to do to one row.
|
|
26
27
|
*
|
|
27
28
|
* Creating is absent on purpose. There is no row to ask about yet, and whether an Add-on may write at
|
|
28
29
|
* all is settled before its handler runs.
|
|
29
30
|
*/
|
|
30
|
-
export type
|
|
31
|
+
export type PhisRowIntent = "read" | "update" | "delete";
|
|
31
32
|
/**
|
|
32
33
|
* A Site capability an Add-on may ask after, named symbolically.
|
|
33
34
|
*
|
|
34
35
|
* An Add-on never states a role flag. Which capabilities are askable is then a decision, and Core is
|
|
35
36
|
* free to change what a role means without an installed Add-on holding a stale bitmask.
|
|
36
37
|
*/
|
|
37
|
-
export declare const
|
|
38
|
+
export declare const PHIS_SITE_ACCESS: {
|
|
38
39
|
readonly siteAdmin: "site-admin";
|
|
39
40
|
readonly developerTools: "developer-tools";
|
|
40
41
|
readonly structureAuthoring: "structure-authoring";
|
|
@@ -44,7 +45,7 @@ export declare const PHI_SERVER_SITE_ACCESS: {
|
|
|
44
45
|
readonly support: "support";
|
|
45
46
|
readonly accounting: "accounting";
|
|
46
47
|
};
|
|
47
|
-
export type
|
|
48
|
+
export type PhisSiteAccess = (typeof PHIS_SITE_ACCESS)[keyof typeof PHIS_SITE_ACCESS];
|
|
48
49
|
/**
|
|
49
50
|
* The authorization capability: `@phis/server/authorization:v1`.
|
|
50
51
|
*
|
|
@@ -60,7 +61,7 @@ export type PhiServerSiteAccess = (typeof PHI_SERVER_SITE_ACCESS)[keyof typeof P
|
|
|
60
61
|
* A table that is neither owner- nor group-scoped has nothing for this to decide and always answers
|
|
61
62
|
* false: absence of a scope is not absence of a restriction.
|
|
62
63
|
*/
|
|
63
|
-
export type
|
|
64
|
+
export type PhisAuthorizationCapabilityV1 = {
|
|
64
65
|
/**
|
|
65
66
|
* Whether the acting user may do this to one row of one of this Add-on's own tables.
|
|
66
67
|
*
|
|
@@ -72,12 +73,12 @@ export type PhiServerAuthorizationCapabilityV1 = {
|
|
|
72
73
|
mayActOnRow(input: {
|
|
73
74
|
table: string;
|
|
74
75
|
id: number | string;
|
|
75
|
-
intent:
|
|
76
|
+
intent: PhisRowIntent;
|
|
76
77
|
}): Promise<boolean>;
|
|
77
78
|
/** The level the acting user effectively holds in one group, or null when they are not in it. */
|
|
78
|
-
groupLevel(groupId: number): Promise<
|
|
79
|
+
groupLevel(groupId: number): Promise<PhisGroupLevel | null>;
|
|
79
80
|
/** Whether the acting user holds one Site capability, with the Core Admin override applied. */
|
|
80
|
-
hasSiteAccess(access:
|
|
81
|
+
hasSiteAccess(access: PhisSiteAccess): Promise<boolean>;
|
|
81
82
|
};
|
|
82
83
|
/**
|
|
83
84
|
* The capabilities a handler may find on its context, keyed as the Core catalog keys them.
|
|
@@ -85,14 +86,16 @@ export type PhiServerAuthorizationCapabilityV1 = {
|
|
|
85
86
|
* Optional throughout: presence follows the manifest, and a capability this release does not offer
|
|
86
87
|
* stops the Add-on at the dispatcher rather than arriving as a broken object.
|
|
87
88
|
*/
|
|
88
|
-
export type
|
|
89
|
-
authorization?:
|
|
90
|
-
data?:
|
|
91
|
-
storage?:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
export type PhisAddonCapabilities = {
|
|
90
|
+
authorization?: PhisAuthorizationCapabilityV1;
|
|
91
|
+
data?: PhisDataCapabilityV1;
|
|
92
|
+
storage?: PhisStorageCapabilityV1;
|
|
93
|
+
assets?: PhisAssetsCapabilityV1;
|
|
94
|
+
secrets?: PhisSecretsCapabilityV1;
|
|
95
|
+
groups?: PhisGroupsCapabilityV1;
|
|
96
|
+
threads?: PhisThreadsCapabilityV1;
|
|
97
|
+
settings?: PhisSettingsCapabilityV1;
|
|
98
|
+
roles?: PhisRolesCapabilityV1;
|
|
96
99
|
};
|
|
97
100
|
/**
|
|
98
101
|
* The roles capability: `@phis/server/roles:v1`.
|
|
@@ -105,7 +108,7 @@ export type PhiServerAddonCapabilities = {
|
|
|
105
108
|
* handed over, the same way a stored setting nobody declared is not -- so an Add-on never sees a role it
|
|
106
109
|
* does not know, whatever has accumulated across versions.
|
|
107
110
|
*/
|
|
108
|
-
export type
|
|
111
|
+
export type PhisRolesCapabilityV1 = {
|
|
109
112
|
/** Whether the acting user holds this role here. False when nobody is acting. */
|
|
110
113
|
has(role: string): Promise<boolean>;
|
|
111
114
|
/** Every declared role the acting user holds, in declaration order. */
|
|
@@ -124,7 +127,7 @@ export type PhiServerRolesCapabilityV1 = {
|
|
|
124
127
|
* Media Space with its folders, quotas and delivery policies. This is for what an Add-on keeps for
|
|
125
128
|
* itself: a release artifact, a generated preview, an export waiting to be fetched.
|
|
126
129
|
*/
|
|
127
|
-
export type
|
|
130
|
+
export type PhisStorageCapabilityV1 = {
|
|
128
131
|
put(input: {
|
|
129
132
|
key: string;
|
|
130
133
|
body: Uint8Array;
|
|
@@ -147,6 +150,23 @@ export type PhiServerStorageCapabilityV1 = {
|
|
|
147
150
|
remove(key: string): Promise<boolean>;
|
|
148
151
|
/** The Add-on's own keys under a prefix, relative as they were given. */
|
|
149
152
|
list(prefix?: string): Promise<string[]>;
|
|
153
|
+
/**
|
|
154
|
+
* What this Add-on holds on this Site, and what it is allowed to hold.
|
|
155
|
+
*
|
|
156
|
+
* The ceilings are the operator's and are enforced by Core: a `put` past one is refused whether or
|
|
157
|
+
* not anybody read this first. It is here so an Add-on can say *why* -- a refusal an interface can
|
|
158
|
+
* explain before it happens beats one that arrives as an error, and a vendor who can see they are at
|
|
159
|
+
* their limit does not file a bug about it.
|
|
160
|
+
*
|
|
161
|
+
* `null` means no ceiling. Both counts are kept as running totals rather than counted on demand,
|
|
162
|
+
* because counting would mean listing the store -- the unbounded read the ceiling exists to prevent.
|
|
163
|
+
*/
|
|
164
|
+
usage(): Promise<{
|
|
165
|
+
bytes: number;
|
|
166
|
+
objects: number;
|
|
167
|
+
quotaBytes: number | null;
|
|
168
|
+
quotaObjects: number | null;
|
|
169
|
+
}>;
|
|
150
170
|
};
|
|
151
171
|
/**
|
|
152
172
|
* The secrets capability: `@phis/server/secrets:v1`.
|
|
@@ -158,7 +178,7 @@ export type PhiServerStorageCapabilityV1 = {
|
|
|
158
178
|
* There is no writer. An operator sets a secret with `phis secret set`; a package that could write its
|
|
159
179
|
* own would be a package that could plant one.
|
|
160
180
|
*/
|
|
161
|
-
export type
|
|
181
|
+
export type PhisSecretsCapabilityV1 = {
|
|
162
182
|
/** The value, or null when the operator has not configured it. */
|
|
163
183
|
read(name: string): Promise<string | null>;
|
|
164
184
|
};
|
|
@@ -168,7 +188,7 @@ export type PhiServerSecretsCapabilityV1 = {
|
|
|
168
188
|
* Deliberately the same projection one group member gets of another: a display name, a company only
|
|
169
189
|
* where the group discloses it, and never an address. An Add-on renders "listed by" and nothing more.
|
|
170
190
|
*/
|
|
171
|
-
export type
|
|
191
|
+
export type PhisUserProjection = {
|
|
172
192
|
userId: number;
|
|
173
193
|
displayName: string | null;
|
|
174
194
|
companyName: string | null;
|
|
@@ -182,22 +202,87 @@ export type PhiServerUserProjection = {
|
|
|
182
202
|
* its actor could already see in them. It cannot become a directory of a Site's users, because there is
|
|
183
203
|
* no question here that reaches past the actor's own membership.
|
|
184
204
|
*/
|
|
185
|
-
export type
|
|
186
|
-
/**
|
|
205
|
+
export type PhisGroupsCapabilityV1 = {
|
|
206
|
+
/**
|
|
207
|
+
* The groups this Add-on declared, on this Site, with the ids Core gave them.
|
|
208
|
+
*
|
|
209
|
+
* The one answer here that is not about the acting user, and it does not have to be: these are the
|
|
210
|
+
* Add-on's own groups, created because its manifest asked for them. It needs the id to write a
|
|
211
|
+
* group-scoped row, and there is no other way to learn one -- a key is what the manifest states, an
|
|
212
|
+
* id is what the database assigns. An unauthenticated hook may ask, because the answer says nothing
|
|
213
|
+
* about anybody.
|
|
214
|
+
*/
|
|
215
|
+
declared(): Promise<Array<{
|
|
216
|
+
id: number;
|
|
217
|
+
key: string;
|
|
218
|
+
name: string;
|
|
219
|
+
}>>;
|
|
220
|
+
/**
|
|
221
|
+
* The groups the acting user belongs to on this Site, and how far into each one they stand.
|
|
222
|
+
*
|
|
223
|
+
* The level is here because an Add-on that administers its own groups has to be able to ask "is this
|
|
224
|
+
* person a Manager of *this* group" -- Core's own check is per Add-on and cannot answer it. Without
|
|
225
|
+
* it, an Add-on whose policy admits its vendors would let any of them into any of its groups.
|
|
226
|
+
*/
|
|
187
227
|
myGroups(): Promise<Array<{
|
|
188
228
|
id: number;
|
|
189
229
|
key: string;
|
|
190
230
|
name: string;
|
|
231
|
+
level: PhisGroupLevel;
|
|
191
232
|
}>>;
|
|
192
233
|
/** The members of one group the acting user is in, projected as a member sees them. */
|
|
193
|
-
members(groupId: number): Promise<
|
|
234
|
+
members(groupId: number): Promise<PhisUserProjection[]>;
|
|
194
235
|
/**
|
|
195
236
|
* One person, if the acting user shares a group with them.
|
|
196
237
|
*
|
|
197
238
|
* Null otherwise, and null is the whole point: it is what stops a row's owner id from becoming a
|
|
198
239
|
* lookup into everybody.
|
|
199
240
|
*/
|
|
200
|
-
user(userId: number): Promise<
|
|
241
|
+
user(userId: number): Promise<PhisUserProjection | null>;
|
|
242
|
+
/**
|
|
243
|
+
* Makes a group of this Add-on's own, or hands back the one that is already there.
|
|
244
|
+
*
|
|
245
|
+
* Only where the manifest declares a `groupWriter`, and only for an acting user its policy admits.
|
|
246
|
+
* Asking twice is the way to learn an id: the key is the Add-on's to choose and the id is the
|
|
247
|
+
* database's to assign, so the call is an upsert rather than a create that fails the second time.
|
|
248
|
+
*
|
|
249
|
+
* The group carries this Add-on as its provider, which is what every write below checks. Core already
|
|
250
|
+
* filters group claims against the available providers, so one of these stops granting anything the
|
|
251
|
+
* moment the Add-on is gone -- and comes back with it, members included.
|
|
252
|
+
*/
|
|
253
|
+
create(input: {
|
|
254
|
+
key: string;
|
|
255
|
+
name: string;
|
|
256
|
+
}): Promise<{
|
|
257
|
+
id: number;
|
|
258
|
+
key: string;
|
|
259
|
+
name: string;
|
|
260
|
+
}>;
|
|
261
|
+
/**
|
|
262
|
+
* Puts somebody in one of this Add-on's groups, at one level.
|
|
263
|
+
*
|
|
264
|
+
* Refused for a group this Add-on does not own, and for somebody who is not a member of the Site --
|
|
265
|
+
* a row owned by an account that cannot sign in here is a row nobody can reach.
|
|
266
|
+
*/
|
|
267
|
+
addMember(input: {
|
|
268
|
+
groupId: number;
|
|
269
|
+
userId: number;
|
|
270
|
+
level: PhisGroupLevel;
|
|
271
|
+
}): Promise<boolean>;
|
|
272
|
+
/** Takes somebody out again. False where there was nothing to take out. */
|
|
273
|
+
removeMember(input: {
|
|
274
|
+
groupId: number;
|
|
275
|
+
userId: number;
|
|
276
|
+
}): Promise<boolean>;
|
|
277
|
+
/**
|
|
278
|
+
* Retires one of this Add-on's groups.
|
|
279
|
+
*
|
|
280
|
+
* Not deleting: the row and its key stay, because access policies are serialized against the
|
|
281
|
+
* provider-qualified key and releasing it would let a later group of the same name inherit them.
|
|
282
|
+
*/
|
|
283
|
+
retire(input: {
|
|
284
|
+
groupId: number;
|
|
285
|
+
}): Promise<boolean>;
|
|
201
286
|
};
|
|
202
287
|
/**
|
|
203
288
|
* The settings capability: `@phis/server/settings:v1`.
|
|
@@ -212,7 +297,7 @@ export type PhiServerGroupsCapabilityV1 = {
|
|
|
212
297
|
* A setting the operator has not set answers with the declared default, so an Add-on has one shape to
|
|
213
298
|
* handle rather than two, and `null` means genuinely unset with no default to fall back to.
|
|
214
299
|
*/
|
|
215
|
-
export type
|
|
300
|
+
export type PhisSettingsCapabilityV1 = {
|
|
216
301
|
read(name: string): Promise<string | number | boolean | null>;
|
|
217
302
|
all(): Promise<Readonly<Record<string, string | number | boolean | null>>>;
|
|
218
303
|
};
|
|
@@ -224,7 +309,7 @@ export type PhiServerSettingsCapabilityV1 = {
|
|
|
224
309
|
* is reachable only from `phis addon job run` and never from a route -- an Add-on that could start its
|
|
225
310
|
* own job from a request would have built itself a way around the ladder.
|
|
226
311
|
*/
|
|
227
|
-
export type
|
|
312
|
+
export type PhisAddonJobContext = {
|
|
228
313
|
addonId: string;
|
|
229
314
|
jobId: string;
|
|
230
315
|
/** This run, for correlating what the job logs with what the operator started. */
|
|
@@ -234,7 +319,97 @@ export type PhiServerAddonJobContext = {
|
|
|
234
319
|
id: number;
|
|
235
320
|
key: string;
|
|
236
321
|
};
|
|
237
|
-
capabilities:
|
|
322
|
+
capabilities: PhisAddonCapabilities;
|
|
238
323
|
signal: AbortSignal;
|
|
239
324
|
};
|
|
240
|
-
|
|
325
|
+
/**
|
|
326
|
+
* The threads capability: `@phis/server/threads:v1`.
|
|
327
|
+
*
|
|
328
|
+
* Core's internal support threads, bounded by the acting user exactly as `groups:v1` is: an Add-on
|
|
329
|
+
* reads the threads its actor is in and nothing else. A thread is visible to whoever created it, was
|
|
330
|
+
* named a recipient of it, or stands in a team that was -- there is no listing that steps outside that,
|
|
331
|
+
* because a Site's threads are its people talking and an Add-on is not a party to them.
|
|
332
|
+
*
|
|
333
|
+
* **Without an actor only `create` is offered.** A job and a hook run as the Add-on rather than as a
|
|
334
|
+
* person, and a person is what thread visibility is made of: an Add-on with no actor is in no thread,
|
|
335
|
+
* so there is nothing for it to list, read or answer. It may still open one -- naming its recipients as
|
|
336
|
+
* it goes -- which is what an integration reporting an outside event needs. How such an integration
|
|
337
|
+
* later appends to the thread it opened is deliberately left unanswered here: it wants a thread the
|
|
338
|
+
* Add-on owns, and ownership of a thread is a thing to design once something real needs it rather than
|
|
339
|
+
* to guess at now.
|
|
340
|
+
*/
|
|
341
|
+
export type PhisThreadsCapabilityV1 = {
|
|
342
|
+
/** The threads the acting user is in, newest activity first. */
|
|
343
|
+
list(options?: {
|
|
344
|
+
page?: number;
|
|
345
|
+
pageSize?: number;
|
|
346
|
+
unreadOnly?: boolean;
|
|
347
|
+
}): Promise<PhisThreadSummary[]>;
|
|
348
|
+
/** One thread with a window of its messages, or null where the actor is not in it. */
|
|
349
|
+
get(threadId: number, options?: {
|
|
350
|
+
messageLimit?: number;
|
|
351
|
+
beforeMessageId?: number | null;
|
|
352
|
+
}): Promise<PhisThreadDetail | null>;
|
|
353
|
+
/**
|
|
354
|
+
* Opens a thread.
|
|
355
|
+
*
|
|
356
|
+
* The one call an Add-on may make with no actor behind it. Recipients are named as user ids, team
|
|
357
|
+
* ids, or both, and every one is checked against this Site before anything is written.
|
|
358
|
+
*/
|
|
359
|
+
create(input: {
|
|
360
|
+
subject?: string | null;
|
|
361
|
+
message: string;
|
|
362
|
+
messageMarkdown?: string | null;
|
|
363
|
+
recipientUserIds?: number[];
|
|
364
|
+
recipientTeamIds?: number[];
|
|
365
|
+
}): Promise<PhisThreadDetail>;
|
|
366
|
+
/** Adds a message to a thread the acting user is in. */
|
|
367
|
+
reply(input: {
|
|
368
|
+
threadId: number;
|
|
369
|
+
message: string;
|
|
370
|
+
messageMarkdown?: string | null;
|
|
371
|
+
}): Promise<PhisThreadDetail | null>;
|
|
372
|
+
/** Brings more people into a thread the acting user is in. Adds only; it never removes. */
|
|
373
|
+
addRecipients(input: {
|
|
374
|
+
threadId: number;
|
|
375
|
+
recipientUserIds?: number[];
|
|
376
|
+
recipientTeamIds?: number[];
|
|
377
|
+
}): Promise<PhisThreadDetail | null>;
|
|
378
|
+
/**
|
|
379
|
+
* Takes a thread out of the active list, for everyone in it.
|
|
380
|
+
*
|
|
381
|
+
* One row carries the status, so this is the thread being closed rather than one reader hiding it.
|
|
382
|
+
*/
|
|
383
|
+
archive(threadId: number): Promise<boolean>;
|
|
384
|
+
/** Marks the thread read for the acting user, up to its newest message or to one named. */
|
|
385
|
+
markRead(input: {
|
|
386
|
+
threadId: number;
|
|
387
|
+
messageId?: number | null;
|
|
388
|
+
}): Promise<boolean>;
|
|
389
|
+
};
|
|
390
|
+
export type PhisThreadSummary = {
|
|
391
|
+
id: number;
|
|
392
|
+
subject: string | null;
|
|
393
|
+
status: number;
|
|
394
|
+
type: number;
|
|
395
|
+
createdAt: string;
|
|
396
|
+
updatedAt: string;
|
|
397
|
+
latestMessageAt: string | null;
|
|
398
|
+
unread: boolean;
|
|
399
|
+
};
|
|
400
|
+
export type PhisThreadMessage = {
|
|
401
|
+
id: number;
|
|
402
|
+
bodyText: string;
|
|
403
|
+
bodyMarkdown: string | null;
|
|
404
|
+
author: PhisUserProjection | null;
|
|
405
|
+
createdAt: string;
|
|
406
|
+
};
|
|
407
|
+
export type PhisThreadDetail = {
|
|
408
|
+
thread: PhisThreadSummary;
|
|
409
|
+
messages: PhisThreadMessage[];
|
|
410
|
+
/** Whether older messages remain beyond the window `messages` carries. */
|
|
411
|
+
hasMoreMessages: boolean;
|
|
412
|
+
recipientUserIds: number[];
|
|
413
|
+
recipientTeamIds: number[];
|
|
414
|
+
};
|
|
415
|
+
export type PhisAddonJobHandler = (context: PhisAddonJobContext) => Promise<void> | void;
|
package/dist/capabilities.js
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
* to do arithmetic on them, and a value that never leaves Core is one an Add-on cannot come to depend
|
|
19
19
|
* on. Each level does everything the one before it does and one thing more.
|
|
20
20
|
*/
|
|
21
|
-
export const
|
|
21
|
+
export const PHIS_GROUP_LEVELS = ["member", "author", "editor", "manager"];
|
|
22
22
|
/**
|
|
23
23
|
* A Site capability an Add-on may ask after, named symbolically.
|
|
24
24
|
*
|
|
25
25
|
* An Add-on never states a role flag. Which capabilities are askable is then a decision, and Core is
|
|
26
26
|
* free to change what a role means without an installed Add-on holding a stale bitmask.
|
|
27
27
|
*/
|
|
28
|
-
export const
|
|
28
|
+
export const PHIS_SITE_ACCESS = {
|
|
29
29
|
siteAdmin: "site-admin",
|
|
30
30
|
developerTools: "developer-tools",
|
|
31
31
|
structureAuthoring: "structure-authoring",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Module is for, as an operator reads it on the Modules page.
|
|
3
|
+
*
|
|
4
|
+
* An Add-on declares it, per Module and not per package, because a package may ship a shop and a
|
|
5
|
+
* report and neither answer would be true of the other. The site UI groups the Modules page by it, and
|
|
6
|
+
* a Module may read another's category and show it.
|
|
7
|
+
*
|
|
8
|
+
* Here rather than in the site UI package because of the reader that cannot follow it there: a
|
|
9
|
+
* marketplace is a phi-server Add-on, it compiles against this package and nothing else, and it is
|
|
10
|
+
* meant to let a shopper filter on category. That filter is not built yet -- the offering's category is
|
|
11
|
+
* still free text -- but it is the reason this list is a contract and not a UI detail.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately not in `/addon`. That subpath is the frozen ABI a third party compiles against, and this
|
|
14
|
+
* list grows as the product does: an eighth category would lift the package everybody builds against,
|
|
15
|
+
* for a change that concerns none of them. phi-server never asks the membership question either -- a
|
|
16
|
+
* package manifest carries its Modules' categories as plain strings, Core checks the shape, and the
|
|
17
|
+
* stricter question is asked where a registry is actually held. That is the same split `/signals`
|
|
18
|
+
* makes, for the same reason.
|
|
19
|
+
*
|
|
20
|
+
* A closed list rather than free text, because free text produced a bijection: every one of the
|
|
21
|
+
* eighteen built-in Modules answered the field with its own name, so the column repeated the title it
|
|
22
|
+
* stood next to and no Add-on could have guessed a value another Module already used. An Add-on that
|
|
23
|
+
* fits nothing here says `other` rather than inventing a nineteenth.
|
|
24
|
+
*
|
|
25
|
+
* Several values claim no built-in at all, and that is deliberate. A vocabulary that only describes
|
|
26
|
+
* what already exists is right for an internal registry and wrong for a market: the shelf has to be
|
|
27
|
+
* there before the goods, or the first arrival is filed under `other` and nobody ever finds it. So
|
|
28
|
+
* this list is read as a question about what a Module is for, not as an index of what we happen to
|
|
29
|
+
* have built.
|
|
30
|
+
*
|
|
31
|
+
* An Area's base Module is `foundation` whatever the Area does, because it is not a Module the
|
|
32
|
+
* operator chose -- it is the ground the Area stands on, and the row for it is the locked one. So is a
|
|
33
|
+
* Module no site can sensibly do without: locales, logs, revisions, the theme, the user table. Those
|
|
34
|
+
* are switchable and the base Modules are not, but the question the category answers is what a Module
|
|
35
|
+
* is for, not whether it can be turned off -- and the answer for all of them is the same: they are what
|
|
36
|
+
* a site is administered with, not something it was extended by.
|
|
37
|
+
*
|
|
38
|
+
* Distinct from the plugin categories the site UI keeps, which file a Widget or Layout into a drawer of
|
|
39
|
+
* the Builder's insert picker. That is a question about where a block is found, not about what a Module
|
|
40
|
+
* is, and it is answered by the UI alone.
|
|
41
|
+
*/
|
|
42
|
+
export declare const PHI_RUNTIME_MODULE_CATEGORIES: readonly ["foundation", "workspace", "content", "media", "commerce", "identity", "communication", "events", "analytics", "integration", "operations", "other"];
|
|
43
|
+
export type PhiRuntimeModuleCategory = (typeof PHI_RUNTIME_MODULE_CATEGORIES)[number];
|
|
44
|
+
export declare function isPhiRuntimeModuleCategory(value: unknown): value is PhiRuntimeModuleCategory;
|
|
45
|
+
/**
|
|
46
|
+
* The category to file a Module under, for a reader that did not compile against this list.
|
|
47
|
+
*
|
|
48
|
+
* A separately shipped Add-on may name a category added after the site it is installed on was built.
|
|
49
|
+
* Refusing it would cost the operator the whole Modules page over one unknown word, so an unrecognised
|
|
50
|
+
* category reads as `other` -- which is what an Add-on that fits nothing is meant to say anyway.
|
|
51
|
+
*
|
|
52
|
+
* A Module built in the same tree never reaches this: its category is typed, so a typo is a compile
|
|
53
|
+
* error long before anything renders. This is for the values that arrive already compiled.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readPhiRuntimeModuleCategory(value: unknown): PhiRuntimeModuleCategory;
|
|
56
|
+
/** The package.json key a Module package declares itself under. */
|
|
57
|
+
export declare const PHIS_MODULE_PACKAGE_KEY: "phis";
|
|
58
|
+
/** One Module a package contributes, as its package.json states it. */
|
|
59
|
+
export type PhisModulePackageEntry = {
|
|
60
|
+
moduleId: string;
|
|
61
|
+
/**
|
|
62
|
+
* Shape, not membership. A category this build has never heard of is still a well-formed
|
|
63
|
+
* declaration; whether it is a category *we* offer is the question a party with the list asks, and
|
|
64
|
+
* asks at intake, where refusing costs the publisher a message rather than the operator a page.
|
|
65
|
+
*/
|
|
66
|
+
category: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* What a Module package says about itself, without being run.
|
|
70
|
+
*
|
|
71
|
+
* A Module is compiled UI code: React, the Site's bundler, four fixed entry points. Reading a category
|
|
72
|
+
* out of it means executing a stranger's package inside whatever process asked -- the `phis` CLI, or a
|
|
73
|
+
* marketplace taking a submission. Neither should, so the package states it instead, in the one file it
|
|
74
|
+
* already has and that a registry serves without anybody fetching a tarball.
|
|
75
|
+
*
|
|
76
|
+
* Not a second manifest. A package carries one product at one version: the Add-on manifest is what
|
|
77
|
+
* phi-server is handed when an artifact is installed, and this is what a catalogue reads about the half
|
|
78
|
+
* that never reaches phi-server at all. Same package, same version, different readers.
|
|
79
|
+
*
|
|
80
|
+
* Written by the author, like every other field in that file, and their responsibility to keep true to
|
|
81
|
+
* their own Modules. Nothing compares the two automatically: `phis module` can say whether the entries
|
|
82
|
+
* are there and well formed, but whether `events` is the right answer for that Module is a judgement
|
|
83
|
+
* only the author holds. A package that says nothing here, or says it wrongly, is refused by a
|
|
84
|
+
* marketplace rather than listed on a guess.
|
|
85
|
+
*/
|
|
86
|
+
export type PhisModulePackageDeclaration = {
|
|
87
|
+
/**
|
|
88
|
+
* The language the Modules' own titles and descriptions are written in. Null means `en`.
|
|
89
|
+
*
|
|
90
|
+
* Per package rather than per Module: one package carries one product, written by one hand. A
|
|
91
|
+
* catalogue that translates has to know what it is translating from -- told the wrong source it does
|
|
92
|
+
* not fail, it produces fluent nonsense, which is the one kind of wrong nobody sees.
|
|
93
|
+
*/
|
|
94
|
+
sourceLocale: string | null;
|
|
95
|
+
modules: PhisModulePackageEntry[];
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Reads the declaration out of a parsed package.json.
|
|
99
|
+
*
|
|
100
|
+
* Null when the key is absent, which is a legitimate answer: a storage adapter is an Add-on and nothing
|
|
101
|
+
* else, and has no Module half to describe. Present but malformed throws, because that is an author's
|
|
102
|
+
* mistake and silence would file it next to the honest absence.
|
|
103
|
+
*/
|
|
104
|
+
export declare function readPhisModulePackageDeclaration(packageJson: unknown): PhisModulePackageDeclaration | null;
|