@authorizedretailers/spec 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.
Files changed (98) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-SPEC +396 -0
  3. package/README.md +130 -0
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.js +7 -0
  6. package/dist/identity.d.ts +27 -0
  7. package/dist/identity.js +92 -0
  8. package/dist/index.d.ts +8 -0
  9. package/dist/index.js +8 -0
  10. package/dist/jcs.d.ts +5 -0
  11. package/dist/jcs.js +47 -0
  12. package/dist/jws.d.ts +42 -0
  13. package/dist/jws.js +90 -0
  14. package/dist/published.d.ts +15 -0
  15. package/dist/published.js +15 -0
  16. package/dist/schemas.d.ts +519 -0
  17. package/dist/schemas.js +662 -0
  18. package/dist/timestamp.d.ts +6 -0
  19. package/dist/timestamp.js +26 -0
  20. package/dist/types.d.ts +116 -0
  21. package/dist/types.js +2 -0
  22. package/dist/validate.d.ts +28 -0
  23. package/dist/validate.js +147 -0
  24. package/fixtures/file/invalid/full-all-mixed.json +47 -0
  25. package/fixtures/file/invalid/full-amazon-missing-seller-id.json +45 -0
  26. package/fixtures/file/invalid/full-attached-jws.json +50 -0
  27. package/fixtures/file/invalid/full-authorization-expires-after-file.json +46 -0
  28. package/fixtures/file/invalid/full-duplicate-authorization-id.json +80 -0
  29. package/fixtures/file/invalid/full-empty-territories.json +43 -0
  30. package/fixtures/file/invalid/full-expires-before-issued.json +46 -0
  31. package/fixtures/file/invalid/full-expiry-over-180-days.json +46 -0
  32. package/fixtures/file/invalid/full-extra-property.json +47 -0
  33. package/fixtures/file/invalid/full-impossible-date.json +46 -0
  34. package/fixtures/file/invalid/full-lowercase-territory.json +45 -0
  35. package/fixtures/file/invalid/full-missing-authorization-expires.json +45 -0
  36. package/fixtures/file/invalid/full-missing-file-expires.json +45 -0
  37. package/fixtures/file/invalid/full-missing-product-lines.json +43 -0
  38. package/fixtures/file/invalid/full-missing-scope.json +37 -0
  39. package/fixtures/file/invalid/full-missing-territories.json +42 -0
  40. package/fixtures/file/invalid/full-name-only-retailer.json +30 -0
  41. package/fixtures/file/invalid/full-no-channels.json +31 -0
  42. package/fixtures/file/invalid/full-non-utc-timestamp.json +46 -0
  43. package/fixtures/file/invalid/full-physical-missing-country.json +50 -0
  44. package/fixtures/file/invalid/full-signature-missing-kid.json +49 -0
  45. package/fixtures/file/invalid/full-unknown-channel-type.json +50 -0
  46. package/fixtures/file/invalid/full-web-domain-is-url.json +46 -0
  47. package/fixtures/file/invalid/full-web-missing-domain.json +45 -0
  48. package/fixtures/file/invalid/full-wildcard-mixed.json +46 -0
  49. package/fixtures/file/invalid/full-wrong-spec.json +46 -0
  50. package/fixtures/file/invalid/pointer-http-url.json +9 -0
  51. package/fixtures/file/invalid/private-missing-verify.json +8 -0
  52. package/fixtures/file/invalid/private-with-authorizations.json +45 -0
  53. package/fixtures/file/invalid/unknown-form.json +9 -0
  54. package/fixtures/file/valid/full-fractional-seconds.json +46 -0
  55. package/fixtures/file/valid/full-max-expiry.json +46 -0
  56. package/fixtures/file/valid/full-no-authorizations.json +11 -0
  57. package/fixtures/file/valid/full-signed.json +50 -0
  58. package/fixtures/file/valid/full-unsigned-handwritten.json +46 -0
  59. package/fixtures/file/valid/full-worldwide-physical-proposed.json +82 -0
  60. package/fixtures/file/valid/pointer.json +9 -0
  61. package/fixtures/file/valid/private.json +9 -0
  62. package/fixtures/manifest.json +434 -0
  63. package/fixtures/verify-request/invalid/domain-with-scheme.json +10 -0
  64. package/fixtures/verify-request/invalid/missing-channel.json +5 -0
  65. package/fixtures/verify-request/invalid/physical-channel.json +10 -0
  66. package/fixtures/verify-request/invalid/retailer-name-only.json +6 -0
  67. package/fixtures/verify-request/invalid/wildcard-territory.json +10 -0
  68. package/fixtures/verify-request/valid/amazon.json +10 -0
  69. package/fixtures/verify-request/valid/web-no-product-line.json +8 -0
  70. package/fixtures/verify-response/invalid/authorized-missing-authorization-id.json +12 -0
  71. package/fixtures/verify-response/invalid/missing-observed.json +12 -0
  72. package/fixtures/verify-response/invalid/missing-signature.json +9 -0
  73. package/fixtures/verify-response/invalid/missing-valid-until.json +12 -0
  74. package/fixtures/verify-response/invalid/observed-without-last-seen.json +13 -0
  75. package/fixtures/verify-response/invalid/unknown-status.json +13 -0
  76. package/fixtures/verify-response/invalid/unlisted-observed.json +13 -0
  77. package/fixtures/verify-response/invalid/unlisted-with-authorization-id.json +12 -0
  78. package/fixtures/verify-response/invalid/unlisted-with-reason.json +12 -0
  79. package/fixtures/verify-response/invalid/valid-until-before-checked.json +13 -0
  80. package/fixtures/verify-response/invalid/valid-until-over-24h.json +13 -0
  81. package/fixtures/verify-response/invalid/with-tier.json +14 -0
  82. package/fixtures/verify-response/valid/authorized-observed.json +15 -0
  83. package/fixtures/verify-response/valid/authorized.json +13 -0
  84. package/fixtures/verify-response/valid/brand-unverified-no-reason.json +11 -0
  85. package/fixtures/verify-response/valid/brand-unverified-reason.json +12 -0
  86. package/fixtures/verify-response/valid/disputed.json +13 -0
  87. package/fixtures/verify-response/valid/expired.json +13 -0
  88. package/fixtures/verify-response/valid/unlisted.json +11 -0
  89. package/fixtures/verify-response/valid/with-signal.json +19 -0
  90. package/package.json +76 -0
  91. package/schemas/common.json +170 -0
  92. package/schemas/file-full.json +23 -0
  93. package/schemas/file-pointer.json +14 -0
  94. package/schemas/file-private.json +14 -0
  95. package/schemas/file.json +13 -0
  96. package/schemas/verify-request.json +18 -0
  97. package/schemas/verify-response.json +61 -0
  98. package/spec/spec-v0.1.md +282 -0
@@ -0,0 +1,662 @@
1
+ // Generated from schemas/*.json by scripts/embed-schemas.mjs. Do not edit by hand.
2
+ export const common = {
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "$id": "https://authorizedretailers.ai/schemas/0.1/common.json",
5
+ "title": "Authorized Retailers v0.1 shared definitions",
6
+ "$defs": {
7
+ "specVersion": {
8
+ "const": "authorized-retailers/0.1"
9
+ },
10
+ "timestamp": {
11
+ "description": "RFC 3339 timestamp in UTC with a Z suffix.",
12
+ "type": "string",
13
+ "format": "date-time",
14
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
15
+ },
16
+ "domain": {
17
+ "description": "A bare hostname: no scheme, port, path or trailing dot. Compared case-insensitively.",
18
+ "type": "string",
19
+ "maxLength": 253,
20
+ "pattern": "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{1,59})$"
21
+ },
22
+ "countryCode": {
23
+ "description": "ISO 3166-1 alpha-2, uppercase.",
24
+ "type": "string",
25
+ "pattern": "^[A-Z]{2}$"
26
+ },
27
+ "httpsUrl": {
28
+ "type": "string",
29
+ "format": "uri",
30
+ "pattern": "^https://",
31
+ "maxLength": 2048
32
+ },
33
+ "id": {
34
+ "type": "string",
35
+ "pattern": "^[A-Za-z0-9_-]{1,64}$"
36
+ },
37
+ "nonEmptyString": {
38
+ "type": "string",
39
+ "minLength": 1,
40
+ "maxLength": 256
41
+ },
42
+ "brand": {
43
+ "type": "object",
44
+ "required": [
45
+ "name",
46
+ "domain"
47
+ ],
48
+ "additionalProperties": false,
49
+ "properties": {
50
+ "name": {
51
+ "$ref": "#/$defs/nonEmptyString"
52
+ },
53
+ "domain": {
54
+ "$ref": "#/$defs/domain"
55
+ }
56
+ }
57
+ },
58
+ "sellerId": {
59
+ "type": "string",
60
+ "pattern": "^\\S{1,128}$"
61
+ },
62
+ "marketplaceChannel": {
63
+ "type": "object",
64
+ "required": [
65
+ "type",
66
+ "marketplace",
67
+ "seller_id"
68
+ ],
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "type": {
72
+ "enum": [
73
+ "amazon",
74
+ "walmart",
75
+ "ebay"
76
+ ]
77
+ },
78
+ "marketplace": {
79
+ "$ref": "#/$defs/countryCode"
80
+ },
81
+ "seller_id": {
82
+ "$ref": "#/$defs/sellerId"
83
+ }
84
+ }
85
+ },
86
+ "webChannel": {
87
+ "type": "object",
88
+ "required": [
89
+ "type",
90
+ "domain"
91
+ ],
92
+ "additionalProperties": false,
93
+ "properties": {
94
+ "type": {
95
+ "const": "web"
96
+ },
97
+ "domain": {
98
+ "$ref": "#/$defs/domain"
99
+ }
100
+ }
101
+ },
102
+ "physicalChannel": {
103
+ "type": "object",
104
+ "required": [
105
+ "type",
106
+ "address",
107
+ "country"
108
+ ],
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "type": {
112
+ "const": "physical"
113
+ },
114
+ "address": {
115
+ "type": "string",
116
+ "minLength": 1,
117
+ "maxLength": 512
118
+ },
119
+ "country": {
120
+ "$ref": "#/$defs/countryCode"
121
+ }
122
+ }
123
+ },
124
+ "verifiableChannel": {
125
+ "description": "A channel an agent can see and match on (section 6). Excludes physical.",
126
+ "oneOf": [
127
+ {
128
+ "$ref": "#/$defs/marketplaceChannel"
129
+ },
130
+ {
131
+ "$ref": "#/$defs/webChannel"
132
+ }
133
+ ]
134
+ },
135
+ "channel": {
136
+ "oneOf": [
137
+ {
138
+ "$ref": "#/$defs/marketplaceChannel"
139
+ },
140
+ {
141
+ "$ref": "#/$defs/webChannel"
142
+ },
143
+ {
144
+ "$ref": "#/$defs/physicalChannel"
145
+ }
146
+ ]
147
+ },
148
+ "party": {
149
+ "type": "object",
150
+ "required": [
151
+ "name"
152
+ ],
153
+ "additionalProperties": false,
154
+ "properties": {
155
+ "name": {
156
+ "$ref": "#/$defs/nonEmptyString"
157
+ },
158
+ "entity_id": {
159
+ "$ref": "#/$defs/id"
160
+ }
161
+ }
162
+ },
163
+ "territories": {
164
+ "description": "ISO 3166-1 alpha-2 codes, or exactly [\"*\"] for worldwide.",
165
+ "type": "array",
166
+ "minItems": 1,
167
+ "uniqueItems": true,
168
+ "anyOf": [
169
+ {
170
+ "const": [
171
+ "*"
172
+ ]
173
+ },
174
+ {
175
+ "items": {
176
+ "$ref": "#/$defs/countryCode"
177
+ }
178
+ }
179
+ ]
180
+ },
181
+ "productLine": {
182
+ "type": "string",
183
+ "minLength": 1,
184
+ "maxLength": 64,
185
+ "pattern": "^\\S(.*\\S)?$"
186
+ },
187
+ "productLines": {
188
+ "description": "Named product lines, or exactly [\"all\"].",
189
+ "type": "array",
190
+ "minItems": 1,
191
+ "uniqueItems": true,
192
+ "anyOf": [
193
+ {
194
+ "const": [
195
+ "all"
196
+ ]
197
+ },
198
+ {
199
+ "items": {
200
+ "allOf": [
201
+ {
202
+ "$ref": "#/$defs/productLine"
203
+ },
204
+ {
205
+ "not": {
206
+ "const": "all"
207
+ }
208
+ }
209
+ ]
210
+ }
211
+ }
212
+ ]
213
+ },
214
+ "scope": {
215
+ "type": "object",
216
+ "required": [
217
+ "territories",
218
+ "product_lines"
219
+ ],
220
+ "additionalProperties": false,
221
+ "properties": {
222
+ "territories": {
223
+ "$ref": "#/$defs/territories"
224
+ },
225
+ "product_lines": {
226
+ "$ref": "#/$defs/productLines"
227
+ }
228
+ }
229
+ },
230
+ "authorization": {
231
+ "type": "object",
232
+ "required": [
233
+ "id",
234
+ "retailer",
235
+ "channels",
236
+ "scope",
237
+ "expires"
238
+ ],
239
+ "additionalProperties": false,
240
+ "properties": {
241
+ "id": {
242
+ "$ref": "#/$defs/id"
243
+ },
244
+ "retailer": {
245
+ "$ref": "#/$defs/party"
246
+ },
247
+ "channels": {
248
+ "description": "Section 6: every authorization MUST name at least one channel identifier.",
249
+ "type": "array",
250
+ "minItems": 1,
251
+ "items": {
252
+ "$ref": "#/$defs/channel"
253
+ }
254
+ },
255
+ "scope": {
256
+ "$ref": "#/$defs/scope"
257
+ },
258
+ "proposed_by": {
259
+ "description": "The distributor that proposed this authorization, if any (section 3).",
260
+ "oneOf": [
261
+ {
262
+ "type": "null"
263
+ },
264
+ {
265
+ "$ref": "#/$defs/party"
266
+ }
267
+ ]
268
+ },
269
+ "expires": {
270
+ "$ref": "#/$defs/timestamp"
271
+ }
272
+ }
273
+ },
274
+ "signature": {
275
+ "type": "object",
276
+ "required": [
277
+ "kid",
278
+ "jws"
279
+ ],
280
+ "additionalProperties": false,
281
+ "properties": {
282
+ "kid": {
283
+ "type": "string",
284
+ "pattern": "^[A-Za-z0-9._-]{1,64}$"
285
+ },
286
+ "jws": {
287
+ "description": "Detached compact JWS: header..signature (empty payload segment).",
288
+ "type": "string",
289
+ "pattern": "^[A-Za-z0-9_-]+\\.\\.[A-Za-z0-9_-]+$"
290
+ }
291
+ }
292
+ }
293
+ }
294
+ };
295
+ export const fileFull = {
296
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
297
+ "$id": "https://authorizedretailers.ai/schemas/0.1/file-full.json",
298
+ "title": "authorized-retailers.json, full form",
299
+ "type": "object",
300
+ "required": [
301
+ "spec",
302
+ "form",
303
+ "brand",
304
+ "issued",
305
+ "expires",
306
+ "authorizations"
307
+ ],
308
+ "additionalProperties": false,
309
+ "properties": {
310
+ "spec": {
311
+ "$ref": "common.json#/$defs/specVersion"
312
+ },
313
+ "form": {
314
+ "const": "full"
315
+ },
316
+ "brand": {
317
+ "$ref": "common.json#/$defs/brand"
318
+ },
319
+ "issued": {
320
+ "$ref": "common.json#/$defs/timestamp"
321
+ },
322
+ "expires": {
323
+ "$ref": "common.json#/$defs/timestamp"
324
+ },
325
+ "authorizations": {
326
+ "type": "array",
327
+ "items": {
328
+ "$ref": "common.json#/$defs/authorization"
329
+ }
330
+ },
331
+ "signature": {
332
+ "description": "Optional in the schema: an unsigned file is well-formed but does not count (section 5).",
333
+ "$ref": "common.json#/$defs/signature"
334
+ }
335
+ }
336
+ };
337
+ export const filePointer = {
338
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
339
+ "$id": "https://authorizedretailers.ai/schemas/0.1/file-pointer.json",
340
+ "title": "authorized-retailers.json, pointer form",
341
+ "type": "object",
342
+ "required": [
343
+ "spec",
344
+ "form",
345
+ "brand",
346
+ "list"
347
+ ],
348
+ "additionalProperties": false,
349
+ "properties": {
350
+ "spec": {
351
+ "$ref": "common.json#/$defs/specVersion"
352
+ },
353
+ "form": {
354
+ "const": "pointer"
355
+ },
356
+ "brand": {
357
+ "$ref": "common.json#/$defs/brand"
358
+ },
359
+ "list": {
360
+ "$ref": "common.json#/$defs/httpsUrl"
361
+ }
362
+ }
363
+ };
364
+ export const filePrivate = {
365
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
366
+ "$id": "https://authorizedretailers.ai/schemas/0.1/file-private.json",
367
+ "title": "authorized-retailers.json, private form",
368
+ "type": "object",
369
+ "required": [
370
+ "spec",
371
+ "form",
372
+ "brand",
373
+ "verify"
374
+ ],
375
+ "additionalProperties": false,
376
+ "properties": {
377
+ "spec": {
378
+ "$ref": "common.json#/$defs/specVersion"
379
+ },
380
+ "form": {
381
+ "const": "private"
382
+ },
383
+ "brand": {
384
+ "$ref": "common.json#/$defs/brand"
385
+ },
386
+ "verify": {
387
+ "$ref": "common.json#/$defs/httpsUrl"
388
+ }
389
+ }
390
+ };
391
+ export const file = {
392
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
393
+ "$id": "https://authorizedretailers.ai/schemas/0.1/file.json",
394
+ "title": "authorized-retailers.json, any form",
395
+ "type": "object",
396
+ "required": [
397
+ "form"
398
+ ],
399
+ "properties": {
400
+ "form": {
401
+ "enum": [
402
+ "full",
403
+ "pointer",
404
+ "private"
405
+ ]
406
+ }
407
+ },
408
+ "allOf": [
409
+ {
410
+ "if": {
411
+ "properties": {
412
+ "form": {
413
+ "const": "full"
414
+ }
415
+ }
416
+ },
417
+ "then": {
418
+ "$ref": "file-full.json"
419
+ }
420
+ },
421
+ {
422
+ "if": {
423
+ "properties": {
424
+ "form": {
425
+ "const": "pointer"
426
+ }
427
+ }
428
+ },
429
+ "then": {
430
+ "$ref": "file-pointer.json"
431
+ }
432
+ },
433
+ {
434
+ "if": {
435
+ "properties": {
436
+ "form": {
437
+ "const": "private"
438
+ }
439
+ }
440
+ },
441
+ "then": {
442
+ "$ref": "file-private.json"
443
+ }
444
+ }
445
+ ]
446
+ };
447
+ export const verifyRequest = {
448
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
449
+ "$id": "https://authorizedretailers.ai/schemas/0.1/verify-request.json",
450
+ "title": "POST /v0/verify request (section 9)",
451
+ "description": "Canonical form. Registries normalize case and whitespace before validating (see normalizeVerifyRequest).",
452
+ "type": "object",
453
+ "required": [
454
+ "brand_domain",
455
+ "channel",
456
+ "territory"
457
+ ],
458
+ "additionalProperties": false,
459
+ "properties": {
460
+ "brand_domain": {
461
+ "$ref": "common.json#/$defs/domain"
462
+ },
463
+ "channel": {
464
+ "$ref": "common.json#/$defs/verifiableChannel"
465
+ },
466
+ "territory": {
467
+ "$ref": "common.json#/$defs/countryCode"
468
+ },
469
+ "product_line": {
470
+ "description": "Defaults to \"all\" when omitted.",
471
+ "$ref": "common.json#/$defs/productLine"
472
+ }
473
+ }
474
+ };
475
+ export const verifyResponse = {
476
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
477
+ "$id": "https://authorizedretailers.ai/schemas/0.1/verify-response.json",
478
+ "title": "POST /v0/verify response (section 9)",
479
+ "type": "object",
480
+ "required": [
481
+ "status",
482
+ "checked",
483
+ "valid_until",
484
+ "observed",
485
+ "signals",
486
+ "signature"
487
+ ],
488
+ "additionalProperties": false,
489
+ "properties": {
490
+ "status": {
491
+ "enum": [
492
+ "authorized",
493
+ "unlisted",
494
+ "expired",
495
+ "brand_unverified",
496
+ "disputed"
497
+ ]
498
+ },
499
+ "reason": {
500
+ "enum": [
501
+ "not_registered",
502
+ "domain_unlinked",
503
+ "verification_lapsed"
504
+ ]
505
+ },
506
+ "authorization_id": {
507
+ "$ref": "common.json#/$defs/id"
508
+ },
509
+ "expires": {
510
+ "$ref": "common.json#/$defs/timestamp"
511
+ },
512
+ "checked": {
513
+ "$ref": "common.json#/$defs/timestamp"
514
+ },
515
+ "valid_until": {
516
+ "$ref": "common.json#/$defs/timestamp"
517
+ },
518
+ "observed": {
519
+ "description": "Section 7: when the registry last saw this seller selling on the channel, within 30 days; otherwise null",
520
+ "oneOf": [
521
+ {
522
+ "type": "null"
523
+ },
524
+ {
525
+ "type": "object",
526
+ "required": [
527
+ "last_seen"
528
+ ],
529
+ "additionalProperties": false,
530
+ "properties": {
531
+ "last_seen": {
532
+ "$ref": "common.json#/$defs/timestamp"
533
+ }
534
+ }
535
+ }
536
+ ]
537
+ },
538
+ "signals": {
539
+ "type": "array",
540
+ "items": {
541
+ "type": "object",
542
+ "required": [
543
+ "type",
544
+ "observed"
545
+ ],
546
+ "additionalProperties": false,
547
+ "properties": {
548
+ "type": {
549
+ "type": "string",
550
+ "pattern": "^[a-z][a-z0-9_]{0,63}$"
551
+ },
552
+ "observed": {
553
+ "$ref": "common.json#/$defs/timestamp"
554
+ },
555
+ "detail": {
556
+ "type": "string",
557
+ "maxLength": 1024
558
+ }
559
+ }
560
+ }
561
+ },
562
+ "signature": {
563
+ "$ref": "common.json#/$defs/signature"
564
+ }
565
+ },
566
+ "allOf": [
567
+ {
568
+ "if": {
569
+ "properties": {
570
+ "status": {
571
+ "const": "brand_unverified"
572
+ }
573
+ }
574
+ },
575
+ "then": {
576
+ "not": {
577
+ "anyOf": [
578
+ {
579
+ "required": [
580
+ "authorization_id"
581
+ ]
582
+ },
583
+ {
584
+ "required": [
585
+ "expires"
586
+ ]
587
+ }
588
+ ]
589
+ }
590
+ },
591
+ "else": {
592
+ "not": {
593
+ "required": [
594
+ "reason"
595
+ ]
596
+ }
597
+ }
598
+ },
599
+ {
600
+ "if": {
601
+ "not": {
602
+ "properties": {
603
+ "status": {
604
+ "const": "authorized"
605
+ }
606
+ }
607
+ }
608
+ },
609
+ "then": {
610
+ "properties": {
611
+ "observed": {
612
+ "const": null
613
+ }
614
+ }
615
+ }
616
+ },
617
+ {
618
+ "if": {
619
+ "properties": {
620
+ "status": {
621
+ "enum": [
622
+ "authorized",
623
+ "expired",
624
+ "disputed"
625
+ ]
626
+ }
627
+ }
628
+ },
629
+ "then": {
630
+ "required": [
631
+ "authorization_id",
632
+ "expires"
633
+ ]
634
+ }
635
+ },
636
+ {
637
+ "if": {
638
+ "properties": {
639
+ "status": {
640
+ "const": "unlisted"
641
+ }
642
+ }
643
+ },
644
+ "then": {
645
+ "not": {
646
+ "anyOf": [
647
+ {
648
+ "required": [
649
+ "authorization_id"
650
+ ]
651
+ },
652
+ {
653
+ "required": [
654
+ "expires"
655
+ ]
656
+ }
657
+ ]
658
+ }
659
+ }
660
+ }
661
+ ]
662
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parses a UTC RFC 3339 timestamp to epoch ms, or returns null if it is malformed or names an
3
+ * impossible date (Date.parse silently rolls "2026-02-30" over into March).
4
+ */
5
+ export declare function parseTimestamp(value: string): number | null;
6
+ export declare function formatTimestamp(ms: number): string;
@@ -0,0 +1,26 @@
1
+ const TIMESTAMP_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?Z$/;
2
+ /**
3
+ * Parses a UTC RFC 3339 timestamp to epoch ms, or returns null if it is malformed or names an
4
+ * impossible date (Date.parse silently rolls "2026-02-30" over into March).
5
+ */
6
+ export function parseTimestamp(value) {
7
+ const m = TIMESTAMP_RE.exec(value);
8
+ if (!m)
9
+ return null;
10
+ const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number);
11
+ const ms = Date.UTC(year, month - 1, day, hour, minute, second);
12
+ const d = new Date(ms);
13
+ if (d.getUTCFullYear() !== year ||
14
+ d.getUTCMonth() !== month - 1 ||
15
+ d.getUTCDate() !== day ||
16
+ d.getUTCHours() !== hour ||
17
+ d.getUTCMinutes() !== minute ||
18
+ d.getUTCSeconds() !== second) {
19
+ return null;
20
+ }
21
+ const frac = m[7] ? Number(m[7].slice(1, 4).padEnd(3, "0")) : 0;
22
+ return ms + frac;
23
+ }
24
+ export function formatTimestamp(ms) {
25
+ return new Date(ms).toISOString().replace(/\.000Z$/, "Z");
26
+ }