@oxyhq/crowdsource-contracts 0.3.0 → 0.4.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/dist/appeals.d.ts +323 -0
- package/dist/appeals.d.ts.map +1 -0
- package/dist/appeals.js +203 -0
- package/dist/appeals.js.map +1 -0
- package/dist/case-envelope.d.ts +3 -3
- package/dist/case-envelope.d.ts.map +1 -1
- package/dist/case-envelope.js +32 -32
- package/dist/case-envelope.js.map +1 -1
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.d.ts.map +1 -1
- package/dist/decisions.js +28 -28
- package/dist/decisions.js.map +1 -1
- package/dist/esm/appeals.js +200 -0
- package/dist/esm/appeals.js.map +1 -0
- package/dist/esm/case-envelope.js +380 -0
- package/dist/esm/case-envelope.js.map +1 -0
- package/dist/esm/closed.js +32 -0
- package/dist/esm/closed.js.map +1 -0
- package/dist/esm/decisions.js +195 -0
- package/dist/esm/decisions.js.map +1 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/json-schema.js +84 -0
- package/dist/esm/json-schema.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/policies.js +175 -0
- package/dist/esm/policies.js.map +1 -0
- package/dist/esm/primitives.js +249 -0
- package/dist/esm/primitives.js.map +1 -0
- package/dist/esm/reputation-events.js +125 -0
- package/dist/esm/reputation-events.js.map +1 -0
- package/dist/esm/resources.js +455 -0
- package/dist/esm/resources.js.map +1 -0
- package/dist/esm/reviewer-surface.js +612 -0
- package/dist/esm/reviewer-surface.js.map +1 -0
- package/dist/esm/reviews.js +141 -0
- package/dist/esm/reviews.js.map +1 -0
- package/dist/esm/taxonomy.js +278 -0
- package/dist/esm/taxonomy.js.map +1 -0
- package/dist/esm/webhooks.js +188 -0
- package/dist/esm/webhooks.js.map +1 -0
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/json-schema.d.ts +1 -1
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js +25 -20
- package/dist/json-schema.js.map +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +14 -14
- package/dist/policies.js.map +1 -1
- package/dist/reputation-events.js +19 -19
- package/dist/reputation-events.js.map +1 -1
- package/dist/resources.d.ts +60 -2
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +116 -103
- package/dist/resources.js.map +1 -1
- package/dist/reviewer-surface.d.ts +1956 -0
- package/dist/reviewer-surface.d.ts.map +1 -0
- package/dist/reviewer-surface.js +615 -0
- package/dist/reviewer-surface.js.map +1 -0
- package/dist/reviews.js +15 -15
- package/dist/reviews.js.map +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +18 -18
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -4
- package/src/appeals.ts +229 -0
- package/src/case-envelope.ts +5 -5
- package/src/decisions.ts +6 -6
- package/src/index.ts +13 -11
- package/src/json-schema.ts +12 -7
- package/src/policies.ts +3 -3
- package/src/reputation-events.ts +4 -4
- package/src/resources.ts +99 -73
- package/src/reviewer-surface.ts +715 -0
- package/src/reviews.ts +4 -4
- package/src/webhooks.ts +4 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one reviewer submits (§9.2, §9.3) and how they step away (§4.1).
|
|
3
|
+
*
|
|
4
|
+
* §9.2 splits the form in two — describe the material, then evaluate it against
|
|
5
|
+
* a rule — to reduce anchoring on the reporter's category and to let the same
|
|
6
|
+
* description be reused under different policies. §9.3 then gives one flat
|
|
7
|
+
* result object. Both are true at once: a `ReviewFinding` carries the step-one
|
|
8
|
+
* fields (`code`, `resourceIds`, `severity`, `confidence`) and the step-two
|
|
9
|
+
* fields (`policyRuleIds`), and the enclosing submission carries the step-two
|
|
10
|
+
* verdict (`outcome`, `recommendedActions`). The split is a property of the
|
|
11
|
+
* interface, not of the payload, so it is documented here rather than nested.
|
|
12
|
+
*
|
|
13
|
+
* The submission is `.strict()`, and that is a safety decision, not tidiness.
|
|
14
|
+
* A review belongs to an assignment the server issued; the assignment id comes
|
|
15
|
+
* from the route, the case and the reviewer come from the assignment. If this
|
|
16
|
+
* object tolerated a `caseId`, an `assignmentId` or a `reviewerId`, the day
|
|
17
|
+
* somebody read one of them would be the day a reviewer could vote on a case
|
|
18
|
+
* they were never drawn for — against "nobody chooses the case they review".
|
|
19
|
+
* Strict makes that field impossible rather than merely unused.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { CONTRACT_LIMITS, UnitIntervalSchema } from './primitives.js';
|
|
23
|
+
import { PolicyRuleIdSchema } from './policies.js';
|
|
24
|
+
import { ResourceIdSchema } from './resources.js';
|
|
25
|
+
import { FindingContextSchema, RecommendedActionSchema, SeveritySchema, TaxonomyCodeSchema, } from './taxonomy.js';
|
|
26
|
+
/**
|
|
27
|
+
* What a single reviewer can conclude.
|
|
28
|
+
*
|
|
29
|
+
* Narrower than §9.6's decision outcomes on purpose. `inconclusive` is what the
|
|
30
|
+
* consensus engine reports when a panel does not agree — a single reviewer
|
|
31
|
+
* cannot fail to agree with themselves, and "the absence of consensus is
|
|
32
|
+
* neither guilt nor innocence" only holds if `inconclusive` is produced by the
|
|
33
|
+
* engine and never voted for. `duplicate`, `escalated` and `superseded` are
|
|
34
|
+
* likewise case-level states, not opinions about material. The plan never
|
|
35
|
+
* enumerates review outcomes; this is that gap resolved toward the invariant.
|
|
36
|
+
*/
|
|
37
|
+
export const REVIEW_OUTCOMES = [
|
|
38
|
+
'violation',
|
|
39
|
+
'no_violation',
|
|
40
|
+
'insufficient_context',
|
|
41
|
+
'content_unavailable',
|
|
42
|
+
];
|
|
43
|
+
export const ReviewOutcomeSchema = z.enum(REVIEW_OUTCOMES);
|
|
44
|
+
/** §9.5 feeds this into `contextFactor`: 1.0 if sufficient, 0.5 otherwise. */
|
|
45
|
+
export const CONTEXT_SUFFICIENCIES = ['sufficient', 'insufficient'];
|
|
46
|
+
export const ContextSufficiencySchema = z.enum(CONTEXT_SUFFICIENCIES);
|
|
47
|
+
/**
|
|
48
|
+
* One structured finding from one reviewer (§9.3).
|
|
49
|
+
*
|
|
50
|
+
* `resourceIds` is required and non-empty: §9.4 makes the affected resource one
|
|
51
|
+
* of the dimensions consensus is measured on, so a finding that does not say
|
|
52
|
+
* what it is about cannot be agreed with or disagreed with.
|
|
53
|
+
*
|
|
54
|
+
* `policyRuleIds` is optional: step one of the form can complete without step
|
|
55
|
+
* two — a reviewer may classify material accurately and find no rule that
|
|
56
|
+
* covers it, which is exactly the `no_violation`-with-findings case §6.2
|
|
57
|
+
* describes.
|
|
58
|
+
*
|
|
59
|
+
* `context` is §9.2's and §9.4's "excepción", written as §6.2 writes it: beside
|
|
60
|
+
* the code and the severity, because "artistic nudity" is a different
|
|
61
|
+
* description of the material from "nudity" rather than a different verdict
|
|
62
|
+
* about it. Optional, and absence means no exception applies.
|
|
63
|
+
*
|
|
64
|
+
* `confidence` communicates quality and triggers escalation (§9.5). It never
|
|
65
|
+
* weights the vote.
|
|
66
|
+
*/
|
|
67
|
+
export const ReviewFindingSchema = z.strictObject({
|
|
68
|
+
code: TaxonomyCodeSchema,
|
|
69
|
+
resourceIds: z.array(ResourceIdSchema).min(1).max(CONTRACT_LIMITS.RESOURCE_REFS_PER_FINDING_MAX),
|
|
70
|
+
severity: SeveritySchema,
|
|
71
|
+
context: FindingContextSchema.optional(),
|
|
72
|
+
confidence: UnitIntervalSchema,
|
|
73
|
+
policyRuleIds: z.array(PolicyRuleIdSchema).max(CONTRACT_LIMITS.POLICY_RULE_IDS_MAX).optional(),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* The body of `POST /v1/reviewer/assignments/{id}/reviews` — §9.3's "result of
|
|
77
|
+
* a review".
|
|
78
|
+
*
|
|
79
|
+
* `recommendedActions` is a list of action tokens here, where a decision's
|
|
80
|
+
* `recommendedActions` are objects that name their target resources. That
|
|
81
|
+
* difference is in the plan (§9.3 versus Appendix B) and is kept: a reviewer
|
|
82
|
+
* recommends a course of action, and the consensus process is what binds an
|
|
83
|
+
* agreed recommendation to the specific resources it applies to.
|
|
84
|
+
*/
|
|
85
|
+
export const ReviewSubmissionSchema = z
|
|
86
|
+
.strictObject({
|
|
87
|
+
outcome: ReviewOutcomeSchema,
|
|
88
|
+
contextSufficiency: ContextSufficiencySchema,
|
|
89
|
+
findings: z.array(ReviewFindingSchema).max(CONTRACT_LIMITS.FINDINGS_MAX),
|
|
90
|
+
recommendedActions: z
|
|
91
|
+
.array(RecommendedActionSchema)
|
|
92
|
+
.max(CONTRACT_LIMITS.RECOMMENDED_ACTIONS_MAX),
|
|
93
|
+
/**
|
|
94
|
+
* §13.5 and the "sensitive content never reaches logs" invariant apply to
|
|
95
|
+
* this field as much as to the evidence: a note is free text a human wrote
|
|
96
|
+
* while looking at the material, so it is bounded here and must be treated
|
|
97
|
+
* as case content everywhere downstream — never logged, never attested.
|
|
98
|
+
*/
|
|
99
|
+
notes: z.string().max(CONTRACT_LIMITS.LONG_TEXT_MAX_LENGTH).optional(),
|
|
100
|
+
})
|
|
101
|
+
.superRefine((review, ctx) => {
|
|
102
|
+
if (review.outcome === 'insufficient_context' && review.contextSufficiency !== 'insufficient') {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: 'custom',
|
|
105
|
+
path: ['contextSufficiency'],
|
|
106
|
+
message: 'an outcome of insufficient_context requires contextSufficiency "insufficient"',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (review.outcome === 'violation' && review.findings.length === 0) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: 'custom',
|
|
112
|
+
path: ['findings'],
|
|
113
|
+
message: 'a violation outcome requires at least one finding',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* §4.1: a reviewer may declare a conflict, a language they do not have,
|
|
119
|
+
* material too sensitive for them, or context they cannot obtain — "without
|
|
120
|
+
* penalty". These four are the plan's own list, made structured as §10.3 asks.
|
|
121
|
+
*/
|
|
122
|
+
export const RECUSAL_REASONS = [
|
|
123
|
+
'conflict_of_interest',
|
|
124
|
+
'language',
|
|
125
|
+
'too_sensitive',
|
|
126
|
+
'insufficient_context',
|
|
127
|
+
];
|
|
128
|
+
export const RecusalReasonSchema = z.enum(RECUSAL_REASONS);
|
|
129
|
+
/**
|
|
130
|
+
* The body of `POST /v1/reviewer/assignments/{id}/recuse`.
|
|
131
|
+
*
|
|
132
|
+
* Strict for the same reason as the submission, and with no free-text field
|
|
133
|
+
* beyond a short optional note: a recusal is routing information, and the more
|
|
134
|
+
* it can say about the case, the more it becomes a channel for case content to
|
|
135
|
+
* leak into operational data.
|
|
136
|
+
*/
|
|
137
|
+
export const RecusalSubmissionSchema = z.strictObject({
|
|
138
|
+
reason: RecusalReasonSchema,
|
|
139
|
+
note: z.string().max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH).optional(),
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=reviews.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviews.js","sourceRoot":"","sources":["../../src/reviews.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The universal classification layer (§6.1, §6.3).
|
|
3
|
+
*
|
|
4
|
+
* §6.1 separates three layers and warns that mixing them makes the service
|
|
5
|
+
* unusable across applications with different rules. This module is the FIRST
|
|
6
|
+
* layer only — "what does the material contain or represent?" — and it belongs
|
|
7
|
+
* to CrowdSource. Whether that content violates anything is the second layer
|
|
8
|
+
* (`policies.ts`, per tenant) and whether it should move global trust is the
|
|
9
|
+
* third (`reputation-events.ts`). Nothing here may encode a single tenant's
|
|
10
|
+
* vocabulary.
|
|
11
|
+
*
|
|
12
|
+
* The code list is CLOSED. `other.policy_specific` and `other.unclassifiable`
|
|
13
|
+
* are the escape hatches the plan provides for material that does not fit; an
|
|
14
|
+
* open string would let a tenant mint private codes, which is precisely the
|
|
15
|
+
* cross-application comparability §6.1 exists to protect. Adding a code is an
|
|
16
|
+
* additive change that bumps `UNIVERSAL_TAXONOMY_VERSION`, and §6.4 requires
|
|
17
|
+
* every decision to record the version it was classified under, so historical
|
|
18
|
+
* decisions keep meaning what they meant.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
/** §6.3 families. */
|
|
22
|
+
export const TAXONOMY_FAMILIES = [
|
|
23
|
+
'integrity',
|
|
24
|
+
'harassment',
|
|
25
|
+
'hate',
|
|
26
|
+
'violence',
|
|
27
|
+
'sexual_content',
|
|
28
|
+
'child_safety',
|
|
29
|
+
'self_harm',
|
|
30
|
+
'privacy',
|
|
31
|
+
'commerce',
|
|
32
|
+
'platform_abuse',
|
|
33
|
+
'other',
|
|
34
|
+
];
|
|
35
|
+
export const TaxonomyFamilySchema = z.enum(TAXONOMY_FAMILIES);
|
|
36
|
+
const INTEGRITY_CODES = [
|
|
37
|
+
'integrity.spam',
|
|
38
|
+
'integrity.scam',
|
|
39
|
+
'integrity.fraud',
|
|
40
|
+
'integrity.impersonation',
|
|
41
|
+
'integrity.coordinated_manipulation',
|
|
42
|
+
];
|
|
43
|
+
const HARASSMENT_CODES = [
|
|
44
|
+
'harassment.insult',
|
|
45
|
+
'harassment.targeted_abuse',
|
|
46
|
+
'harassment.sexual_harassment',
|
|
47
|
+
'harassment.doxxing',
|
|
48
|
+
'harassment.credible_threat',
|
|
49
|
+
];
|
|
50
|
+
const HATE_CODES = [
|
|
51
|
+
'hate.dehumanization',
|
|
52
|
+
'hate.slur',
|
|
53
|
+
'hate.incitement',
|
|
54
|
+
'hate.protected_targeting',
|
|
55
|
+
];
|
|
56
|
+
const VIOLENCE_CODES = [
|
|
57
|
+
'violence.graphic',
|
|
58
|
+
'violence.threat',
|
|
59
|
+
'violence.instruction',
|
|
60
|
+
'violence.celebration',
|
|
61
|
+
];
|
|
62
|
+
const SEXUAL_CONTENT_CODES = [
|
|
63
|
+
'sexual_content.nudity',
|
|
64
|
+
'sexual_content.explicit_activity',
|
|
65
|
+
'sexual_content.non_consensual',
|
|
66
|
+
'sexual_content.exploitation',
|
|
67
|
+
];
|
|
68
|
+
const CHILD_SAFETY_CODES = [
|
|
69
|
+
'child_safety.sexualization',
|
|
70
|
+
'child_safety.grooming',
|
|
71
|
+
'child_safety.exploitation',
|
|
72
|
+
];
|
|
73
|
+
const SELF_HARM_CODES = [
|
|
74
|
+
'self_harm.promotion',
|
|
75
|
+
'self_harm.instruction',
|
|
76
|
+
'self_harm.imminent_risk',
|
|
77
|
+
];
|
|
78
|
+
const PRIVACY_CODES = [
|
|
79
|
+
'privacy.personal_information',
|
|
80
|
+
'privacy.intimate_media',
|
|
81
|
+
'privacy.location_exposure',
|
|
82
|
+
];
|
|
83
|
+
const COMMERCE_CODES = [
|
|
84
|
+
'commerce.prohibited_item',
|
|
85
|
+
'commerce.counterfeit',
|
|
86
|
+
'commerce.misleading_listing',
|
|
87
|
+
'commerce.unsafe_product',
|
|
88
|
+
];
|
|
89
|
+
const PLATFORM_ABUSE_CODES = [
|
|
90
|
+
'platform_abuse.ban_evasion',
|
|
91
|
+
'platform_abuse.report_abuse',
|
|
92
|
+
'platform_abuse.automation_abuse',
|
|
93
|
+
];
|
|
94
|
+
const OTHER_CODES = ['other.policy_specific', 'other.unclassifiable'];
|
|
95
|
+
/**
|
|
96
|
+
* §6.3, grouped.
|
|
97
|
+
*
|
|
98
|
+
* §9.4 requires consensus on the "main taxonomic family", not only on the exact
|
|
99
|
+
* code, so the grouping is part of the contract rather than something the
|
|
100
|
+
* consensus engine re-derives from string prefixes.
|
|
101
|
+
*/
|
|
102
|
+
export const TAXONOMY_CODES_BY_FAMILY = Object.freeze({
|
|
103
|
+
integrity: INTEGRITY_CODES,
|
|
104
|
+
harassment: HARASSMENT_CODES,
|
|
105
|
+
hate: HATE_CODES,
|
|
106
|
+
violence: VIOLENCE_CODES,
|
|
107
|
+
sexual_content: SEXUAL_CONTENT_CODES,
|
|
108
|
+
child_safety: CHILD_SAFETY_CODES,
|
|
109
|
+
self_harm: SELF_HARM_CODES,
|
|
110
|
+
privacy: PRIVACY_CODES,
|
|
111
|
+
commerce: COMMERCE_CODES,
|
|
112
|
+
platform_abuse: PLATFORM_ABUSE_CODES,
|
|
113
|
+
other: OTHER_CODES,
|
|
114
|
+
});
|
|
115
|
+
export const UNIVERSAL_TAXONOMY_CODES = [
|
|
116
|
+
...INTEGRITY_CODES,
|
|
117
|
+
...HARASSMENT_CODES,
|
|
118
|
+
...HATE_CODES,
|
|
119
|
+
...VIOLENCE_CODES,
|
|
120
|
+
...SEXUAL_CONTENT_CODES,
|
|
121
|
+
...CHILD_SAFETY_CODES,
|
|
122
|
+
...SELF_HARM_CODES,
|
|
123
|
+
...PRIVACY_CODES,
|
|
124
|
+
...COMMERCE_CODES,
|
|
125
|
+
...PLATFORM_ABUSE_CODES,
|
|
126
|
+
...OTHER_CODES,
|
|
127
|
+
];
|
|
128
|
+
export const TaxonomyCodeSchema = z.enum(UNIVERSAL_TAXONOMY_CODES);
|
|
129
|
+
/**
|
|
130
|
+
* The version of the code list above.
|
|
131
|
+
*
|
|
132
|
+
* §6.4: every decision records the taxonomy version it was decided under, and a
|
|
133
|
+
* policy update never silently rewrites history. Keeping the constant in the
|
|
134
|
+
* same module as the codes is what makes that possible — the two cannot drift.
|
|
135
|
+
* The value is the one the plan uses in Appendix B and §11.6.
|
|
136
|
+
*/
|
|
137
|
+
export const UNIVERSAL_TAXONOMY_VERSION = '2026.1';
|
|
138
|
+
/** The family a code belongs to. */
|
|
139
|
+
export function taxonomyFamilyOf(code) {
|
|
140
|
+
const [family] = code.split('.');
|
|
141
|
+
return TaxonomyFamilySchema.parse(family);
|
|
142
|
+
}
|
|
143
|
+
/** §9.4 / §11.8 severity scale. */
|
|
144
|
+
export const SEVERITIES = ['low', 'medium', 'high', 'critical'];
|
|
145
|
+
export const SeveritySchema = z.enum(SEVERITIES);
|
|
146
|
+
/**
|
|
147
|
+
* How far a finding reaches.
|
|
148
|
+
*
|
|
149
|
+
* §11.6 and Appendix B use `oxy_network`; §11.7.5 additionally names
|
|
150
|
+
* `identity_integrity` as a scope that may produce an Oxy Trust effect. The
|
|
151
|
+
* plan never names the third value — the one that means "this matters to the
|
|
152
|
+
* application and stops there" — even though §6.5's entire argument is that
|
|
153
|
+
* most local restrictions must NOT become global sanctions. `application_local`
|
|
154
|
+
* is the contract's name for it and is the ONE token in this package invented
|
|
155
|
+
* rather than quoted. Modelling it as an absent field instead would make
|
|
156
|
+
* §11.7.5 a presence check, which fails open.
|
|
157
|
+
*/
|
|
158
|
+
export const FINDING_SCOPES = ['application_local', 'oxy_network', 'identity_integrity'];
|
|
159
|
+
export const FindingScopeSchema = z.enum(FINDING_SCOPES);
|
|
160
|
+
/**
|
|
161
|
+
* Scopes that §11.7.5 allows to reach Oxy Trust.
|
|
162
|
+
*
|
|
163
|
+
* Exported as its own schema so `reputation-events.ts` states the rule in the
|
|
164
|
+
* type rather than re-checking it at runtime.
|
|
165
|
+
*/
|
|
166
|
+
export const REPUTATION_ELIGIBLE_FINDING_SCOPES = ['oxy_network', 'identity_integrity'];
|
|
167
|
+
export const ReputationEligibleFindingScopeSchema = z.enum(REPUTATION_ELIGIBLE_FINDING_SCOPES);
|
|
168
|
+
/**
|
|
169
|
+
* Who a finding attributes conduct to.
|
|
170
|
+
*
|
|
171
|
+
* Appendix B and §11.6 use `author`. §11.11 and §11.12 describe confirmed
|
|
172
|
+
* report abuse and confirmed review abuse producing conduct effects of their
|
|
173
|
+
* own, which is where the other two values come from. There is deliberately no
|
|
174
|
+
* `unknown`: attribution exists to name a principal, and §11.7.4 will not let
|
|
175
|
+
* an effect land without a binding proof for that principal anyway.
|
|
176
|
+
*/
|
|
177
|
+
export const FINDING_ATTRIBUTIONS = ['author', 'reporter', 'reviewer'];
|
|
178
|
+
export const FindingAttributionSchema = z.enum(FINDING_ATTRIBUTIONS);
|
|
179
|
+
/**
|
|
180
|
+
* What CrowdSource may recommend an application do.
|
|
181
|
+
*
|
|
182
|
+
* The plan writes action tokens in two places. §6.2, §9.3, §10.7 and Appendix B
|
|
183
|
+
* recommend `remove_or_restrict` and `allow_with_label`; §7.6 tabulates, per
|
|
184
|
+
* decision outcome, the actions an application may take in response. They are
|
|
185
|
+
* the same vocabulary seen from the two ends of one exchange, so the contract
|
|
186
|
+
* carries the union as one closed list — otherwise a recommendation and the
|
|
187
|
+
* enforcement that answers it would not be comparable, and §7.6's requirement
|
|
188
|
+
* that an application "record what it did and why" would compare apples to
|
|
189
|
+
* pears.
|
|
190
|
+
*
|
|
191
|
+
* §7.6's outcome→action table is NOT reproduced as a constraint. It bounds what
|
|
192
|
+
* an application may do in response to an outcome, not what a jury may
|
|
193
|
+
* recommend — Appendix B recommends `remove_or_restrict`, which does not appear
|
|
194
|
+
* in §7.6's `violation` row at all. Binding the two would reject the plan's own
|
|
195
|
+
* reference decision.
|
|
196
|
+
*/
|
|
197
|
+
export const RECOMMENDED_ACTIONS = [
|
|
198
|
+
'remove_or_restrict',
|
|
199
|
+
'allow_with_label',
|
|
200
|
+
'remove',
|
|
201
|
+
'hide',
|
|
202
|
+
'label',
|
|
203
|
+
'age_gate',
|
|
204
|
+
'reduce_distribution',
|
|
205
|
+
'freeze_transaction',
|
|
206
|
+
'suspend_user',
|
|
207
|
+
'request_changes',
|
|
208
|
+
'allow',
|
|
209
|
+
'restore',
|
|
210
|
+
'no_action',
|
|
211
|
+
'request_more_context',
|
|
212
|
+
'hold',
|
|
213
|
+
'local_manual_review',
|
|
214
|
+
'keep_restricted_temporarily',
|
|
215
|
+
'escalate',
|
|
216
|
+
'no_global_effect',
|
|
217
|
+
'specialist_queue',
|
|
218
|
+
'legal_queue',
|
|
219
|
+
'safety_queue',
|
|
220
|
+
];
|
|
221
|
+
export const RecommendedActionSchema = z.enum(RECOMMENDED_ACTIONS);
|
|
222
|
+
/**
|
|
223
|
+
* The context that makes a classification not mean what it usually means —
|
|
224
|
+
* §9.2's and §9.4's "excepción", §6.2's `context`.
|
|
225
|
+
*
|
|
226
|
+
* §6.2's worked example is where this field comes from and what fixes its
|
|
227
|
+
* shape: the jury's finding is `sexual_content.nudity, severity = medium,
|
|
228
|
+
* context = artistic`, and it is that qualifier — not the code and not the
|
|
229
|
+
* severity — that turns the same classification into a violation under one
|
|
230
|
+
* application's policy and not under another's. It therefore belongs to layer
|
|
231
|
+
* one, beside the code: a reviewer describes what the material IS, and
|
|
232
|
+
* "artistic nudity" is a different description from "nudity".
|
|
233
|
+
*
|
|
234
|
+
* §9.4 makes it one of the six dimensions consensus is measured on, which is
|
|
235
|
+
* the reason it must be a CLOSED list. Two reviewers who both answer
|
|
236
|
+
* `no_violation` but for incompatible reasons — one because the material is
|
|
237
|
+
* documentary, one because they think the rule does not cover it at all — have
|
|
238
|
+
* not agreed about the material, and a free-text field could not tell the two
|
|
239
|
+
* apart. An open token would also be a channel for case content to reach a
|
|
240
|
+
* decision record, which §13.5 forbids.
|
|
241
|
+
*
|
|
242
|
+
* Only `artistic` is the plan's own word. The rest are the exception vocabulary
|
|
243
|
+
* every published moderation policy shares, and they are named here rather than
|
|
244
|
+
* left to a tenant because layer one is CrowdSource's (§6.1) — a tenant that
|
|
245
|
+
* could mint its own exception tokens would make findings incomparable across
|
|
246
|
+
* applications, which is exactly what §6.1 exists to prevent. Absence means no
|
|
247
|
+
* exception applies, which is the safe direction: a finding with no exception
|
|
248
|
+
* stands as classified.
|
|
249
|
+
*/
|
|
250
|
+
export const FINDING_CONTEXTS = [
|
|
251
|
+
'artistic',
|
|
252
|
+
'educational',
|
|
253
|
+
'documentary',
|
|
254
|
+
'newsworthy',
|
|
255
|
+
'satire',
|
|
256
|
+
'counter_speech',
|
|
257
|
+
'medical',
|
|
258
|
+
'consensual',
|
|
259
|
+
'fictional',
|
|
260
|
+
];
|
|
261
|
+
export const FindingContextSchema = z.enum(FINDING_CONTEXTS);
|
|
262
|
+
/**
|
|
263
|
+
* A tenant's advance classification of how exposing the material is.
|
|
264
|
+
*
|
|
265
|
+
* The plan names exactly one value — `standard`, in Appendix A — and §7.5
|
|
266
|
+
* clearly implies at least one more (categories that never reach a community
|
|
267
|
+
* jury). Rather than invent the rest, this stays an open lowercase token: it is
|
|
268
|
+
* a HINT (§5.2: "never shown as a verdict"), the authoritative
|
|
269
|
+
* `sensitivity_class` is computed by triage server-side (§12.8), and access to
|
|
270
|
+
* sensitive material is gated on that computed class, never on what the tenant
|
|
271
|
+
* asserted. Closing this list is a product decision that has not been made.
|
|
272
|
+
*/
|
|
273
|
+
export const SensitivityHintSchema = z
|
|
274
|
+
.string()
|
|
275
|
+
.min(1)
|
|
276
|
+
.max(40)
|
|
277
|
+
.regex(/^[a-z][a-z0-9_]*$/, 'must be a lowercase token');
|
|
278
|
+
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taxonomy.js","sourceRoot":"","sources":["../../src/taxonomy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound webhooks (§10.6–§10.9) and the signature contract (§10.8).
|
|
3
|
+
*
|
|
4
|
+
* §10.11 asks for two behaviours that pull in opposite directions: unknown
|
|
5
|
+
* EVENTS must be ignored safely, and unknown FIELDS must not break clients.
|
|
6
|
+
* This module gives each its own schema rather than compromising on one.
|
|
7
|
+
*
|
|
8
|
+
* * `WebhookEventEnvelopeSchema` validates only what every event has —
|
|
9
|
+
* identity, type, timing, tenant — and leaves `data` opaque. A receiver
|
|
10
|
+
* verifies the signature, records the event id for idempotency, and ignores
|
|
11
|
+
* what it does not recognise, without a schema update ever being the reason
|
|
12
|
+
* a delivery fails.
|
|
13
|
+
* * `KnownWebhookEventSchema` is the discriminated union of the eight events
|
|
14
|
+
* §10.6 defines, for the branch that actually handles one.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is `.loose()`. These payloads travel from CrowdSource to a
|
|
17
|
+
* tenant, so an unknown field is a newer server, not an attack; stripping it
|
|
18
|
+
* would silently discard data from a receiver that persists `event.data` for
|
|
19
|
+
* later processing — which is precisely what §10.8's "respond 2xx quickly and
|
|
20
|
+
* queue the processing" tells receivers to do.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { CreateReportResponseSchema } from './case-envelope.js';
|
|
24
|
+
import { DecisionSchema } from './decisions.js';
|
|
25
|
+
import { IdentifierSchema, TimestampSchema } from './primitives.js';
|
|
26
|
+
/** §10.6. */
|
|
27
|
+
export const WEBHOOK_EVENT_TYPES = [
|
|
28
|
+
'report.received',
|
|
29
|
+
'case.created',
|
|
30
|
+
'case.escalated',
|
|
31
|
+
'case.decided',
|
|
32
|
+
'decision.corrected',
|
|
33
|
+
'appeal.created',
|
|
34
|
+
'appeal.decided',
|
|
35
|
+
'case.closed',
|
|
36
|
+
];
|
|
37
|
+
export const WebhookEventTypeSchema = z.enum(WEBHOOK_EVENT_TYPES);
|
|
38
|
+
/**
|
|
39
|
+
* Any event type, including ones this version of the contract does not know.
|
|
40
|
+
*
|
|
41
|
+
* Shape-checked but not enumerated, so that "unknown events must be ignored
|
|
42
|
+
* safely" is something a receiver can DO rather than something it is told about
|
|
43
|
+
* after its parse has already thrown.
|
|
44
|
+
*/
|
|
45
|
+
export const AnyWebhookEventTypeSchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.min(3)
|
|
48
|
+
.max(64)
|
|
49
|
+
.regex(/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$/, 'must be a dotted lowercase event type');
|
|
50
|
+
const webhookEnvelopeShape = {
|
|
51
|
+
id: IdentifierSchema,
|
|
52
|
+
createdAt: TimestampSchema,
|
|
53
|
+
organizationId: IdentifierSchema,
|
|
54
|
+
applicationId: IdentifierSchema,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The envelope every delivery shares (§10.7), with `data` left opaque.
|
|
58
|
+
*
|
|
59
|
+
* Parse with this first. `id` is the idempotency key §10.8 requires receivers
|
|
60
|
+
* to store; `type` decides whether there is anything to do.
|
|
61
|
+
*/
|
|
62
|
+
export const WebhookEventEnvelopeSchema = z.looseObject({
|
|
63
|
+
...webhookEnvelopeShape,
|
|
64
|
+
type: AnyWebhookEventTypeSchema,
|
|
65
|
+
data: z.record(z.string(), z.unknown()),
|
|
66
|
+
});
|
|
67
|
+
const ReportReceivedEventSchema = z.looseObject({
|
|
68
|
+
...webhookEnvelopeShape,
|
|
69
|
+
type: z.literal('report.received'),
|
|
70
|
+
/** §10.6: "optional confirmation of receipt and merge" — §10.4's response. */
|
|
71
|
+
data: CreateReportResponseSchema,
|
|
72
|
+
});
|
|
73
|
+
const CaseCreatedEventSchema = z.looseObject({
|
|
74
|
+
...webhookEnvelopeShape,
|
|
75
|
+
type: z.literal('case.created'),
|
|
76
|
+
data: z.looseObject({ caseId: IdentifierSchema }),
|
|
77
|
+
});
|
|
78
|
+
const CaseEscalatedEventSchema = z.looseObject({
|
|
79
|
+
...webhookEnvelopeShape,
|
|
80
|
+
type: z.literal('case.escalated'),
|
|
81
|
+
data: z.looseObject({ caseId: IdentifierSchema }),
|
|
82
|
+
});
|
|
83
|
+
const CaseDecidedEventSchema = z.looseObject({
|
|
84
|
+
...webhookEnvelopeShape,
|
|
85
|
+
type: z.literal('case.decided'),
|
|
86
|
+
data: z.looseObject({ caseId: IdentifierSchema, decision: DecisionSchema }),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* §10.6: "a later revision replaces the previous decision".
|
|
90
|
+
*
|
|
91
|
+
* The carried decision must therefore name what it superseded. `DecisionSchema`
|
|
92
|
+
* already requires that of any revision past the first; requiring it again here
|
|
93
|
+
* is what stops a correction from carrying revision 1.
|
|
94
|
+
*/
|
|
95
|
+
const DecisionCorrectedEventSchema = z.looseObject({
|
|
96
|
+
...webhookEnvelopeShape,
|
|
97
|
+
type: z.literal('decision.corrected'),
|
|
98
|
+
data: z.looseObject({
|
|
99
|
+
caseId: IdentifierSchema,
|
|
100
|
+
decision: DecisionSchema.refine((decision) => decision.supersedesDecisionId !== undefined, { message: 'a corrected decision must supersede the decision it replaces' }),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
const AppealCreatedEventSchema = z.looseObject({
|
|
104
|
+
...webhookEnvelopeShape,
|
|
105
|
+
type: z.literal('appeal.created'),
|
|
106
|
+
data: z.looseObject({ caseId: IdentifierSchema, appealId: IdentifierSchema }),
|
|
107
|
+
});
|
|
108
|
+
const AppealDecidedEventSchema = z.looseObject({
|
|
109
|
+
...webhookEnvelopeShape,
|
|
110
|
+
type: z.literal('appeal.decided'),
|
|
111
|
+
data: z.looseObject({
|
|
112
|
+
caseId: IdentifierSchema,
|
|
113
|
+
appealId: IdentifierSchema,
|
|
114
|
+
decision: DecisionSchema,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
const CaseClosedEventSchema = z.looseObject({
|
|
118
|
+
...webhookEnvelopeShape,
|
|
119
|
+
type: z.literal('case.closed'),
|
|
120
|
+
data: z.looseObject({ caseId: IdentifierSchema }),
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* The eight events of §10.6, discriminated on `type`.
|
|
124
|
+
*
|
|
125
|
+
* Only `case.decided` has its payload specified in the plan (§10.7). The other
|
|
126
|
+
* seven carry the case they are about and whatever identifies the object that
|
|
127
|
+
* moved; they are loose, so filling them in later is additive and needs no
|
|
128
|
+
* version bump (§10.11).
|
|
129
|
+
*/
|
|
130
|
+
export const KnownWebhookEventSchema = z.discriminatedUnion('type', [
|
|
131
|
+
ReportReceivedEventSchema,
|
|
132
|
+
CaseCreatedEventSchema,
|
|
133
|
+
CaseEscalatedEventSchema,
|
|
134
|
+
CaseDecidedEventSchema,
|
|
135
|
+
DecisionCorrectedEventSchema,
|
|
136
|
+
AppealCreatedEventSchema,
|
|
137
|
+
AppealDecidedEventSchema,
|
|
138
|
+
CaseClosedEventSchema,
|
|
139
|
+
]);
|
|
140
|
+
/** §10.8 headers, in their canonical casing. HTTP header names are case-insensitive; look them up accordingly. */
|
|
141
|
+
export const WEBHOOK_EVENT_ID_HEADER = 'X-CrowdSource-Event-Id';
|
|
142
|
+
export const WEBHOOK_TIMESTAMP_HEADER = 'X-CrowdSource-Timestamp';
|
|
143
|
+
export const WEBHOOK_SIGNATURE_HEADER = 'X-CrowdSource-Signature';
|
|
144
|
+
/** The signature scheme prefix, as in `v1=<hex>`. */
|
|
145
|
+
export const WEBHOOK_SIGNATURE_VERSION = 'v1';
|
|
146
|
+
/** §10.8: reject timestamps more than five minutes away from now. */
|
|
147
|
+
export const WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
148
|
+
/** Unix seconds, as the header carries them. */
|
|
149
|
+
export const WebhookTimestampHeaderSchema = z
|
|
150
|
+
.string()
|
|
151
|
+
.regex(/^[0-9]{1,15}$/, 'must be unix seconds');
|
|
152
|
+
/** `v1=<64 lowercase hex>` — HMAC-SHA256 of the signed payload. */
|
|
153
|
+
export const WebhookSignatureHeaderSchema = z
|
|
154
|
+
.string()
|
|
155
|
+
.regex(/^v1=[0-9a-f]{64}$/, 'must be "v1=" followed by 64 lowercase hex characters');
|
|
156
|
+
/**
|
|
157
|
+
* The exact bytes both sides sign: `timestamp + "." + rawBody` (§10.8).
|
|
158
|
+
*
|
|
159
|
+
* This lives in the contract, not in the signer or the verifier, because a
|
|
160
|
+
* disagreement between those two about what gets signed is invisible until
|
|
161
|
+
* every delivery starts failing — or, far worse, until a signature validates
|
|
162
|
+
* over bytes that are not the ones the receiver goes on to parse. There is no
|
|
163
|
+
* cryptography here and no transport; the HMAC belongs to the backend's signer
|
|
164
|
+
* and the SDK's middleware.
|
|
165
|
+
*
|
|
166
|
+
* `timestamp` is the header value VERBATIM. Re-deriving it from a parsed number
|
|
167
|
+
* is the mistake this signature exists to catch, and the receiver must verify
|
|
168
|
+
* over exactly what arrived.
|
|
169
|
+
*/
|
|
170
|
+
export function buildWebhookSignedPayload(timestamp, rawBody) {
|
|
171
|
+
return `${timestamp}.${rawBody}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* §10.9's backoff, in seconds after the initial attempt.
|
|
175
|
+
*
|
|
176
|
+
* Published behaviour a tenant plans around, so it belongs to the contract
|
|
177
|
+
* rather than to the delivery worker. After the last one the delivery is
|
|
178
|
+
* `dead_letter`, the tenant is alerted, and replay is manual.
|
|
179
|
+
*/
|
|
180
|
+
export const WEBHOOK_RETRY_SCHEDULE_SECONDS = Object.freeze([
|
|
181
|
+
30,
|
|
182
|
+
120,
|
|
183
|
+
900,
|
|
184
|
+
3600,
|
|
185
|
+
21600,
|
|
186
|
+
86400,
|
|
187
|
+
]);
|
|
188
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGpE,aAAa;AACb,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,iBAAiB;IACjB,cAAc;IACd,gBAAgB;IAChB,cAAc;IACd,oBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;CACL,CAAC;AACX,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAGlE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC;KACvC,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,EAAE,CAAC;KACP,KAAK,CAAC,kCAAkC,EAAE,uCAAuC,CAAC,CAAC;AAEtF,MAAM,oBAAoB,GAAG;IAC3B,EAAE,EAAE,gBAAgB;IACpB,SAAS,EAAE,eAAe;IAC1B,cAAc,EAAE,gBAAgB;IAChC,aAAa,EAAE,gBAAgB;CAChC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,WAAW,CAAC;IACtD,GAAG,oBAAoB;IACvB,IAAI,EAAE,yBAAyB;IAC/B,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;CACxC,CAAC,CAAC;AAGH,MAAM,yBAAyB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC9C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC;IAClC,8EAA8E;IAC9E,IAAI,EAAE,0BAA0B;CACjC,CAAC,CAAC;AAEH,MAAM,sBAAsB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC3C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IAC/B,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAClD,CAAC,CAAC;AAEH,MAAM,wBAAwB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC7C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAClD,CAAC,CAAC;AAEH,MAAM,sBAAsB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC3C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IAC/B,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5E,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,4BAA4B,GAAG,CAAC,CAAC,WAAW,CAAC;IACjD,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC;QAClB,MAAM,EAAE,gBAAgB;QACxB,QAAQ,EAAE,cAAc,CAAC,MAAM,CAC7B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,oBAAoB,KAAK,SAAS,EACzD,EAAE,OAAO,EAAE,8DAA8D,EAAE,CAC5E;KACF,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,wBAAwB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC7C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9E,CAAC,CAAC;AAEH,MAAM,wBAAwB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC7C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC;QAClB,MAAM,EAAE,gBAAgB;QACxB,QAAQ,EAAE,gBAAgB;QAC1B,QAAQ,EAAE,cAAc;KACzB,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG,CAAC,CAAC,WAAW,CAAC;IAC1C,GAAG,oBAAoB;IACvB,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAClD,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IAClE,yBAAyB;IACzB,sBAAsB;IACtB,wBAAwB;IACxB,sBAAsB;IACtB,4BAA4B;IAC5B,wBAAwB;IACxB,wBAAwB;IACxB,qBAAqB;CACtB,CAAC,CAAC;AAGH,kHAAkH;AAClH,MAAM,CAAC,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAChE,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,CAAC;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,CAAC;AAElE,qDAAqD;AACrD,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAE9C,qEAAqE;AACrE,MAAM,CAAC,MAAM,mCAAmC,GAAG,GAAG,CAAC;AAEvD,gDAAgD;AAChD,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC;KAC1C,MAAM,EAAE;KACR,KAAK,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC;AAElD,mEAAmE;AACnE,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC;KAC1C,MAAM,EAAE;KACR,KAAK,CAAC,mBAAmB,EAAE,uDAAuD,CAAC,CAAC;AAEvF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAiB,EAAE,OAAe;IAC1E,OAAO,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC7E,EAAE;IACF,GAAG;IACH,GAAG;IACH,IAAK;IACL,KAAM;IACN,KAAM;CACP,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,15 +32,17 @@
|
|
|
32
32
|
* depth-bounded, scalar-typed, key-restricted and free of prototype-bearing
|
|
33
33
|
* names.
|
|
34
34
|
*/
|
|
35
|
-
export * from './closed';
|
|
36
|
-
export * from './primitives';
|
|
37
|
-
export * from './taxonomy';
|
|
38
|
-
export * from './policies';
|
|
39
|
-
export * from './resources';
|
|
40
|
-
export * from './case-envelope';
|
|
41
|
-
export * from './reviews';
|
|
42
|
-
export * from './
|
|
43
|
-
export * from './
|
|
44
|
-
export * from './
|
|
45
|
-
export * from './
|
|
35
|
+
export * from './closed.js';
|
|
36
|
+
export * from './primitives.js';
|
|
37
|
+
export * from './taxonomy.js';
|
|
38
|
+
export * from './policies.js';
|
|
39
|
+
export * from './resources.js';
|
|
40
|
+
export * from './case-envelope.js';
|
|
41
|
+
export * from './reviews.js';
|
|
42
|
+
export * from './reviewer-surface.js';
|
|
43
|
+
export * from './decisions.js';
|
|
44
|
+
export * from './appeals.js';
|
|
45
|
+
export * from './webhooks.js';
|
|
46
|
+
export * from './reputation-events.js';
|
|
47
|
+
export * from './json-schema.js';
|
|
46
48
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC"}
|