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.
- package/CHANGELOG.md +20 -0
- package/dist/Credential.d.ts +12 -0
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js +22 -4
- package/dist/Credential.js.map +1 -1
- package/dist/Method.d.ts +4 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +2 -1
- package/dist/Method.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +11 -9
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +3 -3
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +10 -5
- package/dist/cli/utils.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +52 -8
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +7 -3
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -71
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +5 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +71 -7
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/internal/html/config.d.ts +144 -0
- package/dist/server/internal/html/config.d.ts.map +1 -0
- package/dist/server/internal/html/config.js +303 -0
- package/dist/server/internal/html/config.js.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts +2 -0
- package/dist/server/internal/html/serviceWorker.gen.d.ts.map +1 -0
- package/dist/server/internal/html/serviceWorker.gen.js +3 -0
- package/dist/server/internal/html/serviceWorker.gen.js.map +1 -0
- package/dist/stripe/internal/types.d.ts +6 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +30 -16
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +35 -6
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html/types.d.ts +2 -0
- package/dist/stripe/server/internal/html/types.d.ts.map +1 -0
- package/dist/stripe/server/internal/html/types.js +2 -0
- package/dist/stripe/server/internal/html/types.js.map +1 -0
- package/dist/stripe/server/internal/html.gen.d.ts +2 -0
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -0
- package/dist/stripe/server/internal/html.gen.js +3 -0
- package/dist/stripe/server/internal/html.gen.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +33 -26
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +46 -11
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +3 -2
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +2 -0
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -0
- package/dist/tempo/server/internal/html.gen.js +3 -0
- package/dist/tempo/server/internal/html.gen.js.map +1 -0
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +45 -58
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/Credential.ts +28 -4
- package/src/Method.ts +6 -1
- package/src/cli/cli.ts +11 -8
- package/src/cli/plugins/tempo.ts +3 -2
- package/src/cli/utils.test.ts +64 -0
- package/src/cli/utils.ts +10 -4
- package/src/env.d.ts +1 -0
- package/src/mcp-sdk/server/Transport.test.ts +6 -0
- package/src/proxy/Proxy.test.ts +188 -1
- package/src/proxy/Proxy.ts +58 -9
- package/src/proxy/internal/Route.test.ts +9 -0
- package/src/proxy/internal/Route.ts +5 -2
- package/src/server/Mppx.test.ts +171 -18
- package/src/server/Mppx.ts +120 -79
- package/src/server/Transport.test.ts +232 -2
- package/src/server/Transport.ts +84 -7
- package/src/server/internal/html/config.ts +414 -0
- package/src/server/internal/html/serviceWorker.client.ts +28 -0
- package/src/server/internal/html/serviceWorker.gen.ts +2 -0
- package/src/server/internal/html/serviceWorker.ts +27 -0
- package/src/server/internal/html/tsconfig.worker.client.json +8 -0
- package/src/server/internal/html/tsconfig.worker.json +8 -0
- package/src/stripe/internal/types.ts +20 -0
- package/src/stripe/server/Charge.ts +62 -6
- package/src/stripe/server/internal/html/main.ts +174 -0
- package/src/stripe/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/stripe/server/internal/html/package.json +9 -0
- package/src/stripe/server/internal/html/stripe-js-pure.d.ts +7 -0
- package/src/stripe/server/internal/html/tsconfig.json +8 -0
- package/src/stripe/server/internal/html/types.ts +5 -0
- package/src/stripe/server/internal/html.gen.ts +2 -0
- package/src/tempo/server/Charge.ts +64 -10
- package/src/tempo/server/Session.ts +3 -2
- package/src/tempo/server/internal/html/main.ts +111 -0
- package/src/tempo/server/internal/html/node_modules/.bin/mppx.src +21 -0
- package/src/tempo/server/internal/html/package.json +10 -0
- package/src/tempo/server/internal/html/tsconfig.json +8 -0
- package/src/tempo/server/internal/html.gen.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +37 -31
- package/src/tempo/server/internal/transport.ts +44 -58
- package/src/tsconfig.json +1 -1
package/src/server/Transport.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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, '&')
|
|
49
|
+
.replace(/</g, '<')
|
|
50
|
+
.replace(/>/g, '>')
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/'/g, ''')
|
|
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
|
+
})
|
|
@@ -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
|