@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.
- package/CHANGELOG.md +235 -0
- package/README.md +162 -9
- package/dist/builder/card-beacon.d.ts +73 -0
- package/dist/builder/card-beacon.d.ts.map +1 -0
- package/dist/builder/card-beacon.js +178 -0
- package/dist/builder/card-beacon.js.map +1 -0
- package/dist/builder/card-contract.d.ts +151 -0
- package/dist/builder/card-contract.d.ts.map +1 -0
- package/dist/builder/card-contract.js +286 -0
- package/dist/builder/card-contract.js.map +1 -0
- package/dist/builder/card-placement.d.ts +45 -0
- package/dist/builder/card-placement.d.ts.map +1 -0
- package/dist/builder/card-placement.js +193 -0
- package/dist/builder/card-placement.js.map +1 -0
- package/dist/builder/card-report.d.ts +49 -0
- package/dist/builder/card-report.d.ts.map +1 -0
- package/dist/builder/card-report.js +97 -0
- package/dist/builder/card-report.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +7 -5
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +202 -170
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/contract-card.d.ts +70 -0
- package/dist/builder/contract-card.d.ts.map +1 -0
- package/dist/builder/contract-card.js +191 -0
- package/dist/builder/contract-card.js.map +1 -0
- package/dist/builder/delivered-face.d.ts +9 -0
- package/dist/builder/delivered-face.d.ts.map +1 -1
- package/dist/builder/delivered-face.js +18 -5
- package/dist/builder/delivered-face.js.map +1 -1
- package/dist/builder/delivery-runtime.d.ts +27 -3
- package/dist/builder/delivery-runtime.d.ts.map +1 -1
- package/dist/builder/delivery-runtime.js +40 -4
- package/dist/builder/delivery-runtime.js.map +1 -1
- package/dist/builder/index.d.ts +7 -4
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +8 -4
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/ssr-seed.d.ts +23 -4
- package/dist/builder/ssr-seed.d.ts.map +1 -1
- package/dist/builder/ssr-seed.js +29 -6
- package/dist/builder/ssr-seed.js.map +1 -1
- package/dist/builder/version.d.ts +3 -0
- package/dist/builder/version.d.ts.map +1 -0
- package/dist/builder/version.js +3 -0
- package/dist/builder/version.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reader/reader-css.generated.d.ts +1 -1
- package/dist/reader/reader-css.generated.d.ts.map +1 -1
- package/dist/reader/reader-css.generated.js +1 -1
- package/dist/reader/reader-css.generated.js.map +1 -1
- package/dist/reader/render.d.ts.map +1 -1
- package/dist/reader/render.js +71 -34
- package/dist/reader/render.js.map +1 -1
- package/dist/reader/tokens.d.ts +2 -0
- package/dist/reader/tokens.d.ts.map +1 -1
- package/dist/reader/tokens.js +28 -6
- package/dist/reader/tokens.js.map +1 -1
- package/dist/verify/get-verified-delivery.d.ts +32 -0
- package/dist/verify/get-verified-delivery.d.ts.map +1 -1
- package/dist/verify/get-verified-delivery.js +30 -5
- package/dist/verify/get-verified-delivery.js.map +1 -1
- package/dist/verify/key-set.d.ts +67 -0
- package/dist/verify/key-set.d.ts.map +1 -0
- package/dist/verify/key-set.js +190 -0
- package/dist/verify/key-set.js.map +1 -0
- package/package.json +115 -112
- package/src/builder/card-beacon.ts +201 -0
- package/src/builder/card-contract.ts +336 -0
- package/src/builder/card-placement.ts +208 -0
- package/src/builder/card-report.ts +126 -0
- package/src/builder/cert-review-card.tsx +285 -201
- package/src/builder/contract-card.tsx +216 -0
- package/src/builder/delivered-face.ts +18 -4
- package/src/builder/delivery-runtime.ts +46 -4
- package/src/builder/index.tsx +16 -4
- package/src/builder/ssr-seed.ts +37 -7
- package/src/builder/version.ts +2 -0
- package/src/index.ts +9 -0
- package/src/reader/reader-css.generated.ts +1 -1
- package/src/reader/reader.css +297 -2
- package/src/reader/render.ts +87 -38
- package/src/reader/tokens.ts +29 -6
- package/src/verify/get-verified-delivery.ts +54 -5
- package/src/verify/key-set.ts +225 -0
package/package.json
CHANGED
|
@@ -1,113 +1,116 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|