@casimesh/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/index.js ADDED
@@ -0,0 +1,884 @@
1
+ import { z } from 'zod';
2
+
3
+ // ../contracts/src/agent.ts
4
+ var CapabilityName = z.string().regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/, "Dotted lowercase name, e.g. terrain.analysis").max(100);
5
+ var Capability = z.object({
6
+ name: CapabilityName,
7
+ version: z.string().max(32).default("1.0.0"),
8
+ description: z.string().max(500).optional()
9
+ }).meta({ id: "Capability", description: "A machine-discoverable description of what an agent can perform." });
10
+ var Timestamp = z.iso.datetime().meta({ example: "2026-09-28T09:28:12.123Z" });
11
+ var Version = z.int().min(1).meta({ description: "Monotonic per-entity version" });
12
+ var DisplayName = z.string().trim().min(1).max(120);
13
+ var Slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Lowercase letters, digits and single hyphens").max(63);
14
+ var Metadata = z.record(z.string(), z.unknown());
15
+ var EntityAudit = {
16
+ version: Version,
17
+ createdAt: Timestamp,
18
+ updatedAt: Timestamp
19
+ };
20
+ var ID_PREFIX = {
21
+ node: "node",
22
+ organization: "org",
23
+ agent: "agt",
24
+ link: "lnk",
25
+ event: "evt",
26
+ command: "cmd",
27
+ principal: "prn",
28
+ roleBinding: "rol",
29
+ apiKey: "key",
30
+ task: "tsk"
31
+ };
32
+ var ULID_PATTERN = "[0-9A-HJKMNP-TV-Z]{26}";
33
+ var prefixedId = (kind) => z.string().regex(new RegExp(`^${ID_PREFIX[kind]}_${ULID_PATTERN}$`), `Expected a ${kind} id (${ID_PREFIX[kind]}_<ULID>)`).meta({ description: `${kind} identifier`, example: `${ID_PREFIX[kind]}_01J9Z3K7Q8R5T2V4W6X8Y0A1B2` });
34
+ var NodeId = prefixedId("node");
35
+ var OrganizationId = prefixedId("organization");
36
+ var AgentId = prefixedId("agent");
37
+ var LinkId = prefixedId("link");
38
+ var EventId = prefixedId("event");
39
+ var CommandId = prefixedId("command");
40
+ var PrincipalId = prefixedId("principal");
41
+ var RoleBindingId = prefixedId("roleBinding");
42
+ var TaskId = prefixedId("task");
43
+
44
+ // ../contracts/src/agent.ts
45
+ var AgentKind = z.enum(["standard", "mesh"]);
46
+ var AgentStatus = z.enum(["created", "starting", "running", "stopped", "error"]);
47
+ var capabilityList = z.array(Capability).max(64).refine((caps) => new Set(caps.map((c) => c.name)).size === caps.length, "Capability names must be unique");
48
+ var Agent = z.object({
49
+ id: AgentId,
50
+ organizationId: OrganizationId,
51
+ nodeId: NodeId,
52
+ name: DisplayName,
53
+ kind: AgentKind,
54
+ status: AgentStatus,
55
+ capabilities: capabilityList,
56
+ /** Model backing the agent, e.g. `claude-sonnet-5`; null for rule-based agents. */
57
+ model: z.string().max(100).nullable(),
58
+ metadata: Metadata,
59
+ ...EntityAudit
60
+ }).meta({ id: "Agent", description: "An autonomous software entity with identity, capabilities and policies." });
61
+ z.object({
62
+ name: DisplayName,
63
+ capabilities: capabilityList.default([]),
64
+ model: z.string().max(100).nullable().default(null),
65
+ metadata: Metadata.optional()
66
+ }).meta({ id: "CreateAgentRequest" });
67
+ z.object({
68
+ name: DisplayName.optional(),
69
+ capabilities: capabilityList.optional(),
70
+ model: z.string().max(100).nullable().optional(),
71
+ metadata: Metadata.optional()
72
+ }).refine((body) => Object.keys(body).length > 0, "At least one field must be provided").meta({ id: "UpdateAgentRequest" });
73
+ var PrincipalKind = z.enum(["user", "service"]);
74
+ var Principal = z.object({
75
+ id: PrincipalId,
76
+ kind: PrincipalKind,
77
+ name: DisplayName,
78
+ publicKey: z.string().nullable(),
79
+ createdAt: Timestamp
80
+ }).meta({ id: "Principal", description: "A user or service that can be authenticated and granted roles." });
81
+ var Role = z.enum(["node_owner", "org_owner"]);
82
+ var RoleScope = z.discriminatedUnion("type", [
83
+ z.object({ type: z.literal("node"), nodeId: NodeId }),
84
+ z.object({ type: z.literal("organization"), organizationId: OrganizationId, nodeId: NodeId })
85
+ ]);
86
+ var RoleBinding = z.object({
87
+ id: RoleBindingId,
88
+ principalId: PrincipalId,
89
+ role: Role,
90
+ scope: RoleScope,
91
+ createdAt: Timestamp
92
+ }).refine(
93
+ (b) => b.role === "node_owner" ? b.scope.type === "node" : b.scope.type === "organization",
94
+ "node_owner must be node-scoped and org_owner organization-scoped"
95
+ ).meta({ id: "RoleBinding" });
96
+ z.object({ principalId: PrincipalId }).meta({ id: "GrantOwnerRequest" });
97
+ var TransportKind = z.enum(["memory", "ws-peer", "tcp", "mqtt", "meshtastic", "meshcore"]);
98
+ var LinkState = z.enum(["requested", "up", "down"]);
99
+ var LinkQuality = z.object({
100
+ latencyMs: z.number().nonnegative().optional(),
101
+ rssi: z.number().optional()
102
+ });
103
+ var Link = z.object({
104
+ id: LinkId,
105
+ aNodeId: NodeId,
106
+ bNodeId: NodeId,
107
+ transport: TransportKind,
108
+ state: LinkState,
109
+ quality: LinkQuality,
110
+ lastSeenAt: Timestamp.nullable(),
111
+ ...EntityAudit
112
+ }).refine((link) => link.aNodeId !== link.bNodeId, "A link must connect two different nodes").meta({ id: "Link", description: "Transport-level adjacency between two nodes \u2014 the edges of the topology." });
113
+ z.object({ aNodeId: NodeId, bNodeId: NodeId, transport: TransportKind }).refine((body) => body.aNodeId !== body.bNodeId, "A link must connect two different nodes").meta({ id: "CreateLinkRequest" });
114
+ var NodeStatus = z.enum(["online", "degraded", "offline"]);
115
+ var Node = z.object({
116
+ id: NodeId,
117
+ name: DisplayName,
118
+ publicKey: z.string().min(1),
119
+ /** Base URL where other nodes and clients reach this node, e.g. `https://node-a.example:7700` (ADR-0017). */
120
+ endpoint: z.url().nullable(),
121
+ status: NodeStatus,
122
+ metadata: Metadata,
123
+ lastSeenAt: Timestamp.nullable(),
124
+ ...EntityAudit
125
+ }).meta({ id: "Node", description: "A physical or virtual endpoint connected to the mesh." });
126
+ z.object({
127
+ name: DisplayName.optional(),
128
+ endpoint: z.url().nullable().optional(),
129
+ metadata: Metadata.optional()
130
+ }).refine((body) => Object.keys(body).length > 0, "At least one field must be provided").meta({ id: "UpdateNodeRequest" });
131
+ var Organization = z.object({
132
+ id: OrganizationId,
133
+ nodeId: NodeId,
134
+ name: DisplayName,
135
+ slug: Slug,
136
+ metadata: Metadata,
137
+ ...EntityAudit
138
+ }).meta({ id: "Organization", description: "A logical boundary on a node that owns policies, identities and agents." });
139
+ z.object({
140
+ name: DisplayName,
141
+ /** Derived from `name` when omitted. */
142
+ slug: Slug.optional(),
143
+ metadata: Metadata.optional(),
144
+ /** Principal to make the first org owner; defaults to the caller. */
145
+ ownerPrincipalId: PrincipalId.optional()
146
+ }).meta({ id: "CreateOrganizationRequest" });
147
+ z.object({
148
+ name: DisplayName.optional(),
149
+ metadata: Metadata.optional()
150
+ }).refine((body) => Object.keys(body).length > 0, "At least one field must be provided").meta({ id: "UpdateOrganizationRequest" });
151
+ var TaskStatus = z.enum(["requested", "accepted", "completed", "failed"]);
152
+ var TaskParty = z.object({ agentId: AgentId, organizationId: OrganizationId, nodeId: NodeId });
153
+ var TaskAssignee = z.object({ organizationId: OrganizationId, nodeId: NodeId, meshAgentId: AgentId });
154
+ var TaskStep = z.object({
155
+ name: z.enum(["observe", "reason", "act", "verify"]),
156
+ at: Timestamp,
157
+ summary: z.string().max(1e3)
158
+ });
159
+ var TaskError = z.object({ code: z.string().max(64), message: z.string().max(1e3) });
160
+ var Task = z.object({
161
+ id: TaskId,
162
+ capability: CapabilityName,
163
+ input: Metadata,
164
+ status: TaskStatus,
165
+ requester: TaskParty,
166
+ /** The requester organization's Mesh Agent, the first hop of the route. */
167
+ requesterMeshAgentId: AgentId,
168
+ assignee: TaskAssignee,
169
+ /** The agent that does the work, chosen by the assignee's Mesh Agent on acceptance. */
170
+ provider: TaskParty.nullable(),
171
+ output: Metadata.nullable(),
172
+ error: TaskError.nullable(),
173
+ steps: z.array(TaskStep).max(32),
174
+ /** Model that produced the result; null when a rule-based model did. */
175
+ model: z.string().max(100).nullable(),
176
+ version: Version,
177
+ createdAt: Timestamp,
178
+ updatedAt: Timestamp
179
+ }).meta({
180
+ id: "Task",
181
+ description: "A capability request routed through Mesh Agents, with its result and provenance (ADR-0020)."
182
+ });
183
+ z.object({
184
+ capability: CapabilityName,
185
+ input: Metadata.default({})
186
+ }).meta({ id: "RequestTaskRequest" });
187
+ z.object({
188
+ agentId: AgentId,
189
+ agentName: z.string(),
190
+ organizationId: OrganizationId,
191
+ organizationName: z.string(),
192
+ nodeId: NodeId,
193
+ /** Running, on a node that is not offline — i.e. a task could be routed to it now. */
194
+ available: z.boolean()
195
+ }).meta({ id: "CapabilityProvider" });
196
+
197
+ // ../contracts/src/events.ts
198
+ var subject = (kind, id2) => z.object({ kind: z.literal(kind), id: id2, version: Version });
199
+ var NodeSubject = subject("node", NodeId);
200
+ var OrganizationSubject = subject("organization", OrganizationId);
201
+ var AgentSubject = subject("agent", AgentId);
202
+ var LinkSubject = subject("link", LinkId);
203
+ var RoleBindingSubject = subject("roleBinding", RoleBindingId);
204
+ var TaskSubject = subject("task", TaskId);
205
+ var event = (type, subjectSchema, payload) => z.object({ type: z.literal(type), subject: subjectSchema, payload });
206
+ var AgentRegistration = z.object({
207
+ meshAgentId: AgentId,
208
+ agentId: AgentId,
209
+ organizationId: OrganizationId,
210
+ nodeId: NodeId
211
+ });
212
+ var NodeCreated = event("node.created", NodeSubject, z.object({ node: Node }));
213
+ var NodeUpdated = event("node.updated", NodeSubject, z.object({ node: Node }));
214
+ var NodeRemoved = event("node.removed", NodeSubject, z.object({ nodeId: NodeId }));
215
+ var NodeStatusChanged = event(
216
+ "node.status_changed",
217
+ NodeSubject,
218
+ z.object({ nodeId: NodeId, status: NodeStatus, previousStatus: NodeStatus, lastSeenAt: Timestamp.nullable() })
219
+ );
220
+ var NodeHeartbeat = event("node.heartbeat", NodeSubject, z.object({ nodeId: NodeId, at: Timestamp }));
221
+ var OrganizationCreated = event(
222
+ "organization.created",
223
+ OrganizationSubject,
224
+ z.object({ organization: Organization })
225
+ );
226
+ var OrganizationUpdated = event(
227
+ "organization.updated",
228
+ OrganizationSubject,
229
+ z.object({ organization: Organization })
230
+ );
231
+ var OrganizationRemoved = event(
232
+ "organization.removed",
233
+ OrganizationSubject,
234
+ z.object({ organizationId: OrganizationId, nodeId: NodeId })
235
+ );
236
+ var AgentCreated = event("agent.created", AgentSubject, z.object({ agent: Agent }));
237
+ var AgentUpdated = event("agent.updated", AgentSubject, z.object({ agent: Agent }));
238
+ var AgentStatusChanged = event(
239
+ "agent.status_changed",
240
+ AgentSubject,
241
+ z.object({ agent: Agent, previousStatus: AgentStatus })
242
+ );
243
+ var AgentRemoved = event(
244
+ "agent.removed",
245
+ AgentSubject,
246
+ z.object({ agentId: AgentId, organizationId: OrganizationId, nodeId: NodeId })
247
+ );
248
+ var MeshAgentAgentRegistered = event("mesh_agent.agent_registered", AgentSubject, AgentRegistration);
249
+ var MeshAgentAgentDeregistered = event("mesh_agent.agent_deregistered", AgentSubject, AgentRegistration);
250
+ var LinkRequested = event("link.requested", LinkSubject, z.object({ link: Link }));
251
+ var LinkUp = event("link.up", LinkSubject, z.object({ link: Link }));
252
+ var LinkUpdated = event("link.updated", LinkSubject, z.object({ link: Link }));
253
+ var LinkDown = event("link.down", LinkSubject, z.object({ link: Link }));
254
+ var LinkRemoved = event("link.removed", LinkSubject, z.object({ linkId: LinkId }));
255
+ var RoleGranted = event("role.granted", RoleBindingSubject, z.object({ binding: RoleBinding }));
256
+ var RoleRevoked = event("role.revoked", RoleBindingSubject, z.object({ binding: RoleBinding }));
257
+ var TaskRequested = event("task.requested", TaskSubject, z.object({ task: Task }));
258
+ var TaskAccepted = event("task.accepted", TaskSubject, z.object({ task: Task }));
259
+ var TaskCompleted = event("task.completed", TaskSubject, z.object({ task: Task }));
260
+ var TaskFailed = event("task.failed", TaskSubject, z.object({ task: Task }));
261
+ var DomainEvent = z.discriminatedUnion("type", [
262
+ NodeCreated,
263
+ NodeUpdated,
264
+ NodeRemoved,
265
+ NodeStatusChanged,
266
+ NodeHeartbeat,
267
+ OrganizationCreated,
268
+ OrganizationUpdated,
269
+ OrganizationRemoved,
270
+ AgentCreated,
271
+ AgentUpdated,
272
+ AgentStatusChanged,
273
+ AgentRemoved,
274
+ MeshAgentAgentRegistered,
275
+ MeshAgentAgentDeregistered,
276
+ LinkRequested,
277
+ LinkUp,
278
+ LinkUpdated,
279
+ LinkDown,
280
+ LinkRemoved,
281
+ RoleGranted,
282
+ RoleRevoked,
283
+ TaskRequested,
284
+ TaskAccepted,
285
+ TaskCompleted,
286
+ TaskFailed
287
+ ]).meta({ id: "DomainEvent" });
288
+ DomainEvent.options.map((o) => o.shape.type.value);
289
+ var ENVELOPE_VERSION = 1;
290
+ var EnvelopeMeta = z.object({
291
+ v: z.literal(ENVELOPE_VERSION),
292
+ id: EventId,
293
+ /** Monotonic per origin node; `(origin.nodeId, seq)` is the replication cursor. */
294
+ seq: z.int().min(1),
295
+ origin: z.object({ nodeId: NodeId, principalId: PrincipalId.nullable() }),
296
+ /** Groups the events produced by one command, e.g. a cascade (ADR-0005). */
297
+ causationId: CommandId,
298
+ timestamp: Timestamp,
299
+ /** Ed25519 signature over the canonical encoding; attached by the protocol layer (Phase 5). */
300
+ sig: z.string().nullable()
301
+ });
302
+ var MeshEvent = z.intersection(EnvelopeMeta, DomainEvent).meta({ id: "MeshEvent" });
303
+ z.object({
304
+ status: z.enum(["ok", "degraded"]),
305
+ service: z.literal("node-server"),
306
+ /** The node this server runs as — clients use it to know where their writes are accepted (ADR-0004). */
307
+ nodeId: NodeId,
308
+ version: z.string(),
309
+ uptimeSeconds: z.number().nonnegative(),
310
+ timestamp: z.iso.datetime()
311
+ }).meta({ id: "HealthResponse", description: "Liveness and version information for a Node Server." });
312
+ z.object({ principal: Principal, roles: z.array(RoleBinding) }).meta({ id: "Me", description: "The authenticated principal and its role bindings." });
313
+ z.object({ name: DisplayName, kind: PrincipalKind.default("user") }).meta({ id: "CreatePrincipalRequest" });
314
+ var Credential = z.discriminatedUnion("type", [
315
+ z.object({ type: z.literal("token"), token: z.string(), expiresAt: Timestamp }),
316
+ z.object({ type: z.literal("apiKey"), apiKey: z.string() })
317
+ ]).meta({ id: "Credential" });
318
+ z.object({ principal: Principal, credential: Credential }).meta({ id: "CreatePrincipalResponse" });
319
+ var MeshCursor = z.record(NodeId, z.int().min(0)).meta({ id: "MeshCursor" });
320
+ z.object({
321
+ nodes: z.array(Node),
322
+ organizations: z.array(Organization),
323
+ agents: z.array(Agent),
324
+ links: z.array(Link),
325
+ /** The most recent tasks, newest first (ADR-0020). */
326
+ tasks: z.array(Task),
327
+ cursor: MeshCursor
328
+ }).meta({ id: "MeshSnapshot", description: "Full mesh state plus the cursor to resume the event stream from." });
329
+ z.object({
330
+ nodes: z.int().min(0),
331
+ online: z.int().min(0),
332
+ degraded: z.int().min(0),
333
+ offline: z.int().min(0),
334
+ organizations: z.int().min(0),
335
+ agents: z.int().min(0)
336
+ }).meta({ id: "MeshStats", description: "Counters shown in the Monitor's mesh summary." });
337
+ z.object({
338
+ node: Node,
339
+ organizations: z.array(Organization),
340
+ agents: z.array(Agent),
341
+ /** Distinct capabilities offered by the node's agents. */
342
+ capabilities: z.array(Capability)
343
+ }).meta({ id: "NodeDetail", description: "Everything the Monitor's node detail panel shows (ui.md)." });
344
+ function encodeCursor(cursor) {
345
+ const bytes = new TextEncoder().encode(JSON.stringify(cursor));
346
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
347
+ }
348
+ function decodeCursor(encoded) {
349
+ const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
350
+ const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
351
+ return MeshCursor.parse(JSON.parse(new TextDecoder().decode(bytes)));
352
+ }
353
+
354
+ // ../contracts/src/projection.ts
355
+ function project(event2) {
356
+ switch (event2.type) {
357
+ case "node.created":
358
+ case "node.updated":
359
+ return { op: "upsert", kind: "node", entity: event2.payload.node };
360
+ case "node.removed":
361
+ return { op: "delete", kind: "node", id: event2.payload.nodeId };
362
+ case "node.status_changed":
363
+ return {
364
+ op: "presence",
365
+ nodeId: event2.payload.nodeId,
366
+ status: event2.payload.status,
367
+ lastSeenAt: event2.payload.lastSeenAt
368
+ };
369
+ case "organization.created":
370
+ case "organization.updated":
371
+ return { op: "upsert", kind: "organization", entity: event2.payload.organization };
372
+ case "organization.removed":
373
+ return { op: "delete", kind: "organization", id: event2.payload.organizationId };
374
+ case "agent.created":
375
+ case "agent.updated":
376
+ case "agent.status_changed":
377
+ return { op: "upsert", kind: "agent", entity: event2.payload.agent };
378
+ case "agent.removed":
379
+ return { op: "delete", kind: "agent", id: event2.payload.agentId };
380
+ case "link.requested":
381
+ case "link.up":
382
+ case "link.updated":
383
+ case "link.down":
384
+ return { op: "upsert", kind: "link", entity: event2.payload.link };
385
+ case "link.removed":
386
+ return { op: "delete", kind: "link", id: event2.payload.linkId };
387
+ case "role.granted":
388
+ return { op: "upsert", kind: "roleBinding", entity: event2.payload.binding };
389
+ case "role.revoked":
390
+ return { op: "delete", kind: "roleBinding", id: event2.payload.binding.id };
391
+ case "task.requested":
392
+ case "task.accepted":
393
+ case "task.completed":
394
+ case "task.failed":
395
+ return { op: "upsert", kind: "task", entity: event2.payload.task };
396
+ // Registration is implied by organization membership; heartbeats are handled by the presence detector.
397
+ case "mesh_agent.agent_registered":
398
+ case "mesh_agent.agent_deregistered":
399
+ case "node.heartbeat":
400
+ return { op: "none" };
401
+ }
402
+ }
403
+
404
+ // ../contracts/src/mesh-state.ts
405
+ var byId = (items) => Object.fromEntries(items.map((item) => [item.id, item]));
406
+ function meshStateFromSnapshot(snapshot) {
407
+ return {
408
+ nodes: byId(snapshot.nodes),
409
+ organizations: byId(snapshot.organizations),
410
+ agents: byId(snapshot.agents),
411
+ links: byId(snapshot.links),
412
+ // Nodes from before Phase 6 send no tasks; tolerate them during rolling upgrades.
413
+ tasks: byId(snapshot.tasks ?? []),
414
+ cursor: { ...snapshot.cursor }
415
+ };
416
+ }
417
+ function applyMeshEvent(state, event2) {
418
+ const seen = state.cursor[event2.origin.nodeId] ?? 0;
419
+ if (event2.seq <= seen) return state;
420
+ const cursor = { ...state.cursor, [event2.origin.nodeId]: event2.seq };
421
+ const op = project(event2);
422
+ switch (op.op) {
423
+ case "upsert":
424
+ switch (op.kind) {
425
+ case "node":
426
+ return { ...state, cursor, nodes: { ...state.nodes, [op.entity.id]: op.entity } };
427
+ case "organization":
428
+ return { ...state, cursor, organizations: { ...state.organizations, [op.entity.id]: op.entity } };
429
+ case "agent":
430
+ return { ...state, cursor, agents: { ...state.agents, [op.entity.id]: op.entity } };
431
+ case "link":
432
+ return { ...state, cursor, links: { ...state.links, [op.entity.id]: op.entity } };
433
+ case "roleBinding":
434
+ return { ...state, cursor };
435
+ case "task": {
436
+ const current = state.tasks[op.entity.id];
437
+ if (current && current.version >= op.entity.version) return { ...state, cursor };
438
+ return { ...state, cursor, tasks: { ...state.tasks, [op.entity.id]: op.entity } };
439
+ }
440
+ }
441
+ break;
442
+ case "delete": {
443
+ if (op.kind === "roleBinding") return { ...state, cursor };
444
+ const table = { node: "nodes", organization: "organizations", agent: "agents", link: "links" };
445
+ const key = table[op.kind];
446
+ const { [op.id]: _removed, ...rest } = state[key];
447
+ return { ...state, cursor, [key]: rest };
448
+ }
449
+ case "presence": {
450
+ const node = state.nodes[op.nodeId];
451
+ if (!node) return { ...state, cursor };
452
+ return {
453
+ ...state,
454
+ cursor,
455
+ nodes: { ...state.nodes, [node.id]: { ...node, status: op.status, lastSeenAt: op.lastSeenAt } }
456
+ };
457
+ }
458
+ case "none":
459
+ return { ...state, cursor };
460
+ }
461
+ }
462
+ z.object({
463
+ limit: z.coerce.number().int().min(1).max(200).default(50),
464
+ cursor: z.string().min(1).optional()
465
+ });
466
+ z.object({
467
+ type: z.string().meta({ example: "/problems/forbidden" }),
468
+ title: z.string(),
469
+ status: z.int().min(400).max(599),
470
+ detail: z.string().optional(),
471
+ instance: z.string().optional(),
472
+ code: z.string().meta({ description: "Stable machine-readable error code" }),
473
+ errors: z.array(z.object({ path: z.string(), message: z.string() })).optional()
474
+ }).meta({ id: "ProblemDetails" });
475
+ var Topic = z.enum(["node", "organization", "agent", "link", "roleBinding", "task"]);
476
+ z.discriminatedUnion("type", [
477
+ z.object({
478
+ type: z.literal("hello"),
479
+ /** Bearer token or API key — sent in the first frame because browsers cannot set WebSocket headers. */
480
+ token: z.string().min(1),
481
+ /** Resume point, usually the cursor of the snapshot the client just loaded. */
482
+ cursor: MeshCursor.optional(),
483
+ topics: z.array(Topic).optional()
484
+ }),
485
+ z.object({ type: z.literal("subscribe"), topics: z.array(Topic) })
486
+ ]);
487
+ z.discriminatedUnion("type", [
488
+ z.object({ type: z.literal("welcome"), principalId: PrincipalId, cursor: MeshCursor }),
489
+ z.object({ type: z.literal("event"), event: MeshEvent }),
490
+ z.object({ type: z.literal("resync.required"), reason: z.string() }),
491
+ z.object({ type: z.literal("error"), code: z.string(), message: z.string() })
492
+ ]);
493
+ var RealtimeCloseCode = {
494
+ normal: 1e3,
495
+ unauthenticated: 4401,
496
+ resyncRequired: 4409
497
+ };
498
+
499
+ // src/errors.ts
500
+ var MeshApiError = class extends Error {
501
+ constructor(status, message, problem) {
502
+ super(message);
503
+ this.status = status;
504
+ this.problem = problem;
505
+ this.name = "MeshApiError";
506
+ }
507
+ status;
508
+ problem;
509
+ /** Stable machine-readable code, e.g. `forbidden`, `version_conflict`, `not_owner`. */
510
+ get code() {
511
+ return this.problem?.code ?? `http_${this.status}`;
512
+ }
513
+ };
514
+
515
+ // src/http.ts
516
+ var DEFAULT_TIMEOUT_MS = 1e4;
517
+ async function resolveToken(token) {
518
+ return typeof token === "function" ? token() : token;
519
+ }
520
+ var HttpClient = class {
521
+ constructor(options) {
522
+ this.options = options;
523
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
524
+ this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
525
+ }
526
+ options;
527
+ baseUrl;
528
+ doFetch;
529
+ token() {
530
+ return resolveToken(this.options.token);
531
+ }
532
+ async request(method, path, options = {}) {
533
+ const headers = { accept: "application/json" };
534
+ const token = await this.token();
535
+ if (token) headers["authorization"] = `Bearer ${token}`;
536
+ if (options.body !== void 0) headers["content-type"] = "application/json";
537
+ if (options.ifMatch !== void 0) headers["if-match"] = `"${options.ifMatch}"`;
538
+ if (options.idempotencyKey !== void 0) headers["idempotency-key"] = options.idempotencyKey;
539
+ const timeout = AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
540
+ const response = await this.doFetch(`${this.baseUrl}${path}${queryString(options.query)}`, {
541
+ method,
542
+ headers,
543
+ ...options.body !== void 0 && { body: JSON.stringify(options.body) },
544
+ signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
545
+ });
546
+ if (!response.ok) throw await toError(method, path, response);
547
+ if (response.status === 204) return void 0;
548
+ return await response.json();
549
+ }
550
+ };
551
+ function queryString(query) {
552
+ if (!query) return "";
553
+ const params = new URLSearchParams();
554
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) params.set(key, String(value));
555
+ const text = params.toString();
556
+ return text ? `?${text}` : "";
557
+ }
558
+ async function toError(method, path, response) {
559
+ const text = await response.text();
560
+ let problem;
561
+ try {
562
+ const parsed = JSON.parse(text);
563
+ if (parsed && typeof parsed.code === "string") problem = parsed;
564
+ } catch {
565
+ }
566
+ const message = problem?.detail ?? problem?.title ?? `${method} ${path} failed with ${response.status}`;
567
+ return new MeshApiError(response.status, message, problem);
568
+ }
569
+
570
+ // src/realtime.ts
571
+ var RealtimeError = class extends Error {
572
+ constructor(message, fatal) {
573
+ super(message);
574
+ this.fatal = fatal;
575
+ this.name = "RealtimeError";
576
+ }
577
+ fatal;
578
+ };
579
+ var MeshRealtime = class {
580
+ constructor(deps, options = {}) {
581
+ this.deps = deps;
582
+ this.options = options;
583
+ this.currentCursor = { ...options.cursor ?? {} };
584
+ this.needsSnapshot = options.cursor === void 0;
585
+ }
586
+ deps;
587
+ options;
588
+ currentStatus = "connecting";
589
+ currentState;
590
+ currentCursor;
591
+ needsSnapshot;
592
+ socket;
593
+ attempt = 0;
594
+ stopped = false;
595
+ timer;
596
+ handlers = /* @__PURE__ */ new Map();
597
+ statusListeners = /* @__PURE__ */ new Set();
598
+ stateListeners = /* @__PURE__ */ new Set();
599
+ errorListeners = /* @__PURE__ */ new Set();
600
+ get status() {
601
+ return this.currentStatus;
602
+ }
603
+ /** The mirrored mesh, when the stream started from (or resynced to) a snapshot. */
604
+ get state() {
605
+ return this.currentState;
606
+ }
607
+ /** Last sequence received per origin node. */
608
+ get cursor() {
609
+ return { ...this.currentCursor };
610
+ }
611
+ start() {
612
+ void this.connect();
613
+ }
614
+ /** Subscribes to one event type, or `"*"` for all. Returns an unsubscribe function. */
615
+ on(type, handler) {
616
+ const set = this.handlers.get(type) ?? /* @__PURE__ */ new Set();
617
+ set.add(handler);
618
+ this.handlers.set(type, set);
619
+ return () => set.delete(handler);
620
+ }
621
+ onStatus(listener) {
622
+ return subscribe(this.statusListeners, listener);
623
+ }
624
+ onState(listener) {
625
+ return subscribe(this.stateListeners, listener);
626
+ }
627
+ onError(listener) {
628
+ return subscribe(this.errorListeners, listener);
629
+ }
630
+ /** Resolves once the stream is caught up and live; rejects if it closes for good first. */
631
+ ready() {
632
+ if (this.currentStatus === "live") return Promise.resolve();
633
+ return new Promise((resolve, reject) => {
634
+ const off = this.onStatus((status) => {
635
+ if (status === "live") {
636
+ off();
637
+ resolve();
638
+ } else if (status === "closed") {
639
+ off();
640
+ reject(new RealtimeError("The realtime connection closed", true));
641
+ }
642
+ });
643
+ });
644
+ }
645
+ close() {
646
+ this.stopped = true;
647
+ clearTimeout(this.timer);
648
+ this.socket?.close(RealtimeCloseCode.normal, "client closed");
649
+ this.socket = void 0;
650
+ this.setStatus("closed");
651
+ }
652
+ async connect() {
653
+ if (this.stopped) return;
654
+ this.setStatus(this.attempt === 0 ? "connecting" : "reconnecting");
655
+ try {
656
+ if (this.needsSnapshot) await this.loadSnapshot();
657
+ const token = await this.deps.token();
658
+ if (!token) throw new RealtimeError("A token is required for the realtime stream", true);
659
+ if (this.stopped) return;
660
+ this.open(token);
661
+ } catch (error) {
662
+ const realtimeError = error instanceof RealtimeError ? error : new RealtimeError(error instanceof Error ? error.message : String(error), false);
663
+ this.emitError(realtimeError);
664
+ if (realtimeError.fatal) this.close();
665
+ else this.scheduleReconnect(false);
666
+ }
667
+ }
668
+ async loadSnapshot() {
669
+ const snapshot = await this.deps.snapshot();
670
+ this.currentState = meshStateFromSnapshot(snapshot);
671
+ this.currentCursor = { ...snapshot.cursor };
672
+ this.needsSnapshot = false;
673
+ this.emitState();
674
+ }
675
+ open(token) {
676
+ const Socket = this.deps.WebSocket ?? globalThis.WebSocket;
677
+ if (!Socket) throw new RealtimeError("No WebSocket implementation available; pass one in the client options", true);
678
+ const socket = new Socket(`${this.deps.baseUrl.replace(/^http/, "ws")}/v1/realtime`);
679
+ this.socket = socket;
680
+ socket.onopen = () => socket.send(
681
+ JSON.stringify({
682
+ type: "hello",
683
+ token,
684
+ cursor: this.currentCursor,
685
+ ...this.options.topics && { topics: this.options.topics }
686
+ })
687
+ );
688
+ socket.onmessage = (event2) => this.onMessage(String(event2.data));
689
+ socket.onclose = (event2) => {
690
+ if (this.socket === socket) this.onClose(event2.code);
691
+ };
692
+ socket.onerror = () => {
693
+ };
694
+ }
695
+ onMessage(raw) {
696
+ let message;
697
+ try {
698
+ message = JSON.parse(raw);
699
+ } catch {
700
+ this.emitError(new RealtimeError("Received a malformed message", false));
701
+ return;
702
+ }
703
+ switch (message.type) {
704
+ case "event":
705
+ this.onEvent(message.event);
706
+ break;
707
+ case "welcome":
708
+ this.attempt = 0;
709
+ this.setStatus("live");
710
+ break;
711
+ case "resync.required":
712
+ this.needsSnapshot = true;
713
+ break;
714
+ case "error":
715
+ this.emitError(new RealtimeError(`${message.code}: ${message.message}`, message.code === "unauthenticated"));
716
+ break;
717
+ }
718
+ }
719
+ onEvent(event2) {
720
+ const seen = this.currentCursor[event2.origin.nodeId] ?? 0;
721
+ if (event2.seq <= seen) return;
722
+ this.currentCursor = { ...this.currentCursor, [event2.origin.nodeId]: event2.seq };
723
+ if (this.currentState) this.currentState = applyMeshEvent(this.currentState, event2);
724
+ for (const handler of [...this.handlers.get(event2.type) ?? [], ...this.handlers.get("*") ?? []]) handler(event2);
725
+ this.emitState();
726
+ }
727
+ onClose(code) {
728
+ this.socket = void 0;
729
+ if (this.stopped) return;
730
+ if (code === RealtimeCloseCode.unauthenticated) {
731
+ this.emitError(new RealtimeError("The node rejected the credential", true));
732
+ this.close();
733
+ return;
734
+ }
735
+ this.scheduleReconnect(code === RealtimeCloseCode.resyncRequired);
736
+ }
737
+ scheduleReconnect(immediately) {
738
+ if (this.options.reconnect === false) {
739
+ this.close();
740
+ return;
741
+ }
742
+ const { initialDelayMs = 500, maxDelayMs = 3e4 } = this.options.reconnect ?? {};
743
+ const ceiling = Math.min(maxDelayMs, initialDelayMs * 2 ** this.attempt);
744
+ const delay = immediately ? 0 : Math.round((this.options.random ?? Math.random)() * ceiling);
745
+ this.attempt += 1;
746
+ this.setStatus("reconnecting");
747
+ this.timer = setTimeout(() => void this.connect(), delay);
748
+ }
749
+ setStatus(status) {
750
+ if (status === this.currentStatus) return;
751
+ this.currentStatus = status;
752
+ for (const listener of this.statusListeners) listener(status);
753
+ }
754
+ emitState() {
755
+ const state = this.currentState;
756
+ if (state) for (const listener of this.stateListeners) listener(state);
757
+ }
758
+ emitError(error) {
759
+ for (const listener of this.errorListeners) listener(error);
760
+ }
761
+ };
762
+ function subscribe(set, listener) {
763
+ set.add(listener);
764
+ return () => set.delete(listener);
765
+ }
766
+
767
+ // src/client.ts
768
+ var id = encodeURIComponent;
769
+ async function* all(fetchPage) {
770
+ let cursor;
771
+ do {
772
+ const page = await fetchPage(cursor);
773
+ yield* page.items;
774
+ cursor = page.nextCursor ?? void 0;
775
+ } while (cursor);
776
+ }
777
+ function ownersOf(http, base) {
778
+ return {
779
+ list: async () => (await http.request("GET", `${base}/owners`)).items,
780
+ grant: (principalId, options = {}) => http.request("POST", `${base}/owners`, { body: { principalId }, ...options }),
781
+ revoke: (principalId) => http.request("DELETE", `${base}/owners/${id(principalId)}`)
782
+ };
783
+ }
784
+ function createMeshClient(options) {
785
+ const http = new HttpClient(options);
786
+ const client = {
787
+ /** Liveness and version (public). */
788
+ health: () => http.request("GET", "/v1/health"),
789
+ /** The caller and their ownership roles. */
790
+ me: () => http.request("GET", "/v1/me"),
791
+ snapshot: () => http.request("GET", "/v1/mesh/snapshot"),
792
+ stats: () => http.request("GET", "/v1/mesh/stats"),
793
+ capabilities: async (search) => (await http.request("GET", "/v1/capabilities", { query: { q: search } })).items,
794
+ /** Agents offering a capability, available ones first (ADR-0020). */
795
+ capabilityProviders: async (name) => (await http.request("GET", `/v1/capabilities/${id(name)}/providers`)).items,
796
+ principals: {
797
+ /** Node owners only. The credential is returned exactly once. */
798
+ create: (input, options2 = {}) => http.request("POST", "/v1/principals", { body: input, ...options2 })
799
+ },
800
+ nodes: {
801
+ list: (page = {}) => http.request("GET", "/v1/nodes", { query: { ...page } }),
802
+ all: () => all((cursor) => client.nodes.list({ cursor })),
803
+ get: (nodeId) => http.request("GET", `/v1/nodes/${id(nodeId)}`),
804
+ detail: (nodeId) => http.request("GET", `/v1/nodes/${id(nodeId)}/detail`),
805
+ organizations: (nodeId, page = {}) => http.request("GET", `/v1/nodes/${id(nodeId)}/organizations`, { query: { ...page } }),
806
+ update: (nodeId, changes, options2 = {}) => http.request("PATCH", `/v1/nodes/${id(nodeId)}`, { body: changes, ...options2 }),
807
+ /** Removes the node and, children first, everything on it. */
808
+ remove: (nodeId, options2 = {}) => http.request("DELETE", `/v1/nodes/${id(nodeId)}`, options2),
809
+ owners: (nodeId) => ownersOf(http, `/v1/nodes/${id(nodeId)}`),
810
+ /** Forgets an offline remote node, with its organizations, agents and links (owners of this node). */
811
+ forget: (nodeId) => http.request("POST", `/v1/nodes/${id(nodeId)}/forget`)
812
+ },
813
+ links: {
814
+ list: async () => (await http.request("GET", "/v1/links")).items,
815
+ /** Connects the node this client talks to with a peer, by URL or by the peer's node id (its advertised endpoint). */
816
+ connect: (target, options2 = {}) => http.request("POST", "/v1/links", { body: target, ...options2 }),
817
+ /** Disconnects the peer at the other end and removes the link. */
818
+ remove: (linkId) => http.request("DELETE", `/v1/links/${id(linkId)}`)
819
+ },
820
+ organizations: {
821
+ /** Creates an organization (and its Mesh Agent) on the node this client talks to. */
822
+ create: (input, options2 = {}) => http.request("POST", "/v1/organizations", { body: input, ...options2 }),
823
+ list: (filter = {}) => http.request("GET", "/v1/organizations", { query: { ...filter } }),
824
+ all: (filter = {}) => all((cursor) => client.organizations.list({ ...filter, cursor })),
825
+ get: (organizationId) => http.request("GET", `/v1/organizations/${id(organizationId)}`),
826
+ update: (organizationId, changes, options2 = {}) => http.request("PATCH", `/v1/organizations/${id(organizationId)}`, { body: changes, ...options2 }),
827
+ /** Removes the organization and all of its agents. */
828
+ remove: (organizationId, options2 = {}) => http.request("DELETE", `/v1/organizations/${id(organizationId)}`, options2),
829
+ owners: (organizationId) => ownersOf(http, `/v1/organizations/${id(organizationId)}`)
830
+ },
831
+ agents: {
832
+ /** Creates an agent; it is registered with its organization's Mesh Agent automatically. */
833
+ create: (organizationId, input, options2 = {}) => http.request("POST", `/v1/organizations/${id(organizationId)}/agents`, { body: input, ...options2 }),
834
+ list: (filter = {}) => http.request("GET", "/v1/agents", { query: { ...filter } }),
835
+ all: (filter = {}) => all((cursor) => client.agents.list({ ...filter, cursor })),
836
+ get: (agentId) => http.request("GET", `/v1/agents/${id(agentId)}`),
837
+ update: (agentId, changes, options2 = {}) => http.request("PATCH", `/v1/agents/${id(agentId)}`, { body: changes, ...options2 }),
838
+ /** Deregisters the agent from its Mesh Agent, then removes it. */
839
+ remove: (agentId, options2 = {}) => http.request("DELETE", `/v1/agents/${id(agentId)}`, options2),
840
+ start: (agentId) => http.request("POST", `/v1/agents/${id(agentId)}/start`),
841
+ stop: (agentId) => http.request("POST", `/v1/agents/${id(agentId)}/stop`)
842
+ },
843
+ tasks: {
844
+ /**
845
+ * Asks the mesh for a capability on behalf of `agentId` (node or org owners). The agent's Mesh Agent routes the
846
+ * request; the returned task is `requested`, and the provider's node completes it (ADR-0020).
847
+ */
848
+ request: (agentId, input, options2 = {}) => http.request("POST", `/v1/agents/${id(agentId)}/tasks`, { body: input, ...options2 }),
849
+ /** Newest first. */
850
+ list: (filter = {}) => http.request("GET", "/v1/tasks", { query: { ...filter } }),
851
+ all: (filter = {}) => all((cursor) => client.tasks.list({ ...filter, cursor })),
852
+ get: (taskId) => http.request("GET", `/v1/tasks/${id(taskId)}`),
853
+ /** Resolves with the task once it is `completed` or `failed`; rejects after `timeoutMs`. */
854
+ wait: async (taskId, waitOptions = {}) => {
855
+ const deadline = Date.now() + (waitOptions.timeoutMs ?? 3e4);
856
+ for (; ; ) {
857
+ const task = await client.tasks.get(taskId);
858
+ if (task.status === "completed" || task.status === "failed") return task;
859
+ if (Date.now() >= deadline) throw new Error(`Task ${taskId} still ${task.status} after waiting`);
860
+ await new Promise((resolve) => setTimeout(resolve, waitOptions.intervalMs ?? 250));
861
+ }
862
+ }
863
+ },
864
+ realtime: {
865
+ /**
866
+ * Opens the realtime stream. Without a `cursor` it first loads a snapshot and keeps `state` current;
867
+ * it reconnects with backoff and resumes from the last event it saw.
868
+ */
869
+ connect: (realtimeOptions = {}) => {
870
+ const realtime = new MeshRealtime(
871
+ { baseUrl: http.baseUrl, token: () => http.token(), snapshot: client.snapshot, WebSocket: options.WebSocket },
872
+ realtimeOptions
873
+ );
874
+ realtime.start();
875
+ return realtime;
876
+ }
877
+ }
878
+ };
879
+ return client;
880
+ }
881
+
882
+ export { MeshApiError, MeshRealtime, RealtimeError, applyMeshEvent, createMeshClient, decodeCursor, encodeCursor, meshStateFromSnapshot };
883
+ //# sourceMappingURL=index.js.map
884
+ //# sourceMappingURL=index.js.map