@distilled.cloud/stripe 1.0.0-rc.15 → 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/lib/credentials.d.ts +1 -1
- package/lib/credentials.d.ts.map +1 -1
- package/lib/credentials.js +17 -14
- package/lib/credentials.js.map +1 -1
- package/lib/errors.d.ts +83 -83
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +99 -99
- package/lib/errors.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +12 -12
- package/lib/pagination.d.ts +3 -3
- package/lib/pagination.js +15 -15
- package/lib/protocol.d.ts +12 -12
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +240 -168
- package/lib/protocol.js.map +1 -1
- package/lib/retry.d.ts +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +25 -24
- package/lib/retry.js.map +1 -1
- package/lib/services/stripe.d.ts +507 -14141
- package/lib/services/stripe.d.ts.map +1 -1
- package/lib/services/stripe.js +2173 -53133
- package/lib/traits.d.ts +2 -2
- package/lib/traits.js +11 -11
- package/lib/webhooks.js +2 -2
- package/lib/webhooks.js.map +1 -1
- package/package.json +13 -9
- package/src/credentials.ts +12 -11
- package/src/errors.ts +89 -88
- package/src/index.ts +1 -1
- package/src/pagination.ts +3 -3
- package/src/protocol.ts +278 -162
- package/src/retry.ts +6 -4
- package/src/services/stripe.ts +37296 -85780
- package/src/traits.ts +2 -2
- package/src/webhooks.ts +2 -2
- package/lib/protocol.test.d.ts +0 -2
- package/lib/protocol.test.d.ts.map +0 -1
- package/lib/protocol.test.js +0 -353
- package/lib/protocol.test.js.map +0 -1
- package/lib/services/stripe.js.map +0 -1
- package/src/protocol.test.ts +0 -519
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 {
|
|
24
|
-
|
|
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
|
|
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
|
|
41
|
+
* request: credentials -> `Authorization: Bearer <apiKey>` + API base URL;
|
|
34
42
|
* per-call {@link StripeRequestOptions} (resolved from the
|
|
35
|
-
* calling fiber via {@link RequestOptions})
|
|
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)
|
|
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,
|
|
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
|
|
51
|
-
* idempotency_error
|
|
52
|
-
* InvalidRequestError, api_error
|
|
53
|
-
* Stripe-specific status map (402
|
|
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
|
|
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
|
|
118
|
+
* the calling fiber's context instead -- see {@link withRequestOptions}.
|
|
112
119
|
*/
|
|
113
|
-
export class RequestOptions
|
|
114
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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,
|
|
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`)
|
|
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)
|
|
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
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
outputAst
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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(
|
|
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);
|