@certrev/cert-block 0.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/README.md +154 -0
- package/dist/components/CertBadge.d.ts +29 -0
- package/dist/components/CertBadge.d.ts.map +1 -0
- package/dist/components/CertBadge.js +36 -0
- package/dist/components/CertBadge.js.map +1 -0
- package/dist/components/CertJsonLd.d.ts +23 -0
- package/dist/components/CertJsonLd.d.ts.map +1 -0
- package/dist/components/CertJsonLd.js +10 -0
- package/dist/components/CertJsonLd.js.map +1 -0
- package/dist/components/CertRevBacklink.d.ts +18 -0
- package/dist/components/CertRevBacklink.d.ts.map +1 -0
- package/dist/components/CertRevBacklink.js +16 -0
- package/dist/components/CertRevBacklink.js.map +1 -0
- package/dist/components/CertReview.d.ts +23 -0
- package/dist/components/CertReview.d.ts.map +1 -0
- package/dist/components/CertReview.js +11 -0
- package/dist/components/CertReview.js.map +1 -0
- package/dist/components/ExpertBio.d.ts +17 -0
- package/dist/components/ExpertBio.d.ts.map +1 -0
- package/dist/components/ExpertBio.js +17 -0
- package/dist/components/ExpertBio.js.map +1 -0
- package/dist/components/escape.d.ts +36 -0
- package/dist/components/escape.d.ts.map +1 -0
- package/dist/components/escape.js +76 -0
- package/dist/components/escape.js.map +1 -0
- package/dist/components/format.d.ts +22 -0
- package/dist/components/format.d.ts.map +1 -0
- package/dist/components/format.js +42 -0
- package/dist/components/format.js.map +1 -0
- package/dist/contract/fixtures.d.ts +36 -0
- package/dist/contract/fixtures.d.ts.map +1 -0
- package/dist/contract/fixtures.js +87 -0
- package/dist/contract/fixtures.js.map +1 -0
- package/dist/contract/kernel-contract.d.ts +154 -0
- package/dist/contract/kernel-contract.d.ts.map +1 -0
- package/dist/contract/kernel-contract.js +35 -0
- package/dist/contract/kernel-contract.js.map +1 -0
- package/dist/contract/kernel-stub.d.ts +44 -0
- package/dist/contract/kernel-stub.d.ts.map +1 -0
- package/dist/contract/kernel-stub.js +163 -0
- package/dist/contract/kernel-stub.js.map +1 -0
- package/dist/contract/kernel.d.ts +20 -0
- package/dist/contract/kernel.d.ts.map +1 -0
- package/dist/contract/kernel.js +19 -0
- package/dist/contract/kernel.js.map +1 -0
- package/dist/contract/verdict-kernel.d.ts +34 -0
- package/dist/contract/verdict-kernel.d.ts.map +1 -0
- package/dist/contract/verdict-kernel.js +13 -0
- package/dist/contract/verdict-kernel.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonld/project.d.ts +71 -0
- package/dist/jsonld/project.d.ts.map +1 -0
- package/dist/jsonld/project.js +183 -0
- package/dist/jsonld/project.js.map +1 -0
- package/dist/verify/cache.d.ts +56 -0
- package/dist/verify/cache.d.ts.map +1 -0
- package/dist/verify/cache.js +93 -0
- package/dist/verify/cache.js.map +1 -0
- package/dist/verify/get-verified-envelope.d.ts +65 -0
- package/dist/verify/get-verified-envelope.d.ts.map +1 -0
- package/dist/verify/get-verified-envelope.js +104 -0
- package/dist/verify/get-verified-envelope.js.map +1 -0
- package/dist/verify/resolve-kid.d.ts +38 -0
- package/dist/verify/resolve-kid.d.ts.map +1 -0
- package/dist/verify/resolve-kid.js +71 -0
- package/dist/verify/resolve-kid.js.map +1 -0
- package/dist/webcomponent/certrev-badge.d.ts +38 -0
- package/dist/webcomponent/certrev-badge.d.ts.map +1 -0
- package/dist/webcomponent/certrev-badge.js +98 -0
- package/dist/webcomponent/certrev-badge.js.map +1 -0
- package/dist/webcomponent/render-badge-html.d.ts +25 -0
- package/dist/webcomponent/render-badge-html.d.ts.map +1 -0
- package/dist/webcomponent/render-badge-html.js +81 -0
- package/dist/webcomponent/render-badge-html.js.map +1 -0
- package/package.json +70 -0
- package/src/__tests__/components.test.tsx +191 -0
- package/src/__tests__/project.test.ts +128 -0
- package/src/__tests__/verify.test.ts +203 -0
- package/src/__tests__/webcomponent.test.tsx +106 -0
- package/src/components/CertBadge.tsx +164 -0
- package/src/components/CertJsonLd.tsx +36 -0
- package/src/components/CertRevBacklink.tsx +63 -0
- package/src/components/CertReview.tsx +42 -0
- package/src/components/ExpertBio.tsx +77 -0
- package/src/components/escape.ts +72 -0
- package/src/components/format.ts +55 -0
- package/src/contract/fixtures.ts +107 -0
- package/src/contract/kernel.ts +20 -0
- package/src/contract/verdict-kernel.ts +47 -0
- package/src/index.ts +85 -0
- package/src/jsonld/project.ts +206 -0
- package/src/verify/cache.ts +116 -0
- package/src/verify/get-verified-envelope.ts +156 -0
- package/src/verify/resolve-kid.ts +103 -0
- package/src/webcomponent/certrev-badge.ts +100 -0
- package/src/webcomponent/render-badge-html.ts +106 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* Contract binding point — the SDK's single import of @certrev/cert-contract
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Every other SDK module imports the contract TYPES and the KERNEL functions from HERE,
|
|
7
|
+
* never directly from `@certrev/cert-contract`, so the whole SDK's dependency on the
|
|
8
|
+
* contract is funnelled through one module. The published package owns the envelope types
|
|
9
|
+
* (`CertDeliveryEnvelope`, `CertPayload`, `CertCredential`, …), the RFC-8785
|
|
10
|
+
* canonicalization (`canonicalPayloadBytes`), and the fail-closed kernel (`verifyEnvelope`,
|
|
11
|
+
* `renderVerdict`, `verifySignatureOnly`, `toEd25519PublicKey`).
|
|
12
|
+
*
|
|
13
|
+
* `VerdictKernel` is the ONE name NOT re-exported from the package: the contract exposes
|
|
14
|
+
* the kernel as free functions, and different edges bundle them with different key-resolver
|
|
15
|
+
* shapes (cert-block uses `Ed25519PublicKeyInput`; the portal Delivery API uses raw
|
|
16
|
+
* `Uint8Array`), so the bundled interface stays edge-local — see ./verdict-kernel.
|
|
17
|
+
*/
|
|
18
|
+
export * from '@certrev/cert-contract';
|
|
19
|
+
//# sourceMappingURL=kernel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kernel.js","sourceRoot":"","sources":["../../src/contract/kernel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,cAAc,wBAAwB,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerdictKernel — cert-block's edge-local view of the kernel function set.
|
|
3
|
+
*
|
|
4
|
+
* The published `@certrev/cert-contract` exposes the kernel as FREE FUNCTIONS
|
|
5
|
+
* (`verifyEnvelope`, `renderVerdict`, `verifySignatureOnly`), NOT as a bundled interface,
|
|
6
|
+
* because different edges bundle them with different key-resolver shapes: cert-block
|
|
7
|
+
* resolves keys as `Ed25519PublicKeyInput`, while the portal Delivery API's kernel
|
|
8
|
+
* resolves raw `Uint8Array`. There is therefore no single canonical `VerdictKernel`
|
|
9
|
+
* interface to live in the contract — each edge declares the bundled shape it depends on.
|
|
10
|
+
* This is cert-block's.
|
|
11
|
+
*/
|
|
12
|
+
import type { CertDeliveryEnvelope, CertPayload, CertSuppressReason, CertVerdict, RenderContext, ResolvePublicKeyByKid } from '@certrev/cert-contract';
|
|
13
|
+
/** Full kernel: verify signature, then apply subject/lifecycle/drift policy. */
|
|
14
|
+
export type VerifyEnvelope = (envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid, ctx: RenderContext) => Promise<CertVerdict>;
|
|
15
|
+
/** Phase-2-only policy over an already-verified payload. */
|
|
16
|
+
export type RenderVerdict = (payload: CertPayload, ctx: RenderContext) => CertVerdict;
|
|
17
|
+
/** Phase-1-only cryptographic verification. */
|
|
18
|
+
export type VerifySignatureOnly = (envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid) => Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: CertSuppressReason;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* The kernel function trio as a single named interface — the value-level surface for
|
|
26
|
+
* callers that want them as one object. `@certrev/cert-contract` exposes these as free
|
|
27
|
+
* functions; this is a convenience binding, deliberately kept out of the wire contract.
|
|
28
|
+
*/
|
|
29
|
+
export interface VerdictKernel {
|
|
30
|
+
readonly verifyEnvelope: VerifyEnvelope;
|
|
31
|
+
readonly renderVerdict: RenderVerdict;
|
|
32
|
+
readonly verifySignatureOnly: VerifySignatureOnly;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=verdict-kernel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict-kernel.d.ts","sourceRoot":"","sources":["../../src/contract/verdict-kernel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACX,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,MAAM,wBAAwB,CAAA;AAE/B,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,CAC5B,QAAQ,EAAE,oBAAoB,EAC9B,UAAU,EAAE,qBAAqB,EACjC,GAAG,EAAE,aAAa,KACd,OAAO,CAAC,WAAW,CAAC,CAAA;AAEzB,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,KAAK,WAAW,CAAA;AAErF,+CAA+C;AAC/C,MAAM,MAAM,mBAAmB,GAAG,CACjC,QAAQ,EAAE,oBAAoB,EAC9B,UAAU,EAAE,qBAAqB,KAC7B,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAA;AAEtE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAA;IACrC,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAA;CACjD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerdictKernel — cert-block's edge-local view of the kernel function set.
|
|
3
|
+
*
|
|
4
|
+
* The published `@certrev/cert-contract` exposes the kernel as FREE FUNCTIONS
|
|
5
|
+
* (`verifyEnvelope`, `renderVerdict`, `verifySignatureOnly`), NOT as a bundled interface,
|
|
6
|
+
* because different edges bundle them with different key-resolver shapes: cert-block
|
|
7
|
+
* resolves keys as `Ed25519PublicKeyInput`, while the portal Delivery API's kernel
|
|
8
|
+
* resolves raw `Uint8Array`. There is therefore no single canonical `VerdictKernel`
|
|
9
|
+
* interface to live in the contract — each edge declares the bundled shape it depends on.
|
|
10
|
+
* This is cert-block's.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=verdict-kernel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict-kernel.js","sourceRoot":"","sources":["../../src/contract/verdict-kernel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @certrev/cert-block — the headless / crypto_verify render edge for CertREV.
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* • React components — <CertBadge>, <ExpertBio>, <CertRevBacklink>, <CertJsonLd>,
|
|
6
|
+
* <CertReview> (composite). SSR-safe, theme-light, accessible, escaping every field.
|
|
7
|
+
* • JSON-LD projector — deterministic facts → schema.org Article+reviewedBy(Person)
|
|
8
|
+
* graph, mergeable by @id (won't collide with Yoast).
|
|
9
|
+
* • Verify layer — `getVerifiedEnvelope` (fetch from metafield OR Delivery API + run
|
|
10
|
+
* the fail-closed VerdictKernel + cache), kid resolvers, the TTL/single-flight cache.
|
|
11
|
+
* • Contract types — re-exported from `@certrev/cert-contract` (stubbed locally until
|
|
12
|
+
* that package publishes; see the contract binding point in ./contract/kernel).
|
|
13
|
+
*
|
|
14
|
+
* The Web Component (`<certrev-badge>`) ships from the './webcomponent' subpath export so
|
|
15
|
+
* importing the main entry doesn't touch `customElements` (which is undefined server-side).
|
|
16
|
+
*/
|
|
17
|
+
export { CertBadge, type CertBadgeProps } from './components/CertBadge.js';
|
|
18
|
+
export { CertJsonLd, type CertJsonLdProps } from './components/CertJsonLd.js';
|
|
19
|
+
export { CertRevBacklink, type CertRevBacklinkProps } from './components/CertRevBacklink.js';
|
|
20
|
+
export { CertReview, type CertReviewProps } from './components/CertReview.js';
|
|
21
|
+
export { ExpertBio, type ExpertBioProps } from './components/ExpertBio.js';
|
|
22
|
+
export { escapeAttribute, escapeHtml, safeCssColor, safeHttpUrl, tidyText } from './components/escape.js';
|
|
23
|
+
export { credentialSuffix, DEFAULT_ACCENT, expertNameWithCredentials, formatDate, type ResolvedDisplay, resolveDisplay, } from './components/format.js';
|
|
24
|
+
export { FIXTURE_KID, makeMockPayload, makeSignedEnvelope, type SignedEnvelopeFixture } from './contract/fixtures.js';
|
|
25
|
+
export { CANONICALIZATION, type CertContent, type CertCredential, type CertDeliveryEnvelope, type CertDisplayConfig, type CertLifecycle, type CertPayload, type CertSignature, type CertSubject, type CertSuppressReason, type CertVerdict, CONTRACT_VERSION, type ContractVersion, type Ed25519PublicKeyInput, type RenderContext, type ResolvePublicKeyByKid, renderVerdict, SIGNATURE_ALG, type SignatureAlg, type VerdictKernel, verifyEnvelope, verifySignatureOnly, } from './contract/kernel.js';
|
|
26
|
+
export { type JsonLdValue, type ProjectJsonLdOptions, projectCertJsonLd, projectCertJsonLdString, serializeJsonLdForScript, } from './jsonld/project.js';
|
|
27
|
+
export { TtlCache, type TtlCacheOptions } from './verify/cache.js';
|
|
28
|
+
export { type EnvelopeSource, type GetVerifiedEnvelopeOptions, getVerifiedEnvelope, invalidateVerdict, sharedVerdictCache, } from './verify/get-verified-envelope.js';
|
|
29
|
+
export { type FetchingKidResolverOptions, fetchingKidResolver, type StaticKeySet, staticKidResolver, } from './verify/resolve-kid.js';
|
|
30
|
+
export { type RenderBadgeOptions, renderBadgeHtml } from './webcomponent/render-badge-html.js';
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAC7E,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,iCAAiC,CAAA;AAC5F,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAC7E,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEzG,OAAO,EACN,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EACzB,UAAU,EACV,KAAK,eAAe,EACpB,cAAc,GACd,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAErH,OAAO,EACN,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,aAAa,EACb,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,cAAc,EACd,mBAAmB,GACnB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,iBAAiB,EACjB,uBAAuB,EACvB,wBAAwB,GACxB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,GAClB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EACN,KAAK,0BAA0B,EAC/B,mBAAmB,EACnB,KAAK,YAAY,EACjB,iBAAiB,GACjB,MAAM,yBAAyB,CAAA;AAEhC,OAAO,EAAE,KAAK,kBAAkB,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @certrev/cert-block — the headless / crypto_verify render edge for CertREV.
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* • React components — <CertBadge>, <ExpertBio>, <CertRevBacklink>, <CertJsonLd>,
|
|
6
|
+
* <CertReview> (composite). SSR-safe, theme-light, accessible, escaping every field.
|
|
7
|
+
* • JSON-LD projector — deterministic facts → schema.org Article+reviewedBy(Person)
|
|
8
|
+
* graph, mergeable by @id (won't collide with Yoast).
|
|
9
|
+
* • Verify layer — `getVerifiedEnvelope` (fetch from metafield OR Delivery API + run
|
|
10
|
+
* the fail-closed VerdictKernel + cache), kid resolvers, the TTL/single-flight cache.
|
|
11
|
+
* • Contract types — re-exported from `@certrev/cert-contract` (stubbed locally until
|
|
12
|
+
* that package publishes; see the contract binding point in ./contract/kernel).
|
|
13
|
+
*
|
|
14
|
+
* The Web Component (`<certrev-badge>`) ships from the './webcomponent' subpath export so
|
|
15
|
+
* importing the main entry doesn't touch `customElements` (which is undefined server-side).
|
|
16
|
+
*/
|
|
17
|
+
// ── React components ──────────────────────────────────────────────────────────────
|
|
18
|
+
export { CertBadge } from './components/CertBadge.js';
|
|
19
|
+
export { CertJsonLd } from './components/CertJsonLd.js';
|
|
20
|
+
export { CertRevBacklink } from './components/CertRevBacklink.js';
|
|
21
|
+
export { CertReview } from './components/CertReview.js';
|
|
22
|
+
export { ExpertBio } from './components/ExpertBio.js';
|
|
23
|
+
export { escapeAttribute, escapeHtml, safeCssColor, safeHttpUrl, tidyText } from './components/escape.js';
|
|
24
|
+
// ── Presentation helpers (shared with the Web Component) ────────────────────────────
|
|
25
|
+
export { credentialSuffix, DEFAULT_ACCENT, expertNameWithCredentials, formatDate, resolveDisplay, } from './components/format.js';
|
|
26
|
+
// ── Test/dev fixtures (mock facts + signed-envelope generator) ─────────────────────
|
|
27
|
+
export { FIXTURE_KID, makeMockPayload, makeSignedEnvelope } from './contract/fixtures.js';
|
|
28
|
+
// ── Contract surface (types + kernel) — re-export the binding point ────────────────
|
|
29
|
+
export { CANONICALIZATION, CONTRACT_VERSION, renderVerdict, SIGNATURE_ALG, verifyEnvelope, verifySignatureOnly, } from './contract/kernel.js';
|
|
30
|
+
// ── JSON-LD projector ───────────────────────────────────────────────────────────────
|
|
31
|
+
export { projectCertJsonLd, projectCertJsonLdString, serializeJsonLdForScript, } from './jsonld/project.js';
|
|
32
|
+
export { TtlCache } from './verify/cache.js';
|
|
33
|
+
// ── Verify layer ──────────────────────────────────────────────────────────────────
|
|
34
|
+
export { getVerifiedEnvelope, invalidateVerdict, sharedVerdictCache, } from './verify/get-verified-envelope.js';
|
|
35
|
+
export { fetchingKidResolver, staticKidResolver, } from './verify/resolve-kid.js';
|
|
36
|
+
// ── Web Component string renderer (the markup shared by SSR + the custom element) ──
|
|
37
|
+
export { renderBadgeHtml } from './webcomponent/render-badge-html.js';
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,qFAAqF;AACrF,OAAO,EAAE,SAAS,EAAuB,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,UAAU,EAAwB,MAAM,4BAA4B,CAAA;AAC7E,OAAO,EAAE,eAAe,EAA6B,MAAM,iCAAiC,CAAA;AAC5F,OAAO,EAAE,UAAU,EAAwB,MAAM,4BAA4B,CAAA;AAC7E,OAAO,EAAE,SAAS,EAAuB,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACzG,uFAAuF;AACvF,OAAO,EACN,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EACzB,UAAU,EAEV,cAAc,GACd,MAAM,wBAAwB,CAAA;AAC/B,sFAAsF;AACtF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAA8B,MAAM,wBAAwB,CAAA;AACrH,sFAAsF;AACtF,OAAO,EACN,gBAAgB,EAWhB,gBAAgB,EAKhB,aAAa,EACb,aAAa,EAGb,cAAc,EACd,mBAAmB,GACnB,MAAM,sBAAsB,CAAA;AAC7B,uFAAuF;AACvF,OAAO,EAGN,iBAAiB,EACjB,uBAAuB,EACvB,wBAAwB,GACxB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,QAAQ,EAAwB,MAAM,mBAAmB,CAAA;AAClE,qFAAqF;AACrF,OAAO,EAGN,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,GAClB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAEN,mBAAmB,EAEnB,iBAAiB,GACjB,MAAM,yBAAyB,CAAA;AAChC,sFAAsF;AACtF,OAAO,EAA2B,eAAe,EAAE,MAAM,qCAAqC,CAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* Deterministic JSON-LD projector — facts → schema.org graph (mergeable by @id)
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* INVARIANT #1 of the contract: the JSON-LD is PROJECTED from `payload.content` facts
|
|
7
|
+
* at render time, never stored in (or signed into) the envelope. This is that projector.
|
|
8
|
+
* It is DETERMINISTIC — same facts in → byte-identical graph out — so two edges (and a
|
|
9
|
+
* server pre-render) produce the same structured data, and a snapshot diff is meaningful.
|
|
10
|
+
*
|
|
11
|
+
* DESIGN GOAL: MERGE, don't collide.
|
|
12
|
+
* A brand page usually already emits an `Article` (or `BlogPosting`) graph — Yoast on
|
|
13
|
+
* WordPress, the theme on Shopify, the framework on headless. If we emit a SECOND
|
|
14
|
+
* top-level `Article`, crawlers see two competing primary entities. Instead we emit a
|
|
15
|
+
* `@graph` whose Article node carries the SAME `@id` the page's primary article uses
|
|
16
|
+
* (the page URL + `#article`, the Yoast convention) so a consumer MERGES our
|
|
17
|
+
* `reviewedBy` / `dateModified` into the existing node by `@id` rather than duplicating
|
|
18
|
+
* it. Our own nodes (the review, the expert Person, the CertREV org) use CertREV-scoped
|
|
19
|
+
* `@id`s that cannot clash with the host page's nodes.
|
|
20
|
+
*
|
|
21
|
+
* The Person we attach is the reviewing EXPERT (schema.org `reviewedBy`), which is the
|
|
22
|
+
* E-E-A-T signal CertREV exists to add — not the author. The author (content byline) is
|
|
23
|
+
* attached as `author` only when present + distinct.
|
|
24
|
+
*/
|
|
25
|
+
import type { CertPayload } from '../contract/kernel.js';
|
|
26
|
+
/** A JSON-LD node/graph value. Loose by design — this is wire JSON, not a TS model. */
|
|
27
|
+
export type JsonLdValue = Record<string, unknown>;
|
|
28
|
+
export interface ProjectJsonLdOptions {
|
|
29
|
+
/**
|
|
30
|
+
* The canonical page URL the article renders on. Used to derive the primary
|
|
31
|
+
* Article node `@id` (`${pageUrl}#article`) so we merge into the host page's graph
|
|
32
|
+
* instead of colliding. When omitted, the first `subject.canonicalUrls` entry is
|
|
33
|
+
* used; when there is none either, a CertREV-scoped article `@id` is derived from
|
|
34
|
+
* `verifyUrl` (still mergeable, just not aligned to a host node).
|
|
35
|
+
*/
|
|
36
|
+
readonly pageUrl?: string;
|
|
37
|
+
/**
|
|
38
|
+
* When true (default), emit the wrapping `{ "@context": "...", "@graph": [...] }`.
|
|
39
|
+
* When false, return just the array of nodes — for a caller that merges them into an
|
|
40
|
+
* existing `@graph` it already owns.
|
|
41
|
+
*/
|
|
42
|
+
readonly wrapGraph?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Project the verified certification facts into a schema.org `@graph`.
|
|
46
|
+
*
|
|
47
|
+
* @param payload The cryptographically-verified payload (the same facts the badge renders).
|
|
48
|
+
* @param opts Page URL for @id alignment + graph-wrapping control.
|
|
49
|
+
* @returns A `{ "@context", "@graph" }` object (wrapGraph !== false) OR a bare
|
|
50
|
+
* array of nodes (wrapGraph === false) for merge-into-existing-graph callers.
|
|
51
|
+
*/
|
|
52
|
+
export declare function projectCertJsonLd(payload: CertPayload, opts?: ProjectJsonLdOptions): JsonLdValue | JsonLdValue[];
|
|
53
|
+
/**
|
|
54
|
+
* Serialize the projected graph to the exact JSON string that goes inside a
|
|
55
|
+
* `<script type="application/ld+json">`. We use `JSON.stringify` with no extra
|
|
56
|
+
* whitespace for a compact, deterministic body. The string is then made safe to embed
|
|
57
|
+
* by `serializeJsonLdForScript` (which neutralizes `</script>`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function projectCertJsonLdString(payload: CertPayload, opts?: ProjectJsonLdOptions): string;
|
|
60
|
+
/**
|
|
61
|
+
* Serialize an arbitrary JSON-LD value for safe inclusion inside a `<script>` element.
|
|
62
|
+
* Inside `application/ld+json`, two HTML sequences can break out of the tag: `</script>`
|
|
63
|
+
* (closes the element early) and HTML-comment markers `<!--` / `-->` (some parsers treat
|
|
64
|
+
* a comment opened inside a script as suspending script-data parsing). A memo or name
|
|
65
|
+
* containing either is attacker-influenced text, so we neutralize BOTH angle brackets to
|
|
66
|
+
* their `\uXXXX` form. This is valid JSON that parses back to the identical string — the
|
|
67
|
+
* structured data is unchanged, but no `<…>`/`-->` sequence can terminate the script.
|
|
68
|
+
* Standard Next.js / Yoast hardening.
|
|
69
|
+
*/
|
|
70
|
+
export declare function serializeJsonLdForScript(value: unknown): string;
|
|
71
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/jsonld/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAErE,uFAAuF;AACvF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEjD,MAAM,WAAW,oBAAoB;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAC5B;AAuHD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAE,oBAAyB,GAAG,WAAW,GAAG,WAAW,EAAE,CASpH;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM,CAErG;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE/D"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* Deterministic JSON-LD projector — facts → schema.org graph (mergeable by @id)
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* INVARIANT #1 of the contract: the JSON-LD is PROJECTED from `payload.content` facts
|
|
7
|
+
* at render time, never stored in (or signed into) the envelope. This is that projector.
|
|
8
|
+
* It is DETERMINISTIC — same facts in → byte-identical graph out — so two edges (and a
|
|
9
|
+
* server pre-render) produce the same structured data, and a snapshot diff is meaningful.
|
|
10
|
+
*
|
|
11
|
+
* DESIGN GOAL: MERGE, don't collide.
|
|
12
|
+
* A brand page usually already emits an `Article` (or `BlogPosting`) graph — Yoast on
|
|
13
|
+
* WordPress, the theme on Shopify, the framework on headless. If we emit a SECOND
|
|
14
|
+
* top-level `Article`, crawlers see two competing primary entities. Instead we emit a
|
|
15
|
+
* `@graph` whose Article node carries the SAME `@id` the page's primary article uses
|
|
16
|
+
* (the page URL + `#article`, the Yoast convention) so a consumer MERGES our
|
|
17
|
+
* `reviewedBy` / `dateModified` into the existing node by `@id` rather than duplicating
|
|
18
|
+
* it. Our own nodes (the review, the expert Person, the CertREV org) use CertREV-scoped
|
|
19
|
+
* `@id`s that cannot clash with the host page's nodes.
|
|
20
|
+
*
|
|
21
|
+
* The Person we attach is the reviewing EXPERT (schema.org `reviewedBy`), which is the
|
|
22
|
+
* E-E-A-T signal CertREV exists to add — not the author. The author (content byline) is
|
|
23
|
+
* attached as `author` only when present + distinct.
|
|
24
|
+
*/
|
|
25
|
+
const SCHEMA_CONTEXT = 'https://schema.org';
|
|
26
|
+
/**
|
|
27
|
+
* Strip the query AND fragment from a URL for a stable, mergeable @id. The Yoast / host
|
|
28
|
+
* convention keys the primary Article node on `{path}#article` with no query — so a page
|
|
29
|
+
* reached with a `?utm=…` tracking param must derive the SAME `@id` as the canonical URL,
|
|
30
|
+
* else our `reviewedBy` lands on a sibling node instead of merging into the host's Article.
|
|
31
|
+
*/
|
|
32
|
+
function baseUrl(url) {
|
|
33
|
+
const cut = Math.min(...[url.indexOf('#'), url.indexOf('?')].filter((i) => i !== -1), url.length);
|
|
34
|
+
return url.slice(0, cut);
|
|
35
|
+
}
|
|
36
|
+
/** Derive the primary Article `@id`, aligned to the Yoast/host convention when we can. */
|
|
37
|
+
function articleId(payload, opts) {
|
|
38
|
+
const page = opts.pageUrl ?? payload.subject.canonicalUrls[0];
|
|
39
|
+
if (page)
|
|
40
|
+
return `${baseUrl(page)}#article`;
|
|
41
|
+
// No page URL known — derive a stable, mergeable id from the verify URL.
|
|
42
|
+
return `${baseUrl(payload.content.verifyUrl)}#article`;
|
|
43
|
+
}
|
|
44
|
+
/** CertREV-scoped node ids — namespaced under the verify URL so they never clash. */
|
|
45
|
+
function reviewId(content) {
|
|
46
|
+
return `${content.verifyUrl}#certrev-review`;
|
|
47
|
+
}
|
|
48
|
+
function expertId(content) {
|
|
49
|
+
return content.expert.profileUrl ? `${content.expert.profileUrl}#person` : `${content.verifyUrl}#expert`;
|
|
50
|
+
}
|
|
51
|
+
function orgId(content) {
|
|
52
|
+
// One stable CertREV organization node across all pages: derive from the verify
|
|
53
|
+
// URL origin so it dedupes cleanly when a page hosts multiple certified articles.
|
|
54
|
+
try {
|
|
55
|
+
return `${new URL(content.verifyUrl).origin}/#certrev-organization`;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return 'https://certrev.com/#certrev-organization';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Build the reviewing-expert Person node (the E-E-A-T signal). */
|
|
62
|
+
function expertPersonNode(payload) {
|
|
63
|
+
const { expert } = payload.content;
|
|
64
|
+
const node = {
|
|
65
|
+
'@type': 'Person',
|
|
66
|
+
'@id': expertId(payload.content),
|
|
67
|
+
name: expert.displayName,
|
|
68
|
+
};
|
|
69
|
+
if (expert.profileUrl)
|
|
70
|
+
node.url = expert.profileUrl;
|
|
71
|
+
if (expert.photoUrl)
|
|
72
|
+
node.image = expert.photoUrl;
|
|
73
|
+
if (expert.credentials.length > 0) {
|
|
74
|
+
// hasCredential → EducationalOccupationalCredential per credential; also a flat
|
|
75
|
+
// honorificSuffix list for consumers that don't read hasCredential.
|
|
76
|
+
node.hasCredential = expert.credentials.map((c) => ({
|
|
77
|
+
'@type': 'EducationalOccupationalCredential',
|
|
78
|
+
name: c.fullName,
|
|
79
|
+
alternateName: c.abbreviation,
|
|
80
|
+
}));
|
|
81
|
+
node.honorificSuffix = expert.credentials.map((c) => c.abbreviation).join(', ');
|
|
82
|
+
}
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
/** Build the CertREV organization node (publisher of the review credential). */
|
|
86
|
+
function certRevOrgNode(payload) {
|
|
87
|
+
let url = 'https://certrev.com';
|
|
88
|
+
try {
|
|
89
|
+
url = new URL(payload.content.verifyUrl).origin;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* keep default */
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
'@type': 'Organization',
|
|
96
|
+
'@id': orgId(payload.content),
|
|
97
|
+
name: 'CertREV',
|
|
98
|
+
url,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Build the Review node binding the article to its expert review. */
|
|
102
|
+
function reviewNode(payload) {
|
|
103
|
+
const node = {
|
|
104
|
+
'@type': 'Review',
|
|
105
|
+
'@id': reviewId(payload.content),
|
|
106
|
+
itemReviewed: { '@id': articleId(payload, {}) },
|
|
107
|
+
author: { '@id': expertId(payload.content) },
|
|
108
|
+
publisher: { '@id': orgId(payload.content) },
|
|
109
|
+
datePublished: payload.content.certifiedAt,
|
|
110
|
+
url: payload.content.verifyUrl,
|
|
111
|
+
};
|
|
112
|
+
if (payload.content.memo)
|
|
113
|
+
node.reviewBody = payload.content.memo;
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The primary Article node — carries the SAME `@id` the host page's article uses so a
|
|
118
|
+
* consumer merges our `reviewedBy` / `dateModified` into the existing node rather than
|
|
119
|
+
* duplicating the primary entity. We deliberately emit ONLY the certification-relevant
|
|
120
|
+
* properties (reviewedBy, dateModified, plus author when distinct) — not headline, body,
|
|
121
|
+
* image, etc. — so we never overwrite the host's richer Article fields on merge.
|
|
122
|
+
*/
|
|
123
|
+
function articleNode(payload, opts) {
|
|
124
|
+
const { content } = payload;
|
|
125
|
+
const node = {
|
|
126
|
+
'@type': 'Article',
|
|
127
|
+
'@id': articleId(payload, opts),
|
|
128
|
+
reviewedBy: { '@id': expertId(content) },
|
|
129
|
+
review: { '@id': reviewId(content) },
|
|
130
|
+
};
|
|
131
|
+
if (content.contentModifiedAt)
|
|
132
|
+
node.dateModified = content.contentModifiedAt;
|
|
133
|
+
// Author only when present + distinct from the expert (don't double-attribute).
|
|
134
|
+
if (content.author.name && content.author.name !== content.expert.displayName) {
|
|
135
|
+
const author = { '@type': 'Person', name: content.author.name };
|
|
136
|
+
if (content.author.title)
|
|
137
|
+
author.jobTitle = content.author.title;
|
|
138
|
+
node.author = author;
|
|
139
|
+
}
|
|
140
|
+
return node;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Project the verified certification facts into a schema.org `@graph`.
|
|
144
|
+
*
|
|
145
|
+
* @param payload The cryptographically-verified payload (the same facts the badge renders).
|
|
146
|
+
* @param opts Page URL for @id alignment + graph-wrapping control.
|
|
147
|
+
* @returns A `{ "@context", "@graph" }` object (wrapGraph !== false) OR a bare
|
|
148
|
+
* array of nodes (wrapGraph === false) for merge-into-existing-graph callers.
|
|
149
|
+
*/
|
|
150
|
+
export function projectCertJsonLd(payload, opts = {}) {
|
|
151
|
+
const nodes = [
|
|
152
|
+
articleNode(payload, opts),
|
|
153
|
+
reviewNode(payload),
|
|
154
|
+
expertPersonNode(payload),
|
|
155
|
+
certRevOrgNode(payload),
|
|
156
|
+
];
|
|
157
|
+
if (opts.wrapGraph === false)
|
|
158
|
+
return nodes;
|
|
159
|
+
return { '@context': SCHEMA_CONTEXT, '@graph': nodes };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Serialize the projected graph to the exact JSON string that goes inside a
|
|
163
|
+
* `<script type="application/ld+json">`. We use `JSON.stringify` with no extra
|
|
164
|
+
* whitespace for a compact, deterministic body. The string is then made safe to embed
|
|
165
|
+
* by `serializeJsonLdForScript` (which neutralizes `</script>`).
|
|
166
|
+
*/
|
|
167
|
+
export function projectCertJsonLdString(payload, opts = {}) {
|
|
168
|
+
return serializeJsonLdForScript(projectCertJsonLd(payload, opts));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Serialize an arbitrary JSON-LD value for safe inclusion inside a `<script>` element.
|
|
172
|
+
* Inside `application/ld+json`, two HTML sequences can break out of the tag: `</script>`
|
|
173
|
+
* (closes the element early) and HTML-comment markers `<!--` / `-->` (some parsers treat
|
|
174
|
+
* a comment opened inside a script as suspending script-data parsing). A memo or name
|
|
175
|
+
* containing either is attacker-influenced text, so we neutralize BOTH angle brackets to
|
|
176
|
+
* their `\uXXXX` form. This is valid JSON that parses back to the identical string — the
|
|
177
|
+
* structured data is unchanged, but no `<…>`/`-->` sequence can terminate the script.
|
|
178
|
+
* Standard Next.js / Yoast hardening.
|
|
179
|
+
*/
|
|
180
|
+
export function serializeJsonLdForScript(value) {
|
|
181
|
+
return JSON.stringify(value).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/jsonld/project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process TTL cache with single-flight de-duplication.
|
|
3
|
+
*
|
|
4
|
+
* WHY: under SSR, a popular product/blog page can render N times concurrently across a
|
|
5
|
+
* server's request workers. Without coordination, each render fires its own fetch to the
|
|
6
|
+
* Shopify metafield / Delivery API → a thundering herd against the issuer on cold cache
|
|
7
|
+
* or cache expiry. This cache:
|
|
8
|
+
* 1. Serves a fresh value from memory within its TTL (no fetch).
|
|
9
|
+
* 2. SINGLE-FLIGHTS concurrent misses for the same key — the first caller's in-flight
|
|
10
|
+
* promise is shared by every other caller for that key, so N concurrent renders do
|
|
11
|
+
* ONE fetch, not N.
|
|
12
|
+
* 3. Negative-caches failures briefly so a hard-down origin doesn't get hammered, while
|
|
13
|
+
* still recovering quickly (short negative TTL).
|
|
14
|
+
*
|
|
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
|
+
* pair with a shared CDN/KV cache (the Delivery API is CDN-cacheable on
|
|
18
|
+
* `lifecycle.revision`) for L2 — see README.
|
|
19
|
+
*/
|
|
20
|
+
export interface TtlCacheOptions {
|
|
21
|
+
/** Positive-result TTL in ms (default 60_000). */
|
|
22
|
+
readonly ttlMs?: number;
|
|
23
|
+
/** Negative-result (miss/error) TTL in ms (default 5_000). */
|
|
24
|
+
readonly negativeTtlMs?: number;
|
|
25
|
+
/** Max entries before LRU-ish eviction of the oldest insert (default 1000). */
|
|
26
|
+
readonly maxEntries?: number;
|
|
27
|
+
/** Injectable clock for deterministic tests. */
|
|
28
|
+
readonly now?: () => number;
|
|
29
|
+
}
|
|
30
|
+
export declare class TtlCache<V> {
|
|
31
|
+
private readonly store;
|
|
32
|
+
private readonly inflight;
|
|
33
|
+
private readonly ttlMs;
|
|
34
|
+
private readonly negativeTtlMs;
|
|
35
|
+
private readonly maxEntries;
|
|
36
|
+
private readonly now;
|
|
37
|
+
constructor(opts?: TtlCacheOptions);
|
|
38
|
+
/** Read a live (non-expired) entry, or undefined. Prunes the entry if expired. */
|
|
39
|
+
peek(key: string): V | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Get-or-load with single-flight. If a fresh value is cached, returns it. Otherwise
|
|
42
|
+
* de-duplicates concurrent loads for `key`: the first caller runs `loader`, every
|
|
43
|
+
* concurrent caller awaits the same promise. The result is cached with the positive
|
|
44
|
+
* TTL; if `isNegative(value)` returns true (e.g. a 'suppress' verdict) it's cached
|
|
45
|
+
* with the shorter negative TTL so a transient failure recovers fast. A thrown loader
|
|
46
|
+
* is NOT cached (it rejects all current waiters and the next call retries).
|
|
47
|
+
*/
|
|
48
|
+
getOrLoad(key: string, loader: () => Promise<V>, isNegative?: (v: V) => boolean): Promise<V>;
|
|
49
|
+
/** Insert/overwrite an entry with the appropriate TTL. */
|
|
50
|
+
set(key: string, value: V, negative?: boolean): void;
|
|
51
|
+
/** Drop a key (e.g. on a known revocation push). */
|
|
52
|
+
delete(key: string): void;
|
|
53
|
+
clear(): void;
|
|
54
|
+
get size(): number;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/verify/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,eAAe;IAC/B,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAQD,qBAAa,QAAQ,CAAC,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;gBAEtB,IAAI,GAAE,eAAoB;IAOtC,kFAAkF;IAClF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAUhC;;;;;;;OAOG;IACG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBlG,0DAA0D;IAC1D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,UAAQ,GAAG,IAAI;IAUlD,oDAAoD;IACpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process TTL cache with single-flight de-duplication.
|
|
3
|
+
*
|
|
4
|
+
* WHY: under SSR, a popular product/blog page can render N times concurrently across a
|
|
5
|
+
* server's request workers. Without coordination, each render fires its own fetch to the
|
|
6
|
+
* Shopify metafield / Delivery API → a thundering herd against the issuer on cold cache
|
|
7
|
+
* or cache expiry. This cache:
|
|
8
|
+
* 1. Serves a fresh value from memory within its TTL (no fetch).
|
|
9
|
+
* 2. SINGLE-FLIGHTS concurrent misses for the same key — the first caller's in-flight
|
|
10
|
+
* promise is shared by every other caller for that key, so N concurrent renders do
|
|
11
|
+
* ONE fetch, not N.
|
|
12
|
+
* 3. Negative-caches failures briefly so a hard-down origin doesn't get hammered, while
|
|
13
|
+
* still recovering quickly (short negative TTL).
|
|
14
|
+
*
|
|
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
|
+
* pair with a shared CDN/KV cache (the Delivery API is CDN-cacheable on
|
|
18
|
+
* `lifecycle.revision`) for L2 — see README.
|
|
19
|
+
*/
|
|
20
|
+
export class TtlCache {
|
|
21
|
+
store = new Map();
|
|
22
|
+
inflight = new Map();
|
|
23
|
+
ttlMs;
|
|
24
|
+
negativeTtlMs;
|
|
25
|
+
maxEntries;
|
|
26
|
+
now;
|
|
27
|
+
constructor(opts = {}) {
|
|
28
|
+
this.ttlMs = opts.ttlMs ?? 60_000;
|
|
29
|
+
this.negativeTtlMs = opts.negativeTtlMs ?? 5_000;
|
|
30
|
+
this.maxEntries = opts.maxEntries ?? 1000;
|
|
31
|
+
this.now = opts.now ?? Date.now;
|
|
32
|
+
}
|
|
33
|
+
/** Read a live (non-expired) entry, or undefined. Prunes the entry if expired. */
|
|
34
|
+
peek(key) {
|
|
35
|
+
const e = this.store.get(key);
|
|
36
|
+
if (!e)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (e.expiresAt <= this.now()) {
|
|
39
|
+
this.store.delete(key);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return e.value;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get-or-load with single-flight. If a fresh value is cached, returns it. Otherwise
|
|
46
|
+
* de-duplicates concurrent loads for `key`: the first caller runs `loader`, every
|
|
47
|
+
* concurrent caller awaits the same promise. The result is cached with the positive
|
|
48
|
+
* TTL; if `isNegative(value)` returns true (e.g. a 'suppress' verdict) it's cached
|
|
49
|
+
* with the shorter negative TTL so a transient failure recovers fast. A thrown loader
|
|
50
|
+
* is NOT cached (it rejects all current waiters and the next call retries).
|
|
51
|
+
*/
|
|
52
|
+
async getOrLoad(key, loader, isNegative) {
|
|
53
|
+
const cached = this.peek(key);
|
|
54
|
+
if (cached !== undefined)
|
|
55
|
+
return cached;
|
|
56
|
+
const existing = this.inflight.get(key);
|
|
57
|
+
if (existing)
|
|
58
|
+
return existing;
|
|
59
|
+
const promise = (async () => {
|
|
60
|
+
const value = await loader();
|
|
61
|
+
const negative = isNegative ? isNegative(value) : false;
|
|
62
|
+
this.set(key, value, negative);
|
|
63
|
+
return value;
|
|
64
|
+
})().finally(() => {
|
|
65
|
+
this.inflight.delete(key);
|
|
66
|
+
});
|
|
67
|
+
this.inflight.set(key, promise);
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
/** Insert/overwrite an entry with the appropriate TTL. */
|
|
71
|
+
set(key, value, negative = false) {
|
|
72
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
73
|
+
// Evict the oldest inserted key (Map preserves insertion order).
|
|
74
|
+
const oldest = this.store.keys().next().value;
|
|
75
|
+
if (oldest !== undefined)
|
|
76
|
+
this.store.delete(oldest);
|
|
77
|
+
}
|
|
78
|
+
const ttl = negative ? this.negativeTtlMs : this.ttlMs;
|
|
79
|
+
this.store.set(key, { value, expiresAt: this.now() + ttl, negative });
|
|
80
|
+
}
|
|
81
|
+
/** Drop a key (e.g. on a known revocation push). */
|
|
82
|
+
delete(key) {
|
|
83
|
+
this.store.delete(key);
|
|
84
|
+
}
|
|
85
|
+
clear() {
|
|
86
|
+
this.store.clear();
|
|
87
|
+
this.inflight.clear();
|
|
88
|
+
}
|
|
89
|
+
get size() {
|
|
90
|
+
return this.store.size;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/verify/cache.ts"],"names":[],"mappings":"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"}
|