@marktiderman/genesis-support 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,477 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * The Genesis support wire contract: what a support client (the web loader, a
5
+ * native app, any HTTP caller) and the `genesis-support-intake` Edge Function
6
+ * say to each other. One file, kept byte-identical in two places:
7
+ * `@marktiderman/genesis-support` (`src/contract.ts`) and the function template
8
+ * (`functions/genesis-support-intake/contract.ts`); a test fails when they drift.
9
+ *
10
+ * Dependency-free and additive: `CONTRACT_VERSION` changes only with a shape
11
+ * change, and a reader tolerates fields and widget types it doesn't know, so an
12
+ * old client keeps working.
13
+ */
14
+ /** Bumped only on a shape change. Old clients must keep working. */
15
+ declare const CONTRACT_VERSION: 1;
16
+ /** The widget kinds a boot can turn on. */
17
+ type WidgetType = "feedback" | "roadmap" | "changelog" | "survey" | "messaging";
18
+ /** On the wire the type is open: a client switches on the known ones and ignores the rest. */
19
+ type WidgetTypeWire = WidgetType | (string & {});
20
+ /**
21
+ * How the person is known to the server.
22
+ * - `genesis-session`: signed in to the app's own Supabase (a user access token). The default.
23
+ * - `signed`: the app's backend mints a short-lived token for the person (reserved).
24
+ * - `anonymous`: a device id only (reserved; the database refuses writes without a session).
25
+ */
26
+ type IdentityMode = "anonymous" | "signed" | "genesis-session";
27
+ type LauncherPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
28
+ /** Theme hints a web launcher may use. Genesis apps use their own theme instead. */
29
+ interface ThemeTokens {
30
+ colorBg?: string;
31
+ colorFg?: string;
32
+ colorPrimary?: string;
33
+ colorPrimaryFg?: string;
34
+ radius?: string;
35
+ fontFamily?: string;
36
+ launcherLabel?: string;
37
+ launcherPosition?: LauncherPosition;
38
+ }
39
+ /** One enabled widget and its public config (never a secret). */
40
+ interface WidgetConfig {
41
+ type: WidgetTypeWire;
42
+ config: Record<string, unknown>;
43
+ }
44
+ /** The boot handshake (`GET`): world-readable, so only public values. */
45
+ interface BootConfig {
46
+ contractVersion: number;
47
+ project: {
48
+ id: string;
49
+ name: string;
50
+ };
51
+ identityMode: IdentityMode;
52
+ theme: ThemeTokens;
53
+ widgets: WidgetConfig[];
54
+ flags: Record<string, boolean>;
55
+ }
56
+ type FeedbackReportType = "bug" | "idea" | "question" | "praise";
57
+ type FeedbackStatus = "open" | "submitted" | "in_progress" | "addressed" | "wont_fix";
58
+ type FindingSeverity = "low" | "medium" | "high" | "critical";
59
+ declare const FEEDBACK_REPORT_TYPES: readonly FeedbackReportType[];
60
+ declare const FEEDBACK_STATUSES: readonly FeedbackStatus[];
61
+ /** Client to server: open a report. Screenshots go to storage, not in this body. */
62
+ interface SubmitFeedbackInput {
63
+ type?: FeedbackReportType;
64
+ title?: string;
65
+ body: string;
66
+ route?: string;
67
+ appVersion?: string;
68
+ envType?: string;
69
+ severity?: FindingSeverity;
70
+ tags?: string[];
71
+ /** Small, free-form context (breadcrumbs, a pin on the page). At most 16 KB as JSON. */
72
+ context?: Record<string, unknown>;
73
+ }
74
+ /** Server to client: the report created. `findingId` is the report's id (one finding per report here). */
75
+ interface SubmitFeedbackResult {
76
+ reportId: string;
77
+ findingId: string;
78
+ status: FeedbackStatus;
79
+ }
80
+ /** One of the caller's own reports. */
81
+ interface FeedbackReportSummary {
82
+ id: string;
83
+ type: FeedbackReportType;
84
+ title: string;
85
+ status: FeedbackStatus;
86
+ route: string | null;
87
+ createdAt: string;
88
+ findingCount: number;
89
+ }
90
+ /** The operations the intake function multiplexes on (`{ op, ...args }`). */
91
+ type FeedbackOp = "submit" | "list";
92
+ /** Headers a client sends. The person's access token goes in `Authorization: Bearer …`. */
93
+ declare const WIDGET_HEADERS: {
94
+ /** The project's publishable key (an identifier, not a secret). */
95
+ readonly key: "x-genesis-key";
96
+ /** `signed` mode: the minted short-lived token. */
97
+ readonly identity: "x-genesis-identity";
98
+ /** `anonymous` mode: the device id. */
99
+ readonly device: "x-genesis-device";
100
+ /** A proof-of-work or captcha token, for anonymous writes. */
101
+ readonly challenge: "x-genesis-challenge";
102
+ /** The contract version the client was built against. */
103
+ readonly contract: "x-genesis-contract";
104
+ };
105
+ /** Text limits, the same as the database's checks. */
106
+ declare const LIMITS: {
107
+ readonly title: 300;
108
+ readonly body: 20000;
109
+ readonly route: 2000;
110
+ readonly appVersion: 200;
111
+ readonly tag: 60;
112
+ readonly tags: 50;
113
+ readonly contextBytes: 16384;
114
+ };
115
+ /**
116
+ * A route as a path only, safe to store and send: the query string and
117
+ * fragment (tokens, codes, emails) are dropped, and path segments that look
118
+ * private (an email, an id, a long token) become `:email`, `:id` or `:token`
119
+ * (`/users/jane@x.test/reset/abc123…` → `/users/:email/reset/:token`).
120
+ */
121
+ declare function cleanRoute(route: string | null | undefined): string | undefined;
122
+ type SubmitValidation = {
123
+ ok: true;
124
+ value: SubmitFeedbackInput;
125
+ } | {
126
+ ok: false;
127
+ error: "invalid_args" | "payload_too_large";
128
+ field: string;
129
+ };
130
+ /** Checks a `submit` body the way the server does: known keys only, every limit. */
131
+ declare function validateSubmitInput(raw: unknown): SubmitValidation;
132
+
133
+ /**
134
+ * The feedback record and its two seams: a collector assembles the context
135
+ * (everything except what the person typed), a sink delivers the finished
136
+ * record. Platform-neutral; one default of each ships (`createContextCollector`,
137
+ * `createSupabaseSink`), and an app can swap either.
138
+ */
139
+
140
+ /** What kind of feedback: a bug, an idea, a question or praise. */
141
+ type FeedbackType = FeedbackReportType;
142
+ declare const FEEDBACK_TYPES: readonly FeedbackType[];
143
+ /** Where the person was and what they were running when they sent it. */
144
+ interface FeedbackContext {
145
+ /** The version signature, `1.4.0 (37) · preview · 3f2a9c1b`. */
146
+ appVersion?: string;
147
+ /** The screen, as a path. Query strings and fragments are always dropped. */
148
+ route?: string;
149
+ /** `ios`, `android`, `web`. */
150
+ platform?: string;
151
+ /** Model, OS version, locale, screen size, update id… At most 4 KB as JSON. */
152
+ device?: Record<string, unknown>;
153
+ /**
154
+ * Who sent it, for sinks that need it. The Supabase sink ignores it: the
155
+ * database takes the person from the session, never from the request.
156
+ */
157
+ userId?: string;
158
+ /** Anything else small (breadcrumbs, a pin on the page). At most 16 KB as JSON. */
159
+ extra?: Record<string, unknown>;
160
+ }
161
+ type ScreenshotContentType = "image/png" | "image/jpeg" | "image/webp";
162
+ /**
163
+ * A screenshot to attach. The record carries the bytes (or base64) and no
164
+ * URL: the destination stores it and keeps its own key, so nothing in the
165
+ * record expires.
166
+ */
167
+ interface FeedbackAttachment {
168
+ kind: "screenshot";
169
+ contentType: ScreenshotContentType;
170
+ /** The image, base64 (no `data:` prefix; a data URI is accepted and stripped). */
171
+ base64?: string;
172
+ /** Or the bytes. */
173
+ bytes?: Uint8Array;
174
+ width?: number;
175
+ height?: number;
176
+ }
177
+ interface FeedbackRecord {
178
+ type: FeedbackType;
179
+ /** What the person wrote. */
180
+ message: string;
181
+ title?: string;
182
+ context: FeedbackContext;
183
+ attachments?: FeedbackAttachment[];
184
+ }
185
+ /** Everything but what the person typed. */
186
+ interface ContextCollector {
187
+ collect(): Promise<FeedbackContext> | FeedbackContext;
188
+ }
189
+ /** What the destination kept. */
190
+ interface FeedbackDelivery {
191
+ /** The saved report's id. */
192
+ id: string;
193
+ /** How many attachments the destination saved the report without (an upload failed, or it takes none). */
194
+ attachmentsDropped: number;
195
+ }
196
+ /** Takes a finished record to its destination. */
197
+ interface FeedbackSink {
198
+ submit(record: FeedbackRecord): Promise<FeedbackDelivery>;
199
+ }
200
+ declare const DEVICE_MAX_BYTES = 4096;
201
+ declare const EXTRA_MAX_BYTES = 16384;
202
+ /** A value given as itself or as a function that reads it now. */
203
+ type Lazy<T> = T | (() => T);
204
+ interface ContextCollectorOptions {
205
+ appVersion?: Lazy<string | undefined>;
206
+ route?: Lazy<string | undefined>;
207
+ platform?: Lazy<string | undefined>;
208
+ device?: Lazy<Record<string, unknown> | undefined>;
209
+ userId?: Lazy<string | undefined>;
210
+ extra?: Lazy<Record<string, unknown> | undefined>;
211
+ }
212
+ /**
213
+ * The default collector: reads each value when feedback is sent, cleans the
214
+ * route, and drops a device or extra bag over its size cap rather than
215
+ * sending a report the database would refuse.
216
+ */
217
+ declare function createContextCollector(options: ContextCollectorOptions): ContextCollector;
218
+ /** Collectors merged left to right (a later one's values win; `device` and `extra` merge by key). */
219
+ declare function composeCollectors(...collectors: ContextCollector[]): ContextCollector;
220
+ /** The context as the database takes it: a clean route, short strings, bags under their caps. */
221
+ declare function sanitizeContext(context: FeedbackContext): FeedbackContext;
222
+ /** Base64 (or a `data:` URI) to bytes, without `atob` (Hermes, workers, Node all agree). */
223
+ declare function base64ToBytes(input: string): Uint8Array;
224
+ /** The bytes of an attachment, from `bytes` or `base64`. */
225
+ declare function attachmentBytes(attachment: FeedbackAttachment): Uint8Array | null;
226
+
227
+ /**
228
+ * The support client: the app's own Supabase (migration 0006 from
229
+ * `@marktiderman/genesis-supabase`) under row level security. Everything a
230
+ * person does (send feedback, read their reports and the replies, reply, vote
231
+ * on the idea board) and the Supabase sink for the feedback record.
232
+ */
233
+
234
+ /** Any supabase-js client; the app's generated Database types don't need to list the support tables. */
235
+ type SupportSupabase = SupabaseClient<any, any, any>;
236
+ /** The storage bucket migration 0006 creates. */
237
+ declare const SUPPORT_BUCKET = "genesis-support";
238
+ /** A Supabase refusal, with its code (`GS429` rate limited, `42501` not allowed, …). */
239
+ declare class SupportError extends Error {
240
+ readonly code: string | undefined;
241
+ readonly operation: string;
242
+ constructor(message: string, code: string | undefined, operation: string);
243
+ /** The database's rate limit (SQLSTATE GS429). */
244
+ get rateLimited(): boolean;
245
+ }
246
+ type IdeaStatus = "open" | "planned" | "in_progress" | "shipped" | "declined" | "hidden";
247
+ interface SupportReport {
248
+ id: string;
249
+ type: FeedbackType;
250
+ title: string | null;
251
+ message: string;
252
+ status: FeedbackStatus;
253
+ /** The FeatureId staff linked it to. */
254
+ featureId: string | null;
255
+ route: string | null;
256
+ appVersion: string | null;
257
+ createdAt: string;
258
+ updatedAt: string;
259
+ lastStaffReplyAt: string | null;
260
+ lastUserReplyAt: string | null;
261
+ userReadAt: string | null;
262
+ staffReadAt: string | null;
263
+ /** A staff reply the person hasn't read. */
264
+ unread: boolean;
265
+ }
266
+ interface SupportReply {
267
+ id: string;
268
+ reportId: string;
269
+ body: string;
270
+ fromStaff: boolean;
271
+ /** Written by the signed-in person. */
272
+ mine: boolean;
273
+ createdAt: string;
274
+ }
275
+ interface SupportAttachmentView {
276
+ id: string;
277
+ path: string;
278
+ contentType: string;
279
+ width: number | null;
280
+ height: number | null;
281
+ /** A signed URL, valid for `signedUrlSeconds` (default an hour). Null when signing failed. */
282
+ url: string | null;
283
+ }
284
+ interface SupportIdea {
285
+ id: string;
286
+ title: string;
287
+ body: string | null;
288
+ status: IdeaStatus;
289
+ /** The FeatureId that delivers it (set when it ships). */
290
+ featureId: string | null;
291
+ shippedAt: string | null;
292
+ voteCount: number;
293
+ /** The signed-in person voted for it. */
294
+ voted: boolean;
295
+ /** The signed-in person suggested it. */
296
+ mine: boolean;
297
+ createdAt: string;
298
+ }
299
+ interface SupportClient extends FeedbackSink {
300
+ /** The signed-in person's reports, newest first. */
301
+ myReports(options?: {
302
+ limit?: number;
303
+ }): Promise<SupportReport[]>;
304
+ report(id: string): Promise<SupportReport | null>;
305
+ replies(reportId: string): Promise<SupportReply[]>;
306
+ reply(reportId: string, body: string): Promise<SupportReply>;
307
+ markRead(reportId: string): Promise<void>;
308
+ /** Deletes the person's own report; its screenshots are queued for removal. */
309
+ deleteReport(reportId: string): Promise<void>;
310
+ attachments(reportId: string): Promise<SupportAttachmentView[]>;
311
+ /** The board, most votes first. Hidden ideas are left out, the person's own included. */
312
+ ideas(options?: {
313
+ limit?: number;
314
+ statuses?: IdeaStatus[];
315
+ }): Promise<SupportIdea[]>;
316
+ suggestIdea(input: {
317
+ title: string;
318
+ body?: string;
319
+ }): Promise<SupportIdea>;
320
+ vote(ideaId: string): Promise<void>;
321
+ unvote(ideaId: string): Promise<void>;
322
+ /** "You asked, we shipped": shipped ideas the person suggested or voted for. */
323
+ shippedForMe(): Promise<SupportIdea[]>;
324
+ }
325
+ interface SupportClientOptions {
326
+ bucket?: string;
327
+ signedUrlSeconds?: number;
328
+ /** For the attachment file names. Default `crypto.randomUUID`. */
329
+ randomId?: () => string;
330
+ }
331
+ /** True when there is a staff reply newer than the person's last read. */
332
+ declare function isUnread(row: {
333
+ lastStaffReplyAt: string | null;
334
+ userReadAt: string | null;
335
+ }): boolean;
336
+ /** The support client over the app's Supabase client. */
337
+ declare function createSupportClient(supabase: SupportSupabase, options?: SupportClientOptions): SupportClient;
338
+ /** The Supabase sink on its own (the support client is one). */
339
+ declare function createSupabaseSink(supabase: SupportSupabase, options?: SupportClientOptions): FeedbackSink;
340
+
341
+ /**
342
+ * Every string the support block shows, in English. Pass `copy` (any part of
343
+ * this shape) to translate or reword; what you leave out stays English.
344
+ */
345
+ declare const supportCopy: {
346
+ settings: {
347
+ section: string;
348
+ sendFeedback: string;
349
+ help: string;
350
+ /** `{count}` new replies. */
351
+ unreadReplies: string;
352
+ };
353
+ feedback: {
354
+ title: string;
355
+ typeLabel: string;
356
+ types: {
357
+ bug: string;
358
+ idea: string;
359
+ question: string;
360
+ praise: string;
361
+ };
362
+ messageLabel: string;
363
+ messagePlaceholder: string;
364
+ screenshotLabel: string;
365
+ includeScreenshot: string;
366
+ screenshotHint: string;
367
+ annotate: string;
368
+ pin: string;
369
+ pinned: string;
370
+ removeScreenshot: string;
371
+ send: string;
372
+ sending: string;
373
+ sentTitle: string;
374
+ sentBody: string;
375
+ sentWithoutScreenshot: string;
376
+ failedTitle: string;
377
+ failedBody: string;
378
+ rateLimited: string;
379
+ signInFirst: string;
380
+ messageRequired: string;
381
+ };
382
+ annotate: {
383
+ title: string;
384
+ pen: string;
385
+ hide: string;
386
+ undo: string;
387
+ done: string;
388
+ cancel: string;
389
+ hint: string;
390
+ failed: string;
391
+ };
392
+ inbox: {
393
+ title: string;
394
+ sectionTitle: string;
395
+ emptyTitle: string;
396
+ emptyBody: string;
397
+ errorTitle: string;
398
+ errorBody: string;
399
+ sendFeedback: string;
400
+ ideaBoard: string;
401
+ shipped: string;
402
+ delete: string;
403
+ deleteConfirmTitle: string;
404
+ deleteConfirmBody: string;
405
+ cancel: string;
406
+ unread: string;
407
+ };
408
+ thread: {
409
+ title: string;
410
+ placeholder: string;
411
+ send: string;
412
+ read: string;
413
+ support: string;
414
+ closed: string;
415
+ emptyTitle: string;
416
+ emptyBody: string;
417
+ screenshot: string;
418
+ notFound: string;
419
+ you: string;
420
+ sending: string;
421
+ notSent: string;
422
+ retryHint: string;
423
+ };
424
+ status: {
425
+ open: string;
426
+ submitted: string;
427
+ in_progress: string;
428
+ addressed: string;
429
+ wont_fix: string;
430
+ };
431
+ ideas: {
432
+ title: string;
433
+ emptyTitle: string;
434
+ emptyBody: string;
435
+ suggest: string;
436
+ suggestTitle: string;
437
+ titleLabel: string;
438
+ titlePlaceholder: string;
439
+ bodyLabel: string;
440
+ submit: string;
441
+ vote: string;
442
+ voted: string;
443
+ /** `{count}` votes. */
444
+ votes: string;
445
+ oneVote: string;
446
+ statuses: {
447
+ open: string;
448
+ planned: string;
449
+ in_progress: string;
450
+ shipped: string;
451
+ declined: string;
452
+ hidden: string;
453
+ };
454
+ };
455
+ shipped: {
456
+ title: string;
457
+ emptyTitle: string;
458
+ emptyBody: string;
459
+ /** `{feature}`: the FeatureId it shipped as. */
460
+ shippedAs: string;
461
+ };
462
+ };
463
+ /** The shape a translation fills. */
464
+ type SupportCopy = typeof supportCopy;
465
+ /** Any part of the catalog. */
466
+ type SupportCopyOverrides = {
467
+ [K in keyof SupportCopy]?: DeepPartial<SupportCopy[K]>;
468
+ };
469
+ type DeepPartial<T> = {
470
+ [K in keyof T]?: T[K] extends string ? string : DeepPartial<T[K]>;
471
+ };
472
+ /** The catalog with overrides merged over the English. */
473
+ declare function resolveSupportCopy(overrides?: SupportCopyOverrides): SupportCopy;
474
+ /** Fills `{name}` placeholders. */
475
+ declare function fill(template: string, values: Readonly<Record<string, string | number>>): string;
476
+
477
+ export { isUnread as $, type SupportCopyOverrides as A, type BootConfig as B, CONTRACT_VERSION as C, DEVICE_MAX_BYTES as D, EXTRA_MAX_BYTES as E, type FeedbackReportSummary as F, SupportError as G, type SupportIdea as H, type IdeaStatus as I, type SupportReply as J, type SupportReport as K, LIMITS as L, type SupportSupabase as M, type WidgetConfig as N, type WidgetType as O, type WidgetTypeWire as P, attachmentBytes as Q, base64ToBytes as R, type SubmitFeedbackInput as S, type ThemeTokens as T, cleanRoute as U, composeCollectors as V, WIDGET_HEADERS as W, createContextCollector as X, createSupabaseSink as Y, createSupportClient as Z, fill as _, type SubmitFeedbackResult as a, resolveSupportCopy as a0, sanitizeContext as a1, supportCopy as a2, validateSubmitInput as a3, type FeedbackSink as b, type ContextCollector as c, type ContextCollectorOptions as d, FEEDBACK_REPORT_TYPES as e, FEEDBACK_STATUSES as f, FEEDBACK_TYPES as g, type FeedbackAttachment as h, type FeedbackContext as i, type FeedbackDelivery as j, type FeedbackOp as k, type FeedbackRecord as l, type FeedbackReportType as m, type FeedbackStatus as n, type FeedbackType as o, type FindingSeverity as p, type IdentityMode as q, type LauncherPosition as r, type Lazy as s, SUPPORT_BUCKET as t, type ScreenshotContentType as u, type SubmitValidation as v, type SupportAttachmentView as w, type SupportClient as x, type SupportClientOptions as y, type SupportCopy as z };
@@ -0,0 +1,43 @@
1
+ import { B as BootConfig, S as SubmitFeedbackInput, a as SubmitFeedbackResult, F as FeedbackReportSummary, b as FeedbackSink } from './copy-CuTArBFg.js';
2
+ export { C as CONTRACT_VERSION, c as ContextCollector, d as ContextCollectorOptions, D as DEVICE_MAX_BYTES, E as EXTRA_MAX_BYTES, e as FEEDBACK_REPORT_TYPES, f as FEEDBACK_STATUSES, g as FEEDBACK_TYPES, h as FeedbackAttachment, i as FeedbackContext, j as FeedbackDelivery, k as FeedbackOp, l as FeedbackRecord, m as FeedbackReportType, n as FeedbackStatus, o as FeedbackType, p as FindingSeverity, I as IdeaStatus, q as IdentityMode, L as LIMITS, r as LauncherPosition, s as Lazy, t as SUPPORT_BUCKET, u as ScreenshotContentType, v as SubmitValidation, w as SupportAttachmentView, x as SupportClient, y as SupportClientOptions, z as SupportCopy, A as SupportCopyOverrides, G as SupportError, H as SupportIdea, J as SupportReply, K as SupportReport, M as SupportSupabase, T as ThemeTokens, W as WIDGET_HEADERS, N as WidgetConfig, O as WidgetType, P as WidgetTypeWire, Q as attachmentBytes, R as base64ToBytes, U as cleanRoute, V as composeCollectors, X as createContextCollector, Y as createSupabaseSink, Z as createSupportClient, _ as fill, $ as isUnread, a0 as resolveSupportCopy, a1 as sanitizeContext, a2 as supportCopy, a3 as validateSubmitInput } from './copy-CuTArBFg.js';
3
+ export { S as SUPPORT_BLOCK_ID, a as SupportBlockOptions, b as SupportPaths, d as defineSupportBlock, s as supportContributions, c as supportPaths } from './block-fZjTS2sY.js';
4
+ import '@supabase/supabase-js';
5
+ import '@marktiderman/genesis-features/blocks';
6
+
7
+ /**
8
+ * The HTTP client for the `genesis-support-intake` Edge Function: the wire
9
+ * contract (boot, submit, list) for code that doesn't hold a supabase-js
10
+ * client, such as a script-tag web loader or another backend. Zero
11
+ * dependencies, global `fetch`.
12
+ */
13
+
14
+ interface IntakeClientConfig {
15
+ /** The app's Supabase URL (`https://<project>.supabase.co`, or the local one). */
16
+ endpoint: string;
17
+ /** The publishable key, sent as `apikey` and `x-genesis-key` (an identifier, not a secret). */
18
+ publishableKey: string;
19
+ /** The signed-in person's access token, read per request. */
20
+ getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
21
+ /** The function's name, if you deployed it under another. */
22
+ functionName?: string;
23
+ fetchImpl?: typeof fetch;
24
+ }
25
+ /** A non-2xx answer (`status` 0: it never arrived). `code` is the function's `error`. */
26
+ declare class IntakeError extends Error {
27
+ readonly status: number;
28
+ readonly code?: string | undefined;
29
+ constructor(message: string, status: number, code?: string | undefined);
30
+ }
31
+ interface IntakeClient {
32
+ boot(signal?: AbortSignal): Promise<BootConfig>;
33
+ submit(input: SubmitFeedbackInput, signal?: AbortSignal): Promise<SubmitFeedbackResult>;
34
+ list(signal?: AbortSignal): Promise<FeedbackReportSummary[]>;
35
+ }
36
+ declare function createIntakeClient(config: IntakeClientConfig): IntakeClient;
37
+ /**
38
+ * A sink over the intake function. The function takes no screenshots (they
39
+ * go to storage with a session), so attachments are reported as dropped.
40
+ */
41
+ declare function createIntakeSink(client: IntakeClient): FeedbackSink;
42
+
43
+ export { BootConfig, FeedbackReportSummary, FeedbackSink, type IntakeClient, type IntakeClientConfig, IntakeError, SubmitFeedbackInput, SubmitFeedbackResult, createIntakeClient, createIntakeSink };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ export { SUPPORT_BLOCK_ID, SUPPORT_BUCKET, SupportError, createSupabaseSink, createSupportClient, defineSupportBlock, isUnread, supportContributions, supportPaths } from './chunk-4VFYVQUF.js';
2
+ import { WIDGET_HEADERS, CONTRACT_VERSION } from './chunk-ZV4KJBUF.js';
3
+ export { CONTRACT_VERSION, DEVICE_MAX_BYTES, EXTRA_MAX_BYTES, FEEDBACK_REPORT_TYPES, FEEDBACK_STATUSES, FEEDBACK_TYPES, LIMITS, WIDGET_HEADERS, attachmentBytes, base64ToBytes, cleanRoute, composeCollectors, createContextCollector, fill, resolveSupportCopy, sanitizeContext, supportCopy, validateSubmitInput } from './chunk-ZV4KJBUF.js';
4
+
5
+ // src/intake.ts
6
+ var IntakeError = class extends Error {
7
+ constructor(message, status, code) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "IntakeError";
12
+ }
13
+ status;
14
+ code;
15
+ };
16
+ function createIntakeClient(config) {
17
+ const url = `${config.endpoint.replace(/\/$/, "")}/functions/v1/${config.functionName ?? "genesis-support-intake"}`;
18
+ const f = config.fetchImpl ?? fetch;
19
+ async function headers() {
20
+ const token = await config.getAccessToken?.();
21
+ return {
22
+ apikey: config.publishableKey,
23
+ [WIDGET_HEADERS.key]: config.publishableKey,
24
+ [WIDGET_HEADERS.contract]: String(CONTRACT_VERSION),
25
+ ...token ? { authorization: `Bearer ${token}` } : {}
26
+ };
27
+ }
28
+ async function send(init) {
29
+ let res;
30
+ try {
31
+ res = await f(url, init);
32
+ } catch (error) {
33
+ throw new IntakeError(error instanceof Error ? error.message : "network request failed", 0, "transport_error");
34
+ }
35
+ if (!res.ok) {
36
+ let code;
37
+ try {
38
+ code = (await res.json()).error;
39
+ } catch {
40
+ }
41
+ throw new IntakeError(code ?? `${res.status} ${res.statusText}`, res.status, code);
42
+ }
43
+ return await res.json();
44
+ }
45
+ const post = async (op, args, signal) => (
46
+ // `op` last, so an `op` in args can never pick the operation.
47
+ send({ method: "POST", headers: { ...await headers(), "content-type": "application/json" }, body: JSON.stringify({ ...args, op }), signal })
48
+ );
49
+ return {
50
+ boot: async (signal) => send({ method: "GET", headers: await headers(), signal }),
51
+ submit: (input, signal) => post("submit", input, signal),
52
+ list: (signal) => post("list", {}, signal)
53
+ };
54
+ }
55
+ function createIntakeSink(client) {
56
+ return {
57
+ async submit(record) {
58
+ const result = await client.submit({
59
+ type: record.type,
60
+ title: record.title,
61
+ body: record.message,
62
+ route: record.context.route,
63
+ appVersion: record.context.appVersion,
64
+ envType: record.context.platform,
65
+ context: record.context.extra
66
+ });
67
+ return { id: result.reportId, attachmentsDropped: record.attachments?.length ?? 0 };
68
+ }
69
+ };
70
+ }
71
+
72
+ export { IntakeError, createIntakeClient, createIntakeSink };
73
+ //# sourceMappingURL=index.js.map
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/intake.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The HTTP client for the `genesis-support-intake` Edge Function: the wire\n * contract (boot, submit, list) for code that doesn't hold a supabase-js\n * client, such as a script-tag web loader or another backend. Zero\n * dependencies, global `fetch`.\n */\nimport {\n CONTRACT_VERSION,\n WIDGET_HEADERS,\n type BootConfig,\n type FeedbackOp,\n type FeedbackReportSummary,\n type SubmitFeedbackInput,\n type SubmitFeedbackResult,\n} from \"./contract\";\nimport type { FeedbackSink } from \"./record\";\n\nexport interface IntakeClientConfig {\n /** The app's Supabase URL (`https://<project>.supabase.co`, or the local one). */\n endpoint: string;\n /** The publishable key, sent as `apikey` and `x-genesis-key` (an identifier, not a secret). */\n publishableKey: string;\n /** The signed-in person's access token, read per request. */\n getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;\n /** The function's name, if you deployed it under another. */\n functionName?: string;\n fetchImpl?: typeof fetch;\n}\n\n/** A non-2xx answer (`status` 0: it never arrived). `code` is the function's `error`. */\nexport class IntakeError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"IntakeError\";\n }\n}\n\nexport interface IntakeClient {\n boot(signal?: AbortSignal): Promise<BootConfig>;\n submit(input: SubmitFeedbackInput, signal?: AbortSignal): Promise<SubmitFeedbackResult>;\n list(signal?: AbortSignal): Promise<FeedbackReportSummary[]>;\n}\n\nexport function createIntakeClient(config: IntakeClientConfig): IntakeClient {\n const url = `${config.endpoint.replace(/\\/$/, \"\")}/functions/v1/${config.functionName ?? \"genesis-support-intake\"}`;\n const f = config.fetchImpl ?? fetch;\n\n async function headers(): Promise<Record<string, string>> {\n const token = await config.getAccessToken?.();\n return {\n apikey: config.publishableKey,\n [WIDGET_HEADERS.key]: config.publishableKey,\n [WIDGET_HEADERS.contract]: String(CONTRACT_VERSION),\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n };\n }\n\n async function send<T>(init: RequestInit): Promise<T> {\n let res: Response;\n try {\n res = await f(url, init);\n } catch (error) {\n throw new IntakeError(error instanceof Error ? error.message : \"network request failed\", 0, \"transport_error\");\n }\n if (!res.ok) {\n let code: string | undefined;\n try {\n code = ((await res.json()) as { error?: string }).error;\n } catch {\n // not JSON\n }\n throw new IntakeError(code ?? `${res.status} ${res.statusText}`, res.status, code);\n }\n return (await res.json()) as T;\n }\n\n const post = async <T,>(op: FeedbackOp, args: object, signal?: AbortSignal) =>\n // `op` last, so an `op` in args can never pick the operation.\n send<T>({ method: \"POST\", headers: { ...(await headers()), \"content-type\": \"application/json\" }, body: JSON.stringify({ ...args, op }), signal });\n\n return {\n boot: async (signal) => send<BootConfig>({ method: \"GET\", headers: await headers(), signal }),\n submit: (input, signal) => post<SubmitFeedbackResult>(\"submit\", input, signal),\n list: (signal) => post<FeedbackReportSummary[]>(\"list\", {}, signal),\n };\n}\n\n/**\n * A sink over the intake function. The function takes no screenshots (they\n * go to storage with a session), so attachments are reported as dropped.\n */\nexport function createIntakeSink(client: IntakeClient): FeedbackSink {\n return {\n async submit(record) {\n const result = await client.submit({\n type: record.type,\n title: record.title,\n body: record.message,\n route: record.context.route,\n appVersion: record.context.appVersion,\n envType: record.context.platform,\n context: record.context.extra,\n });\n return { id: result.reportId, attachmentsDropped: record.attachments?.length ?? 0 };\n },\n };\n}\n"]}