@homeflare/alchemy 0.28.0 → 0.29.1
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/README.md +2 -0
- package/dist/caddy/admin-address.d.ts +27 -0
- package/dist/caddy/admin-address.d.ts.map +1 -0
- package/dist/caddy/admin-calls.d.ts +25 -28
- package/dist/caddy/admin-calls.d.ts.map +1 -1
- package/dist/caddy/admin.d.ts +28 -38
- package/dist/caddy/admin.d.ts.map +1 -1
- package/dist/caddy/caddy-http-client.d.ts +25 -0
- package/dist/caddy/caddy-http-client.d.ts.map +1 -0
- package/dist/caddy/config-errors.d.ts +24 -0
- package/dist/caddy/config-errors.d.ts.map +1 -0
- package/dist/caddy/config-lifecycle.d.ts +20 -49
- package/dist/caddy/config-lifecycle.d.ts.map +1 -1
- package/dist/caddy/config-reconcile.d.ts +45 -0
- package/dist/caddy/config-reconcile.d.ts.map +1 -0
- package/dist/caddy/config.d.ts +4 -3
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/fake-caddy.d.ts +10 -2
- package/dist/caddy/fake-caddy.d.ts.map +1 -1
- package/dist/caddy/index.d.ts +3 -3
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +487 -444
- package/dist/caddy/index.js.map +16 -12
- package/dist/caddy/local-admin.d.ts +20 -34
- package/dist/caddy/local-admin.d.ts.map +1 -1
- package/dist/caddy/providers.d.ts +19 -3
- package/dist/caddy/providers.d.ts.map +1 -1
- 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/github/declare-repo-baseline.d.ts +1 -1
- package/dist/github/declare-repo-baseline.d.ts.map +1 -1
- package/dist/github/index.d.ts +8 -4
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +83 -24
- package/dist/github/index.js.map +14 -12
- package/dist/github/repo-apply-order-fake-providers.d.ts +48 -0
- package/dist/github/repo-apply-order-fake-providers.d.ts.map +1 -0
- package/dist/github/repo-apply-order-fake-stack.d.ts +13 -0
- package/dist/github/repo-apply-order-fake-stack.d.ts.map +1 -0
- package/dist/github/repo-auto-merge-gate.d.ts +54 -0
- package/dist/github/repo-auto-merge-gate.d.ts.map +1 -0
- package/dist/github/repo-baseline-data.d.ts +10 -1
- package/dist/github/repo-baseline-data.d.ts.map +1 -1
- package/dist/github/repo-policy.d.ts +1 -1
- package/dist/github/repo-policy.d.ts.map +1 -1
- package/dist/github/repository-ruleset-errors.d.ts +3 -2
- package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
- package/dist/github/repository-ruleset-form.d.ts.map +1 -1
- package/dist/github/repository-ruleset-guards.d.ts +3 -2
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
- package/dist/github/repository-ruleset-octokit.d.ts +49 -0
- package/dist/github/repository-ruleset-octokit.d.ts.map +1 -1
- package/dist/github/repository-ruleset-probe.d.ts +2 -1
- package/dist/github/repository-ruleset-probe.d.ts.map +1 -1
- package/dist/github/repository-ruleset-props.d.ts +7 -4
- package/dist/github/repository-ruleset-props.d.ts.map +1 -1
- package/dist/github/repository-ruleset-providers.d.ts +42 -0
- package/dist/github/repository-ruleset-providers.d.ts.map +1 -0
- package/dist/github/repository-ruleset.d.ts +26 -4
- package/dist/github/repository-ruleset.d.ts.map +1 -1
- package/dist/litellm/fake-litellm.d.ts +27 -16
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/fake-stack.d.ts +9 -4
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/index.d.ts +7 -5
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +53 -123
- package/dist/litellm/index.js.map +8 -8
- package/dist/litellm/operations.d.ts +60 -0
- package/dist/litellm/operations.d.ts.map +1 -0
- package/dist/litellm/pass-through-endpoint.d.ts +10 -28
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
- package/dist/litellm/pass-through-errors.d.ts +37 -0
- package/dist/litellm/pass-through-errors.d.ts.map +1 -0
- package/dist/litellm/pass-through-form.d.ts +11 -4
- package/dist/litellm/pass-through-form.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +7 -5
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/netbox/prefix.d.ts +4 -4
- package/dist/paperless/custom-field.d.ts +21 -21
- package/dist/paperless/custom-field.d.ts.map +1 -1
- package/dist/paperless/document-type.d.ts +34 -4
- package/dist/paperless/document-type.d.ts.map +1 -1
- package/dist/paperless/fake-paperless.d.ts +36 -19
- package/dist/paperless/fake-paperless.d.ts.map +1 -1
- package/dist/paperless/index.d.ts +7 -2
- package/dist/paperless/index.d.ts.map +1 -1
- package/dist/paperless/index.js +164 -228
- package/dist/paperless/index.js.map +10 -13
- package/dist/paperless/matching-locate.d.ts +42 -20
- package/dist/paperless/matching-locate.d.ts.map +1 -1
- package/dist/paperless/matching-types.d.ts +47 -18
- package/dist/paperless/matching-types.d.ts.map +1 -1
- package/dist/paperless/matching.d.ts +30 -27
- package/dist/paperless/matching.d.ts.map +1 -1
- package/dist/paperless/providers.d.ts +13 -7
- package/dist/paperless/providers.d.ts.map +1 -1
- package/dist/paperless/storage-path.d.ts +34 -4
- package/dist/paperless/storage-path.d.ts.map +1 -1
- package/dist/paperless/tag.d.ts +43 -8
- package/dist/paperless/tag.d.ts.map +1 -1
- package/dist/proxmox/acl.d.ts +30 -23
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/distilled-pve.d.ts +89 -0
- package/dist/proxmox/distilled-pve.d.ts.map +1 -0
- package/dist/proxmox/index.js +663 -609
- package/dist/proxmox/index.js.map +12 -11
- package/dist/proxmox/members.d.ts +29 -10
- package/dist/proxmox/members.d.ts.map +1 -1
- package/docs/caddy-admin.md +1 -1
- package/docs/caddy.md +3 -2
- package/docs/credentials.md +16 -0
- package/docs/litellm.md +21 -13
- package/docs/paperless.md +12 -0
- package/docs/peers.md +5 -0
- package/docs/repo-policy.md +16 -1
- package/docs/repository-ruleset.md +53 -13
- package/docs/upstream-conformance.md +126 -10
- package/package.json +5 -2
- package/src/caddy/admin-address.ts +71 -0
- package/src/caddy/admin-calls.ts +48 -100
- package/src/caddy/admin.ts +37 -41
- package/src/caddy/adopt-scope.test.ts +1 -1
- package/src/caddy/caddy-http-client.ts +229 -0
- package/src/caddy/config-adopt-provider.test.ts +139 -0
- package/src/caddy/config-adopt.test.ts +60 -126
- package/src/caddy/config-errors.ts +47 -0
- package/src/caddy/config-lifecycle.test.ts +55 -42
- package/src/caddy/config-lifecycle.ts +72 -162
- package/src/caddy/config-reconcile.ts +138 -0
- package/src/caddy/config.ts +36 -42
- package/src/caddy/fake-caddy.ts +20 -2
- package/src/caddy/index.ts +3 -8
- package/src/caddy/local-admin.test.ts +35 -21
- package/src/caddy/local-admin.ts +60 -167
- package/src/caddy/providers.test.ts +14 -6
- package/src/caddy/providers.ts +20 -4
- package/src/github/declare-repo-baseline-apply-order.test.ts +86 -0
- package/src/github/declare-repo-baseline.ts +40 -7
- package/src/github/index.ts +8 -4
- package/src/github/repo-apply-order-fake-providers.ts +119 -0
- package/src/github/repo-apply-order-fake-stack.ts +74 -0
- package/src/github/repo-auto-merge-gate.ts +57 -0
- package/src/github/repo-baseline-data.test.ts +13 -0
- package/src/github/repo-baseline-data.ts +11 -2
- package/src/github/repo-policy-apply-order.test.ts +72 -0
- package/src/github/repo-policy-stack.test.ts +59 -0
- package/src/github/repo-policy.ts +31 -3
- package/src/github/repository-ruleset-errors.ts +6 -4
- package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
- package/src/github/repository-ruleset-exact-adopt.test.ts +14 -9
- package/src/github/repository-ruleset-form.ts +7 -3
- package/src/github/repository-ruleset-guards.ts +3 -2
- package/src/github/repository-ruleset-octokit.test.ts +141 -0
- package/src/github/repository-ruleset-octokit.ts +121 -30
- package/src/github/repository-ruleset-probe.ts +2 -1
- package/src/github/repository-ruleset-props.ts +7 -4
- package/src/github/repository-ruleset-providers.ts +46 -0
- package/src/github/repository-ruleset-wire.test.ts +38 -27
- package/src/github/repository-ruleset.ts +27 -3
- package/src/litellm/fake-litellm.ts +83 -78
- package/src/litellm/fake-stack.ts +13 -5
- package/src/litellm/index.ts +7 -21
- package/src/litellm/operations.ts +149 -0
- package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +17 -20
- package/src/litellm/pass-through-endpoint-replace.test.ts +3 -4
- package/src/litellm/pass-through-endpoint.test.ts +8 -8
- package/src/litellm/pass-through-endpoint.ts +21 -31
- package/src/litellm/pass-through-errors.ts +37 -0
- package/src/litellm/pass-through-form.ts +20 -10
- package/src/litellm/providers.ts +8 -7
- package/src/paperless/custom-field.test.ts +49 -40
- package/src/paperless/custom-field.ts +57 -33
- package/src/paperless/document-type.test.ts +48 -0
- package/src/paperless/document-type.ts +51 -22
- package/src/paperless/fake-paperless.ts +63 -72
- package/src/paperless/index.ts +7 -13
- package/src/paperless/matching-locate.test.ts +102 -0
- package/src/paperless/matching-locate.ts +80 -86
- package/src/paperless/matching-types.ts +49 -16
- package/src/paperless/matching.ts +77 -90
- package/src/paperless/providers.ts +14 -10
- package/src/paperless/storage-path.test.ts +56 -0
- package/src/paperless/storage-path.ts +53 -24
- package/src/paperless/tag-identity.test.ts +127 -0
- package/src/paperless/tag.test.ts +100 -0
- package/src/paperless/tag.ts +62 -32
- package/src/proxmox/acl.test.ts +145 -0
- package/src/proxmox/acl.ts +128 -150
- package/src/proxmox/distilled-pve.test.ts +231 -0
- package/src/proxmox/distilled-pve.ts +147 -0
- package/src/proxmox/members-timeout.test.ts +161 -0
- package/src/proxmox/members.test.ts +9 -0
- package/src/proxmox/members.ts +36 -2
- package/dist/litellm/client.d.ts +0 -76
- package/dist/litellm/client.d.ts.map +0 -1
- package/dist/litellm/credentials.d.ts +0 -48
- package/dist/litellm/credentials.d.ts.map +0 -1
- package/dist/litellm/generated/pass-through.d.ts +0 -71
- package/dist/litellm/generated/pass-through.d.ts.map +0 -1
- package/dist/paperless/client.d.ts +0 -37
- package/dist/paperless/client.d.ts.map +0 -1
- package/dist/paperless/credentials.d.ts +0 -40
- package/dist/paperless/credentials.d.ts.map +0 -1
- package/dist/paperless/errors.d.ts +0 -49
- package/dist/paperless/errors.d.ts.map +0 -1
- package/src/litellm/client.ts +0 -195
- package/src/litellm/credentials.ts +0 -73
- package/src/litellm/generated/pass-through.ts +0 -73
- package/src/paperless/client.ts +0 -88
- package/src/paperless/credentials.ts +0 -69
- package/src/paperless/errors.ts +0 -62
- package/src/paperless/matching-identity.test.ts +0 -194
- package/src/paperless/matching.test.ts +0 -128
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* C1 cluster members: ordered failover, last-good preference, transport classification.
|
|
3
|
-
*
|
|
4
|
-
* ★ ANY MEMBER'S :8006 API MANAGES THE WHOLE CLUSTER — PVE proxies node paths itself — so the
|
|
5
|
-
* provider reaches the cluster through whichever member is up, not through one pinned node.
|
|
6
|
-
*
|
|
7
|
-
* ⛔ NEVER cluster-c1.example.com: it resolves to 192.0.2.250, which nothing serves.
|
|
8
|
-
*/
|
|
1
|
+
import type * as Duration from 'effect/Duration';
|
|
9
2
|
import * as Effect from 'effect/Effect';
|
|
10
3
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
11
4
|
import type * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
@@ -14,6 +7,21 @@ import type { PveTarget } from './credentials.ts';
|
|
|
14
7
|
export declare const PVE_API_PORT = 8006;
|
|
15
8
|
/** Strict TLS, always :8006 — each node's certificate carries its mgmt name as a SAN. */
|
|
16
9
|
export declare const pveApiBase: (member: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Bound on ONE member attempt, shared by `client.ts`'s `executeOnCluster` and
|
|
12
|
+
* `distilled-pve.ts`'s `runPveWith` — both loop over `orderedMembers` and both call this file's
|
|
13
|
+
* classifiers, so one constant keeps them in the same failure-mode boundary.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ MEASURED GAP, FIXED 2026-09-24: before this, NOTHING bounded a single attempt. A member that
|
|
16
|
+
* accepted the TCP connection but never answered hung the whole call forever — the for-loop
|
|
17
|
+
* never produced a failure for `isTransportFailure`/`isPreSendTransport` to classify, so it
|
|
18
|
+
* never advanced to a healthy member. `isTimeoutCause` below existed already (written for this
|
|
19
|
+
* exact case, per its own comment: "no answer after connect") but nothing ever called
|
|
20
|
+
* `Effect.timeout` to produce one.
|
|
21
|
+
* ★ TB4 (the live 3-member cluster, `homeflare-proxmox/src/target.ts`) waits at most 3× this
|
|
22
|
+
* before `PveClusterExhausted` — well under S26's ~90-second ceiling on a bounded wait.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MEMBER_TIMEOUT: Duration.Input;
|
|
17
25
|
/** ⚠️ TESTS ONLY — resets sticky member choice between cases. */
|
|
18
26
|
export declare const resetLastGoodForTest: () => void;
|
|
19
27
|
export declare const noteGoodMember: (target: PveTarget, member: string) => void;
|
|
@@ -24,9 +32,16 @@ export declare const orderedMembers: (target: PveTarget) => readonly string[];
|
|
|
24
32
|
* ★ TransportError | InvalidUrlError | EncodeError occur before a response exists; StatusCodeError
|
|
25
33
|
* and kin include a response. client.execute does NOT filter status, so HTTP answers are never
|
|
26
34
|
* TransportError — only the transport layer is.
|
|
35
|
+
* ★ `Cause.TimeoutError` (from `MEMBER_TIMEOUT` below, via `Effect.timeout`) counts too — a read
|
|
36
|
+
* with no side effect to duplicate fails over on ANY transport failure, a hang included.
|
|
27
37
|
*/
|
|
28
38
|
export declare const isTransportFailure: (cause: unknown) => boolean;
|
|
29
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Pre-send only: connection refused, DNS, a certificate refused — never a timeout after connect.
|
|
41
|
+
* ⛔ `Cause.TimeoutError` (MEMBER_TIMEOUT firing) falls through to `false` here on purpose, same
|
|
42
|
+
* as any other non-`HttpClientError` cause: the request may already have reached the member, so
|
|
43
|
+
* a write must not repeat it elsewhere.
|
|
44
|
+
*/
|
|
30
45
|
export declare const isPreSendTransport: (cause: unknown) => boolean;
|
|
31
46
|
export declare const describeTransport: (cause: unknown) => string;
|
|
32
47
|
export type MemberAttempt = {
|
|
@@ -46,8 +61,12 @@ export type PveMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
|
46
61
|
* ★ Reads fail over on any transport failure, including timeout — no side effect to duplicate.
|
|
47
62
|
* MEASURED 2026-09-14: full C1 plans stayed all-noop with the first member refused (127.0.0.1)
|
|
48
63
|
* and with it unresolvable (n9.invalid).
|
|
64
|
+
*
|
|
65
|
+
* @param timeout Bound on ONE member's attempt — defaults to `MEMBER_TIMEOUT`. A test passes a
|
|
66
|
+
* short one to prove failover happens without waiting out the real bound; production code never
|
|
67
|
+
* overrides it.
|
|
49
68
|
*/
|
|
50
|
-
export declare const executeOnCluster: (target: PveTarget, method: PveMethod, path: string, build: (apiBase: string) => HttpClientRequest.HttpClientRequest) => Effect.Effect<HttpClientResponse.HttpClientResponse, {
|
|
69
|
+
export declare const executeOnCluster: (target: PveTarget, method: PveMethod, path: string, build: (apiBase: string) => HttpClientRequest.HttpClientRequest, timeout?: Duration.Input) => Effect.Effect<HttpClientResponse.HttpClientResponse, {
|
|
51
70
|
readonly tag: 'cluster';
|
|
52
71
|
readonly method: PveMethod;
|
|
53
72
|
readonly path: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"members.d.ts","sourceRoot":"","sources":["../../src/proxmox/members.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"members.d.ts","sourceRoot":"","sources":["../../src/proxmox/members.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAE9D,OAAO,KAAK,KAAK,iBAAiB,MAAM,wCAAwC,CAAC;AACjF,OAAO,KAAK,KAAK,kBAAkB,MAAM,yCAAyC,CAAC;AACnF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,yFAAyF;AACzF,eAAO,MAAM,UAAU,WAAY,MAAM,WAA0D,CAAC;AAEpG;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,KAAoB,CAAC;AAK3D,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,YAAgC,CAAC;AAElE,eAAO,MAAM,cAAc,WAAY,SAAS,UAAU,MAAM,SAE/D,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,cAAc,WAAY,SAAS,KAAG,SAAS,MAAM,EAIjE,CAAC;AA2CF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,UAAW,OAAO,KAAG,OAKnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,UAAW,OAAO,KAAG,OASnD,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,OAAO,KAAG,MAIlD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE1D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,WACnB,SAAS,UACT,SAAS,QACX,MAAM,SACL,CAAC,OAAO,EAAE,MAAM,KAAK,iBAAiB,CAAC,iBAAiB,YACtD,QAAQ,CAAC,KAAK,KACtB,MAAM,CAAC,MAAM,CACd,kBAAkB,CAAC,kBAAkB,EACrC;IACE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C,EACD,UAAU,CAAC,UAAU,CAqCnB,CAAC"}
|
package/docs/caddy-admin.md
CHANGED
|
@@ -13,7 +13,7 @@ The overview is [caddy.md](./caddy.md).
|
|
|
13
13
|
- **`admin { origins … }`** narrowed, or **an SSH-forwarded port** (Caddy checks the Host against
|
|
14
14
|
ITS port): pass `hostHeader: 'localhost:2019'`.
|
|
15
15
|
- **Another host's Caddy** (SSH later): forward its socket or port to loopback here and point a
|
|
16
|
-
`localCaddyAdmin()` at it, or implement `
|
|
16
|
+
`localCaddyAdmin()` at it, or implement `CaddyTransport` over your own route. Never expose :2019.
|
|
17
17
|
- **A Caddyfile cannot strand the provider.** Before loading, the adapted `admin` block is refused
|
|
18
18
|
if it turns the API off, listens off loopback or somewhere the transport does not reach (another
|
|
19
19
|
port, socket or loopback address: `[::1]` is not `127.0.0.1`, and `localhost` binds IPv4), allows
|
package/docs/caddy.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Declare a running Caddy's config as Caddyfile text and apply it through Caddy's own admin API:
|
|
4
4
|
`POST /load` to apply (a graceful reload), `GET /config/` to detect drift. `caddyWithFile()` also
|
|
5
5
|
writes the same Caddyfile to the file Caddy starts from, with launchd's `HostFile`, so a restart
|
|
6
|
-
keeps it. Every admin call goes through
|
|
6
|
+
keeps it. Every admin call goes through `@distilled.cloud/caddy`'s typed operations, over one
|
|
7
|
+
injectable `CaddyTransport` (`Credentials` + `HttpClient.HttpClient`).
|
|
7
8
|
|
|
8
9
|
```ts
|
|
9
10
|
import * as Effect from 'effect/Effect';
|
|
@@ -193,7 +194,7 @@ it by hand at its admin API.
|
|
|
193
194
|
- ⚠️ **Bootstrap race.** Where Caddy is not running yet, the config can reach the admin API before
|
|
194
195
|
its launchd job has Caddy listening. The transport retries a refused connection (`retries`,
|
|
195
196
|
`retryDelayMs`; default 2 × 500 ms) — raise them, or rerun the deploy.
|
|
196
|
-
- ⚠️ One `
|
|
197
|
+
- ⚠️ One `CaddyTransport` per stack: the transport picks the Caddy, not a prop. Pointing it at another
|
|
197
198
|
Caddy plans an update; the old one keeps what it had. ⛔ State vouches only for the Caddy it was
|
|
198
199
|
applied to, so the apply loads there only if it serves nothing, runs the declared config or the
|
|
199
200
|
one last stored — anything else needs `--adopt`.
|
package/docs/credentials.md
CHANGED
|
@@ -29,3 +29,19 @@ Framework `TokenAuthentication` scheme — NOT `Bearer`, measured against this e
|
|
|
29
29
|
instance) is built by the package's protocol layer, not by this package. ⛔ The token is never a
|
|
30
30
|
prop: Alchemy persists attributes unencrypted, so nothing stored in a stack file may be a
|
|
31
31
|
credential.
|
|
32
|
+
|
|
33
|
+
The Paperless subpath (2026-09-24, moved onto `@distilled.cloud/paperless-ngx` — same route) also
|
|
34
|
+
resolves credentials through the SDK's own `CredentialsFromEnv`, reading `PAPERLESS_URL` /
|
|
35
|
+
`PAPERLESS_TOKEN` — the same two variable names the retired hand-rolled `credentials.ts` read.
|
|
36
|
+
`Authorization: Token <value>` (Paperless-ngx's own DRF `TokenAuthentication`, same scheme as
|
|
37
|
+
NetBox) is built by the package's protocol layer. Baked directly into each of the four
|
|
38
|
+
`xxxProvider()`s (`paperlessProviders()` no longer takes a credentials-layer override — see the
|
|
39
|
+
migration's changeset), the same way the Forgejo and NetBox subpaths above do.
|
|
40
|
+
|
|
41
|
+
The LiteLLM subpath (2026-09-24, moved onto `@distilled.cloud/litellm` — see
|
|
42
|
+
[distilled-interim.md](./distilled-interim.md)) resolves credentials the same way, through the
|
|
43
|
+
SDK's own `CredentialsFromEnv` layer, which reads `LITELLM_PROXY_URL` / `LITELLM_PROXY_API_KEY` —
|
|
44
|
+
the same two variable names the retired hand-rolled `credentials.ts` read (LiteLLM's own `litellm`
|
|
45
|
+
CLI client's variable names, not a house choice), still resolved on the calling fiber per request,
|
|
46
|
+
never captured at module scope. `Authorization: Bearer <value>` is built by the package's protocol
|
|
47
|
+
layer, not by this package. ⛔ The key is never a prop, for the same reason as above.
|
package/docs/litellm.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# LiteLLM — `@homeflare/alchemy/litellm`
|
|
2
2
|
|
|
3
3
|
`LiteLLM.PassThroughEndpoint` declares one row of LiteLLM's `/config/pass_through_endpoint`
|
|
4
|
-
family: a route on the proxy that forwards requests to an upstream target
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
family: a route on the proxy that forwards requests to an upstream target. Every call goes
|
|
5
|
+
through `@distilled.cloud/litellm`'s typed `misc` operations (2026-09-24, moved off a hand-rolled
|
|
6
|
+
`client.ts`/`generated/pass-through.ts` the same way `/netbox` and `/forgejo` did —
|
|
7
|
+
[distilled-interim.md](./distilled-interim.md); not published upstream yet, so aliased onto
|
|
8
|
+
`@homeflare/distilled-litellm`). Its types are generated from LiteLLM **1.100.0**'s own OpenAPI
|
|
9
|
+
document (the distilled clone's own `packages/litellm/README.md` has the provenance: vendor code
|
|
10
|
+
at tag `v1.100.0`, dumped by the vendor's own `gen-api-types.mjs`, cross-checked byte-identically
|
|
11
|
+
against the tag's committed `schema.d.ts`; NOT a live read, because the reference proxy's
|
|
12
|
+
`/openapi.json` could not be reached when this was walked).
|
|
9
13
|
|
|
10
14
|
## What is DB-backed and API-managed, versus config-file only
|
|
11
15
|
|
|
@@ -21,11 +25,13 @@ Measured at 1.100.0 (`schema.prisma`, `pass_through_endpoints.py`, and the mini'
|
|
|
21
25
|
|
|
22
26
|
## Credentials
|
|
23
27
|
|
|
24
|
-
Reads `LITELLM_PROXY_URL` and `LITELLM_PROXY_API_KEY` at call time
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
|
|
28
|
+
Reads `LITELLM_PROXY_URL` and `LITELLM_PROXY_API_KEY` at call time, through
|
|
29
|
+
`@distilled.cloud/litellm`'s own `CredentialsFromEnv` — the same two variable names LiteLLM's own
|
|
30
|
+
`litellm` CLI reads (`litellm/proxy/client/cli/main.py` at the tag), unchanged by the move off the
|
|
31
|
+
retired hand-rolled `credentials.ts`. No default URL. The key must be LiteLLM's master key or
|
|
32
|
+
another `PROXY_ADMIN` key: `update_config_general_settings`, the function every mutation goes
|
|
33
|
+
through, is `PROXY_ADMIN` only. Never a prop — see [docs/credentials.md](./credentials.md) for the
|
|
34
|
+
house rule.
|
|
29
35
|
|
|
30
36
|
## Refusals
|
|
31
37
|
|
|
@@ -59,9 +65,11 @@ two rows on the same path, which this resource never allows.
|
|
|
59
65
|
## The whole-field write, and why every mutation is serialised
|
|
60
66
|
|
|
61
67
|
Every pass-through endpoint lives in ONE `general_settings.pass_through_endpoints` list. Create,
|
|
62
|
-
update and delete are each a read-modify-write of that WHOLE list. `
|
|
63
|
-
mutating call
|
|
64
|
-
|
|
68
|
+
update and delete are each a read-modify-write of that WHOLE list. `operations.ts` wraps every
|
|
69
|
+
mutating call — now the SDK's typed operations, not a hand-rolled client — in a per-base-URL
|
|
70
|
+
`Effect` semaphore, so two of THIS PROCESS's own calls never race each other — two endpoints
|
|
71
|
+
declared in the same deploy both survive. The SDK has no opinion on this: it is a quirk of how
|
|
72
|
+
THIS vendor stores the resource, not a distilled protocol concern.
|
|
65
73
|
|
|
66
74
|
⛔ **What the semaphore does not cover.** It serialises this package's own writes within one
|
|
67
75
|
process. It cannot serialise against the LiteLLM UI, another stack, or a second concurrent
|
package/docs/paperless.md
CHANGED
|
@@ -4,6 +4,18 @@ Four resources — `Paperless.Tag`, `Paperless.DocumentType`, `Paperless.Storage
|
|
|
4
4
|
`Paperless.CustomField` — the taxonomy Paperless-ngx writes through Django's internal models,
|
|
5
5
|
create-or-update and never deleted by default. Workflows and mail accounts are a later slice.
|
|
6
6
|
|
|
7
|
+
## Moved onto `@distilled.cloud/paperless-ngx` (2026-09-24)
|
|
8
|
+
|
|
9
|
+
🔴 Every wire call in this family now goes through `@distilled.cloud/paperless-ngx`'s typed
|
|
10
|
+
operations (`tags.createTag`, `documentTypes.listDocumentTypes`, …) with
|
|
11
|
+
`Effect.catchTag('NotFound', …)` at each resource file's own `getById`, the same route
|
|
12
|
+
`forgejo/*` and `netbox/*` took — see [distilled-interim.md](./distilled-interim.md) and this
|
|
13
|
+
migration's changeset for the full story. `client.ts`, `errors.ts` and `credentials.ts` are gone.
|
|
14
|
+
The rest of this document — locate-by-name-then-id (PR 163), `owner: null` on create, the
|
|
15
|
+
create-only `dataType`, `retain` by default — is the family's OWN behaviour, unchanged by the
|
|
16
|
+
transport swap and proved unchanged by `tag.test.ts`/`tag-identity.test.ts` against a fake that
|
|
17
|
+
speaks the real distilled protocol.
|
|
18
|
+
|
|
7
19
|
## Credentials
|
|
8
20
|
|
|
9
21
|
Two environment variables, read **at call time**, never props:
|
package/docs/peers.md
CHANGED
|
@@ -64,3 +64,8 @@ automatically for every consumer that installs `@homeflare/alchemy` — nothing
|
|
|
64
64
|
peer install line, and nothing for `peers.test.ts` to check. When the real package ships
|
|
65
65
|
upstream, the alias's target changes (or the entry becomes a normal peer, matching
|
|
66
66
|
`@distilled.cloud/forgejo` above) and this line still does not change for it.
|
|
67
|
+
|
|
68
|
+
★ **`@distilled.cloud/litellm` (added 2026-09-24, moving `/litellm` off a hand-rolled
|
|
69
|
+
`HttpClient` client the same way `/netbox` did) is the same shape**: aliased onto
|
|
70
|
+
`@homeflare/distilled-litellm@0.2.0` as a plain `dependencies` entry, not a peer — nothing to
|
|
71
|
+
add here, nothing for `peers.test.ts` to check.
|
package/docs/repo-policy.md
CHANGED
|
@@ -18,7 +18,22 @@ Effect.gen(function* () {
|
|
|
18
18
|
});
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Provide `
|
|
21
|
+
Provide `repoPolicyProviders()` from this subpath as the stack's `providers` — the composed
|
|
22
|
+
`GitHub.providers()` + `RepositoryRulesetProvider()` layer:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { repoPolicyProviders } from '@homeflare/alchemy/github';
|
|
26
|
+
|
|
27
|
+
// providers: repoPolicyProviders()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
⛔ **Writing the two providers side by side does not typecheck** (measured 2026-09-24,
|
|
31
|
+
homeflare-builds bump PR 6): `RepositoryRulesetProvider()`'s handlers need `GitHubCredentials`,
|
|
32
|
+
and merging layers does not thread one's output into another's requirement.
|
|
33
|
+
`repoPolicyProviders()` feeds `GitHub.providers()` into `RepositoryRulesetProvider()` with
|
|
34
|
+
`Layer.provideMerge` so the composed layer needs nothing further — see
|
|
35
|
+
[repository-ruleset.md](./repository-ruleset.md#providers) for the full account.
|
|
36
|
+
|
|
22
37
|
⚠️ **Rewired 2026-09-23**: the ruleset half now composes this house's own
|
|
23
38
|
`GitHub.RepositoryRuleset` (see [repository-ruleset.md](./repository-ruleset.md)), not
|
|
24
39
|
upstream `GitHub.Ruleset` — every hazard [below](#-the-ruleset-half-has-hazards-of-its-own)
|
|
@@ -30,10 +30,44 @@ Effect.gen(function* () {
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
This is a separate `Provider`, registered under its own type string
|
|
34
|
+
(`GitHub.RepositoryRuleset`), not inside upstream's `GitHub.Providers` collection — the two
|
|
35
|
+
coexist in one stack without any change to upstream's own code.
|
|
36
|
+
|
|
37
|
+
## Providers
|
|
38
|
+
|
|
39
|
+
Provide `repoPolicyProviders()` (`@homeflare/alchemy/github`) as the stack's `providers`, not
|
|
40
|
+
`GitHub.providers()` and `RepositoryRulesetProvider()` written side by side:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { repoPolicyProviders } from '@homeflare/alchemy/github';
|
|
44
|
+
|
|
45
|
+
// providers: repoPolicyProviders()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
⛔ **The side-by-side form does not typecheck** — measured 2026-09-24 (homeflare-builds bump PR
|
|
49
|
+
6, tsc TS2345), two separate bugs stacked on top of each other:
|
|
50
|
+
|
|
51
|
+
1. **The layer** still requires `GitHubCredentials` when the two providers are merged plainly:
|
|
52
|
+
`RepositoryRulesetProvider()`'s handlers call `octokitFor`, which needs it, and merging two
|
|
53
|
+
layers does not thread one's output into the other's requirement. `repoPolicyProviders()`
|
|
54
|
+
feeds `GitHub.providers()` into `RepositoryRulesetProvider()` with `Layer.provideMerge`
|
|
55
|
+
instead of a plain merge. `GitHub.providers()` resolves credentials internally and — because
|
|
56
|
+
it chains `provideMerge`, not `provide`, onto its own auth layer — re-exposes
|
|
57
|
+
`GitHubCredentials` in its own output too, so this both satisfies the requirement and keeps
|
|
58
|
+
`GitHub.Providers` available for any `GitHub.Repository`/`GitHub.Ruleset` declared alongside.
|
|
59
|
+
2. **The declaration** failed to typecheck on its own, even with the layer fixed, whenever a
|
|
60
|
+
stack body called `RepositoryRuleset` — directly, or via
|
|
61
|
+
`declareRepoPolicy`/`declareRepoBaseline`. `RepositoryRuleset`'s `Resource<>` declaration put
|
|
62
|
+
`GitHubCredentials` in the 5th (`Providers`) type parameter, which puts the credential
|
|
63
|
+
requirement on the DECLARATION itself, not just the provider. A stack body cannot supply
|
|
64
|
+
`GitHubCredentials` this way — `Alchemy.Stack`'s own `ProviderServices` type is a closed
|
|
65
|
+
union that `GitHubCredentials` does not structurally match (see repository-ruleset.ts's file
|
|
66
|
+
header for the full trace against `alchemy/src/Resource.ts` and `alchemy/src/Stack.ts`).
|
|
67
|
+
Upstream's own `GitHub.Ruleset` never makes this mistake: its 5th slot is `GitHub.Providers`
|
|
68
|
+
(the collection tag), never the raw credential service. `RepositoryRuleset` now omits the
|
|
69
|
+
5th parameter, defaulting its declaration requirement to `Provider<RepositoryRuleset>`
|
|
70
|
+
instead.
|
|
37
71
|
|
|
38
72
|
## Type string: H14
|
|
39
73
|
|
|
@@ -55,12 +89,12 @@ exported credential service.
|
|
|
55
89
|
|
|
56
90
|
## What it models beyond upstream `RulesetProps`
|
|
57
91
|
|
|
58
|
-
| field | upstream `Ruleset` | this resource
|
|
59
|
-
| ------------------------------------------------- | ------------------ |
|
|
60
|
-
| `pullRequest.allowedMergeMethods` | no | yes
|
|
61
|
-
| `pullRequest.requiredReviewers` | no | yes
|
|
62
|
-
| `pullRequest.extraApprovalForUnattributedChanges` | no | `false`
|
|
63
|
-
| `requiredStatusChecks.doNotEnforceOnCreate` | no | yes
|
|
92
|
+
| field | upstream `Ruleset` | this resource |
|
|
93
|
+
| ------------------------------------------------- | ------------------ | ------------------------ |
|
|
94
|
+
| `pullRequest.allowedMergeMethods` | no | yes |
|
|
95
|
+
| `pullRequest.requiredReviewers` | no | yes |
|
|
96
|
+
| `pullRequest.extraApprovalForUnattributedChanges` | no | `true`/`false` (K1, H15) |
|
|
97
|
+
| `requiredStatusChecks.doNotEnforceOnCreate` | no | yes |
|
|
64
98
|
|
|
65
99
|
`RULE_TYPE_COVERAGE` in `repository-ruleset-constraints.ts` classifies every rule type
|
|
66
100
|
Octokit's own installed types know (21 of them — see the file for the 27.0.0-vs-29.0.1
|
|
@@ -79,9 +113,11 @@ being silently dropped by the wholesale `rules` PUT.
|
|
|
79
113
|
see `undeclaredLiveRuleRefusal`'s own comment for the concrete scenario (forgetting
|
|
80
114
|
`deletion: true` would otherwise silently drop branch-deletion protection) that made
|
|
81
115
|
this the general case rather than a special one.
|
|
82
|
-
- **Adding a required check that has never reported success
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
- **Adding a required check that has never reported success**: refused before any write.
|
|
117
|
+
Checked at the default branch's current tip, then (K2) up to `RECENT_MERGED_PR_LIMIT`
|
|
118
|
+
recent merged PR heads — bounded, not full history (repository-ruleset-octokit.ts). The
|
|
119
|
+
fallback exists because the rendered CI runs on `pull_request` only, so `ci`/`secret
|
|
120
|
+
scan`/`CodeQL` never report on the tip itself.
|
|
85
121
|
- **A readback mismatch**: after any write, an INDEPENDENT `GET` — never the write's own
|
|
86
122
|
response — is compared against what was sent for the approval count, merge methods and
|
|
87
123
|
the extension flag. A mismatch fails the plan.
|
|
@@ -110,5 +146,9 @@ caller, the same as upstream `GitHub.Ruleset`.
|
|
|
110
146
|
what a create sends, zero writes on a noop or a refusal, exactly one update on real
|
|
111
147
|
drift, and the readback check.
|
|
112
148
|
- `repository-ruleset-wire.test.ts` — the H15 measurement against a real Octokit instance.
|
|
149
|
+
- `repository-ruleset-octokit.test.ts` — the K2 measurement, same real-Octokit-with-fetch-
|
|
150
|
+
shim pattern as the wire test: a context reporting only on a recent merged PR head is
|
|
151
|
+
accepted, one reporting nowhere (tip or any recent merged head) is still refused, and
|
|
152
|
+
one reporting only past `RECENT_MERGED_PR_LIMIT` stays refused too (the bound is real).
|
|
113
153
|
- `repository-ruleset-coverage.test.ts` — the rule-type coverage change detector (the
|
|
114
154
|
completeness guarantee itself is enforced by `tsc`, not this file — see its header).
|
|
@@ -69,17 +69,73 @@ then tidiness.
|
|
|
69
69
|
(S42). This is listed in [the Bun line](#the-bun-line). The exported `Bun.*` paths crash
|
|
70
70
|
under Node, which is the runtime this package promises its consumers. The `node:*` paths
|
|
71
71
|
load on Node but break the Effect-only rule.
|
|
72
|
-
5. **`launchd
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
5. **`launchd` and `linux` still have promise-based seams (H3).** `HostRunner` is a plain
|
|
73
|
+
`async` interface, with `node:child_process` and `node:fs/promises` behind it, and
|
|
74
|
+
failures are untyped `Error` (S19, S21). The stated reason is that a consumer implements
|
|
75
|
+
plain async functions. Upstream meets the same need by letting a caller provide
|
|
76
|
+
`FileSystem`, `ChildProcessSpawner` or `CommandExecutor` layers, and
|
|
77
|
+
`alchemy/Util/AtomicFile.writeFileAtomic` covers the temp-and-rename write.
|
|
78
78
|
⚠️ Only in part. `local-runner.ts` creates its temp file with `O_EXCL`, mode 0600 and
|
|
79
79
|
`chown` before `chmod`, and it refuses a planted symlink. `writeFileAtomic` writes with
|
|
80
80
|
default flags and mode, `chmod`s afterwards, and never `chown`s.
|
|
81
81
|
**Decision:** maintainer. These families are also host-specific, so "house-only" is a
|
|
82
82
|
valid answer.
|
|
83
|
+
|
|
84
|
+
✅ **S23 fixed 2026-09-24** (branch `claude2/distilled-caddy-migrate`): `caddy/*` no
|
|
85
|
+
longer has a promise-based seam at all. `Caddy.Config` now calls
|
|
86
|
+
`@distilled.cloud/caddy`'s typed `admin` operations (`adaptConfig`, `loadConfig`,
|
|
87
|
+
`getConfig`), `catchTag`-able via `Caddy.CaddyOpError`, with `isUnreachable`
|
|
88
|
+
(caddy-http-client.ts) replacing the old `CaddyUnreachableError` class as a type guard
|
|
89
|
+
over the SDK's own `HttpClientError`. The old `CaddyAdmin`/`CaddyAdminRequest`/
|
|
90
|
+
`CaddyAdminResponse`/`CaddyAdminError` interfaces and `admin-calls.ts`'s hand-rolled
|
|
91
|
+
`POST /adapt`/`POST /load`/`GET /config/` calls are gone. `local-admin.ts`'s measured
|
|
92
|
+
unix-socket-plus-loopback transport (Host/Origin headers, ECONNREFUSED/ENOENT-only
|
|
93
|
+
retry) survives as caddy-http-client.ts, now built as an Effect `HttpClient.HttpClient`
|
|
94
|
+
the SDK's own protocol runs over, instead of a raw `node:http` promise interface — still
|
|
95
|
+
`node:http`, not `FetchHttpClient`, because only `node:http` dials a unix socket on both
|
|
96
|
+
Bun and Node (`FetchHttpClient`'s `unix` option is Bun-only). The `LoadRefused` 200-trap
|
|
97
|
+
(a refused `/load` that still answers 200) is now first-class in the SDK's own
|
|
98
|
+
`protocol.ts`, not re-detected in this package. A genuine SDK gap this migration found
|
|
99
|
+
and patched in the distilled clone (never in this resource): `AdaptConfig`/`LoadConfig`'s
|
|
100
|
+
`config` field is Caddyfile TEXT under a caller-chosen `Content-Type`, which
|
|
101
|
+
`buildRequest`'s generic `HttpBody()` cascade has no seam for (no static `bodyMediaType`
|
|
102
|
+
for a dynamically-typed body) — it fell to the JSON-body default, which both
|
|
103
|
+
`JSON.stringify`d the Caddyfile text and overwrote the caller's `Content-Type` with
|
|
104
|
+
`application/json`. Fixed in `protocol.ts`'s `encode`, shipped as
|
|
105
|
+
`@homeflare/distilled-caddy@0.2.1` (a patch changeset). A second, unrelated finding: the
|
|
106
|
+
SDK's own default `Caddy.Retry` policy treats ANY `HttpClientError` with a
|
|
107
|
+
`TransportError` reason as retryable — including an ECONNRESET well after a `POST /load`
|
|
108
|
+
was accepted — which would have re-sent an already-accepted load past the point
|
|
109
|
+
admin-calls.ts's original module doc says never to. `local-admin.ts` now disables it
|
|
110
|
+
(`Layer.succeed(Caddy.Retry.Retry, { while: () => false })`), leaving
|
|
111
|
+
caddy-http-client.ts's own narrower retry (ECONNREFUSED/ENOENT only) as the one retry
|
|
112
|
+
policy in play. State did not move: props and attributes stay byte-identical, proven by
|
|
113
|
+
the family's existing tests (updated only where the typed-error message text itself
|
|
114
|
+
changed) plus fake-caddy.ts, a real HTTP server, so every test already drives the real
|
|
115
|
+
distilled wire protocol.
|
|
116
|
+
⛔ **Alchemy trap found along the way, house-wide, not caddy-specific:** a
|
|
117
|
+
`Provider.effect(...)` layer built with `SomeLayer.pipe(Layer.provide(depsLayer))` seals
|
|
118
|
+
`depsLayer`'s services away from the provider's OWN `read`/`diff`/`reconcile` handlers
|
|
119
|
+
when THEY run later — `Layer.provide` satisfies the provider's construction-time
|
|
120
|
+
requirement and then hides it, so a handler that itself does `yield* SomeService` (as
|
|
121
|
+
`Provider.effect`'s own `ReadReq`/`DiffReq`/`ReconcileReq` type parameters invite)
|
|
122
|
+
dies with "Service not found" the moment the engine calls it, not when the layer is
|
|
123
|
+
built. `Layer.provideMerge` is the fix — it feeds the dependency's output into the
|
|
124
|
+
provider's requirement AND keeps that output live in the result. `providers.ts`'s
|
|
125
|
+
`caddyProviders()` and this family's tests were the ones actually broken by it, but any
|
|
126
|
+
family whose provider handlers read a service from context has the same exposure.
|
|
127
|
+
|
|
128
|
+
**File-size note, added in the `claude2/distilled-caddy-followup` review pass:** the
|
|
129
|
+
`encode()` fix above grew `packages/distilled-caddy/src/protocol.ts` from 243 to 294
|
|
130
|
+
lines, past the house's 250-line code-file cap — the original PR didn't flag this. Left
|
|
131
|
+
as-is on purpose, not split: `distilled-interim.md` treats all of
|
|
132
|
+
`packages/distilled-*/src/**` as copied-not-edited vendor code (⛔ "src/ is copied,
|
|
133
|
+
never hand-edited in the kit"; already exempt from `.oxfmtrc.json`/`.oxlintrc.json` for
|
|
134
|
+
the identical reason), which is the AGENTS.md line-cap rule's own carve-out for
|
|
135
|
+
"vendored code." Its source of truth is branch `homeflare/caddy` in the distilled clone
|
|
136
|
+
— a split belongs there first, then gets copied forward on the next regeneration, not
|
|
137
|
+
decided unilaterally in this kit copy.
|
|
138
|
+
|
|
83
139
|
6. **`openbao/*` has no upstream equivalent, and it conforms on the contract** (Effect
|
|
84
140
|
`HttpClient`, strict `Unowned`, retain). It diverges in three ways:
|
|
85
141
|
- 30 `Effect.die` sites in non-test source, several of them on `diff` and
|
|
@@ -121,18 +177,69 @@ HttpClient` client. The old status-carrying `NetboxError` and its `cause.status
|
|
|
121
177
|
- ⛔ **Still diverges on credentials (S24):** `NETBOX_TOKEN` / `NETBOX_URL` are read at
|
|
122
178
|
call time, now through the SDK's `CredentialsFromEnv` rather than a hand-rolled
|
|
123
179
|
`token()` — same divergence, unchanged by the transport swap.
|
|
124
|
-
10.
|
|
180
|
+
10. **`litellm/*` has no upstream equivalent** (LiteLLM has no upstream Alchemy family, the
|
|
181
|
+
same situation `openbao/*` is in above), and conforms on the transport contract.
|
|
182
|
+
- ✅ **S23 fixed 2026-09-24** (branch `claude2/litellm-distilled`, mirroring the NetBox
|
|
183
|
+
migration above, decision 43). `LiteLLM.PassThroughEndpoint` now calls
|
|
184
|
+
`@distilled.cloud/litellm`'s typed `misc` operations (`operations.ts`) instead of a
|
|
185
|
+
hand-rolled `Effect HttpClient` client. The old status-carrying `LitellmBadRequestError`
|
|
186
|
+
family is gone: every failure the four operations declare (`BadRequest`, `NotFound` on
|
|
187
|
+
update, `UnprocessableEntity`, plus the shared `Unauthorized`/`TooManyRequests`/server
|
|
188
|
+
errors) is the SDK's own typed error, `catchTag`'d — `deletePassThroughEndpoint`'s
|
|
189
|
+
re-list-on-ambiguous-`BadRequest` trick (S21: a status-and-a-real-read decision, never
|
|
190
|
+
body text) is unchanged, now keyed on the tag instead of the status code. The
|
|
191
|
+
per-base-URL write semaphore this vendor's whole-list storage forces (docs/litellm.md)
|
|
192
|
+
is unchanged too — the SDK has no opinion on it, so it stays one layer above the typed
|
|
193
|
+
calls, in `operations.ts`. `client.ts` and the kit's own hand-generated
|
|
194
|
+
`generated/pass-through.ts` are both deleted (the SDK's `misc.PassThroughGenericEndpoint`
|
|
195
|
+
is the same shape, generated from the same LiteLLM 1.100.0 OpenAPI document — see
|
|
196
|
+
`codegen/manifest.json`'s `litellm-openapi` entry, left in place and unconsumed rather
|
|
197
|
+
than deleted, the same pattern the two UniFi entries already establish). Not published
|
|
198
|
+
upstream yet, so aliased onto `@homeflare/distilled-litellm@0.2.0` as a plain
|
|
199
|
+
`dependencies` entry, not a peer — [distilled-interim.md](./distilled-interim.md). State
|
|
200
|
+
did not move: props/attributes stay byte-identical, proven by the family's unchanged
|
|
201
|
+
existing tests (now against a fake LiteLLM exercising the real distilled protocol
|
|
202
|
+
through `FetchHttpClient.Fetch`, not a loopback `Bun.serve`) plus the engine-level
|
|
203
|
+
replace test through `fake-stack.ts`. No stack in this estate currently imports
|
|
204
|
+
`litellmProviders`/`LiteLLM.PassThroughEndpoint` (measured 2026-09-24 across
|
|
205
|
+
homeflare-landscape's own repositories, including this one's `stacks/`), so there is no
|
|
206
|
+
live plan to re-run.
|
|
207
|
+
- ⛔ **Still diverges on credentials (S24):** `LITELLM_PROXY_URL` / `LITELLM_PROXY_API_KEY`
|
|
208
|
+
are read at call time, now through the SDK's `CredentialsFromEnv` rather than a
|
|
209
|
+
hand-rolled `resolveCreds` — same divergence, unchanged by the transport swap.
|
|
210
|
+
- ✅ **S20 fixed 2026-09-24** (same branch, PR review pass): `CredentialsFromEnv` ended in
|
|
211
|
+
`Effect.orDie`, so a missing/misspelled env var died as an engine-crashing defect
|
|
212
|
+
instead of the typed `ConfigError` `LitellmOpError` already declared — a regression
|
|
213
|
+
this migration made newly reachable (the retired hand-rolled `credentials.ts` failed
|
|
214
|
+
typed). Fixed at the source: `@distilled.cloud/litellm`'s own `credentials.ts`
|
|
215
|
+
(`homeflare/litellm@4ad19154`, commit local, not pushed — S22), copied forward here.
|
|
216
|
+
`netbox/*`'s `CredentialsFromEnv` has the identical `orDie` shape and is unfixed —
|
|
217
|
+
out of scope for this item; tracked as its own follow-up (task `task_3bbe1684`).
|
|
218
|
+
11. **Every family repeats `list: () => Effect.succeed([])`**, 30 times, and only
|
|
125
219
|
`R2BucketLock` and `MeshNode` declare `nuke`. The constructor already defaults `list`
|
|
126
220
|
(S12). **Fix:** write the reason where it differs, and declare `nuke: { skip: true }`
|
|
127
221
|
where nuke must never reach the object.
|
|
128
|
-
|
|
222
|
+
12. **Resource JSDoc is not in upstream's generator format.** Zero files use `@resource`,
|
|
129
223
|
`**Example:**` or `### Section` (S31, H10). **Decision:** maintainer, because it moves
|
|
130
224
|
the house's glyph rationale into `//` comments.
|
|
131
|
-
|
|
225
|
+
13. **Tests never use `alchemy/Test/Bun`.** Every lifecycle is proven against loopback
|
|
132
226
|
fakes (S28, H12). Live suites need a place to run, and that is a maintainer decision.
|
|
133
227
|
|
|
134
228
|
## Conforms
|
|
135
229
|
|
|
230
|
+
- **`paperless/*` (`Tag`, `DocumentType`, `StoragePath`, `CustomField`)** — ✅ **S23 fixed
|
|
231
|
+
2026-09-24** (branch `claude2/distilled-paperless-family`, decision 43, mirroring the
|
|
232
|
+
netbox/forgejo migrations above). Every call now goes through `@distilled.cloud/paperless-ngx`'s
|
|
233
|
+
typed operations, `catchTag('NotFound', …)` replaced the status-carrying `PaperlessError`
|
|
234
|
+
union, and `client.ts`/`errors.ts`/`credentials.ts` are deleted. The shared
|
|
235
|
+
`matching.ts`/`matching-locate.ts` engine (locate-by-name before state, by `output.id` after —
|
|
236
|
+
PR 163) is unchanged in shape. Not published upstream yet, aliased onto
|
|
237
|
+
`@homeflare/distilled-paperless-ngx@0.3.0` — [distilled-interim.md](./distilled-interim.md).
|
|
238
|
+
State did not move: props/attributes stay byte-identical, proven by `tag.test.ts` and the full
|
|
239
|
+
PR 163 regression suite (`tag-identity.test.ts`) against a fake exercising the real distilled
|
|
240
|
+
protocol. ⛔ **Still diverges on credentials (S24):** `PAPERLESS_URL`/`PAPERLESS_TOKEN` are read
|
|
241
|
+
at call time, now through the SDK's `CredentialsFromEnv` — same divergence, unchanged by the
|
|
242
|
+
transport swap.
|
|
136
243
|
- **`github/RepositoryRuleset`** (added 2026-09-23) closes the hazard the row above names:
|
|
137
244
|
probes by name (`read`/`reconcile` both), answers `Unowned` on a cold name match (H1),
|
|
138
245
|
normalizes before comparing so a matching live ruleset is a true noop, and refuses
|
|
@@ -196,7 +303,7 @@ among them `Fly/Secret.ts` and `Railway/Variable.ts`. They were listed under a b
|
|
|
196
303
|
| `launchd/sudo-stage.ts` | `node:fs/promises`, `node:os`, `node:path` | — | `FileSystem`, `Path` |
|
|
197
304
|
| `launchd/job-form.ts` | `node:crypto` (`createHash`) | — | allowed in `Effect.sync` |
|
|
198
305
|
| `linux/ssh-runner.ts` | `node:child_process`, `node:crypto` | yes | H3 above |
|
|
199
|
-
| `caddy/
|
|
306
|
+
| `caddy/caddy-http-client.ts` | `node:http`, `node:stream` (unix socket) | yes | inherent — an Effect `HttpClient`, not H3 ⚠️ |
|
|
200
307
|
| `proxmox/write-only.ts`, `pbs-notification-target-wire.ts` | `node:crypto`, `node:buffer` | — | allowed in `Effect.sync` ⚠️ |
|
|
201
308
|
| `proxmox/provision-cli-fake.ts` | `Bun.spawn`, `node:fs` | — | test-only; move out of `src/` |
|
|
202
309
|
|
|
@@ -209,6 +316,15 @@ The ⚠️ rows:
|
|
|
209
316
|
- `proxmox/write-only.ts`: a salted scrypt seal with `timingSafeEqual`. No Alchemy helper
|
|
210
317
|
replaces it. A plain `sha256` would make a write-only secret cheap to brute-force from
|
|
211
318
|
state, and it would change the persisted seal format.
|
|
319
|
+
- `caddy/caddy-http-client.ts`: unlike `launchd/local-runner.ts` and `linux/ssh-runner.ts`
|
|
320
|
+
(H3, plain `async` interfaces), this is already the Effect `HttpClient.HttpClient` the
|
|
321
|
+
distilled SDK's own protocol runs on — `node:http` here is the same kind of unavoidable,
|
|
322
|
+
bottom-of-the-stack platform adapter `@effect/platform-node`'s own `NodeHttpClient.ts` is,
|
|
323
|
+
extended with `socketPath` dialing (that module has none) because a unix socket is the
|
|
324
|
+
measured transport a local Caddy needs and neither `FetchHttpClient` (Bun-only `unix`
|
|
325
|
+
option) nor stock `NodeHttpClient` (URL-only) speaks one on both Bun and Node. There is no
|
|
326
|
+
more-portable replacement to point to; the file already conforms to S19 (no async/await,
|
|
327
|
+
no raw `Promise` — `Effect.callback`/`Effect.tryPromise` throughout).
|
|
212
328
|
|
|
213
329
|
Test runners: 51 files import `node:test`/`node:assert` (openbao 42, proxmox 7, forgejo 1,
|
|
214
330
|
talos 1), and 126 import `bun:test`. S43 says `bun:test`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"access": "public"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@distilled.cloud/caddy": "npm:@homeflare/distilled-caddy@0.2.
|
|
97
|
+
"@distilled.cloud/caddy": "npm:@homeflare/distilled-caddy@0.2.2",
|
|
98
98
|
"@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.2.0",
|
|
99
99
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
100
100
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
@@ -103,7 +103,10 @@
|
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|
|
105
105
|
"@distilled.cloud/cloudflare": "1.0.0-rc.12",
|
|
106
|
+
"@distilled.cloud/discord": "1.0.0-rc.12",
|
|
106
107
|
"@distilled.cloud/forgejo": "1.0.0-rc.12",
|
|
108
|
+
"@distilled.cloud/google-workspace": "1.0.0-rc.12",
|
|
109
|
+
"@distilled.cloud/grafana": "1.0.0-rc.12",
|
|
107
110
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
108
111
|
"@effect/sql-pg": "4.0.0-rc.115",
|
|
109
112
|
"alchemy": "2.0.0-beta.79",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a `localCaddyAdmin()` address into a dial target and the listener Caddy checks requests
|
|
3
|
+
* against — pure, so admin-guard.ts and local-admin.ts's tests exercise it with no transport at all.
|
|
4
|
+
*
|
|
5
|
+
* ★ LOOPBACK OR A UNIX SOCKET, NOTHING ELSE. The admin API is unauthenticated plaintext; an address
|
|
6
|
+
* off this machine means it is reachable from the network, which decision 23 forbids. Reaching
|
|
7
|
+
* another host's Caddy means forwarding its socket or port to here (SSH), never exposing :2019.
|
|
8
|
+
* ★ HOST, AS CADDY's OWN CLI SENDS IT (cmd/commandfuncs.go AdminAPIRequest): `host:port`, over TCP.
|
|
9
|
+
* MEASURED against a Mac host's Caddy 2.11.4: any other Host gets 403 `host not allowed`, a
|
|
10
|
+
* forwarded port included — hence `hostHeader`.
|
|
11
|
+
*/
|
|
12
|
+
import type { CaddyAdminListener } from './admin.ts';
|
|
13
|
+
|
|
14
|
+
export type Target =
|
|
15
|
+
| { readonly kind: 'tcp'; readonly host: string; readonly port: number }
|
|
16
|
+
| { readonly kind: 'unix'; readonly socketPath: string };
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_ADMIN_ADDRESS = 'http://127.0.0.1:2019';
|
|
19
|
+
|
|
20
|
+
const UNIX = 'unix://';
|
|
21
|
+
const HOST_PORT = /^(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\]):\d{1,5}$/;
|
|
22
|
+
|
|
23
|
+
const refuse = (address: string, why: string): Error =>
|
|
24
|
+
new Error(`Caddy admin address ${JSON.stringify(address)}: ${why}`);
|
|
25
|
+
|
|
26
|
+
const isLoopback = (hostname: string): boolean =>
|
|
27
|
+
hostname === 'localhost' || hostname === '[::1]' || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
28
|
+
|
|
29
|
+
/** The dial target and listener for an address, or a refusal naming what is wrong with it. */
|
|
30
|
+
export const parseAdminAddress = (
|
|
31
|
+
address: string,
|
|
32
|
+
hostHeader?: string,
|
|
33
|
+
): { target: Target; listener: CaddyAdminListener } => {
|
|
34
|
+
if (hostHeader !== undefined && !HOST_PORT.test(hostHeader)) {
|
|
35
|
+
throw refuse(address, `hostHeader ${JSON.stringify(hostHeader)} must be host:port`);
|
|
36
|
+
}
|
|
37
|
+
if (address.startsWith(UNIX)) {
|
|
38
|
+
const socketPath = address.slice(UNIX.length);
|
|
39
|
+
if (!socketPath.startsWith('/')) throw refuse(address, 'the socket path must be absolute');
|
|
40
|
+
return { listener: { kind: 'unix', path: socketPath }, target: { kind: 'unix', socketPath } };
|
|
41
|
+
}
|
|
42
|
+
let url: URL;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(address);
|
|
45
|
+
} catch {
|
|
46
|
+
throw refuse(address, 'not a URL (http://127.0.0.1:2019 or unix:///path)');
|
|
47
|
+
}
|
|
48
|
+
// ⛔ Caddy's local admin endpoint is plaintext by design; its TLS one (`admin.remote`, mTLS on
|
|
49
|
+
// :2021) is a different, network-facing API this transport deliberately does not speak.
|
|
50
|
+
if (url.protocol !== 'http:') throw refuse(address, 'only http:// (loopback) or unix://');
|
|
51
|
+
if (!isLoopback(url.hostname)) {
|
|
52
|
+
throw refuse(address, 'not loopback — the admin API is unauthenticated; forward it to here');
|
|
53
|
+
}
|
|
54
|
+
if (url.pathname !== '/' || url.search !== '' || url.username !== '') {
|
|
55
|
+
throw refuse(address, 'no path, query or credentials — the API lives at the root');
|
|
56
|
+
}
|
|
57
|
+
const port = url.port === '' ? 80 : Number(url.port);
|
|
58
|
+
// ⚠️ NOT `url.host`: WHATWG URL drops a default port (`http://127.0.0.1:80` has host
|
|
59
|
+
// `127.0.0.1`), and Caddy compares the Host against `host:port` (admin.go allowedOrigins), so
|
|
60
|
+
// a portless Host is a 403 on every call. The Caddy CLI always sends the port (JoinHostPort).
|
|
61
|
+
const host = hostHeader ?? `${url.hostname}:${String(port)}`;
|
|
62
|
+
return {
|
|
63
|
+
listener: {
|
|
64
|
+
hostHeader: host,
|
|
65
|
+
kind: 'tcp',
|
|
66
|
+
port: Number(host.slice(host.lastIndexOf(':') + 1)),
|
|
67
|
+
},
|
|
68
|
+
// ★ `hostname` keeps IPv6 brackets; node:http wants them off.
|
|
69
|
+
target: { host: url.hostname.replace(/^\[(.*)\]$/, '$1'), kind: 'tcp', port },
|
|
70
|
+
};
|
|
71
|
+
};
|