@farmslot/adapter-web 0.18.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ All notable changes to `@farmslot/adapter-web` are tracked here.
6
6
 
7
7
  - Active-development baseline; add user-facing changes here before release or package publication.
8
8
 
9
+ ## 0.19.1 - 2026-11-08
10
+
11
+ - Republish against protocol 0.82.2 and the current package set, so consumers load one copy of each Farmslot package.
12
+
13
+ ## 0.19.0 - 2026-11-06
14
+
15
+ - Web-dapp wallet accounts: a mnemonic entry yields every account its `count` names (`names[]`, else dev1, dev2, ...), and `--account` selects one by name or address. A host can add named derivations of a fixture secret at a higher HD index through `RECIPE_WALLET_DERIVED_ACCOUNTS`; a declared address that does not match is refused. `browser.json` records the account's fixture secret name and index.
16
+
9
17
  ## 0.18.0 - 2026-11-05
10
18
 
11
19
  - Run ordinary websites in an owned extension-free browser with shared CDP actions, screenshots and whole-run recording. Existing extension launches retain their lifecycle.
@@ -125,5 +125,5 @@ export { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, shellQuote, wal
125
125
  export { assertPolicyDigest, fencePolicy, POLICY_DIGEST_ENV, PolicyFenceError } from "./lib/policy-fence.mjs";
126
126
  export { cdpListenerPids, ownedBrowserPids, OWNER_FLAG, OWNER_KINDS, ownerMarker, ownsProcess, pidAlive, portFree, stopConsoleCollector, stopWebDappBrowser, terminate } from "./lib/processes.mjs";
127
127
  export { assertLaunchNetwork, DEV_SERVER_PID_FILE, devServerTestnetDiagnostic, pythonFor, webDappReadiness } from "./lib/readiness.mjs";
128
- export { accountName, appOrigin, appPort, canonicalPath, cdpPort, DEFAULT_ACCOUNT, fixtureAccount, fixtureEntry, fixturePath, loadWalletFixture, readBrowserState, readJsonFile, resolveAccountAddress, resolveSigner, SIGNER_MODES, viemAccountFromEntry, webDappRuntimeDir, webDappRuntimePath, writePrivateJson } from "./lib/runtime.mjs";
128
+ export { accountName, appOrigin, appPort, canonicalPath, cdpPort, DEFAULT_ACCOUNT, DERIVED_ACCOUNTS_ENV, fixtureAccount, fixtureEntry, fixturePath, loadWalletFixture, readBrowserState, readJsonFile, resolveAccountAddress, resolveSigner, SIGNER_MODES, viemAccountFromEntry, walletAccount, walletAccounts, webDappRuntimeDir, webDappRuntimePath, writePrivateJson } from "./lib/runtime.mjs";
129
129
  export { macosSessionLocked, parseSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "./lib/session-lock.mjs";
@@ -15,7 +15,28 @@ export function loadWalletFixture(projectRoot: any, env?: NodeJS.ProcessEnv): Pr
15
15
  fixture: any;
16
16
  }>;
17
17
  export function fixtureEntry(fixture: any, name: any): any;
18
- export function viemAccountFromEntry(entry: any, projectRoot: any): any;
18
+ export function viemAccountFromEntry(entry: any, projectRoot: any, addressIndex?: number): any;
19
+ export function derivedAccounts(env?: NodeJS.ProcessEnv): any[];
20
+ /**
21
+ * Every account the fixture offers: each mnemonic entry expanded to its count,
22
+ * each private key, then the host's named derivations (DERIVED_ACCOUNTS_ENV)
23
+ * of the secrets this fixture holds. `account` is the viem account holding the
24
+ * key: sign with it, never log or persist it.
25
+ */
26
+ export function walletAccounts(fixture: any, projectRoot: any, env?: NodeJS.ProcessEnv): {
27
+ name: any;
28
+ address: any;
29
+ secret: any;
30
+ index: any;
31
+ account: any;
32
+ }[];
33
+ export function walletAccount(fixture: any, selector: any, projectRoot: any, env?: NodeJS.ProcessEnv): {
34
+ name: any;
35
+ address: any;
36
+ secret: any;
37
+ index: any;
38
+ account: any;
39
+ };
19
40
  export function fixtureAccount(projectRoot: any, name: any, env?: NodeJS.ProcessEnv): Promise<any>;
20
41
  export function resolveAccountAddress(input: any, env?: NodeJS.ProcessEnv): Promise<{
21
42
  name: any;
@@ -23,6 +44,7 @@ export function resolveAccountAddress(input: any, env?: NodeJS.ProcessEnv): Prom
23
44
  }>;
24
45
  export const SIGNER_MODES: readonly string[];
25
46
  export const DEFAULT_ACCOUNT: "dev1";
47
+ export const DERIVED_ACCOUNTS_ENV: "RECIPE_WALLET_DERIVED_ACCOUNTS";
26
48
  import { POLICY_ENV } from './policy.mjs';
27
49
  import { webDappAdapterId } from './policy.mjs';
28
50
  import { webDappPolicy } from './policy.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farmslot/adapter-web",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -87,9 +87,9 @@
87
87
  "prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/adapter-web"
88
88
  },
89
89
  "dependencies": {
90
- "@farmslot/adapter-sdk": "0.26.3",
91
- "@farmslot/protocol": "0.76.0",
92
- "@farmslot/recipe-runner": "0.46.6",
90
+ "@farmslot/adapter-sdk": "0.26.4",
91
+ "@farmslot/protocol": "0.82.2",
92
+ "@farmslot/recipe-runner": "0.46.8",
93
93
  "ws": "^8.18.0"
94
94
  },
95
95
  "devDependencies": {
@@ -90,6 +90,7 @@ export {
90
90
  canonicalPath,
91
91
  cdpPort,
92
92
  DEFAULT_ACCOUNT,
93
+ DERIVED_ACCOUNTS_ENV,
93
94
  fixtureAccount,
94
95
  fixtureEntry,
95
96
  fixturePath,
@@ -100,6 +101,8 @@ export {
100
101
  resolveSigner,
101
102
  SIGNER_MODES,
102
103
  viemAccountFromEntry,
104
+ walletAccount,
105
+ walletAccounts,
103
106
  webDappRuntimeDir,
104
107
  webDappRuntimePath,
105
108
  writePrivateJson,
@@ -11,11 +11,12 @@
11
11
  // wallet requests.
12
12
  //
13
13
  // Inputs (flags): --target <checkout> --cdp-port <port> --app-port <port>
14
- // [--signer extension|injected] [--signer-module <path>] [--account <fixture name>]
14
+ // [--signer extension|injected] [--signer-module <path>] [--account <name|address>]
15
15
  // [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--start-chain <id>]
16
16
  // [--json] [--network mainnet --mainnet-confirmation REAL_FUNDS] (testnet
17
17
  // otherwise, enforced in the browser from the venue policy)
18
18
  // env: TERMINAL_CHROME_BIN, RECIPE_WEB_DAPP_SIGNER_MODULE, RECIPE_WALLET_FIXTURE,
19
+ // RECIPE_WALLET_DERIVED_ACCOUNTS,
19
20
  // TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN
20
21
  // (plus whatever the signer module reads)
21
22
  // The browser is headful by default, in a window placed per slot (see
@@ -68,10 +69,9 @@ import {
68
69
  } from './lib/processes.mjs';
69
70
  import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
70
71
  import {
71
- fixtureEntry,
72
72
  loadWalletFixture,
73
73
  resolveSigner,
74
- viemAccountFromEntry,
74
+ walletAccount,
75
75
  webDappPolicy,
76
76
  webDappRuntimeDir,
77
77
  } from './lib/runtime.mjs';
@@ -86,7 +86,7 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
86
86
  const macosFocus = createRequire(import.meta.url)('../macos-focus.cjs');
87
87
 
88
88
  function usage() {
89
- return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--signer-module <path>] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
89
+ return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--signer-module <path>] [--account <name|address>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
90
90
  }
91
91
 
92
92
  export function parseLaunchArgs(argv, env = process.env) {
@@ -490,7 +490,14 @@ export async function launchWebDappBrowser(
490
490
  }
491
491
 
492
492
  const { fixture } = await loadWalletFixture(target, env);
493
- const accountAddress = viemAccountFromEntry(fixtureEntry(fixture, args.account), target).address;
493
+ // --account is an account name or address; the browser runs under its name.
494
+ const selected = walletAccount(fixture, args.account, target, env);
495
+ const account = {
496
+ name: selected.name,
497
+ address: selected.address,
498
+ secret: selected.secret,
499
+ index: selected.index,
500
+ };
494
501
 
495
502
  const previous = readStateFile(stateFile);
496
503
  const reusable =
@@ -499,7 +506,8 @@ export async function launchWebDappBrowser(
499
506
  previous.signer === signer &&
500
507
  previous.cdpPort === cdpPort &&
501
508
  previous.appOrigin === appOrigin &&
502
- previous.account?.name === args.account &&
509
+ previous.account?.name === account.name &&
510
+ previous.account?.address?.toLowerCase() === account.address.toLowerCase() &&
503
511
  previous.browser?.headless === headless &&
504
512
  (previous.network ?? 'testnet') === network &&
505
513
  previous.networkEnforcement?.fingerprint === enforcementFingerprint &&
@@ -507,7 +515,7 @@ export async function launchWebDappBrowser(
507
515
  ownsProcess('wallet-host', previous.hostPid, runtime);
508
516
  if (reusable) {
509
517
  await browserWebSocketUrl(cdpPort, 5000);
510
- log(`reusing browser pid ${previous.browserPid} (${signer}, ${args.account})`);
518
+ log(`reusing browser pid ${previous.browserPid} (${signer}, ${account.name})`);
511
519
  const state = { ...previous, slowMoMs };
512
520
  if (previous.slowMoMs !== slowMoMs)
513
521
  writePrivateFile(stateFile, `${JSON.stringify(state, null, 2)}\n`);
@@ -518,7 +526,7 @@ export async function launchWebDappBrowser(
518
526
  const browser = await resolveBrowser(env);
519
527
  const profile = path.join(
520
528
  runtime,
521
- `profile-${signer}-${args.account.replace(/[^A-Za-z0-9_-]/gu, '_')}`,
529
+ `profile-${signer}-${account.name.replace(/[^A-Za-z0-9_-]/gu, '_')}`,
522
530
  );
523
531
  if (args['fresh-profile'] && existsSync(profile)) {
524
532
  const retired = `${profile}.retired-${Date.now()}`;
@@ -571,7 +579,7 @@ export async function launchWebDappBrowser(
571
579
  target,
572
580
  runtime,
573
581
  profile,
574
- account: args.account,
582
+ account: account.name,
575
583
  fixture,
576
584
  env,
577
585
  freshProfile,
@@ -683,7 +691,7 @@ export async function launchWebDappBrowser(
683
691
  '--target',
684
692
  target,
685
693
  '--account',
686
- args.account,
694
+ account.name,
687
695
  '--start-chain',
688
696
  String(args['start-chain'] ?? policy.startChain),
689
697
  '--network',
@@ -758,7 +766,7 @@ export async function launchWebDappBrowser(
758
766
  appListeners: cdpListenerPids(appPort),
759
767
  cdpPort,
760
768
  appOrigin,
761
- account: { name: args.account, address: accountAddress },
769
+ account,
762
770
  browserPid,
763
771
  hostPid,
764
772
  browser: {
@@ -11,10 +11,9 @@ import path from 'node:path';
11
11
  import { ownsProcess, pidAlive, portFree } from './processes.mjs';
12
12
  import {
13
13
  canonicalPath,
14
- fixtureEntry,
15
14
  loadWalletFixture,
16
15
  readBrowserState,
17
- viemAccountFromEntry,
16
+ walletAccount,
18
17
  webDappPolicy,
19
18
  webDappRuntimeDir,
20
19
  } from './runtime.mjs';
@@ -359,7 +358,7 @@ export async function webDappReadiness({
359
358
 
360
359
  try {
361
360
  const { file, fixture } = await loadWalletFixture(target, env);
362
- const address = viemAccountFromEntry(fixtureEntry(fixture, account), target).address;
361
+ const { address } = walletAccount(fixture, account, target, env);
363
362
  checks.push(
364
363
  check('wallet-fixture', true, `${path.basename(file)}: account ${account} -> ${address}`),
365
364
  );
@@ -151,22 +151,26 @@ export function fixtureEntry(fixture, name) {
151
151
  .join(', ');
152
152
  throw new Error(`wallet fixture has no account named "${name}". Available: ${names}.`);
153
153
  }
154
+ return checkedEntry(entry);
155
+ }
156
+
157
+ function checkedEntry(entry) {
154
158
  if (entry.type !== 'mnemonic' && entry.type !== 'privateKey') {
155
159
  throw new Error(
156
- `wallet fixture account "${name}" must be mnemonic or privateKey, got "${entry.type}".`,
160
+ `wallet fixture account "${entry.name}" must be mnemonic or privateKey, got "${entry.type}".`,
157
161
  );
158
162
  }
159
163
  if (typeof entry.value !== 'string' || !entry.value.trim()) {
160
- throw new Error(`wallet fixture account "${name}" has no value.`);
164
+ throw new Error(`wallet fixture account "${entry.name}" has no value.`);
161
165
  }
162
166
  return entry;
163
167
  }
164
168
 
165
- // Mnemonics use BIP-44 index 0, the MetaMask default and the derivation the
166
- // Extension fixture flow imports.
167
- export function viemAccountFromEntry(entry, projectRoot) {
169
+ // Mnemonics use BIP-44 m/44'/60'/0'/0/<addressIndex>, the MetaMask derivation
170
+ // the Extension fixture flow imports; index 0 is the wallet's first account.
171
+ export function viemAccountFromEntry(entry, projectRoot, addressIndex = 0) {
168
172
  const { mnemonicToAccount, privateKeyToAccount } = loadViemAccounts(projectRoot);
169
- if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex: 0 });
173
+ if (entry.type === 'mnemonic') return mnemonicToAccount(entry.value.trim(), { addressIndex });
170
174
  const raw = entry.value.trim();
171
175
  const key = raw.startsWith('0x') ? raw : `0x${raw}`;
172
176
  if (!/^0x[0-9a-fA-F]{64}$/u.test(key)) {
@@ -175,9 +179,123 @@ export function viemAccountFromEntry(entry, projectRoot) {
175
179
  return privateKeyToAccount(key);
176
180
  }
177
181
 
182
+ // Named HD derivations of the fixture's mnemonic entries that the host adds,
183
+ // e.g. a team's shared test account at a higher index: JSON
184
+ // [{ "name", "secret": <mnemonic entry name>, "index", "address"? }]. Names and
185
+ // indexes only; the key stays in the fixture.
186
+ export const DERIVED_ACCOUNTS_ENV = 'RECIPE_WALLET_DERIVED_ACCOUNTS';
187
+
188
+ export function derivedAccounts(env = process.env) {
189
+ const raw = env[DERIVED_ACCOUNTS_ENV];
190
+ if (!raw) return [];
191
+ let value;
192
+ try {
193
+ value = JSON.parse(raw);
194
+ } catch (error) {
195
+ throw new Error(`${DERIVED_ACCOUNTS_ENV} is not JSON: ${error.message}`);
196
+ }
197
+ const valid = (item) =>
198
+ item &&
199
+ typeof item.name === 'string' &&
200
+ item.name.trim() &&
201
+ typeof item.secret === 'string' &&
202
+ item.secret.trim() &&
203
+ Number.isInteger(item.index) &&
204
+ item.index >= 0 &&
205
+ (item.address === undefined || /^0x[0-9a-fA-F]{40}$/u.test(item.address));
206
+ if (!Array.isArray(value) || !value.every(valid)) {
207
+ throw new Error(
208
+ `${DERIVED_ACCOUNTS_ENV} must be an array of { name, secret, index, address? } (index a non-negative integer).`,
209
+ );
210
+ }
211
+ return value;
212
+ }
213
+
214
+ function mnemonicCount(entry) {
215
+ const count = Number(entry.count ?? entry.numberOfAccounts ?? 1);
216
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
217
+ throw new Error(
218
+ `wallet fixture account "${entry.name}" count must be an integer from 1 through 100.`,
219
+ );
220
+ }
221
+ return count;
222
+ }
223
+
224
+ // names[index] when the entry gives one; a single account keeps the entry's
225
+ // name; otherwise dev1 with count 3 is dev1, dev2, dev3 (a name without a
226
+ // trailing number gets one appended).
227
+ function derivedName(entry, index, count) {
228
+ const explicit = Array.isArray(entry.names) ? entry.names[index] : undefined;
229
+ if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
230
+ if (count === 1) return entry.name;
231
+ const match = /^(.*?)(\d+)$/u.exec(entry.name);
232
+ return match ? `${match[1]}${Number(match[2]) + index}` : `${entry.name}${index + 1}`;
233
+ }
234
+
235
+ /**
236
+ * Every account the fixture offers: each mnemonic entry expanded to its count,
237
+ * each private key, then the host's named derivations (DERIVED_ACCOUNTS_ENV)
238
+ * of the secrets this fixture holds. `account` is the viem account holding the
239
+ * key: sign with it, never log or persist it.
240
+ */
241
+ export function walletAccounts(fixture, projectRoot, env = process.env) {
242
+ const accounts = [];
243
+ for (const entry of fixture.accounts) {
244
+ if (entry?.type !== 'mnemonic' && entry?.type !== 'privateKey') continue;
245
+ checkedEntry(entry);
246
+ const count = entry.type === 'mnemonic' ? mnemonicCount(entry) : 1;
247
+ for (let index = 0; index < count; index += 1) {
248
+ const account = viemAccountFromEntry(entry, projectRoot, index);
249
+ accounts.push({
250
+ name: derivedName(entry, index, count),
251
+ address: account.address,
252
+ secret: entry.name,
253
+ index,
254
+ account,
255
+ });
256
+ }
257
+ }
258
+ for (const item of derivedAccounts(env)) {
259
+ const entry = fixture.accounts.find(
260
+ (candidate) => candidate?.type === 'mnemonic' && candidate.name === item.secret,
261
+ );
262
+ if (!entry) continue;
263
+ const account = viemAccountFromEntry(checkedEntry(entry), projectRoot, item.index);
264
+ if (item.address && item.address.toLowerCase() !== account.address.toLowerCase()) {
265
+ throw new Error(
266
+ `wallet account "${item.name}" derives to ${account.address} from "${item.secret}" index ${item.index}, not the declared ${item.address}.`,
267
+ );
268
+ }
269
+ accounts.push({
270
+ name: item.name,
271
+ address: account.address,
272
+ secret: item.secret,
273
+ index: item.index,
274
+ account,
275
+ });
276
+ }
277
+ return accounts;
278
+ }
279
+
280
+ // The fixture account a selector names: an account name, else an address.
281
+ export function walletAccount(fixture, selector, projectRoot, env = process.env) {
282
+ const accounts = walletAccounts(fixture, projectRoot, env);
283
+ const wanted = String(selector ?? '').trim();
284
+ const found =
285
+ accounts.find((account) => account.name === wanted) ??
286
+ (/^0x[0-9a-fA-F]{40}$/u.test(wanted)
287
+ ? accounts.find((account) => account.address.toLowerCase() === wanted.toLowerCase())
288
+ : undefined);
289
+ if (!found) {
290
+ const names = accounts.map((account) => account.name).join(', ');
291
+ throw new Error(`wallet fixture has no account named "${wanted}". Available: ${names}.`);
292
+ }
293
+ return found;
294
+ }
295
+
178
296
  export async function fixtureAccount(projectRoot, name, env = process.env) {
179
297
  const { fixture } = await loadWalletFixture(projectRoot, env);
180
- return viemAccountFromEntry(fixtureEntry(fixture, name), projectRoot);
298
+ return walletAccount(fixture, name, projectRoot, env).account;
181
299
  }
182
300
 
183
301
  // Address-only view of the account a node targets: an explicit address wins