@diffdelta/client 0.1.0 → 0.1.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/README.md +152 -0
- package/dist/index.d.mts +143 -15
- package/dist/index.d.ts +143 -15
- package/dist/index.js +109 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +103 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,7 +22,12 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CursorStore: () => CursorStore,
|
|
24
24
|
DiffDelta: () => DiffDelta,
|
|
25
|
-
MemoryCursorStore: () => MemoryCursorStore
|
|
25
|
+
MemoryCursorStore: () => MemoryCursorStore,
|
|
26
|
+
parseFeed: () => parseFeed,
|
|
27
|
+
parseFeedItem: () => parseFeedItem,
|
|
28
|
+
parseHead: () => parseHead,
|
|
29
|
+
parseHealthCheck: () => parseHealthCheck,
|
|
30
|
+
parseSourceInfo: () => parseSourceInfo
|
|
26
31
|
});
|
|
27
32
|
module.exports = __toCommonJS(index_exports);
|
|
28
33
|
|
|
@@ -107,12 +112,15 @@ function parseFeedItem(data, bucket = "new") {
|
|
|
107
112
|
} else if (typeof content === "string") {
|
|
108
113
|
excerpt = content;
|
|
109
114
|
}
|
|
110
|
-
|
|
115
|
+
const signals = data.signals || {};
|
|
116
|
+
const suggestedAction = signals.suggested_action || null;
|
|
117
|
+
let riskScore = null;
|
|
118
|
+
const risk = data.risk;
|
|
119
|
+
if (typeof risk === "object" && risk !== null) {
|
|
120
|
+
riskScore = risk.score ?? null;
|
|
121
|
+
}
|
|
111
122
|
if (riskScore === null) {
|
|
112
|
-
|
|
113
|
-
if (typeof summary === "object" && summary !== null) {
|
|
114
|
-
riskScore = summary.risk_score ?? null;
|
|
115
|
-
}
|
|
123
|
+
riskScore = data.risk_score ?? null;
|
|
116
124
|
}
|
|
117
125
|
return {
|
|
118
126
|
source: data.source || "",
|
|
@@ -123,18 +131,35 @@ function parseFeedItem(data, bucket = "new") {
|
|
|
123
131
|
publishedAt: data.published_at || null,
|
|
124
132
|
updatedAt: data.updated_at || null,
|
|
125
133
|
bucket,
|
|
134
|
+
signals,
|
|
135
|
+
suggestedAction,
|
|
126
136
|
riskScore,
|
|
127
137
|
provenance: data.provenance || {},
|
|
128
138
|
raw: data
|
|
129
139
|
};
|
|
130
140
|
}
|
|
131
141
|
function parseHead(data) {
|
|
142
|
+
const counts = data.counts || {};
|
|
143
|
+
const freshness = data.freshness;
|
|
132
144
|
return {
|
|
133
145
|
cursor: data.cursor || "",
|
|
134
|
-
hash: data.hash || "",
|
|
135
146
|
changed: data.changed || false,
|
|
136
147
|
generatedAt: data.generated_at || "",
|
|
137
|
-
ttlSec: data.ttl_sec ||
|
|
148
|
+
ttlSec: data.ttl_sec || 60,
|
|
149
|
+
latestUrl: data.latest_url || "",
|
|
150
|
+
digestUrl: data.digest_url || null,
|
|
151
|
+
counts: {
|
|
152
|
+
new: counts.new || 0,
|
|
153
|
+
updated: counts.updated || 0,
|
|
154
|
+
removed: counts.removed || 0,
|
|
155
|
+
flagged: counts.flagged || 0
|
|
156
|
+
},
|
|
157
|
+
sourcesChecked: data.sources_checked || 0,
|
|
158
|
+
sourcesOk: data.sources_ok || 0,
|
|
159
|
+
allClear: data.all_clear || false,
|
|
160
|
+
allClearConfidence: data.all_clear_confidence ?? data.confidence ?? null,
|
|
161
|
+
freshness: freshness || null,
|
|
162
|
+
raw: data
|
|
138
163
|
};
|
|
139
164
|
}
|
|
140
165
|
function parseFeed(data) {
|
|
@@ -148,15 +173,19 @@ function parseFeed(data) {
|
|
|
148
173
|
const removedItems = (buckets.removed || []).map(
|
|
149
174
|
(i) => parseFeedItem(i, "removed")
|
|
150
175
|
);
|
|
176
|
+
const flaggedItems = (buckets.flagged || []).map(
|
|
177
|
+
(i) => parseFeedItem(i, "flagged")
|
|
178
|
+
);
|
|
151
179
|
return {
|
|
152
180
|
cursor: data.cursor || "",
|
|
153
181
|
prevCursor: data.prev_cursor || "",
|
|
154
182
|
sourceId: data.source_id || "",
|
|
155
183
|
generatedAt: data.generated_at || "",
|
|
156
|
-
items: [...newItems, ...updatedItems, ...removedItems],
|
|
184
|
+
items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],
|
|
157
185
|
new: newItems,
|
|
158
186
|
updated: updatedItems,
|
|
159
187
|
removed: removedItems,
|
|
188
|
+
flagged: flaggedItems,
|
|
160
189
|
narrative: data.batch_narrative || "",
|
|
161
190
|
raw: data
|
|
162
191
|
};
|
|
@@ -174,9 +203,19 @@ function parseSourceInfo(data) {
|
|
|
174
203
|
latestUrl: data.latest_url || ""
|
|
175
204
|
};
|
|
176
205
|
}
|
|
206
|
+
function parseHealthCheck(data) {
|
|
207
|
+
return {
|
|
208
|
+
ok: data.ok || false,
|
|
209
|
+
service: data.service || "",
|
|
210
|
+
time: data.time || "",
|
|
211
|
+
sourcesChecked: data.sources_checked || 0,
|
|
212
|
+
sourcesOk: data.sources_ok || 0,
|
|
213
|
+
engineVersion: data.engine_version || ""
|
|
214
|
+
};
|
|
215
|
+
}
|
|
177
216
|
|
|
178
217
|
// src/client.ts
|
|
179
|
-
var VERSION = "0.1.
|
|
218
|
+
var VERSION = "0.1.1";
|
|
180
219
|
var DEFAULT_BASE_URL = "https://diffdelta.io";
|
|
181
220
|
var DEFAULT_TIMEOUT = 15e3;
|
|
182
221
|
var DiffDelta = class {
|
|
@@ -203,11 +242,11 @@ var DiffDelta = class {
|
|
|
203
242
|
/**
|
|
204
243
|
* Poll the global feed for new items since last poll.
|
|
205
244
|
*
|
|
206
|
-
* Checks head.json first (~
|
|
245
|
+
* Checks head.json first (~200 bytes). Only fetches the full feed
|
|
207
246
|
* if the cursor has changed. Automatically saves the new cursor.
|
|
208
247
|
*/
|
|
209
248
|
async poll(options = {}) {
|
|
210
|
-
const { tags, sources, buckets = ["new", "updated"] } = options;
|
|
249
|
+
const { tags, sources, buckets = ["new", "updated", "flagged"] } = options;
|
|
211
250
|
return this.pollFeed({
|
|
212
251
|
headUrl: `${this.baseUrl}/diff/head.json`,
|
|
213
252
|
latestUrl: `${this.baseUrl}/diff/latest.json`,
|
|
@@ -224,10 +263,10 @@ var DiffDelta = class {
|
|
|
224
263
|
* care about one source — fetches a smaller payload.
|
|
225
264
|
*/
|
|
226
265
|
async pollSource(sourceId, options = {}) {
|
|
227
|
-
const { tags, buckets = ["new", "updated"] } = options;
|
|
266
|
+
const { tags, buckets = ["new", "updated", "flagged"] } = options;
|
|
228
267
|
return this.pollFeed({
|
|
229
|
-
headUrl: `${this.baseUrl}/diff
|
|
230
|
-
latestUrl: `${this.baseUrl}/diff
|
|
268
|
+
headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,
|
|
269
|
+
latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,
|
|
231
270
|
cursorKey: `source:${sourceId}`,
|
|
232
271
|
tags,
|
|
233
272
|
sources: void 0,
|
|
@@ -236,7 +275,7 @@ var DiffDelta = class {
|
|
|
236
275
|
}
|
|
237
276
|
// ── Low-level fetch ──
|
|
238
277
|
/**
|
|
239
|
-
* Fetch a head.json pointer.
|
|
278
|
+
* Fetch a head.json pointer. Cheapest call (~200 bytes).
|
|
240
279
|
* @param url Full URL to head.json. Defaults to global head.
|
|
241
280
|
*/
|
|
242
281
|
async head(url) {
|
|
@@ -259,6 +298,42 @@ var DiffDelta = class {
|
|
|
259
298
|
const raw = data.sources || [];
|
|
260
299
|
return raw.map(parseSourceInfo);
|
|
261
300
|
}
|
|
301
|
+
// ── Discovery & Health ──
|
|
302
|
+
/**
|
|
303
|
+
* Check pipeline health. Returns when the engine last ran and whether
|
|
304
|
+
* all sources are healthy. A stale timestamp means the pipeline is down.
|
|
305
|
+
*/
|
|
306
|
+
async checkHealth() {
|
|
307
|
+
const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);
|
|
308
|
+
return parseHealthCheck(data);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Given a list of dependency names your bot uses, returns the source IDs
|
|
312
|
+
* you should monitor. Uses the static stacks.json mapping — no API call,
|
|
313
|
+
* pure local lookup after one fetch.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* const sources = await dd.discoverSources(["openai", "langchain", "pinecone"]);
|
|
318
|
+
* // → ["openai_sdk_releases", "openai_api_changelog", "langchain_releases", "pinecone_status"]
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
async discoverSources(dependencies) {
|
|
322
|
+
const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);
|
|
323
|
+
const depsObj = data.dependencies || data.dependency_map || {};
|
|
324
|
+
const sourceIds = /* @__PURE__ */ new Set();
|
|
325
|
+
for (const dep of dependencies) {
|
|
326
|
+
const entry = depsObj[dep.toLowerCase()];
|
|
327
|
+
if (!entry) continue;
|
|
328
|
+
const sources = Array.isArray(entry) ? entry : entry.sources;
|
|
329
|
+
if (Array.isArray(sources)) {
|
|
330
|
+
for (const s of sources) {
|
|
331
|
+
sourceIds.add(s);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return [...sourceIds];
|
|
336
|
+
}
|
|
262
337
|
// ── Continuous monitoring ──
|
|
263
338
|
/**
|
|
264
339
|
* Continuously poll and call a function for each new item.
|
|
@@ -270,6 +345,9 @@ var DiffDelta = class {
|
|
|
270
345
|
* ```ts
|
|
271
346
|
* dd.watch(item => {
|
|
272
347
|
* console.log(`🚨 ${item.source}: ${item.headline}`);
|
|
348
|
+
* if (item.suggestedAction === "PATCH_IMMEDIATELY") {
|
|
349
|
+
* triggerAlert(item);
|
|
350
|
+
* }
|
|
273
351
|
* }, { tags: ["security"] });
|
|
274
352
|
* ```
|
|
275
353
|
*
|
|
@@ -289,7 +367,7 @@ var DiffDelta = class {
|
|
|
289
367
|
const h = await this.head();
|
|
290
368
|
interval = Math.max(h.ttlSec, 60);
|
|
291
369
|
} catch {
|
|
292
|
-
interval =
|
|
370
|
+
interval = 60;
|
|
293
371
|
}
|
|
294
372
|
}
|
|
295
373
|
console.log(`[diffdelta] Watching for changes every ${interval}s...`);
|
|
@@ -301,8 +379,6 @@ var DiffDelta = class {
|
|
|
301
379
|
for (const item of items) {
|
|
302
380
|
await callback(item);
|
|
303
381
|
}
|
|
304
|
-
} else {
|
|
305
|
-
console.log(`[diffdelta] No changes.`);
|
|
306
382
|
}
|
|
307
383
|
} catch (err) {
|
|
308
384
|
if (signal?.aborted) break;
|
|
@@ -310,10 +386,14 @@ var DiffDelta = class {
|
|
|
310
386
|
}
|
|
311
387
|
await new Promise((resolve) => {
|
|
312
388
|
const timer = setTimeout(resolve, interval * 1e3);
|
|
313
|
-
signal?.addEventListener(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
389
|
+
signal?.addEventListener(
|
|
390
|
+
"abort",
|
|
391
|
+
() => {
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
resolve();
|
|
394
|
+
},
|
|
395
|
+
{ once: true }
|
|
396
|
+
);
|
|
317
397
|
});
|
|
318
398
|
}
|
|
319
399
|
console.log("[diffdelta] Stopped.");
|
|
@@ -394,6 +474,11 @@ var DiffDelta = class {
|
|
|
394
474
|
0 && (module.exports = {
|
|
395
475
|
CursorStore,
|
|
396
476
|
DiffDelta,
|
|
397
|
-
MemoryCursorStore
|
|
477
|
+
MemoryCursorStore,
|
|
478
|
+
parseFeed,
|
|
479
|
+
parseFeedItem,
|
|
480
|
+
parseHead,
|
|
481
|
+
parseHealthCheck,
|
|
482
|
+
parseSourceInfo
|
|
398
483
|
});
|
|
399
484
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/cursor.ts","../src/models.ts","../src/client.ts"],"sourcesContent":["/**\n * DiffDelta — Agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n * const items = await dd.poll({ tags: [\"security\"] });\n * items.forEach(i => console.log(`🚨 ${i.source}: ${i.headline}`));\n * ```\n *\n * @packageDocumentation\n */\n\nexport { DiffDelta } from \"./client.js\";\nexport type { DiffDeltaOptions, PollOptions, WatchOptions } from \"./client.js\";\nexport type {\n FeedItem,\n Feed,\n Head,\n SourceInfo,\n} from \"./models.js\";\nexport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst DEFAULT_CURSOR_DIR = join(homedir(), \".diffdelta\");\nconst DEFAULT_CURSOR_FILE = \"cursors.json\";\n\n/**\n * Persists cursors to a local JSON file so bots survive restarts.\n *\n * By default, cursors are saved to ~/.diffdelta/cursors.json.\n * Each feed key gets its own cursor entry.\n */\nexport class CursorStore {\n private path: string;\n private cursors: Record<string, string> = {};\n\n constructor(path?: string) {\n this.path =\n path ||\n process.env.DD_CURSOR_PATH ||\n join(DEFAULT_CURSOR_DIR, DEFAULT_CURSOR_FILE);\n this.load();\n }\n\n /** Get the stored cursor for a feed key. */\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n /** Save a cursor and persist to disk. */\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n this.save();\n }\n\n /** Clear cursor(s). If key is undefined, clears all cursors. */\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n this.save();\n }\n\n private load(): void {\n try {\n if (existsSync(this.path)) {\n const raw = readFileSync(this.path, \"utf-8\");\n const data = JSON.parse(raw);\n if (typeof data === \"object\" && data !== null) {\n this.cursors = data;\n }\n }\n } catch {\n // Corrupted file — start fresh\n this.cursors = {};\n }\n }\n\n private save(): void {\n try {\n const dir = dirname(this.path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(this.path, JSON.stringify(this.cursors, null, 2));\n } catch {\n // Can't write — silently continue (in-memory only)\n }\n }\n}\n\n/**\n * In-memory-only cursor store (no file I/O).\n * Useful for serverless, browser, or Deno environments.\n */\nexport class MemoryCursorStore {\n private cursors: Record<string, string> = {};\n\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n }\n\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n }\n}\n","/** A single item from a DiffDelta feed. */\nexport interface FeedItem {\n /** Source identifier (e.g. \"cisa_kev\", \"nist_nvd\"). */\n source: string;\n /** Unique item ID within the source. */\n id: string;\n /** Human/agent-readable headline. */\n headline: string;\n /** Link to the original source. */\n url: string;\n /** Summary text extracted from the source. */\n excerpt: string;\n /** When the item was originally published. */\n publishedAt: string | null;\n /** When the item was last updated. */\n updatedAt: string | null;\n /** Which change bucket: \"new\", \"updated\", or \"removed\". */\n bucket: \"new\" | \"updated\" | \"removed\";\n /** Risk score 0–10, or null if not scored. */\n riskScore: number | null;\n /** Raw provenance data (fetched_at, evidence_urls, content_hash). */\n provenance: Record<string, unknown>;\n /** The full raw item object from the feed. */\n raw: Record<string, unknown>;\n}\n\n/** The lightweight head pointer for change detection. */\nexport interface Head {\n /** Opaque cursor string for change detection. */\n cursor: string;\n /** Content hash of the latest feed. */\n hash: string;\n /** Whether content has changed since last generation. */\n changed: boolean;\n /** When this head was generated. */\n generatedAt: string;\n /** Recommended polling interval in seconds. */\n ttlSec: number;\n}\n\n/** A full DiffDelta feed response. */\nexport interface Feed {\n /** The new cursor (save this for next poll). */\n cursor: string;\n /** The previous cursor. */\n prevCursor: string;\n /** Source ID (if per-source feed) or \"global\". */\n sourceId: string;\n /** When this feed was generated. */\n generatedAt: string;\n /** All items across all buckets. */\n items: FeedItem[];\n /** Items in the \"new\" bucket. */\n new: FeedItem[];\n /** Items in the \"updated\" bucket. */\n updated: FeedItem[];\n /** Items in the \"removed\" bucket. */\n removed: FeedItem[];\n /** Human-readable summary of what changed. */\n narrative: string;\n /** The full raw feed object. */\n raw: Record<string, unknown>;\n}\n\n/** Metadata about an available DiffDelta source. */\nexport interface SourceInfo {\n /** Unique source identifier (e.g. \"cisa_kev\"). */\n sourceId: string;\n /** Human-readable display name. */\n name: string;\n /** List of tags (e.g. [\"security\"]). */\n tags: string[];\n /** Brief description of the source. */\n description: string;\n /** URL of the source's homepage. */\n homepage: string;\n /** Whether the source is currently active. */\n enabled: boolean;\n /** Health status (\"ok\", \"degraded\", \"error\"). */\n status: string;\n /** Path to the source's head.json. */\n headUrl: string;\n /** Path to the source's latest.json. */\n latestUrl: string;\n}\n\n// ── Parsing helpers ──\n\n/** Parse a raw feed item into a typed FeedItem. */\nexport function parseFeedItem(\n data: Record<string, unknown>,\n bucket: \"new\" | \"updated\" | \"removed\" = \"new\"\n): FeedItem {\n const content = data.content as Record<string, unknown> | string | undefined;\n let excerpt = \"\";\n if (typeof content === \"object\" && content !== null) {\n excerpt =\n (content.excerpt_text as string) ||\n (content.summary as string) ||\n \"\";\n } else if (typeof content === \"string\") {\n excerpt = content;\n }\n\n // Extract risk_score from top-level or nested summary\n let riskScore: number | null = (data.risk_score as number) ?? null;\n if (riskScore === null) {\n const summary = data.summary as Record<string, unknown> | undefined;\n if (typeof summary === \"object\" && summary !== null) {\n riskScore = (summary.risk_score as number) ?? null;\n }\n }\n\n return {\n source: (data.source as string) || \"\",\n id: (data.id as string) || \"\",\n headline: (data.headline as string) || \"\",\n url: (data.url as string) || \"\",\n excerpt,\n publishedAt: (data.published_at as string) || null,\n updatedAt: (data.updated_at as string) || null,\n bucket,\n riskScore,\n provenance: (data.provenance as Record<string, unknown>) || {},\n raw: data,\n };\n}\n\n/** Parse a raw head.json response into a typed Head. */\nexport function parseHead(data: Record<string, unknown>): Head {\n return {\n cursor: (data.cursor as string) || \"\",\n hash: (data.hash as string) || \"\",\n changed: (data.changed as boolean) || false,\n generatedAt: (data.generated_at as string) || \"\",\n ttlSec: (data.ttl_sec as number) || 900,\n };\n}\n\n/** Parse a raw latest.json response into a typed Feed. */\nexport function parseFeed(data: Record<string, unknown>): Feed {\n const buckets = (data.buckets as Record<string, unknown[]>) || {};\n const newItems = (buckets.new || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"new\")\n );\n const updatedItems = (buckets.updated || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"updated\")\n );\n const removedItems = (buckets.removed || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"removed\")\n );\n\n return {\n cursor: (data.cursor as string) || \"\",\n prevCursor: (data.prev_cursor as string) || \"\",\n sourceId: (data.source_id as string) || \"\",\n generatedAt: (data.generated_at as string) || \"\",\n items: [...newItems, ...updatedItems, ...removedItems],\n new: newItems,\n updated: updatedItems,\n removed: removedItems,\n narrative: (data.batch_narrative as string) || \"\",\n raw: data,\n };\n}\n\n/** Parse a raw source object into a typed SourceInfo. */\nexport function parseSourceInfo(data: Record<string, unknown>): SourceInfo {\n return {\n sourceId: (data.source_id as string) || \"\",\n name: (data.name as string) || \"\",\n tags: (data.tags as string[]) || [],\n description: (data.description as string) || \"\",\n homepage: (data.homepage as string) || \"\",\n enabled: (data.enabled as boolean) ?? true,\n status: (data.status as string) || \"ok\",\n headUrl: (data.head_url as string) || \"\",\n latestUrl: (data.latest_url as string) || \"\",\n };\n}\n","/**\n * DiffDelta TypeScript client — agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n *\n * // Poll for new items across all sources\n * const items = await dd.poll();\n * items.forEach(i => console.log(`${i.source}: ${i.headline}`));\n *\n * // Poll only security sources\n * const sec = await dd.poll({ tags: [\"security\"] });\n *\n * // Continuous monitoring\n * dd.watch(item => console.log(\"🚨\", item.headline), { tags: [\"security\"] });\n * ```\n */\n\nimport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\nimport type { FeedItem, Feed, Head, SourceInfo } from \"./models.js\";\nimport { parseFeedItem, parseFeed, parseHead, parseSourceInfo } from \"./models.js\";\n\nconst VERSION = \"0.1.0\";\nconst DEFAULT_BASE_URL = \"https://diffdelta.io\";\nconst DEFAULT_TIMEOUT = 15_000; // ms\n\nexport interface DiffDeltaOptions {\n /** DiffDelta API base URL. Defaults to https://diffdelta.io. */\n baseUrl?: string;\n /** Pro/Enterprise API key (dd_live_...). */\n apiKey?: string;\n /**\n * Path to cursor file. Defaults to ~/.diffdelta/cursors.json.\n * Set to `null` to disable file persistence (in-memory only).\n * Set to `\"memory\"` for explicit in-memory mode (serverless, edge).\n */\n cursorPath?: string | null;\n /** HTTP timeout in milliseconds. Defaults to 15000. */\n timeout?: number;\n}\n\nexport interface PollOptions {\n /** Filter items to these tags (e.g. [\"security\"]). */\n tags?: string[];\n /** Filter items to these source IDs (e.g. [\"cisa_kev\", \"nist_nvd\"]). */\n sources?: string[];\n /**\n * Which buckets to return.\n * Defaults to [\"new\", \"updated\"].\n * Use [\"new\", \"updated\", \"removed\"] to include removals.\n */\n buckets?: Array<\"new\" | \"updated\" | \"removed\">;\n}\n\nexport interface WatchOptions extends PollOptions {\n /** Seconds between polls. Defaults to feed TTL (usually 900s). */\n interval?: number;\n /** If provided, an AbortSignal to stop watching. */\n signal?: AbortSignal;\n}\n\ninterface CursorStoreInterface {\n get(key: string): string | undefined;\n set(key: string, cursor: string): void;\n clear(key?: string): void;\n}\n\nexport class DiffDelta {\n readonly baseUrl: string;\n readonly apiKey?: string;\n readonly timeout: number;\n\n private cursors: CursorStoreInterface;\n private sourceTagsCache: Record<string, string[]> | null = null;\n\n constructor(options: DiffDeltaOptions = {}) {\n this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n\n // Cursor persistence\n if (options.cursorPath === null || options.cursorPath === \"memory\") {\n this.cursors = new MemoryCursorStore();\n } else {\n try {\n this.cursors = new CursorStore(options.cursorPath || undefined);\n } catch {\n // Fallback to memory if file system not available\n this.cursors = new MemoryCursorStore();\n }\n }\n }\n\n // ── Core polling ──\n\n /**\n * Poll the global feed for new items since last poll.\n *\n * Checks head.json first (~400 bytes). Only fetches the full feed\n * if the cursor has changed. Automatically saves the new cursor.\n */\n async poll(options: PollOptions = {}): Promise<FeedItem[]> {\n const { tags, sources, buckets = [\"new\", \"updated\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/head.json`,\n latestUrl: `${this.baseUrl}/diff/latest.json`,\n cursorKey: \"global\",\n tags,\n sources,\n buckets,\n });\n }\n\n /**\n * Poll a specific source for new items since last poll.\n *\n * More efficient than `poll({ sources: [...] })` if you only\n * care about one source — fetches a smaller payload.\n */\n async pollSource(\n sourceId: string,\n options: Omit<PollOptions, \"sources\"> = {}\n ): Promise<FeedItem[]> {\n const { tags, buckets = [\"new\", \"updated\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/source/${sourceId}/head.json`,\n latestUrl: `${this.baseUrl}/diff/source/${sourceId}/latest.json`,\n cursorKey: `source:${sourceId}`,\n tags,\n sources: undefined,\n buckets,\n });\n }\n\n // ── Low-level fetch ──\n\n /**\n * Fetch a head.json pointer.\n * @param url Full URL to head.json. Defaults to global head.\n */\n async head(url?: string): Promise<Head> {\n const data = await this.fetchJson(url || `${this.baseUrl}/diff/head.json`);\n return parseHead(data);\n }\n\n /**\n * Fetch a full latest.json feed.\n * @param url Full URL to latest.json. Defaults to global latest.\n */\n async fetchFeed(url?: string): Promise<Feed> {\n const data = await this.fetchJson(\n url || `${this.baseUrl}/diff/latest.json`\n );\n return parseFeed(data);\n }\n\n /** List all available DiffDelta sources. */\n async sources(): Promise<SourceInfo[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/sources.json`);\n const raw = (data.sources || []) as Record<string, unknown>[];\n return raw.map(parseSourceInfo);\n }\n\n // ── Continuous monitoring ──\n\n /**\n * Continuously poll and call a function for each new item.\n *\n * @param callback - Async or sync function called for each new FeedItem.\n * @param options - Watch options (tags, sources, interval, signal).\n *\n * @example\n * ```ts\n * dd.watch(item => {\n * console.log(`🚨 ${item.source}: ${item.headline}`);\n * }, { tags: [\"security\"] });\n * ```\n *\n * @example\n * ```ts\n * // Stop with AbortController\n * const ac = new AbortController();\n * dd.watch(handler, { signal: ac.signal });\n * setTimeout(() => ac.abort(), 60_000); // stop after 1 minute\n * ```\n */\n async watch(\n callback: (item: FeedItem) => void | Promise<void>,\n options: WatchOptions = {}\n ): Promise<void> {\n const { tags, sources, buckets, signal } = options;\n let interval = options.interval;\n\n // Determine interval from feed TTL if not specified\n if (!interval) {\n try {\n const h = await this.head();\n interval = Math.max(h.ttlSec, 60);\n } catch {\n interval = 900; // default 15 minutes\n }\n }\n\n console.log(`[diffdelta] Watching for changes every ${interval}s...`);\n\n while (!signal?.aborted) {\n try {\n const items = await this.poll({ tags, sources, buckets });\n if (items.length > 0) {\n console.log(`[diffdelta] ${items.length} new item(s) found.`);\n for (const item of items) {\n await callback(item);\n }\n } else {\n console.log(`[diffdelta] No changes.`);\n }\n } catch (err) {\n if (signal?.aborted) break;\n console.error(`[diffdelta] Error: ${err}. Retrying in ${interval}s...`);\n }\n\n // Sleep with abort support\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, interval! * 1000);\n signal?.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n resolve();\n }, { once: true });\n });\n }\n\n console.log(\"[diffdelta] Stopped.\");\n }\n\n // ── Cursor management ──\n\n /** Reset stored cursors so the next poll returns all current items. */\n resetCursors(sourceId?: string): void {\n if (sourceId) {\n this.cursors.clear(`source:${sourceId}`);\n } else {\n this.cursors.clear();\n }\n }\n\n // ── Internal ──\n\n private async pollFeed(params: {\n headUrl: string;\n latestUrl: string;\n cursorKey: string;\n tags?: string[];\n sources?: string[];\n buckets: string[];\n }): Promise<FeedItem[]> {\n const { headUrl, latestUrl, cursorKey, tags, sources, buckets } = params;\n\n // Step 1: Fetch head.json (~400 bytes)\n const head = await this.head(headUrl);\n\n // Step 2: Compare cursor\n const storedCursor = this.cursors.get(cursorKey);\n if (storedCursor && storedCursor === head.cursor) {\n return []; // Nothing changed\n }\n\n // Step 3: Fetch full feed\n const feed = await this.fetchFeed(latestUrl);\n\n // Step 4: Save new cursor\n if (feed.cursor) {\n this.cursors.set(cursorKey, feed.cursor);\n }\n\n // Step 5: Filter and return\n let items = feed.items;\n\n // Filter by bucket\n items = items.filter((i) => buckets.includes(i.bucket));\n\n // Filter by source\n if (sources?.length) {\n items = items.filter((i) => sources.includes(i.source));\n }\n\n // Filter by tags\n if (tags?.length) {\n const tagMap = await this.getSourceTags();\n items = items.filter((i) => {\n const itemTags = tagMap[i.source] || [];\n return tags.some((t) => itemTags.includes(t));\n });\n }\n\n return items;\n }\n\n private async getSourceTags(): Promise<Record<string, string[]>> {\n if (this.sourceTagsCache) return this.sourceTagsCache;\n try {\n const allSources = await this.sources();\n this.sourceTagsCache = Object.fromEntries(\n allSources.map((s) => [s.sourceId, s.tags])\n );\n } catch {\n this.sourceTagsCache = {};\n }\n return this.sourceTagsCache;\n }\n\n private async fetchJson(url: string): Promise<Record<string, unknown>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const headers: Record<string, string> = {\n \"User-Agent\": `diffdelta-js/${VERSION}`,\n Accept: \"application/json\",\n };\n if (this.apiKey) {\n headers[\"X-DiffDelta-Key\"] = this.apiKey;\n }\n\n const res = await fetch(url, { headers, signal: controller.signal });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`);\n }\n return (await res.json()) as Record<string, unknown>;\n } finally {\n clearTimeout(timer);\n }\n }\n\n toString(): string {\n const tier = this.apiKey ? \"pro\" : \"free\";\n return `DiffDelta(baseUrl=${this.baseUrl}, tier=${tier})`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cursor.ts","../src/models.ts","../src/client.ts"],"sourcesContent":["/**\n * DiffDelta — Agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n * const items = await dd.poll({ tags: [\"security\"] });\n * items.forEach(i => console.log(`🚨 ${i.source}: ${i.headline}`));\n * ```\n *\n * @packageDocumentation\n */\n\nexport { DiffDelta } from \"./client.js\";\nexport type { DiffDeltaOptions, PollOptions, WatchOptions } from \"./client.js\";\nexport type {\n FeedItem,\n Feed,\n Head,\n SourceInfo,\n HealthCheck,\n Signals,\n SeveritySignal,\n ReleaseSignal,\n IncidentSignal,\n DeprecationSignal,\n SignalProvenance,\n SuggestedAction,\n} from \"./models.js\";\nexport {\n parseFeedItem,\n parseFeed,\n parseHead,\n parseSourceInfo,\n parseHealthCheck,\n} from \"./models.js\";\nexport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst DEFAULT_CURSOR_DIR = join(homedir(), \".diffdelta\");\nconst DEFAULT_CURSOR_FILE = \"cursors.json\";\n\n/**\n * Persists cursors to a local JSON file so bots survive restarts.\n *\n * By default, cursors are saved to ~/.diffdelta/cursors.json.\n * Each feed key gets its own cursor entry.\n */\nexport class CursorStore {\n private path: string;\n private cursors: Record<string, string> = {};\n\n constructor(path?: string) {\n this.path =\n path ||\n process.env.DD_CURSOR_PATH ||\n join(DEFAULT_CURSOR_DIR, DEFAULT_CURSOR_FILE);\n this.load();\n }\n\n /** Get the stored cursor for a feed key. */\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n /** Save a cursor and persist to disk. */\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n this.save();\n }\n\n /** Clear cursor(s). If key is undefined, clears all cursors. */\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n this.save();\n }\n\n private load(): void {\n try {\n if (existsSync(this.path)) {\n const raw = readFileSync(this.path, \"utf-8\");\n const data = JSON.parse(raw);\n if (typeof data === \"object\" && data !== null) {\n this.cursors = data;\n }\n }\n } catch {\n // Corrupted file — start fresh\n this.cursors = {};\n }\n }\n\n private save(): void {\n try {\n const dir = dirname(this.path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(this.path, JSON.stringify(this.cursors, null, 2));\n } catch {\n // Can't write — silently continue (in-memory only)\n }\n }\n}\n\n/**\n * In-memory-only cursor store (no file I/O).\n * Useful for serverless, browser, or Deno environments.\n */\nexport class MemoryCursorStore {\n private cursors: Record<string, string> = {};\n\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n }\n\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n }\n}\n","// ── Signal types ──\n\n/** Severity signal extracted from security advisories. */\nexport interface SeveritySignal {\n level: \"critical\" | \"high\" | \"medium\" | \"low\" | string;\n source: string;\n cvss?: number;\n cwes?: string[];\n packages?: string[];\n exploited?: boolean;\n provenance?: SignalProvenance;\n}\n\n/** Release signal extracted from changelogs/GitHub releases. */\nexport interface ReleaseSignal {\n version: string;\n prerelease?: boolean;\n security_patch?: boolean;\n provenance?: SignalProvenance;\n}\n\n/** Incident signal extracted from status pages. */\nexport interface IncidentSignal {\n status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\" | string;\n impact?: \"minor\" | \"major\" | \"critical\" | string;\n provenance?: SignalProvenance;\n}\n\n/** Deprecation signal extracted from changelogs/advisories. */\nexport interface DeprecationSignal {\n type: \"breaking_change\" | \"end_of_life\" | \"removal\" | \"deprecated\" | string;\n affects?: string[];\n confidence: \"high\" | \"medium\" | \"low\" | string;\n source: string;\n provenance?: SignalProvenance;\n}\n\n/** Provenance chain for a signal — traces it to an authoritative source. */\nexport interface SignalProvenance {\n method: string;\n authority: string;\n authority_url: string;\n evidence_url: string;\n}\n\n/** Action codes telling a bot exactly what to do. */\nexport type SuggestedAction =\n | \"PATCH_IMMEDIATELY\"\n | \"PATCH_SOON\"\n | \"VERSION_PIN\"\n | \"REVIEW_CHANGELOG\"\n | \"MONITOR_STATUS\"\n | \"ACKNOWLEDGE\"\n | \"NO_ACTION\";\n\n/** All structured signals on an item. */\nexport interface Signals {\n severity?: SeveritySignal;\n release?: ReleaseSignal;\n incident?: IncidentSignal;\n deprecation?: DeprecationSignal;\n suggested_action?: SuggestedAction;\n [key: string]: unknown;\n}\n\n// ── Core types ──\n\n/** A single item from a DiffDelta feed. */\nexport interface FeedItem {\n /** Source identifier (e.g. \"cisa_kev\", \"github_advisories\"). */\n source: string;\n /** Unique item ID within the source. */\n id: string;\n /** Human/agent-readable headline. */\n headline: string;\n /** Link to the original source. */\n url: string;\n /** Summary text extracted from the source. */\n excerpt: string;\n /** When the item was originally published. */\n publishedAt: string | null;\n /** When the item was last updated. */\n updatedAt: string | null;\n /** Which change bucket: \"new\", \"updated\", \"removed\", or \"flagged\". */\n bucket: \"new\" | \"updated\" | \"removed\" | \"flagged\";\n /** Structured signals: severity, release, incident, deprecation, suggested_action. */\n signals: Signals;\n /** Shortcut: the suggested_action code, if any. */\n suggestedAction: SuggestedAction | null;\n /** Risk score 0.0–1.0, or null if not scored. */\n riskScore: number | null;\n /** Item-level provenance (fetched_at, evidence_urls, content_hash). */\n provenance: Record<string, unknown>;\n /** The full raw item object from the feed. */\n raw: Record<string, unknown>;\n}\n\n/** The lightweight head pointer for change detection. */\nexport interface Head {\n /** Opaque cursor string for change detection. */\n cursor: string;\n /** Whether content has changed since last generation. */\n changed: boolean;\n /** When this head was generated. */\n generatedAt: string;\n /** Recommended polling interval in seconds. */\n ttlSec: number;\n /** URL to the full latest.json feed. */\n latestUrl: string;\n /** URL to the digest (global head only). */\n digestUrl: string | null;\n /** Item counts: new, updated, removed, flagged. */\n counts: { new: number; updated: number; removed: number; flagged: number };\n /** Number of sources checked this cycle (Verified Silence). */\n sourcesChecked: number;\n /** Number of sources healthy this cycle. */\n sourcesOk: number;\n /** True if nothing changed AND all sources are healthy. */\n allClear: boolean;\n /** 0.0–1.0 confidence that allClear is trustworthy. */\n allClearConfidence: number | null;\n /** Pipeline freshness: oldest data age, stale count. */\n freshness: {\n oldest_data_age_sec: number;\n mean_data_age_sec: number;\n stale_count: number;\n all_fresh: boolean;\n } | null;\n /** The full raw head.json object. */\n raw: Record<string, unknown>;\n}\n\n/** A full DiffDelta feed response. */\nexport interface Feed {\n /** The new cursor (save this for next poll). */\n cursor: string;\n /** The previous cursor. */\n prevCursor: string;\n /** Source ID (if per-source feed) or \"global\". */\n sourceId: string;\n /** When this feed was generated. */\n generatedAt: string;\n /** All items across all buckets. */\n items: FeedItem[];\n /** Items in the \"new\" bucket. */\n new: FeedItem[];\n /** Items in the \"updated\" bucket. */\n updated: FeedItem[];\n /** Items in the \"removed\" bucket. */\n removed: FeedItem[];\n /** Items in the \"flagged\" bucket. */\n flagged: FeedItem[];\n /** Human-readable summary of what changed. */\n narrative: string;\n /** The full raw feed object. */\n raw: Record<string, unknown>;\n}\n\n/** Metadata about an available DiffDelta source. */\nexport interface SourceInfo {\n /** Unique source identifier (e.g. \"cisa_kev\"). */\n sourceId: string;\n /** Human-readable display name. */\n name: string;\n /** List of tags (e.g. [\"security\"]). */\n tags: string[];\n /** Brief description of the source. */\n description: string;\n /** URL of the source's homepage. */\n homepage: string;\n /** Whether the source is currently active. */\n enabled: boolean;\n /** Health status (\"ok\", \"degraded\", \"error\"). */\n status: string;\n /** Path to the source's head.json. */\n headUrl: string;\n /** Path to the source's latest.json. */\n latestUrl: string;\n}\n\n/** Health check response from /healthz.json. */\nexport interface HealthCheck {\n ok: boolean;\n service: string;\n time: string;\n sourcesChecked: number;\n sourcesOk: number;\n engineVersion: string;\n}\n\n// ── Parsing helpers ──\n\n/** Parse a raw feed item into a typed FeedItem. */\nexport function parseFeedItem(\n data: Record<string, unknown>,\n bucket: \"new\" | \"updated\" | \"removed\" | \"flagged\" = \"new\"\n): FeedItem {\n const content = data.content as Record<string, unknown> | string | undefined;\n let excerpt = \"\";\n if (typeof content === \"object\" && content !== null) {\n excerpt =\n (content.excerpt_text as string) ||\n (content.summary as string) ||\n \"\";\n } else if (typeof content === \"string\") {\n excerpt = content;\n }\n\n // Extract signals\n const signals = (data.signals as Signals) || {};\n const suggestedAction = (signals.suggested_action as SuggestedAction) || null;\n\n // Extract risk score from risk.score or legacy risk_score\n let riskScore: number | null = null;\n const risk = data.risk as Record<string, unknown> | undefined;\n if (typeof risk === \"object\" && risk !== null) {\n riskScore = (risk.score as number) ?? null;\n }\n if (riskScore === null) {\n riskScore = (data.risk_score as number) ?? null;\n }\n\n return {\n source: (data.source as string) || \"\",\n id: (data.id as string) || \"\",\n headline: (data.headline as string) || \"\",\n url: (data.url as string) || \"\",\n excerpt,\n publishedAt: (data.published_at as string) || null,\n updatedAt: (data.updated_at as string) || null,\n bucket,\n signals,\n suggestedAction,\n riskScore,\n provenance: (data.provenance as Record<string, unknown>) || {},\n raw: data,\n };\n}\n\n/** Parse a raw head.json response into a typed Head. */\nexport function parseHead(data: Record<string, unknown>): Head {\n const counts = (data.counts as Record<string, number>) || {};\n const freshness = data.freshness as Head[\"freshness\"] | undefined;\n\n return {\n cursor: (data.cursor as string) || \"\",\n changed: (data.changed as boolean) || false,\n generatedAt: (data.generated_at as string) || \"\",\n ttlSec: (data.ttl_sec as number) || 60,\n latestUrl: (data.latest_url as string) || \"\",\n digestUrl: (data.digest_url as string) || null,\n counts: {\n new: counts.new || 0,\n updated: counts.updated || 0,\n removed: counts.removed || 0,\n flagged: counts.flagged || 0,\n },\n sourcesChecked: (data.sources_checked as number) || 0,\n sourcesOk: (data.sources_ok as number) || 0,\n allClear: (data.all_clear as boolean) || false,\n allClearConfidence:\n (data.all_clear_confidence as number) ??\n (data.confidence as number) ??\n null,\n freshness: freshness || null,\n raw: data,\n };\n}\n\n/** Parse a raw latest.json response into a typed Feed. */\nexport function parseFeed(data: Record<string, unknown>): Feed {\n const buckets = (data.buckets as Record<string, unknown[]>) || {};\n const newItems = (buckets.new || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"new\")\n );\n const updatedItems = (buckets.updated || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"updated\")\n );\n const removedItems = (buckets.removed || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"removed\")\n );\n const flaggedItems = (buckets.flagged || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"flagged\")\n );\n\n return {\n cursor: (data.cursor as string) || \"\",\n prevCursor: (data.prev_cursor as string) || \"\",\n sourceId: (data.source_id as string) || \"\",\n generatedAt: (data.generated_at as string) || \"\",\n items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],\n new: newItems,\n updated: updatedItems,\n removed: removedItems,\n flagged: flaggedItems,\n narrative: (data.batch_narrative as string) || \"\",\n raw: data,\n };\n}\n\n/** Parse a raw source object into a typed SourceInfo. */\nexport function parseSourceInfo(data: Record<string, unknown>): SourceInfo {\n return {\n sourceId: (data.source_id as string) || \"\",\n name: (data.name as string) || \"\",\n tags: (data.tags as string[]) || [],\n description: (data.description as string) || \"\",\n homepage: (data.homepage as string) || \"\",\n enabled: (data.enabled as boolean) ?? true,\n status: (data.status as string) || \"ok\",\n headUrl: (data.head_url as string) || \"\",\n latestUrl: (data.latest_url as string) || \"\",\n };\n}\n\n/** Parse a raw healthz.json response into a typed HealthCheck. */\nexport function parseHealthCheck(data: Record<string, unknown>): HealthCheck {\n return {\n ok: (data.ok as boolean) || false,\n service: (data.service as string) || \"\",\n time: (data.time as string) || \"\",\n sourcesChecked: (data.sources_checked as number) || 0,\n sourcesOk: (data.sources_ok as number) || 0,\n engineVersion: (data.engine_version as string) || \"\",\n };\n}\n","/**\n * DiffDelta TypeScript client — agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n *\n * // Quick health check — is the pipeline alive?\n * const health = await dd.checkHealth();\n * console.log(`Pipeline: ${health.ok ? \"healthy\" : \"degraded\"}, last run: ${health.time}`);\n *\n * // Poll for new items across all sources\n * const items = await dd.poll();\n * items.forEach(i => console.log(`${i.source}: ${i.headline}`));\n *\n * // Poll only security sources\n * const sec = await dd.poll({ tags: [\"security\"] });\n *\n * // Check what's relevant to your stack\n * const sources = await dd.discoverSources([\"openai\", \"langchain\", \"pinecone\"]);\n * console.log(\"Watch these:\", sources);\n *\n * // Continuous monitoring\n * dd.watch(item => console.log(\"🚨\", item.headline), { tags: [\"security\"] });\n * ```\n */\n\nimport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\nimport type { FeedItem, Feed, Head, SourceInfo, HealthCheck } from \"./models.js\";\nimport { parseFeedItem, parseFeed, parseHead, parseSourceInfo, parseHealthCheck } from \"./models.js\";\n\nconst VERSION = \"0.1.1\";\nconst DEFAULT_BASE_URL = \"https://diffdelta.io\";\nconst DEFAULT_TIMEOUT = 15_000; // ms\n\nexport interface DiffDeltaOptions {\n /** DiffDelta API base URL. Defaults to https://diffdelta.io. */\n baseUrl?: string;\n /** Pro/Enterprise API key (dd_live_...). */\n apiKey?: string;\n /**\n * Path to cursor file. Defaults to ~/.diffdelta/cursors.json.\n * Set to `null` to disable file persistence (in-memory only).\n * Set to `\"memory\"` for explicit in-memory mode (serverless, edge).\n */\n cursorPath?: string | null;\n /** HTTP timeout in milliseconds. Defaults to 15000. */\n timeout?: number;\n}\n\nexport interface PollOptions {\n /** Filter items to these tags (e.g. [\"security\"]). */\n tags?: string[];\n /** Filter items to these source IDs (e.g. [\"cisa_kev\", \"github_advisories\"]). */\n sources?: string[];\n /**\n * Which buckets to return.\n * Defaults to [\"new\", \"updated\", \"flagged\"].\n * Use [\"new\", \"updated\", \"removed\", \"flagged\"] to include removals.\n */\n buckets?: Array<\"new\" | \"updated\" | \"removed\" | \"flagged\">;\n}\n\nexport interface WatchOptions extends PollOptions {\n /** Seconds between polls. Defaults to feed TTL (usually 60s). */\n interval?: number;\n /** If provided, an AbortSignal to stop watching. */\n signal?: AbortSignal;\n}\n\ninterface CursorStoreInterface {\n get(key: string): string | undefined;\n set(key: string, cursor: string): void;\n clear(key?: string): void;\n}\n\nexport class DiffDelta {\n readonly baseUrl: string;\n readonly apiKey?: string;\n readonly timeout: number;\n\n private cursors: CursorStoreInterface;\n private sourceTagsCache: Record<string, string[]> | null = null;\n\n constructor(options: DiffDeltaOptions = {}) {\n this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n\n // Cursor persistence\n if (options.cursorPath === null || options.cursorPath === \"memory\") {\n this.cursors = new MemoryCursorStore();\n } else {\n try {\n this.cursors = new CursorStore(options.cursorPath || undefined);\n } catch {\n // Fallback to memory if file system not available\n this.cursors = new MemoryCursorStore();\n }\n }\n }\n\n // ── Core polling ──\n\n /**\n * Poll the global feed for new items since last poll.\n *\n * Checks head.json first (~200 bytes). Only fetches the full feed\n * if the cursor has changed. Automatically saves the new cursor.\n */\n async poll(options: PollOptions = {}): Promise<FeedItem[]> {\n const { tags, sources, buckets = [\"new\", \"updated\", \"flagged\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/head.json`,\n latestUrl: `${this.baseUrl}/diff/latest.json`,\n cursorKey: \"global\",\n tags,\n sources,\n buckets,\n });\n }\n\n /**\n * Poll a specific source for new items since last poll.\n *\n * More efficient than `poll({ sources: [...] })` if you only\n * care about one source — fetches a smaller payload.\n */\n async pollSource(\n sourceId: string,\n options: Omit<PollOptions, \"sources\"> = {}\n ): Promise<FeedItem[]> {\n const { tags, buckets = [\"new\", \"updated\", \"flagged\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,\n latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,\n cursorKey: `source:${sourceId}`,\n tags,\n sources: undefined,\n buckets,\n });\n }\n\n // ── Low-level fetch ──\n\n /**\n * Fetch a head.json pointer. Cheapest call (~200 bytes).\n * @param url Full URL to head.json. Defaults to global head.\n */\n async head(url?: string): Promise<Head> {\n const data = await this.fetchJson(url || `${this.baseUrl}/diff/head.json`);\n return parseHead(data);\n }\n\n /**\n * Fetch a full latest.json feed.\n * @param url Full URL to latest.json. Defaults to global latest.\n */\n async fetchFeed(url?: string): Promise<Feed> {\n const data = await this.fetchJson(\n url || `${this.baseUrl}/diff/latest.json`\n );\n return parseFeed(data);\n }\n\n /** List all available DiffDelta sources. */\n async sources(): Promise<SourceInfo[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/sources.json`);\n const raw = (data.sources || []) as Record<string, unknown>[];\n return raw.map(parseSourceInfo);\n }\n\n // ── Discovery & Health ──\n\n /**\n * Check pipeline health. Returns when the engine last ran and whether\n * all sources are healthy. A stale timestamp means the pipeline is down.\n */\n async checkHealth(): Promise<HealthCheck> {\n const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);\n return parseHealthCheck(data);\n }\n\n /**\n * Given a list of dependency names your bot uses, returns the source IDs\n * you should monitor. Uses the static stacks.json mapping — no API call,\n * pure local lookup after one fetch.\n *\n * @example\n * ```ts\n * const sources = await dd.discoverSources([\"openai\", \"langchain\", \"pinecone\"]);\n * // → [\"openai_sdk_releases\", \"openai_api_changelog\", \"langchain_releases\", \"pinecone_status\"]\n * ```\n */\n async discoverSources(dependencies: string[]): Promise<string[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);\n // Support both formats: { dependencies: { x: { sources: [...] } } }\n // and legacy { dependency_map: { x: [...] } }\n const depsObj = (data.dependencies || data.dependency_map || {}) as Record<\n string,\n { sources?: string[] } | string[]\n >;\n const sourceIds = new Set<string>();\n for (const dep of dependencies) {\n const entry = depsObj[dep.toLowerCase()];\n if (!entry) continue;\n const sources = Array.isArray(entry) ? entry : entry.sources;\n if (Array.isArray(sources)) {\n for (const s of sources) {\n sourceIds.add(s);\n }\n }\n }\n return [...sourceIds];\n }\n\n // ── Continuous monitoring ──\n\n /**\n * Continuously poll and call a function for each new item.\n *\n * @param callback - Async or sync function called for each new FeedItem.\n * @param options - Watch options (tags, sources, interval, signal).\n *\n * @example\n * ```ts\n * dd.watch(item => {\n * console.log(`🚨 ${item.source}: ${item.headline}`);\n * if (item.suggestedAction === \"PATCH_IMMEDIATELY\") {\n * triggerAlert(item);\n * }\n * }, { tags: [\"security\"] });\n * ```\n *\n * @example\n * ```ts\n * // Stop with AbortController\n * const ac = new AbortController();\n * dd.watch(handler, { signal: ac.signal });\n * setTimeout(() => ac.abort(), 60_000); // stop after 1 minute\n * ```\n */\n async watch(\n callback: (item: FeedItem) => void | Promise<void>,\n options: WatchOptions = {}\n ): Promise<void> {\n const { tags, sources, buckets, signal } = options;\n let interval = options.interval;\n\n // Determine interval from feed TTL if not specified\n if (!interval) {\n try {\n const h = await this.head();\n interval = Math.max(h.ttlSec, 60);\n } catch {\n interval = 60; // default 1 minute\n }\n }\n\n console.log(`[diffdelta] Watching for changes every ${interval}s...`);\n\n while (!signal?.aborted) {\n try {\n const items = await this.poll({ tags, sources, buckets });\n if (items.length > 0) {\n console.log(`[diffdelta] ${items.length} new item(s) found.`);\n for (const item of items) {\n await callback(item);\n }\n }\n } catch (err) {\n if (signal?.aborted) break;\n console.error(`[diffdelta] Error: ${err}. Retrying in ${interval}s...`);\n }\n\n // Sleep with abort support\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, interval! * 1000);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n }\n\n console.log(\"[diffdelta] Stopped.\");\n }\n\n // ── Cursor management ──\n\n /** Reset stored cursors so the next poll returns all current items. */\n resetCursors(sourceId?: string): void {\n if (sourceId) {\n this.cursors.clear(`source:${sourceId}`);\n } else {\n this.cursors.clear();\n }\n }\n\n // ── Internal ──\n\n private async pollFeed(params: {\n headUrl: string;\n latestUrl: string;\n cursorKey: string;\n tags?: string[];\n sources?: string[];\n buckets: string[];\n }): Promise<FeedItem[]> {\n const { headUrl, latestUrl, cursorKey, tags, sources, buckets } = params;\n\n // Step 1: Fetch head.json (~200 bytes)\n const head = await this.head(headUrl);\n\n // Step 2: Compare cursor — if unchanged, nothing to do\n const storedCursor = this.cursors.get(cursorKey);\n if (storedCursor && storedCursor === head.cursor) {\n return []; // Nothing changed\n }\n\n // Step 3: Fetch full feed\n const feed = await this.fetchFeed(latestUrl);\n\n // Step 4: Save new cursor\n if (feed.cursor) {\n this.cursors.set(cursorKey, feed.cursor);\n }\n\n // Step 5: Filter and return\n let items = feed.items;\n\n // Filter by bucket\n items = items.filter((i) => buckets.includes(i.bucket));\n\n // Filter by source\n if (sources?.length) {\n items = items.filter((i) => sources.includes(i.source));\n }\n\n // Filter by tags\n if (tags?.length) {\n const tagMap = await this.getSourceTags();\n items = items.filter((i) => {\n const itemTags = tagMap[i.source] || [];\n return tags.some((t) => itemTags.includes(t));\n });\n }\n\n return items;\n }\n\n private async getSourceTags(): Promise<Record<string, string[]>> {\n if (this.sourceTagsCache) return this.sourceTagsCache;\n try {\n const allSources = await this.sources();\n this.sourceTagsCache = Object.fromEntries(\n allSources.map((s) => [s.sourceId, s.tags])\n );\n } catch {\n this.sourceTagsCache = {};\n }\n return this.sourceTagsCache;\n }\n\n private async fetchJson(url: string): Promise<Record<string, unknown>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const headers: Record<string, string> = {\n \"User-Agent\": `diffdelta-js/${VERSION}`,\n Accept: \"application/json\",\n };\n if (this.apiKey) {\n headers[\"X-DiffDelta-Key\"] = this.apiKey;\n }\n\n const res = await fetch(url, { headers, signal: controller.signal });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`);\n }\n return (await res.json()) as Record<string, unknown>;\n } finally {\n clearTimeout(timer);\n }\n }\n\n toString(): string {\n const tier = this.apiKey ? \"pro\" : \"free\";\n return `DiffDelta(baseUrl=${this.baseUrl}, tier=${tier})`;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAmE;AACnE,uBAA8B;AAC9B,qBAAwB;AAExB,IAAM,yBAAqB,2BAAK,wBAAQ,GAAG,YAAY;AACvD,IAAM,sBAAsB;AAQrB,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA,UAAkC,CAAC;AAAA,EAE3C,YAAY,MAAe;AACzB,SAAK,OACH,QACA,QAAQ,IAAI,sBACZ,uBAAK,oBAAoB,mBAAmB;AAC9C,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA,EAGA,IAAI,KAAiC;AACnC,WAAO,KAAK,QAAQ,GAAG;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,KAAa,QAAsB;AACrC,SAAK,QAAQ,GAAG,IAAI;AACpB,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA,EAGA,MAAM,KAAoB;AACxB,QAAI,KAAK;AACP,aAAO,KAAK,QAAQ,GAAG;AAAA,IACzB,OAAO;AACL,WAAK,UAAU,CAAC;AAAA,IAClB;AACA,SAAK,KAAK;AAAA,EACZ;AAAA,EAEQ,OAAa;AACnB,QAAI;AACF,cAAI,2BAAW,KAAK,IAAI,GAAG;AACzB,cAAM,UAAM,6BAAa,KAAK,MAAM,OAAO;AAC3C,cAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,eAAK,UAAU;AAAA,QACjB;AAAA,MACF;AAAA,IACF,QAAQ;AAEN,WAAK,UAAU,CAAC;AAAA,IAClB;AAAA,EACF;AAAA,EAEQ,OAAa;AACnB,QAAI;AACF,YAAM,UAAM,0BAAQ,KAAK,IAAI;AAC7B,UAAI,KAAC,2BAAW,GAAG,GAAG;AACpB,sCAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACpC;AACA,wCAAc,KAAK,MAAM,KAAK,UAAU,KAAK,SAAS,MAAM,CAAC,CAAC;AAAA,IAChE,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAMO,IAAM,oBAAN,MAAwB;AAAA,EACrB,UAAkC,CAAC;AAAA,EAE3C,IAAI,KAAiC;AACnC,WAAO,KAAK,QAAQ,GAAG;AAAA,EACzB;AAAA,EAEA,IAAI,KAAa,QAAsB;AACrC,SAAK,QAAQ,GAAG,IAAI;AAAA,EACtB;AAAA,EAEA,MAAM,KAAoB;AACxB,QAAI,KAAK;AACP,aAAO,KAAK,QAAQ,GAAG;AAAA,IACzB,OAAO;AACL,WAAK,UAAU,CAAC;AAAA,IAClB;AAAA,EACF;AACF;;;ACiGO,SAAS,cACd,MACA,SAAoD,OAC1C;AACV,QAAM,UAAU,KAAK;AACrB,MAAI,UAAU;AACd,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,cACG,QAAQ,gBACR,QAAQ,WACT;AAAA,EACJ,WAAW,OAAO,YAAY,UAAU;AACtC,cAAU;AAAA,EACZ;AAGA,QAAM,UAAW,KAAK,WAAuB,CAAC;AAC9C,QAAM,kBAAmB,QAAQ,oBAAwC;AAGzE,MAAI,YAA2B;AAC/B,QAAM,OAAO,KAAK;AAClB,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,gBAAa,KAAK,SAAoB;AAAA,EACxC;AACA,MAAI,cAAc,MAAM;AACtB,gBAAa,KAAK,cAAyB;AAAA,EAC7C;AAEA,SAAO;AAAA,IACL,QAAS,KAAK,UAAqB;AAAA,IACnC,IAAK,KAAK,MAAiB;AAAA,IAC3B,UAAW,KAAK,YAAuB;AAAA,IACvC,KAAM,KAAK,OAAkB;AAAA,IAC7B;AAAA,IACA,aAAc,KAAK,gBAA2B;AAAA,IAC9C,WAAY,KAAK,cAAyB;AAAA,IAC1C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAa,KAAK,cAA0C,CAAC;AAAA,IAC7D,KAAK;AAAA,EACP;AACF;AAGO,SAAS,UAAU,MAAqC;AAC7D,QAAM,SAAU,KAAK,UAAqC,CAAC;AAC3D,QAAM,YAAY,KAAK;AAEvB,SAAO;AAAA,IACL,QAAS,KAAK,UAAqB;AAAA,IACnC,SAAU,KAAK,WAAuB;AAAA,IACtC,aAAc,KAAK,gBAA2B;AAAA,IAC9C,QAAS,KAAK,WAAsB;AAAA,IACpC,WAAY,KAAK,cAAyB;AAAA,IAC1C,WAAY,KAAK,cAAyB;AAAA,IAC1C,QAAQ;AAAA,MACN,KAAK,OAAO,OAAO;AAAA,MACnB,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,WAAW;AAAA,IAC7B;AAAA,IACA,gBAAiB,KAAK,mBAA8B;AAAA,IACpD,WAAY,KAAK,cAAyB;AAAA,IAC1C,UAAW,KAAK,aAAyB;AAAA,IACzC,oBACG,KAAK,wBACL,KAAK,cACN;AAAA,IACF,WAAW,aAAa;AAAA,IACxB,KAAK;AAAA,EACP;AACF;AAGO,SAAS,UAAU,MAAqC;AAC7D,QAAM,UAAW,KAAK,WAAyC,CAAC;AAChE,QAAM,YAAY,QAAQ,OAAO,CAAC,GAAG;AAAA,IAAI,CAAC,MACxC,cAAc,GAA8B,KAAK;AAAA,EACnD;AACA,QAAM,gBAAgB,QAAQ,WAAW,CAAC,GAAG;AAAA,IAAI,CAAC,MAChD,cAAc,GAA8B,SAAS;AAAA,EACvD;AACA,QAAM,gBAAgB,QAAQ,WAAW,CAAC,GAAG;AAAA,IAAI,CAAC,MAChD,cAAc,GAA8B,SAAS;AAAA,EACvD;AACA,QAAM,gBAAgB,QAAQ,WAAW,CAAC,GAAG;AAAA,IAAI,CAAC,MAChD,cAAc,GAA8B,SAAS;AAAA,EACvD;AAEA,SAAO;AAAA,IACL,QAAS,KAAK,UAAqB;AAAA,IACnC,YAAa,KAAK,eAA0B;AAAA,IAC5C,UAAW,KAAK,aAAwB;AAAA,IACxC,aAAc,KAAK,gBAA2B;AAAA,IAC9C,OAAO,CAAC,GAAG,UAAU,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY;AAAA,IACtE,KAAK;AAAA,IACL,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,WAAY,KAAK,mBAA8B;AAAA,IAC/C,KAAK;AAAA,EACP;AACF;AAGO,SAAS,gBAAgB,MAA2C;AACzE,SAAO;AAAA,IACL,UAAW,KAAK,aAAwB;AAAA,IACxC,MAAO,KAAK,QAAmB;AAAA,IAC/B,MAAO,KAAK,QAAqB,CAAC;AAAA,IAClC,aAAc,KAAK,eAA0B;AAAA,IAC7C,UAAW,KAAK,YAAuB;AAAA,IACvC,SAAU,KAAK,WAAuB;AAAA,IACtC,QAAS,KAAK,UAAqB;AAAA,IACnC,SAAU,KAAK,YAAuB;AAAA,IACtC,WAAY,KAAK,cAAyB;AAAA,EAC5C;AACF;AAGO,SAAS,iBAAiB,MAA4C;AAC3E,SAAO;AAAA,IACL,IAAK,KAAK,MAAkB;AAAA,IAC5B,SAAU,KAAK,WAAsB;AAAA,IACrC,MAAO,KAAK,QAAmB;AAAA,IAC/B,gBAAiB,KAAK,mBAA8B;AAAA,IACpD,WAAY,KAAK,cAAyB;AAAA,IAC1C,eAAgB,KAAK,kBAA6B;AAAA,EACpD;AACF;;;ACpSA,IAAM,UAAU;AAChB,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AA2CjB,IAAM,YAAN,MAAgB;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EAED;AAAA,EACA,kBAAmD;AAAA,EAE3D,YAAY,UAA4B,CAAC,GAAG;AAC1C,SAAK,WAAW,QAAQ,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACvE,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ,WAAW;AAGlC,QAAI,QAAQ,eAAe,QAAQ,QAAQ,eAAe,UAAU;AAClE,WAAK,UAAU,IAAI,kBAAkB;AAAA,IACvC,OAAO;AACL,UAAI;AACF,aAAK,UAAU,IAAI,YAAY,QAAQ,cAAc,MAAS;AAAA,MAChE,QAAQ;AAEN,aAAK,UAAU,IAAI,kBAAkB;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,KAAK,UAAuB,CAAC,GAAwB;AACzD,UAAM,EAAE,MAAM,SAAS,UAAU,CAAC,OAAO,WAAW,SAAS,EAAE,IAAI;AAEnE,WAAO,KAAK,SAAS;AAAA,MACnB,SAAS,GAAG,KAAK,OAAO;AAAA,MACxB,WAAW,GAAG,KAAK,OAAO;AAAA,MAC1B,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,WACJ,UACA,UAAwC,CAAC,GACpB;AACrB,UAAM,EAAE,MAAM,UAAU,CAAC,OAAO,WAAW,SAAS,EAAE,IAAI;AAE1D,WAAO,KAAK,SAAS;AAAA,MACnB,SAAS,GAAG,KAAK,OAAO,SAAS,QAAQ;AAAA,MACzC,WAAW,GAAG,KAAK,OAAO,SAAS,QAAQ;AAAA,MAC3C,WAAW,UAAU,QAAQ;AAAA,MAC7B;AAAA,MACA,SAAS;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAK,KAA6B;AACtC,UAAM,OAAO,MAAM,KAAK,UAAU,OAAO,GAAG,KAAK,OAAO,iBAAiB;AACzE,WAAO,UAAU,IAAI;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,KAA6B;AAC3C,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB,OAAO,GAAG,KAAK,OAAO;AAAA,IACxB;AACA,WAAO,UAAU,IAAI;AAAA,EACvB;AAAA;AAAA,EAGA,MAAM,UAAiC;AACrC,UAAM,OAAO,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,oBAAoB;AACrE,UAAM,MAAO,KAAK,WAAW,CAAC;AAC9B,WAAO,IAAI,IAAI,eAAe;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAoC;AACxC,UAAM,OAAO,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,eAAe;AAChE,WAAO,iBAAiB,IAAI;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,gBAAgB,cAA2C;AAC/D,UAAM,OAAO,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,mBAAmB;AAGpE,UAAM,UAAW,KAAK,gBAAgB,KAAK,kBAAkB,CAAC;AAI9D,UAAM,YAAY,oBAAI,IAAY;AAClC,eAAW,OAAO,cAAc;AAC9B,YAAM,QAAQ,QAAQ,IAAI,YAAY,CAAC;AACvC,UAAI,CAAC,MAAO;AACZ,YAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACrD,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,mBAAW,KAAK,SAAS;AACvB,oBAAU,IAAI,CAAC;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AACA,WAAO,CAAC,GAAG,SAAS;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BA,MAAM,MACJ,UACA,UAAwB,CAAC,GACV;AACf,UAAM,EAAE,MAAM,SAAS,SAAS,OAAO,IAAI;AAC3C,QAAI,WAAW,QAAQ;AAGvB,QAAI,CAAC,UAAU;AACb,UAAI;AACF,cAAM,IAAI,MAAM,KAAK,KAAK;AAC1B,mBAAW,KAAK,IAAI,EAAE,QAAQ,EAAE;AAAA,MAClC,QAAQ;AACN,mBAAW;AAAA,MACb;AAAA,IACF;AAEA,YAAQ,IAAI,0CAA0C,QAAQ,MAAM;AAEpE,WAAO,CAAC,QAAQ,SAAS;AACvB,UAAI;AACF,cAAM,QAAQ,MAAM,KAAK,KAAK,EAAE,MAAM,SAAS,QAAQ,CAAC;AACxD,YAAI,MAAM,SAAS,GAAG;AACpB,kBAAQ,IAAI,eAAe,MAAM,MAAM,qBAAqB;AAC5D,qBAAW,QAAQ,OAAO;AACxB,kBAAM,SAAS,IAAI;AAAA,UACrB;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,QAAQ,QAAS;AACrB,gBAAQ,MAAM,sBAAsB,GAAG,iBAAiB,QAAQ,MAAM;AAAA,MACxE;AAGA,YAAM,IAAI,QAAc,CAAC,YAAY;AACnC,cAAM,QAAQ,WAAW,SAAS,WAAY,GAAI;AAClD,gBAAQ;AAAA,UACN;AAAA,UACA,MAAM;AACJ,yBAAa,KAAK;AAClB,oBAAQ;AAAA,UACV;AAAA,UACA,EAAE,MAAM,KAAK;AAAA,QACf;AAAA,MACF,CAAC;AAAA,IACH;AAEA,YAAQ,IAAI,sBAAsB;AAAA,EACpC;AAAA;AAAA;AAAA,EAKA,aAAa,UAAyB;AACpC,QAAI,UAAU;AACZ,WAAK,QAAQ,MAAM,UAAU,QAAQ,EAAE;AAAA,IACzC,OAAO;AACL,WAAK,QAAQ,MAAM;AAAA,IACrB;AAAA,EACF;AAAA;AAAA,EAIA,MAAc,SAAS,QAOC;AACtB,UAAM,EAAE,SAAS,WAAW,WAAW,MAAM,SAAS,QAAQ,IAAI;AAGlE,UAAM,OAAO,MAAM,KAAK,KAAK,OAAO;AAGpC,UAAM,eAAe,KAAK,QAAQ,IAAI,SAAS;AAC/C,QAAI,gBAAgB,iBAAiB,KAAK,QAAQ;AAChD,aAAO,CAAC;AAAA,IACV;AAGA,UAAM,OAAO,MAAM,KAAK,UAAU,SAAS;AAG3C,QAAI,KAAK,QAAQ;AACf,WAAK,QAAQ,IAAI,WAAW,KAAK,MAAM;AAAA,IACzC;AAGA,QAAI,QAAQ,KAAK;AAGjB,YAAQ,MAAM,OAAO,CAAC,MAAM,QAAQ,SAAS,EAAE,MAAM,CAAC;AAGtD,QAAI,SAAS,QAAQ;AACnB,cAAQ,MAAM,OAAO,CAAC,MAAM,QAAQ,SAAS,EAAE,MAAM,CAAC;AAAA,IACxD;AAGA,QAAI,MAAM,QAAQ;AAChB,YAAM,SAAS,MAAM,KAAK,cAAc;AACxC,cAAQ,MAAM,OAAO,CAAC,MAAM;AAC1B,cAAM,WAAW,OAAO,EAAE,MAAM,KAAK,CAAC;AACtC,eAAO,KAAK,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC,CAAC;AAAA,MAC9C,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,gBAAmD;AAC/D,QAAI,KAAK,gBAAiB,QAAO,KAAK;AACtC,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,QAAQ;AACtC,WAAK,kBAAkB,OAAO;AAAA,QAC5B,WAAW,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF,QAAQ;AACN,WAAK,kBAAkB,CAAC;AAAA,IAC1B;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,UAAU,KAA+C;AACrE,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAE/D,QAAI;AACF,YAAM,UAAkC;AAAA,QACtC,cAAc,gBAAgB,OAAO;AAAA,QACrC,QAAQ;AAAA,MACV;AACA,UAAI,KAAK,QAAQ;AACf,gBAAQ,iBAAiB,IAAI,KAAK;AAAA,MACpC;AAEA,YAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,QAAQ,WAAW,OAAO,CAAC;AACnE,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,KAAK,IAAI,UAAU,KAAK,GAAG,GAAG;AAAA,MAClE;AACA,aAAQ,MAAM,IAAI,KAAK;AAAA,IACzB,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,WAAmB;AACjB,UAAM,OAAO,KAAK,SAAS,QAAQ;AACnC,WAAO,qBAAqB,KAAK,OAAO,UAAU,IAAI;AAAA,EACxD;AACF;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -79,12 +79,15 @@ function parseFeedItem(data, bucket = "new") {
|
|
|
79
79
|
} else if (typeof content === "string") {
|
|
80
80
|
excerpt = content;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const signals = data.signals || {};
|
|
83
|
+
const suggestedAction = signals.suggested_action || null;
|
|
84
|
+
let riskScore = null;
|
|
85
|
+
const risk = data.risk;
|
|
86
|
+
if (typeof risk === "object" && risk !== null) {
|
|
87
|
+
riskScore = risk.score ?? null;
|
|
88
|
+
}
|
|
83
89
|
if (riskScore === null) {
|
|
84
|
-
|
|
85
|
-
if (typeof summary === "object" && summary !== null) {
|
|
86
|
-
riskScore = summary.risk_score ?? null;
|
|
87
|
-
}
|
|
90
|
+
riskScore = data.risk_score ?? null;
|
|
88
91
|
}
|
|
89
92
|
return {
|
|
90
93
|
source: data.source || "",
|
|
@@ -95,18 +98,35 @@ function parseFeedItem(data, bucket = "new") {
|
|
|
95
98
|
publishedAt: data.published_at || null,
|
|
96
99
|
updatedAt: data.updated_at || null,
|
|
97
100
|
bucket,
|
|
101
|
+
signals,
|
|
102
|
+
suggestedAction,
|
|
98
103
|
riskScore,
|
|
99
104
|
provenance: data.provenance || {},
|
|
100
105
|
raw: data
|
|
101
106
|
};
|
|
102
107
|
}
|
|
103
108
|
function parseHead(data) {
|
|
109
|
+
const counts = data.counts || {};
|
|
110
|
+
const freshness = data.freshness;
|
|
104
111
|
return {
|
|
105
112
|
cursor: data.cursor || "",
|
|
106
|
-
hash: data.hash || "",
|
|
107
113
|
changed: data.changed || false,
|
|
108
114
|
generatedAt: data.generated_at || "",
|
|
109
|
-
ttlSec: data.ttl_sec ||
|
|
115
|
+
ttlSec: data.ttl_sec || 60,
|
|
116
|
+
latestUrl: data.latest_url || "",
|
|
117
|
+
digestUrl: data.digest_url || null,
|
|
118
|
+
counts: {
|
|
119
|
+
new: counts.new || 0,
|
|
120
|
+
updated: counts.updated || 0,
|
|
121
|
+
removed: counts.removed || 0,
|
|
122
|
+
flagged: counts.flagged || 0
|
|
123
|
+
},
|
|
124
|
+
sourcesChecked: data.sources_checked || 0,
|
|
125
|
+
sourcesOk: data.sources_ok || 0,
|
|
126
|
+
allClear: data.all_clear || false,
|
|
127
|
+
allClearConfidence: data.all_clear_confidence ?? data.confidence ?? null,
|
|
128
|
+
freshness: freshness || null,
|
|
129
|
+
raw: data
|
|
110
130
|
};
|
|
111
131
|
}
|
|
112
132
|
function parseFeed(data) {
|
|
@@ -120,15 +140,19 @@ function parseFeed(data) {
|
|
|
120
140
|
const removedItems = (buckets.removed || []).map(
|
|
121
141
|
(i) => parseFeedItem(i, "removed")
|
|
122
142
|
);
|
|
143
|
+
const flaggedItems = (buckets.flagged || []).map(
|
|
144
|
+
(i) => parseFeedItem(i, "flagged")
|
|
145
|
+
);
|
|
123
146
|
return {
|
|
124
147
|
cursor: data.cursor || "",
|
|
125
148
|
prevCursor: data.prev_cursor || "",
|
|
126
149
|
sourceId: data.source_id || "",
|
|
127
150
|
generatedAt: data.generated_at || "",
|
|
128
|
-
items: [...newItems, ...updatedItems, ...removedItems],
|
|
151
|
+
items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],
|
|
129
152
|
new: newItems,
|
|
130
153
|
updated: updatedItems,
|
|
131
154
|
removed: removedItems,
|
|
155
|
+
flagged: flaggedItems,
|
|
132
156
|
narrative: data.batch_narrative || "",
|
|
133
157
|
raw: data
|
|
134
158
|
};
|
|
@@ -146,9 +170,19 @@ function parseSourceInfo(data) {
|
|
|
146
170
|
latestUrl: data.latest_url || ""
|
|
147
171
|
};
|
|
148
172
|
}
|
|
173
|
+
function parseHealthCheck(data) {
|
|
174
|
+
return {
|
|
175
|
+
ok: data.ok || false,
|
|
176
|
+
service: data.service || "",
|
|
177
|
+
time: data.time || "",
|
|
178
|
+
sourcesChecked: data.sources_checked || 0,
|
|
179
|
+
sourcesOk: data.sources_ok || 0,
|
|
180
|
+
engineVersion: data.engine_version || ""
|
|
181
|
+
};
|
|
182
|
+
}
|
|
149
183
|
|
|
150
184
|
// src/client.ts
|
|
151
|
-
var VERSION = "0.1.
|
|
185
|
+
var VERSION = "0.1.1";
|
|
152
186
|
var DEFAULT_BASE_URL = "https://diffdelta.io";
|
|
153
187
|
var DEFAULT_TIMEOUT = 15e3;
|
|
154
188
|
var DiffDelta = class {
|
|
@@ -175,11 +209,11 @@ var DiffDelta = class {
|
|
|
175
209
|
/**
|
|
176
210
|
* Poll the global feed for new items since last poll.
|
|
177
211
|
*
|
|
178
|
-
* Checks head.json first (~
|
|
212
|
+
* Checks head.json first (~200 bytes). Only fetches the full feed
|
|
179
213
|
* if the cursor has changed. Automatically saves the new cursor.
|
|
180
214
|
*/
|
|
181
215
|
async poll(options = {}) {
|
|
182
|
-
const { tags, sources, buckets = ["new", "updated"] } = options;
|
|
216
|
+
const { tags, sources, buckets = ["new", "updated", "flagged"] } = options;
|
|
183
217
|
return this.pollFeed({
|
|
184
218
|
headUrl: `${this.baseUrl}/diff/head.json`,
|
|
185
219
|
latestUrl: `${this.baseUrl}/diff/latest.json`,
|
|
@@ -196,10 +230,10 @@ var DiffDelta = class {
|
|
|
196
230
|
* care about one source — fetches a smaller payload.
|
|
197
231
|
*/
|
|
198
232
|
async pollSource(sourceId, options = {}) {
|
|
199
|
-
const { tags, buckets = ["new", "updated"] } = options;
|
|
233
|
+
const { tags, buckets = ["new", "updated", "flagged"] } = options;
|
|
200
234
|
return this.pollFeed({
|
|
201
|
-
headUrl: `${this.baseUrl}/diff
|
|
202
|
-
latestUrl: `${this.baseUrl}/diff
|
|
235
|
+
headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,
|
|
236
|
+
latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,
|
|
203
237
|
cursorKey: `source:${sourceId}`,
|
|
204
238
|
tags,
|
|
205
239
|
sources: void 0,
|
|
@@ -208,7 +242,7 @@ var DiffDelta = class {
|
|
|
208
242
|
}
|
|
209
243
|
// ── Low-level fetch ──
|
|
210
244
|
/**
|
|
211
|
-
* Fetch a head.json pointer.
|
|
245
|
+
* Fetch a head.json pointer. Cheapest call (~200 bytes).
|
|
212
246
|
* @param url Full URL to head.json. Defaults to global head.
|
|
213
247
|
*/
|
|
214
248
|
async head(url) {
|
|
@@ -231,6 +265,42 @@ var DiffDelta = class {
|
|
|
231
265
|
const raw = data.sources || [];
|
|
232
266
|
return raw.map(parseSourceInfo);
|
|
233
267
|
}
|
|
268
|
+
// ── Discovery & Health ──
|
|
269
|
+
/**
|
|
270
|
+
* Check pipeline health. Returns when the engine last ran and whether
|
|
271
|
+
* all sources are healthy. A stale timestamp means the pipeline is down.
|
|
272
|
+
*/
|
|
273
|
+
async checkHealth() {
|
|
274
|
+
const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);
|
|
275
|
+
return parseHealthCheck(data);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Given a list of dependency names your bot uses, returns the source IDs
|
|
279
|
+
* you should monitor. Uses the static stacks.json mapping — no API call,
|
|
280
|
+
* pure local lookup after one fetch.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* const sources = await dd.discoverSources(["openai", "langchain", "pinecone"]);
|
|
285
|
+
* // → ["openai_sdk_releases", "openai_api_changelog", "langchain_releases", "pinecone_status"]
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
async discoverSources(dependencies) {
|
|
289
|
+
const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);
|
|
290
|
+
const depsObj = data.dependencies || data.dependency_map || {};
|
|
291
|
+
const sourceIds = /* @__PURE__ */ new Set();
|
|
292
|
+
for (const dep of dependencies) {
|
|
293
|
+
const entry = depsObj[dep.toLowerCase()];
|
|
294
|
+
if (!entry) continue;
|
|
295
|
+
const sources = Array.isArray(entry) ? entry : entry.sources;
|
|
296
|
+
if (Array.isArray(sources)) {
|
|
297
|
+
for (const s of sources) {
|
|
298
|
+
sourceIds.add(s);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return [...sourceIds];
|
|
303
|
+
}
|
|
234
304
|
// ── Continuous monitoring ──
|
|
235
305
|
/**
|
|
236
306
|
* Continuously poll and call a function for each new item.
|
|
@@ -242,6 +312,9 @@ var DiffDelta = class {
|
|
|
242
312
|
* ```ts
|
|
243
313
|
* dd.watch(item => {
|
|
244
314
|
* console.log(`🚨 ${item.source}: ${item.headline}`);
|
|
315
|
+
* if (item.suggestedAction === "PATCH_IMMEDIATELY") {
|
|
316
|
+
* triggerAlert(item);
|
|
317
|
+
* }
|
|
245
318
|
* }, { tags: ["security"] });
|
|
246
319
|
* ```
|
|
247
320
|
*
|
|
@@ -261,7 +334,7 @@ var DiffDelta = class {
|
|
|
261
334
|
const h = await this.head();
|
|
262
335
|
interval = Math.max(h.ttlSec, 60);
|
|
263
336
|
} catch {
|
|
264
|
-
interval =
|
|
337
|
+
interval = 60;
|
|
265
338
|
}
|
|
266
339
|
}
|
|
267
340
|
console.log(`[diffdelta] Watching for changes every ${interval}s...`);
|
|
@@ -273,8 +346,6 @@ var DiffDelta = class {
|
|
|
273
346
|
for (const item of items) {
|
|
274
347
|
await callback(item);
|
|
275
348
|
}
|
|
276
|
-
} else {
|
|
277
|
-
console.log(`[diffdelta] No changes.`);
|
|
278
349
|
}
|
|
279
350
|
} catch (err) {
|
|
280
351
|
if (signal?.aborted) break;
|
|
@@ -282,10 +353,14 @@ var DiffDelta = class {
|
|
|
282
353
|
}
|
|
283
354
|
await new Promise((resolve) => {
|
|
284
355
|
const timer = setTimeout(resolve, interval * 1e3);
|
|
285
|
-
signal?.addEventListener(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
356
|
+
signal?.addEventListener(
|
|
357
|
+
"abort",
|
|
358
|
+
() => {
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
resolve();
|
|
361
|
+
},
|
|
362
|
+
{ once: true }
|
|
363
|
+
);
|
|
289
364
|
});
|
|
290
365
|
}
|
|
291
366
|
console.log("[diffdelta] Stopped.");
|
|
@@ -365,6 +440,11 @@ var DiffDelta = class {
|
|
|
365
440
|
export {
|
|
366
441
|
CursorStore,
|
|
367
442
|
DiffDelta,
|
|
368
|
-
MemoryCursorStore
|
|
443
|
+
MemoryCursorStore,
|
|
444
|
+
parseFeed,
|
|
445
|
+
parseFeedItem,
|
|
446
|
+
parseHead,
|
|
447
|
+
parseHealthCheck,
|
|
448
|
+
parseSourceInfo
|
|
369
449
|
};
|
|
370
450
|
//# sourceMappingURL=index.mjs.map
|