@mccaskie-labs/workbench-config 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.1.13
4
+
5
+ ### Patch Changes
6
+
7
+ - The brand file: `brand.json` at the root of an app's repo (`BRAND_FILE`), checked by `defineBrand` (or `brandSchema`) and filled with the default colours (`BRAND_DEFAULTS`, `resolveBrand`). Name, site address, logo (https PNG, JPEG or SVG), colours, optional fonts, support address and footer line. workbench-email's account emails, workbench-pdf and Supabase's sign-in emails all read it, so an app is branded once.
8
+
9
+ ## 0.1.12
10
+
11
+ ### Patch Changes
12
+
13
+ - Capabilities: analytics lists cf_web_analytics_site_delete and the WEB_ANALYTICS_TOKEN config written by cf_worker_secret_put source=web_analytics; manifests may say a tool ships in phase 5.
14
+
3
15
  ## 0.1.11
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -27,6 +27,7 @@ const { WORKBENCH_GATEWAY_KEY } = config.parse(env)
27
27
  | `sharedSecret(binding, name?)` | Reads a Secrets Store binding; throws if it is empty |
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
+ | `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 |
30
31
  | `DEFAULT_GATEWAY_URL` | `https://gateway.mccaskielabs.dev` |
31
32
 
32
33
  `npx workbench-config check [dir]` compares the variable names in `.dev.vars` with `.dev.vars.example` and prints names only.
@@ -12,21 +12,40 @@
12
12
  "eventsMigration"
13
13
  ],
14
14
  "tools": [
15
- "cf_web_analytics_site_create"
15
+ "cf_web_analytics_site_create",
16
+ "cf_web_analytics_site_delete",
17
+ "cf_worker_secret_put"
16
18
  ]
17
19
  },
20
+ "config": [
21
+ {
22
+ "name": "WEB_ANALYTICS_TOKEN",
23
+ "required": false,
24
+ "secret": false,
25
+ "environments": [
26
+ "staging",
27
+ "production"
28
+ ],
29
+ "description": "The public beacon token of the Web Analytics site for the Worker's own hostname, for beaconScript(); written by cf_worker_secret_put source=web_analytics (free, new or replacing). Unset in development and CI, where beaconScript returns ''"
30
+ }
31
+ ],
18
32
  "actions": [
19
33
  {
20
34
  "tool": "cf_web_analytics_site_create",
21
35
  "gate": "free",
22
36
  "ships": "4"
37
+ },
38
+ {
39
+ "tool": "cf_web_analytics_site_delete",
40
+ "gate": "two_step",
41
+ "ships": "5"
23
42
  }
24
43
  ],
25
44
  "rules": [
26
45
  {
27
46
  "id": "analytics.beacon-on-pages",
28
47
  "kind": "must",
29
- "text": "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.",
48
+ "text": "Put beaconScript(env.WEB_ANALYTICS_TOKEN) on every page; Control writes the site's public beacon token onto each Worker with cf_worker_secret_put source=web_analytics.",
30
49
  "enforced_by": "review",
31
50
  "probe": "analytics.beacon"
32
51
  },
@@ -211,6 +211,7 @@
211
211
  "enum": [
212
212
  "3",
213
213
  "4",
214
+ "5",
214
215
  "later"
215
216
  ]
216
217
  }
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * One brand file per app, read by workbench-email (account emails) and
4
+ * workbench-pdf (branded PDFs), so an app is branded once. Agreed between the
5
+ * sign-in kit and branded PDFs work on 11 November 2026.
6
+ *
7
+ * Lives in the app's repo as `brand.json` at the root, so Control can read it
8
+ * at a ref (as it reads migration files) without running the app's code. The
9
+ * app imports and checks it once:
10
+ *
11
+ * import brandFile from '../brand.json'
12
+ * export const brand = defineBrand(brandFile)
13
+ */
14
+ /** Where the brand file sits in an app's repo. */
15
+ export declare const BRAND_FILE = "brand.json";
16
+ export declare const brandSchema: z.ZodObject<{
17
+ name: z.ZodString;
18
+ siteUrl: z.ZodURL;
19
+ logo: z.ZodObject<{
20
+ url: z.ZodURL;
21
+ alt: z.ZodOptional<z.ZodString>;
22
+ width: z.ZodOptional<z.ZodNumber>;
23
+ height: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strict>;
25
+ colours: z.ZodObject<{
26
+ primary: z.ZodString;
27
+ text: z.ZodOptional<z.ZodString>;
28
+ background: z.ZodOptional<z.ZodString>;
29
+ muted: z.ZodOptional<z.ZodString>;
30
+ }, z.core.$strict>;
31
+ fonts: z.ZodOptional<z.ZodObject<{
32
+ heading: z.ZodOptional<z.ZodObject<{
33
+ family: z.ZodString;
34
+ url: z.ZodOptional<z.ZodURL>;
35
+ }, z.core.$strict>>;
36
+ body: z.ZodOptional<z.ZodObject<{
37
+ family: z.ZodString;
38
+ url: z.ZodOptional<z.ZodURL>;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>>;
41
+ supportEmail: z.ZodOptional<z.ZodEmail>;
42
+ footer: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strict>;
44
+ export type Brand = z.infer<typeof brandSchema>;
45
+ /** The colours a brand leaves out. */
46
+ export declare const BRAND_DEFAULTS: {
47
+ readonly text: "#1a1a1a";
48
+ readonly background: "#ffffff";
49
+ readonly muted: "#6b6b6b";
50
+ };
51
+ /** A brand with every colour filled, so emails and PDFs fill the same values. */
52
+ export type ResolvedBrand = Brand & {
53
+ colours: Required<Brand["colours"]>;
54
+ };
55
+ /**
56
+ * Validates a brand and fills its default colours. Throws a plain Error
57
+ * naming every bad field (the brand holds nothing secret, so values may show).
58
+ */
59
+ export declare function defineBrand(input: unknown): ResolvedBrand;
60
+ /** Fills the default colours of an already valid brand. */
61
+ export declare function resolveBrand(brand: Brand): ResolvedBrand;
package/dist/brand.js ADDED
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * One brand file per app, read by workbench-email (account emails) and
4
+ * workbench-pdf (branded PDFs), so an app is branded once. Agreed between the
5
+ * sign-in kit and branded PDFs work on 11 November 2026.
6
+ *
7
+ * Lives in the app's repo as `brand.json` at the root, so Control can read it
8
+ * at a ref (as it reads migration files) without running the app's code. The
9
+ * app imports and checks it once:
10
+ *
11
+ * import brandFile from '../brand.json'
12
+ * export const brand = defineBrand(brandFile)
13
+ */
14
+ /** Where the brand file sits in an app's repo. */
15
+ export const BRAND_FILE = "brand.json";
16
+ const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, "expected a colour like #1f6feb");
17
+ const https = z.url({ protocol: /^https$/ });
18
+ // A PNG, JPEG or SVG by its path: the PDF inlines the logo and refuses
19
+ // anything else, and email clients show only these reliably.
20
+ const logoUrl = https.refine((u) => /\.(png|jpe?g|svg)$/i.test(new URL(u).pathname), "expected a .png, .jpg, .jpeg or .svg URL");
21
+ const font = z.strictObject({
22
+ /** The CSS family name. Without `url` it is used as a local font, with a system fallback. */
23
+ family: z.string().min(1).max(100),
24
+ /** An https stylesheet, such as a Google Fonts css2 link. */
25
+ url: https.optional(),
26
+ });
27
+ export const brandSchema = z.strictObject({
28
+ /** The app's name as people see it: "Sent by <name>", the PDF header. */
29
+ name: z.string().min(1).max(60),
30
+ /** The app's home page, https. Email links and the PDF footer. */
31
+ siteUrl: https,
32
+ logo: z.strictObject({
33
+ url: logoUrl,
34
+ alt: z.string().min(1).max(100).optional(),
35
+ /** Display width in CSS pixels; emails default to 120. */
36
+ width: z.number().int().min(16).max(600).optional(),
37
+ height: z.number().int().min(16).max(600).optional(),
38
+ }),
39
+ colours: z.strictObject({
40
+ /** Buttons, links and headings. */
41
+ primary: hex,
42
+ text: hex.optional(),
43
+ background: hex.optional(),
44
+ /** Small print and rules. */
45
+ muted: hex.optional(),
46
+ }),
47
+ fonts: z.strictObject({ heading: font.optional(), body: font.optional() }).optional(),
48
+ /** Shown as "Questions? Write to …" in account emails. */
49
+ supportEmail: z.email().optional(),
50
+ /** One line of small print under every email and PDF page. */
51
+ footer: z.string().min(1).max(200).optional(),
52
+ });
53
+ /** The colours a brand leaves out. */
54
+ export const BRAND_DEFAULTS = {
55
+ text: "#1a1a1a",
56
+ background: "#ffffff",
57
+ muted: "#6b6b6b",
58
+ };
59
+ /**
60
+ * Validates a brand and fills its default colours. Throws a plain Error
61
+ * naming every bad field (the brand holds nothing secret, so values may show).
62
+ */
63
+ export function defineBrand(input) {
64
+ const result = brandSchema.safeParse(input);
65
+ if (!result.success) {
66
+ const lines = result.error.issues.map((i) => `${i.path.join(".") || "(brand)"}: ${i.message}`);
67
+ throw new Error(`Invalid brand:\n${lines.join("\n")}`);
68
+ }
69
+ return resolveBrand(result.data);
70
+ }
71
+ /** Fills the default colours of an already valid brand. */
72
+ export function resolveBrand(brand) {
73
+ const c = brand.colours;
74
+ return {
75
+ ...brand,
76
+ colours: {
77
+ primary: c.primary,
78
+ text: c.text ?? BRAND_DEFAULTS.text,
79
+ background: c.background ?? BRAND_DEFAULTS.background,
80
+ muted: c.muted ?? BRAND_DEFAULTS.muted,
81
+ },
82
+ };
83
+ }
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "debb0b3db404";
1
+ export declare const MANIFEST_VERSION: "f3d5b55e579d";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -145,18 +145,29 @@ export declare const capabilities: {
145
145
  readonly summary: "Cloudflare Web Analytics counts page views through a beacon on every page; product events go into the app's own database through workbench-analytics. Apps hold no Cloudflare token: Claude reads page views through Control.";
146
146
  readonly status: "built";
147
147
  readonly surfaces: {
148
- readonly tools: readonly ["cf_web_analytics_site_create"];
148
+ readonly tools: readonly ["cf_web_analytics_site_create", "cf_web_analytics_site_delete", "cf_worker_secret_put"];
149
149
  readonly package: readonly ["beaconScript", "createEvents", "eventsMigration"];
150
150
  };
151
+ readonly config: readonly [{
152
+ readonly name: "WEB_ANALYTICS_TOKEN";
153
+ readonly required: false;
154
+ readonly secret: false;
155
+ readonly environments: readonly ["staging", "production"];
156
+ readonly description: "The public beacon token of the Web Analytics site for the Worker's own hostname, for beaconScript(); written by cf_worker_secret_put source=web_analytics (free, new or replacing). Unset in development and CI, where beaconScript returns ''";
157
+ }];
151
158
  readonly actions: readonly [{
152
159
  readonly tool: "cf_web_analytics_site_create";
153
160
  readonly gate: "free";
154
161
  readonly ships: "4";
162
+ }, {
163
+ readonly tool: "cf_web_analytics_site_delete";
164
+ readonly gate: "two_step";
165
+ readonly ships: "5";
155
166
  }];
156
167
  readonly rules: readonly [{
157
168
  readonly id: "analytics.beacon-on-pages";
158
169
  readonly kind: "must";
159
- readonly text: "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.";
170
+ readonly text: "Put beaconScript(env.WEB_ANALYTICS_TOKEN) on every page; Control writes the site's public beacon token onto each Worker with cf_worker_secret_put source=web_analytics.";
160
171
  readonly enforced_by: "review";
161
172
  readonly probe: "analytics.beacon";
162
173
  }, {
@@ -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 = "debb0b3db404";
3
+ export const MANIFEST_VERSION = "f3d5b55e579d";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -203,7 +203,9 @@ export const capabilities = {
203
203
  "status": "built",
204
204
  "surfaces": {
205
205
  "tools": [
206
- "cf_web_analytics_site_create"
206
+ "cf_web_analytics_site_create",
207
+ "cf_web_analytics_site_delete",
208
+ "cf_worker_secret_put"
207
209
  ],
208
210
  "package": [
209
211
  "beaconScript",
@@ -211,18 +213,35 @@ export const capabilities = {
211
213
  "eventsMigration"
212
214
  ]
213
215
  },
216
+ "config": [
217
+ {
218
+ "name": "WEB_ANALYTICS_TOKEN",
219
+ "required": false,
220
+ "secret": false,
221
+ "environments": [
222
+ "staging",
223
+ "production"
224
+ ],
225
+ "description": "The public beacon token of the Web Analytics site for the Worker's own hostname, for beaconScript(); written by cf_worker_secret_put source=web_analytics (free, new or replacing). Unset in development and CI, where beaconScript returns ''"
226
+ }
227
+ ],
214
228
  "actions": [
215
229
  {
216
230
  "tool": "cf_web_analytics_site_create",
217
231
  "gate": "free",
218
232
  "ships": "4"
233
+ },
234
+ {
235
+ "tool": "cf_web_analytics_site_delete",
236
+ "gate": "two_step",
237
+ "ships": "5"
219
238
  }
220
239
  ],
221
240
  "rules": [
222
241
  {
223
242
  "id": "analytics.beacon-on-pages",
224
243
  "kind": "must",
225
- "text": "Put beaconScript(token) on every page, with the site's public beacon token from cf_web_analytics_site_create.",
244
+ "text": "Put beaconScript(env.WEB_ANALYTICS_TOKEN) on every page; Control writes the site's public beacon token onto each Worker with cf_worker_secret_put source=web_analytics.",
226
245
  "enforced_by": "review",
227
246
  "probe": "analytics.beacon"
228
247
  },
@@ -146,6 +146,7 @@ export declare const capabilitySchema: z.ZodObject<{
146
146
  ships: z.ZodEnum<{
147
147
  3: "3";
148
148
  4: "4";
149
+ 5: "5";
149
150
  later: "later";
150
151
  }>;
151
152
  }, z.core.$strict>>>;
@@ -45,7 +45,7 @@ const action = z.strictObject({
45
45
  tool: z.string().regex(/^[a-z]+_[a-z0-9_]+$/),
46
46
  gate: z.enum(["free", "two_step", "two_step_on_production", "two_step_when"]),
47
47
  when: z.string().min(1).optional(),
48
- ships: z.enum(["3", "4", "later"]),
48
+ ships: z.enum(["3", "4", "5", "later"]),
49
49
  });
50
50
  const errorEntry = z.strictObject({
51
51
  code,
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { WorkbenchConfigError, type ConfigErrorCode, type ConfigProblem } from "
5
5
  export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, } from "./capability-schema.ts";
6
6
  export { capabilities, MANIFEST_VERSION, type Alias, type GatewayScope } from "./capabilities.gen.ts";
7
7
  export { compareDevVars, parseDevVarNames } from "./dev-vars.ts";
8
+ export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS, type Brand, type ResolvedBrand } from "./brand.ts";
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export { WorkbenchConfigError } from "./errors.js";
5
5
  export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, } from "./capability-schema.js";
6
6
  export { capabilities, MANIFEST_VERSION } from "./capabilities.gen.js";
7
7
  export { compareDevVars, parseDevVarNames } from "./dev-vars.js";
8
+ export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS } from "./brand.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",