@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,65 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * getVerifiedEnvelope — the server-side fetch + verify + cache entry point
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * The headless edge calls this ONCE per render in its server loader (Hydrogen loader,
7
+ * Next server component / route handler, Builder server fetch). It:
8
+ * 1. SOURCES the signed `CertDeliveryEnvelope` from EITHER a Shopify metafield (already
9
+ * fetched via the Storefront API) OR the public Delivery API
10
+ * (`GET /api/cert/v1/delivery/{platform}/{externalId}`).
11
+ * 2. Runs the shared VerdictKernel (signature + subject/lifecycle/drift), FAIL-CLOSED.
12
+ * 3. Caches the resulting verdict per-instance (TTL + single-flight) so concurrent SSR
13
+ * renders don't stampede the origin.
14
+ *
15
+ * It returns a `CertVerdict`: `{ decision: 'render', payload }` → render the badge +
16
+ * JSON-LD; `{ decision: 'suppress', reason }` → render NOTHING. Any error (network,
17
+ * malformed JSON, throwing resolver) collapses to a `suppress` verdict — never throws
18
+ * into the render path, never renders an unverified credential.
19
+ */
20
+ import type { CertDeliveryEnvelope, CertVerdict, RenderContext, ResolvePublicKeyByKid } from '../contract/kernel.js';
21
+ import { TtlCache } from './cache.js';
22
+ /** Where the signed envelope comes from. Exactly one of the two source shapes. */
23
+ export type EnvelopeSource =
24
+ /** PULL: the public, CDN-cacheable Delivery API. The helper fetches it. */
25
+ {
26
+ readonly kind: 'delivery_api';
27
+ /** Base URL of the portal Delivery API, e.g. 'https://portal.certrev.com'. */
28
+ readonly baseUrl: string;
29
+ readonly platform: string;
30
+ readonly externalId: string;
31
+ }
32
+ /** PUSH/native: an envelope already read from a Shopify app-owned metafield (or any
33
+ * native store). The caller fetched it via the Storefront API; we just verify it. The
34
+ * value may be the parsed envelope or its JSON string (metafields store strings). */
35
+ | {
36
+ readonly kind: 'metafield';
37
+ readonly value: CertDeliveryEnvelope | string | null | undefined;
38
+ };
39
+ export interface GetVerifiedEnvelopeOptions {
40
+ readonly source: EnvelopeSource;
41
+ /** kid → public-key resolver (see ./resolve-kid). Required: no verify without keys. */
42
+ readonly resolveKid: ResolvePublicKeyByKid;
43
+ /** Render context: the platform + externalId this edge IS, plus optional live hash. */
44
+ readonly context: Omit<RenderContext, 'now'> & {
45
+ readonly now?: Date;
46
+ };
47
+ /** Injectable fetch for tests / non-global-fetch runtimes. */
48
+ readonly fetchImpl?: typeof fetch;
49
+ /** Cache override (per-instance). Defaults to the module-level shared cache. */
50
+ readonly cache?: TtlCache<CertVerdict>;
51
+ /** Positive verdict TTL ms (default 60_000). */
52
+ readonly ttlMs?: number;
53
+ }
54
+ /** Module-level shared cache so all calls in a process coordinate by default. */
55
+ declare const sharedVerdictCache: TtlCache<CertVerdict>;
56
+ /**
57
+ * Fetch (if needed), verify, and cache the certification verdict for one placement.
58
+ * FAIL-CLOSED on every error path. Safe to call on every SSR render — the cache +
59
+ * single-flight keep the origin load bounded.
60
+ */
61
+ export declare function getVerifiedEnvelope(opts: GetVerifiedEnvelopeOptions): Promise<CertVerdict>;
62
+ /** Expose the shared cache so a revocation webhook handler can invalidate proactively. */
63
+ export declare function invalidateVerdict(source: EnvelopeSource, context: RenderContext, cache?: TtlCache<CertVerdict>): void;
64
+ export { sharedVerdictCache };
65
+ //# sourceMappingURL=get-verified-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-verified-envelope.d.ts","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAEpH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErC,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;;sFAEsF;GACpF;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,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAA;KAAE,CAAA;IACtE,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;IACtC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,iFAAiF;AACjF,QAAA,MAAM,kBAAkB,uBAAqE,CAAA;AAgD7F;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,WAAW,CAAC,CAkChG;AAED,0FAA0F;AAC1F,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"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * getVerifiedEnvelope — the server-side fetch + verify + cache entry point
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * The headless edge calls this ONCE per render in its server loader (Hydrogen loader,
7
+ * Next server component / route handler, Builder server fetch). It:
8
+ * 1. SOURCES the signed `CertDeliveryEnvelope` from EITHER a Shopify metafield (already
9
+ * fetched via the Storefront API) OR the public Delivery API
10
+ * (`GET /api/cert/v1/delivery/{platform}/{externalId}`).
11
+ * 2. Runs the shared VerdictKernel (signature + subject/lifecycle/drift), FAIL-CLOSED.
12
+ * 3. Caches the resulting verdict per-instance (TTL + single-flight) so concurrent SSR
13
+ * renders don't stampede the origin.
14
+ *
15
+ * It returns a `CertVerdict`: `{ decision: 'render', payload }` → render the badge +
16
+ * JSON-LD; `{ decision: 'suppress', reason }` → render NOTHING. Any error (network,
17
+ * malformed JSON, throwing resolver) collapses to a `suppress` verdict — never throws
18
+ * into the render path, never renders an unverified credential.
19
+ */
20
+ import { verifyEnvelope } from '../contract/kernel.js';
21
+ import { TtlCache } from './cache.js';
22
+ /** Module-level shared cache so all calls in a process coordinate by default. */
23
+ const sharedVerdictCache = new TtlCache({ ttlMs: 60_000, negativeTtlMs: 5_000 });
24
+ function suppress(reason) {
25
+ return { decision: 'suppress', reason };
26
+ }
27
+ function parseEnvelope(value) {
28
+ if (value == null)
29
+ return null;
30
+ if (typeof value !== 'string')
31
+ return value;
32
+ try {
33
+ return JSON.parse(value);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function deliveryApiUrl(s) {
40
+ const base = s.baseUrl.replace(/\/+$/, '');
41
+ return `${base}/api/cert/v1/delivery/${encodeURIComponent(s.platform)}/${encodeURIComponent(s.externalId)}`;
42
+ }
43
+ /** Stable cache key per (source identity × render externalId). */
44
+ function cacheKey(source, ctx) {
45
+ if (source.kind === 'delivery_api') {
46
+ return `api:${source.platform}:${source.externalId}`;
47
+ }
48
+ // Metafield: key on the rendering identity (the value is already in hand; the verdict
49
+ // still depends on context). Include a short hash of the value to bust on push update.
50
+ return `mf:${ctx.platform}:${ctx.externalId}`;
51
+ }
52
+ async function fetchFromDeliveryApi(s, fetchImpl) {
53
+ const res = await fetchImpl(deliveryApiUrl(s), { headers: { accept: 'application/json' } });
54
+ if (!res.ok)
55
+ return null; // 404 / 410 (revoked, no longer served) → no envelope → suppress
56
+ try {
57
+ return (await res.json());
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Fetch (if needed), verify, and cache the certification verdict for one placement.
65
+ * FAIL-CLOSED on every error path. Safe to call on every SSR render — the cache +
66
+ * single-flight keep the origin load bounded.
67
+ */
68
+ export async function getVerifiedEnvelope(opts) {
69
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
70
+ const cache = opts.cache ?? sharedVerdictCache;
71
+ const ctx = { ...opts.context };
72
+ const key = cacheKey(opts.source, ctx);
73
+ const isSuppress = (v) => v.decision !== 'render';
74
+ return cache.getOrLoad(key, async () => {
75
+ let envelope;
76
+ try {
77
+ envelope =
78
+ opts.source.kind === 'delivery_api'
79
+ ? await fetchFromDeliveryApi(opts.source, fetchImpl)
80
+ : parseEnvelope(opts.source.value);
81
+ }
82
+ catch {
83
+ // Network/JSON failure → fail closed (do NOT render an unverified credential).
84
+ return suppress('unsupported_contract_version');
85
+ }
86
+ if (!envelope) {
87
+ // No envelope present (never certified / revoked + cleared / 404) → suppress.
88
+ return suppress('unsupported_contract_version');
89
+ }
90
+ // The kernel itself never throws (it fails closed), but guard the resolver too.
91
+ try {
92
+ return await verifyEnvelope(envelope, opts.resolveKid, ctx);
93
+ }
94
+ catch {
95
+ return suppress('unknown_key');
96
+ }
97
+ }, isSuppress);
98
+ }
99
+ /** Expose the shared cache so a revocation webhook handler can invalidate proactively. */
100
+ export function invalidateVerdict(source, context, cache = sharedVerdictCache) {
101
+ cache.delete(cacheKey(source, context));
102
+ }
103
+ export { sharedVerdictCache };
104
+ //# sourceMappingURL=get-verified-envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-verified-envelope.js","sourceRoot":"","sources":["../../src/verify/get-verified-envelope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Public-key resolution for the kernel's signature-verification step.
3
+ *
4
+ * The kernel needs a `kid → Ed25519 public key` resolver. CertREV publishes its public
5
+ * key set (a JWKS-like document) so any edge can verify without a shared secret; rotation
6
+ * is "publish N+1 keys, start signing with the new kid, retire the old once edges refresh".
7
+ *
8
+ * This module gives two resolvers:
9
+ * • `staticKidResolver(keys)` — for a key set baked into the deploy (the headless edge
10
+ * ships CertREV's current public keys as config; zero network at render time).
11
+ * • `fetchingKidResolver({ jwksUrl })` — fetches + caches the published key set, so a
12
+ * newly-rotated kid resolves without a redeploy. Cached with a long TTL (keys rotate
13
+ * rarely) + single-flight (no thundering herd on the JWKS endpoint).
14
+ *
15
+ * Both return the `Ed25519PublicKeyInput` shape the kernel accepts. The fetching resolver
16
+ * understands the two encodings CertREV may publish: a PEM string, or a JWK with an
17
+ * Ed25519 `x` (base64url raw key).
18
+ */
19
+ import type { Ed25519PublicKeyInput, ResolvePublicKeyByKid } from '../contract/kernel.js';
20
+ /** A static map of kid → public key (PEM string or an explicit input). */
21
+ export type StaticKeySet = Readonly<Record<string, string | Ed25519PublicKeyInput>>;
22
+ /** Resolver over a key set baked into the deploy. No network at render time. */
23
+ export declare function staticKidResolver(keys: StaticKeySet): ResolvePublicKeyByKid;
24
+ export interface FetchingKidResolverOptions {
25
+ /** URL of CertREV's published key set (JWKS-like). */
26
+ readonly jwksUrl: string;
27
+ /** Key-set cache TTL in ms (default 1h — keys rotate rarely). */
28
+ readonly ttlMs?: number;
29
+ /** Injectable fetch for tests / non-global-fetch runtimes. */
30
+ readonly fetchImpl?: typeof fetch;
31
+ }
32
+ /**
33
+ * Resolver that fetches + caches CertREV's published key set. A single fetch populates
34
+ * all kids; concurrent misses single-flight through the cache. A kid absent from the
35
+ * fetched set resolves to null (→ kernel suppresses 'unknown_key' — fail-closed).
36
+ */
37
+ export declare function fetchingKidResolver(opts: FetchingKidResolverOptions): ResolvePublicKeyByKid;
38
+ //# sourceMappingURL=resolve-kid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-kid.d.ts","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAGzF,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;AA+BD,MAAM,WAAW,0BAA0B;IAC1C,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,0BAA0B,GAAG,qBAAqB,CAqB3F"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Public-key resolution for the kernel's signature-verification step.
3
+ *
4
+ * The kernel needs a `kid → Ed25519 public key` resolver. CertREV publishes its public
5
+ * key set (a JWKS-like document) so any edge can verify without a shared secret; rotation
6
+ * is "publish N+1 keys, start signing with the new kid, retire the old once edges refresh".
7
+ *
8
+ * This module gives two resolvers:
9
+ * • `staticKidResolver(keys)` — for a key set baked into the deploy (the headless edge
10
+ * ships CertREV's current public keys as config; zero network at render time).
11
+ * • `fetchingKidResolver({ jwksUrl })` — fetches + caches the published key set, so a
12
+ * newly-rotated kid resolves without a redeploy. Cached with a long TTL (keys rotate
13
+ * rarely) + single-flight (no thundering herd on the JWKS endpoint).
14
+ *
15
+ * Both return the `Ed25519PublicKeyInput` shape the kernel accepts. The fetching resolver
16
+ * understands the two encodings CertREV may publish: a PEM string, or a JWK with an
17
+ * Ed25519 `x` (base64url raw key).
18
+ */
19
+ import { TtlCache } from './cache.js';
20
+ function asInput(v) {
21
+ // A bare string is treated as PEM (the common deploy-config form).
22
+ return typeof v === 'string' ? { format: 'pem', pem: v } : v;
23
+ }
24
+ /** Resolver over a key set baked into the deploy. No network at render time. */
25
+ export function staticKidResolver(keys) {
26
+ return (kid) => {
27
+ const k = keys[kid];
28
+ return k ? asInput(k) : null;
29
+ };
30
+ }
31
+ function base64urlToBytes(b64url) {
32
+ return new Uint8Array(Buffer.from(b64url, 'base64url'));
33
+ }
34
+ function jwkToInput(jwk) {
35
+ if (jwk.pem)
36
+ return { format: 'pem', pem: jwk.pem };
37
+ if (jwk.x) {
38
+ const bytes = base64urlToBytes(jwk.x);
39
+ if (bytes.length === 32)
40
+ return { format: 'raw', bytes };
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Resolver that fetches + caches CertREV's published key set. A single fetch populates
46
+ * all kids; concurrent misses single-flight through the cache. A kid absent from the
47
+ * fetched set resolves to null (→ kernel suppresses 'unknown_key' — fail-closed).
48
+ */
49
+ export function fetchingKidResolver(opts) {
50
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
51
+ const cache = new TtlCache({ ttlMs: opts.ttlMs ?? 3_600_000 });
52
+ const CACHE_KEY = opts.jwksUrl;
53
+ async function loadKeySet() {
54
+ const map = new Map();
55
+ const res = await fetchImpl(opts.jwksUrl, { headers: { accept: 'application/json' } });
56
+ if (!res.ok)
57
+ return map; // empty → every kid resolves null → fail closed
58
+ const doc = (await res.json());
59
+ for (const jwk of doc.keys ?? []) {
60
+ const input = jwkToInput(jwk);
61
+ if (input && jwk.kid)
62
+ map.set(jwk.kid, input);
63
+ }
64
+ return map;
65
+ }
66
+ return async (kid) => {
67
+ const keySet = await cache.getOrLoad(CACHE_KEY, loadKeySet, (m) => m.size === 0);
68
+ return keySet.get(kid) ?? null;
69
+ };
70
+ }
71
+ //# sourceMappingURL=resolve-kid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-kid.js","sourceRoot":"","sources":["../../src/verify/resolve-kid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAKrC,SAAS,OAAO,CAAC,CAAiC;IACjD,mEAAmE;IACnE,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7D,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,IAAkB;IACnD,OAAO,CAAC,GAAG,EAAE,EAAE;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7B,CAAC,CAAA;AACF,CAAC;AAkBD,SAAS,gBAAgB,CAAC,MAAc;IACvC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAA;AACxD,CAAC;AAED,SAAS,UAAU,CAAC,GAAiB;IACpC,IAAI,GAAG,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAA;IACnD,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE;YAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzD,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgC;IACnE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAA;IACpD,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAqC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC,CAAA;IAClG,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAA;IAE9B,KAAK,UAAU,UAAU;QACxB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAiC,CAAA;QACpD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAA;QACtF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,GAAG,CAAA,CAAC,gDAAgD;QACxE,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAuB,CAAA;QACpD,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;YAC7B,IAAI,KAAK,IAAI,GAAG,CAAC,GAAG;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC9C,CAAC;QACD,OAAO,GAAG,CAAA;IACX,CAAC;IAED,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;QACpB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAA;QAChF,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA;IAC/B,CAAC,CAAA;AACF,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * <certrev-badge> — the framework-agnostic universal-embed Web Component
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * The `universal_embed` surface class: a custom element any site can drop in, fed by the
7
+ * Delivery API, that renders the SAME badge as <CertBadge> (it shares
8
+ * `renderBadgeHtml`). Two usage modes, both crawler-friendly:
9
+ *
10
+ * 1. SSR + hydrate (PREFERRED, crawlable). A server-side include emits the badge HTML
11
+ * (via `renderBadgeHtml`) INSIDE the element, plus the element tag. The component
12
+ * sees existing light-DOM children and leaves them; it only re-renders if asked to
13
+ * refresh. The crawler reads the server HTML; the component is progressive
14
+ * enhancement.
15
+ *
16
+ * 2. Client fetch (fallback, NOT crawlable — for already-client-only contexts). Given a
17
+ * `delivery-api` + `platform` + `external-id`, it fetches the envelope, runs the
18
+ * verdict via the kernel (WebCrypto path lives in cert-contract; here we accept a
19
+ * pre-supplied `resolveKid`), and renders on `render`. Documented as the weaker mode.
20
+ *
21
+ * The element NEVER renders an unverified credential: client-mode renders only on a
22
+ * `render` verdict; on any suppress/error it renders nothing (fail-closed).
23
+ *
24
+ * Registration is side-effect-free until you call `defineCertRevBadge()` (so importing
25
+ * the module in SSR doesn't touch `customElements`, which doesn't exist server-side).
26
+ */
27
+ import type { ResolvePublicKeyByKid } from '../contract/kernel.js';
28
+ export declare const CERTREV_BADGE_TAG = "certrev-badge";
29
+ export declare function setCertRevKidResolver(resolver: ResolvePublicKeyByKid): void;
30
+ export declare class CertRevBadgeElement extends HTMLElement {
31
+ static get observedAttributes(): string[];
32
+ connectedCallback(): void;
33
+ attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
34
+ private clientFetchAndRender;
35
+ }
36
+ /** Register the element. Idempotent + safe to call only in a browser/customElements env. */
37
+ export declare function defineCertRevBadge(): void;
38
+ //# sourceMappingURL=certrev-badge.d.ts.map
@@ -0,0 +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;AAED,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;CA4BlC;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * <certrev-badge> — the framework-agnostic universal-embed Web Component
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * The `universal_embed` surface class: a custom element any site can drop in, fed by the
7
+ * Delivery API, that renders the SAME badge as <CertBadge> (it shares
8
+ * `renderBadgeHtml`). Two usage modes, both crawler-friendly:
9
+ *
10
+ * 1. SSR + hydrate (PREFERRED, crawlable). A server-side include emits the badge HTML
11
+ * (via `renderBadgeHtml`) INSIDE the element, plus the element tag. The component
12
+ * sees existing light-DOM children and leaves them; it only re-renders if asked to
13
+ * refresh. The crawler reads the server HTML; the component is progressive
14
+ * enhancement.
15
+ *
16
+ * 2. Client fetch (fallback, NOT crawlable — for already-client-only contexts). Given a
17
+ * `delivery-api` + `platform` + `external-id`, it fetches the envelope, runs the
18
+ * verdict via the kernel (WebCrypto path lives in cert-contract; here we accept a
19
+ * pre-supplied `resolveKid`), and renders on `render`. Documented as the weaker mode.
20
+ *
21
+ * The element NEVER renders an unverified credential: client-mode renders only on a
22
+ * `render` verdict; on any suppress/error it renders nothing (fail-closed).
23
+ *
24
+ * Registration is side-effect-free until you call `defineCertRevBadge()` (so importing
25
+ * the module in SSR doesn't touch `customElements`, which doesn't exist server-side).
26
+ */
27
+ import { getVerifiedEnvelope } from '../verify/get-verified-envelope.js';
28
+ import { renderBadgeHtml } from './render-badge-html.js';
29
+ export const CERTREV_BADGE_TAG = 'certrev-badge';
30
+ /**
31
+ * A resolver registry the page sets once (the published CertREV keys) so the element can
32
+ * verify in client-fetch mode without each tag carrying key config. SSR mode never needs
33
+ * this (verification happened server-side).
34
+ */
35
+ let globalResolveKid = null;
36
+ export function setCertRevKidResolver(resolver) {
37
+ globalResolveKid = resolver;
38
+ }
39
+ export class CertRevBadgeElement extends HTMLElement {
40
+ static get observedAttributes() {
41
+ return ['delivery-api', 'platform', 'external-id', 'accent-color', 'badge-style', 'content-hash'];
42
+ }
43
+ connectedCallback() {
44
+ // SSR/light-DOM-present mode: server already rendered the badge inside us. Leave it.
45
+ // Only client-fetch when there are no rendered children AND we have a source.
46
+ if (this.querySelector('.certrev-badge'))
47
+ return;
48
+ void this.clientFetchAndRender();
49
+ }
50
+ attributeChangedCallback(_name, oldValue, newValue) {
51
+ if (oldValue === newValue)
52
+ return;
53
+ // Re-fetch only in client mode (no server-rendered child present).
54
+ if (this.isConnected && !this.querySelector('.certrev-badge')) {
55
+ void this.clientFetchAndRender();
56
+ }
57
+ }
58
+ async clientFetchAndRender() {
59
+ const baseUrl = this.getAttribute('delivery-api');
60
+ const platform = this.getAttribute('platform');
61
+ const externalId = this.getAttribute('external-id');
62
+ if (!baseUrl || !platform || !externalId)
63
+ return; // nothing to fetch → render nothing
64
+ if (!globalResolveKid) {
65
+ // No keys configured → cannot verify → fail closed (render nothing).
66
+ return;
67
+ }
68
+ let verdict;
69
+ try {
70
+ verdict = await getVerifiedEnvelope({
71
+ source: { kind: 'delivery_api', baseUrl, platform, externalId },
72
+ resolveKid: globalResolveKid,
73
+ context: {
74
+ platform,
75
+ externalId,
76
+ liveContentHash: this.getAttribute('content-hash'),
77
+ },
78
+ });
79
+ }
80
+ catch {
81
+ return; // fail closed
82
+ }
83
+ if (verdict.decision !== 'render')
84
+ return;
85
+ const accentColor = this.getAttribute('accent-color') ?? undefined;
86
+ const badgeStyle = this.getAttribute('badge-style') ?? undefined;
87
+ this.innerHTML = renderBadgeHtml(verdict.payload, { accentColor, badgeStyle });
88
+ }
89
+ }
90
+ /** Register the element. Idempotent + safe to call only in a browser/customElements env. */
91
+ export function defineCertRevBadge() {
92
+ if (typeof customElements === 'undefined')
93
+ return;
94
+ if (!customElements.get(CERTREV_BADGE_TAG)) {
95
+ customElements.define(CERTREV_BADGE_TAG, CertRevBadgeElement);
96
+ }
97
+ }
98
+ //# sourceMappingURL=certrev-badge.js.map
@@ -0,0 +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,GAAI,IAAI,CAAC,YAAY,CAAC,aAAa,CAA+B,IAAI,SAAS,CAAA;QAC/F,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAA;IAC/E,CAAC;CACD;AAED,4FAA4F;AAC5F,MAAM,UAAU,kBAAkB;IACjC,IAAI,OAAO,cAAc,KAAK,WAAW;QAAE,OAAM;IACjD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC5C,cAAc,CAAC,MAAM,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAA;IAC9D,CAAC;AACF,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Framework-agnostic badge HTML renderer.
3
+ *
4
+ * The single source of badge markup for BOTH the Web Component (client upgrade) and any
5
+ * server-side include that wants to emit the badge as a string without React. It mirrors
6
+ * the <CertBadge> JSX one-for-one — same classes, same structure, same accessibility —
7
+ * but builds an HTML string, so EVERY interpolated field is escaped by hand
8
+ * (`escapeHtml` for content, `escapeAttribute` for attributes, `safeHttpUrl` for hrefs).
9
+ *
10
+ * It is pure + DOM-free, so it runs server-side (the SSR string the crawler sees) and is
11
+ * reused client-side by the Web Component. Crawlability requirement from the contract:
12
+ * the badge must be in the server HTML, never client-only.
13
+ */
14
+ import type { CertPayload } from '../contract/kernel.js';
15
+ export interface RenderBadgeOptions {
16
+ readonly accentColor?: string;
17
+ readonly badgeStyle?: 'full' | 'compact';
18
+ }
19
+ /**
20
+ * Render the certified-badge markup for a verified payload as an HTML string. Returns ''
21
+ * when there is nothing safe to link to AND nothing to show (defensive; the kernel has
22
+ * already decided to render by the time this is called).
23
+ */
24
+ export declare function renderBadgeHtml(payload: CertPayload, opts?: RenderBadgeOptions): string;
25
+ //# sourceMappingURL=render-badge-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-badge-html.d.ts","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AASD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAE,kBAAuB,GAAG,MAAM,CAsE3F"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Framework-agnostic badge HTML renderer.
3
+ *
4
+ * The single source of badge markup for BOTH the Web Component (client upgrade) and any
5
+ * server-side include that wants to emit the badge as a string without React. It mirrors
6
+ * the <CertBadge> JSX one-for-one — same classes, same structure, same accessibility —
7
+ * but builds an HTML string, so EVERY interpolated field is escaped by hand
8
+ * (`escapeHtml` for content, `escapeAttribute` for attributes, `safeHttpUrl` for hrefs).
9
+ *
10
+ * It is pure + DOM-free, so it runs server-side (the SSR string the crawler sees) and is
11
+ * reused client-side by the Web Component. Crawlability requirement from the contract:
12
+ * the badge must be in the server HTML, never client-only.
13
+ */
14
+ import { escapeAttribute, escapeHtml, safeCssColor, safeHttpUrl } from '../components/escape.js';
15
+ import { credentialSuffix, formatDate, resolveDisplay } from '../components/format.js';
16
+ const C = 'certrev-badge';
17
+ function attr(name, value) {
18
+ if (value == null || value === '')
19
+ return '';
20
+ return ` ${name}="${escapeAttribute(value)}"`;
21
+ }
22
+ /**
23
+ * Render the certified-badge markup for a verified payload as an HTML string. Returns ''
24
+ * when there is nothing safe to link to AND nothing to show (defensive; the kernel has
25
+ * already decided to render by the time this is called).
26
+ */
27
+ export function renderBadgeHtml(payload, opts = {}) {
28
+ const content = payload.content;
29
+ const display = resolveDisplay(content.display, opts.accentColor);
30
+ const style = opts.badgeStyle ?? display.badgeStyle;
31
+ const accent = safeCssColor(display.accentColor) ?? '#0f766e';
32
+ const verifyUrl = safeHttpUrl(content.verifyUrl);
33
+ const profileUrl = safeHttpUrl(content.expert.profileUrl);
34
+ const photoUrl = style === 'full' && display.showExpertPhoto ? safeHttpUrl(content.expert.photoUrl) : null;
35
+ const suffix = credentialSuffix(content);
36
+ const certified = formatDate(content.certifiedAt);
37
+ const updated = formatDate(content.contentModifiedAt);
38
+ const name = `${escapeHtml(content.expert.displayName)}${suffix ? `<span class="${C}__credentials">, ${escapeHtml(suffix)}</span>` : ''}`;
39
+ const nameNode = profileUrl
40
+ ? `<a class="${C}__expert-link" href="${escapeAttribute(profileUrl)}" rel="noopener">${name}</a>`
41
+ : name;
42
+ const mark = `<svg class="${C}__mark" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">` +
43
+ `<circle cx="12" cy="12" r="11" fill="${escapeAttribute(accent)}"></circle>` +
44
+ `<path d="M7 12.5l3.2 3.2L17 9" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
45
+ const photo = photoUrl
46
+ ? `<img class="${C}__photo" src="${escapeAttribute(photoUrl)}" alt="Photo of ${escapeAttribute(content.expert.displayName)}" width="40" height="40" loading="lazy" decoding="async" />`
47
+ : '';
48
+ const header = `<div class="${C}__header">${mark}${photo}` +
49
+ `<div class="${C}__heading">` +
50
+ `<span class="${C}__eyebrow">Expert reviewed</span>` +
51
+ `<span class="${C}__byline">Reviewed by ${nameNode}</span>` +
52
+ `</div></div>`;
53
+ let body = '';
54
+ if (style === 'full') {
55
+ if (display.showMemo && content.memo) {
56
+ body += `<p class="${C}__memo">${escapeHtml(content.memo)}</p>`;
57
+ }
58
+ if (display.showAuthor && content.author.name && content.author.name !== content.expert.displayName) {
59
+ const title = content.author.title ? `, ${escapeHtml(content.author.title)}` : '';
60
+ body += `<p class="${C}__author">Written by ${escapeHtml(content.author.name)}${title}</p>`;
61
+ }
62
+ const dates = [];
63
+ if (certified) {
64
+ dates.push(`<div class="${C}__date"><dt>Certified</dt><dd><time datetime="${escapeAttribute(content.certifiedAt)}">${escapeHtml(certified)}</time></dd></div>`);
65
+ }
66
+ if (updated && content.contentModifiedAt) {
67
+ dates.push(`<div class="${C}__date"><dt>Last updated</dt><dd><time datetime="${escapeAttribute(content.contentModifiedAt)}">${escapeHtml(updated)}</time></dd></div>`);
68
+ }
69
+ if (dates.length)
70
+ body += `<dl class="${C}__dates">${dates.join('')}</dl>`;
71
+ }
72
+ const verify = verifyUrl
73
+ ? `<a class="${C}__verify" href="${escapeAttribute(verifyUrl)}" rel="noopener" aria-label="Verify this certification on CertREV">Verify on CertREV</a>`
74
+ : '';
75
+ const ariaLabel = `Content reviewed by ${content.expert.displayName}${suffix ? `, ${suffix}` : ''}`;
76
+ const rootStyle = `--certrev-accent:${accent};border-inline-start-color:${accent}`;
77
+ return (`<section class="${C} ${C}--${escapeAttribute(style)}" style="${escapeAttribute(rootStyle)}"` +
78
+ attr('data-certrev-cert-id', payload.certId) +
79
+ ` aria-label="${escapeAttribute(ariaLabel)}">${header}${body}${verify}</section>`);
80
+ }
81
+ //# sourceMappingURL=render-badge-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-badge-html.js","sourceRoot":"","sources":["../../src/webcomponent/render-badge-html.ts"],"names":[],"mappings":"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"}