also looking at this
feat: import LAS surfaces and consume maintained source releases
#107
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.
| @@ -18,6 +18,8 @@ This is the `stacksjs/drone` Stacks application. Read `README.md` for implemente | ||
| 18 | 18 | - Imported TypeScript modules explicitly import their dependencies. Browser entry bindings do not leak into modules. |
| 19 | 19 | - `better-dx` supplies shared dev tooling; do not redeclare its tools. Keep Bun's hoisted linker. |
| 20 | 20 | - 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. | |
| 21 | 23 | - No em-dash or separator en-dash in newly authored visible copy. |
| 22 | 24 | - Before finishing run `./buddy lint`, `./buddy typecheck`, `./buddy test` and relevant browser scenarios. |
| 23 | 25 | - Test behavioral seams: decision input/output, controller lifecycle, real local UDP and protocol fixtures. |
| @@ -25,7 +25,7 @@ | ||
| 25 | 25 | "@tufjs/canonical-json": "2.0.0", |
| 26 | 26 | "stacks": "^0.75.221", |
| 27 | 27 | "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", | |
| 29 | 29 | }, |
| 30 | 30 | "devDependencies": { |
| 31 | 31 | "@stacksjs/query-builder": "0.75.221", |
| @@ -38,12 +38,8 @@ | ||
| 38 | 38 | "version": "0.75.221", |
| 39 | 39 | }, |
| 40 | 40 | }, |
| 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 | }, | |
| 45 | 41 | "overrides": { |
| 46 | "@stacksjs/bunpress": "0.2.22", | |
| 42 | "@stacksjs/bunpress": "0.2.25", | |
| 47 | 43 | }, |
| 48 | 44 | "packages": { |
| 49 | 45 | "@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 @@ | ||
| 94 | 90 | |
| 95 | 91 | "@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=="], |
| 96 | 92 | |
| 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=="], | |
| 98 | 94 | |
| 99 | 95 | "@stacksjs/cache": ["@stacksjs/cache@0.75.221", "", { "dependencies": { "@stacksjs/ts-cache": "^0.1.7" } }, "sha512-D4B2tkif9FISL+5xaMff1Ceu2A1iSlMPLyLbgHGzWuqp2mR8KoB/8Ilpk5uq0Bd/xTf63U9opxkQvKlOr1jkTA=="], |
| 100 | 96 | |
| @@ -466,7 +462,7 @@ | ||
| 466 | 462 | |
| 467 | 463 | "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=="], |
| 468 | 464 | |
| 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=="], | |
| 470 | 466 | |
| 471 | 467 | "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=="], |
| 472 | 468 | |
| @@ -0,0 +1,43 @@ | ||
| 1 | # Dependency source releases | |
| 2 | ||
| 3 | Dependencies must come from maintained source releases. Dependency patches and | |
| 4 | edits to installed package files are prohibited. Fix and test the owning source | |
| 5 | repository, run its `bun run release:patch` command, then qualify the released | |
| 6 | package in this application with a native frozen install. | |
| 7 | ||
| 8 | ## Maintained TUF client | |
| 9 | ||
| 10 | The `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). | |
| 12 | The source is maintained in [stacksjs/tuf-js](https://github.com/stacksjs/tuf-js), | |
| 13 | with release source commit `40a7992babc29eb99c3ee18290ef7aa438421c08`. | |
| 14 | This is a maintained fork of the MIT-licensed upstream client, distinct from the | |
| 15 | upstream npm package. Its original license is retained in the installed package | |
| 16 | and `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) | |
| 18 | retains the original attribution. | |
| 19 | ||
| 20 | The release publishes its package archive, `SHA512SUMS` and `source.json`. | |
| 21 | Verify the immutable release identity, source commit and archive integrity before | |
| 22 | installation. Retain the exact release URL and frozen lockfile. The existing | |
| 23 | `tuf-js` import name resolves to the scoped maintained package through that | |
| 24 | dependency key. The SDK source repository's CLI and conformance harness use the | |
| 25 | scoped name explicitly. | |
| 26 | ||
| 27 | The observer, copied reference time and typed failure contracts are documented | |
| 28 | in [runtime updates](runtime-updates.md#maintained-sdk-persistence-seam). Do not | |
| 29 | remove their verification or persistence behavior during an upgrade. Qualify | |
| 30 | SDK observer cases, enrollment, persistent store/recovery, interrupted updates, | |
| 31 | rollback floors and copied standalone CLI campaigns against the actual release. | |
| 32 | Do not claim physical firmware deployment from these synthetic tests. | |
| 33 | ||
| 34 | ## Documentation engine | |
| 35 | ||
| 36 | BunPress 0.2.25 includes the deployed-path search fix from | |
| 37 | [source commit 096004cc6e7b4800bfd1e90993866c9011ca706d](https://github.com/stacksjs/bunpress/commit/096004cc6e7b4800bfd1e90993866c9011ca706d). | |
| 38 | Consume the source release rather than editing installed files. The public | |
| 39 | search regressions in `tests/unit/docs-search.test.ts`, native documentation | |
| 40 | build and actual browser search selection qualify the application integration. | |
| 41 | ||
| 42 | [Issue #108](https://github.com/stacksjs/drone/issues/108) tracks source-release | |
| 43 | migration and downstream qualification evidence. | |
| @@ -480,7 +480,7 @@ See the [host slot decision](adr/0029-host-slot-health.md). | ||
| 480 | 480 | |
| 481 | 481 | Use a standard TUF repository with `metadata/` and `targets/` directories, |
| 482 | 482 | including 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 | |
| 484 | 484 | rotation, signature thresholds, expiration, inter-role versions/hashes and |
| 485 | 485 | target length/hashes. Standalone staging/inspection use a private, randomly |
| 486 | 486 | addressed loopback server supplying bounded local files to the public HTTP |
| @@ -496,10 +496,12 @@ expiry comparisons; malformed or automatically normalized dates are rejected. | ||
| 496 | 496 | |
| 497 | 497 | ### Maintained SDK persistence seam |
| 498 | 498 | |
| 499 | This repository applies `patches/tuf-js@6.0.0.patch` through Bun's root | |
| 500 | `patchedDependencies`. It extends the pinned MIT-licensed client; these are | |
| 501 | repository-maintained options, not options supplied by unmodified upstream 6.0.0. | |
| 502 | The patch retains upstream signature, role and target verification. | |
| 499 | This repository consumes the immutable maintained source release through the | |
| 500 | `tuf-js` dependency key, with no distribution patches. These are maintained fork | |
| 501 | options, not options supplied by unmodified upstream 6.0.0. The source client | |
| 502 | retains upstream signature, role and target verification. See the | |
| 503 | [source release policy](dependency-source-releases.md) for source identity, | |
| 504 | original MIT attribution, release integrity and upgrade qualification. | |
| 503 | 505 | |
| 504 | 506 | `onVerifiedMetadata(role, bytes)` receives an owned byte copy after the SDK |
| 505 | 507 | accepts remote metadata. The SDK awaits it before writing that cache file or |
Renamed from patches/tuf-js.LICENSE. The contents are unchanged.
| @@ -50,7 +50,7 @@ | ||
| 50 | 50 | "@stacksjs/validation": "^0.75.221", |
| 51 | 51 | "stacks": "^0.75.221", |
| 52 | 52 | "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", | |
| 54 | 54 | "@tufjs/canonical-json": "2.0.0" |
| 55 | 55 | }, |
| 56 | 56 | "workspaces": [ |
| @@ -81,10 +81,6 @@ | ||
| 81 | 81 | "url": "https://github.com/stacksjs/drone/issues" |
| 82 | 82 | }, |
| 83 | 83 | "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" | |
| 89 | 85 | } |
| 90 | 86 | } |
| @@ -1,45 +0,0 @@ | ||
| 1 | diff --git a/dist/chunk-8rsrw3sw.js b/dist/chunk-8rsrw3sw.js | |
| 2 | index 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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}; | |
| 18 | diff --git a/node_modules/@stacksjs/bunpress/dist/search-base-path.js b/dist/search-base-path.js | |
| 19 | new file mode 100644 | |
| 20 | index 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 | +} | |
| @@ -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 | |
| 7 | commit `c30653d07f0c41fbe5a2030d25eca6fd04f8547f`. The original MIT copyright and permission | |
| 8 | notice is retained in `tuf-js.LICENSE` and in the installed package. | |
| 9 | ||
| 10 | The patch adds an awaited, copy-owning `onVerifiedMetadata` observer before the | |
| 11 | SDK 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 | |
| 13 | time. Persistence failure aborts rather than being interpreted as a missing | |
| 14 | remote root or a cache miss under `forceCache`. Discard failed updater instances; | |
| 15 | their in-memory trust set is not rolled back. It does not make cache writes | |
| 16 | transactional or implement a protected trust store. Public operational error | |
| 17 | exports retain causes and identify remote verification, persistence and download | |
| 18 | phases. Verification errors identify an attempted role, not permission to discard | |
| 19 | state. Classify persistence first, including when an observer throws a | |
| 20 | verification error; unknown causes require maintenance rather than an assumed | |
| 21 | invalid-input verdict. | |
| 22 | Wrapper messages never coerce caught values, so opaque observer rejections keep | |
| 23 | their persistence classification and exact cause under cache-preferred refresh. | |
| 24 | ||
| 25 | Bun applies the patch through root `patchedDependencies` and the lockfile. | |
| 26 | Always prepare with `bun patch tuf-js@6.0.0` before editing an installed package, | |
| 27 | then generate changes with `bun patch --commit tuf-js@6.0.0`. Do not edit shared | |
| 28 | package caches. Exclude empty generated `.bun-tag-*` entries from the patch; | |
| 29 | they are installer cache markers rather than package source. For upgrades, review the upstream change against every patch | |
| 30 | hunk and rerun the SDK observer cases, update/enrollment regressions, clean | |
| 31 | frozen installation and copied standalone CLI qualification. Never silently | |
| 32 | carry a patch onto another version. | |
| 33 | ||
| 34 | See `docs/runtime-updates.md` for the trust and clock limits. The extensions are | |
| 35 | maintained 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 | |
| 40 | BunPress'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 | |
| 42 | result navigated to a 404. The patch applies the same base-path resolution after | |
| 43 | locale translation and fallback selection, preserving anchors and root-hosted | |
| 44 | sites. A real source directory named `docs` still receives the deployment prefix. | |
| 45 | ||
| 46 | The patch changes the native index builder and adds a small helper matching the renderer's path resolution; | |
| 47 | it adds no browser redirect or application search implementation. The installed | |
| 48 | package retains BunPress's MIT license. Regression coverage lives in | |
| 49 | `tests/unit/docs-search.test.ts`. Rebuild native docs and verify an actual search | |
| 50 | selection 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 | ||
| 53 | The shared engine fix is [BunPress PR 91](https://github.com/stacksjs/bunpress/pull/91), | |
| 54 | source commit `096004cc6e7b4800bfd1e90993866c9011ca706d`. Remove the compatibility | |
| 55 | patch only after upgrading to a release that includes it and repeating the checks. | |
| @@ -1,255 +0,0 @@ | ||
| 1 | diff --git a/dist/error.d.ts b/dist/error.d.ts | |
| 2 | index 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 { | |
| 16 | diff --git a/dist/error.js b/dist/error.js | |
| 17 | index 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 | } | |
| 36 | diff --git a/dist/index.d.ts b/dist/index.d.ts | |
| 37 | index 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'; | |
| 45 | diff --git a/dist/index.js b/dist/index.js | |
| 46 | index 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; } }); | |
| 60 | diff --git a/dist/store.d.ts b/dist/store.d.ts | |
| 61 | index 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; | |
| 73 | diff --git a/dist/store.js b/dist/store.js | |
| 74 | index 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 | } | |
| 89 | diff --git a/dist/updater.d.ts b/dist/updater.d.ts | |
| 90 | index 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 | } | |
| 119 | diff --git a/dist/updater.js b/dist/updater.js | |
| 120 | index 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 | } | |
| @@ -7,7 +7,7 @@ import { platform, repository, target } from './repository' | ||
| 7 | 7 | |
| 8 | 8 | async function hash(path: string) { return createHash('sha256').update(await readFile(path)).digest('hex') } |
| 9 | 9 | |
| 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. */ | |
| 11 | 11 | export async function qualifyPortableCandidates(directory: string, source: string, olderBinary?: string) { |
| 12 | 12 | await mkdir(directory, { mode: 0o700 }) |
| 13 | 13 | const binary = join(directory, 'drone'), sourceSha256 = await hash(source) |
| @@ -38,7 +38,13 @@ export async function qualifyPortableCandidates(directory: string, source: strin | ||
| 38 | 38 | const empty = await run(['updates:candidates', store, ...pinned]) |
| 39 | 39 | assert.equal(empty.stateFormat, 1); assert.equal(empty.candidate, null) |
| 40 | 40 | 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 | } | |
| 42 | 48 | await run(['updates:reserve', store, ...pinned, '--repository', repo], 1) |
| 43 | 49 | const prepared = await run(['updates:reserve', store, ...pinned, ...repositoryFlags]) |
| 44 | 50 | assert.equal(prepared.stateFormat, 2); assert.equal(prepared.store.releaseFloor, 9) |
| @@ -85,6 +91,6 @@ export async function qualifyPortableCandidates(directory: string, source: strin | ||
| 85 | 91 | return { evidence: 'copied-standalone-owned-candidates-v1', operations: results.length, sourceSha256, olderSha256, physicalDeploymentAllowed: false } |
| 86 | 92 | } |
| 87 | 93 | if (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]') | |
| 89 | 95 | console.log(JSON.stringify(await qualifyPortableCandidates(resolve(process.argv[2]!), resolve(process.argv[3]!), process.argv[4] ? resolve(process.argv[4]) : undefined))) |
| 90 | 96 | } |