@codyswann/lisa 4.70.4 → 4.71.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.
Files changed (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -0,0 +1,264 @@
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
+ /**
5
+ * @file npm-update-recovery.mjs
6
+ * @description Durable public bytes survive runner loss without supplying authority.
7
+ * @module npm-updater
8
+ */
9
+ import { join } from "node:path";
10
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
11
+ import { readBytes, readJson, writeJson } from "./npm-update-process.mjs";
12
+ import { authorizeAllocation } from "./npm-update-leaf.mjs";
13
+ import {
14
+ sha256,
15
+ verifyDescriptorAttestation,
16
+ verifyCurrentProvider,
17
+ } from "./github-attestation-verifier.mjs";
18
+ import {
19
+ verifyRecoveryOrigin,
20
+ predictedCommit,
21
+ } from "./github-attestation-recovery.mjs";
22
+ import { persistCheckpoint } from "./npm-update-checkpoint.mjs";
23
+ import { required, validateProposal } from "./npm-update-contract.mjs";
24
+
25
+ import {
26
+ checkpointComments,
27
+ decodeCheckpoint,
28
+ } from "./npm-update-checkpoint.mjs";
29
+
30
+ /** A new issuer binds current authority to the immutable original artifact and destination. */
31
+ export function recoveryRecord(
32
+ proposal,
33
+ allocation,
34
+ preview,
35
+ policy,
36
+ automation,
37
+ issue,
38
+ snapshot
39
+ ) {
40
+ const d = preview.descriptor;
41
+ return {
42
+ version: 1,
43
+ purpose: "resume-identical-npm-proposal",
44
+ repository: policy.repository,
45
+ repositoryId: automation.repositoryId,
46
+ ownerId: automation.ownerId,
47
+ queue: d.queue,
48
+ workItem: d.workItem,
49
+ proposalKey: proposal.key,
50
+ bindingKey: d.proposalKey,
51
+ npmPolicySha256: proposal.policySha256,
52
+ automationPolicySha256: d.policySha256,
53
+ originDescriptorSha256: sha256(`${canonicalJson(d)}\n`),
54
+ originRunId: d.runId,
55
+ originRunAttempt: d.runAttempt,
56
+ commit: predictedCommit(d, Buffer.from(preview.message)),
57
+ parent: d.parent,
58
+ tree: d.tree,
59
+ messageSha256: d.messageSha256,
60
+ claimCommentId: allocation.claimCommentId,
61
+ claimSha256: allocation.claimSha256,
62
+ leafBodySha256: sha256(issue.body),
63
+ ...snapshot,
64
+ runId: process.env.GITHUB_RUN_ID,
65
+ runAttempt: process.env.GITHUB_RUN_ATTEMPT,
66
+ };
67
+ }
68
+
69
+ /** Complete untrusted chunks can restore missing transport metadata, never authority. */
70
+ export function recoverPartialCheckpoint(comments, digest) {
71
+ required(
72
+ Array.isArray(comments) && comments.length <= 2000,
73
+ "partial checkpoint inventory exceeds bound"
74
+ );
75
+ const bodies = comments.filter(comment =>
76
+ comment.body?.startsWith(`[lisa-npm-checkpoint] v1 ${digest} `)
77
+ );
78
+ const chunks = new Map();
79
+ let count;
80
+ for (const { body } of bodies) {
81
+ const match =
82
+ /^\[lisa-npm-checkpoint\] v1 ([a-f0-9]{64}) chunk (0|[1-9]\d*)\/([1-9]\d*) ([a-f0-9]{64})\n([A-Za-z0-9+/]+=*)$/.exec(
83
+ body
84
+ );
85
+ required(
86
+ match && Buffer.byteLength(body) <= 24_576 && match[1] === digest,
87
+ "partial checkpoint chunk differs"
88
+ );
89
+ count ??= Number(match[3]);
90
+ const index = Number(match[2]);
91
+ const bytes = Buffer.from(match[5], "base64");
92
+ required(
93
+ count <= 256 &&
94
+ Number(match[3]) === count &&
95
+ index < count &&
96
+ bytes.length <= 16_384 &&
97
+ bytes.toString("base64") === match[5] &&
98
+ sha256(bytes) === match[4],
99
+ "partial checkpoint bounds or digest differ"
100
+ );
101
+ required(
102
+ !chunks.has(index) || chunks.get(index).equals(bytes),
103
+ "partial checkpoint has differing duplicate chunk"
104
+ );
105
+ chunks.set(index, bytes);
106
+ }
107
+ required(
108
+ count > 0 && chunks.size === count,
109
+ "partial checkpoint has missing original material"
110
+ );
111
+ const bytes = Buffer.concat(
112
+ Array.from({ length: count }, (_, index) => chunks.get(index))
113
+ );
114
+ required(
115
+ bytes.length <= 4_194_304,
116
+ "partial checkpoint exceeds aggregate bound"
117
+ );
118
+ const payload = JSON.parse(
119
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes)
120
+ );
121
+ const complete = checkpointComments(payload, digest).at(-1);
122
+ return decodeCheckpoint([...comments, { body: complete }], digest);
123
+ }
124
+
125
+ /** Unsigned draft bytes aid retry; only the separately verified origin can authorize gates. */
126
+ export function locateCheckpoint(comments, proposal, policy) {
127
+ const digests = new Set(
128
+ comments.flatMap(comment => {
129
+ const match =
130
+ /^\[lisa-npm-checkpoint\] v1 ([a-f0-9]{64}) (?:chunk|complete)\b/.exec(
131
+ comment.body ?? ""
132
+ );
133
+ return match && match[1] !== proposal.key ? [match[1]] : [];
134
+ })
135
+ );
136
+ required(digests.size <= 1, "multiple origin checkpoints are preserved");
137
+ const digest = [...digests][0] ?? proposal.key;
138
+ const present = comments.filter(comment =>
139
+ comment.body?.startsWith(`[lisa-npm-checkpoint] v1 ${digest} `)
140
+ );
141
+ const complete = present.some(comment =>
142
+ comment.body.startsWith(`[lisa-npm-checkpoint] v1 ${digest} complete\n`)
143
+ );
144
+ const payload = complete
145
+ ? decodeCheckpoint(comments, digest)
146
+ : present.length
147
+ ? recoverPartialCheckpoint(comments, digest)
148
+ : undefined;
149
+ if (!payload) return undefined;
150
+ const restored = validateProposal(payload.proposal, policy);
151
+ required(
152
+ restored.selectionKey === proposal.selectionKey &&
153
+ restored.parent === proposal.parent,
154
+ "checkpoint selection or base differs"
155
+ );
156
+ const signed = digest !== proposal.key;
157
+ required(
158
+ signed
159
+ ? payload.preview?.descriptor && payload.bundle.length > 0
160
+ : payload.preview === null && payload.bundle === "",
161
+ "checkpoint origin role differs"
162
+ );
163
+ return { payload, digest, signed, complete };
164
+ }
165
+
166
+ export {
167
+ checkpointComments,
168
+ decodeCheckpoint,
169
+ persistCheckpoint,
170
+ } from "./npm-update-checkpoint.mjs";
171
+
172
+ /** Renewal verifies fresh authority against every immutable original identity field. */
173
+ function verifyRenewal({
174
+ api,
175
+ preview,
176
+ paths,
177
+ digest,
178
+ proposal,
179
+ issue,
180
+ policy,
181
+ }) {
182
+ const recovery = readJson(paths.recovery, 65_536);
183
+ verifyRecoveryOrigin(
184
+ api.policy,
185
+ preview.descriptor,
186
+ recovery,
187
+ digest,
188
+ paths,
189
+ {
190
+ npmPolicySha256: proposal.policySha256,
191
+ proposalKey: proposal.key,
192
+ leafBodySha256: sha256(issue.body),
193
+ commit: predictedCommit(preview.descriptor, Buffer.from(preview.message)),
194
+ maintainer: policy.maintainer,
195
+ recoverySha256: sha256(readBytes(paths.recovery, 65_536)),
196
+ }
197
+ );
198
+ }
199
+
200
+ /** Completion after official issuance is required before ordinary commit construction. */
201
+ export async function finalizeAuthorization({
202
+ api,
203
+ output,
204
+ proposal,
205
+ allocation,
206
+ preview,
207
+ policy,
208
+ config,
209
+ }) {
210
+ const paths = {
211
+ descriptor: join(output, "descriptor.json"),
212
+ bundle: join(output, "bundle.json"),
213
+ recovery: join(output, "recovery.json"),
214
+ recoveryBundle: join(output, "recovery-bundle.json"),
215
+ };
216
+ const digest = sha256(readBytes(paths.descriptor, 65_536));
217
+ const issue = await authorizeAllocation(
218
+ api,
219
+ proposal,
220
+ allocation,
221
+ policy,
222
+ config
223
+ );
224
+ const mode = readJson(join(output, "authorization.json")).mode;
225
+ if (mode === "recovery") {
226
+ verifyRenewal({ api, preview, paths, digest, proposal, issue, policy });
227
+ } else {
228
+ required(mode === "origin", "unknown authorization role");
229
+ verifyDescriptorAttestation(api.policy, preview.descriptor, paths, digest);
230
+ verifyCurrentProvider(api.policy, preview.descriptor);
231
+ await persistCheckpoint(
232
+ api,
233
+ allocation.number,
234
+ {
235
+ version: 1,
236
+ proposal,
237
+ allocation,
238
+ preview,
239
+ bundle: readBytes(paths.bundle, 1_048_576).toString("utf8"),
240
+ },
241
+ digest,
242
+ () => authorizeAllocation(api, proposal, allocation, policy, config)
243
+ );
244
+ }
245
+ const checkpoint = locateCheckpoint(
246
+ (await api.issue(allocation.number)).comments,
247
+ proposal,
248
+ policy
249
+ );
250
+ required(
251
+ checkpoint?.signed &&
252
+ checkpoint.complete === true &&
253
+ checkpoint.digest === digest &&
254
+ canonicalJson(checkpoint.payload.preview) === canonicalJson(preview) &&
255
+ canonicalJson(checkpoint.payload.proposal) === canonicalJson(proposal),
256
+ "durable original checkpoint differs"
257
+ );
258
+ writeJson(join(output, "checkpoint.json"), {
259
+ version: 1,
260
+ originDescriptorSha256: digest,
261
+ complete: true,
262
+ });
263
+ return { mode, complete: true };
264
+ }
@@ -0,0 +1,204 @@
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
+ /** Bounded archive bytes and the complete OCI graph are checked without host extraction. @module npm-updater */
5
+ import { createHash } from "node:crypto";
6
+ import { openSync, closeSync, fstatSync, constants, readSync } from "node:fs";
7
+ import { Transform, Writable, Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { createGunzip } from "node:zlib";
10
+ import { required } from "./npm-update-contract.mjs";
11
+ import { validateArchiveGraph } from "./npm-update-runtime-graph.mjs";
12
+ export { validateArchiveGraph } from "./npm-update-runtime-graph.mjs";
13
+ const MAXIMUM = 1_500_000_000;
14
+
15
+ /** Duplicate member names, alias records and unsigned trailing payloads are never extracted. */
16
+ function tarHeader(bytes) {
17
+ const number = field => {
18
+ required(/^[0-7]+$/.test(field), "invalid archive numeric field");
19
+ return parseInt(field, 8);
20
+ };
21
+ const text = field => {
22
+ const end = field.indexOf(0);
23
+ return field.subarray(0, end < 0 ? field.length : end).toString("ascii");
24
+ };
25
+ const checksum = number(text(bytes.subarray(148, 156)).trim());
26
+ let actual = 0;
27
+ for (let i = 0; i < 512; i++) actual += i >= 148 && i < 156 ? 32 : bytes[i];
28
+ required(
29
+ checksum === actual && bytes.subarray(345, 500).every(value => value === 0),
30
+ "archive header checksum or prefix differs"
31
+ );
32
+ const name = text(bytes.subarray(0, 100));
33
+ const size = number(text(bytes.subarray(124, 136)).trim());
34
+ const type = bytes[156];
35
+ const directory = type === 53 && ["blobs/", "blobs/sha256/"].includes(name);
36
+ required(
37
+ (directory && size === 0) ||
38
+ ([0, 48].includes(type) &&
39
+ /^(?:index\.json|manifest\.json|oci-layout|blobs\/sha256\/[a-f0-9]{64})$/.test(
40
+ name
41
+ )),
42
+ "archive contains foreign member or alias"
43
+ );
44
+ required(
45
+ bytes.subarray(157, 257).every(value => value === 0) && size <= MAXIMUM,
46
+ "archive alias or member size differs"
47
+ );
48
+ return {
49
+ name,
50
+ size,
51
+ directory,
52
+ left: size,
53
+ padding: (512 - (size % 512)) % 512,
54
+ hash: createHash("sha256"),
55
+ chunks: [],
56
+ };
57
+ }
58
+
59
+ function completeMember(state) {
60
+ const member = state.member;
61
+ const digest = member.hash.digest("hex");
62
+ if (!member.directory && member.name.startsWith("blobs/sha256/"))
63
+ required(member.name.slice(13) === digest, "archive blob digest differs");
64
+ if (!member.directory)
65
+ state.members.set(member.name, {
66
+ size: member.size,
67
+ sha256: digest,
68
+ bytes: member.size <= 1_048_576 ? Buffer.concat(member.chunks) : null,
69
+ });
70
+ state.member = null;
71
+ }
72
+
73
+ /** Streaming state keeps large layer bodies out of memory and checks padding and terminators. */
74
+ function consumeTar(state, chunk) {
75
+ state.total += chunk.length;
76
+ required(state.total <= MAXIMUM, "archive expanded size exceeds bound");
77
+ state.buffer = Buffer.concat([state.buffer, chunk]);
78
+ while (state.buffer.length) {
79
+ if (state.end) {
80
+ required(
81
+ state.buffer.every(value => value === 0),
82
+ "archive trailing payload differs"
83
+ );
84
+ state.zeros += state.buffer.length;
85
+ state.buffer = Buffer.alloc(0);
86
+ return;
87
+ }
88
+ if (!state.member) {
89
+ if (state.buffer.length < 512) return;
90
+ const header = state.buffer.subarray(0, 512);
91
+ state.buffer = state.buffer.subarray(512);
92
+ if (header.every(value => value === 0)) {
93
+ state.end = true;
94
+ state.zeros = 512;
95
+ continue;
96
+ }
97
+ state.member = tarHeader(header);
98
+ required(
99
+ ++state.count <= 256 && !state.names.has(state.member.name),
100
+ "archive duplicate or excessive members"
101
+ );
102
+ state.names.add(state.member.name);
103
+ }
104
+ const member = state.member;
105
+ if (member.left) {
106
+ const length = Math.min(member.left, state.buffer.length);
107
+ const bytes = state.buffer.subarray(0, length);
108
+ member.hash.update(bytes);
109
+ if (member.size <= 1_048_576) member.chunks.push(Buffer.from(bytes));
110
+ member.left -= length;
111
+ state.buffer = state.buffer.subarray(length);
112
+ if (member.left) return;
113
+ }
114
+ if (state.buffer.length < member.padding) return;
115
+ required(
116
+ state.buffer.subarray(0, member.padding).every(value => value === 0),
117
+ "archive padding differs"
118
+ );
119
+ state.buffer = state.buffer.subarray(member.padding);
120
+ completeMember(state);
121
+ }
122
+ }
123
+
124
+ /** One caller owns descriptor closure even when pipeline rejects a malformed archive. */
125
+ async function* archiveChunks(fd) {
126
+ const buffer = Buffer.alloc(65_536);
127
+ let length;
128
+ while ((length = readSync(fd, buffer, 0, buffer.length, null)) > 0)
129
+ yield Buffer.from(buffer.subarray(0, length));
130
+ }
131
+
132
+ /** Whole compressed bytes are bound before this data can become a daemon-load input. */
133
+ export async function inspectRuntimeArchive(file, expected) {
134
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
135
+ try {
136
+ const stat = fstatSync(fd);
137
+ required(
138
+ stat.isFile() &&
139
+ stat.uid === process.getuid() &&
140
+ stat.nlink === 1 &&
141
+ (stat.mode & 0o077) === 0 &&
142
+ stat.size === expected.bytes &&
143
+ stat.size > 0 &&
144
+ stat.size < 1_000_000_000,
145
+ "runtime archive is not bounded private data"
146
+ );
147
+ const hash = createHash("sha256");
148
+ const state = {
149
+ buffer: Buffer.alloc(0),
150
+ member: null,
151
+ members: new Map(),
152
+ names: new Set(),
153
+ total: 0,
154
+ count: 0,
155
+ end: false,
156
+ zeros: 0,
157
+ };
158
+ let compressed = 0;
159
+ const guard = new Transform({
160
+ transform(chunk, encoding, callback) {
161
+ try {
162
+ compressed += chunk.length;
163
+ required(compressed <= stat.size, "runtime archive grew");
164
+ hash.update(chunk);
165
+ callback(null, chunk);
166
+ } catch (error) {
167
+ callback(error);
168
+ }
169
+ },
170
+ });
171
+ const sink = new Writable({
172
+ write(chunk, encoding, callback) {
173
+ try {
174
+ consumeTar(state, chunk);
175
+ callback();
176
+ } catch (error) {
177
+ callback(error);
178
+ }
179
+ },
180
+ });
181
+ await pipeline(
182
+ Readable.from(archiveChunks(fd)),
183
+ guard,
184
+ createGunzip(),
185
+ sink
186
+ );
187
+ required(
188
+ compressed === stat.size &&
189
+ hash.digest("hex") === expected.sha256 &&
190
+ state.total === expected.uncompressedBytes,
191
+ "runtime archive complete byte identity differs"
192
+ );
193
+ required(
194
+ state.end &&
195
+ state.zeros >= 1024 &&
196
+ !state.member &&
197
+ state.buffer.length === 0,
198
+ "runtime archive is truncated"
199
+ );
200
+ return validateArchiveGraph(state.members, expected);
201
+ } finally {
202
+ closeSync(fd);
203
+ }
204
+ }
@@ -0,0 +1,185 @@
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 complete tagless OCI closure binds distinct index, platform manifest and config identities. @module npm-updater */
5
+ import { required } from "./npm-update-contract.mjs";
6
+ const INDEX = "application/vnd.oci.image.index.v1+json";
7
+ const MANIFEST = "application/vnd.oci.image.manifest.v1+json";
8
+ const CONFIG = "application/vnd.oci.image.config.v1+json";
9
+ const DIGEST = /^sha256:[a-f0-9]{64}$/;
10
+ const MAXIMUM = 1_500_000_000;
11
+
12
+ function jsonMember(members, path) {
13
+ const member = members.get(path);
14
+ required(
15
+ member?.bytes && member.size <= 1_048_576,
16
+ "archive metadata is absent or unbounded"
17
+ );
18
+ return JSON.parse(
19
+ new TextDecoder("utf8", { fatal: true }).decode(member.bytes)
20
+ );
21
+ }
22
+
23
+ /** Every referenced descriptor is bound to the actual member digest and byte length. */
24
+ function descriptorMember(members, descriptor, seen) {
25
+ required(
26
+ descriptor &&
27
+ DIGEST.test(descriptor.digest) &&
28
+ Number.isSafeInteger(descriptor.size) &&
29
+ descriptor.size > 0 &&
30
+ descriptor.size <= MAXIMUM,
31
+ "invalid archive OCI descriptor"
32
+ );
33
+ const path = `blobs/sha256/${descriptor.digest.slice(7)}`;
34
+ const member = members.get(path);
35
+ required(
36
+ member?.sha256 === descriptor.digest.slice(7) &&
37
+ member.size === descriptor.size,
38
+ "archive OCI descriptor bytes differ"
39
+ );
40
+ required(
41
+ !descriptor.annotations ||
42
+ Object.keys(descriptor.annotations).every(key =>
43
+ [
44
+ "vnd.docker.reference.digest",
45
+ "vnd.docker.reference.type",
46
+ "in-toto.io/predicate-type",
47
+ ].includes(key)
48
+ ),
49
+ "archive repository tag annotation is forbidden"
50
+ );
51
+ seen.add(path);
52
+ return path;
53
+ }
54
+
55
+ function manifestMember(members, descriptor, seen) {
56
+ required(
57
+ descriptor.mediaType === MANIFEST,
58
+ "unsupported archive manifest type"
59
+ );
60
+ const manifest = jsonMember(
61
+ members,
62
+ descriptorMember(members, descriptor, seen)
63
+ );
64
+ required(
65
+ manifest.schemaVersion === 2 &&
66
+ manifest.mediaType === MANIFEST &&
67
+ manifest.config?.mediaType === CONFIG &&
68
+ Array.isArray(manifest.layers) &&
69
+ manifest.layers.length <= 128,
70
+ "invalid archive OCI manifest"
71
+ );
72
+ descriptorMember(members, manifest.config, seen);
73
+ for (const layer of manifest.layers) {
74
+ required(
75
+ [
76
+ "application/vnd.oci.image.layer.v1.tar+gzip",
77
+ "application/vnd.oci.image.layer.v1.tar",
78
+ "application/vnd.in-toto+json",
79
+ ].includes(layer.mediaType),
80
+ "unsupported archive layer type"
81
+ );
82
+ descriptorMember(members, layer, seen);
83
+ }
84
+ return manifest;
85
+ }
86
+
87
+ /** Docker's index ID and config ID are distinct; both and every graph member must match. */
88
+ export function validateArchiveGraph(members, expected) {
89
+ required(
90
+ DIGEST.test(expected.image) &&
91
+ DIGEST.test(expected.config) &&
92
+ ["linux/arm64", "linux/amd64"].includes(expected.platform),
93
+ "invalid archive expected identity"
94
+ );
95
+ required(
96
+ jsonMember(members, "oci-layout").imageLayoutVersion === "1.0.0",
97
+ "archive OCI layout differs"
98
+ );
99
+ const top = jsonMember(members, "index.json");
100
+ required(
101
+ top.schemaVersion === 2 &&
102
+ top.mediaType === INDEX &&
103
+ top.manifests?.length === 1 &&
104
+ top.manifests[0].digest === expected.image &&
105
+ top.manifests[0].mediaType === INDEX,
106
+ "archive index identity differs"
107
+ );
108
+ const seen = new Set();
109
+ const index = jsonMember(
110
+ members,
111
+ descriptorMember(members, top.manifests[0], seen)
112
+ );
113
+ required(
114
+ index.schemaVersion === 2 &&
115
+ index.mediaType === INDEX &&
116
+ Array.isArray(index.manifests) &&
117
+ index.manifests.length > 0 &&
118
+ index.manifests.length <= 2,
119
+ "archive image index differs"
120
+ );
121
+ const selected = index.manifests.filter(
122
+ value =>
123
+ `${value.platform?.os}/${value.platform?.architecture}` ===
124
+ expected.platform
125
+ );
126
+ required(selected.length === 1, "archive platform is absent or ambiguous");
127
+ let manifest;
128
+ for (const entry of index.manifests) {
129
+ const current = manifestMember(members, entry, seen);
130
+ if (entry === selected[0]) manifest = current;
131
+ else
132
+ required(
133
+ entry.platform?.os === "unknown" &&
134
+ entry.platform?.architecture === "unknown" &&
135
+ entry.annotations?.["vnd.docker.reference.type"] ===
136
+ "attestation-manifest" &&
137
+ entry.annotations?.["vnd.docker.reference.digest"] ===
138
+ selected[0].digest,
139
+ "archive contains a foreign platform image"
140
+ );
141
+ }
142
+ required(
143
+ manifest.config.digest === expected.config,
144
+ "archive config identity differs"
145
+ );
146
+ const config = jsonMember(
147
+ members,
148
+ `blobs/sha256/${expected.config.slice(7)}`
149
+ );
150
+ required(
151
+ `${config.os}/${config.architecture}` === expected.platform,
152
+ "archive config platform differs"
153
+ );
154
+ legacyClosure(members, expected, manifest, seen);
155
+ return {
156
+ image: expected.image,
157
+ config: expected.config,
158
+ platform: expected.platform,
159
+ };
160
+ }
161
+
162
+ /** A tagless legacy manifest must describe exactly the same referenced OCI image. */
163
+ function legacyClosure(members, expected, manifest, seen) {
164
+ const legacy = jsonMember(members, "manifest.json");
165
+ required(
166
+ Array.isArray(legacy) && legacy.length === 1 && legacy[0].RepoTags === null,
167
+ "archive repository tags are forbidden"
168
+ );
169
+ required(
170
+ legacy[0].Config === `blobs/sha256/${expected.config.slice(7)}` &&
171
+ JSON.stringify(legacy[0].Layers) ===
172
+ JSON.stringify(
173
+ manifest.layers.map(layer => `blobs/sha256/${layer.digest.slice(7)}`)
174
+ ),
175
+ "archive legacy config/layer graph differs"
176
+ );
177
+ required(
178
+ [...members.keys()].every(
179
+ path =>
180
+ ["index.json", "manifest.json", "oci-layout"].includes(path) ||
181
+ seen.has(path)
182
+ ),
183
+ "archive contains unreferenced blobs"
184
+ );
185
+ }