@caelo-cms/shared 0.10.29 → 0.10.30

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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Database connection URLs as every Caelo process reads them from its
3
+ * environment.
4
+ *
5
+ * A URL var (`ADMIN_DATABASE_URL`, `PUBLIC_ADMIN_DATABASE_URL`,
6
+ * `PUBLIC_DATABASE_URL`) may carry its password inline — the shape the
7
+ * self-hosted compose stack, CI and `.env` use — or leave it out and have
8
+ * the password come from a companion `<NAME>_PASSWORD` var
9
+ * (`ADMIN_DATABASE_URL` → `ADMIN_DATABASE_PASSWORD`). Cloud installs use the
10
+ * second shape: the URL (host, role, database) is a plain env var, the
11
+ * password is a Secret Manager reference, so the password never shows up in
12
+ * the service's configuration or its revision history.
13
+ *
14
+ * Pure module — no I/O.
15
+ */
16
+ /** The `<NAME>_PASSWORD` var that pairs with a `<NAME>_URL` var. */
17
+ export declare function databasePasswordVar(urlVar: string): string;
18
+ /**
19
+ * Inject `password` into a password-less connection URL. Refuses a URL that
20
+ * already carries one: two sources for the same credential is a
21
+ * misconfiguration, and silently preferring either would hide it.
22
+ *
23
+ * @example
24
+ * withDatabasePassword("postgresql://admin_role@10.0.0.3:5432/cms_admin", "s3cr3t")
25
+ * // → "postgresql://admin_role:s3cr3t@10.0.0.3:5432/cms_admin"
26
+ */
27
+ export declare function withDatabasePassword(url: string, password: string, label?: string): string;
28
+ /**
29
+ * The connection URL from the first of `urlVars` that is set, with its
30
+ * companion `_PASSWORD` var applied when that is set. `undefined` when none
31
+ * of `urlVars` is set, so callers keep their own "X is required" errors.
32
+ *
33
+ * @param urlVars URL var names in order of preference, e.g.
34
+ * `["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"]`.
35
+ * @param env The environment to read (defaults to `process.env`).
36
+ */
37
+ export declare function databaseUrlFromEnv(urlVars: readonly string[], env?: Readonly<Record<string, string | undefined>>): string | undefined;
38
+ //# sourceMappingURL=database-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-url.d.ts","sourceRoot":"","sources":["../src/database-url.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAiB,GACrB,MAAM,CAoBR;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,MAAM,GAAG,SAAS,CASpB"}
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * Database connection URLs as every Caelo process reads them from its
4
+ * environment.
5
+ *
6
+ * A URL var (`ADMIN_DATABASE_URL`, `PUBLIC_ADMIN_DATABASE_URL`,
7
+ * `PUBLIC_DATABASE_URL`) may carry its password inline — the shape the
8
+ * self-hosted compose stack, CI and `.env` use — or leave it out and have
9
+ * the password come from a companion `<NAME>_PASSWORD` var
10
+ * (`ADMIN_DATABASE_URL` → `ADMIN_DATABASE_PASSWORD`). Cloud installs use the
11
+ * second shape: the URL (host, role, database) is a plain env var, the
12
+ * password is a Secret Manager reference, so the password never shows up in
13
+ * the service's configuration or its revision history.
14
+ *
15
+ * Pure module — no I/O.
16
+ */
17
+ /** The `<NAME>_PASSWORD` var that pairs with a `<NAME>_URL` var. */
18
+ export function databasePasswordVar(urlVar) {
19
+ if (!urlVar.endsWith("_URL")) {
20
+ throw new Error(`database URL var must end in _URL, got ${urlVar}`);
21
+ }
22
+ return `${urlVar.slice(0, -"_URL".length)}_PASSWORD`;
23
+ }
24
+ /**
25
+ * Inject `password` into a password-less connection URL. Refuses a URL that
26
+ * already carries one: two sources for the same credential is a
27
+ * misconfiguration, and silently preferring either would hide it.
28
+ *
29
+ * @example
30
+ * withDatabasePassword("postgresql://admin_role@10.0.0.3:5432/cms_admin", "s3cr3t")
31
+ * // → "postgresql://admin_role:s3cr3t@10.0.0.3:5432/cms_admin"
32
+ */
33
+ export function withDatabasePassword(url, password, label = "database URL") {
34
+ let parsed;
35
+ try {
36
+ parsed = new URL(url);
37
+ }
38
+ catch {
39
+ throw new Error(`${label} is not a valid URL`);
40
+ }
41
+ if (parsed.password) {
42
+ throw new Error(`${label} already carries a password and a separate password is set too — keep one`);
43
+ }
44
+ if (!parsed.username) {
45
+ throw new Error(`${label} names no user to attach the password to`);
46
+ }
47
+ // Encode first: the URL setter leaves a literal `%` alone, so `p%40ss`
48
+ // would decode to `p@ss` and `p%ss` would be an invalid escape. Postgres
49
+ // clients decode the userinfo again.
50
+ parsed.password = encodeURIComponent(password);
51
+ return parsed.toString();
52
+ }
53
+ /**
54
+ * The connection URL from the first of `urlVars` that is set, with its
55
+ * companion `_PASSWORD` var applied when that is set. `undefined` when none
56
+ * of `urlVars` is set, so callers keep their own "X is required" errors.
57
+ *
58
+ * @param urlVars URL var names in order of preference, e.g.
59
+ * `["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"]`.
60
+ * @param env The environment to read (defaults to `process.env`).
61
+ */
62
+ export function databaseUrlFromEnv(urlVars, env = process.env) {
63
+ for (const urlVar of urlVars) {
64
+ const url = env[urlVar];
65
+ if (!url)
66
+ continue;
67
+ const passwordVar = databasePasswordVar(urlVar);
68
+ const password = env[passwordVar];
69
+ return password ? withDatabasePassword(url, password, `${urlVar} (with ${passwordVar})`) : url;
70
+ }
71
+ return undefined;
72
+ }
73
+ //# sourceMappingURL=database-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-url.js","sourceRoot":"","sources":["../src/database-url.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC;;;;;;;;;;;;;;GAcG;AAEH,oEAAoE;AACpE,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,0CAA0C,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;AACvD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,QAAgB,EAChB,KAAK,GAAG,cAAc;IAEtB,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,qBAAqB,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,2EAA2E,CACpF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,0CAA0C,CAAC,CAAC;IACtE,CAAC;IACD,uEAAuE;IACvE,yEAAyE;IACzE,qCAAqC;IACrC,MAAM,CAAC,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA0B,EAC1B,MAAoD,OAAO,CAAC,GAAG;IAE/D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,WAAW,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;QAClC,OAAO,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,MAAM,UAAU,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjG,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./content.js";
8
8
  export * from "./context.js";
9
9
  export * from "./css-gradient-scan.js";
10
10
  export * from "./css-var-scan.js";
11
+ export * from "./database-url.js";
11
12
  export * from "./design-draft-shell.js";
12
13
  export * from "./document-language.js";
13
14
  export * from "./font-assets.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,UAAU,EACV,aAAa,EACb,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,UAAU,EACV,aAAa,EACb,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from "./content.js";
9
9
  export * from "./context.js";
10
10
  export * from "./css-gradient-scan.js";
11
11
  export * from "./css-var-scan.js";
12
+ export * from "./database-url.js";
12
13
  export * from "./design-draft-shell.js";
13
14
  export * from "./document-language.js";
14
15
  export * from "./font-assets.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AAExC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAKL,UAAU,EACV,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AACrB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAqB,YAAY,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AAExC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAKL,UAAU,EACV,aAAa,EACb,MAAM,GAGP,MAAM,aAAa,CAAC;AACrB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAqB,YAAY,EAAE,MAAM,cAAc,CAAC"}
package/dist/seo.d.ts CHANGED
@@ -54,6 +54,46 @@ export declare const siteDefaultsSetSeoInputSchema: z.ZodObject<{
54
54
  }, z.core.$strict>>;
55
55
  }, z.core.$strict>;
56
56
  export type SiteDefaultsSetSeoInput = z.infer<typeof siteDefaultsSetSeoInputSchema>;
57
+ /**
58
+ * Input of `site_defaults.propose_set_seo` — the AI's path to the site SEO
59
+ * settings (CLAUDE.md §11.A). Every field is optional so the AI changes only
60
+ * what the operator asked for; an omitted field keeps its stored value when
61
+ * the Owner approves. `organizationJson` replaces the whole object.
62
+ */
63
+ export declare const siteSeoProposalInputSchema: z.ZodObject<{
64
+ siteBaseUrl: z.ZodOptional<z.ZodString>;
65
+ sitemapEnabled: z.ZodOptional<z.ZodBoolean>;
66
+ organizationJson: z.ZodOptional<z.ZodObject<{
67
+ name: z.ZodOptional<z.ZodString>;
68
+ url: z.ZodOptional<z.ZodString>;
69
+ logo: z.ZodOptional<z.ZodString>;
70
+ sameAs: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ }, z.core.$strict>>;
72
+ }, z.core.$strict>;
73
+ export type SiteSeoProposalInput = z.infer<typeof siteSeoProposalInputSchema>;
74
+ /** Result of {@link checkPublicSiteBaseUrl}. */
75
+ export type PublicSiteBaseUrlCheck = {
76
+ ok: true;
77
+ url: string;
78
+ } | {
79
+ ok: false;
80
+ message: string;
81
+ };
82
+ /**
83
+ * Validate a public site base URL and normalise it to its origin
84
+ * (`https://example.com`, no trailing slash).
85
+ *
86
+ * Every canonical, og:url, JSON-LD url, hreflang target and sitemap entry is
87
+ * `<base><path>`, so the base must be exactly the origin visitors reach: no
88
+ * path, query, fragment or credentials. On a cloud install (`provider` is a
89
+ * `CAELO_PROVIDER` other than self-hosted) it must be https and not a
90
+ * loopback host — a localhost base there ships unreachable canonicals to
91
+ * production. A self-hosted install may use `http://localhost:<port>`, the
92
+ * documented local-dev value; any other host still needs https.
93
+ *
94
+ * @param provider - `CAELO_PROVIDER` of the install; `undefined`/`""`/`"self-hosted"` = self-hosted.
95
+ */
96
+ export declare function checkPublicSiteBaseUrl(raw: string, provider: string | undefined): PublicSiteBaseUrlCheck;
57
97
  export interface PageSeoRow {
58
98
  pageId: string;
59
99
  metaDescription: string;
@@ -81,12 +121,26 @@ export interface SiteSeoSettings {
81
121
  };
82
122
  }
83
123
  /**
84
- * Resolve the canonical URL for a page. If `pages_seo.canonical_url`
85
- * is set it wins; otherwise `<siteBaseUrl><pagePath>` — where
86
- * `pagePath` is the COMPOSED public path from `pages.current_path`
87
- * (#390: the URL composition point materializes prefixes, slug
88
- * formats, and the home designation into that one column; canonical
89
- * simply follows it).
124
+ * How a deploy target serves pages (`deploy_targets.page_url_style`):
125
+ * 'directory' emits `<path>/index.html` and serves `/<path>/`;
126
+ * 'no-extension' emits a bare `<path>` file and serves `/<path>`.
127
+ */
128
+ export declare const PAGE_URL_STYLES: readonly ["directory", "no-extension"];
129
+ export type PageUrlStyle = (typeof PAGE_URL_STYLES)[number];
130
+ /**
131
+ * THE public page URL builder (#590). Every absolute page URL Caelo
132
+ * emits — canonical, og:url, JSON-LD url, sitemap `<loc>`,
133
+ * hreflang/x-default targets, language-switcher links — comes from
134
+ * here, so they are byte-identical by construction. Two builders
135
+ * drifting apart is what shipped `/en/about` as hreflang next to an
136
+ * `/en/about/` canonical.
137
+ *
138
+ * `pages_seo.canonical_url` (`override`) wins when set. Otherwise the
139
+ * URL is `<scheme>//<host><path>`: the path is the COMPOSED public path
140
+ * from `pages.current_path` (#390), its trailing slash follows the
141
+ * serving target's {@link PageUrlStyle}, the host is the URL composition
142
+ * point's `host` slot (a host-strategy locale) or else the site base
143
+ * URL's, and the scheme always comes from the base URL.
90
144
  */
91
145
  export declare function resolveCanonicalUrl(args: {
92
146
  siteBaseUrl: string;
@@ -94,12 +148,12 @@ export declare function resolveCanonicalUrl(args: {
94
148
  * "/" for the site root. */
95
149
  pagePath: string;
96
150
  override: string | null;
97
- /**
98
- * v0.2.85 — page emission style. 'directory' (default) → URLs end
99
- * in `/…/`; 'no-extension' → no trailing slash, matching what the
100
- * bucket serves when pages are emitted as bare files.
101
- */
102
- pageUrlStyle?: "directory" | "no-extension";
151
+ /** The serving deploy target's page emission style. Required — a
152
+ * defaulted style is how the preview drifted from the build. */
153
+ pageUrlStyle: PageUrlStyle;
154
+ /** Host from the URL composition point's `host` slot (e.g.
155
+ * `de.example.com`); null/absent → the site base URL's host. */
156
+ host?: string | null;
103
157
  }): string;
104
158
  export interface SeoMetaInput {
105
159
  title: string;
package/dist/seo.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../src/seo.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,iBAAiB,gFAQpB,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;kBAUnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,sBAAsB;;;;kBAMxB,CAAC;AACZ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;kBAQxB,CAAC;AACZ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,6BAA6B;;;;;;;;;kBAkB/B,CAAC;AACZ,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB;;4EAEwE;IACxE,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB;iCAC6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,WAAW,GAAG,cAAc,CAAC;CAC7C,GAAG,MAAM,CAOT;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAuDzD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CASzE"}
1
+ {"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../src/seo.ts"],"names":[],"mappings":"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"}
package/dist/seo.js CHANGED
@@ -48,6 +48,15 @@ export const seoOptimizeInputSchema = z
48
48
  context: z.string().max(4000).optional(),
49
49
  })
50
50
  .strict();
51
+ /** Organization JSON-LD fields — structured only, never raw markup (CLAUDE.md §2). */
52
+ const organizationJsonSchema = z
53
+ .object({
54
+ name: z.string().max(256).optional(),
55
+ url: z.string().max(2048).optional(),
56
+ logo: z.string().max(2048).optional(),
57
+ sameAs: z.array(z.string().max(2048)).max(20).optional(),
58
+ })
59
+ .strict();
51
60
  export const siteDefaultsSetSeoInputSchema = z
52
61
  .object({
53
62
  siteBaseUrl: z
@@ -56,34 +65,117 @@ export const siteDefaultsSetSeoInputSchema = z
56
65
  .max(2048)
57
66
  .url("siteBaseUrl must be an absolute URL (https://example.com)"),
58
67
  sitemapEnabled: z.boolean(),
59
- organizationJson: z
60
- .object({
61
- name: z.string().max(256).optional(),
62
- url: z.string().max(2048).optional(),
63
- logo: z.string().max(2048).optional(),
64
- sameAs: z.array(z.string().max(2048)).max(20).optional(),
65
- })
66
- .strict()
67
- .default({}),
68
+ organizationJson: organizationJsonSchema.default({}),
68
69
  })
69
70
  .strict();
70
71
  /**
71
- * Resolve the canonical URL for a page. If `pages_seo.canonical_url`
72
- * is set it wins; otherwise `<siteBaseUrl><pagePath>` — where
73
- * `pagePath` is the COMPOSED public path from `pages.current_path`
74
- * (#390: the URL composition point materializes prefixes, slug
75
- * formats, and the home designation into that one column; canonical
76
- * simply follows it).
72
+ * Input of `site_defaults.propose_set_seo` — the AI's path to the site SEO
73
+ * settings (CLAUDE.md §11.A). Every field is optional so the AI changes only
74
+ * what the operator asked for; an omitted field keeps its stored value when
75
+ * the Owner approves. `organizationJson` replaces the whole object.
76
+ */
77
+ export const siteSeoProposalInputSchema = z
78
+ .object({
79
+ siteBaseUrl: z.string().min(1).max(2048).optional(),
80
+ sitemapEnabled: z.boolean().optional(),
81
+ organizationJson: organizationJsonSchema.optional(),
82
+ })
83
+ .strict()
84
+ .refine((v) => v.siteBaseUrl !== undefined ||
85
+ v.sitemapEnabled !== undefined ||
86
+ v.organizationJson !== undefined, "pass at least one of `siteBaseUrl`, `sitemapEnabled`, `organizationJson`");
87
+ const LOOPBACK_HOST = /^(localhost|.+\.localhost|127(\.\d{1,3}){3}|\[::1\])$/i;
88
+ /** Wildcard bind addresses: a server listens on them, no browser can visit them. */
89
+ const WILDCARD_HOST = /^(0\.0\.0\.0|\[::\])$/;
90
+ /**
91
+ * Validate a public site base URL and normalise it to its origin
92
+ * (`https://example.com`, no trailing slash).
93
+ *
94
+ * Every canonical, og:url, JSON-LD url, hreflang target and sitemap entry is
95
+ * `<base><path>`, so the base must be exactly the origin visitors reach: no
96
+ * path, query, fragment or credentials. On a cloud install (`provider` is a
97
+ * `CAELO_PROVIDER` other than self-hosted) it must be https and not a
98
+ * loopback host — a localhost base there ships unreachable canonicals to
99
+ * production. A self-hosted install may use `http://localhost:<port>`, the
100
+ * documented local-dev value; any other host still needs https.
101
+ *
102
+ * @param provider - `CAELO_PROVIDER` of the install; `undefined`/`""`/`"self-hosted"` = self-hosted.
103
+ */
104
+ export function checkPublicSiteBaseUrl(raw, provider) {
105
+ let u;
106
+ try {
107
+ u = new URL(raw.trim());
108
+ }
109
+ catch {
110
+ return {
111
+ ok: false,
112
+ message: `"${raw}" is not an absolute URL — pass the public origin, e.g. https://www.example.com`,
113
+ };
114
+ }
115
+ const origin = `${u.protocol}//${u.host}`;
116
+ if (u.username || u.password) {
117
+ return { ok: false, message: "the site URL must not contain credentials" };
118
+ }
119
+ if ((u.pathname !== "/" && u.pathname !== "") || u.search || u.hash) {
120
+ return {
121
+ ok: false,
122
+ message: `the site URL must be the origin only, without path, query or fragment — use ${origin}`,
123
+ };
124
+ }
125
+ if (WILDCARD_HOST.test(u.hostname)) {
126
+ return {
127
+ ok: false,
128
+ message: `${u.hostname} is a bind address, not an address visitors can open — use the public domain (e.g. https://www.example.com) or, on a local self-hosted install, http://localhost:<port>`,
129
+ };
130
+ }
131
+ const selfHosted = !provider || provider === "self-hosted";
132
+ const loopback = LOOPBACK_HOST.test(u.hostname);
133
+ if (loopback && !selfHosted) {
134
+ return {
135
+ ok: false,
136
+ message: `${origin} is a local address; on this ${provider} install the site URL must be the public domain visitors use (e.g. https://www.example.com)`,
137
+ };
138
+ }
139
+ if (u.protocol !== "https:" && !(selfHosted && loopback && u.protocol === "http:")) {
140
+ return {
141
+ ok: false,
142
+ message: `the site URL must use https (got ${u.protocol.replace(":", "")}) — use https://${u.host}`,
143
+ };
144
+ }
145
+ return { ok: true, url: origin };
146
+ }
147
+ /**
148
+ * How a deploy target serves pages (`deploy_targets.page_url_style`):
149
+ * 'directory' emits `<path>/index.html` and serves `/<path>/`;
150
+ * 'no-extension' emits a bare `<path>` file and serves `/<path>`.
151
+ */
152
+ export const PAGE_URL_STYLES = ["directory", "no-extension"];
153
+ /**
154
+ * THE public page URL builder (#590). Every absolute page URL Caelo
155
+ * emits — canonical, og:url, JSON-LD url, sitemap `<loc>`,
156
+ * hreflang/x-default targets, language-switcher links — comes from
157
+ * here, so they are byte-identical by construction. Two builders
158
+ * drifting apart is what shipped `/en/about` as hreflang next to an
159
+ * `/en/about/` canonical.
160
+ *
161
+ * `pages_seo.canonical_url` (`override`) wins when set. Otherwise the
162
+ * URL is `<scheme>//<host><path>`: the path is the COMPOSED public path
163
+ * from `pages.current_path` (#390), its trailing slash follows the
164
+ * serving target's {@link PageUrlStyle}, the host is the URL composition
165
+ * point's `host` slot (a host-strategy locale) or else the site base
166
+ * URL's, and the scheme always comes from the base URL.
77
167
  */
78
168
  export function resolveCanonicalUrl(args) {
79
169
  if (args.override && args.override.length > 0)
80
170
  return args.override;
81
- const base = args.siteBaseUrl.endsWith("/") ? args.siteBaseUrl.slice(0, -1) : args.siteBaseUrl;
171
+ const siteBase = args.siteBaseUrl.endsWith("/")
172
+ ? args.siteBaseUrl.slice(0, -1)
173
+ : args.siteBaseUrl;
174
+ const base = args.host ? `${new URL(siteBase).protocol}//${args.host}` : siteBase;
82
175
  const trimmed = trimSlashes(args.pagePath);
83
176
  if (trimmed.length === 0)
84
177
  return `${base}/`;
85
- const style = args.pageUrlStyle ?? "directory";
86
- return style === "no-extension" ? `${base}/${trimmed}` : `${base}/${trimmed}/`;
178
+ return args.pageUrlStyle === "no-extension" ? `${base}/${trimmed}` : `${base}/${trimmed}/`;
87
179
  }
88
180
  /**
89
181
  * Render the <head> meta block for a page. Returns the HTML string
package/dist/seo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"seo.js","sourceRoot":"","sources":["../src/seo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"seo.js","sourceRoot":"","sources":["../src/seo.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * Format: SemVer 2.0.0. Pre-1.0 minor bumps are breaking; post-1.0
20
20
  * follow standard SemVer.
21
21
  */
22
- export declare const CAELO_VERSION = "0.10.29";
22
+ export declare const CAELO_VERSION = "0.10.30";
23
23
  /**
24
24
  * Deprecated alias for back-compat — early P17 work spelled this
25
25
  * `CALEO_VERSION` (typo of "Caelo"). New code should import
@@ -31,7 +31,7 @@ export declare const CAELO_VERSION = "0.10.29";
31
31
  *
32
32
  * @deprecated use CAELO_VERSION
33
33
  */
34
- export declare const CALEO_VERSION = "0.10.29";
34
+ export declare const CALEO_VERSION = "0.10.30";
35
35
  /**
36
36
  * Parsed shape — major/minor/patch + optional pre-release tag.
37
37
  * Stable interface for callers that need to feature-gate (rare —
package/dist/version.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * Format: SemVer 2.0.0. Pre-1.0 minor bumps are breaking; post-1.0
21
21
  * follow standard SemVer.
22
22
  */
23
- export const CAELO_VERSION = "0.10.29";
23
+ export const CAELO_VERSION = "0.10.30";
24
24
  /**
25
25
  * Deprecated alias for back-compat — early P17 work spelled this
26
26
  * `CALEO_VERSION` (typo of "Caelo"). New code should import
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caelo-cms/shared",
3
- "version": "0.10.29",
3
+ "version": "0.10.30",
4
4
  "private": false,
5
5
  "license": "MPL-2.0",
6
6
  "description": "Shared Zod schemas + types + the two-pass HTML composer used by every Caelo CMS workspace package. Stable enough for plugin authors and embedders to depend on directly.",
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { databasePasswordVar, databaseUrlFromEnv, withDatabasePassword } from "./database-url.js";
5
+
6
+ describe("databasePasswordVar", () => {
7
+ it("pairs each URL var with its _PASSWORD var", () => {
8
+ expect(databasePasswordVar("ADMIN_DATABASE_URL")).toBe("ADMIN_DATABASE_PASSWORD");
9
+ expect(databasePasswordVar("PUBLIC_ADMIN_DATABASE_URL")).toBe("PUBLIC_ADMIN_DATABASE_PASSWORD");
10
+ expect(databasePasswordVar("PUBLIC_DATABASE_URL")).toBe("PUBLIC_DATABASE_PASSWORD");
11
+ });
12
+
13
+ it("rejects a name that is not a URL var", () => {
14
+ expect(() => databasePasswordVar("ADMIN_DATABASE")).toThrow(/_URL/);
15
+ });
16
+ });
17
+
18
+ describe("withDatabasePassword", () => {
19
+ it("injects the password into a password-less URL, keeping host, db and params", () => {
20
+ expect(
21
+ withDatabasePassword(
22
+ "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
23
+ "0123abcd",
24
+ ),
25
+ ).toBe("postgresql://admin_role:0123abcd@10.20.0.3:5432/cms_admin?sslmode=require");
26
+ });
27
+
28
+ it("keeps literal percent signs in the password (p%40ss stays p%40ss, p%ss stays valid)", () => {
29
+ for (const password of ["p%40ss", "p%ss", "100%"]) {
30
+ const parsed = new URL(withDatabasePassword("postgres://u@h:5432/d", password));
31
+ expect(decodeURIComponent(parsed.password)).toBe(password);
32
+ }
33
+ });
34
+
35
+ it("percent-encodes reserved characters so the URL stays parseable", () => {
36
+ const url = withDatabasePassword("postgres://u@h:5432/d", "p@ss/w:rd#?");
37
+ const parsed = new URL(url);
38
+ expect(decodeURIComponent(parsed.password)).toBe("p@ss/w:rd#?");
39
+ expect(parsed.host).toBe("h:5432");
40
+ expect(parsed.pathname).toBe("/d");
41
+ });
42
+
43
+ it("refuses a URL that already carries a password", () => {
44
+ expect(() => withDatabasePassword("postgres://u:old@h/d", "new")).toThrow(/already carries/);
45
+ });
46
+
47
+ it("refuses a URL without a user and a malformed URL", () => {
48
+ expect(() => withDatabasePassword("postgres://h/d", "p")).toThrow(/no user/);
49
+ expect(() => withDatabasePassword("not a url", "p")).toThrow(/not a valid URL/);
50
+ });
51
+
52
+ it("never echoes the password in its errors", () => {
53
+ try {
54
+ withDatabasePassword("postgres://u:old@h/d", "top-secret-value");
55
+ } catch (e) {
56
+ expect(String(e)).not.toContain("top-secret-value");
57
+ expect(String(e)).not.toContain("old");
58
+ }
59
+ });
60
+ });
61
+
62
+ describe("databaseUrlFromEnv", () => {
63
+ it("returns an inline-password URL unchanged (self-hosted compose, CI, .env)", () => {
64
+ const env = { ADMIN_DATABASE_URL: "postgres://admin_role:dev@localhost:5432/cms_admin" };
65
+ expect(databaseUrlFromEnv(["ADMIN_DATABASE_URL"], env)).toBe(env.ADMIN_DATABASE_URL);
66
+ });
67
+
68
+ it("composes the URL from the plain URL var + its secret-mounted password var (cloud)", () => {
69
+ const env = {
70
+ ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
71
+ ADMIN_DATABASE_PASSWORD: "s3cret",
72
+ };
73
+ expect(databaseUrlFromEnv(["ADMIN_DATABASE_URL"], env)).toBe(
74
+ "postgresql://admin_role:s3cret@10.20.0.3:5432/cms_admin?sslmode=require",
75
+ );
76
+ });
77
+
78
+ it("uses the password var that belongs to the URL var it picked", () => {
79
+ const env = {
80
+ PUBLIC_DATABASE_URL: "postgresql://public_role@h:5432/cms_public",
81
+ PUBLIC_DATABASE_PASSWORD: "pub",
82
+ // Not consulted: it pairs with PUBLIC_ADMIN_DATABASE_URL, which is unset.
83
+ PUBLIC_ADMIN_DATABASE_PASSWORD: "adm",
84
+ };
85
+ expect(databaseUrlFromEnv(["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"], env)).toBe(
86
+ "postgresql://public_role:pub@h:5432/cms_public",
87
+ );
88
+ });
89
+
90
+ it("prefers the first URL var that is set", () => {
91
+ const env = {
92
+ PUBLIC_ADMIN_DATABASE_URL: "postgresql://admin_role@h:5432/cms_public",
93
+ PUBLIC_ADMIN_DATABASE_PASSWORD: "adm",
94
+ PUBLIC_DATABASE_URL: "postgresql://public_role@h:5432/cms_public",
95
+ };
96
+ expect(databaseUrlFromEnv(["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"], env)).toBe(
97
+ "postgresql://admin_role:adm@h:5432/cms_public",
98
+ );
99
+ });
100
+
101
+ it("is undefined when no URL var is set", () => {
102
+ expect(databaseUrlFromEnv(["ADMIN_DATABASE_URL"], { ADMIN_DATABASE_PASSWORD: "x" })).toBe(
103
+ undefined,
104
+ );
105
+ });
106
+
107
+ it("fails loudly when both the URL and the password var carry a password", () => {
108
+ const env = {
109
+ ADMIN_DATABASE_URL: "postgres://admin_role:inline@h/cms_admin",
110
+ ADMIN_DATABASE_PASSWORD: "mounted",
111
+ };
112
+ expect(() => databaseUrlFromEnv(["ADMIN_DATABASE_URL"], env)).toThrow(
113
+ /ADMIN_DATABASE_URL \(with ADMIN_DATABASE_PASSWORD\) already carries a password/,
114
+ );
115
+ });
116
+ });
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Database connection URLs as every Caelo process reads them from its
5
+ * environment.
6
+ *
7
+ * A URL var (`ADMIN_DATABASE_URL`, `PUBLIC_ADMIN_DATABASE_URL`,
8
+ * `PUBLIC_DATABASE_URL`) may carry its password inline — the shape the
9
+ * self-hosted compose stack, CI and `.env` use — or leave it out and have
10
+ * the password come from a companion `<NAME>_PASSWORD` var
11
+ * (`ADMIN_DATABASE_URL` → `ADMIN_DATABASE_PASSWORD`). Cloud installs use the
12
+ * second shape: the URL (host, role, database) is a plain env var, the
13
+ * password is a Secret Manager reference, so the password never shows up in
14
+ * the service's configuration or its revision history.
15
+ *
16
+ * Pure module — no I/O.
17
+ */
18
+
19
+ /** The `<NAME>_PASSWORD` var that pairs with a `<NAME>_URL` var. */
20
+ export function databasePasswordVar(urlVar: string): string {
21
+ if (!urlVar.endsWith("_URL")) {
22
+ throw new Error(`database URL var must end in _URL, got ${urlVar}`);
23
+ }
24
+ return `${urlVar.slice(0, -"_URL".length)}_PASSWORD`;
25
+ }
26
+
27
+ /**
28
+ * Inject `password` into a password-less connection URL. Refuses a URL that
29
+ * already carries one: two sources for the same credential is a
30
+ * misconfiguration, and silently preferring either would hide it.
31
+ *
32
+ * @example
33
+ * withDatabasePassword("postgresql://admin_role@10.0.0.3:5432/cms_admin", "s3cr3t")
34
+ * // → "postgresql://admin_role:s3cr3t@10.0.0.3:5432/cms_admin"
35
+ */
36
+ export function withDatabasePassword(
37
+ url: string,
38
+ password: string,
39
+ label = "database URL",
40
+ ): string {
41
+ let parsed: URL;
42
+ try {
43
+ parsed = new URL(url);
44
+ } catch {
45
+ throw new Error(`${label} is not a valid URL`);
46
+ }
47
+ if (parsed.password) {
48
+ throw new Error(
49
+ `${label} already carries a password and a separate password is set too — keep one`,
50
+ );
51
+ }
52
+ if (!parsed.username) {
53
+ throw new Error(`${label} names no user to attach the password to`);
54
+ }
55
+ // Encode first: the URL setter leaves a literal `%` alone, so `p%40ss`
56
+ // would decode to `p@ss` and `p%ss` would be an invalid escape. Postgres
57
+ // clients decode the userinfo again.
58
+ parsed.password = encodeURIComponent(password);
59
+ return parsed.toString();
60
+ }
61
+
62
+ /**
63
+ * The connection URL from the first of `urlVars` that is set, with its
64
+ * companion `_PASSWORD` var applied when that is set. `undefined` when none
65
+ * of `urlVars` is set, so callers keep their own "X is required" errors.
66
+ *
67
+ * @param urlVars URL var names in order of preference, e.g.
68
+ * `["PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_DATABASE_URL"]`.
69
+ * @param env The environment to read (defaults to `process.env`).
70
+ */
71
+ export function databaseUrlFromEnv(
72
+ urlVars: readonly string[],
73
+ env: Readonly<Record<string, string | undefined>> = process.env,
74
+ ): string | undefined {
75
+ for (const urlVar of urlVars) {
76
+ const url = env[urlVar];
77
+ if (!url) continue;
78
+ const passwordVar = databasePasswordVar(urlVar);
79
+ const password = env[passwordVar];
80
+ return password ? withDatabasePassword(url, password, `${urlVar} (with ${passwordVar})`) : url;
81
+ }
82
+ return undefined;
83
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./content.js";
11
11
  export * from "./context.js";
12
12
  export * from "./css-gradient-scan.js";
13
13
  export * from "./css-var-scan.js";
14
+ export * from "./database-url.js";
14
15
  export * from "./design-draft-shell.js";
15
16
  export * from "./document-language.js";
16
17
  export * from "./font-assets.js";
package/src/seo.test.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describe, expect, it } from "bun:test";
4
4
  import {
5
+ checkPublicSiteBaseUrl,
5
6
  injectSeoIntoHead,
6
7
  renderSeoHead,
7
8
  resolveCanonicalUrl,
@@ -9,6 +10,7 @@ import {
9
10
  seoOptimizeInputSchema,
10
11
  seoSetInputSchema,
11
12
  siteDefaultsSetSeoInputSchema,
13
+ siteSeoProposalInputSchema,
12
14
  } from "./seo.js";
13
15
 
14
16
  describe("resolveCanonicalUrl", () => {
@@ -18,6 +20,8 @@ describe("resolveCanonicalUrl", () => {
18
20
  siteBaseUrl: "https://example.com",
19
21
  pagePath: "/anything",
20
22
  override: "https://canonical.example.com/x",
23
+ pageUrlStyle: "directory",
24
+ host: "de.example.com",
21
25
  }),
22
26
  ).toBe("https://canonical.example.com/x");
23
27
  });
@@ -28,6 +32,7 @@ describe("resolveCanonicalUrl", () => {
28
32
  siteBaseUrl: "https://example.com",
29
33
  pagePath: "/",
30
34
  override: null,
35
+ pageUrlStyle: "directory",
31
36
  }),
32
37
  ).toBe("https://example.com/");
33
38
  });
@@ -38,10 +43,42 @@ describe("resolveCanonicalUrl", () => {
38
43
  siteBaseUrl: "https://example.com/",
39
44
  pagePath: "/about",
40
45
  override: null,
46
+ pageUrlStyle: "directory",
41
47
  }),
42
48
  ).toBe("https://example.com/about/");
43
49
  });
44
50
 
51
+ describe("#590 — composed host (host-strategy locales)", () => {
52
+ for (const [style, expected] of [
53
+ ["directory", "https://de.example.com/preise/"],
54
+ ["no-extension", "https://de.example.com/preise"],
55
+ ] as const) {
56
+ it(`swaps the host, keeps the base scheme and the ${style} slash rule`, () => {
57
+ expect(
58
+ resolveCanonicalUrl({
59
+ siteBaseUrl: "https://example.com/",
60
+ pagePath: "/preise",
61
+ override: null,
62
+ pageUrlStyle: style,
63
+ host: "de.example.com",
64
+ }),
65
+ ).toBe(expected);
66
+ });
67
+ }
68
+
69
+ it("a null host keeps the site base host", () => {
70
+ expect(
71
+ resolveCanonicalUrl({
72
+ siteBaseUrl: "https://example.com",
73
+ pagePath: "/",
74
+ override: null,
75
+ pageUrlStyle: "no-extension",
76
+ host: null,
77
+ }),
78
+ ).toBe("https://example.com/");
79
+ });
80
+ });
81
+
45
82
  describe("v0.2.85 — pageUrlStyle='no-extension'", () => {
46
83
  it("omits the trailing slash for non-home pages", () => {
47
84
  expect(
@@ -65,12 +102,13 @@ describe("resolveCanonicalUrl", () => {
65
102
  ).toBe("https://example.com/");
66
103
  });
67
104
 
68
- it("default style preserves pre-v0.2.85 trailing-slash behavior", () => {
105
+ it("'directory' keeps the trailing slash for non-home pages", () => {
69
106
  expect(
70
107
  resolveCanonicalUrl({
71
108
  siteBaseUrl: "https://example.com",
72
109
  pagePath: "/about",
73
110
  override: null,
111
+ pageUrlStyle: "directory",
74
112
  }),
75
113
  ).toBe("https://example.com/about/");
76
114
  });
@@ -200,3 +238,101 @@ describe("schemas", () => {
200
238
  expect(r.success).toBe(false);
201
239
  });
202
240
  });
241
+
242
+ describe("siteSeoProposalInputSchema", () => {
243
+ it("accepts any single field", () => {
244
+ expect(siteSeoProposalInputSchema.safeParse({ siteBaseUrl: "https://a.example" }).success).toBe(
245
+ true,
246
+ );
247
+ expect(siteSeoProposalInputSchema.safeParse({ sitemapEnabled: false }).success).toBe(true);
248
+ expect(
249
+ siteSeoProposalInputSchema.safeParse({ organizationJson: { name: "Acme" } }).success,
250
+ ).toBe(true);
251
+ });
252
+
253
+ it("rejects an empty proposal with a message naming the fields", () => {
254
+ const r = siteSeoProposalInputSchema.safeParse({});
255
+ expect(r.success).toBe(false);
256
+ expect(JSON.stringify(r.error?.issues)).toContain("siteBaseUrl");
257
+ });
258
+
259
+ it("rejects unknown keys at the top level and inside organizationJson", () => {
260
+ expect(siteSeoProposalInputSchema.safeParse({ sitemapEnabled: true, x: 1 }).success).toBe(
261
+ false,
262
+ );
263
+ expect(
264
+ siteSeoProposalInputSchema.safeParse({ organizationJson: { name: "A", script: "<x>" } })
265
+ .success,
266
+ ).toBe(false);
267
+ });
268
+ });
269
+
270
+ describe("checkPublicSiteBaseUrl", () => {
271
+ it("normalises a valid https URL to its origin", () => {
272
+ expect(checkPublicSiteBaseUrl("https://www.Example.com/", "gcp")).toEqual({
273
+ ok: true,
274
+ url: "https://www.example.com",
275
+ });
276
+ expect(checkPublicSiteBaseUrl(" https://example.com:8443 ", undefined)).toEqual({
277
+ ok: true,
278
+ url: "https://example.com:8443",
279
+ });
280
+ });
281
+
282
+ it("rejects a path, query or fragment and names the origin to use instead", () => {
283
+ for (const raw of [
284
+ "https://example.com/blog",
285
+ "https://example.com/?a=1",
286
+ "https://example.com/#top",
287
+ ]) {
288
+ const r = checkPublicSiteBaseUrl(raw, "aws");
289
+ expect(r.ok).toBe(false);
290
+ if (!r.ok) expect(r.message).toContain("use https://example.com");
291
+ }
292
+ });
293
+
294
+ it("rejects non-URLs and credentials", () => {
295
+ expect(checkPublicSiteBaseUrl("example.com", "gcp").ok).toBe(false);
296
+ expect(checkPublicSiteBaseUrl("https://u:p@example.com", "gcp").ok).toBe(false);
297
+ });
298
+
299
+ it("requires https for public hosts on every provider", () => {
300
+ expect(checkPublicSiteBaseUrl("http://example.com", "gcp").ok).toBe(false);
301
+ expect(checkPublicSiteBaseUrl("http://example.com", "self-hosted").ok).toBe(false);
302
+ expect(checkPublicSiteBaseUrl("ftp://example.com", undefined).ok).toBe(false);
303
+ });
304
+
305
+ it("rejects loopback hosts on cloud providers", () => {
306
+ for (const provider of ["gcp", "gcp-firebase", "aws", "azure"]) {
307
+ for (const raw of [
308
+ "https://localhost",
309
+ "http://localhost:8082",
310
+ "https://127.0.0.1",
311
+ "https://[::1]",
312
+ "https://app.localhost",
313
+ ]) {
314
+ const r = checkPublicSiteBaseUrl(raw, provider);
315
+ expect(r.ok).toBe(false);
316
+ if (!r.ok) expect(r.message).toContain("public domain");
317
+ }
318
+ }
319
+ });
320
+
321
+ it("allows http://localhost on a self-hosted install (local dev)", () => {
322
+ expect(checkPublicSiteBaseUrl("http://localhost:8082", undefined)).toEqual({
323
+ ok: true,
324
+ url: "http://localhost:8082",
325
+ });
326
+ expect(checkPublicSiteBaseUrl("http://127.0.0.1:8082", "self-hosted").ok).toBe(true);
327
+ });
328
+
329
+ it("rejects wildcard bind addresses on every provider, self-hosted included", () => {
330
+ for (const provider of [undefined, "self-hosted", "gcp"]) {
331
+ for (const raw of ["http://0.0.0.0:8082", "https://0.0.0.0", "http://[::]:8082"]) {
332
+ const r = checkPublicSiteBaseUrl(raw, provider);
333
+ expect(r.ok).toBe(false);
334
+ if (!r.ok) expect(r.message).toContain("bind address");
335
+ }
336
+ }
337
+ });
338
+ });
package/src/seo.ts CHANGED
@@ -61,6 +61,16 @@ export const seoOptimizeInputSchema = z
61
61
  .strict();
62
62
  export type SeoOptimizeInput = z.infer<typeof seoOptimizeInputSchema>;
63
63
 
64
+ /** Organization JSON-LD fields — structured only, never raw markup (CLAUDE.md §2). */
65
+ const organizationJsonSchema = z
66
+ .object({
67
+ name: z.string().max(256).optional(),
68
+ url: z.string().max(2048).optional(),
69
+ logo: z.string().max(2048).optional(),
70
+ sameAs: z.array(z.string().max(2048)).max(20).optional(),
71
+ })
72
+ .strict();
73
+
64
74
  export const siteDefaultsSetSeoInputSchema = z
65
75
  .object({
66
76
  siteBaseUrl: z
@@ -69,19 +79,101 @@ export const siteDefaultsSetSeoInputSchema = z
69
79
  .max(2048)
70
80
  .url("siteBaseUrl must be an absolute URL (https://example.com)"),
71
81
  sitemapEnabled: z.boolean(),
72
- organizationJson: z
73
- .object({
74
- name: z.string().max(256).optional(),
75
- url: z.string().max(2048).optional(),
76
- logo: z.string().max(2048).optional(),
77
- sameAs: z.array(z.string().max(2048)).max(20).optional(),
78
- })
79
- .strict()
80
- .default({}),
82
+ organizationJson: organizationJsonSchema.default({}),
81
83
  })
82
84
  .strict();
83
85
  export type SiteDefaultsSetSeoInput = z.infer<typeof siteDefaultsSetSeoInputSchema>;
84
86
 
87
+ /**
88
+ * Input of `site_defaults.propose_set_seo` — the AI's path to the site SEO
89
+ * settings (CLAUDE.md §11.A). Every field is optional so the AI changes only
90
+ * what the operator asked for; an omitted field keeps its stored value when
91
+ * the Owner approves. `organizationJson` replaces the whole object.
92
+ */
93
+ export const siteSeoProposalInputSchema = z
94
+ .object({
95
+ siteBaseUrl: z.string().min(1).max(2048).optional(),
96
+ sitemapEnabled: z.boolean().optional(),
97
+ organizationJson: organizationJsonSchema.optional(),
98
+ })
99
+ .strict()
100
+ .refine(
101
+ (v) =>
102
+ v.siteBaseUrl !== undefined ||
103
+ v.sitemapEnabled !== undefined ||
104
+ v.organizationJson !== undefined,
105
+ "pass at least one of `siteBaseUrl`, `sitemapEnabled`, `organizationJson`",
106
+ );
107
+ export type SiteSeoProposalInput = z.infer<typeof siteSeoProposalInputSchema>;
108
+
109
+ /** Result of {@link checkPublicSiteBaseUrl}. */
110
+ export type PublicSiteBaseUrlCheck = { ok: true; url: string } | { ok: false; message: string };
111
+
112
+ const LOOPBACK_HOST = /^(localhost|.+\.localhost|127(\.\d{1,3}){3}|\[::1\])$/i;
113
+
114
+ /** Wildcard bind addresses: a server listens on them, no browser can visit them. */
115
+ const WILDCARD_HOST = /^(0\.0\.0\.0|\[::\])$/;
116
+
117
+ /**
118
+ * Validate a public site base URL and normalise it to its origin
119
+ * (`https://example.com`, no trailing slash).
120
+ *
121
+ * Every canonical, og:url, JSON-LD url, hreflang target and sitemap entry is
122
+ * `<base><path>`, so the base must be exactly the origin visitors reach: no
123
+ * path, query, fragment or credentials. On a cloud install (`provider` is a
124
+ * `CAELO_PROVIDER` other than self-hosted) it must be https and not a
125
+ * loopback host — a localhost base there ships unreachable canonicals to
126
+ * production. A self-hosted install may use `http://localhost:<port>`, the
127
+ * documented local-dev value; any other host still needs https.
128
+ *
129
+ * @param provider - `CAELO_PROVIDER` of the install; `undefined`/`""`/`"self-hosted"` = self-hosted.
130
+ */
131
+ export function checkPublicSiteBaseUrl(
132
+ raw: string,
133
+ provider: string | undefined,
134
+ ): PublicSiteBaseUrlCheck {
135
+ let u: URL;
136
+ try {
137
+ u = new URL(raw.trim());
138
+ } catch {
139
+ return {
140
+ ok: false,
141
+ message: `"${raw}" is not an absolute URL — pass the public origin, e.g. https://www.example.com`,
142
+ };
143
+ }
144
+ const origin = `${u.protocol}//${u.host}`;
145
+ if (u.username || u.password) {
146
+ return { ok: false, message: "the site URL must not contain credentials" };
147
+ }
148
+ if ((u.pathname !== "/" && u.pathname !== "") || u.search || u.hash) {
149
+ return {
150
+ ok: false,
151
+ message: `the site URL must be the origin only, without path, query or fragment — use ${origin}`,
152
+ };
153
+ }
154
+ if (WILDCARD_HOST.test(u.hostname)) {
155
+ return {
156
+ ok: false,
157
+ message: `${u.hostname} is a bind address, not an address visitors can open — use the public domain (e.g. https://www.example.com) or, on a local self-hosted install, http://localhost:<port>`,
158
+ };
159
+ }
160
+ const selfHosted = !provider || provider === "self-hosted";
161
+ const loopback = LOOPBACK_HOST.test(u.hostname);
162
+ if (loopback && !selfHosted) {
163
+ return {
164
+ ok: false,
165
+ message: `${origin} is a local address; on this ${provider} install the site URL must be the public domain visitors use (e.g. https://www.example.com)`,
166
+ };
167
+ }
168
+ if (u.protocol !== "https:" && !(selfHosted && loopback && u.protocol === "http:")) {
169
+ return {
170
+ ok: false,
171
+ message: `the site URL must use https (got ${u.protocol.replace(":", "")}) — use https://${u.host}`,
172
+ };
173
+ }
174
+ return { ok: true, url: origin };
175
+ }
176
+
85
177
  export interface PageSeoRow {
86
178
  pageId: string;
87
179
  metaDescription: string;
@@ -111,12 +203,27 @@ export interface SiteSeoSettings {
111
203
  }
112
204
 
113
205
  /**
114
- * Resolve the canonical URL for a page. If `pages_seo.canonical_url`
115
- * is set it wins; otherwise `<siteBaseUrl><pagePath>` — where
116
- * `pagePath` is the COMPOSED public path from `pages.current_path`
117
- * (#390: the URL composition point materializes prefixes, slug
118
- * formats, and the home designation into that one column; canonical
119
- * simply follows it).
206
+ * How a deploy target serves pages (`deploy_targets.page_url_style`):
207
+ * 'directory' emits `<path>/index.html` and serves `/<path>/`;
208
+ * 'no-extension' emits a bare `<path>` file and serves `/<path>`.
209
+ */
210
+ export const PAGE_URL_STYLES = ["directory", "no-extension"] as const;
211
+ export type PageUrlStyle = (typeof PAGE_URL_STYLES)[number];
212
+
213
+ /**
214
+ * THE public page URL builder (#590). Every absolute page URL Caelo
215
+ * emits — canonical, og:url, JSON-LD url, sitemap `<loc>`,
216
+ * hreflang/x-default targets, language-switcher links — comes from
217
+ * here, so they are byte-identical by construction. Two builders
218
+ * drifting apart is what shipped `/en/about` as hreflang next to an
219
+ * `/en/about/` canonical.
220
+ *
221
+ * `pages_seo.canonical_url` (`override`) wins when set. Otherwise the
222
+ * URL is `<scheme>//<host><path>`: the path is the COMPOSED public path
223
+ * from `pages.current_path` (#390), its trailing slash follows the
224
+ * serving target's {@link PageUrlStyle}, the host is the URL composition
225
+ * point's `host` slot (a host-strategy locale) or else the site base
226
+ * URL's, and the scheme always comes from the base URL.
120
227
  */
121
228
  export function resolveCanonicalUrl(args: {
122
229
  siteBaseUrl: string;
@@ -124,19 +231,21 @@ export function resolveCanonicalUrl(args: {
124
231
  * "/" for the site root. */
125
232
  pagePath: string;
126
233
  override: string | null;
127
- /**
128
- * v0.2.85 — page emission style. 'directory' (default) → URLs end
129
- * in `/…/`; 'no-extension' → no trailing slash, matching what the
130
- * bucket serves when pages are emitted as bare files.
131
- */
132
- pageUrlStyle?: "directory" | "no-extension";
234
+ /** The serving deploy target's page emission style. Required — a
235
+ * defaulted style is how the preview drifted from the build. */
236
+ pageUrlStyle: PageUrlStyle;
237
+ /** Host from the URL composition point's `host` slot (e.g.
238
+ * `de.example.com`); null/absent → the site base URL's host. */
239
+ host?: string | null;
133
240
  }): string {
134
241
  if (args.override && args.override.length > 0) return args.override;
135
- const base = args.siteBaseUrl.endsWith("/") ? args.siteBaseUrl.slice(0, -1) : args.siteBaseUrl;
242
+ const siteBase = args.siteBaseUrl.endsWith("/")
243
+ ? args.siteBaseUrl.slice(0, -1)
244
+ : args.siteBaseUrl;
245
+ const base = args.host ? `${new URL(siteBase).protocol}//${args.host}` : siteBase;
136
246
  const trimmed = trimSlashes(args.pagePath);
137
247
  if (trimmed.length === 0) return `${base}/`;
138
- const style = args.pageUrlStyle ?? "directory";
139
- return style === "no-extension" ? `${base}/${trimmed}` : `${base}/${trimmed}/`;
248
+ return args.pageUrlStyle === "no-extension" ? `${base}/${trimmed}` : `${base}/${trimmed}/`;
140
249
  }
141
250
 
142
251
  export interface SeoMetaInput {
package/src/version.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * follow standard SemVer.
23
23
  */
24
24
 
25
- export const CAELO_VERSION = "0.10.29";
25
+ export const CAELO_VERSION = "0.10.30";
26
26
 
27
27
  /**
28
28
  * Deprecated alias for back-compat — early P17 work spelled this