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 +48 -0
- package/dist/server.d.mts +57 -2
- package/dist/server.d.ts +57 -2
- package/dist/server.js +71 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +68 -3
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
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}
|
|
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}
|
|
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: {
|
|
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:
|
|
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
|
package/dist/server.js.map
CHANGED
|
@@ -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: {
|
|
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:
|
|
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
|
package/dist/server.mjs.map
CHANGED
|
@@ -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"]}
|