@distilled.cloud/stripe 1.0.0-rc.14 → 1.0.0-rc.16

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/src/protocol.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as API from "@distilled.cloud/core/api";
2
+ import { debugHttp } from "@distilled.cloud/core/debug";
2
3
  import {
3
4
  ConfigError,
4
5
  HTTP_STATUS_MAP,
@@ -16,23 +17,30 @@ import {
16
17
  getAnn,
17
18
  getProps,
18
19
  hasPropAnn,
20
+ makeRequest,
19
21
  mapKeys,
22
+ mapsVerbatim,
20
23
  matchTypedError,
21
24
  nameOf,
22
25
  } from "@distilled.cloud/core/protocol-http";
23
- import { unwrapRedactedDeep, wrapSensitive } from "@distilled.cloud/core/protocol-rest";
24
- import { validateResponse } from "@distilled.cloud/core/response-validation";
26
+ import {
27
+ hasSensitive,
28
+ hasSensitiveMember,
29
+ unwrapRedactedDeep,
30
+ wrapSensitive,
31
+ } from "@distilled.cloud/core/protocol-rest";
32
+ import { checkResponse, withholdActual } from "@distilled.cloud/core/response-validation";
25
33
  import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
26
34
  /**
27
- * StripeProtocol — hand-written.
35
+ * StripeProtocol -- hand-written.
28
36
  *
29
37
  * Speaks Stripe's REST protocol. The generic trait helpers live in
30
38
  * `@distilled.cloud/core/protocol-http`; this module supplies what is
31
39
  * Stripe's own (ported from the distilled v0 stripe client):
32
40
  *
33
- * request: credentials → `Authorization: Bearer <apiKey>` + API base URL;
41
+ * request: credentials -> `Authorization: Bearer <apiKey>` + API base URL;
34
42
  * per-call {@link StripeRequestOptions} (resolved from the
35
- * calling fiber via {@link RequestOptions}) → Idempotency-Key /
43
+ * calling fiber via {@link RequestOptions}) -> Idempotency-Key /
36
44
  * Stripe-Account / Stripe-Context / Stripe-Version headers;
37
45
  * GET inputs flatten body-bound members into the query string
38
46
  * with Stripe bracket notation (`created[gte]`, `expand[]`);
@@ -43,26 +51,25 @@ import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
43
51
  * as JSON otherwise (the /v2 ops), and as multipart form-data
44
52
  * for `contentType: "multipart"` (POST /v1/files)
45
53
  *
46
- * response: 2xx JSON is the payload (no envelope) → wire→TS key mapping →
54
+ * response: 2xx JSON is the payload (no envelope) -> wire->TS key mapping ->
47
55
  * Redacted wrapping of sensitive members; failures parse the
48
- * `{ error: { type, code, message, … } }` envelope and dispatch
56
+ * `{ error: { type, code, message, ... } }` envelope and dispatch
49
57
  * by per-op matcher classes first (`matchTypedError`, e.g.
50
- * ProductHasPrices), then `error.type` (card_error → CardError,
51
- * idempotency_error → IdempotencyError, invalid_request_error →
52
- * InvalidRequestError, api_error → ApiError), then the
53
- * Stripe-specific status map (402 → PaymentError, 424 →
58
+ * ProductHasPrices), then `error.type` (card_error -> CardError,
59
+ * idempotency_error -> IdempotencyError, invalid_request_error ->
60
+ * InvalidRequestError, api_error -> ApiError), then the
61
+ * Stripe-specific status map (402 -> PaymentError, 424 ->
54
62
  * ExternalDependencyFailed), then core's HTTP status map (with
55
63
  * retryAfter hints), then UnknownStripeError.
56
64
  */
57
65
  import * as Context from "effect/Context";
58
66
  import * as Effect from "effect/Effect";
59
67
  import * as HttpBody from "effect/http/HttpBody";
60
- import type * as HttpClient from "effect/http/HttpClient";
68
+ import * as HttpClient from "effect/http/HttpClient";
61
69
  import type * as HttpClientError from "effect/http/HttpClientError";
62
70
  import * as HttpClientRequest from "effect/http/HttpClientRequest";
63
71
  import type * as HttpClientResponse from "effect/http/HttpClientResponse";
64
72
  import * as Layer from "effect/Layer";
65
- import * as Option from "effect/Option";
66
73
  import * as Redacted from "effect/Redacted";
67
74
  import type * as AST from "effect/SchemaAST";
68
75
  import { Credentials, type Config } from "./credentials.ts";
@@ -108,11 +115,12 @@ export type StripeRequestOptions = {
108
115
  /**
109
116
  * Per-call request options service. distilled v0 passed these as a second
110
117
  * call argument; v1 operations take only their input, so options travel on
111
- * the calling fiber's context instead — see {@link withRequestOptions}.
118
+ * the calling fiber's context instead -- see {@link withRequestOptions}.
112
119
  */
113
- export class RequestOptions extends Context.Service<RequestOptions, StripeRequestOptions>()(
114
- "StripeRequestOptions",
115
- ) {}
120
+ export class RequestOptions
121
+ extends /*@__PURE__*/ Context.Service<RequestOptions, StripeRequestOptions>()(
122
+ "StripeRequestOptions",
123
+ ) {}
116
124
 
117
125
  /**
118
126
  * Provide {@link StripeRequestOptions} to every Stripe API call below it.
@@ -188,7 +196,7 @@ const fail = (e: unknown): Effect.Effect<never> => Effect.fail(e) as Effect.Effe
188
196
  // Stripe query / form encoding (ported from distilled v0)
189
197
  // =============================================================================
190
198
 
191
- const BODYLESS = new Set(["GET", "HEAD"]);
199
+ const BODYLESS = /*@__PURE__*/ new Set(["GET", "HEAD"]);
192
200
 
193
201
  const appendQueryValue = (query: URLSearchParams, key: string, value: unknown): void => {
194
202
  if (value === undefined || value === null) return;
@@ -274,108 +282,183 @@ const buildFormUrlEncoded = (body: Record<string, unknown>): string => {
274
282
  // Request building
275
283
  // =============================================================================
276
284
 
277
- // The protocol layer is memoized per process by `API.make`, so the build
278
- // must not capture credentials — `encode` resolves Credentials (and the
279
- // per-call RequestOptions) from the calling fiber's context on every
280
- // request. The requirement is erased at this boundary and reintroduced for
281
- // callers by the generated `StripeOpContext` annotations.
282
- const encode = ({ input, inputAst }: { readonly input: unknown; readonly inputAst: AST.AST }) =>
283
- Effect.gen(function* () {
284
- const resolveCredentials = yield* Credentials;
285
- const creds = yield* resolveCredentials as Effect.Effect<Config>;
286
- const options = Option.getOrUndefined(yield* Effect.serviceOption(RequestOptions));
287
-
288
- const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
289
- if (!http) {
290
- throw new Error("operation input is missing the Http() trait");
285
+ /** One input member, with its binding read from the schema once. */
286
+ interface MemberPlan {
287
+ readonly key: string;
288
+ readonly bind: "label" | "header" | "query" | "body";
289
+ /** Label token, lower-cased header name, query name or body wire name. */
290
+ readonly name: string;
291
+ readonly type: AST.AST;
292
+ /** `mapKeys(type, value, "encode")` returns the value unchanged. */
293
+ readonly verbatim: boolean;
294
+ }
295
+
296
+ interface RequestPlan {
297
+ readonly http: HttpTrait;
298
+ readonly members: ReadonlyArray<MemberPlan>;
299
+ readonly known: ReadonlySet<string>;
300
+ }
301
+
302
+ const requestPlans = /*@__PURE__*/ new WeakMap<AST.AST, RequestPlan>();
303
+ const requestPlanFor = (inputAst: AST.AST): RequestPlan => {
304
+ let plan = requestPlans.get(inputAst);
305
+ if (plan !== undefined) return plan;
306
+ const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
307
+ if (!http) {
308
+ throw new Error("operation input is missing the Http() trait");
309
+ }
310
+ const members = getProps(inputAst).map((prop): MemberPlan => {
311
+ const key = String(prop.name);
312
+ if (hasPropAnn(prop, labelSymbol)) {
313
+ return {
314
+ key,
315
+ bind: "label",
316
+ name: nameOf(prop, labelSymbol),
317
+ type: prop.type,
318
+ verbatim: true,
319
+ };
291
320
  }
292
-
293
- const baseHeaders: Record<string, string> = {
294
- Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
295
- ...stripeRequestHeaders(options),
296
- Accept: "application/json",
321
+ if (hasPropAnn(prop, headerSymbol)) {
322
+ const name = nameOf(prop, headerSymbol).toLowerCase();
323
+ return { key, bind: "header", name, type: prop.type, verbatim: true };
324
+ }
325
+ if (hasPropAnn(prop, querySymbol)) {
326
+ return {
327
+ key,
328
+ bind: "query",
329
+ name: nameOf(prop, querySymbol),
330
+ type: prop.type,
331
+ verbatim: true,
332
+ };
333
+ }
334
+ return {
335
+ key,
336
+ bind: "body",
337
+ name: nameOf(prop, bodySymbol),
338
+ type: prop.type,
339
+ verbatim: mapsVerbatim(prop.type, "encode"),
297
340
  };
341
+ });
342
+ plan = { http, members, known: new Set(members.map((m) => m.key)) };
343
+ requestPlans.set(inputAst, plan);
344
+ return plan;
345
+ };
298
346
 
299
- // Multipart (POST /v1/files): core's buildRequest speaks multipart
300
- // form-data (file parts + JSON-encoded object parts) — same behavior as
301
- // v0's buildFormData.
302
- if (http.contentType === "multipart") {
303
- return buildRequest({
304
- input: unwrapRedactedDeep(input),
305
- inputAst,
306
- baseUrl: creds.apiBaseUrl,
307
- headers: baseHeaders,
308
- });
309
- }
347
+ const requestFor = (
348
+ creds: Config,
349
+ options: StripeRequestOptions | undefined,
350
+ input: unknown,
351
+ inputAst: AST.AST,
352
+ ): HttpClientRequest.HttpClientRequest => {
353
+ const { http, members, known } = requestPlanFor(inputAst);
354
+
355
+ const baseHeaders: Record<string, string> = {
356
+ Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
357
+ ...stripeRequestHeaders(options),
358
+ Accept: "application/json",
359
+ };
310
360
 
311
- const inputObj = (unwrapRedactedDeep(input) ?? {}) as Record<string, unknown>;
312
- const headers: Record<string, string> = { ...baseHeaders };
313
- const body: Record<string, unknown> = {};
314
- const query = new URLSearchParams();
315
- let uri = http.uri;
316
- const consumed = new Set<string>();
317
-
318
- for (const prop of getProps(inputAst)) {
319
- const key = String(prop.name);
320
- consumed.add(key);
321
- const value = inputObj[key];
322
- if (value === undefined) continue;
323
-
324
- if (hasPropAnn(prop, labelSymbol)) {
325
- const token = nameOf(prop, labelSymbol);
326
- uri = uri.replace(`{${token}}`, encodeURIComponent(String(value)));
327
- } else if (hasPropAnn(prop, headerSymbol)) {
328
- headers[nameOf(prop, headerSymbol).toLowerCase()] = String(value);
329
- } else if (hasPropAnn(prop, querySymbol)) {
361
+ // Multipart (POST /v1/files): core's buildRequest speaks multipart
362
+ // form-data (file parts + JSON-encoded object parts) -- same behavior as
363
+ // v0's buildFormData.
364
+ if (http.contentType === "multipart") {
365
+ return buildRequest({
366
+ input: unwrapRedactedDeep(input),
367
+ inputAst,
368
+ baseUrl: creds.apiBaseUrl,
369
+ headers: baseHeaders,
370
+ });
371
+ }
372
+
373
+ const inputObj = (unwrapRedactedDeep(input) ?? {}) as Record<string, unknown>;
374
+ const headers = baseHeaders;
375
+ const body: Record<string, unknown> = {};
376
+ let bodyKeys = 0;
377
+ let query: URLSearchParams | undefined;
378
+ let uri = http.uri;
379
+
380
+ for (let i = 0; i < members.length; i++) {
381
+ const m = members[i]!;
382
+ const value = inputObj[m.key];
383
+ if (value === undefined) continue;
384
+ switch (m.bind) {
385
+ case "label":
386
+ uri = uri.replace(`{${m.name}}`, encodeURIComponent(String(value)));
387
+ break;
388
+ case "header":
389
+ headers[m.name] = String(value);
390
+ break;
391
+ case "query":
330
392
  // Stripe bracket expansion, not dotted params (`created[gte]=1`).
331
- appendStripeQuery(query, nameOf(prop, querySymbol), value);
332
- } else {
333
- body[nameOf(prop, bodySymbol)] = mapKeys(prop.type, value, "encode");
334
- }
393
+ appendStripeQuery((query ??= new URLSearchParams()), m.name, value);
394
+ break;
395
+ default:
396
+ body[m.name] = m.verbatim ? value : mapKeys(m.type, value, "encode");
397
+ bodyKeys++;
335
398
  }
399
+ }
336
400
 
337
- // Input keys the schema doesn't know pass through as body fields — the
338
- // generated schemas can lag the real API, and silently dropping them
339
- // would break working callers.
340
- for (const [key, value] of Object.entries(inputObj)) {
341
- if (consumed.has(key) || value === undefined) continue;
342
- body[key] = value;
343
- }
401
+ // Input keys the schema doesn't know pass through as body fields -- the
402
+ // generated schemas can lag the real API, and silently dropping them
403
+ // would break working callers.
404
+ for (const key in inputObj) {
405
+ if (known.has(key) || !Object.hasOwn(inputObj, key)) continue;
406
+ const value = inputObj[key];
407
+ if (value === undefined) continue;
408
+ body[key] = value;
409
+ bodyKeys++;
410
+ }
344
411
 
345
- // v0's normalizeStripeGetQuery: GET inputs carry no body — flatten the
346
- // body-bound members into the query string with bracket notation.
347
- if (BODYLESS.has(http.method)) {
348
- for (const [key, value] of Object.entries(body)) {
349
- appendStripeQuery(query, key, value);
350
- }
412
+ // v0's normalizeStripeGetQuery: GET inputs carry no body -- flatten the
413
+ // body-bound members into the query string with bracket notation.
414
+ const bodyless = BODYLESS.has(http.method);
415
+ if (bodyless && bodyKeys > 0) {
416
+ for (const [key, value] of Object.entries(body)) {
417
+ appendStripeQuery((query ??= new URLSearchParams()), key, value);
351
418
  }
419
+ }
352
420
 
353
- const qs = query.toString();
354
- const url = `${creds.apiBaseUrl}${uri}${qs ? `?${qs}` : ""}`;
355
- if (process.env.DISTILLED_DEBUG_HTTP) {
356
- console.error(
357
- `[distilled] ${http.method} ${url}` +
358
- (Object.keys(body).length && !BODYLESS.has(http.method)
359
- ? ` body=${JSON.stringify(body).slice(0, 400)}`
360
- : ""),
361
- );
362
- }
421
+ const qs = query === undefined ? "" : query.toString();
422
+ const url = qs ? `${creds.apiBaseUrl}${uri}?${qs}` : `${creds.apiBaseUrl}${uri}`;
423
+ if (debugHttp()) {
424
+ console.error(
425
+ `[distilled] ${http.method} ${url}` +
426
+ (bodyKeys && !bodyless ? ` body=${JSON.stringify(body).slice(0, 400)}` : ""),
427
+ );
428
+ }
429
+
430
+ const request = makeRequest(http.method, url, headers);
431
+ if (bodyless || bodyKeys === 0) return request;
432
+ return HttpClientRequest.setBody(
433
+ request,
434
+ http.contentType === "form-urlencoded"
435
+ ? HttpBody.text(buildFormUrlEncoded(body), "application/x-www-form-urlencoded")
436
+ : // The /v2/* operations are plain JSON.
437
+ HttpBody.jsonUnsafe(body),
438
+ );
439
+ };
363
440
 
364
- let request = HttpClientRequest.make(http.method)(url).pipe(
365
- HttpClientRequest.setHeaders(headers),
441
+ /**
442
+ * The calling fiber's credentials, then `f` on them. The protocol layer is
443
+ * memoized per process by `API.make`, so credentials (and the per-call
444
+ * RequestOptions) are resolved from the calling fiber's context on every
445
+ * request. The requirement is erased at this boundary and reintroduced for
446
+ * callers by the generated `StripeOpContext` annotations.
447
+ */
448
+ const withCredentials = <B>(
449
+ context: Context.Context<any>,
450
+ f: (creds: Config) => Effect.Effect<B, any, any>,
451
+ ): Effect.Effect<B, any, any> =>
452
+ API.withValue(Context.get(context, Credentials) as Effect.Effect<Config, any, any>, f);
453
+
454
+ const encode = ({ input, inputAst }: { readonly input: unknown; readonly inputAst: AST.AST }) =>
455
+ Effect.withFiber((fiber) => {
456
+ const context = fiber.context as Context.Context<any>;
457
+ return withCredentials(context, (creds) =>
458
+ Effect.succeed(
459
+ requestFor(creds, Context.getOrUndefined(context, RequestOptions), input, inputAst),
460
+ ),
366
461
  );
367
- if (!BODYLESS.has(http.method) && Object.keys(body).length > 0) {
368
- request =
369
- http.contentType === "form-urlencoded"
370
- ? request.pipe(
371
- HttpClientRequest.setBody(
372
- HttpBody.text(buildFormUrlEncoded(body), "application/x-www-form-urlencoded"),
373
- ),
374
- )
375
- : // The /v2/* operations are plain JSON.
376
- request.pipe(HttpClientRequest.bodyJsonUnsafe(body));
377
- }
378
- return request;
379
462
  });
380
463
 
381
464
  // =============================================================================
@@ -391,7 +474,7 @@ const stripeStatusMap: Readonly<Record<number, (new (args: any) => unknown) | un
391
474
  const coreStatusMap: Readonly<Record<number, (new (args: any) => unknown) | undefined>> =
392
475
  HTTP_STATUS_MAP;
393
476
 
394
- /** Stripe's `{ error: { type, message, … } }` object, when present. */
477
+ /** Stripe's `{ error: { type, message, ... } }` object, when present. */
395
478
  const stripeErrorEnvelope = (errorBody: unknown): Record<string, unknown> | undefined => {
396
479
  const envelope =
397
480
  errorBody !== null && typeof errorBody === "object"
@@ -411,7 +494,7 @@ const stripeErrorMessage = (errorBody: unknown): string | undefined => {
411
494
  * Match a Stripe API error response to the appropriate error class.
412
495
  *
413
496
  * Stripe dispatches errors by:
414
- * 1. Per-operation matcher classes (`matchTypedError`) — see decode
497
+ * 1. Per-operation matcher classes (`matchTypedError`) -- see decode
415
498
  * 2. The `error.type` field (card_error, idempotency_error,
416
499
  * invalid_request_error, api_error)
417
500
  * 3. HTTP status code (402/424 Stripe-specific, then the core status map)
@@ -470,7 +553,7 @@ const matchStripeError = (
470
553
  });
471
554
  }
472
555
 
473
- // Fall back to standard HTTP status errors (400, 401, 403, 404, …).
556
+ // Fall back to standard HTTP status errors (400, 401, 403, 404, ...).
474
557
  const CoreErrorClass = coreStatusMap[status];
475
558
  if (CoreErrorClass) {
476
559
  return new CoreErrorClass({
@@ -479,7 +562,7 @@ const matchStripeError = (
479
562
  } as any);
480
563
  }
481
564
 
482
- // Unmapped 5xx (e.g. proxy statuses) → retryable server error (v1
565
+ // Unmapped 5xx (e.g. proxy statuses) -> retryable server error (v1
483
566
  // convention; v0 fell through to UnknownStripeError here).
484
567
  if (status >= 500) {
485
568
  return new InternalServerError({
@@ -497,62 +580,95 @@ const matchStripeError = (
497
580
  });
498
581
  };
499
582
 
500
- const decode = ({
501
- response,
502
- outputAst,
503
- errors: errorClasses,
504
- }: {
505
- readonly response: HttpClientResponse.HttpClientResponse;
506
- readonly outputAst: AST.AST;
507
- readonly errors: ReadonlyArray<unknown>;
508
- }) =>
509
- Effect.gen(function* () {
510
- // Read as text and parse tolerantly — error pages can be non-JSON.
511
- const text = (yield* response.text.pipe(Effect.orDie)) ?? "";
512
- if (process.env.DISTILLED_DEBUG_HTTP) {
513
- console.error(`[distilled] <- ${response.status} ${text.slice(0, 400)}`);
514
- }
515
- let json: unknown;
516
- let nonJson = false;
517
- if (text.trim().length > 0) {
518
- try {
519
- json = JSON.parse(text);
520
- } catch {
521
- nonJson = true;
583
+ const decodeResponse = (
584
+ context: Context.Context<any>,
585
+ response: HttpClientResponse.HttpClientResponse,
586
+ outputAst: AST.AST,
587
+ errorClasses: ReadonlyArray<unknown>,
588
+ ): Effect.Effect<unknown> =>
589
+ Effect.matchEffect(response.text, {
590
+ onFailure: Effect.die,
591
+ onSuccess: (read): Effect.Effect<unknown> => {
592
+ // Read as text and parse tolerantly -- error pages can be non-JSON.
593
+ const text = read ?? "";
594
+ if (debugHttp()) {
595
+ console.error(`[distilled] <- ${response.status} ${text.slice(0, 400)}`);
596
+ }
597
+ let json: unknown;
598
+ let nonJson = false;
599
+ if (text.trim().length > 0) {
600
+ try {
601
+ json = JSON.parse(text);
602
+ } catch {
603
+ nonJson = true;
604
+ }
605
+ }
606
+ const status = response.status;
607
+ const headers = response.headers as Record<string, string | undefined>;
608
+
609
+ if (status >= 400) {
610
+ const errorBody: unknown = nonJson ? text : json;
611
+ const message =
612
+ stripeErrorMessage(errorBody) ??
613
+ (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
614
+ const typed = matchTypedError(errorClasses, status, [{ message }], {
615
+ body: nonJson ? text : json,
616
+ headers,
617
+ });
618
+ if (typed !== undefined) return fail(typed);
619
+ return fail(matchStripeError(status, errorBody, headers));
522
620
  }
523
- }
524
- const status = response.status;
525
- const headers = response.headers as Record<string, string | undefined>;
526
-
527
- if (status >= 400) {
528
- const errorBody: unknown = nonJson ? text : json;
529
- const message =
530
- stripeErrorMessage(errorBody) ?? (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
531
- const typed = matchTypedError(errorClasses, status, [{ message }], {
532
- body: nonJson ? text : json,
533
- headers,
534
- });
535
- if (typed !== undefined) return yield* fail(typed);
536
- return yield* fail(matchStripeError(status, errorBody, headers));
537
- }
538
621
 
539
- // 2xx: the response body IS the payload (no envelope). Wire→TS key
540
- // mapping is schema-driven; sensitive members come back Redacted.
541
- // Strict mode checks the mapped body against the output schema first.
542
- const body: unknown = nonJson ? text : (json ?? {});
543
- const mapped = yield* validateResponse(
544
- outputAst,
545
- mapKeys(outputAst, body, "decode"),
546
- (cause) => new StripeParseError({ body: nonJson ? text : json, cause }),
547
- ).pipe(Effect.catch(fail));
548
- return wrapSensitive(outputAst, mapped);
622
+ // 2xx: the response body IS the payload (no envelope). Wire->TS key
623
+ // mapping is schema-driven; sensitive members come back Redacted.
624
+ // Strict mode checks the mapped body against the output schema first.
625
+ // For an output with sensitive members, a non-JSON body fails in every
626
+ // mode and parse errors withhold the body.
627
+ const withholdBody = hasSensitiveMember(outputAst);
628
+ if (nonJson && withholdBody) {
629
+ return fail(new StripeParseError({ body: "[REDACTED]", cause: "Invalid JSON response" }));
630
+ }
631
+ const body: unknown = nonJson ? text : (json ?? {});
632
+ const mapped = mapsVerbatim(outputAst, "decode") ? body : mapKeys(outputAst, body, "decode");
633
+ const cause = checkResponse(context, outputAst, mapped);
634
+ if (cause !== undefined) {
635
+ return fail(
636
+ new StripeParseError({
637
+ body: withholdBody ? "[REDACTED]" : nonJson ? text : json,
638
+ cause: withholdBody ? withholdActual(cause) : cause,
639
+ }),
640
+ );
641
+ }
642
+ return Effect.succeed(hasSensitive(outputAst) ? wrapSensitive(outputAst, mapped) : mapped);
643
+ },
549
644
  });
550
645
 
551
- export const StripeProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
646
+ const call = (op: API.PreparedOperation, input: unknown): Effect.Effect<unknown> =>
647
+ Effect.withFiber((fiber) => {
648
+ const context = fiber.context as Context.Context<any>;
649
+ return withCredentials(context, (creds) => {
650
+ const request = requestFor(
651
+ creds,
652
+ Context.getOrUndefined(context, RequestOptions),
653
+ input,
654
+ op.inputAst,
655
+ );
656
+ const client = Context.get(context, HttpClient.HttpClient);
657
+ return Effect.flatMap(client.execute(request), (response) =>
658
+ decodeResponse(context, response, op.outputAst, op.errors),
659
+ );
660
+ });
661
+ }) as Effect.Effect<unknown>;
662
+
663
+ export const StripeProtocol: Layer.Layer<API.Protocol> = /*@__PURE__*/ Layer.succeed(
552
664
  API.Protocol,
553
- API.Protocol.of({
665
+ /*@__PURE__*/ API.Protocol.of({
554
666
  // Erase encode's Credentials requirement (see comment above).
555
667
  encode: (args) => encode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
556
- decode,
668
+ decode: ({ response, outputAst, errors }) =>
669
+ Effect.withFiber((fiber) =>
670
+ decodeResponse(fiber.context as Context.Context<any>, response, outputAst, errors),
671
+ ),
672
+ call,
557
673
  }),
558
674
  );
package/src/retry.ts CHANGED
@@ -32,17 +32,19 @@ export {
32
32
  } from "@distilled.cloud/core/retry";
33
33
 
34
34
  /** Context tag for configuring retry behavior of Stripe API calls. */
35
- export class Retry extends Context.Service<Retry, Policy>()("StripeRetry") {}
35
+ export class Retry extends /*@__PURE__*/ Context.Service<Retry, Policy>()("StripeRetry") {}
36
36
 
37
37
  /** Provides a custom retry policy to every Stripe API call below it. */
38
38
  export const policy = (optionsOrFactory: Policy) =>
39
39
  Effect.provide(Layer.succeed(Retry, optionsOrFactory));
40
40
 
41
41
  /** Disables all automatic retries. */
42
- export const none = Effect.provide(Layer.succeed(Retry, { while: () => false }));
42
+ export const none = /*@__PURE__*/ Effect.provide(
43
+ /*@__PURE__*/ Layer.succeed(Retry, { while: () => false }),
44
+ );
43
45
 
44
46
  /** Apply the throttling retry policy (retries throttling errors indefinitely). */
45
- export const throttling = policy(throttlingFactory);
47
+ export const throttling = /*@__PURE__*/ policy(throttlingFactory);
46
48
 
47
49
  /** Apply the transient retry policy (retries all transient errors indefinitely). */
48
- export const transient = policy(transientFactory);
50
+ export const transient = /*@__PURE__*/ policy(transientFactory);