@phis/contracts 0.3.1 → 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.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * What a person decided, kept per Site under a key a Module owns.
3
+ *
4
+ * Two ends have to agree about it and neither can defer to the other. phis stores it on the membership
5
+ * row and is the only party that validates a write; `@phis/ui` declares the keys in a Module descriptor,
6
+ * reads them while rendering, and has to know before it renders what shape it will get back. A second
7
+ * spelling of the shapes in either end is the one that drifts, which is why they are here and not there.
8
+ *
9
+ * What this is not: a session, a cache, or a Module's own records. It holds decisions -- a dismissed
10
+ * card, a read marker, where somebody is in a guided sequence -- and losing one must be an annoyance
11
+ * rather than a support case. Core's own account facts stay bit flags on `user_accounts.flags` and
12
+ * `user_site_memberships.user_site_flags`, because those are finite and Core allocates them; the reason
13
+ * this exists at all is that bits cannot be handed to an unknown number of Modules.
14
+ */
15
+ /**
16
+ * The shapes a value may have, closed on purpose.
17
+ *
18
+ * An open value invites the list that grows forever. Every shape here answers a question that a
19
+ * general-purpose JSON blob would also answer, and answers it in a form whose cost is known in advance.
20
+ */
21
+ export declare const PHIS_USER_STATE_SHAPES: readonly ["flag", "marker", "value", "set"];
22
+ export type PhisUserStateShape = (typeof PHIS_USER_STATE_SHAPES)[number];
23
+ export declare function isPhisUserStateShape(value: unknown): value is PhisUserStateShape;
24
+ /**
25
+ * `<module id>/<key>` -- the owning Module's own id in front, as a Widget type already carries it
26
+ * (`@phis/example/modules/site/widgets/note`).
27
+ *
28
+ * The prefix is not decoration, it is the whole authorization. A Site's active Module ids are already
29
+ * published, so phis answers "may this be written" by asking whether the key begins with the id of a
30
+ * Module this Site has switched on -- and needs to know nothing about the key itself. Which is also why
31
+ * the id and not the package name: a package may carry two Modules and a Site may run one of them.
32
+ */
33
+ export type PhisUserStateKey = `${string}/${string}`;
34
+ export declare const PHIS_USER_STATE_MAX_KEY_LENGTH = 128;
35
+ export declare function isPhisUserStateKey(value: unknown): value is PhisUserStateKey;
36
+ /** One bit under one key: a dismissed card, a hint somebody turned off. */
37
+ export type PhisUserStateFlag = boolean;
38
+ /**
39
+ * One value that only ever moves forward: "read up to here".
40
+ *
41
+ * A marker is what keeps a feed affordable, because one number answers a question a set answers only by
42
+ * growing. It is also the one shape where "the last write wins" is the wrong rule -- a second tab
43
+ * answering late would move the mark backwards and unread what somebody read -- so a write that does not
44
+ * increase it is accepted and changes nothing, rather than being refused or applied.
45
+ */
46
+ export type PhisUserStateMarker = number;
47
+ /** One JSON document under a declared schema: a state machine's position, and its like. */
48
+ export type PhisUserStateValue = Record<string, unknown>;
49
+ /**
50
+ * A bounded collection, in insertion order, for what a marker genuinely cannot express.
51
+ *
52
+ * The limit is declared rather than assumed, and reaching it drops the oldest entry instead of refusing
53
+ * the write: a set that refuses is a set that silently stops recording, which is the failure that is
54
+ * noticed last.
55
+ */
56
+ export type PhisUserStateSet = readonly string[];
57
+ export type PhisUserStateStoredValue = PhisUserStateFlag | PhisUserStateMarker | PhisUserStateValue | PhisUserStateSet;
58
+ export declare const PHIS_USER_STATE_MAX_SET_LIMIT = 256;
59
+ /**
60
+ * How much one Module's namespace may weigh, serialized.
61
+ *
62
+ * The membership row travels with every request that resolves a session, so this is paid for constantly
63
+ * and by everybody. It is a ceiling on a Module's whole namespace rather than on one key, because
64
+ * splitting a large value across ten keys is the obvious way around a per-key limit.
65
+ */
66
+ export declare const PHIS_USER_STATE_MAX_SERIALIZED_BYTES = 4096;
67
+ /**
68
+ * What a Module says it keeps, and what a write is checked against.
69
+ *
70
+ * This is contract discipline rather than authorization, and it does not travel to phis. A Module
71
+ * declaring its keys lets the browser refuse a malformed write before sending one, and makes what a
72
+ * Module stores readable from its own source. What phis checks is the prefix -- an active Module's id --
73
+ * and the value against the shape the write itself names, because a Module that calls one key a flag
74
+ * today and a marker tomorrow damages only its own data.
75
+ *
76
+ * Both ends check a write with `readPhisUserStateWrite`, which asks for less than this: the browser hands
77
+ * it the declaration, phis the shape the write names, clamping `limit` to
78
+ * `PHIS_USER_STATE_MAX_SET_LIMIT` because that one arrives from outside.
79
+ *
80
+ * `default` is deliberately absent -- an unwritten key is absent, and what absence means belongs to the
81
+ * Module reading it, which is not the same question as what value to start from.
82
+ */
83
+ export type PhisDeclarableUserStateKey = {
84
+ readonly key: PhisUserStateKey;
85
+ readonly shape: "flag";
86
+ } | {
87
+ readonly key: PhisUserStateKey;
88
+ readonly shape: "marker";
89
+ } | {
90
+ readonly key: PhisUserStateKey;
91
+ readonly shape: "value";
92
+ readonly valueSchema: import("./signals.js").PhiSignalValueSchema;
93
+ } | {
94
+ readonly key: PhisUserStateKey;
95
+ readonly shape: "set";
96
+ readonly limit: number;
97
+ };
98
+ export declare function isPhisDeclarableUserStateKey(value: unknown): value is PhisDeclarableUserStateKey;
99
+ /**
100
+ * What checking a write actually needs: the shape, and a set's limit.
101
+ *
102
+ * Deliberately less than a declaration. The key does not take part in deciding whether a value is
103
+ * admissible, and phis has no declaration to hand -- requiring one would only make it invent the fields
104
+ * it does not use. A `PhisDeclarableUserStateKey` satisfies this, so the browser passes its declaration
105
+ * straight through.
106
+ */
107
+ export type PhisUserStateWriteShape = {
108
+ readonly shape: "flag";
109
+ } | {
110
+ readonly shape: "marker";
111
+ } | {
112
+ readonly shape: "value";
113
+ } | {
114
+ readonly shape: "set";
115
+ readonly limit: number;
116
+ };
117
+ /**
118
+ * The value a write may store, or `null` when the shape does not admit it.
119
+ *
120
+ * Both ends call this -- phis before it writes, `@phis/ui` before it sends -- but they are not equals.
121
+ * The two packages ship separately and may be different versions of this file, so the browser's check is
122
+ * a saved round trip and never a substitute: phis checks again from its own copy and its answer is the
123
+ * one that counts. A shape a server does not know is refused there rather than stored and read back as
124
+ * something else, which is what keeps a newer Module against an older server a clear error instead of a
125
+ * quiet corruption.
126
+ *
127
+ * `stored` is what the key already holds, needed by the two shapes whose answer depends on it -- a
128
+ * marker that may not move backwards, and a set that evicts once it is full.
129
+ */
130
+ export declare function readPhisUserStateWrite(descriptor: PhisUserStateWriteShape, value: unknown, stored?: PhisUserStateStoredValue | null): {
131
+ value: PhisUserStateStoredValue;
132
+ } | null;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What a person decided, kept per Site under a key a Module owns.
3
+ *
4
+ * Two ends have to agree about it and neither can defer to the other. phis stores it on the membership
5
+ * row and is the only party that validates a write; `@phis/ui` declares the keys in a Module descriptor,
6
+ * reads them while rendering, and has to know before it renders what shape it will get back. A second
7
+ * spelling of the shapes in either end is the one that drifts, which is why they are here and not there.
8
+ *
9
+ * What this is not: a session, a cache, or a Module's own records. It holds decisions -- a dismissed
10
+ * card, a read marker, where somebody is in a guided sequence -- and losing one must be an annoyance
11
+ * rather than a support case. Core's own account facts stay bit flags on `user_accounts.flags` and
12
+ * `user_site_memberships.user_site_flags`, because those are finite and Core allocates them; the reason
13
+ * this exists at all is that bits cannot be handed to an unknown number of Modules.
14
+ */
15
+ /**
16
+ * The shapes a value may have, closed on purpose.
17
+ *
18
+ * An open value invites the list that grows forever. Every shape here answers a question that a
19
+ * general-purpose JSON blob would also answer, and answers it in a form whose cost is known in advance.
20
+ */
21
+ export const PHIS_USER_STATE_SHAPES = ["flag", "marker", "value", "set"];
22
+ export function isPhisUserStateShape(value) {
23
+ return typeof value === "string" && PHIS_USER_STATE_SHAPES.includes(value);
24
+ }
25
+ export const PHIS_USER_STATE_MAX_KEY_LENGTH = 128;
26
+ const PHIS_USER_STATE_KEY_PATTERN = /^(@[a-z0-9-][a-z0-9._-]*\/)?[a-z0-9-][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)+$/;
27
+ export function isPhisUserStateKey(value) {
28
+ return typeof value === "string" &&
29
+ value.length <= PHIS_USER_STATE_MAX_KEY_LENGTH &&
30
+ PHIS_USER_STATE_KEY_PATTERN.test(value);
31
+ }
32
+ export const PHIS_USER_STATE_MAX_SET_LIMIT = 256;
33
+ /**
34
+ * How much one Module's namespace may weigh, serialized.
35
+ *
36
+ * The membership row travels with every request that resolves a session, so this is paid for constantly
37
+ * and by everybody. It is a ceiling on a Module's whole namespace rather than on one key, because
38
+ * splitting a large value across ten keys is the obvious way around a per-key limit.
39
+ */
40
+ export const PHIS_USER_STATE_MAX_SERIALIZED_BYTES = 4096;
41
+ export function isPhisDeclarableUserStateKey(value) {
42
+ if (!value || typeof value !== "object" || Array.isArray(value))
43
+ return false;
44
+ const record = value;
45
+ if (!isPhisUserStateKey(record.key) || !isPhisUserStateShape(record.shape))
46
+ return false;
47
+ if (record.shape === "value") {
48
+ return typeof record.valueSchema === "string" && record.valueSchema.includes("/signals/");
49
+ }
50
+ if (record.shape === "set") {
51
+ return typeof record.limit === "number" &&
52
+ Number.isSafeInteger(record.limit) &&
53
+ record.limit > 0 &&
54
+ record.limit <= PHIS_USER_STATE_MAX_SET_LIMIT;
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * The value a write may store, or `null` when the shape does not admit it.
60
+ *
61
+ * Both ends call this -- phis before it writes, `@phis/ui` before it sends -- but they are not equals.
62
+ * The two packages ship separately and may be different versions of this file, so the browser's check is
63
+ * a saved round trip and never a substitute: phis checks again from its own copy and its answer is the
64
+ * one that counts. A shape a server does not know is refused there rather than stored and read back as
65
+ * something else, which is what keeps a newer Module against an older server a clear error instead of a
66
+ * quiet corruption.
67
+ *
68
+ * `stored` is what the key already holds, needed by the two shapes whose answer depends on it -- a
69
+ * marker that may not move backwards, and a set that evicts once it is full.
70
+ */
71
+ export function readPhisUserStateWrite(descriptor, value, stored) {
72
+ switch (descriptor.shape) {
73
+ case "flag":
74
+ return typeof value === "boolean" ? { value } : null;
75
+ case "marker": {
76
+ if (typeof value !== "number" || !Number.isFinite(value))
77
+ return null;
78
+ const current = typeof stored === "number" ? stored : Number.NEGATIVE_INFINITY;
79
+ return { value: value > current ? value : current };
80
+ }
81
+ case "value":
82
+ return value && typeof value === "object" && !Array.isArray(value)
83
+ ? { value: value }
84
+ : null;
85
+ case "set": {
86
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string"))
87
+ return null;
88
+ const entries = [...new Set(value)];
89
+ return { value: entries.slice(Math.max(0, entries.length - descriptor.limit)) };
90
+ }
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@phis/contracts",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
9
  "sideEffects": false,
10
- "description": "The agreements phi-server keeps with what surrounds it: the Add-on ABI, and the authorization vocabulary it shares with the site UI.",
10
+ "description": "The agreements phis keeps with the site UI, Sites and Add-ons: the Add-on ABI, access, signals, catalog, CMS identity, media, HTTP headers, locale, logging, capabilities and group levels.",
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.build.json",
13
13
  "typecheck": "tsc --noEmit",
@@ -55,6 +55,11 @@
55
55
  "import": "./dist/media.js",
56
56
  "default": "./dist/media.js"
57
57
  },
58
+ "./resource-links": {
59
+ "types": "./dist/resource-links.d.ts",
60
+ "import": "./dist/resource-links.js",
61
+ "default": "./dist/resource-links.js"
62
+ },
58
63
  "./server-capabilities": {
59
64
  "types": "./dist/server-capabilities.d.ts",
60
65
  "import": "./dist/server-capabilities.js",
@@ -65,10 +70,25 @@
65
70
  "import": "./dist/site-groups.js",
66
71
  "default": "./dist/site-groups.js"
67
72
  },
73
+ "./support": {
74
+ "types": "./dist/support.d.ts",
75
+ "import": "./dist/support.js",
76
+ "default": "./dist/support.js"
77
+ },
68
78
  "./signals": {
69
79
  "types": "./dist/signals.d.ts",
70
80
  "import": "./dist/signals.js",
71
81
  "default": "./dist/signals.js"
82
+ },
83
+ "./threads": {
84
+ "types": "./dist/threads.d.ts",
85
+ "import": "./dist/threads.js",
86
+ "default": "./dist/threads.js"
87
+ },
88
+ "./user-state": {
89
+ "types": "./dist/user-state.d.ts",
90
+ "import": "./dist/user-state.js",
91
+ "default": "./dist/user-state.js"
72
92
  }
73
93
  },
74
94
  "files": [