@metalntwx/metal-pay 0.2.0 → 0.3.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 (70) hide show
  1. package/README.md +72 -33
  2. package/dist/clearing-abi.d.ts +1836 -0
  3. package/dist/clearing-abi.js +2361 -0
  4. package/dist/clearing-client.d.ts +49 -159
  5. package/dist/clearing-client.js +191 -564
  6. package/dist/clearing-codec.d.ts +57 -0
  7. package/dist/clearing-codec.js +173 -0
  8. package/dist/clearing-custody.d.ts +191 -0
  9. package/dist/clearing-custody.js +490 -0
  10. package/dist/clearing-server.d.ts +30 -49
  11. package/dist/clearing-server.js +87 -67
  12. package/dist/clearing-x402.d.ts +29 -0
  13. package/dist/clearing-x402.js +203 -0
  14. package/dist/client.d.ts +7 -13
  15. package/dist/client.js +16 -18
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +4 -1
  18. package/dist/mandates/binding.d.ts +4 -27
  19. package/dist/mandates/binding.js +5 -9
  20. package/dist/mandates/checkout.js +5 -5
  21. package/dist/mandates/client.js +4 -3
  22. package/dist/mandates/l3a.d.ts +2 -2
  23. package/dist/mandates/l3a.js +5 -4
  24. package/dist/mandates/seller.d.ts +1 -1
  25. package/dist/mandates/seller.js +1 -1
  26. package/dist/mandates/verify.d.ts +4 -6
  27. package/dist/mandates/verify.js +42 -66
  28. package/dist/mandates/vi/credential.js +4 -4
  29. package/dist/node.d.ts +11 -0
  30. package/dist/node.js +97 -0
  31. package/package.json +18 -11
  32. package/src/clearing-abi.ts +2361 -0
  33. package/src/clearing-client.ts +303 -0
  34. package/src/clearing-codec.test.ts +57 -0
  35. package/src/clearing-codec.ts +251 -0
  36. package/src/clearing-custody.test.ts +534 -0
  37. package/src/clearing-custody.ts +665 -0
  38. package/src/clearing-fixture.json +37 -0
  39. package/src/clearing-server.test.ts +388 -0
  40. package/src/clearing-server.ts +123 -0
  41. package/src/clearing-x402.test.ts +181 -0
  42. package/src/clearing-x402.ts +279 -0
  43. package/src/client.test.ts +325 -0
  44. package/src/client.ts +156 -0
  45. package/src/index.ts +33 -0
  46. package/src/mandates/account.ts +91 -0
  47. package/src/mandates/binding.ts +280 -0
  48. package/src/mandates/chain.ts +68 -0
  49. package/src/mandates/checkout.ts +368 -0
  50. package/src/mandates/client.ts +159 -0
  51. package/src/mandates/extension.ts +48 -0
  52. package/src/mandates/index.ts +121 -0
  53. package/src/mandates/ingest.ts +42 -0
  54. package/src/mandates/jcs.ts +16 -0
  55. package/src/mandates/l3a.ts +216 -0
  56. package/src/mandates/seller.ts +91 -0
  57. package/src/mandates/verify.ts +532 -0
  58. package/src/mandates/vi/credential.ts +477 -0
  59. package/src/mandates/vi/issue.ts +118 -0
  60. package/src/mandates/vi/sd-jwt.ts +189 -0
  61. package/src/networks.test.ts +16 -0
  62. package/src/networks.ts +152 -0
  63. package/src/node.test.ts +24 -0
  64. package/src/node.ts +96 -0
  65. package/src/server.test.ts +223 -0
  66. package/src/server.ts +77 -0
  67. package/dist/clearing.d.ts +0 -204
  68. package/dist/clearing.js +0 -151
  69. package/dist/file-storage.d.ts +0 -39
  70. package/dist/file-storage.js +0 -195
@@ -1,195 +0,0 @@
1
- // Durable storage for the clearing client's position: one JSON file. Give
2
- // every process that signs with the same key the same file, so each sees
3
- // what the others signed.
4
- //
5
- // Kept apart from `clearing-client.ts` so the plug-in itself has no
6
- // filesystem dependency, the way the official batch-settlement client ships
7
- // its file storage as a separate entry point.
8
- import { randomUUID } from "node:crypto";
9
- import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
10
- import { dirname } from "node:path";
11
- import { keepHighest, } from "./clearing-client.js";
12
- /** Thrown inside a write whose lock was taken over while it was working. */
13
- class LostLock extends Error {
14
- }
15
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
- /** Whether the process that wrote a lock is gone. Only meaningful for a
17
- * holder on this machine; an unparsable pid is not treated as dead. */
18
- function holderIsDead(token) {
19
- const pid = Number(token.split(":")[0]);
20
- if (!Number.isInteger(pid) || pid <= 0)
21
- return false;
22
- try {
23
- process.kill(pid, 0);
24
- return false;
25
- }
26
- catch (error) {
27
- return error.code === "ESRCH";
28
- }
29
- }
30
- export class FileClearingPositionStorage {
31
- path;
32
- lockPath;
33
- takeoverPath;
34
- lockWaitMs;
35
- lockStaleMs;
36
- constructor(path, options = {}) {
37
- this.path = path;
38
- this.lockPath = `${path}.lock`;
39
- this.takeoverPath = `${path}.lock.takeover`;
40
- this.lockWaitMs = options.lockWaitMs ?? 5_000;
41
- this.lockStaleMs = options.lockStaleMs ?? 30_000;
42
- }
43
- async read() {
44
- let text;
45
- try {
46
- text = await readFile(this.path, "utf8");
47
- }
48
- catch (error) {
49
- if (error.code === "ENOENT")
50
- return {};
51
- throw new Error(`clearing position file ${this.path} is unreadable`, {
52
- cause: error,
53
- });
54
- }
55
- // A file that exists but does not parse is not "no record": treating it
56
- // as empty would drop the protection the record gives.
57
- try {
58
- const parsed = JSON.parse(text);
59
- if (typeof parsed !== "object" ||
60
- parsed === null ||
61
- Array.isArray(parsed))
62
- throw new Error("not an object");
63
- return parsed;
64
- }
65
- catch (error) {
66
- throw new Error(`clearing position file ${this.path} is unreadable`, {
67
- cause: error,
68
- });
69
- }
70
- }
71
- /** Hold the lock file while `job` runs. The whole file is read, changed
72
- * and rewritten, so two writers (two instances, or two processes) must
73
- * not overlap or one would drop the other's record.
74
- *
75
- * The lock is a file created exclusively, holding a token unique to its
76
- * holder. A lock left by a writer that died is taken over, and that is
77
- * the delicate part: two waiters must not both conclude it is abandoned
78
- * and both remove it, or the second would delete the first one's fresh
79
- * lock. So removal itself is guarded (see `takeOver`), and a writer whose
80
- * lock was taken over finds out before it writes (`stillHeld`). */
81
- async locked(job) {
82
- await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
83
- const deadline = Date.now() + this.lockWaitMs;
84
- const token = `${process.pid}:${randomUUID()}`;
85
- for (;;) {
86
- try {
87
- await writeFile(this.lockPath, token, { flag: "wx" });
88
- break;
89
- }
90
- catch (error) {
91
- if (error.code !== "EEXIST")
92
- throw error;
93
- // Every way round this loop is bounded by the wait.
94
- if (Date.now() > deadline)
95
- throw new Error(`timed out waiting for the clearing position lock ${this.lockPath}`, { cause: error });
96
- const seen = await this.inspect(this.lockPath);
97
- if (seen && (holderIsDead(seen.token) || seen.age > this.lockStaleMs))
98
- await this.takeOver(seen.token);
99
- // Held by a live writer, unreadable, or already gone: wait and retry.
100
- else
101
- await sleep(10);
102
- }
103
- }
104
- const stillHeld = async () => {
105
- if ((await this.inspect(this.lockPath))?.token !== token)
106
- throw new LostLock(`the clearing position lock ${this.lockPath} was taken over mid-write`);
107
- };
108
- try {
109
- return await job(stillHeld);
110
- }
111
- finally {
112
- // Release only our own lock: if it was taken over, it is not ours.
113
- if ((await this.inspect(this.lockPath))?.token === token)
114
- await rm(this.lockPath, { force: true });
115
- }
116
- }
117
- async inspect(path) {
118
- // One open file for both reads, so the token and the age always belong
119
- // to the same lock even if it is replaced meanwhile.
120
- let handle;
121
- try {
122
- handle = await open(path, "r");
123
- const info = await handle.stat();
124
- const token = await handle.readFile("utf8");
125
- return { token, age: Date.now() - info.mtimeMs };
126
- }
127
- catch {
128
- return null;
129
- }
130
- finally {
131
- await handle?.close();
132
- }
133
- }
134
- /** Remove `path` only while it still holds `token`. */
135
- async removeIfHolds(path, token) {
136
- if ((await this.inspect(path))?.token === token)
137
- await rm(path, { force: true });
138
- }
139
- /** Remove an abandoned lock, at most one waiter at a time. The takeover
140
- * file is itself created exclusively, so only one waiter is ever inside;
141
- * it removes the lock only if it is still the abandoned one it saw. A
142
- * waiter that arrives later finds either no lock or a fresh one from a
143
- * new holder, and leaves it alone. */
144
- async takeOver(abandoned) {
145
- const mine = `${process.pid}:${randomUUID()}`;
146
- try {
147
- await writeFile(this.takeoverPath, mine, { flag: "wx" });
148
- }
149
- catch (error) {
150
- if (error.code !== "EEXIST")
151
- throw error;
152
- // Another waiter is doing it. If that waiter itself died here, clear
153
- // its marker so takeovers are not blocked for good.
154
- const other = await this.inspect(this.takeoverPath);
155
- if (other && (holderIsDead(other.token) || other.age > this.lockStaleMs))
156
- await this.removeIfHolds(this.takeoverPath, other.token);
157
- await sleep(10);
158
- return;
159
- }
160
- try {
161
- if ((await this.inspect(this.lockPath))?.token === abandoned)
162
- await rm(this.lockPath, { force: true });
163
- }
164
- finally {
165
- await this.removeIfHolds(this.takeoverPath, mine);
166
- }
167
- }
168
- async get(key) {
169
- return (await this.read())[key];
170
- }
171
- set(key, position) {
172
- return this.locked(async (stillHeld) => {
173
- const all = await this.read();
174
- // Merged under the lock: another client may have signed since the
175
- // caller read its position, and the high-water mark must not go down.
176
- all[key] = keepHighest(all[key], position);
177
- // Write beside the file under a name no other writer shares, then swap
178
- // it in, so a crash mid-write cannot leave a truncated record.
179
- const next = `${this.path}.${randomUUID()}.tmp`;
180
- try {
181
- await writeFile(next, `${JSON.stringify(all, null, 2)}\n`, {
182
- mode: 0o600,
183
- });
184
- // A writer that hung long enough to be taken over must not swap in
185
- // what it read before the newer writer's change.
186
- await stillHeld();
187
- await rename(next, this.path);
188
- }
189
- catch (error) {
190
- await rm(next, { force: true });
191
- throw error;
192
- }
193
- });
194
- }
195
- }