@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.
- package/AGENTS.md +137 -0
- package/CONTRIBUTING.md +16 -0
- package/LICENSE +373 -0
- package/NOTICE.md +20 -0
- package/README.md +85 -2
- package/SECURITY.md +16 -0
- package/doc/README.md +52 -0
- package/doc/TODO.md +107 -0
- package/doc/architecture.md +99 -0
- package/doc/design.md +60 -0
- package/doc/develope.md +68 -0
- package/doc/plans/user-wallet-distribution.md +91 -0
- package/doc/reference/harness-skill-runtime.md +120 -0
- package/doc/reference/local-cache-foundation.md +176 -0
- package/doc/reference/local-safe-owner-ui.md +25 -0
- package/doc/reference/local-update.md +18 -0
- package/doc/reference/multi-service-runtime.md +119 -0
- package/doc/reference/npm-integration-verification.md +103 -0
- package/doc/reference/onboarding-writing.md +48 -0
- package/doc/reference/original-order-recovery.md +97 -0
- package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
- package/doc/reference/public-deployment-settings.md +56 -0
- package/doc/reference/random-skill-contract.md +18 -0
- package/doc/reference/result-file-cache.md +166 -0
- package/doc/reference/runtime-address-recovery.md +42 -0
- package/doc/reference/runtime-result-cache.md +157 -0
- package/doc/reference/runtime-startup-recovery.md +141 -0
- package/doc/reference/skill-input-output-cache.md +93 -0
- package/doc/reference/wallet-identity-policy-history.md +23 -0
- package/doc/runbooks/commands.md +103 -0
- package/doc/runbooks/deployment-authorization-update.md +42 -0
- package/doc/runbooks/emergency-payment-stop.md +80 -0
- package/doc/runbooks/local-autostart.md +212 -0
- package/doc/runbooks/local-cache.md +115 -0
- package/doc/runbooks/local-update.md +84 -0
- package/doc/runbooks/market-connection.md +73 -0
- package/doc/runbooks/npm-publication.md +56 -0
- package/doc/runbooks/official-test-package.md +62 -0
- package/doc/runbooks/order-cancellation.md +56 -0
- package/doc/runbooks/order-support.md +41 -0
- package/doc/runbooks/policy-renewal.md +80 -0
- package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
- package/doc/runbooks/runtime-skill-client.md +155 -0
- package/doc/runbooks/safe-owner-web.md +104 -0
- package/doc/runbooks/safe-policy-migration.md +36 -0
- package/doc/runbooks/safe-policy-setup.md +52 -0
- package/doc/runbooks/terminal-authorization-preview.md +38 -0
- package/doc/runbooks/unlock-wallet.md +68 -0
- package/doc/runbooks/user-onboarding.md +87 -0
- package/doc/runbooks/wallet-identity.md +59 -0
- package/doc/security/public-source-review-2026-11-06.md +71 -0
- package/doc/usage.md +54 -0
- package/package.json +49 -4
- package/progress.md +210 -0
- package/run +29 -0
- package/scripts/agentpay-cli.mjs +11 -0
- package/scripts/authorize-existing-safe.mjs +211 -0
- package/scripts/autostart.mjs +186 -0
- package/scripts/cancel-order.mjs +76 -0
- package/scripts/check-installation.mjs +26 -0
- package/scripts/check-official-test-package.mjs +35 -0
- package/scripts/check-purchase-readiness.mjs +275 -0
- package/scripts/configure-runtime-client.mjs +16 -0
- package/scripts/guide-policy.mjs +64 -0
- package/scripts/initialize-caller-credential.mjs +11 -0
- package/scripts/install-skill-bundle.mjs +176 -0
- package/scripts/install-user.mjs +24 -0
- package/scripts/local-service.mjs +60 -0
- package/scripts/lock.mjs +11 -0
- package/scripts/login-notify.mjs +39 -0
- package/scripts/migrate-safe-policy.mjs +165 -0
- package/scripts/official-paid-test.mjs +9 -0
- package/scripts/poll-unconfirmed-notice.mjs +75 -0
- package/scripts/prepare-onboarding.mjs +33 -0
- package/scripts/preview-policy.mjs +65 -0
- package/scripts/random-model-ack-smoke.mjs +235 -0
- package/scripts/random-reject-smoke.mjs +147 -0
- package/scripts/renew.mjs +9 -0
- package/scripts/repair.mjs +10 -0
- package/scripts/revoke.mjs +9 -0
- package/scripts/select-skill-bundle.mjs +39 -0
- package/scripts/setup-local.mjs +199 -0
- package/scripts/status.mjs +13 -0
- package/scripts/test-package.mjs +35 -0
- package/scripts/unlock.mjs +13 -0
- package/scripts/update.mjs +7 -0
- package/scripts/user-setup.mjs +219 -0
- package/scripts/verify-npm-release.mjs +32 -0
- package/src/active-skill.mjs +160 -0
- package/src/anvil-onboarding-state.mjs +96 -0
- package/src/anvil-onboarding.mjs +1036 -0
- package/src/authorization-question-checkpoint.mjs +114 -0
- package/src/autostart-files.mjs +50 -0
- package/src/cache-management-cli.mjs +302 -0
- package/src/cache-management.mjs +47 -0
- package/src/caller-auth.mjs +58 -0
- package/src/client-request-lock.mjs +44 -0
- package/src/connect-market.mjs +408 -0
- package/src/demo-unlock-secret.mjs +64 -0
- package/src/deployment-status.mjs +198 -0
- package/src/file-cache-cli.mjs +83 -0
- package/src/file-cache-owner.mjs +74 -0
- package/src/file-cache-settings.mjs +50 -0
- package/src/file-cache.mjs +790 -0
- package/src/file-download.mjs +281 -0
- package/src/finalization-disclosure.mjs +16 -0
- package/src/guided-setup.mjs +194 -0
- package/src/index.mjs +1233 -0
- package/src/install-and-setup.mjs +38 -0
- package/src/install-preflight.mjs +201 -0
- package/src/install-user.mjs +279 -0
- package/src/installation-paths.mjs +31 -0
- package/src/invocation-result.mjs +148 -0
- package/src/invocation-store.mjs +385 -0
- package/src/local-data-directory.mjs +117 -0
- package/src/local-info.mjs +181 -0
- package/src/local-services.mjs +156 -0
- package/src/local-time.mjs +36 -0
- package/src/local-update.mjs +316 -0
- package/src/local-wallet-ipc.mjs +93 -0
- package/src/login-reminder.mjs +28 -0
- package/src/market-download-url.mjs +21 -0
- package/src/node-version.mjs +12 -0
- package/src/npm-update.mjs +126 -0
- package/src/official-paid-test.mjs +407 -0
- package/src/official-test-package.mjs +41 -0
- package/src/official-test-update.mjs +162 -0
- package/src/order-cancellation.mjs +241 -0
- package/src/order-closure.mjs +42 -0
- package/src/order-list-panel.mjs +110 -0
- package/src/order-review.mjs +424 -0
- package/src/order-status-panel.mjs +65 -0
- package/src/order-support.mjs +365 -0
- package/src/owner-approval-server.mjs +332 -0
- package/src/owner-signer.mjs +67 -0
- package/src/policy-info.mjs +38 -0
- package/src/policy-migration.mjs +84 -0
- package/src/policy-operations.mjs +67 -0
- package/src/policy-preview-server.mjs +236 -0
- package/src/policy-terminal-guide.mjs +377 -0
- package/src/prepare-local-services.mjs +143 -0
- package/src/prepare-onboarding.mjs +210 -0
- package/src/private-journal-files.mjs +29 -0
- package/src/protocol-package.mjs +25 -0
- package/src/readiness-profile.mjs +30 -0
- package/src/renew-policy.mjs +605 -0
- package/src/repair-market-wallet.mjs +137 -0
- package/src/restore-wallet.mjs +255 -0
- package/src/result-cache-view.mjs +276 -0
- package/src/runtime-client.mjs +1145 -0
- package/src/runtime-connection-settings.mjs +101 -0
- package/src/runtime-recovery.mjs +667 -0
- package/src/runtime-status.mjs +32 -0
- package/src/safe-owner-anvil.mjs +115 -0
- package/src/safe-policy-plan.mjs +195 -0
- package/src/service-reference.mjs +233 -0
- package/src/service-url.mjs +52 -0
- package/src/settlement-response.mjs +69 -0
- package/src/setup-autostart.mjs +197 -0
- package/src/setup-demo.mjs +152 -0
- package/src/skill-bundle.mjs +142 -0
- package/src/skill-client.mjs +198 -0
- package/src/skill-install-prompt.mjs +31 -0
- package/src/skill-package.mjs +50 -0
- package/src/skill-zip.mjs +220 -0
- package/src/software-versions.mjs +48 -0
- package/src/status-panel.mjs +603 -0
- package/src/transport.mjs +48 -0
- package/src/uninstall-local.mjs +299 -0
- package/src/user-cli.mjs +354 -0
- package/src/user-setup-guide.mjs +193 -0
- package/src/wallet-control.mjs +182 -0
- package/src/wallet-ipc-error.mjs +16 -0
- package/src/wallet-package.mjs +28 -0
- package/update-pair.json +6 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// Deliberate account connection using a narrow Wallet proof. Website display grouping
|
|
2
|
+
// does not merge Policy authority; a saved receipt is not a live access check.
|
|
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 { createInterface } from "node:readline/promises";
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { open, rename, unlink } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { resolveUserDataDir } from "./local-data-directory.mjs";
|
|
12
|
+
import { checkServiceDirectory } from "./local-services.mjs";
|
|
13
|
+
import { localWalletAdmin } from "./local-wallet-ipc.mjs";
|
|
14
|
+
import { repairMarketWallet } from "./repair-market-wallet.mjs";
|
|
15
|
+
import { controlWallet } from "./wallet-control.mjs";
|
|
16
|
+
import { fetchJsonWithTimeout } from "./transport.mjs";
|
|
17
|
+
export function marketOrigin(value) {
|
|
18
|
+
const u = new URL(value);
|
|
19
|
+
if (
|
|
20
|
+
u.href !== u.origin + "/" ||
|
|
21
|
+
u.username ||
|
|
22
|
+
u.password ||
|
|
23
|
+
!(
|
|
24
|
+
u.protocol === "https:" ||
|
|
25
|
+
(u.protocol === "http:" &&
|
|
26
|
+
["127.0.0.1", "localhost", "[::1]"].includes(u.hostname))
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
throw Error(
|
|
30
|
+
"Use your Market HTTPS address, or a local rehearsal address such as http://127.0.0.1:8080.",
|
|
31
|
+
);
|
|
32
|
+
return u.origin;
|
|
33
|
+
}
|
|
34
|
+
async function askTerminal(message, { hidden = false } = {}) {
|
|
35
|
+
if (!process.stdin.isTTY)
|
|
36
|
+
throw Error("Run this command in an interactive terminal.");
|
|
37
|
+
if (!hidden) {
|
|
38
|
+
const rl = createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stderr,
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
return await rl.question(message + "\n> ");
|
|
44
|
+
} finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
process.stderr.write(message + " (input hidden): ");
|
|
49
|
+
const previous = process.stdin.isRaw;
|
|
50
|
+
process.stdin.setRawMode(true);
|
|
51
|
+
process.stdin.resume();
|
|
52
|
+
process.stdin.setEncoding("utf8");
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let value = "";
|
|
55
|
+
const finish = (e) => {
|
|
56
|
+
process.stdin.off("data", read);
|
|
57
|
+
process.stdin.setRawMode(!!previous);
|
|
58
|
+
process.stdin.pause();
|
|
59
|
+
process.stderr.write("\n");
|
|
60
|
+
const result = value;
|
|
61
|
+
value = "";
|
|
62
|
+
e ? reject(e) : resolve(result);
|
|
63
|
+
};
|
|
64
|
+
const read = (chunk) => {
|
|
65
|
+
for (const c of chunk) {
|
|
66
|
+
if (c === "\u0003" || c === "\u0004")
|
|
67
|
+
return finish(
|
|
68
|
+
Object.assign(Error("Connection cancelled."), {
|
|
69
|
+
code: "CANCELLED",
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
if (c === "\r" || c === "\n") return finish();
|
|
73
|
+
if (c === "\u007f" || c === "\b") value = value.slice(0, -1);
|
|
74
|
+
else if (c >= " ") value += c;
|
|
75
|
+
if (value.length > 4096) return finish(Error("Input is too long."));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
process.stdin.on("data", read);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function previousConnection(dataDir) {
|
|
82
|
+
let h;
|
|
83
|
+
try {
|
|
84
|
+
h = await open(
|
|
85
|
+
join(dataDir, "market-connection.json"),
|
|
86
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
87
|
+
);
|
|
88
|
+
const s = await h.stat();
|
|
89
|
+
if (
|
|
90
|
+
!s.isFile() ||
|
|
91
|
+
s.uid !== process.getuid() ||
|
|
92
|
+
s.mode & 0o077 ||
|
|
93
|
+
s.size > 16384
|
|
94
|
+
)
|
|
95
|
+
throw Error("Unsafe Market connection record.");
|
|
96
|
+
return JSON.parse(await h.readFile("utf8"));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e.code === "ENOENT") return null;
|
|
99
|
+
throw e;
|
|
100
|
+
} finally {
|
|
101
|
+
await h?.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function saveConnection(dataDir, value) {
|
|
105
|
+
await checkServiceDirectory(dataDir);
|
|
106
|
+
const path = join(dataDir, "market-connection-" + randomUUID() + ".tmp");
|
|
107
|
+
let h;
|
|
108
|
+
try {
|
|
109
|
+
h = await open(path, "wx", 0o600);
|
|
110
|
+
await h.writeFile(JSON.stringify(value) + "\n");
|
|
111
|
+
await h.sync();
|
|
112
|
+
await h.close();
|
|
113
|
+
h = null;
|
|
114
|
+
await rename(path, join(dataDir, "market-connection.json"));
|
|
115
|
+
} finally {
|
|
116
|
+
await h?.close();
|
|
117
|
+
await unlink(path).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export async function connectMarket(
|
|
121
|
+
args,
|
|
122
|
+
{
|
|
123
|
+
dataDir: chosen,
|
|
124
|
+
ask = askTerminal,
|
|
125
|
+
write = console.log,
|
|
126
|
+
wallet = localWalletAdmin,
|
|
127
|
+
fetchJson = fetchJsonWithTimeout,
|
|
128
|
+
load = previousConnection,
|
|
129
|
+
save = saveConnection,
|
|
130
|
+
repair = repairMarketWallet,
|
|
131
|
+
unlock = controlWallet,
|
|
132
|
+
} = {},
|
|
133
|
+
) {
|
|
134
|
+
let token, origin, completion;
|
|
135
|
+
try {
|
|
136
|
+
if (args.length && !(args.length === 2 && args[0] === "--market")) {
|
|
137
|
+
write("Usage: agentpay market connect [--market URL]");
|
|
138
|
+
return 2;
|
|
139
|
+
}
|
|
140
|
+
const dataDir = chosen || (await resolveUserDataDir());
|
|
141
|
+
let scope;
|
|
142
|
+
try {
|
|
143
|
+
const local = await wallet(dataDir, "status");
|
|
144
|
+
if (local.bound === false)
|
|
145
|
+
throw Object.assign(Error("Wallet setup is incomplete."), {
|
|
146
|
+
code: "POLICY_UNBOUND",
|
|
147
|
+
});
|
|
148
|
+
scope = await wallet(dataDir, "marketConnectionScope");
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (!["METHOD_NOT_ALLOWED", "WALLET_NOT_RUNNING"].includes(e.code))
|
|
151
|
+
throw e;
|
|
152
|
+
write(
|
|
153
|
+
e.code === "METHOD_NOT_ALLOWED"
|
|
154
|
+
? "Wallet needs a service refresh before it can connect."
|
|
155
|
+
: "Wallet is not running. Let’s prepare it for connection.",
|
|
156
|
+
);
|
|
157
|
+
write(
|
|
158
|
+
"This will check installed packages and restart your local services, then continue here.",
|
|
159
|
+
);
|
|
160
|
+
if (
|
|
161
|
+
(await ask("Type 'confirm' to continue, or 'q' to return")).trim() !==
|
|
162
|
+
"confirm"
|
|
163
|
+
) {
|
|
164
|
+
write("Connection cancelled.");
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (!(await repair({ dataDir, write }))) return 1;
|
|
168
|
+
scope = await wallet(dataDir, "marketConnectionScope");
|
|
169
|
+
}
|
|
170
|
+
if ((await wallet(dataDir, "status")).locked) {
|
|
171
|
+
if (
|
|
172
|
+
(await unlock("unlock", {
|
|
173
|
+
args: [dataDir],
|
|
174
|
+
write: (m) => {
|
|
175
|
+
if (!m.startsWith("Wallet unlocked.")) write(m);
|
|
176
|
+
},
|
|
177
|
+
})) !== 0
|
|
178
|
+
)
|
|
179
|
+
return 1;
|
|
180
|
+
if ((await wallet(dataDir, "status")).locked)
|
|
181
|
+
throw Object.assign(Error("Wallet is locked."), { code: "LOCKED" });
|
|
182
|
+
}
|
|
183
|
+
const previous = await load(dataDir);
|
|
184
|
+
const answer =
|
|
185
|
+
args[1] ||
|
|
186
|
+
(await ask(
|
|
187
|
+
"Market address" +
|
|
188
|
+
(previous?.marketOrigin
|
|
189
|
+
? ` (Enter keeps ${previous.marketOrigin})`
|
|
190
|
+
: ""),
|
|
191
|
+
));
|
|
192
|
+
if (answer.trim() === "q") {
|
|
193
|
+
write("Connection cancelled.");
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
origin = marketOrigin(answer.trim() || previous?.marketOrigin);
|
|
197
|
+
const call = async (path, body, auth = true) => {
|
|
198
|
+
const { response, result } = await fetchJson(
|
|
199
|
+
origin + "/market" + path,
|
|
200
|
+
{
|
|
201
|
+
method: body === undefined ? "GET" : "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"content-type": "application/json",
|
|
204
|
+
...(auth ? { authorization: "Bearer " + token } : {}),
|
|
205
|
+
},
|
|
206
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
207
|
+
},
|
|
208
|
+
30000,
|
|
209
|
+
);
|
|
210
|
+
if (
|
|
211
|
+
!response.ok &&
|
|
212
|
+
result?.error?.code === "ORDER_CONNECTION_LABEL_TAKEN"
|
|
213
|
+
)
|
|
214
|
+
throw Object.assign(
|
|
215
|
+
Error(
|
|
216
|
+
"That nickname is already used by another Wallet. Choose a different name.",
|
|
217
|
+
),
|
|
218
|
+
{ code: "ORDER_CONNECTION_LABEL_TAKEN" },
|
|
219
|
+
);
|
|
220
|
+
if (!response.ok || result?.ok !== true)
|
|
221
|
+
throw Error(
|
|
222
|
+
response.status === 401
|
|
223
|
+
? "Market sign-in was rejected. Check your account and password."
|
|
224
|
+
: "Market could not complete the connection. Run the same command to retry.",
|
|
225
|
+
);
|
|
226
|
+
return result.result;
|
|
227
|
+
};
|
|
228
|
+
write(
|
|
229
|
+
"\n------------------------------------------------------------\n1/3 Sign in to Market\n------------------------------------------------------------",
|
|
230
|
+
);
|
|
231
|
+
write("Market: " + origin);
|
|
232
|
+
const email = (await ask("Market account email")).trim();
|
|
233
|
+
if (email === "q") {
|
|
234
|
+
write("Connection cancelled.");
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
let password = await ask("Market password", { hidden: true }),
|
|
238
|
+
signedIn;
|
|
239
|
+
try {
|
|
240
|
+
signedIn = await call("/developers/login", { email, password }, false);
|
|
241
|
+
} finally {
|
|
242
|
+
password = null;
|
|
243
|
+
}
|
|
244
|
+
token = signedIn.sessionToken;
|
|
245
|
+
if (typeof token !== "string" || !token || !signedIn.developer?.id)
|
|
246
|
+
throw Error("Market returned an invalid sign-in response.");
|
|
247
|
+
write(
|
|
248
|
+
"\n------------------------------------------------------------\n2/3 Name your Wallet\n------------------------------------------------------------",
|
|
249
|
+
);
|
|
250
|
+
const listed = await call("/account/wallet/connections");
|
|
251
|
+
if (!Array.isArray(listed?.connections))
|
|
252
|
+
throw Error("Could not check your connected Wallets. Try again.");
|
|
253
|
+
const existing = listed.connections.find((w) =>
|
|
254
|
+
["network", "module", "safe", "delegate"].every(
|
|
255
|
+
(key) =>
|
|
256
|
+
typeof scope[key] === "string" &&
|
|
257
|
+
String(w[key]).toLowerCase() === scope[key].toLowerCase(),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
if (existing) write(`Using your Wallet name: ${existing.label}`);
|
|
261
|
+
else
|
|
262
|
+
write(
|
|
263
|
+
"Choose a name to recognize this Wallet in Market, such as Home laptop.",
|
|
264
|
+
);
|
|
265
|
+
const defaultLabel =
|
|
266
|
+
previous?.accountId === signedIn.developer.id
|
|
267
|
+
? previous?.connection?.label || "My Wallet"
|
|
268
|
+
: "My Wallet";
|
|
269
|
+
while (true) {
|
|
270
|
+
const label =
|
|
271
|
+
existing?.label ||
|
|
272
|
+
(
|
|
273
|
+
await ask(
|
|
274
|
+
`Wallet nickname (Enter to use '${defaultLabel}', 'q' to cancel)`,
|
|
275
|
+
)
|
|
276
|
+
).trim() ||
|
|
277
|
+
defaultLabel;
|
|
278
|
+
if (label === "q") {
|
|
279
|
+
write("Connection cancelled.");
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
if (label.length > 64 || /[\u0000-\u001f\u007f]/.test(label))
|
|
283
|
+
throw Error("Use a nickname of 1–64 characters.");
|
|
284
|
+
try {
|
|
285
|
+
// Check/reserve the nickname before consent; no Wallet proof or verified
|
|
286
|
+
// connection is created until the participant confirms. Verify rechecks races.
|
|
287
|
+
const challenge = await call("/account/wallet/connections/challenge", {
|
|
288
|
+
scope,
|
|
289
|
+
label,
|
|
290
|
+
marketOrigin: origin,
|
|
291
|
+
});
|
|
292
|
+
for (const key of ["network", "module", "safe", "policyId"])
|
|
293
|
+
if (challenge.scope?.[key] !== scope[key])
|
|
294
|
+
throw Error(
|
|
295
|
+
"Market requested a different Wallet authorization. Connection stopped.",
|
|
296
|
+
);
|
|
297
|
+
const payload = challenge.payload;
|
|
298
|
+
if (
|
|
299
|
+
payload?.accountId !== signedIn.developer.id ||
|
|
300
|
+
payload.marketOrigin !== origin ||
|
|
301
|
+
payload.label !== label ||
|
|
302
|
+
!Number.isSafeInteger(payload.expiresAtMs) ||
|
|
303
|
+
payload.expiresAtMs <= Date.now()
|
|
304
|
+
)
|
|
305
|
+
throw Error("Market returned a mismatched connection challenge.");
|
|
306
|
+
write(`\nMarket account: ${email}\nWallet name: ${label}`);
|
|
307
|
+
write(
|
|
308
|
+
"View this Wallet’s Orders and request help on Market. No spending permission is added.",
|
|
309
|
+
);
|
|
310
|
+
while (true) {
|
|
311
|
+
const choice = (
|
|
312
|
+
await ask(
|
|
313
|
+
"Type 'confirm' to connect, 'details' for Wallet identity, or 'q' to cancel",
|
|
314
|
+
)
|
|
315
|
+
).trim();
|
|
316
|
+
if (choice === "q") {
|
|
317
|
+
write("Connection cancelled.");
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
if (choice === "details") {
|
|
321
|
+
write(
|
|
322
|
+
`Safe: ${scope.safe}\nNetwork: ${scope.network}\nPolicy ID: ${scope.policyId}`,
|
|
323
|
+
);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (choice === "confirm") break;
|
|
327
|
+
write("Enter 'confirm', 'details' or 'q'.");
|
|
328
|
+
}
|
|
329
|
+
write("\n3/3 Verify connection…");
|
|
330
|
+
const proof = await wallet(dataDir, "signMarketConnection", payload);
|
|
331
|
+
const result = await call("/account/wallet/connections/verify", {
|
|
332
|
+
challengeId: payload.challengeId,
|
|
333
|
+
authorization: proof.authorization,
|
|
334
|
+
});
|
|
335
|
+
for (const key of ["network", "module", "safe", "policyId"])
|
|
336
|
+
if (result.connection?.[key] !== scope[key])
|
|
337
|
+
throw Error(
|
|
338
|
+
"Connection response does not match this Wallet. Check Market Settings.",
|
|
339
|
+
);
|
|
340
|
+
if (
|
|
341
|
+
scope.delegate &&
|
|
342
|
+
result.connection?.delegate?.toLowerCase() !==
|
|
343
|
+
scope.delegate.toLowerCase()
|
|
344
|
+
)
|
|
345
|
+
throw Error("Connection execution address differs from this Wallet.");
|
|
346
|
+
if (result.connection.label !== label)
|
|
347
|
+
throw Error("Connection nickname differs. Check Market Settings.");
|
|
348
|
+
try {
|
|
349
|
+
await save(dataDir, {
|
|
350
|
+
version: 1,
|
|
351
|
+
marketOrigin: origin,
|
|
352
|
+
accountId: signedIn.developer.id,
|
|
353
|
+
connection: result.connection,
|
|
354
|
+
});
|
|
355
|
+
} catch {
|
|
356
|
+
write(
|
|
357
|
+
"Market connected this Wallet, but the local shortcut could not be saved. Use --market URL next time.",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
completion = `\nWallet connected: ${label}\nView this Wallet’s Orders in Market → Account → My Orders.\nManage or disconnect it in Market Settings. This guide has finished.`;
|
|
361
|
+
return 0;
|
|
362
|
+
} catch (e) {
|
|
363
|
+
if (e.code !== "ORDER_CONNECTION_LABEL_TAKEN" || existing) throw e;
|
|
364
|
+
write(e.message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
write(
|
|
369
|
+
["LOCKED", "WALLET_LOCKED"].includes(e.code)
|
|
370
|
+
? "Wallet is locked. Run agentpay wallet unlock, then repeat this command."
|
|
371
|
+
: e.code === "POLICY_UNBOUND"
|
|
372
|
+
? "Finish Wallet setup with agentpay setup, then connect again."
|
|
373
|
+
: e.code === "POLICY_MIGRATION_PENDING"
|
|
374
|
+
? "Complete the pending Policy migration before connecting this Wallet."
|
|
375
|
+
: e.code === "METHOD_NOT_ALLOWED"
|
|
376
|
+
? "Wallet still cannot connect. Run agentpay market connect again to prepare it."
|
|
377
|
+
: String(e.message || "Could not connect Wallet."),
|
|
378
|
+
);
|
|
379
|
+
return 1;
|
|
380
|
+
} finally {
|
|
381
|
+
if (token && origin) {
|
|
382
|
+
try {
|
|
383
|
+
const { response } = await fetchJson(
|
|
384
|
+
origin + "/market/developers/logout",
|
|
385
|
+
{
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: {
|
|
388
|
+
authorization: "Bearer " + token,
|
|
389
|
+
"content-type": "application/json",
|
|
390
|
+
},
|
|
391
|
+
body: "{}",
|
|
392
|
+
},
|
|
393
|
+
10000,
|
|
394
|
+
);
|
|
395
|
+
if (!response.ok)
|
|
396
|
+
write(
|
|
397
|
+
"Temporary sign-in cleanup could not be confirmed. Its token was not saved locally.",
|
|
398
|
+
);
|
|
399
|
+
} catch {
|
|
400
|
+
write(
|
|
401
|
+
"Temporary sign-in cleanup could not be confirmed. Its token was not saved locally.",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
token = null;
|
|
406
|
+
if (completion) write(completion);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
// UI exercise only. Never persists or returns the entered secret.
|
|
5
|
+
export async function confirmDemoUnlockSecret({ readHidden, write }) {
|
|
6
|
+
write(
|
|
7
|
+
"Demo: use a temporary practice secret, not a real password.\nInput is hidden and will not be saved.",
|
|
8
|
+
);
|
|
9
|
+
while (true) {
|
|
10
|
+
let first = await readHidden("Create demo unlock secret: ");
|
|
11
|
+
let second = await readHidden("Confirm demo unlock secret: ");
|
|
12
|
+
const matched = first.length > 0 && first === second;
|
|
13
|
+
first = null;
|
|
14
|
+
second = null;
|
|
15
|
+
if (matched) {
|
|
16
|
+
write("Demo unlock secret confirmed. No secret was saved.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
write(
|
|
20
|
+
"Secrets are empty or do not match. Try again, or press Ctrl+C to cancel.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function readHiddenTerminal(
|
|
25
|
+
prompt,
|
|
26
|
+
input = process.stdin,
|
|
27
|
+
output = process.stderr,
|
|
28
|
+
) {
|
|
29
|
+
if (!input.isTTY || typeof input.setRawMode !== "function")
|
|
30
|
+
throw Error("Hidden input requires an interactive terminal.");
|
|
31
|
+
output.write(prompt);
|
|
32
|
+
const previousRaw = input.isRaw;
|
|
33
|
+
input.setRawMode(true);
|
|
34
|
+
input.resume();
|
|
35
|
+
input.setEncoding("utf8");
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let secret = "";
|
|
38
|
+
const finish = (error) => {
|
|
39
|
+
input.off("data", onData);
|
|
40
|
+
input.setRawMode(!!previousRaw);
|
|
41
|
+
input.pause();
|
|
42
|
+
output.write("\n");
|
|
43
|
+
const value = secret;
|
|
44
|
+
secret = "";
|
|
45
|
+
if (error) reject(error);
|
|
46
|
+
else resolve(value);
|
|
47
|
+
};
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
for (const character of chunk) {
|
|
50
|
+
if (character === "\u0003" || character === "\u0004")
|
|
51
|
+
return finish(
|
|
52
|
+
Object.assign(Error("Demo cancelled; nothing was saved."), {
|
|
53
|
+
code: "CANCELLED",
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
if (character === "\r" || character === "\n") return finish();
|
|
57
|
+
if (character === "\u007f" || character === "\b")
|
|
58
|
+
secret = secret.slice(0, -1);
|
|
59
|
+
else if (character >= " ") secret += character;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
input.on("data", onData);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { deploymentFilePath, importSetupFiles } 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 { open, writeFile, rename, mkdir, unlink } from "node:fs/promises";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { installedUserDir } from "./local-data-directory.mjs";
|
|
10
|
+
import { validatePublicDeployment } from "./prepare-onboarding.mjs";
|
|
11
|
+
export async function readPrivateJson(path) {
|
|
12
|
+
let h;
|
|
13
|
+
try {
|
|
14
|
+
h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
15
|
+
const s = await h.stat();
|
|
16
|
+
if (
|
|
17
|
+
!s.isFile() ||
|
|
18
|
+
s.uid !== process.getuid() ||
|
|
19
|
+
s.mode & 0o077 ||
|
|
20
|
+
s.size > 65536
|
|
21
|
+
)
|
|
22
|
+
throw Error("Unsafe local settings");
|
|
23
|
+
return JSON.parse(await h.readFile("utf8"));
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e.code === "ENOENT") return null;
|
|
26
|
+
throw e;
|
|
27
|
+
} finally {
|
|
28
|
+
await h?.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function writePrivateJson(path, value) {
|
|
32
|
+
const temp = path + "." + randomUUID();
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(temp, JSON.stringify(value) + "\n", {
|
|
35
|
+
mode: 0o600,
|
|
36
|
+
flag: "wx",
|
|
37
|
+
});
|
|
38
|
+
await rename(temp, path);
|
|
39
|
+
} finally {
|
|
40
|
+
await unlink(temp).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function deploymentChanges(policy, config) {
|
|
44
|
+
return ["chainId", "module", "factory", "registry", "token"].filter(
|
|
45
|
+
(k) => String(policy[k]).toLowerCase() !== String(config[k]).toLowerCase(),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
export async function loadCurrentDeployment({
|
|
49
|
+
dataDir,
|
|
50
|
+
userDir = installedUserDir,
|
|
51
|
+
fetchImpl = fetch,
|
|
52
|
+
now = Date.now(),
|
|
53
|
+
fresh = false,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const local = await readPrivateJson(
|
|
56
|
+
await deploymentFilePath(dataDir, userDir, "user-deployment.json"),
|
|
57
|
+
);
|
|
58
|
+
const receipt = await readPrivateJson(
|
|
59
|
+
join(dataDir, "market-connection.json"),
|
|
60
|
+
);
|
|
61
|
+
if (!receipt?.marketOrigin)
|
|
62
|
+
return local
|
|
63
|
+
? {
|
|
64
|
+
config: validatePublicDeployment(local),
|
|
65
|
+
source: "downloaded",
|
|
66
|
+
checkedAtMs: null,
|
|
67
|
+
}
|
|
68
|
+
: null;
|
|
69
|
+
const origin = new URL(receipt.marketOrigin);
|
|
70
|
+
if (
|
|
71
|
+
origin.username ||
|
|
72
|
+
origin.password ||
|
|
73
|
+
origin.search ||
|
|
74
|
+
origin.hash ||
|
|
75
|
+
origin.pathname !== "/" ||
|
|
76
|
+
!(
|
|
77
|
+
origin.protocol === "https:" ||
|
|
78
|
+
(origin.protocol === "http:" &&
|
|
79
|
+
["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname))
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
throw Error("Cannot safely check the saved Market address.");
|
|
83
|
+
const path = join(dataDir, "runtime/deployment-status.json");
|
|
84
|
+
const cache = await readPrivateJson(path);
|
|
85
|
+
if (
|
|
86
|
+
!fresh &&
|
|
87
|
+
cache?.origin === origin.href &&
|
|
88
|
+
Number.isSafeInteger(cache.checkedAtMs) &&
|
|
89
|
+
now >= cache.checkedAtMs &&
|
|
90
|
+
now - cache.checkedAtMs < 60000
|
|
91
|
+
) {
|
|
92
|
+
if (cache.error)
|
|
93
|
+
throw Error("Platform deployment check temporarily unavailable.");
|
|
94
|
+
return { ...cache, config: validatePublicDeployment(cache.config) };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetchImpl(
|
|
98
|
+
new URL("/market/web/onboarding/user-config", origin),
|
|
99
|
+
{ signal: AbortSignal.timeout(3000), redirect: "error" },
|
|
100
|
+
);
|
|
101
|
+
if (!response.ok) throw Error("Platform configuration unavailable");
|
|
102
|
+
const reader = response.body.getReader();
|
|
103
|
+
let raw = "",
|
|
104
|
+
size = 0;
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
try {
|
|
107
|
+
for (;;) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
size += value.length;
|
|
111
|
+
if (size > 16384) throw Error("Platform settings too large");
|
|
112
|
+
raw += decoder.decode(value, { stream: true });
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await reader.cancel().catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
raw += decoder.decode();
|
|
118
|
+
const result = {
|
|
119
|
+
origin: origin.href,
|
|
120
|
+
checkedAtMs: now,
|
|
121
|
+
source: "platform",
|
|
122
|
+
config: validatePublicDeployment(JSON.parse(raw)),
|
|
123
|
+
};
|
|
124
|
+
await mkdir(join(dataDir, "runtime"), { recursive: true, mode: 0o700 });
|
|
125
|
+
await writePrivateJson(path, result);
|
|
126
|
+
return result;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
await mkdir(join(dataDir, "runtime"), { recursive: true, mode: 0o700 });
|
|
129
|
+
await writePrivateJson(path, {
|
|
130
|
+
origin: origin.href,
|
|
131
|
+
checkedAtMs: now,
|
|
132
|
+
error: true,
|
|
133
|
+
});
|
|
134
|
+
throw e;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function inspectDeploymentStatus(dataDir, options = {}) {
|
|
138
|
+
try {
|
|
139
|
+
const policy = await readPrivateJson(join(dataDir, "wallet/policy.json"));
|
|
140
|
+
if (!policy) return { state: "unbound" };
|
|
141
|
+
const result = await loadCurrentDeployment({ dataDir, ...options });
|
|
142
|
+
if (!result) return { state: "unknown" };
|
|
143
|
+
const changed = deploymentChanges(policy, result.config);
|
|
144
|
+
return {
|
|
145
|
+
state: changed.length
|
|
146
|
+
? "changed"
|
|
147
|
+
: result.source === "platform"
|
|
148
|
+
? "matching"
|
|
149
|
+
: "downloaded",
|
|
150
|
+
changed,
|
|
151
|
+
checkedAtMs: result.checkedAtMs,
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return { state: "unknown" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function syncSetupDeployment({
|
|
158
|
+
dataDir,
|
|
159
|
+
config,
|
|
160
|
+
authorization,
|
|
161
|
+
userDir = installedUserDir,
|
|
162
|
+
}) {
|
|
163
|
+
const policy = await readPrivateJson(join(dataDir, "wallet/policy.json"));
|
|
164
|
+
if (!policy || deploymentChanges(policy, config).length)
|
|
165
|
+
throw Error("Verified Wallet binding does not match deployment settings.");
|
|
166
|
+
await importSetupFiles(dataDir, userDir);
|
|
167
|
+
const parent = dataDir,
|
|
168
|
+
path = join(parent, "anvil-onboarding.json"),
|
|
169
|
+
saved = await readPrivateJson(path);
|
|
170
|
+
// Preserve owner selection, Safe, Runtime address, recovery drafts and test requests.
|
|
171
|
+
if (saved) {
|
|
172
|
+
await writePrivateJson(path + "." + randomUUID() + ".previous", saved);
|
|
173
|
+
await writePrivateJson(path, { ...saved, ...config, safe: policy.safe });
|
|
174
|
+
}
|
|
175
|
+
if (authorization) {
|
|
176
|
+
if (authorization.policyId.toLowerCase() !== policy.policyId.toLowerCase())
|
|
177
|
+
throw Error("Setup projection Policy mismatch.");
|
|
178
|
+
const draft = join(dataDir, "runtime/authorization-draft.json"),
|
|
179
|
+
oldDraft = await readPrivateJson(draft);
|
|
180
|
+
if (oldDraft && oldDraft.policyId !== policy.policyId)
|
|
181
|
+
await writePrivateJson(
|
|
182
|
+
draft + "." + randomUUID() + ".previous",
|
|
183
|
+
oldDraft,
|
|
184
|
+
);
|
|
185
|
+
if (saved)
|
|
186
|
+
await writePrivateJson(draft, {
|
|
187
|
+
...authorization,
|
|
188
|
+
version: 1,
|
|
189
|
+
delegate: policy.delegate,
|
|
190
|
+
ownerAddress: saved.ownerAddress,
|
|
191
|
+
});
|
|
192
|
+
const publicPath = join(dataDir, "public-policy.json"),
|
|
193
|
+
oldPublic = await readPrivateJson(publicPath);
|
|
194
|
+
if (oldPublic)
|
|
195
|
+
await writePrivateJson(publicPath, { ...oldPublic, ...authorization });
|
|
196
|
+
}
|
|
197
|
+
await writePrivateJson(join(parent, "user-deployment.json"), config);
|
|
198
|
+
}
|