@mccaskie-labs/workbench-config 0.1.3 → 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 +12 -0
- package/capabilities/ai.json +4 -3
- package/capabilities/analytics.json +34 -0
- package/capabilities/database.json +192 -0
- package/dist/capabilities.gen.d.ts +211 -3
- package/dist/capabilities.gen.js +282 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.1.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- The `ai` manifest is built (step 4.7): `Ai.countTokens` in its package surface and an `ai.budget` probe.
|
|
14
|
+
|
|
3
15
|
## 0.1.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/capabilities/ai.json
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"component": "ai",
|
|
5
5
|
"title": "Claude through the Gateway",
|
|
6
6
|
"summary": "Text, structured output and streaming from Claude, metered against the project's budget. Apps never hold an Anthropic key.",
|
|
7
|
-
"status": "
|
|
7
|
+
"status": "built",
|
|
8
8
|
"surfaces": {
|
|
9
9
|
"gateway": ["POST /anthropic/v1/messages", "POST /anthropic/v1/messages/count_tokens"],
|
|
10
|
-
"package": ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.budget", "createFakeAi"]
|
|
10
|
+
"package": ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi"]
|
|
11
11
|
},
|
|
12
12
|
"config": [
|
|
13
13
|
{
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
{ "id": "ai.structured", "half": "runtime", "expect": "pass", "cost_hint_usd": 0.003 },
|
|
65
65
|
{ "id": "ai.text.truncation-throws", "half": "runtime", "expect": "pass", "cost_hint_usd": 0.001 },
|
|
66
66
|
{ "id": "ai.model-not-allowed", "half": "runtime", "expect": "refused_as_expected", "refusal": "model_not_allowed", "cost_hint_usd": 0 },
|
|
67
|
-
{ "id": "ai.count-tokens", "half": "runtime", "expect": "pass", "smoke": true, "cost_hint_usd": 0 }
|
|
67
|
+
{ "id": "ai.count-tokens", "half": "runtime", "expect": "pass", "smoke": true, "cost_hint_usd": 0 },
|
|
68
|
+
{ "id": "ai.budget", "half": "runtime", "expect": "pass", "smoke": true, "cost_hint_usd": 0 }
|
|
68
69
|
]
|
|
69
70
|
}
|
|
@@ -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: "
|
|
1
|
+
export declare const MANIFEST_VERSION: "068bbebdf4de";
|
|
2
2
|
export declare const capabilities: {
|
|
3
3
|
readonly ai: {
|
|
4
4
|
readonly $schema: "./schema.json";
|
|
@@ -6,10 +6,10 @@ export declare const capabilities: {
|
|
|
6
6
|
readonly component: "ai";
|
|
7
7
|
readonly title: "Claude through the Gateway";
|
|
8
8
|
readonly summary: "Text, structured output and streaming from Claude, metered against the project's budget. Apps never hold an Anthropic key.";
|
|
9
|
-
readonly status: "
|
|
9
|
+
readonly status: "built";
|
|
10
10
|
readonly surfaces: {
|
|
11
11
|
readonly gateway: readonly ["POST /anthropic/v1/messages", "POST /anthropic/v1/messages/count_tokens"];
|
|
12
|
-
readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.budget", "createFakeAi"];
|
|
12
|
+
readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi"];
|
|
13
13
|
};
|
|
14
14
|
readonly config: readonly [{
|
|
15
15
|
readonly name: "WORKBENCH_GATEWAY_KEY";
|
|
@@ -129,6 +129,74 @@ export declare const capabilities: {
|
|
|
129
129
|
readonly expect: "pass";
|
|
130
130
|
readonly smoke: true;
|
|
131
131
|
readonly cost_hint_usd: 0;
|
|
132
|
+
}, {
|
|
133
|
+
readonly id: "ai.budget";
|
|
134
|
+
readonly half: "runtime";
|
|
135
|
+
readonly expect: "pass";
|
|
136
|
+
readonly smoke: true;
|
|
137
|
+
readonly cost_hint_usd: 0;
|
|
138
|
+
}];
|
|
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)";
|
|
132
200
|
}];
|
|
133
201
|
};
|
|
134
202
|
readonly config: {
|
|
@@ -208,6 +276,146 @@ export declare const capabilities: {
|
|
|
208
276
|
readonly note: "Manual: run npx workbench-config check in the project";
|
|
209
277
|
}];
|
|
210
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
|
+
};
|
|
211
419
|
readonly gateway: {
|
|
212
420
|
readonly $schema: "./schema.json";
|
|
213
421
|
readonly format: 1;
|
package/dist/capabilities.gen.js
CHANGED
|
@@ -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 = "
|
|
3
|
+
export const MANIFEST_VERSION = "068bbebdf4de";
|
|
4
4
|
export const capabilities = {
|
|
5
5
|
"ai": {
|
|
6
6
|
"$schema": "./schema.json",
|
|
@@ -8,7 +8,7 @@ export const capabilities = {
|
|
|
8
8
|
"component": "ai",
|
|
9
9
|
"title": "Claude through the Gateway",
|
|
10
10
|
"summary": "Text, structured output and streaming from Claude, metered against the project's budget. Apps never hold an Anthropic key.",
|
|
11
|
-
"status": "
|
|
11
|
+
"status": "built",
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"gateway": [
|
|
14
14
|
"POST /anthropic/v1/messages",
|
|
@@ -19,6 +19,7 @@ export const capabilities = {
|
|
|
19
19
|
"Ai.text",
|
|
20
20
|
"Ai.structured",
|
|
21
21
|
"Ai.stream",
|
|
22
|
+
"Ai.countTokens",
|
|
22
23
|
"Ai.budget",
|
|
23
24
|
"createFakeAi"
|
|
24
25
|
]
|
|
@@ -183,6 +184,93 @@ export const capabilities = {
|
|
|
183
184
|
"expect": "pass",
|
|
184
185
|
"smoke": true,
|
|
185
186
|
"cost_hint_usd": 0
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "ai.budget",
|
|
190
|
+
"half": "runtime",
|
|
191
|
+
"expect": "pass",
|
|
192
|
+
"smoke": true,
|
|
193
|
+
"cost_hint_usd": 0
|
|
194
|
+
}
|
|
195
|
+
]
|
|
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)"
|
|
186
274
|
}
|
|
187
275
|
]
|
|
188
276
|
},
|
|
@@ -297,6 +385,198 @@ export const capabilities = {
|
|
|
297
385
|
}
|
|
298
386
|
]
|
|
299
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
|
+
},
|
|
300
580
|
"gateway": {
|
|
301
581
|
"$schema": "./schema.json",
|
|
302
582
|
"format": 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mccaskie-labs/workbench-config",
|
|
3
|
-
"version": "0.1.
|
|
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",
|