@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.
- package/LICENSE +21 -0
- package/README.md +130 -2
- package/dist/block-fZjTS2sY.d.ts +39 -0
- package/dist/chunk-4VFYVQUF.js +278 -0
- package/dist/chunk-4VFYVQUF.js.map +1 -0
- package/dist/chunk-ZV4KJBUF.js +284 -0
- package/dist/chunk-ZV4KJBUF.js.map +1 -0
- package/dist/copy-CuTArBFg.d.ts +477 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +207 -0
- package/dist/native.js +722 -0
- package/dist/native.js.map +1 -0
- package/dist/web.d.ts +137 -0
- package/dist/web.js +484 -0
- package/dist/web.js.map +1 -0
- package/package.json +252 -3
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
var CONTRACT_VERSION = 1;
|
|
3
|
+
var FEEDBACK_REPORT_TYPES = ["bug", "idea", "question", "praise"];
|
|
4
|
+
var FEEDBACK_STATUSES = ["open", "submitted", "in_progress", "addressed", "wont_fix"];
|
|
5
|
+
var SEVERITIES = ["low", "medium", "high", "critical"];
|
|
6
|
+
var WIDGET_HEADERS = {
|
|
7
|
+
/** The project's publishable key (an identifier, not a secret). */
|
|
8
|
+
key: "x-genesis-key",
|
|
9
|
+
/** `signed` mode: the minted short-lived token. */
|
|
10
|
+
identity: "x-genesis-identity",
|
|
11
|
+
/** `anonymous` mode: the device id. */
|
|
12
|
+
device: "x-genesis-device",
|
|
13
|
+
/** A proof-of-work or captcha token, for anonymous writes. */
|
|
14
|
+
challenge: "x-genesis-challenge",
|
|
15
|
+
/** The contract version the client was built against. */
|
|
16
|
+
contract: "x-genesis-contract"
|
|
17
|
+
};
|
|
18
|
+
var LIMITS = { title: 300, body: 2e4, route: 2e3, appVersion: 200, tag: 60, tags: 50, contextBytes: 16384 };
|
|
19
|
+
function privateSegment(segment) {
|
|
20
|
+
let s = segment;
|
|
21
|
+
try {
|
|
22
|
+
s = decodeURIComponent(segment);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
if (s.includes("@")) return ":email";
|
|
26
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) return ":id";
|
|
27
|
+
if (/^\d+$/.test(s)) return ":id";
|
|
28
|
+
if (s.length >= 16 && /\d/.test(s) && /^[A-Za-z0-9_.~-]+$/.test(s)) return ":token";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function cleanRoute(route) {
|
|
32
|
+
if (typeof route !== "string") return void 0;
|
|
33
|
+
const path = route.replace(/[?#].*$/, "").trim();
|
|
34
|
+
if (!path) return void 0;
|
|
35
|
+
const safe = path.split("/").map((segment) => segment ? privateSegment(segment) ?? segment : segment).join("/");
|
|
36
|
+
return safe.slice(0, LIMITS.route);
|
|
37
|
+
}
|
|
38
|
+
var byteLength = (s) => new TextEncoder().encode(s).length;
|
|
39
|
+
function validateSubmitInput(raw) {
|
|
40
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "invalid_args", field: "body" };
|
|
41
|
+
const input = raw;
|
|
42
|
+
const known = /* @__PURE__ */ new Set(["type", "title", "body", "route", "appVersion", "envType", "severity", "tags", "context"]);
|
|
43
|
+
for (const key of Object.keys(input)) if (!known.has(key)) return { ok: false, error: "invalid_args", field: key };
|
|
44
|
+
const optionalString = (key, max) => input[key] == null || typeof input[key] === "string" && input[key].length <= max;
|
|
45
|
+
if (typeof input.body !== "string" || !input.body.trim()) return { ok: false, error: "invalid_args", field: "body" };
|
|
46
|
+
if (input.body.length > LIMITS.body) return { ok: false, error: "payload_too_large", field: "body" };
|
|
47
|
+
if (input.type != null && !FEEDBACK_REPORT_TYPES.includes(input.type)) return { ok: false, error: "invalid_args", field: "type" };
|
|
48
|
+
if (input.severity != null && !SEVERITIES.includes(input.severity)) return { ok: false, error: "invalid_args", field: "severity" };
|
|
49
|
+
if (!optionalString("title", LIMITS.title)) return { ok: false, error: "invalid_args", field: "title" };
|
|
50
|
+
if (!optionalString("route", LIMITS.route)) return { ok: false, error: "invalid_args", field: "route" };
|
|
51
|
+
if (!optionalString("appVersion", LIMITS.appVersion)) return { ok: false, error: "invalid_args", field: "appVersion" };
|
|
52
|
+
if (!optionalString("envType", 60)) return { ok: false, error: "invalid_args", field: "envType" };
|
|
53
|
+
if (input.tags != null) {
|
|
54
|
+
if (!Array.isArray(input.tags) || input.tags.length > LIMITS.tags) return { ok: false, error: "invalid_args", field: "tags" };
|
|
55
|
+
if (!input.tags.every((t) => typeof t === "string" && t.length <= LIMITS.tag)) return { ok: false, error: "invalid_args", field: "tags" };
|
|
56
|
+
}
|
|
57
|
+
if (input.context != null) {
|
|
58
|
+
if (typeof input.context !== "object" || Array.isArray(input.context)) return { ok: false, error: "invalid_args", field: "context" };
|
|
59
|
+
if (byteLength(JSON.stringify(input.context)) > LIMITS.contextBytes) return { ok: false, error: "payload_too_large", field: "context" };
|
|
60
|
+
}
|
|
61
|
+
const value = { body: input.body };
|
|
62
|
+
if (input.type != null) value.type = input.type;
|
|
63
|
+
if (input.title != null) value.title = input.title;
|
|
64
|
+
const route = cleanRoute(input.route);
|
|
65
|
+
if (route) value.route = route;
|
|
66
|
+
if (input.appVersion != null) value.appVersion = input.appVersion;
|
|
67
|
+
if (input.envType != null) value.envType = input.envType;
|
|
68
|
+
if (input.severity != null) value.severity = input.severity;
|
|
69
|
+
if (input.tags != null) value.tags = input.tags;
|
|
70
|
+
if (input.context != null) value.context = input.context;
|
|
71
|
+
return { ok: true, value };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/record.ts
|
|
75
|
+
var FEEDBACK_TYPES = ["bug", "idea", "question", "praise"];
|
|
76
|
+
var DEVICE_MAX_BYTES = 4096;
|
|
77
|
+
var EXTRA_MAX_BYTES = 16384;
|
|
78
|
+
var jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
79
|
+
var read = (value) => typeof value === "function" ? value() : value;
|
|
80
|
+
function createContextCollector(options) {
|
|
81
|
+
return {
|
|
82
|
+
collect() {
|
|
83
|
+
return sanitizeContext({
|
|
84
|
+
appVersion: read(options.appVersion),
|
|
85
|
+
route: read(options.route),
|
|
86
|
+
platform: read(options.platform),
|
|
87
|
+
device: read(options.device),
|
|
88
|
+
userId: read(options.userId),
|
|
89
|
+
extra: read(options.extra)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function composeCollectors(...collectors) {
|
|
95
|
+
return {
|
|
96
|
+
async collect() {
|
|
97
|
+
let out = {};
|
|
98
|
+
for (const c of collectors) {
|
|
99
|
+
const next = await c.collect();
|
|
100
|
+
out = {
|
|
101
|
+
...out,
|
|
102
|
+
...dropUndefined(next),
|
|
103
|
+
device: next.device || out.device ? { ...out.device, ...next.device } : void 0,
|
|
104
|
+
extra: next.extra || out.extra ? { ...out.extra, ...next.extra } : void 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return sanitizeContext(out);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function dropUndefined(value) {
|
|
112
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
113
|
+
}
|
|
114
|
+
function sanitizeContext(context) {
|
|
115
|
+
const out = {};
|
|
116
|
+
const route = cleanRoute(context.route);
|
|
117
|
+
if (route) out.route = route;
|
|
118
|
+
if (context.appVersion) out.appVersion = String(context.appVersion).slice(0, 200);
|
|
119
|
+
if (context.platform) out.platform = String(context.platform).slice(0, 40);
|
|
120
|
+
if (context.userId) out.userId = context.userId;
|
|
121
|
+
if (context.device && typeof context.device === "object" && jsonBytes(context.device) <= DEVICE_MAX_BYTES) out.device = context.device;
|
|
122
|
+
if (context.extra && typeof context.extra === "object" && jsonBytes(context.extra) <= EXTRA_MAX_BYTES) out.extra = context.extra;
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
126
|
+
var LOOKUP = (() => {
|
|
127
|
+
const table = new Int16Array(256).fill(-1);
|
|
128
|
+
for (let i = 0; i < B64.length; i++) table[B64.charCodeAt(i)] = i;
|
|
129
|
+
table["-".charCodeAt(0)] = 62;
|
|
130
|
+
table["_".charCodeAt(0)] = 63;
|
|
131
|
+
return table;
|
|
132
|
+
})();
|
|
133
|
+
function base64ToBytes(input) {
|
|
134
|
+
const comma = input.startsWith("data:") ? input.indexOf(",") : -1;
|
|
135
|
+
const clean = (comma >= 0 ? input.slice(comma + 1) : input).replace(/[\s=]/g, "");
|
|
136
|
+
const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
|
|
137
|
+
let buffer = 0;
|
|
138
|
+
let bits = 0;
|
|
139
|
+
let o = 0;
|
|
140
|
+
for (let i = 0; i < clean.length; i++) {
|
|
141
|
+
const v = LOOKUP[clean.charCodeAt(i)] ?? -1;
|
|
142
|
+
if (v < 0) throw new Error("base64ToBytes: not base64");
|
|
143
|
+
buffer = buffer << 6 | v;
|
|
144
|
+
bits += 6;
|
|
145
|
+
if (bits >= 8) {
|
|
146
|
+
bits -= 8;
|
|
147
|
+
out[o++] = buffer >> bits & 255;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out.subarray(0, o);
|
|
151
|
+
}
|
|
152
|
+
function attachmentBytes(attachment) {
|
|
153
|
+
if (attachment.bytes?.length) return attachment.bytes;
|
|
154
|
+
if (attachment.base64) return base64ToBytes(attachment.base64);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/copy.ts
|
|
159
|
+
var supportCopy = {
|
|
160
|
+
settings: {
|
|
161
|
+
section: "Support",
|
|
162
|
+
sendFeedback: "Send feedback",
|
|
163
|
+
help: "Help",
|
|
164
|
+
/** `{count}` new replies. */
|
|
165
|
+
unreadReplies: "{count} new"
|
|
166
|
+
},
|
|
167
|
+
feedback: {
|
|
168
|
+
title: "Send feedback",
|
|
169
|
+
typeLabel: "What is it?",
|
|
170
|
+
types: { bug: "Bug", idea: "Idea", question: "Question", praise: "Praise" },
|
|
171
|
+
messageLabel: "Tell us more",
|
|
172
|
+
messagePlaceholder: "What happened, and what did you expect?",
|
|
173
|
+
screenshotLabel: "Screenshot",
|
|
174
|
+
includeScreenshot: "Include this screenshot",
|
|
175
|
+
screenshotHint: "Sent only if you include it. Mark it up to hide anything private first.",
|
|
176
|
+
annotate: "Mark up",
|
|
177
|
+
pin: "Point to it on the page",
|
|
178
|
+
pinned: "Pinned",
|
|
179
|
+
removeScreenshot: "Remove",
|
|
180
|
+
send: "Send",
|
|
181
|
+
sending: "Sending",
|
|
182
|
+
sentTitle: "Thanks for the feedback",
|
|
183
|
+
sentBody: "We read every report. You'll find it, and our replies, under Help.",
|
|
184
|
+
sentWithoutScreenshot: "Your report was sent, but its screenshot couldn't be.",
|
|
185
|
+
failedTitle: "Your feedback wasn't sent",
|
|
186
|
+
failedBody: "Check your connection and try again.",
|
|
187
|
+
rateLimited: "You've sent a lot of feedback in a short time. Try again later.",
|
|
188
|
+
signInFirst: "Sign in to send feedback.",
|
|
189
|
+
messageRequired: "Write a few words first."
|
|
190
|
+
},
|
|
191
|
+
annotate: {
|
|
192
|
+
title: "Mark up",
|
|
193
|
+
pen: "Pen",
|
|
194
|
+
hide: "Hide",
|
|
195
|
+
undo: "Undo",
|
|
196
|
+
done: "Done",
|
|
197
|
+
cancel: "Cancel",
|
|
198
|
+
hint: "Draw to point something out. Use Hide to cover anything private.",
|
|
199
|
+
failed: "The marks couldn't be saved. Try again, or cancel and leave the screenshot out."
|
|
200
|
+
},
|
|
201
|
+
inbox: {
|
|
202
|
+
title: "Help",
|
|
203
|
+
sectionTitle: "Your reports",
|
|
204
|
+
emptyTitle: "No reports yet",
|
|
205
|
+
emptyBody: "When you send feedback it shows up here, with our replies.",
|
|
206
|
+
errorTitle: "Couldn't load your reports",
|
|
207
|
+
errorBody: "Check your connection, then pull to refresh.",
|
|
208
|
+
sendFeedback: "Send feedback",
|
|
209
|
+
ideaBoard: "Idea board",
|
|
210
|
+
shipped: "You asked, we shipped",
|
|
211
|
+
delete: "Delete",
|
|
212
|
+
deleteConfirmTitle: "Delete this report?",
|
|
213
|
+
deleteConfirmBody: "The report, its replies and its screenshots are deleted and can't be opened again.",
|
|
214
|
+
cancel: "Cancel",
|
|
215
|
+
unread: "New reply"
|
|
216
|
+
},
|
|
217
|
+
thread: {
|
|
218
|
+
title: "Report",
|
|
219
|
+
placeholder: "Reply",
|
|
220
|
+
send: "Send",
|
|
221
|
+
read: "Read",
|
|
222
|
+
support: "Support",
|
|
223
|
+
closed: "This report is closed. Send new feedback if something's still wrong.",
|
|
224
|
+
emptyTitle: "No replies yet",
|
|
225
|
+
emptyBody: "We'll answer here.",
|
|
226
|
+
screenshot: "Screenshot",
|
|
227
|
+
notFound: "That report isn't there any more.",
|
|
228
|
+
you: "You",
|
|
229
|
+
sending: "Sending",
|
|
230
|
+
notSent: "Not sent. Tap to retry.",
|
|
231
|
+
retryHint: "Double tap to send again"
|
|
232
|
+
},
|
|
233
|
+
status: {
|
|
234
|
+
open: "Open",
|
|
235
|
+
submitted: "Received",
|
|
236
|
+
in_progress: "In progress",
|
|
237
|
+
addressed: "Fixed",
|
|
238
|
+
wont_fix: "Closed"
|
|
239
|
+
},
|
|
240
|
+
ideas: {
|
|
241
|
+
title: "Idea board",
|
|
242
|
+
emptyTitle: "No ideas yet",
|
|
243
|
+
emptyBody: "Suggest the first one.",
|
|
244
|
+
suggest: "Suggest an idea",
|
|
245
|
+
suggestTitle: "Suggest an idea",
|
|
246
|
+
titleLabel: "Your idea",
|
|
247
|
+
titlePlaceholder: "A short title",
|
|
248
|
+
bodyLabel: "Why it would help (optional)",
|
|
249
|
+
submit: "Suggest",
|
|
250
|
+
vote: "Vote",
|
|
251
|
+
voted: "Voted",
|
|
252
|
+
/** `{count}` votes. */
|
|
253
|
+
votes: "{count} votes",
|
|
254
|
+
oneVote: "1 vote",
|
|
255
|
+
statuses: { open: "Open", planned: "Planned", in_progress: "In progress", shipped: "Shipped", declined: "Not planned", hidden: "Hidden" }
|
|
256
|
+
},
|
|
257
|
+
shipped: {
|
|
258
|
+
title: "You asked, we shipped",
|
|
259
|
+
emptyTitle: "Nothing yet",
|
|
260
|
+
emptyBody: "When an idea you suggested or voted for ships, it shows up here.",
|
|
261
|
+
/** `{feature}`: the FeatureId it shipped as. */
|
|
262
|
+
shippedAs: "Shipped as {feature}"
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
function resolveSupportCopy(overrides = {}) {
|
|
266
|
+
const merge = (base, over) => {
|
|
267
|
+
if (!over) return base;
|
|
268
|
+
const out = { ...base };
|
|
269
|
+
for (const [key, value] of Object.entries(over)) {
|
|
270
|
+
if (value === void 0) continue;
|
|
271
|
+
const current = base[key];
|
|
272
|
+
out[key] = current && typeof current === "object" && value && typeof value === "object" ? merge(current, value) : value;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
};
|
|
276
|
+
return merge(supportCopy, overrides);
|
|
277
|
+
}
|
|
278
|
+
function fill(template, values) {
|
|
279
|
+
return template.replace(/\{(\w+)\}/g, (token, name) => values[name] !== void 0 ? String(values[name]) : token);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
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 };
|
|
283
|
+
//# sourceMappingURL=chunk-ZV4KJBUF.js.map
|
|
284
|
+
//# sourceMappingURL=chunk-ZV4KJBUF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contract.ts","../src/record.ts","../src/copy.ts"],"names":[],"mappings":"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","file":"chunk-ZV4KJBUF.js","sourcesContent":["/**\n * The Genesis support wire contract: what a support client (the web loader, a\n * native app, any HTTP caller) and the `genesis-support-intake` Edge Function\n * say to each other. One file, kept byte-identical in two places:\n * `@marktiderman/genesis-support` (`src/contract.ts`) and the function template\n * (`functions/genesis-support-intake/contract.ts`); a test fails when they drift.\n *\n * Dependency-free and additive: `CONTRACT_VERSION` changes only with a shape\n * change, and a reader tolerates fields and widget types it doesn't know, so an\n * old client keeps working.\n */\n\n/** Bumped only on a shape change. Old clients must keep working. */\nexport const CONTRACT_VERSION = 1 as const;\n\n/** The widget kinds a boot can turn on. */\nexport type WidgetType = \"feedback\" | \"roadmap\" | \"changelog\" | \"survey\" | \"messaging\";\n\n/** On the wire the type is open: a client switches on the known ones and ignores the rest. */\nexport type WidgetTypeWire = WidgetType | (string & {});\n\n/**\n * How the person is known to the server.\n * - `genesis-session`: signed in to the app's own Supabase (a user access token). The default.\n * - `signed`: the app's backend mints a short-lived token for the person (reserved).\n * - `anonymous`: a device id only (reserved; the database refuses writes without a session).\n */\nexport type IdentityMode = \"anonymous\" | \"signed\" | \"genesis-session\";\n\nexport type LauncherPosition = \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\";\n\n/** Theme hints a web launcher may use. Genesis apps use their own theme instead. */\nexport interface ThemeTokens {\n colorBg?: string;\n colorFg?: string;\n colorPrimary?: string;\n colorPrimaryFg?: string;\n radius?: string;\n fontFamily?: string;\n launcherLabel?: string;\n launcherPosition?: LauncherPosition;\n}\n\n/** One enabled widget and its public config (never a secret). */\nexport interface WidgetConfig {\n type: WidgetTypeWire;\n config: Record<string, unknown>;\n}\n\n/** The boot handshake (`GET`): world-readable, so only public values. */\nexport interface BootConfig {\n contractVersion: number;\n project: { id: string; name: string };\n identityMode: IdentityMode;\n theme: ThemeTokens;\n widgets: WidgetConfig[];\n flags: Record<string, boolean>;\n}\n\nexport type FeedbackReportType = \"bug\" | \"idea\" | \"question\" | \"praise\";\nexport type FeedbackStatus = \"open\" | \"submitted\" | \"in_progress\" | \"addressed\" | \"wont_fix\";\nexport type FindingSeverity = \"low\" | \"medium\" | \"high\" | \"critical\";\n\nexport const FEEDBACK_REPORT_TYPES: readonly FeedbackReportType[] = [\"bug\", \"idea\", \"question\", \"praise\"];\nexport const FEEDBACK_STATUSES: readonly FeedbackStatus[] = [\"open\", \"submitted\", \"in_progress\", \"addressed\", \"wont_fix\"];\nconst SEVERITIES: readonly FindingSeverity[] = [\"low\", \"medium\", \"high\", \"critical\"];\n\n/** Client to server: open a report. Screenshots go to storage, not in this body. */\nexport interface SubmitFeedbackInput {\n type?: FeedbackReportType;\n title?: string;\n body: string;\n route?: string;\n appVersion?: string;\n envType?: string;\n severity?: FindingSeverity;\n tags?: string[];\n /** Small, free-form context (breadcrumbs, a pin on the page). At most 16 KB as JSON. */\n context?: Record<string, unknown>;\n}\n\n/** Server to client: the report created. `findingId` is the report's id (one finding per report here). */\nexport interface SubmitFeedbackResult {\n reportId: string;\n findingId: string;\n status: FeedbackStatus;\n}\n\n/** One of the caller's own reports. */\nexport interface FeedbackReportSummary {\n id: string;\n type: FeedbackReportType;\n title: string;\n status: FeedbackStatus;\n route: string | null;\n createdAt: string;\n findingCount: number;\n}\n\n/** The operations the intake function multiplexes on (`{ op, ...args }`). */\nexport type FeedbackOp = \"submit\" | \"list\";\n\n/** Headers a client sends. The person's access token goes in `Authorization: Bearer …`. */\nexport const WIDGET_HEADERS = {\n /** The project's publishable key (an identifier, not a secret). */\n key: \"x-genesis-key\",\n /** `signed` mode: the minted short-lived token. */\n identity: \"x-genesis-identity\",\n /** `anonymous` mode: the device id. */\n device: \"x-genesis-device\",\n /** A proof-of-work or captcha token, for anonymous writes. */\n challenge: \"x-genesis-challenge\",\n /** The contract version the client was built against. */\n contract: \"x-genesis-contract\",\n} as const;\n\n/** Text limits, the same as the database's checks. */\nexport const LIMITS = { title: 300, body: 20000, route: 2000, appVersion: 200, tag: 60, tags: 50, contextBytes: 16384 } as const;\n\n/** A path segment that names someone or something private: an email, a UUID, a number, a long token. */\nfunction privateSegment(segment: string): string | null {\n let s = segment;\n try {\n s = decodeURIComponent(segment);\n } catch {\n // a malformed escape: judge it as written\n }\n if (s.includes(\"@\")) return \":email\";\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) return \":id\";\n if (/^\\d+$/.test(s)) return \":id\";\n // A long run of letters and digits with a digit in it reads as a token or a code, not a word.\n if (s.length >= 16 && /\\d/.test(s) && /^[A-Za-z0-9_.~-]+$/.test(s)) return \":token\";\n return null;\n}\n\n/**\n * A route as a path only, safe to store and send: the query string and\n * fragment (tokens, codes, emails) are dropped, and path segments that look\n * private (an email, an id, a long token) become `:email`, `:id` or `:token`\n * (`/users/jane@x.test/reset/abc123…` → `/users/:email/reset/:token`).\n */\nexport function cleanRoute(route: string | null | undefined): string | undefined {\n if (typeof route !== \"string\") return undefined;\n const path = route.replace(/[?#].*$/, \"\").trim();\n if (!path) return undefined;\n const safe = path\n .split(\"/\")\n .map((segment) => (segment ? (privateSegment(segment) ?? segment) : segment))\n .join(\"/\");\n return safe.slice(0, LIMITS.route);\n}\n\nexport type SubmitValidation =\n | { ok: true; value: SubmitFeedbackInput }\n | { ok: false; error: \"invalid_args\" | \"payload_too_large\"; field: string };\n\nconst byteLength = (s: string) => new TextEncoder().encode(s).length;\n\n/** Checks a `submit` body the way the server does: known keys only, every limit. */\nexport function validateSubmitInput(raw: unknown): SubmitValidation {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return { ok: false, error: \"invalid_args\", field: \"body\" };\n const input = raw as Record<string, unknown>;\n const known = new Set([\"type\", \"title\", \"body\", \"route\", \"appVersion\", \"envType\", \"severity\", \"tags\", \"context\"]);\n for (const key of Object.keys(input)) if (!known.has(key)) return { ok: false, error: \"invalid_args\", field: key };\n const optionalString = (key: string, max: number): boolean => input[key] == null || (typeof input[key] === \"string\" && (input[key] as string).length <= max);\n if (typeof input.body !== \"string\" || !input.body.trim()) return { ok: false, error: \"invalid_args\", field: \"body\" };\n if (input.body.length > LIMITS.body) return { ok: false, error: \"payload_too_large\", field: \"body\" };\n if (input.type != null && !FEEDBACK_REPORT_TYPES.includes(input.type as FeedbackReportType)) return { ok: false, error: \"invalid_args\", field: \"type\" };\n if (input.severity != null && !SEVERITIES.includes(input.severity as FindingSeverity)) return { ok: false, error: \"invalid_args\", field: \"severity\" };\n if (!optionalString(\"title\", LIMITS.title)) return { ok: false, error: \"invalid_args\", field: \"title\" };\n if (!optionalString(\"route\", LIMITS.route)) return { ok: false, error: \"invalid_args\", field: \"route\" };\n if (!optionalString(\"appVersion\", LIMITS.appVersion)) return { ok: false, error: \"invalid_args\", field: \"appVersion\" };\n if (!optionalString(\"envType\", 60)) return { ok: false, error: \"invalid_args\", field: \"envType\" };\n if (input.tags != null) {\n if (!Array.isArray(input.tags) || input.tags.length > LIMITS.tags) return { ok: false, error: \"invalid_args\", field: \"tags\" };\n if (!input.tags.every((t) => typeof t === \"string\" && t.length <= LIMITS.tag)) return { ok: false, error: \"invalid_args\", field: \"tags\" };\n }\n if (input.context != null) {\n if (typeof input.context !== \"object\" || Array.isArray(input.context)) return { ok: false, error: \"invalid_args\", field: \"context\" };\n if (byteLength(JSON.stringify(input.context)) > LIMITS.contextBytes) return { ok: false, error: \"payload_too_large\", field: \"context\" };\n }\n const value: SubmitFeedbackInput = { body: input.body };\n if (input.type != null) value.type = input.type as FeedbackReportType;\n if (input.title != null) value.title = input.title as string;\n const route = cleanRoute(input.route as string | undefined);\n if (route) value.route = route;\n if (input.appVersion != null) value.appVersion = input.appVersion as string;\n if (input.envType != null) value.envType = input.envType as string;\n if (input.severity != null) value.severity = input.severity as FindingSeverity;\n if (input.tags != null) value.tags = input.tags as string[];\n if (input.context != null) value.context = input.context as Record<string, unknown>;\n return { ok: true, value };\n}\n","/**\n * The feedback record and its two seams: a collector assembles the context\n * (everything except what the person typed), a sink delivers the finished\n * record. Platform-neutral; one default of each ships (`createContextCollector`,\n * `createSupabaseSink`), and an app can swap either.\n */\nimport { cleanRoute, type FeedbackReportType } from \"./contract\";\n\n/** What kind of feedback: a bug, an idea, a question or praise. */\nexport type FeedbackType = FeedbackReportType;\n\nexport const FEEDBACK_TYPES: readonly FeedbackType[] = [\"bug\", \"idea\", \"question\", \"praise\"];\n\n/** Where the person was and what they were running when they sent it. */\nexport interface FeedbackContext {\n /** The version signature, `1.4.0 (37) · preview · 3f2a9c1b`. */\n appVersion?: string;\n /** The screen, as a path. Query strings and fragments are always dropped. */\n route?: string;\n /** `ios`, `android`, `web`. */\n platform?: string;\n /** Model, OS version, locale, screen size, update id… At most 4 KB as JSON. */\n device?: Record<string, unknown>;\n /**\n * Who sent it, for sinks that need it. The Supabase sink ignores it: the\n * database takes the person from the session, never from the request.\n */\n userId?: string;\n /** Anything else small (breadcrumbs, a pin on the page). At most 16 KB as JSON. */\n extra?: Record<string, unknown>;\n}\n\nexport type ScreenshotContentType = \"image/png\" | \"image/jpeg\" | \"image/webp\";\n\n/**\n * A screenshot to attach. The record carries the bytes (or base64) and no\n * URL: the destination stores it and keeps its own key, so nothing in the\n * record expires.\n */\nexport interface FeedbackAttachment {\n kind: \"screenshot\";\n contentType: ScreenshotContentType;\n /** The image, base64 (no `data:` prefix; a data URI is accepted and stripped). */\n base64?: string;\n /** Or the bytes. */\n bytes?: Uint8Array;\n width?: number;\n height?: number;\n}\n\nexport interface FeedbackRecord {\n type: FeedbackType;\n /** What the person wrote. */\n message: string;\n title?: string;\n context: FeedbackContext;\n attachments?: FeedbackAttachment[];\n}\n\n/** Everything but what the person typed. */\nexport interface ContextCollector {\n collect(): Promise<FeedbackContext> | FeedbackContext;\n}\n\n/** What the destination kept. */\nexport interface FeedbackDelivery {\n /** The saved report's id. */\n id: string;\n /** How many attachments the destination saved the report without (an upload failed, or it takes none). */\n attachmentsDropped: number;\n}\n\n/** Takes a finished record to its destination. */\nexport interface FeedbackSink {\n submit(record: FeedbackRecord): Promise<FeedbackDelivery>;\n}\n\nexport const DEVICE_MAX_BYTES = 4096;\nexport const EXTRA_MAX_BYTES = 16384;\n\nconst jsonBytes = (value: unknown) => new TextEncoder().encode(JSON.stringify(value)).length;\n\n/** A value given as itself or as a function that reads it now. */\nexport type Lazy<T> = T | (() => T);\nconst read = <T,>(value: Lazy<T> | undefined): T | undefined => (typeof value === \"function\" ? (value as () => T)() : value);\n\nexport interface ContextCollectorOptions {\n appVersion?: Lazy<string | undefined>;\n route?: Lazy<string | undefined>;\n platform?: Lazy<string | undefined>;\n device?: Lazy<Record<string, unknown> | undefined>;\n userId?: Lazy<string | undefined>;\n extra?: Lazy<Record<string, unknown> | undefined>;\n}\n\n/**\n * The default collector: reads each value when feedback is sent, cleans the\n * route, and drops a device or extra bag over its size cap rather than\n * sending a report the database would refuse.\n */\nexport function createContextCollector(options: ContextCollectorOptions): ContextCollector {\n return {\n collect() {\n return sanitizeContext({\n appVersion: read(options.appVersion),\n route: read(options.route),\n platform: read(options.platform),\n device: read(options.device),\n userId: read(options.userId),\n extra: read(options.extra),\n });\n },\n };\n}\n\n/** Collectors merged left to right (a later one's values win; `device` and `extra` merge by key). */\nexport function composeCollectors(...collectors: ContextCollector[]): ContextCollector {\n return {\n async collect() {\n let out: FeedbackContext = {};\n for (const c of collectors) {\n const next = await c.collect();\n out = {\n ...out,\n ...dropUndefined(next),\n device: next.device || out.device ? { ...out.device, ...next.device } : undefined,\n extra: next.extra || out.extra ? { ...out.extra, ...next.extra } : undefined,\n };\n }\n return sanitizeContext(out);\n },\n };\n}\n\nfunction dropUndefined<T extends object>(value: T): Partial<T> {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\n/** The context as the database takes it: a clean route, short strings, bags under their caps. */\nexport function sanitizeContext(context: FeedbackContext): FeedbackContext {\n const out: FeedbackContext = {};\n const route = cleanRoute(context.route);\n if (route) out.route = route;\n if (context.appVersion) out.appVersion = String(context.appVersion).slice(0, 200);\n if (context.platform) out.platform = String(context.platform).slice(0, 40);\n if (context.userId) out.userId = context.userId;\n if (context.device && typeof context.device === \"object\" && jsonBytes(context.device) <= DEVICE_MAX_BYTES) out.device = context.device;\n if (context.extra && typeof context.extra === \"object\" && jsonBytes(context.extra) <= EXTRA_MAX_BYTES) out.extra = context.extra;\n return out;\n}\n\nconst B64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\nconst LOOKUP = (() => {\n const table = new Int16Array(256).fill(-1);\n for (let i = 0; i < B64.length; i++) table[B64.charCodeAt(i)] = i;\n table[\"-\".charCodeAt(0)] = 62;\n table[\"_\".charCodeAt(0)] = 63;\n return table;\n})();\n\n/** Base64 (or a `data:` URI) to bytes, without `atob` (Hermes, workers, Node all agree). */\nexport function base64ToBytes(input: string): Uint8Array {\n const comma = input.startsWith(\"data:\") ? input.indexOf(\",\") : -1;\n const clean = (comma >= 0 ? input.slice(comma + 1) : input).replace(/[\\s=]/g, \"\");\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let buffer = 0;\n let bits = 0;\n let o = 0;\n for (let i = 0; i < clean.length; i++) {\n const v = LOOKUP[clean.charCodeAt(i)] ?? -1;\n if (v < 0) throw new Error(\"base64ToBytes: not base64\");\n buffer = (buffer << 6) | v;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[o++] = (buffer >> bits) & 0xff;\n }\n }\n return out.subarray(0, o);\n}\n\n/** The bytes of an attachment, from `bytes` or `base64`. */\nexport function attachmentBytes(attachment: FeedbackAttachment): Uint8Array | null {\n if (attachment.bytes?.length) return attachment.bytes;\n if (attachment.base64) return base64ToBytes(attachment.base64);\n return null;\n}\n","/**\n * Every string the support block shows, in English. Pass `copy` (any part of\n * this shape) to translate or reword; what you leave out stays English.\n */\nexport const supportCopy = {\n settings: {\n section: \"Support\",\n sendFeedback: \"Send feedback\",\n help: \"Help\",\n /** `{count}` new replies. */\n unreadReplies: \"{count} new\",\n },\n feedback: {\n title: \"Send feedback\",\n typeLabel: \"What is it?\",\n types: { bug: \"Bug\", idea: \"Idea\", question: \"Question\", praise: \"Praise\" },\n messageLabel: \"Tell us more\",\n messagePlaceholder: \"What happened, and what did you expect?\",\n screenshotLabel: \"Screenshot\",\n includeScreenshot: \"Include this screenshot\",\n screenshotHint: \"Sent only if you include it. Mark it up to hide anything private first.\",\n annotate: \"Mark up\",\n pin: \"Point to it on the page\",\n pinned: \"Pinned\",\n removeScreenshot: \"Remove\",\n send: \"Send\",\n sending: \"Sending\",\n sentTitle: \"Thanks for the feedback\",\n sentBody: \"We read every report. You'll find it, and our replies, under Help.\",\n sentWithoutScreenshot: \"Your report was sent, but its screenshot couldn't be.\",\n failedTitle: \"Your feedback wasn't sent\",\n failedBody: \"Check your connection and try again.\",\n rateLimited: \"You've sent a lot of feedback in a short time. Try again later.\",\n signInFirst: \"Sign in to send feedback.\",\n messageRequired: \"Write a few words first.\",\n },\n annotate: {\n title: \"Mark up\",\n pen: \"Pen\",\n hide: \"Hide\",\n undo: \"Undo\",\n done: \"Done\",\n cancel: \"Cancel\",\n hint: \"Draw to point something out. Use Hide to cover anything private.\",\n failed: \"The marks couldn't be saved. Try again, or cancel and leave the screenshot out.\",\n },\n inbox: {\n title: \"Help\",\n sectionTitle: \"Your reports\",\n emptyTitle: \"No reports yet\",\n emptyBody: \"When you send feedback it shows up here, with our replies.\",\n errorTitle: \"Couldn't load your reports\",\n errorBody: \"Check your connection, then pull to refresh.\",\n sendFeedback: \"Send feedback\",\n ideaBoard: \"Idea board\",\n shipped: \"You asked, we shipped\",\n delete: \"Delete\",\n deleteConfirmTitle: \"Delete this report?\",\n deleteConfirmBody: \"The report, its replies and its screenshots are deleted and can't be opened again.\",\n cancel: \"Cancel\",\n unread: \"New reply\",\n },\n thread: {\n title: \"Report\",\n placeholder: \"Reply\",\n send: \"Send\",\n read: \"Read\",\n support: \"Support\",\n closed: \"This report is closed. Send new feedback if something's still wrong.\",\n emptyTitle: \"No replies yet\",\n emptyBody: \"We'll answer here.\",\n screenshot: \"Screenshot\",\n notFound: \"That report isn't there any more.\",\n you: \"You\",\n sending: \"Sending\",\n notSent: \"Not sent. Tap to retry.\",\n retryHint: \"Double tap to send again\",\n },\n status: {\n open: \"Open\",\n submitted: \"Received\",\n in_progress: \"In progress\",\n addressed: \"Fixed\",\n wont_fix: \"Closed\",\n },\n ideas: {\n title: \"Idea board\",\n emptyTitle: \"No ideas yet\",\n emptyBody: \"Suggest the first one.\",\n suggest: \"Suggest an idea\",\n suggestTitle: \"Suggest an idea\",\n titleLabel: \"Your idea\",\n titlePlaceholder: \"A short title\",\n bodyLabel: \"Why it would help (optional)\",\n submit: \"Suggest\",\n vote: \"Vote\",\n voted: \"Voted\",\n /** `{count}` votes. */\n votes: \"{count} votes\",\n oneVote: \"1 vote\",\n statuses: { open: \"Open\", planned: \"Planned\", in_progress: \"In progress\", shipped: \"Shipped\", declined: \"Not planned\", hidden: \"Hidden\" },\n },\n shipped: {\n title: \"You asked, we shipped\",\n emptyTitle: \"Nothing yet\",\n emptyBody: \"When an idea you suggested or voted for ships, it shows up here.\",\n /** `{feature}`: the FeatureId it shipped as. */\n shippedAs: \"Shipped as {feature}\",\n },\n};\n\n/** The shape a translation fills. */\nexport type SupportCopy = typeof supportCopy;\n\n/** Any part of the catalog. */\nexport type SupportCopyOverrides = { [K in keyof SupportCopy]?: DeepPartial<SupportCopy[K]> };\ntype DeepPartial<T> = { [K in keyof T]?: T[K] extends string ? string : DeepPartial<T[K]> };\n\n/** The catalog with overrides merged over the English. */\nexport function resolveSupportCopy(overrides: SupportCopyOverrides = {}): SupportCopy {\n const merge = (base: Record<string, unknown>, over: Record<string, unknown> | undefined): Record<string, unknown> => {\n if (!over) return base;\n const out: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(over)) {\n if (value === undefined) continue;\n const current = base[key];\n out[key] = current && typeof current === \"object\" && value && typeof value === \"object\" ? merge(current as Record<string, unknown>, value as Record<string, unknown>) : value;\n }\n return out;\n };\n return merge(supportCopy as unknown as Record<string, unknown>, overrides as Record<string, unknown>) as unknown as SupportCopy;\n}\n\n/** Fills `{name}` placeholders. */\nexport function fill(template: string, values: Readonly<Record<string, string | number>>): string {\n return template.replace(/\\{(\\w+)\\}/g, (token, name: string) => (values[name] !== undefined ? String(values[name]) : token));\n}\n"]}
|