@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/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 };