@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.
package/dist/index.js ADDED
@@ -0,0 +1,3201 @@
1
+ import {
2
+ activeLabels,
3
+ bucketAbusePeerScore,
4
+ createAbuseDecisionAdapter,
5
+ createAbuseFactAdapter,
6
+ createRemoteAdmissionPipeline,
7
+ createRemoteMutationRejectionTelemetry,
8
+ decideAbuse,
9
+ decidePublicInteraction,
10
+ decideReach,
11
+ decideRemoteMutation,
12
+ decideTransport,
13
+ decideWithAdapter,
14
+ hashAbusePeerIdentifier,
15
+ isRejected,
16
+ isVisible,
17
+ normalizeAbuseFacts,
18
+ qualityRiskScore,
19
+ reportRemoteMutationRejection,
20
+ shouldThrottle,
21
+ weightedLabelScore
22
+ } from "./chunk-O3JDSAJP.js";
23
+
24
+ // src/explain.ts
25
+ var REASON_DETAILS = {
26
+ accepted: {
27
+ severity: "low",
28
+ message: "No abuse or quality policy required a restriction."
29
+ },
30
+ "blocked-by-policy": {
31
+ severity: "high",
32
+ message: "A user, workspace, hub, or app-view policy block matched this actor or subject."
33
+ },
34
+ "budget-required": {
35
+ severity: "medium",
36
+ message: "The request exceeded the configured resource budget for this surface."
37
+ },
38
+ "failed-admission": {
39
+ severity: "critical",
40
+ message: "The input failed one or more hard admission checks."
41
+ },
42
+ "first-contact": {
43
+ severity: "medium",
44
+ message: "First-contact interactions are quarantined by policy."
45
+ },
46
+ "invalid-doc-binding": {
47
+ severity: "high",
48
+ message: "The envelope was not bound to the expected document or resource."
49
+ },
50
+ "invalid-freshness": {
51
+ severity: "high",
52
+ message: "The envelope freshness or replay window check failed."
53
+ },
54
+ "invalid-hash": {
55
+ severity: "critical",
56
+ message: "The content hash did not match the signed payload."
57
+ },
58
+ "invalid-signature": {
59
+ severity: "critical",
60
+ message: "The signature did not verify against the claimed author."
61
+ },
62
+ "low-confidence-quality-signal": {
63
+ severity: "low",
64
+ message: "Quality signals suggest caution but not review."
65
+ },
66
+ "over-rate-limit": {
67
+ severity: "medium",
68
+ message: "The actor or peer exceeded the configured rate limit."
69
+ },
70
+ "over-size-limit": {
71
+ severity: "high",
72
+ message: "The payload exceeded the configured size limit."
73
+ },
74
+ "peer-score-block": {
75
+ severity: "high",
76
+ message: "The peer score crossed the block threshold."
77
+ },
78
+ "peer-score-throttle": {
79
+ severity: "medium",
80
+ message: "The peer score crossed the throttle threshold."
81
+ },
82
+ "quality-risk": {
83
+ severity: "medium",
84
+ message: "Quality signals crossed the review threshold."
85
+ },
86
+ "trusted-abuse-label": {
87
+ severity: "high",
88
+ message: "Trusted labels indicate abuse such as spam, scam, malware, or impersonation."
89
+ },
90
+ "trusted-warning-label": {
91
+ severity: "medium",
92
+ message: "Trusted labels indicate a warning or demotion is appropriate."
93
+ },
94
+ unauthorized: {
95
+ severity: "critical",
96
+ message: "The claimed actor is not authorized to perform this action."
97
+ },
98
+ "unsigned-update": {
99
+ severity: "high",
100
+ message: "The remote mutation was unsigned on a surface that requires signed replication."
101
+ },
102
+ "policy-override": {
103
+ severity: "low",
104
+ message: "A workspace or reviewer policy override changed the display or reach decision."
105
+ },
106
+ "user-override": {
107
+ severity: "low",
108
+ message: "A local override changed the display or reach decision."
109
+ }
110
+ };
111
+ function explainDecision(decision) {
112
+ const reasons = decision.reasons.map(toExplanationReason);
113
+ return {
114
+ summary: summarizeDecision(decision),
115
+ reasons
116
+ };
117
+ }
118
+ function getReasonDetail(code) {
119
+ return toExplanationReason(code);
120
+ }
121
+ function summarizeDecision(decision) {
122
+ if (decision.admission === "reject") {
123
+ return "Rejected before acceptance or mutation.";
124
+ }
125
+ if (decision.admission === "quarantine") {
126
+ return "Accepted into quarantine pending review or budget.";
127
+ }
128
+ if (decision.visibility === "hide") {
129
+ return "Accepted but hidden by policy.";
130
+ }
131
+ if (decision.visibility === "warn" || decision.reach === "demote") {
132
+ return "Accepted with warning or reduced reach.";
133
+ }
134
+ return "Accepted normally.";
135
+ }
136
+ function toExplanationReason(code) {
137
+ const detail = REASON_DETAILS[code];
138
+ return {
139
+ code,
140
+ severity: detail.severity,
141
+ message: detail.message
142
+ };
143
+ }
144
+
145
+ // src/fixtures.ts
146
+ var TRUSTED_SPAM_LABEL = {
147
+ value: "spam",
148
+ sourceDID: "did:key:zTrustedLabeler",
149
+ sourceWeight: 2,
150
+ confidence: 1,
151
+ evidenceRefs: ["label:evidence:spam"]
152
+ };
153
+ var WARNING_SLOP_LABEL = {
154
+ value: "slop",
155
+ sourceDID: "did:key:zTrustedQualityLabeler",
156
+ sourceWeight: 1,
157
+ confidence: 0.75,
158
+ evidenceRefs: ["label:evidence:slop"]
159
+ };
160
+ function createBaseFacts(overrides = {}) {
161
+ return {
162
+ surface: overrides.surface ?? "remoteMutation",
163
+ crypto: {
164
+ hashValid: true,
165
+ signatureValid: true,
166
+ authorized: true,
167
+ freshnessValid: true,
168
+ docBindingValid: true,
169
+ ...overrides.crypto
170
+ },
171
+ resource: {
172
+ overSizeLimit: false,
173
+ overRateLimit: false,
174
+ estimatedCost: 0,
175
+ budgetRemaining: null,
176
+ ...overrides.resource
177
+ },
178
+ actor: {
179
+ did: "did:key:zActor",
180
+ peerId: "peer-1",
181
+ firstContact: false,
182
+ peerScore: 100,
183
+ localBlocked: false,
184
+ workspaceBlocked: false,
185
+ hubBlocked: false,
186
+ appViewBlocked: false,
187
+ ...overrides.actor
188
+ },
189
+ labels: overrides.labels ?? [],
190
+ quality: {
191
+ duplicateScore: 0,
192
+ slopScore: 0,
193
+ citationCoverage: 1,
194
+ provenanceScore: 1,
195
+ ...overrides.quality
196
+ },
197
+ policy: overrides.policy,
198
+ override: overrides.override,
199
+ now: overrides.now ?? 17e11
200
+ };
201
+ }
202
+ var abuseFixtures = {
203
+ validRemoteMutation: createBaseFacts(),
204
+ invalidSignatureRemoteMutation: createBaseFacts({
205
+ crypto: { signatureValid: false }
206
+ }),
207
+ oversizedRemoteMutation: createBaseFacts({
208
+ resource: { overSizeLimit: true }
209
+ }),
210
+ firstContactComment: createBaseFacts({
211
+ surface: "commentThread",
212
+ actor: { firstContact: true }
213
+ }),
214
+ trustedSpamComment: createBaseFacts({
215
+ surface: "commentThread",
216
+ labels: [TRUSTED_SPAM_LABEL]
217
+ }),
218
+ lowQualitySearchCandidate: createBaseFacts({
219
+ surface: "searchIndex",
220
+ quality: {
221
+ duplicateScore: 0.8,
222
+ slopScore: 0.8,
223
+ citationCoverage: 0.1,
224
+ provenanceScore: 0.2
225
+ }
226
+ }),
227
+ expiredSpamLabel: createBaseFacts({
228
+ surface: "commentThread",
229
+ labels: [{ ...TRUSTED_SPAM_LABEL, expiresAt: 1 }],
230
+ now: 2
231
+ }),
232
+ throttledPeer: createBaseFacts({
233
+ surface: "transport",
234
+ actor: { peerScore: 20 }
235
+ })
236
+ };
237
+
238
+ // src/ai-provenance.ts
239
+ function isAISignalSourceType(sourceType) {
240
+ return sourceType === "local-ai" || sourceType === "cloud-ai";
241
+ }
242
+ function validateAISignalProvenance(input) {
243
+ if (!isAISignalSourceType(input.sourceType)) {
244
+ return {
245
+ required: false,
246
+ valid: true,
247
+ errors: [],
248
+ provenance: null
249
+ };
250
+ }
251
+ const modelProvider = normalizeRequired(input.modelProvider);
252
+ const modelName = normalizeRequired(input.modelName);
253
+ const errors = [
254
+ modelProvider ? null : "missing-model-provider",
255
+ modelName ? null : "missing-model-name"
256
+ ].filter((error) => error !== null);
257
+ return {
258
+ required: true,
259
+ valid: errors.length === 0,
260
+ errors,
261
+ provenance: errors.length === 0 ? {
262
+ sourceType: input.sourceType,
263
+ modelProvider: modelProvider ?? "",
264
+ modelName: modelName ?? "",
265
+ modelVersion: normalizeOptional(input.modelVersion),
266
+ adapterId: normalizeOptional(input.adapterId),
267
+ adapterVersion: normalizeOptional(input.adapterVersion),
268
+ policyId: normalizeOptional(input.policyId)
269
+ } : null
270
+ };
271
+ }
272
+ function createAISignalProvenanceEvidenceRef(input) {
273
+ const validation = validateAISignalProvenance(input);
274
+ if (!validation.provenance) return null;
275
+ const version = validation.provenance.modelVersion ?? "unversioned";
276
+ return [
277
+ "ai-provenance",
278
+ validation.provenance.sourceType,
279
+ validation.provenance.modelProvider,
280
+ validation.provenance.modelName,
281
+ version
282
+ ].map(normalizeEvidenceRefPart).join(":");
283
+ }
284
+ function normalizeRequired(value) {
285
+ const normalized = value?.trim();
286
+ return normalized && normalized.length > 0 ? normalized : null;
287
+ }
288
+ function normalizeOptional(value) {
289
+ const normalized = value?.trim();
290
+ return normalized && normalized.length > 0 ? normalized : void 0;
291
+ }
292
+ function normalizeEvidenceRefPart(value) {
293
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
294
+ }
295
+
296
+ // src/cloud-classifier.ts
297
+ async function classifyWithCloudAdapter(input, adapter, privacy, budget, _options = {}) {
298
+ const skipped = getCloudClassificationSkipReason(input, adapter, privacy, budget);
299
+ if (skipped) {
300
+ if (skipped !== "over-budget") {
301
+ return createSkippedCloudClassificationResult(input, adapter, privacy, budget, skipped);
302
+ }
303
+ const requestBase2 = createCloudClassifierRequestBase(input, adapter, privacy);
304
+ return createSkippedCloudClassificationResult(input, adapter, privacy, budget, skipped, {
305
+ estimatedCostMicroUsd: estimateCloudClassifierCost(adapter, requestBase2),
306
+ sentBodyChars: requestBase2.body?.length ?? 0
307
+ });
308
+ }
309
+ const requestBase = createCloudClassifierRequestBase(input, adapter, privacy);
310
+ const estimatedCostMicroUsd = estimateCloudClassifierCost(adapter, requestBase);
311
+ const budgetSkipReason = getBudgetSkipReason(estimatedCostMicroUsd, budget);
312
+ if (budgetSkipReason) {
313
+ return createSkippedCloudClassificationResult(
314
+ input,
315
+ adapter,
316
+ privacy,
317
+ budget,
318
+ budgetSkipReason,
319
+ {
320
+ estimatedCostMicroUsd,
321
+ sentBodyChars: requestBase.body?.length ?? 0
322
+ }
323
+ );
324
+ }
325
+ const request = {
326
+ ...requestBase,
327
+ estimatedCostMicroUsd
328
+ };
329
+ const startedAt = Date.now();
330
+ try {
331
+ const result = await adapter.classify(request);
332
+ const chargedCostMicroUsd = result.chargedCostMicroUsd ?? estimatedCostMicroUsd;
333
+ const provenance = createCloudClassifierProvenance(adapter, request.privacyMode);
334
+ return {
335
+ labels: result.labels ? result.labels.map(copyLabel) : [],
336
+ quality: result.quality ?? {},
337
+ signals: normalizeCloudClassifierSignals(result.signals ?? [], provenance),
338
+ provenance,
339
+ usage: {
340
+ estimatedCostMicroUsd,
341
+ chargedCostMicroUsd,
342
+ remainingBudgetMicroUsd: Math.max(0, budget.remainingMicroUsd - chargedCostMicroUsd),
343
+ privacyMode: request.privacyMode,
344
+ sentBodyChars: request.body?.length ?? 0
345
+ },
346
+ skipped: null,
347
+ elapsedMs: Date.now() - startedAt,
348
+ errors: [...result.errors ?? []]
349
+ };
350
+ } catch (error) {
351
+ const provenance = createCloudClassifierProvenance(adapter, request.privacyMode);
352
+ return {
353
+ labels: [],
354
+ quality: {},
355
+ signals: [],
356
+ provenance,
357
+ usage: {
358
+ estimatedCostMicroUsd,
359
+ chargedCostMicroUsd: 0,
360
+ remainingBudgetMicroUsd: budget.remainingMicroUsd,
361
+ privacyMode: request.privacyMode,
362
+ sentBodyChars: request.body?.length ?? 0
363
+ },
364
+ skipped: null,
365
+ elapsedMs: Date.now() - startedAt,
366
+ errors: [error instanceof Error ? error.message : String(error)]
367
+ };
368
+ }
369
+ }
370
+ function createCloudClassifierAdapter(adapter) {
371
+ return adapter;
372
+ }
373
+ function createCloudClassifierRequestBase(input, adapter, privacy) {
374
+ const privacyMode = assertEnabledPrivacyMode(privacy.mode);
375
+ const body = prepareCloudBody(input.body, privacy);
376
+ const title = input.title ? prepareCloudBody(input.title, privacy) : void 0;
377
+ return {
378
+ provider: adapter.provider,
379
+ model: adapter.model,
380
+ adapterId: adapter.id,
381
+ adapterVersion: adapter.version,
382
+ surface: input.surface,
383
+ subjectId: privacy.sendSubjectId ? input.subjectId : void 0,
384
+ title: privacyMode === "metadata-only" ? void 0 : title,
385
+ body: privacyMode === "metadata-only" ? void 0 : body,
386
+ language: input.language,
387
+ metadata: privacy.sendMetadata ? input.metadata : void 0,
388
+ contentFingerprint: privacy.sendContentFingerprint ? input.contentFingerprint : void 0,
389
+ privacyMode
390
+ };
391
+ }
392
+ function estimateCloudClassifierCost(adapter, request) {
393
+ return Math.max(
394
+ 0,
395
+ Math.ceil(adapter.estimateCostMicroUsd?.(request) ?? adapter.defaultEstimatedCostMicroUsd)
396
+ );
397
+ }
398
+ function getCloudClassificationSkipReason(input, adapter, privacy, budget) {
399
+ if (privacy.mode === "disabled") return "cloud-disabled";
400
+ if (!(adapter.supports?.(input) ?? true)) return "unsupported-surface";
401
+ if (privacy.allowedSurfaces && !privacy.allowedSurfaces.includes(input.surface)) {
402
+ return "unsupported-surface";
403
+ }
404
+ if (privacy.allowedProviders && !privacy.allowedProviders.includes(adapter.provider)) {
405
+ return "provider-not-allowed";
406
+ }
407
+ if (privacy.mode === "raw-content" && privacy.requireExplicitRawContentApproval !== false && !privacy.rawContentApproved) {
408
+ return "privacy-policy-blocked";
409
+ }
410
+ const requestBase = createCloudClassifierRequestBase(input, adapter, privacy);
411
+ return getBudgetSkipReason(estimateCloudClassifierCost(adapter, requestBase), budget);
412
+ }
413
+ function redactCloudClassifierText(text, patterns = defaultCloudRedactionPatterns, replacement = "[redacted]") {
414
+ return patterns.reduce((redacted, pattern) => redacted.replace(pattern, replacement), text);
415
+ }
416
+ var defaultCloudRedactionPatterns = [
417
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
418
+ /(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b/g,
419
+ /\bhttps?:\/\/[^\s]+/gi
420
+ ];
421
+ function getBudgetSkipReason(estimatedCostMicroUsd, budget) {
422
+ const minRemaining = budget.minRemainingMicroUsd ?? 0;
423
+ if (estimatedCostMicroUsd > budget.maxPerRequestMicroUsd) return "over-budget";
424
+ if (budget.remainingMicroUsd - estimatedCostMicroUsd < minRemaining) return "over-budget";
425
+ return null;
426
+ }
427
+ function createSkippedCloudClassificationResult(input, adapter, privacy, budget, skipped, usage) {
428
+ const privacyMode = privacy.mode === "disabled" ? "metadata-only" : privacy.mode;
429
+ return {
430
+ labels: [],
431
+ quality: {},
432
+ signals: [],
433
+ provenance: createCloudClassifierProvenance(adapter, privacyMode),
434
+ usage: {
435
+ estimatedCostMicroUsd: usage?.estimatedCostMicroUsd ?? 0,
436
+ chargedCostMicroUsd: 0,
437
+ remainingBudgetMicroUsd: budget.remainingMicroUsd,
438
+ privacyMode: privacy.mode,
439
+ sentBodyChars: usage?.sentBodyChars ?? (privacy.mode === "raw-content" ? input.body.length : 0)
440
+ },
441
+ skipped,
442
+ errors: []
443
+ };
444
+ }
445
+ function createCloudClassifierProvenance(adapter, privacyMode) {
446
+ return {
447
+ provider: "cloud",
448
+ cloudProvider: adapter.provider,
449
+ adapterId: adapter.id,
450
+ adapterVersion: adapter.version,
451
+ model: adapter.model,
452
+ policyId: adapter.policyId,
453
+ privacyMode
454
+ };
455
+ }
456
+ function normalizeCloudClassifierSignals(signals, provenance) {
457
+ return signals.map((signal) => ({
458
+ kind: signal.kind,
459
+ value: signal.value,
460
+ confidence: signal.confidence,
461
+ reason: signal.reason,
462
+ evidenceRefs: [...signal.evidenceRefs ?? []],
463
+ provenance
464
+ }));
465
+ }
466
+ function prepareCloudBody(body, privacy) {
467
+ const bounded = privacy.maxInputChars ? body.slice(0, privacy.maxInputChars) : body;
468
+ if (privacy.mode !== "redacted-content") return bounded;
469
+ return redactCloudClassifierText(
470
+ bounded,
471
+ privacy.redactPatterns ?? defaultCloudRedactionPatterns,
472
+ privacy.redactionReplacement
473
+ );
474
+ }
475
+ function assertEnabledPrivacyMode(mode) {
476
+ if (mode === "disabled") return "metadata-only";
477
+ return mode;
478
+ }
479
+ function copyLabel(label) {
480
+ return {
481
+ ...label,
482
+ evidenceRefs: label.evidenceRefs ? [...label.evidenceRefs] : void 0
483
+ };
484
+ }
485
+
486
+ // src/content-fingerprint.ts
487
+ import { hash, hashHex } from "@xnetjs/crypto";
488
+ var DEFAULT_MIN_TOKEN_LENGTH = 2;
489
+ var DEFAULT_SHINGLE_SIZE = 5;
490
+ var DEFAULT_MAX_SHINGLES = 512;
491
+ var DEFAULT_NEAR_DUPLICATE_THRESHOLD = 0.75;
492
+ var DEFAULT_WEAK_DUPLICATE_THRESHOLD = 0.55;
493
+ var SIMHASH_BITS = 64;
494
+ var encoder = new TextEncoder();
495
+ function createContentFingerprint(input, options = {}) {
496
+ const shingleSize = options.shingleSize ?? DEFAULT_SHINGLE_SIZE;
497
+ const canonicalText = canonicalizeContentText(input);
498
+ const tokens = tokenizeContent(canonicalText, options);
499
+ const uniqueTokens = new Set(tokens);
500
+ return {
501
+ kind: "xnet.content-fingerprint.v1",
502
+ textHash: hashHex(encoder.encode(canonicalText), "blake3"),
503
+ simHash64: createSimHash64(tokens),
504
+ tokenCount: tokens.length,
505
+ uniqueTokenCount: uniqueTokens.size,
506
+ shingleSize,
507
+ shingles: createShingles(tokens, {
508
+ shingleSize,
509
+ maxShingles: options.maxShingles ?? DEFAULT_MAX_SHINGLES
510
+ })
511
+ };
512
+ }
513
+ function assessDuplicateContent(candidate, references, options = {}) {
514
+ const candidateFingerprint = toFingerprint(candidate, options);
515
+ const initial = createDuplicateAssessment({
516
+ duplicateScore: 0,
517
+ matchType: "none",
518
+ matchedIndex: null,
519
+ matchedTextHash: null,
520
+ shingleJaccard: 0,
521
+ simHashSimilarity: 0,
522
+ reasons: []
523
+ });
524
+ if (candidateFingerprint.tokenCount === 0 || references.length === 0) return initial;
525
+ return references.map(
526
+ (reference, index) => compareContentFingerprints(candidateFingerprint, toFingerprint(reference, options), {
527
+ ...options,
528
+ matchedIndex: index
529
+ })
530
+ ).reduce(
531
+ (best, current) => current.duplicateScore > best.duplicateScore ? current : best,
532
+ initial
533
+ );
534
+ }
535
+ function compareContentFingerprints(candidate, reference, options = {}) {
536
+ if (candidate.tokenCount === 0 || reference.tokenCount === 0) {
537
+ return createDuplicateAssessment({
538
+ duplicateScore: 0,
539
+ matchType: "none",
540
+ matchedIndex: options.matchedIndex ?? null,
541
+ matchedTextHash: reference.textHash,
542
+ shingleJaccard: 0,
543
+ simHashSimilarity: 0,
544
+ reasons: []
545
+ });
546
+ }
547
+ const exact = candidate.textHash === reference.textHash;
548
+ const shingleJaccard = jaccard(candidate.shingles, reference.shingles);
549
+ const simHashSimilarity = compareSimHash64(candidate.simHash64, reference.simHash64);
550
+ const duplicateScore = exact ? 1 : clamp01(
551
+ Math.max(
552
+ shingleJaccard,
553
+ simHashSimilarity >= 0.92 ? simHashSimilarity : simHashSimilarity * 0.75
554
+ )
555
+ );
556
+ const nearThreshold = options.nearDuplicateThreshold ?? DEFAULT_NEAR_DUPLICATE_THRESHOLD;
557
+ const weakThreshold = options.weakDuplicateThreshold ?? DEFAULT_WEAK_DUPLICATE_THRESHOLD;
558
+ const matchType = exact ? "exact" : duplicateScore >= nearThreshold ? "near" : duplicateScore >= weakThreshold ? "weak" : "none";
559
+ const reasons = [
560
+ exact ? "exact-content-hash" : null,
561
+ !exact && shingleJaccard >= nearThreshold ? "near-duplicate-shingles" : null,
562
+ !exact && simHashSimilarity >= 0.92 ? "near-duplicate-simhash" : null,
563
+ !exact && matchType === "weak" ? "weak-duplicate-signal" : null
564
+ ].filter((reason) => reason !== null);
565
+ return createDuplicateAssessment({
566
+ duplicateScore,
567
+ matchType,
568
+ exact,
569
+ matchedIndex: options.matchedIndex ?? null,
570
+ matchedTextHash: reference.textHash,
571
+ shingleJaccard,
572
+ simHashSimilarity,
573
+ reasons
574
+ });
575
+ }
576
+ function canonicalizeContentText(input) {
577
+ const text = typeof input === "string" ? input : [input.title, input.body].filter(Boolean).join("\n\n");
578
+ return text.normalize("NFKC").toLowerCase().replace(/https?:\/\/\S+/g, " url ").replace(/['']/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").replace(/\s+/g, " ").trim();
579
+ }
580
+ function tokenizeContent(canonicalText, options = {}) {
581
+ const minTokenLength = options.minTokenLength ?? DEFAULT_MIN_TOKEN_LENGTH;
582
+ if (!canonicalText) return [];
583
+ return canonicalText.split(" ").filter((token) => token.length >= minTokenLength);
584
+ }
585
+ function compareSimHash64(left, right) {
586
+ const leftValue = BigInt(`0x${left}`);
587
+ const rightValue = BigInt(`0x${right}`);
588
+ let diff = leftValue ^ rightValue;
589
+ let distance = 0;
590
+ while (diff > 0n) {
591
+ distance += Number(diff & 1n);
592
+ diff >>= 1n;
593
+ }
594
+ return 1 - distance / SIMHASH_BITS;
595
+ }
596
+ function createShingles(tokens, options) {
597
+ if (tokens.length === 0) return [];
598
+ const width = Math.max(1, Math.min(options.shingleSize, tokens.length));
599
+ const shingles = [];
600
+ for (let i = 0; i <= tokens.length - width; i++) {
601
+ shingles.push(hashHex(encoder.encode(tokens.slice(i, i + width).join(" ")), "blake3"));
602
+ }
603
+ return Array.from(new Set(shingles)).slice(0, options.maxShingles);
604
+ }
605
+ function createSimHash64(tokens) {
606
+ if (tokens.length === 0) return "0000000000000000";
607
+ const weights = Array.from({ length: SIMHASH_BITS }, () => 0);
608
+ for (const token of tokens) {
609
+ const value = hashToken64(token);
610
+ for (let bit = 0; bit < SIMHASH_BITS; bit++) {
611
+ const mask = 1n << BigInt(bit);
612
+ weights[bit] += (value & mask) === 0n ? -1 : 1;
613
+ }
614
+ }
615
+ let result = 0n;
616
+ for (let bit = 0; bit < SIMHASH_BITS; bit++) {
617
+ if (weights[bit] >= 0) {
618
+ result |= 1n << BigInt(bit);
619
+ }
620
+ }
621
+ return result.toString(16).padStart(16, "0");
622
+ }
623
+ function hashToken64(token) {
624
+ const digest = hash(encoder.encode(token), "blake3");
625
+ return digest.slice(0, 8).reduce((value, byte) => value << 8n | BigInt(byte), 0n);
626
+ }
627
+ function jaccard(left, right) {
628
+ if (left.length === 0 || right.length === 0) return 0;
629
+ const leftSet = new Set(left);
630
+ const rightSet = new Set(right);
631
+ const intersection = Array.from(leftSet).filter((value) => rightSet.has(value)).length;
632
+ const union = (/* @__PURE__ */ new Set([...leftSet, ...rightSet])).size;
633
+ return union === 0 ? 0 : intersection / union;
634
+ }
635
+ function toFingerprint(value, options) {
636
+ return isContentFingerprint(value) ? value : createContentFingerprint(value, options);
637
+ }
638
+ function isContentFingerprint(value) {
639
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "xnet.content-fingerprint.v1";
640
+ }
641
+ function createDuplicateAssessment(input) {
642
+ return {
643
+ duplicateScore: input.duplicateScore,
644
+ matchType: input.matchType,
645
+ exact: input.exact ?? input.matchType === "exact",
646
+ matchedIndex: input.matchedIndex ?? null,
647
+ matchedTextHash: input.matchedTextHash ?? null,
648
+ shingleJaccard: input.shingleJaccard ?? 0,
649
+ simHashSimilarity: input.simHashSimilarity ?? 0,
650
+ reasons: input.reasons ? [...input.reasons] : []
651
+ };
652
+ }
653
+ function clamp01(value) {
654
+ return Math.max(0, Math.min(1, value));
655
+ }
656
+
657
+ // src/local-classifier.ts
658
+ async function classifyWithLocalAdapters(input, adapters, options = {}) {
659
+ const boundedInput = boundClassifierInput(input, options.maxInputChars);
660
+ const supported = adapters.filter((adapter) => adapter.supports?.(boundedInput) ?? true);
661
+ const results = await Promise.all(
662
+ supported.map(async (adapter) => {
663
+ const startedAt = Date.now();
664
+ try {
665
+ const result = await adapter.classify(boundedInput, options);
666
+ return {
667
+ ...result,
668
+ elapsedMs: result.elapsedMs ?? Date.now() - startedAt
669
+ };
670
+ } catch (error) {
671
+ return createLocalClassificationResult({
672
+ provenance: {
673
+ provider: "local",
674
+ adapterId: adapter.id,
675
+ adapterVersion: adapter.version,
676
+ model: adapter.model
677
+ },
678
+ errors: [error instanceof Error ? error.message : String(error)]
679
+ });
680
+ }
681
+ })
682
+ );
683
+ return mergeLocalClassificationResults(results, options);
684
+ }
685
+ function createKeywordLocalClassifier(options) {
686
+ const adapterId = options.id ?? "local.keyword";
687
+ const adapterVersion = options.version ?? "1";
688
+ const provenance = {
689
+ provider: "local",
690
+ adapterId,
691
+ adapterVersion,
692
+ model: options.model
693
+ };
694
+ return {
695
+ id: adapterId,
696
+ version: adapterVersion,
697
+ model: options.model,
698
+ classify(input, classifierOptions = {}) {
699
+ const now = classifierOptions.now ?? Date.now();
700
+ const text = canonicalizeContentText({ title: input.title, body: input.body });
701
+ const labels = options.rules.map((rule) => matchKeywordRule(rule, text, options.sourceDid, now)).filter((label) => label !== null).filter((label) => label.confidence >= (classifierOptions.minConfidence ?? 0));
702
+ const signals = labels.map((label) => ({
703
+ kind: "label",
704
+ value: label.value,
705
+ confidence: label.confidence,
706
+ evidenceRefs: [...label.evidenceRefs ?? []],
707
+ provenance
708
+ }));
709
+ return createLocalClassificationResult({
710
+ labels,
711
+ signals,
712
+ provenance
713
+ });
714
+ }
715
+ };
716
+ }
717
+ function mergeLocalClassificationResults(results, options = {}) {
718
+ const minConfidence = options.minConfidence ?? 0;
719
+ const labels = mergeLabels(
720
+ results.flatMap((result) => result.labels).filter((label) => label.confidence >= minConfidence)
721
+ );
722
+ const signals = results.flatMap((result) => result.signals);
723
+ const errors = results.flatMap((result) => result.errors);
724
+ const elapsedMs = results.reduce((total, result) => total + (result.elapsedMs ?? 0), 0);
725
+ return createLocalClassificationResult({
726
+ labels,
727
+ quality: mergeQualitySignals(results.map((result) => result.quality)),
728
+ signals,
729
+ provenance: {
730
+ provider: "local",
731
+ adapterId: "local.aggregate",
732
+ adapterVersion: "1"
733
+ },
734
+ elapsedMs,
735
+ errors
736
+ });
737
+ }
738
+ function createLocalClassificationResult(input) {
739
+ return {
740
+ labels: input.labels ?? [],
741
+ quality: input.quality ?? {},
742
+ signals: input.signals ?? [],
743
+ provenance: input.provenance,
744
+ elapsedMs: input.elapsedMs,
745
+ errors: input.errors ?? []
746
+ };
747
+ }
748
+ function boundClassifierInput(input, maxInputChars) {
749
+ if (!maxInputChars || input.body.length <= maxInputChars) return input;
750
+ return {
751
+ ...input,
752
+ body: input.body.slice(0, maxInputChars)
753
+ };
754
+ }
755
+ function matchKeywordRule(rule, canonicalText, sourceDid, now) {
756
+ const matched = rule.keywords.map((keyword) => canonicalizeContentText(keyword)).find((keyword) => keyword.length > 0 && canonicalText.includes(keyword));
757
+ if (!matched) return null;
758
+ return {
759
+ value: rule.label,
760
+ confidence: rule.confidence,
761
+ sourceDID: sourceDid,
762
+ sourceWeight: rule.sourceWeight ?? 1,
763
+ expiresAt: rule.expiresInMs ? now + rule.expiresInMs : void 0,
764
+ evidenceRefs: [`keyword:${matched}`]
765
+ };
766
+ }
767
+ function mergeLabels(labels) {
768
+ const merged = /* @__PURE__ */ new Map();
769
+ for (const label of labels) {
770
+ const key = `${label.value}:${label.sourceDID ?? "local"}`;
771
+ const existing = merged.get(key);
772
+ if (!existing) {
773
+ merged.set(key, { ...label, evidenceRefs: [...label.evidenceRefs ?? []] });
774
+ continue;
775
+ }
776
+ merged.set(key, {
777
+ ...existing,
778
+ confidence: Math.max(existing.confidence, label.confidence),
779
+ sourceWeight: Math.max(existing.sourceWeight, label.sourceWeight),
780
+ expiresAt: minDefined(existing.expiresAt, label.expiresAt),
781
+ evidenceRefs: Array.from(
782
+ /* @__PURE__ */ new Set([...existing.evidenceRefs ?? [], ...label.evidenceRefs ?? []])
783
+ )
784
+ });
785
+ }
786
+ return Array.from(merged.values());
787
+ }
788
+ function mergeQualitySignals(qualities) {
789
+ return qualities.reduce(
790
+ (merged, quality) => ({
791
+ duplicateScore: maxDefined(merged.duplicateScore, quality.duplicateScore),
792
+ slopScore: maxDefined(merged.slopScore, quality.slopScore),
793
+ citationCoverage: minDefined(merged.citationCoverage, quality.citationCoverage),
794
+ provenanceScore: minDefined(merged.provenanceScore, quality.provenanceScore)
795
+ }),
796
+ {}
797
+ );
798
+ }
799
+ function maxDefined(left, right) {
800
+ if (left === void 0) return right;
801
+ if (right === void 0) return left;
802
+ return Math.max(left, right);
803
+ }
804
+ function minDefined(left, right) {
805
+ if (left === void 0) return right;
806
+ if (right === void 0) return left;
807
+ return Math.min(left, right);
808
+ }
809
+
810
+ // src/classifier-cascade.ts
811
+ async function classifyWithModerationCascade(input, options = {}) {
812
+ const local = await classifyWithLocalAdapters(
813
+ input,
814
+ options.localAdapters ?? [],
815
+ options.localOptions
816
+ );
817
+ const route = decideCloudReviewRoute(input, local, options.cloud?.callPolicy);
818
+ if (!options.cloud) {
819
+ return createCascadeResult(local, void 0, {
820
+ callCloud: false,
821
+ reasons: route.reasons,
822
+ skipped: "cloud-not-configured"
823
+ });
824
+ }
825
+ if (!route.callCloud) {
826
+ return createCascadeResult(local, void 0, route);
827
+ }
828
+ const cloud = await classifyWithCloudAdapter(
829
+ input,
830
+ options.cloud.adapter,
831
+ options.cloud.privacy,
832
+ options.cloud.budget,
833
+ options.cloud.options
834
+ );
835
+ return createCascadeResult(local, cloud, route);
836
+ }
837
+ function decideCloudReviewRoute(input, local, policy = {}) {
838
+ if (policy.enabled === false) {
839
+ return { callCloud: false, reasons: [], skipped: "cloud-disabled" };
840
+ }
841
+ if (policy.allowedSurfaces && !policy.allowedSurfaces.includes(input.surface)) {
842
+ return { callCloud: false, reasons: [], skipped: "unsupported-surface" };
843
+ }
844
+ const labelRisk = maxLocalLabelConfidence(local);
845
+ const qualityRisk = localQualityRisk(local.quality);
846
+ const minLabelRisk = policy.minLocalLabelConfidence ?? 0.85;
847
+ const minQualityRisk = policy.minLocalQualityRisk ?? 0.65;
848
+ const reasons = [
849
+ labelRisk >= minLabelRisk ? "local-label-risk" : null,
850
+ qualityRisk >= minQualityRisk ? "local-quality-risk" : null,
851
+ policy.forceWhenNoLocalSignals === true && hasNoLocalSignals(local) ? "no-local-signals" : null
852
+ ].filter((reason) => reason !== null);
853
+ if (reasons.length > 0) {
854
+ return { callCloud: true, reasons, skipped: null };
855
+ }
856
+ return { callCloud: false, reasons: [], skipped: "low-risk-local-signals" };
857
+ }
858
+ function createCascadeResult(local, cloud, route) {
859
+ return {
860
+ labels: mergeLabels2([...local.labels ?? [], ...cloud?.labels ?? []]),
861
+ quality: mergeQuality(local.quality, cloud?.quality ?? {}),
862
+ local,
863
+ cloud,
864
+ cloudCalled: cloud !== void 0,
865
+ cloudReasons: route.reasons,
866
+ cloudSkippedReason: cloud?.skipped ?? route.skipped,
867
+ errors: [...local.errors, ...cloud?.errors ?? []]
868
+ };
869
+ }
870
+ function maxLocalLabelConfidence(local) {
871
+ return local.labels.reduce((confidence, label) => Math.max(confidence, label.confidence), 0);
872
+ }
873
+ function localQualityRisk(quality) {
874
+ return Math.max(
875
+ quality.duplicateScore ?? 0,
876
+ quality.slopScore ?? 0,
877
+ quality.citationCoverage === void 0 ? 0 : 1 - quality.citationCoverage,
878
+ quality.provenanceScore === void 0 ? 0 : 1 - quality.provenanceScore
879
+ );
880
+ }
881
+ function hasNoLocalSignals(local) {
882
+ return local.labels.length === 0 && local.signals.length === 0 && Object.keys(local.quality).length === 0;
883
+ }
884
+ function mergeLabels2(labels) {
885
+ const merged = /* @__PURE__ */ new Map();
886
+ for (const label of labels) {
887
+ const key = `${label.value}:${label.sourceDID ?? label.evidenceRefs?.join("|") ?? "unknown"}`;
888
+ const existing = merged.get(key);
889
+ if (!existing) {
890
+ merged.set(key, { ...label, evidenceRefs: [...label.evidenceRefs ?? []] });
891
+ continue;
892
+ }
893
+ merged.set(key, {
894
+ ...existing,
895
+ confidence: Math.max(existing.confidence, label.confidence),
896
+ sourceWeight: Math.max(existing.sourceWeight, label.sourceWeight),
897
+ expiresAt: minDefined2(existing.expiresAt, label.expiresAt),
898
+ evidenceRefs: Array.from(
899
+ /* @__PURE__ */ new Set([...existing.evidenceRefs ?? [], ...label.evidenceRefs ?? []])
900
+ )
901
+ });
902
+ }
903
+ return Array.from(merged.values());
904
+ }
905
+ function mergeQuality(local, cloud) {
906
+ return {
907
+ duplicateScore: maxDefined2(local.duplicateScore, cloud.duplicateScore),
908
+ slopScore: maxDefined2(local.slopScore, cloud.slopScore),
909
+ citationCoverage: minDefined2(local.citationCoverage, cloud.citationCoverage),
910
+ provenanceScore: minDefined2(local.provenanceScore, cloud.provenanceScore)
911
+ };
912
+ }
913
+ function maxDefined2(left, right) {
914
+ if (left === void 0) return right;
915
+ if (right === void 0) return left;
916
+ return Math.max(left, right);
917
+ }
918
+ function minDefined2(left, right) {
919
+ if (left === void 0) return right;
920
+ if (right === void 0) return left;
921
+ return Math.min(left, right);
922
+ }
923
+
924
+ // src/deployment-profile.ts
925
+ var DEFAULT_WINDOW_MS = 6e4;
926
+ var DEFAULT_DAY_MS = 864e5;
927
+ function createSmallSelfHostedAbuseProfile(input = {}) {
928
+ const windowMs = input.windowMs ?? DEFAULT_WINDOW_MS;
929
+ const publicWriteBudget = {
930
+ defaultCostUnits: 1,
931
+ limits: [
932
+ { scope: "did", unitsPerWindow: 12, windowMs },
933
+ { scope: "did-surface", unitsPerWindow: 4, windowMs },
934
+ { scope: "workspace", unitsPerWindow: 120, windowMs },
935
+ { scope: "hub", unitsPerWindow: 360, windowMs },
936
+ { scope: "surface", unitsPerWindow: 180, windowMs }
937
+ ]
938
+ };
939
+ const queryCostBudget = {
940
+ defaultCostUnits: 1,
941
+ limits: [
942
+ { scope: "domain-work-type", unitsPerWindow: 18, windowMs },
943
+ { scope: "remote-peer-route", unitsPerWindow: 24, windowMs },
944
+ { scope: "hub-work-type", unitsPerWindow: 240, windowMs },
945
+ { scope: "work-type", unitsPerWindow: 480, windowMs }
946
+ ]
947
+ };
948
+ return {
949
+ id: input.hubId ? `xnet.abuse.profile.small-self-hosted.v1:${input.hubId}` : "xnet.abuse.profile.small-self-hosted.v1",
950
+ kind: "small-self-hosted-hub",
951
+ title: "Small self-hosted hub abuse profile",
952
+ moderation: {
953
+ mode: "local-deterministic",
954
+ requireSignedWrites: true,
955
+ rejectUnsignedFederation: true,
956
+ quarantineFirstContact: true,
957
+ allowLocalOverride: true,
958
+ publishLabelExplanations: true,
959
+ aiReview: {
960
+ localModelsEnabled: true,
961
+ cloudModelsEnabled: false,
962
+ rawContentToCloudAllowed: false,
963
+ defaultReviewQueue: "safety"
964
+ }
965
+ },
966
+ cloudReview: { enabled: false },
967
+ publicWriteBudget,
968
+ queryCostBudget,
969
+ budgetHints: createDeploymentBudgetHints(
970
+ "small-self-hosted",
971
+ publicWriteBudget,
972
+ queryCostBudget
973
+ )
974
+ };
975
+ }
976
+ function createPublicSearchHubAbuseProfile(input = {}) {
977
+ const windowMs = input.windowMs ?? 36e5;
978
+ const cloudReviewDailyMicroUsd = input.cloudReviewDailyMicroUsd ?? 25e4;
979
+ const moderationReviewUnitsPerWindow = input.moderationReviewUnitsPerWindow ?? 600;
980
+ const searchIndexUnitsPerWindow = input.searchIndexUnitsPerWindow ?? 1e4;
981
+ const publicWriteBudget = {
982
+ defaultCostUnits: 1,
983
+ limits: [
984
+ { scope: "did", unitsPerWindow: 60, windowMs },
985
+ { scope: "did-surface", unitsPerWindow: 20, windowMs },
986
+ { scope: "workspace", unitsPerWindow: 1e3, windowMs },
987
+ { scope: "hub", unitsPerWindow: 2e4, windowMs },
988
+ { scope: "surface", unitsPerWindow: 5e3, windowMs }
989
+ ]
990
+ };
991
+ const queryCostBudget = {
992
+ defaultCostUnits: 1,
993
+ limits: [
994
+ { scope: "domain-work-type", unitsPerWindow: 120, windowMs },
995
+ { scope: "remote-peer-route", unitsPerWindow: 300, windowMs },
996
+ { scope: "hub-work-type", unitsPerWindow: 5e3, windowMs },
997
+ { scope: "work-type", unitsPerWindow: 1e4, windowMs }
998
+ ]
999
+ };
1000
+ return {
1001
+ id: input.hubId ? `xnet.abuse.profile.public-search.v1:${input.hubId}` : "xnet.abuse.profile.public-search.v1",
1002
+ kind: "public-search-hub",
1003
+ title: "Public search hub abuse profile",
1004
+ moderation: {
1005
+ mode: "hybrid",
1006
+ requireSignedWrites: true,
1007
+ rejectUnsignedFederation: true,
1008
+ quarantineFirstContact: true,
1009
+ allowLocalOverride: true,
1010
+ publishLabelExplanations: true,
1011
+ aiReview: {
1012
+ localModelsEnabled: true,
1013
+ cloudModelsEnabled: true,
1014
+ rawContentToCloudAllowed: false,
1015
+ maxCloudReviewMicroUsdPerDay: cloudReviewDailyMicroUsd,
1016
+ defaultReviewQueue: "quality"
1017
+ }
1018
+ },
1019
+ cloudReview: {
1020
+ enabled: true,
1021
+ allowedSurfaces: ["crawl", "searchIndex"],
1022
+ minLocalLabelConfidence: 0.7,
1023
+ minLocalQualityRisk: 0.55
1024
+ },
1025
+ publicWriteBudget,
1026
+ queryCostBudget,
1027
+ budgetHints: [
1028
+ ...createDeploymentBudgetHints("public-search", publicWriteBudget, queryCostBudget),
1029
+ {
1030
+ name: "public-search:search-index:domain",
1031
+ workType: "search-index",
1032
+ scope: "domain",
1033
+ unitsPerWindow: searchIndexUnitsPerWindow,
1034
+ windowMs
1035
+ },
1036
+ {
1037
+ name: "public-search:search-index:hub",
1038
+ workType: "search-index",
1039
+ scope: "hub",
1040
+ unitsPerWindow: searchIndexUnitsPerWindow * 10,
1041
+ windowMs
1042
+ },
1043
+ {
1044
+ name: "public-search:moderation-review:safety",
1045
+ workType: "moderation-review",
1046
+ scope: "review-queue:safety",
1047
+ unitsPerWindow: moderationReviewUnitsPerWindow,
1048
+ windowMs
1049
+ },
1050
+ {
1051
+ name: "public-search:cloud-review:daily",
1052
+ workType: "cloud-review",
1053
+ scope: "hub",
1054
+ unitsPerWindow: cloudReviewDailyMicroUsd,
1055
+ windowMs: DEFAULT_DAY_MS
1056
+ }
1057
+ ]
1058
+ };
1059
+ }
1060
+ function createDeploymentBudgetHints(prefix, publicWriteBudget, queryCostBudget) {
1061
+ return [
1062
+ ...publicWriteBudget.limits.map((limit) => ({
1063
+ name: `${prefix}:${limit.scope}`,
1064
+ workType: "public-write",
1065
+ scope: limit.scope,
1066
+ unitsPerWindow: limit.unitsPerWindow,
1067
+ windowMs: limit.windowMs
1068
+ })),
1069
+ ...queryCostBudget.limits.flatMap(
1070
+ (limit) => budgetHintWorkTypes(limit.scope).map((workType) => ({
1071
+ name: `${prefix}:${limit.scope}:${workType}`,
1072
+ workType,
1073
+ scope: limit.scope,
1074
+ unitsPerWindow: limit.unitsPerWindow,
1075
+ windowMs: limit.windowMs
1076
+ }))
1077
+ )
1078
+ ];
1079
+ }
1080
+ function budgetHintWorkTypes(scope) {
1081
+ if (scope.includes("domain")) return ["crawl"];
1082
+ if (scope.includes("remote-peer") || scope.includes("route")) return ["federation-query"];
1083
+ return ["crawl", "federation-query"];
1084
+ }
1085
+
1086
+ // src/citation-coverage.ts
1087
+ function extractCitationReferences(input) {
1088
+ const referenceTargets = extractReferenceTargets(input.body);
1089
+ const citationCandidates = [
1090
+ ...extractMarkdownLinkCitations(input.body),
1091
+ ...extractReferenceUsageCitations(input.body, referenceTargets),
1092
+ ...extractBareUrlCitations(input.body),
1093
+ ...extractDoiCitations(input.body)
1094
+ ];
1095
+ return dedupeCitations(citationCandidates);
1096
+ }
1097
+ function extractKnowledgeClaims(input, options = {}) {
1098
+ const citations = extractCitationReferences(input);
1099
+ return extractKnowledgeClaimsWithCitations(input, citations, options);
1100
+ }
1101
+ function scoreClaimCitationCoverage(input, options = {}) {
1102
+ const citations = extractCitationReferences(input);
1103
+ const claims = extractKnowledgeClaimsWithCitations(input, citations, options);
1104
+ const citedClaimCount = claims.filter((claim) => claim.citationRefs.length > 0).length;
1105
+ const unsupportedClaimCount = Math.max(0, claims.length - citedClaimCount);
1106
+ const citationCoverage = claims.length === 0 ? 1 : citedClaimCount / claims.length;
1107
+ const unsupportedEvidence = claims.filter((claim) => claim.citationRefs.length === 0).map((claim) => `claim:${claim.id}:unsupported`);
1108
+ return {
1109
+ claims,
1110
+ citations,
1111
+ citationCoverage,
1112
+ citedClaimCount,
1113
+ unsupportedClaimCount,
1114
+ claimCount: claims.length,
1115
+ quality: {
1116
+ citationCoverage
1117
+ },
1118
+ evidenceRefs: unsupportedEvidence,
1119
+ reviewEvidenceRefs: unsupportedEvidence,
1120
+ treatment: "review-evidence"
1121
+ };
1122
+ }
1123
+ var DEFAULT_MIN_CLAIM_CHARS = 28;
1124
+ var DEFAULT_CITATION_WINDOW_CHARS = 80;
1125
+ var DEFAULT_MAX_CLAIMS = 100;
1126
+ var factualVerbPattern = /\b(is|are|was|were|has|have|had|will|would|can|could|causes?|caused|increases?|increased|decreases?|decreased|reduces?|reduced|shows?|showed|found|finds|reports?|reported|announced|confirmed|banned|requires?|required|grew|fell|rose|reached|hit)\b/i;
1127
+ var numberOrDatePattern = /\b(\d+(?:[.,]\d+)?%?|\d{4}|jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/i;
1128
+ var capitalizedPhrasePattern = /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/;
1129
+ var citationNeededPattern = /\[(citation needed|source needed|needs source)\]/i;
1130
+ function extractKnowledgeClaimsWithCitations(input, citations, options) {
1131
+ const minClaimChars = options.minClaimChars ?? DEFAULT_MIN_CLAIM_CHARS;
1132
+ const maxClaims = options.maxClaims ?? DEFAULT_MAX_CLAIMS;
1133
+ const citationWindowChars = options.citationWindowChars ?? DEFAULT_CITATION_WINDOW_CHARS;
1134
+ const bodyWithoutReferenceDefinitions = removeReferenceDefinitions(input.body);
1135
+ return splitSentences(bodyWithoutReferenceDefinitions).map((sentence) => scoreSentenceAsClaim(sentence, minClaimChars)).filter(
1136
+ (claim) => claim !== null
1137
+ ).slice(0, maxClaims).map((claim, index) => {
1138
+ const citationRefs = findClaimCitationRefs(claim, citations, citationWindowChars);
1139
+ const id = `claim-${index + 1}`;
1140
+ return {
1141
+ ...claim,
1142
+ id,
1143
+ citationRefs,
1144
+ evidenceRefs: citationRefs.length > 0 ? citationRefs.map((citationId) => `citation:${citationId}`) : [`claim:${id}:unsupported`]
1145
+ };
1146
+ });
1147
+ }
1148
+ function scoreSentenceAsClaim(sentence, minClaimChars) {
1149
+ const text = normalizeClaimText(sentence.text);
1150
+ if (text.length < minClaimChars) return null;
1151
+ if (isStructuralMarkdownLine(text)) return null;
1152
+ const featureCount = [
1153
+ factualVerbPattern.test(text),
1154
+ numberOrDatePattern.test(text),
1155
+ capitalizedPhrasePattern.test(text),
1156
+ citationNeededPattern.test(text)
1157
+ ].filter(Boolean).length;
1158
+ if (featureCount < 2) return null;
1159
+ return {
1160
+ text,
1161
+ startIndex: sentence.startIndex,
1162
+ endIndex: sentence.endIndex,
1163
+ confidence: clamp(0.35 + featureCount * 0.15, 0, 0.95)
1164
+ };
1165
+ }
1166
+ function findClaimCitationRefs(claim, citations, citationWindowChars) {
1167
+ return citations.filter((citation) => {
1168
+ const insideClaim = citation.startIndex >= claim.startIndex && citation.endIndex <= claim.endIndex;
1169
+ const nearbyAfterClaim = citation.startIndex > claim.endIndex && citation.startIndex - claim.endIndex <= citationWindowChars;
1170
+ return insideClaim || nearbyAfterClaim;
1171
+ }).map((citation) => citation.id);
1172
+ }
1173
+ function splitSentences(text) {
1174
+ const sentences = [];
1175
+ let startIndex = 0;
1176
+ for (let index = 0; index < text.length; index += 1) {
1177
+ const char = text[index];
1178
+ const next = text[index + 1];
1179
+ const isBoundary = char === "." || char === "!" || char === "?";
1180
+ if (!isBoundary || next !== void 0 && !/\s/.test(next)) continue;
1181
+ sentences.push({
1182
+ text: text.slice(startIndex, index + 1),
1183
+ startIndex,
1184
+ endIndex: index + 1
1185
+ });
1186
+ startIndex = skipWhitespace(text, index + 1);
1187
+ index = startIndex - 1;
1188
+ }
1189
+ if (startIndex < text.length) {
1190
+ sentences.push({
1191
+ text: text.slice(startIndex),
1192
+ startIndex,
1193
+ endIndex: text.length
1194
+ });
1195
+ }
1196
+ return sentences.filter((sentence) => sentence.text.trim().length > 0);
1197
+ }
1198
+ function extractReferenceTargets(body) {
1199
+ return Array.from(body.matchAll(/^\s*\[([^\]]+)\]:\s*(\S+)/gm)).reduce((targets, match) => {
1200
+ const label = normalizeReferenceLabel(match[1]);
1201
+ const target = match[2]?.trim();
1202
+ if (label && target) targets.set(label, target);
1203
+ return targets;
1204
+ }, /* @__PURE__ */ new Map());
1205
+ }
1206
+ function extractMarkdownLinkCitations(body) {
1207
+ return Array.from(body.matchAll(/\[([^\]]+)\]\((https?:\/\/[^)\s]+|doi:[^)]+)\)/gi)).map(
1208
+ (match, index) => createCitation({
1209
+ id: `md-${index + 1}`,
1210
+ kind: "markdown-link",
1211
+ label: match[1] ?? "",
1212
+ target: match[2] ?? "",
1213
+ startIndex: match.index ?? 0,
1214
+ endIndex: (match.index ?? 0) + match[0].length
1215
+ })
1216
+ );
1217
+ }
1218
+ function extractReferenceUsageCitations(body, referenceTargets) {
1219
+ return Array.from(body.matchAll(/\[([^\]\n]+)\]/g)).filter((match) => !isInsideReferenceDefinition(body, match.index ?? 0)).filter((match) => !citationNeededPattern.test(match[0])).map((match) => ({
1220
+ match,
1221
+ label: normalizeReferenceLabel(match[1])
1222
+ })).filter(({ label }) => referenceTargets.has(label)).map(
1223
+ ({ match, label }, index) => createCitation({
1224
+ id: `ref-${index + 1}`,
1225
+ kind: label.startsWith("^") ? "footnote-link" : "reference-link",
1226
+ label,
1227
+ target: referenceTargets.get(label) ?? "",
1228
+ startIndex: match.index ?? 0,
1229
+ endIndex: (match.index ?? 0) + match[0].length
1230
+ })
1231
+ );
1232
+ }
1233
+ function extractBareUrlCitations(body) {
1234
+ return Array.from(body.matchAll(/\bhttps?:\/\/[^\s)\]]+/gi)).filter((match) => !isInsideReferenceDefinition(body, match.index ?? 0)).map(
1235
+ (match, index) => createCitation({
1236
+ id: `url-${index + 1}`,
1237
+ kind: "bare-url",
1238
+ label: match[0],
1239
+ target: match[0],
1240
+ startIndex: match.index ?? 0,
1241
+ endIndex: (match.index ?? 0) + match[0].length
1242
+ })
1243
+ );
1244
+ }
1245
+ function extractDoiCitations(body) {
1246
+ return Array.from(body.matchAll(/\b10\.\d{4,9}\/[-._;()/:A-Z0-9]+\b/gi)).filter((match) => !isInsideReferenceDefinition(body, match.index ?? 0)).map(
1247
+ (match, index) => createCitation({
1248
+ id: `doi-${index + 1}`,
1249
+ kind: "doi",
1250
+ label: match[0],
1251
+ target: `doi:${match[0]}`,
1252
+ startIndex: match.index ?? 0,
1253
+ endIndex: (match.index ?? 0) + match[0].length
1254
+ })
1255
+ );
1256
+ }
1257
+ function createCitation(input) {
1258
+ return {
1259
+ ...input,
1260
+ domain: getCitationDomain(input.target)
1261
+ };
1262
+ }
1263
+ function dedupeCitations(citations) {
1264
+ const seen = /* @__PURE__ */ new Set();
1265
+ return [...citations].sort(compareCitationPriority).reduce((deduped, citation) => {
1266
+ const overlapsStructuredCitation = deduped.some(
1267
+ (existing) => existing.target === citation.target && rangesOverlap(existing, citation) && citationKindPriority(existing.kind) <= citationKindPriority(citation.kind)
1268
+ );
1269
+ if (overlapsStructuredCitation) return deduped;
1270
+ const key = `${citation.startIndex}:${citation.endIndex}:${citation.target}`;
1271
+ if (seen.has(key)) return deduped;
1272
+ seen.add(key);
1273
+ return [...deduped, citation];
1274
+ }, []).sort((left, right) => left.startIndex - right.startIndex);
1275
+ }
1276
+ function compareCitationPriority(left, right) {
1277
+ return citationKindPriority(left.kind) - citationKindPriority(right.kind) || left.startIndex - right.startIndex;
1278
+ }
1279
+ function citationKindPriority(kind) {
1280
+ if (kind === "markdown-link") return 0;
1281
+ if (kind === "reference-link") return 1;
1282
+ if (kind === "footnote-link") return 1;
1283
+ if (kind === "doi") return 2;
1284
+ return 3;
1285
+ }
1286
+ function rangesOverlap(left, right) {
1287
+ return left.startIndex < right.endIndex && right.startIndex < left.endIndex;
1288
+ }
1289
+ function removeReferenceDefinitions(body) {
1290
+ return body.replace(/^\s*\[[^\]]+\]:\s*\S+.*$/gm, "");
1291
+ }
1292
+ function isInsideReferenceDefinition(body, index) {
1293
+ const lineStart = body.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
1294
+ const lineEndIndex = body.indexOf("\n", index);
1295
+ const lineEnd = lineEndIndex === -1 ? body.length : lineEndIndex;
1296
+ return /^\s*\[[^\]]+\]:\s*\S+/.test(body.slice(lineStart, lineEnd));
1297
+ }
1298
+ function skipWhitespace(text, startIndex) {
1299
+ let index = startIndex;
1300
+ while (index < text.length && /\s/.test(text[index] ?? "")) {
1301
+ index += 1;
1302
+ }
1303
+ return index;
1304
+ }
1305
+ function normalizeClaimText(text) {
1306
+ return text.replace(/\s+/g, " ").replace(/^[#>*\-\d.\s]+/, "").trim();
1307
+ }
1308
+ function isStructuralMarkdownLine(text) {
1309
+ return /^(#{1,6}\s+|[-*]\s*$|```|:::)/.test(text);
1310
+ }
1311
+ function normalizeReferenceLabel(label) {
1312
+ return (label ?? "").trim().toLowerCase();
1313
+ }
1314
+ function getCitationDomain(target) {
1315
+ if (target.startsWith("doi:")) return "doi.org";
1316
+ try {
1317
+ return new URL(target).hostname.toLowerCase().replace(/^www\./, "");
1318
+ } catch {
1319
+ return void 0;
1320
+ }
1321
+ }
1322
+ function clamp(value, min, max) {
1323
+ return Math.max(min, Math.min(max, value));
1324
+ }
1325
+
1326
+ // src/labeler-trust.ts
1327
+ var TRUST_LEVEL_PRIORITY = {
1328
+ blocked: 4,
1329
+ trusted: 3,
1330
+ review: 2,
1331
+ observe: 1
1332
+ };
1333
+ function evaluateLabelerTrust(input, settings) {
1334
+ const now = input.now ?? Date.now();
1335
+ const setting = selectLabelerTrustSetting(input, settings, now);
1336
+ if (!setting) {
1337
+ return {
1338
+ accepted: false,
1339
+ action: "ignore",
1340
+ reasons: ["labeler:unconfigured"],
1341
+ level: "unconfigured",
1342
+ effectiveWeight: 0,
1343
+ minConfidence: 1
1344
+ };
1345
+ }
1346
+ if (setting.level === "blocked" || includesLabel(setting.deniedLabels, input.labelValue)) {
1347
+ return createTrustDecision(setting, "reject", ["labeler:blocked"]);
1348
+ }
1349
+ if (setting.allowedLabels && !includesLabel(setting.allowedLabels, input.labelValue)) {
1350
+ return createTrustDecision(setting, "ignore", ["labeler:label-not-allowed"]);
1351
+ }
1352
+ if (input.confidence < setting.minConfidence) {
1353
+ return createTrustDecision(setting, "review", ["labeler:confidence-too-low"]);
1354
+ }
1355
+ if (setting.level === "observe") {
1356
+ return createTrustDecision(setting, "observe", ["labeler:observe-only"]);
1357
+ }
1358
+ if (setting.level === "review") {
1359
+ return createTrustDecision(setting, "review", ["labeler:review-required"]);
1360
+ }
1361
+ return createTrustDecision(setting, "accept", ["labeler:trusted"]);
1362
+ }
1363
+ function createTrustedLabelFromSetting(input, settings) {
1364
+ const decision = evaluateLabelerTrust(input, settings);
1365
+ if (!decision.accepted) return null;
1366
+ return {
1367
+ value: input.labelValue,
1368
+ sourceDID: input.labelerDID,
1369
+ sourceWeight: decision.effectiveWeight,
1370
+ confidence: clamp012(input.confidence),
1371
+ expiresAt: input.labelExpiresAt,
1372
+ evidenceRefs: input.evidenceRefs
1373
+ };
1374
+ }
1375
+ function evaluateReportEscalation(input, settings) {
1376
+ const evidenceRefs = createReportEvidenceRefs(input);
1377
+ const trustInput = {
1378
+ scope: input.scope,
1379
+ scopeId: input.scopeId,
1380
+ labelerDID: input.reporterDID,
1381
+ labelValue: input.labelValue,
1382
+ confidence: input.confidence,
1383
+ evidenceRefs,
1384
+ labelExpiresAt: input.labelExpiresAt,
1385
+ now: input.now
1386
+ };
1387
+ const trustDecision = evaluateLabelerTrust(trustInput, settings);
1388
+ if (!trustDecision.accepted) {
1389
+ return {
1390
+ canAffectVisibility: false,
1391
+ trustDecision,
1392
+ trustedLabel: null,
1393
+ evidenceRefs
1394
+ };
1395
+ }
1396
+ return {
1397
+ canAffectVisibility: true,
1398
+ trustDecision,
1399
+ trustedLabel: {
1400
+ value: input.labelValue,
1401
+ sourceDID: input.reporterDID,
1402
+ sourceWeight: trustDecision.effectiveWeight,
1403
+ confidence: clamp012(input.confidence),
1404
+ expiresAt: input.labelExpiresAt,
1405
+ evidenceRefs
1406
+ },
1407
+ evidenceRefs
1408
+ };
1409
+ }
1410
+ function createLabelerSubscription(input) {
1411
+ return {
1412
+ id: input.id,
1413
+ labelerDID: input.labelerDID,
1414
+ workspaceId: input.workspaceId,
1415
+ hubId: input.hubId,
1416
+ status: input.status ?? "active",
1417
+ createdAt: input.createdAt ?? Date.now(),
1418
+ expiresAt: input.expiresAt
1419
+ };
1420
+ }
1421
+ function evaluateLabelerSubscriptionLimit(input, policy, subscriptions = []) {
1422
+ const now = input.now ?? Date.now();
1423
+ const active = subscriptions.filter(
1424
+ (subscription) => subscription.status === "active" && subscription.id !== input.id && (subscription.expiresAt === void 0 || subscription.expiresAt > now)
1425
+ );
1426
+ const activeCounts = {
1427
+ workspace: countSubscriptions(active, { workspaceId: input.workspaceId }),
1428
+ hub: countSubscriptions(active, { hubId: input.hubId }),
1429
+ workspaceLabeler: countSubscriptions(active, {
1430
+ workspaceId: input.workspaceId,
1431
+ labelerDID: input.labelerDID
1432
+ }),
1433
+ hubLabeler: countSubscriptions(active, {
1434
+ hubId: input.hubId,
1435
+ labelerDID: input.labelerDID
1436
+ })
1437
+ };
1438
+ const reasons = [
1439
+ exceeds(policy.maxWorkspaceSubscriptions, activeCounts.workspace) ? "labeler-subscription:workspace-limit-exceeded" : null,
1440
+ exceeds(policy.maxHubSubscriptions, activeCounts.hub) ? "labeler-subscription:hub-limit-exceeded" : null,
1441
+ exceeds(policy.maxWorkspaceSubscriptionsPerLabeler, activeCounts.workspaceLabeler) ? "labeler-subscription:workspace-labeler-limit-exceeded" : null,
1442
+ exceeds(policy.maxHubSubscriptionsPerLabeler, activeCounts.hubLabeler) ? "labeler-subscription:hub-labeler-limit-exceeded" : null
1443
+ ].filter((reason) => reason !== null);
1444
+ return {
1445
+ allowed: reasons.length === 0,
1446
+ reasons: reasons.length > 0 ? reasons : ["labeler-subscription:accepted"],
1447
+ activeCounts,
1448
+ nextSubscription: reasons.length === 0 ? createLabelerSubscription({
1449
+ id: input.id,
1450
+ labelerDID: input.labelerDID,
1451
+ workspaceId: input.workspaceId,
1452
+ hubId: input.hubId,
1453
+ createdAt: now
1454
+ }) : void 0
1455
+ };
1456
+ }
1457
+ function trustLevelFromWeight(weight, enabled) {
1458
+ if (!enabled || weight <= 0) return "blocked";
1459
+ if (weight >= 0.75) return "trusted";
1460
+ if (weight >= 0.4) return "review";
1461
+ return "observe";
1462
+ }
1463
+ function runtimeScope(scope) {
1464
+ return scope === "hub" ? "hub" : "workspace";
1465
+ }
1466
+ function subscriptionToTrustSetting(sub, scopeId) {
1467
+ const enabled = sub.enabled !== false;
1468
+ const weight = clamp012(sub.trust);
1469
+ return {
1470
+ scope: runtimeScope(sub.scope),
1471
+ scopeId,
1472
+ labelerDID: sub.labelerDID,
1473
+ level: trustLevelFromWeight(weight, enabled),
1474
+ weight,
1475
+ minConfidence: sub.minConfidence ?? 0.5,
1476
+ allowedLabels: sub.allowedLabels,
1477
+ deniedLabels: sub.deniedLabels,
1478
+ maxLabelsPerSubject: sub.maxLabelsPerSubject,
1479
+ expiresAt: sub.expiresAt
1480
+ };
1481
+ }
1482
+ function subscriptionsToTrustSettings(subs, scopeId, now = Date.now()) {
1483
+ return subs.filter((sub) => sub.expiresAt === void 0 || sub.expiresAt > now).map((sub) => subscriptionToTrustSetting(sub, scopeId));
1484
+ }
1485
+ function selectLabelerTrustSetting(input, settings, now) {
1486
+ return settings.filter(
1487
+ (setting) => setting.scope === input.scope && setting.scopeId === input.scopeId && setting.labelerDID === input.labelerDID && (setting.expiresAt === void 0 || setting.expiresAt > now)
1488
+ ).sort(
1489
+ (left, right) => TRUST_LEVEL_PRIORITY[right.level] - TRUST_LEVEL_PRIORITY[left.level] || right.weight - left.weight
1490
+ )[0] ?? null;
1491
+ }
1492
+ function createTrustDecision(setting, action, reasons) {
1493
+ return {
1494
+ accepted: action === "accept",
1495
+ action,
1496
+ reasons,
1497
+ level: setting.level,
1498
+ effectiveWeight: action === "accept" ? clamp012(setting.weight) : 0,
1499
+ minConfidence: setting.minConfidence,
1500
+ setting
1501
+ };
1502
+ }
1503
+ function includesLabel(labels, label) {
1504
+ return Boolean(labels?.includes(label));
1505
+ }
1506
+ function createReportEvidenceRefs(input) {
1507
+ return [`abuse-report:${input.reportId}`, ...input.evidenceRefs ?? []];
1508
+ }
1509
+ function countSubscriptions(subscriptions, filter) {
1510
+ if (!filter.workspaceId && !filter.hubId) return 0;
1511
+ return subscriptions.filter(
1512
+ (subscription) => (filter.workspaceId === void 0 || subscription.workspaceId === filter.workspaceId) && (filter.hubId === void 0 || subscription.hubId === filter.hubId) && (filter.labelerDID === void 0 || subscription.labelerDID === filter.labelerDID)
1513
+ ).length;
1514
+ }
1515
+ function exceeds(limit, currentCount) {
1516
+ return typeof limit === "number" && currentCount >= limit;
1517
+ }
1518
+ function clamp012(value) {
1519
+ if (!Number.isFinite(value)) return 0;
1520
+ return Math.min(1, Math.max(0, value));
1521
+ }
1522
+
1523
+ // src/community-notes.ts
1524
+ function summarizeCommunityNoteAgreement(ratings, options = {}) {
1525
+ const normalized = ratings.map(normalizeRating);
1526
+ const perspectives = summarizePerspectives(normalized);
1527
+ const effectiveWeight = normalized.reduce((total, rating) => total + rating.weight, 0);
1528
+ const helpfulWeight = sumHelpfulness(normalized, "helpful");
1529
+ const notHelpfulWeight = sumHelpfulness(normalized, "not-helpful") + sumHelpfulness(normalized, "irrelevant");
1530
+ const needsSourceWeight = sumHelpfulness(normalized, "needs-source");
1531
+ const irrelevantWeight = sumHelpfulness(normalized, "irrelevant");
1532
+ const helpfulScore = ratio(helpfulWeight, effectiveWeight);
1533
+ const notHelpfulScore = ratio(notHelpfulWeight, effectiveWeight);
1534
+ const needsSourceScore = ratio(needsSourceWeight, effectiveWeight);
1535
+ const irrelevantScore = ratio(irrelevantWeight, effectiveWeight);
1536
+ const supportingPerspectives = perspectives.filter(
1537
+ (perspective) => perspective.helpfulWeight > 0 && perspective.helpfulWeight >= perspective.notHelpfulWeight + perspective.needsSourceWeight + perspective.irrelevantWeight
1538
+ );
1539
+ const diversityScore = scorePerspectiveDiversity(
1540
+ supportingPerspectives.map((perspective) => perspective.helpfulWeight)
1541
+ );
1542
+ const agreementScore = helpfulScore * diversityScore;
1543
+ const status = resolveCommunityNoteAgreementStatus({
1544
+ ratingCount: normalized.length,
1545
+ helpfulRatings: normalized.filter((rating) => rating.helpfulness === "helpful").length,
1546
+ supportingPerspectiveCount: supportingPerspectives.length,
1547
+ helpfulScore,
1548
+ notHelpfulScore,
1549
+ diversityScore,
1550
+ options
1551
+ });
1552
+ return {
1553
+ status,
1554
+ ratingCount: normalized.length,
1555
+ effectiveWeight,
1556
+ helpfulScore,
1557
+ notHelpfulScore,
1558
+ needsSourceScore,
1559
+ irrelevantScore,
1560
+ diversityScore,
1561
+ agreementScore,
1562
+ supportingPerspectiveCount: supportingPerspectives.length,
1563
+ perspectiveCount: perspectives.length,
1564
+ perspectives,
1565
+ reasons: createCommunityNoteAgreementReasons(status, {
1566
+ ratingCount: normalized.length,
1567
+ helpfulRatings: normalized.filter((rating) => rating.helpfulness === "helpful").length,
1568
+ supportingPerspectiveCount: supportingPerspectives.length,
1569
+ diversityScore,
1570
+ options
1571
+ })
1572
+ };
1573
+ }
1574
+ function groupCommunityNoteRatingsByPerspective(ratings) {
1575
+ return ratings.reduce((groups, rating) => {
1576
+ const perspective = normalizePerspective(rating);
1577
+ const existing = groups.get(perspective) ?? [];
1578
+ groups.set(perspective, [...existing, rating]);
1579
+ return groups;
1580
+ }, /* @__PURE__ */ new Map());
1581
+ }
1582
+ function scoreCommunityNotePerspectiveDiversity(ratings) {
1583
+ return scorePerspectiveDiversity(
1584
+ summarizePerspectives(ratings.map(normalizeRating)).map(
1585
+ (perspective) => perspective.helpfulWeight
1586
+ )
1587
+ );
1588
+ }
1589
+ function isCommunityNoteAgreementVisible(summary) {
1590
+ return summary.status === "helpful";
1591
+ }
1592
+ var DEFAULT_MIN_RATINGS = 5;
1593
+ var DEFAULT_MIN_HELPFUL_RATINGS = 3;
1594
+ var DEFAULT_MIN_PERSPECTIVE_GROUPS = 2;
1595
+ var DEFAULT_MIN_DIVERSITY_SCORE = 0.45;
1596
+ var DEFAULT_HELPFUL_THRESHOLD = 0.68;
1597
+ var DEFAULT_NOT_HELPFUL_THRESHOLD = 0.62;
1598
+ var DEFAULT_CONTESTED_THRESHOLD = 0.3;
1599
+ function normalizeRating(rating) {
1600
+ return {
1601
+ ...rating,
1602
+ confidence: clamp2(rating.confidence, 0, 1),
1603
+ sourceWeight: clamp2(rating.sourceWeight ?? 1, 0.25, 4),
1604
+ weight: clamp2(rating.confidence, 0, 1) * clamp2(rating.sourceWeight ?? 1, 0.25, 4),
1605
+ perspectiveKey: normalizePerspective(rating)
1606
+ };
1607
+ }
1608
+ function summarizePerspectives(ratings) {
1609
+ return Array.from(
1610
+ ratings.reduce((groups, rating) => {
1611
+ const existing = groups.get(rating.perspectiveKey) ?? {
1612
+ perspective: rating.perspectiveKey,
1613
+ ratingCount: 0,
1614
+ helpfulWeight: 0,
1615
+ notHelpfulWeight: 0,
1616
+ needsSourceWeight: 0,
1617
+ irrelevantWeight: 0
1618
+ };
1619
+ groups.set(rating.perspectiveKey, addRatingToPerspective(existing, rating));
1620
+ return groups;
1621
+ }, /* @__PURE__ */ new Map()).values()
1622
+ ).sort((left, right) => left.perspective.localeCompare(right.perspective));
1623
+ }
1624
+ function addRatingToPerspective(perspective, rating) {
1625
+ return {
1626
+ ...perspective,
1627
+ ratingCount: perspective.ratingCount + 1,
1628
+ helpfulWeight: perspective.helpfulWeight + (rating.helpfulness === "helpful" ? rating.weight : 0),
1629
+ notHelpfulWeight: perspective.notHelpfulWeight + (rating.helpfulness === "not-helpful" ? rating.weight : 0),
1630
+ needsSourceWeight: perspective.needsSourceWeight + (rating.helpfulness === "needs-source" ? rating.weight : 0),
1631
+ irrelevantWeight: perspective.irrelevantWeight + (rating.helpfulness === "irrelevant" ? rating.weight : 0)
1632
+ };
1633
+ }
1634
+ function resolveCommunityNoteAgreementStatus(input) {
1635
+ const minRatings = input.options.minRatings ?? DEFAULT_MIN_RATINGS;
1636
+ const minHelpfulRatings = input.options.minHelpfulRatings ?? DEFAULT_MIN_HELPFUL_RATINGS;
1637
+ const minPerspectiveGroups = input.options.minPerspectiveGroups ?? DEFAULT_MIN_PERSPECTIVE_GROUPS;
1638
+ const minDiversityScore = input.options.minDiversityScore ?? DEFAULT_MIN_DIVERSITY_SCORE;
1639
+ const helpfulThreshold = input.options.helpfulThreshold ?? DEFAULT_HELPFUL_THRESHOLD;
1640
+ const notHelpfulThreshold = input.options.notHelpfulThreshold ?? DEFAULT_NOT_HELPFUL_THRESHOLD;
1641
+ const contestedThreshold = input.options.contestedThreshold ?? DEFAULT_CONTESTED_THRESHOLD;
1642
+ if (input.ratingCount < minRatings) return "insufficient";
1643
+ if (input.notHelpfulScore >= notHelpfulThreshold && input.helpfulScore < helpfulThreshold) {
1644
+ return "not-helpful";
1645
+ }
1646
+ if (input.helpfulScore >= helpfulThreshold && input.notHelpfulScore >= contestedThreshold) {
1647
+ return "contested";
1648
+ }
1649
+ if (input.helpfulScore < helpfulThreshold || input.helpfulRatings < minHelpfulRatings) {
1650
+ return "insufficient";
1651
+ }
1652
+ if (input.supportingPerspectiveCount < minPerspectiveGroups || input.diversityScore < minDiversityScore) {
1653
+ return "needs-more-diversity";
1654
+ }
1655
+ return "helpful";
1656
+ }
1657
+ function createCommunityNoteAgreementReasons(status, input) {
1658
+ if (status === "helpful") return ["diverse-helpful-agreement"];
1659
+ if (status === "not-helpful") return ["not-helpful-consensus"];
1660
+ if (status === "contested") return ["contested-ratings"];
1661
+ if (status === "needs-more-diversity") return ["helpful-but-not-diverse"];
1662
+ return [
1663
+ input.ratingCount < (input.options.minRatings ?? DEFAULT_MIN_RATINGS) ? "below-min-ratings" : null,
1664
+ input.helpfulRatings < (input.options.minHelpfulRatings ?? DEFAULT_MIN_HELPFUL_RATINGS) ? "below-min-helpful-ratings" : null,
1665
+ input.supportingPerspectiveCount < (input.options.minPerspectiveGroups ?? DEFAULT_MIN_PERSPECTIVE_GROUPS) ? "below-min-perspective-groups" : null,
1666
+ input.diversityScore < (input.options.minDiversityScore ?? DEFAULT_MIN_DIVERSITY_SCORE) ? "below-min-diversity-score" : null
1667
+ ].filter((reason) => reason !== null);
1668
+ }
1669
+ function scorePerspectiveDiversity(weights) {
1670
+ const positiveWeights = weights.filter((weight) => weight > 0);
1671
+ const total = positiveWeights.reduce((sum, weight) => sum + weight, 0);
1672
+ if (positiveWeights.length <= 1 || total <= 0) return 0;
1673
+ const entropy = positiveWeights.reduce((sum, weight) => {
1674
+ const probability = weight / total;
1675
+ return sum - probability * Math.log2(probability);
1676
+ }, 0);
1677
+ return clamp2(entropy / Math.log2(positiveWeights.length), 0, 1);
1678
+ }
1679
+ function sumHelpfulness(ratings, helpfulness) {
1680
+ return ratings.filter((rating) => rating.helpfulness === helpfulness).reduce((total, rating) => total + rating.weight, 0);
1681
+ }
1682
+ function ratio(numerator, denominator) {
1683
+ if (denominator <= 0) return 0;
1684
+ return numerator / denominator;
1685
+ }
1686
+ function normalizePerspective(rating) {
1687
+ return rating.perspective?.trim().toLowerCase().replace(/\s+/g, "-") || rating.raterDID || "unknown";
1688
+ }
1689
+ function clamp2(value, min, max) {
1690
+ return Math.max(min, Math.min(max, value));
1691
+ }
1692
+
1693
+ // src/staged-writes.ts
1694
+ function planStagedModerationWrites(candidates, policy = {}, options = {}) {
1695
+ const now = options.now ?? Date.now();
1696
+ const writes = candidates.map(
1697
+ (candidate, index) => createStagedModerationWrite(candidate, policy, now, index)
1698
+ );
1699
+ const staged = boundReviewQueue(
1700
+ writes.filter((write) => write.status === "staged"),
1701
+ policy,
1702
+ now
1703
+ );
1704
+ const committed = writes.filter((write) => write.status === "committed");
1705
+ const rejected = writes.filter((write) => write.status === "rejected");
1706
+ return {
1707
+ staged: staged.staged,
1708
+ committed,
1709
+ rejected: [...rejected, ...staged.rejected],
1710
+ materialized: committed.flatMap((write) => materializeStagedModerationWrite(write) ?? []),
1711
+ reviewTasks: staged.staged.map((write) => createReviewTaskForStagedWrite(write, now))
1712
+ };
1713
+ }
1714
+ function approveStagedModerationWrite(write, reviewerDID, options = {}) {
1715
+ const now = options.now ?? Date.now();
1716
+ return {
1717
+ ...write,
1718
+ status: "committed",
1719
+ reviewRequired: false,
1720
+ committedAt: now,
1721
+ committedBy: reviewerDID
1722
+ };
1723
+ }
1724
+ function rejectStagedModerationWrite(write, reviewerDID, reason, options = {}) {
1725
+ const now = options.now ?? Date.now();
1726
+ return {
1727
+ ...write,
1728
+ status: "rejected",
1729
+ reviewRequired: false,
1730
+ rejectedAt: now,
1731
+ rejectedBy: reviewerDID,
1732
+ rejectionReason: reason
1733
+ };
1734
+ }
1735
+ function materializeStagedModerationWrite(write) {
1736
+ if (write.status !== "committed") return null;
1737
+ return {
1738
+ targetId: write.targetId,
1739
+ targetSchema: write.targetSchema,
1740
+ kind: write.kind,
1741
+ value: write.value,
1742
+ score: write.score,
1743
+ confidence: clamp3(write.confidence, 0, 1),
1744
+ sourceType: write.sourceType,
1745
+ sourceDID: write.sourceDID,
1746
+ sourceWeight: clamp3(write.sourceWeight ?? 1, 0, 10),
1747
+ evidenceRefs: [...write.evidenceRefs ?? []],
1748
+ modelProvider: write.modelProvider,
1749
+ modelName: write.modelName,
1750
+ modelVersion: write.modelVersion,
1751
+ expiresAt: write.expiresAt
1752
+ };
1753
+ }
1754
+ var DEFAULT_MIN_STAGE_CONFIDENCE = 0.15;
1755
+ var DEFAULT_AUTO_COMMIT_CONFIDENCE = 0.9;
1756
+ var DEFAULT_AUTO_COMMIT_SOURCES = ["deterministic", "human", "policy-list"];
1757
+ var DEFAULT_REVIEW_SOURCES = ["local-ai", "cloud-ai", "report"];
1758
+ function boundReviewQueue(staged, policy, now) {
1759
+ if (policy.maxReviewTasks === void 0 && policy.maxReviewTasksBySource === void 0) {
1760
+ return { staged: [...staged], rejected: [] };
1761
+ }
1762
+ const maxReviewTasks = Math.max(0, policy.maxReviewTasks ?? Number.POSITIVE_INFINITY);
1763
+ const sourceCounts = /* @__PURE__ */ new Map();
1764
+ const acceptedIds = /* @__PURE__ */ new Set();
1765
+ const ranked = [...staged].sort(
1766
+ (left, right) => reviewPriority(right, now) - reviewPriority(left, now) || left.createdAt - right.createdAt || left.id.localeCompare(right.id)
1767
+ );
1768
+ for (const write of ranked) {
1769
+ const sourceLimit = policy.maxReviewTasksBySource?.[write.sourceType];
1770
+ const currentSourceCount = sourceCounts.get(write.sourceType) ?? 0;
1771
+ if (acceptedIds.size >= maxReviewTasks) continue;
1772
+ if (sourceLimit !== void 0 && currentSourceCount >= Math.max(0, sourceLimit)) continue;
1773
+ acceptedIds.add(write.id);
1774
+ sourceCounts.set(write.sourceType, currentSourceCount + 1);
1775
+ }
1776
+ return {
1777
+ staged: staged.filter((write) => acceptedIds.has(write.id)),
1778
+ rejected: staged.filter((write) => !acceptedIds.has(write.id)).map((write) => rejectReviewOverflow(write, now))
1779
+ };
1780
+ }
1781
+ function rejectReviewOverflow(write, now) {
1782
+ return {
1783
+ ...write,
1784
+ status: "rejected",
1785
+ reviewRequired: false,
1786
+ rejectedAt: now,
1787
+ rejectionReason: `review-queue-overflow:${write.sourceType}`
1788
+ };
1789
+ }
1790
+ function createStagedModerationWrite(candidate, policy, now, index) {
1791
+ const confidence = clamp3(candidate.confidence, 0, 1);
1792
+ const minStageConfidence = policy.minStageConfidence ?? DEFAULT_MIN_STAGE_CONFIDENCE;
1793
+ const provenanceValidation = validateAISignalProvenance(candidate);
1794
+ const reviewRequired = requiresReview(candidate, policy);
1795
+ const status = resolveInitialStatus(candidate, confidence, reviewRequired, policy);
1796
+ const finalStatus = confidence < minStageConfidence || !provenanceValidation.valid ? "rejected" : status;
1797
+ const id = candidate.id ?? `staged-write-${index + 1}`;
1798
+ return {
1799
+ ...candidate,
1800
+ id,
1801
+ confidence,
1802
+ evidenceRefs: createEvidenceRefsWithProvenance(candidate),
1803
+ sourceWeight: candidate.sourceWeight ?? policy.defaultSourceWeight ?? 1,
1804
+ status: finalStatus,
1805
+ createdAt: now,
1806
+ stagedAt: finalStatus === "staged" ? now : void 0,
1807
+ committedAt: finalStatus === "committed" ? now : void 0,
1808
+ reviewRequired: finalStatus === "staged" && reviewRequired,
1809
+ reviewQueue: finalStatus === "staged" ? policy.reviewQueueByKind?.[candidate.kind] ?? defaultQueue(candidate) : void 0,
1810
+ rejectedAt: finalStatus === "rejected" ? now : void 0,
1811
+ rejectionReason: createRejectionReason(
1812
+ confidence,
1813
+ minStageConfidence,
1814
+ provenanceValidation.errors
1815
+ ),
1816
+ expiresAt: candidate.expiresAt ?? (finalStatus === "staged" && policy.stagedExpiresInMs ? now + policy.stagedExpiresInMs : void 0)
1817
+ };
1818
+ }
1819
+ function createEvidenceRefsWithProvenance(candidate) {
1820
+ const provenanceRef = createAISignalProvenanceEvidenceRef(candidate);
1821
+ return provenanceRef ? [.../* @__PURE__ */ new Set([...candidate.evidenceRefs ?? [], provenanceRef])] : [...candidate.evidenceRefs ?? []];
1822
+ }
1823
+ function createRejectionReason(confidence, minStageConfidence, provenanceErrors) {
1824
+ if (provenanceErrors.length > 0) return `missing-ai-provenance:${provenanceErrors.join(",")}`;
1825
+ if (confidence < minStageConfidence) return "below-min-stage-confidence";
1826
+ return void 0;
1827
+ }
1828
+ function resolveInitialStatus(candidate, confidence, reviewRequired, policy) {
1829
+ if (reviewRequired) return "staged";
1830
+ const autoCommitSources = new Set(policy.autoCommitSources ?? DEFAULT_AUTO_COMMIT_SOURCES);
1831
+ const autoCommitConfidence = policy.autoCommitConfidence ?? DEFAULT_AUTO_COMMIT_CONFIDENCE;
1832
+ return autoCommitSources.has(candidate.sourceType) && confidence >= autoCommitConfidence ? "committed" : "staged";
1833
+ }
1834
+ function requiresReview(candidate, policy) {
1835
+ if (candidate.sourceType === "crawler" && policy.allowCrawlerAutoCommit !== true) {
1836
+ return true;
1837
+ }
1838
+ const reviewSources = new Set(policy.requireReviewSources ?? DEFAULT_REVIEW_SOURCES);
1839
+ return reviewSources.has(candidate.sourceType);
1840
+ }
1841
+ function createReviewTaskForStagedWrite(write, now) {
1842
+ const queue = write.reviewQueue ?? defaultQueue(write);
1843
+ return {
1844
+ id: `review-${write.id}`,
1845
+ stagedWriteId: write.id,
1846
+ targetId: write.targetId,
1847
+ queue,
1848
+ priority: reviewPriority(write, now),
1849
+ reasons: reviewReasons(write)
1850
+ };
1851
+ }
1852
+ function defaultQueue(write) {
1853
+ return write.kind === "quality-signal" ? "quality" : "safety";
1854
+ }
1855
+ function reviewPriority(write, now) {
1856
+ const confidencePriority = Math.round(clamp3(write.confidence, 0, 1) * 70);
1857
+ const expiryPriority = write.expiresAt && write.expiresAt < now + 24 * 60 * 60 * 1e3 ? 20 : 0;
1858
+ const cloudPriority = write.sourceType === "cloud-ai" ? 10 : 0;
1859
+ return clamp3(confidencePriority + expiryPriority + cloudPriority, 0, 100);
1860
+ }
1861
+ function reviewReasons(write) {
1862
+ return [
1863
+ write.sourceType === "local-ai" || write.sourceType === "cloud-ai" ? "ai-generated" : null,
1864
+ write.sourceType === "crawler" ? "untrusted-crawl" : null,
1865
+ write.kind === "quality-signal" ? "quality-signal" : "moderation-label",
1866
+ `source:${write.sourceType}`,
1867
+ `confidence:${write.confidence.toFixed(2)}`
1868
+ ].filter((reason) => reason !== null);
1869
+ }
1870
+ function clamp3(value, min, max) {
1871
+ return Math.max(min, Math.min(max, value));
1872
+ }
1873
+
1874
+ // src/appeals.ts
1875
+ function createAppealEffect(input) {
1876
+ const evidenceRefs = createAppealEvidenceRefs(input);
1877
+ const baseAnnotation = {
1878
+ appealId: input.appealId,
1879
+ targetId: input.targetId,
1880
+ status: input.status,
1881
+ action: resolveAppealEffectAction(input),
1882
+ appellantDID: input.appellantDID,
1883
+ reviewerDID: input.reviewerDID,
1884
+ decisionId: input.decisionId,
1885
+ appealedLabelId: input.appealedLabelId,
1886
+ resolution: input.resolution,
1887
+ evidenceRefs
1888
+ };
1889
+ if (baseAnnotation.action !== "reverse") {
1890
+ return {
1891
+ action: baseAnnotation.action,
1892
+ annotation: baseAnnotation,
1893
+ labelsToApply: [],
1894
+ reasons: createAppealReasons(baseAnnotation)
1895
+ };
1896
+ }
1897
+ return {
1898
+ action: "reverse",
1899
+ annotation: baseAnnotation,
1900
+ labelsToApply: [
1901
+ {
1902
+ id: `appeal-reversal:${input.appealId}`,
1903
+ value: "safe",
1904
+ sourceDID: input.reviewerDID,
1905
+ sourceWeight: clamp4(input.sourceWeight ?? 1, 0, 10),
1906
+ confidence: clamp4(input.confidence ?? 1, 0, 1),
1907
+ expiresAt: input.expiresAt,
1908
+ evidenceRefs,
1909
+ negates: input.appealedLabelId
1910
+ }
1911
+ ],
1912
+ reasons: createAppealReasons(baseAnnotation)
1913
+ };
1914
+ }
1915
+ function resolveAppealEffectAction(input) {
1916
+ if (input.status !== "accepted") {
1917
+ return input.status === "rejected" ? "annotate" : "none";
1918
+ }
1919
+ if (input.action !== "reverse") return "annotate";
1920
+ if (!input.reviewerDID || !input.appealedLabelId) return "annotate";
1921
+ return "reverse";
1922
+ }
1923
+ function createAppealEvidenceRefs(input) {
1924
+ return [
1925
+ `appeal:${input.appealId}`,
1926
+ input.decisionId ? `decision:${input.decisionId}` : null,
1927
+ input.appealedLabelId ? `label:${input.appealedLabelId}` : null,
1928
+ ...input.evidenceRefs ?? []
1929
+ ].filter((ref) => ref !== null);
1930
+ }
1931
+ function createAppealReasons(annotation) {
1932
+ return [
1933
+ `appeal:${annotation.status}`,
1934
+ annotation.action === "reverse" ? "appeal:reversal-label" : null,
1935
+ annotation.action === "annotate" ? "appeal:annotation-only" : null,
1936
+ annotation.action === "none" ? "appeal:no-effect" : null
1937
+ ].filter((reason) => reason !== null);
1938
+ }
1939
+ function clamp4(value, min, max) {
1940
+ return Math.max(min, Math.min(max, value));
1941
+ }
1942
+
1943
+ // src/policy-blocks.ts
1944
+ import { base64ToBytes, bytesToBase64, sign, verify } from "@xnetjs/crypto";
1945
+ import { parseDID } from "@xnetjs/identity";
1946
+ var encoder2 = new TextEncoder();
1947
+ var isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1948
+ var toJsonValue = (value) => {
1949
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1950
+ return value;
1951
+ }
1952
+ if (Array.isArray(value)) {
1953
+ return value.map(toJsonValue);
1954
+ }
1955
+ if (isRecord(value)) {
1956
+ return Object.fromEntries(
1957
+ Object.entries(value).filter(([, entryValue]) => typeof entryValue !== "undefined").sort(([a], [b]) => a.localeCompare(b)).map(([key, entryValue]) => [key, toJsonValue(entryValue)])
1958
+ );
1959
+ }
1960
+ return String(value);
1961
+ };
1962
+ var canonicalizePolicyBlockList = (list) => JSON.stringify(toJsonValue(list));
1963
+ var policyBlockListSigningBytes = (list) => encoder2.encode(canonicalizePolicyBlockList(list));
1964
+ var isSignedPolicyBlockList = (list) => isRecord(list) && list.v === 1 && list.kind === "xnet.policy.block-list" && isRecord(list.signature) && list.signature.alg === "Ed25519" && typeof list.signature.value === "string";
1965
+ var createPolicyBlockList = (input) => {
1966
+ const now = Date.now();
1967
+ return {
1968
+ v: 1,
1969
+ kind: "xnet.policy.block-list",
1970
+ createdAt: input.createdAt ?? now,
1971
+ updatedAt: input.updatedAt ?? input.createdAt ?? now,
1972
+ id: input.id,
1973
+ title: input.title,
1974
+ scope: input.scope,
1975
+ issuerDID: input.issuerDID,
1976
+ entries: input.entries
1977
+ };
1978
+ };
1979
+ var signPolicyBlockList = (list, signingKey) => ({
1980
+ ...list,
1981
+ signature: {
1982
+ alg: "Ed25519",
1983
+ value: bytesToBase64(sign(policyBlockListSigningBytes(list), signingKey))
1984
+ }
1985
+ });
1986
+ var unsignedPolicyBlockList = (list) => ({
1987
+ v: list.v,
1988
+ kind: list.kind,
1989
+ id: list.id,
1990
+ title: list.title,
1991
+ scope: list.scope,
1992
+ issuerDID: list.issuerDID,
1993
+ createdAt: list.createdAt,
1994
+ updatedAt: list.updatedAt,
1995
+ entries: list.entries
1996
+ });
1997
+ var verifySignedPolicyBlockList = (list) => {
1998
+ const errors = [];
1999
+ try {
2000
+ const publicKey = parseDID(list.issuerDID);
2001
+ const signature = base64ToBytes(list.signature.value);
2002
+ const valid = verify(
2003
+ policyBlockListSigningBytes(unsignedPolicyBlockList(list)),
2004
+ signature,
2005
+ publicKey
2006
+ );
2007
+ if (!valid) {
2008
+ errors.push("invalid-signature");
2009
+ }
2010
+ } catch (err) {
2011
+ errors.push(err instanceof Error ? err.message : String(err));
2012
+ }
2013
+ return {
2014
+ valid: errors.length === 0,
2015
+ errors
2016
+ };
2017
+ };
2018
+ var policyBlockEntryIsActive = (entry, now = Date.now()) => typeof entry.expiresAt !== "number" || entry.expiresAt > now;
2019
+ var auditPolicyBlockEntries = (list, now = Date.now()) => list.entries.map((entry) => {
2020
+ const active = policyBlockEntryIsActive(entry, now);
2021
+ return {
2022
+ ...entry,
2023
+ active,
2024
+ expired: !active
2025
+ };
2026
+ });
2027
+ var findPolicyBlockAuditEntry = (list, subject, subjectType, now = Date.now()) => auditPolicyBlockEntries(list, now).find(
2028
+ (entry) => entry.subject === subject && entry.subjectType === subjectType
2029
+ ) ?? null;
2030
+ var activePolicyBlockEntries = (list, now = Date.now()) => list.entries.filter((entry) => policyBlockEntryIsActive(entry, now));
2031
+ var findPolicyBlockEntry = (list, subject, subjectType, now = Date.now()) => activePolicyBlockEntries(list, now).find(
2032
+ (entry) => entry.subject === subject && entry.subjectType === subjectType
2033
+ ) ?? null;
2034
+ var policyBlockOverrideEntryIsActive = (entry, now = Date.now()) => typeof entry.expiresAt !== "number" || entry.expiresAt > now;
2035
+ var resolveSubscribedPolicyBlockLists = (input) => {
2036
+ const now = input.now ?? Date.now();
2037
+ const activeOverrides = (input.localOverrides ?? []).filter(
2038
+ (entry) => policyBlockOverrideEntryIsActive(entry, now)
2039
+ );
2040
+ const resolvedEntries = input.lists.flatMap(
2041
+ (list) => activePolicyBlockEntries(list, now).map(
2042
+ (entry) => resolvePolicyBlockEntry(list, entry, activeOverrides)
2043
+ )
2044
+ );
2045
+ return {
2046
+ enforcedEntries: resolvedEntries.filter((entry) => !entry.overridden),
2047
+ overriddenEntries: resolvedEntries.filter((entry) => entry.overridden),
2048
+ activeOverrides
2049
+ };
2050
+ };
2051
+ function resolvePolicyBlockEntry(list, entry, overrides) {
2052
+ const matchingOverrides = overrides.filter(
2053
+ (override) => override.subject === entry.subject && override.subjectType === entry.subjectType
2054
+ );
2055
+ return {
2056
+ ...entry,
2057
+ listId: list.id,
2058
+ listScope: list.scope,
2059
+ issuerDID: list.issuerDID,
2060
+ overridden: matchingOverrides.length > 0,
2061
+ overrideRefs: matchingOverrides.map(policyBlockOverrideRef)
2062
+ };
2063
+ }
2064
+ function policyBlockOverrideRef(entry) {
2065
+ return entry.id ? `policy-override:${entry.scope}:${entry.id}` : `policy-override:${entry.scope}:${entry.subjectType}:${entry.subject}`;
2066
+ }
2067
+
2068
+ // src/hub-policy-offer.ts
2069
+ import { base64ToBytes as base64ToBytes2, bytesToBase64 as bytesToBase642, sign as sign2, verify as verify2 } from "@xnetjs/crypto";
2070
+ import { parseDID as parseDID2 } from "@xnetjs/identity";
2071
+ var encoder3 = new TextEncoder();
2072
+ var DEFAULT_AI_REVIEW_SETTINGS = {
2073
+ localModelsEnabled: true,
2074
+ cloudModelsEnabled: false,
2075
+ rawContentToCloudAllowed: false
2076
+ };
2077
+ var DEFAULT_LABEL_SETTINGS = {
2078
+ trustedLabelerDIDs: [],
2079
+ subscribedPolicyListIds: [],
2080
+ allowLabelNegation: true
2081
+ };
2082
+ var DEFAULT_MODERATION_SETTINGS = {
2083
+ mode: "local-deterministic",
2084
+ requireSignedWrites: true,
2085
+ rejectUnsignedFederation: true,
2086
+ quarantineFirstContact: true,
2087
+ allowLocalOverride: true,
2088
+ publishLabelExplanations: true,
2089
+ defaultVisibility: "warn",
2090
+ defaultReach: "demote",
2091
+ aiReview: DEFAULT_AI_REVIEW_SETTINGS,
2092
+ labels: DEFAULT_LABEL_SETTINGS
2093
+ };
2094
+ var createHubPolicyServiceOffer = (input) => {
2095
+ const now = Date.now();
2096
+ const createdAt = input.createdAt ?? now;
2097
+ const moderation = mergeModerationSettings(input.moderation);
2098
+ return {
2099
+ v: 1,
2100
+ kind: "xnet.hub.policy-service-offer",
2101
+ id: input.id,
2102
+ hubDID: input.hubDID,
2103
+ issuerDID: input.issuerDID,
2104
+ title: input.title,
2105
+ createdAt,
2106
+ updatedAt: input.updatedAt ?? createdAt,
2107
+ expiresAt: input.expiresAt,
2108
+ moderation,
2109
+ services: input.services,
2110
+ budgetHints: input.budgetHints ?? [],
2111
+ policyRefs: input.policyRefs ?? [],
2112
+ operatorContact: input.operatorContact,
2113
+ appealChannels: input.appealChannels ?? []
2114
+ };
2115
+ };
2116
+ var canonicalizeHubPolicyServiceOffer = (offer) => JSON.stringify(toJsonValue2(offer));
2117
+ var hubPolicyServiceOfferSigningBytes = (offer) => encoder3.encode(canonicalizeHubPolicyServiceOffer(offer));
2118
+ var signHubPolicyServiceOffer = (offer, signingKey) => ({
2119
+ ...offer,
2120
+ signature: {
2121
+ alg: "Ed25519",
2122
+ value: bytesToBase642(sign2(hubPolicyServiceOfferSigningBytes(offer), signingKey))
2123
+ }
2124
+ });
2125
+ var unsignedHubPolicyServiceOffer = (offer) => ({
2126
+ v: offer.v,
2127
+ kind: offer.kind,
2128
+ id: offer.id,
2129
+ hubDID: offer.hubDID,
2130
+ issuerDID: offer.issuerDID,
2131
+ title: offer.title,
2132
+ createdAt: offer.createdAt,
2133
+ updatedAt: offer.updatedAt,
2134
+ expiresAt: offer.expiresAt,
2135
+ moderation: offer.moderation,
2136
+ services: offer.services,
2137
+ budgetHints: offer.budgetHints,
2138
+ policyRefs: offer.policyRefs,
2139
+ operatorContact: offer.operatorContact,
2140
+ appealChannels: offer.appealChannels
2141
+ });
2142
+ var isSignedHubPolicyServiceOffer = (offer) => isRecord2(offer) && offer.v === 1 && offer.kind === "xnet.hub.policy-service-offer" && isRecord2(offer.signature) && offer.signature.alg === "Ed25519" && typeof offer.signature.value === "string";
2143
+ var validateHubPolicyServiceOffer = (offer, now = Date.now()) => {
2144
+ const errors = [
2145
+ ...requiredString("id", offer.id),
2146
+ ...requiredString("hubDID", offer.hubDID),
2147
+ ...requiredString("issuerDID", offer.issuerDID),
2148
+ ...validateTimestamps(offer, now),
2149
+ ...validateModerationSettings(offer.moderation),
2150
+ ...offer.services.length === 0 ? ["services-required"] : [],
2151
+ ...offer.budgetHints.flatMap(validateBudgetHint),
2152
+ ...validateOperatorContact(offer.operatorContact),
2153
+ ...validateAppealChannels(offer)
2154
+ ];
2155
+ return { valid: errors.length === 0, errors };
2156
+ };
2157
+ var verifySignedHubPolicyServiceOffer = (offer, now = Date.now()) => {
2158
+ const unsigned = unsignedHubPolicyServiceOffer(offer);
2159
+ const errors = [...validateHubPolicyServiceOffer(unsigned, now).errors];
2160
+ try {
2161
+ const publicKey = parseDID2(offer.issuerDID);
2162
+ const signature = base64ToBytes2(offer.signature.value);
2163
+ const valid = verify2(hubPolicyServiceOfferSigningBytes(unsigned), signature, publicKey);
2164
+ if (!valid) {
2165
+ errors.push("invalid-signature");
2166
+ }
2167
+ } catch (err) {
2168
+ errors.push(err instanceof Error ? err.message : String(err));
2169
+ }
2170
+ return {
2171
+ valid: errors.length === 0,
2172
+ errors
2173
+ };
2174
+ };
2175
+ var activeHubPolicyServices = (offer) => offer.services.filter((service) => service.enabled);
2176
+ var publicAppealChannels = (offer) => offer.appealChannels.filter(
2177
+ (channel) => ["email", "web-form", "xnet-message", "external-ticket"].includes(channel.kind)
2178
+ );
2179
+ function mergeModerationSettings(settings) {
2180
+ return {
2181
+ ...DEFAULT_MODERATION_SETTINGS,
2182
+ ...settings,
2183
+ aiReview: {
2184
+ ...DEFAULT_AI_REVIEW_SETTINGS,
2185
+ ...settings?.aiReview
2186
+ },
2187
+ labels: {
2188
+ ...DEFAULT_LABEL_SETTINGS,
2189
+ ...settings?.labels
2190
+ }
2191
+ };
2192
+ }
2193
+ var isRecord2 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
2194
+ var toJsonValue2 = (value) => {
2195
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2196
+ return value;
2197
+ }
2198
+ if (Array.isArray(value)) {
2199
+ return value.map(toJsonValue2);
2200
+ }
2201
+ if (isRecord2(value)) {
2202
+ return Object.fromEntries(
2203
+ Object.entries(value).filter(([, entryValue]) => typeof entryValue !== "undefined").sort(([a], [b]) => a.localeCompare(b)).map(([key, entryValue]) => [key, toJsonValue2(entryValue)])
2204
+ );
2205
+ }
2206
+ return String(value);
2207
+ };
2208
+ function requiredString(field, value) {
2209
+ return value.trim().length === 0 ? [`${field}-required`] : [];
2210
+ }
2211
+ function validateTimestamps(offer, now) {
2212
+ return [
2213
+ offer.updatedAt < offer.createdAt ? "updated-before-created" : null,
2214
+ typeof offer.expiresAt === "number" && offer.expiresAt <= now ? "expired" : null,
2215
+ typeof offer.expiresAt === "number" && offer.expiresAt <= offer.createdAt ? "expires-before-created" : null
2216
+ ].filter((error) => error !== null);
2217
+ }
2218
+ function validateModerationSettings(settings) {
2219
+ return [
2220
+ settings.aiReview.rawContentToCloudAllowed && !settings.aiReview.cloudModelsEnabled ? "raw-cloud-review-without-cloud-models" : null,
2221
+ settings.labels.maxLabelsPerSubject !== void 0 && settings.labels.maxLabelsPerSubject <= 0 ? "max-labels-per-subject-invalid" : null
2222
+ ].filter((error) => error !== null);
2223
+ }
2224
+ function validateBudgetHint(hint) {
2225
+ return [
2226
+ hint.name.trim().length === 0 ? "budget-name-required" : null,
2227
+ hint.scope.trim().length === 0 ? "budget-scope-required" : null,
2228
+ hint.unitsPerWindow <= 0 ? "budget-units-invalid" : null,
2229
+ hint.windowMs <= 0 ? "budget-window-invalid" : null
2230
+ ].filter((error) => error !== null);
2231
+ }
2232
+ function validateOperatorContact(contact) {
2233
+ if (!contact) return [];
2234
+ return [
2235
+ contact.responseTimeHours !== void 0 && contact.responseTimeHours <= 0 ? "operator-response-time-invalid" : null,
2236
+ contact.email !== void 0 && !contact.email.includes("@") ? "operator-email-invalid" : null
2237
+ ].filter((error) => error !== null);
2238
+ }
2239
+ function validateAppealChannels(offer) {
2240
+ const appealServiceEnabled = offer.services.some(
2241
+ (service) => service.service === "appeal" && service.enabled
2242
+ );
2243
+ return [
2244
+ appealServiceEnabled && offer.appealChannels.length === 0 ? "appeal-channel-required" : null,
2245
+ ...offer.appealChannels.flatMap(validateAppealChannel)
2246
+ ].filter((error) => error !== null);
2247
+ }
2248
+ function validateAppealChannel(channel) {
2249
+ return [
2250
+ channel.kind === "web-form" && !channel.url ? "appeal-web-form-url-required" : null,
2251
+ channel.kind === "external-ticket" && !channel.url ? "appeal-ticket-url-required" : null,
2252
+ channel.kind === "email" && !channel.email ? "appeal-email-required" : null,
2253
+ channel.kind === "xnet-message" && !channel.recipientDID ? "appeal-recipient-did-required" : null,
2254
+ channel.email !== void 0 && !channel.email.includes("@") ? "appeal-email-invalid" : null,
2255
+ channel.maxResponseTimeHours !== void 0 && channel.maxResponseTimeHours <= 0 ? "appeal-response-time-invalid" : null,
2256
+ channel.minResponseTimeHours !== void 0 && channel.maxResponseTimeHours !== void 0 && channel.minResponseTimeHours > channel.maxResponseTimeHours ? "appeal-response-window-invalid" : null
2257
+ ].filter((error) => error !== null);
2258
+ }
2259
+
2260
+ // src/sensitivity.ts
2261
+ var sensitivityLabels = [
2262
+ { id: "sexual", name: "Sexually suggestive", defaultVisibility: "warn" },
2263
+ { id: "nudity", name: "Non-sexual nudity", defaultVisibility: "show" },
2264
+ { id: "porn", name: "Explicit / pornographic", defaultVisibility: "hide" },
2265
+ { id: "graphic-media", name: "Graphic / violent", defaultVisibility: "warn" }
2266
+ ];
2267
+ var SENSITIVITY_LABEL_VALUES = sensitivityLabels.map(
2268
+ (label) => label.id
2269
+ );
2270
+ var SENSITIVITY_LABEL_SET = new Set(SENSITIVITY_LABEL_VALUES);
2271
+ function isSensitivityLabelValue(value) {
2272
+ return SENSITIVITY_LABEL_SET.has(value);
2273
+ }
2274
+ var SENSITIVITY_SOURCE_WEIGHT = {
2275
+ self: 0.5,
2276
+ ml: 0.3,
2277
+ report: 0.15,
2278
+ labeler: 0.05
2279
+ };
2280
+ var SENSITIVITY_PRESENCE_FLOOR = 0.15;
2281
+ var DEFAULT_SENSITIVITY_PREFERENCES = {
2282
+ adultContentEnabled: false,
2283
+ ageConfirmed: false,
2284
+ labels: {},
2285
+ blurUnsolicitedMedia: true
2286
+ };
2287
+ function buildSensitivityLabel(input) {
2288
+ return {
2289
+ id: input.id,
2290
+ value: input.value,
2291
+ sourceDID: input.sourceDID,
2292
+ sourceWeight: SENSITIVITY_SOURCE_WEIGHT[input.source],
2293
+ confidence: clamp013(input.confidence),
2294
+ expiresAt: input.expiresAt,
2295
+ evidenceRefs: input.evidenceRefs
2296
+ };
2297
+ }
2298
+ function activeSensitivityLabels(labels, now) {
2299
+ const active = labels.filter(
2300
+ (label) => isSensitivityLabelValue(label.value) && (label.expiresAt === void 0 || label.expiresAt > now)
2301
+ );
2302
+ const negated = new Set(
2303
+ labels.flatMap((label) => label.negates !== void 0 ? [label.negates] : [])
2304
+ );
2305
+ return active.filter((label) => label.id === void 0 || !negated.has(label.id));
2306
+ }
2307
+ function assessSensitivity(labels, options = {}) {
2308
+ const now = options.now ?? Date.now();
2309
+ const floor = options.presenceFloor ?? SENSITIVITY_PRESENCE_FLOOR;
2310
+ const scores = /* @__PURE__ */ new Map();
2311
+ for (const label of activeSensitivityLabels(labels, now)) {
2312
+ const value = label.value;
2313
+ scores.set(value, (scores.get(value) ?? 0) + label.confidence * label.sourceWeight);
2314
+ }
2315
+ const present = /* @__PURE__ */ new Map();
2316
+ for (const [value, score] of scores) {
2317
+ if (score >= floor) {
2318
+ present.set(value, score);
2319
+ }
2320
+ }
2321
+ return { present, values: [...present.keys()] };
2322
+ }
2323
+ var VISIBILITY_RANK = {
2324
+ show: 0,
2325
+ warn: 1,
2326
+ blur: 2,
2327
+ hide: 3
2328
+ };
2329
+ function strictestVisibility(a, b) {
2330
+ return VISIBILITY_RANK[a] >= VISIBILITY_RANK[b] ? a : b;
2331
+ }
2332
+ var ADULT_LABELS = /* @__PURE__ */ new Set(["sexual", "nudity", "porn"]);
2333
+ function defaultVisibilityFor(value) {
2334
+ return sensitivityLabels.find((label) => label.id === value).defaultVisibility;
2335
+ }
2336
+ function decideSensitivityVisibility(labels, preferences = DEFAULT_SENSITIVITY_PREFERENCES, options = {}) {
2337
+ const assessment = assessSensitivity(labels, options);
2338
+ const adultEnabled = preferences.adultContentEnabled && preferences.ageConfirmed;
2339
+ let visibility = "show";
2340
+ for (const value of assessment.values) {
2341
+ if (!adultEnabled && ADULT_LABELS.has(value)) {
2342
+ visibility = strictestVisibility(visibility, "hide");
2343
+ continue;
2344
+ }
2345
+ const pref = preferences.labels[value] ?? defaultVisibilityFor(value);
2346
+ visibility = strictestVisibility(visibility, pref);
2347
+ }
2348
+ if (visibility === "show" && options.unsolicitedMedia === true && (preferences.blurUnsolicitedMedia ?? true)) {
2349
+ return "blur";
2350
+ }
2351
+ return visibility;
2352
+ }
2353
+ function explainSensitivityVisibility(labels, preferences = DEFAULT_SENSITIVITY_PREFERENCES, options = {}) {
2354
+ const assessment = assessSensitivity(labels, options);
2355
+ const adultEnabled = preferences.adultContentEnabled && preferences.ageConfirmed;
2356
+ const reasons = [];
2357
+ let visibility = "show";
2358
+ for (const value of assessment.values) {
2359
+ if (!adultEnabled && ADULT_LABELS.has(value)) {
2360
+ visibility = strictestVisibility(visibility, "hide");
2361
+ reasons.push({ label: value, effect: "hide", cause: "adult-disabled" });
2362
+ continue;
2363
+ }
2364
+ const pref = preferences.labels[value] ?? defaultVisibilityFor(value);
2365
+ visibility = strictestVisibility(visibility, pref);
2366
+ reasons.push({ label: value, effect: pref, cause: "dial" });
2367
+ }
2368
+ if (visibility === "show" && options.unsolicitedMedia === true && (preferences.blurUnsolicitedMedia ?? true)) {
2369
+ visibility = "blur";
2370
+ reasons.push({ effect: "blur", cause: "unsolicited-media" });
2371
+ }
2372
+ return { visibility, reasons };
2373
+ }
2374
+ function sensitivityOverride(labels, preferences = DEFAULT_SENSITIVITY_PREFERENCES, options = {}) {
2375
+ const visibility = decideSensitivityVisibility(labels, preferences, options);
2376
+ if (visibility === "show") {
2377
+ return void 0;
2378
+ }
2379
+ return { scope: "user", visibility, reason: "sensitivity-preference" };
2380
+ }
2381
+ function resolveContentVisibility(decision, labels, preferences = DEFAULT_SENSITIVITY_PREFERENCES, options = {}) {
2382
+ const sensitivity = decideSensitivityVisibility(labels, preferences, options);
2383
+ return strictestVisibility(decision.visibility, sensitivity);
2384
+ }
2385
+ function clamp013(value) {
2386
+ return Math.min(1, Math.max(0, value));
2387
+ }
2388
+
2389
+ // src/image-fingerprint.ts
2390
+ function resample(image, size) {
2391
+ const out = new Array(size * size).fill(0);
2392
+ if (image.width === 0 || image.height === 0) return out;
2393
+ for (let oy = 0; oy < size; oy++) {
2394
+ const y0 = Math.floor(oy * image.height / size);
2395
+ const y1 = Math.max(y0 + 1, Math.floor((oy + 1) * image.height / size));
2396
+ for (let ox = 0; ox < size; ox++) {
2397
+ const x0 = Math.floor(ox * image.width / size);
2398
+ const x1 = Math.max(x0 + 1, Math.floor((ox + 1) * image.width / size));
2399
+ let sum = 0;
2400
+ let count = 0;
2401
+ for (let y = y0; y < y1 && y < image.height; y++) {
2402
+ for (let x = x0; x < x1 && x < image.width; x++) {
2403
+ sum += image.luma[y * image.width + x] ?? 0;
2404
+ count++;
2405
+ }
2406
+ }
2407
+ out[oy * size + ox] = count > 0 ? sum / count : 0;
2408
+ }
2409
+ }
2410
+ return out;
2411
+ }
2412
+ function bitsToHex(bits) {
2413
+ let hex = "";
2414
+ for (let i = 0; i < bits.length; i += 4) {
2415
+ let nibble = 0;
2416
+ for (let b = 0; b < 4; b++) {
2417
+ if (bits[i + b]) nibble |= 1 << 3 - b;
2418
+ }
2419
+ hex += nibble.toString(16);
2420
+ }
2421
+ return hex;
2422
+ }
2423
+ function averageHash(image) {
2424
+ const px = resample(image, 8);
2425
+ const mean = px.reduce((sum, value) => sum + value, 0) / px.length;
2426
+ return bitsToHex(px.map((value) => value >= mean));
2427
+ }
2428
+ function differenceHash(image) {
2429
+ const w = 9;
2430
+ const h = 8;
2431
+ const px = resample(image, Math.max(w, h));
2432
+ const grid = resampleGrid(image, w, h);
2433
+ void px;
2434
+ const bits = [];
2435
+ for (let y = 0; y < h; y++) {
2436
+ for (let x = 0; x < w - 1; x++) {
2437
+ bits.push(grid[y * w + x] > grid[y * w + x + 1]);
2438
+ }
2439
+ }
2440
+ return bitsToHex(bits);
2441
+ }
2442
+ function resampleGrid(image, w, h) {
2443
+ const out = new Array(w * h).fill(0);
2444
+ if (image.width === 0 || image.height === 0) return out;
2445
+ for (let oy = 0; oy < h; oy++) {
2446
+ const y = Math.min(image.height - 1, Math.floor(oy * image.height / h));
2447
+ for (let ox = 0; ox < w; ox++) {
2448
+ const x = Math.min(image.width - 1, Math.floor(ox * image.width / w));
2449
+ out[oy * w + ox] = image.luma[y * image.width + x] ?? 0;
2450
+ }
2451
+ }
2452
+ return out;
2453
+ }
2454
+ function perceptualHash(image) {
2455
+ const N = 32;
2456
+ const px = resample(image, N);
2457
+ const dct = dct2d(px, N);
2458
+ const coeffs = [];
2459
+ for (let y = 0; y < 8; y++) {
2460
+ for (let x = 0; x < 8; x++) {
2461
+ if (x === 0 && y === 0) continue;
2462
+ coeffs.push(dct[y * N + x]);
2463
+ }
2464
+ }
2465
+ const median = medianOf(coeffs);
2466
+ return bitsToHex(coeffs.map((value) => value > median));
2467
+ }
2468
+ function dct2d(input, n) {
2469
+ const cos = [];
2470
+ for (let u = 0; u < n; u++) {
2471
+ cos[u] = [];
2472
+ for (let x = 0; x < n; x++) {
2473
+ cos[u][x] = Math.cos((2 * x + 1) * u * Math.PI / (2 * n));
2474
+ }
2475
+ }
2476
+ const temp = new Array(n * n).fill(0);
2477
+ for (let y = 0; y < n; y++) {
2478
+ for (let u = 0; u < n; u++) {
2479
+ let sum = 0;
2480
+ for (let x = 0; x < n; x++) sum += input[y * n + x] * cos[u][x];
2481
+ temp[y * n + u] = sum;
2482
+ }
2483
+ }
2484
+ const out = new Array(n * n).fill(0);
2485
+ for (let u = 0; u < n; u++) {
2486
+ for (let v = 0; v < n; v++) {
2487
+ let sum = 0;
2488
+ for (let y = 0; y < n; y++) sum += temp[y * n + v] * cos[u][y];
2489
+ out[u * n + v] = sum;
2490
+ }
2491
+ }
2492
+ return out;
2493
+ }
2494
+ function medianOf(values) {
2495
+ const sorted = [...values].sort((a, b) => a - b);
2496
+ const mid = Math.floor(sorted.length / 2);
2497
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
2498
+ }
2499
+ var HEX_BITS = (() => {
2500
+ const map = {};
2501
+ for (let i = 0; i < 16; i++) {
2502
+ map[i.toString(16)] = (i & 1) + (i >> 1 & 1) + (i >> 2 & 1) + (i >> 3 & 1);
2503
+ }
2504
+ return map;
2505
+ })();
2506
+ function hammingDistanceHex(a, b) {
2507
+ const len = Math.min(a.length, b.length);
2508
+ let distance = Math.abs(a.length - b.length) * 4;
2509
+ for (let i = 0; i < len; i++) {
2510
+ distance += HEX_BITS[(parseInt(a[i], 16) ^ parseInt(b[i], 16)).toString(16)] ?? 0;
2511
+ }
2512
+ return distance;
2513
+ }
2514
+ function imageHashSimilarity(a, b) {
2515
+ const bits = Math.max(a.length, b.length) * 4;
2516
+ if (bits === 0) return 1;
2517
+ return 1 - hammingDistanceHex(a, b) / bits;
2518
+ }
2519
+ function matchKnownImageHash(hash2, known, maxHammingBits = 8) {
2520
+ let best = null;
2521
+ for (const entry of known) {
2522
+ const distance = hammingDistanceHex(hash2, entry.hash);
2523
+ if (distance <= maxHammingBits && (best === null || distance < best.distance)) {
2524
+ best = { label: entry.label, source: entry.source, distance };
2525
+ }
2526
+ }
2527
+ return best;
2528
+ }
2529
+
2530
+ // src/local-image-classifier.ts
2531
+ function mapNsfwLabelToSensitivity(label) {
2532
+ const normalized = label.trim().toLowerCase();
2533
+ switch (normalized) {
2534
+ case "porn":
2535
+ case "explicit":
2536
+ case "hentai":
2537
+ return "porn";
2538
+ case "sexy":
2539
+ case "suggestive":
2540
+ case "sexual":
2541
+ return "sexual";
2542
+ case "nudity":
2543
+ case "nude":
2544
+ return "nudity";
2545
+ case "gore":
2546
+ case "graphic":
2547
+ case "graphic-media":
2548
+ case "violence":
2549
+ return "graphic-media";
2550
+ case "neutral":
2551
+ case "normal":
2552
+ case "drawing":
2553
+ case "safe":
2554
+ return null;
2555
+ default:
2556
+ return isSensitivityLabelValue(normalized) ? normalized : null;
2557
+ }
2558
+ }
2559
+ var DEFAULT_THRESHOLD = 0.5;
2560
+ function createNsfwImageClassifier(options) {
2561
+ const id = options.id ?? "local.image.nsfw";
2562
+ const version = options.version ?? "1";
2563
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
2564
+ const provenance = {
2565
+ provider: "local",
2566
+ adapterId: id,
2567
+ adapterVersion: version,
2568
+ model: options.model
2569
+ };
2570
+ return {
2571
+ id,
2572
+ version,
2573
+ model: options.model,
2574
+ supports(input) {
2575
+ const mediaKind = input.metadata?.mediaKind;
2576
+ return typeof mediaKind === "string" && mediaKind.startsWith("image") || input.metadata?.image !== void 0;
2577
+ },
2578
+ async classify(input) {
2579
+ const detections = await options.detect({ metadata: input.metadata });
2580
+ const byValue = /* @__PURE__ */ new Map();
2581
+ for (const detection of detections) {
2582
+ const value = mapNsfwLabelToSensitivity(detection.label);
2583
+ if (value === null || detection.score < threshold) continue;
2584
+ const existing = byValue.get(value);
2585
+ if (!existing || detection.score > existing.score) {
2586
+ byValue.set(value, detection);
2587
+ }
2588
+ }
2589
+ const labels = [...byValue.entries()].map(
2590
+ ([value, detection]) => buildSensitivityLabel({
2591
+ value,
2592
+ source: "ml",
2593
+ confidence: detection.score,
2594
+ sourceDID: options.sourceDid,
2595
+ evidenceRefs: [`nsfw-model:${detection.label}:${detection.score.toFixed(2)}`]
2596
+ })
2597
+ );
2598
+ const signals = labels.map((label) => ({
2599
+ kind: "label",
2600
+ value: label.value,
2601
+ confidence: label.confidence,
2602
+ evidenceRefs: [...label.evidenceRefs ?? []],
2603
+ provenance
2604
+ }));
2605
+ return createLocalClassificationResult({ labels, signals, provenance });
2606
+ }
2607
+ };
2608
+ }
2609
+
2610
+ // src/image-prescreen.ts
2611
+ var DEFAULT_EXPLICIT_WARN_THRESHOLD = 0.7;
2612
+ var DEFAULT_SUGGEST_THRESHOLD = 0.4;
2613
+ var CLEAN_RESULT = {
2614
+ recommendation: "allow",
2615
+ suggestedLabel: null,
2616
+ confidence: 0,
2617
+ labels: []
2618
+ };
2619
+ function strongestSensitivityLabel(labels) {
2620
+ let strongest = null;
2621
+ for (const label of labels) {
2622
+ if (!isSensitivityLabelValue(label.value)) continue;
2623
+ if (!strongest || label.confidence > strongest.confidence) strongest = label;
2624
+ }
2625
+ return strongest;
2626
+ }
2627
+ function prescreenImageLabels(labels, options = {}) {
2628
+ const explicitWarn = options.explicitWarnThreshold ?? DEFAULT_EXPLICIT_WARN_THRESHOLD;
2629
+ const suggest = options.suggestThreshold ?? DEFAULT_SUGGEST_THRESHOLD;
2630
+ const strongest = strongestSensitivityLabel(labels);
2631
+ if (!strongest) return CLEAN_RESULT;
2632
+ const value = strongest.value;
2633
+ const confidence = strongest.confidence;
2634
+ if (value === "porn" && confidence >= explicitWarn) {
2635
+ return { recommendation: "warn-explicit", suggestedLabel: value, confidence, labels };
2636
+ }
2637
+ if (confidence >= suggest) {
2638
+ return { recommendation: "suggest-label", suggestedLabel: value, confidence, labels };
2639
+ }
2640
+ return { recommendation: "allow", suggestedLabel: null, confidence, labels };
2641
+ }
2642
+ async function prescreenImage(classifier, input, options = {}) {
2643
+ if (classifier.supports && !classifier.supports(input)) return CLEAN_RESULT;
2644
+ const result = await classifier.classify(input);
2645
+ return prescreenImageLabels(result.labels, options);
2646
+ }
2647
+
2648
+ // src/public-write-budget.ts
2649
+ function evaluatePublicWriteBudget(input, policy, usage = []) {
2650
+ const now = input.now ?? Date.now();
2651
+ const costUnits = Math.max(0, input.costUnits ?? policy.defaultCostUnits ?? 1);
2652
+ const activeUsage = usage.filter((entry) => entry.resetAt > now);
2653
+ const charges = policy.limits.flatMap(
2654
+ (limit) => createPublicWriteBudgetCharge(input, limit, activeUsage, costUnits, now) ?? []
2655
+ );
2656
+ const exceeded = charges.filter((charge) => charge.usedUnits + costUnits > charge.limitUnits);
2657
+ if (exceeded.length > 0) {
2658
+ return {
2659
+ allowed: false,
2660
+ resource: "require-budget",
2661
+ reasons: exceeded.map((charge) => `budget:${charge.scope}:exceeded`),
2662
+ charges,
2663
+ nextUsage: activeUsage
2664
+ };
2665
+ }
2666
+ return {
2667
+ allowed: true,
2668
+ resource: "normal",
2669
+ reasons: ["budget:accepted"],
2670
+ charges,
2671
+ nextUsage: mergePublicWriteBudgetCharges(activeUsage, charges, costUnits)
2672
+ };
2673
+ }
2674
+ function createPublicWriteBudgetKey(input, scope) {
2675
+ if (scope === "did") return input.did ? `did:${input.did}` : null;
2676
+ if (scope === "hub") return input.hubId ? `hub:${input.hubId}` : null;
2677
+ if (scope === "workspace") {
2678
+ return input.workspaceId ? `workspace:${input.workspaceId}` : null;
2679
+ }
2680
+ if (scope === "surface") return `surface:${input.surface}`;
2681
+ if (scope === "did-surface") {
2682
+ return input.did ? `did:${input.did}:surface:${input.surface}` : null;
2683
+ }
2684
+ if (scope === "hub-surface") {
2685
+ return input.hubId ? `hub:${input.hubId}:surface:${input.surface}` : null;
2686
+ }
2687
+ return input.workspaceId ? `workspace:${input.workspaceId}:surface:${input.surface}` : null;
2688
+ }
2689
+ function createPublicWriteBudgetCharge(input, limit, usage, costUnits, now) {
2690
+ const key = createPublicWriteBudgetKey(input, limit.scope);
2691
+ if (!key) return null;
2692
+ const existing = usage.find((entry) => entry.key === key && entry.scope === limit.scope);
2693
+ return {
2694
+ key,
2695
+ scope: limit.scope,
2696
+ costUnits,
2697
+ usedUnits: existing?.usedUnits ?? 0,
2698
+ limitUnits: limit.unitsPerWindow,
2699
+ resetAt: existing?.resetAt ?? now + limit.windowMs
2700
+ };
2701
+ }
2702
+ function mergePublicWriteBudgetCharges(usage, charges, costUnits) {
2703
+ const chargedKeys = new Set(charges.map((charge) => `${charge.scope}:${charge.key}`));
2704
+ return [
2705
+ ...usage.filter((entry) => !chargedKeys.has(`${entry.scope}:${entry.key}`)),
2706
+ ...charges.map((charge) => ({
2707
+ key: charge.key,
2708
+ scope: charge.scope,
2709
+ usedUnits: charge.usedUnits + costUnits,
2710
+ resetAt: charge.resetAt
2711
+ }))
2712
+ ].sort(
2713
+ (left, right) => left.key.localeCompare(right.key) || left.scope.localeCompare(right.scope)
2714
+ );
2715
+ }
2716
+
2717
+ // src/query-cost-budget.ts
2718
+ function evaluateQueryCostBudget(input, policy, usage = []) {
2719
+ const now = input.now ?? Date.now();
2720
+ const costUnits = Math.max(0, input.costUnits ?? policy.defaultCostUnits ?? 1);
2721
+ const activeUsage = usage.filter((entry) => entry.resetAt > now);
2722
+ const charges = policy.limits.flatMap(
2723
+ (limit) => createQueryCostBudgetCharge(input, limit, activeUsage, costUnits, now) ?? []
2724
+ );
2725
+ const exceeded = charges.filter((charge) => charge.usedUnits + costUnits > charge.limitUnits);
2726
+ if (exceeded.length > 0) {
2727
+ return {
2728
+ allowed: false,
2729
+ resource: "require-budget",
2730
+ reasons: exceeded.map((charge) => `budget:${charge.scope}:exceeded`),
2731
+ charges,
2732
+ nextUsage: activeUsage
2733
+ };
2734
+ }
2735
+ return {
2736
+ allowed: true,
2737
+ resource: "normal",
2738
+ reasons: ["budget:accepted"],
2739
+ charges,
2740
+ nextUsage: mergeQueryCostBudgetCharges(activeUsage, charges, costUnits)
2741
+ };
2742
+ }
2743
+ function createQueryCostBudgetKey(input, scope) {
2744
+ const domain = normalizeDomain(input.domain);
2745
+ const route = normalizeRoute(input.route);
2746
+ if (scope === "actor") return input.actorDID ? `actor:${input.actorDID}` : null;
2747
+ if (scope === "hub") return input.hubId ? `hub:${input.hubId}` : null;
2748
+ if (scope === "workspace") return input.workspaceId ? `workspace:${input.workspaceId}` : null;
2749
+ if (scope === "remote-peer") {
2750
+ return input.remotePeerId ? `remote-peer:${input.remotePeerId}` : null;
2751
+ }
2752
+ if (scope === "domain") return domain ? `domain:${domain}` : null;
2753
+ if (scope === "route") return route ? `route:${route}` : null;
2754
+ if (scope === "work-type") return `work-type:${input.workType}`;
2755
+ if (scope === "hub-work-type") {
2756
+ return input.hubId ? `hub:${input.hubId}:work-type:${input.workType}` : null;
2757
+ }
2758
+ if (scope === "domain-work-type") {
2759
+ return domain ? `domain:${domain}:work-type:${input.workType}` : null;
2760
+ }
2761
+ return input.remotePeerId && route ? `remote-peer:${input.remotePeerId}:route:${route}` : null;
2762
+ }
2763
+ function createQueryCostBudgetCharge(input, limit, usage, costUnits, now) {
2764
+ const key = createQueryCostBudgetKey(input, limit.scope);
2765
+ if (!key) return null;
2766
+ const existing = usage.find((entry) => entry.key === key && entry.scope === limit.scope);
2767
+ return {
2768
+ key,
2769
+ scope: limit.scope,
2770
+ costUnits,
2771
+ usedUnits: existing?.usedUnits ?? 0,
2772
+ limitUnits: limit.unitsPerWindow,
2773
+ resetAt: existing?.resetAt ?? now + limit.windowMs
2774
+ };
2775
+ }
2776
+ function mergeQueryCostBudgetCharges(usage, charges, costUnits) {
2777
+ const chargedKeys = new Set(charges.map((charge) => `${charge.scope}:${charge.key}`));
2778
+ return [
2779
+ ...usage.filter((entry) => !chargedKeys.has(`${entry.scope}:${entry.key}`)),
2780
+ ...charges.map((charge) => ({
2781
+ key: charge.key,
2782
+ scope: charge.scope,
2783
+ usedUnits: charge.usedUnits + costUnits,
2784
+ resetAt: charge.resetAt
2785
+ }))
2786
+ ].sort(
2787
+ (left, right) => left.key.localeCompare(right.key) || left.scope.localeCompare(right.scope)
2788
+ );
2789
+ }
2790
+ function normalizeDomain(domain) {
2791
+ const normalized = domain?.trim().toLowerCase().replace(/^www\./, "");
2792
+ return normalized && normalized.length > 0 ? normalized : null;
2793
+ }
2794
+ function normalizeRoute(route) {
2795
+ const normalized = route?.trim().replace(/\s+/g, "-").toLowerCase();
2796
+ return normalized && normalized.length > 0 ? normalized : null;
2797
+ }
2798
+
2799
+ // src/usage-events.ts
2800
+ import { hashBase64 } from "@xnetjs/crypto";
2801
+ var ABUSE_USAGE_EVENT_KINDS = [
2802
+ "blocked",
2803
+ "throttled",
2804
+ "reviewed",
2805
+ "billable",
2806
+ "sponsored",
2807
+ "reciprocal"
2808
+ ];
2809
+ var ABUSE_USAGE_SETTLEMENTS = [
2810
+ "free",
2811
+ "paid",
2812
+ "sponsored",
2813
+ "reciprocal",
2814
+ "abuse-blocked"
2815
+ ];
2816
+ var DEFAULT_USAGE_EVENT_HASH_SALT = "xnet.abuse.usage.event.v1";
2817
+ function createAbuseUsageEvent(input) {
2818
+ const settlement = input.settlement ?? settlementForKind(input.kind);
2819
+ const units = nonNegative(input.units ?? 1);
2820
+ const costMicroUsd = nonNegative(input.costMicroUsd ?? 0);
2821
+ const sponsoredMicroUsd = sponsoredAmount(input.sponsoredMicroUsd, costMicroUsd, settlement);
2822
+ const reciprocalCreditUnits = reciprocalUnits(input.reciprocalCreditUnits, units, settlement);
2823
+ const eventWithoutId = {
2824
+ kind: input.kind,
2825
+ settlement,
2826
+ surface: input.surface,
2827
+ workType: input.workType,
2828
+ actorHash: hashAbusePeerIdentifier(input.peerId ?? input.actorDID, input.identityHashSalt),
2829
+ remotePeerHash: input.remotePeerId ? hashAbusePeerIdentifier(input.remotePeerId, input.identityHashSalt) : void 0,
2830
+ labelerHash: input.labelerDID ? hashAbusePeerIdentifier(input.labelerDID, input.identityHashSalt) : void 0,
2831
+ hubId: normalizedString(input.hubId),
2832
+ workspaceId: normalizedString(input.workspaceId),
2833
+ domain: normalizeDomain2(input.domain),
2834
+ route: normalizeRoute2(input.route),
2835
+ units,
2836
+ costMicroUsd,
2837
+ billableMicroUsd: settlement === "paid" ? Math.max(0, costMicroUsd - sponsoredMicroUsd) : 0,
2838
+ sponsoredMicroUsd,
2839
+ reciprocalCreditUnits,
2840
+ resource: input.resource,
2841
+ reviewQueue: input.reviewQueue,
2842
+ reasonCodes: dedupe(input.reasonCodes ?? []),
2843
+ policyId: normalizedString(input.policyId),
2844
+ tags: normalizeTags(input.tags ?? []),
2845
+ occurredAt: input.occurredAt ?? Date.now()
2846
+ };
2847
+ return {
2848
+ ...eventWithoutId,
2849
+ id: input.eventId ?? createAbuseUsageEventId(eventWithoutId, input.eventHashSalt)
2850
+ };
2851
+ }
2852
+ function createAbuseUsageEventsFromDecision(input) {
2853
+ const reasonCodes = input.decision.reasons.filter(
2854
+ (reason) => reason !== "accepted"
2855
+ );
2856
+ const baseInput = {
2857
+ surface: input.surface,
2858
+ workType: input.workType,
2859
+ actorDID: input.actorDID,
2860
+ peerId: input.peerId,
2861
+ remotePeerId: input.remotePeerId,
2862
+ labelerDID: input.labelerDID,
2863
+ hubId: input.hubId,
2864
+ workspaceId: input.workspaceId,
2865
+ domain: input.domain,
2866
+ route: input.route,
2867
+ units: input.units,
2868
+ costMicroUsd: input.costMicroUsd,
2869
+ sponsoredMicroUsd: input.sponsoredMicroUsd,
2870
+ reciprocalCreditUnits: input.reciprocalCreditUnits,
2871
+ policyId: input.policyId,
2872
+ tags: input.tags,
2873
+ occurredAt: input.occurredAt,
2874
+ identityHashSalt: input.identityHashSalt,
2875
+ eventHashSalt: input.eventHashSalt,
2876
+ reasonCodes,
2877
+ resource: input.decision.resource
2878
+ };
2879
+ const moderationEvents = [
2880
+ isBlockedDecision(input.decision) ? createAbuseUsageEvent({ ...baseInput, kind: "blocked" }) : null,
2881
+ input.decision.resource === "throttle" ? createAbuseUsageEvent({ ...baseInput, kind: "throttled" }) : null,
2882
+ input.decision.review.required ? createAbuseUsageEvent({
2883
+ ...baseInput,
2884
+ kind: "reviewed",
2885
+ reviewQueue: input.decision.review.queue
2886
+ }) : null
2887
+ ].filter((event) => event !== null);
2888
+ const economicKind = economicKindForDecision(input);
2889
+ const economicEvent = economicKind ? [createAbuseUsageEvent({ ...baseInput, kind: economicKind })] : [];
2890
+ return [...moderationEvents, ...economicEvent];
2891
+ }
2892
+ function createAbuseUsageEventId(event, salt = DEFAULT_USAGE_EVENT_HASH_SALT) {
2893
+ const payload = stableStringify(toStableEventPayload(event));
2894
+ const encoded = new TextEncoder().encode(`${salt}:${payload}`);
2895
+ return `usage_${hashBase64(encoded, "blake3").slice(0, 24)}`;
2896
+ }
2897
+ function summarizeAbuseUsageEvents(events) {
2898
+ const totalUnits = sumEvents(events, "units");
2899
+ const blockedUnits = sumUnitsForKind(events, "blocked");
2900
+ const throttledUnits = sumUnitsForKind(events, "throttled");
2901
+ const reviewedUnits = sumUnitsForKind(events, "reviewed");
2902
+ const automationSavedUnits = blockedUnits + throttledUnits;
2903
+ const appealUnits = sumUnitsForWorkType(events, "appeal");
2904
+ return {
2905
+ totalEvents: events.length,
2906
+ totalUnits,
2907
+ kindCounts: countBy(
2908
+ ABUSE_USAGE_EVENT_KINDS,
2909
+ events.map((event) => event.kind)
2910
+ ),
2911
+ settlementCounts: countBy(
2912
+ ABUSE_USAGE_SETTLEMENTS,
2913
+ events.map((event) => event.settlement)
2914
+ ),
2915
+ unitsByKind: sumByKey(ABUSE_USAGE_EVENT_KINDS, events, (event) => event.kind, "units"),
2916
+ unitsBySettlement: sumByKey(
2917
+ ABUSE_USAGE_SETTLEMENTS,
2918
+ events,
2919
+ (event) => event.settlement,
2920
+ "units"
2921
+ ),
2922
+ eventsBySurface: countEventValues(events.map((event) => event.surface)),
2923
+ eventsByWorkType: countEventValues(events.map((event) => event.workType)),
2924
+ costMicroUsd: sumEvents(events, "costMicroUsd"),
2925
+ billableMicroUsd: sumEvents(events, "billableMicroUsd"),
2926
+ sponsoredMicroUsd: sumEvents(events, "sponsoredMicroUsd"),
2927
+ reciprocalCreditUnits: sumEvents(events, "reciprocalCreditUnits"),
2928
+ blockedUnits,
2929
+ throttledUnits,
2930
+ reviewedUnits,
2931
+ automationSavedUnits,
2932
+ automationSavedCostMicroUsd: sumCostForKinds(events, ["blocked", "throttled"]),
2933
+ appealUnits,
2934
+ appealCostMicroUsd: sumCostForWorkType(events, "appeal"),
2935
+ automationSavingsRatio: ratio2(automationSavedUnits, totalUnits),
2936
+ reviewLoadRatio: ratio2(reviewedUnits, totalUnits),
2937
+ appealLoadRatio: ratio2(appealUnits, totalUnits)
2938
+ };
2939
+ }
2940
+ function settlementForKind(kind) {
2941
+ if (kind === "blocked") return "abuse-blocked";
2942
+ if (kind === "billable") return "paid";
2943
+ if (kind === "sponsored") return "sponsored";
2944
+ if (kind === "reciprocal") return "reciprocal";
2945
+ return "free";
2946
+ }
2947
+ function isBlockedDecision(decision) {
2948
+ return decision.admission === "reject" || decision.resource === "block-peer";
2949
+ }
2950
+ function economicKindForDecision(input) {
2951
+ if (input.decision.admission !== "accept") return null;
2952
+ if (nonNegative(input.reciprocalCreditUnits ?? 0) > 0) return "reciprocal";
2953
+ if (nonNegative(input.sponsoredMicroUsd ?? 0) > 0) return "sponsored";
2954
+ if (nonNegative(input.costMicroUsd ?? 0) > 0) return "billable";
2955
+ return null;
2956
+ }
2957
+ function sponsoredAmount(amount, costMicroUsd, settlement) {
2958
+ if (settlement === "sponsored") return nonNegative(amount ?? costMicroUsd);
2959
+ return nonNegative(amount ?? 0);
2960
+ }
2961
+ function reciprocalUnits(units, fallbackUnits, settlement) {
2962
+ if (settlement === "reciprocal") return nonNegative(units ?? fallbackUnits);
2963
+ return nonNegative(units ?? 0);
2964
+ }
2965
+ function nonNegative(value) {
2966
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
2967
+ }
2968
+ function dedupe(values) {
2969
+ return [...new Set(values)];
2970
+ }
2971
+ function normalizedString(value) {
2972
+ const normalized = value?.trim();
2973
+ return normalized && normalized.length > 0 ? normalized : void 0;
2974
+ }
2975
+ function normalizeDomain2(domain) {
2976
+ return normalizedString(domain)?.toLowerCase().replace(/^www\./, "");
2977
+ }
2978
+ function normalizeRoute2(route) {
2979
+ return normalizedString(route)?.replace(/\s+/g, "-").toLowerCase();
2980
+ }
2981
+ function normalizeTags(tags) {
2982
+ return [...new Set(tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean))].sort();
2983
+ }
2984
+ function countBy(keys, values) {
2985
+ const initial = createNumberRecord(keys);
2986
+ return values.reduce(
2987
+ (counts, value) => ({
2988
+ ...counts,
2989
+ [value]: counts[value] + 1
2990
+ }),
2991
+ initial
2992
+ );
2993
+ }
2994
+ function countEventValues(values) {
2995
+ return values.reduce(
2996
+ (counts, value) => ({
2997
+ ...counts,
2998
+ [value]: (counts[value] ?? 0) + 1
2999
+ }),
3000
+ {}
3001
+ );
3002
+ }
3003
+ function createNumberRecord(keys) {
3004
+ return keys.reduce(
3005
+ (counts, key) => ({
3006
+ ...counts,
3007
+ [key]: 0
3008
+ }),
3009
+ {}
3010
+ );
3011
+ }
3012
+ function sumByKey(keys, events, getKey, valueKey) {
3013
+ const initial = createNumberRecord(keys);
3014
+ return events.reduce(
3015
+ (totals, event) => ({
3016
+ ...totals,
3017
+ [getKey(event)]: totals[getKey(event)] + event[valueKey]
3018
+ }),
3019
+ initial
3020
+ );
3021
+ }
3022
+ function sumEvents(events, key) {
3023
+ return events.reduce((total, event) => total + event[key], 0);
3024
+ }
3025
+ function sumUnitsForKind(events, kind) {
3026
+ return events.filter((event) => event.kind === kind).reduce((total, event) => total + event.units, 0);
3027
+ }
3028
+ function sumUnitsForWorkType(events, workType) {
3029
+ return events.filter((event) => event.workType === workType).reduce((total, event) => total + event.units, 0);
3030
+ }
3031
+ function sumCostForWorkType(events, workType) {
3032
+ return events.filter((event) => event.workType === workType).reduce((total, event) => total + event.costMicroUsd, 0);
3033
+ }
3034
+ function sumCostForKinds(events, kinds) {
3035
+ return events.filter((event) => kinds.includes(event.kind)).reduce((total, event) => total + event.costMicroUsd, 0);
3036
+ }
3037
+ function ratio2(numerator, denominator) {
3038
+ return denominator > 0 ? numerator / denominator : 0;
3039
+ }
3040
+ function toStableEventPayload(event) {
3041
+ return {
3042
+ kind: event.kind,
3043
+ settlement: event.settlement,
3044
+ surface: event.surface,
3045
+ workType: event.workType,
3046
+ actorHash: event.actorHash,
3047
+ remotePeerHash: event.remotePeerHash ?? null,
3048
+ labelerHash: event.labelerHash ?? null,
3049
+ hubId: event.hubId ?? null,
3050
+ workspaceId: event.workspaceId ?? null,
3051
+ domain: event.domain ?? null,
3052
+ route: event.route ?? null,
3053
+ units: event.units,
3054
+ costMicroUsd: event.costMicroUsd,
3055
+ billableMicroUsd: event.billableMicroUsd,
3056
+ sponsoredMicroUsd: event.sponsoredMicroUsd,
3057
+ reciprocalCreditUnits: event.reciprocalCreditUnits,
3058
+ resource: event.resource ?? null,
3059
+ reviewQueue: event.reviewQueue ?? null,
3060
+ reasonCodes: event.reasonCodes,
3061
+ policyId: event.policyId ?? null,
3062
+ tags: event.tags,
3063
+ occurredAt: event.occurredAt
3064
+ };
3065
+ }
3066
+ function stableStringify(value) {
3067
+ if (Array.isArray(value)) {
3068
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
3069
+ }
3070
+ if (value !== null && typeof value === "object") {
3071
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
3072
+ }
3073
+ return JSON.stringify(value);
3074
+ }
3075
+ export {
3076
+ ABUSE_USAGE_EVENT_KINDS,
3077
+ ABUSE_USAGE_SETTLEMENTS,
3078
+ DEFAULT_SENSITIVITY_PREFERENCES,
3079
+ SENSITIVITY_LABEL_VALUES,
3080
+ SENSITIVITY_PRESENCE_FLOOR,
3081
+ SENSITIVITY_SOURCE_WEIGHT,
3082
+ TRUSTED_SPAM_LABEL,
3083
+ WARNING_SLOP_LABEL,
3084
+ abuseFixtures,
3085
+ activeHubPolicyServices,
3086
+ activeLabels,
3087
+ activePolicyBlockEntries,
3088
+ approveStagedModerationWrite,
3089
+ assessDuplicateContent,
3090
+ assessSensitivity,
3091
+ auditPolicyBlockEntries,
3092
+ averageHash,
3093
+ bucketAbusePeerScore,
3094
+ buildSensitivityLabel,
3095
+ canonicalizeContentText,
3096
+ canonicalizeHubPolicyServiceOffer,
3097
+ canonicalizePolicyBlockList,
3098
+ classifyWithCloudAdapter,
3099
+ classifyWithLocalAdapters,
3100
+ classifyWithModerationCascade,
3101
+ compareContentFingerprints,
3102
+ compareSimHash64,
3103
+ createAISignalProvenanceEvidenceRef,
3104
+ createAbuseDecisionAdapter,
3105
+ createAbuseFactAdapter,
3106
+ createAbuseUsageEvent,
3107
+ createAbuseUsageEventId,
3108
+ createAbuseUsageEventsFromDecision,
3109
+ createAppealEffect,
3110
+ createBaseFacts,
3111
+ createCloudClassifierAdapter,
3112
+ createCloudClassifierRequestBase,
3113
+ createContentFingerprint,
3114
+ createHubPolicyServiceOffer,
3115
+ createKeywordLocalClassifier,
3116
+ createLabelerSubscription,
3117
+ createLocalClassificationResult,
3118
+ createNsfwImageClassifier,
3119
+ createPolicyBlockList,
3120
+ createPublicSearchHubAbuseProfile,
3121
+ createPublicWriteBudgetKey,
3122
+ createQueryCostBudgetKey,
3123
+ createRemoteAdmissionPipeline,
3124
+ createRemoteMutationRejectionTelemetry,
3125
+ createSmallSelfHostedAbuseProfile,
3126
+ createTrustedLabelFromSetting,
3127
+ decideAbuse,
3128
+ decideCloudReviewRoute,
3129
+ decidePublicInteraction,
3130
+ decideReach,
3131
+ decideRemoteMutation,
3132
+ decideSensitivityVisibility,
3133
+ decideTransport,
3134
+ decideWithAdapter,
3135
+ differenceHash,
3136
+ estimateCloudClassifierCost,
3137
+ evaluateLabelerSubscriptionLimit,
3138
+ evaluateLabelerTrust,
3139
+ evaluatePublicWriteBudget,
3140
+ evaluateQueryCostBudget,
3141
+ evaluateReportEscalation,
3142
+ explainDecision,
3143
+ explainSensitivityVisibility,
3144
+ extractCitationReferences,
3145
+ extractKnowledgeClaims,
3146
+ findPolicyBlockAuditEntry,
3147
+ findPolicyBlockEntry,
3148
+ getCloudClassificationSkipReason,
3149
+ getReasonDetail,
3150
+ groupCommunityNoteRatingsByPerspective,
3151
+ hammingDistanceHex,
3152
+ hashAbusePeerIdentifier,
3153
+ hubPolicyServiceOfferSigningBytes,
3154
+ imageHashSimilarity,
3155
+ isAISignalSourceType,
3156
+ isCommunityNoteAgreementVisible,
3157
+ isRejected,
3158
+ isSensitivityLabelValue,
3159
+ isSignedHubPolicyServiceOffer,
3160
+ isSignedPolicyBlockList,
3161
+ isVisible,
3162
+ mapNsfwLabelToSensitivity,
3163
+ matchKnownImageHash,
3164
+ materializeStagedModerationWrite,
3165
+ mergeLocalClassificationResults,
3166
+ normalizeAbuseFacts,
3167
+ perceptualHash,
3168
+ planStagedModerationWrites,
3169
+ policyBlockEntryIsActive,
3170
+ policyBlockListSigningBytes,
3171
+ policyBlockOverrideEntryIsActive,
3172
+ prescreenImage,
3173
+ prescreenImageLabels,
3174
+ publicAppealChannels,
3175
+ qualityRiskScore,
3176
+ redactCloudClassifierText,
3177
+ rejectStagedModerationWrite,
3178
+ reportRemoteMutationRejection,
3179
+ resolveContentVisibility,
3180
+ resolveSubscribedPolicyBlockLists,
3181
+ scoreClaimCitationCoverage,
3182
+ scoreCommunityNotePerspectiveDiversity,
3183
+ sensitivityLabels,
3184
+ sensitivityOverride,
3185
+ shouldThrottle,
3186
+ signHubPolicyServiceOffer,
3187
+ signPolicyBlockList,
3188
+ strictestVisibility,
3189
+ subscriptionToTrustSetting,
3190
+ subscriptionsToTrustSettings,
3191
+ summarizeAbuseUsageEvents,
3192
+ summarizeCommunityNoteAgreement,
3193
+ tokenizeContent,
3194
+ unsignedHubPolicyServiceOffer,
3195
+ unsignedPolicyBlockList,
3196
+ validateAISignalProvenance,
3197
+ validateHubPolicyServiceOffer,
3198
+ verifySignedHubPolicyServiceOffer,
3199
+ verifySignedPolicyBlockList,
3200
+ weightedLabelScore
3201
+ };