@wenathlan/saddle 1.8.5 → 1.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <strong>Storage-backed jobs, scraping contracts and portable runners for Node.js.</strong><br/>
9
9
  <strong>Binary computing agent, agent browser, computer-use, scraper and packager.</strong><br/>
10
10
  <a href="https://github.com/wenathlan/saddle/actions/workflows/ci.yml"><img src="https://github.com/wenathlan/saddle/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
11
- <a href="https://github.com/wenathlan/saddle/releases/tag/v1.8.4"><img src="https://img.shields.io/badge/release-v1.8.4-d35d3d" alt="Release 1.8.4" /></a>
11
+ <a href="https://github.com/wenathlan/saddle/releases/tag/v1.8.6"><img src="https://img.shields.io/badge/release-v1.8.6-d35d3d" alt="Release 1.8.6" /></a>
12
12
  <a href="https://github.com/wenathlan/saddle/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-202a2f" alt="GPL 3.0 license" /></a>
13
13
  </p>
14
14
 
@@ -20,7 +20,7 @@ Ships as a library, CLI, binary, n8n node, CRX extension, Android/iOS and Tauri
20
20
 
21
21
  ## Start here
22
22
 
23
- Saddle requires **Node.js 22 or newer**.
23
+ Saddle requires **Node.js 26.7.0 or newer**.
24
24
 
25
25
  ```bash
26
26
  npm install @wenathlan/saddle
@@ -109,7 +109,7 @@ The caller still chooses how to provide `fetcher`, browser transport, persistenc
109
109
 
110
110
  ## Browser extension
111
111
 
112
- Version 1.8.4 includes a pure JavaScript Manifest V3 reference surface in [`extension/`](extension/). It is deliberately narrow: the user invokes the action, the popup sends a versioned command, the service worker routes it, and an isolated content bridge returns bounded page metadata, visible text or a user initiated action result. The exported `permissionpolicy` keeps base permissions minimal and makes optional escalation caller-owned.
112
+ Version 1.8.6 includes a pure JavaScript Manifest V3 reference surface in [`extension/`](extension/), a read-only page-world `pagefacts` boundary, snapshot diffs and persisted browser context metadata. It is deliberately narrow: the user invokes the action, the popup sends a versioned command, the service worker routes it, and an isolated content bridge returns bounded page metadata, visible text or a user initiated action result. The exported `permissionpolicy` keeps base permissions minimal and makes optional escalation caller-owned.
113
113
 
114
114
  ```bash
115
115
  # load the unpacked extension from chrome://extensions
@@ -145,12 +145,12 @@ saddle mcp
145
145
 
146
146
  | Registry | Artifact | Workflow | Status |
147
147
  | --- | --- | --- | --- |
148
- | GitHub npm | `@wenathlan/saddle@1.8.5` | publishgithubnpm.yml | pending release |
149
- | GHCR | `ghcr.io/wenathlan/saddle:1.8.5` and `latest` | publishghcr.yml | pending release |
150
- | Maven | `io.wenathlan:saddle:1.8.5` | publishmaven.yml | pending release |
151
- | NuGet | `Saddle.1.8.5.nupkg` | publishnuget.yml | pending release |
152
- | RubyGems | `saddle 1.8.5` | publishrubygems.yml | pending release |
153
- | npmjs | `@wenathlan/saddle@1.8.5` | publishnpmjs.yml | pending release |
148
+ | GitHub npm | `@wenathlan/saddle@1.8.6` | publishgithubnpm.yml | pending release |
149
+ | GHCR | `ghcr.io/wenathlan/saddle:1.8.6` and `latest` | publishghcr.yml | pending release |
150
+ | Maven | `io.wenathlan:saddle:1.8.6` | publishmaven.yml | pending release |
151
+ | NuGet | `Saddle.1.8.6.nupkg` | publishnuget.yml | pending release |
152
+ | RubyGems | `saddle 1.8.6` | publishrubygems.yml | pending release |
153
+ | npmjs | `@wenathlan/saddle@1.8.6` | publishnpmjs.yml | pending release |
154
154
 
155
155
  ## Development
156
156
 
@@ -188,7 +188,7 @@ Root-based JavaScript ESM layout, no src/ directory, no TypeScript build require
188
188
 
189
189
  ## Current scope
190
190
 
191
- Version 1.8.5 extends the 1.8.4 engine contracts with package metadata for Node.js 26.7.0 and npm 12, an optional Playwright peer, and an explicit Node-only `browser-playwright` adapter. Native runtimes, browser binaries, n8n host registration, provider credentials, persistent databases and production deployment remain caller-selected adapters. The next improvements should extend these contracts without coupling the core to one forge, registry, browser or storage vendor.
191
+ Version 1.8.6 extends the 1.8.5 engine contracts with isolated page facts, extension snapshot diffs, persisted browser context metadata, deterministic release assets and root-based Pages deployment. Native runtimes, browser binaries, n8n host registration, provider credentials, persistent databases and production deployment remain caller-selected adapters. The next improvements should extend these contracts without coupling the core to one forge, registry, browser or storage vendor.
192
192
 
193
193
  ## License
194
194
 
@@ -0,0 +1,29 @@
1
+ # Actions and Pages incident
2
+
3
+ This note records the GitHub-hosted failures observed while debugging the root web migration on 2026-08-13. The repository is public, the default branch is `main`, and the GitHub API reported `has_pages: false` before Pages configuration.
4
+
5
+ ## observed failures
6
+
7
+ | Surface | Run | Evidence |
8
+ | --- | --- | --- |
9
+ | GitHub Pages | [31703685525](https://github.com/wenathlan/saddle/actions/runs/31703685525) | `actions/configure-pages@v5` returned `HttpError: Not Found`; the repository Pages site was not enabled/configured. |
10
+ | CI | [31703407943](https://github.com/wenathlan/saddle/actions/runs/31703407943) | The historical workflow invoked `npm run build`, which is not a root package script. |
11
+ | Uka tests | [31702858313](https://github.com/wenathlan/saddle/actions/runs/31702858313) | The historical workflow invoked `npm run typecheck`, which is not a root package script. |
12
+ | CI | [31702858281](https://github.com/wenathlan/saddle/actions/runs/31702858281) | The historical workflow invoked both `npm run typecheck` and `npm run build`, which are not the engine gates. |
13
+ | Dependabot | [31701834922](https://github.com/wenathlan/saddle/actions/runs/31701834922) | Dependabot attempted `/web/package.json`, which was intentionally removed during the root migration. |
14
+
15
+ ## correction contract
16
+
17
+ The canonical npm manifest and lockfile are at the repository root. Engine workflows must use `npm ci`, `npm run check`, `npm run formatcheck`, `npm test` and `npm run pack:check`. The web Pages workflow must use `npm run web:check`, `npm run web:build:pages` and publish `web/dist/public`. Dependabot must use the `npm` ecosystem at `/`.
18
+
19
+ The Pages workflow also requires the repository owner to enable GitHub Pages and select **GitHub Actions** as its source. The workflow cannot create the Pages site when the repository API still reports `has_pages: false`; that setting is managed at the repository level, not by the static artifact build.
20
+
21
+ After selecting **GitHub Actions** in the repository settings, the Pages API reported `has_pages: true`, `build_type: workflow`, source `main` and the public URL `https://wenathlan.github.io/saddle/`. The corrected build/deploy workflow then completed successfully in [run 31705301175](https://github.com/wenathlan/saddle/actions/runs/31705301175), with `configure pages`, `web:check`, build, artifact upload and deploy all green.
22
+
23
+ The final workflow commits also passed [Saddle engine CI run 31705367442](https://github.com/wenathlan/saddle/actions/runs/31705367442), [Uka-tests run 31705367433](https://github.com/wenathlan/saddle/actions/runs/31705367433) and [cross-runtime compatibility run 31705367432](https://github.com/wenathlan/saddle/actions/runs/31705367432). All three completed with `success` on `main`.
24
+
25
+ The repository metadata audit found `main` as the default branch and one open Dependabot pull request (`#4`, branch `dependabot/npm_and_yarn/npm_and_yarn-2772e86c4e`). Its only commit (`4cb2a45`) changes the root package lock plus an unrelated nested `scrape/package.json` and `scrape/package-lock.json`; it does not represent a second production branch and is not safe to merge into the root release. The branch should be closed as an obsolete automated update after owner confirmation.
26
+
27
+ The repository homepage About editor was opened in the authenticated browser and saved with the canonical Saddle description. The repository API should now report that description instead of the placeholder `saddle`.
28
+
29
+ The six non-main Dependabot tips were preserved as `archive-dependabot-*` tags, their PRs were closed, and their branch refs were removed. The cleanup leaves `main` as the only active branch without deleting the archived commit objects.
@@ -0,0 +1,21 @@
1
+ # Branch audit
2
+
3
+ This record preserves the branch tips and comparison results collected before the branch cleanup. Deleting a branch reference does not delete the commit objects or the tags that retain them; the hashes below remain available for audit and recovery while repository retention permits.
4
+
5
+ | Branch | Tip commit | Ahead of `main` | Behind `main` | Unique change | Action |
6
+ | --- | --- | ---: | ---: | --- | --- |
7
+ | `main` | `04acff11e7f3c0bc830fdaaf1fc7bad9bdf1fcfa` | 0 | 0 | Current release line | Keep |
8
+ | `dependabot/npm_and_yarn/lucide-react-1.31.0` | `45498007cc539d2c3df3d801cfb23dfdf575d859` | 1 | 11 | `package.json`, `package-lock.json` | Close PR #9 and delete branch |
9
+ | `dependabot/npm_and_yarn/multi-b251156d90` | `cff1abfbcfe6fffae1a5517082c76b9f3e6195ea` | 1 | 11 | `package.json`, `package-lock.json` | Close PR #7 and delete branch |
10
+ | `dependabot/npm_and_yarn/npm_and_yarn-2772e86c4e` | `4cb2a45cc8dd1bc53b05b5ffe8eeb07c1c46bc1c` | 1 | 13 | Root package files plus obsolete `scrape/` package files | Close PR #4 and delete branch |
11
+ | `dependabot/npm_and_yarn/react-resizable-panels-4.12.2` | `8dfa1d1999c6cf0b80d39f54c1194499c2943739` | 1 | 11 | `package.json`, `package-lock.json` | Close PR #5 and delete branch |
12
+ | `dependabot/npm_and_yarn/streamdown-2.5.0` | `ac04d2d01c2caa3c0bb9a8b7e8b952fd0af4b2d6` | 1 | 11 | `package.json`, `package-lock.json` | Close PR #6 and delete branch |
13
+ | `dependabot/npm_and_yarn/typescript-7.0.2` | `7d5d3f7b70d28ed2663a6d9b2d4f8a3280571525` | 1 | 11 | `package.json`, `package-lock.json` | Close PR #8 and delete branch |
14
+
15
+ All six non-main branches are single-commit Dependabot updates based on an older main tip. None is a second production line, and none should be merged blindly after the root web migration. The current `main` line keeps the release work; the branch tips above remain referenced by this audit until the remote branch refs are removed.
16
+
17
+ The cleanup was completed by creating the six `archive-dependabot-*` tags, closing PRs #4 through #9, and deleting the six non-main branch refs. The repository is now intentionally reduced to `main`; the archive tags preserve the exact tips listed above.
18
+
19
+ ## Checks interpretation
20
+
21
+ The check count changed because the workflow set changed during the root migration. Duplicate and obsolete workflow definitions were removed, while the canonical engine CI, Uka-tests, cross-runtime compatibility and Pages workflows were retained. The final `main` workflow set passed the current checks; a lower count therefore reflects fewer active check definitions, not a loss of commits or a failed release line.
@@ -26,13 +26,13 @@ The engine owns **contracts, validation, orchestration, recovery and auditabilit
26
26
  | --- | --- | --- | --- |
27
27
  | 1 | audit and governance | active | gap matrix, sources, claims reconciled |
28
28
  | 2 | browser agent foundation | complete | snapshots, refs, stale errors, tabs, frames, action results and context-aware replay provenance tests |
29
- | 3 | extension runtime | first slice complete | MV3 unpacked surface, protocol, worker, content bridge and tests |
29
+ | 3 | extension runtime | first slice complete | MV3 unpacked surface, protocol, worker, isolated content bridge, read-only page boundary and tests |
30
30
  | 4 | working set and storage | complete | range chunks, content dedupe, tiered cache, conflict sync and memory capabilities |
31
31
  | 5 | runners and execution | complete | provider health, triggers, cancellation, heartbeat and resumable runs |
32
32
  | 6 | scraping and context | complete | semantic extraction, content-type normalization, crawl budgets, RAG lineage and low-cardinality metrics |
33
33
  | 7 | API, MCP and security | complete | request identity, optional auth, secure headers, browser MCP tools and redirect/DNS checks |
34
34
  | 8 | bots and integrations | complete | app lifecycle, command scopes, idempotency, delivery retries and dead letters |
35
- | 9 | packaging and distribution | extension zip and registry slice prepared for 1.8.2 | desktop, mobile, n8n and binary artifacts remain caller-owned |
35
+ | 9 | packaging and distribution | extension zip, checksum, SBOM and provenance slice implemented | desktop, mobile, n8n and binary artifacts remain caller-owned |
36
36
  | 10 | product surfaces and operations | first slice complete | desktop, mobile, n8n and operator control contracts; observability, retention and threat model remain |
37
37
  | 11 | cross-runtime compatibility | transport-neutral graph audit complete | Node, Bun and Deno root probe, browser worker bridge, extension permission/build checks and package graph audit |
38
38
  | 12 | release gates | active | deterministic checks, docs, clean diffs and claim/code parity |
@@ -52,7 +52,7 @@ Each block follows the same loop:
52
52
 
53
53
  ## current implementation
54
54
 
55
- Version 1.8.2 carries the public npm identity migration through the transferred `wenathlan` repository owner, aligns GitHub Packages npm, Maven and GHCR owner metadata, and includes the minimal extension permission policy, deterministic extension zip workflow, context-aware replay, transport-neutral graph audit and bounded content normalization. Registry publication is triggered only after the release tag and independent target checks. The first product surface slice adds desktop, mobile, n8n, operator control, operational policy and framework-neutral HTTP contracts. The first cross-runtime slice validates the root on Node, Bun and Deno, while extension-context and browser bundler checks remain caller-owned.
55
+ Version 1.8.6 carries the public npm identity migration through the transferred `wenathlan` repository owner, aligns GitHub Packages npm, Maven and GHCR owner metadata, and includes the minimal extension permission policy, deterministic extension zip workflow, context-aware replay, transport-neutral graph audit, bounded content normalization, isolated page facts, snapshot context diffs, reproducible release assets and root-based Pages deployment. Registry publication is triggered only after the release tag and independent target checks. The first product surface slice adds desktop, mobile, n8n, operator control, operational policy and framework-neutral HTTP contracts. The first cross-runtime slice validates the root on Node, Bun and Deno, while extension-context and browser bundler checks remain caller-owned.
56
56
 
57
57
  ## evidence sources
58
58
 
@@ -51,7 +51,7 @@ This audit compares the current repository with the project README and the addit
51
51
  | GitHub Packages and GHCR | missing | no package or image publishing jobs |
52
52
  | Maven, NuGet, RubyGems, PyPI | missing | no language specific wrappers or trusted publishing jobs |
53
53
  | jsDelivr, UNPKG, esm.sh | partial | URLs are documented; no release verification or SRI manifest |
54
- | browser extension | partial | surface manifest only; no CRX source or permissions manifest |
54
+ | browser extension | partial | MV3 source, minimal permissions, isolated page boundary, snapshot diffs and unpacked zip build exist; CRX signing and cross-browser profiles remain caller-owned |
55
55
  | mobile and desktop apps | partial | target manifests only; no Tauri, Capacitor, Android, or iOS project |
56
56
  | site and per site database | missing | no `web` application, Hono server, Drizzle schema, Prisma schema, or site deployment adapter |
57
57
  | multi platform app identity | partial | generic bot and forge contracts; no OAuth or GitHub App installation flow |
package/docs/gapmatrix.md CHANGED
@@ -15,15 +15,15 @@ This matrix turns the supplied README and conclusions into implementation decisi
15
15
 
16
16
  | Area | Current state | Gap | Priority | Decision |
17
17
  | --- | --- | --- | --- | --- |
18
- | Root library | Implemented ESM entry point with broad exports | No extension subpath or extension files are shipped | P0 | Add a small `extension/` package surface and export only serializable contracts |
18
+ | Root library | Implemented ESM entry point with broad exports and an extension subpath | Browser-specific packaging and cross-browser profiles remain adapter-owned | P2 | Keep serializable extension contracts separate from browser binaries |
19
19
  | Browser agent | Implemented injected action adapter for navigate, click, type, screenshot, DOM, title, scroll and command batches | Vendor-neutral action results and bounded action batches are public; optional Playwright provider is isolated behind `browser-playwright` | P1 | Keep the adapter boundary; browser binaries and credentials remain caller-owned |
20
20
  | Browser snapshots | Implemented public contract | Snapshot ids, bounded elements, stable refs, stale checks and diffs are covered by deterministic tests | P0 | Reuse the contract from MCP and extension transport |
21
21
  | Session replay | Implemented | Replay restores caller-owned window, tab and frame context before actions; context identifiers are validated and counted | P1 | Keep browser selection and restoration in injected adapters |
22
- | Extension runtime | Surface is only declared in `surfaces/manifest.js` and `surfaces/targets.js` | No Manifest V3 manifest, service worker, content bridge, popup or build artifact | P0 | Implement a pure JavaScript MV3 reference surface with minimal permissions |
23
- | Extension messaging | Not implemented | No versioned envelope, correlation id, timeout, sender metadata or error response contract | P0 | Add transport-neutral message contracts and Chrome runtime adapter |
22
+ | Extension runtime | Manifest V3 manifest, service worker, content bridge, popup, snapshot diffs, context metadata and deterministic build artifact are implemented | Cross-browser manifests remain unbundled | P2 | Keep a pure JavaScript MV3 reference surface with minimal permissions |
23
+ | Extension messaging | Versioned envelope, correlation id, timeout, sender metadata and error response contract are implemented | Long-lived ports remain caller-owned | P1 | Keep transport-neutral messages and explicit runtime adapters |
24
24
  | Service worker resilience | Implemented contract slice | Pending command envelopes, attempt metadata and snapshot summaries persist through injected storage; resume remains explicit and user-owned | P0 | Rehydrate metadata on startup and never replay a command without an explicit caller action |
25
25
  | Permissions | Contract slice | `permissionpolicy` keeps the base permissions minimal and optional escalation caller-owned | P0 | Start with `storage` and no broad host permissions; make host access caller-configured |
26
- | Content isolation | Not implemented | No isolated-world DOM bridge or page-to-extension boundary | P0 | Add a narrow content script that reports page facts through the message contract |
26
+ | Content isolation | Implemented read-only page-to-extension boundary | The page world cannot invoke extension commands; richer page facts remain bounded | P1 | Keep `pagebridge.js` read-only, token-correlated and caller-controlled |
27
27
  | Task agent | Partial | Jobs, workflows and bot commands exist, but no browser task planner or tool registry | P1 | Reuse workflow, trigger and bot contracts; add browser task commands only after snapshots |
28
28
  | MCP | Implemented scrape, crawl, batch, extract and serialize tools with JSON-RPC handling | No browser snapshot or browser action MCP tools | P1 | Add browser tools as an optional adapter over the same snapshot/action contracts |
29
29
  | API security | Implemented URL protocol and private hostname/IP checks | Request envelopes, optional authorization, security headers, redirect bounds and injected DNS resolution checks are now available | P0 | Keep credentials caller-owned and reject private or rebinding targets before transport |
@@ -40,7 +40,7 @@ This matrix turns the supplied README and conclusions into implementation decisi
40
40
  | Auth profiles | Session file and replay contracts exist | No extension profile or consent model | P1 | Defer cookie/profile export; support explicit user-owned session references only |
41
41
  | CAPTCHA | Contract, guard and evidence exist | No automatic solver integration | deferred | Keep external/manual solver boundary; do not promise bypass in the extension |
42
42
  | Stealth | Fingerprint contract exists | No automatic stealth patching | deferred | Keep opt-in fingerprint metadata; no hidden anti-detection behavior |
43
- | Packaging | npm, GHCR, Maven, NuGet, RubyGems and extension zip workflows are live | Desktop, mobile, n8n and binary release artifacts remain caller-owned | P1 | Keep non-JavaScript artifacts in explicit adapters and release jobs |
43
+ | Packaging | npm, GHCR, Maven, NuGet, RubyGems, extension zip, deterministic SHA256SUMS, CycloneDX SBOM and in-toto-shaped provenance assets are implemented | Desktop, mobile, n8n and binary release artifacts remain caller-owned | P1 | Keep non-JavaScript artifacts in explicit adapters and release jobs |
44
44
  | Mobile and desktop apps | Contract slice | Desktop/mobile manifests and caller-owned adapter contracts exist; no native project is bundled | P1 | Keep native projects caller-owned and add runtime conformance tests incrementally |
45
45
  | n8n surface | Contract slice | Node metadata, trigger matching and declared action execution exist; no n8n host package is bundled | P1 | Keep node registration and credentials caller-owned |
46
46
  | Cross-browser | Target profile declares browser and extension | Firefox, Edge or Safari manifests remain unbundled; transport-neutral export graph is statically audited for Node-only imports | P2 | Keep WebExtension-compatible contracts and add browser adapters incrementally |
@@ -59,6 +59,7 @@ The first code slice targeted the P0 rows only. It now contains:
59
59
  6. A Chrome MV3 service worker that rehydrates state and routes messages.
60
60
  7. A narrow content script that reports document metadata and visible text through the bridge.
61
61
  8. Deterministic tests for browser contracts without Chrome credentials or network access.
62
+ 9. A read-only page-world bridge with token-correlated responses and bounded `pagefacts` reads.
62
63
 
63
64
  The extension remains an adapter. The root library continues to work without a browser, without an extension and without external memory.
64
65
 
@@ -66,7 +67,7 @@ The extension remains an adapter. The root library continues to work without a b
66
67
 
67
68
  Version 1.1 implements the first slice in `extension/`: `protocol.js` provides versioned serializable messages and snapshot identity; `serviceworker.js` provides browser independent routing; `worker.js` binds that router to Manifest V3 APIs; `content.js` runs the isolated page bridge; and `popup.html` with `popup.js` provides user initiated snapshot and read actions. The package exports `@wenathlan/saddle/extension`, while the root library remains usable without Chrome.
68
69
 
69
- The slice is intentionally not a full autonomous browser agent. Snapshot diffing, tab and frame identity, resumable command records, optional host escalation, browser action results and multi-browser packaging remain P1 or P2 work.
70
+ The slice is intentionally not a full autonomous browser agent. Snapshot diffing, tab and frame identity, resumable command records, optional host escalation, browser action results and multi-browser packaging remain P1 or P2 work. The page-world bridge is deliberately read-only and cannot evaluate arbitrary page-provided commands.
70
71
 
71
72
  ## references
72
73
 
@@ -1,12 +1,16 @@
1
1
  # Platform pipeline audit
2
2
 
3
- This document records the platform facts used by the 1.8.4 pipeline work. The workflows remain caller-configured: repository owners provide runner availability, deployment secrets and target repository names.
3
+ This document records the platform facts used by the 1.8.6 pipeline work and the subsequent web root migration. The workflows remain caller-configured: repository owners provide runner availability, deployment secrets and target repository names.
4
+
5
+ ## repository layout contract
6
+
7
+ The repository has one package manifest and one npm lockfile at the root. The static application is rooted at `web/` without a nested `client/` or `src/` directory: `web/index.html`, `web/main.tsx`, `web/App.tsx`, `web/components/`, `web/pages/`, `web/public/` and `web/dist/` are the canonical paths. Every Pages pipeline installs from the root with `npm ci`, runs `npm run web:check`, and writes the deployable artifact to `web/dist/public`. No nested web package, pnpm lockfile or platform workflow is retained.
4
8
 
5
9
  ## verified platform facts
6
10
 
7
11
  | Platform | Verified contract | Source |
8
12
  | --- | --- | --- |
9
- | GitHub Pages | A custom workflow uses `actions/configure-pages@v5`, uploads a static artifact with `actions/upload-pages-artifact@v4`, then deploys with `actions/deploy-pages@v4`. The deployment job needs `pages: write` and `id-token: write`, depends on the build job and uses the `github-pages` environment. | [GitHub Pages custom workflows](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages) |
13
+ | GitHub Pages | A custom workflow uses `actions/configure-pages@v5`, derives the project base path from the Pages output or repository name, uploads a static artifact with `actions/upload-pages-artifact@v4`, then deploys from a dependent job with `actions/deploy-pages@v4`. The deployment job needs `pages: write` and `id-token: write`, depends on the build job and uses the `github-pages` environment. The repository source is configured as GitHub Actions and the current site is `https://wenathlan.github.io/saddle/`. | [GitHub Pages custom workflows](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages) |
10
14
  | Codeberg Pages | Forgejo Actions can deploy with `https://codeberg.org/git-pages/action@v2`, using `site`, `source` and the injected `${{ forge.token }}`. Publishing the repository subdomain should be restricted to the default branch. Codeberg is migrating its legacy Pages v2 flow; custom domains still have separate constraints. | [Codeberg Pages via Forgejo Actions](https://docs.codeberg.org/codeberg-pages/forgejo-actions/), [Codeberg Pages output](https://docs.codeberg.org/codeberg-pages/pushing-output/) |
11
15
  | GitLab Pages | A job with `pages: true` publishes the default `public` directory; the job can also use a `pages` hash for a `path_prefix`. Static HTML, CSS and JavaScript are supported through GitLab CI/CD. | [GitLab Pages](https://docs.gitlab.com/user/project/pages/) |
12
16
  | Woodpecker CI | A workflow is a serial list of container steps with `image` and `commands`; branch/event filters belong in `when`, and secrets are injected through the pipeline environment rather than committed YAML. | [Woodpecker workflow syntax](https://woodpecker-ci.org/docs/usage/workflow-syntax) |
@@ -0,0 +1,16 @@
1
+ # Release assets
2
+
3
+ The Node-only release adapter creates deterministic metadata for caller-selected artifacts. It writes `SHA256SUMS`, `sbom.cdx.json` in CycloneDX 1.5 shape and `provenance.intoto.jsonl` in an in-toto statement shape. The adapter never publishes, authenticates or selects a registry.
4
+
5
+ ```bash
6
+ npm run release:assets -- \
7
+ --version 1.8.6 \
8
+ --output build/release \
9
+ --artifact build/saddle.tgz \
10
+ --build-type caller-build \
11
+ --builder caller-ci
12
+ ```
13
+
14
+ The artifact paths are supplied by the caller and are sorted before the checksum and provenance files are written. Package dependencies are read from the root lockfile to build a compact component list. The output can be attached to a release or checked by a registry-specific workflow without adding credentials to the library.
15
+
16
+ The package also exports the adapter as `@wenathlan/saddle/release-assets`. It is intentionally Node-only because release metadata reads files and uses the Node crypto implementation; the transport-neutral root remains free of Node imports.
@@ -14,7 +14,7 @@ The manifest requests only `activeTab`, `scripting` and `storage`. It does not r
14
14
 
15
15
  ## boundaries
16
16
 
17
- The content bridge runs in Chrome's isolated world. It exposes bounded page metadata, visible text, stable references and user initiated click or fill commands. The service worker forwards versioned messages, persists bounded pending command records and stores the latest snapshot metadata in session storage. Rehydration is explicit; startup never replays a command without a caller action. No endpoint, credential, remote script or browser profile is embedded.
17
+ The content bridge runs in Chrome's isolated world. It exposes bounded page metadata, visible text, stable references and user initiated click or fill commands. Extension snapshots carry optional window, tab and frame identifiers and can produce bounded additions, removals and changed-element diffs. `pagebridge.js` runs in the page world as a narrow read-only boundary; the isolated content script requests the `pagefacts` command through a token-correlated `postMessage` channel and rejects foreign sources or timed-out responses. The page world cannot invoke extension commands, receive credentials or evaluate arbitrary extension code. The service worker forwards versioned messages, persists bounded pending command records and restores their context metadata on explicit resume. No endpoint, credential, remote script or browser profile is embedded.
18
18
 
19
19
  `protocol.js` and `serviceworker.js` are reusable ESM contracts. `content.js` is intentionally a classic injected file because programmatic Chrome content scripts are loaded as files; it exposes a small global bridge and avoids arbitrary page JavaScript evaluation.
20
20
 
@@ -24,4 +24,4 @@ The Node-only build adapter creates an isolated unpacked artifact with the relea
24
24
 
25
25
  ## next slices
26
26
 
27
- The next extension slices should add snapshot diffing, richer tab and frame metadata, optional host permission escalation and browser action results. Browser providers, login profiles, captcha solvers and remote runners remain caller owned adapters.
27
+ The next extension slices should add optional host permission escalation, richer browser action results and Firefox, Edge or Safari adapter profiles. Browser providers, login profiles, captcha solvers and remote runners remain caller owned adapters.
@@ -7,7 +7,7 @@ import { dirname, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  const rootpath = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
- const entries = ["manifest.json", "worker.js", "serviceworker.js", "content.js", "popup.js", "popup.html", "popup.css", "protocol.js", "permissions.js"];
10
+ const entries = ["manifest.json", "worker.js", "serviceworker.js", "content.js", "pagebridge.js", "popup.js", "popup.html", "popup.css", "protocol.js", "permissions.js"];
11
11
 
12
12
  function parsearguments(argumentslist) {
13
13
  const options = {};
@@ -6,12 +6,14 @@
6
6
  (function installglobalbridge(global) {
7
7
  const installedkey = "__saddlecontentbridge";
8
8
  const protocolversion = 1;
9
- const commands = ["snapshot", "readpage", "clickref", "fillref"];
9
+ const commands = ["snapshot", "readpage", "pagefacts", "clickref", "fillref"];
10
+ const pagechannelname = "saddle.pagefacts.v1";
10
11
 
11
- function createbridge(documentref, now = () => Date.now()) {
12
+ function createbridge(documentref, now = () => Date.now(), options = {}) {
12
13
  let snapshotid = null;
13
14
  let sequence = 0;
14
15
  const references = new Map();
16
+ const pagechannel = options.pagechannel ?? (typeof global.addEventListener === "function" && typeof global.postMessage === "function" ? createpagechannel(global, now, options.timeout) : null);
15
17
 
16
18
  function snapshotpage() {
17
19
  const nextid = `snap${++sequence}${now()}`;
@@ -45,6 +47,10 @@
45
47
  const payload = request.payload ?? {};
46
48
  if (request.command === "snapshot") return snapshotpage();
47
49
  if (request.command === "readpage") return readpage();
50
+ if (request.command === "pagefacts") {
51
+ if (!pagechannel) throw failure("page_bridge_unavailable", "page bridge is not available");
52
+ return pagechannel.readpage();
53
+ }
48
54
  const element = resolve(payload.ref, payload.snapshotid);
49
55
  if (request.command === "clickref") { element.click?.(); return { ref: payload.ref, clicked: true, snapshotid }; }
50
56
  if (!isfillable(element)) throw failure("not_fillable", `element is not fillable: ${payload.ref}`);
@@ -52,7 +58,43 @@
52
58
  return { ref: payload.ref, filled: true, snapshotid };
53
59
  }
54
60
 
55
- return { handle, snapshotpage, readpage };
61
+ return { handle, snapshotpage, readpage, pagechannel };
62
+ }
63
+
64
+ function createpagechannel(globalref = global, now = () => Date.now(), timeout = 1500) {
65
+ if (typeof globalref.addEventListener !== "function" || typeof globalref.postMessage !== "function") throw new TypeError("page channel requires window messaging APIs");
66
+ if (!Number.isSafeInteger(timeout) || timeout < 1) throw new TypeError("page channel timeout must be a positive safe integer");
67
+ const pending = new Map();
68
+ let sequence = 0;
69
+ const token = `token${now()}${Math.random().toString(16).slice(2)}`;
70
+
71
+ function listener(event) {
72
+ const response = event?.data;
73
+ if (event?.source && event.source !== globalref) return;
74
+ if (response?.channel !== pagechannelname || response.type !== "response" || response.token !== token) return;
75
+ const request = pending.get(response.requestid);
76
+ if (!request) return;
77
+ pending.delete(response.requestid);
78
+ globalref.clearTimeout?.(request.timer);
79
+ request.resolve(response.payload ?? {});
80
+ }
81
+
82
+ function readpage() {
83
+ const requestid = `page${now()}${++sequence}`;
84
+ return new Promise((resolve, reject) => {
85
+ const timer = globalref.setTimeout(() => {
86
+ pending.delete(requestid);
87
+ const error = new Error("page bridge response timed out");
88
+ error.code = "page_bridge_timeout";
89
+ reject(error);
90
+ }, timeout);
91
+ pending.set(requestid, { resolve, timer });
92
+ globalref.postMessage({ channel: pagechannelname, version: 1, type: "request", requestid, token }, "*");
93
+ });
94
+ }
95
+
96
+ globalref.addEventListener("message", listener);
97
+ return { readpage, dispose() { globalref.removeEventListener?.("message", listener); for (const request of pending.values()) globalref.clearTimeout?.(request.timer); pending.clear(); } };
56
98
  }
57
99
 
58
100
  function install(runtime = global.chrome?.runtime, documentref = global.document) {
@@ -80,6 +122,6 @@
80
122
  function setvalue(element, value) { element.value = String(value ?? ""); element.dispatchEvent?.(new Event("input", { bubbles: true })); element.dispatchEvent?.(new Event("change", { bubbles: true })); }
81
123
  function failure(code, message) { const error = new Error(message); error.code = code; return error; }
82
124
 
83
- global.saddlecontent = { createbridge, install };
125
+ global.saddlecontent = { createbridge, createpagechannel, install };
84
126
  if (global.chrome?.runtime?.onMessage && global.document) install();
85
127
  })(globalThis);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Saddle browser bridge",
4
- "version": "1.1.0",
4
+ "version": "1.8.6",
5
5
  "description": "User initiated page snapshots through the Saddle browser contract.",
6
6
  "minimum_chrome_version": "110",
7
7
  "permissions": ["activeTab", "scripting", "storage"],
@@ -0,0 +1,41 @@
1
+ /**
2
+ * page bridge exposes bounded, read-only page facts to the isolated content world.
3
+ * It never executes page supplied commands and never forwards extension credentials.
4
+ */
5
+
6
+ (function installpagebridge(global) {
7
+ const channel = "saddle.pagefacts.v1";
8
+ const installedkey = "__saddlepagebridge";
9
+
10
+ function createpagebridge(globalref = global) {
11
+ function readpage() {
12
+ const documentref = globalref.document;
13
+ return {
14
+ url: String(documentref?.location?.href ?? ""),
15
+ title: String(documentref?.title ?? "").slice(0, 500),
16
+ text: String(documentref?.body?.innerText ?? "").slice(0, 100000)
17
+ };
18
+ }
19
+
20
+ function listener(event) {
21
+ const request = event?.data;
22
+ if (event?.source && event.source !== globalref) return;
23
+ if (request?.channel !== channel || request.type !== "request" || typeof request.requestid !== "string" || typeof request.token !== "string") return;
24
+ globalref.postMessage({ channel, version: 1, type: "response", requestid: request.requestid, token: request.token, payload: readpage() }, "*");
25
+ }
26
+
27
+ return { channel, listener, readpage };
28
+ }
29
+
30
+ function install(globalref = global) {
31
+ if (typeof globalref?.addEventListener !== "function" || !globalref.document) throw new TypeError("page bridge requires a window and document");
32
+ if (globalref[installedkey]) return globalref[installedkey];
33
+ const bridge = createpagebridge(globalref);
34
+ globalref.addEventListener("message", bridge.listener);
35
+ globalref[installedkey] = { ...bridge, dispose() { globalref.removeEventListener?.("message", bridge.listener); delete globalref[installedkey]; } };
36
+ return globalref[installedkey];
37
+ }
38
+
39
+ global.saddlepagebridge = { channel, createpagebridge, install };
40
+ if (global.document && typeof global.addEventListener === "function") install(global);
41
+ })(globalThis);
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  export const protocolversion = 1;
6
- export const extensioncommands = Object.freeze(["snapshot", "readpage", "clickref", "fillref"]);
6
+ export const extensioncommands = Object.freeze(["snapshot", "readpage", "pagefacts", "clickref", "fillref"]);
7
7
 
8
8
  /** Creates a compact identifier without embedding a host, port or credential. */
9
9
  export function createid(prefix = "msg", source) {
@@ -59,10 +59,13 @@ export function createsnapshot(input = {}) {
59
59
  version: protocolversion,
60
60
  snapshotid: String(input.snapshotid ?? createid("snap")),
61
61
  createdat: Number(input.createdat ?? Date.now()),
62
+ windowid: input.windowid === undefined ? undefined : String(input.windowid),
63
+ tabid: input.tabid === undefined ? undefined : String(input.tabid),
64
+ frameid: input.frameid === undefined ? undefined : String(input.frameid),
62
65
  url: String(input.url ?? ""),
63
66
  title: String(input.title ?? ""),
64
67
  text: String(input.text ?? ""),
65
- elements: Array.isArray(input.elements) ? input.elements.map((element) => ({ ref: String(element.ref), role: String(element.role ?? "generic"), name: String(element.name ?? "") })) : []
68
+ elements: Array.isArray(input.elements) ? input.elements.map((element) => ({ ref: String(element.ref), role: String(element.role ?? "generic"), name: String(element.name ?? ""), value: element.value === undefined ? undefined : String(element.value), disabled: Boolean(element.disabled) })) : []
66
69
  };
67
70
  assertserializable(snapshot);
68
71
  return snapshot;
@@ -71,6 +74,22 @@ export function createsnapshot(input = {}) {
71
74
  /** Returns whether a reference still belongs to the current page snapshot. */
72
75
  export function isfreshsnapshot(snapshotid, currentid) { return Boolean(snapshotid && currentid && snapshotid === currentid); }
73
76
 
77
+ /** Computes bounded additions, removals and changed elements between extension snapshots. */
78
+ export function snapshotdiff(previous, current) {
79
+ const before = createsnapshot(previous);
80
+ const after = createsnapshot(current);
81
+ const oldmap = new Map(before.elements.map((element) => [element.ref, element]));
82
+ const newmap = new Map(after.elements.map((element) => [element.ref, element]));
83
+ return {
84
+ from: before.snapshotid,
85
+ to: after.snapshotid,
86
+ contextchanged: before.windowid !== after.windowid || before.tabid !== after.tabid || before.frameid !== after.frameid,
87
+ added: after.elements.filter((element) => !oldmap.has(element.ref)),
88
+ removed: before.elements.filter((element) => !newmap.has(element.ref)),
89
+ changed: after.elements.filter((element) => oldmap.has(element.ref) && JSON.stringify(oldmap.get(element.ref)) !== JSON.stringify(element))
90
+ };
91
+ }
92
+
74
93
  function assertserializable(value) {
75
94
  try { JSON.stringify(value); } catch (error) { throw new TypeError(`extension message is not serializable: ${error.message}`); }
76
95
  }
@@ -60,7 +60,8 @@ export function createworkerrouter(options = {}) {
60
60
  async function dispatch(request, sender = {}) {
61
61
  const tabid = request.payload?.tabid ?? sender.tab?.id;
62
62
  await ensurecontent(tabid);
63
- return tabs.sendMessage(tabid, request);
63
+ const response = await tabs.sendMessage(tabid, request);
64
+ return decorate(response, sender, tabid);
64
65
  }
65
66
 
66
67
  async function handle(message, sender = {}) {
@@ -70,7 +71,7 @@ export function createworkerrouter(options = {}) {
70
71
  await enqueue(request, { ...sender, tab: { ...sender.tab, id: tabid } });
71
72
  try {
72
73
  const response = await dispatch(request, { ...sender, tab: { ...sender.tab, id: tabid } });
73
- await complete(request.id, response?.type === "response" && response.payload?.snapshotid ? { tabid, snapshotid: response.payload.snapshotid, updatedat: Date.now() } : { updatedat: Date.now() });
74
+ await complete(request.id, response?.type === "response" && response.payload?.snapshotid ? { tabid, snapshotid: response.payload.snapshotid, frameid: response.payload.frameid, windowid: response.payload.windowid, updatedat: Date.now() } : { updatedat: Date.now() });
74
75
  return response;
75
76
  } catch (error) {
76
77
  await markfailure(request.id, error);
@@ -85,8 +86,8 @@ export function createworkerrouter(options = {}) {
85
86
  const pending = state.pending.find((item) => item.requestid === requestid);
86
87
  if (!pending) throw extensionerror("PENDING_NOT_FOUND", `pending command not found: ${requestid}`);
87
88
  const tabid = pending.tabid ?? sender.tab?.id;
88
- const response = await dispatch(pending.message, { ...sender, tab: { ...sender.tab, id: tabid } });
89
- await complete(requestid, response?.type === "response" && response.payload?.snapshotid ? { tabid, snapshotid: response.payload.snapshotid, updatedat: Date.now() } : { updatedat: Date.now() });
89
+ const response = await dispatch(pending.message, { ...sender, frameId: pending.frameid ?? sender.frameId, tab: { ...sender.tab, id: tabid, windowId: pending.windowid ?? sender.tab?.windowId } });
90
+ await complete(requestid, response?.type === "response" && response.payload?.snapshotid ? { tabid, snapshotid: response.payload.snapshotid, frameid: response.payload.frameid, windowid: response.payload.windowid, updatedat: Date.now() } : { updatedat: Date.now() });
90
91
  return response;
91
92
  }
92
93
 
@@ -95,7 +96,13 @@ export function createworkerrouter(options = {}) {
95
96
  return { ensurecontent, readstate, savestate, rehydrate, enqueue, resume, cancel, handle };
96
97
  }
97
98
 
98
- function pendingrecord(request, sender = {}) { return { requestid: request.id, command: request.command, message: request, tabid: request.payload?.tabid ?? sender.tab?.id, attempts: 0, createdat: Date.now(), updatedat: Date.now() }; }
99
+ function pendingrecord(request, sender = {}) { return { requestid: request.id, command: request.command, message: request, tabid: request.payload?.tabid ?? sender.tab?.id, frameid: request.payload?.frameid ?? sender.frameId, windowid: request.payload?.windowid ?? sender.tab?.windowId, attempts: 0, createdat: Date.now(), updatedat: Date.now() }; }
100
+
101
+ function decorate(response, sender, tabid) {
102
+ if (response?.type !== "response" || !response.payload || typeof response.payload !== "object" || !response.payload.snapshotid) return response;
103
+ const payload = { ...response.payload, tabid: response.payload.tabid ?? (tabid === undefined ? undefined : String(tabid)), frameid: response.payload.frameid ?? (sender.frameId === undefined ? undefined : String(sender.frameId)), windowid: response.payload.windowid ?? (sender.tab?.windowId === undefined ? undefined : String(sender.tab.windowId)) };
104
+ return { ...response, payload };
105
+ }
99
106
 
100
107
  function validpending(value) { return Boolean(value && typeof value === "object" && typeof value.requestid === "string" && value.message && value.message.type === "command" && Number.isSafeInteger(value.attempts) && value.attempts >= 0); }
101
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/saddle",
3
- "version": "1.8.5",
3
+ "version": "1.8.6",
4
4
  "description": "binary computing engine that turns distributed storage into a publishable working set",
5
5
  "type": "module",
6
6
  "private": false,
@@ -11,15 +11,39 @@
11
11
  "module": "./index.js",
12
12
  "browser": "./index.js",
13
13
  "packageManager": "npm@12.0.2",
14
- "publishConfig": { "access": "public" },
15
- "repository": { "type": "git", "url": "https://github.com/wenathlan/saddle.git" },
16
- "bugs": { "url": "https://github.com/wenathlan/saddle/issues" },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/wenathlan/saddle.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/wenathlan/saddle/issues"
23
+ },
17
24
  "homepage": "https://github.com/wenathlan/saddle#readme",
18
- "keywords": ["distributed computing", "virtual memory", "runner", "storage", "automation"],
19
- "engines": { "node": ">=26.7.0", "npm": ">=10.9.2" },
20
- "peerDependencies": { "playwright": "^1.62.1" },
21
- "peerDependenciesMeta": { "playwright": { "optional": true } },
22
- "bin": { "saddle": "./cli/main.js" },
25
+ "keywords": [
26
+ "distributed computing",
27
+ "virtual memory",
28
+ "runner",
29
+ "storage",
30
+ "automation"
31
+ ],
32
+ "engines": {
33
+ "node": ">=26.7.0",
34
+ "npm": ">=10.9.2"
35
+ },
36
+ "peerDependencies": {
37
+ "playwright": "^1.62.1"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "playwright": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "bin": {
45
+ "saddle": "./cli/main.js"
46
+ },
23
47
  "exports": {
24
48
  ".": "./index.js",
25
49
  "./memory": "./memory/bridge.js",
@@ -43,16 +67,137 @@
43
67
  "./queue-persistent": "./queue/persistent.js",
44
68
  "./captcha-evidence": "./captcha/evidence.js",
45
69
  "./hash": "./core/hash.js",
70
+ "./release-assets": "./release/assets.js",
46
71
  "./worker": "./runtime/worker.js"
47
72
  },
48
- "files": ["core", "domain", "memory", "runners", "runtime", "storage", "sessions", "modes", "adapters", "persistence", "queue", "dispatch", "scrape", "crawl", "api", "mcp", "browser", "proxy", "captcha", "ai", "webhook", "surfaces", "library", "errors", "retry", "server", "binary", "format", "packager", "bot", "protocol", "workflow", "cli", "extension", "index.js", "docs", "examples", "README.md", "LICENSE"],
73
+ "files": [
74
+ "core",
75
+ "domain",
76
+ "memory",
77
+ "runners",
78
+ "runtime",
79
+ "storage",
80
+ "sessions",
81
+ "modes",
82
+ "adapters",
83
+ "persistence",
84
+ "queue",
85
+ "dispatch",
86
+ "scrape",
87
+ "crawl",
88
+ "api",
89
+ "mcp",
90
+ "browser",
91
+ "proxy",
92
+ "captcha",
93
+ "ai",
94
+ "webhook",
95
+ "surfaces",
96
+ "library",
97
+ "errors",
98
+ "retry",
99
+ "server",
100
+ "binary",
101
+ "format",
102
+ "packager",
103
+ "bot",
104
+ "protocol",
105
+ "workflow",
106
+ "cli",
107
+ "extension",
108
+ "release",
109
+ "index.js",
110
+ "docs",
111
+ "examples",
112
+ "README.md",
113
+ "LICENSE"
114
+ ],
49
115
  "scripts": {
50
- "check": "node --check index.js && node --check runtime/engine.js && node --check cli/main.js && node --check extension/protocol.js && node --check extension/serviceworker.js && node --check extension/worker.js && node --check extension/content.js && node --check extension/popup.js && node --check extension/permissions.js && node --check extension/build.js && node --check sessions/replay.js && node --check browser/recorder.js && node --check scrape/normalize.js",
116
+ "check": "node --check index.js && node --check runtime/engine.js && node --check cli/main.js && node --check extension/protocol.js && node --check extension/serviceworker.js && node --check extension/worker.js && node --check extension/content.js && node --check extension/pagebridge.js && node --check extension/popup.js && node --check extension/permissions.js && node --check extension/build.js && node --check release/assets.js && node --check sessions/replay.js && node --check browser/recorder.js && node --check scrape/normalize.js",
51
117
  "formatcheck": "node format/check.js",
52
118
  "test": "node --test tests/*.test.js",
53
119
  "run": "node cli/main.js",
54
120
  "extension:build": "node extension/build.js",
121
+ "release:assets": "node release/assets.js",
55
122
  "pack:check": "npm run check && npm run formatcheck && npm test && npm pack --dry-run",
56
- "prepublishOnly": "npm run pack:check"
123
+ "prepublishOnly": "npm run pack:check",
124
+ "web:dev": "vite --config web/vite.config.ts --host",
125
+ "web:check": "tsc --project web/tsconfig.json --noEmit",
126
+ "web:build": "vite build --config web/vite.config.ts",
127
+ "web:build:pages": "vite build --config web/vite.config.ts"
128
+ },
129
+ "devDependencies": {
130
+ "@hookform/resolvers": "^5.2.2",
131
+ "@radix-ui/react-accordion": "^1.2.12",
132
+ "@radix-ui/react-alert-dialog": "^1.1.15",
133
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
134
+ "@radix-ui/react-avatar": "^1.1.10",
135
+ "@radix-ui/react-checkbox": "^1.3.3",
136
+ "@radix-ui/react-collapsible": "^1.1.12",
137
+ "@radix-ui/react-context-menu": "^2.2.16",
138
+ "@radix-ui/react-dialog": "^1.1.15",
139
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
140
+ "@radix-ui/react-hover-card": "^1.1.15",
141
+ "@radix-ui/react-label": "^2.1.7",
142
+ "@radix-ui/react-menubar": "^1.1.16",
143
+ "@radix-ui/react-navigation-menu": "^1.2.14",
144
+ "@radix-ui/react-popover": "^1.1.15",
145
+ "@radix-ui/react-progress": "^1.1.7",
146
+ "@radix-ui/react-radio-group": "^1.3.8",
147
+ "@radix-ui/react-scroll-area": "^1.2.10",
148
+ "@radix-ui/react-select": "^2.2.6",
149
+ "@radix-ui/react-separator": "^1.1.7",
150
+ "@radix-ui/react-slider": "^1.3.6",
151
+ "@radix-ui/react-slot": "^1.2.3",
152
+ "@radix-ui/react-switch": "^1.2.6",
153
+ "@radix-ui/react-tabs": "^1.1.13",
154
+ "@radix-ui/react-toggle": "^1.1.10",
155
+ "@radix-ui/react-toggle-group": "^1.1.11",
156
+ "@radix-ui/react-tooltip": "^1.2.8",
157
+ "axios": "^1.12.0",
158
+ "class-variance-authority": "^0.7.1",
159
+ "clsx": "^2.1.1",
160
+ "cmdk": "^1.1.1",
161
+ "embla-carousel-react": "^8.6.0",
162
+ "express": "^4.21.2",
163
+ "framer-motion": "^12.23.22",
164
+ "input-otp": "^1.4.2",
165
+ "lucide-react": "^0.453.0",
166
+ "nanoid": "^5.1.5",
167
+ "next-themes": "^0.4.6",
168
+ "react": "^19.2.1",
169
+ "react-day-picker": "^9.11.1",
170
+ "react-dom": "^19.2.1",
171
+ "react-hook-form": "^7.64.0",
172
+ "react-resizable-panels": "^3.0.6",
173
+ "recharts": "^2.15.2",
174
+ "sonner": "^2.0.7",
175
+ "streamdown": "^1.4.0",
176
+ "tailwind-merge": "^3.3.1",
177
+ "tailwindcss-animate": "^1.0.7",
178
+ "vaul": "^1.1.2",
179
+ "wouter": "^3.3.5",
180
+ "zod": "^4.1.12",
181
+ "@builder.io/vite-plugin-jsx-loc": "^0.1.1",
182
+ "@tailwindcss/typography": "^0.5.15",
183
+ "@tailwindcss/vite": "^4.1.3",
184
+ "@types/express": "4.17.21",
185
+ "@types/google.maps": "^3.58.1",
186
+ "@types/node": "^24.7.0",
187
+ "@types/react": "^19.2.1",
188
+ "@types/react-dom": "^19.2.1",
189
+ "@vitejs/plugin-react": "^5.0.4",
190
+ "add": "^2.0.6",
191
+ "autoprefixer": "^10.4.20",
192
+ "esbuild": "^0.25.0",
193
+ "postcss": "^8.4.47",
194
+ "prettier": "^3.6.2",
195
+ "tailwindcss": "^4.1.14",
196
+ "tsx": "^4.19.1",
197
+ "tw-animate-css": "^1.4.0",
198
+ "typescript": "5.6.3",
199
+ "vite": "^7.1.7",
200
+ "vite-plugin-manus-runtime": "^0.0.58",
201
+ "vitest": "^2.1.4"
57
202
  }
58
203
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * release assets create deterministic checksums, SBOM data and provenance statements.
3
+ * The adapter reads caller-selected artifacts and never publishes or handles credentials.
4
+ */
5
+
6
+ import { createHash } from "node:crypto";
7
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { dirname, join, relative, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const rootpath = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+
13
+ /** Builds deterministic release metadata files for caller-selected artifacts. */
14
+ export async function createassets(options = {}) {
15
+ const packagefile = resolve(options.packagefile ?? join(rootpath, "package.json"));
16
+ const lockfile = resolve(options.lockfile ?? join(rootpath, "package-lock.json"));
17
+ const output = resolve(options.output ?? join(rootpath, "build", "release"));
18
+ const artifactroot = resolve(options.artifactroot ?? rootpath);
19
+ const packagejson = JSON.parse(await readFile(packagefile, "utf8"));
20
+ const lockjson = JSON.parse(await readFile(lockfile, "utf8"));
21
+ const artifacts = [...new Set((options.artifacts ?? []).map((artifact) => resolve(String(artifact))))].sort();
22
+ const subjects = await Promise.all(artifacts.map(async (artifact) => ({ name: relative(artifactroot, artifact).replaceAll("\\", "/"), digest: await sha256(artifact) })));
23
+ await mkdir(output, { recursive: true });
24
+ const checksums = `${subjects.map((subject) => `${subject.digest} ${subject.name}`).join("\n")}${subjects.length ? "\n" : ""}`;
25
+ const sbom = createsbom(packagejson, lockjson);
26
+ const provenance = createprovenance(packagejson, subjects, options);
27
+ const files = { checksums: join(output, "SHA256SUMS"), sbom: join(output, "sbom.cdx.json"), provenance: join(output, "provenance.intoto.jsonl") };
28
+ await writeFile(files.checksums, checksums);
29
+ await writeFile(files.sbom, `${JSON.stringify(sbom, null, 2)}\n`);
30
+ await writeFile(files.provenance, `${JSON.stringify(provenance)}\n`);
31
+ return { output, files, subjects, sbom, provenance };
32
+ }
33
+
34
+ /** Creates a compact CycloneDX component list from the root lockfile dependencies. */
35
+ export function createsbom(packagejson, lockjson = {}) {
36
+ const root = lockjson.packages?.[""] ?? {};
37
+ const dependencies = { ...(root.dependencies ?? {}), ...(root.devDependencies ?? {}) };
38
+ const components = Object.keys(dependencies).map((name) => {
39
+ const entry = lockjson.packages?.[`node_modules/${name}`] ?? {};
40
+ const version = String(entry.version ?? dependencies[name]).replace(/^[^0-9]*/, "");
41
+ return { "bom-ref": `pkg:npm/${name}@${version}`, name, version, purl: `pkg:npm/${name}@${version}`, scope: root.devDependencies?.[name] ? "optional" : "required", type: "library" };
42
+ }).sort((left, right) => left.name.localeCompare(right.name));
43
+ return { bomFormat: "CycloneDX", specVersion: "1.5", serialNumber: `urn:uuid:${stableuuid(`${packagejson.name}@${packagejson.version}`)}`, version: 1, metadata: { component: { type: "application", name: String(packagejson.name), version: String(packagejson.version) } }, components };
44
+ }
45
+
46
+ /** Creates an in-toto statement whose subjects are the caller-selected release artifacts. */
47
+ export function createprovenance(packagejson, subjects = [], options = {}) {
48
+ const normalized = subjects.map((subject) => ({ name: String(subject.name), digest: { sha256: String(subject.digest) } })).sort((left, right) => left.name.localeCompare(right.name));
49
+ return { _type: "https://in-toto.io/Statement/v1", subject: normalized, predicateType: "https://slsa.dev/provenance/v1", predicate: { buildDefinition: { buildType: String(options.buildtype ?? "caller-defined"), externalParameters: { package: String(packagejson.name), version: String(options.version ?? packagejson.version) }, internalParameters: {} }, runDetails: { builder: { id: String(options.builder ?? "caller-defined") }, metadata: { invocationId: stableuuid(`${packagejson.name}@${options.version ?? packagejson.version}:${normalized.map((subject) => subject.name).join(",")}`) } } } };
50
+ }
51
+
52
+ async function sha256(path) { const hash = createHash("sha256"); hash.update(await readFile(path)); return hash.digest("hex"); }
53
+
54
+ function stableuuid(value) { const hex = createHash("sha256").update(String(value)).digest("hex").slice(0, 32); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${(8 + (Number.parseInt(hex.slice(16, 17), 16) % 4).toString(16))}${hex.slice(17, 20)}-${hex.slice(20)}`; }
55
+
56
+ function parsearguments(argumentslist) {
57
+ const options = { artifacts: [] };
58
+ for (let index = 0; index < argumentslist.length; index += 1) {
59
+ const argument = argumentslist[index];
60
+ if (argument === "--output") options.output = argumentslist[++index];
61
+ else if (argument === "--version") options.version = argumentslist[++index];
62
+ else if (argument === "--artifact") options.artifacts.push(argumentslist[++index]);
63
+ else if (argument === "--build-type") options.buildtype = argumentslist[++index];
64
+ else if (argument === "--builder") options.builder = argumentslist[++index];
65
+ else throw new TypeError(`unsupported release asset argument: ${argument}`);
66
+ }
67
+ return options;
68
+ }
69
+
70
+ if (process.argv[1] === fileURLToPath(import.meta.url)) createassets(parsearguments(process.argv.slice(2))).then(({ output }) => { console.log(`release assets: ${output}`); }).catch((error) => { console.error(error.message); process.exitCode = 1; });