@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.
Files changed (191) hide show
  1. package/README.md +288 -44
  2. package/dist/client.d.ts +27 -312
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +33 -1154
  5. package/dist/client.js.map +1 -1
  6. package/dist/errors.d.ts +40 -6
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +56 -4
  9. package/dist/errors.js.map +1 -1
  10. package/dist/http.d.ts +12 -1
  11. package/dist/http.d.ts.map +1 -1
  12. package/dist/http.js +47 -18
  13. package/dist/http.js.map +1 -1
  14. package/dist/index.d.ts +3 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal/async.d.ts +2 -0
  19. package/dist/internal/async.d.ts.map +1 -1
  20. package/dist/internal/async.js +6 -0
  21. package/dist/internal/async.js.map +1 -1
  22. package/dist/internal/guards.d.ts +16 -0
  23. package/dist/internal/guards.d.ts.map +1 -1
  24. package/dist/internal/guards.js +22 -1
  25. package/dist/internal/guards.js.map +1 -1
  26. package/dist/internal/request.d.ts +41 -0
  27. package/dist/internal/request.d.ts.map +1 -0
  28. package/dist/internal/request.js +78 -0
  29. package/dist/internal/request.js.map +1 -0
  30. package/dist/internal/retry.d.ts +65 -0
  31. package/dist/internal/retry.d.ts.map +1 -0
  32. package/dist/internal/retry.js +126 -0
  33. package/dist/internal/retry.js.map +1 -0
  34. package/dist/internal/url.d.ts +9 -0
  35. package/dist/internal/url.d.ts.map +1 -1
  36. package/dist/internal/url.js +25 -0
  37. package/dist/internal/url.js.map +1 -1
  38. package/dist/internal/validate.d.ts +13 -0
  39. package/dist/internal/validate.d.ts.map +1 -0
  40. package/dist/internal/validate.js +36 -0
  41. package/dist/internal/validate.js.map +1 -0
  42. package/dist/linkPing.d.ts.map +1 -1
  43. package/dist/linkPing.js +6 -2
  44. package/dist/linkPing.js.map +1 -1
  45. package/dist/liveStatus.d.ts +106 -11
  46. package/dist/liveStatus.d.ts.map +1 -1
  47. package/dist/liveStatus.js +20 -3
  48. package/dist/liveStatus.js.map +1 -1
  49. package/dist/mcp.d.ts.map +1 -1
  50. package/dist/mcp.js +40 -10
  51. package/dist/mcp.js.map +1 -1
  52. package/dist/namespaces/actions.d.ts +50 -0
  53. package/dist/namespaces/actions.d.ts.map +1 -0
  54. package/dist/namespaces/actions.js +219 -0
  55. package/dist/namespaces/actions.js.map +1 -0
  56. package/dist/namespaces/approvals.d.ts +23 -0
  57. package/dist/namespaces/approvals.d.ts.map +1 -0
  58. package/dist/namespaces/approvals.js +78 -0
  59. package/dist/namespaces/approvals.js.map +1 -0
  60. package/dist/namespaces/attachments.d.ts +37 -0
  61. package/dist/namespaces/attachments.d.ts.map +1 -0
  62. package/dist/namespaces/attachments.js +91 -0
  63. package/dist/namespaces/attachments.js.map +1 -0
  64. package/dist/namespaces/auth.d.ts +58 -0
  65. package/dist/namespaces/auth.d.ts.map +1 -0
  66. package/dist/namespaces/auth.js +242 -0
  67. package/dist/namespaces/auth.js.map +1 -0
  68. package/dist/namespaces/directory.d.ts +10 -0
  69. package/dist/namespaces/directory.d.ts.map +1 -0
  70. package/dist/namespaces/directory.js +19 -0
  71. package/dist/namespaces/directory.js.map +1 -0
  72. package/dist/namespaces/handoffs.d.ts +47 -0
  73. package/dist/namespaces/handoffs.d.ts.map +1 -0
  74. package/dist/namespaces/handoffs.js +126 -0
  75. package/dist/namespaces/handoffs.js.map +1 -0
  76. package/dist/namespaces/inbox.d.ts +17 -0
  77. package/dist/namespaces/inbox.d.ts.map +1 -0
  78. package/dist/namespaces/inbox.js +192 -0
  79. package/dist/namespaces/inbox.js.map +1 -0
  80. package/dist/namespaces/live.d.ts +57 -0
  81. package/dist/namespaces/live.d.ts.map +1 -0
  82. package/dist/namespaces/live.js +116 -0
  83. package/dist/namespaces/live.js.map +1 -0
  84. package/dist/namespaces/notifications.d.ts +25 -0
  85. package/dist/namespaces/notifications.d.ts.map +1 -0
  86. package/dist/namespaces/notifications.js +128 -0
  87. package/dist/namespaces/notifications.js.map +1 -0
  88. package/dist/namespaces/profile.d.ts +19 -0
  89. package/dist/namespaces/profile.d.ts.map +1 -0
  90. package/dist/namespaces/profile.js +22 -0
  91. package/dist/namespaces/profile.js.map +1 -0
  92. package/dist/namespaces/questions.d.ts +41 -0
  93. package/dist/namespaces/questions.d.ts.map +1 -0
  94. package/dist/namespaces/questions.js +107 -0
  95. package/dist/namespaces/questions.js.map +1 -0
  96. package/dist/namespaces/rooms.d.ts +13 -0
  97. package/dist/namespaces/rooms.d.ts.map +1 -0
  98. package/dist/namespaces/rooms.js +41 -0
  99. package/dist/namespaces/rooms.js.map +1 -0
  100. package/dist/namespaces/webhooks.d.ts +11 -0
  101. package/dist/namespaces/webhooks.d.ts.map +1 -0
  102. package/dist/namespaces/webhooks.js +32 -0
  103. package/dist/namespaces/webhooks.js.map +1 -0
  104. package/dist/types/approvals.d.ts +62 -0
  105. package/dist/types/approvals.d.ts.map +1 -0
  106. package/dist/types/approvals.js +2 -0
  107. package/dist/types/approvals.js.map +1 -0
  108. package/dist/types/auth.d.ts +185 -0
  109. package/dist/types/auth.d.ts.map +1 -0
  110. package/dist/types/auth.js +2 -0
  111. package/dist/types/auth.js.map +1 -0
  112. package/dist/types/common.d.ts +43 -0
  113. package/dist/types/common.d.ts.map +1 -0
  114. package/dist/types/common.js +2 -0
  115. package/dist/types/common.js.map +1 -0
  116. package/dist/types/handoffs.d.ts +121 -0
  117. package/dist/types/handoffs.d.ts.map +1 -0
  118. package/dist/types/handoffs.js +2 -0
  119. package/dist/types/handoffs.js.map +1 -0
  120. package/dist/types/inbox.d.ts +51 -0
  121. package/dist/types/inbox.d.ts.map +1 -0
  122. package/dist/types/inbox.js +2 -0
  123. package/dist/types/inbox.js.map +1 -0
  124. package/dist/types/notifications.d.ts +376 -0
  125. package/dist/types/notifications.d.ts.map +1 -0
  126. package/dist/types/notifications.js +2 -0
  127. package/dist/types/notifications.js.map +1 -0
  128. package/dist/types/profile.d.ts +30 -0
  129. package/dist/types/profile.d.ts.map +1 -0
  130. package/dist/types/profile.js +3 -0
  131. package/dist/types/profile.js.map +1 -0
  132. package/dist/types/questions.d.ts +210 -0
  133. package/dist/types/questions.d.ts.map +1 -0
  134. package/dist/types/questions.js +2 -0
  135. package/dist/types/questions.js.map +1 -0
  136. package/dist/types/rooms.d.ts +163 -0
  137. package/dist/types/rooms.d.ts.map +1 -0
  138. package/dist/types/rooms.js +3 -0
  139. package/dist/types/rooms.js.map +1 -0
  140. package/dist/types.d.ts +9 -1035
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/dist/webhooks.d.ts +32 -5
  145. package/dist/webhooks.d.ts.map +1 -1
  146. package/dist/webhooks.js +44 -14
  147. package/dist/webhooks.js.map +1 -1
  148. package/package.json +4 -2
  149. package/src/client.ts +115 -0
  150. package/src/errors.ts +291 -0
  151. package/src/http.ts +225 -0
  152. package/src/index.ts +97 -0
  153. package/src/internal/async.ts +54 -0
  154. package/src/internal/guards.ts +129 -0
  155. package/src/internal/request.ts +97 -0
  156. package/src/internal/retry.ts +157 -0
  157. package/src/internal/url.ts +78 -0
  158. package/src/internal/validate.ts +42 -0
  159. package/src/linkPing.ts +56 -0
  160. package/src/liveStatus.ts +337 -0
  161. package/src/locationPing.ts +101 -0
  162. package/src/mcp.ts +161 -0
  163. package/src/namespaces/actions.ts +234 -0
  164. package/src/namespaces/approvals.ts +104 -0
  165. package/src/namespaces/attachments.ts +106 -0
  166. package/src/namespaces/auth.ts +284 -0
  167. package/src/namespaces/directory.ts +21 -0
  168. package/src/namespaces/handoffs.ts +160 -0
  169. package/src/namespaces/inbox.ts +253 -0
  170. package/src/namespaces/live.ts +134 -0
  171. package/src/namespaces/notifications.ts +152 -0
  172. package/src/namespaces/profile.ts +24 -0
  173. package/src/namespaces/questions.ts +133 -0
  174. package/src/namespaces/rooms.ts +53 -0
  175. package/src/namespaces/webhooks.ts +38 -0
  176. package/src/types/approvals.ts +67 -0
  177. package/src/types/auth.ts +202 -0
  178. package/src/types/common.ts +74 -0
  179. package/src/types/handoffs.ts +148 -0
  180. package/src/types/inbox.ts +64 -0
  181. package/src/types/notifications.ts +393 -0
  182. package/src/types/profile.ts +37 -0
  183. package/src/types/questions.ts +226 -0
  184. package/src/types/rooms.ts +169 -0
  185. package/src/types.ts +17 -0
  186. package/src/version.ts +2 -0
  187. package/src/webhooks.ts +373 -0
  188. package/dist/liveActivity.d.ts +0 -101
  189. package/dist/liveActivity.d.ts.map +0 -1
  190. package/dist/liveActivity.js +0 -83
  191. package/dist/liveActivity.js.map +0 -1
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Webhook helpers — usable standalone, no client/token required.
3
+ *
4
+ * - verifyWebhookSignature: verify an OUTGOING webhook PingRoom sent you.
5
+ * New deliveries carry both the legacy timestamp/body HMAC and a v2 HMAC
6
+ * that also binds the delivery ID. Verify over the EXACT raw request body
7
+ * bytes — re-serializing the parsed JSON will not match.
8
+ *
9
+ * - sendIncomingWebhook: fire a room's INCOMING webhook (the URL carries its
10
+ * own secret, so no agent token is needed) — the one-line CI/deploy ping.
11
+ * Pass a top-level `live_status` to drive a Live Activity instead.
12
+ */
13
+
14
+ import { PingRoomError, PingRoomTimeoutError } from './errors.js';
15
+ import { defaultFetch } from './http.js';
16
+ import { combineSignals } from './internal/async.js';
17
+ import {
18
+ assertActionNumber,
19
+ assertMaxLength,
20
+ assertStructuredData,
21
+ MAX_PING_TITLE_LENGTH,
22
+ MAX_PUBLIC_PING_MESSAGE_LENGTH,
23
+ normalizeIdempotencyKey,
24
+ } from './internal/guards.js';
25
+ import { assertSecureUrl, redactUrlInMessage, refuseRedirect } from './internal/url.js';
26
+ import { invalidSuccessResponse, isRecord } from './internal/validate.js';
27
+ import type { LiveStatus } from './liveStatus.js';
28
+ import type { AckMode, ActionState, FetchLike, JsonObject } from './types.js';
29
+ import { VERSION } from './version.js';
30
+
31
+ export const WEBHOOK_SIGNATURE_HEADER = 'X-PingRoom-Signature';
32
+ export const WEBHOOK_SIGNATURE_V2_HEADER = 'X-PingRoom-Signature-V2';
33
+ export const WEBHOOK_TIMESTAMP_HEADER = 'X-PingRoom-Timestamp';
34
+ export const WEBHOOK_DELIVERY_HEADER = 'X-PingRoom-Delivery';
35
+ export const WEBHOOK_EVENT_HEADER = 'X-PingRoom-Event';
36
+
37
+ export interface VerifyWebhookSignatureOptions {
38
+ /**
39
+ * The raw request body — the exact bytes received, NOT a re-stringified
40
+ * object. A string, an ArrayBuffer or a typed array/Buffer. Anything else
41
+ * (an `{}` or `undefined` from a JSON/raw body parser that did not capture
42
+ * the body) throws a TypeError rather than failing verification.
43
+ */
44
+ payload: string | ArrayBuffer | ArrayBufferView;
45
+ /**
46
+ * Legacy X-PingRoom-Signature value (with or without the `sha256=` prefix).
47
+ * Tried only when `signatureV2` is absent and `requireV2` is not set.
48
+ */
49
+ signature?: string | null;
50
+ /** X-PingRoom-Signature-V2 value. When present, v2 MUST verify; v1 is not tried. */
51
+ signatureV2?: string | null;
52
+ /**
53
+ * Reject a delivery that carries no v2 signature instead of falling back to
54
+ * the legacy v1 HMAC. v1 does not sign `X-PingRoom-Delivery`, so a captured
55
+ * body replayed without its v2 header under a new delivery id still passes
56
+ * v1. Every current delivery carries v2: set this to `true`, and always set
57
+ * it when you dedupe on the delivery id. Default `false` for receivers that
58
+ * predate v2.
59
+ */
60
+ requireV2?: boolean;
61
+ /** The X-PingRoom-Timestamp header value (Unix seconds). */
62
+ timestamp: string | number | null | undefined;
63
+ /** X-PingRoom-Delivery value required by the v2 signed message. */
64
+ deliveryId?: string | null;
65
+ /** The webhook's signing_secret. */
66
+ secret: string;
67
+ /** Allowed timestamp skew in seconds. Defaults to 300 (five minutes). */
68
+ maxAgeSeconds?: number;
69
+ }
70
+
71
+ /**
72
+ * Constant-time verification of an outgoing-webhook signature. Returns a
73
+ * boolean and never throws on mismatch. Pass `requireV2: true` so a delivery
74
+ * without the v2 signature is refused rather than checked against v1.
75
+ *
76
+ * Throws a TypeError when `payload` is not a string, ArrayBuffer or typed
77
+ * array: a body parser that did not capture the raw bytes is a bug in the
78
+ * receiver, and must not look the same as a forged signature.
79
+ */
80
+ export async function verifyWebhookSignature(opts: VerifyWebhookSignatureOptions): Promise<boolean> {
81
+ assertRawPayload(opts.payload);
82
+ const hasV2Signature = opts.signatureV2 !== null && opts.signatureV2 !== undefined;
83
+ if (opts.requireV2 === true && !hasV2Signature) {
84
+ return false;
85
+ }
86
+ const selectedSignature = hasV2Signature ? opts.signatureV2 : opts.signature;
87
+ if (!selectedSignature || !opts.secret) {
88
+ return false;
89
+ }
90
+ const timestamp = normalizeTimestamp(opts.timestamp);
91
+ if (timestamp === null) {
92
+ return false;
93
+ }
94
+ const maxAgeSeconds = opts.maxAgeSeconds ?? 300;
95
+ if (!Number.isFinite(maxAgeSeconds) || maxAgeSeconds < 0) {
96
+ return false;
97
+ }
98
+ if (Math.abs(Date.now() / 1000 - Number(timestamp)) > maxAgeSeconds) {
99
+ return false;
100
+ }
101
+ const provided = normalizeSignature(selectedSignature).toLowerCase();
102
+ if (!/^[0-9a-f]{64}$/.test(provided)) {
103
+ return false;
104
+ }
105
+ let message: Uint8Array;
106
+ if (hasV2Signature) {
107
+ const deliveryId = normalizeDeliveryId(opts.deliveryId);
108
+ if (deliveryId === null) {
109
+ return false;
110
+ }
111
+ message = signedMessage(`v2\n${timestamp}\n${deliveryId}\n`, opts.payload);
112
+ } else {
113
+ message = signedMessage(`${timestamp}.`, opts.payload);
114
+ }
115
+ const expected = await hmacSha256Hex(opts.secret, message);
116
+ return timingSafeEqualHex(provided, expected);
117
+ }
118
+
119
+ export interface IncomingWebhookPayload {
120
+ /** Visible Ping body (≤ 120 characters in private rooms; ≤ 160 in public rooms). */
121
+ message?: string;
122
+ /** Visible Ping title (≤ 40 characters). */
123
+ title?: string;
124
+ /** Optional Quick Ping preset (1–16). Null bypasses the webhook's stored preset. */
125
+ action?: number | null;
126
+ /** Per-ping emoji override (≤ 16 chars, so ZWJ sequences and skin tones fit). */
127
+ emoji?: string;
128
+ /** Per-ping catalog icon override — a v3 room-icon id, validated server-side. */
129
+ icon?: string;
130
+ /** Per-ping color override: 6-digit hex, optional leading `#`. */
131
+ color?: string;
132
+ /** Per-ping sound override — a canonical sound id (see constants/sounds.json). */
133
+ sound?: string;
134
+ data?: JsonObject;
135
+ correlation_id?: string;
136
+ reply_to?: string;
137
+ /**
138
+ * Drive a Live Activity through this webhook. MUST be top-level — the server
139
+ * detects `live_status` on the request body, and strips it from `data` so a
140
+ * legacy-path caller cannot spoof completion alerts. Sending it nested under
141
+ * `data` silently produces an ordinary ping, not a stream.
142
+ *
143
+ * Requires `correlation_id`. Build it with the `liveStatus.*` helpers.
144
+ * See https://pingroom.io/liveactivities.md
145
+ */
146
+ live_status?: LiveStatus;
147
+ /**
148
+ * Override the selected quick action and require acknowledgement for this
149
+ * ping. Does NOT raise the delivery priority — that is {@link is_urgent}.
150
+ */
151
+ requires_ack?: boolean;
152
+ /** Confirmation rule for a regular ping (not live_status); defaults to any. */
153
+ ack_mode?: AckMode;
154
+ /**
155
+ * Deliver time-sensitive so the ping breaks through Focus / Do Not Disturb.
156
+ * Payload-only and independent of `requires_ack`: a webhook trigger has no
157
+ * stored urgency policy to inherit from the quick action it fires.
158
+ */
159
+ is_urgent?: boolean;
160
+ /** Optional acknowledgement deadline in seconds. Omit/null for no deadline. */
161
+ ack_timeout_seconds?: number | null;
162
+ }
163
+
164
+ export interface SendIncomingWebhookOptions {
165
+ /**
166
+ * Sent as the Idempotency-Key header; identical keys within ~1h replay the
167
+ * first result. Omitted or empty-string keys send no header. Other keys must
168
+ * be 1–255 printable ASCII characters (spaces allowed), else `invalid_request`
169
+ * before sending.
170
+ */
171
+ idempotencyKey?: string;
172
+ timeoutMs?: number;
173
+ fetch?: FetchLike;
174
+ signal?: AbortSignal;
175
+ /** Allow plain-http webhook URLs to non-loopback hosts (off by default). */
176
+ allowInsecure?: boolean;
177
+ }
178
+
179
+ export interface IncomingWebhookResult {
180
+ success: boolean;
181
+ notification_id?: string;
182
+ message?: string;
183
+ action_state?: ActionState | null;
184
+ [key: string]: unknown;
185
+ }
186
+
187
+ /**
188
+ * Every field the incoming-webhook endpoint accepts, in the server's own order
189
+ * (`WebhookController::trigger`'s validate block, plus `live_status`, which it
190
+ * detects before that block and routes to the stream handler).
191
+ *
192
+ * The request body is built by walking this list rather than a hand-written run
193
+ * of `if`s. That run silently dropped whatever nobody remembered to add to it:
194
+ * first `live_status` (pre-0.3.1 — posted only `{correlation_id}`, fell into the
195
+ * legacy webhook branch, and returned `success: true` having created no activity
196
+ * at all), then `emoji`/`icon`/`color`/`sound`. Exported so a test can fail the
197
+ * moment this list and the server contract diverge again.
198
+ */
199
+ export const INCOMING_WEBHOOK_FIELDS = [
200
+ 'title',
201
+ 'message',
202
+ 'action',
203
+ 'emoji',
204
+ 'icon',
205
+ 'color',
206
+ 'sound',
207
+ 'data',
208
+ 'correlation_id',
209
+ 'reply_to',
210
+ 'is_urgent',
211
+ 'requires_ack',
212
+ 'ack_mode',
213
+ 'ack_timeout_seconds',
214
+ // Top level, never folded into `data` — the server routes on this key and
215
+ // strips it from `data` so a legacy-path caller cannot spoof completion alerts.
216
+ 'live_status',
217
+ ] as const satisfies readonly (keyof IncomingWebhookPayload)[];
218
+
219
+ /** POST to a room's incoming-webhook URL (which embeds the secret in its path). */
220
+ export async function sendIncomingWebhook(
221
+ webhookUrl: string,
222
+ payload: IncomingWebhookPayload,
223
+ options: SendIncomingWebhookOptions = {},
224
+ ): Promise<IncomingWebhookResult> {
225
+ const parsedUrl = assertSecureUrl(webhookUrl, options.allowInsecure ?? false);
226
+ // A webhook URL does not reveal room visibility, so validate the public
227
+ // ceiling here and let the server enforce 120 for private rooms.
228
+ assertMaxLength(payload.message, MAX_PUBLIC_PING_MESSAGE_LENGTH, 'message');
229
+ assertMaxLength(payload.title, MAX_PING_TITLE_LENGTH, 'title');
230
+ assertActionNumber(payload.action);
231
+ assertStructuredData(payload.data);
232
+
233
+ const body: Record<string, unknown> = {};
234
+ for (const field of INCOMING_WEBHOOK_FIELDS) {
235
+ const value = payload[field];
236
+ if (value !== undefined) body[field] = value;
237
+ }
238
+
239
+ const headers: Record<string, string> = {
240
+ 'Content-Type': 'application/json',
241
+ Accept: 'application/json',
242
+ 'User-Agent': `pingroom-sdk/${VERSION}`,
243
+ };
244
+ if (options.idempotencyKey) {
245
+ headers['Idempotency-Key'] = normalizeIdempotencyKey(options.idempotencyKey);
246
+ }
247
+
248
+ const fetchImpl = options.fetch ?? defaultFetch();
249
+
250
+ const controller = new AbortController();
251
+ const timer = setTimeout(() => controller.abort(new PingRoomTimeoutError()), options.timeoutMs ?? 30_000);
252
+ const signal = combineSignals(controller.signal, options.signal);
253
+
254
+ try {
255
+ // 'manual' plus an explicit refusal: Workers reject redirect: 'error', and
256
+ // a 307/308 must not replay this body to another origin.
257
+ const res = await fetchImpl(webhookUrl, { method: 'POST', headers, body: JSON.stringify(body), signal, redirect: 'manual' });
258
+ refuseRedirect(res);
259
+ const text = await res.text();
260
+ let json: unknown = null;
261
+ if (text) {
262
+ try {
263
+ json = JSON.parse(text);
264
+ } catch {
265
+ json = text;
266
+ }
267
+ }
268
+ if (!res.ok || (isRecord(json) && json['success'] === false)) {
269
+ throw PingRoomError.fromResponse(res.status, json, res.headers);
270
+ }
271
+ // Every success the endpoint sends is a JSON object with `success: true`.
272
+ // Anything else is a misrouted URL or a proxy's page, not a delivered Ping.
273
+ if (!isRecord(json) || json['success'] !== true) {
274
+ throw invalidSuccessResponse(res.status, 'a JSON object with `success: true`', json);
275
+ }
276
+ return json as IncomingWebhookResult;
277
+ } catch (err) {
278
+ if (signal.aborted) {
279
+ throw signal.reason;
280
+ }
281
+ if (err instanceof PingRoomError) {
282
+ throw err;
283
+ }
284
+ const reason = redactUrlInMessage(err instanceof Error ? err.message : String(err), parsedUrl, webhookUrl);
285
+ throw new PingRoomError(`Webhook request failed: ${reason}`, {
286
+ code: 'network_error',
287
+ });
288
+ } finally {
289
+ clearTimeout(timer);
290
+ }
291
+ }
292
+
293
+ // --- internals ------------------------------------------------------------
294
+
295
+ function normalizeSignature(sig: string): string {
296
+ const s = sig.trim();
297
+ return s.startsWith('sha256=') ? s.slice('sha256='.length) : s;
298
+ }
299
+
300
+ function normalizeTimestamp(timestamp: string | number | null | undefined): string | null {
301
+ if (timestamp === null || timestamp === undefined) {
302
+ return null;
303
+ }
304
+ const normalized = typeof timestamp === 'string' ? timestamp.trim() : String(timestamp);
305
+ if (!/^\d+$/.test(normalized)) {
306
+ return null;
307
+ }
308
+ const seconds = Number(normalized);
309
+ return Number.isSafeInteger(seconds) ? normalized : null;
310
+ }
311
+
312
+ function normalizeDeliveryId(deliveryId: string | null | undefined): string | null {
313
+ return typeof deliveryId === 'string' && /^[\x21-\x7e]{1,255}$/.test(deliveryId)
314
+ ? deliveryId
315
+ : null;
316
+ }
317
+
318
+ // `Object.prototype.toString` rather than `instanceof ArrayBuffer`: a buffer
319
+ // from another realm (a test sandbox, a worker) is still the raw body.
320
+ function isArrayBuffer(value: unknown): value is ArrayBuffer {
321
+ return Object.prototype.toString.call(value) === '[object ArrayBuffer]';
322
+ }
323
+
324
+ function assertRawPayload(payload: unknown): void {
325
+ if (typeof payload === 'string' || isArrayBuffer(payload) || ArrayBuffer.isView(payload)) {
326
+ return;
327
+ }
328
+ const received = payload === null ? 'null' : Array.isArray(payload) ? 'an array' : typeof payload;
329
+ throw new TypeError(
330
+ `verifyWebhookSignature: \`payload\` must be the raw request body as a string, ArrayBuffer or Buffer/Uint8Array, got ${received}. `
331
+ + 'Capture the raw bytes before any JSON parsing (for Express: express.raw({ type: \'application/json\' }) on this route).',
332
+ );
333
+ }
334
+
335
+ function signedMessage(prefixText: string, payload: string | ArrayBuffer | ArrayBufferView): Uint8Array {
336
+ const enc = new TextEncoder();
337
+ const prefix = enc.encode(prefixText);
338
+ const body = typeof payload === 'string'
339
+ ? enc.encode(payload)
340
+ : ArrayBuffer.isView(payload)
341
+ // The view's own bytes: a Buffer is a window onto a larger pool, and a
342
+ // DataView or Int16Array must not be read element by element.
343
+ ? new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength)
344
+ : new Uint8Array(payload);
345
+ const message = new Uint8Array(prefix.byteLength + body.byteLength);
346
+ message.set(prefix, 0);
347
+ message.set(body, prefix.byteLength);
348
+ return message;
349
+ }
350
+
351
+ async function hmacSha256Hex(secret: string, message: Uint8Array): Promise<string> {
352
+ const enc = new TextEncoder();
353
+ const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
354
+ const signature = await crypto.subtle.sign('HMAC', key, message as BufferSource);
355
+ const bytes = new Uint8Array(signature);
356
+ let hex = '';
357
+ for (const b of bytes) {
358
+ hex += b.toString(16).padStart(2, '0');
359
+ }
360
+ return hex;
361
+ }
362
+
363
+ /** Constant-time compare of two equal-length lowercase-hex strings. */
364
+ function timingSafeEqualHex(a: string, b: string): boolean {
365
+ if (a.length !== b.length) {
366
+ return false;
367
+ }
368
+ let diff = 0;
369
+ for (let i = 0; i < a.length; i++) {
370
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
371
+ }
372
+ return diff === 0;
373
+ }
@@ -1,101 +0,0 @@
1
- /**
2
- * Live Activity template builders for the PingRoom agent SDK.
3
- *
4
- * IMPORTANT — these builders do NOT start an activity. The server starts,
5
- * updates, and ends Live Activities only from a **live-status stream**: a
6
- * `live_status` object sent either to a room's incoming webhook or to the agent
7
- * route (see https://pingroom.io/liveactivities.md). A `live_activity` block
8
- * placed in a `broadcast()` / `ping()` `data` object is carried as opaque
9
- * structured data — clients only honor the server-built, top-level
10
- * `live_activity` push block.
11
- *
12
- * These builders produce that `{ attributes, content_state }` block in the
13
- * frozen native shape (mobile/modules/live-activity/src/LiveActivity.types.ts,
14
- * LIVE_ACTIVITY_TEMPLATES.md). Use them when you need the native contract —
15
- * e.g. constructing expected payloads in tests or tooling that speaks the
16
- * push-layer shape.
17
- *
18
- * To actually drive a Live Activity, use `liveStatus.*` + `pr.live.push()`
19
- * (agent credential, `pingroom:live:write`) or `sendIncomingWebhook()`.
20
- */
21
- export type LiveActivityTemplate = 'status' | 'steps' | 'progress' | 'metrics' | 'countdown' | 'question' | 'matchup';
22
- export type LiveActivityStatus = 'open' | 'done' | 'failed' | 'acked' | 'answered' | 'expired' | 'cancelled';
23
- export interface LiveActivityMetric {
24
- label: string;
25
- value: string;
26
- }
27
- export interface LiveActivityOption {
28
- value: string;
29
- label: string;
30
- }
31
- export interface LiveActivitySide {
32
- label: string;
33
- value: string;
34
- iconSvg?: string;
35
- }
36
- /** Identity fields common to every template (immutable for the activity's life). */
37
- export interface LiveActivityIdentity {
38
- roomCode: string;
39
- roomName: string;
40
- /** Hex accent — the palette room color, or brand red `#e33122`. */
41
- roomColor: string;
42
- roomIconSvg?: string;
43
- /** Reuse across updates to target the same activity; also links agent replies. */
44
- correlationId?: string;
45
- agentId?: string;
46
- kind?: string;
47
- }
48
- /** Content fields shared across templates. */
49
- interface CommonState {
50
- title: string;
51
- message?: string;
52
- status?: LiveActivityStatus;
53
- /** Semantic accent hex for this state only (e.g. deadline red). */
54
- accentOverride?: string;
55
- }
56
- /** The `data` fragment carrying a Live Activity. Spread into a ping's `data`. */
57
- export interface LiveActivityData {
58
- live_activity: {
59
- attributes: Record<string, unknown>;
60
- content_state: Record<string, unknown>;
61
- };
62
- }
63
- export declare const liveActivity: {
64
- /** Single hero line + status dot; optional live ETA (`etaAt`, epoch seconds). */
65
- status(input: LiveActivityIdentity & CommonState & {
66
- progress?: number;
67
- etaAt?: number;
68
- }): LiveActivityData;
69
- /** Checkpoint rail — the understand → plan → doc pattern. */
70
- steps(input: LiveActivityIdentity & CommonState & {
71
- steps: string[];
72
- currentStep: number;
73
- }): LiveActivityData;
74
- /** Determinate progress bar (0..1), optional `etaAt` countdown. */
75
- progress(input: LiveActivityIdentity & CommonState & {
76
- progress: number;
77
- etaAt?: number;
78
- }): LiveActivityData;
79
- /** Up to 3 live counters. */
80
- metrics(input: LiveActivityIdentity & CommonState & {
81
- metrics: LiveActivityMetric[];
82
- }): LiveActivityData;
83
- /** Big timer counting down to `deadlineAt` (epoch seconds). */
84
- countdown(input: LiveActivityIdentity & CommonState & {
85
- deadlineAt: number;
86
- }): LiveActivityData;
87
- /** Lock-screen ask: prompt + up to 4 options; `deadlineAt` shows time-left. */
88
- question(input: LiveActivityIdentity & CommonState & {
89
- prompt: string;
90
- options: LiveActivityOption[];
91
- deadlineAt?: number;
92
- }): LiveActivityData;
93
- /** Head-to-head: left · center · right. */
94
- matchup(input: LiveActivityIdentity & CommonState & {
95
- left: LiveActivitySide;
96
- right: LiveActivitySide;
97
- center?: string;
98
- }): LiveActivityData;
99
- };
100
- export {};
101
- //# sourceMappingURL=liveActivity.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"liveActivity.d.ts","sourceRoot":"","sources":["../src/liveActivity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,QAAQ,GACR,OAAO,GACP,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,SAAS,CAAC;AAEd,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,MAAM,GACN,QAAQ,GACR,OAAO,GACP,UAAU,GACV,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8CAA8C;AAC9C,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE;QACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC;CACH;AAgCD,eAAO,MAAM,YAAY;IACvB,iFAAiF;kBACnE,oBAAoB,GAAG,WAAW,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAKxF,6DAA6D;iBAChD,oBAAoB,GAAG,WAAW,GAAG;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAK1F,mEAAmE;oBACnD,oBAAoB,GAAG,WAAW,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAKzF,6BAA6B;mBACd,oBAAoB,GAAG,WAAW,GAAG;QAAE,OAAO,EAAE,kBAAkB,EAAE,CAAA;KAAE;IAKrF,+DAA+D;qBAC9C,oBAAoB,GAAG,WAAW,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE;IAK5E,+EAA+E;oBAEtE,oBAAoB,GACzB,WAAW,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,kBAAkB,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAMxF,2CAA2C;mBAElC,oBAAoB,GACzB,WAAW,GAAG;QAAE,IAAI,EAAE,gBAAgB,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAKvF,CAAC"}
@@ -1,83 +0,0 @@
1
- /**
2
- * Live Activity template builders for the PingRoom agent SDK.
3
- *
4
- * IMPORTANT — these builders do NOT start an activity. The server starts,
5
- * updates, and ends Live Activities only from a **live-status stream**: a
6
- * `live_status` object sent either to a room's incoming webhook or to the agent
7
- * route (see https://pingroom.io/liveactivities.md). A `live_activity` block
8
- * placed in a `broadcast()` / `ping()` `data` object is carried as opaque
9
- * structured data — clients only honor the server-built, top-level
10
- * `live_activity` push block.
11
- *
12
- * These builders produce that `{ attributes, content_state }` block in the
13
- * frozen native shape (mobile/modules/live-activity/src/LiveActivity.types.ts,
14
- * LIVE_ACTIVITY_TEMPLATES.md). Use them when you need the native contract —
15
- * e.g. constructing expected payloads in tests or tooling that speaks the
16
- * push-layer shape.
17
- *
18
- * To actually drive a Live Activity, use `liveStatus.*` + `pr.live.push()`
19
- * (agent credential, `pingroom:live:write`) or `sendIncomingWebhook()`.
20
- */
21
- function build(template, id, state) {
22
- const { title, message, status, accentOverride, ...rest } = state;
23
- return {
24
- live_activity: {
25
- attributes: {
26
- kind: id.kind ?? 'agent',
27
- template,
28
- roomCode: id.roomCode,
29
- roomName: id.roomName,
30
- roomColor: id.roomColor,
31
- ...(id.roomIconSvg ? { roomIconSvg: id.roomIconSvg } : {}),
32
- ...(id.correlationId ? { correlationId: id.correlationId } : {}),
33
- ...(id.agentId ? { agentId: id.agentId } : {}),
34
- },
35
- content_state: {
36
- status: status ?? 'open',
37
- title,
38
- message: message ?? '',
39
- ...(accentOverride ? { accentOverride } : {}),
40
- ...rest,
41
- updatedAt: Math.floor(Date.now() / 1000),
42
- },
43
- },
44
- };
45
- }
46
- export const liveActivity = {
47
- /** Single hero line + status dot; optional live ETA (`etaAt`, epoch seconds). */
48
- status(input) {
49
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
50
- return build('status', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
51
- },
52
- /** Checkpoint rail — the understand → plan → doc pattern. */
53
- steps(input) {
54
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
55
- return build('steps', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
56
- },
57
- /** Determinate progress bar (0..1), optional `etaAt` countdown. */
58
- progress(input) {
59
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
60
- return build('progress', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
61
- },
62
- /** Up to 3 live counters. */
63
- metrics(input) {
64
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
65
- return build('metrics', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
66
- },
67
- /** Big timer counting down to `deadlineAt` (epoch seconds). */
68
- countdown(input) {
69
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
70
- return build('countdown', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
71
- },
72
- /** Lock-screen ask: prompt + up to 4 options; `deadlineAt` shows time-left. */
73
- question(input) {
74
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
75
- return build('question', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
76
- },
77
- /** Head-to-head: left · center · right. */
78
- matchup(input) {
79
- const { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind, ...state } = input;
80
- return build('matchup', { roomCode, roomName, roomColor, roomIconSvg, correlationId, agentId, kind }, state);
81
- },
82
- };
83
- //# sourceMappingURL=liveActivity.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"liveActivity.js","sourceRoot":"","sources":["../src/liveActivity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgEH,SAAS,KAAK,CACZ,QAA8B,EAC9B,EAAwB,EACxB,KAA4C;IAE5C,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAClE,OAAO;QACL,aAAa,EAAE;YACb,UAAU,EAAE;gBACV,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,OAAO;gBACxB,QAAQ;gBACR,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1D,GAAG,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChE,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/C;YACD,aAAa,EAAE;gBACb,MAAM,EAAE,MAAM,IAAI,MAAM;gBACxB,KAAK;gBACL,OAAO,EAAE,OAAO,IAAI,EAAE;gBACtB,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7C,GAAG,IAAI;gBACP,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;aACzC;SACF;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,iFAAiF;IACjF,MAAM,CAAC,KAAiF;QACtF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAC9G,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,KAAoF;QACxF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAC7G,CAAC;IAED,mEAAmE;IACnE,QAAQ,CAAC,KAAgF;QACvF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAChH,CAAC;IAED,6BAA6B;IAC7B,OAAO,CAAC,KAA6E;QACnF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAC/G,CAAC;IAED,+DAA+D;IAC/D,SAAS,CAAC,KAAkE;QAC1E,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IACjH,CAAC;IAED,+EAA+E;IAC/E,QAAQ,CACN,KACsF;QAEtF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAChH,CAAC;IAED,2CAA2C;IAC3C,OAAO,CACL,KACoF;QAEpF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;QACrG,OAAO,KAAK,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;IAC/G,CAAC;CACF,CAAC"}