@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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters-B-9W_mn3.d.ts +190 -0
- package/dist/adapters-B63EWOI3.d.ts +196 -0
- package/dist/adapters-B92QvJK9.d.ts +190 -0
- package/dist/adapters-BE8Dk1m1.d.ts +193 -0
- package/dist/adapters-BPXuKMIo.d.ts +196 -0
- package/dist/adapters-BtDzNNjU.d.ts +190 -0
- package/dist/adapters-CBfkKocx.d.ts +196 -0
- package/dist/adapters-CD50ercr.d.ts +191 -0
- package/dist/adapters-CIj_ODTY.d.ts +190 -0
- package/dist/adapters-D2lWWuTk.d.ts +196 -0
- package/dist/adapters-Qu3BkEMk.d.ts +153 -0
- package/dist/adapters-hOtVE8hd.d.ts +153 -0
- package/dist/adapters-k10HxbtE.d.ts +190 -0
- package/dist/adapters.d.ts +1 -0
- package/dist/adapters.js +12 -0
- package/dist/chunk-4FW6LHWU.js +495 -0
- package/dist/chunk-O2ZULYP6.js +487 -0
- package/dist/chunk-O3JDSAJP.js +507 -0
- package/dist/chunk-O4S27KHE.js +390 -0
- package/dist/chunk-Y2RKXAMV.js +499 -0
- package/dist/index.d.ts +1408 -0
- package/dist/index.js +3201 -0
- package/package.json +42 -0
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
|
+
};
|