@phis/contracts 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +1 -1
- package/README.md +175 -67
- package/dist/access.d.ts +63 -0
- package/dist/access.js +78 -0
- package/dist/addon-index.d.ts +5 -0
- package/dist/addon-index.js +5 -0
- package/dist/addon.d.ts +181 -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 +175 -33
- 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 +52 -6
- package/dist/core.js +55 -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 +371 -0
- package/dist/media-storage.js +84 -0
- package/dist/media.d.ts +272 -0
- package/dist/media.js +421 -0
- package/dist/queries.d.ts +280 -48
- package/dist/queries.js +10 -1
- package/dist/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -0
- 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 +53 -0
- package/dist/site-groups.js +54 -0
- package/dist/support.d.ts +390 -0
- package/dist/support.js +148 -0
- package/dist/threads.d.ts +363 -0
- package/dist/threads.js +146 -0
- package/dist/translation.d.ts +125 -0
- package/dist/translation.js +15 -0
- package/dist/user-state.d.ts +132 -0
- package/dist/user-state.js +92 -0
- package/package.json +64 -4
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
package/dist/cms.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The numbers a CMS row carries, in the one reading both sides must give them.
|
|
3
|
+
*
|
|
4
|
+
* A status, a Region type, a visibility mask and a flag set are written by the site UI, checked and
|
|
5
|
+
* stored by phi-server, and read back by both -- as columns, as fields of a write payload, and as
|
|
6
|
+
* values inside a stored tree. They are the same kind of agreement as the identity below and were
|
|
7
|
+
* kept the same way the arithmetic was: two files, identical line for line, in two repositories.
|
|
8
|
+
*
|
|
9
|
+
* The visibility mask is the one that shows what that costs. Its bits are not dense -- public 1,
|
|
10
|
+
* app 2, admin 8, editor 16, accounting 64, builder 256 -- so a side that renumbered them would not
|
|
11
|
+
* fail anything loudly. It would store a Page in a different Area than the other side reads it from,
|
|
12
|
+
* and every row would look correct where it was written.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PhiCmsPageType: {
|
|
15
|
+
readonly Standard: 0;
|
|
16
|
+
readonly Landing: 1;
|
|
17
|
+
readonly Legal: 2;
|
|
18
|
+
readonly System: 3;
|
|
19
|
+
readonly Redirect: 4;
|
|
20
|
+
};
|
|
21
|
+
export declare const PhiCmsStatus: {
|
|
22
|
+
readonly Draft: 0;
|
|
23
|
+
readonly Published: 1;
|
|
24
|
+
readonly Archived: 2;
|
|
25
|
+
readonly Deleted: 3;
|
|
26
|
+
};
|
|
27
|
+
export declare const PhiCmsRegionStatus: {
|
|
28
|
+
readonly Draft: 0;
|
|
29
|
+
readonly Active: 1;
|
|
30
|
+
readonly Disabled: 2;
|
|
31
|
+
};
|
|
32
|
+
/** Numbered with room between the groups, so a Region can be added beside its neighbours. */
|
|
33
|
+
export declare const PhiCmsRegionType: {
|
|
34
|
+
readonly HeaderTop: 10;
|
|
35
|
+
readonly HeaderMain: 11;
|
|
36
|
+
readonly HeaderBottom: 12;
|
|
37
|
+
readonly SiderLeft: 20;
|
|
38
|
+
readonly SiderRight: 21;
|
|
39
|
+
readonly Hero: 25;
|
|
40
|
+
readonly Content: 30;
|
|
41
|
+
readonly FooterTop: 39;
|
|
42
|
+
readonly Footer: 40;
|
|
43
|
+
readonly FooterBottom: 41;
|
|
44
|
+
readonly Drawer: 50;
|
|
45
|
+
};
|
|
46
|
+
export type PhiCmsRegionTypeValue = (typeof PhiCmsRegionType)[keyof typeof PhiCmsRegionType];
|
|
47
|
+
/**
|
|
48
|
+
* Which Areas a row is visible in, as one value per Area rather than a running number.
|
|
49
|
+
*
|
|
50
|
+
* Bits 2, 5 and 7 are unassigned. That is what makes the numbers worth stating once: 8 reads like the
|
|
51
|
+
* fourth Area and is the third, and phi-server stores it as `area_id` where nothing spells it out.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PhiCmsVisibilityContext: {
|
|
54
|
+
readonly PublicArea: number;
|
|
55
|
+
readonly AppArea: number;
|
|
56
|
+
readonly AdminArea: number;
|
|
57
|
+
readonly EditorArea: number;
|
|
58
|
+
readonly AccountingArea: number;
|
|
59
|
+
readonly BuilderArea: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The flags a CMS row carries, in the one numbering both sides read them from.
|
|
63
|
+
*
|
|
64
|
+
* Not every flag reaches every kind of row -- `Collapsed` is a Region's answer and `MobileOnly` a
|
|
65
|
+
* renderable's -- but they share one space, because a row is stored as a row whatever it holds.
|
|
66
|
+
*
|
|
67
|
+
* `NoIndex` is a Page's, and it is the Site's answer rather than the preset's: a Module states what a
|
|
68
|
+
* Page it ships starts out as, and the Builder's switch is what decides from then on. That is why it
|
|
69
|
+
* lives here, on the record, and not on the route descriptor -- a descriptor is read fresh on every
|
|
70
|
+
* request and would overrule the Operator on every one of them.
|
|
71
|
+
*/
|
|
72
|
+
export declare const PhiCmsFlags: {
|
|
73
|
+
readonly SiteCustom: number;
|
|
74
|
+
readonly Hidden: number;
|
|
75
|
+
readonly MobileOnly: number;
|
|
76
|
+
readonly DesktopOnly: number;
|
|
77
|
+
readonly Collapsed: number;
|
|
78
|
+
readonly NoTranslate: number;
|
|
79
|
+
readonly NoIndex: number;
|
|
80
|
+
};
|
|
81
|
+
export declare const PhiCmsRevisionFlags: {
|
|
82
|
+
readonly Draft: number;
|
|
83
|
+
readonly Published: number;
|
|
84
|
+
readonly Deleted: number;
|
|
85
|
+
};
|
|
86
|
+
export declare const DEFAULT_PHI_CMS_VISIBILITY_MASK: number;
|
|
87
|
+
declare const PHI_CMS_INSTANCE_ID_BRAND: unique symbol;
|
|
88
|
+
export type PhiCmsInstanceId = string & {
|
|
89
|
+
readonly [PHI_CMS_INSTANCE_ID_BRAND]: "PhiCmsInstanceId";
|
|
90
|
+
};
|
|
91
|
+
export type PhiCmsInstanceOrigin = "preset" | "draft";
|
|
92
|
+
export type PhiCmsInstanceDomain = "area" | "page" | "navigation";
|
|
93
|
+
export type PhiCmsInstanceIdDescriptor = {
|
|
94
|
+
version: 1;
|
|
95
|
+
origin: "preset";
|
|
96
|
+
domain: PhiCmsInstanceDomain;
|
|
97
|
+
} | {
|
|
98
|
+
version: 1;
|
|
99
|
+
origin: "draft";
|
|
100
|
+
domain: PhiCmsInstanceDomain;
|
|
101
|
+
draftRevisionId: number;
|
|
102
|
+
sequence: number;
|
|
103
|
+
};
|
|
104
|
+
export type PhiCmsPresetInstanceIdentity = {
|
|
105
|
+
domain: PhiCmsInstanceDomain;
|
|
106
|
+
ownerModuleId: string;
|
|
107
|
+
presetKey: string;
|
|
108
|
+
nodeKey: string;
|
|
109
|
+
};
|
|
110
|
+
export declare function createPhiDraftCmsInstanceId({ domain, draftRevisionId, sequence, }: {
|
|
111
|
+
domain: PhiCmsInstanceDomain;
|
|
112
|
+
draftRevisionId: number;
|
|
113
|
+
sequence: number;
|
|
114
|
+
}): PhiCmsInstanceId;
|
|
115
|
+
export declare function createPhiPresetCmsInstanceId({ domain, ownerModuleId, presetKey, nodeKey, }: PhiCmsPresetInstanceIdentity): PhiCmsInstanceId;
|
|
116
|
+
export declare function readPhiCmsInstanceIdDescriptor(value: unknown): PhiCmsInstanceIdDescriptor | null;
|
|
117
|
+
export declare function isPhiCmsInstanceId(value: unknown): value is PhiCmsInstanceId;
|
|
118
|
+
export declare function readPhiCmsInstanceId(value: unknown): PhiCmsInstanceId | null;
|
|
119
|
+
export declare function comparePhiCmsInstanceIds(left: PhiCmsInstanceId, right: PhiCmsInstanceId): 1 | 0 | -1;
|
|
120
|
+
export {};
|
package/dist/cms.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The numbers a CMS row carries, in the one reading both sides must give them.
|
|
3
|
+
*
|
|
4
|
+
* A status, a Region type, a visibility mask and a flag set are written by the site UI, checked and
|
|
5
|
+
* stored by phi-server, and read back by both -- as columns, as fields of a write payload, and as
|
|
6
|
+
* values inside a stored tree. They are the same kind of agreement as the identity below and were
|
|
7
|
+
* kept the same way the arithmetic was: two files, identical line for line, in two repositories.
|
|
8
|
+
*
|
|
9
|
+
* The visibility mask is the one that shows what that costs. Its bits are not dense -- public 1,
|
|
10
|
+
* app 2, admin 8, editor 16, accounting 64, builder 256 -- so a side that renumbered them would not
|
|
11
|
+
* fail anything loudly. It would store a Page in a different Area than the other side reads it from,
|
|
12
|
+
* and every row would look correct where it was written.
|
|
13
|
+
*/
|
|
14
|
+
export const PhiCmsPageType = {
|
|
15
|
+
Standard: 0,
|
|
16
|
+
Landing: 1,
|
|
17
|
+
Legal: 2,
|
|
18
|
+
System: 3,
|
|
19
|
+
Redirect: 4,
|
|
20
|
+
};
|
|
21
|
+
export const PhiCmsStatus = {
|
|
22
|
+
Draft: 0,
|
|
23
|
+
Published: 1,
|
|
24
|
+
Archived: 2,
|
|
25
|
+
Deleted: 3,
|
|
26
|
+
};
|
|
27
|
+
export const PhiCmsRegionStatus = {
|
|
28
|
+
Draft: 0,
|
|
29
|
+
Active: 1,
|
|
30
|
+
Disabled: 2,
|
|
31
|
+
};
|
|
32
|
+
/** Numbered with room between the groups, so a Region can be added beside its neighbours. */
|
|
33
|
+
export const PhiCmsRegionType = {
|
|
34
|
+
HeaderTop: 10,
|
|
35
|
+
HeaderMain: 11,
|
|
36
|
+
HeaderBottom: 12,
|
|
37
|
+
SiderLeft: 20,
|
|
38
|
+
SiderRight: 21,
|
|
39
|
+
Hero: 25,
|
|
40
|
+
Content: 30,
|
|
41
|
+
FooterTop: 39,
|
|
42
|
+
Footer: 40,
|
|
43
|
+
FooterBottom: 41,
|
|
44
|
+
Drawer: 50,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Which Areas a row is visible in, as one value per Area rather than a running number.
|
|
48
|
+
*
|
|
49
|
+
* Bits 2, 5 and 7 are unassigned. That is what makes the numbers worth stating once: 8 reads like the
|
|
50
|
+
* fourth Area and is the third, and phi-server stores it as `area_id` where nothing spells it out.
|
|
51
|
+
*/
|
|
52
|
+
export const PhiCmsVisibilityContext = {
|
|
53
|
+
PublicArea: 1 << 0,
|
|
54
|
+
AppArea: 1 << 1,
|
|
55
|
+
AdminArea: 1 << 3,
|
|
56
|
+
EditorArea: 1 << 4,
|
|
57
|
+
AccountingArea: 1 << 6,
|
|
58
|
+
BuilderArea: 1 << 8,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The flags a CMS row carries, in the one numbering both sides read them from.
|
|
62
|
+
*
|
|
63
|
+
* Not every flag reaches every kind of row -- `Collapsed` is a Region's answer and `MobileOnly` a
|
|
64
|
+
* renderable's -- but they share one space, because a row is stored as a row whatever it holds.
|
|
65
|
+
*
|
|
66
|
+
* `NoIndex` is a Page's, and it is the Site's answer rather than the preset's: a Module states what a
|
|
67
|
+
* Page it ships starts out as, and the Builder's switch is what decides from then on. That is why it
|
|
68
|
+
* lives here, on the record, and not on the route descriptor -- a descriptor is read fresh on every
|
|
69
|
+
* request and would overrule the Operator on every one of them.
|
|
70
|
+
*/
|
|
71
|
+
export const PhiCmsFlags = {
|
|
72
|
+
SiteCustom: 1 << 0,
|
|
73
|
+
Hidden: 1 << 1,
|
|
74
|
+
MobileOnly: 1 << 2,
|
|
75
|
+
DesktopOnly: 1 << 3,
|
|
76
|
+
Collapsed: 1 << 4,
|
|
77
|
+
NoTranslate: 1 << 5,
|
|
78
|
+
NoIndex: 1 << 6,
|
|
79
|
+
};
|
|
80
|
+
export const PhiCmsRevisionFlags = {
|
|
81
|
+
Draft: 1 << 0,
|
|
82
|
+
Published: 1 << 1,
|
|
83
|
+
Deleted: 1 << 2,
|
|
84
|
+
};
|
|
85
|
+
export const DEFAULT_PHI_CMS_VISIBILITY_MASK = PhiCmsVisibilityContext.PublicArea;
|
|
86
|
+
/**
|
|
87
|
+
* The identity a CMS node carries, in the one encoding both sides must compute alike.
|
|
88
|
+
*
|
|
89
|
+
* A node id is not allocated, it is derived: a Preset node's id is a hash of the Module, the preset
|
|
90
|
+
* and the node key, so the same node has the same id before any row exists and after every
|
|
91
|
+
* republish. The site UI derives it to address blocks and Pages; phi-server derives it to check that
|
|
92
|
+
* stored wiring points at nodes that are really there, and to write Page identities of its own.
|
|
93
|
+
*
|
|
94
|
+
* Which is why this cannot be two implementations agreeing by inspection -- and it was exactly that:
|
|
95
|
+
* `hashPresetIdentity` stood twice, line for line, in `@phis/ui` and in phi-server. The two had to
|
|
96
|
+
* produce identical bytes forever, and a changed constant on one side would not have failed
|
|
97
|
+
* anything loudly. It would have given one process a different Page identity than the other, and
|
|
98
|
+
* Pages would have stopped resolving with nothing to point at.
|
|
99
|
+
*
|
|
100
|
+
* That makes it the exception this package's rule allows for. Everywhere else the rule is shape here,
|
|
101
|
+
* membership with whoever holds the registry; here there is no shape to check, only an arithmetic --
|
|
102
|
+
* and the reason it belongs here is precisely that the arithmetic must be one.
|
|
103
|
+
*/
|
|
104
|
+
const PHI_CMS_INSTANCE_ID_BYTE_LENGTH = 12;
|
|
105
|
+
const PHI_CMS_INSTANCE_ID_ENCODED_LENGTH = 16;
|
|
106
|
+
const PHI_CMS_INSTANCE_ID_VERSION = 1;
|
|
107
|
+
const PHI_CMS_INSTANCE_ID_ORIGIN_PRESET = 1;
|
|
108
|
+
const PHI_CMS_INSTANCE_ID_ORIGIN_DRAFT = 2;
|
|
109
|
+
const PHI_CMS_INSTANCE_ID_VERSION_SHIFT = 4;
|
|
110
|
+
const PHI_CMS_INSTANCE_ID_ORIGIN_MASK = 0x0f;
|
|
111
|
+
const PHI_CMS_INSTANCE_ID_PATTERN = /^[A-Za-z0-9_-]{16}$/;
|
|
112
|
+
const PHI_CMS_BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
113
|
+
const PHI_CMS_MAX_DRAFT_REVISION_ID = 0xffff_ffff_ffff;
|
|
114
|
+
const PHI_CMS_MAX_DRAFT_SEQUENCE = 0xffff_ffff;
|
|
115
|
+
const PHI_CMS_INSTANCE_DOMAIN_CODES = {
|
|
116
|
+
area: 1,
|
|
117
|
+
page: 2,
|
|
118
|
+
navigation: 3,
|
|
119
|
+
};
|
|
120
|
+
const PHI_CMS_INSTANCE_DOMAINS_BY_CODE = new Map(Object.entries(PHI_CMS_INSTANCE_DOMAIN_CODES).map(([domain, code]) => [
|
|
121
|
+
code,
|
|
122
|
+
domain,
|
|
123
|
+
]));
|
|
124
|
+
function encodeBase64Url(bytes) {
|
|
125
|
+
let output = "";
|
|
126
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
127
|
+
const first = bytes[index] ?? 0;
|
|
128
|
+
const second = bytes[index + 1] ?? 0;
|
|
129
|
+
const third = bytes[index + 2] ?? 0;
|
|
130
|
+
output += PHI_CMS_BASE64URL_ALPHABET[first >>> 2];
|
|
131
|
+
output += PHI_CMS_BASE64URL_ALPHABET[((first & 0x03) << 4) | (second >>> 4)];
|
|
132
|
+
output += PHI_CMS_BASE64URL_ALPHABET[((second & 0x0f) << 2) | (third >>> 6)];
|
|
133
|
+
output += PHI_CMS_BASE64URL_ALPHABET[third & 0x3f];
|
|
134
|
+
}
|
|
135
|
+
return output;
|
|
136
|
+
}
|
|
137
|
+
function decodeBase64Url(value) {
|
|
138
|
+
if (!PHI_CMS_INSTANCE_ID_PATTERN.test(value)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const bytes = new Uint8Array(PHI_CMS_INSTANCE_ID_BYTE_LENGTH);
|
|
142
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
143
|
+
const first = PHI_CMS_BASE64URL_ALPHABET.indexOf(value[index]);
|
|
144
|
+
const second = PHI_CMS_BASE64URL_ALPHABET.indexOf(value[index + 1]);
|
|
145
|
+
const third = PHI_CMS_BASE64URL_ALPHABET.indexOf(value[index + 2]);
|
|
146
|
+
const fourth = PHI_CMS_BASE64URL_ALPHABET.indexOf(value[index + 3]);
|
|
147
|
+
if (first < 0 || second < 0 || third < 0 || fourth < 0) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const byteIndex = (index / 4) * 3;
|
|
151
|
+
bytes[byteIndex] = (first << 2) | (second >>> 4);
|
|
152
|
+
bytes[byteIndex + 1] = ((second & 0x0f) << 4) | (third >>> 2);
|
|
153
|
+
bytes[byteIndex + 2] = ((third & 0x03) << 6) | fourth;
|
|
154
|
+
}
|
|
155
|
+
return bytes;
|
|
156
|
+
}
|
|
157
|
+
function createHeader(origin) {
|
|
158
|
+
return (PHI_CMS_INSTANCE_ID_VERSION << PHI_CMS_INSTANCE_ID_VERSION_SHIFT) | origin;
|
|
159
|
+
}
|
|
160
|
+
function readHeader(bytes) {
|
|
161
|
+
const version = bytes[0] >>> PHI_CMS_INSTANCE_ID_VERSION_SHIFT;
|
|
162
|
+
const origin = bytes[0] & PHI_CMS_INSTANCE_ID_ORIGIN_MASK;
|
|
163
|
+
return { version, origin };
|
|
164
|
+
}
|
|
165
|
+
function assertSafeIntegerInRange(value, min, max, label) {
|
|
166
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
167
|
+
throw new Error(`${label} must be a safe integer from ${min} through ${max}.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function writeUnsignedBigEndian(bytes, offset, length, value) {
|
|
171
|
+
let remaining = value;
|
|
172
|
+
for (let index = offset + length - 1; index >= offset; index -= 1) {
|
|
173
|
+
bytes[index] = remaining % 256;
|
|
174
|
+
remaining = Math.floor(remaining / 256);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function readUnsignedBigEndian(bytes, offset, length) {
|
|
178
|
+
let value = 0;
|
|
179
|
+
for (let index = offset; index < offset + length; index += 1) {
|
|
180
|
+
value = (value * 256) + bytes[index];
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function normalizeIdentityPart(value, label) {
|
|
185
|
+
const normalized = value.trim();
|
|
186
|
+
if (!normalized) {
|
|
187
|
+
throw new Error(`${label} must not be empty.`);
|
|
188
|
+
}
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
function hashPresetIdentity(value) {
|
|
192
|
+
const bytes = new TextEncoder().encode(value);
|
|
193
|
+
const state = new Uint32Array([0x811c9dc5, 0x9e3779b9, 0x85ebca6b]);
|
|
194
|
+
const primes = [0x01000193, 0x27d4eb2d, 0x165667b1];
|
|
195
|
+
for (const byte of bytes) {
|
|
196
|
+
for (let index = 0; index < state.length; index += 1) {
|
|
197
|
+
state[index] = Math.imul(state[index] ^ (byte + index * 17), primes[index]) >>> 0;
|
|
198
|
+
state[index] = state[index] ^ (state[index] >>> 13);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const result = new Uint8Array(10);
|
|
202
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
203
|
+
const lane = index % state.length;
|
|
204
|
+
state[lane] = Math.imul(state[lane] ^ (index + bytes.length), primes[lane]) >>> 0;
|
|
205
|
+
result[index] = state[lane] >>> ((index % 4) * 8);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
export function createPhiDraftCmsInstanceId({ domain, draftRevisionId, sequence, }) {
|
|
210
|
+
assertSafeIntegerInRange(draftRevisionId, 1, PHI_CMS_MAX_DRAFT_REVISION_ID, "Draft revision id");
|
|
211
|
+
assertSafeIntegerInRange(sequence, 1, PHI_CMS_MAX_DRAFT_SEQUENCE, "Draft node sequence");
|
|
212
|
+
const bytes = new Uint8Array(PHI_CMS_INSTANCE_ID_BYTE_LENGTH);
|
|
213
|
+
bytes[0] = createHeader(PHI_CMS_INSTANCE_ID_ORIGIN_DRAFT);
|
|
214
|
+
bytes[1] = PHI_CMS_INSTANCE_DOMAIN_CODES[domain];
|
|
215
|
+
writeUnsignedBigEndian(bytes, 2, 6, draftRevisionId);
|
|
216
|
+
writeUnsignedBigEndian(bytes, 8, 4, sequence);
|
|
217
|
+
return encodeBase64Url(bytes);
|
|
218
|
+
}
|
|
219
|
+
export function createPhiPresetCmsInstanceId({ domain, ownerModuleId, presetKey, nodeKey, }) {
|
|
220
|
+
const identity = [
|
|
221
|
+
`v${PHI_CMS_INSTANCE_ID_VERSION}`,
|
|
222
|
+
domain,
|
|
223
|
+
normalizeIdentityPart(ownerModuleId, "Preset owner module id"),
|
|
224
|
+
normalizeIdentityPart(presetKey, "Preset key"),
|
|
225
|
+
normalizeIdentityPart(nodeKey, "Preset node key"),
|
|
226
|
+
].join("\u001f");
|
|
227
|
+
const bytes = new Uint8Array(PHI_CMS_INSTANCE_ID_BYTE_LENGTH);
|
|
228
|
+
bytes[0] = createHeader(PHI_CMS_INSTANCE_ID_ORIGIN_PRESET);
|
|
229
|
+
bytes[1] = PHI_CMS_INSTANCE_DOMAIN_CODES[domain];
|
|
230
|
+
bytes.set(hashPresetIdentity(identity), 2);
|
|
231
|
+
return encodeBase64Url(bytes);
|
|
232
|
+
}
|
|
233
|
+
export function readPhiCmsInstanceIdDescriptor(value) {
|
|
234
|
+
if (typeof value !== "string" || value.length !== PHI_CMS_INSTANCE_ID_ENCODED_LENGTH) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const bytes = decodeBase64Url(value);
|
|
238
|
+
if (!bytes) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const header = readHeader(bytes);
|
|
242
|
+
if (header.version !== PHI_CMS_INSTANCE_ID_VERSION) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const domain = PHI_CMS_INSTANCE_DOMAINS_BY_CODE.get(bytes[1]);
|
|
246
|
+
if (!domain) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
if (header.origin === PHI_CMS_INSTANCE_ID_ORIGIN_PRESET) {
|
|
250
|
+
return { version: 1, origin: "preset", domain };
|
|
251
|
+
}
|
|
252
|
+
if (header.origin !== PHI_CMS_INSTANCE_ID_ORIGIN_DRAFT) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const draftRevisionId = readUnsignedBigEndian(bytes, 2, 6);
|
|
256
|
+
const sequence = readUnsignedBigEndian(bytes, 8, 4);
|
|
257
|
+
if (draftRevisionId < 1 || sequence < 1) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return { version: 1, origin: "draft", domain, draftRevisionId, sequence };
|
|
261
|
+
}
|
|
262
|
+
export function isPhiCmsInstanceId(value) {
|
|
263
|
+
return readPhiCmsInstanceIdDescriptor(value) !== null;
|
|
264
|
+
}
|
|
265
|
+
export function readPhiCmsInstanceId(value) {
|
|
266
|
+
return isPhiCmsInstanceId(value) ? value : null;
|
|
267
|
+
}
|
|
268
|
+
export function comparePhiCmsInstanceIds(left, right) {
|
|
269
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
270
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* without hashing anything -- Core recomputes them and checks, which is where that belongs.
|
|
15
15
|
*/
|
|
16
16
|
/** The Core capabilities an Add-on may require. */
|
|
17
|
-
export declare const
|
|
17
|
+
export declare const PHIS_CORE_CAPABILITIES: {
|
|
18
18
|
readonly authentication: "@phis/server/authentication:v1";
|
|
19
19
|
/**
|
|
20
20
|
* "May this actor", in all three of its sources: does the row belong to them, what level do they
|
|
@@ -31,6 +31,14 @@ export declare const PHI_SERVER_CORE_CAPABILITIES: {
|
|
|
31
31
|
/** Group facts -- which groups exist, who is in them. What follows from them is `authorization`. */
|
|
32
32
|
readonly groups: "@phis/server/groups:v1";
|
|
33
33
|
readonly storage: "@phis/server/storage:v1";
|
|
34
|
+
/**
|
|
35
|
+
* The files a row carries, in the slots its table declared.
|
|
36
|
+
*
|
|
37
|
+
* Separate from `storage` because they answer different questions. `storage` hands out a key space an
|
|
38
|
+
* Add-on manages itself; this hands out places Core knows about, which is what makes the file of a
|
|
39
|
+
* deleted row collectable rather than merely gone from view.
|
|
40
|
+
*/
|
|
41
|
+
readonly assets: "@phis/server/assets:v1";
|
|
34
42
|
/** Reading the secrets an operator set for this Add-on, and no others. */
|
|
35
43
|
readonly secrets: "@phis/server/secrets:v1";
|
|
36
44
|
/** Reading what an operator configured for this Add-on on this Site. */
|
|
@@ -38,7 +46,29 @@ export declare const PHI_SERVER_CORE_CAPABILITIES: {
|
|
|
38
46
|
/** Whether the acting user holds one of this Add-on's own declared roles. */
|
|
39
47
|
readonly roles: "@phis/server/roles:v1";
|
|
40
48
|
};
|
|
41
|
-
export type
|
|
49
|
+
export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof PHIS_CORE_CAPABILITIES];
|
|
50
|
+
/**
|
|
51
|
+
* What Core tells an Add-on about after the fact.
|
|
52
|
+
*
|
|
53
|
+
* An integration that mirrors a conversation cannot wait to be asked: somebody writes a reply here and
|
|
54
|
+
* it has to appear over there, and no request the Add-on serves is involved. Polling would be the
|
|
55
|
+
* alternative, and polling for "did anybody write anything" is the Site-wide read every capability here
|
|
56
|
+
* is shaped to prevent.
|
|
57
|
+
*
|
|
58
|
+
* An event is delivered only to an Add-on linked to the thread it happened in, and a message this same
|
|
59
|
+
* Add-on imported is never announced back to it -- which is what stops a mirror from echoing.
|
|
60
|
+
*
|
|
61
|
+
* The list is short on purpose and grows when an Add-on needs an entry, the way the capability catalogue
|
|
62
|
+
* does. Every event a Site could emit, declared in advance, would be a contract to get wrong.
|
|
63
|
+
*/
|
|
64
|
+
export declare const PHIS_ADDON_EVENTS: {
|
|
65
|
+
readonly threadMessageCreated: "thread.message.created";
|
|
66
|
+
readonly threadMessageUpdated: "thread.message.updated";
|
|
67
|
+
readonly threadStatusChanged: "thread.status.changed";
|
|
68
|
+
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
69
|
+
readonly threadLinkRemoved: "thread.link.removed";
|
|
70
|
+
};
|
|
71
|
+
export type PhisAddonEventId = (typeof PHIS_ADDON_EVENTS)[keyof typeof PHIS_ADDON_EVENTS];
|
|
42
72
|
/**
|
|
43
73
|
* The Core-owned service kinds an Add-on may implement.
|
|
44
74
|
*
|
|
@@ -46,11 +76,20 @@ export type PhiServerCoreCapabilityId = (typeof PHI_SERVER_CORE_CAPABILITIES)[ke
|
|
|
46
76
|
* is something Core already owns and an Add-on supplies an implementation of -- S3 for Media Storage,
|
|
47
77
|
* LDAP for a Directory. Core keeps selecting the configured implementation itself.
|
|
48
78
|
*/
|
|
49
|
-
export declare const
|
|
79
|
+
export declare const PHIS_SERVICE_KINDS: {
|
|
50
80
|
readonly mediaStorage: "@phis/server/service/media-storage";
|
|
51
81
|
readonly directory: "@phis/server/service/directory";
|
|
82
|
+
/**
|
|
83
|
+
* Turning text in one language into text in another.
|
|
84
|
+
*
|
|
85
|
+
* A kind rather than a capability for the same reason as the two above: Core owns the question and
|
|
86
|
+
* keeps asking it, and what an implementation talks to -- a vendor, a model on the machine, a
|
|
87
|
+
* catalogue it maintains itself -- is its own business. Several may be installed at once; the
|
|
88
|
+
* operator selects which one an installation uses.
|
|
89
|
+
*/
|
|
90
|
+
readonly translation: "@phis/server/service/translation";
|
|
52
91
|
};
|
|
53
|
-
export type
|
|
92
|
+
export type PhisServiceKind = (typeof PHIS_SERVICE_KINDS)[keyof typeof PHIS_SERVICE_KINDS];
|
|
54
93
|
/**
|
|
55
94
|
* The digest of the interface each service kind currently has.
|
|
56
95
|
*
|
|
@@ -61,6 +100,13 @@ export type PhiServerServiceKind = (typeof PHI_SERVER_SERVICE_KINDS)[keyof typeo
|
|
|
61
100
|
*
|
|
62
101
|
* The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
|
|
63
102
|
* documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
|
|
103
|
+
*
|
|
104
|
+
* **Before the v1 release every version here stays at 1, whatever changes shape.** There is no released
|
|
105
|
+
* Add-on to protect and nothing in the field built against an older interface, so raising a version buys
|
|
106
|
+
* no compatibility and costs the installation: the recorded manifest of every installed Add-on stops
|
|
107
|
+
* matching, `buildPhisCapabilitySnapshot` refuses the desired state, and every request that resolves a
|
|
108
|
+
* viewer answers 500 until each Add-on has been rebundled and upgraded. An interface that changes before
|
|
109
|
+
* v1 is simply the interface, and both sides are rebuilt from one tree.
|
|
64
110
|
*/
|
|
65
|
-
export declare const
|
|
66
|
-
export declare const
|
|
111
|
+
export declare const PHIS_SERVICE_INTERFACE_VERSIONS: Readonly<Record<PhisServiceKind, number>>;
|
|
112
|
+
export declare const PHIS_SERVICE_INTERFACE_DIGESTS: Readonly<Record<PhisServiceKind, string>>;
|
package/dist/core.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* without hashing anything -- Core recomputes them and checks, which is where that belongs.
|
|
15
15
|
*/
|
|
16
16
|
/** The Core capabilities an Add-on may require. */
|
|
17
|
-
export const
|
|
17
|
+
export const PHIS_CORE_CAPABILITIES = {
|
|
18
18
|
authentication: "@phis/server/authentication:v1",
|
|
19
19
|
/**
|
|
20
20
|
* "May this actor", in all three of its sources: does the row belong to them, what level do they
|
|
@@ -31,6 +31,14 @@ export const PHI_SERVER_CORE_CAPABILITIES = {
|
|
|
31
31
|
/** Group facts -- which groups exist, who is in them. What follows from them is `authorization`. */
|
|
32
32
|
groups: "@phis/server/groups:v1",
|
|
33
33
|
storage: "@phis/server/storage:v1",
|
|
34
|
+
/**
|
|
35
|
+
* The files a row carries, in the slots its table declared.
|
|
36
|
+
*
|
|
37
|
+
* Separate from `storage` because they answer different questions. `storage` hands out a key space an
|
|
38
|
+
* Add-on manages itself; this hands out places Core knows about, which is what makes the file of a
|
|
39
|
+
* deleted row collectable rather than merely gone from view.
|
|
40
|
+
*/
|
|
41
|
+
assets: "@phis/server/assets:v1",
|
|
34
42
|
/** Reading the secrets an operator set for this Add-on, and no others. */
|
|
35
43
|
secrets: "@phis/server/secrets:v1",
|
|
36
44
|
/** Reading what an operator configured for this Add-on on this Site. */
|
|
@@ -38,6 +46,27 @@ export const PHI_SERVER_CORE_CAPABILITIES = {
|
|
|
38
46
|
/** Whether the acting user holds one of this Add-on's own declared roles. */
|
|
39
47
|
roles: "@phis/server/roles:v1",
|
|
40
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* What Core tells an Add-on about after the fact.
|
|
51
|
+
*
|
|
52
|
+
* An integration that mirrors a conversation cannot wait to be asked: somebody writes a reply here and
|
|
53
|
+
* it has to appear over there, and no request the Add-on serves is involved. Polling would be the
|
|
54
|
+
* alternative, and polling for "did anybody write anything" is the Site-wide read every capability here
|
|
55
|
+
* is shaped to prevent.
|
|
56
|
+
*
|
|
57
|
+
* An event is delivered only to an Add-on linked to the thread it happened in, and a message this same
|
|
58
|
+
* Add-on imported is never announced back to it -- which is what stops a mirror from echoing.
|
|
59
|
+
*
|
|
60
|
+
* The list is short on purpose and grows when an Add-on needs an entry, the way the capability catalogue
|
|
61
|
+
* does. Every event a Site could emit, declared in advance, would be a contract to get wrong.
|
|
62
|
+
*/
|
|
63
|
+
export const PHIS_ADDON_EVENTS = {
|
|
64
|
+
threadMessageCreated: "thread.message.created",
|
|
65
|
+
threadMessageUpdated: "thread.message.updated",
|
|
66
|
+
threadStatusChanged: "thread.status.changed",
|
|
67
|
+
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
68
|
+
threadLinkRemoved: "thread.link.removed",
|
|
69
|
+
};
|
|
41
70
|
/**
|
|
42
71
|
* The Core-owned service kinds an Add-on may implement.
|
|
43
72
|
*
|
|
@@ -45,9 +74,18 @@ export const PHI_SERVER_CORE_CAPABILITIES = {
|
|
|
45
74
|
* is something Core already owns and an Add-on supplies an implementation of -- S3 for Media Storage,
|
|
46
75
|
* LDAP for a Directory. Core keeps selecting the configured implementation itself.
|
|
47
76
|
*/
|
|
48
|
-
export const
|
|
77
|
+
export const PHIS_SERVICE_KINDS = {
|
|
49
78
|
mediaStorage: "@phis/server/service/media-storage",
|
|
50
79
|
directory: "@phis/server/service/directory",
|
|
80
|
+
/**
|
|
81
|
+
* Turning text in one language into text in another.
|
|
82
|
+
*
|
|
83
|
+
* A kind rather than a capability for the same reason as the two above: Core owns the question and
|
|
84
|
+
* keeps asking it, and what an implementation talks to -- a vendor, a model on the machine, a
|
|
85
|
+
* catalogue it maintains itself -- is its own business. Several may be installed at once; the
|
|
86
|
+
* operator selects which one an installation uses.
|
|
87
|
+
*/
|
|
88
|
+
translation: "@phis/server/service/translation",
|
|
51
89
|
};
|
|
52
90
|
/**
|
|
53
91
|
* The digest of the interface each service kind currently has.
|
|
@@ -59,12 +97,21 @@ export const PHI_SERVER_SERVICE_KINDS = {
|
|
|
59
97
|
*
|
|
60
98
|
* The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
|
|
61
99
|
* documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
|
|
100
|
+
*
|
|
101
|
+
* **Before the v1 release every version here stays at 1, whatever changes shape.** There is no released
|
|
102
|
+
* Add-on to protect and nothing in the field built against an older interface, so raising a version buys
|
|
103
|
+
* no compatibility and costs the installation: the recorded manifest of every installed Add-on stops
|
|
104
|
+
* matching, `buildPhisCapabilitySnapshot` refuses the desired state, and every request that resolves a
|
|
105
|
+
* viewer answers 500 until each Add-on has been rebundled and upgraded. An interface that changes before
|
|
106
|
+
* v1 is simply the interface, and both sides are rebuilt from one tree.
|
|
62
107
|
*/
|
|
63
|
-
export const
|
|
64
|
-
[
|
|
65
|
-
[
|
|
108
|
+
export const PHIS_SERVICE_INTERFACE_VERSIONS = {
|
|
109
|
+
[PHIS_SERVICE_KINDS.mediaStorage]: 1,
|
|
110
|
+
[PHIS_SERVICE_KINDS.directory]: 1,
|
|
111
|
+
[PHIS_SERVICE_KINDS.translation]: 1,
|
|
66
112
|
};
|
|
67
|
-
export const
|
|
68
|
-
[
|
|
69
|
-
[
|
|
113
|
+
export const PHIS_SERVICE_INTERFACE_DIGESTS = {
|
|
114
|
+
[PHIS_SERVICE_KINDS.mediaStorage]: "45a871522511f7d77841ca8dd0113b423b3cfd4cb921f143cd1d3d116cab6952",
|
|
115
|
+
[PHIS_SERVICE_KINDS.directory]: "2cac7397c7beb1b8e71fc67f5cae3be89182dddf096895570a15deafa1adbfc9",
|
|
116
|
+
[PHIS_SERVICE_KINDS.translation]: "9bae2e7b92ded5cdde9321939a3bb8cd589f6e1ef8bca7bc5f65608313b3207d",
|
|
70
117
|
};
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request headers phis and the site UI must spell the same way.
|
|
3
|
+
*
|
|
4
|
+
* These five names are a wire protocol between processes that deploy independently. phis reads them in
|
|
5
|
+
* its guards and route handlers; the site UI writes them in its proxies, gateways and browser widgets.
|
|
6
|
+
* A disagreement is not a type error and not a failing test -- it is a 403 from `guardApiV1` or a 400
|
|
7
|
+
* from `requireSiteFromExplicitKeyRequest`, at runtime, in whichever direction the mismatch happens to
|
|
8
|
+
* point.
|
|
9
|
+
*
|
|
10
|
+
* They were string literals in both repositories before this file existed, eighty-odd of them, and the
|
|
11
|
+
* one place that had bothered to name a constant named it in only one of the two. Renaming the prefix
|
|
12
|
+
* from `x-phi-` to `x-phis-` is what made that visible; the point of collecting them here is that the
|
|
13
|
+
* next such change is one edit rather than a search.
|
|
14
|
+
*/
|
|
15
|
+
/** Carries the public or internal API token on every `/api/v1/*` request. */
|
|
16
|
+
export declare const PHIS_TOKEN_HEADER: "x-phis-token";
|
|
17
|
+
/**
|
|
18
|
+
* Names the Site a request is scoped to, and is the only thing that does.
|
|
19
|
+
*
|
|
20
|
+
* There is deliberately no host, origin or referer fallback: those are caller-controlled, so a request
|
|
21
|
+
* without this header could otherwise claim any Site whose hostname it spoofed.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PHIS_SITE_KEY_HEADER: "x-phis-site-key";
|
|
24
|
+
/** Names the CMS area a media request acts in, which decides what the actor may reach. */
|
|
25
|
+
export declare const PHIS_AREA_HEADER: "x-phis-area";
|
|
26
|
+
/**
|
|
27
|
+
* Hand the original request path and query from the site proxy to the server helpers behind it.
|
|
28
|
+
*
|
|
29
|
+
* Both are set and read inside the site runtime and never reach phis; they exist because a Server
|
|
30
|
+
* Component cannot otherwise see the URL the proxy rewrote.
|
|
31
|
+
*/
|
|
32
|
+
export declare const PHIS_REQUEST_PATH_HEADER: "x-phis-request-path";
|
|
33
|
+
export declare const PHIS_REQUEST_SEARCH_HEADER: "x-phis-request-search";
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request headers phis and the site UI must spell the same way.
|
|
3
|
+
*
|
|
4
|
+
* These five names are a wire protocol between processes that deploy independently. phis reads them in
|
|
5
|
+
* its guards and route handlers; the site UI writes them in its proxies, gateways and browser widgets.
|
|
6
|
+
* A disagreement is not a type error and not a failing test -- it is a 403 from `guardApiV1` or a 400
|
|
7
|
+
* from `requireSiteFromExplicitKeyRequest`, at runtime, in whichever direction the mismatch happens to
|
|
8
|
+
* point.
|
|
9
|
+
*
|
|
10
|
+
* They were string literals in both repositories before this file existed, eighty-odd of them, and the
|
|
11
|
+
* one place that had bothered to name a constant named it in only one of the two. Renaming the prefix
|
|
12
|
+
* from `x-phi-` to `x-phis-` is what made that visible; the point of collecting them here is that the
|
|
13
|
+
* next such change is one edit rather than a search.
|
|
14
|
+
*/
|
|
15
|
+
/** Carries the public or internal API token on every `/api/v1/*` request. */
|
|
16
|
+
export const PHIS_TOKEN_HEADER = "x-phis-token";
|
|
17
|
+
/**
|
|
18
|
+
* Names the Site a request is scoped to, and is the only thing that does.
|
|
19
|
+
*
|
|
20
|
+
* There is deliberately no host, origin or referer fallback: those are caller-controlled, so a request
|
|
21
|
+
* without this header could otherwise claim any Site whose hostname it spoofed.
|
|
22
|
+
*/
|
|
23
|
+
export const PHIS_SITE_KEY_HEADER = "x-phis-site-key";
|
|
24
|
+
/** Names the CMS area a media request acts in, which decides what the actor may reach. */
|
|
25
|
+
export const PHIS_AREA_HEADER = "x-phis-area";
|
|
26
|
+
/**
|
|
27
|
+
* Hand the original request path and query from the site proxy to the server helpers behind it.
|
|
28
|
+
*
|
|
29
|
+
* Both are set and read inside the site runtime and never reach phis; they exist because a Server
|
|
30
|
+
* Component cannot otherwise see the URL the proxy rewrote.
|
|
31
|
+
*/
|
|
32
|
+
export const PHIS_REQUEST_PATH_HEADER = "x-phis-request-path";
|
|
33
|
+
export const PHIS_REQUEST_SEARCH_HEADER = "x-phis-request-search";
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language this software is written in.
|
|
3
|
+
*
|
|
4
|
+
* Not a default and not a preference: it is the locale every label, every email template and every
|
|
5
|
+
* global message is authored in, and therefore the one a translation starts from. `TRANSLATIONS.md`
|
|
6
|
+
* fixes it -- global messages in `phis.tr_msg` always use English as their canonical source, while a
|
|
7
|
+
* Site's own messages use the immutable `sourceLocale` chosen when the Site was created.
|
|
8
|
+
*
|
|
9
|
+
* It was called DEFAULT_LOCALE on both sides, which is the name of a different thing: a Site's default
|
|
10
|
+
* locale is configuration, stored per Site in `sites.default_locale`, and an installation may set it to
|
|
11
|
+
* anything. Under one name the two were indistinguishable at the call site, and a reader could not tell
|
|
12
|
+
* whether a line stated a fact about the source text or made an assumption about somebody's Site.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PHI_CANONICAL_SOURCE_LOCALE = "en";
|