@marktiderman/genesis-support 0.2.0 → 0.2.1

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,288 @@
1
+ -- @feat:support-chat
2
+ -- pgTAP test for 0001_genesis_support_chat.sql (after 0006_genesis_support.sql and genesis-chat's
3
+ -- 0001 and 0002). Copy into your app's `supabase/tests/database/` and run `supabase test db`.
4
+ -- It writes only its own users, reports and conversations and rolls everything back. If your
5
+ -- app already has the staff hook (`private.genesis_support_staff`), it is replaced inside this
6
+ -- transaction only.
7
+ --
8
+ -- People: a1 reports, b2 is someone else, c3 and d4 are staff, e5 is not staff.
9
+ begin;
10
+ create extension if not exists pgtap with schema extensions;
11
+ select plan(66);
12
+
13
+ insert into auth.users (id, aud, role, email, instance_id)
14
+ select ('00000000-0000-4000-8000-0000000000' || n)::uuid, 'authenticated', 'authenticated',
15
+ 'genesis-support-chat-' || n || '@example.test', '00000000-0000-0000-0000-000000000000'
16
+ from unnest(array['a1', 'b2', 'c3', 'd4', 'e5']) n;
17
+
18
+ create function pg_temp.person(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-0000000000' || n)::uuid $$;
19
+ create function pg_temp.sign_in(n text) returns void language plpgsql as $$
20
+ begin
21
+ perform set_config('request.jwt.claims', json_build_object('sub', pg_temp.person(n), 'role', 'authenticated')::text, true);
22
+ perform set_config('request.jwt.claim.sub', pg_temp.person(n)::text, true);
23
+ end $$;
24
+ create table pg_temp.ids (name text primary key, id uuid);
25
+ create function pg_temp.remember(name text, q text) returns void language plpgsql as $$
26
+ declare v uuid;
27
+ begin
28
+ execute q into v;
29
+ insert into pg_temp.ids values (name, v);
30
+ end $$;
31
+ create function pg_temp.id(n text) returns uuid language sql stable as $$ select id from pg_temp.ids where name = n $$;
32
+ create function pg_temp.conv(n text) returns uuid language sql stable security definer as $$
33
+ select c.id from genesis_chat.conversations c where c.kind = 'support' and c.subject_id = (select id from pg_temp.ids where name = n)::text
34
+ $$;
35
+ create function pg_temp.send(conv uuid, body text, visibility text default 'everyone') returns uuid language sql as $$
36
+ select (genesis_chat.send_message(conv, gen_random_uuid(), body, '[]', null, null, null, '{}', visibility)).id
37
+ $$;
38
+ grant select, insert on pg_temp.ids to anon, authenticated;
39
+ grant execute on function pg_temp.person(text), pg_temp.sign_in(text), pg_temp.remember(text, text), pg_temp.id(text),
40
+ pg_temp.conv(text), pg_temp.send(uuid, text, text) to anon, authenticated, service_role;
41
+
42
+ drop function if exists private.genesis_support_staff(uuid);
43
+ create function private.genesis_support_staff(person uuid) returns boolean
44
+ language sql stable set search_path = '' as $$
45
+ select person in ('00000000-0000-4000-8000-0000000000c3'::uuid, '00000000-0000-4000-8000-0000000000d4'::uuid)
46
+ $$;
47
+ revoke execute on function private.genesis_support_staff(uuid) from public, anon, authenticated;
48
+
49
+ -- The kind ----------------------------------------------------------------------------
50
+ select is(
51
+ (select row(max_members, membership, threads, observers_post, who_posts, observer_check::text)::text
52
+ from genesis_chat.kinds where kind = 'support'),
53
+ row(2, 'app', false, true, 'members', 'private.genesis_support_chat_observer(uuid,uuid)')::text,
54
+ 'support: two at most (the person), the app adds members, no threads, staff observe and post'
55
+ );
56
+ select ok(
57
+ not has_function_privilege('authenticated', 'private.genesis_support_chat_observer(uuid, uuid)', 'execute')
58
+ and not has_function_privilege('anon', 'private.genesis_support_chat_backfill()', 'execute')
59
+ and not has_function_privilege('anon', 'public.support_chat_conversation(uuid)', 'execute')
60
+ and not has_function_privilege('anon', 'public.support_assign_report(uuid, uuid)', 'execute'),
61
+ 'the private functions are the database''s, and anon calls nothing'
62
+ );
63
+ select ok(
64
+ not has_column_privilege('authenticated', 'public.support_replies', 'chat_message_id', 'select'),
65
+ 'which message a reply mirrors is not readable'
66
+ );
67
+
68
+ -- The copy: a report and replies from before this block -----------------------------------
69
+ alter table public.support_reports disable trigger genesis_support_chat_report_added;
70
+ alter table public.support_replies disable trigger genesis_support_chat_reply_added;
71
+ alter table genesis_chat.messages disable trigger genesis_support_chat_message_posted;
72
+
73
+ select pg_temp.sign_in('a1');
74
+ set local role authenticated;
75
+ select pg_temp.remember('r0', $$ insert into public.support_reports (type, title, message) values ('bug', 'Old report', 'It broke before chat') returning id $$);
76
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r0'), 'first');
77
+ reset role;
78
+ select pg_temp.sign_in('c3');
79
+ set local role authenticated;
80
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r0'), 'second');
81
+ reset role;
82
+ select pg_temp.sign_in('a1');
83
+ set local role authenticated;
84
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r0'), 'third');
85
+ reset role;
86
+ select pg_temp.sign_in('c3');
87
+ set local role authenticated;
88
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r0'), 'fourth');
89
+ reset role;
90
+
91
+ alter table public.support_reports enable trigger genesis_support_chat_report_added;
92
+ alter table public.support_replies enable trigger genesis_support_chat_reply_added;
93
+ alter table genesis_chat.messages enable trigger genesis_support_chat_message_posted;
94
+
95
+ select is(pg_temp.conv('r0'), null, 'before the copy the old report has no conversation');
96
+ create table pg_temp.effects_before as select count(*) as n from genesis_chat.effects;
97
+ select lives_ok($$ select private.genesis_support_chat_backfill() $$, 'the copy runs');
98
+ select isnt(pg_temp.conv('r0'), null, 'the old report has its conversation');
99
+ select is(
100
+ (select array_agg(user_id::text || ':' || role) from genesis_chat.members where conversation_id = pg_temp.conv('r0')),
101
+ array[pg_temp.person('a1')::text || ':member'],
102
+ 'its person is its only member'
103
+ );
104
+ select is(
105
+ (select array_agg(body order by seq) from genesis_chat.messages where conversation_id = pg_temp.conv('r0')),
106
+ array['first', 'second', 'third', 'fourth'],
107
+ 'the replies are copied in order'
108
+ );
109
+ select is(
110
+ (select array_agg(sender_role order by seq) from genesis_chat.messages where conversation_id = pg_temp.conv('r0')),
111
+ array['member', 'observer', 'member', 'observer'],
112
+ 'the person''s as the member, staff''s as an observer'
113
+ );
114
+ select ok(
115
+ (select bool_and(m.created_at = p.created_at and m.sender_id = p.author_id and p.chat_message_id = m.id)
116
+ from public.support_replies p join genesis_chat.messages m on m.client_id = p.id
117
+ where p.report_id = pg_temp.id('r0')),
118
+ 'each keeps its time and writer, and the reply knows its message'
119
+ );
120
+ select is((select count(*) from genesis_chat.effects), (select n from pg_temp.effects_before), 'the copy queued no push, preview or other effect');
121
+ select is(
122
+ (select last_read_seq from genesis_chat.members where conversation_id = pg_temp.conv('r0') and user_id = pg_temp.person('a1')),
123
+ 3::bigint,
124
+ 'the person has read up to their own last reply'
125
+ );
126
+ select is((select status from genesis_chat.conversations where id = pg_temp.conv('r0')), 'open', 'the conversation is open, like the report');
127
+ select lives_ok($$ select private.genesis_support_chat_backfill() $$, 'the copy runs again');
128
+ select is((select count(*)::int from genesis_chat.messages where conversation_id = pg_temp.conv('r0')), 4, '... and copies nothing twice');
129
+ select is((select count(*) from genesis_chat.effects), (select n from pg_temp.effects_before), '... nor queues anything');
130
+
131
+ -- A new report ------------------------------------------------------------------------------
132
+ select pg_temp.sign_in('a1');
133
+ set local role authenticated;
134
+ select pg_temp.remember('r1', $$ insert into public.support_reports (type, message) values ('question', E'How do I export?\nI looked everywhere.') returning id $$);
135
+ select is(public.support_chat_conversation(pg_temp.id('r1')), pg_temp.conv('r1'), 'a new report has its conversation, and its person finds it');
136
+ select is((select title from genesis_chat.conversations where id = pg_temp.conv('r1')), 'How do I export?', 'titled with the first line of its message');
137
+ select is(genesis_chat.my_role(pg_temp.conv('r1')), 'member', 'its person is its member');
138
+ select throws_ok($$ select genesis_chat.start_conversation('support', array['00000000-0000-4000-8000-0000000000c3'::uuid]) $$,
139
+ '42501', null, 'nobody starts a support conversation by hand');
140
+ select lives_ok($$ select pg_temp.send(pg_temp.conv('r1'), 'Any news?') $$, 'the person writes in it');
141
+ select throws_ok($$ select pg_temp.send(pg_temp.conv('r1'), 'note to self', 'observers') $$,
142
+ '42501', null, 'the person cannot write an internal note');
143
+ reset role;
144
+ select ok(
145
+ (select last_user_reply_at is not null and user_read_at is not null from public.support_reports where id = pg_temp.id('r1')),
146
+ 'the person''s message moves the report on and marks it read for them'
147
+ );
148
+ select is(
149
+ (select array_agg(body || ':' || from_staff::text) from public.support_replies where report_id = pg_temp.id('r1')),
150
+ array['Any news?:false'],
151
+ 'older clients see it in support_replies, as the person''s'
152
+ );
153
+
154
+ -- Staff ------------------------------------------------------------------------------------
155
+ select pg_temp.sign_in('c3');
156
+ set local role authenticated;
157
+ select is(public.support_chat_conversation(pg_temp.id('r1')), pg_temp.conv('r1'), 'staff find the conversation');
158
+ select is(genesis_chat.my_role(pg_temp.conv('r1')), 'observer', 'staff observe it without being in it');
159
+ select is((select count(*)::int from genesis_chat.messages where conversation_id = pg_temp.conv('r1')), 1, 'staff read it');
160
+ select pg_temp.remember('m_staff', $$ select pg_temp.send(pg_temp.conv('r1'), 'Settings, then Export.') $$);
161
+ select pg_temp.remember('m_note', $$ select pg_temp.send(pg_temp.conv('r1'), 'Customer on the old plan', 'observers') $$);
162
+ select is(
163
+ (select array_agg(sender_role || ':' || visibility order by seq) from genesis_chat.messages where conversation_id = pg_temp.conv('r1')),
164
+ array['member:everyone', 'observer:everyone', 'observer:observers'],
165
+ 'staff reply and write internal notes, as observers'
166
+ );
167
+ reset role;
168
+ select ok(
169
+ (select last_staff_reply_at is not null and staff_read_at is not null from public.support_reports where id = pg_temp.id('r1')),
170
+ 'a staff reply moves the report on for the person'
171
+ );
172
+ select is(
173
+ (select array_agg(body || ':' || from_staff::text order by created_at) from public.support_replies where report_id = pg_temp.id('r1')),
174
+ array['Any news?:false', 'Settings, then Export.:true'],
175
+ 'older clients see the staff reply, and never the internal note'
176
+ );
177
+
178
+ select pg_temp.sign_in('a1');
179
+ set local role authenticated;
180
+ select is(
181
+ (select array_agg(body order by seq) from genesis_chat.messages where conversation_id = pg_temp.conv('r1')),
182
+ array['Any news?', 'Settings, then Export.'],
183
+ 'the person reads the reply and never the internal note'
184
+ );
185
+ select ok(
186
+ (select last_staff_reply_at > user_read_at from public.support_reports where id = pg_temp.id('r1')),
187
+ 'the person has an unread staff reply'
188
+ );
189
+ select lives_ok($$ select genesis_chat.mark_read(pg_temp.conv('r1')) $$, 'the person reads the conversation');
190
+ select ok(
191
+ (select user_read_at >= last_staff_reply_at from public.support_reports where id = pg_temp.id('r1')),
192
+ '... and the report is read for them'
193
+ );
194
+ select throws_ok($$ select genesis_chat.edit_message(pg_temp.id('m_staff'), 'mine now') $$,
195
+ '42501', null, 'the person cannot edit a staff reply');
196
+ reset role;
197
+
198
+ -- Someone else, and nobody ------------------------------------------------------------------
199
+ select pg_temp.sign_in('b2');
200
+ set local role authenticated;
201
+ select is(public.support_chat_conversation(pg_temp.id('r1')), null, 'someone else finds no conversation');
202
+ select is((select count(*)::int from genesis_chat.conversations where kind = 'support'), 0, '... reads none');
203
+ select is((select count(*)::int from genesis_chat.messages), 0, '... nor any message');
204
+ select throws_ok($$ select pg_temp.send(pg_temp.conv('r1'), 'hello') $$, '42501', null, '... nor writes in one');
205
+ reset role;
206
+ set local role anon;
207
+ select throws_ok($$ select count(*) from genesis_chat.messages $$, '42501', null, 'anon reads nothing');
208
+ select throws_ok($$ select public.support_chat_conversation('00000000-0000-4000-8000-000000000000') $$, '42501', null, '... and calls nothing');
209
+ reset role;
210
+
211
+ -- Older clients write through support_replies ---------------------------------------------
212
+ select pg_temp.sign_in('a1');
213
+ set local role authenticated;
214
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r1'), 'Found it, thanks');
215
+ reset role;
216
+ select is(
217
+ (select sender_role || ':' || body from genesis_chat.messages where conversation_id = pg_temp.conv('r1') order by seq desc limit 1),
218
+ 'member:Found it, thanks',
219
+ 'a reply from an older client becomes the person''s message'
220
+ );
221
+ select pg_temp.sign_in('c3');
222
+ set local role authenticated;
223
+ insert into public.support_replies (report_id, body) values (pg_temp.id('r1'), 'Glad to help');
224
+ reset role;
225
+ select is(
226
+ (select sender_role || ':' || body from genesis_chat.messages where conversation_id = pg_temp.conv('r1') order by seq desc limit 1),
227
+ 'observer:Glad to help',
228
+ '... and a staff reply from one becomes a staff message'
229
+ );
230
+ select is((select count(*)::int from public.support_replies where report_id = pg_temp.id('r1')), 4, '... each once (no echo back)');
231
+ select pg_temp.sign_in('a1');
232
+ set local role authenticated;
233
+ select lives_ok($$ select public.support_mark_read(pg_temp.id('r1')) $$, 'an older client marks the report read');
234
+ reset role;
235
+ select ok(
236
+ (select m.last_read_seq = c.last_seq from genesis_chat.members m join genesis_chat.conversations c on c.id = m.conversation_id
237
+ where m.conversation_id = pg_temp.conv('r1') and m.user_id = pg_temp.person('a1')),
238
+ '... and the person''s place in the conversation follows'
239
+ );
240
+
241
+ -- Edits and deletes reach older clients ------------------------------------------------------
242
+ select pg_temp.sign_in('c3');
243
+ set local role authenticated;
244
+ select lives_ok($$ select genesis_chat.edit_message(pg_temp.id('m_staff'), 'Settings, then Export data.') $$, 'staff edit their reply');
245
+ reset role;
246
+ select is((select body from public.support_replies where chat_message_id = pg_temp.id('m_staff')), 'Settings, then Export data.', '... and support_replies has the edit');
247
+ select pg_temp.sign_in('c3');
248
+ set local role authenticated;
249
+ select lives_ok($$ select genesis_chat.delete_message(pg_temp.id('m_staff')) $$, 'staff delete it');
250
+ reset role;
251
+ select is((select count(*)::int from public.support_replies where body like 'Settings%'), 0, '... and it leaves support_replies');
252
+
253
+ -- Status and assignee ------------------------------------------------------------------------
254
+ select pg_temp.sign_in('c3');
255
+ set local role authenticated;
256
+ select lives_ok($$ select public.support_set_report_status(pg_temp.id('r1'), 'in_progress') $$, 'staff set the report in progress');
257
+ select is((select status from genesis_chat.conversations where id = pg_temp.conv('r1')), 'pending', '... and the conversation is pending');
258
+ select lives_ok($$ select genesis_chat.set_status(pg_temp.conv('r1'), 'solved', '00000000-0000-4000-8000-0000000000d4') $$,
259
+ 'a staff tool solves the conversation and gives it to d4');
260
+ select is((select status from public.support_reports where id = pg_temp.id('r1')), 'addressed', '... and the report is addressed');
261
+ select lives_ok($$ select public.support_set_report_status(pg_temp.id('r1'), 'submitted') $$, 'staff set it submitted');
262
+ select lives_ok($$ select genesis_chat.set_status(pg_temp.conv('r1'), 'open', null) $$, 'the conversation, open, is set open again');
263
+ select is((select status from public.support_reports where id = pg_temp.id('r1')), 'submitted', '... which keeps the report submitted');
264
+ select lives_ok($$ select public.support_assign_report(pg_temp.id('r1'), '00000000-0000-4000-8000-0000000000d4') $$, 'staff assign the report to d4');
265
+ select is((select assignee_id from genesis_chat.conversations where id = pg_temp.conv('r1')), pg_temp.person('d4'), '... who has it');
266
+ select throws_ok($$ select public.support_assign_report(pg_temp.id('r1'), '00000000-0000-4000-8000-0000000000e5') $$,
267
+ '42501', null, 'only staff are assigned');
268
+ select throws_ok($$ select genesis_chat.set_status(pg_temp.conv('r1'), 'pending', '00000000-0000-4000-8000-0000000000e5') $$,
269
+ '42501', null, '... through chat too');
270
+ reset role;
271
+ select pg_temp.sign_in('a1');
272
+ set local role authenticated;
273
+ select throws_ok($$ select public.support_assign_report(pg_temp.id('r1'), null) $$, '42501', null, 'the person assigns nobody');
274
+ select throws_ok($$ select genesis_chat.set_status(pg_temp.conv('r1'), 'closed', null) $$, '42501', null, '... nor sets the status in chat');
275
+
276
+ -- Deleting ------------------------------------------------------------------------------
277
+ create table pg_temp.gone as select pg_temp.conv('r1') as conv;
278
+ grant select on pg_temp.gone to authenticated;
279
+ delete from public.support_reports where id = pg_temp.id('r1');
280
+ reset role;
281
+ select is((select count(*)::int from genesis_chat.conversations where id = (select conv from pg_temp.gone)), 0, 'deleting a report deletes its conversation');
282
+ select is((select count(*)::int from genesis_chat.messages where conversation_id = (select conv from pg_temp.gone)), 0, '... and its messages');
283
+ create table pg_temp.gone0 as select pg_temp.conv('r0') as conv;
284
+ delete from auth.users where id = pg_temp.person('a1');
285
+ select is((select count(*)::int from genesis_chat.conversations where id = (select conv from pg_temp.gone0)), 0, 'deleting the account deletes its reports'' conversations');
286
+
287
+ select * from finish();
288
+ rollback;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-support",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
- "description": "Support and feedback for Genesis apps, in the app's own Supabase: a feedback record (bug, idea, question, praise) with the app's context and screenshots, collector and sink seams with a Supabase sink, a web feedback panel and launcher, a native feedback screen with screenshot capture, annotation and shake to report, the person's reports with staff replies, and an idea board with votes and \"you asked, we shipped\". Declared as a defineBlock block. Part of the Genesis framework.",
7
+ "description": "Support and feedback for Genesis apps, in the app's own Supabase: a feedback record (bug, idea, question, praise) with the app's context and screenshots, collector and sink seams with a Supabase sink, a web feedback panel and launcher, a native feedback screen with screenshot capture, annotation and shake to report, the person's reports with staff replies (on genesis-chat with the support-chat block: live, with files, an assignee and internal notes), and an idea board with votes and \"you asked, we shipped\". Declared as a defineBlock block. Part of the Genesis framework.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/marktiderman/genesis.git",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/web.d.ts",
29
29
  "import": "./dist/web.js"
30
30
  },
31
+ "./web-chat": {
32
+ "types": "./dist/web-chat.d.ts",
33
+ "import": "./dist/web-chat.js"
34
+ },
31
35
  "./native": {
32
36
  "types": "./dist/native.d.ts",
33
37
  "import": "./dist/native.js"
@@ -35,15 +39,17 @@
35
39
  },
36
40
  "files": [
37
41
  "dist",
42
+ "migrations",
38
43
  "README.md"
39
44
  ],
40
45
  "dependencies": {
41
46
  "@marktiderman/genesis-features": "^0.3.0"
42
47
  },
43
48
  "peerDependencies": {
49
+ "@marktiderman/genesis-chat": ">=0.1.0 <1.0.0",
44
50
  "@marktiderman/genesis-native": ">=0.1.0 <1.0.0",
45
51
  "@marktiderman/genesis-ui": "^2.13.0",
46
- "@marktiderman/genesis-ui-native": "^2.3.0",
52
+ "@marktiderman/genesis-ui-native": "^2.4.0",
47
53
  "@supabase/supabase-js": "^2.118.0",
48
54
  "@tanstack/react-query": "^5.0.0",
49
55
  "react": "^19.0.0",
@@ -52,6 +58,9 @@
52
58
  "react-native-safe-area-context": ">=4.0.0"
53
59
  },
54
60
  "peerDependenciesMeta": {
61
+ "@marktiderman/genesis-chat": {
62
+ "optional": true
63
+ },
55
64
  "@marktiderman/genesis-native": {
56
65
  "optional": true
57
66
  },
@@ -84,9 +93,10 @@
84
93
  }
85
94
  },
86
95
  "devDependencies": {
87
- "@marktiderman/genesis-native": "^0.3.0",
96
+ "@marktiderman/genesis-chat": "^0.2.0",
97
+ "@marktiderman/genesis-native": "^0.4.0",
88
98
  "@marktiderman/genesis-ui": "^2.13.0",
89
- "@marktiderman/genesis-ui-native": "^2.3.0",
99
+ "@marktiderman/genesis-ui-native": "^2.4.0",
90
100
  "@supabase/supabase-js": "^2.118.0",
91
101
  "@tanstack/react-query": "^5.75.0",
92
102
  "@testing-library/react": "^16.3.2",
@@ -243,6 +253,48 @@
243
253
  "Feedback includes the app version, the screen's path (never its query string; private-looking segments are replaced), the platform and basic device facts."
244
254
  ]
245
255
  }
256
+ },
257
+ "support-chat": {
258
+ "feature": {
259
+ "name": "Support threads on chat",
260
+ "summary": "A support report's conversation runs on genesis-chat: live replies, photos, files and voice notes, an assignee and staff-only internal notes, with the report kept as the record.",
261
+ "keywords": [
262
+ "support inbox",
263
+ "help desk",
264
+ "ticket chat",
265
+ "internal notes",
266
+ "assignee",
267
+ "live support"
268
+ ],
269
+ "audience": [
270
+ "person",
271
+ "admin"
272
+ ]
273
+ },
274
+ "requires": [
275
+ "support-feedback",
276
+ "chat"
277
+ ],
278
+ "platforms": [
279
+ "web",
280
+ "native"
281
+ ],
282
+ "packages": [
283
+ "@marktiderman/genesis-support",
284
+ "@marktiderman/genesis-chat"
285
+ ],
286
+ "migrations": [
287
+ {
288
+ "file": "migrations/0001_genesis_support_chat.sql",
289
+ "test": "migrations/tests/0001_genesis_support_chat.test.sql"
290
+ }
291
+ ],
292
+ "steps": [
293
+ "Apply this block's migration after 0006_genesis_support.sql and genesis-chat's 0001 and 0002 (genesis add support-chat orders them). It gives every existing report its conversation and copies support_replies into it, with no push.",
294
+ "Pass genesis-chat's client to support: createSupport({ ..., chat: createChatClient(supabase) }) on native, createSupportClient(supabase, { chat }) on the web. Without it, threads stay on support_replies.",
295
+ "Web: render SupportReportThread (the person) or SupportStaffThread (staff: status, assignee, internal notes) from @marktiderman/genesis-support/web-chat under genesis-chat's ChatProvider.",
296
+ "Keep genesis-chat's runner going (push for replies). support_replies stays mirrored for one release, for older app builds; a later migration removes it."
297
+ ]
246
298
  }
247
299
  }
248
300
  },
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts","../src/block.ts"],"names":[],"mappings":"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","file":"chunk-4VFYVQUF.js","sourcesContent":["/**\n * The support client: the app's own Supabase (migration 0006 from\n * `@marktiderman/genesis-supabase`) under row level security. Everything a\n * person does (send feedback, read their reports and the replies, reply, vote\n * on the idea board) and the Supabase sink for the feedback record.\n */\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { FeedbackStatus } from \"./contract\";\nimport { attachmentBytes, sanitizeContext, type FeedbackDelivery, type FeedbackRecord, type FeedbackSink, type FeedbackType } from \"./record\";\n\n/** Any supabase-js client; the app's generated Database types don't need to list the support tables. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type SupportSupabase = SupabaseClient<any, any, any>;\n\n/** The storage bucket migration 0006 creates. */\nexport const SUPPORT_BUCKET = \"genesis-support\";\n\n/** A Supabase refusal, with its code (`GS429` rate limited, `42501` not allowed, …). */\nexport class SupportError extends Error {\n constructor(\n message: string,\n readonly code: string | undefined,\n readonly operation: string,\n ) {\n super(message);\n this.name = \"SupportError\";\n }\n /** The database's rate limit (SQLSTATE GS429). */\n get rateLimited(): boolean {\n return this.code === \"GS429\";\n }\n}\n\nfunction must<T>(operation: string, result: { data: T | null; error: { message: string; code?: string } | null }): T {\n if (result.error) throw new SupportError(result.error.message, result.error.code, operation);\n return result.data as T;\n}\n\nexport type IdeaStatus = \"open\" | \"planned\" | \"in_progress\" | \"shipped\" | \"declined\" | \"hidden\";\n\nexport interface SupportReport {\n id: string;\n type: FeedbackType;\n title: string | null;\n message: string;\n status: FeedbackStatus;\n /** The FeatureId staff linked it to. */\n featureId: string | null;\n route: string | null;\n appVersion: string | null;\n createdAt: string;\n updatedAt: string;\n lastStaffReplyAt: string | null;\n lastUserReplyAt: string | null;\n userReadAt: string | null;\n staffReadAt: string | null;\n /** A staff reply the person hasn't read. */\n unread: boolean;\n}\n\nexport interface SupportReply {\n id: string;\n reportId: string;\n body: string;\n fromStaff: boolean;\n /** Written by the signed-in person. */\n mine: boolean;\n createdAt: string;\n}\n\nexport interface SupportAttachmentView {\n id: string;\n path: string;\n contentType: string;\n width: number | null;\n height: number | null;\n /** A signed URL, valid for `signedUrlSeconds` (default an hour). Null when signing failed. */\n url: string | null;\n}\n\nexport interface SupportIdea {\n id: string;\n title: string;\n body: string | null;\n status: IdeaStatus;\n /** The FeatureId that delivers it (set when it ships). */\n featureId: string | null;\n shippedAt: string | null;\n voteCount: number;\n /** The signed-in person voted for it. */\n voted: boolean;\n /** The signed-in person suggested it. */\n mine: boolean;\n createdAt: string;\n}\n\nexport interface SupportClient extends FeedbackSink {\n /** The signed-in person's reports, newest first. */\n myReports(options?: { limit?: number }): Promise<SupportReport[]>;\n report(id: string): Promise<SupportReport | null>;\n replies(reportId: string): Promise<SupportReply[]>;\n reply(reportId: string, body: string): Promise<SupportReply>;\n markRead(reportId: string): Promise<void>;\n /** Deletes the person's own report; its screenshots are queued for removal. */\n deleteReport(reportId: string): Promise<void>;\n attachments(reportId: string): Promise<SupportAttachmentView[]>;\n /** The board, most votes first. Hidden ideas are left out, the person's own included. */\n ideas(options?: { limit?: number; statuses?: IdeaStatus[] }): Promise<SupportIdea[]>;\n suggestIdea(input: { title: string; body?: string }): Promise<SupportIdea>;\n vote(ideaId: string): Promise<void>;\n unvote(ideaId: string): Promise<void>;\n /** \"You asked, we shipped\": shipped ideas the person suggested or voted for. */\n shippedForMe(): Promise<SupportIdea[]>;\n}\n\nexport interface SupportClientOptions {\n bucket?: string;\n signedUrlSeconds?: number;\n /** For the attachment file names. Default `crypto.randomUUID`. */\n randomId?: () => string;\n}\n\nconst REPORT_COLUMNS =\n \"id, type, title, message, status, feature_id, route, app_version, created_at, updated_at, last_staff_reply_at, last_user_reply_at, user_read_at, staff_read_at\";\nconst IDEA_COLUMNS = \"id, title, body, status, feature_id, shipped_at, vote_count, created_at\";\n\ntype Row = Record<string, unknown>;\n\n/** True when there is a staff reply newer than the person's last read. */\nexport function isUnread(row: { lastStaffReplyAt: string | null; userReadAt: string | null }): boolean {\n if (!row.lastStaffReplyAt) return false;\n return !row.userReadAt || new Date(row.lastStaffReplyAt).getTime() > new Date(row.userReadAt).getTime();\n}\n\nfunction toReport(r: Row): SupportReport {\n const report: Omit<SupportReport, \"unread\"> = {\n id: String(r.id),\n type: r.type as FeedbackType,\n title: (r.title as string | null) ?? null,\n message: String(r.message ?? \"\"),\n status: r.status as FeedbackStatus,\n featureId: (r.feature_id as string | null) ?? null,\n route: (r.route as string | null) ?? null,\n appVersion: (r.app_version as string | null) ?? null,\n createdAt: String(r.created_at),\n updatedAt: String(r.updated_at),\n lastStaffReplyAt: (r.last_staff_reply_at as string | null) ?? null,\n lastUserReplyAt: (r.last_user_reply_at as string | null) ?? null,\n userReadAt: (r.user_read_at as string | null) ?? null,\n staffReadAt: (r.staff_read_at as string | null) ?? null,\n };\n return { ...report, unread: isUnread(report) };\n}\n\nfunction toIdea(r: Row, mine: ReadonlySet<string>, voted: ReadonlySet<string>): SupportIdea {\n return {\n id: String(r.id),\n title: String(r.title ?? \"\"),\n body: (r.body as string | null) ?? null,\n status: r.status as IdeaStatus,\n featureId: (r.feature_id as string | null) ?? null,\n shippedAt: (r.shipped_at as string | null) ?? null,\n voteCount: Number(r.vote_count ?? 0),\n voted: voted.has(String(r.id)),\n mine: mine.has(String(r.id)),\n createdAt: String(r.created_at),\n };\n}\n\nconst EXTENSIONS: Record<string, string> = { \"image/png\": \"png\", \"image/jpeg\": \"jpg\", \"image/webp\": \"webp\" };\n\nfunction defaultRandomId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/** The support client over the app's Supabase client. */\nexport function createSupportClient(supabase: SupportSupabase, options: SupportClientOptions = {}): SupportClient {\n const bucket = options.bucket ?? SUPPORT_BUCKET;\n const signedUrlSeconds = options.signedUrlSeconds ?? 3600;\n const randomId = options.randomId ?? defaultRandomId;\n\n const me = async (): Promise<string | null> => {\n const { data } = await supabase.auth.getSession();\n return data.session?.user?.id ?? null;\n };\n\n const client: SupportClient = {\n async submit(record: FeedbackRecord): Promise<FeedbackDelivery> {\n const userId = await me();\n if (!userId) throw new SupportError(\"Sign in to send feedback.\", \"42501\", \"submit\");\n const context = sanitizeContext(record.context);\n const row = must<Row>(\n \"submit\",\n await supabase\n .from(\"support_reports\")\n .insert({\n type: record.type,\n title: record.title?.trim() ? record.title.trim().slice(0, 300) : null,\n message: record.message,\n route: context.route ?? null,\n app_version: context.appVersion ?? null,\n platform: context.platform ?? null,\n device: context.device ?? {},\n context: context.extra ?? {},\n })\n .select(\"id\")\n .single(),\n );\n const id = String(row.id);\n let dropped = 0;\n for (const attachment of (record.attachments ?? []).slice(0, 5)) {\n try {\n const bytes = attachmentBytes(attachment);\n if (!bytes) throw new Error(\"no image data\");\n const path = `${userId}/${id}/${randomId().replace(/[^A-Za-z0-9_-]/g, \"\")}.${EXTENSIONS[attachment.contentType] ?? \"png\"}`;\n const upload = await supabase.storage.from(bucket).upload(path, bytes, { contentType: attachment.contentType, upsert: false });\n if (upload.error) throw upload.error;\n must(\n \"attach\",\n await supabase.from(\"support_attachments\").insert({\n report_id: id,\n path,\n content_type: attachment.contentType,\n byte_size: bytes.byteLength,\n width: attachment.width ? Math.round(attachment.width) : null,\n height: attachment.height ? Math.round(attachment.height) : null,\n }),\n );\n } catch {\n dropped += 1;\n }\n }\n dropped += Math.max(0, (record.attachments?.length ?? 0) - 5);\n return { id, attachmentsDropped: dropped };\n },\n\n async myReports({ limit = 50 } = {}) {\n const userId = await me();\n if (!userId) return [];\n const rows = must<Row[]>(\n \"myReports\",\n await supabase.from(\"support_reports\").select(REPORT_COLUMNS).eq(\"user_id\", userId).order(\"created_at\", { ascending: false }).limit(limit),\n );\n return rows.map(toReport);\n },\n\n async report(id) {\n const row = must<Row | null>(\"report\", await supabase.from(\"support_reports\").select(REPORT_COLUMNS).eq(\"id\", id).maybeSingle());\n return row ? toReport(row) : null;\n },\n\n async replies(reportId) {\n // Who wrote a reply isn't readable (staff stay anonymous): on the person's own report, theirs are the non-staff ones.\n const rows = must<Row[]>(\n \"replies\",\n await supabase.from(\"support_replies\").select(\"id, report_id, body, from_staff, created_at\").eq(\"report_id\", reportId).order(\"created_at\", { ascending: true }),\n );\n return rows.map((r) => ({\n id: String(r.id),\n reportId: String(r.report_id),\n body: String(r.body ?? \"\"),\n fromStaff: r.from_staff === true,\n mine: r.from_staff !== true,\n createdAt: String(r.created_at),\n }));\n },\n\n async reply(reportId, body) {\n const row = must<Row>(\n \"reply\",\n await supabase.from(\"support_replies\").insert({ report_id: reportId, body }).select(\"id, report_id, body, from_staff, created_at\").single(),\n );\n return { id: String(row.id), reportId: String(row.report_id), body: String(row.body), fromStaff: row.from_staff === true, mine: true, createdAt: String(row.created_at) };\n },\n\n async markRead(reportId) {\n must(\"markRead\", await supabase.rpc(\"support_mark_read\", { report: reportId }));\n },\n\n async deleteReport(reportId) {\n const rows = must<Row[]>(\"deleteReport\", await supabase.from(\"support_reports\").delete().eq(\"id\", reportId).select(\"id\"));\n if (!rows.length) throw new SupportError(\"That report wasn't deleted.\", \"42501\", \"deleteReport\");\n },\n\n async attachments(reportId) {\n const rows = must<Row[]>(\n \"attachments\",\n await supabase.from(\"support_attachments\").select(\"id, path, content_type, width, height\").eq(\"report_id\", reportId).order(\"created_at\"),\n );\n if (!rows.length) return [];\n const signed = await supabase.storage.from(bucket).createSignedUrls(\n rows.map((r) => String(r.path)),\n signedUrlSeconds,\n );\n const urls = new Map((signed.data ?? []).map((s) => [s.path, s.signedUrl] as const));\n return rows.map((r) => ({\n id: String(r.id),\n path: String(r.path),\n contentType: String(r.content_type),\n width: (r.width as number | null) ?? null,\n height: (r.height as number | null) ?? null,\n url: urls.get(String(r.path)) ?? null,\n }));\n },\n\n async ideas({ limit = 100, statuses } = {}) {\n const userId = await me();\n let query = supabase.from(\"support_ideas\").select(IDEA_COLUMNS).neq(\"status\", \"hidden\");\n if (statuses?.length) query = query.in(\"status\", statuses);\n const rows = must<Row[]>(\"ideas\", await query.order(\"vote_count\", { ascending: false }).order(\"created_at\", { ascending: false }).limit(limit));\n const votes = userId ? must<Row[]>(\"ideas\", await supabase.from(\"support_idea_votes\").select(\"idea_id\").eq(\"user_id\", userId)) : [];\n const voted = new Set(votes.map((v) => String(v.idea_id)));\n const mineIds = userId ? must<unknown[]>(\"ideas\", await supabase.rpc(\"support_my_idea_ids\")) : [];\n const mine = new Set((mineIds ?? []).map((v) => String(typeof v === \"object\" && v !== null ? Object.values(v)[0] : v)));\n return rows.map((r) => toIdea(r, mine, voted));\n },\n\n async suggestIdea({ title, body }) {\n const row = must<Row>(\"suggestIdea\", await supabase.from(\"support_ideas\").insert({ title: title.trim(), body: body?.trim() || null }).select(IDEA_COLUMNS).single());\n return toIdea(row, new Set([String(row.id)]), new Set());\n },\n\n async vote(ideaId) {\n const result = await supabase.from(\"support_idea_votes\").insert({ idea_id: ideaId });\n // Voting twice is already voting.\n if (result.error && result.error.code !== \"23505\") must(\"vote\", result);\n },\n\n async unvote(ideaId) {\n const userId = await me();\n if (!userId) return;\n must(\"unvote\", await supabase.from(\"support_idea_votes\").delete().eq(\"idea_id\", ideaId).eq(\"user_id\", userId));\n },\n\n async shippedForMe() {\n const rows = must<Row[]>(\"shippedForMe\", await supabase.rpc(\"support_shipped_for_me\", { max_rows: 50 }));\n const ids = (pick: string) => new Set(rows.filter((r) => r[pick] === true).map((r) => String(r.id)));\n return rows.map((r) => toIdea(r, ids(\"mine\"), ids(\"voted\")));\n },\n };\n return client;\n}\n\n/** The Supabase sink on its own (the support client is one). */\nexport function createSupabaseSink(supabase: SupportSupabase, options: SupportClientOptions = {}): FeedbackSink {\n const client = createSupportClient(supabase, options);\n return { submit: (record) => client.submit(record) };\n}\n","/**\n * The support block as a `defineBlock` declaration: what it adds to an app\n * (two Settings rows, its routes, migration 0006) and what it means for the\n * stores' privacy forms. Platform-neutral; `createSupport` (native) builds\n * on it, and a web app can resolve it with `resolveBlocks` too.\n */\nimport { defineBlock, type BlockContributions, type BlockSettingsRow } from \"@marktiderman/genesis-features/blocks\";\nimport { resolveSupportCopy, type SupportCopyOverrides } from \"./copy\";\n\n/** The FeatureId of the support block (features.ts). */\nexport const SUPPORT_BLOCK_ID = \"support-feedback\";\n\n/** Where the block's screens live, under `basePath`. */\nexport interface SupportPaths {\n /** Help: the person's reports and links to the idea board. */\n inbox: string;\n /** The feedback form. */\n feedback: string;\n /** One report's thread: `thread(id)`. */\n thread: (id: string) => string;\n ideas: string;\n shipped: string;\n}\n\nexport function supportPaths(basePath = \"/support\"): SupportPaths {\n const base = basePath.replace(/\\/$/, \"\") || \"/support\";\n return {\n inbox: base,\n feedback: `${base}/new`,\n thread: (id) => `${base}/${encodeURIComponent(id)}`,\n ideas: `${base}/ideas`,\n shipped: `${base}/shipped`,\n };\n}\n\nexport interface SupportBlockOptions {\n /** Default `/support`. The app owns the route files under it. */\n basePath?: string;\n copy?: SupportCopyOverrides;\n /** The Settings rows' section (default `support`), or `false` for none. */\n settings?: false | { section?: string };\n /** A subtitle hook for the Help row (an unread count). Called on every Settings render. */\n useHelpSubtitle?: () => string | null | undefined;\n /** `false` leaves the idea board out. Default on. */\n ideas?: boolean;\n /** The block's other FeatureIds it needs (default `[\"account\"]`: a signed-in person). */\n requires?: readonly string[];\n}\n\nconst PACKAGE = \"@marktiderman/genesis-supabase\";\n\n/** The block's contributions, for any platform. */\nexport function supportContributions(options: SupportBlockOptions = {}): BlockContributions<never> {\n const copy = resolveSupportCopy(options.copy);\n const paths = supportPaths(options.basePath);\n const section = options.settings === false ? null : (options.settings?.section ?? \"support\");\n const settings: BlockSettingsRow[] = section\n ? [\n { id: \"support-send-feedback\", section, title: copy.settings.sendFeedback, href: paths.feedback, order: 10, testID: \"genesis-support-send-feedback\" },\n { id: \"support-help\", section, title: copy.settings.help, href: paths.inbox, order: 20, testID: \"genesis-support-help\", useSubtitle: options.useHelpSubtitle },\n ]\n : [];\n const routes = [\n { path: paths.inbox, description: \"the person's reports and staff replies\", signedIn: true },\n { path: paths.feedback, description: \"send feedback, with a screenshot\", signedIn: true },\n { path: `${paths.inbox}/[id]`, description: \"one report's conversation\", signedIn: true },\n ...(options.ideas === false\n ? []\n : [\n { path: paths.ideas, description: \"the idea board, with votes\", signedIn: true },\n { path: paths.shipped, description: \"ideas the person asked for that shipped\", signedIn: true },\n ]),\n ];\n return {\n settings,\n routes,\n migrations: [{ package: PACKAGE, file: \"migrations/0006_genesis_support.sql\", test: \"migrations/tests/0006_genesis_support.test.sql\" }],\n privacy: {\n collects: [\n { type: \"user ID\", purposes: [\"app functionality\"], linked: true, tracking: false },\n { type: \"customer support\", purposes: [\"app functionality\"], linked: true, tracking: false },\n { type: \"other user content\", purposes: [\"app functionality\"], linked: true, tracking: false },\n { type: \"photos or videos\", purposes: [\"app functionality\"], linked: true, tracking: false },\n { type: \"other diagnostic data\", purposes: [\"app functionality\"], linked: true, tracking: false },\n ],\n notes: [\n \"Feedback is sent only when the person taps Send, and a screenshot only when they include it, after they can hide parts of it.\",\n \"Reports, screenshots and replies are visible to the person and the app's support staff only, and go when the person deletes them or their account.\",\n \"Feedback includes the app version, the screen's path (never its query string; private-looking segments are replaced), the platform and basic device facts.\",\n ],\n },\n };\n}\n\n/** The platform-neutral declaration. */\nexport function defineSupportBlock(options: SupportBlockOptions = {}) {\n return defineBlock({\n id: SUPPORT_BLOCK_ID,\n requires: options.requires ?? [\"account\"],\n contributes: supportContributions(options),\n });\n}\n"]}
@@ -1 +0,0 @@
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"]}