@certrev/cert-block 0.5.5 → 1.0.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/CHANGELOG.md +164 -26
- package/LICENSE +21 -0
- package/README.md +415 -54
- package/dist/builder/cert-chrome-data.d.ts +26 -8
- package/dist/builder/cert-chrome-data.d.ts.map +1 -1
- package/dist/builder/cert-chrome-data.js +5 -5
- package/dist/builder/cert-chrome-data.js.map +1 -1
- package/dist/builder/cert-review-card.d.ts +35 -5
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +115 -34
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/index.d.ts +33 -16
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +33 -16
- package/dist/builder/index.js.map +1 -1
- package/dist/components/CertBadge.d.ts +2 -2
- package/dist/components/CertBadge.d.ts.map +1 -1
- package/dist/components/CertBadge.js +11 -5
- package/dist/components/CertBadge.js.map +1 -1
- package/dist/components/CertJsonLd.d.ts.map +1 -1
- package/dist/components/CertJsonLd.js.map +1 -1
- package/dist/components/CertRevBacklink.d.ts.map +1 -1
- package/dist/components/CertRevBacklink.js +3 -2
- package/dist/components/CertRevBacklink.js.map +1 -1
- package/dist/components/CertReview.d.ts +1 -1
- package/dist/components/ExpertBio.d.ts.map +1 -1
- package/dist/components/ExpertBio.js +12 -4
- package/dist/components/ExpertBio.js.map +1 -1
- package/dist/components/escape.d.ts +28 -1
- package/dist/components/escape.d.ts.map +1 -1
- package/dist/components/escape.js +37 -8
- package/dist/components/escape.js.map +1 -1
- package/dist/components/format.d.ts +51 -2
- package/dist/components/format.d.ts.map +1 -1
- package/dist/components/format.js +103 -6
- package/dist/components/format.js.map +1 -1
- package/dist/components/preview-block.d.ts +1 -1
- package/dist/components/preview-block.js +3 -3
- package/dist/components/preview-block.js.map +1 -1
- package/dist/components/rel.d.ts +24 -0
- package/dist/components/rel.d.ts.map +1 -0
- package/dist/components/rel.js +24 -0
- package/dist/components/rel.js.map +1 -0
- package/dist/components/render-cert-block.d.ts +50 -22
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +146 -80
- package/dist/components/render-cert-block.js.map +1 -1
- package/dist/components/render-def.d.ts +13 -14
- package/dist/components/render-def.d.ts.map +1 -1
- package/dist/components/render-def.js +14 -15
- package/dist/components/render-def.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/jsonld/project.d.ts +46 -13
- package/dist/jsonld/project.d.ts.map +1 -1
- package/dist/jsonld/project.js +139 -56
- package/dist/jsonld/project.js.map +1 -1
- package/dist/modal/avatar-resize.d.ts +10 -10
- package/dist/modal/avatar-resize.js +14 -14
- package/dist/modal/avatar-resize.js.map +1 -1
- package/dist/modal/cert-modal-view.d.ts +27 -7
- package/dist/modal/cert-modal-view.d.ts.map +1 -1
- package/dist/modal/cert-modal-view.js +49 -25
- package/dist/modal/cert-modal-view.js.map +1 -1
- package/dist/modal/certrev-cert-modal.d.ts +5 -5
- package/dist/modal/certrev-cert-modal.js +9 -9
- package/dist/modal/certrev-cert-modal.js.map +1 -1
- package/dist/modal/certrev-cert-modal.styles.d.ts +6 -6
- package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -1
- package/dist/modal/certrev-cert-modal.styles.js +8 -8
- package/dist/modal/certrev-cert-modal.styles.js.map +1 -1
- package/dist/modal/certrev-cert.entry.d.ts +8 -6
- package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
- package/dist/modal/certrev-cert.entry.js +9 -7
- package/dist/modal/certrev-cert.entry.js.map +1 -1
- package/dist/modal/certrev-cert.js +6 -6
- package/dist/modal/display-strings.d.ts +25 -5
- package/dist/modal/display-strings.d.ts.map +1 -1
- package/dist/modal/display-strings.js +27 -5
- package/dist/modal/display-strings.js.map +1 -1
- package/dist/modal/escape.d.ts +26 -3
- package/dist/modal/escape.d.ts.map +1 -1
- package/dist/modal/escape.js +32 -3
- package/dist/modal/escape.js.map +1 -1
- package/dist/modal/font-face.d.ts.map +1 -1
- package/dist/modal/font-face.js +8 -11
- package/dist/modal/font-face.js.map +1 -1
- package/dist/modal/ftc-disclosure.d.ts +2 -2
- package/dist/modal/ftc-disclosure.js +2 -2
- package/dist/modal/ftc-guard.d.ts +44 -9
- package/dist/modal/ftc-guard.d.ts.map +1 -1
- package/dist/modal/ftc-guard.js +50 -17
- package/dist/modal/ftc-guard.js.map +1 -1
- package/dist/modal/index.d.ts +1 -1
- package/dist/modal/index.js +1 -1
- package/dist/modal/interactions.d.ts +2 -2
- package/dist/modal/interactions.js +2 -2
- package/dist/modal/logo-paths.js +2 -2
- package/dist/modal/logo-paths.js.map +1 -1
- package/dist/modal/placement.d.ts +4 -3
- package/dist/modal/placement.d.ts.map +1 -1
- package/dist/modal/placement.js +7 -6
- package/dist/modal/placement.js.map +1 -1
- package/dist/modal/revalidate.d.ts +2 -3
- package/dist/modal/revalidate.d.ts.map +1 -1
- package/dist/modal/revalidate.js +2 -3
- package/dist/modal/revalidate.js.map +1 -1
- package/dist/modal/verify-client.d.ts +2 -2
- package/dist/modal/verify-client.js +2 -2
- package/dist/verify/cache.d.ts +21 -6
- package/dist/verify/cache.d.ts.map +1 -1
- package/dist/verify/cache.js +29 -8
- package/dist/verify/cache.js.map +1 -1
- package/dist/verify/fetch-with-deadline.d.ts +35 -0
- package/dist/verify/fetch-with-deadline.d.ts.map +1 -0
- package/dist/verify/fetch-with-deadline.js +55 -0
- package/dist/verify/fetch-with-deadline.js.map +1 -0
- package/dist/verify/get-verified-envelope.d.ts +23 -9
- package/dist/verify/get-verified-envelope.d.ts.map +1 -1
- package/dist/verify/get-verified-envelope.js +89 -40
- package/dist/verify/get-verified-envelope.js.map +1 -1
- package/dist/verify/resolve-kid.d.ts +3 -0
- package/dist/verify/resolve-kid.d.ts.map +1 -1
- package/dist/verify/resolve-kid.js +17 -8
- package/dist/verify/resolve-kid.js.map +1 -1
- package/dist/webcomponent/certrev-badge.d.ts.map +1 -1
- package/dist/webcomponent/certrev-badge.js +24 -2
- package/dist/webcomponent/certrev-badge.js.map +1 -1
- package/dist/webcomponent/render-badge-html.d.ts +1 -1
- package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
- package/dist/webcomponent/render-badge-html.js +36 -17
- package/dist/webcomponent/render-badge-html.js.map +1 -1
- package/package.json +5 -2
- package/src/builder/cert-chrome-data.ts +26 -8
- package/src/builder/cert-review-card.tsx +147 -38
- package/src/builder/index.tsx +34 -17
- package/src/components/CertBadge.tsx +14 -9
- package/src/components/CertJsonLd.tsx +1 -1
- package/src/components/CertRevBacklink.tsx +10 -3
- package/src/components/CertReview.tsx +1 -1
- package/src/components/ExpertBio.tsx +12 -7
- package/src/components/escape.ts +37 -7
- package/src/components/format.ts +102 -6
- package/src/components/preview-block.ts +3 -3
- package/src/components/rel.ts +23 -0
- package/src/components/render-cert-block.ts +192 -109
- package/src/components/render-def.ts +14 -15
- package/src/index.ts +4 -2
- package/src/jsonld/project.ts +155 -59
- package/src/modal/avatar-resize.ts +14 -14
- package/src/modal/cert-modal-view.ts +52 -27
- package/src/modal/certrev-cert-modal.styles.ts +8 -8
- package/src/modal/certrev-cert-modal.ts +9 -9
- package/src/modal/certrev-cert.entry.ts +9 -7
- package/src/modal/display-strings.ts +26 -5
- package/src/modal/escape.ts +32 -4
- package/src/modal/font-face.ts +8 -11
- package/src/modal/ftc-disclosure.ts +2 -2
- package/src/modal/ftc-guard.ts +73 -18
- package/src/modal/index.ts +1 -1
- package/src/modal/interactions.ts +2 -2
- package/src/modal/logo-paths.ts +2 -2
- package/src/modal/placement.ts +7 -6
- package/src/modal/revalidate.ts +2 -3
- package/src/modal/verify-client.ts +2 -2
- package/src/verify/cache.ts +34 -8
- package/src/verify/fetch-with-deadline.ts +60 -0
- package/src/verify/get-verified-envelope.ts +131 -48
- package/src/verify/resolve-kid.ts +22 -6
- package/src/webcomponent/certrev-badge.ts +24 -2
- package/src/webcomponent/render-badge-html.ts +66 -19
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +0 -17
- package/src/__tests__/components.test.tsx +0 -206
- package/src/__tests__/preview-block.test.ts +0 -84
- package/src/__tests__/project.test.ts +0 -128
- package/src/__tests__/readme-version.test.ts +0 -51
- package/src/__tests__/render-cert-block.test.ts +0 -657
- package/src/__tests__/render-def.test.tsx +0 -295
- package/src/__tests__/verify.test.ts +0 -239
- package/src/__tests__/webcomponent.test.tsx +0 -116
- package/src/builder/__tests__/builder.test.tsx +0 -158
- package/src/modal/cert-modal-view.test.ts +0 -175
- package/src/modal/certrev-cert-modal.test.ts +0 -125
- package/src/modal/ftc-guard.test.ts +0 -153
- package/src/modal/interactions.test.ts +0 -250
- package/src/modal/placement.test.ts +0 -398
- package/src/modal/revalidate.test.ts +0 -111
- package/src/modal/verify-client.test.ts +0 -164
package/dist/verify/cache.js
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* still recovering quickly (short negative TTL).
|
|
14
14
|
*
|
|
15
15
|
* It is deliberately tiny + dependency-free (a Map + timestamps) so it runs in any server
|
|
16
|
-
* runtime (Node, edge, workerd). For multi-instance deployments this is per-instance L1
|
|
17
|
-
*
|
|
18
|
-
* `lifecycle.revision
|
|
16
|
+
* runtime (Node, edge, workerd). For multi-instance deployments this is per-instance L1 and
|
|
17
|
+
* nothing more: each instance keeps its own copy, so an L2 (the CDN in front of the Delivery
|
|
18
|
+
* API, which is cacheable on `lifecycle.revision`, or a shared KV) is the caller's to add.
|
|
19
19
|
*/
|
|
20
20
|
export class TtlCache {
|
|
21
21
|
store = new Map();
|
|
@@ -48,8 +48,14 @@ export class TtlCache {
|
|
|
48
48
|
* TTL; if `isNegative(value)` returns true (e.g. a 'suppress' verdict) it's cached
|
|
49
49
|
* with the shorter negative TTL so a transient failure recovers fast. A thrown loader
|
|
50
50
|
* is NOT cached (it rejects all current waiters and the next call retries).
|
|
51
|
+
*
|
|
52
|
+
* `positiveTtlMs` overrides the cache's positive TTL FOR THIS WRITE only, so one caller
|
|
53
|
+
* can hold a verdict longer/shorter than the cache's default without owning the cache.
|
|
54
|
+
* It deliberately does NOT apply to a negative result: a caller asking for a 10-minute
|
|
55
|
+
* positive TTL is asking for fewer origin reads, not for a transient failure to be
|
|
56
|
+
* remembered for ten minutes (see the negative-TTL rationale in the module header).
|
|
51
57
|
*/
|
|
52
|
-
async getOrLoad(key, loader, isNegative) {
|
|
58
|
+
async getOrLoad(key, loader, isNegative, positiveTtlMs) {
|
|
53
59
|
const cached = this.peek(key);
|
|
54
60
|
if (cached !== undefined)
|
|
55
61
|
return cached;
|
|
@@ -59,7 +65,7 @@ export class TtlCache {
|
|
|
59
65
|
const promise = (async () => {
|
|
60
66
|
const value = await loader();
|
|
61
67
|
const negative = isNegative ? isNegative(value) : false;
|
|
62
|
-
this.set(key, value, negative);
|
|
68
|
+
this.set(key, value, negative, negative ? undefined : positiveTtlMs);
|
|
63
69
|
return value;
|
|
64
70
|
})().finally(() => {
|
|
65
71
|
this.inflight.delete(key);
|
|
@@ -67,21 +73,36 @@ export class TtlCache {
|
|
|
67
73
|
this.inflight.set(key, promise);
|
|
68
74
|
return promise;
|
|
69
75
|
}
|
|
70
|
-
/** Insert/overwrite an entry
|
|
71
|
-
|
|
76
|
+
/** Insert/overwrite an entry. `ttlMsOverride` replaces the TTL this write would otherwise
|
|
77
|
+
* get (positive or negative); omit it for the cache's configured TTLs. */
|
|
78
|
+
set(key, value, negative = false, ttlMsOverride) {
|
|
72
79
|
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
73
80
|
// Evict the oldest inserted key (Map preserves insertion order).
|
|
74
81
|
const oldest = this.store.keys().next().value;
|
|
75
82
|
if (oldest !== undefined)
|
|
76
83
|
this.store.delete(oldest);
|
|
77
84
|
}
|
|
78
|
-
const
|
|
85
|
+
const configured = negative ? this.negativeTtlMs : this.ttlMs;
|
|
86
|
+
const ttl = ttlMsOverride !== undefined && Number.isFinite(ttlMsOverride) ? ttlMsOverride : configured;
|
|
79
87
|
this.store.set(key, { value, expiresAt: this.now() + ttl, negative });
|
|
80
88
|
}
|
|
81
89
|
/** Drop a key (e.g. on a known revocation push). */
|
|
82
90
|
delete(key) {
|
|
83
91
|
this.store.delete(key);
|
|
84
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Drop every key under a prefix. WHY it exists: a cache key identifies everything the
|
|
95
|
+
* cached value depends on, which for a verdict includes the live content hash the render
|
|
96
|
+
* observed — a fact a revocation webhook does not know. Invalidating by prefix lets the
|
|
97
|
+
* webhook drop a placement without having to reconstruct every render context that ever
|
|
98
|
+
* cached it.
|
|
99
|
+
*/
|
|
100
|
+
deletePrefix(prefix) {
|
|
101
|
+
for (const key of this.store.keys()) {
|
|
102
|
+
if (key.startsWith(prefix))
|
|
103
|
+
this.store.delete(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
85
106
|
clear() {
|
|
86
107
|
this.store.clear();
|
|
87
108
|
this.inflight.clear();
|
package/dist/verify/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/verify/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmBH,MAAM,OAAO,QAAQ;IACH,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAA;IACnC,QAAQ,GAAG,IAAI,GAAG,EAAsB,CAAA;IACxC,KAAK,CAAQ;IACb,aAAa,CAAQ;IACrB,UAAU,CAAQ;IAClB,GAAG,CAAc;IAElC,YAAY,OAAwB,EAAE;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAA;QACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,KAAK,CAAA;QAChD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAA;QACzC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IAChC,CAAC;IAED,kFAAkF;IAClF,IAAI,CAAC,GAAW;QACf,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;QACxB,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC/B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACtB,OAAO,SAAS,CAAA;QACjB,CAAC;QACD,OAAO,CAAC,CAAC,KAAK,CAAA;IACf,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/verify/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetchWithDeadline` — every network read on the render path gets a deadline.
|
|
3
|
+
*
|
|
4
|
+
* WHY: the verify layer runs INSIDE a server render (a Hydrogen loader, a Next server
|
|
5
|
+
* component, a Builder server fetch). A hung origin with no deadline holds that render open
|
|
6
|
+
* for as long as the runtime's socket timeout allows — one confirmed case was still pending
|
|
7
|
+
* at 45 SECONDS, i.e. a page that never answered because a decorative badge could not be
|
|
8
|
+
* verified. A badge is never worth a page: past the deadline the request is ABORTED (the
|
|
9
|
+
* connection goes back) and the caller fails closed, the same safe outcome as any other
|
|
10
|
+
* fetch failure.
|
|
11
|
+
*
|
|
12
|
+
* Two deliberate choices:
|
|
13
|
+
* • The deadline is enforced by a RACE, not by the AbortSignal alone. A `fetchImpl` that
|
|
14
|
+
* ignores its signal (a wrapper, a proxy, a non-conforming polyfill) must not be able to
|
|
15
|
+
* hold a render open. The abort still fires, so a conforming implementation also releases
|
|
16
|
+
* the socket rather than merely being abandoned.
|
|
17
|
+
* • `setTimeout` + `AbortController`, not `AbortSignal.timeout()`. The one-liner reads
|
|
18
|
+
* better but its timer is not the one a test's fake clock controls, and a deadline that
|
|
19
|
+
* can only be tested by really waiting is a deadline nobody keeps tested.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Default deadline for a render-path fetch. The Delivery API is CDN-cacheable and answers
|
|
23
|
+
* well under a second at p99; three seconds is generous for a cold origin and still bounded
|
|
24
|
+
* by what a page can afford to spend on a decoration whose failure mode is "render nothing".
|
|
25
|
+
* Callers on a tighter TTFB budget pass their own.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEFAULT_FETCH_TIMEOUT_MS = 3000;
|
|
28
|
+
/**
|
|
29
|
+
* `fetchImpl(url, init)` with a hard deadline. Rejects when the deadline passes (after
|
|
30
|
+
* aborting the request); every caller on the verify path treats that rejection like any other
|
|
31
|
+
* fetch failure — fail closed. A non-finite or non-positive `timeoutMs` is an explicit opt-out
|
|
32
|
+
* (no deadline), for a caller that owns its own cancellation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fetchWithDeadline(fetchImpl: typeof fetch, url: string, init: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
35
|
+
//# sourceMappingURL=fetch-with-deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-with-deadline.d.ts","sourceRoot":"","sources":["../../src/verify/fetch-with-deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAAQ,CAAA;AAE7C;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,SAAS,EAAE,OAAO,KAAK,EACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,SAAS,GAAE,MAAiC,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAmBnB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetchWithDeadline` — every network read on the render path gets a deadline.
|
|
3
|
+
*
|
|
4
|
+
* WHY: the verify layer runs INSIDE a server render (a Hydrogen loader, a Next server
|
|
5
|
+
* component, a Builder server fetch). A hung origin with no deadline holds that render open
|
|
6
|
+
* for as long as the runtime's socket timeout allows — one confirmed case was still pending
|
|
7
|
+
* at 45 SECONDS, i.e. a page that never answered because a decorative badge could not be
|
|
8
|
+
* verified. A badge is never worth a page: past the deadline the request is ABORTED (the
|
|
9
|
+
* connection goes back) and the caller fails closed, the same safe outcome as any other
|
|
10
|
+
* fetch failure.
|
|
11
|
+
*
|
|
12
|
+
* Two deliberate choices:
|
|
13
|
+
* • The deadline is enforced by a RACE, not by the AbortSignal alone. A `fetchImpl` that
|
|
14
|
+
* ignores its signal (a wrapper, a proxy, a non-conforming polyfill) must not be able to
|
|
15
|
+
* hold a render open. The abort still fires, so a conforming implementation also releases
|
|
16
|
+
* the socket rather than merely being abandoned.
|
|
17
|
+
* • `setTimeout` + `AbortController`, not `AbortSignal.timeout()`. The one-liner reads
|
|
18
|
+
* better but its timer is not the one a test's fake clock controls, and a deadline that
|
|
19
|
+
* can only be tested by really waiting is a deadline nobody keeps tested.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Default deadline for a render-path fetch. The Delivery API is CDN-cacheable and answers
|
|
23
|
+
* well under a second at p99; three seconds is generous for a cold origin and still bounded
|
|
24
|
+
* by what a page can afford to spend on a decoration whose failure mode is "render nothing".
|
|
25
|
+
* Callers on a tighter TTFB budget pass their own.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 3_000;
|
|
28
|
+
/**
|
|
29
|
+
* `fetchImpl(url, init)` with a hard deadline. Rejects when the deadline passes (after
|
|
30
|
+
* aborting the request); every caller on the verify path treats that rejection like any other
|
|
31
|
+
* fetch failure — fail closed. A non-finite or non-positive `timeoutMs` is an explicit opt-out
|
|
32
|
+
* (no deadline), for a caller that owns its own cancellation.
|
|
33
|
+
*/
|
|
34
|
+
export async function fetchWithDeadline(fetchImpl, url, init, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
35
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
36
|
+
return fetchImpl(url, init);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
let timer;
|
|
39
|
+
const deadline = new Promise((_, reject) => {
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
controller.abort();
|
|
42
|
+
reject(new Error(`cert-block: fetch exceeded its ${timeoutMs}ms deadline — ${url}`));
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
// Promise.race subscribes to both, so an abort-rejection that lands after the race has
|
|
47
|
+
// settled is still handled — it never surfaces as an unhandled rejection.
|
|
48
|
+
return await Promise.race([fetchImpl(url, { ...init, signal: controller.signal }), deadline]);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=fetch-with-deadline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-with-deadline.js","sourceRoot":"","sources":["../../src/verify/fetch-with-deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,SAAuB,EACvB,GAAW,EACX,IAAiB,EACjB,YAAoB,wBAAwB;IAE5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IAE9E,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,IAAI,KAAgD,CAAA;IACpD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QACjD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACvB,UAAU,CAAC,KAAK,EAAE,CAAA;YAClB,MAAM,CAAC,IAAI,KAAK,CAAC,kCAAkC,SAAS,iBAAiB,GAAG,EAAE,CAAC,CAAC,CAAA;QACrF,CAAC,EAAE,SAAS,CAAC,CAAA;IACd,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC;QACJ,uFAAuF;QACvF,0EAA0E;QAC1E,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC9F,CAAC;YAAS,CAAC;QACV,IAAI,KAAK,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC;AACF,CAAC"}
|
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
* JSON-LD; `{ decision: 'suppress', reason }` → render NOTHING. Any error (network,
|
|
17
17
|
* malformed JSON, throwing resolver) collapses to a `suppress` verdict — never throws
|
|
18
18
|
* into the render path, never renders an unverified credential.
|
|
19
|
+
*
|
|
20
|
+
* `verdict.reason` is the ONLY signal a consumer gets when a badge does not appear (this
|
|
21
|
+
* package logs nothing), so every failure path here reports what ACTUALLY failed rather than
|
|
22
|
+
* a convenient catch-all. Every fetch also carries a deadline: a hung origin fails closed at
|
|
23
|
+
* `timeoutMs` instead of holding the render open (see ./fetch-with-deadline).
|
|
19
24
|
*/
|
|
20
|
-
import type {
|
|
25
|
+
import type { CertDeliveryArtifact, CertVerdict, RenderContext, ResolvePublicKeyByKid } from '../contract/kernel.js';
|
|
21
26
|
import { TtlCache } from './cache.js';
|
|
22
27
|
/** Where the signed envelope comes from. Exactly one of the two source shapes. */
|
|
23
28
|
export type EnvelopeSource =
|
|
@@ -29,27 +34,31 @@ export type EnvelopeSource =
|
|
|
29
34
|
readonly platform: string;
|
|
30
35
|
readonly externalId: string;
|
|
31
36
|
}
|
|
32
|
-
/** PUSH/native: an
|
|
37
|
+
/** PUSH/native: an artifact already read from a Shopify app-owned metafield (or any
|
|
33
38
|
* native store). The caller fetched it via the Storefront API; we just verify it. The
|
|
34
|
-
* value may be the parsed
|
|
39
|
+
* value may be the parsed artifact — a full envelope OR the slim revocation tombstone —
|
|
40
|
+
* or its JSON string (metafields store strings). */
|
|
35
41
|
| {
|
|
36
42
|
readonly kind: 'metafield';
|
|
37
|
-
readonly value:
|
|
43
|
+
readonly value: CertDeliveryArtifact | string | null | undefined;
|
|
38
44
|
};
|
|
39
45
|
export interface GetVerifiedEnvelopeOptions {
|
|
40
46
|
readonly source: EnvelopeSource;
|
|
41
47
|
/** kid → public-key resolver (see ./resolve-kid). Required: no verify without keys. */
|
|
42
48
|
readonly resolveKid: ResolvePublicKeyByKid;
|
|
43
49
|
/** Render context: the platform + externalId this edge IS, plus optional live hash. */
|
|
44
|
-
readonly context:
|
|
45
|
-
readonly now?: Date;
|
|
46
|
-
};
|
|
50
|
+
readonly context: RenderContext;
|
|
47
51
|
/** Injectable fetch for tests / non-global-fetch runtimes. */
|
|
48
52
|
readonly fetchImpl?: typeof fetch;
|
|
49
53
|
/** Cache override (per-instance). Defaults to the module-level shared cache. */
|
|
50
54
|
readonly cache?: TtlCache<CertVerdict>;
|
|
51
|
-
/**
|
|
55
|
+
/** TTL in ms for a RENDER verdict written by this call (default: the cache's own positive
|
|
56
|
+
* TTL — 60_000 on the shared cache). A suppress verdict keeps the cache's short negative
|
|
57
|
+
* TTL regardless, so a longer `ttlMs` can never make a transient failure sticky. */
|
|
52
58
|
readonly ttlMs?: number;
|
|
59
|
+
/** Deadline in ms for the Delivery API fetch (default `DEFAULT_FETCH_TIMEOUT_MS`, 3s).
|
|
60
|
+
* Ignored by the metafield source, which does no I/O. */
|
|
61
|
+
readonly timeoutMs?: number;
|
|
53
62
|
}
|
|
54
63
|
/** Module-level shared cache so all calls in a process coordinate by default. */
|
|
55
64
|
declare const sharedVerdictCache: TtlCache<CertVerdict>;
|
|
@@ -59,7 +68,12 @@ declare const sharedVerdictCache: TtlCache<CertVerdict>;
|
|
|
59
68
|
* single-flight keep the origin load bounded.
|
|
60
69
|
*/
|
|
61
70
|
export declare function getVerifiedEnvelope(opts: GetVerifiedEnvelopeOptions): Promise<CertVerdict>;
|
|
62
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Expose the shared cache so a revocation webhook handler can invalidate proactively. Drops
|
|
73
|
+
* EVERY entry for this source at this placement, whatever live content hash each was cached
|
|
74
|
+
* under — the webhook knows the article, never the body hash a given render observed, and an
|
|
75
|
+
* invalidation that misses because of a hash mismatch is a revoked cert that keeps rendering.
|
|
76
|
+
*/
|
|
63
77
|
export declare function invalidateVerdict(source: EnvelopeSource, context: RenderContext, cache?: TtlCache<CertVerdict>): void;
|
|
64
78
|
export { sharedVerdictCache };
|
|
65
79
|
//# sourceMappingURL=get-verified-envelope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-verified-envelope.d.ts","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"get-verified-envelope.d.ts","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACX,oBAAoB,EAEpB,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,kFAAkF;AAClF,MAAM,MAAM,cAAc;AACzB,2EAA2E;AACzE;IACA,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC1B;AACH;;;qDAGqD;GACnD;IACA,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAC/D,CAAA;AAEJ,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAA;IAC1C,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;IACtC;;yFAEqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB;8DAC0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,iFAAiF;AACjF,QAAA,MAAM,kBAAkB,uBAAqE,CAAA;AAwI7F;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BhG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,aAAa,EACtB,KAAK,GAAE,QAAQ,CAAC,WAAW,CAAsB,GAC/C,IAAI,CAEN;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -16,24 +16,34 @@
|
|
|
16
16
|
* JSON-LD; `{ decision: 'suppress', reason }` → render NOTHING. Any error (network,
|
|
17
17
|
* malformed JSON, throwing resolver) collapses to a `suppress` verdict — never throws
|
|
18
18
|
* into the render path, never renders an unverified credential.
|
|
19
|
+
*
|
|
20
|
+
* `verdict.reason` is the ONLY signal a consumer gets when a badge does not appear (this
|
|
21
|
+
* package logs nothing), so every failure path here reports what ACTUALLY failed rather than
|
|
22
|
+
* a convenient catch-all. Every fetch also carries a deadline: a hung origin fails closed at
|
|
23
|
+
* `timeoutMs` instead of holding the render open (see ./fetch-with-deadline).
|
|
19
24
|
*/
|
|
20
|
-
import {
|
|
25
|
+
import { verifyArtifact } from '../contract/kernel.js';
|
|
21
26
|
import { TtlCache } from './cache.js';
|
|
27
|
+
import { DEFAULT_FETCH_TIMEOUT_MS, fetchWithDeadline } from './fetch-with-deadline.js';
|
|
22
28
|
/** Module-level shared cache so all calls in a process coordinate by default. */
|
|
23
29
|
const sharedVerdictCache = new TtlCache({ ttlMs: 60_000, negativeTtlMs: 5_000 });
|
|
24
30
|
function suppress(reason) {
|
|
25
31
|
return { decision: 'suppress', reason };
|
|
26
32
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
/** Read the artifact out of a metafield value. Never throws. */
|
|
34
|
+
function parseMetafieldValue(value) {
|
|
35
|
+
// Absent — never certified, or the app cleared the metafield (Shopify writes an empty
|
|
36
|
+
// string rather than deleting it). Nothing is published for this subject.
|
|
37
|
+
if (value == null || (typeof value === 'string' && value.trim() === '')) {
|
|
38
|
+
return { ok: false, reason: 'subject_mismatch' };
|
|
39
|
+
}
|
|
30
40
|
if (typeof value !== 'string')
|
|
31
|
-
return value;
|
|
41
|
+
return { ok: true, artifact: value };
|
|
32
42
|
try {
|
|
33
|
-
return JSON.parse(value);
|
|
43
|
+
return { ok: true, artifact: JSON.parse(value) };
|
|
34
44
|
}
|
|
35
45
|
catch {
|
|
36
|
-
return
|
|
46
|
+
return { ok: false, reason: 'unsupported_contract_version' };
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
function deliveryApiUrl(s) {
|
|
@@ -63,25 +73,64 @@ function metafieldValueKey(value) {
|
|
|
63
73
|
const s = typeof value === 'string' ? value : JSON.stringify(value);
|
|
64
74
|
return fnv1a(s);
|
|
65
75
|
}
|
|
66
|
-
/**
|
|
76
|
+
/** WHICH artifact is being verified. For the delivery source that is the full URL — the base
|
|
77
|
+
* URL is part of the identity (a staging portal and production serve DIFFERENT envelopes for
|
|
78
|
+
* the same platform/externalId); for a metafield it's a hash of the value the caller passed. */
|
|
79
|
+
function sourceKey(source) {
|
|
80
|
+
return source.kind === 'delivery_api' ? `api:${deliveryApiUrl(source)}` : `mf:${metafieldValueKey(source.value)}`;
|
|
81
|
+
}
|
|
82
|
+
/** WHERE it is being rendered. `now` is deliberately NOT part of the key: elapsed time is what
|
|
83
|
+
* the TTL already bounds, and keying on an injected clock would turn the cache into a
|
|
84
|
+
* permanent miss for every caller that passes one. */
|
|
85
|
+
function placementKey(ctx) {
|
|
86
|
+
return `${encodeURIComponent(ctx.platform)}:${encodeURIComponent(ctx.externalId)}`;
|
|
87
|
+
}
|
|
88
|
+
/** Everything in the key ABOVE the live content hash — one placement, all of its hashes. */
|
|
89
|
+
function placementPrefix(source, ctx) {
|
|
90
|
+
return `${sourceKey(source)}|${placementKey(ctx)}|`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Stable cache key per (artifact identity × placement × live content hash) — i.e. every input
|
|
94
|
+
* the verdict actually depends on. Both source branches now key on the same three things: the
|
|
95
|
+
* old delivery key (`api:<platform>:<externalId>`) named neither the base URL nor the render
|
|
96
|
+
* context, so one entry was shared by a staging and a production portal, and — the dangerous
|
|
97
|
+
* one — a verdict computed while the article matched its `contentDigest` was served for a
|
|
98
|
+
* later render whose body had DRIFTED, rendering a credential the drift check would have
|
|
99
|
+
* suppressed. The live hash goes LAST so `invalidateVerdict` can drop a placement by prefix.
|
|
100
|
+
*/
|
|
67
101
|
function cacheKey(source, ctx) {
|
|
68
|
-
|
|
69
|
-
return `api:${source.platform}:${source.externalId}`;
|
|
70
|
-
}
|
|
71
|
-
// Metafield: key on the rendering identity AND a short hash of the envelope value, so a
|
|
72
|
-
// push update (new envelope at the same placement) busts the entry, and verifying
|
|
73
|
-
// different envelopes against one render context never collides on a stale verdict.
|
|
74
|
-
return `mf:${ctx.platform}:${ctx.externalId}:${metafieldValueKey(source.value)}`;
|
|
102
|
+
return `${placementPrefix(source, ctx)}${ctx.liveContentHash ?? '-'}`;
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
/** Fetch the artifact from the Delivery API. Never throws: every failure comes back as the
|
|
105
|
+
* truthful suppress reason (see `SourceOutcome`). */
|
|
106
|
+
async function fetchFromDeliveryApi(s, fetchImpl, timeoutMs) {
|
|
107
|
+
let res;
|
|
108
|
+
try {
|
|
109
|
+
res = await fetchWithDeadline(fetchImpl, deliveryApiUrl(s), { headers: { accept: 'application/json' } }, timeoutMs);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Unreachable origin, DNS/TLS failure, or the deadline above. No transport member
|
|
113
|
+
// exists in the contract enum; this is the least-wrong one.
|
|
114
|
+
return { ok: false, reason: 'unsupported_contract_version' };
|
|
115
|
+
}
|
|
116
|
+
if (res.status === 404 || res.status === 410) {
|
|
117
|
+
// The API serves nothing for this (platform, externalId): never certified, or the
|
|
118
|
+
// placement was withdrawn. Revocation of a LIVE cert comes back 200 with a tombstone
|
|
119
|
+
// so a 4xx here is genuinely "no credential for this subject" — which is
|
|
120
|
+
// also the question the consumer needs to ask (is this the externalId I think it is?).
|
|
121
|
+
return { ok: false, reason: 'subject_mismatch' };
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
// 5xx / 429 / anything else: the origin is failing, not the credential. Transport gap
|
|
125
|
+
// again — same least-wrong member, and the short negative TTL keeps it recoverable.
|
|
126
|
+
return { ok: false, reason: 'unsupported_contract_version' };
|
|
127
|
+
}
|
|
80
128
|
try {
|
|
81
|
-
return (await res.json());
|
|
129
|
+
return { ok: true, artifact: (await res.json()) };
|
|
82
130
|
}
|
|
83
131
|
catch {
|
|
84
|
-
|
|
132
|
+
// A body arrived and could not be parsed — here the reason is literally true.
|
|
133
|
+
return { ok: false, reason: 'unsupported_contract_version' };
|
|
85
134
|
}
|
|
86
135
|
}
|
|
87
136
|
/**
|
|
@@ -96,33 +145,33 @@ export async function getVerifiedEnvelope(opts) {
|
|
|
96
145
|
const key = cacheKey(opts.source, ctx);
|
|
97
146
|
const isSuppress = (v) => v.decision !== 'render';
|
|
98
147
|
return cache.getOrLoad(key, async () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
if (!envelope) {
|
|
111
|
-
// No envelope present (never certified / revoked + cleared / 404) → suppress.
|
|
112
|
-
return suppress('unsupported_contract_version');
|
|
113
|
-
}
|
|
148
|
+
const sourced = opts.source.kind === 'delivery_api'
|
|
149
|
+
? await fetchFromDeliveryApi(opts.source, fetchImpl, opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
|
|
150
|
+
: parseMetafieldValue(opts.source.value);
|
|
151
|
+
if (!sourced.ok)
|
|
152
|
+
return suppress(sourced.reason);
|
|
153
|
+
// `verifyArtifact` — not `verifyEnvelope` — because the Delivery API answers a
|
|
154
|
+
// revoked cert with the slim signed TOMBSTONE. The envelope-only kernel
|
|
155
|
+
// still fails closed on one, but it reports it as an unparseable envelope; the
|
|
156
|
+
// artifact kernel verifies the tombstone's own signature + subject and says
|
|
157
|
+
// 'revoked', which is both true and the thing a consumer needs to hear.
|
|
114
158
|
// The kernel itself never throws (it fails closed), but guard the resolver too.
|
|
115
159
|
try {
|
|
116
|
-
return await
|
|
160
|
+
return await verifyArtifact(sourced.artifact, opts.resolveKid, ctx);
|
|
117
161
|
}
|
|
118
162
|
catch {
|
|
119
163
|
return suppress('unknown_key');
|
|
120
164
|
}
|
|
121
|
-
}, isSuppress);
|
|
165
|
+
}, isSuppress, opts.ttlMs);
|
|
122
166
|
}
|
|
123
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Expose the shared cache so a revocation webhook handler can invalidate proactively. Drops
|
|
169
|
+
* EVERY entry for this source at this placement, whatever live content hash each was cached
|
|
170
|
+
* under — the webhook knows the article, never the body hash a given render observed, and an
|
|
171
|
+
* invalidation that misses because of a hash mismatch is a revoked cert that keeps rendering.
|
|
172
|
+
*/
|
|
124
173
|
export function invalidateVerdict(source, context, cache = sharedVerdictCache) {
|
|
125
|
-
cache.
|
|
174
|
+
cache.deletePrefix(placementPrefix(source, context));
|
|
126
175
|
}
|
|
127
176
|
export { sharedVerdictCache };
|
|
128
177
|
//# sourceMappingURL=get-verified-envelope.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-verified-envelope.js","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"get-verified-envelope.js","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,9 @@ export interface FetchingKidResolverOptions {
|
|
|
26
26
|
readonly jwksUrl: string;
|
|
27
27
|
/** Key-set cache TTL in ms (default 1h — keys rotate rarely). */
|
|
28
28
|
readonly ttlMs?: number;
|
|
29
|
+
/** Deadline in ms for the JWKS fetch (default `DEFAULT_FETCH_TIMEOUT_MS`, 3s). This fetch
|
|
30
|
+
* is on the render path too — a hung key endpoint must not hold a page open. */
|
|
31
|
+
readonly timeoutMs?: number;
|
|
29
32
|
/** Injectable fetch for tests / non-global-fetch runtimes. */
|
|
30
33
|
readonly fetchImpl?: typeof fetch;
|
|
31
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-kid.d.ts","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"resolve-kid.d.ts","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAIzF,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,CAAC,CAAC,CAAA;AAOnF,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,qBAAqB,CAK3E;AAgCD,MAAM,WAAW,0BAA0B;IAC1C,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB;qFACiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,qBAAqB,CAiC3F"}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { base64urlDecode } from '@certrev/cert-contract';
|
|
20
20
|
import { TtlCache } from './cache.js';
|
|
21
|
+
import { DEFAULT_FETCH_TIMEOUT_MS, fetchWithDeadline } from './fetch-with-deadline.js';
|
|
21
22
|
function asInput(v) {
|
|
22
23
|
// A bare string is treated as PEM (the common deploy-config form).
|
|
23
24
|
return typeof v === 'string' ? { format: 'pem', pem: v } : v;
|
|
@@ -54,14 +55,22 @@ export function fetchingKidResolver(opts) {
|
|
|
54
55
|
const CACHE_KEY = opts.jwksUrl;
|
|
55
56
|
async function loadKeySet() {
|
|
56
57
|
const map = new Map();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetchWithDeadline(fetchImpl, opts.jwksUrl, { headers: { accept: 'application/json' } }, opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return map; // empty → every kid resolves null → fail closed
|
|
62
|
+
const doc = (await res.json());
|
|
63
|
+
for (const jwk of doc.keys ?? []) {
|
|
64
|
+
const input = jwkToInput(jwk);
|
|
65
|
+
if (input && jwk.kid)
|
|
66
|
+
map.set(jwk.kid, input);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Unreachable / deadline / unreadable body → the EMPTY key set below, negative-cached
|
|
71
|
+
// briefly. The resolver itself never throws: a kid that cannot be resolved must come
|
|
72
|
+
// back as null ('unknown_key' — true) rather than as an exception the caller has to
|
|
73
|
+
// remember to catch.
|
|
65
74
|
}
|
|
66
75
|
return map;
|
|
67
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-kid.js","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"resolve-kid.js","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certrev-badge.d.ts","sourceRoot":"","sources":["../../src/webcomponent/certrev-badge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAI/E,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAQhD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAE3E;
|
|
1
|
+
{"version":3,"file":"certrev-badge.d.ts","sourceRoot":"","sources":["../../src/webcomponent/certrev-badge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAI/E,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAQhD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAE3E;AAaD,qBAAa,mBAAoB,SAAQ,WAAW;IACnD,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAED,iBAAiB,IAAI,IAAI;IAOzB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;YAQjF,oBAAoB;CAuClC;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"}
|
|
@@ -36,6 +36,16 @@ let globalResolveKid = null;
|
|
|
36
36
|
export function setCertRevKidResolver(resolver) {
|
|
37
37
|
globalResolveKid = resolver;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* `badge-style` is AUTHOR-typed markup, not signed data — a hand-written attribute on a page
|
|
41
|
+
* we don't control. Only the two real faces are honored; anything else (`"Full"`, a stray
|
|
42
|
+
* value, an empty attribute) reads as ABSENT, so the signed display config decides. The
|
|
43
|
+
* attribute was previously CAST (`as 'full' | 'compact' | null`), which let a typo travel
|
|
44
|
+
* into the renderer as if it were a face.
|
|
45
|
+
*/
|
|
46
|
+
function badgeStyleAttribute(value) {
|
|
47
|
+
return value === 'full' || value === 'compact' ? value : undefined;
|
|
48
|
+
}
|
|
39
49
|
export class CertRevBadgeElement extends HTMLElement {
|
|
40
50
|
static get observedAttributes() {
|
|
41
51
|
return ['delivery-api', 'platform', 'external-id', 'accent-color', 'badge-style', 'content-hash'];
|
|
@@ -83,8 +93,20 @@ export class CertRevBadgeElement extends HTMLElement {
|
|
|
83
93
|
if (verdict.decision !== 'render')
|
|
84
94
|
return;
|
|
85
95
|
const accentColor = this.getAttribute('accent-color') ?? undefined;
|
|
86
|
-
const badgeStyle = this.getAttribute('badge-style')
|
|
87
|
-
|
|
96
|
+
const badgeStyle = badgeStyleAttribute(this.getAttribute('badge-style'));
|
|
97
|
+
// The render is INSIDE the fail-closed boundary too. `connectedCallback` `void`s this
|
|
98
|
+
// promise, so a throw from the renderer (a payload shape nothing runtime-validates —
|
|
99
|
+
// the kernel checks the signature, never `content`) would escape as an unhandled
|
|
100
|
+
// rejection instead of rendering nothing, which is the one behaviour this element
|
|
101
|
+
// documents. Compute first, assign after, so a failure leaves the element untouched.
|
|
102
|
+
let html;
|
|
103
|
+
try {
|
|
104
|
+
html = renderBadgeHtml(verdict.payload, { accentColor, badgeStyle });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return; // fail closed
|
|
108
|
+
}
|
|
109
|
+
this.innerHTML = html;
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
/** Register the element. Idempotent + safe to call only in a browser/customElements env. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certrev-badge.js","sourceRoot":"","sources":["../../src/webcomponent/certrev-badge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAEhD;;;;GAIG;AACH,IAAI,gBAAgB,GAAiC,IAAI,CAAA;AACzD,MAAM,UAAU,qBAAqB,CAAC,QAA+B;IACpE,gBAAgB,GAAG,QAAQ,CAAA;AAC5B,CAAC;AAED,MAAM,OAAO,mBAAoB,SAAQ,WAAW;IACnD,MAAM,KAAK,kBAAkB;QAC5B,OAAO,CAAC,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,CAAC,CAAA;IAClG,CAAC;IAED,iBAAiB;QAChB,qFAAqF;QACrF,8EAA8E;QAC9E,IAAI,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC;YAAE,OAAM;QAChD,KAAK,IAAI,CAAC,oBAAoB,EAAE,CAAA;IACjC,CAAC;IAED,wBAAwB,CAAC,KAAa,EAAE,QAAuB,EAAE,QAAuB;QACvF,IAAI,QAAQ,KAAK,QAAQ;YAAE,OAAM;QACjC,mEAAmE;QACnE,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC/D,KAAK,IAAI,CAAC,oBAAoB,EAAE,CAAA;QACjC,CAAC;IACF,CAAC;IAEO,KAAK,CAAC,oBAAoB;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,CAAA;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAA;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,CAAA;QACnD,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU;YAAE,OAAM,CAAC,oCAAoC;QACrF,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACvB,qEAAqE;YACrE,OAAM;QACP,CAAC;QACD,IAAI,OAAoB,CAAA;QACxB,IAAI,CAAC;YACJ,OAAO,GAAG,MAAM,mBAAmB,CAAC;gBACnC,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE;gBAC/D,UAAU,EAAE,gBAAgB;gBAC5B,OAAO,EAAE;oBACR,QAAQ;oBACR,UAAU;oBACV,eAAe,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC;iBAClD;aACD,CAAC,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACR,OAAM,CAAC,cAAc;QACtB,CAAC;QACD,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAM;QACzC,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,IAAI,SAAS,CAAA;QAClE,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"certrev-badge.js","sourceRoot":"","sources":["../../src/webcomponent/certrev-badge.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@ export interface RenderBadgeOptions {
|
|
|
17
17
|
readonly accentColor?: string;
|
|
18
18
|
readonly badgeStyle?: 'full' | 'compact';
|
|
19
19
|
/**
|
|
20
|
-
* The brand render-def
|
|
20
|
+
* The brand render-def. ABSENT → the badge renders the CertREV preset
|
|
21
21
|
* default (theme output byte-identical to un-themed). PRESENT → validated
|
|
22
22
|
* themeable tokens are emitted as `--certrev-*` CSS vars + subtractive
|
|
23
23
|
* visibility applies. Mirrors `<CertBadge>`'s `renderDef` prop exactly.
|