@homeflare/alchemy 0.2.0 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo and Talos.",
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",
@@ -14,6 +14,7 @@
14
14
  import * as Context from 'effect/Context';
15
15
  import * as Effect from 'effect/Effect';
16
16
  import * as Option from 'effect/Option';
17
+ import * as Schedule from 'effect/Schedule';
17
18
  import * as Semaphore from 'effect/Semaphore';
18
19
  import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
19
20
  import * as Headers from 'effect/unstable/http/Headers';
@@ -66,6 +67,19 @@ export const BaoGate = Context.Reference<Semaphore.Semaphore>('homeflare/openbao
66
67
  */
67
68
  const TIMEOUT = '60 seconds';
68
69
 
70
+ /**
71
+ * Mesh in front of a remote OpenBao drops connections under Alchemy's unbounded
72
+ * fan-out. Status 0 is transport, not an OpenBao 4xx — retry twice. Measured
73
+ * 2026-09-16 against api.v.homeflare.dev: a 585-role plan died mid-diff with
74
+ * `no response: (no errors given)` while the vault stayed unsealed.
75
+ */
76
+ const retryTransport = <A, R>(effect: Effect.Effect<A, BaoError, R>) =>
77
+ Effect.retry(effect, {
78
+ schedule: Schedule.spaced('750 millis'),
79
+ times: 2,
80
+ while: (error: BaoError) => error.status === 0,
81
+ });
82
+
69
83
  /**
70
84
  * ⛔ EFFECT RECORDS EVERY REQUEST HEADER ON THE CLIENT SPAN and redacts only the names in
71
85
  * `Headers.CurrentRedactedNames` — by default authorization, cookie, set-cookie and x-api-key
@@ -142,10 +156,12 @@ export const baoCall = (
142
156
  // ★ THE PERMIT WRAPS THE TIMEOUT, NOT THE REVERSE: a call queued behind the gate is not yet
143
157
  // talking to OpenBao, so its wait must not count against the 60 seconds.
144
158
  const gate = yield* BaoGate;
145
- const { status, text } = yield* exchange.pipe(
146
- overSocket(address.socket, method, path),
147
- redactingToken,
148
- Semaphore.withPermits(gate, 1),
159
+ const { status, text } = yield* retryTransport(
160
+ exchange.pipe(
161
+ overSocket(address.socket, method, path),
162
+ redactingToken,
163
+ Semaphore.withPermits(gate, 1),
164
+ ),
149
165
  );
150
166
  const outcome = settle(intent, method, path, status, text);
151
167
  return 'error' in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
@@ -8,9 +8,18 @@
8
8
  * ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
9
9
  * deliberate, visible act rather than an accident of barrelling.
10
10
  */
11
- export { BaoAuthRoleProvider } from './auth-role.ts';
12
- export { BaoCloudflareRoleProvider } from './cloudflare-role.ts';
13
- export { BaoMountProvider } from './mount.ts';
11
+ export { BaoAuthRole, BaoAuthRoleProvider } from './auth-role.ts';
12
+ export { BaoCloudflareRole, BaoCloudflareRoleProvider } from './cloudflare-role.ts';
13
+ export {
14
+ CloudflarePermissionGroups,
15
+ permissionGroupsFromEngine,
16
+ permissionGroupsFromLiveRoles,
17
+ } from './cloudflare-permission-groups.ts';
18
+ export type { CloudflareZone, ExpandedRole } from './cloudflare-roles-expand.ts';
19
+ export { expandAccount, expandAll, mountName } from './cloudflare-roles-expand.ts';
20
+ export type { RolesConfig, SurfaceSpec } from './cloudflare-roles-config.ts';
21
+ export { parseRolesConfig } from './cloudflare-roles-config.ts';
22
+ export { BaoMount, BaoMountProvider } from './mount.ts';
14
23
  export { BaoPkiRoleProvider } from './pki-role.ts';
15
24
  export { BaoPolicy, BaoPolicyProvider } from './policy.ts';
16
25
  export { BaoProxmoxRole, BaoProxmoxRoleProvider } from './proxmox-role.ts';
@@ -55,7 +55,13 @@ const remapFetch = (
55
55
  url.port = String(port);
56
56
  return underlying(url, init);
57
57
  }
58
- return underlying(input, init);
58
+ // ⛔ NEVER fall through to the real resolver. `.test` is reserved (RFC 2606) but
59
+ // GitHub's Linux runners sometimes hang on it until node:test's 5s timeout —
60
+ // measured 2026-09-16: release verify failed on "skips an unresolvable first
61
+ // member" while `ci` on the same SHA passed. The file header says no live hosts.
62
+ return Promise.reject(
63
+ Object.assign(new Error(`getaddrinfo ENOTFOUND ${url.hostname}`), { code: 'ENOTFOUND' }),
64
+ );
59
65
  },
60
66
  { preconnect: globalThis.fetch.preconnect },
61
67
  );