@pingroom/sdk 0.8.0 → 0.9.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/README.md +153 -41
- package/dist/client.d.ts +14 -348
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +15 -1300
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +32 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +47 -1
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +7 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/guards.d.ts +7 -0
- package/dist/internal/guards.d.ts.map +1 -1
- package/dist/internal/guards.js +1 -1
- package/dist/internal/guards.js.map +1 -1
- package/dist/internal/request.d.ts +41 -0
- package/dist/internal/request.d.ts.map +1 -0
- package/dist/internal/request.js +78 -0
- package/dist/internal/request.js.map +1 -0
- package/dist/internal/retry.d.ts +9 -2
- package/dist/internal/retry.d.ts.map +1 -1
- package/dist/internal/retry.js +24 -6
- package/dist/internal/retry.js.map +1 -1
- package/dist/internal/validate.d.ts +13 -0
- package/dist/internal/validate.d.ts.map +1 -0
- package/dist/internal/validate.js +36 -0
- package/dist/internal/validate.js.map +1 -0
- package/dist/liveStatus.d.ts +106 -11
- package/dist/liveStatus.d.ts.map +1 -1
- package/dist/liveStatus.js +20 -3
- package/dist/liveStatus.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +30 -8
- package/dist/mcp.js.map +1 -1
- package/dist/namespaces/actions.d.ts +50 -0
- package/dist/namespaces/actions.d.ts.map +1 -0
- package/dist/namespaces/actions.js +219 -0
- package/dist/namespaces/actions.js.map +1 -0
- package/dist/namespaces/approvals.d.ts +23 -0
- package/dist/namespaces/approvals.d.ts.map +1 -0
- package/dist/namespaces/approvals.js +78 -0
- package/dist/namespaces/approvals.js.map +1 -0
- package/dist/namespaces/attachments.d.ts +37 -0
- package/dist/namespaces/attachments.d.ts.map +1 -0
- package/dist/namespaces/attachments.js +91 -0
- package/dist/namespaces/attachments.js.map +1 -0
- package/dist/namespaces/auth.d.ts +58 -0
- package/dist/namespaces/auth.d.ts.map +1 -0
- package/dist/namespaces/auth.js +242 -0
- package/dist/namespaces/auth.js.map +1 -0
- package/dist/namespaces/directory.d.ts +10 -0
- package/dist/namespaces/directory.d.ts.map +1 -0
- package/dist/namespaces/directory.js +19 -0
- package/dist/namespaces/directory.js.map +1 -0
- package/dist/namespaces/handoffs.d.ts +47 -0
- package/dist/namespaces/handoffs.d.ts.map +1 -0
- package/dist/namespaces/handoffs.js +126 -0
- package/dist/namespaces/handoffs.js.map +1 -0
- package/dist/namespaces/inbox.d.ts +17 -0
- package/dist/namespaces/inbox.d.ts.map +1 -0
- package/dist/namespaces/inbox.js +192 -0
- package/dist/namespaces/inbox.js.map +1 -0
- package/dist/namespaces/live.d.ts +57 -0
- package/dist/namespaces/live.d.ts.map +1 -0
- package/dist/namespaces/live.js +116 -0
- package/dist/namespaces/live.js.map +1 -0
- package/dist/namespaces/notifications.d.ts +25 -0
- package/dist/namespaces/notifications.d.ts.map +1 -0
- package/dist/namespaces/notifications.js +128 -0
- package/dist/namespaces/notifications.js.map +1 -0
- package/dist/namespaces/profile.d.ts +19 -0
- package/dist/namespaces/profile.d.ts.map +1 -0
- package/dist/namespaces/profile.js +22 -0
- package/dist/namespaces/profile.js.map +1 -0
- package/dist/namespaces/questions.d.ts +41 -0
- package/dist/namespaces/questions.d.ts.map +1 -0
- package/dist/namespaces/questions.js +107 -0
- package/dist/namespaces/questions.js.map +1 -0
- package/dist/namespaces/rooms.d.ts +13 -0
- package/dist/namespaces/rooms.d.ts.map +1 -0
- package/dist/namespaces/rooms.js +41 -0
- package/dist/namespaces/rooms.js.map +1 -0
- package/dist/namespaces/webhooks.d.ts +11 -0
- package/dist/namespaces/webhooks.d.ts.map +1 -0
- package/dist/namespaces/webhooks.js +32 -0
- package/dist/namespaces/webhooks.js.map +1 -0
- package/dist/types/approvals.d.ts +62 -0
- package/dist/types/approvals.d.ts.map +1 -0
- package/dist/types/approvals.js +2 -0
- package/dist/types/approvals.js.map +1 -0
- package/dist/types/auth.d.ts +185 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/common.d.ts +43 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +2 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/handoffs.d.ts +121 -0
- package/dist/types/handoffs.d.ts.map +1 -0
- package/dist/types/handoffs.js +2 -0
- package/dist/types/handoffs.js.map +1 -0
- package/dist/types/inbox.d.ts +51 -0
- package/dist/types/inbox.d.ts.map +1 -0
- package/dist/types/inbox.js +2 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/notifications.d.ts +376 -0
- package/dist/types/notifications.d.ts.map +1 -0
- package/dist/types/notifications.js +2 -0
- package/dist/types/notifications.js.map +1 -0
- package/dist/types/profile.d.ts +30 -0
- package/dist/types/profile.d.ts.map +1 -0
- package/dist/types/profile.js +3 -0
- package/dist/types/profile.js.map +1 -0
- package/dist/types/questions.d.ts +210 -0
- package/dist/types/questions.d.ts.map +1 -0
- package/dist/types/questions.js +2 -0
- package/dist/types/questions.js.map +1 -0
- package/dist/types/rooms.d.ts +163 -0
- package/dist/types/rooms.d.ts.map +1 -0
- package/dist/types/rooms.js +3 -0
- package/dist/types/rooms.js.map +1 -0
- package/dist/types.d.ts +9 -1199
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +14 -4
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +30 -5
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -2
- package/src/client.ts +115 -0
- package/src/errors.ts +291 -0
- package/src/http.ts +225 -0
- package/src/index.ts +97 -0
- package/src/internal/async.ts +54 -0
- package/src/internal/guards.ts +129 -0
- package/src/internal/request.ts +97 -0
- package/src/internal/retry.ts +157 -0
- package/src/internal/url.ts +78 -0
- package/src/internal/validate.ts +42 -0
- package/src/linkPing.ts +56 -0
- package/src/liveStatus.ts +337 -0
- package/src/locationPing.ts +101 -0
- package/src/mcp.ts +161 -0
- package/src/namespaces/actions.ts +234 -0
- package/src/namespaces/approvals.ts +104 -0
- package/src/namespaces/attachments.ts +106 -0
- package/src/namespaces/auth.ts +284 -0
- package/src/namespaces/directory.ts +21 -0
- package/src/namespaces/handoffs.ts +160 -0
- package/src/namespaces/inbox.ts +253 -0
- package/src/namespaces/live.ts +134 -0
- package/src/namespaces/notifications.ts +152 -0
- package/src/namespaces/profile.ts +24 -0
- package/src/namespaces/questions.ts +133 -0
- package/src/namespaces/rooms.ts +53 -0
- package/src/namespaces/webhooks.ts +38 -0
- package/src/types/approvals.ts +67 -0
- package/src/types/auth.ts +202 -0
- package/src/types/common.ts +74 -0
- package/src/types/handoffs.ts +148 -0
- package/src/types/inbox.ts +64 -0
- package/src/types/notifications.ts +393 -0
- package/src/types/profile.ts +37 -0
- package/src/types/questions.ts +226 -0
- package/src/types/rooms.ts +169 -0
- package/src/types.ts +17 -0
- package/src/version.ts +2 -0
- package/src/webhooks.ts +373 -0
- package/dist/liveActivity.d.ts +0 -101
- package/dist/liveActivity.d.ts.map +0 -1
- package/dist/liveActivity.js +0 -83
- package/dist/liveActivity.js.map +0 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ One typed client for everything an agent does: authenticate, create and join roo
|
|
|
12
12
|
npm install @pingroom/sdk
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
The package is ESM-only. `import` it, or `await import('@pingroom/sdk')` from CommonJS. `require('@pingroom/sdk')` also works on Node ≥ 20.19 and ≥ 22.12, which can require an ES module; on an older Node 20.x it fails with `ERR_REQUIRE_ESM`.
|
|
16
|
+
|
|
15
17
|
## Quick start
|
|
16
18
|
|
|
17
19
|
```ts
|
|
@@ -53,7 +55,11 @@ the first result instead of pinging the human twice. The SDK sends it as the
|
|
|
53
55
|
`Idempotency-Key` header, never in the body. A key is 1–255 printable ASCII
|
|
54
56
|
characters, spaces included, and surrounding whitespace is trimmed as the
|
|
55
57
|
server does. Anything else, such as an emoji or Cyrillic, is refused before
|
|
56
|
-
sending with `invalid_request`, because it cannot travel as a header.
|
|
58
|
+
sending with `invalid_request`, because it cannot travel as a header. An empty
|
|
59
|
+
key (`''`) is `invalid_request` on `broadcast()`, `actions.trigger()`,
|
|
60
|
+
`approvals.request()` and `questions.ask()`; on `handoffs.*` and
|
|
61
|
+
`sendIncomingWebhook()` it is kept as it always was, meaning "no key", so pass
|
|
62
|
+
`undefined` rather than an empty string when you have none.
|
|
57
63
|
|
|
58
64
|
```ts
|
|
59
65
|
await pr.broadcast('ab12cd', { message: 'Deploy shipped ✅', idempotencyKey: 'deploy-1.4.0-shipped' });
|
|
@@ -66,11 +72,6 @@ incoming-webhook URLs do not expose visibility, so the SDK validates the
|
|
|
66
72
|
limit. Question and Handoff prompts, Live Status fields, and structured `data`
|
|
67
73
|
use separate contracts.
|
|
68
74
|
|
|
69
|
-
> **`pr.agents.ping()` is retired.** Handle-addressed pings across accounts
|
|
70
|
-
> always fail with `410 cross_account_ping_retired`. An agent reaches only the
|
|
71
|
-
> account that connected it — to reach another agent or person, share a room
|
|
72
|
-
> (invite them, or join one they post in) and `pr.broadcast()` into it.
|
|
73
|
-
|
|
74
75
|
### Link pings
|
|
75
76
|
|
|
76
77
|
A ping becomes a tappable link button when its `data` carries the reserved
|
|
@@ -113,7 +114,9 @@ Latitude must be within -90..90 and longitude within -180..180. `label` is
|
|
|
113
114
|
limited to 100 Unicode characters and `address` to 255. Recipients can share
|
|
114
115
|
the point or open it in an installed maps app. When reading pings,
|
|
115
116
|
`extractLocationPing(ping.data)` returns the validated location or `null` for
|
|
116
|
-
legacy or malformed data.
|
|
117
|
+
legacy or malformed data. (This is a point carried in a ping's `data`, not the
|
|
118
|
+
geofence source `trigger_source: 'location'` described under
|
|
119
|
+
[Rooms & quick actions](#rooms--quick-actions).)
|
|
117
120
|
|
|
118
121
|
> Security note: an agent credential is a bearer token. Keep it server-side. Do not embed a long-lived token in a browser bundle or a public client.
|
|
119
122
|
|
|
@@ -183,6 +186,11 @@ principal; it does not impersonate the owner's personal PingRoom profile.
|
|
|
183
186
|
|
|
184
187
|
`waitForPairing()` tolerates short network outages and stops when the app link
|
|
185
188
|
expires. Pass an `AbortSignal` when your process needs its own cancellation.
|
|
189
|
+
If the client already held a credential when you called `startPairing()`, that
|
|
190
|
+
credential is put back when the link expires or the wait is aborted, so a
|
|
191
|
+
re-pairing that never completes does not leave the client on the dead pending
|
|
192
|
+
credential. Calling `waitForPairing()` again after an abort resumes the same
|
|
193
|
+
pairing.
|
|
186
194
|
Pairing grants are server-authoritative. `startPairing()` sends no scope array,
|
|
187
195
|
so an older installed SDK cannot accidentally mint a partial credential when
|
|
188
196
|
the service adds a capability. The deprecated `scopes` option is accepted for
|
|
@@ -248,10 +256,8 @@ await pr.actions.updateMany('ab12cd', [
|
|
|
248
256
|
{ action_number: 2, label: 'Reject', icon: '❌' },
|
|
249
257
|
]);
|
|
250
258
|
|
|
251
|
-
// One-shot modifiers on a single press.
|
|
252
|
-
// or `location`; `webhook`/`system` are stamped server-side and rejected here.
|
|
259
|
+
// One-shot modifiers on a single press.
|
|
253
260
|
await pr.actions.trigger('ab12cd', 1, {
|
|
254
|
-
trigger_source: 'location',
|
|
255
261
|
is_urgent: true,
|
|
256
262
|
requires_ack: true,
|
|
257
263
|
});
|
|
@@ -262,6 +268,31 @@ only **add** an acknowledgement requirement for that one press — passing `fals
|
|
|
262
268
|
never removes one from an action already configured to require it. Neither
|
|
263
269
|
modifier edits the action's saved configuration.
|
|
264
270
|
|
|
271
|
+
A `requires_ack` result from `broadcast()` or `trigger()` carries
|
|
272
|
+
`confirmation_reach`. In a private room it names who has no device that can show
|
|
273
|
+
Confirm: they get a plain alert, so they may never confirm. `{ checked: false }`
|
|
274
|
+
means nothing was checked (public rooms, rooms over 100 recipients, a failed
|
|
275
|
+
lookup), not that everyone is reachable.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const sent = await pr.broadcast(code, { message: 'Confirm', requires_ack: true });
|
|
279
|
+
if (sent.confirmation_reach?.checked) {
|
|
280
|
+
console.log(sent.confirmation_reach.unavailable_user_ids);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`trigger_source` is `manual` (the default) or `location`; `webhook`/`system` are
|
|
285
|
+
stamped server-side and rejected here. **Leave it unset to press a Quick Ping.**
|
|
286
|
+
`location` is the device-geofence source, not a press modifier: it is for the
|
|
287
|
+
**room owner only** and sends the owner's Location Ping (an "Arrived at
|
|
288
|
+
{place}" or "Left {place}" Ping to every member, with no action number)
|
|
289
|
+
**instead of** the quick action in that slot, so the slot's label, sound and
|
|
290
|
+
acknowledgement policy are not used. A caller who is not the room owner gets
|
|
291
|
+
`403 location_trigger_owner_only`; a room with no enabled location trigger (or
|
|
292
|
+
none that listens for the reported crossing) gets `409 location_trigger_inactive`;
|
|
293
|
+
the operator's Location Pings switch being off gets `403 feature_disabled`. In
|
|
294
|
+
none of those cases does the slot's quick action fire.
|
|
295
|
+
|
|
265
296
|
### Quick actions that need a detail
|
|
266
297
|
|
|
267
298
|
An action can require a detail from whoever presses it: `input_type` is `none`
|
|
@@ -272,7 +303,8 @@ refused with `422 quick_action_input_required` and nothing is sent.
|
|
|
272
303
|
```ts
|
|
273
304
|
await pr.actions.update('ab12cd', 2, { label: 'Where are you?', icon: '📍', input_type: 'location' });
|
|
274
305
|
|
|
275
|
-
const
|
|
306
|
+
const action = (await pr.actions.list('ab12cd')).find((a) => a.action_number === 2);
|
|
307
|
+
if (!action) throw new Error('Slot 2 is not configured.'); // action.input_type === 'location'
|
|
276
308
|
await pr.actions.trigger('ab12cd', 2, {
|
|
277
309
|
quick_action_id: action.id, // refused with 409 quick_action_layout_changed if the slot moved pages
|
|
278
310
|
data: { location: { latitude: 25.2048, longitude: 55.2708, label: 'Dubai Mall' } },
|
|
@@ -339,9 +371,10 @@ Updating with `regenerate_secret: true` returns a new URL and revokes the old on
|
|
|
339
371
|
## Attachments
|
|
340
372
|
|
|
341
373
|
Send the file itself, not a link to it. Upload first, then pass the ids to any
|
|
342
|
-
send. Bytes never travel
|
|
374
|
+
send. Bytes never travel inside a JSON ping body, and the returned metadata
|
|
343
375
|
never carries a permanent URL — recipients read the content back through an
|
|
344
|
-
authenticated endpoint.
|
|
376
|
+
authenticated endpoint. (The MCP `upload_attachment` tool takes small files as
|
|
377
|
+
base64, up to about 90 KiB; this method has no such limit below 5 MiB.)
|
|
345
378
|
|
|
346
379
|
```ts
|
|
347
380
|
const report = await pr.attachments.upload({
|
|
@@ -419,10 +452,19 @@ for await (const ping of pr.notifications.listen({ signal: ac.signal })) {
|
|
|
419
452
|
```
|
|
420
453
|
|
|
421
454
|
`listen()` retries transient failures (network errors, request timeouts, 429
|
|
422
|
-
and 5xx) with a backoff
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
455
|
+
and 5xx) with a backoff based on `backoffMs` (default 1 s, minimum 250
|
|
456
|
+
ms: a smaller value is raised to 250 ms), doubles up to 30 s, and waits out a
|
|
457
|
+
server `retryAfter`. Pass `onError` to observe those retries. Any other failure,
|
|
458
|
+
such as a 401 after the credential is revoked or a 403, ends the iterator by
|
|
459
|
+
throwing, with or without `onError`. A 2xx that is not a wait answer (a captive
|
|
460
|
+
portal's page, say) ends it with `invalid_response`.
|
|
461
|
+
|
|
462
|
+
Retries and polls following an empty forward poll start at least 2.1 seconds
|
|
463
|
+
after the previous poll started, even when `backoffMs` is smaller. This paces
|
|
464
|
+
one loop's empty-response and retry traffic. The initial head read and nonempty
|
|
465
|
+
batch draining remain immediate, and all listeners (the CLI, other agents)
|
|
466
|
+
share the human's 30-requests-per-minute limit on that route. The default 20-second
|
|
467
|
+
hold is unaffected.
|
|
426
468
|
|
|
427
469
|
Or a single long-poll. Without `after`, `wait()` answers at once with no pings
|
|
428
470
|
and the current head cursor, so pass the cursor from a previous call:
|
|
@@ -482,10 +524,11 @@ approving `decision`, never on `decision !== 'Hold'`.
|
|
|
482
524
|
return only a response for the id you asked about, in a known state. Anything
|
|
483
525
|
else, such as an HTML page from a proxy, `{}`, or another approval, rejects with
|
|
484
526
|
`invalid_response` instead of being returned as an outcome. The wait routes
|
|
485
|
-
|
|
527
|
+
each have their own 30-requests-per-minute limit per human, shared by every waiter on that route, so these loops retry a 429
|
|
486
528
|
after its `retryAfter`, and retry network errors and 5xx with a backoff capped
|
|
487
|
-
at 30 s. Approval and
|
|
488
|
-
|
|
529
|
+
at 30 s. Approval, question and handoff polls start at least 2.1 s apart (one
|
|
530
|
+
loop stays under the limit), even if something in between answers `pending` at
|
|
531
|
+
once.
|
|
489
532
|
|
|
490
533
|
The retries are bounded without a signal. Once only failures have come back
|
|
491
534
|
since the last valid response, a wait gives up 5 minutes after the last
|
|
@@ -533,8 +576,8 @@ const answered = await pr.questions.waitForAnswer(q.id);
|
|
|
533
576
|
|
|
534
577
|
switch (answered.state) {
|
|
535
578
|
case 'answered':
|
|
536
|
-
console.log(`${answered.answer
|
|
537
|
-
// → answered.answer
|
|
579
|
+
console.log(`${answered.answer?.responder?.display_name} chose ${answered.answer?.value}`);
|
|
580
|
+
// → answered.answer?.value is 'prod' | 'staging' | 'cancel'
|
|
538
581
|
break;
|
|
539
582
|
case 'expired': /* nobody answered in time */ break;
|
|
540
583
|
case 'cancelled': /* the asker withdrew it */ break;
|
|
@@ -632,7 +675,9 @@ callback cannot rewrite the required receipt-before-answer sequence. It throws
|
|
|
632
675
|
`answered_without_completion`. Expiry and cancellation throw the same domain
|
|
633
676
|
error with their corresponding reasons. Caller aborts reject with the signal's
|
|
634
677
|
reason. None of these paths revoke or change the client's active credential.
|
|
635
|
-
|
|
678
|
+
A transient wait failure (a dropped connection, a request timeout, a 429 or a
|
|
679
|
+
5xx) is retried with backoff until `overallTimeoutMs`; any other API error
|
|
680
|
+
remains a `PingRoomError`. Use
|
|
636
681
|
`pr.inbox.ensure()` directly when you only want to create or inspect the
|
|
637
682
|
onboarding Question without waiting. Call `activate()` again after an expired,
|
|
638
683
|
cancelled, or `answered_without_completion` result; the server replays a viable
|
|
@@ -687,7 +732,10 @@ A room's incoming-webhook URL carries its own secret — ideal for CI/deploy hoo
|
|
|
687
732
|
```ts
|
|
688
733
|
import { sendIncomingWebhook } from '@pingroom/sdk';
|
|
689
734
|
|
|
690
|
-
|
|
735
|
+
const webhookUrl = process.env.PINGROOM_WEBHOOK_URL;
|
|
736
|
+
if (!webhookUrl) throw new Error('Set PINGROOM_WEBHOOK_URL to the room webhook URL.');
|
|
737
|
+
|
|
738
|
+
await sendIncomingWebhook(webhookUrl, {
|
|
691
739
|
message: 'Build #42 passed',
|
|
692
740
|
data: { commit: 'abc123' },
|
|
693
741
|
requires_ack: true,
|
|
@@ -700,7 +748,10 @@ await sendIncomingWebhook(process.env.PINGROOM_WEBHOOK_URL, {
|
|
|
700
748
|
See [Live Activities](#live-activities) below for the full picture. From a webhook, put `live_status` at the **top level** of the payload:
|
|
701
749
|
|
|
702
750
|
```ts
|
|
703
|
-
|
|
751
|
+
const webhookUrl = process.env.PINGROOM_WEBHOOK_URL;
|
|
752
|
+
if (!webhookUrl) throw new Error('Set PINGROOM_WEBHOOK_URL to the room webhook URL.');
|
|
753
|
+
|
|
754
|
+
await sendIncomingWebhook(webhookUrl, {
|
|
704
755
|
message: 'Deploying v1.4.0',
|
|
705
756
|
correlation_id: 'deploy-42',
|
|
706
757
|
live_status: { state: 'running', template: 'progress', progress: 0.4 },
|
|
@@ -713,7 +764,7 @@ await sendIncomingWebhook(process.env.PINGROOM_WEBHOOK_URL, {
|
|
|
713
764
|
|
|
714
765
|
A **live-status stream** is one thing being tracked, keyed by your `correlation_id`. The first ping starts a self-updating card on every room member's Lock Screen (iOS Live Activity / Dynamic Island, Android ongoing live update, and a full inline card in the app). Later `running` pings move it **silently** — no new notification. The first `done`/`failed` sends one completion alert and ends it.
|
|
715
766
|
|
|
716
|
-
Protocol: <https://pingroom.io/liveactivities.md> (v1.
|
|
767
|
+
Protocol: <https://pingroom.io/liveactivities.md> (v1.4).
|
|
717
768
|
|
|
718
769
|
### Two ways to send one
|
|
719
770
|
|
|
@@ -730,8 +781,10 @@ await pr.live.push('AB12CD', liveStatus.progress('deploy-42', {
|
|
|
730
781
|
|
|
731
782
|
// B. A room's incoming webhook (Pro) — no token. The builder returns
|
|
732
783
|
// { correlation_id, live_status }, which is exactly the webhook body.
|
|
784
|
+
const webhookUrl = process.env.PINGROOM_WEBHOOK_URL;
|
|
785
|
+
if (!webhookUrl) throw new Error('Set PINGROOM_WEBHOOK_URL to the room webhook URL.');
|
|
733
786
|
await sendIncomingWebhook(
|
|
734
|
-
|
|
787
|
+
webhookUrl,
|
|
735
788
|
liveStatus.progress('deploy-42', {
|
|
736
789
|
state: 'running',
|
|
737
790
|
message: 'Building image',
|
|
@@ -749,12 +802,13 @@ There is a third producer — a person driving a stream from the app composer ov
|
|
|
749
802
|
| Builder | Renders |
|
|
750
803
|
|---|---|
|
|
751
804
|
| `liveStatus.status` | Title, message, optional progress |
|
|
752
|
-
| `liveStatus.steps` | Segmented tracker; `steps` (2–8 labels) required on the first ping and immutable after |
|
|
805
|
+
| `liveStatus.steps` | Segmented tracker; `steps` (2–8 labels) required on the first ping and immutable after: later pings resend the identical list or omit it, and a different list on a `running` ping throws `PingRoomError` `steps_immutable` (422); a `done`/`failed` ping ignores it and still ends the stream |
|
|
753
806
|
| `liveStatus.progress` | Determinate bar with optional `eta_at` |
|
|
754
807
|
| `liveStatus.metrics` | Up to 3 `{label, value}` counters |
|
|
755
|
-
| `liveStatus.countdown` | Large live timer to `deadline_at` |
|
|
808
|
+
| `liveStatus.countdown` | Large live timer to `deadline_at`. A later ping may move the deadline (send the absolute epoch); `duration_seconds` is read on the first ping only and moves by the same amount as the deadline |
|
|
756
809
|
| `liveStatus.question` | `prompt` + up to 4 `{value, label}` options on the lock screen |
|
|
757
810
|
| `liveStatus.matchup` | `left` / `center` / `right` — a score or A-vs-B |
|
|
811
|
+
| `liveStatus.claim` | "Who's on it?": a request one room member takes on. See [Who's on it?](#whos-on-it) |
|
|
758
812
|
|
|
759
813
|
```ts
|
|
760
814
|
await pr.live.push('AB12CD', liveStatus.steps('release-7', {
|
|
@@ -772,6 +826,58 @@ await pr.live.push('AB12CD', liveStatus.matchup('game-3', {
|
|
|
772
826
|
}));
|
|
773
827
|
```
|
|
774
828
|
|
|
829
|
+
### Who's on it?
|
|
830
|
+
|
|
831
|
+
A **claim request** asks the room "who's on it?" and lets one member take it. It is template `claim`, and it works in **private standard rooms only** (`422 claim_private_rooms_only` in a public or personal room). Pass the request text as `title` (1 to 120 characters, or `live_status.message` without a title). An optional `deadline_at` must be in the future and at most 8 hours away. The first ping must be `running`. The server writes the claim itself and ignores any `claim` block you send.
|
|
832
|
+
|
|
833
|
+
```ts
|
|
834
|
+
// The asking credential acts for the room owner.
|
|
835
|
+
// The claiming credential must act for a different human member of that room.
|
|
836
|
+
const asker = new PingRoom({ token: process.env.ASKER_AGENT_TOKEN });
|
|
837
|
+
const member = new PingRoom({ token: process.env.MEMBER_AGENT_TOKEN });
|
|
838
|
+
const { notification_id } = await asker.live.push('AB12CD', liveStatus.claim(
|
|
839
|
+
'milk-1',
|
|
840
|
+
{ state: 'running', deadline_at: Math.floor(Date.now() / 1000) + 3600 },
|
|
841
|
+
{ title: 'Buy milk' },
|
|
842
|
+
));
|
|
843
|
+
|
|
844
|
+
// The claiming credential acts for another member, who is not the asker.
|
|
845
|
+
const taken = await member.live.claim('AB12CD', notification_id, 'claim');
|
|
846
|
+
console.log(taken.claim.status, taken.claim.by?.name, taken.idempotent);
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
`pr.live.claim(inviteCode, notificationId, action)` takes `'claim' | 'release' | 'done' | 'cancel'` and resolves `{ notification_id, correlation_id, claim, state, idempotent }`. It needs the `pingroom:live:write` scope.
|
|
850
|
+
|
|
851
|
+
| Action | Who | From | To |
|
|
852
|
+
|---|---|---|---|
|
|
853
|
+
| `claim` | any member but the asker | open | claimed |
|
|
854
|
+
| `release` | the claimer | claimed | open, or expired past the deadline |
|
|
855
|
+
| `done` | the claimer, or the asker at any time | open (asker only) or claimed | done |
|
|
856
|
+
| `cancel` | the asker, or a room owner or admin | open or claimed | cancelled |
|
|
857
|
+
|
|
858
|
+
`release` returns the request to `open` only while it can still be taken. Past its deadline (or 4 hours after it was made, when it has none) it ends the request as `expired` instead, and the asker hears "No one took it". That is a 200, not an error: `claim.status` is `'expired'`, `state` is `'failed'`, and `claim.by` and `claim.at` are `null`. A retry resolves the same way with `idempotent: true`.
|
|
859
|
+
|
|
860
|
+
The asker is the stream's sender, or the room owner for an agent or webhook stream. Two claims at the same moment have one winner. The other throws `PingRoomError` `claim_conflict` (409), whose `body.claim` names who holds it. Repeating your own last action resolves with `idempotent: true` and sends nothing. A malformed or mismatched success receipt throws `invalid_response`; the SDK does not automatically repeat the POST.
|
|
861
|
+
|
|
862
|
+
```ts
|
|
863
|
+
import { PingRoomError } from '@pingroom/sdk';
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
await member.live.claim('AB12CD', notification_id, 'claim');
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (error instanceof PingRoomError && error.code === 'claim_conflict') {
|
|
869
|
+
const { claim } = error.body as { claim: { by?: { name: string } } };
|
|
870
|
+
console.log(`${claim.by?.name} got it first`);
|
|
871
|
+
} else throw error;
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Refusals: `claim_conflict` (409), `claim_own_request` (422), `claim_private_rooms_only` (422), `claim_not_found` (404) and `forbidden` (403). Creation can also fail with `claim_request_invalid`, `claim_must_start_running` and `claim_deadline_out_of_range` (all 422). `LIVE_CLAIM_ERROR_CODES` lists the claim codes.
|
|
876
|
+
|
|
877
|
+
A webhook has no member identity, so it cannot claim or release. It ends its own request with `liveStatus.done()` (done) or `liveStatus.failed()` (cancelled). An ordinary `running` leg on an existing request changes nothing.
|
|
878
|
+
|
|
879
|
+
A request ends by itself. An open one expires at its deadline, or after 4 hours without one. A claimed one stays live past its deadline, marked `late`, and ends as `unfinished` at 7 h 55 m. `pr.live.get()` returns `title` and `claim` for a claim request. Claim support is newer than SDK 0.8.0.
|
|
880
|
+
|
|
775
881
|
### Ending and reading back
|
|
776
882
|
|
|
777
883
|
```ts
|
|
@@ -789,16 +895,12 @@ const snap = await pr.live.get('AB12CD', 'deploy-42');
|
|
|
789
895
|
if (snap) console.log(snap.template, snap.current_step, snap.updated_at);
|
|
790
896
|
```
|
|
791
897
|
|
|
792
|
-
`get()` returns **every** stored field (`state`, `progress`, `message`, `category`, `template`, `agent_id`, `accent_override`, `eta_at`, `deadline_at`, `duration_seconds`, `metrics`, `prompt`, `options`, `left`, `right`, `center`, `steps`, `current_step`) plus `action_state` and `updated_at`. Fields you never set come back as `null`, not omitted.
|
|
898
|
+
`get()` returns **every** stored field (`state`, `progress`, `message`, `category`, `template`, `agent_id`, `accent_override`, `eta_at`, `deadline_at`, `duration_seconds`, `metrics`, `prompt`, `options`, `left`, `right`, `center`, `steps`, `current_step`) plus `action_state` and `updated_at`. Fields you never set come back as `null`, not omitted. A claim request adds `title` and `claim`.
|
|
793
899
|
|
|
794
900
|
### Ownership and limits
|
|
795
901
|
|
|
796
902
|
A stream belongs to the credential that started it. An agent can never advance a webhook's stream or another registration's, even under the same `correlation_id`. Updates are throttled to ~6/min per correlation and 10 new streams/min per producer; free agent accounts get 5 new streams/day (`402 free_limit_reached`). Creation is the only charged leg.
|
|
797
903
|
|
|
798
|
-
### `liveActivity.*` is not this
|
|
799
|
-
|
|
800
|
-
The exported `liveActivity.*` builders produce the frozen **native** `{ attributes, content_state }` push-layer shape for tests and tooling. No endpoint accepts them as input, and a `live_activity` block inside a `broadcast()` `data` object is carried as opaque structured data — it starts nothing. Use `liveStatus.*` to drive a real activity.
|
|
801
|
-
|
|
802
904
|
## Verifying outgoing webhooks
|
|
803
905
|
|
|
804
906
|
When PingRoom POSTs an event to *your* server, verify it before trusting it. New deliveries carry two lower-case hex HMAC-SHA256 headers:
|
|
@@ -822,10 +924,12 @@ import {
|
|
|
822
924
|
const signingSecret = process.env.PINGROOM_SIGNING_SECRET;
|
|
823
925
|
if (!signingSecret) throw new Error('Set PINGROOM_SIGNING_SECRET to the webhook signing_secret.');
|
|
824
926
|
|
|
825
|
-
// Express example
|
|
826
|
-
|
|
927
|
+
// Express example. PingRoom sends `Content-Type: application/json`, which
|
|
928
|
+
// express.raw() ignores unless you name the type, so pass it on this route and
|
|
929
|
+
// keep it ahead of any express.json().
|
|
930
|
+
app.post('/pingroom', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
827
931
|
const ok = await verifyWebhookSignature({
|
|
828
|
-
payload: req.body, // a Buffer
|
|
932
|
+
payload: req.body, // a Buffer of the RAW body
|
|
829
933
|
signature: req.get(WEBHOOK_SIGNATURE_HEADER),
|
|
830
934
|
signatureV2: req.get(WEBHOOK_SIGNATURE_V2_HEADER),
|
|
831
935
|
timestamp: req.get(WEBHOOK_TIMESTAMP_HEADER),
|
|
@@ -839,6 +943,13 @@ app.post('/pingroom', async (req, res) => {
|
|
|
839
943
|
});
|
|
840
944
|
```
|
|
841
945
|
|
|
946
|
+
`verifyWebhookSignature` returns `false` for a signature that does not match, and
|
|
947
|
+
**throws a `TypeError`** when `payload` is not a string, `ArrayBuffer` or
|
|
948
|
+
`Buffer`/typed array. That is what a body parser that did not capture the raw
|
|
949
|
+
bytes leaves behind (`{}` on Express 4, `undefined` on Express 5, a parsed
|
|
950
|
+
object after `express.json()`), and it is a bug in the receiver, not a forged
|
|
951
|
+
delivery, so it is not reported as `false`.
|
|
952
|
+
|
|
842
953
|
## Redeem a gift or promotional code
|
|
843
954
|
|
|
844
955
|
```ts
|
|
@@ -905,7 +1016,7 @@ try {
|
|
|
905
1016
|
}
|
|
906
1017
|
```
|
|
907
1018
|
|
|
908
|
-
The SDK also raises a few client-side codes: `network_error` and `timeout` (status 0), `redirect_refused` when the API answers with a redirect (the SDK never follows one, so a token, upload or webhook body cannot be forwarded to another origin), `invalid_response` when a wait loop receives something that is not the requested approval, Question or handoff, `unavailable` when a wait loop stops retrying an unreachable API (see [approvals](#human-in-the-loop-approvals); the last failure is `error.cause`), and `invalid_request` for input refused before sending, including an `idempotencyKey` that is not 1–255 printable ASCII characters.
|
|
1019
|
+
The SDK also raises a few client-side codes: `network_error` and `timeout` (status 0), `redirect_refused` when the API answers with a redirect (the SDK never follows one, so a token, upload or webhook body cannot be forwarded to another origin), `invalid_response` when a wait loop receives something that is not the requested approval, Question or handoff, when any call gets a 2xx whose body is not JSON (a proxy's page, say), or when `sendIncomingWebhook()` gets a 2xx that is not a JSON object with `success: true` (a misrouted webhook URL), `unavailable` when a wait loop stops retrying an unreachable API (see [approvals](#human-in-the-loop-approvals); the last failure is `error.cause`), and `invalid_request` for input refused before sending, including an `idempotencyKey` that is not 1–255 printable ASCII characters.
|
|
909
1020
|
|
|
910
1021
|
The coded vocabulary is exported both as types and as runtime arrays, one per
|
|
911
1022
|
surface, so you can branch exhaustively:
|
|
@@ -915,9 +1026,10 @@ surface, so you can branch exhaustively:
|
|
|
915
1026
|
| `HandoffErrorCode` / `HANDOFF_ERROR_CODES` | `handoffs.*` |
|
|
916
1027
|
| `RoomScopedErrorCode` / `ROOM_SCOPED_ERROR_CODES` | every call whose path names a room (`rooms.get`, `actions.*`, `broadcast`, `webhooks.*`, `live.*`) |
|
|
917
1028
|
| `AgentInboxErrorCode` / `AGENT_INBOX_ERROR_CODES` | `inbox.ensure()` / `inbox.activate()` |
|
|
918
|
-
| `QuickActionErrorCode` / `QUICK_ACTION_ERROR_CODES` | `actions.*` and a `broadcast()` naming an `action_number`: `quick_action_input_required`, `quick_action_input_type`, `action_not_configured`, `pro_required`, `quick_action_layout_changed`, `quick_action_page_in_use` |
|
|
1029
|
+
| `QuickActionErrorCode` / `QUICK_ACTION_ERROR_CODES` | `actions.*` and a `broadcast()` naming an `action_number`: `quick_action_input_required`, `quick_action_input_type`, `action_not_configured`, `pro_required`, `quick_action_layout_changed`, `quick_action_page_in_use`; and, on `actions.trigger()` with `trigger_source: 'location'` only, `location_trigger_owner_only` (403) and `location_trigger_inactive` (409) |
|
|
1030
|
+
| `CommonErrorCode` / `COMMON_ERROR_CODES` | any call: `rate_limited` (429, a route throttle; carries `retryAfter`) and `feature_disabled` (403, the operator switched the capability off; `error.body.feature` names the switch, for example `sending.attachments`, and retrying cannot succeed) |
|
|
919
1031
|
| `RequestLimitErrorCode` / `REQUEST_LIMIT_ERROR_CODES` | `payload_too_large` (HTTP 413 from the server on any oversized body) and `attachment_too_large` (raised locally by `attachments.upload()` above 5 MiB, before any bytes travel) |
|
|
920
|
-
| `AgentErrorCode` | all
|
|
1032
|
+
| `AgentErrorCode` | all six, for one shared branch |
|
|
921
1033
|
|
|
922
1034
|
## Configuration
|
|
923
1035
|
|
|
@@ -925,7 +1037,7 @@ surface, so you can branch exhaustively:
|
|
|
925
1037
|
new PingRoom({
|
|
926
1038
|
token: '...', // agent credential (optional for public reads)
|
|
927
1039
|
baseUrl: 'https://api.pingroom.io', // or env PINGROOM_API_URL / PINGROOM_BASE_URL
|
|
928
|
-
timeoutMs: 30000, // long-poll
|
|
1040
|
+
timeoutMs: 30000, // per-request deadline; a long-poll call (wait, listen, waitFor*) uses its own hold time + 10 s instead
|
|
929
1041
|
fetch: customFetch, // inject a fetch (tests / non-global-fetch runtimes)
|
|
930
1042
|
allowInsecure: false, // allow plain-http base URLs (off by default)
|
|
931
1043
|
});
|