mppx 0.5.1 → 0.5.4

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 (111) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/Credential.d.ts +12 -0
  3. package/dist/Credential.d.ts.map +1 -1
  4. package/dist/Credential.js +22 -4
  5. package/dist/Credential.js.map +1 -1
  6. package/dist/Method.d.ts +4 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +2 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/cli/cli.d.ts.map +1 -1
  11. package/dist/cli/cli.js +11 -9
  12. package/dist/cli/cli.js.map +1 -1
  13. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  14. package/dist/cli/plugins/tempo.js +3 -3
  15. package/dist/cli/plugins/tempo.js.map +1 -1
  16. package/dist/cli/utils.d.ts +2 -0
  17. package/dist/cli/utils.d.ts.map +1 -1
  18. package/dist/cli/utils.js +10 -5
  19. package/dist/cli/utils.js.map +1 -1
  20. package/dist/proxy/Proxy.d.ts.map +1 -1
  21. package/dist/proxy/Proxy.js +52 -8
  22. package/dist/proxy/Proxy.js.map +1 -1
  23. package/dist/proxy/internal/Route.d.ts.map +1 -1
  24. package/dist/proxy/internal/Route.js +7 -3
  25. package/dist/proxy/internal/Route.js.map +1 -1
  26. package/dist/server/Mppx.d.ts.map +1 -1
  27. package/dist/server/Mppx.js +90 -71
  28. package/dist/server/Mppx.js.map +1 -1
  29. package/dist/server/Transport.d.ts +5 -1
  30. package/dist/server/Transport.d.ts.map +1 -1
  31. package/dist/server/Transport.js +71 -7
  32. package/dist/server/Transport.js.map +1 -1
  33. package/dist/server/internal/html/config.d.ts +144 -0
  34. package/dist/server/internal/html/config.d.ts.map +1 -0
  35. package/dist/server/internal/html/config.js +303 -0
  36. package/dist/server/internal/html/config.js.map +1 -0
  37. package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
  38. package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
  39. package/dist/server/internal/html/serviceWorker.gen.js +3 -0
  40. package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
  41. package/dist/stripe/internal/types.d.ts +6 -0
  42. package/dist/stripe/internal/types.d.ts.map +1 -1
  43. package/dist/stripe/server/Charge.d.ts +30 -16
  44. package/dist/stripe/server/Charge.d.ts.map +1 -1
  45. package/dist/stripe/server/Charge.js +35 -6
  46. package/dist/stripe/server/Charge.js.map +1 -1
  47. package/dist/stripe/server/internal/html/types.d.ts +2 -0
  48. package/dist/stripe/server/internal/html/types.d.ts.map +1 -0
  49. package/dist/stripe/server/internal/html/types.js +2 -0
  50. package/dist/stripe/server/internal/html/types.js.map +1 -0
  51. package/dist/stripe/server/internal/html.gen.d.ts +2 -0
  52. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
  53. package/dist/stripe/server/internal/html.gen.js +3 -0
  54. package/dist/stripe/server/internal/html.gen.js.map +1 -0
  55. package/dist/tempo/server/Charge.d.ts +33 -26
  56. package/dist/tempo/server/Charge.d.ts.map +1 -1
  57. package/dist/tempo/server/Charge.js +46 -11
  58. package/dist/tempo/server/Charge.js.map +1 -1
  59. package/dist/tempo/server/Session.d.ts.map +1 -1
  60. package/dist/tempo/server/Session.js +3 -2
  61. package/dist/tempo/server/Session.js.map +1 -1
  62. package/dist/tempo/server/internal/html.gen.d.ts +2 -0
  63. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
  64. package/dist/tempo/server/internal/html.gen.js +3 -0
  65. package/dist/tempo/server/internal/html.gen.js.map +1 -0
  66. package/dist/tempo/server/internal/transport.d.ts +1 -1
  67. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  68. package/dist/tempo/server/internal/transport.js +45 -58
  69. package/dist/tempo/server/internal/transport.js.map +1 -1
  70. package/package.json +2 -2
  71. package/src/Credential.ts +28 -4
  72. package/src/Method.ts +6 -1
  73. package/src/cli/cli.ts +11 -8
  74. package/src/cli/plugins/tempo.ts +3 -2
  75. package/src/cli/utils.test.ts +64 -0
  76. package/src/cli/utils.ts +10 -4
  77. package/src/env.d.ts +1 -0
  78. package/src/mcp-sdk/server/Transport.test.ts +6 -0
  79. package/src/proxy/Proxy.test.ts +188 -1
  80. package/src/proxy/Proxy.ts +58 -9
  81. package/src/proxy/internal/Route.test.ts +9 -0
  82. package/src/proxy/internal/Route.ts +5 -2
  83. package/src/server/Mppx.test.ts +171 -18
  84. package/src/server/Mppx.ts +120 -79
  85. package/src/server/Transport.test.ts +232 -2
  86. package/src/server/Transport.ts +84 -7
  87. package/src/server/internal/html/config.ts +414 -0
  88. package/src/server/internal/html/serviceWorker.client.ts +28 -0
  89. package/src/server/internal/html/serviceWorker.gen.ts +2 -0
  90. package/src/server/internal/html/serviceWorker.ts +27 -0
  91. package/src/server/internal/html/tsconfig.worker.client.json +8 -0
  92. package/src/server/internal/html/tsconfig.worker.json +8 -0
  93. package/src/stripe/internal/types.ts +20 -0
  94. package/src/stripe/server/Charge.ts +62 -6
  95. package/src/stripe/server/internal/html/main.ts +174 -0
  96. package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
  97. package/src/stripe/server/internal/html/package.json +9 -0
  98. package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
  99. package/src/stripe/server/internal/html/tsconfig.json +8 -0
  100. package/src/stripe/server/internal/html/types.ts +5 -0
  101. package/src/stripe/server/internal/html.gen.ts +2 -0
  102. package/src/tempo/server/Charge.ts +64 -10
  103. package/src/tempo/server/Session.ts +3 -2
  104. package/src/tempo/server/internal/html/main.ts +111 -0
  105. package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
  106. package/src/tempo/server/internal/html/package.json +10 -0
  107. package/src/tempo/server/internal/html/tsconfig.json +8 -0
  108. package/src/tempo/server/internal/html.gen.ts +2 -0
  109. package/src/tempo/server/internal/transport.test.ts +37 -31
  110. package/src/tempo/server/internal/transport.ts +44 -58
  111. package/src/tsconfig.json +1 -1
@@ -1,9 +1,14 @@
1
+ import { Json } from 'ox'
2
+
1
3
  import * as Challenge from '../Challenge.js'
2
4
  import * as Credential from '../Credential.js'
3
5
  import * as Errors from '../Errors.js'
4
6
  import type { Distribute, UnionToIntersection } from '../internal/types.js'
5
7
  import * as core_Mcp from '../Mcp.js'
6
8
  import * as Receipt from '../Receipt.js'
9
+ import * as Html from './internal/html/config.js'
10
+ import { html } from './internal/html/config.js'
11
+ import { serviceWorker } from './internal/html/serviceWorker.gen.js'
7
12
 
8
13
  export { type McpSdk, mcpSdk } from '../mcp-sdk/server/Transport.js'
9
14
 
@@ -30,11 +35,14 @@ export type Transport<
30
35
  respondChallenge: (options: {
31
36
  challenge: Challenge.Challenge
32
37
  error?: Errors.PaymentError | undefined
38
+ html?: Html.Options | undefined
33
39
  input: input
34
40
  }) => challengeOutput | Promise<challengeOutput>
35
41
  /** Attaches a receipt to a successful response. */
36
42
  respondReceipt: (options: {
37
43
  challengeId: string
44
+ credential: Credential.Credential
45
+ input: input
38
46
  receipt: Receipt.Receipt
39
47
  response: receiptResponse
40
48
  }) => receiptOutput
@@ -87,7 +95,7 @@ export type WithReceipt<transport extends AnyTransport = Http> = WithReceiptOver
87
95
  * name: 'custom',
88
96
  * getCredential(input) { ... },
89
97
  * respondChallenge({ challenge, input }) { ... },
90
- * respondReceipt({ receipt, response, challengeId }) { ... },
98
+ * respondReceipt({ receipt, response, challengeId, credential, input }) { ... },
91
99
  * })
92
100
  * ```
93
101
  */
@@ -121,17 +129,86 @@ export function http(): Http {
121
129
  return Credential.deserialize(payment)
122
130
  },
123
131
 
124
- respondChallenge({ challenge, error }) {
132
+ async respondChallenge(options) {
133
+ const { challenge, error, input } = options
134
+
135
+ if (options.html && new URL(input.url).searchParams.has(Html.serviceWorkerParam))
136
+ return new Response(serviceWorker, {
137
+ status: 200,
138
+ headers: {
139
+ 'Content-Type': 'application/javascript',
140
+ 'Cache-Control': 'no-store',
141
+ },
142
+ })
143
+
125
144
  const headers: Record<string, string> = {
126
145
  'WWW-Authenticate': Challenge.serialize(challenge),
127
146
  'Cache-Control': 'no-store',
128
147
  }
129
148
 
130
- let body: string | null = null
131
- if (error) {
132
- headers['Content-Type'] = 'application/problem+json'
133
- body = JSON.stringify(error.toProblemDetails(challenge.id))
134
- }
149
+ const body = await (async () => {
150
+ if (options.html && input.headers.get('Accept')?.includes('text/html')) {
151
+ headers['Content-Type'] = 'text/html; charset=utf-8'
152
+
153
+ const theme = Html.mergeDefined(
154
+ {
155
+ favicon: undefined as Html.Theme['favicon'],
156
+ fontUrl: undefined as Html.Theme['fontUrl'],
157
+ logo: undefined as Html.Theme['logo'],
158
+ ...Html.defaultTheme,
159
+ },
160
+ (options.html.theme as never) ?? {},
161
+ )
162
+ const text = Html.sanitizeRecord(
163
+ Html.mergeDefined(Html.defaultText, (options.html.text as never) ?? {}),
164
+ )
165
+ const amount = await options.html.formatAmount(challenge.request)
166
+
167
+ return html`<!doctype html>
168
+ <html lang="en">
169
+ <head>
170
+ <meta charset="UTF-8" />
171
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
172
+ <meta name="robots" content="noindex" />
173
+ <meta name="color-scheme" content="${theme.colorScheme}" />
174
+ <title>${text.title}</title>
175
+ ${Html.favicon(theme, challenge.realm)} ${Html.font(theme)} ${Html.style(theme)}
176
+ </head>
177
+ <body>
178
+ <main>
179
+ <header class="${Html.classNames.header}">
180
+ ${Html.logo(theme)}
181
+ <span>${text.paymentRequired}</span>
182
+ </header>
183
+ <section class="${Html.classNames.summary}" aria-label="Payment summary">
184
+ <h1 class="${Html.classNames.summaryAmount}">${Html.sanitize(amount)}</h1>
185
+ ${challenge.description
186
+ ? `<p class="${Html.classNames.summaryDescription}">${Html.sanitize(challenge.description)}</p>`
187
+ : ''}
188
+ ${challenge.expires
189
+ ? `<p class="${Html.classNames.summaryExpires}">${text.expires} <time datetime="${new Date(challenge.expires).toISOString()}">${new Date(challenge.expires).toLocaleString()}</time></p>`
190
+ : ''}
191
+ </section>
192
+ <div id="${Html.rootId}" aria-label="Payment form"></div>
193
+ <script id="${Html.dataId}" type="application/json">
194
+ ${Json.stringify({
195
+ config: options.html.config,
196
+ challenge,
197
+ text,
198
+ theme,
199
+ } satisfies Html.Data).replace(/</g, '\\u003c')}
200
+ </script>
201
+ ${options.html.content}
202
+ </main>
203
+ </body>
204
+ </html> `
205
+ }
206
+ if (error) {
207
+ headers['Content-Type'] = 'application/problem+json'
208
+ return JSON.stringify(error.toProblemDetails(challenge.id))
209
+ }
210
+ return null
211
+ })()
135
212
 
136
213
  return new Response(body, { status: error?.status ?? 402, headers })
137
214
  },
@@ -0,0 +1,414 @@
1
+ import type * as Challenge from '../../../Challenge.js'
2
+ import type * as Method from '../../../Method.js'
3
+
4
+ export type Options = {
5
+ config: Record<string, unknown>
6
+ content: string
7
+ formatAmount: (request: any) => string | Promise<string>
8
+ text: Text | undefined
9
+ theme: Theme | undefined
10
+ }
11
+
12
+ export type Data<
13
+ method extends Method.Method = Method.Method,
14
+ config extends Record<string, unknown> = {},
15
+ > = {
16
+ config: config
17
+ challenge: Challenge.FromMethods<[method]>
18
+ text: { [k in keyof Text]-?: NonNullable<Text[k]> }
19
+ theme: {
20
+ [k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
21
+ }
22
+ }
23
+
24
+ export const dataId = '__MPPX_DATA__'
25
+
26
+ export const errorId = 'root_error'
27
+
28
+ export const rootId = 'root'
29
+
30
+ export const serviceWorkerParam = '__mppx_worker'
31
+
32
+ export const classNames = {
33
+ error: 'mppx-error',
34
+ header: 'mppx-header',
35
+ logo: 'mppx-logo',
36
+ logoColorScheme: (colorScheme: string) =>
37
+ colorScheme === 'dark' || colorScheme === 'light'
38
+ ? `${classNames.logo}--${colorScheme}`
39
+ : undefined,
40
+ summary: 'mppx-summary',
41
+ summaryAmount: 'mppx-summary-amount',
42
+ summaryDescription: 'mppx-summary-description',
43
+ summaryExpires: 'mppx-summary-expires',
44
+ }
45
+
46
+ export function sanitize(str: string): string {
47
+ return str
48
+ .replace(/&/g, '&amp;')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/'/g, '&#39;')
53
+ }
54
+
55
+ export function sanitizeRecord<type extends Record<string, string>>(record: type): type {
56
+ return Object.fromEntries(
57
+ Object.entries(record).map(([key, value]) => [key, sanitize(value)]),
58
+ ) as type
59
+ }
60
+
61
+ export const html = String.raw
62
+
63
+ class CssVar {
64
+ readonly name: string
65
+ constructor(token: string) {
66
+ this.name = `--mppx-${token}`
67
+ }
68
+ toString() {
69
+ return `var(${this.name})`
70
+ }
71
+ }
72
+
73
+ export const vars = {
74
+ accent: new CssVar('accent'),
75
+ background: new CssVar('background'),
76
+ border: new CssVar('border'),
77
+ foreground: new CssVar('foreground'),
78
+ muted: new CssVar('muted'),
79
+ negative: new CssVar('negative'),
80
+ positive: new CssVar('positive'),
81
+ surface: new CssVar('surface'),
82
+ fontFamily: new CssVar('font-family'),
83
+ fontSizeBase: new CssVar('font-size-base'),
84
+ radius: new CssVar('radius'),
85
+ spacingUnit: new CssVar('spacing-unit'),
86
+ } as const
87
+
88
+ export function font(theme: Theme) {
89
+ if (!theme.fontUrl) return ''
90
+ return html`<link
91
+ rel="preconnect"
92
+ href="${sanitize(new URL(theme.fontUrl).origin)}"
93
+ crossorigin
94
+ />
95
+ <link rel="stylesheet" href="${sanitize(theme.fontUrl)}" />`
96
+ }
97
+
98
+ export function style(theme: {
99
+ [k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
100
+ }) {
101
+ const colors = Object.fromEntries(
102
+ colorTokens.map((name) => [name, resolveColor(theme[name], defaultTheme[name])]),
103
+ ) as Record<(typeof colorTokens)[number], readonly [light: string, dark: string]>
104
+ const lightVars = colorTokens
105
+ .map((token) => `${vars[token].name}: ${colors[token][0]};`)
106
+ .join('\n ')
107
+ const darkVars = colorTokens
108
+ .map((token) => `${vars[token].name}: ${colors[token][1]};`)
109
+ .join('\n ')
110
+ const isLightOnly = theme.colorScheme === 'light'
111
+ const isDarkOnly = theme.colorScheme === 'dark'
112
+ const rootVars = isDarkOnly ? darkVars : lightVars
113
+ const darkMedia =
114
+ !isLightOnly && !isDarkOnly
115
+ ? `\n @media (prefers-color-scheme: dark) {\n :root {\n ${darkVars}\n }\n }`
116
+ : ''
117
+ return html`
118
+ <style>
119
+ ${reset}
120
+ :root {
121
+ color-scheme: ${theme.colorScheme};
122
+ ${vars.fontFamily.name}: ${theme.fontFamily};
123
+ ${vars.fontSizeBase.name}: ${theme.fontSizeBase};
124
+ ${vars.radius.name}: ${theme.radius};
125
+ ${vars.spacingUnit.name}: ${theme.spacingUnit};
126
+ ${rootVars}
127
+ }${darkMedia}
128
+ *:focus-visible {
129
+ outline-color: ${vars.accent};
130
+ outline-offset: 0.15rem;
131
+ outline-style: solid;
132
+ outline-width: 2px;
133
+ }
134
+ body {
135
+ -webkit-font-smoothing: antialiased;
136
+ -moz-osx-font-smoothing: grayscale;
137
+ background: ${vars.background};
138
+ color: ${vars.foreground};
139
+ font-family: ${vars.fontFamily};
140
+ font-size: ${vars.fontSizeBase};
141
+ }
142
+ main {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: calc(${vars.spacingUnit} * 8);
146
+ margin-left: auto;
147
+ margin-right: auto;
148
+ max-width: clamp(300px, calc(${vars.spacingUnit} * 224), 896px);
149
+ padding: calc(${vars.spacingUnit} * 12) calc(${vars.spacingUnit} * 8) calc(${vars.spacingUnit} * 16);
150
+ }
151
+ .${classNames.header} {
152
+ align-items: center;
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: calc(${vars.spacingUnit} * 4);
156
+ justify-content: space-between;
157
+ span {
158
+ background: ${vars.surface};
159
+ border: 1px solid ${vars.border};
160
+ border-radius: calc(${vars.spacingUnit} * 50);
161
+ font-size: 0.75rem;
162
+ font-weight: 500;
163
+ letter-spacing: 0.025em;
164
+ padding: calc(${vars.spacingUnit} * 1) calc(${vars.spacingUnit} * 4);
165
+ }
166
+ }
167
+ .${classNames.logo} {
168
+ max-height: 1.75rem;
169
+ }
170
+ .${classNames.logoColorScheme('dark')} {
171
+ @media (prefers-color-scheme: light) {
172
+ display: none;
173
+ }
174
+ }
175
+ .${classNames.logoColorScheme('light')} {
176
+ @media (prefers-color-scheme: dark) {
177
+ display: none;
178
+ }
179
+ }
180
+ .${classNames.summary} {
181
+ background: ${vars.surface};
182
+ border: 1px solid ${vars.border};
183
+ border-radius: ${vars.radius};
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: calc(${vars.spacingUnit} * 3);
187
+ padding: calc(${vars.spacingUnit} * 6) calc(${vars.spacingUnit} * 6);
188
+ }
189
+ .${classNames.summaryAmount} {
190
+ font-size: 2.5rem;
191
+ font-variant-numeric: tabular-nums;
192
+ font-weight: 700;
193
+ line-height: 1.2;
194
+ }
195
+ .${classNames.summaryDescription} {
196
+ font-size: 1.25rem;
197
+ }
198
+ .${classNames.summaryExpires} {
199
+ color: ${vars.muted};
200
+ }
201
+ .${classNames.error} {
202
+ color: ${vars.negative};
203
+ font-size: 0.95rem;
204
+ margin-top: calc(${vars.spacingUnit} * -1.5);
205
+ text-align: center;
206
+ }
207
+ </style>
208
+ `
209
+ }
210
+
211
+ export function showError(message: string) {
212
+ const existing = document.getElementById(errorId)
213
+ if (existing) {
214
+ existing.textContent = message
215
+ return
216
+ }
217
+ const el = document.createElement('p')
218
+ el.id = errorId
219
+ el.className = classNames.error
220
+ el.role = 'alert'
221
+ el.textContent = message
222
+ document.getElementById(rootId)?.after(el)
223
+ }
224
+
225
+ export function favicon(theme: Theme, realm: string) {
226
+ if (typeof theme.favicon === 'string')
227
+ return html`<link rel="icon" href="${sanitize(theme.favicon)}" />`
228
+ if (typeof theme.favicon === 'object') {
229
+ return html`<link
230
+ rel="icon"
231
+ href="${sanitize(theme.favicon.light)}"
232
+ media="(prefers-color-scheme: light)"
233
+ />
234
+ <link
235
+ rel="icon"
236
+ href="${sanitize(theme.favicon.dark)}"
237
+ media="(prefers-color-scheme: dark)"
238
+ />`
239
+ }
240
+ // Fallback: use host's favicon via Google S2 service
241
+ try {
242
+ const domain = new URL(realm).hostname
243
+ return html`<link
244
+ rel="icon"
245
+ href="https://www.google.com/s2/favicons?domain=${domain}&sz=64"
246
+ />`
247
+ } catch {
248
+ return ''
249
+ }
250
+ }
251
+
252
+ export function logo(value: Theme) {
253
+ if (typeof value.logo === 'undefined') return ''
254
+ if (typeof value.logo === 'string')
255
+ return html`<img alt="" class="${classNames.logo}" src="${sanitize(value.logo)}" />`
256
+ return Object.entries(value.logo)
257
+ .map(
258
+ (entry) =>
259
+ html`<img
260
+ alt=""
261
+ class="${classNames.logo} ${classNames.logoColorScheme(entry[0])}"
262
+ src="${sanitize(entry[1])}"
263
+ />`,
264
+ )
265
+ .join('\n')
266
+ }
267
+
268
+ export type Text = {
269
+ /** Prefix for the expiry line. @default 'Expires at' */
270
+ expires?: string | undefined
271
+ /** Pay button label. @default 'Pay' */
272
+ pay?: string | undefined
273
+ /** Badge label. @default 'Payment Required' */
274
+ paymentRequired?: string | undefined
275
+ /** Page title. @default text.paymentRequired */
276
+ title?: string | undefined
277
+ }
278
+
279
+ export const defaultText = {
280
+ expires: 'Expires at',
281
+ pay: 'Pay',
282
+ paymentRequired: 'Payment Required',
283
+ title: 'Payment Required',
284
+ } as const satisfies Required<Text>
285
+
286
+ export type Theme = {
287
+ /** Color scheme. @default 'light dark' */
288
+ colorScheme?: 'light' | 'dark' | 'light dark' | undefined
289
+ /** Font family. @default 'system-ui, -apple-system, sans-serif' */
290
+ fontFamily?: string | undefined
291
+ /** Base font size. @default '16px' */
292
+ fontSizeBase?: string | undefined
293
+ /** Font URL to inject (e.g. Google Fonts `<link>`). */
294
+ fontUrl?: string | undefined
295
+ /** Favicon URL. Light/dark variants supported. Falls back to host's favicon via Google S2 service. */
296
+ favicon?: string | { light: string; dark: string } | undefined
297
+ /** Logo URL shown in header. Light/dark variants supported. */
298
+ logo?: string | { light: string; dark: string } | undefined
299
+ /** Border radius. @default '6px' */
300
+ radius?: string | undefined
301
+ /** The base spacing unit that all other spacing is derived from. Increase or decrease this value to make your layout more or less spacious. @default '2px' */
302
+ spacingUnit?: string | undefined
303
+
304
+ /** Accent color (buttons, links). @default ['#171717', '#ededed'] */
305
+ accent?: LightDark | undefined
306
+ /** Page background. @default ['#ffffff', '#0a0a0a'] */
307
+ background?: LightDark | undefined
308
+ /** Border color. @default ['#e5e5e5', '#2e2e2e'] */
309
+ border?: LightDark | undefined
310
+ /** Primary text/content color. @default ['#0a0a0a', '#ededed'] */
311
+ foreground?: LightDark | undefined
312
+ /** Secondary/muted text. @default ['#666666', '#a1a1a1'] */
313
+ muted?: LightDark | undefined
314
+ /** Error/danger color. @default ['#e5484d', '#e5484d'] */
315
+ negative?: LightDark | undefined
316
+ /** Success color. @default ['#30a46c', '#30a46c'] */
317
+ positive?: LightDark | undefined
318
+ /** Input/card surface. @default ['#f5f5f5', '#1a1a1a'] */
319
+ surface?: LightDark | undefined
320
+ }
321
+
322
+ export type LightDark = string | readonly [light: string, dark: string]
323
+
324
+ export const defaultTheme = {
325
+ colorScheme: 'light dark',
326
+ fontFamily: 'system-ui, -apple-system, sans-serif',
327
+ fontSizeBase: '16px',
328
+ radius: '6px',
329
+ spacingUnit: '2px',
330
+
331
+ accent: ['#171717', '#ededed'],
332
+ background: ['#ffffff', '#0a0a0a'],
333
+ border: ['#e5e5e5', '#2e2e2e'],
334
+ foreground: ['#0a0a0a', '#ededed'],
335
+ muted: ['#666666', '#a1a1a1'],
336
+ negative: ['#e5484d', '#e5484d'],
337
+ positive: ['#30a46c', '#30a46c'],
338
+ surface: ['#f5f5f5', '#1a1a1a'],
339
+ } as const satisfies Required<Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>>
340
+
341
+ export const colorTokens = [
342
+ 'accent',
343
+ 'negative',
344
+ 'positive',
345
+ 'background',
346
+ 'foreground',
347
+ 'muted',
348
+ 'surface',
349
+ 'border',
350
+ ] as const satisfies readonly (keyof typeof defaultTheme)[]
351
+
352
+ export function resolveColor(
353
+ value: Theme[(typeof colorTokens)[number]] | undefined,
354
+ fallback: readonly [string, string],
355
+ ): readonly [light: string, dark: string] {
356
+ if (!value) return fallback
357
+ if (typeof value === 'string') return [value, value]
358
+ return value
359
+ }
360
+
361
+ export function mergeDefined<type>(defaults: type, value: DeepPartial<type> | undefined): type {
362
+ if (value === undefined) return defaults
363
+ if (!isPlainObject(defaults) || !isPlainObject(value)) return (value ?? defaults) as type
364
+
365
+ const result: Record<string, unknown> = { ...defaults }
366
+
367
+ for (const [key, nextValue] of Object.entries(value)) {
368
+ if (nextValue === undefined) continue
369
+
370
+ const currentValue = result[key]
371
+
372
+ result[key] =
373
+ isPlainObject(currentValue) && isPlainObject(nextValue)
374
+ ? mergeDefined(currentValue, nextValue)
375
+ : nextValue
376
+ }
377
+
378
+ return result as type
379
+ }
380
+ type DeepPartial<type> = {
381
+ [key in keyof type]?: type[key] extends readonly unknown[]
382
+ ? type[key] | undefined
383
+ : type[key] extends object
384
+ ? DeepPartial<type[key]> | undefined
385
+ : type[key] | undefined
386
+ }
387
+
388
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
389
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
390
+ }
391
+
392
+ // Slimmed down Tailwind preflight
393
+ // https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
394
+ const reset = html`
395
+ *, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0;
396
+ padding: 0; border: 0 solid; border-color: ${vars.border}; } html, :host { line-height: 1.5;
397
+ -webkit-text-size-adjust: 100%; tab-size: 4; -webkit-tap-highlight-color: transparent; } h1, h2,
398
+ h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } a { color: inherit;
399
+ -webkit-text-decoration: inherit; text-decoration: inherit; } b, strong { font-weight: bolder; }
400
+ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
401
+ 'Liberation Mono', 'Courier New', monospace; font-size: 1em; } small { font-size: 80%; } ol, ul,
402
+ menu { list-style: none; } img, svg, video, canvas, audio, iframe, embed, object { display: block;
403
+ vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select,
404
+ optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit;
405
+ font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0;
406
+ background-color: transparent; opacity: 1; } ::file-selector-button { margin-inline-end: 4px; }
407
+ ::placeholder { opacity: 1; } @supports (not (-webkit-appearance: -apple-pay-button)) or
408
+ (contain-intrinsic-size: 1px) { ::placeholder { color: color-mix(in oklab, currentcolor 50%,
409
+ transparent); } } textarea { resize: vertical; } ::-webkit-search-decoration { -webkit-appearance:
410
+ none; } :-moz-ui-invalid { box-shadow: none; } button, input:where([type='button'],
411
+ [type='reset'], [type='submit']), ::file-selector-button { appearance: button; }
412
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
413
+ [hidden]:where(:not([hidden='until-found'])) { display: none !important; }
414
+ `
@@ -0,0 +1,28 @@
1
+ import { serviceWorkerParam } from './config.js'
2
+
3
+ export async function submitCredential(credential: string): Promise<void> {
4
+ const url = new URL(location.href)
5
+ url.searchParams.set(serviceWorkerParam, '')
6
+
7
+ const registration = await navigator.serviceWorker.register(url.pathname + url.search)
8
+
9
+ const serviceWorker = await new Promise<ServiceWorker>((resolve) => {
10
+ const mppxWorker = registration.installing ?? registration.waiting ?? registration.active
11
+ if (mppxWorker?.state === 'activated') return resolve(mppxWorker)
12
+ const target = mppxWorker ?? registration
13
+ target.addEventListener('statechange', function handler() {
14
+ const active = registration.active
15
+ if (active?.state === 'activated') {
16
+ target.removeEventListener('statechange', handler)
17
+ resolve(active)
18
+ }
19
+ })
20
+ })
21
+
22
+ await new Promise<void>((resolve) => {
23
+ const channel = new MessageChannel()
24
+ channel.port1.onmessage = () => resolve()
25
+ serviceWorker.postMessage({ credential }, [channel.port2])
26
+ })
27
+ location.reload()
28
+ }
@@ -0,0 +1,2 @@
1
+ // Generated — do not edit.
2
+ export const serviceWorker = "(function(){let e=self,t;e.addEventListener(`activate`,t=>{t.waitUntil(e.clients.claim())}),e.addEventListener(`message`,e=>{if(!e.source)return;let n=e.data?.credential;typeof n!=`string`||!n.startsWith(`Payment `)||(t=n,e.ports[0]?.postMessage(`ack`))}),e.addEventListener(`fetch`,n=>{if(!t||n.request.mode!==`navigate`||new URL(n.request.url).origin!==e.location.origin)return;let r=new Headers(n.request.headers);r.set(`Authorization`,t),t=void 0,n.respondWith(fetch(n.request,{headers:r})),e.registration.unregister()})})();"
@@ -0,0 +1,27 @@
1
+ const serviceWorker = self as unknown as ServiceWorkerGlobalScope
2
+
3
+ let credential: string | undefined
4
+
5
+ serviceWorker.addEventListener('activate', (event) => {
6
+ event.waitUntil(serviceWorker.clients.claim())
7
+ })
8
+
9
+ serviceWorker.addEventListener('message', (event) => {
10
+ if (!event.source) return
11
+ const value = event.data?.credential
12
+ if (typeof value !== 'string' || !value.startsWith('Payment ')) return
13
+ credential = value
14
+ event.ports[0]?.postMessage('ack')
15
+ })
16
+
17
+ serviceWorker.addEventListener('fetch', (event) => {
18
+ if (!credential || event.request.mode !== 'navigate') return
19
+ if (new URL(event.request.url).origin !== serviceWorker.location.origin) return
20
+
21
+ const headers = new Headers(event.request.headers)
22
+ headers.set('Authorization', credential)
23
+ credential = undefined
24
+
25
+ event.respondWith(fetch(event.request, { headers }))
26
+ serviceWorker.registration.unregister()
27
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["es2022", "dom"],
5
+ "types": []
6
+ },
7
+ "include": ["serviceWorker.client.ts"]
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["es2022", "webworker"],
5
+ "types": []
6
+ },
7
+ "include": ["serviceWorker.ts"]
8
+ }
@@ -1,3 +1,5 @@
1
+ import * as StripeJsTypes from '../../stripe/server/internal/html/types.js'
2
+
1
3
  /**
2
4
  * Duck-typed interface for the Stripe Node SDK (`stripe` npm package).
3
5
  * Matches the subset of the API used by mppx for server-side payment verification.
@@ -24,3 +26,21 @@ export type StripeJs = {
24
26
  createPaymentMethod(...args: any[]): Promise<Record<string, unknown>>
25
27
  elements(...args: any[]): unknown
26
28
  }
29
+
30
+ export type CreatePaymentMethodFromElements = Omit<
31
+ StripeJsTypes.CreatePaymentMethodFromElements,
32
+ 'elements'
33
+ > & {}
34
+
35
+ export type StripeElementsOptionsMode = Omit<
36
+ Extract<StripeJsTypes.StripeElementsOptionsMode, { mode: 'payment' }>,
37
+ | 'amount'
38
+ | 'currency'
39
+ | 'mode'
40
+ | 'excludedPaymentMethodTypes'
41
+ | 'paymentMethodCreation'
42
+ | 'paymentMethodTypes'
43
+ | 'payment_method_types'
44
+ > & {}
45
+
46
+ export type StripePaymentElementOptions = StripeJsTypes.StripePaymentElementOptions