@q32/signal-scanner 0.1.0 → 0.2.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/COPYING +674 -0
- package/COPYING.LESSER +165 -0
- package/README.md +57 -9
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +592 -0
- package/dist/cli.js.map +1 -0
- package/dist/dynamic.d.ts +43 -0
- package/dist/dynamic.d.ts.map +1 -0
- package/{src/dynamic.ts → dist/dynamic.js} +133 -156
- package/dist/dynamic.js.map +1 -0
- package/dist/feeds.d.ts +66 -0
- package/dist/feeds.d.ts.map +1 -0
- package/dist/feeds.js +259 -0
- package/dist/feeds.js.map +1 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1251 -0
- package/dist/index.js.map +1 -0
- package/dist/intel.d.ts +72 -0
- package/dist/intel.d.ts.map +1 -0
- package/dist/intel.js +480 -0
- package/dist/intel.js.map +1 -0
- package/dist/node-tls.d.ts +8 -0
- package/dist/node-tls.d.ts.map +1 -0
- package/dist/node-tls.js +48 -0
- package/dist/node-tls.js.map +1 -0
- package/dist/render-isolate/entry.d.ts +2 -0
- package/dist/render-isolate/entry.d.ts.map +1 -0
- package/dist/render-isolate/entry.js +3 -0
- package/dist/render-isolate/entry.js.map +1 -0
- package/dist/render-isolate/polyfills.d.ts +2 -0
- package/dist/render-isolate/polyfills.d.ts.map +1 -0
- package/dist/render-isolate/polyfills.js +41 -0
- package/dist/render-isolate/polyfills.js.map +1 -0
- package/dist/render-isolate/run.d.ts +3 -0
- package/dist/render-isolate/run.d.ts.map +1 -0
- package/dist/render-isolate/run.js +88 -0
- package/dist/render-isolate/run.js.map +1 -0
- package/dist/render.d.ts +26 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +248 -0
- package/dist/render.js.map +1 -0
- package/dist/rules/packs/binary.d.ts +4 -0
- package/dist/rules/packs/binary.d.ts.map +1 -0
- package/dist/rules/packs/binary.js +101 -0
- package/dist/rules/packs/binary.js.map +1 -0
- package/dist/rules/packs/css.d.ts +3 -0
- package/dist/rules/packs/css.d.ts.map +1 -0
- package/dist/rules/packs/css.js +43 -0
- package/dist/rules/packs/css.js.map +1 -0
- package/dist/rules/packs/decoders.d.ts +3 -0
- package/dist/rules/packs/decoders.d.ts.map +1 -0
- package/dist/rules/packs/decoders.js +46 -0
- package/dist/rules/packs/decoders.js.map +1 -0
- package/dist/rules/packs/html.d.ts +4 -0
- package/dist/rules/packs/html.d.ts.map +1 -0
- package/dist/rules/packs/html.js +227 -0
- package/dist/rules/packs/html.js.map +1 -0
- package/dist/rules/packs/index.d.ts +24 -0
- package/dist/rules/packs/index.d.ts.map +1 -0
- package/dist/rules/packs/index.js +75 -0
- package/dist/rules/packs/index.js.map +1 -0
- package/dist/rules/packs/script-risk.d.ts +4 -0
- package/dist/rules/packs/script-risk.d.ts.map +1 -0
- package/dist/rules/packs/script-risk.js +231 -0
- package/dist/rules/packs/script-risk.js.map +1 -0
- package/dist/rules/packs/source-code.d.ts +3 -0
- package/dist/rules/packs/source-code.d.ts.map +1 -0
- package/dist/rules/packs/source-code.js +179 -0
- package/dist/rules/packs/source-code.js.map +1 -0
- package/dist/rules/packs/urls.d.ts +3 -0
- package/dist/rules/packs/urls.d.ts.map +1 -0
- package/dist/rules/packs/urls.js +123 -0
- package/dist/rules/packs/urls.js.map +1 -0
- package/dist/rules/types.d.ts +34 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/package.json +33 -18
- package/scripts/check-coverage.ts +0 -33
- package/scripts/eval.ts +0 -311
- package/scripts/render-isolate/entry.ts +0 -2
- package/scripts/render-isolate/polyfills.ts +0 -33
- package/scripts/render-isolate/run.ts +0 -63
- package/scripts/scan.ts +0 -612
- package/src/feeds.ts +0 -334
- package/src/index.ts +0 -1366
- package/src/intel.ts +0 -561
- package/src/node-tls.ts +0 -55
- package/src/render.ts +0 -233
- package/src/rules/packs/binary.ts +0 -103
- package/src/rules/packs/css.ts +0 -44
- package/src/rules/packs/decoders.ts +0 -47
- package/src/rules/packs/html.ts +0 -255
- package/src/rules/packs/index.ts +0 -76
- package/src/rules/packs/script-risk.ts +0 -236
- package/src/rules/packs/source-code.ts +0 -180
- package/src/rules/packs/urls.ts +0 -138
- package/src/rules/types.ts +0 -56
package/src/feeds.ts
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
// Cached blocklist-feed index for the signal scanner.
|
|
2
|
-
//
|
|
3
|
-
// Runtime-agnostic: all persistence goes through an injected `IntelStorage`
|
|
4
|
-
// (R2 in a Worker, the filesystem in a CLI, an in-memory map in tests). The
|
|
5
|
-
// scanner never knows where bytes live.
|
|
6
|
-
//
|
|
7
|
-
// Feeds can be millions of entries, far too large to hold in a Worker, so the
|
|
8
|
-
// index is sharded by a stable host bucket (`shardOf`) into small files. The
|
|
9
|
-
// match path fetches only the few shards a scan actually needs.
|
|
10
|
-
//
|
|
11
|
-
// Evidence strength is a numeric score (the lib's native currency), decided at
|
|
12
|
-
// ingest and encoded by which score-band shard family a host lands in — so
|
|
13
|
-
// shard files stay compact host arrays. Recent/active/evidence-backed entries
|
|
14
|
-
// get a high score; aged/weak entries a lower one. Matching returns the highest
|
|
15
|
-
// band a host appears in; the caller turns that score into a finding severity
|
|
16
|
-
// via the usual scoring helpers.
|
|
17
|
-
|
|
18
|
-
export interface IntelStorage {
|
|
19
|
-
get(key: string): Promise<Uint8Array | null>;
|
|
20
|
-
put(key: string, value: Uint8Array): Promise<void>;
|
|
21
|
-
list(prefix: string): Promise<string[]>;
|
|
22
|
-
delete?(key: string): Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface FeedEntry {
|
|
26
|
-
host: string;
|
|
27
|
-
score: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface FeedMeta {
|
|
31
|
-
source?: string;
|
|
32
|
-
generatedAt?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface FeedRecord extends FeedMeta {
|
|
36
|
-
version: string;
|
|
37
|
-
/** Distinct score bands present in this version, and the host count in each. */
|
|
38
|
-
bands: Record<string, number>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface GlobalFeedManifest {
|
|
42
|
-
feeds: Record<string, FeedRecord>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface CachedFeedMatch {
|
|
46
|
-
feedId: string;
|
|
47
|
-
host: string;
|
|
48
|
-
score: number;
|
|
49
|
-
source?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Default score bands. Callers may use any integer band; these are conventions. */
|
|
53
|
-
export const FEED_SCORE_ACTIVE = 90; // live / recent / evidence-backed
|
|
54
|
-
export const FEED_SCORE_AGED = 55; // historical / weak / unverified
|
|
55
|
-
|
|
56
|
-
const ROOT = "feeds";
|
|
57
|
-
const GLOBAL_MANIFEST_KEY = `${ROOT}/manifest.json`;
|
|
58
|
-
|
|
59
|
-
const encoder = new TextEncoder();
|
|
60
|
-
const decoder = new TextDecoder();
|
|
61
|
-
|
|
62
|
-
/** All 256 shard prefixes ("00".."ff"); a staged build finalizes one per job. */
|
|
63
|
-
export const SHARD_PREFIXES: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
64
|
-
|
|
65
|
-
/** Stable, synchronous host -> shard bucket. FNV-1a low byte; loader and matcher must agree. */
|
|
66
|
-
export function shardOf(host: string): string {
|
|
67
|
-
let hash = 0x811c9dc5;
|
|
68
|
-
const lower = host.toLowerCase();
|
|
69
|
-
for (let i = 0; i < lower.length; i++) {
|
|
70
|
-
hash ^= lower.charCodeAt(i);
|
|
71
|
-
hash = Math.imul(hash, 0x01000193);
|
|
72
|
-
}
|
|
73
|
-
return ((hash >>> 0) & 0xff).toString(16).padStart(2, "0");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Score a feed entry from its evidence: active/recent => high, else aged/weak. */
|
|
77
|
-
export function scoreFor(input: {
|
|
78
|
-
active?: boolean;
|
|
79
|
-
addedAt?: string | null;
|
|
80
|
-
recencyDays?: number;
|
|
81
|
-
now?: number;
|
|
82
|
-
activeScore?: number;
|
|
83
|
-
agedScore?: number;
|
|
84
|
-
}): number {
|
|
85
|
-
const recencyDays = input.recencyDays ?? 90;
|
|
86
|
-
const now = input.now ?? Date.parse(new Date().toISOString());
|
|
87
|
-
const recent = input.addedAt ? now - Date.parse(input.addedAt) <= recencyDays * 86_400_000 : true;
|
|
88
|
-
return input.active !== false && recent ? input.activeScore ?? FEED_SCORE_ACTIVE : input.agedScore ?? FEED_SCORE_AGED;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- Small feeds: full rebuild in one pass --------------------------------
|
|
92
|
-
|
|
93
|
-
/** Rebuild a feed's shard index from a complete entry set (feeds that fit in memory). */
|
|
94
|
-
export async function rebuildFeed(
|
|
95
|
-
storage: IntelStorage,
|
|
96
|
-
feedId: string,
|
|
97
|
-
version: string,
|
|
98
|
-
entries: FeedEntry[],
|
|
99
|
-
meta: FeedMeta = {}
|
|
100
|
-
): Promise<FeedRecord> {
|
|
101
|
-
const buckets = bucketEntries(dedupeEntries(entries));
|
|
102
|
-
const bands: Record<string, number> = {};
|
|
103
|
-
for (const [band, prefixes] of buckets) {
|
|
104
|
-
for (const [prefix, hosts] of prefixes) {
|
|
105
|
-
await putJson(storage, shardKey(feedId, version, band, prefix), [...hosts]);
|
|
106
|
-
bands[band] = (bands[band] ?? 0) + hosts.size;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return finalizeFeed(storage, feedId, version, bands, meta);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---- Huge feeds: staged chunk + per-shard merge ---------------------------
|
|
113
|
-
|
|
114
|
-
/** Write one download chunk's parsed entries to staging. Safe to run many in parallel. */
|
|
115
|
-
export async function writeFeedChunk(
|
|
116
|
-
storage: IntelStorage,
|
|
117
|
-
feedId: string,
|
|
118
|
-
version: string,
|
|
119
|
-
chunkId: string,
|
|
120
|
-
entries: FeedEntry[]
|
|
121
|
-
): Promise<void> {
|
|
122
|
-
const buckets = bucketEntries(entries);
|
|
123
|
-
for (const [band, prefixes] of buckets) {
|
|
124
|
-
for (const [prefix, hosts] of prefixes) {
|
|
125
|
-
await putJson(storage, stagingKey(feedId, version, chunkId, band, prefix), [...hosts]);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Merge every chunk's partials for one (band, prefix) into the final shard. One job per shard. */
|
|
131
|
-
export async function finalizeFeedShard(
|
|
132
|
-
storage: IntelStorage,
|
|
133
|
-
feedId: string,
|
|
134
|
-
version: string,
|
|
135
|
-
band: number,
|
|
136
|
-
prefix: string
|
|
137
|
-
): Promise<number> {
|
|
138
|
-
// Staging is keyed band/prefix/chunk, so this lists only the chunks for this
|
|
139
|
-
// one shard rather than scanning the whole staging tree.
|
|
140
|
-
const shardStaging = `${ROOT}/${feedId}/${version}/staging/${band}/${prefix}/`;
|
|
141
|
-
const merged = new Set<string>();
|
|
142
|
-
for (const key of await storage.list(shardStaging)) {
|
|
143
|
-
for (const host of await readArray(storage, key)) merged.add(host);
|
|
144
|
-
}
|
|
145
|
-
if (merged.size) await putJson(storage, shardKey(feedId, version, String(band), prefix), [...merged]);
|
|
146
|
-
return merged.size;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Publish the feed: write its manifest, register it globally, and sweep staging + old versions. */
|
|
150
|
-
export async function finalizeFeed(
|
|
151
|
-
storage: IntelStorage,
|
|
152
|
-
feedId: string,
|
|
153
|
-
version: string,
|
|
154
|
-
bands: Record<string, number>,
|
|
155
|
-
meta: FeedMeta = {}
|
|
156
|
-
): Promise<FeedRecord> {
|
|
157
|
-
const record: FeedRecord = {
|
|
158
|
-
version,
|
|
159
|
-
bands,
|
|
160
|
-
source: meta.source,
|
|
161
|
-
generatedAt: meta.generatedAt ?? new Date().toISOString()
|
|
162
|
-
};
|
|
163
|
-
await putJson(storage, `${ROOT}/${feedId}/${version}/manifest.json`, record);
|
|
164
|
-
|
|
165
|
-
const manifest = (await readJson<GlobalFeedManifest>(storage, GLOBAL_MANIFEST_KEY)) ?? { feeds: {} };
|
|
166
|
-
const previous = manifest.feeds[feedId]?.version;
|
|
167
|
-
manifest.feeds[feedId] = record;
|
|
168
|
-
await putJson(storage, GLOBAL_MANIFEST_KEY, manifest);
|
|
169
|
-
|
|
170
|
-
await sweep(storage, `${ROOT}/${feedId}/${version}/staging/`);
|
|
171
|
-
if (previous && previous !== version) await sweep(storage, `${ROOT}/${feedId}/${previous}/`);
|
|
172
|
-
return record;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---- Match path -----------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
/** Match candidate hosts against all published feeds, returning the highest score band per hit. */
|
|
178
|
-
export async function matchCachedFeeds(storage: IntelStorage, hosts: string[]): Promise<CachedFeedMatch[]> {
|
|
179
|
-
const manifest = await readJson<GlobalFeedManifest>(storage, GLOBAL_MANIFEST_KEY);
|
|
180
|
-
if (!manifest) return [];
|
|
181
|
-
const uniqueHosts = [...new Set(hosts.map((host) => host.toLowerCase()).filter(Boolean))];
|
|
182
|
-
const shardCache = new Map<string, Set<string>>();
|
|
183
|
-
const matches: CachedFeedMatch[] = [];
|
|
184
|
-
|
|
185
|
-
for (const [feedId, record] of Object.entries(manifest.feeds)) {
|
|
186
|
-
const bands = Object.keys(record.bands)
|
|
187
|
-
.map(Number)
|
|
188
|
-
.sort((a, b) => b - a); // highest score first
|
|
189
|
-
for (const host of uniqueHosts) {
|
|
190
|
-
const prefix = shardOf(host);
|
|
191
|
-
for (const band of bands) {
|
|
192
|
-
const key = shardKey(feedId, record.version, String(band), prefix);
|
|
193
|
-
let set = shardCache.get(key);
|
|
194
|
-
if (!set) {
|
|
195
|
-
set = new Set(await readArray(storage, key));
|
|
196
|
-
shardCache.set(key, set);
|
|
197
|
-
}
|
|
198
|
-
if (set.has(host)) {
|
|
199
|
-
matches.push({ feedId, host, score: band, source: record.source });
|
|
200
|
-
break; // strongest band wins for this host
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return matches;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ---- Parsers (lib owns the format; the app handles byte/line chunking) -----
|
|
209
|
-
|
|
210
|
-
/** Extract a lowercase host from a URL or bare host line; null for comments/blanks. */
|
|
211
|
-
export function hostFromLine(line: string): string | null {
|
|
212
|
-
const trimmed = line.trim();
|
|
213
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return null;
|
|
214
|
-
const candidate = trimmed.includes("://") ? trimmed : `http://${trimmed.split(/\s+/)[0]}`;
|
|
215
|
-
try {
|
|
216
|
-
const host = new URL(candidate).hostname.toLowerCase();
|
|
217
|
-
// Real domains/IPv4 always contain a dot; reject single-label junk lines.
|
|
218
|
-
return host && host.includes(".") ? host : null;
|
|
219
|
-
} catch {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** OpenPhish community feed: one active phishing URL per line. */
|
|
225
|
-
export function parseOpenPhishFeed(text: string, score = FEED_SCORE_ACTIVE): FeedEntry[] {
|
|
226
|
-
return dedupeEntries(linesOf(text).map(hostFromLine).filter(isHost).map((host) => ({ host, score })));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** A bare domain/host blocklist (e.g. Phishing.Database lists) at a caller-chosen score. */
|
|
230
|
-
export function parseHostList(text: string, score: number): FeedEntry[] {
|
|
231
|
-
return dedupeEntries(linesOf(text).map(hostFromLine).filter(isHost).map((host) => ({ host, score })));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** URLhaus CSV (id,dateadded,url,url_status,...): online+recent scores high, else aged. */
|
|
235
|
-
export function parseUrlhausCsv(text: string, opts: { recencyDays?: number; now?: number } = {}): FeedEntry[] {
|
|
236
|
-
const entries: FeedEntry[] = [];
|
|
237
|
-
for (const line of linesOf(text)) {
|
|
238
|
-
if (!line || line.startsWith("#")) continue;
|
|
239
|
-
const cols = parseCsvRow(line);
|
|
240
|
-
if (cols.length < 4) continue;
|
|
241
|
-
const host = hostFromLine(cols[2]);
|
|
242
|
-
if (!host) continue;
|
|
243
|
-
entries.push({ host, score: scoreFor({ active: cols[3] === "online", addedAt: cols[1], recencyDays: opts.recencyDays, now: opts.now }) });
|
|
244
|
-
}
|
|
245
|
-
return dedupeEntries(entries);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ---- internals ------------------------------------------------------------
|
|
249
|
-
|
|
250
|
-
function shardKey(feedId: string, version: string, band: string, prefix: string): string {
|
|
251
|
-
return `${ROOT}/${feedId}/${version}/${band}/${prefix}.json`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function stagingKey(feedId: string, version: string, chunkId: string, band: string, prefix: string): string {
|
|
255
|
-
// band/prefix first so a single shard's chunks share a narrow list prefix.
|
|
256
|
-
return `${ROOT}/${feedId}/${version}/staging/${band}/${prefix}/${chunkId}.json`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// score band (string) -> shard prefix -> hosts
|
|
260
|
-
function bucketEntries(entries: FeedEntry[]): Map<string, Map<string, Set<string>>> {
|
|
261
|
-
const buckets = new Map<string, Map<string, Set<string>>>();
|
|
262
|
-
for (const entry of entries) {
|
|
263
|
-
const host = entry.host.toLowerCase();
|
|
264
|
-
if (!host) continue;
|
|
265
|
-
const band = String(entry.score);
|
|
266
|
-
let prefixes = buckets.get(band);
|
|
267
|
-
if (!prefixes) buckets.set(band, (prefixes = new Map()));
|
|
268
|
-
const prefix = shardOf(host);
|
|
269
|
-
let set = prefixes.get(prefix);
|
|
270
|
-
if (!set) prefixes.set(prefix, (set = new Set()));
|
|
271
|
-
set.add(host);
|
|
272
|
-
}
|
|
273
|
-
return buckets;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function dedupeEntries(entries: FeedEntry[]): FeedEntry[] {
|
|
277
|
-
// Keep the strongest score when a host appears more than once.
|
|
278
|
-
const scoreByHost = new Map<string, number>();
|
|
279
|
-
for (const { host, score } of entries) {
|
|
280
|
-
scoreByHost.set(host, Math.max(scoreByHost.get(host) ?? 0, score));
|
|
281
|
-
}
|
|
282
|
-
return [...scoreByHost].map(([host, score]) => ({ host, score }));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function isHost(value: string | null): value is string {
|
|
286
|
-
return typeof value === "string" && value.length > 0;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function linesOf(text: string): string[] {
|
|
290
|
-
return text.split(/\r?\n/);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function parseCsvRow(line: string): string[] {
|
|
294
|
-
const cols: string[] = [];
|
|
295
|
-
let current = "";
|
|
296
|
-
let inQuotes = false;
|
|
297
|
-
for (let i = 0; i < line.length; i++) {
|
|
298
|
-
const char = line[i];
|
|
299
|
-
if (char === '"') {
|
|
300
|
-
if (inQuotes && line[i + 1] === '"') { current += '"'; i++; } else inQuotes = !inQuotes;
|
|
301
|
-
} else if (char === "," && !inQuotes) {
|
|
302
|
-
cols.push(current);
|
|
303
|
-
current = "";
|
|
304
|
-
} else {
|
|
305
|
-
current += char;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
cols.push(current);
|
|
309
|
-
return cols;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function putJson(storage: IntelStorage, key: string, value: unknown): Promise<void> {
|
|
313
|
-
await storage.put(key, encoder.encode(JSON.stringify(value)));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function readJson<T>(storage: IntelStorage, key: string): Promise<T | null> {
|
|
317
|
-
const bytes = await storage.get(key);
|
|
318
|
-
if (!bytes) return null;
|
|
319
|
-
try {
|
|
320
|
-
return JSON.parse(decoder.decode(bytes)) as T;
|
|
321
|
-
} catch {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function readArray(storage: IntelStorage, key: string): Promise<string[]> {
|
|
327
|
-
const value = await readJson<string[]>(storage, key);
|
|
328
|
-
return Array.isArray(value) ? value : [];
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function sweep(storage: IntelStorage, prefix: string): Promise<void> {
|
|
332
|
-
if (!storage.delete) return;
|
|
333
|
-
for (const key of await storage.list(prefix)) await storage.delete(key);
|
|
334
|
-
}
|