@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.
Files changed (99) hide show
  1. package/README.md +154 -0
  2. package/dist/components/CertBadge.d.ts +29 -0
  3. package/dist/components/CertBadge.d.ts.map +1 -0
  4. package/dist/components/CertBadge.js +36 -0
  5. package/dist/components/CertBadge.js.map +1 -0
  6. package/dist/components/CertJsonLd.d.ts +23 -0
  7. package/dist/components/CertJsonLd.d.ts.map +1 -0
  8. package/dist/components/CertJsonLd.js +10 -0
  9. package/dist/components/CertJsonLd.js.map +1 -0
  10. package/dist/components/CertRevBacklink.d.ts +18 -0
  11. package/dist/components/CertRevBacklink.d.ts.map +1 -0
  12. package/dist/components/CertRevBacklink.js +16 -0
  13. package/dist/components/CertRevBacklink.js.map +1 -0
  14. package/dist/components/CertReview.d.ts +23 -0
  15. package/dist/components/CertReview.d.ts.map +1 -0
  16. package/dist/components/CertReview.js +11 -0
  17. package/dist/components/CertReview.js.map +1 -0
  18. package/dist/components/ExpertBio.d.ts +17 -0
  19. package/dist/components/ExpertBio.d.ts.map +1 -0
  20. package/dist/components/ExpertBio.js +17 -0
  21. package/dist/components/ExpertBio.js.map +1 -0
  22. package/dist/components/escape.d.ts +36 -0
  23. package/dist/components/escape.d.ts.map +1 -0
  24. package/dist/components/escape.js +76 -0
  25. package/dist/components/escape.js.map +1 -0
  26. package/dist/components/format.d.ts +22 -0
  27. package/dist/components/format.d.ts.map +1 -0
  28. package/dist/components/format.js +42 -0
  29. package/dist/components/format.js.map +1 -0
  30. package/dist/contract/fixtures.d.ts +36 -0
  31. package/dist/contract/fixtures.d.ts.map +1 -0
  32. package/dist/contract/fixtures.js +87 -0
  33. package/dist/contract/fixtures.js.map +1 -0
  34. package/dist/contract/kernel-contract.d.ts +154 -0
  35. package/dist/contract/kernel-contract.d.ts.map +1 -0
  36. package/dist/contract/kernel-contract.js +35 -0
  37. package/dist/contract/kernel-contract.js.map +1 -0
  38. package/dist/contract/kernel-stub.d.ts +44 -0
  39. package/dist/contract/kernel-stub.d.ts.map +1 -0
  40. package/dist/contract/kernel-stub.js +163 -0
  41. package/dist/contract/kernel-stub.js.map +1 -0
  42. package/dist/contract/kernel.d.ts +20 -0
  43. package/dist/contract/kernel.d.ts.map +1 -0
  44. package/dist/contract/kernel.js +19 -0
  45. package/dist/contract/kernel.js.map +1 -0
  46. package/dist/contract/verdict-kernel.d.ts +34 -0
  47. package/dist/contract/verdict-kernel.d.ts.map +1 -0
  48. package/dist/contract/verdict-kernel.js +13 -0
  49. package/dist/contract/verdict-kernel.js.map +1 -0
  50. package/dist/index.d.ts +31 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +38 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/jsonld/project.d.ts +71 -0
  55. package/dist/jsonld/project.d.ts.map +1 -0
  56. package/dist/jsonld/project.js +183 -0
  57. package/dist/jsonld/project.js.map +1 -0
  58. package/dist/verify/cache.d.ts +56 -0
  59. package/dist/verify/cache.d.ts.map +1 -0
  60. package/dist/verify/cache.js +93 -0
  61. package/dist/verify/cache.js.map +1 -0
  62. package/dist/verify/get-verified-envelope.d.ts +65 -0
  63. package/dist/verify/get-verified-envelope.d.ts.map +1 -0
  64. package/dist/verify/get-verified-envelope.js +104 -0
  65. package/dist/verify/get-verified-envelope.js.map +1 -0
  66. package/dist/verify/resolve-kid.d.ts +38 -0
  67. package/dist/verify/resolve-kid.d.ts.map +1 -0
  68. package/dist/verify/resolve-kid.js +71 -0
  69. package/dist/verify/resolve-kid.js.map +1 -0
  70. package/dist/webcomponent/certrev-badge.d.ts +38 -0
  71. package/dist/webcomponent/certrev-badge.d.ts.map +1 -0
  72. package/dist/webcomponent/certrev-badge.js +98 -0
  73. package/dist/webcomponent/certrev-badge.js.map +1 -0
  74. package/dist/webcomponent/render-badge-html.d.ts +25 -0
  75. package/dist/webcomponent/render-badge-html.d.ts.map +1 -0
  76. package/dist/webcomponent/render-badge-html.js +81 -0
  77. package/dist/webcomponent/render-badge-html.js.map +1 -0
  78. package/package.json +70 -0
  79. package/src/__tests__/components.test.tsx +191 -0
  80. package/src/__tests__/project.test.ts +128 -0
  81. package/src/__tests__/verify.test.ts +203 -0
  82. package/src/__tests__/webcomponent.test.tsx +106 -0
  83. package/src/components/CertBadge.tsx +164 -0
  84. package/src/components/CertJsonLd.tsx +36 -0
  85. package/src/components/CertRevBacklink.tsx +63 -0
  86. package/src/components/CertReview.tsx +42 -0
  87. package/src/components/ExpertBio.tsx +77 -0
  88. package/src/components/escape.ts +72 -0
  89. package/src/components/format.ts +55 -0
  90. package/src/contract/fixtures.ts +107 -0
  91. package/src/contract/kernel.ts +20 -0
  92. package/src/contract/verdict-kernel.ts +47 -0
  93. package/src/index.ts +85 -0
  94. package/src/jsonld/project.ts +206 -0
  95. package/src/verify/cache.ts +116 -0
  96. package/src/verify/get-verified-envelope.ts +156 -0
  97. package/src/verify/resolve-kid.ts +103 -0
  98. package/src/webcomponent/certrev-badge.ts +100 -0
  99. 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"}
@@ -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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"}