@oxyhq/crowdsource-contracts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/dist/case-envelope.d.ts +1130 -0
- package/dist/case-envelope.d.ts.map +1 -0
- package/dist/case-envelope.js +383 -0
- package/dist/case-envelope.js.map +1 -0
- package/dist/decisions.d.ts +353 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +198 -0
- package/dist/decisions.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +43 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +83 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/policies.d.ts +286 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +178 -0
- package/dist/policies.js.map +1 -0
- package/dist/primitives.d.ts +185 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +231 -0
- package/dist/primitives.js.map +1 -0
- package/dist/reputation-events.d.ts +349 -0
- package/dist/reputation-events.d.ts.map +1 -0
- package/dist/reputation-events.js +128 -0
- package/dist/reputation-events.js.map +1 -0
- package/dist/resources.d.ts +484 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +436 -0
- package/dist/resources.js.map +1 -0
- package/dist/reviews.d.ts +276 -0
- package/dist/reviews.d.ts.map +1 -0
- package/dist/reviews.js +144 -0
- package/dist/reviews.js.map +1 -0
- package/dist/taxonomy.d.ts +266 -0
- package/dist/taxonomy.d.ts.map +1 -0
- package/dist/taxonomy.js +282 -0
- package/dist/taxonomy.js.map +1 -0
- package/dist/webhooks.d.ts +604 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +192 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +56 -0
- package/src/case-envelope.ts +433 -0
- package/src/decisions.ts +216 -0
- package/src/index.ts +45 -0
- package/src/json-schema.ts +89 -0
- package/src/policies.ts +203 -0
- package/src/primitives.ts +283 -0
- package/src/reputation-events.ts +144 -0
- package/src/resources.ts +489 -0
- package/src/reviews.ts +159 -0
- package/src/taxonomy.ts +313 -0
- package/src/webhooks.ts +215 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scalar contracts shared by every CrowdSource surface.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here belongs to a single contract module; everything here is depended
|
|
5
|
+
* on by several of them. Two properties drive most of the choices below:
|
|
6
|
+
*
|
|
7
|
+
* 1. **The envelope is content-addressed.** §7.3 derives `caseDedupKey` from
|
|
8
|
+
* `applicationId`, `subject.externalId`, the canonical envelope hash and
|
|
9
|
+
* the policy version. Two byte-different representations of the SAME value
|
|
10
|
+
* produce two different keys, therefore two cases, therefore two penalties
|
|
11
|
+
* for one incident — the exact opposite of the "one penalty per incident"
|
|
12
|
+
* invariant. So every scalar that can travel in more than one shape is
|
|
13
|
+
* pinned to ONE canonical shape here: digests are always `sha256:<hex>`,
|
|
14
|
+
* timestamps are always millisecond-precision UTC.
|
|
15
|
+
*
|
|
16
|
+
* 2. **`:` is a structural separator.** §7.3 joins the dedup key components
|
|
17
|
+
* with `:`. An identifier that may itself contain `:` makes that join
|
|
18
|
+
* ambiguous — two distinct tuples can flatten to the same string and merge
|
|
19
|
+
* two unrelated cases. Every identifier schema below therefore excludes
|
|
20
|
+
* `:`. (The same character is independently forbidden in BullMQ queue and
|
|
21
|
+
* job ids, so this also keeps identifiers usable as dispatch keys.)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bounds applied across the contracts.
|
|
28
|
+
*
|
|
29
|
+
* §7.2.3 requires ingress to check "size limits, number of resources and MIME
|
|
30
|
+
* types" but the plan never states the numbers. These are the contract's own
|
|
31
|
+
* declared limits: generous enough not to reject real moderation material,
|
|
32
|
+
* finite so that no array or string in the contract is unbounded. A tenant may
|
|
33
|
+
* be held to something tighter by quota; nothing may exceed these.
|
|
34
|
+
*/
|
|
35
|
+
export const CONTRACT_LIMITS = Object.freeze({
|
|
36
|
+
/** Envelope-local ids (`res_post`), policy ids, principal refs. */
|
|
37
|
+
IDENTIFIER_MAX_LENGTH: 128,
|
|
38
|
+
/** Ids minted by the application (`mention_report_123`, `post_987`). */
|
|
39
|
+
EXTERNAL_ID_MAX_LENGTH: 200,
|
|
40
|
+
|
|
41
|
+
RESOURCES_PER_ENVELOPE_MAX: 50,
|
|
42
|
+
RELATIONS_PER_ENVELOPE_MAX: 200,
|
|
43
|
+
ALLEGATIONS_PER_ENVELOPE_MAX: 20,
|
|
44
|
+
PRINCIPAL_BINDINGS_PER_ENVELOPE_MAX: 50,
|
|
45
|
+
|
|
46
|
+
/** A reported text resource. Long enough for an article, not a corpus. */
|
|
47
|
+
TEXT_RESOURCE_MAX_LENGTH: 100_000,
|
|
48
|
+
/** Titles, labels, display names, place names. */
|
|
49
|
+
SHORT_TEXT_MAX_LENGTH: 500,
|
|
50
|
+
/** Descriptions, bios, reporter details, reviewer notes. */
|
|
51
|
+
LONG_TEXT_MAX_LENGTH: 5_000,
|
|
52
|
+
/** Audio transcript, document extracted text, link snapshot. */
|
|
53
|
+
EXTRACTED_TEXT_MAX_LENGTH: 200_000,
|
|
54
|
+
|
|
55
|
+
METADATA_KEYS_MAX: 50,
|
|
56
|
+
METADATA_KEY_MAX_LENGTH: 64,
|
|
57
|
+
METADATA_STRING_VALUE_MAX_LENGTH: 1_000,
|
|
58
|
+
|
|
59
|
+
/** §5.7 custom payloads: data, bounded, never a document tree. */
|
|
60
|
+
CUSTOM_PAYLOAD_MAX_DEPTH: 5,
|
|
61
|
+
CUSTOM_PAYLOAD_ARRAY_MAX_LENGTH: 100,
|
|
62
|
+
CUSTOM_PAYLOAD_STRING_MAX_LENGTH: 10_000,
|
|
63
|
+
|
|
64
|
+
CONVERSATION_MESSAGES_MAX: 50,
|
|
65
|
+
LISTING_MEDIA_REFS_MAX: 20,
|
|
66
|
+
PROFILE_CLAIMS_MAX: 20,
|
|
67
|
+
|
|
68
|
+
FINDINGS_MAX: 20,
|
|
69
|
+
RESOURCE_REFS_PER_FINDING_MAX: 50,
|
|
70
|
+
POLICY_RULE_IDS_MAX: 20,
|
|
71
|
+
RECOMMENDED_ACTIONS_MAX: 10,
|
|
72
|
+
|
|
73
|
+
/** §13.6 defaults to 30 days "configurable by policy"; this is the ceiling. */
|
|
74
|
+
RETENTION_DAYS_MAX: 3_650,
|
|
75
|
+
/** A jury is 3, 5 or 7 today (§9.4). The bound only stops absurd values. */
|
|
76
|
+
JURY_SIZE_MAX: 99,
|
|
77
|
+
} as const);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Keys that must never appear in a caller-supplied object, at any depth.
|
|
81
|
+
*
|
|
82
|
+
* A payload is data. These three names are how a data payload stops being data
|
|
83
|
+
* the moment anything merges, clones or `Object.assign`s it into a prototype
|
|
84
|
+
* chain. Rejecting them at the contract boundary is cheap and total; sanitising
|
|
85
|
+
* them later depends on every consumer remembering to.
|
|
86
|
+
*
|
|
87
|
+
* One of the three behaves differently and it is worth knowing which, because
|
|
88
|
+
* the list reads as though it were doing all the work: Zod removes an own
|
|
89
|
+
* `__proto__` property from any input before a key schema or a strict-key check
|
|
90
|
+
* ever sees it, so `__proto__` is DROPPED rather than rejected, and this list
|
|
91
|
+
* never fires for it. The safety outcome is the same — nothing is polluted, and
|
|
92
|
+
* the key cannot survive into parsed output — but a test asserting that
|
|
93
|
+
* `__proto__` is *rejected* would fail, and a reader assuming this constant is
|
|
94
|
+
* what prevents pollution would be assuming the wrong thing. `constructor` and
|
|
95
|
+
* `prototype` are ordinary keys to Zod and are rejected here.
|
|
96
|
+
*/
|
|
97
|
+
export const FORBIDDEN_OBJECT_KEYS: readonly string[] = Object.freeze([
|
|
98
|
+
'__proto__',
|
|
99
|
+
'constructor',
|
|
100
|
+
'prototype',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* An id that is local to one envelope, policy set or contract payload.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately opaque: the plan writes both slugs (`app_mention`,
|
|
109
|
+
* `mention.community`) and ULID-prefixed ids (`case_01...`) in the same fields,
|
|
110
|
+
* so pinning the format would reject the plan's own reference documents. What
|
|
111
|
+
* IS pinned is the character set — see the `:` note in the module comment.
|
|
112
|
+
*/
|
|
113
|
+
export const IdentifierSchema = z
|
|
114
|
+
.string()
|
|
115
|
+
.min(1)
|
|
116
|
+
.max(CONTRACT_LIMITS.IDENTIFIER_MAX_LENGTH)
|
|
117
|
+
.regex(
|
|
118
|
+
IDENTIFIER_PATTERN,
|
|
119
|
+
'must start with a letter or digit and contain only letters, digits, ".", "_" or "-"',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
/** An id minted by the reporting application, echoed back but never trusted. */
|
|
123
|
+
export const ExternalIdSchema = z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1)
|
|
126
|
+
.max(CONTRACT_LIMITS.EXTERNAL_ID_MAX_LENGTH)
|
|
127
|
+
.regex(
|
|
128
|
+
IDENTIFIER_PATTERN,
|
|
129
|
+
'must start with a letter or digit and contain only letters, digits, ".", "_" or "-"',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `sha256:<64 lowercase hex>` — the ONLY accepted digest form.
|
|
134
|
+
*
|
|
135
|
+
* Appendix A writes `"sha256:..."`; §5.8 writes `"..."`. Accepting both would
|
|
136
|
+
* let the same bytes be described two ways, and the canonical envelope hash
|
|
137
|
+
* would differ between them (see property 1 in the module comment). Appendix A
|
|
138
|
+
* is the reference document, so the prefixed form wins and the bare form is
|
|
139
|
+
* rejected rather than silently normalised — normalising would mean the digest
|
|
140
|
+
* CrowdSource stores is not the digest the application sent.
|
|
141
|
+
*/
|
|
142
|
+
export const Sha256DigestSchema = z
|
|
143
|
+
.string()
|
|
144
|
+
.regex(/^sha256:[0-9a-f]{64}$/, 'must be "sha256:" followed by 64 lowercase hex characters');
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Millisecond-precision UTC, e.g. `2026-07-28T18:00:00.000Z`.
|
|
148
|
+
*
|
|
149
|
+
* Every timestamp in the plan is written this way, and it is what
|
|
150
|
+
* `Date.prototype.toISOString()` produces, so the canonical form costs a
|
|
151
|
+
* JavaScript integrator nothing. Offsets are rejected: `18:00+02:00` and
|
|
152
|
+
* `16:00Z` are the same instant and would hash differently.
|
|
153
|
+
*/
|
|
154
|
+
export const TimestampSchema = z.iso.datetime({ offset: false, precision: 3 });
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* BCP 47, restricted to `language[-Script][-Region][-variant…]`.
|
|
158
|
+
*
|
|
159
|
+
* A syntactic subset, not the full grammar (no extensions, no private use, no
|
|
160
|
+
* grandfathered tags). Covers `es` and `es-ES` from the plan and everything a
|
|
161
|
+
* real application sends; a tag outside the subset is a signal worth rejecting
|
|
162
|
+
* at ingress rather than carrying into reviewer language eligibility.
|
|
163
|
+
*/
|
|
164
|
+
export const LanguageTagSchema = z
|
|
165
|
+
.string()
|
|
166
|
+
.regex(
|
|
167
|
+
/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?(-([0-9][A-Za-z0-9]{3}|[A-Za-z0-9]{5,8}))*$/,
|
|
168
|
+
'must be a BCP 47 tag of the form language[-Script][-Region][-variant]',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* An absolute `http`/`https` URL.
|
|
173
|
+
*
|
|
174
|
+
* §7.2.7 rejects "dangerous schemes and executable resources". Scheme is a
|
|
175
|
+
* purely syntactic property, so the contract enforces it. Host reachability is
|
|
176
|
+
* NOT enforced here: deciding whether a host is internal needs DNS resolution
|
|
177
|
+
* and belongs to `safeFetch` at ingestion time. A scheme check that pretended
|
|
178
|
+
* to be an SSRF control would be the more dangerous of the two.
|
|
179
|
+
*/
|
|
180
|
+
export const HttpUrlSchema = z.url({ protocol: /^https?$/ }).max(2_048);
|
|
181
|
+
|
|
182
|
+
/** A `type/subtype` media type, without parameters. */
|
|
183
|
+
export const MimeTypeSchema = z
|
|
184
|
+
.string()
|
|
185
|
+
.max(255)
|
|
186
|
+
.regex(
|
|
187
|
+
/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/,
|
|
188
|
+
'must be a lowercase "type/subtype" media type without parameters',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const METADATA_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
192
|
+
|
|
193
|
+
/** A key in any caller-supplied open bag. */
|
|
194
|
+
export const ObjectKeySchema = z
|
|
195
|
+
.string()
|
|
196
|
+
.min(1)
|
|
197
|
+
.max(CONTRACT_LIMITS.METADATA_KEY_MAX_LENGTH)
|
|
198
|
+
.regex(METADATA_KEY_PATTERN, 'must start with a letter and contain only letters, digits, ".", "_" or "-"')
|
|
199
|
+
.refine((key) => !FORBIDDEN_OBJECT_KEYS.includes(key), {
|
|
200
|
+
message: 'must not be a prototype-bearing key',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/** A single metadata value: scalar only, never a nested structure. */
|
|
204
|
+
export const MetadataValueSchema = z.union([
|
|
205
|
+
z.string().max(CONTRACT_LIMITS.METADATA_STRING_VALUE_MAX_LENGTH),
|
|
206
|
+
z.number().finite(),
|
|
207
|
+
z.boolean(),
|
|
208
|
+
z.null(),
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* An open key/value bag (§5.1 `metadata`, §5.3 the `metadata` resource type).
|
|
213
|
+
*
|
|
214
|
+
* Open by definition — the plan calls it "typed key value fields" and every
|
|
215
|
+
* tenant's keys differ — but flat and bounded. Allowing nesting here would make
|
|
216
|
+
* this the one field in the envelope through which an application could ship an
|
|
217
|
+
* arbitrary document tree past every other constraint in the contract.
|
|
218
|
+
*/
|
|
219
|
+
export const MetadataBagSchema = z
|
|
220
|
+
.record(ObjectKeySchema, MetadataValueSchema)
|
|
221
|
+
.refine((bag) => Object.keys(bag).length <= CONTRACT_LIMITS.METADATA_KEYS_MAX, {
|
|
222
|
+
message: `must not have more than ${CONTRACT_LIMITS.METADATA_KEYS_MAX} keys`,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/** A JSON value that a §5.7 custom payload may contain. */
|
|
226
|
+
export type CustomPayloadValue =
|
|
227
|
+
| string
|
|
228
|
+
| number
|
|
229
|
+
| boolean
|
|
230
|
+
| null
|
|
231
|
+
| CustomPayloadValue[]
|
|
232
|
+
| { [key: string]: CustomPayloadValue };
|
|
233
|
+
|
|
234
|
+
const customPayloadScalarSchema = z.union([
|
|
235
|
+
z.string().max(CONTRACT_LIMITS.CUSTOM_PAYLOAD_STRING_MAX_LENGTH),
|
|
236
|
+
z.number().finite(),
|
|
237
|
+
z.boolean(),
|
|
238
|
+
z.null(),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const buildCustomPayloadValueSchema = (): z.ZodType<CustomPayloadValue> => {
|
|
242
|
+
let value: z.ZodType<CustomPayloadValue> = customPayloadScalarSchema;
|
|
243
|
+
for (let depth = 1; depth < CONTRACT_LIMITS.CUSTOM_PAYLOAD_MAX_DEPTH; depth += 1) {
|
|
244
|
+
value = z.union([
|
|
245
|
+
customPayloadScalarSchema,
|
|
246
|
+
z.array(value).max(CONTRACT_LIMITS.CUSTOM_PAYLOAD_ARRAY_MAX_LENGTH),
|
|
247
|
+
z.record(ObjectKeySchema, value),
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The value grammar for a §5.7 custom payload.
|
|
255
|
+
*
|
|
256
|
+
* Nesting is unrolled to a fixed depth rather than made recursive so the bound
|
|
257
|
+
* is part of the type, enforced by the same parse as everything else, and
|
|
258
|
+
* visible in the exported JSON Schema. Depth is what stops a payload being a
|
|
259
|
+
* document; `ObjectKeySchema` is what stops it being a prototype.
|
|
260
|
+
*
|
|
261
|
+
* Note what is deliberately absent: no lexical filter on string values. A
|
|
262
|
+
* moderation payload legitimately carries the exact hostile text under review —
|
|
263
|
+
* markup, script fragments, `javascript:` URLs quoted inside a harassment
|
|
264
|
+
* report. Rejecting those strings would reject the material the system exists
|
|
265
|
+
* to review. §5.7's boundary is structural, not lexical: the contract has no
|
|
266
|
+
* field anywhere that carries markup, a template, a component or a remote URL
|
|
267
|
+
* to be rendered, so there is nothing for such a string to be interpreted as.
|
|
268
|
+
*/
|
|
269
|
+
export const CustomPayloadValueSchema = buildCustomPayloadValueSchema();
|
|
270
|
+
|
|
271
|
+
/** The top level of a §5.7 custom payload: always an object, never an array. */
|
|
272
|
+
export const CustomPayloadSchema = z.record(ObjectKeySchema, CustomPayloadValueSchema);
|
|
273
|
+
|
|
274
|
+
/** A number in the closed interval [0, 1] — §9.5 clamps confidence to it. */
|
|
275
|
+
export const UnitIntervalSchema = z.number().min(0).max(1);
|
|
276
|
+
|
|
277
|
+
export type Identifier = z.infer<typeof IdentifierSchema>;
|
|
278
|
+
export type ExternalId = z.infer<typeof ExternalIdSchema>;
|
|
279
|
+
export type Sha256Digest = z.infer<typeof Sha256DigestSchema>;
|
|
280
|
+
export type Timestamp = z.infer<typeof TimestampSchema>;
|
|
281
|
+
export type LanguageTag = z.infer<typeof LanguageTagSchema>;
|
|
282
|
+
export type MetadataBag = z.infer<typeof MetadataBagSchema>;
|
|
283
|
+
export type CustomPayload = z.infer<typeof CustomPayloadSchema>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The internal reputation event (§11.5, §11.6, §11.7).
|
|
3
|
+
*
|
|
4
|
+
* This is the only thing CrowdSource ever sends toward Oxy Trust, and it is a
|
|
5
|
+
* statement, not an instruction. "CrowdSource does not write to reputation
|
|
6
|
+
* collections. It publishes an authenticated internal event" (§11.5), and Oxy
|
|
7
|
+
* Trust's own consequence engine decides what, if anything, follows. Nothing in
|
|
8
|
+
* this payload names points, a tier, a strike or a standing: an application —
|
|
9
|
+
* and CrowdSource on its behalf — never chooses an Oxy reputation figure.
|
|
10
|
+
*
|
|
11
|
+
* Three of §11.7's eight pre-effect validations are structural, and all three
|
|
12
|
+
* are moved into the type rather than left to a runtime check that a future
|
|
13
|
+
* refactor can drop:
|
|
14
|
+
*
|
|
15
|
+
* * §11.7.4 — `subject.bindingProofId` is required. "No binding proof, no Oxy
|
|
16
|
+
* Trust effect" becomes an event that cannot be constructed.
|
|
17
|
+
* * §11.7.5 — a finding's scope must be `oxy_network` or `identity_integrity`.
|
|
18
|
+
* `application_local` cannot appear here at all, which is §6.5's rule that
|
|
19
|
+
* a local restriction does not become a global sanction, enforced at the
|
|
20
|
+
* wire rather than by the receiver's diligence.
|
|
21
|
+
* * §11.7.8 — the decision may not be superseded or corrected, so the status
|
|
22
|
+
* enum has only the two values that may carry an effect (§11.7.3).
|
|
23
|
+
*
|
|
24
|
+
* The remaining five are stateful — who signed it, whether this event was seen
|
|
25
|
+
* before, whether the incident already produced an equivalent effect — and stay
|
|
26
|
+
* with the bridge.
|
|
27
|
+
*
|
|
28
|
+
* This is the one payload in the package that is `.strict()` in the OUTBOUND
|
|
29
|
+
* direction. §10.11's rule about unknown fields has an explicit exception for
|
|
30
|
+
* "where the schema forbids them for safety", and this is that case twice over:
|
|
31
|
+
* a finding here deliberately carries no `resourceIds` and no free text, so
|
|
32
|
+
* that nothing about the reviewed material reaches a reputation ledger or a
|
|
33
|
+
* signed attestation, and an unrecognised field is exactly how that would
|
|
34
|
+
* happen. Forward compatibility is handled by the `.v1` in the event type.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
|
|
39
|
+
import { CONTRACT_LIMITS, IdentifierSchema, Sha256DigestSchema } from './primitives';
|
|
40
|
+
import { PrincipalTypeSchema } from './case-envelope';
|
|
41
|
+
import { ReputationPolicyVersionsSchema } from './policies';
|
|
42
|
+
import {
|
|
43
|
+
FindingAttributionSchema,
|
|
44
|
+
ReputationEligibleFindingScopeSchema,
|
|
45
|
+
SeveritySchema,
|
|
46
|
+
TaxonomyCodeSchema,
|
|
47
|
+
} from './taxonomy';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* §11.6 names one event type. §11.5 names four bridge operations — apply,
|
|
51
|
+
* finalize, reverse, reconcile — so siblings are expected, but the plan does
|
|
52
|
+
* not write them and they are not invented here. Adding one is additive: a new
|
|
53
|
+
* literal and a new variant, with the `.v1` suffix carrying the version.
|
|
54
|
+
*/
|
|
55
|
+
export const MODERATION_DECISION_FINALIZED_EVENT_TYPE = 'moderation.decision.finalized.v1';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* §11.7.3 and §11.7.8 together: an effect may follow a final decision or,
|
|
59
|
+
* where policy allows, a provisional one — and never a superseded or corrected
|
|
60
|
+
* one. Those two states are absent from this enum rather than rejected by a
|
|
61
|
+
* refinement, so the impossibility is visible in the type.
|
|
62
|
+
*/
|
|
63
|
+
export const REPUTATION_EVENT_DECISION_STATUSES = ['provisional', 'final'] as const;
|
|
64
|
+
export const ReputationEventDecisionStatusSchema = z.enum(REPUTATION_EVENT_DECISION_STATUSES);
|
|
65
|
+
export type ReputationEventDecisionStatus = z.infer<typeof ReputationEventDecisionStatusSchema>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Who the effect would land on (§11.6 `subject`).
|
|
69
|
+
*
|
|
70
|
+
* `bindingProofId` is required, unconditionally — see §11.7.4 above. The
|
|
71
|
+
* envelope's `principalBindings` make the same proof optional for non-Oxy
|
|
72
|
+
* principal types, because those can never reach this event; here there is
|
|
73
|
+
* nothing to be lenient about.
|
|
74
|
+
*/
|
|
75
|
+
export const ReputationEventSubjectSchema = z.strictObject({
|
|
76
|
+
principalType: PrincipalTypeSchema,
|
|
77
|
+
principalId: IdentifierSchema,
|
|
78
|
+
bindingProofId: IdentifierSchema,
|
|
79
|
+
});
|
|
80
|
+
export type ReputationEventSubject = z.infer<typeof ReputationEventSubjectSchema>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A finding as Oxy Trust sees it (§11.6).
|
|
84
|
+
*
|
|
85
|
+
* Deliberately narrower than a `DecisionFinding`: no `resourceIds`, no policy
|
|
86
|
+
* rule ids, no text. Oxy Trust needs to know what was confirmed, how serious it
|
|
87
|
+
* was, how far it reaches and whose conduct it was — it does not need to know
|
|
88
|
+
* which piece of content it happened on, and §13.5's minimisation plus the
|
|
89
|
+
* invariant that sensitive content never appears in logs or public attestations
|
|
90
|
+
* mean it must not be told.
|
|
91
|
+
*
|
|
92
|
+
* `attribution` is required here where it is optional on a decision finding: an
|
|
93
|
+
* effect must land on somebody, and a finding that attributes nothing has
|
|
94
|
+
* nothing to contribute to this event.
|
|
95
|
+
*/
|
|
96
|
+
export const ReputationEventFindingSchema = z.strictObject({
|
|
97
|
+
code: TaxonomyCodeSchema,
|
|
98
|
+
severity: SeveritySchema,
|
|
99
|
+
scope: ReputationEligibleFindingScopeSchema,
|
|
100
|
+
attribution: FindingAttributionSchema,
|
|
101
|
+
});
|
|
102
|
+
export type ReputationEventFinding = z.infer<typeof ReputationEventFindingSchema>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `moderation.decision.finalized.v1` (§11.6).
|
|
106
|
+
*
|
|
107
|
+
* `incidentId` is what makes "one penalty per incident" enforceable: it is the
|
|
108
|
+
* first component of the effect's idempotency key (Appendix D:
|
|
109
|
+
* `incidentId + principalId + effectType + decisionRevision`), so the same
|
|
110
|
+
* incident reaching the bridge twice — from a replay, from a redelivery, from
|
|
111
|
+
* two cases that were merged — produces one effect.
|
|
112
|
+
*
|
|
113
|
+
* `proofHash` binds the event to the decision it reports, so an effect can be
|
|
114
|
+
* explained and, if the decision is later corrected, reversed against the exact
|
|
115
|
+
* revision that caused it.
|
|
116
|
+
*/
|
|
117
|
+
export const ModerationDecisionFinalizedEventSchema = z.strictObject({
|
|
118
|
+
eventId: IdentifierSchema,
|
|
119
|
+
type: z.literal(MODERATION_DECISION_FINALIZED_EVENT_TYPE),
|
|
120
|
+
caseId: IdentifierSchema,
|
|
121
|
+
incidentId: IdentifierSchema,
|
|
122
|
+
decisionId: IdentifierSchema,
|
|
123
|
+
decisionRevision: z.number().int().positive(),
|
|
124
|
+
applicationId: IdentifierSchema,
|
|
125
|
+
subject: ReputationEventSubjectSchema,
|
|
126
|
+
findings: z.array(ReputationEventFindingSchema).min(1).max(CONTRACT_LIMITS.FINDINGS_MAX),
|
|
127
|
+
decisionStatus: ReputationEventDecisionStatusSchema,
|
|
128
|
+
policyVersions: ReputationPolicyVersionsSchema,
|
|
129
|
+
proofHash: Sha256DigestSchema,
|
|
130
|
+
});
|
|
131
|
+
export type ModerationDecisionFinalizedEvent = z.infer<
|
|
132
|
+
typeof ModerationDecisionFinalizedEventSchema
|
|
133
|
+
>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every reputation event CrowdSource emits.
|
|
137
|
+
*
|
|
138
|
+
* A union of one today. It exists so consumers switch on `type` from the start
|
|
139
|
+
* and adding §11.5's remaining operations does not change how they are written.
|
|
140
|
+
*/
|
|
141
|
+
export const ReputationEventSchema = z.discriminatedUnion('type', [
|
|
142
|
+
ModerationDecisionFinalizedEventSchema,
|
|
143
|
+
]);
|
|
144
|
+
export type ReputationEvent = z.infer<typeof ReputationEventSchema>;
|