@relaymessenger/chat-sdk-adapter 0.2.1 → 0.3.0-staging.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/LICENSE +3 -3
- package/README.md +185 -98
- package/dist/adapter.d.ts +121 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +643 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +230 -0
- package/dist/client.js.map +1 -0
- package/dist/content.d.ts +11 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +172 -0
- package/dist/content.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +28 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/reactions.d.ts +15 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +58 -0
- package/dist/reactions.js.map +1 -0
- package/dist/signature.d.ts +22 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +98 -0
- package/dist/signature.js.map +1 -0
- package/dist/thread-id.d.ts +8 -0
- package/dist/thread-id.d.ts.map +1 -0
- package/dist/thread-id.js +27 -0
- package/dist/thread-id.js.map +1 -0
- package/dist/turn.d.ts +16 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +22 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook.d.ts +9 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +115 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +51 -32
- package/dist/src/adapter.d.ts +0 -125
- package/dist/src/adapter.js +0 -724
- package/dist/src/chunk.d.ts +0 -25
- package/dist/src/chunk.js +0 -108
- package/dist/src/client.d.ts +0 -112
- package/dist/src/client.js +0 -180
- package/dist/src/format.d.ts +0 -43
- package/dist/src/format.js +0 -266
- package/dist/src/idempotency.d.ts +0 -50
- package/dist/src/idempotency.js +0 -75
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.js +0 -8
- package/dist/src/reactions.d.ts +0 -12
- package/dist/src/reactions.js +0 -98
- package/dist/src/signature.d.ts +0 -46
- package/dist/src/signature.js +0 -95
- package/dist/src/threadId.d.ts +0 -12
- package/dist/src/threadId.js +0 -28
- package/dist/src/turn.d.ts +0 -38
- package/dist/src/turn.js +0 -15
- package/dist/src/types.d.ts +0 -177
- package/dist/src/types.js +0 -7
package/dist/src/format.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown, tableToAscii } from "chat";
|
|
2
|
-
/** Relay accepts at most 200 style ranges on one text part. */
|
|
3
|
-
export const MAX_STYLE_RANGES = 200;
|
|
4
|
-
function sameStyles(left, right) {
|
|
5
|
-
if (left.length !== right.length)
|
|
6
|
-
return false;
|
|
7
|
-
return left.every((style, index) => style === right[index]);
|
|
8
|
-
}
|
|
9
|
-
class TextBuilder {
|
|
10
|
-
out = "";
|
|
11
|
-
runs = [];
|
|
12
|
-
get length() {
|
|
13
|
-
return this.out.length;
|
|
14
|
-
}
|
|
15
|
-
append(value, styles) {
|
|
16
|
-
if (!value)
|
|
17
|
-
return;
|
|
18
|
-
const start = this.out.length;
|
|
19
|
-
this.out += value;
|
|
20
|
-
if (styles.length > 0) {
|
|
21
|
-
this.runs.push({ start, length: value.length, styles: [...styles] });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/** Splice an already-rendered fragment, rebasing its style offsets. */
|
|
25
|
-
appendRendered(rendered, styles) {
|
|
26
|
-
if (!rendered.text)
|
|
27
|
-
return;
|
|
28
|
-
const base = this.out.length;
|
|
29
|
-
this.out += rendered.text;
|
|
30
|
-
if (styles.length > 0) {
|
|
31
|
-
this.runs.push({
|
|
32
|
-
start: base,
|
|
33
|
-
length: rendered.text.length,
|
|
34
|
-
styles: [...styles],
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
for (const run of rendered.styles) {
|
|
38
|
-
this.runs.push({
|
|
39
|
-
start: base + run.start,
|
|
40
|
-
length: run.length,
|
|
41
|
-
styles: [...run.styles],
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
finish() {
|
|
46
|
-
return { text: this.out, styles: normalizeStyles(this.runs) };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Collapse the raw run list into what Relay's contract requires: sorted by
|
|
51
|
-
* start, non-overlapping, and no more than 200 entries. Nested emphasis
|
|
52
|
-
* produces overlapping runs, so they are flattened onto a per-character style
|
|
53
|
-
* set and then re-run-length-encoded.
|
|
54
|
-
*/
|
|
55
|
-
export function normalizeStyles(runs) {
|
|
56
|
-
if (runs.length === 0)
|
|
57
|
-
return [];
|
|
58
|
-
let end = 0;
|
|
59
|
-
for (const run of runs)
|
|
60
|
-
end = Math.max(end, run.start + run.length);
|
|
61
|
-
const perCharacter = Array.from({ length: end }, () => []);
|
|
62
|
-
for (const run of runs) {
|
|
63
|
-
for (let i = run.start; i < run.start + run.length; i += 1) {
|
|
64
|
-
const slot = perCharacter[i];
|
|
65
|
-
if (!slot)
|
|
66
|
-
continue;
|
|
67
|
-
for (const style of run.styles) {
|
|
68
|
-
if (!slot.includes(style))
|
|
69
|
-
slot.push(style);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const merged = [];
|
|
74
|
-
let index = 0;
|
|
75
|
-
while (index < end) {
|
|
76
|
-
const styles = perCharacter[index] ?? [];
|
|
77
|
-
if (styles.length === 0) {
|
|
78
|
-
index += 1;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
let span = index + 1;
|
|
82
|
-
while (span < end && sameStyles(perCharacter[span] ?? [], styles))
|
|
83
|
-
span += 1;
|
|
84
|
-
merged.push({ start: index, length: span - index, styles: [...styles].sort() });
|
|
85
|
-
index = span;
|
|
86
|
-
}
|
|
87
|
-
// Styles are presentation only, so an overflowing document keeps every word
|
|
88
|
-
// and loses only the runs past Relay's ceiling.
|
|
89
|
-
return merged.slice(0, MAX_STYLE_RANGES);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Insert a prefix at the start of every line, remapping style offsets so the
|
|
93
|
-
* runs still cover the same characters.
|
|
94
|
-
*/
|
|
95
|
-
export function prefixLines(rendered, prefix, firstPrefix = prefix) {
|
|
96
|
-
if (!rendered.text)
|
|
97
|
-
return { text: firstPrefix, styles: [] };
|
|
98
|
-
const map = new Array(rendered.text.length + 1);
|
|
99
|
-
let out = firstPrefix;
|
|
100
|
-
let atLineStart = false;
|
|
101
|
-
for (let i = 0; i < rendered.text.length; i += 1) {
|
|
102
|
-
if (atLineStart) {
|
|
103
|
-
out += prefix;
|
|
104
|
-
atLineStart = false;
|
|
105
|
-
}
|
|
106
|
-
map[i] = out.length;
|
|
107
|
-
const character = rendered.text[i];
|
|
108
|
-
out += character;
|
|
109
|
-
if (character === "\n")
|
|
110
|
-
atLineStart = true;
|
|
111
|
-
}
|
|
112
|
-
map[rendered.text.length] = out.length;
|
|
113
|
-
const styles = rendered.styles.map((run) => {
|
|
114
|
-
const start = map[run.start];
|
|
115
|
-
const stop = map[run.start + run.length];
|
|
116
|
-
return { start, length: stop - start, styles: run.styles };
|
|
117
|
-
});
|
|
118
|
-
return { text: out, styles };
|
|
119
|
-
}
|
|
120
|
-
function nodeText(node) {
|
|
121
|
-
if ("value" in node && typeof node.value === "string")
|
|
122
|
-
return node.value;
|
|
123
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
124
|
-
return node.children.map(nodeText).join("");
|
|
125
|
-
}
|
|
126
|
-
return "";
|
|
127
|
-
}
|
|
128
|
-
function renderInline(nodes, builder, styles) {
|
|
129
|
-
for (const node of nodes) {
|
|
130
|
-
switch (node.type) {
|
|
131
|
-
case "text":
|
|
132
|
-
builder.append(node.value, styles);
|
|
133
|
-
break;
|
|
134
|
-
case "strong":
|
|
135
|
-
renderInline(node.children, builder, [...styles, "bold"]);
|
|
136
|
-
break;
|
|
137
|
-
case "emphasis":
|
|
138
|
-
renderInline(node.children, builder, [...styles, "italic"]);
|
|
139
|
-
break;
|
|
140
|
-
case "delete":
|
|
141
|
-
renderInline(node.children, builder, [
|
|
142
|
-
...styles,
|
|
143
|
-
"strikethrough",
|
|
144
|
-
]);
|
|
145
|
-
break;
|
|
146
|
-
case "inlineCode":
|
|
147
|
-
builder.append(node.value, [...styles, "monospace"]);
|
|
148
|
-
break;
|
|
149
|
-
case "break":
|
|
150
|
-
builder.append("\n", []);
|
|
151
|
-
break;
|
|
152
|
-
case "link": {
|
|
153
|
-
const label = nodeText(node);
|
|
154
|
-
renderInline(node.children, builder, styles);
|
|
155
|
-
// Relay has no link style, so the destination is written out rather
|
|
156
|
-
// than dropped whenever the label does not already carry it.
|
|
157
|
-
if (node.url && node.url !== label)
|
|
158
|
-
builder.append(` (${node.url})`, []);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case "image": {
|
|
162
|
-
const alt = node.alt ?? "";
|
|
163
|
-
builder.append(alt ? `${alt} (${node.url})` : node.url, styles);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "html":
|
|
167
|
-
builder.append(node.value, styles);
|
|
168
|
-
break;
|
|
169
|
-
case "footnoteReference":
|
|
170
|
-
builder.append(`[^${node.identifier}]`, styles);
|
|
171
|
-
break;
|
|
172
|
-
default: {
|
|
173
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
174
|
-
renderInline(node.children, builder, styles);
|
|
175
|
-
}
|
|
176
|
-
else if ("value" in node && typeof node.value === "string") {
|
|
177
|
-
builder.append(node.value, styles);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function renderBlock(node) {
|
|
184
|
-
switch (node.type) {
|
|
185
|
-
case "paragraph": {
|
|
186
|
-
const builder = new TextBuilder();
|
|
187
|
-
renderInline(node.children, builder, []);
|
|
188
|
-
return builder.finish();
|
|
189
|
-
}
|
|
190
|
-
case "heading": {
|
|
191
|
-
const builder = new TextBuilder();
|
|
192
|
-
renderInline(node.children, builder, ["bold"]);
|
|
193
|
-
return builder.finish();
|
|
194
|
-
}
|
|
195
|
-
case "code": {
|
|
196
|
-
const builder = new TextBuilder();
|
|
197
|
-
builder.append(node.value, ["monospace"]);
|
|
198
|
-
return builder.finish();
|
|
199
|
-
}
|
|
200
|
-
case "blockquote": {
|
|
201
|
-
const inner = renderBlocks(node.children);
|
|
202
|
-
return prefixLines(inner, "> ");
|
|
203
|
-
}
|
|
204
|
-
case "list": {
|
|
205
|
-
const parts = [];
|
|
206
|
-
let counter = node.start ?? 1;
|
|
207
|
-
for (const item of node.children) {
|
|
208
|
-
const marker = node.ordered ? `${counter}. ` : "- ";
|
|
209
|
-
counter += 1;
|
|
210
|
-
const inner = item.type === "listItem"
|
|
211
|
-
? renderBlocks(item.children)
|
|
212
|
-
: renderBlock(item);
|
|
213
|
-
parts.push(prefixLines(inner, " ".repeat(marker.length), marker));
|
|
214
|
-
}
|
|
215
|
-
return joinRendered(parts, "\n");
|
|
216
|
-
}
|
|
217
|
-
case "table":
|
|
218
|
-
return { text: tableToAscii(node), styles: [] };
|
|
219
|
-
case "thematicBreak":
|
|
220
|
-
return { text: "---", styles: [] };
|
|
221
|
-
case "html":
|
|
222
|
-
return { text: node.value, styles: [] };
|
|
223
|
-
default: {
|
|
224
|
-
const builder = new TextBuilder();
|
|
225
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
226
|
-
renderInline(node.children, builder, []);
|
|
227
|
-
}
|
|
228
|
-
else if ("value" in node && typeof node.value === "string") {
|
|
229
|
-
builder.append(node.value, []);
|
|
230
|
-
}
|
|
231
|
-
return builder.finish();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
function joinRendered(parts, separator) {
|
|
236
|
-
const builder = new TextBuilder();
|
|
237
|
-
let first = true;
|
|
238
|
-
for (const part of parts) {
|
|
239
|
-
if (!part.text)
|
|
240
|
-
continue;
|
|
241
|
-
if (!first)
|
|
242
|
-
builder.append(separator, []);
|
|
243
|
-
builder.appendRendered(part, []);
|
|
244
|
-
first = false;
|
|
245
|
-
}
|
|
246
|
-
return builder.finish();
|
|
247
|
-
}
|
|
248
|
-
function renderBlocks(nodes) {
|
|
249
|
-
return joinRendered(nodes.map(renderBlock), "\n\n");
|
|
250
|
-
}
|
|
251
|
-
/** Flatten an mdast tree into Relay text plus style runs. */
|
|
252
|
-
export function renderAst(ast) {
|
|
253
|
-
return renderBlocks(ast.children);
|
|
254
|
-
}
|
|
255
|
-
/** Flatten a Markdown string into Relay text plus style runs. */
|
|
256
|
-
export function renderMarkdown(markdown) {
|
|
257
|
-
return renderAst(parseMarkdown(markdown));
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Text a caller supplied verbatim. The empty `styles` array is meaningful to
|
|
261
|
-
* Relay: it marks the part as structured plain text so no client tries to read
|
|
262
|
-
* it as a legacy Markdown body.
|
|
263
|
-
*/
|
|
264
|
-
export function renderRawText(value) {
|
|
265
|
-
return { text: value, styles: [] };
|
|
266
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic `Idempotency-Key` derivation for outbound sends, and the
|
|
3
|
-
* `event_id` window that keeps at-least-once delivery from dispatching the
|
|
4
|
-
* same inbound event twice.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Key one outbound send against the inbound event that caused it: the event id
|
|
8
|
-
* and the send's position in the turn, and nothing else.
|
|
9
|
-
*
|
|
10
|
-
* The content is deliberately not in the key. Relay hashes the whole request
|
|
11
|
-
* server side, stores that hash beside the key, replays the stored response
|
|
12
|
-
* when a retry carries the same hash, and answers 409 `idempotency_conflict`
|
|
13
|
-
* when it does not. Folding the content into the key would change the key
|
|
14
|
-
* whenever the body changed, so the conflict could never fire and a handler
|
|
15
|
-
* whose model wrote different words the second time would post a genuine
|
|
16
|
-
* second message to the person. A key that names only the position is what
|
|
17
|
-
* lets Relay replay a faithful retry and refuse a diverging one.
|
|
18
|
-
*
|
|
19
|
-
* The prefix keeps the key at or above the 8 character floor even for an empty
|
|
20
|
-
* event id, and the event id is bounded so the whole key stays under 255.
|
|
21
|
-
*/
|
|
22
|
-
export declare function deriveIdempotencyKey(eventId: string, ordinal: number): string;
|
|
23
|
-
/**
|
|
24
|
-
* Key a send that no inbound event caused, such as a proactive session opened
|
|
25
|
-
* against a stored thread. There is no event to key against and no way to tell
|
|
26
|
-
* a deliberate repeat of the same words from a retry, so the key is unique per
|
|
27
|
-
* call: it makes the request well formed without claiming a replay guarantee
|
|
28
|
-
* the caller cannot have.
|
|
29
|
-
*/
|
|
30
|
-
export declare function unkeyedIdempotencyKey(conversationId: string): string;
|
|
31
|
-
/**
|
|
32
|
-
* Bounded insertion-ordered set of handled `event_id` values.
|
|
33
|
-
*
|
|
34
|
-
* `claim` is the only way in, and it both tests and inserts with nothing
|
|
35
|
-
* awaited in between, so two redeliveries of one event racing each other in
|
|
36
|
-
* the same process cannot both win. The window lives in memory in one process:
|
|
37
|
-
* a restart, or a second instance behind the same webhook URL, has no claim to
|
|
38
|
-
* lose and will dispatch the event again. The `Idempotency-Key` on every send
|
|
39
|
-
* is what makes that second dispatch harmless.
|
|
40
|
-
*/
|
|
41
|
-
export declare class DedupeWindow {
|
|
42
|
-
private readonly capacity;
|
|
43
|
-
private readonly seen;
|
|
44
|
-
constructor(capacity: number);
|
|
45
|
-
has(id: string): boolean;
|
|
46
|
-
/** Take the event id. Answers false when it was already taken. */
|
|
47
|
-
claim(id: string): boolean;
|
|
48
|
-
/** Give the event id back, so a later delivery of it is dispatched. */
|
|
49
|
-
release(id: string): void;
|
|
50
|
-
}
|
package/dist/src/idempotency.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic `Idempotency-Key` derivation for outbound sends, and the
|
|
3
|
-
* `event_id` window that keeps at-least-once delivery from dispatching the
|
|
4
|
-
* same inbound event twice.
|
|
5
|
-
*/
|
|
6
|
-
const KEY_PREFIX = "relay:";
|
|
7
|
-
/** Relay requires 8 to 255 characters on the `Idempotency-Key` header. */
|
|
8
|
-
const MAX_KEY_LENGTH = 255;
|
|
9
|
-
/**
|
|
10
|
-
* Key one outbound send against the inbound event that caused it: the event id
|
|
11
|
-
* and the send's position in the turn, and nothing else.
|
|
12
|
-
*
|
|
13
|
-
* The content is deliberately not in the key. Relay hashes the whole request
|
|
14
|
-
* server side, stores that hash beside the key, replays the stored response
|
|
15
|
-
* when a retry carries the same hash, and answers 409 `idempotency_conflict`
|
|
16
|
-
* when it does not. Folding the content into the key would change the key
|
|
17
|
-
* whenever the body changed, so the conflict could never fire and a handler
|
|
18
|
-
* whose model wrote different words the second time would post a genuine
|
|
19
|
-
* second message to the person. A key that names only the position is what
|
|
20
|
-
* lets Relay replay a faithful retry and refuse a diverging one.
|
|
21
|
-
*
|
|
22
|
-
* The prefix keeps the key at or above the 8 character floor even for an empty
|
|
23
|
-
* event id, and the event id is bounded so the whole key stays under 255.
|
|
24
|
-
*/
|
|
25
|
-
export function deriveIdempotencyKey(eventId, ordinal) {
|
|
26
|
-
const suffix = `:${ordinal}`;
|
|
27
|
-
const room = MAX_KEY_LENGTH - KEY_PREFIX.length - suffix.length;
|
|
28
|
-
return `${KEY_PREFIX}${eventId.slice(0, room)}${suffix}`;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Key a send that no inbound event caused, such as a proactive session opened
|
|
32
|
-
* against a stored thread. There is no event to key against and no way to tell
|
|
33
|
-
* a deliberate repeat of the same words from a retry, so the key is unique per
|
|
34
|
-
* call: it makes the request well formed without claiming a replay guarantee
|
|
35
|
-
* the caller cannot have.
|
|
36
|
-
*/
|
|
37
|
-
export function unkeyedIdempotencyKey(conversationId) {
|
|
38
|
-
return `${KEY_PREFIX}${conversationId}:${crypto.randomUUID()}`;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Bounded insertion-ordered set of handled `event_id` values.
|
|
42
|
-
*
|
|
43
|
-
* `claim` is the only way in, and it both tests and inserts with nothing
|
|
44
|
-
* awaited in between, so two redeliveries of one event racing each other in
|
|
45
|
-
* the same process cannot both win. The window lives in memory in one process:
|
|
46
|
-
* a restart, or a second instance behind the same webhook URL, has no claim to
|
|
47
|
-
* lose and will dispatch the event again. The `Idempotency-Key` on every send
|
|
48
|
-
* is what makes that second dispatch harmless.
|
|
49
|
-
*/
|
|
50
|
-
export class DedupeWindow {
|
|
51
|
-
capacity;
|
|
52
|
-
seen = new Set();
|
|
53
|
-
constructor(capacity) {
|
|
54
|
-
this.capacity = capacity;
|
|
55
|
-
}
|
|
56
|
-
has(id) {
|
|
57
|
-
return this.seen.has(id);
|
|
58
|
-
}
|
|
59
|
-
/** Take the event id. Answers false when it was already taken. */
|
|
60
|
-
claim(id) {
|
|
61
|
-
if (this.seen.has(id))
|
|
62
|
-
return false;
|
|
63
|
-
this.seen.add(id);
|
|
64
|
-
if (this.seen.size > this.capacity) {
|
|
65
|
-
const oldest = this.seen.values().next().value;
|
|
66
|
-
if (oldest !== undefined)
|
|
67
|
-
this.seen.delete(oldest);
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
/** Give the event id back, so a later delivery of it is dispatched. */
|
|
72
|
-
release(id) {
|
|
73
|
-
this.seen.delete(id);
|
|
74
|
-
}
|
|
75
|
-
}
|
package/dist/src/index.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export { createRelayAdapter, RelayAdapter, RelayInvocationSpentError, RELAY_ADAPTER_NAME, } from "./adapter.js";
|
|
2
|
-
export type { RelayAdapterOptions } from "./adapter.js";
|
|
3
|
-
export { RelayApiError, RelayClient } from "./client.js";
|
|
4
|
-
export type { RelayClientOptions, RelayHistoryOptions, RelayReactionOptions, RelaySendOptions, RelayUploadOptions, } from "./client.js";
|
|
5
|
-
export { MAX_PARTS_PER_MESSAGE, MAX_TEXT_PART_BYTES, chunkRenderedText, utf8Length, } from "./chunk.js";
|
|
6
|
-
export { MAX_STYLE_RANGES, renderAst, renderMarkdown, renderRawText, } from "./format.js";
|
|
7
|
-
export type { RenderedText } from "./format.js";
|
|
8
|
-
export { DedupeWindow, deriveIdempotencyKey, unkeyedIdempotencyKey, } from "./idempotency.js";
|
|
9
|
-
export { toRelayReaction } from "./reactions.js";
|
|
10
|
-
export type { RelayReaction } from "./reactions.js";
|
|
11
|
-
export { decodeWebhookSecret, verifyWebhookSignature, WebhookSecretError, WebhookVerificationError, } from "./signature.js";
|
|
12
|
-
export type { VerifyOptions } from "./signature.js";
|
|
13
|
-
export { decodeRelayThreadId, encodeRelayThreadId, RELAY_THREAD_PREFIX, relayChannelIdFromThreadId, } from "./threadId.js";
|
|
14
|
-
export type * from "./types.js";
|
package/dist/src/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { createRelayAdapter, RelayAdapter, RelayInvocationSpentError, RELAY_ADAPTER_NAME, } from "./adapter.js";
|
|
2
|
-
export { RelayApiError, RelayClient } from "./client.js";
|
|
3
|
-
export { MAX_PARTS_PER_MESSAGE, MAX_TEXT_PART_BYTES, chunkRenderedText, utf8Length, } from "./chunk.js";
|
|
4
|
-
export { MAX_STYLE_RANGES, renderAst, renderMarkdown, renderRawText, } from "./format.js";
|
|
5
|
-
export { DedupeWindow, deriveIdempotencyKey, unkeyedIdempotencyKey, } from "./idempotency.js";
|
|
6
|
-
export { toRelayReaction } from "./reactions.js";
|
|
7
|
-
export { decodeWebhookSecret, verifyWebhookSignature, WebhookSecretError, WebhookVerificationError, } from "./signature.js";
|
|
8
|
-
export { decodeRelayThreadId, encodeRelayThreadId, RELAY_THREAD_PREFIX, relayChannelIdFromThreadId, } from "./threadId.js";
|
package/dist/src/reactions.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { EmojiValue } from "chat";
|
|
2
|
-
import type { RelayReactionType } from "./types.js";
|
|
3
|
-
export interface RelayReaction {
|
|
4
|
-
type: RelayReactionType;
|
|
5
|
-
emoji?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Resolve a Chat SDK reaction into Relay's `{ type, emoji }` pair. Accepts an
|
|
9
|
-
* `EmojiValue`, a `:shortcode:`, a bare well-known name, the `{{emoji:name}}`
|
|
10
|
-
* placeholder an `EmojiValue` stringifies to, or a literal character.
|
|
11
|
-
*/
|
|
12
|
-
export declare function toRelayReaction(input: EmojiValue | string): RelayReaction;
|
package/dist/src/reactions.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_EMOJI_MAP } from "chat";
|
|
2
|
-
/**
|
|
3
|
-
* A Relay reaction is an emoji character. Relay does render it as an
|
|
4
|
-
* iMessage-style balloon, but the balloon draws whatever string is stored, so
|
|
5
|
-
* the character is what has to be sent. Relay once also accepted six named
|
|
6
|
-
* types (love, like, dislike, laugh, emphasize, question) and stored the name
|
|
7
|
-
* itself in the emoji column, which put the word "like" inside the balloon.
|
|
8
|
-
* Those names are gone from the API and this adapter never produces one.
|
|
9
|
-
*/
|
|
10
|
-
const PLACEHOLDER = /^\{\{emoji:([^}]+)\}\}$/;
|
|
11
|
-
const SHORTCODE = /^:([a-z0-9_+-]+):$/i;
|
|
12
|
-
/** Variation selector 16 and the five skin tone modifiers. */
|
|
13
|
-
const DECORATION = /[️\u{1F3FB}-\u{1F3FF}]/gu;
|
|
14
|
-
function firstFormat(value) {
|
|
15
|
-
return Array.isArray(value) ? value[0] : value;
|
|
16
|
-
}
|
|
17
|
-
function bare(value) {
|
|
18
|
-
return value.replace(DECORATION, "");
|
|
19
|
-
}
|
|
20
|
-
let literalIndex;
|
|
21
|
-
/** Lazily invert the emoji map so a literal character resolves to its name. */
|
|
22
|
-
function nameForLiteral(literal) {
|
|
23
|
-
if (!literalIndex) {
|
|
24
|
-
literalIndex = new Map();
|
|
25
|
-
for (const [name, formats] of Object.entries(DEFAULT_EMOJI_MAP)) {
|
|
26
|
-
const unicode = Array.isArray(formats.gchat) ? formats.gchat : [formats.gchat];
|
|
27
|
-
for (const candidate of unicode) {
|
|
28
|
-
const key = bare(candidate);
|
|
29
|
-
if (!literalIndex.has(key))
|
|
30
|
-
literalIndex.set(key, name);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return literalIndex.get(bare(literal));
|
|
35
|
-
}
|
|
36
|
-
let slackIndex;
|
|
37
|
-
function nameForAlias(alias) {
|
|
38
|
-
if (!slackIndex) {
|
|
39
|
-
slackIndex = new Map();
|
|
40
|
-
for (const [name, formats] of Object.entries(DEFAULT_EMOJI_MAP)) {
|
|
41
|
-
const aliases = Array.isArray(formats.slack) ? formats.slack : [formats.slack];
|
|
42
|
-
for (const candidate of aliases) {
|
|
43
|
-
if (!slackIndex.has(candidate))
|
|
44
|
-
slackIndex.set(candidate, name);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return slackIndex.get(alias);
|
|
49
|
-
}
|
|
50
|
-
function isEmojiValue(value) {
|
|
51
|
-
return typeof value === "object" && value !== null && "name" in value;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Resolve a Chat SDK reaction into Relay's `{ type, emoji }` pair. Accepts an
|
|
55
|
-
* `EmojiValue`, a `:shortcode:`, a bare well-known name, the `{{emoji:name}}`
|
|
56
|
-
* placeholder an `EmojiValue` stringifies to, or a literal character.
|
|
57
|
-
*/
|
|
58
|
-
export function toRelayReaction(input) {
|
|
59
|
-
let name;
|
|
60
|
-
let literal;
|
|
61
|
-
if (isEmojiValue(input)) {
|
|
62
|
-
name = input.name;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
const trimmed = input.trim();
|
|
66
|
-
const placeholder = PLACEHOLDER.exec(trimmed);
|
|
67
|
-
const shortcode = SHORTCODE.exec(trimmed);
|
|
68
|
-
const candidate = placeholder?.[1] ?? shortcode?.[1] ?? trimmed;
|
|
69
|
-
if (candidate in DEFAULT_EMOJI_MAP) {
|
|
70
|
-
name = candidate;
|
|
71
|
-
}
|
|
72
|
-
else if (nameForAlias(candidate)) {
|
|
73
|
-
name = nameForAlias(candidate);
|
|
74
|
-
}
|
|
75
|
-
else if (placeholder || shortcode) {
|
|
76
|
-
throw new Error(`unknown emoji name for a Relay reaction: ${trimmed}`);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
literal = trimmed;
|
|
80
|
-
name = nameForLiteral(trimmed);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// One normalization, on both the add and the remove path, because Relay
|
|
84
|
-
// deletes a reaction by exact string match
|
|
85
|
-
// (`Relay-Server/server/src/routes/platform.ts:571-572`). Without it,
|
|
86
|
-
// `addReaction("👍🏽")` followed by `removeReaction("👍")` answers 200 and
|
|
87
|
-
// deletes nothing. A known character resolves to the map's canonical form,
|
|
88
|
-
// which keeps a variation selector where the canonical form has one; an
|
|
89
|
-
// unknown character keeps its own bare form.
|
|
90
|
-
if (name) {
|
|
91
|
-
const formats = DEFAULT_EMOJI_MAP[name];
|
|
92
|
-
if (formats)
|
|
93
|
-
return { type: "emoji", emoji: firstFormat(formats.gchat) };
|
|
94
|
-
}
|
|
95
|
-
if (literal)
|
|
96
|
-
return { type: "emoji", emoji: bare(literal) };
|
|
97
|
-
throw new Error("a Relay reaction needs an emoji character or a well-known emoji name");
|
|
98
|
-
}
|
package/dist/src/signature.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export interface VerifyOptions {
|
|
2
|
-
/** Maximum allowed clock skew for `webhook-timestamp`, in seconds. */
|
|
3
|
-
toleranceSeconds?: number;
|
|
4
|
-
/** Override "now" for verification (seconds since epoch). Test hook. */
|
|
5
|
-
nowSeconds?: number;
|
|
6
|
-
}
|
|
7
|
-
export declare class WebhookVerificationError extends Error {
|
|
8
|
-
constructor(message: string);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* The configured signing secret is unusable. This is a deployment mistake, not
|
|
12
|
-
* a bad delivery, so it is a separate error: catch it where the secret is
|
|
13
|
-
* configured and fail there.
|
|
14
|
-
*/
|
|
15
|
-
export declare class WebhookSecretError extends Error {
|
|
16
|
-
constructor(message: string);
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Decode the signing secret Relay issued, with or without its `whsec_` prefix.
|
|
20
|
-
*
|
|
21
|
-
* `atob` answers a bare `InvalidCharacterError` on a secret that is not
|
|
22
|
-
* base64, which no caller recognizes: it escapes signature verification, the
|
|
23
|
-
* mount answers 500, and Relay reads that as a transient failure and redelivers
|
|
24
|
-
* ten times. Name it as a `WebhookSecretError` so it can be caught once, where
|
|
25
|
-
* the secret is configured, instead of on every delivery.
|
|
26
|
-
*/
|
|
27
|
-
export declare function decodeWebhookSecret(secret: string): Uint8Array<ArrayBuffer>;
|
|
28
|
-
/**
|
|
29
|
-
* Verify a Standard Webhooks signature exactly as Relay signs deliveries:
|
|
30
|
-
* HMAC-SHA256 over `${webhook-id}.${webhook-timestamp}.${raw body}` with the
|
|
31
|
-
* base64 secret, compared against every `v1,` candidate in
|
|
32
|
-
* `webhook-signature` (rotation sends two). Throws `WebhookVerificationError`
|
|
33
|
-
* on any failure, and `WebhookSecretError` when the configured secret itself
|
|
34
|
-
* is unusable. Uses only Web platform APIs (`crypto.subtle`, `atob`), so it
|
|
35
|
-
* runs on Node 22+, Vercel Edge, and Cloudflare Workers unchanged.
|
|
36
|
-
*/
|
|
37
|
-
export declare function verifyWebhookSignature(input: {
|
|
38
|
-
secret: string;
|
|
39
|
-
payload: string;
|
|
40
|
-
headers: {
|
|
41
|
-
"webhook-id": string | null | undefined;
|
|
42
|
-
"webhook-timestamp": string | null | undefined;
|
|
43
|
-
"webhook-signature": string | null | undefined;
|
|
44
|
-
};
|
|
45
|
-
options?: VerifyOptions;
|
|
46
|
-
}): Promise<void>;
|
package/dist/src/signature.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
export class WebhookVerificationError extends Error {
|
|
2
|
-
constructor(message) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = "WebhookVerificationError";
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* The configured signing secret is unusable. This is a deployment mistake, not
|
|
9
|
-
* a bad delivery, so it is a separate error: catch it where the secret is
|
|
10
|
-
* configured and fail there.
|
|
11
|
-
*/
|
|
12
|
-
export class WebhookSecretError extends Error {
|
|
13
|
-
constructor(message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = "WebhookSecretError";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const DEFAULT_TOLERANCE_SECONDS = 5 * 60;
|
|
19
|
-
function base64ToBytes(value) {
|
|
20
|
-
const binary = atob(value);
|
|
21
|
-
const bytes = new Uint8Array(binary.length);
|
|
22
|
-
for (let i = 0; i < binary.length; i += 1)
|
|
23
|
-
bytes[i] = binary.charCodeAt(i);
|
|
24
|
-
return bytes;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Decode the signing secret Relay issued, with or without its `whsec_` prefix.
|
|
28
|
-
*
|
|
29
|
-
* `atob` answers a bare `InvalidCharacterError` on a secret that is not
|
|
30
|
-
* base64, which no caller recognizes: it escapes signature verification, the
|
|
31
|
-
* mount answers 500, and Relay reads that as a transient failure and redelivers
|
|
32
|
-
* ten times. Name it as a `WebhookSecretError` so it can be caught once, where
|
|
33
|
-
* the secret is configured, instead of on every delivery.
|
|
34
|
-
*/
|
|
35
|
-
export function decodeWebhookSecret(secret) {
|
|
36
|
-
const raw = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
|
|
37
|
-
try {
|
|
38
|
-
return base64ToBytes(raw);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
throw new WebhookSecretError("the webhook signing secret is not base64: pass the whsec_ value Relay issued");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function constantTimeEqual(a, b) {
|
|
45
|
-
if (a.length !== b.length)
|
|
46
|
-
return false;
|
|
47
|
-
let diff = 0;
|
|
48
|
-
for (let i = 0; i < a.length; i += 1)
|
|
49
|
-
diff |= a[i] ^ b[i];
|
|
50
|
-
return diff === 0;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Verify a Standard Webhooks signature exactly as Relay signs deliveries:
|
|
54
|
-
* HMAC-SHA256 over `${webhook-id}.${webhook-timestamp}.${raw body}` with the
|
|
55
|
-
* base64 secret, compared against every `v1,` candidate in
|
|
56
|
-
* `webhook-signature` (rotation sends two). Throws `WebhookVerificationError`
|
|
57
|
-
* on any failure, and `WebhookSecretError` when the configured secret itself
|
|
58
|
-
* is unusable. Uses only Web platform APIs (`crypto.subtle`, `atob`), so it
|
|
59
|
-
* runs on Node 22+, Vercel Edge, and Cloudflare Workers unchanged.
|
|
60
|
-
*/
|
|
61
|
-
export async function verifyWebhookSignature(input) {
|
|
62
|
-
const id = input.headers["webhook-id"];
|
|
63
|
-
const timestamp = input.headers["webhook-timestamp"];
|
|
64
|
-
const signatureHeader = input.headers["webhook-signature"];
|
|
65
|
-
if (!id || !timestamp || !signatureHeader) {
|
|
66
|
-
throw new WebhookVerificationError("missing webhook signature headers");
|
|
67
|
-
}
|
|
68
|
-
const tolerance = input.options?.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
69
|
-
const now = input.options?.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
70
|
-
const ts = Number(timestamp);
|
|
71
|
-
if (!Number.isFinite(ts)) {
|
|
72
|
-
throw new WebhookVerificationError("invalid webhook-timestamp");
|
|
73
|
-
}
|
|
74
|
-
if (Math.abs(now - ts) > tolerance) {
|
|
75
|
-
throw new WebhookVerificationError("webhook-timestamp outside tolerance");
|
|
76
|
-
}
|
|
77
|
-
const key = await crypto.subtle.importKey("raw", decodeWebhookSecret(input.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
78
|
-
const signedContent = `${id}.${timestamp}.${input.payload}`;
|
|
79
|
-
const expected = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signedContent)));
|
|
80
|
-
for (const candidate of signatureHeader.split(" ")) {
|
|
81
|
-
const [version, value] = candidate.split(",", 2);
|
|
82
|
-
if (version !== "v1" || !value)
|
|
83
|
-
continue;
|
|
84
|
-
let provided;
|
|
85
|
-
try {
|
|
86
|
-
provided = base64ToBytes(value);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (constantTimeEqual(expected, provided))
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
throw new WebhookVerificationError("no matching v1 signature");
|
|
95
|
-
}
|
package/dist/src/threadId.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { RelayThreadId } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Relay thread ids are `relay:{conversation_id}`. A Relay conversation has no
|
|
4
|
-
* enclosing channel, so the channel id is the same string. Both are written
|
|
5
|
-
* out here rather than left to the Chat SDK defaults, because the default
|
|
6
|
-
* channel derivation keeps the first two colon-separated segments and that
|
|
7
|
-
* only happens to be correct for this shape.
|
|
8
|
-
*/
|
|
9
|
-
export declare const RELAY_THREAD_PREFIX = "relay:";
|
|
10
|
-
export declare function encodeRelayThreadId(platformData: RelayThreadId): string;
|
|
11
|
-
export declare function decodeRelayThreadId(threadId: string): RelayThreadId;
|
|
12
|
-
export declare function relayChannelIdFromThreadId(threadId: string): string;
|