@wenathlan/saddle 1.8.5 → 1.8.7
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 +115 -104
- package/docs/actionsincident.md +33 -0
- package/docs/branchaudit.md +21 -0
- package/docs/ecosystemplan.md +3 -3
- package/docs/featureaudit.md +1 -1
- package/docs/gapmatrix.md +7 -6
- package/docs/platformpipelineaudit.md +6 -2
- package/docs/release.md +5 -5
- package/docs/releaseassets.md +16 -0
- package/docs/securityaudit-1.8.7.md +21 -0
- package/extension/README.md +2 -2
- package/extension/build.js +1 -1
- package/extension/content.js +46 -4
- package/extension/manifest.json +1 -1
- package/extension/pagebridge.js +41 -0
- package/extension/protocol.js +21 -2
- package/extension/serviceworker.js +12 -5
- package/package.json +156 -12
- package/release/assets.js +70 -0
- package/readme.txt +0 -163
- package/scrape/package-lock.json +0 -9397
- package/scrape/package.json +0 -1420
package/extension/content.js
CHANGED
|
@@ -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);
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Saddle browser bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.7",
|
|
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);
|
package/extension/protocol.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.8.7",
|
|
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": {
|
|
15
|
-
|
|
16
|
-
|
|
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": [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,136 @@
|
|
|
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": [
|
|
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
|
+
"@tailwindcss/typography": "^0.5.15",
|
|
182
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
183
|
+
"@types/express": "4.17.21",
|
|
184
|
+
"@types/google.maps": "^3.58.1",
|
|
185
|
+
"@types/node": "^24.7.0",
|
|
186
|
+
"@types/react": "^19.2.1",
|
|
187
|
+
"@types/react-dom": "^19.2.1",
|
|
188
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
189
|
+
"add": "^2.0.6",
|
|
190
|
+
"autoprefixer": "^10.4.20",
|
|
191
|
+
"esbuild": "^0.25.0",
|
|
192
|
+
"postcss": "^8.4.47",
|
|
193
|
+
"prettier": "^3.6.2",
|
|
194
|
+
"tailwindcss": "^4.1.14",
|
|
195
|
+
"tsx": "^4.19.1",
|
|
196
|
+
"tw-animate-css": "^1.4.0",
|
|
197
|
+
"typescript": "5.6.3",
|
|
198
|
+
"vite": "^7.1.7",
|
|
199
|
+
"vite-plugin-manus-runtime": "^0.0.58",
|
|
200
|
+
"vitest": "^4.1.10"
|
|
57
201
|
}
|
|
58
202
|
}
|
|
@@ -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; });
|
package/readme.txt
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
SADDLE - README
|
|
2
|
-
Version 1.0, August 2026
|
|
3
|
-
|
|
4
|
-
Copyright (C) August 2026 devthink, nathlan, iakadion, nathu filho, allan neris, andraneris
|
|
5
|
-
Everyone is permitted to view this document, but changing it
|
|
6
|
-
is not allowed. This document is part of Project saddle.
|
|
7
|
-
|
|
8
|
-
Preamble
|
|
9
|
-
|
|
10
|
-
Project saddle unifies both README sources.
|
|
11
|
-
|
|
12
|
-
Saddle is a JavaScript ESM engine for jobs that move data between storage,
|
|
13
|
-
a working set, an injected runner and durable artifacts. It includes
|
|
14
|
-
contracts for scraping, crawling, browser agents, queues, persistence,
|
|
15
|
-
MCP transport, webhooks and package delivery.
|
|
16
|
-
|
|
17
|
-
Core thesis: storage bytes and compute-memory bytes are the same bytes.
|
|
18
|
-
A Node.js framework runs on other people's runners, loading storage
|
|
19
|
-
buckets as virtual RAM/GPU via storage->RAM bridge.
|
|
20
|
-
|
|
21
|
-
This README is the single source of truth combining Foundation, Engine,
|
|
22
|
-
and Productization sections from both original READMEs.
|
|
23
|
-
|
|
24
|
-
TERMS AND CONDITIONS
|
|
25
|
-
|
|
26
|
-
0. Overview.
|
|
27
|
-
|
|
28
|
-
See full readme.md for complete documentation, API, execution model,
|
|
29
|
-
CLI, security boundaries, package surfaces, development, and repository
|
|
30
|
-
map.
|
|
31
|
-
|
|
32
|
-
1. What is Included.
|
|
33
|
-
|
|
34
|
-
Jobs, Storage, Working set, Scraping, Crawl, Browser, Operations,
|
|
35
|
-
Protocols, Delivery, Agent Browser, Compute Backends, Storage Backends.
|
|
36
|
-
|
|
37
|
-
2. License.
|
|
38
|
-
|
|
39
|
-
Proprietary - View Only. See license.txt.
|
|
40
|
-
|
|
41
|
-
END OF TERMS AND CONDITIONS
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Saddle
|
|
45
|
-
|
|
46
|
-
<p align="center">
|
|
47
|
-
<img src="docs/assets/saddlemark.svg" alt="Saddle" width="720" />
|
|
48
|
-
</p>
|
|
49
|
-
|
|
50
|
-
<p align="center">
|
|
51
|
-
<strong>Storage-backed jobs, scraping contracts and portable runners for Node.js.</strong><br/>
|
|
52
|
-
<strong>Binary computing agent, agent browser, computer-use, scraper and packager.</strong><br/>
|
|
53
|
-
<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>
|
|
54
|
-
<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>
|
|
55
|
-
<a href="https://github.com/wenathlan/saddle/blob/main/license.md"><img src="https://img.shields.io/badge/license-Proprietary--View--Only-202a2f" alt="Proprietary View Only" /></a>
|
|
56
|
-
</p>
|
|
57
|
-
|
|
58
|
-
> **Core idea:** storage is the durable side of the working set; the runner is replaceable; the artifact is the boundary. **Storage == Compute** — RAM and disk are the same construct, differing only by usage flag.
|
|
59
|
-
|
|
60
|
-
Saddle is a **JavaScript ESM engine** for jobs that move data between storage, a working set, an injected runner and durable artifacts. It is also a **virtual machine you publish as a package** that runs on other people's computers (GitHub Actions, Forgejo, Gitea, GitLab, Codeberg, free Docker containers) and turns unlimited third-party storage buckets into virtual RAM/GPU/CPU. Nothing runs on the operator's local machine.
|
|
61
|
-
|
|
62
|
-
Ships as a library, CLI, binary, n8n node, CRX extension, Android/iOS and Tauri desktop app. The canonical JavaScript package is `@wenathlan/saddle`; GitHub Packages npm, Maven and GHCR use the transferred `wenathlan` owner namespace, while NuGet and RubyGems retain their unscoped ecosystem package names.
|
|
63
|
-
|
|
64
|
-
## Start here
|
|
65
|
-
|
|
66
|
-
Saddle requires **Node.js 22 or newer**.
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npm install @wenathlan/saddle
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
import { scrapeurl, formatforagent } from "@wenathlan/saddle";
|
|
74
|
-
|
|
75
|
-
const result = await scrapeurl("https://example.com", { format: "markdown" });
|
|
76
|
-
const context = formatforagent(result, { maxchunksize: 2000, keypoints: 4 });
|
|
77
|
-
|
|
78
|
-
console.log(context.summary);
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Deterministic example with no network:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
node examples/publicapi.js
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## What is included
|
|
88
|
-
|
|
89
|
-
| Area | Contract | Result |
|
|
90
|
-
| --- | --- | --- |
|
|
91
|
-
| Jobs | `engine`, `scheduler`, `inprocess` | `prepare → process → sync → cleanup` |
|
|
92
|
-
| Storage | local, chunked, S3-compatible, GitHub Contents, file hosting | durable objects and chunks |
|
|
93
|
-
| Working set | memory bridge, modes, objects, transforms | storage-to-compute and compute-to-storage |
|
|
94
|
-
| Scraping | robots, cache, extraction, schema, scraper | text, metadata, links and structured output |
|
|
95
|
-
| Crawl | normalization, BFS crawler, persistent frontier | bounded domain-aware crawling |
|
|
96
|
-
| Browser | fingerprint, session, replay and injected agent | browser actions without vendor lock-in |
|
|
97
|
-
| Operations | queues, idempotency, saga, retry, circuit breaker | controlled execution and recovery |
|
|
98
|
-
| Protocols | JSON, NDJSON, SSE, blocks and MCP | transport-neutral messages |
|
|
99
|
-
| Delivery | manifests, workflow registry, binary/container plans | package and runner surfaces |
|
|
100
|
-
| Agent Browser | capture & replay, stealth, fingerprint | Brave capture, movement replay, session recording |
|
|
101
|
-
| Compute Backends | github-actions, huggingface, gitlab-ci, kaggle, oracle-cloud | free runners chain |
|
|
102
|
-
| Storage Backends | HF, Kaggle, Terabox, R2, Telegram, Discord via rclone | unlimited disk as RAM |
|
|
103
|
-
|
|
104
|
-
## Public API
|
|
105
|
-
|
|
106
|
-
| Export | Purpose |
|
|
107
|
-
| --- | --- |
|
|
108
|
-
| `saddleurl` | choose fetch or injected browser path |
|
|
109
|
-
| `scrapeurl` | fetch one URL and extract |
|
|
110
|
-
| `scrapehtml` | extract from HTML without network |
|
|
111
|
-
| `extractcontent` | structured extraction |
|
|
112
|
-
| `serializeresult` | serialize as JSON, Markdown, XML |
|
|
113
|
-
| `formatforagent` | summary, chunks, token count |
|
|
114
|
-
| `batchscrape` | bounded URL groups |
|
|
115
|
-
| `crawlurl` | crawl contract |
|
|
116
|
-
| `browseragent` | navigation, click, type, screenshot |
|
|
117
|
-
| `mcpserver` / `mcptransport` | MCP tools over JSONL/HTTP |
|
|
118
|
-
| `nodeserver` | Web Request/Response handler |
|
|
119
|
-
|
|
120
|
-
Complete API: `docs/libraryapi.md`
|
|
121
|
-
|
|
122
|
-
## The execution model
|
|
123
|
-
|
|
124
|
-
Saddle coordinates contracts instead of hiding providers. A repo + CI runner is a virtual processor:
|
|
125
|
-
|
|
126
|
-
- Repo = Disk (persistent state)
|
|
127
|
-
- CI = CPU (workflow_dispatch = function call)
|
|
128
|
-
- Pages = Bus + CDN
|
|
129
|
-
- Static site = BIOS
|
|
130
|
-
- repository_dispatch = IPC
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
import { engine, eventbus, inprocess, localmemory, localstorage, scheduler } from "@wenathlan/saddle";
|
|
134
|
-
const events = eventbus();
|
|
135
|
-
const run = engine({
|
|
136
|
-
storage: localstorage("./.saddle-data"),
|
|
137
|
-
memory: localmemory(),
|
|
138
|
-
scheduler: scheduler([inprocess()]),
|
|
139
|
-
events
|
|
140
|
-
});
|
|
141
|
-
const result = await run.run(
|
|
142
|
-
{ name: "example", input: { value: 42 } },
|
|
143
|
-
({ job }) => ({ jobid: job.id, ok: true })
|
|
144
|
-
);
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## CLI
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
saddle help
|
|
151
|
-
saddle modes
|
|
152
|
-
saddle runexample
|
|
153
|
-
saddle mcp
|
|
154
|
-
saddle capture --url <url>
|
|
155
|
-
saddle bot --platform github --token $SBOT_TOKEN
|
|
156
|
-
saddle memory --load repo://owner/repo/path/file.json
|
|
157
|
-
saddle deploy --target netlify
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Security boundaries
|
|
161
|
-
|
|
162
|
-
| Boundary | Policy |
|
|
163
|
-
| --- | --- |
|