@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,363 @@
1
+ /**
2
+ * Threads, in the words both ends read them with.
3
+ *
4
+ * A thread is a conversation between people on one Site: two colleagues, a group, two groups, or a
5
+ * customer and a Support queue. Core stores it, the site UI renders it, and an Add-on may be handed a
6
+ * bounded view of one -- three readers of one shape, which is why the shape lives here rather than in
7
+ * any of them.
8
+ *
9
+ * It was two shapes before this file existed. The Site routes answered with database records carrying
10
+ * addresses, `threads:v1` answered with a projection that deliberately carried none, and the Support
11
+ * Module typed itself against the first. The same thread therefore had two descriptions, and the one a
12
+ * reader got depended on which door they came through.
13
+ *
14
+ * Who may see what is not in this file and cannot be: visibility is decided in Core, per reader, and a
15
+ * vocabulary shared with a browser is the last place to state a rule. What is here is what a visible
16
+ * thread looks like once Core has decided it is one.
17
+ */
18
+ import type { PhisUserProjection } from "./capabilities.js";
19
+ /**
20
+ * Who a thread is between.
21
+ *
22
+ * The kind is fixed when the thread is opened and is not a filter over its participants: it says which
23
+ * conversation this is, and each one is offered, or not, by the Modules a Site runs.
24
+ */
25
+ export declare const PhisThreadKind: {
26
+ /** Staff to Staff, between people who share a group. */
27
+ readonly Direct: 0;
28
+ /** Inside one group, its members among themselves. */
29
+ readonly Group: 1;
30
+ /** Between groups, each participating as a whole. */
31
+ readonly CrossGroup: 2;
32
+ /** A requester and a Support queue's group. */
33
+ readonly Support: 3;
34
+ };
35
+ export type PhisThreadKindValue = (typeof PhisThreadKind)[keyof typeof PhisThreadKind];
36
+ /**
37
+ * Where a thread stands.
38
+ *
39
+ * One row carries it, so archiving is the thread being closed for everyone in it rather than one reader
40
+ * hiding it from themselves.
41
+ */
42
+ export declare const PhisThreadStatus: {
43
+ readonly Open: 0;
44
+ readonly Archived: 1;
45
+ };
46
+ export type PhisThreadStatusValue = (typeof PhisThreadStatus)[keyof typeof PhisThreadStatus];
47
+ /** What is true of a thread, as bit flags. */
48
+ export declare const PhisThreadFlag: {
49
+ /** No new messages are accepted; what is there stays readable. */
50
+ readonly Locked: number;
51
+ };
52
+ /**
53
+ * What a participant is to the thread, as bit flags.
54
+ *
55
+ * `Requester` is the one that changes what a reader sees rather than what they may do: it marks the
56
+ * person a Support thread is *about*, and internal messages are written past them.
57
+ */
58
+ export declare const PhisThreadParticipantFlag: {
59
+ /** The person this Support thread belongs to. At most one participant carries it. */
60
+ readonly Requester: number;
61
+ /**
62
+ * This requester has acknowledged that the conversation is mirrored outside.
63
+ *
64
+ * Their messages are exported only once it is set. A customer's words are their own, and handing them
65
+ * to a third party is not a decision Staff may take for them.
66
+ */
67
+ readonly ExternalConsent: number;
68
+ /** Left the thread; their messages stay, and nothing new reaches them. */
69
+ readonly Left: number;
70
+ /** Still in it, without being notified. */
71
+ readonly Muted: number;
72
+ };
73
+ /**
74
+ * What is true of one message, as bit flags.
75
+ *
76
+ * `Internal` and `Confidential` answer different questions and are deliberately independent. `Internal`
77
+ * is *who may read it* -- Staff participants, and not the requester the note is about. `Confidential`
78
+ * is *where it may go* -- nowhere outside Core: not to an integration, not into the body of a
79
+ * notification, and not to an Add-on, which is handed the message with its text withheld and this flag
80
+ * set. A customer may set it themselves, which is the case that matters: a password pasted into a
81
+ * Support thread must be readable by the people helping and must never be mirrored anywhere.
82
+ */
83
+ export declare const PhisThreadMessageFlag: {
84
+ readonly Internal: number;
85
+ readonly Confidential: number;
86
+ /** Edited after it was written, at its source where it came from one. */
87
+ readonly Edited: number;
88
+ /** Withdrawn: the text is gone, the message stays so the conversation still reads. */
89
+ readonly Redacted: number;
90
+ /** Written by an integration from something outside, and therefore never exported back to it. */
91
+ readonly Imported: number;
92
+ /**
93
+ * `sourceLang` was recognised by a translator rather than declared by whoever delivered the message.
94
+ *
95
+ * The difference between an assertion and a guess, which is why it is a flag and not a shrug. An
96
+ * integration states the language from something it actually has -- a mail header, the locale of the
97
+ * page a form was submitted from -- and a translator states what a model made of the text: right for a
98
+ * paragraph, a coin toss for "bongo". Anything routing or counting on the column has to be able to see
99
+ * which of the two it is looking at.
100
+ *
101
+ * It is set where a translation reported a language for a message that declared none, because the
102
+ * recognition arrives as a field on a translation that was paid for anyway and throwing it away would
103
+ * leave the column empty for every message a person ever wrote. Never set by a caller: Core sets it
104
+ * together with the value, and a declared language is never overwritten by a detected one.
105
+ */
106
+ readonly LangDetected: number;
107
+ };
108
+ /**
109
+ * Which kinds of thread a Site offers at all, as bit flags.
110
+ *
111
+ * Not a switch anybody flips. It is the union of what the Site's active Modules declare, materialized
112
+ * the way declared Media Space kinds are, so a Site gains a kind by running a Module that needs it. A
113
+ * kind that stops being declared hides its threads and deletes nothing.
114
+ */
115
+ export declare const PhisSiteThreadKindFlag: {
116
+ readonly Direct: number;
117
+ readonly Group: number;
118
+ readonly CrossGroup: number;
119
+ readonly Support: number;
120
+ };
121
+ /**
122
+ * How a Module names the kind it needs, in the Area preset.
123
+ *
124
+ * A declaration travels as text, not as a number: it is written into a published Area preset by the
125
+ * Builder and read back by the control plane, and a stored bit would freeze the numbering of a
126
+ * vocabulary that is still allowed to grow. Two ends therefore have to spell these identically, which
127
+ * is why they are here and not in either of them.
128
+ *
129
+ * The order is the serialization order, so an unchanged selection writes byte-identically.
130
+ */
131
+ export declare const PHIS_DECLARABLE_THREAD_KINDS: readonly ["direct", "group", "crossGroup", "support"];
132
+ export type PhisDeclarableThreadKind = (typeof PHIS_DECLARABLE_THREAD_KINDS)[number];
133
+ /** What a declared name means, for whoever has to turn one into a thread or a Site flag. */
134
+ export declare const PHIS_THREAD_KIND_BY_DECLARATION: Readonly<Record<PhisDeclarableThreadKind, PhisThreadKindValue>>;
135
+ export declare const PHIS_SITE_THREAD_KIND_FLAG_BY_DECLARATION: Readonly<Record<PhisDeclarableThreadKind, number>>;
136
+ /** One thread, as a list shows it. */
137
+ export type PhisThreadSummary = {
138
+ id: number;
139
+ kind: PhisThreadKindValue;
140
+ status: PhisThreadStatusValue;
141
+ flags: number;
142
+ subject: string | null;
143
+ createdAt: string;
144
+ updatedAt: string;
145
+ latestMessageAt: string | null;
146
+ /** Whether this reader has messages they have not seen. */
147
+ unread: boolean;
148
+ };
149
+ /**
150
+ * Who is in a thread.
151
+ *
152
+ * A group participates as a group: its members are resolved when the thread is read, so somebody who
153
+ * joins the group afterwards is in the conversation and somebody who leaves it is not. Expanding a
154
+ * group into people at the moment it was named would have frozen a membership that keeps moving.
155
+ */
156
+ export type PhisThreadParticipant = {
157
+ kind: "user";
158
+ user: PhisUserProjection;
159
+ flags: number;
160
+ } | {
161
+ kind: "group";
162
+ groupId: number;
163
+ name: string;
164
+ flags: number;
165
+ };
166
+ /**
167
+ * Who wrote a message.
168
+ *
169
+ * A person is projected as a group member sees one: a display name, a company where the group discloses
170
+ * it, never an address. An integration is not dressed up as a person -- it says which provider it is and
171
+ * carries the outside name it is relaying, so "octocat on GitHub" reads as what it is.
172
+ */
173
+ export type PhisThreadMessageAuthor = {
174
+ kind: "user";
175
+ user: PhisUserProjection;
176
+ } | {
177
+ kind: "integration";
178
+ providerId: string;
179
+ externalName: string | null;
180
+ externalUrl: string | null;
181
+ } | {
182
+ kind: "system";
183
+ };
184
+ /**
185
+ * A file hanging on a message.
186
+ *
187
+ * Readable by whoever may read the message it hangs on, which is the whole rule: it travels inside the
188
+ * message rather than beside it, so an attachment on a note this reader is not shown is not in their
189
+ * copy of the conversation at all. Where the bytes sit -- a person's own Space, an Add-on's -- says who
190
+ * pays for them and who administers them, and answers nothing about who reads.
191
+ *
192
+ * There is no address on the Asset itself. Its delivery policy is `Internal`, and the only way to its
193
+ * bytes is the message that names it.
194
+ */
195
+ export type PhisThreadMessageAsset = {
196
+ assetId: number;
197
+ fileName: string;
198
+ contentType: string;
199
+ byteSize: number;
200
+ flags: number;
201
+ /** Where the bytes are fetched from, relative to the Site. */
202
+ href: string;
203
+ };
204
+ export type PhisThreadMessage = {
205
+ id: number;
206
+ author: PhisThreadMessageAuthor;
207
+ /** Null where the text is withheld from this reader: redacted, or confidential to an Add-on. */
208
+ bodyText: string | null;
209
+ bodyMarkdown: string | null;
210
+ /**
211
+ * The language the body is written in, where an integration recorded it. Null is `not declared`.
212
+ *
213
+ * Never to be read as the Site's default. Nobody is asked for this: a person writing on a Site is
214
+ * offered no language picker, because a translator determines the source language itself. What fills it
215
+ * is an integration that already knows -- a mail header, the locale of the page a form came from -- so a
216
+ * message written by a person arrives carrying null, which is the honest answer and the one a
217
+ * translation reads as "you detect it".
218
+ *
219
+ * It is also filled afterwards, by the translation of a message that declared none: the language comes
220
+ * back as a field on a translation somebody paid for, and dropping it would keep the column empty for
221
+ * everything a person ever wrote. Such a value carries `LangDetected`, because a recognition is a guess
222
+ * and an integration's declaration is not, and a declared language is never overwritten by one.
223
+ *
224
+ * It sits on the message and not on the thread, because two people writing to each other in different
225
+ * languages is the case it exists for.
226
+ */
227
+ sourceLang: string | null;
228
+ flags: number;
229
+ /** What hangs on it, in the order it was attached. Empty for a message carrying only text. */
230
+ assets: PhisThreadMessageAsset[];
231
+ createdAt: string;
232
+ updatedAt: string;
233
+ };
234
+ export type PhisThreadDetail = {
235
+ thread: PhisThreadSummary;
236
+ participants: PhisThreadParticipant[];
237
+ /** A window of the conversation, oldest first. */
238
+ messages: PhisThreadMessage[];
239
+ /** Whether older messages remain beyond the window `messages` carries. */
240
+ hasMoreMessages: boolean;
241
+ };
242
+ /**
243
+ * The threads capability: `@phis/server/threads:v1`.
244
+ *
245
+ * **With an actor, an Add-on sees what its actor sees.** Every read goes through the predicate Core
246
+ * applies to its own thread routes, so there is no listing here a person could not have obtained
247
+ * themselves, and no way to turn a Site's threads into something an Add-on can walk.
248
+ *
249
+ * **Without an actor -- a job, a verified hook, an event -- it reaches a thread it owns or one it is
250
+ * linked to**, and nothing else. It owns the threads it opened with no actor behind it. It is linked to
251
+ * a thread when a `resource-links:v1` row of its own carries `Synced` and points at that thread, at a
252
+ * ticket, or at a ticket task whose thread it is -- and that row was written by a person who could see
253
+ * the thread. The link is therefore that person's consent, for one integration and one conversation,
254
+ * and removing it withdraws the access. There is still no listing: an actorless Add-on finds a thread by
255
+ * the reference it recorded, never by walking.
256
+ *
257
+ * `Internal` messages are never handed to an actorless Add-on. `Confidential` ones arrive with their
258
+ * text withheld, whatever the Add-on's actor could read.
259
+ */
260
+ export type PhisThreadsCapabilityV1 = {
261
+ /** The threads the acting user is in, newest activity first. Needs an actor. */
262
+ list(options?: {
263
+ page?: number;
264
+ pageSize?: number;
265
+ unreadOnly?: boolean;
266
+ }): Promise<PhisThreadSummary[]>;
267
+ /** One thread with a window of its messages, or null where it is not reachable. */
268
+ get(threadId: number, options?: {
269
+ messageLimit?: number;
270
+ beforeMessageId?: number | null;
271
+ }): Promise<PhisThreadDetail | null>;
272
+ /**
273
+ * Opens a thread.
274
+ *
275
+ * With no actor the thread is opened as this Add-on's own, which is what an integration reporting an
276
+ * outside event needs and what makes the conversation reachable afterwards. Participants are named as
277
+ * user ids, group ids, or both, and every one is checked against this Site before anything is written.
278
+ *
279
+ * `messageFlags` is normalized rather than trusted: `Internal` requires an actor who is Staff, and
280
+ * `Imported` is Core's to set on what arrives through an integration.
281
+ */
282
+ create(input: {
283
+ kind: PhisThreadKindValue;
284
+ subject?: string | null;
285
+ message: string;
286
+ messageMarkdown?: string | null;
287
+ /**
288
+ * What the opening message is written in, where the integration knows.
289
+ *
290
+ * This is the **only** way the field is ever written: no Site route accepts it and no surface offers
291
+ * it, so a value present means an integration said so. An integration that does not know leaves it
292
+ * out, and the message carries null.
293
+ *
294
+ * A locale key the platform holds; anything else is refused rather than stored. Core checks that the
295
+ * key is well-formed and not that it is true -- naming a language over text in another one misleads
296
+ * every reader of it, and nothing contradicts the claim.
297
+ */
298
+ messageSourceLang?: string | null;
299
+ messageFlags?: number;
300
+ participantUserIds?: number[];
301
+ participantGroupIds?: number[];
302
+ /**
303
+ * Files to hang on the message, from this Add-on's own Space.
304
+ *
305
+ * Custody follows the uploader: an Add-on attaches what it wrote through `assets:v1`, never a
306
+ * person's file, because attaching somebody else's would put a record on their quota they did not
307
+ * agree to carry. Who may read it is the message's answer and never the Space's -- so a file an
308
+ * Add-on attaches is read by everyone in the conversation, and one it is shown is not thereby its
309
+ * own.
310
+ */
311
+ assetIds?: number[];
312
+ /** This Add-on's own reference for the message, unique within this Add-on. */
313
+ externalRef?: string;
314
+ externalAuthor?: {
315
+ name: string;
316
+ url?: string;
317
+ };
318
+ }): Promise<PhisThreadDetail>;
319
+ /**
320
+ * Adds a message to a reachable thread.
321
+ *
322
+ * Idempotent on `externalRef` where one is given: the same reference answers with the thread as it
323
+ * stands instead of writing a second message, which is what a webhook delivered twice needs.
324
+ */
325
+ reply(input: {
326
+ threadId: number;
327
+ message: string;
328
+ messageMarkdown?: string | null;
329
+ /** What this message is written in, on the terms `create` states. */
330
+ messageSourceLang?: string | null;
331
+ messageFlags?: number;
332
+ /** Files to hang on the reply, from this Add-on's own Space. See `create`. */
333
+ assetIds?: number[];
334
+ externalRef?: string;
335
+ externalAuthor?: {
336
+ name: string;
337
+ url?: string;
338
+ };
339
+ }): Promise<PhisThreadDetail | null>;
340
+ /** Brings more people into a reachable thread. Adds only; it never removes. */
341
+ addParticipants(input: {
342
+ threadId: number;
343
+ participantUserIds?: number[];
344
+ participantGroupIds?: number[];
345
+ }): Promise<PhisThreadDetail | null>;
346
+ /** Moves a reachable thread between open and archived, for everyone in it. */
347
+ setStatus(input: {
348
+ threadId: number;
349
+ status: PhisThreadStatusValue;
350
+ }): Promise<boolean>;
351
+ /** Marks the thread read for the acting user, up to its newest message or to one named. */
352
+ markRead(input: {
353
+ threadId: number;
354
+ messageId?: number | null;
355
+ }): Promise<boolean>;
356
+ /**
357
+ * The thread carrying one of this Add-on's own message references, or null.
358
+ *
359
+ * How an actorless Add-on arrives at a thread at all. The reference is the one it passed to `create`
360
+ * or `reply`, so it finds its way back to a conversation it already knows about and to no other.
361
+ */
362
+ findByExternalRef(externalRef: string): Promise<PhisThreadDetail | null>;
363
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Threads, in the words both ends read them with.
3
+ *
4
+ * A thread is a conversation between people on one Site: two colleagues, a group, two groups, or a
5
+ * customer and a Support queue. Core stores it, the site UI renders it, and an Add-on may be handed a
6
+ * bounded view of one -- three readers of one shape, which is why the shape lives here rather than in
7
+ * any of them.
8
+ *
9
+ * It was two shapes before this file existed. The Site routes answered with database records carrying
10
+ * addresses, `threads:v1` answered with a projection that deliberately carried none, and the Support
11
+ * Module typed itself against the first. The same thread therefore had two descriptions, and the one a
12
+ * reader got depended on which door they came through.
13
+ *
14
+ * Who may see what is not in this file and cannot be: visibility is decided in Core, per reader, and a
15
+ * vocabulary shared with a browser is the last place to state a rule. What is here is what a visible
16
+ * thread looks like once Core has decided it is one.
17
+ */
18
+ /**
19
+ * Who a thread is between.
20
+ *
21
+ * The kind is fixed when the thread is opened and is not a filter over its participants: it says which
22
+ * conversation this is, and each one is offered, or not, by the Modules a Site runs.
23
+ */
24
+ export const PhisThreadKind = {
25
+ /** Staff to Staff, between people who share a group. */
26
+ Direct: 0,
27
+ /** Inside one group, its members among themselves. */
28
+ Group: 1,
29
+ /** Between groups, each participating as a whole. */
30
+ CrossGroup: 2,
31
+ /** A requester and a Support queue's group. */
32
+ Support: 3,
33
+ };
34
+ /**
35
+ * Where a thread stands.
36
+ *
37
+ * One row carries it, so archiving is the thread being closed for everyone in it rather than one reader
38
+ * hiding it from themselves.
39
+ */
40
+ export const PhisThreadStatus = {
41
+ Open: 0,
42
+ Archived: 1,
43
+ };
44
+ /** What is true of a thread, as bit flags. */
45
+ export const PhisThreadFlag = {
46
+ /** No new messages are accepted; what is there stays readable. */
47
+ Locked: 1 << 0,
48
+ };
49
+ /**
50
+ * What a participant is to the thread, as bit flags.
51
+ *
52
+ * `Requester` is the one that changes what a reader sees rather than what they may do: it marks the
53
+ * person a Support thread is *about*, and internal messages are written past them.
54
+ */
55
+ export const PhisThreadParticipantFlag = {
56
+ /** The person this Support thread belongs to. At most one participant carries it. */
57
+ Requester: 1 << 0,
58
+ /**
59
+ * This requester has acknowledged that the conversation is mirrored outside.
60
+ *
61
+ * Their messages are exported only once it is set. A customer's words are their own, and handing them
62
+ * to a third party is not a decision Staff may take for them.
63
+ */
64
+ ExternalConsent: 1 << 1,
65
+ /** Left the thread; their messages stay, and nothing new reaches them. */
66
+ Left: 1 << 2,
67
+ /** Still in it, without being notified. */
68
+ Muted: 1 << 3,
69
+ };
70
+ /**
71
+ * What is true of one message, as bit flags.
72
+ *
73
+ * `Internal` and `Confidential` answer different questions and are deliberately independent. `Internal`
74
+ * is *who may read it* -- Staff participants, and not the requester the note is about. `Confidential`
75
+ * is *where it may go* -- nowhere outside Core: not to an integration, not into the body of a
76
+ * notification, and not to an Add-on, which is handed the message with its text withheld and this flag
77
+ * set. A customer may set it themselves, which is the case that matters: a password pasted into a
78
+ * Support thread must be readable by the people helping and must never be mirrored anywhere.
79
+ */
80
+ export const PhisThreadMessageFlag = {
81
+ Internal: 1 << 0,
82
+ Confidential: 1 << 1,
83
+ /** Edited after it was written, at its source where it came from one. */
84
+ Edited: 1 << 2,
85
+ /** Withdrawn: the text is gone, the message stays so the conversation still reads. */
86
+ Redacted: 1 << 3,
87
+ /** Written by an integration from something outside, and therefore never exported back to it. */
88
+ Imported: 1 << 4,
89
+ /**
90
+ * `sourceLang` was recognised by a translator rather than declared by whoever delivered the message.
91
+ *
92
+ * The difference between an assertion and a guess, which is why it is a flag and not a shrug. An
93
+ * integration states the language from something it actually has -- a mail header, the locale of the
94
+ * page a form was submitted from -- and a translator states what a model made of the text: right for a
95
+ * paragraph, a coin toss for "bongo". Anything routing or counting on the column has to be able to see
96
+ * which of the two it is looking at.
97
+ *
98
+ * It is set where a translation reported a language for a message that declared none, because the
99
+ * recognition arrives as a field on a translation that was paid for anyway and throwing it away would
100
+ * leave the column empty for every message a person ever wrote. Never set by a caller: Core sets it
101
+ * together with the value, and a declared language is never overwritten by a detected one.
102
+ */
103
+ LangDetected: 1 << 5,
104
+ };
105
+ /**
106
+ * Which kinds of thread a Site offers at all, as bit flags.
107
+ *
108
+ * Not a switch anybody flips. It is the union of what the Site's active Modules declare, materialized
109
+ * the way declared Media Space kinds are, so a Site gains a kind by running a Module that needs it. A
110
+ * kind that stops being declared hides its threads and deletes nothing.
111
+ */
112
+ export const PhisSiteThreadKindFlag = {
113
+ Direct: 1 << 0,
114
+ Group: 1 << 1,
115
+ CrossGroup: 1 << 2,
116
+ Support: 1 << 3,
117
+ };
118
+ /**
119
+ * How a Module names the kind it needs, in the Area preset.
120
+ *
121
+ * A declaration travels as text, not as a number: it is written into a published Area preset by the
122
+ * Builder and read back by the control plane, and a stored bit would freeze the numbering of a
123
+ * vocabulary that is still allowed to grow. Two ends therefore have to spell these identically, which
124
+ * is why they are here and not in either of them.
125
+ *
126
+ * The order is the serialization order, so an unchanged selection writes byte-identically.
127
+ */
128
+ export const PHIS_DECLARABLE_THREAD_KINDS = [
129
+ "direct",
130
+ "group",
131
+ "crossGroup",
132
+ "support",
133
+ ];
134
+ /** What a declared name means, for whoever has to turn one into a thread or a Site flag. */
135
+ export const PHIS_THREAD_KIND_BY_DECLARATION = {
136
+ direct: PhisThreadKind.Direct,
137
+ group: PhisThreadKind.Group,
138
+ crossGroup: PhisThreadKind.CrossGroup,
139
+ support: PhisThreadKind.Support,
140
+ };
141
+ export const PHIS_SITE_THREAD_KIND_FLAG_BY_DECLARATION = {
142
+ direct: PhisSiteThreadKindFlag.Direct,
143
+ group: PhisSiteThreadKindFlag.Group,
144
+ crossGroup: PhisSiteThreadKindFlag.CrossGroup,
145
+ support: PhisSiteThreadKindFlag.Support,
146
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The Translation service kind, as a Provider implements it.
3
+ *
4
+ * Core owns the question -- which text, out of which language, into which one -- and keeps asking it
5
+ * from the places that already translate: the global and Site message stores, and a conversation
6
+ * message somebody asked to read in their own language. A Provider answers it and nothing else.
7
+ * Nothing here reaches a database, a request, or a secret store: the Provider gets configuration and a
8
+ * context, and answers about text.
9
+ *
10
+ * What a Provider talks to is its own business, and deliberately not described here. A vendor over
11
+ * HTTP, a model on the machine, a catalogue it maintains itself and a CDN it consults first are all the
12
+ * same shape from Core's side, which is the reason this is a service kind rather than a list of
13
+ * vendors Core knows the names of.
14
+ */
15
+ export type PhisTranslationProviderId = `@${string}/${string}`;
16
+ /**
17
+ * How the text is to be read.
18
+ *
19
+ * `text` includes Markdown. Control characters survive a translation unchanged, so a Provider is not
20
+ * told about Markdown and has no reason to be: the markup arrives as part of the text and leaves as
21
+ * part of it. `html` is the case where tags must be recognised rather than carried, because a
22
+ * translator that reorders words around them has to know which spans it may not split.
23
+ */
24
+ export type PhisTranslationFormat = "text" | "html";
25
+ export type PhisTranslationRequest = {
26
+ /**
27
+ * A batch, and the ordinary case rather than the exception.
28
+ *
29
+ * Core asks for everything it needs in one call -- a page of labels, a store's worth of messages --
30
+ * and splits only where a Provider's own request limit forces it. The answer holds exactly as many
31
+ * entries as the request, in its order; a Provider never drops, merges or reorders one, because the
32
+ * caller matches them back by position and nothing else.
33
+ *
34
+ * One text is a batch of one and takes the same path.
35
+ */
36
+ texts: readonly string[];
37
+ /**
38
+ * What the text is written in, or `null` to let the Provider decide.
39
+ *
40
+ * `null` is a real case and not an omission: a Site message may carry no declared source, and a
41
+ * conversation message written before anybody was asked carries none either. What Core must not do
42
+ * is invent one, which is why this is nullable rather than defaulted.
43
+ */
44
+ sourceLang: string | null;
45
+ targetLang: string;
46
+ format: PhisTranslationFormat;
47
+ /**
48
+ * Context for the translator, and never translated itself.
49
+ *
50
+ * Natural language that influences how the text is read -- the subject matter, the surrounding
51
+ * sentence, the register to keep. It is not a description of where the text sits: resource identity,
52
+ * URLs, message keys and source languages are excluded by contract, and a Provider given those would
53
+ * be given noise to translate around.
54
+ *
55
+ * Core's own storage flags travel in the same field internally and are stripped before this one is
56
+ * built, so a Provider never sees `[html]` and has no reason to know such a thing exists. Absent
57
+ * where there is nothing to say, which is every conversation message.
58
+ */
59
+ context?: string;
60
+ /**
61
+ * Told that nobody is waiting for this any more.
62
+ *
63
+ * A translation is one request in flight to somewhere else. A Provider that ignores this still
64
+ * finishes and still costs what it costs; honouring it is how it stops paying for an answer nobody
65
+ * will read.
66
+ */
67
+ signal?: AbortSignal;
68
+ };
69
+ /**
70
+ * Both stores ask the same way.
71
+ *
72
+ * A global message is authored in English and a Site message in that Site's own immutable source
73
+ * locale, and the difference lands in `sourceLang` and nowhere else. A Provider is not told which store
74
+ * a text came from, because knowing would let it answer differently for one -- and the two are the same
75
+ * question asked about different text.
76
+ */
77
+ export type PhisTranslationResult = {
78
+ /** As many entries as the request had, in its order. */
79
+ texts: readonly string[];
80
+ };
81
+ /**
82
+ * What this Provider was found to be able to do, asked once when it is configured.
83
+ *
84
+ * Not per translation, for the reason the Storage probe is not per upload: an installation has to be
85
+ * able to offer a choice of target language before anybody presses anything, and a list fetched at the
86
+ * moment of use is a list that decides whether a control can be drawn after it has been drawn.
87
+ *
88
+ * A Provider that cannot be reached answers `usable: false` with a finding rather than throwing. Not
89
+ * being reachable is an answer, and an operator configuring one needs to be told which of the two it
90
+ * is.
91
+ */
92
+ export type PhisTranslationProbe = {
93
+ usable: boolean;
94
+ /**
95
+ * The languages it accepts as a source, as locale keys.
96
+ *
97
+ * Empty means it accepts any, or decides for itself -- a Provider that only ever detects has nothing
98
+ * to list. It does not mean it accepts none; a Provider that accepts none is not usable.
99
+ */
100
+ sourceLangs: string[];
101
+ /** The languages it can produce. An installation offers the intersection with what a Site has. */
102
+ targetLangs: string[];
103
+ /** What did not answer, in an operator's terms. Empty when everything did. */
104
+ findings: string[];
105
+ };
106
+ export interface PhisTranslationAdapter {
107
+ probeCapabilities(): Promise<PhisTranslationProbe>;
108
+ translate(input: PhisTranslationRequest): Promise<PhisTranslationResult>;
109
+ }
110
+ /**
111
+ * What a Provider is given, and the whole of it.
112
+ *
113
+ * A Provider needs its credential and must not be able to reach anything else, so the context carries
114
+ * no database handle and no general secret accessor: `readSecret` resolves the one reference this
115
+ * Provider's own configuration names, and there is no argument that could make it resolve another.
116
+ *
117
+ * There is no Site here. Which translator an installation uses is an installation's decision and not a
118
+ * Site's, unlike a Storage Profile, so a Provider is never handed a Site to branch on and can never
119
+ * come to treat one differently from another.
120
+ */
121
+ export type PhisTranslationServiceContext = {
122
+ providerId: PhisTranslationProviderId;
123
+ /** The Provider's own secret, or null when its configuration names none. */
124
+ readSecret: () => Promise<string | null>;
125
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The Translation service kind, as a Provider implements it.
3
+ *
4
+ * Core owns the question -- which text, out of which language, into which one -- and keeps asking it
5
+ * from the places that already translate: the global and Site message stores, and a conversation
6
+ * message somebody asked to read in their own language. A Provider answers it and nothing else.
7
+ * Nothing here reaches a database, a request, or a secret store: the Provider gets configuration and a
8
+ * context, and answers about text.
9
+ *
10
+ * What a Provider talks to is its own business, and deliberately not described here. A vendor over
11
+ * HTTP, a model on the machine, a catalogue it maintains itself and a CDN it consults first are all the
12
+ * same shape from Core's side, which is the reason this is a service kind rather than a list of
13
+ * vendors Core knows the names of.
14
+ */
15
+ export {};