@homeflare/alchemy 0.27.2 → 0.27.4
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/dist/cloudflare/mesh-node-token.d.ts +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
- package/dist/forgejo/branch-protection.d.ts +4 -4
- package/dist/forgejo/org-actions-secrets.d.ts +4 -4
- package/dist/forgejo/org-label.d.ts +4 -4
- package/dist/forgejo/org-team.d.ts +4 -4
- package/dist/forgejo/repo-webhook.d.ts +4 -4
- package/dist/forgejo/repository.d.ts +4 -4
- package/dist/forgejo/team-member.d.ts +4 -4
- package/dist/netbox/fake-netbox.d.ts +43 -0
- package/dist/netbox/fake-netbox.d.ts.map +1 -0
- package/dist/netbox/index.d.ts +1 -2
- package/dist/netbox/index.d.ts.map +1 -1
- package/dist/netbox/index.js +71 -119
- package/dist/netbox/index.js.map +5 -6
- package/dist/netbox/prefix.d.ts +33 -2
- package/dist/netbox/prefix.d.ts.map +1 -1
- package/dist/netbox/resource.d.ts +73 -48
- package/dist/netbox/resource.d.ts.map +1 -1
- package/docs/credentials.md +10 -0
- package/docs/peers.md +11 -0
- package/docs/upstream-conformance.md +17 -2
- package/package.json +5 -1
- package/src/netbox/fake-netbox.ts +56 -0
- package/src/netbox/index.ts +8 -2
- package/src/netbox/prefix.test.ts +223 -0
- package/src/netbox/prefix.ts +68 -32
- package/src/netbox/resource.test.ts +67 -0
- package/src/netbox/resource.ts +133 -112
- package/src/proxmox/distilled-task-await.test.ts +124 -0
- package/dist/netbox/client.d.ts +0 -52
- package/dist/netbox/client.d.ts.map +0 -1
- package/src/netbox/client.test.ts +0 -67
- package/src/netbox/client.ts +0 -146
package/dist/netbox/index.js
CHANGED
|
@@ -1,59 +1,5 @@
|
|
|
1
|
-
// src/netbox/client.ts
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
4
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
5
|
-
var PATH_PREFIX = "/api";
|
|
6
|
-
var apiBase = () => {
|
|
7
|
-
const raw = process.env["NETBOX_URL"];
|
|
8
|
-
if (raw === undefined || raw.trim() === "") {
|
|
9
|
-
throw new Error("NETBOX_URL is not set. Point it at your NetBox instance, e.g. https://netbox.example.com");
|
|
10
|
-
}
|
|
11
|
-
return raw.trim().replace(/\/$/, "");
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
class NetboxError extends Error {
|
|
15
|
-
status;
|
|
16
|
-
method;
|
|
17
|
-
path;
|
|
18
|
-
constructor(status, method, path, detail) {
|
|
19
|
-
super(`NetBox ${method} ${path} -> ${String(status)}: ${detail}`);
|
|
20
|
-
this.status = status;
|
|
21
|
-
this.method = method;
|
|
22
|
-
this.path = path;
|
|
23
|
-
this.name = "NetboxError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
var token = () => Effect.gen(function* () {
|
|
27
|
-
const raw = process.env["NETBOX_TOKEN"];
|
|
28
|
-
if (raw === undefined || raw.trim() === "") {
|
|
29
|
-
return yield* Effect.fail(new NetboxError(0, "GET", "", "NETBOX_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props."));
|
|
30
|
-
}
|
|
31
|
-
return raw.trim();
|
|
32
|
-
});
|
|
33
|
-
var netbox = (method, path, body) => Effect.gen(function* () {
|
|
34
|
-
const credential = yield* token();
|
|
35
|
-
const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\//, "")}`;
|
|
36
|
-
const client = yield* HttpClient.HttpClient;
|
|
37
|
-
const request = HttpClientRequest.make(method)(url).pipe(HttpClientRequest.setHeaders({
|
|
38
|
-
Accept: "application/json",
|
|
39
|
-
Authorization: `Token ${credential}`
|
|
40
|
-
}), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
|
|
41
|
-
const response = yield* client.execute(request).pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));
|
|
42
|
-
if (response.status < 200 || response.status >= 300) {
|
|
43
|
-
const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
44
|
-
return yield* Effect.fail(new NetboxError(response.status, method, path, detail.slice(0, 300)));
|
|
45
|
-
}
|
|
46
|
-
const text = yield* response.text.pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));
|
|
47
|
-
return text === "" ? undefined : JSON.parse(text);
|
|
48
|
-
});
|
|
49
|
-
var soleMatch = (rows, describe) => {
|
|
50
|
-
if (rows.length > 1) {
|
|
51
|
-
throw new Error(`${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` + "unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.");
|
|
52
|
-
}
|
|
53
|
-
return rows[0];
|
|
54
|
-
};
|
|
55
1
|
// src/netbox/constraint-guard.ts
|
|
56
|
-
import * as
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
57
3
|
|
|
58
4
|
// src/netbox/constraints.ts
|
|
59
5
|
var lengthOf = (value) => [...value].length;
|
|
@@ -141,16 +87,17 @@ var constraintsFor = (key) => {
|
|
|
141
87
|
return table;
|
|
142
88
|
};
|
|
143
89
|
var bodyViolations = (key, body, presence) => violations(constraintsFor(key), body, { presence });
|
|
144
|
-
var guardBody = (key, body, presence) =>
|
|
90
|
+
var guardBody = (key, body, presence) => Effect.suspend(() => {
|
|
145
91
|
if (key === undefined)
|
|
146
|
-
return
|
|
92
|
+
return Effect.void;
|
|
147
93
|
const found = bodyViolations(key, body, presence);
|
|
148
|
-
return found.length === 0 ?
|
|
94
|
+
return found.length === 0 ? Effect.void : Effect.die(new Error(refusal(key, found)));
|
|
149
95
|
});
|
|
150
96
|
// src/netbox/prefix.ts
|
|
151
97
|
import { Resource } from "alchemy";
|
|
152
98
|
import * as Provider from "alchemy/Provider";
|
|
153
|
-
import * as
|
|
99
|
+
import * as ipam from "@distilled.cloud/netbox/ipam";
|
|
100
|
+
import * as Effect3 from "effect/Effect";
|
|
154
101
|
|
|
155
102
|
// src/netbox/prefix-form.ts
|
|
156
103
|
var DEFAULT_STATUS = "active";
|
|
@@ -186,27 +133,27 @@ var prefixMatches = (attributes, props) => {
|
|
|
186
133
|
|
|
187
134
|
// src/netbox/resource.ts
|
|
188
135
|
import { isResolved } from "alchemy/Diff";
|
|
189
|
-
import
|
|
190
|
-
|
|
136
|
+
import { CredentialsFromEnv } from "@distilled.cloud/netbox/Credentials";
|
|
137
|
+
import * as Effect2 from "effect/Effect";
|
|
191
138
|
var LOCATE_PAGE = 20;
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
139
|
+
var soleMatch = (rows, describe) => {
|
|
140
|
+
if (rows.length > 1) {
|
|
141
|
+
throw new Error(`${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` + "unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.");
|
|
142
|
+
}
|
|
143
|
+
return rows[0];
|
|
144
|
+
};
|
|
145
|
+
var locateOne = (list, describe, identifies) => list.pipe(Effect2.map((page) => {
|
|
146
|
+
if (page.count > LOCATE_PAGE) {
|
|
147
|
+
throw new Error(`${describe}: the locate filter matched ${String(page.count)} rows, more than the ` + `${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — narrow the ` + "filter rather than paging through candidates.");
|
|
148
|
+
}
|
|
149
|
+
const rows = identifies === undefined ? page.results : page.results.filter(identifies);
|
|
150
|
+
return soleMatch(rows, describe);
|
|
151
|
+
}));
|
|
196
152
|
var netboxOperations = (spec) => {
|
|
197
|
-
const
|
|
198
|
-
if (list !== undefined && list.count > LOCATE_PAGE) {
|
|
199
|
-
throw new Error(`${spec.describe(props)}: the locate filter matched ${String(list.count)} rows, more ` + `than the ${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — ` + "narrow the filter rather than paging through candidates.");
|
|
200
|
-
}
|
|
201
|
-
const rows = list?.results ?? [];
|
|
202
|
-
const narrow = spec.identifies;
|
|
203
|
-
return soleMatch(narrow === undefined ? rows : rows.filter((row) => narrow(row, props)), spec.describe(props));
|
|
204
|
-
}));
|
|
205
|
-
const objectPath = (live) => `${spec.collection}/${String(live["id"])}`;
|
|
206
|
-
const read = (props) => fetchLive(props).pipe(Effect3.map((live) => live === undefined ? undefined : spec.attributes(live, props)));
|
|
153
|
+
const read = (props) => spec.fetchLive(props).pipe(Effect2.map((live) => live === undefined ? undefined : spec.attributes(live, props)));
|
|
207
154
|
return {
|
|
208
155
|
read,
|
|
209
|
-
diff: (news, output) =>
|
|
156
|
+
diff: (news, output) => Effect2.gen(function* () {
|
|
210
157
|
if (output === undefined || !isResolved(news))
|
|
211
158
|
return;
|
|
212
159
|
const live = yield* read(news);
|
|
@@ -214,46 +161,47 @@ var netboxOperations = (spec) => {
|
|
|
214
161
|
return { action: "update" };
|
|
215
162
|
if (spec.matches(live, news))
|
|
216
163
|
return { action: "noop" };
|
|
217
|
-
return spec.
|
|
164
|
+
return spec.update === undefined ? { action: "replace" } : { action: "update" };
|
|
218
165
|
}),
|
|
219
|
-
reconcile: (news) =>
|
|
220
|
-
const before = yield* fetchLive(news);
|
|
166
|
+
reconcile: (news) => Effect2.gen(function* () {
|
|
167
|
+
const before = yield* spec.fetchLive(news);
|
|
221
168
|
if (before === undefined) {
|
|
222
169
|
const body = spec.createBody(news);
|
|
223
170
|
yield* guardBody(spec.endpoint.create, body, true);
|
|
224
|
-
yield*
|
|
225
|
-
} else if (spec.
|
|
171
|
+
yield* spec.create(news, body);
|
|
172
|
+
} else if (spec.update !== undefined) {
|
|
226
173
|
const mapped = spec.attributes(before, news);
|
|
227
174
|
if (mapped !== undefined && !spec.matches(mapped, news)) {
|
|
228
|
-
const body = spec.
|
|
175
|
+
const body = spec.update.body(news);
|
|
229
176
|
yield* guardBody(spec.endpoint.update, body, false);
|
|
230
177
|
if (Object.keys(body).length > 0) {
|
|
231
|
-
yield*
|
|
178
|
+
yield* spec.update.call(news, before, body);
|
|
232
179
|
}
|
|
233
180
|
}
|
|
234
181
|
}
|
|
235
182
|
const after = yield* read(news);
|
|
236
183
|
if (after === undefined) {
|
|
237
|
-
return yield*
|
|
184
|
+
return yield* Effect2.die(new Error(`${spec.describe(news)}: the write returned no error but the object is still ` + "absent. NetBox returns the object it wrote — read back rather than trusting the " + "status code."));
|
|
238
185
|
}
|
|
239
186
|
return after;
|
|
240
187
|
}),
|
|
241
|
-
destroy: (olds) =>
|
|
242
|
-
const live = yield* fetchLive(olds);
|
|
188
|
+
destroy: (olds) => Effect2.gen(function* () {
|
|
189
|
+
const live = yield* spec.fetchLive(olds);
|
|
243
190
|
if (live === undefined)
|
|
244
191
|
return;
|
|
245
|
-
yield*
|
|
192
|
+
yield* spec.destroy(olds, live);
|
|
246
193
|
})
|
|
247
194
|
};
|
|
248
195
|
};
|
|
249
196
|
var netboxHandlers = (spec) => {
|
|
250
197
|
const ops = netboxOperations(spec);
|
|
198
|
+
const withCredentials = (effect) => Effect2.provide(effect, CredentialsFromEnv);
|
|
251
199
|
return {
|
|
252
|
-
list: () =>
|
|
253
|
-
read: ({ olds }) => ops.read(olds),
|
|
254
|
-
diff: ({ news, output }) => ops.diff(news, output),
|
|
255
|
-
reconcile: ({ news }) => ops.reconcile(news),
|
|
256
|
-
delete: ({ olds }) => ops.destroy(olds)
|
|
200
|
+
list: () => Effect2.succeed([]),
|
|
201
|
+
read: ({ olds }) => withCredentials(ops.read(olds)),
|
|
202
|
+
diff: ({ news, output }) => withCredentials(ops.diff(news, output)),
|
|
203
|
+
reconcile: ({ news }) => withCredentials(ops.reconcile(news)),
|
|
204
|
+
delete: ({ olds }) => withCredentials(ops.destroy(olds))
|
|
257
205
|
};
|
|
258
206
|
};
|
|
259
207
|
|
|
@@ -281,51 +229,55 @@ var fk = (value) => {
|
|
|
281
229
|
var NetboxPrefix = Resource("Netbox.Prefix", {
|
|
282
230
|
defaultRemovalPolicy: "retain"
|
|
283
231
|
});
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
vrf: fk(live["vrf"])
|
|
300
|
-
};
|
|
301
|
-
},
|
|
302
|
-
collection: "ipam/prefixes",
|
|
232
|
+
var attributesOf = (live, props) => ({
|
|
233
|
+
comments: text(live.comments),
|
|
234
|
+
description: text(live.description),
|
|
235
|
+
isPool: live.is_pool === true,
|
|
236
|
+
markUtilized: live.mark_utilized === true,
|
|
237
|
+
prefix: live.prefix || props.prefix,
|
|
238
|
+
prefixId: live.id,
|
|
239
|
+
status: choice(live.status),
|
|
240
|
+
tenant: fk(live.tenant),
|
|
241
|
+
vlan: fk(live.vlan),
|
|
242
|
+
vrf: fk(live.vrf)
|
|
243
|
+
});
|
|
244
|
+
var spec = {
|
|
245
|
+
attributes: attributesOf,
|
|
246
|
+
create: (_props, body) => ipam.createIpamPrefix({ body }),
|
|
303
247
|
createBody: prefixBody,
|
|
248
|
+
destroy: (_props, live) => ipam.ipamPrefixesDestroy({ id: live.id }),
|
|
304
249
|
describe: (props) => `ipam/prefixes ${props.prefix}`,
|
|
305
250
|
endpoint: {
|
|
306
251
|
create: "netbox:POST /api/ipam/prefixes/",
|
|
307
252
|
update: "netbox:PATCH /api/ipam/prefixes/{id}/"
|
|
308
253
|
},
|
|
309
|
-
|
|
310
|
-
locate: (props) => [["prefix", props.prefix]],
|
|
254
|
+
fetchLive: (props) => locateOne(ipam.listIpamPrefixes({ limit: LOCATE_PAGE, prefix: [props.prefix] }), `ipam/prefixes ${props.prefix}`, (row) => fk(row.vrf) === props.vrf),
|
|
311
255
|
matches: prefixMatches,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
256
|
+
update: {
|
|
257
|
+
body: prefixBody,
|
|
258
|
+
call: (_props, live, body) => ipam.updateIpamPrefixesPartial({
|
|
259
|
+
...body,
|
|
260
|
+
id: live.id
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var handlers = netboxHandlers(spec);
|
|
265
|
+
var NetboxPrefixProvider = () => Provider.effect(NetboxPrefix, Effect3.succeed(NetboxPrefix.Provider.of(handlers)));
|
|
315
266
|
export {
|
|
267
|
+
LOCATE_PAGE,
|
|
316
268
|
NETBOX_CONSTRAINTS,
|
|
317
269
|
NETBOX_CONSTRAINTS_DIGEST,
|
|
318
|
-
NetboxError,
|
|
319
270
|
NetboxPrefix,
|
|
320
271
|
NetboxPrefixProvider,
|
|
321
|
-
apiBase,
|
|
322
272
|
bodyViolations,
|
|
323
273
|
constraintsFor,
|
|
324
274
|
guardBody,
|
|
275
|
+
locateOne,
|
|
325
276
|
netboxHandlers,
|
|
326
277
|
refusal,
|
|
278
|
+
soleMatch,
|
|
327
279
|
violations
|
|
328
280
|
};
|
|
329
281
|
|
|
330
|
-
//# debugId=
|
|
282
|
+
//# debugId=546DA12AD639183464756E2164756E21
|
|
331
283
|
//# sourceMappingURL=index.js.map
|
package/dist/netbox/index.js.map
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/netbox/
|
|
3
|
+
"sources": ["../src/netbox/constraint-guard.ts", "../src/netbox/constraints.ts", "../src/netbox/generated/constraints/netbox-ipam.ts", "../src/netbox/generated/constraints/index.ts", "../src/netbox/prefix.ts", "../src/netbox/prefix-form.ts", "../src/netbox/resource.ts", "../src/netbox/values.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * One authenticated call to the NetBox REST API at `/api`.\n *\n * ⚠️ `Authorization: Token <value>`, NOT `Bearer`. NetBox is Django REST Framework using DRF's own\n * `TokenAuthentication`. MEASURED on this estate's own instance while it was up: `Bearer`\n * returns 401 and `Token` returns 200. ⛔ A wrong scheme and a wrong credential are\n * indistinguishable in the response, which is why this is written down rather than inferred.\n *\n * ⚠️ NETBOX DOES NOT WRAP A DETAIL RESPONSE THE WAY PVE DOES — a GET of one object returns the\n * object. A LIST, however, IS wrapped: `{count, next, previous, results: [...]}`. A caller that\n * treats a list like a detail sees `undefined` and calls a healthy object absent, which plans a\n * create over something that already exists.\n *\n * ⛔ THE TOKEN IS NEVER A PROP. It is read from `NETBOX_TOKEN` at call time only. Alchemy persists\n * attributes unencrypted; nothing stored here may be a credential.\n */\nimport * as Effect from 'effect/Effect';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\n\nconst PATH_PREFIX = '/api';\n\n/**\n * The instance, from `NETBOX_URL`. Trailing slash stripped.\n *\n * ⛔ NO DEFAULT HOSTNAME AND NO DEFAULT PORT. A published package that fell back to a loopback\n * address would let a consumer who forgot the variable watch every call fail against a machine\n * that is not theirs, instead of being told which variable is missing.\n * ⚠️ Read at CALL time, never at module scope — a value captured at import is pinned to whatever\n * the first isolate saw.\n */\nexport const apiBase = (): string => {\n const raw = process.env['NETBOX_URL'];\n if (raw === undefined || raw.trim() === '') {\n throw new Error(\n 'NETBOX_URL is not set. Point it at your NetBox instance, e.g. https://netbox.example.com',\n );\n }\n return raw.trim().replace(/\\/$/, '');\n};\n\nexport class NetboxError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n detail: string,\n ) {\n super(`NetBox ${method} ${path} -> ${String(status)}: ${detail}`);\n this.name = 'NetboxError';\n }\n}\n\nconst token = (): Effect.Effect<string, NetboxError> =>\n Effect.gen(function* () {\n const raw = process.env['NETBOX_TOKEN'];\n if (raw === undefined || raw.trim() === '') {\n return yield* Effect.fail(\n new NetboxError(\n 0,\n 'GET',\n '',\n 'NETBOX_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props.',\n ),\n );\n }\n return raw.trim();\n });\n\n/** A NetBox list response. ⚠️ Paginated: `next` is a URL, not a cursor to guess at. */\nexport interface NetboxList<T> {\n readonly count: number;\n readonly next: string | null;\n readonly results: readonly T[];\n}\n\nexport type NetboxRow = Record<string, unknown>;\n\n/** One JSON call. Returns the parsed body, or `undefined` when the response is empty (DELETE). */\nexport const netbox = <T>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: Record<string, unknown>,\n) =>\n Effect.gen(function* () {\n const credential = yield* token();\n const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\\//, '')}`;\n /**\n * ★ THROUGH `HttpClient`, NOT THE GLOBAL `fetch` — the idiom Alchemy's own providers follow.\n * It buys a typed error channel, interruption when a plan is cancelled, the runtime's\n * tracing, and a client a test can replace without monkey-patching a global.\n */\n const client = yield* HttpClient.HttpClient;\n /**\n * ⛔ THE CONTENT TYPE GOES ON `bodyText`, NOT IN THE HEADER MAP. `bodyText(s)` with no second\n * argument sets `text/plain` and OVERWRITES a `Content-Type` set by `setHeaders`, because it\n * is piped afterwards. That shipped `@homeflare/alchemy/forgejo` unable to write at all —\n * every create and update in the package, broken identically, for as long as the only\n * credential on the shelf was read-only and so never reached a request body.\n */\n const request = HttpClientRequest.make(method)(url).pipe(\n HttpClientRequest.setHeaders({\n Accept: 'application/json',\n Authorization: `Token ${credential}`,\n }),\n body === undefined\n ? (self) => self\n : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),\n );\n const response = yield* client\n .execute(request)\n .pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));\n // ⛔ Status first, body once — a NetBox error body is JSON but a reverse proxy's 502 is not.\n // ⚠️ AND A 502 IS THE EXPECTED SYMPTOM WHEN NETBOX IS DOWN BUT ITS FRONT DOOR IS UP, which\n // is the state the reference instance has been in since 2026-09-14. Folding it to \"absent\"\n // would plan a create for every object that already exists.\n if (response.status < 200 || response.status >= 300) {\n const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ''));\n return yield* Effect.fail(\n new NetboxError(response.status, method, path, detail.slice(0, 300)),\n );\n }\n // ⚠️ 204 AND AN EMPTY BODY ARE REAL: a DELETE answers no content, and parsing an empty body\n // throws. Read the text once and parse only when there is something to parse.\n const text = yield* response.text.pipe(\n Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))),\n );\n return (text === '' ? undefined : JSON.parse(text)) as T;\n });\n\n/**\n * The single row a filtered list returns, or `undefined`.\n *\n * ⛔ MORE THAN ONE MATCH IS A DEFECT, NOT A REASON TO TAKE THE FIRST. A `locate` filter that is\n * not unique would let adopt bind to whichever row NetBox happened to order first, and the next\n * plan would bind to the other one and report drift that is not there.\n */\nexport const soleMatch = <T>(rows: readonly T[], describe: string): T | undefined => {\n if (rows.length > 1) {\n throw new Error(\n `${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` +\n 'unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.',\n );\n }\n return rows[0];\n};\n",
|
|
6
5
|
"/**\n * The generated tables, looked up by endpoint, and the refusal that stops a plan.\n *\n * ★ ONE FILE BETWEEN THE PROVIDERS AND THE GENERATED DATA, so `resource.ts` gains three lines\n * rather than an import of the generated module and a merge, and so a family that writes its\n * own handlers reaches the same check by the same name.\n *\n * ⛔ AN UNKNOWN KEY IS A DEFECT, NOT A REASON TO SKIP THE CHECK. Silently passing would put us\n * back where this started: a declaration nothing checked, and a plan that says so in no way at\n * all. The generator resolves every key in its source scan against the vendor document, so a\n * key that is absent here means the tables were not regenerated — which the message says.\n */\nimport * as Effect from 'effect/Effect';\nimport {\n type EndpointConstraints,\n type EndpointKey,\n type NetboxBody,\n refusal,\n violations,\n} from './constraints.ts';\nimport { NETBOX_CONSTRAINTS } from './generated/constraints/index.ts';\n\nexport { NETBOX_CONSTRAINTS };\n\nexport const constraintsFor = (key: EndpointKey): EndpointConstraints => {\n const table = NETBOX_CONSTRAINTS[key];\n if (table === undefined) {\n throw new Error(\n `no vendor constraint table for '${key}'. It is named in this package but not generated: ` +\n 'run `bun codegen/netbox.ts` (see codegen/README.md).',\n );\n }\n return table;\n};\n\n/** Every vendor rule this body breaks. Pure — a test calls it with a literal body. */\nexport const bodyViolations = (\n key: EndpointKey,\n body: NetboxBody,\n presence: boolean,\n): readonly string[] => violations(constraintsFor(key), body, { presence });\n\n/**\n * ⛔ `Effect.die`, NOT `Effect.fail`. A declaration the vendor would reject is a DEFECT in the\n * stack file, not a recoverable condition: there is no retry and no fallback that makes a\n * 201-character description legal.\n */\nexport const guardBody = (\n key: EndpointKey | undefined,\n body: NetboxBody,\n presence: boolean,\n): Effect.Effect<void> =>\n Effect.suspend(() => {\n if (key === undefined) return Effect.void;\n const found = bodyViolations(key, body, presence);\n return found.length === 0 ? Effect.void : Effect.die(new Error(refusal(key, found)));\n });\n",
|
|
7
6
|
"/**\n * NetBox's own rules, checked here, before the request is built.\n *\n * 🔴 THE INCIDENT THIS PATTERN EXISTS FOR, 2026-09-22 (Proxmox, not NetBox). `homeflare-proxmox`'s\n * first `deploy:pbs` adopted ten objects and then failed its one create:\n * `comment: value may only be 128 characters long`. Half a deploy in, on the server, from a\n * limit the vendor publishes in its own schema and the generated type did not carry. Plan was\n * green, `bun run check` was green, the adopt verifier was green. ★ NetBox gets the same\n * treatment BEFORE its first write rather than after it.\n *\n * ★ SO THE TABLE IS GENERATED AND THIS FILE IS THE READER. `generated/constraints/*` is emitted\n * from NetBox's own OpenAPI document by `bun codegen/netbox.ts`; nothing here is hand-typed,\n * because a hand-typed limit is tribal knowledge that does not run.\n *\n * ⛔ A SEPARATE READER FROM `../proxmox/constraints.ts`, AND IT IS NOT DUPLICATION. That one\n * checks a `PveForm` — every value already a string, because PVE takes form encoding. A NetBox\n * body is JSON: a number stays a number, a boolean stays a boolean, a foreign key is an\n * integer, and `String(value)` on any of them would make `minimum: 0` pass for `false` and\n * `maxLength` meaningless. Sharing one reader would mean stringifying at the door, which is\n * exactly the mangling the Proxmox version's own ⛔ warns about.\n *\n * ⚠️ WHAT IT DELIBERATELY DOES NOT ENFORCE, so nobody reads a green plan as more than it is:\n * - a `format` NAME (`uri`, `email`, `date-time`): a validator DRF implements server-side.\n * Recorded, never checked.\n * - a pattern the generator could not carry faithfully (codegen/py-pattern.ts). Recorded\n * verbatim as `patternSource`, never checked.\n * - UNIQUENESS, VRF containment, custom-field validators, and every other rule that lives in\n * Django rather than in the schema. Those still arrive as a server-side 400.\n * Each is a fact in the table rather than a guess, which is the rule this follows: if the\n * vendor did not state it, it is not enforced and the table says so.\n */\n\nexport interface ParamConstraint {\n readonly type?: string;\n /** From the schema's `required` array. ⚠️ Checked on CREATE only — see `violations`. */\n readonly required?: boolean;\n readonly maxLength?: number;\n readonly minLength?: number;\n readonly minimum?: number;\n readonly maximum?: number;\n readonly enum?: readonly string[];\n /** A JavaScript-safe translation. Absent when Python's dialect could not be carried over. */\n readonly pattern?: string;\n /** The vendor's own spelling, always — what a violation message quotes, and the audit trail. */\n readonly patternSource?: string;\n /** DRF's server-side validator name. Recorded so a reader knows what is NOT checked here. */\n readonly format?: string;\n readonly default?: string;\n}\n\nexport type EndpointConstraints = Readonly<Record<string, ParamConstraint>>;\n\n/** `netbox:POST /api/ipam/prefixes/` — exactly how the generated tables key an endpoint. */\nexport type EndpointKey = string;\n\n/** A NetBox request body: JSON, with the types the schema declares. */\nexport type NetboxBody = Record<string, unknown>;\n\nexport interface ViolationOptions {\n /**\n * Check that every schema-required property is present.\n *\n * ⛔ CREATE ONLY. A NetBox update is a PATCH and is deliberately PARTIAL — it carries the fields\n * that changed and nothing else — so requiring presence there would refuse every ordinary\n * edit.\n */\n readonly presence?: boolean;\n}\n\n/**\n * ⚠️ `[...value].length`, NOT `value.length`. Django counts characters (Python `len` on a `str`\n * counts code points); JavaScript counts UTF-16 code units, so an emoji in a description counts\n * twice and a 200-character limit would refuse a description NetBox accepts. Rejecting a legal\n * value at plan time is the one failure mode worse than the server 400 this replaces.\n */\nconst lengthOf = (value: string): number => [...value].length;\n\nconst checkString = (name: string, value: string, rule: ParamConstraint): string[] => {\n const out: string[] = [];\n if (rule.maxLength !== undefined && lengthOf(value) > rule.maxLength) {\n out.push(`${name}: at most ${String(rule.maxLength)} characters`);\n }\n if (rule.minLength !== undefined && lengthOf(value) < rule.minLength) {\n out.push(`${name}: at least ${String(rule.minLength)} characters`);\n }\n if (rule.enum !== undefined && !rule.enum.includes(value)) {\n out.push(`${name}: must be one of ${rule.enum.join(', ')}`);\n }\n if (rule.pattern !== undefined && !new RegExp(rule.pattern).test(value)) {\n out.push(`${name}: must match ${rule.patternSource ?? rule.pattern}`);\n }\n return out;\n};\n\n/**\n * ⛔ BOUNDS ARE CHECKED ONLY ON A REAL NUMBER, NEVER ON A COERCED ONE. `Number(false)` is 0 and\n * `Number('')` is 0, so coercing would silently pass a boolean and an empty string through a\n * `minimum: 0`. A NetBox body carries real numbers; anything else is a type error upstream.\n */\nconst checkNumber = (name: string, value: number, rule: ParamConstraint): string[] => {\n const out: string[] = [];\n if (!Number.isFinite(value)) return out;\n if (rule.minimum !== undefined && value < rule.minimum) {\n out.push(`${name}: at least ${String(rule.minimum)}`);\n }\n if (rule.maximum !== undefined && value > rule.maximum) {\n out.push(`${name}: at most ${String(rule.maximum)}`);\n }\n return out;\n};\n\nconst checkValue = (name: string, value: unknown, rule: ParamConstraint): readonly string[] => {\n if (typeof value === 'string') return checkString(name, value, rule);\n if (typeof value === 'number') return checkNumber(name, value, rule);\n return [];\n};\n\n/**\n * Every vendor rule this body breaks, in the order the properties are declared. Pure: no network,\n * no clock, no environment — so `diff` can call it and a test can call it with a literal.\n *\n * ⚠️ A KEY THE TABLE DOES NOT MENTION IS NOT AN ERROR. The table carries the constrained\n * properties, not every property, and a body may legitimately send an unconstrained one.\n * ⚠️ `null` IS A VALUE, NOT AN ABSENCE. NetBox uses explicit `null` to CLEAR a nullable foreign\n * key, so it must not trip the presence check — and no string or number rule applies to it.\n */\nexport const violations = (\n table: EndpointConstraints,\n body: NetboxBody,\n options: ViolationOptions = {},\n): readonly string[] => {\n const out: string[] = [];\n for (const [name, rule] of Object.entries(table)) {\n if (!Object.hasOwn(body, name)) {\n if (options.presence === true && rule.required === true) out.push(`${name}: required`);\n continue;\n }\n const value = body[name];\n // ⚠️ AN ARRAY IS A REPEATED VALUE (tags, and every many-to-many), so every element faces the\n // same rule rather than the array being skipped as \"not a scalar\".\n for (const item of Array.isArray(value) ? value : [value]) {\n out.push(...checkValue(name, item, rule));\n }\n }\n return out;\n};\n\n/** The message a plan dies with. ⛔ Names the endpoint, because the rule belongs to the vendor. */\nexport const refusal = (endpoint: EndpointKey, found: readonly string[]): string =>\n `${endpoint} would be rejected by the vendor:\\n - ${found.join('\\n - ')}\\n` +\n \"These are NetBox's own limits, read from its published OpenAPI document \" +\n '(packages/alchemy/src/netbox/generated/constraints). Fix the declaration.';\n",
|
|
8
7
|
"/**\n * Generated NetBox REST API (OpenAPI 3.0.3) request-body constraints for `/api/ipam` — DO NOT EDIT BY HAND.\n *\n * Run: bun codegen/netbox.ts\n * Manifest entry: `netbox-openapi` — NetBox REST API (OpenAPI 3.0.3) 4.7.0\n * sha256 be7f971179b1d6ba, vendor repository, pinned to the release tag\n * contrib/openapi.json in netbox-community/netbox, at tag v4.7.0\n *\n * 2 of this product's 949 POST/PUT/PATCH/DELETE endpoints are tabled across all\n * areas: the ones this package writes to, named in its own source. Every other vendor write\n * endpoint is UNTABLED and therefore unchecked at plan time.\n *\n * ⚠️ A `patternSource` with no `pattern` beside it could NOT be carried into a JavaScript\n * RegExp faithfully — Python's `\\w` is Unicode and JavaScript's is ASCII\n * (codegen/py-pattern.ts). It is recorded and NOT enforced.\n * ⚠️ A FOREIGN KEY CARRIES ONLY ITS TYPE. `tenant`, `vlan`, `role` and friends are\n * `integer|object` in the schema, and the related object's own rules do not govern them.\n */\nimport type { EndpointConstraints } from '../../constraints.ts';\n\nexport const NETBOX_IPAM_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {\n \"netbox:PATCH /api/ipam/prefixes/{id}/\": {\n \"description\": {\"maxLength\":200,\"type\":\"string\"},\n \"prefix\": {\"minLength\":1,\"type\":\"string\"},\n \"status\": {\"enum\":[\"container\",\"active\",\"reserved\",\"deprecated\"],\"type\":\"string\"},\n },\n \"netbox:POST /api/ipam/prefixes/\": {\n \"description\": {\"maxLength\":200,\"type\":\"string\"},\n \"prefix\": {\"minLength\":1,\"required\":true,\"type\":\"string\"},\n \"status\": {\"enum\":[\"container\",\"active\",\"reserved\",\"deprecated\"],\"type\":\"string\"},\n },\n};\n",
|
|
9
8
|
"/**\n * Every generated NetBox constraint table, merged — DO NOT EDIT BY HAND.\n *\n * Run: bun codegen/netbox.ts\n *\n * ⚠️ EVERY KEY IS PREFIXED `netbox:` FOR THE SAME REASON THE PROXMOX TABLES ARE PREFIXED. One\n * estate runs several vendors and the reader is shared; an unprefixed `POST /api/…` would be\n * one vendor away from colliding.\n */\nimport type { EndpointConstraints } from '../../constraints.ts';\nimport { NETBOX_IPAM_CONSTRAINTS } from './netbox-ipam.ts';\n\nexport const NETBOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {\n ...NETBOX_IPAM_CONSTRAINTS,\n};\n\n/**\n * sha256 of the merged table, truncated.\n *\n * ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while\n * changing a 200 to a 201 by hand is. `tests/netbox-manifest.test.ts` recomputes it.\n */\nexport const NETBOX_CONSTRAINTS_DIGEST = '1de1cebe7491d94f';\n",
|
|
10
|
-
"/**\n * `Netbox.Prefix` — one IP prefix and the decision recorded against it.\n *\n * ★ WHY THIS OBJECT CLASS IS THE FIRST ONE, AND NOT `Device`, `VLAN` OR `Site`. Three reasons,\n * each measurable rather than aesthetic:\n *\n * 1. **It is the only interesting NetBox object with no foreign-key prerequisite.** MEASURED in\n * the 4.7.0 schema: `WritablePrefixRequest.required` is exactly `[\"prefix\"]`. A `VLAN` needs\n * `vid` AND `name` and belongs to a group or site; a `Device` needs a role, a type, a site\n * and usually a manufacturer — four more Resources before the first one can be declared.\n * A scaffold that cannot declare anything until five families exist proves nothing.\n * 2. **It is what the estate's NetBox is FOR.** The house's own IPAM agent describes its job as\n * reconciling \"what the network IS against what NetBox says it was DECIDED to be\". A prefix\n * is the atom of that decision. The estate has prefixes whose status is currently recorded\n * only in an SSH config comment and a retirement doc — a management VLAN, a Thunderbolt\n * point-to-point, and three ranges that are retired and still resolve in people's heads.\n * 3. **`status: 'deprecated'` makes retirement declarable.** That is the move this unlocks:\n * a retired range stops being folklore and becomes a line in a stack file with a reviewable\n * diff. No other NetBox class turns an estate decision into one field this cleanly.\n *\n * ⚠️ ADOPT-FRIENDLY BY CONSTRUCTION. `locate` filters on the exact `prefix`; `identifies` then\n * picks the declared VRF, because NetBox allows the same CIDR in two VRFs and in the global\n * table. An instance that already holds `10.0.0.0/24` is BOUND, never duplicated — and ⛔ an\n * adopt never clears prose it did not declare (prefix-form.ts).\n *\n * ⛔ THIS RESOURCE DOES NOT REMOVE PREFIXES IT DOES NOT DECLARE, and `defaultRemovalPolicy` is\n * `retain` (resource.ts). Deleting a prefix in NetBox reparents its children and detaches its\n * IP assignments; the record of why it existed survives only in the change log.\n *\n * ⛔ NO CREDENTIAL IS A PROP. `NETBOX_URL` and `NETBOX_TOKEN` are read from the environment at\n * call time — the house convention, and the only shape that keeps a token out of Alchemy state.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { prefixBody, prefixMatches } from './prefix-form.ts';\nimport {
|
|
9
|
+
"/**\n * `Netbox.Prefix` — one IP prefix and the decision recorded against it.\n *\n * ★ WHY THIS OBJECT CLASS IS THE FIRST ONE, AND NOT `Device`, `VLAN` OR `Site`. Three reasons,\n * each measurable rather than aesthetic:\n *\n * 1. **It is the only interesting NetBox object with no foreign-key prerequisite.** MEASURED in\n * the 4.7.0 schema: `WritablePrefixRequest.required` is exactly `[\"prefix\"]`. A `VLAN` needs\n * `vid` AND `name` and belongs to a group or site; a `Device` needs a role, a type, a site\n * and usually a manufacturer — four more Resources before the first one can be declared.\n * A scaffold that cannot declare anything until five families exist proves nothing.\n * 2. **It is what the estate's NetBox is FOR.** The house's own IPAM agent describes its job as\n * reconciling \"what the network IS against what NetBox says it was DECIDED to be\". A prefix\n * is the atom of that decision. The estate has prefixes whose status is currently recorded\n * only in an SSH config comment and a retirement doc — a management VLAN, a Thunderbolt\n * point-to-point, and three ranges that are retired and still resolve in people's heads.\n * 3. **`status: 'deprecated'` makes retirement declarable.** That is the move this unlocks:\n * a retired range stops being folklore and becomes a line in a stack file with a reviewable\n * diff. No other NetBox class turns an estate decision into one field this cleanly.\n *\n * ⚠️ ADOPT-FRIENDLY BY CONSTRUCTION. `locate` filters on the exact `prefix`; `identifies` then\n * picks the declared VRF, because NetBox allows the same CIDR in two VRFs and in the global\n * table. An instance that already holds `10.0.0.0/24` is BOUND, never duplicated — and ⛔ an\n * adopt never clears prose it did not declare (prefix-form.ts).\n *\n * ⛔ THIS RESOURCE DOES NOT REMOVE PREFIXES IT DOES NOT DECLARE, and `defaultRemovalPolicy` is\n * `retain` (resource.ts). Deleting a prefix in NetBox reparents its children and detaches its\n * IP assignments; the record of why it existed survives only in the change log.\n *\n * ⛔ NO CREDENTIAL IS A PROP. `NETBOX_URL` and `NETBOX_TOKEN` are read from the environment at\n * call time, through `@distilled.cloud/netbox`'s `CredentialsFromEnv` (resource.ts) — the house\n * convention, and the only shape that keeps a token out of Alchemy state. Unchanged by this\n * migration off `client.ts`'s hand-rolled `HttpClient` calls onto distilled's typed operations.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as ipam from '@distilled.cloud/netbox/ipam';\nimport * as Effect from 'effect/Effect';\nimport { prefixBody, prefixMatches } from './prefix-form.ts';\nimport {\n LOCATE_PAGE,\n type NetboxRequirements,\n type NetboxSpec,\n locateOne,\n netboxHandlers,\n} from './resource.ts';\nimport { choice, fk, text } from './values.ts';\n\n/**\n * ⚠️ THE FOUR VALUES ARE THE VENDOR'S, NOT OURS — copied from the generated table, which is\n * generated from NetBox's own enum. The table is what ENFORCES them; this type is what makes\n * the mistake visible in an editor.\n */\nexport type PrefixStatus = 'container' | 'active' | 'reserved' | 'deprecated';\n\nexport interface PrefixProps {\n /** CIDR, exactly as NetBox stores it — `10.0.0.0/24`. The stable key for locate. */\n prefix: string;\n /**\n * VRF id, or omitted for the global table.\n *\n * ⛔ PART OF THE IDENTITY, NOT A SETTING. NetBox's uniqueness on a prefix is per-VRF, so a\n * locate that ignored it could match two rows — which `soleMatch` refuses rather than\n * guessing between.\n */\n vrf?: number;\n status?: PrefixStatus;\n /** ⚠️ `maxLength: 200` in the schema; the generated table enforces it at plan time. */\n description?: string;\n /** Free text. ⚠️ NetBox states no length limit, so nothing here checks one. */\n comments?: string;\n tenant?: number;\n vlan?: number;\n /** All addresses inside are usable. */\n isPool?: boolean;\n markUtilized?: boolean;\n}\n\nexport interface PrefixAttributes {\n prefixId: number;\n prefix: string;\n status: string;\n description: string;\n comments: string;\n vrf: number | undefined;\n tenant: number | undefined;\n vlan: number | undefined;\n isPool: boolean;\n markUtilized: boolean;\n}\n\nexport interface NetboxPrefix extends Resource<\n 'Netbox.Prefix',\n PrefixProps,\n PrefixAttributes,\n never,\n NetboxRequirements\n> {}\n\nexport const NetboxPrefix = Resource<NetboxPrefix>('Netbox.Prefix', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst attributesOf = (live: ipam.Prefix, props: PrefixProps): PrefixAttributes => ({\n comments: text(live.comments),\n description: text(live.description),\n isPool: live.is_pool === true,\n markUtilized: live.mark_utilized === true,\n prefix: live.prefix || props.prefix,\n prefixId: live.id,\n status: choice(live.status),\n tenant: fk(live.tenant),\n vlan: fk(live.vlan),\n vrf: fk(live.vrf),\n});\n\n/**\n * ★ EXPORTED for direct testing with an explicit fake `Credentials` layer — see\n * `fake-netbox.ts`/`prefix.test.ts`, the same seam `../forgejo/repository.ts` uses.\n */\nexport const spec: NetboxSpec<\n PrefixProps,\n ipam.Prefix,\n PrefixAttributes,\n | ipam.CreateIpamPrefixError\n | ipam.ListIpamPrefixesError\n | ipam.UpdateIpamPrefixesPartialError\n | ipam.IpamPrefixesDestroyError\n> = {\n attributes: attributesOf,\n create: (_props, body) =>\n ipam.createIpamPrefix({ body: body as unknown as ipam.WritablePrefixRequest }),\n createBody: prefixBody,\n destroy: (_props, live) => ipam.ipamPrefixesDestroy({ id: live.id }),\n describe: (props) => `ipam/prefixes ${props.prefix}`,\n endpoint: {\n create: 'netbox:POST /api/ipam/prefixes/',\n update: 'netbox:PATCH /api/ipam/prefixes/{id}/',\n },\n /**\n * 🔴 THE VRF IS **NOT** IN THIS SERVER-SIDE FILTER, AND THE REASON IS A GUESS THAT WAS CAUGHT.\n * An earlier version filtered `vrf_id=null` for the global table — NetBox does define\n * `FILTERS_NULL_CHOICE_VALUE = 'null'` and does honour it on filters that opt in. ⛔ MEASURED\n * IN THE VENDOR'S OWN SOURCE at v4.7.0: `PrefixFilterSet.vrf_id` is a plain\n * `django_filters.ModelMultipleChoiceFilter` with NO `null_value`, so `'null'` is validated\n * against the VRF queryset, fails, and NetBox answers 400 under strict filtering. The\n * sentinel is real but it is opt-in per filter, and this one did not opt in.\n * ★ SO THE DISCRIMINATOR STAYS IN THIS PROCESS, VIA `locateOne`'s `identifies` — one extra page\n * of candidates, no vendor semantics guessed. `listIpamPrefixes` never declares `NotFound`\n * (a list call 200s with an empty page, it does not 404), so there is no status to fold here.\n */\n fetchLive: (props) =>\n locateOne(\n ipam.listIpamPrefixes({ limit: LOCATE_PAGE, prefix: [props.prefix] }),\n `ipam/prefixes ${props.prefix}`,\n (row) => fk(row.vrf) === props.vrf,\n ),\n matches: prefixMatches,\n update: {\n body: prefixBody,\n call: (_props, live, body) =>\n ipam.updateIpamPrefixesPartial({\n ...(body as unknown as Omit<ipam.UpdateIpamPrefixesPartialRequest, 'id'>),\n id: live.id,\n }),\n },\n};\n\nexport const handlers = netboxHandlers(spec);\n\nexport const NetboxPrefixProvider = () =>\n Provider.effect(NetboxPrefix, Effect.succeed(NetboxPrefix.Provider.of(handlers)));\n",
|
|
11
10
|
"/**\n * The wire body for a `Netbox.Prefix`, and the comparison that decides whether it needs writing.\n *\n * ★ EXTRACTED SO IT CAN BE TESTED WITHOUT A SERVER. `body` and `matches` are where a NetBox\n * resource either converges or quietly destroys something, and both are pure functions of the\n * props — so they belong somewhere a test can call them with a literal, the way the Proxmox\n * families keep their `*-form.ts` beside the resource.\n */\nimport type { PrefixAttributes, PrefixProps, PrefixStatus } from './prefix.ts';\n\n/** ⚠️ NetBox's own default when the property is omitted, copied from the schema, not assumed. */\nconst DEFAULT_STATUS: PrefixStatus = 'active';\n\n/**\n * The fields an undeclared prop still settles, because NetBox itself would.\n *\n * ⛔ FREE TEXT IS NOT IN HERE, AND THE LINE IS NOT COSMETIC. `status`, `is_pool` and\n * `mark_utilized` have vendor defaults in the schema — omitting one genuinely means \"the\n * default\", so settling it says what NetBox would have done anyway. `description` and\n * `comments` have no such default: the schema's `''` is the absence of a value, not a decision.\n * Treating \"I did not mention it\" as \"set it to empty\" is how a declaration that says nothing\n * about prose ERASES prose somebody wrote.\n */\nexport const settled = (props: PrefixProps) => ({\n isPool: props.isPool ?? false,\n markUtilized: props.markUtilized ?? false,\n status: props.status ?? DEFAULT_STATUS,\n});\n\n/**\n * ⛔ THE BODY IS BUILT ONCE AND USED FOR BOTH CREATE AND UPDATE, so a field cannot be settable on\n * one path and forgotten on the other — the failure mode that makes a resource converge on\n * create and drift forever after.\n *\n * 🔴 AN UNDECLARED FIELD THAT NETBOX DOES NOT DEFAULT IS OMITTED, NEVER SENT EMPTY OR NULL. This\n * resource shipped sending `description: ''` whenever the prop was absent. On a fresh create\n * that is invisible — the field was empty anyway. ⛔ ON AN ADOPT IT IS DATA LOSS: NetBox is the\n * estate's record of DECISIONS, so the description on an existing prefix is usually the only\n * written trace of why that range exists, and the first deploy that adopted it would have\n * PATCHed it to empty. `matches` would have reported drift, the plan would have said `update`,\n * and the diff would have read as converging a declaration rather than deleting a sentence.\n * ★ THE TELL WAS ALREADY IN THIS FILE: foreign keys were omitted for exactly this reason while\n * free text was not. An inconsistency between two fields with the same hazard is the bug.\n * ⚠️ THE COST IS THAT PROSE CANNOT BE CLEARED BY OMISSION — clearing it is `description: ''`,\n * written on purpose, which is the readable way to say a destructive thing.\n */\nexport const prefixBody = (props: PrefixProps): Record<string, unknown> => {\n const fixed = settled(props);\n const out: Record<string, unknown> = {\n is_pool: fixed.isPool,\n mark_utilized: fixed.markUtilized,\n prefix: props.prefix,\n status: fixed.status,\n };\n if (props.description !== undefined) out['description'] = props.description;\n if (props.comments !== undefined) out['comments'] = props.comments;\n if (props.vrf !== undefined) out['vrf'] = props.vrf;\n if (props.tenant !== undefined) out['tenant'] = props.tenant;\n if (props.vlan !== undefined) out['vlan'] = props.vlan;\n return out;\n};\n\n/**\n * Whether the live object already says what the declaration says.\n *\n * ⛔ EVERY FIELD `prefixBody` OMITS IS ALSO UNCOMPARED HERE, AND THE TWO MUST MOVE TOGETHER. A\n * comparison against a field the body does not send reports drift no write can fix: the plan\n * says `update`, the PATCH omits the field, and the next plan says `update` again — forever.\n * An undeclared prop means \"not mine\", in both directions.\n * ⚠️ `vrf` IS ABSENT ON PURPOSE. It is identity, not settings: `identifies` has already proved\n * this row is in the declared VRF, and a prefix's VRF cannot be edited into another one here.\n */\nexport const prefixMatches = (attributes: PrefixAttributes, props: PrefixProps): boolean => {\n const fixed = settled(props);\n return (\n attributes.status === fixed.status &&\n attributes.isPool === fixed.isPool &&\n attributes.markUtilized === fixed.markUtilized &&\n (props.description === undefined || attributes.description === props.description) &&\n (props.comments === undefined || attributes.comments === props.comments) &&\n (props.tenant === undefined || attributes.tenant === props.tenant) &&\n (props.vlan === undefined || attributes.vlan === props.vlan)\n );\n};\n",
|
|
12
|
-
"/**\n * One shape for every NetBox object — the same four operations, different path and JSON body.\n *\n * ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Prefixes, VLANs, devices, tenants\n * and interfaces are all create / read / update / delete over `/api` with JSON bodies and a\n * numeric primary key. Hand-rolling the fourth copy is where someone drops the read-back or the\n * constraint guard and the plan lies.\n *\n * ★ ADOPT IS THE DEFAULT POSTURE, NOT A FLAG. `reconcile` LOCATES before it writes: an object the\n * estate already has is bound, not duplicated. NetBox is a record that predates this code by\n * years; a provider that created a second `10.20.10.0/24` on first deploy would be worse than\n * no provider.\n *\n * ★ `defaultRemovalPolicy: 'retain'` ON EVERY FAMILY. NetBox holds the estate's record of what\n * the network was DECIDED to be, and deleting a row deletes history — child prefixes reparent,\n * IP assignments detach, and the change log is the only trace left. `retain` means the engine\n * skips `delete` when a resource is orphaned unless the caller opts into\n * `.pipe(RemovalPolicy.destroy())`. The `delete` handler is FULLY IMPLEMENTED anyway: a stub\n * that silently does nothing lies to whoever reads the plan.\n */\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { NetboxError, type NetboxList, type NetboxRow, netbox, soleMatch } from './client.ts';\nimport { guardBody } from './constraint-guard.ts';\nimport type { EndpointKey, NetboxBody } from './constraints.ts';\n\n/**\n * ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME, AND NOTHING MORE. The token comes from\n * `NETBOX_TOKEN` in the environment, so there is no process to spawn and no `bao` to shell out\n * to. Declaring a service you never use is not harmless — it makes a stack provide a layer for\n * nothing, and it hides the one service you DO need.\n */\nexport type NetboxRequirements = HttpClient.HttpClient;\n\nexport interface NetboxSpec<Props extends object, Attributes> {\n /** `ipam/prefixes` — where a new one is POSTed and where `locate` filters. */\n readonly collection: string;\n /**\n * The query that narrows the candidates, as `URLSearchParams` pairs.\n *\n * ⛔ ONLY FILTERS THE VENDOR DOCUMENT DECLARES, AND ONLY IN THE FORM IT DECLARES THEM. A\n * filter NetBox rejects answers 400, which `absentOn404` correctly does NOT fold to \"absent\" —\n * so a guessed filter shape is a hard failure on every plan, not a soft one.\n * ★ IT DOES NOT HAVE TO BE UNIQUE. `identifies` finishes the job in this process, where the\n * rule is readable and testable offline.\n */\n readonly locate: (props: Props) => readonly (readonly [string, string])[];\n /**\n * Which of the returned rows IS this object.\n *\n * 🔴 THIS EXISTS BECAUSE THE OBVIOUS ALTERNATIVE WAS A GUESS. `Netbox.Prefix` first narrowed\n * server-side with `vrf_id=null`, the sentinel NetBox uses elsewhere for \"no foreign key\".\n * ⛔ MEASURED IN THE VENDOR'S OWN SOURCE at v4.7.0: `PrefixFilterSet.vrf_id` is a plain\n * `django_filters.ModelMultipleChoiceFilter` with NO `null_value`, so `'null'` is validated\n * against the VRF queryset, fails, and NetBox answers 400 under strict filtering. The\n * sentinel is real (`FILTERS_NULL_CHOICE_VALUE = 'null'`) but it is opt-in per filter, and\n * this one did not opt in.\n * ★ SO THE DISCRIMINATOR MOVED INTO THIS PROCESS. It costs one page of candidates and buys a\n * rule that can be read, tested without a server, and cannot be wrong about a vendor's\n * filter semantics. Omit it when `locate` really is unique on its own.\n */\n readonly identifies?: (live: NetboxRow, props: Props) => boolean;\n readonly attributes: (live: NetboxRow, props: Props) => Attributes | undefined;\n readonly createBody: (props: Props) => NetboxBody;\n /** ⚠️ PARTIAL BY DESIGN: NetBox updates are PATCH, so presence is not checked on this body. */\n readonly updateBody?: (props: Props) => NetboxBody;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n /** The generated constraint table to check each body against, by endpoint key. */\n readonly endpoint: { readonly create: EndpointKey; readonly update?: EndpointKey };\n /** For the message `soleMatch` raises and for read-back failures. */\n readonly describe: (props: Props) => string;\n}\n\n/** ⚠️ Only 404 folds to \"absent\". A 403 or a 502 is a real failure — folding it would plan a create over an existing object. */\nconst absentOn404 = <A>(io: Effect.Effect<A, NetboxError, NetboxRequirements>) =>\n io.pipe(\n Effect.catchIf(\n (cause): cause is NetboxError => cause instanceof NetboxError && cause.status === 404,\n () => Effect.succeed(undefined as A),\n ),\n );\n\n/**\n * ⚠️ NOT `limit=1`, AND NOT `limit=2` EITHER. One row is all a caller wants, but the page has to\n * be wide enough for `identifies` to see every candidate `locate` matched — a prefix that\n * exists in several VRFs comes back several times, and a page of two would hide the third.\n * ★ TWENTY, AND THE NUMBER IS AN ASSERTION: a natural key matching more than twenty rows is not\n * a natural key. `count` is checked against it, so the failure is \"your filter is too broad\"\n * rather than a silent truncation.\n */\nconst LOCATE_PAGE = 20;\n\nconst query = (pairs: readonly (readonly [string, string])[]): string =>\n new URLSearchParams([\n ...pairs.map(([k, v]) => [k, v] as [string, string]),\n ['limit', String(LOCATE_PAGE)],\n ]).toString();\n\nexport const netboxOperations = <Props extends object, Attributes>(\n spec: NetboxSpec<Props, Attributes>,\n) => {\n const fetchLive = (props: Props) =>\n absentOn404(\n netbox<NetboxList<NetboxRow>>('GET', `${spec.collection}/?${query(spec.locate(props))}`),\n ).pipe(\n Effect.map((list) => {\n if (list !== undefined && list.count > LOCATE_PAGE) {\n throw new Error(\n `${spec.describe(props)}: the locate filter matched ${String(list.count)} rows, more ` +\n `than the ${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — ` +\n 'narrow the filter rather than paging through candidates.',\n );\n }\n const rows = list?.results ?? [];\n const narrow = spec.identifies;\n return soleMatch(\n narrow === undefined ? rows : rows.filter((row) => narrow(row, props)),\n spec.describe(props),\n );\n }),\n );\n\n /** ⛔ The numeric `id` is the wire path. NetBox has no update-by-natural-key. */\n const objectPath = (live: NetboxRow): string => `${spec.collection}/${String(live['id'])}`;\n\n const read = (props: Props) =>\n fetchLive(props).pipe(\n Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),\n );\n\n return {\n read,\n\n diff: (news: Input<Props>, output: Attributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live === undefined) return { action: 'update' } as const;\n if (spec.matches(live, news)) return { action: 'noop' } as const;\n return spec.updateBody === undefined\n ? ({ action: 'replace' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: (news: Props) =>\n Effect.gen(function* () {\n const before = yield* fetchLive(news);\n if (before === undefined) {\n const body = spec.createBody(news);\n // ⛔ GUARD BEFORE THE REQUEST, WITH PRESENCE ON. This is the only place a missing\n // required property can still be caught locally.\n yield* guardBody(spec.endpoint.create, body, true);\n yield* netbox('POST', `${spec.collection}/`, body);\n } else if (spec.updateBody !== undefined) {\n const mapped = spec.attributes(before, news);\n // ⚠️ ADOPTION IS THE `mapped === undefined` CASE TOO: a row this spec cannot map is not\n // a row to PATCH blindly.\n if (mapped !== undefined && !spec.matches(mapped, news)) {\n const body = spec.updateBody(news);\n yield* guardBody(spec.endpoint.update, body, false);\n if (Object.keys(body).length > 0) {\n yield* netbox('PATCH', `${objectPath(before)}/`, body);\n }\n }\n }\n const after = yield* read(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n `${spec.describe(news)}: the write returned no error but the object is still ` +\n 'absent. NetBox returns the object it wrote — read back rather than trusting the ' +\n 'status code.',\n ),\n );\n }\n return after;\n }),\n\n destroy: (olds: Props) =>\n Effect.gen(function* () {\n const live = yield* fetchLive(olds);\n if (live === undefined) return;\n yield* netbox('DELETE', `${objectPath(live)}/`);\n }),\n };\n};\n\n/**\n * The five provider handlers for a spec'd NetBox object, wired once.\n *\n * ⚠️ `list` answers empty. NetBox index endpoints return the whole estate — tens of thousands of\n * rows on a real instance — so adoption stays explicit rather than sweeping.\n */\nexport const netboxHandlers = <Props extends object, Attributes extends object>(\n spec: NetboxSpec<Props, Attributes>,\n) => {\n const ops = netboxOperations(spec);\n return {\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: Props }) => ops.read(olds),\n diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>\n ops.diff(news, output),\n reconcile: ({ news }: { news: Props }) => ops.reconcile(news),\n delete: ({ olds }: { olds: Props }) => ops.destroy(olds),\n };\n};\n",
|
|
11
|
+
"/**\n * One shape for every NetBox object — the same four operations, different distilled call.\n *\n * ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Prefixes, VLANs, devices, tenants\n * and interfaces are all create / read / update / delete against `@distilled.cloud/netbox`'s\n * typed operations. Hand-rolling the fourth copy is where someone drops the read-back or the\n * constraint guard and the plan lies.\n *\n * ★ SO THE FOUR OPERATIONS LIVE HERE, ONCE — mirroring `../forgejo/resource.ts`'s engine exactly,\n * plus the constraint-guard wiring every NetBox write already had (guardBody, unchanged).\n *\n * 🔴 WHY THIS REPLACED `client.ts`'s HAND-ROLLED `HttpClient` CALLS. Measured against Tim's\n * distilled research page 2026-09-23: upstream Alchemy only accepts providers that call\n * `@distilled.cloud/<vendor>` operations and `catchTag` their typed errors — not a raw\n * `fetch`/`HttpClient` client over a hand-maintained path table. `@distilled.cloud/netbox`\n * (published as `@homeflare/distilled-netbox@0.2.0`, aliased in — see\n * `docs/distilled-interim.md`) covers every operation this family calls.\n *\n * ⚠️ `NotFound` IS FOLDED TO \"ABSENT\" INSIDE EACH RESOURCE FILE'S OWN `fetchLive`, NOT HERE — the\n * direct successor to the old, centralized `absentOn404`, and the same split `../forgejo/resource.ts`\n * uses. `Effect.catchTag`'s tag-literal inference needs a concrete error union to resolve\n * `\"NotFound\"` against; a spec's `E` is only concrete at each resource file's own call site. A\n * NetBox *list* call (what `locateOne` below wraps) never 404s — narrowing happens by an empty\n * `results` page, not a status code — so nothing here folds a status at all.\n *\n * ★ ADOPT IS THE DEFAULT POSTURE, NOT A FLAG. `reconcile` LOCATES before it writes: an object the\n * estate already has is bound, not duplicated. NetBox is a record that predates this code by\n * years; a provider that created a second `10.20.10.0/24` on first deploy would be worse than\n * no provider.\n *\n * ★ `defaultRemovalPolicy: 'retain'` ON EVERY FAMILY — declared per resource file (prefix.ts),\n * same as before this migration.\n */\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport { CredentialsFromEnv } from '@distilled.cloud/netbox/Credentials';\nimport type { NetboxOpContext } from '@distilled.cloud/netbox/Protocol';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { guardBody } from './constraint-guard.ts';\nimport type { EndpointKey, NetboxBody } from './constraints.ts';\n\n/**\n * ★ WHAT EVERY HANDLER NEEDS FROM THE CALLER'S RUNTIME, after Credentials are provided below.\n * `Netbox.NetboxOpContext` is `Credentials | HttpClient.HttpClient`; `netboxHandlers` closes\n * over `CredentialsFromEnv` itself (`NETBOX_URL` / `NETBOX_TOKEN`, the names client.ts already\n * read — see the SDK's own credentials.ts note), so a consuming stack only has to provide\n * `HttpClient.HttpClient`, exactly as it did before this migration.\n */\nexport type NetboxRequirements = HttpClient.HttpClient;\n\n/**\n * ⚠️ NOT `limit=1`, AND NOT `limit=2` EITHER. One row is all a caller wants, but the page has to\n * be wide enough for an `identifies` narrowing to see every candidate a server-side filter\n * matched — a prefix that exists in several VRFs comes back several times, and a page of two\n * would hide the third.\n * ★ TWENTY, AND THE NUMBER IS AN ASSERTION: a natural key matching more than twenty rows is not\n * a natural key. `count` is checked against it, so the failure is \"your filter is too broad\"\n * rather than a silent truncation. Unchanged from client.ts's original value.\n */\nexport const LOCATE_PAGE = 20;\n\n/**\n * The single row a filtered list returns, or `undefined`.\n *\n * ⛔ MORE THAN ONE MATCH IS A DEFECT, NOT A REASON TO TAKE THE FIRST. A `locate` filter that is\n * not unique would let adopt bind to whichever row NetBox happened to order first, and the next\n * plan would bind to the other one and report drift that is not there.\n */\nexport const soleMatch = <T>(rows: readonly T[], describe: string): T | undefined => {\n if (rows.length > 1) {\n throw new Error(\n `${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` +\n 'unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.',\n );\n }\n return rows[0];\n};\n\n/**\n * List, then narrow to at most one candidate — the same \"which of these rows IS this object\"\n * problem a server-side filter cannot always answer (see prefix.ts's own note on `vrf_id`). Any\n * NetBox resource whose locate is \"filter server-side, then disambiguate in this process\" reaches\n * this once rather than reimplementing the page-width assertion and the ambiguity defect per\n * resource — the direct successor to client.ts's `fetchLive` helper, generalized over the SDK's\n * own paginated-list shape instead of a hand-decoded `NetboxList<NetboxRow>`.\n */\nexport const locateOne = <Row, E, R>(\n list: Effect.Effect<{ readonly count: number; readonly results: readonly Row[] }, E, R>,\n describe: string,\n identifies?: (row: Row) => boolean,\n): Effect.Effect<Row | undefined, E, R> =>\n list.pipe(\n Effect.map((page) => {\n if (page.count > LOCATE_PAGE) {\n throw new Error(\n `${describe}: the locate filter matched ${String(page.count)} rows, more than the ` +\n `${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — narrow the ` +\n 'filter rather than paging through candidates.',\n );\n }\n const rows = identifies === undefined ? page.results : page.results.filter(identifies);\n return soleMatch(rows, describe);\n }),\n );\n\n/**\n * One NetBox object's distilled calls. `Live` is whatever the SDK decodes (a `Prefix`, a `VLAN`,\n * …) — no more untyped `NetboxRow`. `E` is left to each resource file to declare (the union of the\n * distilled operations it actually calls), so a call site's precise per-operation error union\n * flows straight through instead of being widened by hand here.\n */\nexport type NetboxSpec<Props extends object, Live, Attributes, E> = {\n /** Already folds any real \"absent\" outcome (an empty list, or a `catchTag('NotFound', ...)`). */\n readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, NetboxOpContext>;\n readonly attributes: (live: Live, props: Props) => Attributes | undefined;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n readonly createBody: (props: Props) => NetboxBody;\n readonly create: (props: Props, body: NetboxBody) => Effect.Effect<unknown, E, NetboxOpContext>;\n /** Absent for a create-only family — reconcile then never attempts a write against an adopted row. */\n readonly update?: {\n /** ⚠️ PARTIAL BY DESIGN: NetBox updates are PATCH, so presence is not checked on this body. */\n readonly body: (props: Props) => NetboxBody;\n readonly call: (\n props: Props,\n live: Live,\n body: NetboxBody,\n ) => Effect.Effect<unknown, E, NetboxOpContext>;\n };\n readonly destroy: (props: Props, live: Live) => Effect.Effect<unknown, E, NetboxOpContext>;\n /** The generated constraint table to check each body against, by endpoint key. */\n readonly endpoint: { readonly create: EndpointKey; readonly update?: EndpointKey };\n /** For the message `soleMatch` raises and for read-back failures. */\n readonly describe: (props: Props) => string;\n};\n\nexport const netboxOperations = <Props extends object, Live, Attributes, E>(\n spec: NetboxSpec<Props, Live, Attributes, E>,\n) => {\n const read = (props: Props) =>\n spec\n .fetchLive(props)\n .pipe(Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))));\n\n return {\n read,\n\n diff: (news: Input<Props>, output: Attributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live === undefined) return { action: 'update' } as const;\n if (spec.matches(live, news)) return { action: 'noop' } as const;\n return spec.update === undefined\n ? ({ action: 'replace' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: (news: Props) =>\n Effect.gen(function* () {\n const before = yield* spec.fetchLive(news);\n if (before === undefined) {\n const body = spec.createBody(news);\n // ⛔ GUARD BEFORE THE REQUEST, WITH PRESENCE ON. This is the only place a missing\n // required property can still be caught locally.\n yield* guardBody(spec.endpoint.create, body, true);\n yield* spec.create(news, body);\n } else if (spec.update !== undefined) {\n const mapped = spec.attributes(before, news);\n // ⚠️ ADOPTION IS THE `mapped === undefined` CASE TOO: a row this spec cannot map is not\n // a row to PATCH blindly.\n if (mapped !== undefined && !spec.matches(mapped, news)) {\n const body = spec.update.body(news);\n yield* guardBody(spec.endpoint.update, body, false);\n if (Object.keys(body).length > 0) {\n yield* spec.update.call(news, before, body);\n }\n }\n }\n const after = yield* read(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n `${spec.describe(news)}: the write returned no error but the object is still ` +\n 'absent. NetBox returns the object it wrote — read back rather than trusting the ' +\n 'status code.',\n ),\n );\n }\n return after;\n }),\n\n destroy: (olds: Props) =>\n Effect.gen(function* () {\n const live = yield* spec.fetchLive(olds);\n if (live === undefined) return;\n yield* spec.destroy(olds, live);\n }),\n };\n};\n\n/**\n * The five provider handlers for a spec'd NetBox object, wired once.\n *\n * ★ CREDENTIALS ARE PROVIDED HERE, ONCE, NOT BY EVERY CALLER. `CredentialsFromEnv` resolves\n * `NETBOX_URL` / `NETBOX_TOKEN` on the calling fiber per request, so this closure keeps that\n * laziness — nothing is captured at module load. ⛔ THE TOKEN IS NEVER A PROP: Alchemy persists\n * attributes unencrypted, so nothing stored in a stack file may be a credential.\n *\n * ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER — same reasoning as `forgejoHandlers`.\n *\n * ⚠️ `list` answers empty. NetBox index endpoints return the whole estate — tens of thousands of\n * rows on a real instance — so adoption stays explicit rather than sweeping.\n */\nexport const netboxHandlers = <Props extends object, Live, Attributes extends object, E>(\n spec: NetboxSpec<Props, Live, Attributes, E>,\n) => {\n const ops = netboxOperations(spec);\n const withCredentials = <A>(effect: Effect.Effect<A, E, NetboxOpContext>) =>\n Effect.provide(effect, CredentialsFromEnv);\n return {\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: Props }) => withCredentials(ops.read(olds)),\n diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>\n withCredentials(ops.diff(news, output)),\n reconcile: ({ news }: { news: Props }) => withCredentials(ops.reconcile(news)),\n delete: ({ olds }: { olds: Props }) => withCredentials(ops.destroy(olds)),\n };\n};\n",
|
|
13
12
|
"/**\n * Reading a NetBox row into settled values.\n *\n * ⛔ A NETBOX READ AND A NETBOX WRITE ARE NOT THE SAME SHAPE, AND THIS IS WHERE THAT IS HANDLED.\n * MEASURED in the 4.7.0 document: `WritablePrefixRequest.status` is a bare string enum\n * (`\"active\"`), while `Prefix.status` on the way back is an OBJECT — `{value, label}`. A `diff`\n * that compared the declared string against the live object would see drift on every plan, for\n * every prefix, forever, and each \"update\" would PATCH the same value back.\n * ⚠️ The same asymmetry holds for every choice field in NetBox (`dcim` device `status`,\n * `ipam` IP `role`, …). Anything read through `choice` is safe against it.\n */\n\n/** ⚠️ `''` for absent, not `undefined` — NetBox stores an empty string, never null, for text. */\nexport const text = (value: unknown): string => (typeof value === 'string' ? value : '');\n\nexport const bool = (value: unknown): boolean => value === true;\n\n/** A `{value, label}` choice object, reduced to the string a declaration writes. */\nexport const choice = (value: unknown): string => {\n if (typeof value === 'string') return value;\n if (typeof value === 'object' && value !== null && 'value' in value) {\n return text((value as { value: unknown }).value);\n }\n return '';\n};\n\n/**\n * A foreign key, as the integer a declaration writes.\n *\n * ⚠️ IT ARRIVES AS A NESTED OBJECT ON READ AND AN INTEGER ON WRITE — `{id, url, display, …}`\n * versus `4`. Comparing the two directly is the same never-converges bug as `choice`.\n */\nexport const fk = (value: unknown): number | undefined => {\n if (typeof value === 'number') return value;\n if (typeof value === 'object' && value !== null && 'id' in value) {\n const id = (value as { id: unknown }).id;\n return typeof id === 'number' ? id : undefined;\n }\n return undefined;\n};\n"
|
|
14
13
|
],
|
|
15
|
-
"mappings": ";
|
|
16
|
-
"debugId": "
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "546DA12AD639183464756E2164756E21",
|
|
17
16
|
"names": []
|
|
18
17
|
}
|
package/dist/netbox/prefix.d.ts
CHANGED
|
@@ -28,11 +28,15 @@
|
|
|
28
28
|
* IP assignments; the record of why it existed survives only in the change log.
|
|
29
29
|
*
|
|
30
30
|
* ⛔ NO CREDENTIAL IS A PROP. `NETBOX_URL` and `NETBOX_TOKEN` are read from the environment at
|
|
31
|
-
* call time
|
|
31
|
+
* call time, through `@distilled.cloud/netbox`'s `CredentialsFromEnv` (resource.ts) — the house
|
|
32
|
+
* convention, and the only shape that keeps a token out of Alchemy state. Unchanged by this
|
|
33
|
+
* migration off `client.ts`'s hand-rolled `HttpClient` calls onto distilled's typed operations.
|
|
32
34
|
*/
|
|
33
35
|
import { Resource } from 'alchemy';
|
|
34
36
|
import * as Provider from 'alchemy/Provider';
|
|
35
|
-
import
|
|
37
|
+
import * as ipam from '@distilled.cloud/netbox/ipam';
|
|
38
|
+
import * as Effect from 'effect/Effect';
|
|
39
|
+
import { type NetboxRequirements, type NetboxSpec } from './resource.ts';
|
|
36
40
|
/**
|
|
37
41
|
* ⚠️ THE FOUR VALUES ARE THE VENDOR'S, NOT OURS — copied from the generated table, which is
|
|
38
42
|
* generated from NetBox's own enum. The table is what ENFORCES them; this type is what makes
|
|
@@ -76,5 +80,32 @@ export interface PrefixAttributes {
|
|
|
76
80
|
export interface NetboxPrefix extends Resource<'Netbox.Prefix', PrefixProps, PrefixAttributes, never, NetboxRequirements> {
|
|
77
81
|
}
|
|
78
82
|
export declare const NetboxPrefix: import("alchemy").ResourceClass<NetboxPrefix>;
|
|
83
|
+
/**
|
|
84
|
+
* ★ EXPORTED for direct testing with an explicit fake `Credentials` layer — see
|
|
85
|
+
* `fake-netbox.ts`/`prefix.test.ts`, the same seam `../forgejo/repository.ts` uses.
|
|
86
|
+
*/
|
|
87
|
+
export declare const spec: NetboxSpec<PrefixProps, ipam.Prefix, PrefixAttributes, ipam.CreateIpamPrefixError | ipam.ListIpamPrefixesError | ipam.UpdateIpamPrefixesPartialError | ipam.IpamPrefixesDestroyError>;
|
|
88
|
+
export declare const handlers: {
|
|
89
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
90
|
+
read: ({ olds }: {
|
|
91
|
+
olds: PrefixProps;
|
|
92
|
+
}) => Effect.Effect<PrefixAttributes | undefined, import("@distilled.cloud/core/errors").BadGateway | ipam.BadRequest | import("@distilled.cloud/core/errors").ConfigError | ipam.Forbidden | import("@distilled.cloud/core/errors").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/core/errors").InternalServerError | import("@distilled.cloud/netbox/Errors").NetboxParseError | ipam.NotFound | import("@distilled.cloud/core/errors").ServiceUnavailable | import("@distilled.cloud/core/errors").TooManyRequests | import("@distilled.cloud/core/errors").Unauthorized | import("@distilled.cloud/netbox/Errors").UnknownNetboxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
93
|
+
diff: ({ news, output }: {
|
|
94
|
+
news: import("alchemy").Input<PrefixProps>;
|
|
95
|
+
output: PrefixAttributes | undefined;
|
|
96
|
+
}) => Effect.Effect<{
|
|
97
|
+
readonly action: 'update';
|
|
98
|
+
} | {
|
|
99
|
+
readonly action: 'noop';
|
|
100
|
+
} | {
|
|
101
|
+
readonly action: 'replace';
|
|
102
|
+
} | undefined, import("@distilled.cloud/core/errors").BadGateway | ipam.BadRequest | import("@distilled.cloud/core/errors").ConfigError | ipam.Forbidden | import("@distilled.cloud/core/errors").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/core/errors").InternalServerError | import("@distilled.cloud/netbox/Errors").NetboxParseError | ipam.NotFound | import("@distilled.cloud/core/errors").ServiceUnavailable | import("@distilled.cloud/core/errors").TooManyRequests | import("@distilled.cloud/core/errors").Unauthorized | import("@distilled.cloud/netbox/Errors").UnknownNetboxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
103
|
+
reconcile: ({ news }: {
|
|
104
|
+
news: PrefixProps;
|
|
105
|
+
}) => Effect.Effect<PrefixAttributes, import("@distilled.cloud/core/errors").BadGateway | ipam.BadRequest | import("@distilled.cloud/core/errors").ConfigError | ipam.Forbidden | import("@distilled.cloud/core/errors").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/core/errors").InternalServerError | import("@distilled.cloud/netbox/Errors").NetboxParseError | ipam.NotFound | import("@distilled.cloud/core/errors").ServiceUnavailable | import("@distilled.cloud/core/errors").TooManyRequests | import("@distilled.cloud/core/errors").Unauthorized | import("@distilled.cloud/netbox/Errors").UnknownNetboxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
106
|
+
delete: ({ olds }: {
|
|
107
|
+
olds: PrefixProps;
|
|
108
|
+
}) => Effect.Effect<void, import("@distilled.cloud/core/errors").BadGateway | ipam.BadRequest | import("@distilled.cloud/core/errors").ConfigError | ipam.Forbidden | import("@distilled.cloud/core/errors").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/core/errors").InternalServerError | import("@distilled.cloud/netbox/Errors").NetboxParseError | ipam.NotFound | import("@distilled.cloud/core/errors").ServiceUnavailable | import("@distilled.cloud/core/errors").TooManyRequests | import("@distilled.cloud/core/errors").Unauthorized | import("@distilled.cloud/netbox/Errors").UnknownNetboxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
109
|
+
};
|
|
79
110
|
export declare const NetboxPrefixProvider: () => import("effect/Layer").Layer<Provider.Provider<NetboxPrefix>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
80
111
|
//# sourceMappingURL=prefix.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefix.d.ts","sourceRoot":"","sources":["../../src/netbox/prefix.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"prefix.d.ts","sourceRoot":"","sources":["../../src/netbox/prefix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,IAAI,MAAM,8BAA8B,CAAC;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EAGhB,MAAM,eAAe,CAAC;AAGvB;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;AAE9E,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,KAAK,EACL,kBAAkB,CACnB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAeH;;;GAGG;AACH,eAAO,MAAM,IAAI,EAAE,UAAU,CAC3B,WAAW,EACX,IAAI,CAAC,MAAM,EACX,gBAAgB,EACd,IAAI,CAAC,qBAAqB,GAC1B,IAAI,CAAC,qBAAqB,GAC1B,IAAI,CAAC,8BAA8B,GACnC,IAAI,CAAC,wBAAwB,CAuChC,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;CAAuB,CAAC;AAE7C,eAAO,MAAM,oBAAoB,kIACkD,CAAC"}
|