@nifrajs/link-safety 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import type { LinkCorpus } from "./types.js";
2
+ export interface MemoryCorpusOptions {
3
+ readonly version?: string;
4
+ }
5
+ export declare function createMemoryCorpus(entries: readonly unknown[], options?: MemoryCorpusOptions): LinkCorpus;
6
+ export declare function parseCorpusJsonl(input: string, options?: MemoryCorpusOptions): LinkCorpus;
7
+ //# sourceMappingURL=corpus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../src/corpus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAMV,UAAU,EAEX,MAAM,YAAY,CAAC;AAuBpB,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,GAAE,mBAAwB,GAChC,UAAU,CAuCZ;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,UAAU,CAoBZ"}
package/dist/corpus.js ADDED
@@ -0,0 +1,167 @@
1
+ import { normalizeLink } from "./normalize.js";
2
+ const MAX_CORPUS_BYTES = 64 * 1024 * 1024;
3
+ const MAX_CORPUS_ENTRIES = 100_000;
4
+ const MAX_LINE_BYTES = 32 * 1024;
5
+ const MAX_ID_LENGTH = 128;
6
+ const MAX_INDICATOR_LENGTH = 4096;
7
+ const MAX_TEXT_LENGTH = 256;
8
+ const LABELS = new Set(["benign", "suspicious", "malicious"]);
9
+ const INDICATOR_TYPES = new Set([
10
+ "hostname",
11
+ "hostname_path_prefix",
12
+ "url_prefix",
13
+ ]);
14
+ export function createMemoryCorpus(entries, options = {}) {
15
+ if (!Array.isArray(entries) || entries.length > MAX_CORPUS_ENTRIES) {
16
+ throw new RangeError("corpus: too many entries");
17
+ }
18
+ const version = validateText(options.version ?? "memory", "version", 128);
19
+ const ids = new Set();
20
+ const indexed = [];
21
+ for (const [index, raw] of entries.entries()) {
22
+ const entry = validateEntry(raw, index);
23
+ if (ids.has(entry.id))
24
+ throw new TypeError("corpus: duplicate entry id");
25
+ ids.add(entry.id);
26
+ indexed.push(indexEntry(entry));
27
+ }
28
+ return Object.freeze({
29
+ version,
30
+ lookup: async (link) => {
31
+ const matches = indexed
32
+ .filter((item) => matchesLink(item, link))
33
+ .map((item) => Object.freeze({
34
+ entry: item.entry,
35
+ matchType: item.entry.indicatorType,
36
+ }));
37
+ return Object.freeze(matches);
38
+ },
39
+ examples: async (link, exampleOptions = {}) => {
40
+ const limit = validateLimit(exampleOptions.limit);
41
+ const hostMatches = indexed
42
+ .filter((item) => item.host === link.hostname)
43
+ .slice(0, limit)
44
+ .map((item) => item.entry);
45
+ return Object.freeze(hostMatches);
46
+ },
47
+ });
48
+ }
49
+ export function parseCorpusJsonl(input, options = {}) {
50
+ if (typeof input !== "string")
51
+ throw new TypeError("corpus: input is invalid");
52
+ if (new TextEncoder().encode(input).byteLength > MAX_CORPUS_BYTES) {
53
+ throw new RangeError("corpus: input is too large");
54
+ }
55
+ const entries = [];
56
+ for (const [index, line] of input.split(/\r?\n/u).entries()) {
57
+ if (line.trim() === "")
58
+ continue;
59
+ if (new TextEncoder().encode(line).byteLength > MAX_LINE_BYTES) {
60
+ throw new RangeError(`corpus: line ${index + 1} is too large`);
61
+ }
62
+ let value;
63
+ try {
64
+ value = JSON.parse(line);
65
+ }
66
+ catch {
67
+ throw new TypeError(`corpus: line ${index + 1} is not valid JSON`);
68
+ }
69
+ entries.push(value);
70
+ }
71
+ return createMemoryCorpus(entries, options);
72
+ }
73
+ function validateEntry(value, index) {
74
+ if (!isRecord(value))
75
+ throw new TypeError(`corpus: entry ${index + 1} is invalid`);
76
+ const id = validateText(value.id, "entry id", MAX_ID_LENGTH);
77
+ const indicatorType = value.indicatorType;
78
+ if (typeof indicatorType !== "string" ||
79
+ !INDICATOR_TYPES.has(indicatorType)) {
80
+ throw new TypeError(`corpus: entry ${index + 1} has an invalid indicator type`);
81
+ }
82
+ const indicator = validateText(value.indicator, "indicator", MAX_INDICATOR_LENGTH);
83
+ const label = value.label;
84
+ if (typeof label !== "string" || !LABELS.has(label)) {
85
+ throw new TypeError(`corpus: entry ${index + 1} has an invalid label`);
86
+ }
87
+ const category = optionalText(value.category, "category", MAX_TEXT_LENGTH);
88
+ const source = optionalText(value.source, "source", MAX_TEXT_LENGTH);
89
+ const updatedAt = optionalText(value.updatedAt, "updatedAt", 64);
90
+ const entry = {
91
+ id,
92
+ indicatorType: indicatorType,
93
+ indicator,
94
+ label: label,
95
+ ...(category === undefined ? {} : { category }),
96
+ ...(source === undefined ? {} : { source }),
97
+ ...(updatedAt === undefined ? {} : { updatedAt }),
98
+ };
99
+ return Object.freeze(entry);
100
+ }
101
+ function indexEntry(entry) {
102
+ if (entry.indicatorType === "hostname") {
103
+ return Object.freeze({ entry, host: normalizeHost(entry.indicator) });
104
+ }
105
+ if (entry.indicatorType === "hostname_path_prefix") {
106
+ const slash = entry.indicator.indexOf("/");
107
+ if (slash <= 0) {
108
+ throw new TypeError("corpus: hostname path indicator is invalid");
109
+ }
110
+ const host = normalizeHost(entry.indicator.slice(0, slash));
111
+ const pathPrefix = normalizePathPrefix(entry.indicator.slice(slash));
112
+ return Object.freeze({ entry, host, pathPrefix });
113
+ }
114
+ const normalized = normalizeLink(entry.indicator);
115
+ if (!normalized.ok)
116
+ throw new TypeError("corpus: URL indicator is invalid");
117
+ return Object.freeze({
118
+ entry,
119
+ host: normalized.link.hostname,
120
+ canonicalPrefix: normalized.link.canonical,
121
+ });
122
+ }
123
+ function matchesLink(item, link) {
124
+ if (item.host !== link.hostname)
125
+ return false;
126
+ if (item.pathPrefix !== undefined && !link.pathname.startsWith(item.pathPrefix)) {
127
+ return false;
128
+ }
129
+ return (item.canonicalPrefix === undefined ||
130
+ link.canonical.startsWith(item.canonicalPrefix));
131
+ }
132
+ function normalizeHost(value) {
133
+ const host = value.trim().toLowerCase().replace(/\.$/u, "");
134
+ if (host.length === 0 || host.includes("/") || host.includes("@")) {
135
+ throw new TypeError("corpus: hostname indicator is invalid");
136
+ }
137
+ return host;
138
+ }
139
+ function normalizePathPrefix(value) {
140
+ if (!value.startsWith("/") || value.length > 2048) {
141
+ throw new TypeError("corpus: path prefix is invalid");
142
+ }
143
+ return value;
144
+ }
145
+ function validateText(value, label, maxLength) {
146
+ if (typeof value !== "string" || value.trim() === "" || value.length > maxLength) {
147
+ throw new TypeError(`corpus: ${label} is invalid`);
148
+ }
149
+ return value;
150
+ }
151
+ function optionalText(value, label, maxLength) {
152
+ if (value === undefined)
153
+ return undefined;
154
+ return validateText(value, label, maxLength);
155
+ }
156
+ function validateLimit(value) {
157
+ if (value === undefined)
158
+ return 8;
159
+ if (!Number.isSafeInteger(value) || value < 0 || value > 32) {
160
+ throw new RangeError("corpus: example limit must be between 0 and 32");
161
+ }
162
+ return value;
163
+ }
164
+ function isRecord(value) {
165
+ return typeof value === "object" && value !== null && !Array.isArray(value);
166
+ }
167
+ //# sourceMappingURL=corpus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corpus.js","sourceRoot":"","sources":["../src/corpus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { createMemoryCorpus, parseCorpusJsonl } from "./corpus.js";
2
+ export { normalizeLink } from "./normalize.js";
3
+ export { createLinkSafetyScanner, scanLink } from "./scan.js";
4
+ export type { ClassifiedLinkVerdict, CorpusEntry, CorpusExamplesOptions, CorpusIndicatorType, CorpusLabel, CorpusMatch, LinkClassifier, LinkClassifierFailure, LinkClassifierInput, LinkClassifierResult, LinkClassifierSuccess, LinkCorpus, LinkSafetyAction, LinkSafetyErrorCode, LinkSafetyPolicy, LinkSafetyReason, LinkSafetyScanner, LinkSafetyVerdict, LinkScanEvidence, LinkScanFailure, LinkScanOptions, LinkScanResult, LinkScanSuccess, NormalizedLink, } from "./types.js";
5
+ export { LINK_SAFETY_ACTIONS, LINK_SAFETY_ERROR_CODES, LINK_SAFETY_REASONS, LINK_SAFETY_VERDICTS, } from "./types.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC9D,YAAY,EACV,qBAAqB,EACrB,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createMemoryCorpus, parseCorpusJsonl } from "./corpus.js";
2
+ export { normalizeLink } from "./normalize.js";
3
+ export { createLinkSafetyScanner, scanLink } from "./scan.js";
4
+ export { LINK_SAFETY_ACTIONS, LINK_SAFETY_ERROR_CODES, LINK_SAFETY_REASONS, LINK_SAFETY_VERDICTS, } from "./types.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AA2B9D,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { LinkSafetyErrorCode, NormalizedLink } from "./types.js";
2
+ export type NormalizeResult = {
3
+ readonly ok: true;
4
+ readonly link: NormalizedLink;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly error: LinkSafetyErrorCode;
8
+ };
9
+ export declare function normalizeLink(input: unknown): NormalizeResult;
10
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAoB,cAAc,EAAE,MAAM,YAAY,CAAC;AASxF,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAEhE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAgE7D"}
@@ -0,0 +1,108 @@
1
+ const MAX_URL_LENGTH = 4096;
2
+ const MAX_PATH_LENGTH = 2048;
3
+ const MAX_QUERY_LENGTH = 4096;
4
+ const IPV4 = /^(?:\d{1,3}\.){3}\d{1,3}$/;
5
+ const IPV6 = /^\[[0-9a-f:]+\]$/i;
6
+ export function normalizeLink(input) {
7
+ if (typeof input !== "string")
8
+ return { ok: false, error: "invalid_url" };
9
+ const value = input.trim();
10
+ if (value.length === 0 || value.length > MAX_URL_LENGTH) {
11
+ return { ok: false, error: "invalid_url" };
12
+ }
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(value);
16
+ }
17
+ catch {
18
+ return { ok: false, error: "invalid_url" };
19
+ }
20
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
21
+ return { ok: false, error: "invalid_url" };
22
+ }
23
+ if (parsed.username || parsed.password || parsed.hostname.length === 0) {
24
+ return { ok: false, error: "invalid_url" };
25
+ }
26
+ if (parsed.pathname.length > MAX_PATH_LENGTH ||
27
+ parsed.search.length > MAX_QUERY_LENGTH) {
28
+ return { ok: false, error: "invalid_url" };
29
+ }
30
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, "");
31
+ const pathname = parsed.pathname.length === 0 ? "/" : parsed.pathname;
32
+ const port = parsed.port === "" ? undefined : Number(parsed.port);
33
+ if (port !== undefined && (!Number.isSafeInteger(port) || port < 1 || port > 65535)) {
34
+ return { ok: false, error: "invalid_url" };
35
+ }
36
+ const flags = [];
37
+ if (hostname.includes("xn--"))
38
+ flags.push("punycode_hostname");
39
+ if (isIpLiteral(hostname))
40
+ flags.push("ip_literal");
41
+ if (isPrivateAddress(hostname))
42
+ flags.push("private_network_candidate");
43
+ if (port !== undefined && port !== (parsed.protocol === "https:" ? 443 : 80)) {
44
+ flags.push("non_default_port");
45
+ }
46
+ if (hostname.length > 100)
47
+ flags.push("long_hostname");
48
+ if (hostname.split(".").length > 5)
49
+ flags.push("many_subdomains");
50
+ if (/%(?:2f|5c)/i.test(pathname))
51
+ flags.push("encoded_path_separator");
52
+ if (pathname.length > 1024)
53
+ flags.push("long_path");
54
+ if (parsed.search.length > 0)
55
+ flags.push("query_present");
56
+ const canonical = `${parsed.protocol}//${hostname}${port === undefined ? "" : `:${port}`}${pathname}`;
57
+ return {
58
+ ok: true,
59
+ link: Object.freeze({
60
+ protocol: parsed.protocol === "https:" ? "https" : "http",
61
+ hostname,
62
+ ...(port === undefined ? {} : { port }),
63
+ pathname,
64
+ canonical,
65
+ hasQuery: parsed.search.length > 0,
66
+ queryParameterCount: countQueryParameters(parsed.search),
67
+ queryLength: parsed.search.length,
68
+ flags: Object.freeze(flags),
69
+ }),
70
+ };
71
+ }
72
+ function countQueryParameters(search) {
73
+ if (search.length === 0)
74
+ return 0;
75
+ try {
76
+ return [...new URLSearchParams(search)].length;
77
+ }
78
+ catch {
79
+ return 0;
80
+ }
81
+ }
82
+ function isIpLiteral(hostname) {
83
+ return IPV4.test(hostname) || IPV6.test(hostname);
84
+ }
85
+ function isPrivateAddress(hostname) {
86
+ const unwrapped = hostname.replace(/^\[/, "").replace(/\]$/u, "");
87
+ if (unwrapped === "localhost" ||
88
+ unwrapped === "::1" ||
89
+ unwrapped.startsWith("fc") ||
90
+ unwrapped.startsWith("fd") ||
91
+ unwrapped.startsWith("fe80:")) {
92
+ return true;
93
+ }
94
+ if (!IPV4.test(unwrapped))
95
+ return false;
96
+ const octets = unwrapped.split(".").map(Number);
97
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
98
+ return false;
99
+ }
100
+ const first = octets[0] ?? -1;
101
+ const second = octets[1] ?? -1;
102
+ return (first === 10 ||
103
+ first === 127 ||
104
+ (first === 169 && second === 254) ||
105
+ (first === 172 && second >= 16 && second <= 31) ||
106
+ (first === 192 && second === 168));
107
+ }
108
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"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"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { LinkScanOptions, LinkScanResult } from "./types.js";
2
+ export declare function createLinkSafetyScanner(defaults?: LinkScanOptions): {
3
+ scan(input: unknown, options?: LinkScanOptions): Promise<LinkScanResult>;
4
+ };
5
+ export declare function scanLink(input: unknown, options?: LinkScanOptions): Promise<LinkScanResult>;
6
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAWV,eAAe,EACf,cAAc,EAGf,MAAM,YAAY,CAAC;AAOpB,wBAAgB,uBAAuB,CAAC,QAAQ,GAAE,eAAoB,GAAG;IACvE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC1E,CAKA;AAED,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAuEzB"}
package/dist/scan.js ADDED
@@ -0,0 +1,299 @@
1
+ import { normalizeLink } from "./normalize.js";
2
+ import { LINK_SAFETY_ERROR_CODES, LINK_SAFETY_REASONS } from "./types.js";
3
+ const DEFAULT_MINIMUM_CONFIDENCE = 0.92;
4
+ const ERROR_CODE_SET = new Set(LINK_SAFETY_ERROR_CODES);
5
+ const REASON_SET = new Set(LINK_SAFETY_REASONS);
6
+ export function createLinkSafetyScanner(defaults = {}) {
7
+ return Object.freeze({
8
+ scan: (input, options = {}) => scanLink(input, mergeOptions(defaults, options)),
9
+ });
10
+ }
11
+ export async function scanLink(input, options = {}) {
12
+ const startedAt = Date.now();
13
+ const normalized = normalizeLink(input);
14
+ if (!normalized.ok)
15
+ return failure(normalized.error, startedAt);
16
+ const signal = options.signal ?? new AbortController().signal;
17
+ if (signal.aborted)
18
+ return failure("cancelled", startedAt, normalized.link);
19
+ const policyResult = validatePolicy(options.policy);
20
+ if (!policyResult.ok)
21
+ return failure("policy_invalid", startedAt, normalized.link);
22
+ const policy = policyResult.policy;
23
+ let matches = [];
24
+ let corpusVersion;
25
+ if (options.corpus !== undefined) {
26
+ if (!isValidCorpus(options.corpus)) {
27
+ return failure("corpus_invalid", startedAt, normalized.link);
28
+ }
29
+ corpusVersion = options.corpus.version;
30
+ try {
31
+ matches = await options.corpus.lookup(normalized.link);
32
+ }
33
+ catch {
34
+ return failure("corpus_unavailable", startedAt, normalized.link);
35
+ }
36
+ }
37
+ if (signal.aborted)
38
+ return failure("cancelled", startedAt, normalized.link);
39
+ const deterministic = deterministicClassification(normalized.link, matches);
40
+ const hardMalicious = matches.some((match) => match.entry.label === "malicious");
41
+ let semantic;
42
+ let examples = [];
43
+ if (options.classifier !== undefined && !hardMalicious) {
44
+ if (!isValidClassifier(options.classifier)) {
45
+ return failure("classifier_response_invalid", startedAt, normalized.link);
46
+ }
47
+ if (options.corpus?.examples !== undefined) {
48
+ try {
49
+ examples = await options.corpus.examples(normalized.link, { limit: 8 });
50
+ }
51
+ catch {
52
+ return failure("corpus_unavailable", startedAt, normalized.link);
53
+ }
54
+ }
55
+ try {
56
+ semantic = validateClassifierResult(await options.classifier.classify({
57
+ link: normalized.link,
58
+ matches,
59
+ examples,
60
+ signal,
61
+ }));
62
+ }
63
+ catch {
64
+ return failure(signal.aborted ? "cancelled" : "classifier_unavailable", startedAt, normalized.link);
65
+ }
66
+ if (!semantic.ok)
67
+ return failure(semantic.error, startedAt, normalized.link);
68
+ }
69
+ if (signal.aborted)
70
+ return failure("cancelled", startedAt, normalized.link);
71
+ return combineClassification(normalized.link, matches, semantic?.ok === true ? semantic : undefined, deterministic, policy, corpusVersion, options.classifier?.id, startedAt);
72
+ }
73
+ function combineClassification(link, matches, semantic, deterministic, policy, corpusVersion, classifierId, startedAt) {
74
+ const labels = new Set(matches.map((match) => match.entry.label));
75
+ const conflict = labels.size > 1;
76
+ const reasons = new Set(deterministic.reasons);
77
+ let verdict = deterministic.verdict;
78
+ let confidence = deterministic.confidence;
79
+ let riskScore = deterministic.riskScore;
80
+ let provider;
81
+ let model;
82
+ if (semantic !== undefined) {
83
+ provider = semantic.provider ?? classifierId;
84
+ model = semantic.model;
85
+ reasons.add("semantic_classifier");
86
+ for (const reason of semantic.reasons ?? [])
87
+ reasons.add(reason);
88
+ riskScore = Math.max(riskScore, semantic.riskScore);
89
+ confidence = semantic.confidence;
90
+ verdict = semantic.verdict;
91
+ if (deterministic.verdict === "malicious" || conflict) {
92
+ verdict = conflict ? "suspicious" : "malicious";
93
+ }
94
+ else if (deterministic.riskScore >= 0.65 && semantic.verdict === "benign") {
95
+ verdict = "suspicious";
96
+ }
97
+ }
98
+ if (conflict) {
99
+ reasons.add("corpus_conflict");
100
+ verdict = "suspicious";
101
+ confidence = Math.min(confidence, 0.5);
102
+ }
103
+ const policyDecision = recommendedAction(verdict, confidence, policy, matches, conflict);
104
+ const evidence = Object.freeze({
105
+ ...(corpusVersion === undefined ? {} : { corpusVersion }),
106
+ corpusMatchCount: matches.length,
107
+ ...(provider === undefined ? {} : { provider }),
108
+ ...(model === undefined ? {} : { model }),
109
+ deterministicReasons: Object.freeze([...deterministic.reasons]),
110
+ latencyMs: Math.max(0, Date.now() - startedAt),
111
+ });
112
+ return Object.freeze({
113
+ ok: true,
114
+ link,
115
+ verdict,
116
+ riskScore: clampUnit(riskScore),
117
+ confidence: clampUnit(confidence),
118
+ reasons: Object.freeze([...reasons]),
119
+ recommendedAction: policyDecision.action,
120
+ policyReason: policyDecision.reason,
121
+ evidence,
122
+ });
123
+ }
124
+ function deterministicClassification(link, matches) {
125
+ const reasons = new Set(link.flags);
126
+ const labels = new Set(matches.map((match) => match.entry.label));
127
+ if (labels.has("malicious"))
128
+ reasons.add("corpus_malicious_match");
129
+ if (labels.has("suspicious"))
130
+ reasons.add("corpus_suspicious_match");
131
+ if (labels.has("benign"))
132
+ reasons.add("corpus_benign_match");
133
+ if (labels.size > 1)
134
+ reasons.add("corpus_conflict");
135
+ if (labels.size > 1) {
136
+ return {
137
+ verdict: "suspicious",
138
+ riskScore: 0.75,
139
+ confidence: 0.5,
140
+ reasons: [...reasons],
141
+ };
142
+ }
143
+ if (labels.has("malicious")) {
144
+ return { verdict: "malicious", riskScore: 1, confidence: 1, reasons: [...reasons] };
145
+ }
146
+ if (labels.has("suspicious")) {
147
+ return {
148
+ verdict: "suspicious",
149
+ riskScore: 0.75,
150
+ confidence: 1,
151
+ reasons: [...reasons],
152
+ };
153
+ }
154
+ if (labels.has("benign") && link.flags.length === 0) {
155
+ return { verdict: "benign", riskScore: 0.05, confidence: 1, reasons: [...reasons] };
156
+ }
157
+ const riskScore = heuristicRisk(link);
158
+ return {
159
+ verdict: riskScore >= 0.65 ? "suspicious" : "unknown",
160
+ riskScore,
161
+ confidence: riskScore >= 0.65 ? 0.6 : 0,
162
+ reasons: [...reasons],
163
+ };
164
+ }
165
+ function heuristicRisk(link) {
166
+ const weights = {
167
+ punycode_hostname: 0.3,
168
+ ip_literal: 0.25,
169
+ private_network_candidate: 0.4,
170
+ non_default_port: 0.12,
171
+ long_hostname: 0.12,
172
+ many_subdomains: 0.12,
173
+ encoded_path_separator: 0.3,
174
+ long_path: 0.08,
175
+ };
176
+ return clampUnit(link.flags.reduce((total, flag) => total + (weights[flag] ?? 0), 0));
177
+ }
178
+ function recommendedAction(verdict, confidence, policy, matches, conflict) {
179
+ if (!policy.allowAct) {
180
+ return { action: "review", reason: "automatic_action_not_enabled" };
181
+ }
182
+ if (conflict)
183
+ return { action: "review", reason: "conflicting_signals" };
184
+ const knownMalicious = matches.some((match) => match.entry.label === "malicious");
185
+ if (verdict === "malicious" &&
186
+ (knownMalicious
187
+ ? policy.blockKnownMalicious
188
+ : confidence >= policy.minimumConfidence)) {
189
+ return { action: "block", reason: "passed" };
190
+ }
191
+ if (verdict === "benign" && confidence >= policy.minimumConfidence) {
192
+ return { action: "allow", reason: "passed" };
193
+ }
194
+ if (verdict === "unknown")
195
+ return { action: "review", reason: "no_reliable_signal" };
196
+ return { action: "review", reason: "confidence_below_threshold" };
197
+ }
198
+ function validatePolicy(policy) {
199
+ const value = policy ?? {};
200
+ const minimumConfidence = value.minimumConfidence ?? DEFAULT_MINIMUM_CONFIDENCE;
201
+ if (typeof minimumConfidence !== "number" ||
202
+ !Number.isFinite(minimumConfidence) ||
203
+ minimumConfidence < 0 ||
204
+ minimumConfidence > 1) {
205
+ return { ok: false };
206
+ }
207
+ return {
208
+ ok: true,
209
+ policy: {
210
+ allowAct: value.allowAct === true,
211
+ minimumConfidence,
212
+ blockKnownMalicious: value.blockKnownMalicious !== false,
213
+ },
214
+ };
215
+ }
216
+ function validateClassifierResult(result) {
217
+ if (!isRecord(result) || typeof result.ok !== "boolean") {
218
+ return { ok: false, error: "classifier_response_invalid" };
219
+ }
220
+ if (!result.ok) {
221
+ return {
222
+ ok: false,
223
+ error: isLinkSafetyErrorCode(result.error)
224
+ ? result.error
225
+ : "classifier_response_invalid",
226
+ };
227
+ }
228
+ if (result.verdict !== "benign" &&
229
+ result.verdict !== "suspicious" &&
230
+ result.verdict !== "malicious") {
231
+ return { ok: false, error: "classifier_response_invalid" };
232
+ }
233
+ if (!isUnit(result.riskScore) || !isUnit(result.confidence)) {
234
+ return { ok: false, error: "classifier_response_invalid" };
235
+ }
236
+ const reasons = result.reasons;
237
+ if (reasons !== undefined &&
238
+ (!Array.isArray(reasons) ||
239
+ reasons.length > 16 ||
240
+ reasons.some((reason) => !isReason(reason)))) {
241
+ return { ok: false, error: "classifier_response_invalid" };
242
+ }
243
+ return {
244
+ ok: true,
245
+ verdict: result.verdict,
246
+ riskScore: result.riskScore,
247
+ confidence: result.confidence,
248
+ ...(reasons === undefined ? {} : { reasons: Object.freeze([...reasons]) }),
249
+ ...(typeof result.provider === "string" && result.provider.length <= 128
250
+ ? { provider: result.provider }
251
+ : {}),
252
+ ...(typeof result.model === "string" && result.model.length <= 128
253
+ ? { model: result.model }
254
+ : {}),
255
+ };
256
+ }
257
+ function isValidCorpus(value) {
258
+ return (isRecord(value) &&
259
+ typeof value.version === "string" &&
260
+ typeof value.lookup === "function");
261
+ }
262
+ function isValidClassifier(value) {
263
+ return (isRecord(value) &&
264
+ typeof value.id === "string" &&
265
+ value.id.length > 0 &&
266
+ typeof value.classify === "function");
267
+ }
268
+ function mergeOptions(defaults, options) {
269
+ return {
270
+ ...defaults,
271
+ ...options,
272
+ policy: { ...defaults.policy, ...options.policy },
273
+ };
274
+ }
275
+ function failure(error, startedAt, link) {
276
+ return Object.freeze({
277
+ ok: false,
278
+ error,
279
+ recommendedAction: "review",
280
+ ...(link === undefined ? {} : { link }),
281
+ latencyMs: Math.max(0, Date.now() - startedAt),
282
+ });
283
+ }
284
+ function isLinkSafetyErrorCode(value) {
285
+ return typeof value === "string" && ERROR_CODE_SET.has(value);
286
+ }
287
+ function isReason(value) {
288
+ return typeof value === "string" && REASON_SET.has(value);
289
+ }
290
+ function isUnit(value) {
291
+ return (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1);
292
+ }
293
+ function clampUnit(value) {
294
+ return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
295
+ }
296
+ function isRecord(value) {
297
+ return typeof value === "object" && value !== null && !Array.isArray(value);
298
+ }
299
+ //# sourceMappingURL=scan.js.map