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

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,3 +1,28 @@
1
+ import * as API from "@distilled.cloud/core/api";
2
+ import {
3
+ ConfigError,
4
+ HTTP_STATUS_MAP,
5
+ InternalServerError,
6
+ type DefaultErrors as CoreDefaultErrors,
7
+ BadRequest,
8
+ Conflict,
9
+ Forbidden,
10
+ Locked,
11
+ NotFound,
12
+ UnprocessableEntity,
13
+ } from "@distilled.cloud/core/errors";
14
+ import {
15
+ buildRequest,
16
+ getAnn,
17
+ getProps,
18
+ hasPropAnn,
19
+ mapKeys,
20
+ matchTypedError,
21
+ nameOf,
22
+ } 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";
25
+ import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
1
26
  /**
2
27
  * StripeProtocol — hand-written.
3
28
  *
@@ -31,42 +56,15 @@
31
56
  */
32
57
  import * as Context from "effect/Context";
33
58
  import * as Effect from "effect/Effect";
59
+ import * as HttpBody from "effect/http/HttpBody";
60
+ import type * as HttpClient from "effect/http/HttpClient";
61
+ import type * as HttpClientError from "effect/http/HttpClientError";
62
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
63
+ import type * as HttpClientResponse from "effect/http/HttpClientResponse";
34
64
  import * as Layer from "effect/Layer";
35
65
  import * as Option from "effect/Option";
36
66
  import * as Redacted from "effect/Redacted";
37
67
  import type * as AST from "effect/SchemaAST";
38
- import type * as HttpClient from "effect/unstable/http/HttpClient";
39
- import type * as HttpClientError from "effect/unstable/http/HttpClientError";
40
- import * as HttpBody from "effect/unstable/http/HttpBody";
41
- import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
42
- import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
43
- import * as API from "@distilled.cloud/core/api";
44
- import {
45
- buildRequest,
46
- getAnn,
47
- getProps,
48
- hasPropAnn,
49
- mapKeys,
50
- matchTypedError,
51
- nameOf,
52
- } from "@distilled.cloud/core/protocol-http";
53
- import {
54
- unwrapRedactedDeep,
55
- wrapSensitive,
56
- } from "@distilled.cloud/core/protocol-rest";
57
- import {
58
- ConfigError,
59
- HTTP_STATUS_MAP,
60
- InternalServerError,
61
- type DefaultErrors as CoreDefaultErrors,
62
- BadRequest,
63
- Conflict,
64
- Forbidden,
65
- Locked,
66
- NotFound,
67
- UnprocessableEntity,
68
- } from "@distilled.cloud/core/errors";
69
- import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
70
68
  import { Credentials, type Config } from "./credentials.ts";
71
69
  import {
72
70
  ApiError,
@@ -112,10 +110,9 @@ export type StripeRequestOptions = {
112
110
  * call argument; v1 operations take only their input, so options travel on
113
111
  * the calling fiber's context instead — see {@link withRequestOptions}.
114
112
  */
115
- export class RequestOptions extends Context.Service<
116
- RequestOptions,
117
- StripeRequestOptions
118
- >()("StripeRequestOptions") {}
113
+ export class RequestOptions extends Context.Service<RequestOptions, StripeRequestOptions>()(
114
+ "StripeRequestOptions",
115
+ ) {}
119
116
 
120
117
  /**
121
118
  * Provide {@link StripeRequestOptions} to every Stripe API call below it.
@@ -185,8 +182,7 @@ export type StripeOpContext = Credentials | HttpClient.HttpClient;
185
182
  // Stripe failures are real typed errors that an operation re-surfaces via its
186
183
  // `errors: [...]` list / StripeOpError annotation. Fail with the instance and
187
184
  // erase the error type here; the generated annotations reintroduce it.
188
- const fail = (e: unknown): Effect.Effect<never> =>
189
- Effect.fail(e) as Effect.Effect<never>;
185
+ const fail = (e: unknown): Effect.Effect<never> => Effect.fail(e) as Effect.Effect<never>;
190
186
 
191
187
  // =============================================================================
192
188
  // Stripe query / form encoding (ported from distilled v0)
@@ -194,36 +190,23 @@ const fail = (e: unknown): Effect.Effect<never> =>
194
190
 
195
191
  const BODYLESS = new Set(["GET", "HEAD"]);
196
192
 
197
- const appendQueryValue = (
198
- query: URLSearchParams,
199
- key: string,
200
- value: unknown,
201
- ): void => {
193
+ const appendQueryValue = (query: URLSearchParams, key: string, value: unknown): void => {
202
194
  if (value === undefined || value === null) return;
203
- query.append(
204
- key,
205
- typeof value === "boolean" ? (value ? "true" : "false") : String(value),
206
- );
195
+ query.append(key, typeof value === "boolean" ? (value ? "true" : "false") : String(value));
207
196
  };
208
197
 
209
198
  /**
210
199
  * Stripe GET-query bracket expansion (v0's `appendStripeQuery`): arrays as
211
200
  * repeated `key[]` entries, nested objects as `key[nested]`, recursively.
212
201
  */
213
- const appendStripeQuery = (
214
- query: URLSearchParams,
215
- key: string,
216
- value: unknown,
217
- ): void => {
202
+ const appendStripeQuery = (query: URLSearchParams, key: string, value: unknown): void => {
218
203
  if (value === undefined || value === null) return;
219
204
  if (Array.isArray(value)) {
220
205
  for (const item of value) appendQueryValue(query, `${key}[]`, item);
221
206
  return;
222
207
  }
223
208
  if (typeof value === "object" && !(value instanceof Date)) {
224
- for (const [nestedKey, nestedValue] of Object.entries(
225
- value as Record<string, unknown>,
226
- )) {
209
+ for (const [nestedKey, nestedValue] of Object.entries(value as Record<string, unknown>)) {
227
210
  appendStripeQuery(query, `${key}[${nestedKey}]`, nestedValue);
228
211
  }
229
212
  return;
@@ -261,20 +244,13 @@ const flattenToFormPairs = (
261
244
  typeof item === "object" &&
262
245
  !Array.isArray(item)
263
246
  ) {
264
- pairs.push(
265
- ...flattenToFormPairs(
266
- item as Record<string, unknown>,
267
- `${fullKey}[${i}]`,
268
- ),
269
- );
247
+ pairs.push(...flattenToFormPairs(item as Record<string, unknown>, `${fullKey}[${i}]`));
270
248
  } else if (item !== undefined && item !== null) {
271
249
  pairs.push([`${fullKey}[${i}]`, String(item)]);
272
250
  }
273
251
  }
274
252
  } else if (typeof value === "object") {
275
- pairs.push(
276
- ...flattenToFormPairs(value as Record<string, unknown>, fullKey),
277
- );
253
+ pairs.push(...flattenToFormPairs(value as Record<string, unknown>, fullKey));
278
254
  } else if (typeof value === "boolean") {
279
255
  pairs.push([fullKey, value ? "true" : "false"]);
280
256
  } else {
@@ -303,19 +279,11 @@ const buildFormUrlEncoded = (body: Record<string, unknown>): string => {
303
279
  // per-call RequestOptions) from the calling fiber's context on every
304
280
  // request. The requirement is erased at this boundary and reintroduced for
305
281
  // callers by the generated `StripeOpContext` annotations.
306
- const encode = ({
307
- input,
308
- inputAst,
309
- }: {
310
- readonly input: unknown;
311
- readonly inputAst: AST.AST;
312
- }) =>
282
+ const encode = ({ input, inputAst }: { readonly input: unknown; readonly inputAst: AST.AST }) =>
313
283
  Effect.gen(function* () {
314
284
  const resolveCredentials = yield* Credentials;
315
285
  const creds = yield* resolveCredentials as Effect.Effect<Config>;
316
- const options = Option.getOrUndefined(
317
- yield* Effect.serviceOption(RequestOptions),
318
- );
286
+ const options = Option.getOrUndefined(yield* Effect.serviceOption(RequestOptions));
319
287
 
320
288
  const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
321
289
  if (!http) {
@@ -340,10 +308,7 @@ const encode = ({
340
308
  });
341
309
  }
342
310
 
343
- const inputObj = (unwrapRedactedDeep(input) ?? {}) as Record<
344
- string,
345
- unknown
346
- >;
311
+ const inputObj = (unwrapRedactedDeep(input) ?? {}) as Record<string, unknown>;
347
312
  const headers: Record<string, string> = { ...baseHeaders };
348
313
  const body: Record<string, unknown> = {};
349
314
  const query = new URLSearchParams();
@@ -404,10 +369,7 @@ const encode = ({
404
369
  http.contentType === "form-urlencoded"
405
370
  ? request.pipe(
406
371
  HttpClientRequest.setBody(
407
- HttpBody.text(
408
- buildFormUrlEncoded(body),
409
- "application/x-www-form-urlencoded",
410
- ),
372
+ HttpBody.text(buildFormUrlEncoded(body), "application/x-www-form-urlencoded"),
411
373
  ),
412
374
  )
413
375
  : // The /v2/* operations are plain JSON.
@@ -420,22 +382,17 @@ const encode = ({
420
382
  // Response decoding / error matching (ported from v0's matchError)
421
383
  // =============================================================================
422
384
 
423
- const str = (v: unknown): string | undefined =>
424
- typeof v === "string" ? v : undefined;
385
+ const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
425
386
 
426
387
  // Structural views of the status maps for numeric lookup (the const objects'
427
388
  // per-status class types don't unify without widening the constructor).
428
- const stripeStatusMap: Readonly<
429
- Record<number, (new (args: any) => unknown) | undefined>
430
- > = STRIPE_HTTP_STATUS_MAP;
431
- const coreStatusMap: Readonly<
432
- Record<number, (new (args: any) => unknown) | undefined>
433
- > = HTTP_STATUS_MAP;
389
+ const stripeStatusMap: Readonly<Record<number, (new (args: any) => unknown) | undefined>> =
390
+ STRIPE_HTTP_STATUS_MAP;
391
+ const coreStatusMap: Readonly<Record<number, (new (args: any) => unknown) | undefined>> =
392
+ HTTP_STATUS_MAP;
434
393
 
435
394
  /** Stripe's `{ error: { type, message, … } }` object, when present. */
436
- const stripeErrorEnvelope = (
437
- errorBody: unknown,
438
- ): Record<string, unknown> | undefined => {
395
+ const stripeErrorEnvelope = (errorBody: unknown): Record<string, unknown> | undefined => {
439
396
  const envelope =
440
397
  errorBody !== null && typeof errorBody === "object"
441
398
  ? (errorBody as Record<string, unknown>).error
@@ -505,15 +462,11 @@ const matchStripeError = (
505
462
  return new StripeErrorClass({
506
463
  message: common.message,
507
464
  code: common.code,
508
- ...(str(err.decline_code) !== undefined
509
- ? { decline_code: str(err.decline_code) }
510
- : {}),
465
+ ...(str(err.decline_code) !== undefined ? { decline_code: str(err.decline_code) } : {}),
511
466
  ...(str(err.charge) !== undefined ? { charge: str(err.charge) } : {}),
512
467
  ...(str(err.param) !== undefined ? { param: str(err.param) } : {}),
513
468
  ...(common.doc_url !== undefined ? { doc_url: common.doc_url } : {}),
514
- ...(common.request_log_url !== undefined
515
- ? { request_log_url: common.request_log_url }
516
- : {}),
469
+ ...(common.request_log_url !== undefined ? { request_log_url: common.request_log_url } : {}),
517
470
  });
518
471
  }
519
472
 
@@ -574,25 +527,32 @@ const decode = ({
574
527
  if (status >= 400) {
575
528
  const errorBody: unknown = nonJson ? text : json;
576
529
  const message =
577
- stripeErrorMessage(errorBody) ??
578
- (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
579
- const typed = matchTypedError(errorClasses, status, [{ 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
+ });
580
535
  if (typed !== undefined) return yield* fail(typed);
581
536
  return yield* fail(matchStripeError(status, errorBody, headers));
582
537
  }
583
538
 
584
539
  // 2xx: the response body IS the payload (no envelope). Wire→TS key
585
540
  // mapping is schema-driven; sensitive members come back Redacted.
541
+ // Strict mode checks the mapped body against the output schema first.
586
542
  const body: unknown = nonJson ? text : (json ?? {});
587
- return wrapSensitive(outputAst, mapKeys(outputAst, body, "decode"));
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);
588
549
  });
589
550
 
590
551
  export const StripeProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
591
552
  API.Protocol,
592
553
  API.Protocol.of({
593
554
  // Erase encode's Credentials requirement (see comment above).
594
- encode: (args) =>
595
- encode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
555
+ encode: (args) => encode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
596
556
  decode,
597
557
  }),
598
558
  );
package/src/retry.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Policy, throttlingFactory, transientFactory } from "@distilled.cloud/core/retry";
1
2
  /**
2
3
  * Stripe retry configuration.
3
4
  *
@@ -16,11 +17,6 @@
16
17
  import * as Context from "effect/Context";
17
18
  import * as Effect from "effect/Effect";
18
19
  import * as Layer from "effect/Layer";
19
- import {
20
- type Policy,
21
- throttlingFactory,
22
- transientFactory,
23
- } from "@distilled.cloud/core/retry";
24
20
 
25
21
  export {
26
22
  type Options,
@@ -43,9 +39,7 @@ export const policy = (optionsOrFactory: Policy) =>
43
39
  Effect.provide(Layer.succeed(Retry, optionsOrFactory));
44
40
 
45
41
  /** Disables all automatic retries. */
46
- export const none = Effect.provide(
47
- Layer.succeed(Retry, { while: () => false }),
48
- );
42
+ export const none = Effect.provide(Layer.succeed(Retry, { while: () => false }));
49
43
 
50
44
  /** Apply the throttling retry policy (retries throttling errors indefinitely). */
51
45
  export const throttling = policy(throttlingFactory);