@mccaskie-labs/workbench-config 0.1.25 → 0.1.27
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/database.json +50 -2
- package/capabilities/forms.json +93 -0
- package/capabilities/schema.json +2 -1
- package/dist/capabilities.gen.d.ts +98 -3
- package/dist/capabilities.gen.js +144 -3
- package/dist/capability-schema.d.ts +2 -1
- package/dist/capability-schema.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @mccaskie-labs/workbench-config
|
|
2
2
|
|
|
3
|
+
## 0.1.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The forms manifest (`capabilities/forms.json`): five rules for `workbench-forms` (the endpoint decides, one verdict on the page and at the endpoint, codes turned into words by the strings catalogue, bounds from the limits register, no members on a form) and two no-cost probes, `forms.validate` and `forms.browser`. `forms` joins the component list.
|
|
8
|
+
|
|
9
|
+
## 0.1.26
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- The database manifest gains the sign-in bot check: Control's `cf_turnstile_widget_create` and `cf_turnstile_widget_delete`, the `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` config (written by `cf_worker_secret_put` sources `turnstile_sitekey` and `turnstile_secret`), the rule `database.sign-in-checks-for-bots` and its drill probe `database.sign-in-bot-check`.
|
|
14
|
+
|
|
3
15
|
## 0.1.25
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"sb_query",
|
|
25
25
|
"sb_project_delete",
|
|
26
26
|
"cf_worker_secret_put",
|
|
27
|
-
"vendor_settings_apply"
|
|
27
|
+
"vendor_settings_apply",
|
|
28
|
+
"cf_turnstile_widget_create",
|
|
29
|
+
"cf_turnstile_widget_delete"
|
|
28
30
|
]
|
|
29
31
|
},
|
|
30
32
|
"config": [
|
|
@@ -70,6 +72,28 @@
|
|
|
70
72
|
"production"
|
|
71
73
|
],
|
|
72
74
|
"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"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "TURNSTILE_SITE_KEY",
|
|
78
|
+
"required": false,
|
|
79
|
+
"secret": false,
|
|
80
|
+
"environments": [
|
|
81
|
+
"development",
|
|
82
|
+
"staging",
|
|
83
|
+
"production"
|
|
84
|
+
],
|
|
85
|
+
"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"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "TURNSTILE_SECRET_KEY",
|
|
89
|
+
"required": false,
|
|
90
|
+
"secret": true,
|
|
91
|
+
"environments": [
|
|
92
|
+
"development",
|
|
93
|
+
"staging",
|
|
94
|
+
"production"
|
|
95
|
+
],
|
|
96
|
+
"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"
|
|
73
97
|
}
|
|
74
98
|
],
|
|
75
99
|
"actions": [
|
|
@@ -104,7 +128,7 @@
|
|
|
104
128
|
{
|
|
105
129
|
"tool": "cf_worker_secret_put",
|
|
106
130
|
"gate": "two_step_when",
|
|
107
|
-
"when": "source supabase_secret, or replacing a secret that exists",
|
|
131
|
+
"when": "source supabase_secret, or replacing a secret that exists (not sources web_analytics, turnstile_sitekey or turnstile_secret, whose value comes from the recorded resource)",
|
|
108
132
|
"ships": "4"
|
|
109
133
|
},
|
|
110
134
|
{
|
|
@@ -112,6 +136,16 @@
|
|
|
112
136
|
"gate": "two_step_when",
|
|
113
137
|
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
114
138
|
"ships": "5"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"tool": "cf_turnstile_widget_create",
|
|
142
|
+
"gate": "free",
|
|
143
|
+
"ships": "5"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"tool": "cf_turnstile_widget_delete",
|
|
147
|
+
"gate": "two_step",
|
|
148
|
+
"ships": "5"
|
|
115
149
|
}
|
|
116
150
|
],
|
|
117
151
|
"rules": [
|
|
@@ -170,6 +204,13 @@
|
|
|
170
204
|
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
171
205
|
"enforced_by": "review",
|
|
172
206
|
"probe": "database.session-cookie"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"id": "database.sign-in-checks-for-bots",
|
|
210
|
+
"kind": "must",
|
|
211
|
+
"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.",
|
|
212
|
+
"enforced_by": "review",
|
|
213
|
+
"probe": "database.sign-in-bot-check"
|
|
173
214
|
}
|
|
174
215
|
],
|
|
175
216
|
"examples": [
|
|
@@ -252,6 +293,13 @@
|
|
|
252
293
|
"expect": "pass",
|
|
253
294
|
"cost_hint_usd": 0,
|
|
254
295
|
"note": "The session cookie is __Host-, Secure, HttpOnly, SameSite=Lax and sealed; a cross-site write is refused; sign-out clears it with every attribute"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"id": "database.sign-in-bot-check",
|
|
299
|
+
"half": "drill",
|
|
300
|
+
"expect": "pass",
|
|
301
|
+
"cost_hint_usd": 0,
|
|
302
|
+
"note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
|
|
255
303
|
}
|
|
256
304
|
]
|
|
257
305
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schema.json",
|
|
3
|
+
"format": 1,
|
|
4
|
+
"component": "forms",
|
|
5
|
+
"title": "Form validation",
|
|
6
|
+
"summary": "workbench-forms checks what people type into a form with one validator that runs the same on the page and at the endpoint. It returns every problem at once as permanent codes, the tidied value to show back in each field and the value to store, with limits from the app's own register and country shapes from workbench-country. It runs in the app itself, with no Gateway call and no cost.",
|
|
7
|
+
"status": "built",
|
|
8
|
+
"surfaces": {
|
|
9
|
+
"package": [
|
|
10
|
+
"defineForm",
|
|
11
|
+
"text",
|
|
12
|
+
"longText",
|
|
13
|
+
"email",
|
|
14
|
+
"phone",
|
|
15
|
+
"money",
|
|
16
|
+
"whole",
|
|
17
|
+
"date",
|
|
18
|
+
"choice",
|
|
19
|
+
"validate",
|
|
20
|
+
"readForm",
|
|
21
|
+
"embedConfig",
|
|
22
|
+
"inputAttributes",
|
|
23
|
+
"messageKeys",
|
|
24
|
+
"tidyLine",
|
|
25
|
+
"tidyText",
|
|
26
|
+
"CODES",
|
|
27
|
+
"FormsSetupError"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"rules": [
|
|
31
|
+
{
|
|
32
|
+
"id": "forms.endpoint-decides",
|
|
33
|
+
"kind": "must",
|
|
34
|
+
"text": "Validate every form at the endpoint with validate() and store only result.values; the page's copy of the validator (FORMS_SCRIPT) only helps the person, it never decides.",
|
|
35
|
+
"enforced_by": "review",
|
|
36
|
+
"probe": "forms.validate"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "forms.same-verdict",
|
|
40
|
+
"kind": "must",
|
|
41
|
+
"text": "Put the form on the page with embedConfig and FORMS_SCRIPT, never a second set of rules, so the page and the endpoint reach the same verdict.",
|
|
42
|
+
"enforced_by": "review",
|
|
43
|
+
"probe": "forms.browser"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "forms.codes-not-sentences",
|
|
47
|
+
"kind": "must",
|
|
48
|
+
"text": "Turn problem codes into words through the strings catalogue (messageKeys: validation.<form>.<field>.<code>, then validation.<code>), never words written in code.",
|
|
49
|
+
"enforced_by": "review",
|
|
50
|
+
"probe": "forms.validate"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "forms.limits-register",
|
|
54
|
+
"kind": "must",
|
|
55
|
+
"text": "Take every length, amount and date bound from the app's limits register (defineLimits, boundsFor), and keep it matched to the database with workbench-supabase check:limits.",
|
|
56
|
+
"enforced_by": "types",
|
|
57
|
+
"probe": "forms.validate"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "forms.no-members-on-a-form",
|
|
61
|
+
"kind": "must_not",
|
|
62
|
+
"text": "List a choice field's members on the form: the vocabulary owns them, and the app passes them in context.choices.",
|
|
63
|
+
"enforced_by": "types",
|
|
64
|
+
"probe": "forms.validate"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"examples": [
|
|
68
|
+
{
|
|
69
|
+
"id": "forms.example.endpoint",
|
|
70
|
+
"title": "Check a posted form at the endpoint",
|
|
71
|
+
"lang": "ts",
|
|
72
|
+
"probe": "forms.validate",
|
|
73
|
+
"code": "import { defineForm, email, money, readForm, text, validate } from '@mccaskie-labs/workbench-forms'\nimport { boundsFor } from '@mccaskie-labs/workbench-forms/limits'\nimport { COUNTRY_PACKS } from '@mccaskie-labs/workbench-country'\n\nconst lead = defineForm('lead', { name: text({ limit: 'lead.name', required: true }), email: email({ required: true }), budget: money({ limit: 'lead.budget' }) })\nconst result = validate(lead, readForm(lead, await request.formData()), { bounds: boundsFor(LIMITS, lead), country: COUNTRY_PACKS.PH! })\nif (!result.ok) return show(result.problems, result.shown)\nawait db.from('leads').insert(result.values)"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"probes": [
|
|
77
|
+
{
|
|
78
|
+
"id": "forms.validate",
|
|
79
|
+
"half": "runtime",
|
|
80
|
+
"expect": "pass",
|
|
81
|
+
"smoke": true,
|
|
82
|
+
"cost_hint_usd": 0,
|
|
83
|
+
"note": "In the Worker itself: validates a fixed form (text, email, phone, money, date, choice) for PH with one good and one bad input, and compares the stored values (E.164 phone, decimal-string amount, ISO date) and the problem codes with the expected ones"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "forms.browser",
|
|
87
|
+
"half": "runtime",
|
|
88
|
+
"expect": "pass",
|
|
89
|
+
"cost_hint_usd": 0,
|
|
90
|
+
"note": "The probe page loads FORMS_SCRIPT and the form's embedConfig block, runs the same inputs in the browser and checks the verdict matches the Worker's; no Gateway call"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
package/capabilities/schema.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MANIFEST_VERSION: "
|
|
1
|
+
export declare const MANIFEST_VERSION: "c83a7c612319";
|
|
2
2
|
export declare const capabilities: {
|
|
3
3
|
readonly ai: {
|
|
4
4
|
readonly $schema: "./schema.json";
|
|
@@ -421,7 +421,7 @@ export declare const capabilities: {
|
|
|
421
421
|
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.";
|
|
422
422
|
readonly status: "built";
|
|
423
423
|
readonly surfaces: {
|
|
424
|
-
readonly tools: readonly ["sb_project_create", "sb_migration_apply", "sb_migration_rollback", "sb_query", "sb_project_delete", "cf_worker_secret_put", "vendor_settings_apply"];
|
|
424
|
+
readonly tools: readonly ["sb_project_create", "sb_migration_apply", "sb_migration_rollback", "sb_query", "sb_project_delete", "cf_worker_secret_put", "vendor_settings_apply", "cf_turnstile_widget_create", "cf_turnstile_widget_delete"];
|
|
425
425
|
readonly package: readonly ["supabaseConfig", "createBrowserClient", "createServerClient", "createUserClient", "createServiceClient", "WorkbenchSupabaseError", "createSessions", "safeReturnPath", "sameOrigin"];
|
|
426
426
|
};
|
|
427
427
|
readonly config: readonly [{
|
|
@@ -448,6 +448,18 @@ export declare const capabilities: {
|
|
|
448
448
|
readonly secret: true;
|
|
449
449
|
readonly environments: readonly ["development", "staging", "production"];
|
|
450
450
|
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";
|
|
451
|
+
}, {
|
|
452
|
+
readonly name: "TURNSTILE_SITE_KEY";
|
|
453
|
+
readonly required: false;
|
|
454
|
+
readonly secret: false;
|
|
455
|
+
readonly environments: readonly ["development", "staging", "production"];
|
|
456
|
+
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";
|
|
457
|
+
}, {
|
|
458
|
+
readonly name: "TURNSTILE_SECRET_KEY";
|
|
459
|
+
readonly required: false;
|
|
460
|
+
readonly secret: true;
|
|
461
|
+
readonly environments: readonly ["development", "staging", "production"];
|
|
462
|
+
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";
|
|
451
463
|
}];
|
|
452
464
|
readonly actions: readonly [{
|
|
453
465
|
readonly tool: "sb_project_create";
|
|
@@ -475,13 +487,21 @@ export declare const capabilities: {
|
|
|
475
487
|
}, {
|
|
476
488
|
readonly tool: "cf_worker_secret_put";
|
|
477
489
|
readonly gate: "two_step_when";
|
|
478
|
-
readonly when: "source supabase_secret, or replacing a secret that exists";
|
|
490
|
+
readonly when: "source supabase_secret, or replacing a secret that exists (not sources web_analytics, turnstile_sitekey or turnstile_secret, whose value comes from the recorded resource)";
|
|
479
491
|
readonly ships: "4";
|
|
480
492
|
}, {
|
|
481
493
|
readonly tool: "vendor_settings_apply";
|
|
482
494
|
readonly gate: "two_step_when";
|
|
483
495
|
readonly when: "production, after the first successful apply on that database; staging and check_only are free";
|
|
484
496
|
readonly ships: "5";
|
|
497
|
+
}, {
|
|
498
|
+
readonly tool: "cf_turnstile_widget_create";
|
|
499
|
+
readonly gate: "free";
|
|
500
|
+
readonly ships: "5";
|
|
501
|
+
}, {
|
|
502
|
+
readonly tool: "cf_turnstile_widget_delete";
|
|
503
|
+
readonly gate: "two_step";
|
|
504
|
+
readonly ships: "5";
|
|
485
505
|
}];
|
|
486
506
|
readonly rules: readonly [{
|
|
487
507
|
readonly id: "database.rls-isolates-users";
|
|
@@ -531,6 +551,12 @@ export declare const capabilities: {
|
|
|
531
551
|
readonly text: "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.";
|
|
532
552
|
readonly enforced_by: "review";
|
|
533
553
|
readonly probe: "database.session-cookie";
|
|
554
|
+
}, {
|
|
555
|
+
readonly id: "database.sign-in-checks-for-bots";
|
|
556
|
+
readonly kind: "must";
|
|
557
|
+
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.";
|
|
558
|
+
readonly enforced_by: "review";
|
|
559
|
+
readonly probe: "database.sign-in-bot-check";
|
|
534
560
|
}];
|
|
535
561
|
readonly examples: readonly [{
|
|
536
562
|
readonly id: "database.example.user-client";
|
|
@@ -600,6 +626,12 @@ export declare const capabilities: {
|
|
|
600
626
|
readonly expect: "pass";
|
|
601
627
|
readonly cost_hint_usd: 0;
|
|
602
628
|
readonly note: "The session cookie is __Host-, Secure, HttpOnly, SameSite=Lax and sealed; a cross-site write is refused; sign-out clears it with every attribute";
|
|
629
|
+
}, {
|
|
630
|
+
readonly id: "database.sign-in-bot-check";
|
|
631
|
+
readonly half: "drill";
|
|
632
|
+
readonly expect: "pass";
|
|
633
|
+
readonly cost_hint_usd: 0;
|
|
634
|
+
readonly note: "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does";
|
|
603
635
|
}];
|
|
604
636
|
};
|
|
605
637
|
readonly email: {
|
|
@@ -1052,6 +1084,69 @@ export declare const capabilities: {
|
|
|
1052
1084
|
readonly note: "Another project's namespace";
|
|
1053
1085
|
}];
|
|
1054
1086
|
};
|
|
1087
|
+
readonly forms: {
|
|
1088
|
+
readonly $schema: "./schema.json";
|
|
1089
|
+
readonly format: 1;
|
|
1090
|
+
readonly component: "forms";
|
|
1091
|
+
readonly title: "Form validation";
|
|
1092
|
+
readonly summary: "workbench-forms checks what people type into a form with one validator that runs the same on the page and at the endpoint. It returns every problem at once as permanent codes, the tidied value to show back in each field and the value to store, with limits from the app's own register and country shapes from workbench-country. It runs in the app itself, with no Gateway call and no cost.";
|
|
1093
|
+
readonly status: "built";
|
|
1094
|
+
readonly surfaces: {
|
|
1095
|
+
readonly package: readonly ["defineForm", "text", "longText", "email", "phone", "money", "whole", "date", "choice", "validate", "readForm", "embedConfig", "inputAttributes", "messageKeys", "tidyLine", "tidyText", "CODES", "FormsSetupError"];
|
|
1096
|
+
};
|
|
1097
|
+
readonly rules: readonly [{
|
|
1098
|
+
readonly id: "forms.endpoint-decides";
|
|
1099
|
+
readonly kind: "must";
|
|
1100
|
+
readonly text: "Validate every form at the endpoint with validate() and store only result.values; the page's copy of the validator (FORMS_SCRIPT) only helps the person, it never decides.";
|
|
1101
|
+
readonly enforced_by: "review";
|
|
1102
|
+
readonly probe: "forms.validate";
|
|
1103
|
+
}, {
|
|
1104
|
+
readonly id: "forms.same-verdict";
|
|
1105
|
+
readonly kind: "must";
|
|
1106
|
+
readonly text: "Put the form on the page with embedConfig and FORMS_SCRIPT, never a second set of rules, so the page and the endpoint reach the same verdict.";
|
|
1107
|
+
readonly enforced_by: "review";
|
|
1108
|
+
readonly probe: "forms.browser";
|
|
1109
|
+
}, {
|
|
1110
|
+
readonly id: "forms.codes-not-sentences";
|
|
1111
|
+
readonly kind: "must";
|
|
1112
|
+
readonly text: "Turn problem codes into words through the strings catalogue (messageKeys: validation.<form>.<field>.<code>, then validation.<code>), never words written in code.";
|
|
1113
|
+
readonly enforced_by: "review";
|
|
1114
|
+
readonly probe: "forms.validate";
|
|
1115
|
+
}, {
|
|
1116
|
+
readonly id: "forms.limits-register";
|
|
1117
|
+
readonly kind: "must";
|
|
1118
|
+
readonly text: "Take every length, amount and date bound from the app's limits register (defineLimits, boundsFor), and keep it matched to the database with workbench-supabase check:limits.";
|
|
1119
|
+
readonly enforced_by: "types";
|
|
1120
|
+
readonly probe: "forms.validate";
|
|
1121
|
+
}, {
|
|
1122
|
+
readonly id: "forms.no-members-on-a-form";
|
|
1123
|
+
readonly kind: "must_not";
|
|
1124
|
+
readonly text: "List a choice field's members on the form: the vocabulary owns them, and the app passes them in context.choices.";
|
|
1125
|
+
readonly enforced_by: "types";
|
|
1126
|
+
readonly probe: "forms.validate";
|
|
1127
|
+
}];
|
|
1128
|
+
readonly examples: readonly [{
|
|
1129
|
+
readonly id: "forms.example.endpoint";
|
|
1130
|
+
readonly title: "Check a posted form at the endpoint";
|
|
1131
|
+
readonly lang: "ts";
|
|
1132
|
+
readonly code: "import { defineForm, email, money, readForm, text, validate } from '@mccaskie-labs/workbench-forms'\nimport { boundsFor } from '@mccaskie-labs/workbench-forms/limits'\nimport { COUNTRY_PACKS } from '@mccaskie-labs/workbench-country'\n\nconst lead = defineForm('lead', { name: text({ limit: 'lead.name', required: true }), email: email({ required: true }), budget: money({ limit: 'lead.budget' }) })\nconst result = validate(lead, readForm(lead, await request.formData()), { bounds: boundsFor(LIMITS, lead), country: COUNTRY_PACKS.PH! })\nif (!result.ok) return show(result.problems, result.shown)\nawait db.from('leads').insert(result.values)";
|
|
1133
|
+
readonly probe: "forms.validate";
|
|
1134
|
+
}];
|
|
1135
|
+
readonly probes: readonly [{
|
|
1136
|
+
readonly id: "forms.validate";
|
|
1137
|
+
readonly half: "runtime";
|
|
1138
|
+
readonly expect: "pass";
|
|
1139
|
+
readonly smoke: true;
|
|
1140
|
+
readonly cost_hint_usd: 0;
|
|
1141
|
+
readonly note: "In the Worker itself: validates a fixed form (text, email, phone, money, date, choice) for PH with one good and one bad input, and compares the stored values (E.164 phone, decimal-string amount, ISO date) and the problem codes with the expected ones";
|
|
1142
|
+
}, {
|
|
1143
|
+
readonly id: "forms.browser";
|
|
1144
|
+
readonly half: "runtime";
|
|
1145
|
+
readonly expect: "pass";
|
|
1146
|
+
readonly cost_hint_usd: 0;
|
|
1147
|
+
readonly note: "The probe page loads FORMS_SCRIPT and the form's embedConfig block, runs the same inputs in the browser and checks the verdict matches the Worker's; no Gateway call";
|
|
1148
|
+
}];
|
|
1149
|
+
};
|
|
1055
1150
|
readonly gateway: {
|
|
1056
1151
|
readonly $schema: "./schema.json";
|
|
1057
1152
|
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 = "c83a7c612319";
|
|
4
4
|
export const capabilities = {
|
|
5
5
|
"ai": {
|
|
6
6
|
"$schema": "./schema.json",
|
|
@@ -621,7 +621,9 @@ export const capabilities = {
|
|
|
621
621
|
"sb_query",
|
|
622
622
|
"sb_project_delete",
|
|
623
623
|
"cf_worker_secret_put",
|
|
624
|
-
"vendor_settings_apply"
|
|
624
|
+
"vendor_settings_apply",
|
|
625
|
+
"cf_turnstile_widget_create",
|
|
626
|
+
"cf_turnstile_widget_delete"
|
|
625
627
|
],
|
|
626
628
|
"package": [
|
|
627
629
|
"supabaseConfig",
|
|
@@ -678,6 +680,28 @@ export const capabilities = {
|
|
|
678
680
|
"production"
|
|
679
681
|
],
|
|
680
682
|
"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"
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
"name": "TURNSTILE_SITE_KEY",
|
|
686
|
+
"required": false,
|
|
687
|
+
"secret": false,
|
|
688
|
+
"environments": [
|
|
689
|
+
"development",
|
|
690
|
+
"staging",
|
|
691
|
+
"production"
|
|
692
|
+
],
|
|
693
|
+
"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"
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"name": "TURNSTILE_SECRET_KEY",
|
|
697
|
+
"required": false,
|
|
698
|
+
"secret": true,
|
|
699
|
+
"environments": [
|
|
700
|
+
"development",
|
|
701
|
+
"staging",
|
|
702
|
+
"production"
|
|
703
|
+
],
|
|
704
|
+
"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"
|
|
681
705
|
}
|
|
682
706
|
],
|
|
683
707
|
"actions": [
|
|
@@ -712,7 +736,7 @@ export const capabilities = {
|
|
|
712
736
|
{
|
|
713
737
|
"tool": "cf_worker_secret_put",
|
|
714
738
|
"gate": "two_step_when",
|
|
715
|
-
"when": "source supabase_secret, or replacing a secret that exists",
|
|
739
|
+
"when": "source supabase_secret, or replacing a secret that exists (not sources web_analytics, turnstile_sitekey or turnstile_secret, whose value comes from the recorded resource)",
|
|
716
740
|
"ships": "4"
|
|
717
741
|
},
|
|
718
742
|
{
|
|
@@ -720,6 +744,16 @@ export const capabilities = {
|
|
|
720
744
|
"gate": "two_step_when",
|
|
721
745
|
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
722
746
|
"ships": "5"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
"tool": "cf_turnstile_widget_create",
|
|
750
|
+
"gate": "free",
|
|
751
|
+
"ships": "5"
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"tool": "cf_turnstile_widget_delete",
|
|
755
|
+
"gate": "two_step",
|
|
756
|
+
"ships": "5"
|
|
723
757
|
}
|
|
724
758
|
],
|
|
725
759
|
"rules": [
|
|
@@ -778,6 +812,13 @@ export const capabilities = {
|
|
|
778
812
|
"text": "Refuse a write (anything but GET or HEAD) made with the session cookie unless sameOrigin(request) is true.",
|
|
779
813
|
"enforced_by": "review",
|
|
780
814
|
"probe": "database.session-cookie"
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
"id": "database.sign-in-checks-for-bots",
|
|
818
|
+
"kind": "must",
|
|
819
|
+
"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.",
|
|
820
|
+
"enforced_by": "review",
|
|
821
|
+
"probe": "database.sign-in-bot-check"
|
|
781
822
|
}
|
|
782
823
|
],
|
|
783
824
|
"examples": [
|
|
@@ -860,6 +901,13 @@ export const capabilities = {
|
|
|
860
901
|
"expect": "pass",
|
|
861
902
|
"cost_hint_usd": 0,
|
|
862
903
|
"note": "The session cookie is __Host-, Secure, HttpOnly, SameSite=Lax and sealed; a cross-site write is refused; sign-out clears it with every attribute"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"id": "database.sign-in-bot-check",
|
|
907
|
+
"half": "drill",
|
|
908
|
+
"expect": "pass",
|
|
909
|
+
"cost_hint_usd": 0,
|
|
910
|
+
"note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
|
|
863
911
|
}
|
|
864
912
|
]
|
|
865
913
|
},
|
|
@@ -1427,6 +1475,99 @@ export const capabilities = {
|
|
|
1427
1475
|
}
|
|
1428
1476
|
]
|
|
1429
1477
|
},
|
|
1478
|
+
"forms": {
|
|
1479
|
+
"$schema": "./schema.json",
|
|
1480
|
+
"format": 1,
|
|
1481
|
+
"component": "forms",
|
|
1482
|
+
"title": "Form validation",
|
|
1483
|
+
"summary": "workbench-forms checks what people type into a form with one validator that runs the same on the page and at the endpoint. It returns every problem at once as permanent codes, the tidied value to show back in each field and the value to store, with limits from the app's own register and country shapes from workbench-country. It runs in the app itself, with no Gateway call and no cost.",
|
|
1484
|
+
"status": "built",
|
|
1485
|
+
"surfaces": {
|
|
1486
|
+
"package": [
|
|
1487
|
+
"defineForm",
|
|
1488
|
+
"text",
|
|
1489
|
+
"longText",
|
|
1490
|
+
"email",
|
|
1491
|
+
"phone",
|
|
1492
|
+
"money",
|
|
1493
|
+
"whole",
|
|
1494
|
+
"date",
|
|
1495
|
+
"choice",
|
|
1496
|
+
"validate",
|
|
1497
|
+
"readForm",
|
|
1498
|
+
"embedConfig",
|
|
1499
|
+
"inputAttributes",
|
|
1500
|
+
"messageKeys",
|
|
1501
|
+
"tidyLine",
|
|
1502
|
+
"tidyText",
|
|
1503
|
+
"CODES",
|
|
1504
|
+
"FormsSetupError"
|
|
1505
|
+
]
|
|
1506
|
+
},
|
|
1507
|
+
"rules": [
|
|
1508
|
+
{
|
|
1509
|
+
"id": "forms.endpoint-decides",
|
|
1510
|
+
"kind": "must",
|
|
1511
|
+
"text": "Validate every form at the endpoint with validate() and store only result.values; the page's copy of the validator (FORMS_SCRIPT) only helps the person, it never decides.",
|
|
1512
|
+
"enforced_by": "review",
|
|
1513
|
+
"probe": "forms.validate"
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
"id": "forms.same-verdict",
|
|
1517
|
+
"kind": "must",
|
|
1518
|
+
"text": "Put the form on the page with embedConfig and FORMS_SCRIPT, never a second set of rules, so the page and the endpoint reach the same verdict.",
|
|
1519
|
+
"enforced_by": "review",
|
|
1520
|
+
"probe": "forms.browser"
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
"id": "forms.codes-not-sentences",
|
|
1524
|
+
"kind": "must",
|
|
1525
|
+
"text": "Turn problem codes into words through the strings catalogue (messageKeys: validation.<form>.<field>.<code>, then validation.<code>), never words written in code.",
|
|
1526
|
+
"enforced_by": "review",
|
|
1527
|
+
"probe": "forms.validate"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"id": "forms.limits-register",
|
|
1531
|
+
"kind": "must",
|
|
1532
|
+
"text": "Take every length, amount and date bound from the app's limits register (defineLimits, boundsFor), and keep it matched to the database with workbench-supabase check:limits.",
|
|
1533
|
+
"enforced_by": "types",
|
|
1534
|
+
"probe": "forms.validate"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
"id": "forms.no-members-on-a-form",
|
|
1538
|
+
"kind": "must_not",
|
|
1539
|
+
"text": "List a choice field's members on the form: the vocabulary owns them, and the app passes them in context.choices.",
|
|
1540
|
+
"enforced_by": "types",
|
|
1541
|
+
"probe": "forms.validate"
|
|
1542
|
+
}
|
|
1543
|
+
],
|
|
1544
|
+
"examples": [
|
|
1545
|
+
{
|
|
1546
|
+
"id": "forms.example.endpoint",
|
|
1547
|
+
"title": "Check a posted form at the endpoint",
|
|
1548
|
+
"lang": "ts",
|
|
1549
|
+
"code": "import { defineForm, email, money, readForm, text, validate } from '@mccaskie-labs/workbench-forms'\nimport { boundsFor } from '@mccaskie-labs/workbench-forms/limits'\nimport { COUNTRY_PACKS } from '@mccaskie-labs/workbench-country'\n\nconst lead = defineForm('lead', { name: text({ limit: 'lead.name', required: true }), email: email({ required: true }), budget: money({ limit: 'lead.budget' }) })\nconst result = validate(lead, readForm(lead, await request.formData()), { bounds: boundsFor(LIMITS, lead), country: COUNTRY_PACKS.PH! })\nif (!result.ok) return show(result.problems, result.shown)\nawait db.from('leads').insert(result.values)",
|
|
1550
|
+
"probe": "forms.validate"
|
|
1551
|
+
}
|
|
1552
|
+
],
|
|
1553
|
+
"probes": [
|
|
1554
|
+
{
|
|
1555
|
+
"id": "forms.validate",
|
|
1556
|
+
"half": "runtime",
|
|
1557
|
+
"expect": "pass",
|
|
1558
|
+
"smoke": true,
|
|
1559
|
+
"cost_hint_usd": 0,
|
|
1560
|
+
"note": "In the Worker itself: validates a fixed form (text, email, phone, money, date, choice) for PH with one good and one bad input, and compares the stored values (E.164 phone, decimal-string amount, ISO date) and the problem codes with the expected ones"
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
"id": "forms.browser",
|
|
1564
|
+
"half": "runtime",
|
|
1565
|
+
"expect": "pass",
|
|
1566
|
+
"cost_hint_usd": 0,
|
|
1567
|
+
"note": "The probe page loads FORMS_SCRIPT and the form's embedConfig block, runs the same inputs in the browser and checks the verdict matches the Worker's; no Gateway call"
|
|
1568
|
+
}
|
|
1569
|
+
]
|
|
1570
|
+
},
|
|
1430
1571
|
"gateway": {
|
|
1431
1572
|
"$schema": "./schema.json",
|
|
1432
1573
|
"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"];
|
|
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"];
|
|
3
3
|
export type Component = (typeof COMPONENTS)[number];
|
|
4
4
|
export declare const allowedSchemas: {
|
|
5
5
|
gateway: z.ZodObject<{
|
|
@@ -103,6 +103,7 @@ export declare const capabilitySchema: z.ZodObject<{
|
|
|
103
103
|
email: "email";
|
|
104
104
|
"email-inbound": "email-inbound";
|
|
105
105
|
"email-sending": "email-sending";
|
|
106
|
+
forms: "forms";
|
|
106
107
|
gateway: "gateway";
|
|
107
108
|
github: "github";
|
|
108
109
|
"image-search": "image-search";
|
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.27",
|
|
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",
|