@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.
@@ -9,6 +9,7 @@ import type {
9
9
  SchemeServerHooks,
10
10
  } from '@x402/core/types';
11
11
  import {
12
+ CHANNEL_BASE_AA,
12
13
  ObbyxError,
13
14
  fromDisplay,
14
15
  getNetwork,
@@ -17,14 +18,24 @@ import {
17
18
  parseAmount,
18
19
  } from '@obbyx/obyte';
19
20
  import { type Logger, moduleLogger } from '@obbyx/obyte/logger';
20
- import { EXACT_SCHEME, bindingsEqual, exactExtraSchema } from '@obbyx/x402-obyte';
21
+ import {
22
+ BATCH_SCHEME,
23
+ EXACT_SCHEME,
24
+ batchExtraSchema,
25
+ bindingsEqual,
26
+ canonical,
27
+ exactExtraSchema,
28
+ sha256Hex,
29
+ } from '@obbyx/x402-obyte';
21
30
  import type { ConsumedProofsStore } from './consumed-proofs.js';
22
31
  import { DEFAULT_QUOTE_TTL_SECONDS, DEFAULT_SERVING_LEASE_SECONDS } from './defaults.js';
23
32
  import { type Quote, bodyHashOf, newQuote, offerBinding } from './quote.js';
24
33
 
25
34
  /**
26
- * Obyte `exact` for the OFFICIAL x402 resource server (`@x402/core/server`,
27
- * `@x402/express`, `@x402/hono`, …): a SchemeNetworkServer.
35
+ * Obyte schemes for the OFFICIAL x402 resource server (`@x402/core/server`,
36
+ * `@x402/express`, `@x402/hono`, …): SchemeNetworkServer implementations for
37
+ * `exact` ({@link ObyteExactServer}) and `batch-settlement`
38
+ * ({@link ObyteBatchServer}), sharing {@link ObyteSchemeServer}.
28
39
  *
29
40
  * - Binding (I1/I2): `enrichPaymentRequiredResponse` adds a per-request HMAC
30
41
  * binding + quote to every offer. On a paid request it recomputes the HMAC
@@ -38,7 +49,7 @@ import { type Quote, bodyHashOf, newQuote, offerBinding } from './quote.js';
38
49
  * Unlike `@obbyx/sdk`'s ObytePaywall, a repeated request gets a 402 instead
39
50
  * of the stored response (the official middleware cannot replay responses).
40
51
  */
41
- export interface ObyteExactServerOptions {
52
+ export interface ObyteSchemeServerOptions {
42
53
  /** How long a request may hold a payment in `serving` before a retry takes over (default 300 s). */
43
54
  readonly servingLeaseSeconds?: number;
44
55
  readonly sellerId: string;
@@ -72,8 +83,32 @@ function requestOf(transportContext: unknown): RequestLike | undefined {
72
83
  };
73
84
  }
74
85
 
75
- export class ObyteExactServer implements SchemeNetworkServer {
76
- readonly scheme = EXACT_SCHEME;
86
+ /** @deprecated Use {@link ObyteSchemeServerOptions}. */
87
+ export type ObyteExactServerOptions = ObyteSchemeServerOptions;
88
+
89
+ /** Options of {@link ObyteBatchServer}. */
90
+ export interface ObyteBatchServerOptions extends ObyteSchemeServerOptions {
91
+ /** Minimum channel dispute window the seller accepts (seconds); must match the facilitator's. */
92
+ readonly minChannelTimeoutSeconds: number;
93
+ }
94
+
95
+ /** Per-scheme parts of an Obyte resource-server scheme. */
96
+ export interface ObyteSchemeSpec {
97
+ readonly scheme: typeof EXACT_SCHEME | typeof BATCH_SCHEME;
98
+ /** `extra` fields the scheme adds to every offer besides binding and quote. */
99
+ readonly staticExtra: Record<string, unknown>;
100
+ /** Parses the quote fields of the accepted offer. */
101
+ readonly acceptedExtra: (extra: unknown) => {
102
+ readonly binding: string;
103
+ readonly quoteId: string;
104
+ readonly validUntil: number;
105
+ };
106
+ /** Proof id and key kind of a payment payload (undefined when malformed). */
107
+ readonly proofOf: (payload: Record<string, unknown>) => { kind: string; id: string } | undefined;
108
+ }
109
+
110
+ export abstract class ObyteSchemeServer implements SchemeNetworkServer {
111
+ readonly scheme: string;
77
112
  readonly defaultAssetTransferMethod = 'default';
78
113
  readonly paymentFlows = {
79
114
  default: { supported: ['upfront'] as const, default: 'upfront' as const },
@@ -83,24 +118,25 @@ export class ObyteExactServer implements SchemeNetworkServer {
83
118
  private readonly log: Logger;
84
119
  private readonly now: () => number;
85
120
 
86
- constructor(private readonly options: ObyteExactServerOptions) {
87
- this.log = moduleLogger(options.logger, 'sdk.x402-server', { scheme: EXACT_SCHEME });
121
+ protected constructor(
122
+ private readonly options: ObyteSchemeServerOptions,
123
+ private readonly spec: ObyteSchemeSpec,
124
+ ) {
125
+ this.scheme = spec.scheme;
126
+ this.log = moduleLogger(options.logger, 'sdk.x402-server', { scheme: spec.scheme });
88
127
  this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
89
128
  this.schemeHooks = {
90
129
  onBeforeVerify: async (ctx) => {
91
- const unit = (ctx.paymentPayload.payload as { unit?: { unit?: unknown } }).unit?.unit;
130
+ const key = this.keyOf(ctx);
92
131
  const binding = (ctx.requirements.extra as { binding?: unknown }).binding;
93
- if (typeof unit !== 'string' || typeof binding !== 'string') {
132
+ if (key === undefined || typeof binding !== 'string') {
94
133
  return { abort: true, reason: 'invalid_payload' };
95
134
  }
96
- const { record } = this.options.proofs.begin(
97
- this.key(ctx.requirements.network, unit),
98
- binding,
99
- );
135
+ const { record } = this.options.proofs.begin(key, binding);
100
136
  if (!bindingsEqual(record.binding, binding))
101
137
  return { abort: true, reason: 'invalid_payment_reused' };
102
138
  if (record.state === 'served') {
103
- this.log.warn({ proofId: unit }, 'payment already served a request; refused');
139
+ this.log.warn({ proofId: key }, 'payment already served a request; refused');
104
140
  return { abort: true, reason: 'payment_already_used' };
105
141
  }
106
142
  return undefined;
@@ -183,7 +219,7 @@ export class ObyteExactServer implements SchemeNetworkServer {
183
219
  return asset === 'base' ? 9 : undefined;
184
220
  }
185
221
 
186
- /** Nothing to add from /supported for exact; the per-request fields come from the enrich hook. */
222
+ /** Nothing to add from /supported; the per-request fields come from the enrich hook. */
187
223
  async enhancePaymentRequirements(
188
224
  requirements: PaymentRequirements,
189
225
  ): Promise<PaymentRequirements> {
@@ -196,20 +232,21 @@ export class ObyteExactServer implements SchemeNetworkServer {
196
232
  ): Promise<PaymentRequirements[]> => {
197
233
  const request = requestOf(ctx.transportContext);
198
234
  if (request === undefined)
199
- throw new ObbyxError('no_request', 'Obyte exact needs the HTTP request context');
235
+ throw new ObbyxError('no_request', `Obyte ${this.scheme} needs the HTTP request context`);
200
236
  const accepted = this.acceptedQuote(request);
201
237
  return ctx.requirements.map((req) => {
202
- if (req.scheme !== EXACT_SCHEME) return req;
238
+ if (req.scheme !== this.spec.scheme) return req;
203
239
  const quote =
204
240
  accepted !== undefined &&
205
241
  bindingsEqual(this.bindingFor(req, request, accepted), accepted.binding)
206
242
  ? accepted
207
243
  : undefined;
208
- if (quote !== undefined) return { ...req, extra: { ...req.extra, ...quote } };
244
+ const extra = { ...req.extra, ...this.spec.staticExtra };
245
+ if (quote !== undefined) return { ...req, extra: { ...extra, ...quote } };
209
246
  const fresh = newQuote(this.now(), this.options.quoteTtlSeconds ?? DEFAULT_QUOTE_TTL_SECONDS);
210
247
  return {
211
248
  ...req,
212
- extra: { ...req.extra, binding: this.bindingFor(req, request, fresh), ...fresh },
249
+ extra: { ...extra, binding: this.bindingFor(req, request, fresh), ...fresh },
213
250
  };
214
251
  });
215
252
  };
@@ -217,10 +254,10 @@ export class ObyteExactServer implements SchemeNetworkServer {
217
254
  private acceptedQuote(request: RequestLike) {
218
255
  if (request.paymentHeader === undefined) return undefined;
219
256
  try {
220
- const extra = exactExtraSchema.safeParse(
221
- decodePaymentSignatureHeader(request.paymentHeader).accepted?.extra,
222
- );
223
- return extra.success ? extra.data : undefined;
257
+ const accepted = decodePaymentSignatureHeader(request.paymentHeader).accepted;
258
+ if (accepted?.scheme !== this.spec.scheme) return undefined;
259
+ const extra = this.spec.acceptedExtra(accepted.extra);
260
+ return { quoteId: extra.quoteId, validUntil: extra.validUntil, binding: extra.binding };
224
261
  } catch {
225
262
  return undefined;
226
263
  }
@@ -229,7 +266,7 @@ export class ObyteExactServer implements SchemeNetworkServer {
229
266
  private bindingFor(req: PaymentRequirements, request: RequestLike, quote: Quote): string {
230
267
  return offerBinding(
231
268
  this.options,
232
- EXACT_SCHEME,
269
+ this.spec.scheme,
233
270
  quote,
234
271
  { method: request.method, url: request.url, bodyHash: bodyHashOf(request.body) },
235
272
  { network: req.network, asset: req.asset, amount: req.amount, payTo: req.payTo },
@@ -241,11 +278,46 @@ export class ObyteExactServer implements SchemeNetworkServer {
241
278
  paymentPayload: { payload: Record<string, unknown> };
242
279
  requirements: { network: string };
243
280
  }): string | undefined {
244
- const unit = (ctx.paymentPayload.payload as { unit?: { unit?: unknown } }).unit?.unit;
245
- return typeof unit === 'string' ? this.key(ctx.requirements.network, unit) : undefined;
281
+ const proof = this.spec.proofOf(ctx.paymentPayload.payload);
282
+ return proof === undefined
283
+ ? undefined
284
+ : networkKey(getNetwork(ctx.requirements.network), proof.kind, proof.id);
246
285
  }
286
+ }
247
287
 
248
- private key(network: string, unit: string): string {
249
- return networkKey(getNetwork(network), 'unit', unit);
288
+ /** `exact`: the proof is the unit (key `<network>:unit:<hash>`, as ObytePaywall). */
289
+ export class ObyteExactServer extends ObyteSchemeServer {
290
+ constructor(options: ObyteSchemeServerOptions) {
291
+ super(options, {
292
+ scheme: EXACT_SCHEME,
293
+ staticExtra: {},
294
+ acceptedExtra: (extra) => exactExtraSchema.parse(extra),
295
+ proofOf: (payload) => {
296
+ const unit = (payload as { unit?: { unit?: unknown } }).unit?.unit;
297
+ return typeof unit === 'string' ? { kind: 'unit', id: unit } : undefined;
298
+ },
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * `batch-settlement` (AA payment channels): the proof is the voucher (key
305
+ * `<network>:voucher:<sha256 of the canonical signed package>`, as
306
+ * ObytePaywall). Offers carry the channel base AA and the minimum dispute
307
+ * window. Needs a facilitator with batch-settlement and the seller's watcher.
308
+ */
309
+ export class ObyteBatchServer extends ObyteSchemeServer {
310
+ constructor(options: ObyteBatchServerOptions) {
311
+ super(options, {
312
+ scheme: BATCH_SCHEME,
313
+ staticExtra: { baseAa: CHANNEL_BASE_AA, minTimeout: options.minChannelTimeoutSeconds },
314
+ acceptedExtra: (extra) => batchExtraSchema.parse(extra),
315
+ proofOf: (payload) => {
316
+ const pkg = payload['signedPackage'];
317
+ return pkg !== null && typeof pkg === 'object'
318
+ ? { kind: 'voucher', id: sha256Hex(canonical(pkg)) }
319
+ : undefined;
320
+ },
321
+ });
250
322
  }
251
323
  }