@sensiblestats/widget-sdk 0.74.0 → 0.75.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sensiblestats/widget-sdk
2
2
 
3
+ ## 0.75.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Personal greeting sections, a Favourites tab, and a combo_add event when a builder combo is added to the slip.
8
+
9
+ ## 0.74.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Send the visitor ids (`operatorUserId`, `anonymousUserId`) in the `X-SS-Operator-User-Id` and `X-SS-Anonymous-User-Id` request headers on every gateway call instead of the query string and JSON body. Blank ids, and ids the gateway would reject (non-ASCII or over 128 characters), send no header; `sendSignal` then falls back to the anonymous id. Public function signatures are unchanged.
14
+
3
15
  ## 0.74.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -22,13 +22,17 @@ for await (const ev of client.chat({ message: 'odds for Arsenal?' })) {
22
22
  await client.chat({ message: 'odds for Arsenal?' }, { onAnswer: a => render(a.text) })
23
23
 
24
24
  // Multi-turn
25
- const convo = client.conversation({ operatorUserId: 'u_42' })
25
+ const convo = client.conversation({ operatorUserId: '6f1c2a9e-4b7d-4e2a-9c3f-8a5d1e7b2c40' })
26
26
  await convo.send({ message: 'and their recent form?' })
27
27
 
28
28
  // Signals
29
- await client.sendSignal({ signalType: 'MatchPageView', entityType: 'Match', entityName: 'Arsenal vs Chelsea', operatorUserId: 'u_42' })
29
+ await client.sendSignal({ signalType: 'MatchPageView', entityType: 'Match', entityName: 'Arsenal vs Chelsea', operatorUserId: '6f1c2a9e-4b7d-4e2a-9c3f-8a5d1e7b2c40' })
30
30
  ```
31
31
 
32
+ Visitor ids (`operatorUserId`, `anonymousUserId`) are passed to the SDK as before. On the wire they travel in the `X-SS-Operator-User-Id` and `X-SS-Anonymous-User-Id` request headers on every gateway call, never in the URL or the JSON body; a blank id sends no header. Ids must be printable ASCII, at most 128 characters (surrounding spaces are trimmed); an id that does not qualify is not sent, as if it were blank.
33
+
34
+ `operatorUserId` is sent from the browser: use a random, non-guessable value you map to the user on your side (a UUID works), never an account number, email, username or sequential id.
35
+
32
36
  Page grounding: `ChatInput.page` is a `PageContext` (`{ sport: 'football' | 'nba', kind?: 'section' | 'competition' | 'match', matchId?, competitionId? }`). Send it on the session-start greeting only; the server ignores it on any other request. `ChatGrounding.sport` names the sport of a tapped grounding. Greeting dashboard tiles may carry `meta` (a short line in the hero's top row where a price would go) and `imageCodes` (team abbreviations drawn as text marks when there are no images).
33
37
 
34
38
  Errors are typed: `AuthError` (401), `ForbiddenError` (403), `RateLimitError` (429), `UpstreamError` (5xx), `NetworkError`, `ParseError`.
package/dist/index.cjs CHANGED
@@ -110,6 +110,23 @@ function headersFor(cfg, json) {
110
110
  if (cfg.client) h["X-SS-Client"] = cfg.client;
111
111
  return h;
112
112
  }
113
+ var OPERATOR_USER_ID_HEADER = "X-SS-Operator-User-Id";
114
+ var ANONYMOUS_USER_ID_HEADER = "X-SS-Anonymous-User-Id";
115
+ var SENDABLE_ID = /^[\x20-\x7E]{1,128}$/;
116
+ function isSendableId(id) {
117
+ const v = id?.trim();
118
+ return !!v && SENDABLE_ID.test(v);
119
+ }
120
+ function identityHeaders(operatorUserId, anonymousUserId) {
121
+ const h = {};
122
+ if (isSendableId(operatorUserId)) h[OPERATOR_USER_ID_HEADER] = operatorUserId.trim();
123
+ if (isSendableId(anonymousUserId)) h[ANONYMOUS_USER_ID_HEADER] = anonymousUserId.trim();
124
+ return h;
125
+ }
126
+ function splitIdentity(body) {
127
+ const { operatorUserId, anonymousUserId, ...rest } = body;
128
+ return { identity: identityHeaders(operatorUserId, anonymousUserId), rest };
129
+ }
113
130
  function endpoint(cfg, path) {
114
131
  return `${cfg.baseUrl.replace(/\/+$/, "")}/v1/${encodeURIComponent(cfg.operatorId)}/${path}`;
115
132
  }
@@ -151,11 +168,11 @@ async function chatError(res) {
151
168
  function isAbort(error) {
152
169
  return error instanceof Error && error.name === "AbortError";
153
170
  }
154
- async function send(cfg, path, body, signal) {
171
+ async function send(cfg, path, body, signal, identity = {}) {
155
172
  try {
156
173
  return await cfg.fetch(endpoint(cfg, path), {
157
174
  method: "POST",
158
- headers: headersFor(cfg, true),
175
+ headers: { ...headersFor(cfg, true), ...identity },
159
176
  body: JSON.stringify(body),
160
177
  signal
161
178
  });
@@ -165,24 +182,35 @@ async function send(cfg, path, body, signal) {
165
182
  }
166
183
  }
167
184
  async function postChat(cfg, input, signal) {
168
- const res = await send(cfg, "chat", input, signal);
185
+ const { userContext, ...rest } = input;
186
+ let body = rest;
187
+ let identity = {};
188
+ if (userContext) {
189
+ const split = splitIdentity(userContext);
190
+ identity = split.identity;
191
+ body = Object.keys(split.rest).length > 0 ? { ...rest, userContext: split.rest } : rest;
192
+ }
193
+ const res = await send(cfg, "chat", body, signal, identity);
169
194
  if (!res.ok || res.body == null) throw await chatError(res);
170
195
  return res;
171
196
  }
172
197
  async function postSignal(cfg, wireBody, signal) {
173
- const res = await send(cfg, "signals", wireBody, signal);
198
+ const { identity, rest } = splitIdentity(wireBody);
199
+ const res = await send(cfg, "signals", rest, signal, identity);
174
200
  if (!res.ok) throw toWidgetError(res.status, res.headers.get("Retry-After"));
175
201
  }
176
202
  async function postEvents(cfg, batch) {
177
- const res = await send(cfg, "events", batch);
203
+ const { identity, rest } = splitIdentity(batch);
204
+ const res = await send(cfg, "events", rest, void 0, identity);
178
205
  if (!res.ok) throw toWidgetError(res.status, res.headers.get("Retry-After"));
179
206
  }
180
207
  function postEventsKeepalive(cfg, batch) {
181
208
  try {
209
+ const { identity, rest } = splitIdentity(batch);
182
210
  void cfg.fetch(endpoint(cfg, "events"), {
183
211
  method: "POST",
184
- headers: headersFor(cfg, true),
185
- body: JSON.stringify(batch),
212
+ headers: { ...headersFor(cfg, true), ...identity },
213
+ body: JSON.stringify(rest),
186
214
  keepalive: true
187
215
  }).catch(() => {
188
216
  });
@@ -232,8 +260,8 @@ async function postPlayerProps(cfg, matchId, sport) {
232
260
  async function postSchedule(cfg, localDate, utcOffsetMinutes, operatorUserId, anonymousUserId) {
233
261
  const res = await cfg.fetch(endpoint(cfg, "live-monitor/schedule"), {
234
262
  method: "POST",
235
- headers: headersFor(cfg, true),
236
- body: JSON.stringify({ localDate, utcOffsetMinutes, sports: ["basketball", "americanfootball"], operatorUserId, anonymousUserId })
263
+ headers: { ...headersFor(cfg, true), ...identityHeaders(operatorUserId, anonymousUserId) },
264
+ body: JSON.stringify({ localDate, utcOffsetMinutes, sports: ["basketball", "americanfootball"] })
237
265
  });
238
266
  if (res.status === 404) return null;
239
267
  if (!res.ok) throw new Error(`live-monitor/schedule ${res.status}`);
@@ -242,8 +270,8 @@ async function postSchedule(cfg, localDate, utcOffsetMinutes, operatorUserId, an
242
270
  async function postLeagueStar(cfg, competitionId, starred, operatorUserId, anonymousUserId) {
243
271
  const res = await cfg.fetch(endpoint(cfg, `leagues/${competitionId}/star`), {
244
272
  method: "POST",
245
- headers: headersFor(cfg, true),
246
- body: JSON.stringify({ starred, operatorUserId, anonymousUserId })
273
+ headers: { ...headersFor(cfg, true), ...identityHeaders(operatorUserId, anonymousUserId) },
274
+ body: JSON.stringify({ starred })
247
275
  });
248
276
  if (res.status === 404) return null;
249
277
  if (!res.ok) throw new Error(`leagues/star ${res.status}`);
@@ -268,7 +296,7 @@ function q(parts) {
268
296
  return kept.length ? `?${kept.join("&")}` : "";
269
297
  }
270
298
  async function getGames(cfg, operatorUserId, gameDay) {
271
- const res = await cfg.fetch(endpoint(cfg, "games" + q({ operatorUserId, gameDay })), { method: "GET", headers: headersFor(cfg, false) });
299
+ const res = await cfg.fetch(endpoint(cfg, "games" + q({ gameDay })), { method: "GET", headers: { ...headersFor(cfg, false), ...identityHeaders(operatorUserId) } });
272
300
  if (res.status === 404) return null;
273
301
  if (!res.ok) throw new Error(`games ${res.status}`);
274
302
  return await res.json();
@@ -280,23 +308,24 @@ async function getGameConfig(cfg, gameId, fixtureId, gameDay, locale) {
280
308
  return await res.json();
281
309
  }
282
310
  async function getGameResult(cfg, gameId, operatorUserId, fixtureId, gameDay, locale) {
283
- const res = await cfg.fetch(endpoint(cfg, `games/${encodeURIComponent(gameId)}/result` + q({ operatorUserId, fixtureId, gameDay, locale })), { method: "GET", headers: headersFor(cfg, false) });
311
+ const res = await cfg.fetch(endpoint(cfg, `games/${encodeURIComponent(gameId)}/result` + q({ fixtureId, gameDay, locale })), { method: "GET", headers: { ...headersFor(cfg, false), ...identityHeaders(operatorUserId) } });
284
312
  if (res.status === 404) return null;
285
313
  if (!res.ok) throw new Error(`game-result ${res.status}`);
286
314
  return await res.json();
287
315
  }
288
316
  async function postPlay(cfg, gameId, body, locale) {
317
+ const { identity, rest } = splitIdentity(body);
289
318
  const res = await cfg.fetch(endpoint(cfg, `games/${encodeURIComponent(gameId)}/plays` + q({ locale })), {
290
319
  method: "POST",
291
- headers: headersFor(cfg, true),
292
- body: JSON.stringify(body)
320
+ headers: { ...headersFor(cfg, true), ...identity },
321
+ body: JSON.stringify(rest)
293
322
  });
294
323
  if (res.status === 404) return null;
295
324
  if (!res.ok) throw new Error(`play ${res.status}`);
296
325
  return await res.json();
297
326
  }
298
327
  async function getLeaderboard(cfg, operatorUserId, week) {
299
- const res = await cfg.fetch(endpoint(cfg, "games/leaderboard" + q({ operatorUserId, week })), { method: "GET", headers: headersFor(cfg, false) });
328
+ const res = await cfg.fetch(endpoint(cfg, "games/leaderboard" + q({ week })), { method: "GET", headers: { ...headersFor(cfg, false), ...identityHeaders(operatorUserId) } });
300
329
  if (res.status === 404) return null;
301
330
  if (!res.ok) throw new Error(`leaderboard ${res.status}`);
302
331
  return await res.json();
@@ -338,11 +367,68 @@ async function getClubRecords(cfg, limit) {
338
367
  return await res.json();
339
368
  }
340
369
  async function getPricePresets(cfg, locale, operatorUserId, options) {
341
- const res = await cfg.fetch(endpoint(cfg, "price-presets" + q({ locale, operatorUserId, pool: options?.pool })), { method: "GET", headers: headersFor(cfg, false) });
370
+ const res = await cfg.fetch(endpoint(cfg, "price-presets" + q({ locale, pool: options?.pool })), { method: "GET", headers: { ...headersFor(cfg, false), ...identityHeaders(operatorUserId) } });
342
371
  if (res.status === 404) return null;
343
372
  if (!res.ok) throw new Error(`price-presets ${res.status}`);
344
373
  return await res.json();
345
374
  }
375
+ async function getBuilderGrid(cfg, locale, req) {
376
+ const query = q({
377
+ locale,
378
+ sport: req.sport,
379
+ legs: req.legs,
380
+ total: req.total,
381
+ marketKey: req.marketKey ?? void 0,
382
+ matchIds: req.widen ? void 0 : req.matchIds.join(","),
383
+ widen: req.widen ? "true" : void 0
384
+ });
385
+ const res = await cfg.fetch(endpoint(cfg, "price-presets" + query), {
386
+ method: "GET",
387
+ headers: { ...headersFor(cfg, false), ...identityHeaders(req.operatorUserId, req.anonymousUserId) }
388
+ });
389
+ if (res.status === 404) return null;
390
+ if (!res.ok) throw new Error(`price-presets ${res.status}`);
391
+ return await res.json();
392
+ }
393
+ function favouritePath(kind, id) {
394
+ return `favourites/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`;
395
+ }
396
+ function favouritesFetch(cfg, method, path, operatorUserId, anonymousUserId) {
397
+ return cfg.fetch(endpoint(cfg, path), { method, headers: { ...headersFor(cfg, false), ...identityHeaders(operatorUserId, anonymousUserId) } });
398
+ }
399
+ async function getFavourites(cfg, sport, operatorUserId, anonymousUserId) {
400
+ const res = await favouritesFetch(cfg, "GET", "favourites" + q({ sport }), operatorUserId, anonymousUserId);
401
+ if (res.status === 400) {
402
+ const body = await res.json().catch(() => null);
403
+ return body?.code === "NoAcceptedIdentity" ? "noAcceptedIdentity" : null;
404
+ }
405
+ if (!res.ok) return null;
406
+ return await res.json();
407
+ }
408
+ var MIN_FAVOURITES_QUERY = 2;
409
+ async function searchFavourites(cfg, sport, query, kind, operatorUserId, anonymousUserId) {
410
+ const text = query.trim();
411
+ if (text.length < MIN_FAVOURITES_QUERY) return [];
412
+ const res = await favouritesFetch(cfg, "GET", "favourites/search" + q({ sport, q: text, kind }), operatorUserId, anonymousUserId);
413
+ if (!res.ok) return null;
414
+ return (await res.json()).results;
415
+ }
416
+ async function putFavourite(cfg, kind, id, operatorUserId, anonymousUserId) {
417
+ const res = await favouritesFetch(cfg, "PUT", favouritePath(kind, id), operatorUserId, anonymousUserId);
418
+ if (res.ok) return await res.json();
419
+ if (res.status !== 409) return null;
420
+ const body = await res.json().catch(() => null);
421
+ return body?.status === "atCap" ? { status: "atCap", count: body.count, cap: body.cap } : null;
422
+ }
423
+ async function deleteFavourite(cfg, kind, id, operatorUserId, anonymousUserId) {
424
+ return (await favouritesFetch(cfg, "DELETE", favouritePath(kind, id), operatorUserId, anonymousUserId)).ok;
425
+ }
426
+ async function clearFavourites(cfg, sport, operatorUserId, anonymousUserId) {
427
+ return (await favouritesFetch(cfg, "DELETE", "favourites" + q({ sport }), operatorUserId, anonymousUserId)).ok;
428
+ }
429
+ async function hideFavourite(cfg, kind, id, operatorUserId, anonymousUserId) {
430
+ return (await favouritesFetch(cfg, "POST", `${favouritePath(kind, id)}/hide`, operatorUserId, anonymousUserId)).ok;
431
+ }
346
432
 
347
433
  // src/ndjson.ts
348
434
  function parseLine(line) {
@@ -538,7 +624,7 @@ var Conversation = class {
538
624
  }
539
625
  sendSignal(input) {
540
626
  const operatorUserId = input.operatorUserId ?? this.context?.operatorUserId;
541
- const anonymousUserId = operatorUserId ? void 0 : input.anonymousUserId ?? this.context?.anonymousUserId;
627
+ const anonymousUserId = isSendableId(operatorUserId) ? void 0 : input.anonymousUserId ?? this.context?.anonymousUserId;
542
628
  if (!operatorUserId && !anonymousUserId) {
543
629
  throw new Error("operatorUserId or anonymousUserId is required (set it on conversation() or pass it to sendSignal)");
544
630
  }
@@ -562,12 +648,33 @@ var Conversation = class {
562
648
  getPricePresets(locale, options) {
563
649
  return getPricePresets(this.cfg, locale, this.context?.operatorUserId, options);
564
650
  }
651
+ getBuilderGrid(locale, req) {
652
+ return getBuilderGrid(this.cfg, locale, req);
653
+ }
565
654
  getSchedule(localDate, utcOffsetMinutes, operatorUserId, anonymousUserId) {
566
655
  return postSchedule(this.cfg, localDate, utcOffsetMinutes, operatorUserId, anonymousUserId);
567
656
  }
568
657
  starLeague(competitionId, starred, operatorUserId, anonymousUserId) {
569
658
  return postLeagueStar(this.cfg, competitionId, starred, operatorUserId, anonymousUserId);
570
659
  }
660
+ getFavourites(sport, operatorUserId, anonymousUserId) {
661
+ return getFavourites(this.cfg, sport, operatorUserId, anonymousUserId);
662
+ }
663
+ searchFavourites(sport, query, kind, operatorUserId, anonymousUserId) {
664
+ return searchFavourites(this.cfg, sport, query, kind, operatorUserId, anonymousUserId);
665
+ }
666
+ putFavourite(kind, id, operatorUserId, anonymousUserId) {
667
+ return putFavourite(this.cfg, kind, id, operatorUserId, anonymousUserId);
668
+ }
669
+ deleteFavourite(kind, id, operatorUserId, anonymousUserId) {
670
+ return deleteFavourite(this.cfg, kind, id, operatorUserId, anonymousUserId);
671
+ }
672
+ clearFavourites(sport, operatorUserId, anonymousUserId) {
673
+ return clearFavourites(this.cfg, sport, operatorUserId, anonymousUserId);
674
+ }
675
+ hideFavourite(kind, id, operatorUserId, anonymousUserId) {
676
+ return hideFavourite(this.cfg, kind, id, operatorUserId, anonymousUserId);
677
+ }
571
678
  getStates(matchIds, nbaGameIds, nflGameIds) {
572
679
  return postStates(this.cfg, matchIds, nbaGameIds, nflGameIds);
573
680
  }
@@ -617,7 +724,8 @@ var IMMEDIATE = /* @__PURE__ */ new Set([
617
724
  "slip_click",
618
725
  "slip_result",
619
726
  "slip_remove",
620
- "bet_placed"
727
+ "bet_placed",
728
+ "combo_add"
621
729
  ]);
622
730
  var DEFERRED = /* @__PURE__ */ new Set(["match_view"]);
623
731
  var DEFAULT_BATCH_INTERVAL_MS = 1e4;