@mccaskie-labs/workbench-config 0.1.12 → 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,11 @@
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
+
3
9
  ## 0.1.12
4
10
 
5
11
  ### 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.
@@ -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
+ }
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.12",
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",