@homeflare/alchemy 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +63 -23
- package/dist/github/index.js.map +13 -12
- 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.ts +6 -6
- package/src/github/index.ts +8 -4
- package/src/github/repo-baseline-data.test.ts +13 -0
- package/src/github/repo-baseline-data.ts +11 -2
- package/src/github/repo-policy-stack.test.ts +59 -0
- package/src/github/repo-policy.ts +3 -2
- 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
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
* should reach the wire. repository-ruleset-wire.test.ts MEASURES this against a real Octokit
|
|
9
9
|
* instance with a fetch shim rather than asserting it from reading the source.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* ★ K2 (2026-09-24): "REPORTED SUCCESS" IS THE DEFAULT BRANCH'S TIP, THEN RECENT MERGED PR
|
|
12
|
+
* HEADS — NOT FULL HISTORY. The rendered CI (`packages/config/src/repo-shape/ci.ts`) triggers
|
|
13
|
+
* on `pull_request` ONLY, deliberately (measured 2026-09-15..22: 613 of 619 main-push runs
|
|
14
|
+
* duplicated an already-green PR run, ~41% of the mini's CI load) — so `ci`/`secret scan`/
|
|
15
|
+
* `CodeQL` NEVER report on the default branch's tip itself, only on PR heads. Checking the tip
|
|
16
|
+
* alone therefore refused adding any of them everywhere, including homeflare-builds' pending
|
|
17
|
+
* first ruleset. `contextReportedSuccess` now falls back to `recentMergedHeadShas` — bounded
|
|
18
|
+
* (`RECENT_MERGED_PR_LIMIT` heads, from one `per_page`-bounded list call, never the repo's
|
|
19
|
+
* full PR history) — so a context that only ever reports on PR heads is no longer refused
|
|
20
|
+
* everywhere, while a context that has never reported ANYWHERE (not the tip, not any recent
|
|
21
|
+
* merged head) still is: `NeverReportedContext`'s whole purpose survives unchanged.
|
|
22
|
+
* ⚠️ Still not full history: a context whose only successful run predates
|
|
23
|
+
* `RECENT_MERGED_PR_LIMIT` merges ago reads as never-reported until it runs again. Widening
|
|
24
|
+
* the bound trades this guard's cost (more vendor calls per newly-required context) against
|
|
25
|
+
* its reach; `RECENT_MERGED_PR_LIMIT` is a deliberate, named number for exactly that reason,
|
|
26
|
+
* not a magic constant.
|
|
19
27
|
*/
|
|
20
28
|
import type { Octokit as OctokitClient } from '@octokit/rest';
|
|
21
29
|
import { GitHubCredentials } from 'alchemy/GitHub';
|
|
@@ -37,6 +45,110 @@ const octokitFor = (
|
|
|
37
45
|
|
|
38
46
|
const is404 = (e: unknown): boolean => (e as { status?: number } | undefined)?.status === 404;
|
|
39
47
|
|
|
48
|
+
/** How many of the most-recently-updated MERGED pull requests' head SHAs to check, once the
|
|
49
|
+
* default branch's own tip has no reported success — see the file header (K2). */
|
|
50
|
+
export const RECENT_MERGED_PR_LIMIT = 10;
|
|
51
|
+
/** How many closed PRs to list (in one bounded call) to find up to `RECENT_MERGED_PR_LIMIT`
|
|
52
|
+
* merged ones among them — larger than the limit because `state: 'closed'` also returns PRs
|
|
53
|
+
* closed without merging, sorted by `updated_at`, not `merged_at` (the REST API has no
|
|
54
|
+
* merged-only list). A GitHub API list call has no `merged` filter, so this is `100` — GitHub's
|
|
55
|
+
* own per-page maximum, not a smaller round number — to keep unmerged churn (e.g. Dependabot
|
|
56
|
+
* PRs closed without merging, which still bump `updated_at`) from crowding out real merges in a
|
|
57
|
+
* repo with heavy PR traffic (2026-09-24 review finding: a smaller page risked under-reaching in
|
|
58
|
+
* exactly the kind of repo this fix targets). Still bounded, still one call, still never the
|
|
59
|
+
* repo's full history — a repo whose last `RECENT_MERGED_PR_LIMIT` merges don't even fit in the
|
|
60
|
+
* last 100 closed PRs is treated the same as one with too little history: fewer heads found,
|
|
61
|
+
* same "still refused" fallback. */
|
|
62
|
+
const RECENT_CLOSED_PR_PAGE_SIZE = 100;
|
|
63
|
+
|
|
64
|
+
/** Whether `context` reported success for a single ref — a branch tip or a PR head SHA, both
|
|
65
|
+
* checked the same way (checks + statuses, exactly as GitHub's own UI does). Takes a plain
|
|
66
|
+
* `OctokitClient`, not an Effect service, so it and its caller below are testable directly
|
|
67
|
+
* against a real `@octokit/rest` instance with a fetch shim (repository-ruleset-octokit.test.ts)
|
|
68
|
+
* — no `GitHubCredentials` layer needed, mirroring `desiredWireRuleset`'s pure-function seam. */
|
|
69
|
+
export const hasRefReportedSuccess = (
|
|
70
|
+
octokit: OctokitClient,
|
|
71
|
+
owner: string,
|
|
72
|
+
repo: string,
|
|
73
|
+
ref: string,
|
|
74
|
+
context: string,
|
|
75
|
+
): Effect.Effect<boolean, Error> =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const [checkRuns, statuses] = yield* Effect.all([
|
|
78
|
+
Effect.tryPromise({
|
|
79
|
+
try: () => octokit.rest.checks.listForRef({ owner, repo, ref, check_name: context }),
|
|
80
|
+
catch: asError,
|
|
81
|
+
}),
|
|
82
|
+
Effect.tryPromise({
|
|
83
|
+
try: () => octokit.rest.repos.listCommitStatusesForRef({ owner, repo, ref }),
|
|
84
|
+
catch: asError,
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
const checkReported = checkRuns.data.check_runs.some(
|
|
88
|
+
(run) => run.name === context && run.conclusion === 'success',
|
|
89
|
+
);
|
|
90
|
+
const statusReported = statuses.data.some(
|
|
91
|
+
(status) => status.context === context && status.state === 'success',
|
|
92
|
+
);
|
|
93
|
+
return checkReported || statusReported;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** The head SHAs of up to `RECENT_MERGED_PR_LIMIT` most-recently-updated MERGED pull requests
|
|
97
|
+
* against `base` — one bounded list call (`RECENT_CLOSED_PR_PAGE_SIZE` closed PRs), filtered to
|
|
98
|
+
* merged, then capped. Never paginates further: a repo whose last `RECENT_MERGED_PR_LIMIT`
|
|
99
|
+
* merges are all older than its last `RECENT_CLOSED_PR_PAGE_SIZE` closed PRs simply yields fewer
|
|
100
|
+
* than the limit, same as a repo with too little history — both are the "still refused" case. */
|
|
101
|
+
export const recentMergedHeadShas = (
|
|
102
|
+
octokit: OctokitClient,
|
|
103
|
+
owner: string,
|
|
104
|
+
repo: string,
|
|
105
|
+
base: string,
|
|
106
|
+
): Effect.Effect<readonly string[], Error> =>
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
const closed = yield* Effect.tryPromise({
|
|
109
|
+
try: () =>
|
|
110
|
+
octokit.rest.pulls.list({
|
|
111
|
+
owner,
|
|
112
|
+
repo,
|
|
113
|
+
state: 'closed',
|
|
114
|
+
base,
|
|
115
|
+
sort: 'updated',
|
|
116
|
+
direction: 'desc',
|
|
117
|
+
per_page: RECENT_CLOSED_PR_PAGE_SIZE,
|
|
118
|
+
}),
|
|
119
|
+
catch: asError,
|
|
120
|
+
});
|
|
121
|
+
return closed.data
|
|
122
|
+
.filter((pr) => pr.merged_at !== null)
|
|
123
|
+
.slice(0, RECENT_MERGED_PR_LIMIT)
|
|
124
|
+
.map((pr) => pr.head.sha);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** The full K2 answer: the default branch's tip, then (only if the tip has no success) up to
|
|
128
|
+
* `RECENT_MERGED_PR_LIMIT` recent merged PR heads, short-circuiting on the first success. `false`
|
|
129
|
+
* only once every one of those has been checked and none reported — the case
|
|
130
|
+
* `NeverReportedContext` exists to refuse. */
|
|
131
|
+
export const contextReportedSuccess = (
|
|
132
|
+
octokit: OctokitClient,
|
|
133
|
+
owner: string,
|
|
134
|
+
repo: string,
|
|
135
|
+
context: string,
|
|
136
|
+
): Effect.Effect<boolean, Error> =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
const repository = yield* Effect.tryPromise({
|
|
139
|
+
try: () => octokit.rest.repos.get({ owner, repo }),
|
|
140
|
+
catch: asError,
|
|
141
|
+
});
|
|
142
|
+
const ref = repository.data.default_branch;
|
|
143
|
+
if (yield* hasRefReportedSuccess(octokit, owner, repo, ref, context)) return true;
|
|
144
|
+
|
|
145
|
+
const heads = yield* recentMergedHeadShas(octokit, owner, repo, ref);
|
|
146
|
+
for (const sha of heads) {
|
|
147
|
+
if (yield* hasRefReportedSuccess(octokit, owner, repo, sha, context)) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
|
|
40
152
|
export const makeRulesetOctokit = (baseUrl: string | undefined): RealRulesetOctokit => ({
|
|
41
153
|
list: ({ owner, repo }) =>
|
|
42
154
|
Effect.gen(function* () {
|
|
@@ -114,27 +226,6 @@ export const makeRulesetOctokit = (baseUrl: string | undefined): RealRulesetOcto
|
|
|
114
226
|
hasContextReportedSuccess: ({ owner, repo, context }) =>
|
|
115
227
|
Effect.gen(function* () {
|
|
116
228
|
const octokit = yield* octokitFor(baseUrl);
|
|
117
|
-
|
|
118
|
-
try: () => octokit.rest.repos.get({ owner, repo }),
|
|
119
|
-
catch: asError,
|
|
120
|
-
});
|
|
121
|
-
const ref = repository.data.default_branch;
|
|
122
|
-
const [checkRuns, statuses] = yield* Effect.all([
|
|
123
|
-
Effect.tryPromise({
|
|
124
|
-
try: () => octokit.rest.checks.listForRef({ owner, repo, ref, check_name: context }),
|
|
125
|
-
catch: asError,
|
|
126
|
-
}),
|
|
127
|
-
Effect.tryPromise({
|
|
128
|
-
try: () => octokit.rest.repos.listCommitStatusesForRef({ owner, repo, ref }),
|
|
129
|
-
catch: asError,
|
|
130
|
-
}),
|
|
131
|
-
]);
|
|
132
|
-
const checkReported = checkRuns.data.check_runs.some(
|
|
133
|
-
(run) => run.name === context && run.conclusion === 'success',
|
|
134
|
-
);
|
|
135
|
-
const statusReported = statuses.data.some(
|
|
136
|
-
(status) => status.context === context && status.state === 'success',
|
|
137
|
-
);
|
|
138
|
-
return checkReported || statusReported;
|
|
229
|
+
return yield* contextReportedSuccess(octokit, owner, repo, context);
|
|
139
230
|
}),
|
|
140
231
|
});
|
|
@@ -71,7 +71,8 @@ export interface RulesetOctokit<R = never> {
|
|
|
71
71
|
readonly repo: string;
|
|
72
72
|
readonly rulesetId: number;
|
|
73
73
|
}) => Effect.Effect<void, Error, R>;
|
|
74
|
-
/** Whether `context` has
|
|
74
|
+
/** Whether `context` has reported success against the repository's default branch tip OR a
|
|
75
|
+
* recent merged PR head (K2, bounded — see repository-ruleset-octokit.ts's file header) —
|
|
75
76
|
* backs the `NeverReportedContext` guard. */
|
|
76
77
|
readonly hasContextReportedSuccess: (input: {
|
|
77
78
|
readonly owner: string;
|
|
@@ -18,10 +18,13 @@ export interface RepositoryRulesetPullRequestRule extends NonNullable<
|
|
|
18
18
|
/** @default every method GitHub allows — the house baseline always pins `['squash']`. */
|
|
19
19
|
readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
|
|
20
20
|
readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
|
|
21
|
-
/** `require_extra_approval_for_unattributed_changes` — see repository-ruleset.ts's H15.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
21
|
+
/** `require_extra_approval_for_unattributed_changes` — see repository-ruleset.ts's H15.
|
|
22
|
+
* Both values are modeled (K1, 2026-09-23): 8 live rulesets carry `true` (aop, cloudflareforms,
|
|
23
|
+
* doesthishelp-workeropen, homeflare-anyauth, homeflare-desktop, loggarr, magictransit,
|
|
24
|
+
* proxmox-tb4 — re-read live via `gh api repos/taslabs-net/<repo>/rulesets/<id>`,
|
|
25
|
+
* 2026-09-23), and this resource declares exactly what is live or it cannot adopt them —
|
|
26
|
+
* `undefined` stays "no opinion" (GitHub defaults an absent key to `true`). */
|
|
27
|
+
readonly extraApprovalForUnattributedChanges?: boolean;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface RepositoryRulesetStatusChecksRule extends NonNullable<
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `repoPolicyProviders` — the documented stack wiring for `RepositoryRuleset` (directly, or
|
|
3
|
+
* through `declareRepoPolicy` / `declareRepoBaseline`): `GitHub.providers()` and
|
|
4
|
+
* `RepositoryRulesetProvider()`, composed so the whole thing typechecks and needs nothing
|
|
5
|
+
* further from the stack.
|
|
6
|
+
*
|
|
7
|
+
* providers: repoPolicyProviders()
|
|
8
|
+
*
|
|
9
|
+
* ⛔ WHY THIS EXISTS — MEASURED 2026-09-24 (homeflare-builds bump PR 6, tsc). Writing the two
|
|
10
|
+
* providers side by side, `Layer.mergeAll(GitHub.providers(), RepositoryRulesetProvider())`,
|
|
11
|
+
* type-errors: `RepositoryRulesetProvider()`'s handlers call `octokitFor`, which needs
|
|
12
|
+
* `GitHubCredentials` — a requirement of the PROVIDER's layer, correctly inferred by
|
|
13
|
+
* `Provider.succeed` from `repositoryRulesetHandlers` in repository-ruleset.ts. Merging two
|
|
14
|
+
* layers does not thread one's output into the other's requirement; `RepositoryRulesetProvider()`
|
|
15
|
+
* still needs `GitHubCredentials` fed in. `GitHub.providers()` is the only thing here that
|
|
16
|
+
* resolves credentials (env, stored PAT, `gh` CLI, or OAuth — see `alchemy/src/GitHub/
|
|
17
|
+
* Providers.ts`), and — because it chains `Layer.provideMerge(Credentials.fromAuthProvider(…))`
|
|
18
|
+
* internally rather than `Layer.provide` — its own OUTPUT includes `GitHubCredentials`
|
|
19
|
+
* alongside `GitHub.Providers`. `Layer.provideMerge(github)` on `RepositoryRulesetProvider()`
|
|
20
|
+
* both supplies that requirement AND keeps `github`'s own output merged in for any
|
|
21
|
+
* `GitHub.Repository`/`GitHub.Ruleset` the caller also declares. `github` is built once and
|
|
22
|
+
* referenced twice below — Effect's layer memoization (by reference) builds it once, not
|
|
23
|
+
* twice, not twice against the auth provider either.
|
|
24
|
+
*
|
|
25
|
+
* ★ THIS IS ONLY HALF OF 2026-09-24's FIX. The other half — `RepositoryRuleset`'s own `Resource<>`
|
|
26
|
+
* declaration no longer naming `GitHubCredentials` in its 5th (`Providers`) slot — lives in
|
|
27
|
+
* repository-ruleset.ts's file header; that half is what let a stack BODY calling
|
|
28
|
+
* `declareRepoPolicy`/`declareRepoBaseline` typecheck at all. Composing the providers
|
|
29
|
+
* correctly, as this function does, does not by itself fix a body that still requires
|
|
30
|
+
* `GitHubCredentials` directly.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ DON'T ALSO MERGE A SEPARATE `GitHub.providers()` CALL IN. `github` here is one Layer
|
|
33
|
+
* object referenced twice, which Effect's layer memoization (by reference, ADVERSARIAL
|
|
34
|
+
* REVIEW 2026-09-24: confirmed against `effect@4.0.0-rc.115`'s own `MemoMap`) builds once. A
|
|
35
|
+
* stack that ALSO writes its own `GitHub.providers()` alongside this function's result is a
|
|
36
|
+
* second, distinct Layer object — a genuine second credential resolution, not shared with
|
|
37
|
+
* this one. Get every GitHub resource's providers from this one call.
|
|
38
|
+
*/
|
|
39
|
+
import * as GitHub from 'alchemy/GitHub';
|
|
40
|
+
import * as Layer from 'effect/Layer';
|
|
41
|
+
import { RepositoryRulesetProvider } from './repository-ruleset.ts';
|
|
42
|
+
|
|
43
|
+
export const repoPolicyProviders = (options?: GitHub.ProvidersOptions) => {
|
|
44
|
+
const github = GitHub.providers(options);
|
|
45
|
+
return Layer.mergeAll(github, RepositoryRulesetProvider().pipe(Layer.provideMerge(github)));
|
|
46
|
+
};
|
|
@@ -32,35 +32,46 @@ function makeCapturingFetch(captured: CapturedRequest[]): typeof fetch {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
describe('does Octokit forward an untyped extra body field to the wire?', () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
// K1 (2026-09-23): both values are modeled now, not only `false` — 8 live rulesets (aop,
|
|
36
|
+
// cloudflareforms, doesthishelp-workeropen, homeflare-anyauth, homeflare-desktop, loggarr,
|
|
37
|
+
// magictransit, proxmox-tb4; re-read live 2026-09-23) carry `true`, so this measurement needs
|
|
38
|
+
// both, not just the one value this resource used to be able to send.
|
|
39
|
+
test.each([false, true])(
|
|
40
|
+
'require_extra_approval_for_unattributed_changes: %p survives createRepoRuleset',
|
|
41
|
+
async (value) => {
|
|
42
|
+
const captured: CapturedRequest[] = [];
|
|
43
|
+
const octokit = new Octokit({
|
|
44
|
+
auth: 'fake-token-not-a-real-credential',
|
|
45
|
+
request: { fetch: makeCapturingFetch(captured) },
|
|
46
|
+
});
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const body = desiredWireRuleset({
|
|
49
|
+
owner: 'taslabs-net',
|
|
50
|
+
repository: 'widgets',
|
|
51
|
+
name: 'main',
|
|
52
|
+
rules: {
|
|
53
|
+
pullRequest: {
|
|
54
|
+
requiredApprovingReviewCount: 0,
|
|
55
|
+
allowedMergeMethods: ['squash'],
|
|
56
|
+
extraApprovalForUnattributedChanges: value,
|
|
57
|
+
},
|
|
51
58
|
},
|
|
52
|
-
}
|
|
53
|
-
});
|
|
59
|
+
});
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
await octokit.rest.repos.createRepoRuleset({
|
|
62
|
+
owner: 'taslabs-net',
|
|
63
|
+
repo: 'widgets',
|
|
64
|
+
...body,
|
|
65
|
+
});
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
expect(captured).toHaveLength(1);
|
|
68
|
+
const sent = captured[0]?.body as {
|
|
69
|
+
rules?: { type: string; parameters?: Record<string, unknown> }[];
|
|
70
|
+
};
|
|
71
|
+
const pr = sent?.rules?.find((r) => r.type === 'pull_request');
|
|
72
|
+
// MEASURED: this is what actually crossed the wire, not what Octokit's types say is legal.
|
|
73
|
+
expect(pr?.parameters?.require_extra_approval_for_unattributed_changes).toBe(value);
|
|
74
|
+
expect(pr?.parameters?.allowed_merge_methods).toEqual(['squash']);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
66
77
|
});
|
|
@@ -38,11 +38,33 @@
|
|
|
38
38
|
* meant to allow. `octokitFor`/`GitHubCredentials` are upstream's own EXPORTED building
|
|
39
39
|
* blocks (S23's second clause: reuse Alchemy's own client construction), so the client itself
|
|
40
40
|
* is not hand-rolled — only the choice of Octokit over distilled is a recorded divergence.
|
|
41
|
+
*
|
|
42
|
+
* ⛔ CREDENTIALS BELONG TO THE PROVIDER'S LAYER, NOT THE RESOURCE'S DECLARATION — MEASURED
|
|
43
|
+
* 2026-09-24 (homeflare-builds bump PR 6, tsc TS2345). The `Resource<Type, Props, Attributes,
|
|
44
|
+
* Binding, Providers>` 5th type parameter had `GitHubCredentials` here; that puts the
|
|
45
|
+
* credential requirement on every `yield* RepositoryRuleset(...)` CALL SITE (see
|
|
46
|
+
* `alchemy/src/Resource.ts#ResourceClass`: `Req = R["Providers"] extends undefined ?
|
|
47
|
+
* Provider<R> : R["Providers"]`), so a STACK BODY that merely declares the resource would
|
|
48
|
+
* itself need `GitHubCredentials` in its own effect context — which a stack body cannot
|
|
49
|
+
* produce (`Alchemy.Stack`'s own type, `alchemy/src/Stack.ts#ProviderServices`, is a closed
|
|
50
|
+
* union of tagged marker interfaces — `ProviderCollectionLike | Provider<any> |
|
|
51
|
+
* EnvironmentLike | CredentialsLike | DockerLike` — and `GitHubCredentials` matches none of
|
|
52
|
+
* them structurally). VERIFIED against upstream `Ruleset` (`node_modules/alchemy/src/GitHub/
|
|
53
|
+
* Ruleset.ts`): its 5th slot is `GitHub.Providers`, the ProviderCollection tag `GitHub.
|
|
54
|
+
* providers()` OUTPUTS — never the raw `GitHubCredentials` service upstream's OWN provider
|
|
55
|
+
* handlers pull in via `octokitFor`. `GitHubCredentials` stays confined to the PROVIDER's
|
|
56
|
+
* layer requirement (`Provider.succeed`'s inferred `ReadReq | DiffReq | ReconcileReq |
|
|
57
|
+
* DeleteReq`, below), never the declaration's. Omitting the 5th parameter here defaults it to
|
|
58
|
+
* `undefined`, so the declaration's own requirement becomes `Provider<RepositoryRuleset>` —
|
|
59
|
+
* itself a `Provider<any>`, a real `ProviderServices` member — matching the pattern a
|
|
60
|
+
* standalone (non-collection) resource actually needs. See `repoPolicyProviders()` in
|
|
61
|
+
* repository-ruleset-providers.ts for the layer composition this now requires (the provider
|
|
62
|
+
* still needs `GitHubCredentials` fed in — that half of the fix was already correct, per
|
|
63
|
+
* `docs/repository-ruleset.md`).
|
|
41
64
|
*/
|
|
42
65
|
import { Resource } from 'alchemy';
|
|
43
66
|
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
44
67
|
import { isResolved } from 'alchemy/Diff';
|
|
45
|
-
import type { GitHubCredentials } from 'alchemy/GitHub';
|
|
46
68
|
import * as Provider from 'alchemy/Provider';
|
|
47
69
|
import * as Effect from 'effect/Effect';
|
|
48
70
|
import {
|
|
@@ -75,8 +97,10 @@ export interface RepositoryRuleset extends Resource<
|
|
|
75
97
|
'GitHub.RepositoryRuleset',
|
|
76
98
|
RepositoryRulesetProps,
|
|
77
99
|
RepositoryRulesetAttributes,
|
|
78
|
-
never
|
|
79
|
-
|
|
100
|
+
never
|
|
101
|
+
// ⛔ NO 5th (`Providers`) ARGUMENT — see the file header. Defaulting it to `undefined` makes
|
|
102
|
+
// a `yield* RepositoryRuleset(...)` call site require `Provider<RepositoryRuleset>`, not
|
|
103
|
+
// `GitHubCredentials`; the credential requirement lives on the PROVIDER below instead.
|
|
80
104
|
> {}
|
|
81
105
|
|
|
82
106
|
export const RepositoryRuleset = Resource<RepositoryRuleset>('GitHub.RepositoryRuleset', {
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
3
|
-
*
|
|
2
|
+
* A fake of LiteLLM's `/config/pass_through_endpoint` routes for tests — TEST ONLY, never imported
|
|
3
|
+
* by `index.ts` (the netbox/forgejo house convention for test-only helpers). Mirrors
|
|
4
|
+
* `../netbox/fake-netbox.ts`/`../forgejo/fake-forgejo.ts`: a `fetch` function handed to Effect's
|
|
5
|
+
* real `FetchHttpClient` through its `Fetch` reference, so a test exercises distilled's REAL path
|
|
6
|
+
* assembly, JSON encode/decode and status→typed-error matching (`BadRequest`, `NotFound`, …) —
|
|
7
|
+
* nothing about that matching is re-implemented here, only the wire responses a scenario needs.
|
|
4
8
|
*
|
|
5
9
|
* ★ MODELS THE WHOLE-FIELD READ-MODIFY-WRITE ON PURPOSE. `general_settings.pass_through_endpoints`
|
|
6
|
-
* is ONE field (
|
|
7
|
-
* (so two concurrent
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* is ONE field (operations.ts's header), so every mutation here reads the CURRENT row list,
|
|
11
|
+
* awaits (so two concurrent calls can interleave — no real socket needed, `fetch` is just an
|
|
12
|
+
* async function the runtime awaits), then writes back what it captured — not what is there by
|
|
13
|
+
* the time it writes. Two `create` calls that race lose one row, exactly like the vendor field
|
|
14
|
+
* would without `operations.ts`'s per-base-URL semaphore serialising them.
|
|
10
15
|
* ⚠️ WHAT THIS FAKE DOES NOT MODEL: PROXY_ADMIN vs. a lesser role for most routes (any correct
|
|
11
|
-
* bearer token is accepted)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* bearer token is accepted). `forbidDelete` is the one exception: it models the real asymmetry
|
|
17
|
+
* `operations.ts`'s header describes — `DELETE` routes through `update_config_general_settings`,
|
|
18
|
+
* which 400s `not_allowed_access` for a non-PROXY_ADMIN caller (or a disconnected DB) on a row
|
|
19
|
+
* that is genuinely still live, the SAME status a missing-id delete gets (proxy_server.py:
|
|
20
|
+
* 16389-16416 at v1.100.0).
|
|
21
|
+
* ⚠️ UPDATE-OF-A-MISSING-ID IS 404, NOT 400 — corrected by this migration. The retired hand-rolled
|
|
22
|
+
* fake 400'd it as "not measured at the tag, a documented test simplification". The distilled
|
|
23
|
+
* SDK's own `patches/misc/update_pass_through_endpoints_*.json` cites the real source
|
|
24
|
+
* (`pass_through_endpoints.py`'s `update_pass_through_endpoints` at the tag): unlike DELETE, this
|
|
25
|
+
* route raises a genuine, unambiguous `HTTPException(404)` for a missing id, distinct from the
|
|
26
|
+
* 400 `update_config_general_settings` raises for the trailing auth/DB-disconnected write. This
|
|
27
|
+
* fake now matches that measured vendor behaviour.
|
|
18
28
|
*/
|
|
19
|
-
import type { PassThroughGenericEndpoint } from '
|
|
29
|
+
import type { PassThroughGenericEndpoint } from '@distilled.cloud/litellm/misc';
|
|
30
|
+
|
|
31
|
+
/** RFC 2606 placeholder — never a real host. Shared across tests, like `../netbox/fake-netbox.ts`. */
|
|
32
|
+
export const FAKE_BASE = 'https://litellm.example.com';
|
|
20
33
|
|
|
21
34
|
export interface FakeRequest {
|
|
22
35
|
readonly method: string;
|
|
@@ -24,15 +37,17 @@ export interface FakeRequest {
|
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export interface FakeLitellm {
|
|
27
|
-
readonly
|
|
40
|
+
readonly fetch: typeof globalThis.fetch;
|
|
28
41
|
readonly rows: () => readonly PassThroughGenericEndpoint[];
|
|
29
42
|
readonly requests: () => readonly FakeRequest[];
|
|
30
|
-
readonly stop: () => void;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
const RACE_WINDOW_MS = 15;
|
|
34
46
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
35
47
|
|
|
48
|
+
const json = (status: number, body: unknown) =>
|
|
49
|
+
new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' }, status });
|
|
50
|
+
|
|
36
51
|
export const startFakeLitellm = (options?: {
|
|
37
52
|
readonly masterKey?: string;
|
|
38
53
|
readonly seed?: readonly PassThroughGenericEndpoint[];
|
|
@@ -44,73 +59,63 @@ export const startFakeLitellm = (options?: {
|
|
|
44
59
|
let rows: PassThroughGenericEndpoint[] = [...(options?.seed ?? [])];
|
|
45
60
|
const requests: FakeRequest[] = [];
|
|
46
61
|
|
|
47
|
-
const
|
|
48
|
-
|
|
62
|
+
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
63
|
+
const request =
|
|
64
|
+
input instanceof Request ? new Request(input, init) : new Request(String(input), init);
|
|
65
|
+
const url = new URL(request.url);
|
|
66
|
+
requests.push({ method: request.method, path: `${url.pathname}${url.search}` });
|
|
67
|
+
const auth = request.headers.get('authorization');
|
|
68
|
+
if (auth !== `Bearer ${masterKey}`) return json(401, { detail: 'invalid api key' });
|
|
69
|
+
if (
|
|
70
|
+
url.pathname !== '/config/pass_through_endpoint' &&
|
|
71
|
+
!url.pathname.startsWith('/config/pass_through_endpoint/')
|
|
72
|
+
) {
|
|
73
|
+
return json(404, { detail: 'not found' });
|
|
74
|
+
}
|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const url = new URL(req.url);
|
|
56
|
-
requests.push({ method: req.method, path: url.pathname });
|
|
57
|
-
const auth = req.headers.get('authorization');
|
|
58
|
-
if (auth !== `Bearer ${masterKey}`) return json(401, { detail: 'invalid api key' });
|
|
59
|
-
if (
|
|
60
|
-
url.pathname !== '/config/pass_through_endpoint' &&
|
|
61
|
-
!url.pathname.startsWith('/config/pass_through_endpoint/')
|
|
62
|
-
) {
|
|
63
|
-
return json(404, { detail: 'not found' });
|
|
64
|
-
}
|
|
76
|
+
if (request.method === 'GET') {
|
|
77
|
+
const endpointId = url.searchParams.get('endpoint_id');
|
|
78
|
+
const filtered = endpointId === null ? rows : rows.filter((row) => row.id === endpointId);
|
|
79
|
+
return json(200, { endpoints: filtered });
|
|
80
|
+
}
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
82
|
+
if (request.method === 'POST' && url.pathname === '/config/pass_through_endpoint') {
|
|
83
|
+
const body = (await request.json()) as PassThroughGenericEndpoint;
|
|
84
|
+
const before = rows; // ★ captured before the window — the race the semaphore prevents
|
|
85
|
+
await sleep(RACE_WINDOW_MS);
|
|
86
|
+
rows = [...before, { ...body, is_from_config: false }];
|
|
87
|
+
return json(200, { body: {} });
|
|
88
|
+
}
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (index === -1) return json(400, { detail: `No pass-through endpoint with id=${id}` });
|
|
87
|
-
// ⚠️ `exclude_none`-shaped: only keys the patch actually carries are merged, matching
|
|
88
|
-
// client.ts's `updateBody` never sending an unset/null field (pass-through-form.ts).
|
|
89
|
-
const merged = { ...before[index], ...patch };
|
|
90
|
-
rows = before.map((row, i) => (i === index ? (merged as PassThroughGenericEndpoint) : row));
|
|
91
|
-
return json(200, {});
|
|
92
|
-
}
|
|
90
|
+
if (request.method === 'POST' && url.pathname.startsWith('/config/pass_through_endpoint/')) {
|
|
91
|
+
const id = decodeURIComponent(url.pathname.slice('/config/pass_through_endpoint/'.length));
|
|
92
|
+
const patch = (await request.json()) as Partial<PassThroughGenericEndpoint>;
|
|
93
|
+
const before = rows;
|
|
94
|
+
await sleep(RACE_WINDOW_MS);
|
|
95
|
+
const index = before.findIndex((row) => row.id === id);
|
|
96
|
+
// ⚠️ 404, not 400 — see the file header.
|
|
97
|
+
if (index === -1) return json(404, { detail: `Endpoint with ID '${id}' not found` });
|
|
98
|
+
// ⚠️ `exclude_none`-shaped: only keys the patch actually carries are merged, matching
|
|
99
|
+
// operations.ts's `updateBody` never sending an unset/null field (pass-through-form.ts).
|
|
100
|
+
const merged = { ...before[index], ...patch };
|
|
101
|
+
rows = before.map((row, i) => (i === index ? (merged as PassThroughGenericEndpoint) : row));
|
|
102
|
+
return json(200, { body: {} });
|
|
103
|
+
}
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
rows = before.filter((row) => row.id !== id);
|
|
103
|
-
return json(200, { endpoints: rows });
|
|
105
|
+
if (request.method === 'DELETE' && url.pathname === '/config/pass_through_endpoint') {
|
|
106
|
+
const id = url.searchParams.get('endpoint_id');
|
|
107
|
+
if (forbidDelete) return json(400, { error: 'not_allowed_access' });
|
|
108
|
+
const before = rows;
|
|
109
|
+
await sleep(RACE_WINDOW_MS);
|
|
110
|
+
if (!before.some((row) => row.id === id)) {
|
|
111
|
+
return json(400, { detail: `No pass-through endpoint with id=${String(id)}` });
|
|
104
112
|
}
|
|
113
|
+
rows = before.filter((row) => row.id !== id);
|
|
114
|
+
return json(200, { endpoints: rows });
|
|
115
|
+
}
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
117
|
+
return json(405, { detail: `unhandled ${request.method} ${url.pathname}` });
|
|
118
|
+
}) as typeof globalThis.fetch;
|
|
109
119
|
|
|
110
|
-
return {
|
|
111
|
-
requests: () => [...requests],
|
|
112
|
-
rows: () => [...rows],
|
|
113
|
-
stop: () => server.stop(true),
|
|
114
|
-
url: server.url.toString().replace(/\/$/, ''),
|
|
115
|
-
};
|
|
120
|
+
return { fetch, requests: () => [...requests], rows: () => [...rows] };
|
|
116
121
|
};
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
* ★ NOT alchemy/Test/Bun — same reasoning as openbao/fake-stack.ts: no CLI profile/credentials
|
|
13
13
|
* files, no file logger. Just the provider, an in-memory state store, and the stack's services.
|
|
14
14
|
*/
|
|
15
|
+
import { credentials } from '@distilled.cloud/litellm/Credentials';
|
|
15
16
|
import * as Alchemy from 'alchemy';
|
|
16
17
|
import { AdoptPolicy } from 'alchemy/AdoptPolicy';
|
|
17
18
|
import { provideFreshArtifactStore } from 'alchemy/Artifacts';
|
|
18
19
|
import * as Effect from 'effect/Effect';
|
|
19
20
|
import * as Layer from 'effect/Layer';
|
|
20
21
|
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
21
|
-
import { type LitellmCreds, litellmCredentialsLayerFor } from './credentials.ts';
|
|
22
22
|
import { LiteLLMPassThroughEndpointProvider } from './pass-through-endpoint.ts';
|
|
23
23
|
|
|
24
24
|
/** The planned action for each resource, deletions included, keyed by FQN. */
|
|
@@ -53,13 +53,20 @@ const actionsOf = (plan: PlanView): Planned => {
|
|
|
53
53
|
/**
|
|
54
54
|
* ⚠️ CREDENTIALS/HTTPCLIENT ARE PROVIDED AROUND `deploy`, NOT BAKED INTO THE PROVIDER LAYER.
|
|
55
55
|
* `Provider.effect`'s own construction effect is `Effect.succeed(...)` (pass-through-endpoint.ts)
|
|
56
|
-
* — it never actually reads `
|
|
57
|
-
*
|
|
56
|
+
* — it never actually reads `Credentials`/`HttpClient`; only the HANDLERS it returns do, and
|
|
57
|
+
* those run later, whenever `Apply.ts` calls `provider.reconcile`/`read`/`delete`. Effect
|
|
58
58
|
* resolves a `yield*`'s service from whatever ambient context wraps the CALL SITE, so the layers
|
|
59
59
|
* those handlers need must wrap the whole plan+apply pipeline below, exactly where openbao's
|
|
60
60
|
* fake-stack.ts wraps `BaoEnv` around its own `deploy`.
|
|
61
|
+
* ★ `fetchFn` IS `startFakeLitellm(...).fetch` (fake-litellm.ts) — this harness never opens a real
|
|
62
|
+
* socket; `FetchHttpClient.Fetch` is the seam the SDK's protocol layer calls through, same as
|
|
63
|
+
* `../netbox/fake-netbox.ts`'s `fakeNetboxLayer`.
|
|
61
64
|
*/
|
|
62
|
-
export const fakeStack = (
|
|
65
|
+
export const fakeStack = (
|
|
66
|
+
creds: { readonly apiKey: string; readonly baseUrl: string },
|
|
67
|
+
fetchFn: typeof globalThis.fetch,
|
|
68
|
+
name = 'PassThroughStack',
|
|
69
|
+
): FakeStack => {
|
|
63
70
|
const state = Alchemy.inMemoryState();
|
|
64
71
|
const providers = LiteLLMPassThroughEndpointProvider();
|
|
65
72
|
const stack = Alchemy.Stack as unknown as (
|
|
@@ -83,8 +90,9 @@ export const fakeStack = (creds: LitellmCreds, name = 'PassThroughStack'): FakeS
|
|
|
83
90
|
}).pipe(
|
|
84
91
|
options.adopt === undefined ? (e) => e : Effect.provideService(AdoptPolicy, options.adopt),
|
|
85
92
|
Effect.provideService(Alchemy.Stage, 'test'),
|
|
86
|
-
Effect.provide(
|
|
93
|
+
Effect.provide(credentials(creds)),
|
|
87
94
|
Effect.provide(FetchHttpClient.layer),
|
|
95
|
+
Effect.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
|
|
88
96
|
Effect.scoped,
|
|
89
97
|
);
|
|
90
98
|
|