@certrev/cert-block 0.3.0 → 0.5.1

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 (129) hide show
  1. package/README.md +48 -1
  2. package/dist/builder/cert-chrome-data.d.ts +129 -0
  3. package/dist/builder/cert-chrome-data.d.ts.map +1 -0
  4. package/dist/builder/cert-chrome-data.js +59 -0
  5. package/dist/builder/cert-chrome-data.js.map +1 -0
  6. package/dist/builder/cert-review-card.d.ts +68 -0
  7. package/dist/builder/cert-review-card.d.ts.map +1 -0
  8. package/dist/builder/cert-review-card.js +129 -0
  9. package/dist/builder/cert-review-card.js.map +1 -0
  10. package/dist/builder/index.d.ts +2 -0
  11. package/dist/builder/index.d.ts.map +1 -1
  12. package/dist/builder/index.js +3 -0
  13. package/dist/builder/index.js.map +1 -1
  14. package/dist/components/preview-block.d.ts +43 -0
  15. package/dist/components/preview-block.d.ts.map +1 -0
  16. package/dist/components/preview-block.js +53 -0
  17. package/dist/components/preview-block.js.map +1 -0
  18. package/dist/components/render-cert-block.d.ts +50 -7
  19. package/dist/components/render-cert-block.d.ts.map +1 -1
  20. package/dist/components/render-cert-block.js +304 -80
  21. package/dist/components/render-cert-block.js.map +1 -1
  22. package/dist/components/render-def.d.ts +27 -0
  23. package/dist/components/render-def.d.ts.map +1 -1
  24. package/dist/components/render-def.js +38 -0
  25. package/dist/components/render-def.js.map +1 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/modal/avatar-resize.d.ts +60 -0
  31. package/dist/modal/avatar-resize.d.ts.map +1 -0
  32. package/dist/modal/avatar-resize.js +91 -0
  33. package/dist/modal/avatar-resize.js.map +1 -0
  34. package/dist/modal/cert-modal-view.d.ts +74 -0
  35. package/dist/modal/cert-modal-view.d.ts.map +1 -0
  36. package/dist/modal/cert-modal-view.js +160 -0
  37. package/dist/modal/cert-modal-view.js.map +1 -0
  38. package/dist/modal/certrev-cert-modal.d.ts +72 -0
  39. package/dist/modal/certrev-cert-modal.d.ts.map +1 -0
  40. package/dist/modal/certrev-cert-modal.js +270 -0
  41. package/dist/modal/certrev-cert-modal.js.map +1 -0
  42. package/dist/modal/certrev-cert-modal.styles.d.ts +35 -0
  43. package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -0
  44. package/dist/modal/certrev-cert-modal.styles.js +488 -0
  45. package/dist/modal/certrev-cert-modal.styles.js.map +1 -0
  46. package/dist/modal/certrev-cert.entry.d.ts +19 -0
  47. package/dist/modal/certrev-cert.entry.d.ts.map +1 -0
  48. package/dist/modal/certrev-cert.entry.js +39 -0
  49. package/dist/modal/certrev-cert.entry.js.map +1 -0
  50. package/dist/modal/certrev-cert.js +557 -0
  51. package/dist/modal/display-strings.d.ts +22 -0
  52. package/dist/modal/display-strings.d.ts.map +1 -0
  53. package/dist/modal/display-strings.js +35 -0
  54. package/dist/modal/display-strings.js.map +1 -0
  55. package/dist/modal/escape.d.ts +13 -0
  56. package/dist/modal/escape.d.ts.map +1 -0
  57. package/dist/modal/escape.js +20 -0
  58. package/dist/modal/escape.js.map +1 -0
  59. package/dist/modal/font-face.d.ts +4 -0
  60. package/dist/modal/font-face.d.ts.map +1 -0
  61. package/dist/modal/font-face.js +137 -0
  62. package/dist/modal/font-face.js.map +1 -0
  63. package/dist/modal/ftc-disclosure.d.ts +12 -0
  64. package/dist/modal/ftc-disclosure.d.ts.map +1 -0
  65. package/dist/modal/ftc-disclosure.js +12 -0
  66. package/dist/modal/ftc-disclosure.js.map +1 -0
  67. package/dist/modal/ftc-guard.d.ts +50 -0
  68. package/dist/modal/ftc-guard.d.ts.map +1 -0
  69. package/dist/modal/ftc-guard.js +117 -0
  70. package/dist/modal/ftc-guard.js.map +1 -0
  71. package/dist/modal/index.d.ts +20 -0
  72. package/dist/modal/index.d.ts.map +1 -0
  73. package/dist/modal/index.js +25 -0
  74. package/dist/modal/index.js.map +1 -0
  75. package/dist/modal/interactions.d.ts +53 -0
  76. package/dist/modal/interactions.d.ts.map +1 -0
  77. package/dist/modal/interactions.js +147 -0
  78. package/dist/modal/interactions.js.map +1 -0
  79. package/dist/modal/logo-paths.d.ts +15 -0
  80. package/dist/modal/logo-paths.d.ts.map +1 -0
  81. package/dist/modal/logo-paths.js +24 -0
  82. package/dist/modal/logo-paths.js.map +1 -0
  83. package/dist/modal/placement.d.ts +95 -0
  84. package/dist/modal/placement.d.ts.map +1 -0
  85. package/dist/modal/placement.js +302 -0
  86. package/dist/modal/placement.js.map +1 -0
  87. package/dist/modal/revalidate.d.ts +48 -0
  88. package/dist/modal/revalidate.d.ts.map +1 -0
  89. package/dist/modal/revalidate.js +65 -0
  90. package/dist/modal/revalidate.js.map +1 -0
  91. package/dist/modal/verify-client.d.ts +24 -0
  92. package/dist/modal/verify-client.d.ts.map +1 -0
  93. package/dist/modal/verify-client.js +38 -0
  94. package/dist/modal/verify-client.js.map +1 -0
  95. package/package.json +14 -4
  96. package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +10 -6
  97. package/src/__tests__/preview-block.test.ts +84 -0
  98. package/src/__tests__/render-cert-block.test.ts +380 -5
  99. package/src/builder/__tests__/builder.test.tsx +71 -0
  100. package/src/builder/cert-chrome-data.ts +159 -0
  101. package/src/builder/cert-review-card.tsx +177 -0
  102. package/src/builder/index.tsx +18 -0
  103. package/src/components/preview-block.ts +74 -0
  104. package/src/components/render-cert-block.ts +387 -102
  105. package/src/components/render-def.ts +55 -0
  106. package/src/index.ts +9 -0
  107. package/src/modal/avatar-resize.ts +99 -0
  108. package/src/modal/cert-modal-view.test.ts +175 -0
  109. package/src/modal/cert-modal-view.ts +210 -0
  110. package/src/modal/certrev-cert-modal.styles.ts +494 -0
  111. package/src/modal/certrev-cert-modal.test.ts +125 -0
  112. package/src/modal/certrev-cert-modal.ts +273 -0
  113. package/src/modal/certrev-cert.entry.ts +44 -0
  114. package/src/modal/display-strings.ts +38 -0
  115. package/src/modal/escape.ts +20 -0
  116. package/src/modal/font-face.ts +138 -0
  117. package/src/modal/ftc-disclosure.ts +13 -0
  118. package/src/modal/ftc-guard.test.ts +153 -0
  119. package/src/modal/ftc-guard.ts +119 -0
  120. package/src/modal/index.ts +34 -0
  121. package/src/modal/interactions.test.ts +250 -0
  122. package/src/modal/interactions.ts +166 -0
  123. package/src/modal/logo-paths.ts +23 -0
  124. package/src/modal/placement.test.ts +398 -0
  125. package/src/modal/placement.ts +331 -0
  126. package/src/modal/revalidate.test.ts +111 -0
  127. package/src/modal/revalidate.ts +81 -0
  128. package/src/modal/verify-client.test.ts +164 -0
  129. package/src/modal/verify-client.ts +42 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * End-to-end test of the Web Component's Delivery-API verifier with REAL crypto: mint a signed
3
+ * artifact (local Ed25519), stub `fetch` to serve it, and assert createDeliveryVerifier runs the
4
+ * canonical `verifyArtifact` kernel correctly — render on a good envelope, suppress on a subject
5
+ * mismatch, `suppress:'revoked'` on the SLIM `CertTombstone` (POR-10684 / POR-10481), throw on a
6
+ * 404 (→ the DOM layer keeps the SSR badge). Proves the fetch + URL + verify wiring the
7
+ * unit-mocked revalidate.test.ts stubs out.
8
+ */
9
+
10
+ import { generateKeyPairSync } from "node:crypto";
11
+ import type {
12
+ CertSubject,
13
+ ResolvePublicKeyByKid,
14
+ } from "@certrev/cert-contract";
15
+ import {
16
+ localEd25519Signer,
17
+ signTombstone,
18
+ } from "@certrev/cert-contract/signer";
19
+ import { describe, expect, it, vi } from "vitest";
20
+ // Relocated from portal (POR-10721 W2): mint a valid signed envelope with the package's
21
+ // own fixture (cert-contract-native) instead of portal's `mintCertEnvelope` — the modal
22
+ // package must not depend on portal for its tests.
23
+ import { makeSignedEnvelope } from "../contract/fixtures.js";
24
+ import { createDeliveryVerifier } from "./verify-client.js";
25
+
26
+ const KID = "test-issuer";
27
+ const ARTICLE_GID = "gid://shopify/Article/12345";
28
+
29
+ function signerFor() {
30
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
31
+ const pem = publicKey.export({ type: "spki", format: "pem" }) as string;
32
+ const resolveKid: ResolvePublicKeyByKid = (k) =>
33
+ k === KID ? { format: "pem", pem } : null;
34
+ return { resolveKid, sign: localEd25519Signer(privateKey) };
35
+ }
36
+
37
+ function subjectFor(externalId = ARTICLE_GID): CertSubject {
38
+ return {
39
+ platform: "shopify",
40
+ externalId,
41
+ logicalArticleId: "art_wc_test",
42
+ canonicalUrls: ["https://brand.example.com/blogs/news/post"],
43
+ installationId: null,
44
+ contentDigest: "a".repeat(64),
45
+ };
46
+ }
47
+
48
+ function stubFetch(envelopeJson: unknown, ok = true, status = 200) {
49
+ return vi.fn(
50
+ async () =>
51
+ ({ ok, status, json: async () => envelopeJson }) as unknown as Response,
52
+ );
53
+ }
54
+
55
+ describe("createDeliveryVerifier", () => {
56
+ it("fetches the Delivery API URL and returns `render` for a valid served envelope", async () => {
57
+ // Envelope bound to ARTICLE_GID — the article we ask to verify → render.
58
+ const { envelope, resolveKid } = makeSignedEnvelope({
59
+ subject: subjectFor(),
60
+ });
61
+ const fetchImpl = stubFetch(envelope);
62
+ const verify = createDeliveryVerifier(
63
+ resolveKid,
64
+ fetchImpl as unknown as typeof fetch,
65
+ );
66
+
67
+ const verdict = await verify({
68
+ baseUrl: "https://portal.certrev.com/",
69
+ platform: "shopify",
70
+ externalId: ARTICLE_GID,
71
+ });
72
+
73
+ expect(verdict.decision).toBe("render");
74
+ expect(fetchImpl).toHaveBeenCalledWith(
75
+ `https://portal.certrev.com/api/cert/v1/delivery/shopify/${encodeURIComponent(ARTICLE_GID)}`,
76
+ expect.objectContaining({ headers: { accept: "application/json" } }),
77
+ );
78
+ });
79
+
80
+ it("returns a `suppress` verdict when the served envelope is bound to a different article", async () => {
81
+ // Envelope minted for a DIFFERENT article than the one we ask to verify.
82
+ const { envelope, resolveKid } = makeSignedEnvelope({
83
+ subject: subjectFor("gid://shopify/Article/99999"),
84
+ });
85
+ const verify = createDeliveryVerifier(
86
+ resolveKid,
87
+ stubFetch(envelope) as unknown as typeof fetch,
88
+ );
89
+
90
+ const verdict = await verify({
91
+ baseUrl: "https://portal.certrev.com",
92
+ platform: "shopify",
93
+ externalId: ARTICLE_GID,
94
+ });
95
+
96
+ expect(verdict.decision).toBe("suppress");
97
+ });
98
+
99
+ it('resolves the SLIM tombstone the control plane serves to `suppress:"revoked"` (POR-10684)', async () => {
100
+ const { resolveKid, sign } = signerFor();
101
+ // The revoked-placement Delivery response: a slim `CertTombstone` (kind: 'tombstone', NO
102
+ // payload) — what verifyEnvelope used to mis-suppress as 'unsupported_contract_version'.
103
+ const artifact = await signTombstone(
104
+ {
105
+ subject: subjectFor(),
106
+ revokedAt: "2026-06-20T00:00:00.000Z",
107
+ revocationReason: "cert_revoked",
108
+ },
109
+ { kid: KID, sign },
110
+ );
111
+ const verify = createDeliveryVerifier(
112
+ resolveKid,
113
+ stubFetch(artifact) as unknown as typeof fetch,
114
+ );
115
+
116
+ const verdict = await verify({
117
+ baseUrl: "https://portal.certrev.com",
118
+ platform: "shopify",
119
+ externalId: ARTICLE_GID,
120
+ });
121
+
122
+ expect(verdict.decision).toBe("suppress");
123
+ expect(verdict.decision === "suppress" && verdict.reason).toBe("revoked");
124
+ });
125
+
126
+ it("suppresses a tombstone minted for a DIFFERENT article (subject binding enforced)", async () => {
127
+ const { resolveKid, sign } = signerFor();
128
+ const artifact = await signTombstone(
129
+ {
130
+ subject: subjectFor("gid://shopify/Article/99999"),
131
+ revokedAt: "2026-06-20T00:00:00.000Z",
132
+ revocationReason: "cert_revoked",
133
+ },
134
+ { kid: KID, sign },
135
+ );
136
+ const verify = createDeliveryVerifier(
137
+ resolveKid,
138
+ stubFetch(artifact) as unknown as typeof fetch,
139
+ );
140
+
141
+ const verdict = await verify({
142
+ baseUrl: "https://portal.certrev.com",
143
+ platform: "shopify",
144
+ externalId: ARTICLE_GID,
145
+ });
146
+
147
+ expect(verdict.decision).toBe("suppress");
148
+ });
149
+
150
+ it("throws on a non-2xx Delivery API response (404 → caller keeps the SSR badge)", async () => {
151
+ const { resolveKid } = signerFor();
152
+ const verify = createDeliveryVerifier(
153
+ resolveKid,
154
+ stubFetch(null, false, 404) as unknown as typeof fetch,
155
+ );
156
+ await expect(
157
+ verify({
158
+ baseUrl: "https://portal.certrev.com",
159
+ platform: "shopify",
160
+ externalId: ARTICLE_GID,
161
+ }),
162
+ ).rejects.toThrow();
163
+ });
164
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The browser-side Delivery-API fetch + verify the Shopify Web Component uses. Thin glue around
3
+ * the CANONICAL kernel: it GETs the public, CDN-cacheable Delivery API
4
+ * (`{baseUrl}/api/cert/v1/delivery/{platform}/{externalId}`) and runs `verifyArtifact` from
5
+ * `@certrev/cert-contract` (WebCrypto Ed25519 over RFC-8785 JCS — the same kernel the server +
6
+ * every other edge run). No crypto is reimplemented here.
7
+ *
8
+ * `verifyArtifact` (POR-10684, cert-contract 0.3.0) dispatches on the `kind: 'tombstone'`
9
+ * discriminator: a full `CertDeliveryEnvelope` runs the whole crypto + lifecycle pipeline, while
10
+ * the SLIM `CertTombstone` the control plane now serves for a revoked placement (POR-10481) —
11
+ * which carries NO payload — verifies its own signature + subject and resolves to
12
+ * `suppress:'revoked'`. The old `verifyEnvelope` fails a tombstone's shape-check and returns
13
+ * `suppress:'unsupported_contract_version'` (still a blank badge, but the wrong suppressed-reason
14
+ * telemetry), so the suppression path routes through `verifyArtifact`.
15
+ *
16
+ * Drift is core-compensated (the browser can't recompute the canonical body hash), so
17
+ * `liveContentHash` is null → the kernel verifies signature + subject + lifecycle and skips
18
+ * drift. A non-2xx response (the API 404s on an untracked placement) THROWS, so the caller keeps
19
+ * the lifecycle-gated SSR badge rather than false-hiding it.
20
+ */
21
+
22
+ import { type CertDeliveryArtifact, type ResolvePublicKeyByKid, verifyArtifact } from '@certrev/cert-contract'
23
+ import type { BadgeSource, VerifyBadgeFn } from './revalidate.js'
24
+
25
+ export function createDeliveryVerifier(
26
+ resolveKid: ResolvePublicKeyByKid,
27
+ fetchImpl: typeof fetch = fetch,
28
+ ): VerifyBadgeFn {
29
+ return async (source: BadgeSource) => {
30
+ const base = source.baseUrl.replace(/\/+$/, '')
31
+ const url = `${base}/api/cert/v1/delivery/${encodeURIComponent(source.platform)}/${encodeURIComponent(source.externalId)}`
32
+ const res = await fetchImpl(url, { headers: { accept: 'application/json' } })
33
+ if (!res.ok) throw new Error(`cert delivery ${res.status}`)
34
+ const artifact = (await res.json()) as CertDeliveryArtifact
35
+ return verifyArtifact(artifact, resolveKid, {
36
+ platform: source.platform,
37
+ externalId: source.externalId,
38
+ liveContentHash: null,
39
+ now: new Date(),
40
+ })
41
+ }
42
+ }