@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.
- package/README.md +66 -47
- package/dist/constants.d.ts +25 -6
- package/dist/constants.js +41 -7
- package/dist/identity.d.ts +29 -14
- package/dist/identity.js +79 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/jws.d.ts +36 -5
- package/dist/jws.js +48 -14
- package/dist/match.d.ts +21 -0
- package/dist/match.js +89 -0
- package/dist/product.d.ts +34 -0
- package/dist/product.js +75 -0
- package/dist/published.d.ts +14 -8
- package/dist/published.js +32 -10
- package/dist/schemas.d.ts +366 -91
- package/dist/schemas.js +802 -181
- package/dist/types.d.ts +121 -47
- package/dist/types.js +2 -2
- package/dist/validate.d.ts +24 -10
- package/dist/validate.js +300 -107
- package/fixtures/list/invalid/list-all-mixed.json +53 -0
- package/fixtures/list/invalid/list-amazon-missing-seller-id.json +51 -0
- package/fixtures/list/invalid/list-amazon-on-walmart-domain.json +52 -0
- package/fixtures/{file/invalid/full-attached-jws.json → list/invalid/list-attached-jws.json} +14 -12
- package/fixtures/list/invalid/list-bad-asin.json +102 -0
- package/fixtures/list/invalid/list-bad-gtin.json +102 -0
- package/fixtures/list/invalid/list-brand-exclusion-no-catalog.json +57 -0
- package/fixtures/list/invalid/list-duplicate-asin.json +102 -0
- package/fixtures/{file/invalid/full-duplicate-authorization-id.json → list/invalid/list-duplicate-authorization-id.json} +23 -18
- package/fixtures/list/invalid/list-duplicate-gtin.json +102 -0
- package/fixtures/list/invalid/list-duplicate-product-id.json +102 -0
- package/fixtures/{file/invalid/full-empty-territories.json → list/invalid/list-empty-territories.json} +17 -10
- package/fixtures/list/invalid/list-exclude-unknown-product.json +105 -0
- package/fixtures/list/invalid/list-expires-field.json +53 -0
- package/fixtures/list/invalid/list-extra-property.json +53 -0
- package/fixtures/list/invalid/list-generic-web-no-territories.json +39 -0
- package/fixtures/list/invalid/list-gtin-11-digits.json +102 -0
- package/fixtures/list/invalid/list-impossible-date.json +53 -0
- package/fixtures/list/invalid/list-lowercase-territory.json +52 -0
- package/fixtures/{file/invalid/full-signature-missing-kid.json → list/invalid/list-marketplace-field.json} +14 -11
- package/fixtures/list/invalid/list-marketplace-territory-mismatch.json +52 -0
- package/fixtures/list/invalid/list-missing-revocation-id.json +51 -0
- package/fixtures/list/invalid/list-missing-scope.json +44 -0
- package/fixtures/list/invalid/list-missing-stale-until.json +51 -0
- package/fixtures/list/invalid/list-name-only-retailer.json +36 -0
- package/fixtures/list/invalid/list-no-channels.json +37 -0
- package/fixtures/list/invalid/list-non-utc-timestamp.json +52 -0
- package/fixtures/list/invalid/list-not-public-with-authorizations.json +18 -0
- package/fixtures/list/invalid/list-not-public-with-catalog.json +18 -0
- package/fixtures/{file/invalid/full-physical-only.json → list/invalid/list-physical.json} +16 -10
- package/fixtures/list/invalid/list-product-no-identifier.json +98 -0
- package/fixtures/list/invalid/list-scope-no-products.json +49 -0
- package/fixtures/{file/valid/full-signed.json → list/invalid/list-single-domain.json} +12 -12
- package/fixtures/list/invalid/list-stale-over-24-hours.json +52 -0
- package/fixtures/list/invalid/list-territory-uk.json +52 -0
- package/fixtures/{file/invalid/full-unknown-channel-type.json → list/invalid/list-unknown-channel-type.json} +18 -12
- package/fixtures/list/invalid/list-unknown-line.json +102 -0
- package/fixtures/list/invalid/list-unknown-product.json +105 -0
- package/fixtures/{file/invalid/full-missing-authorization-expires.json → list/invalid/list-unsigned.json} +12 -9
- package/fixtures/list/invalid/list-valid-over-15-minutes.json +52 -0
- package/fixtures/{file/invalid/full-web-domain-is-url.json → list/invalid/list-web-domain-is-url.json} +18 -12
- package/fixtures/list/invalid/list-web-on-marketplace-domain.json +52 -0
- package/fixtures/{file/invalid/full-wildcard-mixed.json → list/invalid/list-wildcard-mixed.json} +17 -10
- package/fixtures/list/valid/list-catalog-line-scope.json +102 -0
- package/fixtures/list/valid/list-cctld-web.json +39 -0
- package/fixtures/list/valid/list-fractional-seconds.json +52 -0
- package/fixtures/{file/invalid/full-amazon-missing-seller-id.json → list/valid/list-generic-web-territories.json} +15 -17
- package/fixtures/list/valid/list-marketplace-no-territories.json +40 -0
- package/fixtures/list/valid/list-no-authorizations.json +18 -0
- package/fixtures/list/valid/list-no-catalog.json +52 -0
- package/fixtures/list/valid/list-no-expires.json +52 -0
- package/fixtures/list/valid/list-not-public.json +17 -0
- package/fixtures/list/valid/list-product-scope.json +103 -0
- package/fixtures/list/valid/list-proposed-by.json +55 -0
- package/fixtures/list/valid/list-with-expires.json +53 -0
- package/fixtures/list/valid/list-worldwide.json +52 -0
- package/fixtures/manifest.json +606 -158
- package/fixtures/match/all-scope-excluded-line.json +194 -0
- package/fixtures/match/all-scope-not-in-catalog.json +194 -0
- package/fixtures/match/as-of-before-end-date.json +194 -0
- package/fixtures/match/brand-exclusion-beats-named-product.json +194 -0
- package/fixtures/match/duplicate-asin-authorized-seller.json +192 -0
- package/fixtures/match/duplicate-asin-unauthorized-seller.json +191 -0
- package/fixtures/match/expired-end-date.json +193 -0
- package/fixtures/match/marketplace-territory-default.json +186 -0
- package/fixtures/match/marketplace-territory-mismatch.json +187 -0
- package/fixtures/match/match-by-channel-key.json +186 -0
- package/fixtures/match/product-excluded-by-authorization.json +194 -0
- package/fixtures/match/product-line-request.json +186 -0
- package/fixtures/match/product-not-in-scope.json +194 -0
- package/fixtures/match/seller-authorized-by-gtin.json +194 -0
- package/fixtures/match/seller-authorized-for-line.json +194 -0
- package/fixtures/match/worldwide-web.json +186 -0
- package/fixtures/pointer/invalid/full-form.json +11 -0
- package/fixtures/pointer/invalid/pointer-domain-mismatch.json +10 -0
- package/fixtures/{file → pointer}/invalid/pointer-http-url.json +2 -1
- package/fixtures/pointer/invalid/pointer-missing-verify-token.json +9 -0
- package/fixtures/pointer/invalid/pointer-other-host.json +10 -0
- package/fixtures/{file/valid/pointer.json → pointer/invalid/pointer-wrong-spec.json} +1 -0
- package/fixtures/pointer/invalid/private-form.json +10 -0
- package/fixtures/pointer/invalid/unknown-form.json +10 -0
- package/fixtures/pointer/valid/pointer-fetched-via-www.json +10 -0
- package/fixtures/pointer/valid/pointer.json +10 -0
- package/fixtures/revocation-feed/invalid/feed-hashed-entry.json +17 -0
- package/fixtures/revocation-feed/invalid/feed-valid-over-15-minutes.json +17 -0
- package/fixtures/revocation-feed/valid/feed-empty.json +12 -0
- package/fixtures/revocation-feed/valid/feed.json +17 -0
- package/fixtures/verify-request/invalid/bad-as-of.json +9 -0
- package/fixtures/verify-request/invalid/bad-gtin.json +11 -0
- package/fixtures/verify-request/invalid/domain-with-scheme.json +2 -4
- package/fixtures/verify-request/{valid/web-no-product-line.json → invalid/generic-web-no-territory.json} +1 -2
- package/fixtures/verify-request/invalid/gtin-and-asin.json +12 -0
- package/fixtures/verify-request/{valid/amazon.json → invalid/marketplace-field.json} +1 -3
- package/fixtures/verify-request/invalid/missing-channel.json +1 -3
- package/fixtures/verify-request/invalid/physical-channel.json +1 -3
- package/fixtures/verify-request/invalid/retailer-name-only.json +0 -2
- package/fixtures/verify-request/invalid/territory-uk.json +9 -0
- package/fixtures/verify-request/invalid/wildcard-territory.json +2 -3
- package/fixtures/verify-request/valid/as-of-datetime.json +9 -0
- package/fixtures/verify-request/valid/full.json +13 -0
- package/fixtures/verify-request/valid/gtin.json +11 -0
- package/fixtures/verify-request/valid/minimal-marketplace.json +8 -0
- package/fixtures/verify-request/valid/web-cctld-no-territory.json +7 -0
- package/fixtures/verify-request/valid/web-generic-with-territory.json +9 -0
- package/fixtures/verify-response/invalid/authorized-missing-revocation-id.json +18 -0
- package/fixtures/verify-response/invalid/authorized-with-reason.json +20 -0
- package/fixtures/verify-response/invalid/brand-unverified.json +13 -0
- package/fixtures/verify-response/invalid/expired-without-expires.json +18 -0
- package/fixtures/verify-response/invalid/missing-observed.json +11 -5
- package/fixtures/verify-response/invalid/missing-signature.json +9 -3
- package/fixtures/verify-response/invalid/missing-stale-until.json +18 -0
- package/fixtures/verify-response/invalid/not-in-catalog-with-id.json +17 -0
- package/fixtures/verify-response/invalid/not-registered-with-unlisted-reason.json +13 -0
- package/fixtures/verify-response/invalid/stale-until-over-24-hours.json +19 -0
- package/fixtures/verify-response/invalid/unlisted-observed.json +5 -4
- package/fixtures/verify-response/invalid/unlisted-with-authorization-id.json +5 -4
- package/fixtures/verify-response/invalid/unlisted-with-not-registered-reason.json +13 -0
- package/fixtures/verify-response/invalid/unlisted-with-revocation-id.json +13 -0
- package/fixtures/verify-response/invalid/valid-until-before-checked.json +11 -5
- package/fixtures/verify-response/invalid/valid-until-over-15-minutes.json +19 -0
- package/fixtures/verify-response/invalid/with-stale-flag.json +20 -0
- package/fixtures/verify-response/invalid/with-tier.json +11 -5
- package/fixtures/verify-response/valid/authorized-no-product.json +14 -0
- package/fixtures/verify-response/valid/authorized-not-in-catalog.json +22 -0
- package/fixtures/verify-response/valid/authorized-observed.json +11 -5
- package/fixtures/verify-response/valid/authorized-private-product.json +17 -0
- package/fixtures/verify-response/valid/authorized-with-end-date.json +20 -0
- package/fixtures/verify-response/valid/authorized.json +11 -5
- package/fixtures/verify-response/valid/disputed.json +10 -5
- package/fixtures/verify-response/valid/expired.json +12 -6
- package/fixtures/verify-response/valid/not-registered-no-reason.json +12 -0
- package/fixtures/verify-response/valid/not-registered-reason.json +13 -0
- package/fixtures/verify-response/valid/unlisted-product-excluded.json +18 -0
- package/fixtures/verify-response/valid/unlisted-product-not-in-catalog.json +16 -0
- package/fixtures/verify-response/valid/unlisted-territory-mismatch.json +13 -0
- package/fixtures/verify-response/valid/unlisted.json +5 -4
- package/fixtures/verify-response/valid/with-as-of.json +20 -0
- package/fixtures/verify-response/valid/with-signal.json +11 -5
- package/package.json +5 -4
- package/schemas/0.2/common.json +761 -0
- package/schemas/0.2/list.json +87 -0
- package/schemas/0.2/pointer.json +33 -0
- package/schemas/0.2/revocation-feed.json +55 -0
- package/schemas/0.2/verify-request.json +65 -0
- package/schemas/0.2/verify-response.json +284 -0
- package/spec/spec-v0.2.md +531 -0
- package/fixtures/file/invalid/full-all-mixed.json +0 -47
- package/fixtures/file/invalid/full-authorization-expires-after-file.json +0 -46
- package/fixtures/file/invalid/full-expires-before-issued.json +0 -46
- package/fixtures/file/invalid/full-expiry-over-180-days.json +0 -46
- package/fixtures/file/invalid/full-extra-property.json +0 -47
- package/fixtures/file/invalid/full-impossible-date.json +0 -46
- package/fixtures/file/invalid/full-lowercase-territory.json +0 -45
- package/fixtures/file/invalid/full-missing-file-expires.json +0 -45
- package/fixtures/file/invalid/full-missing-product-lines.json +0 -43
- package/fixtures/file/invalid/full-missing-scope.json +0 -37
- package/fixtures/file/invalid/full-missing-territories.json +0 -42
- package/fixtures/file/invalid/full-name-only-retailer.json +0 -30
- package/fixtures/file/invalid/full-no-channels.json +0 -31
- package/fixtures/file/invalid/full-non-utc-timestamp.json +0 -46
- package/fixtures/file/invalid/full-physical-missing-country.json +0 -50
- package/fixtures/file/invalid/full-web-missing-domain.json +0 -45
- package/fixtures/file/invalid/full-wrong-spec.json +0 -46
- package/fixtures/file/invalid/private-missing-verify.json +0 -8
- package/fixtures/file/invalid/private-with-authorizations.json +0 -45
- package/fixtures/file/invalid/unknown-form.json +0 -9
- package/fixtures/file/valid/full-fractional-seconds.json +0 -46
- package/fixtures/file/valid/full-max-expiry.json +0 -46
- package/fixtures/file/valid/full-no-authorizations.json +0 -11
- package/fixtures/file/valid/full-unsigned-handwritten.json +0 -46
- package/fixtures/file/valid/full-worldwide-physical-proposed.json +0 -82
- package/fixtures/file/valid/private.json +0 -9
- package/fixtures/verify-response/invalid/authorized-missing-authorization-id.json +0 -12
- package/fixtures/verify-response/invalid/missing-valid-until.json +0 -12
- package/fixtures/verify-response/invalid/observed-without-last-seen.json +0 -13
- package/fixtures/verify-response/invalid/unknown-status.json +0 -13
- package/fixtures/verify-response/invalid/unlisted-with-reason.json +0 -12
- package/fixtures/verify-response/invalid/valid-until-over-24h.json +0 -13
- package/fixtures/verify-response/valid/brand-unverified-no-reason.json +0 -11
- package/fixtures/verify-response/valid/brand-unverified-reason.json +0 -12
- /package/schemas/{common.json → 0.1/common.json} +0 -0
- /package/schemas/{file-full.json → 0.1/file-full.json} +0 -0
- /package/schemas/{file-pointer.json → 0.1/file-pointer.json} +0 -0
- /package/schemas/{file-private.json → 0.1/file-private.json} +0 -0
- /package/schemas/{file.json → 0.1/file.json} +0 -0
- /package/schemas/{verify-request.json → 0.1/verify-request.json} +0 -0
- /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
|
|
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.
|
|
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
|
|
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.
|
|
10
|
-
- **
|
|
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
|
|
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 |
|
|
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
|
-
##
|
|
30
|
+
## Register a brand
|
|
31
31
|
|
|
32
|
-
A brand
|
|
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
|
-
|
|
35
|
+
authorizedretailers-verify=<token from the registry>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
|
|
40
|
+
```
|
|
41
|
+
https://<brand-domain>/.well-known/authorized-retailers.json
|
|
42
|
+
```
|
|
47
43
|
|
|
48
44
|
```json
|
|
49
45
|
{
|
|
50
|
-
"spec": "authorized-retailers/0.
|
|
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
|
-
|
|
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
|
-
```
|
|
64
|
-
import { validateFile, validAuthorizations } from "@authorizedretailers/spec";
|
|
62
|
+
```js
|
|
63
|
+
import { validateFile, checkPublishedList, validAuthorizations } from "@authorizedretailers/spec";
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
```
|
|
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",
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
]);
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
import type { Authorization, Channel,
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
/**
|
|
10
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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:${
|
|
68
|
+
return `amazon:${domain}:${channel.seller_id.trim().toUpperCase()}`;
|
|
19
69
|
case "walmart":
|
|
20
|
-
return `walmart:${
|
|
70
|
+
return `walmart:${domain}:${channel.seller_id.trim()}`;
|
|
21
71
|
case "ebay":
|
|
22
|
-
return `ebay:${
|
|
72
|
+
return `ebay:${domain}:${channel.seller_id.trim().toLowerCase()}`;
|
|
23
73
|
case "web":
|
|
24
|
-
return `web:${
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
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 (
|
|
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
|
-
* -
|
|
40
|
-
*
|
|
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>;
|