@obbyx/sdk 0.1.1 → 0.2.1
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/README.md +14 -0
- package/dist/client/paying-fetch.d.ts.map +1 -1
- package/dist/client/paying-fetch.js +68 -61
- package/dist/client/paying-fetch.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/seller/consumed-proofs.d.ts.map +1 -1
- package/dist/seller/consumed-proofs.js +8 -15
- package/dist/seller/consumed-proofs.js.map +1 -1
- package/dist/seller/cors.d.ts +33 -0
- package/dist/seller/cors.d.ts.map +1 -0
- package/dist/seller/cors.js +77 -0
- package/dist/seller/cors.js.map +1 -0
- package/dist/seller/paywall.d.ts +32 -0
- package/dist/seller/paywall.d.ts.map +1 -1
- package/dist/seller/paywall.js +183 -127
- package/dist/seller/paywall.js.map +1 -1
- package/dist/seller/x402-server.d.ts +48 -8
- package/dist/seller/x402-server.d.ts.map +1 -1
- package/dist/seller/x402-server.js +63 -22
- package/dist/seller/x402-server.js.map +1 -1
- package/package.json +3 -3
- package/src/client/paying-fetch.ts +96 -64
- package/src/index.ts +10 -1
- package/src/seller/consumed-proofs.ts +10 -16
- package/src/seller/cors.ts +106 -0
- package/src/seller/paywall.ts +237 -177
- package/src/seller/x402-server.ts +101 -29
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodePaymentSignatureHeader } from '@x402/core/http';
|
|
2
|
-
import { ObbyxError, fromDisplay, getNetwork, isAssetId, networkKey, parseAmount, } from '@obbyx/obyte';
|
|
2
|
+
import { CHANNEL_BASE_AA, ObbyxError, fromDisplay, getNetwork, isAssetId, networkKey, parseAmount, } from '@obbyx/obyte';
|
|
3
3
|
import { moduleLogger } from '@obbyx/obyte/logger';
|
|
4
|
-
import { EXACT_SCHEME, bindingsEqual, exactExtraSchema } from '@obbyx/x402-obyte';
|
|
4
|
+
import { BATCH_SCHEME, EXACT_SCHEME, batchExtraSchema, bindingsEqual, canonical, exactExtraSchema, sha256Hex, } from '@obbyx/x402-obyte';
|
|
5
5
|
import { DEFAULT_QUOTE_TTL_SECONDS, DEFAULT_SERVING_LEASE_SECONDS } from './defaults.js';
|
|
6
6
|
import { bodyHashOf, newQuote, offerBinding } from './quote.js';
|
|
7
7
|
function requestOf(transportContext) {
|
|
@@ -17,9 +17,10 @@ function requestOf(transportContext) {
|
|
|
17
17
|
paymentHeader: typeof request?.['paymentHeader'] === 'string' ? request['paymentHeader'] : undefined,
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
export class
|
|
20
|
+
export class ObyteSchemeServer {
|
|
21
21
|
options;
|
|
22
|
-
|
|
22
|
+
spec;
|
|
23
|
+
scheme;
|
|
23
24
|
defaultAssetTransferMethod = 'default';
|
|
24
25
|
paymentFlows = {
|
|
25
26
|
default: { supported: ['upfront'], default: 'upfront' },
|
|
@@ -28,22 +29,24 @@ export class ObyteExactServer {
|
|
|
28
29
|
schemeHooks;
|
|
29
30
|
log;
|
|
30
31
|
now;
|
|
31
|
-
constructor(options) {
|
|
32
|
+
constructor(options, spec) {
|
|
32
33
|
this.options = options;
|
|
33
|
-
this.
|
|
34
|
+
this.spec = spec;
|
|
35
|
+
this.scheme = spec.scheme;
|
|
36
|
+
this.log = moduleLogger(options.logger, 'sdk.x402-server', { scheme: spec.scheme });
|
|
34
37
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
35
38
|
this.schemeHooks = {
|
|
36
39
|
onBeforeVerify: async (ctx) => {
|
|
37
|
-
const
|
|
40
|
+
const key = this.keyOf(ctx);
|
|
38
41
|
const binding = ctx.requirements.extra.binding;
|
|
39
|
-
if (
|
|
42
|
+
if (key === undefined || typeof binding !== 'string') {
|
|
40
43
|
return { abort: true, reason: 'invalid_payload' };
|
|
41
44
|
}
|
|
42
|
-
const { record } = this.options.proofs.begin(
|
|
45
|
+
const { record } = this.options.proofs.begin(key, binding);
|
|
43
46
|
if (!bindingsEqual(record.binding, binding))
|
|
44
47
|
return { abort: true, reason: 'invalid_payment_reused' };
|
|
45
48
|
if (record.state === 'served') {
|
|
46
|
-
this.log.warn({ proofId:
|
|
49
|
+
this.log.warn({ proofId: key }, 'payment already served a request; refused');
|
|
47
50
|
return { abort: true, reason: 'payment_already_used' };
|
|
48
51
|
}
|
|
49
52
|
return undefined;
|
|
@@ -121,7 +124,7 @@ export class ObyteExactServer {
|
|
|
121
124
|
getAssetDecimals(asset) {
|
|
122
125
|
return asset === 'base' ? 9 : undefined;
|
|
123
126
|
}
|
|
124
|
-
/** Nothing to add from /supported
|
|
127
|
+
/** Nothing to add from /supported; the per-request fields come from the enrich hook. */
|
|
125
128
|
async enhancePaymentRequirements(requirements) {
|
|
126
129
|
return requirements;
|
|
127
130
|
}
|
|
@@ -129,21 +132,22 @@ export class ObyteExactServer {
|
|
|
129
132
|
enrichPaymentRequiredResponse = async (ctx) => {
|
|
130
133
|
const request = requestOf(ctx.transportContext);
|
|
131
134
|
if (request === undefined)
|
|
132
|
-
throw new ObbyxError('no_request',
|
|
135
|
+
throw new ObbyxError('no_request', `Obyte ${this.scheme} needs the HTTP request context`);
|
|
133
136
|
const accepted = this.acceptedQuote(request);
|
|
134
137
|
return ctx.requirements.map((req) => {
|
|
135
|
-
if (req.scheme !==
|
|
138
|
+
if (req.scheme !== this.spec.scheme)
|
|
136
139
|
return req;
|
|
137
140
|
const quote = accepted !== undefined &&
|
|
138
141
|
bindingsEqual(this.bindingFor(req, request, accepted), accepted.binding)
|
|
139
142
|
? accepted
|
|
140
143
|
: undefined;
|
|
144
|
+
const extra = { ...req.extra, ...this.spec.staticExtra };
|
|
141
145
|
if (quote !== undefined)
|
|
142
|
-
return { ...req, extra: { ...
|
|
146
|
+
return { ...req, extra: { ...extra, ...quote } };
|
|
143
147
|
const fresh = newQuote(this.now(), this.options.quoteTtlSeconds ?? DEFAULT_QUOTE_TTL_SECONDS);
|
|
144
148
|
return {
|
|
145
149
|
...req,
|
|
146
|
-
extra: { ...
|
|
150
|
+
extra: { ...extra, binding: this.bindingFor(req, request, fresh), ...fresh },
|
|
147
151
|
};
|
|
148
152
|
});
|
|
149
153
|
};
|
|
@@ -151,23 +155,60 @@ export class ObyteExactServer {
|
|
|
151
155
|
if (request.paymentHeader === undefined)
|
|
152
156
|
return undefined;
|
|
153
157
|
try {
|
|
154
|
-
const
|
|
155
|
-
|
|
158
|
+
const accepted = decodePaymentSignatureHeader(request.paymentHeader).accepted;
|
|
159
|
+
if (accepted?.scheme !== this.spec.scheme)
|
|
160
|
+
return undefined;
|
|
161
|
+
const extra = this.spec.acceptedExtra(accepted.extra);
|
|
162
|
+
return { quoteId: extra.quoteId, validUntil: extra.validUntil, binding: extra.binding };
|
|
156
163
|
}
|
|
157
164
|
catch {
|
|
158
165
|
return undefined;
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
bindingFor(req, request, quote) {
|
|
162
|
-
return offerBinding(this.options,
|
|
169
|
+
return offerBinding(this.options, this.spec.scheme, quote, { method: request.method, url: request.url, bodyHash: bodyHashOf(request.body) }, { network: req.network, asset: req.asset, amount: req.amount, payTo: req.payTo });
|
|
163
170
|
}
|
|
164
171
|
/** Proof key of the payment in a hook context (undefined when malformed). */
|
|
165
172
|
keyOf(ctx) {
|
|
166
|
-
const
|
|
167
|
-
return
|
|
173
|
+
const proof = this.spec.proofOf(ctx.paymentPayload.payload);
|
|
174
|
+
return proof === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: networkKey(getNetwork(ctx.requirements.network), proof.kind, proof.id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** `exact`: the proof is the unit (key `<network>:unit:<hash>`, as ObytePaywall). */
|
|
180
|
+
export class ObyteExactServer extends ObyteSchemeServer {
|
|
181
|
+
constructor(options) {
|
|
182
|
+
super(options, {
|
|
183
|
+
scheme: EXACT_SCHEME,
|
|
184
|
+
staticExtra: {},
|
|
185
|
+
acceptedExtra: (extra) => exactExtraSchema.parse(extra),
|
|
186
|
+
proofOf: (payload) => {
|
|
187
|
+
const unit = payload.unit?.unit;
|
|
188
|
+
return typeof unit === 'string' ? { kind: 'unit', id: unit } : undefined;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
168
191
|
}
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `batch-settlement` (AA payment channels): the proof is the voucher (key
|
|
195
|
+
* `<network>:voucher:<sha256 of the canonical signed package>`, as
|
|
196
|
+
* ObytePaywall). Offers carry the channel base AA and the minimum dispute
|
|
197
|
+
* window. Needs a facilitator with batch-settlement and the seller's watcher.
|
|
198
|
+
*/
|
|
199
|
+
export class ObyteBatchServer extends ObyteSchemeServer {
|
|
200
|
+
constructor(options) {
|
|
201
|
+
super(options, {
|
|
202
|
+
scheme: BATCH_SCHEME,
|
|
203
|
+
staticExtra: { baseAa: CHANNEL_BASE_AA, minTimeout: options.minChannelTimeoutSeconds },
|
|
204
|
+
acceptedExtra: (extra) => batchExtraSchema.parse(extra),
|
|
205
|
+
proofOf: (payload) => {
|
|
206
|
+
const pkg = payload['signedPackage'];
|
|
207
|
+
return pkg !== null && typeof pkg === 'object'
|
|
208
|
+
? { kind: 'voucher', id: sha256Hex(canonical(pkg)) }
|
|
209
|
+
: undefined;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
//# sourceMappingURL=x402-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x402-server.js","sourceRoot":"","sources":["../../src/seller/x402-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAU/D,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,SAAS,EACT,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAe,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,
|
|
1
|
+
{"version":3,"file":"x402-server.js","sourceRoot":"","sources":["../../src/seller/x402-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obbyx/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Accept and make x402 payments on Obyte: Express paywall, adapter for the official @x402 servers, paying fetch with spending budgets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"x402",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@obbyx/obyte": "^0.1.
|
|
47
|
-
"@obbyx/x402-obyte": "^0.2.
|
|
46
|
+
"@obbyx/obyte": "^0.1.1",
|
|
47
|
+
"@obbyx/x402-obyte": "^0.2.2",
|
|
48
48
|
"@x402/core": "^2.29.0",
|
|
49
49
|
"obyte": "0.2.1",
|
|
50
50
|
"zod": "^4.6.7"
|
|
@@ -5,7 +5,13 @@ import {
|
|
|
5
5
|
decodePaymentSignatureHeader,
|
|
6
6
|
encodePaymentSignatureHeader,
|
|
7
7
|
} from '@x402/core/http';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
PaymentPayload,
|
|
10
|
+
PaymentPayloadResult,
|
|
11
|
+
PaymentRequired,
|
|
12
|
+
PaymentRequirements,
|
|
13
|
+
SettleResponse,
|
|
14
|
+
} from '@x402/core/types';
|
|
9
15
|
import { ObbyxError } from '@obbyx/obyte';
|
|
10
16
|
import { type Logger, moduleLogger } from '@obbyx/obyte/logger';
|
|
11
17
|
import type { BudgetReservation, SpendingBudget } from './budget.js';
|
|
@@ -84,59 +90,13 @@ export async function payingFetch(
|
|
|
84
90
|
const required = response.headers.get('PAYMENT-REQUIRED');
|
|
85
91
|
if (required === null)
|
|
86
92
|
throw new ObbyxError('invalid_402', '402 without PAYMENT-REQUIRED header');
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const candidate of chooseOffers(offer.accepts, options)) {
|
|
95
|
-
const mechanism =
|
|
96
|
-
candidate.scheme === BATCH_SCHEME ? options.mechanisms.batch : options.mechanisms.exact;
|
|
97
|
-
if (mechanism === undefined) continue;
|
|
98
|
-
log.info(
|
|
99
|
-
{
|
|
100
|
-
url,
|
|
101
|
-
scheme: candidate.scheme,
|
|
102
|
-
payTo: candidate.payTo,
|
|
103
|
-
asset: candidate.asset,
|
|
104
|
-
amount: candidate.amount,
|
|
105
|
-
},
|
|
106
|
-
'paying for request',
|
|
107
|
-
);
|
|
108
|
-
// Reserve the price BEFORE signing: concurrent calls cannot overshoot the budget.
|
|
109
|
-
reservation = options.budget?.reserve(candidate.asset, BigInt(candidate.amount));
|
|
110
|
-
try {
|
|
111
|
-
result = await mechanism.createPaymentPayload(2, candidate);
|
|
112
|
-
accepted = candidate;
|
|
113
|
-
break;
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (reservation) options.budget?.release(reservation); // nothing was signed or sent
|
|
116
|
-
reservation = undefined;
|
|
117
|
-
if (err instanceof ObbyxError && CHANNEL_UNUSABLE.has(err.code)) {
|
|
118
|
-
lastChannelError = err;
|
|
119
|
-
log.info(
|
|
120
|
-
{ reason: err.code, detail: err.message },
|
|
121
|
-
'channel unusable; trying the next offer',
|
|
122
|
-
);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
throw err;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (accepted === undefined || result === undefined) {
|
|
129
|
-
// Nothing else to pay with: report why the channel could not (e.g. payment_pending).
|
|
130
|
-
throw lastChannelError ?? new ObbyxError('no_acceptable_offer', 'No offer could be paid');
|
|
131
|
-
}
|
|
132
|
-
const payload: PaymentPayload = {
|
|
133
|
-
x402Version: 2,
|
|
134
|
-
resource: offer.resource,
|
|
135
|
-
accepted,
|
|
136
|
-
payload: result.payload,
|
|
137
|
-
...(offer.extensions ? { extensions: withPaymentId(offer.extensions, init.paymentId) } : {}),
|
|
138
|
-
};
|
|
139
|
-
header = encodePaymentSignatureHeader(payload);
|
|
93
|
+
({ header, reservation } = await signPayment(
|
|
94
|
+
decodePaymentRequiredHeader(required),
|
|
95
|
+
init.paymentId,
|
|
96
|
+
url,
|
|
97
|
+
options,
|
|
98
|
+
log,
|
|
99
|
+
));
|
|
140
100
|
response = await request(header);
|
|
141
101
|
}
|
|
142
102
|
|
|
@@ -144,16 +104,7 @@ export async function payingFetch(
|
|
|
144
104
|
for (;;) {
|
|
145
105
|
const settle = readSettle(response);
|
|
146
106
|
if (response.status !== 402 || settle?.errorReason !== 'settlement_pending') {
|
|
147
|
-
|
|
148
|
-
if (accepted?.scheme === BATCH_SCHEME) options.mechanisms.batch?.onSettled(accepted, settle);
|
|
149
|
-
if (reservation !== undefined && options.budget !== undefined) {
|
|
150
|
-
// I7: spend on success; release ONLY on a definite refusal (402: the
|
|
151
|
-
// payment was not used). Anything else (5xx, 409, no settlement info)
|
|
152
|
-
// is an unknown outcome: the reservation stays counted.
|
|
153
|
-
if (settle?.success === true || (response.ok && settle === undefined))
|
|
154
|
-
options.budget.spend(reservation);
|
|
155
|
-
else if (response.status === 402) options.budget.release(reservation);
|
|
156
|
-
}
|
|
107
|
+
recordOutcome(response, settle, header, reservation, options);
|
|
157
108
|
return { response, ...(settle ? { payment: settle } : {}), paymentHeader: header };
|
|
158
109
|
}
|
|
159
110
|
if (Date.now() > deadline) throw new PaymentPendingError(header, settle.transaction);
|
|
@@ -169,6 +120,87 @@ export async function payingFetch(
|
|
|
169
120
|
}
|
|
170
121
|
}
|
|
171
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Signs a payment for the first usable offer, in preference order (channel
|
|
125
|
+
* first). A channel that turns out to be unusable before anything is signed
|
|
126
|
+
* (not funded, closing, exhausted, busy) falls through to the next offer,
|
|
127
|
+
* normally `exact`. The price is reserved in the budget BEFORE signing, so
|
|
128
|
+
* concurrent calls cannot overshoot it.
|
|
129
|
+
*/
|
|
130
|
+
async function signPayment(
|
|
131
|
+
offer: PaymentRequired,
|
|
132
|
+
paymentId: string | undefined,
|
|
133
|
+
url: string,
|
|
134
|
+
options: PayingFetchOptions,
|
|
135
|
+
log: Logger,
|
|
136
|
+
): Promise<{ header: string; reservation: BudgetReservation | undefined }> {
|
|
137
|
+
let lastChannelError: ObbyxError | undefined;
|
|
138
|
+
for (const candidate of chooseOffers(offer.accepts, options)) {
|
|
139
|
+
const mechanism =
|
|
140
|
+
candidate.scheme === BATCH_SCHEME ? options.mechanisms.batch : options.mechanisms.exact;
|
|
141
|
+
if (mechanism === undefined) continue;
|
|
142
|
+
log.info(
|
|
143
|
+
{
|
|
144
|
+
url,
|
|
145
|
+
scheme: candidate.scheme,
|
|
146
|
+
payTo: candidate.payTo,
|
|
147
|
+
asset: candidate.asset,
|
|
148
|
+
amount: candidate.amount,
|
|
149
|
+
},
|
|
150
|
+
'paying for request',
|
|
151
|
+
);
|
|
152
|
+
const reservation = options.budget?.reserve(candidate.asset, BigInt(candidate.amount));
|
|
153
|
+
let result: PaymentPayloadResult;
|
|
154
|
+
try {
|
|
155
|
+
result = await mechanism.createPaymentPayload(2, candidate);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (reservation) options.budget?.release(reservation); // nothing was signed or sent
|
|
158
|
+
if (err instanceof ObbyxError && CHANNEL_UNUSABLE.has(err.code)) {
|
|
159
|
+
lastChannelError = err;
|
|
160
|
+
log.info(
|
|
161
|
+
{ reason: err.code, detail: err.message },
|
|
162
|
+
'channel unusable; trying the next offer',
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
const payload: PaymentPayload = {
|
|
169
|
+
x402Version: 2,
|
|
170
|
+
resource: offer.resource,
|
|
171
|
+
accepted: candidate,
|
|
172
|
+
payload: result.payload,
|
|
173
|
+
...(offer.extensions ? { extensions: withPaymentId(offer.extensions, paymentId) } : {}),
|
|
174
|
+
};
|
|
175
|
+
return { header: encodePaymentSignatureHeader(payload), reservation };
|
|
176
|
+
}
|
|
177
|
+
// Nothing else to pay with: report why the channel could not (e.g. payment_pending).
|
|
178
|
+
throw lastChannelError ?? new ObbyxError('no_acceptable_offer', 'No offer could be paid');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Applies the final answer: the channel voucher is confirmed or released, and
|
|
183
|
+
* the budget reservation is spent on success and released ONLY on a definite
|
|
184
|
+
* refusal (402: the payment was not used). Anything else (5xx, 409, no
|
|
185
|
+
* settlement info) is an unknown outcome: the reservation stays counted (I7).
|
|
186
|
+
*/
|
|
187
|
+
function recordOutcome(
|
|
188
|
+
response: Response,
|
|
189
|
+
settle: SettleResponse | undefined,
|
|
190
|
+
header: string,
|
|
191
|
+
reservation: BudgetReservation | undefined,
|
|
192
|
+
options: PayingFetchOptions,
|
|
193
|
+
): void {
|
|
194
|
+
const accepted = acceptedOf(header);
|
|
195
|
+
if (accepted?.scheme === BATCH_SCHEME) options.mechanisms.batch?.onSettled(accepted, settle);
|
|
196
|
+
if (reservation === undefined || options.budget === undefined) return;
|
|
197
|
+
if (settle?.success === true || (response.ok && settle === undefined)) {
|
|
198
|
+
options.budget.spend(reservation);
|
|
199
|
+
} else if (response.status === 402) {
|
|
200
|
+
options.budget.release(reservation);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
172
204
|
/** An offer from an untrusted seller: integer amount in smallest units, strings elsewhere. */
|
|
173
205
|
const offerShape = z
|
|
174
206
|
.object({
|
package/src/index.ts
CHANGED
|
@@ -32,4 +32,13 @@ export {
|
|
|
32
32
|
type BudgetReservation,
|
|
33
33
|
SpendingBudget,
|
|
34
34
|
} from './client/budget.js';
|
|
35
|
-
export {
|
|
35
|
+
export {
|
|
36
|
+
ObyteBatchServer,
|
|
37
|
+
type ObyteBatchServerOptions,
|
|
38
|
+
ObyteExactServer,
|
|
39
|
+
type ObyteExactServerOptions,
|
|
40
|
+
ObyteSchemeServer,
|
|
41
|
+
type ObyteSchemeSpec,
|
|
42
|
+
type ObyteSchemeServerOptions,
|
|
43
|
+
} from './seller/x402-server.js';
|
|
44
|
+
export type { PaywallCorsOptions } from './seller/cors.js';
|
|
@@ -181,22 +181,16 @@ export class ConsumedProofsStore {
|
|
|
181
181
|
*/
|
|
182
182
|
markSettled(key: string, binding: string, paymentResponse: string): ProofState {
|
|
183
183
|
const ts = this.now();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
.run(key, binding, paymentResponse, ts, ts);
|
|
195
|
-
this.db.exec('COMMIT');
|
|
196
|
-
} catch (err) {
|
|
197
|
-
this.db.exec('ROLLBACK');
|
|
198
|
-
throw err;
|
|
199
|
-
}
|
|
184
|
+
// One UPSERT statement: atomic on its own.
|
|
185
|
+
this.db
|
|
186
|
+
.prepare(
|
|
187
|
+
`INSERT INTO consumed_proofs (key, binding, state, payment_response, created_at, updated_at)
|
|
188
|
+
VALUES (?, ?, 'paid_not_served', ?, ?, ?)
|
|
189
|
+
ON CONFLICT(key) DO UPDATE SET state = 'paid_not_served', payment_response = excluded.payment_response,
|
|
190
|
+
updated_at = excluded.updated_at
|
|
191
|
+
WHERE consumed_proofs.state = 'settling'`,
|
|
192
|
+
)
|
|
193
|
+
.run(key, binding, paymentResponse, ts, ts);
|
|
200
194
|
const record = this.get(key);
|
|
201
195
|
if (record === undefined) throw new Error(`consumed proof ${key} vanished`);
|
|
202
196
|
return record.state;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Request, RequestHandler, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CORS for browser clients of a paid route (x402 `transports-v2/http.md`,
|
|
5
|
+
* "Browser clients"). None of the x402 headers is CORS-safelisted: without
|
|
6
|
+
* these headers a cross-origin page cannot read the price (402) or send the
|
|
7
|
+
* payment. CORS decides who may READ a response; it is not a payment control.
|
|
8
|
+
*/
|
|
9
|
+
export interface PaywallCorsOptions {
|
|
10
|
+
/** Allowed origins (exact `scheme://host[:port]`), or `'*'` for any. */
|
|
11
|
+
readonly origins: readonly string[] | '*';
|
|
12
|
+
/** Whether requests may carry credentials (cookies); origins are then reflected, never `*`. */
|
|
13
|
+
readonly credentials?: boolean;
|
|
14
|
+
/** Preflight cache lifetime (default 600 s). */
|
|
15
|
+
readonly maxAgeSeconds?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Response headers a browser client must read: the price and the settlement. */
|
|
19
|
+
export const EXPOSED_HEADERS = ['PAYMENT-REQUIRED', 'PAYMENT-RESPONSE'] as const;
|
|
20
|
+
/** Request headers a browser client sends: the payment, and Content-Type for a JSON body. */
|
|
21
|
+
export const ALLOWED_HEADERS = ['PAYMENT-SIGNATURE', 'Content-Type'] as const;
|
|
22
|
+
const DEFAULT_MAX_AGE_SECONDS = 600;
|
|
23
|
+
|
|
24
|
+
/** Merges header names into a comma-separated list, keeping existing ones (case-insensitive). */
|
|
25
|
+
export function mergeHeaderList(
|
|
26
|
+
existing: string | number | readonly string[] | undefined,
|
|
27
|
+
add: readonly string[],
|
|
28
|
+
): string {
|
|
29
|
+
const current =
|
|
30
|
+
existing === undefined
|
|
31
|
+
? []
|
|
32
|
+
: (Array.isArray(existing) ? existing.join(',') : String(existing))
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((h) => h.trim())
|
|
35
|
+
.filter((h) => h.length > 0);
|
|
36
|
+
const seen = new Set(current.map((h) => h.toLowerCase()));
|
|
37
|
+
for (const h of add) {
|
|
38
|
+
if (!seen.has(h.toLowerCase())) {
|
|
39
|
+
current.push(h);
|
|
40
|
+
seen.add(h.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return current.join(', ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Always: lets a cross-origin page read the x402 headers (merged with any value already set). */
|
|
47
|
+
export function exposePaymentHeaders(res: Response): void {
|
|
48
|
+
res.setHeader(
|
|
49
|
+
'Access-Control-Expose-Headers',
|
|
50
|
+
mergeHeaderList(res.getHeader('Access-Control-Expose-Headers'), EXPOSED_HEADERS),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Sets `Access-Control-Allow-Origin` for an allowed calling origin; false when not allowed. */
|
|
55
|
+
export function allowOrigin(req: Request, res: Response, cors: PaywallCorsOptions): boolean {
|
|
56
|
+
const origin = req.get('origin');
|
|
57
|
+
if (origin === undefined) return false;
|
|
58
|
+
const anyOrigin = cors.origins === '*';
|
|
59
|
+
if (!anyOrigin && !cors.origins.includes(origin)) return false;
|
|
60
|
+
if (anyOrigin && cors.credentials !== true) {
|
|
61
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
62
|
+
} else {
|
|
63
|
+
// A reflected origin varies per request: shared caches must key on it.
|
|
64
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
65
|
+
res.setHeader('Vary', mergeHeaderList(res.getHeader('Vary'), ['Origin']));
|
|
66
|
+
}
|
|
67
|
+
if (cors.credentials === true) res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Answers the CORS preflight (`OPTIONS`) of paid routes without requiring
|
|
73
|
+
* payment. Allows the x402 request headers plus whatever else the browser
|
|
74
|
+
* announces: x402 client SDKs have added request headers of their own, and an
|
|
75
|
+
* allow list naming exactly the protocol headers would refuse their retry.
|
|
76
|
+
*/
|
|
77
|
+
export function preflightHandler(cors: PaywallCorsOptions): RequestHandler {
|
|
78
|
+
return (req, res, next) => {
|
|
79
|
+
if (req.method !== 'OPTIONS' || req.get('access-control-request-method') === undefined) {
|
|
80
|
+
next();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!allowOrigin(req, res, cors)) {
|
|
84
|
+
res.status(204).end(); // no CORS headers: the browser refuses the call
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const requested = (req.get('access-control-request-headers') ?? '')
|
|
88
|
+
.split(',')
|
|
89
|
+
.map((h) => h.trim())
|
|
90
|
+
.filter((h) => h.length > 0);
|
|
91
|
+
res.setHeader(
|
|
92
|
+
'Access-Control-Allow-Headers',
|
|
93
|
+
mergeHeaderList(undefined, [...ALLOWED_HEADERS, ...requested]),
|
|
94
|
+
);
|
|
95
|
+
res.setHeader(
|
|
96
|
+
'Access-Control-Allow-Methods',
|
|
97
|
+
mergeHeaderList(undefined, [
|
|
98
|
+
req.get('access-control-request-method') ?? 'POST',
|
|
99
|
+
'GET',
|
|
100
|
+
'POST',
|
|
101
|
+
]),
|
|
102
|
+
);
|
|
103
|
+
res.setHeader('Access-Control-Max-Age', String(cors.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS));
|
|
104
|
+
res.status(204).end();
|
|
105
|
+
};
|
|
106
|
+
}
|