@pingroom/sdk 0.4.5 → 0.4.7
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 +83 -20
- package/dist/client.d.ts +30 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +149 -24
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +17 -11
- package/dist/http.js.map +1 -1
- package/dist/types.d.ts +142 -18
- 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.map +1 -1
- package/dist/webhooks.js +24 -19
- package/dist/webhooks.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,7 @@ One typed client for everything an agent does: authenticate, create and join roo
|
|
|
6
6
|
|
|
7
7
|
- **Zero runtime dependencies** — built on the platform `fetch`. Works in Node ≥ 20, browsers, Cloudflare Workers, Deno, and Bun.
|
|
8
8
|
- **Fully typed** — ships `.d.ts`; request fields mirror the HTTP API verbatim.
|
|
9
|
-
- **Secure by default** — refuses
|
|
10
|
-
|
|
11
|
-
> **Release status:** npm serves 0.4.3. This source tree is 0.4.4 —
|
|
12
|
-
> unpublished — adding the `locationPing()` helper, the one-shot
|
|
13
|
-
> `requires_ack` modifier on `actions.trigger()`, and the `question` /
|
|
14
|
-
> `is_handoff` fields on listened notifications.
|
|
9
|
+
- **Secure by default** — refuses plain-http credential requests and redirects, applies request deadlines through JSON response reads, and verifies webhook signatures in constant time.
|
|
15
10
|
|
|
16
11
|
```bash
|
|
17
12
|
npm install @pingroom/sdk
|
|
@@ -106,39 +101,80 @@ legacy or malformed data.
|
|
|
106
101
|
|
|
107
102
|
## Authentication
|
|
108
103
|
|
|
109
|
-
|
|
104
|
+
Pair local tools in a browser or the PingRoom app without copying a token or
|
|
105
|
+
room code. Pairing provisions a separate PingRoom robot first; the person who
|
|
106
|
+
opens the link claims that robot and delegates its room access:
|
|
110
107
|
|
|
111
108
|
```ts
|
|
112
109
|
const pr = new PingRoom();
|
|
113
110
|
|
|
114
111
|
const pairing = await pr.auth.startPairing({
|
|
115
112
|
agent_label: 'Deploy bot',
|
|
116
|
-
scopes: ['pingroom:rooms:read', 'pingroom:broadcast:send', 'pingroom:handoffs:create'],
|
|
117
113
|
});
|
|
118
114
|
|
|
119
|
-
console.log(`Open
|
|
115
|
+
console.log(`Open to approve: ${pairing.pair_browser_url ?? pairing.pair_url}`);
|
|
116
|
+
console.log(`Install PingRoom: ${pairing.app_install_url ?? 'https://pingroom.io/i'}`);
|
|
117
|
+
if (pairing.agent?.profile) {
|
|
118
|
+
const { display_name, handle } = pairing.agent.profile;
|
|
119
|
+
console.log(`Claim ${display_name ?? 'this robot'}${handle ? ` (@${handle})` : ''}`);
|
|
120
|
+
}
|
|
121
|
+
// QR renderers should encode the native app link when the server supplies it.
|
|
122
|
+
const qrUrl = pairing.pair_qr_url ?? pairing.pair_url;
|
|
120
123
|
const active = await pr.auth.waitForPairing(pairing);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
console.log(`Latest pings: ${active.links?.latest_pings ?? 'not supplied by this server'}`);
|
|
125
|
+
|
|
126
|
+
// The client now uses the robot's active credential. `active.home_room` is the
|
|
127
|
+
// additive v2 name for the same delivery room exposed as legacy `active.room`.
|
|
128
|
+
// It is the room the approver picked, or the server chose deterministically for
|
|
129
|
+
// an all-rooms grant. It is null only when no eligible private room exists (and
|
|
130
|
+
// on older servers), so keep the legacy fallback and still check before use.
|
|
131
|
+
const homeRoom = active.home_room ?? active.room;
|
|
132
|
+
if (homeRoom) {
|
|
133
|
+
await pr.broadcast(homeRoom.invite_code, { message: 'SDK connected ✅' });
|
|
127
134
|
} else {
|
|
128
|
-
//
|
|
135
|
+
// No eligible private delivery room exists yet. List or create one.
|
|
129
136
|
const [room] = await pr.rooms.list();
|
|
130
137
|
if (room) await pr.broadcast(room.invite_code, { message: 'SDK connected ✅' });
|
|
131
138
|
}
|
|
132
139
|
```
|
|
133
140
|
|
|
141
|
+
Before pairing, tell a person why PingRoom is useful: it lets the robot reach
|
|
142
|
+
them on their phone with urgent Pings, questions, approvals, handoffs, and live
|
|
143
|
+
progress. If they need the app, offer `pairing.app_install_url` (or
|
|
144
|
+
`https://pingroom.io/i` for an older server), and ask them to install or open
|
|
145
|
+
PingRoom and sign in before starting pairing when possible. Installing the app
|
|
146
|
+
does not grant the robot access.
|
|
147
|
+
|
|
148
|
+
`pair_browser_url` opens the API-hosted approval page, where users can sign in
|
|
149
|
+
or scan the app QR; fall back to `pair_url` on older servers. `pair_url` remains
|
|
150
|
+
QR-compatible for existing clients, and `pair_qr_url` is the app universal link
|
|
151
|
+
to encode in QR codes. If a pairing is already pending, reuse that exact link
|
|
152
|
+
while the person installs; do not create a second robot. Never copy
|
|
153
|
+
`pair_token` into an install or store URL.
|
|
154
|
+
|
|
134
155
|
The grant also comes back as `active.room_access` (`'all'` | `'selected'`) and
|
|
135
156
|
`active.rooms` (`{ id, invite_code, name }[]`, empty under `'all'`).
|
|
136
157
|
|
|
158
|
+
Supporting servers mark this contract with `flow_version: 2` and
|
|
159
|
+
`claim_mode: 'agent_identity'`. `pairing.agent.profile` identifies the robot
|
|
160
|
+
before approval. After approval, `active.owner` names the person who claimed it,
|
|
161
|
+
while `active.home_room` and `active.room_membership` describe where the robot
|
|
162
|
+
joined. These fields are additive and optional in the TypeScript surface, so
|
|
163
|
+
the same code continues to work against older servers. The robot is the acting
|
|
164
|
+
principal; it does not impersonate the owner's personal PingRoom profile.
|
|
165
|
+
|
|
137
166
|
`waitForPairing()` tolerates short network outages and stops when the app link
|
|
138
167
|
expires. Pass an `AbortSignal` when your process needs its own cancellation.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
Pairing grants are server-authoritative. `startPairing()` sends no scope array,
|
|
169
|
+
so an older installed SDK cannot accidentally mint a partial credential when
|
|
170
|
+
the service adds a capability. The deprecated `scopes` option is accepted for
|
|
171
|
+
source compatibility but ignored. The approval screen remains the authority
|
|
172
|
+
for the access the user grants.
|
|
173
|
+
|
|
174
|
+
Supporting servers return `active.links.latest_pings`, a stable REST URL for
|
|
175
|
+
reading the agent's latest pings with its normal Bearer credential, and may
|
|
176
|
+
return `active.links.install_app` for later recovery. Older servers may omit
|
|
177
|
+
these fields; malformed or non-HTTP(S) link values are discarded.
|
|
142
178
|
|
|
143
179
|
If you already manage credentials, bring an existing agent token or run the
|
|
144
180
|
lower-level auth.md email flow:
|
|
@@ -168,12 +204,20 @@ Generic MCP clients (Cursor, Claude Desktop, Claude Code) should use the standar
|
|
|
168
204
|
```ts
|
|
169
205
|
await pr.rooms.list();
|
|
170
206
|
await pr.rooms.get('ab12cd');
|
|
207
|
+
await pr.rooms.icons(); // catalog ids, categories, filenames and tags
|
|
171
208
|
await pr.rooms.create({ name: 'Deploys', icon: 'rocket', color: '#e33122' });
|
|
172
209
|
await pr.rooms.join({ invite_code: 'ab12cd' });
|
|
210
|
+
await pr.rooms.join({ invite_code: 'protected-room', password: 'room-password' });
|
|
173
211
|
|
|
174
|
-
await pr.actions.update('ab12cd', 1, { label: 'Approve', icon: '
|
|
212
|
+
await pr.actions.update('ab12cd', 1, { label: 'Approve', icon: '✅' });
|
|
175
213
|
await pr.actions.trigger('ab12cd', 1);
|
|
176
214
|
|
|
215
|
+
// Configure several buttons in one request. Omitted slots stay unchanged.
|
|
216
|
+
await pr.actions.updateMany('ab12cd', [
|
|
217
|
+
{ action_number: 1, label: 'Approve', icon: '✅' },
|
|
218
|
+
{ action_number: 2, label: 'Reject', icon: '❌' },
|
|
219
|
+
]);
|
|
220
|
+
|
|
177
221
|
// One-shot modifiers on a single press. `trigger_source` is `manual` (default)
|
|
178
222
|
// or `location`; `webhook`/`system` are stamped server-side and rejected here.
|
|
179
223
|
await pr.actions.trigger('ab12cd', 1, {
|
|
@@ -188,6 +232,18 @@ only **add** an acknowledgement requirement for that one press — passing `fals
|
|
|
188
232
|
never removes one from an action already configured to require it. Neither
|
|
189
233
|
modifier edits the action's saved configuration.
|
|
190
234
|
|
|
235
|
+
Manage incoming webhooks with an agent credential:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const hooks = await pr.webhooks.list('ab12cd');
|
|
239
|
+
const hook = await pr.webhooks.create('ab12cd', { name: 'Deploys', icon: 'bell' });
|
|
240
|
+
await pr.webhooks.update('ab12cd', hook.id, { enabled: false });
|
|
241
|
+
await pr.webhooks.delete('ab12cd', hook.id);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Webhook records include `webhook_url`, which contains the trigger credential.
|
|
245
|
+
Updating with `regenerate_secret: true` returns a new URL and revokes the old one.
|
|
246
|
+
|
|
191
247
|
## Attachments
|
|
192
248
|
|
|
193
249
|
Send the file itself, not a link to it. Upload first, then pass the ids to any
|
|
@@ -260,6 +316,12 @@ Or a single long-poll:
|
|
|
260
316
|
const { notifications, cursor } = await pr.notifications.wait({ after: lastCursor, timeout: 20 });
|
|
261
317
|
```
|
|
262
318
|
|
|
319
|
+
Read history with `pr.notifications.list({ limit: 25, page: 2 })`. Explicit
|
|
320
|
+
limits are 1–25; omitting the limit retains the server's legacy default of 50.
|
|
321
|
+
The deprecated `notifications_per_page` option maps to `limit`; an explicit
|
|
322
|
+
`limit` takes precedence. History preserves the API's room object, including
|
|
323
|
+
`room.invite_code`, and adds `room.code` for consistency with `listen()`.
|
|
324
|
+
|
|
263
325
|
Read one ping and wait for its acknowledgement through the same namespace:
|
|
264
326
|
|
|
265
327
|
```ts
|
|
@@ -313,6 +375,7 @@ const q = await pr.questions.ask('ab12cd', {
|
|
|
313
375
|
responder_scope: 'room', // or 'direct' (defaults to your bound user)
|
|
314
376
|
ttl: 600,
|
|
315
377
|
correlation_id: 'deploy-1.4.0',
|
|
378
|
+
idempotencyKey: 'deploy-1.4.0-environment', // reuse when retrying this create
|
|
316
379
|
});
|
|
317
380
|
|
|
318
381
|
// Block until a human taps (or it expires / is cancelled)
|
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, Credential, ClaimCompleteParams, ClaimStartParams, AskInput, DirectoryEntry, DirectoryListInput, DirectoryProfile, DirectPingResult, Handoff, ListHandoffsInput, JoinRoomInput, ListNotificationsInput, ListenInput, ListQuestionsInput, NotificationDetail, Paginated, PingInput, PingResult, PingRoomOptions, Question, QuestionInput, QuickAction, RegisterParams, PairingStart, PairingStatus, PairedCredential, Room, RequestAckInput, RotateHandleResult, StartPairingParams, TriggerInput, UpdateQuickActionInput, UploadAttachmentInput, WaitApprovalInput, WaitHandoffInput, WaitForAcknowledgementInput, WaitInput, WaitQuestionInput, WaitResult, WaitForPairingOptions } 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, PairingStart, PairingStatus, PairedCredential, Room, RoomIconCatalog, RequestAckInput, RotateHandleResult, StartPairingParams, TriggerInput, QuickActionInput, UpdateQuickActionInput, 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;
|
|
@@ -17,9 +17,9 @@ declare class AuthApi {
|
|
|
17
17
|
claimComplete(params: ClaimCompleteParams): Promise<Credential>;
|
|
18
18
|
/**
|
|
19
19
|
* Register a pending agent and create a short-lived link that opens the
|
|
20
|
-
* PingRoom app. The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* PingRoom app. The server owns the full grant shown to the approver; this
|
|
21
|
+
* client sends no scope list. The pending credential stays private inside the
|
|
22
|
+
* client and is replaced automatically once waitForPairing() succeeds.
|
|
23
23
|
*/
|
|
24
24
|
startPairing(params?: StartPairingParams): Promise<PairingStart>;
|
|
25
25
|
/** Read the current state of an app pairing using the pending credential. */
|
|
@@ -40,6 +40,7 @@ declare class AuthApi {
|
|
|
40
40
|
declare class RoomsApi {
|
|
41
41
|
private readonly http;
|
|
42
42
|
constructor(http: HttpClient);
|
|
43
|
+
icons(): Promise<RoomIconCatalog>;
|
|
43
44
|
list(): Promise<Room[]>;
|
|
44
45
|
get(inviteCode: string): Promise<Room>;
|
|
45
46
|
create(input: CreateRoomInput): Promise<Room>;
|
|
@@ -51,13 +52,36 @@ declare class ActionsApi {
|
|
|
51
52
|
constructor(http: HttpClient);
|
|
52
53
|
list(inviteCode: string): Promise<QuickAction[]>;
|
|
53
54
|
update(inviteCode: string, actionNumber: number, input: UpdateQuickActionInput): Promise<QuickAction>;
|
|
55
|
+
/**
|
|
56
|
+
* Write up to four action slots in ONE request.
|
|
57
|
+
*
|
|
58
|
+
* Prefer this over looping `update()`. Each single-slot write enqueues its
|
|
59
|
+
* own silent rooms-refresh push, so configuring a room's four Pings one at a
|
|
60
|
+
* time wakes the owner's device four times inside a few seconds — four of a
|
|
61
|
+
* finite daily background-push budget spent on one logical operation.
|
|
62
|
+
*
|
|
63
|
+
* Partial and additive: slots absent from `actions` keep their current
|
|
64
|
+
* configuration, so this is also the right call for editing a single Ping.
|
|
65
|
+
* Nothing here deletes an action, and an empty array is rejected locally
|
|
66
|
+
* rather than being sent as a clear. Resolves to the room's full ordered
|
|
67
|
+
* action set, not just the slots written.
|
|
68
|
+
*/
|
|
69
|
+
updateMany(inviteCode: string, actions: QuickActionInput[]): Promise<QuickAction[]>;
|
|
54
70
|
trigger(inviteCode: string, actionNumber: number, input?: TriggerInput): Promise<PingResult>;
|
|
55
71
|
}
|
|
72
|
+
declare class WebhooksApi {
|
|
73
|
+
private readonly http;
|
|
74
|
+
constructor(http: HttpClient);
|
|
75
|
+
list(inviteCode: string): Promise<Webhook[]>;
|
|
76
|
+
create(inviteCode: string, input: CreateWebhookInput): Promise<Webhook>;
|
|
77
|
+
update(inviteCode: string, webhookId: string, input: UpdateWebhookInput): Promise<Webhook>;
|
|
78
|
+
delete(inviteCode: string, webhookId: string): Promise<void>;
|
|
79
|
+
}
|
|
56
80
|
declare class NotificationsApi {
|
|
57
81
|
private readonly http;
|
|
58
82
|
constructor(http: HttpClient);
|
|
59
83
|
/** Paginated list (polling fallback). Prefer wait()/listen() for real-time. */
|
|
60
|
-
list(input?: ListNotificationsInput): Promise<Paginated<
|
|
84
|
+
list(input?: ListNotificationsInput): Promise<Paginated<NotificationHistoryEntry>>;
|
|
61
85
|
/** Fetch one visible notification, including its current acknowledgement state. */
|
|
62
86
|
getNotification(notificationId: string): Promise<NotificationDetail>;
|
|
63
87
|
/**
|
|
@@ -262,6 +286,7 @@ declare class DirectoryApi {
|
|
|
262
286
|
export declare class PingRoom {
|
|
263
287
|
readonly auth: AuthApi;
|
|
264
288
|
readonly rooms: RoomsApi;
|
|
289
|
+
readonly webhooks: WebhooksApi;
|
|
265
290
|
readonly actions: ActionsApi;
|
|
266
291
|
readonly notifications: NotificationsApi;
|
|
267
292
|
readonly attachments: AttachmentsApi;
|
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;AAWvC,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,EACV,yBAAyB,EACzB,UAAU,EACV,WAAW,EACX,sBAAsB,EACtB,uBAAuB,EAEvB,0BAA0B,EAC1B,qBAAqB,EACrB,iBAAiB,EACjB,QAAQ,EACR,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EAGP,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,EACT,SAAS,EACT,UAAU,EACV,eAAe,EACf,QAAQ,EACR,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,2BAA2B,EAC3B,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,qBAAqB,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -4,11 +4,6 @@ import { combineSignals, sleep } from './internal/async.js';
|
|
|
4
4
|
import { assertActionNumber, assertMaxLength, assertStructuredData, MAX_PING_TITLE_LENGTH, MAX_PUBLIC_PING_MESSAGE_LENGTH, requireNonEmpty, requirePresentString, } from './internal/guards.js';
|
|
5
5
|
import { McpClient } from './mcp.js';
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://api.pingroom.io';
|
|
7
|
-
const DEFAULT_PAIRING_SCOPES = [
|
|
8
|
-
'pingroom:rooms:read',
|
|
9
|
-
'pingroom:broadcast:send',
|
|
10
|
-
'pingroom:handoffs:create',
|
|
11
|
-
];
|
|
12
7
|
/** Extra wall-clock allowance over the server's hold window for long-poll calls. */
|
|
13
8
|
const LONG_POLL_BUFFER_MS = 10_000;
|
|
14
9
|
const DEFAULT_WAIT_SECONDS = 20;
|
|
@@ -51,6 +46,39 @@ function isNonEmptyString(value) {
|
|
|
51
46
|
function isNullableString(value) {
|
|
52
47
|
return value === null || typeof value === 'string';
|
|
53
48
|
}
|
|
49
|
+
/** Drop a malformed or unsafe server-provided URL without rejecting its response. */
|
|
50
|
+
function normalizePublicHttpUrl(candidate) {
|
|
51
|
+
if (typeof candidate !== 'string' || candidate.trim() === '')
|
|
52
|
+
return undefined;
|
|
53
|
+
const trimmed = candidate.trim();
|
|
54
|
+
// eslint-disable-next-line no-control-regex
|
|
55
|
+
if (/[\u0000-\u001F\u007F-\u009F]/.test(trimmed))
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(trimmed);
|
|
59
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
60
|
+
return undefined;
|
|
61
|
+
if (url.username !== '' || url.password !== '')
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return trimmed;
|
|
68
|
+
}
|
|
69
|
+
/** Drop malformed or unsafe server-provided links without rejecting a valid credential. */
|
|
70
|
+
function normalizePairingLinks(value) {
|
|
71
|
+
if (!isRecord(value))
|
|
72
|
+
return undefined;
|
|
73
|
+
const latestPings = normalizePublicHttpUrl(value['latest_pings']);
|
|
74
|
+
if (!latestPings)
|
|
75
|
+
return undefined;
|
|
76
|
+
const installApp = normalizePublicHttpUrl(value['install_app']);
|
|
77
|
+
return {
|
|
78
|
+
latest_pings: latestPings,
|
|
79
|
+
...(installApp ? { install_app: installApp } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
54
82
|
function invalidInboxResponse(message, body = {}) {
|
|
55
83
|
throw new PingRoomError(message, { code: 'inbox_invalid_response', body });
|
|
56
84
|
}
|
|
@@ -162,22 +190,20 @@ class AuthApi {
|
|
|
162
190
|
}
|
|
163
191
|
/**
|
|
164
192
|
* Register a pending agent and create a short-lived link that opens the
|
|
165
|
-
* PingRoom app. The
|
|
166
|
-
*
|
|
167
|
-
*
|
|
193
|
+
* PingRoom app. The server owns the full grant shown to the approver; this
|
|
194
|
+
* client sends no scope list. The pending credential stays private inside the
|
|
195
|
+
* client and is replaced automatically once waitForPairing() succeeds.
|
|
168
196
|
*/
|
|
169
197
|
async startPairing(params = {}) {
|
|
170
|
-
const scopes = params.scopes ?? [...DEFAULT_PAIRING_SCOPES];
|
|
171
198
|
const previousToken = this.http.getToken();
|
|
172
199
|
const pending = await this.register({
|
|
173
200
|
type: 'anonymous',
|
|
174
|
-
scopes,
|
|
175
201
|
...(params.agent_label ? { agent_label: params.agent_label } : {}),
|
|
176
202
|
});
|
|
177
203
|
this.http.setToken(pending.credential);
|
|
178
204
|
try {
|
|
179
205
|
return await this.http.request('POST', '/api/agent/auth/pair/start', {
|
|
180
|
-
body: {
|
|
206
|
+
body: {},
|
|
181
207
|
});
|
|
182
208
|
}
|
|
183
209
|
catch (error) {
|
|
@@ -201,7 +227,8 @@ class AuthApi {
|
|
|
201
227
|
const lifetimeMs = Math.max(1, Number(pairing.expires_in) || 900) * 1000;
|
|
202
228
|
const requestedInterval = options.pollIntervalMs ?? pairing.poll_interval_ms;
|
|
203
229
|
const intervalMs = Math.min(Math.max(Number(requestedInterval) || 1500, 1000), 10_000);
|
|
204
|
-
const
|
|
230
|
+
const startedAt = Date.now();
|
|
231
|
+
const deadline = startedAt + lifetimeMs;
|
|
205
232
|
let transientFailures = 0;
|
|
206
233
|
while (Date.now() < deadline) {
|
|
207
234
|
this.throwIfPairingAborted(options.signal);
|
|
@@ -213,17 +240,13 @@ class AuthApi {
|
|
|
213
240
|
if (typeof status.credential !== 'string'
|
|
214
241
|
|| status.credential.trim() === ''
|
|
215
242
|
|| status.credential_type !== 'active'
|
|
216
|
-
|| !Array.isArray(status.scopes)
|
|
217
|
-
|| status.scopes.some((scope) => typeof scope !== 'string' || scope.trim() === '')
|
|
218
243
|
|| (status.expires_in !== null
|
|
219
244
|
&& (typeof status.expires_in !== 'number'
|
|
220
245
|
|| !Number.isFinite(status.expires_in)
|
|
221
246
|
|| status.expires_in < 0))
|
|
222
|
-
// A null/absent `room` is
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// credential on the most permissive grant. Only a room that is
|
|
226
|
-
// PRESENT but malformed is a broken response.
|
|
247
|
+
// A null/absent `room` is valid when the account has no eligible
|
|
248
|
+
// private delivery room (and on older servers). Only a room that is
|
|
249
|
+
// present but malformed is a broken response.
|
|
227
250
|
|| (status.room != null
|
|
228
251
|
&& (typeof status.room.invite_code !== 'string'
|
|
229
252
|
|| status.room.invite_code.trim() === ''))) {
|
|
@@ -233,8 +256,12 @@ class AuthApi {
|
|
|
233
256
|
});
|
|
234
257
|
}
|
|
235
258
|
this.http.setToken(status.credential);
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
// Pairing is server-authoritative. Some servers still include their
|
|
260
|
+
// internal grant marker for protocol compatibility; it is neither a
|
|
261
|
+
// client input nor part of the paired credential surface.
|
|
262
|
+
const { status: _status, scopes: _serverScopes, links: rawLinks, ...credential } = status;
|
|
263
|
+
const links = normalizePairingLinks(rawLinks);
|
|
264
|
+
return links ? { ...credential, links } : credential;
|
|
238
265
|
}
|
|
239
266
|
if (status.status === 'expired') {
|
|
240
267
|
throw new PingRoomError('The PingRoom pairing link expired before it was approved.', {
|
|
@@ -243,6 +270,15 @@ class AuthApi {
|
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
272
|
catch (error) {
|
|
273
|
+
if (error instanceof PingRoomError
|
|
274
|
+
&& [401, 403, 404].includes(error.status)
|
|
275
|
+
&& Date.now() - startedAt >= lifetimeMs / 2) {
|
|
276
|
+
// The pending bearer can expire before the server returns an
|
|
277
|
+
// explicit expired pairing state. Keep early auth failures intact.
|
|
278
|
+
throw new PingRoomError('The PingRoom pairing link expired before it was approved.', {
|
|
279
|
+
code: 'pairing_expired',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
246
282
|
if (error instanceof PingRoomError
|
|
247
283
|
&& (error.code === 'network_error'
|
|
248
284
|
|| error.code === 'timeout'
|
|
@@ -280,6 +316,9 @@ class RoomsApi {
|
|
|
280
316
|
constructor(http) {
|
|
281
317
|
this.http = http;
|
|
282
318
|
}
|
|
319
|
+
icons() {
|
|
320
|
+
return this.http.request('GET', '/api/agent/room-icons');
|
|
321
|
+
}
|
|
283
322
|
list() {
|
|
284
323
|
return this.http.request('GET', '/api/agent/rooms');
|
|
285
324
|
}
|
|
@@ -318,6 +357,46 @@ class ActionsApi {
|
|
|
318
357
|
body: dropUndefined({ ...input }),
|
|
319
358
|
});
|
|
320
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Write up to four action slots in ONE request.
|
|
362
|
+
*
|
|
363
|
+
* Prefer this over looping `update()`. Each single-slot write enqueues its
|
|
364
|
+
* own silent rooms-refresh push, so configuring a room's four Pings one at a
|
|
365
|
+
* time wakes the owner's device four times inside a few seconds — four of a
|
|
366
|
+
* finite daily background-push budget spent on one logical operation.
|
|
367
|
+
*
|
|
368
|
+
* Partial and additive: slots absent from `actions` keep their current
|
|
369
|
+
* configuration, so this is also the right call for editing a single Ping.
|
|
370
|
+
* Nothing here deletes an action, and an empty array is rejected locally
|
|
371
|
+
* rather than being sent as a clear. Resolves to the room's full ordered
|
|
372
|
+
* action set, not just the slots written.
|
|
373
|
+
*/
|
|
374
|
+
updateMany(inviteCode, actions) {
|
|
375
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
376
|
+
throw new PingRoomError('`actions` must contain at least one action.', { code: 'invalid_request' });
|
|
377
|
+
}
|
|
378
|
+
if (actions.length > 4) {
|
|
379
|
+
throw new PingRoomError('A room has only 4 action slots.', { code: 'invalid_request' });
|
|
380
|
+
}
|
|
381
|
+
const seen = new Set();
|
|
382
|
+
for (const action of actions) {
|
|
383
|
+
assertActionNumber(action.action_number);
|
|
384
|
+
if (action.action_number === undefined || action.action_number === null) {
|
|
385
|
+
throw new PingRoomError('`action_number` is required for each action.', { code: 'invalid_request' });
|
|
386
|
+
}
|
|
387
|
+
// Two entries for one slot would let the last silently win, and the
|
|
388
|
+
// caller would never learn which of its definitions was stored.
|
|
389
|
+
if (seen.has(action.action_number)) {
|
|
390
|
+
throw new PingRoomError(`Duplicate action_number ${action.action_number}.`, { code: 'invalid_request' });
|
|
391
|
+
}
|
|
392
|
+
seen.add(action.action_number);
|
|
393
|
+
requirePresentString(action.label, 'label');
|
|
394
|
+
requireNonEmpty(action.icon, 'icon');
|
|
395
|
+
}
|
|
396
|
+
return this.http.request('PUT', `/api/agent/rooms/${enc(inviteCode)}/actions`, {
|
|
397
|
+
body: { actions: actions.map((action) => dropUndefined({ ...action })) },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
321
400
|
trigger(inviteCode, actionNumber, input = {}) {
|
|
322
401
|
assertActionNumber(actionNumber);
|
|
323
402
|
return this.http.request('POST', `/api/agent/rooms/${enc(inviteCode)}/actions/${actionNumber}/trigger`, {
|
|
@@ -325,14 +404,52 @@ class ActionsApi {
|
|
|
325
404
|
});
|
|
326
405
|
}
|
|
327
406
|
}
|
|
407
|
+
class WebhooksApi {
|
|
408
|
+
http;
|
|
409
|
+
constructor(http) {
|
|
410
|
+
this.http = http;
|
|
411
|
+
}
|
|
412
|
+
async list(inviteCode) {
|
|
413
|
+
const result = await this.http.request('GET', `/api/agent/rooms/${enc(inviteCode)}/webhooks`);
|
|
414
|
+
return result.webhooks;
|
|
415
|
+
}
|
|
416
|
+
create(inviteCode, input) {
|
|
417
|
+
requireNonEmpty(input.name, 'name');
|
|
418
|
+
assertActionNumber(input.action_number);
|
|
419
|
+
return this.http.request('POST', `/api/agent/rooms/${enc(inviteCode)}/webhooks`, {
|
|
420
|
+
body: dropUndefined({ ...input }),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
update(inviteCode, webhookId, input) {
|
|
424
|
+
requireNonEmpty(webhookId, 'webhookId');
|
|
425
|
+
assertActionNumber(input.action_number);
|
|
426
|
+
return this.http.request('PUT', `/api/agent/rooms/${enc(inviteCode)}/webhooks/${enc(webhookId)}`, {
|
|
427
|
+
body: dropUndefined({ ...input }),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async delete(inviteCode, webhookId) {
|
|
431
|
+
requireNonEmpty(webhookId, 'webhookId');
|
|
432
|
+
await this.http.request('DELETE', `/api/agent/rooms/${enc(inviteCode)}/webhooks/${enc(webhookId)}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
328
435
|
class NotificationsApi {
|
|
329
436
|
http;
|
|
330
437
|
constructor(http) {
|
|
331
438
|
this.http = http;
|
|
332
439
|
}
|
|
333
440
|
/** Paginated list (polling fallback). Prefer wait()/listen() for real-time. */
|
|
334
|
-
list(input = {}) {
|
|
335
|
-
|
|
441
|
+
async list(input = {}) {
|
|
442
|
+
const { notifications_per_page, ...query } = input;
|
|
443
|
+
const page = await this.http.request('GET', '/api/agent/notifications', {
|
|
444
|
+
query: dropUndefined({ ...query, limit: input.limit ?? notifications_per_page }),
|
|
445
|
+
});
|
|
446
|
+
return {
|
|
447
|
+
...page,
|
|
448
|
+
data: page.data.map(({ room, ...notification }) => ({
|
|
449
|
+
...notification,
|
|
450
|
+
...(room ? { room: { ...room, code: room.invite_code } } : {}),
|
|
451
|
+
})),
|
|
452
|
+
};
|
|
336
453
|
}
|
|
337
454
|
/** Fetch one visible notification, including its current acknowledgement state. */
|
|
338
455
|
getNotification(notificationId) {
|
|
@@ -471,8 +588,14 @@ class QuestionsApi {
|
|
|
471
588
|
ask(inviteCode, input) {
|
|
472
589
|
requireNonEmpty(input.prompt, 'prompt');
|
|
473
590
|
assertStructuredData(input.data);
|
|
591
|
+
const { idempotencyKey, ...body } = input;
|
|
592
|
+
if (idempotencyKey !== undefined) {
|
|
593
|
+
requireNonEmpty(idempotencyKey, 'idempotencyKey');
|
|
594
|
+
assertMaxLength(idempotencyKey, 255, 'idempotencyKey');
|
|
595
|
+
}
|
|
474
596
|
return this.http.request('POST', `/api/agent/rooms/${enc(inviteCode)}/questions`, {
|
|
475
|
-
body: dropUndefined(
|
|
597
|
+
body: dropUndefined(body),
|
|
598
|
+
...(idempotencyKey !== undefined ? { headers: { 'Idempotency-Key': idempotencyKey } } : {}),
|
|
476
599
|
});
|
|
477
600
|
}
|
|
478
601
|
/** Fetch a Question's current state. */
|
|
@@ -856,6 +979,7 @@ class DirectoryApi {
|
|
|
856
979
|
export class PingRoom {
|
|
857
980
|
auth;
|
|
858
981
|
rooms;
|
|
982
|
+
webhooks;
|
|
859
983
|
actions;
|
|
860
984
|
notifications;
|
|
861
985
|
attachments;
|
|
@@ -881,6 +1005,7 @@ export class PingRoom {
|
|
|
881
1005
|
});
|
|
882
1006
|
this.auth = new AuthApi(this.http);
|
|
883
1007
|
this.rooms = new RoomsApi(this.http);
|
|
1008
|
+
this.webhooks = new WebhooksApi(this.http);
|
|
884
1009
|
this.actions = new ActionsApi(this.http);
|
|
885
1010
|
this.notifications = new NotificationsApi(this.http);
|
|
886
1011
|
this.attachments = new AttachmentsApi(this.http);
|