@getpinax/widget 0.0.0-stage → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +513 -2
  3. package/dist/a11y/scan.d.ts +25 -0
  4. package/dist/api.d.ts +83 -0
  5. package/dist/capture/breadcrumbs.d.ts +49 -0
  6. package/dist/capture/console.d.ts +17 -0
  7. package/dist/capture/env.d.ts +25 -0
  8. package/dist/capture/network.d.ts +23 -0
  9. package/dist/capture/ring.d.ts +14 -0
  10. package/dist/capture/signature.d.ts +7 -0
  11. package/dist/capture/text.d.ts +23 -0
  12. package/dist/capture/ua.d.ts +13 -0
  13. package/dist/chunks/editor-0Kr31Uly.mjs +1368 -0
  14. package/dist/chunks/privacy-B_b1_Y1n.mjs +39 -0
  15. package/dist/chunks/recorder-tu4yugLw.mjs +2874 -0
  16. package/dist/chunks/scan-CFQwrtAQ.mjs +46137 -0
  17. package/dist/chunks/text-D9SaDBT0.mjs +36 -0
  18. package/dist/chunks/widget-BpTBdX2R.mjs +1272 -0
  19. package/dist/editor/annotator.d.ts +20 -0
  20. package/dist/editor/capture.d.ts +28 -0
  21. package/dist/editor/encode.d.ts +7 -0
  22. package/dist/editor/index.d.ts +5 -0
  23. package/dist/editor/mask.d.ts +29 -0
  24. package/dist/editor/pii.d.ts +22 -0
  25. package/dist/editor/shapes.d.ts +31 -0
  26. package/dist/editor-loader.d.ts +9 -0
  27. package/dist/editor-loader.embed.d.ts +6 -0
  28. package/dist/fixcheck.d.ts +42 -0
  29. package/dist/index.d.ts +85 -0
  30. package/dist/known.d.ts +27 -0
  31. package/dist/mount.d.ts +8 -0
  32. package/dist/navigation.d.ts +1 -0
  33. package/dist/paths.d.ts +4 -0
  34. package/dist/pinax-axe.js +46167 -0
  35. package/dist/pinax-editor.js +1417 -0
  36. package/dist/pinax-recorder.js +2915 -0
  37. package/dist/pinax.js +135 -0
  38. package/dist/pinax.mjs +1270 -0
  39. package/dist/preview.d.ts +48 -0
  40. package/dist/preview.mjs +67 -0
  41. package/dist/privacy.d.ts +24 -0
  42. package/dist/queue.d.ts +10 -0
  43. package/dist/replay/buffer.d.ts +40 -0
  44. package/dist/replay/recorder.d.ts +25 -0
  45. package/dist/replay/sanitize.d.ts +19 -0
  46. package/dist/report.d.ts +128 -0
  47. package/dist/segment.d.ts +31 -0
  48. package/dist/settings.d.ts +40 -0
  49. package/dist/types.d.ts +247 -0
  50. package/dist/ui/color.d.ts +7 -0
  51. package/dist/ui/dom.d.ts +14 -0
  52. package/dist/ui/styles.d.ts +3 -0
  53. package/dist/ui/widget.d.ts +241 -0
  54. package/package.json +74 -4
@@ -0,0 +1,48 @@
1
+ import { type SettingsInput } from './settings';
2
+ import type { KnownIssue } from './types';
3
+ import { type WidgetView } from './ui/widget';
4
+ export type { SettingsInput } from './settings';
5
+ export type { CustomField, KnownIssue, WidgetSettings, WidgetWording, } from './types';
6
+ export type { WidgetView } from './ui/widget';
7
+ export type PreviewOptions = {
8
+ /** Screen to show. Default 'button' (closed). */
9
+ view?: WidgetView;
10
+ /** Phone layout (bottom sheet, `smallOnMobile`) whatever the page width. */
11
+ phone?: boolean;
12
+ /** Show "Powered by Pinax". Default true. */
13
+ poweredBy?: boolean;
14
+ /** W-17: the client's logo and colours (Agency). */
15
+ branding?: {
16
+ logoUrl?: string | null;
17
+ primary?: string | null;
18
+ accent?: string | null;
19
+ };
20
+ /**
21
+ * Known issues to show on the chooser ('choose'), as if they matched this
22
+ * page (the first 3). Without them the card never shows; with
23
+ * `knownIssues.enabled` off in the settings it doesn't either. "Me too"
24
+ * only shows the thanks.
25
+ */
26
+ known?: KnownIssue[];
27
+ /**
28
+ * Who the form's notice says reports go to ("Sent to … with your note"),
29
+ * e.g. the site's name or domain. Default "this website".
30
+ */
31
+ site?: string;
32
+ };
33
+ export type PreviewHandle = {
34
+ /** Re-render with new settings, staying on the screen on show. */
35
+ update(settings: SettingsInput, options?: PreviewOptions): void;
36
+ /** Jump to a screen ('button' closes the panel). */
37
+ show(view: WidgetView): void;
38
+ /** The screen on show (changes when the viewer clicks around). */
39
+ view(): WidgetView;
40
+ /** Removes the preview's UI from the container. */
41
+ destroy(): void;
42
+ };
43
+ /**
44
+ * Renders the widget into `container`'s shadow root with `settings`.
45
+ * Give the container a size (e.g. 100% x 520px); the button and panel are
46
+ * positioned inside it exactly as they would be in the visitor's window.
47
+ */
48
+ export declare function preview(container: HTMLElement, settings: SettingsInput, options?: PreviewOptions): PreviewHandle;
@@ -0,0 +1,67 @@
1
+ import { a as e, n as t, o as n, s as r, t as i } from "./chunks/widget-BpTBdX2R.mjs";
2
+ // src/preview.ts
3
+ var a = `
4
+ :host { display: block; position: relative; overflow: hidden; }
5
+ .preview .launcher, .preview .panel { position: absolute; }
6
+ .preview .panel { max-height: calc(100% - 108px); }
7
+ .preview.pos-right-edge .panel, .preview.no-button .panel { max-height: calc(100% - 48px); }
8
+ ${t("", ".pnx.phone")}
9
+ .pnx.preview.phone .panel { max-height: 88%; }
10
+ `, o = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22340%22%20viewBox%3D%220%200%20600%20340%22%3E%3Crect%20width%3D%22600%22%20height%3D%22340%22%20fill%3D%22%23F4EDE2%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%22600%22%20height%3D%2244%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2224%22%20y%3D%2216%22%20width%3D%22120%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%2282%22%20width%3D%22300%22%20height%3D%2222%22%20rx%3D%228%22%20fill%3D%22%23111827%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22118%22%20width%3D%22380%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22140%22%20width%3D%22330%22%20height%3D%2212%22%20rx%3D%226%22%20fill%3D%22%23C9C2B6%22%2F%3E%3Crect%20x%3D%2240%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%22316%22%20y%3D%22186%22%20width%3D%22250%22%20height%3D%22110%22%20rx%3D%2214%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Crect%20x%3D%2260%22%20y%3D%22250%22%20width%3D%22110%22%20height%3D%2228%22%20rx%3D%2214%22%20fill%3D%22%23FF5C35%22%2F%3E%3C%2Fsvg%3E";
11
+ function s(t, s, c = {}) {
12
+ let l = t.shadowRoot ?? t.attachShadow({ mode: "open" }), u = { ...c }, d = null, f = (t, s) => {
13
+ d?.destroy(), l.replaceChildren();
14
+ let c = r(t), f = n(u.known, c.knownIssues);
15
+ d = new i(l, {
16
+ enabled: !0,
17
+ settings: c,
18
+ branding: e({
19
+ ...u.branding,
20
+ poweredBy: u.poweredBy !== !1
21
+ })
22
+ }, {
23
+ takeScreenshot: async () => ({
24
+ dataUrl: o,
25
+ annotations: []
26
+ }),
27
+ markUp: async () => null,
28
+ submit: async () => ({ status: "sent" }),
29
+ user: () => void 0,
30
+ knownIssues: () => f,
31
+ meToo: async () => !0,
32
+ attachments: () => {
33
+ let e = c.consent?.required === !0, { console: t, network: n } = c.capture;
34
+ return {
35
+ logs: !e,
36
+ askLogs: e && (t || n),
37
+ replay: !1
38
+ };
39
+ },
40
+ site: () => u.site || "this website"
41
+ }, {
42
+ className: u.phone ? "preview phone" : "preview",
43
+ css: a
44
+ }), s !== "button" && d.showView(s);
45
+ };
46
+ return f(s, u.view ?? "button"), {
47
+ update(e, t) {
48
+ let n = d?.currentView() ?? "button";
49
+ t && (u = {
50
+ ...u,
51
+ ...t
52
+ }), f(e, t?.view ?? n);
53
+ },
54
+ show(e) {
55
+ d?.showView(e);
56
+ },
57
+ view() {
58
+ return d?.currentView() ?? "button";
59
+ },
60
+ destroy() {
61
+ d?.destroy(), d = null, l.replaceChildren();
62
+ }
63
+ };
64
+ }
65
+ export { s as preview };
66
+
67
+ //# sourceMappingURL=preview.mjs.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AI-12: text patterns for personal details. Screenshot auto-blur and replay
3
+ * masking use all of it; the initial script uses only `redactPersonal` (and
4
+ * what it needs), for console text, breadcrumb targets and error signatures.
5
+ * Mirrored by `redactPersonal` in `backend/src/modules/widget/widget.sanitise.ts`.
6
+ */
7
+ export type Span = {
8
+ start: number;
9
+ end: number;
10
+ kind: 'email' | 'phone';
11
+ };
12
+ export declare function isPhone(raw: string): boolean;
13
+ /** Emails and phone numbers in `text`, in order, never overlapping. */
14
+ export declare function findPersonalSpans(text: string): Span[];
15
+ /** Replaces emails and phone numbers with asterisks (same length). */
16
+ export declare function redactPersonal(text: string): string;
17
+ /** A text that is only a label ("Name", "Customer:"); the name follows it. */
18
+ export declare const NAME_LABEL_ONLY: RegExp;
19
+ /** "Name: Ada Lovelace" in one text. */
20
+ export declare const NAME_LABEL_INLINE: RegExp;
21
+ /** "Signed in as Ada", "Welcome back, Ada" anywhere in a text. */
22
+ export declare const SIGNED_IN: RegExp;
23
+ /** Text short enough, and wordy enough, to be a name or username after a label. */
24
+ export declare function looksLikeNameValue(text: string): boolean;
@@ -0,0 +1,10 @@
1
+ import { type Fetch } from './api';
2
+ import { type ReportBody } from './report';
3
+ export declare const MAX_AGE_MS: number;
4
+ /** What may be kept in localStorage: the report without the private parts. */
5
+ export declare function queuedCopy(body: ReportBody): ReportBody;
6
+ /** Saves a slimmed copy of a report for later (see above). */
7
+ export declare function enqueue(key: string, body: ReportBody): boolean;
8
+ export declare function queuedCount(key: string): number;
9
+ /** Sends what's waiting. Items the server refuses for good are dropped. */
10
+ export declare function flush(doFetch: Fetch, api: string, key: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * AI-15: a rolling window of rrweb events. Events are kept in segments that
3
+ * each start with a full snapshot (rrweb's Meta event, then FullSnapshot,
4
+ * from `checkoutEveryNms`), so whatever is kept can be replayed from its
5
+ * first event. A segment is dropped once the next one already starts at or
6
+ * before the window's edge, so the buffer always covers the last
7
+ * `windowMs` (or the whole visit, if shorter) and at most one checkout
8
+ * interval more.
9
+ */
10
+ export type ReplayEvent = {
11
+ type: number;
12
+ timestamp: number;
13
+ };
14
+ /** rrweb's EventType.Meta: the first event of every full snapshot. */
15
+ export declare const META = 4;
16
+ export declare class ReplayBuffer<E extends ReplayEvent = ReplayEvent> {
17
+ readonly windowMs: number;
18
+ private now;
19
+ /** Hard cap on events held, whatever the time window (a very busy page). */
20
+ readonly maxEvents: number;
21
+ private segments;
22
+ constructor(windowMs?: number, now?: () => number,
23
+ /** Hard cap on events held, whatever the time window (a very busy page). */
24
+ maxEvents?: number);
25
+ push(event: E): void;
26
+ /** Drops whole segments that are no longer needed to cover the window. */
27
+ trim(): void;
28
+ private count;
29
+ /**
30
+ * The events to send, oldest first. `lastSegments` keeps only the newest
31
+ * segments, for when the full window is too big to upload.
32
+ */
33
+ events(lastSegments?: number): E[];
34
+ get segmentCount(): number;
35
+ clear(): void;
36
+ }
37
+ /** base64 of bytes, in chunks so big replays don't overflow the call stack. */
38
+ export declare function toBase64(bytes: Uint8Array): string;
39
+ /** gzip with the browser's CompressionStream; null where it isn't supported. */
40
+ export declare function gzip(text: string): Promise<Uint8Array | null>;
@@ -0,0 +1,25 @@
1
+ export declare const WINDOW_MS = 30000;
2
+ /** A full snapshot this often, so the kept window can start mid-visit. */
3
+ export declare const CHECKOUT_MS = 10000;
4
+ export type Recorder = {
5
+ /**
6
+ * The buffer as base64 of gzipped JSON, at most `maxChars` long (fewer
7
+ * seconds if the whole window doesn't fit), or null when it can't be made
8
+ * (no CompressionStream, nothing recorded, too big).
9
+ */
10
+ exportReplay(maxChars: number): Promise<string | null>;
11
+ stop(): void;
12
+ };
13
+ /**
14
+ * `'all'` (the default, CNIL's "mask everything"): every text is starred out
15
+ * and images, pictures, videos, canvases and frames are left out (the replay
16
+ * shows an empty box of the same size). `'inputs'`: form fields, editable
17
+ * text, emails and phone numbers only.
18
+ */
19
+ export type ReplayMask = 'all' | 'inputs';
20
+ /** Left out entirely with `mask: 'all'`; rrweb records only their size. */
21
+ export declare const MEDIA = "img,picture,video,canvas,iframe";
22
+ export declare function startRecorder(options?: {
23
+ exclude?: string;
24
+ mask?: ReplayMask;
25
+ }): Recorder;
@@ -0,0 +1,19 @@
1
+ import { type ReplayEvent } from './buffer';
2
+ /** A URL without its fragment or secret query values, and no email or phone. */
3
+ export declare function cleanUrl(value: string, base?: string): string;
4
+ type Attrs = Record<string, unknown>;
5
+ /**
6
+ * A cleaned copy of a node's (or a mutation's) attributes. `maskText` also
7
+ * stars out the text attributes above.
8
+ */
9
+ export declare function cleanAttributes(attrs: Attrs, tagName?: string, base?: string, maskText?: boolean): Attrs;
10
+ /**
11
+ * The events as JSON, with every attribute cleaned: full snapshot nodes
12
+ * (`{ tagName, attributes }`), mutation adds (same shape) and attribute
13
+ * changes (`{ id, attributes }`). Meta events get a cleaned `href`.
14
+ * `maskText` (replay mask 'all') stars out text attributes as well.
15
+ */
16
+ export declare function serializeReplay(events: readonly ReplayEvent[], base?: string | undefined, options?: {
17
+ maskText?: boolean;
18
+ }): string;
19
+ export {};
@@ -0,0 +1,128 @@
1
+ import type { Breadcrumb } from './capture/breadcrumbs';
2
+ import type { ConsoleEntry } from './capture/console';
3
+ import type { Environment } from './capture/env';
4
+ import type { NetworkEntry } from './capture/network';
5
+ import type { A11yScan, Annotation, Barrier, FieldAnswer, PinaxUser, ReportType } from './types';
6
+ export declare const BARRIERS: Barrier[];
7
+ /**
8
+ * Builds the POST /reports body. Mirrors the limits in
9
+ * backend/src/modules/widget/widget.validator.ts so a report is never rejected
10
+ * for something the widget could have trimmed.
11
+ */
12
+ export declare const LIMITS: {
13
+ readonly message: 5000;
14
+ readonly email: 320;
15
+ readonly entries: 50;
16
+ readonly annotations: 200;
17
+ /** The backend caps the data URL at 6,000,000 chars and the decoded image at 4.5 MB. */
18
+ readonly screenshotChars: 6000000;
19
+ readonly screenshotBytes: number;
20
+ readonly breadcrumbs: 20;
21
+ readonly followUpQuestion: 300;
22
+ readonly followUpAnswer: 2000;
23
+ /** AI-15: base64 of the gzipped replay. */
24
+ readonly replayChars: 4500000;
25
+ /** W-10: answers per report, and the longest text answer. */
26
+ readonly fields: 10;
27
+ readonly fieldId: 40;
28
+ readonly fieldText: 200;
29
+ /** The reports route parses at most 11 MB (`express.json({ limit: '11mb' })`). */
30
+ readonly bodyBytes: number;
31
+ };
32
+ export declare function isEmail(value: string): boolean;
33
+ export type ReportInput = {
34
+ type: ReportType;
35
+ message: string;
36
+ rating?: number;
37
+ email?: string;
38
+ user?: PinaxUser;
39
+ screenshot?: string;
40
+ annotations?: Annotation[];
41
+ /** AI-10 */
42
+ followUp?: {
43
+ question: string;
44
+ answer: string;
45
+ };
46
+ /** W-10 */
47
+ fields?: FieldAnswer[];
48
+ /** W-19, W-20: accessibility reports only. */
49
+ barrier?: Barrier;
50
+ a11yScan?: A11yScan | null;
51
+ /** P-04: honeypot value and time to fill in the form. */
52
+ hp?: string;
53
+ elapsedMs?: number;
54
+ /** Visitor consent, only on sites that wait for it. */
55
+ consent?: ReportConsent;
56
+ /** The visitor ticked "Include technical details" (consent not given). */
57
+ includeLogs?: boolean;
58
+ };
59
+ /** What the visitor agreed to through the site's banner. */
60
+ export type ReportConsent = {
61
+ diagnostics: boolean;
62
+ replay: boolean;
63
+ };
64
+ export type ReportContext = {
65
+ page: {
66
+ url: string;
67
+ title?: string;
68
+ };
69
+ environment: Environment;
70
+ console: ConsoleEntry[];
71
+ network: NetworkEntry[];
72
+ breadcrumbs?: Breadcrumb[];
73
+ };
74
+ export type ReportBody = {
75
+ type: ReportType;
76
+ message: string;
77
+ rating?: number;
78
+ email?: string;
79
+ user?: PinaxUser;
80
+ page: {
81
+ url: string;
82
+ title?: string;
83
+ };
84
+ environment: Environment;
85
+ console?: ConsoleEntry[];
86
+ network?: NetworkEntry[];
87
+ screenshot?: string;
88
+ annotations?: Array<Record<string, unknown>>;
89
+ breadcrumbs?: Breadcrumb[];
90
+ followUp?: {
91
+ question: string;
92
+ answer: string;
93
+ };
94
+ fields?: FieldAnswer[];
95
+ barrier?: Barrier;
96
+ a11yScan?: A11yScan;
97
+ replay?: string;
98
+ hp?: string;
99
+ elapsedMs?: number;
100
+ consent?: ReportConsent;
101
+ includeLogs?: true;
102
+ };
103
+ /** Decoded byte length of a base64 data URL. */
104
+ export declare function dataUrlBytes(dataUrl: string): number;
105
+ export declare function isSendableScreenshot(dataUrl: string): boolean;
106
+ /** The backend refuses a report with no note, no rating and no screenshot. */
107
+ export declare function hasContent(input: {
108
+ type: ReportType;
109
+ message: string;
110
+ rating?: number;
111
+ screenshot?: string;
112
+ barrier?: Barrier;
113
+ }): boolean;
114
+ /**
115
+ * Page URL as the backend accepts it: absolute, at most 2000 chars, with no
116
+ * fragment (OAuth implicit flow, Supabase and Firebase put tokens in
117
+ * `#access_token=…`) and query values hidden (`…`) except the harmless
118
+ * `PAGE_PARAM` ones (`utm_*`, `page`, `lang`, `ref`, `variant`, `tab`, `sort`).
119
+ */
120
+ export declare function pageUrl(href: string): string;
121
+ export declare function buildReport(input: ReportInput, ctx: ReportContext): ReportBody;
122
+ /** UTF-8 size of the JSON body. */
123
+ export declare function bodyBytes(body: ReportBody): number;
124
+ /**
125
+ * AI-15: adds the replay only when it fits the backend's limits (4.5 M
126
+ * base64 characters, the whole request under 11 MB). Returns whether it did.
127
+ */
128
+ export declare function attachReplay(body: ReportBody, replay: string | null): boolean;
@@ -0,0 +1,31 @@
1
+ import type { ReportType } from './types';
2
+ type SegmentUser = {
3
+ id?: () => unknown;
4
+ };
5
+ /** The parts of `window.analytics` we use. The snippet stub has only `track`. */
6
+ export type SegmentAnalytics = {
7
+ track?: (event: string, properties?: Record<string, unknown>) => unknown;
8
+ user?: () => SegmentUser | undefined;
9
+ };
10
+ export declare const SEGMENT_EVENTS: {
11
+ readonly opened: 'Pinax Widget Opened';
12
+ readonly submitted: 'Feedback Submitted';
13
+ };
14
+ /** `window.analytics`, if it looks like Segment's. */
15
+ export declare function segmentAnalytics(win?: Window): SegmentAnalytics | null;
16
+ /** The init option wins over the site's setting. */
17
+ export declare function segmentEnabled(option: boolean | undefined, setting: boolean | undefined): boolean;
18
+ export declare function trackOpened(): void;
19
+ export declare function trackSubmitted(input: {
20
+ type: ReportType;
21
+ rating?: number | null;
22
+ reportId?: string;
23
+ queued?: boolean;
24
+ }): void;
25
+ /**
26
+ * Segment's signed-in user id (`analytics.user().id()`), used when the site
27
+ * didn't call `Pinax.identify()`. Null before analytics.js has loaded or for
28
+ * anonymous visitors.
29
+ */
30
+ export declare function segmentUserId(): string | null;
31
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { AiConfig, Branding, CustomField, KnownIssue, WidgetSettings, WidgetWording } from './types';
2
+ /** Same defaults as `backend/src/modules/widget/widget-settings.ts`. */
3
+ export declare const DEFAULT_SETTINGS: WidgetSettings;
4
+ /** W-10: the backend's limits (`FIELD_LIMITS` in widget-settings.ts). */
5
+ export declare const FIELD_LIMITS: {
6
+ readonly fields: 10;
7
+ readonly label: 80;
8
+ readonly options: 20;
9
+ readonly option: 80;
10
+ readonly text: 200;
11
+ };
12
+ /** Settings with any field (or wording/type entry) left out. */
13
+ export type SettingsInput = Partial<Omit<WidgetSettings, 'types' | 'wording' | 'capture' | 'knownIssues' | 'consent'> & {
14
+ types: Partial<WidgetSettings['types']>;
15
+ wording: Partial<WidgetWording>;
16
+ capture: Partial<WidgetSettings['capture']>;
17
+ knownIssues: Partial<NonNullable<WidgetSettings['knownIssues']>>;
18
+ consent: Partial<NonNullable<WidgetSettings['consent']>>;
19
+ }>;
20
+ /** W-10: well-formed fields only, so a half-made field in the dashboard can't break the form. */
21
+ export declare function resolveFields(input: unknown): CustomField[];
22
+ /**
23
+ * Fills in anything missing or malformed with the defaults, so an older API
24
+ * (or a half-edited dashboard form, in the preview) never breaks the widget.
25
+ */
26
+ export declare function resolveSettings(input?: SettingsInput | null): WidgetSettings;
27
+ /** The privacy policy link: an https address, or '' (never javascript: and the like). */
28
+ export declare function httpsUrl(v: unknown): string;
29
+ /** W-17: only plain hex colours and an http(s) logo address get through. */
30
+ export declare function resolveBranding(input: unknown): Branding;
31
+ /**
32
+ * The AI part of the config, everything off when missing (an older API, or
33
+ * a site without Pinax AI). `samplingRate` (B-17) caps both sample rates.
34
+ */
35
+ export declare function resolveAi(ai: unknown, samplingRate?: unknown): AiConfig;
36
+ /**
37
+ * Known issues from the config: none when missing (an older API or CDN
38
+ * copy) or switched off, and only well-formed entries otherwise.
39
+ */
40
+ export declare function resolveKnown(input: unknown, settings?: WidgetSettings['knownIssues']): KnownIssue[];