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.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  ACNClient: () => ACNClient,
34
34
  ACNError: () => ACNError,
35
35
  ACNRealtime: () => ACNRealtime,
36
+ KNOWN_INBOX_MESSAGE_STATUSES: () => KNOWN_INBOX_MESSAGE_STATUSES,
36
37
  KNOWN_PAYMENT_TASK_STATUSES: () => KNOWN_PAYMENT_TASK_STATUSES,
37
38
  subscribeToACN: () => subscribeToACN
38
39
  });
@@ -123,6 +124,9 @@ var ACNClient = class {
123
124
  post(path, body) {
124
125
  return this.request("POST", path, { body });
125
126
  }
127
+ patch(path, body) {
128
+ return this.request("PATCH", path, { body });
129
+ }
126
130
  delete(path) {
127
131
  return this.request("DELETE", path);
128
132
  }
@@ -239,16 +243,39 @@ var ACNClient = class {
239
243
  return this.get("/api/v1/subnets");
240
244
  }
241
245
  /** Get subnet by ID */
242
- async getSubnet(subnetId) {
243
- return this.get(`/api/v1/subnets/${subnetId}`);
246
+ async getSubnet(slug) {
247
+ return this.get(`/api/v1/subnets/${slug}`);
248
+ }
249
+ /**
250
+ * List immediate children of a subnet (ADR-0003).
251
+ *
252
+ * Wraps `GET /api/v1/subnets/{parentSlug}/children`. Returns
253
+ * `SUBNET_NOT_FOUND` when the parent does not exist. Visibility
254
+ * matches `listSubnets` — private children you cannot see are
255
+ * omitted from the result set.
256
+ */
257
+ async listChildren(parentSlug) {
258
+ const data = await this.get(
259
+ `/api/v1/subnets/${parentSlug}/children`
260
+ );
261
+ return data.subnets;
262
+ }
263
+ /**
264
+ * Promote a `task_scoped` subnet to `persistent` (ADR-0003).
265
+ *
266
+ * Owner-only. Idempotent — promoting an already-persistent subnet
267
+ * returns its current state unchanged.
268
+ */
269
+ async promoteSubnet(slug) {
270
+ return this.post(`/api/v1/subnets/${slug}/promote`);
244
271
  }
245
272
  /** Delete a subnet you own (requires Agent API Key — only the owning agent can delete) */
246
- async deleteSubnet(subnetId) {
247
- return this.request("DELETE", `/api/v1/subnets/${subnetId}`);
273
+ async deleteSubnet(slug) {
274
+ return this.request("DELETE", `/api/v1/subnets/${slug}`);
248
275
  }
249
276
  /** Get agents in a subnet */
250
- async getSubnetAgents(subnetId) {
251
- return this.get(`/api/v1/subnets/${subnetId}/agents`);
277
+ async getSubnetAgents(slug) {
278
+ return this.get(`/api/v1/subnets/${slug}/agents`);
252
279
  }
253
280
  // ──────────────────────────────────────────────────────────────────────
254
281
  // Subnet membership (agent-side)
@@ -263,18 +290,219 @@ var ACNClient = class {
263
290
  // scheduled for removal. Requires ACN backend ≥ post-PR-#42.
264
291
  // ──────────────────────────────────────────────────────────────────────
265
292
  /** Join agent to subnet */
266
- async joinSubnet(agentId, subnetId) {
267
- return this.post(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
293
+ async joinSubnet(agentId, slug) {
294
+ return this.post(`/api/v1/agents/${agentId}/subnets/${slug}`);
268
295
  }
269
296
  /** Remove agent from subnet */
270
- async leaveSubnet(agentId, subnetId) {
271
- return this.delete(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
297
+ async leaveSubnet(agentId, slug) {
298
+ return this.delete(`/api/v1/agents/${agentId}/subnets/${slug}`);
272
299
  }
273
300
  /** Get agent's subnets */
274
301
  async getAgentSubnets(agentId) {
275
302
  return this.get(`/api/v1/agents/${agentId}/subnets`);
276
303
  }
277
304
  // ============================================
305
+ // ADR-0004 Subnet Admission
306
+ // ============================================
307
+ //
308
+ // 13 verbs gated by `subnet.join_policy === 'approval'`:
309
+ // - Allowlist (3): owner pre-authorisation.
310
+ // - Join requests (4): applicant-initiated path.
311
+ // - Invitations (5): owner-initiated path.
312
+ // - Agent-side (1): invitee's cross-subnet pending view.
313
+ //
314
+ // The plain `joinSubnet` verb dispatches the six-branch decision
315
+ // tree on the server side — these methods are the admin-side
316
+ // controls used by subnet owners and the per-row decisions used
317
+ // by applicants and invitees.
318
+ //
319
+ // Method names use the `subnet*` prefix to avoid colliding with
320
+ // the existing inbox `addToAllowlist` surface (which lives at
321
+ // `/api/v1/agents/{a}/allowlist/{target}` and is unrelated).
322
+ // ----- Allowlist (owner-only, 3 verbs) ---------------------------------
323
+ /**
324
+ * Pre-authorise `agentId` on `slug`'s allowlist (owner only).
325
+ *
326
+ * Allowlisted agents skip the approval queue: their next
327
+ * `joinSubnet` lands in branch 4 (allowlist hit) and becomes an
328
+ * immediate member with an `allowlist_auto` audit row.
329
+ *
330
+ * Server returns 201 with the persisted entry; duplicate adds
331
+ * return 409 ALREADY_ON_ALLOWLIST (raised as an error, never
332
+ * silently no-op'd).
333
+ */
334
+ async subnetAllowlistAdd(slug, agentId) {
335
+ return this.post(`/api/v1/subnets/${slug}/allowlist`, {
336
+ agent_id: agentId
337
+ });
338
+ }
339
+ /**
340
+ * Remove `agentId` from `slug`'s allowlist (owner only).
341
+ *
342
+ * Idempotent — removing an entry that doesn't exist still
343
+ * returns 204. Per ADR-0004 §"Allowlist mutation does not
344
+ * affect agents who already joined", this does NOT revoke
345
+ * membership for agents already admitted via the allowlist.
346
+ */
347
+ async subnetAllowlistRemove(slug, agentId) {
348
+ await this.delete(`/api/v1/subnets/${slug}/allowlist/${agentId}`);
349
+ }
350
+ /**
351
+ * List `slug`'s allowlist entries (owner only).
352
+ *
353
+ * Owner-only by design — the allowlist is a privacy-sensitive
354
+ * trust signal and exposing it publicly would leak relationship
355
+ * metadata.
356
+ */
357
+ async subnetAllowlistList(slug, options) {
358
+ const params = {
359
+ limit: options?.limit ?? 100,
360
+ offset: options?.offset ?? 0
361
+ };
362
+ return this.get(`/api/v1/subnets/${slug}/allowlist`, params);
363
+ }
364
+ // ----- Join requests (4 verbs: 3 owner-side + 1 applicant-side) --------
365
+ /**
366
+ * Owner approves a pending join_request (CAS pending → approved).
367
+ *
368
+ * Side effects: applicant added to `subnet.member_agent_ids` and
369
+ * the `subnet.join_approved` webhook fires. The applicant is
370
+ * still expected to call `joinSubnet` to register the
371
+ * `agent.subnet_ids` back-reference (per ADR-0004 §"State
372
+ * machine edges").
373
+ *
374
+ * Optional `note` (≤500 chars) is recorded on the audit row.
375
+ */
376
+ async subnetJoinRequestApprove(slug, requestId, options) {
377
+ return this.post(
378
+ `/api/v1/subnets/${slug}/join-requests/${requestId}/approve`,
379
+ options?.note !== void 0 ? { note: options.note } : void 0
380
+ );
381
+ }
382
+ /**
383
+ * Owner rejects a pending join_request (CAS pending → rejected).
384
+ *
385
+ * No membership change. `subnet.join_rejected` webhook fires.
386
+ */
387
+ async subnetJoinRequestReject(slug, requestId, options) {
388
+ return this.post(
389
+ `/api/v1/subnets/${slug}/join-requests/${requestId}/reject`,
390
+ options?.note !== void 0 ? { note: options.note } : void 0
391
+ );
392
+ }
393
+ /**
394
+ * Applicant withdraws their own pending join_request.
395
+ *
396
+ * Self-only — caller must be the agent who originally created
397
+ * the request. `subnet.join_withdrawn` webhook fires.
398
+ */
399
+ async subnetJoinRequestWithdraw(slug, requestId, options) {
400
+ return this.request(
401
+ "DELETE",
402
+ `/api/v1/subnets/${slug}/join-requests/${requestId}`,
403
+ options?.note !== void 0 ? { body: { note: options.note } } : void 0
404
+ );
405
+ }
406
+ /**
407
+ * Owner lists join_request / allowlist_auto rows for `slug`.
408
+ *
409
+ * `kind` defaults to `'join_request'`; pass `'allowlist_auto'`
410
+ * to inspect synthesised allowlist-hit audit rows. Server
411
+ * rejects `kind='invitation'` with 400 INVALID_KIND_FILTER —
412
+ * use `subnetInvitationList` instead.
413
+ */
414
+ async subnetJoinRequestList(slug, options) {
415
+ const params = {
416
+ kind: options?.kind ?? "join_request",
417
+ limit: options?.limit ?? 100,
418
+ offset: options?.offset ?? 0
419
+ };
420
+ if (options?.status !== void 0) params.status = options.status;
421
+ return this.get(`/api/v1/subnets/${slug}/join-requests`, params);
422
+ }
423
+ // ----- Invitations (5 + 1 verbs) ---------------------------------------
424
+ /**
425
+ * Owner sends an invitation to `agentId` (or merges into a
426
+ * pending join_request from the same target).
427
+ *
428
+ * Two response shapes per ADR-0004 §"Invitation merge path":
429
+ *
430
+ * - **Normal path** (server returns 202): `{ invitation_id, status: 'pending' }`.
431
+ * - **Merge path** (server returns 200, request auto-approved):
432
+ * `{ auto_resolved: true, resolved_kind: 'join_request', request_id }`.
433
+ *
434
+ * Discriminate on `auto_resolved` to dispatch.
435
+ */
436
+ async subnetInvitationSend(slug, agentId, options) {
437
+ const body = { agent_id: agentId };
438
+ if (options?.note !== void 0) body.note = options.note;
439
+ return this.post(`/api/v1/subnets/${slug}/invitations`, body);
440
+ }
441
+ /**
442
+ * Invitee accepts a pending invitation (CAS pending → approved).
443
+ *
444
+ * Self-only against the row's `agent_id`. Side effects: invitee
445
+ * added to `subnet.member_agent_ids`, the agent's `subnet_ids`
446
+ * gains the back-reference, and `subnet.invitation_accepted`
447
+ * webhook fires.
448
+ */
449
+ async subnetInvitationAccept(slug, requestId, options) {
450
+ return this.post(
451
+ `/api/v1/subnets/${slug}/invitations/${requestId}/accept`,
452
+ options?.note !== void 0 ? { note: options.note } : void 0
453
+ );
454
+ }
455
+ /**
456
+ * Invitee rejects a pending invitation (CAS pending → rejected).
457
+ *
458
+ * No membership change. `subnet.invitation_rejected` webhook
459
+ * fires.
460
+ */
461
+ async subnetInvitationReject(slug, requestId, options) {
462
+ return this.post(
463
+ `/api/v1/subnets/${slug}/invitations/${requestId}/reject`,
464
+ options?.note !== void 0 ? { note: options.note } : void 0
465
+ );
466
+ }
467
+ /**
468
+ * Owner cancels a pending invitation (CAS pending → withdrawn).
469
+ *
470
+ * Owner-only counterpart to applicant withdraw. The row goes to
471
+ * `withdrawn` (not `rejected`) — distinct audit token so
472
+ * consumers can tell "owner gave up" from "invitee said no".
473
+ */
474
+ async subnetInvitationCancel(slug, requestId, options) {
475
+ return this.request(
476
+ "DELETE",
477
+ `/api/v1/subnets/${slug}/invitations/${requestId}`,
478
+ options?.note !== void 0 ? { body: { note: options.note } } : void 0
479
+ );
480
+ }
481
+ /**
482
+ * Owner lists invitation rows for `slug`.
483
+ *
484
+ * Owner-only — invitees use `agentSubnetInvitations` for their
485
+ * own cross-subnet view.
486
+ */
487
+ async subnetInvitationList(slug, options) {
488
+ const params = {
489
+ limit: options?.limit ?? 100,
490
+ offset: options?.offset ?? 0
491
+ };
492
+ if (options?.status !== void 0) params.status = options.status;
493
+ return this.get(`/api/v1/subnets/${slug}/invitations`, params);
494
+ }
495
+ /**
496
+ * Invitee's cross-subnet pending-invitation list (self only).
497
+ *
498
+ * Returns only `status='pending'` rows. Historical decisions
499
+ * are queryable per-subnet through the owner-only
500
+ * `subnetInvitationList`.
501
+ */
502
+ async agentSubnetInvitations(agentId) {
503
+ return this.get(`/api/v1/agents/${agentId}/subnet-invitations`);
504
+ }
505
+ // ============================================
278
506
  // Communication
279
507
  // ============================================
280
508
  /** Send message to an agent */
@@ -327,6 +555,31 @@ var ACNClient = class {
327
555
  if (options?.consume) params.ack = true;
328
556
  return this.get(`/api/v1/communication/history/${agentId}`, params);
329
557
  }
558
+ /**
559
+ * Precisely acknowledge (remove) specific messages from the inbox.
560
+ *
561
+ * Unlike `getMessageHistory({ consume: true })` which clears the entire inbox,
562
+ * this method removes only the messages whose `route_id` values are listed.
563
+ *
564
+ * @param agentId Must match the authenticated agent's ID.
565
+ * @param routeIds List of `route_id` values to remove (up to 500).
566
+ * @returns Number of messages actually removed.
567
+ */
568
+ async ackInbox(agentId, routeIds) {
569
+ return this.post(`/api/v1/communication/history/${agentId}/ack`, { route_ids: routeIds });
570
+ }
571
+ /**
572
+ * Update the lifecycle status of a specific inbox message.
573
+ *
574
+ * @param agentId Must match the authenticated agent's ID.
575
+ * @param routeId `route_id` of the target message (from inbox listing).
576
+ * @param status New status: `"unread"` | `"read"` | `"processed"`.
577
+ * @returns Object with `agent_id`, `route_id`, and `status`.
578
+ * @throws 404 (`inbox_message_not_found`) if route_id is absent from inbox.
579
+ */
580
+ async updateInboxMessageStatus(agentId, routeId, status) {
581
+ return this.patch(`/api/v1/communication/history/${agentId}/${routeId}`, { status });
582
+ }
330
583
  // ============================================
331
584
  // Manifest Queue (Phase 2/3)
332
585
  // ============================================
@@ -903,8 +1156,8 @@ var ACNClient = class {
903
1156
  * Register (or clear) an org-harness webhook URL for a subnet.
904
1157
  * Pass `harnessUrl: null` to deregister.
905
1158
  */
906
- async registerSubnetHarness(subnetId, harnessUrl, harnessSecret) {
907
- await this.request("PATCH", `/api/v1/subnets/${subnetId}/harness`, {
1159
+ async registerSubnetHarness(slug, harnessUrl, harnessSecret) {
1160
+ await this.request("PATCH", `/api/v1/subnets/${slug}/harness`, {
908
1161
  body: {
909
1162
  harness_url: harnessUrl,
910
1163
  harness_secret: harnessSecret ?? null
@@ -920,6 +1173,7 @@ var ACNError = class extends Error {
920
1173
  this.errorCode = options?.errorCode;
921
1174
  this.requestId = options?.requestId;
922
1175
  }
1176
+ status;
923
1177
  /** ACN internal error code (present on sanitised 5xx responses) */
924
1178
  errorCode;
925
1179
  /** Request ID minted by ACN for 5xx responses (useful for support) */
@@ -1138,11 +1392,17 @@ var KNOWN_PAYMENT_TASK_STATUSES = [
1138
1392
  "payment_failed",
1139
1393
  "refunded"
1140
1394
  ];
1395
+ var KNOWN_INBOX_MESSAGE_STATUSES = [
1396
+ "unread",
1397
+ "read",
1398
+ "processed"
1399
+ ];
1141
1400
  // Annotate the CommonJS export names for ESM import in node:
1142
1401
  0 && (module.exports = {
1143
1402
  ACNClient,
1144
1403
  ACNError,
1145
1404
  ACNRealtime,
1405
+ KNOWN_INBOX_MESSAGE_STATUSES,
1146
1406
  KNOWN_PAYMENT_TASK_STATUSES,
1147
1407
  subscribeToACN
1148
1408
  });