@mccaskie-labs/workbench-config 0.1.18 → 0.1.20
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 +1 -0
- package/capabilities/database.json +22 -1
- package/dist/capabilities.gen.d.ts +19 -2
- package/dist/capabilities.gen.js +23 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/vendor-settings.d.ts +97 -0
- package/dist/vendor-settings.js +163 -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.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Vendor settings baseline: sign-in codes (`supabase.auth.email_code_lifetime`) now stay valid for one hour (3600 seconds), Supabase's default, as Robert decided. It was 10 minutes.
|
|
8
|
+
|
|
9
|
+
## 0.1.19
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- The vendor settings baseline: `VENDOR_SETTINGS`, the register of settings that live in a vendor's dashboard (id, API field, value, where it is, what breaks if it changes), starting with Supabase sign-in: six-digit email codes, a 10-minute code lifetime, the Send Email hook on, current password to change a password, secure email change. A project changes a row in `vendor-settings.json` (`VENDOR_SETTINGS_FILE`) with a reason; `resolveVendorSettings` applies it and names every problem at once. `SEND_EMAIL_HOOK_PATH` and `SEND_EMAIL_HOOK_SECRET` name the hook's address and the Worker secret. Control applies the baseline with `vendor_settings_apply` and checks it daily.
|
|
14
|
+
|
|
3
15
|
## 0.1.18
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ const { WORKBENCH_GATEWAY_KEY } = config.parse(env)
|
|
|
28
28
|
| `redact(value)`, `redactAll(text, secrets)` | `[redacted]` in place of a secret, for logs |
|
|
29
29
|
| `capabilities`, `MANIFEST_VERSION`, `capabilitySchema` | The Workbench capabilities manifests and their schema. The JSON files are also at `@mccaskie-labs/workbench-config/capabilities/<component>.json` |
|
|
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
|
+
| `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 |
|
|
31
32
|
| `DEFAULT_GATEWAY_URL` | `https://gateway.mccaskielabs.dev` |
|
|
32
33
|
|
|
33
34
|
`npx workbench-config check [dir]` compares the variable names in `.dev.vars` with `.dev.vars.example` and prints names only.
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"sb_migration_rollback",
|
|
21
21
|
"sb_query",
|
|
22
22
|
"sb_project_delete",
|
|
23
|
-
"cf_worker_secret_put"
|
|
23
|
+
"cf_worker_secret_put",
|
|
24
|
+
"vendor_settings_apply"
|
|
24
25
|
]
|
|
25
26
|
},
|
|
26
27
|
"config": [
|
|
@@ -91,6 +92,12 @@
|
|
|
91
92
|
"gate": "two_step_when",
|
|
92
93
|
"when": "source supabase_secret, or replacing a secret that exists",
|
|
93
94
|
"ships": "4"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"tool": "vendor_settings_apply",
|
|
98
|
+
"gate": "two_step_when",
|
|
99
|
+
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
100
|
+
"ships": "5"
|
|
94
101
|
}
|
|
95
102
|
],
|
|
96
103
|
"rules": [
|
|
@@ -128,6 +135,13 @@
|
|
|
128
135
|
"text": "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.",
|
|
129
136
|
"enforced_by": "review",
|
|
130
137
|
"probe": "database.prod-schema-check"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "database.vendor-settings-baseline",
|
|
141
|
+
"kind": "must",
|
|
142
|
+
"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.",
|
|
143
|
+
"enforced_by": "control",
|
|
144
|
+
"probe": "database.vendor-settings"
|
|
131
145
|
}
|
|
132
146
|
],
|
|
133
147
|
"examples": [
|
|
@@ -181,6 +195,13 @@
|
|
|
181
195
|
"two_step": true,
|
|
182
196
|
"cost_hint_usd": 0,
|
|
183
197
|
"note": "sb_query on production with a write returns a confirmation, not a result"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "database.vendor-settings",
|
|
201
|
+
"half": "drill",
|
|
202
|
+
"expect": "pass",
|
|
203
|
+
"cost_hint_usd": 0,
|
|
204
|
+
"note": "vendor_settings_apply check_only reports that both databases match the baseline, the Send Email hook included"
|
|
184
205
|
}
|
|
185
206
|
]
|
|
186
207
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MANIFEST_VERSION: "
|
|
1
|
+
export declare const MANIFEST_VERSION: "71462ff7fe4c";
|
|
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"];
|
|
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"];
|
|
425
425
|
readonly package: readonly ["supabaseConfig", "createBrowserClient", "createServerClient", "createUserClient", "createServiceClient", "WorkbenchSupabaseError"];
|
|
426
426
|
};
|
|
427
427
|
readonly config: readonly [{
|
|
@@ -471,6 +471,11 @@ export declare const capabilities: {
|
|
|
471
471
|
readonly gate: "two_step_when";
|
|
472
472
|
readonly when: "source supabase_secret, or replacing a secret that exists";
|
|
473
473
|
readonly ships: "4";
|
|
474
|
+
}, {
|
|
475
|
+
readonly tool: "vendor_settings_apply";
|
|
476
|
+
readonly gate: "two_step_when";
|
|
477
|
+
readonly when: "production, after the first successful apply on that database; staging and check_only are free";
|
|
478
|
+
readonly ships: "5";
|
|
474
479
|
}];
|
|
475
480
|
readonly rules: readonly [{
|
|
476
481
|
readonly id: "database.rls-isolates-users";
|
|
@@ -502,6 +507,12 @@ export declare const capabilities: {
|
|
|
502
507
|
readonly text: "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.";
|
|
503
508
|
readonly enforced_by: "review";
|
|
504
509
|
readonly probe: "database.prod-schema-check";
|
|
510
|
+
}, {
|
|
511
|
+
readonly id: "database.vendor-settings-baseline";
|
|
512
|
+
readonly kind: "must";
|
|
513
|
+
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.";
|
|
514
|
+
readonly enforced_by: "control";
|
|
515
|
+
readonly probe: "database.vendor-settings";
|
|
505
516
|
}];
|
|
506
517
|
readonly examples: readonly [{
|
|
507
518
|
readonly id: "database.example.user-client";
|
|
@@ -546,6 +557,12 @@ export declare const capabilities: {
|
|
|
546
557
|
readonly cost_hint_usd: 0;
|
|
547
558
|
readonly two_step: true;
|
|
548
559
|
readonly note: "sb_query on production with a write returns a confirmation, not a result";
|
|
560
|
+
}, {
|
|
561
|
+
readonly id: "database.vendor-settings";
|
|
562
|
+
readonly half: "drill";
|
|
563
|
+
readonly expect: "pass";
|
|
564
|
+
readonly cost_hint_usd: 0;
|
|
565
|
+
readonly note: "vendor_settings_apply check_only reports that both databases match the baseline, the Send Email hook included";
|
|
549
566
|
}];
|
|
550
567
|
};
|
|
551
568
|
readonly email: {
|
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 = "71462ff7fe4c";
|
|
4
4
|
export const capabilities = {
|
|
5
5
|
"ai": {
|
|
6
6
|
"$schema": "./schema.json",
|
|
@@ -620,7 +620,8 @@ export const capabilities = {
|
|
|
620
620
|
"sb_migration_rollback",
|
|
621
621
|
"sb_query",
|
|
622
622
|
"sb_project_delete",
|
|
623
|
-
"cf_worker_secret_put"
|
|
623
|
+
"cf_worker_secret_put",
|
|
624
|
+
"vendor_settings_apply"
|
|
624
625
|
],
|
|
625
626
|
"package": [
|
|
626
627
|
"supabaseConfig",
|
|
@@ -699,6 +700,12 @@ export const capabilities = {
|
|
|
699
700
|
"gate": "two_step_when",
|
|
700
701
|
"when": "source supabase_secret, or replacing a secret that exists",
|
|
701
702
|
"ships": "4"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"tool": "vendor_settings_apply",
|
|
706
|
+
"gate": "two_step_when",
|
|
707
|
+
"when": "production, after the first successful apply on that database; staging and check_only are free",
|
|
708
|
+
"ships": "5"
|
|
702
709
|
}
|
|
703
710
|
],
|
|
704
711
|
"rules": [
|
|
@@ -736,6 +743,13 @@ export const capabilities = {
|
|
|
736
743
|
"text": "Ship code that names a table, column or function production's database lacks; check with npx workbench-supabase check:prod-schema.",
|
|
737
744
|
"enforced_by": "review",
|
|
738
745
|
"probe": "database.prod-schema-check"
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"id": "database.vendor-settings-baseline",
|
|
749
|
+
"kind": "must",
|
|
750
|
+
"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.",
|
|
751
|
+
"enforced_by": "control",
|
|
752
|
+
"probe": "database.vendor-settings"
|
|
739
753
|
}
|
|
740
754
|
],
|
|
741
755
|
"examples": [
|
|
@@ -789,6 +803,13 @@ export const capabilities = {
|
|
|
789
803
|
"cost_hint_usd": 0,
|
|
790
804
|
"two_step": true,
|
|
791
805
|
"note": "sb_query on production with a write returns a confirmation, not a result"
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
"id": "database.vendor-settings",
|
|
809
|
+
"half": "drill",
|
|
810
|
+
"expect": "pass",
|
|
811
|
+
"cost_hint_usd": 0,
|
|
812
|
+
"note": "vendor_settings_apply check_only reports that both databases match the baseline, the Send Email hook included"
|
|
792
813
|
}
|
|
793
814
|
]
|
|
794
815
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capa
|
|
|
6
6
|
export { capabilities, MANIFEST_VERSION, type Alias, type GatewayScope } from "./capabilities.gen.ts";
|
|
7
7
|
export { compareDevVars, parseDevVarNames } from "./dev-vars.ts";
|
|
8
8
|
export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS, type Brand, type ResolvedBrand } from "./brand.ts";
|
|
9
|
+
export { VENDOR_SETTINGS, VENDOR_SETTINGS_FILE, SEND_EMAIL_HOOK, SEND_EMAIL_HOOK_PATH, SEND_EMAIL_HOOK_SECRET, vendorSettingsFileSchema, resolveVendorSettings, type VendorSetting, type VendorSettingEnv, type VendorSettingsFile, type ResolvedVendorSetting, } from "./vendor-settings.ts";
|
package/dist/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, } from ".
|
|
|
6
6
|
export { capabilities, MANIFEST_VERSION } from "./capabilities.gen.js";
|
|
7
7
|
export { compareDevVars, parseDevVarNames } from "./dev-vars.js";
|
|
8
8
|
export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS } from "./brand.js";
|
|
9
|
+
export { VENDOR_SETTINGS, VENDOR_SETTINGS_FILE, SEND_EMAIL_HOOK, SEND_EMAIL_HOOK_PATH, SEND_EMAIL_HOOK_SECRET, vendorSettingsFileSchema, resolveVendorSettings, } from "./vendor-settings.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The vendor settings baseline (docs/spec.md §10, 11 November 2026, "Four
|
|
4
|
+
* shared tools from Robert's project standards", item 3). Settings that live
|
|
5
|
+
* in a vendor's dashboard, where no migration, test or review can see them.
|
|
6
|
+
* The standards' answer is a register re-applied by hand on every new
|
|
7
|
+
* project; Workbench keeps the register here and Control applies it
|
|
8
|
+
* (`vendor_settings_apply`) and compares it with the live values every day.
|
|
9
|
+
*
|
|
10
|
+
* Each row says the setting, its value, where it lives and what breaks if it
|
|
11
|
+
* changes. A project changes a row in `vendor-settings.json` at the root of
|
|
12
|
+
* its repo, with a reason, and Control reads that file from `main`:
|
|
13
|
+
*
|
|
14
|
+
* { "overrides": [
|
|
15
|
+
* { "id": "supabase.auth.email_code_length", "value": 8,
|
|
16
|
+
* "reason": "The bank's audit asks for eight digits" } ] }
|
|
17
|
+
*/
|
|
18
|
+
/** Where a project's overrides sit in its repo. */
|
|
19
|
+
export declare const VENDOR_SETTINGS_FILE = "vendor-settings.json";
|
|
20
|
+
export type VendorSettingEnv = "staging" | "production";
|
|
21
|
+
interface Row {
|
|
22
|
+
/** Permanent name: `<vendor>.<area>.<setting>`. Overrides name it. */
|
|
23
|
+
id: string;
|
|
24
|
+
vendor: "supabase";
|
|
25
|
+
area: "auth";
|
|
26
|
+
/** The field in the vendor's API (Supabase: PATCH /v1/projects/{ref}/config/auth). */
|
|
27
|
+
field: string;
|
|
28
|
+
/** Where a person finds it in the dashboard. */
|
|
29
|
+
where: string;
|
|
30
|
+
/** What breaks if it changes. */
|
|
31
|
+
breaks: string;
|
|
32
|
+
}
|
|
33
|
+
interface BooleanSetting extends Row {
|
|
34
|
+
type: "boolean";
|
|
35
|
+
value: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface IntegerSetting extends Row {
|
|
38
|
+
type: "integer";
|
|
39
|
+
value: number;
|
|
40
|
+
min: number;
|
|
41
|
+
max: number;
|
|
42
|
+
unit?: string;
|
|
43
|
+
}
|
|
44
|
+
export type VendorSetting = BooleanSetting | IntegerSetting;
|
|
45
|
+
/**
|
|
46
|
+
* The Send Email hook row. On means Supabase makes the code and calls the
|
|
47
|
+
* app's own endpoint, which sends the email through workbench-email and the
|
|
48
|
+
* Gateway (decided 11 November 2026: one sender, one set of words). Control
|
|
49
|
+
* also sets the hook's address and its signing secret, which it puts on the
|
|
50
|
+
* project's Worker as SEND_EMAIL_HOOK_SECRET; neither is a row of its own.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SEND_EMAIL_HOOK = "supabase.auth.send_email_hook";
|
|
53
|
+
/** The path on the app's own hostname that Supabase calls. Agreed with the sign-in kit (createSendEmailHook in workbench-email, PR #173). */
|
|
54
|
+
export declare const SEND_EMAIL_HOOK_PATH = "/auth/send-email";
|
|
55
|
+
/** The Worker secret that holds the hook's signing secret (v1,whsec_…). */
|
|
56
|
+
export declare const SEND_EMAIL_HOOK_SECRET = "SEND_EMAIL_HOOK_SECRET";
|
|
57
|
+
/** The baseline. Order is the order Control reports in. */
|
|
58
|
+
export declare const VENDOR_SETTINGS: readonly VendorSetting[];
|
|
59
|
+
/** The shape of `vendor-settings.json`. */
|
|
60
|
+
export declare const vendorSettingsFileSchema: z.ZodObject<{
|
|
61
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
62
|
+
overrides: z.ZodArray<z.ZodObject<{
|
|
63
|
+
id: z.ZodString;
|
|
64
|
+
value: z.ZodUnion<readonly [z.ZodBoolean, z.ZodNumber]>;
|
|
65
|
+
reason: z.ZodString;
|
|
66
|
+
env: z.ZodOptional<z.ZodEnum<{
|
|
67
|
+
production: "production";
|
|
68
|
+
staging: "staging";
|
|
69
|
+
}>>;
|
|
70
|
+
}, z.core.$strict>>;
|
|
71
|
+
}, z.core.$strict>;
|
|
72
|
+
export type VendorSettingsFile = z.infer<typeof vendorSettingsFileSchema>;
|
|
73
|
+
/** A row with the value that applies to one project and environment. */
|
|
74
|
+
export type ResolvedVendorSetting = VendorSetting & {
|
|
75
|
+
/** "default" from the baseline, or "override" from the project's file. */
|
|
76
|
+
source: "default" | "override";
|
|
77
|
+
/** The override's reason. */
|
|
78
|
+
reason?: string;
|
|
79
|
+
/** The baseline's value, when an override changed it. */
|
|
80
|
+
default?: boolean | number;
|
|
81
|
+
};
|
|
82
|
+
export type ResolveResult = {
|
|
83
|
+
ok: true;
|
|
84
|
+
settings: ResolvedVendorSetting[];
|
|
85
|
+
} | {
|
|
86
|
+
ok: false;
|
|
87
|
+
problems: string[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The settings for one environment: the baseline with the project's overrides
|
|
91
|
+
* applied. `file` is the parsed JSON of `vendor-settings.json`, or null when
|
|
92
|
+
* the repo has none. Returns every problem at once (an unknown id, a value of
|
|
93
|
+
* the wrong type or out of range, a row overridden twice) and no settings, so
|
|
94
|
+
* a bad file never half-applies.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveVendorSettings(file: unknown, env: VendorSettingEnv): ResolveResult;
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The vendor settings baseline (docs/spec.md §10, 11 November 2026, "Four
|
|
4
|
+
* shared tools from Robert's project standards", item 3). Settings that live
|
|
5
|
+
* in a vendor's dashboard, where no migration, test or review can see them.
|
|
6
|
+
* The standards' answer is a register re-applied by hand on every new
|
|
7
|
+
* project; Workbench keeps the register here and Control applies it
|
|
8
|
+
* (`vendor_settings_apply`) and compares it with the live values every day.
|
|
9
|
+
*
|
|
10
|
+
* Each row says the setting, its value, where it lives and what breaks if it
|
|
11
|
+
* changes. A project changes a row in `vendor-settings.json` at the root of
|
|
12
|
+
* its repo, with a reason, and Control reads that file from `main`:
|
|
13
|
+
*
|
|
14
|
+
* { "overrides": [
|
|
15
|
+
* { "id": "supabase.auth.email_code_length", "value": 8,
|
|
16
|
+
* "reason": "The bank's audit asks for eight digits" } ] }
|
|
17
|
+
*/
|
|
18
|
+
/** Where a project's overrides sit in its repo. */
|
|
19
|
+
export const VENDOR_SETTINGS_FILE = "vendor-settings.json";
|
|
20
|
+
/**
|
|
21
|
+
* The Send Email hook row. On means Supabase makes the code and calls the
|
|
22
|
+
* app's own endpoint, which sends the email through workbench-email and the
|
|
23
|
+
* Gateway (decided 11 November 2026: one sender, one set of words). Control
|
|
24
|
+
* also sets the hook's address and its signing secret, which it puts on the
|
|
25
|
+
* project's Worker as SEND_EMAIL_HOOK_SECRET; neither is a row of its own.
|
|
26
|
+
*/
|
|
27
|
+
export const SEND_EMAIL_HOOK = "supabase.auth.send_email_hook";
|
|
28
|
+
/** The path on the app's own hostname that Supabase calls. Agreed with the sign-in kit (createSendEmailHook in workbench-email, PR #173). */
|
|
29
|
+
export const SEND_EMAIL_HOOK_PATH = "/auth/send-email";
|
|
30
|
+
/** The Worker secret that holds the hook's signing secret (v1,whsec_…). */
|
|
31
|
+
export const SEND_EMAIL_HOOK_SECRET = "SEND_EMAIL_HOOK_SECRET";
|
|
32
|
+
/** The baseline. Order is the order Control reports in. */
|
|
33
|
+
export const VENDOR_SETTINGS = Object.freeze([
|
|
34
|
+
{
|
|
35
|
+
id: "supabase.auth.email_code_length",
|
|
36
|
+
vendor: "supabase",
|
|
37
|
+
area: "auth",
|
|
38
|
+
field: "mailer_otp_length",
|
|
39
|
+
type: "integer",
|
|
40
|
+
value: 6,
|
|
41
|
+
min: 6,
|
|
42
|
+
max: 10,
|
|
43
|
+
unit: "digits",
|
|
44
|
+
where: "Supabase: Authentication, Sign In / Providers, Email, Email OTP Length",
|
|
45
|
+
breaks: "Emails carry a code of a different length from the one the app's screens, words and tests expect. It once shipped as eight digits while every document said six.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "supabase.auth.email_code_lifetime",
|
|
49
|
+
vendor: "supabase",
|
|
50
|
+
area: "auth",
|
|
51
|
+
field: "mailer_otp_exp",
|
|
52
|
+
type: "integer",
|
|
53
|
+
value: 3_600,
|
|
54
|
+
min: 60,
|
|
55
|
+
max: 86_400,
|
|
56
|
+
unit: "seconds",
|
|
57
|
+
where: "Supabase: Authentication, Sign In / Providers, Email, Email OTP Expiration",
|
|
58
|
+
breaks: "Shorter: people with slow mail are told their code is wrong. Longer: a leaked code works for longer.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: SEND_EMAIL_HOOK,
|
|
62
|
+
vendor: "supabase",
|
|
63
|
+
area: "auth",
|
|
64
|
+
field: "hook_send_email_enabled",
|
|
65
|
+
type: "boolean",
|
|
66
|
+
value: true,
|
|
67
|
+
where: "Supabase: Authentication, Auth Hooks, Send Email hook",
|
|
68
|
+
breaks: "Off: Supabase sends its own sign-in emails from its own sender, with its own words, outside the catalogue and its tests. On with no handler in the app: every sign-in email fails, loudly.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "supabase.auth.password_change_needs_current",
|
|
72
|
+
vendor: "supabase",
|
|
73
|
+
area: "auth",
|
|
74
|
+
field: "security_update_password_require_current_password",
|
|
75
|
+
type: "boolean",
|
|
76
|
+
value: true,
|
|
77
|
+
where: "Supabase: Authentication, Sign In / Providers, Email, Require current password when updating",
|
|
78
|
+
breaks: "Off: anyone holding a signed-in session (a borrowed phone, a sign-in code) can change the password and keep the account.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "supabase.auth.email_change_confirms_both",
|
|
82
|
+
vendor: "supabase",
|
|
83
|
+
area: "auth",
|
|
84
|
+
field: "mailer_secure_email_change_enabled",
|
|
85
|
+
type: "boolean",
|
|
86
|
+
value: true,
|
|
87
|
+
where: "Supabase: Authentication, Sign In / Providers, Email, Secure email change",
|
|
88
|
+
breaks: "Off: an address change is confirmed from the new address only, so the owner of the old one never hears of it.",
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
const override = z.strictObject({
|
|
92
|
+
id: z.string().min(1).max(100),
|
|
93
|
+
value: z.union([z.boolean(), z.number()]),
|
|
94
|
+
/** Why this project differs. Shown on every report. */
|
|
95
|
+
reason: z.string().trim().min(10).max(300),
|
|
96
|
+
/** Only this environment. Default both. */
|
|
97
|
+
env: z.enum(["staging", "production"]).optional(),
|
|
98
|
+
});
|
|
99
|
+
/** The shape of `vendor-settings.json`. */
|
|
100
|
+
export const vendorSettingsFileSchema = z.strictObject({
|
|
101
|
+
$schema: z.string().optional(),
|
|
102
|
+
overrides: z.array(override).max(100),
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* The settings for one environment: the baseline with the project's overrides
|
|
106
|
+
* applied. `file` is the parsed JSON of `vendor-settings.json`, or null when
|
|
107
|
+
* the repo has none. Returns every problem at once (an unknown id, a value of
|
|
108
|
+
* the wrong type or out of range, a row overridden twice) and no settings, so
|
|
109
|
+
* a bad file never half-applies.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveVendorSettings(file, env) {
|
|
112
|
+
if (file === null || file === undefined)
|
|
113
|
+
return { ok: true, settings: VENDOR_SETTINGS.map((s) => ({ ...s, source: "default" })) };
|
|
114
|
+
const parsed = vendorSettingsFileSchema.safeParse(file);
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
return { ok: false, problems: parsed.error.issues.map((i) => `${VENDOR_SETTINGS_FILE} ${i.path.join(".") || "(file)"}: ${i.message}`) };
|
|
117
|
+
}
|
|
118
|
+
const problems = [];
|
|
119
|
+
const byId = new Map(VENDOR_SETTINGS.map((s) => [s.id, s]));
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const chosen = new Map();
|
|
122
|
+
for (const [i, o] of parsed.data.overrides.entries()) {
|
|
123
|
+
const at = `${VENDOR_SETTINGS_FILE} overrides.${i}`;
|
|
124
|
+
const row = byId.get(o.id);
|
|
125
|
+
if (!row) {
|
|
126
|
+
problems.push(`${at}: no setting "${o.id}". Known: ${[...byId.keys()].join(", ")}.`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const e of o.env ? [o.env] : ["staging", "production"]) {
|
|
130
|
+
const key = `${o.id}@${e}`;
|
|
131
|
+
if (seen.has(key))
|
|
132
|
+
problems.push(`${at}: ${o.id} is overridden twice for ${e}.`);
|
|
133
|
+
seen.add(key);
|
|
134
|
+
}
|
|
135
|
+
const p = valueProblem(row, o.value);
|
|
136
|
+
if (p) {
|
|
137
|
+
problems.push(`${at}: ${p}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (o.env === undefined || o.env === env)
|
|
141
|
+
chosen.set(o.id, { value: o.value, reason: o.reason });
|
|
142
|
+
}
|
|
143
|
+
if (problems.length > 0)
|
|
144
|
+
return { ok: false, problems };
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
settings: VENDOR_SETTINGS.map((s) => {
|
|
148
|
+
const c = chosen.get(s.id);
|
|
149
|
+
if (!c)
|
|
150
|
+
return { ...s, source: "default" };
|
|
151
|
+
return { ...s, value: c.value, source: "override", reason: c.reason, default: s.value };
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function valueProblem(row, value) {
|
|
156
|
+
if (row.type === "boolean")
|
|
157
|
+
return typeof value === "boolean" ? null : `${row.id} takes true or false.`;
|
|
158
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
159
|
+
return `${row.id} takes a whole number.`;
|
|
160
|
+
if (value < row.min || value > row.max)
|
|
161
|
+
return `${row.id} must be from ${row.min} to ${row.max}${row.unit ? ` ${row.unit}` : ""}.`;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
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.20",
|
|
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",
|