@farmslot/adapter-web 0.6.0 → 0.6.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 +5 -0
- package/README.md +1 -0
- package/dist/web-dapp/index.d.mts +1 -0
- package/dist/web-dapp/launch.d.mts +3 -2
- package/dist/web-dapp/lib/session-lock.d.mts +21 -0
- package/package.json +1 -1
- package/src/web-dapp/index.mjs +6 -0
- package/src/web-dapp/launch.mjs +14 -2
- package/src/web-dapp/lib/session-lock.mjs +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@ 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.6.1 - 2026-10-08
|
|
10
|
+
|
|
11
|
+
- `web-dapp` refuses a headful launch while the macOS login session is locked (`SESSION_LOCKED`, with the unlock step), before starting, reusing or stopping a browser, as mm-harness does since its #363 (0.6.0 shipped `web-dapp` without it). `macosSessionLocked()`, `parseSessionLocked()`, `SESSION_LOCKED_MESSAGE` and `SESSION_LOCKED_USER_ACTION` are exported.
|
|
12
|
+
- Publish with adapter-sdk 0.7.1 and recipe-runner 0.27.0 so consumers share one adapter-sdk and one recipe-runner copy.
|
|
13
|
+
|
|
9
14
|
## 0.6.0 - 2026-10-08
|
|
10
15
|
|
|
11
16
|
- Add `web-dapp` (`@farmslot/adapter-web/web-dapp`, an ESM subpath): `createWebDappAdapter({ id, signerModule, cli, hooks })` returns an `@farmslot/adapter-sdk` `PlatformAdapter` for a web app under test whose dev server the slot owns, moved from mm-harness's Web Terminal adapter. It runs a slot browser and wallet host with the injected strict wallet or a host-supplied extension signer, held to the testnet venue of the adapter that extends it. See the adapter-web reference for the signer module, venue policy and hooks.
|
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ const binding = createWalletRequestBinding({
|
|
|
114
114
|
|
|
115
115
|
- **Venue policy.** The app's venue (the hosts a testnet run blocks and serves, the typed data to refuse, the start page) comes from the adapter that `extends: 'web-dapp'`: its policy module goes to `RECIPE_WEB_DAPP_POLICY` through `bindWebDappPolicy(adapter)`. Bare web-dapp has no policy and refuses to launch. `fencePolicy` and `assertPolicyDigest` hold the module and what it imports to the files the plugin digest covers.
|
|
116
116
|
- **Served network.** A policy that declares served hosts (`venueHosts().served`) has the wallet host wait for the app to request one of them, which proves the dev server serves testnet. A policy for an app whose page makes no venue request of its own (such as test-dapp-multichain, whose wallet traffic goes through the extension) opts out explicitly: `venueHosts()` returns `served: []` with `servedCheck: 'not-applicable'`. The check is then skipped, and `networkEnforcement.served` in `browser.json` and the `network-enforcement` entry of the wallet request log say `not-applicable`. An empty `served` without that opt-out keeps the check, which fails closed, so a policy that computes its served hosts can't lose the check by finding none. Listing served hosts and opting out together is refused. Mainnet blocking is enforced either way.
|
|
117
|
+
- **Locked session.** A headful launch refuses while the macOS login session is locked (`SESSION_LOCKED`), before starting, reusing or stopping a browser: macOS stops painting visible windows, so screenshots, video and UI actions would stall. `macosSessionLocked()` reads `ioreg` (3 s bound; `null` off macOS or when unknown). A headless launch never probes.
|
|
117
118
|
- **Signers.** `signer=injected` needs nothing. `signer=extension` belongs to the host: a signer module exports `signers.extension` with `prepareProfile` (load the extension, seed the profile; returns `{ browserArgs, secrets, state }` and may carry an `afterBrowserStart` step; both steps get `trackSecret` for key-material files), `confirm` (required with `prepareProfile`: without it the wallet host would close the wallet's own windows) and `readinessChecks`. `signers.injected.identity` sets the EIP-6963 identity the injected strict wallet presents. The module path (`signerModule`, `--signer-module` or `RECIPE_WEB_DAPP_SIGNER_MODULE`) is the one source: readiness, launch, verify and the wallet host all load it, and `createWebDappAdapter` throws if given an in-process `signers` object. With no signer requested, the signer is `extension` when a module is configured and `injected` otherwise, so a host whose module exports only `injected` sets the signer explicitly (`--signer injected` or `TERMINAL_SIGNER=injected`).
|
|
118
119
|
- **Hooks.** The action set, console capture, and network and performance observation are the host's; pass them as `hooks`. `diagnostics`, `readiness` and `harness` merge over the generic members.
|
|
119
120
|
- **Leaves.** `webDappLeafPath('launch' | 'wallet-host' | 'inject' | 'verify' | 'stop' | 'cleanup')` names the CLI scripts.
|
|
@@ -121,3 +121,4 @@ export { assertPolicyDigest, fencePolicy, POLICY_DIGEST_ENV, PolicyFenceError }
|
|
|
121
121
|
export { cdpListenerPids, ownedBrowserPids, OWNER_FLAG, OWNER_KINDS, ownerMarker, ownsProcess, pidAlive, portFree, stopConsoleCollector, stopWebDappBrowser, terminate } from "./lib/processes.mjs";
|
|
122
122
|
export { assertLaunchNetwork, DEV_SERVER_PID_FILE, devServerTestnetDiagnostic, pythonFor, webDappReadiness } from "./lib/readiness.mjs";
|
|
123
123
|
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";
|
|
124
|
+
export { macosSessionLocked, parseSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "./lib/session-lock.mjs";
|
|
@@ -40,13 +40,14 @@ export function writePrivateFile(file: any, text: any): void;
|
|
|
40
40
|
/**
|
|
41
41
|
* @param {Record<string, any>} args the parsed launch flags (see `parseLaunchArgs`)
|
|
42
42
|
* @param {NodeJS.ProcessEnv} [env]
|
|
43
|
-
* @param {{ timeouts?: { browserStartMs: number, hostReadyMs: number } }} [options]
|
|
43
|
+
* @param {{ timeouts?: { browserStartMs: number, hostReadyMs: number }, sessionLocked?: () => boolean | null }} [options]
|
|
44
44
|
*/
|
|
45
|
-
export function launchWebDappBrowser(args: Record<string, any>, env?: NodeJS.ProcessEnv, { timeouts }?: {
|
|
45
|
+
export function launchWebDappBrowser(args: Record<string, any>, env?: NodeJS.ProcessEnv, { timeouts, sessionLocked }?: {
|
|
46
46
|
timeouts?: {
|
|
47
47
|
browserStartMs: number;
|
|
48
48
|
hostReadyMs: number;
|
|
49
49
|
};
|
|
50
|
+
sessionLocked?: () => boolean | null;
|
|
50
51
|
}): Promise<any>;
|
|
51
52
|
export const SPAWN: "spawn";
|
|
52
53
|
export const LAUNCH_SERVICES: "launch-services";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ioreg -n Root -d1 -a` output: the lock flag of the on-console session in
|
|
3
|
+
* IOConsoleUsers (any session's when none is on console); false for a plist
|
|
4
|
+
* without it, null for anything else.
|
|
5
|
+
* @param {string} output
|
|
6
|
+
* @returns {boolean | null}
|
|
7
|
+
*/
|
|
8
|
+
export function parseSessionLocked(output: string): boolean | null;
|
|
9
|
+
/**
|
|
10
|
+
* true or false on macOS; null when unknown (another platform, or ioreg failed
|
|
11
|
+
* or took longer than 3 s). Never throws.
|
|
12
|
+
* @param {{ platform?: string, spawnSync?: typeof nodeSpawnSync }} [deps]
|
|
13
|
+
* @returns {boolean | null}
|
|
14
|
+
*/
|
|
15
|
+
export function macosSessionLocked({ platform, spawnSync, }?: {
|
|
16
|
+
platform?: string;
|
|
17
|
+
spawnSync?: typeof nodeSpawnSync;
|
|
18
|
+
}): boolean | null;
|
|
19
|
+
export const SESSION_LOCKED_MESSAGE: "The macOS login session is locked, so the headful browser can't paint (screenshots, video and UI actions stall).";
|
|
20
|
+
export const SESSION_LOCKED_USER_ACTION: string;
|
|
21
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/adapter-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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": {
|
package/src/web-dapp/index.mjs
CHANGED
|
@@ -101,6 +101,12 @@ export {
|
|
|
101
101
|
webDappRuntimePath,
|
|
102
102
|
writePrivateJson,
|
|
103
103
|
} from './lib/runtime.mjs';
|
|
104
|
+
export {
|
|
105
|
+
macosSessionLocked,
|
|
106
|
+
parseSessionLocked,
|
|
107
|
+
SESSION_LOCKED_MESSAGE,
|
|
108
|
+
SESSION_LOCKED_USER_ACTION,
|
|
109
|
+
} from './lib/session-lock.mjs';
|
|
104
110
|
|
|
105
111
|
const LEAVES = Object.freeze(['launch', 'wallet-host', 'inject', 'verify', 'stop', 'cleanup']);
|
|
106
112
|
|
package/src/web-dapp/launch.mjs
CHANGED
|
@@ -73,6 +73,11 @@ import {
|
|
|
73
73
|
webDappPolicy,
|
|
74
74
|
webDappRuntimeDir,
|
|
75
75
|
} from './lib/runtime.mjs';
|
|
76
|
+
import {
|
|
77
|
+
macosSessionLocked,
|
|
78
|
+
SESSION_LOCKED_MESSAGE,
|
|
79
|
+
SESSION_LOCKED_USER_ACTION,
|
|
80
|
+
} from './lib/session-lock.mjs';
|
|
76
81
|
import { defaultSigner, loadSigners, SIGNER_MODULE_ENV } from './lib/signers.mjs';
|
|
77
82
|
|
|
78
83
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -393,12 +398,12 @@ export const LAUNCH_TIMEOUTS = Object.freeze({ browserStartMs: 90000, hostReadyM
|
|
|
393
398
|
/**
|
|
394
399
|
* @param {Record<string, any>} args the parsed launch flags (see `parseLaunchArgs`)
|
|
395
400
|
* @param {NodeJS.ProcessEnv} [env]
|
|
396
|
-
* @param {{ timeouts?: { browserStartMs: number, hostReadyMs: number } }} [options]
|
|
401
|
+
* @param {{ timeouts?: { browserStartMs: number, hostReadyMs: number }, sessionLocked?: () => boolean | null }} [options]
|
|
397
402
|
*/
|
|
398
403
|
export async function launchWebDappBrowser(
|
|
399
404
|
args,
|
|
400
405
|
env = process.env,
|
|
401
|
-
{ timeouts = LAUNCH_TIMEOUTS } = {},
|
|
406
|
+
{ timeouts = LAUNCH_TIMEOUTS, sessionLocked = macosSessionLocked } = {},
|
|
402
407
|
) {
|
|
403
408
|
const policy = webDappPolicy(env);
|
|
404
409
|
const signerModule = args['signer-module'] ?? env[SIGNER_MODULE_ENV];
|
|
@@ -474,6 +479,13 @@ export async function launchWebDappBrowser(
|
|
|
474
479
|
const appOrigin = `http://localhost:${appPort}`;
|
|
475
480
|
const headless = resolveHeadless({ headless: args.headless, headful: args.headful, env });
|
|
476
481
|
const slowMoMs = resolveSlowMo({ flag: args['slow-mo'], env });
|
|
482
|
+
// A headful browser can't paint while the macOS session is locked: refuse
|
|
483
|
+
// before starting, reusing or stopping one.
|
|
484
|
+
if (!headless && sessionLocked() === true) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`SESSION_LOCKED: ${SESSION_LOCKED_MESSAGE}\nNext: ${SESSION_LOCKED_USER_ACTION}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
477
489
|
|
|
478
490
|
const { fixture } = await loadWalletFixture(target, env);
|
|
479
491
|
const accountAddress = viemAccountFromEntry(fixtureEntry(fixture, args.account), target).address;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Whether the macOS login session is locked. macOS stops painting visible
|
|
2
|
+
// windows while it is, so a headful browser's screenshots, video and UI actions
|
|
3
|
+
// stall until their own timeouts. The launcher refuses a headful launch up front
|
|
4
|
+
// instead (SESSION_LOCKED).
|
|
5
|
+
|
|
6
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
export const SESSION_LOCKED_MESSAGE =
|
|
9
|
+
"The macOS login session is locked, so the headful browser can't paint (screenshots, video and UI actions stall).";
|
|
10
|
+
export const SESSION_LOCKED_USER_ACTION =
|
|
11
|
+
'Unlock the Mac and keep its login session unlocked while the run drives the browser, then rerun. ' +
|
|
12
|
+
'caffeinate keeps the Mac awake but does not unlock it.';
|
|
13
|
+
|
|
14
|
+
const LOCKED = /<key>CGSSessionScreenIsLocked<\/key>\s*<true\/>/u;
|
|
15
|
+
const ON_CONSOLE = /<key>kCGSSessionOnConsoleKey<\/key>\s*<true\/>/u;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `ioreg -n Root -d1 -a` output: the lock flag of the on-console session in
|
|
19
|
+
* IOConsoleUsers (any session's when none is on console); false for a plist
|
|
20
|
+
* without it, null for anything else.
|
|
21
|
+
* @param {string} output
|
|
22
|
+
* @returns {boolean | null}
|
|
23
|
+
*/
|
|
24
|
+
export function parseSessionLocked(output) {
|
|
25
|
+
if (!/<plist[\s>]/u.test(output)) return null;
|
|
26
|
+
const users = /<key>IOConsoleUsers<\/key>\s*<array>([\s\S]*?)<\/array>/u.exec(output)?.[1] ?? '';
|
|
27
|
+
const onConsole = users
|
|
28
|
+
.match(/<dict>[\s\S]*?<\/dict>/gu)
|
|
29
|
+
?.find((session) => ON_CONSOLE.test(session));
|
|
30
|
+
return LOCKED.test(onConsole ?? output);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* true or false on macOS; null when unknown (another platform, or ioreg failed
|
|
35
|
+
* or took longer than 3 s). Never throws.
|
|
36
|
+
* @param {{ platform?: string, spawnSync?: typeof nodeSpawnSync }} [deps]
|
|
37
|
+
* @returns {boolean | null}
|
|
38
|
+
*/
|
|
39
|
+
export function macosSessionLocked({
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
spawnSync = nodeSpawnSync,
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (platform !== 'darwin') return null;
|
|
44
|
+
try {
|
|
45
|
+
const result = spawnSync('ioreg', ['-n', 'Root', '-d1', '-a'], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
timeout: 3_000,
|
|
48
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
+
});
|
|
51
|
+
if (result.error || result.status !== 0) return null;
|
|
52
|
+
return parseSessionLocked(String(result.stdout));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|