@pingroom/sdk 0.7.1 → 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 +288 -44
- package/dist/client.d.ts +27 -312
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +33 -1154
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +40 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +56 -4
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts +12 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +47 -18
- 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/async.d.ts +2 -0
- package/dist/internal/async.d.ts.map +1 -1
- package/dist/internal/async.js +6 -0
- package/dist/internal/async.js.map +1 -1
- package/dist/internal/guards.d.ts +16 -0
- package/dist/internal/guards.d.ts.map +1 -1
- package/dist/internal/guards.js +22 -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 +65 -0
- package/dist/internal/retry.d.ts.map +1 -0
- package/dist/internal/retry.js +126 -0
- package/dist/internal/retry.js.map +1 -0
- package/dist/internal/url.d.ts +9 -0
- package/dist/internal/url.d.ts.map +1 -1
- package/dist/internal/url.js +25 -0
- package/dist/internal/url.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/linkPing.d.ts.map +1 -1
- package/dist/linkPing.js +6 -2
- package/dist/linkPing.js.map +1 -1
- 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 +40 -10
- 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 -1035
- 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 +32 -5
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +44 -14
- 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/src/errors.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types. PingRoomError carries the HTTP status and the API's machine
|
|
3
|
+
* `code` (e.g. `room_not_granted`, `cooldown_active`, `rate_limited`) so callers
|
|
4
|
+
* can branch without string-matching messages. Retry any 429 after
|
|
5
|
+
* `retryAfter` rather than matching one code: route throttles answer
|
|
6
|
+
* `rate_limited`, but public-room and quick-action limits carry their own
|
|
7
|
+
* (`public_room_rate_limited`, `cooldown_active`). The agent token is NEVER
|
|
8
|
+
* stored on or serialized through an error.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ApiErrorBody {
|
|
12
|
+
code?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
retry_after?: number;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The codes the `agent.room` gate emits on every ROOM-SCOPED route — the ones
|
|
21
|
+
* whose path carries an invite code: `rooms.get()`, `actions.list()`,
|
|
22
|
+
* `actions.trigger()`, `broadcast()`, the webhook calls, and
|
|
23
|
+
* `liveStatus.get()`/`liveStatus.push()`. HTTP 403.
|
|
24
|
+
*
|
|
25
|
+
* NOT reachable from `handoffs.create()`: `POST /api/agent/handoffs` names no
|
|
26
|
+
* room in its path and so is not behind that gate.
|
|
27
|
+
*
|
|
28
|
+
* Server: `EnsureAgentRoomAccess` middleware.
|
|
29
|
+
*/
|
|
30
|
+
export const ROOM_SCOPED_ERROR_CODES = [
|
|
31
|
+
// The room is outside the grant the human gave this agent when connecting.
|
|
32
|
+
'room_not_granted',
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The machine `code`s a handoff create/read can surface on a {@link PingRoomError}.
|
|
37
|
+
* Branch on `error.code` (and `error.status`) instead of the message.
|
|
38
|
+
*
|
|
39
|
+
* Authentication and schema-validation failures may have no machine code; use
|
|
40
|
+
* `status` for those responses. This union holds the codes specific to handoffs.
|
|
41
|
+
* Codes any route can answer (`rate_limited`, `feature_disabled`) are in
|
|
42
|
+
* {@link COMMON_ERROR_CODES}.
|
|
43
|
+
*
|
|
44
|
+
* Server: `AgentHandoffController`, `HandoffService`, `RequestIdempotency`, the
|
|
45
|
+
* `agent.scope`/`agent.quota` middleware, and the `RecipientNotReady` /
|
|
46
|
+
* `CapabilityCheckUnavailable` renderers in `bootstrap/app.php`.
|
|
47
|
+
*/
|
|
48
|
+
export const HANDOFF_ERROR_CODES = [
|
|
49
|
+
'feature_temporarily_unavailable',
|
|
50
|
+
'handoff_room_unsupported',
|
|
51
|
+
'invalid_target',
|
|
52
|
+
// The agent has no delivery room designated; the human must pick one.
|
|
53
|
+
'no_room_configured',
|
|
54
|
+
// An agent may hand off only to the account that connected it.
|
|
55
|
+
'target_not_permitted',
|
|
56
|
+
'recipient_not_ready',
|
|
57
|
+
'idempotency_conflict',
|
|
58
|
+
'invalid_idempotency_key',
|
|
59
|
+
'capability_check_unavailable',
|
|
60
|
+
'insufficient_scope',
|
|
61
|
+
'free_limit_reached',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The coded failures of the Inbox activation path (`inbox.ensure()` /
|
|
66
|
+
* `inbox.activate()`). All HTTP 409 except `feature_temporarily_unavailable`,
|
|
67
|
+
* which is 422.
|
|
68
|
+
*
|
|
69
|
+
* Server: `AgentInboxController`.
|
|
70
|
+
*/
|
|
71
|
+
export const AGENT_INBOX_ERROR_CODES = [
|
|
72
|
+
// Already activated, but the original connection-test record is gone.
|
|
73
|
+
'activation_evidence_unavailable',
|
|
74
|
+
// The human must pick the room this agent pings them in.
|
|
75
|
+
'no_room_configured',
|
|
76
|
+
// The chosen room is muted, or the human is no longer a member of it.
|
|
77
|
+
'activation_room_muted',
|
|
78
|
+
// No device could render the activation Question.
|
|
79
|
+
'activation_delivery_unavailable',
|
|
80
|
+
'feature_temporarily_unavailable',
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The coded failures of the quick-action surface (`actions.*` and a
|
|
85
|
+
* `broadcast()` that names an `action_number`).
|
|
86
|
+
*
|
|
87
|
+
* Server: `Room::assertQuickActionAccessible/Configured`, `QuickActionDispatcher`
|
|
88
|
+
* and `QuickActionLayoutService`.
|
|
89
|
+
*/
|
|
90
|
+
export const QUICK_ACTION_ERROR_CODES = [
|
|
91
|
+
// 422 — the action has an input_type and the press carried no matching detail.
|
|
92
|
+
'quick_action_input_required',
|
|
93
|
+
// 422 — photo/pdf actions accept only jpg/png or pdf attachments.
|
|
94
|
+
'quick_action_input_type',
|
|
95
|
+
// 404 — the slot is reserved but disabled (empty label and icon).
|
|
96
|
+
'action_not_configured',
|
|
97
|
+
// 403 — slots 5–16 and layout edits need the ROOM OWNER on Pro; attachments need this account.
|
|
98
|
+
'pro_required',
|
|
99
|
+
// 409 — base_action_ids / quick_action_id no longer match the stored layout.
|
|
100
|
+
'quick_action_layout_changed',
|
|
101
|
+
// 409 — a time trigger, webhook, agent binding or running live update still uses the page.
|
|
102
|
+
'quick_action_page_in_use',
|
|
103
|
+
// 403 — `actions.trigger()` with trigger_source 'location' from anyone but the room owner.
|
|
104
|
+
'location_trigger_owner_only',
|
|
105
|
+
// 409 — `actions.trigger()` with trigger_source 'location' in a room with no enabled
|
|
106
|
+
// location trigger, or one that does not listen for the reported crossing.
|
|
107
|
+
'location_trigger_inactive',
|
|
108
|
+
] as const;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The coded refusals of the "Who's on it?" surface (`live.claim()`, and a
|
|
112
|
+
* `live.push()` that starts a `claim` stream).
|
|
113
|
+
*
|
|
114
|
+
* Server: `ClaimService`, `ClaimService::startingBlock()`.
|
|
115
|
+
*/
|
|
116
|
+
export const LIVE_CLAIM_ERROR_CODES = [
|
|
117
|
+
// 409 — not allowed from the request's current state (someone else holds it,
|
|
118
|
+
// it already ended, or you are not who may do this). `body.claim` shows who.
|
|
119
|
+
'claim_conflict',
|
|
120
|
+
// 422 — the asker tried to claim their own request.
|
|
121
|
+
'claim_own_request',
|
|
122
|
+
// 422 — claim requests exist only in private standard rooms.
|
|
123
|
+
'claim_private_rooms_only',
|
|
124
|
+
// 404 — no claim request with that notification id in this room.
|
|
125
|
+
'claim_not_found',
|
|
126
|
+
// 422, on create — the request text is missing or over 120 characters.
|
|
127
|
+
'claim_request_invalid',
|
|
128
|
+
// 422, on create — the first ping must be `running`.
|
|
129
|
+
'claim_must_start_running',
|
|
130
|
+
// 422, on create — `deadline_at` is in the past or more than 8 hours away.
|
|
131
|
+
'claim_deadline_out_of_range',
|
|
132
|
+
] as const;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Codes any route can answer, whatever the surface, so they sit in no
|
|
136
|
+
* per-surface union.
|
|
137
|
+
*
|
|
138
|
+
* - `rate_limited` — 429, a route throttle (`throttle:N,1`). The body carries
|
|
139
|
+
* `retry_after`. Per-surface limits answer their own 429 code instead
|
|
140
|
+
* (`public_room_rate_limited`, `cooldown_active`), so retry any 429.
|
|
141
|
+
* - `feature_disabled` — 403, the operator switched the capability off for every
|
|
142
|
+
* client (attachments, links, locations, questions, public rooms, Location
|
|
143
|
+
* Pings, incoming webhooks, live progress, …). The body's `feature` names the
|
|
144
|
+
* switch (for example `sending.attachments`). Retrying cannot succeed.
|
|
145
|
+
*
|
|
146
|
+
* Server: the `ThrottleRequestsException` renderer in `bootstrap/app.php` and
|
|
147
|
+
* `ClientFeatureGate` (the `feature:` middleware and the request-level gates).
|
|
148
|
+
*/
|
|
149
|
+
export const COMMON_ERROR_CODES = ['rate_limited', 'feature_disabled'] as const;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Request-size failures. The server answers every oversized `api/*` body with
|
|
153
|
+
* `413 {"code":"payload_too_large"}`; the SDK refuses a >5 MiB attachment
|
|
154
|
+
* locally with `attachment_too_large` before any bytes are sent.
|
|
155
|
+
*/
|
|
156
|
+
export const REQUEST_LIMIT_ERROR_CODES = ['payload_too_large', 'attachment_too_large'] as const;
|
|
157
|
+
|
|
158
|
+
/** @see COMMON_ERROR_CODES */
|
|
159
|
+
export type CommonErrorCode = (typeof COMMON_ERROR_CODES)[number];
|
|
160
|
+
|
|
161
|
+
/** @see ROOM_SCOPED_ERROR_CODES */
|
|
162
|
+
export type RoomScopedErrorCode = (typeof ROOM_SCOPED_ERROR_CODES)[number];
|
|
163
|
+
|
|
164
|
+
/** @see QUICK_ACTION_ERROR_CODES */
|
|
165
|
+
export type QuickActionErrorCode = (typeof QUICK_ACTION_ERROR_CODES)[number];
|
|
166
|
+
|
|
167
|
+
/** @see REQUEST_LIMIT_ERROR_CODES */
|
|
168
|
+
export type RequestLimitErrorCode = (typeof REQUEST_LIMIT_ERROR_CODES)[number];
|
|
169
|
+
|
|
170
|
+
/** @see LIVE_CLAIM_ERROR_CODES */
|
|
171
|
+
export type LiveClaimErrorCode = (typeof LIVE_CLAIM_ERROR_CODES)[number];
|
|
172
|
+
|
|
173
|
+
/** @see HANDOFF_ERROR_CODES */
|
|
174
|
+
export type HandoffErrorCode = (typeof HANDOFF_ERROR_CODES)[number];
|
|
175
|
+
|
|
176
|
+
/** @see AGENT_INBOX_ERROR_CODES */
|
|
177
|
+
export type AgentInboxErrorCode = (typeof AGENT_INBOX_ERROR_CODES)[number];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Every machine `code` this SDK's typed unions cover, for callers that branch
|
|
181
|
+
* once across surfaces instead of per-endpoint.
|
|
182
|
+
*/
|
|
183
|
+
export type AgentErrorCode =
|
|
184
|
+
| HandoffErrorCode
|
|
185
|
+
| RoomScopedErrorCode
|
|
186
|
+
| AgentInboxErrorCode
|
|
187
|
+
| QuickActionErrorCode
|
|
188
|
+
| LiveClaimErrorCode
|
|
189
|
+
| RequestLimitErrorCode
|
|
190
|
+
| CommonErrorCode;
|
|
191
|
+
|
|
192
|
+
export interface PingRoomErrorInit {
|
|
193
|
+
status?: number;
|
|
194
|
+
code?: string | null;
|
|
195
|
+
retryAfter?: number | null;
|
|
196
|
+
body?: unknown;
|
|
197
|
+
/** The underlying failure, exposed as the standard `error.cause`. */
|
|
198
|
+
cause?: unknown;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class PingRoomError extends Error {
|
|
202
|
+
/** HTTP status, or 0 for client-side/network errors. */
|
|
203
|
+
readonly status: number;
|
|
204
|
+
/** Machine-readable code from the API body (`code` or `error`), or a client code. */
|
|
205
|
+
readonly code: string | null;
|
|
206
|
+
/** Seconds to wait before retrying, when the API or a Retry-After header provides it. */
|
|
207
|
+
readonly retryAfter: number | null;
|
|
208
|
+
/** The parsed response body (or raw text), for inspection. Contains no credentials. */
|
|
209
|
+
readonly body: unknown;
|
|
210
|
+
|
|
211
|
+
constructor(message: string, init: PingRoomErrorInit = {}) {
|
|
212
|
+
super(message);
|
|
213
|
+
this.name = 'PingRoomError';
|
|
214
|
+
this.status = init.status ?? 0;
|
|
215
|
+
this.code = init.code ?? null;
|
|
216
|
+
this.retryAfter = init.retryAfter ?? null;
|
|
217
|
+
this.body = init.body ?? null;
|
|
218
|
+
if (init.cause !== undefined) {
|
|
219
|
+
(this as { cause?: unknown }).cause = init.cause;
|
|
220
|
+
}
|
|
221
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static fromResponse(status: number, body: unknown, headers?: Headers): PingRoomError {
|
|
225
|
+
const b: ApiErrorBody = body && typeof body === 'object' ? (body as ApiErrorBody) : {};
|
|
226
|
+
const code =
|
|
227
|
+
(typeof b.code === 'string' && b.code) ||
|
|
228
|
+
(typeof b.error === 'string' && b.error) ||
|
|
229
|
+
null;
|
|
230
|
+
const message =
|
|
231
|
+
(typeof b.message === 'string' && b.message) ||
|
|
232
|
+
(code ? code : `HTTP ${status}`);
|
|
233
|
+
|
|
234
|
+
let retryAfter: number | null = typeof b.retry_after === 'number' ? b.retry_after : null;
|
|
235
|
+
if (retryAfter === null && headers) {
|
|
236
|
+
const h = headers.get('retry-after');
|
|
237
|
+
if (h && /^\d+$/.test(h.trim())) {
|
|
238
|
+
retryAfter = Number(h.trim());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new PingRoomError(message, { status, code, retryAfter, body });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Thrown when a request exceeds its timeout. */
|
|
247
|
+
export class PingRoomTimeoutError extends PingRoomError {
|
|
248
|
+
constructor(message = 'Request timed out') {
|
|
249
|
+
super(message, { code: 'timeout' });
|
|
250
|
+
this.name = 'PingRoomTimeoutError';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Thrown when the underlying fetch fails (DNS, connection, TLS, etc.). */
|
|
255
|
+
export class PingRoomNetworkError extends PingRoomError {
|
|
256
|
+
constructor(message: string, cause?: unknown) {
|
|
257
|
+
super(message, { code: 'network_error' });
|
|
258
|
+
this.name = 'PingRoomNetworkError';
|
|
259
|
+
if (cause !== undefined) {
|
|
260
|
+
(this as { cause?: unknown }).cause = cause;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export type AgentInboxActivationIncompleteReason =
|
|
266
|
+
| 'deadline_exceeded'
|
|
267
|
+
| 'answered_without_completion'
|
|
268
|
+
| 'expired'
|
|
269
|
+
| 'cancelled';
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The onboarding Question reached a non-activating terminal outcome, or the
|
|
273
|
+
* caller's overall activation deadline elapsed. The client's credential is not
|
|
274
|
+
* changed or revoked when this error is thrown.
|
|
275
|
+
*/
|
|
276
|
+
export class PingRoomActivationIncompleteError extends PingRoomError {
|
|
277
|
+
readonly reason: AgentInboxActivationIncompleteReason;
|
|
278
|
+
|
|
279
|
+
constructor(
|
|
280
|
+
reason: AgentInboxActivationIncompleteReason,
|
|
281
|
+
message: string,
|
|
282
|
+
details: Record<string, unknown> = {},
|
|
283
|
+
) {
|
|
284
|
+
super(message, {
|
|
285
|
+
code: 'inbox_activation_incomplete',
|
|
286
|
+
body: { reason, ...details },
|
|
287
|
+
});
|
|
288
|
+
this.name = 'PingRoomActivationIncompleteError';
|
|
289
|
+
this.reason = reason;
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { PingRoomError, PingRoomNetworkError, PingRoomTimeoutError } from './errors.js';
|
|
2
|
+
import { combineSignals } from './internal/async.js';
|
|
3
|
+
import { assertSecureUrl, refuseRedirect } from './internal/url.js';
|
|
4
|
+
import { invalidSuccessResponse } from './internal/validate.js';
|
|
5
|
+
import type { FetchLike } from './types.js';
|
|
6
|
+
import { VERSION } from './version.js';
|
|
7
|
+
|
|
8
|
+
export interface HttpClientOptions {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
token?: string | null;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
allowInsecure?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RequestOptions {
|
|
18
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
19
|
+
body?: unknown;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/** Send the Authorization header (default true). Public endpoints pass false. */
|
|
22
|
+
auth?: boolean;
|
|
23
|
+
/** Per-call timeout override in ms. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
29
|
+
|
|
30
|
+
/** Node's util.inspect hook, named through the global registry so no import is needed. */
|
|
31
|
+
const INSPECT: unique symbol = Symbol.for('nodejs.util.inspect.custom');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Thin authenticated fetch wrapper. Holds the token privately, enforces https
|
|
35
|
+
* for credentialed traffic, applies a timeout, and maps non-2xx responses to a
|
|
36
|
+
* PingRoomError. The token is never written into thrown errors or logs.
|
|
37
|
+
*/
|
|
38
|
+
export class HttpClient {
|
|
39
|
+
// An ES private field: TypeScript's `private` is compile-time only, so the
|
|
40
|
+
// token would otherwise reach JSON.stringify() and util.inspect() output.
|
|
41
|
+
#token: string | null;
|
|
42
|
+
private readonly baseUrl: string;
|
|
43
|
+
private readonly timeoutMs: number;
|
|
44
|
+
private readonly fetchImpl: FetchLike;
|
|
45
|
+
private readonly userAgent: string;
|
|
46
|
+
|
|
47
|
+
constructor(options: HttpClientOptions) {
|
|
48
|
+
const base = assertSecureUrl(options.baseUrl, options.allowInsecure ?? false);
|
|
49
|
+
// Normalise: keep origin + any path prefix, drop a trailing slash.
|
|
50
|
+
this.baseUrl = (base.origin + base.pathname).replace(/\/$/, '');
|
|
51
|
+
this.#token = options.token ?? null;
|
|
52
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
53
|
+
this.fetchImpl = options.fetch ?? defaultFetch();
|
|
54
|
+
this.userAgent = options.userAgent ?? `pingroom-sdk/${VERSION}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setToken(token: string | null): void {
|
|
58
|
+
this.#token = token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getToken(): string | null {
|
|
62
|
+
return this.#token;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What a logger or JSON.stringify() sees: the API origin, never the credential. */
|
|
66
|
+
toJSON(): { baseUrl: string; token: '[redacted]' | null } {
|
|
67
|
+
return { baseUrl: this.baseUrl, token: this.#token === null ? null : '[redacted]' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[INSPECT](): string {
|
|
71
|
+
return `HttpClient { baseUrl: ${JSON.stringify(this.baseUrl)}, token: ${this.#token === null ? 'null' : '[redacted]'} }`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async request<T>(method: string, path: string, opts: RequestOptions = {}): Promise<T> {
|
|
75
|
+
return this.send(method, path, opts, (res) => this.parse<T>(res));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The undecoded Response, for endpoints that return bytes rather than JSON.
|
|
80
|
+
* Non-2xx still raises a PingRoomError so callers never read an error body
|
|
81
|
+
* as if it were content.
|
|
82
|
+
*/
|
|
83
|
+
async raw(method: string, path: string, opts: RequestOptions = {}): Promise<Response> {
|
|
84
|
+
return this.send(method, path, {
|
|
85
|
+
...opts,
|
|
86
|
+
headers: { Accept: '*/*', ...opts.headers },
|
|
87
|
+
}, async (res) => {
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
// Route the failure through the shared JSON error mapping.
|
|
90
|
+
await this.parse<unknown>(res);
|
|
91
|
+
}
|
|
92
|
+
return res;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async send<T>(
|
|
97
|
+
method: string,
|
|
98
|
+
path: string,
|
|
99
|
+
opts: RequestOptions,
|
|
100
|
+
consume: (res: Response) => Promise<T>,
|
|
101
|
+
): Promise<T> {
|
|
102
|
+
const url = this.buildUrl(path, opts.query);
|
|
103
|
+
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
'User-Agent': this.userAgent,
|
|
107
|
+
...opts.headers,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (opts.auth !== false) {
|
|
111
|
+
if (!this.#token) {
|
|
112
|
+
throw new PingRoomError(
|
|
113
|
+
'This call requires an agent token. Pass `token` to the client or call setToken().',
|
|
114
|
+
{ code: 'no_token' },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
headers['Authorization'] = `Bearer ${this.#token}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// FormData is passed through untouched: the runtime has to generate the
|
|
121
|
+
// multipart boundary itself, so setting Content-Type here would corrupt it.
|
|
122
|
+
let payload: string | FormData | undefined;
|
|
123
|
+
if (opts.body instanceof FormData) {
|
|
124
|
+
payload = opts.body;
|
|
125
|
+
} else if (opts.body !== undefined) {
|
|
126
|
+
headers['Content-Type'] = 'application/json';
|
|
127
|
+
payload = JSON.stringify(opts.body);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timer = setTimeout(() => controller.abort(new PingRoomTimeoutError()), timeoutMs);
|
|
133
|
+
const signal = combineSignals(controller.signal, opts.signal);
|
|
134
|
+
|
|
135
|
+
// Called as a plain function, never as a method of this client: browsers
|
|
136
|
+
// and Workers reject a fetch whose receiver is not the global object.
|
|
137
|
+
const fetchImpl = this.fetchImpl;
|
|
138
|
+
try {
|
|
139
|
+
// A 307/308 can forward pairing secrets or uploaded bytes to another
|
|
140
|
+
// origin even when fetch strips the Authorization header. 'manual' plus
|
|
141
|
+
// an explicit refusal, because Workers reject redirect: 'error'.
|
|
142
|
+
const res = await fetchImpl(url, { method, headers, body: payload, signal, redirect: 'manual' });
|
|
143
|
+
refuseRedirect(res);
|
|
144
|
+
// Keep the deadline active while reading JSON, including error bodies.
|
|
145
|
+
return await consume(res);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (signal.aborted) {
|
|
148
|
+
throw signal.reason;
|
|
149
|
+
}
|
|
150
|
+
// Our own timeout reason propagates as the rejection.
|
|
151
|
+
if (err instanceof PingRoomError) {
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
if (isAbortError(err) || opts.signal?.aborted) {
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
throw new PingRoomNetworkError(`Network request failed: ${errMessage(err)}`, err);
|
|
158
|
+
} finally {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async parse<T>(res: Response): Promise<T> {
|
|
164
|
+
if (res.status === 204) {
|
|
165
|
+
return undefined as T;
|
|
166
|
+
}
|
|
167
|
+
const text = await res.text();
|
|
168
|
+
let json: unknown = null;
|
|
169
|
+
if (text.trim()) {
|
|
170
|
+
try {
|
|
171
|
+
json = JSON.parse(text);
|
|
172
|
+
} catch {
|
|
173
|
+
// A non-JSON error stays text for the error mapping below; a non-JSON
|
|
174
|
+
// success is a proxy's page, not data.
|
|
175
|
+
if (res.ok) {
|
|
176
|
+
throw invalidSuccessResponse(res.status, 'a JSON body', text);
|
|
177
|
+
}
|
|
178
|
+
json = text;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
throw PingRoomError.fromResponse(res.status, json, res.headers);
|
|
183
|
+
}
|
|
184
|
+
return json as T;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private buildUrl(path: string, query?: RequestOptions['query']): string {
|
|
188
|
+
// Every call is relative to the base URL the token was configured for. An
|
|
189
|
+
// absolute URL would carry the Bearer token to another origin unchecked.
|
|
190
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path)) {
|
|
191
|
+
throw new PingRoomError('Request paths must be relative to the API base URL.', { code: 'invalid_request' });
|
|
192
|
+
}
|
|
193
|
+
const full = this.baseUrl + (path.startsWith('/') ? path : `/${path}`);
|
|
194
|
+
if (!query) {
|
|
195
|
+
return full;
|
|
196
|
+
}
|
|
197
|
+
const url = new URL(full);
|
|
198
|
+
for (const [k, v] of Object.entries(query)) {
|
|
199
|
+
if (v !== undefined) {
|
|
200
|
+
url.searchParams.set(k, String(v));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return url.toString();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The global fetch, bound so that calling it detached keeps a valid receiver. */
|
|
208
|
+
export function defaultFetch(): FetchLike {
|
|
209
|
+
const f = globalThis.fetch as unknown as FetchLike | undefined;
|
|
210
|
+
if (typeof f !== 'function') {
|
|
211
|
+
throw new PingRoomError(
|
|
212
|
+
'No fetch implementation available. Use Node >= 20, a modern browser, or pass `fetch`.',
|
|
213
|
+
{ code: 'no_fetch' },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return f.bind(globalThis);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isAbortError(err: unknown): boolean {
|
|
220
|
+
return Boolean(err) && typeof err === 'object' && (err as { name?: string }).name === 'AbortError';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function errMessage(err: unknown): string {
|
|
224
|
+
return err instanceof Error ? err.message : String(err);
|
|
225
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pingroom/sdk — official TypeScript/JavaScript SDK for the PingRoom agent API.
|
|
3
|
+
*
|
|
4
|
+
* import { PingRoom } from '@pingroom/sdk';
|
|
5
|
+
* const pr = new PingRoom({ token: process.env.PINGROOM_TOKEN });
|
|
6
|
+
* await pr.broadcast('ab12cd', { message: 'Deploy shipped ✅' });
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PingRoom } from './client.js';
|
|
10
|
+
|
|
11
|
+
export { PingRoom };
|
|
12
|
+
/** Alias for {@link PingRoom}. */
|
|
13
|
+
export { PingRoom as PingRoomClient } from './client.js';
|
|
14
|
+
|
|
15
|
+
export { McpClient } from './mcp.js';
|
|
16
|
+
export type { JsonRpcError, McpInitializeResult, McpTool, McpToolResult } from './mcp.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
PingRoomError,
|
|
20
|
+
PingRoomTimeoutError,
|
|
21
|
+
PingRoomNetworkError,
|
|
22
|
+
PingRoomActivationIncompleteError,
|
|
23
|
+
AGENT_INBOX_ERROR_CODES,
|
|
24
|
+
COMMON_ERROR_CODES,
|
|
25
|
+
HANDOFF_ERROR_CODES,
|
|
26
|
+
LIVE_CLAIM_ERROR_CODES,
|
|
27
|
+
ROOM_SCOPED_ERROR_CODES,
|
|
28
|
+
QUICK_ACTION_ERROR_CODES,
|
|
29
|
+
REQUEST_LIMIT_ERROR_CODES,
|
|
30
|
+
} from './errors.js';
|
|
31
|
+
export type {
|
|
32
|
+
AgentErrorCode,
|
|
33
|
+
AgentInboxActivationIncompleteReason,
|
|
34
|
+
AgentInboxErrorCode,
|
|
35
|
+
ApiErrorBody,
|
|
36
|
+
CommonErrorCode,
|
|
37
|
+
HandoffErrorCode,
|
|
38
|
+
LiveClaimErrorCode,
|
|
39
|
+
PingRoomErrorInit,
|
|
40
|
+
QuickActionErrorCode,
|
|
41
|
+
RequestLimitErrorCode,
|
|
42
|
+
RoomScopedErrorCode,
|
|
43
|
+
} from './errors.js';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
verifyWebhookSignature,
|
|
47
|
+
sendIncomingWebhook,
|
|
48
|
+
WEBHOOK_SIGNATURE_HEADER,
|
|
49
|
+
WEBHOOK_SIGNATURE_V2_HEADER,
|
|
50
|
+
WEBHOOK_TIMESTAMP_HEADER,
|
|
51
|
+
WEBHOOK_DELIVERY_HEADER,
|
|
52
|
+
WEBHOOK_EVENT_HEADER,
|
|
53
|
+
INCOMING_WEBHOOK_FIELDS,
|
|
54
|
+
} from './webhooks.js';
|
|
55
|
+
export type {
|
|
56
|
+
VerifyWebhookSignatureOptions,
|
|
57
|
+
IncomingWebhookPayload,
|
|
58
|
+
SendIncomingWebhookOptions,
|
|
59
|
+
IncomingWebhookResult,
|
|
60
|
+
} from './webhooks.js';
|
|
61
|
+
|
|
62
|
+
export { linkPing } from './linkPing.js';
|
|
63
|
+
export type { LinkPingInput, LinkPingData } from './linkPing.js';
|
|
64
|
+
|
|
65
|
+
export { locationPing, extractLocationPing } from './locationPing.js';
|
|
66
|
+
export type {
|
|
67
|
+
LocationPingInput,
|
|
68
|
+
LocationPingLocation,
|
|
69
|
+
LocationPingData,
|
|
70
|
+
} from './locationPing.js';
|
|
71
|
+
|
|
72
|
+
export { liveStatus } from './liveStatus.js';
|
|
73
|
+
export type {
|
|
74
|
+
LiveActivityTemplate,
|
|
75
|
+
LiveStatus,
|
|
76
|
+
LiveStatusState,
|
|
77
|
+
LiveStatusCategory,
|
|
78
|
+
LiveStatusMetric,
|
|
79
|
+
LiveStatusOption,
|
|
80
|
+
LiveStatusSide,
|
|
81
|
+
LiveClaim,
|
|
82
|
+
LiveClaimAction,
|
|
83
|
+
LiveClaimParty,
|
|
84
|
+
LiveClaimRefusal,
|
|
85
|
+
LiveClaimResult,
|
|
86
|
+
LiveClaimStatus,
|
|
87
|
+
LiveClaimStreamState,
|
|
88
|
+
LiveStatusPing,
|
|
89
|
+
LiveStatusResult,
|
|
90
|
+
LiveStatusSnapshot,
|
|
91
|
+
} from './liveStatus.js';
|
|
92
|
+
|
|
93
|
+
export { VERSION } from './version.js';
|
|
94
|
+
|
|
95
|
+
export type * from './types.js';
|
|
96
|
+
|
|
97
|
+
export default PingRoom;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Merge two abort signals into one (uses AbortSignal.any when available). */
|
|
2
|
+
export function combineSignals(primary: AbortSignal, extra?: AbortSignal): AbortSignal {
|
|
3
|
+
if (!extra) {
|
|
4
|
+
return primary;
|
|
5
|
+
}
|
|
6
|
+
const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
|
|
7
|
+
if (typeof anyFn === 'function') {
|
|
8
|
+
return anyFn([primary, extra]);
|
|
9
|
+
}
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const forward = (s: AbortSignal): void => {
|
|
12
|
+
if (!controller.signal.aborted) {
|
|
13
|
+
controller.abort(s.reason);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
for (const s of [primary, extra]) {
|
|
17
|
+
if (s.aborted) {
|
|
18
|
+
forward(s);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
s.addEventListener('abort', () => forward(s), { once: true });
|
|
22
|
+
}
|
|
23
|
+
return controller.signal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolve after `ms`, or early (without rejecting) if `signal` aborts. */
|
|
27
|
+
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
28
|
+
return new Promise<void>((resolve) => {
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
cleanup();
|
|
35
|
+
resolve();
|
|
36
|
+
}, ms);
|
|
37
|
+
const onAbort = (): void => {
|
|
38
|
+
cleanup();
|
|
39
|
+
resolve();
|
|
40
|
+
};
|
|
41
|
+
const cleanup = (): void => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
signal?.removeEventListener('abort', onAbort);
|
|
44
|
+
};
|
|
45
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Throw the signal's reason once it has aborted. A missing signal never throws. */
|
|
50
|
+
export function throwIfAborted(signal?: AbortSignal): void {
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
throw signal.reason ?? new Error('aborted');
|
|
53
|
+
}
|
|
54
|
+
}
|