@obbyx/sdk 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/client/budget.d.ts +43 -0
- package/dist/client/budget.d.ts.map +1 -0
- package/dist/client/budget.js +56 -0
- package/dist/client/budget.js.map +1 -0
- package/dist/client/paying-fetch.d.ts +46 -0
- package/dist/client/paying-fetch.d.ts.map +1 -0
- package/dist/client/paying-fetch.js +186 -0
- package/dist/client/paying-fetch.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/seller/binding.d.ts +2 -0
- package/dist/seller/binding.d.ts.map +1 -0
- package/dist/seller/binding.js +3 -0
- package/dist/seller/binding.js.map +1 -0
- package/dist/seller/consumed-proofs.d.ts +72 -0
- package/dist/seller/consumed-proofs.d.ts.map +1 -0
- package/dist/seller/consumed-proofs.js +123 -0
- package/dist/seller/consumed-proofs.js.map +1 -0
- package/dist/seller/payment-identifier.d.ts +73 -0
- package/dist/seller/payment-identifier.d.ts.map +1 -0
- package/dist/seller/payment-identifier.js +75 -0
- package/dist/seller/payment-identifier.js.map +1 -0
- package/dist/seller/paywall.d.ts +99 -0
- package/dist/seller/paywall.d.ts.map +1 -0
- package/dist/seller/paywall.js +372 -0
- package/dist/seller/paywall.js.map +1 -0
- package/dist/seller/quote.d.ts +38 -0
- package/dist/seller/quote.d.ts.map +1 -0
- package/dist/seller/quote.js +39 -0
- package/dist/seller/quote.js.map +1 -0
- package/dist/seller/x402-server.d.ts +57 -0
- package/dist/seller/x402-server.d.ts.map +1 -0
- package/dist/seller/x402-server.js +152 -0
- package/dist/seller/x402-server.js.map +1 -0
- package/package.json +68 -3
- package/src/client/budget.ts +88 -0
- package/src/client/paying-fetch.ts +256 -0
- package/src/index.ts +35 -0
- package/src/seller/binding.ts +8 -0
- package/src/seller/consumed-proofs.ts +217 -0
- package/src/seller/payment-identifier.ts +104 -0
- package/src/seller/paywall.ts +566 -0
- package/src/seller/quote.ts +82 -0
- package/src/seller/x402-server.ts +224 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
export class ConsumedProofsStore {
|
|
3
|
+
now;
|
|
4
|
+
db;
|
|
5
|
+
constructor(path, now = () => Math.floor(Date.now() / 1000)) {
|
|
6
|
+
this.now = now;
|
|
7
|
+
this.db = new DatabaseSync(path);
|
|
8
|
+
this.db.exec(`
|
|
9
|
+
PRAGMA journal_mode = WAL;
|
|
10
|
+
PRAGMA synchronous = FULL;
|
|
11
|
+
PRAGMA busy_timeout = 5000;
|
|
12
|
+
CREATE TABLE IF NOT EXISTS consumed_proofs (
|
|
13
|
+
key TEXT PRIMARY KEY,
|
|
14
|
+
binding TEXT NOT NULL,
|
|
15
|
+
state TEXT NOT NULL,
|
|
16
|
+
payment_response TEXT,
|
|
17
|
+
response_status INTEGER,
|
|
18
|
+
response_headers TEXT,
|
|
19
|
+
response_body BLOB,
|
|
20
|
+
created_at INTEGER NOT NULL,
|
|
21
|
+
updated_at INTEGER NOT NULL
|
|
22
|
+
) STRICT;
|
|
23
|
+
CREATE TABLE IF NOT EXISTS payment_ids (
|
|
24
|
+
scope_id TEXT PRIMARY KEY,
|
|
25
|
+
fingerprint TEXT NOT NULL,
|
|
26
|
+
proof_key TEXT NOT NULL,
|
|
27
|
+
created_at INTEGER NOT NULL
|
|
28
|
+
) STRICT;
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* x402 `payment-identifier`: remembers which proof served a client-chosen id
|
|
33
|
+
* for one operation fingerprint. Returns the stored mapping (existing or new).
|
|
34
|
+
*/
|
|
35
|
+
bindPaymentId(scopeId, fingerprint, proofKey) {
|
|
36
|
+
const res = this.db
|
|
37
|
+
.prepare('INSERT INTO payment_ids (scope_id, fingerprint, proof_key, created_at) VALUES (?, ?, ?, ?) ON CONFLICT(scope_id) DO NOTHING')
|
|
38
|
+
.run(scopeId, fingerprint, proofKey, this.now());
|
|
39
|
+
const row = this.db
|
|
40
|
+
.prepare('SELECT fingerprint, proof_key FROM payment_ids WHERE scope_id = ?')
|
|
41
|
+
.get(scopeId);
|
|
42
|
+
return {
|
|
43
|
+
fingerprint: row.fingerprint,
|
|
44
|
+
proofKey: row.proof_key,
|
|
45
|
+
created: Number(res.changes) === 1,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Compare-and-set: moves the id only if it still points at `previous` (one winner). */
|
|
49
|
+
rebindPaymentId(scopeId, previous, proofKey) {
|
|
50
|
+
const res = this.db
|
|
51
|
+
.prepare('UPDATE payment_ids SET proof_key = ? WHERE scope_id = ? AND proof_key = ?')
|
|
52
|
+
.run(proofKey, scopeId, previous);
|
|
53
|
+
return Number(res.changes) === 1;
|
|
54
|
+
}
|
|
55
|
+
close() {
|
|
56
|
+
this.db.close();
|
|
57
|
+
}
|
|
58
|
+
get(key) {
|
|
59
|
+
const row = this.db.prepare('SELECT * FROM consumed_proofs WHERE key = ?').get(key);
|
|
60
|
+
return row === undefined ? undefined : toRecord(row);
|
|
61
|
+
}
|
|
62
|
+
/** Inserts the proof in `settling` unless it exists; returns the stored record. */
|
|
63
|
+
begin(key, binding) {
|
|
64
|
+
const ts = this.now();
|
|
65
|
+
const res = this.db
|
|
66
|
+
.prepare(`INSERT INTO consumed_proofs (key, binding, state, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, 'settling', ?, ?) ON CONFLICT(key) DO NOTHING`)
|
|
68
|
+
.run(key, binding, ts, ts);
|
|
69
|
+
const record = this.get(key);
|
|
70
|
+
if (record === undefined)
|
|
71
|
+
throw new Error(`consumed proof ${key} vanished`);
|
|
72
|
+
return { created: Number(res.changes) === 1, record };
|
|
73
|
+
}
|
|
74
|
+
/** Compare-and-set; returns false if the proof is not in `from`. */
|
|
75
|
+
transition(key, from, to, patch = {}) {
|
|
76
|
+
const res = this.db
|
|
77
|
+
.prepare(`UPDATE consumed_proofs
|
|
78
|
+
SET state = ?, updated_at = ?,
|
|
79
|
+
payment_response = COALESCE(?, payment_response),
|
|
80
|
+
response_status = COALESCE(?, response_status),
|
|
81
|
+
response_headers = COALESCE(?, response_headers),
|
|
82
|
+
response_body = COALESCE(?, response_body)
|
|
83
|
+
WHERE key = ? AND state = ?`)
|
|
84
|
+
.run(to, this.now(), patch.paymentResponse ?? null, patch.response?.status ?? null, patch.response === undefined ? null : JSON.stringify(patch.response.headers), patch.response?.body ?? null, key, from);
|
|
85
|
+
return Number(res.changes) === 1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Recovers a proof stuck in `serving` (the process died while the handler
|
|
89
|
+
* ran): after `leaseSeconds` it goes back to `paid_not_served` (already
|
|
90
|
+
* settled) or `settling` (authorization flow, not settled yet), so the SAME
|
|
91
|
+
* payment can complete the operation. Compare-and-set: one winner.
|
|
92
|
+
*/
|
|
93
|
+
reclaimStale(key, leaseSeconds) {
|
|
94
|
+
const res = this.db
|
|
95
|
+
.prepare(`UPDATE consumed_proofs
|
|
96
|
+
SET state = CASE WHEN payment_response IS NULL THEN 'settling' ELSE 'paid_not_served' END,
|
|
97
|
+
updated_at = ?
|
|
98
|
+
WHERE key = ? AND state = 'serving' AND updated_at <= ?`)
|
|
99
|
+
.run(this.now(), key, this.now() - leaseSeconds);
|
|
100
|
+
return Number(res.changes) === 1;
|
|
101
|
+
}
|
|
102
|
+
/** Removes a proof that never reached the network (e.g. rejected at verification). */
|
|
103
|
+
forget(key, state) {
|
|
104
|
+
this.db.prepare('DELETE FROM consumed_proofs WHERE key = ? AND state = ?').run(key, state);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function toRecord(row) {
|
|
108
|
+
return {
|
|
109
|
+
key: row.key,
|
|
110
|
+
binding: row.binding,
|
|
111
|
+
state: row.state,
|
|
112
|
+
paymentResponse: row.payment_response,
|
|
113
|
+
response: row.response_status === null
|
|
114
|
+
? null
|
|
115
|
+
: {
|
|
116
|
+
status: row.response_status,
|
|
117
|
+
headers: JSON.parse(row.response_headers ?? '{}'),
|
|
118
|
+
body: row.response_body ?? new Uint8Array(),
|
|
119
|
+
},
|
|
120
|
+
updatedAt: row.updated_at,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=consumed-proofs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumed-proofs.js","sourceRoot":"","sources":["../../src/seller/consumed-proofs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PaymentPayload } from '@x402/core/types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* x402 `payment-identifier` extension (seller side): a client-chosen id makes
|
|
5
|
+
* a repeated operation return the stored response instead of charging again.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PAYMENT_IDENTIFIER = "payment-identifier";
|
|
8
|
+
/** Same constraints as @x402/extensions (16–128 chars of [A-Za-z0-9_-]). */
|
|
9
|
+
export declare const paymentIdSchema: z.ZodString;
|
|
10
|
+
/** Same declaration as @x402/extensions `declarePaymentIdentifierExtension`. */
|
|
11
|
+
export declare function paymentIdentifierDeclaration(required: boolean): {
|
|
12
|
+
info: {
|
|
13
|
+
required: boolean;
|
|
14
|
+
};
|
|
15
|
+
schema: {
|
|
16
|
+
$schema: string;
|
|
17
|
+
type: string;
|
|
18
|
+
properties: {
|
|
19
|
+
required: {
|
|
20
|
+
type: string;
|
|
21
|
+
};
|
|
22
|
+
id: {
|
|
23
|
+
type: string;
|
|
24
|
+
minLength: number;
|
|
25
|
+
maxLength: number;
|
|
26
|
+
pattern: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
required: string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** The client's id: `undefined` when absent, `'invalid'` when malformed. */
|
|
33
|
+
export declare function paymentIdOf(payload: PaymentPayload): string | undefined | 'invalid';
|
|
34
|
+
/** What the store needs for payment-identifier decisions. */
|
|
35
|
+
export interface PaymentIdStore {
|
|
36
|
+
bindPaymentId(scopeId: string, fingerprint: string, proofKey: string): {
|
|
37
|
+
fingerprint: string;
|
|
38
|
+
proofKey: string;
|
|
39
|
+
created: boolean;
|
|
40
|
+
};
|
|
41
|
+
/** Compare-and-set: moves the id to `proofKey` only if it still points at `previous`. */
|
|
42
|
+
rebindPaymentId(scopeId: string, previous: string, proofKey: string): boolean;
|
|
43
|
+
get(key: string): {
|
|
44
|
+
state: string;
|
|
45
|
+
response: unknown;
|
|
46
|
+
paymentResponse: string | null;
|
|
47
|
+
} | undefined;
|
|
48
|
+
}
|
|
49
|
+
export type PaymentIdDecision = {
|
|
50
|
+
kind: 'proceed';
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'replay';
|
|
53
|
+
proofKey: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'reject';
|
|
56
|
+
status: 400 | 409;
|
|
57
|
+
error: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* One client id = one operation. Contract: the caller has already created the
|
|
61
|
+
* proof row for `proofKey` (so other requests see it as in progress).
|
|
62
|
+
* Concurrent retries with DIFFERENT payments
|
|
63
|
+
* for the same id: exactly one wins the compare-and-set; the others get 409,
|
|
64
|
+
* so an id can never be charged twice.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decidePaymentId(store: PaymentIdStore, input: {
|
|
67
|
+
paymentId: string | undefined | 'invalid';
|
|
68
|
+
required: boolean;
|
|
69
|
+
scopeId: (id: string) => string;
|
|
70
|
+
fingerprint: string;
|
|
71
|
+
proofKey: string;
|
|
72
|
+
}): PaymentIdDecision;
|
|
73
|
+
//# sourceMappingURL=payment-identifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-identifier.d.ts","sourceRoot":"","sources":["../../src/seller/payment-identifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,4EAA4E;AAC5E,eAAO,MAAM,eAAe,aAA8C,CAAC;AAM3E,gFAAgF;AAChF,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;EAa7D;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CASnF;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,aAAa,CACX,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/D,yFAAyF;IACzF,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9E,GAAG,CACD,GAAG,EAAE,MAAM,GACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC;CACrF;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE;IACL,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1C,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,iBAAiB,CAwBnB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* x402 `payment-identifier` extension (seller side): a client-chosen id makes
|
|
4
|
+
* a repeated operation return the stored response instead of charging again.
|
|
5
|
+
*/
|
|
6
|
+
export const PAYMENT_IDENTIFIER = 'payment-identifier';
|
|
7
|
+
/** Same constraints as @x402/extensions (16–128 chars of [A-Za-z0-9_-]). */
|
|
8
|
+
export const paymentIdSchema = z.string().regex(/^[a-zA-Z0-9_-]{16,128}$/);
|
|
9
|
+
const extensionSchema = z
|
|
10
|
+
.object({ info: z.object({ id: z.unknown().optional() }).loose() })
|
|
11
|
+
.loose();
|
|
12
|
+
/** Same declaration as @x402/extensions `declarePaymentIdentifierExtension`. */
|
|
13
|
+
export function paymentIdentifierDeclaration(required) {
|
|
14
|
+
return {
|
|
15
|
+
info: { required },
|
|
16
|
+
schema: {
|
|
17
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
required: { type: 'boolean' },
|
|
21
|
+
id: { type: 'string', minLength: 16, maxLength: 128, pattern: '^[a-zA-Z0-9_-]+$' },
|
|
22
|
+
},
|
|
23
|
+
required: ['required'],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The client's id: `undefined` when absent, `'invalid'` when malformed. */
|
|
28
|
+
export function paymentIdOf(payload) {
|
|
29
|
+
const raw = payload.extensions?.[PAYMENT_IDENTIFIER];
|
|
30
|
+
if (raw === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const ext = extensionSchema.safeParse(raw);
|
|
33
|
+
if (!ext.success)
|
|
34
|
+
return 'invalid';
|
|
35
|
+
const id = ext.data.info.id;
|
|
36
|
+
if (id === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const parsed = paymentIdSchema.safeParse(id);
|
|
39
|
+
return parsed.success ? parsed.data : 'invalid';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One client id = one operation. Contract: the caller has already created the
|
|
43
|
+
* proof row for `proofKey` (so other requests see it as in progress).
|
|
44
|
+
* Concurrent retries with DIFFERENT payments
|
|
45
|
+
* for the same id: exactly one wins the compare-and-set; the others get 409,
|
|
46
|
+
* so an id can never be charged twice.
|
|
47
|
+
*/
|
|
48
|
+
export function decidePaymentId(store, input) {
|
|
49
|
+
const { paymentId } = input;
|
|
50
|
+
if (paymentId === 'invalid')
|
|
51
|
+
return { kind: 'reject', status: 400, error: 'invalid_payment_identifier' };
|
|
52
|
+
if (paymentId === undefined) {
|
|
53
|
+
return input.required
|
|
54
|
+
? { kind: 'reject', status: 400, error: 'payment_identifier_required' }
|
|
55
|
+
: { kind: 'proceed' };
|
|
56
|
+
}
|
|
57
|
+
const scopeId = input.scopeId(paymentId);
|
|
58
|
+
const mapping = store.bindPaymentId(scopeId, input.fingerprint, input.proofKey);
|
|
59
|
+
if (mapping.fingerprint !== input.fingerprint) {
|
|
60
|
+
return { kind: 'reject', status: 409, error: 'payment_identifier_conflict' };
|
|
61
|
+
}
|
|
62
|
+
if (mapping.proofKey === input.proofKey)
|
|
63
|
+
return { kind: 'proceed' };
|
|
64
|
+
const original = store.get(mapping.proofKey);
|
|
65
|
+
if (original?.state === 'served')
|
|
66
|
+
return { kind: 'replay', proofKey: mapping.proofKey };
|
|
67
|
+
if (original !== undefined && original.state !== 'failed') {
|
|
68
|
+
return { kind: 'reject', status: 409, error: 'payment_in_progress' };
|
|
69
|
+
}
|
|
70
|
+
// The earlier attempt definitely failed (or never consumed its proof): move the id.
|
|
71
|
+
return store.rebindPaymentId(scopeId, mapping.proofKey, input.proofKey)
|
|
72
|
+
? { kind: 'proceed' }
|
|
73
|
+
: { kind: 'reject', status: 409, error: 'payment_in_progress' };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=payment-identifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-identifier.js","sourceRoot":"","sources":["../../src/seller/payment-identifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Request, RequestHandler } from 'express';
|
|
2
|
+
import { type FacilitatorClient } from '@x402/core/http';
|
|
3
|
+
import { type Logger } from '@obbyx/obyte/logger';
|
|
4
|
+
import { BATCH_SCHEME, EXACT_SCHEME } from '@obbyx/x402-obyte';
|
|
5
|
+
import type { ConsumedProofsStore } from './consumed-proofs.js';
|
|
6
|
+
/**
|
|
7
|
+
* Express paywall for x402 `exact` payments on Obyte (seller side).
|
|
8
|
+
*
|
|
9
|
+
* Flow `upfront` (phase 3): settle to the facilitator's finality BEFORE the
|
|
10
|
+
* handler runs; the handler runs at most once per payment proof; a repeat of
|
|
11
|
+
* the same operation with the same proof returns the stored response.
|
|
12
|
+
*
|
|
13
|
+
* Paid handlers return their response instead of writing to `res`, so the
|
|
14
|
+
* paywall can store it for idempotent replays.
|
|
15
|
+
*/
|
|
16
|
+
export interface PaidResult {
|
|
17
|
+
readonly status?: number;
|
|
18
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
19
|
+
/** Objects are sent as JSON; strings/bytes as-is. */
|
|
20
|
+
readonly body: unknown;
|
|
21
|
+
}
|
|
22
|
+
export type PaidHandler = (req: Request) => Promise<PaidResult> | PaidResult;
|
|
23
|
+
export interface RoutePrice {
|
|
24
|
+
/** Atomic units as a decimal string. */
|
|
25
|
+
readonly amount: string;
|
|
26
|
+
/** `'base'` for bytes or an asset id. */
|
|
27
|
+
readonly asset?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PaidRouteOptions {
|
|
30
|
+
readonly price: RoutePrice;
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
readonly mimeType?: string;
|
|
33
|
+
/** What identifies "the same operation". Default: sha256 of the canonical JSON body. */
|
|
34
|
+
readonly fingerprint?: (req: Request) => string;
|
|
35
|
+
/** Offered schemes in order of preference. Default: batch-settlement (if enabled), exact. */
|
|
36
|
+
readonly schemes?: readonly PaywallScheme[];
|
|
37
|
+
/**
|
|
38
|
+
* `upfront` (default): settle, then run the handler — for anything expensive
|
|
39
|
+
* or irreversible. `authorization`: verify, run the handler, settle, then
|
|
40
|
+
* send — only for cheap, idempotent handlers: if the settlement then fails,
|
|
41
|
+
* the handler's side effects have happened but its response is withheld.
|
|
42
|
+
*/
|
|
43
|
+
readonly flow?: 'upfront' | 'authorization';
|
|
44
|
+
}
|
|
45
|
+
export type PaywallScheme = typeof EXACT_SCHEME | typeof BATCH_SCHEME;
|
|
46
|
+
export interface ObytePaywallOptions {
|
|
47
|
+
readonly facilitator: FacilitatorClient;
|
|
48
|
+
readonly network: string;
|
|
49
|
+
readonly payTo: string;
|
|
50
|
+
/** Stable identifier of this seller/API (part of the binding). */
|
|
51
|
+
readonly sellerId: string;
|
|
52
|
+
/** HMAC key for bindings; keep secret, ≥ 32 bytes. */
|
|
53
|
+
readonly secret: string | Uint8Array;
|
|
54
|
+
readonly store: ConsumedProofsStore;
|
|
55
|
+
readonly logger: Logger;
|
|
56
|
+
readonly quoteTtlSeconds?: number;
|
|
57
|
+
readonly maxTimeoutSeconds?: number;
|
|
58
|
+
/** How long one request waits for `settlement_pending` to resolve. */
|
|
59
|
+
readonly settleWaitMs?: number;
|
|
60
|
+
readonly settlePollMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* How long a request may hold a payment in `serving` (default 300 s). After
|
|
63
|
+
* that a retry with the same payment takes over (crash recovery); set it
|
|
64
|
+
* above the slowest handler.
|
|
65
|
+
*/
|
|
66
|
+
readonly servingLeaseSeconds?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Declares the x402 `payment-identifier` extension: a client-chosen id makes a
|
|
69
|
+
* repeated operation return the stored response instead of charging again.
|
|
70
|
+
*/
|
|
71
|
+
readonly paymentIdentifier?: {
|
|
72
|
+
readonly required: boolean;
|
|
73
|
+
};
|
|
74
|
+
/** Enables batch-settlement (AA payment channels) offers; requires a running seller watcher. */
|
|
75
|
+
readonly batch?: {
|
|
76
|
+
readonly minChannelTimeoutSeconds: number;
|
|
77
|
+
};
|
|
78
|
+
readonly now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
export declare class ObytePaywall {
|
|
81
|
+
private readonly options;
|
|
82
|
+
private readonly log;
|
|
83
|
+
private readonly now;
|
|
84
|
+
constructor(options: ObytePaywallOptions);
|
|
85
|
+
/** Wraps a handler so it only runs after a settled payment. */
|
|
86
|
+
paid(route: PaidRouteOptions, handler: PaidHandler): RequestHandler;
|
|
87
|
+
private handle;
|
|
88
|
+
/** authorization flow: verify → handler (response stored) → settle → send. */
|
|
89
|
+
private authorize;
|
|
90
|
+
private settleServed;
|
|
91
|
+
private serve;
|
|
92
|
+
private send;
|
|
93
|
+
private paymentRequired;
|
|
94
|
+
private requestFacts;
|
|
95
|
+
private terms;
|
|
96
|
+
private schemesFor;
|
|
97
|
+
private settleWithWait;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=paywall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../../src/seller/paywall.ts"],"names":[],"mappings":"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"}
|