@codyswann/lisa 4.72.6 → 4.73.0

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 (93) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +19 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +4 -2
  3. package/all/copy-overwrite/scripts/lib/history-secret-evidence-shape.mjs +398 -6
  4. package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +175 -7
  5. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +7 -1
  6. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +11 -66
  7. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +3 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +10 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +162 -121
  10. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +8 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-hosted-scope.mjs +177 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-policy.mjs +73 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +1 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-daemon.mjs +227 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-resource.mjs +176 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql-storage.mjs +201 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-rails-mysql.mjs +220 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-rails-runtime-contract.mjs +75 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-downloads.mjs +187 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-rails-tool-identity.mjs +288 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-rails-tools.mjs +292 -0
  22. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +3 -0
  23. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +1 -1
  24. package/all/copy-overwrite/scripts/lisa-scratch-run.sh +7 -4
  25. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +148 -20
  26. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.js +126 -0
  28. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  29. package/dist/core/lisa.d.ts.map +1 -1
  30. package/dist/core/lisa.js +2 -1
  31. package/dist/core/lisa.js.map +1 -1
  32. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  33. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  34. package/dist/core/upstream-evidence-manifest.js +79 -17
  35. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  36. package/dist/opencode/plugin-templates/parity-safety-net.sh +1 -1
  37. package/package.json +4 -4
  38. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa/hooks/parity-safety-net.sh +1 -1
  41. package/plugins/lisa-agy/hooks/parity-safety-net.sh +1 -1
  42. package/plugins/lisa-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  46. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +1 -1
  50. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +1 -1
  52. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-agy/plugin.json +1 -1
  55. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  75. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-agy/plugin.json +1 -1
  80. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  85. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  90. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/src/base/hooks/parity-safety-net.sh +1 -1
  93. package/scripts/two-channel-couplings.json +1 -1
@@ -0,0 +1,220 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** The one authenticated Rails profile uses exactly four fixed test databases. */
5
+ import { realpathSync } from "node:fs";
6
+ import { randomBytes } from "node:crypto";
7
+ import { required } from "./npm-update-contract.mjs";
8
+ import { runtimeSchemas } from "./npm-update-rails-runtime-contract.mjs";
9
+ import { runtimeTime } from "./npm-update-rails-tool-identity.mjs";
10
+ import { runProcess } from "./npm-update-process-core.mjs";
11
+ import {
12
+ openMysqlResource,
13
+ assertMysqlRoot,
14
+ mysqlFailure,
15
+ } from "./npm-update-rails-mysql-resource.mjs";
16
+
17
+ const APPLICATION_KEYS = new Set([
18
+ "PATH",
19
+ "HOME",
20
+ "LANG",
21
+ "LC_ALL",
22
+ "TZ",
23
+ "TMPDIR",
24
+ "TMP",
25
+ "TEMP",
26
+ "NPM_CONFIG_USERCONFIG",
27
+ "NPM_CONFIG_GLOBALCONFIG",
28
+ "NPM_CONFIG_CACHE",
29
+ "NPM_CONFIG_REGISTRY",
30
+ "NPM_CONFIG_AUDIT",
31
+ "NPM_CONFIG_FUND",
32
+ "NPM_CONFIG_UPDATE_NOTIFIER",
33
+ "NPM_CONFIG_IGNORE_SCRIPTS",
34
+ "GIT_TERMINAL_PROMPT",
35
+ "BUNDLE_PATH",
36
+ "BUNDLE_APP_CONFIG",
37
+ "BUNDLE_FROZEN",
38
+ "BUNDLER_VERSION",
39
+ "BUNDLE_SILENCE_ROOT_WARNING",
40
+ "GEM_HOME",
41
+ "GEM_PATH",
42
+ ]);
43
+
44
+ /** Readiness is a real authenticated SQL query; query refusal never becomes ready. */
45
+ async function waitReady(resource, deadline) {
46
+ for (;;) {
47
+ runtimeTime(deadline);
48
+ try {
49
+ required(
50
+ (await resource.query("SELECT 1, @@partial_revokes;\n")) === "1\t1\n",
51
+ "MySQL fixed privilege mode differs"
52
+ );
53
+ return;
54
+ } catch (error) {
55
+ if (error.code === null || error.code === undefined) throw error;
56
+ runtimeTime(deadline);
57
+ await new Promise(resolve =>
58
+ setTimeout(resolve, Math.min(200, runtimeTime(deadline)))
59
+ );
60
+ }
61
+ }
62
+ }
63
+
64
+ /** Only local schema privileges are granted; underscores are literal under the checked fixed mode. */
65
+ function grantSql(schemas, user, password) {
66
+ const statements = [
67
+ `CREATE USER '${user}'@'%' IDENTIFIED BY '${password}';`,
68
+ ...schemas.flatMap(name => [
69
+ `CREATE DATABASE \`${name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;`,
70
+ `GRANT ALL PRIVILEGES ON \`${name}\`.* TO '${user}'@'%';`,
71
+ ]),
72
+ ];
73
+ return `${statements.join("\n")}\n`;
74
+ }
75
+
76
+ /** The least-privilege account, not administrator metadata alone, reaches every role. */
77
+ async function verifySchemas(resource, schemas, user) {
78
+ const grants = (
79
+ await resource.query("SHOW GRANTS FOR CURRENT_USER();\n", true)
80
+ )
81
+ .trim()
82
+ .split("\n");
83
+ const account = `\`${user}\`@\`%\``;
84
+ const expected = [
85
+ `GRANT USAGE ON *.* TO ${account}`,
86
+ ...schemas.map(
87
+ name => `GRANT ALL PRIVILEGES ON \`${name}\`.* TO ${account}`
88
+ ),
89
+ ].sort();
90
+ required(
91
+ grants.sort().join("\n") === expected.join("\n"),
92
+ "MySQL account schema privileges differ"
93
+ );
94
+ const selected = schemas.map(name => `'${name}'`).join(",");
95
+ const rows = await resource.query(
96
+ `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME IN (${selected}) ORDER BY SCHEMA_NAME;\n`,
97
+ true
98
+ );
99
+ required(
100
+ rows.trim().split("\n").sort().join("\n") ===
101
+ schemas
102
+ .map(name => `${name}\tutf8mb4\tutf8mb4_0900_ai_ci`)
103
+ .sort()
104
+ .join("\n"),
105
+ "MySQL four-schema readback differs"
106
+ );
107
+ for (const name of schemas)
108
+ required(
109
+ (await resource.query(`USE \`${name}\`; SELECT 1;\n`, true)) === "1\n",
110
+ "MySQL role access readback differs"
111
+ );
112
+ }
113
+
114
+ /** Empty administrator-created databases cannot masquerade as Rails schema preparation. */
115
+ async function verifyPreparedSchemas(resource, schemas) {
116
+ const selected = schemas.map(name => `'${name}'`).join(",");
117
+ const rows = await resource.query(
118
+ `SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA IN (${selected}) AND TABLE_NAME IN ('ar_internal_metadata','schema_migrations') ORDER BY TABLE_SCHEMA, TABLE_NAME;\n`,
119
+ true
120
+ );
121
+ const expected = schemas
122
+ .flatMap(name =>
123
+ ["ar_internal_metadata", "schema_migrations"].map(
124
+ table => `${name}\t${table}`
125
+ )
126
+ )
127
+ .sort();
128
+ required(
129
+ rows.trim().split("\n").sort().join("\n") === expected.join("\n"),
130
+ "MySQL prepared four-schema metadata differs"
131
+ );
132
+ }
133
+
134
+ /** Caller-authenticated profile and frozen Bundler must precede this fixed resource lifecycle. */
135
+ export async function openRailsMysqlRuntime(
136
+ { cwd, root, deadline, profile, docker },
137
+ env
138
+ ) {
139
+ const schemas = runtimeSchemas({ runtime: profile });
140
+ runtimeTime(deadline);
141
+ assertMysqlRoot(root);
142
+ required(realpathSync(cwd) === cwd, "MySQL application cwd is aliased");
143
+ required(
144
+ env &&
145
+ Object.keys(env).every(name => APPLICATION_KEYS.has(name)) &&
146
+ Object.values(env).every(value => typeof value === "string") &&
147
+ env.BUNDLE_FROZEN === "true",
148
+ "MySQL application environment is not closed frozen Bundler data"
149
+ );
150
+ required(
151
+ env.BUNDLER_VERSION === "2.4.10",
152
+ "MySQL Bundler version is not qualified"
153
+ );
154
+ const user = `lisa_${randomBytes(12).toString("hex")}`;
155
+ const password = randomBytes(32).toString("hex");
156
+ const resource = await openMysqlResource({
157
+ root,
158
+ deadline,
159
+ docker,
160
+ user,
161
+ password,
162
+ });
163
+ let prepared = false;
164
+ const environment = Object.freeze({
165
+ ...env,
166
+ DATABASE_NAME: profile.database,
167
+ DATABASE_USER: user,
168
+ DATABASE_PASSWORD: password,
169
+ DATABASE_PORT: resource.state.port,
170
+ PRIMARY_DB_HOST: "127.0.0.1",
171
+ DATABASE_REPLICA_HOST: "127.0.0.1",
172
+ DATABASE_SSL: "false",
173
+ DATABASE_IAM_AUTH: "false",
174
+ RAILS_ENV: "test",
175
+ RACK_ENV: "test",
176
+ });
177
+ try {
178
+ await waitReady(resource, deadline);
179
+ await resource.query(grantSql(schemas, user, password));
180
+ await verifySchemas(resource, schemas, user);
181
+ } catch (error) {
182
+ try {
183
+ await resource.close();
184
+ } catch (cleanup) {
185
+ throw new AggregateError(
186
+ [error, cleanup],
187
+ "MySQL readiness and cleanup failed",
188
+ { cause: error }
189
+ );
190
+ }
191
+ throw error;
192
+ }
193
+ return {
194
+ env: environment,
195
+ async prepareSchemas() {
196
+ required(!resource.state.closed, "MySQL runtime is closed");
197
+ try {
198
+ await runProcess("bundle", ["exec", "rails", "db:prepare"], {
199
+ cwd,
200
+ env: environment,
201
+ timeout: runtimeTime(deadline, 1800000),
202
+ maximum: 3145728,
203
+ });
204
+ } catch (error) {
205
+ throw mysqlFailure(error, "Rails prepare");
206
+ }
207
+ await verifySchemas(resource, schemas, user);
208
+ await verifyPreparedSchemas(resource, schemas);
209
+ prepared = true;
210
+ },
211
+ close: resource.close,
212
+ get receipt() {
213
+ return Object.freeze({
214
+ ...resource.state,
215
+ schemas: [...schemas],
216
+ prepared,
217
+ });
218
+ },
219
+ };
220
+ }
@@ -0,0 +1,75 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Closed committed runtime data cannot select commands, credentials or tool versions. */
5
+ import { keys, required } from "./npm-update-invariants.mjs";
6
+ import { canonicalJson } from "./automation-provenance-contract.mjs";
7
+ import { sha256 } from "./github-attestation-verifier.mjs";
8
+
9
+ /** Explicit absence preserves the historical npm-only contract byte for byte. */
10
+ export function railsRuntime(policy) {
11
+ if (!Object.hasOwn(policy, "runtime")) return undefined;
12
+ const value = policy.runtime;
13
+ keys(value, ["profile", "database", "browser", "dockerFixtures"]);
14
+ required(
15
+ value.profile === "rails-mysql" &&
16
+ typeof value.database === "string" &&
17
+ /^[a-z][a-z0-9_]{0,40}$/.test(value.database) &&
18
+ typeof value.browser === "boolean" &&
19
+ typeof value.dockerFixtures === "boolean",
20
+ "unsupported closed Rails runtime profile"
21
+ );
22
+ return structuredClone(value);
23
+ }
24
+
25
+ /** All four finite fields enter the signed proposal, independently of caller inputs. */
26
+ export function runtimeBinding(policy) {
27
+ const value = railsRuntime(policy);
28
+ return value === undefined
29
+ ? {}
30
+ : { runtimeSha256: sha256(canonicalJson(value)) };
31
+ }
32
+
33
+ /** Four role names have one bounded ASCII base; no SQL or environment keys are accepted. */
34
+ export function runtimeSchemas(policy) {
35
+ const value = railsRuntime(policy);
36
+ required(value !== undefined, "Rails runtime profile is absent");
37
+ return ["test", "queue_test", "cache_test", "cable_test"].map(
38
+ suffix => `${value.database}_${suffix}`
39
+ );
40
+ }
41
+
42
+ /** Original committed policy must affirm a descriptor's optional runtime authority. */
43
+ export function assertRuntimeBinding(value, policy) {
44
+ const expected = runtimeBinding(policy);
45
+ required(
46
+ Object.hasOwn(value, "runtimeSha256") ===
47
+ Object.hasOwn(expected, "runtimeSha256") &&
48
+ value.runtimeSha256 === expected.runtimeSha256,
49
+ "original committed Rails runtime differs"
50
+ );
51
+ }
52
+
53
+ /** The finite workflow selector can only affirm the already signed committed choice. */
54
+ export function affirmRuntimeProfile(value, policy, input) {
55
+ assertRuntimeBinding(value, policy);
56
+ const runtime = railsRuntime(policy);
57
+ required(
58
+ input === (runtime?.profile ?? "none"),
59
+ "caller Rails runtime affirmation differs"
60
+ );
61
+ return runtime;
62
+ }
63
+
64
+ /** Current native HEAD, including genuine absence, selects the sole original runtime policy. */
65
+ export function assertCommittedRuntime(value, git) {
66
+ const entry = git(["ls-tree", "HEAD", "--", ".lisa.config.json"]);
67
+ required(
68
+ entry === "" ||
69
+ /^100644 blob [a-f0-9]{40}\t\.lisa\.config\.json$/.test(entry),
70
+ "original runtime configuration is not regular"
71
+ );
72
+ const original =
73
+ entry === "" ? {} : JSON.parse(git(["show", "HEAD:.lisa.config.json"]));
74
+ assertRuntimeBinding(value, original.npmUpdater ?? {});
75
+ }
@@ -0,0 +1,187 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Fixed official archives are authenticated before any member becomes executable. */
5
+ import {
6
+ constants,
7
+ openSync,
8
+ closeSync,
9
+ writeSync,
10
+ fsyncSync,
11
+ fstatSync,
12
+ lstatSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ realpathSync,
16
+ } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { createHash } from "node:crypto";
19
+ import { required } from "./npm-update-contract.mjs";
20
+
21
+ const VERSION = "154.0.8037.92";
22
+
23
+ /** The verified browser paths enter a finite compatibility environment, never arbitrary policy keys. */
24
+ export function railsBrowserEnvironment(chrome, driver, sandbox) {
25
+ return {
26
+ CHROME_BINARY: chrome,
27
+ CHROME_BIN: chrome,
28
+ CHROMEDRIVER: driver,
29
+ CHROME_DEVEL_SANDBOX: sandbox,
30
+ };
31
+ }
32
+
33
+ const DOWNLOADS = Object.freeze({
34
+ docker: {
35
+ url: "https://download.docker.com/linux/static/stable/x86_64/docker-29.8.2.tgz",
36
+ sha256: "995d1ef289677f74fd58d8d2c35727b6a4ee389c69db8638a3e42d0487aa5b0f",
37
+ binary: "6f7a95d27f98fc8c5f6b220898988bf8f61dd9b040936797917cbdc72c6cabe6",
38
+ },
39
+ compose: {
40
+ url: "https://github.com/docker/compose/releases/download/v5.6.0/docker-compose-linux-x86_64",
41
+ sha256: "40343e21ca777173e69cff5dbafeb37c6f81f3b0d57d9e597f036e95eb63e76a",
42
+ },
43
+ buildx: {
44
+ url: "https://github.com/docker/buildx/releases/download/v0.38.0/buildx-v0.38.0.linux-amd64",
45
+ sha256: "4fe4cc38adf48169132749b6ca22a990928db0118e3407584ee553723115d287",
46
+ },
47
+ chrome: {
48
+ url: `https://storage.googleapis.com/chrome-for-testing-public/${VERSION}/linux64/chrome-linux64.zip`,
49
+ sha256: "ff43322f335e436b2f4dcdfeeec5db032299e335a7e8c1c618b326e100ce8732",
50
+ binary: "439f367a9a24dde467dca9c4e4c2feedd3266899c5a8e3eb17b68b05d4da9f5f",
51
+ sandbox: "24beb85e4149c65db5bf40fa307721143d0883ab8952e60dde1158606f644dee",
52
+ },
53
+ chromedriver: {
54
+ url: `https://storage.googleapis.com/chrome-for-testing-public/${VERSION}/linux64/chromedriver-linux64.zip`,
55
+ sha256: "cc99c87cfd10e1da1b7d42ea9800a211df279f5a47c19a55b02c927cb9f606c2",
56
+ binary: "83dad64578afcd2036c3c31d02fd9e01efdd957ac45dd1de7c97b061ace9d6dc",
57
+ },
58
+ });
59
+
60
+ /** Exact pinned archive bytes are rechecked through a private no-follow descriptor before extraction. */
61
+ export function assertRailsArchive(name, file) {
62
+ required(
63
+ Object.hasOwn(DOWNLOADS, name) && realpathSync(file) === file,
64
+ "fixed vendor archive identity differs"
65
+ );
66
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
67
+ try {
68
+ const st = fstatSync(fd);
69
+ required(
70
+ st.isFile() &&
71
+ st.nlink === 1 &&
72
+ st.uid === process.getuid() &&
73
+ (st.mode & 0o077) === 0 &&
74
+ st.size > 0 &&
75
+ st.size <= 250000000,
76
+ "fixed vendor archive is not private owned data"
77
+ );
78
+ required(
79
+ createHash("sha256").update(readFileSync(fd)).digest("hex") ===
80
+ DOWNLOADS[name].sha256,
81
+ "fixed vendor archive integrity differs"
82
+ );
83
+ const current = lstatSync(file);
84
+ required(
85
+ current.dev === st.dev &&
86
+ current.ino === st.ino &&
87
+ !current.isSymbolicLink(),
88
+ "fixed vendor archive ownership changed"
89
+ );
90
+ } finally {
91
+ closeSync(fd);
92
+ }
93
+ }
94
+
95
+ /** The exclusive descriptor receives bounded vendor bytes; an existing leaf is never overwritten. */
96
+ export async function downloadRailsTool(name, root, deadline) {
97
+ required(
98
+ Object.hasOwn(DOWNLOADS, name),
99
+ "unsupported fixed runtime download"
100
+ );
101
+ const remaining = deadline - Date.now();
102
+ required(
103
+ Number.isSafeInteger(deadline) && remaining > 0 && remaining <= 1800000,
104
+ "original runtime download deadline is absent or expired"
105
+ );
106
+ const file = join(root, `${name}.archive`);
107
+ const fd = openSync(
108
+ file,
109
+ constants.O_WRONLY |
110
+ constants.O_CREAT |
111
+ constants.O_EXCL |
112
+ constants.O_NOFOLLOW,
113
+ 0o600
114
+ );
115
+ const controller = new AbortController();
116
+ const timer = setTimeout(
117
+ () => controller.abort(),
118
+ Math.min(remaining, 60000)
119
+ );
120
+ const digest = createHash("sha256");
121
+ let size = 0;
122
+ try {
123
+ const response = await fetch(DOWNLOADS[name].url, {
124
+ signal: controller.signal,
125
+ });
126
+ required(
127
+ response.ok && response.body && response.url.startsWith("https://"),
128
+ "fixed vendor download failed"
129
+ );
130
+ for await (const part of response.body) {
131
+ size += part.length;
132
+ required(size <= 250000000, "fixed vendor download exceeds bound");
133
+ digest.update(part);
134
+ let offset = 0;
135
+ while (offset < part.length) offset += writeSync(fd, part, offset);
136
+ }
137
+ fsyncSync(fd);
138
+ required(
139
+ size > 0 && digest.digest("hex") === DOWNLOADS[name].sha256,
140
+ "fixed vendor download integrity differs"
141
+ );
142
+ const st = fstatSync(fd);
143
+ const current = lstatSync(file);
144
+ required(
145
+ st.isFile() &&
146
+ st.nlink === 1 &&
147
+ st.uid === process.getuid() &&
148
+ (st.mode & 0o077) === 0 &&
149
+ current.dev === st.dev &&
150
+ current.ino === st.ino &&
151
+ !current.isSymbolicLink(),
152
+ "fixed vendor download ownership changed"
153
+ );
154
+ return { file, ...DOWNLOADS[name] };
155
+ } finally {
156
+ clearTimeout(timer);
157
+ closeSync(fd);
158
+ }
159
+ }
160
+
161
+ /** Only an authenticated ZIP's fixed prefix is extracted into a newly owned empty directory. */
162
+ export async function extractRailsBrowser(name, archive, root, step) {
163
+ required(
164
+ name === "chrome" || name === "chromedriver",
165
+ "unsupported fixed browser archive"
166
+ );
167
+ assertRailsArchive(name, archive);
168
+ const prefix = `${name}-linux64/`;
169
+ const listing = await step("/usr/bin/unzip", ["-Z1", archive]);
170
+ const names = listing.stdout.toString().trimEnd().split("\n");
171
+ required(
172
+ names.length > 0 &&
173
+ names.length <= 400 &&
174
+ names.every(
175
+ value =>
176
+ value.startsWith(prefix) &&
177
+ !/[\0\r\\]/.test(value) &&
178
+ !value.split("/").some(part => part === ".." || part === ".") &&
179
+ /^[a-zA-Z0-9_./-]+$/.test(value)
180
+ ),
181
+ "fixed browser archive member differs"
182
+ );
183
+ const target = join(root, name);
184
+ mkdirSync(target, { mode: 0o700 });
185
+ await step("/usr/bin/unzip", ["-q", archive, "-d", target]);
186
+ return join(target, prefix, name);
187
+ }