@pingroom/sdk 0.5.1 → 0.7.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 +76 -11
- package/dist/client.d.ts +29 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +187 -8
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/url.d.ts +7 -0
- package/dist/internal/url.d.ts.map +1 -1
- package/dist/internal/url.js +24 -1
- package/dist/internal/url.js.map +1 -1
- package/dist/liveStatus.d.ts +8 -0
- package/dist/liveStatus.d.ts.map +1 -1
- package/dist/liveStatus.js.map +1 -1
- package/dist/mcp.d.ts +0 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +0 -5
- package/dist/mcp.js.map +1 -1
- package/dist/types.d.ts +101 -5
- 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 +2 -2
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +4 -3
- package/dist/webhooks.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ await pr.broadcast('ab12cd', {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// `is_urgent` and `requires_ack` are independent. Urgent changes DELIVERY —
|
|
31
|
-
// the push breaks through Focus / Do Not Disturb
|
|
32
|
-
// recipient. `requires_ack` holds the ping open until someone taps Acknowledge
|
|
31
|
+
// the push breaks through Focus / Do Not Disturb and reaches members who muted
|
|
32
|
+
// the room or you (a block still wins) — and asks nothing of the recipient. `requires_ack` holds the ping open until someone taps Acknowledge
|
|
33
33
|
// on the lock-screen card, and does not raise the interruption level on its
|
|
34
34
|
// own. Set both for an acknowledgement that also cuts through Focus.
|
|
35
35
|
await pr.broadcast('ab12cd', {
|
|
@@ -232,6 +232,68 @@ only **add** an acknowledgement requirement for that one press — passing `fals
|
|
|
232
232
|
never removes one from an action already configured to require it. Neither
|
|
233
233
|
modifier edits the action's saved configuration.
|
|
234
234
|
|
|
235
|
+
### Quick actions that need a detail
|
|
236
|
+
|
|
237
|
+
An action can require a detail from whoever presses it: `input_type` is `none`
|
|
238
|
+
(default), `location`, `link`, `file`, `photo` or `pdf`. Read it from
|
|
239
|
+
`actions.list()` before pressing — a press without the matching detail is
|
|
240
|
+
refused with `422 quick_action_input_required` and nothing is sent.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
await pr.actions.update('ab12cd', 2, { label: 'Where are you?', icon: '📍', input_type: 'location' });
|
|
244
|
+
|
|
245
|
+
const [action] = await pr.actions.list('ab12cd'); // action.input_type === 'location'
|
|
246
|
+
await pr.actions.trigger('ab12cd', 2, {
|
|
247
|
+
quick_action_id: action.id, // refused with 409 quick_action_layout_changed if the slot moved pages
|
|
248
|
+
data: { location: { latitude: 25.2048, longitude: 55.2708, label: 'Dubai Mall' } },
|
|
249
|
+
});
|
|
250
|
+
await pr.actions.trigger('ab12cd', 3, { data: { url: 'https://ci.example.com/run/42' } }); // input_type: link
|
|
251
|
+
const file = await pr.attachments.upload({ filename: 'receipt.pdf', content: bytes }); // Pro
|
|
252
|
+
await pr.actions.trigger('ab12cd', 4, { attachment_ids: [file.id] }); // input_type: file | photo | pdf
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`data` on a press accepts only `location` and `url` (no `button_label`); the
|
|
256
|
+
SDK validates both locally with the same rules as `locationPing()` / `linkPing()`.
|
|
257
|
+
`photo` accepts jpg/png only and `pdf` only pdf — a mismatch is
|
|
258
|
+
`422 quick_action_input_type`.
|
|
259
|
+
|
|
260
|
+
Sending `label` and `icon` both empty reserves a slot as **disabled**: it keeps
|
|
261
|
+
its place in the layout and a press answers `404 action_not_configured`. An
|
|
262
|
+
icon is still required whenever a label is set.
|
|
263
|
+
|
|
264
|
+
### Quick Ping pages
|
|
265
|
+
|
|
266
|
+
Rooms hold up to four pages of four slots (5–16 only while the room owner is
|
|
267
|
+
Pro). `updateMany()` adds pages as complete groups of four. To delete or
|
|
268
|
+
reorder pages use the layout route:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await pr.actions.deletePage('ab12cd', 2); // reads the layout, drops page 2, shifts page 3–4 left
|
|
272
|
+
|
|
273
|
+
// Or drive the snapshot contract yourself:
|
|
274
|
+
const stored = await pr.actions.list('ab12cd');
|
|
275
|
+
await pr.actions.updateLayout('ab12cd', {
|
|
276
|
+
base_action_ids: stored.map((a) => a.id!), // every stored id in action_number order
|
|
277
|
+
page_order: [2, 1], // original page 2 first, then page 1; page 3+ deleted
|
|
278
|
+
actions: renumbered, // the full 8 slots for two pages
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
A stale `base_action_ids` is `409 quick_action_layout_changed`; a page still
|
|
283
|
+
targeted by a time trigger, webhook, agent binding or running live update is
|
|
284
|
+
`409 quick_action_page_in_use`. Rooms also report `quick_action_limit` (4 or
|
|
285
|
+
16) and `quick_action_count`.
|
|
286
|
+
|
|
287
|
+
Public rooms can carry a location for nearby discovery — send all three fields
|
|
288
|
+
or none:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
await pr.rooms.createPublic({
|
|
292
|
+
name: 'Meetup', icon: 'globe', color: '#0391fe', handle: 'meetup',
|
|
293
|
+
location_name: 'Dubai Mall', location_latitude: 25.2048, location_longitude: 55.2708,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
235
297
|
Manage incoming webhooks with an agent credential:
|
|
236
298
|
|
|
237
299
|
```ts
|
|
@@ -354,11 +416,15 @@ const approval = await pr.approvals.request('ab12cd', {
|
|
|
354
416
|
});
|
|
355
417
|
|
|
356
418
|
const decided = await pr.approvals.waitForDecision(approval.id);
|
|
357
|
-
if (decided.decision === 'Ship it') {
|
|
419
|
+
if (decided.status === 'decided' && decided.decision === 'Ship it') {
|
|
358
420
|
// proceed
|
|
359
421
|
}
|
|
360
422
|
```
|
|
361
423
|
|
|
424
|
+
For a runnable example that binds approval to a file's SHA-256, handles timeouts,
|
|
425
|
+
and refuses a changed artifact, see [release approval](examples/release-approval/README.md).
|
|
426
|
+
The demo runs locally with simulated decisions and needs no account or credentials.
|
|
427
|
+
|
|
362
428
|
## Human-in-the-loop questions
|
|
363
429
|
|
|
364
430
|
A **question** is the general form of an approval: 2–4 options (or a short typed answer), delivered as a push with tappable buttons, resolving to exactly one answer — first tap wins. Reach for `pr.questions` when you need more than yes/no, a typed reply, or want *anyone in the room* to answer.
|
|
@@ -605,7 +671,7 @@ const snap = await pr.live.get('AB12CD', 'deploy-42');
|
|
|
605
671
|
if (snap) console.log(snap.template, snap.current_step, snap.updated_at);
|
|
606
672
|
```
|
|
607
673
|
|
|
608
|
-
`get()` returns **every** stored field (`state`, `progress`, `message`, `category`, `template`, `agent_id`, `accent_override`, `eta_at`, `deadline_at`, `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.
|
|
674
|
+
`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.
|
|
609
675
|
|
|
610
676
|
### Ownership and limits
|
|
611
677
|
|
|
@@ -679,10 +745,6 @@ const { tools } = await pr.mcp.listTools();
|
|
|
679
745
|
const result = await pr.mcp.callTool('broadcast', { invite_code: 'ab12cd', message: 'hi' });
|
|
680
746
|
```
|
|
681
747
|
|
|
682
|
-
For redemption over MCP, call `await pr.mcp.redeemCode('AB12CD34EF56')`;
|
|
683
|
-
it returns the MCP tool result envelope. Use `pr.redeemCode()` for the typed
|
|
684
|
-
REST result.
|
|
685
|
-
|
|
686
748
|
`pr.mcp` is a PingRoom-specific JSON-RPC convenience client, not a general MCP
|
|
687
749
|
host. Cursor, Claude, and other MCP hosts should connect to the hosted endpoint
|
|
688
750
|
directly and use OAuth instead: <https://pingroom.io/connect-mcp.md>.
|
|
@@ -721,7 +783,9 @@ surface, so you can branch exhaustively:
|
|
|
721
783
|
| `HandoffErrorCode` / `HANDOFF_ERROR_CODES` | `handoffs.*` |
|
|
722
784
|
| `RoomScopedErrorCode` / `ROOM_SCOPED_ERROR_CODES` | every call whose path names a room (`rooms.get`, `actions.*`, `broadcast`, `webhooks.*`, `live.*`) |
|
|
723
785
|
| `AgentInboxErrorCode` / `AGENT_INBOX_ERROR_CODES` | `inbox.ensure()` / `inbox.activate()` |
|
|
724
|
-
| `
|
|
786
|
+
| `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` |
|
|
787
|
+
| `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) |
|
|
788
|
+
| `AgentErrorCode` | all five, for one shared branch |
|
|
725
789
|
|
|
726
790
|
## Configuration
|
|
727
791
|
|
|
@@ -756,8 +820,9 @@ console.log(result.action_state);
|
|
|
756
820
|
`ack_mode: 'any'` is the default and resolves on the first eligible confirmation.
|
|
757
821
|
`all` waits for every original eligible recipient; members who join later are
|
|
758
822
|
excluded. Partial confirmations leave `action_state.status` as `open`, with
|
|
759
|
-
`mode`, `confirmed_count`, and `required_count` showing progress
|
|
760
|
-
|
|
823
|
+
`mode`, `confirmed_count`, and `required_count` showing progress;
|
|
824
|
+
`dismissed_count` counts recipients who dismissed the card, which never counts
|
|
825
|
+
as a confirmation. A wait timeout is not a confirmation. The server sends a confirmation notice for each accepted
|
|
761
826
|
confirmation; outgoing `notification.acked` fires only when the rule is met.
|
|
762
827
|
|
|
763
828
|
The same option works on `pr.actions.trigger(room, slot, { ack_mode: 'all' })`
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from './http.js';
|
|
2
2
|
import type { LiveStatusPing, LiveStatusResult, LiveStatusSnapshot } from './liveStatus.js';
|
|
3
3
|
import { McpClient } from './mcp.js';
|
|
4
|
-
import type { AcknowledgementWaitResult, Attachment, ZipManifest, AgentInboxEnsureResult, AgentInboxActivateInput, AgentInboxActivationResult, AgentInboxEnsureInput, AgentNotification, Approval, ApprovalInput, CreatePublicRoomInput, CreateRoomInput, CreateWebhookInput, Credential, ClaimCompleteParams, ClaimStartParams, AskInput, DirectoryEntry, DirectoryListInput, DirectoryProfile, DirectPingResult, Handoff, ListHandoffsInput, JoinRoomInput, ListNotificationsInput, ListenInput, ListQuestionsInput, NotificationDetail, NotificationHistoryEntry, Paginated, PingInput, PingResult, PingRoomOptions, Question, QuestionInput, QuickAction, RegisterParams, RedeemCodeResult, PairingStart, PairingStatus, PairedCredential, Room, RoomIconCatalog, RequestAckInput, RotateHandleResult, StartPairingParams, TriggerInput, QuickActionInput, UpdateQuickActionInput, UpdateWebhookInput, UploadAttachmentInput, WaitApprovalInput, WaitHandoffInput, WaitForAcknowledgementInput, WaitInput, WaitQuestionInput, WaitResult, WaitForPairingOptions, Webhook } from './types.js';
|
|
4
|
+
import type { AcknowledgementWaitResult, Attachment, ZipManifest, AgentInboxEnsureResult, AgentInboxActivateInput, AgentInboxActivationResult, AgentInboxEnsureInput, AgentNotification, Approval, ApprovalInput, CreatePublicRoomInput, CreateRoomInput, CreateWebhookInput, Credential, ClaimCompleteParams, ClaimStartParams, AskInput, DirectoryEntry, DirectoryListInput, DirectoryProfile, DirectPingResult, Handoff, ListHandoffsInput, JoinRoomInput, ListNotificationsInput, ListenInput, ListQuestionsInput, NotificationDetail, NotificationHistoryEntry, Paginated, PingInput, PingResult, PingRoomOptions, Question, QuestionInput, QuickAction, RegisterParams, RedeemCodeResult, PairingStart, PairingStatus, PairedCredential, Room, RoomIconCatalog, RequestAckInput, RotateHandleResult, StartPairingParams, TriggerInput, QuickActionInput, UpdateQuickActionInput, UpdateQuickActionLayoutInput, UpdateWebhookInput, UploadAttachmentInput, WaitApprovalInput, WaitHandoffInput, WaitForAcknowledgementInput, WaitInput, WaitQuestionInput, WaitResult, WaitForPairingOptions, Webhook } from './types.js';
|
|
5
5
|
/** Agent credential lifecycle (auth.md flow). */
|
|
6
6
|
declare class AuthApi {
|
|
7
7
|
private readonly http;
|
|
@@ -58,7 +58,7 @@ declare class ActionsApi {
|
|
|
58
58
|
*
|
|
59
59
|
* Prefer this over looping `update()`. Each single-slot write enqueues its
|
|
60
60
|
* own silent rooms-refresh push, so configuring a room's four Pings one at a
|
|
61
|
-
* time wakes
|
|
61
|
+
* time wakes room members' devices four times inside a few seconds — four of a
|
|
62
62
|
* finite daily background-push budget spent on one logical operation.
|
|
63
63
|
*
|
|
64
64
|
* Partial and additive: slots absent from `actions` keep their current
|
|
@@ -68,7 +68,31 @@ declare class ActionsApi {
|
|
|
68
68
|
* action set, not just the slots written.
|
|
69
69
|
*/
|
|
70
70
|
updateMany(inviteCode: string, actions: QuickActionInput[]): Promise<QuickAction[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Press a slot. When the action has an `input_type`, pass the matching
|
|
73
|
+
* detail (`data.location`, `data.url`, or `attachment_ids`) or the server
|
|
74
|
+
* refuses the press with `422 quick_action_input_required` and sends nothing.
|
|
75
|
+
* Read `input_type` from `list()` first; pass the action's `id` as
|
|
76
|
+
* `quick_action_id` to be refused rather than fire the wrong Ping if the
|
|
77
|
+
* owner moved pages meanwhile.
|
|
78
|
+
*/
|
|
71
79
|
trigger(inviteCode: string, actionNumber: number, input?: TriggerInput): Promise<PingResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Replace the room's page layout atomically (room owner on Pro only). See
|
|
82
|
+
* `UpdateQuickActionLayoutInput` for the snapshot contract; a stale
|
|
83
|
+
* `base_action_ids` is `409 quick_action_layout_changed`, a page still used
|
|
84
|
+
* by a time trigger, webhook, agent binding or running live update is
|
|
85
|
+
* `409 quick_action_page_in_use`. Prefer `deletePage()`, which builds the
|
|
86
|
+
* body from a fresh read.
|
|
87
|
+
*/
|
|
88
|
+
updateLayout(inviteCode: string, input: UpdateQuickActionLayoutInput): Promise<QuickAction[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Delete one page (1–4) and shift the later pages left, keeping every other
|
|
91
|
+
* slot's configuration. Reads the current action set first so the layout
|
|
92
|
+
* snapshot is real, then calls `updateLayout()`. Page 1 can be deleted only
|
|
93
|
+
* when another page remains.
|
|
94
|
+
*/
|
|
95
|
+
deletePage(inviteCode: string, page: number): Promise<QuickAction[]>;
|
|
72
96
|
}
|
|
73
97
|
declare class WebhooksApi {
|
|
74
98
|
private readonly http;
|
|
@@ -221,8 +245,9 @@ declare class ProfileApi {
|
|
|
221
245
|
* Live-status streams — a self-updating card on the room members' lock screen.
|
|
222
246
|
*
|
|
223
247
|
* Requires the `pingroom:live:write` scope. Free accounts get a small daily
|
|
224
|
-
* budget of NEW streams; updates and the terminal ping
|
|
225
|
-
* card can never be quota-blocked into hanging open.
|
|
248
|
+
* budget of NEW streams; updates and the terminal ping of an open stream are
|
|
249
|
+
* never charged, so a card can never be quota-blocked into hanging open. A
|
|
250
|
+
* first ping that is already `done`/`failed` is a new stream and is charged.
|
|
226
251
|
*/
|
|
227
252
|
declare class LiveApi {
|
|
228
253
|
private readonly http;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -2,8 +2,15 @@ import { PingRoomActivationIncompleteError, PingRoomError } from './errors.js';
|
|
|
2
2
|
import { HttpClient } from './http.js';
|
|
3
3
|
import { combineSignals, sleep } from './internal/async.js';
|
|
4
4
|
import { assertActionNumber, assertMaxLength, assertStructuredData, MAX_PING_TITLE_LENGTH, MAX_PUBLIC_PING_MESSAGE_LENGTH, normalizeRedeemCode, requireNonEmpty, requirePresentString, } from './internal/guards.js';
|
|
5
|
+
import { linkPing } from './linkPing.js';
|
|
6
|
+
import { locationPing } from './locationPing.js';
|
|
5
7
|
import { McpClient } from './mcp.js';
|
|
6
8
|
const DEFAULT_BASE_URL = 'https://api.pingroom.io';
|
|
9
|
+
/** Server cap per uploaded file; a larger body is a 413 the server answers before reading it. */
|
|
10
|
+
const ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024;
|
|
11
|
+
const MAX_TRIGGER_ATTACHMENTS = 4;
|
|
12
|
+
const QUICK_ACTION_PAGE_SIZE = 4;
|
|
13
|
+
const MAX_QUICK_ACTION_PAGES = 4;
|
|
7
14
|
/** Extra wall-clock allowance over the server's hold window for long-poll calls. */
|
|
8
15
|
const LONG_POLL_BUFFER_MS = 10_000;
|
|
9
16
|
const DEFAULT_WAIT_SECONDS = 20;
|
|
@@ -25,6 +32,58 @@ function dropUndefined(obj) {
|
|
|
25
32
|
}
|
|
26
33
|
return out;
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Mirrors the server's `icon` rule (`present`, `required_with:label`): the emoji
|
|
37
|
+
* is the half that must be there unless BOTH are empty, which reserves the
|
|
38
|
+
* slot as disabled.
|
|
39
|
+
*/
|
|
40
|
+
function assertQuickActionIcon(action) {
|
|
41
|
+
requirePresentString(action.label, 'label');
|
|
42
|
+
requirePresentString(action.icon, 'icon');
|
|
43
|
+
if (action.icon.trim() === '' && action.label.trim() !== '') {
|
|
44
|
+
throw new PingRoomError('`icon` is required when `label` is set; send both empty to disable the slot.', {
|
|
45
|
+
code: 'invalid_request',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function assertTriggerInput(input) {
|
|
50
|
+
const data = input.data;
|
|
51
|
+
if (data !== undefined) {
|
|
52
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
53
|
+
throw new PingRoomError('`data` must be an object with `location` and/or `url`.', { code: 'invalid_request' });
|
|
54
|
+
}
|
|
55
|
+
for (const key of Object.keys(data)) {
|
|
56
|
+
if (key !== 'location' && key !== 'url') {
|
|
57
|
+
throw new PingRoomError(`\`data.${key}\` is not accepted on a quick-action press; only location and url are.`, {
|
|
58
|
+
code: 'invalid_request',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (data.location !== undefined)
|
|
64
|
+
locationPing(data.location);
|
|
65
|
+
if (data.url !== undefined)
|
|
66
|
+
linkPing({ url: data.url });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new PingRoomError(error.message, { code: 'invalid_request' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (input.attachment_ids !== undefined) {
|
|
73
|
+
if (!Array.isArray(input.attachment_ids) || input.attachment_ids.length > MAX_TRIGGER_ATTACHMENTS) {
|
|
74
|
+
throw new PingRoomError(`\`attachment_ids\` accepts at most ${MAX_TRIGGER_ATTACHMENTS} ids.`, { code: 'invalid_request' });
|
|
75
|
+
}
|
|
76
|
+
for (const id of input.attachment_ids)
|
|
77
|
+
requireNonEmpty(id, 'attachment_ids[]');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function attachmentByteLength(content) {
|
|
81
|
+
if (content instanceof Blob)
|
|
82
|
+
return content.size;
|
|
83
|
+
if (typeof content === 'string')
|
|
84
|
+
return new TextEncoder().encode(content).byteLength;
|
|
85
|
+
return content.byteLength;
|
|
86
|
+
}
|
|
28
87
|
function assertPing(ping) {
|
|
29
88
|
requireNonEmpty(ping.message, 'message');
|
|
30
89
|
// The invite code does not expose room visibility. Permit the public-room
|
|
@@ -332,6 +391,14 @@ class RoomsApi {
|
|
|
332
391
|
createPublic(input) {
|
|
333
392
|
requireNonEmpty(input.name, 'name');
|
|
334
393
|
requireNonEmpty(input.handle, 'handle');
|
|
394
|
+
// The server takes the location trio all-or-nothing; a partial set is a
|
|
395
|
+
// 422 on a field the caller may not have meant to send at all.
|
|
396
|
+
const present = [input.location_name, input.location_latitude, input.location_longitude].filter((v) => v !== undefined).length;
|
|
397
|
+
if (present !== 0 && present !== 3) {
|
|
398
|
+
throw new PingRoomError('Send `location_name`, `location_latitude` and `location_longitude` together, or none of them.', {
|
|
399
|
+
code: 'invalid_request',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
335
402
|
return this.http.request('POST', '/api/agent/rooms/public', { body: dropUndefined({ ...input }) });
|
|
336
403
|
}
|
|
337
404
|
join(input) {
|
|
@@ -350,9 +417,9 @@ class ActionsApi {
|
|
|
350
417
|
update(inviteCode, actionNumber, input) {
|
|
351
418
|
assertActionNumber(actionNumber);
|
|
352
419
|
// A Ping's title is optional — its emoji can be the whole name — so an
|
|
353
|
-
// empty `label` is a valid update. `icon` is the half that must be there
|
|
354
|
-
|
|
355
|
-
|
|
420
|
+
// empty `label` is a valid update. `icon` is the half that must be there,
|
|
421
|
+
// unless both are empty: that reserves the slot as disabled.
|
|
422
|
+
assertQuickActionIcon(input);
|
|
356
423
|
return this.http.request('PUT', `/api/agent/rooms/${enc(inviteCode)}/actions/${actionNumber}`, {
|
|
357
424
|
body: dropUndefined({ ...input }),
|
|
358
425
|
});
|
|
@@ -363,7 +430,7 @@ class ActionsApi {
|
|
|
363
430
|
*
|
|
364
431
|
* Prefer this over looping `update()`. Each single-slot write enqueues its
|
|
365
432
|
* own silent rooms-refresh push, so configuring a room's four Pings one at a
|
|
366
|
-
* time wakes
|
|
433
|
+
* time wakes room members' devices four times inside a few seconds — four of a
|
|
367
434
|
* finite daily background-push budget spent on one logical operation.
|
|
368
435
|
*
|
|
369
436
|
* Partial and additive: slots absent from `actions` keep their current
|
|
@@ -391,19 +458,116 @@ class ActionsApi {
|
|
|
391
458
|
throw new PingRoomError(`Duplicate action_number ${action.action_number}.`, { code: 'invalid_request' });
|
|
392
459
|
}
|
|
393
460
|
seen.add(action.action_number);
|
|
394
|
-
|
|
395
|
-
requireNonEmpty(action.icon, 'icon');
|
|
461
|
+
assertQuickActionIcon(action);
|
|
396
462
|
}
|
|
397
463
|
return this.http.request('PUT', `/api/agent/rooms/${enc(inviteCode)}/actions`, {
|
|
398
464
|
body: { actions: actions.map((action) => dropUndefined({ ...action })) },
|
|
399
465
|
});
|
|
400
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Press a slot. When the action has an `input_type`, pass the matching
|
|
469
|
+
* detail (`data.location`, `data.url`, or `attachment_ids`) or the server
|
|
470
|
+
* refuses the press with `422 quick_action_input_required` and sends nothing.
|
|
471
|
+
* Read `input_type` from `list()` first; pass the action's `id` as
|
|
472
|
+
* `quick_action_id` to be refused rather than fire the wrong Ping if the
|
|
473
|
+
* owner moved pages meanwhile.
|
|
474
|
+
*/
|
|
401
475
|
trigger(inviteCode, actionNumber, input = {}) {
|
|
402
476
|
assertActionNumber(actionNumber);
|
|
477
|
+
assertTriggerInput(input);
|
|
403
478
|
return this.http.request('POST', `/api/agent/rooms/${enc(inviteCode)}/actions/${actionNumber}/trigger`, {
|
|
404
479
|
body: dropUndefined({ ...input }),
|
|
405
480
|
});
|
|
406
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Replace the room's page layout atomically (room owner on Pro only). See
|
|
484
|
+
* `UpdateQuickActionLayoutInput` for the snapshot contract; a stale
|
|
485
|
+
* `base_action_ids` is `409 quick_action_layout_changed`, a page still used
|
|
486
|
+
* by a time trigger, webhook, agent binding or running live update is
|
|
487
|
+
* `409 quick_action_page_in_use`. Prefer `deletePage()`, which builds the
|
|
488
|
+
* body from a fresh read.
|
|
489
|
+
*/
|
|
490
|
+
updateLayout(inviteCode, input) {
|
|
491
|
+
if (!Array.isArray(input.base_action_ids)) {
|
|
492
|
+
throw new PingRoomError('`base_action_ids` must be an array of action ids (may be empty).', { code: 'invalid_request' });
|
|
493
|
+
}
|
|
494
|
+
if (!Array.isArray(input.page_order) || input.page_order.length < 1 || input.page_order.length > MAX_QUICK_ACTION_PAGES) {
|
|
495
|
+
throw new PingRoomError(`\`page_order\` must list 1–${MAX_QUICK_ACTION_PAGES} pages.`, { code: 'invalid_request' });
|
|
496
|
+
}
|
|
497
|
+
for (const page of input.page_order) {
|
|
498
|
+
if (page !== null && (!Number.isInteger(page) || page < 1 || page > MAX_QUICK_ACTION_PAGES)) {
|
|
499
|
+
throw new PingRoomError('`page_order` entries must be an original page number 1–4 or null for a blank page.', {
|
|
500
|
+
code: 'invalid_request',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const expected = input.page_order.length * QUICK_ACTION_PAGE_SIZE;
|
|
505
|
+
if (!Array.isArray(input.actions) || input.actions.length !== expected) {
|
|
506
|
+
throw new PingRoomError(`\`actions\` must hold exactly ${expected} slots for ${input.page_order.length} page(s).`, {
|
|
507
|
+
code: 'invalid_request',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
for (const action of input.actions) {
|
|
512
|
+
assertActionNumber(action.action_number);
|
|
513
|
+
if (seen.has(action.action_number)) {
|
|
514
|
+
throw new PingRoomError(`Duplicate action_number ${action.action_number}.`, { code: 'invalid_request' });
|
|
515
|
+
}
|
|
516
|
+
seen.add(action.action_number);
|
|
517
|
+
assertQuickActionIcon(action);
|
|
518
|
+
}
|
|
519
|
+
return this.http.request('PUT', `/api/agent/rooms/${enc(inviteCode)}/actions/layout`, {
|
|
520
|
+
body: {
|
|
521
|
+
base_action_ids: input.base_action_ids,
|
|
522
|
+
page_order: input.page_order,
|
|
523
|
+
actions: input.actions.map((action) => dropUndefined({ ...action })),
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Delete one page (1–4) and shift the later pages left, keeping every other
|
|
529
|
+
* slot's configuration. Reads the current action set first so the layout
|
|
530
|
+
* snapshot is real, then calls `updateLayout()`. Page 1 can be deleted only
|
|
531
|
+
* when another page remains.
|
|
532
|
+
*/
|
|
533
|
+
async deletePage(inviteCode, page) {
|
|
534
|
+
if (!Number.isInteger(page) || page < 1 || page > MAX_QUICK_ACTION_PAGES) {
|
|
535
|
+
throw new PingRoomError('`page` must be an integer 1–4.', { code: 'invalid_request' });
|
|
536
|
+
}
|
|
537
|
+
const current = await this.list(inviteCode);
|
|
538
|
+
const stored = [...current].sort((a, b) => a.action_number - b.action_number);
|
|
539
|
+
const pageCount = Math.max(1, Math.ceil((stored.at(-1)?.action_number ?? 0) / QUICK_ACTION_PAGE_SIZE));
|
|
540
|
+
if (page > pageCount) {
|
|
541
|
+
throw new PingRoomError(`The room has ${pageCount} page(s); page ${page} does not exist.`, { code: 'invalid_request' });
|
|
542
|
+
}
|
|
543
|
+
if (pageCount === 1) {
|
|
544
|
+
throw new PingRoomError('A room keeps at least one page; configure its slots instead of deleting it.', {
|
|
545
|
+
code: 'invalid_request',
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const keep = Array.from({ length: pageCount }, (_, i) => i + 1).filter((p) => p !== page);
|
|
549
|
+
const byNumber = new Map(stored.map((a) => [a.action_number, a]));
|
|
550
|
+
const actions = [];
|
|
551
|
+
keep.forEach((originalPage, index) => {
|
|
552
|
+
for (let offset = 1; offset <= QUICK_ACTION_PAGE_SIZE; offset++) {
|
|
553
|
+
const source = byNumber.get((originalPage - 1) * QUICK_ACTION_PAGE_SIZE + offset);
|
|
554
|
+
actions.push({
|
|
555
|
+
action_number: index * QUICK_ACTION_PAGE_SIZE + offset,
|
|
556
|
+
label: source?.label ?? '',
|
|
557
|
+
icon: source?.icon ?? '',
|
|
558
|
+
...(source?.sound !== undefined ? { sound: source.sound } : {}),
|
|
559
|
+
...(source?.haptic_style !== undefined ? { haptic_style: source.haptic_style } : {}),
|
|
560
|
+
...(source?.requires_ack !== undefined ? { requires_ack: source.requires_ack } : {}),
|
|
561
|
+
...(source?.input_type !== undefined ? { input_type: source.input_type } : {}),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
return this.updateLayout(inviteCode, {
|
|
566
|
+
base_action_ids: stored.map((a) => a.id).filter((id) => typeof id === 'string'),
|
|
567
|
+
page_order: keep,
|
|
568
|
+
actions,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
407
571
|
}
|
|
408
572
|
class WebhooksApi {
|
|
409
573
|
http;
|
|
@@ -518,6 +682,8 @@ class NotificationsApi {
|
|
|
518
682
|
for (const n of result.notifications) {
|
|
519
683
|
yield n;
|
|
520
684
|
}
|
|
685
|
+
// Yield between polls so timer-driven cancellation can run (see ApprovalsApi).
|
|
686
|
+
await sleep(0, input.signal);
|
|
521
687
|
}
|
|
522
688
|
}
|
|
523
689
|
}
|
|
@@ -571,6 +737,9 @@ class ApprovalsApi {
|
|
|
571
737
|
if (approval.decided_at !== null || approval.status !== 'pending') {
|
|
572
738
|
return approval;
|
|
573
739
|
}
|
|
740
|
+
// An injected transport can resolve pending polls immediately. Yield
|
|
741
|
+
// between them so timer-driven cancellation and deadlines can run.
|
|
742
|
+
await sleep(0, input.signal);
|
|
574
743
|
}
|
|
575
744
|
}
|
|
576
745
|
}
|
|
@@ -625,6 +794,8 @@ class QuestionsApi {
|
|
|
625
794
|
if (question.state !== 'pending') {
|
|
626
795
|
return question;
|
|
627
796
|
}
|
|
797
|
+
// Yield between polls so timer-driven cancellation can run (see ApprovalsApi).
|
|
798
|
+
await sleep(0, input.signal);
|
|
628
799
|
}
|
|
629
800
|
}
|
|
630
801
|
/** List the asker's Questions, optionally filtered by state. */
|
|
@@ -705,6 +876,8 @@ class HandoffsApi {
|
|
|
705
876
|
if (isHandoffTerminal(handoff)) {
|
|
706
877
|
return handoff;
|
|
707
878
|
}
|
|
879
|
+
// Yield between polls so timer-driven cancellation can run (see ApprovalsApi).
|
|
880
|
+
await sleep(0, input.signal);
|
|
708
881
|
}
|
|
709
882
|
}
|
|
710
883
|
/** List the agent's open handoffs or bounded recent history. */
|
|
@@ -848,8 +1021,9 @@ class ProfileApi {
|
|
|
848
1021
|
* Live-status streams — a self-updating card on the room members' lock screen.
|
|
849
1022
|
*
|
|
850
1023
|
* Requires the `pingroom:live:write` scope. Free accounts get a small daily
|
|
851
|
-
* budget of NEW streams; updates and the terminal ping
|
|
852
|
-
* card can never be quota-blocked into hanging open.
|
|
1024
|
+
* budget of NEW streams; updates and the terminal ping of an open stream are
|
|
1025
|
+
* never charged, so a card can never be quota-blocked into hanging open. A
|
|
1026
|
+
* first ping that is already `done`/`failed` is a new stream and is charged.
|
|
853
1027
|
*/
|
|
854
1028
|
class LiveApi {
|
|
855
1029
|
http;
|
|
@@ -909,6 +1083,11 @@ class AttachmentsApi {
|
|
|
909
1083
|
/** Upload one file and get back the id a send can claim. */
|
|
910
1084
|
upload(input) {
|
|
911
1085
|
requireNonEmpty(input.filename, 'filename');
|
|
1086
|
+
// The server answers an oversized body with 413 before reading it; refuse
|
|
1087
|
+
// locally so no bytes travel for a file that can never be accepted.
|
|
1088
|
+
if (attachmentByteLength(input.content) > ATTACHMENT_MAX_BYTES) {
|
|
1089
|
+
return Promise.reject(new PingRoomError('Attachments are at most 5 MiB each.', { code: 'attachment_too_large', status: 413 }));
|
|
1090
|
+
}
|
|
912
1091
|
const body = new FormData();
|
|
913
1092
|
body.append('file', toAttachmentBlob(input), input.filename);
|
|
914
1093
|
return this.http
|