glitchgrab 1.58.2 → 1.58.3
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 +4 -0
- package/dist/index.js +1453 -690
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1453 -690
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +11 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +7 -2
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/server.d.mts
CHANGED
|
@@ -147,6 +147,11 @@ interface GuideSummary {
|
|
|
147
147
|
orderIndex: number;
|
|
148
148
|
/** Always `"PUBLISHED"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */
|
|
149
149
|
status: "DRAFT" | "PUBLISHED";
|
|
150
|
+
/**
|
|
151
|
+
* `"INTERNAL"` = a team-only guide. Only the `ggc_` server key sees those,
|
|
152
|
+
* and only when it asks with `audience: "internal"` or `"all"`.
|
|
153
|
+
*/
|
|
154
|
+
audience: "PUBLIC" | "INTERNAL";
|
|
150
155
|
/** When someone last re-checked the steps against the product. */
|
|
151
156
|
lastVerifiedAt: string | null;
|
|
152
157
|
updatedAt: string;
|
|
@@ -192,6 +197,12 @@ interface GuideReadOptions {
|
|
|
192
197
|
revalidate?: number;
|
|
193
198
|
/** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */
|
|
194
199
|
timeoutMs?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Which guides: `"public"` (default), `"internal"` (team-only) or `"all"`.
|
|
202
|
+
* Anything but `"public"` needs the `ggc_` server key — for your own admin
|
|
203
|
+
* panel, behind your own login. Never on a page a customer can open.
|
|
204
|
+
*/
|
|
205
|
+
audience?: "public" | "internal" | "all";
|
|
195
206
|
}
|
|
196
207
|
/**
|
|
197
208
|
* Every published guide for the token's repo, in the order set in Glitchgrab.
|
package/dist/server.d.ts
CHANGED
|
@@ -147,6 +147,11 @@ interface GuideSummary {
|
|
|
147
147
|
orderIndex: number;
|
|
148
148
|
/** Always `"PUBLISHED"` with a `gg_` token; only the project's `ggc_` server key sees drafts. */
|
|
149
149
|
status: "DRAFT" | "PUBLISHED";
|
|
150
|
+
/**
|
|
151
|
+
* `"INTERNAL"` = a team-only guide. Only the `ggc_` server key sees those,
|
|
152
|
+
* and only when it asks with `audience: "internal"` or `"all"`.
|
|
153
|
+
*/
|
|
154
|
+
audience: "PUBLIC" | "INTERNAL";
|
|
150
155
|
/** When someone last re-checked the steps against the product. */
|
|
151
156
|
lastVerifiedAt: string | null;
|
|
152
157
|
updatedAt: string;
|
|
@@ -192,6 +197,12 @@ interface GuideReadOptions {
|
|
|
192
197
|
revalidate?: number;
|
|
193
198
|
/** Give up and return empty after this many ms. Default 5000 — a slow Glitchgrab must not hang your render. */
|
|
194
199
|
timeoutMs?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Which guides: `"public"` (default), `"internal"` (team-only) or `"all"`.
|
|
202
|
+
* Anything but `"public"` needs the `ggc_` server key — for your own admin
|
|
203
|
+
* panel, behind your own login. Never on a page a customer can open.
|
|
204
|
+
*/
|
|
205
|
+
audience?: "public" | "internal" | "all";
|
|
195
206
|
}
|
|
196
207
|
/**
|
|
197
208
|
* Every published guide for the token's repo, in the order set in Glitchgrab.
|
package/dist/server.js
CHANGED
|
@@ -341,14 +341,19 @@ function readGuideSummary(value) {
|
|
|
341
341
|
category: typeof guide.category === "string" ? guide.category : null,
|
|
342
342
|
orderIndex: typeof guide.orderIndex === "number" ? guide.orderIndex : 0,
|
|
343
343
|
status: guide.status === "DRAFT" ? "DRAFT" : "PUBLISHED",
|
|
344
|
+
audience: guide.audience === "INTERNAL" ? "INTERNAL" : "PUBLIC",
|
|
344
345
|
lastVerifiedAt: typeof guide.lastVerifiedAt === "string" ? guide.lastVerifiedAt : null,
|
|
345
346
|
updatedAt: typeof guide.updatedAt === "string" ? guide.updatedAt : "",
|
|
346
347
|
link: typeof guide.link === "string" ? guide.link : null
|
|
347
348
|
};
|
|
348
349
|
}
|
|
350
|
+
function audienceQuery(options, joiner) {
|
|
351
|
+
const audience = options.audience;
|
|
352
|
+
return audience === "internal" || audience === "all" ? `${joiner}audience=${audience}` : "";
|
|
353
|
+
}
|
|
349
354
|
async function listGuides(options = {}) {
|
|
350
355
|
try {
|
|
351
|
-
const data = await readGuidesApi("", options);
|
|
356
|
+
const data = await readGuidesApi(audienceQuery(options, "?"), options);
|
|
352
357
|
if (!Array.isArray(data)) return [];
|
|
353
358
|
return data.map(readGuideSummary).filter((guide) => guide !== null);
|
|
354
359
|
} catch (e) {
|
|
@@ -359,7 +364,7 @@ async function getGuide(slug, options = {}) {
|
|
|
359
364
|
try {
|
|
360
365
|
const wanted = typeof slug === "string" ? slug.trim() : "";
|
|
361
366
|
if (!wanted) return null;
|
|
362
|
-
const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}`, options);
|
|
367
|
+
const data = await readGuidesApi(`?slug=${encodeURIComponent(wanted)}${audienceQuery(options, "&")}`, options);
|
|
363
368
|
const summary = readGuideSummary(data);
|
|
364
369
|
const body = data;
|
|
365
370
|
if (!summary || typeof (body == null ? void 0 : body.html) !== "string") return null;
|
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 * What the job printed before it failed — lines or one string.\n *\n * Attached to the issue as a `.log` file: the first part is previewed in the\n * issue body, the whole file is kept for download. A stack says where it\n * broke; the log lines before it are usually what says why. Only the last\n * {@link MAX_LOG_CHARS} characters are sent.\n */\n logs?: string | string[];\n}\n\n/** The newest end of a log is the useful end. Well under the API's per-file cap. */\nconst MAX_LOG_CHARS = 200_000;\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\n/**\n * `logs` as the API's text-attachment shape: `[{ name, dataUrl }]`, JSON in\n * `metadata.attachments` — the same slot the report dialog uses for files, so\n * the endpoint needs nothing new. Null when there is nothing to attach.\n */\nfunction logsAttachment(logs: string | string[] | undefined, context: string | undefined): string | null {\n try {\n const text = Array.isArray(logs) ? logs.join(\"\\n\") : logs;\n if (!text || !text.trim()) return null;\n const tail = text.length > MAX_LOG_CHARS ? text.slice(-MAX_LOG_CHARS) : text;\n\n // UTF-8 → base64 without Buffer: this entry stays free of Node typings.\n const bytes = new TextEncoder().encode(tail);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 0x8000) {\n binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));\n }\n const name = `${(context ?? \"server\").replace(/[^a-z0-9._-]+/gi, \"-\").replace(/^-+|-+$/g, \"\") || \"server\"}.log`;\n return JSON.stringify([{ name, dataUrl: `data:text/plain;base64,${btoa(binary)}` }]);\n } catch {\n return null;\n }\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 const attachments = logsAttachment(options.logs, context);\n if (attachments && payload.metadata) payload.metadata.attachments = attachments;\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\" | \"PATCH\" | \"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 /** Who the visitor meets, shown beside the calendar in the booking dialog. */\n hostName: string | null;\n /** https URL of the host's photo. Anything that is not https is ignored on save. */\n hostAvatarUrl: 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 * Who else is WhatsApped when a demo is booked, besides the project owner —\n * sales, the founders, whoever needs the lead as it lands. At most 5; a\n * ten-digit Indian mobile is stored with its 91. Send the whole list to\n * change it; leave the field out and it keeps what is stored.\n */\n notifyRecipients: BookingNotifyRecipient[];\n /**\n * What a prospect is asked before they see the calendar — in the booking\n * dialog and over WhatsApp — so the right person takes the demo. At most 5.\n * Send the whole list to change it (`[]` asks nothing); leave the field out\n * and it keeps what is stored. The answers come back on {@link Booking.answers}.\n */\n questions: BookingQuestion[];\n /**\n * Questions saved as a draft and not live yet — the dialog and the WhatsApp\n * chat still ask {@link questions}. Preview them with\n * `useGlitchgrab().openBookingPreview()`, make them live with\n * {@link publishBookingQuestions}. `null` = no draft; `[]` = a draft that\n * asks nothing.\n */\n draftQuestions: BookingQuestion[] | null;\n /**\n * Text a code to the prospect's phone before the demo is confirmed. `false`\n * books the slot the moment the form is sent — one step fewer, but a\n * mistyped number still books. Defaults to `true`.\n */\n requireOtp: boolean;\n}\n\n/** One question asked before a demo is booked. The prospect taps one of `options`. */\nexport interface BookingQuestion {\n /**\n * Stable name for the question, e.g. `\"language\"` — lower case letters,\n * digits, `-` and `_`. Leave it out on a new question and one is made from\n * the label; keep it when rewording so earlier answers still line up.\n */\n id: string;\n /** What is asked, up to 80 characters. */\n label: string;\n /** 2–9 choices, up to 40 characters each. */\n options: string[];\n /** False lets the prospect skip it. */\n required: boolean;\n}\n\n/** A question as you send it: `id` and `required` may be left out on a new one. */\nexport type BookingQuestionInput = Omit<BookingQuestion, \"id\" | \"required\"> & { id?: string; required?: boolean };\n\n/** What {@link updateBookingSettings} accepts — any subset of the settings. */\nexport type BookingSettingsChanges = Partial<Omit<BookingSettings, \"questions\" | \"draftQuestions\">> & {\n questions?: BookingQuestionInput[];\n /**\n * With `questions`: save them as a draft instead — the live dialog and chat\n * are unchanged until {@link publishBookingQuestions}. Left out, `questions`\n * go live at once (and any draft is dropped), as before.\n */\n draft?: boolean;\n};\n\n/** One answer a prospect gave, with the question as it was worded when they booked. */\nexport interface BookingAnswer {\n /** The {@link BookingQuestion.id} it answers. */\n id: string;\n question: string;\n answer: string;\n}\n\n/** One extra person alerted when a demo is booked. */\nexport interface BookingNotifyRecipient {\n name: string;\n /** Digits only, country code included. */\n phone: string;\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\n/** A stored question list, or null when the value is not a list at all. */\nfunction readQuestionList(value: unknown): BookingQuestion[] | null {\n if (!Array.isArray(value)) return null;\n return value\n .filter(isRecord)\n .map((row) => ({\n id: stringOrNull(row.id) ?? \"\",\n label: stringOrNull(row.label) ?? \"\",\n options: Array.isArray(row.options)\n ? row.options.filter((option): option is string => typeof option === \"string\")\n : [],\n required: row.required !== false,\n }))\n .filter((row) => row.id !== \"\" && row.label !== \"\");\n}\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 hostName: stringOrNull(s.hostName),\n hostAvatarUrl: stringOrNull(s.hostAvatarUrl),\n horizonDays: numberOrNull(s.horizonDays) ?? 15,\n noticeMinutes: numberOrNull(s.noticeMinutes) ?? 120,\n whatsappCode: stringOrNull(s.whatsappCode),\n notifyRecipients: Array.isArray(s.notifyRecipients)\n ? s.notifyRecipients\n .filter(isRecord)\n .map((row) => ({\n name: stringOrNull(row.name) ?? \"\",\n phone: stringOrNull(row.phone) ?? \"\",\n }))\n .filter((row) => row.phone !== \"\")\n : [],\n questions: readQuestionList(s.questions) ?? [],\n // A server from before drafts sends no key: no draft.\n draftQuestions: readQuestionList(s.draftQuestions),\n // Missing means a server from before the switch, which always asked.\n requireOtp: s.requireOtp !== false,\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 function updateBookingSettings(\n changes: BookingSettingsChanges,\n options: CallsOptions = {}\n): Promise<BookingSettingsResult> {\n return putBookingSettings(changes ?? {}, options);\n}\n\nasync function putBookingSettings(\n body: BookingSettingsChanges | { publishDraft: true } | { discardDraft: true },\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,\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 * Make the draft questions live — the booking dialog and the WhatsApp chat ask\n * them from now on. `error` when there is no draft to publish.\n *\n * ```ts\n * await updateBookingSettings({ questions, draft: true }); // live dialog unchanged\n * // …preview with useGlitchgrab().openBookingPreview() on your admin page…\n * await publishBookingQuestions();\n * ```\n */\nexport function publishBookingQuestions(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n return putBookingSettings({ publishDraft: true }, options);\n}\n\n/** Throw the draft questions away; the live ones are untouched. */\nexport function discardBookingQuestionsDraft(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n return putBookingSettings({ discardDraft: true }, options);\n}\n\n// ── Demo bookings ────────────────────────────────────────────────────────────\n//\n// The demos prospects booked on this project, so the team's own admin panel can\n// show them, move them and call them off without a Glitchgrab login. Same\n// server key as calls. Moving one patches the real Google event, so the Meet\n// link every earlier message already carries keeps working.\n\n/** One booked demo. Times are ISO strings in UTC; `timezone` is what the prospect picked in. */\nexport interface Booking {\n id: string;\n /** Who booked. */\n name: string;\n email: string;\n /** Digits only, country code included. */\n phone: string;\n startsAt: string;\n endsAt: string;\n /** IANA zone the prospect booked in, e.g. `\"Asia/Kolkata\"`. */\n timezone: string | null;\n /** What they typed when booking, if anything. */\n note: string | null;\n /** What they answered to the project's {@link BookingSettings.questions}. Empty when nothing was asked. */\n answers: BookingAnswer[];\n /** `CONFIRMED` or `CANCELLED`. Unverified holds are never returned. */\n status: string;\n /** The Google Meet room. Null until the booking is confirmed. */\n meetUrl: string | null;\n createdAt: string;\n}\n\nexport interface ListBookingsOptions extends CallsOptions {\n /** `\"upcoming\"` (default) or `\"past\"`. */\n scope?: \"upcoming\" | \"past\";\n /** Up to 100. Defaults to 50. */\n limit?: number;\n}\n\n/**\n * Whether the prospect was told. A move re-sends the approved confirmation\n * template, so `\"sent\"` is real. A cancellation can only go as free text, which\n * WhatsApp delivers only inside its 24-hour window and reports nothing about —\n * `\"attempted\"` means exactly that, and your panel should say so rather than\n * telling an admin the client knows.\n */\nexport type BookingClientNotice = \"sent\" | \"attempted\" | \"none\";\n\nexport type BookingMutationResult =\n | { ok: true; booking: Booking | null; clientNotice: BookingClientNotice }\n | { ok: false; error: string; code: string | null };\n\nfunction readBooking(value: unknown): Booking | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n name: stringOrNull(value.name) ?? \"\",\n email: stringOrNull(value.email) ?? \"\",\n phone: stringOrNull(value.phone) ?? \"\",\n startsAt: stringOrNull(value.startsAt) ?? \"\",\n endsAt: stringOrNull(value.endsAt) ?? \"\",\n timezone: stringOrNull(value.timezone),\n note: stringOrNull(value.note),\n answers: Array.isArray(value.answers)\n ? value.answers\n .filter(isRecord)\n .map((row) => ({\n id: stringOrNull(row.id) ?? \"\",\n question: stringOrNull(row.question) ?? \"\",\n answer: stringOrNull(row.answer) ?? \"\",\n }))\n .filter((row) => row.answer !== \"\")\n : [],\n status: stringOrNull(value.status) ?? \"CONFIRMED\",\n meetUrl: stringOrNull(value.meetUrl),\n createdAt: stringOrNull(value.createdAt) ?? \"\",\n };\n}\n\n/**\n * The project's booked demos, soonest first. Never throws — an empty list on\n * any failure, so a panel renders \"no demos\" instead of a stack trace.\n *\n * ```ts\n * const demos = await listBookings(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * ```\n */\nexport async function listBookings(options: ListBookingsOptions = {}): Promise<Booking[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n if (options.scope === \"past\") query.set(\"scope\", \"past\");\n setNumber(query, \"limit\", options.limit);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/bookings${qs ? `?${qs}` : \"\"}`, key, options, {\n cache: \"no-store\",\n });\n if (!Array.isArray(data)) return [];\n return data.map(readBooking).filter((booking): booking is Booking => booking !== null);\n } catch {\n return [];\n }\n}\n\nfunction readMutation(data: unknown, error: string | null): BookingMutationResult {\n if (error !== null) return { ok: false, error, code: null };\n const record = isRecord(data) ? data : {};\n const notice = stringOrNull(record.clientNotice);\n return {\n ok: true,\n booking: readBooking(record.booking),\n clientNotice: notice === \"sent\" || notice === \"attempted\" ? notice : \"none\",\n };\n}\n\n/**\n * Move a demo to a new start time. The Google event is patched, never\n * recreated, so the Meet link in every message already sent stays alive, and\n * the prospect is re-sent the confirmation with the new time.\n *\n * `{ ok: false, code: \"SLOT_TAKEN\" }` when another booking holds that slot,\n * `\"PAST\"` for a time that has gone, `\"CALENDAR\"` when Google refused — in the\n * last case nothing was changed and the demo still stands at its old time.\n */\nexport async function rescheduleBooking(\n id: string,\n startsAt: Date | string,\n options: CallsOptions = {}\n): Promise<BookingMutationResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted) return { ok: false, error: \"No booking id\", code: null };\n if (!key) return { ok: false, error: NO_KEY_ERROR, code: null };\n\n const when = startsAt instanceof Date ? startsAt.toISOString() : String(startsAt);\n const { data, error } = await callSdkApi(\n `/api/v1/sdk/bookings?id=${encodeURIComponent(wanted)}`,\n key,\n options,\n { method: \"PATCH\", body: { startsAt: when }, cache: \"no-store\" }\n );\n return readMutation(data, error);\n } catch {\n return { ok: false, error: \"Could not move that demo\", code: null };\n }\n}\n\n/**\n * Call a demo off: the Google event is deleted, the recorder is stood down, and\n * the prospect is messaged if WhatsApp will carry it (see\n * {@link BookingClientNotice}).\n *\n * `reason` is included in that message. Nothing is marked cancelled unless the\n * calendar agreed first, so `{ ok: false, code: \"CALENDAR\" }` means the demo is\n * still on.\n */\nexport async function cancelBooking(\n id: string,\n reason?: string,\n options: CallsOptions = {}\n): Promise<BookingMutationResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted) return { ok: false, error: \"No booking id\", code: null };\n if (!key) return { ok: false, error: NO_KEY_ERROR, code: null };\n\n const query = new URLSearchParams({ id: wanted });\n if (typeof reason === \"string\" && reason.trim()) query.set(\"reason\", reason.trim().slice(0, 200));\n\n const { data, error } = await callSdkApi(`/api/v1/sdk/bookings?${query.toString()}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return readMutation(data, error);\n } catch {\n return { ok: false, error: \"Could not cancel that demo\", code: null };\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\n// ── Leads ────────────────────────────────────────────────────────────────────\n//\n// Contact details prospects left with `useGlitchgrab().submitLead()` (#534) —\n// the demo form, the contact page — so your own admin can show them. Same\n// server key as calls and bookings. A lead never becomes a GitHub issue.\n\n/** One lead. One per phone number per project; a repeat submission updates it. */\nexport interface Lead {\n id: string;\n name: string;\n /** Digits only, country code included. */\n phone: string;\n email: string | null;\n company: string | null;\n address: string | null;\n /** What your form passed as `source`, e.g. `\"demo\"`. */\n source: string;\n metadata: Record<string, string | number | boolean> | null;\n /** The page the lead was left on. */\n pageUrl: string | null;\n /** How many times this phone submitted. */\n submissions: number;\n /** First submission. */\n createdAt: string;\n /** Latest submission. */\n updatedAt: string;\n /** The demo this lead went on to book, matched by phone or email. */\n booking: { id: string; startsAt: string; status: string } | null;\n}\n\nexport interface ListLeadsOptions extends CallsOptions {\n /** Only leads with this `source`. */\n source?: string;\n /** Only leads first left at or after this time. */\n since?: Date | string;\n /** Up to 200. Defaults to 100. */\n limit?: number;\n}\n\nfunction readLead(value: unknown): Lead | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n const metadata = isRecord(value.metadata) ? (value.metadata as Lead[\"metadata\"]) : null;\n const booking = isRecord(value.booking) && typeof value.booking.id === \"string\"\n ? {\n id: value.booking.id,\n startsAt: stringOrNull(value.booking.startsAt) ?? \"\",\n status: stringOrNull(value.booking.status) ?? \"CONFIRMED\",\n }\n : null;\n return {\n id: value.id,\n name: stringOrNull(value.name) ?? \"\",\n phone: stringOrNull(value.phone) ?? \"\",\n email: stringOrNull(value.email),\n company: stringOrNull(value.company),\n address: stringOrNull(value.address),\n source: stringOrNull(value.source) ?? \"\",\n metadata,\n pageUrl: stringOrNull(value.pageUrl),\n submissions: typeof value.submissions === \"number\" ? value.submissions : 1,\n createdAt: stringOrNull(value.createdAt) ?? \"\",\n updatedAt: stringOrNull(value.updatedAt) ?? \"\",\n booking,\n };\n}\n\n/**\n * The project's leads, newest first. Never throws — an empty list on any\n * failure.\n *\n * ```ts\n * const leads = await listLeads({ source: \"demo\" }); // GLITCHGRAB_CALLS_KEY=ggc_…\n * ```\n */\nexport async function listLeads(options: ListLeadsOptions = {}): Promise<Lead[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n if (options.source) query.set(\"source\", options.source);\n if (options.since) {\n const since = options.since instanceof Date ? options.since : new Date(options.since);\n if (!Number.isNaN(since.getTime())) query.set(\"since\", since.toISOString());\n }\n setNumber(query, \"limit\", options.limit);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/leads${qs ? `?${qs}` : \"\"}`, key, options, {\n cache: \"no-store\",\n });\n if (!Array.isArray(data)) return [];\n return data.map(readLead).filter((lead): lead is Lead => lead !== null);\n } catch {\n return [];\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 LeadInput,\n LeadResult,\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 * How long one report attempt may take before it is abandoned and retried.\n * The server files a report in a few seconds; on a flaky mobile network the\n * response can get lost and fetch then hangs ~30s before failing (#557). The\n * body share assumes a slow ~100 KB/s uplink, so a large screenshot is never\n * cut off mid-upload. A retry of a report the server already has is answered\n * with that report, not filed again. The final attempt runs without one.\n */\nexport function reportAttemptTimeoutMs(bodyLength: number): number {\n return 10_000 + Math.ceil(bodyLength / 100_000) * 1_000;\n}\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 const timeoutMs = reportAttemptTimeoutMs(body.length);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n // The last attempt has no deadline: a slow uplink still gets there, and\n // sendBeacon can't carry a screenshot-sized body if it gave up.\n const isLast = attempt === MAX_RETRIES - 1;\n const controller = !isLast && typeof AbortController === \"function\" ? new AbortController() : null;\n const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n ...(controller ? { signal: controller.signal } : {}),\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 } finally {\n if (timer) clearTimeout(timer);\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\n/**\n * POST a lead (#534). Retries transient failures like {@link sendFeedback}; a\n * 4xx other than 429 is a caller mistake and returns null at once.\n */\nexport async function sendLead(\n token: string,\n lead: LeadInput & { pageUrl?: string },\n baseUrl?: string\n): Promise<LeadResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/leads`;\n const body = JSON.stringify(lead);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}` },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { leadId?: string; created?: boolean };\n };\n return { success: envelope.success, id: envelope.data?.leadId, created: envelope.data?.created };\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) => setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt)));\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 /** Attached files as text (#563) — the server reads a short summary of them. */\n files?: { name: string; content: string }[]\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 // No `keepalive`: with files the body can pass its 64 KB cap, and the\n // request would fail without a word.\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: description,\n ...(files?.length ? { files } : {}),\n ...signedFields(identity),\n }),\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 * The text inside a PDF the reporter attached (#563), so the assistant and the\n * duplicate check can read it. Called once per PDF, when it is attached.\n *\n * `\"\"` for a PDF with no text layer (a scan); `null` when the project has the\n * assistant off, the file is too big, or anything failed. Never throws.\n */\nexport async function readAttachment(\n file: { name: string; dataUrl: string },\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/read-file`;\n // No `keepalive`: a PDF is far past its 64 KB body cap.\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({ name: file.name, data: file.dataUrl, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; text?: string };\n } | null;\n if (!response.ok || !envelope?.success || !envelope.data?.enabled) return null;\n return typeof envelope.data.text === \"string\" ? envelope.data.text : null;\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","_b","_c"]}
|
|
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 * What the job printed before it failed — lines or one string.\n *\n * Attached to the issue as a `.log` file: the first part is previewed in the\n * issue body, the whole file is kept for download. A stack says where it\n * broke; the log lines before it are usually what says why. Only the last\n * {@link MAX_LOG_CHARS} characters are sent.\n */\n logs?: string | string[];\n}\n\n/** The newest end of a log is the useful end. Well under the API's per-file cap. */\nconst MAX_LOG_CHARS = 200_000;\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\n/**\n * `logs` as the API's text-attachment shape: `[{ name, dataUrl }]`, JSON in\n * `metadata.attachments` — the same slot the report dialog uses for files, so\n * the endpoint needs nothing new. Null when there is nothing to attach.\n */\nfunction logsAttachment(logs: string | string[] | undefined, context: string | undefined): string | null {\n try {\n const text = Array.isArray(logs) ? logs.join(\"\\n\") : logs;\n if (!text || !text.trim()) return null;\n const tail = text.length > MAX_LOG_CHARS ? text.slice(-MAX_LOG_CHARS) : text;\n\n // UTF-8 → base64 without Buffer: this entry stays free of Node typings.\n const bytes = new TextEncoder().encode(tail);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 0x8000) {\n binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));\n }\n const name = `${(context ?? \"server\").replace(/[^a-z0-9._-]+/gi, \"-\").replace(/^-+|-+$/g, \"\") || \"server\"}.log`;\n return JSON.stringify([{ name, dataUrl: `data:text/plain;base64,${btoa(binary)}` }]);\n } catch {\n return null;\n }\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 const attachments = logsAttachment(options.logs, context);\n if (attachments && payload.metadata) payload.metadata.attachments = attachments;\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 /**\n * `\"INTERNAL\"` = a team-only guide. Only the `ggc_` server key sees those,\n * and only when it asks with `audience: \"internal\"` or `\"all\"`.\n */\n audience: \"PUBLIC\" | \"INTERNAL\";\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 * Which guides: `\"public\"` (default), `\"internal\"` (team-only) or `\"all\"`.\n * Anything but `\"public\"` needs the `ggc_` server key — for your own admin\n * panel, behind your own login. Never on a page a customer can open.\n */\n audience?: \"public\" | \"internal\" | \"all\";\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\" | \"PATCH\" | \"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 audience: guide.audience === \"INTERNAL\" ? \"INTERNAL\" : \"PUBLIC\",\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/** `audience=` for the guides route; nothing for the default, so old servers see the same URL. */\nfunction audienceQuery(options: GuideReadOptions, joiner: \"?\" | \"&\"): string {\n const audience = options.audience;\n return audience === \"internal\" || audience === \"all\" ? `${joiner}audience=${audience}` : \"\";\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(audienceQuery(options, \"?\"), 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)}${audienceQuery(options, \"&\")}`, 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 /** Who the visitor meets, shown beside the calendar in the booking dialog. */\n hostName: string | null;\n /** https URL of the host's photo. Anything that is not https is ignored on save. */\n hostAvatarUrl: 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 * Who else is WhatsApped when a demo is booked, besides the project owner —\n * sales, the founders, whoever needs the lead as it lands. At most 5; a\n * ten-digit Indian mobile is stored with its 91. Send the whole list to\n * change it; leave the field out and it keeps what is stored.\n */\n notifyRecipients: BookingNotifyRecipient[];\n /**\n * What a prospect is asked before they see the calendar — in the booking\n * dialog and over WhatsApp — so the right person takes the demo. At most 5.\n * Send the whole list to change it (`[]` asks nothing); leave the field out\n * and it keeps what is stored. The answers come back on {@link Booking.answers}.\n */\n questions: BookingQuestion[];\n /**\n * Questions saved as a draft and not live yet — the dialog and the WhatsApp\n * chat still ask {@link questions}. Preview them with\n * `useGlitchgrab().openBookingPreview()`, make them live with\n * {@link publishBookingQuestions}. `null` = no draft; `[]` = a draft that\n * asks nothing.\n */\n draftQuestions: BookingQuestion[] | null;\n /**\n * Text a code to the prospect's phone before the demo is confirmed. `false`\n * books the slot the moment the form is sent — one step fewer, but a\n * mistyped number still books. Defaults to `true`.\n */\n requireOtp: boolean;\n}\n\n/** One question asked before a demo is booked. The prospect taps one of `options`. */\nexport interface BookingQuestion {\n /**\n * Stable name for the question, e.g. `\"language\"` — lower case letters,\n * digits, `-` and `_`. Leave it out on a new question and one is made from\n * the label; keep it when rewording so earlier answers still line up.\n */\n id: string;\n /** What is asked, up to 80 characters. */\n label: string;\n /** 2–9 choices, up to 40 characters each. */\n options: string[];\n /** False lets the prospect skip it. */\n required: boolean;\n}\n\n/** A question as you send it: `id` and `required` may be left out on a new one. */\nexport type BookingQuestionInput = Omit<BookingQuestion, \"id\" | \"required\"> & { id?: string; required?: boolean };\n\n/** What {@link updateBookingSettings} accepts — any subset of the settings. */\nexport type BookingSettingsChanges = Partial<Omit<BookingSettings, \"questions\" | \"draftQuestions\">> & {\n questions?: BookingQuestionInput[];\n /**\n * With `questions`: save them as a draft instead — the live dialog and chat\n * are unchanged until {@link publishBookingQuestions}. Left out, `questions`\n * go live at once (and any draft is dropped), as before.\n */\n draft?: boolean;\n};\n\n/** One answer a prospect gave, with the question as it was worded when they booked. */\nexport interface BookingAnswer {\n /** The {@link BookingQuestion.id} it answers. */\n id: string;\n question: string;\n answer: string;\n}\n\n/** One extra person alerted when a demo is booked. */\nexport interface BookingNotifyRecipient {\n name: string;\n /** Digits only, country code included. */\n phone: string;\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\n/** A stored question list, or null when the value is not a list at all. */\nfunction readQuestionList(value: unknown): BookingQuestion[] | null {\n if (!Array.isArray(value)) return null;\n return value\n .filter(isRecord)\n .map((row) => ({\n id: stringOrNull(row.id) ?? \"\",\n label: stringOrNull(row.label) ?? \"\",\n options: Array.isArray(row.options)\n ? row.options.filter((option): option is string => typeof option === \"string\")\n : [],\n required: row.required !== false,\n }))\n .filter((row) => row.id !== \"\" && row.label !== \"\");\n}\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 hostName: stringOrNull(s.hostName),\n hostAvatarUrl: stringOrNull(s.hostAvatarUrl),\n horizonDays: numberOrNull(s.horizonDays) ?? 15,\n noticeMinutes: numberOrNull(s.noticeMinutes) ?? 120,\n whatsappCode: stringOrNull(s.whatsappCode),\n notifyRecipients: Array.isArray(s.notifyRecipients)\n ? s.notifyRecipients\n .filter(isRecord)\n .map((row) => ({\n name: stringOrNull(row.name) ?? \"\",\n phone: stringOrNull(row.phone) ?? \"\",\n }))\n .filter((row) => row.phone !== \"\")\n : [],\n questions: readQuestionList(s.questions) ?? [],\n // A server from before drafts sends no key: no draft.\n draftQuestions: readQuestionList(s.draftQuestions),\n // Missing means a server from before the switch, which always asked.\n requireOtp: s.requireOtp !== false,\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 function updateBookingSettings(\n changes: BookingSettingsChanges,\n options: CallsOptions = {}\n): Promise<BookingSettingsResult> {\n return putBookingSettings(changes ?? {}, options);\n}\n\nasync function putBookingSettings(\n body: BookingSettingsChanges | { publishDraft: true } | { discardDraft: true },\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,\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 * Make the draft questions live — the booking dialog and the WhatsApp chat ask\n * them from now on. `error` when there is no draft to publish.\n *\n * ```ts\n * await updateBookingSettings({ questions, draft: true }); // live dialog unchanged\n * // …preview with useGlitchgrab().openBookingPreview() on your admin page…\n * await publishBookingQuestions();\n * ```\n */\nexport function publishBookingQuestions(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n return putBookingSettings({ publishDraft: true }, options);\n}\n\n/** Throw the draft questions away; the live ones are untouched. */\nexport function discardBookingQuestionsDraft(options: CallsOptions = {}): Promise<BookingSettingsResult> {\n return putBookingSettings({ discardDraft: true }, options);\n}\n\n// ── Demo bookings ────────────────────────────────────────────────────────────\n//\n// The demos prospects booked on this project, so the team's own admin panel can\n// show them, move them and call them off without a Glitchgrab login. Same\n// server key as calls. Moving one patches the real Google event, so the Meet\n// link every earlier message already carries keeps working.\n\n/** One booked demo. Times are ISO strings in UTC; `timezone` is what the prospect picked in. */\nexport interface Booking {\n id: string;\n /** Who booked. */\n name: string;\n email: string;\n /** Digits only, country code included. */\n phone: string;\n startsAt: string;\n endsAt: string;\n /** IANA zone the prospect booked in, e.g. `\"Asia/Kolkata\"`. */\n timezone: string | null;\n /** What they typed when booking, if anything. */\n note: string | null;\n /** What they answered to the project's {@link BookingSettings.questions}. Empty when nothing was asked. */\n answers: BookingAnswer[];\n /** `CONFIRMED` or `CANCELLED`. Unverified holds are never returned. */\n status: string;\n /** The Google Meet room. Null until the booking is confirmed. */\n meetUrl: string | null;\n createdAt: string;\n}\n\nexport interface ListBookingsOptions extends CallsOptions {\n /** `\"upcoming\"` (default) or `\"past\"`. */\n scope?: \"upcoming\" | \"past\";\n /** Up to 100. Defaults to 50. */\n limit?: number;\n}\n\n/**\n * Whether the prospect was told. A move re-sends the approved confirmation\n * template, so `\"sent\"` is real. A cancellation can only go as free text, which\n * WhatsApp delivers only inside its 24-hour window and reports nothing about —\n * `\"attempted\"` means exactly that, and your panel should say so rather than\n * telling an admin the client knows.\n */\nexport type BookingClientNotice = \"sent\" | \"attempted\" | \"none\";\n\nexport type BookingMutationResult =\n | { ok: true; booking: Booking | null; clientNotice: BookingClientNotice }\n | { ok: false; error: string; code: string | null };\n\nfunction readBooking(value: unknown): Booking | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n return {\n id: value.id,\n name: stringOrNull(value.name) ?? \"\",\n email: stringOrNull(value.email) ?? \"\",\n phone: stringOrNull(value.phone) ?? \"\",\n startsAt: stringOrNull(value.startsAt) ?? \"\",\n endsAt: stringOrNull(value.endsAt) ?? \"\",\n timezone: stringOrNull(value.timezone),\n note: stringOrNull(value.note),\n answers: Array.isArray(value.answers)\n ? value.answers\n .filter(isRecord)\n .map((row) => ({\n id: stringOrNull(row.id) ?? \"\",\n question: stringOrNull(row.question) ?? \"\",\n answer: stringOrNull(row.answer) ?? \"\",\n }))\n .filter((row) => row.answer !== \"\")\n : [],\n status: stringOrNull(value.status) ?? \"CONFIRMED\",\n meetUrl: stringOrNull(value.meetUrl),\n createdAt: stringOrNull(value.createdAt) ?? \"\",\n };\n}\n\n/**\n * The project's booked demos, soonest first. Never throws — an empty list on\n * any failure, so a panel renders \"no demos\" instead of a stack trace.\n *\n * ```ts\n * const demos = await listBookings(); // GLITCHGRAB_CALLS_KEY=ggc_…\n * ```\n */\nexport async function listBookings(options: ListBookingsOptions = {}): Promise<Booking[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n if (options.scope === \"past\") query.set(\"scope\", \"past\");\n setNumber(query, \"limit\", options.limit);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/bookings${qs ? `?${qs}` : \"\"}`, key, options, {\n cache: \"no-store\",\n });\n if (!Array.isArray(data)) return [];\n return data.map(readBooking).filter((booking): booking is Booking => booking !== null);\n } catch {\n return [];\n }\n}\n\nfunction readMutation(data: unknown, error: string | null): BookingMutationResult {\n if (error !== null) return { ok: false, error, code: null };\n const record = isRecord(data) ? data : {};\n const notice = stringOrNull(record.clientNotice);\n return {\n ok: true,\n booking: readBooking(record.booking),\n clientNotice: notice === \"sent\" || notice === \"attempted\" ? notice : \"none\",\n };\n}\n\n/**\n * Move a demo to a new start time. The Google event is patched, never\n * recreated, so the Meet link in every message already sent stays alive, and\n * the prospect is re-sent the confirmation with the new time.\n *\n * `{ ok: false, code: \"SLOT_TAKEN\" }` when another booking holds that slot,\n * `\"PAST\"` for a time that has gone, `\"CALENDAR\"` when Google refused — in the\n * last case nothing was changed and the demo still stands at its old time.\n */\nexport async function rescheduleBooking(\n id: string,\n startsAt: Date | string,\n options: CallsOptions = {}\n): Promise<BookingMutationResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted) return { ok: false, error: \"No booking id\", code: null };\n if (!key) return { ok: false, error: NO_KEY_ERROR, code: null };\n\n const when = startsAt instanceof Date ? startsAt.toISOString() : String(startsAt);\n const { data, error } = await callSdkApi(\n `/api/v1/sdk/bookings?id=${encodeURIComponent(wanted)}`,\n key,\n options,\n { method: \"PATCH\", body: { startsAt: when }, cache: \"no-store\" }\n );\n return readMutation(data, error);\n } catch {\n return { ok: false, error: \"Could not move that demo\", code: null };\n }\n}\n\n/**\n * Call a demo off: the Google event is deleted, the recorder is stood down, and\n * the prospect is messaged if WhatsApp will carry it (see\n * {@link BookingClientNotice}).\n *\n * `reason` is included in that message. Nothing is marked cancelled unless the\n * calendar agreed first, so `{ ok: false, code: \"CALENDAR\" }` means the demo is\n * still on.\n */\nexport async function cancelBooking(\n id: string,\n reason?: string,\n options: CallsOptions = {}\n): Promise<BookingMutationResult> {\n try {\n const wanted = typeof id === \"string\" ? id.trim() : \"\";\n const key = callsKey(options);\n if (!wanted) return { ok: false, error: \"No booking id\", code: null };\n if (!key) return { ok: false, error: NO_KEY_ERROR, code: null };\n\n const query = new URLSearchParams({ id: wanted });\n if (typeof reason === \"string\" && reason.trim()) query.set(\"reason\", reason.trim().slice(0, 200));\n\n const { data, error } = await callSdkApi(`/api/v1/sdk/bookings?${query.toString()}`, key, options, {\n method: \"DELETE\",\n cache: \"no-store\",\n });\n return readMutation(data, error);\n } catch {\n return { ok: false, error: \"Could not cancel that demo\", code: null };\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\n// ── Leads ────────────────────────────────────────────────────────────────────\n//\n// Contact details prospects left with `useGlitchgrab().submitLead()` (#534) —\n// the demo form, the contact page — so your own admin can show them. Same\n// server key as calls and bookings. A lead never becomes a GitHub issue.\n\n/** One lead. One per phone number per project; a repeat submission updates it. */\nexport interface Lead {\n id: string;\n name: string;\n /** Digits only, country code included. */\n phone: string;\n email: string | null;\n company: string | null;\n address: string | null;\n /** What your form passed as `source`, e.g. `\"demo\"`. */\n source: string;\n metadata: Record<string, string | number | boolean> | null;\n /** The page the lead was left on. */\n pageUrl: string | null;\n /** How many times this phone submitted. */\n submissions: number;\n /** First submission. */\n createdAt: string;\n /** Latest submission. */\n updatedAt: string;\n /** The demo this lead went on to book, matched by phone or email. */\n booking: { id: string; startsAt: string; status: string } | null;\n}\n\nexport interface ListLeadsOptions extends CallsOptions {\n /** Only leads with this `source`. */\n source?: string;\n /** Only leads first left at or after this time. */\n since?: Date | string;\n /** Up to 200. Defaults to 100. */\n limit?: number;\n}\n\nfunction readLead(value: unknown): Lead | null {\n if (!isRecord(value) || typeof value.id !== \"string\") return null;\n const metadata = isRecord(value.metadata) ? (value.metadata as Lead[\"metadata\"]) : null;\n const booking = isRecord(value.booking) && typeof value.booking.id === \"string\"\n ? {\n id: value.booking.id,\n startsAt: stringOrNull(value.booking.startsAt) ?? \"\",\n status: stringOrNull(value.booking.status) ?? \"CONFIRMED\",\n }\n : null;\n return {\n id: value.id,\n name: stringOrNull(value.name) ?? \"\",\n phone: stringOrNull(value.phone) ?? \"\",\n email: stringOrNull(value.email),\n company: stringOrNull(value.company),\n address: stringOrNull(value.address),\n source: stringOrNull(value.source) ?? \"\",\n metadata,\n pageUrl: stringOrNull(value.pageUrl),\n submissions: typeof value.submissions === \"number\" ? value.submissions : 1,\n createdAt: stringOrNull(value.createdAt) ?? \"\",\n updatedAt: stringOrNull(value.updatedAt) ?? \"\",\n booking,\n };\n}\n\n/**\n * The project's leads, newest first. Never throws — an empty list on any\n * failure.\n *\n * ```ts\n * const leads = await listLeads({ source: \"demo\" }); // GLITCHGRAB_CALLS_KEY=ggc_…\n * ```\n */\nexport async function listLeads(options: ListLeadsOptions = {}): Promise<Lead[]> {\n try {\n const key = callsKey(options);\n if (!key) return [];\n\n const query = new URLSearchParams();\n if (options.source) query.set(\"source\", options.source);\n if (options.since) {\n const since = options.since instanceof Date ? options.since : new Date(options.since);\n if (!Number.isNaN(since.getTime())) query.set(\"since\", since.toISOString());\n }\n setNumber(query, \"limit\", options.limit);\n const qs = query.toString();\n\n const { data } = await callSdkApi(`/api/v1/sdk/leads${qs ? `?${qs}` : \"\"}`, key, options, {\n cache: \"no-store\",\n });\n if (!Array.isArray(data)) return [];\n return data.map(readLead).filter((lead): lead is Lead => lead !== null);\n } catch {\n return [];\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 LeadInput,\n LeadResult,\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 * How long one report attempt may take before it is abandoned and retried.\n * The server files a report in a few seconds; on a flaky mobile network the\n * response can get lost and fetch then hangs ~30s before failing (#557). The\n * body share assumes a slow ~100 KB/s uplink, so a large screenshot is never\n * cut off mid-upload. A retry of a report the server already has is answered\n * with that report, not filed again. The final attempt runs without one.\n */\nexport function reportAttemptTimeoutMs(bodyLength: number): number {\n return 10_000 + Math.ceil(bodyLength / 100_000) * 1_000;\n}\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 const timeoutMs = reportAttemptTimeoutMs(body.length);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n // The last attempt has no deadline: a slow uplink still gets there, and\n // sendBeacon can't carry a screenshot-sized body if it gave up.\n const isLast = attempt === MAX_RETRIES - 1;\n const controller = !isLast && typeof AbortController === \"function\" ? new AbortController() : null;\n const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n ...(useKeepalive ? { keepalive: true } : {}),\n ...(controller ? { signal: controller.signal } : {}),\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 } finally {\n if (timer) clearTimeout(timer);\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\n/**\n * POST a lead (#534). Retries transient failures like {@link sendFeedback}; a\n * 4xx other than 429 is a caller mistake and returns null at once.\n */\nexport async function sendLead(\n token: string,\n lead: LeadInput & { pageUrl?: string },\n baseUrl?: string\n): Promise<LeadResult | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/sdk/leads`;\n const body = JSON.stringify(lead);\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}` },\n body,\n });\n\n if (response.ok) {\n const envelope = (await response.json()) as {\n success: boolean;\n data?: { leadId?: string; created?: boolean };\n };\n return { success: envelope.success, id: envelope.data?.leadId, created: envelope.data?.created };\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) => setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt)));\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 /** Attached files as text (#563) — the server reads a short summary of them. */\n files?: { name: string; content: string }[]\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 // No `keepalive`: with files the body can pass its 64 KB cap, and the\n // request would fail without a word.\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: description,\n ...(files?.length ? { files } : {}),\n ...signedFields(identity),\n }),\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 * The text inside a PDF the reporter attached (#563), so the assistant and the\n * duplicate check can read it. Called once per PDF, when it is attached.\n *\n * `\"\"` for a PDF with no text layer (a scan); `null` when the project has the\n * assistant off, the file is too big, or anything failed. Never throws.\n */\nexport async function readAttachment(\n file: { name: string; dataUrl: string },\n token: string,\n baseUrl?: string,\n identity?: SignedIdentity | null\n): Promise<string | null> {\n try {\n const url = `${baseUrl ?? DEFAULT_BASE_URL}/api/v1/ai/read-file`;\n // No `keepalive`: a PDF is far past its 64 KB body cap.\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({ name: file.name, data: file.dataUrl, ...signedFields(identity) }),\n });\n const envelope = (await response.json().catch(() => null)) as {\n success?: boolean;\n data?: { enabled?: boolean; text?: string };\n } | null;\n if (!response.ok || !envelope?.success || !envelope.data?.enabled) return null;\n return typeof envelope.data.text === \"string\" ? envelope.data.text : null;\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","_b","_c"]}
|