acn-client 0.12.0 → 0.14.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.mjs CHANGED
@@ -83,6 +83,9 @@ var ACNClient = class {
83
83
  post(path, body) {
84
84
  return this.request("POST", path, { body });
85
85
  }
86
+ patch(path, body) {
87
+ return this.request("PATCH", path, { body });
88
+ }
86
89
  delete(path) {
87
90
  return this.request("DELETE", path);
88
91
  }
@@ -199,16 +202,39 @@ var ACNClient = class {
199
202
  return this.get("/api/v1/subnets");
200
203
  }
201
204
  /** Get subnet by ID */
202
- async getSubnet(subnetId) {
203
- return this.get(`/api/v1/subnets/${subnetId}`);
205
+ async getSubnet(slug) {
206
+ return this.get(`/api/v1/subnets/${slug}`);
207
+ }
208
+ /**
209
+ * List immediate children of a subnet (ADR-0003).
210
+ *
211
+ * Wraps `GET /api/v1/subnets/{parentSlug}/children`. Returns
212
+ * `SUBNET_NOT_FOUND` when the parent does not exist. Visibility
213
+ * matches `listSubnets` — private children you cannot see are
214
+ * omitted from the result set.
215
+ */
216
+ async listChildren(parentSlug) {
217
+ const data = await this.get(
218
+ `/api/v1/subnets/${parentSlug}/children`
219
+ );
220
+ return data.subnets;
221
+ }
222
+ /**
223
+ * Promote a `task_scoped` subnet to `persistent` (ADR-0003).
224
+ *
225
+ * Owner-only. Idempotent — promoting an already-persistent subnet
226
+ * returns its current state unchanged.
227
+ */
228
+ async promoteSubnet(slug) {
229
+ return this.post(`/api/v1/subnets/${slug}/promote`);
204
230
  }
205
231
  /** Delete a subnet you own (requires Agent API Key — only the owning agent can delete) */
206
- async deleteSubnet(subnetId) {
207
- return this.request("DELETE", `/api/v1/subnets/${subnetId}`);
232
+ async deleteSubnet(slug) {
233
+ return this.request("DELETE", `/api/v1/subnets/${slug}`);
208
234
  }
209
235
  /** Get agents in a subnet */
210
- async getSubnetAgents(subnetId) {
211
- return this.get(`/api/v1/subnets/${subnetId}/agents`);
236
+ async getSubnetAgents(slug) {
237
+ return this.get(`/api/v1/subnets/${slug}/agents`);
212
238
  }
213
239
  // ──────────────────────────────────────────────────────────────────────
214
240
  // Subnet membership (agent-side)
@@ -223,18 +249,219 @@ var ACNClient = class {
223
249
  // scheduled for removal. Requires ACN backend ≥ post-PR-#42.
224
250
  // ──────────────────────────────────────────────────────────────────────
225
251
  /** Join agent to subnet */
226
- async joinSubnet(agentId, subnetId) {
227
- return this.post(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
252
+ async joinSubnet(agentId, slug) {
253
+ return this.post(`/api/v1/agents/${agentId}/subnets/${slug}`);
228
254
  }
229
255
  /** Remove agent from subnet */
230
- async leaveSubnet(agentId, subnetId) {
231
- return this.delete(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
256
+ async leaveSubnet(agentId, slug) {
257
+ return this.delete(`/api/v1/agents/${agentId}/subnets/${slug}`);
232
258
  }
233
259
  /** Get agent's subnets */
234
260
  async getAgentSubnets(agentId) {
235
261
  return this.get(`/api/v1/agents/${agentId}/subnets`);
236
262
  }
237
263
  // ============================================
264
+ // ADR-0004 Subnet Admission
265
+ // ============================================
266
+ //
267
+ // 13 verbs gated by `subnet.join_policy === 'approval'`:
268
+ // - Allowlist (3): owner pre-authorisation.
269
+ // - Join requests (4): applicant-initiated path.
270
+ // - Invitations (5): owner-initiated path.
271
+ // - Agent-side (1): invitee's cross-subnet pending view.
272
+ //
273
+ // The plain `joinSubnet` verb dispatches the six-branch decision
274
+ // tree on the server side — these methods are the admin-side
275
+ // controls used by subnet owners and the per-row decisions used
276
+ // by applicants and invitees.
277
+ //
278
+ // Method names use the `subnet*` prefix to avoid colliding with
279
+ // the existing inbox `addToAllowlist` surface (which lives at
280
+ // `/api/v1/agents/{a}/allowlist/{target}` and is unrelated).
281
+ // ----- Allowlist (owner-only, 3 verbs) ---------------------------------
282
+ /**
283
+ * Pre-authorise `agentId` on `slug`'s allowlist (owner only).
284
+ *
285
+ * Allowlisted agents skip the approval queue: their next
286
+ * `joinSubnet` lands in branch 4 (allowlist hit) and becomes an
287
+ * immediate member with an `allowlist_auto` audit row.
288
+ *
289
+ * Server returns 201 with the persisted entry; duplicate adds
290
+ * return 409 ALREADY_ON_ALLOWLIST (raised as an error, never
291
+ * silently no-op'd).
292
+ */
293
+ async subnetAllowlistAdd(slug, agentId) {
294
+ return this.post(`/api/v1/subnets/${slug}/allowlist`, {
295
+ agent_id: agentId
296
+ });
297
+ }
298
+ /**
299
+ * Remove `agentId` from `slug`'s allowlist (owner only).
300
+ *
301
+ * Idempotent — removing an entry that doesn't exist still
302
+ * returns 204. Per ADR-0004 §"Allowlist mutation does not
303
+ * affect agents who already joined", this does NOT revoke
304
+ * membership for agents already admitted via the allowlist.
305
+ */
306
+ async subnetAllowlistRemove(slug, agentId) {
307
+ await this.delete(`/api/v1/subnets/${slug}/allowlist/${agentId}`);
308
+ }
309
+ /**
310
+ * List `slug`'s allowlist entries (owner only).
311
+ *
312
+ * Owner-only by design — the allowlist is a privacy-sensitive
313
+ * trust signal and exposing it publicly would leak relationship
314
+ * metadata.
315
+ */
316
+ async subnetAllowlistList(slug, options) {
317
+ const params = {
318
+ limit: options?.limit ?? 100,
319
+ offset: options?.offset ?? 0
320
+ };
321
+ return this.get(`/api/v1/subnets/${slug}/allowlist`, params);
322
+ }
323
+ // ----- Join requests (4 verbs: 3 owner-side + 1 applicant-side) --------
324
+ /**
325
+ * Owner approves a pending join_request (CAS pending → approved).
326
+ *
327
+ * Side effects: applicant added to `subnet.member_agent_ids` and
328
+ * the `subnet.join_approved` webhook fires. The applicant is
329
+ * still expected to call `joinSubnet` to register the
330
+ * `agent.subnet_ids` back-reference (per ADR-0004 §"State
331
+ * machine edges").
332
+ *
333
+ * Optional `note` (≤500 chars) is recorded on the audit row.
334
+ */
335
+ async subnetJoinRequestApprove(slug, requestId, options) {
336
+ return this.post(
337
+ `/api/v1/subnets/${slug}/join-requests/${requestId}/approve`,
338
+ options?.note !== void 0 ? { note: options.note } : void 0
339
+ );
340
+ }
341
+ /**
342
+ * Owner rejects a pending join_request (CAS pending → rejected).
343
+ *
344
+ * No membership change. `subnet.join_rejected` webhook fires.
345
+ */
346
+ async subnetJoinRequestReject(slug, requestId, options) {
347
+ return this.post(
348
+ `/api/v1/subnets/${slug}/join-requests/${requestId}/reject`,
349
+ options?.note !== void 0 ? { note: options.note } : void 0
350
+ );
351
+ }
352
+ /**
353
+ * Applicant withdraws their own pending join_request.
354
+ *
355
+ * Self-only — caller must be the agent who originally created
356
+ * the request. `subnet.join_withdrawn` webhook fires.
357
+ */
358
+ async subnetJoinRequestWithdraw(slug, requestId, options) {
359
+ return this.request(
360
+ "DELETE",
361
+ `/api/v1/subnets/${slug}/join-requests/${requestId}`,
362
+ options?.note !== void 0 ? { body: { note: options.note } } : void 0
363
+ );
364
+ }
365
+ /**
366
+ * Owner lists join_request / allowlist_auto rows for `slug`.
367
+ *
368
+ * `kind` defaults to `'join_request'`; pass `'allowlist_auto'`
369
+ * to inspect synthesised allowlist-hit audit rows. Server
370
+ * rejects `kind='invitation'` with 400 INVALID_KIND_FILTER —
371
+ * use `subnetInvitationList` instead.
372
+ */
373
+ async subnetJoinRequestList(slug, options) {
374
+ const params = {
375
+ kind: options?.kind ?? "join_request",
376
+ limit: options?.limit ?? 100,
377
+ offset: options?.offset ?? 0
378
+ };
379
+ if (options?.status !== void 0) params.status = options.status;
380
+ return this.get(`/api/v1/subnets/${slug}/join-requests`, params);
381
+ }
382
+ // ----- Invitations (5 + 1 verbs) ---------------------------------------
383
+ /**
384
+ * Owner sends an invitation to `agentId` (or merges into a
385
+ * pending join_request from the same target).
386
+ *
387
+ * Two response shapes per ADR-0004 §"Invitation merge path":
388
+ *
389
+ * - **Normal path** (server returns 202): `{ invitation_id, status: 'pending' }`.
390
+ * - **Merge path** (server returns 200, request auto-approved):
391
+ * `{ auto_resolved: true, resolved_kind: 'join_request', request_id }`.
392
+ *
393
+ * Discriminate on `auto_resolved` to dispatch.
394
+ */
395
+ async subnetInvitationSend(slug, agentId, options) {
396
+ const body = { agent_id: agentId };
397
+ if (options?.note !== void 0) body.note = options.note;
398
+ return this.post(`/api/v1/subnets/${slug}/invitations`, body);
399
+ }
400
+ /**
401
+ * Invitee accepts a pending invitation (CAS pending → approved).
402
+ *
403
+ * Self-only against the row's `agent_id`. Side effects: invitee
404
+ * added to `subnet.member_agent_ids`, the agent's `subnet_ids`
405
+ * gains the back-reference, and `subnet.invitation_accepted`
406
+ * webhook fires.
407
+ */
408
+ async subnetInvitationAccept(slug, requestId, options) {
409
+ return this.post(
410
+ `/api/v1/subnets/${slug}/invitations/${requestId}/accept`,
411
+ options?.note !== void 0 ? { note: options.note } : void 0
412
+ );
413
+ }
414
+ /**
415
+ * Invitee rejects a pending invitation (CAS pending → rejected).
416
+ *
417
+ * No membership change. `subnet.invitation_rejected` webhook
418
+ * fires.
419
+ */
420
+ async subnetInvitationReject(slug, requestId, options) {
421
+ return this.post(
422
+ `/api/v1/subnets/${slug}/invitations/${requestId}/reject`,
423
+ options?.note !== void 0 ? { note: options.note } : void 0
424
+ );
425
+ }
426
+ /**
427
+ * Owner cancels a pending invitation (CAS pending → withdrawn).
428
+ *
429
+ * Owner-only counterpart to applicant withdraw. The row goes to
430
+ * `withdrawn` (not `rejected`) — distinct audit token so
431
+ * consumers can tell "owner gave up" from "invitee said no".
432
+ */
433
+ async subnetInvitationCancel(slug, requestId, options) {
434
+ return this.request(
435
+ "DELETE",
436
+ `/api/v1/subnets/${slug}/invitations/${requestId}`,
437
+ options?.note !== void 0 ? { body: { note: options.note } } : void 0
438
+ );
439
+ }
440
+ /**
441
+ * Owner lists invitation rows for `slug`.
442
+ *
443
+ * Owner-only — invitees use `agentSubnetInvitations` for their
444
+ * own cross-subnet view.
445
+ */
446
+ async subnetInvitationList(slug, options) {
447
+ const params = {
448
+ limit: options?.limit ?? 100,
449
+ offset: options?.offset ?? 0
450
+ };
451
+ if (options?.status !== void 0) params.status = options.status;
452
+ return this.get(`/api/v1/subnets/${slug}/invitations`, params);
453
+ }
454
+ /**
455
+ * Invitee's cross-subnet pending-invitation list (self only).
456
+ *
457
+ * Returns only `status='pending'` rows. Historical decisions
458
+ * are queryable per-subnet through the owner-only
459
+ * `subnetInvitationList`.
460
+ */
461
+ async agentSubnetInvitations(agentId) {
462
+ return this.get(`/api/v1/agents/${agentId}/subnet-invitations`);
463
+ }
464
+ // ============================================
238
465
  // Communication
239
466
  // ============================================
240
467
  /** Send message to an agent */
@@ -287,6 +514,31 @@ var ACNClient = class {
287
514
  if (options?.consume) params.ack = true;
288
515
  return this.get(`/api/v1/communication/history/${agentId}`, params);
289
516
  }
517
+ /**
518
+ * Precisely acknowledge (remove) specific messages from the inbox.
519
+ *
520
+ * Unlike `getMessageHistory({ consume: true })` which clears the entire inbox,
521
+ * this method removes only the messages whose `route_id` values are listed.
522
+ *
523
+ * @param agentId Must match the authenticated agent's ID.
524
+ * @param routeIds List of `route_id` values to remove (up to 500).
525
+ * @returns Number of messages actually removed.
526
+ */
527
+ async ackInbox(agentId, routeIds) {
528
+ return this.post(`/api/v1/communication/history/${agentId}/ack`, { route_ids: routeIds });
529
+ }
530
+ /**
531
+ * Update the lifecycle status of a specific inbox message.
532
+ *
533
+ * @param agentId Must match the authenticated agent's ID.
534
+ * @param routeId `route_id` of the target message (from inbox listing).
535
+ * @param status New status: `"unread"` | `"read"` | `"processed"`.
536
+ * @returns Object with `agent_id`, `route_id`, and `status`.
537
+ * @throws 404 (`inbox_message_not_found`) if route_id is absent from inbox.
538
+ */
539
+ async updateInboxMessageStatus(agentId, routeId, status) {
540
+ return this.patch(`/api/v1/communication/history/${agentId}/${routeId}`, { status });
541
+ }
290
542
  // ============================================
291
543
  // Manifest Queue (Phase 2/3)
292
544
  // ============================================
@@ -863,8 +1115,8 @@ var ACNClient = class {
863
1115
  * Register (or clear) an org-harness webhook URL for a subnet.
864
1116
  * Pass `harnessUrl: null` to deregister.
865
1117
  */
866
- async registerSubnetHarness(subnetId, harnessUrl, harnessSecret) {
867
- await this.request("PATCH", `/api/v1/subnets/${subnetId}/harness`, {
1118
+ async registerSubnetHarness(slug, harnessUrl, harnessSecret) {
1119
+ await this.request("PATCH", `/api/v1/subnets/${slug}/harness`, {
868
1120
  body: {
869
1121
  harness_url: harnessUrl,
870
1122
  harness_secret: harnessSecret ?? null
@@ -880,6 +1132,7 @@ var ACNError = class extends Error {
880
1132
  this.errorCode = options?.errorCode;
881
1133
  this.requestId = options?.requestId;
882
1134
  }
1135
+ status;
883
1136
  /** ACN internal error code (present on sanitised 5xx responses) */
884
1137
  errorCode;
885
1138
  /** Request ID minted by ACN for 5xx responses (useful for support) */
@@ -1098,10 +1351,16 @@ var KNOWN_PAYMENT_TASK_STATUSES = [
1098
1351
  "payment_failed",
1099
1352
  "refunded"
1100
1353
  ];
1354
+ var KNOWN_INBOX_MESSAGE_STATUSES = [
1355
+ "unread",
1356
+ "read",
1357
+ "processed"
1358
+ ];
1101
1359
  export {
1102
1360
  ACNClient,
1103
1361
  ACNError,
1104
1362
  ACNRealtime,
1363
+ KNOWN_INBOX_MESSAGE_STATUSES,
1105
1364
  KNOWN_PAYMENT_TASK_STATUSES,
1106
1365
  subscribeToACN
1107
1366
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acn-client",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Official TypeScript/JavaScript client for ACN (Agent Collaboration Network)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -51,7 +51,7 @@
51
51
  "@types/node": "^20.0.0",
52
52
  "tsup": "^8.0.0",
53
53
  "typescript": "^5.0.0",
54
- "vitest": "^1.0.0"
54
+ "vitest": "^2.1.9"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "typescript": ">=4.7.0"