@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/dist/validate.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Validator } from "@cfworker/json-schema";
|
|
2
|
-
import { common,
|
|
3
|
-
import {
|
|
2
|
+
import { common, list, pointer, revocationFeed, verifyRequest, verifyResponse } from "./schemas.js";
|
|
3
|
+
import { MAX_STALE_MS, MAX_VALIDITY_MS, MARKETPLACE_DOMAINS } from "./constants.js";
|
|
4
|
+
import { isCountryCode, isMarketplaceDomain, isRecord, normalizeDomain, territoryForDomain } from "./identity.js";
|
|
5
|
+
import { isValidAsin, isValidGtin, toGtin14 } from "./product.js";
|
|
4
6
|
import { parseTimestamp } from "./timestamp.js";
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
7
|
+
const V = "https://authorizedretailers.ai/schemas/0.2";
|
|
8
|
+
const ALL_SCHEMAS = [common, pointer, list, verifyRequest, verifyResponse, revocationFeed];
|
|
9
|
+
const scopeSchema = { $id: `${V}/scope.json`, $ref: "common.json#/$defs/scope" };
|
|
10
|
+
const channelSchema = { $id: `${V}/channel.json`, $ref: "common.json#/$defs/channel" };
|
|
11
|
+
const productSchema = { $id: `${V}/product.json`, $ref: "common.json#/$defs/product" };
|
|
8
12
|
function makeValidator(root) {
|
|
9
13
|
const v = new Validator(root, "2020-12", false);
|
|
10
14
|
for (const s of ALL_SCHEMAS)
|
|
@@ -39,136 +43,325 @@ function dedupe(errors) {
|
|
|
39
43
|
return true;
|
|
40
44
|
});
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
class Report {
|
|
47
|
+
errors = [];
|
|
48
|
+
warnings = [];
|
|
49
|
+
/** Paths a specific error already explains; schema errors under them only repeat it. */
|
|
50
|
+
claimed = [];
|
|
51
|
+
error(code, path, message) {
|
|
52
|
+
this.errors.push({ code, path, message });
|
|
53
|
+
}
|
|
54
|
+
warn(code, path, message) {
|
|
55
|
+
this.warnings.push({ code, path, message });
|
|
56
|
+
}
|
|
57
|
+
done(value) {
|
|
58
|
+
return this.errors.length === 0
|
|
59
|
+
? { valid: true, value: value, errors: [], warnings: this.warnings }
|
|
60
|
+
: { valid: false, errors: this.errors, warnings: this.warnings };
|
|
61
|
+
}
|
|
44
62
|
}
|
|
45
|
-
function ts(
|
|
63
|
+
function ts(r, path, value) {
|
|
46
64
|
const ms = parseTimestamp(value);
|
|
47
65
|
if (ms === null)
|
|
48
|
-
|
|
66
|
+
r.error("invalid_timestamp", path, `not a valid UTC timestamp: ${value}`);
|
|
49
67
|
return ms;
|
|
50
68
|
}
|
|
51
|
-
const
|
|
69
|
+
const CHANNEL_TYPES = new Set(["amazon", "walmart", "ebay", "web"]);
|
|
52
70
|
/**
|
|
53
|
-
* Section 6
|
|
54
|
-
*
|
|
55
|
-
* Runs on unvalidated input, so it checks shapes defensively.
|
|
71
|
+
* Section 6 rules on one channel that the generic schema output would bury: removed fields and
|
|
72
|
+
* types, and marketplace domains that don't match the type. Runs on unvalidated input.
|
|
56
73
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const issued = ts(errors, "/issued", f.issued);
|
|
81
|
-
const expires = ts(errors, "/expires", f.expires);
|
|
82
|
-
if (issued !== null && expires !== null) {
|
|
83
|
-
if (expires <= issued) {
|
|
84
|
-
errors.push({ code: "expires_not_after_issued", path: "/expires", message: "file expires must be after issued" });
|
|
74
|
+
function channelRules(r, path, input) {
|
|
75
|
+
if (!isRecord(input))
|
|
76
|
+
return;
|
|
77
|
+
const before = r.errors.length;
|
|
78
|
+
channelChecks(r, path, input);
|
|
79
|
+
if (r.errors.length > before)
|
|
80
|
+
r.claimed.push(path);
|
|
81
|
+
}
|
|
82
|
+
function channelChecks(r, path, input) {
|
|
83
|
+
const type = input.type;
|
|
84
|
+
if (typeof type === "string" && !CHANNEL_TYPES.has(type)) {
|
|
85
|
+
r.error("unknown_channel_type", path, `unknown channel type "${type}"; v0.2 has amazon, walmart, ebay and web (section 6)`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if ("marketplace" in input) {
|
|
89
|
+
r.error("marketplace_field_removed", `${path}/marketplace`, 'the "marketplace" field was removed in v0.2: identify the marketplace by "domain", e.g. "amazon.com" (section 6)');
|
|
90
|
+
}
|
|
91
|
+
if (typeof input.domain !== "string")
|
|
92
|
+
return;
|
|
93
|
+
const domain = normalizeDomain(input.domain);
|
|
94
|
+
if (type === "web") {
|
|
95
|
+
if (isMarketplaceDomain(domain)) {
|
|
96
|
+
r.error("marketplace_domain_mismatch", `${path}/domain`, `${domain} is a marketplace domain; list the seller as a ${MARKETPLACE_DOMAINS[domain].type} channel (section 6)`);
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
98
|
+
}
|
|
99
|
+
else if (typeof type === "string") {
|
|
100
|
+
const market = MARKETPLACE_DOMAINS[domain];
|
|
101
|
+
if (!market || market.type !== type) {
|
|
102
|
+
r.error("marketplace_domain_mismatch", `${path}/domain`, `${domain} is not one of the ${type} marketplace domains (section 6)`);
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
}
|
|
106
|
+
/** Territory codes must be ISO 3166-1 alpha-2 (UK is GB). The schema enforces this too; name it plainly. */
|
|
107
|
+
function countryRule(r, path, value) {
|
|
108
|
+
if (typeof value === "string" && value !== "*" && !isCountryCode(value)) {
|
|
109
|
+
r.error("invalid_country", path, `${value} is not an ISO 3166-1 alpha-2 code${value.toUpperCase() === "UK" ? " (the United Kingdom is GB)" : ""}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Section 6.2 territory rules for one authorization. Assumes the list passed the schema. */
|
|
113
|
+
function territoryRules(r, base, channels, scope) {
|
|
114
|
+
const listed = scope.territories;
|
|
115
|
+
const worldwide = listed?.[0] === "*";
|
|
116
|
+
channels.forEach((c, j) => {
|
|
117
|
+
const own = territoryForDomain(c.domain);
|
|
118
|
+
const path = `${base}/channels/${j}`;
|
|
119
|
+
if (c.type !== "web") {
|
|
120
|
+
if (listed && !worldwide && own !== null && !listed.includes(own)) {
|
|
121
|
+
r.error("territory_mismatch", `${base}/scope/territories`, `${c.domain} is in ${own}, which scope.territories leaves out (section 6.2)`);
|
|
122
|
+
}
|
|
99
123
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
message: "an authorization's expires MUST NOT be later than the file's expires (section 5)",
|
|
107
|
-
});
|
|
124
|
+
else if (own === null) {
|
|
125
|
+
if (!listed)
|
|
126
|
+
r.error("territories_required", path, `${c.domain} is a generic domain, so the authorization must list scope.territories (section 6.2)`);
|
|
127
|
+
}
|
|
128
|
+
else if (listed && !worldwide && !listed.includes(own)) {
|
|
129
|
+
r.warn("territory_differs_from_domain", `${base}/scope/territories`, `${c.domain} is a ${own} domain but the territories listed leave out ${own}`);
|
|
108
130
|
}
|
|
109
131
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
function
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
}
|
|
133
|
+
/** Section 3.1 catalog rules. Assumes the list passed the schema. */
|
|
134
|
+
function catalogRules(r, l) {
|
|
135
|
+
const catalog = l.catalog ?? [];
|
|
136
|
+
const ids = new Set();
|
|
137
|
+
const lines = new Set();
|
|
138
|
+
const identifiers = new Map();
|
|
139
|
+
catalog.forEach((p, i) => {
|
|
140
|
+
const base = `/catalog/${i}`;
|
|
141
|
+
if (ids.has(p.id))
|
|
142
|
+
r.error("duplicate_product_id", `${base}/id`, `duplicate product id ${p.id}`);
|
|
143
|
+
ids.add(p.id);
|
|
144
|
+
if (p.product_line)
|
|
145
|
+
lines.add(p.product_line);
|
|
146
|
+
const claim = (key, path, shown) => {
|
|
147
|
+
const other = identifiers.get(key);
|
|
148
|
+
if (other !== undefined && other !== p.id)
|
|
149
|
+
r.error("duplicate_identifier", path, `${shown} is on products ${other} and ${p.id}`);
|
|
150
|
+
identifiers.set(key, p.id);
|
|
151
|
+
};
|
|
152
|
+
if (p.gtin !== undefined) {
|
|
153
|
+
if (!isValidGtin(p.gtin))
|
|
154
|
+
r.error("invalid_gtin", `${base}/gtin`, `${p.gtin} is not a valid GTIN (bad length or check digit)`);
|
|
155
|
+
else
|
|
156
|
+
claim(`gtin:${toGtin14(p.gtin)}`, `${base}/gtin`, `GTIN ${p.gtin}`);
|
|
157
|
+
}
|
|
158
|
+
p.asins?.forEach((a, j) => claim(`asin:${a}`, `${base}/asins/${j}`, `ASIN ${a}`));
|
|
159
|
+
});
|
|
160
|
+
const hasCatalog = catalog.length > 0;
|
|
161
|
+
const checkLines = (names, path) => names?.forEach((n, k) => {
|
|
162
|
+
if (n === "all")
|
|
163
|
+
return;
|
|
164
|
+
if (hasCatalog && !lines.has(n))
|
|
165
|
+
r.error("unknown_product_line", `${path}/${k}`, `product line "${n}" is not in the catalog (section 3.1)`);
|
|
166
|
+
});
|
|
167
|
+
const checkExclusionLines = (names, path) => names?.forEach((n, k) => {
|
|
168
|
+
if (!lines.has(n))
|
|
169
|
+
r.error("unknown_product_line", `${path}/${k}`, `excluded product line "${n}" is not in the catalog (section 3.1)`);
|
|
170
|
+
});
|
|
171
|
+
const checkProducts = (list, path) => list?.forEach((id, k) => {
|
|
172
|
+
if (!ids.has(id))
|
|
173
|
+
r.error("unknown_product", `${path}/${k}`, `product ${id} is not in the catalog (section 3.1)`);
|
|
174
|
+
});
|
|
175
|
+
checkExclusionLines(l.exclusions?.product_lines, "/exclusions/product_lines");
|
|
176
|
+
checkProducts(l.exclusions?.products, "/exclusions/products");
|
|
177
|
+
l.authorizations.forEach((a, i) => {
|
|
178
|
+
const base = `/authorizations/${i}/scope`;
|
|
179
|
+
checkLines(a.scope.product_lines, `${base}/product_lines`);
|
|
180
|
+
checkExclusionLines(a.scope.exclude_product_lines, `${base}/exclude_product_lines`);
|
|
181
|
+
checkProducts(a.scope.products, `${base}/products`);
|
|
182
|
+
checkProducts(a.scope.exclude_products, `${base}/exclude_products`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** Section 3.1: each product has a gtin or asins. Named plainly, since the schema's anyOf error is vague. */
|
|
186
|
+
function productIdentifierRules(r, input) {
|
|
187
|
+
const catalog = isRecord(input) ? input.catalog : undefined;
|
|
188
|
+
if (!Array.isArray(catalog))
|
|
189
|
+
return;
|
|
190
|
+
catalog.forEach((p, i) => {
|
|
191
|
+
if (isRecord(p) && !("gtin" in p) && !("asins" in p)) {
|
|
192
|
+
r.error("product_without_identifier", `/catalog/${i}`, "every product needs a gtin or at least one ASIN (section 3.1)");
|
|
125
193
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
message: "valid_until MUST be no later than 24 hours after checked (section 9)",
|
|
194
|
+
if (isRecord(p) && Array.isArray(p.asins)) {
|
|
195
|
+
p.asins.forEach((a, j) => {
|
|
196
|
+
if (typeof a === "string" && !isValidAsin(a))
|
|
197
|
+
r.error("invalid_asin", `/catalog/${i}/asins/${j}`, `${a} is not an ASIN (10 uppercase letters or digits)`);
|
|
131
198
|
});
|
|
132
199
|
}
|
|
133
|
-
}
|
|
134
|
-
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Section 10: valid_until within 15 minutes of issue, stale_until within 24 hours and not before valid_until. */
|
|
203
|
+
function freshnessRules(r, issuedAt, issuedPath, doc) {
|
|
204
|
+
const issued = ts(r, issuedPath, issuedAt);
|
|
205
|
+
const validUntil = ts(r, "/valid_until", doc.valid_until);
|
|
206
|
+
const staleUntil = ts(r, "/stale_until", doc.stale_until);
|
|
207
|
+
if (issued === null || validUntil === null || staleUntil === null)
|
|
208
|
+
return;
|
|
209
|
+
if (validUntil <= issued)
|
|
210
|
+
r.error("valid_until_not_after_issued", "/valid_until", `valid_until must be after ${issuedPath.slice(1)}`);
|
|
211
|
+
else if (validUntil - issued > MAX_VALIDITY_MS)
|
|
212
|
+
r.error("valid_until_exceeds_max", "/valid_until", "valid_until MUST be no later than 15 minutes after issue (section 10.1)");
|
|
213
|
+
if (staleUntil < validUntil)
|
|
214
|
+
r.error("stale_until_before_valid_until", "/stale_until", "stale_until must not be before valid_until");
|
|
215
|
+
else if (staleUntil - issued > MAX_STALE_MS)
|
|
216
|
+
r.error("stale_until_exceeds_max", "/stale_until", "stale_until MUST be no later than 24 hours after issue (section 10.2)");
|
|
135
217
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
218
|
+
/** Walks the channels in an unvalidated list or request, applying channelRules and countryRule. */
|
|
219
|
+
function preSchemaChecks(r, input) {
|
|
220
|
+
if (!isRecord(input))
|
|
221
|
+
return;
|
|
222
|
+
if (isRecord(input.channel))
|
|
223
|
+
channelRules(r, "/channel", input.channel);
|
|
224
|
+
countryRule(r, "/territory", input.territory);
|
|
225
|
+
if (Array.isArray(input.authorizations)) {
|
|
226
|
+
input.authorizations.forEach((a, i) => {
|
|
227
|
+
if (!isRecord(a))
|
|
228
|
+
return;
|
|
229
|
+
if (Array.isArray(a.channels))
|
|
230
|
+
a.channels.forEach((c, j) => channelRules(r, `/authorizations/${i}/channels/${j}`, c));
|
|
231
|
+
if (isRecord(a.scope) && Array.isArray(a.scope.territories)) {
|
|
232
|
+
a.scope.territories.forEach((t, k) => countryRule(r, `/authorizations/${i}/scope/territories/${k}`, t));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
142
235
|
}
|
|
143
|
-
|
|
236
|
+
productIdentifierRules(r, input);
|
|
144
237
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
238
|
+
/** Keeps the specific errors and drops schema errors at or under the same paths, which only repeat them. */
|
|
239
|
+
function merge(r, schema) {
|
|
240
|
+
const specific = [...r.errors.map((e) => e.path), ...r.claimed];
|
|
241
|
+
for (const e of schema) {
|
|
242
|
+
if (!specific.some((p) => e.path === p || e.path.startsWith(`${p}/`) || p.startsWith(`${e.path}/`)))
|
|
243
|
+
r.errors.push(e);
|
|
244
|
+
}
|
|
150
245
|
}
|
|
151
|
-
|
|
152
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Validates a pointer file (section 5.1). Pass the domain it was fetched from (the domain first
|
|
248
|
+
* asked for, before any redirects) to check that the pointer vouches for it.
|
|
249
|
+
*/
|
|
250
|
+
export function validateFile(input, fetchedFrom) {
|
|
251
|
+
const r = new Report();
|
|
252
|
+
const form = isRecord(input) ? input.form : undefined;
|
|
253
|
+
if (form !== "pointer") {
|
|
254
|
+
r.error("form_not_supported", "/form", `form ${JSON.stringify(form)} is not supported; v0.2 has only "pointer" (section 5.1)`);
|
|
255
|
+
return r.done(input);
|
|
256
|
+
}
|
|
257
|
+
const list = input.list;
|
|
258
|
+
if (typeof list === "string" && /^https:\/\//.test(list) && !list.startsWith("https://authorizedretailers.ai/")) {
|
|
259
|
+
r.error("list_not_on_registry", "/list", "the list MUST be an https://authorizedretailers.ai/ URL; a pointer to any other host is no file (section 5.1)");
|
|
260
|
+
}
|
|
261
|
+
const brand = input.brand;
|
|
262
|
+
if (fetchedFrom !== undefined && typeof brand?.domain === "string" && normalizeDomain(brand.domain) !== normalizeDomain(fetchedFrom)) {
|
|
263
|
+
r.error("brand_domain_mismatch", "/brand/domain", `the pointer names ${brand.domain} but was fetched from ${fetchedFrom}; a pointer vouches only for its own domain (section 5.1)`);
|
|
264
|
+
}
|
|
265
|
+
merge(r, schemaErrors(pointer, input));
|
|
266
|
+
return r.done(input);
|
|
153
267
|
}
|
|
154
|
-
|
|
155
|
-
|
|
268
|
+
/** Validates a signed list or not-public response (section 5.2). Does not check the signature: see checkPublishedList. */
|
|
269
|
+
export function validateList(input) {
|
|
270
|
+
const r = new Report();
|
|
271
|
+
preSchemaChecks(r, input);
|
|
272
|
+
merge(r, schemaErrors(list, input));
|
|
273
|
+
if (r.errors.length === 0) {
|
|
274
|
+
const l = input;
|
|
275
|
+
freshnessRules(r, l.issued, "/issued", l);
|
|
276
|
+
if (l.public) {
|
|
277
|
+
const ids = new Set();
|
|
278
|
+
l.authorizations.forEach((a, i) => {
|
|
279
|
+
const base = `/authorizations/${i}`;
|
|
280
|
+
if (ids.has(a.id))
|
|
281
|
+
r.error("duplicate_authorization_id", `${base}/id`, `duplicate authorization id ${a.id}`);
|
|
282
|
+
ids.add(a.id);
|
|
283
|
+
if (a.expires !== undefined)
|
|
284
|
+
ts(r, `${base}/expires`, a.expires);
|
|
285
|
+
territoryRules(r, base, a.channels, a.scope);
|
|
286
|
+
});
|
|
287
|
+
catalogRules(r, l);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return r.done(input);
|
|
156
291
|
}
|
|
157
292
|
/** Validates a canonical verify request. Call normalizeVerifyRequest on raw agent input first. */
|
|
158
293
|
export function validateVerifyRequest(input) {
|
|
159
|
-
|
|
294
|
+
const r = new Report();
|
|
295
|
+
preSchemaChecks(r, input);
|
|
296
|
+
const product = isRecord(input) && isRecord(input.product) ? input.product : undefined;
|
|
297
|
+
if (typeof product?.gtin === "string" && /^(\d{8}|\d{12,14})$/.test(product.gtin) && !isValidGtin(product.gtin)) {
|
|
298
|
+
r.error("invalid_gtin", "/product/gtin", `${product.gtin} has a bad check digit`);
|
|
299
|
+
}
|
|
300
|
+
if (typeof product?.asin === "string" && !isValidAsin(product.asin)) {
|
|
301
|
+
r.error("invalid_asin", "/product/asin", `${product.asin} is not an ASIN (10 uppercase letters or digits)`);
|
|
302
|
+
}
|
|
303
|
+
merge(r, schemaErrors(verifyRequest, input));
|
|
304
|
+
if (r.errors.length === 0) {
|
|
305
|
+
const req = input;
|
|
306
|
+
if (req.territory === undefined && territoryForDomain(req.channel.domain) === null) {
|
|
307
|
+
r.error("territory_required", "/territory", `${req.channel.domain} is a generic domain, so the request must name a territory (section 6.2)`);
|
|
308
|
+
}
|
|
309
|
+
if (req.as_of !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(req.as_of))
|
|
310
|
+
ts(r, "/as_of", req.as_of);
|
|
311
|
+
}
|
|
312
|
+
return r.done(input);
|
|
160
313
|
}
|
|
161
314
|
export function validateVerifyResponse(input) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
315
|
+
const r = new Report();
|
|
316
|
+
merge(r, schemaErrors(verifyResponse, input));
|
|
317
|
+
if (r.errors.length === 0) {
|
|
318
|
+
const res = input;
|
|
319
|
+
freshnessRules(r, res.checked, "/checked", res);
|
|
320
|
+
if ("expires" in res && res.expires !== undefined)
|
|
321
|
+
ts(r, "/expires", res.expires);
|
|
322
|
+
if (res.as_of !== undefined)
|
|
323
|
+
ts(r, "/as_of", res.as_of);
|
|
324
|
+
for (const [i, s] of res.signals.entries())
|
|
325
|
+
ts(r, `/signals/${i}/observed`, s.observed);
|
|
326
|
+
if (res.observed)
|
|
327
|
+
ts(r, "/observed/last_seen", res.observed.last_seen);
|
|
328
|
+
}
|
|
329
|
+
return r.done(input);
|
|
330
|
+
}
|
|
331
|
+
/** Validates a revocation feed (section 10.3). Does not check the signature: see checkRevocationFeed. */
|
|
332
|
+
export function validateRevocationFeed(input) {
|
|
333
|
+
const r = new Report();
|
|
334
|
+
merge(r, schemaErrors(revocationFeed, input));
|
|
335
|
+
if (r.errors.length === 0) {
|
|
336
|
+
const f = input;
|
|
337
|
+
freshnessRules(r, f.issued, "/issued", f);
|
|
338
|
+
f.entries.forEach((e, i) => ts(r, `/entries/${i}/revoked`, e.revoked));
|
|
339
|
+
}
|
|
340
|
+
return r.done(input);
|
|
166
341
|
}
|
|
167
|
-
/** Validates one authorization scope (
|
|
342
|
+
/** Validates one authorization scope (section 3), e.g. before the registry stores it. */
|
|
168
343
|
export function validateScope(input) {
|
|
169
|
-
|
|
344
|
+
const r = new Report();
|
|
345
|
+
if (isRecord(input) && Array.isArray(input.territories))
|
|
346
|
+
input.territories.forEach((t, k) => countryRule(r, `/territories/${k}`, t));
|
|
347
|
+
merge(r, schemaErrors(scopeSchema, input));
|
|
348
|
+
return r.done(input);
|
|
170
349
|
}
|
|
171
|
-
/** Validates one channel identifier (
|
|
350
|
+
/** Validates one channel identifier (section 6), e.g. before the registry stores it. */
|
|
172
351
|
export function validateChannel(input) {
|
|
173
|
-
|
|
352
|
+
const r = new Report();
|
|
353
|
+
channelRules(r, "", input);
|
|
354
|
+
merge(r, schemaErrors(channelSchema, input));
|
|
355
|
+
return r.done(input);
|
|
356
|
+
}
|
|
357
|
+
/** Validates one catalog product (section 3.1), e.g. a row from a brand's CSV import. */
|
|
358
|
+
export function validateProduct(input) {
|
|
359
|
+
const r = new Report();
|
|
360
|
+
productIdentifierRules(r, { catalog: [input] });
|
|
361
|
+
r.errors.forEach((e) => (e.path = e.path.replace(/^\/catalog\/0/, "")));
|
|
362
|
+
if (isRecord(input) && typeof input.gtin === "string" && /^(\d{8}|\d{12,14})$/.test(input.gtin) && !isValidGtin(input.gtin)) {
|
|
363
|
+
r.error("invalid_gtin", "/gtin", `${input.gtin} has a bad check digit`);
|
|
364
|
+
}
|
|
365
|
+
merge(r, schemaErrors(productSchema, input));
|
|
366
|
+
return r.done(input);
|
|
174
367
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "authorized-retailers/0.2",
|
|
3
|
+
"brand": {
|
|
4
|
+
"name": "Example Brand",
|
|
5
|
+
"domains": [
|
|
6
|
+
"brand.example"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"public": true,
|
|
10
|
+
"issued": "2026-09-28T14:00:00Z",
|
|
11
|
+
"valid_until": "2026-09-28T14:15:00Z",
|
|
12
|
+
"stale_until": "2026-09-29T14:00:00Z",
|
|
13
|
+
"authorizations": [
|
|
14
|
+
{
|
|
15
|
+
"id": "auth_01J9X2",
|
|
16
|
+
"revocation_id": "rv_Q2l8dX4bT0aN5kPw",
|
|
17
|
+
"retailer": {
|
|
18
|
+
"name": "Example Retail LLC",
|
|
19
|
+
"entity_id": "ent_7Q4M"
|
|
20
|
+
},
|
|
21
|
+
"channels": [
|
|
22
|
+
{
|
|
23
|
+
"type": "amazon",
|
|
24
|
+
"domain": "amazon.com",
|
|
25
|
+
"seller_id": "A1B2C3D4E5F6G7"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "walmart",
|
|
29
|
+
"domain": "walmart.com",
|
|
30
|
+
"seller_id": "101234567"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "web",
|
|
34
|
+
"domain": "retailer.example"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"scope": {
|
|
38
|
+
"territories": [
|
|
39
|
+
"US"
|
|
40
|
+
],
|
|
41
|
+
"product_lines": [
|
|
42
|
+
"all",
|
|
43
|
+
"Running"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"proposed_by": null
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"signature": {
|
|
50
|
+
"kid": "ar-20260928-6e37",
|
|
51
|
+
"jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFyLTIwMjYwOTI4LTZlMzcifQ..c2lnbmF0dXJlLXBsYWNlaG9sZGVy"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "authorized-retailers/0.2",
|
|
3
|
+
"brand": {
|
|
4
|
+
"name": "Example Brand",
|
|
5
|
+
"domains": [
|
|
6
|
+
"brand.example"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"public": true,
|
|
10
|
+
"issued": "2026-09-28T14:00:00Z",
|
|
11
|
+
"valid_until": "2026-09-28T14:15:00Z",
|
|
12
|
+
"stale_until": "2026-09-29T14:00:00Z",
|
|
13
|
+
"authorizations": [
|
|
14
|
+
{
|
|
15
|
+
"id": "auth_01J9X2",
|
|
16
|
+
"revocation_id": "rv_Q2l8dX4bT0aN5kPw",
|
|
17
|
+
"retailer": {
|
|
18
|
+
"name": "Example Retail LLC",
|
|
19
|
+
"entity_id": "ent_7Q4M"
|
|
20
|
+
},
|
|
21
|
+
"channels": [
|
|
22
|
+
{
|
|
23
|
+
"type": "amazon",
|
|
24
|
+
"domain": "amazon.com"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "walmart",
|
|
28
|
+
"domain": "walmart.com",
|
|
29
|
+
"seller_id": "101234567"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "web",
|
|
33
|
+
"domain": "retailer.example"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"scope": {
|
|
37
|
+
"territories": [
|
|
38
|
+
"US"
|
|
39
|
+
],
|
|
40
|
+
"product_lines": [
|
|
41
|
+
"all"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"proposed_by": null
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"signature": {
|
|
48
|
+
"kid": "ar-20260928-6e37",
|
|
49
|
+
"jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFyLTIwMjYwOTI4LTZlMzcifQ..c2lnbmF0dXJlLXBsYWNlaG9sZGVy"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "authorized-retailers/0.2",
|
|
3
|
+
"brand": {
|
|
4
|
+
"name": "Example Brand",
|
|
5
|
+
"domains": [
|
|
6
|
+
"brand.example"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"public": true,
|
|
10
|
+
"issued": "2026-09-28T14:00:00Z",
|
|
11
|
+
"valid_until": "2026-09-28T14:15:00Z",
|
|
12
|
+
"stale_until": "2026-09-29T14:00:00Z",
|
|
13
|
+
"authorizations": [
|
|
14
|
+
{
|
|
15
|
+
"id": "auth_01J9X2",
|
|
16
|
+
"revocation_id": "rv_Q2l8dX4bT0aN5kPw",
|
|
17
|
+
"retailer": {
|
|
18
|
+
"name": "Example Retail LLC",
|
|
19
|
+
"entity_id": "ent_7Q4M"
|
|
20
|
+
},
|
|
21
|
+
"channels": [
|
|
22
|
+
{
|
|
23
|
+
"type": "amazon",
|
|
24
|
+
"domain": "walmart.com",
|
|
25
|
+
"seller_id": "A1B2C3D4E5F6G7"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "walmart",
|
|
29
|
+
"domain": "walmart.com",
|
|
30
|
+
"seller_id": "101234567"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "web",
|
|
34
|
+
"domain": "retailer.example"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"scope": {
|
|
38
|
+
"territories": [
|
|
39
|
+
"US"
|
|
40
|
+
],
|
|
41
|
+
"product_lines": [
|
|
42
|
+
"all"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"proposed_by": null
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"signature": {
|
|
49
|
+
"kid": "ar-20260928-6e37",
|
|
50
|
+
"jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFyLTIwMjYwOTI4LTZlMzcifQ..c2lnbmF0dXJlLXBsYWNlaG9sZGVy"
|
|
51
|
+
}
|
|
52
|
+
}
|