@oxyhq/crowdsource-contracts 0.2.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.
Files changed (87) hide show
  1. package/dist/appeals.d.ts +323 -0
  2. package/dist/appeals.d.ts.map +1 -0
  3. package/dist/appeals.js +203 -0
  4. package/dist/appeals.js.map +1 -0
  5. package/dist/case-envelope.d.ts +59 -59
  6. package/dist/case-envelope.d.ts.map +1 -1
  7. package/dist/case-envelope.js +32 -32
  8. package/dist/case-envelope.js.map +1 -1
  9. package/dist/decisions.d.ts +3 -3
  10. package/dist/decisions.d.ts.map +1 -1
  11. package/dist/decisions.js +28 -28
  12. package/dist/decisions.js.map +1 -1
  13. package/dist/esm/appeals.js +200 -0
  14. package/dist/esm/appeals.js.map +1 -0
  15. package/dist/esm/case-envelope.js +380 -0
  16. package/dist/esm/case-envelope.js.map +1 -0
  17. package/dist/esm/closed.js +32 -0
  18. package/dist/esm/closed.js.map +1 -0
  19. package/dist/esm/decisions.js +195 -0
  20. package/dist/esm/decisions.js.map +1 -0
  21. package/dist/esm/index.js +48 -0
  22. package/dist/esm/index.js.map +1 -0
  23. package/dist/esm/json-schema.js +84 -0
  24. package/dist/esm/json-schema.js.map +1 -0
  25. package/dist/esm/package.json +1 -0
  26. package/dist/esm/policies.js +175 -0
  27. package/dist/esm/policies.js.map +1 -0
  28. package/dist/esm/primitives.js +249 -0
  29. package/dist/esm/primitives.js.map +1 -0
  30. package/dist/esm/reputation-events.js +125 -0
  31. package/dist/esm/reputation-events.js.map +1 -0
  32. package/dist/esm/resources.js +455 -0
  33. package/dist/esm/resources.js.map +1 -0
  34. package/dist/esm/reviewer-surface.js +612 -0
  35. package/dist/esm/reviewer-surface.js.map +1 -0
  36. package/dist/esm/reviews.js +141 -0
  37. package/dist/esm/reviews.js.map +1 -0
  38. package/dist/esm/taxonomy.js +278 -0
  39. package/dist/esm/taxonomy.js.map +1 -0
  40. package/dist/esm/webhooks.js +188 -0
  41. package/dist/esm/webhooks.js.map +1 -0
  42. package/dist/index.d.ts +13 -11
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +13 -11
  45. package/dist/index.js.map +1 -1
  46. package/dist/json-schema.d.ts +2 -2
  47. package/dist/json-schema.d.ts.map +1 -1
  48. package/dist/json-schema.js +26 -21
  49. package/dist/json-schema.js.map +1 -1
  50. package/dist/policies.d.ts +1 -1
  51. package/dist/policies.d.ts.map +1 -1
  52. package/dist/policies.js +14 -14
  53. package/dist/policies.js.map +1 -1
  54. package/dist/primitives.d.ts +17 -0
  55. package/dist/primitives.d.ts.map +1 -1
  56. package/dist/primitives.js +22 -1
  57. package/dist/primitives.js.map +1 -1
  58. package/dist/reputation-events.js +19 -19
  59. package/dist/reputation-events.js.map +1 -1
  60. package/dist/resources.d.ts +116 -39
  61. package/dist/resources.d.ts.map +1 -1
  62. package/dist/resources.js +143 -121
  63. package/dist/resources.js.map +1 -1
  64. package/dist/reviewer-surface.d.ts +1956 -0
  65. package/dist/reviewer-surface.d.ts.map +1 -0
  66. package/dist/reviewer-surface.js +615 -0
  67. package/dist/reviewer-surface.js.map +1 -0
  68. package/dist/reviews.d.ts +2 -2
  69. package/dist/reviews.js +15 -15
  70. package/dist/reviews.js.map +1 -1
  71. package/dist/webhooks.d.ts +4 -4
  72. package/dist/webhooks.d.ts.map +1 -1
  73. package/dist/webhooks.js +18 -18
  74. package/dist/webhooks.js.map +1 -1
  75. package/package.json +4 -4
  76. package/src/appeals.ts +229 -0
  77. package/src/case-envelope.ts +5 -5
  78. package/src/decisions.ts +6 -6
  79. package/src/index.ts +13 -11
  80. package/src/json-schema.ts +13 -8
  81. package/src/policies.ts +3 -3
  82. package/src/primitives.ts +25 -0
  83. package/src/reputation-events.ts +4 -4
  84. package/src/resources.ts +136 -100
  85. package/src/reviewer-surface.ts +715 -0
  86. package/src/reviews.ts +4 -4
  87. package/src/webhooks.ts +4 -4
package/src/primitives.ts CHANGED
@@ -179,6 +179,31 @@ export const LanguageTagSchema = z
179
179
  */
180
180
  export const HttpUrlSchema = z.url({ protocol: /^https?$/ }).max(2_048);
181
181
 
182
+ /**
183
+ * A bare Oxy file id — the one way bytes enter a case.
184
+ *
185
+ * Evidence goes through the Oxy media chokepoint (`getFileDownloadUrl`) rather
186
+ * than a presigned bucket, which is the §12.3 divergence recorded in
187
+ * `AGENTS.md`. A file id is what that chokepoint takes, so it is what an asset
188
+ * carries: never a URL, never a host, never a variant. The reviewer client
189
+ * resolves it once through Bloom's `ImageResolver`, so nothing downstream needs
190
+ * to know where `cloud.oxy.so` is.
191
+ *
192
+ * Deliberately opaque, for the same reason `IdentifierSchema` is: the format
193
+ * belongs to Oxy, not to CrowdSource, and pinning it here would make an Oxy
194
+ * change look like a CrowdSource contract violation. What is pinned is that it
195
+ * is a bounded, non-empty token with no separators that could smuggle a path or
196
+ * a scheme into a resolver — `..`, `/` and `:` are all excluded by the pattern.
197
+ */
198
+ export const OxyFileIdSchema = z
199
+ .string()
200
+ .min(1)
201
+ .max(CONTRACT_LIMITS.IDENTIFIER_MAX_LENGTH)
202
+ .regex(
203
+ /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
204
+ 'must be a bare Oxy file id: letters, digits, "_" or "-", starting with a letter or digit',
205
+ );
206
+
182
207
  /** A `type/subtype` media type, without parameters. */
183
208
  export const MimeTypeSchema = z
184
209
  .string()
@@ -36,15 +36,15 @@
36
36
 
37
37
  import { z } from 'zod';
38
38
 
39
- import { CONTRACT_LIMITS, IdentifierSchema, Sha256DigestSchema } from './primitives';
40
- import { PrincipalTypeSchema } from './case-envelope';
41
- import { ReputationPolicyVersionsSchema } from './policies';
39
+ import { CONTRACT_LIMITS, IdentifierSchema, Sha256DigestSchema } from './primitives.js';
40
+ import { PrincipalTypeSchema } from './case-envelope.js';
41
+ import { ReputationPolicyVersionsSchema } from './policies.js';
42
42
  import {
43
43
  FindingAttributionSchema,
44
44
  ReputationEligibleFindingScopeSchema,
45
45
  SeveritySchema,
46
46
  TaxonomyCodeSchema,
47
- } from './taxonomy';
47
+ } from './taxonomy.js';
48
48
 
49
49
  /**
50
50
  * §11.6 names one event type. §11.5 names four bridge operations — apply,
package/src/resources.ts CHANGED
@@ -38,11 +38,12 @@ import {
38
38
  LanguageTagSchema,
39
39
  MetadataBagSchema,
40
40
  MimeTypeSchema,
41
+ OxyFileIdSchema,
41
42
  Sha256DigestSchema,
42
43
  TimestampSchema,
43
- } from './primitives';
44
- import { PolicyVersionSchema } from './policies';
45
- import { SensitivityHintSchema } from './taxonomy';
44
+ } from './primitives.js';
45
+ import { PolicyVersionSchema } from './policies.js';
46
+ import { SensitivityHintSchema } from './taxonomy.js';
46
47
 
47
48
  /** A resource id, unique within one envelope (§5.2 `id`). */
48
49
  export const ResourceIdSchema = IdentifierSchema;
@@ -98,34 +99,43 @@ export type TextFormatting = z.infer<typeof TextFormattingSchema>;
98
99
  /**
99
100
  * A reference to bytes held outside the envelope (§5.2 `asset`).
100
101
  *
101
- * Exactly one of `uploadId` and `url` never both, never neither. Two
102
- * locations for one piece of evidence means two answers to "what exactly did
103
- * the jury look at", and §5.6 requires the case to pin the exact version that
104
- * was reported. `sha256` is required for the same reason: it is what makes the
105
- * answer verifiable after the application has deleted the original.
102
+ * **`fileId` is where the bytes are. `url` is where they came from.** Those are
103
+ * not alternatives, and the earlier contract encoding them as "exactly one of
104
+ * `uploadId` or `url`" is what left an integrator with no safe way to attach an
105
+ * image at all: the `uploadId` branch needed a presigned upload route that was
106
+ * superseded by the Oxy media chokepoint before it was ever built, and the `url`
107
+ * branch put a URL on the reporting application's own host in front of a
108
+ * reviewer — breaking §9.1's branding rule and the chokepoint rule at once.
109
+ *
110
+ * So `fileId` is required and `url` is optional provenance:
111
+ *
112
+ * * **`fileId`** — a bare Oxy file id. The application uploads the bytes
113
+ * through the Oxy media chokepoint with its own Oxy credentials and passes
114
+ * the id. That is why CrowdSource serves no upload route of its own.
115
+ * * **`url`** — a PROVENANCE RECORD, and never a fetch target. A third-party
116
+ * URL is not wrong the way a first-party one is (a federated post's image
117
+ * genuinely lives elsewhere), but no reviewer client may dereference it:
118
+ * fetching it would tell that host exactly when its content is under review,
119
+ * which lets the reported party correlate reviewer accesses, and it would
120
+ * deliver bytes live rather than the pinned ones §5.6 requires.
121
+ *
122
+ * `sha256` stays required, and requiring `fileId` alongside it costs an
123
+ * integrator nothing new — an application able to compute that digest already
124
+ * holds the bytes. What changes is that it puts bytes it already has somewhere a
125
+ * reviewer can read safely, instead of pointing at its own host.
106
126
  */
107
- export const AssetRefSchema = z
108
- .strictObject({
109
- uploadId: IdentifierSchema.optional(),
110
- url: HttpUrlSchema.optional(),
111
- mimeType: MimeTypeSchema,
112
- sha256: Sha256DigestSchema,
113
- sizeBytes: z.number().int().positive().optional(),
114
- width: z.number().int().positive().optional(),
115
- height: z.number().int().positive().optional(),
116
- durationSeconds: z.number().positive().optional(),
117
- })
118
- .superRefine((asset, ctx) => {
119
- const hasUpload = asset.uploadId !== undefined;
120
- const hasUrl = asset.url !== undefined;
121
- if (hasUpload === hasUrl) {
122
- ctx.addIssue({
123
- code: 'custom',
124
- path: ['uploadId'],
125
- message: 'an asset must reference exactly one of uploadId or url',
126
- });
127
- }
128
- });
127
+ export const AssetRefSchema = z.strictObject({
128
+ /** Bare Oxy file id. The only source of the bytes a jury sees. */
129
+ fileId: OxyFileIdSchema,
130
+ /** Where the material was found. Recorded, never fetched — see above. */
131
+ url: HttpUrlSchema.optional(),
132
+ mimeType: MimeTypeSchema,
133
+ sha256: Sha256DigestSchema,
134
+ sizeBytes: z.number().int().positive().optional(),
135
+ width: z.number().int().positive().optional(),
136
+ height: z.number().int().positive().optional(),
137
+ durationSeconds: z.number().positive().optional(),
138
+ });
129
139
  export type AssetRef = z.infer<typeof AssetRefSchema>;
130
140
 
131
141
  const mediaTypeMismatch = (
@@ -166,13 +176,46 @@ const resourceBaseShape = {
166
176
  const inlineResourceShape = { ...resourceBaseShape, sha256: Sha256DigestSchema };
167
177
  const assetResourceShape = { ...resourceBaseShape, sha256: Sha256DigestSchema.optional() };
168
178
 
179
+ /**
180
+ * The `data` of each resource type, named.
181
+ *
182
+ * Extracted so the reviewer surface (`reviewer-surface.ts`) can build its own
183
+ * resource union — same content, no `sha256`, a media handle instead of an asset
184
+ * locator — out of THESE schemas rather than restating twelve content shapes.
185
+ * A second copy would drift, and the direction it would drift in is a jury shown
186
+ * less than the application sent.
187
+ */
188
+ export const TextResourceDataSchema = z.strictObject({
189
+ text: z.string().min(1).max(CONTRACT_LIMITS.TEXT_RESOURCE_MAX_LENGTH),
190
+ formatting: TextFormattingSchema.optional(),
191
+ });
192
+
169
193
  const TextResourceSchema = z.strictObject({
170
194
  ...inlineResourceShape,
171
195
  type: z.literal('text'),
172
- data: z.strictObject({
173
- text: z.string().min(1).max(CONTRACT_LIMITS.TEXT_RESOURCE_MAX_LENGTH),
174
- formatting: TextFormattingSchema.optional(),
175
- }),
196
+ data: TextResourceDataSchema,
197
+ });
198
+
199
+ export const VideoResourceDataSchema = z.strictObject({
200
+ timeRange: z
201
+ .strictObject({
202
+ startSeconds: z.number().nonnegative(),
203
+ endSeconds: z.number().positive(),
204
+ })
205
+ .refine((range) => range.endSeconds > range.startSeconds, {
206
+ message: 'endSeconds must be greater than startSeconds',
207
+ path: ['endSeconds'],
208
+ })
209
+ .optional(),
210
+ });
211
+
212
+ export const AudioResourceDataSchema = z.strictObject({
213
+ transcript: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
214
+ });
215
+
216
+ export const DocumentResourceDataSchema = z.strictObject({
217
+ title: z.string().min(1).max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH),
218
+ extractedText: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
176
219
  });
177
220
 
178
221
  const ImageResourceSchema = z
@@ -188,20 +231,7 @@ const VideoResourceSchema = z
188
231
  ...assetResourceShape,
189
232
  type: z.literal('video'),
190
233
  asset: AssetRefSchema,
191
- data: z
192
- .strictObject({
193
- timeRange: z
194
- .strictObject({
195
- startSeconds: z.number().nonnegative(),
196
- endSeconds: z.number().positive(),
197
- })
198
- .refine((range) => range.endSeconds > range.startSeconds, {
199
- message: 'endSeconds must be greater than startSeconds',
200
- path: ['endSeconds'],
201
- })
202
- .optional(),
203
- })
204
- .optional(),
234
+ data: VideoResourceDataSchema.optional(),
205
235
  })
206
236
  .superRefine((resource, ctx) => {
207
237
  mediaTypeMismatch(ctx, resource.asset.mimeType, ['video/']);
@@ -219,11 +249,7 @@ const AudioResourceSchema = z
219
249
  ...assetResourceShape,
220
250
  type: z.literal('audio'),
221
251
  asset: AssetRefSchema,
222
- data: z
223
- .strictObject({
224
- transcript: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
225
- })
226
- .optional(),
252
+ data: AudioResourceDataSchema.optional(),
227
253
  })
228
254
  .superRefine((resource, ctx) => {
229
255
  mediaTypeMismatch(ctx, resource.asset.mimeType, ['audio/']);
@@ -241,34 +267,30 @@ const DocumentResourceSchema = z
241
267
  ...assetResourceShape,
242
268
  type: z.literal('document'),
243
269
  asset: AssetRefSchema,
244
- data: z.strictObject({
245
- title: z.string().min(1).max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH),
246
- extractedText: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
247
- }),
270
+ data: DocumentResourceDataSchema,
248
271
  })
249
272
  .superRefine((resource, ctx) =>
250
273
  mediaTypeMismatch(ctx, resource.asset.mimeType, ['application/', 'text/']),
251
274
  );
252
275
 
276
+ export const LinkResourceDataSchema = z.strictObject({
277
+ url: HttpUrlSchema,
278
+ title: z.string().max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH).optional(),
279
+ resolvedHost: z
280
+ .string()
281
+ .max(253)
282
+ .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/, 'must be a hostname')
283
+ .optional(),
284
+ snapshot: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
285
+ });
286
+
253
287
  const LinkResourceSchema = z.strictObject({
254
288
  ...inlineResourceShape,
255
289
  type: z.literal('link'),
256
- data: z.strictObject({
257
- url: HttpUrlSchema,
258
- title: z.string().max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH).optional(),
259
- resolvedHost: z
260
- .string()
261
- .max(253)
262
- .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/, 'must be a hostname')
263
- .optional(),
264
- snapshot: z.string().max(CONTRACT_LIMITS.EXTRACTED_TEXT_MAX_LENGTH).optional(),
265
- }),
290
+ data: LinkResourceDataSchema,
266
291
  });
267
292
 
268
- const ProfileResourceSchema = z.strictObject({
269
- ...inlineResourceShape,
270
- type: z.literal('profile'),
271
- data: z.strictObject({
293
+ export const ProfileResourceDataSchema = z.strictObject({
272
294
  /**
273
295
  * Every field is optional on purpose. A federated or unresolved actor
274
296
  * routinely has no display name, and requiring one would push applications
@@ -279,41 +301,50 @@ const ProfileResourceSchema = z.strictObject({
279
301
  bio: z.string().max(CONTRACT_LIMITS.LONG_TEXT_MAX_LENGTH).optional(),
280
302
  /** An `image` resource in the same envelope. */
281
303
  avatarRef: ResourceIdSchema.optional(),
282
- claims: MetadataBagSchema.optional(),
283
- }),
304
+ claims: MetadataBagSchema.optional(),
284
305
  });
285
306
 
286
- const ConversationResourceSchema = z.strictObject({
307
+ const ProfileResourceSchema = z.strictObject({
287
308
  ...inlineResourceShape,
288
- type: z.literal('conversation'),
289
- data: z.strictObject({
309
+ type: z.literal('profile'),
310
+ data: ProfileResourceDataSchema,
311
+ });
312
+
313
+ export const ConversationResourceDataSchema = z.strictObject({
290
314
  /**
291
315
  * Ordered (§5.3). §13.5 asks for five messages around the incident rather
292
316
  * than the whole thread, which is why this is a bounded list of ids the
293
317
  * application chose, not a pointer to a conversation.
294
318
  */
295
- messageResourceIds: z
296
- .array(ResourceIdSchema)
297
- .min(1)
298
- .max(CONTRACT_LIMITS.CONVERSATION_MESSAGES_MAX),
299
- }),
319
+ messageResourceIds: z
320
+ .array(ResourceIdSchema)
321
+ .min(1)
322
+ .max(CONTRACT_LIMITS.CONVERSATION_MESSAGES_MAX),
323
+ });
324
+
325
+ const ConversationResourceSchema = z.strictObject({
326
+ ...inlineResourceShape,
327
+ type: z.literal('conversation'),
328
+ data: ConversationResourceDataSchema,
329
+ });
330
+
331
+ export const ListingResourceDataSchema = z.strictObject({
332
+ title: z.string().min(1).max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH),
333
+ description: z.string().max(CONTRACT_LIMITS.LONG_TEXT_MAX_LENGTH).optional(),
334
+ price: z.number().finite().nonnegative().optional(),
335
+ currency: z
336
+ .string()
337
+ .regex(/^[A-Z]{3}$/, 'must be an ISO 4217 alphabetic code')
338
+ .optional(),
339
+ sellerRef: PrincipalRefSchema.optional(),
340
+ mediaRefs: z.array(ResourceIdSchema).max(CONTRACT_LIMITS.LISTING_MEDIA_REFS_MAX).optional(),
300
341
  });
301
342
 
302
343
  const ListingResourceSchema = z
303
344
  .strictObject({
304
345
  ...inlineResourceShape,
305
346
  type: z.literal('listing'),
306
- data: z.strictObject({
307
- title: z.string().min(1).max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH),
308
- description: z.string().max(CONTRACT_LIMITS.LONG_TEXT_MAX_LENGTH).optional(),
309
- price: z.number().finite().nonnegative().optional(),
310
- currency: z
311
- .string()
312
- .regex(/^[A-Z]{3}$/, 'must be an ISO 4217 alphabetic code')
313
- .optional(),
314
- sellerRef: PrincipalRefSchema.optional(),
315
- mediaRefs: z.array(ResourceIdSchema).max(CONTRACT_LIMITS.LISTING_MEDIA_REFS_MAX).optional(),
316
- }),
347
+ data: ListingResourceDataSchema,
317
348
  })
318
349
  .superRefine((resource, ctx) => {
319
350
  const hasPrice = resource.data.price !== undefined;
@@ -333,15 +364,17 @@ const COARSE_COORDINATE_DECIMALS = 2;
333
364
  const isCoarse = (value: number): boolean =>
334
365
  Number.isInteger(value * 10 ** COARSE_COORDINATE_DECIMALS);
335
366
 
367
+ export const LocationResourceDataSchema = z.strictObject({
368
+ label: z.string().max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH).optional(),
369
+ latitude: z.number().min(-90).max(90).optional(),
370
+ longitude: z.number().min(-180).max(180).optional(),
371
+ });
372
+
336
373
  const LocationResourceSchema = z
337
374
  .strictObject({
338
375
  ...inlineResourceShape,
339
376
  type: z.literal('location'),
340
- data: z.strictObject({
341
- label: z.string().max(CONTRACT_LIMITS.SHORT_TEXT_MAX_LENGTH).optional(),
342
- latitude: z.number().min(-90).max(90).optional(),
343
- longitude: z.number().min(-180).max(180).optional(),
344
- }),
377
+ data: LocationResourceDataSchema,
345
378
  })
346
379
  .superRefine((resource, ctx) => {
347
380
  const { label, latitude, longitude } = resource.data;
@@ -381,12 +414,15 @@ const LocationResourceSchema = z
381
414
  }
382
415
  });
383
416
 
417
+ export const MetadataResourceDataSchema = MetadataBagSchema.refine(
418
+ (bag) => Object.keys(bag).length >= 1,
419
+ { message: 'a metadata resource must carry at least one field' },
420
+ );
421
+
384
422
  const MetadataResourceSchema = z.strictObject({
385
423
  ...inlineResourceShape,
386
424
  type: z.literal('metadata'),
387
- data: MetadataBagSchema.refine((bag) => Object.keys(bag).length >= 1, {
388
- message: 'a metadata resource must carry at least one field',
389
- }),
425
+ data: MetadataResourceDataSchema,
390
426
  });
391
427
 
392
428
  const CustomResourceSchema = z.strictObject({