@distilled.cloud/core 0.30.3 → 1.0.0-rc.10

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.
Files changed (216) hide show
  1. package/LICENSE +201 -0
  2. package/lib/api.d.ts +165 -0
  3. package/lib/api.d.ts.map +1 -0
  4. package/lib/api.js +190 -0
  5. package/lib/api.js.map +1 -0
  6. package/lib/category.d.ts +4 -4
  7. package/lib/category.js +4 -4
  8. package/lib/codegen/boolean-string-enums.d.ts +36 -0
  9. package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
  10. package/lib/codegen/boolean-string-enums.js +94 -0
  11. package/lib/codegen/boolean-string-enums.js.map +1 -0
  12. package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
  13. package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
  14. package/lib/codegen/boolean-string-enums.test.js +147 -0
  15. package/lib/codegen/boolean-string-enums.test.js.map +1 -0
  16. package/lib/codegen/cli.d.ts +79 -0
  17. package/lib/codegen/cli.d.ts.map +1 -0
  18. package/lib/codegen/cli.js +149 -0
  19. package/lib/codegen/cli.js.map +1 -0
  20. package/lib/codegen/emit.d.ts +125 -0
  21. package/lib/codegen/emit.d.ts.map +1 -0
  22. package/lib/codegen/emit.js +101 -0
  23. package/lib/codegen/emit.js.map +1 -0
  24. package/lib/codegen/format.d.ts +23 -0
  25. package/lib/codegen/format.d.ts.map +1 -0
  26. package/lib/codegen/format.js +28 -0
  27. package/lib/codegen/format.js.map +1 -0
  28. package/lib/codegen/generator.d.ts +334 -0
  29. package/lib/codegen/generator.d.ts.map +1 -0
  30. package/lib/codegen/generator.js +813 -0
  31. package/lib/codegen/generator.js.map +1 -0
  32. package/lib/codegen/graph.d.ts +36 -0
  33. package/lib/codegen/graph.d.ts.map +1 -0
  34. package/lib/codegen/graph.js +136 -0
  35. package/lib/codegen/graph.js.map +1 -0
  36. package/lib/codegen/graphql-client.d.ts +62 -0
  37. package/lib/codegen/graphql-client.d.ts.map +1 -0
  38. package/lib/codegen/graphql-client.js +294 -0
  39. package/lib/codegen/graphql-client.js.map +1 -0
  40. package/lib/codegen/graphql-client.test.d.ts +2 -0
  41. package/lib/codegen/graphql-client.test.d.ts.map +1 -0
  42. package/lib/codegen/graphql-client.test.js +311 -0
  43. package/lib/codegen/graphql-client.test.js.map +1 -0
  44. package/lib/codegen/graphql.d.ts +207 -0
  45. package/lib/codegen/graphql.d.ts.map +1 -0
  46. package/lib/codegen/graphql.js +799 -0
  47. package/lib/codegen/graphql.js.map +1 -0
  48. package/lib/codegen/members.d.ts +25 -0
  49. package/lib/codegen/members.d.ts.map +1 -0
  50. package/lib/codegen/members.js +55 -0
  51. package/lib/codegen/members.js.map +1 -0
  52. package/lib/codegen/naming.d.ts +29 -0
  53. package/lib/codegen/naming.d.ts.map +1 -0
  54. package/lib/codegen/naming.js +74 -0
  55. package/lib/codegen/naming.js.map +1 -0
  56. package/lib/codegen/openapi-cli.d.ts +52 -0
  57. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  58. package/lib/codegen/openapi-cli.js +109 -0
  59. package/lib/codegen/openapi-cli.js.map +1 -0
  60. package/lib/codegen/openapi.d.ts +178 -0
  61. package/lib/codegen/openapi.d.ts.map +1 -0
  62. package/lib/codegen/openapi.js +1377 -0
  63. package/lib/codegen/openapi.js.map +1 -0
  64. package/lib/codegen/operations.d.ts +24 -0
  65. package/lib/codegen/operations.d.ts.map +1 -0
  66. package/lib/codegen/operations.js +56 -0
  67. package/lib/codegen/operations.js.map +1 -0
  68. package/lib/codegen/pagination.d.ts +39 -0
  69. package/lib/codegen/pagination.d.ts.map +1 -0
  70. package/lib/codegen/pagination.js +33 -0
  71. package/lib/codegen/pagination.js.map +1 -0
  72. package/lib/codegen/patches.d.ts +65 -0
  73. package/lib/codegen/patches.d.ts.map +1 -0
  74. package/lib/codegen/patches.js +236 -0
  75. package/lib/codegen/patches.js.map +1 -0
  76. package/lib/codegen/patches.test.d.ts +2 -0
  77. package/lib/codegen/patches.test.d.ts.map +1 -0
  78. package/lib/codegen/patches.test.js +105 -0
  79. package/lib/codegen/patches.test.js.map +1 -0
  80. package/lib/codegen/prelude.d.ts +15 -0
  81. package/lib/codegen/prelude.d.ts.map +1 -0
  82. package/lib/codegen/prelude.js +60 -0
  83. package/lib/codegen/prelude.js.map +1 -0
  84. package/lib/codegen/proto.d.ts +121 -0
  85. package/lib/codegen/proto.d.ts.map +1 -0
  86. package/lib/codegen/proto.js +962 -0
  87. package/lib/codegen/proto.js.map +1 -0
  88. package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
  89. package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
  90. package/lib/codegen/rewrite-operation-ids.js +1079 -0
  91. package/lib/codegen/rewrite-operation-ids.js.map +1 -0
  92. package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
  93. package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
  94. package/lib/codegen/rewrite-operation-ids.test.js +533 -0
  95. package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
  96. package/lib/codegen/spec-path.d.ts +16 -0
  97. package/lib/codegen/spec-path.d.ts.map +1 -0
  98. package/lib/codegen/spec-path.js +101 -0
  99. package/lib/codegen/spec-path.js.map +1 -0
  100. package/lib/error-category.d.ts +28 -0
  101. package/lib/error-category.d.ts.map +1 -0
  102. package/lib/error-category.js +46 -0
  103. package/lib/error-category.js.map +1 -0
  104. package/lib/errors.d.ts +1 -0
  105. package/lib/errors.d.ts.map +1 -1
  106. package/lib/errors.js +18 -13
  107. package/lib/errors.js.map +1 -1
  108. package/lib/graphql.d.ts +284 -0
  109. package/lib/graphql.d.ts.map +1 -0
  110. package/lib/graphql.fixture.d.ts +249 -0
  111. package/lib/graphql.fixture.d.ts.map +1 -0
  112. package/lib/graphql.fixture.js +240 -0
  113. package/lib/graphql.fixture.js.map +1 -0
  114. package/lib/graphql.js +718 -0
  115. package/lib/graphql.js.map +1 -0
  116. package/lib/graphql.test.d.ts +2 -0
  117. package/lib/graphql.test.d.ts.map +1 -0
  118. package/lib/graphql.test.js +780 -0
  119. package/lib/graphql.test.js.map +1 -0
  120. package/lib/graphql.types.d.ts +2 -0
  121. package/lib/graphql.types.d.ts.map +1 -0
  122. package/lib/graphql.types.js +45 -0
  123. package/lib/graphql.types.js.map +1 -0
  124. package/lib/json-patch.d.ts +30 -30
  125. package/lib/json-patch.d.ts.map +1 -1
  126. package/lib/json-patch.js +73 -107
  127. package/lib/json-patch.js.map +1 -1
  128. package/lib/pagination.d.ts +77 -51
  129. package/lib/pagination.d.ts.map +1 -1
  130. package/lib/pagination.js +162 -94
  131. package/lib/pagination.js.map +1 -1
  132. package/lib/protocol-http.d.ts +74 -0
  133. package/lib/protocol-http.d.ts.map +1 -0
  134. package/lib/protocol-http.js +590 -0
  135. package/lib/protocol-http.js.map +1 -0
  136. package/lib/protocol-http.test.d.ts +2 -0
  137. package/lib/protocol-http.test.d.ts.map +1 -0
  138. package/lib/protocol-http.test.js +88 -0
  139. package/lib/protocol-http.test.js.map +1 -0
  140. package/lib/protocol-rest.d.ts +134 -0
  141. package/lib/protocol-rest.d.ts.map +1 -0
  142. package/lib/protocol-rest.js +256 -0
  143. package/lib/protocol-rest.js.map +1 -0
  144. package/lib/retry.d.ts +8 -2
  145. package/lib/retry.d.ts.map +1 -1
  146. package/lib/retry.js +22 -16
  147. package/lib/retry.js.map +1 -1
  148. package/lib/schema.d.ts +8 -9
  149. package/lib/schema.d.ts.map +1 -1
  150. package/lib/schema.js +8 -9
  151. package/lib/schema.js.map +1 -1
  152. package/lib/trait.d.ts +174 -0
  153. package/lib/trait.d.ts.map +1 -0
  154. package/lib/trait.js +123 -0
  155. package/lib/trait.js.map +1 -0
  156. package/package.json +24 -78
  157. package/src/api.ts +460 -0
  158. package/src/category.ts +4 -4
  159. package/src/codegen/boolean-string-enums.test.ts +168 -0
  160. package/src/codegen/boolean-string-enums.ts +106 -0
  161. package/src/codegen/cli.ts +285 -0
  162. package/src/codegen/emit.ts +203 -0
  163. package/src/codegen/format.ts +47 -0
  164. package/src/codegen/generator.ts +1283 -0
  165. package/src/codegen/graph.ts +151 -0
  166. package/src/codegen/graphql-client.test.ts +386 -0
  167. package/src/codegen/graphql-client.ts +419 -0
  168. package/src/codegen/graphql.ts +1217 -0
  169. package/src/codegen/members.ts +71 -0
  170. package/src/codegen/naming.ts +86 -0
  171. package/src/codegen/openapi-cli.ts +182 -0
  172. package/src/codegen/openapi.ts +1689 -0
  173. package/src/codegen/operations.ts +76 -0
  174. package/src/codegen/pagination.ts +71 -0
  175. package/src/codegen/patches.test.ts +130 -0
  176. package/src/codegen/patches.ts +291 -0
  177. package/src/codegen/prelude.ts +70 -0
  178. package/src/codegen/proto.ts +1128 -0
  179. package/src/codegen/rewrite-operation-ids.test.ts +563 -0
  180. package/src/codegen/rewrite-operation-ids.ts +1206 -0
  181. package/src/codegen/spec-path.ts +115 -0
  182. package/src/error-category.ts +84 -0
  183. package/src/errors.ts +22 -25
  184. package/src/graphql.fixture.ts +371 -0
  185. package/src/graphql.test.ts +974 -0
  186. package/src/graphql.ts +1321 -0
  187. package/src/graphql.types.ts +185 -0
  188. package/src/json-patch.ts +95 -122
  189. package/src/pagination.ts +217 -146
  190. package/src/protocol-http.test.ts +107 -0
  191. package/src/protocol-http.ts +735 -0
  192. package/src/protocol-rest.ts +391 -0
  193. package/src/retry.ts +21 -22
  194. package/src/schema.ts +9 -10
  195. package/src/trait.ts +274 -0
  196. package/README.md +0 -30
  197. package/lib/client.d.ts +0 -167
  198. package/lib/client.d.ts.map +0 -1
  199. package/lib/client.js +0 -659
  200. package/lib/client.js.map +0 -1
  201. package/lib/schemas.d.ts +0 -60
  202. package/lib/schemas.d.ts.map +0 -1
  203. package/lib/schemas.js +0 -79
  204. package/lib/schemas.js.map +0 -1
  205. package/lib/sensitive.d.ts +0 -71
  206. package/lib/sensitive.d.ts.map +0 -1
  207. package/lib/sensitive.js +0 -96
  208. package/lib/sensitive.js.map +0 -1
  209. package/lib/traits.d.ts +0 -421
  210. package/lib/traits.d.ts.map +0 -1
  211. package/lib/traits.js +0 -737
  212. package/lib/traits.js.map +0 -1
  213. package/src/client.ts +0 -1177
  214. package/src/schemas.ts +0 -128
  215. package/src/sensitive.ts +0 -119
  216. package/src/traits.ts +0 -996
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Generic REST protocol factory for simple bearer/header-auth JSON APIs
3
+ * (Neon, Fly.io, PlanetScale, Supabase, Turso, …).
4
+ *
5
+ * `makeRestProtocol(options)` builds a core {@link API.Protocol} layer:
6
+ *
7
+ * request: credentials resolved from the CALLING fiber's context on every
8
+ * request (the layer itself is memoized per process — see
9
+ * `core/api`), auth headers + base URL from the provider's
10
+ * options, request built by `buildRequest` from
11
+ * `core/protocol-http` (deep-unwrapping any `Redacted` input
12
+ * values first)
13
+ *
14
+ * response: 2xx JSON → optional `transformResponse` → recursive wire→TS
15
+ * key mapping (`mapKeys`) → `Redacted` wrapping of members
16
+ * marked with {@link SensitiveValue}; non-2xx → typed error:
17
+ * per-op matcher classes (`matchTypedError`), then the status
18
+ * map (default `HTTP_STATUS_MAP`), then an `InternalServerError`
19
+ * for unmapped 5xx, then the provider's `unknownError` fallback
20
+ * — with `retryAfter` durations stamped from the standard hint
21
+ * headers on retryable statuses.
22
+ *
23
+ * Anything provider-specific (envelope quirks, bespoke error codes) belongs
24
+ * in the provider package — via these options or a hand-written protocol.
25
+ */
26
+ import * as Effect from "effect/Effect";
27
+ import * as Layer from "effect/Layer";
28
+ import * as Redacted from "effect/Redacted";
29
+ import type * as AST from "effect/SchemaAST";
30
+ import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
31
+ import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
32
+ import * as API from "./api.ts";
33
+ import { httpSymbol, makeAnnotation, type HttpTrait } from "./trait.ts";
34
+ import {
35
+ buildRequest,
36
+ getAnn,
37
+ getPropAnn,
38
+ getProps,
39
+ isOpaqueValue,
40
+ mapKeys,
41
+ matchTypedError,
42
+ resolveNode,
43
+ } from "./protocol-http.ts";
44
+ import { HTTP_STATUS_MAP, InternalServerError } from "./errors.ts";
45
+ import { parseRetryAfterForStatus } from "./retry-after.ts";
46
+
47
+ // =============================================================================
48
+ // Traits
49
+ // =============================================================================
50
+
51
+ export const sensitiveValueSymbol = Symbol.for(
52
+ "@distilled.cloud/core/sensitive-value",
53
+ );
54
+ /**
55
+ * Marks a string member as sensitive (mirrors `smithy.api#sensitive`).
56
+ * The REST protocol wraps decoded values in `Redacted` on the way out and
57
+ * accepts `string | Redacted<string>` on the way in (unwrapped before
58
+ * serialization). Takes an ignored argument so generators can inline the
59
+ * smithy trait value (`T.SensitiveValue({})`).
60
+ */
61
+ export const SensitiveValue = (_value?: unknown) =>
62
+ makeAnnotation(sensitiveValueSymbol, true);
63
+
64
+ export const rawResponseSymbol = Symbol.for(
65
+ "@distilled.cloud/core/raw-response",
66
+ );
67
+ /**
68
+ * Marks the sole output member that carries a bare (array/scalar) response
69
+ * body (mirrors `com.distilled.openapi#rawResponse`).
70
+ */
71
+ export const RawResponse = (_value?: unknown) =>
72
+ makeAnnotation(rawResponseSymbol, true);
73
+
74
+ export const rawResponseRootSymbol = Symbol.for(
75
+ "@distilled.cloud/core/raw-response-root",
76
+ );
77
+ /**
78
+ * Marks a response schema whose ENTIRE value is the response body (the
79
+ * generator's `rootPipe` for synthesized bare-payload wrappers): the emitted
80
+ * response type IS the payload type and the protocol returns the mapped body
81
+ * directly.
82
+ */
83
+ export const RawResponseRoot = () =>
84
+ makeAnnotation(rawResponseRootSymbol, true);
85
+
86
+ // =============================================================================
87
+ // Value helpers
88
+ // =============================================================================
89
+
90
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
91
+ v !== null &&
92
+ typeof v === "object" &&
93
+ !Array.isArray(v) &&
94
+ !isOpaqueValue(v) &&
95
+ (Object.getPrototypeOf(v) === Object.prototype ||
96
+ Object.getPrototypeOf(v) === null);
97
+
98
+ /**
99
+ * Deep-unwrap `Redacted` values in an input (plain objects/arrays only —
100
+ * class instances, files, and binary payloads pass through untouched).
101
+ * Sensitive input members accept `string | Redacted<string>`; the wire wants
102
+ * the raw string.
103
+ */
104
+ export const unwrapRedactedDeep = (value: unknown): unknown => {
105
+ if (Redacted.isRedacted(value)) return Redacted.value(value);
106
+ if (Array.isArray(value)) return value.map(unwrapRedactedDeep);
107
+ if (isPlainObject(value)) {
108
+ const out: Record<string, unknown> = {};
109
+ for (const [k, v] of Object.entries(value)) {
110
+ if (v === undefined) continue;
111
+ out[k] = unwrapRedactedDeep(v);
112
+ }
113
+ return out;
114
+ }
115
+ return value;
116
+ };
117
+
118
+ /**
119
+ * Walk a decoded (TS-named) value alongside its schema AST, wrapping members
120
+ * marked {@link SensitiveValue} in `Redacted`. Keys the schema doesn't model
121
+ * pass through verbatim.
122
+ */
123
+ export const wrapSensitive = (ast: AST.AST, value: unknown): unknown => {
124
+ if (value === null || typeof value !== "object" || isOpaqueValue(value)) {
125
+ return value;
126
+ }
127
+ const node = resolveNode(ast);
128
+ if (node._tag === "Arrays") {
129
+ if (!Array.isArray(value)) return value;
130
+ const elem = (node as any).rest?.[0] as AST.AST | undefined;
131
+ return elem ? value.map((v) => wrapSensitive(elem, v)) : value;
132
+ }
133
+ if (node._tag === "Objects" && !Array.isArray(value)) {
134
+ const props = getProps(node);
135
+ if (props.length === 0) return value;
136
+ const byName = new Map(props.map((p) => [String(p.name), p]));
137
+ const out: Record<string, unknown> = {};
138
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
139
+ if (v === undefined) continue;
140
+ const prop = byName.get(k);
141
+ if (!prop) {
142
+ out[k] = v;
143
+ } else if (
144
+ getPropAnn(prop, sensitiveValueSymbol) !== undefined &&
145
+ typeof v === "string"
146
+ ) {
147
+ out[k] = Redacted.make(v);
148
+ } else {
149
+ out[k] = wrapSensitive(prop.type, v);
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+ return value;
155
+ };
156
+
157
+ // =============================================================================
158
+ // Protocol factory
159
+ // =============================================================================
160
+
161
+ /** What the wire said about a failure, for the `unknownError` fallback. */
162
+ export interface RestErrorInfo {
163
+ readonly status: number;
164
+ /** Error code from the envelope (services vary between string and number). */
165
+ readonly code?: string | number;
166
+ readonly message: string;
167
+ /** Parsed JSON body, or the raw text when the body wasn't JSON. */
168
+ readonly body: unknown;
169
+ readonly headers: Record<string, string | undefined>;
170
+ }
171
+
172
+ export interface RestErrorEnvelope {
173
+ readonly code?: string | number;
174
+ readonly message?: string;
175
+ }
176
+
177
+ export interface RestProtocolOptions<C> {
178
+ /**
179
+ * Resolve credentials ON THE CALLING FIBER — evaluated per request, never
180
+ * at layer build time, so context-provided credentials and token refreshes
181
+ * are picked up. Typically `Effect.gen(function* () { const resolve =
182
+ * yield* Credentials; return yield* resolve; })` for the distilled
183
+ * credentials-service convention. Its error/requirement channels are
184
+ * erased at the protocol boundary (Protocol effects carry none) and
185
+ * reintroduced for callers by the generated `<Sdk>OpError` /
186
+ * `<Sdk>OpContext` annotations.
187
+ */
188
+ readonly credentials: Effect.Effect<C, any, any>;
189
+ /**
190
+ * API base URL from the resolved credentials. Receives the operation's
191
+ * route so APIs with more than one endpoint (e.g. S2's account vs
192
+ * per-basin hosts, chosen by path) can route per request; single-endpoint
193
+ * providers ignore the argument. A THROWN error (e.g. a required scope
194
+ * missing from the credentials) is caught and surfaced on the calling
195
+ * effect's error channel rather than dying as a defect.
196
+ */
197
+ readonly baseUrl: (
198
+ credentials: C,
199
+ target: { readonly uri: string; readonly method: string },
200
+ ) => string;
201
+ /** Auth (and any fixed) headers from the resolved credentials. */
202
+ readonly headers: (credentials: C) => Record<string, string>;
203
+ /**
204
+ * Extract `{ code?, message? }` from a parsed non-2xx JSON body. Default:
205
+ * lenient `{ code?, message? | error? }` (covers the common REST error
206
+ * envelopes).
207
+ */
208
+ readonly errorEnvelope?: (body: unknown) => RestErrorEnvelope | undefined;
209
+ /**
210
+ * HTTP status → error class constructed as `new Cls({ message, retryAfter
211
+ * })`. Default: core `HTTP_STATUS_MAP`. Consulted after per-op typed error
212
+ * matching; unmapped 5xx fall back to `InternalServerError`, everything
213
+ * else to {@link unknownError}.
214
+ */
215
+ readonly statusMap?: Readonly<Record<number, new (args: any) => any>>;
216
+ /** Fallback error for failures nothing else matched. */
217
+ readonly unknownError: (info: RestErrorInfo) => unknown;
218
+ /** Transform the parsed 2xx JSON before decoding (e.g. stripNulls). */
219
+ readonly transformResponse?: (body: unknown) => unknown;
220
+ /** Passed through to `buildRequest` (member-header transforms). */
221
+ readonly mapMemberHeader?: (name: string, value: string) => string;
222
+ /** Passed through to `buildRequest` (wire names for unmodeled input keys). */
223
+ readonly unknownKeyToWire?: (key: string) => string;
224
+ }
225
+
226
+ const defaultErrorEnvelope = (body: unknown): RestErrorEnvelope | undefined => {
227
+ if (body === null || typeof body !== "object") return undefined;
228
+ const b = body as Record<string, unknown>;
229
+ const code =
230
+ typeof b.code === "string" || typeof b.code === "number"
231
+ ? b.code
232
+ : undefined;
233
+ const message =
234
+ typeof b.message === "string"
235
+ ? b.message
236
+ : typeof b.error === "string"
237
+ ? b.error
238
+ : undefined;
239
+ return { code, message };
240
+ };
241
+
242
+ // Bridge: Protocol.decode is typed as Effect<unknown> (no error channel),
243
+ // but REST failures are real typed errors that operations re-surface via
244
+ // their `errors: [...]` lists. Fail with the instance and erase the type
245
+ // here; the generated operation annotations reintroduce it for callers.
246
+ const fail = (e: unknown): Effect.Effect<never> =>
247
+ Effect.fail(e) as Effect.Effect<never>;
248
+
249
+ /**
250
+ * Build a `Layer<Protocol>` for a simple REST JSON API. Assign the result to
251
+ * a module-level const in the provider's `protocol.ts` — `API.make` memoizes
252
+ * protocol layers by value identity.
253
+ */
254
+ export const makeRestProtocol = <C>(
255
+ options: RestProtocolOptions<C>,
256
+ ): Layer.Layer<API.Protocol> => {
257
+ const errorEnvelope = options.errorEnvelope ?? defaultErrorEnvelope;
258
+ const statusMap: Readonly<
259
+ Record<number, (new (args: any) => any) | undefined>
260
+ > = options.statusMap ?? HTTP_STATUS_MAP;
261
+
262
+ const encode = ({
263
+ input,
264
+ inputAst,
265
+ }: {
266
+ readonly input: unknown;
267
+ readonly inputAst: AST.AST;
268
+ }) =>
269
+ Effect.gen(function* () {
270
+ const creds = yield* options.credentials as Effect.Effect<C>;
271
+ const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
272
+ let baseUrl: string;
273
+ try {
274
+ baseUrl = options.baseUrl(creds, {
275
+ uri: http?.uri ?? "",
276
+ method: http?.method ?? "",
277
+ });
278
+ } catch (e) {
279
+ // A baseUrl that refuses the route (missing scope in the
280
+ // credentials) fails the call with that error, typed for callers by
281
+ // the operation's declared error channel.
282
+ return yield* Effect.fail(e);
283
+ }
284
+ return buildRequest({
285
+ input: unwrapRedactedDeep(input),
286
+ inputAst,
287
+ baseUrl,
288
+ headers: options.headers(creds),
289
+ mapMemberHeader: options.mapMemberHeader,
290
+ unknownKeyToWire: options.unknownKeyToWire,
291
+ });
292
+ });
293
+
294
+ const decode = ({
295
+ response,
296
+ outputAst,
297
+ errors: errorClasses,
298
+ }: {
299
+ readonly response: HttpClientResponse.HttpClientResponse;
300
+ readonly outputAst: AST.AST;
301
+ readonly errors: ReadonlyArray<unknown>;
302
+ }) =>
303
+ Effect.gen(function* () {
304
+ // Read as text and parse tolerantly — error pages are often non-JSON.
305
+ const text = (yield* response.text.pipe(Effect.orDie)) ?? "";
306
+ if (process.env.DISTILLED_DEBUG_HTTP) {
307
+ console.error(
308
+ `[distilled] <- ${response.status} ${text.slice(0, 400)}`,
309
+ );
310
+ }
311
+ let json: unknown;
312
+ let nonJson = false;
313
+ if (text.trim().length > 0) {
314
+ try {
315
+ json = JSON.parse(text);
316
+ } catch {
317
+ nonJson = true;
318
+ }
319
+ }
320
+ const status = response.status;
321
+ const headers = response.headers as Record<string, string | undefined>;
322
+
323
+ if (status >= 400) {
324
+ const env = (nonJson ? undefined : errorEnvelope(json)) ?? {};
325
+ const message =
326
+ env.message ??
327
+ (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
328
+
329
+ // 1. Per-operation typed error (matcher metadata on the class).
330
+ const typed = matchTypedError(errorClasses, status, [
331
+ {
332
+ code: typeof env.code === "number" ? env.code : undefined,
333
+ message,
334
+ },
335
+ ]);
336
+ if (typed !== undefined) return yield* fail(typed);
337
+
338
+ // 2. Status-mapped class (retryAfter only stamps on retryable
339
+ // statuses — parseRetryAfterForStatus gates on that).
340
+ const StatusErrorClass = statusMap[status];
341
+ if (StatusErrorClass) {
342
+ return yield* fail(
343
+ new StatusErrorClass({
344
+ message,
345
+ retryAfter: parseRetryAfterForStatus(status, headers),
346
+ }),
347
+ );
348
+ }
349
+
350
+ // 3. Unmapped 5xx (e.g. proxy-specific statuses) → retryable server
351
+ // error rather than the unknown fallback.
352
+ if (status >= 500) {
353
+ return yield* fail(
354
+ new InternalServerError({
355
+ message,
356
+ retryAfter: parseRetryAfterForStatus(status, headers),
357
+ }),
358
+ );
359
+ }
360
+
361
+ // 4. Provider fallback.
362
+ return yield* fail(
363
+ options.unknownError({
364
+ status,
365
+ code: env.code,
366
+ message,
367
+ body: nonJson ? text : json,
368
+ headers,
369
+ }),
370
+ );
371
+ }
372
+
373
+ // 2xx: the response body IS the payload (no envelope). Wire→TS key
374
+ // mapping is schema-driven; `RawResponseRoot` responses are the body
375
+ // verbatim (mapKeys handles arrays/scalars structurally either way).
376
+ let body: unknown = nonJson ? text : (json ?? {});
377
+ if (options.transformResponse) body = options.transformResponse(body);
378
+ return wrapSensitive(outputAst, mapKeys(outputAst, body, "decode"));
379
+ });
380
+
381
+ return Layer.succeed(
382
+ API.Protocol,
383
+ API.Protocol.of({
384
+ // Erase encode's credentials requirement (resolved on the calling
385
+ // fiber; see RestProtocolOptions.credentials).
386
+ encode: (args) =>
387
+ encode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
388
+ decode,
389
+ }),
390
+ );
391
+ };
package/src/retry.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  * for that SDK without wrapping every call with `Effect.retry`.
9
9
  */
10
10
  import * as Config from "effect/Config";
11
- import * as ConfigProvider from "effect/ConfigProvider";
12
11
  import * as Context from "effect/Context";
13
12
  import * as Duration from "effect/Duration";
14
13
  import * as Effect from "effect/Effect";
@@ -84,9 +83,7 @@ export const jittered = Schedule.addDelay(() =>
84
83
  */
85
84
  export const capped = (max: Duration.Duration) =>
86
85
  Schedule.modifyDelay(({ duration }) =>
87
- Effect.succeed(
88
- Duration.isGreaterThan(duration, max) ? Duration.millis(5000) : duration,
89
- ),
86
+ Effect.succeed(Duration.isGreaterThan(duration, max) ? max : duration),
90
87
  );
91
88
 
92
89
  // ============================================================================
@@ -124,7 +121,7 @@ export const ServerRetryHintCapMs = Context.Service<number>(
124
121
  export const serverRetryHintCapLayer = (capMs: number) =>
125
122
  Layer.succeed(ServerRetryHintCapMs, capMs);
126
123
 
127
- const serverRetryHintCapMsConfig: Config.Config<number> = Config.string(
124
+ const serverRetryHintCapMsConfig: Config.Config<number> = Config.String(
128
125
  ENV_SERVER_RETRY_HINT_CAP_MS,
129
126
  ).pipe(
130
127
  Config.map((raw) => {
@@ -148,12 +145,6 @@ export const readServerRetryHintCapMsFromEnv = (): number =>
148
145
  Effect.runSync(
149
146
  serverRetryHintCapMsConfig.pipe(
150
147
  Effect.orElseSucceed(() => DEFAULT_SERVER_RETRY_HINT_CAP_MS),
151
- // The default ConfigProvider caches env reads; use a fresh provider so
152
- // each call observes the current process.env value.
153
- Effect.provideService(
154
- ConfigProvider.ConfigProvider,
155
- ConfigProvider.fromEnv(),
156
- ),
157
148
  ),
158
149
  );
159
150
 
@@ -166,10 +157,6 @@ const resolveServerRetryHintCapMs = (): Effect.Effect<number, never, never> =>
166
157
  }
167
158
  return yield* serverRetryHintCapMsConfig.pipe(
168
159
  Effect.orElseSucceed(() => DEFAULT_SERVER_RETRY_HINT_CAP_MS),
169
- Effect.provideService(
170
- ConfigProvider.ConfigProvider,
171
- ConfigProvider.fromEnv(),
172
- ),
173
160
  );
174
161
  });
175
162
 
@@ -223,25 +210,37 @@ const honorServerHint = (
223
210
  * - Honors `error.retryAfter` (server-provided hint) with precedence, capped
224
211
  * by {@link DEFAULT_SERVER_RETRY_HINT_CAP_MS} by default; override with
225
212
  * `DISTILLED_SERVER_RETRY_HINT_CAP_MS` or {@link ServerRetryHintCapMs}
226
- * - Otherwise uses exponential backoff starting at 100ms with a factor of 2
213
+ * - Otherwise uses exponential backoff starting at 250ms with a factor of 2,
214
+ * capped at 5s per delay
227
215
  * - Ensures at least 500ms delay for throttling errors
228
- * - Limits to 5 retry attempts
216
+ * - Limits to 8 retry attempts
229
217
  * - Applies jitter to avoid thundering herd
218
+ *
219
+ * The 250ms/5s-cap/8-attempt shape (~20s of total patience) comes from
220
+ * alchemy's Cloudflare provider suite, where transient auth blips under
221
+ * high request concurrency routinely outlast a short 5-attempt/3s policy
222
+ * while a genuinely broken credential still fails within seconds.
230
223
  */
231
224
  export const makeDefault: Factory = (lastError) => ({
232
225
  while: (error) => isTransientError(error),
226
+ // The 5s cap applies to the exponential backoff BEFORE the server hint is
227
+ // considered, so a server-provided retryAfter longer than 5s is honored in
228
+ // full (bounded only by the 60s hint cap inside honorServerHint). Capping
229
+ // after the hint would silently clamp e.g. a Retry-After: 30 to 5s.
233
230
  schedule: Schedule.max([
234
231
  pipe(
235
- Schedule.exponential(100, 2),
232
+ Schedule.exponential(250, 2),
233
+ capped(Duration.seconds(5)),
236
234
  honorServerHint(lastError, (duration, error) => {
237
235
  if (isThrottling(error) && Duration.toMillis(duration) < 500) {
238
236
  return Duration.millis(500);
239
237
  }
240
238
  return duration;
241
239
  }),
240
+ jittered,
242
241
  ),
243
- Schedule.recurs(5),
244
- ]).pipe(jittered),
242
+ Schedule.recurs(8),
243
+ ]),
245
244
  });
246
245
 
247
246
  /**
@@ -252,8 +251,8 @@ export const throttlingFactory: Factory = (lastError) => ({
252
251
  while: (error) => isThrottling(error),
253
252
  schedule: pipe(
254
253
  Schedule.exponential(1000, 2),
255
- honorServerHint(lastError),
256
254
  capped(Duration.seconds(5)),
255
+ honorServerHint(lastError),
257
256
  jittered,
258
257
  ),
259
258
  });
@@ -282,8 +281,8 @@ export const transientFactory: Factory = (lastError) => ({
282
281
  while: isTransientError,
283
282
  schedule: pipe(
284
283
  Schedule.exponential(1000, 2),
285
- honorServerHint(lastError),
286
284
  capped(Duration.seconds(5)),
285
+ honorServerHint(lastError),
287
286
  jittered,
288
287
  ),
289
288
  });
package/src/schema.ts CHANGED
@@ -2,16 +2,15 @@
2
2
  * `any`-collapsing re-export of `effect/Schema` for generated service files.
3
3
  *
4
4
  * This is the single, shared definition consumed by every code-generated
5
- * provider package (AWS, Azure, Cloudflare, GCP, Kubernetes, …) via
6
- * `@distilled.cloud/core/schema`. Do not copy it into individual packages.
5
+ * provider package via `@distilled.cloud/core/schema`. Do not copy it into
6
+ * individual packages.
7
7
  *
8
8
  * Every real TYPE (`Schema`, `Codec`, `Top`, …) is re-exported untouched, so
9
- * the `Schema.Schema<Foo>` / `Schema.Codec<Foo>` annotations on generated
10
- * consts (and the public `.d.ts`) stay precise. The schema *construction
11
- * surface* (`Struct`, `optional`, `suspend`, …) and the leaf scalar schemas are
12
- * retyped to `any`, so the compiler instantiates none of the heavy Schema
13
- * generics while building a service file — the explicit annotations carry the
14
- * real types.
9
+ * the `Schema.Schema<Foo>` annotations on generated consts (and the public
10
+ * `.d.ts`) stay precise. The schema *construction surface* (`Struct`,
11
+ * `optional`, `suspend`, …) and the leaf scalar schemas are retyped to `any`,
12
+ * so the compiler instantiates none of the heavy Schema generics while
13
+ * building a service file — the explicit annotations carry the real types.
15
14
  *
16
15
  * Overriding a *value* export never affects the same-named *type* export (they
17
16
  * live in separate namespaces and continue to flow through `export *`), so
@@ -24,7 +23,7 @@ export * from "effect/Schema";
24
23
 
25
24
  import * as S from "effect/Schema";
26
25
 
27
- type AnyFn = (...args: any[]) => any;
26
+ export type AnyFn = (...args: any[]) => any;
28
27
 
29
28
  // Construction surface — collapse to `any` so generics are never instantiated.
30
29
  export const optional: AnyFn = S.optional as AnyFn;
@@ -49,6 +48,6 @@ export const Null: any = S.Null;
49
48
  export const Void: any = S.Void;
50
49
  export const Any: any = S.Any;
51
50
 
52
- // NOTE: `Schema` / `Codec` (types) and `TaggedErrorClass` are intentionally NOT
51
+ // NOTE: `Schema` / `Codec` (types) and `TaggedError` are intentionally NOT
53
52
  // overridden — they flow through `export *` with real types so cast targets and
54
53
  // typed error classes stay precise.