@codyswann/lisa 4.74.7 → 4.74.9
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/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +4 -2
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint-codec.mjs +141 -0
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +162 -116
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +1 -0
- package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +9 -60
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +18 -6
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +20 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/rails/copy-overwrite/lefthook.yml +2 -2
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -45,7 +45,7 @@ export function completeCheckpoints(comments) {
|
|
|
45
45
|
);
|
|
46
46
|
const digests = new Set();
|
|
47
47
|
for (const comment of comments) {
|
|
48
|
-
const match = /^\[lisa-npm-checkpoint\]
|
|
48
|
+
const match = /^\[lisa-npm-checkpoint\] \S+ ([a-f0-9]{64}) complete\n/.exec(
|
|
49
49
|
comment.body ?? ""
|
|
50
50
|
);
|
|
51
51
|
if (match) digests.add(match[1]);
|
|
@@ -113,7 +113,9 @@ export function assertOriginTransport(
|
|
|
113
113
|
required(chronology, "invalid original transport chronology");
|
|
114
114
|
const { start, end } = chronology;
|
|
115
115
|
const selected = comments.filter(comment =>
|
|
116
|
-
|
|
116
|
+
new RegExp(`^\\[lisa-npm-checkpoint\\] v[12] ${digest} `).test(
|
|
117
|
+
comment.body ?? ""
|
|
118
|
+
)
|
|
117
119
|
);
|
|
118
120
|
required(selected.length > 0, "original transport is missing");
|
|
119
121
|
for (const comment of selected) {
|
|
@@ -0,0 +1,141 @@
|
|
|
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-checkpoint-codec.mjs
|
|
6
|
+
* @description Reduce serial public writes without extending signed authorization freshness.
|
|
7
|
+
* @module npm-updater
|
|
8
|
+
*/
|
|
9
|
+
import { TextDecoder } from "node:util";
|
|
10
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
11
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
12
|
+
import { keys, required } from "./npm-update-contract.mjs";
|
|
13
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
14
|
+
|
|
15
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
16
|
+
|
|
17
|
+
export const PAYLOAD_MAXIMUM = 4_194_304;
|
|
18
|
+
export const TRANSPORT_MAXIMUM = 4_194_304;
|
|
19
|
+
|
|
20
|
+
/** Closed public payloads contain no bearer token or executable extension field. */
|
|
21
|
+
export function checkpointPayloadBytes(payload) {
|
|
22
|
+
keys(payload, ["version", "proposal", "allocation", "preview", "bundle"]);
|
|
23
|
+
required(
|
|
24
|
+
payload.version === 1 && typeof payload.bundle === "string",
|
|
25
|
+
"invalid public checkpoint payload"
|
|
26
|
+
);
|
|
27
|
+
const bytes = Buffer.from(`${canonicalJson(payload)}\n`);
|
|
28
|
+
required(bytes.length <= PAYLOAD_MAXIMUM, "checkpoint payload exceeds bound");
|
|
29
|
+
required(
|
|
30
|
+
!/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/.test(
|
|
31
|
+
payload.bundle
|
|
32
|
+
),
|
|
33
|
+
"bearer JWT cannot be retained"
|
|
34
|
+
);
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The fixed codec changes transport only; signed subjects and canonical payloads stay intact. */
|
|
39
|
+
export function encodeCheckpointBytes(bytes, version) {
|
|
40
|
+
required(
|
|
41
|
+
version === 1 || version === 2,
|
|
42
|
+
"unknown checkpoint transport version"
|
|
43
|
+
);
|
|
44
|
+
const encoded = version === 1 ? bytes : deflateSync(bytes, { level: 6 });
|
|
45
|
+
required(
|
|
46
|
+
encoded.length <= TRANSPORT_MAXIMUM,
|
|
47
|
+
"checkpoint transport exceeds bound"
|
|
48
|
+
);
|
|
49
|
+
return encoded;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An independent output ceiling and exact stream consumption refuse bombs and appended streams. */
|
|
53
|
+
export function decodeCheckpointBytes(transport, version) {
|
|
54
|
+
required(
|
|
55
|
+
(version === 1 || version === 2) &&
|
|
56
|
+
transport.length > 0 &&
|
|
57
|
+
transport.length <= TRANSPORT_MAXIMUM,
|
|
58
|
+
"checkpoint transport bounds or version differ"
|
|
59
|
+
);
|
|
60
|
+
const inflated =
|
|
61
|
+
version === 2
|
|
62
|
+
? inflateSync(transport, { info: true, maxOutputLength: PAYLOAD_MAXIMUM })
|
|
63
|
+
: undefined;
|
|
64
|
+
required(
|
|
65
|
+
!inflated || inflated.engine.bytesWritten === transport.length,
|
|
66
|
+
"checkpoint compressed stream has trailing bytes"
|
|
67
|
+
);
|
|
68
|
+
const bytes = inflated ? inflated.buffer : transport;
|
|
69
|
+
required(
|
|
70
|
+
bytes.length <= PAYLOAD_MAXIMUM,
|
|
71
|
+
"checkpoint decoded payload exceeds bound"
|
|
72
|
+
);
|
|
73
|
+
const payload = JSON.parse(
|
|
74
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes)
|
|
75
|
+
);
|
|
76
|
+
required(
|
|
77
|
+
checkpointPayloadBytes(payload).equals(bytes),
|
|
78
|
+
"checkpoint is noncanonical or duplicate-keyed"
|
|
79
|
+
);
|
|
80
|
+
return { payload, bytes };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Bind decoded bytes independently from the compressed stream and each numbered comment. */
|
|
84
|
+
export function manifestFor(bytes, transport, chunks, digest, version) {
|
|
85
|
+
return {
|
|
86
|
+
version,
|
|
87
|
+
...(version === 2
|
|
88
|
+
? {
|
|
89
|
+
encoding: "deflate",
|
|
90
|
+
transportSha256: sha256(transport),
|
|
91
|
+
transportBytes: transport.length,
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
94
|
+
originDescriptorSha256: digest,
|
|
95
|
+
payloadSha256: sha256(bytes),
|
|
96
|
+
bytes: bytes.length,
|
|
97
|
+
chunks: chunks.map((chunk, index) => ({ index, sha256: sha256(chunk) })),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The unique canonical completion independently declares encoded and decoded aggregate ceilings. */
|
|
102
|
+
export function completionManifest(body, digest, version) {
|
|
103
|
+
const text = body.split("\n").slice(1).join("\n");
|
|
104
|
+
const manifest = JSON.parse(text);
|
|
105
|
+
keys(manifest, [
|
|
106
|
+
"version",
|
|
107
|
+
"originDescriptorSha256",
|
|
108
|
+
"payloadSha256",
|
|
109
|
+
"bytes",
|
|
110
|
+
"chunks",
|
|
111
|
+
...(version === 2 ? ["encoding", "transportSha256", "transportBytes"] : []),
|
|
112
|
+
]);
|
|
113
|
+
required(
|
|
114
|
+
canonicalJson(manifest) === text &&
|
|
115
|
+
manifest.version === version &&
|
|
116
|
+
manifest.originDescriptorSha256 === digest &&
|
|
117
|
+
typeof manifest.payloadSha256 === "string" &&
|
|
118
|
+
HEX.test(manifest.payloadSha256),
|
|
119
|
+
"checkpoint manifest differs"
|
|
120
|
+
);
|
|
121
|
+
required(
|
|
122
|
+
Number.isSafeInteger(manifest.bytes) &&
|
|
123
|
+
manifest.bytes > 0 &&
|
|
124
|
+
manifest.bytes <= PAYLOAD_MAXIMUM &&
|
|
125
|
+
Array.isArray(manifest.chunks) &&
|
|
126
|
+
manifest.chunks.length > 0 &&
|
|
127
|
+
manifest.chunks.length <= 256,
|
|
128
|
+
"checkpoint manifest bounds differ"
|
|
129
|
+
);
|
|
130
|
+
required(
|
|
131
|
+
version === 1 ||
|
|
132
|
+
(manifest.encoding === "deflate" &&
|
|
133
|
+
typeof manifest.transportSha256 === "string" &&
|
|
134
|
+
HEX.test(manifest.transportSha256) &&
|
|
135
|
+
Number.isSafeInteger(manifest.transportBytes) &&
|
|
136
|
+
manifest.transportBytes > 0 &&
|
|
137
|
+
manifest.transportBytes <= TRANSPORT_MAXIMUM),
|
|
138
|
+
"checkpoint encoded manifest differs"
|
|
139
|
+
);
|
|
140
|
+
return manifest;
|
|
141
|
+
}
|
|
@@ -1,75 +1,43 @@
|
|
|
1
1
|
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
2
|
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @file npm-update-checkpoint.mjs
|
|
6
|
+
* @description Bounded compressed public storage is data only, never provenance authority.
|
|
7
|
+
* @module npm-updater
|
|
8
|
+
*/
|
|
6
9
|
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
7
10
|
import { keys, required } from "./npm-update-contract.mjs";
|
|
8
11
|
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
12
|
+
import {
|
|
13
|
+
checkpointPayloadBytes,
|
|
14
|
+
encodeCheckpointBytes,
|
|
15
|
+
decodeCheckpointBytes,
|
|
16
|
+
manifestFor,
|
|
17
|
+
completionManifest,
|
|
18
|
+
} from "./npm-update-checkpoint-codec.mjs";
|
|
9
19
|
|
|
10
|
-
const MAXIMUM = 4_194_304;
|
|
11
20
|
const CHUNK = 16_384;
|
|
12
21
|
const PREFIX = "[lisa-npm-checkpoint]";
|
|
13
22
|
const HEX = /^[a-f0-9]{64}$/;
|
|
14
23
|
|
|
15
|
-
/**
|
|
16
|
-
function
|
|
17
|
-
keys(payload, ["version", "proposal", "allocation", "preview", "bundle"]);
|
|
18
|
-
required(
|
|
19
|
-
payload.version === 1 && typeof payload.bundle === "string",
|
|
20
|
-
"invalid public checkpoint payload"
|
|
21
|
-
);
|
|
22
|
-
const bytes = Buffer.from(`${canonicalJson(payload)}\n`);
|
|
23
|
-
required(bytes.length <= MAXIMUM, "checkpoint payload exceeds bound");
|
|
24
|
-
required(
|
|
25
|
-
!/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/.test(
|
|
26
|
-
payload.bundle
|
|
27
|
-
),
|
|
28
|
-
"bearer JWT cannot be retained"
|
|
29
|
-
);
|
|
30
|
-
return bytes;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Numbered digest-checked chunks precede one exact completion manifest. */
|
|
34
|
-
export function checkpointComments(payload, originDigest) {
|
|
24
|
+
/** Recognize closed wire versions, rejecting mixed or unknown formats for the same digest. */
|
|
25
|
+
function relevant(comments, digest) {
|
|
35
26
|
required(
|
|
36
|
-
typeof
|
|
37
|
-
"invalid checkpoint
|
|
38
|
-
);
|
|
39
|
-
const bytes = payloadBytes(payload);
|
|
40
|
-
const count = Math.ceil(bytes.length / CHUNK);
|
|
41
|
-
required(count > 0 && count <= 256, "checkpoint chunk count exceeds bound");
|
|
42
|
-
const chunks = Array.from({ length: count }, (_, index) =>
|
|
43
|
-
bytes.subarray(index * CHUNK, (index + 1) * CHUNK)
|
|
27
|
+
typeof digest === "string" && HEX.test(digest),
|
|
28
|
+
"invalid checkpoint digest"
|
|
44
29
|
);
|
|
45
|
-
const manifest = {
|
|
46
|
-
version: 1,
|
|
47
|
-
originDescriptorSha256: originDigest,
|
|
48
|
-
payloadSha256: sha256(bytes),
|
|
49
|
-
bytes: bytes.length,
|
|
50
|
-
chunks: chunks.map((chunk, index) => ({ index, sha256: sha256(chunk) })),
|
|
51
|
-
};
|
|
52
|
-
return [
|
|
53
|
-
...chunks.map(
|
|
54
|
-
(chunk, index) =>
|
|
55
|
-
`${PREFIX} v1 ${originDigest} chunk ${index}/${count} ${sha256(chunk)}\n${chunk.toString("base64")}`
|
|
56
|
-
),
|
|
57
|
-
`${PREFIX} v1 ${originDigest} complete\n${canonicalJson(manifest)}`,
|
|
58
|
-
];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A strict marker locates data only; signatures are still required separately. */
|
|
62
|
-
function relevant(comments, digest) {
|
|
63
30
|
required(
|
|
64
31
|
Array.isArray(comments) && comments.length <= 2000,
|
|
65
32
|
"checkpoint comment inventory exceeds bound"
|
|
66
33
|
);
|
|
67
|
-
|
|
68
|
-
.filter(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
34
|
+
const bodies = comments
|
|
35
|
+
.filter(comment => {
|
|
36
|
+
const match = /^\[lisa-npm-checkpoint\] (\S+) ([a-f0-9]{64})(?: |$)/.exec(
|
|
37
|
+
comment.body ?? ""
|
|
38
|
+
);
|
|
39
|
+
return match?.[2] === digest;
|
|
40
|
+
})
|
|
73
41
|
.map(comment => {
|
|
74
42
|
required(
|
|
75
43
|
Buffer.byteLength(comment.body) <= 24_576,
|
|
@@ -77,59 +45,62 @@ function relevant(comments, digest) {
|
|
|
77
45
|
);
|
|
78
46
|
return comment.body;
|
|
79
47
|
});
|
|
48
|
+
const versions = new Set(bodies.map(body => body.split(" ")[1]));
|
|
49
|
+
required(
|
|
50
|
+
versions.size <= 1 &&
|
|
51
|
+
[...versions].every(version => version === "v1" || version === "v2"),
|
|
52
|
+
"checkpoint transport version is unknown or mixed"
|
|
53
|
+
);
|
|
54
|
+
return bodies;
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/**
|
|
83
|
-
function
|
|
84
|
-
const text = body.split("\n").slice(1).join("\n");
|
|
85
|
-
const manifest = JSON.parse(text);
|
|
86
|
-
keys(manifest, [
|
|
87
|
-
"version",
|
|
88
|
-
"originDescriptorSha256",
|
|
89
|
-
"payloadSha256",
|
|
90
|
-
"bytes",
|
|
91
|
-
"chunks",
|
|
92
|
-
]);
|
|
57
|
+
/** New writes compress; an explicitly selected legacy retry preserves its original wire bytes. */
|
|
58
|
+
export function checkpointComments(payload, originDigest, version = 2) {
|
|
93
59
|
required(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
manifest.originDescriptorSha256 === digest &&
|
|
97
|
-
HEX.test(manifest.payloadSha256),
|
|
98
|
-
"checkpoint manifest differs"
|
|
60
|
+
typeof originDigest === "string" && HEX.test(originDigest),
|
|
61
|
+
"invalid checkpoint origin"
|
|
99
62
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
manifest.chunks.length <= 256,
|
|
107
|
-
"checkpoint manifest bounds differ"
|
|
63
|
+
const bytes = checkpointPayloadBytes(payload);
|
|
64
|
+
const transport = encodeCheckpointBytes(bytes, version);
|
|
65
|
+
const count = Math.ceil(transport.length / CHUNK);
|
|
66
|
+
required(count > 0 && count <= 256, "checkpoint chunk count exceeds bound");
|
|
67
|
+
const chunks = Array.from({ length: count }, (_, index) =>
|
|
68
|
+
transport.subarray(index * CHUNK, (index + 1) * CHUNK)
|
|
108
69
|
);
|
|
109
|
-
return
|
|
70
|
+
return [
|
|
71
|
+
...chunks.map(
|
|
72
|
+
(chunk, index) =>
|
|
73
|
+
`${PREFIX} v${version} ${originDigest} chunk ${index}/${count} ${sha256(chunk)}\n${chunk.toString("base64")}`
|
|
74
|
+
),
|
|
75
|
+
`${PREFIX} v${version} ${originDigest} complete\n${canonicalJson(manifestFor(bytes, transport, chunks, originDigest, version))}`,
|
|
76
|
+
];
|
|
110
77
|
}
|
|
111
78
|
|
|
112
|
-
/** Each
|
|
113
|
-
function decodedChunks(bodies, digest,
|
|
79
|
+
/** Each chunk is canonical, bounded and byte-bound before any decompression. */
|
|
80
|
+
function decodedChunks(bodies, digest, version, expectedCount) {
|
|
114
81
|
const chunks = new Map();
|
|
82
|
+
let count = expectedCount;
|
|
115
83
|
for (const body of bodies) {
|
|
116
84
|
const match =
|
|
117
|
-
/^\[lisa-npm-checkpoint\]
|
|
85
|
+
/^\[lisa-npm-checkpoint\] v([12]) ([a-f0-9]{64}) chunk (0|[1-9]\d*)\/([1-9]\d*) ([a-f0-9]{64})\n([A-Za-z0-9+/]+=*)$/.exec(
|
|
118
86
|
body
|
|
119
87
|
);
|
|
120
88
|
required(
|
|
121
|
-
match &&
|
|
122
|
-
match[1] === digest &&
|
|
123
|
-
Number(match[3]) === manifest.chunks.length,
|
|
89
|
+
match && Number(match[1]) === version && match[2] === digest,
|
|
124
90
|
"checkpoint chunk shape differs"
|
|
125
91
|
);
|
|
126
|
-
|
|
127
|
-
const
|
|
92
|
+
count ??= Number(match[4]);
|
|
93
|
+
const index = Number(match[3]);
|
|
94
|
+
const bytes = Buffer.from(match[6], "base64");
|
|
128
95
|
required(
|
|
129
|
-
|
|
96
|
+
count > 0 &&
|
|
97
|
+
count <= 256 &&
|
|
98
|
+
Number(match[4]) === count &&
|
|
99
|
+
index < count &&
|
|
100
|
+
bytes.length > 0 &&
|
|
130
101
|
bytes.length <= CHUNK &&
|
|
131
|
-
bytes.toString("base64") === match[
|
|
132
|
-
sha256(bytes) === match[
|
|
102
|
+
bytes.toString("base64") === match[6] &&
|
|
103
|
+
sha256(bytes) === match[5],
|
|
133
104
|
"checkpoint chunk bytes differ"
|
|
134
105
|
);
|
|
135
106
|
required(
|
|
@@ -138,52 +109,108 @@ function decodedChunks(bodies, digest, manifest) {
|
|
|
138
109
|
);
|
|
139
110
|
chunks.set(index, bytes);
|
|
140
111
|
}
|
|
141
|
-
|
|
112
|
+
required(
|
|
113
|
+
count > 0 && chunks.size === count,
|
|
114
|
+
"checkpoint missing or reordered chunk"
|
|
115
|
+
);
|
|
116
|
+
return Array.from({ length: count }, (_, index) => chunks.get(index));
|
|
142
117
|
}
|
|
143
118
|
|
|
144
|
-
/** Decode bounded canonical public storage;
|
|
119
|
+
/** Decode bounded canonical public storage; signatures are always required separately. */
|
|
145
120
|
export function decodeCheckpoint(comments, digest) {
|
|
146
|
-
required(
|
|
147
|
-
typeof digest === "string" && HEX.test(digest),
|
|
148
|
-
"invalid checkpoint digest"
|
|
149
|
-
);
|
|
150
121
|
const bodies = relevant(comments, digest);
|
|
151
122
|
if (!bodies.length) return undefined;
|
|
123
|
+
const version = Number(bodies[0].split(" ")[1].slice(1));
|
|
152
124
|
const complete = bodies.filter(body =>
|
|
153
|
-
body.startsWith(`${PREFIX}
|
|
125
|
+
body.startsWith(`${PREFIX} v${version} ${digest} complete\n`)
|
|
154
126
|
);
|
|
155
127
|
required(complete.length === 1, "checkpoint incomplete or ambiguous");
|
|
156
|
-
const manifest = completionManifest(complete[0], digest);
|
|
128
|
+
const manifest = completionManifest(complete[0], digest, version);
|
|
157
129
|
const chunks = decodedChunks(
|
|
158
130
|
bodies.filter(body => body !== complete[0]),
|
|
159
131
|
digest,
|
|
160
|
-
|
|
132
|
+
version,
|
|
133
|
+
manifest.chunks.length
|
|
161
134
|
);
|
|
162
|
-
|
|
135
|
+
manifest.chunks.forEach((entry, index) => {
|
|
163
136
|
keys(entry, ["index", "sha256"]);
|
|
164
|
-
const chunk = chunks.get(index);
|
|
165
137
|
required(
|
|
166
|
-
entry.index === index &&
|
|
138
|
+
entry.index === index && sha256(chunks[index]) === entry.sha256,
|
|
167
139
|
"checkpoint missing or reordered chunk"
|
|
168
140
|
);
|
|
169
|
-
return chunk;
|
|
170
141
|
});
|
|
171
|
-
const
|
|
142
|
+
const transport = Buffer.concat(chunks);
|
|
143
|
+
required(
|
|
144
|
+
version === 1 ||
|
|
145
|
+
(transport.length === manifest.transportBytes &&
|
|
146
|
+
sha256(transport) === manifest.transportSha256),
|
|
147
|
+
"checkpoint encoded aggregate differs"
|
|
148
|
+
);
|
|
149
|
+
const { payload, bytes } = decodeCheckpointBytes(transport, version);
|
|
172
150
|
required(
|
|
173
151
|
bytes.length === manifest.bytes && sha256(bytes) === manifest.payloadSha256,
|
|
174
152
|
"checkpoint aggregate differs"
|
|
175
153
|
);
|
|
176
|
-
|
|
177
|
-
|
|
154
|
+
return payload;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Complete chunk material retains its observed encoding instead of depending on recompression equality. */
|
|
158
|
+
function partialCompletion(comments, digest) {
|
|
159
|
+
const bodies = relevant(comments, digest);
|
|
160
|
+
required(
|
|
161
|
+
bodies.length > 0,
|
|
162
|
+
"partial checkpoint has missing original material"
|
|
178
163
|
);
|
|
164
|
+
const version = Number(bodies[0].split(" ")[1].slice(1));
|
|
165
|
+
const chunks = decodedChunks(bodies, digest, version);
|
|
166
|
+
const transport = Buffer.concat(chunks);
|
|
167
|
+
const { payload, bytes } = decodeCheckpointBytes(transport, version);
|
|
168
|
+
const completion = `${PREFIX} v${version} ${digest} complete\n${canonicalJson(manifestFor(bytes, transport, chunks, digest, version))}`;
|
|
169
|
+
return { payload, completion };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Missing completion is recoverable data, never a successful durable authorization. */
|
|
173
|
+
export function recoverPartialCheckpoint(comments, digest) {
|
|
174
|
+
const recovered = partialCompletion(comments, digest);
|
|
175
|
+
return decodeCheckpoint(
|
|
176
|
+
[...comments, { body: recovered.completion }],
|
|
177
|
+
digest
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Exact retries preserve observed versions; alternate valid compression is retained when all chunks exist. */
|
|
182
|
+
function writeBodies(payload, digest, existing) {
|
|
183
|
+
const observed = relevant(existing, digest);
|
|
184
|
+
if (!observed.length) return checkpointComments(payload, digest);
|
|
185
|
+
const version = Number(observed[0].split(" ")[1].slice(1));
|
|
186
|
+
const complete = observed.some(body =>
|
|
187
|
+
body.startsWith(`${PREFIX} v${version} ${digest} complete\n`)
|
|
188
|
+
);
|
|
189
|
+
if (complete) {
|
|
190
|
+
required(
|
|
191
|
+
canonicalJson(decodeCheckpoint(existing, digest)) ===
|
|
192
|
+
canonicalJson(payload),
|
|
193
|
+
"checkpoint retry payload differs"
|
|
194
|
+
);
|
|
195
|
+
return [...new Set(observed)].sort((left, right) => {
|
|
196
|
+
const index = body =>
|
|
197
|
+
body.includes(" complete\n")
|
|
198
|
+
? Infinity
|
|
199
|
+
: Number(body.split(" ")[4].split("/")[0]);
|
|
200
|
+
return index(left) - index(right);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const expected = checkpointComments(payload, digest, version);
|
|
204
|
+
if (observed.every(body => expected.includes(body))) return expected;
|
|
205
|
+
const recovered = partialCompletion(existing, digest);
|
|
179
206
|
required(
|
|
180
|
-
|
|
181
|
-
"checkpoint
|
|
207
|
+
canonicalJson(recovered.payload) === canonicalJson(payload),
|
|
208
|
+
"checkpoint retry payload differs"
|
|
182
209
|
);
|
|
183
|
-
return
|
|
210
|
+
return [...new Set(observed), recovered.completion];
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
/**
|
|
213
|
+
/** Every write still requires current authority and immediate exact provider readback. */
|
|
187
214
|
export async function persistCheckpoint(
|
|
188
215
|
api,
|
|
189
216
|
number,
|
|
@@ -191,14 +218,34 @@ export async function persistCheckpoint(
|
|
|
191
218
|
digest,
|
|
192
219
|
authorize
|
|
193
220
|
) {
|
|
194
|
-
const bodies = checkpointComments(payload, digest);
|
|
195
221
|
const endpoint = `repos/${api.policy.repository}/issues/${number}/comments`;
|
|
196
222
|
const issueUrl = `https://api.github.com/repos/${api.policy.repository}/issues/${number}`;
|
|
223
|
+
await authorize();
|
|
224
|
+
let existing = await api.list(endpoint);
|
|
225
|
+
const bodies = writeBodies(payload, digest, existing);
|
|
197
226
|
for (const body of bodies) {
|
|
227
|
+
const selected = relevant(existing, digest);
|
|
228
|
+
required(
|
|
229
|
+
new Set(selected).size === selected.length &&
|
|
230
|
+
selected.every(value => bodies.includes(value)),
|
|
231
|
+
"checkpoint retry transport differs"
|
|
232
|
+
);
|
|
233
|
+
let matches = existing.filter(comment => comment.body === body);
|
|
234
|
+
required(matches.length <= 1, "duplicate checkpoint write state");
|
|
235
|
+
if (matches.length) continue;
|
|
236
|
+
// Existing exact bodies require no mutation. Missing bodies always renew live authority
|
|
237
|
+
// and inventory first, including when completion is the sole remaining legacy write.
|
|
198
238
|
await authorize();
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
239
|
+
existing = await api.list(endpoint);
|
|
240
|
+
const refreshed = relevant(existing, digest);
|
|
241
|
+
required(
|
|
242
|
+
new Set(refreshed).size === refreshed.length &&
|
|
243
|
+
refreshed.every(value => bodies.includes(value)),
|
|
244
|
+
"checkpoint retry transport differs"
|
|
245
|
+
);
|
|
246
|
+
matches = existing.filter(comment => comment.body === body);
|
|
247
|
+
required(matches.length <= 1, "duplicate checkpoint write state");
|
|
248
|
+
if (/ complete\n/.test(body)) {
|
|
202
249
|
const projected = matches.length ? existing : [...existing, { body }];
|
|
203
250
|
required(
|
|
204
251
|
canonicalJson(decodeCheckpoint(projected, digest)) ===
|
|
@@ -206,7 +253,6 @@ export async function persistCheckpoint(
|
|
|
206
253
|
"checkpoint chunks must read back before completion"
|
|
207
254
|
);
|
|
208
255
|
}
|
|
209
|
-
required(matches.length <= 1, "duplicate checkpoint write state");
|
|
210
256
|
if (!matches.length) {
|
|
211
257
|
const created = await api.request(endpoint, "POST", { body });
|
|
212
258
|
required(
|
|
@@ -23,8 +23,8 @@ import { persistCheckpoint } from "./npm-update-checkpoint.mjs";
|
|
|
23
23
|
import { required, validateProposal } from "./npm-update-contract.mjs";
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
-
checkpointComments,
|
|
27
26
|
decodeCheckpoint,
|
|
27
|
+
recoverPartialCheckpoint,
|
|
28
28
|
} from "./npm-update-checkpoint.mjs";
|
|
29
29
|
|
|
30
30
|
/** A new issuer binds current authority to the immutable original artifact and destination. */
|
|
@@ -66,68 +66,12 @@ export function recoveryRecord(
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
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
69
|
/** Unsigned draft bytes aid retry; only the separately verified origin can authorize gates. */
|
|
126
70
|
export function locateCheckpoint(comments, proposal, policy) {
|
|
127
71
|
const digests = new Set(
|
|
128
72
|
comments.flatMap(comment => {
|
|
129
73
|
const match =
|
|
130
|
-
/^\[lisa-npm-checkpoint\]
|
|
74
|
+
/^\[lisa-npm-checkpoint\] \S+ ([a-f0-9]{64}) (?:chunk|complete)\b/.exec(
|
|
131
75
|
comment.body ?? ""
|
|
132
76
|
);
|
|
133
77
|
return match && match[1] !== proposal.key ? [match[1]] : [];
|
|
@@ -136,10 +80,14 @@ export function locateCheckpoint(comments, proposal, policy) {
|
|
|
136
80
|
required(digests.size <= 1, "multiple origin checkpoints are preserved");
|
|
137
81
|
const digest = [...digests][0] ?? proposal.key;
|
|
138
82
|
const present = comments.filter(comment =>
|
|
139
|
-
|
|
83
|
+
new RegExp(`^\\[lisa-npm-checkpoint\\] \\S+ ${digest} `).test(
|
|
84
|
+
comment.body ?? ""
|
|
85
|
+
)
|
|
140
86
|
);
|
|
141
87
|
const complete = present.some(comment =>
|
|
142
|
-
|
|
88
|
+
new RegExp(`^\\[lisa-npm-checkpoint\\] \\S+ ${digest} complete\\n`).test(
|
|
89
|
+
comment.body
|
|
90
|
+
)
|
|
143
91
|
);
|
|
144
92
|
const payload = complete
|
|
145
93
|
? decodeCheckpoint(comments, digest)
|
|
@@ -167,6 +115,7 @@ export {
|
|
|
167
115
|
checkpointComments,
|
|
168
116
|
decodeCheckpoint,
|
|
169
117
|
persistCheckpoint,
|
|
118
|
+
recoverPartialCheckpoint,
|
|
170
119
|
} from "./npm-update-checkpoint.mjs";
|
|
171
120
|
|
|
172
121
|
/** Renewal verifies fresh authority against every immutable original identity field. */
|