@obbyx/x402-obyte 0.0.0-stage → 0.2.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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/batch/client-store.d.ts +50 -0
  4. package/dist/batch/client-store.d.ts.map +1 -0
  5. package/dist/batch/client-store.js +93 -0
  6. package/dist/batch/client-store.js.map +1 -0
  7. package/dist/batch/client.d.ts +63 -0
  8. package/dist/batch/client.d.ts.map +1 -0
  9. package/dist/batch/client.js +226 -0
  10. package/dist/batch/client.js.map +1 -0
  11. package/dist/batch/facilitator.d.ts +73 -0
  12. package/dist/batch/facilitator.d.ts.map +1 -0
  13. package/dist/batch/facilitator.js +341 -0
  14. package/dist/batch/facilitator.js.map +1 -0
  15. package/dist/batch/gateway.d.ts +21 -0
  16. package/dist/batch/gateway.d.ts.map +1 -0
  17. package/dist/batch/gateway.js +2 -0
  18. package/dist/batch/gateway.js.map +1 -0
  19. package/dist/batch/requirements.d.ts +52 -0
  20. package/dist/batch/requirements.d.ts.map +1 -0
  21. package/dist/batch/requirements.js +80 -0
  22. package/dist/batch/requirements.js.map +1 -0
  23. package/dist/batch/store.d.ts +112 -0
  24. package/dist/batch/store.d.ts.map +1 -0
  25. package/dist/batch/store.js +198 -0
  26. package/dist/batch/store.js.map +1 -0
  27. package/dist/batch/watcher-protocol.d.ts +91 -0
  28. package/dist/batch/watcher-protocol.d.ts.map +1 -0
  29. package/dist/batch/watcher-protocol.js +82 -0
  30. package/dist/batch/watcher-protocol.js.map +1 -0
  31. package/dist/binding/binding.d.ts +32 -0
  32. package/dist/binding/binding.d.ts.map +1 -0
  33. package/dist/binding/binding.js +19 -0
  34. package/dist/binding/binding.js.map +1 -0
  35. package/dist/coins/composer.d.ts +58 -0
  36. package/dist/coins/composer.d.ts.map +1 -0
  37. package/dist/coins/composer.js +102 -0
  38. package/dist/coins/composer.js.map +1 -0
  39. package/dist/coins/index.d.ts +4 -0
  40. package/dist/coins/index.d.ts.map +1 -0
  41. package/dist/coins/index.js +4 -0
  42. package/dist/coins/index.js.map +1 -0
  43. package/dist/coins/reservations.d.ts +37 -0
  44. package/dist/coins/reservations.d.ts.map +1 -0
  45. package/dist/coins/reservations.js +102 -0
  46. package/dist/coins/reservations.js.map +1 -0
  47. package/dist/coins/selection.d.ts +42 -0
  48. package/dist/coins/selection.d.ts.map +1 -0
  49. package/dist/coins/selection.js +60 -0
  50. package/dist/coins/selection.js.map +1 -0
  51. package/dist/exact/client.d.ts +33 -0
  52. package/dist/exact/client.d.ts.map +1 -0
  53. package/dist/exact/client.js +93 -0
  54. package/dist/exact/client.js.map +1 -0
  55. package/dist/exact/facilitator-v1.d.ts +48 -0
  56. package/dist/exact/facilitator-v1.d.ts.map +1 -0
  57. package/dist/exact/facilitator-v1.js +81 -0
  58. package/dist/exact/facilitator-v1.js.map +1 -0
  59. package/dist/exact/facilitator.d.ts +94 -0
  60. package/dist/exact/facilitator.d.ts.map +1 -0
  61. package/dist/exact/facilitator.js +361 -0
  62. package/dist/exact/facilitator.js.map +1 -0
  63. package/dist/exact/finality.d.ts +18 -0
  64. package/dist/exact/finality.d.ts.map +1 -0
  65. package/dist/exact/finality.js +45 -0
  66. package/dist/exact/finality.js.map +1 -0
  67. package/dist/exact/ledger-checks.d.ts +15 -0
  68. package/dist/exact/ledger-checks.d.ts.map +1 -0
  69. package/dist/exact/ledger-checks.js +85 -0
  70. package/dist/exact/ledger-checks.js.map +1 -0
  71. package/dist/exact/reasons.d.ts +29 -0
  72. package/dist/exact/reasons.d.ts.map +1 -0
  73. package/dist/exact/reasons.js +21 -0
  74. package/dist/exact/reasons.js.map +1 -0
  75. package/dist/exact/requirements.d.ts +37 -0
  76. package/dist/exact/requirements.d.ts.map +1 -0
  77. package/dist/exact/requirements.js +80 -0
  78. package/dist/exact/requirements.js.map +1 -0
  79. package/dist/exact/store.d.ts +136 -0
  80. package/dist/exact/store.d.ts.map +1 -0
  81. package/dist/exact/store.js +217 -0
  82. package/dist/exact/store.js.map +1 -0
  83. package/dist/hub/heartbeat.d.ts +12 -0
  84. package/dist/hub/heartbeat.d.ts.map +1 -0
  85. package/dist/hub/heartbeat.js +17 -0
  86. package/dist/hub/heartbeat.js.map +1 -0
  87. package/dist/hub/hub-call.d.ts +20 -0
  88. package/dist/hub/hub-call.d.ts.map +1 -0
  89. package/dist/hub/hub-call.js +30 -0
  90. package/dist/hub/hub-call.js.map +1 -0
  91. package/dist/hub/schemas.d.ts +41 -0
  92. package/dist/hub/schemas.d.ts.map +1 -0
  93. package/dist/hub/schemas.js +71 -0
  94. package/dist/hub/schemas.js.map +1 -0
  95. package/dist/index.d.ts +19 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +19 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/util/keyed-queue.d.ts +13 -0
  100. package/dist/util/keyed-queue.d.ts.map +1 -0
  101. package/dist/util/keyed-queue.js +25 -0
  102. package/dist/util/keyed-queue.js.map +1 -0
  103. package/package.json +51 -3
  104. package/src/batch/client-store.ts +167 -0
  105. package/src/batch/client.ts +317 -0
  106. package/src/batch/facilitator.ts +461 -0
  107. package/src/batch/gateway.ts +22 -0
  108. package/src/batch/requirements.ts +108 -0
  109. package/src/batch/store.ts +359 -0
  110. package/src/batch/watcher-protocol.ts +89 -0
  111. package/src/binding/binding.ts +50 -0
  112. package/src/coins/composer.ts +159 -0
  113. package/src/coins/index.ts +9 -0
  114. package/src/coins/reservations.ts +139 -0
  115. package/src/coins/selection.ts +93 -0
  116. package/src/exact/client.ts +144 -0
  117. package/src/exact/facilitator-v1.ts +137 -0
  118. package/src/exact/facilitator.ts +496 -0
  119. package/src/exact/finality.ts +56 -0
  120. package/src/exact/ledger-checks.ts +113 -0
  121. package/src/exact/reasons.ts +25 -0
  122. package/src/exact/requirements.ts +109 -0
  123. package/src/exact/store.ts +399 -0
  124. package/src/hub/heartbeat.ts +18 -0
  125. package/src/hub/hub-call.ts +46 -0
  126. package/src/hub/schemas.ts +89 -0
  127. package/src/index.ts +111 -0
  128. package/src/util/keyed-queue.ts +25 -0
@@ -0,0 +1,112 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /**
3
+ * Facilitator-side storage for `batch-settlement` (ADR-0003), sharing the
4
+ * facilitator's SQLite connection (one process per database file).
5
+ *
6
+ * - `batch_channels`: per (network, aa) the committed cumulative amount for the
7
+ * current period and at most ONE pending commitment (serialization, MVP).
8
+ * - `batch_commitments`: the journal. `seq` is global and monotonic; the seller's
9
+ * watcher reads it by cursor. An entry is `pending` until the watcher has
10
+ * durably stored it (ack), then `committed`.
11
+ *
12
+ * Amounts are decimal strings (BigInt in code).
13
+ */
14
+ export interface ChannelRow {
15
+ readonly key: string;
16
+ readonly network: string;
17
+ readonly aa: string;
18
+ readonly payTo: string;
19
+ readonly payer: string;
20
+ readonly asset: string;
21
+ readonly period: number;
22
+ readonly committed: bigint;
23
+ readonly pendingSeq: number | null;
24
+ }
25
+ export type CommitmentState = 'pending' | 'committed';
26
+ export interface CommitmentRow {
27
+ readonly seq: number;
28
+ readonly channelKey: string;
29
+ readonly network: string;
30
+ readonly aa: string;
31
+ readonly payTo: string;
32
+ readonly period: number;
33
+ readonly amountSpent: bigint;
34
+ readonly paymentAmount: bigint;
35
+ readonly binding: string;
36
+ readonly packageHash: string;
37
+ readonly packageJson: string;
38
+ readonly state: CommitmentState;
39
+ }
40
+ export type AppendResult = {
41
+ ok: true;
42
+ commitment: CommitmentRow;
43
+ } | {
44
+ ok: false;
45
+ reason: 'channel_busy' | 'cumulative_amount_mismatch' | 'period_mismatch';
46
+ committed: bigint;
47
+ };
48
+ export interface AppendInput {
49
+ channelKey: string;
50
+ network: string;
51
+ aa: string;
52
+ payTo: string;
53
+ payer: string;
54
+ asset: string;
55
+ period: number;
56
+ amountSpent: bigint;
57
+ paymentAmount: bigint;
58
+ binding: string;
59
+ packageHash: string;
60
+ packageJson: string;
61
+ }
62
+ /** Port for batch-settlement storage (node:sqlite here, Prisma in apps/facilitator). */
63
+ export interface BatchStore {
64
+ channel(key: string): Promise<ChannelRow | undefined>;
65
+ commitmentByHash(packageHash: string): Promise<CommitmentRow | undefined>;
66
+ commitment(seq: number): Promise<CommitmentRow | undefined>;
67
+ journal(payTo: string, cursor: number, limit?: number): Promise<CommitmentRow[]>;
68
+ /**
69
+ * Every still-pending entry of a seller, whatever its seq: on reconnect they
70
+ * are re-delivered even if the watcher's cursor is past them (stored but the
71
+ * ack was lost, or stored out of order) — otherwise they would stay pending
72
+ * forever and block their channels.
73
+ */
74
+ pending(payTo: string): Promise<CommitmentRow[]>;
75
+ /** Atomically: check the channel is free and the cumulative rule, append a pending entry. */
76
+ appendPending(input: AppendInput): Promise<AppendResult>;
77
+ /**
78
+ * pending → committed (idempotent); releases the channel. Only commits a
79
+ * voucher of `owner` (seller + network): a watcher can never ack — and thus
80
+ * commit — another seller's voucher. Returns undefined if not owned.
81
+ */
82
+ commit(seq: number, owner: {
83
+ payTo: string;
84
+ network: string;
85
+ }): Promise<CommitmentRow | undefined>;
86
+ }
87
+ export declare class SqliteBatchStore implements BatchStore {
88
+ private readonly db;
89
+ private readonly now;
90
+ constructor(db: DatabaseSync, now?: () => number);
91
+ channel(key: string): Promise<ChannelRow | undefined>;
92
+ private channelSync;
93
+ commitmentByHash(packageHash: string): Promise<CommitmentRow | undefined>;
94
+ commitment(seq: number): Promise<CommitmentRow | undefined>;
95
+ private commitmentSync;
96
+ /** Journal for one seller after `cursor` (both pending and committed). */
97
+ pending(payTo: string): Promise<CommitmentRow[]>;
98
+ journal(payTo: string, cursor: number, limit?: number): Promise<CommitmentRow[]>;
99
+ /**
100
+ * Appends a pending commitment if the channel is free and the cumulative
101
+ * rule holds against the stored state; all in one IMMEDIATE transaction.
102
+ * A new period (the channel was closed and reopened) resets `committed`.
103
+ */
104
+ appendPending(input: AppendInput): Promise<AppendResult>;
105
+ /** Watcher acked `seq` (durably stored): pending → committed. Idempotent. */
106
+ commit(seq: number, owner: {
107
+ payTo: string;
108
+ network: string;
109
+ }): Promise<CommitmentRow | undefined>;
110
+ private tx;
111
+ }
112
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/batch/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ import { ObbyxError } from '@obbyx/obyte';
2
+ const SCHEMA = `
3
+ CREATE TABLE IF NOT EXISTS batch_channels (
4
+ key TEXT PRIMARY KEY,
5
+ network TEXT NOT NULL,
6
+ aa TEXT NOT NULL,
7
+ pay_to TEXT NOT NULL,
8
+ payer TEXT NOT NULL,
9
+ asset TEXT NOT NULL,
10
+ period INTEGER NOT NULL,
11
+ committed TEXT NOT NULL,
12
+ pending_seq INTEGER,
13
+ updated_at INTEGER NOT NULL
14
+ ) STRICT;
15
+ CREATE TABLE IF NOT EXISTS batch_commitments (
16
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ channel_key TEXT NOT NULL,
18
+ network TEXT NOT NULL,
19
+ aa TEXT NOT NULL,
20
+ pay_to TEXT NOT NULL,
21
+ period INTEGER NOT NULL,
22
+ amount_spent TEXT NOT NULL,
23
+ payment_amount TEXT NOT NULL,
24
+ binding TEXT NOT NULL,
25
+ package_hash TEXT NOT NULL UNIQUE,
26
+ package_json TEXT NOT NULL,
27
+ state TEXT NOT NULL,
28
+ created_at INTEGER NOT NULL,
29
+ committed_at INTEGER
30
+ ) STRICT;
31
+ CREATE INDEX IF NOT EXISTS batch_commitments_seller ON batch_commitments(pay_to, seq);
32
+ `;
33
+ export class SqliteBatchStore {
34
+ db;
35
+ now;
36
+ constructor(db, now = () => Math.floor(Date.now() / 1000)) {
37
+ this.db = db;
38
+ this.now = now;
39
+ db.exec(SCHEMA);
40
+ }
41
+ async channel(key) {
42
+ return this.channelSync(key);
43
+ }
44
+ channelSync(key) {
45
+ const row = this.db.prepare('SELECT * FROM batch_channels WHERE key = ?').get(key);
46
+ return row === undefined ? undefined : toChannel(row);
47
+ }
48
+ async commitmentByHash(packageHash) {
49
+ const row = this.db
50
+ .prepare('SELECT * FROM batch_commitments WHERE package_hash = ?')
51
+ .get(packageHash);
52
+ return row === undefined ? undefined : toCommitment(row);
53
+ }
54
+ async commitment(seq) {
55
+ return this.commitmentSync(seq);
56
+ }
57
+ commitmentSync(seq) {
58
+ const row = this.db.prepare('SELECT * FROM batch_commitments WHERE seq = ?').get(seq);
59
+ return row === undefined ? undefined : toCommitment(row);
60
+ }
61
+ /** Journal for one seller after `cursor` (both pending and committed). */
62
+ async pending(payTo) {
63
+ const rows = this.db
64
+ .prepare("SELECT * FROM batch_commitments WHERE pay_to = ? AND state = 'pending' ORDER BY seq")
65
+ .all(payTo);
66
+ return rows.map(toCommitment);
67
+ }
68
+ async journal(payTo, cursor, limit = 500) {
69
+ const rows = this.db
70
+ .prepare('SELECT * FROM batch_commitments WHERE pay_to = ? AND seq > ? ORDER BY seq LIMIT ?')
71
+ .all(payTo, cursor, limit);
72
+ return rows.map(toCommitment);
73
+ }
74
+ /**
75
+ * Appends a pending commitment if the channel is free and the cumulative
76
+ * rule holds against the stored state; all in one IMMEDIATE transaction.
77
+ * A new period (the channel was closed and reopened) resets `committed`.
78
+ */
79
+ async appendPending(input) {
80
+ return this.tx(() => {
81
+ let channel = this.channelSync(input.channelKey);
82
+ const ts = this.now();
83
+ if (channel === undefined || channel.period < input.period) {
84
+ if (channel?.pendingSeq != null) {
85
+ return {
86
+ ok: false,
87
+ reason: 'channel_busy',
88
+ committed: channel.committed,
89
+ };
90
+ }
91
+ this.db
92
+ .prepare(`INSERT INTO batch_channels (key, network, aa, pay_to, payer, asset, period, committed, pending_seq, updated_at)
93
+ VALUES (?, ?, ?, ?, ?, ?, ?, '0', NULL, ?)
94
+ ON CONFLICT(key) DO UPDATE SET period = excluded.period, committed = '0', updated_at = excluded.updated_at`)
95
+ .run(input.channelKey, input.network, input.aa, input.payTo, input.payer, input.asset, input.period, ts);
96
+ channel = this.channelSync(input.channelKey);
97
+ }
98
+ if (channel === undefined)
99
+ throw new ObbyxError('store_inconsistent', 'channel row missing');
100
+ if (channel.period !== input.period) {
101
+ return {
102
+ ok: false,
103
+ reason: 'period_mismatch',
104
+ committed: channel.committed,
105
+ };
106
+ }
107
+ if (channel.pendingSeq !== null) {
108
+ return {
109
+ ok: false,
110
+ reason: 'channel_busy',
111
+ committed: channel.committed,
112
+ };
113
+ }
114
+ if (input.amountSpent !== channel.committed + input.paymentAmount) {
115
+ return {
116
+ ok: false,
117
+ reason: 'cumulative_amount_mismatch',
118
+ committed: channel.committed,
119
+ };
120
+ }
121
+ const res = this.db
122
+ .prepare(`INSERT INTO batch_commitments
123
+ (channel_key, network, aa, pay_to, period, amount_spent, payment_amount, binding, package_hash,
124
+ package_json, state, created_at)
125
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`)
126
+ .run(input.channelKey, input.network, input.aa, input.payTo, input.period, input.amountSpent.toString(), input.paymentAmount.toString(), input.binding, input.packageHash, input.packageJson, ts);
127
+ const seq = Number(res.lastInsertRowid);
128
+ this.db
129
+ .prepare('UPDATE batch_channels SET pending_seq = ?, updated_at = ? WHERE key = ?')
130
+ .run(seq, ts, input.channelKey);
131
+ const commitment = this.commitmentSync(seq);
132
+ if (commitment === undefined)
133
+ throw new ObbyxError('store_inconsistent', 'commitment missing');
134
+ return { ok: true, commitment };
135
+ });
136
+ }
137
+ /** Watcher acked `seq` (durably stored): pending → committed. Idempotent. */
138
+ async commit(seq, owner) {
139
+ return this.tx(() => {
140
+ const c = this.commitmentSync(seq);
141
+ if (c === undefined || c.payTo !== owner.payTo || c.network !== owner.network)
142
+ return undefined;
143
+ if (c.state === 'committed')
144
+ return c;
145
+ const ts = this.now();
146
+ this.db
147
+ .prepare("UPDATE batch_commitments SET state = 'committed', committed_at = ? WHERE seq = ?")
148
+ .run(ts, seq);
149
+ this.db
150
+ .prepare(`UPDATE batch_channels SET committed = ?, pending_seq = NULL, updated_at = ?
151
+ WHERE key = ? AND pending_seq = ? AND period = ?`)
152
+ .run(c.amountSpent.toString(), ts, c.channelKey, seq, c.period);
153
+ return this.commitmentSync(seq);
154
+ });
155
+ }
156
+ tx(fn) {
157
+ this.db.exec('BEGIN IMMEDIATE');
158
+ try {
159
+ const result = fn();
160
+ this.db.exec('COMMIT');
161
+ return result;
162
+ }
163
+ catch (err) {
164
+ this.db.exec('ROLLBACK');
165
+ throw err;
166
+ }
167
+ }
168
+ }
169
+ function toChannel(r) {
170
+ return {
171
+ key: r.key,
172
+ network: r.network,
173
+ aa: r.aa,
174
+ payTo: r.pay_to,
175
+ payer: r.payer,
176
+ asset: r.asset,
177
+ period: r.period,
178
+ committed: BigInt(r.committed),
179
+ pendingSeq: r.pending_seq,
180
+ };
181
+ }
182
+ function toCommitment(r) {
183
+ return {
184
+ seq: r.seq,
185
+ channelKey: r.channel_key,
186
+ network: r.network,
187
+ aa: r.aa,
188
+ payTo: r.pay_to,
189
+ period: r.period,
190
+ amountSpent: BigInt(r.amount_spent),
191
+ paymentAmount: BigInt(r.payment_amount),
192
+ binding: r.binding,
193
+ packageHash: r.package_hash,
194
+ packageJson: r.package_json,
195
+ state: r.state,
196
+ };
197
+ }
198
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/batch/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { type ObyteNetwork, type Verdict } from '@obbyx/obyte';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Facilitator ↔ seller-watcher protocol (ADR-0003), JSON over WebSocket at
5
+ * `/watcher`.
6
+ *
7
+ * F → W challenge {challenge}
8
+ * W → F hello {network, login: signed {purpose, challenge, network, expires}, cursor, bytesBalance}
9
+ * F → W welcome {seller} then replays the journal after `cursor`
10
+ * F → W commitment {seq, ...voucher} (pending or committed)
11
+ * W → F ack {seq} ONLY after the voucher is durably stored
12
+ * W → F heartbeat {cursor, bytesBalance, closing?} closing: channels the seller is closing (no new vouchers)
13
+ * F → W error {message} then close
14
+ *
15
+ * The login is an Obyte signed message by the seller's address (payTo), so
16
+ * only the seller's watcher receives that seller's vouchers. Vouchers are
17
+ * bearer-sensitive (ADR-0001): never log `signedPackage`.
18
+ */
19
+ export declare const WATCHER_LOGIN_PURPOSE = "obbyx-watcher-login";
20
+ export declare const commitmentMessageSchema: z.ZodObject<{
21
+ type: z.ZodLiteral<"commitment">;
22
+ seq: z.ZodNumber;
23
+ network: z.ZodString;
24
+ aa: z.ZodString;
25
+ period: z.ZodNumber;
26
+ amountSpent: z.ZodString;
27
+ paymentAmount: z.ZodString;
28
+ binding: z.ZodString;
29
+ signedPackage: z.ZodRecord<z.ZodString, z.ZodUnknown>;
30
+ state: z.ZodEnum<{
31
+ pending: "pending";
32
+ committed: "committed";
33
+ }>;
34
+ }, z.core.$strip>;
35
+ export type CommitmentMessage = z.infer<typeof commitmentMessageSchema>;
36
+ export declare const serverMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
37
+ type: z.ZodLiteral<"challenge">;
38
+ challenge: z.ZodString;
39
+ }, z.core.$strip>, z.ZodObject<{
40
+ type: z.ZodLiteral<"welcome">;
41
+ seller: z.ZodString;
42
+ network: z.ZodString;
43
+ }, z.core.$strip>, z.ZodObject<{
44
+ type: z.ZodLiteral<"commitment">;
45
+ seq: z.ZodNumber;
46
+ network: z.ZodString;
47
+ aa: z.ZodString;
48
+ period: z.ZodNumber;
49
+ amountSpent: z.ZodString;
50
+ paymentAmount: z.ZodString;
51
+ binding: z.ZodString;
52
+ signedPackage: z.ZodRecord<z.ZodString, z.ZodUnknown>;
53
+ state: z.ZodEnum<{
54
+ pending: "pending";
55
+ committed: "committed";
56
+ }>;
57
+ }, z.core.$strip>, z.ZodObject<{
58
+ type: z.ZodLiteral<"error">;
59
+ message: z.ZodString;
60
+ }, z.core.$strip>], "type">;
61
+ export type ServerMessage = z.infer<typeof serverMessageSchema>;
62
+ export declare const clientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
63
+ type: z.ZodLiteral<"hello">;
64
+ network: z.ZodString;
65
+ login: z.ZodRecord<z.ZodString, z.ZodUnknown>;
66
+ cursor: z.ZodNumber;
67
+ bytesBalance: z.ZodString;
68
+ closing: z.ZodOptional<z.ZodArray<z.ZodString>>;
69
+ }, z.core.$strip>, z.ZodObject<{
70
+ type: z.ZodLiteral<"ack">;
71
+ seq: z.ZodNumber;
72
+ }, z.core.$strip>, z.ZodObject<{
73
+ type: z.ZodLiteral<"heartbeat">;
74
+ cursor: z.ZodNumber;
75
+ bytesBalance: z.ZodString;
76
+ closing: z.ZodOptional<z.ZodArray<z.ZodString>>;
77
+ }, z.core.$strip>], "type">;
78
+ export type ClientMessage = z.infer<typeof clientMessageSchema>;
79
+ export declare function loginMessage(challenge: string, network: string, now: number): {
80
+ purpose: string;
81
+ challenge: string;
82
+ network: string;
83
+ expires: number;
84
+ };
85
+ /** Verifies a watcher login; returns the seller address that signed it. */
86
+ export declare function verifyWatcherLogin(login: unknown, expected: {
87
+ challenge: string;
88
+ network: ObyteNetwork;
89
+ now: number;
90
+ }): Verdict<string>;
91
+ //# sourceMappingURL=watcher-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher-protocol.d.ts","sourceRoot":"","sources":["../../src/batch/watcher-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAuC,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAE3D,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;iBAWlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;2BAK9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;2BAgB9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAWhE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;;;;;EAE3E;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,MAAM,CAAC,CASjB"}
@@ -0,0 +1,82 @@
1
+ import { accept, reject, verifySignedPackage } from '@obbyx/obyte';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Facilitator ↔ seller-watcher protocol (ADR-0003), JSON over WebSocket at
5
+ * `/watcher`.
6
+ *
7
+ * F → W challenge {challenge}
8
+ * W → F hello {network, login: signed {purpose, challenge, network, expires}, cursor, bytesBalance}
9
+ * F → W welcome {seller} then replays the journal after `cursor`
10
+ * F → W commitment {seq, ...voucher} (pending or committed)
11
+ * W → F ack {seq} ONLY after the voucher is durably stored
12
+ * W → F heartbeat {cursor, bytesBalance, closing?} closing: channels the seller is closing (no new vouchers)
13
+ * F → W error {message} then close
14
+ *
15
+ * The login is an Obyte signed message by the seller's address (payTo), so
16
+ * only the seller's watcher receives that seller's vouchers. Vouchers are
17
+ * bearer-sensitive (ADR-0001): never log `signedPackage`.
18
+ */
19
+ export const WATCHER_LOGIN_PURPOSE = 'obbyx-watcher-login';
20
+ export const commitmentMessageSchema = z.object({
21
+ type: z.literal('commitment'),
22
+ seq: z.number().int().positive(),
23
+ network: z.string(),
24
+ aa: z.string(),
25
+ period: z.number().int().positive(),
26
+ amountSpent: z.string().regex(/^\d+$/),
27
+ paymentAmount: z.string().regex(/^\d+$/),
28
+ binding: z.string(),
29
+ signedPackage: z.record(z.string(), z.unknown()),
30
+ state: z.enum(['pending', 'committed']),
31
+ });
32
+ export const serverMessageSchema = z.discriminatedUnion('type', [
33
+ z.object({ type: z.literal('challenge'), challenge: z.string() }),
34
+ z.object({ type: z.literal('welcome'), seller: z.string(), network: z.string() }),
35
+ commitmentMessageSchema,
36
+ z.object({ type: z.literal('error'), message: z.string() }),
37
+ ]);
38
+ export const clientMessageSchema = z.discriminatedUnion('type', [
39
+ z.object({
40
+ type: z.literal('hello'),
41
+ network: z.string(),
42
+ login: z.record(z.string(), z.unknown()),
43
+ cursor: z.number().int().nonnegative(),
44
+ bytesBalance: z.string().regex(/^\d+$/),
45
+ closing: z.array(z.string()).max(10_000).optional(),
46
+ }),
47
+ z.object({ type: z.literal('ack'), seq: z.number().int().positive() }),
48
+ z.object({
49
+ type: z.literal('heartbeat'),
50
+ cursor: z.number().int().nonnegative(),
51
+ bytesBalance: z.string().regex(/^\d+$/),
52
+ closing: z.array(z.string()).max(10_000).optional(),
53
+ }),
54
+ ]);
55
+ const loginSchema = z
56
+ .object({
57
+ purpose: z.literal(WATCHER_LOGIN_PURPOSE),
58
+ challenge: z.string(),
59
+ network: z.string(),
60
+ expires: z.number().int(),
61
+ })
62
+ .strict();
63
+ export function loginMessage(challenge, network, now) {
64
+ return { purpose: WATCHER_LOGIN_PURPOSE, challenge, network, expires: now + 60 };
65
+ }
66
+ /** Verifies a watcher login; returns the seller address that signed it. */
67
+ export function verifyWatcherLogin(login, expected) {
68
+ const verified = verifySignedPackage(login, { network: expected.network });
69
+ if (!verified.ok)
70
+ return reject('watcher_login', verified.message);
71
+ const body = loginSchema.safeParse(verified.value.message);
72
+ if (!body.success)
73
+ return reject('watcher_login', 'Malformed login message');
74
+ if (body.data.challenge !== expected.challenge)
75
+ return reject('watcher_login', 'Wrong challenge');
76
+ if (body.data.network !== expected.network.caip2)
77
+ return reject('watcher_login', 'Wrong network');
78
+ if (body.data.expires < expected.now)
79
+ return reject('watcher_login', 'Login expired');
80
+ return accept(verified.value.signer);
81
+ }
82
+ //# sourceMappingURL=watcher-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher-protocol.js","sourceRoot":"","sources":["../../src/batch/watcher-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Binding of a payment to ONE operation (invariant I1/I2).
3
+ *
4
+ * binding = HMAC-SHA256(sellerSecret, canonical{v, sellerId, scheme, quoteId,
5
+ * validUntil, method, url, bodyHash, network, asset, amount, payTo})
6
+ *
7
+ * - The client signs the binding into its payment (unit data message), so a
8
+ * payment cannot be moved to another operation.
9
+ * - The HMAC makes quotes unforgeable without storing them: on the paid
10
+ * request the seller recomputes it from the request and the quote fields.
11
+ */
12
+ export interface BindingInput {
13
+ readonly sellerId: string;
14
+ /** x402 scheme of the offer (a payment of one scheme never counts for another). */
15
+ readonly scheme: string;
16
+ readonly quoteId: string;
17
+ readonly validUntil: number;
18
+ readonly method: string;
19
+ /** Path + query string, as requested. */
20
+ readonly url: string;
21
+ /** sha256 hex of the request body ('' for none), or a custom fingerprint. */
22
+ readonly bodyHash: string;
23
+ readonly network: string;
24
+ readonly asset: string;
25
+ readonly amount: string;
26
+ readonly payTo: string;
27
+ }
28
+ export declare function computeBinding(secret: Uint8Array | string, input: BindingInput): string;
29
+ export declare function bindingsEqual(a: string, b: string): boolean;
30
+ export declare function newQuoteId(): string;
31
+ export declare function sha256Hex(data: string | Uint8Array): string;
32
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/binding/binding.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAIvF;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3D;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAE3D"}
@@ -0,0 +1,19 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { canonical } from '../exact/requirements.js';
3
+ export function computeBinding(secret, input) {
4
+ return createHmac('sha256', secret)
5
+ .update(canonical({ v: 1, ...input, method: input.method.toUpperCase() }))
6
+ .digest('hex');
7
+ }
8
+ export function bindingsEqual(a, b) {
9
+ const x = Buffer.from(a, 'utf8');
10
+ const y = Buffer.from(b, 'utf8');
11
+ return x.length === y.length && timingSafeEqual(x, y);
12
+ }
13
+ export function newQuoteId() {
14
+ return randomBytes(16).toString('base64url');
15
+ }
16
+ export function sha256Hex(data) {
17
+ return createHash('sha256').update(data).digest('hex');
18
+ }
19
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/binding/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA8BrD,MAAM,UAAU,cAAc,CAAC,MAA2B,EAAE,KAAmB;IAC7E,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAChC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;SACzE,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAS,EAAE,CAAS;IAChD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAyB;IACjD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type obyte from 'obyte';
2
+ import { type Logger } from '@obbyx/obyte/logger';
3
+ import type { CoinReservations } from './reservations.js';
4
+ /**
5
+ * Input reservation for client-composed units (ADR-0002 "publication").
6
+ *
7
+ * Problem: an `exact` unit is signed by the client but broadcast later by the
8
+ * facilitator. Until the hub knows it, the hub would pick the SAME coins for
9
+ * the next unit → double spend, one unit becomes bad.
10
+ *
11
+ * Solution: our own coin picker, installed on obyte.js's extension point
12
+ * `client.api.pickDivisibleCoinsForAmount`. It computes the wallet's unspent
13
+ * outputs from `light/get_history`, skips reserved ones, reserves what it
14
+ * picks (SQLite, durable) and binds the reservation to the unit hash. Composes
15
+ * per wallet are serialized. A reservation ends when the unit is seen on the
16
+ * ledger (its inputs are then spent for the hub) or, if it never appears, only
17
+ * after `releaseAfterSeconds` (≥ quote validity + the facilitator's publication
18
+ * window) — never earlier, because a signed unit can still be published.
19
+ *
20
+ * Coin rules mirror an ocore light wallet with `spend_unconfirmed: 'own'`:
21
+ * outputs of OTHER authors must be stable and at or before `last_ball_mci`;
22
+ * own unstable outputs (change) may be spent.
23
+ */
24
+ export interface ReservingComposerOptions {
25
+ readonly client: obyte.Client;
26
+ readonly network: string;
27
+ readonly address: string;
28
+ readonly reservations: CoinReservations;
29
+ readonly logger: Logger;
30
+ /** Minimum age before an unpublished unit's inputs are released (s). */
31
+ readonly releaseAfterSeconds?: number;
32
+ readonly hubTimeoutMs?: number;
33
+ readonly now?: () => number;
34
+ }
35
+ /**
36
+ * Installs the reserving coin picker on an obyte.js client and serializes
37
+ * composes for the wallet. Use `compose(fn)` around every `client.compose.*`.
38
+ */
39
+ export declare class ReservingComposer {
40
+ private readonly options;
41
+ private queue;
42
+ private lastPick;
43
+ private readonly log;
44
+ private readonly now;
45
+ constructor(options: ReservingComposerOptions);
46
+ /** Runs `fn` (which composes exactly one unit) with reservations; returns its unit. */
47
+ compose<T extends {
48
+ unit?: unknown;
49
+ }>(fn: () => Promise<T>, opts?: {
50
+ holdUntil?: number;
51
+ }): Promise<T>;
52
+ /** Releases the inputs of a unit that was composed but provably never sent anywhere. */
53
+ discard(unit: string): void;
54
+ /** Ends reservations of units now known to the hub, or provably expired. */
55
+ sweep(): Promise<void>;
56
+ private pick;
57
+ }
58
+ //# sourceMappingURL=composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/coins/composer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAMhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEN,OAAO,EAAE,wBAAwB;IAY9D,uFAAuF;IACvF,OAAO,CAAC,CAAC,SAAS;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAClC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAChC,OAAO,CAAC,CAAC,CAAC;IAsBb,wFAAwF;IACxF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI3B,4EAA4E;IACtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAmBd,IAAI;CA6CnB"}