@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,67 @@
1
+ // Explicit owner signer selection. A chain ID never implicitly grants access to
2
+ // unlocked test accounts; external owners approve fixed plans outside local Wallet.
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 { constants } from "node:fs";
7
+ import { open } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+
10
+ export function ownerSignerKind(value = "external") {
11
+ if (!["external", "anvil"].includes(value))
12
+ throw Error("Owner signer must be external or anvil.");
13
+ return value;
14
+ }
15
+ async function read(path) {
16
+ let h;
17
+ try {
18
+ h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
19
+ const st = await h.stat();
20
+ if (
21
+ !st.isFile() ||
22
+ st.uid !== process.getuid() ||
23
+ st.mode & 0o077 ||
24
+ st.size > 4096
25
+ )
26
+ throw Error("Unsafe owner signer settings.");
27
+ return JSON.parse(await h.readFile("utf8"));
28
+ } catch (e) {
29
+ if (e.code === "ENOENT") return null;
30
+ throw e;
31
+ } finally {
32
+ await h?.close();
33
+ }
34
+ }
35
+ export async function selectedOwnerSigner({
36
+ dataDir,
37
+ choice,
38
+ readState = read,
39
+ env = process.env,
40
+ } = {}) {
41
+ const explicit = choice ?? env.AGENTPAY_OWNER_SIGNER;
42
+ if (explicit !== undefined) return ownerSignerKind(explicit);
43
+ const saved = await readState(join(dataDir, "runtime", "owner-signer.json"));
44
+ if (saved === null || saved === undefined) return "external";
45
+ if (
46
+ saved.version !== 1 ||
47
+ Object.keys(saved).some((k) => !["version", "kind"].includes(k))
48
+ )
49
+ throw Error("Invalid owner signer settings.");
50
+ return ownerSignerKind(saved.kind);
51
+ }
52
+ // This guard is independent of any chain mode label downloaded from Market.
53
+ export function checkTestSigner(kind, source) {
54
+ ownerSignerKind(kind);
55
+ if (kind !== "anvil") return;
56
+ const rpc = new URL(source.registryRpcUrl || source.rpcUrl);
57
+ if (
58
+ source.chainId !== 31337 ||
59
+ rpc.protocol !== "http:" ||
60
+ !["127.0.0.1", "localhost", "[::1]"].includes(rpc.hostname) ||
61
+ rpc.username ||
62
+ rpc.password ||
63
+ rpc.search ||
64
+ rpc.hash
65
+ )
66
+ throw Error("Anvil owner signer requires chain 31337 and loopback HTTP.");
67
+ }
@@ -0,0 +1,38 @@
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 { resolveUserDataDir } from "./local-data-directory.mjs";
5
+ import { readAuthorizationStatus } from "./status-panel.mjs";
6
+ export async function showPolicyInfo({
7
+ write = console.log,
8
+ resolveDirectory = resolveUserDataDir,
9
+ } = {}) {
10
+ try {
11
+ const p = await readAuthorizationStatus(await resolveDirectory());
12
+ write(
13
+ "\n> Spending permission\n Policy ID: " +
14
+ p.policyId +
15
+ "\n Valid until: " +
16
+ new Date(p.validUntil * 1000).toLocaleString(),
17
+ );
18
+ write(
19
+ " Status: " +
20
+ (p.revoked
21
+ ? "Revoked (last verified)"
22
+ : p.validUntil <= Math.floor(Date.now() / 1000)
23
+ ? "Expired"
24
+ : "Within saved validity period"),
25
+ );
26
+ write(
27
+ "\nRenew or change limits: agentpay policy renew\nRevoke permission: agentpay policy revoke\nWallet and network details: agentpay wallet info",
28
+ );
29
+ return 0;
30
+ } catch (e) {
31
+ write(
32
+ e.code === "ENOENT"
33
+ ? "No saved spending permission. Continue agentpay setup."
34
+ : "Could not read the saved permission. Check agentpay wallet info.",
35
+ );
36
+ return 1;
37
+ }
38
+ }
@@ -0,0 +1,84 @@
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 { unixSecondsToMilliseconds } from "@c4roa0/agentpay-protocol";
5
+ import { readFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import {
8
+ assertPrivateFile,
9
+ atomicWritePrivate,
10
+ } from "./private-journal-files.mjs";
11
+ // Public user-facing projection. Chain verification and local switching belong to Wallet.
12
+ export function migrationSummary(plan) {
13
+ return {
14
+ migrationId: plan.migrationId,
15
+ chainId: plan.source.chainId,
16
+ safe: plan.source.safe,
17
+ delegate: plan.source.delegate,
18
+ token: plan.source.token,
19
+ registry: plan.source.registry,
20
+ oldPolicyId: plan.source.policyId,
21
+ newPolicyId: plan.destination.policyId,
22
+ oldModule: plan.source.module,
23
+ newModule: plan.destination.module,
24
+ oldFactory: plan.source.factory,
25
+ newFactory: plan.destination.factory,
26
+ originalBudgetAtomic: plan.sourceTerms.totalBudget,
27
+ alreadySpentAtomic: plan.sourceTerms.spent,
28
+ replacementBudgetAtomic: plan.totalBudget,
29
+ oldPerPaymentAtomic: plan.source.maxAmount,
30
+ newPerPaymentAtomic: plan.destination.maxAmount,
31
+ validAfter: plan.validAfter,
32
+ validUntil: plan.destination.validUntil,
33
+ expiresAt: new Date(
34
+ unixSecondsToMilliseconds(plan.destination.validUntil),
35
+ ).toISOString(),
36
+ expiresAtMs: unixSecondsToMilliseconds(plan.destination.validUntil),
37
+ note: "Migration pauses Wallet signing. Revoke the old Policy before creating the replacement. No budget increase or expiry extension.",
38
+ };
39
+ }
40
+
41
+ export function migratedPublicPolicy(plan) {
42
+ const p = plan.destination;
43
+ return {
44
+ chainId: p.chainId,
45
+ rpcUrl: p.registryRpcUrl,
46
+ safe: p.safe,
47
+ module: p.module,
48
+ factory: p.factory,
49
+ registry: p.registry,
50
+ token: p.token,
51
+ policyId: p.policyId,
52
+ maxPerPayment: p.maxAmount,
53
+ totalBudget: plan.totalBudget,
54
+ validAfter: plan.validAfter,
55
+ validUntil: p.validUntil,
56
+ };
57
+ }
58
+
59
+ export async function syncMigratedPublicPolicy(dataDir, plan) {
60
+ const path = join(dataDir, "public-policy.json");
61
+ await assertPrivateFile(path);
62
+ const current = JSON.parse(await readFile(path, "utf8"));
63
+ const matches = (p) =>
64
+ [
65
+ "chainId",
66
+ "safe",
67
+ "module",
68
+ "factory",
69
+ "registry",
70
+ "token",
71
+ "policyId",
72
+ "validUntil",
73
+ ].every(
74
+ (key) =>
75
+ String(current[key]).toLowerCase() === String(p[key]).toLowerCase(),
76
+ );
77
+ if (!matches(plan.source) && !matches(plan.destination))
78
+ throw new Error("PUBLIC_POLICY_CONFIG_CONFLICT");
79
+ const replacement = { ...current, ...migratedPublicPolicy(plan) };
80
+ // The Wallet journal already preserves the old binding and on-chain budget.
81
+ // If this write fails, rerunning complete verifies the chain and retries only this projection.
82
+ await atomicWritePrivate(path, JSON.stringify(replacement, null, 2));
83
+ return path;
84
+ }
@@ -0,0 +1,67 @@
1
+ // Resumable owner-authorized permission changes. Keep original operation identity
2
+ // and verify chain effects before updating local binding or continuing connection.
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 { approveOwnerOperations } from "./owner-approval-server.mjs";
7
+ import { ownerSignerKind, checkTestSigner } from "./owner-signer.mjs";
8
+ import { Contract, Interface } from "ethers";
9
+ import { executeAnvilOwnerCall } from "./safe-owner-anvil.mjs";
10
+ const abi = [
11
+ "function policies(address,bytes32) view returns(address delegate,address token,address factory,address registry,uint256 maxPerPayment,uint256 totalBudget,uint256 spent,uint64 validAfter,uint64 validUntil,bool revoked)",
12
+ "function revokePolicy(bytes32 policyId)",
13
+ ];
14
+ export async function revokeOnChain({
15
+ rpc,
16
+ policy,
17
+ write = console.log,
18
+ submit = executeAnvilOwnerCall,
19
+ ownerSigner = "external",
20
+ dataDir,
21
+ ask,
22
+ approve = approveOwnerOperations,
23
+ }) {
24
+ const signer = ownerSignerKind(ownerSigner);
25
+ checkTestSigner(signer, policy);
26
+ const contract = new Contract(policy.module, abi, rpc);
27
+ const current = await contract.policies(policy.safe, policy.policyId);
28
+ if (current.delegate.toLowerCase() !== policy.delegate.toLowerCase())
29
+ throw Error("Policy delegate could not be verified.");
30
+ if (current.revoked) return true;
31
+ const call = {
32
+ purpose: "revoke previous spending permission",
33
+ to: policy.module,
34
+ value: "0",
35
+ operation: 0,
36
+ data: new Interface(abi).encodeFunctionData("revokePolicy", [
37
+ policy.policyId,
38
+ ]),
39
+ };
40
+ if (signer === "external")
41
+ return approve({
42
+ source: policy,
43
+ calls: [call],
44
+ dataDir,
45
+ ask,
46
+ write,
47
+ check: async () =>
48
+ Boolean(
49
+ (await contract.policies(policy.safe, policy.policyId)).revoked,
50
+ ),
51
+ });
52
+ const safe = new Contract(
53
+ policy.safe,
54
+ [
55
+ "function getOwners() view returns(address[])",
56
+ "function getThreshold() view returns(uint256)",
57
+ ],
58
+ rpc,
59
+ );
60
+ const owners = await safe.getOwners();
61
+ if (Number(await safe.getThreshold()) !== 1 || owners.length !== 1)
62
+ throw Error("Private rehearsal requires the sole prepared Safe owner.");
63
+ await submit({ source: policy }, owners[0], call);
64
+ if (!(await contract.policies(policy.safe, policy.policyId)).revoked)
65
+ throw Error("Policy revocation has not been verified.");
66
+ return true;
67
+ }
@@ -0,0 +1,236 @@
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 {
5
+ assertUnixMilliseconds,
6
+ parseLegacyIsoMilliseconds,
7
+ } from "@c4roa0/agentpay-protocol";
8
+ import { isIP } from "node:net";
9
+ import { createServer } from "node:http";
10
+ import { randomBytes } from "node:crypto";
11
+ import { readFile } from "node:fs/promises";
12
+ import { parseUnits, JsonRpcProvider } from "ethers";
13
+ import { buildSafePolicyPlan, inspectSafePolicy } from "./safe-policy-plan.mjs";
14
+
15
+ export function createPolicyPreviewServer({
16
+ deployment,
17
+ delegate,
18
+ demoNetworkPreview = false,
19
+ inspect = async (plan) => {
20
+ const rpc = new JsonRpcProvider(plan.rpcUrl);
21
+ try {
22
+ return await inspectSafePolicy(rpc, plan);
23
+ } finally {
24
+ rpc.destroy();
25
+ }
26
+ },
27
+ }) {
28
+ const token = randomBytes(32).toString("hex");
29
+ const policyId = `0x${randomBytes(32).toString("hex")}`;
30
+ const trusted = Object.fromEntries(
31
+ ["chainId", "rpcUrl", "module", "token", "factory", "registry"].map((k) => [
32
+ k,
33
+ deployment[k],
34
+ ]),
35
+ );
36
+ const decimals = deployment.tokenDecimals;
37
+ if (
38
+ !Number.isInteger(decimals) ||
39
+ decimals < 0 ||
40
+ decimals > 18 ||
41
+ typeof deployment.source !== "string" ||
42
+ !deployment.source.trim()
43
+ ) {
44
+ throw new Error(
45
+ "Public deployment file requires tokenDecimals (0–18) and source",
46
+ );
47
+ }
48
+ buildSafePolicyPlan(
49
+ {
50
+ ...trusted,
51
+ safe: delegate,
52
+ policyId,
53
+ maxPerPayment: "1",
54
+ totalBudget: "1",
55
+ validAfter: 0,
56
+ validUntil: Math.floor(Date.now() / 1000) + 3600,
57
+ },
58
+ delegate,
59
+ );
60
+ let busy = false;
61
+ return createServer(async (req, res) => {
62
+ const localOrigin = `http://127.0.0.1:${res.socket.localPort}`;
63
+ let origin = localOrigin;
64
+ let validHost = req.headers.host === new URL(localOrigin).host;
65
+ if (demoNetworkPreview) {
66
+ try {
67
+ const requested = new URL(`http://${req.headers.host}`);
68
+ validHost =
69
+ (!!isIP(requested.hostname) || requested.hostname === "localhost") &&
70
+ requested.host === req.headers.host;
71
+ if (validHost) origin = requested.origin;
72
+ } catch {
73
+ validHost = false;
74
+ }
75
+ }
76
+ const send = (status, data) => {
77
+ res.writeHead(status, {
78
+ "content-type": "application/json; charset=utf-8",
79
+ });
80
+ res.end(JSON.stringify(data));
81
+ };
82
+ res.setHeader("cache-control", "no-store");
83
+ res.setHeader("x-content-type-options", "nosniff");
84
+ res.setHeader(
85
+ "content-security-policy",
86
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; form-action 'self'; base-uri 'none'",
87
+ );
88
+ if (!validHost || req.headers["sec-fetch-site"] === "cross-site")
89
+ return send(403, { error: "Use the local preview address directly" });
90
+ try {
91
+ if (req.method === "GET" && req.url === "/") {
92
+ res.setHeader(
93
+ "set-cookie",
94
+ `preview=${token}; HttpOnly; SameSite=Strict; Path=/`,
95
+ );
96
+ res.setHeader("content-type", "text/html; charset=utf-8");
97
+ return res.end(
98
+ await readFile(
99
+ new URL("../web/policy-preview.html", import.meta.url),
100
+ "utf8",
101
+ ),
102
+ );
103
+ }
104
+ if (req.method === "GET" && req.url === "/preview.js") {
105
+ res.setHeader("content-type", "text/javascript; charset=utf-8");
106
+ return res.end(
107
+ await readFile(
108
+ new URL("../web/policy-preview.js", import.meta.url),
109
+ "utf8",
110
+ ),
111
+ );
112
+ }
113
+ if (
114
+ !String(req.headers.cookie || "")
115
+ .split(";")
116
+ .some((item) => item.trim() === `preview=${token}`)
117
+ )
118
+ return send(403, { error: "Open the local page first" });
119
+ if (req.method === "GET" && req.url === "/context")
120
+ return send(200, {
121
+ deployment: {
122
+ ...trusted,
123
+ tokenDecimals: decimals,
124
+ source: deployment.source,
125
+ },
126
+ delegate,
127
+ policyId,
128
+ });
129
+ if (req.method !== "POST" || req.url !== "/preview")
130
+ return send(404, { error: "Not found" });
131
+ if (
132
+ req.headers.origin !== origin ||
133
+ req.headers["content-type"] !== "application/json" ||
134
+ req.headers["x-agentpay-preview"] !== "1"
135
+ )
136
+ return send(403, { error: "Local page request required" });
137
+ if (busy)
138
+ return send(409, {
139
+ error: "A preview is already running; wait and retry",
140
+ });
141
+ busy = true;
142
+ try {
143
+ let body = "";
144
+ for await (const chunk of req) {
145
+ body += chunk.toString();
146
+ if (Buffer.byteLength(body) > 4096)
147
+ return send(413, { error: "Request too large" });
148
+ }
149
+ const input = JSON.parse(body);
150
+ if (
151
+ Object.keys(input).some(
152
+ (k) =>
153
+ ![
154
+ "safe",
155
+ "budget",
156
+ "perPayment",
157
+ "validUntil",
158
+ "validUntilMs",
159
+ ].includes(k),
160
+ )
161
+ )
162
+ throw new Error("Unsupported preview field");
163
+ for (const key of ["budget", "perPayment"])
164
+ if (
165
+ typeof input[key] !== "string" ||
166
+ !/^\d+(\.\d+)?$/.test(input[key])
167
+ )
168
+ throw new Error("Amounts must be positive decimal values");
169
+ const until = Object.hasOwn(input, "validUntilMs")
170
+ ? assertUnixMilliseconds(input.validUntilMs)
171
+ : parseLegacyIsoMilliseconds(input.validUntil);
172
+ if (
173
+ input.validUntil !== undefined &&
174
+ parseLegacyIsoMilliseconds(input.validUntil) !== until
175
+ )
176
+ throw Error("Expiry representations disagree");
177
+ if (!Number.isFinite(until)) throw new Error("Choose a valid expiry");
178
+ const plan = buildSafePolicyPlan(
179
+ {
180
+ ...trusted,
181
+ safe: input.safe,
182
+ policyId,
183
+ totalBudget: parseUnits(input.budget, decimals).toString(),
184
+ maxPerPayment: parseUnits(input.perPayment, decimals).toString(),
185
+ validAfter: Math.floor(Date.now() / 1000),
186
+ validUntil: Math.floor(until / 1000),
187
+ },
188
+ delegate,
189
+ );
190
+ let state = null,
191
+ chainError = null;
192
+ try {
193
+ state = await Promise.race([
194
+ inspect(plan),
195
+ new Promise((_, reject) => {
196
+ const timer = setTimeout(
197
+ () => reject(new Error("timeout")),
198
+ 10000,
199
+ );
200
+ timer.unref();
201
+ }),
202
+ ]);
203
+ } catch {
204
+ chainError =
205
+ "链上只读核对未通过。检查 RPC、网络和 Safe/合约地址;不能继续签名。";
206
+ }
207
+ send(200, {
208
+ ok: true,
209
+ draft: {
210
+ safe: plan.safe,
211
+ delegate: plan.delegate,
212
+ chainId: plan.chainId,
213
+ budget: input.budget,
214
+ perPayment: input.perPayment,
215
+ validUntil: new Date(until).toISOString(),
216
+ validUntilMs: until,
217
+ policyId: plan.policyId,
218
+ calls: plan.calls,
219
+ },
220
+ chainState: state,
221
+ chainError,
222
+ readyToSign: false,
223
+ readyToPurchase: false,
224
+ mode: "preview_only",
225
+ });
226
+ } finally {
227
+ busy = false;
228
+ }
229
+ } catch {
230
+ send(400, {
231
+ error:
232
+ "请检查 Safe 地址、预算、单笔上限和未来的有效期;总预算不能小于单笔上限。",
233
+ });
234
+ }
235
+ });
236
+ }