@certrev/cert-block 1.3.0 → 2.0.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 (88) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/README.md +162 -9
  3. package/dist/builder/card-beacon.d.ts +73 -0
  4. package/dist/builder/card-beacon.d.ts.map +1 -0
  5. package/dist/builder/card-beacon.js +178 -0
  6. package/dist/builder/card-beacon.js.map +1 -0
  7. package/dist/builder/card-contract.d.ts +151 -0
  8. package/dist/builder/card-contract.d.ts.map +1 -0
  9. package/dist/builder/card-contract.js +286 -0
  10. package/dist/builder/card-contract.js.map +1 -0
  11. package/dist/builder/card-placement.d.ts +45 -0
  12. package/dist/builder/card-placement.d.ts.map +1 -0
  13. package/dist/builder/card-placement.js +193 -0
  14. package/dist/builder/card-placement.js.map +1 -0
  15. package/dist/builder/card-report.d.ts +49 -0
  16. package/dist/builder/card-report.d.ts.map +1 -0
  17. package/dist/builder/card-report.js +97 -0
  18. package/dist/builder/card-report.js.map +1 -0
  19. package/dist/builder/cert-review-card.d.ts +7 -5
  20. package/dist/builder/cert-review-card.d.ts.map +1 -1
  21. package/dist/builder/cert-review-card.js +202 -170
  22. package/dist/builder/cert-review-card.js.map +1 -1
  23. package/dist/builder/contract-card.d.ts +70 -0
  24. package/dist/builder/contract-card.d.ts.map +1 -0
  25. package/dist/builder/contract-card.js +191 -0
  26. package/dist/builder/contract-card.js.map +1 -0
  27. package/dist/builder/delivered-face.d.ts +9 -0
  28. package/dist/builder/delivered-face.d.ts.map +1 -1
  29. package/dist/builder/delivered-face.js +18 -5
  30. package/dist/builder/delivered-face.js.map +1 -1
  31. package/dist/builder/delivery-runtime.d.ts +27 -3
  32. package/dist/builder/delivery-runtime.d.ts.map +1 -1
  33. package/dist/builder/delivery-runtime.js +40 -4
  34. package/dist/builder/delivery-runtime.js.map +1 -1
  35. package/dist/builder/index.d.ts +7 -4
  36. package/dist/builder/index.d.ts.map +1 -1
  37. package/dist/builder/index.js +8 -4
  38. package/dist/builder/index.js.map +1 -1
  39. package/dist/builder/ssr-seed.d.ts +23 -4
  40. package/dist/builder/ssr-seed.d.ts.map +1 -1
  41. package/dist/builder/ssr-seed.js +29 -6
  42. package/dist/builder/ssr-seed.js.map +1 -1
  43. package/dist/builder/version.d.ts +3 -0
  44. package/dist/builder/version.d.ts.map +1 -0
  45. package/dist/builder/version.js +3 -0
  46. package/dist/builder/version.js.map +1 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +2 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/reader/reader-css.generated.d.ts +1 -1
  52. package/dist/reader/reader-css.generated.d.ts.map +1 -1
  53. package/dist/reader/reader-css.generated.js +1 -1
  54. package/dist/reader/reader-css.generated.js.map +1 -1
  55. package/dist/reader/render.d.ts.map +1 -1
  56. package/dist/reader/render.js +71 -34
  57. package/dist/reader/render.js.map +1 -1
  58. package/dist/reader/tokens.d.ts +2 -0
  59. package/dist/reader/tokens.d.ts.map +1 -1
  60. package/dist/reader/tokens.js +28 -6
  61. package/dist/reader/tokens.js.map +1 -1
  62. package/dist/verify/get-verified-delivery.d.ts +32 -0
  63. package/dist/verify/get-verified-delivery.d.ts.map +1 -1
  64. package/dist/verify/get-verified-delivery.js +30 -5
  65. package/dist/verify/get-verified-delivery.js.map +1 -1
  66. package/dist/verify/key-set.d.ts +67 -0
  67. package/dist/verify/key-set.d.ts.map +1 -0
  68. package/dist/verify/key-set.js +190 -0
  69. package/dist/verify/key-set.js.map +1 -0
  70. package/package.json +115 -112
  71. package/src/builder/card-beacon.ts +201 -0
  72. package/src/builder/card-contract.ts +336 -0
  73. package/src/builder/card-placement.ts +208 -0
  74. package/src/builder/card-report.ts +126 -0
  75. package/src/builder/cert-review-card.tsx +285 -201
  76. package/src/builder/contract-card.tsx +216 -0
  77. package/src/builder/delivered-face.ts +18 -4
  78. package/src/builder/delivery-runtime.ts +46 -4
  79. package/src/builder/index.tsx +16 -4
  80. package/src/builder/ssr-seed.ts +37 -7
  81. package/src/builder/version.ts +2 -0
  82. package/src/index.ts +9 -0
  83. package/src/reader/reader-css.generated.ts +1 -1
  84. package/src/reader/reader.css +297 -2
  85. package/src/reader/render.ts +87 -38
  86. package/src/reader/tokens.ts +29 -6
  87. package/src/verify/get-verified-delivery.ts +54 -5
  88. package/src/verify/key-set.ts +225 -0
package/package.json CHANGED
@@ -1,113 +1,116 @@
1
1
  {
2
- "name": "@certrev/cert-block",
3
- "version": "1.3.0",
4
- "description": "Headless render edge for the CertREV CertDeliveryEnvelope: SSR-safe React cert components, a framework-agnostic <certrev-badge> Web Component, a schema.org JSON-LD projector, and a fail-closed verify layer over the shared VerdictKernel.",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/CertREV/cert-kit.git",
8
- "directory": "packages/cert-block"
9
- },
10
- "homepage": "https://github.com/CertREV/cert-kit/tree/HEAD/packages/cert-block#readme",
11
- "bugs": {
12
- "url": "https://github.com/CertREV/cert-kit/issues"
13
- },
14
- "type": "module",
15
- "main": "./dist/index.js",
16
- "types": "./dist/index.d.ts",
17
- "exports": {
18
- ".": {
19
- "types": "./dist/index.d.ts",
20
- "default": "./dist/index.js"
21
- },
22
- "./builder": {
23
- "types": "./dist/builder/index.d.ts",
24
- "default": "./dist/builder/index.js"
25
- },
26
- "./builder/face": {
27
- "types": "./dist/builder/envelope-face.d.ts",
28
- "default": "./dist/builder/envelope-face.js"
29
- },
30
- "./reader": {
31
- "types": "./dist/reader/index.d.ts",
32
- "default": "./dist/reader/index.js"
33
- },
34
- "./reader.css": "./src/reader/reader.css",
35
- "./webcomponent": {
36
- "types": "./dist/webcomponent/certrev-badge.d.ts",
37
- "default": "./dist/webcomponent/certrev-badge.js"
38
- },
39
- "./fixtures": {
40
- "types": "./dist/contract/fixtures.d.ts",
41
- "default": "./dist/contract/fixtures.js"
42
- },
43
- "./modal": {
44
- "types": "./dist/modal/index.d.ts",
45
- "default": "./dist/modal/index.js"
46
- },
47
- "./modal/placement": {
48
- "types": "./dist/modal/placement.d.ts",
49
- "default": "./dist/modal/placement.js"
50
- },
51
- "./modal/certrev-cert.js": "./dist/modal/certrev-cert.js"
52
- },
53
- "files": [
54
- "dist",
55
- "src",
56
- "!src/**/__tests__",
57
- "!src/**/*.test.ts",
58
- "!src/**/*.test.tsx",
59
- "README.md",
60
- "CHANGELOG.md"
61
- ],
62
- "publishConfig": {
63
- "registry": "https://registry.npmjs.org",
64
- "access": "public"
65
- },
66
- "publishTargets": [
67
- "npmjs",
68
- "github-packages"
69
- ],
70
- "scripts": {
71
- "build": "tsc && pnpm build:modal-bundle",
72
- "build:modal-bundle": "pnpm dlx esbuild@0.27.7 src/modal/certrev-cert.entry.ts --bundle --minify --target=es2020 --format=iife --legal-comments=none --tsconfig-raw='{\"compilerOptions\":{\"target\":\"ES2017\",\"module\":\"esnext\",\"strict\":true}}' --banner:js='/* biome-ignore-all format: generated cert-badge bundle, see @certrev/cert-block src/modal/ */ /* biome-ignore-all lint: generated cert-badge bundle, see @certrev/cert-block src/modal/ */' --outfile=dist/modal/certrev-cert.js",
73
- "refresh:reader": "node scripts/refresh-reader.mjs",
74
- "typecheck": "tsc --noEmit",
75
- "test": "vitest run"
76
- },
77
- "dependencies": {
78
- "@certrev/cert-contract": "^0.6.0"
79
- },
80
- "peerDependencies": {
81
- "react": ">=18.0.0"
82
- },
83
- "peerDependenciesMeta": {
84
- "react": {
85
- "optional": false
86
- }
87
- },
88
- "devDependencies": {
89
- "@testing-library/react": "^16.1.0",
90
- "@types/node": "^20.0.0",
91
- "@types/react": "^18.3.0",
92
- "@types/react-dom": "^18.3.0",
93
- "jsdom": "^25.0.0",
94
- "react": "^18.3.1",
95
- "react-dom": "^18.3.1",
96
- "typescript": "^6.0.3",
97
- "vitest": "^4.1.5"
98
- },
99
- "keywords": [
100
- "certrev",
101
- "certification",
102
- "react",
103
- "server-components",
104
- "hydrogen",
105
- "shopify",
106
- "json-ld",
107
- "schema-org",
108
- "web-component",
109
- "ed25519",
110
- "ssr"
111
- ],
112
- "license": "MIT"
113
- }
2
+ "name": "@certrev/cert-block",
3
+ "version": "2.0.1",
4
+ "description": "Headless render edge for the CertREV CertDeliveryEnvelope: SSR-safe React cert components, a framework-agnostic <certrev-badge> Web Component, a schema.org JSON-LD projector, and a fail-closed verify layer over the shared VerdictKernel.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/CertREV/cert-kit.git",
8
+ "directory": "packages/cert-block"
9
+ },
10
+ "homepage": "https://github.com/CertREV/cert-kit/tree/HEAD/packages/cert-block#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/CertREV/cert-kit/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./builder": {
23
+ "types": "./dist/builder/index.d.ts",
24
+ "default": "./dist/builder/index.js"
25
+ },
26
+ "./builder/face": {
27
+ "types": "./dist/builder/envelope-face.d.ts",
28
+ "default": "./dist/builder/envelope-face.js"
29
+ },
30
+ "./reader": {
31
+ "types": "./dist/reader/index.d.ts",
32
+ "default": "./dist/reader/index.js"
33
+ },
34
+ "./reader.css": "./src/reader/reader.css",
35
+ "./webcomponent": {
36
+ "types": "./dist/webcomponent/certrev-badge.d.ts",
37
+ "default": "./dist/webcomponent/certrev-badge.js"
38
+ },
39
+ "./fixtures": {
40
+ "types": "./dist/contract/fixtures.d.ts",
41
+ "default": "./dist/contract/fixtures.js"
42
+ },
43
+ "./modal": {
44
+ "types": "./dist/modal/index.d.ts",
45
+ "default": "./dist/modal/index.js"
46
+ },
47
+ "./modal/placement": {
48
+ "types": "./dist/modal/placement.d.ts",
49
+ "default": "./dist/modal/placement.js"
50
+ },
51
+ "./modal/certrev-cert.js": "./dist/modal/certrev-cert.js"
52
+ },
53
+ "files": [
54
+ "dist",
55
+ "src",
56
+ "!src/**/__tests__",
57
+ "!src/**/*.test.ts",
58
+ "!src/**/*.test.tsx",
59
+ "README.md",
60
+ "CHANGELOG.md"
61
+ ],
62
+ "publishConfig": {
63
+ "registry": "https://registry.npmjs.org",
64
+ "access": "public"
65
+ },
66
+ "publishTargets": [
67
+ "npmjs",
68
+ "github-packages"
69
+ ],
70
+ "dependencies": {
71
+ "@certrev/cert-contract": "^0.6.0"
72
+ },
73
+ "peerDependencies": {
74
+ "react": ">=18.0.0"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "react": {
78
+ "optional": false
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@testing-library/react": "^16.1.0",
83
+ "@types/node": "^20.0.0",
84
+ "@types/react": "^18.3.0",
85
+ "@types/react-dom": "^18.3.0",
86
+ "esbuild": "0.27.7",
87
+ "jsdom": "^25.0.0",
88
+ "playwright-core": "1.59.1",
89
+ "react": "^18.3.1",
90
+ "react-dom": "^18.3.1",
91
+ "typescript": "^6.0.3",
92
+ "vitest": "^4.1.5"
93
+ },
94
+ "keywords": [
95
+ "certrev",
96
+ "certification",
97
+ "react",
98
+ "server-components",
99
+ "hydrogen",
100
+ "shopify",
101
+ "json-ld",
102
+ "schema-org",
103
+ "web-component",
104
+ "ed25519",
105
+ "ssr"
106
+ ],
107
+ "license": "MIT",
108
+ "scripts": {
109
+ "build": "tsc && pnpm build:modal-bundle",
110
+ "build:modal-bundle": "pnpm dlx esbuild@0.27.7 src/modal/certrev-cert.entry.ts --bundle --minify --target=es2020 --format=iife --legal-comments=none --tsconfig-raw='{\"compilerOptions\":{\"target\":\"ES2017\",\"module\":\"esnext\",\"strict\":true}}' --banner:js='/* biome-ignore-all format: generated cert-badge bundle, see @certrev/cert-block src/modal/ */ /* biome-ignore-all lint: generated cert-badge bundle, see @certrev/cert-block src/modal/ */' --outfile=dist/modal/certrev-cert.js",
111
+ "refresh:reader": "node scripts/refresh-reader.mjs",
112
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.e2e.json",
113
+ "test": "vitest run",
114
+ "test:replica": "vitest run --config vitest.e2e.config.ts"
115
+ }
116
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The Builder card's beacon (2.0.1): what the card tells CertREV about itself, so a card that does not print on a brand's
3
+ * page is seen by CertREV and not only by the brand's console.
4
+ *
5
+ * One `POST <delivery origin>/api/cert/v1/beacon` per event, a `text/plain` JSON body (a CORS simple request, so no
6
+ * preflight; the origin is the delivery origin the page's `connect-src` already allows, so no new CSP source):
7
+ *
8
+ * { v: 1, client: 'cert-block', clientVersion, contract, platform: 'builder', externalId, family,
9
+ * events: [{ kind: 'fallback' | 'health', reason, outcome, status? }] }
10
+ *
11
+ * What it never carries: cookies (`credentials: 'omit'`), the page's address (`referrerPolicy: 'no-referrer'`) or text, the reader's user agent (only a
12
+ * coarse family: chromium, webkit, gecko, other, or server), or anything the reader typed. `externalId` is the Builder
13
+ * entry the block renders in, which the brand's page already sends to CertREV when it reads the card.
14
+ *
15
+ * How much: in a browser, every fallback (each placement and reason once per page, `card-report.ts`) and one health
16
+ * event per page in a hundred, at most three beacons per page (a page is a document address: an in-app navigation to
17
+ * another article starts a new budget); on a server, each placement, reason and origin at most
18
+ * once an hour per process, with a short deadline, never awaited. A beacon that fails is dropped: it never retries,
19
+ * never stores anything and never changes what the card prints.
20
+ */
21
+
22
+ import { CARD_CONTRACT_VERSION } from './card-contract.js'
23
+ import { CERT_BLOCK_VERSION } from './version.js'
24
+
25
+ export const BEACON_PATH = '/api/cert/v1/beacon'
26
+ /** Where a block with no accepted delivery marker reports: CertREV's production delivery origin. */
27
+ export const DEFAULT_BEACON_ORIGIN = 'https://portal.certrev.com'
28
+ /** The most beacons one page sends. */
29
+ export const BEACON_MAX_PER_PAGE = 3
30
+ /** The share of page views that send one health event. */
31
+ export const BEACON_HEALTH_SAMPLE = 0.01
32
+ /** A server sends one beacon per placement, reason and origin at most this often. */
33
+ export const BEACON_SERVER_INTERVAL_MS = 3_600_000
34
+ const SERVER_MAX_KEYS = 500
35
+ const SERVER_TIMEOUT_MS = 1_500
36
+
37
+ export type BeaconOutcome = 'ok' | 'unreachable' | 'http_4xx' | 'http_5xx' | 'verify_failed' | 'unsupported' | 'other'
38
+ export type BeaconFamily = 'chromium' | 'webkit' | 'gecko' | 'server' | 'other'
39
+
40
+ export interface BeaconEvent {
41
+ readonly kind: 'fallback' | 'health'
42
+ readonly reason: string
43
+ readonly outcome: BeaconOutcome
44
+ /** The HTTP status CertREV answered with, when a read failed on one (outcome `http_4xx` or `http_5xx`). */
45
+ readonly status?: number
46
+ }
47
+
48
+ export interface CardBeacon {
49
+ readonly v: 1
50
+ readonly client: 'cert-block'
51
+ readonly clientVersion: string
52
+ readonly contract: number
53
+ readonly platform: 'builder'
54
+ readonly externalId?: string
55
+ readonly family: BeaconFamily
56
+ readonly events: readonly BeaconEvent[]
57
+ }
58
+
59
+ /** How a beacon leaves: `url`, the JSON body, and where it is sent from. */
60
+ export type BeaconTransport = (url: string, body: string, where: 'browser' | 'server') => void
61
+
62
+ /** The transport the card ships with: `fetch`, never awaited, never throwing (exported for CertREV's replica harness). */
63
+ export const fetchBeaconTransport: BeaconTransport = (url, body, where) => {
64
+ try {
65
+ const send = globalThis.fetch
66
+ if (typeof send !== 'function') return
67
+ const init: RequestInit = {
68
+ method: 'POST',
69
+ body,
70
+ headers: { 'content-type': 'text/plain;charset=UTF-8' },
71
+ credentials: 'omit',
72
+ // Never the reader's page address, whatever the storefront's Referrer-Policy.
73
+ referrerPolicy: 'no-referrer',
74
+ ...(where === 'browser'
75
+ ? // Opaque and kept alive: no CORS read of the answer, and it outlives a navigation away.
76
+ { mode: 'no-cors' as const, keepalive: true }
77
+ : typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
78
+ ? { signal: AbortSignal.timeout(SERVER_TIMEOUT_MS) }
79
+ : {}),
80
+ }
81
+ void send(url, init).catch(() => {})
82
+ } catch {
83
+ // A beacon never throws into a render.
84
+ }
85
+ }
86
+
87
+ let transport: BeaconTransport = fetchBeaconTransport
88
+ let sentOnPage = 0
89
+ let healthDrawn: boolean | null = null
90
+ /** The document address the page budget belongs to. */
91
+ let budgetPage: string | null = null
92
+
93
+ /** Start a new page budget when the document's address changed (an in-app navigation). */
94
+ function samePageBudget(): void {
95
+ const page = typeof location !== 'undefined' ? `${location.pathname}${location.search}` : ''
96
+ if (page === budgetPage) return
97
+ budgetPage = page
98
+ sentOnPage = 0
99
+ healthDrawn = null
100
+ }
101
+ const sentOnServer = new Map<string, number>()
102
+ let random: () => number = Math.random
103
+ let clock: () => number = Date.now
104
+
105
+ /** The coarse browser family, from the user agent; nothing else of it is kept. */
106
+ export function browserFamily(userAgent: string): BeaconFamily {
107
+ if (/Firefox\//.test(userAgent)) return 'gecko'
108
+ if (/(Chrome|Chromium|CriOS|Edg)\//.test(userAgent)) return 'chromium'
109
+ if (/AppleWebKit\//.test(userAgent)) return 'webkit'
110
+ return 'other'
111
+ }
112
+
113
+ /** The delivery origin a beacon goes to: an `https:` origin on certrev.com, else CertREV's production origin. */
114
+ function beaconOrigin(baseUrl: string | undefined): string {
115
+ if (!baseUrl) return DEFAULT_BEACON_ORIGIN
116
+ try {
117
+ const url = new URL(baseUrl)
118
+ const host = url.hostname
119
+ if (url.protocol === 'https:' && (host === 'certrev.com' || host.endsWith('.certrev.com'))) return url.origin
120
+ } catch {
121
+ // Fall through.
122
+ }
123
+ return DEFAULT_BEACON_ORIGIN
124
+ }
125
+
126
+ export function beaconBody(externalId: string, events: readonly BeaconEvent[], family: BeaconFamily): CardBeacon {
127
+ return {
128
+ v: 1,
129
+ client: 'cert-block',
130
+ clientVersion: CERT_BLOCK_VERSION,
131
+ contract: CARD_CONTRACT_VERSION,
132
+ platform: 'builder',
133
+ ...(externalId ? { externalId } : {}),
134
+ family,
135
+ events,
136
+ }
137
+ }
138
+
139
+ /** Send one event from the browser, within the page's budget. */
140
+ export function sendBrowserBeacon(baseUrl: string | undefined, externalId: string, event: BeaconEvent): void {
141
+ if (typeof document === 'undefined') return
142
+ samePageBudget()
143
+ if (sentOnPage >= BEACON_MAX_PER_PAGE) return
144
+ sentOnPage++
145
+ const userAgent = typeof navigator !== 'undefined' ? String(navigator.userAgent ?? '') : ''
146
+ const body = beaconBody(externalId, [event], browserFamily(userAgent))
147
+ transport(`${beaconOrigin(baseUrl)}${BEACON_PATH}`, JSON.stringify(body), 'browser')
148
+ }
149
+
150
+ /** Send one health event from the browser on one page view in a hundred (drawn once per page). */
151
+ export function maybeSendBrowserHealth(baseUrl: string | undefined, externalId: string): void {
152
+ if (typeof document === 'undefined') return
153
+ samePageBudget()
154
+ if (healthDrawn === null) healthDrawn = random() < BEACON_HEALTH_SAMPLE
155
+ if (!healthDrawn) return
156
+ healthDrawn = false
157
+ sendBrowserBeacon(baseUrl, externalId, { kind: 'health', reason: 'card_printed', outcome: 'ok' })
158
+ }
159
+
160
+ /** Send one event from a server: each placement, reason and origin at most once an hour per process. */
161
+ export function sendServerBeacon(baseUrl: string | undefined, externalId: string, event: BeaconEvent): void {
162
+ const origin = beaconOrigin(baseUrl)
163
+ const key = `${origin}|${externalId}|${event.kind}|${event.reason}`
164
+ const now = clock()
165
+ const last = sentOnServer.get(key)
166
+ if (last !== undefined && now - last < BEACON_SERVER_INTERVAL_MS) return
167
+ sentOnServer.delete(key)
168
+ if (sentOnServer.size >= SERVER_MAX_KEYS) {
169
+ const oldest = sentOnServer.keys().next().value
170
+ if (oldest !== undefined) sentOnServer.delete(oldest)
171
+ }
172
+ sentOnServer.set(key, now)
173
+ transport(`${origin}${BEACON_PATH}`, JSON.stringify(beaconBody(externalId, [event], 'server')), 'server')
174
+ }
175
+
176
+ /**
177
+ * Test seam: replace the transport (and the sampling draw and clock) and forget the page's and the process's budget.
178
+ * Returns a restore.
179
+ */
180
+ export function setCardBeaconForTests(
181
+ overrides: { transport?: BeaconTransport; random?: () => number; now?: () => number } = {},
182
+ ): () => void {
183
+ const previous = { transport, random, clock }
184
+ transport = overrides.transport ?? (() => {})
185
+ random = overrides.random ?? (() => 1)
186
+ clock = overrides.now ?? Date.now
187
+ forgetCardBeaconsForTests()
188
+ return () => {
189
+ transport = previous.transport
190
+ random = previous.random
191
+ clock = previous.clock
192
+ }
193
+ }
194
+
195
+ /** Test seam: a new page and a new process. */
196
+ export function forgetCardBeaconsForTests(): void {
197
+ sentOnPage = 0
198
+ healthDrawn = null
199
+ budgetPage = null
200
+ sentOnServer.clear()
201
+ }