ReviewOS

also looking at this

stacks/drone

feat: import LAS surfaces and consume maintained source releases

#107
Merged chris wants to merge codex/las-stockpile-processing into main
35 files +1061 -416

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

AGENTS.mdmodified+2-0
Changes to AGENTS.md
@@ -18,6 +18,8 @@ This is the `stacksjs/drone` Stacks application. Read `README.md` for implemente
1818- Imported TypeScript modules explicitly import their dependencies. Browser entry bindings do not leak into modules.
1919- `better-dx` supplies shared dev tooling; do not redeclare its tools. Keep Bun's hoisted linker.
2020- No `catalog:` dependency ranges. Dependency updates belong to buddy-bot.
21- Dependency fixes and upgrades: read `docs/dependency-source-releases.md`.
22 Fix the owning source and consume its qualified release; dependency patches are prohibited.
2123- No em-dash or separator en-dash in newly authored visible copy.
2224- Before finishing run `./buddy lint`, `./buddy typecheck`, `./buddy test` and relevant browser scenarios.
2325- Test behavioral seams: decision input/output, controller lifecycle, real local UDP and protocol fixtures.
bun.lockmodified+4-8
Changes to bun.lock
@@ -25,7 +25,7 @@
2525 "@tufjs/canonical-json": "2.0.0",
2626 "stacks": "^0.75.221",
2727 "ts-images": "0.2.31",
28 "tuf-js": "6.0.0",
28 "tuf-js": "https://github.com/stacksjs/tuf-js/releases/download/sdk-v6.0.1/stacksjs-tuf-js-6.0.1.tgz",
2929 },
3030 "devDependencies": {
3131 "@stacksjs/query-builder": "0.75.221",
@@ -38,12 +38,8 @@
3838 "version": "0.75.221",
3939 },
4040 },
41 "patchedDependencies": {
42 "tuf-js@6.0.0": "patches/tuf-js@6.0.0.patch",
43 "@stacksjs/bunpress@0.2.22": "patches/@stacksjs%2Fbunpress@0.2.22.patch",
44 },
4541 "overrides": {
46 "@stacksjs/bunpress": "0.2.22",
42 "@stacksjs/bunpress": "0.2.25",
4743 },
4844 "packages": {
4945 "@buddysh/buddy": ["@buddysh/buddy@0.11.13", "", { "dependencies": { "prompts": "^2.4.2", "ts-pantry": "^0.11.64" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.117.1 <0.123.0" }, "optionalPeers": ["@anthropic-ai/sdk"], "bin": { "buddy": "./dist/bin/cli.js" } }, "sha512-xES8SfgtsXeswuQmFLVj/YV0nOECA4LGl7Oe2ZBqh6otABQY7LT7hAcz9KD/ORvVWtVr+tUt2sA+Vgd5hsm+Eg=="],
@@ -94,7 +90,7 @@
9490
9591 "@stacksjs/bun-router": ["@stacksjs/bun-router@0.1.24", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "ts-rate-limiter": "^0.4.2" }, "peerDependencies": { "@stacksjs/stx": ">=0.2.227" }, "optionalPeers": ["@stacksjs/stx"], "bin": { "router": "./dist/cli.js", "bun-router": "./dist/cli.js" } }, "sha512-vXzpmYBe9o0NOVWAZ1kqZJv7VAKTd84rEs3e8e+oEeX3Q4zE078SHTxrYsNcr2n1tb8EwQawaC4YGmFFKrgrzA=="],
9692
97 "@stacksjs/bunpress": ["@stacksjs/bunpress@0.2.22", "", { "dependencies": { "@cwcss/crosswind": "^0.2.16", "@stacksjs/clapp": "^0.2.0", "@stacksjs/stx": "^0.2.550", "@stacksjs/ts-i18n": "^0.1.11", "bunfig": "^0.15.17", "ts-syntax-highlighter": "^0.2.14" }, "peerDependencies": { "@stacksjs/ts-cloud": "^0.7.51" }, "optionalPeers": ["@stacksjs/ts-cloud"], "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-xxyLY7rU5v0VGDzv+7AzpFWPMEhMVQn6vZ6FrrAsmb2NShc5MfowO5yig1I3YYdzrSrHH/pDxETUtpC9f8X84A=="],
93 "@stacksjs/bunpress": ["@stacksjs/bunpress@0.2.25", "", { "dependencies": { "@cwcss/crosswind": "^0.2.16", "@stacksjs/clapp": "^0.2.0", "@stacksjs/stx": "^0.2.550", "@stacksjs/ts-i18n": "^0.1.11", "bunfig": "^0.15.17", "ts-syntax-highlighter": "^0.2.14" }, "peerDependencies": { "@stacksjs/ts-cloud": "^0.7.51" }, "optionalPeers": ["@stacksjs/ts-cloud"], "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-4mzXXQlCgMbkFe4Re6jAcKyGE2hSevpPFBbIytBdrrpEhCDl3VR5UH0v6Ei73wh3jkPAtxWBbJu4B7f72nqrsQ=="],
9894
9995 "@stacksjs/cache": ["@stacksjs/cache@0.75.221", "", { "dependencies": { "@stacksjs/ts-cache": "^0.1.7" } }, "sha512-D4B2tkif9FISL+5xaMff1Ceu2A1iSlMPLyLbgHGzWuqp2mR8KoB/8Ilpk5uq0Bd/xTf63U9opxkQvKlOr1jkTA=="],
10096
@@ -466,7 +462,7 @@
466462
467463 "ts-videos": ["ts-videos@0.1.2", "", { "dependencies": { "bunfig": "^0.15.6" }, "optionalDependencies": { "@ts-videos/mp4": "0.1.2", "@ts-videos/webm": "0.1.2" }, "bin": { "ts-videos": "./dist/bin/cli.js" } }, "sha512-J3s3hd6om0y7f7cANlCY72dTYReS/V/D6x3jKUQa358Y1cw5hdggF5LV/6BAYjhMFK5m6Ss7x2GbPyvKmoNimQ=="],
468464
469 "tuf-js": ["tuf-js@6.0.0", "", { "dependencies": { "@gar/promise-retry": "^1.0.3", "@tufjs/models": "5.0.0", "debug": "^4.4.3" } }, "sha512-zlJVOIO68hmgo1//X4ENEcTGfuOTAtDPi8PsTsG+FyxD85E/ww1ZnwBbWo/yCEExGpI+Kilg7Z3qCdHX2BoJTQ=="],
465 "tuf-js": ["@stacksjs/tuf-js@https://github.com/stacksjs/tuf-js/releases/download/sdk-v6.0.1/stacksjs-tuf-js-6.0.1.tgz", { "dependencies": { "@gar/promise-retry": "^1.0.3", "@tufjs/models": "5.0.0", "debug": "^4.4.3" } }, "sha512-E24zEwH9SkFXyxWBUESJ+D0uTHO+4lXOHyYft1tNoEBrNuJe08rUK2xW2rcMYg06WaQOFrcYttm2fDCPK7NWQQ=="],
470466
471467 "typescript": ["typescript@7.1.0-dev.20261011.1", "", { "optionalDependencies": { "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261011.1", "@typescript/typescript-darwin-x64": "7.1.0-dev.20261011.1", "@typescript/typescript-linux-arm": "7.1.0-dev.20261011.1", "@typescript/typescript-linux-arm64": "7.1.0-dev.20261011.1", "@typescript/typescript-linux-x64": "7.1.0-dev.20261011.1", "@typescript/typescript-win32-arm64": "7.1.0-dev.20261011.1", "@typescript/typescript-win32-x64": "7.1.0-dev.20261011.1" }, "bin": { "tsc": "bin/tsc" } }, "sha512-Oestzc61TT5cqxAyVxRsbL+pmMF8pzPlsnLcbCqAWqvC0eYQcp8EJrLWatIgjdUh4RiL79avUCHfM8hdkv07xg=="],
472468
docs/dependency-source-releases.mdadded+43-0
Changes to docs/dependency-source-releases.md
@@ -0,0 +1,43 @@
1# Dependency source releases
2
3Dependencies must come from maintained source releases. Dependency patches and
4edits to installed package files are prohibited. Fix and test the owning source
5repository, run its `bun run release:patch` command, then qualify the released
6package in this application with a native frozen install.
7
8## Maintained TUF client
9
10The `tuf-js` dependency key points to the immutable
11[`@stacksjs/tuf-js` 6.0.1 release archive](https://github.com/stacksjs/tuf-js/releases/tag/sdk-v6.0.1).
12The source is maintained in [stacksjs/tuf-js](https://github.com/stacksjs/tuf-js),
13with release source commit `40a7992babc29eb99c3ee18290ef7aa438421c08`.
14This is a maintained fork of the MIT-licensed upstream client, distinct from the
15upstream npm package. Its original license is retained in the installed package
16and `licenses/tuf-js.LICENSE` in this repository. The
17[SDK source license](https://github.com/stacksjs/tuf-js/blob/sdk-v6.0.1/packages/client/LICENSE)
18retains the original attribution.
19
20The release publishes its package archive, `SHA512SUMS` and `source.json`.
21Verify the immutable release identity, source commit and archive integrity before
22installation. Retain the exact release URL and frozen lockfile. The existing
23`tuf-js` import name resolves to the scoped maintained package through that
24dependency key. The SDK source repository's CLI and conformance harness use the
25scoped name explicitly.
26
27The observer, copied reference time and typed failure contracts are documented
28in [runtime updates](runtime-updates.md#maintained-sdk-persistence-seam). Do not
29remove their verification or persistence behavior during an upgrade. Qualify
30SDK observer cases, enrollment, persistent store/recovery, interrupted updates,
31rollback floors and copied standalone CLI campaigns against the actual release.
32Do not claim physical firmware deployment from these synthetic tests.
33
34## Documentation engine
35
36BunPress 0.2.25 includes the deployed-path search fix from
37[source commit 096004cc6e7b4800bfd1e90993866c9011ca706d](https://github.com/stacksjs/bunpress/commit/096004cc6e7b4800bfd1e90993866c9011ca706d).
38Consume the source release rather than editing installed files. The public
39search regressions in `tests/unit/docs-search.test.ts`, native documentation
40build and actual browser search selection qualify the application integration.
41
42[Issue #108](https://github.com/stacksjs/drone/issues/108) tracks source-release
43migration and downstream qualification evidence.
docs/runtime-updates.mdmodified+7-5
Changes to docs/runtime-updates.md
@@ -480,7 +480,7 @@ See the [host slot decision](adr/0029-host-slot-health.md).
480480
481481Use a standard TUF repository with `metadata/` and `targets/` directories,
482482including consistent-snapshot names when enabled. Verification uses the pinned
483[`tuf-js` 6.0.0 client](https://github.com/theupdateframework/tuf-js) for root
483[maintained `@stacksjs/tuf-js` 6.0.1 client](https://github.com/stacksjs/tuf-js/releases/tag/sdk-v6.0.1) for root
484484rotation, signature thresholds, expiration, inter-role versions/hashes and
485485target length/hashes. Standalone staging/inspection use a private, randomly
486486addressed loopback server supplying bounded local files to the public HTTP
@@ -496,10 +496,12 @@ expiry comparisons; malformed or automatically normalized dates are rejected.
496496
497497### Maintained SDK persistence seam
498498
499This repository applies `patches/tuf-js@6.0.0.patch` through Bun's root
500`patchedDependencies`. It extends the pinned MIT-licensed client; these are
501repository-maintained options, not options supplied by unmodified upstream 6.0.0.
502The patch retains upstream signature, role and target verification.
499This repository consumes the immutable maintained source release through the
500`tuf-js` dependency key, with no distribution patches. These are maintained fork
501options, not options supplied by unmodified upstream 6.0.0. The source client
502retains upstream signature, role and target verification. See the
503[source release policy](dependency-source-releases.md) for source identity,
504original MIT attribution, release integrity and upgrade qualification.
503505
504506`onVerifiedMetadata(role, bytes)` receives an owned byte copy after the SDK
505507accepts remote metadata. The SDK awaits it before writing that cache file or
patches/tuf-js.LICENSE licenses/tuf-js.LICENSErenamed+0-0

Renamed from patches/tuf-js.LICENSE. The contents are unchanged.

package.jsonmodified+2-6
Changes to package.json
@@ -50,7 +50,7 @@
5050 "@stacksjs/validation": "^0.75.221",
5151 "stacks": "^0.75.221",
5252 "ts-images": "0.2.31",
53 "tuf-js": "6.0.0",
53 "tuf-js": "https://github.com/stacksjs/tuf-js/releases/download/sdk-v6.0.1/stacksjs-tuf-js-6.0.1.tgz",
5454 "@tufjs/canonical-json": "2.0.0"
5555 },
5656 "workspaces": [
@@ -81,10 +81,6 @@
8181 "url": "https://github.com/stacksjs/drone/issues"
8282 },
8383 "overrides": {
84 "@stacksjs/bunpress": "0.2.22"
85 },
86 "patchedDependencies": {
87 "tuf-js@6.0.0": "patches/tuf-js@6.0.0.patch",
88 "@stacksjs/bunpress@0.2.22": "patches/@stacksjs%2Fbunpress@0.2.22.patch"
84 "@stacksjs/bunpress": "0.2.25"
8985 }
9086}
patches/@stacksjs%2Fbunpress@0.2.22.patchdeleted+0-45
Changes to patches/@stacksjs%2Fbunpress@0.2.22.patch
@@ -1,45 +0,0 @@
1diff --git a/dist/chunk-8rsrw3sw.js b/dist/chunk-8rsrw3sw.js
2index 1155ca4aa2f63eac326bc3373afdecd8cfa8bfc3..faf2b12ba255f9cc8fcbe813abf9f07c77a7549d 100644
3--- a/dist/chunk-8rsrw3sw.js
4+++ b/dist/chunk-8rsrw3sw.js
5@@ -1,4 +1,5 @@
6 // @bun
7+import { getConfiguredBasePath as searchBasePath } from "./search-base-path.js";
8 import{ia as Q0,la as L,ma as Y0}from"./chunk-ynp2pgxy.js";import{loadConfig as z0}from"bunfig";var K0={docsDir:"./docs",outDir:"./dist",theme:"vitepress",nav:[{text:"Guide",link:"/install"},{text:"API",link:"/advanced#api-reference"},{text:"Examples",link:"/examples"}],plugins:[],markdown:{title:"BunPress Documentation",meta:{description:"Documentation built with BunPress",generator:"BunPress",viewport:"width=device-width, initial-scale=1.0"},sidebar:{"/":[{text:"Getting Started",items:[{text:"Introduction",link:"/"},{text:"Installation",link:"/install"},{text:"Quick Start",link:"/quick-start"},{text:"Usage",link:"/usage"},{text:"Configuration",link:"/config"}]},{text:"Core Features",items:[{text:"Features Overview",link:"/features"},{text:"Markdown Extensions",link:"/markdown-extensions"},{text:"Syntax Highlighting",link:"/syntax-highlighting"},{text:"Table of Contents",link:"/table-of-contents"},{text:"CLI Commands",link:"/cli"}]},{text:"SEO & Analytics",items:[{text:"SEO Features",link:"/seo"},{text:"Analytics (Fathom)",link:"/config#fathom-analytics-configuration"}]},{text:"Advanced",items:[{text:"Advanced Usage",link:"/advanced"},{text:"Examples",link:"/examples"},{text:"Best Practices",link:"/best-practices"}]},{text:"More",items:[{text:"Showcase",link:"/showcase"},{text:"Partners",link:"/partners"},{text:"License",link:"/license"},{text:"Postcardware",link:"/postcardware"}]}]},css:`
9 /* Additional BunPress customizations */
10 /* Note: Base styles are provided by the VitePress theme */
11@@ -1735,5 +1736,5 @@ else {
12 }
13 `}var{Glob:i,YAML:m0}=globalThis.Bun;import{join as u0}from"path";var U1="/search-index.json",O={include:["**/*.md"],exclude:["public/**","**/node_modules/**"],searchFields:["title","content","headings","keywords"],maxResults:10,minQueryLength:1,fuzzy:!1,fuzziness:1,stemmer:"none",maxContentLength:800,boost:{title:10,headings:5,content:1}};function X0(X){let Q=X?.options??{};return{...O,...Q,include:Q.include??O.include,exclude:Q.exclude??O.exclude,searchFields:Q.searchFields??O.searchFields,maxResults:Q.maxResults??X?.maxResults??O.maxResults,boost:{...O.boost,...Q.boost??{}}}}var a=[{key:"k",meta:!0,ctrl:!1,alt:!1,shift:!1},{key:"k",meta:!1,ctrl:!0,alt:!1,shift:!1},{key:"/",meta:!1,ctrl:!1,alt:!1,shift:!1}];function p0(X){if(!X)return a;let Q=(Array.isArray(X)?X:[X]).map((J)=>String(J).toLowerCase()),Z={meta:!1,ctrl:!1,alt:!1,shift:!1},$="";for(let J of Q)if(J==="meta"||J==="cmd"||J==="command")Z.meta=!0;else if(J==="ctrl"||J==="control")Z.ctrl=!0;else if(J==="alt"||J==="option")Z.alt=!0;else if(J==="shift")Z.shift=!0;else $=J;if(!$)return a;return[{key:$,...Z}]}function n(X){if(typeof X!=="function")return null;let Q=Function.prototype.toString.call(X);return Q.includes("[native code]")?null:Q}function I1(X,Q){let Z=X?.search??X?.markdown?.search,$=X0(Z),J=Z?.resultOptions??{};return{indexUrl:Q,placeholder:Z?.placeholder??"Search documentation",lazy:Z?.lazy!==!1,maxResults:$.maxResults,minQueryLength:$.minQueryLength,searchFields:$.searchFields,boost:{title:$.boost.title??10,headings:$.boost.headings??5,content:$.boost.content??1},fuzzy:$.fuzzy,fuzziness:$.fuzziness,stemmer:$.stemmer,shortcuts:{keyboard:Z?.keyboardShortcuts!==!1,keys:p0(Z?.shortcut)},result:{showDescription:J.showDescription!==!1,descriptionLength:J.descriptionLength??180,highlightMatches:J.highlightMatches!==!1,showPath:J.showPath!==!1},tokenizeSource:n(Z?.options?.tokenize),onSearchSource:n(Z?.onSearch),locale:null}}function t(X){return p(r(X)).replace(/^:::.*$/gm," ").replace(/^\s{0,3}>\s*\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/gim," ").replace(/<[^>]+>/g," ").replace(/!\[[^\]]*\]\([^)]*\)/g," ").replace(/\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/`([^`]+)`/g,"$1").replace(/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/g,"$1").replace(/^\s{0,3}>\s?/gm,"").replace(/^\s*[-*+]\s+/gm,"").replace(/^\s*\d+\.\s+/gm,"").replace(/^\s*\|?[\s:|-]+\|[\s:|-]*$/gm," ").replace(/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/gm," ").replace(/\|/g," ").replace(/\s+/g," ").trim()}function g0(X){let Q=X.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);if(!Q)return{body:X,data:{}};let Z={};try{Z=m0.parse(Q[1])??{}}catch{Z={}}return{body:X.slice(Q[0].length),data:Z}}function h0(X){let Q=X.replace(/\.md$/,"");if(Q==="index")return"/";if(Q.endsWith("/index"))return`/${Q.slice(0,-6)}`;return`/${Q}`}function d0(X,Q,Z){let{body:$,data:J}=g0(Q);if(J.search===!1)return[];if(J.layout==="home")return[];let V=typeof J.search==="object"&&J.search!==null?J.search:{},W=Array.isArray(V.keywords)?V.keywords.map(String):[],q=h0(X),j=$.split(`
14 `),G=$.match(/^#\s+(.+)$/m),K=V.title||J.title||(G?G[1].trim():q),B=[],z=new Set,Y={title:K,anchor:"",level:1,buffer:[]},M=()=>{let N=t(Y.buffer.join(`
15-`)).slice(0,Z.maxContentLength);if(!N&&!Y.title)return;let U={url:Y.anchor?`${q}#${Y.anchor}`:q,page:K,title:t(Y.title)||K,text:N,level:Y.level};if(W.length)U.keywords=W;B.push(U)},I=E();for(let N of j){let U=I(N)?null:N.match(/^(#{1,6})\s+(.+?)\s*$/);if(!U){Y.buffer.push(N);continue}let H=U[1].length,A=U[2].replace(/\s*\{#([\w-]+)\}\s*$/,""),R=U[2].match(/\{#([\w-]+)\}\s*$/),D=b(R?R[1]:T(v(A)),z);if(H===1){Y.buffer.push("");continue}M(),Y={title:A,anchor:D,level:H,buffer:[]}}return M(),B.filter((N)=>N.text||N.level>1)}function c0(X,Q){let{storeFields:Z}=Q;if(!Z||!Z.length)return X;let $=new Set(["url",...Z]);if(Q.searchFields.includes("title"))$.add("title");if(Q.searchFields.includes("content"))$.add("text");if(Q.searchFields.includes("headings"))$.add("page");if(Q.searchFields.includes("keywords"))$.add("keywords");return X.map((J)=>{let V={};for(let[W,q]of Object.entries(J))if($.has(W)&&q!==void 0)V[W]=q;return V})}function e(X){return X.split("#")[0]}function l0(X,Q){if(!Q.enabled)return{locale:Q.defaultLocale,page:X};let Z=X.split("/");if(Z.length>1&&Q.locales.includes(Z[0])&&Z[0]!==Q.defaultLocale)return{locale:Z[0],page:Z.slice(1).join("/")};let $=X.match(/^(.*)\.([A-Za-z-]+)(\.md)$/);if($&&Q.locales.includes($[2]))return{locale:$[2],page:`${$[1]}${$[3]}`};return{locale:Q.defaultLocale,page:X}}async function _1(X,Q){let Z=Q?.search??Q?.markdown?.search,$=X0(Z),J=Q0(Q),V=$.exclude.map((G)=>new i(G)),W=[],q=new Set,j=new Map;for(let G of J.locales)j.set(G,new Set);for(let G of $.include){let K=new i(G);for await(let B of K.scan(X)){let z=B.replace(/\\/g,"/");if(V.some((Y)=>Y.match(z)))continue;q.add(z)}}for(let G of[...q].sort())try{let K=await Bun.file(u0(X,G)).text(),{locale:B,page:z}=l0(G,J);for(let Y of d0(z,K,$)){if(J.enabled)Y.locale=B,Y.url=L(J,B,Y.url);W.push(Y),j.get(B)?.add(e(Y.url))}}catch{}if(J.enabled){let G=W.filter((K)=>K.locale===J.defaultLocale);for(let K of J.locales){if(K===J.defaultLocale)continue;let B=j.get(K)??new Set;for(let z of G){let Y=L(J,K,z.url);if(B.has(e(Y)))continue;W.push({...z,url:Y,locale:K})}}}return c0(W,$)}function Z0(X){let Q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0"};return X.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi,(Z,$)=>{if($[0]==="#"){let J=$[1]==="x"||$[1]==="X"?Number.parseInt($.slice(2),16):Number.parseInt($.slice(1),10);return Number.isFinite(J)&&J>0&&J<=1114111?String.fromCodePoint(J):Z}return Q[$.toLowerCase()]??Z})}function $0(X){return String(X??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}var{Glob:r0}=globalThis.Bun;import{dirname as s0,isAbsolute as J0,join as o0,relative as V0,resolve as C,sep as W0}from"path";async function L1(X){let Q=[];for await(let Z of new r0("**/*.md").scan({cwd:X,onlyFiles:!0}))Q.push(o0(X,Z));return Q}function q0(X){return X.split("/").map((Q)=>encodeURIComponent(Q)).join("/")}function x(X){let Q=X==="index"?"":X.replace(/\/index$/,"/");return`/${q0(Q)}`}function k(X){return X.split("/").map((Z)=>encodeURIComponent(decodeURIComponent(Z))).join("/").replace(/\/+$/,"")||"/"}function C1(X,Q,Z){let $=C(Q),J={...Z,locales:[...Z.locales]},V=new Set(X.map((z)=>C(z))),W=new Map,q=new Set;for(let z of V){let Y=V0($,z).split(W0).join("/").replace(/\.md$/,"");if(Y===".."||Y.startsWith("../")||J0(Y))throw Error("Markdown source is outside the documentation root");let M=J.defaultLocale,I=!1;if(J.enabled){let N=Y.split("/");if(N.length>1&&J.locales.includes(N[0])&&N[0]!==J.defaultLocale)M=N.shift(),I=!0,Y=N.join("/");else{let U=Y.match(/^(.*)\.([A-Za-z-]+)$/);if(U&&J.locales.includes(U[2]))Y=U[1],M=U[2],I=!0}}W.set(z,{path:Y,locale:M,explicitLocale:I}),q.add(Y)}let j=[],G=new Map,K=new Map,B=new Map;for(let z of J.locales)for(let Y of q){let M=J.enabled?Y0(J,$,z,`/${Y}`).map((U)=>C(U)).find((U)=>V.has(U)):C($,`${Y}.md`);if(!M||!V.has(M))continue;let I=Object.freeze({sourceFile:M,relativePath:Y,locale:z}),N=`${z}\x00${k(x(Y))}`;if(K.has(N))throw Error(`Documentation pages share an output route: ${x(Y)}`);K.set(N,I),G.set(`${z}\x00${Y}`,I),B.set(`${z}\x00${k(`/${q0(Y)}`)}`,I),j.push(I)}return Object.freeze({pages:Object.freeze(j),findPage(z,Y){let M=`${z}\x00${k(Y)}`;return K.get(M)??B.get(M)},resolveLink(z,Y){if(!Y||/^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(Y))return Y;let M=Y.search(/[?#]/),I=M===-1?Y:Y.slice(0,M),N=M===-1?"":Y.slice(M),U=I.replace(/%([\da-f]{2})/gi,(i0,j0)=>String.fromCharCode(Number.parseInt(j0,16)));if(!/\.md$/i.test(U))return Y;let H=decodeURIComponent(I),A=C(s0(z.sourceFile),H),R=V0($,A);if(R===".."||R.startsWith(`..${W0}`)||J0(R))return Y;let D=W.get(A);if(!D)throw Error(`Markdown link has no documentation source: ${I}`);let f=D.explicitLocale?D.locale:z.locale,m=G.get(`${f}\x00${D.path}`);if(!m)throw Error(`Markdown link has no rendered page: ${I}`);return`${L(J,f,x(m.relativePath))}${N}`}})}async function E1(X,Q,Z=new Map){let $=[Q.page],J=new Set(Z.keys()),V=await new HTMLRewriter().onDocument({comments(W){let q=Z.get(W.text);if(q===void 0)return;if(!J.delete(W.text))throw Error("Markdown include source boundary was duplicated");if(q===null){if($.length===1)throw Error("Markdown include source boundaries are unbalanced");$.pop()}else $.push(q);W.remove()}}).on("a[href]",{element(W){let q=Z0(W.getAttribute("href")),j=Q.catalog.resolveLink($[$.length-1],q);if(j!==q)W.setAttribute("href",$0(j))}}).transform(new Response(X)).text();if(J.size||$.length!==1)throw Error("Markdown include source boundaries did not survive rendering");return V}
16+`)).slice(0,Z.maxContentLength);if(!N&&!Y.title)return;let U={url:Y.anchor?`${q}#${Y.anchor}`:q,page:K,title:t(Y.title)||K,text:N,level:Y.level};if(W.length)U.keywords=W;B.push(U)},I=E();for(let N of j){let U=I(N)?null:N.match(/^(#{1,6})\s+(.+?)\s*$/);if(!U){Y.buffer.push(N);continue}let H=U[1].length,A=U[2].replace(/\s*\{#([\w-]+)\}\s*$/,""),R=U[2].match(/\{#([\w-]+)\}\s*$/),D=b(R?R[1]:T(v(A)),z);if(H===1){Y.buffer.push("");continue}M(),Y={title:A,anchor:D,level:H,buffer:[]}}return M(),B.filter((N)=>N.text||N.level>1)}function c0(X,Q){let{storeFields:Z}=Q;if(!Z||!Z.length)return X;let $=new Set(["url",...Z]);if(Q.searchFields.includes("title"))$.add("title");if(Q.searchFields.includes("content"))$.add("text");if(Q.searchFields.includes("headings"))$.add("page");if(Q.searchFields.includes("keywords"))$.add("keywords");return X.map((J)=>{let V={};for(let[W,q]of Object.entries(J))if($.has(W)&&q!==void 0)V[W]=q;return V})}function e(X){return X.split("#")[0]}function l0(X,Q){if(!Q.enabled)return{locale:Q.defaultLocale,page:X};let Z=X.split("/");if(Z.length>1&&Q.locales.includes(Z[0])&&Z[0]!==Q.defaultLocale)return{locale:Z[0],page:Z.slice(1).join("/")};let $=X.match(/^(.*)\.([A-Za-z-]+)(\.md)$/);if($&&Q.locales.includes($[2]))return{locale:$[2],page:`${$[1]}${$[3]}`};return{locale:Q.defaultLocale,page:X}}async function _1(X,Q){let Z=Q?.search??Q?.markdown?.search,$=X0(Z),J=Q0(Q),V=$.exclude.map((G)=>new i(G)),W=[],q=new Set,j=new Map;for(let G of J.locales)j.set(G,new Set);for(let G of $.include){let K=new i(G);for await(let B of K.scan(X)){let z=B.replace(/\\/g,"/");if(V.some((Y)=>Y.match(z)))continue;q.add(z)}}for(let G of[...q].sort())try{let K=await Bun.file(u0(X,G)).text(),{locale:B,page:z}=l0(G,J);for(let Y of d0(z,K,$)){if(J.enabled)Y.locale=B,Y.url=L(J,B,Y.url);W.push(Y),j.get(B)?.add(e(Y.url))}}catch{}if(J.enabled){let G=W.filter((K)=>K.locale===J.defaultLocale);for(let K of J.locales){if(K===J.defaultLocale)continue;let B=j.get(K)??new Set;for(let z of G){let Y=L(J,K,z.url);if(B.has(e(Y)))continue;W.push({...z,url:Y,locale:K})}}}let searchPrefix=Q?searchBasePath(Q):"";if(searchPrefix)for(let record of W)record.url=searchPrefix+record.url;return c0(W,$)}function Z0(X){let Q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0"};return X.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi,(Z,$)=>{if($[0]==="#"){let J=$[1]==="x"||$[1]==="X"?Number.parseInt($.slice(2),16):Number.parseInt($.slice(1),10);return Number.isFinite(J)&&J>0&&J<=1114111?String.fromCodePoint(J):Z}return Q[$.toLowerCase()]??Z})}function $0(X){return String(X??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}var{Glob:r0}=globalThis.Bun;import{dirname as s0,isAbsolute as J0,join as o0,relative as V0,resolve as C,sep as W0}from"path";async function L1(X){let Q=[];for await(let Z of new r0("**/*.md").scan({cwd:X,onlyFiles:!0}))Q.push(o0(X,Z));return Q}function q0(X){return X.split("/").map((Q)=>encodeURIComponent(Q)).join("/")}function x(X){let Q=X==="index"?"":X.replace(/\/index$/,"/");return`/${q0(Q)}`}function k(X){return X.split("/").map((Z)=>encodeURIComponent(decodeURIComponent(Z))).join("/").replace(/\/+$/,"")||"/"}function C1(X,Q,Z){let $=C(Q),J={...Z,locales:[...Z.locales]},V=new Set(X.map((z)=>C(z))),W=new Map,q=new Set;for(let z of V){let Y=V0($,z).split(W0).join("/").replace(/\.md$/,"");if(Y===".."||Y.startsWith("../")||J0(Y))throw Error("Markdown source is outside the documentation root");let M=J.defaultLocale,I=!1;if(J.enabled){let N=Y.split("/");if(N.length>1&&J.locales.includes(N[0])&&N[0]!==J.defaultLocale)M=N.shift(),I=!0,Y=N.join("/");else{let U=Y.match(/^(.*)\.([A-Za-z-]+)$/);if(U&&J.locales.includes(U[2]))Y=U[1],M=U[2],I=!0}}W.set(z,{path:Y,locale:M,explicitLocale:I}),q.add(Y)}let j=[],G=new Map,K=new Map,B=new Map;for(let z of J.locales)for(let Y of q){let M=J.enabled?Y0(J,$,z,`/${Y}`).map((U)=>C(U)).find((U)=>V.has(U)):C($,`${Y}.md`);if(!M||!V.has(M))continue;let I=Object.freeze({sourceFile:M,relativePath:Y,locale:z}),N=`${z}\x00${k(x(Y))}`;if(K.has(N))throw Error(`Documentation pages share an output route: ${x(Y)}`);K.set(N,I),G.set(`${z}\x00${Y}`,I),B.set(`${z}\x00${k(`/${q0(Y)}`)}`,I),j.push(I)}return Object.freeze({pages:Object.freeze(j),findPage(z,Y){let M=`${z}\x00${k(Y)}`;return K.get(M)??B.get(M)},resolveLink(z,Y){if(!Y||/^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(Y))return Y;let M=Y.search(/[?#]/),I=M===-1?Y:Y.slice(0,M),N=M===-1?"":Y.slice(M),U=I.replace(/%([\da-f]{2})/gi,(i0,j0)=>String.fromCharCode(Number.parseInt(j0,16)));if(!/\.md$/i.test(U))return Y;let H=decodeURIComponent(I),A=C(s0(z.sourceFile),H),R=V0($,A);if(R===".."||R.startsWith(`..${W0}`)||J0(R))return Y;let D=W.get(A);if(!D)throw Error(`Markdown link has no documentation source: ${I}`);let f=D.explicitLocale?D.locale:z.locale,m=G.get(`${f}\x00${D.path}`);if(!m)throw Error(`Markdown link has no rendered page: ${I}`);return`${L(J,f,x(m.relativePath))}${N}`}})}async function E1(X,Q,Z=new Map){let $=[Q.page],J=new Set(Z.keys()),V=await new HTMLRewriter().onDocument({comments(W){let q=Z.get(W.text);if(q===void 0)return;if(!J.delete(W.text))throw Error("Markdown include source boundary was duplicated");if(q===null){if($.length===1)throw Error("Markdown include source boundaries are unbalanced");$.pop()}else $.push(q);W.remove()}}).on("a[href]",{element(W){let q=Z0(W.getAttribute("href")),j=Q.catalog.resolveLink($[$.length-1],q);if(j!==q)W.setAttribute("href",$0(j))}}).transform(new Response(X)).text();if(J.size||$.length!==1)throw Error("Markdown include source boundaries did not survive rendering");return V}
17 export{K0 as A,G0 as B,M0 as C,n0 as D,p as E,e0 as F,X1 as G,Z1 as H,S0 as I,v as J,T as K,b as L,s as M,v0 as N,T0 as O,b0 as P,w as Q,o as R,x0 as S,k0 as T,f0 as U,J1 as V,V1 as W,W1 as X,Y1 as Y,q1 as Z,U1 as _,I1 as $,_1 as aa,Z0 as ba,$0 as ca,L1 as da,C1 as ea,E1 as fa};
18diff --git a/node_modules/@stacksjs/bunpress/dist/search-base-path.js b/dist/search-base-path.js
19new file mode 100644
20index 0000000000000000000000000000000000000000..017c550ed825d5704524923bd510147bfaba1e09
21--- /dev/null
22+++ b/dist/search-base-path.js
23@@ -0,0 +1,22 @@
24+// Deployment path compatibility patch for BunPress 0.2.22.
25+export function getConfiguredBasePath(config) {
26+ if (config.basePath) {
27+ const normalized = config.basePath.trim().replace(/\/+$/, '')
28+ if (!normalized || normalized === '/')
29+ return ''
30+ return normalized.startsWith('/') ? normalized : `/${normalized}`
31+ }
32+
33+ const baseUrl = (config.sitemap?.baseUrl || config.url || '').replace(/\/$/, '')
34+ if (!baseUrl) {
35+ return ''
36+ }
37+
38+ try {
39+ const pathname = new URL(baseUrl).pathname.replace(/\/+$/, '')
40+ return pathname === '/' ? '' : pathname
41+ }
42+ catch {
43+ return ''
44+ }
45+}
patches/README.mddeleted+0-55
Changes to patches/README.md
@@ -1,55 +0,0 @@
1# Maintained dependency patches
2
3## tuf-js 6.0.0
4
5`tuf-js@6.0.0.patch` applies to the npm 6.0.0 release from
6[the TUF JavaScript client](https://github.com/theupdateframework/tuf-js), upstream
7commit `c30653d07f0c41fbe5a2030d25eca6fd04f8547f`. The original MIT copyright and permission
8notice is retained in `tuf-js.LICENSE` and in the installed package.
9
10The patch adds an awaited, copy-owning `onVerifiedMetadata` observer before the
11SDK writes each newly verified remote metadata file, plus an owned finite
12`referenceTime` for recovery replay. Default callers keep upstream verification rules and the current fixed start
13time. Persistence failure aborts rather than being interpreted as a missing
14remote root or a cache miss under `forceCache`. Discard failed updater instances;
15their in-memory trust set is not rolled back. It does not make cache writes
16transactional or implement a protected trust store. Public operational error
17exports retain causes and identify remote verification, persistence and download
18phases. Verification errors identify an attempted role, not permission to discard
19state. Classify persistence first, including when an observer throws a
20verification error; unknown causes require maintenance rather than an assumed
21invalid-input verdict.
22Wrapper messages never coerce caught values, so opaque observer rejections keep
23their persistence classification and exact cause under cache-preferred refresh.
24
25Bun applies the patch through root `patchedDependencies` and the lockfile.
26Always prepare with `bun patch tuf-js@6.0.0` before editing an installed package,
27then generate changes with `bun patch --commit tuf-js@6.0.0`. Do not edit shared
28package caches. Exclude empty generated `.bun-tag-*` entries from the patch;
29they are installer cache markers rather than package source. For upgrades, review the upstream change against every patch
30hunk and rerun the SDK observer cases, update/enrollment regressions, clean
31frozen installation and copied standalone CLI qualification. Never silently
32carry a patch onto another version.
33
34See `docs/runtime-updates.md` for the trust and clock limits. The extensions are
35maintained here and are not claimed to be unmodified upstream options.
36
37## BunPress 0.2.22
38
39`@stacksjs%2Fbunpress@0.2.22.patch` keeps generated local search results under
40BunPress's configured deployment path. The native page renderer already uses
41`/docs`, but 0.2.22's search index contained origin-root paths, so selecting a
42result navigated to a 404. The patch applies the same base-path resolution after
43locale translation and fallback selection, preserving anchors and root-hosted
44sites. A real source directory named `docs` still receives the deployment prefix.
45
46The patch changes the native index builder and adds a small helper matching the renderer's path resolution;
47it adds no browser redirect or application search implementation. The installed
48package retains BunPress's MIT license. Regression coverage lives in
49`tests/unit/docs-search.test.ts`. Rebuild native docs and verify an actual search
50selection after upgrading or removing this patch. Prepare installed changes with
51`bun patch @stacksjs/bunpress@0.2.22` and retain the generated Bun patch and lockfile.
52
53The shared engine fix is [BunPress PR 91](https://github.com/stacksjs/bunpress/pull/91),
54source commit `096004cc6e7b4800bfd1e90993866c9011ca706d`. Remove the compatibility
55patch only after upgrading to a release that includes it and repeating the checks.
patches/tuf-js@6.0.0.patchdeleted+0-255
Changes to patches/tuf-js@6.0.0.patch
@@ -1,255 +0,0 @@
1diff --git a/dist/error.d.ts b/dist/error.d.ts
2index 3a42f0441b68f961d6e367efee45b4f4158bbccd..727dd12095b649a8d6203e7eba6d9ebb59be68d6 100644
3--- a/dist/error.d.ts
4+++ b/dist/error.d.ts
5@@ -6,6 +6,10 @@ export declare class PersistError extends Error {
6 }
7 export declare class RepositoryError extends Error {
8 }
9+export declare class MetadataVerificationError extends Error {
10+ readonly role: string;
11+ constructor(role: string, message: string, options?: ErrorOptions);
12+}
13 export declare class BadVersionError extends RepositoryError {
14 }
15 export declare class EqualVersionError extends BadVersionError {
16diff --git a/dist/error.js b/dist/error.js
17index 3a3c26a068a953998e16e552faf96b6fbe956f90..e4c07310caa36177ce390e6e3d82dd76178432f4 100644
18--- a/dist/error.js
19+++ b/dist/error.js
20@@ -17,6 +17,15 @@ exports.PersistError = PersistError;
21 class RepositoryError extends Error {
22 }
23 exports.RepositoryError = RepositoryError;
24+// Repository-maintained public distinction for pure remote metadata validation.
25+class MetadataVerificationError extends Error {
26+ role;
27+ constructor(role, message, options) {
28+ super(message, options);
29+ this.role = role;
30+ }
31+}
32+exports.MetadataVerificationError = MetadataVerificationError;
33 // An error for metadata that contains an invalid version number.
34 class BadVersionError extends RepositoryError {
35 }
36diff --git a/dist/index.d.ts b/dist/index.d.ts
37index 895fdf4b57c27baf4571e8d06bb93de69166f574..aa1e170b7832c2df49cf2628f77d7895a5f0cbf1 100644
38--- a/dist/index.d.ts
39+++ b/dist/index.d.ts
40@@ -4,3 +4,4 @@ export { Updater } from './updater';
41 export type { Config } from './config';
42 export type { Fetcher } from './fetcher';
43 export type { UpdaterOptions } from './updater';
44+export { DownloadError, DownloadHTTPError, MetadataVerificationError, PersistError } from './error';
45diff --git a/dist/index.js b/dist/index.js
46index 5a83b91f355d88c27615d2bc07310958b18d0142..645728da0d96110d323b0f5e5b2c5f0e839c6fce 100644
47--- a/dist/index.js
48+++ b/dist/index.js
49@@ -7,3 +7,10 @@ var fetcher_1 = require("./fetcher");
50 Object.defineProperty(exports, "BaseFetcher", { enumerable: true, get: function () { return fetcher_1.BaseFetcher; } });
51 var updater_1 = require("./updater");
52 Object.defineProperty(exports, "Updater", { enumerable: true, get: function () { return updater_1.Updater; } });
53+
54+// Repository-maintained operational error exports; use causes rather than message matching.
55+var error_1 = require("./error");
56+Object.defineProperty(exports, "DownloadError", { enumerable: true, get: function () { return error_1.DownloadError; } });
57+Object.defineProperty(exports, "DownloadHTTPError", { enumerable: true, get: function () { return error_1.DownloadHTTPError; } });
58+Object.defineProperty(exports, "MetadataVerificationError", { enumerable: true, get: function () { return error_1.MetadataVerificationError; } });
59+Object.defineProperty(exports, "PersistError", { enumerable: true, get: function () { return error_1.PersistError; } });
60diff --git a/dist/store.d.ts b/dist/store.d.ts
61index 20f835c29dee82ea3e61c775c83d8a34fad787dc..3b9c2f88cbe9082b45927ccabccb44f1d56d8739 100644
62--- a/dist/store.d.ts
63+++ b/dist/store.d.ts
64@@ -2,7 +2,7 @@ import { Metadata, Root, Snapshot, Targets, Timestamp } from '@tufjs/models';
65 export declare class TrustedMetadataStore {
66 private trustedSet;
67 private referenceTime;
68- constructor(rootData: Buffer);
69+ constructor(rootData: Buffer, referenceTime?: Date);
70 get root(): Metadata<Root>;
71 get timestamp(): Metadata<Timestamp> | undefined;
72 get snapshot(): Metadata<Snapshot> | undefined;
73diff --git a/dist/store.js b/dist/store.js
74index 1b1669029a8db00b7cc594ef202984f3d243a0e4..0ef50b3d8b599b7d49392d7d8c5f63d3c6f0ecce 100644
75--- a/dist/store.js
76+++ b/dist/store.js
77@@ -6,9 +6,9 @@ const error_1 = require("./error");
78 class TrustedMetadataStore {
79 trustedSet = {};
80 referenceTime;
81- constructor(rootData) {
82+ constructor(rootData, referenceTime = new Date()) {
83 // Client workflow 5.1: record fixed update start time
84- this.referenceTime = new Date();
85+ this.referenceTime = new Date(referenceTime.getTime());
86 // Client workflow 5.2: load trusted root metadata
87 this.loadTrustedRoot(rootData);
88 }
89diff --git a/dist/updater.d.ts b/dist/updater.d.ts
90index 883df0fe6a6628ef5d16bdb0de17c0850d7d19ec..0fe93d96f9a7912c6708bfdcd6f8bcc7330c9e2e 100644
91--- a/dist/updater.d.ts
92+++ b/dist/updater.d.ts
93@@ -9,6 +9,10 @@ export interface UpdaterOptions {
94 fetcher?: Fetcher;
95 forceCache?: boolean;
96 config?: Partial<Config>;
97+ /** Repository-maintained extension: called after verification, awaited before cache persistence. */
98+ onVerifiedMetadata?: (role: string, bytes: Buffer) => void | Promise<void>;
99+ /** Repository-maintained extension: copied fixed start time for recovery replay, not a trusted clock source. */
100+ referenceTime?: Date;
101 }
102 export declare class Updater {
103 private dir;
104@@ -19,6 +23,7 @@ export declare class Updater {
105 private trustedSet;
106 private config;
107 private fetcher;
108+ private onVerifiedMetadata;
109 constructor(options: UpdaterOptions);
110 refresh(): Promise<void>;
111 getTargetInfo(targetPath: string): Promise<TargetFile | undefined>;
112@@ -31,5 +36,6 @@ export declare class Updater {
113 private loadTargets;
114 private preorderDepthFirstWalk;
115 private generateTargetPath;
116+ private verifyMetadata;
117 private persistMetadata;
118 }
119diff --git a/dist/updater.js b/dist/updater.js
120index 94a7658155740fee3f3b7b32937905cc7f791e90..ce62cfdc83eeec41479089aa301cbea250d8d116 100644
121--- a/dist/updater.js
122+++ b/dist/updater.js
123@@ -57,6 +57,7 @@ class Updater {
124 trustedSet;
125 config;
126 fetcher;
127+ onVerifiedMetadata;
128 constructor(options) {
129 const { metadataDir, metadataBaseUrl, targetDir, targetBaseUrl, fetcher, config, } = options;
130 this.dir = metadataDir;
131@@ -64,8 +65,16 @@ class Updater {
132 this.targetDir = targetDir;
133 this.targetBaseUrl = targetBaseUrl;
134 this.forceCache = options.forceCache ?? false;
135+ const referenceTime = options.referenceTime ?? new Date();
136+ if (!(referenceTime instanceof Date) || !Number.isFinite(referenceTime.getTime())) {
137+ throw new error_1.ValueError('Invalid fixed update reference time');
138+ }
139+ if (options.onVerifiedMetadata !== undefined && typeof options.onVerifiedMetadata !== 'function') {
140+ throw new error_1.ValueError('Invalid verified metadata observer');
141+ }
142+ this.onVerifiedMetadata = options.onVerifiedMetadata;
143 const data = this.loadLocalMetadata(models_1.MetadataKind.Root);
144- this.trustedSet = new store_1.TrustedMetadataStore(data);
145+ this.trustedSet = new store_1.TrustedMetadataStore(data, new Date(referenceTime.getTime()));
146 this.config = { ...config_1.defaultConfig, ...config };
147 const userAgent = config?.userAgent
148 ? `${config.userAgent} tuf-js/${package_json_1.version}`
149@@ -92,6 +101,10 @@ class Updater {
150 await this.loadTimestamp({ checkRemote: false });
151 }
152 catch (error) {
153+ // A failed persistence observer/cache write is not a cache miss.
154+ if (error instanceof error_1.PersistError) {
155+ throw error;
156+ }
157 await this.loadRoot();
158 await this.loadTimestamp();
159 }
160@@ -173,9 +186,9 @@ class Updater {
161 // Client workflow 5.3.3: download new root metadata file
162 const bytesData = await this.fetcher.downloadBytes(rootUrl, this.config.rootMaxLength);
163 // Client workflow 5.3.4 - 5.4.7
164- this.trustedSet.updateRoot(bytesData);
165+ this.verifyMetadata(models_1.MetadataKind.Root, () => this.trustedSet.updateRoot(bytesData));
166 // Client workflow 5.3.8: persist root metadata file
167- this.persistMetadata(models_1.MetadataKind.Root, bytesData);
168+ await this.persistMetadata(models_1.MetadataKind.Root, bytesData);
169 }
170 catch (error) {
171 if (error instanceof error_1.DownloadHTTPError) {
172@@ -210,7 +223,7 @@ class Updater {
173 const bytesData = await this.fetcher.downloadBytes(timestampUrl, this.config.timestampMaxLength);
174 try {
175 // Client workflow 5.4.2 - 5.4.4
176- this.trustedSet.updateTimestamp(bytesData);
177+ this.verifyMetadata(models_1.MetadataKind.Timestamp, () => this.trustedSet.updateTimestamp(bytesData));
178 }
179 catch (error) {
180 // If new timestamp version is same as current, discardd the new one.
181@@ -222,7 +235,7 @@ class Updater {
182 throw error;
183 }
184 // Client workflow 5.4.5: persist timestamp metadata
185- this.persistMetadata(models_1.MetadataKind.Timestamp, bytesData);
186+ await this.persistMetadata(models_1.MetadataKind.Timestamp, bytesData);
187 }
188 // Load local and remote snapshot metadata.
189 // Client workflow 5.5: update snapshot role
190@@ -246,12 +259,12 @@ class Updater {
191 // Client workflow 5.5.1: download snapshot metadata file
192 const bytesData = await this.fetcher.downloadBytes(snapshotUrl, maxLength);
193 // Client workflow 5.5.2 - 5.5.6
194- this.trustedSet.updateSnapshot(bytesData);
195+ this.verifyMetadata(models_1.MetadataKind.Snapshot, () => this.trustedSet.updateSnapshot(bytesData));
196 // Client workflow 5.5.7: persist snapshot metadata file
197- this.persistMetadata(models_1.MetadataKind.Snapshot, bytesData);
198+ await this.persistMetadata(models_1.MetadataKind.Snapshot, bytesData);
199 }
200 catch (error) {
201- throw new error_1.RuntimeError(`Unable to load snapshot metadata error ${error}`);
202+ throw new error_1.RuntimeError('Unable to load snapshot metadata', { cause: error });
203 }
204 }
205 }
206@@ -282,12 +295,12 @@ class Updater {
207 // Client workflow 5.6.1: download targets metadata file
208 const bytesData = await this.fetcher.downloadBytes(metadataUrl, maxLength);
209 // Client workflow 5.6.2 - 5.6.6
210- this.trustedSet.updateDelegatedTargets(bytesData, role, parentRole);
211+ this.verifyMetadata(role, () => this.trustedSet.updateDelegatedTargets(bytesData, role, parentRole));
212 // Client workflow 5.6.7: persist targets metadata file
213- this.persistMetadata(role, bytesData);
214+ await this.persistMetadata(role, bytesData);
215 }
216 catch (error) {
217- throw new error_1.RuntimeError(`Unable to load targets error ${error}`);
218+ throw new error_1.RuntimeError('Unable to load targets metadata', { cause: error });
219 }
220 }
221 return this.trustedSet.getRole(role);
222@@ -358,15 +371,31 @@ class Updater {
223 const filePath = encodeURIComponent(targetInfo.path);
224 return path.join(this.targetDir, filePath);
225 }
226- persistMetadata(metaDataName, bytesData) {
227+ verifyMetadata(role, operation) {
228+ try {
229+ return operation();
230+ }
231+ catch (error) {
232+ // Equal timestamp versions retain the existing normal no-new-metadata path.
233+ if (error instanceof error_1.EqualVersionError) {
234+ throw error;
235+ }
236+ throw new error_1.MetadataVerificationError(role, `Failed to verify metadata ${encodeURIComponent(role)}`, { cause: error });
237+ }
238+ }
239+ async persistMetadata(metaDataName, bytesData) {
240 const encodedName = encodeURIComponent(metaDataName);
241 try {
242+ // The observer owns its copy and must finish before this cache or the next role advances.
243+ if (this.onVerifiedMetadata) {
244+ await this.onVerifiedMetadata(metaDataName, Buffer.from(bytesData));
245+ }
246 const filePath = path.join(this.dir, `${encodedName}.json`);
247 log('WRITE %s', filePath);
248 fs.writeFileSync(filePath, bytesData.toString('utf8'));
249 }
250 catch (error) {
251- throw new error_1.PersistError(`Failed to persist metadata ${encodedName} error: ${error}`);
252+ throw new error_1.PersistError(`Failed to persist metadata ${encodedName}`, { cause: error });
253 }
254 }
255 }
tests/fixtures/updates/qualify-candidates.tsmodified+9-3
Changes to tests/fixtures/updates/qualify-candidates.ts
@@ -7,7 +7,7 @@ import { platform, repository, target } from './repository'
77
88async function hash(path: string) { return createHash('sha256').update(await readFile(path)).digest('hex') }
99
10/** Real copied commands, signed synthetic bytes and optional retained older binary. Never executes a candidate. */
10/** Real copied commands, signed synthetic bytes and optional retained format-1-only binary. Never executes a candidate. */
1111export async function qualifyPortableCandidates(directory: string, source: string, olderBinary?: string) {
1212 await mkdir(directory, { mode: 0o700 })
1313 const binary = join(directory, 'drone'), sourceSha256 = await hash(source)
@@ -38,7 +38,13 @@ export async function qualifyPortableCandidates(directory: string, source: strin
3838 const empty = await run(['updates:candidates', store, ...pinned])
3939 assert.equal(empty.stateFormat, 1); assert.equal(empty.candidate, null)
4040 assert.equal(await hash(join(store, 'state.sqlite')), initialHash)
41 if (olderBinary) assert.deepEqual(await run(['updates:store', store, ...pinned], 0, olderBinary), created)
41 if (olderBinary) {
42 // A format-1 predecessor cannot emit the later, empty maintenance status field.
43 const { maintenanceHandoffs, ...format1Status } = created
44 assert.deepEqual(maintenanceHandoffs, [])
45 assert.deepEqual(await run(['updates:store', store, ...pinned], 0, olderBinary), format1Status)
46 assert.equal(await hash(join(store, 'state.sqlite')), initialHash)
47 }
4248 await run(['updates:reserve', store, ...pinned, '--repository', repo], 1)
4349 const prepared = await run(['updates:reserve', store, ...pinned, ...repositoryFlags])
4450 assert.equal(prepared.stateFormat, 2); assert.equal(prepared.store.releaseFloor, 9)
@@ -85,6 +91,6 @@ export async function qualifyPortableCandidates(directory: string, source: strin
8591 return { evidence: 'copied-standalone-owned-candidates-v1', operations: results.length, sourceSha256, olderSha256, physicalDeploymentAllowed: false }
8692}
8793if (import.meta.main) {
88 if (process.argv.length !== 4 && process.argv.length !== 5) throw new Error('Use qualify-candidates.ts NEW_DIRECTORY COMPILED_BINARY [OLDER_BINARY]')
94 if (process.argv.length !== 4 && process.argv.length !== 5) throw new Error('Use qualify-candidates.ts NEW_DIRECTORY COMPILED_BINARY [FORMAT1_BINARY]')
8995 console.log(JSON.stringify(await qualifyPortableCandidates(resolve(process.argv[2]!), resolve(process.argv[3]!), process.argv[4] ? resolve(process.argv[4]) : undefined)))
9096}