@xnetjs/abuse 0.0.1 → 0.0.3

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.
@@ -1,487 +0,0 @@
1
- // src/decision.ts
2
- var DEFAULT_CRYPTO = {
3
- hashValid: true,
4
- signatureValid: true,
5
- authorized: true,
6
- freshnessValid: true,
7
- docBindingValid: true
8
- };
9
- var DEFAULT_RESOURCE = {
10
- overSizeLimit: false,
11
- overRateLimit: false,
12
- estimatedCost: 0,
13
- budgetRemaining: null
14
- };
15
- var DEFAULT_ACTOR = {
16
- firstContact: false,
17
- peerScore: 100,
18
- localBlocked: false,
19
- workspaceBlocked: false,
20
- hubBlocked: false
21
- };
22
- var DEFAULT_QUALITY = {
23
- duplicateScore: 0,
24
- slopScore: 0,
25
- citationCoverage: 1,
26
- provenanceScore: 1
27
- };
28
- var DEFAULT_POLICY = {
29
- peerScoreBlockThreshold: 10,
30
- peerScoreThrottleThreshold: 30,
31
- abuseLabelHideThreshold: 1.5,
32
- abuseLabelWarnThreshold: 0.5,
33
- qualityReviewThreshold: 0.65,
34
- qualityWarnThreshold: 0.35,
35
- quarantineFirstContact: true
36
- };
37
- var ABUSE_LABELS = ["malware", "scam", "spam", "impersonation", "harassment"];
38
- var WARNING_LABELS = ["inaccurate", "slop", "unsupported", "stale", "synthetic"];
39
- function normalizeAbuseFacts(facts) {
40
- return {
41
- surface: facts.surface,
42
- crypto: { ...DEFAULT_CRYPTO, ...facts.crypto },
43
- resource: { ...DEFAULT_RESOURCE, ...facts.resource },
44
- actor: { ...DEFAULT_ACTOR, ...facts.actor },
45
- labels: facts.labels ?? [],
46
- quality: { ...DEFAULT_QUALITY, ...facts.quality },
47
- policy: { ...DEFAULT_POLICY, ...facts.policy },
48
- override: facts.override,
49
- now: facts.now ?? Date.now()
50
- };
51
- }
52
- function decideTransport(facts = {}) {
53
- return decideAbuse({ ...facts, surface: "transport" });
54
- }
55
- function decideRemoteMutation(facts = {}) {
56
- return decideAbuse({ ...facts, surface: "remoteMutation" });
57
- }
58
- function decidePublicInteraction(facts = {}) {
59
- return decideAbuse({ ...facts, surface: facts.surface ?? "commentThread" });
60
- }
61
- function decideReach(facts = {}) {
62
- return decideAbuse({ ...facts, surface: facts.surface ?? "searchIndex" });
63
- }
64
- function decideAbuse(input) {
65
- const facts = normalizeAbuseFacts(input);
66
- const hardDecision = decideHardAdmission(facts);
67
- if (hardDecision) {
68
- return hardDecision;
69
- }
70
- const labelDecision = decideByLabels(facts);
71
- if (labelDecision) {
72
- return applySafeOverride(labelDecision, facts);
73
- }
74
- const resourceDecision = decideByResource(facts);
75
- if (resourceDecision) {
76
- return applySafeOverride(resourceDecision, facts);
77
- }
78
- const firstContactDecision = decideByFirstContact(facts);
79
- if (firstContactDecision) {
80
- return applySafeOverride(firstContactDecision, facts);
81
- }
82
- const qualityDecision = decideByQuality(facts);
83
- if (qualityDecision) {
84
- return applySafeOverride(qualityDecision, facts);
85
- }
86
- return applySafeOverride(
87
- createDecision({
88
- reasons: ["accepted"]
89
- }),
90
- facts
91
- );
92
- }
93
- function decideHardAdmission(facts) {
94
- if (facts.resource.overSizeLimit) {
95
- return createDecision({
96
- admission: "reject",
97
- visibility: "hide",
98
- reach: "exclude",
99
- notify: false,
100
- includeInCounters: false,
101
- includeInSearch: false,
102
- reasons: ["over-size-limit"],
103
- telemetry: [securityEvent("xnet.security.invalid_data", "high", "over-size-limit")]
104
- });
105
- }
106
- const invalidCryptoReasons = getInvalidCryptoReasons(facts);
107
- if (invalidCryptoReasons.length > 0) {
108
- return createDecision({
109
- admission: "reject",
110
- visibility: "hide",
111
- reach: "exclude",
112
- resource: facts.actor.peerScore <= facts.policy.peerScoreBlockThreshold ? "block-peer" : "normal",
113
- reasons: ["failed-admission", ...invalidCryptoReasons],
114
- telemetry: invalidCryptoReasons.map(
115
- (reason) => securityEvent(
116
- reason === "invalid-signature" ? "xnet.security.invalid_signature" : "xnet.security.invalid_data",
117
- "high",
118
- reason
119
- )
120
- )
121
- });
122
- }
123
- if (isBlockedByPolicy(facts)) {
124
- return createDecision({
125
- admission: isMutationSurface(facts) ? "reject" : "accept",
126
- visibility: "hide",
127
- reach: "exclude",
128
- notify: false,
129
- includeInCounters: false,
130
- includeInSearch: false,
131
- reasons: ["blocked-by-policy"]
132
- });
133
- }
134
- if (facts.actor.peerScore <= facts.policy.peerScoreBlockThreshold) {
135
- return createDecision({
136
- admission: isMutationSurface(facts) ? "reject" : "quarantine",
137
- visibility: "hide",
138
- reach: "exclude",
139
- resource: "block-peer",
140
- notify: false,
141
- includeInCounters: false,
142
- includeInSearch: false,
143
- reasons: ["peer-score-block"],
144
- telemetry: [securityEvent("xnet.security.peer_blocked", "high", "peer-score-block")]
145
- });
146
- }
147
- return null;
148
- }
149
- function decideByLabels(facts) {
150
- const abuseScore = weightedLabelScore(facts, ABUSE_LABELS);
151
- if (abuseScore >= facts.policy.abuseLabelHideThreshold) {
152
- return createDecision({
153
- admission: isMutationSurface(facts) ? "reject" : "accept",
154
- visibility: "hide",
155
- reach: "exclude",
156
- notify: false,
157
- includeInCounters: false,
158
- includeInSearch: false,
159
- review: review("safety", 80),
160
- reasons: ["trusted-abuse-label"]
161
- });
162
- }
163
- const warningScore = weightedLabelScore(facts, WARNING_LABELS);
164
- if (abuseScore >= facts.policy.abuseLabelWarnThreshold || warningScore >= facts.policy.abuseLabelWarnThreshold) {
165
- return createDecision({
166
- visibility: "warn",
167
- reach: "demote",
168
- includeInCounters: abuseScore === 0,
169
- includeInSearch: false,
170
- reasons: ["trusted-warning-label"]
171
- });
172
- }
173
- return null;
174
- }
175
- function decideByResource(facts) {
176
- if (facts.resource.overRateLimit) {
177
- return createDecision({
178
- admission: isMutationSurface(facts) ? "reject" : "quarantine",
179
- visibility: "warn",
180
- reach: "demote",
181
- resource: "throttle",
182
- notify: false,
183
- includeInCounters: false,
184
- includeInSearch: false,
185
- reasons: ["over-rate-limit"],
186
- telemetry: [securityEvent("xnet.security.rate_limit_exceeded", "medium", "over-rate-limit")]
187
- });
188
- }
189
- if (facts.resource.budgetRemaining !== null && facts.resource.estimatedCost > facts.resource.budgetRemaining) {
190
- return createDecision({
191
- admission: "quarantine",
192
- visibility: "warn",
193
- reach: "exclude",
194
- resource: "require-budget",
195
- notify: false,
196
- includeInCounters: false,
197
- includeInSearch: false,
198
- review: review("operator", 60),
199
- reasons: ["budget-required"]
200
- });
201
- }
202
- if (facts.actor.peerScore <= facts.policy.peerScoreThrottleThreshold) {
203
- return createDecision({
204
- admission: isMutationSurface(facts) ? "reject" : "quarantine",
205
- visibility: "warn",
206
- reach: "demote",
207
- resource: "throttle",
208
- notify: false,
209
- includeInCounters: false,
210
- includeInSearch: false,
211
- reasons: ["peer-score-throttle"]
212
- });
213
- }
214
- return null;
215
- }
216
- function decideByFirstContact(facts) {
217
- if (!facts.policy.quarantineFirstContact || !facts.actor.firstContact) {
218
- return null;
219
- }
220
- if (facts.surface !== "commentThread" && facts.surface !== "messageInbox") {
221
- return null;
222
- }
223
- return createDecision({
224
- admission: "quarantine",
225
- visibility: "warn",
226
- reach: "demote",
227
- notify: false,
228
- includeInCounters: false,
229
- includeInSearch: false,
230
- review: review("safety", 50),
231
- reasons: ["first-contact"]
232
- });
233
- }
234
- function decideByQuality(facts) {
235
- const score = qualityRiskScore(facts);
236
- if (score >= facts.policy.qualityReviewThreshold) {
237
- return createDecision({
238
- admission: facts.surface === "remoteMutation" ? "accept" : "quarantine",
239
- visibility: "warn",
240
- reach: "demote",
241
- includeInCounters: false,
242
- includeInSearch: false,
243
- review: review("quality", Math.round(score * 100)),
244
- reasons: ["quality-risk"]
245
- });
246
- }
247
- if (score >= facts.policy.qualityWarnThreshold) {
248
- return createDecision({
249
- visibility: "warn",
250
- reach: "demote",
251
- includeInSearch: false,
252
- reasons: ["low-confidence-quality-signal"]
253
- });
254
- }
255
- return null;
256
- }
257
- function activeLabels(facts) {
258
- return facts.labels.filter(
259
- (label) => label.expiresAt === void 0 || label.expiresAt > facts.now
260
- );
261
- }
262
- function weightedLabelScore(facts, values) {
263
- return activeLabels(facts).filter((label) => values.includes(label.value)).reduce((score, label) => score + label.confidence * label.sourceWeight, 0);
264
- }
265
- function qualityRiskScore(facts) {
266
- return clamp01(
267
- facts.quality.slopScore * 0.4 + facts.quality.duplicateScore * 0.25 + (1 - facts.quality.citationCoverage) * 0.2 + (1 - facts.quality.provenanceScore) * 0.15
268
- );
269
- }
270
- function getInvalidCryptoReasons(facts) {
271
- return [
272
- facts.crypto.hashValid ? null : "invalid-hash",
273
- facts.crypto.signatureValid ? null : "invalid-signature",
274
- facts.crypto.freshnessValid ? null : "invalid-freshness",
275
- facts.crypto.docBindingValid ? null : "invalid-doc-binding",
276
- facts.crypto.authorized ? null : "unauthorized"
277
- ].filter((reason) => reason !== null);
278
- }
279
- function isBlockedByPolicy(facts) {
280
- return facts.actor.localBlocked || facts.actor.workspaceBlocked || facts.actor.hubBlocked;
281
- }
282
- function isMutationSurface(facts) {
283
- return facts.surface === "remoteMutation" || facts.surface === "localApi";
284
- }
285
- function applySafeOverride(decision, facts) {
286
- if (!facts.override || decision.admission === "reject") {
287
- return decision;
288
- }
289
- return {
290
- ...decision,
291
- visibility: facts.override.visibility ?? decision.visibility,
292
- reach: facts.override.reach ?? decision.reach,
293
- notify: facts.override.notify ?? decision.notify,
294
- includeInCounters: facts.override.includeInCounters ?? decision.includeInCounters,
295
- includeInSearch: facts.override.includeInSearch ?? decision.includeInSearch,
296
- reasons: appendReason(decision.reasons, "user-override"),
297
- evidenceRefs: facts.override.reason ? [...decision.evidenceRefs, facts.override.reason] : decision.evidenceRefs
298
- };
299
- }
300
- function createDecision(input) {
301
- return {
302
- admission: input.admission ?? "accept",
303
- visibility: input.visibility ?? "show",
304
- reach: input.reach ?? "normal",
305
- resource: input.resource ?? "normal",
306
- notify: input.notify ?? true,
307
- includeInCounters: input.includeInCounters ?? true,
308
- includeInSearch: input.includeInSearch ?? true,
309
- review: input.review ?? { required: false },
310
- reasons: dedupeReasons(input.reasons),
311
- evidenceRefs: input.evidenceRefs ?? [],
312
- labelsToEmit: input.labelsToEmit ?? [],
313
- telemetry: input.telemetry ?? []
314
- };
315
- }
316
- function review(queue, priority) {
317
- return { required: true, queue, priority };
318
- }
319
- function securityEvent(eventName, severity, reason) {
320
- return { eventName, severity, reason };
321
- }
322
- function appendReason(reasons, reason) {
323
- return dedupeReasons([...reasons, reason]);
324
- }
325
- function dedupeReasons(reasons) {
326
- return [...new Set(reasons)];
327
- }
328
- function clamp01(value) {
329
- return Math.min(1, Math.max(0, value));
330
- }
331
- function shouldThrottle(decision) {
332
- return decision.resource === "throttle" || decision.resource === "block-peer";
333
- }
334
- function isVisible(decision) {
335
- return decision.visibility === "show" || decision.visibility === "warn" || decision.visibility === "blur";
336
- }
337
- function isRejected(decision) {
338
- return decision.admission === "reject";
339
- }
340
-
341
- // src/telemetry.ts
342
- import { hashBase64 } from "@xnetjs/crypto";
343
- var DEFAULT_REMOTE_MUTATION_REJECTION_EVENT = "xnet.security.remote_mutation_rejected";
344
- var DEFAULT_PEER_HASH_SALT = "xnet.abuse.telemetry.peer.v1";
345
- var SEVERITY_RANK = {
346
- low: 0,
347
- medium: 1,
348
- high: 2,
349
- critical: 3
350
- };
351
- function bucketAbusePeerScore(score) {
352
- if (typeof score !== "number" || !Number.isFinite(score)) return "unknown";
353
- if (score <= 10) return "<=10";
354
- if (score <= 30) return "11-30";
355
- if (score <= 50) return "31-50";
356
- if (score <= 80) return "51-80";
357
- if (score <= 100) return "81-100";
358
- return ">100";
359
- }
360
- function hashAbusePeerIdentifier(peerId, salt = DEFAULT_PEER_HASH_SALT) {
361
- const normalized = peerId?.trim();
362
- if (!normalized) return "unknown";
363
- const input = new TextEncoder().encode(`${salt}:${normalized}`);
364
- return `p_${hashBase64(input, "blake3").slice(0, 16)}`;
365
- }
366
- function createRemoteMutationRejectionTelemetry(input) {
367
- if (input.decision.admission !== "reject") return null;
368
- const facts = normalizeAbuseFacts(input.facts);
369
- const reasons = input.decision.reasons.filter(
370
- (reason) => reason !== "accepted"
371
- );
372
- const primaryReason = reasons[0] ?? "failed-admission";
373
- const peerIdentity = facts.actor.peerId ?? facts.actor.did;
374
- return {
375
- eventName: input.eventName ?? DEFAULT_REMOTE_MUTATION_REJECTION_EVENT,
376
- severity: rejectionSeverity(input.decision),
377
- details: {
378
- actionTaken: "remote_mutation_rejected",
379
- surface: facts.surface,
380
- primaryReason,
381
- reasons,
382
- peerHash: hashAbusePeerIdentifier(peerIdentity, input.peerHashSalt),
383
- peerScoreBucket: bucketAbusePeerScore(facts.actor.peerScore),
384
- resourceAction: input.decision.resource,
385
- shouldThrottle: shouldThrottle(input.decision)
386
- }
387
- };
388
- }
389
- function reportRemoteMutationRejection(telemetry, input) {
390
- if (!telemetry) return false;
391
- const event = createRemoteMutationRejectionTelemetry(input);
392
- if (!event) return false;
393
- telemetry.reportSecurityEvent(event.eventName, event.severity, event.details);
394
- telemetry.reportUsage?.("xnet.security.remote_mutation_rejections", 1);
395
- return true;
396
- }
397
- function rejectionSeverity(decision) {
398
- const explicit = decision.telemetry.map((event) => event.severity);
399
- if (explicit.length > 0) {
400
- return explicit.reduce(
401
- (highest, severity) => SEVERITY_RANK[severity] > SEVERITY_RANK[highest] ? severity : highest
402
- );
403
- }
404
- if (decision.reasons.some(
405
- (reason) => [
406
- "blocked-by-policy",
407
- "invalid-doc-binding",
408
- "invalid-signature",
409
- "peer-score-block",
410
- "unsigned-update",
411
- "unauthorized"
412
- ].includes(reason)
413
- )) {
414
- return "high";
415
- }
416
- if (decision.reasons.some(
417
- (reason) => ["over-rate-limit", "over-size-limit", "peer-score-throttle"].includes(reason)
418
- )) {
419
- return "medium";
420
- }
421
- return "low";
422
- }
423
-
424
- // src/adapters.ts
425
- function createAbuseFactAdapter(adapter) {
426
- return adapter;
427
- }
428
- function decideWithAdapter(input, adapter, decide = decideAbuse) {
429
- const facts = adapter(input);
430
- return {
431
- facts,
432
- decision: decide(facts)
433
- };
434
- }
435
- function createAbuseDecisionAdapter(adapter, decide = decideAbuse) {
436
- return (input) => decideWithAdapter(input, adapter, decide);
437
- }
438
- function createRemoteAdmissionPipeline(options) {
439
- const decide = options.decide ?? decideRemoteMutation;
440
- return {
441
- evaluate(input) {
442
- const result = decideWithAdapter(input, options.adapt, decide);
443
- const accepted = result.decision.admission === "accept";
444
- const rejected = isRejected(result.decision);
445
- const shouldMutate = !rejected;
446
- const throttle = shouldThrottle(result.decision);
447
- if (rejected) {
448
- reportRemoteMutationRejection(options.telemetry, {
449
- facts: result.facts,
450
- decision: result.decision,
451
- eventName: options.telemetryEventName,
452
- peerHashSalt: options.telemetryPeerHashSalt
453
- });
454
- }
455
- return {
456
- ...result,
457
- accepted,
458
- shouldMutate,
459
- shouldRelay: accepted && result.decision.reach !== "exclude",
460
- shouldThrottle: throttle
461
- };
462
- }
463
- };
464
- }
465
-
466
- export {
467
- normalizeAbuseFacts,
468
- decideTransport,
469
- decideRemoteMutation,
470
- decidePublicInteraction,
471
- decideReach,
472
- decideAbuse,
473
- activeLabels,
474
- weightedLabelScore,
475
- qualityRiskScore,
476
- shouldThrottle,
477
- isVisible,
478
- isRejected,
479
- bucketAbusePeerScore,
480
- hashAbusePeerIdentifier,
481
- createRemoteMutationRejectionTelemetry,
482
- reportRemoteMutationRejection,
483
- createAbuseFactAdapter,
484
- decideWithAdapter,
485
- createAbuseDecisionAdapter,
486
- createRemoteAdmissionPipeline
487
- };