fastdar 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FastDAR
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # fastdar
2
+
3
+ The official JavaScript client for [FastDAR](https://fastdar.com). It covers buyer-intent lead alerts, the public proof ledger and market signals.
4
+
5
+ It has no dependencies and runs on Node 18+, Deno, Bun and modern browsers.
6
+
7
+ ```sh
8
+ npm install fastdar
9
+ ```
10
+
11
+ ## Check our track record yourself
12
+
13
+ Every lead alert and signal FastDAR issues is committed to a public, hash-chained ledger before anyone knows how it turns out. This package re-checks the whole thing on your machine:
14
+
15
+ ```sh
16
+ npx fastdar verify # the chain, every revealed call, sealed rules, every result
17
+ npx fastdar receipt message.json # a signed alert you received really came from FastDAR
18
+ npx fastdar speed # median minutes from a post appearing to our sealed alert
19
+ ```
20
+
21
+ ```js
22
+ import {FastDAR} from "fastdar";
23
+
24
+ const fd = new FastDAR(); // public calls need no key
25
+ const {ok, message} = await fd.proof.verify();
26
+ console.log(ok, message); // true "18234 entries verified, head 9f3c…"
27
+ ```
28
+
29
+ ## Lead alerts
30
+
31
+ ```js
32
+ const fd = new FastDAR({apiKey: process.env.FASTDAR_API_KEY});
33
+
34
+ const m = await fd.leads.create({
35
+ template: "home", area: "Leeds", offer: "emergency plumbing",
36
+ alerts: {email: "me@example.com", mode: "hot"},
37
+ });
38
+
39
+ for await (const lead of fd.leads.watch(m.id)) {
40
+ console.log(lead.score, lead.url, lead.text);
41
+ }
42
+
43
+ await fd.leads.label(m.id, postId, true); // yes, that was a real lead
44
+ await fd.leads.won(m.id, postId, 1200, "GBP"); // and it became a sale (sealed in the ledger)
45
+ const report = await fd.leads.report(m.id); // this week's numbers
46
+ ```
47
+
48
+ Other calls on `fd.leads` are `templates()`, `channels()`, `list()`, `get(id)`, `update(id, {...})`, `run(id)`, `ingest(id, {posts})`, `posts(id)`, `test(id)`, `exportCsv(id)` and `insights(industry)`.
49
+
50
+ ## Proof, follows, market
51
+
52
+ - `fd.proof` has `head()`, `key()`, `tracks()`, `rules(track)`, `stats()`, `anchors()`, `speed()`, `won()`, `ledger({after})`, `verify()` and `verifyReceipt(msg)`.
53
+ - `fd.follow` has `tracks()`, `add({track, webhook | email})`, `list()` and `remove(id)`.
54
+ - `fd.market` has `strategies()`, `indicators({bars, strategies})` and `scoreNews({items})`. FastDAR never serves prices; you send your own bars.
55
+
56
+ Errors are thrown as `FastDARError`, which carries `status` and `detail`.
57
+
58
+ ## Checking without the client
59
+
60
+ `import {verifyEntries, verifyReceipt, parseLossless, canonical} from "fastdar/verify"`.
61
+
62
+ Parse ledger JSON with `parseLossless` rather than `JSON.parse`. Hashes are taken over the exact number text, so `1200.0` must stay `1200.0`.
63
+
64
+ Receipts need Ed25519 in WebCrypto, which means Node 20+ or a current browser.
65
+
66
+ MIT licence.
package/bin/fastdar.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ // npx fastdar verify [site] check the whole public ledger
3
+ // npx fastdar receipt msg.json [site] check a signed message
4
+ // npx fastdar speed|tracks|won [site]
5
+ import {readFileSync} from "node:fs";
6
+ import {FastDAR, parseLossless} from "../src/index.js";
7
+
8
+ const [cmd, a, b] = process.argv.slice(2);
9
+ const site = (cmd === "receipt" ? b : a) || process.env.FASTDAR_BASE_URL || "https://fastdar.com";
10
+ const c = new FastDAR({baseUrl: site});
11
+ const out = (x) => console.log(JSON.stringify(x, null, 2));
12
+
13
+ try {
14
+ if (cmd === "verify") {
15
+ const r = await c.proof.verify({onPage: (n) => process.stderr.write(`\rread ${n} entries`)});
16
+ process.stderr.write("\n");
17
+ console.log((r.ok ? "OK: " : "FAILED: ") + r.message);
18
+ process.exit(r.ok ? 0 : 1);
19
+ } else if (cmd === "receipt" && a) {
20
+ const ok = await c.proof.verifyReceipt(parseLossless(readFileSync(a, "utf8")));
21
+ console.log(ok ? "OK: signed by this site's FastDAR key" : "FAILED: the signature doesn't match this message");
22
+ process.exit(ok ? 0 : 1);
23
+ } else if (cmd === "speed") out(await c.proof.speed());
24
+ else if (cmd === "tracks") out(await c.proof.tracks());
25
+ else if (cmd === "won") out(await c.proof.won());
26
+ else {
27
+ console.log("usage: fastdar verify [site] | receipt <message.json> [site] | speed [site] | tracks [site] | won [site]");
28
+ process.exit(cmd ? 2 : 0);
29
+ }
30
+ } catch (e) {
31
+ console.error(`error: ${e.message}`);
32
+ process.exit(2);
33
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "fastdar",
3
+ "version": "0.1.0",
4
+ "description": "FastDAR client: buyer-intent lead alerts, a public proof ledger you can check yourself, and market signals.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {"types": "./src/index.d.ts", "default": "./src/index.js"},
10
+ "./verify": {"types": "./src/verify.d.ts", "default": "./src/verify.js"}
11
+ },
12
+ "bin": {"fastdar": "./bin/fastdar.js"},
13
+ "files": ["src", "bin", "README.md", "LICENSE"],
14
+ "engines": {"node": ">=18"},
15
+ "scripts": {"test": "node --test test/*.test.js"},
16
+ "keywords": ["fastdar", "lead-alerts", "buyer-intent", "proof", "transparency-log", "trading-signals"],
17
+ "homepage": "https://fastdar.com",
18
+ "repository": {"type": "git", "url": "git+https://github.com/FastDAR/fastdar.git", "directory": "sdk/js"},
19
+ "license": "MIT",
20
+ "sideEffects": false
21
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,154 @@
1
+ import type {LedgerEntry, VerifyResult} from "./verify.js";
2
+ export {parseLossless, verifyEntries, verifyReceipt, canonical, RawNumber, GENESIS} from "./verify.js";
3
+ export type {LedgerEntry, VerifyResult} from "./verify.js";
4
+
5
+ export declare const DEFAULT_BASE_URL: string;
6
+
7
+ export declare class FastDARError extends Error {
8
+ status: number;
9
+ detail: unknown;
10
+ body: unknown;
11
+ }
12
+
13
+ export interface FastDAROptions {
14
+ /** Defaults to process.env.FASTDAR_API_KEY. Public calls (proof, trending, templates) need no key. */
15
+ apiKey?: string;
16
+ /** Defaults to process.env.FASTDAR_BASE_URL, then https://fastdar.com */
17
+ baseUrl?: string;
18
+ fetch?: typeof fetch;
19
+ timeoutMs?: number;
20
+ }
21
+
22
+ export type AlertMode = "instant" | "hot" | "digest";
23
+
24
+ export interface Alerts {
25
+ email?: string;
26
+ slack_webhook?: string;
27
+ discord_webhook?: string;
28
+ teams_webhook?: string;
29
+ webhook?: string;
30
+ telegram_chat_id?: string;
31
+ ntfy_topic?: string;
32
+ sms_to?: string;
33
+ mode?: AlertMode;
34
+ hot_score?: number;
35
+ digest?: "hourly" | "daily";
36
+ digest_hour?: number;
37
+ timezone?: string;
38
+ quiet_hours?: string;
39
+ min_score?: number;
40
+ must_include?: string[];
41
+ exclude?: string[];
42
+ max_per_day?: number;
43
+ weekly_report?: boolean;
44
+ currency?: string;
45
+ hubspot_token?: string;
46
+ hubspot_stage?: string;
47
+ pipedrive_token?: string;
48
+ crm_push?: "confirmed" | "all";
49
+ [k: string]: unknown;
50
+ }
51
+
52
+ export interface MonitorInput {
53
+ /** An industry template id from leads.templates(); fills terms and sources. */
54
+ template?: string;
55
+ /** Place name added to local templates' phrases. */
56
+ area?: string;
57
+ /** What you sell, in one plain line. */
58
+ offer: string;
59
+ name?: string;
60
+ terms?: string[];
61
+ sources?: Record<string, unknown>;
62
+ alerts?: Alerts;
63
+ }
64
+
65
+ export interface Monitor {
66
+ id: number;
67
+ name: string;
68
+ offer: string;
69
+ industry: string | null;
70
+ terms: string[];
71
+ sources: Record<string, unknown>;
72
+ alerts: Alerts;
73
+ active: number;
74
+ last_run: number;
75
+ review_url: string;
76
+ report_url: string;
77
+ [k: string]: unknown;
78
+ }
79
+
80
+ export interface LeadItem {
81
+ id: number;
82
+ source: string;
83
+ url: string | null;
84
+ text: string;
85
+ score: number;
86
+ label: "lead" | "not a lead" | null;
87
+ found: number;
88
+ won?: number;
89
+ won_currency?: string;
90
+ }
91
+
92
+ declare class Leads {
93
+ templates(): Promise<{templates: Array<{id: string; name: string; [k: string]: unknown}>}>;
94
+ channels(): Promise<any>;
95
+ create(body: MonitorInput): Promise<Monitor>;
96
+ list(): Promise<Monitor[]>;
97
+ get(id: number): Promise<Monitor>;
98
+ update(id: number, body: Partial<MonitorInput> & {active?: boolean}): Promise<Monitor>;
99
+ run(id: number): Promise<any>;
100
+ ingest(id: number, body: {posts: Array<{text: string; id?: string; url?: string; author?: string; platform?: string; created?: number}>}): Promise<any>;
101
+ posts(id: number, opts?: {which?: "alerts" | "review"; limit?: number}): Promise<any[]>;
102
+ label(id: number, postId: number, isLead: boolean): Promise<any>;
103
+ won(id: number, postId: number, value: number | null, currency?: string): Promise<{ok: boolean; won: {sales: number; value: Array<{currency: string; total: number}>}}>;
104
+ report(id: number, opts?: {days?: number}): Promise<any>;
105
+ test(id: number): Promise<{sent: string[]; failed: string[]}>;
106
+ exportCsv(id: number): Promise<string>;
107
+ insights(industry: string): Promise<any>;
108
+ /** New alerts as they're found. */
109
+ watch(id: number, opts?: {signal?: AbortSignal; lastEventId?: string}): AsyncGenerator<LeadItem>;
110
+ }
111
+
112
+ declare class Proof {
113
+ head(): Promise<any>;
114
+ key(): Promise<{alg: "ed25519"; public_key: string; key_id: string}>;
115
+ tracks(): Promise<any>;
116
+ rules(track: string): Promise<any>;
117
+ stats(): Promise<any>;
118
+ anchors(): Promise<any>;
119
+ speed(opts?: {days?: number}): Promise<{window_days: number; alerts: number; enough: boolean; median_minutes?: number; p90_minutes?: number}>;
120
+ won(opts?: {days?: number}): Promise<any>;
121
+ ledger(opts?: {after?: number; limit?: number}): Promise<LedgerEntry[]>;
122
+ verify(opts?: {onPage?: (entriesSoFar: number) => void}): Promise<VerifyResult & {entries: number}>;
123
+ verifyReceipt(message: Record<string, unknown>): Promise<boolean>;
124
+ }
125
+
126
+ declare class Follow {
127
+ tracks(): Promise<any>;
128
+ add(body: {track: string; webhook?: string; email?: string}): Promise<any>;
129
+ list(): Promise<any>;
130
+ remove(id: number): Promise<any>;
131
+ }
132
+
133
+ declare class Market {
134
+ strategies(): Promise<Record<string, {bar_minutes: number; multi_day: boolean}>>;
135
+ indicators(body: {bars: unknown; strategies?: string[] | "all"; [k: string]: unknown}): Promise<any>;
136
+ scoreNews(body: {symbol?: string; headline?: string; summary?: string; items?: Array<{symbol: string; headline: string; summary?: string}>}): Promise<any>;
137
+ }
138
+
139
+ export declare class FastDAR {
140
+ constructor(opts?: FastDAROptions);
141
+ apiKey?: string;
142
+ baseUrl: string;
143
+ leads: Leads;
144
+ proof: Proof;
145
+ follow: Follow;
146
+ market: Market;
147
+ usage(): Promise<any>;
148
+ health(): Promise<any>;
149
+ trending(): Promise<any>;
150
+ stream(industry: string): Promise<any>;
151
+ systemone(req: {state: string; questions: Record<string, unknown>}): Promise<any>;
152
+ }
153
+
154
+ export default FastDAR;
package/src/index.js ADDED
@@ -0,0 +1,201 @@
1
+ // FastDAR API client: buyer-intent lead alerts, the public proof ledger, and market signals.
2
+ // Zero dependencies; needs a global fetch (Node 18+, Deno, Bun, browsers).
3
+ import {parseLossless, verifyEntries, verifyReceipt} from "./verify.js";
4
+
5
+ export {parseLossless, verifyEntries, verifyReceipt, canonical, RawNumber, GENESIS} from "./verify.js";
6
+
7
+ export const DEFAULT_BASE_URL = "https://fastdar.com";
8
+
9
+ export class FastDARError extends Error {
10
+ constructor(status, detail, body) {
11
+ super(typeof detail === "string" ? detail : `HTTP ${status}`);
12
+ this.name = "FastDARError";
13
+ this.status = status;
14
+ this.detail = detail;
15
+ this.body = body;
16
+ }
17
+ }
18
+
19
+ const enc = encodeURIComponent;
20
+
21
+ export class FastDAR {
22
+ /**
23
+ * @param {{apiKey?: string, baseUrl?: string, fetch?: typeof fetch, timeoutMs?: number}} [opts]
24
+ */
25
+ constructor(opts = {}) {
26
+ const env = typeof process !== "undefined" && process.env ? process.env : {};
27
+ this.apiKey = opts.apiKey ?? env.FASTDAR_API_KEY;
28
+ this.baseUrl = (opts.baseUrl ?? env.FASTDAR_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
29
+ this.fetch = opts.fetch ?? globalThis.fetch;
30
+ this.timeoutMs = opts.timeoutMs ?? 30000;
31
+ if (!this.fetch) throw new Error("no fetch available; pass opts.fetch");
32
+ this.leads = new Leads(this);
33
+ this.proof = new Proof(this);
34
+ this.follow = new Follow(this);
35
+ this.market = new Market(this);
36
+ }
37
+
38
+ async _raw(method, path, {body, auth = true, query} = {}) {
39
+ const url = new URL(this.baseUrl + path);
40
+ for (const [k, v] of Object.entries(query || {})) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
41
+ const headers = {Accept: "application/json"};
42
+ if (auth) {
43
+ if (!this.apiKey) throw new FastDARError(401, "this call needs an API key (opts.apiKey or FASTDAR_API_KEY)");
44
+ headers.Authorization = `Bearer ${this.apiKey}`;
45
+ }
46
+ if (body !== undefined) headers["Content-Type"] = "application/json";
47
+ const ctl = typeof AbortController !== "undefined" ? new AbortController() : null;
48
+ const t = ctl ? setTimeout(() => ctl.abort(), this.timeoutMs) : null;
49
+ try {
50
+ const r = await this.fetch(url, {method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: ctl?.signal});
51
+ if (!r.ok) {
52
+ const text = await r.text();
53
+ let j = null;
54
+ try { j = JSON.parse(text); } catch {}
55
+ throw new FastDARError(r.status, j?.detail ?? (text || `HTTP ${r.status}`), j ?? text);
56
+ }
57
+ return r;
58
+ } finally {
59
+ if (t) clearTimeout(t);
60
+ }
61
+ }
62
+
63
+ async _json(method, path, opts) {
64
+ return (await this._raw(method, path, opts)).json();
65
+ }
66
+
67
+ /** Your key's plan and usage. */
68
+ usage() { return this._json("GET", "/v1/usage"); }
69
+ /** Server health. */
70
+ health() { return this._json("GET", "/health", {auth: false}); }
71
+ /** Public trending asks per category (last 7 days). */
72
+ trending() { return this._json("GET", "/v1/trending", {auth: false}); }
73
+ /** Public live asks in one industry: links and scores only. */
74
+ stream(industry) { return this._json("GET", `/stream/${enc(industry)}.json`, {auth: false}); }
75
+ /** Fast-tier structured answers: {state, questions}. */
76
+ systemone(req) { return this._json("POST", "/v1/systemone", {body: req}); }
77
+ }
78
+
79
+ class Leads {
80
+ constructor(c) { this.c = c; }
81
+ /** Industry starter templates. */
82
+ templates() { return this.c._json("GET", "/v1/leads/templates", {auth: false}); }
83
+ /** Alert channels and options this server supports. */
84
+ channels() { return this.c._json("GET", "/v1/leads/channels", {auth: false}); }
85
+ /** Create a monitor: {template?, area?, offer, terms?, sources?, alerts?, name?}. */
86
+ create(body) { return this.c._json("POST", "/v1/leads/monitors", {body}); }
87
+ list() { return this.c._json("GET", "/v1/leads/monitors"); }
88
+ get(id) { return this.c._json("GET", `/v1/leads/monitors/${enc(id)}`); }
89
+ /** Change name, offer, terms, sources, alerts or active. */
90
+ update(id, body) { return this.c._json("PATCH", `/v1/leads/monitors/${enc(id)}`, {body}); }
91
+ /** Poll a monitor's sources now. */
92
+ run(id) { return this.c._json("POST", `/v1/leads/monitors/${enc(id)}/run`); }
93
+ /** Push your own posts to be screened: {posts: [{text, id or url, url?, author?, platform?, created?}]} (up to 1000). */
94
+ ingest(id, body) { return this.c._json("POST", `/v1/leads/monitors/${enc(id)}/ingest`, {body}); }
95
+ /** which: "alerts" (default) or "review". */
96
+ posts(id, {which, limit} = {}) { return this.c._json("GET", `/v1/leads/monitors/${enc(id)}/posts`, {query: {which, limit}}); }
97
+ /** Answer an alert: it was (true) or wasn't (false) a real lead. */
98
+ label(id, postId, isLead) { return this.c._json("POST", `/v1/leads/monitors/${enc(id)}/posts/${enc(postId)}/label`, {body: {is_lead: !!isLead}}); }
99
+ /** This lead became a sale. value null undoes it. The sale is sealed in the proof ledger. */
100
+ won(id, postId, value, currency) {
101
+ return this.c._json("POST", `/v1/leads/monitors/${enc(id)}/posts/${enc(postId)}/won`, {body: {value, ...(currency ? {currency} : {})}});
102
+ }
103
+ report(id, {days} = {}) { return this.c._json("GET", `/v1/leads/monitors/${enc(id)}/report`, {query: {days}}); }
104
+ /** Send a test alert to every configured channel. */
105
+ test(id) { return this.c._json("POST", `/v1/leads/monitors/${enc(id)}/test`); }
106
+ /** Every post read for this monitor, as CSV text. */
107
+ async exportCsv(id) { return (await this.c._raw("GET", `/v1/leads/monitors/${enc(id)}/export.csv`)).text(); }
108
+ /** Which phrases and sources find real leads in an industry, pooled across customers. */
109
+ insights(industry) { return this.c._json("GET", `/v1/leads/insights/${enc(industry)}`); }
110
+
111
+ /**
112
+ * New alerts as they're found (server-sent events). Yields parsed alerts; stop with break or opts.signal.
113
+ * @param {number|string} id
114
+ * @param {{signal?: AbortSignal, lastEventId?: string}} [opts]
115
+ */
116
+ async *watch(id, opts = {}) {
117
+ const headers = {Accept: "text/event-stream", Authorization: `Bearer ${this.c.apiKey}`};
118
+ if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
119
+ const r = await this.c.fetch(`${this.c.baseUrl}/v1/leads/monitors/${enc(id)}/stream`, {headers, signal: opts.signal});
120
+ if (!r.ok) throw new FastDARError(r.status, await r.text());
121
+ const reader = r.body.getReader(), dec = new TextDecoder();
122
+ let buf = "";
123
+ for (;;) {
124
+ const {value, done} = await reader.read();
125
+ if (done) return;
126
+ buf += dec.decode(value, {stream: true});
127
+ let cut;
128
+ while ((cut = buf.indexOf("\n\n")) >= 0) {
129
+ const block = buf.slice(0, cut); buf = buf.slice(cut + 2);
130
+ let event = "message", data = "";
131
+ for (const line of block.split("\n")) {
132
+ if (line.startsWith("event:")) event = line.slice(6).trim();
133
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
134
+ }
135
+ if (event === "lead" && data) yield JSON.parse(data);
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ class Proof {
142
+ constructor(c) { this.c = c; }
143
+ head() { return this.c._json("GET", "/proof/head", {auth: false}); }
144
+ key() { return this.c._json("GET", "/proof/key", {auth: false}); }
145
+ /** Every track ever run, losers and retired ones included. */
146
+ tracks() { return this.c._json("GET", "/proof/tracks", {auth: false}); }
147
+ rules(track) { return this.c._json("GET", `/proof/rules/${enc(track)}`, {auth: false}); }
148
+ stats() { return this.c._json("GET", "/proof/stats", {auth: false}); }
149
+ anchors() { return this.c._json("GET", "/proof/anchors", {auth: false}); }
150
+ /** Median minutes from a post appearing to its sealed alert. */
151
+ speed({days} = {}) { return this.c._json("GET", "/proof/speed", {auth: false, query: {days}}); }
152
+ /** Sales customers reported, pooled. */
153
+ won({days} = {}) { return this.c._json("GET", "/proof/won", {auth: false, query: {days}}); }
154
+
155
+ /** Ledger entries, numbers kept exactly as sent (see parseLossless). Pages of up to 1000. */
156
+ async ledger({after = 0, limit = 1000} = {}) {
157
+ const r = await this.c._raw("GET", "/proof/ledger", {auth: false, query: {after, limit}});
158
+ return parseLossless(await r.text()).entries;
159
+ }
160
+
161
+ /** Download the whole ledger and check it. Resolves to {ok, message, entries}. */
162
+ async verify({onPage} = {}) {
163
+ const all = [];
164
+ for (let after = 0; ;) {
165
+ const page = await this.ledger({after});
166
+ all.push(...page);
167
+ if (onPage) onPage(all.length);
168
+ if (page.length < 1000) break;
169
+ after = Number(page[page.length - 1].seq);
170
+ }
171
+ const res = await verifyEntries(all);
172
+ return {...res, entries: all.length};
173
+ }
174
+
175
+ /** Check a signed message (a follower alert, or /proof/head) with the server's published key. */
176
+ async verifyReceipt(message) {
177
+ const k = await this.key();
178
+ return verifyReceipt(message, k.public_key);
179
+ }
180
+ }
181
+
182
+ class Follow {
183
+ constructor(c) { this.c = c; }
184
+ tracks() { return this.c._json("GET", "/v1/follow/tracks", {auth: false}); }
185
+ /** Get each sealed call on a track: {track, webhook} or {track, email}. */
186
+ add(body) { return this.c._json("POST", "/v1/follow", {body}); }
187
+ list() { return this.c._json("GET", "/v1/follow"); }
188
+ remove(id) { return this.c._json("DELETE", `/v1/follow/${enc(id)}`); }
189
+ }
190
+
191
+ class Market {
192
+ constructor(c) { this.c = c; }
193
+ /** Strategy names with their bar size. */
194
+ strategies() { return this.c._json("GET", "/v1/strategies", {auth: false}); }
195
+ /** Signals on bars you send (FastDAR never serves prices): {bars, strategies?}. */
196
+ indicators(body) { return this.c._json("POST", "/v1/indicators", {body}); }
197
+ /** Score headlines: {symbol, headline, summary?} or {items: [...]}. */
198
+ scoreNews(body) { return this.c._json("POST", "/v1/news/score", {body}); }
199
+ }
200
+
201
+ export default FastDAR;
@@ -0,0 +1,37 @@
1
+ export declare const GENESIS: string;
2
+
3
+ /** A number kept as the exact text the server sent. */
4
+ export declare class RawNumber {
5
+ constructor(raw: string);
6
+ raw: string;
7
+ valueOf(): number;
8
+ toJSON(): number;
9
+ }
10
+
11
+ /** JSON.parse that keeps every number as a RawNumber, so hashes can be recomputed exactly. */
12
+ export declare function parseLossless(text: string): any;
13
+ /** Canonical JSON, byte-for-byte as the FastDAR server hashes it. */
14
+ export declare function canonical(value: unknown): string;
15
+ export declare function sha256Hex(text: string): Promise<string>;
16
+ export declare function commitment(salt: string, payload: unknown): Promise<string>;
17
+ export declare function link(prev: string, seq: number | RawNumber, kind: string, issued: number | RawNumber, commit: string): Promise<string>;
18
+
19
+ export interface LedgerEntry {
20
+ seq: number | RawNumber;
21
+ kind: string;
22
+ issued: number | RawNumber;
23
+ commit: string;
24
+ prev: string;
25
+ chain: string;
26
+ payload?: any;
27
+ salt?: string;
28
+ outcome?: any;
29
+ scored?: number | RawNumber;
30
+ }
31
+
32
+ export interface VerifyResult { ok: boolean; message: string }
33
+
34
+ /** Check the hash chain, every revealed payload, the sealed rules and every shown outcome. */
35
+ export declare function verifyEntries(entries: LedgerEntry[], startPrev?: string): Promise<VerifyResult>;
36
+ /** Check a signed message against FastDAR's public key (hex). Needs Ed25519 in WebCrypto (Node 20+). */
37
+ export declare function verifyReceipt(message: {receipt?: {sig: string; alg?: string; key_id?: string}; [k: string]: unknown}, publicKeyHex: string): Promise<boolean>;
package/src/verify.js ADDED
@@ -0,0 +1,167 @@
1
+ // Check a FastDAR proof ledger yourself, in Node 18+ or any modern browser, with no dependencies.
2
+ // It mirrors fastjev/proof.py verify(): the hash chain, every revealed payload against its commitment,
3
+ // the sealed-rules rules, and that each shown outcome is the one sealed in its result or correction.
4
+ //
5
+ // Hashes are computed over canonical JSON exactly as the server writes it, so numbers must keep the
6
+ // text they were sent with (1200.0 stays "1200.0"). parseLossless() below does that; plain JSON.parse
7
+ // would turn 1200.0 into 1200 and every commitment containing it would look wrong.
8
+
9
+ export const GENESIS = "0".repeat(64);
10
+ const SEALS = new Set(["result", "correction"]);
11
+
12
+ /** A number kept as the exact text the server sent. */
13
+ export class RawNumber {
14
+ constructor(raw) { this.raw = raw; }
15
+ valueOf() { return Number(this.raw); }
16
+ toJSON() { return Number(this.raw); }
17
+ }
18
+
19
+ /** JSON.parse that keeps every number as a RawNumber. */
20
+ export function parseLossless(text) {
21
+ let i = 0;
22
+ const ws = () => { while (i < text.length && " \t\n\r".includes(text[i])) i++; };
23
+ const fail = (m) => { throw new SyntaxError(`${m} at position ${i}`); };
24
+ function value() {
25
+ ws();
26
+ const c = text[i];
27
+ if (c === "{") {
28
+ i++; const o = {}; ws();
29
+ if (text[i] === "}") { i++; return o; }
30
+ for (;;) {
31
+ ws(); if (text[i] !== '"') fail("expected a key");
32
+ const k = str(); ws(); if (text[i++] !== ":") fail("expected ':'");
33
+ o[k] = value(); ws();
34
+ if (text[i] === ",") { i++; continue; }
35
+ if (text[i++] === "}") return o;
36
+ fail("expected ',' or '}'");
37
+ }
38
+ }
39
+ if (c === "[") {
40
+ i++; const a = []; ws();
41
+ if (text[i] === "]") { i++; return a; }
42
+ for (;;) {
43
+ a.push(value()); ws();
44
+ if (text[i] === ",") { i++; continue; }
45
+ if (text[i++] === "]") return a;
46
+ fail("expected ',' or ']'");
47
+ }
48
+ }
49
+ if (c === '"') return str();
50
+ if (text.startsWith("true", i)) { i += 4; return true; }
51
+ if (text.startsWith("false", i)) { i += 5; return false; }
52
+ if (text.startsWith("null", i)) { i += 4; return null; }
53
+ const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i, i + 64));
54
+ if (!m) fail("unexpected token");
55
+ i += m[0].length;
56
+ return new RawNumber(m[0]);
57
+ }
58
+ function str() {
59
+ const start = i; i++;
60
+ while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
61
+ i++;
62
+ return JSON.parse(text.slice(start, i));
63
+ }
64
+ const out = value(); ws();
65
+ if (i !== text.length) fail("trailing data");
66
+ return out;
67
+ }
68
+
69
+ // Python json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=False)
70
+ export function canonical(v) {
71
+ if (v === null) return "null";
72
+ if (v instanceof RawNumber) return v.raw;
73
+ if (typeof v === "number") {
74
+ if (!Number.isFinite(v)) throw new TypeError("not a JSON number");
75
+ return Number.isInteger(v) ? String(v) : pyFloat(v);
76
+ }
77
+ if (typeof v === "boolean") return v ? "true" : "false";
78
+ if (typeof v === "string") return JSON.stringify(v);
79
+ if (Array.isArray(v)) return "[" + v.map(canonical).join(",") + "]";
80
+ const keys = Object.keys(v).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
81
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",") + "}";
82
+ }
83
+
84
+ // repr() of a Python float, for numbers built in JS rather than parsed losslessly
85
+ function pyFloat(x) {
86
+ const s = String(x);
87
+ if (/e/.test(s)) {
88
+ const [m, e] = s.split("e");
89
+ const exp = Number(e);
90
+ if (exp >= 16 || exp < -4) return `${m}e${exp < 0 ? "-" : "+"}${String(Math.abs(exp)).padStart(2, "0")}`;
91
+ }
92
+ return s.includes(".") ? s : s + ".0";
93
+ }
94
+
95
+ const enc = new TextEncoder();
96
+ export async function sha256Hex(text) {
97
+ const d = await globalThis.crypto.subtle.digest("SHA-256", enc.encode(text));
98
+ return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
99
+ }
100
+
101
+ export const commitment = (salt, payload) => sha256Hex(salt + canonical(payload));
102
+ export const link = (prev, seq, kind, issued, commit) =>
103
+ sha256Hex(`${prev}|${Number(seq)}|${kind}|${Number(issued).toFixed(3)}|${commit}`);
104
+
105
+ const n = (x) => (x == null ? x : Number(x));
106
+
107
+ /**
108
+ * Verify ledger entries (as returned by GET /proof/ledger, parsed with parseLossless).
109
+ * Resolves to {ok, message}.
110
+ */
111
+ export async function verifyEntries(entries, startPrev = GENESIS) {
112
+ let prev = startPrev;
113
+ for (const e of entries) {
114
+ const seq = n(e.seq);
115
+ if (e.prev !== prev) return {ok: false, message: `seq ${seq}: prev does not match the chain before it`};
116
+ if ((await link(prev, seq, e.kind, e.issued, e.commit)) !== e.chain)
117
+ return {ok: false, message: `seq ${seq}: chain hash is wrong`};
118
+ if ("payload" in e && (await commitment(e.salt, e.payload)) !== e.commit)
119
+ return {ok: false, message: `seq ${seq}: revealed payload does not match what was committed`};
120
+ prev = e.chain;
121
+ }
122
+ const seenRules = new Map(), current = new Map();
123
+ for (const e of entries) {
124
+ const p = e.payload || {}, seq = n(e.seq);
125
+ if (e.kind === "rules" && "payload" in e) { seenRules.set(seq, e); current.set(p.strategy, seq); }
126
+ const rs = SEALS.has(e.kind) ? null : n(p.rules_seq);
127
+ if (rs != null) {
128
+ const r = seenRules.get(rs);
129
+ if (!r || n(r.seq) >= seq) return {ok: false, message: `seq ${seq}: names rules entry ${rs}, which isn't a published rules entry before it`};
130
+ if (r.payload.retired) return {ok: false, message: `seq ${seq}: issued under retired rules ${rs}`};
131
+ if (r.payload.strategy !== p.strategy || current.get(p.strategy) !== rs)
132
+ return {ok: false, message: `seq ${seq}: rules ${rs} aren't the current rules for ${p.strategy}`};
133
+ }
134
+ }
135
+ const bySeq = new Map(entries.map((e) => [n(e.seq), e])), last = new Map();
136
+ for (const e of entries) {
137
+ if (SEALS.has(e.kind) && "payload" in e) {
138
+ const ref = n(e.payload.ref);
139
+ if (e.kind === "correction" && last.has(ref) && n(e.payload.supersedes) !== n(last.get(ref).seq))
140
+ return {ok: false, message: `seq ${n(e.seq)}: correction doesn't supersede the latest result for seq ${ref}`};
141
+ last.set(ref, e);
142
+ }
143
+ }
144
+ for (const [ref, e] of last) {
145
+ const r = bySeq.get(ref);
146
+ if (r && canonical(r.outcome ?? null) !== canonical(e.payload.outcome))
147
+ return {ok: false, message: `seq ${ref}: shown outcome differs from the sealed ${e.kind} at seq ${n(e.seq)}`};
148
+ }
149
+ return {ok: true, message: `${entries.length} entries verified, head ${prev}`};
150
+ }
151
+
152
+ const hexBytes = (h) => new Uint8Array((h.match(/../g) || []).map((b) => parseInt(b, 16)));
153
+
154
+ /**
155
+ * Check a signed message (a follower alert, or /proof/head) against FastDAR's public key (hex, from /proof/key).
156
+ * Needs Ed25519 in WebCrypto: Node 20+, and current Chrome, Firefox and Safari.
157
+ */
158
+ export async function verifyReceipt(message, publicKeyHex) {
159
+ const {receipt, ...rest} = message;
160
+ if (!receipt || !receipt.sig) return false;
161
+ try {
162
+ const key = await globalThis.crypto.subtle.importKey("raw", hexBytes(publicKeyHex), {name: "Ed25519"}, false, ["verify"]);
163
+ return await globalThis.crypto.subtle.verify({name: "Ed25519"}, key, hexBytes(receipt.sig), enc.encode(canonical(rest)));
164
+ } catch {
165
+ return false;
166
+ }
167
+ }