@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.
- package/dist/lib/adapters/implicit-account-store.d.mts +12 -7
- package/dist/lib/adapters/implicit-account-store.d.ts +12 -7
- package/dist/lib/adapters/implicit-account-store.js +69 -15
- package/dist/lib/adapters/implicit-account-store.mjs +69 -15
- package/dist/lib/core/AgentClient.d.mts +1 -0
- package/dist/lib/core/AgentClient.d.ts +1 -0
- package/dist/lib/core/AgentClient.js +3 -0
- package/dist/lib/core/AgentClient.mjs +3 -0
- package/dist/lib/core/SingleAgentClient.d.mts +31 -1
- package/dist/lib/core/SingleAgentClient.d.ts +31 -1
- package/dist/lib/core/SingleAgentClient.js +355 -35
- package/dist/lib/core/SingleAgentClient.mjs +365 -37
- package/dist/lib/core/TaskExecutor.js +2 -1
- package/dist/lib/core/TaskExecutor.mjs +2 -1
- package/dist/lib/core/account-key.d.mts +3 -0
- package/dist/lib/core/account-key.d.ts +3 -0
- package/dist/lib/core/account-key.js +41 -0
- package/dist/lib/core/account-key.mjs +17 -0
- package/dist/lib/core/account-resolution.d.mts +2 -0
- package/dist/lib/core/account-resolution.d.ts +2 -0
- package/dist/lib/core/buyer-account-registry.d.mts +65 -0
- package/dist/lib/core/buyer-account-registry.d.ts +65 -0
- package/dist/lib/core/buyer-account-registry.js +518 -0
- package/dist/lib/core/buyer-account-registry.mjs +494 -0
- package/dist/lib/core/product-cache.d.mts +18 -0
- package/dist/lib/core/product-cache.d.ts +18 -0
- package/dist/lib/core/product-cache.js +137 -0
- package/dist/lib/core/product-cache.mjs +112 -0
- package/dist/lib/errors/index.d.mts +40 -1
- package/dist/lib/errors/index.d.ts +40 -1
- package/dist/lib/errors/index.js +69 -3
- package/dist/lib/errors/index.mjs +64 -3
- package/dist/lib/governance/authorization.d.mts +17 -1
- package/dist/lib/governance/authorization.d.ts +17 -1
- package/dist/lib/governance/authorization.js +55 -7
- package/dist/lib/governance/authorization.mjs +59 -7
- package/dist/lib/governance/index.d.mts +2 -2
- package/dist/lib/governance/index.d.ts +2 -2
- package/dist/lib/governance/index.js +2 -0
- package/dist/lib/governance/index.mjs +3 -1
- package/dist/lib/index.d.mts +5 -3
- package/dist/lib/index.d.ts +5 -3
- package/dist/lib/index.js +20 -0
- package/dist/lib/index.mjs +19 -0
- package/dist/lib/protocols/a2a.js +9 -1
- package/dist/lib/protocols/a2a.mjs +9 -1
- package/dist/lib/protocols/index.js +9 -2
- package/dist/lib/protocols/index.mjs +9 -2
- package/dist/lib/protocols/mcp-modern.js +2 -1
- package/dist/lib/protocols/mcp-modern.mjs +2 -1
- package/dist/lib/protocols/mcp.js +5 -2
- package/dist/lib/protocols/mcp.mjs +5 -2
- package/dist/lib/protocols/rawResponseCapture.d.mts +6 -0
- package/dist/lib/protocols/rawResponseCapture.d.ts +6 -0
- package/dist/lib/protocols/rawResponseCapture.js +41 -29
- package/dist/lib/protocols/rawResponseCapture.mjs +40 -29
- package/dist/lib/protocols/signedRequestRejection.d.mts +9 -0
- package/dist/lib/protocols/signedRequestRejection.d.ts +9 -0
- package/dist/lib/protocols/signedRequestRejection.js +209 -0
- package/dist/lib/protocols/signedRequestRejection.mjs +189 -0
- package/dist/lib/protocols/transportDiagnostics.d.mts +1 -0
- package/dist/lib/protocols/transportDiagnostics.d.ts +1 -0
- package/dist/lib/protocols/transportDiagnostics.js +2 -0
- package/dist/lib/protocols/transportDiagnostics.mjs +1 -0
- package/dist/lib/registry/types.generated.d.mts +112 -45
- package/dist/lib/registry/types.generated.d.ts +112 -45
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/account-provisioning.d.mts +2 -0
- package/dist/lib/server/account-provisioning.d.ts +2 -0
- package/dist/lib/server/account-provisioning.js +30 -0
- package/dist/lib/server/account-provisioning.mjs +6 -0
- package/dist/lib/server/account-reference-warnings.d.mts +12 -0
- package/dist/lib/server/account-reference-warnings.d.ts +12 -0
- package/dist/lib/server/account-reference-warnings.js +48 -0
- package/dist/lib/server/account-reference-warnings.mjs +23 -0
- package/dist/lib/server/auth-signature.js +1 -0
- package/dist/lib/server/auth-signature.mjs +1 -0
- package/dist/lib/server/create-adcp-server.d.mts +34 -0
- package/dist/lib/server/create-adcp-server.d.ts +34 -0
- package/dist/lib/server/create-adcp-server.js +225 -14
- package/dist/lib/server/create-adcp-server.mjs +225 -14
- package/dist/lib/server/decisioning/account.d.mts +2 -0
- package/dist/lib/server/decisioning/account.d.ts +2 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +49 -10
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +49 -10
- package/dist/lib/server/index.d.mts +2 -2
- package/dist/lib/server/index.d.ts +2 -2
- package/dist/lib/server/index.js +2 -0
- package/dist/lib/server/index.mjs +3 -1
- package/dist/lib/signing/agent-resolver/consistency.d.mts +6 -13
- package/dist/lib/signing/agent-resolver/consistency.d.ts +6 -13
- package/dist/lib/signing/agent-resolver/consistency.js +0 -1
- package/dist/lib/signing/agent-resolver/consistency.mjs +0 -1
- package/dist/lib/signing/agent-resolver/errors.d.mts +1 -1
- package/dist/lib/signing/agent-resolver/errors.d.ts +1 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.d.mts +2 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.d.ts +2 -0
- package/dist/lib/signing/agent-resolver/fetch-helpers.js +2 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +2 -1
- package/dist/lib/signing/agent-resolver/jwks-set.js +24 -3
- package/dist/lib/signing/agent-resolver/jwks-set.mjs +24 -3
- package/dist/lib/signing/agent-resolver/legacy-brand.d.mts +15 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.d.ts +15 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.js +60 -0
- package/dist/lib/signing/agent-resolver/legacy-brand.mjs +36 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.d.mts +13 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.d.ts +13 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.js +108 -0
- package/dist/lib/signing/agent-resolver/operator-authorization.mjs +84 -0
- package/dist/lib/signing/agent-resolver/resolve-agent.d.mts +14 -4
- package/dist/lib/signing/agent-resolver/resolve-agent.d.ts +14 -4
- package/dist/lib/signing/agent-resolver/resolve-agent.js +101 -132
- package/dist/lib/signing/agent-resolver/resolve-agent.mjs +102 -133
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.mts +7 -1
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.d.ts +7 -1
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.js +50 -17
- package/dist/lib/signing/agent-resolver/resolved-agent-jwks.mjs +50 -17
- package/dist/lib/signing/agent-resolver/select-agent.d.mts +14 -15
- package/dist/lib/signing/agent-resolver/select-agent.d.ts +14 -15
- package/dist/lib/signing/agent-resolver/select-agent.js +98 -13
- package/dist/lib/signing/agent-resolver/select-agent.mjs +95 -13
- package/dist/lib/signing/brand-jwks.d.mts +27 -75
- package/dist/lib/signing/brand-jwks.d.ts +27 -75
- package/dist/lib/signing/brand-jwks.js +112 -182
- package/dist/lib/signing/brand-jwks.mjs +112 -182
- package/dist/lib/signing/errors.d.mts +3 -1
- package/dist/lib/signing/errors.d.ts +3 -1
- package/dist/lib/signing/errors.js +4 -1
- package/dist/lib/signing/errors.mjs +4 -1
- package/dist/lib/signing/jwks-https.d.mts +7 -0
- package/dist/lib/signing/jwks-https.d.ts +7 -0
- package/dist/lib/signing/jwks-https.js +31 -8
- package/dist/lib/signing/jwks-https.mjs +31 -8
- package/dist/lib/signing/jwks.d.mts +8 -0
- package/dist/lib/signing/jwks.d.ts +8 -0
- package/dist/lib/signing/middleware.js +2 -1
- package/dist/lib/signing/middleware.mjs +2 -1
- package/dist/lib/signing/publisher-pins.d.mts +11 -0
- package/dist/lib/signing/publisher-pins.d.ts +11 -0
- package/dist/lib/signing/publisher-pins.js +125 -0
- package/dist/lib/signing/publisher-pins.mjs +101 -0
- package/dist/lib/signing/server.d.mts +1 -0
- package/dist/lib/signing/server.d.ts +1 -0
- package/dist/lib/signing/types.d.mts +5 -0
- package/dist/lib/signing/types.d.ts +5 -0
- package/dist/lib/signing/verifier.js +49 -4
- package/dist/lib/signing/verifier.mjs +49 -4
- package/dist/lib/signing/webhook-verifier.d.mts +7 -2
- package/dist/lib/signing/webhook-verifier.d.ts +7 -2
- package/dist/lib/signing/webhook-verifier.js +42 -2
- package/dist/lib/signing/webhook-verifier.mjs +43 -3
- package/dist/lib/testing/storyboard/account-policy.d.mts +2 -0
- package/dist/lib/testing/storyboard/account-policy.d.ts +2 -0
- package/dist/lib/testing/storyboard/account-policy.js +35 -0
- package/dist/lib/testing/storyboard/account-policy.mjs +11 -0
- package/dist/lib/testing/storyboard/context.js +6 -0
- package/dist/lib/testing/storyboard/context.mjs +6 -0
- package/dist/lib/testing/storyboard/request-builder.js +12 -2
- package/dist/lib/testing/storyboard/request-builder.mjs +12 -2
- package/dist/lib/testing/storyboard/runner.js +3 -2
- package/dist/lib/testing/storyboard/runner.mjs +3 -2
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/dist/lib/wholesale-feed-sync/sync.d.mts +1 -0
- package/dist/lib/wholesale-feed-sync/sync.d.ts +1 -0
- package/dist/lib/wholesale-feed-sync/sync.js +92 -21
- package/dist/lib/wholesale-feed-sync/sync.mjs +92 -21
- package/dist/lib/wholesale-feed-sync/types.d.mts +4 -3
- package/dist/lib/wholesale-feed-sync/types.d.ts +4 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/BUILD-AN-AGENT.md +2 -2
- package/docs/guides/BUYER-QUICKSTART-3.2.md +2 -0
- package/docs/guides/FIRST-CALL-TO-A-SELLER.md +104 -0
- package/docs/guides/SIGNING-GUIDE.md +16 -7
- package/docs/guides/account-resolution.md +86 -0
- package/docs/llms.txt +3 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-4.x-to-5.x.md +1 -0
- package/docs/migration-agent-resolution-3.3.md +123 -0
- package/docs/recipes/verifying-inbound-webhooks.md +56 -15
- package/package.json +2 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ssrfSafeFetch } from "../net/index.mjs";
|
|
2
|
-
import {
|
|
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.
|
|
35
|
-
this.
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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.
|
|
83
|
-
this.
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
66
|
+
if (this.now() < this.cache.expiresAt) {
|
|
49
67
|
return this.cache.keys.get(keyid) ?? null;
|
|
50
68
|
}
|
|
51
|
-
if (this.now() - this.
|
|
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.
|
|
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 =
|
|
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()
|
|
43
|
+
if (this.now() < this.cache.expiresAt) {
|
|
26
44
|
return this.cache.keys.get(keyid) ?? null;
|
|
27
45
|
}
|
|
28
|
-
if (this.now() - this.
|
|
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.
|
|
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 =
|
|
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
|
}
|