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