@authorizedretailers/spec 0.1.1 → 0.2.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 (208) hide show
  1. package/README.md +66 -47
  2. package/dist/constants.d.ts +25 -6
  3. package/dist/constants.js +41 -7
  4. package/dist/identity.d.ts +29 -14
  5. package/dist/identity.js +79 -39
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2 -0
  8. package/dist/jws.d.ts +36 -5
  9. package/dist/jws.js +48 -14
  10. package/dist/match.d.ts +21 -0
  11. package/dist/match.js +89 -0
  12. package/dist/product.d.ts +34 -0
  13. package/dist/product.js +75 -0
  14. package/dist/published.d.ts +14 -8
  15. package/dist/published.js +32 -10
  16. package/dist/schemas.d.ts +366 -91
  17. package/dist/schemas.js +802 -181
  18. package/dist/types.d.ts +121 -47
  19. package/dist/types.js +2 -2
  20. package/dist/validate.d.ts +24 -10
  21. package/dist/validate.js +300 -107
  22. package/fixtures/list/invalid/list-all-mixed.json +53 -0
  23. package/fixtures/list/invalid/list-amazon-missing-seller-id.json +51 -0
  24. package/fixtures/list/invalid/list-amazon-on-walmart-domain.json +52 -0
  25. package/fixtures/{file/invalid/full-attached-jws.json → list/invalid/list-attached-jws.json} +14 -12
  26. package/fixtures/list/invalid/list-bad-asin.json +102 -0
  27. package/fixtures/list/invalid/list-bad-gtin.json +102 -0
  28. package/fixtures/list/invalid/list-brand-exclusion-no-catalog.json +57 -0
  29. package/fixtures/list/invalid/list-duplicate-asin.json +102 -0
  30. package/fixtures/{file/invalid/full-duplicate-authorization-id.json → list/invalid/list-duplicate-authorization-id.json} +23 -18
  31. package/fixtures/list/invalid/list-duplicate-gtin.json +102 -0
  32. package/fixtures/list/invalid/list-duplicate-product-id.json +102 -0
  33. package/fixtures/{file/invalid/full-empty-territories.json → list/invalid/list-empty-territories.json} +17 -10
  34. package/fixtures/list/invalid/list-exclude-unknown-product.json +105 -0
  35. package/fixtures/list/invalid/list-expires-field.json +53 -0
  36. package/fixtures/list/invalid/list-extra-property.json +53 -0
  37. package/fixtures/list/invalid/list-generic-web-no-territories.json +39 -0
  38. package/fixtures/list/invalid/list-gtin-11-digits.json +102 -0
  39. package/fixtures/list/invalid/list-impossible-date.json +53 -0
  40. package/fixtures/list/invalid/list-lowercase-territory.json +52 -0
  41. package/fixtures/{file/invalid/full-signature-missing-kid.json → list/invalid/list-marketplace-field.json} +14 -11
  42. package/fixtures/list/invalid/list-marketplace-territory-mismatch.json +52 -0
  43. package/fixtures/list/invalid/list-missing-revocation-id.json +51 -0
  44. package/fixtures/list/invalid/list-missing-scope.json +44 -0
  45. package/fixtures/list/invalid/list-missing-stale-until.json +51 -0
  46. package/fixtures/list/invalid/list-name-only-retailer.json +36 -0
  47. package/fixtures/list/invalid/list-no-channels.json +37 -0
  48. package/fixtures/list/invalid/list-non-utc-timestamp.json +52 -0
  49. package/fixtures/list/invalid/list-not-public-with-authorizations.json +18 -0
  50. package/fixtures/list/invalid/list-not-public-with-catalog.json +18 -0
  51. package/fixtures/{file/invalid/full-physical-only.json → list/invalid/list-physical.json} +16 -10
  52. package/fixtures/list/invalid/list-product-no-identifier.json +98 -0
  53. package/fixtures/list/invalid/list-scope-no-products.json +49 -0
  54. package/fixtures/{file/valid/full-signed.json → list/invalid/list-single-domain.json} +12 -12
  55. package/fixtures/list/invalid/list-stale-over-24-hours.json +52 -0
  56. package/fixtures/list/invalid/list-territory-uk.json +52 -0
  57. package/fixtures/{file/invalid/full-unknown-channel-type.json → list/invalid/list-unknown-channel-type.json} +18 -12
  58. package/fixtures/list/invalid/list-unknown-line.json +102 -0
  59. package/fixtures/list/invalid/list-unknown-product.json +105 -0
  60. package/fixtures/{file/invalid/full-missing-authorization-expires.json → list/invalid/list-unsigned.json} +12 -9
  61. package/fixtures/list/invalid/list-valid-over-15-minutes.json +52 -0
  62. package/fixtures/{file/invalid/full-web-domain-is-url.json → list/invalid/list-web-domain-is-url.json} +18 -12
  63. package/fixtures/list/invalid/list-web-on-marketplace-domain.json +52 -0
  64. package/fixtures/{file/invalid/full-wildcard-mixed.json → list/invalid/list-wildcard-mixed.json} +17 -10
  65. package/fixtures/list/valid/list-catalog-line-scope.json +102 -0
  66. package/fixtures/list/valid/list-cctld-web.json +39 -0
  67. package/fixtures/list/valid/list-fractional-seconds.json +52 -0
  68. package/fixtures/{file/invalid/full-amazon-missing-seller-id.json → list/valid/list-generic-web-territories.json} +15 -17
  69. package/fixtures/list/valid/list-marketplace-no-territories.json +40 -0
  70. package/fixtures/list/valid/list-no-authorizations.json +18 -0
  71. package/fixtures/list/valid/list-no-catalog.json +52 -0
  72. package/fixtures/list/valid/list-no-expires.json +52 -0
  73. package/fixtures/list/valid/list-not-public.json +17 -0
  74. package/fixtures/list/valid/list-product-scope.json +103 -0
  75. package/fixtures/list/valid/list-proposed-by.json +55 -0
  76. package/fixtures/list/valid/list-with-expires.json +53 -0
  77. package/fixtures/list/valid/list-worldwide.json +52 -0
  78. package/fixtures/manifest.json +606 -158
  79. package/fixtures/match/all-scope-excluded-line.json +194 -0
  80. package/fixtures/match/all-scope-not-in-catalog.json +194 -0
  81. package/fixtures/match/as-of-before-end-date.json +194 -0
  82. package/fixtures/match/brand-exclusion-beats-named-product.json +194 -0
  83. package/fixtures/match/duplicate-asin-authorized-seller.json +192 -0
  84. package/fixtures/match/duplicate-asin-unauthorized-seller.json +191 -0
  85. package/fixtures/match/expired-end-date.json +193 -0
  86. package/fixtures/match/marketplace-territory-default.json +186 -0
  87. package/fixtures/match/marketplace-territory-mismatch.json +187 -0
  88. package/fixtures/match/match-by-channel-key.json +186 -0
  89. package/fixtures/match/product-excluded-by-authorization.json +194 -0
  90. package/fixtures/match/product-line-request.json +186 -0
  91. package/fixtures/match/product-not-in-scope.json +194 -0
  92. package/fixtures/match/seller-authorized-by-gtin.json +194 -0
  93. package/fixtures/match/seller-authorized-for-line.json +194 -0
  94. package/fixtures/match/worldwide-web.json +186 -0
  95. package/fixtures/pointer/invalid/full-form.json +11 -0
  96. package/fixtures/pointer/invalid/pointer-domain-mismatch.json +10 -0
  97. package/fixtures/{file → pointer}/invalid/pointer-http-url.json +2 -1
  98. package/fixtures/pointer/invalid/pointer-missing-verify-token.json +9 -0
  99. package/fixtures/pointer/invalid/pointer-other-host.json +10 -0
  100. package/fixtures/{file/valid/pointer.json → pointer/invalid/pointer-wrong-spec.json} +1 -0
  101. package/fixtures/pointer/invalid/private-form.json +10 -0
  102. package/fixtures/pointer/invalid/unknown-form.json +10 -0
  103. package/fixtures/pointer/valid/pointer-fetched-via-www.json +10 -0
  104. package/fixtures/pointer/valid/pointer.json +10 -0
  105. package/fixtures/revocation-feed/invalid/feed-hashed-entry.json +17 -0
  106. package/fixtures/revocation-feed/invalid/feed-valid-over-15-minutes.json +17 -0
  107. package/fixtures/revocation-feed/valid/feed-empty.json +12 -0
  108. package/fixtures/revocation-feed/valid/feed.json +17 -0
  109. package/fixtures/verify-request/invalid/bad-as-of.json +9 -0
  110. package/fixtures/verify-request/invalid/bad-gtin.json +11 -0
  111. package/fixtures/verify-request/invalid/domain-with-scheme.json +2 -4
  112. package/fixtures/verify-request/{valid/web-no-product-line.json → invalid/generic-web-no-territory.json} +1 -2
  113. package/fixtures/verify-request/invalid/gtin-and-asin.json +12 -0
  114. package/fixtures/verify-request/{valid/amazon.json → invalid/marketplace-field.json} +1 -3
  115. package/fixtures/verify-request/invalid/missing-channel.json +1 -3
  116. package/fixtures/verify-request/invalid/physical-channel.json +1 -3
  117. package/fixtures/verify-request/invalid/retailer-name-only.json +0 -2
  118. package/fixtures/verify-request/invalid/territory-uk.json +9 -0
  119. package/fixtures/verify-request/invalid/wildcard-territory.json +2 -3
  120. package/fixtures/verify-request/valid/as-of-datetime.json +9 -0
  121. package/fixtures/verify-request/valid/full.json +13 -0
  122. package/fixtures/verify-request/valid/gtin.json +11 -0
  123. package/fixtures/verify-request/valid/minimal-marketplace.json +8 -0
  124. package/fixtures/verify-request/valid/web-cctld-no-territory.json +7 -0
  125. package/fixtures/verify-request/valid/web-generic-with-territory.json +9 -0
  126. package/fixtures/verify-response/invalid/authorized-missing-revocation-id.json +18 -0
  127. package/fixtures/verify-response/invalid/authorized-with-reason.json +20 -0
  128. package/fixtures/verify-response/invalid/brand-unverified.json +13 -0
  129. package/fixtures/verify-response/invalid/expired-without-expires.json +18 -0
  130. package/fixtures/verify-response/invalid/missing-observed.json +11 -5
  131. package/fixtures/verify-response/invalid/missing-signature.json +9 -3
  132. package/fixtures/verify-response/invalid/missing-stale-until.json +18 -0
  133. package/fixtures/verify-response/invalid/not-in-catalog-with-id.json +17 -0
  134. package/fixtures/verify-response/invalid/not-registered-with-unlisted-reason.json +13 -0
  135. package/fixtures/verify-response/invalid/stale-until-over-24-hours.json +19 -0
  136. package/fixtures/verify-response/invalid/unlisted-observed.json +5 -4
  137. package/fixtures/verify-response/invalid/unlisted-with-authorization-id.json +5 -4
  138. package/fixtures/verify-response/invalid/unlisted-with-not-registered-reason.json +13 -0
  139. package/fixtures/verify-response/invalid/unlisted-with-revocation-id.json +13 -0
  140. package/fixtures/verify-response/invalid/valid-until-before-checked.json +11 -5
  141. package/fixtures/verify-response/invalid/valid-until-over-15-minutes.json +19 -0
  142. package/fixtures/verify-response/invalid/with-stale-flag.json +20 -0
  143. package/fixtures/verify-response/invalid/with-tier.json +11 -5
  144. package/fixtures/verify-response/valid/authorized-no-product.json +14 -0
  145. package/fixtures/verify-response/valid/authorized-not-in-catalog.json +22 -0
  146. package/fixtures/verify-response/valid/authorized-observed.json +11 -5
  147. package/fixtures/verify-response/valid/authorized-private-product.json +17 -0
  148. package/fixtures/verify-response/valid/authorized-with-end-date.json +20 -0
  149. package/fixtures/verify-response/valid/authorized.json +11 -5
  150. package/fixtures/verify-response/valid/disputed.json +10 -5
  151. package/fixtures/verify-response/valid/expired.json +12 -6
  152. package/fixtures/verify-response/valid/not-registered-no-reason.json +12 -0
  153. package/fixtures/verify-response/valid/not-registered-reason.json +13 -0
  154. package/fixtures/verify-response/valid/unlisted-product-excluded.json +18 -0
  155. package/fixtures/verify-response/valid/unlisted-product-not-in-catalog.json +16 -0
  156. package/fixtures/verify-response/valid/unlisted-territory-mismatch.json +13 -0
  157. package/fixtures/verify-response/valid/unlisted.json +5 -4
  158. package/fixtures/verify-response/valid/with-as-of.json +20 -0
  159. package/fixtures/verify-response/valid/with-signal.json +11 -5
  160. package/package.json +5 -4
  161. package/schemas/0.2/common.json +761 -0
  162. package/schemas/0.2/list.json +87 -0
  163. package/schemas/0.2/pointer.json +33 -0
  164. package/schemas/0.2/revocation-feed.json +55 -0
  165. package/schemas/0.2/verify-request.json +65 -0
  166. package/schemas/0.2/verify-response.json +284 -0
  167. package/spec/spec-v0.2.md +531 -0
  168. package/fixtures/file/invalid/full-all-mixed.json +0 -47
  169. package/fixtures/file/invalid/full-authorization-expires-after-file.json +0 -46
  170. package/fixtures/file/invalid/full-expires-before-issued.json +0 -46
  171. package/fixtures/file/invalid/full-expiry-over-180-days.json +0 -46
  172. package/fixtures/file/invalid/full-extra-property.json +0 -47
  173. package/fixtures/file/invalid/full-impossible-date.json +0 -46
  174. package/fixtures/file/invalid/full-lowercase-territory.json +0 -45
  175. package/fixtures/file/invalid/full-missing-file-expires.json +0 -45
  176. package/fixtures/file/invalid/full-missing-product-lines.json +0 -43
  177. package/fixtures/file/invalid/full-missing-scope.json +0 -37
  178. package/fixtures/file/invalid/full-missing-territories.json +0 -42
  179. package/fixtures/file/invalid/full-name-only-retailer.json +0 -30
  180. package/fixtures/file/invalid/full-no-channels.json +0 -31
  181. package/fixtures/file/invalid/full-non-utc-timestamp.json +0 -46
  182. package/fixtures/file/invalid/full-physical-missing-country.json +0 -50
  183. package/fixtures/file/invalid/full-web-missing-domain.json +0 -45
  184. package/fixtures/file/invalid/full-wrong-spec.json +0 -46
  185. package/fixtures/file/invalid/private-missing-verify.json +0 -8
  186. package/fixtures/file/invalid/private-with-authorizations.json +0 -45
  187. package/fixtures/file/invalid/unknown-form.json +0 -9
  188. package/fixtures/file/valid/full-fractional-seconds.json +0 -46
  189. package/fixtures/file/valid/full-max-expiry.json +0 -46
  190. package/fixtures/file/valid/full-no-authorizations.json +0 -11
  191. package/fixtures/file/valid/full-unsigned-handwritten.json +0 -46
  192. package/fixtures/file/valid/full-worldwide-physical-proposed.json +0 -82
  193. package/fixtures/file/valid/private.json +0 -9
  194. package/fixtures/verify-response/invalid/authorized-missing-authorization-id.json +0 -12
  195. package/fixtures/verify-response/invalid/missing-valid-until.json +0 -12
  196. package/fixtures/verify-response/invalid/observed-without-last-seen.json +0 -13
  197. package/fixtures/verify-response/invalid/unknown-status.json +0 -13
  198. package/fixtures/verify-response/invalid/unlisted-with-reason.json +0 -12
  199. package/fixtures/verify-response/invalid/valid-until-over-24h.json +0 -13
  200. package/fixtures/verify-response/valid/brand-unverified-no-reason.json +0 -11
  201. package/fixtures/verify-response/valid/brand-unverified-reason.json +0 -12
  202. /package/schemas/{common.json → 0.1/common.json} +0 -0
  203. /package/schemas/{file-full.json → 0.1/file-full.json} +0 -0
  204. /package/schemas/{file-pointer.json → 0.1/file-pointer.json} +0 -0
  205. /package/schemas/{file-private.json → 0.1/file-private.json} +0 -0
  206. /package/schemas/{file.json → 0.1/file.json} +0 -0
  207. /package/schemas/{verify-request.json → 0.1/verify-request.json} +0 -0
  208. /package/schemas/{verify-response.json → 0.1/verify-response.json} +0 -0
package/README.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # authorized-retailers.json
2
2
 
3
- An open standard for brands to publish which retailers they have authorized, and for software to check a seller against that list.
3
+ An open format with a single registry, for brands to record which retailers they have authorized and for software to check a seller against that record.
4
4
 
5
- > **Status: draft v0.1.** Breaking changes are possible before v1.0. See [GOVERNANCE.md](GOVERNANCE.md) for how changes are made.
5
+ > **Status: draft v0.2.** Breaking changes are possible before v1.0. See [GOVERNANCE.md](GOVERNANCE.md) for how changes are made.
6
6
 
7
- The standard answers one question: has this brand authorized this seller, on this channel, in this territory, for this product line, as of this date? The format is built for AI shopping agents to check before they recommend or buy, and is ready for them as they adopt it. It works just as well for marketplaces, brand protection teams and anyone else who needs the answer.
7
+ The standard answers one question: has this brand authorized this seller, on this channel, in this territory, for this product, as of this date? Built for AI shopping agents to check before they recommend or buy. It works just as well for marketplaces, brand protection teams and anyone else who needs the answer.
8
8
 
9
- - **Read the spec:** [`spec/spec-v0.1.md`](spec/spec-v0.1.md), also at [authorizedretailers.ai/spec](https://authorizedretailers.ai/spec)
10
- - **Reference registry:** [authorizedretailers.ai](https://authorizedretailers.ai)
9
+ - **Read the spec:** [`spec/spec-v0.2.md`](spec/spec-v0.2.md), also at [authorizedretailers.ai/spec](https://authorizedretailers.ai/spec)
10
+ - **The registry:** [authorizedretailers.ai](https://authorizedretailers.ai). The registry currently lists US channels. Other countries are coming.
11
11
 
12
12
  ## Core principles
13
13
 
14
- - **Every authorization comes from the brand.** No one else can authorize a retailer: not a distributor, not a retailer and not the registry.
14
+ - **Every authorization comes from the brand.** No one else can authorize a retailer: not a distributor, not a retailer and not the registry. The registry confirms the brand controls its domain, publishes the brand's list and signs it. It never changes a list on its own. The signature says who published the list, not who is authorized.
15
15
  - **Distributors can propose, only the brand approves.** A proposal changes nothing until the brand says yes.
16
- - **Authorization is scoped and expires.** Each one names its channels, territories and product lines, and lapses unless the brand reconfirms it.
16
+ - **Authorization is scoped.** Each authorization names its channels and territories and the product lines or products it covers. It lasts until the brand revokes it, or until an end date if the brand sets one.
17
+ - **One source of truth.** authorizedretailers.ai is the only registry. An answer counts only if it verifies against the registry's published keys. Anyone may cache or relay signed answers, but no one else can issue them.
17
18
  - **Authorization is not authenticity.** An answer says what the brand has approved. It does not say whether any item is genuine.
18
19
  - **The registry doesn't rule on pricing.** Pricing and commercial terms are between the brand and its retailers.
19
20
  - **Listing is free.** Brands publish and retailers are listed at no cost.
@@ -23,69 +24,70 @@ The standard answers one question: has this brand authorized this seller, on thi
23
24
 
24
25
  | You are | You use it to |
25
26
  | --- | --- |
26
- | A brand | Publish which retailers you've authorized, where and for what |
27
- | A retailer | Show that the brands you sell have authorized you, and see how you're listed |
27
+ | A brand | Record which retailers you've authorized, where and for which products |
28
28
  | An agent builder | Check a seller before recommending or buying, with an answer you can verify |
29
29
 
30
- ## Publish a file (brands)
30
+ ## Register a brand
31
31
 
32
- A brand verifies control of its domain with a registry, which signs its list. The brand then serves a file at:
32
+ A brand keeps its catalog and authorizations on the registry, which signs its list. The list lives on the registry, so it is always current. The brand proves it controls its domain with a DNS TXT record, which works on every site platform:
33
33
 
34
34
  ```
35
- https://<brand-domain>/.well-known/authorized-retailers.json
35
+ authorizedretailers-verify=<token from the registry>
36
36
  ```
37
37
 
38
- The file takes one of three forms:
39
-
40
- | Form | What it holds | Use when |
41
- | --- | --- | --- |
42
- | `full` | The complete signed list | You want your list public |
43
- | `pointer` | A URL to your signed list on a registry | You want the file to stay current without re-uploading |
44
- | `private` | Your identity and a verify endpoint, no retailers | You don't want your distribution network public |
38
+ A brand that can serve files under `/.well-known/` may also serve a pointer, or use one in place of the DNS record:
45
39
 
46
- Only files signed by a registry count. An unsigned file is well formed but treated as no file (spec section 5). The simplest choice is the `pointer` form:
40
+ ```
41
+ https://<brand-domain>/.well-known/authorized-retailers.json
42
+ ```
47
43
 
48
44
  ```json
49
45
  {
50
- "spec": "authorized-retailers/0.1",
46
+ "spec": "authorized-retailers/0.2",
51
47
  "form": "pointer",
52
48
  "brand": { "name": "Example Brand", "domain": "brand.example" },
49
+ "verify_token": "<token from the registry>",
53
50
  "list": "https://authorizedretailers.ai/v0/brands/brand.example/list"
54
51
  }
55
52
  ```
56
53
 
57
- ## Validate a file
54
+ The pointer holds no authorization data, so it never goes out of date. The token is public: it proves control only of the domain it was issued for. The registry checks the DNS record or pointer regularly. If both are removed, the brand's list stops counting until one is restored. The pointer itself is not signed. The list it names is, and so is every answer from the verify endpoint.
55
+
56
+ ## Validate a pointer and a list
58
57
 
59
58
  ```bash
60
59
  npm install @authorizedretailers/spec
61
60
  ```
62
61
 
63
- ```ts
64
- import { validateFile, validAuthorizations } from "@authorizedretailers/spec";
62
+ ```js
63
+ import { validateFile, checkPublishedList, validAuthorizations } from "@authorizedretailers/spec";
65
64
 
66
- const result = validateFile(file);
67
- if (!result.valid) console.log(result.errors);
68
- else if (result.value.form === "full") {
69
- const current = validAuthorizations(result.value); // an expired file yields none
70
- }
71
- ```
72
-
73
- To accept a file you fetched, check its signature and that it vouches for the domain it came from:
65
+ // The registry's public keys. Fetch once and cache.
66
+ const jwks = await fetch("https://authorizedretailers.ai/.well-known/jwks.json").then((r) => r.json());
74
67
 
75
- ```ts
76
- import { checkPublishedFile } from "@authorizedretailers/spec";
68
+ const pointer = await fetch("https://brand.example/.well-known/authorized-retailers.json").then((r) => r.json());
69
+ const file = validateFile(pointer, "brand.example");
70
+ if (!file.valid) console.log(file.errors);
77
71
 
78
- const check = await checkPublishedFile("brand.example", file, jwks);
72
+ // Fetch the signed list the pointer names, then check it.
73
+ const list = await fetch(file.value.list).then((r) => r.json());
74
+ const check = await checkPublishedList("brand.example", list, jwks);
79
75
  // { counts: true, kid } or { counts: false, reason }
76
+
77
+ if (check.counts && list.public) {
78
+ const current = validAuthorizations(list); // leaves out authorizations past their end date
79
+ }
80
80
  ```
81
81
 
82
+ A brand with only a DNS record has no pointer. Its list is at the same registry URL: `https://authorizedretailers.ai/v0/brands/<domain>/list`.
83
+
82
84
  The package works in Node 20+, Cloudflare Workers, Deno and browsers. It uses only Web Crypto, makes no network calls and sends no telemetry.
83
85
 
84
86
  ## Verify a signed registry answer (agent builders)
85
87
 
86
- Ask a registry whether a seller is authorized, then verify the answer against the registry's published keys:
88
+ Ask the registry whether a seller is authorized, then verify the answer against the registry's published keys (`jwks`, fetched above):
87
89
 
88
- ```ts
90
+ ```js
89
91
  import { verifyDocument } from "@authorizedretailers/spec";
90
92
 
91
93
  const answer = await fetch("https://authorizedretailers.ai/v0/verify", {
@@ -93,35 +95,52 @@ const answer = await fetch("https://authorizedretailers.ai/v0/verify", {
93
95
  headers: { "Content-Type": "application/json" },
94
96
  body: JSON.stringify({
95
97
  brand_domain: "brand.example",
96
- channel: { type: "amazon", marketplace: "US", seller_id: "A1B2C3D4E5F6G7" },
97
- territory: "US",
98
+ channel: { type: "amazon", domain: "amazon.com", seller_id: "A1B2C3D4E5F6G7" },
99
+ // Optional:
100
+ territory: "US", // defaults to the domain's country
101
+ product: { asin: "B0EXAMPLE1" }, // or { gtin: "00012345678905" } for UPC or EAN
102
+ as_of: "2026-09-28", // defaults to now
98
103
  }),
99
104
  }).then((r) => r.json());
100
105
 
101
- const jwks = await fetch("https://authorizedretailers.ai/.well-known/jwks.json").then((r) => r.json());
102
- const check = await verifyDocument(answer, jwks);
106
+ const verified = await verifyDocument(answer, jwks);
103
107
  // { valid: true, kid } or { valid: false, reason }
104
108
  ```
105
109
 
106
- The verifier rejects keys no longer in the key set and answers past their `valid_until`. On `unknown_kid`, fetch the key set again once. Match sellers on the identifier you see, never on a retailer name: `channelKey()` normalizes identifiers so every reader agrees.
110
+ Marketplace channels are identified by their domain and seller ID, and web stores by their domain. A marketplace domain sets the territory (amazon.com is US, amazon.ca is CA), and a request for a different territory is answered `unlisted`. Product is optional: ask with a GTIN or, on Amazon, an ASIN. Authorization is decided per seller first, then checked against the product lines or products the brand has authorized. Every answer that includes a product says whether it is in the brand's catalog. A product that isn't may be a duplicate listing.
111
+
112
+ The verifier rejects keys no longer in the key set and answers past their `valid_until` (15 minutes). On `unknown_kid`, fetch the key set again once. Match sellers on the identifier you see, never on a retailer name: `channelKey()` normalizes identifiers so every reader agrees.
113
+
114
+ If the registry is unreachable, never treat a seller as authorized without a current answer. Between `valid_until` and `stale_until` (24 hours) you may use an answer you already hold, marked as stale, after checking it against the revocation feed: `verifyDocument(answer, jwks, { allowStale: true, revocationFeed })`. Otherwise the seller is unconfirmed (spec section 10.2).
115
+
116
+ ## Upgrading from 0.1
117
+
118
+ - `full` and `private` files are gone. Replace any `/.well-known/authorized-retailers.json` with a pointer, or remove it and rely on the DNS record.
119
+ - The hosted `authorized-retailers-verify.txt` file is gone. Use the DNS record or the pointer's `verify_token`.
120
+ - Marketplace channels use `domain` (`amazon.com`) instead of `marketplace` (`US`). The old field is rejected.
121
+ - `brand_unverified` is now `brand_not_registered`.
122
+ - Authorizations have no default expiry. `valid_until` is 15 minutes, with `stale_until` for outages.
123
+ - `validateFullFile` is replaced by `validateList`, and `checkPublishedFile` by `checkPublishedList`. `validateFile` validates pointers only and takes the domain it was fetched from.
107
124
 
108
125
  ## What's in this repository
109
126
 
110
127
  | Path | What |
111
128
  | --- | --- |
112
- | `spec/` | The specification |
113
- | `schemas/` | JSON Schemas (draft 2020-12) for the three file forms and the verify request and response |
114
- | `fixtures/` | Valid and invalid examples, with `manifest.json` saying what each should produce. Usable as a conformance suite |
115
- | `src/` | Types, validator, identifier matching, RFC 8785 canonicalization (JCS) and detached EdDSA JWS signing and verification |
129
+ | `spec/` | The specification, every version |
130
+ | `schemas/` | JSON Schemas (draft 2020-12) for the pointer file, the signed list, the revocation feed and the verify request and response, one folder per version |
131
+ | `fixtures/` | Valid and invalid examples and matching cases, with `manifest.json` saying what each should produce. Usable as a conformance suite |
132
+ | `src/` | Types, validator, identifier, territory and product matching, RFC 8785 canonicalization (JCS) and detached EdDSA JWS signing and verification |
116
133
  | `test/` | Tests for all of the above |
117
134
  | `docs/MUST-coverage.md` | Every MUST in the spec and where it is tested |
118
135
 
119
- Anyone may run a registry that implements this standard. See [TRADEMARKS.md](TRADEMARKS.md) for how to describe it.
136
+ authorizedretailers.ai is the only registry. Anyone may build on the standard and cache or relay signed answers, but only answers signed with the registry's published keys count. See [TRADEMARKS.md](TRADEMARKS.md).
120
137
 
121
138
  ## Feedback
122
139
 
123
140
  Send feedback on the spec at [authorizedretailers.ai/spec](https://authorizedretailers.ai/spec#feedback). This repository doesn't accept pull requests. See [CONTRIBUTING.md](CONTRIBUTING.md).
124
141
 
142
+ Use of the registry is subject to the terms at authorizedretailers.ai/terms.
143
+
125
144
  ## License
126
145
 
127
146
  - The specification and documents in `spec/` and `docs/`: [CC BY 4.0](LICENSE-SPEC)
@@ -1,6 +1,25 @@
1
- /** Section 10: maximum expiry is 180 days from issue. */
2
- export declare const MAX_EXPIRY_MS: number;
3
- /** Section 10: recommended default expiry. */
4
- export declare const DEFAULT_EXPIRY_MS: number;
5
- /** Section 9: a verify answer's valid_until is at most 24 hours after checked. */
6
- export declare const MAX_ANSWER_VALIDITY_MS: number;
1
+ import type { MarketplaceChannelType } from "./types.js";
2
+ /** Section 10.1: a signed list's or answer's valid_until is at most 15 minutes after it was issued. */
3
+ export declare const MAX_VALIDITY_MS: number;
4
+ /** Section 10.2: stale_until is at most 24 hours after issue. */
5
+ export declare const MAX_STALE_MS: number;
6
+ /** Section 10: the verify API reflects a revocation within this long. */
7
+ export declare const REVOCATION_MAX_DELAY_MS: number;
8
+ /** Section 5.1: readers may follow this many HTTPS redirects when fetching a pointer. */
9
+ export declare const MAX_POINTER_REDIRECTS = 3;
10
+ /** Section 5.1: a pointer's list URL must be on this origin. */
11
+ export declare const REGISTRY_ORIGIN = "https://authorizedretailers.ai";
12
+ export declare const REGISTRY_JWKS_URL = "https://authorizedretailers.ai/.well-known/jwks.json";
13
+ export declare const REGISTRY_REVOCATIONS_URL = "https://authorizedretailers.ai/v0/revocations";
14
+ /** Section 10.2: mirror base URLs. None yet; listed here and in the spec when they launch. */
15
+ export declare const MIRRORS: readonly string[];
16
+ /** Section 4: the DNS TXT record value prefix. */
17
+ export declare const DNS_VERIFY_PREFIX = "authorizedretailers-verify=";
18
+ export declare const POINTER_PATH = "/.well-known/authorized-retailers.json";
19
+ /** Section 6: marketplace domains, their channel type and country. */
20
+ export declare const MARKETPLACE_DOMAINS: Readonly<Record<string, {
21
+ type: MarketplaceChannelType;
22
+ country: string;
23
+ }>>;
24
+ /** Section 6.2: two-letter domain endings widely used as generic domains, so they imply no territory. */
25
+ export declare const GENERIC_CCTLDS: ReadonlySet<string>;
package/dist/constants.js CHANGED
@@ -1,7 +1,41 @@
1
- const DAY_MS = 24 * 60 * 60 * 1000;
2
- /** Section 10: maximum expiry is 180 days from issue. */
3
- export const MAX_EXPIRY_MS = 180 * DAY_MS;
4
- /** Section 10: recommended default expiry. */
5
- export const DEFAULT_EXPIRY_MS = 90 * DAY_MS;
6
- /** Section 9: a verify answer's valid_until is at most 24 hours after checked. */
7
- export const MAX_ANSWER_VALIDITY_MS = DAY_MS;
1
+ const MINUTE_MS = 60 * 1000;
2
+ const HOUR_MS = 60 * MINUTE_MS;
3
+ /** Section 10.1: a signed list's or answer's valid_until is at most 15 minutes after it was issued. */
4
+ export const MAX_VALIDITY_MS = 15 * MINUTE_MS;
5
+ /** Section 10.2: stale_until is at most 24 hours after issue. */
6
+ export const MAX_STALE_MS = 24 * HOUR_MS;
7
+ /** Section 10: the verify API reflects a revocation within this long. */
8
+ export const REVOCATION_MAX_DELAY_MS = MINUTE_MS;
9
+ /** Section 5.1: readers may follow this many HTTPS redirects when fetching a pointer. */
10
+ export const MAX_POINTER_REDIRECTS = 3;
11
+ /** Section 5.1: a pointer's list URL must be on this origin. */
12
+ export const REGISTRY_ORIGIN = "https://authorizedretailers.ai";
13
+ export const REGISTRY_JWKS_URL = `${REGISTRY_ORIGIN}/.well-known/jwks.json`;
14
+ export const REGISTRY_REVOCATIONS_URL = `${REGISTRY_ORIGIN}/v0/revocations`;
15
+ /** Section 10.2: mirror base URLs. None yet; listed here and in the spec when they launch. */
16
+ export const MIRRORS = [];
17
+ /** Section 4: the DNS TXT record value prefix. */
18
+ export const DNS_VERIFY_PREFIX = "authorizedretailers-verify=";
19
+ export const POINTER_PATH = "/.well-known/authorized-retailers.json";
20
+ /** Section 6: marketplace domains, their channel type and country. */
21
+ export const MARKETPLACE_DOMAINS = {
22
+ "amazon.com": { type: "amazon", country: "US" },
23
+ "amazon.ca": { type: "amazon", country: "CA" },
24
+ "amazon.co.uk": { type: "amazon", country: "GB" },
25
+ "amazon.de": { type: "amazon", country: "DE" },
26
+ "amazon.fr": { type: "amazon", country: "FR" },
27
+ "amazon.it": { type: "amazon", country: "IT" },
28
+ "amazon.es": { type: "amazon", country: "ES" },
29
+ "amazon.com.au": { type: "amazon", country: "AU" },
30
+ "walmart.com": { type: "walmart", country: "US" },
31
+ "walmart.ca": { type: "walmart", country: "CA" },
32
+ "ebay.com": { type: "ebay", country: "US" },
33
+ "ebay.co.uk": { type: "ebay", country: "GB" },
34
+ "ebay.ca": { type: "ebay", country: "CA" },
35
+ "ebay.de": { type: "ebay", country: "DE" },
36
+ "ebay.com.au": { type: "ebay", country: "AU" },
37
+ };
38
+ /** Section 6.2: two-letter domain endings widely used as generic domains, so they imply no territory. */
39
+ export const GENERIC_CCTLDS = new Set([
40
+ "ac", "ai", "cc", "co", "fm", "gg", "io", "la", "ly", "me", "sh", "so", "to", "tv", "vc", "ws",
41
+ ]);
@@ -1,27 +1,42 @@
1
- import type { Authorization, Channel, FullFile } from "./types.js";
1
+ import type { Authorization, Channel, MarketplaceChannel } from "./types.js";
2
+ /** Section 6: lowercase, no trailing dot, no leading "www.". */
2
3
  export declare function normalizeDomain(domain: string): string;
4
+ /** Uppercases and maps common aliases (UK) to ISO 3166-1 alpha-2 (GB). */
3
5
  export declare function normalizeCountry(code: string): string;
6
+ export declare function isCountryCode(code: string): boolean;
7
+ export declare function isMarketplaceDomain(domain: string): boolean;
4
8
  /**
5
- * The identity an agent matches on (section 6: match on the channel identifier, never the
6
- * retailer name). Returns null for physical channels, which agents cannot verify.
9
+ * Section 6.2: the country a domain implies. A marketplace domain's country from the table, a
10
+ * country-code domain's country (".co.uk" is GB), or null for a generic domain (".com", ".io").
7
11
  */
8
- export declare function channelKey(channel: Channel): string | null;
9
- /** Authorizations naming this channel. Matching uses channelKey only; retailer names are ignored. */
10
- export declare function authorizationsForChannel(authorizations: Authorization[], channel: Channel): Authorization[];
12
+ export declare function territoryForDomain(domain: string): string | null;
13
+ /**
14
+ * Section 6.2: the territories one channel of an authorization covers. A marketplace channel covers
15
+ * only its domain's country, and only if the scope (when given) allows it. A web store covers
16
+ * scope.territories when given, otherwise its domain's country. An empty result means none: a
17
+ * marketplace country the scope leaves out, or a generic-domain web store with no territories.
18
+ */
19
+ export declare function effectiveTerritories(authorization: Authorization, channel: Channel): string[];
20
+ /** Whether a channel of an authorization covers a territory. */
21
+ export declare function coversTerritory(authorization: Authorization, channel: Channel, territory: string): boolean;
11
22
  /**
12
- * The authorizations a reader may rely on at `now`. Section 5: an expired file contains no valid
13
- * authorizations. Individually expired authorizations are dropped too.
23
+ * Section 6.1: the identity an agent matches on (never the retailer name). Domains are normalized;
24
+ * Amazon seller IDs are uppercased, eBay user IDs lowercased and Walmart partner IDs kept as they are.
14
25
  */
15
- export declare function validAuthorizations(file: FullFile, now?: number): Authorization[];
26
+ export declare function channelKey(channel: Channel): string;
27
+ /** The marketplace channel's type as its domain implies, or null for a non-marketplace domain. */
28
+ export declare function marketplaceTypeForDomain(domain: string): MarketplaceChannel["type"] | null;
29
+ /** Authorizations naming this channel. Matching uses channelKey only; retailer names are ignored. */
30
+ export declare function authorizationsForChannel(authorizations: Authorization[], channel: Channel): Authorization[];
16
31
  /**
17
32
  * Brings raw agent input into the canonical form validateVerifyRequest expects: trims strings,
18
- * lowercases domains (dropping "www."), uppercases country codes. Leaves unexpected shapes alone
19
- * so the validator reports them.
33
+ * lowercases domains (dropping "www."), uppercases country codes and ASINs, maps "UK" to "GB" and
34
+ * strips spaces and dashes from GTINs. Leaves unexpected shapes alone so the validator reports them.
20
35
  */
21
36
  export declare function normalizeVerifyRequest(input: unknown): unknown;
22
37
  /**
23
- * Canonical form of a channel as typed by a person or agent: trims, lowercases the type and
24
- * domains (dropping "www."), uppercases country codes. Unexpected shapes pass through untouched.
25
- * Identifier case is kept; channelKey decides how case compares.
38
+ * Canonical form of a channel as typed by a person or agent: trims, lowercases the type and domain
39
+ * (dropping "www."). Seller ID case is kept; channelKey decides how case compares.
26
40
  */
27
41
  export declare function normalizeChannel(input: unknown): unknown;
42
+ export declare function isRecord(v: unknown): v is Record<string, unknown>;
package/dist/identity.js CHANGED
@@ -1,55 +1,92 @@
1
- import { parseTimestamp } from "./timestamp.js";
2
- /** Marketplace codes some platforms use in place of ISO 3166-1 alpha-2. */
3
- const MARKETPLACE_ALIASES = { UK: "GB" };
1
+ import { GENERIC_CCTLDS, MARKETPLACE_DOMAINS } from "./constants.js";
2
+ import { common } from "./schemas.js";
3
+ /** ISO 3166-1 alpha-2 codes, from the schema so the two can't disagree. */
4
+ const ISO_COUNTRIES = new Set(common.$defs.countryCode.enum);
5
+ /** Codes people commonly type in place of ISO 3166-1 alpha-2. Schemas accept only the ISO code. */
6
+ const COUNTRY_ALIASES = { UK: "GB" };
7
+ /** Section 6: lowercase, no trailing dot, no leading "www.". */
4
8
  export function normalizeDomain(domain) {
5
9
  return domain.trim().toLowerCase().replace(/\.$/, "").replace(/^www\./, "");
6
10
  }
11
+ /** Uppercases and maps common aliases (UK) to ISO 3166-1 alpha-2 (GB). */
7
12
  export function normalizeCountry(code) {
8
13
  const upper = code.trim().toUpperCase();
9
- return MARKETPLACE_ALIASES[upper] ?? upper;
14
+ return COUNTRY_ALIASES[upper] ?? upper;
15
+ }
16
+ export function isCountryCode(code) {
17
+ return ISO_COUNTRIES.has(code);
18
+ }
19
+ export function isMarketplaceDomain(domain) {
20
+ return Object.hasOwn(MARKETPLACE_DOMAINS, normalizeDomain(domain));
21
+ }
22
+ /**
23
+ * Section 6.2: the country a domain implies. A marketplace domain's country from the table, a
24
+ * country-code domain's country (".co.uk" is GB), or null for a generic domain (".com", ".io").
25
+ */
26
+ export function territoryForDomain(domain) {
27
+ const d = normalizeDomain(domain);
28
+ const market = MARKETPLACE_DOMAINS[d];
29
+ if (market)
30
+ return market.country;
31
+ const tld = d.slice(d.lastIndexOf(".") + 1);
32
+ if (tld.length !== 2 || GENERIC_CCTLDS.has(tld))
33
+ return null;
34
+ const country = tld === "uk" ? "GB" : tld.toUpperCase();
35
+ return ISO_COUNTRIES.has(country) ? country : null;
10
36
  }
11
37
  /**
12
- * The identity an agent matches on (section 6: match on the channel identifier, never the
13
- * retailer name). Returns null for physical channels, which agents cannot verify.
38
+ * Section 6.2: the territories one channel of an authorization covers. A marketplace channel covers
39
+ * only its domain's country, and only if the scope (when given) allows it. A web store covers
40
+ * scope.territories when given, otherwise its domain's country. An empty result means none: a
41
+ * marketplace country the scope leaves out, or a generic-domain web store with no territories.
42
+ */
43
+ export function effectiveTerritories(authorization, channel) {
44
+ const listed = authorization.scope.territories;
45
+ const own = territoryForDomain(channel.domain);
46
+ if (channel.type !== "web") {
47
+ if (own === null)
48
+ return [];
49
+ return !listed || listed[0] === "*" || listed.includes(own) ? [own] : [];
50
+ }
51
+ if (listed)
52
+ return listed;
53
+ return own === null ? [] : [own];
54
+ }
55
+ /** Whether a channel of an authorization covers a territory. */
56
+ export function coversTerritory(authorization, channel, territory) {
57
+ const t = effectiveTerritories(authorization, channel);
58
+ return t[0] === "*" || t.includes(territory);
59
+ }
60
+ /**
61
+ * Section 6.1: the identity an agent matches on (never the retailer name). Domains are normalized;
62
+ * Amazon seller IDs are uppercased, eBay user IDs lowercased and Walmart partner IDs kept as they are.
14
63
  */
15
64
  export function channelKey(channel) {
65
+ const domain = normalizeDomain(channel.domain);
16
66
  switch (channel.type) {
17
67
  case "amazon":
18
- return `amazon:${normalizeCountry(channel.marketplace)}:${channel.seller_id.trim().toUpperCase()}`;
68
+ return `amazon:${domain}:${channel.seller_id.trim().toUpperCase()}`;
19
69
  case "walmart":
20
- return `walmart:${normalizeCountry(channel.marketplace)}:${channel.seller_id.trim()}`;
70
+ return `walmart:${domain}:${channel.seller_id.trim()}`;
21
71
  case "ebay":
22
- return `ebay:${normalizeCountry(channel.marketplace)}:${channel.seller_id.trim().toLowerCase()}`;
72
+ return `ebay:${domain}:${channel.seller_id.trim().toLowerCase()}`;
23
73
  case "web":
24
- return `web:${normalizeDomain(channel.domain)}`;
25
- case "physical":
26
- return null;
74
+ return `web:${domain}`;
27
75
  }
28
76
  }
77
+ /** The marketplace channel's type as its domain implies, or null for a non-marketplace domain. */
78
+ export function marketplaceTypeForDomain(domain) {
79
+ return MARKETPLACE_DOMAINS[normalizeDomain(domain)]?.type ?? null;
80
+ }
29
81
  /** Authorizations naming this channel. Matching uses channelKey only; retailer names are ignored. */
30
82
  export function authorizationsForChannel(authorizations, channel) {
31
83
  const key = channelKey(channel);
32
- if (key === null)
33
- return [];
34
84
  return authorizations.filter((a) => a.channels.some((c) => channelKey(c) === key));
35
85
  }
36
- /**
37
- * The authorizations a reader may rely on at `now`. Section 5: an expired file contains no valid
38
- * authorizations. Individually expired authorizations are dropped too.
39
- */
40
- export function validAuthorizations(file, now = Date.now()) {
41
- const fileExpires = parseTimestamp(file.expires);
42
- if (fileExpires === null || now >= fileExpires)
43
- return [];
44
- return file.authorizations.filter((a) => {
45
- const exp = parseTimestamp(a.expires);
46
- return exp !== null && now < exp;
47
- });
48
- }
49
86
  /**
50
87
  * Brings raw agent input into the canonical form validateVerifyRequest expects: trims strings,
51
- * lowercases domains (dropping "www."), uppercases country codes. Leaves unexpected shapes alone
52
- * so the validator reports them.
88
+ * lowercases domains (dropping "www."), uppercases country codes and ASINs, maps "UK" to "GB" and
89
+ * strips spaces and dashes from GTINs. Leaves unexpected shapes alone so the validator reports them.
53
90
  */
54
91
  export function normalizeVerifyRequest(input) {
55
92
  if (!isRecord(input))
@@ -61,13 +98,22 @@ export function normalizeVerifyRequest(input) {
61
98
  out.territory = normalizeCountry(out.territory);
62
99
  if (typeof out.product_line === "string")
63
100
  out.product_line = out.product_line.trim();
101
+ if (typeof out.as_of === "string")
102
+ out.as_of = out.as_of.trim();
103
+ if (isRecord(out.product)) {
104
+ const p = { ...out.product };
105
+ if (typeof p.gtin === "string")
106
+ p.gtin = p.gtin.replace(/[\s-]/g, "");
107
+ if (typeof p.asin === "string")
108
+ p.asin = p.asin.trim().toUpperCase();
109
+ out.product = p;
110
+ }
64
111
  out.channel = normalizeChannel(out.channel);
65
112
  return out;
66
113
  }
67
114
  /**
68
- * Canonical form of a channel as typed by a person or agent: trims, lowercases the type and
69
- * domains (dropping "www."), uppercases country codes. Unexpected shapes pass through untouched.
70
- * Identifier case is kept; channelKey decides how case compares.
115
+ * Canonical form of a channel as typed by a person or agent: trims, lowercases the type and domain
116
+ * (dropping "www."). Seller ID case is kept; channelKey decides how case compares.
71
117
  */
72
118
  export function normalizeChannel(input) {
73
119
  if (!isRecord(input))
@@ -75,18 +121,12 @@ export function normalizeChannel(input) {
75
121
  const ch = { ...input };
76
122
  if (typeof ch.type === "string")
77
123
  ch.type = ch.type.trim().toLowerCase();
78
- if (typeof ch.marketplace === "string")
79
- ch.marketplace = normalizeCountry(ch.marketplace);
80
124
  if (typeof ch.seller_id === "string")
81
125
  ch.seller_id = ch.seller_id.trim();
82
126
  if (typeof ch.domain === "string")
83
127
  ch.domain = normalizeDomain(ch.domain);
84
- if (typeof ch.address === "string")
85
- ch.address = ch.address.trim();
86
- if (typeof ch.country === "string")
87
- ch.country = ch.country.trim().toUpperCase();
88
128
  return ch;
89
129
  }
90
- function isRecord(v) {
130
+ export function isRecord(v) {
91
131
  return typeof v === "object" && v !== null && !Array.isArray(v);
92
132
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export * from "./constants.js";
3
3
  export * from "./timestamp.js";
4
4
  export * from "./validate.js";
5
5
  export * from "./identity.js";
6
+ export * from "./product.js";
7
+ export * from "./match.js";
6
8
  export * from "./jcs.js";
7
9
  export * from "./jws.js";
8
10
  export * from "./published.js";
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export * from "./constants.js";
3
3
  export * from "./timestamp.js";
4
4
  export * from "./validate.js";
5
5
  export * from "./identity.js";
6
+ export * from "./product.js";
7
+ export * from "./match.js";
6
8
  export * from "./jcs.js";
7
9
  export * from "./jws.js";
8
10
  export * from "./published.js";
package/dist/jws.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { RevocationFeed } from "./types.js";
1
2
  export declare const JWS_ALG = "EdDSA";
2
3
  export interface PublicJwk {
3
4
  kty: "OKP";
@@ -19,10 +20,11 @@ export declare function signingPayload(doc: object): Uint8Array<ArrayBuffer>;
19
20
  * or a future managed key service) call this; nothing else in the registry sees the key.
20
21
  */
21
22
  export declare function signDetached(privateKey: CryptoKey, kid: string, payload: Uint8Array): Promise<string>;
22
- export type VerifyFailure = "missing_signature" | "malformed_jws" | "alg_not_allowed" | "kid_mismatch" | "unknown_kid" | "bad_signature" | "answer_expired" | "list_expired";
23
+ export type VerifyFailure = "missing_signature" | "malformed_jws" | "alg_not_allowed" | "kid_mismatch" | "unknown_kid" | "bad_signature" | "answer_expired" | "list_expired" | "feed_expired" | "revocation_feed_required" | "revocation_feed_invalid" | "revoked";
23
24
  export type SignatureCheck = {
24
25
  valid: true;
25
26
  kid: string;
27
+ stale?: true;
26
28
  } | {
27
29
  valid: false;
28
30
  reason: VerifyFailure;
@@ -30,13 +32,42 @@ export type SignatureCheck = {
30
32
  export interface VerifyOptions {
31
33
  /** Epoch ms to check validity against. Defaults to Date.now(). */
32
34
  now?: number;
35
+ /**
36
+ * Section 10.2: accept a list or answer between its valid_until and stale_until. Only for when the
37
+ * registry and every mirror are unreachable, and only with a revocation feed to check against.
38
+ */
39
+ allowStale?: boolean;
40
+ /** The newest revocation feed the caller could get, unverified; verifyDocument checks it. */
41
+ revocationFeed?: RevocationFeed;
33
42
  }
34
43
  /**
35
- * Reference verifier for agents (§8, §9, §12). Checks, in order:
44
+ * Reference verifier for agents (sections 8, 9, 10, 12). Checks, in order:
36
45
  * - the signature is a well-formed detached EdDSA JWS whose header kid matches signature.kid;
37
- * - the kid is in the current key set: signatures from keys no longer in the set are rejected (§8 MUST);
46
+ * - the kid is in the current key set: signatures from keys no longer in the set are rejected (section 8 MUST);
38
47
  * - the signature verifies over the canonical document;
39
- * - a verify answer is not past its valid_until (§9, §12 MUST), and a list is not past its expires (§5).
40
- * Callers SHOULD refetch the key set once on "unknown_kid" before giving up (§8).
48
+ * - the document is not past its valid_until (section 10.1 MUST). With allowStale and a revocation
49
+ * feed, a list or answer up to its stale_until is accepted as { valid: true, stale: true }, unless
50
+ * the feed lists the answer's revocation_id (section 10.3). A stale list's revoked authorizations
51
+ * are dropped by validAuthorizations(list, now, feed), not here.
52
+ * - a revocation feed is usable up to its own stale_until, and is flagged stale after valid_until.
53
+ * Callers SHOULD refetch the key set once on "unknown_kid" before giving up (section 8).
41
54
  */
42
55
  export declare function verifyDocument(doc: object, jwks: Jwks, opts?: VerifyOptions): Promise<SignatureCheck>;
56
+ /** Whether a revocation feed lists this revocation_id (section 10.3). Doesn't check the feed's signature. */
57
+ export declare function isRevoked(feed: RevocationFeed, revocationId: string): boolean;
58
+ export type RevocationFeedCheck = {
59
+ valid: true;
60
+ kid: string;
61
+ stale: boolean;
62
+ revoked: (revocationId: string) => boolean;
63
+ } | {
64
+ valid: false;
65
+ reason: VerifyFailure | "not_a_feed";
66
+ };
67
+ /**
68
+ * Section 10.3: checks a revocation feed's signature and freshness (usable to its stale_until) and
69
+ * returns a lookup for revocation IDs. Makes no network calls: the caller fetches the feed.
70
+ */
71
+ export declare function checkRevocationFeed(feed: unknown, jwks: Jwks, opts?: {
72
+ now?: number;
73
+ }): Promise<RevocationFeedCheck>;