@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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/checkout.d.ts +26 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/decode.d.ts +26 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/derive.d.ts +66 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/guards.d.ts +19 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/identity.d.ts +33 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index-dvzn0279.js +288 -0
- package/dist/index-dvzn0279.js.map +17 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +14 -0
- package/dist/inventory.d.ts +12 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/load.d.ts +34 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +190 -0
- package/dist/load.js.map +12 -0
- package/dist/net.d.ts +5 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/overrides.d.ts +45 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/pins.d.ts +15 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/primitives.d.ts +57 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/references.d.ts +6 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/schema.d.ts +100 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/tokens.d.ts +24 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +41 -0
- package/site.example.json +81 -0
- package/src/checkout.ts +109 -0
- package/src/decode.ts +99 -0
- package/src/derive.ts +188 -0
- package/src/errors.ts +60 -0
- package/src/guards.ts +42 -0
- package/src/identity.ts +93 -0
- package/src/index.ts +43 -0
- package/src/inventory.ts +55 -0
- package/src/load.ts +186 -0
- package/src/net.ts +29 -0
- package/src/overrides.ts +79 -0
- package/src/pins.ts +40 -0
- package/src/primitives.ts +139 -0
- package/src/references.ts +82 -0
- package/src/schema.ts +144 -0
- package/src/tokens.ts +60 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
var VERSION = "0.1.0";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
class SiteError extends Error {
|
|
6
|
+
name = "SiteError";
|
|
7
|
+
code;
|
|
8
|
+
issues;
|
|
9
|
+
constructor(code, message, issues = []) {
|
|
10
|
+
super(issues.length === 0 ? message : `${message}
|
|
11
|
+
${issues.join(`
|
|
12
|
+
`)}`);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function unknownKey(kind, key, declared) {
|
|
18
|
+
const known = [...declared].sort();
|
|
19
|
+
const list = known.length === 0 ? "none are declared" : `declared: ${known.join(", ")}`;
|
|
20
|
+
throw new SiteError("unknown-key", `unknown ${kind} "${key}" (${list})`);
|
|
21
|
+
}
|
|
22
|
+
function lookup(kind, record, key) {
|
|
23
|
+
if (!Object.hasOwn(record, key))
|
|
24
|
+
unknownKey(kind, key, Object.keys(record));
|
|
25
|
+
return record[key];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/schema.ts
|
|
29
|
+
import * as Effect from "effect/Effect";
|
|
30
|
+
import * as Schema2 from "effect/Schema";
|
|
31
|
+
|
|
32
|
+
// src/primitives.ts
|
|
33
|
+
import * as Schema from "effect/Schema";
|
|
34
|
+
var Label = Schema.String.check(Schema.isPattern(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, {
|
|
35
|
+
message: "Expected a lowercase DNS label (a-z, 0-9, inner hyphens, at most 63)"
|
|
36
|
+
}));
|
|
37
|
+
var Hostname = Schema.String.check(Schema.isPattern(/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/, { message: "Expected a lowercase fully qualified hostname such as example.com" }));
|
|
38
|
+
var CertificateName = Schema.String.check(Schema.isPattern(/^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/, { message: "Expected a hostname, optionally with one leading *. wildcard" }));
|
|
39
|
+
var OCTET = "(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])";
|
|
40
|
+
var Ipv4 = Schema.String.check(Schema.isPattern(new RegExp(`^(${OCTET}\\.){3}${OCTET}$`), {
|
|
41
|
+
message: "Expected an IPv4 address such as 192.0.2.10"
|
|
42
|
+
}));
|
|
43
|
+
function parseCidr(cidr) {
|
|
44
|
+
const match = new RegExp(`^((${OCTET}\\.){3}${OCTET})/([0-9]{1,2})$`).exec(cidr);
|
|
45
|
+
if (match === null)
|
|
46
|
+
return;
|
|
47
|
+
const prefix = Number(match[match.length - 1]);
|
|
48
|
+
const base = (match[1] ?? "").split(".").reduce((acc, octet) => acc * 256 + Number(octet), 0);
|
|
49
|
+
return { base, prefix };
|
|
50
|
+
}
|
|
51
|
+
var Ipv4Cidr = Schema.String.check(Schema.makeFilter((value) => {
|
|
52
|
+
const parsed = parseCidr(value);
|
|
53
|
+
if (parsed === undefined)
|
|
54
|
+
return "Expected an IPv4 network such as 192.0.2.0/24";
|
|
55
|
+
if (parsed.prefix < 8 || parsed.prefix > 30)
|
|
56
|
+
return "Expected a prefix between /8 and /30";
|
|
57
|
+
const hostBits = 2 ** (32 - parsed.prefix);
|
|
58
|
+
if (parsed.base % hostBits !== 0)
|
|
59
|
+
return "Expected the network address (host bits zero)";
|
|
60
|
+
return;
|
|
61
|
+
}, { expected: "an IPv4 network" }));
|
|
62
|
+
var HostNumber = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 16777214 }));
|
|
63
|
+
var Port = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 65535 }));
|
|
64
|
+
var AccountId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/, {
|
|
65
|
+
message: "Expected a 32-character lowercase hex account id"
|
|
66
|
+
}));
|
|
67
|
+
var GithubOwner = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/, {
|
|
68
|
+
message: "Expected a GitHub login (letters, digits, inner hyphens)"
|
|
69
|
+
}));
|
|
70
|
+
var AbsolutePath = Schema.String.check(Schema.isPattern(/^(\/[^/\0]+)+$/, {
|
|
71
|
+
message: "Expected an absolute path without a trailing slash"
|
|
72
|
+
}));
|
|
73
|
+
var SemVer = Schema.String.check(Schema.isPattern(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, { message: "Expected a semver version" }));
|
|
74
|
+
var PinKey = Schema.String.check(Schema.isPattern(/^[a-z0-9]+([._-][a-z0-9]+)*$/, {
|
|
75
|
+
message: "Expected a lowercase dotted key such as alerts.d1"
|
|
76
|
+
}));
|
|
77
|
+
var PhysicalName = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
|
|
78
|
+
message: "Expected a resource name (letters, digits, dot, dash, underscore)"
|
|
79
|
+
}));
|
|
80
|
+
var ListEntry = Schema.String.check(Schema.isPattern(/^[^\s,]+$/, { message: "Expected one entry with no commas or whitespace" }));
|
|
81
|
+
var NonBlank = Schema.String.check(Schema.isPattern(/^\S(.*\S)?$/, { message: "Expected a non-blank value without edge spaces" }));
|
|
82
|
+
|
|
83
|
+
// src/schema.ts
|
|
84
|
+
var SITE_FORMAT = 1;
|
|
85
|
+
var emptyRecord = (schema) => schema.pipe(Schema2.withDecodingDefaultKey(Effect.succeed({})));
|
|
86
|
+
var Scheme = Schema2.Literals(["http", "https"]);
|
|
87
|
+
var Host = Schema2.Struct({
|
|
88
|
+
zone: Schema2.optionalKey(Label),
|
|
89
|
+
aliases: Schema2.optionalKey(Schema2.Array(Label)),
|
|
90
|
+
legs: Schema2.optionalKey(Schema2.Record(Label, HostNumber))
|
|
91
|
+
});
|
|
92
|
+
var Product = Schema2.Struct({
|
|
93
|
+
label: Schema2.optionalKey(Label),
|
|
94
|
+
domain: Schema2.optionalKey(Hostname)
|
|
95
|
+
});
|
|
96
|
+
var Service = Schema2.Struct({ host: Label, port: Port, scheme: Scheme });
|
|
97
|
+
var Account = Schema2.Struct({
|
|
98
|
+
id: AccountId,
|
|
99
|
+
zones: Schema2.Array(Hostname),
|
|
100
|
+
surfaces: Schema2.Array(Label)
|
|
101
|
+
});
|
|
102
|
+
var Vault = Schema2.Struct({
|
|
103
|
+
label: Label,
|
|
104
|
+
apiLabel: Label,
|
|
105
|
+
clusterName: NonBlank,
|
|
106
|
+
namespace: Schema2.String.pipe(Schema2.withDecodingDefaultKey(Effect.succeed(""))),
|
|
107
|
+
port: Port.pipe(Schema2.withDecodingDefaultKey(Effect.succeed(8200))),
|
|
108
|
+
meshAddress: Ipv4,
|
|
109
|
+
lan: Schema2.Struct({ host: Label, port: Port, scheme: Scheme }),
|
|
110
|
+
oidcMount: Label.pipe(Schema2.withDecodingDefaultKey(Effect.succeed("oidc"))),
|
|
111
|
+
cliCallbackPort: Port.pipe(Schema2.withDecodingDefaultKey(Effect.succeed(8250)))
|
|
112
|
+
});
|
|
113
|
+
var Pinned = Schema2.Struct({
|
|
114
|
+
names: emptyRecord(Schema2.Record(PinKey, PhysicalName)),
|
|
115
|
+
certificates: emptyRecord(Schema2.Record(PinKey, Schema2.NonEmptyArray(CertificateName))),
|
|
116
|
+
adopted: emptyRecord(Schema2.Record(PinKey, NonBlank)),
|
|
117
|
+
policies: emptyRecord(Schema2.Record(PinKey, Schema2.Array(ListEntry))),
|
|
118
|
+
sshPrincipals: emptyRecord(Schema2.Record(PinKey, Schema2.Array(ListEntry)))
|
|
119
|
+
});
|
|
120
|
+
var SiteSchema = Schema2.Struct({
|
|
121
|
+
version: Schema2.Literal(SITE_FORMAT),
|
|
122
|
+
deriveVersion: SemVer,
|
|
123
|
+
kind: Schema2.Literals(["live", "testing", "example"]),
|
|
124
|
+
apex: Hostname,
|
|
125
|
+
zones: Schema2.StructWithRest(Schema2.Struct({ mgmt: Label }), [Schema2.Record(Label, Label)]),
|
|
126
|
+
networks: Schema2.Record(Label, Ipv4Cidr),
|
|
127
|
+
hosts: Schema2.Record(Label, Host),
|
|
128
|
+
clusters: emptyRecord(Schema2.Record(Label, Schema2.Struct({ members: Schema2.Array(Label) }))),
|
|
129
|
+
products: emptyRecord(Schema2.Record(Label, Product)),
|
|
130
|
+
services: emptyRecord(Schema2.Record(Label, Service)),
|
|
131
|
+
cloudflare: Schema2.Struct({
|
|
132
|
+
accounts: Schema2.Record(Label, Account),
|
|
133
|
+
access: Schema2.Struct({ team: Label })
|
|
134
|
+
}),
|
|
135
|
+
github: Schema2.Struct({ owner: GithubOwner }),
|
|
136
|
+
paths: Schema2.Struct({ estateRoot: AbsolutePath }),
|
|
137
|
+
vault: Vault,
|
|
138
|
+
pinned: Pinned.pipe(Schema2.withDecodingDefaultKey(Effect.succeed({})))
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/guards.ts
|
|
142
|
+
function checkDeriveVersion(site, installed) {
|
|
143
|
+
if (site.deriveVersion === installed)
|
|
144
|
+
return;
|
|
145
|
+
throw new SiteError("derive-version", `the site file was reviewed against @homeflare/site ${site.deriveVersion}, ` + `but ${installed} is installed. Read the CHANGELOG between them for derive changes, ` + `then set "deriveVersion": "${installed}" — or install ${site.deriveVersion}.`);
|
|
146
|
+
}
|
|
147
|
+
function assertStage(site, stage) {
|
|
148
|
+
if (stage !== "live" || site.kind === "live")
|
|
149
|
+
return;
|
|
150
|
+
throw new SiteError("stage", `stage "live" needs a live site; this one is kind "${site.kind}". ` + `Point HF_SITE_FILE at the live site file, or plan a non-live stage.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/decode.ts
|
|
154
|
+
import * as Cause from "effect/Cause";
|
|
155
|
+
import * as Exit from "effect/Exit";
|
|
156
|
+
import * as Schema3 from "effect/Schema";
|
|
157
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
158
|
+
|
|
159
|
+
// src/net.ts
|
|
160
|
+
function dotted(value) {
|
|
161
|
+
return [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join(".");
|
|
162
|
+
}
|
|
163
|
+
function lastHostNumber(cidr) {
|
|
164
|
+
const parsed = parseCidr(cidr);
|
|
165
|
+
if (parsed === undefined)
|
|
166
|
+
throw new Error(`not an IPv4 network: ${cidr}`);
|
|
167
|
+
return 2 ** (32 - parsed.prefix) - 2;
|
|
168
|
+
}
|
|
169
|
+
function addressOn(cidr, hostNumber) {
|
|
170
|
+
const parsed = parseCidr(cidr);
|
|
171
|
+
if (parsed === undefined)
|
|
172
|
+
throw new Error(`not an IPv4 network: ${cidr}`);
|
|
173
|
+
if (hostNumber < 1 || hostNumber > lastHostNumber(cidr)) {
|
|
174
|
+
throw new Error(`host number ${hostNumber} does not fit ${cidr}`);
|
|
175
|
+
}
|
|
176
|
+
return dotted(parsed.base + hostNumber);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/references.ts
|
|
180
|
+
var APEX_ZONE = "apex";
|
|
181
|
+
function hostHasZone(site, host) {
|
|
182
|
+
return Object.hasOwn(site.hosts, host) && site.hosts[host]?.zone !== undefined;
|
|
183
|
+
}
|
|
184
|
+
function checkHosts(site, issues) {
|
|
185
|
+
for (const [key, host] of Object.entries(site.hosts)) {
|
|
186
|
+
const zone = host.zone;
|
|
187
|
+
if (zone !== undefined && zone !== APEX_ZONE && !Object.hasOwn(site.zones, zone)) {
|
|
188
|
+
issues.push(`hosts.${key}.zone: "${zone}" is not a declared zone or "${APEX_ZONE}"`);
|
|
189
|
+
}
|
|
190
|
+
for (const [network, number] of Object.entries(host.legs ?? {})) {
|
|
191
|
+
const cidr = site.networks[network];
|
|
192
|
+
if (cidr === undefined) {
|
|
193
|
+
issues.push(`hosts.${key}.legs.${network}: "${network}" is not a declared network`);
|
|
194
|
+
} else if (number > lastHostNumber(cidr)) {
|
|
195
|
+
issues.push(`hosts.${key}.legs.${network}: host number ${number} does not fit ${network}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function checkZonedHost(site, path, host, issues) {
|
|
201
|
+
if (!Object.hasOwn(site.hosts, host)) {
|
|
202
|
+
issues.push(`${path}: "${host}" is not a declared host`);
|
|
203
|
+
} else if (!hostHasZone(site, host)) {
|
|
204
|
+
issues.push(`${path}: host "${host}" has no zone, so it has no hostname`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function checkAccounts(site, issues) {
|
|
208
|
+
for (const [alias, account] of Object.entries(site.cloudflare.accounts)) {
|
|
209
|
+
const seen = new Set;
|
|
210
|
+
for (const surface of account.surfaces) {
|
|
211
|
+
if (seen.has(surface)) {
|
|
212
|
+
issues.push(`cloudflare.accounts.${alias}.surfaces: "${surface}" is listed twice`);
|
|
213
|
+
}
|
|
214
|
+
seen.add(surface);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function referenceIssues(site) {
|
|
219
|
+
const issues = [];
|
|
220
|
+
if (Object.hasOwn(site.zones, APEX_ZONE)) {
|
|
221
|
+
issues.push(`zones.${APEX_ZONE}: reserved — a host with zone "${APEX_ZONE}" sits on the apex`);
|
|
222
|
+
}
|
|
223
|
+
checkHosts(site, issues);
|
|
224
|
+
for (const [key, cluster] of Object.entries(site.clusters)) {
|
|
225
|
+
for (const [i, member] of cluster.members.entries()) {
|
|
226
|
+
checkZonedHost(site, `clusters.${key}.members.${i}`, member, issues);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [key, product] of Object.entries(site.products)) {
|
|
230
|
+
if (product.label !== undefined && product.domain !== undefined) {
|
|
231
|
+
issues.push(`products.${key}: set label OR domain, not both`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const [key, service] of Object.entries(site.services)) {
|
|
235
|
+
checkZonedHost(site, `services.${key}.host`, service.host, issues);
|
|
236
|
+
}
|
|
237
|
+
checkZonedHost(site, "vault.lan.host", site.vault.lan.host, issues);
|
|
238
|
+
checkAccounts(site, issues);
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/decode.ts
|
|
243
|
+
var strict = Schema3.decodeUnknownExit(SiteSchema, { onExcessProperty: "error", errors: "all" });
|
|
244
|
+
var standard = SchemaIssue.makeFormatterStandardSchemaV1();
|
|
245
|
+
var EXCESS = "Expected no excess property";
|
|
246
|
+
function joinPath(segments) {
|
|
247
|
+
return segments.map((segment, i) => {
|
|
248
|
+
if (segment.includes("."))
|
|
249
|
+
return `[${JSON.stringify(segment)}]`;
|
|
250
|
+
return i === 0 ? segment : `.${segment}`;
|
|
251
|
+
}).join("");
|
|
252
|
+
}
|
|
253
|
+
function formatIssues(issue, skip) {
|
|
254
|
+
return standard(issue).issues.map((entry) => {
|
|
255
|
+
const segments = (entry.path ?? []).map((part) => typeof part === "object" && part !== null ? String(part.key) : String(part));
|
|
256
|
+
if (skip !== undefined && segments[0] === skip)
|
|
257
|
+
segments.shift();
|
|
258
|
+
const path = segments.length === 0 ? "(root)" : joinPath(segments);
|
|
259
|
+
const message = entry.message === EXCESS ? "unknown key (a misspelt field, or a record key that is not a lowercase label)" : entry.message;
|
|
260
|
+
return `${path}: ${message}`;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function decodeStrict(input) {
|
|
264
|
+
const exit = strict(input);
|
|
265
|
+
if (Exit.isSuccess(exit))
|
|
266
|
+
return exit.value;
|
|
267
|
+
const error = Cause.squash(exit.cause);
|
|
268
|
+
if (Schema3.isSchemaError(error)) {
|
|
269
|
+
throw new SiteError("decode", "the site file does not match the schema", formatIssues(error.issue));
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
function validateSite(site, options = {}) {
|
|
274
|
+
const issues = referenceIssues(site);
|
|
275
|
+
if (issues.length > 0) {
|
|
276
|
+
throw new SiteError("reference", "the site file names things it does not declare", issues);
|
|
277
|
+
}
|
|
278
|
+
checkDeriveVersion(site, options.installed ?? VERSION);
|
|
279
|
+
return site;
|
|
280
|
+
}
|
|
281
|
+
function decodeSite(input, options = {}) {
|
|
282
|
+
return validateSite(decodeStrict(input), options);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { VERSION, SiteError, unknownKey, lookup, SITE_FORMAT, SiteSchema, checkDeriveVersion, assertStage, addressOn, APEX_ZONE, joinPath, formatIssues, decodeStrict, validateSite, decodeSite };
|
|
286
|
+
|
|
287
|
+
//# debugId=F23FD3C4E59FE45C64756E2164756E21
|
|
288
|
+
//# sourceMappingURL=index-dvzn0279.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/version.ts", "../src/errors.ts", "../src/schema.ts", "../src/primitives.ts", "../src/guards.ts", "../src/decode.ts", "../src/net.ts", "../src/references.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.0';\n",
|
|
6
|
+
"/**\n * The one error this package throws.\n *\n * ★ ONE CLASS, A `code` TO SWITCH ON. Callers mostly print the message and stop; the few\n * that branch (a scaffold harness skipping `derive-version` in a dry run, say) branch on\n * `code`, which is stable, rather than on message text, which is not.\n *\n * ⛔ Messages name paths, keys and the fix — never a value from a live file beyond the\n * one being complained about. A site file is private, and an error string ends up in CI\n * logs.\n */\n\nexport type SiteErrorCode =\n /** The file does not match the schema. `issues` lists `path: problem`. */\n | 'decode'\n /** A value points at something the file does not declare (a zone, host, network). */\n | 'reference'\n /** A consumer asked for a product, service, zone, host or pin nobody declared. */\n | 'unknown-key'\n /** The file was reviewed against a different `@homeflare/site` version. */\n | 'derive-version'\n /** A non-live site was asked to plan stage `live`. */\n | 'stage'\n /** The observed vault or account is not the one the site describes. */\n | 'identity'\n /** The file could not be located, read or parsed. */\n | 'load'\n /** The file is not committed on `main` and `siteDev` was not set. */\n | 'checkout'\n /** An environment override was refused. */\n | 'override';\n\nexport class SiteError extends Error {\n override readonly name = 'SiteError';\n readonly code: SiteErrorCode;\n /** One line per problem, each naming its path. Empty when the message says it all. */\n readonly issues: readonly string[];\n\n constructor(code: SiteErrorCode, message: string, issues: readonly string[] = []) {\n super(issues.length === 0 ? message : `${message}\\n ${issues.join('\\n ')}`);\n this.code = code;\n this.issues = issues;\n }\n}\n\n/** Throw `unknown-key`, listing what IS declared so the fix is one glance away. */\nexport function unknownKey(kind: string, key: string, declared: Iterable<string>): never {\n const known = [...declared].sort();\n const list = known.length === 0 ? 'none are declared' : `declared: ${known.join(', ')}`;\n throw new SiteError('unknown-key', `unknown ${kind} \"${key}\" (${list})`);\n}\n\n/**\n * Look a key up in a record, or throw `unknown-key`.\n * ⛔ No fallback, ever. A plausible default host is how a typo becomes a live DNS record.\n */\nexport function lookup<V>(kind: string, record: Readonly<Record<string, V>>, key: string): V {\n if (!Object.hasOwn(record, key)) unknownKey(kind, key, Object.keys(record));\n return record[key] as V;\n}\n",
|
|
7
|
+
"/**\n * The site file: BASE VALUES and PINNED values, nothing derived.\n *\n * ★ BASE VALUES are the few things every name is relative to — the apex, a handful of\n * labels, networks and hosts. `derive()` builds hostnames and addresses from them, so\n * swapping the apex changes every derived name by substitution and nothing else.\n *\n * ⛔ PINNED VALUES ARE NEVER DERIVED. Physical names (Workers, D1, KV, R2, Durable Objects)\n * follow no rule, and deriving one would REPLACE it: a derived D1 name deletes the data\n * behind the old one. Certificate hostnames on adopted certificates reissue and revoke\n * the certificate a host is still serving if they change. Adopted ids name objects that\n * already exist. Policy and SSH principal lists are explicit so a break-glass principal\n * can never vanish because a network leg was renamed.\n *\n * ⛔ UNKNOWN KEYS ARE REFUSED (see decode.ts). A misspelt optional field would otherwise\n * decode as \"absent\" and the file would look valid while saying less than its author\n * meant.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Schema from 'effect/Schema';\nimport {\n AbsolutePath,\n AccountId,\n CertificateName,\n GithubOwner,\n HostNumber,\n Hostname,\n Ipv4,\n Ipv4Cidr,\n Label,\n ListEntry,\n NonBlank,\n PhysicalName,\n PinKey,\n Port,\n SemVer,\n} from './primitives.ts';\n\n/** The site file format this package reads. A new format is a new literal, never a reuse. */\nexport const SITE_FORMAT = 1;\n\nconst emptyRecord = <S extends Schema.Top>(schema: S) =>\n schema.pipe(Schema.withDecodingDefaultKey(Effect.succeed({})));\n\nconst Scheme = Schema.Literals(['http', 'https']);\n\n/**\n * A machine. Its FQDN is `<key>.<zone>`; its address on a network is that network's base\n * plus `legs[network]`.\n * ★ `zone: \"apex\"` places the host directly under the apex (`docs.<apex>`).\n */\nconst Host = Schema.Struct({\n zone: Schema.optionalKey(Label),\n aliases: Schema.optionalKey(Schema.Array(Label)),\n legs: Schema.optionalKey(Schema.Record(Label, HostNumber)),\n});\n\n/** A public product: `<label ?? key>.<apex>`, or its own `domain`. Never both. */\nconst Product = Schema.Struct({\n label: Schema.optionalKey(Label),\n domain: Schema.optionalKey(Hostname),\n});\n\n/** An internal endpoint on a declared host. ⛔ The scheme is explicit, never guessed. */\nconst Service = Schema.Struct({ host: Label, port: Port, scheme: Scheme });\n\nconst Account = Schema.Struct({\n id: AccountId,\n zones: Schema.Array(Hostname),\n /** Each surface becomes an OpenBao mount `cloudflare-<alias>-<surface>`. */\n surfaces: Schema.Array(Label),\n});\n\nconst Vault = Schema.Struct({\n /** `<vault-host>` = `<label>.<apex>`. */\n label: Label,\n /**\n * `<vault-api-host>` = `<apiLabel>.<vault-host>`.\n * ⚠️ NOT `vaultApi` beside a `vault` label. Measured 2026-09-21 on effect 4.0.0-rc.115:\n * constant-cased env names nest by prefix, so `HF_SITE_VAULT_API` made the provider\n * read `vault` as a record and the whole decode failed \"Expected string\". No field\n * name here may be another's name plus `_…`; tests/overrides.test.ts asserts it.\n */\n apiLabel: Label,\n /**\n * ⛔ PINNED IDENTITY: the `cluster_name` the vault reports on `sys/health`. Set at init,\n * never derived; plans compare it before touching anything (see identity.ts).\n */\n clusterName: NonBlank,\n namespace: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed(''))),\n port: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(8200))),\n /** The vault's address on the private Mesh path. Machines use it; strict TLS applies. */\n meshAddress: Ipv4,\n /** The LAN pass-through proxy: a declared host, its port, and its scheme. */\n lan: Schema.Struct({ host: Label, port: Port, scheme: Scheme }),\n oidcMount: Label.pipe(Schema.withDecodingDefaultKey(Effect.succeed('oidc'))),\n /** The port `bao login -method=oidc` listens on for its localhost callback. */\n cliCallbackPort: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(8250))),\n});\n\nconst Pinned = Schema.Struct({\n names: emptyRecord(Schema.Record(PinKey, PhysicalName)),\n certificates: emptyRecord(Schema.Record(PinKey, Schema.NonEmptyArray(CertificateName))),\n adopted: emptyRecord(Schema.Record(PinKey, NonBlank)),\n policies: emptyRecord(Schema.Record(PinKey, Schema.Array(ListEntry))),\n sshPrincipals: emptyRecord(Schema.Record(PinKey, Schema.Array(ListEntry))),\n});\n\nexport const SiteSchema = Schema.Struct({\n /** The file format. Only {@link SITE_FORMAT} decodes. */\n version: Schema.Literal(SITE_FORMAT),\n /**\n * The `@homeflare/site` version whose derive rules this file was reviewed against.\n * ⛔ A repo refuses to load when its installed package differs (see guards.ts), so a\n * derive change can never rename live objects without someone bumping this line.\n */\n deriveVersion: SemVer,\n /** ⛔ Only a `live` site may plan stage `live` (see guards.ts). */\n kind: Schema.Literals(['live', 'testing', 'example']),\n apex: Hostname,\n /**\n * Zone suffixes under the apex: `{ \"mgmt\": \"mgmt\" }` gives `mgmt.<apex>`.\n * ⛔ `mgmt` is required (it is `<mgmt-zone>`), and the key `apex` is reserved.\n */\n zones: Schema.StructWithRest(Schema.Struct({ mgmt: Label }), [Schema.Record(Label, Label)]),\n networks: Schema.Record(Label, Ipv4Cidr),\n hosts: Schema.Record(Label, Host),\n clusters: emptyRecord(Schema.Record(Label, Schema.Struct({ members: Schema.Array(Label) }))),\n products: emptyRecord(Schema.Record(Label, Product)),\n services: emptyRecord(Schema.Record(Label, Service)),\n cloudflare: Schema.Struct({\n accounts: Schema.Record(Label, Account),\n access: Schema.Struct({ team: Label }),\n }),\n github: Schema.Struct({ owner: GithubOwner }),\n paths: Schema.Struct({ estateRoot: AbsolutePath }),\n vault: Vault,\n pinned: Pinned.pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),\n});\n\n/** A decoded site: defaults applied, every value checked. */\nexport type Site = typeof SiteSchema.Type;\n/** What a site file may contain before defaults are applied. */\nexport type SiteInput = typeof SiteSchema.Encoded;\n",
|
|
8
|
+
"/**\n * The value shapes a site file is made of. Each one refuses at decode time, naming its path,\n * so a typo fails when the file is read rather than when a plan renames something.\n *\n * ⛔ LOWERCASE ONLY for DNS names. Cloudflare, OpenBao and ssh compare names as strings in\n * places, and `Mgmt.Example.com` is not `mgmt.example.com` to a principal list.\n */\nimport * as Schema from 'effect/Schema';\n\n/** One DNS label: `v`, `mgmt`, `n1`. Also the shape of every key a consumer looks up. */\nexport const Label = Schema.String.check(\n Schema.isPattern(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, {\n message: 'Expected a lowercase DNS label (a-z, 0-9, inner hyphens, at most 63)',\n }),\n);\n\n/** A fully qualified name with at least two labels: `example.com`, `shop.example.net`. */\nexport const Hostname = Schema.String.check(\n Schema.isPattern(\n /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/,\n { message: 'Expected a lowercase fully qualified hostname such as example.com' },\n ),\n);\n\n/** A certificate name: a hostname, or one leading wildcard label. */\nexport const CertificateName = Schema.String.check(\n Schema.isPattern(\n /^(\\*\\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/,\n { message: 'Expected a hostname, optionally with one leading *. wildcard' },\n ),\n);\n\nconst OCTET = '(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])';\n\n/** A dotted-quad IPv4 address, every octet 0–255. */\nexport const Ipv4 = Schema.String.check(\n Schema.isPattern(new RegExp(`^(${OCTET}\\\\.){3}${OCTET}$`), {\n message: 'Expected an IPv4 address such as 192.0.2.10',\n }),\n);\n\n/** Parse `a.b.c.d/p` into a 32-bit base and a prefix length. `undefined` when malformed. */\nexport function parseCidr(cidr: string): { base: number; prefix: number } | undefined {\n const match = new RegExp(`^((${OCTET}\\\\.){3}${OCTET})/([0-9]{1,2})$`).exec(cidr);\n if (match === null) return undefined;\n const prefix = Number(match[match.length - 1]);\n const base = (match[1] ?? '').split('.').reduce((acc, octet) => acc * 256 + Number(octet), 0);\n return { base, prefix };\n}\n\n/**\n * An IPv4 network, `/8` to `/30`, written at its network address.\n *\n * ⚠️ `192.0.2.5/24` is refused, not normalised. A host address with a prefix usually means\n * someone pasted an interface address where a network belongs; normalising it would hide\n * the mistake and derive every leg address from the wrong idea of the network.\n */\nexport const Ipv4Cidr = Schema.String.check(\n Schema.makeFilter(\n (value: string) => {\n const parsed = parseCidr(value);\n if (parsed === undefined) return 'Expected an IPv4 network such as 192.0.2.0/24';\n if (parsed.prefix < 8 || parsed.prefix > 30) return 'Expected a prefix between /8 and /30';\n const hostBits = 2 ** (32 - parsed.prefix);\n if (parsed.base % hostBits !== 0) return 'Expected the network address (host bits zero)';\n return undefined;\n },\n { expected: 'an IPv4 network' },\n ),\n);\n\n/** A host's number on a network: `.11` on a /24 is 11. Checked against the prefix later. */\nexport const HostNumber = Schema.Number.check(\n Schema.isInt(),\n Schema.isBetween({ minimum: 1, maximum: 16_777_214 }),\n);\n\nexport const Port = Schema.Number.check(\n Schema.isInt(),\n Schema.isBetween({ minimum: 1, maximum: 65_535 }),\n);\n\n/** A Cloudflare account id: 32 lowercase hex characters. */\nexport const AccountId = Schema.String.check(\n Schema.isPattern(/^[0-9a-f]{32}$/, {\n message: 'Expected a 32-character lowercase hex account id',\n }),\n);\n\n/** A GitHub user or organisation login. GitHub itself allows uppercase here. */\nexport const GithubOwner = Schema.String.check(\n Schema.isPattern(/^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/, {\n message: 'Expected a GitHub login (letters, digits, inner hyphens)',\n }),\n);\n\n/** An absolute POSIX path with no trailing slash: `/opt/example`. */\nexport const AbsolutePath = Schema.String.check(\n Schema.isPattern(/^(\\/[^/\\0]+)+$/, {\n message: 'Expected an absolute path without a trailing slash',\n }),\n);\n\n/** A semver version, as npm writes one: `0.1.0`, `1.0.0-rc.1`. */\nexport const SemVer = Schema.String.check(\n Schema.isPattern(/^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$/, { message: 'Expected a semver version' }),\n);\n\n/** A key into a `pinned` table: `alerts.d1`, `ssh-host.host`. Dotted, lowercase. */\nexport const PinKey = Schema.String.check(\n Schema.isPattern(/^[a-z0-9]+([._-][a-z0-9]+)*$/, {\n message: 'Expected a lowercase dotted key such as alerts.d1',\n }),\n);\n\n/**\n * A physical resource name: Worker, D1, KV, R2, Durable Object class.\n * ★ Case is allowed because Durable Object bindings name PascalCase classes.\n */\nexport const PhysicalName = Schema.String.check(\n Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {\n message: 'Expected a resource name (letters, digits, dot, dash, underscore)',\n }),\n);\n\n/**\n * One SSH principal or policy entry.\n * ⛔ No commas and no whitespace. OpenBao stores principal lists comma-joined, so an entry\n * holding a comma silently becomes two principals, and a stray space becomes a name no\n * host will ever present.\n */\nexport const ListEntry = Schema.String.check(\n Schema.isPattern(/^[^\\s,]+$/, { message: 'Expected one entry with no commas or whitespace' }),\n);\n\n/** A free-form identifier that must not be blank: cluster names, adopted ids, CNs. */\nexport const NonBlank = Schema.String.check(\n Schema.isPattern(/^\\S(.*\\S)?$/, { message: 'Expected a non-blank value without edge spaces' }),\n);\n",
|
|
9
|
+
"/**\n * Guards that refuse a plan before it can touch the wrong thing.\n *\n * ⛔ EACH ONE FAILS CLOSED. A guard that warns is a guard nobody reads in a CI log.\n */\nimport { SiteError } from './errors.ts';\nimport type { Site } from './schema.ts';\n\n/**\n * Refuse a site reviewed against a different `@homeflare/site` than the one installed.\n *\n * ★ WHY EXACT EQUALITY. Derive rules are this package's behaviour. If a new version builds\n * one hostname differently, every repo on it would plan a rename — a REPLACE — without\n * anyone having looked. Holding `deriveVersion` equal to the installed version makes that\n * a deliberate, reviewed one-line bump in the site file instead.\n */\nexport function checkDeriveVersion(site: Site, installed: string): void {\n if (site.deriveVersion === installed) return;\n throw new SiteError(\n 'derive-version',\n `the site file was reviewed against @homeflare/site ${site.deriveVersion}, ` +\n `but ${installed} is installed. Read the CHANGELOG between them for derive changes, ` +\n `then set \"deriveVersion\": \"${installed}\" — or install ${site.deriveVersion}.`,\n );\n}\n\n/** Stages as a deploy tool names them. Only `live` is special here. */\nexport type Stage = string;\n\n/**\n * ⛔ STAGE `live` ONLY WITH A LIVE SITE. An example or testing site can never plan\n * against live state — its account ids and cluster name are placeholders, and a plan\n * against real state would read every live object as \"not declared: delete\".\n */\nexport function assertStage(site: Site, stage: Stage): void {\n if (stage !== 'live' || site.kind === 'live') return;\n throw new SiteError(\n 'stage',\n `stage \"live\" needs a live site; this one is kind \"${site.kind}\". ` +\n `Point HF_SITE_FILE at the live site file, or plan a non-live stage.`,\n );\n}\n",
|
|
10
|
+
"/**\n * Decode a site from plain data. Runtime-neutral: no filesystem, no environment.\n *\n * ★ Workers and scripts that already hold the JSON (a binding, a fetched file) call\n * `decodeSite`. Scripts that read `HF_SITE_FILE` call `loadSite` from\n * `@homeflare/site/load`, which ends up here too.\n */\nimport * as Cause from 'effect/Cause';\nimport * as Exit from 'effect/Exit';\nimport * as Schema from 'effect/Schema';\nimport * as SchemaIssue from 'effect/SchemaIssue';\nimport { SiteError } from './errors.ts';\nimport { checkDeriveVersion } from './guards.ts';\nimport { referenceIssues } from './references.ts';\nimport { type Site, SiteSchema } from './schema.ts';\nimport { VERSION } from './version.ts';\n\n/**\n * ⛔ `onExcessProperty: 'error'` IS LOAD-BEARING, twice over. Measured 2026-09-21 on\n * effect 4.0.0-rc.115: with the default (`ignore`), a misspelt field is dropped, AND a\n * record entry whose key fails the key schema (`\"Mgmt\"` in `networks`) is dropped too —\n * the file decodes, one network short, with no error at all.\n * ⚠️ With it on, that bad record key reports as \"excess property\", which reads oddly.\n * `formatIssues` rewrites it to say what it actually means.\n */\nconst strict = Schema.decodeUnknownExit(SiteSchema, { onExcessProperty: 'error', errors: 'all' });\nconst standard = SchemaIssue.makeFormatterStandardSchemaV1();\n\nconst EXCESS = 'Expected no excess property';\n\n/**\n * `pinned.certificates[\"vault.origin\"].0` — a key that itself holds a dot is bracketed,\n * or the path would read as two levels that do not exist.\n */\nexport function joinPath(segments: readonly string[]): string {\n return segments\n .map((segment, i) => {\n if (segment.includes('.')) return `[${JSON.stringify(segment)}]`;\n return i === 0 ? segment : `.${segment}`;\n })\n .join('');\n}\n\n/** Render a schema issue as `path: problem` lines, dropping a leading `skip` segment. */\nexport function formatIssues(issue: SchemaIssue.Issue, skip?: string): readonly string[] {\n return standard(issue).issues.map((entry) => {\n const segments = (entry.path ?? []).map((part) =>\n typeof part === 'object' && part !== null ? String(part.key) : String(part),\n );\n if (skip !== undefined && segments[0] === skip) segments.shift();\n const path = segments.length === 0 ? '(root)' : joinPath(segments);\n const message =\n entry.message === EXCESS\n ? 'unknown key (a misspelt field, or a record key that is not a lowercase label)'\n : entry.message;\n return `${path}: ${message}`;\n });\n}\n\n/** Schema-only decode. Throws `decode` listing every problem, each with its path. */\nexport function decodeStrict(input: unknown): Site {\n const exit = strict(input);\n if (Exit.isSuccess(exit)) return exit.value;\n const error = Cause.squash(exit.cause);\n if (Schema.isSchemaError(error)) {\n throw new SiteError(\n 'decode',\n 'the site file does not match the schema',\n formatIssues(error.issue),\n );\n }\n throw error;\n}\n\nexport interface ValidateOptions {\n /**\n * The `@homeflare/site` version to hold `deriveVersion` against. Defaults to the\n * installed one — which is the point; override only to test the refusal.\n */\n readonly installed?: string;\n}\n\n/** Cross-field references, then the derive-version guard. Returns the same site. */\nexport function validateSite(site: Site, options: ValidateOptions = {}): Site {\n const issues = referenceIssues(site);\n if (issues.length > 0) {\n throw new SiteError('reference', 'the site file names things it does not declare', issues);\n }\n checkDeriveVersion(site, options.installed ?? VERSION);\n return site;\n}\n\n/**\n * Decode and validate a site from plain data (already-parsed JSON).\n * Throws {@link SiteError}: `decode`, `reference` or `derive-version`.\n */\nexport function decodeSite(input: unknown, options: ValidateOptions = {}): Site {\n return validateSite(decodeStrict(input), options);\n}\n",
|
|
11
|
+
"/**\n * IPv4 arithmetic for leg addresses: a network's base plus a host number.\n *\n * ★ WHY A HOST NUMBER AND NOT A LAST OCTET. On a /24 they are the same thing, and that is\n * the common case. On a /23 or a /16 a \"last octet\" is ambiguous, while \"the 11th\n * address after the network address\" is not. The /24 reading stays exact.\n */\nimport { parseCidr } from './primitives.ts';\n\nfunction dotted(value: number): string {\n return [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join('.');\n}\n\n/** The largest usable host number on a network (the broadcast address is excluded). */\nexport function lastHostNumber(cidr: string): number {\n const parsed = parseCidr(cidr);\n if (parsed === undefined) throw new Error(`not an IPv4 network: ${cidr}`);\n return 2 ** (32 - parsed.prefix) - 2;\n}\n\n/** `addressOn('192.0.2.0/24', 11)` is `192.0.2.11`. */\nexport function addressOn(cidr: string, hostNumber: number): string {\n const parsed = parseCidr(cidr);\n if (parsed === undefined) throw new Error(`not an IPv4 network: ${cidr}`);\n if (hostNumber < 1 || hostNumber > lastHostNumber(cidr)) {\n throw new Error(`host number ${hostNumber} does not fit ${cidr}`);\n }\n return dotted(parsed.base + hostNumber);\n}\n",
|
|
12
|
+
"/**\n * Checks that span fields: every zone, host and network a value names is declared.\n *\n * ★ WHY NOT IN THE SCHEMA. A struct-level filter reports one issue at the struct's own\n * path; these checks want to name the exact field (`services.grafana.host`), and they\n * want to report every broken reference at once rather than the first.\n */\nimport { lastHostNumber } from './net.ts';\nimport type { Site } from './schema.ts';\n\n/** The zone key that means the apex itself. Reserved: a site may not declare it. */\nexport const APEX_ZONE = 'apex';\n\nfunction hostHasZone(site: Site, host: string): boolean {\n return Object.hasOwn(site.hosts, host) && site.hosts[host]?.zone !== undefined;\n}\n\nfunction checkHosts(site: Site, issues: string[]): void {\n for (const [key, host] of Object.entries(site.hosts)) {\n const zone = host.zone;\n if (zone !== undefined && zone !== APEX_ZONE && !Object.hasOwn(site.zones, zone)) {\n issues.push(`hosts.${key}.zone: \"${zone}\" is not a declared zone or \"${APEX_ZONE}\"`);\n }\n for (const [network, number] of Object.entries(host.legs ?? {})) {\n const cidr = site.networks[network];\n if (cidr === undefined) {\n issues.push(`hosts.${key}.legs.${network}: \"${network}\" is not a declared network`);\n } else if (number > lastHostNumber(cidr)) {\n issues.push(`hosts.${key}.legs.${network}: host number ${number} does not fit ${network}`);\n }\n }\n }\n}\n\nfunction checkZonedHost(site: Site, path: string, host: string, issues: string[]): void {\n if (!Object.hasOwn(site.hosts, host)) {\n issues.push(`${path}: \"${host}\" is not a declared host`);\n } else if (!hostHasZone(site, host)) {\n issues.push(`${path}: host \"${host}\" has no zone, so it has no hostname`);\n }\n}\n\nfunction checkAccounts(site: Site, issues: string[]): void {\n for (const [alias, account] of Object.entries(site.cloudflare.accounts)) {\n const seen = new Set<string>();\n for (const surface of account.surfaces) {\n // ⚠️ A repeated surface derives the same mount name twice; the second declaration\n // would silently be the same mount, so a typo'd duplicate is refused here.\n if (seen.has(surface)) {\n issues.push(`cloudflare.accounts.${alias}.surfaces: \"${surface}\" is listed twice`);\n }\n seen.add(surface);\n }\n }\n}\n\n/** Every broken reference in `site`, each naming its path. Empty when the site is sound. */\nexport function referenceIssues(site: Site): readonly string[] {\n const issues: string[] = [];\n\n if (Object.hasOwn(site.zones, APEX_ZONE)) {\n issues.push(`zones.${APEX_ZONE}: reserved — a host with zone \"${APEX_ZONE}\" sits on the apex`);\n }\n checkHosts(site, issues);\n for (const [key, cluster] of Object.entries(site.clusters)) {\n for (const [i, member] of cluster.members.entries()) {\n checkZonedHost(site, `clusters.${key}.members.${i}`, member, issues);\n }\n }\n for (const [key, product] of Object.entries(site.products)) {\n if (product.label !== undefined && product.domain !== undefined) {\n issues.push(`products.${key}: set label OR domain, not both`);\n }\n }\n for (const [key, service] of Object.entries(site.services)) {\n checkZonedHost(site, `services.${key}.host`, service.host, issues);\n }\n checkZonedHost(site, 'vault.lan.host', site.vault.lan.host, issues);\n checkAccounts(site, issues);\n\n return issues;\n}\n"
|
|
13
|
+
],
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "F23FD3C4E59FE45C64756E2164756E21",
|
|
16
|
+
"names": []
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @homeflare/site — one typed site config, and every name derived from it.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY. A public stack that types a hostname cannot be run by anyone else, and a stack
|
|
5
|
+
* that re-derives a name its own way drifts from its siblings. Here the site file holds
|
|
6
|
+
* the base values once; `derive()` builds every hostname, address and mount name the
|
|
7
|
+
* same way in every repo; and names that must never follow a rule are PINNED.
|
|
8
|
+
*
|
|
9
|
+
* import { decodeSite, derive } from '@homeflare/site'; // runtime-neutral
|
|
10
|
+
* import { loadSite } from '@homeflare/site/load'; // Node / Bun: HF_SITE_FILE
|
|
11
|
+
*
|
|
12
|
+
* ⛔ RUNTIME-NEUTRAL. Nothing reachable from this file touches `node:*`, `bun:*`, the
|
|
13
|
+
* filesystem or the environment. File and env reading live in `./load.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export { VERSION } from './version.ts';
|
|
16
|
+
export { SiteError, type SiteErrorCode } from './errors.ts';
|
|
17
|
+
export { SITE_FORMAT, type Site, type SiteInput, SiteSchema } from './schema.ts';
|
|
18
|
+
export { type ValidateOptions, decodeSite, validateSite } from './decode.ts';
|
|
19
|
+
export { type Derived, type DerivedAccess, type DerivedHost, type DerivedVault, derive, } from './derive.ts';
|
|
20
|
+
export { type Pins, pins } from './pins.ts';
|
|
21
|
+
export { inventory, pinnedPrincipalIssues, unknownPrincipals } from './inventory.ts';
|
|
22
|
+
export { type Stage, assertStage, checkDeriveVersion } from './guards.ts';
|
|
23
|
+
export { type ExpectedIdentityOptions, type Identity, type IdentityMismatch, assertIdentity, compareIdentity, expectedIdentity, } from './identity.ts';
|
|
24
|
+
export { SITE_TOKENS, type SiteToken, type ValuedToken, renderTokens, tokenValues, } from './tokens.ts';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,KAAK,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,KAAK,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,WAAW,EACX,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APEX_ZONE,
|
|
3
|
+
SITE_FORMAT,
|
|
4
|
+
SiteError,
|
|
5
|
+
SiteSchema,
|
|
6
|
+
VERSION,
|
|
7
|
+
addressOn,
|
|
8
|
+
assertStage,
|
|
9
|
+
checkDeriveVersion,
|
|
10
|
+
decodeSite,
|
|
11
|
+
joinPath,
|
|
12
|
+
lookup,
|
|
13
|
+
unknownKey,
|
|
14
|
+
validateSite
|
|
15
|
+
} from "./index-dvzn0279.js";
|
|
16
|
+
// src/derive.ts
|
|
17
|
+
function zoneOf(site, key) {
|
|
18
|
+
if (key === APEX_ZONE)
|
|
19
|
+
return site.apex;
|
|
20
|
+
return `${lookup("zone", site.zones, key)}.${site.apex}`;
|
|
21
|
+
}
|
|
22
|
+
function deriveHosts(site) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [key, host] of Object.entries(site.hosts)) {
|
|
25
|
+
const addresses = {};
|
|
26
|
+
for (const [network, number] of Object.entries(host.legs ?? {})) {
|
|
27
|
+
addresses[network] = addressOn(lookup("network", site.networks, network), number);
|
|
28
|
+
}
|
|
29
|
+
out[key] = {
|
|
30
|
+
...host.zone === undefined ? {} : { fqdn: `${key}.${zoneOf(site, host.zone)}` },
|
|
31
|
+
addresses
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function mapRecord(record, f) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [key, value] of Object.entries(record))
|
|
39
|
+
out[key] = f(value, key);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function derive(site) {
|
|
43
|
+
const hosts = deriveHosts(site);
|
|
44
|
+
const hostFqdn = (key) => {
|
|
45
|
+
const fqdn = lookup("host", hosts, key).fqdn;
|
|
46
|
+
if (fqdn === undefined) {
|
|
47
|
+
throw new SiteError("unknown-key", `host "${key}" has no zone, so it has no hostname`);
|
|
48
|
+
}
|
|
49
|
+
return fqdn;
|
|
50
|
+
};
|
|
51
|
+
const vaultHost = `${site.vault.label}.${site.apex}`;
|
|
52
|
+
const apiHost = `${site.vault.apiLabel}.${vaultHost}`;
|
|
53
|
+
const { lan } = site.vault;
|
|
54
|
+
const teamDomain = `https://${site.cloudflare.access.team}.cloudflareaccess.com`;
|
|
55
|
+
const products = mapRecord(site.products, (p, key) => p.domain ?? `${p.label ?? key}.${site.apex}`);
|
|
56
|
+
const services = mapRecord(site.services, (s) => `${s.scheme}://${hostFqdn(s.host)}:${s.port}`);
|
|
57
|
+
const clusters = mapRecord(site.clusters, (c) => c.members.map(hostFqdn));
|
|
58
|
+
const accounts = site.cloudflare.accounts;
|
|
59
|
+
const cloudflareMounts = Object.entries(accounts).flatMap(([alias, account]) => account.surfaces.map((surface) => `cloudflare-${alias}-${surface}`));
|
|
60
|
+
return {
|
|
61
|
+
apex: site.apex,
|
|
62
|
+
zones: mapRecord(site.zones, (_label, key) => zoneOf(site, key)),
|
|
63
|
+
mgmtZone: zoneOf(site, "mgmt"),
|
|
64
|
+
vault: {
|
|
65
|
+
host: vaultHost,
|
|
66
|
+
apiHost,
|
|
67
|
+
publicAddr: `https://${apiHost}`,
|
|
68
|
+
meshAddr: `https://${site.vault.meshAddress}:${site.vault.port}`,
|
|
69
|
+
lanAddr: `${lan.scheme}://${hostFqdn(lan.host)}:${lan.port}`,
|
|
70
|
+
namespace: site.vault.namespace,
|
|
71
|
+
oidcRedirects: [
|
|
72
|
+
`https://${vaultHost}/ui/vault/auth/${site.vault.oidcMount}/oidc/callback`,
|
|
73
|
+
`http://localhost:${site.vault.cliCallbackPort}/oidc/callback`
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
access: {
|
|
77
|
+
team: site.cloudflare.access.team,
|
|
78
|
+
teamDomain,
|
|
79
|
+
certsUrl: `${teamDomain}/cdn-cgi/access/certs`
|
|
80
|
+
},
|
|
81
|
+
hosts,
|
|
82
|
+
products,
|
|
83
|
+
services,
|
|
84
|
+
clusters,
|
|
85
|
+
cloudflareMounts,
|
|
86
|
+
zone: (key) => zoneOf(site, key),
|
|
87
|
+
host: hostFqdn,
|
|
88
|
+
address: (host, network) => {
|
|
89
|
+
const addresses = lookup("host", hosts, host).addresses;
|
|
90
|
+
if (!Object.hasOwn(addresses, network))
|
|
91
|
+
unknownKey(`network leg of host "${host}"`, network, Object.keys(addresses));
|
|
92
|
+
return addresses[network];
|
|
93
|
+
},
|
|
94
|
+
productHost: (key) => lookup("product", products, key),
|
|
95
|
+
productUrl: (key) => `https://${lookup("product", products, key)}`,
|
|
96
|
+
serviceUrl: (key) => lookup("service", services, key),
|
|
97
|
+
clusterMembers: (key) => lookup("cluster", clusters, key),
|
|
98
|
+
cloudflareMount: (alias, surface) => {
|
|
99
|
+
const account = lookup("cloudflare account", accounts, alias);
|
|
100
|
+
if (!account.surfaces.includes(surface)) {
|
|
101
|
+
unknownKey(`surface of account "${alias}"`, surface, account.surfaces);
|
|
102
|
+
}
|
|
103
|
+
return `cloudflare-${alias}-${surface}`;
|
|
104
|
+
},
|
|
105
|
+
accountId: (alias) => lookup("cloudflare account", accounts, alias).id
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// src/pins.ts
|
|
109
|
+
function pins(site) {
|
|
110
|
+
const { pinned } = site;
|
|
111
|
+
return {
|
|
112
|
+
name: (key) => lookup("pinned name", pinned.names, key),
|
|
113
|
+
certificate: (key) => lookup("pinned certificate", pinned.certificates, key),
|
|
114
|
+
adopted: (key) => lookup("adopted id", pinned.adopted, key),
|
|
115
|
+
policy: (key) => lookup("pinned policy list", pinned.policies, key),
|
|
116
|
+
principals: (key) => lookup("pinned principal list", pinned.sshPrincipals, key)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// src/inventory.ts
|
|
120
|
+
function inventory(site) {
|
|
121
|
+
const derived = derive(site);
|
|
122
|
+
const names = new Set([
|
|
123
|
+
site.apex,
|
|
124
|
+
...Object.values(derived.zones),
|
|
125
|
+
derived.vault.host,
|
|
126
|
+
derived.vault.apiHost,
|
|
127
|
+
site.vault.meshAddress
|
|
128
|
+
]);
|
|
129
|
+
for (const [key, host] of Object.entries(site.hosts)) {
|
|
130
|
+
names.add(key);
|
|
131
|
+
for (const alias of host.aliases ?? [])
|
|
132
|
+
names.add(alias);
|
|
133
|
+
const built = derived.hosts[key];
|
|
134
|
+
if (built?.fqdn !== undefined)
|
|
135
|
+
names.add(built.fqdn);
|
|
136
|
+
for (const address of Object.values(built?.addresses ?? {}))
|
|
137
|
+
names.add(address);
|
|
138
|
+
}
|
|
139
|
+
return names;
|
|
140
|
+
}
|
|
141
|
+
function unknownPrincipals(site, list) {
|
|
142
|
+
const known = inventory(site);
|
|
143
|
+
return list.filter((entry) => !known.has(entry));
|
|
144
|
+
}
|
|
145
|
+
function pinnedPrincipalIssues(site) {
|
|
146
|
+
const issues = [];
|
|
147
|
+
for (const [key, list] of Object.entries(site.pinned.sshPrincipals)) {
|
|
148
|
+
for (const entry of unknownPrincipals(site, list)) {
|
|
149
|
+
const path = joinPath(["pinned", "sshPrincipals", key]);
|
|
150
|
+
issues.push(`${path}: "${entry}" is not in the inventory`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return issues;
|
|
154
|
+
}
|
|
155
|
+
// src/identity.ts
|
|
156
|
+
function expectedIdentity(site, options = {}) {
|
|
157
|
+
const account = options.account === undefined ? undefined : lookup("cloudflare account", site.cloudflare.accounts, options.account).id;
|
|
158
|
+
return {
|
|
159
|
+
vaultClusterName: site.vault.clusterName,
|
|
160
|
+
vaultNamespace: site.vault.namespace,
|
|
161
|
+
...account === undefined ? {} : { cloudflareAccountId: account }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
var FIELDS = ["vaultClusterName", "vaultNamespace", "cloudflareAccountId"];
|
|
165
|
+
function compareIdentity(expected, observed) {
|
|
166
|
+
if (FIELDS.every((field) => expected[field] === undefined)) {
|
|
167
|
+
throw new SiteError("identity", "refusing to plan: the expected identity names no field, so it would match any system");
|
|
168
|
+
}
|
|
169
|
+
const mismatches = [];
|
|
170
|
+
for (const field of FIELDS) {
|
|
171
|
+
const want = expected[field];
|
|
172
|
+
if (want === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const got = observed[field];
|
|
175
|
+
if (got !== want)
|
|
176
|
+
mismatches.push({ field, expected: want, observed: got });
|
|
177
|
+
}
|
|
178
|
+
return mismatches;
|
|
179
|
+
}
|
|
180
|
+
function assertIdentity(expected, observed) {
|
|
181
|
+
const mismatches = compareIdentity(expected, observed);
|
|
182
|
+
if (mismatches.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
throw new SiteError("identity", "refusing to plan: the live system is not the one this site describes", mismatches.map((m) => `${m.field}: expected "${m.expected}", observed ${m.observed === undefined ? "nothing" : `"${m.observed}"`}`));
|
|
185
|
+
}
|
|
186
|
+
// src/tokens.ts
|
|
187
|
+
var SITE_TOKENS = {
|
|
188
|
+
"<apex>": "the apex domain every derived name hangs off",
|
|
189
|
+
"<vault-host>": "the vault UI hostname: <vault label>.<apex>",
|
|
190
|
+
"<vault-api-host>": "the vault public API hostname: <api label>.<vault-host>",
|
|
191
|
+
"<mgmt-zone>": "the management zone: <mgmt label>.<apex>",
|
|
192
|
+
"<access-team>": "the Cloudflare Access team name (<access-team>.cloudflareaccess.com)",
|
|
193
|
+
"<github-owner>": "the GitHub user or organisation that owns the repos",
|
|
194
|
+
"<estate-root>": "the absolute directory holding host-local estate files",
|
|
195
|
+
"<cluster>": "any key of site.clusters — a variable, never substituted"
|
|
196
|
+
};
|
|
197
|
+
function tokenValues(site) {
|
|
198
|
+
const derived = derive(site);
|
|
199
|
+
return {
|
|
200
|
+
"<apex>": site.apex,
|
|
201
|
+
"<vault-host>": derived.vault.host,
|
|
202
|
+
"<vault-api-host>": derived.vault.apiHost,
|
|
203
|
+
"<mgmt-zone>": derived.mgmtZone,
|
|
204
|
+
"<access-team>": site.cloudflare.access.team,
|
|
205
|
+
"<github-owner>": site.github.owner,
|
|
206
|
+
"<estate-root>": site.paths.estateRoot
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function renderTokens(text, site) {
|
|
210
|
+
const values = tokenValues(site);
|
|
211
|
+
const tokens = Object.keys(values).sort((a, b) => b.length - a.length);
|
|
212
|
+
const pattern = new RegExp(tokens.map((t) => t.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g");
|
|
213
|
+
return text.replaceAll(pattern, (token) => values[token] ?? token);
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
SITE_FORMAT,
|
|
217
|
+
SITE_TOKENS,
|
|
218
|
+
SiteError,
|
|
219
|
+
SiteSchema,
|
|
220
|
+
VERSION,
|
|
221
|
+
assertIdentity,
|
|
222
|
+
assertStage,
|
|
223
|
+
checkDeriveVersion,
|
|
224
|
+
compareIdentity,
|
|
225
|
+
decodeSite,
|
|
226
|
+
derive,
|
|
227
|
+
expectedIdentity,
|
|
228
|
+
inventory,
|
|
229
|
+
pinnedPrincipalIssues,
|
|
230
|
+
pins,
|
|
231
|
+
renderTokens,
|
|
232
|
+
tokenValues,
|
|
233
|
+
unknownPrincipals,
|
|
234
|
+
validateSite
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
//# debugId=6A231AADB000E4A864756E2164756E21
|
|
238
|
+
//# sourceMappingURL=index.js.map
|