better-ship 0.25.0 → 0.26.0

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 (66) hide show
  1. package/dist/auth-secondary-storage-b9ptwPSB.d.ts +37 -0
  2. package/dist/auth-secondary-storage-b9ptwPSB.d.ts.map +1 -0
  3. package/dist/better-auth/secondary-storage.d.ts +1 -1
  4. package/dist/bin/notify-ci.d.ts +1 -0
  5. package/dist/bin/notify-ci.js +24 -0
  6. package/dist/bin/notify-ci.js.map +1 -0
  7. package/dist/client-Dv1Ch7yY.js +20 -0
  8. package/dist/client-Dv1Ch7yY.js.map +1 -0
  9. package/dist/cloudflare/auth-secondary-storage.d.ts +2 -36
  10. package/dist/cloudflare/durable-object-client.js +1 -1
  11. package/dist/cloudflare/durable-object-errors.js +2 -47
  12. package/dist/cloudflare/kv-client.js +1 -1
  13. package/dist/cloudflare/kv-errors.js +2 -143
  14. package/dist/cloudflare/r2-client.js +1 -1
  15. package/dist/cloudflare/r2-errors.js +2 -89
  16. package/dist/durable-object-errors-DkMFXQnK.js +48 -0
  17. package/dist/durable-object-errors-DkMFXQnK.js.map +1 -0
  18. package/dist/kv-errors-BUTpFy0t.js +144 -0
  19. package/dist/kv-errors-BUTpFy0t.js.map +1 -0
  20. package/dist/metrics-CWv37yf9.js +30 -0
  21. package/dist/metrics-CWv37yf9.js.map +1 -0
  22. package/dist/metrics-cv9P5nvk.d.ts +19 -0
  23. package/dist/metrics-cv9P5nvk.d.ts.map +1 -0
  24. package/dist/payment-service-DUYd3Itt.d.ts +52 -0
  25. package/dist/payment-service-DUYd3Itt.d.ts.map +1 -0
  26. package/dist/payment-service.error-CqF7GkNp.js +30 -0
  27. package/dist/payment-service.error-CqF7GkNp.js.map +1 -0
  28. package/dist/posthog/metrics.d.ts +2 -18
  29. package/dist/posthog/metrics.js +2 -29
  30. package/dist/posthog/server.d.ts +1 -1
  31. package/dist/posthog/server.js +1 -1
  32. package/dist/r2-errors-rk7n7rvk.js +90 -0
  33. package/dist/r2-errors-rk7n7rvk.js.map +1 -0
  34. package/dist/request-4vJEWxyZ.js +21 -0
  35. package/dist/request-4vJEWxyZ.js.map +1 -0
  36. package/dist/slack-webhook-j1FCPeu8.js +270 -0
  37. package/dist/slack-webhook-j1FCPeu8.js.map +1 -0
  38. package/dist/slack.d.ts +102 -0
  39. package/dist/slack.d.ts.map +1 -0
  40. package/dist/slack.js +3 -0
  41. package/dist/stripe/client.js +2 -19
  42. package/dist/stripe/payment-service.d.ts +2 -51
  43. package/dist/stripe/payment-service.error.js +2 -29
  44. package/dist/stripe/payment-service.js +1 -1
  45. package/dist/stripe/request.js +2 -20
  46. package/dist/stripe-fixture-yFUknTJO.js +33 -0
  47. package/dist/stripe-fixture-yFUknTJO.js.map +1 -0
  48. package/dist/testing/payment-service.d.ts +1 -1
  49. package/dist/testing/payment-service.js +2 -2
  50. package/dist/testing/stripe.js +2 -32
  51. package/package.json +6 -1
  52. package/src/infrastructure/slack/ci-message.ts +276 -0
  53. package/src/infrastructure/slack/index.ts +10 -0
  54. package/src/infrastructure/slack/notify-ci.ts +22 -0
  55. package/src/infrastructure/slack/slack-webhook.ts +36 -0
  56. package/dist/cloudflare/auth-secondary-storage.d.ts.map +0 -1
  57. package/dist/cloudflare/durable-object-errors.js.map +0 -1
  58. package/dist/cloudflare/kv-errors.js.map +0 -1
  59. package/dist/cloudflare/r2-errors.js.map +0 -1
  60. package/dist/posthog/metrics.d.ts.map +0 -1
  61. package/dist/posthog/metrics.js.map +0 -1
  62. package/dist/stripe/client.js.map +0 -1
  63. package/dist/stripe/payment-service.d.ts.map +0 -1
  64. package/dist/stripe/payment-service.error.js.map +0 -1
  65. package/dist/stripe/request.js.map +0 -1
  66. package/dist/testing/stripe.js.map +0 -1
@@ -0,0 +1,276 @@
1
+ import { z } from 'zod'
2
+
3
+ /** The result of one CI job or step, in GitHub Actions terms. */
4
+ export type CiOutcome = 'success' | 'failure' | 'cancelled' | 'skipped'
5
+
6
+ /** What the run delivers after its checks pass. */
7
+ export type CiTarget =
8
+ | { readonly kind: 'check' }
9
+ | {
10
+ readonly kind: 'deploy'
11
+ readonly accountId: string
12
+ readonly worker: string
13
+ readonly siteUrl: string
14
+ /** Absent when the run reached no deploy, such as a pull request. */
15
+ readonly versionId: string | undefined
16
+ }
17
+ | {
18
+ readonly kind: 'publish'
19
+ readonly packageName: string
20
+ /** Absent when npm already has the package version. */
21
+ readonly version: string | undefined
22
+ }
23
+
24
+ /** One CI run, parsed from the environment that the workflow sets. */
25
+ export interface CiRun {
26
+ readonly repository: string
27
+ readonly commitSha: string
28
+ readonly ref:
29
+ | { readonly kind: 'branch'; readonly name: string }
30
+ | {
31
+ readonly kind: 'pullRequest'
32
+ readonly number: number
33
+ readonly url: string
34
+ readonly title: string
35
+ }
36
+ readonly runUrl: string
37
+ readonly jobs: ReadonlyArray<CiStepResult>
38
+ readonly steps: ReadonlyArray<CiStepResult>
39
+ readonly target: CiTarget
40
+ }
41
+
42
+ /** A named job or step and its outcome. */
43
+ export interface CiStepResult {
44
+ readonly name: string
45
+ readonly outcome: CiOutcome
46
+ }
47
+
48
+ /** The commit facts that only git knows. */
49
+ export interface CiCommit {
50
+ readonly subject: string
51
+ readonly author: string
52
+ }
53
+
54
+ /** A Slack incoming-webhook payload. */
55
+ export interface SlackMessage {
56
+ readonly text: string
57
+ readonly blocks: ReadonlyArray<SlackBlock>
58
+ }
59
+
60
+ type SlackElement =
61
+ | { readonly type: 'mrkdwn'; readonly text: string }
62
+ | { readonly type: 'image'; readonly image_url: string; readonly alt_text: string }
63
+
64
+ type SlackBlock =
65
+ | { readonly type: 'section'; readonly text: { readonly type: 'mrkdwn'; readonly text: string } }
66
+ | { readonly type: 'context'; readonly elements: ReadonlyArray<SlackElement> }
67
+
68
+ /** The CI environment could not be parsed. `issues` names each invalid variable. */
69
+ export class CiRunParseError extends Error {
70
+ readonly _tag = 'CiRunParseError' as const
71
+
72
+ constructor(
73
+ readonly issues: ReadonlyArray<string>,
74
+ override readonly cause: unknown,
75
+ ) {
76
+ super(`Invalid CI environment: ${issues.join(', ')}`)
77
+ }
78
+ }
79
+
80
+ const OutcomeSchema = z.enum(['success', 'failure', 'cancelled', 'skipped'])
81
+
82
+ const jsonRecord = <T extends z.ZodType>(value: T) =>
83
+ z
84
+ .string()
85
+ .transform((text, context) => {
86
+ try {
87
+ const value: unknown = JSON.parse(text)
88
+ return value
89
+ } catch {
90
+ context.addIssue({ code: 'custom', message: 'Invalid JSON' })
91
+ return z.NEVER
92
+ }
93
+ })
94
+ .pipe(z.record(z.string(), value))
95
+
96
+ const CiEnvSchema = z.object({
97
+ REPO: z.string().regex(/^[\w.-]+\/[\w.-]+$/),
98
+ COMMIT_SHA: z.string().regex(/^[0-9a-f]{40}$/),
99
+ BRANCH: z.string().min(1),
100
+ RUN_URL: z.httpUrl(),
101
+ JOBS_JSON: jsonRecord(z.object({ result: OutcomeSchema })).optional(),
102
+ STEPS_JSON: jsonRecord(z.object({ outcome: OutcomeSchema })).optional(),
103
+ PR_NUMBER: z.coerce.number().int().positive().optional(),
104
+ PR_URL: z.httpUrl().optional(),
105
+ PR_TITLE: z.string().min(1).optional(),
106
+ ACTION: z.enum(['check', 'deploy', 'publish']).default('check'),
107
+ CLOUDFLARE_ACCOUNT_ID: z.string().min(1).optional(),
108
+ CLOUDFLARE_WORKER: z.string().min(1).optional(),
109
+ SITE_URL: z.httpUrl().optional(),
110
+ DEPLOY_VERSION_ID: z.string().min(1).optional(),
111
+ NPM_PACKAGE: z.string().min(1).optional(),
112
+ NPM_VERSION: z.string().min(1).optional(),
113
+ })
114
+
115
+ type CiEnv = z.infer<typeof CiEnvSchema>
116
+
117
+ /**
118
+ * Parse the variables that a workflow passes to the notifier. GitHub expressions render a
119
+ * missing value as an empty string, so an empty variable counts as unset.
120
+ * @throws {CiRunParseError} when a variable is missing or invalid.
121
+ */
122
+ export function parseCiRun(env: Readonly<Record<string, string | undefined>>): CiRun {
123
+ const present = Object.fromEntries(Object.entries(env).filter(([, value]) => value !== ''))
124
+ const parsed = CiEnvSchema.safeParse(present)
125
+ if (!parsed.success) {
126
+ const issues = parsed.error.issues.map((issue) => issue.path.join('.'))
127
+ throw new CiRunParseError(issues, parsed.error)
128
+ }
129
+ return toCiRun(parsed.data)
130
+ }
131
+
132
+ function toCiRun(env: CiEnv): CiRun {
133
+ const ref: CiRun['ref'] =
134
+ env.PR_NUMBER !== undefined && env.PR_URL !== undefined && env.PR_TITLE !== undefined
135
+ ? { kind: 'pullRequest', number: env.PR_NUMBER, url: env.PR_URL, title: env.PR_TITLE }
136
+ : { kind: 'branch', name: env.BRANCH }
137
+ return {
138
+ repository: env.REPO,
139
+ commitSha: env.COMMIT_SHA,
140
+ ref,
141
+ runUrl: env.RUN_URL,
142
+ jobs: Object.entries(env.JOBS_JSON ?? {}).map(([id, job]) => ({
143
+ name: displayName(id),
144
+ outcome: job.result,
145
+ })),
146
+ steps: Object.entries(env.STEPS_JSON ?? {}).map(([id, step]) => ({
147
+ name: displayName(id),
148
+ outcome: step.outcome,
149
+ })),
150
+ target: toTarget(env),
151
+ }
152
+ }
153
+
154
+ function toTarget(env: CiEnv): CiTarget {
155
+ switch (env.ACTION) {
156
+ case 'check':
157
+ return { kind: 'check' }
158
+ case 'deploy': {
159
+ const { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_WORKER: worker, SITE_URL: siteUrl } = env
160
+ if (accountId === undefined || worker === undefined || siteUrl === undefined) {
161
+ throw new CiRunParseError(
162
+ ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_WORKER', 'SITE_URL'],
163
+ undefined,
164
+ )
165
+ }
166
+ return { kind: 'deploy', accountId, worker, siteUrl, versionId: env.DEPLOY_VERSION_ID }
167
+ }
168
+ case 'publish':
169
+ if (env.NPM_PACKAGE === undefined) throw new CiRunParseError(['NPM_PACKAGE'], undefined)
170
+ return { kind: 'publish', packageName: env.NPM_PACKAGE, version: env.NPM_VERSION }
171
+ }
172
+ }
173
+
174
+ // A job or step id such as `check-and-build` reads as "Check and build".
175
+ function displayName(id: string): string {
176
+ const words = id.replaceAll(/[-_]/g, ' ')
177
+ return words.charAt(0).toUpperCase() + words.slice(1)
178
+ }
179
+
180
+ const logos = {
181
+ depot: 'https://avatars.githubusercontent.com/u/68166601?s=64',
182
+ github: 'https://avatars.githubusercontent.com/u/9919?s=64',
183
+ cloudflare: 'https://avatars.githubusercontent.com/u/314135?s=64',
184
+ npm: 'https://avatars.githubusercontent.com/u/6078720?s=64',
185
+ } as const
186
+
187
+ /** Build the four-line CI message: result, change, links, and identity. */
188
+ export function buildCiMessage(run: CiRun, commit: CiCommit): SlackMessage {
189
+ const headline = headlineFor(run)
190
+ const change = run.ref.kind === 'pullRequest' ? run.ref.title : commit.subject
191
+ return {
192
+ text: `${headline} — ${change}`,
193
+ blocks: [
194
+ { type: 'section', text: { type: 'mrkdwn', text: `*${headline}*\n${escape(change)}` } },
195
+ { type: 'context', elements: linkElements(run) },
196
+ { type: 'context', elements: [{ type: 'mrkdwn', text: identityLine(run, commit) }] },
197
+ ],
198
+ }
199
+ }
200
+
201
+ function headlineFor(run: CiRun): string {
202
+ const failedJobs = namesWith(run.jobs, 'failure')
203
+ if (failedJobs) return `:x: CI failed: ${failedJobs}`
204
+ const cancelledJobs = namesWith(run.jobs, 'cancelled')
205
+ if (cancelledJobs) return `:white_circle: CI cancelled: ${cancelledJobs}`
206
+ const failedSteps = namesWith(run.steps, 'failure')
207
+ const { target } = run
208
+ switch (target.kind) {
209
+ case 'check':
210
+ return failedSteps ? `:x: CI failed: ${failedSteps}` : ':white_check_mark: CI passed'
211
+ case 'deploy':
212
+ if (failedSteps) return `:x: Deploy failed: ${failedSteps}`
213
+ return target.versionId === undefined
214
+ ? ':white_check_mark: CI passed'
215
+ : ':white_check_mark: Deployed to production'
216
+ case 'publish':
217
+ if (failedSteps) return `:x: Publish failed: ${failedSteps}`
218
+ return target.version === undefined
219
+ ? ':white_check_mark: CI passed, nothing to publish'
220
+ : `:white_check_mark: Published ${target.packageName} ${target.version}`
221
+ }
222
+ }
223
+
224
+ function namesWith(results: ReadonlyArray<CiStepResult>, outcome: CiOutcome): string {
225
+ return results
226
+ .filter((result) => result.outcome === outcome)
227
+ .map((result) => result.name)
228
+ .join(', ')
229
+ }
230
+
231
+ function linkElements(run: CiRun): ReadonlyArray<SlackElement> {
232
+ const runHost = new URL(run.runUrl).hostname
233
+ const runLogo = runHost.endsWith('github.com') ? logos.github : logos.depot
234
+ const elements: SlackElement[] = [
235
+ { type: 'image', image_url: runLogo, alt_text: runHost },
236
+ { type: 'mrkdwn', text: `<${run.runUrl}|CI run>` },
237
+ ]
238
+ const { target } = run
239
+ if (target.kind === 'deploy' && target.versionId !== undefined) {
240
+ const dashboard = `https://dash.cloudflare.com/${target.accountId}/workers/services/view/${target.worker}/production/deployments`
241
+ const site = new URL(target.siteUrl)
242
+ elements.push(
243
+ { type: 'mrkdwn', text: '·' },
244
+ { type: 'image', image_url: logos.cloudflare, alt_text: 'Cloudflare' },
245
+ { type: 'mrkdwn', text: `<${dashboard}|Version \`${target.versionId.slice(0, 8)}\`>` },
246
+ { type: 'mrkdwn', text: `· :globe_with_meridians: <${site.origin}|${site.host}>` },
247
+ )
248
+ }
249
+ if (target.kind === 'publish' && target.version !== undefined) {
250
+ const npm = `https://www.npmjs.com/package/${target.packageName}/v/${target.version}`
251
+ elements.push(
252
+ { type: 'mrkdwn', text: '·' },
253
+ { type: 'image', image_url: logos.npm, alt_text: 'npm' },
254
+ { type: 'mrkdwn', text: `<${npm}|${target.packageName}@${target.version}>` },
255
+ )
256
+ }
257
+ return elements
258
+ }
259
+
260
+ function identityLine(run: CiRun, commit: CiCommit): string {
261
+ const repository = run.repository.split('/').at(-1) ?? run.repository
262
+ const ref =
263
+ run.ref.kind === 'pullRequest' ? `<${run.ref.url}|PR #${run.ref.number}>` : run.ref.name
264
+ const commitUrl = `https://github.com/${run.repository}/commit/${run.commitSha}`
265
+ return [
266
+ repository,
267
+ ref,
268
+ `<${commitUrl}|\`${run.commitSha.slice(0, 7)}\`>`,
269
+ escape(commit.author),
270
+ ].join(' · ')
271
+ }
272
+
273
+ // Slack mrkdwn treats these three characters as markup.
274
+ function escape(text: string): string {
275
+ return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
276
+ }
@@ -0,0 +1,10 @@
1
+ export { buildCiMessage, CiRunParseError, parseCiRun } from './ci-message.ts'
2
+ export type {
3
+ CiCommit,
4
+ CiOutcome,
5
+ CiRun,
6
+ CiStepResult,
7
+ CiTarget,
8
+ SlackMessage,
9
+ } from './ci-message.ts'
10
+ export { postSlackMessage, SlackWebhookError } from './slack-webhook.ts'
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // Entry point of `better-ship-notify-ci`. Ported from suparelay and better-ship `.depot/scripts/notify-slack.sh`.
3
+ import { execFileSync } from 'node:child_process'
4
+
5
+ import { buildCiMessage, parseCiRun } from './ci-message.ts'
6
+ import { postSlackMessage } from './slack-webhook.ts'
7
+
8
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL
9
+ if (webhookUrl === undefined || webhookUrl === '') {
10
+ console.log('Slack notification skipped: SLACK_WEBHOOK_URL is unset.')
11
+ } else {
12
+ const run = parseCiRun(process.env)
13
+ const git = (format: string) =>
14
+ execFileSync('git', ['log', '-1', `--format=${format}`, run.commitSha], {
15
+ encoding: 'utf8',
16
+ }).trim()
17
+ await postSlackMessage(
18
+ webhookUrl,
19
+ buildCiMessage(run, { subject: git('%s'), author: git('%an') }),
20
+ )
21
+ console.log('Slack notification sent.')
22
+ }
@@ -0,0 +1,36 @@
1
+ import type { SlackMessage } from './ci-message.ts'
2
+
3
+ /** Slack refused the message or could not be reached. */
4
+ export class SlackWebhookError extends Error {
5
+ readonly _tag = 'SlackWebhookError' as const
6
+
7
+ constructor(
8
+ readonly status: number | undefined,
9
+ override readonly cause: unknown,
10
+ ) {
11
+ super(status === undefined ? 'Slack webhook unreachable' : `Slack webhook returned ${status}`)
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Post one message to a Slack incoming webhook.
17
+ * @throws {SlackWebhookError} when the request fails or Slack answers with an error status.
18
+ */
19
+ export async function postSlackMessage(
20
+ webhookUrl: string,
21
+ message: SlackMessage,
22
+ fetcher: typeof fetch = fetch,
23
+ ): Promise<void> {
24
+ let response: Response
25
+ try {
26
+ response = await fetcher(webhookUrl, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify(message),
30
+ signal: AbortSignal.timeout(30_000),
31
+ })
32
+ } catch (cause) {
33
+ throw new SlackWebhookError(undefined, cause)
34
+ }
35
+ if (!response.ok) throw new SlackWebhookError(response.status, await response.text())
36
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-secondary-storage.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/auth-secondary-storage.ts"],"mappings":";;;;qBAWa,wBAAwB;WAC1B;WACA;EAET;;;qBAMW,4BAA4B;WAC9B;WACA;EAET;;;;;;;qBAUW,6BAA6B;;EAElC,OAAO;;EAMP,IAAI,eAAe,eAAe;;EAUlC,UAAU;;EAKV,gBAAgB;;EAQhB,UAAU,cAAc;;EAef,SAAS"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"durable-object-errors.js","names":[],"sources":["../../src/infrastructure/cloudflare/durable-object-errors.ts"],"sourcesContent":["import { z } from 'zod'\n\nimport {\n AppError,\n isAbortError,\n isCloudflareOverloadedError,\n isCloudflareRetryableError,\n isTransientCloudflareRuntimeError,\n isTransientTransportError,\n type FailureClassification,\n} from '@/core'\n\n/** Stable tag for a failed Durable Object call. */\nexport const DURABLE_OBJECT_CALL_ERROR = 'DurableObjectCallError'\n\nconst RemoteClassifiedErrorSchema = z.object({\n _tag: z.string(),\n classification: z.enum(['transient', 'terminal', 'unknown']),\n remote: z.literal(true),\n})\n\nconst RemoteCallErrorSchema = RemoteClassifiedErrorSchema.extend({\n _tag: z.literal(DURABLE_OBJECT_CALL_ERROR),\n cause: z.unknown(),\n})\n\n/** Recognize a judged local or remote error without admitting its tag to the public error table. */\nexport function isClassifiedDurableObjectError(\n cause: unknown,\n): cause is AppError | (Error & z.output<typeof RemoteClassifiedErrorSchema>) {\n return (\n cause instanceof AppError ||\n (cause instanceof Error && RemoteClassifiedErrorSchema.safeParse(cause).success)\n )\n}\n\n/** Classify the error itself. Its cause remains diagnostic evidence, not a second retry decision. */\nexport function classifyDurableObjectError(cause: unknown): FailureClassification {\n if (isAbortError(cause)) return 'terminal'\n if (isClassifiedDurableObjectError(cause)) return cause.classification\n if (isCloudflareOverloadedError(cause)) return 'terminal'\n if (\n isCloudflareRetryableError(cause) ||\n isTransientCloudflareRuntimeError(cause) ||\n isTransientTransportError(cause)\n )\n return 'transient'\n return 'unknown'\n}\n\n/** Identify a local call error or its validated Workers RPC representation. */\nexport function isDurableObjectCallError(\n cause: unknown,\n): cause is DurableObjectCallError | (Error & z.output<typeof RemoteCallErrorSchema>) {\n return (\n cause instanceof DurableObjectCallError ||\n (cause instanceof Error && RemoteCallErrorSchema.safeParse(cause).success)\n )\n}\n\n/** A call failed after retries stopped. Classification does not establish repeat safety. */\nexport class DurableObjectCallError extends AppError {\n readonly _tag = DURABLE_OBJECT_CALL_ERROR\n readonly classification: FailureClassification\n\n constructor(args: { readonly cause: unknown }) {\n super('Durable Object call failed', { cause: args.cause })\n this.classification = classifyDurableObjectError(args.cause)\n }\n}\n"],"mappings":";;;;;AAaA,MAAa,4BAA4B;AAEzC,MAAM,8BAA8B,EAAE,OAAO;CAC3C,MAAM,EAAE,OAAO;CACf,gBAAgB,EAAE,KAAK;EAAC;EAAa;EAAY;CAAS,CAAC;CAC3D,QAAQ,EAAE,QAAQ,IAAI;AACxB,CAAC;AAED,MAAM,wBAAwB,4BAA4B,OAAO;CAC/D,MAAM,EAAE,QAAQ,yBAAyB;CACzC,OAAO,EAAE,QAAQ;AACnB,CAAC;;AAGD,SAAgB,+BACd,OAC4E;CAC5E,OACE,iBAAiB,YAChB,iBAAiB,SAAS,4BAA4B,UAAU,KAAK,CAAC,CAAC;AAE5E;;AAGA,SAAgB,2BAA2B,OAAuC;CAChF,IAAI,aAAa,KAAK,GAAG,OAAO;CAChC,IAAI,+BAA+B,KAAK,GAAG,OAAO,MAAM;CACxD,IAAI,4BAA4B,KAAK,GAAG,OAAO;CAC/C,IACE,2BAA2B,KAAK,KAChC,kCAAkC,KAAK,KACvC,0BAA0B,KAAK,GAE/B,OAAO;CACT,OAAO;AACT;;AAGA,SAAgB,yBACd,OACoF;CACpF,OACE,iBAAiB,0BAChB,iBAAiB,SAAS,sBAAsB,UAAU,KAAK,CAAC,CAAC;AAEtE;;AAGA,IAAa,yBAAb,cAA4C,SAAS;CACnD,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,MAAmC;EAC7C,MAAM,8BAA8B,EAAE,OAAO,KAAK,MAAM,CAAC;EACzD,KAAK,iBAAiB,2BAA2B,KAAK,KAAK;CAC7D;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"kv-errors.js","names":[],"sources":["../../src/infrastructure/cloudflare/kv-errors.ts"],"sourcesContent":["import {\n AppError,\n isCloudflareOverloadedError,\n isCloudflareRetryableError,\n isTransientTransportError,\n type FailureClassification,\n} from '@/core'\n\n/** KV operations recognized in binding error messages. */\nexport type KVOperation = 'get' | 'put' | 'delete' | 'list'\n\n/** KV failure kinds retained from Typist's binding tests. */\nexport type KVErrorKind =\n | 'invalid_key'\n | 'key_too_large'\n | 'invalid_expiration_ttl'\n | 'invalid_expiration'\n | 'invalid_cache_ttl'\n | 'invalid_list_limit'\n | 'value_too_large'\n | 'client_error'\n | 'rate_limited'\n | 'server_error'\n\n/** Evidence extracted from a KV error and its causes. */\nexport interface KVErrorMeta {\n kind: KVErrorKind\n message: string\n status?: number | undefined\n operation?: Uppercase<KVOperation> | undefined\n}\n\n/** Extract Typist's verified KV message formats, including nested causes. */\nexport function extractKVMeta(cause: unknown): KVErrorMeta | undefined {\n const message = getDeepErrorMessage(cause)\n if (!message) return undefined\n const statusMatch = message.match(/\\bKV\\s+(GET|PUT|DELETE|LIST)\\s+failed:\\s+(\\d{3})\\b/)\n // SAFETY: The regular expression permits only uppercase KV operation names in this capture.\n const operation = statusMatch?.[1] as Uppercase<KVOperation> | undefined\n const status = statusMatch ? Number(statusMatch[2]) : undefined\n\n if (/Key name cannot be empty/i.test(message)) {\n return { kind: 'invalid_key', message, status, operation }\n }\n if (/exceeds key length limit/i.test(message)) {\n return { kind: 'key_too_large', message, status, operation }\n }\n if (/Invalid expiration_ttl/i.test(message)) {\n return { kind: 'invalid_expiration_ttl', message, status, operation }\n }\n if (/Invalid expiration of/i.test(message)) {\n return { kind: 'invalid_expiration', message, status, operation }\n }\n if (/Invalid cache_ttl/i.test(message)) {\n return { kind: 'invalid_cache_ttl', message, status, operation }\n }\n if (/Invalid key_count_limit/i.test(message)) {\n return { kind: 'invalid_list_limit', message, status, operation }\n }\n if (/value.*(too large|exceeds)|exceeds.*value/i.test(message)) {\n return { kind: 'value_too_large', message, status, operation }\n }\n if (status === 429) return { kind: 'rate_limited', message, status, operation }\n if (status !== undefined && status >= 500) {\n return { kind: 'server_error', message, status, operation }\n }\n if (status !== undefined && status >= 400) {\n return { kind: 'client_error', message, status, operation }\n }\n return undefined\n}\n\n/** Identify recognized KV failures that another attempt cannot resolve. */\nexport function isTerminalKVError(cause: unknown): boolean {\n const meta = extractKVMeta(cause)\n if (!meta) return false\n if (meta.kind === 'rate_limited' || meta.kind === 'server_error') return false\n if (meta.status === 408) return false\n return true\n}\n\n/** Identify a PUT rate limit that requires at least one second before retrying. */\nexport function isKVPutRateLimitError(cause: unknown): boolean {\n const meta = extractKVMeta(cause)\n return meta?.operation === 'PUT' && meta.kind === 'rate_limited'\n}\n\n/** Identify PUT rate limits, server failures, and request timeouts. */\nexport function isTransientKVError(cause: unknown): boolean {\n const meta = extractKVMeta(cause)\n if (!meta) return false\n return isKVPutRateLimitError(cause) || meta.kind === 'server_error' || meta.status === 408\n}\n\n/** Apply Typist's KV, overload, platform, and transport checks in their original order. */\nexport function isRetryableKVError(cause: unknown): boolean {\n if (isTerminalKVError(cause)) return false\n if (isTransientKVError(cause)) return true\n if (isCloudflareOverloadedError(cause)) return false\n return isCloudflareRetryableError(cause) || isTransientTransportError(cause)\n}\n\n/** Return the existing retry decision and minimum delay in milliseconds. */\nexport function decideKVRetry(\n cause: unknown,\n): { retry: false } | { retry: true; retryAfterMs: number } {\n if (!isRetryableKVError(cause)) return { retry: false }\n return { retry: true, retryAfterMs: isKVPutRateLimitError(cause) ? 1_000 : 0 }\n}\n\n/** A final KV failure with Typist's diagnostics and Better Ship's error classification. */\nexport class KVError extends AppError {\n readonly _tag = 'KVError'\n readonly classification: FailureClassification\n retryable: boolean\n readonly operation: KVOperation\n readonly key?: string | undefined\n override readonly cause: unknown\n readonly kv?: KVErrorMeta | undefined\n readonly context: {\n operation: KVOperation\n key?: string | undefined\n kv?: KVErrorMeta | undefined\n }\n\n constructor(args: { operation: KVOperation; key?: string | undefined; cause: unknown }) {\n const kv = extractKVMeta(args.cause)\n const causeMessage = getDeepErrorMessage(args.cause) || String(args.cause)\n const keyContext = args.key === undefined ? '' : ` for key \"${args.key}\"`\n super(`KV ${args.operation} failed${keyContext}: ${causeMessage}`, { cause: args.cause })\n this.operation = args.operation\n this.key = args.key\n this.cause = args.cause\n this.kv = kv\n this.context = { operation: args.operation, key: args.key, kv }\n this.retryable = isRetryableKVError(args.cause)\n this.classification = isTerminalKVError(args.cause)\n ? 'terminal'\n : this.retryable\n ? 'transient'\n : 'unknown'\n }\n}\n\n// oxlint-disable anti-slop/no-runtime-typeof -- Preserve Typist's primitive messages and nested causes at the raw binding boundary.\nfunction getDeepErrorMessage(cause: unknown): string {\n if (typeof cause === 'string') return cause\n if (cause === null || cause === undefined) return ''\n if (typeof cause === 'number' || typeof cause === 'boolean' || typeof cause === 'bigint') {\n return String(cause)\n }\n if (typeof cause !== 'object') return ''\n const message = 'message' in cause && typeof cause.message === 'string' ? cause.message : ''\n const causeMessage = 'cause' in cause ? getDeepErrorMessage(cause.cause) : ''\n return `${message} ${causeMessage}`.trim()\n}\n// oxlint-enable anti-slop/no-runtime-typeof\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"r2-errors.js","names":[],"sources":["../../src/infrastructure/cloudflare/r2-errors.ts"],"sourcesContent":["import { z } from 'zod'\n\nimport { AppError, classifyBoundaryError, isAbortError, type FailureClassification } from '@/core'\n\n/** The storage operation that failed. */\nexport type R2Operation =\n | 'head'\n | 'get'\n | 'getJson'\n | 'put'\n | 'delete'\n | 'list'\n | 'createMultipartUpload'\n | 'uploadPart'\n | 'complete'\n | 'abort'\n\n/** Provider diagnostics from the outer failure, without request bodies or credentials. */\nexport type R2Failure = {\n readonly code: number\n readonly action?: string | undefined\n}\n\nconst R2FailureSchema = z.object({\n code: z.number().int().positive(),\n action: z.string().optional(),\n})\n\n// R2 codes describe failures; the caller owns retry decisions and repeat safety.\nconst classifications: ReadonlyMap<number, FailureClassification> = new Map([\n [10001, 'transient'],\n [10043, 'transient'],\n [10054, 'transient'],\n [10058, 'transient'],\n [10013, 'transient'],\n [10002, 'terminal'],\n [10003, 'terminal'],\n [10018, 'terminal'],\n [10035, 'terminal'],\n [10042, 'terminal'],\n [10005, 'terminal'],\n [10006, 'terminal'],\n [10008, 'terminal'],\n [10009, 'terminal'],\n [10073, 'terminal'],\n [10007, 'terminal'],\n [10020, 'terminal'],\n [100100, 'terminal'],\n [10012, 'terminal'],\n [10069, 'terminal'],\n [10033, 'terminal'],\n [10014, 'terminal'],\n [10037, 'terminal'],\n [10039, 'terminal'],\n [10031, 'terminal'],\n [10011, 'terminal'],\n [10024, 'terminal'],\n [10025, 'terminal'],\n [10048, 'terminal'],\n])\n\nfunction extractR2Failure(cause: unknown): R2Failure | undefined {\n if (!(cause instanceof Error)) return undefined\n const parsed = R2FailureSchema.safeParse(cause)\n if (parsed.success) return parsed.data\n // Cloudflare documents the numeric suffix for bindings that omit structured fields.\n const match = /\\((\\d{5,6})\\)\\s*$/.exec(cause.message)\n return match?.[1] === undefined ? undefined : { code: Number(match[1]) }\n}\n\nfunction classifyR2Code(cause: unknown): FailureClassification | undefined {\n const failure = extractR2Failure(cause)\n return failure === undefined ? undefined : (classifications.get(failure.code) ?? 'unknown')\n}\n\nfunction classifyR2Error(cause: unknown): FailureClassification {\n if (isAbortError(cause)) return 'terminal'\n if (cause instanceof AppError) return cause.classification\n return classifyBoundaryError({ cause, classifyBoundary: classifyR2Code, includeCloudflare: true })\n}\n\n/** A raw binding failure after one attempt. Classification does not establish whether a mutation committed. */\nexport class R2Error extends AppError {\n readonly _tag = 'R2Error'\n readonly classification: FailureClassification\n readonly operation: R2Operation\n readonly r2?: R2Failure | undefined\n override readonly cause: unknown\n\n constructor(input: { operation: R2Operation; cause: unknown }) {\n super(`R2 ${input.operation} failed`, { cause: input.cause })\n this.operation = input.operation\n this.cause = input.cause\n this.r2 = extractR2Failure(input.cause)\n this.classification = classifyR2Error(input.cause)\n }\n}\n\n/** JSON serialization or syntax failed. Binding and body-read failures use R2Error. */\nexport class R2JsonError extends AppError {\n readonly _tag = 'R2JsonError'\n readonly classification = 'terminal'\n readonly operation: 'encode' | 'decode'\n override readonly cause: unknown\n\n constructor(input: { operation: 'encode' | 'decode'; cause: unknown }) {\n super(`R2 JSON ${input.operation} failed`, { cause: input.cause })\n this.operation = input.operation\n this.cause = input.cause\n }\n}\n"],"mappings":";;;;AAuBA,MAAM,kBAAkB,EAAE,OAAO;CAC/B,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAChC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;AAGD,MAAM,kCAA8D,IAAI,IAAI;CAC1E,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,QAAQ,UAAU;CACnB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;AACpB,CAAC;AAED,SAAS,iBAAiB,OAAuC;CAC/D,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,MAAM,SAAS,gBAAgB,UAAU,KAAK;CAC9C,IAAI,OAAO,SAAS,OAAO,OAAO;CAElC,MAAM,QAAQ,oBAAoB,KAAK,MAAM,OAAO;CACpD,OAAO,QAAQ,OAAO,SAAY,SAAY,EAAE,MAAM,OAAO,MAAM,EAAE,EAAE;AACzE;AAEA,SAAS,eAAe,OAAmD;CACzE,MAAM,UAAU,iBAAiB,KAAK;CACtC,OAAO,YAAY,SAAY,SAAa,gBAAgB,IAAI,QAAQ,IAAI,KAAK;AACnF;AAEA,SAAS,gBAAgB,OAAuC;CAC9D,IAAI,aAAa,KAAK,GAAG,OAAO;CAChC,IAAI,iBAAiB,UAAU,OAAO,MAAM;CAC5C,OAAO,sBAAsB;EAAE;EAAO,kBAAkB;EAAgB,mBAAmB;CAAK,CAAC;AACnG;;AAGA,IAAa,UAAb,cAA6B,SAAS;CACpC,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAkB;CAElB,YAAY,OAAmD;EAC7D,MAAM,MAAM,MAAM,UAAU,UAAU,EAAE,OAAO,MAAM,MAAM,CAAC;EAC5D,KAAK,YAAY,MAAM;EACvB,KAAK,QAAQ,MAAM;EACnB,KAAK,KAAK,iBAAiB,MAAM,KAAK;EACtC,KAAK,iBAAiB,gBAAgB,MAAM,KAAK;CACnD;AACF;;AAGA,IAAa,cAAb,cAAiC,SAAS;CACxC,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAC1B,AAAS;CACT,AAAkB;CAElB,YAAY,OAA2D;EACrE,MAAM,WAAW,MAAM,UAAU,UAAU,EAAE,OAAO,MAAM,MAAM,CAAC;EACjE,KAAK,YAAY,MAAM;EACvB,KAAK,QAAQ,MAAM;CACrB;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"metrics.d.ts","names":[],"sources":["../../src/infrastructure/posthog/metrics.ts"],"mappings":";;;;qBAKa,iCAAiC;mBAGzB;mBACA;;EAFnB,YACmB,QAAQ,KAAK,qBACb,YAAY,SAAS;;EAIxC,MAAM,cAAc,gBAAW,UAAU;;EAMzC,MAAM,cAAc,eAAe,UAAU;;EAM7C,UAAU,cAAc,eAAe,UAAU"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../../src/infrastructure/posthog/metrics.ts"],"sourcesContent":["import type { PostHog } from 'posthog-node'\n\nimport type { MetricOptions, MetricsService } from '@/application'\n\n/** PostHog Metrics with Worker lifetime management, ported from Typist's metrics-service.ts. */\nexport class PostHogMetricsService implements MetricsService {\n /** Use the analytics client's metrics; the runtime owns each flush promise. */\n constructor(\n private readonly client: Pick<PostHog, 'metrics'>,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {}\n\n /** Record a total and schedule its flush. */\n count(name: string, value = 1, options?: MetricOptions): void {\n this.client.metrics.count(name, value, options)\n this.waitUntil(this.client.metrics.flush())\n }\n\n /** Record a current value and schedule its flush. */\n gauge(name: string, value: number, options?: MetricOptions): void {\n this.client.metrics.gauge(name, value, options)\n this.waitUntil(this.client.metrics.flush())\n }\n\n /** Record an observation and schedule its flush. */\n histogram(name: string, value: number, options?: MetricOptions): void {\n this.client.metrics.histogram(name, value, options)\n this.waitUntil(this.client.metrics.flush())\n }\n}\n"],"mappings":";;AAKA,IAAa,wBAAb,MAA6D;CAGxC;CACA;;CAFnB,YACE,AAAiB,QACjB,AAAiB,WACjB;EAFiB;EACA;CAChB;;CAGH,MAAM,MAAc,QAAQ,GAAG,SAA+B;EAC5D,KAAK,OAAO,QAAQ,MAAM,MAAM,OAAO,OAAO;EAC9C,KAAK,UAAU,KAAK,OAAO,QAAQ,MAAM,CAAC;CAC5C;;CAGA,MAAM,MAAc,OAAe,SAA+B;EAChE,KAAK,OAAO,QAAQ,MAAM,MAAM,OAAO,OAAO;EAC9C,KAAK,UAAU,KAAK,OAAO,QAAQ,MAAM,CAAC;CAC5C;;CAGA,UAAU,MAAc,OAAe,SAA+B;EACpE,KAAK,OAAO,QAAQ,UAAU,MAAM,OAAO,OAAO;EAClD,KAAK,UAAU,KAAK,OAAO,QAAQ,MAAM,CAAC;CAC5C;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../src/infrastructure/stripe/client.ts"],"sourcesContent":["import Stripe from 'stripe'\n\n/** Default SDK retries; client configuration and individual requests can override this value. */\nexport const STRIPE_REQUEST_OPTIONS = Object.freeze({ maxNetworkRetries: 2 })\n\n/**\n * Construct a native Stripe client with fetch transport and two SDK retries by default.\n * Native configuration overrides these defaults. Inject the client into application services.\n */\nexport function createStripeClient(secretKey: string, options: Stripe.StripeConfig = {}): Stripe {\n return new Stripe(secretKey, {\n httpClient: Stripe.createFetchHttpClient(),\n ...STRIPE_REQUEST_OPTIONS,\n ...options,\n })\n}\n"],"mappings":";;;;AAGA,MAAa,yBAAyB,OAAO,OAAO,EAAE,mBAAmB,EAAE,CAAC;;;;;AAM5E,SAAgB,mBAAmB,WAAmB,UAA+B,CAAC,GAAW;CAC/F,OAAO,IAAI,OAAO,WAAW;EAC3B,YAAY,OAAO,sBAAsB;EACzC,GAAG;EACH,GAAG;CACL,CAAC;AACH"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"payment-service.d.ts","names":[],"sources":["../../src/infrastructure/stripe/payment-service.ts"],"mappings":";;;YAKY,wBAAwB,OAAO,iBACzC,SAAS,KAAK,OAAO;;YAGX,gBAAgB,KAAK,OAAO,SAAS;;YAGrC,oBAAoB,KAAK,OAAO,wDAC1C,SAAS,KAAK,OAAO;;YAGX,iBAAiB,QAAQ,WAAW;;YAGpC,iBAAiB,QAAQ,WAAW;;YAGpC,gBAAgB,QAAQ,WAAW;;;;;iBAM9B;;EAEf,eACE,OAAO,OAAO,sBACd,SAAS,wBACR,QAAQ;;EAEX,eAAe,OAAO,eAAe,SAAS,wBAAwB,QAAQ;;EAE9E,mBAAmB,OAAO,eAAe,SAAS,wBAAwB,QAAQ;;EAElF,kBACE,OAAO,mBACP,SAAS,wBACR,QAAQ;;EAEX,WACE,mBACA,SAAS,OAAO,6BAChB,UAAU,OAAO,iBAChB,QAAQ;;;;;;qBAOA,gCAAgC;mBAEd;;EAA7B,YAA6B,QAAQ;;EAGrC,eACE,OAAO,OAAO,sBACd,SAAS,wBACR,QAAQ;;EAKX,eAAe,OAAO,eAAe,SAAS,wBAAwB,QAAQ;;EAO9E,mBACE,OAAO,eACP,SAAS,wBACR,QAAQ;;EAOX,kBACE,OAAO,mBACP,SAAS,wBACR,QAAQ;;EAOX,WACE,mBACA,SAAS,OAAO,6BAChB,UAAU,OAAO,iBAChB,QAAQ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"payment-service.error.js","names":[],"sources":["../../src/infrastructure/stripe/payment-service.error.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { AppError, type FailureClassification } from '@/core'\n\n/** A final Stripe request failure. The caller logs once; native details remain in cause. */\nexport class PaymentServiceError extends AppError {\n readonly _tag = 'PaymentServiceError'\n readonly classification: FailureClassification\n\n /** Classify the final SDK error without scheduling another payment attempt. */\n constructor(\n readonly operation: string,\n cause: Stripe.errors.StripeError,\n ) {\n super(`Stripe request failed: ${operation}`, { cause })\n this.classification = classifyStripeError(cause)\n }\n}\n\nfunction classifyStripeError(error: Stripe.errors.StripeError): FailureClassification {\n if (error.headers?.['stripe-should-retry'] === 'false') return 'terminal'\n const status = error.statusCode\n if (status !== undefined && status >= 400 && status < 500 && status !== 409 && status !== 429)\n return 'terminal'\n if (\n error instanceof Stripe.errors.StripeAuthenticationError ||\n error instanceof Stripe.errors.StripePermissionError ||\n error instanceof Stripe.errors.StripeInvalidRequestError ||\n error instanceof Stripe.errors.StripeIdempotencyError ||\n error instanceof Stripe.errors.StripeCardError\n )\n return 'terminal'\n if (error.headers?.['stripe-should-retry'] === 'true') return 'transient'\n if (status === 409 || status === 429 || (status !== undefined && status >= 500))\n return 'transient'\n if (\n error instanceof Stripe.errors.StripeConnectionError ||\n error instanceof Stripe.errors.StripeAPIError ||\n error instanceof Stripe.errors.StripeRateLimitError\n )\n return 'transient'\n return 'unknown'\n}\n"],"mappings":";;;;;AAKA,IAAa,sBAAb,cAAyC,SAAS;CAMrC;CALX,AAAS,OAAO;CAChB,AAAS;;CAGT,YACE,AAAS,WACT,OACA;EACA,MAAM,0BAA0B,aAAa,EAAE,MAAM,CAAC;EAH7C;EAIT,KAAK,iBAAiB,oBAAoB,KAAK;CACjD;AACF;AAEA,SAAS,oBAAoB,OAAyD;CACpF,IAAI,MAAM,UAAU,2BAA2B,SAAS,OAAO;CAC/D,MAAM,SAAS,MAAM;CACrB,IAAI,WAAW,UAAa,UAAU,OAAO,SAAS,OAAO,WAAW,OAAO,WAAW,KACxF,OAAO;CACT,IACE,iBAAiB,OAAO,OAAO,6BAC/B,iBAAiB,OAAO,OAAO,yBAC/B,iBAAiB,OAAO,OAAO,6BAC/B,iBAAiB,OAAO,OAAO,0BAC/B,iBAAiB,OAAO,OAAO,iBAE/B,OAAO;CACT,IAAI,MAAM,UAAU,2BAA2B,QAAQ,OAAO;CAC9D,IAAI,WAAW,OAAO,WAAW,OAAQ,WAAW,UAAa,UAAU,KACzE,OAAO;CACT,IACE,iBAAiB,OAAO,OAAO,yBAC/B,iBAAiB,OAAO,OAAO,kBAC/B,iBAAiB,OAAO,OAAO,sBAE/B,OAAO;CACT,OAAO;AACT"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"request.js","names":[],"sources":["../../src/infrastructure/stripe/request.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { PaymentServiceError } from './payment-service.error.ts'\n\n/**\n * Invoke one SDK operation; the SDK owns retries configured on its client or request.\n * Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.\n * Non-Stripe failures propagate unchanged.\n */\nexport async function stripeRequest<T>(operation: string, work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (!(cause instanceof Stripe.errors.StripeError)) throw cause\n throw new PaymentServiceError(operation, cause)\n }\n}\n"],"mappings":";;;;;;;;;AASA,eAAsB,cAAiB,WAAmB,MAAoC;CAC5F,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,OAAO,OAAO,cAAc,MAAM;EACzD,MAAM,IAAI,oBAAoB,WAAW,KAAK;CAChD;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"stripe.js","names":[],"sources":["../../src/infrastructure/testing/stripe-fixture.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { createStripeClient } from '../stripe/client.ts'\n\n/** One SDK HTTP attempt; query and body expose Stripe's URL-encoded parameters. */\nexport type StripeCall = {\n readonly method: string\n readonly path: string\n readonly query: URLSearchParams\n readonly body: URLSearchParams\n readonly headers: Headers\n}\n\ntype StripeFixture = { stripe: Stripe; requests: readonly StripeCall[] }\n\n/**\n * Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.\n * The responder receives an unread request. The shared client factory supplies retry defaults.\n */\nexport function stripeFixture(\n respond: (request: Request) => Response | Promise<Response>,\n): StripeFixture {\n const requests: StripeCall[] = []\n const stripe = createStripeClient('sk_test_fixture', {\n httpClient: Stripe.createFetchHttpClient(async (input, init) => {\n const request = new Request(input, init)\n const url = new URL(request.url)\n const call: StripeCall = {\n method: request.method,\n path: url.pathname,\n query: url.searchParams,\n body: new URLSearchParams(),\n headers: new Headers(request.headers),\n }\n requests.push(call)\n const body = new TextDecoder().decode(await request.clone().arrayBuffer())\n for (const [key, value] of new URLSearchParams(body)) call.body.append(key, value)\n return respond(request)\n }),\n })\n return { stripe, requests }\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,cACd,SACe;CACf,MAAM,WAAyB,CAAC;CAkBhC,OAAO;EAAE,QAjBM,mBAAmB,mBAAmB,EACnD,YAAY,OAAO,sBAAsB,OAAO,OAAO,SAAS;GAC9D,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;GACvC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;GAC/B,MAAM,OAAmB;IACvB,QAAQ,QAAQ;IAChB,MAAM,IAAI;IACV,OAAO,IAAI;IACX,MAAM,IAAI,gBAAgB;IAC1B,SAAS,IAAI,QAAQ,QAAQ,OAAO;GACtC;GACA,SAAS,KAAK,IAAI;GAClB,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC;GACzE,KAAK,MAAM,CAAC,KAAK,UAAU,IAAI,gBAAgB,IAAI,GAAG,KAAK,KAAK,OAAO,KAAK,KAAK;GACjF,OAAO,QAAQ,OAAO;EACxB,CAAC,EACH,CACc;EAAG;CAAS;AAC5B"}