@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 +12 -0
- package/README.md +1 -0
- package/capabilities/analytics.json +21 -2
- package/capabilities/schema.json +1 -0
- package/dist/brand.d.ts +61 -0
- package/dist/brand.js +83 -0
- package/dist/capabilities.gen.d.ts +14 -3
- package/dist/capabilities.gen.js +22 -3
- package/dist/capability-schema.d.ts +1 -0
- package/dist/capability-schema.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.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.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(
|
|
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
|
},
|
package/capabilities/schema.json
CHANGED
package/dist/brand.d.ts
ADDED
|
@@ -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: "
|
|
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(
|
|
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
|
}, {
|
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 = "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(
|
|
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
|
},
|
|
@@ -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.
|
|
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",
|