@wenathlan/saddle 1.8.2 → 1.8.4

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.2"><img src="https://img.shields.io/badge/release-v1.8.2-d35d3d" alt="Release 1.8.2" /></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>
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
 
@@ -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.2 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.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.
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.2` | publishgithubnpm.yml | release candidate |
149
- | GHCR | `ghcr.io/wenathlan/saddle:1.8.2` and `latest` | publishghcr.yml | release candidate |
150
- | Maven | `io.wenathlan:saddle:1.8.2` | publishmaven.yml | release candidate |
151
- | NuGet | `Saddle.1.8.2.nupkg` | publishnuget.yml | release candidate |
152
- | RubyGems | `saddle 1.8.2` | publishrubygems.yml | release candidate |
153
- | npmjs | `@wenathlan/saddle@1.8.2` | publishnpmjs.yml | release candidate |
148
+ | GitHub npm | `@wenathlan/saddle@1.8.4` | publishgithubnpm.yml | pending release |
149
+ | GHCR | `ghcr.io/wenathlan/saddle:1.8.4` and `latest` | publishghcr.yml | pending release |
150
+ | Maven | `io.wenathlan:saddle:1.8.4` | publishmaven.yml | pending release |
151
+ | NuGet | `Saddle.1.8.4.nupkg` | publishnuget.yml | pending release |
152
+ | RubyGems | `saddle 1.8.4` | publishrubygems.yml | pending release |
153
+ | npmjs | `@wenathlan/saddle@1.8.4` | 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.2 establishes the engine contracts, browser snapshot foundation, storage sync primitives, runner recovery contracts, scraping context provenance and normalization, API/MCP security contracts, bot integration lifecycle, the tested extension bridge and permission policy, deterministic extension packaging, context-aware replay, desktop/mobile/n8n surface contracts, a framework-neutral operator control boundary and the first cross-runtime import boundary. Native runtimes, 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.4 establishes the engine contracts, browser snapshot foundation, storage sync primitives, runner recovery contracts, scraping context provenance and normalization, API/MCP security contracts, bot integration lifecycle, the tested extension bridge and permission policy, deterministic extension packaging, context-aware replay, Node.js 26.7.0 cross-forge gates, desktop/mobile/n8n surface contracts, a framework-neutral operator control boundary and the first cross-runtime import boundary. Native runtimes, 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
 
package/docs/gapmatrix.md CHANGED
@@ -21,7 +21,7 @@ This matrix turns the supplied README and conclusions into implementation decisi
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
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
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 |
24
- | Service worker resilience | Not implemented | No rehydration or durable state strategy for worker termination | P0 | Persist pending command metadata and session summaries through injected storage |
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
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 |
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 |
@@ -29,7 +29,7 @@ This matrix turns the supplied README and conclusions into implementation decisi
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 |
30
30
  | Apps and bots | Implemented platform adapter, commands, bot and webhook signature contracts | App install/suspend/revoke, command scope checks, idempotency and delivery retry/dead-letter records are now available | P1 | Keep platform tokens and OAuth lifecycle caller-owned |
31
31
  | Storage | Implemented local, chunked, S3-compatible, GitHub Contents and file hosting adapters | Range reads, content dedupe, tiered cache, capabilities and conflict-aware sync were missing | P1 | Use the new neutral storage helpers and keep extension storage injected |
32
- | Queue | Implemented in-memory and persistent queue contracts | No worker-aware resume protocol for extension commands | P1 | Add resumable command records and idempotency keys to extension transport |
32
+ | Queue | Implemented in-memory, persistent and extension-resumable command contracts | Browser host still owns long-lived queue workers and cancellation policy | P1 | Keep command records serializable and resume explicit through the extension adapter |
33
33
  | Remote execution | Implemented provider, scheduler, health, triggers, heartbeat and resumable run contracts | No permissioned extension-to-runner bridge or forge-specific status adapters for every provider | P1 | Require explicit caller-provided endpoint and auth; no default remote host |
34
34
  | Scraping | Implemented robots, cache, HTML extraction, schema extraction, scraper and content normalization | Rich document parsers and binary format adapters remain caller-owned | P1 | Keep extraction safe and bounded; add adapters for richer document types |
35
35
  | Crawl | Implemented normalized BFS and persistent frontier | Priority and per-domain budget frontier now exist; sitemap refresh remains absent | P1 | Keep frontier state serializable and caller-persistable |
@@ -0,0 +1,18 @@
1
+ # Platform pipeline audit
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.
4
+
5
+ ## verified platform facts
6
+
7
+ | Platform | Verified contract | Source |
8
+ | --- | --- | --- |
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) |
10
+ | 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
+ | 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
+ | 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) |
13
+ | Forgejo Actions | Fully qualified actions are recommended; the official checkout reference is `https://data.forgejo.org/actions/checkout@v6`. Short `actions/checkout@v6` resolves through the instance `DEFAULT_ACTIONS_URL`, which can vary. | [Forgejo Actions: Using Actions](https://forgejo.org/docs/v15.0/user/actions/actions/) |
14
+ | Gitea Actions | Gitea Actions is mostly compatible with GitHub Actions and requires a separately installed Gitea Runner. The official documentation lists `actions/checkout@v4` as a supported action reference and warns that runner trust matters for public instances. | [Gitea Actions overview](https://docs.gitea.com/usage/actions/overview) |
15
+
16
+ ## boundary
17
+
18
+ GitHub Pages has a first-party artifact/deploy contract. Codeberg Pages has a git-pages Forgejo Action contract. Forgejo, Gitea and GitLab workflow files can build the same static output, but their hosting destination and token names are instance-specific. Woodpecker can build and publish artifacts, but a Pages deploy step requires the target host's configured service or a caller-owned token. No workflow in this release hardcodes credentials, hostnames or a private deployment target.
@@ -0,0 +1,13 @@
1
+ # Cross-forge pipelines
2
+
3
+ The library keeps GitHub Actions as the release-authoritative workflow because its six registry publishers already share the release-tag action and validation action. Forgejo, Gitea, GitLab and Woodpecker now execute the same deterministic gates with Node.js 26.7.0, but they do not assume a shared package registry, release API or secret name.
4
+
5
+ | Forge | File | Current role | Required caller configuration |
6
+ | --- | --- | --- | --- |
7
+ | GitLab | `.gitlab-ci.yml` | verify and package dry-run | runner, optional package registry credentials and artifact retention |
8
+ | Forgejo | `.forgejo/workflows/saddle.yml` | verify and package dry-run | trusted Forgejo runner and action source policy |
9
+ | Gitea | `.gitea/workflows/saddle.yml` | verify and package dry-run | Gitea Runner and instance action policy |
10
+ | Codeberg | Forgejo workflow files | verify plus optional Codeberg Pages action | Codeberg repository, `forge.token` and default-branch policy |
11
+ | Woodpecker | `.woodpecker/deploy.yml` | verify and package dry-run | trusted agent, image pull policy and optional artifact plugin |
12
+
13
+ These files intentionally do not invent a cross-forge release token or hardcode a host. A caller can add a publish step for the target forge after configuring its own secret and package endpoint. The public site has its own platform workflow set in the `saddle-pages` repository.
@@ -48,6 +48,8 @@ The active package identity on main is now `@wenathlan/saddle`. The repository h
48
48
 
49
49
  Release `v1.8.1` was created from the identity migration commit `efcbb02`. The npmjs workflow `31557618590` authenticated as `wenathlan`, generated `@wenathlan/saddle@1.8.1`, and ended with npm's `+ @wenathlan/saddle@1.8.1` success line. A later independent `npm view` returned `1.8.1`, and the direct registry metadata endpoint returned HTTP 200 with the same version record, confirming public visibility. The other v1.8.1 registry workflows also completed successfully: GitHub npm `31557618600`, GHCR `31557618571`, Maven `31557618597`, NuGet `31557618573` and RubyGems `31557618575`.
50
50
 
51
+ Release `v1.8.2` was created from commit `ac7c355` after the repository transfer to `wenathlan`. All release jobs succeeded: GitHub npm `31559770092` logged the owner-derived `@wenathlan/saddle` package, npmjs `31559770096` published the same public package, GHCR `31559770016` pushed `ghcr.io/wenathlan/saddle:1.8.2` and `latest`, Maven `31559770216` deployed `io.wenathlan:saddle:1.8.2`, NuGet `31559770142` created and pushed `Saddle.1.8.2.nupkg`, RubyGems `31559770023` registered `saddle (1.8.2)`, and extension build `31559769740` attached `saddle-extension-1.8.2.zip`. Release validation `31559770059` also succeeded. Independent checks confirmed npmjs `@wenathlan/saddle@1.8.2` and the GitHub release asset. The GitHub organization package-list API returned 403 for the available integration, while unauthenticated GHCR, Maven and NuGet endpoints returned 401; these protected visibility boundaries do not contradict the successful authenticated workflow logs.
52
+
51
53
  ## sources
52
54
 
53
55
  1. GitHub Docs — About permissions for GitHub Packages: https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages
package/docs/release.md CHANGED
@@ -1,4 +1,4 @@
1
- # saddle release 1.8.2
1
+ # saddle release 1.8.4
2
2
 
3
3
  ## release path
4
4
 
@@ -6,10 +6,10 @@ The release path is intentionally split into source validation, package validati
6
6
 
7
7
  | step | owner | condition |
8
8
  |---|---|---|
9
- | package version | repository | `package.json` is `1.8.2` |
9
+ | package version | repository | `package.json` is `1.8.4` |
10
10
  | quality gate | GitHub Actions | `npm run pack:check` passes |
11
- | tag | repository owner | tag `v1.8.2` points to the validated release commit |
12
- | GitHub release | repository owner | release `v1.8.2` is created from the validated tag |
11
+ | tag | repository owner | tag `v1.8.4` points to the validated release commit |
12
+ | GitHub release | repository owner | release `v1.8.4` is created from the validated tag |
13
13
  | GitHub Packages | GitHub Actions | `publishgithubnpm.yml`, `publishghcr.yml`, `publishmaven.yml`, `publishnuget.yml`, and `publishrubygems.yml` use `GITHUB_TOKEN` |
14
14
  | public npmjs | owner-managed GitHub Actions secret | `publishnpmjs.yml` uses `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` and derives the version from the release tag or latest release in manual maintenance runs |
15
15
  | extension zip | GitHub Actions | `buildextension.yml` derives the version from the release tag, validates the unpacked artifact and attaches `saddle-extension-<version>.zip` |
@@ -23,3 +23,5 @@ This release includes the minimal extension permission policy, deterministic ext
23
23
  ## validation boundary
24
24
 
25
25
  The release must pass `npm run check`, `npm run formatcheck`, `npm test`, `npm run pack:check` and `git diff --check` before the tag is created. Each registry workflow remains isolated because its package protocol and credential boundary differ. Publication evidence is reported only after the workflow succeeds and the target registry can be queried independently.
26
+
27
+ The `v1.8.2` release and all publication workflows completed successfully. Public npm and the extension asset were independently visible. GitHub Package registries returned their expected protected-access responses to unauthenticated probes, so their workflow logs remain the authoritative evidence available to the authenticated publisher context.
@@ -0,0 +1,7 @@
1
+ # Saddle 1.8.4
2
+
3
+ Saddle 1.8.4 updates every active library and forge pipeline to Node.js 26.7.0 and adds deterministic package gates to GitLab, Forgejo, Gitea and Woodpecker workflows. GitHub Actions remains the authoritative multi-registry release path; the other forge files are caller-configured validation and artifact paths because their runners, secrets and package registries differ.
4
+
5
+ The legacy public Maven package `io.devthink.saddle` was identified under the `wenathlan` organization. Its deletion request was attempted through the GitHub Packages API, but the authenticated GitHub App integration returned HTTP 403 because it lacks package-management deletion permission. The new owner-aligned Maven coordinate is `io.wenathlan:saddle` and must not be removed.
6
+
7
+ The companion Saddle Pages repository now contains a GitHub Pages workflow using `actions/configure-pages@v5`, `actions/upload-pages-artifact@v4` and `actions/deploy-pages@v4`, plus caller-configured GitLab Pages, Forgejo, Codeberg Pages, Gitea and Woodpecker pipelines. The site builds only `dist/public` with `VITE_BASE_PATH=/saddle-pages/` and uses Node.js 26.7.0.
@@ -14,14 +14,14 @@ 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 and stores only the latest snapshot metadata in session storage. 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. 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.
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
 
21
21
  ## deterministic release artifact
22
22
 
23
- The Node-only build adapter creates an isolated unpacked artifact with the release version in its manifest. A caller can run `npm run extension:build -- --version 1.8.1 --output build/extension` and package that directory with the archive tool available in the host environment. The release workflow derives the version from the published tag and attaches `saddle-extension-<version>.zip` without changing the source manifest.
23
+ The Node-only build adapter creates an isolated unpacked artifact with the release version in its manifest. A caller can run `npm run extension:build -- --version 1.8.2 --output build/extension` and package that directory with the archive tool available in the host environment. The release workflow derives the version from the published tag and attaches `saddle-extension-<version>.zip` without changing the source manifest.
24
24
 
25
25
  ## next slices
26
26
 
27
- The next extension slices should add snapshot diffing, tab and frame identity, resumable command records, 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 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.
@@ -11,7 +11,9 @@ export function createworkerrouter(options = {}) {
11
11
  const storage = options.storage;
12
12
  const contentfile = options.contentfile ?? "content.js";
13
13
  const statekey = options.statekey ?? "saddleextensionstate";
14
+ const maxpending = options.maxpending ?? 32;
14
15
  if (typeof tabs?.sendMessage !== "function") throw new TypeError("extension router requires tabs.sendMessage");
16
+ if (!Number.isSafeInteger(maxpending) || maxpending < 1) throw new TypeError("extension router maxpending must be a positive safe integer");
15
17
 
16
18
  async function ensurecontent(tabid) {
17
19
  if (!Number.isInteger(tabid)) throw new TypeError("extension command requires a tab id");
@@ -29,15 +31,72 @@ export function createworkerrouter(options = {}) {
29
31
  if (typeof storage?.set === "function") await storage.set({ [statekey]: value });
30
32
  }
31
33
 
34
+ async function pendingstate() {
35
+ const value = await readstate();
36
+ return { ...value, pending: Array.isArray(value.pending) ? value.pending.filter(validpending) : [] };
37
+ }
38
+
39
+ async function enqueue(request, sender) {
40
+ const state = await pendingstate();
41
+ const record = pendingrecord(request, sender);
42
+ const pending = state.pending.filter((item) => item.requestid !== record.requestid);
43
+ if (pending.length >= maxpending && !state.pending.some((item) => item.requestid === record.requestid)) throw extensionerror("PENDING_LIMIT", "extension pending command limit reached");
44
+ pending.push(record);
45
+ await savestate({ ...state, pending });
46
+ return record;
47
+ }
48
+
49
+ async function complete(requestid, patch = {}) {
50
+ const state = await pendingstate();
51
+ await savestate({ ...state, ...patch, pending: state.pending.filter((item) => item.requestid !== requestid) });
52
+ }
53
+
54
+ async function markfailure(requestid, error) {
55
+ const state = await pendingstate();
56
+ const pending = state.pending.map((item) => item.requestid === requestid ? { ...item, attempts: item.attempts + 1, lasterror: { code: String(error?.code ?? "extension_error"), message: String(error?.message ?? error) }, updatedat: Date.now() } : item);
57
+ await savestate({ ...state, pending });
58
+ }
59
+
60
+ async function dispatch(request, sender = {}) {
61
+ const tabid = request.payload?.tabid ?? sender.tab?.id;
62
+ await ensurecontent(tabid);
63
+ return tabs.sendMessage(tabid, request);
64
+ }
65
+
32
66
  async function handle(message, sender = {}) {
33
67
  const request = assertmessage(message);
34
68
  if (request.type !== "command") throw new TypeError("extension router accepts commands only");
35
69
  const tabid = request.payload?.tabid ?? sender.tab?.id;
36
- await ensurecontent(tabid);
37
- const response = await tabs.sendMessage(tabid, request);
38
- if (response?.type === "response" && response.payload?.snapshotid) await savestate({ tabid, snapshotid: response.payload.snapshotid, updatedat: Date.now() });
70
+ await enqueue(request, { ...sender, tab: { ...sender.tab, id: tabid } });
71
+ try {
72
+ 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
+ return response;
75
+ } catch (error) {
76
+ await markfailure(request.id, error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ async function rehydrate() { return pendingstate(); }
82
+
83
+ async function resume(requestid, sender = {}) {
84
+ const state = await pendingstate();
85
+ const pending = state.pending.find((item) => item.requestid === requestid);
86
+ if (!pending) throw extensionerror("PENDING_NOT_FOUND", `pending command not found: ${requestid}`);
87
+ 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() });
39
90
  return response;
40
91
  }
41
92
 
42
- return { ensurecontent, readstate, savestate, handle };
93
+ async function cancel(requestid) { const state = await pendingstate(); await savestate({ ...state, pending: state.pending.filter((item) => item.requestid !== requestid) }); }
94
+
95
+ return { ensurecontent, readstate, savestate, rehydrate, enqueue, resume, cancel, handle };
43
96
  }
97
+
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
+
100
+ 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
+
102
+ function extensionerror(code, message) { const error = new Error(message); error.code = code; return error; }
@@ -13,8 +13,10 @@ export function startworker(chromeapi = globalThis.chrome) {
13
13
  router.handle(message, sender).then(sendresponse).catch((error) => sendresponse(createerror(message, error)));
14
14
  return true;
15
15
  };
16
+ const startup = () => router.rehydrate().catch(() => undefined);
16
17
  chromeapi.runtime.onMessage.addListener(listener);
17
- return { router, dispose() { chromeapi.runtime.onMessage.removeListener?.(listener); } };
18
+ chromeapi.runtime.onStartup?.addListener(startup);
19
+ return { router, dispose() { chromeapi.runtime.onMessage.removeListener?.(listener); chromeapi.runtime.onStartup?.removeListener?.(startup); } };
18
20
  }
19
21
 
20
22
  if (globalThis.chrome?.runtime?.onMessage) startworker();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/saddle",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "binary computing engine that turns distributed storage into a publishable working set",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -19,7 +19,7 @@ export function binaryplan(manifest, options = {}) {
19
19
  }
20
20
 
21
21
  export function containerplan(manifest, options = {}) {
22
- const base = options.base ?? "node:22-alpine";
22
+ const base = options.base ?? "node:26.7.0-alpine";
23
23
  const workdir = options.workdir ?? "/app";
24
24
  const command = options.command ?? ["node", manifest.entry];
25
25
  const lines = [`from ${base}`, `workdir ${workdir}`, "copy package.json package-lock.json ./", "run npm ci --omit=dev", "copy . .", `cmd ${JSON.stringify(command)}`];
@@ -5,14 +5,14 @@ import { workflowinputs } from "./manifest.js";
5
5
 
6
6
  export function githubworkflow(manifest) {
7
7
  const input = workflowinputs(manifest);
8
- return `name: ${manifest.name}\non:\n workflow_dispatch:\n inputs:\n jobid:\n required: true\n type: string\n command:\n required: true\n type: string\n default: ${manifest.command}\n repository_dispatch:\n types: [saddle-job]\npermissions:\n contents: read\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${input.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm ci\n - run: \${{ github.event.inputs.command || '${manifest.command}' }}\n env:\n SBOT_JOB_ID: \${{ github.event.inputs.jobid || github.event.client_payload.jobid }}\n - uses: actions/upload-artifact@v4\n with:\n name: saddle-results\n path: ${manifest.artifacts.join("\n ")}\n if-no-files-found: ignore\n`;
8
+ return `name: ${manifest.name}\non:\n workflow_dispatch:\n inputs:\n jobid:\n required: true\n type: string\n command:\n required: true\n type: string\n default: ${manifest.command}\n repository_dispatch:\n types: [saddle-job]\npermissions:\n contents: read\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${input.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 26.7.0\n - run: npm ci\n - run: \${{ github.event.inputs.command || '${manifest.command}' }}\n env:\n SBOT_JOB_ID: \${{ github.event.inputs.jobid || github.event.client_payload.jobid }}\n - uses: actions/upload-artifact@v4\n with:\n name: saddle-results\n path: ${manifest.artifacts.join("\n ")}\n if-no-files-found: ignore\n`;
9
9
  }
10
10
 
11
11
  export function forgejoworkflow(manifest) { return genericworkflow(manifest, "forgejo"); }
12
12
  export function giteaworkflow(manifest) { return genericworkflow(manifest, "gitea"); }
13
- export function woodpeckerworkflow(manifest) { return `when:\n - event: push\n - event: manual\nsteps:\n process:\n image: node:22\n commands:\n - npm ci\n - ${manifest.command}\n`;
13
+ export function woodpeckerworkflow(manifest) { return `when:\n - event: push\n - event: manual\nsteps:\n process:\n image: node:26.7.0\n commands:\n - npm ci\n - ${manifest.command}\n`;
14
14
  }
15
- export function gitlabworkflow(manifest) { return `stages:\n - process\nprocess:\n stage: process\n image: node:22\n script:\n - npm ci\n - ${manifest.command}\n artifacts:\n when: always\n paths:\n${manifest.artifacts.map((item) => ` - ${item}`).join("\n")}\n`; }
15
+ export function gitlabworkflow(manifest) { return `stages:\n - process\nprocess:\n stage: process\n image: node:26.7.0\n script:\n - npm ci\n - ${manifest.command}\n artifacts:\n when: always\n paths:\n${manifest.artifacts.map((item) => ` - ${item}`).join("\n")}\n`; }
16
16
 
17
- function genericworkflow(manifest, name) { return `name: ${manifest.name}\non:\n workflow_dispatch:\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${manifest.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm ci\n - run: ${manifest.command}\n`;
17
+ function genericworkflow(manifest, name) { return `name: ${manifest.name}\non:\n workflow_dispatch:\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${manifest.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 26.7.0\n - run: npm ci\n - run: ${manifest.command}\n`;
18
18
  }