@wenathlan/saddle 1.8.1
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/LICENSE +203 -0
- package/README.md +192 -0
- package/adapters/forge.js +16 -0
- package/adapters/forgejo.js +8 -0
- package/adapters/github.js +19 -0
- package/adapters/gitlab.js +10 -0
- package/adapters/huggingface.js +6 -0
- package/adapters/socket.js +14 -0
- package/adapters/transport.js +30 -0
- package/ai/chunk.js +22 -0
- package/ai/llmstxt.js +12 -0
- package/ai/provenance.js +18 -0
- package/ai/rag.js +14 -0
- package/ai/tokens.js +9 -0
- package/api/auth.js +13 -0
- package/api/contracts.js +17 -0
- package/api/control.js +33 -0
- package/api/http.js +12 -0
- package/api/rate.js +31 -0
- package/api/security.js +42 -0
- package/api/service.js +36 -0
- package/binary/build.js +17 -0
- package/bot/adapter.js +8 -0
- package/bot/bot.js +39 -0
- package/bot/commands.js +18 -0
- package/bot/permissions.js +16 -0
- package/browser/actions.js +33 -0
- package/browser/agent.js +9 -0
- package/browser/context.js +52 -0
- package/browser/fingerprint.js +12 -0
- package/browser/index.js +10 -0
- package/browser/recorder.js +15 -0
- package/browser/session.js +19 -0
- package/browser/snapshot.js +57 -0
- package/captcha/contract.js +15 -0
- package/captcha/evidence.js +9 -0
- package/captcha/guard.js +10 -0
- package/cli/main.js +36 -0
- package/core/errors.js +37 -0
- package/core/events.js +21 -0
- package/core/hash.js +73 -0
- package/core/ids.js +15 -0
- package/crawl/crawler.js +29 -0
- package/crawl/frontier.js +34 -0
- package/crawl/normalize.js +14 -0
- package/crawl/persistent.js +13 -0
- package/dispatch/resumable.js +31 -0
- package/dispatch/workflow.js +33 -0
- package/docs/assets/architecture.svg +45 -0
- package/docs/assets/saddlemark.svg +13 -0
- package/docs/comparativeaudit.md +63 -0
- package/docs/ecosystemplan.md +59 -0
- package/docs/enginearchitecture.md +83 -0
- package/docs/featureaudit.md +63 -0
- package/docs/gapmatrix.md +80 -0
- package/docs/libraryapi.md +63 -0
- package/docs/modes.md +27 -0
- package/docs/productindex.md +28 -0
- package/docs/registryresearch.md +56 -0
- package/docs/release.md +28 -0
- package/docs/release17notes.md +24 -0
- package/docs/release181notes.md +15 -0
- package/docs/release18notes.md +15 -0
- package/docs/roadmapp2p3.md +33 -0
- package/docs/toolchains.md +28 -0
- package/docs/usage.md +107 -0
- package/domain/artifacts.js +13 -0
- package/domain/jobs.js +20 -0
- package/domain/providers.js +8 -0
- package/domain/runtime.js +10 -0
- package/domain/sessions.js +34 -0
- package/errors/taxonomy.js +18 -0
- package/examples/localjob.js +15 -0
- package/examples/publicapi.js +7 -0
- package/extension/README.md +23 -0
- package/extension/content.js +85 -0
- package/extension/index.js +5 -0
- package/extension/manifest.json +10 -0
- package/extension/popup.css +13 -0
- package/extension/popup.html +24 -0
- package/extension/popup.js +25 -0
- package/extension/protocol.js +76 -0
- package/extension/serviceworker.js +43 -0
- package/extension/worker.js +20 -0
- package/format/check.js +21 -0
- package/index.js +120 -0
- package/library/public.js +83 -0
- package/license.md +203 -0
- package/license.txt +203 -0
- package/mcp/browser.js +12 -0
- package/mcp/server.js +28 -0
- package/mcp/transport.js +14 -0
- package/memory/bridge.js +16 -0
- package/memory/engine.js +45 -0
- package/memory/modes.js +55 -0
- package/memory/objects.js +18 -0
- package/memory/targets.js +21 -0
- package/memory/transforms.js +15 -0
- package/modes/matrix.js +20 -0
- package/modes/modes.js +16 -0
- package/modes/resolve.js +39 -0
- package/package.json +47 -0
- package/packager/manifest.js +28 -0
- package/packager/publish.js +15 -0
- package/persistence/adapter.js +8 -0
- package/persistence/drizzle.js +10 -0
- package/persistence/memory.js +26 -0
- package/persistence/migrations.js +14 -0
- package/persistence/prisma.js +23 -0
- package/persistence/schema.js +29 -0
- package/persistence/sql.js +30 -0
- package/protocol/blocks.js +18 -0
- package/protocol/json.js +5 -0
- package/protocol/ndjson.js +17 -0
- package/protocol/sse.js +22 -0
- package/proxy/pool.js +12 -0
- package/queue/idempotency.js +12 -0
- package/queue/persistent.js +44 -0
- package/queue/queue.js +50 -0
- package/queue/saga.js +13 -0
- package/readme.txt +163 -0
- package/retry/circuit.js +15 -0
- package/retry/policy.js +12 -0
- package/runners/health.js +23 -0
- package/runners/heartbeat.js +26 -0
- package/runners/inprocess.js +19 -0
- package/runners/scheduler.js +16 -0
- package/runtime/abort.js +10 -0
- package/runtime/compatibility.js +13 -0
- package/runtime/detect.js +14 -0
- package/runtime/engine.js +56 -0
- package/runtime/worker.js +18 -0
- package/scrape/cache.js +14 -0
- package/scrape/extract.js +14 -0
- package/scrape/robots.js +32 -0
- package/scrape/schema.js +21 -0
- package/scrape/scraper.js +40 -0
- package/scrape/semantic.js +22 -0
- package/server/node.js +34 -0
- package/sessions/file.js +13 -0
- package/sessions/replay.js +21 -0
- package/sessions/store.js +13 -0
- package/storage/adapter.js +8 -0
- package/storage/cache.js +54 -0
- package/storage/checksum.js +17 -0
- package/storage/chunked.js +58 -0
- package/storage/content.js +42 -0
- package/storage/filehosting.js +17 -0
- package/storage/githubcontents.js +18 -0
- package/storage/index.js +10 -0
- package/storage/local.js +35 -0
- package/storage/memory.js +28 -0
- package/storage/s3compatible.js +23 -0
- package/storage/sync.js +55 -0
- package/surfaces/adapters.js +48 -0
- package/surfaces/controls.js +37 -0
- package/surfaces/manifest.js +25 -0
- package/surfaces/n8n.js +24 -0
- package/surfaces/operations.js +43 -0
- package/surfaces/targets.js +16 -0
- package/webhook/delivery.js +26 -0
- package/webhook/receiver.js +20 -0
- package/webhook/signature.js +7 -0
- package/workflow/manifest.js +20 -0
- package/workflow/registry.js +16 -0
- package/workflow/templates.js +18 -0
- package/workflow/triggers.js +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
PROPRIETARY SOURCE-AVAILABLE LICENSE - VIEW ONLY
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (C) August 2026 devthink, nathlan, akadion, nathu filho, allan neris, andraneris
|
|
5
|
+
Everyone is permitted to view this license document, but changing it
|
|
6
|
+
is not allowed. This license is NOT an Open Source license.
|
|
7
|
+
|
|
8
|
+
Preamble
|
|
9
|
+
|
|
10
|
+
This License governs the source code made available by the Licensor.
|
|
11
|
+
Unlike free software licenses such as the GNU GPL, this is a
|
|
12
|
+
proprietary, source-available license.
|
|
13
|
+
|
|
14
|
+
The Software is made visible for transparency and verification
|
|
15
|
+
purposes only. It is NOT free software. It is NOT open source.
|
|
16
|
+
|
|
17
|
+
When we speak of proprietary, we are referring to full ownership and
|
|
18
|
+
restriction, not freedom. Our intention is to protect the Licensor's
|
|
19
|
+
exclusive rights to copy, modify, distribute and use the work. By
|
|
20
|
+
contrast to the GPL which guarantees your freedom to share and change,
|
|
21
|
+
this License is intended to guarantee that the Software remains the
|
|
22
|
+
exclusive property of its authors and that no rights beyond viewing
|
|
23
|
+
are granted.
|
|
24
|
+
|
|
25
|
+
To protect our rights, we must prevent others from copying, modifying,
|
|
26
|
+
distributing, or using the Software without permission. Therefore,
|
|
27
|
+
you have no rights to convey, modify, or use the Software beyond
|
|
28
|
+
viewing it in its public repository form.
|
|
29
|
+
|
|
30
|
+
For the authors' protection, this License clearly explains that there
|
|
31
|
+
is no warranty for this proprietary software and that any unauthorized
|
|
32
|
+
use will be treated as copyright infringement.
|
|
33
|
+
|
|
34
|
+
The precise terms and conditions for viewing and restriction follow.
|
|
35
|
+
|
|
36
|
+
TERMS AND CONDITIONS
|
|
37
|
+
|
|
38
|
+
0. Definitions.
|
|
39
|
+
|
|
40
|
+
"This License" refers to version 1.0 of the Proprietary Source-Available
|
|
41
|
+
License - View Only.
|
|
42
|
+
|
|
43
|
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
44
|
+
works.
|
|
45
|
+
|
|
46
|
+
"The Program" or "The Software" refers to any copyrightable work licensed
|
|
47
|
+
under this License, including source code, object code, documentation,
|
|
48
|
+
and associated files. Each licensee is addressed as "you".
|
|
49
|
+
|
|
50
|
+
"Licensor" refers to the copyright holders: devthink, nathlan, akadion,
|
|
51
|
+
nathu filho, alllan neris, andraneris.
|
|
52
|
+
|
|
53
|
+
To "view" means to read and examine the Software in its public repository
|
|
54
|
+
form through a web browser, without creating a local copy beyond the
|
|
55
|
+
temporary cache of your browser.
|
|
56
|
+
|
|
57
|
+
To "use" means to execute, run, compile, deploy, host, or otherwise
|
|
58
|
+
benefit from the functionality of the Software, which is NOT permitted.
|
|
59
|
+
|
|
60
|
+
1. Ownership and Intellectual Property.
|
|
61
|
+
|
|
62
|
+
The Software is and shall remain the exclusive property of the Licensor.
|
|
63
|
+
All right, title, and interest in and to the Software, including all
|
|
64
|
+
copyrights, trademarks, trade secrets, and other intellectual property
|
|
65
|
+
rights, are owned solely by the Licensor.
|
|
66
|
+
|
|
67
|
+
No ownership rights, implied or otherwise, are transferred to you under
|
|
68
|
+
this License. All rights not expressly granted are reserved.
|
|
69
|
+
|
|
70
|
+
2. Grant of Rights - VIEW ONLY.
|
|
71
|
+
|
|
72
|
+
Subject to the terms of this License, Licensor grants you a limited,
|
|
73
|
+
non-exclusive, non-transferable, revocable license solely to VIEW the
|
|
74
|
+
Software in its public repository form for personal evaluation.
|
|
75
|
+
|
|
76
|
+
This grant DOES NOT include any right to:
|
|
77
|
+
|
|
78
|
+
a) use, execute, or run the Software;
|
|
79
|
+
b) copy, reproduce, or duplicate;
|
|
80
|
+
c) modify, adapt, or create derivative works;
|
|
81
|
+
d) distribute, publish, sublicense, or sell;
|
|
82
|
+
e) reverse engineer or decompile.
|
|
83
|
+
|
|
84
|
+
3. Prohibitions and Restrictions.
|
|
85
|
+
|
|
86
|
+
You are STRICTLY PROHIBITED from undertaking any of the following
|
|
87
|
+
without prior written permission from the Licensor:
|
|
88
|
+
|
|
89
|
+
a) Copying, reproducing, duplicating, or storing the Software, in
|
|
90
|
+
whole or in part, in any medium;
|
|
91
|
+
|
|
92
|
+
b) Modifying, adapting, translating, transforming, or creating
|
|
93
|
+
derivative works based on the Software;
|
|
94
|
+
|
|
95
|
+
c) Distributing, publishing, sublicensing, disclosing, or otherwise
|
|
96
|
+
making the Software available to third parties, whether gratis or
|
|
97
|
+
for a fee;
|
|
98
|
+
|
|
99
|
+
d) Using the Software for any purpose, including but not limited to
|
|
100
|
+
development, production, commercial use, internal business operations,
|
|
101
|
+
offering as a service (SaaS), or training artificial intelligence
|
|
102
|
+
models;
|
|
103
|
+
|
|
104
|
+
e) Removing, altering, or obscuring any copyright, proprietary, or
|
|
105
|
+
license notices contained in the Software;
|
|
106
|
+
|
|
107
|
+
f) Reverse engineering, decompiling, disassembling, or attempting to
|
|
108
|
+
derive the source code of the Software.
|
|
109
|
+
|
|
110
|
+
Any use beyond viewing constitutes copyright infringement and will be
|
|
111
|
+
prosecuted to the fullest extent of the law.
|
|
112
|
+
|
|
113
|
+
4. Confidentiality.
|
|
114
|
+
|
|
115
|
+
The Software contains confidential information and proprietary trade
|
|
116
|
+
secrets of the Licensor. Unauthorized access, inspection, or disclosure
|
|
117
|
+
is strictly prohibited and may result in civil and criminal liability.
|
|
118
|
+
|
|
119
|
+
5. No Warranty.
|
|
120
|
+
|
|
121
|
+
The Software is provided for viewing only and may contain errors or
|
|
122
|
+
omissions. No support, maintenance, or updates are implied.
|
|
123
|
+
|
|
124
|
+
6. Acceptance Not Required for Viewing.
|
|
125
|
+
|
|
126
|
+
You are not required to accept this License to view the Software in a
|
|
127
|
+
web browser. However, any action beyond viewing, such as copying,
|
|
128
|
+
modifying, or using the Software, requires explicit written permission
|
|
129
|
+
and a separate license agreement. By doing so without permission, you
|
|
130
|
+
infringe copyright.
|
|
131
|
+
|
|
132
|
+
7. Termination.
|
|
133
|
+
|
|
134
|
+
This License and all rights granted herein automatically terminate upon
|
|
135
|
+
any breach of Section 3 by you. Upon termination, you must cease all
|
|
136
|
+
viewing and destroy all copies, including cached or local copies, of
|
|
137
|
+
the Software in your possession.
|
|
138
|
+
|
|
139
|
+
Moreover, termination does not terminate the rights of the Licensor
|
|
140
|
+
to seek damages or injunctive relief.
|
|
141
|
+
|
|
142
|
+
8. Disclaimer of Warranty.
|
|
143
|
+
|
|
144
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
145
|
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
146
|
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
147
|
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
148
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
149
|
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
150
|
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
151
|
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
152
|
+
|
|
153
|
+
9. Limitation of Liability.
|
|
154
|
+
|
|
155
|
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
156
|
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO HAS VIEWED THE PROGRAM
|
|
157
|
+
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
158
|
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
159
|
+
VIEWING OR INABILITY TO VIEW THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
|
|
160
|
+
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
161
|
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE), EVEN IF SUCH HOLDER OR
|
|
162
|
+
OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
163
|
+
|
|
164
|
+
10. Interpretation and Governing Law.
|
|
165
|
+
|
|
166
|
+
If the disclaimer of warranty and limitation of liability provided above
|
|
167
|
+
cannot be given local legal effect according to their terms, reviewing
|
|
168
|
+
courts shall apply local law that most closely approximates an absolute
|
|
169
|
+
waiver of all civil liability in connection with the Program, unless a
|
|
170
|
+
warranty or assumption of liability accompanies a copy of the Program
|
|
171
|
+
in return for a fee.
|
|
172
|
+
|
|
173
|
+
This License shall be governed by the laws of Brazil.
|
|
174
|
+
|
|
175
|
+
END OF TERMS AND CONDITIONS
|
|
176
|
+
|
|
177
|
+
How to Apply These Terms to Your New Programs
|
|
178
|
+
|
|
179
|
+
If you develop a new program, and you want it to remain proprietary and
|
|
180
|
+
fully owned by you, with source visible but with no rights granted, the
|
|
181
|
+
best way to achieve this is to attach the following notices to the program.
|
|
182
|
+
It is safest to attach them to the start of each source file.
|
|
183
|
+
|
|
184
|
+
<one line to give the program's name and a brief idea of what it does.>
|
|
185
|
+
Copyright (C) August 2026 devthink, nathlan, akadion, nathu filho, alllan neris, andraneris
|
|
186
|
+
Project ID: <saddle>
|
|
187
|
+
|
|
188
|
+
This program is proprietary and confidential: you can view its source
|
|
189
|
+
code for evaluation purposes only under the terms of the Proprietary
|
|
190
|
+
Source-Available License - View Only as published by the Licensor.
|
|
191
|
+
You are not permitted to copy, modify, distribute, or use this program
|
|
192
|
+
without prior written permission.
|
|
193
|
+
|
|
194
|
+
This program is distributed in the hope that it will be transparent,
|
|
195
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
196
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
197
|
+
Proprietary Source-Available License for more details.
|
|
198
|
+
|
|
199
|
+
You should have received a copy of the Proprietary Source-Available
|
|
200
|
+
License along with this program. If not, see
|
|
201
|
+
<https://github.com/iakadion/saddle/LICENSE>
|
|
202
|
+
|
|
203
|
+
Also add information on how to contact you by electronic and paper mail.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Saddle
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/assets/saddlemark.svg" alt="Saddle" width="720" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Storage-backed jobs, scraping contracts and portable runners for Node.js.</strong><br/>
|
|
9
|
+
<strong>Binary computing agent, agent browser, computer-use, scraper and packager.</strong><br/>
|
|
10
|
+
<a href="https://github.com/iakadion/saddle/actions/workflows/ci.yml"><img src="https://github.com/iakadion/saddle/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
11
|
+
<a href="https://github.com/iakadion/saddle/releases/tag/v1.8.0"><img src="https://img.shields.io/badge/release-v1.8.0-d35d3d" alt="Release 1.8.0" /></a>
|
|
12
|
+
<a href="https://github.com/iakadion/saddle/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-202a2f" alt="GPL 3.0 license" /></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
> **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.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
Ships as a library, CLI, binary, n8n node, CRX extension, Android/iOS and Tauri desktop app. Package `@wenathlan/saddle` — published to npm, GitHub Packages, Maven, NuGet, RubyGems and GHCR (auto-mirrored to jsDelivr).
|
|
20
|
+
|
|
21
|
+
## Start here
|
|
22
|
+
|
|
23
|
+
Saddle requires **Node.js 22 or newer**.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @wenathlan/saddle
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { scrapeurl, formatforagent } from "@wenathlan/saddle";
|
|
31
|
+
|
|
32
|
+
const result = await scrapeurl("https://example.com", { format: "markdown" });
|
|
33
|
+
const context = formatforagent(result, { maxchunksize: 2000, keypoints: 4 });
|
|
34
|
+
|
|
35
|
+
console.log(context.summary);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Deterministic example with no network:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node examples/publicapi.js
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What is included
|
|
45
|
+
|
|
46
|
+
| Area | Contract | Result |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| Jobs | `engine`, `scheduler`, `inprocess` | `prepare → process → sync → cleanup` |
|
|
49
|
+
| Storage | local, chunked, content-addressed, S3-compatible, GitHub Contents, file hosting | durable objects, ranges, dedupe and sync |
|
|
50
|
+
| Working set | memory bridge, modes, objects, transforms | storage-to-compute and compute-to-storage |
|
|
51
|
+
| Scraping | robots, cache, extraction, semantic facts, schema, scraper | text, metadata, links, controls and structured output |
|
|
52
|
+
| Crawl | normalization, priority frontier, BFS crawler, per-domain budgets and persistent frontier | bounded domain-aware crawling |
|
|
53
|
+
| Browser | snapshots, tabs, frames, actions, fingerprint, session, replay and injected agent | browser actions without vendor lock-in |
|
|
54
|
+
| Operations | queues, idempotency, saga, retry, circuit breaker, health and heartbeat | controlled execution and recovery |
|
|
55
|
+
| Protocols | JSON, NDJSON, SSE, blocks, API envelopes and MCP | transport-neutral messages |
|
|
56
|
+
| Delivery | manifests, workflow registry, binary/container plans | package and runner surfaces |
|
|
57
|
+
| Integrations | GitHub, GitLab, Forgejo, app lifecycle, command scopes and delivery adapters | caller-owned provider connectivity |
|
|
58
|
+
| Agent Browser | capture & replay, stealth, fingerprint | Brave capture, movement replay, session recording |
|
|
59
|
+
| Compute Backends | github-actions, huggingface, gitlab-ci, kaggle, oracle-cloud | free runners chain |
|
|
60
|
+
| Storage Backends | HF, Kaggle, Terabox, R2, Telegram, Discord via rclone | unlimited disk as RAM |
|
|
61
|
+
| Extension | Manifest V3 bridge, snapshot protocol, popup and service worker | user initiated browser control |
|
|
62
|
+
|
|
63
|
+
## Public API
|
|
64
|
+
|
|
65
|
+
| Export | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `saddleurl` | choose fetch or injected browser path |
|
|
68
|
+
| `scrapeurl` | fetch one URL and extract |
|
|
69
|
+
| `scrapehtml` | extract from HTML without network |
|
|
70
|
+
| `extractcontent` | structured extraction |
|
|
71
|
+
| `serializeresult` | serialize as JSON, Markdown, XML |
|
|
72
|
+
| `formatforagent` | summary, chunks, token count |
|
|
73
|
+
| `batchscrape` | bounded URL groups |
|
|
74
|
+
| `crawlurl` | crawl contract |
|
|
75
|
+
| `browseragent` | navigation, click, type, screenshot |
|
|
76
|
+
| `mcpserver` / `mcptransport` | MCP tools over JSONL/HTTP |
|
|
77
|
+
| `nodeserver` | Web Request/Response handler |
|
|
78
|
+
|
|
79
|
+
Complete API: `docs/libraryapi.md`. Surface overview: [`docs/productindex.md`](docs/productindex.md). Usage examples: [`docs/usage.md`](docs/usage.md).
|
|
80
|
+
|
|
81
|
+
## The execution model
|
|
82
|
+
|
|
83
|
+
Saddle coordinates contracts instead of hiding providers. A repo + CI runner is a virtual processor:
|
|
84
|
+
|
|
85
|
+
- Repo = Disk (persistent state)
|
|
86
|
+
- CI = CPU (workflow_dispatch = function call)
|
|
87
|
+
- Pages = Bus + CDN
|
|
88
|
+
- Static site = BIOS
|
|
89
|
+
- repository_dispatch = IPC
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { engine, eventbus, inprocess, scheduler } from "@wenathlan/saddle";
|
|
93
|
+
import { localmemory } from "@wenathlan/saddle/memory-node";
|
|
94
|
+
import { localstorage } from "@wenathlan/saddle/storage-node";
|
|
95
|
+
const events = eventbus();
|
|
96
|
+
const run = engine({
|
|
97
|
+
storage: localstorage("./.saddle-data"),
|
|
98
|
+
memory: localmemory(),
|
|
99
|
+
scheduler: scheduler([inprocess()]),
|
|
100
|
+
events
|
|
101
|
+
});
|
|
102
|
+
const result = await run.run(
|
|
103
|
+
{ name: "example", input: { value: 42 } },
|
|
104
|
+
({ job }) => ({ jobid: job.id, ok: true })
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The caller still chooses how to provide `fetcher`, browser transport, persistence, proxy pool, captcha solver, webhook secret and remote credentials. The root entry is transport-neutral; Node filesystem and HTTP adapters are explicit subpaths such as `@wenathlan/saddle/storage-node`, `@wenathlan/saddle/memory-node`, `@wenathlan/saddle/server-node`, `@wenathlan/saddle/sessions-file` and `@wenathlan/saddle/queue-persistent`. Saddle does not embed secrets, fixed hosts or a mandatory cloud vendor.
|
|
109
|
+
|
|
110
|
+
## Browser extension
|
|
111
|
+
|
|
112
|
+
Version 1.1 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.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# load the unpacked extension from chrome://extensions
|
|
116
|
+
ls extension/manifest.json extension/worker.js extension/content.js extension/popup.html
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The extension requests `activeTab`, `scripting` and `storage`; it does not request broad host permissions, cookies, `webRequest`, debugger access or arbitrary page code execution. Its public contracts are available from `@wenathlan/saddle/extension`. See [`extension/README.md`](extension/README.md) for the unpacked development flow.
|
|
120
|
+
|
|
121
|
+
## CLI
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
saddle help
|
|
125
|
+
saddle modes
|
|
126
|
+
saddle runexample
|
|
127
|
+
saddle mcp
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Security boundaries
|
|
131
|
+
|
|
132
|
+
| Boundary | Policy |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| Credentials | injected at runtime; never committed |
|
|
135
|
+
| Network | http/https validated; private targets blocked |
|
|
136
|
+
| Crawling | robots rules and crawl delay explicit |
|
|
137
|
+
| Storage | adapters replaceable |
|
|
138
|
+
| Runtime | Node HTTP isolated |
|
|
139
|
+
| Failure | retry, circuit breaker, idempotency configurable |
|
|
140
|
+
|
|
141
|
+
## Package surfaces
|
|
142
|
+
|
|
143
|
+
| Registry | Artifact | Workflow | Status |
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
|
+
| GitHub npm | `@iakadion/saddle@1.8.0` | publishgithubnpm.yml | published |
|
|
146
|
+
| GHCR | `ghcr.io/iakadion/saddle:1.8.0` and `latest` | publishghcr.yml | published |
|
|
147
|
+
| Maven | `io.devthink:saddle:1.8.0` | publishmaven.yml | published after JDK 26 retry |
|
|
148
|
+
| NuGet | `Saddle.1.8.0.nupkg` | publishnuget.yml | published |
|
|
149
|
+
| RubyGems | `saddle 1.8.0` | publishrubygems.yml | published |
|
|
150
|
+
| npmjs | `@wenathlan/saddle@1.8.1` | publishnpmjs.yml | pending follow-up release workflow |
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm ci
|
|
156
|
+
npm test
|
|
157
|
+
npm run check
|
|
158
|
+
npm run formatcheck
|
|
159
|
+
npm run pack:check
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Test suite deterministic, no network or real credentials required.
|
|
163
|
+
|
|
164
|
+
## Repository map
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
core/ errors, events and identifiers
|
|
168
|
+
domain/ jobs, artifacts, sessions and providers
|
|
169
|
+
memory/ working-set bridge, modes, objects and transforms
|
|
170
|
+
storage/ local, chunked, remote and file-hosting adapters
|
|
171
|
+
scrape/ robots, cache, extraction, schema and scraper
|
|
172
|
+
crawl/ URL normalization, crawler and persistent frontier
|
|
173
|
+
queue/ queue, idempotency, saga and recovery
|
|
174
|
+
browser/ fingerprint, session and agent contracts
|
|
175
|
+
browser/ snapshots, tabs, frames, actions and recorder contracts
|
|
176
|
+
mcp/ optional server and JSONL/HTTP transport
|
|
177
|
+
protocol/ JSON, NDJSON, SSE and block serializers
|
|
178
|
+
workflow/ manifests, templates and registry
|
|
179
|
+
tests/ deterministic engine coverage
|
|
180
|
+
docs/ architecture, API, release and registry notes
|
|
181
|
+
surfaces/ browser, extension, desktop, mobile and n8n contracts
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Root-based JavaScript ESM layout, no src/ directory, no TypeScript build required.
|
|
185
|
+
|
|
186
|
+
## Current scope
|
|
187
|
+
|
|
188
|
+
Version 1.8 establishes the engine contracts, browser snapshot foundation, storage sync primitives, runner recovery contracts, scraping context provenance, API/MCP security contracts, bot integration lifecycle, the first tested extension bridge, 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.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
Saddle is distributed under the [GNU General Public License v3.0](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forge adapter defines the common dispatch and artifact surface for compatible forges.
|
|
3
|
+
*/
|
|
4
|
+
import { transport } from "./transport.js";
|
|
5
|
+
|
|
6
|
+
export function forgeadapter(options = {}) {
|
|
7
|
+
if (!options.baseurl || typeof options.token !== "function") throw new TypeError("forge adapter requires baseurl and token function");
|
|
8
|
+
const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
|
|
9
|
+
async function call(path, init = {}) { const token = await options.token(); return client.request(new URL(path, options.baseurl), { ...init, headers: { authorization: `Bearer ${token}`, accept: "application/json", ...(init.headers ?? {}) } }); }
|
|
10
|
+
return {
|
|
11
|
+
kind: options.kind ?? "forge",
|
|
12
|
+
async health(path = "/") { const response = await call(path); return { ok: response.ok, status: response.status }; },
|
|
13
|
+
async dispatch(spec) { if (!spec?.path || !spec.ref) throw new TypeError("forge dispatch requires path and ref"); const response = await call(spec.path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: spec.ref, inputs: spec.inputs ?? {} }) }); return { accepted: response.ok, status: response.status, body: response.json ? await response.json() : undefined }; },
|
|
14
|
+
async upload(spec) { if (!spec?.path || !spec.data) throw new TypeError("forge upload requires path and data"); const response = await call(spec.path, { method: "PUT", headers: { "content-type": spec.contenttype ?? "application/octet-stream" }, body: spec.data }); return { accepted: response.ok, status: response.status }; }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forgejo and gitea can reuse the open forge contract with a caller supplied base url.
|
|
3
|
+
*/
|
|
4
|
+
import { forgeadapter } from "./forge.js";
|
|
5
|
+
|
|
6
|
+
export function forgejoadapter(options = {}) { return forgeadapter({ ...options, kind: "forgejo" }); }
|
|
7
|
+
export function giteaadapter(options = {}) { return forgeadapter({ ...options, kind: "gitea" }); }
|
|
8
|
+
export function codebergadapter(options = {}) { return forgeadapter({ ...options, kind: "codeberg" }); }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* github adapter uses caller supplied credentials and base url configuration.
|
|
3
|
+
*/
|
|
4
|
+
import { transport } from "./transport.js";
|
|
5
|
+
|
|
6
|
+
export function githubadapter(options = {}) {
|
|
7
|
+
if (!options.baseurl || typeof options.token !== "function") throw new TypeError("github adapter requires baseurl and token function");
|
|
8
|
+
const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
|
|
9
|
+
async function call(path, init = {}) {
|
|
10
|
+
const token = await options.token();
|
|
11
|
+
const headers = { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "x-github-api-version": options.apiversion ?? "2022-11-28", ...(init.headers ?? {}) };
|
|
12
|
+
return client.request(new URL(path, options.baseurl), { ...init, headers });
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
async health() { const response = await call("/rate_limit"); return { ok: response.ok, status: response.status }; },
|
|
16
|
+
async dispatch(owner, repository, workflow, input = {}) { const response = await call(`/repos/${owner}/${repository}/actions/workflows/${workflow}/dispatches`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: input.ref ?? "main", inputs: input.inputs ?? {} }) }); return { accepted: response.status === 204, status: response.status }; },
|
|
17
|
+
async run(owner, repository, runid) { const response = await call(`/repos/${owner}/${repository}/actions/runs/${runid}`); return response.json(); }
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitlab adapter keeps project addressing and token ownership outside the package.
|
|
3
|
+
*/
|
|
4
|
+
import { forgeadapter } from "./forge.js";
|
|
5
|
+
|
|
6
|
+
export function gitlabadapter(options = {}) {
|
|
7
|
+
const project = encodeURIComponent(options.project ?? "");
|
|
8
|
+
const base = forgeadapter({ ...options, kind: "gitlab" });
|
|
9
|
+
return { ...base, async dispatch(spec) { return base.dispatch({ ...spec, path: spec.path ?? `/api/v4/projects/${project}/pipeline` }); } };
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* socket adapter keeps realtime optional and accepts a caller supplied websocket constructor.
|
|
3
|
+
*/
|
|
4
|
+
export function socketadapter(options = {}) {
|
|
5
|
+
const websocket = options.websocket ?? globalThis.WebSocket;
|
|
6
|
+
if (!websocket) throw new Error("websocket implementation is required");
|
|
7
|
+
return {
|
|
8
|
+
connect(url, protocols) {
|
|
9
|
+
if (!url) throw new TypeError("socket url is required");
|
|
10
|
+
const socket = new websocket(url, protocols);
|
|
11
|
+
return { socket, send(value) { socket.send(typeof value === "string" ? value : JSON.stringify(value)); }, close(code, reason) { socket.close(code, reason); } };
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transport centralizes timeout retry and jitter without choosing a host or vendor.
|
|
3
|
+
*/
|
|
4
|
+
export function transport(options = {}) {
|
|
5
|
+
const fetcher = options.fetcher ?? fetch;
|
|
6
|
+
const attempts = options.attempts ?? 3;
|
|
7
|
+
const timeout = options.timeout ?? 30000;
|
|
8
|
+
const retrycodes = new Set(options.retrycodes ?? [408, 409, 429, 500, 502, 503, 504]);
|
|
9
|
+
async function request(url, init = {}) {
|
|
10
|
+
let last;
|
|
11
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetcher(url, { ...init, signal: init.signal ?? controller.signal });
|
|
16
|
+
if (response.ok || !retrycodes.has(response.status) || attempt === attempts - 1) return response;
|
|
17
|
+
last = new Error(`request failed with ${response.status}`);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
last = error;
|
|
20
|
+
if (attempt === attempts - 1) throw error;
|
|
21
|
+
} finally { clearTimeout(timer); }
|
|
22
|
+
await delay(backoff(options, attempt));
|
|
23
|
+
}
|
|
24
|
+
throw last ?? new Error("request failed");
|
|
25
|
+
}
|
|
26
|
+
return { request };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function backoff(options, attempt) { const base = options.backoff ?? 250; const jitter = options.jitter ?? 0; return base * 2 ** attempt + Math.floor(Math.random() * jitter); }
|
|
30
|
+
function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }
|
package/ai/chunk.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown chunking preserves heading paths and uses paragraph boundaries before hard cuts.
|
|
3
|
+
*/
|
|
4
|
+
import { estimatetokens } from "./tokens.js";
|
|
5
|
+
|
|
6
|
+
export function chunkmarkdown(markdown, options = {}) {
|
|
7
|
+
const maxtokens = options.maxtokens ?? 512;
|
|
8
|
+
const overlap = options.overlaptokens ?? 50;
|
|
9
|
+
const lines = String(markdown ?? "").split(/\r?\n/);
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let headingpath = [];
|
|
12
|
+
let buffer = [];
|
|
13
|
+
function flush() { if (!buffer.length) return; const content = buffer.join("\n").trim(); if (content) chunks.push({ content, headingpath: [...headingpath], tokencount: estimatetokens(content, options.model) }); buffer = []; }
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
16
|
+
if (heading) { flush(); const level = heading[1].length; headingpath = headingpath.slice(0, level - 1); headingpath[level - 1] = heading[2].trim(); buffer.push(line); continue; }
|
|
17
|
+
buffer.push(line);
|
|
18
|
+
if (estimatetokens(buffer.join("\n"), options.model) > maxtokens) { const last = buffer.pop(); flush(); const overlaptext = buffer.slice(-overlap).join("\n"); buffer = overlaptext ? [overlaptext, last] : [last]; }
|
|
19
|
+
}
|
|
20
|
+
flush();
|
|
21
|
+
return chunks.map((chunk, index) => ({ ...chunk, id: `chunk${index}` }));
|
|
22
|
+
}
|
package/ai/llmstxt.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llms text generation creates compact absolute links for agent consumption.
|
|
3
|
+
*/
|
|
4
|
+
export function llmstxt(options = {}) {
|
|
5
|
+
const title = options.title ?? "saddle";
|
|
6
|
+
const description = options.description ?? "binary computing engine and browser automation library";
|
|
7
|
+
const pages = (options.pages ?? []).filter((page) => page?.url && /^https:\/\//.test(page.url)).slice(0, options.limit ?? 100);
|
|
8
|
+
const lines = [`# ${title}`, `> ${description}`, "", "## pages", "", ...pages.map((page) => `- [${page.title ?? page.url}](${page.url}): ${(page.description ?? "").slice(0, 100)}`)];
|
|
9
|
+
return `${lines.join("\n")}\n`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function llmsfull(options = {}) { return (options.pages ?? []).filter((page) => page?.url && /^https:\/\//.test(page.url)).map((page) => `# ${page.title ?? page.url}\n\nsource: ${page.url}\n\n${page.content ?? ""}`).join("\n\n---\n\n"); }
|
package/ai/provenance.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context provenance links retrieved chunks to source, query and transformation metadata.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Creates a serializable retrieval record for an agent context result. */
|
|
6
|
+
export function provenance(input = {}) {
|
|
7
|
+
if (!input.source && !input.sourceurl) throw new TypeError("provenance requires a source");
|
|
8
|
+
return { version: 1, source: input.source ?? input.sourceurl, sourceurl: input.sourceurl, documentid: input.documentid, query: input.query, retrievedat: Number(input.retrievedat ?? Date.now()), chunks: Array.isArray(input.chunks) ? input.chunks.map((chunk, index) => ({ id: String(chunk.id ?? index), contenthash: chunk.contenthash, score: chunk.score === undefined ? undefined : Number(chunk.score), headingpath: chunk.headingpath, tokencount: chunk.tokencount, citation: chunk.citation ?? input.sourceurl })) : [], metadata: { ...(input.metadata ?? {}) } };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Merges provenance records while deduplicating chunk identifiers. */
|
|
12
|
+
export function mergeprovenance(records = []) {
|
|
13
|
+
const valid = records.filter(Boolean);
|
|
14
|
+
const chunks = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (const record of valid) for (const chunk of record.chunks ?? []) { const key = `${record.documentid ?? record.source}:${chunk.id}`; if (seen.has(key)) continue; seen.add(key); chunks.push({ ...chunk, source: record.source, documentid: record.documentid }); }
|
|
17
|
+
return { version: 1, sources: [...new Set(valid.map((record) => record.source))], chunks, mergedat: Date.now() };
|
|
18
|
+
}
|
package/ai/rag.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rag manifests connect chunks to embedding stores without forcing a vendor client.
|
|
3
|
+
*/
|
|
4
|
+
export async function ragmanifest(input = {}) {
|
|
5
|
+
const chunks = input.chunks ?? [];
|
|
6
|
+
const unique = [];
|
|
7
|
+
const hashes = new Set();
|
|
8
|
+
for (const chunk of chunks) { const hash = await hashtext(chunk.content); if (hashes.has(hash)) continue; hashes.add(hash); unique.push({ ...chunk, contenthash: hash, documentid: hash.slice(0, 16), metadata: { ...(input.metadata ?? {}), ...(chunk.metadata ?? {}) } }); }
|
|
9
|
+
return { documentid: (await hashtext(input.source ?? unique.map((chunk) => chunk.content).join("\n"))).slice(0, 16), source: input.source, chunks: unique, embeddingmodel: input.embeddingmodel, embeddingdimensions: input.embeddingdimensions, createdat: Date.now() };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function vectorrecord(chunk, vector, options = {}) { return { id: `${chunk.documentid}-${chunk.id}`, vector, metadata: { headingpath: chunk.headingpath, contenthash: chunk.contenthash, sourceurl: options.sourceurl, tokencount: chunk.tokencount, contenttype: options.contenttype ?? "text", language: options.language ?? "en", embeddingmodel: options.embeddingmodel, embeddingdimensions: vector?.length } }; }
|
|
13
|
+
|
|
14
|
+
async function hashtext(text) { const bytes = new TextEncoder().encode(String(text)); if (globalThis.crypto?.subtle) { const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join(""); } return Array.from(bytes).map((value) => value.toString(16).padStart(2, "0")).join("").slice(0, 64).padEnd(64, "0"); }
|
package/ai/tokens.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* token helpers use configurable model ratios and never require a provider tokenizer.
|
|
3
|
+
*/
|
|
4
|
+
const ratios = Object.freeze({ default: 4, gpt: 3.5, claude: 3.2, gemini: 3.5 });
|
|
5
|
+
|
|
6
|
+
export function estimatetokens(text, model = "default") { const ratio = ratios[model] ?? ratios.default; return Math.ceil(String(text ?? "").length / ratio); }
|
|
7
|
+
export function fitscontext(text, context, model = "default") { return estimatetokens(text, model) <= context; }
|
|
8
|
+
export function tokenbudget(text, options = {}) { const tokens = estimatetokens(text, options.model); return { tokens, context: options.context ?? null, fits: options.context == null ? true : tokens <= options.context, remaining: options.context == null ? null : Math.max(0, options.context - tokens) }; }
|
|
9
|
+
export function settokenratios(values = {}) { Object.assign(ratios, values); return { ...ratios }; }
|
package/api/auth.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api authorization delegates token verification to the caller and never stores credentials.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Authorizes a request through an injected verifier or returns an anonymous principal. */
|
|
6
|
+
export async function authorize(request, options = {}) {
|
|
7
|
+
const token = request?.headers?.get?.("authorization")?.replace(/^Bearer\s+/i, "") ?? request?.headers?.get?.("x-api-key");
|
|
8
|
+
if (typeof options.verify !== "function") return { authenticated: false, subject: "anonymous", tokenpresent: Boolean(token) };
|
|
9
|
+
if (!token) return { authenticated: false, subject: "anonymous", tokenpresent: false };
|
|
10
|
+
const principal = await options.verify(token, request);
|
|
11
|
+
if (!principal) { const error = new Error("request is not authorized"); error.code = "UNAUTHORIZED"; throw error; }
|
|
12
|
+
return { authenticated: true, subject: String(principal.subject ?? principal.id ?? "caller"), claims: { ...(principal.claims ?? {}) } };
|
|
13
|
+
}
|
package/api/contracts.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api contracts keep request identity and success envelopes stable across HTTP and MCP adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const apiversion = 1;
|
|
6
|
+
|
|
7
|
+
/** Extracts a caller supplied request id or creates a local id without exposing secrets. */
|
|
8
|
+
export function requestcontext(request, options = {}) {
|
|
9
|
+
const requestid = request?.headers?.get?.("x-request-id") ?? options.requestid ?? `request${Date.now().toString(36)}`;
|
|
10
|
+
return { version: apiversion, requestid: String(requestid), method: request?.method ?? options.method, path: options.path };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Creates a versioned success envelope for APIs that opt into envelopes. */
|
|
14
|
+
export function successpayload(data, context = {}) { return { version: apiversion, requestid: String(context.requestid ?? `request${Date.now().toString(36)}`), data }; }
|
|
15
|
+
|
|
16
|
+
/** Creates a versioned error payload with a stable retry hint. */
|
|
17
|
+
export function errorpayload(code, message, context = {}) { return { version: apiversion, requestid: String(context.requestid ?? `request${Date.now().toString(36)}`), error: { code: String(code), message: String(message), retryafter: Number(context.retryafter ?? 0) } }; }
|