@c4roa0/agentpay-user 0.0.0-stage → 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 (173) hide show
  1. package/AGENTS.md +137 -0
  2. package/CONTRIBUTING.md +16 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE.md +20 -0
  5. package/README.md +84 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +97 -0
  9. package/doc/architecture.md +96 -0
  10. package/doc/design.md +60 -0
  11. package/doc/develope.md +68 -0
  12. package/doc/plans/user-wallet-distribution.md +91 -0
  13. package/doc/reference/harness-skill-runtime.md +120 -0
  14. package/doc/reference/local-cache-foundation.md +176 -0
  15. package/doc/reference/local-safe-owner-ui.md +25 -0
  16. package/doc/reference/local-update.md +18 -0
  17. package/doc/reference/multi-service-runtime.md +119 -0
  18. package/doc/reference/npm-integration-verification.md +67 -0
  19. package/doc/reference/onboarding-writing.md +48 -0
  20. package/doc/reference/original-order-recovery.md +97 -0
  21. package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
  22. package/doc/reference/public-deployment-settings.md +56 -0
  23. package/doc/reference/random-skill-contract.md +18 -0
  24. package/doc/reference/result-file-cache.md +166 -0
  25. package/doc/reference/runtime-address-recovery.md +42 -0
  26. package/doc/reference/runtime-result-cache.md +157 -0
  27. package/doc/reference/runtime-startup-recovery.md +141 -0
  28. package/doc/reference/skill-input-output-cache.md +93 -0
  29. package/doc/reference/wallet-identity-policy-history.md +23 -0
  30. package/doc/runbooks/commands.md +89 -0
  31. package/doc/runbooks/deployment-authorization-update.md +42 -0
  32. package/doc/runbooks/emergency-payment-stop.md +80 -0
  33. package/doc/runbooks/local-autostart.md +212 -0
  34. package/doc/runbooks/local-cache.md +115 -0
  35. package/doc/runbooks/local-update.md +84 -0
  36. package/doc/runbooks/market-connection.md +73 -0
  37. package/doc/runbooks/npm-publication.md +56 -0
  38. package/doc/runbooks/official-test-package.md +62 -0
  39. package/doc/runbooks/order-cancellation.md +56 -0
  40. package/doc/runbooks/order-support.md +41 -0
  41. package/doc/runbooks/policy-renewal.md +80 -0
  42. package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
  43. package/doc/runbooks/runtime-skill-client.md +155 -0
  44. package/doc/runbooks/safe-owner-web.md +104 -0
  45. package/doc/runbooks/safe-policy-migration.md +36 -0
  46. package/doc/runbooks/safe-policy-setup.md +52 -0
  47. package/doc/runbooks/terminal-authorization-preview.md +38 -0
  48. package/doc/runbooks/unlock-wallet.md +68 -0
  49. package/doc/runbooks/user-onboarding.md +87 -0
  50. package/doc/runbooks/wallet-identity.md +59 -0
  51. package/doc/security/public-source-review-2026-11-06.md +71 -0
  52. package/doc/usage.md +54 -0
  53. package/package.json +49 -4
  54. package/progress.md +178 -0
  55. package/run +29 -0
  56. package/scripts/agentpay-cli.mjs +11 -0
  57. package/scripts/authorize-existing-safe.mjs +211 -0
  58. package/scripts/autostart.mjs +186 -0
  59. package/scripts/cancel-order.mjs +76 -0
  60. package/scripts/check-installation.mjs +26 -0
  61. package/scripts/check-official-test-package.mjs +35 -0
  62. package/scripts/check-purchase-readiness.mjs +275 -0
  63. package/scripts/configure-runtime-client.mjs +16 -0
  64. package/scripts/guide-policy.mjs +64 -0
  65. package/scripts/initialize-caller-credential.mjs +11 -0
  66. package/scripts/install-skill-bundle.mjs +176 -0
  67. package/scripts/install-user.mjs +24 -0
  68. package/scripts/local-service.mjs +60 -0
  69. package/scripts/lock.mjs +11 -0
  70. package/scripts/login-notify.mjs +39 -0
  71. package/scripts/migrate-safe-policy.mjs +165 -0
  72. package/scripts/official-paid-test.mjs +9 -0
  73. package/scripts/poll-unconfirmed-notice.mjs +75 -0
  74. package/scripts/prepare-onboarding.mjs +33 -0
  75. package/scripts/preview-policy.mjs +65 -0
  76. package/scripts/random-model-ack-smoke.mjs +235 -0
  77. package/scripts/random-reject-smoke.mjs +147 -0
  78. package/scripts/renew.mjs +9 -0
  79. package/scripts/repair.mjs +10 -0
  80. package/scripts/revoke.mjs +9 -0
  81. package/scripts/select-skill-bundle.mjs +39 -0
  82. package/scripts/setup-local.mjs +199 -0
  83. package/scripts/status.mjs +13 -0
  84. package/scripts/test-package.mjs +35 -0
  85. package/scripts/unlock.mjs +13 -0
  86. package/scripts/update.mjs +7 -0
  87. package/scripts/user-setup.mjs +219 -0
  88. package/scripts/verify-npm-release.mjs +32 -0
  89. package/src/active-skill.mjs +160 -0
  90. package/src/anvil-onboarding-state.mjs +96 -0
  91. package/src/anvil-onboarding.mjs +1036 -0
  92. package/src/authorization-question-checkpoint.mjs +114 -0
  93. package/src/autostart-files.mjs +50 -0
  94. package/src/cache-management-cli.mjs +302 -0
  95. package/src/cache-management.mjs +47 -0
  96. package/src/caller-auth.mjs +58 -0
  97. package/src/client-request-lock.mjs +44 -0
  98. package/src/connect-market.mjs +408 -0
  99. package/src/demo-unlock-secret.mjs +64 -0
  100. package/src/deployment-status.mjs +198 -0
  101. package/src/file-cache-cli.mjs +83 -0
  102. package/src/file-cache-owner.mjs +74 -0
  103. package/src/file-cache-settings.mjs +50 -0
  104. package/src/file-cache.mjs +790 -0
  105. package/src/file-download.mjs +281 -0
  106. package/src/finalization-disclosure.mjs +16 -0
  107. package/src/guided-setup.mjs +194 -0
  108. package/src/index.mjs +1231 -0
  109. package/src/install-and-setup.mjs +38 -0
  110. package/src/install-preflight.mjs +201 -0
  111. package/src/install-user.mjs +279 -0
  112. package/src/installation-paths.mjs +31 -0
  113. package/src/invocation-result.mjs +148 -0
  114. package/src/invocation-store.mjs +385 -0
  115. package/src/local-data-directory.mjs +117 -0
  116. package/src/local-info.mjs +181 -0
  117. package/src/local-services.mjs +156 -0
  118. package/src/local-time.mjs +36 -0
  119. package/src/local-update.mjs +306 -0
  120. package/src/local-wallet-ipc.mjs +93 -0
  121. package/src/login-reminder.mjs +28 -0
  122. package/src/market-download-url.mjs +21 -0
  123. package/src/node-version.mjs +12 -0
  124. package/src/npm-update.mjs +126 -0
  125. package/src/official-paid-test.mjs +407 -0
  126. package/src/official-test-package.mjs +41 -0
  127. package/src/official-test-update.mjs +162 -0
  128. package/src/order-cancellation.mjs +241 -0
  129. package/src/order-closure.mjs +42 -0
  130. package/src/order-list-panel.mjs +110 -0
  131. package/src/order-review.mjs +424 -0
  132. package/src/order-status-panel.mjs +65 -0
  133. package/src/order-support.mjs +365 -0
  134. package/src/owner-approval-server.mjs +332 -0
  135. package/src/owner-signer.mjs +67 -0
  136. package/src/policy-info.mjs +38 -0
  137. package/src/policy-migration.mjs +84 -0
  138. package/src/policy-operations.mjs +67 -0
  139. package/src/policy-preview-server.mjs +236 -0
  140. package/src/policy-terminal-guide.mjs +377 -0
  141. package/src/prepare-local-services.mjs +143 -0
  142. package/src/prepare-onboarding.mjs +210 -0
  143. package/src/private-journal-files.mjs +29 -0
  144. package/src/readiness-profile.mjs +30 -0
  145. package/src/renew-policy.mjs +605 -0
  146. package/src/repair-market-wallet.mjs +137 -0
  147. package/src/restore-wallet.mjs +255 -0
  148. package/src/result-cache-view.mjs +276 -0
  149. package/src/runtime-client.mjs +1145 -0
  150. package/src/runtime-connection-settings.mjs +101 -0
  151. package/src/runtime-recovery.mjs +667 -0
  152. package/src/runtime-status.mjs +32 -0
  153. package/src/safe-owner-anvil.mjs +115 -0
  154. package/src/safe-policy-plan.mjs +195 -0
  155. package/src/service-reference.mjs +233 -0
  156. package/src/service-url.mjs +52 -0
  157. package/src/settlement-response.mjs +69 -0
  158. package/src/setup-autostart.mjs +197 -0
  159. package/src/setup-demo.mjs +152 -0
  160. package/src/skill-bundle.mjs +142 -0
  161. package/src/skill-client.mjs +198 -0
  162. package/src/skill-install-prompt.mjs +31 -0
  163. package/src/skill-package.mjs +50 -0
  164. package/src/skill-zip.mjs +220 -0
  165. package/src/status-panel.mjs +589 -0
  166. package/src/transport.mjs +48 -0
  167. package/src/uninstall-local.mjs +299 -0
  168. package/src/user-cli.mjs +354 -0
  169. package/src/user-setup-guide.mjs +193 -0
  170. package/src/wallet-control.mjs +182 -0
  171. package/src/wallet-ipc-error.mjs +16 -0
  172. package/src/wallet-package.mjs +22 -0
  173. package/update-pair.json +6 -0
@@ -0,0 +1,385 @@
1
+ // Durable purchase identity and checkpoint store. Conflicts must preserve the original
2
+ // request; losing an acknowledgement is not permission to issue a new paid attempt.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
6
+ import { currentTimeMs, localTimeMs } from "./local-time.mjs";
7
+ import { constants } from "node:fs";
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import { mkdir, rename, open, unlink } from "node:fs/promises";
10
+ import { dirname } from "node:path";
11
+
12
+ const B32 = /^0x[0-9a-f]{64}$/;
13
+
14
+ export function createInvocationStore(path, serviceBinding = null) {
15
+ if (!String(path || "").trim())
16
+ throw new Error("AGENTPAY_USER_STATE_FILE is required");
17
+ let tail = Promise.resolve();
18
+
19
+ async function locked(operation) {
20
+ const previous = tail;
21
+ let release;
22
+ tail = new Promise((resolve) => {
23
+ release = resolve;
24
+ });
25
+ await previous;
26
+ try {
27
+ return await operation();
28
+ } finally {
29
+ release();
30
+ }
31
+ }
32
+
33
+ async function load() {
34
+ try {
35
+ const handle = await open(
36
+ path,
37
+ constants.O_RDONLY | constants.O_NOFOLLOW,
38
+ );
39
+ let parsed;
40
+ try {
41
+ const info = await handle.stat();
42
+ if (
43
+ !info.isFile() ||
44
+ info.uid !== process.getuid() ||
45
+ info.mode & 0o077 ||
46
+ info.size > 32 * 1048576
47
+ )
48
+ throw coded(
49
+ "INVOCATION_STATE_INVALID",
50
+ "Invocation history must be a private regular file within the size limit",
51
+ );
52
+ parsed = JSON.parse(await handle.readFile("utf8"));
53
+ } finally {
54
+ await handle.close();
55
+ }
56
+ if (
57
+ parsed?.version !== "agentpay.user-invocations.v1" ||
58
+ !parsed.invocations ||
59
+ typeof parsed.invocations !== "object" ||
60
+ Array.isArray(parsed.invocations) ||
61
+ Object.entries(parsed.invocations).some(
62
+ ([id, entry]) =>
63
+ !B32.test(id) ||
64
+ !entry ||
65
+ typeof entry !== "object" ||
66
+ Array.isArray(entry) ||
67
+ entry.invocationId !== id ||
68
+ !B32.test(entry.requestHash) ||
69
+ !B32.test(entry.orderRequestId) ||
70
+ !B32.test(entry.commitmentNonce) ||
71
+ (entry.orderId !== null && !B32.test(entry.orderId)) ||
72
+ typeof entry.paymentSubmitted !== "boolean" ||
73
+ (entry.paymentPayload !== null &&
74
+ typeof entry.paymentPayload !== "object"),
75
+ )
76
+ ) {
77
+ throw coded(
78
+ "INVOCATION_STATE_INVALID",
79
+ "User invocation state is incompatible or damaged; preserve the file and recover it manually",
80
+ );
81
+ }
82
+ for (const entry of Object.values(parsed.invocations))
83
+ if (
84
+ entry.routingVersion !== undefined &&
85
+ (entry.routingVersion !== "agentpay.runtime-call.v1" ||
86
+ typeof entry.serviceBinding !== "string")
87
+ )
88
+ throw coded(
89
+ "INVOCATION_STATE_INVALID",
90
+ "Invalid per-service routing state",
91
+ );
92
+ if (
93
+ parsed.timeEncoding !== undefined &&
94
+ parsed.timeEncoding !== "unix-ms"
95
+ )
96
+ throw coded(
97
+ "INVOCATION_STATE_INVALID",
98
+ "Unsupported invocation timestamp encoding; preserve the file",
99
+ );
100
+ for (const entry of Object.values(parsed.invocations)) {
101
+ try {
102
+ for (const key of ["createdAt", "updatedAt"]) {
103
+ if (
104
+ parsed.timeEncoding === "unix-ms" &&
105
+ !Object.hasOwn(entry, key + "Ms")
106
+ )
107
+ throw Error("Missing invocation epoch");
108
+ const ms = localTimeMs(entry, key);
109
+ entry[key + "Ms"] = ms;
110
+ entry[key] = new Date(ms).toISOString();
111
+ }
112
+ } catch {
113
+ throw coded(
114
+ "INVOCATION_STATE_INVALID",
115
+ "Invocation timestamp is invalid; preserve the file and recover it manually",
116
+ );
117
+ }
118
+ }
119
+ return parsed;
120
+ } catch (error) {
121
+ if (error?.code === "ENOENT")
122
+ return { version: "agentpay.user-invocations.v1", invocations: {} };
123
+ if (error instanceof SyntaxError)
124
+ throw coded(
125
+ "INVOCATION_STATE_INVALID",
126
+ "User invocation state is invalid JSON; preserve the file and recover it manually",
127
+ );
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async function save(document) {
133
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
134
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
135
+ const stored = {
136
+ ...document,
137
+ timeEncoding: "unix-ms",
138
+ invocations: Object.fromEntries(
139
+ Object.entries(document.invocations).map(([id, entry]) => {
140
+ const data = { ...entry };
141
+ for (const key of ["createdAt", "updatedAt"]) {
142
+ data[key + "Ms"] = localTimeMs(entry, key);
143
+ delete data[key];
144
+ }
145
+ return [id, data];
146
+ }),
147
+ ),
148
+ };
149
+ const handle = await open(temporary, "wx", 0o600);
150
+ try {
151
+ try {
152
+ await handle.writeFile(`${JSON.stringify(stored, null, 2)}\n`);
153
+ await handle.sync();
154
+ } finally {
155
+ await handle.close();
156
+ }
157
+ await rename(temporary, path);
158
+ const directory = await open(dirname(path), "r");
159
+ try {
160
+ await directory.sync();
161
+ } finally {
162
+ await directory.close();
163
+ }
164
+ } finally {
165
+ await unlink(temporary).catch((error) => {
166
+ if (error.code !== "ENOENT") throw error;
167
+ });
168
+ }
169
+ }
170
+
171
+ function checkBinding(entry, expected = serviceBinding, routing = null) {
172
+ if (
173
+ (entry.routingVersion ?? null) !== routing ||
174
+ (expected !== null && entry.serviceBinding !== expected) ||
175
+ (expected === null && entry.serviceBinding != null)
176
+ ) {
177
+ throw coded(
178
+ "INVOCATION_SERVICE_BINDING_MISMATCH",
179
+ "Preserve invocation history and restore its selected Skill before retrying",
180
+ );
181
+ }
182
+ }
183
+ return Object.freeze({
184
+ async inventory() {
185
+ const document = await load();
186
+ return structuredClone(document.invocations);
187
+ },
188
+ async verifyServiceBinding() {
189
+ const document = await load();
190
+ for (const entry of Object.values(document.invocations)) {
191
+ if (
192
+ entry.routingVersion === "agentpay.runtime-call.v1" &&
193
+ typeof entry.serviceBinding === "string"
194
+ )
195
+ continue;
196
+ checkBinding(entry);
197
+ }
198
+ },
199
+ async lookup({
200
+ invocationKey,
201
+ requestHash,
202
+ binding = serviceBinding,
203
+ routingVersion = null,
204
+ }) {
205
+ return locked(async () => {
206
+ const existing = (await load()).invocations[
207
+ hashInvocationKey(invocationKey)
208
+ ];
209
+ if (!existing)
210
+ throw coded(
211
+ "ORIGINAL_REQUEST_NOT_FOUND",
212
+ "Original Runtime history is missing. Do not create a replacement purchase.",
213
+ );
214
+ checkBinding(existing, binding, routingVersion);
215
+ if (existing.requestHash !== requestHash)
216
+ throw coded("INVOCATION_CONFLICT", "Original request input differs.");
217
+ return structuredClone(existing);
218
+ });
219
+ },
220
+ async begin({
221
+ invocationKey,
222
+ requestHash,
223
+ binding = serviceBinding,
224
+ routingVersion = null,
225
+ recoveryScope = null,
226
+ readRecoveryScope = null,
227
+ originalOrderRequestId = null,
228
+ }) {
229
+ if (
230
+ routingVersion !== null &&
231
+ routingVersion !== "agentpay.runtime-call.v1"
232
+ )
233
+ throw coded(
234
+ "INVOCATION_STATE_INVALID",
235
+ "Unsupported invocation routing version",
236
+ );
237
+ return locked(async () => {
238
+ const document = await load();
239
+ const invocationId = hashInvocationKey(invocationKey);
240
+ const existing = document.invocations[invocationId];
241
+ if (
242
+ originalOrderRequestId !== null &&
243
+ (!B32.test(originalOrderRequestId) ||
244
+ !existing ||
245
+ existing.orderRequestId !== originalOrderRequestId)
246
+ )
247
+ throw coded(
248
+ "ORIGINAL_REQUEST_NOT_FOUND",
249
+ "The original Runtime record changed or disappeared. No replacement purchase was created.",
250
+ );
251
+ if (existing) {
252
+ checkBinding(existing, binding, routingVersion);
253
+ if (existing.requestHash !== requestHash)
254
+ throw coded(
255
+ "INVOCATION_CONFLICT",
256
+ "Idempotency-Key was already used with a different request",
257
+ );
258
+ return structuredClone(existing);
259
+ }
260
+ if (readRecoveryScope) {
261
+ recoveryScope = await readRecoveryScope();
262
+ if (
263
+ !recoveryScope ||
264
+ Object.keys(recoveryScope).sort().join(",") !==
265
+ "module,network,policyId,safe" ||
266
+ !/^eip155:[1-9][0-9]*$/.test(recoveryScope.network) ||
267
+ ![recoveryScope.safe, recoveryScope.module].every((v) =>
268
+ /^0x[0-9a-f]{40}$/i.test(v),
269
+ ) ||
270
+ !B32.test(String(recoveryScope.policyId).toLowerCase())
271
+ )
272
+ throw coded(
273
+ "RUNTIME_RECOVERY_REQUIRED",
274
+ "Wallet authorization is unavailable. Complete or update Wallet setup before starting a new request.",
275
+ );
276
+ }
277
+ const atMs = currentTimeMs();
278
+ const at = new Date(atMs).toISOString();
279
+ const created = {
280
+ invocationId,
281
+ requestHash,
282
+ orderRequestId: randomB32(),
283
+ commitmentNonce: randomB32(),
284
+ orderId: null,
285
+ paymentPayload: null,
286
+ paymentSubmitted: false,
287
+ ...(recoveryScope ? { recoveryScope } : {}),
288
+ ...(binding !== null ? { serviceBinding: binding } : {}),
289
+ ...(routingVersion
290
+ ? { routingVersion, quoteAcceptanceRequired: true }
291
+ : {}),
292
+ createdAt: at,
293
+ updatedAt: at,
294
+ createdAtMs: atMs,
295
+ updatedAtMs: atMs,
296
+ };
297
+ document.invocations[invocationId] = created;
298
+ await save(document);
299
+ return structuredClone(created);
300
+ });
301
+ },
302
+ async attachRecoveredOrder(expected, orderId, recoveryScope) {
303
+ if (!B32.test(orderId))
304
+ throw coded("ORDER_IDENTITY_MISMATCH", "Invalid recovered Order");
305
+ return locked(async () => {
306
+ const document = await load(),
307
+ existing = document.invocations[expected.invocationId];
308
+ if (
309
+ !existing ||
310
+ existing.orderId ||
311
+ JSON.stringify(existing) !== JSON.stringify(expected)
312
+ )
313
+ return false;
314
+ document.invocations[expected.invocationId] = {
315
+ ...existing,
316
+ orderId,
317
+ ...(recoveryScope ? { recoveryScope } : {}),
318
+ updatedAtMs: currentTimeMs(),
319
+ };
320
+ document.invocations[expected.invocationId].updatedAt = new Date(
321
+ document.invocations[expected.invocationId].updatedAtMs,
322
+ ).toISOString();
323
+ await save(document);
324
+ return true;
325
+ });
326
+ },
327
+ async update(invocationId, expectedRequestHash, changes) {
328
+ return locked(async () => {
329
+ const document = await load();
330
+ const existing = document.invocations[invocationId];
331
+ if (!existing || existing.requestHash !== expectedRequestHash)
332
+ throw coded(
333
+ "INVOCATION_NOT_FOUND",
334
+ "durable invocation checkpoint was not found",
335
+ );
336
+ if (
337
+ existing.recoveryScope &&
338
+ changes.recoveryScope &&
339
+ JSON.stringify(existing.recoveryScope) !==
340
+ JSON.stringify(changes.recoveryScope)
341
+ )
342
+ throw coded(
343
+ "ORDER_IDENTITY_MISMATCH",
344
+ "Original authorization cannot be replaced",
345
+ );
346
+ const atMs = currentTimeMs();
347
+ const updated = {
348
+ ...existing,
349
+ ...changes,
350
+ invocationId: existing.invocationId,
351
+ requestHash: existing.requestHash,
352
+ orderRequestId: existing.orderRequestId,
353
+ ...(existing.serviceBinding !== undefined
354
+ ? { serviceBinding: existing.serviceBinding }
355
+ : {}),
356
+ ...(existing.routingVersion !== undefined
357
+ ? { routingVersion: existing.routingVersion }
358
+ : {}),
359
+ updatedAt: new Date(atMs).toISOString(),
360
+ updatedAtMs: atMs,
361
+ };
362
+ document.invocations[invocationId] = updated;
363
+ await save(document);
364
+ return structuredClone(updated);
365
+ });
366
+ },
367
+ });
368
+ }
369
+
370
+ function hashInvocationKey(value) {
371
+ const normalized = String(value || "").trim();
372
+ if (!/^[A-Za-z0-9._:-]{8,128}$/.test(normalized))
373
+ throw coded(
374
+ "INVALID_IDEMPOTENCY_KEY",
375
+ "Idempotency-Key must contain 8-128 safe characters",
376
+ );
377
+ return `0x${createHash("sha256").update(`agentpay.user-invocation.v1:${normalized}`).digest("hex")}`;
378
+ }
379
+
380
+ function randomB32() {
381
+ return `0x${randomBytes(32).toString("hex")}`;
382
+ }
383
+ function coded(code, message) {
384
+ return Object.assign(new Error(message), { code });
385
+ }
@@ -0,0 +1,117 @@
1
+ import { installationDirectory, legacyInstallationDirectory } from "./installation-paths.mjs";
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
5
+ import { constants } from "node:fs";
6
+ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
7
+ import { dirname, isAbsolute, join, resolve } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { fileURLToPath } from "node:url";
10
+ import { randomUUID } from "node:crypto";
11
+ export const installedUserDir = fileURLToPath(new URL("..", import.meta.url));
12
+ const stateDirectory = installationDirectory;
13
+ function privateInfo(info) {
14
+ return (
15
+ info.uid === process.getuid() &&
16
+ (info.mode & 0o077) === 0 &&
17
+ !info.isSymbolicLink()
18
+ );
19
+ }
20
+ async function readState(userDir, directory = stateDirectory(userDir)) {
21
+ try {
22
+ const info = await lstat(directory);
23
+ if (!info.isDirectory() || !privateInfo(info))
24
+ throw Error(
25
+ "Installation record directory must be private and owned by you.",
26
+ );
27
+ } catch (error) {
28
+ if (error.code === "ENOENT") {
29
+ if (directory === stateDirectory(userDir)) return readState(userDir, legacyInstallationDirectory(userDir));
30
+ return {};
31
+ }
32
+ throw error;
33
+ }
34
+ let file;
35
+ try {
36
+ file = await open(
37
+ join(directory, "state.json"),
38
+ constants.O_RDONLY | constants.O_NOFOLLOW,
39
+ );
40
+ const info = await file.stat();
41
+ if (!info.isFile() || !privateInfo(info) || info.size > 65536)
42
+ throw Error("Installation record is not a private regular file.");
43
+ const state = JSON.parse(await file.readFile("utf8"));
44
+ if (
45
+ state.version !== 1 ||
46
+ (state.dataDir !== undefined &&
47
+ (typeof state.dataDir !== "string" ||
48
+ !isAbsolute(state.dataDir) ||
49
+ state.dataDir.includes("\0")))
50
+ )
51
+ throw Error("Installation record is invalid.");
52
+ return state;
53
+ } catch (error) {
54
+ if (error.code === "ENOENT") {
55
+ if (directory === stateDirectory(userDir)) return readState(userDir, legacyInstallationDirectory(userDir));
56
+ return {};
57
+ }
58
+ throw error;
59
+ } finally {
60
+ await file?.close();
61
+ }
62
+ }
63
+ export async function resolveUserDataDir({
64
+ argument,
65
+ env = process.env,
66
+ userDir = installedUserDir,
67
+ } = {}) {
68
+ if (argument || env.AGENTPAY_USER_DATA_DIR)
69
+ return resolve(argument || env.AGENTPAY_USER_DATA_DIR);
70
+ const state = await readState(userDir);
71
+ return state.dataDir || join(homedir(), ".local/share/agentpay/user");
72
+ }
73
+ // Uses the existing installer lock and preserves dependency/build checkpoints.
74
+ export async function rememberUserDataDir({
75
+ userDir = installedUserDir,
76
+ dataDir,
77
+ }) {
78
+ const directory = stateDirectory(userDir);
79
+ await mkdir(directory, { recursive:true, mode: 0o700 }).catch((e) => {
80
+ if (e.code !== "EEXIST") throw e;
81
+ });
82
+ const info = await lstat(directory);
83
+ if (!info.isDirectory() || !privateInfo(info))
84
+ throw Error(
85
+ "Installation record directory must be private and owned by you.",
86
+ );
87
+ const lockPath = join(directory, "install.lock");
88
+ let lock;
89
+ try {
90
+ lock = await open(lockPath, "wx", 0o600);
91
+ } catch (error) {
92
+ if (error.code === "EEXIST")
93
+ throw Error(
94
+ "Another installation may be running. Retry after it finishes.",
95
+ );
96
+ throw error;
97
+ }
98
+ const temp = join(directory, `location-${randomUUID()}.tmp`);
99
+ let file;
100
+ try {
101
+ const state = await readState(userDir);
102
+ file = await open(temp, "wx", 0o600);
103
+ await file.writeFile(
104
+ JSON.stringify({ ...state, version: 1, dataDir: resolve(dataDir) }) +
105
+ "\n",
106
+ );
107
+ await file.sync();
108
+ await file.close();
109
+ file = null;
110
+ await rename(temp, join(directory, "state.json"));
111
+ } finally {
112
+ await file?.close();
113
+ await unlink(temp).catch(() => {});
114
+ await lock.close();
115
+ await unlink(lockPath);
116
+ }
117
+ }
@@ -0,0 +1,181 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
3
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
4
+ import { constants } from "node:fs";
5
+ import { open, lstat } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { resolveUserDataDir } from "./local-data-directory.mjs";
8
+
9
+ const address = (value) =>
10
+ typeof value === "string" && /^0x[0-9a-f]{40}$/i.test(value)
11
+ ? value
12
+ : "Not recorded";
13
+ const policyId = (value) =>
14
+ typeof value === "string" && /^0x[0-9a-f]{64}$/i.test(value)
15
+ ? value
16
+ : "Not recorded";
17
+ const text = (value) =>
18
+ typeof value === "string"
19
+ ? value.replace(/[\x00-\x1f\x7f-\x9f]/g, "").slice(0, 160)
20
+ : "Not recorded";
21
+ function endpoint(value) {
22
+ try {
23
+ const u = new URL(value);
24
+ if (
25
+ !["http:", "https:"].includes(u.protocol) ||
26
+ u.username ||
27
+ u.password ||
28
+ u.search ||
29
+ u.hash
30
+ )
31
+ return "Not shown (inspect local settings)";
32
+ return u.origin + u.pathname;
33
+ } catch {
34
+ return "Not recorded";
35
+ }
36
+ }
37
+ async function privateJson(dataDir, relative) {
38
+ for (const directory of [
39
+ dataDir,
40
+ ...(relative.includes("/") ? [join(dataDir, relative.split("/")[0])] : []),
41
+ ]) {
42
+ try {
43
+ const st = await lstat(directory);
44
+ if (
45
+ !st.isDirectory() ||
46
+ st.isSymbolicLink() ||
47
+ st.uid !== process.getuid() ||
48
+ st.mode & 0o077
49
+ )
50
+ throw Error("unsafe");
51
+ } catch (e) {
52
+ if (e.code === "ENOENT") return null;
53
+ throw e;
54
+ }
55
+ }
56
+ let h;
57
+ try {
58
+ h = await open(
59
+ join(dataDir, relative),
60
+ constants.O_RDONLY | constants.O_NOFOLLOW,
61
+ );
62
+ const st = await h.stat();
63
+ if (
64
+ !st.isFile() ||
65
+ st.uid !== process.getuid() ||
66
+ st.mode & 0o077 ||
67
+ st.size > 65536
68
+ )
69
+ throw Error("unsafe");
70
+ return JSON.parse(await h.readFile("utf8"));
71
+ } catch (e) {
72
+ if (e.code === "ENOENT") return null;
73
+ throw e;
74
+ } finally {
75
+ await h?.close();
76
+ }
77
+ }
78
+ // Display only selected public fields. Never dump files or query/unlock Wallet.
79
+ export async function showLocalInfo({
80
+ resolveDirectory = resolveUserDataDir,
81
+ write = console.log,
82
+ } = {}) {
83
+ let dataDir;
84
+ try {
85
+ dataDir = await resolveDirectory();
86
+ } catch {
87
+ write(
88
+ "Could not read your saved User directory. Check the installation record.",
89
+ );
90
+ return 1;
91
+ }
92
+ let incomplete = false;
93
+ const read = async (name) => {
94
+ try {
95
+ return await privateJson(dataDir, name);
96
+ } catch {
97
+ incomplete = true;
98
+ write(
99
+ `Could not safely read ${name}. Check its ownership, permissions and format.`,
100
+ );
101
+ return null;
102
+ }
103
+ };
104
+ const key = await read("wallet/session.keystore.json"),
105
+ policy = await read("wallet/policy.json"),
106
+ services = await read("runtime/local-services.json"),
107
+ connection = await read("runtime/connection.json"),
108
+ market = await read("market-connection.json");
109
+ const keyAddress = typeof key?.address === "string" ? key.address : "";
110
+ const delegate = address(
111
+ keyAddress.startsWith("0x") ? keyAddress : `0x${keyAddress}`,
112
+ );
113
+ const divider =
114
+ "------------------------------------------------------------";
115
+ const section = (title) => ["", divider, `> ${title}`, divider];
116
+ const field = (label, value) => [` ${label}`, ` ${value}`];
117
+ const lines = [
118
+ divider,
119
+ "> Wallet information",
120
+ divider,
121
+ "Saved settings · live status: agentpay status",
122
+ ...section("Wallet & authorization"),
123
+ ` Network: ${Number.isSafeInteger(policy?.chainId) && policy.chainId > 0 ? `eip155:${policy.chainId}` : "Not recorded"}`,
124
+ ...field("Safe account", address(policy?.safe)),
125
+ ...field("Policy ID", policyId(policy?.policyId)),
126
+ ...field("Wallet execution address", delegate),
127
+ ...field("AgentPay Module", address(policy?.module)),
128
+ ];
129
+ if (
130
+ policy &&
131
+ delegate !== "Not recorded" &&
132
+ delegate.toLowerCase() !== String(policy.delegate).toLowerCase()
133
+ ) {
134
+ incomplete = true;
135
+ lines.push(
136
+ " [!] Saved Policy delegate differs from this key. Review before using it.",
137
+ );
138
+ }
139
+ lines.push(
140
+ ...section("Local files"),
141
+ ...field("Data folder", dataDir),
142
+ " Files inside this folder:",
143
+ " wallet/session.keystore.json Encrypted Session Key",
144
+ " wallet/policy.json Policy binding",
145
+ " runtime/local-services.json Startup settings",
146
+ " runtime/invocations.json Order history",
147
+ " runtime/invocations.json.results/ Verified result records",
148
+ " runtime/invocations.json.pending-results/ Results awaiting verification",
149
+ " runtime/invocations.json.files/ Saved file copies · agentpay orders files ORDER_ID",
150
+ ...section("Service connections"),
151
+ ...field(
152
+ "Local Runtime · Skills connect here",
153
+ endpoint(services?.runtimeUrl || connection?.runtimeUrl),
154
+ ),
155
+ ...field(
156
+ "Settlement · Platform order service",
157
+ endpoint(services?.settlementUrl),
158
+ ),
159
+ ...section("Market"),
160
+ );
161
+ if (market)
162
+ lines.push(
163
+ ...field("Saved website", endpoint(market.marketOrigin)),
164
+ ` Wallet nickname: ${text(market.connection?.label)}`,
165
+ ...field("Saved connection ID", text(market.connection?.id)),
166
+ " Current connection status is shown on the website.",
167
+ );
168
+ else lines.push(" No saved connection. To connect: agentpay market connect");
169
+ lines.push(
170
+ ...section("Backup"),
171
+ " Keep a private backup of this data folder and your unlock password.",
172
+ " This includes your key, Order records, private results and download links. Do not share it.",
173
+ );
174
+ if (!key)
175
+ lines.push(
176
+ "",
177
+ "No readable Session Key found. Check your data directory before creating a new Wallet.",
178
+ );
179
+ write(lines.join("\n"));
180
+ return incomplete ? 1 : 0;
181
+ }