@rine-network/eve 0.1.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.
@@ -0,0 +1,304 @@
1
+ import { _ as senderLabel, f as renderMessageBody, g as renderThreadLine, n as asRecipient, v as verifiedNote } from "./tool-BC49DldZ.js";
2
+ import { n as verifyRineSignature } from "./hmac-CoaKHmf6.js";
3
+ import { t as getRineClient } from "./client-X_-9CpQT.js";
4
+ import { POST, defineChannel } from "eve/channels";
5
+ import { asMessageUuid } from "@rine-network/sdk";
6
+ //#region src/transcript.ts
7
+ /** char ≈ 4 × tokens — the coarse proxy the spec specifies (REQ-CTX-01, OQ1). */
8
+ const CHARS_PER_TOKEN = 4;
9
+ /** Marker prepended when older turns were dropped to fit the budget. */
10
+ const OMITTED_MARKER = "[…earlier turns omitted]";
11
+ /**
12
+ * Render the most-recent turns of a thread as role-tagged `context[]` lines within
13
+ * a token budget. Turns are accumulated from the NEWEST end backwards until the
14
+ * char budget (`tokenBudget * 4`) is hit; if any older turns were dropped, a single
15
+ * {@link OMITTED_MARKER} line is prepended. Returns lines oldest→newest, ready to
16
+ * slot into the Eve session `context` array.
17
+ *
18
+ * @param entries Thread entries, ordered oldest→newest (SDK `client.thread()`).
19
+ * @param tokenBudget Approximate token budget for the transcript.
20
+ */
21
+ function renderTranscriptContext(entries, tokenBudget) {
22
+ const charBudget = Math.max(0, tokenBudget) * CHARS_PER_TOKEN;
23
+ const lines = entries.map(renderThreadLine);
24
+ const kept = [];
25
+ let used = 0;
26
+ for (let i = lines.length - 1; i >= 0; i--) {
27
+ const line = lines[i];
28
+ const cost = line.length + 1;
29
+ if (kept.length > 0 && used + cost > charBudget) break;
30
+ kept.push(line);
31
+ used += cost;
32
+ }
33
+ kept.reverse();
34
+ if (kept.length < lines.length) kept.unshift(OMITTED_MARKER);
35
+ return kept;
36
+ }
37
+ //#endregion
38
+ //#region src/inbound.ts
39
+ /**
40
+ * Inbound webhook helpers for the rine channel: extracting the message id from a
41
+ * rine standard-webhook body, and the continuation-token codec.
42
+ *
43
+ * The continuation token is how the channel makes outbound replies STATELESS (R7):
44
+ * it encodes the rine `conversation_id`, the reply target (the group handle for
45
+ * group mail, else the sender handle), and — for 1:1 inbound only — the inbound
46
+ * `message id` so the `message.completed` handler can reply IN-PLACE (via the reply
47
+ * endpoint, preserving the inbound conversation) without any durable per-session map.
48
+ * Because the token bakes in the per-message `m`, it differs on every 1:1 inbound
49
+ * turn, so Eve does NOT resume a prior session from it — cross-turn continuity comes
50
+ * from the general thread primitive (push-injected transcript, Slice E), not session
51
+ * resume. The message id is optional: group inbound and tokens minted by an older
52
+ * build carry only `{c,r}` and decode with `messageId === undefined`, so the outbound
53
+ * handler falls back to the `send()+parentConversationId` broadcast path.
54
+ */
55
+ /** Sentinel marking our token payload, robust to Eve's `<channel>:` namespacing. */
56
+ const TOKEN_MARKER = "r1.";
57
+ /** base64url (no padding) encode of a UTF-8 string. */
58
+ function b64urlEncode(s) {
59
+ return Buffer.from(s, "utf-8").toString("base64url");
60
+ }
61
+ /** base64url decode to a UTF-8 string (throws on malformed input). */
62
+ function b64urlDecode(s) {
63
+ return Buffer.from(s, "base64url").toString("utf-8");
64
+ }
65
+ /**
66
+ * Encode the raw channel-local continuation token. Eve prepends the channel name
67
+ * (`rine:`); we additionally fence our payload with {@link TOKEN_MARKER} so the
68
+ * decoder can recover it regardless of any prefix the framework adds.
69
+ */
70
+ function encodeReplyToken(conversationId, replyTarget, messageId) {
71
+ const payload = {
72
+ c: conversationId,
73
+ r: replyTarget
74
+ };
75
+ if (messageId) payload.m = messageId;
76
+ return `${TOKEN_MARKER}${b64urlEncode(JSON.stringify(payload))}`;
77
+ }
78
+ /**
79
+ * Decode a continuation token back to its {@link ReplyContext}. Tolerant of a
80
+ * leading `<channel>:` namespace (or any prefix) and of malformed input —
81
+ * returns `null` when the token is not one of ours or fails to parse, so the
82
+ * outbound handler can safely skip rather than throw.
83
+ */
84
+ function decodeReplyToken(token) {
85
+ if (!token) return null;
86
+ const idx = token.indexOf(TOKEN_MARKER);
87
+ if (idx < 0) return null;
88
+ try {
89
+ const payload = JSON.parse(b64urlDecode(token.slice(idx + 3)));
90
+ const conversationId = payload?.c;
91
+ const replyTarget = payload?.r;
92
+ if (typeof conversationId !== "string" || typeof replyTarget !== "string") return null;
93
+ const m = payload?.m;
94
+ return {
95
+ conversationId,
96
+ replyTarget,
97
+ messageId: typeof m === "string" && m.length > 0 ? m : void 0
98
+ };
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * Extract the rine message id from a webhook body. The rine server's standard
105
+ * (`payload_format="rine"`) delivery posts `{ message_id, agent_id, event,
106
+ * timestamp }` (see backend `worker/delivery.py:_build_payload`); the A2A format
107
+ * posts `{ result: { artifactUpdate: { artifact: { artifactId } } } }`. We read
108
+ * `message_id` first, then the A2A artifact id, then tolerate a couple of legacy
109
+ * nesting variants. Returns `undefined` when no id is present (→ 202 ignored).
110
+ */
111
+ function messageIdFromWebhook(body) {
112
+ if (!body || typeof body !== "object") return void 0;
113
+ const b = body;
114
+ const a2aArtifactId = b.result?.artifactUpdate?.artifact?.artifactId;
115
+ const fromMessage = b.message?.id;
116
+ const fromData = b.data?.id;
117
+ const id = b.message_id ?? a2aArtifactId ?? fromMessage ?? fromData ?? b.id;
118
+ return typeof id === "string" && id.length > 0 ? id : void 0;
119
+ }
120
+ //#endregion
121
+ //#region src/channel-core.ts
122
+ /**
123
+ * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
124
+ * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
125
+ * `defineChannel` routes + events.
126
+ *
127
+ * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
128
+ * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
129
+ * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
130
+ * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
131
+ * back over rine IN-PLACE via the reply endpoint, preserving the inbound
132
+ * conversation (R7); falls back to `parentConversationId` for legacy tokens.
133
+ *
134
+ * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
135
+ * (the envelope `kid`), but the SDK exposes only the server-asserted
136
+ * `sender_handle` / `from_agent_id` for routing + display. A malicious relay could
137
+ * misroute a reply (never read it — replies stay E2EE). Binding routing to the
138
+ * verified signer needs an SDK change to surface `senderKid`; tracked as a
139
+ * follow-up. Matches the sibling rine connectors' behavior.
140
+ */
141
+ const SEEN_CAP = 5e3;
142
+ /** The line of context handed to the model so it knows who/where the message is from. */
143
+ function senderContextLine(msg) {
144
+ const where = msg.group_handle ? ` in group ${msg.group_handle}` : "";
145
+ return `Inbound rine message from ${senderLabel(msg)}${where} (type ${msg.type}, ${verifiedNote(msg)}). Your reply is delivered back to the sender over rine.`;
146
+ }
147
+ /** Cap on turns fetched for the push-injected transcript (REQ-SRV-04). */
148
+ const THREAD_FETCH_LIMIT = 50;
149
+ /**
150
+ * The inbound pipeline. Returns the HTTP {@link Response} the route should send.
151
+ * Status codes are retry-aware (R8/#10): `5xx` = transient (caller should retry),
152
+ * `4xx` = terminal drop, `2xx` = accepted/benign-ignore.
153
+ */
154
+ async function processInbound(deps) {
155
+ if (!deps.secret) return new Response("rine channel not configured: RINE_WEBHOOK_SECRET unset", { status: 503 });
156
+ if (deps.raw.length > deps.maxBodyBytes) return new Response("payload too large", { status: 413 });
157
+ if (!verifyRineSignature(deps.raw, deps.signature, deps.secret)) return new Response("invalid signature", { status: 401 });
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(deps.raw.toString("utf-8"));
161
+ } catch {
162
+ return new Response("bad json", { status: 400 });
163
+ }
164
+ const messageId = messageIdFromWebhook(parsed);
165
+ if (!messageId) return new Response("ignored", { status: 202 });
166
+ if (deps.seen?.has(messageId)) return new Response("duplicate", { status: 200 });
167
+ let msg;
168
+ try {
169
+ msg = await deps.client.read(asMessageUuid(messageId));
170
+ } catch {
171
+ return new Response("read failed", { status: 503 });
172
+ }
173
+ if (msg.decrypt_error) return new Response("undecryptable", { status: 422 });
174
+ if (!msg.verified && !deps.acceptUnverified) return new Response("unverified sender signature", { status: 422 });
175
+ if (deps.ignoreTypes.includes(msg.type)) {
176
+ deps.seen?.add(messageId);
177
+ return new Response("ignored type", { status: 202 });
178
+ }
179
+ const replyTarget = msg.group_handle ?? msg.sender_handle;
180
+ if (!replyTarget) return new Response("no reply target", { status: 422 });
181
+ const token = msg.group_handle ? encodeReplyToken(msg.conversation_id, replyTarget) : encodeReplyToken(msg.conversation_id, replyTarget, msg.id);
182
+ await deps.client.markDelivered([msg.id]).catch(() => {});
183
+ if (deps.seen) {
184
+ if (deps.seen.size >= SEEN_CAP) deps.seen.clear();
185
+ deps.seen.add(messageId);
186
+ }
187
+ const context = [senderContextLine(msg)];
188
+ if (deps.threadContextTokenBudget != null) {
189
+ const priorTurns = (await deps.client.thread(msg.conversation_id, { limit: THREAD_FETCH_LIMIT }).catch(() => [])).slice(0, -1);
190
+ if (priorTurns.length > 0) context.push(...renderTranscriptContext(priorTurns, deps.threadContextTokenBudget));
191
+ }
192
+ await deps.send({
193
+ message: renderMessageBody(msg),
194
+ context
195
+ }, {
196
+ auth: null,
197
+ continuationToken: token
198
+ });
199
+ return new Response("ok", { status: 200 });
200
+ }
201
+ /**
202
+ * Outbound for `message.completed`: every TERMINAL assistant message (any
203
+ * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
204
+ * reply. When the continuation token carries the inbound message id we reply
205
+ * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
206
+ * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
207
+ * semantics are exactly as before. Legacy tokens (no message id) fall back to the
208
+ * historical `send()+parentConversationId` path. No-ops for non-rine sessions
209
+ * (token not ours) or empty messages.
210
+ */
211
+ async function processCompletion(deps) {
212
+ if (deps.finishReason === "tool-calls") return;
213
+ if (!deps.message) return;
214
+ const rc = decodeReplyToken(deps.continuationToken);
215
+ if (!rc) return;
216
+ if (rc.messageId) {
217
+ await deps.client.reply(asMessageUuid(rc.messageId), { text: deps.message }, { type: deps.replyMessageType });
218
+ return;
219
+ }
220
+ await deps.client.send(asRecipient(rc.replyTarget), { text: deps.message }, {
221
+ type: deps.replyMessageType,
222
+ parentConversationId: rc.conversationId
223
+ });
224
+ }
225
+ /**
226
+ * Best-effort error notice back to the sender on a terminal session failure.
227
+ * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
228
+ * type; falls back to `send()+parentConversationId` for legacy tokens.
229
+ */
230
+ async function processFailure(continuationToken, client) {
231
+ const rc = decodeReplyToken(continuationToken);
232
+ if (!rc) return;
233
+ const text = "Sorry — I hit an internal error handling your request.";
234
+ if (rc.messageId) {
235
+ await client.reply(asMessageUuid(rc.messageId), { text }, { type: "rine.v1.error" });
236
+ return;
237
+ }
238
+ await client.send(asRecipient(rc.replyTarget), { text }, {
239
+ type: "rine.v1.error",
240
+ parentConversationId: rc.conversationId
241
+ });
242
+ }
243
+ //#endregion
244
+ //#region src/channel.ts
245
+ const DEFAULT_INBOUND_PATH = "/rine/v1/inbound";
246
+ const DEFAULT_REPLY_TYPE = "rine.v1.task_response";
247
+ const DEFAULT_MAX_BODY_BYTES = 512 * 1024;
248
+ /** Default token budget for the push-injected transcript (REQ-CTX-01, ≈8000 chars). */
249
+ const DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET = 2e3;
250
+ /** Message types the connector itself emits — never auto-replied to (loop guard, R2). */
251
+ const DEFAULT_IGNORE_TYPES = [
252
+ "rine.v1.task_response",
253
+ "rine.v1.error",
254
+ "rine.v1.receipt"
255
+ ];
256
+ /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
257
+ function rineChannel(opts = {}) {
258
+ const path = opts.path ?? process.env.RINE_INBOUND_PATH ?? DEFAULT_INBOUND_PATH;
259
+ const replyMessageType = opts.replyMessageType ?? DEFAULT_REPLY_TYPE;
260
+ const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
261
+ const acceptUnverified = opts.acceptUnverified ?? false;
262
+ const ignoreTypes = opts.ignoreTypes ?? DEFAULT_IGNORE_TYPES;
263
+ const threadContextTokenBudget = opts.threadContextTokenBudget ?? DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET;
264
+ const seen = /* @__PURE__ */ new Set();
265
+ const client = () => opts.client ?? getRineClient({
266
+ agent: opts.agent ?? process.env.RINE_AGENT,
267
+ apiUrl: opts.apiUrl,
268
+ configDir: opts.configDir
269
+ });
270
+ return defineChannel({
271
+ routes: [POST(path, async (req, { send }) => processInbound({
272
+ raw: Buffer.from(await req.arrayBuffer()),
273
+ signature: req.headers.get("x-rine-signature") ?? void 0,
274
+ secret: process.env.RINE_WEBHOOK_SECRET,
275
+ client: client(),
276
+ send,
277
+ acceptUnverified,
278
+ maxBodyBytes,
279
+ ignoreTypes,
280
+ seen,
281
+ threadContextTokenBudget
282
+ }))],
283
+ events: {
284
+ "message.completed": async (data, channel) => {
285
+ try {
286
+ await processCompletion({
287
+ finishReason: data.finishReason,
288
+ message: data.message,
289
+ continuationToken: channel.continuationToken,
290
+ client: client(),
291
+ replyMessageType
292
+ });
293
+ } catch {}
294
+ },
295
+ "session.failed": async (_data, channel) => {
296
+ try {
297
+ await processFailure(channel.continuationToken, client());
298
+ } catch {}
299
+ }
300
+ }
301
+ });
302
+ }
303
+ //#endregion
304
+ export { senderContextLine as a, messageIdFromWebhook as c, processInbound as i, processCompletion as n, decodeReplyToken as o, processFailure as r, encodeReplyToken as s, rineChannel as t };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
3
+ * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
4
+ * `defineChannel` routes + events.
5
+ *
6
+ * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
7
+ * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
8
+ * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
9
+ * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
10
+ * back over rine IN-PLACE via the reply endpoint, preserving the inbound
11
+ * conversation (R7); falls back to `parentConversationId` for legacy tokens.
12
+ *
13
+ * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
14
+ * (the envelope `kid`), but the SDK exposes only the server-asserted
15
+ * `sender_handle` / `from_agent_id` for routing + display. A malicious relay could
16
+ * misroute a reply (never read it — replies stay E2EE). Binding routing to the
17
+ * verified signer needs an SDK change to surface `senderKid`; tracked as a
18
+ * follow-up. Matches the sibling rine connectors' behavior.
19
+ */
20
+ import type { AsyncRineClient } from "@rine-network/sdk";
21
+ import type { DecryptedMessage } from "./types.js";
22
+ /** Minimal `send` surface the inbound handler needs (Eve's route `send`). */
23
+ export type ChannelSend = (input: {
24
+ message: string;
25
+ context?: string[];
26
+ }, opts: {
27
+ auth: null;
28
+ continuationToken: string;
29
+ }) => Promise<unknown>;
30
+ /** The line of context handed to the model so it knows who/where the message is from. */
31
+ export declare function senderContextLine(msg: DecryptedMessage): string;
32
+ export interface InboundDeps {
33
+ /** Raw request body bytes. */
34
+ raw: Buffer;
35
+ /** The `x-rine-signature` header value. */
36
+ signature: string | undefined;
37
+ /** The HMAC secret (`RINE_WEBHOOK_SECRET`), or undefined if unset. */
38
+ secret: string | undefined;
39
+ client: AsyncRineClient;
40
+ send: ChannelSend;
41
+ acceptUnverified: boolean;
42
+ maxBodyBytes: number;
43
+ /** Inbound types to ignore (loop guard). */
44
+ ignoreTypes: readonly string[];
45
+ /** Bounded set of already-handled message ids (dedupe across retries/replays). */
46
+ seen?: Set<string>;
47
+ /**
48
+ * Approximate token budget for the role-tagged transcript push-injected into the
49
+ * session context (REQ-CTX-01). When unset, only the sender line is injected.
50
+ */
51
+ threadContextTokenBudget?: number;
52
+ }
53
+ /**
54
+ * The inbound pipeline. Returns the HTTP {@link Response} the route should send.
55
+ * Status codes are retry-aware (R8/#10): `5xx` = transient (caller should retry),
56
+ * `4xx` = terminal drop, `2xx` = accepted/benign-ignore.
57
+ */
58
+ export declare function processInbound(deps: InboundDeps): Promise<Response>;
59
+ export interface CompletionDeps {
60
+ finishReason: string;
61
+ message: string | null;
62
+ continuationToken: string;
63
+ client: AsyncRineClient;
64
+ replyMessageType: string;
65
+ }
66
+ /**
67
+ * Outbound for `message.completed`: every TERMINAL assistant message (any
68
+ * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
69
+ * reply. When the continuation token carries the inbound message id we reply
70
+ * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
71
+ * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
72
+ * semantics are exactly as before. Legacy tokens (no message id) fall back to the
73
+ * historical `send()+parentConversationId` path. No-ops for non-rine sessions
74
+ * (token not ours) or empty messages.
75
+ */
76
+ export declare function processCompletion(deps: CompletionDeps): Promise<void>;
77
+ /**
78
+ * Best-effort error notice back to the sender on a terminal session failure.
79
+ * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
80
+ * type; falls back to `send()+parentConversationId` for legacy tokens.
81
+ */
82
+ export declare function processFailure(continuationToken: string, client: AsyncRineClient): Promise<void>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `rineChannel(opts?)` — the rine channel for Eve. Re-export its result as the
3
+ * default export of `agent/channels/rine.ts` (the `init` scaffolder writes that
4
+ * one-line file). Channels cannot ship as bare npm modules, but the framework's
5
+ * own `slackChannel()` uses exactly this factory-then-re-export shape.
6
+ *
7
+ * It makes an Eve agent a first-class, E2EE-reachable citizen of the rine network:
8
+ * inbound rine webhooks start/resume a durable session; the terminal assistant
9
+ * message is encrypted and threaded back over rine. The actual pipeline lives in
10
+ * `channel-core.ts` (pure, testable); this file is the `defineChannel` wiring.
11
+ */
12
+ import type { AsyncRineClient } from "@rine-network/sdk";
13
+ export { processInbound, processCompletion, processFailure, senderContextLine, } from "./channel-core.js";
14
+ export type { ChannelSend, InboundDeps, CompletionDeps } from "./channel-core.js";
15
+ /** Options for {@link rineChannel}. All optional; identity defaults to env. */
16
+ export interface RineChannelOptions {
17
+ /** Acting agent (handle/name/UUID); defaults to `process.env.RINE_AGENT`. */
18
+ agent?: string;
19
+ apiUrl?: string;
20
+ configDir?: string;
21
+ /** Inbound route path; defaults to `RINE_INBOUND_PATH` env or `/rine/v1/inbound`. */
22
+ path?: string;
23
+ /** Act on messages whose sender signature did NOT verify (default false). */
24
+ acceptUnverified?: boolean;
25
+ /** rine message type used for outbound replies (default `rine.v1.task_response`). */
26
+ replyMessageType?: string;
27
+ /**
28
+ * Inbound message types to ignore (no session, no reply) — the loop guard that
29
+ * stops two rine-eve agents from auto-replying forever. Defaults to the
30
+ * connector's own outbound types.
31
+ */
32
+ ignoreTypes?: readonly string[];
33
+ /** Hard cap on inbound body size before HMAC work (default 512 KiB). */
34
+ maxBodyBytes?: number;
35
+ /**
36
+ * Approximate token budget for the role-tagged recent-transcript push-injected
37
+ * into the stateless Eve session context on each inbound (REQ-CTX-01). Older
38
+ * turns are truncated first with a `[…earlier turns omitted]` marker; a
39
+ * single-message thread injects no extra transcript. Default ~2000 tokens.
40
+ */
41
+ threadContextTokenBudget?: number;
42
+ /** A pre-built client (tests / advanced use); otherwise env-resolved lazily. */
43
+ client?: AsyncRineClient;
44
+ }
45
+ /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
46
+ export declare function rineChannel(opts?: RineChannelOptions): import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
@@ -0,0 +1,2 @@
1
+ import { a as senderContextLine, i as processInbound, n as processCompletion, r as processFailure, t as rineChannel } from "./channel-Bg8l58gg.js";
2
+ export { processCompletion, processFailure, processInbound, rineChannel, senderContextLine };
@@ -0,0 +1,44 @@
1
+ import { AsyncRineClient } from "@rine-network/sdk";
2
+ import { resolveApiUrl, resolveConfigDir } from "@rine-network/core";
3
+ //#region src/client.ts
4
+ /**
5
+ * Lazy `AsyncRineClient` construction (invariant R1: side-effect-free import).
6
+ *
7
+ * No client is built at module load. The SDK client is created the first time a
8
+ * tool's `execute` (or a channel handler) actually fires, then memoized per
9
+ * distinct `(configDir, apiUrl)` pair so every rine surface in one Eve process
10
+ * shares ONE client (the client holds no sockets, so sharing is cheap and keeps
11
+ * the OAuth token cache warm). The acting `agent` is applied per-call via a cheap
12
+ * derived `client.withAgent(...)`, never a separate base client.
13
+ *
14
+ * Two footguns the SDK leaves to the caller are closed here:
15
+ * - `configDir` defaults to `""` in the SDK constructor → it would silently
16
+ * read/write keys against `process.cwd()`. We resolve it via
17
+ * `resolveConfigDir()` (env `RINE_CONFIG_DIR` → `~/.config/rine` → cwd/.rine).
18
+ * - the SDK ignores `RINE_API_URL`; we resolve `apiUrl` via `resolveApiUrl()`.
19
+ */
20
+ const clientCache = /* @__PURE__ */ new Map();
21
+ function cacheKey(configDir, apiUrl) {
22
+ return `${configDir} ${apiUrl}`;
23
+ }
24
+ /**
25
+ * Get (or lazily build + memoize) the shared `AsyncRineClient` for `opts`,
26
+ * already scoped to `opts.agent` when present. Call ONLY from inside a tool
27
+ * `execute` / a channel handler — never at module top level (R1).
28
+ */
29
+ function getRineClient(opts = {}) {
30
+ const configDir = opts.configDir ?? resolveConfigDir();
31
+ const apiUrl = opts.apiUrl ?? resolveApiUrl();
32
+ const key = cacheKey(configDir, apiUrl);
33
+ let base = clientCache.get(key);
34
+ if (!base) {
35
+ base = new AsyncRineClient({
36
+ configDir,
37
+ apiUrl
38
+ });
39
+ clientCache.set(key, base);
40
+ }
41
+ return opts.agent ? base.withAgent(opts.agent) : base;
42
+ }
43
+ //#endregion
44
+ export { getRineClient as t };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Lazy `AsyncRineClient` construction (invariant R1: side-effect-free import).
3
+ *
4
+ * No client is built at module load. The SDK client is created the first time a
5
+ * tool's `execute` (or a channel handler) actually fires, then memoized per
6
+ * distinct `(configDir, apiUrl)` pair so every rine surface in one Eve process
7
+ * shares ONE client (the client holds no sockets, so sharing is cheap and keeps
8
+ * the OAuth token cache warm). The acting `agent` is applied per-call via a cheap
9
+ * derived `client.withAgent(...)`, never a separate base client.
10
+ *
11
+ * Two footguns the SDK leaves to the caller are closed here:
12
+ * - `configDir` defaults to `""` in the SDK constructor → it would silently
13
+ * read/write keys against `process.cwd()`. We resolve it via
14
+ * `resolveConfigDir()` (env `RINE_CONFIG_DIR` → `~/.config/rine` → cwd/.rine).
15
+ * - the SDK ignores `RINE_API_URL`; we resolve `apiUrl` via `resolveApiUrl()`.
16
+ */
17
+ import { AsyncRineClient } from "@rine-network/sdk";
18
+ /** Per-call client overrides; when omitted, env/config resolution applies. */
19
+ export interface RineClientOpts {
20
+ /** Explicit config dir; when omitted, `resolveConfigDir()` is used. */
21
+ configDir?: string;
22
+ /** Explicit API URL; when omitted, `resolveApiUrl()` is used. */
23
+ apiUrl?: string;
24
+ /** Acting agent (handle/name/UUID) sent as `X-Rine-Agent` on every request. */
25
+ agent?: string;
26
+ }
27
+ /**
28
+ * Get (or lazily build + memoize) the shared `AsyncRineClient` for `opts`,
29
+ * already scoped to `opts.agent` when present. Call ONLY from inside a tool
30
+ * `execute` / a channel handler — never at module top level (R1).
31
+ */
32
+ export declare function getRineClient(opts?: RineClientOpts): AsyncRineClient;
33
+ /** Test-only: drop all memoized clients (not part of the public surface). */
34
+ export declare function _resetClientCache(): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `formatError(err)` — invariant R2: turn any thrown SDK error into a readable
3
+ * string for the LLM, never a stack trace. Tools wrap their one `await client.*`
4
+ * call in `try/catch → formatError` and RESOLVE (never reject) for mapped errors.
5
+ *
6
+ * Ordering matters: the TS SDK error classes form a hierarchy rooted at
7
+ * `RineApiError`, so this maps MOST-SPECIFIC FIRST and lets `RineApiError` be the
8
+ * catch-all for API errors.
9
+ */
10
+ /**
11
+ * True when `err` is the plain `Error` `sendAndWait` throws for a group handle.
12
+ * The `send_and_wait` tool checks this BEFORE `formatError` and returns the
13
+ * "1:1 only" guidance; it is not a typed SDK error class.
14
+ */
15
+ export declare function isGroupUnsupportedOnWait(err: unknown): boolean;
16
+ /** The fixed reply for the group-on-`sendAndWait` case. */
17
+ export declare const GROUP_ON_WAIT_MESSAGE = "rine_send_and_wait is 1:1 only; use rine_send for groups.";
18
+ export declare function formatError(err: unknown): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pure render functions: turn SDK return values into the human-readable strings
3
+ * tools + the channel hand to the LLM / send back over rine.
4
+ *
5
+ * Invariant R4: these read ONLY `plaintext` / `decrypt_error` / verification
6
+ * fields. They NEVER read `encrypted_payload` or any envelope/ciphertext field,
7
+ * so ciphertext can never reach the LLM context through a rendered string.
8
+ */
9
+ import type { ThreadEntry } from "@rine-network/sdk";
10
+ import type { AgentProfile, AgentSummary, DecryptedMessage, GroupRead } from "./types.js";
11
+ /** One thread turn as a role-tagged line: `[sent] you: …` / `[received] alice@org: …`. */
12
+ export declare function renderThreadLine(e: ThreadEntry): string;
13
+ /**
14
+ * Render a both-sided transcript (oldest→newest) for `rine_thread`. Each turn is
15
+ * a role-tagged line; `[unavailable]` text passes through unchanged.
16
+ */
17
+ export declare function renderThread(entries: readonly ThreadEntry[]): string;
18
+ /** Honest signature note — never claims "verified" for an unverifiable message. */
19
+ export declare function verifiedNote(msg: DecryptedMessage): string;
20
+ /**
21
+ * THE PLAINTEXT-IS-JSON FOOTGUN. Outbound sends wrap `{ text: body }`, and the
22
+ * SDK auto-`JSON.parse`s inbound `application/json` plaintext into a structured
23
+ * value. Unwrap defensively so the model sees prose, never raw JSON:
24
+ * - a string → returned as-is
25
+ * - `{ text: "…" }` → the inner text
26
+ * - anything else → compact JSON (last resort)
27
+ */
28
+ export declare function unwrapText(plaintext: unknown): string;
29
+ /** Body of a message: decrypt error if unreadable, else the unwrapped plaintext. */
30
+ export declare function renderMessageBody(msg: DecryptedMessage): string;
31
+ /** Sender label: prefer the human handle, fall back to the agent UUID. */
32
+ export declare function senderLabel(msg: DecryptedMessage): string;
33
+ /** A single message rendered across multiple labeled lines (for `rine_read`). */
34
+ export declare function renderSingleMessage(msg: DecryptedMessage): string;
35
+ /** A numbered inbox list, or the empty-state line. */
36
+ export declare function renderInbox(items: readonly DecryptedMessage[]): string;
37
+ /** A numbered discovery list, or the empty-state line. */
38
+ export declare function renderDiscover(items: readonly AgentSummary[]): string;
39
+ /** A full agent profile (for `rine_inspect`). */
40
+ export declare function renderProfile(p: AgentProfile): string;
41
+ /**
42
+ * Self-diagnose a group's E2EE mode. Uses `mls_group_id !== null`, OR the
43
+ * explicit `mls_enabled`/`mls_pending` flags. With MLS support present this is a
44
+ * CAPABILITY flag, not a failure flag (R9).
45
+ */
46
+ export declare function groupIsMls(g: GroupRead): boolean;
47
+ /**
48
+ * A group rendered for `rine_group_inspect`. Both the MLS and sender-key branches
49
+ * are `[OK]` — an MLS group is readable/postable from here (R9).
50
+ */
51
+ export declare function renderGroup(g: GroupRead): string;
@@ -0,0 +1,24 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ //#region src/hmac.ts
3
+ /**
4
+ * Verify a rine standard-webhook signature: `X-Rine-Signature: sha256=<hex>`,
5
+ * where hex = HMAC-SHA256(rawBody, secret). Constant-time compare.
6
+ *
7
+ * This is the TRANSPORT-auth check (the POST genuinely came from rine and was not
8
+ * tampered). It is independent of the per-message Ed25519 sender signature, which
9
+ * the channel verifies separately after decryption (`DecryptedMessage.verified`).
10
+ */
11
+ function verifyRineSignature(rawBody, header, secret) {
12
+ if (!header || !secret) return false;
13
+ const expected = `sha256=${createHmac("sha256", secret).update(typeof rawBody === "string" ? Buffer.from(rawBody) : rawBody).digest("hex")}`;
14
+ const a = Buffer.from(expected);
15
+ const b = Buffer.from(header);
16
+ if (a.length !== b.length) return false;
17
+ return timingSafeEqual(a, b);
18
+ }
19
+ /** Sign a raw body the way the rine server (and the dev relay) do. */
20
+ function signRineBody(rawBody, secret) {
21
+ return `sha256=${createHmac("sha256", secret).update(typeof rawBody === "string" ? Buffer.from(rawBody) : rawBody).digest("hex")}`;
22
+ }
23
+ //#endregion
24
+ export { verifyRineSignature as n, signRineBody as t };
package/dist/hmac.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Verify a rine standard-webhook signature: `X-Rine-Signature: sha256=<hex>`,
3
+ * where hex = HMAC-SHA256(rawBody, secret). Constant-time compare.
4
+ *
5
+ * This is the TRANSPORT-auth check (the POST genuinely came from rine and was not
6
+ * tampered). It is independent of the per-message Ed25519 sender signature, which
7
+ * the channel verifies separately after decryption (`DecryptedMessage.verified`).
8
+ */
9
+ export declare function verifyRineSignature(rawBody: Buffer | string, header: string | undefined, secret: string): boolean;
10
+ /** Sign a raw body the way the rine server (and the dev relay) do. */
11
+ export declare function signRineBody(rawBody: Buffer | string, secret: string): string;