glitchgrab 1.51.0 → 1.52.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 CHANGED
@@ -944,6 +944,54 @@ interface CallsOptions {
944
944
  }
945
945
  ```
946
946
 
947
+ ## How do I change demo booking settings from my admin panel?
948
+
949
+ The same server key reads and writes the project's demo booking settings —
950
+ whether prospects can book, working hours, slot length, notice, and the
951
+ WhatsApp code. Which Google calendar demos land in is **not** exposed: that
952
+ stays set in Glitchgrab → Calls → Booking.
953
+
954
+ ```ts
955
+ // app/api/admin/booking/route.ts — behind your admin auth
956
+ import { getBookingSettings, updateBookingSettings } from "glitchgrab/server";
957
+
958
+ const current = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…
959
+ if (current.ok && !current.data.calendarConnected) {
960
+ // no calendar → no slots, whatever `enabled` says
961
+ }
962
+
963
+ const saved = await updateBookingSettings({ enabled: true, slotMinutes: 45 });
964
+ if (!saved.ok) console.error(saved.error); // e.g. WhatsApp code already taken
965
+ ```
966
+
967
+ - **Send only what changed.** A field left out keeps its value.
968
+ - **Values are clamped** (slot 15–180 min, gap 0–120, 1–90 days ahead, notice
969
+ 0–10080 min) and invalid working hours are ignored — read `data.settings`
970
+ back instead of trusting what you sent.
971
+ - **Never throws.** Failures come back as `{ ok: false, error }`, never as defaults.
972
+
973
+ ```ts
974
+ getBookingSettings(options?: CallsOptions): Promise<BookingSettingsResult>
975
+ updateBookingSettings(changes: Partial<BookingSettings>, options?: CallsOptions): Promise<BookingSettingsResult>
976
+
977
+ type BookingSettingsResult =
978
+ | { ok: true; data: { settings: BookingSettings; saved: boolean; calendarConnected: boolean } }
979
+ | { ok: false; error: string };
980
+
981
+ interface BookingSettings {
982
+ enabled: boolean;
983
+ slotMinutes: number;
984
+ bufferMinutes: number;
985
+ timezone: string; // IANA, e.g. "Asia/Kolkata"
986
+ workingHours: Record<string, [string, string][]>; // { "1": [["09:00","17:00"]] }, 1 = Monday
987
+ title: string | null;
988
+ description: string | null;
989
+ horizonDays: number;
990
+ noticeMinutes: number;
991
+ whatsappCode: string | null;
992
+ }
993
+ ```
994
+
947
995
  ## What configuration options are available?
948
996
 
949
997
  | Prop | Type | Default | Description |
package/dist/server.d.mts CHANGED
@@ -270,7 +270,7 @@ interface Call extends CallListItem {
270
270
  }[];
271
271
  };
272
272
  }
273
- /** Options for {@link listCalls}, {@link getCall} and {@link deleteCall}. */
273
+ /** Options for {@link listCalls}, {@link getCall}, {@link deleteCall} and the booking settings calls. */
274
274
  interface CallsOptions {
275
275
  /**
276
276
  * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.
@@ -334,6 +334,61 @@ declare function getCall(id: string, options?: GetCallOptions): Promise<Call | n
334
334
  * showing ("The bot is still in this call — delete it after the call ends").
335
335
  */
336
336
  declare function deleteCall(id: string, options?: CallsOptions): Promise<DeleteCallResult>;
337
+ /** `{ "1": [["09:00", "17:00"]] }` — ISO weekday (1 = Monday) to opening ranges, in {@link BookingSettings.timezone}. */
338
+ type BookingWorkingHours = Record<string, [string, string][]>;
339
+ interface BookingSettings {
340
+ /** Off means the booking dialog and WhatsApp refuse to offer slots. */
341
+ enabled: boolean;
342
+ /** Length of a demo, 15–180. */
343
+ slotMinutes: number;
344
+ /** Free minutes kept after each booking, 0–120. */
345
+ bufferMinutes: number;
346
+ /** IANA zone the working hours are in, e.g. `"Asia/Kolkata"`. */
347
+ timezone: string;
348
+ /** A day left out is closed. */
349
+ workingHours: BookingWorkingHours;
350
+ title: string | null;
351
+ description: string | null;
352
+ /** How far ahead a slot can be booked, 1–90 days. */
353
+ horizonDays: number;
354
+ /** Earliest a slot can start from now, 0–10080 minutes. */
355
+ noticeMinutes: number;
356
+ /** Code in the "Book on WhatsApp" link. Lower case, letters, digits and `-`; unique across projects. */
357
+ whatsappCode: string | null;
358
+ }
359
+ interface BookingSettingsState {
360
+ settings: BookingSettings;
361
+ /** False until someone saves once — `settings` are the defaults. */
362
+ saved: boolean;
363
+ /** No calendar means no slots, whatever `enabled` says. Connect one in Glitchgrab → Calls → Booking. */
364
+ calendarConnected: boolean;
365
+ }
366
+ type BookingSettingsResult = {
367
+ ok: true;
368
+ data: BookingSettingsState;
369
+ } | {
370
+ ok: false;
371
+ error: string;
372
+ };
373
+ /**
374
+ * The key's project booking settings. Never throws — `{ ok: false, error }` on
375
+ * any failure, so the page can say why instead of showing defaults as if saved.
376
+ *
377
+ * ```ts
378
+ * const result = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…
379
+ * if (result.ok && !result.data.calendarConnected) warn("Connect a calendar in Glitchgrab");
380
+ * ```
381
+ */
382
+ declare function getBookingSettings(options?: CallsOptions): Promise<BookingSettingsResult>;
383
+ /**
384
+ * Change booking settings. Send only what changed — a field left out keeps its
385
+ * value. Numbers are clamped to their range and invalid working hours are
386
+ * ignored, so read `data.settings` back rather than trusting what you sent.
387
+ *
388
+ * `error` is worth showing: a WhatsApp code already taken by another project
389
+ * comes back as one.
390
+ */
391
+ declare function updateBookingSettings(changes: Partial<BookingSettings>, options?: CallsOptions): Promise<BookingSettingsResult>;
337
392
  /**
338
393
  * `JSON.stringify` that is safe inside `<script type="application/ld+json">`.
339
394
  *
@@ -354,4 +409,4 @@ declare function deleteCall(id: string, options?: CallsOptions): Promise<DeleteC
354
409
  */
355
410
  declare function escapeJsonForScript(value: unknown): string;
356
411
 
357
- export { type Call, type CallAudio, type CallListItem, type CallMoment, type CallParticipant, type CallsOptions, type CaptureServerErrorsOptions, type DeleteCallResult, type GetCallOptions, type Guide, type GuideReadOptions, type GuideSummary, type ListCallsOptions, ReportResult, ReportSeverity, ReportType, type ServerReportOptions, type ServerReporterConfig, type ServerReporterIdentity, type SignSessionOptions, captureServerErrors, configureServerReporter, deleteCall, escapeJsonForScript, getCall, getGuide, listCalls, listGuides, reportServerError, resetServerReporter, signSession };
412
+ export { type BookingSettings, type BookingSettingsResult, type BookingSettingsState, type BookingWorkingHours, type Call, type CallAudio, type CallListItem, type CallMoment, type CallParticipant, type CallsOptions, type CaptureServerErrorsOptions, type DeleteCallResult, type GetCallOptions, type Guide, type GuideReadOptions, type GuideSummary, type ListCallsOptions, ReportResult, ReportSeverity, ReportType, type ServerReportOptions, type ServerReporterConfig, type ServerReporterIdentity, type SignSessionOptions, captureServerErrors, configureServerReporter, deleteCall, escapeJsonForScript, getBookingSettings, getCall, getGuide, listCalls, listGuides, reportServerError, resetServerReporter, signSession, updateBookingSettings };
package/dist/server.d.ts CHANGED
@@ -270,7 +270,7 @@ interface Call extends CallListItem {
270
270
  }[];
271
271
  };
272
272
  }
273
- /** Options for {@link listCalls}, {@link getCall} and {@link deleteCall}. */
273
+ /** Options for {@link listCalls}, {@link getCall}, {@link deleteCall} and the booking settings calls. */
274
274
  interface CallsOptions {
275
275
  /**
276
276
  * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.
@@ -334,6 +334,61 @@ declare function getCall(id: string, options?: GetCallOptions): Promise<Call | n
334
334
  * showing ("The bot is still in this call — delete it after the call ends").
335
335
  */
336
336
  declare function deleteCall(id: string, options?: CallsOptions): Promise<DeleteCallResult>;
337
+ /** `{ "1": [["09:00", "17:00"]] }` — ISO weekday (1 = Monday) to opening ranges, in {@link BookingSettings.timezone}. */
338
+ type BookingWorkingHours = Record<string, [string, string][]>;
339
+ interface BookingSettings {
340
+ /** Off means the booking dialog and WhatsApp refuse to offer slots. */
341
+ enabled: boolean;
342
+ /** Length of a demo, 15–180. */
343
+ slotMinutes: number;
344
+ /** Free minutes kept after each booking, 0–120. */
345
+ bufferMinutes: number;
346
+ /** IANA zone the working hours are in, e.g. `"Asia/Kolkata"`. */
347
+ timezone: string;
348
+ /** A day left out is closed. */
349
+ workingHours: BookingWorkingHours;
350
+ title: string | null;
351
+ description: string | null;
352
+ /** How far ahead a slot can be booked, 1–90 days. */
353
+ horizonDays: number;
354
+ /** Earliest a slot can start from now, 0–10080 minutes. */
355
+ noticeMinutes: number;
356
+ /** Code in the "Book on WhatsApp" link. Lower case, letters, digits and `-`; unique across projects. */
357
+ whatsappCode: string | null;
358
+ }
359
+ interface BookingSettingsState {
360
+ settings: BookingSettings;
361
+ /** False until someone saves once — `settings` are the defaults. */
362
+ saved: boolean;
363
+ /** No calendar means no slots, whatever `enabled` says. Connect one in Glitchgrab → Calls → Booking. */
364
+ calendarConnected: boolean;
365
+ }
366
+ type BookingSettingsResult = {
367
+ ok: true;
368
+ data: BookingSettingsState;
369
+ } | {
370
+ ok: false;
371
+ error: string;
372
+ };
373
+ /**
374
+ * The key's project booking settings. Never throws — `{ ok: false, error }` on
375
+ * any failure, so the page can say why instead of showing defaults as if saved.
376
+ *
377
+ * ```ts
378
+ * const result = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…
379
+ * if (result.ok && !result.data.calendarConnected) warn("Connect a calendar in Glitchgrab");
380
+ * ```
381
+ */
382
+ declare function getBookingSettings(options?: CallsOptions): Promise<BookingSettingsResult>;
383
+ /**
384
+ * Change booking settings. Send only what changed — a field left out keeps its
385
+ * value. Numbers are clamped to their range and invalid working hours are
386
+ * ignored, so read `data.settings` back rather than trusting what you sent.
387
+ *
388
+ * `error` is worth showing: a WhatsApp code already taken by another project
389
+ * comes back as one.
390
+ */
391
+ declare function updateBookingSettings(changes: Partial<BookingSettings>, options?: CallsOptions): Promise<BookingSettingsResult>;
337
392
  /**
338
393
  * `JSON.stringify` that is safe inside `<script type="application/ld+json">`.
339
394
  *
@@ -354,4 +409,4 @@ declare function deleteCall(id: string, options?: CallsOptions): Promise<DeleteC
354
409
  */
355
410
  declare function escapeJsonForScript(value: unknown): string;
356
411
 
357
- export { type Call, type CallAudio, type CallListItem, type CallMoment, type CallParticipant, type CallsOptions, type CaptureServerErrorsOptions, type DeleteCallResult, type GetCallOptions, type Guide, type GuideReadOptions, type GuideSummary, type ListCallsOptions, ReportResult, ReportSeverity, ReportType, type ServerReportOptions, type ServerReporterConfig, type ServerReporterIdentity, type SignSessionOptions, captureServerErrors, configureServerReporter, deleteCall, escapeJsonForScript, getCall, getGuide, listCalls, listGuides, reportServerError, resetServerReporter, signSession };
412
+ export { type BookingSettings, type BookingSettingsResult, type BookingSettingsState, type BookingWorkingHours, type Call, type CallAudio, type CallListItem, type CallMoment, type CallParticipant, type CallsOptions, type CaptureServerErrorsOptions, type DeleteCallResult, type GetCallOptions, type Guide, type GuideReadOptions, type GuideSummary, type ListCallsOptions, ReportResult, ReportSeverity, ReportType, type ServerReportOptions, type ServerReporterConfig, type ServerReporterIdentity, type SignSessionOptions, captureServerErrors, configureServerReporter, deleteCall, escapeJsonForScript, getBookingSettings, getCall, getGuide, listCalls, listGuides, reportServerError, resetServerReporter, signSession, updateBookingSettings };
package/dist/server.js CHANGED
@@ -24,13 +24,15 @@ __export(server_exports, {
24
24
  configureServerReporter: () => configureServerReporter,
25
25
  deleteCall: () => deleteCall,
26
26
  escapeJsonForScript: () => escapeJsonForScript,
27
+ getBookingSettings: () => getBookingSettings,
27
28
  getCall: () => getCall,
28
29
  getGuide: () => getGuide,
29
30
  listCalls: () => listCalls,
30
31
  listGuides: () => listGuides,
31
32
  reportServerError: () => reportServerError,
32
33
  resetServerReporter: () => resetServerReporter,
33
- signSession: () => signSession
34
+ signSession: () => signSession,
35
+ updateBookingSettings: () => updateBookingSettings
34
36
  });
35
37
  module.exports = __toCommonJS(server_exports);
36
38
  var import_node_crypto = require("crypto");
@@ -263,7 +265,11 @@ async function callSdkApi(path, token, options, init) {
263
265
  try {
264
266
  const request = {
265
267
  method: (_e = init.method) != null ? _e : "GET",
266
- headers: { Authorization: `Bearer ${token}` },
268
+ headers: {
269
+ Authorization: `Bearer ${token}`,
270
+ ...init.body === void 0 ? {} : { "Content-Type": "application/json" }
271
+ },
272
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
267
273
  ...init.cache === "no-store" ? { cache: "no-store" } : { next: { revalidate: init.cache.revalidate } },
268
274
  ...controller ? { signal: controller.signal } : {}
269
275
  };
@@ -332,6 +338,7 @@ async function getGuide(slug, options = {}) {
332
338
  }
333
339
  }
334
340
  var CALLS_KEY_PREFIX = "ggc_";
341
+ var NO_KEY_ERROR = "No server key \u2014 set GLITCHGRAB_CALLS_KEY (ggc_\u2026)";
335
342
  function callsKey(options) {
336
343
  var _a;
337
344
  const key = (_a = options.key) != null ? _a : readEnv("GLITCHGRAB_CALLS_KEY");
@@ -425,7 +432,7 @@ async function deleteCall(id, options = {}) {
425
432
  const wanted = typeof id === "string" ? id.trim() : "";
426
433
  if (!wanted) return { deleted: false, error: "No call id" };
427
434
  const key = callsKey(options);
428
- if (!key) return { deleted: false, error: "No server key \u2014 set GLITCHGRAB_CALLS_KEY (ggc_\u2026)" };
435
+ if (!key) return { deleted: false, error: NO_KEY_ERROR };
429
436
  const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {
430
437
  method: "DELETE",
431
438
  cache: "no-store"
@@ -435,6 +442,64 @@ async function deleteCall(id, options = {}) {
435
442
  return { deleted: false, error: "Could not delete the call" };
436
443
  }
437
444
  }
445
+ var WEEKDAY = /^[1-7]$/;
446
+ function readBookingSettingsState(value) {
447
+ var _a, _b, _c, _d;
448
+ if (!isRecord(value) || !isRecord(value.settings)) return null;
449
+ const s = value.settings;
450
+ const hours = {};
451
+ if (isRecord(s.workingHours)) {
452
+ for (const [day, ranges] of Object.entries(s.workingHours)) {
453
+ if (!WEEKDAY.test(day) || !Array.isArray(ranges)) continue;
454
+ const valid = ranges.filter(
455
+ (r) => Array.isArray(r) && typeof r[0] === "string" && typeof r[1] === "string"
456
+ );
457
+ if (valid.length) hours[day] = valid.map(([open, close]) => [open, close]);
458
+ }
459
+ }
460
+ return {
461
+ settings: {
462
+ enabled: s.enabled === true,
463
+ slotMinutes: (_a = numberOrNull(s.slotMinutes)) != null ? _a : 60,
464
+ bufferMinutes: (_b = numberOrNull(s.bufferMinutes)) != null ? _b : 0,
465
+ timezone: typeof s.timezone === "string" ? s.timezone : "Asia/Kolkata",
466
+ workingHours: hours,
467
+ title: stringOrNull(s.title),
468
+ description: stringOrNull(s.description),
469
+ horizonDays: (_c = numberOrNull(s.horizonDays)) != null ? _c : 15,
470
+ noticeMinutes: (_d = numberOrNull(s.noticeMinutes)) != null ? _d : 120,
471
+ whatsappCode: stringOrNull(s.whatsappCode)
472
+ },
473
+ saved: value.saved === true,
474
+ calendarConnected: value.calendarConnected === true
475
+ };
476
+ }
477
+ async function getBookingSettings(options = {}) {
478
+ try {
479
+ const key = callsKey(options);
480
+ if (!key) return { ok: false, error: NO_KEY_ERROR };
481
+ const { data, error } = await callSdkApi("/api/v1/sdk/booking-settings", key, options, { cache: "no-store" });
482
+ const state = error === null ? readBookingSettingsState(data) : null;
483
+ return state ? { ok: true, data: state } : { ok: false, error: error != null ? error : "Unexpected response" };
484
+ } catch (e) {
485
+ return { ok: false, error: "Could not read booking settings" };
486
+ }
487
+ }
488
+ async function updateBookingSettings(changes, options = {}) {
489
+ try {
490
+ const key = callsKey(options);
491
+ if (!key) return { ok: false, error: NO_KEY_ERROR };
492
+ const { data, error } = await callSdkApi("/api/v1/sdk/booking-settings", key, options, {
493
+ method: "PUT",
494
+ body: changes != null ? changes : {},
495
+ cache: "no-store"
496
+ });
497
+ const state = error === null ? readBookingSettingsState(data) : null;
498
+ return state ? { ok: true, data: state } : { ok: false, error: error != null ? error : "Unexpected response" };
499
+ } catch (e) {
500
+ return { ok: false, error: "Could not save booking settings" };
501
+ }
502
+ }
438
503
  function escapeJsonForScript(value) {
439
504
  try {
440
505
  const json = JSON.stringify(value);
@@ -450,12 +515,14 @@ function escapeJsonForScript(value) {
450
515
  configureServerReporter,
451
516
  deleteCall,
452
517
  escapeJsonForScript,
518
+ getBookingSettings,
453
519
  getCall,
454
520
  getGuide,
455
521
  listCalls,
456
522
  listGuides,
457
523
  reportServerError,
458
524
  resetServerReporter,
459
- signSession
525
+ signSession,
526
+ updateBookingSettings
460
527
  });
461
528
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/runtime.ts","../src/utils.ts"],"sourcesContent":["/**\n * Server-side reporting — `import { reportServerError } from \"glitchgrab/server\"`.\n *\n * The rest of this SDK is `\"use client\"`: it hooks `window.onerror`, walks the\n * DOM for a screenshot, and needs a mounted provider for its token. None of\n * that exists in a cron job at 3am, an API route handler, or a queue worker —\n * which is precisely where the failures nobody is watching happen. A WhatsApp\n * send Meta rejects, an SMTP timeout, a nightly digest that throws: no tab is\n * open, so the browser SDK never sees them and no issue is ever filed.\n *\n * This entry has no `\"use client\"`, touches no DOM API, and imports no React.\n * It POSTs to the same `/api/v1/sdk/report` endpoint the browser uses, with\n * `source: \"SDK_AUTO\"` so the server-side dedup applies (24h per signature,\n * 7 days once an issue exists) — otherwise a cron failing hourly would open\n * twenty-four identical issues a day.\n *\n * Like the rest of the SDK it never throws: a reporting failure must not turn\n * a handled error into an unhandled one.\n */\nimport { createHmac } from \"node:crypto\";\nimport type { ReportPayload, ReportResult, ReportSeverity, ReportType } from \"./types\";\nimport { DEFAULT_BASE_URL, sendReport } from \"./utils\";\n\n/** Who to attribute the report to. A server has no signed-in user, so this is the job or service. */\nexport interface ServerReporterIdentity {\n /** Stable id — the cron name, the service name. Defaults to `\"server\"`. */\n id?: string;\n /** Display name shown on the report. Defaults to `\"Server\"`. */\n name?: string;\n email?: string;\n phone?: string;\n}\n\n/** Defaults applied to every server report. Set once at boot with {@link configureServerReporter}. */\nexport interface ServerReporterConfig {\n /** Repo token (`gg_…`). Falls back to `process.env.GLITCHGRAB_TOKEN`. */\n token?: string;\n /** Override the API host. Falls back to `process.env.GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Where this ran — `\"cron/attendance-prompts\"`, `\"worker/email-queue\"`.\n *\n * It is the grouping key, not a link: it is sent as the report's `pageUrl`\n * so two different jobs throwing the same `\"Timeout\"` stay two issues\n * instead of collapsing into one.\n */\n context?: string;\n /** Merged into every report's metadata. Good place for region, deployment id, service name. */\n metadata?: Record<string, string>;\n reporter?: ServerReporterIdentity;\n /**\n * Report from `NODE_ENV=development` too. Off by default, matching the\n * browser SDK — a laptop crashing its way through a refactor should not file\n * issues. Unlike the browser there is no `Origin` header for the API to\n * recognise, so nothing else stops a dev machine from filing.\n */\n enableInDevelopment?: boolean;\n}\n\n/** Per-call options. Anything set here wins over {@link configureServerReporter}. */\nexport interface ServerReportOptions extends ServerReporterConfig {\n /** Issue type. Defaults to `BUG`. */\n type?: ReportType;\n /** Rendered as a `severity:<value>` label on the GitHub issue. */\n severity?: ReportSeverity;\n /** Extra prose for the issue body — what the job was doing, which record it was on. */\n description?: string;\n /**\n * A real request URL, when there is one (an API route handler). Overrides\n * the synthetic URL built from `context`.\n */\n pageUrl?: string;\n}\n\nconst DEV_BLOCKED_MESSAGE = \"development\";\n\n/**\n * Node's `process`, as much of it as this file uses.\n *\n * `src/global.d.ts` declares a browser-shaped `process` with nothing but\n * `env`, and that is deliberate — the client bundle must not be able to reach\n * for Node APIs. So this entry reads the real one off `globalThis` behind its\n * own narrow type instead of pulling `@types/node` into the package.\n */\ninterface NodeProcessLike {\n env?: Record<string, string | undefined>;\n version?: string;\n on?(event: string, listener: (arg: never) => void): unknown;\n off?(event: string, listener: (arg: never) => void): unknown;\n listenerCount?(event: string): number;\n}\n\nfunction nodeProcess(): NodeProcessLike | undefined {\n try {\n const candidate = (globalThis as { process?: NodeProcessLike }).process;\n return candidate && typeof candidate === \"object\" ? candidate : undefined;\n } catch {\n return undefined;\n }\n}\n\nlet defaults: ServerReporterConfig = {};\n\n/**\n * Set the token and the defaults once, at module load, instead of passing them\n * to every call site. A later call's own options still win.\n */\nexport function configureServerReporter(config: ServerReporterConfig): void {\n defaults = { ...defaults, ...config };\n}\n\n/** Test-only — drop everything {@link configureServerReporter} set. */\nexport function resetServerReporter(): void {\n defaults = {};\n}\n\nfunction readEnv(name: string): string | undefined {\n try {\n return nodeProcess()?.env?.[name];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Message and stack out of whatever was thrown.\n *\n * A thrown string, a rejected `{ code, message }`, an `undefined` from a\n * `Promise.reject()` — all of it reaches here, and all of it must produce a\n * non-empty message: the API's dedup signature is `null` without one, and a\n * report with no signature is a fresh issue every single time.\n */\nfunction describeError(error: unknown): { message: string; stack?: string } {\n try {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || \"Unknown server error\",\n stack: error.stack,\n };\n }\n if (typeof error === \"string\") {\n return { message: error || \"Unknown server error\" };\n }\n if (error && typeof error === \"object\") {\n const maybe = error as { message?: unknown; stack?: unknown };\n const message = typeof maybe.message === \"string\" ? maybe.message : JSON.stringify(error);\n return {\n message: message || \"Unknown server error\",\n stack: typeof maybe.stack === \"string\" ? maybe.stack : undefined,\n };\n }\n return { message: String(error ?? \"\") || \"Unknown server error\" };\n } catch {\n return { message: \"Unknown server error\" };\n }\n}\n\n/**\n * `pageUrl` doubles as the dedup grouping key, so a job needs one even though\n * no page was involved. `server://` keeps it obviously synthetic — and keeps it\n * clear of the API's localhost check, which only matches http(s) and would\n * otherwise swallow the report with a 200 and create nothing.\n */\nfunction contextUrl(context: string | undefined): string | undefined {\n if (!context) return undefined;\n return `server://${context.replace(/^\\/+/, \"\")}`;\n}\n\nfunction runtimeMetadata(): Record<string, string> {\n try {\n const proc = nodeProcess();\n if (!proc) return { runtime: \"server\" };\n const region = proc.env?.VERCEL_REGION;\n const deploymentEnv = proc.env?.VERCEL_ENV;\n return {\n runtime: \"node\",\n ...(proc.version ? { nodeVersion: proc.version } : {}),\n ...(region ? { region } : {}),\n ...(deploymentEnv ? { deploymentEnv } : {}),\n };\n } catch {\n return { runtime: \"server\" };\n }\n}\n\n/**\n * File a server-side error as a GitHub issue.\n *\n * Await it. On a serverless platform the function may be frozen the moment your\n * handler returns, and a floating promise dies with it — the report never\n * leaves the machine.\n *\n * ```ts\n * try {\n * await sendDigest();\n * } catch (error) {\n * await reportServerError(error, { context: \"cron/daily-digest\" });\n * throw error;\n * }\n * ```\n *\n * Returns `null` when nothing was filed — no token, development mode, or the\n * API refused it. Never throws.\n */\nexport async function reportServerError(\n error: unknown,\n options: ServerReportOptions = {}\n): Promise<ReportResult | null> {\n try {\n const token = options.token ?? defaults.token ?? readEnv(\"GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const enableInDevelopment = options.enableInDevelopment ?? defaults.enableInDevelopment ?? false;\n if (!enableInDevelopment && readEnv(\"NODE_ENV\") === DEV_BLOCKED_MESSAGE) return null;\n\n const { message, stack } = describeError(error);\n const context = options.context ?? defaults.context;\n const reporter = { ...defaults.reporter, ...options.reporter };\n\n const payload: ReportPayload = {\n token,\n source: \"SDK_AUTO\",\n type: options.type ?? \"BUG\",\n errorMessage: message,\n ...(stack ? { errorStack: stack } : {}),\n ...(options.description ? { description: options.description } : {}),\n ...(options.pageUrl ?? contextUrl(context)\n ? { pageUrl: options.pageUrl ?? contextUrl(context) }\n : {}),\n metadata: {\n ...runtimeMetadata(),\n ...defaults.metadata,\n ...options.metadata,\n ...(context ? { serverContext: context } : {}),\n ...(options.severity ? { severity: options.severity } : {}),\n sessionUserId: reporter.id ?? context ?? \"server\",\n sessionUserName: reporter.name ?? \"Server\",\n ...(reporter.email ? { sessionUserEmail: reporter.email } : {}),\n ...(reporter.phone ? { sessionUserPhone: reporter.phone } : {}),\n },\n };\n\n return await sendReport(payload, options.baseUrl ?? defaults.baseUrl ?? readEnv(\"GLITCHGRAB_BASE_URL\"));\n } catch {\n return null;\n }\n}\n\n/** Options for {@link captureServerErrors}. */\nexport interface CaptureServerErrorsOptions extends ServerReportOptions {\n /**\n * Also report rejected promises nobody handled. Default `true`.\n *\n * Node crashes the process on an unhandled rejection — but only while no\n * `unhandledRejection` listener exists. Attaching one silently converts a\n * crash into a warning, so this re-throws afterwards to put the default\n * behaviour back, unless the host app has its own listener (then it is theirs\n * to decide).\n */\n captureUnhandledRejections?: boolean;\n}\n\n/**\n * Report every uncaught exception and unhandled rejection in this process.\n *\n * Call it once at boot — `instrumentation.ts` in a Next.js app. Returns a\n * function that removes the listeners again.\n *\n * Uncaught exceptions use `uncaughtExceptionMonitor`, which observes without\n * taking over: the process still crashes exactly as it would have. A reporter\n * that keeps a broken process alive is worse than no reporter.\n */\nexport function captureServerErrors(options: CaptureServerErrorsOptions = {}): () => void {\n try {\n const proc = nodeProcess();\n if (!proc || typeof proc.on !== \"function\") return () => {};\n\n const onException = (error: unknown) => {\n void reportServerError(error, { ...options, context: options.context ?? \"uncaughtException\" });\n };\n\n const onRejection = (reason: unknown) => {\n void reportServerError(reason, {\n ...options,\n context: options.context ?? \"unhandledRejection\",\n });\n\n // Ours is the only listener, so suppressing the crash was never the\n // host app's choice. Hand the default behaviour back.\n if (proc.listenerCount?.(\"unhandledRejection\") === 1) {\n setTimeout(() => {\n throw reason;\n }, 0);\n }\n };\n\n proc.on(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n\n const captureRejections = options.captureUnhandledRejections ?? true;\n if (captureRejections) proc.on(\"unhandledRejection\", onRejection as (arg: never) => void);\n\n return () => {\n try {\n proc.off?.(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n if (captureRejections) {\n proc.off?.(\"unhandledRejection\", onRejection as (arg: never) => void);\n }\n } catch {\n // Nothing to undo.\n }\n };\n } catch {\n return () => {};\n }\n}\n\n/** Options for {@link signSession}. */\nexport interface SignSessionOptions {\n /** Your user's id — the same value you pass as `session.userId`. */\n userId: string;\n /** The repo's signing secret (`ggs_…`). Falls back to `process.env.GLITCHGRAB_SIGNING_SECRET`. */\n secret?: string;\n /** How long the signature stays valid, in seconds. Default one day. */\n ttlSeconds?: number;\n /** Clock override in epoch ms. For tests. */\n now?: number;\n}\n\n/**\n * Vouch for the logged-in user so the SDK's AI features work when the project\n * owner turned on signed users.\n *\n * The `gg_` token ships in your JavaScript, so on its own it proves nothing\n * about who is calling. Your server holds the signing secret; call this where\n * you build the `session` prop and pass the result as `session.signature`.\n * Render it per request so it refreshes before `ttlSeconds` runs out.\n *\n * Format: `v1.<exp>.<base64url HMAC-SHA256(secret, \"v1.<exp>.<userId>\")>`.\n *\n * Unlike the rest of this SDK it throws: it runs in your own server code, where\n * a missing secret is a deploy mistake you want to see — not a user who quietly\n * loses the AI features.\n */\nexport function signSession(options: SignSessionOptions): string {\n const userId = options?.userId;\n if (typeof userId !== \"string\" || userId.length === 0) {\n throw new Error(\"glitchgrab signSession: userId is required\");\n }\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n const secret = options.secret || env?.GLITCHGRAB_SIGNING_SECRET;\n if (!secret) {\n throw new Error(\n \"glitchgrab signSession: no signing secret — pass `secret` or set GLITCHGRAB_SIGNING_SECRET\"\n );\n }\n const ttl = options.ttlSeconds ?? 86_400;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new Error(\"glitchgrab signSession: ttlSeconds must be a positive number\");\n }\n const exp = Math.floor((options.now ?? Date.now()) / 1000) + Math.floor(ttl);\n const sig = createHmac(\"sha256\", secret).update(`v1.${exp}.${userId}`).digest(\"base64url\");\n return `v1.${exp}.${sig}`;\n}\n\n// ── Guides ───────────────────────────────────────────────────────────────────\n//\n// A team writes its how-tos in Glitchgrab and renders them on its own site from\n// `GET /api/v1/sdk/guides` (#370). Every site needs the same client — token,\n// caching, never failing the page, shape checks — so it lives here instead of\n// being rewritten, slightly differently, in each product (#409).\n\n/** One guide as the list returns it — no body. Dates are ISO strings: this came over JSON. */\nexport interface GuideSummary {\n slug: string;\n title: string;\n /** One line saying what the guide answers. */\n summary: string;\n category: string | null;\n /** The order set in Glitchgrab. The list already arrives sorted by it. */\n orderIndex: number;\n /** Always `\"PUBLISHED\"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */\n status: \"DRAFT\" | \"PUBLISHED\";\n /** When someone last re-checked the steps against the product. */\n lastVerifiedAt: string | null;\n updatedAt: string;\n /**\n * Where this guide lives on your site: its own link, else\n * `<guides base URL>/<slug>`, else null when the repo sets neither.\n */\n link: string | null;\n}\n\n/** One guide with its body. */\nexport interface Guide extends GuideSummary {\n /** The markdown as written. */\n content: string;\n /**\n * `content` rendered by Glitchgrab: raw HTML in the markdown arrives as text,\n * only allowlisted attributes survive, and links keep only http(s), mailto\n * and relative targets. Headings carry `id=\"guide-<heading text>\"`, for a\n * table of contents or a link to a single step.\n *\n * Safe to render as-is. On a domain that holds your users' sessions, a second\n * sanitize pass of your own is cheap defence in depth — this SDK ships no\n * sanitizer so it stays dependency-free.\n */\n html: string;\n}\n\n/** Options for {@link listGuides} and {@link getGuide}. */\nexport interface GuideReadOptions {\n /**\n * Repo token (`gg_…`). Falls back to {@link configureServerReporter}'s token,\n * then `GLITCHGRAB_TOKEN`, then `NEXT_PUBLIC_GLITCHGRAB_TOKEN` — the token is\n * public, so the one your provider already uses is fine.\n *\n * Never pass the `ggc_` server key for a public page: it reads drafts too, and client calls.\n */\n token?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Seconds Next.js may cache the response. Default 300; `0` reads fresh on\n * every request. The route is `force-dynamic`, so without this nothing is\n * cached at all. Ignored outside Next.js.\n */\n revalidate?: number;\n /** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */\n timeoutMs?: number;\n}\n\nconst GUIDES_REVALIDATE_SECONDS = 300;\nconst SDK_API_TIMEOUT_MS = 5000;\n\n/** `RequestInit` plus Next.js's cache option, without pulling Next's types into this entry. */\ntype NextFetchInit = RequestInit & { next?: { revalidate?: number } };\n\ninterface SdkApiResult {\n /** `data` from a successful response, else null. */\n data: unknown;\n /** Null on success; the route's own `error` when it sent one, so a caller can show why. */\n error: string | null;\n}\n\n/**\n * One request to a Glitchgrab SDK route. Never throws: a refusal, a timeout, a\n * network failure and unreadable JSON all come back as `error`.\n */\nasync function callSdkApi(\n path: string,\n token: string,\n options: { baseUrl?: string; timeoutMs?: number },\n init: { method?: \"GET\" | \"DELETE\"; cache: { revalidate: number } | \"no-store\" }\n): Promise<SdkApiResult> {\n const baseUrl = (\n options.baseUrl ??\n defaults.baseUrl ??\n readEnv(\"GLITCHGRAB_BASE_URL\") ??\n DEFAULT_BASE_URL\n ).replace(/\\/+$/, \"\");\n\n const controller = typeof AbortController === \"function\" ? new AbortController() : undefined;\n const timer = controller\n ? setTimeout(() => controller.abort(), options.timeoutMs ?? SDK_API_TIMEOUT_MS)\n : undefined;\n\n try {\n const request: NextFetchInit = {\n method: init.method ?? \"GET\",\n headers: { Authorization: `Bearer ${token}` },\n ...(init.cache === \"no-store\" ? { cache: \"no-store\" as const } : { next: { revalidate: init.cache.revalidate } }),\n ...(controller ? { signal: controller.signal } : {}),\n };\n const response = await fetch(`${baseUrl}${path}`, request);\n const body = (await response.json().catch(() => null)) as\n | { success?: unknown; data?: unknown; error?: unknown }\n | null;\n if (!response.ok || body?.success !== true) {\n return { data: null, error: typeof body?.error === \"string\" ? body.error : `HTTP ${response.status}` };\n }\n return { data: body.data ?? null, error: null };\n } catch {\n return { data: null, error: \"Could not reach Glitchgrab\" };\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/** `data` from the guides route, or null on any failure — no token, non-2xx, timeout, bad JSON. */\nasync function readGuidesApi(query: string, options: GuideReadOptions): Promise<unknown> {\n const token =\n options.token ??\n defaults.token ??\n readEnv(\"GLITCHGRAB_TOKEN\") ??\n readEnv(\"NEXT_PUBLIC_GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const revalidate =\n typeof options.revalidate === \"number\" && Number.isFinite(options.revalidate) && options.revalidate >= 0\n ? options.revalidate\n : GUIDES_REVALIDATE_SECONDS;\n\n const result = await callSdkApi(`/api/v1/sdk/guides${query}`, token, options, { cache: { revalidate } });\n return result.data;\n}\n\n/**\n * A guide summary out of whatever the API sent, or null when it is not one.\n * Only slug, title and summary are required — a page cannot render without\n * them. The rest default, so the returned type is honest either way.\n */\nfunction readGuideSummary(value: unknown): GuideSummary | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const guide = value as Record<string, unknown>;\n if (\n typeof guide.slug !== \"string\" ||\n typeof guide.title !== \"string\" ||\n typeof guide.summary !== \"string\"\n ) {\n return null;\n }\n return {\n slug: guide.slug,\n title: guide.title,\n summary: guide.summary,\n category: typeof guide.category === \"string\" ? guide.category : null,\n orderIndex: typeof guide.orderIndex === \"number\" ? guide.orderIndex : 0,\n status: guide.status === \"DRAFT\" ? \"DRAFT\" : \"PUBLISHED\",\n lastVerifiedAt: typeof guide.lastVerifiedAt === \"string\" ? guide.lastVerifiedAt : null,\n updatedAt: typeof guide.updatedAt === \"string\" ? guide.updatedAt : \"\",\n link: typeof guide.link === \"string\" ? guide.link : null,\n };\n}\n\n/**\n * Every published guide for the token's repo, in the order set in Glitchgrab.\n * No bodies — this is the index page.\n *\n * ```ts\n * // app/guides/page.tsx\n * import { listGuides } from \"glitchgrab/server\";\n *\n * export default async function GuidesPage() {\n * const guides = await listGuides();\n * return guides.map((g) => <a key={g.slug} href={`/guides/${g.slug}`}>{g.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws: a Glitchgrab outage leaves the\n * page empty instead of turning it into a 500.\n */\nexport async function listGuides(options: GuideReadOptions = {}): Promise<GuideSummary[]> {\n try {\n const data = await readGuidesApi(\"\", options);\n if (!Array.isArray(data)) return [];\n return data.map(readGuideSummary).filter((guide): guide is GuideSummary => guide !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One published guide with its markdown and rendered HTML.\n *\n * Returns `null` when there is no such guide and on any failure — call\n * `notFound()` on it. Never throws.\n */\nexport async function getGuide(slug: string, options: GuideReadOptions = {}): Promise<Guide | null> {\n try {\n const wanted = typeof slug === \"string\" ? slug.trim() : \"\";\n if (!wanted) return null;\n\n const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}`, options);\n const summary = readGuideSummary(data);\n const body = data as { content?: unknown; html?: unknown } | null;\n if (!summary || typeof body?.html !== \"string\") return null;\n\n return {\n ...summary,\n content: typeof body.content === \"string\" ? body.content : \"\",\n html: body.html,\n };\n } catch {\n return null;\n }\n}\n\n// ── Calls ────────────────────────────────────────────────────────────────────\n//\n// Recorded client calls a project shared with its client (#416), for the team's\n// own admin panel. Unlike guides these are private: they need the project's\n// server key (`ggc_…`, API Tokens → Server keys), which lives on the server and never in a bundle, and\n// every response holds links that expire — so nothing here is cached.\n\n/** Someone on the call, as the recorder saw them. */\nexport interface CallParticipant {\n name: string | null;\n email: string | null;\n}\n\n/** One shared call as the list returns it — no transcript, no links. */\nexport interface CallListItem {\n id: string;\n title: string | null;\n startsAt: string | null;\n endsAt: string | null;\n durationSec: number | null;\n /** `\"bot\"` (joined the Meet) or `\"extension\"` (recorded from someone's Chrome). */\n recorder: string | null;\n /** Match a call to your customer by these emails. */\n participants: CallParticipant[];\n hasRecording: boolean;\n /** `IDLE`, `RUNNING`, `DONE` or `FAILED`. */\n transcriptStatus: string;\n /** One line on what the call was about, once Glitchgrab has read it. */\n overview: string | null;\n createdAt: string;\n}\n\nexport interface CallAudio {\n /**\n * `call` — a bot recording's single mixed track. An extension recording has\n * two instead: `client` (everyone else on the call) and `recorder` (the\n * person who recorded it).\n */\n track: \"call\" | \"client\" | \"recorder\";\n /** Plays for {@link Call.linksExpireInSeconds}. */\n url: string;\n}\n\nexport interface CallMoment {\n /** Milliseconds into the call — seek the audio here. */\n tMs: number | null;\n speaker: string | null;\n text: string;\n}\n\n/** One shared call in full. */\nexport interface Call extends CallListItem {\n meetUrl: string | null;\n audio: CallAudio[];\n /** Audio and screenshot links stop working after this many seconds. Read the call per render; never store them. */\n linksExpireInSeconds: number;\n transcript: string | null;\n summary: { overview: string; moments: CallMoment[] } | null;\n /** Screenshots grabbed during the call, a page at a time. `tMs` is on the transcript's clock. */\n frames: { total: number; offset: number; limit: number; items: { tMs: number; url: string }[] };\n}\n\n/** Options for {@link listCalls}, {@link getCall} and {@link deleteCall}. */\nexport interface CallsOptions {\n /**\n * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.\n * Server only — never a `NEXT_PUBLIC_` variable: it plays and deletes calls.\n */\n key?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /** Give up after this many ms. Default 5000. */\n timeoutMs?: number;\n}\n\nexport interface ListCallsOptions extends CallsOptions {\n /** Default 20, max 100. */\n limit?: number;\n /** ISO date: only calls recorded before it. Pass the last call's `createdAt` for the next page. */\n before?: string;\n}\n\nexport interface GetCallOptions extends CallsOptions {\n /** First screenshot to return. Default 0. */\n framesOffset?: number;\n /** Screenshots per page. Default 50, max 200, `0` for none. */\n framesLimit?: number;\n}\n\nexport type DeleteCallResult = { deleted: true } | { deleted: false; error: string };\n\nconst CALLS_KEY_PREFIX = \"ggc_\";\n\n/** Only a `ggc_` key. A `gg_` token here would only 401 — and means the public token sits where a secret belongs. */\nfunction callsKey(options: CallsOptions): string | null {\n const key = options.key ?? readEnv(\"GLITCHGRAB_CALLS_KEY\");\n return typeof key === \"string\" && key.startsWith(CALLS_KEY_PREFIX) ? key : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction numberOrNull(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction readCallListItem(value: unknown): CallListItem | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n title: stringOrNull(value.title),\n startsAt: stringOrNull(value.startsAt),\n endsAt: stringOrNull(value.endsAt),\n durationSec: numberOrNull(value.durationSec),\n recorder: stringOrNull(value.recorder),\n participants: Array.isArray(value.participants)\n ? value.participants.filter(isRecord).map((p) => ({ name: stringOrNull(p.name), email: stringOrNull(p.email) }))\n : [],\n hasRecording: value.hasRecording === true,\n transcriptStatus: typeof value.transcriptStatus === \"string\" ? value.transcriptStatus : \"IDLE\",\n overview: stringOrNull(value.overview),\n createdAt: typeof value.createdAt === \"string\" ? value.createdAt : \"\",\n };\n}\n\nconst AUDIO_TRACKS: ReadonlySet<string> = new Set([\"call\", \"client\", \"recorder\"]);\n\nfunction readCall(value: unknown): Call | null {\n const base = readCallListItem(value);\n if (!base || !isRecord(value)) return null;\n\n const rawSummary = value.summary;\n const summary =\n isRecord(rawSummary) && typeof rawSummary.overview === \"string\"\n ? {\n overview: rawSummary.overview,\n moments: Array.isArray(rawSummary.moments)\n ? rawSummary.moments\n .filter(isRecord)\n .filter((m) => typeof m.text === \"string\")\n .map((m) => ({ tMs: numberOrNull(m.tMs), speaker: stringOrNull(m.speaker), text: m.text as string }))\n : [],\n }\n : null;\n\n const frames = isRecord(value.frames) ? value.frames : {};\n\n return {\n ...base,\n meetUrl: stringOrNull(value.meetUrl),\n audio: Array.isArray(value.audio)\n ? value.audio\n .filter(isRecord)\n .filter((a) => typeof a.url === \"string\" && typeof a.track === \"string\" && AUDIO_TRACKS.has(a.track))\n .map((a) => ({ track: a.track as CallAudio[\"track\"], url: a.url as string }))\n : [],\n linksExpireInSeconds: numberOrNull(value.linksExpireInSeconds) ?? 0,\n transcript: stringOrNull(value.transcript),\n summary,\n frames: {\n total: numberOrNull(frames.total) ?? 0,\n offset: numberOrNull(frames.offset) ?? 0,\n limit: numberOrNull(frames.limit) ?? 0,\n items: Array.isArray(frames.items)\n ? frames.items\n .filter(isRecord)\n .filter((f) => typeof f.url === \"string\" && typeof f.tMs === \"number\")\n .map((f) => ({ tMs: f.tMs as number, url: f.url as string }))\n : [],\n },\n };\n}\n\nfunction setNumber(query: URLSearchParams, name: string, value: unknown): void {\n if (typeof value === \"number\" && Number.isFinite(value)) query.set(name, String(Math.floor(value)));\n}\n\n/**\n * Calls shared with the client on the key's project, newest first. No links —\n * open one with {@link getCall}.\n *\n * ```ts\n * // app/admin/calls/page.tsx — server component\n * import { listCalls } from \"glitchgrab/server\";\n *\n * export default async function CallsPage() {\n * const calls = await listCalls(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * return calls.map((c) => <a key={c.id} href={`/admin/calls/${c.id}`}>{c.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws.\n */\nexport async function listCalls(options: ListCallsOptions = {}): Promise<CallListItem[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n setNumber(query, \"limit\", options.limit);\n if (typeof options.before === \"string\" && options.before) query.set(\"before\", options.before);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls${qs ? `?${qs}` : \"\"}`, key, options, { cache: \"no-store\" });\n if (!Array.isArray(data)) return [];\n return data.map(readCallListItem).filter((call): call is CallListItem => call !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One shared call: audio links, transcript, summary and a page of screenshots.\n * Links expire after `linksExpireInSeconds` — read the call when the page\n * renders and never store them.\n *\n * Returns `null` when the call does not exist, is not shared, and on any\n * failure — call `notFound()` on it. Never throws.\n */\nexport async function getCall(id: string, options: GetCallOptions = {}): Promise<Call | null> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted || !key) return null;\n\n const query = new URLSearchParams({ id: wanted });\n setNumber(query, \"framesOffset\", options.framesOffset);\n setNumber(query, \"framesLimit\", options.framesLimit);\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls?${query.toString()}`, key, options, { cache: \"no-store\" });\n return readCall(data);\n } catch {\n return null;\n }\n}\n\n/**\n * Permanently delete one shared call — the recording, the screenshots, the\n * transcript. Cannot be undone.\n *\n * Resolves `{ deleted: false, error }` instead of throwing: the reason is worth\n * showing (\"The bot is still in this call — delete it after the call ends\").\n */\nexport async function deleteCall(id: string, options: CallsOptions = {}): Promise<DeleteCallResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n if (!wanted) return { deleted: false, error: \"No call id\" };\n const key = callsKey(options);\n if (!key) return { deleted: false, error: \"No server key — set GLITCHGRAB_CALLS_KEY (ggc_…)\" };\n\n const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return error === null ? { deleted: true } : { deleted: false, error };\n } catch {\n return { deleted: false, error: \"Could not delete the call\" };\n }\n}\n\n/**\n * `JSON.stringify` that is safe inside `<script type=\"application/ld+json\">`.\n *\n * Guide titles and summaries are written outside your codebase, and\n * `JSON.stringify` leaves `<` alone — a title containing `</script>` closes the\n * tag and whatever follows runs as HTML. The JSON unicode escape for `<` is the same\n * character to a JSON parser and inert to the HTML one. `>`, `&` and the two\n * JavaScript line separators are escaped for the same reason.\n *\n * ```tsx\n * <script\n * type=\"application/ld+json\"\n * dangerouslySetInnerHTML={{ __html: escapeJsonForScript({ \"@type\": \"HowTo\", name: guide.title }) }}\n * />\n * ```\n *\n * Never throws: a value JSON cannot represent (circular, `undefined`) becomes `null`.\n */\nexport function escapeJsonForScript(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return \"null\";\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n } catch {\n return \"null\";\n }\n}\n\nexport type { ReportResult, ReportSeverity, ReportType } from \"./types\";\n","import type { RuntimeInfo } from \"./types\";\n\n/**\n * Runtime health at the moment of the crash. `deviceInfo` says what the machine\n * is; this says what shape it was in — a leak that ran the heap to its ceiling,\n * a 2G connection that starved a fetch, a bug that only shows up after ten\n * minutes on the page or on the fifth error in a row.\n */\n\nconst pageLoadedAt = Date.now();\nlet errorCount = 0;\n\n/** Bumped by every auto-captured error so a report says which one in the run it was. */\nexport function incrementErrorCount(): number {\n errorCount += 1;\n return errorCount;\n}\n\n/** Test-only — reset the per-session counter. */\nexport function resetErrorCount(): void {\n errorCount = 0;\n}\n\ninterface MemoryCapableWindow {\n memory?: { usedJSHeapSize?: number; jsHeapSizeLimit?: number };\n}\n\ninterface NetworkInformation {\n effectiveType?: string;\n downlink?: number;\n rtt?: number;\n saveData?: boolean;\n}\n\nconst toMb = (bytes: number | undefined): number | undefined =>\n typeof bytes === \"number\" ? Math.round((bytes / 1048576) * 10) / 10 : undefined;\n\nexport function captureRuntimeInfo(): RuntimeInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n\n // Chromium-only, and absent entirely under cross-origin isolation.\n const memory = (performance as unknown as MemoryCapableWindow)?.memory;\n // Chromium + Android only.\n const connection = (navigator as unknown as { connection?: NetworkInformation })?.connection;\n\n return {\n timeOnPageMs: Date.now() - pageLoadedAt,\n errorCount,\n visibility: typeof document !== \"undefined\" ? document.visibilityState : \"unknown\",\n ...(memory\n ? {\n jsHeapUsedMb: toMb(memory.usedJSHeapSize),\n jsHeapLimitMb: toMb(memory.jsHeapSizeLimit),\n }\n : {}),\n ...(connection\n ? {\n connectionType: connection.effectiveType,\n downlinkMbps: connection.downlink,\n rttMs: connection.rtt,\n saveData: connection.saveData,\n }\n : {}),\n };\n } catch {\n return null;\n }\n}\n","import type {\n CapturedContext,\n DeviceInfo,\n FeedbackPayload,\n FeedbackResult,\n ReportPayload,\n ReportResult,\n} from \"./types\";\n// The React-free subpath: utils.ts is also bundled into `glitchgrab/server`.\nimport { assistOffline, toAssistTurnResult } from \"@glitchgrab/report-ui/assist-response\";\nimport type { AssistTurnResult } from \"@glitchgrab/report-ui\";\nimport { getBreadcrumbs } from \"./breadcrumbs\";\nimport { captureRuntimeInfo } from \"./runtime\";\nimport { getAppContext, getRelease } from \"./app-context\";\n\nconst SENSITIVE_PARAMS = [\n \"token\", \"key\", \"secret\", \"password\", \"passwd\", \"auth\", \"authorization\",\n \"session\", \"sessionid\", \"session_id\", \"api_key\", \"apikey\", \"access_token\",\n \"refresh_token\", \"client_secret\", \"code\", \"state\", \"nonce\", \"credential\", \"private\",\n];\n\nexport function sanitizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const params = new URLSearchParams(parsed.search);\n let modified = false;\n for (const key of Array.from(params.keys())) {\n if (SENSITIVE_PARAMS.some((s) => key.toLowerCase().includes(s))) {\n params.set(key, \"[REDACTED]\");\n modified = true;\n }\n }\n if (modified) parsed.search = params.toString();\n return parsed.toString();\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nexport function captureDeviceInfo(): DeviceInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n return {\n screenWidth: screen.width,\n screenHeight: screen.height,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n platform: navigator.platform ?? \"unknown\",\n language: navigator.language ?? \"unknown\",\n online: navigator.onLine,\n colorScheme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\",\n devicePixelRatio: window.devicePixelRatio ?? 1,\n };\n } catch {\n return null;\n }\n}\n\nexport function captureContext(visitedPages: string[]): CapturedContext {\n try {\n return {\n url: sanitizeUrl(typeof window !== \"undefined\" ? window.location.href : \"\"),\n userAgent: typeof navigator !== \"undefined\" ? navigator.userAgent : \"\",\n timestamp: new Date().toISOString(),\n visitedPages: visitedPages.map(sanitizeUrl),\n breadcrumbs: getBreadcrumbs(),\n deviceInfo: captureDeviceInfo(),\n runtime: captureRuntimeInfo(),\n appContext: getAppContext(),\n release: getRelease(),\n };\n } catch {\n return {\n url: \"\",\n userAgent: \"\",\n timestamp: new Date().toISOString(),\n visitedPages: [],\n breadcrumbs: [],\n deviceInfo: null,\n runtime: null,\n appContext: {},\n };\n }\n}\n\n/**\n * Metadata every report carries regardless of how it was raised — release,\n * runtime health, and the host app's own context. One builder so a user-filed\n * report and an auto-captured crash describe the same world; the payload sites\n * only add what is specific to them.\n *\n * App context is prefixed so a key named `timestamp` or `status` can't quietly\n * overwrite a field the dashboard relies on.\n */\nexport function contextMetadata(context: CapturedContext): Record<string, string> {\n try {\n const runtime = context.runtime;\n const appContext = Object.entries(context.appContext ?? {}).reduce<Record<string, string>>(\n (acc, [key, value]) => {\n acc[`ctx_${key}`] = value;\n return acc;\n },\n {}\n );\n\n return {\n ...(context.release ? { release: context.release } : {}),\n ...(runtime\n ? {\n timeOnPageMs: String(runtime.timeOnPageMs),\n errorCount: String(runtime.errorCount),\n visibility: runtime.visibility,\n ...(runtime.jsHeapUsedMb !== undefined\n ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) }\n : {}),\n ...(runtime.jsHeapLimitMb !== undefined\n ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) }\n : {}),\n ...(runtime.connectionType ? { connectionType: runtime.connectionType } : {}),\n ...(runtime.downlinkMbps !== undefined\n ? { downlinkMbps: String(runtime.downlinkMbps) }\n : {}),\n ...(runtime.rttMs !== undefined ? { rttMs: String(runtime.rttMs) } : {}),\n ...(runtime.saveData !== undefined ? { saveData: String(runtime.saveData) } : {}),\n }\n : {}),\n ...appContext,\n };\n } catch {\n return {};\n }\n}\n\nexport const DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 500;\n\n/**\n * Send a report and return the result.\n * Never throws — returns null on failure.\n */\nexport async function sendReport(\n payload: ReportPayload,\n baseUrl?: string\n): Promise<ReportResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/report`;\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n };\n\n // keepalive has a 64KB body limit in browsers — skip it for large payloads (e.g., screenshots)\n const useKeepalive = body.length < 60_000;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<ReportResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n // Fallback: sendBeacon (fire-and-forget, no result)\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n try {\n navigator.sendBeacon(url, new Blob([body], { type: \"application/json\" }));\n } catch {\n // Silently fail\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a star rating your end-user left about your app.\n * Never throws — returns null on failure.\n *\n * Unlike `sendReport`, this is always a deliberate user action with a dialog\n * open in front of them, so it retries on transient failure but skips the\n * sendBeacon fallback: the caller needs a real result to show a thank-you.\n */\nexport async function sendFeedback(\n payload: FeedbackPayload,\n baseUrl?: string\n): Promise<FeedbackResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/feedback`;\n const body = JSON.stringify(payload);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<FeedbackResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n // 4xx other than rate limiting is a caller mistake — retrying won't help.\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface EnhanceContext {\n url?: string;\n visitedPages?: string[];\n breadcrumbs?: Array<{ type: string; message: string }>;\n}\n\n/**\n * The signed-in user as the host's server vouched for them (`signSession`).\n *\n * Rides in the request BODY, never a header: every token route sets its own\n * CORS allow-list of headers, so a new header would fail preflight on all of\n * them. Omitted entirely when there is no signature — an unsigned id proves\n * nothing, and the server reads its absence as \"unsigned\".\n */\nexport interface SignedIdentity {\n userId?: string | null;\n signature?: string | null;\n}\n\n/** `{ userId, signature }` to spread into a JSON body, or nothing. */\nexport function signedFields(identity?: SignedIdentity | null): { userId?: string; signature?: string } {\n if (!identity?.signature || !identity.userId) return {};\n return { userId: identity.userId, signature: identity.signature };\n}\n\n/**\n * Polish raw description text via the Glitchgrab AI enhance endpoint.\n * Returns the polished text on success, or the original text on any failure.\n * Never throws.\n */\nexport async function enhanceText(\n text: string,\n token: string,\n baseUrl?: string,\n screenshot?: string | null,\n context?: EnhanceContext | null,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n const trimmed = text.trim();\n if (!trimmed) return text;\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n text: trimmed,\n ...(screenshot ? { screenshot } : {}),\n ...(context ? { context } : {}),\n ...signedFields(identity),\n }),\n });\n if (!response.ok) return text;\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { text?: string };\n };\n if (envelope?.success && typeof envelope.data?.text === \"string\") {\n return envelope.data.text;\n }\n return text;\n } catch {\n return text;\n }\n}\n\n/**\n * \"Is this already filed?\" for the report dialog's plain form. Asked when Send\n * is pressed; a match lets the reporter add to the open issue instead of\n * opening a second one.\n *\n * `[]` when the project has it off, `null` on any failure — the dialog sends\n * straight away on either. Never throws.\n */\nexport async function findSimilarIssues(\n description: string,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<{ number: number; title: string; url: string; status?: string }[] | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/reports/similar`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ text: description, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; matches?: { number: number; title: string; url: string; status?: string }[] };\n } | null;\n if (!response.ok || !envelope?.success) return null;\n if (!envelope.data?.enabled) return [];\n return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];\n } catch {\n return null;\n }\n}\n\n/**\n * One turn of the AI report assistant (#330).\n *\n * Same never-throw contract as everything else in the SDK: a failure comes back\n * as `degraded` and the dialog quietly falls back to the plain form. An\n * end-user of the host app must never see a Glitchgrab error, and must never be\n * blocked from filing a bug because a model was busy.\n */\nexport async function assistTurn(\n params: {\n messages: { role: \"user\" | \"assistant\"; content: string }[];\n conversationId: string | null;\n screenshot?: string | null;\n context?: Record<string, unknown> | null;\n /**\n * Who the host app says is logged in, from the provider's `session` prop.\n * Stored against the conversation so a chat and the report it produced\n * agree on who filed — one SDK token covers every end user of the host app,\n * so without this every stranger's chat looks like the same person.\n */\n reporter?: {\n key?: string | null;\n name?: string | null;\n email?: string | null;\n /** `signSession()` output for `key`; absent when the host did not sign. */\n signature?: string | null;\n };\n },\n token: string,\n baseUrl?: string\n): Promise<AssistTurnResult> {\n const offline = assistOffline();\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/report-chat`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(params),\n });\n // One mapping for every host (packages/report-ui/src/assist-response.ts):\n // the server's degrade reason passes through, and nothing the sheet renders\n // is dropped.\n return toAssistTurnResult(response.ok, await response.json().catch(() => null));\n } catch {\n return offline;\n }\n}\n\n/**\n * Send an audio Blob to the Glitchgrab STT proxy (which calls Sarvam).\n * Returns the transcript string, or \"\" on any failure. Never throws.\n */\nexport async function transcribeAudio(\n blob: Blob,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n if (blob.size === 0) return \"\";\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/stt`;\n const form = new FormData();\n form.append(\"file\", blob, \"audio.webm\");\n const signed = signedFields(identity);\n if (signed.userId && signed.signature) {\n form.append(\"userId\", signed.userId);\n form.append(\"signature\", signed.signature);\n }\n const response = await fetch(url, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: form,\n });\n if (!response.ok) return \"\";\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { transcript?: string };\n };\n if (envelope?.success && typeof envelope.data?.transcript === \"string\") {\n return envelope.data.transcript;\n }\n return \"\";\n } catch {\n return \"\";\n }\n}\n"],"mappings":"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","names":["_a"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/runtime.ts","../src/utils.ts"],"sourcesContent":["/**\n * Server-side reporting — `import { reportServerError } from \"glitchgrab/server\"`.\n *\n * The rest of this SDK is `\"use client\"`: it hooks `window.onerror`, walks the\n * DOM for a screenshot, and needs a mounted provider for its token. None of\n * that exists in a cron job at 3am, an API route handler, or a queue worker —\n * which is precisely where the failures nobody is watching happen. A WhatsApp\n * send Meta rejects, an SMTP timeout, a nightly digest that throws: no tab is\n * open, so the browser SDK never sees them and no issue is ever filed.\n *\n * This entry has no `\"use client\"`, touches no DOM API, and imports no React.\n * It POSTs to the same `/api/v1/sdk/report` endpoint the browser uses, with\n * `source: \"SDK_AUTO\"` so the server-side dedup applies (24h per signature,\n * 7 days once an issue exists) — otherwise a cron failing hourly would open\n * twenty-four identical issues a day.\n *\n * Like the rest of the SDK it never throws: a reporting failure must not turn\n * a handled error into an unhandled one.\n */\nimport { createHmac } from \"node:crypto\";\nimport type { ReportPayload, ReportResult, ReportSeverity, ReportType } from \"./types\";\nimport { DEFAULT_BASE_URL, sendReport } from \"./utils\";\n\n/** Who to attribute the report to. A server has no signed-in user, so this is the job or service. */\nexport interface ServerReporterIdentity {\n /** Stable id — the cron name, the service name. Defaults to `\"server\"`. */\n id?: string;\n /** Display name shown on the report. Defaults to `\"Server\"`. */\n name?: string;\n email?: string;\n phone?: string;\n}\n\n/** Defaults applied to every server report. Set once at boot with {@link configureServerReporter}. */\nexport interface ServerReporterConfig {\n /** Repo token (`gg_…`). Falls back to `process.env.GLITCHGRAB_TOKEN`. */\n token?: string;\n /** Override the API host. Falls back to `process.env.GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Where this ran — `\"cron/attendance-prompts\"`, `\"worker/email-queue\"`.\n *\n * It is the grouping key, not a link: it is sent as the report's `pageUrl`\n * so two different jobs throwing the same `\"Timeout\"` stay two issues\n * instead of collapsing into one.\n */\n context?: string;\n /** Merged into every report's metadata. Good place for region, deployment id, service name. */\n metadata?: Record<string, string>;\n reporter?: ServerReporterIdentity;\n /**\n * Report from `NODE_ENV=development` too. Off by default, matching the\n * browser SDK — a laptop crashing its way through a refactor should not file\n * issues. Unlike the browser there is no `Origin` header for the API to\n * recognise, so nothing else stops a dev machine from filing.\n */\n enableInDevelopment?: boolean;\n}\n\n/** Per-call options. Anything set here wins over {@link configureServerReporter}. */\nexport interface ServerReportOptions extends ServerReporterConfig {\n /** Issue type. Defaults to `BUG`. */\n type?: ReportType;\n /** Rendered as a `severity:<value>` label on the GitHub issue. */\n severity?: ReportSeverity;\n /** Extra prose for the issue body — what the job was doing, which record it was on. */\n description?: string;\n /**\n * A real request URL, when there is one (an API route handler). Overrides\n * the synthetic URL built from `context`.\n */\n pageUrl?: string;\n}\n\nconst DEV_BLOCKED_MESSAGE = \"development\";\n\n/**\n * Node's `process`, as much of it as this file uses.\n *\n * `src/global.d.ts` declares a browser-shaped `process` with nothing but\n * `env`, and that is deliberate — the client bundle must not be able to reach\n * for Node APIs. So this entry reads the real one off `globalThis` behind its\n * own narrow type instead of pulling `@types/node` into the package.\n */\ninterface NodeProcessLike {\n env?: Record<string, string | undefined>;\n version?: string;\n on?(event: string, listener: (arg: never) => void): unknown;\n off?(event: string, listener: (arg: never) => void): unknown;\n listenerCount?(event: string): number;\n}\n\nfunction nodeProcess(): NodeProcessLike | undefined {\n try {\n const candidate = (globalThis as { process?: NodeProcessLike }).process;\n return candidate && typeof candidate === \"object\" ? candidate : undefined;\n } catch {\n return undefined;\n }\n}\n\nlet defaults: ServerReporterConfig = {};\n\n/**\n * Set the token and the defaults once, at module load, instead of passing them\n * to every call site. A later call's own options still win.\n */\nexport function configureServerReporter(config: ServerReporterConfig): void {\n defaults = { ...defaults, ...config };\n}\n\n/** Test-only — drop everything {@link configureServerReporter} set. */\nexport function resetServerReporter(): void {\n defaults = {};\n}\n\nfunction readEnv(name: string): string | undefined {\n try {\n return nodeProcess()?.env?.[name];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Message and stack out of whatever was thrown.\n *\n * A thrown string, a rejected `{ code, message }`, an `undefined` from a\n * `Promise.reject()` — all of it reaches here, and all of it must produce a\n * non-empty message: the API's dedup signature is `null` without one, and a\n * report with no signature is a fresh issue every single time.\n */\nfunction describeError(error: unknown): { message: string; stack?: string } {\n try {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || \"Unknown server error\",\n stack: error.stack,\n };\n }\n if (typeof error === \"string\") {\n return { message: error || \"Unknown server error\" };\n }\n if (error && typeof error === \"object\") {\n const maybe = error as { message?: unknown; stack?: unknown };\n const message = typeof maybe.message === \"string\" ? maybe.message : JSON.stringify(error);\n return {\n message: message || \"Unknown server error\",\n stack: typeof maybe.stack === \"string\" ? maybe.stack : undefined,\n };\n }\n return { message: String(error ?? \"\") || \"Unknown server error\" };\n } catch {\n return { message: \"Unknown server error\" };\n }\n}\n\n/**\n * `pageUrl` doubles as the dedup grouping key, so a job needs one even though\n * no page was involved. `server://` keeps it obviously synthetic — and keeps it\n * clear of the API's localhost check, which only matches http(s) and would\n * otherwise swallow the report with a 200 and create nothing.\n */\nfunction contextUrl(context: string | undefined): string | undefined {\n if (!context) return undefined;\n return `server://${context.replace(/^\\/+/, \"\")}`;\n}\n\nfunction runtimeMetadata(): Record<string, string> {\n try {\n const proc = nodeProcess();\n if (!proc) return { runtime: \"server\" };\n const region = proc.env?.VERCEL_REGION;\n const deploymentEnv = proc.env?.VERCEL_ENV;\n return {\n runtime: \"node\",\n ...(proc.version ? { nodeVersion: proc.version } : {}),\n ...(region ? { region } : {}),\n ...(deploymentEnv ? { deploymentEnv } : {}),\n };\n } catch {\n return { runtime: \"server\" };\n }\n}\n\n/**\n * File a server-side error as a GitHub issue.\n *\n * Await it. On a serverless platform the function may be frozen the moment your\n * handler returns, and a floating promise dies with it — the report never\n * leaves the machine.\n *\n * ```ts\n * try {\n * await sendDigest();\n * } catch (error) {\n * await reportServerError(error, { context: \"cron/daily-digest\" });\n * throw error;\n * }\n * ```\n *\n * Returns `null` when nothing was filed — no token, development mode, or the\n * API refused it. Never throws.\n */\nexport async function reportServerError(\n error: unknown,\n options: ServerReportOptions = {}\n): Promise<ReportResult | null> {\n try {\n const token = options.token ?? defaults.token ?? readEnv(\"GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const enableInDevelopment = options.enableInDevelopment ?? defaults.enableInDevelopment ?? false;\n if (!enableInDevelopment && readEnv(\"NODE_ENV\") === DEV_BLOCKED_MESSAGE) return null;\n\n const { message, stack } = describeError(error);\n const context = options.context ?? defaults.context;\n const reporter = { ...defaults.reporter, ...options.reporter };\n\n const payload: ReportPayload = {\n token,\n source: \"SDK_AUTO\",\n type: options.type ?? \"BUG\",\n errorMessage: message,\n ...(stack ? { errorStack: stack } : {}),\n ...(options.description ? { description: options.description } : {}),\n ...(options.pageUrl ?? contextUrl(context)\n ? { pageUrl: options.pageUrl ?? contextUrl(context) }\n : {}),\n metadata: {\n ...runtimeMetadata(),\n ...defaults.metadata,\n ...options.metadata,\n ...(context ? { serverContext: context } : {}),\n ...(options.severity ? { severity: options.severity } : {}),\n sessionUserId: reporter.id ?? context ?? \"server\",\n sessionUserName: reporter.name ?? \"Server\",\n ...(reporter.email ? { sessionUserEmail: reporter.email } : {}),\n ...(reporter.phone ? { sessionUserPhone: reporter.phone } : {}),\n },\n };\n\n return await sendReport(payload, options.baseUrl ?? defaults.baseUrl ?? readEnv(\"GLITCHGRAB_BASE_URL\"));\n } catch {\n return null;\n }\n}\n\n/** Options for {@link captureServerErrors}. */\nexport interface CaptureServerErrorsOptions extends ServerReportOptions {\n /**\n * Also report rejected promises nobody handled. Default `true`.\n *\n * Node crashes the process on an unhandled rejection — but only while no\n * `unhandledRejection` listener exists. Attaching one silently converts a\n * crash into a warning, so this re-throws afterwards to put the default\n * behaviour back, unless the host app has its own listener (then it is theirs\n * to decide).\n */\n captureUnhandledRejections?: boolean;\n}\n\n/**\n * Report every uncaught exception and unhandled rejection in this process.\n *\n * Call it once at boot — `instrumentation.ts` in a Next.js app. Returns a\n * function that removes the listeners again.\n *\n * Uncaught exceptions use `uncaughtExceptionMonitor`, which observes without\n * taking over: the process still crashes exactly as it would have. A reporter\n * that keeps a broken process alive is worse than no reporter.\n */\nexport function captureServerErrors(options: CaptureServerErrorsOptions = {}): () => void {\n try {\n const proc = nodeProcess();\n if (!proc || typeof proc.on !== \"function\") return () => {};\n\n const onException = (error: unknown) => {\n void reportServerError(error, { ...options, context: options.context ?? \"uncaughtException\" });\n };\n\n const onRejection = (reason: unknown) => {\n void reportServerError(reason, {\n ...options,\n context: options.context ?? \"unhandledRejection\",\n });\n\n // Ours is the only listener, so suppressing the crash was never the\n // host app's choice. Hand the default behaviour back.\n if (proc.listenerCount?.(\"unhandledRejection\") === 1) {\n setTimeout(() => {\n throw reason;\n }, 0);\n }\n };\n\n proc.on(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n\n const captureRejections = options.captureUnhandledRejections ?? true;\n if (captureRejections) proc.on(\"unhandledRejection\", onRejection as (arg: never) => void);\n\n return () => {\n try {\n proc.off?.(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n if (captureRejections) {\n proc.off?.(\"unhandledRejection\", onRejection as (arg: never) => void);\n }\n } catch {\n // Nothing to undo.\n }\n };\n } catch {\n return () => {};\n }\n}\n\n/** Options for {@link signSession}. */\nexport interface SignSessionOptions {\n /** Your user's id — the same value you pass as `session.userId`. */\n userId: string;\n /** The repo's signing secret (`ggs_…`). Falls back to `process.env.GLITCHGRAB_SIGNING_SECRET`. */\n secret?: string;\n /** How long the signature stays valid, in seconds. Default one day. */\n ttlSeconds?: number;\n /** Clock override in epoch ms. For tests. */\n now?: number;\n}\n\n/**\n * Vouch for the logged-in user so the SDK's AI features work when the project\n * owner turned on signed users.\n *\n * The `gg_` token ships in your JavaScript, so on its own it proves nothing\n * about who is calling. Your server holds the signing secret; call this where\n * you build the `session` prop and pass the result as `session.signature`.\n * Render it per request so it refreshes before `ttlSeconds` runs out.\n *\n * Format: `v1.<exp>.<base64url HMAC-SHA256(secret, \"v1.<exp>.<userId>\")>`.\n *\n * Unlike the rest of this SDK it throws: it runs in your own server code, where\n * a missing secret is a deploy mistake you want to see — not a user who quietly\n * loses the AI features.\n */\nexport function signSession(options: SignSessionOptions): string {\n const userId = options?.userId;\n if (typeof userId !== \"string\" || userId.length === 0) {\n throw new Error(\"glitchgrab signSession: userId is required\");\n }\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n const secret = options.secret || env?.GLITCHGRAB_SIGNING_SECRET;\n if (!secret) {\n throw new Error(\n \"glitchgrab signSession: no signing secret — pass `secret` or set GLITCHGRAB_SIGNING_SECRET\"\n );\n }\n const ttl = options.ttlSeconds ?? 86_400;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new Error(\"glitchgrab signSession: ttlSeconds must be a positive number\");\n }\n const exp = Math.floor((options.now ?? Date.now()) / 1000) + Math.floor(ttl);\n const sig = createHmac(\"sha256\", secret).update(`v1.${exp}.${userId}`).digest(\"base64url\");\n return `v1.${exp}.${sig}`;\n}\n\n// ── Guides ───────────────────────────────────────────────────────────────────\n//\n// A team writes its how-tos in Glitchgrab and renders them on its own site from\n// `GET /api/v1/sdk/guides` (#370). Every site needs the same client — token,\n// caching, never failing the page, shape checks — so it lives here instead of\n// being rewritten, slightly differently, in each product (#409).\n\n/** One guide as the list returns it — no body. Dates are ISO strings: this came over JSON. */\nexport interface GuideSummary {\n slug: string;\n title: string;\n /** One line saying what the guide answers. */\n summary: string;\n category: string | null;\n /** The order set in Glitchgrab. The list already arrives sorted by it. */\n orderIndex: number;\n /** Always `\"PUBLISHED\"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */\n status: \"DRAFT\" | \"PUBLISHED\";\n /** When someone last re-checked the steps against the product. */\n lastVerifiedAt: string | null;\n updatedAt: string;\n /**\n * Where this guide lives on your site: its own link, else\n * `<guides base URL>/<slug>`, else null when the repo sets neither.\n */\n link: string | null;\n}\n\n/** One guide with its body. */\nexport interface Guide extends GuideSummary {\n /** The markdown as written. */\n content: string;\n /**\n * `content` rendered by Glitchgrab: raw HTML in the markdown arrives as text,\n * only allowlisted attributes survive, and links keep only http(s), mailto\n * and relative targets. Headings carry `id=\"guide-<heading text>\"`, for a\n * table of contents or a link to a single step.\n *\n * Safe to render as-is. On a domain that holds your users' sessions, a second\n * sanitize pass of your own is cheap defence in depth — this SDK ships no\n * sanitizer so it stays dependency-free.\n */\n html: string;\n}\n\n/** Options for {@link listGuides} and {@link getGuide}. */\nexport interface GuideReadOptions {\n /**\n * Repo token (`gg_…`). Falls back to {@link configureServerReporter}'s token,\n * then `GLITCHGRAB_TOKEN`, then `NEXT_PUBLIC_GLITCHGRAB_TOKEN` — the token is\n * public, so the one your provider already uses is fine.\n *\n * Never pass the `ggc_` server key for a public page: it reads drafts too, and client calls.\n */\n token?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Seconds Next.js may cache the response. Default 300; `0` reads fresh on\n * every request. The route is `force-dynamic`, so without this nothing is\n * cached at all. Ignored outside Next.js.\n */\n revalidate?: number;\n /** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */\n timeoutMs?: number;\n}\n\nconst GUIDES_REVALIDATE_SECONDS = 300;\nconst SDK_API_TIMEOUT_MS = 5000;\n\n/** `RequestInit` plus Next.js's cache option, without pulling Next's types into this entry. */\ntype NextFetchInit = RequestInit & { next?: { revalidate?: number } };\n\ninterface SdkApiResult {\n /** `data` from a successful response, else null. */\n data: unknown;\n /** Null on success; the route's own `error` when it sent one, so a caller can show why. */\n error: string | null;\n}\n\n/**\n * One request to a Glitchgrab SDK route. Never throws: a refusal, a timeout, a\n * network failure and unreadable JSON all come back as `error`.\n */\nasync function callSdkApi(\n path: string,\n token: string,\n options: { baseUrl?: string; timeoutMs?: number },\n init: { method?: \"GET\" | \"PUT\" | \"DELETE\"; body?: unknown; cache: { revalidate: number } | \"no-store\" }\n): Promise<SdkApiResult> {\n const baseUrl = (\n options.baseUrl ??\n defaults.baseUrl ??\n readEnv(\"GLITCHGRAB_BASE_URL\") ??\n DEFAULT_BASE_URL\n ).replace(/\\/+$/, \"\");\n\n const controller = typeof AbortController === \"function\" ? new AbortController() : undefined;\n const timer = controller\n ? setTimeout(() => controller.abort(), options.timeoutMs ?? SDK_API_TIMEOUT_MS)\n : undefined;\n\n try {\n const request: NextFetchInit = {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),\n ...(init.cache === \"no-store\" ? { cache: \"no-store\" as const } : { next: { revalidate: init.cache.revalidate } }),\n ...(controller ? { signal: controller.signal } : {}),\n };\n const response = await fetch(`${baseUrl}${path}`, request);\n const body = (await response.json().catch(() => null)) as\n | { success?: unknown; data?: unknown; error?: unknown }\n | null;\n if (!response.ok || body?.success !== true) {\n return { data: null, error: typeof body?.error === \"string\" ? body.error : `HTTP ${response.status}` };\n }\n return { data: body.data ?? null, error: null };\n } catch {\n return { data: null, error: \"Could not reach Glitchgrab\" };\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/** `data` from the guides route, or null on any failure — no token, non-2xx, timeout, bad JSON. */\nasync function readGuidesApi(query: string, options: GuideReadOptions): Promise<unknown> {\n const token =\n options.token ??\n defaults.token ??\n readEnv(\"GLITCHGRAB_TOKEN\") ??\n readEnv(\"NEXT_PUBLIC_GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const revalidate =\n typeof options.revalidate === \"number\" && Number.isFinite(options.revalidate) && options.revalidate >= 0\n ? options.revalidate\n : GUIDES_REVALIDATE_SECONDS;\n\n const result = await callSdkApi(`/api/v1/sdk/guides${query}`, token, options, { cache: { revalidate } });\n return result.data;\n}\n\n/**\n * A guide summary out of whatever the API sent, or null when it is not one.\n * Only slug, title and summary are required — a page cannot render without\n * them. The rest default, so the returned type is honest either way.\n */\nfunction readGuideSummary(value: unknown): GuideSummary | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const guide = value as Record<string, unknown>;\n if (\n typeof guide.slug !== \"string\" ||\n typeof guide.title !== \"string\" ||\n typeof guide.summary !== \"string\"\n ) {\n return null;\n }\n return {\n slug: guide.slug,\n title: guide.title,\n summary: guide.summary,\n category: typeof guide.category === \"string\" ? guide.category : null,\n orderIndex: typeof guide.orderIndex === \"number\" ? guide.orderIndex : 0,\n status: guide.status === \"DRAFT\" ? \"DRAFT\" : \"PUBLISHED\",\n lastVerifiedAt: typeof guide.lastVerifiedAt === \"string\" ? guide.lastVerifiedAt : null,\n updatedAt: typeof guide.updatedAt === \"string\" ? guide.updatedAt : \"\",\n link: typeof guide.link === \"string\" ? guide.link : null,\n };\n}\n\n/**\n * Every published guide for the token's repo, in the order set in Glitchgrab.\n * No bodies — this is the index page.\n *\n * ```ts\n * // app/guides/page.tsx\n * import { listGuides } from \"glitchgrab/server\";\n *\n * export default async function GuidesPage() {\n * const guides = await listGuides();\n * return guides.map((g) => <a key={g.slug} href={`/guides/${g.slug}`}>{g.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws: a Glitchgrab outage leaves the\n * page empty instead of turning it into a 500.\n */\nexport async function listGuides(options: GuideReadOptions = {}): Promise<GuideSummary[]> {\n try {\n const data = await readGuidesApi(\"\", options);\n if (!Array.isArray(data)) return [];\n return data.map(readGuideSummary).filter((guide): guide is GuideSummary => guide !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One published guide with its markdown and rendered HTML.\n *\n * Returns `null` when there is no such guide and on any failure — call\n * `notFound()` on it. Never throws.\n */\nexport async function getGuide(slug: string, options: GuideReadOptions = {}): Promise<Guide | null> {\n try {\n const wanted = typeof slug === \"string\" ? slug.trim() : \"\";\n if (!wanted) return null;\n\n const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}`, options);\n const summary = readGuideSummary(data);\n const body = data as { content?: unknown; html?: unknown } | null;\n if (!summary || typeof body?.html !== \"string\") return null;\n\n return {\n ...summary,\n content: typeof body.content === \"string\" ? body.content : \"\",\n html: body.html,\n };\n } catch {\n return null;\n }\n}\n\n// ── Calls ────────────────────────────────────────────────────────────────────\n//\n// Recorded client calls a project shared with its client (#416), for the team's\n// own admin panel. Unlike guides these are private: they need the project's\n// server key (`ggc_…`, API Tokens → Server keys), which lives on the server and never in a bundle, and\n// every response holds links that expire — so nothing here is cached.\n\n/** Someone on the call, as the recorder saw them. */\nexport interface CallParticipant {\n name: string | null;\n email: string | null;\n}\n\n/** One shared call as the list returns it — no transcript, no links. */\nexport interface CallListItem {\n id: string;\n title: string | null;\n startsAt: string | null;\n endsAt: string | null;\n durationSec: number | null;\n /** `\"bot\"` (joined the Meet) or `\"extension\"` (recorded from someone's Chrome). */\n recorder: string | null;\n /** Match a call to your customer by these emails. */\n participants: CallParticipant[];\n hasRecording: boolean;\n /** `IDLE`, `RUNNING`, `DONE` or `FAILED`. */\n transcriptStatus: string;\n /** One line on what the call was about, once Glitchgrab has read it. */\n overview: string | null;\n createdAt: string;\n}\n\nexport interface CallAudio {\n /**\n * `call` — a bot recording's single mixed track. An extension recording has\n * two instead: `client` (everyone else on the call) and `recorder` (the\n * person who recorded it).\n */\n track: \"call\" | \"client\" | \"recorder\";\n /** Plays for {@link Call.linksExpireInSeconds}. */\n url: string;\n}\n\nexport interface CallMoment {\n /** Milliseconds into the call — seek the audio here. */\n tMs: number | null;\n speaker: string | null;\n text: string;\n}\n\n/** One shared call in full. */\nexport interface Call extends CallListItem {\n meetUrl: string | null;\n audio: CallAudio[];\n /** Audio and screenshot links stop working after this many seconds. Read the call per render; never store them. */\n linksExpireInSeconds: number;\n transcript: string | null;\n summary: { overview: string; moments: CallMoment[] } | null;\n /** Screenshots grabbed during the call, a page at a time. `tMs` is on the transcript's clock. */\n frames: { total: number; offset: number; limit: number; items: { tMs: number; url: string }[] };\n}\n\n/** Options for {@link listCalls}, {@link getCall}, {@link deleteCall} and the booking settings calls. */\nexport interface CallsOptions {\n /**\n * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.\n * Server only — never a `NEXT_PUBLIC_` variable: it plays and deletes calls.\n */\n key?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /** Give up after this many ms. Default 5000. */\n timeoutMs?: number;\n}\n\nexport interface ListCallsOptions extends CallsOptions {\n /** Default 20, max 100. */\n limit?: number;\n /** ISO date: only calls recorded before it. Pass the last call's `createdAt` for the next page. */\n before?: string;\n}\n\nexport interface GetCallOptions extends CallsOptions {\n /** First screenshot to return. Default 0. */\n framesOffset?: number;\n /** Screenshots per page. Default 50, max 200, `0` for none. */\n framesLimit?: number;\n}\n\nexport type DeleteCallResult = { deleted: true } | { deleted: false; error: string };\n\nconst CALLS_KEY_PREFIX = \"ggc_\";\nconst NO_KEY_ERROR = \"No server key — set GLITCHGRAB_CALLS_KEY (ggc_…)\";\n\n/** Only a `ggc_` key. A `gg_` token here would only 401 — and means the public token sits where a secret belongs. */\nfunction callsKey(options: CallsOptions): string | null {\n const key = options.key ?? readEnv(\"GLITCHGRAB_CALLS_KEY\");\n return typeof key === \"string\" && key.startsWith(CALLS_KEY_PREFIX) ? key : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction numberOrNull(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction readCallListItem(value: unknown): CallListItem | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n title: stringOrNull(value.title),\n startsAt: stringOrNull(value.startsAt),\n endsAt: stringOrNull(value.endsAt),\n durationSec: numberOrNull(value.durationSec),\n recorder: stringOrNull(value.recorder),\n participants: Array.isArray(value.participants)\n ? value.participants.filter(isRecord).map((p) => ({ name: stringOrNull(p.name), email: stringOrNull(p.email) }))\n : [],\n hasRecording: value.hasRecording === true,\n transcriptStatus: typeof value.transcriptStatus === \"string\" ? value.transcriptStatus : \"IDLE\",\n overview: stringOrNull(value.overview),\n createdAt: typeof value.createdAt === \"string\" ? value.createdAt : \"\",\n };\n}\n\nconst AUDIO_TRACKS: ReadonlySet<string> = new Set([\"call\", \"client\", \"recorder\"]);\n\nfunction readCall(value: unknown): Call | null {\n const base = readCallListItem(value);\n if (!base || !isRecord(value)) return null;\n\n const rawSummary = value.summary;\n const summary =\n isRecord(rawSummary) && typeof rawSummary.overview === \"string\"\n ? {\n overview: rawSummary.overview,\n moments: Array.isArray(rawSummary.moments)\n ? rawSummary.moments\n .filter(isRecord)\n .filter((m) => typeof m.text === \"string\")\n .map((m) => ({ tMs: numberOrNull(m.tMs), speaker: stringOrNull(m.speaker), text: m.text as string }))\n : [],\n }\n : null;\n\n const frames = isRecord(value.frames) ? value.frames : {};\n\n return {\n ...base,\n meetUrl: stringOrNull(value.meetUrl),\n audio: Array.isArray(value.audio)\n ? value.audio\n .filter(isRecord)\n .filter((a) => typeof a.url === \"string\" && typeof a.track === \"string\" && AUDIO_TRACKS.has(a.track))\n .map((a) => ({ track: a.track as CallAudio[\"track\"], url: a.url as string }))\n : [],\n linksExpireInSeconds: numberOrNull(value.linksExpireInSeconds) ?? 0,\n transcript: stringOrNull(value.transcript),\n summary,\n frames: {\n total: numberOrNull(frames.total) ?? 0,\n offset: numberOrNull(frames.offset) ?? 0,\n limit: numberOrNull(frames.limit) ?? 0,\n items: Array.isArray(frames.items)\n ? frames.items\n .filter(isRecord)\n .filter((f) => typeof f.url === \"string\" && typeof f.tMs === \"number\")\n .map((f) => ({ tMs: f.tMs as number, url: f.url as string }))\n : [],\n },\n };\n}\n\nfunction setNumber(query: URLSearchParams, name: string, value: unknown): void {\n if (typeof value === \"number\" && Number.isFinite(value)) query.set(name, String(Math.floor(value)));\n}\n\n/**\n * Calls shared with the client on the key's project, newest first. No links —\n * open one with {@link getCall}.\n *\n * ```ts\n * // app/admin/calls/page.tsx — server component\n * import { listCalls } from \"glitchgrab/server\";\n *\n * export default async function CallsPage() {\n * const calls = await listCalls(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * return calls.map((c) => <a key={c.id} href={`/admin/calls/${c.id}`}>{c.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws.\n */\nexport async function listCalls(options: ListCallsOptions = {}): Promise<CallListItem[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n setNumber(query, \"limit\", options.limit);\n if (typeof options.before === \"string\" && options.before) query.set(\"before\", options.before);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls${qs ? `?${qs}` : \"\"}`, key, options, { cache: \"no-store\" });\n if (!Array.isArray(data)) return [];\n return data.map(readCallListItem).filter((call): call is CallListItem => call !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One shared call: audio links, transcript, summary and a page of screenshots.\n * Links expire after `linksExpireInSeconds` — read the call when the page\n * renders and never store them.\n *\n * Returns `null` when the call does not exist, is not shared, and on any\n * failure — call `notFound()` on it. Never throws.\n */\nexport async function getCall(id: string, options: GetCallOptions = {}): Promise<Call | null> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted || !key) return null;\n\n const query = new URLSearchParams({ id: wanted });\n setNumber(query, \"framesOffset\", options.framesOffset);\n setNumber(query, \"framesLimit\", options.framesLimit);\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls?${query.toString()}`, key, options, { cache: \"no-store\" });\n return readCall(data);\n } catch {\n return null;\n }\n}\n\n/**\n * Permanently delete one shared call — the recording, the screenshots, the\n * transcript. Cannot be undone.\n *\n * Resolves `{ deleted: false, error }` instead of throwing: the reason is worth\n * showing (\"The bot is still in this call — delete it after the call ends\").\n */\nexport async function deleteCall(id: string, options: CallsOptions = {}): Promise<DeleteCallResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n if (!wanted) return { deleted: false, error: \"No call id\" };\n const key = callsKey(options);\n if (!key) return { deleted: false, error: NO_KEY_ERROR };\n\n const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return error === null ? { deleted: true } : { deleted: false, error };\n } catch {\n return { deleted: false, error: \"Could not delete the call\" };\n }\n}\n\n// ── Booking settings ─────────────────────────────────────────────────────────\n//\n// A project's demo booking settings — whether prospects can book, the hours,\n// slot length and the WhatsApp code — so a team's admin panel can change them\n// without a Glitchgrab login. Same server key as calls. Which Google calendar\n// demos land in is not here: it stays set on the Glitchgrab dashboard.\n\n/** `{ \"1\": [[\"09:00\", \"17:00\"]] }` — ISO weekday (1 = Monday) to opening ranges, in {@link BookingSettings.timezone}. */\nexport type BookingWorkingHours = Record<string, [string, string][]>;\n\nexport interface BookingSettings {\n /** Off means the booking dialog and WhatsApp refuse to offer slots. */\n enabled: boolean;\n /** Length of a demo, 15–180. */\n slotMinutes: number;\n /** Free minutes kept after each booking, 0–120. */\n bufferMinutes: number;\n /** IANA zone the working hours are in, e.g. `\"Asia/Kolkata\"`. */\n timezone: string;\n /** A day left out is closed. */\n workingHours: BookingWorkingHours;\n title: string | null;\n description: string | null;\n /** How far ahead a slot can be booked, 1–90 days. */\n horizonDays: number;\n /** Earliest a slot can start from now, 0–10080 minutes. */\n noticeMinutes: number;\n /** Code in the \"Book on WhatsApp\" link. Lower case, letters, digits and `-`; unique across projects. */\n whatsappCode: string | null;\n}\n\nexport interface BookingSettingsState {\n settings: BookingSettings;\n /** False until someone saves once — `settings` are the defaults. */\n saved: boolean;\n /** No calendar means no slots, whatever `enabled` says. Connect one in Glitchgrab → Calls → Booking. */\n calendarConnected: boolean;\n}\n\nexport type BookingSettingsResult = { ok: true; data: BookingSettingsState } | { ok: false; error: string };\n\nconst WEEKDAY = /^[1-7]$/;\n\nfunction readBookingSettingsState(value: unknown): BookingSettingsState | null {\n if (!isRecord(value) || !isRecord(value.settings)) return null;\n const s = value.settings;\n const hours: BookingWorkingHours = {};\n if (isRecord(s.workingHours)) {\n for (const [day, ranges] of Object.entries(s.workingHours)) {\n if (!WEEKDAY.test(day) || !Array.isArray(ranges)) continue;\n const valid = ranges.filter(\n (r): r is [string, string] => Array.isArray(r) && typeof r[0] === \"string\" && typeof r[1] === \"string\"\n );\n if (valid.length) hours[day] = valid.map(([open, close]) => [open, close]);\n }\n }\n return {\n settings: {\n enabled: s.enabled === true,\n slotMinutes: numberOrNull(s.slotMinutes) ?? 60,\n bufferMinutes: numberOrNull(s.bufferMinutes) ?? 0,\n timezone: typeof s.timezone === \"string\" ? s.timezone : \"Asia/Kolkata\",\n workingHours: hours,\n title: stringOrNull(s.title),\n description: stringOrNull(s.description),\n horizonDays: numberOrNull(s.horizonDays) ?? 15,\n noticeMinutes: numberOrNull(s.noticeMinutes) ?? 120,\n whatsappCode: stringOrNull(s.whatsappCode),\n },\n saved: value.saved === true,\n calendarConnected: value.calendarConnected === true,\n };\n}\n\n/**\n * The key's project booking settings. Never throws — `{ ok: false, error }` on\n * any failure, so the page can say why instead of showing defaults as if saved.\n *\n * ```ts\n * const result = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * if (result.ok && !result.data.calendarConnected) warn(\"Connect a calendar in Glitchgrab\");\n * ```\n */\nexport async function getBookingSettings(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n try {\n const key = callsKey(options);\n if (!key) return { ok: false, error: NO_KEY_ERROR };\n const { data, error } = await callSdkApi(\"/api/v1/sdk/booking-settings\", key, options, { cache: \"no-store\" });\n const state = error === null ? readBookingSettingsState(data) : null;\n return state ? { ok: true, data: state } : { ok: false, error: error ?? \"Unexpected response\" };\n } catch {\n return { ok: false, error: \"Could not read booking settings\" };\n }\n}\n\n/**\n * Change booking settings. Send only what changed — a field left out keeps its\n * value. Numbers are clamped to their range and invalid working hours are\n * ignored, so read `data.settings` back rather than trusting what you sent.\n *\n * `error` is worth showing: a WhatsApp code already taken by another project\n * comes back as one.\n */\nexport async function updateBookingSettings(\n changes: Partial<BookingSettings>,\n options: CallsOptions = {}\n): Promise<BookingSettingsResult> {\n try {\n const key = callsKey(options);\n if (!key) return { ok: false, error: NO_KEY_ERROR };\n const { data, error } = await callSdkApi(\"/api/v1/sdk/booking-settings\", key, options, {\n method: \"PUT\",\n body: changes ?? {},\n cache: \"no-store\",\n });\n const state = error === null ? readBookingSettingsState(data) : null;\n return state ? { ok: true, data: state } : { ok: false, error: error ?? \"Unexpected response\" };\n } catch {\n return { ok: false, error: \"Could not save booking settings\" };\n }\n}\n\n/**\n * `JSON.stringify` that is safe inside `<script type=\"application/ld+json\">`.\n *\n * Guide titles and summaries are written outside your codebase, and\n * `JSON.stringify` leaves `<` alone — a title containing `</script>` closes the\n * tag and whatever follows runs as HTML. The JSON unicode escape for `<` is the same\n * character to a JSON parser and inert to the HTML one. `>`, `&` and the two\n * JavaScript line separators are escaped for the same reason.\n *\n * ```tsx\n * <script\n * type=\"application/ld+json\"\n * dangerouslySetInnerHTML={{ __html: escapeJsonForScript({ \"@type\": \"HowTo\", name: guide.title }) }}\n * />\n * ```\n *\n * Never throws: a value JSON cannot represent (circular, `undefined`) becomes `null`.\n */\nexport function escapeJsonForScript(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return \"null\";\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n } catch {\n return \"null\";\n }\n}\n\nexport type { ReportResult, ReportSeverity, ReportType } from \"./types\";\n","import type { RuntimeInfo } from \"./types\";\n\n/**\n * Runtime health at the moment of the crash. `deviceInfo` says what the machine\n * is; this says what shape it was in — a leak that ran the heap to its ceiling,\n * a 2G connection that starved a fetch, a bug that only shows up after ten\n * minutes on the page or on the fifth error in a row.\n */\n\nconst pageLoadedAt = Date.now();\nlet errorCount = 0;\n\n/** Bumped by every auto-captured error so a report says which one in the run it was. */\nexport function incrementErrorCount(): number {\n errorCount += 1;\n return errorCount;\n}\n\n/** Test-only — reset the per-session counter. */\nexport function resetErrorCount(): void {\n errorCount = 0;\n}\n\ninterface MemoryCapableWindow {\n memory?: { usedJSHeapSize?: number; jsHeapSizeLimit?: number };\n}\n\ninterface NetworkInformation {\n effectiveType?: string;\n downlink?: number;\n rtt?: number;\n saveData?: boolean;\n}\n\nconst toMb = (bytes: number | undefined): number | undefined =>\n typeof bytes === \"number\" ? Math.round((bytes / 1048576) * 10) / 10 : undefined;\n\nexport function captureRuntimeInfo(): RuntimeInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n\n // Chromium-only, and absent entirely under cross-origin isolation.\n const memory = (performance as unknown as MemoryCapableWindow)?.memory;\n // Chromium + Android only.\n const connection = (navigator as unknown as { connection?: NetworkInformation })?.connection;\n\n return {\n timeOnPageMs: Date.now() - pageLoadedAt,\n errorCount,\n visibility: typeof document !== \"undefined\" ? document.visibilityState : \"unknown\",\n ...(memory\n ? {\n jsHeapUsedMb: toMb(memory.usedJSHeapSize),\n jsHeapLimitMb: toMb(memory.jsHeapSizeLimit),\n }\n : {}),\n ...(connection\n ? {\n connectionType: connection.effectiveType,\n downlinkMbps: connection.downlink,\n rttMs: connection.rtt,\n saveData: connection.saveData,\n }\n : {}),\n };\n } catch {\n return null;\n }\n}\n","import type {\n CapturedContext,\n DeviceInfo,\n FeedbackPayload,\n FeedbackResult,\n ReportPayload,\n ReportResult,\n} from \"./types\";\n// The React-free subpath: utils.ts is also bundled into `glitchgrab/server`.\nimport { assistOffline, toAssistTurnResult } from \"@glitchgrab/report-ui/assist-response\";\nimport type { AssistTurnResult } from \"@glitchgrab/report-ui\";\nimport { getBreadcrumbs } from \"./breadcrumbs\";\nimport { captureRuntimeInfo } from \"./runtime\";\nimport { getAppContext, getRelease } from \"./app-context\";\n\nconst SENSITIVE_PARAMS = [\n \"token\", \"key\", \"secret\", \"password\", \"passwd\", \"auth\", \"authorization\",\n \"session\", \"sessionid\", \"session_id\", \"api_key\", \"apikey\", \"access_token\",\n \"refresh_token\", \"client_secret\", \"code\", \"state\", \"nonce\", \"credential\", \"private\",\n];\n\nexport function sanitizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const params = new URLSearchParams(parsed.search);\n let modified = false;\n for (const key of Array.from(params.keys())) {\n if (SENSITIVE_PARAMS.some((s) => key.toLowerCase().includes(s))) {\n params.set(key, \"[REDACTED]\");\n modified = true;\n }\n }\n if (modified) parsed.search = params.toString();\n return parsed.toString();\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nexport function captureDeviceInfo(): DeviceInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n return {\n screenWidth: screen.width,\n screenHeight: screen.height,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n platform: navigator.platform ?? \"unknown\",\n language: navigator.language ?? \"unknown\",\n online: navigator.onLine,\n colorScheme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\",\n devicePixelRatio: window.devicePixelRatio ?? 1,\n };\n } catch {\n return null;\n }\n}\n\nexport function captureContext(visitedPages: string[]): CapturedContext {\n try {\n return {\n url: sanitizeUrl(typeof window !== \"undefined\" ? window.location.href : \"\"),\n userAgent: typeof navigator !== \"undefined\" ? navigator.userAgent : \"\",\n timestamp: new Date().toISOString(),\n visitedPages: visitedPages.map(sanitizeUrl),\n breadcrumbs: getBreadcrumbs(),\n deviceInfo: captureDeviceInfo(),\n runtime: captureRuntimeInfo(),\n appContext: getAppContext(),\n release: getRelease(),\n };\n } catch {\n return {\n url: \"\",\n userAgent: \"\",\n timestamp: new Date().toISOString(),\n visitedPages: [],\n breadcrumbs: [],\n deviceInfo: null,\n runtime: null,\n appContext: {},\n };\n }\n}\n\n/**\n * Metadata every report carries regardless of how it was raised — release,\n * runtime health, and the host app's own context. One builder so a user-filed\n * report and an auto-captured crash describe the same world; the payload sites\n * only add what is specific to them.\n *\n * App context is prefixed so a key named `timestamp` or `status` can't quietly\n * overwrite a field the dashboard relies on.\n */\nexport function contextMetadata(context: CapturedContext): Record<string, string> {\n try {\n const runtime = context.runtime;\n const appContext = Object.entries(context.appContext ?? {}).reduce<Record<string, string>>(\n (acc, [key, value]) => {\n acc[`ctx_${key}`] = value;\n return acc;\n },\n {}\n );\n\n return {\n ...(context.release ? { release: context.release } : {}),\n ...(runtime\n ? {\n timeOnPageMs: String(runtime.timeOnPageMs),\n errorCount: String(runtime.errorCount),\n visibility: runtime.visibility,\n ...(runtime.jsHeapUsedMb !== undefined\n ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) }\n : {}),\n ...(runtime.jsHeapLimitMb !== undefined\n ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) }\n : {}),\n ...(runtime.connectionType ? { connectionType: runtime.connectionType } : {}),\n ...(runtime.downlinkMbps !== undefined\n ? { downlinkMbps: String(runtime.downlinkMbps) }\n : {}),\n ...(runtime.rttMs !== undefined ? { rttMs: String(runtime.rttMs) } : {}),\n ...(runtime.saveData !== undefined ? { saveData: String(runtime.saveData) } : {}),\n }\n : {}),\n ...appContext,\n };\n } catch {\n return {};\n }\n}\n\nexport const DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 500;\n\n/**\n * Send a report and return the result.\n * Never throws — returns null on failure.\n */\nexport async function sendReport(\n payload: ReportPayload,\n baseUrl?: string\n): Promise<ReportResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/report`;\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n };\n\n // keepalive has a 64KB body limit in browsers — skip it for large payloads (e.g., screenshots)\n const useKeepalive = body.length < 60_000;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<ReportResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n // Fallback: sendBeacon (fire-and-forget, no result)\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n try {\n navigator.sendBeacon(url, new Blob([body], { type: \"application/json\" }));\n } catch {\n // Silently fail\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a star rating your end-user left about your app.\n * Never throws — returns null on failure.\n *\n * Unlike `sendReport`, this is always a deliberate user action with a dialog\n * open in front of them, so it retries on transient failure but skips the\n * sendBeacon fallback: the caller needs a real result to show a thank-you.\n */\nexport async function sendFeedback(\n payload: FeedbackPayload,\n baseUrl?: string\n): Promise<FeedbackResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/feedback`;\n const body = JSON.stringify(payload);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<FeedbackResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n // 4xx other than rate limiting is a caller mistake — retrying won't help.\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface EnhanceContext {\n url?: string;\n visitedPages?: string[];\n breadcrumbs?: Array<{ type: string; message: string }>;\n}\n\n/**\n * The signed-in user as the host's server vouched for them (`signSession`).\n *\n * Rides in the request BODY, never a header: every token route sets its own\n * CORS allow-list of headers, so a new header would fail preflight on all of\n * them. Omitted entirely when there is no signature — an unsigned id proves\n * nothing, and the server reads its absence as \"unsigned\".\n */\nexport interface SignedIdentity {\n userId?: string | null;\n signature?: string | null;\n}\n\n/** `{ userId, signature }` to spread into a JSON body, or nothing. */\nexport function signedFields(identity?: SignedIdentity | null): { userId?: string; signature?: string } {\n if (!identity?.signature || !identity.userId) return {};\n return { userId: identity.userId, signature: identity.signature };\n}\n\n/**\n * Polish raw description text via the Glitchgrab AI enhance endpoint.\n * Returns the polished text on success, or the original text on any failure.\n * Never throws.\n */\nexport async function enhanceText(\n text: string,\n token: string,\n baseUrl?: string,\n screenshot?: string | null,\n context?: EnhanceContext | null,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n const trimmed = text.trim();\n if (!trimmed) return text;\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n text: trimmed,\n ...(screenshot ? { screenshot } : {}),\n ...(context ? { context } : {}),\n ...signedFields(identity),\n }),\n });\n if (!response.ok) return text;\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { text?: string };\n };\n if (envelope?.success && typeof envelope.data?.text === \"string\") {\n return envelope.data.text;\n }\n return text;\n } catch {\n return text;\n }\n}\n\n/**\n * \"Is this already filed?\" for the report dialog's plain form. Asked when Send\n * is pressed; a match lets the reporter add to the open issue instead of\n * opening a second one.\n *\n * `[]` when the project has it off, `null` on any failure — the dialog sends\n * straight away on either. Never throws.\n */\nexport async function findSimilarIssues(\n description: string,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<{ number: number; title: string; url: string; status?: string }[] | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/reports/similar`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ text: description, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; matches?: { number: number; title: string; url: string; status?: string }[] };\n } | null;\n if (!response.ok || !envelope?.success) return null;\n if (!envelope.data?.enabled) return [];\n return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];\n } catch {\n return null;\n }\n}\n\n/**\n * One turn of the AI report assistant (#330).\n *\n * Same never-throw contract as everything else in the SDK: a failure comes back\n * as `degraded` and the dialog quietly falls back to the plain form. An\n * end-user of the host app must never see a Glitchgrab error, and must never be\n * blocked from filing a bug because a model was busy.\n */\nexport async function assistTurn(\n params: {\n messages: { role: \"user\" | \"assistant\"; content: string }[];\n conversationId: string | null;\n screenshot?: string | null;\n context?: Record<string, unknown> | null;\n /**\n * Who the host app says is logged in, from the provider's `session` prop.\n * Stored against the conversation so a chat and the report it produced\n * agree on who filed — one SDK token covers every end user of the host app,\n * so without this every stranger's chat looks like the same person.\n */\n reporter?: {\n key?: string | null;\n name?: string | null;\n email?: string | null;\n /** `signSession()` output for `key`; absent when the host did not sign. */\n signature?: string | null;\n };\n },\n token: string,\n baseUrl?: string\n): Promise<AssistTurnResult> {\n const offline = assistOffline();\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/report-chat`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(params),\n });\n // One mapping for every host (packages/report-ui/src/assist-response.ts):\n // the server's degrade reason passes through, and nothing the sheet renders\n // is dropped.\n return toAssistTurnResult(response.ok, await response.json().catch(() => null));\n } catch {\n return offline;\n }\n}\n\n/**\n * Send an audio Blob to the Glitchgrab STT proxy (which calls Sarvam).\n * Returns the transcript string, or \"\" on any failure. Never throws.\n */\nexport async function transcribeAudio(\n blob: Blob,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n if (blob.size === 0) return \"\";\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/stt`;\n const form = new FormData();\n form.append(\"file\", blob, \"audio.webm\");\n const signed = signedFields(identity);\n if (signed.userId && signed.signature) {\n form.append(\"userId\", signed.userId);\n form.append(\"signature\", signed.signature);\n }\n const response = await fetch(url, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: form,\n });\n if (!response.ok) return \"\";\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { transcript?: string };\n };\n if (envelope?.success && typeof envelope.data?.transcript === \"string\") {\n return envelope.data.transcript;\n }\n return \"\";\n } catch {\n return \"\";\n }\n}\n"],"mappings":"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","names":["_a"]}
package/dist/server.mjs CHANGED
@@ -229,7 +229,11 @@ async function callSdkApi(path, token, options, init) {
229
229
  try {
230
230
  const request = {
231
231
  method: (_e = init.method) != null ? _e : "GET",
232
- headers: { Authorization: `Bearer ${token}` },
232
+ headers: {
233
+ Authorization: `Bearer ${token}`,
234
+ ...init.body === void 0 ? {} : { "Content-Type": "application/json" }
235
+ },
236
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
233
237
  ...init.cache === "no-store" ? { cache: "no-store" } : { next: { revalidate: init.cache.revalidate } },
234
238
  ...controller ? { signal: controller.signal } : {}
235
239
  };
@@ -298,6 +302,7 @@ async function getGuide(slug, options = {}) {
298
302
  }
299
303
  }
300
304
  var CALLS_KEY_PREFIX = "ggc_";
305
+ var NO_KEY_ERROR = "No server key \u2014 set GLITCHGRAB_CALLS_KEY (ggc_\u2026)";
301
306
  function callsKey(options) {
302
307
  var _a;
303
308
  const key = (_a = options.key) != null ? _a : readEnv("GLITCHGRAB_CALLS_KEY");
@@ -391,7 +396,7 @@ async function deleteCall(id, options = {}) {
391
396
  const wanted = typeof id === "string" ? id.trim() : "";
392
397
  if (!wanted) return { deleted: false, error: "No call id" };
393
398
  const key = callsKey(options);
394
- if (!key) return { deleted: false, error: "No server key \u2014 set GLITCHGRAB_CALLS_KEY (ggc_\u2026)" };
399
+ if (!key) return { deleted: false, error: NO_KEY_ERROR };
395
400
  const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {
396
401
  method: "DELETE",
397
402
  cache: "no-store"
@@ -401,6 +406,64 @@ async function deleteCall(id, options = {}) {
401
406
  return { deleted: false, error: "Could not delete the call" };
402
407
  }
403
408
  }
409
+ var WEEKDAY = /^[1-7]$/;
410
+ function readBookingSettingsState(value) {
411
+ var _a, _b, _c, _d;
412
+ if (!isRecord(value) || !isRecord(value.settings)) return null;
413
+ const s = value.settings;
414
+ const hours = {};
415
+ if (isRecord(s.workingHours)) {
416
+ for (const [day, ranges] of Object.entries(s.workingHours)) {
417
+ if (!WEEKDAY.test(day) || !Array.isArray(ranges)) continue;
418
+ const valid = ranges.filter(
419
+ (r) => Array.isArray(r) && typeof r[0] === "string" && typeof r[1] === "string"
420
+ );
421
+ if (valid.length) hours[day] = valid.map(([open, close]) => [open, close]);
422
+ }
423
+ }
424
+ return {
425
+ settings: {
426
+ enabled: s.enabled === true,
427
+ slotMinutes: (_a = numberOrNull(s.slotMinutes)) != null ? _a : 60,
428
+ bufferMinutes: (_b = numberOrNull(s.bufferMinutes)) != null ? _b : 0,
429
+ timezone: typeof s.timezone === "string" ? s.timezone : "Asia/Kolkata",
430
+ workingHours: hours,
431
+ title: stringOrNull(s.title),
432
+ description: stringOrNull(s.description),
433
+ horizonDays: (_c = numberOrNull(s.horizonDays)) != null ? _c : 15,
434
+ noticeMinutes: (_d = numberOrNull(s.noticeMinutes)) != null ? _d : 120,
435
+ whatsappCode: stringOrNull(s.whatsappCode)
436
+ },
437
+ saved: value.saved === true,
438
+ calendarConnected: value.calendarConnected === true
439
+ };
440
+ }
441
+ async function getBookingSettings(options = {}) {
442
+ try {
443
+ const key = callsKey(options);
444
+ if (!key) return { ok: false, error: NO_KEY_ERROR };
445
+ const { data, error } = await callSdkApi("/api/v1/sdk/booking-settings", key, options, { cache: "no-store" });
446
+ const state = error === null ? readBookingSettingsState(data) : null;
447
+ return state ? { ok: true, data: state } : { ok: false, error: error != null ? error : "Unexpected response" };
448
+ } catch (e) {
449
+ return { ok: false, error: "Could not read booking settings" };
450
+ }
451
+ }
452
+ async function updateBookingSettings(changes, options = {}) {
453
+ try {
454
+ const key = callsKey(options);
455
+ if (!key) return { ok: false, error: NO_KEY_ERROR };
456
+ const { data, error } = await callSdkApi("/api/v1/sdk/booking-settings", key, options, {
457
+ method: "PUT",
458
+ body: changes != null ? changes : {},
459
+ cache: "no-store"
460
+ });
461
+ const state = error === null ? readBookingSettingsState(data) : null;
462
+ return state ? { ok: true, data: state } : { ok: false, error: error != null ? error : "Unexpected response" };
463
+ } catch (e) {
464
+ return { ok: false, error: "Could not save booking settings" };
465
+ }
466
+ }
404
467
  function escapeJsonForScript(value) {
405
468
  try {
406
469
  const json = JSON.stringify(value);
@@ -415,12 +478,14 @@ export {
415
478
  configureServerReporter,
416
479
  deleteCall,
417
480
  escapeJsonForScript,
481
+ getBookingSettings,
418
482
  getCall,
419
483
  getGuide,
420
484
  listCalls,
421
485
  listGuides,
422
486
  reportServerError,
423
487
  resetServerReporter,
424
- signSession
488
+ signSession,
489
+ updateBookingSettings
425
490
  };
426
491
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/runtime.ts","../src/utils.ts"],"sourcesContent":["/**\n * Server-side reporting — `import { reportServerError } from \"glitchgrab/server\"`.\n *\n * The rest of this SDK is `\"use client\"`: it hooks `window.onerror`, walks the\n * DOM for a screenshot, and needs a mounted provider for its token. None of\n * that exists in a cron job at 3am, an API route handler, or a queue worker —\n * which is precisely where the failures nobody is watching happen. A WhatsApp\n * send Meta rejects, an SMTP timeout, a nightly digest that throws: no tab is\n * open, so the browser SDK never sees them and no issue is ever filed.\n *\n * This entry has no `\"use client\"`, touches no DOM API, and imports no React.\n * It POSTs to the same `/api/v1/sdk/report` endpoint the browser uses, with\n * `source: \"SDK_AUTO\"` so the server-side dedup applies (24h per signature,\n * 7 days once an issue exists) — otherwise a cron failing hourly would open\n * twenty-four identical issues a day.\n *\n * Like the rest of the SDK it never throws: a reporting failure must not turn\n * a handled error into an unhandled one.\n */\nimport { createHmac } from \"node:crypto\";\nimport type { ReportPayload, ReportResult, ReportSeverity, ReportType } from \"./types\";\nimport { DEFAULT_BASE_URL, sendReport } from \"./utils\";\n\n/** Who to attribute the report to. A server has no signed-in user, so this is the job or service. */\nexport interface ServerReporterIdentity {\n /** Stable id — the cron name, the service name. Defaults to `\"server\"`. */\n id?: string;\n /** Display name shown on the report. Defaults to `\"Server\"`. */\n name?: string;\n email?: string;\n phone?: string;\n}\n\n/** Defaults applied to every server report. Set once at boot with {@link configureServerReporter}. */\nexport interface ServerReporterConfig {\n /** Repo token (`gg_…`). Falls back to `process.env.GLITCHGRAB_TOKEN`. */\n token?: string;\n /** Override the API host. Falls back to `process.env.GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Where this ran — `\"cron/attendance-prompts\"`, `\"worker/email-queue\"`.\n *\n * It is the grouping key, not a link: it is sent as the report's `pageUrl`\n * so two different jobs throwing the same `\"Timeout\"` stay two issues\n * instead of collapsing into one.\n */\n context?: string;\n /** Merged into every report's metadata. Good place for region, deployment id, service name. */\n metadata?: Record<string, string>;\n reporter?: ServerReporterIdentity;\n /**\n * Report from `NODE_ENV=development` too. Off by default, matching the\n * browser SDK — a laptop crashing its way through a refactor should not file\n * issues. Unlike the browser there is no `Origin` header for the API to\n * recognise, so nothing else stops a dev machine from filing.\n */\n enableInDevelopment?: boolean;\n}\n\n/** Per-call options. Anything set here wins over {@link configureServerReporter}. */\nexport interface ServerReportOptions extends ServerReporterConfig {\n /** Issue type. Defaults to `BUG`. */\n type?: ReportType;\n /** Rendered as a `severity:<value>` label on the GitHub issue. */\n severity?: ReportSeverity;\n /** Extra prose for the issue body — what the job was doing, which record it was on. */\n description?: string;\n /**\n * A real request URL, when there is one (an API route handler). Overrides\n * the synthetic URL built from `context`.\n */\n pageUrl?: string;\n}\n\nconst DEV_BLOCKED_MESSAGE = \"development\";\n\n/**\n * Node's `process`, as much of it as this file uses.\n *\n * `src/global.d.ts` declares a browser-shaped `process` with nothing but\n * `env`, and that is deliberate — the client bundle must not be able to reach\n * for Node APIs. So this entry reads the real one off `globalThis` behind its\n * own narrow type instead of pulling `@types/node` into the package.\n */\ninterface NodeProcessLike {\n env?: Record<string, string | undefined>;\n version?: string;\n on?(event: string, listener: (arg: never) => void): unknown;\n off?(event: string, listener: (arg: never) => void): unknown;\n listenerCount?(event: string): number;\n}\n\nfunction nodeProcess(): NodeProcessLike | undefined {\n try {\n const candidate = (globalThis as { process?: NodeProcessLike }).process;\n return candidate && typeof candidate === \"object\" ? candidate : undefined;\n } catch {\n return undefined;\n }\n}\n\nlet defaults: ServerReporterConfig = {};\n\n/**\n * Set the token and the defaults once, at module load, instead of passing them\n * to every call site. A later call's own options still win.\n */\nexport function configureServerReporter(config: ServerReporterConfig): void {\n defaults = { ...defaults, ...config };\n}\n\n/** Test-only — drop everything {@link configureServerReporter} set. */\nexport function resetServerReporter(): void {\n defaults = {};\n}\n\nfunction readEnv(name: string): string | undefined {\n try {\n return nodeProcess()?.env?.[name];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Message and stack out of whatever was thrown.\n *\n * A thrown string, a rejected `{ code, message }`, an `undefined` from a\n * `Promise.reject()` — all of it reaches here, and all of it must produce a\n * non-empty message: the API's dedup signature is `null` without one, and a\n * report with no signature is a fresh issue every single time.\n */\nfunction describeError(error: unknown): { message: string; stack?: string } {\n try {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || \"Unknown server error\",\n stack: error.stack,\n };\n }\n if (typeof error === \"string\") {\n return { message: error || \"Unknown server error\" };\n }\n if (error && typeof error === \"object\") {\n const maybe = error as { message?: unknown; stack?: unknown };\n const message = typeof maybe.message === \"string\" ? maybe.message : JSON.stringify(error);\n return {\n message: message || \"Unknown server error\",\n stack: typeof maybe.stack === \"string\" ? maybe.stack : undefined,\n };\n }\n return { message: String(error ?? \"\") || \"Unknown server error\" };\n } catch {\n return { message: \"Unknown server error\" };\n }\n}\n\n/**\n * `pageUrl` doubles as the dedup grouping key, so a job needs one even though\n * no page was involved. `server://` keeps it obviously synthetic — and keeps it\n * clear of the API's localhost check, which only matches http(s) and would\n * otherwise swallow the report with a 200 and create nothing.\n */\nfunction contextUrl(context: string | undefined): string | undefined {\n if (!context) return undefined;\n return `server://${context.replace(/^\\/+/, \"\")}`;\n}\n\nfunction runtimeMetadata(): Record<string, string> {\n try {\n const proc = nodeProcess();\n if (!proc) return { runtime: \"server\" };\n const region = proc.env?.VERCEL_REGION;\n const deploymentEnv = proc.env?.VERCEL_ENV;\n return {\n runtime: \"node\",\n ...(proc.version ? { nodeVersion: proc.version } : {}),\n ...(region ? { region } : {}),\n ...(deploymentEnv ? { deploymentEnv } : {}),\n };\n } catch {\n return { runtime: \"server\" };\n }\n}\n\n/**\n * File a server-side error as a GitHub issue.\n *\n * Await it. On a serverless platform the function may be frozen the moment your\n * handler returns, and a floating promise dies with it — the report never\n * leaves the machine.\n *\n * ```ts\n * try {\n * await sendDigest();\n * } catch (error) {\n * await reportServerError(error, { context: \"cron/daily-digest\" });\n * throw error;\n * }\n * ```\n *\n * Returns `null` when nothing was filed — no token, development mode, or the\n * API refused it. Never throws.\n */\nexport async function reportServerError(\n error: unknown,\n options: ServerReportOptions = {}\n): Promise<ReportResult | null> {\n try {\n const token = options.token ?? defaults.token ?? readEnv(\"GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const enableInDevelopment = options.enableInDevelopment ?? defaults.enableInDevelopment ?? false;\n if (!enableInDevelopment && readEnv(\"NODE_ENV\") === DEV_BLOCKED_MESSAGE) return null;\n\n const { message, stack } = describeError(error);\n const context = options.context ?? defaults.context;\n const reporter = { ...defaults.reporter, ...options.reporter };\n\n const payload: ReportPayload = {\n token,\n source: \"SDK_AUTO\",\n type: options.type ?? \"BUG\",\n errorMessage: message,\n ...(stack ? { errorStack: stack } : {}),\n ...(options.description ? { description: options.description } : {}),\n ...(options.pageUrl ?? contextUrl(context)\n ? { pageUrl: options.pageUrl ?? contextUrl(context) }\n : {}),\n metadata: {\n ...runtimeMetadata(),\n ...defaults.metadata,\n ...options.metadata,\n ...(context ? { serverContext: context } : {}),\n ...(options.severity ? { severity: options.severity } : {}),\n sessionUserId: reporter.id ?? context ?? \"server\",\n sessionUserName: reporter.name ?? \"Server\",\n ...(reporter.email ? { sessionUserEmail: reporter.email } : {}),\n ...(reporter.phone ? { sessionUserPhone: reporter.phone } : {}),\n },\n };\n\n return await sendReport(payload, options.baseUrl ?? defaults.baseUrl ?? readEnv(\"GLITCHGRAB_BASE_URL\"));\n } catch {\n return null;\n }\n}\n\n/** Options for {@link captureServerErrors}. */\nexport interface CaptureServerErrorsOptions extends ServerReportOptions {\n /**\n * Also report rejected promises nobody handled. Default `true`.\n *\n * Node crashes the process on an unhandled rejection — but only while no\n * `unhandledRejection` listener exists. Attaching one silently converts a\n * crash into a warning, so this re-throws afterwards to put the default\n * behaviour back, unless the host app has its own listener (then it is theirs\n * to decide).\n */\n captureUnhandledRejections?: boolean;\n}\n\n/**\n * Report every uncaught exception and unhandled rejection in this process.\n *\n * Call it once at boot — `instrumentation.ts` in a Next.js app. Returns a\n * function that removes the listeners again.\n *\n * Uncaught exceptions use `uncaughtExceptionMonitor`, which observes without\n * taking over: the process still crashes exactly as it would have. A reporter\n * that keeps a broken process alive is worse than no reporter.\n */\nexport function captureServerErrors(options: CaptureServerErrorsOptions = {}): () => void {\n try {\n const proc = nodeProcess();\n if (!proc || typeof proc.on !== \"function\") return () => {};\n\n const onException = (error: unknown) => {\n void reportServerError(error, { ...options, context: options.context ?? \"uncaughtException\" });\n };\n\n const onRejection = (reason: unknown) => {\n void reportServerError(reason, {\n ...options,\n context: options.context ?? \"unhandledRejection\",\n });\n\n // Ours is the only listener, so suppressing the crash was never the\n // host app's choice. Hand the default behaviour back.\n if (proc.listenerCount?.(\"unhandledRejection\") === 1) {\n setTimeout(() => {\n throw reason;\n }, 0);\n }\n };\n\n proc.on(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n\n const captureRejections = options.captureUnhandledRejections ?? true;\n if (captureRejections) proc.on(\"unhandledRejection\", onRejection as (arg: never) => void);\n\n return () => {\n try {\n proc.off?.(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n if (captureRejections) {\n proc.off?.(\"unhandledRejection\", onRejection as (arg: never) => void);\n }\n } catch {\n // Nothing to undo.\n }\n };\n } catch {\n return () => {};\n }\n}\n\n/** Options for {@link signSession}. */\nexport interface SignSessionOptions {\n /** Your user's id — the same value you pass as `session.userId`. */\n userId: string;\n /** The repo's signing secret (`ggs_…`). Falls back to `process.env.GLITCHGRAB_SIGNING_SECRET`. */\n secret?: string;\n /** How long the signature stays valid, in seconds. Default one day. */\n ttlSeconds?: number;\n /** Clock override in epoch ms. For tests. */\n now?: number;\n}\n\n/**\n * Vouch for the logged-in user so the SDK's AI features work when the project\n * owner turned on signed users.\n *\n * The `gg_` token ships in your JavaScript, so on its own it proves nothing\n * about who is calling. Your server holds the signing secret; call this where\n * you build the `session` prop and pass the result as `session.signature`.\n * Render it per request so it refreshes before `ttlSeconds` runs out.\n *\n * Format: `v1.<exp>.<base64url HMAC-SHA256(secret, \"v1.<exp>.<userId>\")>`.\n *\n * Unlike the rest of this SDK it throws: it runs in your own server code, where\n * a missing secret is a deploy mistake you want to see — not a user who quietly\n * loses the AI features.\n */\nexport function signSession(options: SignSessionOptions): string {\n const userId = options?.userId;\n if (typeof userId !== \"string\" || userId.length === 0) {\n throw new Error(\"glitchgrab signSession: userId is required\");\n }\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n const secret = options.secret || env?.GLITCHGRAB_SIGNING_SECRET;\n if (!secret) {\n throw new Error(\n \"glitchgrab signSession: no signing secret — pass `secret` or set GLITCHGRAB_SIGNING_SECRET\"\n );\n }\n const ttl = options.ttlSeconds ?? 86_400;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new Error(\"glitchgrab signSession: ttlSeconds must be a positive number\");\n }\n const exp = Math.floor((options.now ?? Date.now()) / 1000) + Math.floor(ttl);\n const sig = createHmac(\"sha256\", secret).update(`v1.${exp}.${userId}`).digest(\"base64url\");\n return `v1.${exp}.${sig}`;\n}\n\n// ── Guides ───────────────────────────────────────────────────────────────────\n//\n// A team writes its how-tos in Glitchgrab and renders them on its own site from\n// `GET /api/v1/sdk/guides` (#370). Every site needs the same client — token,\n// caching, never failing the page, shape checks — so it lives here instead of\n// being rewritten, slightly differently, in each product (#409).\n\n/** One guide as the list returns it — no body. Dates are ISO strings: this came over JSON. */\nexport interface GuideSummary {\n slug: string;\n title: string;\n /** One line saying what the guide answers. */\n summary: string;\n category: string | null;\n /** The order set in Glitchgrab. The list already arrives sorted by it. */\n orderIndex: number;\n /** Always `\"PUBLISHED\"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */\n status: \"DRAFT\" | \"PUBLISHED\";\n /** When someone last re-checked the steps against the product. */\n lastVerifiedAt: string | null;\n updatedAt: string;\n /**\n * Where this guide lives on your site: its own link, else\n * `<guides base URL>/<slug>`, else null when the repo sets neither.\n */\n link: string | null;\n}\n\n/** One guide with its body. */\nexport interface Guide extends GuideSummary {\n /** The markdown as written. */\n content: string;\n /**\n * `content` rendered by Glitchgrab: raw HTML in the markdown arrives as text,\n * only allowlisted attributes survive, and links keep only http(s), mailto\n * and relative targets. Headings carry `id=\"guide-<heading text>\"`, for a\n * table of contents or a link to a single step.\n *\n * Safe to render as-is. On a domain that holds your users' sessions, a second\n * sanitize pass of your own is cheap defence in depth — this SDK ships no\n * sanitizer so it stays dependency-free.\n */\n html: string;\n}\n\n/** Options for {@link listGuides} and {@link getGuide}. */\nexport interface GuideReadOptions {\n /**\n * Repo token (`gg_…`). Falls back to {@link configureServerReporter}'s token,\n * then `GLITCHGRAB_TOKEN`, then `NEXT_PUBLIC_GLITCHGRAB_TOKEN` — the token is\n * public, so the one your provider already uses is fine.\n *\n * Never pass the `ggc_` server key for a public page: it reads drafts too, and client calls.\n */\n token?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Seconds Next.js may cache the response. Default 300; `0` reads fresh on\n * every request. The route is `force-dynamic`, so without this nothing is\n * cached at all. Ignored outside Next.js.\n */\n revalidate?: number;\n /** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */\n timeoutMs?: number;\n}\n\nconst GUIDES_REVALIDATE_SECONDS = 300;\nconst SDK_API_TIMEOUT_MS = 5000;\n\n/** `RequestInit` plus Next.js's cache option, without pulling Next's types into this entry. */\ntype NextFetchInit = RequestInit & { next?: { revalidate?: number } };\n\ninterface SdkApiResult {\n /** `data` from a successful response, else null. */\n data: unknown;\n /** Null on success; the route's own `error` when it sent one, so a caller can show why. */\n error: string | null;\n}\n\n/**\n * One request to a Glitchgrab SDK route. Never throws: a refusal, a timeout, a\n * network failure and unreadable JSON all come back as `error`.\n */\nasync function callSdkApi(\n path: string,\n token: string,\n options: { baseUrl?: string; timeoutMs?: number },\n init: { method?: \"GET\" | \"DELETE\"; cache: { revalidate: number } | \"no-store\" }\n): Promise<SdkApiResult> {\n const baseUrl = (\n options.baseUrl ??\n defaults.baseUrl ??\n readEnv(\"GLITCHGRAB_BASE_URL\") ??\n DEFAULT_BASE_URL\n ).replace(/\\/+$/, \"\");\n\n const controller = typeof AbortController === \"function\" ? new AbortController() : undefined;\n const timer = controller\n ? setTimeout(() => controller.abort(), options.timeoutMs ?? SDK_API_TIMEOUT_MS)\n : undefined;\n\n try {\n const request: NextFetchInit = {\n method: init.method ?? \"GET\",\n headers: { Authorization: `Bearer ${token}` },\n ...(init.cache === \"no-store\" ? { cache: \"no-store\" as const } : { next: { revalidate: init.cache.revalidate } }),\n ...(controller ? { signal: controller.signal } : {}),\n };\n const response = await fetch(`${baseUrl}${path}`, request);\n const body = (await response.json().catch(() => null)) as\n | { success?: unknown; data?: unknown; error?: unknown }\n | null;\n if (!response.ok || body?.success !== true) {\n return { data: null, error: typeof body?.error === \"string\" ? body.error : `HTTP ${response.status}` };\n }\n return { data: body.data ?? null, error: null };\n } catch {\n return { data: null, error: \"Could not reach Glitchgrab\" };\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/** `data` from the guides route, or null on any failure — no token, non-2xx, timeout, bad JSON. */\nasync function readGuidesApi(query: string, options: GuideReadOptions): Promise<unknown> {\n const token =\n options.token ??\n defaults.token ??\n readEnv(\"GLITCHGRAB_TOKEN\") ??\n readEnv(\"NEXT_PUBLIC_GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const revalidate =\n typeof options.revalidate === \"number\" && Number.isFinite(options.revalidate) && options.revalidate >= 0\n ? options.revalidate\n : GUIDES_REVALIDATE_SECONDS;\n\n const result = await callSdkApi(`/api/v1/sdk/guides${query}`, token, options, { cache: { revalidate } });\n return result.data;\n}\n\n/**\n * A guide summary out of whatever the API sent, or null when it is not one.\n * Only slug, title and summary are required — a page cannot render without\n * them. The rest default, so the returned type is honest either way.\n */\nfunction readGuideSummary(value: unknown): GuideSummary | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const guide = value as Record<string, unknown>;\n if (\n typeof guide.slug !== \"string\" ||\n typeof guide.title !== \"string\" ||\n typeof guide.summary !== \"string\"\n ) {\n return null;\n }\n return {\n slug: guide.slug,\n title: guide.title,\n summary: guide.summary,\n category: typeof guide.category === \"string\" ? guide.category : null,\n orderIndex: typeof guide.orderIndex === \"number\" ? guide.orderIndex : 0,\n status: guide.status === \"DRAFT\" ? \"DRAFT\" : \"PUBLISHED\",\n lastVerifiedAt: typeof guide.lastVerifiedAt === \"string\" ? guide.lastVerifiedAt : null,\n updatedAt: typeof guide.updatedAt === \"string\" ? guide.updatedAt : \"\",\n link: typeof guide.link === \"string\" ? guide.link : null,\n };\n}\n\n/**\n * Every published guide for the token's repo, in the order set in Glitchgrab.\n * No bodies — this is the index page.\n *\n * ```ts\n * // app/guides/page.tsx\n * import { listGuides } from \"glitchgrab/server\";\n *\n * export default async function GuidesPage() {\n * const guides = await listGuides();\n * return guides.map((g) => <a key={g.slug} href={`/guides/${g.slug}`}>{g.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws: a Glitchgrab outage leaves the\n * page empty instead of turning it into a 500.\n */\nexport async function listGuides(options: GuideReadOptions = {}): Promise<GuideSummary[]> {\n try {\n const data = await readGuidesApi(\"\", options);\n if (!Array.isArray(data)) return [];\n return data.map(readGuideSummary).filter((guide): guide is GuideSummary => guide !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One published guide with its markdown and rendered HTML.\n *\n * Returns `null` when there is no such guide and on any failure — call\n * `notFound()` on it. Never throws.\n */\nexport async function getGuide(slug: string, options: GuideReadOptions = {}): Promise<Guide | null> {\n try {\n const wanted = typeof slug === \"string\" ? slug.trim() : \"\";\n if (!wanted) return null;\n\n const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}`, options);\n const summary = readGuideSummary(data);\n const body = data as { content?: unknown; html?: unknown } | null;\n if (!summary || typeof body?.html !== \"string\") return null;\n\n return {\n ...summary,\n content: typeof body.content === \"string\" ? body.content : \"\",\n html: body.html,\n };\n } catch {\n return null;\n }\n}\n\n// ── Calls ────────────────────────────────────────────────────────────────────\n//\n// Recorded client calls a project shared with its client (#416), for the team's\n// own admin panel. Unlike guides these are private: they need the project's\n// server key (`ggc_…`, API Tokens → Server keys), which lives on the server and never in a bundle, and\n// every response holds links that expire — so nothing here is cached.\n\n/** Someone on the call, as the recorder saw them. */\nexport interface CallParticipant {\n name: string | null;\n email: string | null;\n}\n\n/** One shared call as the list returns it — no transcript, no links. */\nexport interface CallListItem {\n id: string;\n title: string | null;\n startsAt: string | null;\n endsAt: string | null;\n durationSec: number | null;\n /** `\"bot\"` (joined the Meet) or `\"extension\"` (recorded from someone's Chrome). */\n recorder: string | null;\n /** Match a call to your customer by these emails. */\n participants: CallParticipant[];\n hasRecording: boolean;\n /** `IDLE`, `RUNNING`, `DONE` or `FAILED`. */\n transcriptStatus: string;\n /** One line on what the call was about, once Glitchgrab has read it. */\n overview: string | null;\n createdAt: string;\n}\n\nexport interface CallAudio {\n /**\n * `call` — a bot recording's single mixed track. An extension recording has\n * two instead: `client` (everyone else on the call) and `recorder` (the\n * person who recorded it).\n */\n track: \"call\" | \"client\" | \"recorder\";\n /** Plays for {@link Call.linksExpireInSeconds}. */\n url: string;\n}\n\nexport interface CallMoment {\n /** Milliseconds into the call — seek the audio here. */\n tMs: number | null;\n speaker: string | null;\n text: string;\n}\n\n/** One shared call in full. */\nexport interface Call extends CallListItem {\n meetUrl: string | null;\n audio: CallAudio[];\n /** Audio and screenshot links stop working after this many seconds. Read the call per render; never store them. */\n linksExpireInSeconds: number;\n transcript: string | null;\n summary: { overview: string; moments: CallMoment[] } | null;\n /** Screenshots grabbed during the call, a page at a time. `tMs` is on the transcript's clock. */\n frames: { total: number; offset: number; limit: number; items: { tMs: number; url: string }[] };\n}\n\n/** Options for {@link listCalls}, {@link getCall} and {@link deleteCall}. */\nexport interface CallsOptions {\n /**\n * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.\n * Server only — never a `NEXT_PUBLIC_` variable: it plays and deletes calls.\n */\n key?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /** Give up after this many ms. Default 5000. */\n timeoutMs?: number;\n}\n\nexport interface ListCallsOptions extends CallsOptions {\n /** Default 20, max 100. */\n limit?: number;\n /** ISO date: only calls recorded before it. Pass the last call's `createdAt` for the next page. */\n before?: string;\n}\n\nexport interface GetCallOptions extends CallsOptions {\n /** First screenshot to return. Default 0. */\n framesOffset?: number;\n /** Screenshots per page. Default 50, max 200, `0` for none. */\n framesLimit?: number;\n}\n\nexport type DeleteCallResult = { deleted: true } | { deleted: false; error: string };\n\nconst CALLS_KEY_PREFIX = \"ggc_\";\n\n/** Only a `ggc_` key. A `gg_` token here would only 401 — and means the public token sits where a secret belongs. */\nfunction callsKey(options: CallsOptions): string | null {\n const key = options.key ?? readEnv(\"GLITCHGRAB_CALLS_KEY\");\n return typeof key === \"string\" && key.startsWith(CALLS_KEY_PREFIX) ? key : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction numberOrNull(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction readCallListItem(value: unknown): CallListItem | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n title: stringOrNull(value.title),\n startsAt: stringOrNull(value.startsAt),\n endsAt: stringOrNull(value.endsAt),\n durationSec: numberOrNull(value.durationSec),\n recorder: stringOrNull(value.recorder),\n participants: Array.isArray(value.participants)\n ? value.participants.filter(isRecord).map((p) => ({ name: stringOrNull(p.name), email: stringOrNull(p.email) }))\n : [],\n hasRecording: value.hasRecording === true,\n transcriptStatus: typeof value.transcriptStatus === \"string\" ? value.transcriptStatus : \"IDLE\",\n overview: stringOrNull(value.overview),\n createdAt: typeof value.createdAt === \"string\" ? value.createdAt : \"\",\n };\n}\n\nconst AUDIO_TRACKS: ReadonlySet<string> = new Set([\"call\", \"client\", \"recorder\"]);\n\nfunction readCall(value: unknown): Call | null {\n const base = readCallListItem(value);\n if (!base || !isRecord(value)) return null;\n\n const rawSummary = value.summary;\n const summary =\n isRecord(rawSummary) && typeof rawSummary.overview === \"string\"\n ? {\n overview: rawSummary.overview,\n moments: Array.isArray(rawSummary.moments)\n ? rawSummary.moments\n .filter(isRecord)\n .filter((m) => typeof m.text === \"string\")\n .map((m) => ({ tMs: numberOrNull(m.tMs), speaker: stringOrNull(m.speaker), text: m.text as string }))\n : [],\n }\n : null;\n\n const frames = isRecord(value.frames) ? value.frames : {};\n\n return {\n ...base,\n meetUrl: stringOrNull(value.meetUrl),\n audio: Array.isArray(value.audio)\n ? value.audio\n .filter(isRecord)\n .filter((a) => typeof a.url === \"string\" && typeof a.track === \"string\" && AUDIO_TRACKS.has(a.track))\n .map((a) => ({ track: a.track as CallAudio[\"track\"], url: a.url as string }))\n : [],\n linksExpireInSeconds: numberOrNull(value.linksExpireInSeconds) ?? 0,\n transcript: stringOrNull(value.transcript),\n summary,\n frames: {\n total: numberOrNull(frames.total) ?? 0,\n offset: numberOrNull(frames.offset) ?? 0,\n limit: numberOrNull(frames.limit) ?? 0,\n items: Array.isArray(frames.items)\n ? frames.items\n .filter(isRecord)\n .filter((f) => typeof f.url === \"string\" && typeof f.tMs === \"number\")\n .map((f) => ({ tMs: f.tMs as number, url: f.url as string }))\n : [],\n },\n };\n}\n\nfunction setNumber(query: URLSearchParams, name: string, value: unknown): void {\n if (typeof value === \"number\" && Number.isFinite(value)) query.set(name, String(Math.floor(value)));\n}\n\n/**\n * Calls shared with the client on the key's project, newest first. No links —\n * open one with {@link getCall}.\n *\n * ```ts\n * // app/admin/calls/page.tsx — server component\n * import { listCalls } from \"glitchgrab/server\";\n *\n * export default async function CallsPage() {\n * const calls = await listCalls(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * return calls.map((c) => <a key={c.id} href={`/admin/calls/${c.id}`}>{c.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws.\n */\nexport async function listCalls(options: ListCallsOptions = {}): Promise<CallListItem[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n setNumber(query, \"limit\", options.limit);\n if (typeof options.before === \"string\" && options.before) query.set(\"before\", options.before);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls${qs ? `?${qs}` : \"\"}`, key, options, { cache: \"no-store\" });\n if (!Array.isArray(data)) return [];\n return data.map(readCallListItem).filter((call): call is CallListItem => call !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One shared call: audio links, transcript, summary and a page of screenshots.\n * Links expire after `linksExpireInSeconds` — read the call when the page\n * renders and never store them.\n *\n * Returns `null` when the call does not exist, is not shared, and on any\n * failure — call `notFound()` on it. Never throws.\n */\nexport async function getCall(id: string, options: GetCallOptions = {}): Promise<Call | null> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted || !key) return null;\n\n const query = new URLSearchParams({ id: wanted });\n setNumber(query, \"framesOffset\", options.framesOffset);\n setNumber(query, \"framesLimit\", options.framesLimit);\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls?${query.toString()}`, key, options, { cache: \"no-store\" });\n return readCall(data);\n } catch {\n return null;\n }\n}\n\n/**\n * Permanently delete one shared call — the recording, the screenshots, the\n * transcript. Cannot be undone.\n *\n * Resolves `{ deleted: false, error }` instead of throwing: the reason is worth\n * showing (\"The bot is still in this call — delete it after the call ends\").\n */\nexport async function deleteCall(id: string, options: CallsOptions = {}): Promise<DeleteCallResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n if (!wanted) return { deleted: false, error: \"No call id\" };\n const key = callsKey(options);\n if (!key) return { deleted: false, error: \"No server key — set GLITCHGRAB_CALLS_KEY (ggc_…)\" };\n\n const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return error === null ? { deleted: true } : { deleted: false, error };\n } catch {\n return { deleted: false, error: \"Could not delete the call\" };\n }\n}\n\n/**\n * `JSON.stringify` that is safe inside `<script type=\"application/ld+json\">`.\n *\n * Guide titles and summaries are written outside your codebase, and\n * `JSON.stringify` leaves `<` alone — a title containing `</script>` closes the\n * tag and whatever follows runs as HTML. The JSON unicode escape for `<` is the same\n * character to a JSON parser and inert to the HTML one. `>`, `&` and the two\n * JavaScript line separators are escaped for the same reason.\n *\n * ```tsx\n * <script\n * type=\"application/ld+json\"\n * dangerouslySetInnerHTML={{ __html: escapeJsonForScript({ \"@type\": \"HowTo\", name: guide.title }) }}\n * />\n * ```\n *\n * Never throws: a value JSON cannot represent (circular, `undefined`) becomes `null`.\n */\nexport function escapeJsonForScript(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return \"null\";\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n } catch {\n return \"null\";\n }\n}\n\nexport type { ReportResult, ReportSeverity, ReportType } from \"./types\";\n","import type { RuntimeInfo } from \"./types\";\n\n/**\n * Runtime health at the moment of the crash. `deviceInfo` says what the machine\n * is; this says what shape it was in — a leak that ran the heap to its ceiling,\n * a 2G connection that starved a fetch, a bug that only shows up after ten\n * minutes on the page or on the fifth error in a row.\n */\n\nconst pageLoadedAt = Date.now();\nlet errorCount = 0;\n\n/** Bumped by every auto-captured error so a report says which one in the run it was. */\nexport function incrementErrorCount(): number {\n errorCount += 1;\n return errorCount;\n}\n\n/** Test-only — reset the per-session counter. */\nexport function resetErrorCount(): void {\n errorCount = 0;\n}\n\ninterface MemoryCapableWindow {\n memory?: { usedJSHeapSize?: number; jsHeapSizeLimit?: number };\n}\n\ninterface NetworkInformation {\n effectiveType?: string;\n downlink?: number;\n rtt?: number;\n saveData?: boolean;\n}\n\nconst toMb = (bytes: number | undefined): number | undefined =>\n typeof bytes === \"number\" ? Math.round((bytes / 1048576) * 10) / 10 : undefined;\n\nexport function captureRuntimeInfo(): RuntimeInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n\n // Chromium-only, and absent entirely under cross-origin isolation.\n const memory = (performance as unknown as MemoryCapableWindow)?.memory;\n // Chromium + Android only.\n const connection = (navigator as unknown as { connection?: NetworkInformation })?.connection;\n\n return {\n timeOnPageMs: Date.now() - pageLoadedAt,\n errorCount,\n visibility: typeof document !== \"undefined\" ? document.visibilityState : \"unknown\",\n ...(memory\n ? {\n jsHeapUsedMb: toMb(memory.usedJSHeapSize),\n jsHeapLimitMb: toMb(memory.jsHeapSizeLimit),\n }\n : {}),\n ...(connection\n ? {\n connectionType: connection.effectiveType,\n downlinkMbps: connection.downlink,\n rttMs: connection.rtt,\n saveData: connection.saveData,\n }\n : {}),\n };\n } catch {\n return null;\n }\n}\n","import type {\n CapturedContext,\n DeviceInfo,\n FeedbackPayload,\n FeedbackResult,\n ReportPayload,\n ReportResult,\n} from \"./types\";\n// The React-free subpath: utils.ts is also bundled into `glitchgrab/server`.\nimport { assistOffline, toAssistTurnResult } from \"@glitchgrab/report-ui/assist-response\";\nimport type { AssistTurnResult } from \"@glitchgrab/report-ui\";\nimport { getBreadcrumbs } from \"./breadcrumbs\";\nimport { captureRuntimeInfo } from \"./runtime\";\nimport { getAppContext, getRelease } from \"./app-context\";\n\nconst SENSITIVE_PARAMS = [\n \"token\", \"key\", \"secret\", \"password\", \"passwd\", \"auth\", \"authorization\",\n \"session\", \"sessionid\", \"session_id\", \"api_key\", \"apikey\", \"access_token\",\n \"refresh_token\", \"client_secret\", \"code\", \"state\", \"nonce\", \"credential\", \"private\",\n];\n\nexport function sanitizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const params = new URLSearchParams(parsed.search);\n let modified = false;\n for (const key of Array.from(params.keys())) {\n if (SENSITIVE_PARAMS.some((s) => key.toLowerCase().includes(s))) {\n params.set(key, \"[REDACTED]\");\n modified = true;\n }\n }\n if (modified) parsed.search = params.toString();\n return parsed.toString();\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nexport function captureDeviceInfo(): DeviceInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n return {\n screenWidth: screen.width,\n screenHeight: screen.height,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n platform: navigator.platform ?? \"unknown\",\n language: navigator.language ?? \"unknown\",\n online: navigator.onLine,\n colorScheme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\",\n devicePixelRatio: window.devicePixelRatio ?? 1,\n };\n } catch {\n return null;\n }\n}\n\nexport function captureContext(visitedPages: string[]): CapturedContext {\n try {\n return {\n url: sanitizeUrl(typeof window !== \"undefined\" ? window.location.href : \"\"),\n userAgent: typeof navigator !== \"undefined\" ? navigator.userAgent : \"\",\n timestamp: new Date().toISOString(),\n visitedPages: visitedPages.map(sanitizeUrl),\n breadcrumbs: getBreadcrumbs(),\n deviceInfo: captureDeviceInfo(),\n runtime: captureRuntimeInfo(),\n appContext: getAppContext(),\n release: getRelease(),\n };\n } catch {\n return {\n url: \"\",\n userAgent: \"\",\n timestamp: new Date().toISOString(),\n visitedPages: [],\n breadcrumbs: [],\n deviceInfo: null,\n runtime: null,\n appContext: {},\n };\n }\n}\n\n/**\n * Metadata every report carries regardless of how it was raised — release,\n * runtime health, and the host app's own context. One builder so a user-filed\n * report and an auto-captured crash describe the same world; the payload sites\n * only add what is specific to them.\n *\n * App context is prefixed so a key named `timestamp` or `status` can't quietly\n * overwrite a field the dashboard relies on.\n */\nexport function contextMetadata(context: CapturedContext): Record<string, string> {\n try {\n const runtime = context.runtime;\n const appContext = Object.entries(context.appContext ?? {}).reduce<Record<string, string>>(\n (acc, [key, value]) => {\n acc[`ctx_${key}`] = value;\n return acc;\n },\n {}\n );\n\n return {\n ...(context.release ? { release: context.release } : {}),\n ...(runtime\n ? {\n timeOnPageMs: String(runtime.timeOnPageMs),\n errorCount: String(runtime.errorCount),\n visibility: runtime.visibility,\n ...(runtime.jsHeapUsedMb !== undefined\n ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) }\n : {}),\n ...(runtime.jsHeapLimitMb !== undefined\n ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) }\n : {}),\n ...(runtime.connectionType ? { connectionType: runtime.connectionType } : {}),\n ...(runtime.downlinkMbps !== undefined\n ? { downlinkMbps: String(runtime.downlinkMbps) }\n : {}),\n ...(runtime.rttMs !== undefined ? { rttMs: String(runtime.rttMs) } : {}),\n ...(runtime.saveData !== undefined ? { saveData: String(runtime.saveData) } : {}),\n }\n : {}),\n ...appContext,\n };\n } catch {\n return {};\n }\n}\n\nexport const DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 500;\n\n/**\n * Send a report and return the result.\n * Never throws — returns null on failure.\n */\nexport async function sendReport(\n payload: ReportPayload,\n baseUrl?: string\n): Promise<ReportResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/report`;\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n };\n\n // keepalive has a 64KB body limit in browsers — skip it for large payloads (e.g., screenshots)\n const useKeepalive = body.length < 60_000;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<ReportResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n // Fallback: sendBeacon (fire-and-forget, no result)\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n try {\n navigator.sendBeacon(url, new Blob([body], { type: \"application/json\" }));\n } catch {\n // Silently fail\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a star rating your end-user left about your app.\n * Never throws — returns null on failure.\n *\n * Unlike `sendReport`, this is always a deliberate user action with a dialog\n * open in front of them, so it retries on transient failure but skips the\n * sendBeacon fallback: the caller needs a real result to show a thank-you.\n */\nexport async function sendFeedback(\n payload: FeedbackPayload,\n baseUrl?: string\n): Promise<FeedbackResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/feedback`;\n const body = JSON.stringify(payload);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<FeedbackResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n // 4xx other than rate limiting is a caller mistake — retrying won't help.\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface EnhanceContext {\n url?: string;\n visitedPages?: string[];\n breadcrumbs?: Array<{ type: string; message: string }>;\n}\n\n/**\n * The signed-in user as the host's server vouched for them (`signSession`).\n *\n * Rides in the request BODY, never a header: every token route sets its own\n * CORS allow-list of headers, so a new header would fail preflight on all of\n * them. Omitted entirely when there is no signature — an unsigned id proves\n * nothing, and the server reads its absence as \"unsigned\".\n */\nexport interface SignedIdentity {\n userId?: string | null;\n signature?: string | null;\n}\n\n/** `{ userId, signature }` to spread into a JSON body, or nothing. */\nexport function signedFields(identity?: SignedIdentity | null): { userId?: string; signature?: string } {\n if (!identity?.signature || !identity.userId) return {};\n return { userId: identity.userId, signature: identity.signature };\n}\n\n/**\n * Polish raw description text via the Glitchgrab AI enhance endpoint.\n * Returns the polished text on success, or the original text on any failure.\n * Never throws.\n */\nexport async function enhanceText(\n text: string,\n token: string,\n baseUrl?: string,\n screenshot?: string | null,\n context?: EnhanceContext | null,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n const trimmed = text.trim();\n if (!trimmed) return text;\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n text: trimmed,\n ...(screenshot ? { screenshot } : {}),\n ...(context ? { context } : {}),\n ...signedFields(identity),\n }),\n });\n if (!response.ok) return text;\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { text?: string };\n };\n if (envelope?.success && typeof envelope.data?.text === \"string\") {\n return envelope.data.text;\n }\n return text;\n } catch {\n return text;\n }\n}\n\n/**\n * \"Is this already filed?\" for the report dialog's plain form. Asked when Send\n * is pressed; a match lets the reporter add to the open issue instead of\n * opening a second one.\n *\n * `[]` when the project has it off, `null` on any failure — the dialog sends\n * straight away on either. Never throws.\n */\nexport async function findSimilarIssues(\n description: string,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<{ number: number; title: string; url: string; status?: string }[] | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/reports/similar`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ text: description, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; matches?: { number: number; title: string; url: string; status?: string }[] };\n } | null;\n if (!response.ok || !envelope?.success) return null;\n if (!envelope.data?.enabled) return [];\n return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];\n } catch {\n return null;\n }\n}\n\n/**\n * One turn of the AI report assistant (#330).\n *\n * Same never-throw contract as everything else in the SDK: a failure comes back\n * as `degraded` and the dialog quietly falls back to the plain form. An\n * end-user of the host app must never see a Glitchgrab error, and must never be\n * blocked from filing a bug because a model was busy.\n */\nexport async function assistTurn(\n params: {\n messages: { role: \"user\" | \"assistant\"; content: string }[];\n conversationId: string | null;\n screenshot?: string | null;\n context?: Record<string, unknown> | null;\n /**\n * Who the host app says is logged in, from the provider's `session` prop.\n * Stored against the conversation so a chat and the report it produced\n * agree on who filed — one SDK token covers every end user of the host app,\n * so without this every stranger's chat looks like the same person.\n */\n reporter?: {\n key?: string | null;\n name?: string | null;\n email?: string | null;\n /** `signSession()` output for `key`; absent when the host did not sign. */\n signature?: string | null;\n };\n },\n token: string,\n baseUrl?: string\n): Promise<AssistTurnResult> {\n const offline = assistOffline();\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/report-chat`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(params),\n });\n // One mapping for every host (packages/report-ui/src/assist-response.ts):\n // the server's degrade reason passes through, and nothing the sheet renders\n // is dropped.\n return toAssistTurnResult(response.ok, await response.json().catch(() => null));\n } catch {\n return offline;\n }\n}\n\n/**\n * Send an audio Blob to the Glitchgrab STT proxy (which calls Sarvam).\n * Returns the transcript string, or \"\" on any failure. Never throws.\n */\nexport async function transcribeAudio(\n blob: Blob,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n if (blob.size === 0) return \"\";\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/stt`;\n const form = new FormData();\n form.append(\"file\", blob, \"audio.webm\");\n const signed = signedFields(identity);\n if (signed.userId && signed.signature) {\n form.append(\"userId\", signed.userId);\n form.append(\"signature\", signed.signature);\n }\n const response = await fetch(url, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: form,\n });\n if (!response.ok) return \"\";\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { transcript?: string };\n };\n if (envelope?.success && typeof envelope.data?.transcript === \"string\") {\n return envelope.data.transcript;\n }\n return \"\";\n } catch {\n return \"\";\n }\n}\n"],"mappings":"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","names":["_a"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/runtime.ts","../src/utils.ts"],"sourcesContent":["/**\n * Server-side reporting — `import { reportServerError } from \"glitchgrab/server\"`.\n *\n * The rest of this SDK is `\"use client\"`: it hooks `window.onerror`, walks the\n * DOM for a screenshot, and needs a mounted provider for its token. None of\n * that exists in a cron job at 3am, an API route handler, or a queue worker —\n * which is precisely where the failures nobody is watching happen. A WhatsApp\n * send Meta rejects, an SMTP timeout, a nightly digest that throws: no tab is\n * open, so the browser SDK never sees them and no issue is ever filed.\n *\n * This entry has no `\"use client\"`, touches no DOM API, and imports no React.\n * It POSTs to the same `/api/v1/sdk/report` endpoint the browser uses, with\n * `source: \"SDK_AUTO\"` so the server-side dedup applies (24h per signature,\n * 7 days once an issue exists) — otherwise a cron failing hourly would open\n * twenty-four identical issues a day.\n *\n * Like the rest of the SDK it never throws: a reporting failure must not turn\n * a handled error into an unhandled one.\n */\nimport { createHmac } from \"node:crypto\";\nimport type { ReportPayload, ReportResult, ReportSeverity, ReportType } from \"./types\";\nimport { DEFAULT_BASE_URL, sendReport } from \"./utils\";\n\n/** Who to attribute the report to. A server has no signed-in user, so this is the job or service. */\nexport interface ServerReporterIdentity {\n /** Stable id — the cron name, the service name. Defaults to `\"server\"`. */\n id?: string;\n /** Display name shown on the report. Defaults to `\"Server\"`. */\n name?: string;\n email?: string;\n phone?: string;\n}\n\n/** Defaults applied to every server report. Set once at boot with {@link configureServerReporter}. */\nexport interface ServerReporterConfig {\n /** Repo token (`gg_…`). Falls back to `process.env.GLITCHGRAB_TOKEN`. */\n token?: string;\n /** Override the API host. Falls back to `process.env.GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Where this ran — `\"cron/attendance-prompts\"`, `\"worker/email-queue\"`.\n *\n * It is the grouping key, not a link: it is sent as the report's `pageUrl`\n * so two different jobs throwing the same `\"Timeout\"` stay two issues\n * instead of collapsing into one.\n */\n context?: string;\n /** Merged into every report's metadata. Good place for region, deployment id, service name. */\n metadata?: Record<string, string>;\n reporter?: ServerReporterIdentity;\n /**\n * Report from `NODE_ENV=development` too. Off by default, matching the\n * browser SDK — a laptop crashing its way through a refactor should not file\n * issues. Unlike the browser there is no `Origin` header for the API to\n * recognise, so nothing else stops a dev machine from filing.\n */\n enableInDevelopment?: boolean;\n}\n\n/** Per-call options. Anything set here wins over {@link configureServerReporter}. */\nexport interface ServerReportOptions extends ServerReporterConfig {\n /** Issue type. Defaults to `BUG`. */\n type?: ReportType;\n /** Rendered as a `severity:<value>` label on the GitHub issue. */\n severity?: ReportSeverity;\n /** Extra prose for the issue body — what the job was doing, which record it was on. */\n description?: string;\n /**\n * A real request URL, when there is one (an API route handler). Overrides\n * the synthetic URL built from `context`.\n */\n pageUrl?: string;\n}\n\nconst DEV_BLOCKED_MESSAGE = \"development\";\n\n/**\n * Node's `process`, as much of it as this file uses.\n *\n * `src/global.d.ts` declares a browser-shaped `process` with nothing but\n * `env`, and that is deliberate — the client bundle must not be able to reach\n * for Node APIs. So this entry reads the real one off `globalThis` behind its\n * own narrow type instead of pulling `@types/node` into the package.\n */\ninterface NodeProcessLike {\n env?: Record<string, string | undefined>;\n version?: string;\n on?(event: string, listener: (arg: never) => void): unknown;\n off?(event: string, listener: (arg: never) => void): unknown;\n listenerCount?(event: string): number;\n}\n\nfunction nodeProcess(): NodeProcessLike | undefined {\n try {\n const candidate = (globalThis as { process?: NodeProcessLike }).process;\n return candidate && typeof candidate === \"object\" ? candidate : undefined;\n } catch {\n return undefined;\n }\n}\n\nlet defaults: ServerReporterConfig = {};\n\n/**\n * Set the token and the defaults once, at module load, instead of passing them\n * to every call site. A later call's own options still win.\n */\nexport function configureServerReporter(config: ServerReporterConfig): void {\n defaults = { ...defaults, ...config };\n}\n\n/** Test-only — drop everything {@link configureServerReporter} set. */\nexport function resetServerReporter(): void {\n defaults = {};\n}\n\nfunction readEnv(name: string): string | undefined {\n try {\n return nodeProcess()?.env?.[name];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Message and stack out of whatever was thrown.\n *\n * A thrown string, a rejected `{ code, message }`, an `undefined` from a\n * `Promise.reject()` — all of it reaches here, and all of it must produce a\n * non-empty message: the API's dedup signature is `null` without one, and a\n * report with no signature is a fresh issue every single time.\n */\nfunction describeError(error: unknown): { message: string; stack?: string } {\n try {\n if (error instanceof Error) {\n return {\n message: error.message || error.name || \"Unknown server error\",\n stack: error.stack,\n };\n }\n if (typeof error === \"string\") {\n return { message: error || \"Unknown server error\" };\n }\n if (error && typeof error === \"object\") {\n const maybe = error as { message?: unknown; stack?: unknown };\n const message = typeof maybe.message === \"string\" ? maybe.message : JSON.stringify(error);\n return {\n message: message || \"Unknown server error\",\n stack: typeof maybe.stack === \"string\" ? maybe.stack : undefined,\n };\n }\n return { message: String(error ?? \"\") || \"Unknown server error\" };\n } catch {\n return { message: \"Unknown server error\" };\n }\n}\n\n/**\n * `pageUrl` doubles as the dedup grouping key, so a job needs one even though\n * no page was involved. `server://` keeps it obviously synthetic — and keeps it\n * clear of the API's localhost check, which only matches http(s) and would\n * otherwise swallow the report with a 200 and create nothing.\n */\nfunction contextUrl(context: string | undefined): string | undefined {\n if (!context) return undefined;\n return `server://${context.replace(/^\\/+/, \"\")}`;\n}\n\nfunction runtimeMetadata(): Record<string, string> {\n try {\n const proc = nodeProcess();\n if (!proc) return { runtime: \"server\" };\n const region = proc.env?.VERCEL_REGION;\n const deploymentEnv = proc.env?.VERCEL_ENV;\n return {\n runtime: \"node\",\n ...(proc.version ? { nodeVersion: proc.version } : {}),\n ...(region ? { region } : {}),\n ...(deploymentEnv ? { deploymentEnv } : {}),\n };\n } catch {\n return { runtime: \"server\" };\n }\n}\n\n/**\n * File a server-side error as a GitHub issue.\n *\n * Await it. On a serverless platform the function may be frozen the moment your\n * handler returns, and a floating promise dies with it — the report never\n * leaves the machine.\n *\n * ```ts\n * try {\n * await sendDigest();\n * } catch (error) {\n * await reportServerError(error, { context: \"cron/daily-digest\" });\n * throw error;\n * }\n * ```\n *\n * Returns `null` when nothing was filed — no token, development mode, or the\n * API refused it. Never throws.\n */\nexport async function reportServerError(\n error: unknown,\n options: ServerReportOptions = {}\n): Promise<ReportResult | null> {\n try {\n const token = options.token ?? defaults.token ?? readEnv(\"GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const enableInDevelopment = options.enableInDevelopment ?? defaults.enableInDevelopment ?? false;\n if (!enableInDevelopment && readEnv(\"NODE_ENV\") === DEV_BLOCKED_MESSAGE) return null;\n\n const { message, stack } = describeError(error);\n const context = options.context ?? defaults.context;\n const reporter = { ...defaults.reporter, ...options.reporter };\n\n const payload: ReportPayload = {\n token,\n source: \"SDK_AUTO\",\n type: options.type ?? \"BUG\",\n errorMessage: message,\n ...(stack ? { errorStack: stack } : {}),\n ...(options.description ? { description: options.description } : {}),\n ...(options.pageUrl ?? contextUrl(context)\n ? { pageUrl: options.pageUrl ?? contextUrl(context) }\n : {}),\n metadata: {\n ...runtimeMetadata(),\n ...defaults.metadata,\n ...options.metadata,\n ...(context ? { serverContext: context } : {}),\n ...(options.severity ? { severity: options.severity } : {}),\n sessionUserId: reporter.id ?? context ?? \"server\",\n sessionUserName: reporter.name ?? \"Server\",\n ...(reporter.email ? { sessionUserEmail: reporter.email } : {}),\n ...(reporter.phone ? { sessionUserPhone: reporter.phone } : {}),\n },\n };\n\n return await sendReport(payload, options.baseUrl ?? defaults.baseUrl ?? readEnv(\"GLITCHGRAB_BASE_URL\"));\n } catch {\n return null;\n }\n}\n\n/** Options for {@link captureServerErrors}. */\nexport interface CaptureServerErrorsOptions extends ServerReportOptions {\n /**\n * Also report rejected promises nobody handled. Default `true`.\n *\n * Node crashes the process on an unhandled rejection — but only while no\n * `unhandledRejection` listener exists. Attaching one silently converts a\n * crash into a warning, so this re-throws afterwards to put the default\n * behaviour back, unless the host app has its own listener (then it is theirs\n * to decide).\n */\n captureUnhandledRejections?: boolean;\n}\n\n/**\n * Report every uncaught exception and unhandled rejection in this process.\n *\n * Call it once at boot — `instrumentation.ts` in a Next.js app. Returns a\n * function that removes the listeners again.\n *\n * Uncaught exceptions use `uncaughtExceptionMonitor`, which observes without\n * taking over: the process still crashes exactly as it would have. A reporter\n * that keeps a broken process alive is worse than no reporter.\n */\nexport function captureServerErrors(options: CaptureServerErrorsOptions = {}): () => void {\n try {\n const proc = nodeProcess();\n if (!proc || typeof proc.on !== \"function\") return () => {};\n\n const onException = (error: unknown) => {\n void reportServerError(error, { ...options, context: options.context ?? \"uncaughtException\" });\n };\n\n const onRejection = (reason: unknown) => {\n void reportServerError(reason, {\n ...options,\n context: options.context ?? \"unhandledRejection\",\n });\n\n // Ours is the only listener, so suppressing the crash was never the\n // host app's choice. Hand the default behaviour back.\n if (proc.listenerCount?.(\"unhandledRejection\") === 1) {\n setTimeout(() => {\n throw reason;\n }, 0);\n }\n };\n\n proc.on(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n\n const captureRejections = options.captureUnhandledRejections ?? true;\n if (captureRejections) proc.on(\"unhandledRejection\", onRejection as (arg: never) => void);\n\n return () => {\n try {\n proc.off?.(\"uncaughtExceptionMonitor\", onException as (arg: never) => void);\n if (captureRejections) {\n proc.off?.(\"unhandledRejection\", onRejection as (arg: never) => void);\n }\n } catch {\n // Nothing to undo.\n }\n };\n } catch {\n return () => {};\n }\n}\n\n/** Options for {@link signSession}. */\nexport interface SignSessionOptions {\n /** Your user's id — the same value you pass as `session.userId`. */\n userId: string;\n /** The repo's signing secret (`ggs_…`). Falls back to `process.env.GLITCHGRAB_SIGNING_SECRET`. */\n secret?: string;\n /** How long the signature stays valid, in seconds. Default one day. */\n ttlSeconds?: number;\n /** Clock override in epoch ms. For tests. */\n now?: number;\n}\n\n/**\n * Vouch for the logged-in user so the SDK's AI features work when the project\n * owner turned on signed users.\n *\n * The `gg_` token ships in your JavaScript, so on its own it proves nothing\n * about who is calling. Your server holds the signing secret; call this where\n * you build the `session` prop and pass the result as `session.signature`.\n * Render it per request so it refreshes before `ttlSeconds` runs out.\n *\n * Format: `v1.<exp>.<base64url HMAC-SHA256(secret, \"v1.<exp>.<userId>\")>`.\n *\n * Unlike the rest of this SDK it throws: it runs in your own server code, where\n * a missing secret is a deploy mistake you want to see — not a user who quietly\n * loses the AI features.\n */\nexport function signSession(options: SignSessionOptions): string {\n const userId = options?.userId;\n if (typeof userId !== \"string\" || userId.length === 0) {\n throw new Error(\"glitchgrab signSession: userId is required\");\n }\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n const secret = options.secret || env?.GLITCHGRAB_SIGNING_SECRET;\n if (!secret) {\n throw new Error(\n \"glitchgrab signSession: no signing secret — pass `secret` or set GLITCHGRAB_SIGNING_SECRET\"\n );\n }\n const ttl = options.ttlSeconds ?? 86_400;\n if (!Number.isFinite(ttl) || ttl <= 0) {\n throw new Error(\"glitchgrab signSession: ttlSeconds must be a positive number\");\n }\n const exp = Math.floor((options.now ?? Date.now()) / 1000) + Math.floor(ttl);\n const sig = createHmac(\"sha256\", secret).update(`v1.${exp}.${userId}`).digest(\"base64url\");\n return `v1.${exp}.${sig}`;\n}\n\n// ── Guides ───────────────────────────────────────────────────────────────────\n//\n// A team writes its how-tos in Glitchgrab and renders them on its own site from\n// `GET /api/v1/sdk/guides` (#370). Every site needs the same client — token,\n// caching, never failing the page, shape checks — so it lives here instead of\n// being rewritten, slightly differently, in each product (#409).\n\n/** One guide as the list returns it — no body. Dates are ISO strings: this came over JSON. */\nexport interface GuideSummary {\n slug: string;\n title: string;\n /** One line saying what the guide answers. */\n summary: string;\n category: string | null;\n /** The order set in Glitchgrab. The list already arrives sorted by it. */\n orderIndex: number;\n /** Always `\"PUBLISHED\"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */\n status: \"DRAFT\" | \"PUBLISHED\";\n /** When someone last re-checked the steps against the product. */\n lastVerifiedAt: string | null;\n updatedAt: string;\n /**\n * Where this guide lives on your site: its own link, else\n * `<guides base URL>/<slug>`, else null when the repo sets neither.\n */\n link: string | null;\n}\n\n/** One guide with its body. */\nexport interface Guide extends GuideSummary {\n /** The markdown as written. */\n content: string;\n /**\n * `content` rendered by Glitchgrab: raw HTML in the markdown arrives as text,\n * only allowlisted attributes survive, and links keep only http(s), mailto\n * and relative targets. Headings carry `id=\"guide-<heading text>\"`, for a\n * table of contents or a link to a single step.\n *\n * Safe to render as-is. On a domain that holds your users' sessions, a second\n * sanitize pass of your own is cheap defence in depth — this SDK ships no\n * sanitizer so it stays dependency-free.\n */\n html: string;\n}\n\n/** Options for {@link listGuides} and {@link getGuide}. */\nexport interface GuideReadOptions {\n /**\n * Repo token (`gg_…`). Falls back to {@link configureServerReporter}'s token,\n * then `GLITCHGRAB_TOKEN`, then `NEXT_PUBLIC_GLITCHGRAB_TOKEN` — the token is\n * public, so the one your provider already uses is fine.\n *\n * Never pass the `ggc_` server key for a public page: it reads drafts too, and client calls.\n */\n token?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /**\n * Seconds Next.js may cache the response. Default 300; `0` reads fresh on\n * every request. The route is `force-dynamic`, so without this nothing is\n * cached at all. Ignored outside Next.js.\n */\n revalidate?: number;\n /** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */\n timeoutMs?: number;\n}\n\nconst GUIDES_REVALIDATE_SECONDS = 300;\nconst SDK_API_TIMEOUT_MS = 5000;\n\n/** `RequestInit` plus Next.js's cache option, without pulling Next's types into this entry. */\ntype NextFetchInit = RequestInit & { next?: { revalidate?: number } };\n\ninterface SdkApiResult {\n /** `data` from a successful response, else null. */\n data: unknown;\n /** Null on success; the route's own `error` when it sent one, so a caller can show why. */\n error: string | null;\n}\n\n/**\n * One request to a Glitchgrab SDK route. Never throws: a refusal, a timeout, a\n * network failure and unreadable JSON all come back as `error`.\n */\nasync function callSdkApi(\n path: string,\n token: string,\n options: { baseUrl?: string; timeoutMs?: number },\n init: { method?: \"GET\" | \"PUT\" | \"DELETE\"; body?: unknown; cache: { revalidate: number } | \"no-store\" }\n): Promise<SdkApiResult> {\n const baseUrl = (\n options.baseUrl ??\n defaults.baseUrl ??\n readEnv(\"GLITCHGRAB_BASE_URL\") ??\n DEFAULT_BASE_URL\n ).replace(/\\/+$/, \"\");\n\n const controller = typeof AbortController === \"function\" ? new AbortController() : undefined;\n const timer = controller\n ? setTimeout(() => controller.abort(), options.timeoutMs ?? SDK_API_TIMEOUT_MS)\n : undefined;\n\n try {\n const request: NextFetchInit = {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),\n ...(init.cache === \"no-store\" ? { cache: \"no-store\" as const } : { next: { revalidate: init.cache.revalidate } }),\n ...(controller ? { signal: controller.signal } : {}),\n };\n const response = await fetch(`${baseUrl}${path}`, request);\n const body = (await response.json().catch(() => null)) as\n | { success?: unknown; data?: unknown; error?: unknown }\n | null;\n if (!response.ok || body?.success !== true) {\n return { data: null, error: typeof body?.error === \"string\" ? body.error : `HTTP ${response.status}` };\n }\n return { data: body.data ?? null, error: null };\n } catch {\n return { data: null, error: \"Could not reach Glitchgrab\" };\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/** `data` from the guides route, or null on any failure — no token, non-2xx, timeout, bad JSON. */\nasync function readGuidesApi(query: string, options: GuideReadOptions): Promise<unknown> {\n const token =\n options.token ??\n defaults.token ??\n readEnv(\"GLITCHGRAB_TOKEN\") ??\n readEnv(\"NEXT_PUBLIC_GLITCHGRAB_TOKEN\");\n if (!token) return null;\n\n const revalidate =\n typeof options.revalidate === \"number\" && Number.isFinite(options.revalidate) && options.revalidate >= 0\n ? options.revalidate\n : GUIDES_REVALIDATE_SECONDS;\n\n const result = await callSdkApi(`/api/v1/sdk/guides${query}`, token, options, { cache: { revalidate } });\n return result.data;\n}\n\n/**\n * A guide summary out of whatever the API sent, or null when it is not one.\n * Only slug, title and summary are required — a page cannot render without\n * them. The rest default, so the returned type is honest either way.\n */\nfunction readGuideSummary(value: unknown): GuideSummary | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const guide = value as Record<string, unknown>;\n if (\n typeof guide.slug !== \"string\" ||\n typeof guide.title !== \"string\" ||\n typeof guide.summary !== \"string\"\n ) {\n return null;\n }\n return {\n slug: guide.slug,\n title: guide.title,\n summary: guide.summary,\n category: typeof guide.category === \"string\" ? guide.category : null,\n orderIndex: typeof guide.orderIndex === \"number\" ? guide.orderIndex : 0,\n status: guide.status === \"DRAFT\" ? \"DRAFT\" : \"PUBLISHED\",\n lastVerifiedAt: typeof guide.lastVerifiedAt === \"string\" ? guide.lastVerifiedAt : null,\n updatedAt: typeof guide.updatedAt === \"string\" ? guide.updatedAt : \"\",\n link: typeof guide.link === \"string\" ? guide.link : null,\n };\n}\n\n/**\n * Every published guide for the token's repo, in the order set in Glitchgrab.\n * No bodies — this is the index page.\n *\n * ```ts\n * // app/guides/page.tsx\n * import { listGuides } from \"glitchgrab/server\";\n *\n * export default async function GuidesPage() {\n * const guides = await listGuides();\n * return guides.map((g) => <a key={g.slug} href={`/guides/${g.slug}`}>{g.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws: a Glitchgrab outage leaves the\n * page empty instead of turning it into a 500.\n */\nexport async function listGuides(options: GuideReadOptions = {}): Promise<GuideSummary[]> {\n try {\n const data = await readGuidesApi(\"\", options);\n if (!Array.isArray(data)) return [];\n return data.map(readGuideSummary).filter((guide): guide is GuideSummary => guide !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One published guide with its markdown and rendered HTML.\n *\n * Returns `null` when there is no such guide and on any failure — call\n * `notFound()` on it. Never throws.\n */\nexport async function getGuide(slug: string, options: GuideReadOptions = {}): Promise<Guide | null> {\n try {\n const wanted = typeof slug === \"string\" ? slug.trim() : \"\";\n if (!wanted) return null;\n\n const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}`, options);\n const summary = readGuideSummary(data);\n const body = data as { content?: unknown; html?: unknown } | null;\n if (!summary || typeof body?.html !== \"string\") return null;\n\n return {\n ...summary,\n content: typeof body.content === \"string\" ? body.content : \"\",\n html: body.html,\n };\n } catch {\n return null;\n }\n}\n\n// ── Calls ────────────────────────────────────────────────────────────────────\n//\n// Recorded client calls a project shared with its client (#416), for the team's\n// own admin panel. Unlike guides these are private: they need the project's\n// server key (`ggc_…`, API Tokens → Server keys), which lives on the server and never in a bundle, and\n// every response holds links that expire — so nothing here is cached.\n\n/** Someone on the call, as the recorder saw them. */\nexport interface CallParticipant {\n name: string | null;\n email: string | null;\n}\n\n/** One shared call as the list returns it — no transcript, no links. */\nexport interface CallListItem {\n id: string;\n title: string | null;\n startsAt: string | null;\n endsAt: string | null;\n durationSec: number | null;\n /** `\"bot\"` (joined the Meet) or `\"extension\"` (recorded from someone's Chrome). */\n recorder: string | null;\n /** Match a call to your customer by these emails. */\n participants: CallParticipant[];\n hasRecording: boolean;\n /** `IDLE`, `RUNNING`, `DONE` or `FAILED`. */\n transcriptStatus: string;\n /** One line on what the call was about, once Glitchgrab has read it. */\n overview: string | null;\n createdAt: string;\n}\n\nexport interface CallAudio {\n /**\n * `call` — a bot recording's single mixed track. An extension recording has\n * two instead: `client` (everyone else on the call) and `recorder` (the\n * person who recorded it).\n */\n track: \"call\" | \"client\" | \"recorder\";\n /** Plays for {@link Call.linksExpireInSeconds}. */\n url: string;\n}\n\nexport interface CallMoment {\n /** Milliseconds into the call — seek the audio here. */\n tMs: number | null;\n speaker: string | null;\n text: string;\n}\n\n/** One shared call in full. */\nexport interface Call extends CallListItem {\n meetUrl: string | null;\n audio: CallAudio[];\n /** Audio and screenshot links stop working after this many seconds. Read the call per render; never store them. */\n linksExpireInSeconds: number;\n transcript: string | null;\n summary: { overview: string; moments: CallMoment[] } | null;\n /** Screenshots grabbed during the call, a page at a time. `tMs` is on the transcript's clock. */\n frames: { total: number; offset: number; limit: number; items: { tMs: number; url: string }[] };\n}\n\n/** Options for {@link listCalls}, {@link getCall}, {@link deleteCall} and the booking settings calls. */\nexport interface CallsOptions {\n /**\n * The project's server key (`ggc_…`, API Tokens → Server keys). Falls back to `GLITCHGRAB_CALLS_KEY`.\n * Server only — never a `NEXT_PUBLIC_` variable: it plays and deletes calls.\n */\n key?: string;\n /** Override the API host. Falls back to `GLITCHGRAB_BASE_URL`, then `https://glitchgrab.dev`. */\n baseUrl?: string;\n /** Give up after this many ms. Default 5000. */\n timeoutMs?: number;\n}\n\nexport interface ListCallsOptions extends CallsOptions {\n /** Default 20, max 100. */\n limit?: number;\n /** ISO date: only calls recorded before it. Pass the last call's `createdAt` for the next page. */\n before?: string;\n}\n\nexport interface GetCallOptions extends CallsOptions {\n /** First screenshot to return. Default 0. */\n framesOffset?: number;\n /** Screenshots per page. Default 50, max 200, `0` for none. */\n framesLimit?: number;\n}\n\nexport type DeleteCallResult = { deleted: true } | { deleted: false; error: string };\n\nconst CALLS_KEY_PREFIX = \"ggc_\";\nconst NO_KEY_ERROR = \"No server key — set GLITCHGRAB_CALLS_KEY (ggc_…)\";\n\n/** Only a `ggc_` key. A `gg_` token here would only 401 — and means the public token sits where a secret belongs. */\nfunction callsKey(options: CallsOptions): string | null {\n const key = options.key ?? readEnv(\"GLITCHGRAB_CALLS_KEY\");\n return typeof key === \"string\" && key.startsWith(CALLS_KEY_PREFIX) ? key : null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction numberOrNull(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction readCallListItem(value: unknown): CallListItem | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n title: stringOrNull(value.title),\n startsAt: stringOrNull(value.startsAt),\n endsAt: stringOrNull(value.endsAt),\n durationSec: numberOrNull(value.durationSec),\n recorder: stringOrNull(value.recorder),\n participants: Array.isArray(value.participants)\n ? value.participants.filter(isRecord).map((p) => ({ name: stringOrNull(p.name), email: stringOrNull(p.email) }))\n : [],\n hasRecording: value.hasRecording === true,\n transcriptStatus: typeof value.transcriptStatus === \"string\" ? value.transcriptStatus : \"IDLE\",\n overview: stringOrNull(value.overview),\n createdAt: typeof value.createdAt === \"string\" ? value.createdAt : \"\",\n };\n}\n\nconst AUDIO_TRACKS: ReadonlySet<string> = new Set([\"call\", \"client\", \"recorder\"]);\n\nfunction readCall(value: unknown): Call | null {\n const base = readCallListItem(value);\n if (!base || !isRecord(value)) return null;\n\n const rawSummary = value.summary;\n const summary =\n isRecord(rawSummary) && typeof rawSummary.overview === \"string\"\n ? {\n overview: rawSummary.overview,\n moments: Array.isArray(rawSummary.moments)\n ? rawSummary.moments\n .filter(isRecord)\n .filter((m) => typeof m.text === \"string\")\n .map((m) => ({ tMs: numberOrNull(m.tMs), speaker: stringOrNull(m.speaker), text: m.text as string }))\n : [],\n }\n : null;\n\n const frames = isRecord(value.frames) ? value.frames : {};\n\n return {\n ...base,\n meetUrl: stringOrNull(value.meetUrl),\n audio: Array.isArray(value.audio)\n ? value.audio\n .filter(isRecord)\n .filter((a) => typeof a.url === \"string\" && typeof a.track === \"string\" && AUDIO_TRACKS.has(a.track))\n .map((a) => ({ track: a.track as CallAudio[\"track\"], url: a.url as string }))\n : [],\n linksExpireInSeconds: numberOrNull(value.linksExpireInSeconds) ?? 0,\n transcript: stringOrNull(value.transcript),\n summary,\n frames: {\n total: numberOrNull(frames.total) ?? 0,\n offset: numberOrNull(frames.offset) ?? 0,\n limit: numberOrNull(frames.limit) ?? 0,\n items: Array.isArray(frames.items)\n ? frames.items\n .filter(isRecord)\n .filter((f) => typeof f.url === \"string\" && typeof f.tMs === \"number\")\n .map((f) => ({ tMs: f.tMs as number, url: f.url as string }))\n : [],\n },\n };\n}\n\nfunction setNumber(query: URLSearchParams, name: string, value: unknown): void {\n if (typeof value === \"number\" && Number.isFinite(value)) query.set(name, String(Math.floor(value)));\n}\n\n/**\n * Calls shared with the client on the key's project, newest first. No links —\n * open one with {@link getCall}.\n *\n * ```ts\n * // app/admin/calls/page.tsx — server component\n * import { listCalls } from \"glitchgrab/server\";\n *\n * export default async function CallsPage() {\n * const calls = await listCalls(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * return calls.map((c) => <a key={c.id} href={`/admin/calls/${c.id}`}>{c.title}</a>);\n * }\n * ```\n *\n * Returns `[]` on any failure. Never throws.\n */\nexport async function listCalls(options: ListCallsOptions = {}): Promise<CallListItem[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n setNumber(query, \"limit\", options.limit);\n if (typeof options.before === \"string\" && options.before) query.set(\"before\", options.before);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls${qs ? `?${qs}` : \"\"}`, key, options, { cache: \"no-store\" });\n if (!Array.isArray(data)) return [];\n return data.map(readCallListItem).filter((call): call is CallListItem => call !== null);\n } catch {\n return [];\n }\n}\n\n/**\n * One shared call: audio links, transcript, summary and a page of screenshots.\n * Links expire after `linksExpireInSeconds` — read the call when the page\n * renders and never store them.\n *\n * Returns `null` when the call does not exist, is not shared, and on any\n * failure — call `notFound()` on it. Never throws.\n */\nexport async function getCall(id: string, options: GetCallOptions = {}): Promise<Call | null> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted || !key) return null;\n\n const query = new URLSearchParams({ id: wanted });\n setNumber(query, \"framesOffset\", options.framesOffset);\n setNumber(query, \"framesLimit\", options.framesLimit);\n\n const { data } = await callSdkApi(`/api/v1/sdk/calls?${query.toString()}`, key, options, { cache: \"no-store\" });\n return readCall(data);\n } catch {\n return null;\n }\n}\n\n/**\n * Permanently delete one shared call — the recording, the screenshots, the\n * transcript. Cannot be undone.\n *\n * Resolves `{ deleted: false, error }` instead of throwing: the reason is worth\n * showing (\"The bot is still in this call — delete it after the call ends\").\n */\nexport async function deleteCall(id: string, options: CallsOptions = {}): Promise<DeleteCallResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n if (!wanted) return { deleted: false, error: \"No call id\" };\n const key = callsKey(options);\n if (!key) return { deleted: false, error: NO_KEY_ERROR };\n\n const { error } = await callSdkApi(`/api/v1/sdk/calls?id=${encodeURIComponent(wanted)}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return error === null ? { deleted: true } : { deleted: false, error };\n } catch {\n return { deleted: false, error: \"Could not delete the call\" };\n }\n}\n\n// ── Booking settings ─────────────────────────────────────────────────────────\n//\n// A project's demo booking settings — whether prospects can book, the hours,\n// slot length and the WhatsApp code — so a team's admin panel can change them\n// without a Glitchgrab login. Same server key as calls. Which Google calendar\n// demos land in is not here: it stays set on the Glitchgrab dashboard.\n\n/** `{ \"1\": [[\"09:00\", \"17:00\"]] }` — ISO weekday (1 = Monday) to opening ranges, in {@link BookingSettings.timezone}. */\nexport type BookingWorkingHours = Record<string, [string, string][]>;\n\nexport interface BookingSettings {\n /** Off means the booking dialog and WhatsApp refuse to offer slots. */\n enabled: boolean;\n /** Length of a demo, 15–180. */\n slotMinutes: number;\n /** Free minutes kept after each booking, 0–120. */\n bufferMinutes: number;\n /** IANA zone the working hours are in, e.g. `\"Asia/Kolkata\"`. */\n timezone: string;\n /** A day left out is closed. */\n workingHours: BookingWorkingHours;\n title: string | null;\n description: string | null;\n /** How far ahead a slot can be booked, 1–90 days. */\n horizonDays: number;\n /** Earliest a slot can start from now, 0–10080 minutes. */\n noticeMinutes: number;\n /** Code in the \"Book on WhatsApp\" link. Lower case, letters, digits and `-`; unique across projects. */\n whatsappCode: string | null;\n}\n\nexport interface BookingSettingsState {\n settings: BookingSettings;\n /** False until someone saves once — `settings` are the defaults. */\n saved: boolean;\n /** No calendar means no slots, whatever `enabled` says. Connect one in Glitchgrab → Calls → Booking. */\n calendarConnected: boolean;\n}\n\nexport type BookingSettingsResult = { ok: true; data: BookingSettingsState } | { ok: false; error: string };\n\nconst WEEKDAY = /^[1-7]$/;\n\nfunction readBookingSettingsState(value: unknown): BookingSettingsState | null {\n if (!isRecord(value) || !isRecord(value.settings)) return null;\n const s = value.settings;\n const hours: BookingWorkingHours = {};\n if (isRecord(s.workingHours)) {\n for (const [day, ranges] of Object.entries(s.workingHours)) {\n if (!WEEKDAY.test(day) || !Array.isArray(ranges)) continue;\n const valid = ranges.filter(\n (r): r is [string, string] => Array.isArray(r) && typeof r[0] === \"string\" && typeof r[1] === \"string\"\n );\n if (valid.length) hours[day] = valid.map(([open, close]) => [open, close]);\n }\n }\n return {\n settings: {\n enabled: s.enabled === true,\n slotMinutes: numberOrNull(s.slotMinutes) ?? 60,\n bufferMinutes: numberOrNull(s.bufferMinutes) ?? 0,\n timezone: typeof s.timezone === \"string\" ? s.timezone : \"Asia/Kolkata\",\n workingHours: hours,\n title: stringOrNull(s.title),\n description: stringOrNull(s.description),\n horizonDays: numberOrNull(s.horizonDays) ?? 15,\n noticeMinutes: numberOrNull(s.noticeMinutes) ?? 120,\n whatsappCode: stringOrNull(s.whatsappCode),\n },\n saved: value.saved === true,\n calendarConnected: value.calendarConnected === true,\n };\n}\n\n/**\n * The key's project booking settings. Never throws — `{ ok: false, error }` on\n * any failure, so the page can say why instead of showing defaults as if saved.\n *\n * ```ts\n * const result = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * if (result.ok && !result.data.calendarConnected) warn(\"Connect a calendar in Glitchgrab\");\n * ```\n */\nexport async function getBookingSettings(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n try {\n const key = callsKey(options);\n if (!key) return { ok: false, error: NO_KEY_ERROR };\n const { data, error } = await callSdkApi(\"/api/v1/sdk/booking-settings\", key, options, { cache: \"no-store\" });\n const state = error === null ? readBookingSettingsState(data) : null;\n return state ? { ok: true, data: state } : { ok: false, error: error ?? \"Unexpected response\" };\n } catch {\n return { ok: false, error: \"Could not read booking settings\" };\n }\n}\n\n/**\n * Change booking settings. Send only what changed — a field left out keeps its\n * value. Numbers are clamped to their range and invalid working hours are\n * ignored, so read `data.settings` back rather than trusting what you sent.\n *\n * `error` is worth showing: a WhatsApp code already taken by another project\n * comes back as one.\n */\nexport async function updateBookingSettings(\n changes: Partial<BookingSettings>,\n options: CallsOptions = {}\n): Promise<BookingSettingsResult> {\n try {\n const key = callsKey(options);\n if (!key) return { ok: false, error: NO_KEY_ERROR };\n const { data, error } = await callSdkApi(\"/api/v1/sdk/booking-settings\", key, options, {\n method: \"PUT\",\n body: changes ?? {},\n cache: \"no-store\",\n });\n const state = error === null ? readBookingSettingsState(data) : null;\n return state ? { ok: true, data: state } : { ok: false, error: error ?? \"Unexpected response\" };\n } catch {\n return { ok: false, error: \"Could not save booking settings\" };\n }\n}\n\n/**\n * `JSON.stringify` that is safe inside `<script type=\"application/ld+json\">`.\n *\n * Guide titles and summaries are written outside your codebase, and\n * `JSON.stringify` leaves `<` alone — a title containing `</script>` closes the\n * tag and whatever follows runs as HTML. The JSON unicode escape for `<` is the same\n * character to a JSON parser and inert to the HTML one. `>`, `&` and the two\n * JavaScript line separators are escaped for the same reason.\n *\n * ```tsx\n * <script\n * type=\"application/ld+json\"\n * dangerouslySetInnerHTML={{ __html: escapeJsonForScript({ \"@type\": \"HowTo\", name: guide.title }) }}\n * />\n * ```\n *\n * Never throws: a value JSON cannot represent (circular, `undefined`) becomes `null`.\n */\nexport function escapeJsonForScript(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return \"null\";\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n } catch {\n return \"null\";\n }\n}\n\nexport type { ReportResult, ReportSeverity, ReportType } from \"./types\";\n","import type { RuntimeInfo } from \"./types\";\n\n/**\n * Runtime health at the moment of the crash. `deviceInfo` says what the machine\n * is; this says what shape it was in — a leak that ran the heap to its ceiling,\n * a 2G connection that starved a fetch, a bug that only shows up after ten\n * minutes on the page or on the fifth error in a row.\n */\n\nconst pageLoadedAt = Date.now();\nlet errorCount = 0;\n\n/** Bumped by every auto-captured error so a report says which one in the run it was. */\nexport function incrementErrorCount(): number {\n errorCount += 1;\n return errorCount;\n}\n\n/** Test-only — reset the per-session counter. */\nexport function resetErrorCount(): void {\n errorCount = 0;\n}\n\ninterface MemoryCapableWindow {\n memory?: { usedJSHeapSize?: number; jsHeapSizeLimit?: number };\n}\n\ninterface NetworkInformation {\n effectiveType?: string;\n downlink?: number;\n rtt?: number;\n saveData?: boolean;\n}\n\nconst toMb = (bytes: number | undefined): number | undefined =>\n typeof bytes === \"number\" ? Math.round((bytes / 1048576) * 10) / 10 : undefined;\n\nexport function captureRuntimeInfo(): RuntimeInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n\n // Chromium-only, and absent entirely under cross-origin isolation.\n const memory = (performance as unknown as MemoryCapableWindow)?.memory;\n // Chromium + Android only.\n const connection = (navigator as unknown as { connection?: NetworkInformation })?.connection;\n\n return {\n timeOnPageMs: Date.now() - pageLoadedAt,\n errorCount,\n visibility: typeof document !== \"undefined\" ? document.visibilityState : \"unknown\",\n ...(memory\n ? {\n jsHeapUsedMb: toMb(memory.usedJSHeapSize),\n jsHeapLimitMb: toMb(memory.jsHeapSizeLimit),\n }\n : {}),\n ...(connection\n ? {\n connectionType: connection.effectiveType,\n downlinkMbps: connection.downlink,\n rttMs: connection.rtt,\n saveData: connection.saveData,\n }\n : {}),\n };\n } catch {\n return null;\n }\n}\n","import type {\n CapturedContext,\n DeviceInfo,\n FeedbackPayload,\n FeedbackResult,\n ReportPayload,\n ReportResult,\n} from \"./types\";\n// The React-free subpath: utils.ts is also bundled into `glitchgrab/server`.\nimport { assistOffline, toAssistTurnResult } from \"@glitchgrab/report-ui/assist-response\";\nimport type { AssistTurnResult } from \"@glitchgrab/report-ui\";\nimport { getBreadcrumbs } from \"./breadcrumbs\";\nimport { captureRuntimeInfo } from \"./runtime\";\nimport { getAppContext, getRelease } from \"./app-context\";\n\nconst SENSITIVE_PARAMS = [\n \"token\", \"key\", \"secret\", \"password\", \"passwd\", \"auth\", \"authorization\",\n \"session\", \"sessionid\", \"session_id\", \"api_key\", \"apikey\", \"access_token\",\n \"refresh_token\", \"client_secret\", \"code\", \"state\", \"nonce\", \"credential\", \"private\",\n];\n\nexport function sanitizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const params = new URLSearchParams(parsed.search);\n let modified = false;\n for (const key of Array.from(params.keys())) {\n if (SENSITIVE_PARAMS.some((s) => key.toLowerCase().includes(s))) {\n params.set(key, \"[REDACTED]\");\n modified = true;\n }\n }\n if (modified) parsed.search = params.toString();\n return parsed.toString();\n } catch {\n return url.split(\"?\")[0] ?? url;\n }\n}\n\nexport function captureDeviceInfo(): DeviceInfo | null {\n try {\n if (typeof window === \"undefined\") return null;\n return {\n screenWidth: screen.width,\n screenHeight: screen.height,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n platform: navigator.platform ?? \"unknown\",\n language: navigator.language ?? \"unknown\",\n online: navigator.onLine,\n colorScheme: window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\",\n devicePixelRatio: window.devicePixelRatio ?? 1,\n };\n } catch {\n return null;\n }\n}\n\nexport function captureContext(visitedPages: string[]): CapturedContext {\n try {\n return {\n url: sanitizeUrl(typeof window !== \"undefined\" ? window.location.href : \"\"),\n userAgent: typeof navigator !== \"undefined\" ? navigator.userAgent : \"\",\n timestamp: new Date().toISOString(),\n visitedPages: visitedPages.map(sanitizeUrl),\n breadcrumbs: getBreadcrumbs(),\n deviceInfo: captureDeviceInfo(),\n runtime: captureRuntimeInfo(),\n appContext: getAppContext(),\n release: getRelease(),\n };\n } catch {\n return {\n url: \"\",\n userAgent: \"\",\n timestamp: new Date().toISOString(),\n visitedPages: [],\n breadcrumbs: [],\n deviceInfo: null,\n runtime: null,\n appContext: {},\n };\n }\n}\n\n/**\n * Metadata every report carries regardless of how it was raised — release,\n * runtime health, and the host app's own context. One builder so a user-filed\n * report and an auto-captured crash describe the same world; the payload sites\n * only add what is specific to them.\n *\n * App context is prefixed so a key named `timestamp` or `status` can't quietly\n * overwrite a field the dashboard relies on.\n */\nexport function contextMetadata(context: CapturedContext): Record<string, string> {\n try {\n const runtime = context.runtime;\n const appContext = Object.entries(context.appContext ?? {}).reduce<Record<string, string>>(\n (acc, [key, value]) => {\n acc[`ctx_${key}`] = value;\n return acc;\n },\n {}\n );\n\n return {\n ...(context.release ? { release: context.release } : {}),\n ...(runtime\n ? {\n timeOnPageMs: String(runtime.timeOnPageMs),\n errorCount: String(runtime.errorCount),\n visibility: runtime.visibility,\n ...(runtime.jsHeapUsedMb !== undefined\n ? { jsHeapUsedMb: String(runtime.jsHeapUsedMb) }\n : {}),\n ...(runtime.jsHeapLimitMb !== undefined\n ? { jsHeapLimitMb: String(runtime.jsHeapLimitMb) }\n : {}),\n ...(runtime.connectionType ? { connectionType: runtime.connectionType } : {}),\n ...(runtime.downlinkMbps !== undefined\n ? { downlinkMbps: String(runtime.downlinkMbps) }\n : {}),\n ...(runtime.rttMs !== undefined ? { rttMs: String(runtime.rttMs) } : {}),\n ...(runtime.saveData !== undefined ? { saveData: String(runtime.saveData) } : {}),\n }\n : {}),\n ...appContext,\n };\n } catch {\n return {};\n }\n}\n\nexport const DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 500;\n\n/**\n * Send a report and return the result.\n * Never throws — returns null on failure.\n */\nexport async function sendReport(\n payload: ReportPayload,\n baseUrl?: string\n): Promise<ReportResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/report`;\n const body = JSON.stringify(payload);\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n };\n\n // keepalive has a 64KB body limit in browsers — skip it for large payloads (e.g., screenshots)\n const useKeepalive = body.length < 60_000;\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<ReportResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n // Fallback: sendBeacon (fire-and-forget, no result)\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n try {\n navigator.sendBeacon(url, new Blob([body], { type: \"application/json\" }));\n } catch {\n // Silently fail\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a star rating your end-user left about your app.\n * Never throws — returns null on failure.\n *\n * Unlike `sendReport`, this is always a deliberate user action with a dialog\n * open in front of them, so it retries on transient failure but skips the\n * sendBeacon fallback: the caller needs a real result to show a thank-you.\n */\nexport async function sendFeedback(\n payload: FeedbackPayload,\n baseUrl?: string\n): Promise<FeedbackResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/feedback`;\n const body = JSON.stringify(payload);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${payload.token}`,\n },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: Omit<FeedbackResult, \"success\">;\n };\n return { success: envelope.success, ...(envelope.data ?? {}) };\n }\n\n // 4xx other than rate limiting is a caller mistake — retrying won't help.\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n return null;\n }\n } catch {\n // Will retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await new Promise((resolve) =>\n setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt))\n );\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface EnhanceContext {\n url?: string;\n visitedPages?: string[];\n breadcrumbs?: Array<{ type: string; message: string }>;\n}\n\n/**\n * The signed-in user as the host's server vouched for them (`signSession`).\n *\n * Rides in the request BODY, never a header: every token route sets its own\n * CORS allow-list of headers, so a new header would fail preflight on all of\n * them. Omitted entirely when there is no signature — an unsigned id proves\n * nothing, and the server reads its absence as \"unsigned\".\n */\nexport interface SignedIdentity {\n userId?: string | null;\n signature?: string | null;\n}\n\n/** `{ userId, signature }` to spread into a JSON body, or nothing. */\nexport function signedFields(identity?: SignedIdentity | null): { userId?: string; signature?: string } {\n if (!identity?.signature || !identity.userId) return {};\n return { userId: identity.userId, signature: identity.signature };\n}\n\n/**\n * Polish raw description text via the Glitchgrab AI enhance endpoint.\n * Returns the polished text on success, or the original text on any failure.\n * Never throws.\n */\nexport async function enhanceText(\n text: string,\n token: string,\n baseUrl?: string,\n screenshot?: string | null,\n context?: EnhanceContext | null,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n const trimmed = text.trim();\n if (!trimmed) return text;\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/enhance-text`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n text: trimmed,\n ...(screenshot ? { screenshot } : {}),\n ...(context ? { context } : {}),\n ...signedFields(identity),\n }),\n });\n if (!response.ok) return text;\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { text?: string };\n };\n if (envelope?.success && typeof envelope.data?.text === \"string\") {\n return envelope.data.text;\n }\n return text;\n } catch {\n return text;\n }\n}\n\n/**\n * \"Is this already filed?\" for the report dialog's plain form. Asked when Send\n * is pressed; a match lets the reporter add to the open issue instead of\n * opening a second one.\n *\n * `[]` when the project has it off, `null` on any failure — the dialog sends\n * straight away on either. Never throws.\n */\nexport async function findSimilarIssues(\n description: string,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<{ number: number; title: string; url: string; status?: string }[] | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/reports/similar`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({ text: description, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; matches?: { number: number; title: string; url: string; status?: string }[] };\n } | null;\n if (!response.ok || !envelope?.success) return null;\n if (!envelope.data?.enabled) return [];\n return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];\n } catch {\n return null;\n }\n}\n\n/**\n * One turn of the AI report assistant (#330).\n *\n * Same never-throw contract as everything else in the SDK: a failure comes back\n * as `degraded` and the dialog quietly falls back to the plain form. An\n * end-user of the host app must never see a Glitchgrab error, and must never be\n * blocked from filing a bug because a model was busy.\n */\nexport async function assistTurn(\n params: {\n messages: { role: \"user\" | \"assistant\"; content: string }[];\n conversationId: string | null;\n screenshot?: string | null;\n context?: Record<string, unknown> | null;\n /**\n * Who the host app says is logged in, from the provider's `session` prop.\n * Stored against the conversation so a chat and the report it produced\n * agree on who filed — one SDK token covers every end user of the host app,\n * so without this every stranger's chat looks like the same person.\n */\n reporter?: {\n key?: string | null;\n name?: string | null;\n email?: string | null;\n /** `signSession()` output for `key`; absent when the host did not sign. */\n signature?: string | null;\n };\n },\n token: string,\n baseUrl?: string\n): Promise<AssistTurnResult> {\n const offline = assistOffline();\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/report-chat`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(params),\n });\n // One mapping for every host (packages/report-ui/src/assist-response.ts):\n // the server's degrade reason passes through, and nothing the sheet renders\n // is dropped.\n return toAssistTurnResult(response.ok, await response.json().catch(() => null));\n } catch {\n return offline;\n }\n}\n\n/**\n * Send an audio Blob to the Glitchgrab STT proxy (which calls Sarvam).\n * Returns the transcript string, or \"\" on any failure. Never throws.\n */\nexport async function transcribeAudio(\n blob: Blob,\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string> {\n try {\n if (blob.size === 0) return \"\";\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/stt`;\n const form = new FormData();\n form.append(\"file\", blob, \"audio.webm\");\n const signed = signedFields(identity);\n if (signed.userId && signed.signature) {\n form.append(\"userId\", signed.userId);\n form.append(\"signature\", signed.signature);\n }\n const response = await fetch(url, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: form,\n });\n if (!response.ok) return \"\";\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { transcript?: string };\n };\n if (envelope?.success && typeof envelope.data?.transcript === \"string\") {\n return envelope.data.transcript;\n }\n return \"\";\n } catch {\n return \"\";\n }\n}\n"],"mappings":"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","names":["_a"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "glitchgrab",
3
3
  "license": "SEE LICENSE IN LICENSE",
4
- "version": "1.51.0",
4
+ "version": "1.52.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",