@mccaskie-labs/workbench-config 0.1.41 → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +0 -1
- package/capabilities/ai.json +1 -1
- package/capabilities/database.json +2 -71
- package/capabilities/email.json +1 -1
- package/capabilities/gateway.json +3 -5
- package/capabilities/image-search.json +1 -1
- package/capabilities/images.json +1 -1
- package/capabilities/schema.json +34 -2
- package/capabilities/sign-in.json +137 -0
- package/dist/capabilities.gen.d.ts +101 -55
- package/dist/capabilities.gen.js +146 -82
- package/dist/capability-schema.d.ts +27 -1
- package/dist/capability-schema.js +30 -5
- package/dist/env.d.ts +0 -1
- package/dist/env.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @mccaskie-labs/workbench-config
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Sign-in gets its own component, `sign-in`: a built-in skill (`workbench-sign-in`) that `workbench-start` lists, with one client question (which pages and actions need a signed-in person, who among them may use each, and which also need a fresh code), written to `workbench/sign-in.json`. Its surfaces, config (`SESSION_SECRET`, `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`), Turnstile actions and three rules move out of `database` (rule ids now `sign-in.server-side`, `sign-in.writes-same-origin`, `sign-in.checks-for-bots`); the probes keep their `database.*` ids. A new optional `proved_by` lets a manifest's rules name probes in the components it lists (sign-in names `database`); only then may its own `probes` be empty.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Breaking: there is no built-in Gateway address any more (hard rule 13, `docs/install-plan.md`). `DEFAULT_GATEWAY_URL` is removed from workbench-config, `createAi` needs `url`, and `createMailer`'s gateway transport needs `gateway.url`. Pass the app's `WORKBENCH_GATEWAY_URL`, which Control writes into `wrangler.jsonc` when it creates a project. A missing address fails at start-up with `invalid_config` and the message "WORKBENCH_GATEWAY_URL is missing". The gateway manifest marks `WORKBENCH_GATEWAY_URL` required with no default.
|
|
14
|
+
|
|
3
15
|
## 0.1.41
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -30,7 +30,6 @@ const { WORKBENCH_GATEWAY_KEY } = config.parse(env)
|
|
|
30
30
|
| `defineBrand(json)`, `brandSchema`, `resolveBrand`, `BRAND_DEFAULTS`, `BRAND_FILE` | The app's brand file, `brand.json` at the repo root: `name`, `siteUrl` (https), `logo.url` (https PNG, JPEG or SVG; optional `alt`, `width`, `height`), `colours.primary` (`#rrggbb`; `text`, `background`, `muted` default), optional `fonts.heading`/`fonts.body` (`{ family, url? }`, `url` an https stylesheet), `supportEmail` and `footer`. Unknown keys fail. Account emails, PDFs and Supabase's sign-in emails all read it |
|
|
31
31
|
| `checkLogo(bytes)`, `placeholderLogo(name, colour)`, `colourForId(id)`, `parseColour`, `whiteContrast`, `shownSize`, `LOGO_PATHS`, `MAX_LOGO_BYTES`, `BrandAssetError` | Setting a brand from an upload: the logo is checked by its own bytes (PNG or JPEG, at most 256 KiB; SVG refused because Gmail and Outlook don't show it), a project with no logo gets its initials on its colour as a 96 × 96 PNG, and a project with no colour gets a steady one from its id that white text reads on (WCAG 4.5:1). Runs in Workers, the browser and Node |
|
|
32
32
|
| `VENDOR_SETTINGS`, `resolveVendorSettings(file, env)`, `vendorSettingsFileSchema`, `VENDOR_SETTINGS_FILE`, `SEND_EMAIL_HOOK`, `SEND_EMAIL_HOOK_PATH`, `SEND_EMAIL_HOOK_SECRET` | The vendor settings baseline: settings that live in a vendor dashboard (field, value, where it is, what breaks if it changes). A project changes a row in `vendor-settings.json` at its repo root, with a reason. Control applies it with `vendor_settings_apply` and compares it every day |
|
|
33
|
-
| `DEFAULT_GATEWAY_URL` | `https://gateway.mccaskielabs.dev` |
|
|
34
33
|
|
|
35
34
|
`npx workbench-config check [dir]` compares the variable names in `.dev.vars` with `.dev.vars.example` and prints names only.
|
|
36
35
|
|
package/capabilities/ai.json
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"title": "One-shot text",
|
|
96
96
|
"lang": "ts",
|
|
97
97
|
"probe": "ai.text.claude-default",
|
|
98
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })"
|
|
98
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"id": "ai.example.voice",
|
|
@@ -17,10 +17,7 @@
|
|
|
17
17
|
"createServerClient",
|
|
18
18
|
"createUserClient",
|
|
19
19
|
"createServiceClient",
|
|
20
|
-
"WorkbenchSupabaseError"
|
|
21
|
-
"createSessions",
|
|
22
|
-
"safeReturnPath",
|
|
23
|
-
"sameOrigin"
|
|
20
|
+
"WorkbenchSupabaseError"
|
|
24
21
|
],
|
|
25
22
|
"tools": [
|
|
26
23
|
"sb_project_create",
|
|
@@ -29,9 +26,7 @@
|
|
|
29
26
|
"sb_query",
|
|
30
27
|
"sb_project_delete",
|
|
31
28
|
"cf_worker_secret_put",
|
|
32
|
-
"vendor_settings_apply"
|
|
33
|
-
"cf_turnstile_widget_create",
|
|
34
|
-
"cf_turnstile_widget_delete"
|
|
29
|
+
"vendor_settings_apply"
|
|
35
30
|
]
|
|
36
31
|
},
|
|
37
32
|
"config": [
|
|
@@ -66,39 +61,6 @@
|
|
|
66
61
|
"production"
|
|
67
62
|
],
|
|
68
63
|
"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"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"name": "SESSION_SECRET",
|
|
72
|
-
"required": false,
|
|
73
|
-
"secret": true,
|
|
74
|
-
"environments": [
|
|
75
|
-
"development",
|
|
76
|
-
"staging",
|
|
77
|
-
"production"
|
|
78
|
-
],
|
|
79
|
-
"description": "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in"
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"name": "TURNSTILE_SITE_KEY",
|
|
83
|
-
"required": false,
|
|
84
|
-
"secret": false,
|
|
85
|
-
"environments": [
|
|
86
|
-
"development",
|
|
87
|
-
"staging",
|
|
88
|
-
"production"
|
|
89
|
-
],
|
|
90
|
-
"description": "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"name": "TURNSTILE_SECRET_KEY",
|
|
94
|
-
"required": false,
|
|
95
|
-
"secret": true,
|
|
96
|
-
"environments": [
|
|
97
|
-
"development",
|
|
98
|
-
"staging",
|
|
99
|
-
"production"
|
|
100
|
-
],
|
|
101
|
-
"description": "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it"
|
|
102
64
|
}
|
|
103
65
|
],
|
|
104
66
|
"actions": [
|
|
@@ -141,16 +103,6 @@
|
|
|
141
103
|
"gate": "two_step_when",
|
|
142
104
|
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
143
105
|
"ships": "5"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
"tool": "cf_turnstile_widget_create",
|
|
147
|
-
"gate": "free",
|
|
148
|
-
"ships": "5"
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
"tool": "cf_turnstile_widget_delete",
|
|
152
|
-
"gate": "two_step",
|
|
153
|
-
"ships": "5"
|
|
154
106
|
}
|
|
155
107
|
],
|
|
156
108
|
"rules": [
|
|
@@ -195,27 +147,6 @@
|
|
|
195
147
|
"text": "Keep Supabase's sign-in settings at the Workbench baseline (workbench-config VENDOR_SETTINGS), or record a project's change in vendor-settings.json with a reason. Control applies it with vendor_settings_apply and checks it every day.",
|
|
196
148
|
"enforced_by": "control",
|
|
197
149
|
"probe": "database.vendor-settings"
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
"id": "database.sign-in-server-side",
|
|
201
|
-
"kind": "must",
|
|
202
|
-
"text": "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.",
|
|
203
|
-
"enforced_by": "review",
|
|
204
|
-
"probe": "database.sign-in"
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"id": "database.session-writes-same-origin",
|
|
208
|
-
"kind": "must",
|
|
209
|
-
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
210
|
-
"enforced_by": "review",
|
|
211
|
-
"probe": "database.session-cookie"
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
"id": "database.sign-in-checks-for-bots",
|
|
215
|
-
"kind": "must",
|
|
216
|
-
"text": "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.",
|
|
217
|
-
"enforced_by": "review",
|
|
218
|
-
"probe": "database.sign-in-bot-check"
|
|
219
150
|
}
|
|
220
151
|
],
|
|
221
152
|
"examples": [
|
package/capabilities/email.json
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"title": "Send a transactional mail with an inline logo",
|
|
53
53
|
"lang": "ts",
|
|
54
54
|
"probe": "email.send.inline-attachment",
|
|
55
|
-
"code": "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })"
|
|
55
|
+
"code": "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY!, url: process.env.WORKBENCH_GATEWAY_URL! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })"
|
|
56
56
|
}
|
|
57
57
|
],
|
|
58
58
|
"probes": [
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
"summary": "What every Gateway endpoint shares: project keys by environment and scope, the request and response headers, the monthly budget check and the error codes. Apps hold one Workbench key and never a vendor key.",
|
|
7
7
|
"status": "built",
|
|
8
8
|
"surfaces": {
|
|
9
|
-
"gateway": ["GET /v1/health", "GET /v1/usage", "GET /v1/capabilities", "GET /v1/calls"]
|
|
10
|
-
"package": ["DEFAULT_GATEWAY_URL"]
|
|
9
|
+
"gateway": ["GET /v1/health", "GET /v1/usage", "GET /v1/capabilities", "GET /v1/calls"]
|
|
11
10
|
},
|
|
12
11
|
"config": [
|
|
13
12
|
{
|
|
@@ -19,11 +18,10 @@
|
|
|
19
18
|
},
|
|
20
19
|
{
|
|
21
20
|
"name": "WORKBENCH_GATEWAY_URL",
|
|
22
|
-
"required":
|
|
21
|
+
"required": true,
|
|
23
22
|
"secret": false,
|
|
24
|
-
"default": "https://gateway.mccaskielabs.dev",
|
|
25
23
|
"environments": ["development", "staging", "production"],
|
|
26
|
-
"description": "Gateway
|
|
24
|
+
"description": "The address of this Workbench install's Gateway. Control writes it into wrangler.jsonc when it creates the project; there is no built-in address"
|
|
27
25
|
}
|
|
28
26
|
],
|
|
29
27
|
"allowed": {
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"title": "Search for images",
|
|
121
121
|
"lang": "ts",
|
|
122
122
|
"probe": "image-search.serper",
|
|
123
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })"
|
|
123
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })"
|
|
124
124
|
}
|
|
125
125
|
],
|
|
126
126
|
"probes": [
|
package/capabilities/images.json
CHANGED
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"title": "Generate one image",
|
|
167
167
|
"lang": "ts",
|
|
168
168
|
"probe": "images.generate.image-default",
|
|
169
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })"
|
|
169
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })"
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
"id": "images.example.reference",
|
package/capabilities/schema.json
CHANGED
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"github",
|
|
35
35
|
"strings",
|
|
36
36
|
"ui",
|
|
37
|
-
"forms"
|
|
37
|
+
"forms",
|
|
38
|
+
"sign-in"
|
|
38
39
|
]
|
|
39
40
|
},
|
|
40
41
|
"title": {
|
|
@@ -352,7 +353,6 @@
|
|
|
352
353
|
}
|
|
353
354
|
},
|
|
354
355
|
"probes": {
|
|
355
|
-
"minItems": 1,
|
|
356
356
|
"type": "array",
|
|
357
357
|
"items": {
|
|
358
358
|
"type": "object",
|
|
@@ -402,6 +402,38 @@
|
|
|
402
402
|
"additionalProperties": false
|
|
403
403
|
}
|
|
404
404
|
},
|
|
405
|
+
"proved_by": {
|
|
406
|
+
"minItems": 1,
|
|
407
|
+
"type": "array",
|
|
408
|
+
"items": {
|
|
409
|
+
"type": "string",
|
|
410
|
+
"enum": [
|
|
411
|
+
"gateway",
|
|
412
|
+
"ai",
|
|
413
|
+
"images",
|
|
414
|
+
"image-search",
|
|
415
|
+
"config",
|
|
416
|
+
"email",
|
|
417
|
+
"email-inbound",
|
|
418
|
+
"email-sending",
|
|
419
|
+
"database",
|
|
420
|
+
"workers",
|
|
421
|
+
"domains",
|
|
422
|
+
"dns",
|
|
423
|
+
"analytics",
|
|
424
|
+
"country",
|
|
425
|
+
"pdf",
|
|
426
|
+
"access",
|
|
427
|
+
"r2",
|
|
428
|
+
"kv",
|
|
429
|
+
"github",
|
|
430
|
+
"strings",
|
|
431
|
+
"ui",
|
|
432
|
+
"forms",
|
|
433
|
+
"sign-in"
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
},
|
|
405
437
|
"offer": {
|
|
406
438
|
"type": "object",
|
|
407
439
|
"properties": {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schema.json",
|
|
3
|
+
"format": 1,
|
|
4
|
+
"component": "sign-in",
|
|
5
|
+
"title": "Sign-in",
|
|
6
|
+
"summary": "People sign in to the app with a code emailed to them and typed on the next screen: no password, no link and no reset email. The Worker holds the session in one sealed cookie and talks to Supabase Auth itself, so the browser never sees a token, and a Cloudflare Turnstile check runs before any code is sent. The template has it all; a page that needs a signed-in person asks for one with requireUser. Signing in proves who someone is, not what they may do: anyone can make an account.",
|
|
7
|
+
"status": "built",
|
|
8
|
+
"delivery": {
|
|
9
|
+
"choice": "built_in",
|
|
10
|
+
"intro": "People signing in to the app with a code emailed to them, with no passwords to forget."
|
|
11
|
+
},
|
|
12
|
+
"surfaces": {
|
|
13
|
+
"package": [
|
|
14
|
+
"createSessions",
|
|
15
|
+
"safeReturnPath",
|
|
16
|
+
"sameOrigin",
|
|
17
|
+
"plainAuthError"
|
|
18
|
+
],
|
|
19
|
+
"tools": [
|
|
20
|
+
"cf_worker_secret_put",
|
|
21
|
+
"cf_turnstile_widget_create",
|
|
22
|
+
"cf_turnstile_widget_delete"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"config": [
|
|
26
|
+
{
|
|
27
|
+
"name": "SESSION_SECRET",
|
|
28
|
+
"required": false,
|
|
29
|
+
"secret": true,
|
|
30
|
+
"environments": [
|
|
31
|
+
"development",
|
|
32
|
+
"staging",
|
|
33
|
+
"production"
|
|
34
|
+
],
|
|
35
|
+
"description": "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "TURNSTILE_SITE_KEY",
|
|
39
|
+
"required": false,
|
|
40
|
+
"secret": false,
|
|
41
|
+
"environments": [
|
|
42
|
+
"development",
|
|
43
|
+
"staging",
|
|
44
|
+
"production"
|
|
45
|
+
],
|
|
46
|
+
"description": "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "TURNSTILE_SECRET_KEY",
|
|
50
|
+
"required": false,
|
|
51
|
+
"secret": true,
|
|
52
|
+
"environments": [
|
|
53
|
+
"development",
|
|
54
|
+
"staging",
|
|
55
|
+
"production"
|
|
56
|
+
],
|
|
57
|
+
"description": "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"actions": [
|
|
61
|
+
{
|
|
62
|
+
"tool": "cf_turnstile_widget_create",
|
|
63
|
+
"gate": "free",
|
|
64
|
+
"ships": "5"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"tool": "cf_turnstile_widget_delete",
|
|
68
|
+
"gate": "two_step",
|
|
69
|
+
"ships": "5"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"rules": [
|
|
73
|
+
{
|
|
74
|
+
"id": "sign-in.server-side",
|
|
75
|
+
"kind": "must",
|
|
76
|
+
"text": "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.",
|
|
77
|
+
"enforced_by": "review",
|
|
78
|
+
"probe": "database.sign-in"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "sign-in.writes-same-origin",
|
|
82
|
+
"kind": "must",
|
|
83
|
+
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
84
|
+
"enforced_by": "review",
|
|
85
|
+
"probe": "database.session-cookie"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "sign-in.checks-for-bots",
|
|
89
|
+
"kind": "must",
|
|
90
|
+
"text": "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.",
|
|
91
|
+
"enforced_by": "review",
|
|
92
|
+
"probe": "database.sign-in-bot-check"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"probes": [],
|
|
96
|
+
"offer": {
|
|
97
|
+
"pattern": "switch_on",
|
|
98
|
+
"when": "The client wants people to sign in or have accounts: a members' area, saved orders or bookings, a profile, \"my account\", or a page only some people may see.",
|
|
99
|
+
"say": "Sign-in is already built in: people get a code by email and type it, so there are no passwords to forget or leak. I'll put the pages that need it behind it. Here's what I think needs sign-in; tell me what to change.",
|
|
100
|
+
"answers_file": "workbench/sign-in.json",
|
|
101
|
+
"answers_format": "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"pages\": [{ \"path\": \"/orders\", \"what\": \"<what it shows or does, in the client's words>\", \"who\": \"signed_in\" | \"owner\" | \"admins\", \"fresh\": true | false }] }`, one entry per page or action that needs a signed-in person. `who` is `signed_in` (anyone with an account), `owner` (only the person the thing belongs to, enforced by row-level security on their user id through `dbAs`) or `admins` (only people in `app_admins`). `fresh` is true where it also needs a code typed in the last 15 minutes. Save it as a draft until the client accepts the table. Then build each page with `requireUser`, enforce its `who`, and check `fresh` on the ones marked so.",
|
|
102
|
+
"questions": [
|
|
103
|
+
{
|
|
104
|
+
"id": "sign-in.pages",
|
|
105
|
+
"sets": [
|
|
106
|
+
"page.sign_in",
|
|
107
|
+
"page.who",
|
|
108
|
+
"page.fresh"
|
|
109
|
+
],
|
|
110
|
+
"ask": "Here are the pages and actions that need someone signed in, who among them may use each one, and which also need a fresh code because they show or change something sensitive. Accept them, or change some?",
|
|
111
|
+
"style": "table",
|
|
112
|
+
"note": "Propose from the pages built or planned. Anything that shows or changes a person's own things (orders, bookings, a profile) needs sign-in, with `who` set to `owner`. Signing in only proves who someone is, and anyone can make an account, so a page for staff or admins must say `admins`: `requireUser` alone would let any stranger who signs up use it. Mark `fresh` where a stranger holding a signed-in phone could do harm: showing payment details, changing an email address, adding an admin, deleting an account. The template has no re-check screen: when `fresh` is false, the page tells the person to sign out and sign in again with a new code, as the admins section of `/admin/brand` does. Pages anyone may read, such as the home page, a menu or a contact form, stay open. Only the client knows what is private and who may do what, so always show the table."
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"not_asked": [
|
|
116
|
+
{
|
|
117
|
+
"sets": "sign_in.method",
|
|
118
|
+
"why": "The template signs people in with an emailed code only (Workbench's choice: no password to forget, leak or reuse), and the authentication standard rules out sign-in links. Passwords and social sign-in (\"sign in with Google\") aren't built. If the client asks for one, say so and that it is the Workbench owner's call; don't build it."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"sets": "sign_in.length",
|
|
122
|
+
"why": "Sessions last weeks, with a cap, as the authentication standard (§10) asks. The package's defaults (SESSION_DEFAULTS) sign a person out after 14 days without a visit and 30 days after the code was typed, whatever happens; on a shared device they untick \"keep me signed in\" and are signed out after 30 minutes idle. Say so if the client asks, and don't change them."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"sets": "sign_in.who",
|
|
126
|
+
"why": "Anyone with an email address can make an account; invite-only sign-up isn't built yet. If the client needs only invited people, say it isn't built yet and ask them how they want to handle it. Don't build an invite system of your own."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"sets": "sign_in.bot_check",
|
|
130
|
+
"why": "The Turnstile check is always on outside development. Control sets it up (cf_turnstile_widget_create, then cf_worker_secret_put sources turnstile_sitekey and turnstile_secret); there is nothing to ask."
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"proved_by": [
|
|
135
|
+
"database"
|
|
136
|
+
]
|
|
137
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MANIFEST_VERSION: "
|
|
1
|
+
export declare const MANIFEST_VERSION: "984099bc7d3f";
|
|
2
2
|
export declare const capabilities: {
|
|
3
3
|
readonly ai: {
|
|
4
4
|
readonly $schema: "./schema.json";
|
|
@@ -119,7 +119,7 @@ export declare const capabilities: {
|
|
|
119
119
|
readonly id: "ai.example.text";
|
|
120
120
|
readonly title: "One-shot text";
|
|
121
121
|
readonly lang: "ts";
|
|
122
|
-
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })";
|
|
122
|
+
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })";
|
|
123
123
|
readonly probe: "ai.text.claude-default";
|
|
124
124
|
}, {
|
|
125
125
|
readonly id: "ai.example.voice";
|
|
@@ -437,8 +437,8 @@ export declare const capabilities: {
|
|
|
437
437
|
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.";
|
|
438
438
|
readonly status: "built";
|
|
439
439
|
readonly surfaces: {
|
|
440
|
-
readonly tools: readonly ["sb_project_create", "sb_migration_apply", "sb_migration_rollback", "sb_query", "sb_project_delete", "cf_worker_secret_put", "vendor_settings_apply"
|
|
441
|
-
readonly package: readonly ["supabaseConfig", "createBrowserClient", "createServerClient", "createUserClient", "createServiceClient", "WorkbenchSupabaseError"
|
|
440
|
+
readonly tools: readonly ["sb_project_create", "sb_migration_apply", "sb_migration_rollback", "sb_query", "sb_project_delete", "cf_worker_secret_put", "vendor_settings_apply"];
|
|
441
|
+
readonly package: readonly ["supabaseConfig", "createBrowserClient", "createServerClient", "createUserClient", "createServiceClient", "WorkbenchSupabaseError"];
|
|
442
442
|
};
|
|
443
443
|
readonly config: readonly [{
|
|
444
444
|
readonly name: "SUPABASE_URL";
|
|
@@ -458,24 +458,6 @@ export declare const capabilities: {
|
|
|
458
458
|
readonly secret: true;
|
|
459
459
|
readonly environments: readonly ["staging", "production"];
|
|
460
460
|
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";
|
|
461
|
-
}, {
|
|
462
|
-
readonly name: "SESSION_SECRET";
|
|
463
|
-
readonly required: false;
|
|
464
|
-
readonly secret: true;
|
|
465
|
-
readonly environments: readonly ["development", "staging", "production"];
|
|
466
|
-
readonly description: "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in";
|
|
467
|
-
}, {
|
|
468
|
-
readonly name: "TURNSTILE_SITE_KEY";
|
|
469
|
-
readonly required: false;
|
|
470
|
-
readonly secret: false;
|
|
471
|
-
readonly environments: readonly ["development", "staging", "production"];
|
|
472
|
-
readonly description: "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it";
|
|
473
|
-
}, {
|
|
474
|
-
readonly name: "TURNSTILE_SECRET_KEY";
|
|
475
|
-
readonly required: false;
|
|
476
|
-
readonly secret: true;
|
|
477
|
-
readonly environments: readonly ["development", "staging", "production"];
|
|
478
|
-
readonly description: "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it";
|
|
479
461
|
}];
|
|
480
462
|
readonly actions: readonly [{
|
|
481
463
|
readonly tool: "sb_project_create";
|
|
@@ -510,14 +492,6 @@ export declare const capabilities: {
|
|
|
510
492
|
readonly gate: "two_step_when";
|
|
511
493
|
readonly when: "production, after the first successful apply on that database; staging and check_only are free";
|
|
512
494
|
readonly ships: "5";
|
|
513
|
-
}, {
|
|
514
|
-
readonly tool: "cf_turnstile_widget_create";
|
|
515
|
-
readonly gate: "free";
|
|
516
|
-
readonly ships: "5";
|
|
517
|
-
}, {
|
|
518
|
-
readonly tool: "cf_turnstile_widget_delete";
|
|
519
|
-
readonly gate: "two_step";
|
|
520
|
-
readonly ships: "5";
|
|
521
495
|
}];
|
|
522
496
|
readonly rules: readonly [{
|
|
523
497
|
readonly id: "database.rls-isolates-users";
|
|
@@ -555,24 +529,6 @@ export declare const capabilities: {
|
|
|
555
529
|
readonly text: "Keep Supabase's sign-in settings at the Workbench baseline (workbench-config VENDOR_SETTINGS), or record a project's change in vendor-settings.json with a reason. Control applies it with vendor_settings_apply and checks it every day.";
|
|
556
530
|
readonly enforced_by: "control";
|
|
557
531
|
readonly probe: "database.vendor-settings";
|
|
558
|
-
}, {
|
|
559
|
-
readonly id: "database.sign-in-server-side";
|
|
560
|
-
readonly kind: "must";
|
|
561
|
-
readonly text: "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.";
|
|
562
|
-
readonly enforced_by: "review";
|
|
563
|
-
readonly probe: "database.sign-in";
|
|
564
|
-
}, {
|
|
565
|
-
readonly id: "database.session-writes-same-origin";
|
|
566
|
-
readonly kind: "must";
|
|
567
|
-
readonly text: "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.";
|
|
568
|
-
readonly enforced_by: "review";
|
|
569
|
-
readonly probe: "database.session-cookie";
|
|
570
|
-
}, {
|
|
571
|
-
readonly id: "database.sign-in-checks-for-bots";
|
|
572
|
-
readonly kind: "must";
|
|
573
|
-
readonly text: "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.";
|
|
574
|
-
readonly enforced_by: "review";
|
|
575
|
-
readonly probe: "database.sign-in-bot-check";
|
|
576
532
|
}];
|
|
577
533
|
readonly examples: readonly [{
|
|
578
534
|
readonly id: "database.example.user-client";
|
|
@@ -816,7 +772,7 @@ export declare const capabilities: {
|
|
|
816
772
|
readonly id: "email.example.send";
|
|
817
773
|
readonly title: "Send a transactional mail with an inline logo";
|
|
818
774
|
readonly lang: "ts";
|
|
819
|
-
readonly code: "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })";
|
|
775
|
+
readonly code: "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY!, url: process.env.WORKBENCH_GATEWAY_URL! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })";
|
|
820
776
|
readonly probe: "email.send.inline-attachment";
|
|
821
777
|
}];
|
|
822
778
|
readonly probes: readonly [{
|
|
@@ -1366,7 +1322,6 @@ export declare const capabilities: {
|
|
|
1366
1322
|
readonly status: "built";
|
|
1367
1323
|
readonly surfaces: {
|
|
1368
1324
|
readonly gateway: readonly ["GET /v1/health", "GET /v1/usage", "GET /v1/capabilities", "GET /v1/calls"];
|
|
1369
|
-
readonly package: readonly ["DEFAULT_GATEWAY_URL"];
|
|
1370
1325
|
};
|
|
1371
1326
|
readonly config: readonly [{
|
|
1372
1327
|
readonly name: "WORKBENCH_GATEWAY_KEY";
|
|
@@ -1376,11 +1331,10 @@ export declare const capabilities: {
|
|
|
1376
1331
|
readonly description: "The project's Gateway key for this environment, delivered as a Worker secret by gw_key_issue. Never in code, .dev.vars.example or logs";
|
|
1377
1332
|
}, {
|
|
1378
1333
|
readonly name: "WORKBENCH_GATEWAY_URL";
|
|
1379
|
-
readonly required:
|
|
1334
|
+
readonly required: true;
|
|
1380
1335
|
readonly secret: false;
|
|
1381
|
-
readonly default: "https://gateway.mccaskielabs.dev";
|
|
1382
1336
|
readonly environments: readonly ["development", "staging", "production"];
|
|
1383
|
-
readonly description: "Gateway
|
|
1337
|
+
readonly description: "The address of this Workbench install's Gateway. Control writes it into wrangler.jsonc when it creates the project; there is no built-in address";
|
|
1384
1338
|
}];
|
|
1385
1339
|
readonly allowed: {
|
|
1386
1340
|
readonly scopes: readonly ["ai", "images", "search", "email"];
|
|
@@ -1679,7 +1633,7 @@ export declare const capabilities: {
|
|
|
1679
1633
|
readonly id: "image-search.example.search";
|
|
1680
1634
|
readonly title: "Search for images";
|
|
1681
1635
|
readonly lang: "ts";
|
|
1682
|
-
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })";
|
|
1636
|
+
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })";
|
|
1683
1637
|
readonly probe: "image-search.serper";
|
|
1684
1638
|
}];
|
|
1685
1639
|
readonly probes: readonly [{
|
|
@@ -1809,7 +1763,7 @@ export declare const capabilities: {
|
|
|
1809
1763
|
readonly id: "images.example.generate";
|
|
1810
1764
|
readonly title: "Generate one image";
|
|
1811
1765
|
readonly lang: "ts";
|
|
1812
|
-
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })";
|
|
1766
|
+
readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })";
|
|
1813
1767
|
readonly probe: "images.generate.image-default";
|
|
1814
1768
|
}, {
|
|
1815
1769
|
readonly id: "images.example.reference";
|
|
@@ -1936,6 +1890,98 @@ export declare const capabilities: {
|
|
|
1936
1890
|
readonly when: "The client wants a PDF: an invoice, receipt, quote, certificate, report or anything else to download or print.";
|
|
1937
1891
|
};
|
|
1938
1892
|
};
|
|
1893
|
+
readonly "sign-in": {
|
|
1894
|
+
readonly $schema: "./schema.json";
|
|
1895
|
+
readonly format: 1;
|
|
1896
|
+
readonly component: "sign-in";
|
|
1897
|
+
readonly title: "Sign-in";
|
|
1898
|
+
readonly summary: "People sign in to the app with a code emailed to them and typed on the next screen: no password, no link and no reset email. The Worker holds the session in one sealed cookie and talks to Supabase Auth itself, so the browser never sees a token, and a Cloudflare Turnstile check runs before any code is sent. The template has it all; a page that needs a signed-in person asks for one with requireUser. Signing in proves who someone is, not what they may do: anyone can make an account.";
|
|
1899
|
+
readonly status: "built";
|
|
1900
|
+
readonly surfaces: {
|
|
1901
|
+
readonly tools: readonly ["cf_worker_secret_put", "cf_turnstile_widget_create", "cf_turnstile_widget_delete"];
|
|
1902
|
+
readonly package: readonly ["createSessions", "safeReturnPath", "sameOrigin", "plainAuthError"];
|
|
1903
|
+
};
|
|
1904
|
+
readonly config: readonly [{
|
|
1905
|
+
readonly name: "SESSION_SECRET";
|
|
1906
|
+
readonly required: false;
|
|
1907
|
+
readonly secret: true;
|
|
1908
|
+
readonly environments: readonly ["development", "staging", "production"];
|
|
1909
|
+
readonly description: "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in";
|
|
1910
|
+
}, {
|
|
1911
|
+
readonly name: "TURNSTILE_SITE_KEY";
|
|
1912
|
+
readonly required: false;
|
|
1913
|
+
readonly secret: false;
|
|
1914
|
+
readonly environments: readonly ["development", "staging", "production"];
|
|
1915
|
+
readonly description: "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it";
|
|
1916
|
+
}, {
|
|
1917
|
+
readonly name: "TURNSTILE_SECRET_KEY";
|
|
1918
|
+
readonly required: false;
|
|
1919
|
+
readonly secret: true;
|
|
1920
|
+
readonly environments: readonly ["development", "staging", "production"];
|
|
1921
|
+
readonly description: "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it";
|
|
1922
|
+
}];
|
|
1923
|
+
readonly actions: readonly [{
|
|
1924
|
+
readonly tool: "cf_turnstile_widget_create";
|
|
1925
|
+
readonly gate: "free";
|
|
1926
|
+
readonly ships: "5";
|
|
1927
|
+
}, {
|
|
1928
|
+
readonly tool: "cf_turnstile_widget_delete";
|
|
1929
|
+
readonly gate: "two_step";
|
|
1930
|
+
readonly ships: "5";
|
|
1931
|
+
}];
|
|
1932
|
+
readonly rules: readonly [{
|
|
1933
|
+
readonly id: "sign-in.server-side";
|
|
1934
|
+
readonly kind: "must";
|
|
1935
|
+
readonly text: "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.";
|
|
1936
|
+
readonly enforced_by: "review";
|
|
1937
|
+
readonly probe: "database.sign-in";
|
|
1938
|
+
}, {
|
|
1939
|
+
readonly id: "sign-in.writes-same-origin";
|
|
1940
|
+
readonly kind: "must";
|
|
1941
|
+
readonly text: "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.";
|
|
1942
|
+
readonly enforced_by: "review";
|
|
1943
|
+
readonly probe: "database.session-cookie";
|
|
1944
|
+
}, {
|
|
1945
|
+
readonly id: "sign-in.checks-for-bots";
|
|
1946
|
+
readonly kind: "must";
|
|
1947
|
+
readonly text: "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.";
|
|
1948
|
+
readonly enforced_by: "review";
|
|
1949
|
+
readonly probe: "database.sign-in-bot-check";
|
|
1950
|
+
}];
|
|
1951
|
+
readonly probes: readonly [];
|
|
1952
|
+
readonly proved_by: readonly ["database"];
|
|
1953
|
+
readonly offer: {
|
|
1954
|
+
readonly pattern: "switch_on";
|
|
1955
|
+
readonly when: "The client wants people to sign in or have accounts: a members' area, saved orders or bookings, a profile, \"my account\", or a page only some people may see.";
|
|
1956
|
+
readonly say: "Sign-in is already built in: people get a code by email and type it, so there are no passwords to forget or leak. I'll put the pages that need it behind it. Here's what I think needs sign-in; tell me what to change.";
|
|
1957
|
+
readonly answers_file: "workbench/sign-in.json";
|
|
1958
|
+
readonly answers_format: "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"pages\": [{ \"path\": \"/orders\", \"what\": \"<what it shows or does, in the client's words>\", \"who\": \"signed_in\" | \"owner\" | \"admins\", \"fresh\": true | false }] }`, one entry per page or action that needs a signed-in person. `who` is `signed_in` (anyone with an account), `owner` (only the person the thing belongs to, enforced by row-level security on their user id through `dbAs`) or `admins` (only people in `app_admins`). `fresh` is true where it also needs a code typed in the last 15 minutes. Save it as a draft until the client accepts the table. Then build each page with `requireUser`, enforce its `who`, and check `fresh` on the ones marked so.";
|
|
1959
|
+
readonly questions: readonly [{
|
|
1960
|
+
readonly id: "sign-in.pages";
|
|
1961
|
+
readonly sets: readonly ["page.sign_in", "page.who", "page.fresh"];
|
|
1962
|
+
readonly ask: "Here are the pages and actions that need someone signed in, who among them may use each one, and which also need a fresh code because they show or change something sensitive. Accept them, or change some?";
|
|
1963
|
+
readonly style: "table";
|
|
1964
|
+
readonly note: "Propose from the pages built or planned. Anything that shows or changes a person's own things (orders, bookings, a profile) needs sign-in, with `who` set to `owner`. Signing in only proves who someone is, and anyone can make an account, so a page for staff or admins must say `admins`: `requireUser` alone would let any stranger who signs up use it. Mark `fresh` where a stranger holding a signed-in phone could do harm: showing payment details, changing an email address, adding an admin, deleting an account. The template has no re-check screen: when `fresh` is false, the page tells the person to sign out and sign in again with a new code, as the admins section of `/admin/brand` does. Pages anyone may read, such as the home page, a menu or a contact form, stay open. Only the client knows what is private and who may do what, so always show the table.";
|
|
1965
|
+
}];
|
|
1966
|
+
readonly not_asked: readonly [{
|
|
1967
|
+
readonly sets: "sign_in.method";
|
|
1968
|
+
readonly why: "The template signs people in with an emailed code only (Workbench's choice: no password to forget, leak or reuse), and the authentication standard rules out sign-in links. Passwords and social sign-in (\"sign in with Google\") aren't built. If the client asks for one, say so and that it is the Workbench owner's call; don't build it.";
|
|
1969
|
+
}, {
|
|
1970
|
+
readonly sets: "sign_in.length";
|
|
1971
|
+
readonly why: "Sessions last weeks, with a cap, as the authentication standard (§10) asks. The package's defaults (SESSION_DEFAULTS) sign a person out after 14 days without a visit and 30 days after the code was typed, whatever happens; on a shared device they untick \"keep me signed in\" and are signed out after 30 minutes idle. Say so if the client asks, and don't change them.";
|
|
1972
|
+
}, {
|
|
1973
|
+
readonly sets: "sign_in.who";
|
|
1974
|
+
readonly why: "Anyone with an email address can make an account; invite-only sign-up isn't built yet. If the client needs only invited people, say it isn't built yet and ask them how they want to handle it. Don't build an invite system of your own.";
|
|
1975
|
+
}, {
|
|
1976
|
+
readonly sets: "sign_in.bot_check";
|
|
1977
|
+
readonly why: "The Turnstile check is always on outside development. Control sets it up (cf_turnstile_widget_create, then cf_worker_secret_put sources turnstile_sitekey and turnstile_secret); there is nothing to ask.";
|
|
1978
|
+
}];
|
|
1979
|
+
};
|
|
1980
|
+
readonly delivery: {
|
|
1981
|
+
readonly choice: "built_in";
|
|
1982
|
+
readonly intro: "People signing in to the app with a code emailed to them, with no passwords to forget.";
|
|
1983
|
+
};
|
|
1984
|
+
};
|
|
1939
1985
|
readonly strings: {
|
|
1940
1986
|
readonly $schema: "./schema.json";
|
|
1941
1987
|
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 = "984099bc7d3f";
|
|
4
4
|
export const capabilities = {
|
|
5
5
|
"ai": {
|
|
6
6
|
"$schema": "./schema.json",
|
|
@@ -204,7 +204,7 @@ export const capabilities = {
|
|
|
204
204
|
"id": "ai.example.text",
|
|
205
205
|
"title": "One-shot text",
|
|
206
206
|
"lang": "ts",
|
|
207
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })",
|
|
207
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })",
|
|
208
208
|
"probe": "ai.text.claude-default"
|
|
209
209
|
},
|
|
210
210
|
{
|
|
@@ -637,9 +637,7 @@ export const capabilities = {
|
|
|
637
637
|
"sb_query",
|
|
638
638
|
"sb_project_delete",
|
|
639
639
|
"cf_worker_secret_put",
|
|
640
|
-
"vendor_settings_apply"
|
|
641
|
-
"cf_turnstile_widget_create",
|
|
642
|
-
"cf_turnstile_widget_delete"
|
|
640
|
+
"vendor_settings_apply"
|
|
643
641
|
],
|
|
644
642
|
"package": [
|
|
645
643
|
"supabaseConfig",
|
|
@@ -647,10 +645,7 @@ export const capabilities = {
|
|
|
647
645
|
"createServerClient",
|
|
648
646
|
"createUserClient",
|
|
649
647
|
"createServiceClient",
|
|
650
|
-
"WorkbenchSupabaseError"
|
|
651
|
-
"createSessions",
|
|
652
|
-
"safeReturnPath",
|
|
653
|
-
"sameOrigin"
|
|
648
|
+
"WorkbenchSupabaseError"
|
|
654
649
|
]
|
|
655
650
|
},
|
|
656
651
|
"config": [
|
|
@@ -685,39 +680,6 @@ export const capabilities = {
|
|
|
685
680
|
"production"
|
|
686
681
|
],
|
|
687
682
|
"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"
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
"name": "SESSION_SECRET",
|
|
691
|
-
"required": false,
|
|
692
|
-
"secret": true,
|
|
693
|
-
"environments": [
|
|
694
|
-
"development",
|
|
695
|
-
"staging",
|
|
696
|
-
"production"
|
|
697
|
-
],
|
|
698
|
-
"description": "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in"
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
"name": "TURNSTILE_SITE_KEY",
|
|
702
|
-
"required": false,
|
|
703
|
-
"secret": false,
|
|
704
|
-
"environments": [
|
|
705
|
-
"development",
|
|
706
|
-
"staging",
|
|
707
|
-
"production"
|
|
708
|
-
],
|
|
709
|
-
"description": "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it"
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
"name": "TURNSTILE_SECRET_KEY",
|
|
713
|
-
"required": false,
|
|
714
|
-
"secret": true,
|
|
715
|
-
"environments": [
|
|
716
|
-
"development",
|
|
717
|
-
"staging",
|
|
718
|
-
"production"
|
|
719
|
-
],
|
|
720
|
-
"description": "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it"
|
|
721
683
|
}
|
|
722
684
|
],
|
|
723
685
|
"actions": [
|
|
@@ -760,16 +722,6 @@ export const capabilities = {
|
|
|
760
722
|
"gate": "two_step_when",
|
|
761
723
|
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
762
724
|
"ships": "5"
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
"tool": "cf_turnstile_widget_create",
|
|
766
|
-
"gate": "free",
|
|
767
|
-
"ships": "5"
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
"tool": "cf_turnstile_widget_delete",
|
|
771
|
-
"gate": "two_step",
|
|
772
|
-
"ships": "5"
|
|
773
725
|
}
|
|
774
726
|
],
|
|
775
727
|
"rules": [
|
|
@@ -814,27 +766,6 @@ export const capabilities = {
|
|
|
814
766
|
"text": "Keep Supabase's sign-in settings at the Workbench baseline (workbench-config VENDOR_SETTINGS), or record a project's change in vendor-settings.json with a reason. Control applies it with vendor_settings_apply and checks it every day.",
|
|
815
767
|
"enforced_by": "control",
|
|
816
768
|
"probe": "database.vendor-settings"
|
|
817
|
-
},
|
|
818
|
-
{
|
|
819
|
-
"id": "database.sign-in-server-side",
|
|
820
|
-
"kind": "must",
|
|
821
|
-
"text": "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.",
|
|
822
|
-
"enforced_by": "review",
|
|
823
|
-
"probe": "database.sign-in"
|
|
824
|
-
},
|
|
825
|
-
{
|
|
826
|
-
"id": "database.session-writes-same-origin",
|
|
827
|
-
"kind": "must",
|
|
828
|
-
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
829
|
-
"enforced_by": "review",
|
|
830
|
-
"probe": "database.session-cookie"
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
"id": "database.sign-in-checks-for-bots",
|
|
834
|
-
"kind": "must",
|
|
835
|
-
"text": "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.",
|
|
836
|
-
"enforced_by": "review",
|
|
837
|
-
"probe": "database.sign-in-bot-check"
|
|
838
769
|
}
|
|
839
770
|
],
|
|
840
771
|
"examples": [
|
|
@@ -1139,7 +1070,7 @@ export const capabilities = {
|
|
|
1139
1070
|
"id": "email.example.send",
|
|
1140
1071
|
"title": "Send a transactional mail with an inline logo",
|
|
1141
1072
|
"lang": "ts",
|
|
1142
|
-
"code": "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })",
|
|
1073
|
+
"code": "const env = workbenchEnv(process.env)\nconst mailer = createMailer({ env, transport: selectTransport(env), from: { address: 'hello@mail.example.com', name: 'Example' }, gateway: { key: process.env.WORKBENCH_GATEWAY_KEY!, url: process.env.WORKBENCH_GATEWAY_URL! } })\nawait mailer.send({ to: user.email, subject: 'Welcome', text, html: '<img src=\"cid:logo\"> ...', template: 'welcome', idempotencyKey: `welcome:${user.id}`, attachments: [{ filename: 'logo.png', mimeType: 'image/png', bytes: logo, contentId: 'logo' }] })",
|
|
1143
1074
|
"probe": "email.send.inline-attachment"
|
|
1144
1075
|
}
|
|
1145
1076
|
],
|
|
@@ -1861,9 +1792,6 @@ export const capabilities = {
|
|
|
1861
1792
|
"GET /v1/usage",
|
|
1862
1793
|
"GET /v1/capabilities",
|
|
1863
1794
|
"GET /v1/calls"
|
|
1864
|
-
],
|
|
1865
|
-
"package": [
|
|
1866
|
-
"DEFAULT_GATEWAY_URL"
|
|
1867
1795
|
]
|
|
1868
1796
|
},
|
|
1869
1797
|
"config": [
|
|
@@ -1879,15 +1807,14 @@ export const capabilities = {
|
|
|
1879
1807
|
},
|
|
1880
1808
|
{
|
|
1881
1809
|
"name": "WORKBENCH_GATEWAY_URL",
|
|
1882
|
-
"required":
|
|
1810
|
+
"required": true,
|
|
1883
1811
|
"secret": false,
|
|
1884
|
-
"default": "https://gateway.mccaskielabs.dev",
|
|
1885
1812
|
"environments": [
|
|
1886
1813
|
"development",
|
|
1887
1814
|
"staging",
|
|
1888
1815
|
"production"
|
|
1889
1816
|
],
|
|
1890
|
-
"description": "Gateway
|
|
1817
|
+
"description": "The address of this Workbench install's Gateway. Control writes it into wrangler.jsonc when it creates the project; there is no built-in address"
|
|
1891
1818
|
}
|
|
1892
1819
|
],
|
|
1893
1820
|
"allowed": {
|
|
@@ -2278,7 +2205,7 @@ export const capabilities = {
|
|
|
2278
2205
|
"id": "image-search.example.search",
|
|
2279
2206
|
"title": "Search for images",
|
|
2280
2207
|
"lang": "ts",
|
|
2281
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })",
|
|
2208
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst results = await ai.images.search({ q: 'Mayon volcano', job: 'editor.image-search', num: 10 })",
|
|
2282
2209
|
"probe": "image-search.serper"
|
|
2283
2210
|
}
|
|
2284
2211
|
],
|
|
@@ -2459,7 +2386,7 @@ export const capabilities = {
|
|
|
2459
2386
|
"id": "images.example.generate",
|
|
2460
2387
|
"title": "Generate one image",
|
|
2461
2388
|
"lang": "ts",
|
|
2462
|
-
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })",
|
|
2389
|
+
"code": "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL })\nconst img = await ai.images.generate({ prompt, aspectRatio: '3:2', job: 'article.hero' })\nawait env.IMAGES.put(key, img.bytes, { httpMetadata: { contentType: img.mimeType }, customMetadata: { model: img.model, requestId: img.requestId } })",
|
|
2463
2390
|
"probe": "images.generate.image-default"
|
|
2464
2391
|
},
|
|
2465
2392
|
{
|
|
@@ -2611,6 +2538,143 @@ export const capabilities = {
|
|
|
2611
2538
|
"when": "The client wants a PDF: an invoice, receipt, quote, certificate, report or anything else to download or print."
|
|
2612
2539
|
}
|
|
2613
2540
|
},
|
|
2541
|
+
"sign-in": {
|
|
2542
|
+
"$schema": "./schema.json",
|
|
2543
|
+
"format": 1,
|
|
2544
|
+
"component": "sign-in",
|
|
2545
|
+
"title": "Sign-in",
|
|
2546
|
+
"summary": "People sign in to the app with a code emailed to them and typed on the next screen: no password, no link and no reset email. The Worker holds the session in one sealed cookie and talks to Supabase Auth itself, so the browser never sees a token, and a Cloudflare Turnstile check runs before any code is sent. The template has it all; a page that needs a signed-in person asks for one with requireUser. Signing in proves who someone is, not what they may do: anyone can make an account.",
|
|
2547
|
+
"status": "built",
|
|
2548
|
+
"surfaces": {
|
|
2549
|
+
"tools": [
|
|
2550
|
+
"cf_worker_secret_put",
|
|
2551
|
+
"cf_turnstile_widget_create",
|
|
2552
|
+
"cf_turnstile_widget_delete"
|
|
2553
|
+
],
|
|
2554
|
+
"package": [
|
|
2555
|
+
"createSessions",
|
|
2556
|
+
"safeReturnPath",
|
|
2557
|
+
"sameOrigin",
|
|
2558
|
+
"plainAuthError"
|
|
2559
|
+
]
|
|
2560
|
+
},
|
|
2561
|
+
"config": [
|
|
2562
|
+
{
|
|
2563
|
+
"name": "SESSION_SECRET",
|
|
2564
|
+
"required": false,
|
|
2565
|
+
"secret": true,
|
|
2566
|
+
"environments": [
|
|
2567
|
+
"development",
|
|
2568
|
+
"staging",
|
|
2569
|
+
"production"
|
|
2570
|
+
],
|
|
2571
|
+
"description": "Seals the sign-in session cookie (createSessions). 32 random bytes written by cf_worker_secret_put source=generate:32; replacing it signs everybody out, so it is two-step. Only needed by an app with sign-in"
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
"name": "TURNSTILE_SITE_KEY",
|
|
2575
|
+
"required": false,
|
|
2576
|
+
"secret": false,
|
|
2577
|
+
"environments": [
|
|
2578
|
+
"development",
|
|
2579
|
+
"staging",
|
|
2580
|
+
"production"
|
|
2581
|
+
],
|
|
2582
|
+
"description": "The public site key of the hostname's Turnstile widget, for the sign-in bot check; written by cf_worker_secret_put source=turnstile_sitekey after cf_turnstile_widget_create. Outside development, sign-in won't send codes without it"
|
|
2583
|
+
},
|
|
2584
|
+
{
|
|
2585
|
+
"name": "TURNSTILE_SECRET_KEY",
|
|
2586
|
+
"required": false,
|
|
2587
|
+
"secret": true,
|
|
2588
|
+
"environments": [
|
|
2589
|
+
"development",
|
|
2590
|
+
"staging",
|
|
2591
|
+
"production"
|
|
2592
|
+
],
|
|
2593
|
+
"description": "The secret of the hostname's Turnstile widget, sent only to Cloudflare's Siteverify; written by cf_worker_secret_put source=turnstile_secret. Outside development, sign-in won't send codes without it"
|
|
2594
|
+
}
|
|
2595
|
+
],
|
|
2596
|
+
"actions": [
|
|
2597
|
+
{
|
|
2598
|
+
"tool": "cf_turnstile_widget_create",
|
|
2599
|
+
"gate": "free",
|
|
2600
|
+
"ships": "5"
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
"tool": "cf_turnstile_widget_delete",
|
|
2604
|
+
"gate": "two_step",
|
|
2605
|
+
"ships": "5"
|
|
2606
|
+
}
|
|
2607
|
+
],
|
|
2608
|
+
"rules": [
|
|
2609
|
+
{
|
|
2610
|
+
"id": "sign-in.server-side",
|
|
2611
|
+
"kind": "must",
|
|
2612
|
+
"text": "Sign people in through the app's own endpoints with createSessions: the browser never talks to Supabase Auth and never holds the access token, and codes are typed, never links.",
|
|
2613
|
+
"enforced_by": "review",
|
|
2614
|
+
"probe": "database.sign-in"
|
|
2615
|
+
},
|
|
2616
|
+
{
|
|
2617
|
+
"id": "sign-in.writes-same-origin",
|
|
2618
|
+
"kind": "must",
|
|
2619
|
+
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
2620
|
+
"enforced_by": "review",
|
|
2621
|
+
"probe": "database.session-cookie"
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
"id": "sign-in.checks-for-bots",
|
|
2625
|
+
"kind": "must",
|
|
2626
|
+
"text": "Outside development, verify a Turnstile token with Siteverify (success, this hostname and the sign-in action) before asking Supabase to send or re-send a sign-in code, and send no code when either Turnstile key is missing.",
|
|
2627
|
+
"enforced_by": "review",
|
|
2628
|
+
"probe": "database.sign-in-bot-check"
|
|
2629
|
+
}
|
|
2630
|
+
],
|
|
2631
|
+
"probes": [],
|
|
2632
|
+
"proved_by": [
|
|
2633
|
+
"database"
|
|
2634
|
+
],
|
|
2635
|
+
"offer": {
|
|
2636
|
+
"pattern": "switch_on",
|
|
2637
|
+
"when": "The client wants people to sign in or have accounts: a members' area, saved orders or bookings, a profile, \"my account\", or a page only some people may see.",
|
|
2638
|
+
"say": "Sign-in is already built in: people get a code by email and type it, so there are no passwords to forget or leak. I'll put the pages that need it behind it. Here's what I think needs sign-in; tell me what to change.",
|
|
2639
|
+
"answers_file": "workbench/sign-in.json",
|
|
2640
|
+
"answers_format": "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"pages\": [{ \"path\": \"/orders\", \"what\": \"<what it shows or does, in the client's words>\", \"who\": \"signed_in\" | \"owner\" | \"admins\", \"fresh\": true | false }] }`, one entry per page or action that needs a signed-in person. `who` is `signed_in` (anyone with an account), `owner` (only the person the thing belongs to, enforced by row-level security on their user id through `dbAs`) or `admins` (only people in `app_admins`). `fresh` is true where it also needs a code typed in the last 15 minutes. Save it as a draft until the client accepts the table. Then build each page with `requireUser`, enforce its `who`, and check `fresh` on the ones marked so.",
|
|
2641
|
+
"questions": [
|
|
2642
|
+
{
|
|
2643
|
+
"id": "sign-in.pages",
|
|
2644
|
+
"sets": [
|
|
2645
|
+
"page.sign_in",
|
|
2646
|
+
"page.who",
|
|
2647
|
+
"page.fresh"
|
|
2648
|
+
],
|
|
2649
|
+
"ask": "Here are the pages and actions that need someone signed in, who among them may use each one, and which also need a fresh code because they show or change something sensitive. Accept them, or change some?",
|
|
2650
|
+
"style": "table",
|
|
2651
|
+
"note": "Propose from the pages built or planned. Anything that shows or changes a person's own things (orders, bookings, a profile) needs sign-in, with `who` set to `owner`. Signing in only proves who someone is, and anyone can make an account, so a page for staff or admins must say `admins`: `requireUser` alone would let any stranger who signs up use it. Mark `fresh` where a stranger holding a signed-in phone could do harm: showing payment details, changing an email address, adding an admin, deleting an account. The template has no re-check screen: when `fresh` is false, the page tells the person to sign out and sign in again with a new code, as the admins section of `/admin/brand` does. Pages anyone may read, such as the home page, a menu or a contact form, stay open. Only the client knows what is private and who may do what, so always show the table."
|
|
2652
|
+
}
|
|
2653
|
+
],
|
|
2654
|
+
"not_asked": [
|
|
2655
|
+
{
|
|
2656
|
+
"sets": "sign_in.method",
|
|
2657
|
+
"why": "The template signs people in with an emailed code only (Workbench's choice: no password to forget, leak or reuse), and the authentication standard rules out sign-in links. Passwords and social sign-in (\"sign in with Google\") aren't built. If the client asks for one, say so and that it is the Workbench owner's call; don't build it."
|
|
2658
|
+
},
|
|
2659
|
+
{
|
|
2660
|
+
"sets": "sign_in.length",
|
|
2661
|
+
"why": "Sessions last weeks, with a cap, as the authentication standard (§10) asks. The package's defaults (SESSION_DEFAULTS) sign a person out after 14 days without a visit and 30 days after the code was typed, whatever happens; on a shared device they untick \"keep me signed in\" and are signed out after 30 minutes idle. Say so if the client asks, and don't change them."
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
"sets": "sign_in.who",
|
|
2665
|
+
"why": "Anyone with an email address can make an account; invite-only sign-up isn't built yet. If the client needs only invited people, say it isn't built yet and ask them how they want to handle it. Don't build an invite system of your own."
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
"sets": "sign_in.bot_check",
|
|
2669
|
+
"why": "The Turnstile check is always on outside development. Control sets it up (cf_turnstile_widget_create, then cf_worker_secret_put sources turnstile_sitekey and turnstile_secret); there is nothing to ask."
|
|
2670
|
+
}
|
|
2671
|
+
]
|
|
2672
|
+
},
|
|
2673
|
+
"delivery": {
|
|
2674
|
+
"choice": "built_in",
|
|
2675
|
+
"intro": "People signing in to the app with a code emailed to them, with no passwords to forget."
|
|
2676
|
+
}
|
|
2677
|
+
},
|
|
2614
2678
|
"strings": {
|
|
2615
2679
|
"$schema": "./schema.json",
|
|
2616
2680
|
"format": 1,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms"];
|
|
2
|
+
export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms", "sign-in"];
|
|
3
3
|
export type Component = (typeof COMPONENTS)[number];
|
|
4
4
|
export declare const allowedSchemas: {
|
|
5
5
|
gateway: z.ZodObject<{
|
|
@@ -179,6 +179,7 @@ export declare const capabilitySchema: z.ZodObject<{
|
|
|
179
179
|
kv: "kv";
|
|
180
180
|
pdf: "pdf";
|
|
181
181
|
r2: "r2";
|
|
182
|
+
"sign-in": "sign-in";
|
|
182
183
|
strings: "strings";
|
|
183
184
|
ui: "ui";
|
|
184
185
|
workers: "workers";
|
|
@@ -289,6 +290,31 @@ export declare const capabilitySchema: z.ZodObject<{
|
|
|
289
290
|
two_step: z.ZodOptional<z.ZodBoolean>;
|
|
290
291
|
note: z.ZodOptional<z.ZodString>;
|
|
291
292
|
}, z.core.$strict>>;
|
|
293
|
+
proved_by: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
294
|
+
access: "access";
|
|
295
|
+
ai: "ai";
|
|
296
|
+
analytics: "analytics";
|
|
297
|
+
config: "config";
|
|
298
|
+
country: "country";
|
|
299
|
+
database: "database";
|
|
300
|
+
dns: "dns";
|
|
301
|
+
domains: "domains";
|
|
302
|
+
email: "email";
|
|
303
|
+
"email-inbound": "email-inbound";
|
|
304
|
+
"email-sending": "email-sending";
|
|
305
|
+
forms: "forms";
|
|
306
|
+
gateway: "gateway";
|
|
307
|
+
github: "github";
|
|
308
|
+
"image-search": "image-search";
|
|
309
|
+
images: "images";
|
|
310
|
+
kv: "kv";
|
|
311
|
+
pdf: "pdf";
|
|
312
|
+
r2: "r2";
|
|
313
|
+
"sign-in": "sign-in";
|
|
314
|
+
strings: "strings";
|
|
315
|
+
ui: "ui";
|
|
316
|
+
workers: "workers";
|
|
317
|
+
}>>>;
|
|
292
318
|
offer: z.ZodOptional<z.ZodObject<{
|
|
293
319
|
pattern: z.ZodEnum<{
|
|
294
320
|
drafts: "drafts";
|
|
@@ -25,6 +25,7 @@ export const COMPONENTS = [
|
|
|
25
25
|
"strings",
|
|
26
26
|
"ui",
|
|
27
27
|
"forms",
|
|
28
|
+
"sign-in",
|
|
28
29
|
];
|
|
29
30
|
const id = z.string().regex(/^[a-z0-9][a-z0-9.-]*$/, "lower case, digits, dots and hyphens");
|
|
30
31
|
const code = z.string().regex(/^[a-z][a-z0-9_]*$/, "a snake_case error code");
|
|
@@ -226,7 +227,14 @@ const baseShape = {
|
|
|
226
227
|
errors: z.array(errorEntry).min(1).optional(),
|
|
227
228
|
rules: z.array(rule).min(1),
|
|
228
229
|
examples: z.array(example).min(1).optional(),
|
|
229
|
-
|
|
230
|
+
/** Empty only with `proved_by`, when every rule's probe lives in those manifests. */
|
|
231
|
+
probes: z.array(probe),
|
|
232
|
+
/**
|
|
233
|
+
* Components whose probes may prove this one's rules, when one run proves
|
|
234
|
+
* both (sign-in's rules are proved by database's sign-in probes, whose ids
|
|
235
|
+
* wb-probe's run history keys on).
|
|
236
|
+
*/
|
|
237
|
+
proved_by: z.array(z.enum(COMPONENTS)).min(1).optional(),
|
|
230
238
|
offer: offer.optional(),
|
|
231
239
|
/** Why a client has nothing to answer for this component (plumbing). A manifest has `offer` or `no_offer`. */
|
|
232
240
|
no_offer: z.string().min(1).optional(),
|
|
@@ -307,6 +315,8 @@ export function checkManifests(files) {
|
|
|
307
315
|
const problems = [];
|
|
308
316
|
const manifests = [];
|
|
309
317
|
const globalIds = new Map();
|
|
318
|
+
// A rule may name a probe in a `proved_by` manifest; checked once every file is read.
|
|
319
|
+
const deferred = [];
|
|
310
320
|
for (const [file, json] of Object.entries(files)) {
|
|
311
321
|
const parsed = capabilitySchema.safeParse(json);
|
|
312
322
|
if (!parsed.success) {
|
|
@@ -344,10 +354,9 @@ export function checkManifests(files) {
|
|
|
344
354
|
seen("limit", l.id);
|
|
345
355
|
for (const e of m.examples ?? [])
|
|
346
356
|
seen("example", e.id);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
357
|
+
deferred.push({ file, m, own: probeIds });
|
|
358
|
+
if (m.probes.length === 0 && !m.proved_by)
|
|
359
|
+
problems.push({ file, check: "proof", message: "has no probes: add one, or name in proved_by the component whose probes prove these rules" });
|
|
351
360
|
for (const e of m.examples ?? []) {
|
|
352
361
|
if (!probeIds.has(e.probe))
|
|
353
362
|
problems.push({ file, check: "proof", message: `example ${e.id} names probe ${e.probe}, which isn't in this file` });
|
|
@@ -388,5 +397,21 @@ export function checkManifests(files) {
|
|
|
388
397
|
if (m.delivery && !m.offer && !m.delivery.when)
|
|
389
398
|
problems.push({ file, check: "schema", message: "delivery needs when, because there is no offer to say when the tool applies" });
|
|
390
399
|
}
|
|
400
|
+
const byComponent = new Map(manifests.map((m) => [m.component, m]));
|
|
401
|
+
for (const { file, m, own } of deferred) {
|
|
402
|
+
const provers = (m.proved_by ?? []).flatMap((c) => {
|
|
403
|
+
const prover = byComponent.get(c);
|
|
404
|
+
if (!prover)
|
|
405
|
+
problems.push({ file, check: "proof", message: `proved_by names ${c}, which isn't among the manifests` });
|
|
406
|
+
else if (m.status === "built" && prover.status !== "built")
|
|
407
|
+
problems.push({ file, check: "proof", message: `proved_by names ${c}, which isn't built` });
|
|
408
|
+
return prover ? [prover] : [];
|
|
409
|
+
});
|
|
410
|
+
for (const r of m.rules) {
|
|
411
|
+
if (own.has(r.probe) || provers.some((p) => p.probes.some((x) => x.id === r.probe)))
|
|
412
|
+
continue;
|
|
413
|
+
problems.push({ file, check: "proof", message: `rule ${r.id} names probe ${r.probe}, which isn't in this file${m.proved_by ? " or a proved_by manifest" : ""}` });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
391
416
|
return { manifests, problems };
|
|
392
417
|
}
|
package/dist/env.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export declare const WORKBENCH_ENVS: readonly ["development", "staging", "production"];
|
|
2
2
|
export type WorkbenchEnv = (typeof WORKBENCH_ENVS)[number];
|
|
3
|
-
export declare const DEFAULT_GATEWAY_URL = "https://gateway.mccaskielabs.dev";
|
|
4
3
|
/**
|
|
5
4
|
* Reads `WORKBENCH_ENV`. Throws if it is missing or not one of the three
|
|
6
5
|
* environments. Never falls back to `NODE_ENV` or a default (audit F9, F18).
|
package/dist/env.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { invalid, missing } from "./errors.js";
|
|
2
2
|
export const WORKBENCH_ENVS = ["development", "staging", "production"];
|
|
3
|
-
export const DEFAULT_GATEWAY_URL = "https://gateway.mccaskielabs.dev";
|
|
4
3
|
/**
|
|
5
4
|
* Reads `WORKBENCH_ENV`. Throws if it is missing or not one of the three
|
|
6
5
|
* environments. Never falls back to `NODE_ENV` or a default (audit F9, F18).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { WORKBENCH_ENVS,
|
|
1
|
+
export { WORKBENCH_ENVS, workbenchEnv, type WorkbenchEnv } from "./env.ts";
|
|
2
2
|
export { defineConfig, type Config } from "./define-config.ts";
|
|
3
3
|
export { sharedSecret, workerSecret, redact, redactAll, type SecretsStoreSecretLike } from "./secrets.ts";
|
|
4
4
|
export { WorkbenchConfigError, type ConfigErrorCode, type ConfigProblem } from "./errors.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { WORKBENCH_ENVS,
|
|
1
|
+
export { WORKBENCH_ENVS, workbenchEnv } from "./env.js";
|
|
2
2
|
export { defineConfig } from "./define-config.js";
|
|
3
3
|
export { sharedSecret, workerSecret, redact, redactAll } from "./secrets.js";
|
|
4
4
|
export { WorkbenchConfigError } from "./errors.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mccaskie-labs/workbench-config",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|