@homeflare/site 0.1.0

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/checkout.d.ts +26 -0
  4. package/dist/checkout.d.ts.map +1 -0
  5. package/dist/decode.d.ts +26 -0
  6. package/dist/decode.d.ts.map +1 -0
  7. package/dist/derive.d.ts +66 -0
  8. package/dist/derive.d.ts.map +1 -0
  9. package/dist/errors.d.ts +45 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/guards.d.ts +19 -0
  12. package/dist/guards.d.ts.map +1 -0
  13. package/dist/identity.d.ts +33 -0
  14. package/dist/identity.d.ts.map +1 -0
  15. package/dist/index-dvzn0279.js +288 -0
  16. package/dist/index-dvzn0279.js.map +17 -0
  17. package/dist/index.d.ts +25 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +238 -0
  20. package/dist/index.js.map +14 -0
  21. package/dist/inventory.d.ts +12 -0
  22. package/dist/inventory.d.ts.map +1 -0
  23. package/dist/load.d.ts +34 -0
  24. package/dist/load.d.ts.map +1 -0
  25. package/dist/load.js +190 -0
  26. package/dist/load.js.map +12 -0
  27. package/dist/net.d.ts +5 -0
  28. package/dist/net.d.ts.map +1 -0
  29. package/dist/overrides.d.ts +45 -0
  30. package/dist/overrides.d.ts.map +1 -0
  31. package/dist/pins.d.ts +15 -0
  32. package/dist/pins.d.ts.map +1 -0
  33. package/dist/primitives.d.ts +57 -0
  34. package/dist/primitives.d.ts.map +1 -0
  35. package/dist/references.d.ts +6 -0
  36. package/dist/references.d.ts.map +1 -0
  37. package/dist/schema.d.ts +100 -0
  38. package/dist/schema.d.ts.map +1 -0
  39. package/dist/tokens.d.ts +24 -0
  40. package/dist/tokens.d.ts.map +1 -0
  41. package/dist/version.d.ts +2 -0
  42. package/dist/version.d.ts.map +1 -0
  43. package/package.json +41 -0
  44. package/site.example.json +81 -0
  45. package/src/checkout.ts +109 -0
  46. package/src/decode.ts +99 -0
  47. package/src/derive.ts +188 -0
  48. package/src/errors.ts +60 -0
  49. package/src/guards.ts +42 -0
  50. package/src/identity.ts +93 -0
  51. package/src/index.ts +43 -0
  52. package/src/inventory.ts +55 -0
  53. package/src/load.ts +186 -0
  54. package/src/net.ts +29 -0
  55. package/src/overrides.ts +79 -0
  56. package/src/pins.ts +40 -0
  57. package/src/primitives.ts +139 -0
  58. package/src/references.ts +82 -0
  59. package/src/schema.ts +144 -0
  60. package/src/tokens.ts +60 -0
  61. package/src/version.ts +2 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/derive.ts", "../src/pins.ts", "../src/inventory.ts", "../src/identity.ts", "../src/tokens.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `derive(site)`: every hostname, address, zone suffix, Access URL, OIDC redirect and\n * vault mount name, built from base values. Pure — same site in, same names out.\n *\n * ⛔ DERIVE ONLY WHAT FOLLOWS A RULE. Physical names, certificate hostnames, adopted ids\n * and policy / principal lists are PINNED (see pins.ts). Anything added here must be\n * a pure function of base values, or the relativity test stops meaning anything.\n *\n * ⛔ UNKNOWN KEYS THROW. `productHost('typo')` refuses, naming what is declared. It never\n * falls back to `typo.<apex>`: a plausible default is how a typo becomes a DNS record.\n *\n * ★ The result is plain data plus lookup methods. `JSON.stringify(derive(site))` is the\n * whole rendering (methods drop out), which is what the leak and relativity tests read.\n */\nimport { SiteError, lookup, unknownKey } from './errors.ts';\nimport { addressOn } from './net.ts';\nimport { APEX_ZONE } from './references.ts';\nimport type { Site } from './schema.ts';\n\nexport interface DerivedHost {\n /** `<key>.<zone>`; absent when the host declares no zone. */\n readonly fqdn?: string;\n /** Network key → address on that network. */\n readonly addresses: Readonly<Record<string, string>>;\n}\n\nexport interface DerivedVault {\n /** `<vault-host>`: the browser / UI hostname. */\n readonly host: string;\n /** `<vault-api-host>`: the public API hostname. */\n readonly apiHost: string;\n /** `https://<vault-api-host>` — the public API. Humans; machines use Mesh or the LAN. */\n readonly publicAddr: string;\n /** `https://<mesh address>:<port>` — machines over the private Mesh path. */\n readonly meshAddr: string;\n /** The LAN pass-through proxy, as a `BAO_ADDR`. */\n readonly lanAddr: string;\n readonly namespace: string;\n /** Browser callback on `<vault-host>`, then the CLI's localhost listener. */\n readonly oidcRedirects: readonly string[];\n}\n\nexport interface DerivedAccess {\n readonly team: string;\n /**\n * `https://<team>.cloudflareaccess.com`. ★ Named for `verifyAccessJwt`'s `teamDomain`\n * option in `@homeflare/cloudflare`, so it passes straight through.\n */\n readonly teamDomain: string;\n /** The JWKS endpoint Access signs with. */\n readonly certsUrl: string;\n}\n\nexport interface Derived {\n readonly apex: string;\n /** Zone key → FQDN (`mgmt` → `mgmt.<apex>`). */\n readonly zones: Readonly<Record<string, string>>;\n /** `<mgmt-zone>`. */\n readonly mgmtZone: string;\n readonly vault: DerivedVault;\n readonly access: DerivedAccess;\n readonly hosts: Readonly<Record<string, DerivedHost>>;\n /** Product key → public hostname. */\n readonly products: Readonly<Record<string, string>>;\n /** Service key → URL. */\n readonly services: Readonly<Record<string, string>>;\n /** Cluster key → member FQDNs, in declared order. */\n readonly clusters: Readonly<Record<string, readonly string[]>>;\n /** Every `cloudflare-<alias>-<surface>` mount, in declared order. */\n readonly cloudflareMounts: readonly string[];\n\n /** A zone's FQDN. `\"apex\"` is the apex itself. */\n zone(key: string): string;\n /** A host's FQDN. Throws when the host is unknown or has no zone. */\n host(key: string): string;\n /** A host's address on a network. Throws when either is unknown or not a leg. */\n address(host: string, network: string): string;\n productHost(key: string): string;\n /** `https://<productHost>`. */\n productUrl(key: string): string;\n serviceUrl(key: string): string;\n clusterMembers(key: string): readonly string[];\n /** Throws when the alias is unknown or that account does not list the surface. */\n cloudflareMount(alias: string, surface: string): string;\n accountId(alias: string): string;\n}\n\nfunction zoneOf(site: Site, key: string): string {\n if (key === APEX_ZONE) return site.apex;\n return `${lookup('zone', site.zones, key)}.${site.apex}`;\n}\n\nfunction deriveHosts(site: Site): Record<string, DerivedHost> {\n const out: Record<string, DerivedHost> = {};\n for (const [key, host] of Object.entries(site.hosts)) {\n const addresses: Record<string, string> = {};\n for (const [network, number] of Object.entries(host.legs ?? {})) {\n addresses[network] = addressOn(lookup('network', site.networks, network), number);\n }\n out[key] = {\n ...(host.zone === undefined ? {} : { fqdn: `${key}.${zoneOf(site, host.zone)}` }),\n addresses,\n };\n }\n return out;\n}\n\nfunction mapRecord<V, R>(record: Readonly<Record<string, V>>, f: (value: V, key: string) => R) {\n const out: Record<string, R> = {};\n for (const [key, value] of Object.entries(record)) out[key] = f(value, key);\n return out;\n}\n\nexport function derive(site: Site): Derived {\n const hosts = deriveHosts(site);\n const hostFqdn = (key: string): string => {\n const fqdn = lookup('host', hosts, key).fqdn;\n if (fqdn === undefined) {\n throw new SiteError('unknown-key', `host \"${key}\" has no zone, so it has no hostname`);\n }\n return fqdn;\n };\n\n const vaultHost = `${site.vault.label}.${site.apex}`;\n const apiHost = `${site.vault.apiLabel}.${vaultHost}`;\n const { lan } = site.vault;\n const teamDomain = `https://${site.cloudflare.access.team}.cloudflareaccess.com`;\n\n const products = mapRecord(\n site.products,\n (p, key) => p.domain ?? `${p.label ?? key}.${site.apex}`,\n );\n const services = mapRecord(site.services, (s) => `${s.scheme}://${hostFqdn(s.host)}:${s.port}`);\n const clusters = mapRecord(site.clusters, (c) => c.members.map(hostFqdn));\n const accounts = site.cloudflare.accounts;\n const cloudflareMounts = Object.entries(accounts).flatMap(([alias, account]) =>\n account.surfaces.map((surface) => `cloudflare-${alias}-${surface}`),\n );\n\n return {\n apex: site.apex,\n zones: mapRecord(site.zones, (_label, key) => zoneOf(site, key)),\n mgmtZone: zoneOf(site, 'mgmt'),\n vault: {\n host: vaultHost,\n apiHost,\n publicAddr: `https://${apiHost}`,\n meshAddr: `https://${site.vault.meshAddress}:${site.vault.port}`,\n lanAddr: `${lan.scheme}://${hostFqdn(lan.host)}:${lan.port}`,\n namespace: site.vault.namespace,\n oidcRedirects: [\n `https://${vaultHost}/ui/vault/auth/${site.vault.oidcMount}/oidc/callback`,\n `http://localhost:${site.vault.cliCallbackPort}/oidc/callback`,\n ],\n },\n access: {\n team: site.cloudflare.access.team,\n teamDomain,\n certsUrl: `${teamDomain}/cdn-cgi/access/certs`,\n },\n hosts,\n products,\n services,\n clusters,\n cloudflareMounts,\n\n zone: (key) => zoneOf(site, key),\n host: hostFqdn,\n address: (host, network) => {\n const addresses = lookup('host', hosts, host).addresses;\n if (!Object.hasOwn(addresses, network))\n unknownKey(`network leg of host \"${host}\"`, network, Object.keys(addresses));\n return addresses[network] as string;\n },\n productHost: (key) => lookup('product', products, key),\n productUrl: (key) => `https://${lookup('product', products, key)}`,\n serviceUrl: (key) => lookup('service', services, key),\n clusterMembers: (key) => lookup('cluster', clusters, key),\n cloudflareMount: (alias, surface) => {\n const account = lookup('cloudflare account', accounts, alias);\n if (!account.surfaces.includes(surface)) {\n unknownKey(`surface of account \"${alias}\"`, surface, account.surfaces);\n }\n return `cloudflare-${alias}-${surface}`;\n },\n accountId: (alias) => lookup('cloudflare account', accounts, alias).id,\n };\n}\n",
6
+ "/**\n * Read PINNED values by key. Nothing here is computed; it only refuses unknown keys.\n *\n * ⛔ WHY THESE ARE NOT DERIVED — each is a replace or an outage if it ever follows a rule:\n * - `names`: Workers, D1, KV, R2 and Durable Object names follow no rule, and a derived\n * D1 name REPLACES the database, deleting its data.\n * - `certificates`: hostnames on an adopted certificate. A change reissues it and revokes\n * the one a host is still serving.\n * - `adopted`: ids and names of objects that already exist (an LB pool, a WAF ruleset,\n * Ceph and PBS names, PKI common names). Rendering one from a rule means a new object.\n * - `policies` and `sshPrincipals`: explicit lists. A break-glass principal must never\n * vanish because a network leg was renamed; `unknownPrincipals` checks them against\n * the inventory instead of generating them from it.\n */\nimport { lookup } from './errors.ts';\nimport type { Site } from './schema.ts';\n\nexport interface Pins {\n /** A physical resource name, e.g. `pins.name('alerts.d1')`. */\n name(key: string): string;\n /** The hostnames on an adopted certificate. */\n certificate(key: string): readonly string[];\n /** An adopted object's id or name. */\n adopted(key: string): string;\n /** A policy list (emails, groups, CIDRs — whatever the enforcing repo reads). */\n policy(key: string): readonly string[];\n /** An SSH principal list. */\n principals(key: string): readonly string[];\n}\n\nexport function pins(site: Site): Pins {\n const { pinned } = site;\n return {\n name: (key) => lookup('pinned name', pinned.names, key),\n certificate: (key) => lookup('pinned certificate', pinned.certificates, key),\n adopted: (key) => lookup('adopted id', pinned.adopted, key),\n policy: (key) => lookup('pinned policy list', pinned.policies, key),\n principals: (key) => lookup('pinned principal list', pinned.sshPrincipals, key),\n };\n}\n",
7
+ "/**\n * The inventory: every name and address the site declares for a machine.\n *\n * ★ PINNED LISTS ARE CHECKED AGAINST IT, NOT GENERATED FROM IT. A principal list is a\n * subset of the inventory — an assertion a consumer's test makes — never a rendering of\n * it. The difference matters on the bad day: when a leg is renamed, a generated list\n * silently drops the old address, and the break-glass path goes with it (measured on\n * this estate 2026-08-12: \"name is not a listed principal\" on the only way in).\n */\nimport { joinPath } from './decode.ts';\nimport { derive } from './derive.ts';\nimport type { Site } from './schema.ts';\n\n/** Apex, zones, the vault's names, and every host key, alias, FQDN and leg address. */\nexport function inventory(site: Site): ReadonlySet<string> {\n const derived = derive(site);\n const names = new Set<string>([\n site.apex,\n ...Object.values(derived.zones),\n derived.vault.host,\n derived.vault.apiHost,\n site.vault.meshAddress,\n ]);\n for (const [key, host] of Object.entries(site.hosts)) {\n names.add(key);\n for (const alias of host.aliases ?? []) names.add(alias);\n const built = derived.hosts[key];\n if (built?.fqdn !== undefined) names.add(built.fqdn);\n for (const address of Object.values(built?.addresses ?? {})) names.add(address);\n }\n return names;\n}\n\n/** The entries of `list` the inventory does not know. Empty means `list` is a subset. */\nexport function unknownPrincipals(site: Site, list: readonly string[]): readonly string[] {\n const known = inventory(site);\n return list.filter((entry) => !known.has(entry));\n}\n\n/**\n * Every pinned SSH principal the inventory does not know, as `path: entry` lines.\n * ⚠️ A consumer TEST calls this; loading does not. A stale principal is a finding to fix\n * in review, not a reason to refuse a break-glass plan at 3am.\n */\nexport function pinnedPrincipalIssues(site: Site): readonly string[] {\n const issues: string[] = [];\n for (const [key, list] of Object.entries(site.pinned.sshPrincipals)) {\n for (const entry of unknownPrincipals(site, list)) {\n // ★ Bracketed like decode errors: `ssh-host.host` is one key, not two levels.\n const path = joinPath(['pinned', 'sshPrincipals', key]);\n issues.push(`${path}: \"${entry}\" is not in the inventory`);\n }\n }\n return issues;\n}\n",
8
+ "/**\n * Compare the identity a site EXPECTS with the identity a live system REPORTS.\n *\n * ⛔ WHY EVERY LIVE PLAN CALLS THIS. An exported `BAO_ADDR` from another shell points a\n * stack at a different vault with nothing else looking wrong: the token works, the\n * mounts exist, and the plan reads that vault's objects as drift to \"fix\". Only the\n * vault's own `cluster_name` (from `sys/health`) and the Cloudflare account id say which\n * system is on the other end.\n *\n * ★ PURE. This module fetches nothing. The caller reads `sys/health` and the account with\n * its own client (the kit's OpenBao and Cloudflare resources already have one) and hands\n * the observed values in, so this runs anywhere and is trivial to test.\n */\nimport { SiteError, lookup } from './errors.ts';\nimport type { Site } from './schema.ts';\n\nexport interface Identity {\n /** `cluster_name` from the vault's `sys/health`. */\n readonly vaultClusterName?: string;\n /** The namespace the client is using. `''` is the root namespace. */\n readonly vaultNamespace?: string;\n /** The Cloudflare account the credential resolves to. */\n readonly cloudflareAccountId?: string;\n}\n\nexport interface IdentityMismatch {\n readonly field: keyof Identity;\n readonly expected: string;\n /** `undefined` when the caller did not observe it — which is itself a mismatch. */\n readonly observed: string | undefined;\n}\n\nexport interface ExpectedIdentityOptions {\n /** The account alias the plan targets. Omit to leave the account out of the compare. */\n readonly account?: string;\n}\n\n/** What the site says the live systems are. Throws `unknown-key` for an unknown alias. */\nexport function expectedIdentity(site: Site, options: ExpectedIdentityOptions = {}): Identity {\n const account =\n options.account === undefined\n ? undefined\n : lookup('cloudflare account', site.cloudflare.accounts, options.account).id;\n return {\n vaultClusterName: site.vault.clusterName,\n vaultNamespace: site.vault.namespace,\n ...(account === undefined ? {} : { cloudflareAccountId: account }),\n };\n}\n\nconst FIELDS = ['vaultClusterName', 'vaultNamespace', 'cloudflareAccountId'] as const;\n\n/**\n * Every field the expectation sets and the observation does not match.\n * ⛔ An expected field the caller did not observe COUNTS AS A MISMATCH. \"We did not check\"\n * must never read the same as \"it matched\".\n * ⛔ An expectation that sets NO field throws `identity`. Compared field by field, `{}`\n * matches every system there is, so a caller that built it by mistake (a wrong spread, a\n * renamed key) would pass the guard on any vault and any account.\n */\nexport function compareIdentity(\n expected: Identity,\n observed: Identity,\n): readonly IdentityMismatch[] {\n if (FIELDS.every((field) => expected[field] === undefined)) {\n throw new SiteError(\n 'identity',\n 'refusing to plan: the expected identity names no field, so it would match any system',\n );\n }\n const mismatches: IdentityMismatch[] = [];\n for (const field of FIELDS) {\n const want = expected[field];\n if (want === undefined) continue;\n const got = observed[field];\n if (got !== want) mismatches.push({ field, expected: want, observed: got });\n }\n return mismatches;\n}\n\n/** {@link compareIdentity}, throwing `identity` with one line per mismatch. */\nexport function assertIdentity(expected: Identity, observed: Identity): void {\n const mismatches = compareIdentity(expected, observed);\n if (mismatches.length === 0) return;\n throw new SiteError(\n 'identity',\n 'refusing to plan: the live system is not the one this site describes',\n mismatches.map(\n (m) =>\n `${m.field}: expected \"${m.expected}\", observed ${m.observed === undefined ? 'nothing' : `\"${m.observed}\"`}`,\n ),\n );\n}\n",
9
+ "/**\n * The placeholders public docs use instead of estate values.\n *\n * ⛔ A PUBLIC DOC NEVER NAMES A LIVE VALUE. It writes `<vault-host>`, and anyone reading\n * it substitutes their own. `tokenValues(site)` is that substitution for one site; a\n * leak gate builds its needles from `tokenValues(liveSite)` in memory, so the live\n * values never have to be written down anywhere else.\n *\n * ★ `<cluster>` HAS NO SINGLE VALUE. It stands for any key of `site.clusters` (as\n * `<alias>` and `<surface>` do inside `cloudflare-<alias>-<surface>`), so it is listed\n * for doc lint but never substituted.\n */\nimport { derive } from './derive.ts';\nimport type { Site } from './schema.ts';\n\nexport const SITE_TOKENS = {\n '<apex>': 'the apex domain every derived name hangs off',\n '<vault-host>': 'the vault UI hostname: <vault label>.<apex>',\n '<vault-api-host>': 'the vault public API hostname: <api label>.<vault-host>',\n '<mgmt-zone>': 'the management zone: <mgmt label>.<apex>',\n '<access-team>': 'the Cloudflare Access team name (<access-team>.cloudflareaccess.com)',\n '<github-owner>': 'the GitHub user or organisation that owns the repos',\n '<estate-root>': 'the absolute directory holding host-local estate files',\n '<cluster>': 'any key of site.clusters — a variable, never substituted',\n} as const;\n\nexport type SiteToken = keyof typeof SITE_TOKENS;\n\n/** Tokens with exactly one value per site. */\nexport type ValuedToken = Exclude<SiteToken, '<cluster>'>;\n\n/** Each single-valued token's value for `site`. */\nexport function tokenValues(site: Site): Readonly<Record<ValuedToken, string>> {\n const derived = derive(site);\n return {\n '<apex>': site.apex,\n '<vault-host>': derived.vault.host,\n '<vault-api-host>': derived.vault.apiHost,\n '<mgmt-zone>': derived.mgmtZone,\n '<access-team>': site.cloudflare.access.team,\n '<github-owner>': site.github.owner,\n '<estate-root>': site.paths.estateRoot,\n };\n}\n\n/**\n * Replace every single-valued token in `text` with its value for `site`.\n * ★ ONE PASS, longest token first in the alternation. A value is never read again, so a\n * value that itself contains a token stays literal. ⚠️ Measured 2026-09-21: the earlier\n * token-by-token loop rendered an estate root of `/opt/<apex>` as `/opt/example.com`.\n */\nexport function renderTokens(text: string, site: Site): string {\n const values: Readonly<Record<string, string>> = tokenValues(site);\n const tokens = Object.keys(values).sort((a, b) => b.length - a.length);\n const pattern = new RegExp(\n tokens.map((t) => t.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('|'),\n 'g',\n );\n return text.replaceAll(pattern, (token) => values[token] ?? token);\n}\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "6A231AADB000E4A864756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,12 @@
1
+ import type { Site } from './schema.ts';
2
+ /** Apex, zones, the vault's names, and every host key, alias, FQDN and leg address. */
3
+ export declare function inventory(site: Site): ReadonlySet<string>;
4
+ /** The entries of `list` the inventory does not know. Empty means `list` is a subset. */
5
+ export declare function unknownPrincipals(site: Site, list: readonly string[]): readonly string[];
6
+ /**
7
+ * Every pinned SSH principal the inventory does not know, as `path: entry` lines.
8
+ * ⚠️ A consumer TEST calls this; loading does not. A stale principal is a finding to fix
9
+ * in review, not a reason to refuse a break-glass plan at 3am.
10
+ */
11
+ export declare function pinnedPrincipalIssues(site: Site): readonly string[];
12
+ //# sourceMappingURL=inventory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,CAiBzD;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAGxF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,MAAM,EAAE,CAUnE"}
package/dist/load.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { type Site } from './schema.ts';
2
+ export { ENV_OVERRIDES, SITE_FILE_VAR } from './overrides.ts';
3
+ export { type CheckoutState, checkoutProblem, readCheckout } from './checkout.ts';
4
+ /** Where the refusal points people. Shipped in the tarball and exported by path. */
5
+ export declare const SITE_EXAMPLE = "node_modules/@homeflare/site/site.example.json";
6
+ export interface LoadSiteOptions {
7
+ /** Defaults to `process.env`. */
8
+ readonly env?: Readonly<Record<string, string | undefined>>;
9
+ /** Resolves a relative `HF_SITE_FILE`. Defaults to `process.cwd()`. */
10
+ readonly cwd?: string;
11
+ /**
12
+ * Skip the committed-on-`main` check and accept `HF_SITE_*` overrides — what a
13
+ * consumer's `--site-dev` flag sets. Without it, any accepted override refuses.
14
+ */
15
+ readonly siteDev?: boolean;
16
+ /** The branch a reviewed site file lives on. Defaults to `main`. */
17
+ readonly branch?: string;
18
+ /** Test hook for the derive-version refusal. Defaults to the installed version. */
19
+ readonly installed?: string;
20
+ }
21
+ export interface LoadedSite {
22
+ readonly site: Site;
23
+ /** The absolute path that was read. */
24
+ readonly file: string;
25
+ /** Which `HF_SITE_*` overrides changed the file's values. Print these; they are silent otherwise. */
26
+ readonly overrides: readonly string[];
27
+ }
28
+ /**
29
+ * Locate, check, read, decode and validate the site file.
30
+ * Throws {@link SiteError}: `load`, `checkout`, `override`, `decode`, `reference`,
31
+ * `derive-version`.
32
+ */
33
+ export declare function loadSite(options?: LoadSiteOptions): Promise<LoadedSite>;
34
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,KAAK,IAAI,EAAc,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,KAAK,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElF,oFAAoF;AACpF,eAAO,MAAM,YAAY,mDAAmD,CAAC;AAE7E,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,uEAAuE;IACvE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAqED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwDjF"}
package/dist/load.js ADDED
@@ -0,0 +1,190 @@
1
+ import {
2
+ SiteError,
3
+ SiteSchema,
4
+ decodeStrict,
5
+ formatIssues,
6
+ validateSite
7
+ } from "./index-dvzn0279.js";
8
+
9
+ // src/load.ts
10
+ import { readFile } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+ import * as Cause from "effect/Cause";
13
+ import * as Config from "effect/Config";
14
+ import * as ConfigProvider from "effect/ConfigProvider";
15
+ import * as Effect from "effect/Effect";
16
+ import * as Exit from "effect/Exit";
17
+ import * as Schema from "effect/Schema";
18
+
19
+ // src/checkout.ts
20
+ import { execFile } from "node:child_process";
21
+ import { realpath } from "node:fs/promises";
22
+ import { basename, dirname } from "node:path";
23
+ import { promisify } from "node:util";
24
+ var run = promisify(execFile);
25
+ function gitEnv() {
26
+ return {
27
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
28
+ GIT_LITERAL_PATHSPECS: "1"
29
+ };
30
+ }
31
+ async function git(dir, args) {
32
+ try {
33
+ const { stdout } = await run("git", ["-C", dir, ...args], { encoding: "utf8", env: gitEnv() });
34
+ return stdout.trim();
35
+ } catch {
36
+ return;
37
+ }
38
+ }
39
+ async function resolved(file) {
40
+ try {
41
+ return await realpath(file);
42
+ } catch {
43
+ return file;
44
+ }
45
+ }
46
+ async function readCheckout(file) {
47
+ const real = await resolved(file);
48
+ const dir = dirname(real);
49
+ const name = basename(real);
50
+ if (await git(dir, ["rev-parse", "--is-inside-work-tree"]) !== "true")
51
+ return;
52
+ const branch = await git(dir, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
53
+ const tracked = await git(dir, ["ls-files", "--error-unmatch", "--", name]) !== undefined;
54
+ const status = await git(dir, ["status", "--porcelain", "--", name]);
55
+ const onDisk = await git(dir, ["hash-object", "--", name]);
56
+ const committed = await git(dir, ["rev-parse", "--verify", "--quiet", `HEAD:./${name}`]);
57
+ const differs = onDisk === undefined || committed === undefined || onDisk !== committed;
58
+ return { branch, tracked, dirty: status === undefined || status !== "" || differs };
59
+ }
60
+ function checkoutProblem(state, branch) {
61
+ if (state === undefined)
62
+ return "it is not inside a git checkout";
63
+ if (!state.tracked)
64
+ return "it is not committed (untracked or ignored)";
65
+ if (state.dirty)
66
+ return "it has uncommitted changes";
67
+ if (state.branch !== branch) {
68
+ return `the checkout is on ${state.branch === undefined ? "a detached HEAD" : `"${state.branch}"`}, not "${branch}"`;
69
+ }
70
+ return;
71
+ }
72
+
73
+ // src/overrides.ts
74
+ var ENV_OVERRIDES = {
75
+ HF_SITE_APEX: ["apex"],
76
+ HF_SITE_VAULT_LABEL: ["vault", "label"],
77
+ HF_SITE_VAULT_API_LABEL: ["vault", "apiLabel"],
78
+ HF_SITE_VAULT_PORT: ["vault", "port"],
79
+ HF_SITE_VAULT_MESH_ADDRESS: ["vault", "meshAddress"],
80
+ HF_SITE_VAULT_OIDC_MOUNT: ["vault", "oidcMount"],
81
+ HF_SITE_VAULT_CLI_CALLBACK_PORT: ["vault", "cliCallbackPort"],
82
+ HF_SITE_VAULT_LAN_HOST: ["vault", "lan", "host"],
83
+ HF_SITE_VAULT_LAN_PORT: ["vault", "lan", "port"],
84
+ HF_SITE_VAULT_LAN_SCHEME: ["vault", "lan", "scheme"],
85
+ HF_SITE_CLOUDFLARE_ACCESS_TEAM: ["cloudflare", "access", "team"],
86
+ HF_SITE_GITHUB_OWNER: ["github", "owner"],
87
+ HF_SITE_PATHS_ESTATE_ROOT: ["paths", "estateRoot"]
88
+ };
89
+ var GUARD_FIELDS = [
90
+ "version",
91
+ "deriveVersion",
92
+ "kind",
93
+ "vault.clusterName",
94
+ "vault.namespace"
95
+ ];
96
+ var SITE_FILE_VAR = "HF_SITE_FILE";
97
+ var PREFIX = "HF_SITE_";
98
+ function pickOverrides(env) {
99
+ const accepted = {};
100
+ const refused = [];
101
+ for (const [name, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {
102
+ if (!name.startsWith(PREFIX) || name === SITE_FILE_VAR || value === undefined)
103
+ continue;
104
+ if (Object.hasOwn(ENV_OVERRIDES, name))
105
+ accepted[name] = value;
106
+ else
107
+ refused.push(name);
108
+ }
109
+ return { accepted, refused };
110
+ }
111
+
112
+ // src/load.ts
113
+ var SITE_EXAMPLE = "node_modules/@homeflare/site/site.example.json";
114
+ var SiteConfig = Config.schema(SiteSchema, "site");
115
+ function envProvider(accepted) {
116
+ return ConfigProvider.fromEnv({ env: accepted }).pipe(ConfigProvider.nested("hf"), ConfigProvider.constantCase);
117
+ }
118
+ function withOverrides(json, accepted) {
119
+ const provider = ConfigProvider.orElse(envProvider(accepted), ConfigProvider.fromUnknown({ site: json }));
120
+ const exit = Effect.runSyncExit(SiteConfig.parse(provider));
121
+ if (Exit.isSuccess(exit))
122
+ return exit.value;
123
+ const error = Cause.squash(exit.cause);
124
+ const cause = typeof error === "object" && error !== null && "cause" in error ? error.cause : error;
125
+ if (Schema.isSchemaError(cause)) {
126
+ throw new SiteError("override", "an HF_SITE_* override is not a valid value", formatIssues(cause.issue, "site").map(nameTheVariable));
127
+ }
128
+ throw error;
129
+ }
130
+ function nameTheVariable(issue) {
131
+ for (const [name, path] of Object.entries(ENV_OVERRIDES)) {
132
+ const dotted = path.join(".");
133
+ if (issue.startsWith(`${dotted}: `))
134
+ return `${name} (${issue}`.replace(": ", "): ");
135
+ }
136
+ return issue;
137
+ }
138
+ async function readJson(file) {
139
+ let text;
140
+ try {
141
+ text = await readFile(file, "utf8");
142
+ } catch (error) {
143
+ const reason = error instanceof Error ? error.message : String(error);
144
+ throw new SiteError("load", `${SITE_FILE_VAR} points at ${file}, which cannot be read: ${reason}`);
145
+ }
146
+ try {
147
+ return JSON.parse(text);
148
+ } catch (error) {
149
+ const reason = error instanceof Error ? error.message : String(error);
150
+ throw new SiteError("load", `${file} is not plain JSON (${reason}). Site files carry no comments and no trailing commas.`);
151
+ }
152
+ }
153
+ async function loadSite(options = {}) {
154
+ const env = options.env ?? process.env;
155
+ const named = env[SITE_FILE_VAR]?.trim();
156
+ if (named === undefined || named === "") {
157
+ throw new SiteError("load", `${SITE_FILE_VAR} is not set, and there is no default path. Start from the example: ` + `copy ${SITE_EXAMPLE}, replace every value, then export ${SITE_FILE_VAR}=<that file>.`);
158
+ }
159
+ const file = resolve(options.cwd ?? process.cwd(), named);
160
+ const json = await readJson(file);
161
+ if (options.siteDev !== true) {
162
+ const branch = options.branch ?? "main";
163
+ const problem = checkoutProblem(await readCheckout(file), branch);
164
+ if (problem !== undefined) {
165
+ throw new SiteError("checkout", `refusing ${file}: ${problem}. Live values come from a reviewed "${branch}"; ` + `pass --site-dev (siteDev: true) to load it anyway.`);
166
+ }
167
+ }
168
+ const fromFile = decodeStrict(json);
169
+ const { accepted, refused } = pickOverrides(env);
170
+ if (refused.length > 0) {
171
+ throw new SiteError("override", "unsupported HF_SITE_* variables; edit the site file instead", refused.map((name) => `${name}: not in ENV_OVERRIDES — records, lists and guard fields ` + `(${GUARD_FIELDS.join(", ")}) are never overridable`));
172
+ }
173
+ const names = Object.keys(accepted);
174
+ if (names.length > 0 && options.siteDev !== true) {
175
+ throw new SiteError("override", "HF_SITE_* overrides change reviewed values, so they need --site-dev (siteDev: true); " + "unset them, or edit the site file on a branch", names);
176
+ }
177
+ const site = names.length === 0 ? fromFile : withOverrides(json, accepted);
178
+ return { site: validateSite(site, options), file, overrides: names };
179
+ }
180
+ export {
181
+ ENV_OVERRIDES,
182
+ SITE_EXAMPLE,
183
+ SITE_FILE_VAR,
184
+ checkoutProblem,
185
+ loadSite,
186
+ readCheckout
187
+ };
188
+
189
+ //# debugId=2E7AE5CBD991A25764756E2164756E21
190
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/load.ts", "../src/checkout.ts", "../src/overrides.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `@homeflare/site/load` — read the site file named by `HF_SITE_FILE`.\n *\n * ⛔ NODE / BUN ONLY: this subpath reads a file and spawns git. Workers use `decodeSite`\n * from the main entry with JSON they already hold.\n *\n * ⛔ NO DEFAULT PATH, AND NO DEFAULT SITE. A missing `HF_SITE_FILE` refuses, naming the\n * example. A fallback would plan someone's stack against whatever file happened to be\n * there — or against the example's placeholder account.\n *\n * ★ PLAIN JSON, not JSONC: Nix, jq and Python read the same file, and none of them read\n * comments.\n */\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport * as Cause from 'effect/Cause';\nimport * as Config from 'effect/Config';\nimport * as ConfigProvider from 'effect/ConfigProvider';\nimport * as Effect from 'effect/Effect';\nimport * as Exit from 'effect/Exit';\nimport * as Schema from 'effect/Schema';\nimport { checkoutProblem, readCheckout } from './checkout.ts';\nimport { decodeStrict, formatIssues, validateSite } from './decode.ts';\nimport { SiteError } from './errors.ts';\nimport { ENV_OVERRIDES, GUARD_FIELDS, SITE_FILE_VAR, pickOverrides } from './overrides.ts';\nimport { type Site, SiteSchema } from './schema.ts';\n\nexport { ENV_OVERRIDES, SITE_FILE_VAR } from './overrides.ts';\nexport { type CheckoutState, checkoutProblem, readCheckout } from './checkout.ts';\n\n/** Where the refusal points people. Shipped in the tarball and exported by path. */\nexport const SITE_EXAMPLE = 'node_modules/@homeflare/site/site.example.json';\n\nexport interface LoadSiteOptions {\n /** Defaults to `process.env`. */\n readonly env?: Readonly<Record<string, string | undefined>>;\n /** Resolves a relative `HF_SITE_FILE`. Defaults to `process.cwd()`. */\n readonly cwd?: string;\n /**\n * Skip the committed-on-`main` check and accept `HF_SITE_*` overrides — what a\n * consumer's `--site-dev` flag sets. Without it, any accepted override refuses.\n */\n readonly siteDev?: boolean;\n /** The branch a reviewed site file lives on. Defaults to `main`. */\n readonly branch?: string;\n /** Test hook for the derive-version refusal. Defaults to the installed version. */\n readonly installed?: string;\n}\n\nexport interface LoadedSite {\n readonly site: Site;\n /** The absolute path that was read. */\n readonly file: string;\n /** Which `HF_SITE_*` overrides changed the file's values. Print these; they are silent otherwise. */\n readonly overrides: readonly string[];\n}\n\nconst SiteConfig = Config.schema(SiteSchema, 'site');\n\n/**\n * ⚠️ `nested('hf')` MUST COME BEFORE `constantCase`. Measured 2026-09-21 on effect\n * 4.0.0-rc.115: each transformation sees the path the previous one produced, so the\n * other order upper-cases `site.apex` to `SITE_APEX` and THEN prefixes a lower-case\n * `hf` — the provider looks for `hf_SITE_APEX`, finds nothing, and the file's value\n * wins. `HF_SITE_APEX` is ignored with no error at all. tests/load.test.ts measures both.\n */\nfunction envProvider(accepted: Readonly<Record<string, string>>): ConfigProvider.ConfigProvider {\n return ConfigProvider.fromEnv({ env: accepted }).pipe(\n ConfigProvider.nested('hf'),\n ConfigProvider.constantCase,\n );\n}\n\nfunction withOverrides(json: unknown, accepted: Readonly<Record<string, string>>): Site {\n const provider = ConfigProvider.orElse(\n envProvider(accepted),\n ConfigProvider.fromUnknown({ site: json }),\n );\n const exit = Effect.runSyncExit(SiteConfig.parse(provider));\n if (Exit.isSuccess(exit)) return exit.value;\n const error = Cause.squash(exit.cause);\n const cause =\n typeof error === 'object' && error !== null && 'cause' in error ? error.cause : error;\n if (Schema.isSchemaError(cause)) {\n throw new SiteError(\n 'override',\n 'an HF_SITE_* override is not a valid value',\n formatIssues(cause.issue, 'site').map(nameTheVariable),\n );\n }\n throw error;\n}\n\n/** `vault.port: …` → `HF_SITE_VAULT_PORT (vault.port): …`, so the fix names what to unset. */\nfunction nameTheVariable(issue: string): string {\n for (const [name, path] of Object.entries(ENV_OVERRIDES)) {\n const dotted = path.join('.');\n if (issue.startsWith(`${dotted}: `)) return `${name} (${issue}`.replace(': ', '): ');\n }\n return issue;\n}\n\nasync function readJson(file: string): Promise<unknown> {\n let text: string;\n try {\n text = await readFile(file, 'utf8');\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n throw new SiteError(\n 'load',\n `${SITE_FILE_VAR} points at ${file}, which cannot be read: ${reason}`,\n );\n }\n try {\n return JSON.parse(text) as unknown;\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n throw new SiteError(\n 'load',\n `${file} is not plain JSON (${reason}). Site files carry no comments and no trailing commas.`,\n );\n }\n}\n\n/**\n * Locate, check, read, decode and validate the site file.\n * Throws {@link SiteError}: `load`, `checkout`, `override`, `decode`, `reference`,\n * `derive-version`.\n */\nexport async function loadSite(options: LoadSiteOptions = {}): Promise<LoadedSite> {\n const env = options.env ?? process.env;\n const named = env[SITE_FILE_VAR]?.trim();\n if (named === undefined || named === '') {\n throw new SiteError(\n 'load',\n `${SITE_FILE_VAR} is not set, and there is no default path. Start from the example: ` +\n `copy ${SITE_EXAMPLE}, replace every value, then export ${SITE_FILE_VAR}=<that file>.`,\n );\n }\n const file = resolve(options.cwd ?? process.cwd(), named);\n // ★ Read before the checkout check, so a mistyped path says \"cannot be read\" rather\n // than \"not committed\" about a file that does not exist.\n const json = await readJson(file);\n\n if (options.siteDev !== true) {\n const branch = options.branch ?? 'main';\n const problem = checkoutProblem(await readCheckout(file), branch);\n if (problem !== undefined) {\n throw new SiteError(\n 'checkout',\n `refusing ${file}: ${problem}. Live values come from a reviewed \"${branch}\"; ` +\n `pass --site-dev (siteDev: true) to load it anyway.`,\n );\n }\n }\n\n // ★ The file is decoded ALONE first, strictly, so its own errors name file paths and a\n // misspelt key is refused before any override can paper over it.\n const fromFile = decodeStrict(json);\n\n const { accepted, refused } = pickOverrides(env);\n if (refused.length > 0) {\n throw new SiteError(\n 'override',\n 'unsupported HF_SITE_* variables; edit the site file instead',\n refused.map(\n (name) =>\n `${name}: not in ENV_OVERRIDES — records, lists and guard fields ` +\n `(${GUARD_FIELDS.join(', ')}) are never overridable`,\n ),\n );\n }\n const names = Object.keys(accepted);\n // ⛔ An override is an unreviewed value: same switch as the checkout guard (overrides.ts).\n if (names.length > 0 && options.siteDev !== true) {\n throw new SiteError(\n 'override',\n 'HF_SITE_* overrides change reviewed values, so they need --site-dev (siteDev: true); ' +\n 'unset them, or edit the site file on a branch',\n names,\n );\n }\n const site = names.length === 0 ? fromFile : withOverrides(json, accepted);\n\n return { site: validateSite(site, options), file, overrides: names };\n}\n",
6
+ "/**\n * Is the site file committed, unmodified, on `main`?\n *\n * ⛔ NODE-ONLY (spawns git). Imported by `@homeflare/site/load`, never by the main entry.\n *\n * ★ WHY THE LOADER ASKS. Live plans must run on reviewed values. A site file edited in a\n * working tree, or read from a feature branch, would plan whatever it says — and a plan\n * against live state is where an unreviewed rename becomes a replace. `siteDev` (the\n * `--site-dev` flag in a consumer's CLI) is the explicit, visible way around it.\n *\n * ★ DIRTINESS IS THE FILE'S OWN, not the whole checkout's. Unrelated uncommitted work in\n * the repo holding the site file says nothing about the values being loaded, and would\n * otherwise block every deploy while someone edits a doc.\n */\nimport { execFile } from 'node:child_process';\nimport { realpath } from 'node:fs/promises';\nimport { basename, dirname } from 'node:path';\nimport { promisify } from 'node:util';\n\nconst run = promisify(execFile);\n\n/**\n * ⚠️ GIT_* FROM THE CALLER'S ENVIRONMENT IS DROPPED. Git exports `GIT_DIR` (and friends)\n * to hooks, and `-C <dir>` does not override an exported `GIT_DIR`. Measured 2026-09-21:\n * `GIT_DIR=<some repo>/.git git -C <a directory outside any repo> symbolic-ref HEAD`\n * printed that other repo's branch. A loader run from a pre-push hook would ask about\n * the HOOK's repository, and answer \"clean, on main\" for a file it never looked at.\n * ★ `GIT_LITERAL_PATHSPECS=1` is the one git variable set back: the file name is a name,\n * never a glob. Without it `site[1].json` would match a tracked `site1.json`.\n */\nfunction gitEnv(): NodeJS.ProcessEnv {\n return {\n ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_'))),\n GIT_LITERAL_PATHSPECS: '1',\n };\n}\n\nexport interface CheckoutState {\n /** The checked-out branch, or `undefined` on a detached HEAD. */\n readonly branch: string | undefined;\n /** Git knows the file (⚠️ an ignored or untracked file was never reviewed). */\n readonly tracked: boolean;\n /**\n * The file's bytes differ from HEAD, staged or not.\n * ⚠️ Compared by BLOB HASH, not only by `git status`. Measured 2026-09-21: after\n * `git update-index --skip-worktree` (the usual way to keep a local config tweak),\n * `status --porcelain` printed nothing for an edited site file, and a status-only\n * guard loaded the edited apex as \"clean, on main\".\n */\n readonly dirty: boolean;\n}\n\nasync function git(dir: string, args: readonly string[]): Promise<string | undefined> {\n try {\n const { stdout } = await run('git', ['-C', dir, ...args], { encoding: 'utf8', env: gitEnv() });\n return stdout.trim();\n } catch {\n return undefined;\n }\n}\n\n/** `realpath`, or the path unchanged when it cannot be resolved (the git calls then fail). */\nasync function resolved(file: string): Promise<string> {\n try {\n return await realpath(file);\n } catch {\n return file;\n }\n}\n\n/**\n * The file's checkout state, or `undefined` when it is not inside a git work tree.\n *\n * ⚠️ THE REAL FILE IS CHECKED, NOT THE LINK. Measured 2026-09-21: a committed symlink whose\n * target lived outside the repository passed as \"clean, on main\" while the target was\n * edited freely — git tracks the link's target PATH, not the bytes behind it. Resolving\n * first makes that target \"not inside a git checkout\", which refuses.\n */\nexport async function readCheckout(file: string): Promise<CheckoutState | undefined> {\n const real = await resolved(file);\n const dir = dirname(real);\n const name = basename(real);\n if ((await git(dir, ['rev-parse', '--is-inside-work-tree'])) !== 'true') return undefined;\n // ★ symbolic-ref, not `rev-parse --abbrev-ref`: it answers on an unborn branch too, and\n // fails (→ undefined) on a detached HEAD instead of printing the word \"HEAD\".\n const branch = await git(dir, ['symbolic-ref', '--quiet', '--short', 'HEAD']);\n const tracked = (await git(dir, ['ls-files', '--error-unmatch', '--', name])) !== undefined;\n const status = await git(dir, ['status', '--porcelain', '--', name]);\n // ★ hash-object applies the path's clean filters, so it names the blob git WOULD store;\n // HEAD:./<name> is the blob that was committed. Either failing counts as dirty.\n const onDisk = await git(dir, ['hash-object', '--', name]);\n const committed = await git(dir, ['rev-parse', '--verify', '--quiet', `HEAD:./${name}`]);\n const differs = onDisk === undefined || committed === undefined || onDisk !== committed;\n return { branch, tracked, dirty: status === undefined || status !== '' || differs };\n}\n\n/** Why `state` is not a reviewed checkout of `branch`, or `undefined` when it is. */\nexport function checkoutProblem(\n state: CheckoutState | undefined,\n branch: string,\n): string | undefined {\n if (state === undefined) return 'it is not inside a git checkout';\n if (!state.tracked) return 'it is not committed (untracked or ignored)';\n if (state.dirty) return 'it has uncommitted changes';\n if (state.branch !== branch) {\n return `the checkout is on ${state.branch === undefined ? 'a detached HEAD' : `\"${state.branch}\"`}, not \"${branch}\"`;\n }\n return undefined;\n}\n",
7
+ "/**\n * The environment variables `loadSite` accepts as overrides — and the ONLY ones.\n *\n * ★ WHY A LIST AND NOT \"ANY HF_SITE_*\". Effect's env provider will happily answer any\n * path, and three of the answers are wrong without an error. Measured 2026-09-21 on\n * effect 4.0.0-rc.115, overriding on top of the file via `ConfigProvider.orElse`:\n * 1. A RECORD or ARRAY is REPLACED, not merged. `HF_SITE_NETWORKS_MGMT` decoded\n * `networks` as `{ \"MGMT\": … }` — every other network gone, and the key upper-cased.\n * `HF_SITE_ZONES_MGMT` did the same to `zones`, and `…_LIST_0` truncated a list.\n * 2. A name that is another's name plus `_…` SHADOWS it. With `HF_SITE_LABELS_VAULT_API`\n * set, `labels.vault` was read as a record (every `HF_SITE_LABELS_VAULT_*` var) and\n * the decode failed \"Expected string\". Hence `vault.label` / `vault.apiLabel`.\n * 3. The ORDER of `nested` and `constantCase` decides the prefix: see load.ts.\n * So only scalar leaves under plain structs are listed, and tests/overrides.test.ts\n * proves each one changes exactly its own path and nothing else.\n *\n * ⛔ GUARD FIELDS ARE NEVER OVERRIDABLE: `version`, `deriveVersion`, `kind`,\n * `vault.clusterName` and `vault.namespace`. An exported `HF_SITE_KIND=live` would defeat\n * the stage guard, and an overridden cluster name or namespace would make the identity\n * check compare against itself: a client that takes its namespace from the site would\n * switch namespace AND expectation together, and plan into the other one \"matching\".\n *\n * ⛔ AND OVERRIDES ARE DEV-ONLY. An override is an unreviewed value, exactly like an\n * uncommitted edit to the file, so `loadSite` accepts one only with `siteDev` — the same\n * switch as the checkout guard. A stray `HF_SITE_APEX` left in a shell would otherwise\n * plan a rename of every derived hostname against live state.\n *\n * ⚠️ `HF_` IS ALSO HUGGING FACE'S PREFIX (`HF_TOKEN`, `HF_HOME`). Only `HF_SITE_*` is read,\n * and only the names below ever reach the provider.\n */\n\n/** Env var → the site path it overrides. */\nexport const ENV_OVERRIDES: Readonly<Record<string, readonly string[]>> = {\n HF_SITE_APEX: ['apex'],\n HF_SITE_VAULT_LABEL: ['vault', 'label'],\n HF_SITE_VAULT_API_LABEL: ['vault', 'apiLabel'],\n HF_SITE_VAULT_PORT: ['vault', 'port'],\n HF_SITE_VAULT_MESH_ADDRESS: ['vault', 'meshAddress'],\n HF_SITE_VAULT_OIDC_MOUNT: ['vault', 'oidcMount'],\n HF_SITE_VAULT_CLI_CALLBACK_PORT: ['vault', 'cliCallbackPort'],\n HF_SITE_VAULT_LAN_HOST: ['vault', 'lan', 'host'],\n HF_SITE_VAULT_LAN_PORT: ['vault', 'lan', 'port'],\n HF_SITE_VAULT_LAN_SCHEME: ['vault', 'lan', 'scheme'],\n HF_SITE_CLOUDFLARE_ACCESS_TEAM: ['cloudflare', 'access', 'team'],\n HF_SITE_GITHUB_OWNER: ['github', 'owner'],\n HF_SITE_PATHS_ESTATE_ROOT: ['paths', 'estateRoot'],\n};\n\n/** Site paths no variable may ever set. `tests/overrides.test.ts` holds the list to this. */\nexport const GUARD_FIELDS: readonly string[] = [\n 'version',\n 'deriveVersion',\n 'kind',\n 'vault.clusterName',\n 'vault.namespace',\n];\n\n/** Locates the file; read by `loadSite`, never passed to the provider. */\nexport const SITE_FILE_VAR = 'HF_SITE_FILE';\n\nconst PREFIX = 'HF_SITE_';\n\n/**\n * The accepted overrides present in `env`, or a refusal naming every rejected variable.\n * Returns the names sorted, so a caller can print exactly what changed the file's values.\n */\nexport function pickOverrides(env: Readonly<Record<string, string | undefined>>): {\n readonly accepted: Readonly<Record<string, string>>;\n readonly refused: readonly string[];\n} {\n const accepted: Record<string, string> = {};\n const refused: string[] = [];\n for (const [name, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) {\n if (!name.startsWith(PREFIX) || name === SITE_FILE_VAR || value === undefined) continue;\n if (Object.hasOwn(ENV_OVERRIDES, name)) accepted[name] = value;\n else refused.push(name);\n }\n return { accepted, refused };\n}\n"
8
+ ],
9
+ "mappings": "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",
10
+ "debugId": "2E7AE5CBD991A25764756E2164756E21",
11
+ "names": []
12
+ }
package/dist/net.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** The largest usable host number on a network (the broadcast address is excluded). */
2
+ export declare function lastHostNumber(cidr: string): number;
3
+ /** `addressOn('192.0.2.0/24', 11)` is `192.0.2.11`. */
4
+ export declare function addressOn(cidr: string, hostNumber: number): string;
5
+ //# sourceMappingURL=net.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAaA,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAInD;AAED,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAOlE"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The environment variables `loadSite` accepts as overrides — and the ONLY ones.
3
+ *
4
+ * ★ WHY A LIST AND NOT "ANY HF_SITE_*". Effect's env provider will happily answer any
5
+ * path, and three of the answers are wrong without an error. Measured 2026-09-21 on
6
+ * effect 4.0.0-rc.115, overriding on top of the file via `ConfigProvider.orElse`:
7
+ * 1. A RECORD or ARRAY is REPLACED, not merged. `HF_SITE_NETWORKS_MGMT` decoded
8
+ * `networks` as `{ "MGMT": … }` — every other network gone, and the key upper-cased.
9
+ * `HF_SITE_ZONES_MGMT` did the same to `zones`, and `…_LIST_0` truncated a list.
10
+ * 2. A name that is another's name plus `_…` SHADOWS it. With `HF_SITE_LABELS_VAULT_API`
11
+ * set, `labels.vault` was read as a record (every `HF_SITE_LABELS_VAULT_*` var) and
12
+ * the decode failed "Expected string". Hence `vault.label` / `vault.apiLabel`.
13
+ * 3. The ORDER of `nested` and `constantCase` decides the prefix: see load.ts.
14
+ * So only scalar leaves under plain structs are listed, and tests/overrides.test.ts
15
+ * proves each one changes exactly its own path and nothing else.
16
+ *
17
+ * ⛔ GUARD FIELDS ARE NEVER OVERRIDABLE: `version`, `deriveVersion`, `kind`,
18
+ * `vault.clusterName` and `vault.namespace`. An exported `HF_SITE_KIND=live` would defeat
19
+ * the stage guard, and an overridden cluster name or namespace would make the identity
20
+ * check compare against itself: a client that takes its namespace from the site would
21
+ * switch namespace AND expectation together, and plan into the other one "matching".
22
+ *
23
+ * ⛔ AND OVERRIDES ARE DEV-ONLY. An override is an unreviewed value, exactly like an
24
+ * uncommitted edit to the file, so `loadSite` accepts one only with `siteDev` — the same
25
+ * switch as the checkout guard. A stray `HF_SITE_APEX` left in a shell would otherwise
26
+ * plan a rename of every derived hostname against live state.
27
+ *
28
+ * ⚠️ `HF_` IS ALSO HUGGING FACE'S PREFIX (`HF_TOKEN`, `HF_HOME`). Only `HF_SITE_*` is read,
29
+ * and only the names below ever reach the provider.
30
+ */
31
+ /** Env var → the site path it overrides. */
32
+ export declare const ENV_OVERRIDES: Readonly<Record<string, readonly string[]>>;
33
+ /** Site paths no variable may ever set. `tests/overrides.test.ts` holds the list to this. */
34
+ export declare const GUARD_FIELDS: readonly string[];
35
+ /** Locates the file; read by `loadSite`, never passed to the provider. */
36
+ export declare const SITE_FILE_VAR = "HF_SITE_FILE";
37
+ /**
38
+ * The accepted overrides present in `env`, or a refusal naming every rejected variable.
39
+ * Returns the names sorted, so a caller can print exactly what changed the file's values.
40
+ */
41
+ export declare function pickOverrides(env: Readonly<Record<string, string | undefined>>): {
42
+ readonly accepted: Readonly<Record<string, string>>;
43
+ readonly refused: readonly string[];
44
+ };
45
+ //# sourceMappingURL=overrides.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../src/overrides.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAcrE,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAMzC,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAI5C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG;IAChF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CASA"}
package/dist/pins.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Site } from './schema.ts';
2
+ export interface Pins {
3
+ /** A physical resource name, e.g. `pins.name('alerts.d1')`. */
4
+ name(key: string): string;
5
+ /** The hostnames on an adopted certificate. */
6
+ certificate(key: string): readonly string[];
7
+ /** An adopted object's id or name. */
8
+ adopted(key: string): string;
9
+ /** A policy list (emails, groups, CIDRs — whatever the enforcing repo reads). */
10
+ policy(key: string): readonly string[];
11
+ /** An SSH principal list. */
12
+ principals(key: string): readonly string[];
13
+ }
14
+ export declare function pins(site: Site): Pins;
15
+ //# sourceMappingURL=pins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pins.d.ts","sourceRoot":"","sources":["../src/pins.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,WAAW,IAAI;IACnB,+DAA+D;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC5C,sCAAsC;IACtC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,iFAAiF;IACjF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACvC,6BAA6B;IAC7B,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CAC5C;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CASrC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The value shapes a site file is made of. Each one refuses at decode time, naming its path,
3
+ * so a typo fails when the file is read rather than when a plan renames something.
4
+ *
5
+ * ⛔ LOWERCASE ONLY for DNS names. Cloudflare, OpenBao and ssh compare names as strings in
6
+ * places, and `Mgmt.Example.com` is not `mgmt.example.com` to a principal list.
7
+ */
8
+ import * as Schema from 'effect/Schema';
9
+ /** One DNS label: `v`, `mgmt`, `n1`. Also the shape of every key a consumer looks up. */
10
+ export declare const Label: Schema.String;
11
+ /** A fully qualified name with at least two labels: `example.com`, `shop.example.net`. */
12
+ export declare const Hostname: Schema.String;
13
+ /** A certificate name: a hostname, or one leading wildcard label. */
14
+ export declare const CertificateName: Schema.String;
15
+ /** A dotted-quad IPv4 address, every octet 0–255. */
16
+ export declare const Ipv4: Schema.String;
17
+ /** Parse `a.b.c.d/p` into a 32-bit base and a prefix length. `undefined` when malformed. */
18
+ export declare function parseCidr(cidr: string): {
19
+ base: number;
20
+ prefix: number;
21
+ } | undefined;
22
+ /**
23
+ * An IPv4 network, `/8` to `/30`, written at its network address.
24
+ *
25
+ * ⚠️ `192.0.2.5/24` is refused, not normalised. A host address with a prefix usually means
26
+ * someone pasted an interface address where a network belongs; normalising it would hide
27
+ * the mistake and derive every leg address from the wrong idea of the network.
28
+ */
29
+ export declare const Ipv4Cidr: Schema.String;
30
+ /** A host's number on a network: `.11` on a /24 is 11. Checked against the prefix later. */
31
+ export declare const HostNumber: Schema.Number;
32
+ export declare const Port: Schema.Number;
33
+ /** A Cloudflare account id: 32 lowercase hex characters. */
34
+ export declare const AccountId: Schema.String;
35
+ /** A GitHub user or organisation login. GitHub itself allows uppercase here. */
36
+ export declare const GithubOwner: Schema.String;
37
+ /** An absolute POSIX path with no trailing slash: `/opt/example`. */
38
+ export declare const AbsolutePath: Schema.String;
39
+ /** A semver version, as npm writes one: `0.1.0`, `1.0.0-rc.1`. */
40
+ export declare const SemVer: Schema.String;
41
+ /** A key into a `pinned` table: `alerts.d1`, `ssh-host.host`. Dotted, lowercase. */
42
+ export declare const PinKey: Schema.String;
43
+ /**
44
+ * A physical resource name: Worker, D1, KV, R2, Durable Object class.
45
+ * ★ Case is allowed because Durable Object bindings name PascalCase classes.
46
+ */
47
+ export declare const PhysicalName: Schema.String;
48
+ /**
49
+ * One SSH principal or policy entry.
50
+ * ⛔ No commas and no whitespace. OpenBao stores principal lists comma-joined, so an entry
51
+ * holding a comma silently becomes two principals, and a stray space becomes a name no
52
+ * host will ever present.
53
+ */
54
+ export declare const ListEntry: Schema.String;
55
+ /** A free-form identifier that must not be blank: cluster names, adopted ids, CNs. */
56
+ export declare const NonBlank: Schema.String;
57
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,yFAAyF;AACzF,eAAO,MAAM,KAAK,eAIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,eAKpB,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,eAAe,eAK3B,CAAC;AAIF,qDAAqD;AACrD,eAAO,MAAM,IAAI,eAIhB,CAAC;AAEF,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAMpF;AAED;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,eAYpB,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,UAAU,eAGtB,CAAC;AAEF,eAAO,MAAM,IAAI,eAGhB,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,SAAS,eAIrB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,WAAW,eAIvB,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,YAAY,eAIxB,CAAC;AAEF,kEAAkE;AAClE,eAAO,MAAM,MAAM,eAElB,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,MAAM,eAIlB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,eAIxB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,eAErB,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,QAAQ,eAEpB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Site } from './schema.ts';
2
+ /** The zone key that means the apex itself. Reserved: a site may not declare it. */
3
+ export declare const APEX_ZONE = "apex";
4
+ /** Every broken reference in `site`, each naming its path. Empty when the site is sound. */
5
+ export declare function referenceIssues(site: Site): readonly string[];
6
+ //# sourceMappingURL=references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"references.d.ts","sourceRoot":"","sources":["../src/references.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,oFAAoF;AACpF,eAAO,MAAM,SAAS,SAAS,CAAC;AA6ChC,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,MAAM,EAAE,CAwB7D"}