@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.
- 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 +59 -59
- 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 +3 -3
- 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 +2 -2
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js +26 -21
- 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/primitives.d.ts +17 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +22 -1
- package/dist/primitives.js.map +1 -1
- package/dist/reputation-events.js +19 -19
- package/dist/reputation-events.js.map +1 -1
- package/dist/resources.d.ts +116 -39
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +143 -121
- 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.d.ts +2 -2
- package/dist/reviews.js +15 -15
- package/dist/reviews.js.map +1 -1
- package/dist/webhooks.d.ts +4 -4
- 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 +13 -8
- package/src/policies.ts +3 -3
- package/src/primitives.ts +25 -0
- package/src/reputation-events.ts +4 -4
- package/src/resources.ts +136 -100
- package/src/reviewer-surface.ts +715 -0
- package/src/reviews.ts +4 -4
- 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()
|
package/src/reputation-events.ts
CHANGED
|
@@ -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
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
283
|
-
}),
|
|
304
|
+
claims: MetadataBagSchema.optional(),
|
|
284
305
|
});
|
|
285
306
|
|
|
286
|
-
const
|
|
307
|
+
const ProfileResourceSchema = z.strictObject({
|
|
287
308
|
...inlineResourceShape,
|
|
288
|
-
type: z.literal('
|
|
289
|
-
data:
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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({
|