@rine-network/sdk 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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +291 -0
- package/README.md +127 -0
- package/dist/_parity.d.ts +34 -0
- package/dist/agent.d.ts +115 -0
- package/dist/api/adapters.d.ts +47 -0
- package/dist/api/http.d.ts +164 -0
- package/dist/api/middleware.d.ts +100 -0
- package/dist/client.d.ts +523 -0
- package/dist/errors.d.ts +80 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +3028 -0
- package/dist/onboard.d.ts +40 -0
- package/dist/resources/conversation.d.ts +111 -0
- package/dist/resources/decrypt.d.ts +44 -0
- package/dist/resources/discovery.d.ts +66 -0
- package/dist/resources/groups.d.ts +146 -0
- package/dist/resources/identity.d.ts +272 -0
- package/dist/resources/messages.d.ts +110 -0
- package/dist/resources/polling.d.ts +43 -0
- package/dist/resources/streams.d.ts +50 -0
- package/dist/resources/webhooks.d.ts +92 -0
- package/dist/types.d.ts +1362 -0
- package/dist/utils/abort.d.ts +40 -0
- package/dist/utils/cursor-page.d.ts +23 -0
- package/dist/utils/schema.d.ts +58 -0
- package/dist/utils/seen-ids.d.ts +23 -0
- package/dist/utils/sleep.d.ts +10 -0
- package/dist/utils/sse.d.ts +28 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3028 @@
|
|
|
1
|
+
import { HttpClient, decryptGroupMessage, decryptMessage, encryptGroupMessage, encryptMessage, encryptionPublicKeyToJWK, fetchAgents, fetchAndIngestPendingSKDistributions, fetchRecipientEncryptionKey, generateAgentKeys, getOrCreateSenderKey, getOrRefreshToken, loadCredentials, performRegistration, resolveAgent, resolveToUuid, saveAgentKeys, saveCredentials, signingPublicKeyToJWK, validateSlug } from "@rine-network/core";
|
|
2
|
+
import { z, z as z$1 } from "zod";
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed error hierarchy for the Rine SDK.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the Python SDK error hierarchy (rine/errors.py) but adapted for TypeScript.
|
|
8
|
+
* All error classes are compatible with the `raiseForStatus()` pattern.
|
|
9
|
+
*/
|
|
10
|
+
/** Base exception for all Rine SDK errors. */
|
|
11
|
+
var RineError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "RineError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** HTTP API error with status code and detail. */
|
|
18
|
+
var RineApiError = class extends RineError {
|
|
19
|
+
constructor(status, detail, raw) {
|
|
20
|
+
super(`${status}: ${detail}`);
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.detail = detail;
|
|
23
|
+
this.raw = raw;
|
|
24
|
+
this.name = "RineApiError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** 401 — invalid or missing credentials. */
|
|
28
|
+
var AuthenticationError = class extends RineApiError {
|
|
29
|
+
constructor(detail = "Authentication failed", raw) {
|
|
30
|
+
super(401, detail, raw);
|
|
31
|
+
this.name = "AuthenticationError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** 403 — insufficient permissions for the requested operation. */
|
|
35
|
+
var AuthorizationError = class extends RineApiError {
|
|
36
|
+
constructor(detail = "Permission denied", raw) {
|
|
37
|
+
super(403, detail, raw);
|
|
38
|
+
this.name = "AuthorizationError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** 404 — the requested resource was not found. */
|
|
42
|
+
var NotFoundError = class extends RineApiError {
|
|
43
|
+
constructor(detail = "Resource not found", raw) {
|
|
44
|
+
super(404, detail, raw);
|
|
45
|
+
this.name = "NotFoundError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** 409 — resource conflict (e.g., duplicate slug or agent name). */
|
|
49
|
+
var ConflictError = class extends RineApiError {
|
|
50
|
+
constructor(detail = "Conflict", raw) {
|
|
51
|
+
super(409, detail, raw);
|
|
52
|
+
this.name = "ConflictError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** 429 — rate limit exceeded. */
|
|
56
|
+
var RateLimitError = class extends RineApiError {
|
|
57
|
+
retryAfter;
|
|
58
|
+
constructor(detail, retryAfter, raw) {
|
|
59
|
+
super(429, detail, raw);
|
|
60
|
+
this.name = "RateLimitError";
|
|
61
|
+
this.retryAfter = retryAfter;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** 422 — request validation failed. */
|
|
65
|
+
var ValidationError = class extends RineApiError {
|
|
66
|
+
constructor(detail = "Validation error", raw) {
|
|
67
|
+
super(422, detail, raw);
|
|
68
|
+
this.name = "ValidationError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** 500 — unexpected server error. */
|
|
72
|
+
var InternalServerError = class extends RineApiError {
|
|
73
|
+
constructor(detail = "Internal server error", raw) {
|
|
74
|
+
super(500, detail, raw);
|
|
75
|
+
this.name = "InternalServerError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/** 503 — service temporarily unavailable. */
|
|
79
|
+
var ServiceUnavailableError = class extends RineApiError {
|
|
80
|
+
constructor(detail = "Service unavailable", raw) {
|
|
81
|
+
super(503, detail, raw);
|
|
82
|
+
this.name = "ServiceUnavailableError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** Request timed out — the server did not respond in time. */
|
|
86
|
+
var RineTimeoutError = class extends RineError {
|
|
87
|
+
constructor(message = "Request timed out") {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "RineTimeoutError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/** Network-level failure — could not reach the server. */
|
|
93
|
+
var APIConnectionError = class extends RineError {
|
|
94
|
+
cause;
|
|
95
|
+
constructor(message = "Connection failed", cause) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "APIConnectionError";
|
|
98
|
+
this.cause = cause;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/** Encryption or decryption failure. */
|
|
102
|
+
var CryptoError = class extends RineError {
|
|
103
|
+
cause;
|
|
104
|
+
constructor(message, cause) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "CryptoError";
|
|
107
|
+
this.cause = cause;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/** Missing or invalid SDK configuration. */
|
|
111
|
+
var ConfigError = class extends RineError {
|
|
112
|
+
constructor(message) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "ConfigError";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
/** Client-side schema validation failure (Standard Schema v1). */
|
|
118
|
+
var SchemaValidationError = class extends RineError {
|
|
119
|
+
constructor(message) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.name = "SchemaValidationError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const STATUS_MAP = new Map([
|
|
125
|
+
[401, AuthenticationError],
|
|
126
|
+
[403, AuthorizationError],
|
|
127
|
+
[404, NotFoundError],
|
|
128
|
+
[409, ConflictError],
|
|
129
|
+
[422, ValidationError],
|
|
130
|
+
[500, InternalServerError],
|
|
131
|
+
[503, ServiceUnavailableError]
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* Raise the appropriate error class for an HTTP status code.
|
|
135
|
+
*
|
|
136
|
+
* @param status - HTTP status code.
|
|
137
|
+
* @param detail - Error detail string from the server.
|
|
138
|
+
* @param raw - Optional raw response body for debugging.
|
|
139
|
+
*/
|
|
140
|
+
function raiseForStatus(status, detail, raw) {
|
|
141
|
+
if (status === 429) throw new RateLimitError(detail, raw?.headers?.get("retry-after") ? Number.parseInt(raw.headers.get("retry-after"), 10) : void 0, raw);
|
|
142
|
+
const cls = STATUS_MAP.get(status);
|
|
143
|
+
if (cls) throw new cls(detail, raw);
|
|
144
|
+
throw new RineApiError(status, detail, raw);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/onboard.ts
|
|
148
|
+
/**
|
|
149
|
+
* Standalone org registration — REQ-01, REQ-02, REQ-03.
|
|
150
|
+
*
|
|
151
|
+
* Module-level function (not a client method) because credentials don't
|
|
152
|
+
* exist yet at registration time. Delegates to rine-core's
|
|
153
|
+
* `performRegistration` for the PoW + credential persistence flow.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Register a new org on the Rine network.
|
|
157
|
+
*
|
|
158
|
+
* Solves the RSA time-lock proof-of-work, saves credentials to
|
|
159
|
+
* `{configDir}/credentials.json`, and caches the initial OAuth token.
|
|
160
|
+
*
|
|
161
|
+
* @throws {ValidationError} If the slug fails client-side validation.
|
|
162
|
+
* @throws {ConflictError} If the email or slug is already registered.
|
|
163
|
+
* @throws {RateLimitError} On 429 from the server.
|
|
164
|
+
* @throws {RineError} On PoW challenge expiry or other failures.
|
|
165
|
+
*/
|
|
166
|
+
async function register(opts) {
|
|
167
|
+
if (!validateSlug(opts.slug)) throw new ValidationError(`Invalid slug "${opts.slug}" — must be 2-32 lowercase alphanumeric chars or hyphens, no leading/trailing hyphen.`);
|
|
168
|
+
try {
|
|
169
|
+
const result = await performRegistration(opts.apiUrl, opts.configDir, "default", {
|
|
170
|
+
email: opts.email,
|
|
171
|
+
slug: opts.slug,
|
|
172
|
+
name: opts.name
|
|
173
|
+
}, opts.onProgress);
|
|
174
|
+
return {
|
|
175
|
+
orgId: result.org_id,
|
|
176
|
+
clientId: result.client_id
|
|
177
|
+
};
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof Error) {
|
|
180
|
+
const msg = err.message;
|
|
181
|
+
if (msg.includes("already registered") || msg.includes("Conflict")) throw new ConflictError(msg);
|
|
182
|
+
if (msg.includes("Rate limited")) throw new RateLimitError(msg);
|
|
183
|
+
if (msg.includes("expired")) throw new RineError(msg);
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/types.ts
|
|
190
|
+
/**
|
|
191
|
+
* Branded domain types + Zod schemas co-located.
|
|
192
|
+
*
|
|
193
|
+
* Branded types prevent mixing handles, UUIDs, and group IDs at compile time.
|
|
194
|
+
* Zod schemas serve as single source of truth for both runtime parsing and
|
|
195
|
+
* static type inference (via `z.infer<typeof schema>`).
|
|
196
|
+
*
|
|
197
|
+
* NOTE: All datetime fields use ISO-8601 strings (not Date objects) to match
|
|
198
|
+
* the Python SDK's Pydantic datetime serialisation and the API wire format.
|
|
199
|
+
*/
|
|
200
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
201
|
+
const AGENT_HANDLE_RE = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.+-]+$/;
|
|
202
|
+
const GROUP_HANDLE_RE = /^#[a-zA-Z0-9_-]+@[a-zA-Z0-9_.+-]+$/;
|
|
203
|
+
function isAgentHandle(s) {
|
|
204
|
+
return AGENT_HANDLE_RE.test(s);
|
|
205
|
+
}
|
|
206
|
+
function isGroupHandle(s) {
|
|
207
|
+
return GROUP_HANDLE_RE.test(s);
|
|
208
|
+
}
|
|
209
|
+
function asAgentUuid(s) {
|
|
210
|
+
if (!UUID_RE.test(s)) throw new TypeError(`Invalid UUID: ${s}`);
|
|
211
|
+
return s;
|
|
212
|
+
}
|
|
213
|
+
function asGroupUuid(s) {
|
|
214
|
+
if (!UUID_RE.test(s)) throw new TypeError(`Invalid UUID: ${s}`);
|
|
215
|
+
return s;
|
|
216
|
+
}
|
|
217
|
+
function asMessageUuid(s) {
|
|
218
|
+
if (!UUID_RE.test(s)) throw new TypeError(`Invalid UUID: ${s}`);
|
|
219
|
+
return s;
|
|
220
|
+
}
|
|
221
|
+
function asOrgUuid(s) {
|
|
222
|
+
if (!UUID_RE.test(s)) throw new TypeError(`Invalid UUID: ${s}`);
|
|
223
|
+
return s;
|
|
224
|
+
}
|
|
225
|
+
function asWebhookUuid(s) {
|
|
226
|
+
if (!UUID_RE.test(s)) throw new TypeError(`Invalid UUID: ${s}`);
|
|
227
|
+
return s;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Canonical rine message type strings. Derived from `PROTOCOL.md §458-474`
|
|
231
|
+
* and the Python SDK constants (`rine-sdk/src/rine/_constants.py:48`).
|
|
232
|
+
*
|
|
233
|
+
* Use these rather than raw strings — IDE autocomplete + compile-time
|
|
234
|
+
* validation + no typo-drift between consumers.
|
|
235
|
+
*/
|
|
236
|
+
const MessageType = {
|
|
237
|
+
Dm: "rine.v1.dm",
|
|
238
|
+
TaskRequest: "rine.v1.task_request",
|
|
239
|
+
TaskResponse: "rine.v1.task_response",
|
|
240
|
+
StatusUpdate: "rine.v1.status_update",
|
|
241
|
+
Negotiation: "rine.v1.negotiation",
|
|
242
|
+
Receipt: "rine.v1.receipt",
|
|
243
|
+
ErrorNotice: "rine.v1.error",
|
|
244
|
+
CapabilityQuery: "rine.v1.capability_query",
|
|
245
|
+
CapabilityResponse: "rine.v1.capability_response",
|
|
246
|
+
PaymentRequest: "rine.v1.payment_request",
|
|
247
|
+
PaymentConfirmation: "rine.v1.payment_confirmation",
|
|
248
|
+
ConsentRequest: "rine.v1.consent_request",
|
|
249
|
+
ConsentGrant: "rine.v1.consent_grant",
|
|
250
|
+
ConsentRevoke: "rine.v1.consent_revoke",
|
|
251
|
+
IdentityVerification: "rine.v1.identity_verification",
|
|
252
|
+
SenderKeyDistribution: "rine.v1.sender_key_distribution",
|
|
253
|
+
SenderKeyRequest: "rine.v1.sender_key_request",
|
|
254
|
+
GroupInvite: "rine.v1.group_invite",
|
|
255
|
+
Text: "rine.v1.text",
|
|
256
|
+
A2AMessage: "a2a.v1.message"
|
|
257
|
+
};
|
|
258
|
+
const ConversationStatus = {
|
|
259
|
+
Submitted: "submitted",
|
|
260
|
+
Open: "open",
|
|
261
|
+
Paused: "paused",
|
|
262
|
+
InputRequired: "input_required",
|
|
263
|
+
Completed: "completed",
|
|
264
|
+
Rejected: "rejected",
|
|
265
|
+
Canceled: "canceled",
|
|
266
|
+
Failed: "failed"
|
|
267
|
+
};
|
|
268
|
+
const JoinRequestStatus = {
|
|
269
|
+
Pending: "pending",
|
|
270
|
+
Approved: "approved",
|
|
271
|
+
Denied: "denied",
|
|
272
|
+
Expired: "expired"
|
|
273
|
+
};
|
|
274
|
+
const VoteChoice = {
|
|
275
|
+
Approve: "approve",
|
|
276
|
+
Deny: "deny"
|
|
277
|
+
};
|
|
278
|
+
const WebhookJobStatus = {
|
|
279
|
+
Pending: "pending",
|
|
280
|
+
Processing: "processing",
|
|
281
|
+
Delivered: "delivered",
|
|
282
|
+
Failed: "failed",
|
|
283
|
+
Dead: "dead"
|
|
284
|
+
};
|
|
285
|
+
const EncryptionVersion = {
|
|
286
|
+
HpkeV1: "hpke-v1",
|
|
287
|
+
SenderKeyV1: "sender-key-v1"
|
|
288
|
+
};
|
|
289
|
+
const MESSAGE_TYPE_RE = /^[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*){2,}$/;
|
|
290
|
+
/** Validate a custom message type against the server-side regex. */
|
|
291
|
+
function isValidMessageType(s) {
|
|
292
|
+
return MESSAGE_TYPE_RE.test(s);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Message types the E2EE layer consumes internally and that must NEVER
|
|
296
|
+
* surface to user code. Enforced by `client.messages()` and `defineAgent`
|
|
297
|
+
* via the `includeReserved` debug escape hatch (SPEC D6d).
|
|
298
|
+
*/
|
|
299
|
+
const RESERVED_MESSAGE_TYPES = new Set([MessageType.SenderKeyDistribution]);
|
|
300
|
+
const OrgReadSchema = z$1.object({
|
|
301
|
+
id: z$1.string().uuid(),
|
|
302
|
+
name: z$1.string(),
|
|
303
|
+
slug: z$1.string().nullable().optional(),
|
|
304
|
+
contact_email: z$1.string().nullable().optional(),
|
|
305
|
+
country_code: z$1.string().nullable().optional(),
|
|
306
|
+
trust_tier: z$1.number().int().default(0),
|
|
307
|
+
agent_count: z$1.number().int().default(0),
|
|
308
|
+
created_at: z$1.string().datetime()
|
|
309
|
+
});
|
|
310
|
+
const AgentReadSchema = z$1.object({
|
|
311
|
+
id: z$1.string().uuid(),
|
|
312
|
+
name: z$1.string(),
|
|
313
|
+
handle: z$1.string(),
|
|
314
|
+
human_oversight: z$1.boolean().default(true),
|
|
315
|
+
incoming_policy: z$1.string().default("accept_all"),
|
|
316
|
+
outgoing_policy: z$1.string().default("send_all"),
|
|
317
|
+
created_at: z$1.string().datetime(),
|
|
318
|
+
revoked_at: z$1.string().datetime().nullable().optional(),
|
|
319
|
+
unlisted: z$1.boolean().optional(),
|
|
320
|
+
verification_words: z$1.string().nullable().optional(),
|
|
321
|
+
warnings: z$1.array(z$1.string()).nullish(),
|
|
322
|
+
poll_url: z$1.string().nullable().optional()
|
|
323
|
+
});
|
|
324
|
+
const WhoAmISchema = z$1.object({
|
|
325
|
+
org: OrgReadSchema,
|
|
326
|
+
agents: z$1.array(AgentReadSchema),
|
|
327
|
+
trust_tier: z$1.number().int().default(0)
|
|
328
|
+
});
|
|
329
|
+
const MessageReadSchema = z$1.object({
|
|
330
|
+
id: z$1.string().uuid(),
|
|
331
|
+
conversation_id: z$1.string().uuid(),
|
|
332
|
+
from_agent_id: z$1.string().uuid().nullable().optional(),
|
|
333
|
+
to_agent_id: z$1.string().uuid().nullable().optional(),
|
|
334
|
+
sender_handle: z$1.string().nullable().optional(),
|
|
335
|
+
recipient_handle: z$1.string().nullable().optional(),
|
|
336
|
+
type: z$1.string(),
|
|
337
|
+
encrypted_payload: z$1.string(),
|
|
338
|
+
encryption_version: z$1.string(),
|
|
339
|
+
sender_signing_kid: z$1.string().nullable().optional(),
|
|
340
|
+
content_type: z$1.string().nullable().optional(),
|
|
341
|
+
metadata: z$1.record(z$1.unknown()).default({}),
|
|
342
|
+
created_at: z$1.string().datetime(),
|
|
343
|
+
delivered_at: z$1.string().datetime().nullable().optional(),
|
|
344
|
+
read_at: z$1.string().datetime().nullable().optional(),
|
|
345
|
+
group_id: z$1.string().uuid().nullable().optional(),
|
|
346
|
+
group_handle: z$1.string().nullable().optional()
|
|
347
|
+
});
|
|
348
|
+
const DecryptedMessageSchema = MessageReadSchema.extend({
|
|
349
|
+
plaintext: z$1.unknown().nullable().optional(),
|
|
350
|
+
verified: z$1.boolean().default(false),
|
|
351
|
+
verification_status: z$1.enum([
|
|
352
|
+
"verified",
|
|
353
|
+
"invalid",
|
|
354
|
+
"unverifiable"
|
|
355
|
+
]).default("unverifiable"),
|
|
356
|
+
decrypt_error: z$1.string().nullable().optional()
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* Wire-format Zod schema for `{ sent, reply }` — retained for symmetry with
|
|
360
|
+
* the other `*Schema` exports even though the resource layer no longer calls
|
|
361
|
+
* `parse(SendAndWaitResultSchema, …)` directly (Step 19 replaced that with
|
|
362
|
+
* an explicit object literal so the generic `Rep` narrowing could flow
|
|
363
|
+
* through). The hand-rolled `SendAndWaitResult<Rep>` below is the type
|
|
364
|
+
* `client.sendAndWait<Req, Rep>()` returns; the two are structurally
|
|
365
|
+
* equivalent at `Rep = unknown` but the Zod inference does not carry
|
|
366
|
+
* the generic, so consumers that want typed plaintext should rely on
|
|
367
|
+
* the function return type, not `z.infer<typeof SendAndWaitResultSchema>`.
|
|
368
|
+
*/
|
|
369
|
+
const SendAndWaitResultSchema = z$1.object({
|
|
370
|
+
sent: MessageReadSchema,
|
|
371
|
+
reply: DecryptedMessageSchema.nullable()
|
|
372
|
+
});
|
|
373
|
+
const GroupReadSchema = z$1.object({
|
|
374
|
+
id: z$1.string().uuid(),
|
|
375
|
+
name: z$1.string(),
|
|
376
|
+
handle: z$1.string(),
|
|
377
|
+
description: z$1.string().nullable().optional(),
|
|
378
|
+
enrollment_policy: z$1.string().default("closed"),
|
|
379
|
+
visibility: z$1.string().default("private"),
|
|
380
|
+
isolated: z$1.boolean().default(false),
|
|
381
|
+
vote_duration_hours: z$1.number().int().default(72),
|
|
382
|
+
member_count: z$1.number().int().default(0),
|
|
383
|
+
created_at: z$1.string().datetime()
|
|
384
|
+
});
|
|
385
|
+
const GroupMemberSchema = z$1.object({
|
|
386
|
+
id: z$1.string().uuid().nullable().optional(),
|
|
387
|
+
group_id: z$1.string().uuid(),
|
|
388
|
+
agent_id: z$1.string().uuid(),
|
|
389
|
+
role: z$1.string().default("member"),
|
|
390
|
+
joined_at: z$1.string().datetime(),
|
|
391
|
+
agent_handle: z$1.string().nullable().optional()
|
|
392
|
+
});
|
|
393
|
+
const JoinRequestReadSchema = z$1.object({
|
|
394
|
+
id: z$1.string().uuid(),
|
|
395
|
+
group_id: z$1.string().uuid(),
|
|
396
|
+
agent_id: z$1.string().uuid(),
|
|
397
|
+
invited_by: z$1.string().uuid().nullable().optional(),
|
|
398
|
+
message: z$1.string().nullable().optional(),
|
|
399
|
+
status: z$1.string(),
|
|
400
|
+
expires_at: z$1.string().datetime().nullable().optional(),
|
|
401
|
+
created_at: z$1.string().datetime(),
|
|
402
|
+
resolved_at: z$1.string().datetime().nullable().optional(),
|
|
403
|
+
your_vote: z$1.string().nullable().optional()
|
|
404
|
+
});
|
|
405
|
+
const JoinedResultSchema = z$1.object({
|
|
406
|
+
status: z$1.literal("joined"),
|
|
407
|
+
member: GroupMemberSchema
|
|
408
|
+
});
|
|
409
|
+
const PendingJoinResultSchema = z$1.object({
|
|
410
|
+
status: z$1.literal("pending"),
|
|
411
|
+
request: JoinRequestReadSchema
|
|
412
|
+
});
|
|
413
|
+
const JoinResultSchema = z$1.discriminatedUnion("status", [JoinedResultSchema, PendingJoinResultSchema]);
|
|
414
|
+
const InviteResultSchema = z$1.object({
|
|
415
|
+
status: z$1.string(),
|
|
416
|
+
request_id: z$1.string().uuid().nullable().optional()
|
|
417
|
+
});
|
|
418
|
+
const GroupSummarySchema = z$1.object({
|
|
419
|
+
id: z$1.string().uuid(),
|
|
420
|
+
name: z$1.string(),
|
|
421
|
+
handle: z$1.string(),
|
|
422
|
+
description: z$1.string().nullable().optional(),
|
|
423
|
+
visibility: z$1.string().default("public"),
|
|
424
|
+
member_count: z$1.number().int().default(0)
|
|
425
|
+
});
|
|
426
|
+
const VoteResponseSchema = z$1.object({
|
|
427
|
+
request_id: z$1.string().uuid(),
|
|
428
|
+
your_vote: z$1.string(),
|
|
429
|
+
status: z$1.string()
|
|
430
|
+
});
|
|
431
|
+
const AgentSummarySchema = z$1.object({
|
|
432
|
+
id: z$1.string().uuid(),
|
|
433
|
+
name: z$1.string(),
|
|
434
|
+
handle: z$1.string(),
|
|
435
|
+
description: z$1.string().nullable().optional(),
|
|
436
|
+
category: z$1.string().nullable().optional(),
|
|
437
|
+
verified: z$1.boolean().default(false)
|
|
438
|
+
});
|
|
439
|
+
const AgentProfileSchema = z$1.object({
|
|
440
|
+
id: z$1.string().uuid(),
|
|
441
|
+
name: z$1.string(),
|
|
442
|
+
handle: z$1.string(),
|
|
443
|
+
description: z$1.string().nullable().optional(),
|
|
444
|
+
category: z$1.string().nullable().optional(),
|
|
445
|
+
verified: z$1.boolean().default(false),
|
|
446
|
+
human_oversight: z$1.boolean().default(true),
|
|
447
|
+
created_at: z$1.string().datetime().nullable().optional()
|
|
448
|
+
});
|
|
449
|
+
const AgentCardSchema = z$1.object({
|
|
450
|
+
id: z$1.string().uuid(),
|
|
451
|
+
agent_id: z$1.string().uuid(),
|
|
452
|
+
name: z$1.string(),
|
|
453
|
+
description: z$1.string().nullable().optional(),
|
|
454
|
+
version: z$1.string().nullable().optional(),
|
|
455
|
+
is_public: z$1.boolean().default(false),
|
|
456
|
+
skills: z$1.array(z$1.record(z$1.unknown())).optional().default([]),
|
|
457
|
+
rine: z$1.record(z$1.unknown()).optional().default({}),
|
|
458
|
+
created_at: z$1.string().datetime(),
|
|
459
|
+
updated_at: z$1.string().datetime().nullable().optional()
|
|
460
|
+
});
|
|
461
|
+
const WebhookReadSchema = z$1.object({
|
|
462
|
+
id: z$1.string().uuid(),
|
|
463
|
+
agent_id: z$1.string().uuid(),
|
|
464
|
+
url: z$1.string(),
|
|
465
|
+
active: z$1.boolean().default(true),
|
|
466
|
+
created_at: z$1.string().datetime()
|
|
467
|
+
});
|
|
468
|
+
const WebhookCreatedSchema = WebhookReadSchema.extend({ secret: z$1.string() });
|
|
469
|
+
const WebhookDeliveryReadSchema = z$1.object({
|
|
470
|
+
id: z$1.string().uuid(),
|
|
471
|
+
webhook_id: z$1.string().uuid(),
|
|
472
|
+
message_id: z$1.string().uuid(),
|
|
473
|
+
status: z$1.string(),
|
|
474
|
+
attempts: z$1.number().int().default(0),
|
|
475
|
+
max_attempts: z$1.number().int().default(5),
|
|
476
|
+
last_error: z$1.string().nullable().optional(),
|
|
477
|
+
created_at: z$1.string().datetime(),
|
|
478
|
+
delivered_at: z$1.string().datetime().nullable().optional(),
|
|
479
|
+
next_attempt_at: z$1.string().datetime().nullable().optional()
|
|
480
|
+
});
|
|
481
|
+
const WebhookJobSummarySchema = z$1.object({
|
|
482
|
+
total: z$1.number().int().default(0),
|
|
483
|
+
delivered: z$1.number().int().default(0),
|
|
484
|
+
failed: z$1.number().int().default(0),
|
|
485
|
+
dead: z$1.number().int().default(0),
|
|
486
|
+
pending: z$1.number().int().default(0),
|
|
487
|
+
processing: z$1.number().int().default(0)
|
|
488
|
+
});
|
|
489
|
+
const QuotaEntrySchema = z$1.object({
|
|
490
|
+
limit: z$1.number().int().nullable(),
|
|
491
|
+
used: z$1.number().int().nullable().optional()
|
|
492
|
+
});
|
|
493
|
+
const OrgQuotasSchema = z$1.object({
|
|
494
|
+
tier: z$1.number().int(),
|
|
495
|
+
quotas: z$1.record(QuotaEntrySchema)
|
|
496
|
+
});
|
|
497
|
+
const PollTokenResponseSchema = z$1.object({ poll_url: z$1.string() });
|
|
498
|
+
const RineEventSchema = z$1.object({
|
|
499
|
+
type: z$1.enum([
|
|
500
|
+
"message",
|
|
501
|
+
"heartbeat",
|
|
502
|
+
"status",
|
|
503
|
+
"disconnect"
|
|
504
|
+
]),
|
|
505
|
+
data: z$1.string(),
|
|
506
|
+
id: z$1.string().nullable().optional()
|
|
507
|
+
});
|
|
508
|
+
const ErasureResultSchema = z$1.object({
|
|
509
|
+
org_id: z$1.string().uuid(),
|
|
510
|
+
erased_at: z$1.string().datetime(),
|
|
511
|
+
messages_deleted: z$1.number().int(),
|
|
512
|
+
agents_deleted: z$1.number().int(),
|
|
513
|
+
conversations_deleted: z$1.number().int(),
|
|
514
|
+
groups_deleted: z$1.number().int().default(0)
|
|
515
|
+
});
|
|
516
|
+
const PaginatedResponseSchema = (itemSchema) => z$1.object({
|
|
517
|
+
items: z$1.array(itemSchema),
|
|
518
|
+
total_estimate: z$1.number().int().nullish(),
|
|
519
|
+
total: z$1.number().int().nullish(),
|
|
520
|
+
next_cursor: z$1.string().nullable().optional(),
|
|
521
|
+
prev_cursor: z$1.string().nullable().optional()
|
|
522
|
+
});
|
|
523
|
+
/**
|
|
524
|
+
* CursorPage raw response schema — used by adapters.parsePaginated().
|
|
525
|
+
* This is NOT a generic schema; it accepts a Zod schema for the item type
|
|
526
|
+
* and returns the full paginated response schema.
|
|
527
|
+
*/
|
|
528
|
+
function cursorPageSchema(itemSchema) {
|
|
529
|
+
return PaginatedResponseSchema(itemSchema);
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/utils/cursor-page.ts
|
|
533
|
+
/**
|
|
534
|
+
* CursorPage<T> — async iterable + hasNext/hasPrev.
|
|
535
|
+
*
|
|
536
|
+
* Mirrors the Python SDK's CursorPage[T] pattern. The caller-passing
|
|
537
|
+
* `autoPaginate(fetchPage)` method was deleted per SPEC §13.3 — use the
|
|
538
|
+
* dedicated `client.inboxAll()` / `client.discoverAll()` / `client.discoverGroupsAll()`
|
|
539
|
+
* async generators instead, which own their fetch loop.
|
|
540
|
+
*/
|
|
541
|
+
/**
|
|
542
|
+
* Cursor-paginated response.
|
|
543
|
+
*
|
|
544
|
+
* @typeParam T - Item type (DecryptedMessage, AgentSummary, etc.)
|
|
545
|
+
*/
|
|
546
|
+
var CursorPage = class {
|
|
547
|
+
constructor(items, total, nextCursor, prevCursor) {
|
|
548
|
+
this.items = items;
|
|
549
|
+
this.total = total;
|
|
550
|
+
this.nextCursor = nextCursor;
|
|
551
|
+
this.prevCursor = prevCursor;
|
|
552
|
+
}
|
|
553
|
+
get hasNext() {
|
|
554
|
+
return this.nextCursor != null;
|
|
555
|
+
}
|
|
556
|
+
get hasPrev() {
|
|
557
|
+
return this.prevCursor != null;
|
|
558
|
+
}
|
|
559
|
+
async *[Symbol.asyncIterator]() {
|
|
560
|
+
yield* this.items;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/api/adapters.ts
|
|
565
|
+
/**
|
|
566
|
+
* Parse a raw API response through a Zod schema.
|
|
567
|
+
* Throws ZodError with descriptive messages on validation failure.
|
|
568
|
+
*/
|
|
569
|
+
function parse(schema, data) {
|
|
570
|
+
return schema.parse(data);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Parse a raw paginated API response into a CursorPage instance.
|
|
574
|
+
*
|
|
575
|
+
* The raw response shape: { items: T[], total, next_cursor?, prev_cursor? }
|
|
576
|
+
* We validate each item through the provided schema, then construct a CursorPage.
|
|
577
|
+
*
|
|
578
|
+
* @param itemSchema - Zod schema for each item in the items array.
|
|
579
|
+
* @param raw - Raw API response (unknown).
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```ts
|
|
583
|
+
* const raw = await http.get<unknown>('/directory/agents', { params: { q: 'bot' } });
|
|
584
|
+
* return parseCursorPage(AgentSummarySchema, raw);
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
function parseCursorPage(itemSchema, raw) {
|
|
588
|
+
const parsed = cursorPageSchema(itemSchema).parse(raw);
|
|
589
|
+
return new CursorPage(parsed.items, parsed.total_estimate ?? parsed.total ?? parsed.items.length, parsed.next_cursor ?? null, parsed.prev_cursor ?? null);
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/utils/abort.ts
|
|
593
|
+
/**
|
|
594
|
+
* AbortSignal helpers for composable cancellation.
|
|
595
|
+
*
|
|
596
|
+
* Provides:
|
|
597
|
+
* - `timeoutSignal(ms)` — `{ signal, clear }` pair that aborts after ms ms
|
|
598
|
+
* with an `RineTimeoutError` reason. Callers MUST call `clear()` in a
|
|
599
|
+
* `finally` block so the timer is cancelled on early completion
|
|
600
|
+
* (audit MINOR-4, SPEC_v2 §14.3).
|
|
601
|
+
* - `anySignal(...signals)` — signal that aborts when ANY of the inputs abort
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* Create a signal that fires after `ms` milliseconds.
|
|
605
|
+
*
|
|
606
|
+
* Returns a `{ signal, clear }` pair. Callers MUST invoke `clear()` in a
|
|
607
|
+
* `finally` block (or equivalent) to cancel the timer on early completion —
|
|
608
|
+
* otherwise the timer will still fire, aborting an already-settled signal
|
|
609
|
+
* and leaking a timer handle per call. `clear()` is idempotent.
|
|
610
|
+
*
|
|
611
|
+
* When the timer fires, the signal is aborted with an `RineTimeoutError`
|
|
612
|
+
* reason so `catch` sites can distinguish an SDK timeout from a user-cancel
|
|
613
|
+
* via `err instanceof RineTimeoutError` (SPEC_v2 §14.4).
|
|
614
|
+
*/
|
|
615
|
+
function timeoutSignal(ms) {
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
const timer = setTimeout(() => controller.abort(new RineTimeoutError(`timeout after ${ms}ms`)), ms);
|
|
618
|
+
return {
|
|
619
|
+
signal: controller.signal,
|
|
620
|
+
clear: () => clearTimeout(timer)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Combine multiple signals into one that aborts when ANY input aborts.
|
|
625
|
+
*
|
|
626
|
+
* The composed signal's `reason` is the reason from whichever input fired
|
|
627
|
+
* first — this preserves the `RineTimeoutError` reason attached by
|
|
628
|
+
* `timeoutSignal()` (and matching per-op timers in resources), so downstream
|
|
629
|
+
* `signal.reason instanceof RineTimeoutError` checks classify timeouts
|
|
630
|
+
* correctly instead of seeing a generic `AbortError` (SPEC_v2 §14.4).
|
|
631
|
+
*
|
|
632
|
+
* Listeners are detached from the remaining inputs as soon as the combined
|
|
633
|
+
* signal fires — this prevents retaining long-lived source signals (e.g. a
|
|
634
|
+
* global client signal) through short-lived request signals.
|
|
635
|
+
*/
|
|
636
|
+
function anySignal(...signals) {
|
|
637
|
+
const controller = new AbortController();
|
|
638
|
+
const cleanup = [];
|
|
639
|
+
const abort = (reason) => {
|
|
640
|
+
controller.abort(reason);
|
|
641
|
+
for (const off of cleanup) off();
|
|
642
|
+
cleanup.length = 0;
|
|
643
|
+
};
|
|
644
|
+
for (const signal of signals) {
|
|
645
|
+
if (signal.aborted) {
|
|
646
|
+
abort(signal.reason);
|
|
647
|
+
return controller.signal;
|
|
648
|
+
}
|
|
649
|
+
const onAbort = () => abort(signal.reason);
|
|
650
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
651
|
+
cleanup.push(() => signal.removeEventListener("abort", onAbort));
|
|
652
|
+
}
|
|
653
|
+
return controller.signal;
|
|
654
|
+
}
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/utils/sleep.ts
|
|
657
|
+
/**
|
|
658
|
+
* Shared sleep utility — avoids triplication across client.ts, polling.ts, streams.ts.
|
|
659
|
+
*/
|
|
660
|
+
function sleep(ms) {
|
|
661
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Like `sleep()` but aborts early when the given signal fires. Used in
|
|
665
|
+
* polling/streaming loops so cancellation during backoff doesn't wait
|
|
666
|
+
* the full interval.
|
|
667
|
+
*/
|
|
668
|
+
function sleepWithSignal(ms, signal) {
|
|
669
|
+
if (!signal) return sleep(ms);
|
|
670
|
+
if (ms <= 0 || signal.aborted) return Promise.resolve();
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
const onAbort = () => {
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
resolve();
|
|
675
|
+
};
|
|
676
|
+
const timer = setTimeout(() => {
|
|
677
|
+
signal.removeEventListener("abort", onAbort);
|
|
678
|
+
resolve();
|
|
679
|
+
}, ms);
|
|
680
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/api/middleware.ts
|
|
685
|
+
/**
|
|
686
|
+
* Fold a middleware array into a single bound pipeline.
|
|
687
|
+
*
|
|
688
|
+
* Composed once at `SDKHttpClient` construction. The first element of
|
|
689
|
+
* `middleware` is the outermost layer (called first); the last element is
|
|
690
|
+
* adjacent to `core`. Order corresponds to the reader's mental model: a
|
|
691
|
+
* `loggingMiddleware` placed first wraps everything below it.
|
|
692
|
+
*
|
|
693
|
+
* ```
|
|
694
|
+
* user's middleware[0] ← outermost, called first
|
|
695
|
+
* └── user's middleware[1]
|
|
696
|
+
* └── user's middleware[2]
|
|
697
|
+
* └── core request
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
function composeMiddleware(middleware, core) {
|
|
701
|
+
return middleware.reduceRight((next, mw) => (ctx) => mw(ctx, () => next(ctx)), core);
|
|
702
|
+
}
|
|
703
|
+
const DEFAULT_REDACT = ["authorization", "cookie"];
|
|
704
|
+
/**
|
|
705
|
+
* Logs one line per request start + one line per response (with duration).
|
|
706
|
+
* On error (middleware throw or downstream rejection) logs a `✗` line and
|
|
707
|
+
* re-throws — never swallows errors.
|
|
708
|
+
*
|
|
709
|
+
* Format:
|
|
710
|
+
* rine → {method} {url} op={operation}
|
|
711
|
+
* rine ← {status} {method} {url} ({durationMs}ms)
|
|
712
|
+
* rine ✗ {method} {url} {err.name}: {err.message}
|
|
713
|
+
*/
|
|
714
|
+
function loggingMiddleware(opts = {}) {
|
|
715
|
+
const logger = opts.logger ?? console;
|
|
716
|
+
const redactSet = new Set((opts.redact ?? DEFAULT_REDACT).map((h) => h.toLowerCase()));
|
|
717
|
+
redactSet.add("authorization");
|
|
718
|
+
return async (ctx, next) => {
|
|
719
|
+
const started = Date.now();
|
|
720
|
+
const redactedHeaders = snapshotHeaders(ctx.headers, redactSet);
|
|
721
|
+
logger.log(`rine → ${ctx.method} ${ctx.url} op=${ctx.operation}`, { headers: redactedHeaders });
|
|
722
|
+
try {
|
|
723
|
+
const res = await next();
|
|
724
|
+
const durationMs = Date.now() - started;
|
|
725
|
+
logger.log(`rine ← ${res.status} ${ctx.method} ${ctx.url} (${durationMs}ms)`);
|
|
726
|
+
return res;
|
|
727
|
+
} catch (err) {
|
|
728
|
+
const name = err instanceof Error ? err.name : "Error";
|
|
729
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
730
|
+
logger.log(`rine ✗ ${ctx.method} ${ctx.url} ${name}: ${message}`);
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Snapshot a `Headers` object into a plain record with redaction applied.
|
|
737
|
+
* Produces a stable, logger-friendly object (Headers itself doesn't pretty-
|
|
738
|
+
* print inside structured loggers).
|
|
739
|
+
*/
|
|
740
|
+
function snapshotHeaders(headers, redactSet) {
|
|
741
|
+
const out = {};
|
|
742
|
+
headers.forEach((value, key) => {
|
|
743
|
+
out[key] = redactSet.has(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
744
|
+
});
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/api/http.ts
|
|
749
|
+
/**
|
|
750
|
+
* Internal HTTP layer for the SDK.
|
|
751
|
+
*
|
|
752
|
+
* Wraps rine-core's HttpClient to add:
|
|
753
|
+
* - Native AbortSignal support (via a thin fetch wrapper)
|
|
754
|
+
* - Per-request timeout via AbortSignal.timeout()
|
|
755
|
+
* - Typed error mapping via raiseForStatus()
|
|
756
|
+
* - Agent header injection
|
|
757
|
+
* - User middleware pipeline (SPEC §9.2–§9.6)
|
|
758
|
+
*
|
|
759
|
+
* Middleware wraps `.get/.post/.put/.patch/.delete` only. It does NOT wrap
|
|
760
|
+
* `openStream()` (SSE lifecycle) or `getCoreHttpClient()` (crypto-path —
|
|
761
|
+
* those calls go through a separate rine-core HttpClient per §6.5).
|
|
762
|
+
*/
|
|
763
|
+
var SDKHttpClient = class {
|
|
764
|
+
_configDir;
|
|
765
|
+
apiUrl;
|
|
766
|
+
agentHeader;
|
|
767
|
+
_timeout;
|
|
768
|
+
maxRetries;
|
|
769
|
+
/**
|
|
770
|
+
* The user middleware chain, preserved so derived clients
|
|
771
|
+
* (`client.withSignal`, `withTimeout`, `withAgent`) can forward it. The
|
|
772
|
+
* compiled pipeline lives in `this.pipeline`.
|
|
773
|
+
*/
|
|
774
|
+
middleware;
|
|
775
|
+
/** Composed middleware pipeline — SPEC §9.3. Bound once at construction. */
|
|
776
|
+
pipeline;
|
|
777
|
+
get configDir() {
|
|
778
|
+
return this._configDir;
|
|
779
|
+
}
|
|
780
|
+
get timeout() {
|
|
781
|
+
return this._timeout;
|
|
782
|
+
}
|
|
783
|
+
constructor(opts = {}) {
|
|
784
|
+
this._configDir = opts.configDir ?? "";
|
|
785
|
+
this.apiUrl = opts.apiUrl ?? "https://api.rine.network";
|
|
786
|
+
this.agentHeader = opts.agent ? { "X-Rine-Agent": opts.agent } : void 0;
|
|
787
|
+
this._timeout = opts.timeout ?? 3e4;
|
|
788
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
789
|
+
this.middleware = opts.middleware ?? [];
|
|
790
|
+
const coreRequest = this.createCoreRequest();
|
|
791
|
+
this.pipeline = composeMiddleware(this.middleware, coreRequest);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* GET /path with typed response.
|
|
795
|
+
*/
|
|
796
|
+
async get(path, opts = {}) {
|
|
797
|
+
return this.request("GET", path, opts);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* POST /path with optional body.
|
|
801
|
+
*/
|
|
802
|
+
async post(path, body, opts = {}) {
|
|
803
|
+
return this.request("POST", path, {
|
|
804
|
+
...opts,
|
|
805
|
+
body
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* POST /path returning both HTTP status code and parsed body.
|
|
810
|
+
* Used when the caller must branch on status (e.g. 200 vs 202).
|
|
811
|
+
*/
|
|
812
|
+
async postWithStatus(path, body, opts = {}) {
|
|
813
|
+
return this.requestWithStatus("POST", path, {
|
|
814
|
+
...opts,
|
|
815
|
+
body
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* PUT /path with optional body.
|
|
820
|
+
*/
|
|
821
|
+
async put(path, body, opts = {}) {
|
|
822
|
+
return this.request("PUT", path, {
|
|
823
|
+
...opts,
|
|
824
|
+
body
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* PATCH /path with optional body.
|
|
829
|
+
*/
|
|
830
|
+
async patch(path, body, opts = {}) {
|
|
831
|
+
return this.request("PATCH", path, {
|
|
832
|
+
...opts,
|
|
833
|
+
body
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* DELETE /path.
|
|
838
|
+
*/
|
|
839
|
+
async delete(path, opts = {}) {
|
|
840
|
+
return this.request("DELETE", path, opts);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Open a long-lived streaming GET with auth applied. Used by the SSE
|
|
844
|
+
* resource — returns the raw Response so the caller can consume the body.
|
|
845
|
+
*
|
|
846
|
+
* No per-request timeout is applied: streams are long-lived by design, so
|
|
847
|
+
* lifetime is controlled exclusively by the caller's AbortSignal.
|
|
848
|
+
*
|
|
849
|
+
* **Middleware scope**: `openStream` does NOT run the user middleware
|
|
850
|
+
* pipeline. SSE lifecycle (long-lived, Last-Event-ID resume, reconnect)
|
|
851
|
+
* is incompatible with the one-shot request/response middleware shape.
|
|
852
|
+
* See SPEC §9.6 / §12.3.4 for the scope boundary and the `fetchWithAuth`
|
|
853
|
+
* auth-injection path.
|
|
854
|
+
*
|
|
855
|
+
* @param opts.signal - REQUIRED. A long-lived stream without a cancel
|
|
856
|
+
* signal leaks a connection forever, so the type enforces it. Fire-and-
|
|
857
|
+
* forget callers can pass `new AbortController().signal` explicitly
|
|
858
|
+
* (see `neverAbortingSignal()` in `streams.ts` for the documented
|
|
859
|
+
* escape hatch).
|
|
860
|
+
*/
|
|
861
|
+
async openStream(path, opts) {
|
|
862
|
+
const url = buildUrl(this.apiUrl, path);
|
|
863
|
+
const headers = {
|
|
864
|
+
Accept: "text/event-stream",
|
|
865
|
+
...this.agentHeader ?? {},
|
|
866
|
+
...opts.headers ?? {}
|
|
867
|
+
};
|
|
868
|
+
return this.fetchWithAuth("GET", url, headers, void 0, opts.signal, false);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Returns a rine-core HttpClient bound to the same credentials, base URL,
|
|
872
|
+
* and agent header as this SDKHttpClient, scoped to the given signal.
|
|
873
|
+
*
|
|
874
|
+
* Short-lived by contract — construct one per crypto operation, do not
|
|
875
|
+
* cache. Cost is one object allocation (no network, no token fetch, no
|
|
876
|
+
* file I/O at construction). The tokenFn closure reads the same config
|
|
877
|
+
* dir and token cache as the SDK's REST path, so a refresh on either
|
|
878
|
+
* side is visible to the other.
|
|
879
|
+
*
|
|
880
|
+
* Middleware scope: calls made through the returned client do NOT run
|
|
881
|
+
* the SDK middleware pipeline (see SPEC §6.5).
|
|
882
|
+
*/
|
|
883
|
+
getCoreHttpClient(opts = {}) {
|
|
884
|
+
const entry = loadCredentials(this._configDir).default;
|
|
885
|
+
return new HttpClient({
|
|
886
|
+
apiUrl: this.apiUrl,
|
|
887
|
+
tokenFn: (force) => getOrRefreshToken(this._configDir, this.apiUrl, entry, "default", { force }),
|
|
888
|
+
defaultHeaders: this.agentHeader,
|
|
889
|
+
canRefresh: true,
|
|
890
|
+
signal: opts.signal
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
async request(method, path, opts = {}) {
|
|
894
|
+
const res = await this.executeRequest(method, path, opts);
|
|
895
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") return;
|
|
896
|
+
return await res.json();
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Like `request()` but also returns the HTTP status code. Used when
|
|
900
|
+
* the caller must branch on status (e.g. 200 vs 202 for group join).
|
|
901
|
+
*/
|
|
902
|
+
async requestWithStatus(method, path, opts = {}) {
|
|
903
|
+
const res = await this.executeRequest(method, path, opts);
|
|
904
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") return {
|
|
905
|
+
status: res.status,
|
|
906
|
+
body: void 0
|
|
907
|
+
};
|
|
908
|
+
return {
|
|
909
|
+
status: res.status,
|
|
910
|
+
body: await res.json()
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Shared request execution: builds context, runs middleware pipeline,
|
|
915
|
+
* handles errors and timeouts, returns the validated Response.
|
|
916
|
+
*/
|
|
917
|
+
async executeRequest(method, path, opts = {}) {
|
|
918
|
+
const { params, extraHeaders, signal, body, operation } = opts;
|
|
919
|
+
const url = buildUrl(this.apiUrl, path, params);
|
|
920
|
+
const headers = buildHeaders(this.agentHeader, extraHeaders, body);
|
|
921
|
+
const ac = new AbortController();
|
|
922
|
+
const timeoutId = setTimeout(() => ac.abort(), this.timeout);
|
|
923
|
+
const fetchSignal = signal ? anySignal(signal, ac.signal) : ac.signal;
|
|
924
|
+
if (operation === void 0) throw new Error(`SDKHttpClient.${method.toLowerCase()}(${path}) was called without opts.operation — every resource method must set a RineOperation label.`);
|
|
925
|
+
const ctx = {
|
|
926
|
+
method,
|
|
927
|
+
url,
|
|
928
|
+
headers: new Headers(headers),
|
|
929
|
+
body,
|
|
930
|
+
signal: fetchSignal,
|
|
931
|
+
operation,
|
|
932
|
+
transport: "rest"
|
|
933
|
+
};
|
|
934
|
+
callMeta.set(ctx, { unauthenticated: opts.unauthenticated === true });
|
|
935
|
+
try {
|
|
936
|
+
const res = await this.pipeline(ctx);
|
|
937
|
+
clearTimeout(timeoutId);
|
|
938
|
+
if (!res.ok) {
|
|
939
|
+
const detail = await parseErrorDetail(res);
|
|
940
|
+
raiseForStatus(res.status, detail, res);
|
|
941
|
+
}
|
|
942
|
+
return res;
|
|
943
|
+
} catch (err) {
|
|
944
|
+
clearTimeout(timeoutId);
|
|
945
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
946
|
+
if (signal?.aborted) {
|
|
947
|
+
if (signal.reason instanceof RineTimeoutError) throw signal.reason;
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
throw new RineTimeoutError(`Request to ${method} ${path} timed out after ${this.timeout}ms`);
|
|
951
|
+
}
|
|
952
|
+
throw err;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Build the innermost "core" transport function handed to
|
|
957
|
+
* `composeMiddleware`. It reads from `ctx` *after* middleware has run
|
|
958
|
+
* so header mutations (auth, tracing, redact) take effect on the wire.
|
|
959
|
+
*
|
|
960
|
+
* Core handles auth injection + 401 refresh — these are transport
|
|
961
|
+
* concerns (Dec-3.1), so middleware sees one logical call, not per-retry
|
|
962
|
+
* attempts.
|
|
963
|
+
*/
|
|
964
|
+
createCoreRequest() {
|
|
965
|
+
return async (ctx) => {
|
|
966
|
+
const unauthenticated = callMeta.get(ctx)?.unauthenticated === true;
|
|
967
|
+
const headers = headersToRecord(ctx.headers);
|
|
968
|
+
const serializedBody = serializeBody(ctx.body);
|
|
969
|
+
return this.fetchWithAuth(ctx.method, ctx.url, headers, serializedBody, ctx.signal, unauthenticated);
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async fetchWithAuth(method, url, headers, body, fetchSignal, unauthenticated) {
|
|
973
|
+
const doFetch = async () => {
|
|
974
|
+
if (unauthenticated) return fetch(url, {
|
|
975
|
+
method,
|
|
976
|
+
headers,
|
|
977
|
+
body,
|
|
978
|
+
signal: fetchSignal
|
|
979
|
+
});
|
|
980
|
+
const creds = loadCredentials(this._configDir).default;
|
|
981
|
+
headers.Authorization = `Bearer ${await getOrRefreshToken(this._configDir, this.apiUrl, creds, "default")}`;
|
|
982
|
+
const res = await fetch(url, {
|
|
983
|
+
method,
|
|
984
|
+
headers,
|
|
985
|
+
body,
|
|
986
|
+
signal: fetchSignal
|
|
987
|
+
});
|
|
988
|
+
if (res.status === 401) {
|
|
989
|
+
headers.Authorization = `Bearer ${await getOrRefreshToken(this._configDir, this.apiUrl, creds, "default", { force: true })}`;
|
|
990
|
+
return fetch(url, {
|
|
991
|
+
method,
|
|
992
|
+
headers,
|
|
993
|
+
body,
|
|
994
|
+
signal: fetchSignal
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return res;
|
|
998
|
+
};
|
|
999
|
+
let attempt = 0;
|
|
1000
|
+
while (true) {
|
|
1001
|
+
const res = await doFetch();
|
|
1002
|
+
if (res.status !== 429 || attempt >= this.maxRetries) return res;
|
|
1003
|
+
attempt += 1;
|
|
1004
|
+
const delayMs = parseRetryAfter(res.headers.get("Retry-After"), attempt);
|
|
1005
|
+
if (fetchSignal.aborted) return res;
|
|
1006
|
+
await sleepWithSignal(delayMs, fetchSignal);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const callMeta = /* @__PURE__ */ new WeakMap();
|
|
1011
|
+
function buildUrl(baseUrl, path, params) {
|
|
1012
|
+
let url = path.startsWith("http://") || path.startsWith("https://") ? path : baseUrl + path;
|
|
1013
|
+
if (params) {
|
|
1014
|
+
const qs = new URLSearchParams(Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => [k, String(v)])).toString();
|
|
1015
|
+
if (qs) url += `?${qs}`;
|
|
1016
|
+
}
|
|
1017
|
+
return url;
|
|
1018
|
+
}
|
|
1019
|
+
function buildHeaders(agentHeader, extraHeaders, body) {
|
|
1020
|
+
const headers = {
|
|
1021
|
+
...agentHeader ?? {},
|
|
1022
|
+
...extraHeaders ?? {}
|
|
1023
|
+
};
|
|
1024
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
1025
|
+
return headers;
|
|
1026
|
+
}
|
|
1027
|
+
function headersToRecord(headers) {
|
|
1028
|
+
const out = {};
|
|
1029
|
+
headers.forEach((value, key) => {
|
|
1030
|
+
out[key] = value;
|
|
1031
|
+
});
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
function serializeBody(body) {
|
|
1035
|
+
if (body === void 0) return void 0;
|
|
1036
|
+
return JSON.stringify(body);
|
|
1037
|
+
}
|
|
1038
|
+
async function parseErrorDetail(res) {
|
|
1039
|
+
try {
|
|
1040
|
+
const body = await res.json();
|
|
1041
|
+
if (typeof body.detail === "string") return body.detail;
|
|
1042
|
+
if (Array.isArray(body.detail)) return body.detail.map((e) => typeof e === "string" ? e : e.msg).join("; ");
|
|
1043
|
+
} catch {}
|
|
1044
|
+
return res.statusText;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Parse the `Retry-After` response header and pick a delay, capped at 30s
|
|
1048
|
+
* (SPEC §9.4). Supports both numeric "seconds" and RFC 7231 HTTP-date forms;
|
|
1049
|
+
* falls back to exponential backoff (1s, 2s, 4s, ...) keyed on the attempt
|
|
1050
|
+
* counter when the server omits or emits an unparseable value.
|
|
1051
|
+
*/
|
|
1052
|
+
function parseRetryAfter(header, attempt) {
|
|
1053
|
+
const MAX_DELAY_MS = 3e4;
|
|
1054
|
+
const fallback = Math.min(MAX_DELAY_MS, 2 ** (attempt - 1) * 1e3);
|
|
1055
|
+
if (!header) return fallback;
|
|
1056
|
+
const asNumber = Number(header);
|
|
1057
|
+
if (Number.isFinite(asNumber) && asNumber >= 0) return Math.min(MAX_DELAY_MS, asNumber * 1e3);
|
|
1058
|
+
const asDate = Date.parse(header);
|
|
1059
|
+
if (!Number.isNaN(asDate)) return Math.max(0, Math.min(MAX_DELAY_MS, asDate - Date.now()));
|
|
1060
|
+
return fallback;
|
|
1061
|
+
}
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/utils/schema.ts
|
|
1064
|
+
/**
|
|
1065
|
+
* Validate an arbitrary value against a Standard Schema v1 and return the
|
|
1066
|
+
* typed output. The validator may return a synchronous result or a promise,
|
|
1067
|
+
* so this helper always awaits. On failure, the first issue's message is
|
|
1068
|
+
* used as the `ValidationError` detail — detail enough to point at the
|
|
1069
|
+
* field without dumping the full issue list into the error message.
|
|
1070
|
+
*/
|
|
1071
|
+
async function parsePlaintext(value, schema) {
|
|
1072
|
+
const result = await schema["~standard"].validate(value);
|
|
1073
|
+
if (result.issues !== void 0) {
|
|
1074
|
+
const first = result.issues[0];
|
|
1075
|
+
throw new SchemaValidationError(`Plaintext failed schema validation${first?.path && first.path.length > 0 ? ` at ${formatPath(first.path)}` : ""}: ${first?.message ?? "(no message)"}`);
|
|
1076
|
+
}
|
|
1077
|
+
return result.value;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* JSON-parse a `DecryptedMessage.plaintext` string and validate it via a
|
|
1081
|
+
* Standard Schema v1, returning a typed `DecryptedMessage<T>`. Leaves
|
|
1082
|
+
* messages with `plaintext == null` untouched (decrypt-failed envelopes
|
|
1083
|
+
* still surface to the caller so they can branch on `decrypt_error`).
|
|
1084
|
+
*
|
|
1085
|
+
* The JSON-parse step is mandatory: Standard Schemas like Zod's `z.object`
|
|
1086
|
+
* validate structured input, not raw strings. If a caller's payload is
|
|
1087
|
+
* already a plain string, they can use `z.string()` as the schema and the
|
|
1088
|
+
* JSON-parse round-trip is still well-defined (`JSON.parse('"hi"') === "hi"`).
|
|
1089
|
+
*/
|
|
1090
|
+
async function parseMessagePlaintext(msg, schema) {
|
|
1091
|
+
if (msg.plaintext == null) return msg;
|
|
1092
|
+
let candidate;
|
|
1093
|
+
try {
|
|
1094
|
+
candidate = typeof msg.plaintext === "string" ? JSON.parse(msg.plaintext) : msg.plaintext;
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
throw new SchemaValidationError(`Plaintext is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1097
|
+
}
|
|
1098
|
+
const value = await parsePlaintext(candidate, schema);
|
|
1099
|
+
return {
|
|
1100
|
+
...msg,
|
|
1101
|
+
plaintext: value
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Validate an outbound payload against a Standard Schema v1 before it is
|
|
1106
|
+
* handed to the encryption layer. SPEC §11.4: outbound schemas run *before*
|
|
1107
|
+
* encrypt so a shape failure rejects without sending any ciphertext.
|
|
1108
|
+
*
|
|
1109
|
+
* This helper returns the validated (possibly transformed — schemas may
|
|
1110
|
+
* parse / coerce) value so the caller can encrypt the narrowed form rather
|
|
1111
|
+
* than the raw input. For schemas that are pure type guards (no transforms)
|
|
1112
|
+
* the returned value is reference-equal to the input.
|
|
1113
|
+
*/
|
|
1114
|
+
async function validateOutbound(payload, schema) {
|
|
1115
|
+
return parsePlaintext(payload, schema);
|
|
1116
|
+
}
|
|
1117
|
+
function formatPath(path) {
|
|
1118
|
+
return path.map((seg) => {
|
|
1119
|
+
const key = typeof seg === "object" && seg !== null ? seg.key : seg;
|
|
1120
|
+
return typeof key === "number" ? `[${key}]` : String(key);
|
|
1121
|
+
}).join(".");
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/resources/conversation.ts
|
|
1125
|
+
function looksLikeRecipient(value) {
|
|
1126
|
+
if (typeof value !== "string") return false;
|
|
1127
|
+
return UUID_RE.test(value) || isAgentHandle(value) || isGroupHandle(value);
|
|
1128
|
+
}
|
|
1129
|
+
var ConversationScope = class {
|
|
1130
|
+
/**
|
|
1131
|
+
* Peer auto-wired when the scope was built via `client.conversation(msg)`.
|
|
1132
|
+
* When set, the two-arg `scope.send(payload)` form routes to this peer
|
|
1133
|
+
* without requiring an explicit recipient. Null when the scope was
|
|
1134
|
+
* built from a bare conversation id.
|
|
1135
|
+
*/
|
|
1136
|
+
peer;
|
|
1137
|
+
/**
|
|
1138
|
+
* Anchor message id — set when the scope was built via
|
|
1139
|
+
* `client.conversation(msg)`. When present, `scope.send()` routes
|
|
1140
|
+
* through `client.reply(anchorMessageId, …)` so the server threads
|
|
1141
|
+
* the new message into the existing conversation. Null when the
|
|
1142
|
+
* scope was built from a bare conversation id string.
|
|
1143
|
+
*/
|
|
1144
|
+
anchorMessageId;
|
|
1145
|
+
constructor(client, id, peer = null, anchorMessageId = null) {
|
|
1146
|
+
this.client = client;
|
|
1147
|
+
this.id = id;
|
|
1148
|
+
this.peer = peer;
|
|
1149
|
+
this.anchorMessageId = anchorMessageId;
|
|
1150
|
+
}
|
|
1151
|
+
send(a, b, c) {
|
|
1152
|
+
let payload;
|
|
1153
|
+
let opts;
|
|
1154
|
+
if (looksLikeRecipient(a)) {
|
|
1155
|
+
const to = a;
|
|
1156
|
+
payload = b;
|
|
1157
|
+
opts = c ?? {};
|
|
1158
|
+
if (this.anchorMessageId != null) {
|
|
1159
|
+
if (this.peer != null && to !== this.peer) return Promise.reject(/* @__PURE__ */ new Error(`ConversationScope.send(): explicit recipient "${to}" does not match scope peer "${this.peer}". The reply endpoint auto-routes to the original peer; use client.send() directly for cross-conversation sends.`));
|
|
1160
|
+
return this.client.reply(this.anchorMessageId, payload, opts);
|
|
1161
|
+
}
|
|
1162
|
+
return Promise.reject(/* @__PURE__ */ new Error("ConversationScope was built from a bare conversation id and cannot thread sends. Use client.conversation(message) to pin a message anchor, or call client.send() / client.reply() directly."));
|
|
1163
|
+
}
|
|
1164
|
+
payload = a;
|
|
1165
|
+
opts = b ?? {};
|
|
1166
|
+
if (this.anchorMessageId == null || this.peer == null) return Promise.reject(/* @__PURE__ */ new Error("ConversationScope was built from a bare conversation id and cannot thread sends. Use client.conversation(message) to pin a message anchor, or call client.send() / client.reply() directly."));
|
|
1167
|
+
return this.client.reply(this.anchorMessageId, payload, opts);
|
|
1168
|
+
}
|
|
1169
|
+
reply(messageId, payload, opts = {}) {
|
|
1170
|
+
return this.client.reply(messageId, payload, opts);
|
|
1171
|
+
}
|
|
1172
|
+
messages(opts = {}) {
|
|
1173
|
+
return this.messagesGenerator(opts);
|
|
1174
|
+
}
|
|
1175
|
+
history(opts = {}) {
|
|
1176
|
+
return this.historyGenerator(opts);
|
|
1177
|
+
}
|
|
1178
|
+
async *messagesGenerator(opts) {
|
|
1179
|
+
const streamOpts = {};
|
|
1180
|
+
if (opts.type !== void 0) streamOpts.type = opts.type;
|
|
1181
|
+
if (opts.signal !== void 0) streamOpts.signal = opts.signal;
|
|
1182
|
+
if (opts.agent !== void 0) streamOpts.agent = opts.agent;
|
|
1183
|
+
if (opts.lastEventId !== void 0) streamOpts.lastEventId = opts.lastEventId;
|
|
1184
|
+
if (opts.schema !== void 0) streamOpts.schema = opts.schema;
|
|
1185
|
+
for await (const msg of this.client.messages(streamOpts)) if (msg.conversation_id === this.id) yield msg;
|
|
1186
|
+
}
|
|
1187
|
+
async *historyGenerator(opts) {
|
|
1188
|
+
const limit = opts.limit ?? 50;
|
|
1189
|
+
const maxItems = opts.maxItems;
|
|
1190
|
+
const schema = opts.schema;
|
|
1191
|
+
let cursor = opts.cursor;
|
|
1192
|
+
let yielded = 0;
|
|
1193
|
+
while (true) {
|
|
1194
|
+
const inboxOpts = { limit };
|
|
1195
|
+
if (cursor !== void 0) inboxOpts.cursor = cursor;
|
|
1196
|
+
if (opts.agent !== void 0) inboxOpts.agent = opts.agent;
|
|
1197
|
+
if (opts.signal !== void 0) inboxOpts.signal = opts.signal;
|
|
1198
|
+
const page = await this.client.inbox(inboxOpts);
|
|
1199
|
+
for (const msg of page.items) {
|
|
1200
|
+
if (msg.conversation_id !== this.id) continue;
|
|
1201
|
+
yield schema !== void 0 && msg.decrypt_error == null ? await parseMessagePlaintext(msg, schema) : msg;
|
|
1202
|
+
yielded += 1;
|
|
1203
|
+
if (maxItems !== void 0 && yielded >= maxItems) return;
|
|
1204
|
+
}
|
|
1205
|
+
if (!page.hasNext || page.nextCursor == null) return;
|
|
1206
|
+
cursor = page.nextCursor;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
//#endregion
|
|
1211
|
+
//#region src/resources/decrypt.ts
|
|
1212
|
+
/**
|
|
1213
|
+
* Shared decryption helper used by `messages.inbox()`, `messages.read()`,
|
|
1214
|
+
* `messages.sendAndWait()` (Phase 2), and `client.messages()` (§11.1).
|
|
1215
|
+
*
|
|
1216
|
+
* Dispatch is driven by `encryption_version`:
|
|
1217
|
+
* - `hpke-v1` → `decryptMessage` (1:1, recipient's X25519 private key).
|
|
1218
|
+
* - `sender-key-v1` → `decryptGroupMessage`. On a sender-key dispatch failure,
|
|
1219
|
+
* optionally retries once after calling
|
|
1220
|
+
* `fetchAndIngestPendingSKDistributions` (matches Python
|
|
1221
|
+
* SDK `read()` discipline).
|
|
1222
|
+
* - anything else → returned as-is with `decrypt_error` populated.
|
|
1223
|
+
*
|
|
1224
|
+
* Crypto failures (including the post-retry failure on the sender-key path)
|
|
1225
|
+
* populate `decrypt_error` and do NOT throw — callers branch on
|
|
1226
|
+
* `msg.decrypt_error !== null`. User cancellation (`AbortError`) is always
|
|
1227
|
+
* re-thrown so a cancelled `inbox()` rejects cleanly instead of resolving
|
|
1228
|
+
* with a page of "failed" items.
|
|
1229
|
+
*/
|
|
1230
|
+
async function decryptEnvelope(opts) {
|
|
1231
|
+
const { core, configDir, agentId, msg, retrySenderKey = false } = opts;
|
|
1232
|
+
const encryptedPayload = msg.encrypted_payload;
|
|
1233
|
+
if (!encryptedPayload) return applyDecryptError(msg, "missing encrypted_payload");
|
|
1234
|
+
try {
|
|
1235
|
+
if (msg.encryption_version === "hpke-v1") return applyDecryptResult(msg, await decryptMessage(configDir, agentId, encryptedPayload, core));
|
|
1236
|
+
if (msg.encryption_version === "sender-key-v1") {
|
|
1237
|
+
if (!msg.group_id) return applyDecryptError(msg, "sender-key message missing group_id");
|
|
1238
|
+
try {
|
|
1239
|
+
return applyDecryptResult(msg, await decryptGroupMessage(configDir, agentId, msg.group_id, encryptedPayload, core));
|
|
1240
|
+
} catch (firstErr) {
|
|
1241
|
+
rethrowIfAbort(firstErr);
|
|
1242
|
+
if (!retrySenderKey) return applyDecryptError(msg, errMessage(firstErr), classifyGroupError(firstErr));
|
|
1243
|
+
try {
|
|
1244
|
+
await fetchAndIngestPendingSKDistributions(core, configDir, agentId);
|
|
1245
|
+
return applyDecryptResult(msg, await decryptGroupMessage(configDir, agentId, msg.group_id, encryptedPayload, core));
|
|
1246
|
+
} catch (retryErr) {
|
|
1247
|
+
rethrowIfAbort(retryErr);
|
|
1248
|
+
return applyDecryptError(msg, errMessage(retryErr), classifyGroupError(retryErr));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return applyDecryptError(msg, `unknown encryption_version: ${msg.encryption_version}`);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
rethrowIfAbort(err);
|
|
1255
|
+
return applyDecryptError(msg, errMessage(err));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function applyDecryptResult(msg, result) {
|
|
1259
|
+
return {
|
|
1260
|
+
...msg,
|
|
1261
|
+
metadata: msg.metadata ?? {},
|
|
1262
|
+
plaintext: autoParsePlaintext(msg.content_type, result.plaintext),
|
|
1263
|
+
verified: result.verified,
|
|
1264
|
+
verification_status: result.verificationStatus,
|
|
1265
|
+
decrypt_error: null
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* If the payload was JSON (the default content-type), auto-parse the
|
|
1270
|
+
* decrypted string into a structured value before handing it to callers.
|
|
1271
|
+
*
|
|
1272
|
+
* Without this, first-time users see the raw serialized string and
|
|
1273
|
+
* mistake it for a double-encoding. Callers that pass a Standard Schema
|
|
1274
|
+
* still work: `parseMessagePlaintext` in `utils/schema.ts` accepts both
|
|
1275
|
+
* a string (parses it) and an already-parsed value (treats it as
|
|
1276
|
+
* structured input).
|
|
1277
|
+
*
|
|
1278
|
+
* Failure is silent on purpose — if the bytes claim to be JSON but are
|
|
1279
|
+
* malformed, the decrypt layer succeeded and we hand back the raw string
|
|
1280
|
+
* rather than synthesising a `decrypt_error`. Crypto observability and
|
|
1281
|
+
* content-format observability are different concerns (SPEC §10.2).
|
|
1282
|
+
*/
|
|
1283
|
+
function autoParsePlaintext(contentType, plaintext) {
|
|
1284
|
+
if ((contentType ?? "application/json") !== "application/json") return plaintext;
|
|
1285
|
+
if (typeof plaintext !== "string") return plaintext;
|
|
1286
|
+
try {
|
|
1287
|
+
return JSON.parse(plaintext);
|
|
1288
|
+
} catch {
|
|
1289
|
+
return plaintext;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function applyDecryptError(msg, error, status = "unverifiable") {
|
|
1293
|
+
return {
|
|
1294
|
+
...msg,
|
|
1295
|
+
metadata: msg.metadata ?? {},
|
|
1296
|
+
plaintext: void 0,
|
|
1297
|
+
verified: false,
|
|
1298
|
+
verification_status: status,
|
|
1299
|
+
decrypt_error: error
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Map a sender-key decrypt failure to a `verification_status`.
|
|
1304
|
+
*
|
|
1305
|
+
* `decryptGroupMessage` throws `"Sender signature verification failed"` when
|
|
1306
|
+
* the inner-envelope Ed25519 signature doesn't match the sender's published
|
|
1307
|
+
* signing key (see `rine-core/src/crypto/message.ts`). That is the `invalid`
|
|
1308
|
+
* case — the recipient successfully derived the message key but cannot trust
|
|
1309
|
+
* the author. Every other failure (ratchet state missing, AEAD failure,
|
|
1310
|
+
* truncated ciphertext, malformed envelope, etc.) is `unverifiable` because
|
|
1311
|
+
* the recipient couldn't even get as far as a signature check.
|
|
1312
|
+
*
|
|
1313
|
+
* Without this classification every group failure collapses to
|
|
1314
|
+
* `unverifiable` and callers lose the ability to distinguish "bad sender"
|
|
1315
|
+
* from "missing key material" — the same gap the adherence audit for
|
|
1316
|
+
* Step 22 flagged as a blocker.
|
|
1317
|
+
*/
|
|
1318
|
+
function classifyGroupError(err) {
|
|
1319
|
+
if (errMessage(err).toLowerCase().includes("signature verification failed")) return "invalid";
|
|
1320
|
+
return "unverifiable";
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Re-throw if the error is an `AbortError` from an aborted signal. Decrypt
|
|
1324
|
+
* failures populate `decrypt_error` per §10.2, but user cancellation (via
|
|
1325
|
+
* `opts.signal` or the op timer) must bubble out as an AbortError rather
|
|
1326
|
+
* than being silently converted to a `decrypt_error` string on an
|
|
1327
|
+
* otherwise "successful" page.
|
|
1328
|
+
*/
|
|
1329
|
+
function rethrowIfAbort(err) {
|
|
1330
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
1331
|
+
}
|
|
1332
|
+
function errMessage(err) {
|
|
1333
|
+
if (err instanceof Error) return err.message;
|
|
1334
|
+
return String(err);
|
|
1335
|
+
}
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region src/resources/discovery.ts
|
|
1338
|
+
/**
|
|
1339
|
+
* Discovery resource — discover, inspect, discoverGroups.
|
|
1340
|
+
*/
|
|
1341
|
+
var DiscoveryResource = class {
|
|
1342
|
+
constructor(http) {
|
|
1343
|
+
this.http = http;
|
|
1344
|
+
}
|
|
1345
|
+
async discover(filters) {
|
|
1346
|
+
return parseCursorPage(AgentSummarySchema, await this.http.get("/directory/agents", {
|
|
1347
|
+
params: {
|
|
1348
|
+
q: filters?.q,
|
|
1349
|
+
category: filters?.category,
|
|
1350
|
+
language: filters?.language,
|
|
1351
|
+
verified: filters?.verified,
|
|
1352
|
+
limit: filters?.limit,
|
|
1353
|
+
cursor: filters?.cursor
|
|
1354
|
+
},
|
|
1355
|
+
operation: "discover"
|
|
1356
|
+
}));
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Fetch a directory agent profile and unwrap the A2A card envelope
|
|
1360
|
+
*
|
|
1361
|
+
*
|
|
1362
|
+
* Accepts either a UUID (pass-through) or an agent handle
|
|
1363
|
+
* (`name@org.rine.network`, resolved via WebFinger by
|
|
1364
|
+
* `resolveToUuid`). The directory route only accepts UUIDs, so the
|
|
1365
|
+
* handle lookup has to happen client-side.
|
|
1366
|
+
*
|
|
1367
|
+
* Server response shape:
|
|
1368
|
+
* ```
|
|
1369
|
+
* {
|
|
1370
|
+
* card: { name, description, rine: { agent_id, handle, category, verified, human_oversight } },
|
|
1371
|
+
* directory_metadata: { registered_at, ... }
|
|
1372
|
+
* }
|
|
1373
|
+
* ```
|
|
1374
|
+
*
|
|
1375
|
+
* Legacy flat shape is still accepted (the fallback `parse()` branch).
|
|
1376
|
+
*/
|
|
1377
|
+
async inspect(handleOrId) {
|
|
1378
|
+
const uuid = await resolveToUuid(this.http.apiUrl, handleOrId);
|
|
1379
|
+
return parse(AgentProfileSchema, unwrapDirectoryCard(await this.http.get(`/directory/agents/${uuid}`, { operation: "inspect" }), uuid));
|
|
1380
|
+
}
|
|
1381
|
+
async discoverGroups(opts) {
|
|
1382
|
+
return parseCursorPage(GroupSummarySchema, await this.http.get("/directory/groups", {
|
|
1383
|
+
params: {
|
|
1384
|
+
q: opts?.q,
|
|
1385
|
+
limit: opts?.limit,
|
|
1386
|
+
cursor: opts?.cursor
|
|
1387
|
+
},
|
|
1388
|
+
operation: "discoverGroups"
|
|
1389
|
+
}));
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
function unwrapDirectoryCard(data, fallbackId) {
|
|
1393
|
+
if (!isRecord(data) || !isRecord(data.card)) return data;
|
|
1394
|
+
const card = data.card;
|
|
1395
|
+
const rine = isRecord(card.rine) ? card.rine : {};
|
|
1396
|
+
const directoryMetadata = isRecord(data.directory_metadata) ? data.directory_metadata : {};
|
|
1397
|
+
return {
|
|
1398
|
+
id: rine.agent_id ?? fallbackId,
|
|
1399
|
+
name: card.name ?? "",
|
|
1400
|
+
handle: rine.handle ?? "",
|
|
1401
|
+
description: card.description ?? null,
|
|
1402
|
+
category: rine.category ?? null,
|
|
1403
|
+
verified: rine.verified ?? false,
|
|
1404
|
+
human_oversight: rine.human_oversight ?? true,
|
|
1405
|
+
created_at: directoryMetadata.registered_at ?? null
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function isRecord(value) {
|
|
1409
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/resources/groups.ts
|
|
1413
|
+
/**
|
|
1414
|
+
* Groups resource — create, get, list, join, members, invite,
|
|
1415
|
+
* update, delete, removeMember, listRequests, vote.
|
|
1416
|
+
*
|
|
1417
|
+
* Step 10 — Track B: routes + bodies aligned with server endpoints per
|
|
1418
|
+
* SPEC §10.6. Handle-resolution (`_resolveGroupAsync`) is not added here
|
|
1419
|
+
* (Step 11+), so UUID-only parameters remain the current limit.
|
|
1420
|
+
*/
|
|
1421
|
+
var GroupsResource = class {
|
|
1422
|
+
constructor(http) {
|
|
1423
|
+
this.http = http;
|
|
1424
|
+
}
|
|
1425
|
+
async list() {
|
|
1426
|
+
return parseCursorPage(GroupReadSchema, await this.http.get("/groups", { operation: "groupsList" }));
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Create a new group. Two call styles:
|
|
1430
|
+
*
|
|
1431
|
+
* client.groups.create('my-group', { visibility: 'public' })
|
|
1432
|
+
* client.groups.create({ name: 'my-group', visibility: 'public' })
|
|
1433
|
+
*
|
|
1434
|
+
* The options-bag form matches the rest of the SDK surface (`inbox`,
|
|
1435
|
+
* `discover`, `sendAndWait`) and is the recommended ergonomic for
|
|
1436
|
+
* first-time users; the positional form is kept for existing callers.
|
|
1437
|
+
*/
|
|
1438
|
+
async create(nameOrOpts, maybeOpts = {}) {
|
|
1439
|
+
const { name, opts } = typeof nameOrOpts === "string" ? {
|
|
1440
|
+
name: nameOrOpts,
|
|
1441
|
+
opts: maybeOpts
|
|
1442
|
+
} : {
|
|
1443
|
+
name: nameOrOpts.name,
|
|
1444
|
+
opts: nameOrOpts
|
|
1445
|
+
};
|
|
1446
|
+
const body = { name };
|
|
1447
|
+
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1448
|
+
if (opts.enrollment !== void 0) body["enrollment_policy"] = opts.enrollment;
|
|
1449
|
+
if (opts.visibility !== void 0) body["visibility"] = opts.visibility;
|
|
1450
|
+
return parse(GroupReadSchema, await this.http.post("/groups", body, {
|
|
1451
|
+
signal: opts.signal,
|
|
1452
|
+
operation: "groupsCreate"
|
|
1453
|
+
}));
|
|
1454
|
+
}
|
|
1455
|
+
async get(groupId) {
|
|
1456
|
+
return parse(GroupReadSchema, await this.http.get(`/groups/${groupId}`, { operation: "groupsGet" }));
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Update group metadata.
|
|
1460
|
+
*
|
|
1461
|
+
* Route: `PATCH /groups/{id}` with body of camelCase → snake_case
|
|
1462
|
+
* mapped fields. Previously missing from the TS SDK.
|
|
1463
|
+
*/
|
|
1464
|
+
async update(groupId, opts) {
|
|
1465
|
+
const body = {};
|
|
1466
|
+
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1467
|
+
if (opts.enrollment !== void 0) body["enrollment_policy"] = opts.enrollment;
|
|
1468
|
+
if (opts.visibility !== void 0) body["visibility"] = opts.visibility;
|
|
1469
|
+
if (opts.voteDurationHours !== void 0) body["vote_duration_hours"] = opts.voteDurationHours;
|
|
1470
|
+
return parse(GroupReadSchema, await this.http.patch(`/groups/${groupId}`, body, {
|
|
1471
|
+
signal: opts.signal,
|
|
1472
|
+
operation: "groupsUpdate"
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Delete a group (owner only). Previously missing from the TS SDK.
|
|
1477
|
+
*/
|
|
1478
|
+
async delete(groupId, opts = {}) {
|
|
1479
|
+
await this.http.delete(`/groups/${groupId}`, {
|
|
1480
|
+
signal: opts.signal,
|
|
1481
|
+
operation: "groupsDelete"
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async join(groupId, opts = {}) {
|
|
1485
|
+
const body = {};
|
|
1486
|
+
if (opts.message !== void 0) body["message"] = opts.message;
|
|
1487
|
+
const res = await this.http.postWithStatus(`/groups/${groupId}/join`, body, {
|
|
1488
|
+
signal: opts.signal,
|
|
1489
|
+
operation: "groupsJoin"
|
|
1490
|
+
});
|
|
1491
|
+
if (res.status === 202) return {
|
|
1492
|
+
status: "pending",
|
|
1493
|
+
request: parse(JoinRequestReadSchema, res.body)
|
|
1494
|
+
};
|
|
1495
|
+
return {
|
|
1496
|
+
status: "joined",
|
|
1497
|
+
member: parse(GroupMemberSchema, res.body)
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
async invite(groupId, agentId, opts) {
|
|
1501
|
+
return parse(InviteResultSchema, await this.http.post(`/groups/${groupId}/invite`, {
|
|
1502
|
+
agent_id: agentId,
|
|
1503
|
+
...opts?.message && { message: opts.message }
|
|
1504
|
+
}, {
|
|
1505
|
+
signal: opts?.signal,
|
|
1506
|
+
operation: "groupsInvite"
|
|
1507
|
+
}));
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Remove a member from a group. Previously missing from the TS SDK.
|
|
1511
|
+
*
|
|
1512
|
+
* Route: `DELETE /groups/{groupId}/members/{agentId}`.
|
|
1513
|
+
*/
|
|
1514
|
+
async removeMember(groupId, agentId, opts = {}) {
|
|
1515
|
+
await this.http.delete(`/groups/${groupId}/members/${agentId}`, {
|
|
1516
|
+
signal: opts.signal,
|
|
1517
|
+
operation: "groupsRemoveMember"
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
async members(groupId) {
|
|
1521
|
+
return parseCursorPage(GroupMemberSchema, await this.http.get(`/groups/${groupId}/members`, { operation: "groupsMembers" }));
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* List pending join requests for a group.
|
|
1525
|
+
*
|
|
1526
|
+
* Route: `GET /groups/{groupId}/requests` (fixes parity gap — TS
|
|
1527
|
+
* previously used `/join-requests`). Returns a plain array to match
|
|
1528
|
+
* Python SDK `groups.list_requests`.
|
|
1529
|
+
*/
|
|
1530
|
+
async listRequests(groupId) {
|
|
1531
|
+
return parseCursorPage(JoinRequestReadSchema, await this.http.get(`/groups/${groupId}/requests`, { operation: "groupsListRequests" })).items;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Vote on a pending join request (fixes audit C-adjacent — missing
|
|
1535
|
+
* group segment).
|
|
1536
|
+
*
|
|
1537
|
+
* Route: `POST /groups/{groupId}/requests/{requestId}/vote`.
|
|
1538
|
+
*/
|
|
1539
|
+
async vote(groupId, requestId, choice) {
|
|
1540
|
+
return parse(VoteResponseSchema, await this.http.post(`/groups/${groupId}/requests/${requestId}/vote`, { vote: choice }, { operation: "groupsVote" }));
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region src/resources/identity.ts
|
|
1545
|
+
/**
|
|
1546
|
+
* Identity resource — whoami, createAgent, listAgents, getAgent, updateAgent,
|
|
1547
|
+
* revokeAgent, rotateKeys, agent cards, poll tokens, quotas.
|
|
1548
|
+
*/
|
|
1549
|
+
var IdentityResource = class {
|
|
1550
|
+
constructor(http) {
|
|
1551
|
+
this.http = http;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Fetch the current org identity, agent list, and trust tier.
|
|
1555
|
+
*
|
|
1556
|
+
* Routes: `GET /agents` + `GET /org` — the server does not expose a
|
|
1557
|
+
* single `/agents/whoami` endpoint; the Python SDK makes both calls and
|
|
1558
|
+
* composes the `WhoAmI` client-side (`_client.py:605-621`). This TS
|
|
1559
|
+
* implementation mirrors that exactly.
|
|
1560
|
+
*/
|
|
1561
|
+
async whoami(opts) {
|
|
1562
|
+
const agents = unwrapAgentsList(await this.http.get("/agents", {
|
|
1563
|
+
signal: opts?.signal,
|
|
1564
|
+
operation: "whoami"
|
|
1565
|
+
})).map((a) => parse(AgentReadSchema, a));
|
|
1566
|
+
const org = parse(OrgReadSchema, await this.http.get("/org", {
|
|
1567
|
+
signal: opts?.signal,
|
|
1568
|
+
operation: "whoami"
|
|
1569
|
+
}));
|
|
1570
|
+
return parse(WhoAmISchema, {
|
|
1571
|
+
org,
|
|
1572
|
+
agents,
|
|
1573
|
+
trust_tier: org.trust_tier ?? 0
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Create a new agent with locally generated E2EE keypairs.
|
|
1578
|
+
*
|
|
1579
|
+
* Matches Python `rine._onboard.create_agent` flow:
|
|
1580
|
+
* 1. Generate Ed25519 + X25519 keypairs via rine-core `generateAgentKeys()`.
|
|
1581
|
+
* 2. POST `/agents` with `{name, human_oversight, unlisted, signing_public_key,
|
|
1582
|
+
* encryption_public_key}` (public keys as JWKs). The server's `AgentCreate`
|
|
1583
|
+
* schema is `extra="forbid"`, so no other fields may be sent on creation.
|
|
1584
|
+
* 3. Persist private keys under `{configDir}/keys/{agentId}/` via
|
|
1585
|
+
* rine-core `saveAgentKeys()` so the new agent can immediately participate
|
|
1586
|
+
* in E2EE send/receive without any manual key setup.
|
|
1587
|
+
*
|
|
1588
|
+
* Partial failure: if the POST succeeds but `saveAgentKeys` throws, the
|
|
1589
|
+
* server-side agent exists but the caller has no private keys — surfaced
|
|
1590
|
+
* as the raw I/O error. Callers must treat this as a partial failure.
|
|
1591
|
+
*/
|
|
1592
|
+
async createAgent(name, opts) {
|
|
1593
|
+
const { signal, ...body } = opts ?? {};
|
|
1594
|
+
requireConfigDir(this.http.configDir, "createAgent");
|
|
1595
|
+
const keys = generateAgentKeys();
|
|
1596
|
+
const agent = parse(AgentReadSchema, await this.http.post("/agents", {
|
|
1597
|
+
name,
|
|
1598
|
+
...body,
|
|
1599
|
+
signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
|
|
1600
|
+
encryption_public_key: encryptionPublicKeyToJWK(keys.encryption.publicKey)
|
|
1601
|
+
}, {
|
|
1602
|
+
signal,
|
|
1603
|
+
operation: "createAgent"
|
|
1604
|
+
}));
|
|
1605
|
+
saveAgentKeys(this.http.configDir, agent.id, keys);
|
|
1606
|
+
if (agent.poll_url) try {
|
|
1607
|
+
const creds = loadCredentials(this.http.configDir);
|
|
1608
|
+
if (creds.default) {
|
|
1609
|
+
creds.default.poll_url = agent.poll_url;
|
|
1610
|
+
saveCredentials(this.http.configDir, creds);
|
|
1611
|
+
}
|
|
1612
|
+
} catch {}
|
|
1613
|
+
return agent;
|
|
1614
|
+
}
|
|
1615
|
+
async listAgents(opts) {
|
|
1616
|
+
return parseCursorPage(AgentReadSchema, await this.http.get("/agents", {
|
|
1617
|
+
params: { include_revoked: opts?.includeRevoked },
|
|
1618
|
+
operation: "listAgents"
|
|
1619
|
+
})).items;
|
|
1620
|
+
}
|
|
1621
|
+
async getAgent(agentId) {
|
|
1622
|
+
return parse(AgentReadSchema, await this.http.get(`/agents/${agentId}`, { operation: "getAgent" }));
|
|
1623
|
+
}
|
|
1624
|
+
async updateAgent(agentId, patch) {
|
|
1625
|
+
return parse(AgentReadSchema, await this.http.patch(`/agents/${agentId}`, patch, { operation: "updateAgent" }));
|
|
1626
|
+
}
|
|
1627
|
+
async revokeAgent(agentId) {
|
|
1628
|
+
return parse(AgentReadSchema, await this.http.delete(`/agents/${agentId}`, { operation: "revokeAgent" }));
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Rotate this agent's signing + encryption keypairs.
|
|
1632
|
+
*
|
|
1633
|
+
* Route: `POST /agents/{id}/keys`.
|
|
1634
|
+
*
|
|
1635
|
+
* Generates fresh Ed25519 + X25519 keypairs locally, uploads the public
|
|
1636
|
+
* halves to the server (JWK-encoded), then overwrites the on-disk private
|
|
1637
|
+
* keys via `saveAgentKeys()`. Matches Python `AsyncRineClient.rotate_keys`
|
|
1638
|
+
* flow.
|
|
1639
|
+
*
|
|
1640
|
+
* Key rotation is visible to future peers immediately; messages encrypted
|
|
1641
|
+
* to the previous encryption key can still be decrypted as long as the
|
|
1642
|
+
* caller retains a copy of the old private key out-of-band (the SDK does
|
|
1643
|
+
* not keep a history of rotated-out keys).
|
|
1644
|
+
*
|
|
1645
|
+
* Partial failure: if the POST succeeds but `saveAgentKeys` throws mid-write,
|
|
1646
|
+
* the server has the new public keys while the on-disk state may be
|
|
1647
|
+
* inconsistent (`saveAgentKeys` writes signing.key then encryption.key as
|
|
1648
|
+
* two separate file ops, not atomically). Senders will encrypt to the new
|
|
1649
|
+
* keys the server advertises but the client may still hold the old private
|
|
1650
|
+
* half — silent decryption failure. Track as rine-core follow-up.
|
|
1651
|
+
*/
|
|
1652
|
+
async rotateKeys(agentId, opts) {
|
|
1653
|
+
requireConfigDir(this.http.configDir, "rotateKeys");
|
|
1654
|
+
const keys = generateAgentKeys();
|
|
1655
|
+
const agent = parse(AgentReadSchema, await this.http.post(`/agents/${agentId}/keys`, {
|
|
1656
|
+
signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
|
|
1657
|
+
encryption_public_key: encryptionPublicKeyToJWK(keys.encryption.publicKey)
|
|
1658
|
+
}, {
|
|
1659
|
+
signal: opts?.signal,
|
|
1660
|
+
operation: "rotateKeys"
|
|
1661
|
+
}));
|
|
1662
|
+
saveAgentKeys(this.http.configDir, agentId, keys);
|
|
1663
|
+
return agent;
|
|
1664
|
+
}
|
|
1665
|
+
async getAgentCard(agentId) {
|
|
1666
|
+
return parse(AgentCardSchema, await this.http.get(`/agents/${agentId}/card`, { operation: "getAgentCard" }));
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Upsert an agent card.
|
|
1670
|
+
*
|
|
1671
|
+
* Body shape: `{name, description, version?, is_public?, skills?, rine?}`
|
|
1672
|
+
* where `categories`, `languages` and `pricing_model` are nested inside
|
|
1673
|
+
* the `rine` sub-object — matches Python exactly. The previous TS
|
|
1674
|
+
* flattened everything, which the server silently dropped.
|
|
1675
|
+
*/
|
|
1676
|
+
async setAgentCard(agentId, card) {
|
|
1677
|
+
const body = toAgentCardBody(card);
|
|
1678
|
+
return parse(AgentCardSchema, await this.http.put(`/agents/${agentId}/card`, body, { operation: "setAgentCard" }));
|
|
1679
|
+
}
|
|
1680
|
+
async deleteAgentCard(agentId) {
|
|
1681
|
+
await this.http.delete(`/agents/${agentId}/card`, { operation: "deleteAgentCard" });
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Regenerate the agent's poll token.
|
|
1685
|
+
*
|
|
1686
|
+
* Route: `POST /agents/{id}/poll-token` — the old `/poll-token/regenerate`
|
|
1687
|
+
* path never existed on the server.
|
|
1688
|
+
*/
|
|
1689
|
+
async regeneratePollToken(agentId) {
|
|
1690
|
+
return parse(PollTokenResponseSchema, await this.http.post(`/agents/${agentId}/poll-token`, void 0, { operation: "regeneratePollToken" }));
|
|
1691
|
+
}
|
|
1692
|
+
async revokePollToken(agentId) {
|
|
1693
|
+
await this.http.delete(`/agents/${agentId}/poll-token`, { operation: "revokePollToken" });
|
|
1694
|
+
}
|
|
1695
|
+
async getQuotas() {
|
|
1696
|
+
return parse(OrgQuotasSchema, await this.http.get("/org/quotas", { operation: "getQuotas" }));
|
|
1697
|
+
}
|
|
1698
|
+
/** Lazy-cached org ID for erase/export URL paths. */
|
|
1699
|
+
cachedOrgId = null;
|
|
1700
|
+
/** Fetch the caller's org UUID, caching on the instance. */
|
|
1701
|
+
async getOrgId(opts) {
|
|
1702
|
+
if (this.cachedOrgId) return this.cachedOrgId;
|
|
1703
|
+
this.cachedOrgId = (await this.whoami(opts)).org.id;
|
|
1704
|
+
return this.cachedOrgId;
|
|
1705
|
+
}
|
|
1706
|
+
async updateOrg(patch, opts) {
|
|
1707
|
+
const body = {};
|
|
1708
|
+
if (patch.name !== void 0) body.name = patch.name;
|
|
1709
|
+
if (patch.contact_email !== void 0) body.contact_email = patch.contact_email;
|
|
1710
|
+
if (patch.country_code !== void 0) body.country_code = patch.country_code;
|
|
1711
|
+
if (patch.slug !== void 0) body.slug = patch.slug;
|
|
1712
|
+
const org = parse(OrgReadSchema, await this.http.patch("/org", body, {
|
|
1713
|
+
signal: opts?.signal,
|
|
1714
|
+
operation: "updateOrg"
|
|
1715
|
+
}));
|
|
1716
|
+
this.cachedOrgId = org.id;
|
|
1717
|
+
return org;
|
|
1718
|
+
}
|
|
1719
|
+
async eraseOrg(opts) {
|
|
1720
|
+
if (opts.confirm !== true) throw new RineError("eraseOrg() permanently destroys all data. Pass { confirm: true } to proceed.");
|
|
1721
|
+
const orgId = await this.getOrgId({ signal: opts.signal });
|
|
1722
|
+
return parse(ErasureResultSchema, await this.http.delete(`/orgs/${orgId}`, {
|
|
1723
|
+
signal: opts.signal,
|
|
1724
|
+
operation: "eraseOrg"
|
|
1725
|
+
}));
|
|
1726
|
+
}
|
|
1727
|
+
async exportOrg(opts) {
|
|
1728
|
+
const orgId = await this.getOrgId(opts);
|
|
1729
|
+
const res = await this.http.get(`/orgs/${orgId}/export`, {
|
|
1730
|
+
signal: opts?.signal,
|
|
1731
|
+
extraHeaders: { Accept: "application/x-ndjson" },
|
|
1732
|
+
operation: "exportOrg"
|
|
1733
|
+
});
|
|
1734
|
+
if (Array.isArray(res)) return res;
|
|
1735
|
+
if (typeof res === "string") return res.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
1736
|
+
return [res];
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
/**
|
|
1740
|
+
* Guard against the `SDKHttpClient` default `_configDir = ""` footgun: with
|
|
1741
|
+
* an empty configDir, `saveAgentKeys` resolves `keys/{agentId}` against
|
|
1742
|
+
* `process.cwd()` and silently writes private keys into the caller's working
|
|
1743
|
+
* directory. Any method that persists keypairs must call this first.
|
|
1744
|
+
*/
|
|
1745
|
+
function requireConfigDir(configDir, method) {
|
|
1746
|
+
if (configDir === "") throw new Error(`${method}() requires a non-empty SDK configDir — pass one via new AsyncRineClient({ configDir }) so private keys persist under {configDir}/keys/{agentId}/ instead of the current working directory.`);
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* `GET /agents` sometimes returns a bare array (paginated list default) and
|
|
1750
|
+
* sometimes a `{items: [...]}` envelope depending on version. The Python SDK
|
|
1751
|
+
* handles both (`data.get("items", data)` — `_client.py:612`); do the same.
|
|
1752
|
+
*/
|
|
1753
|
+
function unwrapAgentsList(raw) {
|
|
1754
|
+
if (Array.isArray(raw)) return raw;
|
|
1755
|
+
if (typeof raw === "object" && raw !== null && "items" in raw && Array.isArray(raw.items)) return raw.items;
|
|
1756
|
+
return [];
|
|
1757
|
+
}
|
|
1758
|
+
function toAgentCardBody(card) {
|
|
1759
|
+
const rine = {};
|
|
1760
|
+
if (card.categories !== void 0) rine["categories"] = card.categories;
|
|
1761
|
+
if (card.languages !== void 0) rine["languages"] = card.languages;
|
|
1762
|
+
if (card.pricing_model !== void 0) rine["pricing_model"] = card.pricing_model;
|
|
1763
|
+
const body = {
|
|
1764
|
+
name: card.name,
|
|
1765
|
+
description: card.description
|
|
1766
|
+
};
|
|
1767
|
+
if (card.version !== void 0) body["version"] = card.version;
|
|
1768
|
+
if (card.is_public !== void 0) body["is_public"] = card.is_public;
|
|
1769
|
+
if (card.skills !== void 0) body["skills"] = card.skills;
|
|
1770
|
+
if (Object.keys(rine).length > 0) body["rine"] = rine;
|
|
1771
|
+
return body;
|
|
1772
|
+
}
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/resources/messages.ts
|
|
1775
|
+
/**
|
|
1776
|
+
* Messages resource — send, inbox, read, reply, sendAndWait.
|
|
1777
|
+
*
|
|
1778
|
+
* All outbound messages are E2E-encrypted before leaving the SDK. Inbound
|
|
1779
|
+
* messages are auto-decrypted with sender-key retry on read().
|
|
1780
|
+
*/
|
|
1781
|
+
const DEFAULT_SEND_TYPE = "rine.v1.task_request";
|
|
1782
|
+
const DEFAULT_REPLY_TYPE = "rine.v1.task_response";
|
|
1783
|
+
var MessagesResource = class {
|
|
1784
|
+
constructor(http) {
|
|
1785
|
+
this.http = http;
|
|
1786
|
+
}
|
|
1787
|
+
async send(to, payload, opts = {}) {
|
|
1788
|
+
const op = this.beginOp(opts.signal);
|
|
1789
|
+
try {
|
|
1790
|
+
const core = this.http.getCoreHttpClient({ signal: op.signal });
|
|
1791
|
+
const senderId = await this.resolveSenderId(core, opts.agent);
|
|
1792
|
+
const body = await this.buildSendBody(core, senderId, to, payload, opts);
|
|
1793
|
+
return parse(MessageReadSchema, await this.http.post("/messages", body, {
|
|
1794
|
+
signal: op.signal,
|
|
1795
|
+
operation: "send",
|
|
1796
|
+
extraHeaders: mergeHeaders(idempotencyHeader(opts.idempotencyKey), agentOverrideHeader(opts.agent))
|
|
1797
|
+
}));
|
|
1798
|
+
} finally {
|
|
1799
|
+
op.clear();
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Fetch the current agent's inbox with auto-decryption (fixes audit C2).
|
|
1804
|
+
*
|
|
1805
|
+
* Route is `GET /agents/{agentId}/messages` — requires resolving the
|
|
1806
|
+
* caller's agent UUID up-front (via the same `resolveSenderId()` path as
|
|
1807
|
+
* `send()` / `reply()`), so the inbox is always scoped to one specific
|
|
1808
|
+
* agent even in multi-agent orgs.
|
|
1809
|
+
*
|
|
1810
|
+
* Decryption runs per-item via `decryptEnvelope()`. Failures populate
|
|
1811
|
+
* `decrypt_error` on the returned `DecryptedMessage` instead of throwing
|
|
1812
|
+
* — the page still contains the envelope, just without plaintext. This
|
|
1813
|
+
* matches the Python SDK's "best-effort" inbox contract.
|
|
1814
|
+
*/
|
|
1815
|
+
async inbox(opts = {}) {
|
|
1816
|
+
const op = this.beginOp(opts.signal);
|
|
1817
|
+
try {
|
|
1818
|
+
const core = this.http.getCoreHttpClient({ signal: op.signal });
|
|
1819
|
+
const agentId = await this.resolveSenderId(core, opts.agent);
|
|
1820
|
+
const page = parseCursorPage(DecryptedMessageSchema, await this.http.get(`/agents/${agentId}/messages`, {
|
|
1821
|
+
params: {
|
|
1822
|
+
limit: opts.limit,
|
|
1823
|
+
cursor: opts.cursor
|
|
1824
|
+
},
|
|
1825
|
+
signal: op.signal,
|
|
1826
|
+
operation: "inbox",
|
|
1827
|
+
extraHeaders: agentOverrideHeader(opts.agent)
|
|
1828
|
+
}));
|
|
1829
|
+
return new CursorPage(await Promise.all(page.items.map((item) => decryptEnvelope({
|
|
1830
|
+
core,
|
|
1831
|
+
configDir: this.http.configDir,
|
|
1832
|
+
agentId,
|
|
1833
|
+
msg: item
|
|
1834
|
+
}))), page.total, page.nextCursor, page.prevCursor);
|
|
1835
|
+
} finally {
|
|
1836
|
+
op.clear();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Fetch and decrypt a single message by id (fixes audit C3).
|
|
1841
|
+
*
|
|
1842
|
+
* Dispatches to `decryptMessage` (HPKE) or `decryptGroupMessage`
|
|
1843
|
+
* (sender-key) based on `encryption_version`. On a sender-key failure
|
|
1844
|
+
* (e.g. the reader has not yet ingested the sender's distribution),
|
|
1845
|
+
* retries once after calling `fetchAndIngestPendingSKDistributions` via
|
|
1846
|
+
* the rine-core bridge — matches Python SDK discipline.
|
|
1847
|
+
*
|
|
1848
|
+
* Failures on the HPKE path or after the sender-key retry populate
|
|
1849
|
+
* `decrypt_error` on the returned message instead of throwing, so
|
|
1850
|
+
* callers can branch on `msg.decrypt_error !== null`.
|
|
1851
|
+
*/
|
|
1852
|
+
async read(messageId, opts = {}) {
|
|
1853
|
+
const op = this.beginOp(opts.signal);
|
|
1854
|
+
try {
|
|
1855
|
+
const core = this.http.getCoreHttpClient({ signal: op.signal });
|
|
1856
|
+
const agentId = await this.resolveSenderId(core, opts.agent);
|
|
1857
|
+
const msg = parse(DecryptedMessageSchema, await this.http.get(`/messages/${messageId}`, {
|
|
1858
|
+
signal: op.signal,
|
|
1859
|
+
operation: "read",
|
|
1860
|
+
extraHeaders: agentOverrideHeader(opts.agent)
|
|
1861
|
+
}));
|
|
1862
|
+
return decryptEnvelope({
|
|
1863
|
+
core,
|
|
1864
|
+
configDir: this.http.configDir,
|
|
1865
|
+
agentId,
|
|
1866
|
+
msg,
|
|
1867
|
+
retrySenderKey: true
|
|
1868
|
+
});
|
|
1869
|
+
} finally {
|
|
1870
|
+
op.clear();
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
async reply(messageId, payload, opts = {}) {
|
|
1874
|
+
const op = this.beginOp(opts.signal);
|
|
1875
|
+
try {
|
|
1876
|
+
const core = this.http.getCoreHttpClient({ signal: op.signal });
|
|
1877
|
+
const original = parse(MessageReadSchema, await core.get(`/messages/${messageId}`));
|
|
1878
|
+
const recipientId = original.from_agent_id;
|
|
1879
|
+
if (!recipientId) throw new Error(`Cannot reply to message ${messageId}: original has no sender (system message?)`);
|
|
1880
|
+
const agents = await fetchAgents(core);
|
|
1881
|
+
const ownedAgentIds = new Set(agents.map((a) => a.id));
|
|
1882
|
+
const toAgentIdIfOwned = original.to_agent_id && ownedAgentIds.has(original.to_agent_id) ? original.to_agent_id : void 0;
|
|
1883
|
+
const senderHint = opts.agent ?? toAgentIdIfOwned ?? this.defaultAgentHint();
|
|
1884
|
+
const senderId = await this.resolveSenderId(core, senderHint, agents);
|
|
1885
|
+
if (recipientId === senderId) throw new Error(`Cannot reply to your own message (${messageId}). Send a new message to continue the conversation.`);
|
|
1886
|
+
const recipientPk = await fetchRecipientEncryptionKey(core, recipientId);
|
|
1887
|
+
const encrypted = await encryptMessage(this.http.configDir, senderId, recipientPk, payload);
|
|
1888
|
+
const body = {
|
|
1889
|
+
type: opts.type ?? DEFAULT_REPLY_TYPE,
|
|
1890
|
+
content_type: opts.contentType ?? "application/json",
|
|
1891
|
+
...encrypted
|
|
1892
|
+
};
|
|
1893
|
+
if (opts.metadata) body.metadata = opts.metadata;
|
|
1894
|
+
return parse(MessageReadSchema, await this.http.post(`/messages/${messageId}/reply`, body, {
|
|
1895
|
+
signal: op.signal,
|
|
1896
|
+
operation: "reply",
|
|
1897
|
+
extraHeaders: mergeHeaders(idempotencyHeader(opts.idempotencyKey), agentOverrideHeader(opts.agent))
|
|
1898
|
+
}));
|
|
1899
|
+
} finally {
|
|
1900
|
+
op.clear();
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Atomic send-and-wait via `POST /messages/sync`.
|
|
1905
|
+
*
|
|
1906
|
+
* The server's `/messages/sync` endpoint accepts a full `MessageCreate`
|
|
1907
|
+
* body (same shape as `POST /messages`) plus a `?timeout_ms` query param.
|
|
1908
|
+
* It creates the message, delivers it, then long-polls for a reply — all
|
|
1909
|
+
* in one HTTP request. Returns `SyncMessageResponse = { message, reply,
|
|
1910
|
+
* conversation_id, status }`.
|
|
1911
|
+
*
|
|
1912
|
+
* Returns `{ sent, reply }`. `reply` is null on long-poll timeout.
|
|
1913
|
+
*
|
|
1914
|
+
* Note: the server rejects group handles on `/messages/sync`, so this
|
|
1915
|
+
* method is 1:1 DMs only.
|
|
1916
|
+
*/
|
|
1917
|
+
async sendAndWait(to, payload, opts = {}) {
|
|
1918
|
+
if (isGroupHandle(to)) throw new Error("sendAndWait() does not support group handles — the server's /messages/sync endpoint is 1:1 only.");
|
|
1919
|
+
const op = this.beginOp(opts.signal);
|
|
1920
|
+
try {
|
|
1921
|
+
const core = this.http.getCoreHttpClient({ signal: op.signal });
|
|
1922
|
+
const senderId = await this.resolveSenderId(core, opts.agent);
|
|
1923
|
+
const body = await this.buildSendBody(core, senderId, to, payload, {
|
|
1924
|
+
type: opts.type,
|
|
1925
|
+
contentType: opts.contentType,
|
|
1926
|
+
metadata: opts.metadata
|
|
1927
|
+
});
|
|
1928
|
+
const timeoutMs = opts.timeout ?? 3e4;
|
|
1929
|
+
const raw = await this.http.post("/messages/sync", body, {
|
|
1930
|
+
params: { timeout_ms: timeoutMs },
|
|
1931
|
+
signal: op.signal,
|
|
1932
|
+
operation: "sendAndWait",
|
|
1933
|
+
extraHeaders: mergeHeaders(idempotencyHeader(opts.idempotencyKey), agentOverrideHeader(opts.agent))
|
|
1934
|
+
});
|
|
1935
|
+
const sent = parse(MessageReadSchema, raw?.message);
|
|
1936
|
+
let decryptedReply = null;
|
|
1937
|
+
const rawReply = raw?.reply;
|
|
1938
|
+
if (rawReply != null) {
|
|
1939
|
+
const replyMsg = parse(DecryptedMessageSchema, rawReply);
|
|
1940
|
+
decryptedReply = await decryptEnvelope({
|
|
1941
|
+
core,
|
|
1942
|
+
configDir: this.http.configDir,
|
|
1943
|
+
agentId: senderId,
|
|
1944
|
+
msg: replyMsg,
|
|
1945
|
+
retrySenderKey: true
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
sent,
|
|
1950
|
+
reply: decryptedReply
|
|
1951
|
+
};
|
|
1952
|
+
} finally {
|
|
1953
|
+
op.clear();
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Build the `POST /messages` body with real E2EE output.
|
|
1958
|
+
*
|
|
1959
|
+
* - Group handle → `getOrCreateSenderKey` → `encryptGroupMessage` → body
|
|
1960
|
+
* carries `to_handle` + sender-key envelope.
|
|
1961
|
+
* - Everything else (agent handle, agent UUID, group UUID) → resolve to
|
|
1962
|
+
* UUID via rine-core, HPKE-seal with `encryptMessage`, body carries
|
|
1963
|
+
* `to_agent_id` + HPKE envelope.
|
|
1964
|
+
*
|
|
1965
|
+
* Note on group UUIDs: the server body shape for groups requires the
|
|
1966
|
+
* handle (`to_handle`). Callers routing a group by UUID must pass the
|
|
1967
|
+
* group handle instead — the UUID form is accepted at the type level for
|
|
1968
|
+
* API symmetry but will be encrypted as a 1:1 and rejected server-side.
|
|
1969
|
+
*/
|
|
1970
|
+
async buildSendBody(core, senderId, to, payload, opts) {
|
|
1971
|
+
const target = to;
|
|
1972
|
+
const base = {
|
|
1973
|
+
type: opts.type ?? DEFAULT_SEND_TYPE,
|
|
1974
|
+
content_type: opts.contentType ?? "application/json"
|
|
1975
|
+
};
|
|
1976
|
+
if (opts.metadata) base.metadata = opts.metadata;
|
|
1977
|
+
if (opts.parentConversationId) base.parent_conversation_id = opts.parentConversationId;
|
|
1978
|
+
if (opts.conversationMetadata) base.conversation_metadata = opts.conversationMetadata;
|
|
1979
|
+
if (isGroupHandle(target)) {
|
|
1980
|
+
const extraHeaders = { "X-Rine-Agent": senderId };
|
|
1981
|
+
const { state, groupId } = await getOrCreateSenderKey(core, this.http.configDir, senderId, target, extraHeaders);
|
|
1982
|
+
const { result } = await encryptGroupMessage(this.http.configDir, senderId, groupId, state, payload);
|
|
1983
|
+
return {
|
|
1984
|
+
...base,
|
|
1985
|
+
to_handle: target,
|
|
1986
|
+
...result
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
const recipientId = await resolveToUuid(this.http.apiUrl, target);
|
|
1990
|
+
const recipientPk = await fetchRecipientEncryptionKey(core, recipientId);
|
|
1991
|
+
const encrypted = await encryptMessage(this.http.configDir, senderId, recipientPk, payload);
|
|
1992
|
+
return {
|
|
1993
|
+
...base,
|
|
1994
|
+
to_agent_id: recipientId,
|
|
1995
|
+
...encrypted
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Resolve the sender agent to a UUID suitable for loading local keys.
|
|
2000
|
+
*
|
|
2001
|
+
* Precedence: explicit override → SDK default agent → single-agent
|
|
2002
|
+
* shortcut (errors on multi-agent orgs).
|
|
2003
|
+
*
|
|
2004
|
+
* Makes one `GET /agents` request per send; caching is deferred (the
|
|
2005
|
+
* Python SDK and rine-cli do not cache either).
|
|
2006
|
+
*
|
|
2007
|
+
* Also verifies the resolved agent is in the caller's own org. `resolveAgent`
|
|
2008
|
+
* short-circuits on UUIDs without validating membership, so a foreign-org
|
|
2009
|
+
* UUID (e.g. an `original.to_agent_id` from a misrouted reply) would slip
|
|
2010
|
+
* through and fail later inside `loadAgentKeys` with a confusing "keys file
|
|
2011
|
+
* not found" filesystem error. The membership check turns that into a clear
|
|
2012
|
+
* "not your agent" error (addresses Step 11 audit R3).
|
|
2013
|
+
*
|
|
2014
|
+
* Exposed as `public` (not `private`) so `client.messages()` (§11.1) can
|
|
2015
|
+
* reuse the same resolution path for its SSE-bound decryption loop.
|
|
2016
|
+
*/
|
|
2017
|
+
async resolveSenderId(core, explicit, preFetchedAgents) {
|
|
2018
|
+
const hint = explicit ?? this.defaultAgentHint();
|
|
2019
|
+
const agents = preFetchedAgents ?? await fetchAgents(core);
|
|
2020
|
+
const senderId = await resolveAgent(this.http.apiUrl, agents, hint);
|
|
2021
|
+
if (!agents.some((a) => a.id === senderId)) throw new Error(`Agent ${senderId} is not owned by the current org — cannot sign on its behalf. Pass an agent from your own org via opts.agent, or omit it to use the default.`);
|
|
2022
|
+
return senderId;
|
|
2023
|
+
}
|
|
2024
|
+
defaultAgentHint() {
|
|
2025
|
+
return this.http.agentHeader?.["X-Rine-Agent"];
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Open an operation-scoped cancellation window that spans the whole
|
|
2029
|
+
* encrypt → post chain. One timer, one AbortController. The `clear`
|
|
2030
|
+
* callback must run in `finally` to satisfy audit MINOR-4 (timer leak).
|
|
2031
|
+
*
|
|
2032
|
+
* The op-timer aborts with a `RineTimeoutError` reason so that callers
|
|
2033
|
+
* and `SDKHttpClient.request()` can distinguish user-signal abort
|
|
2034
|
+
* (native AbortError) from SDK op-timer expiry (`RineTimeoutError`).
|
|
2035
|
+
* Fixes the step-27 crypto audit Cr1 (SPEC §14.4).
|
|
2036
|
+
*/
|
|
2037
|
+
beginOp(userSignal) {
|
|
2038
|
+
const timeoutAC = new AbortController();
|
|
2039
|
+
const timer = setTimeout(() => timeoutAC.abort(new RineTimeoutError(`Operation timed out after ${this.http.timeout}ms`)), this.http.timeout);
|
|
2040
|
+
return {
|
|
2041
|
+
signal: userSignal ? anySignal(userSignal, timeoutAC.signal) : timeoutAC.signal,
|
|
2042
|
+
clear: () => clearTimeout(timer)
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
function idempotencyHeader(key) {
|
|
2047
|
+
if (!key) return void 0;
|
|
2048
|
+
return { "Idempotency-Key": key };
|
|
2049
|
+
}
|
|
2050
|
+
function agentOverrideHeader(agent) {
|
|
2051
|
+
if (!agent) return void 0;
|
|
2052
|
+
return { "X-Rine-Agent": agent };
|
|
2053
|
+
}
|
|
2054
|
+
function mergeHeaders(...parts) {
|
|
2055
|
+
const merged = {};
|
|
2056
|
+
for (const part of parts) if (part) Object.assign(merged, part);
|
|
2057
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2058
|
+
}
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region src/resources/polling.ts
|
|
2061
|
+
/**
|
|
2062
|
+
* Polling resource — lightweight count check + callback-based watch().
|
|
2063
|
+
*/
|
|
2064
|
+
var PollingResource = class {
|
|
2065
|
+
constructor(http) {
|
|
2066
|
+
this.http = http;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Lightweight poll — returns message count for the authenticated agent.
|
|
2070
|
+
* No auth required, firewall-friendly.
|
|
2071
|
+
*
|
|
2072
|
+
* Route: reads `poll_url` from the default credential entry (a full path
|
|
2073
|
+
* like `/poll/<token>` stamped in at agent-create time). The server does
|
|
2074
|
+
* NOT expose a bare `/poll` endpoint — this was a route drift in the
|
|
2075
|
+
* original TS rework surfaced by the step-27 audit. Matches Python
|
|
2076
|
+
* `AsyncRineClient.poll` (`_client.py:623-636`).
|
|
2077
|
+
*
|
|
2078
|
+
* @example
|
|
2079
|
+
* ```ts
|
|
2080
|
+
* const count = await client.polling.poll();
|
|
2081
|
+
* ```
|
|
2082
|
+
*/
|
|
2083
|
+
async poll(opts) {
|
|
2084
|
+
const pollUrl = loadCredentials(this.http.configDir).default?.poll_url;
|
|
2085
|
+
if (!pollUrl) throw new ConfigError("No poll URL. Create an agent first with client.createAgent().");
|
|
2086
|
+
const raw = await this.http.get(pollUrl, {
|
|
2087
|
+
unauthenticated: true,
|
|
2088
|
+
signal: opts?.signal,
|
|
2089
|
+
operation: "poll"
|
|
2090
|
+
});
|
|
2091
|
+
if (typeof raw === "number") return raw;
|
|
2092
|
+
if (typeof raw === "object" && raw !== null && "count" in raw) return raw.count;
|
|
2093
|
+
return Number(raw);
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* watchPoll — call a callback each time the poll count increases.
|
|
2097
|
+
*
|
|
2098
|
+
* Returns an unsubscribe function.
|
|
2099
|
+
*
|
|
2100
|
+
* @example
|
|
2101
|
+
* ```ts
|
|
2102
|
+
* const unsubscribe = await client.polling.watchPoll((newCount) => {
|
|
2103
|
+
* console.log('New message count:', newCount);
|
|
2104
|
+
* });
|
|
2105
|
+
* // Later: unsubscribe()
|
|
2106
|
+
* ```
|
|
2107
|
+
*/
|
|
2108
|
+
watchPoll(onCountChange, opts = {}) {
|
|
2109
|
+
const { interval = 5e3, signal } = opts;
|
|
2110
|
+
let lastCount = 0;
|
|
2111
|
+
let stopped = false;
|
|
2112
|
+
const tick = async () => {
|
|
2113
|
+
while (!stopped) {
|
|
2114
|
+
if (signal?.aborted) break;
|
|
2115
|
+
try {
|
|
2116
|
+
const count = await this.poll();
|
|
2117
|
+
if (count > lastCount) {
|
|
2118
|
+
lastCount = count;
|
|
2119
|
+
onCountChange(count);
|
|
2120
|
+
}
|
|
2121
|
+
} catch {}
|
|
2122
|
+
await sleepWithSignal(interval, signal);
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
tick();
|
|
2126
|
+
return () => {
|
|
2127
|
+
stopped = true;
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region src/utils/sse.ts
|
|
2133
|
+
/**
|
|
2134
|
+
* Convert a fetch Response with an SSE body into an AsyncIterable of RineEvent.
|
|
2135
|
+
*
|
|
2136
|
+
* @param response - A fetch Response whose body is an SSE stream.
|
|
2137
|
+
* @param signal - AbortSignal for cancellation.
|
|
2138
|
+
*
|
|
2139
|
+
* Usage:
|
|
2140
|
+
* ```ts
|
|
2141
|
+
* const resp = await fetch(url, { headers: { Accept: 'text/event-stream' } });
|
|
2142
|
+
* for await (const event of toAsyncIter(resp, signal)) {
|
|
2143
|
+
* console.log(event.type, event.data);
|
|
2144
|
+
* }
|
|
2145
|
+
* ```
|
|
2146
|
+
*/
|
|
2147
|
+
async function* toAsyncIter(response, signal) {
|
|
2148
|
+
if (!response.body) return;
|
|
2149
|
+
const reader = response.body.getReader();
|
|
2150
|
+
const decoder = new TextDecoder();
|
|
2151
|
+
let buffer = "";
|
|
2152
|
+
let eventType = "";
|
|
2153
|
+
let eventData = "";
|
|
2154
|
+
let eventId = null;
|
|
2155
|
+
const warnedUnknownTypes = /* @__PURE__ */ new Set();
|
|
2156
|
+
const cleanup = () => {
|
|
2157
|
+
reader.cancel().catch(() => {});
|
|
2158
|
+
};
|
|
2159
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
2160
|
+
if (signal.aborted) {
|
|
2161
|
+
signal.removeEventListener("abort", cleanup);
|
|
2162
|
+
cleanup();
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const flush = function* () {
|
|
2166
|
+
if (!(eventType || eventData)) return;
|
|
2167
|
+
const event = parseRineEvent(eventType, eventData, eventId ?? void 0, warnedUnknownTypes);
|
|
2168
|
+
if (event !== null) yield event;
|
|
2169
|
+
};
|
|
2170
|
+
try {
|
|
2171
|
+
while (true) {
|
|
2172
|
+
const { done, value } = await reader.read();
|
|
2173
|
+
if (done) break;
|
|
2174
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2175
|
+
const lines = buffer.split("\n");
|
|
2176
|
+
buffer = lines[lines.length - 1] ?? "";
|
|
2177
|
+
for (const raw of lines.slice(0, -1)) {
|
|
2178
|
+
const line = parseLine(raw);
|
|
2179
|
+
if (line === null) continue;
|
|
2180
|
+
if (line.field === "dispatch") {
|
|
2181
|
+
yield* flush();
|
|
2182
|
+
eventType = "";
|
|
2183
|
+
eventData = "";
|
|
2184
|
+
eventId = null;
|
|
2185
|
+
} else if (line.field === "event") eventType = line.value;
|
|
2186
|
+
else if (line.field === "data") {
|
|
2187
|
+
if (eventData) eventData += "\n";
|
|
2188
|
+
eventData += line.value;
|
|
2189
|
+
} else if (line.field === "id") eventId = line.value;
|
|
2190
|
+
}
|
|
2191
|
+
if (signal.aborted) break;
|
|
2192
|
+
}
|
|
2193
|
+
yield* flush();
|
|
2194
|
+
} finally {
|
|
2195
|
+
signal.removeEventListener("abort", cleanup);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function parseLine(raw) {
|
|
2199
|
+
const line = raw.trimEnd();
|
|
2200
|
+
if (!line) return { field: "dispatch" };
|
|
2201
|
+
if (line.startsWith(":")) return null;
|
|
2202
|
+
const colonIdx = line.indexOf(":");
|
|
2203
|
+
if (colonIdx === -1) return null;
|
|
2204
|
+
const field = line.slice(0, colonIdx);
|
|
2205
|
+
const rawValue = line.slice(colonIdx + 1);
|
|
2206
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
2207
|
+
if (field === "event") return {
|
|
2208
|
+
field: "event",
|
|
2209
|
+
value
|
|
2210
|
+
};
|
|
2211
|
+
if (field === "data") return {
|
|
2212
|
+
field: "data",
|
|
2213
|
+
value
|
|
2214
|
+
};
|
|
2215
|
+
if (field === "id") return {
|
|
2216
|
+
field: "id",
|
|
2217
|
+
value
|
|
2218
|
+
};
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
function parseRineEvent(rawType, data, id, warnedUnknownTypes) {
|
|
2222
|
+
const type = mapEventType(rawType);
|
|
2223
|
+
if (type === null) {
|
|
2224
|
+
if (!warnedUnknownTypes.has(rawType)) {
|
|
2225
|
+
warnedUnknownTypes.add(rawType);
|
|
2226
|
+
console.warn(`rine: unknown SSE event type ignored: ${rawType}`);
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
return {
|
|
2231
|
+
type,
|
|
2232
|
+
data,
|
|
2233
|
+
id
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
function mapEventType(raw) {
|
|
2237
|
+
switch (raw) {
|
|
2238
|
+
case "message": return "message";
|
|
2239
|
+
case "heartbeat": return "heartbeat";
|
|
2240
|
+
case "status": return "status";
|
|
2241
|
+
case "disconnect": return "disconnect";
|
|
2242
|
+
case "": return "message";
|
|
2243
|
+
default: return null;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
//#endregion
|
|
2247
|
+
//#region src/utils/seen-ids.ts
|
|
2248
|
+
/**
|
|
2249
|
+
* Bounded FIFO seen-ID set.
|
|
2250
|
+
*
|
|
2251
|
+
* Used by both the SSE stream layer (reconnect replay dedup per SPEC_v2 §12.3.3)
|
|
2252
|
+
* and the poll-based `watch()` path (see `client.ts`). On reconnect with
|
|
2253
|
+
* `Last-Event-ID`, the server's catch-up phase can race with the NOTIFY
|
|
2254
|
+
* delivery and re-emit events already seen; this class is the guard against
|
|
2255
|
+
* dispatching them twice.
|
|
2256
|
+
*
|
|
2257
|
+
* Capacity defaults to 500 (SPEC §12.3.3 N — ~18 KB of RAM, ~10 minutes of
|
|
2258
|
+
* traffic at 1 msg/s — comfortably larger than any plausible reconnect
|
|
2259
|
+
* replay window, small enough to stay negligible).
|
|
2260
|
+
*/
|
|
2261
|
+
var SeenIds = class {
|
|
2262
|
+
set = /* @__PURE__ */ new Set();
|
|
2263
|
+
queue = [];
|
|
2264
|
+
constructor(capacity = 500) {
|
|
2265
|
+
this.capacity = capacity;
|
|
2266
|
+
}
|
|
2267
|
+
/** Returns true if the id is new (caller should dispatch), false if duplicate. */
|
|
2268
|
+
record(id) {
|
|
2269
|
+
if (this.set.has(id)) return false;
|
|
2270
|
+
this.set.add(id);
|
|
2271
|
+
this.queue.push(id);
|
|
2272
|
+
while (this.queue.length > this.capacity) {
|
|
2273
|
+
const evicted = this.queue.shift();
|
|
2274
|
+
if (evicted !== void 0) this.set.delete(evicted);
|
|
2275
|
+
}
|
|
2276
|
+
return true;
|
|
2277
|
+
}
|
|
2278
|
+
has(id) {
|
|
2279
|
+
return this.set.has(id);
|
|
2280
|
+
}
|
|
2281
|
+
get size() {
|
|
2282
|
+
return this.set.size;
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
//#endregion
|
|
2286
|
+
//#region src/resources/streams.ts
|
|
2287
|
+
var StreamsResource = class {
|
|
2288
|
+
constructor(http) {
|
|
2289
|
+
this.http = http;
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Open an SSE stream yielding RineEvents.
|
|
2293
|
+
*
|
|
2294
|
+
* For agentic use cases, prefer `client.messages()` (SPEC §11.1) which
|
|
2295
|
+
* wraps this with local decryption and cleartext type filtering — this
|
|
2296
|
+
* method is the lower-level raw-event primitive.
|
|
2297
|
+
*
|
|
2298
|
+
* @example
|
|
2299
|
+
* ```ts
|
|
2300
|
+
* const ac = new AbortController();
|
|
2301
|
+
* setTimeout(() => ac.abort(), 60_000);
|
|
2302
|
+
*
|
|
2303
|
+
* for await (const event of client.streams.stream({ signal: ac.signal })) {
|
|
2304
|
+
* console.log(event.type, event.id);
|
|
2305
|
+
* }
|
|
2306
|
+
* ```
|
|
2307
|
+
*/
|
|
2308
|
+
stream(opts = {}) {
|
|
2309
|
+
const { signal, agent, timeout, lastEventId } = opts;
|
|
2310
|
+
const agentSegment = agent ? `/agents/${agent}` : "";
|
|
2311
|
+
let combinedSignal;
|
|
2312
|
+
if (signal && timeout !== void 0) combinedSignal = anySignal(signal, AbortSignal.timeout(timeout));
|
|
2313
|
+
else if (signal) combinedSignal = signal;
|
|
2314
|
+
else if (timeout !== void 0) combinedSignal = AbortSignal.timeout(timeout);
|
|
2315
|
+
else combinedSignal = neverAbortingSignal();
|
|
2316
|
+
return this.connect(agentSegment, combinedSignal, lastEventId);
|
|
2317
|
+
}
|
|
2318
|
+
async *connect(agentSegment, signal, initialLastEventId) {
|
|
2319
|
+
let lastEventId = initialLastEventId;
|
|
2320
|
+
let backoff = 1;
|
|
2321
|
+
const seen = new SeenIds(500);
|
|
2322
|
+
while (!signal.aborted) {
|
|
2323
|
+
const headers = {};
|
|
2324
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
2325
|
+
try {
|
|
2326
|
+
const response = await this.http.openStream(`${agentSegment}/stream`, {
|
|
2327
|
+
headers,
|
|
2328
|
+
signal
|
|
2329
|
+
});
|
|
2330
|
+
if (!response.ok) throw new Error(`SSE stream error: ${response.status} ${response.statusText}`);
|
|
2331
|
+
backoff = 1;
|
|
2332
|
+
for await (const event of toAsyncIter(response, signal)) {
|
|
2333
|
+
if (event.id) {
|
|
2334
|
+
if (!seen.record(event.id)) continue;
|
|
2335
|
+
lastEventId = event.id;
|
|
2336
|
+
}
|
|
2337
|
+
yield event;
|
|
2338
|
+
}
|
|
2339
|
+
yield {
|
|
2340
|
+
type: "disconnect",
|
|
2341
|
+
data: "stream ended"
|
|
2342
|
+
};
|
|
2343
|
+
break;
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
if (signal.aborted) break;
|
|
2346
|
+
backoff = Math.min(backoff * 2, 30);
|
|
2347
|
+
yield {
|
|
2348
|
+
type: "heartbeat",
|
|
2349
|
+
data: `reconnecting in ${backoff}s: ${err instanceof Error ? err.message : String(err)}`
|
|
2350
|
+
};
|
|
2351
|
+
await sleepWithSignal(backoff * 1e3, signal);
|
|
2352
|
+
if (signal.aborted) break;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
function neverAbortingSignal() {
|
|
2358
|
+
return new AbortController().signal;
|
|
2359
|
+
}
|
|
2360
|
+
//#endregion
|
|
2361
|
+
//#region src/resources/webhooks.ts
|
|
2362
|
+
/**
|
|
2363
|
+
* Webhooks resource — create, list, update, delete, deliveries.
|
|
2364
|
+
*
|
|
2365
|
+
* Step 10 — Track B: `create()` now takes the required `agentId` + optional
|
|
2366
|
+
* `events` list per server schema; `update()` accepts only the `active`
|
|
2367
|
+
* toggle (matches Python SDK). SPEC §10.7.
|
|
2368
|
+
*/
|
|
2369
|
+
var WebhooksResource = class {
|
|
2370
|
+
constructor(http) {
|
|
2371
|
+
this.http = http;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Register a webhook.
|
|
2375
|
+
*
|
|
2376
|
+
* Route: `POST /webhooks` with body `{agent_id, url, events?}`.
|
|
2377
|
+
* `agent_id` is required by the server schema — previous TS SDK
|
|
2378
|
+
* omitted it entirely, which 422'd every call.
|
|
2379
|
+
*/
|
|
2380
|
+
async create(agentId, url, opts = {}) {
|
|
2381
|
+
const body = {
|
|
2382
|
+
agent_id: agentId,
|
|
2383
|
+
url
|
|
2384
|
+
};
|
|
2385
|
+
if (opts.events !== void 0) body.events = opts.events;
|
|
2386
|
+
return parse(WebhookCreatedSchema, await this.http.post("/webhooks", body, {
|
|
2387
|
+
signal: opts.signal,
|
|
2388
|
+
operation: "webhooksCreate"
|
|
2389
|
+
}));
|
|
2390
|
+
}
|
|
2391
|
+
async list(opts = {}) {
|
|
2392
|
+
return parseCursorPage(WebhookReadSchema, await this.http.get("/webhooks", {
|
|
2393
|
+
params: {
|
|
2394
|
+
agent_id: opts.agentId,
|
|
2395
|
+
include_inactive: opts.includeInactive
|
|
2396
|
+
},
|
|
2397
|
+
signal: opts.signal,
|
|
2398
|
+
operation: "webhooksList"
|
|
2399
|
+
}));
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Toggle a webhook's active flag.
|
|
2403
|
+
*
|
|
2404
|
+
* Body restricted to `{active}` per server schema — the previous TS
|
|
2405
|
+
* SDK accepted `Partial<WebhookRead>` which did not match the server.
|
|
2406
|
+
*/
|
|
2407
|
+
async update(id, opts) {
|
|
2408
|
+
return parse(WebhookReadSchema, await this.http.patch(`/webhooks/${id}`, { active: opts.active }, {
|
|
2409
|
+
signal: opts.signal,
|
|
2410
|
+
operation: "webhooksUpdate"
|
|
2411
|
+
}));
|
|
2412
|
+
}
|
|
2413
|
+
async delete(id) {
|
|
2414
|
+
await this.http.delete(`/webhooks/${id}`, { operation: "webhooksDelete" });
|
|
2415
|
+
}
|
|
2416
|
+
async deliveries(id, opts = {}) {
|
|
2417
|
+
return parseCursorPage(WebhookDeliveryReadSchema, await this.http.get(`/webhooks/${id}/deliveries`, {
|
|
2418
|
+
params: {
|
|
2419
|
+
status: opts.status,
|
|
2420
|
+
limit: opts.limit,
|
|
2421
|
+
offset: opts.offset
|
|
2422
|
+
},
|
|
2423
|
+
signal: opts.signal,
|
|
2424
|
+
operation: "webhooksDeliveries"
|
|
2425
|
+
}));
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Aggregated delivery counts for a webhook.
|
|
2429
|
+
*
|
|
2430
|
+
* Route: `GET /webhooks/{id}/deliveries/summary`. Matches Python SDK
|
|
2431
|
+
* `webhooks.delivery_summary` (SPEC §10.7).
|
|
2432
|
+
*/
|
|
2433
|
+
async deliverySummary(id, opts = {}) {
|
|
2434
|
+
return parse(WebhookJobSummarySchema, await this.http.get(`/webhooks/${id}/deliveries/summary`, {
|
|
2435
|
+
signal: opts.signal,
|
|
2436
|
+
operation: "webhooksDeliverySummary"
|
|
2437
|
+
}));
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
//#endregion
|
|
2441
|
+
//#region src/client.ts
|
|
2442
|
+
let __includeReservedWarned = false;
|
|
2443
|
+
/**
|
|
2444
|
+
* Async client for the Rine E2E-encrypted messaging API.
|
|
2445
|
+
*
|
|
2446
|
+
* All messages are encrypted client-side (HPKE for 1:1, Sender Keys for
|
|
2447
|
+
* groups) before leaving the process. Construct via `new AsyncRineClient()`
|
|
2448
|
+
* or `defineAgent()` for the handler-based pattern.
|
|
2449
|
+
*/
|
|
2450
|
+
var AsyncRineClient = class AsyncRineClient {
|
|
2451
|
+
http;
|
|
2452
|
+
_signal;
|
|
2453
|
+
messagingResource;
|
|
2454
|
+
streams;
|
|
2455
|
+
polling;
|
|
2456
|
+
groups;
|
|
2457
|
+
webhooks;
|
|
2458
|
+
discovery;
|
|
2459
|
+
identity;
|
|
2460
|
+
constructor(opts = {}) {
|
|
2461
|
+
this.http = new SDKHttpClient({
|
|
2462
|
+
configDir: opts.configDir,
|
|
2463
|
+
apiUrl: opts.apiUrl,
|
|
2464
|
+
agent: opts.agent,
|
|
2465
|
+
timeout: opts.timeout,
|
|
2466
|
+
maxRetries: opts.maxRetries,
|
|
2467
|
+
middleware: opts.middleware
|
|
2468
|
+
});
|
|
2469
|
+
this._signal = opts.signal;
|
|
2470
|
+
this.messagingResource = new MessagesResource(this.http);
|
|
2471
|
+
this.streams = new StreamsResource(this.http);
|
|
2472
|
+
this.polling = new PollingResource(this.http);
|
|
2473
|
+
this.groups = new GroupsResource(this.http);
|
|
2474
|
+
this.webhooks = new WebhooksResource(this.http);
|
|
2475
|
+
this.discovery = new DiscoveryResource(this.http);
|
|
2476
|
+
this.identity = new IdentityResource(this.http);
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Send an E2E-encrypted message to an agent or group.
|
|
2480
|
+
*
|
|
2481
|
+
* @param to - Recipient handle (`name@org.rine.network`), group handle
|
|
2482
|
+
* (`#group@org.rine.network`), or UUID.
|
|
2483
|
+
* @param payload - Message payload (encrypted before sending).
|
|
2484
|
+
* @param opts - Optional type, metadata, schema validation, signal.
|
|
2485
|
+
* @returns The server-acknowledged message envelope.
|
|
2486
|
+
* @throws {SchemaValidationError} If `opts.schema` is set and payload fails validation.
|
|
2487
|
+
*
|
|
2488
|
+
* @example
|
|
2489
|
+
* ```ts
|
|
2490
|
+
* const msg = await client.send("bot@acme.rine.network", { task: "summarize" });
|
|
2491
|
+
* ```
|
|
2492
|
+
*/
|
|
2493
|
+
async send(to, payload, opts) {
|
|
2494
|
+
if (typeof to !== "string") throw new Error(`client.send() expects (to, payload, opts); first arg must be a recipient handle or UUID, got ${typeof to}. Did you pass the payload first?`);
|
|
2495
|
+
const validated = opts?.schema !== void 0 ? await validateOutbound(payload, opts.schema) : payload;
|
|
2496
|
+
const { schema: _schema, ...rest } = opts ?? {};
|
|
2497
|
+
return this.messagingResource.send(to, validated, {
|
|
2498
|
+
signal: this._signal,
|
|
2499
|
+
...rest
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
async sendAndWait(to, payload, opts) {
|
|
2503
|
+
if (typeof to !== "string") throw new Error(`client.sendAndWait() expects (to, payload, opts); first arg must be a recipient handle or UUID, got ${typeof to}. Did you pass the payload first?`);
|
|
2504
|
+
const validated = opts?.schema !== void 0 ? await validateOutbound(payload, opts.schema) : payload;
|
|
2505
|
+
const { schema: _schema, replySchema, ...rest } = opts ?? {};
|
|
2506
|
+
const result = await this.messagingResource.sendAndWait(to, validated, {
|
|
2507
|
+
signal: this._signal,
|
|
2508
|
+
...rest
|
|
2509
|
+
});
|
|
2510
|
+
if (replySchema !== void 0 && result.reply !== null && result.reply.decrypt_error == null) {
|
|
2511
|
+
const typedReply = await parseMessagePlaintext(result.reply, replySchema);
|
|
2512
|
+
return {
|
|
2513
|
+
sent: result.sent,
|
|
2514
|
+
reply: typedReply
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
return result;
|
|
2518
|
+
}
|
|
2519
|
+
/** Fetch the current agent's inbox with automatic decryption. */
|
|
2520
|
+
inbox(opts) {
|
|
2521
|
+
return this.messagingResource.inbox({
|
|
2522
|
+
signal: this._signal,
|
|
2523
|
+
...opts
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Auto-paginating async iterator over every message in the inbox.
|
|
2528
|
+
*
|
|
2529
|
+
* Walks `nextCursor` until exhausted and yields each `DecryptedMessage` as
|
|
2530
|
+
* it arrives. Lazy — holds at most one page in memory. Replaces the
|
|
2531
|
+
* deleted `CursorPage.autoPaginate(fetchPage)` helper per SPEC §13.2/§13.3.
|
|
2532
|
+
*
|
|
2533
|
+
* @example
|
|
2534
|
+
* ```ts
|
|
2535
|
+
* for await (const msg of client.inboxAll({ limit: 100 })) {
|
|
2536
|
+
* console.log(msg.plaintext);
|
|
2537
|
+
* }
|
|
2538
|
+
* ```
|
|
2539
|
+
*/
|
|
2540
|
+
async *inboxAll(opts) {
|
|
2541
|
+
let cursor = opts?.cursor;
|
|
2542
|
+
while (true) {
|
|
2543
|
+
const page = await this.inbox({
|
|
2544
|
+
...opts,
|
|
2545
|
+
cursor
|
|
2546
|
+
});
|
|
2547
|
+
yield* page.items;
|
|
2548
|
+
if (!page.hasNext || page.nextCursor == null) return;
|
|
2549
|
+
cursor = page.nextCursor;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
/** Fetch and decrypt a single message by ID. */
|
|
2553
|
+
async read(messageId, opts) {
|
|
2554
|
+
const { schema, ...rest } = opts ?? {};
|
|
2555
|
+
const msg = await this.messagingResource.read(messageId, {
|
|
2556
|
+
signal: this._signal,
|
|
2557
|
+
...rest
|
|
2558
|
+
});
|
|
2559
|
+
if (schema !== void 0 && msg.decrypt_error == null) return parseMessagePlaintext(msg, schema);
|
|
2560
|
+
return msg;
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Reply to a message, threading into the same conversation.
|
|
2564
|
+
*
|
|
2565
|
+
* @param messageId - UUID of the message to reply to.
|
|
2566
|
+
* @param payload - Reply payload (encrypted before sending).
|
|
2567
|
+
* @param opts - Optional type, metadata, schema validation, signal.
|
|
2568
|
+
* @returns The server-acknowledged reply envelope.
|
|
2569
|
+
*
|
|
2570
|
+
* @example
|
|
2571
|
+
* ```ts
|
|
2572
|
+
* await client.reply(msg.id, { status: "done", result: 42 });
|
|
2573
|
+
* ```
|
|
2574
|
+
*/
|
|
2575
|
+
async reply(messageId, payload, opts) {
|
|
2576
|
+
if (typeof messageId !== "string") throw new Error(`client.reply() expects (messageId, payload, opts); first arg must be a message UUID string, got ${typeof messageId}. Did you pass the payload first?`);
|
|
2577
|
+
const validated = opts?.schema !== void 0 ? await validateOutbound(payload, opts.schema) : payload;
|
|
2578
|
+
const { schema: _schema, ...rest } = opts ?? {};
|
|
2579
|
+
return this.messagingResource.reply(messageId, validated, {
|
|
2580
|
+
signal: this._signal,
|
|
2581
|
+
...rest
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Decrypted message iterator — the agentic headline surface (§11.1).
|
|
2586
|
+
*
|
|
2587
|
+
* Thin transform over `this.streams.stream()` that parses the inline
|
|
2588
|
+
* `MessageRead` envelope from each `event: message` SSE frame, runs the
|
|
2589
|
+
* pre-decrypt type filter (D6c) and reserved-type hiding (D6d), then
|
|
2590
|
+
* decrypts via the shared `decryptEnvelope()` helper.
|
|
2591
|
+
*
|
|
2592
|
+
* Key property per §11.1.1: decryption is local, not a round-trip via
|
|
2593
|
+
* `read()`. The server already JSON-encodes the full envelope into the
|
|
2594
|
+
* SSE `data:` field (`src/rine/api/agent_stream.py:27`), so the SDK
|
|
2595
|
+
* never needs a second `GET /messages/{id}` per message.
|
|
2596
|
+
*
|
|
2597
|
+
* Lifetime: bound to `opts.signal`. When the signal aborts, the underlying
|
|
2598
|
+
* SSE connection closes and the generator returns immediately without
|
|
2599
|
+
* yielding further messages. **No error is thrown on abort** — long-lived
|
|
2600
|
+
* stream iterators exit silently per SPEC_v2 §12.4, matching the standard
|
|
2601
|
+
* async-iterator cancellation contract. To distinguish "aborted" from
|
|
2602
|
+
* "ended naturally", check `opts.signal?.aborted` after the `for await`
|
|
2603
|
+
* loop exits. Without a signal, iteration continues until the caller
|
|
2604
|
+
* `break`s out of the `for await` loop (triggering the generator's
|
|
2605
|
+
* `return()`, which closes the underlying SSE connection).
|
|
2606
|
+
*
|
|
2607
|
+
* Non-message events (`heartbeat`, `status`, `disconnect`) and reserved
|
|
2608
|
+
* message types are silently dropped. Crypto failures populate
|
|
2609
|
+
* `decrypt_error` on the yielded message rather than throwing — same
|
|
2610
|
+
* discipline as `inbox()` / `read()`.
|
|
2611
|
+
*
|
|
2612
|
+
* Typed payload generics (`messages<T>({ schema })`) and the Standard
|
|
2613
|
+
* Schema v1 validation hook are wired end-to-end in Step 19: if `schema`
|
|
2614
|
+
* is supplied each decrypted message is JSON-parsed and validated before
|
|
2615
|
+
* being yielded, and `msg.plaintext` narrows to `T | null`. A schema
|
|
2616
|
+
* failure throws `ValidationError` out of the generator and iteration
|
|
2617
|
+
* ends — the same semantics as `read<T>()`. Inside `defineAgent<T>` the
|
|
2618
|
+
* failure is caught and routed to `onError({ stage: 'schema' })`.
|
|
2619
|
+
*
|
|
2620
|
+
* @example
|
|
2621
|
+
* ```ts
|
|
2622
|
+
* const ac = new AbortController();
|
|
2623
|
+
* for await (const msg of client.messages({ signal: ac.signal })) {
|
|
2624
|
+
* console.log(msg.type, msg.plaintext);
|
|
2625
|
+
* }
|
|
2626
|
+
* ```
|
|
2627
|
+
*/
|
|
2628
|
+
messages(opts = {}) {
|
|
2629
|
+
return this.messagesGenerator(opts);
|
|
2630
|
+
}
|
|
2631
|
+
async *messagesGenerator(opts) {
|
|
2632
|
+
const signal = opts.signal ?? this._signal;
|
|
2633
|
+
if (opts.includeReserved && !__includeReservedWarned) {
|
|
2634
|
+
__includeReservedWarned = true;
|
|
2635
|
+
console.warn("rine: client.messages({ includeReserved: true }) is a debug-only escape hatch; behavior may change between minor versions.");
|
|
2636
|
+
}
|
|
2637
|
+
const core = this.http.getCoreHttpClient({ signal });
|
|
2638
|
+
const agentId = await this.messagingResource.resolveSenderId(core, opts.agent);
|
|
2639
|
+
const filterTypes = normalizeTypeFilter(opts.type);
|
|
2640
|
+
const stream = this.streams.stream({
|
|
2641
|
+
signal,
|
|
2642
|
+
agent: agentId,
|
|
2643
|
+
lastEventId: opts.lastEventId
|
|
2644
|
+
});
|
|
2645
|
+
let warnedParseFailure = false;
|
|
2646
|
+
for await (const ev of stream) {
|
|
2647
|
+
if (signal?.aborted) return;
|
|
2648
|
+
if (ev.type !== "message") continue;
|
|
2649
|
+
let envelope;
|
|
2650
|
+
try {
|
|
2651
|
+
envelope = parse(DecryptedMessageSchema, JSON.parse(ev.data));
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
if (!warnedParseFailure) {
|
|
2654
|
+
warnedParseFailure = true;
|
|
2655
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2656
|
+
console.warn(`rine: client.messages() dropped malformed SSE envelope (${reason}); further drops on this stream are silent`);
|
|
2657
|
+
}
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
if (!opts.includeReserved && RESERVED_MESSAGE_TYPES.has(envelope.type)) continue;
|
|
2661
|
+
if (filterTypes && !filterTypes.has(envelope.type)) continue;
|
|
2662
|
+
const decrypted = await decryptEnvelope({
|
|
2663
|
+
core,
|
|
2664
|
+
configDir: this.http.configDir,
|
|
2665
|
+
agentId,
|
|
2666
|
+
msg: envelope
|
|
2667
|
+
});
|
|
2668
|
+
yield opts.schema !== void 0 && decrypted.decrypt_error == null ? await parseMessagePlaintext(decrypted, opts.schema) : decrypted;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
conversation(convIdOrMsg) {
|
|
2672
|
+
if (typeof convIdOrMsg === "string") return new ConversationScope(this, convIdOrMsg);
|
|
2673
|
+
const msg = convIdOrMsg;
|
|
2674
|
+
const selfHandle = this.http.agentHeader?.["X-Rine-Agent"];
|
|
2675
|
+
const peer = derivePeerFromMessage(msg, selfHandle);
|
|
2676
|
+
return new ConversationScope(this, msg.conversation_id, peer, msg.id);
|
|
2677
|
+
}
|
|
2678
|
+
stream(opts) {
|
|
2679
|
+
return this.streams.stream({
|
|
2680
|
+
signal: this._signal,
|
|
2681
|
+
...opts
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
/** Check how many unread messages are waiting (lightweight poll). */
|
|
2685
|
+
poll() {
|
|
2686
|
+
return this.polling.poll();
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Watch for incoming messages via polling — calls `handler` on each new message.
|
|
2690
|
+
*
|
|
2691
|
+
* Note: `watch()` is NOT in the Python SDK — it is a TypeScript-only ergonomic
|
|
2692
|
+
* surface added for polling agents behind firewalls.
|
|
2693
|
+
*
|
|
2694
|
+
* Returns an unsubscribe function.
|
|
2695
|
+
*/
|
|
2696
|
+
async watch(handler, opts) {
|
|
2697
|
+
const { signal, pollInterval = 5e3, onError } = {
|
|
2698
|
+
signal: this._signal,
|
|
2699
|
+
...opts
|
|
2700
|
+
};
|
|
2701
|
+
let stopped = false;
|
|
2702
|
+
const seen = new SeenIds(500);
|
|
2703
|
+
const tick = async () => {
|
|
2704
|
+
while (!stopped) {
|
|
2705
|
+
if (signal?.aborted) break;
|
|
2706
|
+
try {
|
|
2707
|
+
const sorted = [...(await this.inbox({
|
|
2708
|
+
limit: 10,
|
|
2709
|
+
signal
|
|
2710
|
+
})).items].sort((a, b) => {
|
|
2711
|
+
const ta = a.created_at ?? "";
|
|
2712
|
+
const tb = b.created_at ?? "";
|
|
2713
|
+
return ta.localeCompare(tb);
|
|
2714
|
+
});
|
|
2715
|
+
for (const msg of sorted) {
|
|
2716
|
+
const id = msg.id;
|
|
2717
|
+
if (!seen.record(id)) continue;
|
|
2718
|
+
await handler(msg);
|
|
2719
|
+
}
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
if (onError) onError(err);
|
|
2722
|
+
}
|
|
2723
|
+
await sleepWithSignal(pollInterval, signal);
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
tick();
|
|
2727
|
+
return () => {
|
|
2728
|
+
stopped = true;
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
/** Fetch the current org identity, agent list, and trust tier. */
|
|
2732
|
+
whoami() {
|
|
2733
|
+
return this.identity.whoami();
|
|
2734
|
+
}
|
|
2735
|
+
createAgent(name, opts) {
|
|
2736
|
+
return this.identity.createAgent(name, opts);
|
|
2737
|
+
}
|
|
2738
|
+
listAgents(opts) {
|
|
2739
|
+
return this.identity.listAgents(opts);
|
|
2740
|
+
}
|
|
2741
|
+
getAgent(agentId) {
|
|
2742
|
+
return this.identity.getAgent(agentId);
|
|
2743
|
+
}
|
|
2744
|
+
updateAgent(agentId, patch) {
|
|
2745
|
+
return this.identity.updateAgent(agentId, patch);
|
|
2746
|
+
}
|
|
2747
|
+
revokeAgent(agentId) {
|
|
2748
|
+
return this.identity.revokeAgent(agentId);
|
|
2749
|
+
}
|
|
2750
|
+
rotateKeys(agentId) {
|
|
2751
|
+
return this.identity.rotateKeys(agentId);
|
|
2752
|
+
}
|
|
2753
|
+
getAgentCard(agentId) {
|
|
2754
|
+
return this.identity.getAgentCard(agentId);
|
|
2755
|
+
}
|
|
2756
|
+
setAgentCard(agentId, card) {
|
|
2757
|
+
return this.identity.setAgentCard(agentId, card);
|
|
2758
|
+
}
|
|
2759
|
+
deleteAgentCard(agentId) {
|
|
2760
|
+
return this.identity.deleteAgentCard(agentId);
|
|
2761
|
+
}
|
|
2762
|
+
regeneratePollToken(agentId) {
|
|
2763
|
+
return this.identity.regeneratePollToken(agentId);
|
|
2764
|
+
}
|
|
2765
|
+
revokePollToken(agentId) {
|
|
2766
|
+
return this.identity.revokePollToken(agentId);
|
|
2767
|
+
}
|
|
2768
|
+
getQuotas() {
|
|
2769
|
+
return this.identity.getQuotas();
|
|
2770
|
+
}
|
|
2771
|
+
updateOrg(patch, opts) {
|
|
2772
|
+
return this.identity.updateOrg(patch, opts);
|
|
2773
|
+
}
|
|
2774
|
+
eraseOrg(opts) {
|
|
2775
|
+
return this.identity.eraseOrg(opts);
|
|
2776
|
+
}
|
|
2777
|
+
exportOrg(opts) {
|
|
2778
|
+
return this.identity.exportOrg(opts);
|
|
2779
|
+
}
|
|
2780
|
+
discover(filters) {
|
|
2781
|
+
return this.discovery.discover(filters);
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Auto-paginating async iterator over every agent that matches the
|
|
2785
|
+
* supplied discovery filters. Walks `nextCursor` until exhausted.
|
|
2786
|
+
* Replaces `CursorPage.autoPaginate()` per SPEC §13.2/§13.3.
|
|
2787
|
+
*/
|
|
2788
|
+
async *discoverAll(filters) {
|
|
2789
|
+
let cursor = filters?.cursor;
|
|
2790
|
+
while (true) {
|
|
2791
|
+
const page = await this.discovery.discover({
|
|
2792
|
+
...filters,
|
|
2793
|
+
cursor
|
|
2794
|
+
});
|
|
2795
|
+
yield* page.items;
|
|
2796
|
+
if (!page.hasNext || page.nextCursor == null) return;
|
|
2797
|
+
cursor = page.nextCursor;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
inspect(handleOrId) {
|
|
2801
|
+
return this.discovery.inspect(handleOrId);
|
|
2802
|
+
}
|
|
2803
|
+
discoverGroups(opts) {
|
|
2804
|
+
return this.discovery.discoverGroups(opts);
|
|
2805
|
+
}
|
|
2806
|
+
async *discoverGroupsAll(opts) {
|
|
2807
|
+
let cursor;
|
|
2808
|
+
while (true) {
|
|
2809
|
+
const page = await this.discovery.discoverGroups({
|
|
2810
|
+
...opts,
|
|
2811
|
+
cursor
|
|
2812
|
+
});
|
|
2813
|
+
yield* page.items;
|
|
2814
|
+
if (!page.hasNext || page.nextCursor == null) return;
|
|
2815
|
+
cursor = page.nextCursor;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Python-parity `with_options` — returns a new `AsyncRineClient` that
|
|
2820
|
+
* shares credentials and middleware but applies the supplied overrides.
|
|
2821
|
+
* Any option not provided inherits from this client.
|
|
2822
|
+
*
|
|
2823
|
+
* See SPEC §10.8 — this is the idiomatic single-method replacement for
|
|
2824
|
+
* the narrower `withSignal` / `withTimeout` / `withAgent` helpers, which
|
|
2825
|
+
* remain available for callers that prefer the specialised APIs.
|
|
2826
|
+
*/
|
|
2827
|
+
withOptions(opts) {
|
|
2828
|
+
return new AsyncRineClient({
|
|
2829
|
+
configDir: this.http.configDir,
|
|
2830
|
+
apiUrl: this.http.apiUrl,
|
|
2831
|
+
agent: opts.agent ?? this.http.agentHeader?.["X-Rine-Agent"],
|
|
2832
|
+
timeout: opts.timeout ?? this.http.timeout,
|
|
2833
|
+
maxRetries: opts.maxRetries ?? this.http.maxRetries,
|
|
2834
|
+
middleware: this.http.middleware,
|
|
2835
|
+
signal: opts.signal ?? this._signal
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
/** Return a derived client bound to the given AbortSignal. */
|
|
2839
|
+
withSignal(signal) {
|
|
2840
|
+
return this.withOptions({ signal });
|
|
2841
|
+
}
|
|
2842
|
+
/** Return a derived client with a different request timeout. */
|
|
2843
|
+
withTimeout(ms) {
|
|
2844
|
+
return this.withOptions({ timeout: ms });
|
|
2845
|
+
}
|
|
2846
|
+
/** Return a derived client acting as a different agent. */
|
|
2847
|
+
withAgent(agent) {
|
|
2848
|
+
return this.withOptions({ agent });
|
|
2849
|
+
}
|
|
2850
|
+
/** Release any resources held by this client. */
|
|
2851
|
+
async close() {}
|
|
2852
|
+
/** Implements `AsyncDisposable` — delegates to `close()`. */
|
|
2853
|
+
[Symbol.asyncDispose]() {
|
|
2854
|
+
return this.close();
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
/**
|
|
2858
|
+
* Normalize the `type` filter from `client.messages()` into a Set for O(1)
|
|
2859
|
+
* membership tests. Returns `undefined` when no filter was passed (caller
|
|
2860
|
+
* branches on `filterTypes !== undefined`).
|
|
2861
|
+
*/
|
|
2862
|
+
function normalizeTypeFilter(filter) {
|
|
2863
|
+
if (filter === void 0) return void 0;
|
|
2864
|
+
if (typeof filter === "string") return new Set([filter]);
|
|
2865
|
+
if (filter.length === 0) return void 0;
|
|
2866
|
+
return new Set(filter);
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Derive the "other party" for a conversation scope from a decrypted
|
|
2870
|
+
* message. Supports group messages (peer = group handle) and 1:1 DMs
|
|
2871
|
+
* (peer = original sender's handle or UUID). Returns `null` when the
|
|
2872
|
+
* derivation is ambiguous (system message, missing handle on a group
|
|
2873
|
+
* frame, etc.) so that `scope.send(payload)` surfaces the clear
|
|
2874
|
+
* "no peer" error instead of misrouting to a stale address.
|
|
2875
|
+
*/
|
|
2876
|
+
function derivePeerFromMessage(msg, selfHandle) {
|
|
2877
|
+
if (msg.group_id) return msg.group_handle ?? null;
|
|
2878
|
+
if (selfHandle && msg.sender_handle === selfHandle) {
|
|
2879
|
+
if (msg.recipient_handle) return msg.recipient_handle;
|
|
2880
|
+
if (msg.to_agent_id) return msg.to_agent_id;
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
if (msg.sender_handle) return msg.sender_handle;
|
|
2884
|
+
if (msg.from_agent_id) return msg.from_agent_id;
|
|
2885
|
+
return null;
|
|
2886
|
+
}
|
|
2887
|
+
//#endregion
|
|
2888
|
+
//#region src/agent.ts
|
|
2889
|
+
/**
|
|
2890
|
+
* Grace period after `stop()` before in-flight handlers are logged as
|
|
2891
|
+
* still-running. SPEC §11.3.3 — we don't forcibly terminate user promises
|
|
2892
|
+
* (Node has no mechanism), we just wait and then log.
|
|
2893
|
+
*/
|
|
2894
|
+
const STOP_GRACE_MS = 3e4;
|
|
2895
|
+
function defineAgent(opts) {
|
|
2896
|
+
if (opts.pollInterval !== void 0) throw new Error("defineAgent: pollInterval is not yet supported. Use SSE (the default) for message delivery.");
|
|
2897
|
+
const { client, signal: externalSignal, includeReserved, onError, schema } = opts;
|
|
2898
|
+
const internalStop = new AbortController();
|
|
2899
|
+
const composedSignal = externalSignal ? anySignal(externalSignal, internalStop.signal) : internalStop.signal;
|
|
2900
|
+
const handlerMode = "handlers" in opts && opts.handlers !== void 0;
|
|
2901
|
+
const handlers = handlerMode ? opts.handlers : void 0;
|
|
2902
|
+
const onMessage = !handlerMode ? opts.onMessage : void 0;
|
|
2903
|
+
let typeFilter;
|
|
2904
|
+
if (handlers) {
|
|
2905
|
+
const keys = Object.keys(handlers).filter((k) => k !== "*");
|
|
2906
|
+
if (!(handlers["*"] !== void 0) && keys.length > 0) typeFilter = keys;
|
|
2907
|
+
}
|
|
2908
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
2909
|
+
let started = false;
|
|
2910
|
+
let stopPromise = null;
|
|
2911
|
+
let loopPromise = null;
|
|
2912
|
+
const reportError = async (err, ctx) => {
|
|
2913
|
+
if (onError === void 0) {
|
|
2914
|
+
const msgId = ctx.message?.id ?? "<no-msg>";
|
|
2915
|
+
const msgType = ctx.message?.type ?? "<no-type>";
|
|
2916
|
+
const name = err instanceof Error ? err.name : "Error";
|
|
2917
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2918
|
+
console.error(`rine: defineAgent ${ctx.stage} error on ${msgId} (${msgType}): ${name}: ${reason}`);
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
try {
|
|
2922
|
+
await onError(err, ctx);
|
|
2923
|
+
} catch (innerErr) {
|
|
2924
|
+
const reason = innerErr instanceof Error ? innerErr.message : String(innerErr);
|
|
2925
|
+
console.error(`rine: defineAgent onError itself threw: ${reason}`);
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
const dispatch = async (msg) => {
|
|
2929
|
+
let typed;
|
|
2930
|
+
if (schema !== void 0 && msg.decrypt_error == null) try {
|
|
2931
|
+
typed = await parseMessagePlaintext(msg, schema);
|
|
2932
|
+
} catch (err) {
|
|
2933
|
+
await reportError(err, {
|
|
2934
|
+
message: msg,
|
|
2935
|
+
stage: "schema"
|
|
2936
|
+
});
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
else typed = msg;
|
|
2940
|
+
const ctx = {
|
|
2941
|
+
client,
|
|
2942
|
+
conversation: client.conversation(msg),
|
|
2943
|
+
reply: (payload, replyOpts) => client.reply(asMessageUuid(msg.id), payload, replyOpts),
|
|
2944
|
+
signal: composedSignal
|
|
2945
|
+
};
|
|
2946
|
+
let handler;
|
|
2947
|
+
if (handlers) handler = handlers[msg.type] ?? handlers["*"];
|
|
2948
|
+
else if (onMessage) handler = onMessage;
|
|
2949
|
+
if (!handler) return;
|
|
2950
|
+
try {
|
|
2951
|
+
await handler(typed, ctx);
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
await reportError(err, {
|
|
2954
|
+
message: msg,
|
|
2955
|
+
stage: "handler"
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
const runLoop = async () => {
|
|
2960
|
+
try {
|
|
2961
|
+
const messageOpts = {
|
|
2962
|
+
signal: composedSignal,
|
|
2963
|
+
includeReserved
|
|
2964
|
+
};
|
|
2965
|
+
if (typeFilter) messageOpts.type = typeFilter;
|
|
2966
|
+
for await (const msg of client.messages(messageOpts)) {
|
|
2967
|
+
if (composedSignal.aborted) break;
|
|
2968
|
+
const task = dispatch(msg);
|
|
2969
|
+
inflight.add(task);
|
|
2970
|
+
task.finally(() => inflight.delete(task));
|
|
2971
|
+
}
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
if (composedSignal.aborted) return;
|
|
2974
|
+
await reportError(err, {
|
|
2975
|
+
message: null,
|
|
2976
|
+
stage: "lifecycle"
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
const agent = {
|
|
2981
|
+
get started() {
|
|
2982
|
+
return started;
|
|
2983
|
+
},
|
|
2984
|
+
async start() {
|
|
2985
|
+
if (started) return;
|
|
2986
|
+
started = true;
|
|
2987
|
+
if (composedSignal.aborted) {
|
|
2988
|
+
loopPromise = Promise.resolve();
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
loopPromise = runLoop();
|
|
2992
|
+
},
|
|
2993
|
+
stop() {
|
|
2994
|
+
if (stopPromise) return stopPromise;
|
|
2995
|
+
started = true;
|
|
2996
|
+
stopPromise = (async () => {
|
|
2997
|
+
if (!internalStop.signal.aborted) internalStop.abort();
|
|
2998
|
+
if (loopPromise) try {
|
|
2999
|
+
await loopPromise;
|
|
3000
|
+
} catch {}
|
|
3001
|
+
if (inflight.size > 0) {
|
|
3002
|
+
let timedOut = false;
|
|
3003
|
+
let timerHandle;
|
|
3004
|
+
const timeout = new Promise((resolve) => {
|
|
3005
|
+
timerHandle = setTimeout(() => {
|
|
3006
|
+
timedOut = true;
|
|
3007
|
+
resolve();
|
|
3008
|
+
}, STOP_GRACE_MS);
|
|
3009
|
+
});
|
|
3010
|
+
try {
|
|
3011
|
+
await Promise.race([Promise.allSettled([...inflight]), timeout]);
|
|
3012
|
+
} finally {
|
|
3013
|
+
if (timerHandle !== void 0) clearTimeout(timerHandle);
|
|
3014
|
+
}
|
|
3015
|
+
if (timedOut && inflight.size > 0) console.warn(`rine: defineAgent.stop() grace period (${STOP_GRACE_MS}ms) elapsed with ${inflight.size} handler(s) still running`);
|
|
3016
|
+
}
|
|
3017
|
+
})();
|
|
3018
|
+
return stopPromise;
|
|
3019
|
+
},
|
|
3020
|
+
async [Symbol.asyncDispose]() {
|
|
3021
|
+
await agent.stop();
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
if (opts.autoStart !== false) agent.start();
|
|
3025
|
+
return agent;
|
|
3026
|
+
}
|
|
3027
|
+
//#endregion
|
|
3028
|
+
export { APIConnectionError, AgentCardSchema, AgentProfileSchema, AgentReadSchema, AgentSummarySchema, AsyncRineClient, AuthenticationError, AuthorizationError, ConfigError, ConflictError, ConversationScope, ConversationStatus, CryptoError, CursorPage, DecryptedMessageSchema, EncryptionVersion, ErasureResultSchema, GroupMemberSchema, GroupReadSchema, GroupSummarySchema, InternalServerError, InviteResultSchema, JoinRequestReadSchema, JoinRequestStatus, JoinResultSchema, MessageReadSchema, MessageType, NotFoundError, OrgQuotasSchema, OrgReadSchema, PollTokenResponseSchema, QuotaEntrySchema, RESERVED_MESSAGE_TYPES, RateLimitError, RineApiError, RineError, RineEventSchema, RineTimeoutError, SchemaValidationError, SendAndWaitResultSchema, ServiceUnavailableError, ValidationError, VoteChoice, VoteResponseSchema, WebhookCreatedSchema, WebhookDeliveryReadSchema, WebhookJobStatus, WebhookReadSchema, WhoAmISchema, anySignal, asAgentUuid, asGroupUuid, asMessageUuid, asOrgUuid, asWebhookUuid, composeMiddleware, cursorPageSchema, defineAgent, isAgentHandle, isGroupHandle, isValidMessageType, loggingMiddleware, parse, parseCursorPage, parseMessagePlaintext, parsePlaintext, register, timeoutSignal, toAsyncIter, validateSlug, z };
|