@phis/contracts 0.3.1 → 0.5.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.
Files changed (46) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +159 -151
  3. package/dist/access.d.ts +13 -0
  4. package/dist/access.js +13 -0
  5. package/dist/addon-index.d.ts +4 -0
  6. package/dist/addon-index.js +4 -0
  7. package/dist/addon.d.ts +53 -2
  8. package/dist/capabilities.d.ts +54 -87
  9. package/dist/catalog.d.ts +14 -0
  10. package/dist/catalog.js +14 -2
  11. package/dist/cms.d.ts +11 -0
  12. package/dist/cms.js +20 -0
  13. package/dist/conditions.d.ts +96 -0
  14. package/dist/conditions.js +163 -0
  15. package/dist/controls.d.ts +83 -0
  16. package/dist/controls.js +133 -0
  17. package/dist/core.d.ts +38 -0
  18. package/dist/core.js +39 -0
  19. package/dist/forms.d.ts +496 -0
  20. package/dist/forms.js +508 -0
  21. package/dist/http.d.ts +10 -1
  22. package/dist/http.js +10 -1
  23. package/dist/layout.d.ts +22 -0
  24. package/dist/layout.js +30 -0
  25. package/dist/media-storage.d.ts +256 -14
  26. package/dist/media-storage.js +30 -2
  27. package/dist/media.d.ts +47 -3
  28. package/dist/media.js +59 -0
  29. package/dist/record.d.ts +8 -0
  30. package/dist/record.js +24 -0
  31. package/dist/references.d.ts +130 -0
  32. package/dist/references.js +208 -0
  33. package/dist/resource-links.d.ts +94 -0
  34. package/dist/resource-links.js +27 -0
  35. package/dist/signals.d.ts +1 -0
  36. package/dist/signals.js +3 -0
  37. package/dist/site-groups.d.ts +15 -0
  38. package/dist/support.d.ts +390 -0
  39. package/dist/support.js +148 -0
  40. package/dist/threads.d.ts +363 -0
  41. package/dist/threads.js +146 -0
  42. package/dist/translation.d.ts +125 -0
  43. package/dist/translation.js +15 -0
  44. package/dist/user-state.d.ts +132 -0
  45. package/dist/user-state.js +92 -0
  46. package/package.json +47 -2
@@ -0,0 +1,130 @@
1
+ /**
2
+ * What one side of the house means when it points at a Page: `@phis/server/references:v1`.
3
+ *
4
+ * The codec stood in two copies, in phis-ui and in phi-server, for the same reason the CMS instance id
5
+ * did before it moved here -- and with the same risk, which is that two sides agreeing byte for byte is
6
+ * a thing nobody notices stopping. It is one file now.
7
+ *
8
+ * **A reference is short because a Page already has a name.** It used to be base64-encoded JSON carrying
9
+ * the whole identity, which for a Module Page ran to 98 characters in every Markdown body that linked to
10
+ * one. The envelope was never protection -- it is encoding, not a signature, and the server revalidates
11
+ * Site and Area regardless -- so what it actually bought was a payload the server could unpack. It no
12
+ * longer needs to: a Module Page is named by the CMS Page id derived from `(ownerModuleId, presetKey)`,
13
+ * which is recomputable rather than allocated and therefore stands before any row does.
14
+ *
15
+ * The two variants are shaped differently on purpose. A Site Page carries its Page Scope id as a plain
16
+ * number because the server writes that number into a foreign key with `ON DELETE RESTRICT`: the
17
+ * database itself refuses to delete a Page something still points at, and nothing derived could take
18
+ * that column's place. A Module Page has no row to point a foreign key at -- the Site may never have
19
+ * touched it -- so it carries the derived id, which the server passes on without ever unpacking.
20
+ */
21
+ import { type PhiCmsAreaKey, type PhiCmsInstanceId } from "./cms.js";
22
+ export declare const PHIS_INTERNAL_PAGE_SCHEME: "phis:page/";
23
+ export declare const PHIS_INTERNAL_ASSET_SCHEME: "phis:asset/";
24
+ /**
25
+ * A Module Page's identity, hashed to one token both sides can recompute.
26
+ *
27
+ * Recomputable rather than allocated, so no row has to exist before the Page can be addressed, and it
28
+ * never mentions the path: reassigning where a Page answers leaves everything pointing at it alone.
29
+ */
30
+ export declare function createPhiPresetCmsPageId(identity: {
31
+ ownerModuleId: string;
32
+ presetKey: string;
33
+ }): PhiCmsInstanceId;
34
+ /** What a reference says, once read. A Module Page answers with its id, never with what was hashed. */
35
+ export type PhiPageTarget = {
36
+ kind: "site";
37
+ pageScopeId: number;
38
+ } | {
39
+ kind: "module";
40
+ pageId: PhiCmsInstanceId;
41
+ };
42
+ /** What a reference is made from. The Module variant differs: the identity goes in, the id comes out. */
43
+ export type PhiPageTargetInput = {
44
+ kind: "site";
45
+ pageScopeId: number;
46
+ } | {
47
+ kind: "module";
48
+ ownerModuleId: string;
49
+ presetKey: string;
50
+ };
51
+ export type PhiPageReference = string & {
52
+ readonly __phiPageReference: unique symbol;
53
+ };
54
+ export type PhiInternalReference = {
55
+ kind: "page";
56
+ reference: PhiPageReference;
57
+ target: PhiPageTarget;
58
+ fragment: string | null;
59
+ } | {
60
+ kind: "asset";
61
+ assetId: number;
62
+ };
63
+ export declare function createPhiPageReference(target: PhiPageTargetInput): PhiPageReference;
64
+ export declare function readPhiPageReference(value: unknown): {
65
+ reference: PhiPageReference;
66
+ target: PhiPageTarget;
67
+ } | null;
68
+ export declare function createPhiPageUri(reference: PhiPageReference, fragment?: string | null): string;
69
+ export declare function createPhiAssetUri(assetId: number): string;
70
+ export declare function readPhiInternalReference(value: unknown): PhiInternalReference | null;
71
+ /**
72
+ * Where a Control that offers one link leads.
73
+ *
74
+ * Two kinds and no third. A Page is named by its reference, which is identity and outlives the path
75
+ * moving under it; everything else is a literal URL somebody typed. What this refuses is the middle --
76
+ * the root-relative string that reads as external and means internal -- because that is the shape that
77
+ * rots in silence: the Page moves, the string does not, and nothing in the system ever knew the two were
78
+ * related.
79
+ *
80
+ * `newTab` sits inside the target rather than beside it. A Widget that offers two links -- a Card with a
81
+ * heading and an action -- otherwise grows one flag per link and names them in parallel (`newTab`,
82
+ * `actionNewTab`, and a third the day a third link lands). That is one decision written twice, and it
83
+ * drifts the first time only one of the two is read.
84
+ */
85
+ export type PhiLinkTarget = {
86
+ kind: "page";
87
+ reference: PhiPageReference;
88
+ /**
89
+ * Which Area to resolve the reference in, where it is not the one asking.
90
+ *
91
+ * A reference names a Page and not where to look for it: the Area is resolution context, supplied
92
+ * by whoever asks. Every asker supplied its own, so a link out of one Area into another resolved
93
+ * to nothing -- the Navigation source tree let an author pick a Page from another Area, the drop
94
+ * kept it, and the table then showed it as unresolvable.
95
+ *
96
+ * Absent means the asking Area, which is nearly every link, so nothing is stored for the ordinary
97
+ * case. Present, it does not make the reference mean something else; it says where the question
98
+ * goes. Whether the reader may follow it is still the target Area's own answer.
99
+ */
100
+ area?: PhiCmsAreaKey;
101
+ fragment?: string | null;
102
+ newTab?: boolean;
103
+ } | {
104
+ kind: "external";
105
+ href: string;
106
+ newTab?: boolean;
107
+ };
108
+ /**
109
+ * The field names a stored link target may stand under, and nothing else identifies one in persisted
110
+ * config.
111
+ *
112
+ * The Asset rule is the precedent and the reason: the server's reference collector reads persisted JSON,
113
+ * not the Widget catalogue, so it cannot ask a plugin which of its fields hold targets. A Widget that
114
+ * keeps one under another name authors a reference the delete guard cannot see, and the Page it points
115
+ * at can be removed while the Widget still draws a link to it.
116
+ *
117
+ * One plain name for the single-link case and a suffix for the rest, so a Card's second link is
118
+ * `actionLinkTarget` and is found by the same rule that finds the first.
119
+ */
120
+ export declare const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
121
+ export declare function isPhiLinkTargetConfigKey(key: string): boolean;
122
+ export declare function isPhiStorableExternalHref(value: string): boolean;
123
+ /**
124
+ * A stored link target, read back under the contract rather than trusted.
125
+ *
126
+ * Anything that is not one of the two shapes answers `null`, and a Control that gets `null` draws no
127
+ * link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
128
+ * non-interactive text beats a link that goes somewhere nobody chose.
129
+ */
130
+ export declare function readPhiLinkTarget(value: unknown): PhiLinkTarget | null;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * What one side of the house means when it points at a Page: `@phis/server/references:v1`.
3
+ *
4
+ * The codec stood in two copies, in phis-ui and in phi-server, for the same reason the CMS instance id
5
+ * did before it moved here -- and with the same risk, which is that two sides agreeing byte for byte is
6
+ * a thing nobody notices stopping. It is one file now.
7
+ *
8
+ * **A reference is short because a Page already has a name.** It used to be base64-encoded JSON carrying
9
+ * the whole identity, which for a Module Page ran to 98 characters in every Markdown body that linked to
10
+ * one. The envelope was never protection -- it is encoding, not a signature, and the server revalidates
11
+ * Site and Area regardless -- so what it actually bought was a payload the server could unpack. It no
12
+ * longer needs to: a Module Page is named by the CMS Page id derived from `(ownerModuleId, presetKey)`,
13
+ * which is recomputable rather than allocated and therefore stands before any row does.
14
+ *
15
+ * The two variants are shaped differently on purpose. A Site Page carries its Page Scope id as a plain
16
+ * number because the server writes that number into a foreign key with `ON DELETE RESTRICT`: the
17
+ * database itself refuses to delete a Page something still points at, and nothing derived could take
18
+ * that column's place. A Module Page has no row to point a foreign key at -- the Site may never have
19
+ * touched it -- so it carries the derived id, which the server passes on without ever unpacking.
20
+ */
21
+ import { createPhiPresetCmsInstanceId, isPhiCmsAreaKey, readPhiCmsInstanceIdDescriptor, } from "./cms.js";
22
+ export const PHIS_INTERNAL_PAGE_SCHEME = "phis:page/";
23
+ export const PHIS_INTERNAL_ASSET_SCHEME = "phis:asset/";
24
+ /** The Page itself, not a node inside it -- the one place a Page id differs from a node id. */
25
+ const PHI_CMS_PAGE_NODE_KEY = "page";
26
+ const PHI_PAGE_REFERENCE_VERSION = "v1";
27
+ const PHI_PAGE_REFERENCE_SITE_TAG = "s";
28
+ const PHI_PAGE_REFERENCE_MODULE_TAG = "p";
29
+ const PHI_MODULE_ID_PATTERN = /^@[^/]+\/[^/]+(?:\/[^/]+)*$/;
30
+ const PHI_PAGE_SCOPE_ID_PATTERN = /^[1-9]\d*$/u;
31
+ /**
32
+ * A Module Page's identity, hashed to one token both sides can recompute.
33
+ *
34
+ * Recomputable rather than allocated, so no row has to exist before the Page can be addressed, and it
35
+ * never mentions the path: reassigning where a Page answers leaves everything pointing at it alone.
36
+ */
37
+ export function createPhiPresetCmsPageId(identity) {
38
+ return createPhiPresetCmsInstanceId({
39
+ ...identity,
40
+ domain: "page",
41
+ nodeKey: PHI_CMS_PAGE_NODE_KEY,
42
+ });
43
+ }
44
+ function isPositiveInteger(value) {
45
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
46
+ }
47
+ function readPresetPageId(value) {
48
+ const descriptor = readPhiCmsInstanceIdDescriptor(value);
49
+ return descriptor?.origin === "preset" && descriptor.domain === "page"
50
+ ? value
51
+ : null;
52
+ }
53
+ export function createPhiPageReference(target) {
54
+ if (target.kind === "site") {
55
+ if (!isPositiveInteger(target.pageScopeId)) {
56
+ throw new Error("Phi Page Scope ids must be positive integers.");
57
+ }
58
+ return `${PHI_PAGE_REFERENCE_VERSION}${PHI_PAGE_REFERENCE_SITE_TAG}${target.pageScopeId}`;
59
+ }
60
+ /*
61
+ * Checked here rather than left to the hash. A malformed owner id hashes perfectly well and produces
62
+ * a reference that resolves to nothing, for a reason nobody can read back out of it -- the one real
63
+ * cost of a one-way id, paid once at the only place that can still see what went in.
64
+ */
65
+ if (!PHI_MODULE_ID_PATTERN.test(target.ownerModuleId.trim())) {
66
+ throw new Error("Invalid Phi Module id.");
67
+ }
68
+ const pageId = createPhiPresetCmsPageId({
69
+ ownerModuleId: target.ownerModuleId,
70
+ presetKey: target.presetKey,
71
+ });
72
+ return `${PHI_PAGE_REFERENCE_VERSION}${PHI_PAGE_REFERENCE_MODULE_TAG}${pageId}`;
73
+ }
74
+ export function readPhiPageReference(value) {
75
+ if (typeof value !== "string" || !value.startsWith(PHI_PAGE_REFERENCE_VERSION)) {
76
+ return null;
77
+ }
78
+ const tag = value.slice(PHI_PAGE_REFERENCE_VERSION.length, PHI_PAGE_REFERENCE_VERSION.length + 1);
79
+ const body = value.slice(PHI_PAGE_REFERENCE_VERSION.length + 1);
80
+ if (tag === PHI_PAGE_REFERENCE_SITE_TAG) {
81
+ const pageScopeId = PHI_PAGE_SCOPE_ID_PATTERN.test(body) ? Number(body) : NaN;
82
+ return isPositiveInteger(pageScopeId)
83
+ ? { reference: value, target: { kind: "site", pageScopeId } }
84
+ : null;
85
+ }
86
+ if (tag === PHI_PAGE_REFERENCE_MODULE_TAG) {
87
+ const pageId = readPresetPageId(body);
88
+ return pageId
89
+ ? { reference: value, target: { kind: "module", pageId } }
90
+ : null;
91
+ }
92
+ return null;
93
+ }
94
+ export function createPhiPageUri(reference, fragment) {
95
+ if (!readPhiPageReference(reference)) {
96
+ throw new Error("Invalid Phi Page reference.");
97
+ }
98
+ const normalizedFragment = fragment?.replace(/^#/u, "").trim();
99
+ return `${PHIS_INTERNAL_PAGE_SCHEME}${reference}${normalizedFragment ? `#${encodeURIComponent(normalizedFragment)}` : ""}`;
100
+ }
101
+ export function createPhiAssetUri(assetId) {
102
+ if (!isPositiveInteger(assetId)) {
103
+ throw new Error("Phi Asset ids must be positive integers.");
104
+ }
105
+ return `${PHIS_INTERNAL_ASSET_SCHEME}${assetId}`;
106
+ }
107
+ export function readPhiInternalReference(value) {
108
+ if (typeof value !== "string") {
109
+ return null;
110
+ }
111
+ if (value.startsWith(PHIS_INTERNAL_ASSET_SCHEME)) {
112
+ const id = value.slice(PHIS_INTERNAL_ASSET_SCHEME.length);
113
+ return /^[1-9]\d*$/u.test(id) && Number.isSafeInteger(Number(id))
114
+ ? { kind: "asset", assetId: Number(id) }
115
+ : null;
116
+ }
117
+ if (!value.startsWith(PHIS_INTERNAL_PAGE_SCHEME)) {
118
+ return null;
119
+ }
120
+ const [encodedReference, ...fragmentParts] = value.slice(PHIS_INTERNAL_PAGE_SCHEME.length).split("#");
121
+ const parsed = readPhiPageReference(encodedReference);
122
+ if (!parsed) {
123
+ return null;
124
+ }
125
+ try {
126
+ const fragment = fragmentParts.length > 0 ? decodeURIComponent(fragmentParts.join("#")) : null;
127
+ return { kind: "page", ...parsed, fragment };
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * The field names a stored link target may stand under, and nothing else identifies one in persisted
135
+ * config.
136
+ *
137
+ * The Asset rule is the precedent and the reason: the server's reference collector reads persisted JSON,
138
+ * not the Widget catalogue, so it cannot ask a plugin which of its fields hold targets. A Widget that
139
+ * keeps one under another name authors a reference the delete guard cannot see, and the Page it points
140
+ * at can be removed while the Widget still draws a link to it.
141
+ *
142
+ * One plain name for the single-link case and a suffix for the rest, so a Card's second link is
143
+ * `actionLinkTarget` and is found by the same rule that finds the first.
144
+ */
145
+ export const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
146
+ const PHI_LINK_TARGET_CONFIG_KEY_SUFFIX = "LinkTarget";
147
+ export function isPhiLinkTargetConfigKey(key) {
148
+ return key === PHI_LINK_TARGET_CONFIG_KEY
149
+ || (key.endsWith(PHI_LINK_TARGET_CONFIG_KEY_SUFFIX) && key.length > PHI_LINK_TARGET_CONFIG_KEY_SUFFIX.length);
150
+ }
151
+ /**
152
+ * Whether a string may be kept as an external target.
153
+ *
154
+ * Absolute, or an anchor on the document the reader already has. Everything else is refused, and the
155
+ * refusal is the whole point rather than strictness for its own sake: a stored `/pricing` is a Page link
156
+ * that has thrown its identity away, and one of them is enough to give the Site a second internal format
157
+ * -- one the resolver cannot resolve, the index cannot see, and the delete guard cannot warn about.
158
+ *
159
+ * The reserved scheme is rejected here in its plain form only. Normalising the encodings it can hide in
160
+ * belongs to the server's sanitizer, which sees the whole document; this is the near guard, so a target
161
+ * never carries an unresolved `phis:` URI in the field where a literal URL belongs.
162
+ */
163
+ const PHI_STORABLE_EXTERNAL_HREF_PATTERN = /^(?:https?:\/\/|\/\/|mailto:|tel:|#)/iu;
164
+ export function isPhiStorableExternalHref(value) {
165
+ const trimmed = value.trim();
166
+ return trimmed.length > 0
167
+ && !trimmed.toLowerCase().startsWith("phis:")
168
+ && PHI_STORABLE_EXTERNAL_HREF_PATTERN.test(trimmed);
169
+ }
170
+ /**
171
+ * A stored link target, read back under the contract rather than trusted.
172
+ *
173
+ * Anything that is not one of the two shapes answers `null`, and a Control that gets `null` draws no
174
+ * link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
175
+ * non-interactive text beats a link that goes somewhere nobody chose.
176
+ */
177
+ export function readPhiLinkTarget(value) {
178
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
179
+ return null;
180
+ }
181
+ const record = value;
182
+ const newTab = record.newTab === true ? { newTab: true } : {};
183
+ if (record.kind === "page") {
184
+ const parsed = readPhiPageReference(record.reference);
185
+ if (!parsed) {
186
+ return null;
187
+ }
188
+ const fragment = typeof record.fragment === "string" ? record.fragment.replace(/^#/u, "").trim() : "";
189
+ /*
190
+ * An Area that is not one is dropped rather than kept: the alternative is a stored value that asks
191
+ * a question nowhere can answer, and a link drawing nothing with no way to see why. Falling back to
192
+ * the asking Area is the behaviour of every link that names none, which is the honest default.
193
+ */
194
+ const area = isPhiCmsAreaKey(record.area) ? { area: record.area } : {};
195
+ return {
196
+ kind: "page",
197
+ reference: parsed.reference,
198
+ ...area,
199
+ ...(fragment ? { fragment } : {}),
200
+ ...newTab,
201
+ };
202
+ }
203
+ if (record.kind === "external") {
204
+ const href = typeof record.href === "string" ? record.href.trim() : "";
205
+ return isPhiStorableExternalHref(href) ? { kind: "external", href, ...newTab } : null;
206
+ }
207
+ return null;
208
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * References from a Phi record to something in another system: `@phis/server/resource-links:v1`.
3
+ *
4
+ * A ticket that is also a GitHub issue, a task that is also a pull request, a thread that is also a
5
+ * conversation somewhere else. The reference is a row of its own rather than a column on the record,
6
+ * because a record may have several at once, across several systems, and a column per system is a
7
+ * schema change per integration.
8
+ *
9
+ * The provider is the Add-on that wrote the link, and it is the whole of the ownership rule: an Add-on
10
+ * reads and writes its own links and sees no others. Two integrations pointing at one ticket therefore
11
+ * do not have to know about each other, and removing one takes its references with it.
12
+ *
13
+ * The link is also what an Add-on's actorless access is made of. A person who can see a record writes
14
+ * the link; from then on the Add-on may read and append to the conversation that record carries, and
15
+ * nothing beyond it. Unlinking ends that -- which is why `Synced` is stated per link rather than
16
+ * assumed: a reference that is only a pointer should not become a standing permission.
17
+ */
18
+ /** What a link points away from. */
19
+ export type PhisResourceLinkSubject = {
20
+ kind: "thread";
21
+ id: number;
22
+ } | {
23
+ kind: "ticket";
24
+ id: number;
25
+ } | {
26
+ kind: "ticketTask";
27
+ id: number;
28
+ };
29
+ /** What is true of a link, as bit flags. */
30
+ export declare const PhisResourceLinkFlag: {
31
+ /**
32
+ * The two sides are kept in step, and this Add-on may reach the subject's thread without an actor.
33
+ *
34
+ * A link without it is a reference someone recorded: it is shown, it is followed, and it grants
35
+ * nothing.
36
+ */
37
+ readonly Synced: number;
38
+ };
39
+ /**
40
+ * One reference, as Core stores it.
41
+ *
42
+ * `type` and `state` are the provider's own vocabulary -- an issue, a pull request, open, merged -- and
43
+ * Core never interprets either. It stores them, indexes the external id against them, and hands them
44
+ * back; what `21` means is the Add-on's business, exactly as a role name is.
45
+ */
46
+ export type PhisResourceLink = {
47
+ id: number;
48
+ subject: PhisResourceLinkSubject;
49
+ type: number;
50
+ externalId: string;
51
+ url: string | null;
52
+ state: number | null;
53
+ flags: number;
54
+ createdAt: string;
55
+ updatedAt: string;
56
+ };
57
+ /**
58
+ * The resource links capability: `@phis/server/resource-links:v1`.
59
+ *
60
+ * Bounded twice: to this Add-on's own links, and to subjects the acting user may reach. Writing needs
61
+ * an actor -- a link is a person saying these two things belong together, and it is what later lets the
62
+ * Add-on act with no person there. A hook or a job may read the links it wrote and may not make new
63
+ * ones.
64
+ */
65
+ export type PhisResourceLinksCapabilityV1 = {
66
+ /** This Add-on's links on one subject, or none where the subject is out of reach. */
67
+ list(subject: PhisResourceLinkSubject): Promise<PhisResourceLink[]>;
68
+ /**
69
+ * Records a reference, or hands back the one that is already there.
70
+ *
71
+ * Asking twice is how an id is learned, the way a declared group is: `(type, externalId)` is unique
72
+ * within this Add-on and this Site, so a second call answers with the first link instead of failing.
73
+ */
74
+ link(input: {
75
+ subject: PhisResourceLinkSubject;
76
+ type: number;
77
+ externalId: string;
78
+ url?: string | null;
79
+ state?: number | null;
80
+ flags?: number;
81
+ }): Promise<PhisResourceLink>;
82
+ /** Moves a link's provider-local state. */
83
+ setState(input: {
84
+ linkId: number;
85
+ state: number | null;
86
+ }): Promise<boolean>;
87
+ /** Removes a link, and with it whatever actorless reach it granted. */
88
+ unlink(linkId: number): Promise<boolean>;
89
+ /** One link of this Add-on by the external id it recorded, or null. */
90
+ findByExternalId(input: {
91
+ type: number;
92
+ externalId: string;
93
+ }): Promise<PhisResourceLink | null>;
94
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * References from a Phi record to something in another system: `@phis/server/resource-links:v1`.
3
+ *
4
+ * A ticket that is also a GitHub issue, a task that is also a pull request, a thread that is also a
5
+ * conversation somewhere else. The reference is a row of its own rather than a column on the record,
6
+ * because a record may have several at once, across several systems, and a column per system is a
7
+ * schema change per integration.
8
+ *
9
+ * The provider is the Add-on that wrote the link, and it is the whole of the ownership rule: an Add-on
10
+ * reads and writes its own links and sees no others. Two integrations pointing at one ticket therefore
11
+ * do not have to know about each other, and removing one takes its references with it.
12
+ *
13
+ * The link is also what an Add-on's actorless access is made of. A person who can see a record writes
14
+ * the link; from then on the Add-on may read and append to the conversation that record carries, and
15
+ * nothing beyond it. Unlinking ends that -- which is why `Synced` is stated per link rather than
16
+ * assumed: a reference that is only a pointer should not become a standing permission.
17
+ */
18
+ /** What is true of a link, as bit flags. */
19
+ export const PhisResourceLinkFlag = {
20
+ /**
21
+ * The two sides are kept in step, and this Add-on may reach the subject's thread without an actor.
22
+ *
23
+ * A link without it is a reference someone recorded: it is shown, it is followed, and it grants
24
+ * nothing.
25
+ */
26
+ Synced: 1 << 0,
27
+ };
package/dist/signals.d.ts CHANGED
@@ -78,6 +78,7 @@ export declare function isPhiSignalAddressSegment(value: string): boolean;
78
78
  */
79
79
  export declare function isPhiControllerPluginKey(value: string): boolean;
80
80
  export declare function isPhiControllerSignalAddress(address: unknown): address is PhiControllerSignalAddress;
81
+ export declare function readPhiControllerSignalAddress(value: unknown): PhiControllerSignalAddress | undefined;
81
82
  /** The instance a `cms:` address names, or null when the string is not one. */
82
83
  export declare function readPhiCmsSignalAddressInstanceId(address: string): PhiCmsInstanceId | null;
83
84
  export declare function isPhiRegionSignalAddress(address: string): boolean;
package/dist/signals.js CHANGED
@@ -146,6 +146,9 @@ export function isPhiControllerSignalAddress(address) {
146
146
  isPhiSignalAddressSegment(namespacedType.slice(controllerSeparatorIndex + 1)) &&
147
147
  isPhiSignalAddressSegment(instanceKey));
148
148
  }
149
+ export function readPhiControllerSignalAddress(value) {
150
+ return isPhiControllerSignalAddress(value) ? value.trim() : undefined;
151
+ }
149
152
  /** The instance a `cms:` address names, or null when the string is not one. */
150
153
  export function readPhiCmsSignalAddressInstanceId(address) {
151
154
  if (!address.startsWith("cms:")) {
@@ -36,3 +36,18 @@ export declare const PHI_GROUP_MEMBERSHIP_LEVELS: readonly [1, number, number, n
36
36
  * Reading downwards is what makes an unknown higher bit fail closed rather than round up.
37
37
  */
38
38
  export declare function readPhiGroupMembershipLevel(value: unknown): PhiGroupMembershipFlagValue | null;
39
+ /**
40
+ * A group a Module needs the Site to have, named rather than created.
41
+ *
42
+ * It travels in a Module's `seed` and is written when the Area carrying that Module is published. The
43
+ * key is the Module's own and never changes: it is what makes a second publish find the group it made
44
+ * the first time instead of making another. `provider_id` is the Module's id and is supplied by the
45
+ * materialization, not by the declaration, so a Module cannot claim a group belonging to another.
46
+ *
47
+ * The name is a starting value. Once the row exists it is the Site's, and administration renames it
48
+ * without the next publish undoing that.
49
+ */
50
+ export type PhisDeclaredSiteGroup = {
51
+ readonly key: string;
52
+ readonly name: string;
53
+ };