@adcp/sdk 14.0.0 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/lib/adapters/implicit-account-store.d.mts +12 -7
  2. package/dist/lib/adapters/implicit-account-store.d.ts +12 -7
  3. package/dist/lib/adapters/implicit-account-store.js +69 -15
  4. package/dist/lib/adapters/implicit-account-store.mjs +69 -15
  5. package/dist/lib/core/AgentClient.d.mts +1 -0
  6. package/dist/lib/core/AgentClient.d.ts +1 -0
  7. package/dist/lib/core/AgentClient.js +3 -0
  8. package/dist/lib/core/AgentClient.mjs +3 -0
  9. package/dist/lib/core/SingleAgentClient.d.mts +31 -1
  10. package/dist/lib/core/SingleAgentClient.d.ts +31 -1
  11. package/dist/lib/core/SingleAgentClient.js +355 -35
  12. package/dist/lib/core/SingleAgentClient.mjs +365 -37
  13. package/dist/lib/core/TaskExecutor.js +2 -1
  14. package/dist/lib/core/TaskExecutor.mjs +2 -1
  15. package/dist/lib/core/account-key.d.mts +3 -0
  16. package/dist/lib/core/account-key.d.ts +3 -0
  17. package/dist/lib/core/account-key.js +41 -0
  18. package/dist/lib/core/account-key.mjs +17 -0
  19. package/dist/lib/core/account-resolution.d.mts +2 -0
  20. package/dist/lib/core/account-resolution.d.ts +2 -0
  21. package/dist/lib/core/buyer-account-registry.d.mts +65 -0
  22. package/dist/lib/core/buyer-account-registry.d.ts +65 -0
  23. package/dist/lib/core/buyer-account-registry.js +518 -0
  24. package/dist/lib/core/buyer-account-registry.mjs +494 -0
  25. package/dist/lib/core/product-cache.d.mts +18 -0
  26. package/dist/lib/core/product-cache.d.ts +18 -0
  27. package/dist/lib/core/product-cache.js +137 -0
  28. package/dist/lib/core/product-cache.mjs +112 -0
  29. package/dist/lib/errors/index.d.mts +40 -1
  30. package/dist/lib/errors/index.d.ts +40 -1
  31. package/dist/lib/errors/index.js +69 -3
  32. package/dist/lib/errors/index.mjs +64 -3
  33. package/dist/lib/governance/authorization.d.mts +17 -1
  34. package/dist/lib/governance/authorization.d.ts +17 -1
  35. package/dist/lib/governance/authorization.js +55 -7
  36. package/dist/lib/governance/authorization.mjs +59 -7
  37. package/dist/lib/governance/index.d.mts +2 -2
  38. package/dist/lib/governance/index.d.ts +2 -2
  39. package/dist/lib/governance/index.js +2 -0
  40. package/dist/lib/governance/index.mjs +3 -1
  41. package/dist/lib/index.d.mts +5 -3
  42. package/dist/lib/index.d.ts +5 -3
  43. package/dist/lib/index.js +20 -0
  44. package/dist/lib/index.mjs +19 -0
  45. package/dist/lib/protocols/a2a.js +9 -1
  46. package/dist/lib/protocols/a2a.mjs +9 -1
  47. package/dist/lib/protocols/index.js +9 -2
  48. package/dist/lib/protocols/index.mjs +9 -2
  49. package/dist/lib/protocols/mcp-modern.js +2 -1
  50. package/dist/lib/protocols/mcp-modern.mjs +2 -1
  51. package/dist/lib/protocols/mcp.js +5 -2
  52. package/dist/lib/protocols/mcp.mjs +5 -2
  53. package/dist/lib/protocols/rawResponseCapture.d.mts +6 -0
  54. package/dist/lib/protocols/rawResponseCapture.d.ts +6 -0
  55. package/dist/lib/protocols/rawResponseCapture.js +41 -29
  56. package/dist/lib/protocols/rawResponseCapture.mjs +40 -29
  57. package/dist/lib/protocols/signedRequestRejection.d.mts +9 -0
  58. package/dist/lib/protocols/signedRequestRejection.d.ts +9 -0
  59. package/dist/lib/protocols/signedRequestRejection.js +209 -0
  60. package/dist/lib/protocols/signedRequestRejection.mjs +189 -0
  61. package/dist/lib/protocols/transportDiagnostics.d.mts +1 -0
  62. package/dist/lib/protocols/transportDiagnostics.d.ts +1 -0
  63. package/dist/lib/protocols/transportDiagnostics.js +2 -0
  64. package/dist/lib/protocols/transportDiagnostics.mjs +1 -0
  65. package/dist/lib/registry/types.generated.d.mts +112 -45
  66. package/dist/lib/registry/types.generated.d.ts +112 -45
  67. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  68. package/dist/lib/server/account-provisioning.d.mts +2 -0
  69. package/dist/lib/server/account-provisioning.d.ts +2 -0
  70. package/dist/lib/server/account-provisioning.js +30 -0
  71. package/dist/lib/server/account-provisioning.mjs +6 -0
  72. package/dist/lib/server/account-reference-warnings.d.mts +12 -0
  73. package/dist/lib/server/account-reference-warnings.d.ts +12 -0
  74. package/dist/lib/server/account-reference-warnings.js +48 -0
  75. package/dist/lib/server/account-reference-warnings.mjs +23 -0
  76. package/dist/lib/server/auth-signature.js +1 -0
  77. package/dist/lib/server/auth-signature.mjs +1 -0
  78. package/dist/lib/server/create-adcp-server.d.mts +34 -0
  79. package/dist/lib/server/create-adcp-server.d.ts +34 -0
  80. package/dist/lib/server/create-adcp-server.js +225 -14
  81. package/dist/lib/server/create-adcp-server.mjs +225 -14
  82. package/dist/lib/server/decisioning/account.d.mts +2 -0
  83. package/dist/lib/server/decisioning/account.d.ts +2 -0
  84. package/dist/lib/server/decisioning/runtime/from-platform.js +49 -10
  85. package/dist/lib/server/decisioning/runtime/from-platform.mjs +49 -10
  86. package/dist/lib/server/index.d.mts +2 -2
  87. package/dist/lib/server/index.d.ts +2 -2
  88. package/dist/lib/server/index.js +2 -0
  89. package/dist/lib/server/index.mjs +3 -1
  90. package/dist/lib/signing/agent-resolver/consistency.d.mts +6 -13
  91. package/dist/lib/signing/agent-resolver/consistency.d.ts +6 -13
  92. package/dist/lib/signing/agent-resolver/consistency.js +0 -1
  93. package/dist/lib/signing/agent-resolver/consistency.mjs +0 -1
  94. package/dist/lib/signing/agent-resolver/errors.d.mts +1 -1
  95. package/dist/lib/signing/agent-resolver/errors.d.ts +1 -1
  96. package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +2 -0
  97. package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +2 -0
  98. package/dist/lib/signing/agent-resolver/fetch-helpers.js +2 -1
  99. package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +2 -1
  100. package/dist/lib/signing/agent-resolver/jwks-set.js +24 -3
  101. package/dist/lib/signing/agent-resolver/jwks-set.mjs +24 -3
  102. package/dist/lib/signing/agent-resolver/legacy-brand.d.mts +15 -0
  103. package/dist/lib/signing/agent-resolver/legacy-brand.d.ts +15 -0
  104. package/dist/lib/signing/agent-resolver/legacy-brand.js +60 -0
  105. package/dist/lib/signing/agent-resolver/legacy-brand.mjs +36 -0
  106. package/dist/lib/signing/agent-resolver/operator-authorization.d.mts +13 -0
  107. package/dist/lib/signing/agent-resolver/operator-authorization.d.ts +13 -0
  108. package/dist/lib/signing/agent-resolver/operator-authorization.js +108 -0
  109. package/dist/lib/signing/agent-resolver/operator-authorization.mjs +84 -0
  110. package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +14 -4
  111. package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +14 -4
  112. package/dist/lib/signing/agent-resolver/resolve-agent.js +101 -132
  113. package/dist/lib/signing/agent-resolver/resolve-agent.mjs +102 -133
  114. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +7 -1
  115. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +7 -1
  116. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +50 -17
  117. package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +50 -17
  118. package/dist/lib/signing/agent-resolver/select-agent.d.mts +14 -15
  119. package/dist/lib/signing/agent-resolver/select-agent.d.ts +14 -15
  120. package/dist/lib/signing/agent-resolver/select-agent.js +98 -13
  121. package/dist/lib/signing/agent-resolver/select-agent.mjs +95 -13
  122. package/dist/lib/signing/brand-jwks.d.mts +27 -75
  123. package/dist/lib/signing/brand-jwks.d.ts +27 -75
  124. package/dist/lib/signing/brand-jwks.js +112 -182
  125. package/dist/lib/signing/brand-jwks.mjs +112 -182
  126. package/dist/lib/signing/errors.d.mts +3 -1
  127. package/dist/lib/signing/errors.d.ts +3 -1
  128. package/dist/lib/signing/errors.js +4 -1
  129. package/dist/lib/signing/errors.mjs +4 -1
  130. package/dist/lib/signing/jwks-https.d.mts +7 -0
  131. package/dist/lib/signing/jwks-https.d.ts +7 -0
  132. package/dist/lib/signing/jwks-https.js +31 -8
  133. package/dist/lib/signing/jwks-https.mjs +31 -8
  134. package/dist/lib/signing/jwks.d.mts +8 -0
  135. package/dist/lib/signing/jwks.d.ts +8 -0
  136. package/dist/lib/signing/middleware.js +2 -1
  137. package/dist/lib/signing/middleware.mjs +2 -1
  138. package/dist/lib/signing/publisher-pins.d.mts +11 -0
  139. package/dist/lib/signing/publisher-pins.d.ts +11 -0
  140. package/dist/lib/signing/publisher-pins.js +125 -0
  141. package/dist/lib/signing/publisher-pins.mjs +101 -0
  142. package/dist/lib/signing/server.d.mts +1 -0
  143. package/dist/lib/signing/server.d.ts +1 -0
  144. package/dist/lib/signing/types.d.mts +5 -0
  145. package/dist/lib/signing/types.d.ts +5 -0
  146. package/dist/lib/signing/verifier.js +49 -4
  147. package/dist/lib/signing/verifier.mjs +49 -4
  148. package/dist/lib/signing/webhook-verifier.d.mts +7 -2
  149. package/dist/lib/signing/webhook-verifier.d.ts +7 -2
  150. package/dist/lib/signing/webhook-verifier.js +42 -2
  151. package/dist/lib/signing/webhook-verifier.mjs +43 -3
  152. package/dist/lib/testing/storyboard/account-policy.d.mts +2 -0
  153. package/dist/lib/testing/storyboard/account-policy.d.ts +2 -0
  154. package/dist/lib/testing/storyboard/account-policy.js +35 -0
  155. package/dist/lib/testing/storyboard/account-policy.mjs +11 -0
  156. package/dist/lib/testing/storyboard/context.js +6 -0
  157. package/dist/lib/testing/storyboard/context.mjs +6 -0
  158. package/dist/lib/testing/storyboard/request-builder.js +12 -2
  159. package/dist/lib/testing/storyboard/request-builder.mjs +12 -2
  160. package/dist/lib/testing/storyboard/runner.js +3 -2
  161. package/dist/lib/testing/storyboard/runner.mjs +3 -2
  162. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  163. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  164. package/dist/lib/version.d.mts +3 -3
  165. package/dist/lib/version.d.ts +3 -3
  166. package/dist/lib/version.js +3 -3
  167. package/dist/lib/version.mjs +3 -3
  168. package/dist/lib/wholesale-feed-sync/sync.d.mts +1 -0
  169. package/dist/lib/wholesale-feed-sync/sync.d.ts +1 -0
  170. package/dist/lib/wholesale-feed-sync/sync.js +92 -21
  171. package/dist/lib/wholesale-feed-sync/sync.mjs +92 -21
  172. package/dist/lib/wholesale-feed-sync/types.d.mts +4 -3
  173. package/dist/lib/wholesale-feed-sync/types.d.ts +4 -3
  174. package/docs/TYPE-SUMMARY.md +2 -2
  175. package/docs/guides/BUILD-AN-AGENT.md +2 -2
  176. package/docs/guides/BUYER-QUICKSTART-3.2.md +2 -0
  177. package/docs/guides/FIRST-CALL-TO-A-SELLER.md +104 -0
  178. package/docs/guides/SIGNING-GUIDE.md +16 -7
  179. package/docs/guides/account-resolution.md +86 -0
  180. package/docs/llms.txt +3 -2
  181. package/docs/migration-14.x-rc-worksheet.md +4 -4
  182. package/docs/migration-4.x-to-5.x.md +1 -0
  183. package/docs/migration-agent-resolution-3.3.md +123 -0
  184. package/docs/recipes/verifying-inbound-webhooks.md +56 -15
  185. package/package.json +2 -2
@@ -1,5 +1,7 @@
1
1
  import { ssrfSafeFetch } from "../net/index.mjs";
2
- import { HttpsJwksResolver } from "./jwks-https.mjs";
2
+ import { ResolvedAgentJwksResolver } from "./agent-resolver/resolved-agent-jwks.mjs";
3
+ import { parseStrictJson } from "./agent-resolver/strict-json.mjs";
4
+ import { canonicalAgentUrl } from "./agent-resolver/select-agent.mjs";
3
5
  class BrandJsonResolverError extends Error {
4
6
  code;
5
7
  cause;
@@ -18,115 +20,110 @@ const DEFAULT_MAX_AGE_SECONDS = 3600;
18
20
  const DEFAULT_MAX_REDIRECTS = 3;
19
21
  const BARE_HOSTNAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
20
22
  class BrandJsonJwksResolver {
21
- url;
22
- selector;
23
- minCooldown;
24
- maxAge;
25
- maxRedirects;
26
- allowPrivateIp;
27
- lookup;
28
- jwksOptions;
29
- now;
30
- snapshot;
31
- inner;
32
- inFlight;
33
23
  constructor(brandJsonUrl, options) {
34
- this.url = brandJsonUrl;
35
- this.selector = {
24
+ this.brandJsonUrl = brandJsonUrl;
25
+ this.options = options;
26
+ for (const lifetime of [options.maxAgeSeconds, options.jwksOptions?.maxAgeSeconds]) {
27
+ if (lifetime !== void 0 && (!Number.isFinite(lifetime) || lifetime <= 0))
28
+ throw new TypeError("maxAgeSeconds must be a finite positive number.");
29
+ }
30
+ for (const cooldown of [options.minCooldownSeconds, options.jwksOptions?.minCooldownSeconds]) {
31
+ if (cooldown !== void 0 && (!Number.isFinite(cooldown) || cooldown < 0))
32
+ throw new TypeError("minCooldownSeconds must be a finite non-negative number.");
33
+ }
34
+ if (options.agentUrl !== void 0) this.resolver = this.createResolver(options.agentUrl, brandJsonUrl);
35
+ }
36
+ brandJsonUrl;
37
+ options;
38
+ resolver;
39
+ onboarding;
40
+ lastOnboardingAttempt = Number.NEGATIVE_INFINITY;
41
+ createResolver(agentUrl, operatorUrl) {
42
+ const options = this.options;
43
+ return new ResolvedAgentJwksResolver(agentUrl, options.protocol ?? "mcp", {
36
44
  agentType: options.agentType,
37
- ...options.agentId !== void 0 && { agentId: options.agentId },
38
- ...options.brandId !== void 0 && { brandId: options.brandId }
39
- };
40
- this.minCooldown = options.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS;
41
- this.maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
42
- this.maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
43
- this.allowPrivateIp = options.allowPrivateIp ?? false;
44
- this.lookup = options.lookup;
45
- this.jwksOptions = options.jwksOptions ?? {};
46
- this.now = options.now ?? (() => Math.floor(Date.now() / 1e3));
45
+ agentId: options.agentId,
46
+ expectedBrandJsonUrl: operatorUrl,
47
+ fetchCapabilities: options.fetchCapabilities,
48
+ legacyWebhookFallback: options.legacyWebhookFallback ?? true,
49
+ allowPrivateIp: options.allowPrivateIp,
50
+ lookup: options.lookup,
51
+ now: options.now,
52
+ cacheTtlSeconds: Math.min(
53
+ options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS,
54
+ options.jwksOptions?.maxAgeSeconds ?? 1800
55
+ ),
56
+ unknownKidCooldownSeconds: options.minCooldownSeconds ?? options.jwksOptions?.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS
57
+ });
47
58
  }
48
- /**
49
- * Resolve a JWK by `kid`. On a cold cache, fetches brand.json first; on an
50
- * expired brand.json snapshot, refreshes respecting the cooldown. Unknown
51
- * kids cascade: first ask the inner HttpsJwksResolver (which will refetch
52
- * its own URL if cooldown has elapsed); if still unknown, refresh
53
- * brand.json in case `jwks_uri` rotated.
54
- */
55
- async resolve(keyid) {
56
- if (!this.snapshot || !this.inner) {
57
- await this.refresh();
58
- } else if (this.now() > this.snapshot.expiresAt && this.now() - this.snapshot.fetchedAt >= this.minCooldown) {
59
- await this.refresh().catch(() => {
60
- });
61
- }
62
- if (!this.inner) return null;
63
- const hit = await this.inner.resolve(keyid);
64
- if (hit) return hit;
65
- if (this.snapshot && this.now() - this.snapshot.fetchedAt >= this.minCooldown) {
66
- await this.refresh().catch(() => {
59
+ async confirmedResolver() {
60
+ if (this.resolver) return this.resolver;
61
+ if (this.onboarding) return this.onboarding;
62
+ const now = this.options.now?.() ?? Date.now() / 1e3;
63
+ if (now - this.lastOnboardingAttempt < 30)
64
+ throw new BrandJsonResolverError("fetch_failed", "Operator onboarding discovery is in its retry cooldown.");
65
+ this.lastOnboardingAttempt = now;
66
+ this.onboarding = (async () => {
67
+ const record = await fetchBrandJson({
68
+ startUrl: this.brandJsonUrl,
69
+ maxRedirects: this.options.maxRedirects,
70
+ allowPrivateIp: this.options.allowPrivateIp,
71
+ lookup: this.options.lookup
67
72
  });
68
- return this.inner ? this.inner.resolve(keyid) : null;
69
- }
70
- return null;
73
+ const agentUrl = inferOnboardingAgentUrl(record.data, this.options);
74
+ const resolver = this.createResolver(agentUrl, record.finalUrl);
75
+ await resolver.forceRefresh();
76
+ this.resolver = resolver;
77
+ return resolver;
78
+ })().finally(() => {
79
+ this.onboarding = void 0;
80
+ });
81
+ return this.onboarding;
82
+ }
83
+ async resolve(keyid) {
84
+ return (await this.confirmedResolver()).resolve(keyid);
85
+ }
86
+ async resolveWithMetadata(keyid) {
87
+ return (await this.confirmedResolver()).resolveWithMetadata(keyid);
71
88
  }
72
- /**
73
- * The agent URL we resolved `jwks_uri` from. Populated after the first
74
- * successful refresh; useful for verifier result attribution
75
- * (`VerifyWebhookOptions.agentUrlForKeyid`).
76
- */
77
89
  get agentUrl() {
78
- return this.snapshot?.agentUrl;
90
+ return this.resolver?.resolvedAgentUrl;
79
91
  }
80
- /** Force a refetch of both brand.json and the inner JWKS, bypassing the cooldown. */
81
92
  async forceRefresh() {
82
- this.snapshot = void 0;
83
- this.inner = void 0;
84
- await this.refresh();
85
- }
86
- async refresh() {
87
- if (this.inFlight) {
88
- await this.inFlight;
89
- return;
90
- }
91
- this.inFlight = this.doRefresh().finally(() => {
92
- this.inFlight = void 0;
93
- });
94
- await this.inFlight;
93
+ if (this.resolver) await this.resolver.forceRefresh();
94
+ else await this.confirmedResolver();
95
95
  }
96
- async doRefresh() {
97
- const fetched = await fetchBrandJson({
98
- startUrl: this.url,
99
- currentEtag: this.snapshot?.etag,
100
- maxRedirects: this.maxRedirects,
101
- allowPrivateIp: this.allowPrivateIp,
102
- lookup: this.lookup
103
- });
104
- if (fetched.status === "not_modified" && this.snapshot) {
105
- this.snapshot = {
106
- ...this.snapshot,
107
- ...fetched.etag && { etag: fetched.etag },
108
- fetchedAt: this.now(),
109
- expiresAt: this.now() + computeLifetime(fetched.cacheControl, this.maxAge)
110
- };
111
- return;
96
+ }
97
+ function inferOnboardingAgentUrl(document, selector) {
98
+ const record = document;
99
+ const candidates = (agents) => !Array.isArray(agents) ? [] : agents.filter(
100
+ (entry) => entry?.type === selector.agentType && (selector.agentId === void 0 || entry.id === selector.agentId) && typeof entry.url === "string"
101
+ ).flatMap((entry) => {
102
+ try {
103
+ return [canonicalAgentUrl(entry.url)];
104
+ } catch {
105
+ return [];
112
106
  }
113
- const agent = selectAgent(fetched.data, fetched.finalUrl, this.selector);
114
- if (!this.inner || this.snapshot?.jwksUri !== agent.jwksUri) {
115
- this.inner = new HttpsJwksResolver(agent.jwksUri, {
116
- ...this.jwksOptions,
117
- allowPrivateIp: this.allowPrivateIp,
118
- lookup: this.lookup,
119
- now: this.now
120
- });
107
+ });
108
+ let urls;
109
+ if (isPortfolioHouse(record.house)) {
110
+ const house = record.house;
111
+ urls = [];
112
+ if (selector.brandId !== void 0 && Array.isArray(record.brands)) {
113
+ const brands = record.brands.filter((brand) => brand?.id === selector.brandId);
114
+ if (brands.length > 1)
115
+ throw new BrandJsonResolverError("agent_ambiguous", "Onboarding brand selector is ambiguous.");
116
+ urls = candidates(brands[0]?.agents);
121
117
  }
122
- this.snapshot = {
123
- jwksUri: agent.jwksUri,
124
- agentUrl: agent.url,
125
- ...fetched.etag && { etag: fetched.etag },
126
- fetchedAt: this.now(),
127
- expiresAt: this.now() + computeLifetime(fetched.cacheControl, this.maxAge)
128
- };
129
- }
118
+ if (urls.length === 0) urls = candidates(house.agents);
119
+ } else urls = candidates(record.agents);
120
+ const unique = [...new Set(urls)];
121
+ if (unique.length !== 1)
122
+ throw new BrandJsonResolverError(
123
+ unique.length === 0 ? "agent_not_found" : "agent_ambiguous",
124
+ "Onboarding must identify exactly one agent URL; supply agentUrl explicitly."
125
+ );
126
+ return unique[0];
130
127
  }
131
128
  const MAX_BRAND_JSON_TIMEOUT_MS = 1e4;
132
129
  const MAX_BRAND_JSON_BODY_BYTES = 262144;
@@ -147,6 +144,8 @@ async function fetchBrandJson(args) {
147
144
  const allowPrivateIp = args.allowPrivateIp === true;
148
145
  const seen = /* @__PURE__ */ new Set();
149
146
  let url = canonicalizeUrl(args.startUrl, allowPrivateIp);
147
+ const initial = new URL(url);
148
+ let httpRedirects = 0;
150
149
  for (let hop = 0; hop <= maxRedirects; hop++) {
151
150
  if (seen.has(url)) {
152
151
  throw new BrandJsonResolverError("redirect_loop", `brand.json redirect loop detected`);
@@ -169,6 +168,19 @@ async function fetchBrandJson(args) {
169
168
  } catch (cause) {
170
169
  throw new BrandJsonResolverError("fetch_failed", "Unable to fetch brand.json", { cause });
171
170
  }
171
+ if (args.domainDerived && hop === 0 && [301, 302, 303, 307, 308].includes(res.status)) {
172
+ const location = res.headers.location;
173
+ if (!location || httpRedirects++ >= 3)
174
+ throw new BrandJsonResolverError("fetch_failed", "Invalid domain-derived redirect");
175
+ const target = new URL(location, url);
176
+ const baseHost = initial.hostname.startsWith("www.") ? initial.hostname.slice(4) : initial.hostname;
177
+ if (target.hostname !== baseHost && target.hostname !== `www.${baseHost}` || target.port !== initial.port || target.protocol !== initial.protocol || target.username || target.password) {
178
+ throw new BrandJsonResolverError("invalid_url", "brand.json redirect leaves its original host/www boundary");
179
+ }
180
+ url = canonicalizeUrl(target.href, allowPrivateIp);
181
+ hop--;
182
+ continue;
183
+ }
172
184
  if (hop === 0 && res.status === 304) {
173
185
  return {
174
186
  status: "not_modified",
@@ -186,7 +198,7 @@ async function fetchBrandJson(args) {
186
198
  const text = Buffer.from(res.body).toString("utf8");
187
199
  let parsed;
188
200
  try {
189
- parsed = JSON.parse(text);
201
+ parsed = parseStrictJson(text);
190
202
  } catch {
191
203
  throw new BrandJsonResolverError("invalid_body", `brand.json response is not valid JSON`);
192
204
  }
@@ -278,88 +290,6 @@ function assertBrandJsonShape(obj) {
278
290
  }
279
291
  }
280
292
  }
281
- function selectAgent(data, finalBrandUrl, selector) {
282
- if (!data || typeof data !== "object") {
283
- throw new BrandJsonResolverError(
284
- "agent_not_found",
285
- `brand.json has no agent matching ${describeSelector(selector)}`
286
- );
287
- }
288
- const obj = data;
289
- let picked;
290
- if (isPortfolioHouse(obj.house)) {
291
- const house = obj.house;
292
- if (selector.brandId !== void 0) {
293
- const brands = Array.isArray(obj.brands) ? obj.brands : [];
294
- const brand = brands.find((b) => b && b.id === selector.brandId);
295
- if (brand) picked = pickAgent(brand.agents, finalBrandUrl, selector);
296
- }
297
- picked ??= pickAgent(house.agents, finalBrandUrl, selector);
298
- } else {
299
- picked = pickAgent(obj.agents, finalBrandUrl, selector);
300
- }
301
- if (!picked) {
302
- throw new BrandJsonResolverError(
303
- "agent_not_found",
304
- `brand.json has no agent matching ${describeSelector(selector)}`
305
- );
306
- }
307
- return picked;
308
- }
309
- function pickAgent(agents, finalBrandUrl, selector) {
310
- if (!Array.isArray(agents)) return void 0;
311
- const matches = agents.filter((a) => {
312
- if (!a || typeof a !== "object") return false;
313
- const e = a;
314
- if (e.type !== selector.agentType) return false;
315
- if (selector.agentId !== void 0 && e.id !== selector.agentId) return false;
316
- return typeof e.url === "string";
317
- });
318
- if (matches.length === 0) return void 0;
319
- if (matches.length > 1 && selector.agentId === void 0) {
320
- throw new BrandJsonResolverError(
321
- "agent_ambiguous",
322
- `brand.json declares ${matches.length} agents of type "${selector.agentType}"; pass \`agentId\` to disambiguate (choices: ${matches.map((m) => m.id ?? "<no-id>").join(", ")})`
323
- );
324
- }
325
- const agent = matches[0];
326
- const url = agent.url;
327
- const jwksUri = agent.jwks_uri ?? defaultJwksUri(url, finalBrandUrl);
328
- return { url, jwksUri };
329
- }
330
- function defaultJwksUri(agentUrl, finalBrandUrl) {
331
- let agent;
332
- try {
333
- agent = new URL(agentUrl);
334
- } catch {
335
- throw new BrandJsonResolverError("invalid_url", `agent.url is not a valid URL`);
336
- }
337
- const brand = new URL(finalBrandUrl);
338
- if (agent.origin !== brand.origin) {
339
- throw new BrandJsonResolverError(
340
- "jwks_origin_mismatch",
341
- `agent.url origin (${agent.origin}) does not match brand.json origin (${brand.origin}); publisher must declare an explicit jwks_uri for cross-origin agents`
342
- );
343
- }
344
- return `${agent.origin}/.well-known/jwks.json`;
345
- }
346
- function describeSelector(selector) {
347
- const parts = [`type=${selector.agentType}`];
348
- if (selector.agentId !== void 0) parts.push(`id=${selector.agentId}`);
349
- if (selector.brandId !== void 0) parts.push(`brand=${selector.brandId}`);
350
- return parts.join(" ");
351
- }
352
- function computeLifetime(cacheControl, maxAge) {
353
- if (!cacheControl) return maxAge;
354
- const lower = cacheControl.toLowerCase();
355
- if (/\bno-store\b|\bno-cache\b/.test(lower)) return 0;
356
- const match = /max-age\s*=\s*(\d+)/.exec(lower);
357
- if (match) {
358
- const serverMax = Number(match[1]);
359
- if (Number.isFinite(serverMax)) return Math.min(serverMax, maxAge);
360
- }
361
- return maxAge;
362
- }
363
293
  export {
364
294
  BrandJsonJwksResolver,
365
295
  BrandJsonResolverError,
@@ -24,7 +24,9 @@ export type WebhookSignatureErrorCode = 'webhook_signature_header_malformed' | '
24
24
  export declare class WebhookSignatureError extends ADCPError {
25
25
  readonly code: WebhookSignatureErrorCode;
26
26
  readonly failedStep: number;
27
- constructor(code: WebhookSignatureErrorCode, failedStep: number, message: string, details?: unknown);
27
+ /** Local retry signal; the wire error code stays in the webhook vocabulary. */
28
+ readonly retryable: boolean;
29
+ constructor(code: WebhookSignatureErrorCode, failedStep: number, message: string, details?: unknown, retryable?: boolean);
28
30
  }
29
31
  export type ResponseSignatureErrorCode = 'response_signature_key_purpose_invalid';
30
32
  export declare class ResponseSignatureError extends ADCPError {
@@ -24,7 +24,9 @@ export type WebhookSignatureErrorCode = 'webhook_signature_header_malformed' | '
24
24
  export declare class WebhookSignatureError extends ADCPError {
25
25
  readonly code: WebhookSignatureErrorCode;
26
26
  readonly failedStep: number;
27
- constructor(code: WebhookSignatureErrorCode, failedStep: number, message: string, details?: unknown);
27
+ /** Local retry signal; the wire error code stays in the webhook vocabulary. */
28
+ readonly retryable: boolean;
29
+ constructor(code: WebhookSignatureErrorCode, failedStep: number, message: string, details?: unknown, retryable?: boolean);
28
30
  }
29
31
  export type ResponseSignatureErrorCode = 'response_signature_key_purpose_invalid';
30
32
  export declare class ResponseSignatureError extends ADCPError {
@@ -39,10 +39,13 @@ class RequestSignatureError extends import_errors.ADCPError {
39
39
  class WebhookSignatureError extends import_errors.ADCPError {
40
40
  code;
41
41
  failedStep;
42
- constructor(code, failedStep, message, details) {
42
+ /** Local retry signal; the wire error code stays in the webhook vocabulary. */
43
+ retryable;
44
+ constructor(code, failedStep, message, details, retryable = false) {
43
45
  super(message, details);
44
46
  this.code = code;
45
47
  this.failedStep = failedStep;
48
+ this.retryable = retryable;
46
49
  }
47
50
  }
48
51
  class ResponseSignatureError extends import_errors.ADCPError {
@@ -12,10 +12,13 @@ class RequestSignatureError extends ADCPError {
12
12
  class WebhookSignatureError extends ADCPError {
13
13
  code;
14
14
  failedStep;
15
- constructor(code, failedStep, message, details) {
15
+ /** Local retry signal; the wire error code stays in the webhook vocabulary. */
16
+ retryable;
17
+ constructor(code, failedStep, message, details, retryable = false) {
16
18
  super(message, details);
17
19
  this.code = code;
18
20
  this.failedStep = failedStep;
21
+ this.retryable = retryable;
19
22
  }
20
23
  }
21
24
  class ResponseSignatureError extends ADCPError {
@@ -15,6 +15,10 @@ export interface HttpsJwksResolverOptions {
15
15
  * longer. Default 3600s (1 hour).
16
16
  */
17
17
  maxAgeSeconds?: number;
18
+ /** Protocol cache floor. Defaults to 0; governance uses 60 seconds. */
19
+ minCacheAgeSeconds?: number;
20
+ /** Reject refresh failures instead of serving an expired snapshot. Default false. */
21
+ failClosed?: boolean;
18
22
  /** Allow `http://` / private-IP JWKS URLs (dev loops only). Default false. */
19
23
  allowPrivateIp?: boolean;
20
24
  /** DNS resolver forwarded to the SSRF-safe fetch path. Defaults to `dns/promises.lookup`. */
@@ -45,11 +49,14 @@ export declare class HttpsJwksResolver implements JwksResolver {
45
49
  private readonly url;
46
50
  private readonly minCooldown;
47
51
  private readonly maxAge;
52
+ private readonly minCacheAge;
53
+ private readonly failClosed;
48
54
  private readonly allowPrivateIp;
49
55
  private readonly lookup;
50
56
  private readonly now;
51
57
  private cache;
52
58
  private inFlight;
59
+ private lastAttemptAt;
53
60
  constructor(url: string, options?: HttpsJwksResolverOptions);
54
61
  resolve(keyid: string): Promise<AdcpJsonWebKey | null>;
55
62
  /**
@@ -15,6 +15,10 @@ export interface HttpsJwksResolverOptions {
15
15
  * longer. Default 3600s (1 hour).
16
16
  */
17
17
  maxAgeSeconds?: number;
18
+ /** Protocol cache floor. Defaults to 0; governance uses 60 seconds. */
19
+ minCacheAgeSeconds?: number;
20
+ /** Reject refresh failures instead of serving an expired snapshot. Default false. */
21
+ failClosed?: boolean;
18
22
  /** Allow `http://` / private-IP JWKS URLs (dev loops only). Default false. */
19
23
  allowPrivateIp?: boolean;
20
24
  /** DNS resolver forwarded to the SSRF-safe fetch path. Defaults to `dns/promises.lookup`. */
@@ -45,11 +49,14 @@ export declare class HttpsJwksResolver implements JwksResolver {
45
49
  private readonly url;
46
50
  private readonly minCooldown;
47
51
  private readonly maxAge;
52
+ private readonly minCacheAge;
53
+ private readonly failClosed;
48
54
  private readonly allowPrivateIp;
49
55
  private readonly lookup;
50
56
  private readonly now;
51
57
  private cache;
52
58
  private inFlight;
59
+ private lastAttemptAt;
53
60
  constructor(url: string, options?: HttpsJwksResolverOptions);
54
61
  resolve(keyid: string): Promise<AdcpJsonWebKey | null>;
55
62
  /**
@@ -22,40 +22,61 @@ __export(jwks_https_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(jwks_https_exports);
24
24
  var import_net = require('../net/index.js');
25
+ var import_strict_json = require('./agent-resolver/strict-json.js');
25
26
  const DEFAULT_MIN_COOLDOWN_SECONDS = 30;
26
27
  const DEFAULT_MAX_AGE_SECONDS = 3600;
27
28
  class HttpsJwksResolver {
28
29
  url;
29
30
  minCooldown;
30
31
  maxAge;
32
+ minCacheAge;
33
+ failClosed;
31
34
  allowPrivateIp;
32
35
  lookup;
33
36
  now;
34
37
  cache;
35
38
  inFlight;
39
+ lastAttemptAt = Number.NEGATIVE_INFINITY;
36
40
  constructor(url, options = {}) {
37
41
  this.url = url;
38
42
  this.minCooldown = options.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS;
39
43
  this.maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
44
+ this.minCacheAge = Math.min(options.minCacheAgeSeconds ?? 0, this.maxAge);
45
+ if (!Number.isFinite(this.maxAge) || this.maxAge < 0) {
46
+ throw new TypeError("maxAgeSeconds must be a finite non-negative number");
47
+ }
48
+ if (!Number.isFinite(this.minCooldown) || this.minCooldown < 0) {
49
+ throw new TypeError("minCooldownSeconds must be a finite non-negative number");
50
+ }
51
+ if (!Number.isFinite(options.minCacheAgeSeconds ?? 0) || (options.minCacheAgeSeconds ?? 0) < 0) {
52
+ throw new TypeError("minCacheAgeSeconds must be a finite non-negative number");
53
+ }
54
+ this.failClosed = options.failClosed === true;
40
55
  this.allowPrivateIp = options.allowPrivateIp ?? false;
41
56
  this.lookup = options.lookup;
42
57
  this.now = options.now ?? (() => Math.floor(Date.now() / 1e3));
43
58
  }
44
59
  async resolve(keyid) {
45
60
  if (!this.cache) {
61
+ if (!this.inFlight && this.now() - this.lastAttemptAt < this.minCooldown) {
62
+ throw new Error("JWKS lookup is in its retry cooldown");
63
+ }
46
64
  await this.refresh();
47
65
  } else if (this.cache.keys.has(keyid)) {
48
- if (this.now() <= this.cache.expiresAt) {
66
+ if (this.now() < this.cache.expiresAt) {
49
67
  return this.cache.keys.get(keyid) ?? null;
50
68
  }
51
- if (this.now() - this.cache.fetchedAt >= this.minCooldown) {
52
- await this.refresh().catch(() => {
69
+ if (this.now() - this.lastAttemptAt >= this.minCooldown) {
70
+ await this.refresh().catch((error) => {
71
+ if (this.failClosed) throw error;
53
72
  });
54
73
  }
55
- } else if (this.now() - this.cache.fetchedAt >= this.minCooldown) {
56
- await this.refresh().catch(() => {
74
+ } else if (this.now() - this.lastAttemptAt >= this.minCooldown) {
75
+ await this.refresh().catch((error) => {
76
+ if (this.failClosed) throw error;
57
77
  });
58
78
  }
79
+ if (this.failClosed && this.cache && this.now() >= this.cache.expiresAt) throw new Error("JWKS snapshot expired");
59
80
  return this.cache?.keys.get(keyid) ?? null;
60
81
  }
61
82
  /**
@@ -72,6 +93,7 @@ class HttpsJwksResolver {
72
93
  await this.inFlight;
73
94
  return;
74
95
  }
96
+ this.lastAttemptAt = this.now();
75
97
  this.inFlight = this.doRefresh().finally(() => {
76
98
  this.inFlight = void 0;
77
99
  });
@@ -103,7 +125,7 @@ class HttpsJwksResolver {
103
125
  const text = Buffer.from(res.body).toString("utf8");
104
126
  let parsed;
105
127
  try {
106
- parsed = JSON.parse(text);
128
+ parsed = (0, import_strict_json.parseStrictJson)(text);
107
129
  } catch {
108
130
  throw new Error(`JWKS fetch ${this.url} returned non-JSON body`);
109
131
  }
@@ -118,6 +140,7 @@ class HttpsJwksResolver {
118
140
  for (const entry of keysField) {
119
141
  if (entry && typeof entry === "object" && typeof entry.kid === "string") {
120
142
  const jwk = entry;
143
+ if (byKid.has(jwk.kid)) throw new Error("JWKS has duplicate key identifiers");
121
144
  byKid.set(jwk.kid, jwk);
122
145
  }
123
146
  }
@@ -130,11 +153,11 @@ class HttpsJwksResolver {
130
153
  }
131
154
  computeExpiry(headers) {
132
155
  const cacheCtl = headers["cache-control"]?.toLowerCase() ?? "";
133
- if (/\bno-store\b|\bno-cache\b/.test(cacheCtl)) return this.now();
156
+ if (/\bno-store\b|\bno-cache\b/.test(cacheCtl)) return this.now() + this.minCacheAge;
134
157
  const match = /max-age\s*=\s*(\d+)/.exec(cacheCtl);
135
158
  if (match) {
136
159
  const serverMax = Number(match[1]);
137
- if (Number.isFinite(serverMax)) return this.now() + Math.min(serverMax, this.maxAge);
160
+ if (Number.isFinite(serverMax)) return this.now() + Math.max(this.minCacheAge, Math.min(serverMax, this.maxAge));
138
161
  }
139
162
  return this.now() + this.maxAge;
140
163
  }
@@ -1,38 +1,59 @@
1
1
  import { ssrfSafeFetch } from "../net/index.mjs";
2
+ import { parseStrictJson } from "./agent-resolver/strict-json.mjs";
2
3
  const DEFAULT_MIN_COOLDOWN_SECONDS = 30;
3
4
  const DEFAULT_MAX_AGE_SECONDS = 3600;
4
5
  class HttpsJwksResolver {
5
6
  url;
6
7
  minCooldown;
7
8
  maxAge;
9
+ minCacheAge;
10
+ failClosed;
8
11
  allowPrivateIp;
9
12
  lookup;
10
13
  now;
11
14
  cache;
12
15
  inFlight;
16
+ lastAttemptAt = Number.NEGATIVE_INFINITY;
13
17
  constructor(url, options = {}) {
14
18
  this.url = url;
15
19
  this.minCooldown = options.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS;
16
20
  this.maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
21
+ this.minCacheAge = Math.min(options.minCacheAgeSeconds ?? 0, this.maxAge);
22
+ if (!Number.isFinite(this.maxAge) || this.maxAge < 0) {
23
+ throw new TypeError("maxAgeSeconds must be a finite non-negative number");
24
+ }
25
+ if (!Number.isFinite(this.minCooldown) || this.minCooldown < 0) {
26
+ throw new TypeError("minCooldownSeconds must be a finite non-negative number");
27
+ }
28
+ if (!Number.isFinite(options.minCacheAgeSeconds ?? 0) || (options.minCacheAgeSeconds ?? 0) < 0) {
29
+ throw new TypeError("minCacheAgeSeconds must be a finite non-negative number");
30
+ }
31
+ this.failClosed = options.failClosed === true;
17
32
  this.allowPrivateIp = options.allowPrivateIp ?? false;
18
33
  this.lookup = options.lookup;
19
34
  this.now = options.now ?? (() => Math.floor(Date.now() / 1e3));
20
35
  }
21
36
  async resolve(keyid) {
22
37
  if (!this.cache) {
38
+ if (!this.inFlight && this.now() - this.lastAttemptAt < this.minCooldown) {
39
+ throw new Error("JWKS lookup is in its retry cooldown");
40
+ }
23
41
  await this.refresh();
24
42
  } else if (this.cache.keys.has(keyid)) {
25
- if (this.now() <= this.cache.expiresAt) {
43
+ if (this.now() < this.cache.expiresAt) {
26
44
  return this.cache.keys.get(keyid) ?? null;
27
45
  }
28
- if (this.now() - this.cache.fetchedAt >= this.minCooldown) {
29
- await this.refresh().catch(() => {
46
+ if (this.now() - this.lastAttemptAt >= this.minCooldown) {
47
+ await this.refresh().catch((error) => {
48
+ if (this.failClosed) throw error;
30
49
  });
31
50
  }
32
- } else if (this.now() - this.cache.fetchedAt >= this.minCooldown) {
33
- await this.refresh().catch(() => {
51
+ } else if (this.now() - this.lastAttemptAt >= this.minCooldown) {
52
+ await this.refresh().catch((error) => {
53
+ if (this.failClosed) throw error;
34
54
  });
35
55
  }
56
+ if (this.failClosed && this.cache && this.now() >= this.cache.expiresAt) throw new Error("JWKS snapshot expired");
36
57
  return this.cache?.keys.get(keyid) ?? null;
37
58
  }
38
59
  /**
@@ -49,6 +70,7 @@ class HttpsJwksResolver {
49
70
  await this.inFlight;
50
71
  return;
51
72
  }
73
+ this.lastAttemptAt = this.now();
52
74
  this.inFlight = this.doRefresh().finally(() => {
53
75
  this.inFlight = void 0;
54
76
  });
@@ -80,7 +102,7 @@ class HttpsJwksResolver {
80
102
  const text = Buffer.from(res.body).toString("utf8");
81
103
  let parsed;
82
104
  try {
83
- parsed = JSON.parse(text);
105
+ parsed = parseStrictJson(text);
84
106
  } catch {
85
107
  throw new Error(`JWKS fetch ${this.url} returned non-JSON body`);
86
108
  }
@@ -95,6 +117,7 @@ class HttpsJwksResolver {
95
117
  for (const entry of keysField) {
96
118
  if (entry && typeof entry === "object" && typeof entry.kid === "string") {
97
119
  const jwk = entry;
120
+ if (byKid.has(jwk.kid)) throw new Error("JWKS has duplicate key identifiers");
98
121
  byKid.set(jwk.kid, jwk);
99
122
  }
100
123
  }
@@ -107,11 +130,11 @@ class HttpsJwksResolver {
107
130
  }
108
131
  computeExpiry(headers) {
109
132
  const cacheCtl = headers["cache-control"]?.toLowerCase() ?? "";
110
- if (/\bno-store\b|\bno-cache\b/.test(cacheCtl)) return this.now();
133
+ if (/\bno-store\b|\bno-cache\b/.test(cacheCtl)) return this.now() + this.minCacheAge;
111
134
  const match = /max-age\s*=\s*(\d+)/.exec(cacheCtl);
112
135
  if (match) {
113
136
  const serverMax = Number(match[1]);
114
- if (Number.isFinite(serverMax)) return this.now() + Math.min(serverMax, this.maxAge);
137
+ if (Number.isFinite(serverMax)) return this.now() + Math.max(this.minCacheAge, Math.min(serverMax, this.maxAge));
115
138
  }
116
139
  return this.now() + this.maxAge;
117
140
  }