@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.
- package/NOTICE +1 -1
- package/README.md +159 -151
- package/dist/access.d.ts +13 -0
- package/dist/access.js +13 -0
- package/dist/addon-index.d.ts +4 -0
- package/dist/addon-index.js +4 -0
- package/dist/addon.d.ts +53 -2
- package/dist/capabilities.d.ts +54 -87
- package/dist/core.d.ts +38 -0
- package/dist/core.js +39 -0
- package/dist/media-storage.d.ts +256 -14
- package/dist/media-storage.js +30 -2
- package/dist/media.d.ts +47 -3
- package/dist/media.js +59 -0
- package/dist/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -0
- package/dist/site-groups.d.ts +15 -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 +22 -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
|
+
};
|
package/dist/threads.js
ADDED
|
@@ -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 {};
|