@le-space/playwright 0.6.30 → 0.6.32

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.
@@ -0,0 +1,66 @@
1
+ // src/aleph-instance.ts
2
+ var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
3
+ var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
4
+ var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function normalizeApiOrigin(value) {
9
+ try {
10
+ const url = new URL(value);
11
+ if (API3_HOST_PATTERN.test(url.hostname)) return null;
12
+ return url.origin;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function resolveAlephApiHosts(candidates) {
18
+ const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
19
+ const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
20
+ return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
21
+ }
22
+ async function waitForAlephInstanceDeletion(options) {
23
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
24
+ if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
25
+ const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
26
+ const hosts = resolveAlephApiHosts(options.apiHosts);
27
+ const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
28
+ let lastSummary = "Deletion has not been observed yet.";
29
+ while (Date.now() < deadline) {
30
+ const observations = [];
31
+ let replicasForgotten = true;
32
+ for (const apiHost of hosts) {
33
+ try {
34
+ const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
35
+ const payload = await response.json().catch(() => null);
36
+ const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
37
+ replicasForgotten &&= forgotten;
38
+ observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
39
+ } catch (error) {
40
+ replicasForgotten = false;
41
+ observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ let unallocated = false;
45
+ try {
46
+ const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
47
+ const payload = await response.json().catch(() => null);
48
+ unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
49
+ observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
50
+ } catch (error) {
51
+ observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ lastSummary = observations.join("; ");
54
+ if (replicasForgotten && unallocated) return lastSummary;
55
+ await delay(options.pollIntervalMs ?? 2e3);
56
+ }
57
+ throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
58
+ }
59
+
60
+ export {
61
+ SUPPORTED_ALEPH_API_HOSTS,
62
+ DEFAULT_ALEPH_SCHEDULER_URL,
63
+ delay,
64
+ resolveAlephApiHosts,
65
+ waitForAlephInstanceDeletion
66
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ declare function parseCleanupCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv): {
3
+ instanceHash: string;
4
+ apiHosts: string[];
5
+ evidencePath: string;
6
+ reason: string;
7
+ };
8
+ declare function runCleanupCli(argv?: readonly string[], env?: NodeJS.ProcessEnv): Promise<void>;
9
+
10
+ export { parseCleanupCliArgs, runCleanupCli };
package/cleanup-cli.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveAlephApiHosts,
4
+ waitForAlephInstanceDeletion
5
+ } from "./chunk-YAODEYKV.js";
6
+
7
+ // src/cleanup-cli.ts
8
+ import { createHash } from "crypto";
9
+ import { appendFile, mkdir, writeFile } from "fs/promises";
10
+ import { dirname } from "path";
11
+ import process from "process";
12
+ import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
13
+ function parseCleanupCliArgs(argv, env) {
14
+ const flags = /* @__PURE__ */ new Map();
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const argument = argv[index];
17
+ if (!argument.startsWith("--")) continue;
18
+ const equals = argument.indexOf("=");
19
+ if (equals > -1) {
20
+ flags.set(argument.slice(2, equals), argument.slice(equals + 1));
21
+ } else {
22
+ const value = argv[index + 1];
23
+ if (value == null || value.startsWith("--")) throw new Error(`Missing value for --${argument.slice(2)}`);
24
+ flags.set(argument.slice(2), value);
25
+ index += 1;
26
+ }
27
+ }
28
+ const instanceHash = (flags.get("instance-hash") ?? env.ALEPH_PLAYWRIGHT_INSTANCE_HASH ?? "").trim();
29
+ if (!/^[a-f0-9]{64}$/iu.test(instanceHash)) {
30
+ throw new Error("Cleanup requires one exact INSTANCE hash (--instance-hash or ALEPH_PLAYWRIGHT_INSTANCE_HASH)");
31
+ }
32
+ const hostCandidates = (flags.get("api-hosts") ?? env.ALEPH_VM_API_HOSTS ?? "").split(/[\s,]+/u).filter(Boolean);
33
+ return {
34
+ instanceHash,
35
+ apiHosts: resolveAlephApiHosts(hostCandidates.length > 0 ? hostCandidates : void 0),
36
+ evidencePath: (flags.get("evidence-path") ?? env.EVIDENCE_PATH ?? "").trim() || `playwright-runner-cleanup-${instanceHash.slice(0, 12)}.json`,
37
+ reason: (flags.get("reason") ?? "").trim() || `Ephemeral Playwright runner cleanup for ${instanceHash}`
38
+ };
39
+ }
40
+ function importOptional(specifier) {
41
+ return import(specifier);
42
+ }
43
+ async function createSigner(privateKey) {
44
+ const normalizedKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
45
+ try {
46
+ const { Wallet } = await importOptional("ethers");
47
+ const wallet = new Wallet(normalizedKey);
48
+ return { address: await wallet.getAddress(), sign: (payload) => wallet.signMessage(payload) };
49
+ } catch (error) {
50
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
51
+ }
52
+ try {
53
+ const { privateKeyToAccount } = await importOptional("viem/accounts");
54
+ const account = privateKeyToAccount(normalizedKey);
55
+ return { address: account.address, sign: (payload) => account.signMessage({ message: payload }) };
56
+ } catch (error) {
57
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
58
+ }
59
+ throw new Error('playwright-runner-cleanup needs either "ethers" or "viem" to sign the FORGET message');
60
+ }
61
+ async function runCleanupCli(argv = process.argv.slice(2), env = process.env) {
62
+ const options = parseCleanupCliArgs(argv, env);
63
+ const privateKey = env.ALEPH_PRIVATE_KEY?.trim();
64
+ if (!privateKey) throw new Error("ALEPH_PRIVATE_KEY is required");
65
+ const identity = await createSigner(privateKey);
66
+ const signer = (_sender, payload) => identity.sign(payload);
67
+ const fetchImpl = globalThis.fetch.bind(globalThis);
68
+ let erase;
69
+ for (const apiHost of options.apiHosts) {
70
+ try {
71
+ erase = await eraseInstanceOnCrn({
72
+ sender: identity.address,
73
+ signer,
74
+ instanceHash: options.instanceHash,
75
+ fetch: fetchImpl,
76
+ apiHost
77
+ });
78
+ break;
79
+ } catch {
80
+ }
81
+ }
82
+ let forget = null;
83
+ for (const apiHost of options.apiHosts) {
84
+ try {
85
+ forget = await forgetAlephMessages({
86
+ sender: identity.address,
87
+ hashes: [options.instanceHash],
88
+ reason: options.reason,
89
+ signer,
90
+ hasher: (content) => createHash("sha256").update(content).digest("hex"),
91
+ fetch: fetchImpl,
92
+ apiHost,
93
+ sync: true
94
+ });
95
+ if (forget.status === "rejected") throw new Error("FORGET rejected");
96
+ break;
97
+ } catch {
98
+ forget = null;
99
+ }
100
+ }
101
+ if (!forget) throw new Error(`Owner-signed FORGET failed for ${options.instanceHash}`);
102
+ const verification = await waitForAlephInstanceDeletion({
103
+ instanceHash: options.instanceHash,
104
+ apiHosts: options.apiHosts,
105
+ fetch: fetchImpl
106
+ });
107
+ await mkdir(dirname(options.evidencePath) || ".", { recursive: true });
108
+ await writeFile(
109
+ options.evidencePath,
110
+ `${JSON.stringify(
111
+ {
112
+ instanceHash: options.instanceHash,
113
+ owner: identity.address,
114
+ apiHosts: options.apiHosts,
115
+ erase,
116
+ forget: forget.itemHash,
117
+ verification
118
+ },
119
+ null,
120
+ 2
121
+ )}
122
+ `
123
+ );
124
+ if (env.GITHUB_OUTPUT) await appendFile(env.GITHUB_OUTPUT, `evidence_path=${options.evidencePath}
125
+ `);
126
+ if (env.GITHUB_STEP_SUMMARY) {
127
+ await appendFile(
128
+ env.GITHUB_STEP_SUMMARY,
129
+ `
130
+ ## Aleph Playwright cleanup
131
+
132
+ - Exact INSTANCE: \`${options.instanceHash}\`
133
+ - Runtime erase: \`${erase?.status ?? "unavailable"}\`
134
+ - Owner FORGET: \`${forget.status}\`
135
+ - Verification: ${verification}
136
+ `
137
+ );
138
+ }
139
+ }
140
+ var invokedDirectly = process.argv[1]?.endsWith("cleanup-cli.ts") || process.argv[1]?.endsWith("cleanup-cli.js") || process.argv[1]?.endsWith("playwright-runner-cleanup");
141
+ if (invokedDirectly) {
142
+ runCleanupCli().catch((error) => {
143
+ console.error(error instanceof Error ? error.message : String(error));
144
+ process.exitCode = 1;
145
+ });
146
+ }
147
+ export {
148
+ parseCleanupCliArgs,
149
+ runCleanupCli
150
+ };
package/index.d.ts CHANGED
@@ -3,9 +3,19 @@ import { Browser, Page, Locator, BrowserContext } from '@playwright/test';
3
3
  import { RelayBootstrapPostRecord, RelayBootstrapContent, fetchAlephBootstrapPosts } from '@le-space/aleph-bootstrap';
4
4
  import { eraseInstanceOnCrn, forgetAlephMessages } from '@le-space/core';
5
5
 
6
- declare const PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
7
6
  declare const SUPPORTED_ALEPH_API_HOSTS: readonly ["https://api2.aleph.im", "https://api.aleph.im"];
8
7
  declare const DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
8
+ declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
9
+ declare function waitForAlephInstanceDeletion(options: {
10
+ instanceHash: string;
11
+ apiHosts?: readonly string[];
12
+ schedulerUrl?: string;
13
+ timeoutMs?: number;
14
+ pollIntervalMs?: number;
15
+ fetch?: typeof fetch;
16
+ }): Promise<string>;
17
+
18
+ declare const PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
9
19
  interface RelayWalletAccount {
10
20
  address: string;
11
21
  signMessage(args: {
@@ -100,7 +110,7 @@ interface WaitForPubsubSubscriberOptions {
100
110
  stableForMs?: number;
101
111
  }
102
112
  interface RelayButtonDriverOptions {
103
- launcherName?: RegExp;
113
+ launcherName?: string | RegExp;
104
114
  instanceNamePlaceholder?: string;
105
115
  sshPublicKeyPlaceholder?: string;
106
116
  connectWalletName?: string;
@@ -176,7 +186,6 @@ interface CreateRelayTestOptions {
176
186
  evidence: RelayEvidence;
177
187
  cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
178
188
  }
179
- declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
180
189
  declare function connectAlephChromium(options: AlephRemoteBrowserOptions): Promise<Browser>;
181
190
  declare function buildAlephCostEvidence(options: {
182
191
  startedAt: string;
@@ -232,14 +241,6 @@ declare function waitForBootstrapRegistration(options: {
232
241
  }): Promise<RelayBootstrapPostRecord>;
233
242
  declare function selectBrowserRelayAddresses(content: Pick<RelayBootstrapContent, 'browserMultiaddrs' | 'multiaddrs'>, policy?: RelayAddressPolicy): string[];
234
243
  declare function provisionRelay(page: Page, options: ProvisionRelayOptions): Promise<ProvisionedRelay>;
235
- declare function waitForAlephInstanceDeletion(options: {
236
- instanceHash: string;
237
- apiHosts?: readonly string[];
238
- schedulerUrl?: string;
239
- timeoutMs?: number;
240
- pollIntervalMs?: number;
241
- fetch?: typeof fetch;
242
- }): Promise<string>;
243
244
  declare function waitForAlephMessageForgotten(options: {
244
245
  messageHash: string;
245
246
  apiHosts?: readonly string[];
package/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ DEFAULT_ALEPH_SCHEDULER_URL,
3
+ SUPPORTED_ALEPH_API_HOSTS,
4
+ delay,
5
+ resolveAlephApiHosts,
6
+ waitForAlephInstanceDeletion
7
+ } from "./chunk-YAODEYKV.js";
8
+
1
9
  // src/index.ts
2
10
  import { createHash } from "crypto";
3
11
  import { appendFile, mkdir, writeFile } from "fs/promises";
@@ -6,23 +14,6 @@ import { fetchAlephBootstrapPosts } from "@le-space/aleph-bootstrap";
6
14
  import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
7
15
  import { test as playwrightTest } from "@playwright/test";
8
16
  var PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
9
- var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
10
- var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
11
- var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
12
- function normalizeApiOrigin(value) {
13
- try {
14
- const url = new URL(value);
15
- if (API3_HOST_PATTERN.test(url.hostname)) return null;
16
- return url.origin;
17
- } catch {
18
- return null;
19
- }
20
- }
21
- function resolveAlephApiHosts(candidates) {
22
- const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
23
- const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
24
- return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
25
- }
26
17
  function requireFiniteNumber(value, label) {
27
18
  const number = Number(value);
28
19
  if (!Number.isFinite(number)) throw new Error(`${label} must be a finite number`);
@@ -209,7 +200,7 @@ var RelayButtonDriver = class {
209
200
  constructor(page, options = {}) {
210
201
  this.page = page;
211
202
  this.options = {
212
- launcherName: options.launcherName ?? /(?:Sponsor Relay|Relay Button|Relay)/,
203
+ launcherName: options.launcherName ?? "Relay Button",
213
204
  instanceNamePlaceholder: options.instanceNamePlaceholder ?? "Instance name",
214
205
  sshPublicKeyPlaceholder: options.sshPublicKeyPlaceholder ?? "SSH public key",
215
206
  connectWalletName: options.connectWalletName ?? "Connect MetaMask",
@@ -298,9 +289,6 @@ async function waitForDeploymentUi(page, instanceName, timeoutMs) {
298
289
  throw new Error(`Relay Button deployment failed: ${result.message}`);
299
290
  }
300
291
  }
301
- function delay(ms) {
302
- return new Promise((resolve) => setTimeout(resolve, ms));
303
- }
304
292
  async function findAlephInstanceHash(options) {
305
293
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
306
294
  if (!fetchImpl) throw new Error("A fetch implementation is required for INSTANCE lookup");
@@ -452,43 +440,6 @@ async function provisionRelay(page, options) {
452
440
  driver
453
441
  };
454
442
  }
455
- async function waitForAlephInstanceDeletion(options) {
456
- const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
457
- if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
458
- const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
459
- const hosts = resolveAlephApiHosts(options.apiHosts);
460
- const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
461
- let lastSummary = "Deletion has not been observed yet.";
462
- while (Date.now() < deadline) {
463
- const observations = [];
464
- let replicasForgotten = true;
465
- for (const apiHost of hosts) {
466
- try {
467
- const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
468
- const payload = await response.json().catch(() => null);
469
- const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
470
- replicasForgotten &&= forgotten;
471
- observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
472
- } catch (error) {
473
- replicasForgotten = false;
474
- observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
475
- }
476
- }
477
- let unallocated = false;
478
- try {
479
- const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
480
- const payload = await response.json().catch(() => null);
481
- unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
482
- observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
483
- } catch (error) {
484
- observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
485
- }
486
- lastSummary = observations.join("; ");
487
- if (replicasForgotten && unallocated) return lastSummary;
488
- await delay(options.pollIntervalMs ?? 2e3);
489
- }
490
- throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
491
- }
492
443
  async function waitForAlephMessageForgotten(options) {
493
444
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
494
445
  if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/playwright",
3
- "version": "0.6.30",
3
+ "version": "0.6.32",
4
4
  "description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,9 @@
12
12
  "import": "./index.js"
13
13
  }
14
14
  },
15
+ "bin": {
16
+ "playwright-runner-cleanup": "./cleanup-cli.js"
17
+ },
15
18
  "publishConfig": {
16
19
  "access": "public"
17
20
  },
@@ -24,8 +27,8 @@
24
27
  "url": "https://github.com/NiKrause/relay-button/issues"
25
28
  },
26
29
  "dependencies": {
27
- "@le-space/aleph-bootstrap": "0.6.30",
28
- "@le-space/core": "0.6.30"
30
+ "@le-space/aleph-bootstrap": "0.6.32",
31
+ "@le-space/core": "0.6.32"
29
32
  },
30
33
  "peerDependencies": {
31
34
  "@playwright/test": ">=1.61.1 <1.62.0"