@mccaskie-labs/workbench-config 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Add the `database` and `analytics` capabilities manifests (step 4.11), both built.
8
+
3
9
  ## 0.1.4
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "./schema.json",
3
+ "format": 1,
4
+ "component": "analytics",
5
+ "title": "Page views and product events",
6
+ "summary": "Cloudflare Web Analytics counts page views through a beacon on every page; product events go into the app's own database through workbench-analytics. Apps hold no Cloudflare token: Claude reads page views through Control.",
7
+ "status": "built",
8
+ "surfaces": {
9
+ "package": ["beaconScript", "createEvents", "eventsMigration"],
10
+ "tools": ["cf_web_analytics_site_create"]
11
+ },
12
+ "actions": [
13
+ { "tool": "cf_web_analytics_site_create", "gate": "free", "ships": "4" }
14
+ ],
15
+ "rules": [
16
+ { "id": "analytics.beacon-on-pages", "kind": "must", "text": "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.", "enforced_by": "review", "probe": "analytics.beacon-present" },
17
+ { "id": "analytics.track-never-throws", "kind": "must_not", "text": "Let a failed event write break the request it describes: track logs and drops it.", "enforced_by": "types", "probe": "analytics.event-round-trip" },
18
+ { "id": "analytics.no-cf-token", "kind": "must_not", "text": "Hold a Cloudflare analytics token in the app (hard rule 1); page views are read with Control.", "enforced_by": "review", "probe": "analytics.page-views-drill" }
19
+ ],
20
+ "examples": [
21
+ {
22
+ "id": "analytics.example.track",
23
+ "title": "Record a product event",
24
+ "lang": "ts",
25
+ "probe": "analytics.event-round-trip",
26
+ "code": "import { createEvents } from '@mccaskie-labs/workbench-analytics'\n\nawait createEvents(db).track('note.created', { length: body.length }, { type: 'note', id })"
27
+ }
28
+ ],
29
+ "probes": [
30
+ { "id": "analytics.beacon-present", "half": "runtime", "expect": "pass", "smoke": true, "cost_hint_usd": 0 },
31
+ { "id": "analytics.event-round-trip", "half": "runtime", "expect": "pass", "cost_hint_usd": 0, "note": "A product event is written and read back" },
32
+ { "id": "analytics.page-views-drill", "half": "drill", "expect": "pass", "cost_hint_usd": 0, "note": "Create the site, drive page views, query them back (cf_analytics_query, later)" }
33
+ ]
34
+ }
@@ -0,0 +1,192 @@
1
+ {
2
+ "$schema": "./schema.json",
3
+ "format": 1,
4
+ "component": "database",
5
+ "title": "The app's own Supabase database",
6
+ "summary": "Each project has its own Supabase projects, <id>-staging and <id>-production. Apps use them natively through workbench-supabase; Control creates them, writes their URL and keys onto the Workers, and applies migrations read from the repo.",
7
+ "status": "built",
8
+ "surfaces": {
9
+ "package": [
10
+ "supabaseConfig",
11
+ "createBrowserClient",
12
+ "createServerClient",
13
+ "createUserClient",
14
+ "createServiceClient",
15
+ "WorkbenchSupabaseError"
16
+ ],
17
+ "tools": [
18
+ "sb_project_create",
19
+ "sb_migration_apply",
20
+ "sb_migration_rollback",
21
+ "sb_query",
22
+ "sb_project_delete",
23
+ "cf_worker_secret_put"
24
+ ]
25
+ },
26
+ "config": [
27
+ {
28
+ "name": "SUPABASE_URL",
29
+ "required": true,
30
+ "secret": false,
31
+ "environments": [
32
+ "development",
33
+ "staging",
34
+ "production"
35
+ ],
36
+ "description": "https://<ref>.supabase.co of the Worker's own database; written by cf_worker_secret_put source=supabase_url (or the local stack's URL in .dev.vars)"
37
+ },
38
+ {
39
+ "name": "SUPABASE_PUBLISHABLE_KEY",
40
+ "required": true,
41
+ "secret": false,
42
+ "environments": [
43
+ "development",
44
+ "staging",
45
+ "production"
46
+ ],
47
+ "description": "The sb_publishable_ key; written by cf_worker_secret_put source=supabase_publishable. Legacy anon keys are refused"
48
+ },
49
+ {
50
+ "name": "SUPABASE_SECRET_KEY",
51
+ "required": false,
52
+ "secret": true,
53
+ "environments": [
54
+ "staging",
55
+ "production"
56
+ ],
57
+ "description": "The sb_secret_ key, only for code that must bypass RLS (createServiceClient); written by cf_worker_secret_put source=supabase_secret, always two-step"
58
+ }
59
+ ],
60
+ "actions": [
61
+ {
62
+ "tool": "sb_project_create",
63
+ "gate": "free",
64
+ "ships": "3"
65
+ },
66
+ {
67
+ "tool": "sb_migration_apply",
68
+ "gate": "two_step_when",
69
+ "when": "production, or a staging up file that isn't additive",
70
+ "ships": "3"
71
+ },
72
+ {
73
+ "tool": "sb_migration_rollback",
74
+ "gate": "two_step_when",
75
+ "when": "production, or a staging down file that reverses more than its own up file",
76
+ "ships": "3"
77
+ },
78
+ {
79
+ "tool": "sb_query",
80
+ "gate": "two_step_when",
81
+ "when": "a production write, or a staging write outside the additive classifier; reads are free",
82
+ "ships": "3"
83
+ },
84
+ {
85
+ "tool": "sb_project_delete",
86
+ "gate": "two_step",
87
+ "ships": "4"
88
+ },
89
+ {
90
+ "tool": "cf_worker_secret_put",
91
+ "gate": "two_step_when",
92
+ "when": "source supabase_secret, or replacing a secret that exists",
93
+ "ships": "4"
94
+ }
95
+ ],
96
+ "rules": [
97
+ {
98
+ "id": "database.rls-isolates-users",
99
+ "kind": "must",
100
+ "text": "Turn on row level security for every table the publishable key can reach, so one user can't read another's rows.",
101
+ "enforced_by": "database",
102
+ "probe": "database.rls-other-user"
103
+ },
104
+ {
105
+ "id": "database.service-reason",
106
+ "kind": "must",
107
+ "text": "Give createServiceClient a reason saying why RLS is bypassed; it is logged once per isolate.",
108
+ "enforced_by": "types",
109
+ "probe": "database.service-logs-reason"
110
+ },
111
+ {
112
+ "id": "database.no-legacy-keys",
113
+ "kind": "must_not",
114
+ "text": "Use legacy JWT keys (anon, service_role). Only sb_publishable_ and sb_secret_ keys are accepted.",
115
+ "enforced_by": "types",
116
+ "probe": "database.config-named"
117
+ },
118
+ {
119
+ "id": "database.down-files",
120
+ "kind": "must",
121
+ "text": "Give every migration a down file in supabase/rollback; check with npx workbench-supabase check:migrations.",
122
+ "enforced_by": "review",
123
+ "probe": "database.migrations-up-down-up"
124
+ },
125
+ {
126
+ "id": "database.prod-schema",
127
+ "kind": "must_not",
128
+ "text": "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.",
129
+ "enforced_by": "review",
130
+ "probe": "database.prod-schema-check"
131
+ }
132
+ ],
133
+ "examples": [
134
+ {
135
+ "id": "database.example.user-client",
136
+ "title": "Read the caller's own rows",
137
+ "lang": "ts",
138
+ "probe": "database.crud",
139
+ "code": "import { createUserClient, supabaseConfig } from '@mccaskie-labs/workbench-supabase'\n\nconst db = createUserClient({ ...supabaseConfig(env), accessToken })\nconst { data, error } = await db.from('notes').select('id, body')"
140
+ }
141
+ ],
142
+ "probes": [
143
+ {
144
+ "id": "database.crud",
145
+ "half": "runtime",
146
+ "expect": "pass",
147
+ "smoke": true,
148
+ "cost_hint_usd": 0,
149
+ "note": "Insert, read, update and delete, each timed"
150
+ },
151
+ {
152
+ "id": "database.rls-other-user",
153
+ "half": "runtime",
154
+ "expect": "pass",
155
+ "cost_hint_usd": 0,
156
+ "note": "A second user's reads return none of the first user's rows"
157
+ },
158
+ {
159
+ "id": "database.service-logs-reason",
160
+ "half": "runtime",
161
+ "expect": "pass",
162
+ "cost_hint_usd": 0
163
+ },
164
+ {
165
+ "id": "database.config-named",
166
+ "half": "runtime",
167
+ "expect": "pass",
168
+ "cost_hint_usd": 0
169
+ },
170
+ {
171
+ "id": "database.migrations-up-down-up",
172
+ "half": "drill",
173
+ "expect": "pass",
174
+ "cost_hint_usd": 0
175
+ },
176
+ {
177
+ "id": "database.prod-schema-check",
178
+ "half": "runtime",
179
+ "expect": "pass",
180
+ "cost_hint_usd": 0,
181
+ "note": "Manual: run npx workbench-supabase check:prod-schema in the project"
182
+ },
183
+ {
184
+ "id": "database.production-write-two-step",
185
+ "half": "drill",
186
+ "expect": "pass",
187
+ "two_step": true,
188
+ "cost_hint_usd": 0,
189
+ "note": "sb_query on production with a write returns a confirmation, not a result"
190
+ }
191
+ ]
192
+ }
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "9005150c45b1";
1
+ export declare const MANIFEST_VERSION: "068bbebdf4de";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -137,6 +137,68 @@ export declare const capabilities: {
137
137
  readonly cost_hint_usd: 0;
138
138
  }];
139
139
  };
140
+ readonly analytics: {
141
+ readonly $schema: "./schema.json";
142
+ readonly format: 1;
143
+ readonly component: "analytics";
144
+ readonly title: "Page views and product events";
145
+ readonly summary: "Cloudflare Web Analytics counts page views through a beacon on every page; product events go into the app's own database through workbench-analytics. Apps hold no Cloudflare token: Claude reads page views through Control.";
146
+ readonly status: "built";
147
+ readonly surfaces: {
148
+ readonly tools: readonly ["cf_web_analytics_site_create"];
149
+ readonly package: readonly ["beaconScript", "createEvents", "eventsMigration"];
150
+ };
151
+ readonly actions: readonly [{
152
+ readonly tool: "cf_web_analytics_site_create";
153
+ readonly gate: "free";
154
+ readonly ships: "4";
155
+ }];
156
+ readonly rules: readonly [{
157
+ readonly id: "analytics.beacon-on-pages";
158
+ readonly kind: "must";
159
+ readonly text: "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.";
160
+ readonly enforced_by: "review";
161
+ readonly probe: "analytics.beacon-present";
162
+ }, {
163
+ readonly id: "analytics.track-never-throws";
164
+ readonly kind: "must_not";
165
+ readonly text: "Let a failed event write break the request it describes: track logs and drops it.";
166
+ readonly enforced_by: "types";
167
+ readonly probe: "analytics.event-round-trip";
168
+ }, {
169
+ readonly id: "analytics.no-cf-token";
170
+ readonly kind: "must_not";
171
+ readonly text: "Hold a Cloudflare analytics token in the app (hard rule 1); page views are read with Control.";
172
+ readonly enforced_by: "review";
173
+ readonly probe: "analytics.page-views-drill";
174
+ }];
175
+ readonly examples: readonly [{
176
+ readonly id: "analytics.example.track";
177
+ readonly title: "Record a product event";
178
+ readonly lang: "ts";
179
+ readonly code: "import { createEvents } from '@mccaskie-labs/workbench-analytics'\n\nawait createEvents(db).track('note.created', { length: body.length }, { type: 'note', id })";
180
+ readonly probe: "analytics.event-round-trip";
181
+ }];
182
+ readonly probes: readonly [{
183
+ readonly id: "analytics.beacon-present";
184
+ readonly half: "runtime";
185
+ readonly expect: "pass";
186
+ readonly smoke: true;
187
+ readonly cost_hint_usd: 0;
188
+ }, {
189
+ readonly id: "analytics.event-round-trip";
190
+ readonly half: "runtime";
191
+ readonly expect: "pass";
192
+ readonly cost_hint_usd: 0;
193
+ readonly note: "A product event is written and read back";
194
+ }, {
195
+ readonly id: "analytics.page-views-drill";
196
+ readonly half: "drill";
197
+ readonly expect: "pass";
198
+ readonly cost_hint_usd: 0;
199
+ readonly note: "Create the site, drive page views, query them back (cf_analytics_query, later)";
200
+ }];
201
+ };
140
202
  readonly config: {
141
203
  readonly $schema: "./schema.json";
142
204
  readonly format: 1;
@@ -214,6 +276,146 @@ export declare const capabilities: {
214
276
  readonly note: "Manual: run npx workbench-config check in the project";
215
277
  }];
216
278
  };
279
+ readonly database: {
280
+ readonly $schema: "./schema.json";
281
+ readonly format: 1;
282
+ readonly component: "database";
283
+ readonly title: "The app's own Supabase database";
284
+ readonly summary: "Each project has its own Supabase projects, <id>-staging and <id>-production. Apps use them natively through workbench-supabase; Control creates them, writes their URL and keys onto the Workers, and applies migrations read from the repo.";
285
+ readonly status: "built";
286
+ readonly surfaces: {
287
+ readonly tools: readonly ["sb_project_create", "sb_migration_apply", "sb_migration_rollback", "sb_query", "sb_project_delete", "cf_worker_secret_put"];
288
+ readonly package: readonly ["supabaseConfig", "createBrowserClient", "createServerClient", "createUserClient", "createServiceClient", "WorkbenchSupabaseError"];
289
+ };
290
+ readonly config: readonly [{
291
+ readonly name: "SUPABASE_URL";
292
+ readonly required: true;
293
+ readonly secret: false;
294
+ readonly environments: readonly ["development", "staging", "production"];
295
+ readonly description: "https://<ref>.supabase.co of the Worker's own database; written by cf_worker_secret_put source=supabase_url (or the local stack's URL in .dev.vars)";
296
+ }, {
297
+ readonly name: "SUPABASE_PUBLISHABLE_KEY";
298
+ readonly required: true;
299
+ readonly secret: false;
300
+ readonly environments: readonly ["development", "staging", "production"];
301
+ readonly description: "The sb_publishable_ key; written by cf_worker_secret_put source=supabase_publishable. Legacy anon keys are refused";
302
+ }, {
303
+ readonly name: "SUPABASE_SECRET_KEY";
304
+ readonly required: false;
305
+ readonly secret: true;
306
+ readonly environments: readonly ["staging", "production"];
307
+ readonly description: "The sb_secret_ key, only for code that must bypass RLS (createServiceClient); written by cf_worker_secret_put source=supabase_secret, always two-step";
308
+ }];
309
+ readonly actions: readonly [{
310
+ readonly tool: "sb_project_create";
311
+ readonly gate: "free";
312
+ readonly ships: "3";
313
+ }, {
314
+ readonly tool: "sb_migration_apply";
315
+ readonly gate: "two_step_when";
316
+ readonly when: "production, or a staging up file that isn't additive";
317
+ readonly ships: "3";
318
+ }, {
319
+ readonly tool: "sb_migration_rollback";
320
+ readonly gate: "two_step_when";
321
+ readonly when: "production, or a staging down file that reverses more than its own up file";
322
+ readonly ships: "3";
323
+ }, {
324
+ readonly tool: "sb_query";
325
+ readonly gate: "two_step_when";
326
+ readonly when: "a production write, or a staging write outside the additive classifier; reads are free";
327
+ readonly ships: "3";
328
+ }, {
329
+ readonly tool: "sb_project_delete";
330
+ readonly gate: "two_step";
331
+ readonly ships: "4";
332
+ }, {
333
+ readonly tool: "cf_worker_secret_put";
334
+ readonly gate: "two_step_when";
335
+ readonly when: "source supabase_secret, or replacing a secret that exists";
336
+ readonly ships: "4";
337
+ }];
338
+ readonly rules: readonly [{
339
+ readonly id: "database.rls-isolates-users";
340
+ readonly kind: "must";
341
+ readonly text: "Turn on row level security for every table the publishable key can reach, so one user can't read another's rows.";
342
+ readonly enforced_by: "database";
343
+ readonly probe: "database.rls-other-user";
344
+ }, {
345
+ readonly id: "database.service-reason";
346
+ readonly kind: "must";
347
+ readonly text: "Give createServiceClient a reason saying why RLS is bypassed; it is logged once per isolate.";
348
+ readonly enforced_by: "types";
349
+ readonly probe: "database.service-logs-reason";
350
+ }, {
351
+ readonly id: "database.no-legacy-keys";
352
+ readonly kind: "must_not";
353
+ readonly text: "Use legacy JWT keys (anon, service_role). Only sb_publishable_ and sb_secret_ keys are accepted.";
354
+ readonly enforced_by: "types";
355
+ readonly probe: "database.config-named";
356
+ }, {
357
+ readonly id: "database.down-files";
358
+ readonly kind: "must";
359
+ readonly text: "Give every migration a down file in supabase/rollback; check with npx workbench-supabase check:migrations.";
360
+ readonly enforced_by: "review";
361
+ readonly probe: "database.migrations-up-down-up";
362
+ }, {
363
+ readonly id: "database.prod-schema";
364
+ readonly kind: "must_not";
365
+ readonly text: "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.";
366
+ readonly enforced_by: "review";
367
+ readonly probe: "database.prod-schema-check";
368
+ }];
369
+ readonly examples: readonly [{
370
+ readonly id: "database.example.user-client";
371
+ readonly title: "Read the caller's own rows";
372
+ readonly lang: "ts";
373
+ readonly code: "import { createUserClient, supabaseConfig } from '@mccaskie-labs/workbench-supabase'\n\nconst db = createUserClient({ ...supabaseConfig(env), accessToken })\nconst { data, error } = await db.from('notes').select('id, body')";
374
+ readonly probe: "database.crud";
375
+ }];
376
+ readonly probes: readonly [{
377
+ readonly id: "database.crud";
378
+ readonly half: "runtime";
379
+ readonly expect: "pass";
380
+ readonly smoke: true;
381
+ readonly cost_hint_usd: 0;
382
+ readonly note: "Insert, read, update and delete, each timed";
383
+ }, {
384
+ readonly id: "database.rls-other-user";
385
+ readonly half: "runtime";
386
+ readonly expect: "pass";
387
+ readonly cost_hint_usd: 0;
388
+ readonly note: "A second user's reads return none of the first user's rows";
389
+ }, {
390
+ readonly id: "database.service-logs-reason";
391
+ readonly half: "runtime";
392
+ readonly expect: "pass";
393
+ readonly cost_hint_usd: 0;
394
+ }, {
395
+ readonly id: "database.config-named";
396
+ readonly half: "runtime";
397
+ readonly expect: "pass";
398
+ readonly cost_hint_usd: 0;
399
+ }, {
400
+ readonly id: "database.migrations-up-down-up";
401
+ readonly half: "drill";
402
+ readonly expect: "pass";
403
+ readonly cost_hint_usd: 0;
404
+ }, {
405
+ readonly id: "database.prod-schema-check";
406
+ readonly half: "runtime";
407
+ readonly expect: "pass";
408
+ readonly cost_hint_usd: 0;
409
+ readonly note: "Manual: run npx workbench-supabase check:prod-schema in the project";
410
+ }, {
411
+ readonly id: "database.production-write-two-step";
412
+ readonly half: "drill";
413
+ readonly expect: "pass";
414
+ readonly cost_hint_usd: 0;
415
+ readonly two_step: true;
416
+ readonly note: "sb_query on production with a write returns a confirmation, not a result";
417
+ }];
418
+ };
217
419
  readonly gateway: {
218
420
  readonly $schema: "./schema.json";
219
421
  readonly format: 1;
@@ -1,6 +1,6 @@
1
1
  // Generated by `npm run capabilities:build` from capabilities/*.json. Do not edit.
2
2
  // check:capabilities fails in Checks when this file is stale.
3
- export const MANIFEST_VERSION = "9005150c45b1";
3
+ export const MANIFEST_VERSION = "068bbebdf4de";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -194,6 +194,86 @@ export const capabilities = {
194
194
  }
195
195
  ]
196
196
  },
197
+ "analytics": {
198
+ "$schema": "./schema.json",
199
+ "format": 1,
200
+ "component": "analytics",
201
+ "title": "Page views and product events",
202
+ "summary": "Cloudflare Web Analytics counts page views through a beacon on every page; product events go into the app's own database through workbench-analytics. Apps hold no Cloudflare token: Claude reads page views through Control.",
203
+ "status": "built",
204
+ "surfaces": {
205
+ "tools": [
206
+ "cf_web_analytics_site_create"
207
+ ],
208
+ "package": [
209
+ "beaconScript",
210
+ "createEvents",
211
+ "eventsMigration"
212
+ ]
213
+ },
214
+ "actions": [
215
+ {
216
+ "tool": "cf_web_analytics_site_create",
217
+ "gate": "free",
218
+ "ships": "4"
219
+ }
220
+ ],
221
+ "rules": [
222
+ {
223
+ "id": "analytics.beacon-on-pages",
224
+ "kind": "must",
225
+ "text": "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.",
226
+ "enforced_by": "review",
227
+ "probe": "analytics.beacon-present"
228
+ },
229
+ {
230
+ "id": "analytics.track-never-throws",
231
+ "kind": "must_not",
232
+ "text": "Let a failed event write break the request it describes: track logs and drops it.",
233
+ "enforced_by": "types",
234
+ "probe": "analytics.event-round-trip"
235
+ },
236
+ {
237
+ "id": "analytics.no-cf-token",
238
+ "kind": "must_not",
239
+ "text": "Hold a Cloudflare analytics token in the app (hard rule 1); page views are read with Control.",
240
+ "enforced_by": "review",
241
+ "probe": "analytics.page-views-drill"
242
+ }
243
+ ],
244
+ "examples": [
245
+ {
246
+ "id": "analytics.example.track",
247
+ "title": "Record a product event",
248
+ "lang": "ts",
249
+ "code": "import { createEvents } from '@mccaskie-labs/workbench-analytics'\n\nawait createEvents(db).track('note.created', { length: body.length }, { type: 'note', id })",
250
+ "probe": "analytics.event-round-trip"
251
+ }
252
+ ],
253
+ "probes": [
254
+ {
255
+ "id": "analytics.beacon-present",
256
+ "half": "runtime",
257
+ "expect": "pass",
258
+ "smoke": true,
259
+ "cost_hint_usd": 0
260
+ },
261
+ {
262
+ "id": "analytics.event-round-trip",
263
+ "half": "runtime",
264
+ "expect": "pass",
265
+ "cost_hint_usd": 0,
266
+ "note": "A product event is written and read back"
267
+ },
268
+ {
269
+ "id": "analytics.page-views-drill",
270
+ "half": "drill",
271
+ "expect": "pass",
272
+ "cost_hint_usd": 0,
273
+ "note": "Create the site, drive page views, query them back (cf_analytics_query, later)"
274
+ }
275
+ ]
276
+ },
197
277
  "config": {
198
278
  "$schema": "./schema.json",
199
279
  "format": 1,
@@ -305,6 +385,198 @@ export const capabilities = {
305
385
  }
306
386
  ]
307
387
  },
388
+ "database": {
389
+ "$schema": "./schema.json",
390
+ "format": 1,
391
+ "component": "database",
392
+ "title": "The app's own Supabase database",
393
+ "summary": "Each project has its own Supabase projects, <id>-staging and <id>-production. Apps use them natively through workbench-supabase; Control creates them, writes their URL and keys onto the Workers, and applies migrations read from the repo.",
394
+ "status": "built",
395
+ "surfaces": {
396
+ "tools": [
397
+ "sb_project_create",
398
+ "sb_migration_apply",
399
+ "sb_migration_rollback",
400
+ "sb_query",
401
+ "sb_project_delete",
402
+ "cf_worker_secret_put"
403
+ ],
404
+ "package": [
405
+ "supabaseConfig",
406
+ "createBrowserClient",
407
+ "createServerClient",
408
+ "createUserClient",
409
+ "createServiceClient",
410
+ "WorkbenchSupabaseError"
411
+ ]
412
+ },
413
+ "config": [
414
+ {
415
+ "name": "SUPABASE_URL",
416
+ "required": true,
417
+ "secret": false,
418
+ "environments": [
419
+ "development",
420
+ "staging",
421
+ "production"
422
+ ],
423
+ "description": "https://<ref>.supabase.co of the Worker's own database; written by cf_worker_secret_put source=supabase_url (or the local stack's URL in .dev.vars)"
424
+ },
425
+ {
426
+ "name": "SUPABASE_PUBLISHABLE_KEY",
427
+ "required": true,
428
+ "secret": false,
429
+ "environments": [
430
+ "development",
431
+ "staging",
432
+ "production"
433
+ ],
434
+ "description": "The sb_publishable_ key; written by cf_worker_secret_put source=supabase_publishable. Legacy anon keys are refused"
435
+ },
436
+ {
437
+ "name": "SUPABASE_SECRET_KEY",
438
+ "required": false,
439
+ "secret": true,
440
+ "environments": [
441
+ "staging",
442
+ "production"
443
+ ],
444
+ "description": "The sb_secret_ key, only for code that must bypass RLS (createServiceClient); written by cf_worker_secret_put source=supabase_secret, always two-step"
445
+ }
446
+ ],
447
+ "actions": [
448
+ {
449
+ "tool": "sb_project_create",
450
+ "gate": "free",
451
+ "ships": "3"
452
+ },
453
+ {
454
+ "tool": "sb_migration_apply",
455
+ "gate": "two_step_when",
456
+ "when": "production, or a staging up file that isn't additive",
457
+ "ships": "3"
458
+ },
459
+ {
460
+ "tool": "sb_migration_rollback",
461
+ "gate": "two_step_when",
462
+ "when": "production, or a staging down file that reverses more than its own up file",
463
+ "ships": "3"
464
+ },
465
+ {
466
+ "tool": "sb_query",
467
+ "gate": "two_step_when",
468
+ "when": "a production write, or a staging write outside the additive classifier; reads are free",
469
+ "ships": "3"
470
+ },
471
+ {
472
+ "tool": "sb_project_delete",
473
+ "gate": "two_step",
474
+ "ships": "4"
475
+ },
476
+ {
477
+ "tool": "cf_worker_secret_put",
478
+ "gate": "two_step_when",
479
+ "when": "source supabase_secret, or replacing a secret that exists",
480
+ "ships": "4"
481
+ }
482
+ ],
483
+ "rules": [
484
+ {
485
+ "id": "database.rls-isolates-users",
486
+ "kind": "must",
487
+ "text": "Turn on row level security for every table the publishable key can reach, so one user can't read another's rows.",
488
+ "enforced_by": "database",
489
+ "probe": "database.rls-other-user"
490
+ },
491
+ {
492
+ "id": "database.service-reason",
493
+ "kind": "must",
494
+ "text": "Give createServiceClient a reason saying why RLS is bypassed; it is logged once per isolate.",
495
+ "enforced_by": "types",
496
+ "probe": "database.service-logs-reason"
497
+ },
498
+ {
499
+ "id": "database.no-legacy-keys",
500
+ "kind": "must_not",
501
+ "text": "Use legacy JWT keys (anon, service_role). Only sb_publishable_ and sb_secret_ keys are accepted.",
502
+ "enforced_by": "types",
503
+ "probe": "database.config-named"
504
+ },
505
+ {
506
+ "id": "database.down-files",
507
+ "kind": "must",
508
+ "text": "Give every migration a down file in supabase/rollback; check with npx workbench-supabase check:migrations.",
509
+ "enforced_by": "review",
510
+ "probe": "database.migrations-up-down-up"
511
+ },
512
+ {
513
+ "id": "database.prod-schema",
514
+ "kind": "must_not",
515
+ "text": "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.",
516
+ "enforced_by": "review",
517
+ "probe": "database.prod-schema-check"
518
+ }
519
+ ],
520
+ "examples": [
521
+ {
522
+ "id": "database.example.user-client",
523
+ "title": "Read the caller's own rows",
524
+ "lang": "ts",
525
+ "code": "import { createUserClient, supabaseConfig } from '@mccaskie-labs/workbench-supabase'\n\nconst db = createUserClient({ ...supabaseConfig(env), accessToken })\nconst { data, error } = await db.from('notes').select('id, body')",
526
+ "probe": "database.crud"
527
+ }
528
+ ],
529
+ "probes": [
530
+ {
531
+ "id": "database.crud",
532
+ "half": "runtime",
533
+ "expect": "pass",
534
+ "smoke": true,
535
+ "cost_hint_usd": 0,
536
+ "note": "Insert, read, update and delete, each timed"
537
+ },
538
+ {
539
+ "id": "database.rls-other-user",
540
+ "half": "runtime",
541
+ "expect": "pass",
542
+ "cost_hint_usd": 0,
543
+ "note": "A second user's reads return none of the first user's rows"
544
+ },
545
+ {
546
+ "id": "database.service-logs-reason",
547
+ "half": "runtime",
548
+ "expect": "pass",
549
+ "cost_hint_usd": 0
550
+ },
551
+ {
552
+ "id": "database.config-named",
553
+ "half": "runtime",
554
+ "expect": "pass",
555
+ "cost_hint_usd": 0
556
+ },
557
+ {
558
+ "id": "database.migrations-up-down-up",
559
+ "half": "drill",
560
+ "expect": "pass",
561
+ "cost_hint_usd": 0
562
+ },
563
+ {
564
+ "id": "database.prod-schema-check",
565
+ "half": "runtime",
566
+ "expect": "pass",
567
+ "cost_hint_usd": 0,
568
+ "note": "Manual: run npx workbench-supabase check:prod-schema in the project"
569
+ },
570
+ {
571
+ "id": "database.production-write-two-step",
572
+ "half": "drill",
573
+ "expect": "pass",
574
+ "cost_hint_usd": 0,
575
+ "two_step": true,
576
+ "note": "sb_query on production with a write returns a confirmation, not a result"
577
+ }
578
+ ]
579
+ },
308
580
  "gateway": {
309
581
  "$schema": "./schema.json",
310
582
  "format": 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",