@c4roa0/agentpay-user 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) 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 +85 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +107 -0
  9. package/doc/architecture.md +99 -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 +103 -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 +103 -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 +210 -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 +1233 -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 +316 -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/protocol-package.mjs +25 -0
  145. package/src/readiness-profile.mjs +30 -0
  146. package/src/renew-policy.mjs +605 -0
  147. package/src/repair-market-wallet.mjs +137 -0
  148. package/src/restore-wallet.mjs +255 -0
  149. package/src/result-cache-view.mjs +276 -0
  150. package/src/runtime-client.mjs +1145 -0
  151. package/src/runtime-connection-settings.mjs +101 -0
  152. package/src/runtime-recovery.mjs +667 -0
  153. package/src/runtime-status.mjs +32 -0
  154. package/src/safe-owner-anvil.mjs +115 -0
  155. package/src/safe-policy-plan.mjs +195 -0
  156. package/src/service-reference.mjs +233 -0
  157. package/src/service-url.mjs +52 -0
  158. package/src/settlement-response.mjs +69 -0
  159. package/src/setup-autostart.mjs +197 -0
  160. package/src/setup-demo.mjs +152 -0
  161. package/src/skill-bundle.mjs +142 -0
  162. package/src/skill-client.mjs +198 -0
  163. package/src/skill-install-prompt.mjs +31 -0
  164. package/src/skill-package.mjs +50 -0
  165. package/src/skill-zip.mjs +220 -0
  166. package/src/software-versions.mjs +48 -0
  167. package/src/status-panel.mjs +603 -0
  168. package/src/transport.mjs +48 -0
  169. package/src/uninstall-local.mjs +299 -0
  170. package/src/user-cli.mjs +354 -0
  171. package/src/user-setup-guide.mjs +193 -0
  172. package/src/wallet-control.mjs +182 -0
  173. package/src/wallet-ipc-error.mjs +16 -0
  174. package/src/wallet-package.mjs +28 -0
  175. package/update-pair.json +6 -0
@@ -0,0 +1,115 @@
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 { Contract, JsonRpcProvider, getAddress } from "ethers";
5
+ import { safeEthSignSignature } from "./safe-policy-plan.mjs";
6
+
7
+ // Rehearsal adapter only. Production sends the exported calls through the User's
8
+ // external owner wallet / Safe approval workflow, then uses the same chain checks.
9
+ export async function executeAnvilOwnerCall(plan, ownerInput, call) {
10
+ const url = new URL(plan.source.registryRpcUrl);
11
+ if (
12
+ plan.source.chainId !== 31337 ||
13
+ url.protocol !== "http:" ||
14
+ !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
15
+ )
16
+ throw new Error("ANVIL_OWNER_SIGNING_TEST_ONLY");
17
+ const owner = getAddress(ownerInput);
18
+ const rpc = new JsonRpcProvider(url.toString(), undefined, {
19
+ batchMaxCount: 1,
20
+ });
21
+ try {
22
+ if (BigInt(await rpc.send("eth_chainId", [])) !== 31337n)
23
+ throw new Error("CHAIN_ID_MISMATCH");
24
+ const safe = new Contract(
25
+ plan.source.safe,
26
+ [
27
+ "function getOwners() view returns (address[])",
28
+ "function getThreshold() view returns (uint256)",
29
+ "function nonce() view returns (uint256)",
30
+ "function getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256) view returns (bytes32)",
31
+ "function execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) returns (bool)",
32
+ "event ExecutionSuccess(bytes32 indexed txHash,uint256 payment)",
33
+ "event ExecutionFailure(bytes32 indexed txHash,uint256 payment)",
34
+ ],
35
+ await rpc.getSigner(owner),
36
+ );
37
+ if (
38
+ (await safe.getThreshold()) !== 1n ||
39
+ !(await safe.getOwners()).includes(owner)
40
+ )
41
+ throw new Error("ANVIL_OWNER_THRESHOLD_UNSUPPORTED");
42
+ const zero = "0x0000000000000000000000000000000000000000";
43
+ const nonce = await safe.nonce();
44
+ const hash = await safe.getTransactionHash(
45
+ call.to,
46
+ 0,
47
+ call.data,
48
+ 0,
49
+ 0,
50
+ 0,
51
+ 0,
52
+ zero,
53
+ zero,
54
+ nonce,
55
+ );
56
+ const signature = safeEthSignSignature(
57
+ hash,
58
+ await rpc.send("eth_sign", [owner, hash]),
59
+ owner,
60
+ );
61
+ const tx = await safe.execTransaction(
62
+ call.to,
63
+ 0,
64
+ call.data,
65
+ 0,
66
+ 0,
67
+ 0,
68
+ 0,
69
+ zero,
70
+ zero,
71
+ signature,
72
+ );
73
+ process.stdout.write(
74
+ `${JSON.stringify({ safeCall: call.purpose, transactionHash: tx.hash, state: "submitted" })}\n`,
75
+ );
76
+ const receipt = await tx.wait(1, 60_000);
77
+ const events =
78
+ receipt?.logs.map((log) => {
79
+ try {
80
+ return safe.interface.parseLog(log);
81
+ } catch {
82
+ return null;
83
+ }
84
+ }) ?? [];
85
+ if (
86
+ receipt?.status !== 1 ||
87
+ !events.some((e) => e?.name === "ExecutionSuccess") ||
88
+ events.some((e) => e?.name === "ExecutionFailure")
89
+ )
90
+ throw new Error("SAFE_OWNER_CALL_FAILED");
91
+ return {
92
+ safeCall: call.purpose,
93
+ transactionHash: tx.hash,
94
+ safeNonce: nonce.toString(),
95
+ confirmed: true,
96
+ };
97
+ } catch (error) {
98
+ // RPC/ethers errors may contain raw signed Safe calldata. Do not place that
99
+ // payload in terminal logs; the submitted hash above and chain status suffice
100
+ // for recovery after an ambiguous send or receipt timeout.
101
+ const publicCodes = new Set([
102
+ "CHAIN_ID_MISMATCH",
103
+ "ANVIL_OWNER_THRESHOLD_UNSUPPORTED",
104
+ "ANVIL_OWNER_SIGNATURE_MISMATCH",
105
+ "SAFE_OWNER_CALL_FAILED",
106
+ ]);
107
+ throw new Error(
108
+ publicCodes.has(error?.message)
109
+ ? error.message
110
+ : "SAFE_OWNER_CALL_UNCONFIRMED_RECHECK_CHAIN",
111
+ );
112
+ } finally {
113
+ rpc.destroy();
114
+ }
115
+ }
@@ -0,0 +1,195 @@
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
+ Contract,
6
+ getBytes,
7
+ hashMessage,
8
+ Interface,
9
+ getAddress,
10
+ isHexString,
11
+ recoverAddress,
12
+ Signature,
13
+ } from "ethers";
14
+
15
+ const ZERO = "0x0000000000000000000000000000000000000000";
16
+ const moduleAbi = [
17
+ "function createPolicy(bytes32 policyId,address delegate,address token,address factory,address registry,uint256 maxPerPayment,uint256 totalBudget,uint64 validAfter,uint64 validUntil)",
18
+ "function policies(address safe,bytes32 policyId) view returns (address delegate,address token,address factory,address registry,uint256 maxPerPayment,uint256 totalBudget,uint256 spent,uint64 validAfter,uint64 validUntil,bool revoked)",
19
+ ];
20
+ const safeAbi = [
21
+ "function getOwners() view returns (address[])",
22
+ "function getThreshold() view returns (uint256)",
23
+ "function isModuleEnabled(address) view returns (bool)",
24
+ ];
25
+ const enableInterface = new Interface([
26
+ "function enableModule(address module)",
27
+ ]);
28
+ const policyInterface = new Interface(moduleAbi);
29
+
30
+ export function safeEthSignSignature(hash, signedMessage, ownerAddress) {
31
+ if (
32
+ !isHexString(hash, 32) ||
33
+ recoverAddress(hashMessage(getBytes(hash)), signedMessage) !==
34
+ getAddress(ownerAddress)
35
+ ) {
36
+ throw new Error("ANVIL_OWNER_SIGNATURE_MISMATCH");
37
+ }
38
+ // Safe v1.4.x interprets v=31/32 as an EIP-191 eth_sign signature.
39
+ const parsed = Signature.from(signedMessage);
40
+ return `${signedMessage.slice(0, -2)}${(parsed.v + 4).toString(16).padStart(2, "0")}`;
41
+ }
42
+
43
+ function positive(value, name) {
44
+ if (typeof value !== "string" || !/^[1-9][0-9]*$/.test(value))
45
+ throw new Error(`${name} must be a positive atomic integer string`);
46
+ return BigInt(value);
47
+ }
48
+ function address(value, name) {
49
+ try {
50
+ return getAddress(value);
51
+ } catch {
52
+ throw new Error(`${name} must be an address`);
53
+ }
54
+ }
55
+ export function buildSafePolicyPlan(config, delegate) {
56
+ if (!config || typeof config !== "object" || Array.isArray(config))
57
+ throw new Error("POLICY_CONFIG_INVALID");
58
+ const chainId = Number(config.chainId);
59
+ if (!Number.isSafeInteger(chainId) || chainId < 1)
60
+ throw new Error("CHAIN_ID_INVALID");
61
+ const rpcUrl = new URL(config.rpcUrl);
62
+ if (
63
+ !(
64
+ rpcUrl.protocol === "https:" ||
65
+ (rpcUrl.protocol === "http:" &&
66
+ ["127.0.0.1", "localhost", "[::1]"].includes(rpcUrl.hostname))
67
+ )
68
+ )
69
+ throw new Error("RPC_URL_UNSAFE");
70
+ if (!isHexString(config.policyId, 32) || /^0x0{64}$/i.test(config.policyId))
71
+ throw new Error("POLICY_ID_INVALID");
72
+ const safe = address(config.safe, "safe");
73
+ const module = address(config.module, "module");
74
+ const token = address(config.token, "token");
75
+ const factory = address(config.factory, "factory");
76
+ const registry = address(config.registry, "registry");
77
+ const sessionDelegate = address(delegate, "delegate");
78
+ const maxPerPayment = positive(config.maxPerPayment, "maxPerPayment");
79
+ const totalBudget = positive(config.totalBudget, "totalBudget");
80
+ if (totalBudget < maxPerPayment)
81
+ throw new Error("BUDGET_BELOW_PER_PAYMENT_LIMIT");
82
+ const validAfter = Number(config.validAfter);
83
+ const validUntil = Number(config.validUntil);
84
+ if (
85
+ ![validAfter, validUntil].every(Number.isSafeInteger) ||
86
+ validAfter < 0 ||
87
+ validUntil <= validAfter ||
88
+ validUntil <= Math.floor(Date.now() / 1000)
89
+ )
90
+ throw new Error("POLICY_VALIDITY_INVALID");
91
+ const policyId = config.policyId.toLowerCase();
92
+ const enableData = enableInterface.encodeFunctionData("enableModule", [
93
+ module,
94
+ ]);
95
+ const createData = policyInterface.encodeFunctionData("createPolicy", [
96
+ policyId,
97
+ sessionDelegate,
98
+ token,
99
+ factory,
100
+ registry,
101
+ maxPerPayment,
102
+ totalBudget,
103
+ validAfter,
104
+ validUntil,
105
+ ]);
106
+ return Object.freeze({
107
+ chainId,
108
+ rpcUrl: rpcUrl.toString(),
109
+ safe,
110
+ module,
111
+ token,
112
+ factory,
113
+ registry,
114
+ delegate: sessionDelegate,
115
+ policyId,
116
+ maxPerPayment,
117
+ totalBudget,
118
+ validAfter,
119
+ validUntil,
120
+ calls: [
121
+ {
122
+ purpose: "enable AgentPay Module",
123
+ to: safe,
124
+ value: "0",
125
+ operation: 0,
126
+ data: enableData,
127
+ },
128
+ {
129
+ purpose: "create bounded Spending Policy",
130
+ to: module,
131
+ value: "0",
132
+ operation: 0,
133
+ data: createData,
134
+ },
135
+ ],
136
+ walletPolicy: {
137
+ version: "agentpay.safe-module.v1",
138
+ chainId,
139
+ module,
140
+ safe,
141
+ delegate: sessionDelegate,
142
+ policyId,
143
+ factory,
144
+ registry,
145
+ registryRpcUrl: rpcUrl.toString(),
146
+ token,
147
+ maxAmount: maxPerPayment.toString(),
148
+ validUntil,
149
+ },
150
+ });
151
+ }
152
+
153
+ export async function inspectSafePolicy(provider, plan) {
154
+ const chain = await provider.getNetwork();
155
+ if (Number(chain.chainId) !== plan.chainId)
156
+ throw new Error("CHAIN_ID_MISMATCH");
157
+ for (const [name, at] of [
158
+ ["safe", plan.safe],
159
+ ["module", plan.module],
160
+ ["token", plan.token],
161
+ ["factory", plan.factory],
162
+ ["registry", plan.registry],
163
+ ]) {
164
+ if ((await provider.getCode(at)) === "0x")
165
+ throw new Error(`${name.toUpperCase()}_CODE_MISSING`);
166
+ }
167
+ const safe = new Contract(plan.safe, safeAbi, provider);
168
+ const module = new Contract(plan.module, moduleAbi, provider);
169
+ const [owners, threshold, moduleEnabled, policy] = await Promise.all([
170
+ safe.getOwners(),
171
+ safe.getThreshold(),
172
+ safe.isModuleEnabled(plan.module),
173
+ module.policies(plan.safe, plan.policyId),
174
+ ]);
175
+ const existing = policy.delegate !== ZERO;
176
+ const policyMatches =
177
+ existing &&
178
+ policy.delegate === plan.delegate &&
179
+ policy.token === plan.token &&
180
+ policy.factory === plan.factory &&
181
+ policy.registry === plan.registry &&
182
+ policy.maxPerPayment === plan.maxPerPayment &&
183
+ policy.totalBudget === plan.totalBudget &&
184
+ Number(policy.validAfter) === plan.validAfter &&
185
+ Number(policy.validUntil) === plan.validUntil &&
186
+ !policy.revoked;
187
+ return {
188
+ owners,
189
+ threshold: Number(threshold),
190
+ moduleEnabled,
191
+ policyExists: existing,
192
+ policyMatches,
193
+ policySpent: existing ? policy.spent.toString() : null,
194
+ };
195
+ }
@@ -0,0 +1,233 @@
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
+ normalizeServiceReference,
6
+ canonicalizeServiceJson,
7
+ normalizeServiceResult,
8
+ AGENTPAY_SERVICE_INVOCATION_VERSION,
9
+ } from "@c4roa0/agentpay-protocol";
10
+ export const SERVICE_REFERENCE_VERSION = "agentpay.service-reference.v1";
11
+ export function parseServiceReference(value) {
12
+ try {
13
+ return normalizeServiceReference(value);
14
+ } catch {
15
+ throw coded(
16
+ "SERVICE_REFERENCE_INVALID",
17
+ "Service reference or registered profile hash is invalid.",
18
+ );
19
+ }
20
+ }
21
+ export function serviceRequest(value) {
22
+ try {
23
+ return JSON.parse(canonicalizeServiceJson(value));
24
+ } catch {
25
+ throw coded(
26
+ "INVALID_INPUT",
27
+ "Request must be bounded JSON with no more than 32 levels.",
28
+ );
29
+ }
30
+ }
31
+ export function usageCap(value = "1") {
32
+ if (typeof value !== "string" || !/^[1-9][0-9]{0,76}$/.test(value))
33
+ throw coded(
34
+ "INVALID_USAGE_CAP",
35
+ "Maximum usage must be a positive integer string.",
36
+ );
37
+ return value;
38
+ }
39
+ export function serviceContext(body, localUsageCap = "1000000") {
40
+ if (
41
+ !body ||
42
+ Object.keys(body)
43
+ .filter((k) => k !== "acceptedTerms")
44
+ .sort()
45
+ .join(",") !== "request,requestedMaxUsageUnits,service,version" ||
46
+ body.version !== "agentpay.runtime-call.v1"
47
+ )
48
+ throw coded(
49
+ "INVALID_INPUT",
50
+ "Use the versioned Runtime call with service, request and maximum usage.",
51
+ );
52
+ const reference = parseServiceReference(body.service),
53
+ requestedMaxUsageUnits = usageCap(body.requestedMaxUsageUnits);
54
+ if (BigInt(requestedMaxUsageUnits) > BigInt(usageCap(localUsageCap)))
55
+ throw coded(
56
+ "INVALID_USAGE_CAP",
57
+ "Requested usage exceeds the local Runtime cap.",
58
+ );
59
+ const request = serviceRequest(body.request);
60
+ return {
61
+ reference,
62
+ skillId: reference.skillId,
63
+ profileHash: reference.profileHash,
64
+ endpointUrl: reference.profile.paymentEndpoint,
65
+ adapter: reference.adapter,
66
+ requestedMaxUsageUnits,
67
+ request,
68
+ ...(body.acceptedTerms === undefined
69
+ ? {}
70
+ : { acceptedTerms: normalizeAcceptedTerms(body.acceptedTerms) }),
71
+ binding: JSON.stringify({
72
+ version: "agentpay.runtime-service.v1",
73
+ reference,
74
+ requestedMaxUsageUnits,
75
+ }),
76
+ };
77
+ }
78
+ export function assertPreparedService(result, context, requestHash) {
79
+ const order = result?.order,
80
+ profile = context.reference.profile;
81
+ if (
82
+ !order ||
83
+ order.skillId !== context.skillId ||
84
+ order.profileHash !== context.profileHash ||
85
+ order.requestHash !== requestHash ||
86
+ order.providerId !== profile.providerId ||
87
+ String(order.assetAddress).toLowerCase() !== profile.asset ||
88
+ `eip155:${order.chainId}` !== profile.network ||
89
+ String(result.commitmentContext?.seller).toLowerCase() !== profile.seller ||
90
+ BigInt(order.maxUsageUnits) > BigInt(context.requestedMaxUsageUnits)
91
+ )
92
+ throw coded(
93
+ "SETTLEMENT_SERVICE_MISMATCH",
94
+ "Prepared Order does not match the registered service reference.",
95
+ );
96
+ }
97
+ export function providerRequest(context, orderId, requestHash, request) {
98
+ return {
99
+ version: AGENTPAY_SERVICE_INVOCATION_VERSION,
100
+ orderId,
101
+ requestHash,
102
+ request,
103
+ };
104
+ }
105
+ export function providerOutput(payload, context, input, orderId, requestHash) {
106
+ let result;
107
+ try {
108
+ result = normalizeServiceResult(payload?.result);
109
+ if (
110
+ result.orderId !== orderId ||
111
+ result.requestHash !== requestHash ||
112
+ !/^(0|[1-9][0-9]{0,76})$/.test(result.usageUnits || "") ||
113
+ BigInt(result.usageUnits) > BigInt(context.requestedMaxUsageUnits)
114
+ )
115
+ throw Error();
116
+ } catch {
117
+ throw coded(
118
+ "PROVIDER_RESPONSE_INVALID",
119
+ "Provider output, hash, usage or request identity does not match this invocation.",
120
+ );
121
+ }
122
+ return result;
123
+ }
124
+ function coded(code, message) {
125
+ return Object.assign(Error(message), { code });
126
+ }
127
+
128
+ export function normalizeRuntimeResult(
129
+ value,
130
+ context,
131
+ requestHash,
132
+ orderId = null,
133
+ ) {
134
+ try {
135
+ if (
136
+ value?.version !== "agentpay.runtime-result.v1" ||
137
+ value.service?.skillId !== context.skillId ||
138
+ value.service?.profileHash !== context.profileHash ||
139
+ value.requestHash !== requestHash ||
140
+ !/^0x[0-9a-f]{64}$/.test(value.orderId || "") ||
141
+ (orderId && value.orderId !== orderId) ||
142
+ value.deliveryEvidenceVerified !== true
143
+ )
144
+ throw Error();
145
+ const delivery = normalizeServiceResult({
146
+ version: "agentpay.service-result.v1",
147
+ orderId: value.orderId,
148
+ requestHash,
149
+ output: value.output,
150
+ outputHash: value.outputHash,
151
+ usageUnits: value.usageUnits,
152
+ });
153
+ if (BigInt(delivery.usageUnits) > BigInt(context.requestedMaxUsageUnits))
154
+ throw Error();
155
+ return {
156
+ version: "agentpay.runtime-result.v1",
157
+ orderId: value.orderId,
158
+ requestHash,
159
+ service: { skillId: context.skillId, profileHash: context.profileHash },
160
+ output: delivery.output,
161
+ outputHash: delivery.outputHash,
162
+ usageUnits: delivery.usageUnits,
163
+ deliveryEvidenceVerified: true,
164
+ confirmationRequired: false,
165
+ finalization: value.finalization,
166
+ deadlines: value.deadlines,
167
+ };
168
+ } catch {
169
+ throw coded(
170
+ "RUNTIME_RESPONSE_INVALID",
171
+ "Runtime result does not match the bound service, input or usage cap.",
172
+ );
173
+ }
174
+ }
175
+
176
+ export function normalizeAcceptedTerms(value) {
177
+ if (
178
+ !value ||
179
+ Object.keys(value).sort().join(",") !==
180
+ "baseAmountAtomic,serviceWindowSeconds,unitPriceAtomic" ||
181
+ typeof value.baseAmountAtomic !== "string" ||
182
+ typeof value.unitPriceAtomic !== "string" ||
183
+ !/^(0|[1-9][0-9]*)$/.test(value.baseAmountAtomic) ||
184
+ !/^(0|[1-9][0-9]*)$/.test(value.unitPriceAtomic) ||
185
+ !Number.isSafeInteger(value.serviceWindowSeconds) ||
186
+ value.serviceWindowSeconds < 1 ||
187
+ value.serviceWindowSeconds > 31536000
188
+ )
189
+ throw coded(
190
+ "QUOTE_TERMS_INVALID",
191
+ "Review the price and maximum delivery duration.",
192
+ );
193
+ return {
194
+ baseAmountAtomic: value.baseAmountAtomic,
195
+ unitPriceAtomic: value.unitPriceAtomic,
196
+ serviceWindowSeconds: value.serviceWindowSeconds,
197
+ };
198
+ }
199
+
200
+ // Check again before signing, including when an older Wallet transport omitted
201
+ // the accepted conditions. A mismatch keeps the original Order, never reaccepts it.
202
+ export function assertPreparedCommercialTerms(result, accepted) {
203
+ if (!accepted) return;
204
+ const fail = () => {
205
+ throw coded(
206
+ "ORDER_TERMS_MISMATCH",
207
+ "Prepared Order differs from the accepted price or delivery time. Preserve this Order; no payment was authorized.",
208
+ );
209
+ };
210
+ const order = result?.order,
211
+ due = result?.deadlines?.deliveryDueAtMs;
212
+ if (
213
+ !order ||
214
+ order.basePriceAtomic !== accepted.baseAmountAtomic ||
215
+ order.unitPriceAtomic !== accepted.unitPriceAtomic ||
216
+ !Number.isSafeInteger(order.createdAtMs) ||
217
+ due - order.createdAtMs !== accepted.serviceWindowSeconds * 1000
218
+ )
219
+ fail();
220
+ try {
221
+ const amount =
222
+ BigInt(accepted.baseAmountAtomic) +
223
+ BigInt(accepted.unitPriceAtomic) * BigInt(order.maxUsageUnits);
224
+ if (
225
+ amount !== BigInt(order.prepaidAmountAtomic) ||
226
+ (result.paymentRequired &&
227
+ BigInt(result.paymentRequired.amount) !== amount)
228
+ )
229
+ fail();
230
+ } catch {
231
+ fail();
232
+ }
233
+ }
@@ -0,0 +1,52 @@
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
+ // A deployment base may include an ingress prefix. Local control stays separate.
5
+ export function serviceBase(value, label = "Service URL") {
6
+ if (
7
+ typeof value !== "string" ||
8
+ value !== value.trim() ||
9
+ value.includes("\\")
10
+ )
11
+ throw Error(`Invalid ${label}`);
12
+ const rawPath = /^[a-z]+:\/\/[^/?#]+([^?#]*)/.exec(value)?.[1];
13
+ if (
14
+ rawPath === undefined ||
15
+ !/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]*$/.test(rawPath || "/")
16
+ )
17
+ throw Error(`Invalid ${label} path`);
18
+ const url = new URL(value);
19
+ if (
20
+ url.username ||
21
+ url.password ||
22
+ url.search ||
23
+ url.hash ||
24
+ !(
25
+ url.protocol === "https:" ||
26
+ (url.protocol === "http:" &&
27
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
28
+ )
29
+ )
30
+ throw Error(`${label} must use HTTPS or loopback HTTP`);
31
+ return url.origin + url.pathname.replace(/\/+$/, "");
32
+ }
33
+ export function serviceUrl(base, path) {
34
+ if (
35
+ typeof path !== "string" ||
36
+ !path.startsWith("/") ||
37
+ path.startsWith("//") ||
38
+ path.includes("#") ||
39
+ path.includes("\\")
40
+ )
41
+ throw Error("Invalid service endpoint");
42
+ const normalized = serviceBase(String(base));
43
+ const url = new URL(path.slice(1), normalized + "/");
44
+ if (
45
+ url.origin !== new URL(normalized).origin ||
46
+ !url.pathname.startsWith(
47
+ new URL(normalized).pathname.replace(/\/$/, "") + "/",
48
+ )
49
+ )
50
+ throw Error("Endpoint escapes service base");
51
+ return url;
52
+ }
@@ -0,0 +1,69 @@
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
+ normalizeParticipantOrder,
6
+ normalizeFinalizationDisclosure,
7
+ normalizeOrderDeadlines,
8
+ normalizeServiceAuthorization,
9
+ AGENTPAY_SERVICE_AUTHORIZATION_V2,
10
+ normalizeDeliveryResult,
11
+ validateOrderWaiting,
12
+ validateOrderClosure,
13
+ AGENTPAY_DELIVERY_RESULT_V2,
14
+ } from "@c4roa0/agentpay-protocol";
15
+
16
+ // Keep integer timestamps inside Runtime; v1 is an explicit ingress adapter.
17
+ export function readSettlementResult(value) {
18
+ const fail = () => {
19
+ throw Object.assign(
20
+ new Error("Settlement response has invalid fields or timestamps"),
21
+ { code: "SETTLEMENT_RESPONSE_INVALID" },
22
+ );
23
+ };
24
+ if (
25
+ value?.ok !== true ||
26
+ !value.result ||
27
+ typeof value.result !== "object" ||
28
+ Array.isArray(value.result)
29
+ )
30
+ fail();
31
+ try {
32
+ let result = { ...value.result };
33
+ if (
34
+ result.version === AGENTPAY_SERVICE_AUTHORIZATION_V2 ||
35
+ (Object.hasOwn(result, "authorizationId") &&
36
+ (Object.hasOwn(result, "expiresAt") ||
37
+ Object.hasOwn(result, "expiresAtMs") ||
38
+ Object.hasOwn(result, "state") ||
39
+ Object.hasOwn(result, "authorization")))
40
+ )
41
+ result = normalizeServiceAuthorization(result);
42
+ if (
43
+ result.version === AGENTPAY_DELIVERY_RESULT_V2 ||
44
+ Object.hasOwn(result, "confirmedAt") ||
45
+ Object.hasOwn(result, "confirmedAtMs") ||
46
+ (Object.hasOwn(result, "receiptId") && Object.hasOwn(result, "billing"))
47
+ )
48
+ result = normalizeDeliveryResult(result);
49
+ if (Object.hasOwn(result, "order")) {
50
+ result.order = normalizeParticipantOrder(result.order);
51
+ if (result.order.role !== "buyer") fail();
52
+ }
53
+ if (result.waiting != null)
54
+ result.waiting = validateOrderWaiting(result.waiting);
55
+ if (result.closure != null) {
56
+ result.closure = validateOrderClosure(result.closure);
57
+ if (result.paymentRequired != null) fail();
58
+ }
59
+ if (result.deadlines != null)
60
+ result.deadlines = normalizeOrderDeadlines(result.deadlines);
61
+ if (result.finalization != null)
62
+ result.finalization = normalizeFinalizationDisclosure(
63
+ result.finalization,
64
+ );
65
+ return result;
66
+ } catch {
67
+ fail();
68
+ }
69
+ }