@relaymessenger/sdk 0.2.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/README.md +28 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.js +156 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/file-cursor.d.ts +10 -0
- package/dist/file-cursor.js +53 -0
- package/dist/file-cursor.js.map +1 -0
- package/dist/idempotency.d.ts +8 -0
- package/dist/idempotency.js +35 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-dedupe.d.ts +13 -0
- package/dist/memory-dedupe.js +25 -0
- package/dist/memory-dedupe.js.map +1 -0
- package/dist/poll-loop.d.ts +43 -0
- package/dist/poll-loop.js +164 -0
- package/dist/poll-loop.js.map +1 -0
- package/dist/signature.d.ts +18 -0
- package/dist/signature.js +61 -0
- package/dist/signature.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/url.d.ts +6 -0
- package/dist/url.js +40 -0
- package/dist/url.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# `@relaymessenger/sdk`
|
|
2
|
+
|
|
3
|
+
Shared Relay transport for native plugins and runnable examples.
|
|
4
|
+
|
|
5
|
+
Raw HTTPS against `https://api.relayapp.im` remains the public contract. This
|
|
6
|
+
package is a thin TypeScript binding used by host plugins: Agent Token auth,
|
|
7
|
+
Standard Webhooks verification, durable long-poll cursors, event dedupe, and
|
|
8
|
+
idempotent `POST /v1/messages`.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createRelayClient, runPollLoop, MemoryDedupe } from "@relaymessenger/sdk";
|
|
12
|
+
|
|
13
|
+
const client = createRelayClient({ token: process.env.RELAY_AGENT_TOKEN! });
|
|
14
|
+
const me = await client.getMe();
|
|
15
|
+
const dedupe = new MemoryDedupe();
|
|
16
|
+
|
|
17
|
+
await runPollLoop({
|
|
18
|
+
client,
|
|
19
|
+
getCursor: () => 0,
|
|
20
|
+
setCursor: async () => {},
|
|
21
|
+
dedupe,
|
|
22
|
+
onMessage: async ({ event, message, reply }) => {
|
|
23
|
+
await reply.text(`hi from ${me.handle}`);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Docs: https://docs.relayapp.im
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { RelayAgentProfile, RelayEventsPage, RelayOutgoingPart, RelayReplyRef, RelaySendResult } from "./types.js";
|
|
2
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
export type RelayClientOptions = {
|
|
4
|
+
token: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
fetchImpl?: FetchLike;
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type RelayClient = {
|
|
10
|
+
readonly baseUrl: string;
|
|
11
|
+
getMe: (params?: {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}) => Promise<RelayAgentProfile>;
|
|
14
|
+
pollEvents: (params: {
|
|
15
|
+
cursor: number;
|
|
16
|
+
timeoutSeconds?: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}) => Promise<RelayEventsPage>;
|
|
20
|
+
sendMessage: (params: {
|
|
21
|
+
conversationId: string;
|
|
22
|
+
parts: RelayOutgoingPart[];
|
|
23
|
+
replyTo?: RelayReplyRef;
|
|
24
|
+
invocationId?: string;
|
|
25
|
+
idempotencyKey: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}) => Promise<RelaySendResult>;
|
|
28
|
+
sendText: (params: {
|
|
29
|
+
conversationId: string;
|
|
30
|
+
text: string;
|
|
31
|
+
replyTo?: RelayReplyRef;
|
|
32
|
+
invocationId?: string;
|
|
33
|
+
idempotencyKey: string;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}) => Promise<RelaySendResult>;
|
|
36
|
+
setTyping: (params: {
|
|
37
|
+
conversationId: string;
|
|
38
|
+
started: boolean;
|
|
39
|
+
label?: string;
|
|
40
|
+
invocationId?: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}) => Promise<void>;
|
|
43
|
+
setResponding: (params: {
|
|
44
|
+
conversationId: string;
|
|
45
|
+
messageId: string;
|
|
46
|
+
label?: string;
|
|
47
|
+
invocationId?: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}) => Promise<void>;
|
|
50
|
+
markRead: (params: {
|
|
51
|
+
conversationId: string;
|
|
52
|
+
messageId: string;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
export declare function createRelayClient(options: RelayClientOptions): RelayClient;
|
|
57
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { RelayApiError, classifyRelayHttpStatus, isAbortError, } from "./errors.js";
|
|
2
|
+
import { normalizeRelayBaseUrl } from "./url.js";
|
|
3
|
+
async function readErrorDetail(response) {
|
|
4
|
+
try {
|
|
5
|
+
const body = (await response.json());
|
|
6
|
+
return {
|
|
7
|
+
...(body?.error?.code ? { code: body.error.code } : {}),
|
|
8
|
+
...(body?.error?.details ? { details: body.error.details } : {}),
|
|
9
|
+
message: body?.error?.message ?? body?.message ?? "",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { message: "" };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createRelayClient(options) {
|
|
17
|
+
if (!options.token.trim()) {
|
|
18
|
+
throw new Error("relay: Agent Token is required");
|
|
19
|
+
}
|
|
20
|
+
const baseUrl = normalizeRelayBaseUrl(options.baseUrl);
|
|
21
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
22
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
|
23
|
+
const request = async (params) => {
|
|
24
|
+
const url = new URL(`${baseUrl}${params.path}`);
|
|
25
|
+
for (const [key, value] of Object.entries(params.query ?? {})) {
|
|
26
|
+
if (value !== undefined)
|
|
27
|
+
url.searchParams.set(key, String(value));
|
|
28
|
+
}
|
|
29
|
+
const timeoutSignal = AbortSignal.timeout(params.timeoutMs ?? requestTimeoutMs);
|
|
30
|
+
const signal = params.signal
|
|
31
|
+
? AbortSignal.any([params.signal, timeoutSignal])
|
|
32
|
+
: timeoutSignal;
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await fetchImpl(url.toString(), {
|
|
36
|
+
method: params.method,
|
|
37
|
+
headers: {
|
|
38
|
+
authorization: `Bearer ${options.token}`,
|
|
39
|
+
...(params.body === undefined ? {} : { "content-type": "application/json" }),
|
|
40
|
+
...params.headers,
|
|
41
|
+
},
|
|
42
|
+
...(params.body === undefined ? {} : { body: JSON.stringify(params.body) }),
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (timeoutSignal.aborted && !params.signal?.aborted) {
|
|
48
|
+
throw new RelayApiError(`relay: ${params.method} ${params.path} timed out after ${params.timeoutMs ?? requestTimeoutMs}ms`, { kind: "retryable" });
|
|
49
|
+
}
|
|
50
|
+
if (isAbortError(error))
|
|
51
|
+
throw error;
|
|
52
|
+
throw new RelayApiError(`relay: network error: ${String(error)}`, {
|
|
53
|
+
kind: "retryable",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const detail = await readErrorDetail(response);
|
|
58
|
+
throw new RelayApiError(`relay: ${params.method} ${params.path} failed with ${response.status}${detail.message ? `: ${detail.message}` : ""}`, {
|
|
59
|
+
status: response.status,
|
|
60
|
+
kind: classifyRelayHttpStatus(response.status),
|
|
61
|
+
...(detail.code ? { code: detail.code } : {}),
|
|
62
|
+
...(detail.details ? { details: detail.details } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return response;
|
|
66
|
+
};
|
|
67
|
+
const client = {
|
|
68
|
+
baseUrl,
|
|
69
|
+
getMe: async (params) => {
|
|
70
|
+
const response = await request({
|
|
71
|
+
method: "GET",
|
|
72
|
+
path: "/v1/agents/me",
|
|
73
|
+
...(params?.signal ? { signal: params.signal } : {}),
|
|
74
|
+
});
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
return body.agent;
|
|
77
|
+
},
|
|
78
|
+
pollEvents: async (params) => {
|
|
79
|
+
const timeoutSeconds = Math.min(Math.max(params.timeoutSeconds ?? 30, 1), 30);
|
|
80
|
+
const response = await request({
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: "/v1/events",
|
|
83
|
+
query: {
|
|
84
|
+
cursor: params.cursor,
|
|
85
|
+
timeout: timeoutSeconds,
|
|
86
|
+
...(params.limit === undefined ? {} : { limit: params.limit }),
|
|
87
|
+
},
|
|
88
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
89
|
+
timeoutMs: (timeoutSeconds + 15) * 1_000,
|
|
90
|
+
});
|
|
91
|
+
const body = (await response.json());
|
|
92
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
93
|
+
const nextCursor = typeof body.next_cursor === "number" && Number.isSafeInteger(body.next_cursor)
|
|
94
|
+
? body.next_cursor
|
|
95
|
+
: params.cursor;
|
|
96
|
+
return { events, nextCursor };
|
|
97
|
+
},
|
|
98
|
+
sendMessage: async (params) => {
|
|
99
|
+
const response = await request({
|
|
100
|
+
method: "POST",
|
|
101
|
+
path: "/v1/messages",
|
|
102
|
+
headers: { "idempotency-key": params.idempotencyKey },
|
|
103
|
+
body: {
|
|
104
|
+
conversation_id: params.conversationId,
|
|
105
|
+
parts: params.parts,
|
|
106
|
+
...(params.invocationId ? { invocation_id: params.invocationId } : {}),
|
|
107
|
+
...(params.replyTo ? { reply_to: params.replyTo } : {}),
|
|
108
|
+
},
|
|
109
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
110
|
+
});
|
|
111
|
+
const body = (await response.json());
|
|
112
|
+
return { messages: body.messages };
|
|
113
|
+
},
|
|
114
|
+
sendText: async (params) => {
|
|
115
|
+
const { text, ...rest } = params;
|
|
116
|
+
return client.sendMessage({
|
|
117
|
+
...rest,
|
|
118
|
+
parts: [{ type: "text", text }],
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
setTyping: async (params) => {
|
|
122
|
+
await request({
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: `/v1/conversations/${encodeURIComponent(params.conversationId)}/typing`,
|
|
125
|
+
body: {
|
|
126
|
+
started: params.started,
|
|
127
|
+
...(params.label ? { label: params.label } : {}),
|
|
128
|
+
...(params.invocationId ? { invocation_id: params.invocationId } : {}),
|
|
129
|
+
},
|
|
130
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
setResponding: async (params) => {
|
|
134
|
+
await request({
|
|
135
|
+
method: "POST",
|
|
136
|
+
path: `/v1/conversations/${encodeURIComponent(params.conversationId)}/responding`,
|
|
137
|
+
body: {
|
|
138
|
+
message_id: params.messageId,
|
|
139
|
+
...(params.label ? { label: params.label } : {}),
|
|
140
|
+
...(params.invocationId ? { invocation_id: params.invocationId } : {}),
|
|
141
|
+
},
|
|
142
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
markRead: async (params) => {
|
|
146
|
+
await request({
|
|
147
|
+
method: "POST",
|
|
148
|
+
path: `/v1/conversations/${encodeURIComponent(params.conversationId)}/read`,
|
|
149
|
+
body: { message_id: params.messageId },
|
|
150
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return client;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RelayApiErrorKind = "auth" | "conflict" | "retryable" | "rejected";
|
|
2
|
+
/** Classified Relay API failure. `terminal` means retrying the same request cannot succeed. */
|
|
3
|
+
export declare class RelayApiError extends Error {
|
|
4
|
+
readonly status: number | undefined;
|
|
5
|
+
readonly kind: RelayApiErrorKind;
|
|
6
|
+
readonly code: string | undefined;
|
|
7
|
+
/** Structured `error.details` from the response body, e.g. `highest_delivered_cursor` on 422. */
|
|
8
|
+
readonly details: Record<string, unknown> | undefined;
|
|
9
|
+
constructor(message: string, params: {
|
|
10
|
+
status?: number;
|
|
11
|
+
kind: RelayApiErrorKind;
|
|
12
|
+
code?: string;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
});
|
|
15
|
+
get terminal(): boolean;
|
|
16
|
+
get retryable(): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class WebhookVerificationError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function classifyRelayHttpStatus(status: number): RelayApiErrorKind;
|
|
22
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
23
|
+
export declare function isRelayWebhookConflict(error: unknown): error is RelayApiError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Classified Relay API failure. `terminal` means retrying the same request cannot succeed. */
|
|
2
|
+
export class RelayApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
kind;
|
|
5
|
+
code;
|
|
6
|
+
/** Structured `error.details` from the response body, e.g. `highest_delivered_cursor` on 422. */
|
|
7
|
+
details;
|
|
8
|
+
constructor(message, params) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "RelayApiError";
|
|
11
|
+
this.status = params.status;
|
|
12
|
+
this.kind = params.kind;
|
|
13
|
+
this.code = params.code;
|
|
14
|
+
this.details = params.details;
|
|
15
|
+
}
|
|
16
|
+
get terminal() {
|
|
17
|
+
return this.kind !== "retryable";
|
|
18
|
+
}
|
|
19
|
+
get retryable() {
|
|
20
|
+
return this.kind === "retryable";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class WebhookVerificationError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "WebhookVerificationError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function classifyRelayHttpStatus(status) {
|
|
30
|
+
if (status === 401)
|
|
31
|
+
return "auth";
|
|
32
|
+
if (status === 409)
|
|
33
|
+
return "conflict";
|
|
34
|
+
if (status === 408 || status === 429 || status >= 500)
|
|
35
|
+
return "retryable";
|
|
36
|
+
return "rejected";
|
|
37
|
+
}
|
|
38
|
+
export function isAbortError(error) {
|
|
39
|
+
return error instanceof Error && error.name === "AbortError";
|
|
40
|
+
}
|
|
41
|
+
export function isRelayWebhookConflict(error) {
|
|
42
|
+
return (error instanceof RelayApiError &&
|
|
43
|
+
error.status === 409 &&
|
|
44
|
+
error.code !== "terminated_by_other_consumer");
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAqB;IAC3B,IAAI,CAAoB;IACxB,IAAI,CAAqB;IAClC,iGAAiG;IACxF,OAAO,CAAsC;IAEtD,YACE,OAAe,EACf,MAKC;QAED,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAChC,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC;IACnC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC;IACnC,CAAC;CACF;AAED,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,UAAU,CAAC;IACtC,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG;QAAE,OAAO,WAAW,CAAC;IAC1E,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,KAAK,YAAY,aAAa;QAC9B,KAAK,CAAC,MAAM,KAAK,GAAG;QACpB,KAAK,CAAC,IAAI,KAAK,8BAA8B,CAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FileCursorStore = {
|
|
2
|
+
load(): number;
|
|
3
|
+
current(): number;
|
|
4
|
+
advance(next: number): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Simple durable cursor file for long-poll consumers.
|
|
8
|
+
* Advances are monotonic; a corrupt file refuses to reset to zero silently.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createFileCursorStore(path: string): FileCursorStore;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Simple durable cursor file for long-poll consumers.
|
|
5
|
+
* Advances are monotonic; a corrupt file refuses to reset to zero silently.
|
|
6
|
+
*/
|
|
7
|
+
export function createFileCursorStore(path) {
|
|
8
|
+
let cursor = 0;
|
|
9
|
+
let loaded = false;
|
|
10
|
+
const read = () => {
|
|
11
|
+
try {
|
|
12
|
+
const raw = readFileSync(path, "utf8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (typeof parsed.cursor !== "number" ||
|
|
15
|
+
!Number.isSafeInteger(parsed.cursor) ||
|
|
16
|
+
parsed.cursor < 0) {
|
|
17
|
+
throw new Error(`corrupt cursor file: ${path}`);
|
|
18
|
+
}
|
|
19
|
+
return parsed.cursor;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = error.code;
|
|
23
|
+
if (code === "ENOENT")
|
|
24
|
+
return 0;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const write = (value) => {
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, `${JSON.stringify({ cursor: value })}\n`, { mode: 0o600 });
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
load: () => {
|
|
34
|
+
cursor = read();
|
|
35
|
+
loaded = true;
|
|
36
|
+
return cursor;
|
|
37
|
+
},
|
|
38
|
+
current: () => {
|
|
39
|
+
if (!loaded)
|
|
40
|
+
throw new Error("relay cursor: current() before load()");
|
|
41
|
+
return cursor;
|
|
42
|
+
},
|
|
43
|
+
advance: (next) => {
|
|
44
|
+
if (!loaded)
|
|
45
|
+
throw new Error("relay cursor: advance() before load()");
|
|
46
|
+
if (!Number.isSafeInteger(next) || next < 0 || next <= cursor)
|
|
47
|
+
return;
|
|
48
|
+
write(next);
|
|
49
|
+
cursor = next;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=file-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-cursor.js","sourceRoot":"","sources":["../src/file-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,MAAM,IAAI,GAAG,GAAW,EAAE;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC;YACvD,IACE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;gBACjC,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;gBACpC,MAAM,CAAC,MAAM,GAAG,CAAC,EACjB,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC;YAClD,CAAC;YACD,OAAO,MAAM,CAAC,MAAM,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;YACnD,IAAI,IAAI,KAAK,QAAQ;gBAAE,OAAO,CAAC,CAAC;YAChC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE;QAC9B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjF,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,GAAG,EAAE;YACT,MAAM,GAAG,IAAI,EAAE,CAAC;YAChB,MAAM,GAAG,IAAI,CAAC;YACd,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;YACtE,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YAChB,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;YACtE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,MAAM;gBAAE,OAAO;YACtE,KAAK,CAAC,IAAI,CAAC,CAAC;YACZ,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Idempotency-Key for one reply attempt.
|
|
3
|
+
* Includes a content digest so a redelivery that produces different text does
|
|
4
|
+
* not collide with a prior body under the same event id.
|
|
5
|
+
*/
|
|
6
|
+
export declare function deriveIdempotencyKey(eventId: string, ordinal: number, content: unknown): Promise<string>;
|
|
7
|
+
/** Simple key when the reply body is fixed for the event. */
|
|
8
|
+
export declare function replyIdempotencyKey(eventId: string, suffix?: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function canonicalJson(value) {
|
|
2
|
+
if (value === null || typeof value !== "object") {
|
|
3
|
+
return JSON.stringify(value) ?? "null";
|
|
4
|
+
}
|
|
5
|
+
const withToJson = value;
|
|
6
|
+
if (typeof withToJson.toJSON === "function") {
|
|
7
|
+
return canonicalJson(withToJson.toJSON());
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
11
|
+
}
|
|
12
|
+
const entries = Object.entries(value)
|
|
13
|
+
.filter(([, entry]) => entry !== undefined)
|
|
14
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
15
|
+
return `{${entries
|
|
16
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
17
|
+
.join(",")}}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic Idempotency-Key for one reply attempt.
|
|
21
|
+
* Includes a content digest so a redelivery that produces different text does
|
|
22
|
+
* not collide with a prior body under the same event id.
|
|
23
|
+
*/
|
|
24
|
+
export async function deriveIdempotencyKey(eventId, ordinal, content) {
|
|
25
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(content)));
|
|
26
|
+
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0"))
|
|
27
|
+
.join("")
|
|
28
|
+
.slice(0, 32);
|
|
29
|
+
return `${eventId.slice(0, 180)}:${ordinal}:${hex}`;
|
|
30
|
+
}
|
|
31
|
+
/** Simple key when the reply body is fixed for the event. */
|
|
32
|
+
export function replyIdempotencyKey(eventId, suffix = "0") {
|
|
33
|
+
return `reply-${eventId}-${suffix}`;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;IACzC,CAAC;IACD,MAAM,UAAU,GAAG,KAAmC,CAAC;IACvD,IAAI,OAAO,UAAU,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAC5C,OAAO,aAAa,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACnD,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;SAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;SAC1C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,IAAI,OAAO;SACf,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;SACvE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,OAAe,EACf,OAAgB;IAEhB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CACvC,SAAS,EACT,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CACjD,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CACtD,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CACnC;SACE,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;AACtD,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,MAAM,GAAG,GAAG;IAC/D,OAAO,SAAS,OAAO,IAAI,MAAM,EAAE,CAAC;AACtC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createRelayClient, type RelayClient, type RelayClientOptions, } from "./client.js";
|
|
2
|
+
export { RelayApiError, WebhookVerificationError, classifyRelayHttpStatus, isAbortError, isRelayWebhookConflict, type RelayApiErrorKind, } from "./errors.js";
|
|
3
|
+
export { createFileCursorStore, type FileCursorStore } from "./file-cursor.js";
|
|
4
|
+
export { deriveIdempotencyKey, replyIdempotencyKey, } from "./idempotency.js";
|
|
5
|
+
export { MemoryDedupe, type EventDedupe } from "./memory-dedupe.js";
|
|
6
|
+
export { runPollLoop, type MessageHandlerContext, type PollLoopParams, type ReplyOptions, } from "./poll-loop.js";
|
|
7
|
+
export { verifyWebhookSignature, type VerifyWebhookOptions, } from "./signature.js";
|
|
8
|
+
export type { MessageReceivedData, MessageReceivedEvent, RelayAgentProfile, RelayEventEnvelope, RelayEventsPage, RelayMessage, RelayOutgoingPart, RelayPart, RelayReplyRef, RelaySender, RelaySendResult, } from "./types.js";
|
|
9
|
+
export { DEFAULT_RELAY_BASE_URL, normalizeRelayBaseUrl } from "./url.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createRelayClient, } from "./client.js";
|
|
2
|
+
export { RelayApiError, WebhookVerificationError, classifyRelayHttpStatus, isAbortError, isRelayWebhookConflict, } from "./errors.js";
|
|
3
|
+
export { createFileCursorStore } from "./file-cursor.js";
|
|
4
|
+
export { deriveIdempotencyKey, replyIdempotencyKey, } from "./idempotency.js";
|
|
5
|
+
export { MemoryDedupe } from "./memory-dedupe.js";
|
|
6
|
+
export { runPollLoop, } from "./poll-loop.js";
|
|
7
|
+
export { verifyWebhookSignature, } from "./signature.js";
|
|
8
|
+
export { DEFAULT_RELAY_BASE_URL, normalizeRelayBaseUrl } from "./url.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,GAGlB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,uBAAuB,EACvB,YAAY,EACZ,sBAAsB,GAEvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAwB,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAoB,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACL,WAAW,GAIZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,sBAAsB,GAEvB,MAAM,gBAAgB,CAAC;AAcxB,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** In-memory event_id window for at-least-once delivery. */
|
|
2
|
+
export type EventDedupe = {
|
|
3
|
+
has(eventId: string): boolean;
|
|
4
|
+
/** Record only after the event was durably handled. */
|
|
5
|
+
record(eventId: string): void;
|
|
6
|
+
};
|
|
7
|
+
export declare class MemoryDedupe implements EventDedupe {
|
|
8
|
+
private readonly capacity;
|
|
9
|
+
private readonly seen;
|
|
10
|
+
constructor(capacity?: number);
|
|
11
|
+
has(eventId: string): boolean;
|
|
12
|
+
record(eventId: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** In-memory event_id window for at-least-once delivery. */
|
|
2
|
+
export class MemoryDedupe {
|
|
3
|
+
capacity;
|
|
4
|
+
seen = new Set();
|
|
5
|
+
constructor(capacity = 4096) {
|
|
6
|
+
this.capacity = capacity;
|
|
7
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
8
|
+
throw new Error("relay dedupe capacity must be a positive safe integer");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
has(eventId) {
|
|
12
|
+
return this.seen.has(eventId);
|
|
13
|
+
}
|
|
14
|
+
record(eventId) {
|
|
15
|
+
if (!eventId)
|
|
16
|
+
return;
|
|
17
|
+
this.seen.add(eventId);
|
|
18
|
+
if (this.seen.size > this.capacity) {
|
|
19
|
+
const oldest = this.seen.values().next().value;
|
|
20
|
+
if (oldest !== undefined)
|
|
21
|
+
this.seen.delete(oldest);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=memory-dedupe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-dedupe.js","sourceRoot":"","sources":["../src/memory-dedupe.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAQ5D,MAAM,OAAO,YAAY;IAGM,QAAQ;IAFpB,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,YAA6B,QAAQ,GAAG,IAAI;wBAAf,QAAQ;QACnC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,GAAG,CAAC,OAAe;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAED,MAAM,CAAC,OAAe;QACpB,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC/C,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RelayClient } from "./client.js";
|
|
2
|
+
import type { EventDedupe } from "./memory-dedupe.js";
|
|
3
|
+
import type { MessageReceivedData, MessageReceivedEvent, RelayEventEnvelope, RelayOutgoingPart, RelaySendResult } from "./types.js";
|
|
4
|
+
export type ReplyOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Render the reply as a quoted reply bubble referencing the inbound message.
|
|
7
|
+
* Off by default: plain sends match the shipped OpenClaw integration.
|
|
8
|
+
*/
|
|
9
|
+
quote?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type MessageHandlerContext = {
|
|
12
|
+
event: MessageReceivedEvent;
|
|
13
|
+
message: MessageReceivedData["message"];
|
|
14
|
+
invocationId?: string;
|
|
15
|
+
client: RelayClient;
|
|
16
|
+
reply: {
|
|
17
|
+
text: (text: string, options?: ReplyOptions) => Promise<RelaySendResult>;
|
|
18
|
+
parts: (parts: RelayOutgoingPart[], options?: ReplyOptions) => Promise<RelaySendResult>;
|
|
19
|
+
};
|
|
20
|
+
responding: (label?: string) => Promise<void>;
|
|
21
|
+
typing: (started?: boolean, label?: string) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export type PollLoopParams = {
|
|
24
|
+
client: RelayClient;
|
|
25
|
+
getCursor: () => number;
|
|
26
|
+
setCursor: (cursor: number) => void | Promise<void>;
|
|
27
|
+
dedupe: EventDedupe;
|
|
28
|
+
onMessage: (ctx: MessageHandlerContext) => Promise<void> | void;
|
|
29
|
+
/** Drop events whose sender is not allowed. Default: allow all users. */
|
|
30
|
+
allowSender?: (senderId: string) => boolean;
|
|
31
|
+
shouldProcess?: (event: RelayEventEnvelope) => boolean;
|
|
32
|
+
timeoutSeconds?: number;
|
|
33
|
+
limit?: number;
|
|
34
|
+
abortSignal?: AbortSignal;
|
|
35
|
+
log?: (line: string) => void;
|
|
36
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
37
|
+
random?: () => number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Long-poll receive loop for hosts that cannot expose a public webhook URL.
|
|
41
|
+
* Webhooks and long polling are mutually exclusive per Agent Token.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runPollLoop(params: PollLoopParams): Promise<void>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { RelayApiError, isAbortError } from "./errors.js";
|
|
2
|
+
import { replyIdempotencyKey } from "./idempotency.js";
|
|
3
|
+
const TRANSIENT_BASE_DELAY_MS = 500;
|
|
4
|
+
const TRANSIENT_MAX_DELAY_MS = 30_000;
|
|
5
|
+
function defaultSleep(ms, signal) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
if (signal.aborted) {
|
|
8
|
+
resolve();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const timer = setTimeout(() => {
|
|
12
|
+
signal.removeEventListener("abort", onAbort);
|
|
13
|
+
resolve();
|
|
14
|
+
}, ms);
|
|
15
|
+
const onAbort = () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
resolve();
|
|
18
|
+
};
|
|
19
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isMessageReceived(event) {
|
|
23
|
+
return (event.event_type === "message.received" &&
|
|
24
|
+
typeof event.data === "object" &&
|
|
25
|
+
event.data !== null &&
|
|
26
|
+
"message" in event.data &&
|
|
27
|
+
typeof event.data.message === "object");
|
|
28
|
+
}
|
|
29
|
+
function buildContext(client, event) {
|
|
30
|
+
const message = event.data.message;
|
|
31
|
+
const invocationId = event.data.invocation_id;
|
|
32
|
+
let ordinal = 0;
|
|
33
|
+
const ctx = {
|
|
34
|
+
event,
|
|
35
|
+
message,
|
|
36
|
+
client,
|
|
37
|
+
reply: {
|
|
38
|
+
text: async (text, options) => {
|
|
39
|
+
const key = replyIdempotencyKey(event.event_id, String(ordinal++));
|
|
40
|
+
return client.sendText({
|
|
41
|
+
conversationId: message.conversation_id,
|
|
42
|
+
text,
|
|
43
|
+
idempotencyKey: key,
|
|
44
|
+
...(invocationId ? { invocationId } : {}),
|
|
45
|
+
...(options?.quote
|
|
46
|
+
? { replyTo: { message_id: message.id } }
|
|
47
|
+
: {}),
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
parts: async (parts, options) => {
|
|
51
|
+
const key = replyIdempotencyKey(event.event_id, String(ordinal++));
|
|
52
|
+
return client.sendMessage({
|
|
53
|
+
conversationId: message.conversation_id,
|
|
54
|
+
parts,
|
|
55
|
+
idempotencyKey: key,
|
|
56
|
+
...(invocationId ? { invocationId } : {}),
|
|
57
|
+
...(options?.quote
|
|
58
|
+
? { replyTo: { message_id: message.id } }
|
|
59
|
+
: {}),
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
responding: async (label) => {
|
|
64
|
+
await client.setResponding({
|
|
65
|
+
conversationId: message.conversation_id,
|
|
66
|
+
messageId: message.id,
|
|
67
|
+
...(label ? { label } : {}),
|
|
68
|
+
...(invocationId ? { invocationId } : {}),
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
typing: async (started = true, label) => {
|
|
72
|
+
await client.setTyping({
|
|
73
|
+
conversationId: message.conversation_id,
|
|
74
|
+
started,
|
|
75
|
+
...(label ? { label } : {}),
|
|
76
|
+
...(invocationId ? { invocationId } : {}),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
if (invocationId)
|
|
81
|
+
ctx.invocationId = invocationId;
|
|
82
|
+
return ctx;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Long-poll receive loop for hosts that cannot expose a public webhook URL.
|
|
86
|
+
* Webhooks and long polling are mutually exclusive per Agent Token.
|
|
87
|
+
*/
|
|
88
|
+
export async function runPollLoop(params) {
|
|
89
|
+
const signal = params.abortSignal ?? new AbortController().signal;
|
|
90
|
+
const sleep = params.sleep ?? defaultSleep;
|
|
91
|
+
const random = params.random ?? Math.random;
|
|
92
|
+
const log = params.log ?? (() => { });
|
|
93
|
+
let transientAttempts = 0;
|
|
94
|
+
const transientDelayMs = () => {
|
|
95
|
+
const backoff = Math.min(TRANSIENT_MAX_DELAY_MS, TRANSIENT_BASE_DELAY_MS * 2 ** Math.min(transientAttempts, 6));
|
|
96
|
+
return backoff + Math.floor(random() * 250);
|
|
97
|
+
};
|
|
98
|
+
while (!signal.aborted) {
|
|
99
|
+
let page;
|
|
100
|
+
try {
|
|
101
|
+
page = await params.client.pollEvents({
|
|
102
|
+
cursor: params.getCursor(),
|
|
103
|
+
timeoutSeconds: params.timeoutSeconds ?? 30,
|
|
104
|
+
...(params.limit === undefined ? {} : { limit: params.limit }),
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (signal.aborted || isAbortError(error))
|
|
110
|
+
return;
|
|
111
|
+
if (error instanceof RelayApiError && error.terminal) {
|
|
112
|
+
// Retrying cannot fix these; surface them to the operator.
|
|
113
|
+
if (error.status === 410) {
|
|
114
|
+
log("[relay] cursor expired (410): the cursor is behind the seven-day " +
|
|
115
|
+
"retention ceiling. Reconcile from conversation history; do not " +
|
|
116
|
+
"reset the cursor to zero.");
|
|
117
|
+
}
|
|
118
|
+
else if (error.status === 422) {
|
|
119
|
+
const highest = error.details?.["highest_delivered_cursor"];
|
|
120
|
+
log("[relay] cursor ahead of the delivered ledger (422): reconcile " +
|
|
121
|
+
"history over REST, then resume from " +
|
|
122
|
+
(highest === undefined
|
|
123
|
+
? "error.details.highest_delivered_cursor."
|
|
124
|
+
: `cursor ${String(highest)}.`));
|
|
125
|
+
}
|
|
126
|
+
else if (error.kind === "conflict") {
|
|
127
|
+
log(`[relay] long poll conflict: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
transientAttempts += 1;
|
|
132
|
+
log(`[relay] transient poll error (attempt ${transientAttempts}): ${String(error)}`);
|
|
133
|
+
await sleep(transientDelayMs(), signal);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
transientAttempts = 0;
|
|
137
|
+
for (const event of page.events) {
|
|
138
|
+
if (params.shouldProcess && !params.shouldProcess(event))
|
|
139
|
+
continue;
|
|
140
|
+
if (!isMessageReceived(event))
|
|
141
|
+
continue;
|
|
142
|
+
if (params.dedupe.has(event.event_id))
|
|
143
|
+
continue;
|
|
144
|
+
const sender = event.data.message.sender;
|
|
145
|
+
if (sender.kind === "agent")
|
|
146
|
+
continue;
|
|
147
|
+
if (params.allowSender && sender.kind === "user" && !params.allowSender(sender.id)) {
|
|
148
|
+
params.dedupe.record(event.event_id);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const ctx = buildContext(params.client, event);
|
|
152
|
+
try {
|
|
153
|
+
await params.onMessage(ctx);
|
|
154
|
+
params.dedupe.record(event.event_id);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
log(`[relay] handler failed for ${event.event_id}: ${String(error)}`);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await params.setCursor(page.nextCursor);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=poll-loop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll-loop.js","sourceRoot":"","sources":["../src/poll-loop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type VerifyWebhookOptions = {
|
|
2
|
+
toleranceSeconds?: number;
|
|
3
|
+
nowSeconds?: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Verify a Standard Webhooks signature as Relay signs deliveries:
|
|
7
|
+
* HMAC-SHA256 over `${webhook-id}.${webhook-timestamp}.${raw body}`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyWebhookSignature(input: {
|
|
10
|
+
secret: string;
|
|
11
|
+
payload: string;
|
|
12
|
+
headers: {
|
|
13
|
+
"webhook-id": string | null | undefined;
|
|
14
|
+
"webhook-timestamp": string | null | undefined;
|
|
15
|
+
"webhook-signature": string | null | undefined;
|
|
16
|
+
};
|
|
17
|
+
options?: VerifyWebhookOptions;
|
|
18
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { WebhookVerificationError } from "./errors.js";
|
|
2
|
+
const DEFAULT_TOLERANCE_SECONDS = 5 * 60;
|
|
3
|
+
function base64ToBytes(value) {
|
|
4
|
+
const binary = atob(value);
|
|
5
|
+
const bytes = new Uint8Array(binary.length);
|
|
6
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
7
|
+
bytes[i] = binary.charCodeAt(i);
|
|
8
|
+
return bytes;
|
|
9
|
+
}
|
|
10
|
+
function decodeSecret(secret) {
|
|
11
|
+
const raw = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
|
|
12
|
+
return base64ToBytes(raw);
|
|
13
|
+
}
|
|
14
|
+
function constantTimeEqual(a, b) {
|
|
15
|
+
if (a.length !== b.length)
|
|
16
|
+
return false;
|
|
17
|
+
let diff = 0;
|
|
18
|
+
for (let i = 0; i < a.length; i += 1)
|
|
19
|
+
diff |= a[i] ^ b[i];
|
|
20
|
+
return diff === 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Verify a Standard Webhooks signature as Relay signs deliveries:
|
|
24
|
+
* HMAC-SHA256 over `${webhook-id}.${webhook-timestamp}.${raw body}`.
|
|
25
|
+
*/
|
|
26
|
+
export async function verifyWebhookSignature(input) {
|
|
27
|
+
const id = input.headers["webhook-id"];
|
|
28
|
+
const timestamp = input.headers["webhook-timestamp"];
|
|
29
|
+
const signatureHeader = input.headers["webhook-signature"];
|
|
30
|
+
if (!id || !timestamp || !signatureHeader) {
|
|
31
|
+
throw new WebhookVerificationError("missing webhook signature headers");
|
|
32
|
+
}
|
|
33
|
+
const tolerance = input.options?.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
34
|
+
const now = input.options?.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
35
|
+
const ts = Number(timestamp);
|
|
36
|
+
if (!Number.isFinite(ts)) {
|
|
37
|
+
throw new WebhookVerificationError("invalid webhook-timestamp");
|
|
38
|
+
}
|
|
39
|
+
if (Math.abs(now - ts) > tolerance) {
|
|
40
|
+
throw new WebhookVerificationError("webhook-timestamp outside tolerance");
|
|
41
|
+
}
|
|
42
|
+
const key = await crypto.subtle.importKey("raw", decodeSecret(input.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
43
|
+
const signedContent = `${id}.${timestamp}.${input.payload}`;
|
|
44
|
+
const expected = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signedContent)));
|
|
45
|
+
for (const candidate of signatureHeader.split(" ")) {
|
|
46
|
+
const [version, value] = candidate.split(",", 2);
|
|
47
|
+
if (version !== "v1" || !value)
|
|
48
|
+
continue;
|
|
49
|
+
let provided;
|
|
50
|
+
try {
|
|
51
|
+
provided = base64ToBytes(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (constantTimeEqual(expected, provided))
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new WebhookVerificationError("no matching v1 signature");
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.js","sourceRoot":"","sources":["../src/signature.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Wire types for the Relay v1 developer API used by plugins and examples. */
|
|
2
|
+
export type RelaySender = {
|
|
3
|
+
kind: "user" | "agent" | "system";
|
|
4
|
+
id: string;
|
|
5
|
+
display_name?: string;
|
|
6
|
+
};
|
|
7
|
+
export type RelayOutgoingPart = {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "media";
|
|
12
|
+
attachment_id?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "voice_memo";
|
|
16
|
+
attachment_id?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
duration_ms?: number;
|
|
19
|
+
} | {
|
|
20
|
+
type: "link_preview";
|
|
21
|
+
url: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: "data";
|
|
24
|
+
data: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
export type RelayPart = {
|
|
27
|
+
type: string;
|
|
28
|
+
part_index?: number;
|
|
29
|
+
text?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
attachment_id?: string;
|
|
32
|
+
duration_ms?: number;
|
|
33
|
+
data?: Record<string, unknown>;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
export type RelayReplyRef = {
|
|
37
|
+
message_id: string;
|
|
38
|
+
};
|
|
39
|
+
export type RelayMessage = {
|
|
40
|
+
id: string;
|
|
41
|
+
conversation_id: string;
|
|
42
|
+
sequence: number;
|
|
43
|
+
sender: RelaySender;
|
|
44
|
+
parts: RelayPart[];
|
|
45
|
+
reply_to?: RelayReplyRef | null;
|
|
46
|
+
fallback_text?: string;
|
|
47
|
+
status?: string;
|
|
48
|
+
created_at: string;
|
|
49
|
+
};
|
|
50
|
+
export type RelayEventEnvelope<TData = Record<string, unknown>> = {
|
|
51
|
+
event_id: string;
|
|
52
|
+
event_type: string;
|
|
53
|
+
agent_id: string;
|
|
54
|
+
created_at: string;
|
|
55
|
+
data: TData;
|
|
56
|
+
};
|
|
57
|
+
export type MessageReceivedData = {
|
|
58
|
+
message: RelayMessage;
|
|
59
|
+
invocation_id?: string;
|
|
60
|
+
};
|
|
61
|
+
export type MessageReceivedEvent = RelayEventEnvelope<MessageReceivedData>;
|
|
62
|
+
export type RelayAgentProfile = {
|
|
63
|
+
id: string;
|
|
64
|
+
owner_user_id?: string;
|
|
65
|
+
handle: string;
|
|
66
|
+
display_name: string;
|
|
67
|
+
tagline?: string;
|
|
68
|
+
avatar_url?: string | null;
|
|
69
|
+
visibility?: "private" | "unlisted" | "public";
|
|
70
|
+
created_at?: string;
|
|
71
|
+
};
|
|
72
|
+
export type RelayEventsPage = {
|
|
73
|
+
events: RelayEventEnvelope[];
|
|
74
|
+
nextCursor: number;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The 202 from `POST /v1/messages`. The server splits the accepted parts at
|
|
78
|
+
* ingest: each visible non-media part becomes its own message, contiguous
|
|
79
|
+
* media parts stay one media message, and a voice memo always commits alone,
|
|
80
|
+
* so one send commits one or more messages, in display order.
|
|
81
|
+
*/
|
|
82
|
+
export type RelaySendResult = {
|
|
83
|
+
messages: RelayMessage[];
|
|
84
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8EAA8E"}
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const DEFAULT_RELAY_BASE_URL = "https://api.relayapp.im";
|
|
2
|
+
/**
|
|
3
|
+
* Validate and canonicalize the API origin before a bearer token can be sent.
|
|
4
|
+
* Remote origins must use HTTPS. Plain HTTP is allowed only on loopback.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeRelayBaseUrl(raw?: string): string;
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
export const DEFAULT_RELAY_BASE_URL = "https://api.relayapp.im";
|
|
3
|
+
function isLoopbackHostname(hostname) {
|
|
4
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
5
|
+
const ipVersion = isIP(normalized);
|
|
6
|
+
if (ipVersion === 4)
|
|
7
|
+
return normalized.split(".")[0] === "127";
|
|
8
|
+
if (ipVersion === 6)
|
|
9
|
+
return normalized === "::1";
|
|
10
|
+
return normalized === "localhost" || normalized.endsWith(".localhost");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validate and canonicalize the API origin before a bearer token can be sent.
|
|
14
|
+
* Remote origins must use HTTPS. Plain HTTP is allowed only on loopback.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeRelayBaseUrl(raw) {
|
|
17
|
+
const candidate = raw?.trim() || DEFAULT_RELAY_BASE_URL;
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(candidate);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`relay: invalid baseUrl ${JSON.stringify(candidate)}`);
|
|
24
|
+
}
|
|
25
|
+
if (url.username || url.password) {
|
|
26
|
+
throw new Error("relay: baseUrl must not contain credentials");
|
|
27
|
+
}
|
|
28
|
+
if (url.search || url.hash) {
|
|
29
|
+
throw new Error("relay: baseUrl must not contain a query or fragment");
|
|
30
|
+
}
|
|
31
|
+
if (!/^\/+$/u.test(url.pathname)) {
|
|
32
|
+
throw new Error("relay: baseUrl must be an origin without a path");
|
|
33
|
+
}
|
|
34
|
+
if (url.protocol !== "https:" &&
|
|
35
|
+
!(url.protocol === "http:" && isLoopbackHostname(url.hostname))) {
|
|
36
|
+
throw new Error("relay: baseUrl must use HTTPS (HTTP is allowed only for loopback development)");
|
|
37
|
+
}
|
|
38
|
+
return url.origin;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,MAAM,CAAC,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAEhE,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,MAAM,UAAU,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;IAC/D,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,UAAU,KAAK,KAAK,CAAC;IACjD,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,MAAM,SAAS,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,sBAAsB,CAAC;IACxD,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IACE,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACzB,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAC/D,CAAC;QACD,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC;AACpB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relaymessenger/sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Relay transport library: HTTPS client, Standard Webhooks verify, durable long-poll, idempotent sends.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"test": "vitest run --config vitest.config.ts",
|
|
23
|
+
"prepack": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.18"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/relaymessenger/Relay-SDK.git",
|
|
35
|
+
"directory": "packages/sdk"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://docs.relayapp.im",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/relaymessenger/Relay-SDK/issues"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"relay",
|
|
43
|
+
"relaymessenger",
|
|
44
|
+
"agents",
|
|
45
|
+
"webhooks",
|
|
46
|
+
"long-poll"
|
|
47
|
+
]
|
|
48
|
+
}
|