@certrev/cert-block 2.0.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 (56) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +45 -8
  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 +10 -2
  8. package/dist/builder/card-contract.d.ts.map +1 -1
  9. package/dist/builder/card-contract.js +27 -3
  10. package/dist/builder/card-contract.js.map +1 -1
  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 +26 -14
  16. package/dist/builder/card-report.d.ts.map +1 -1
  17. package/dist/builder/card-report.js +68 -14
  18. package/dist/builder/card-report.js.map +1 -1
  19. package/dist/builder/cert-review-card.d.ts +4 -4
  20. package/dist/builder/cert-review-card.d.ts.map +1 -1
  21. package/dist/builder/cert-review-card.js +77 -189
  22. package/dist/builder/cert-review-card.js.map +1 -1
  23. package/dist/builder/contract-card.d.ts +18 -1
  24. package/dist/builder/contract-card.d.ts.map +1 -1
  25. package/dist/builder/contract-card.js +60 -10
  26. package/dist/builder/contract-card.js.map +1 -1
  27. package/dist/builder/delivery-runtime.d.ts +13 -3
  28. package/dist/builder/delivery-runtime.d.ts.map +1 -1
  29. package/dist/builder/delivery-runtime.js +17 -4
  30. package/dist/builder/delivery-runtime.js.map +1 -1
  31. package/dist/builder/index.d.ts +4 -4
  32. package/dist/builder/index.js +4 -4
  33. package/dist/builder/ssr-seed.d.ts +5 -4
  34. package/dist/builder/ssr-seed.d.ts.map +1 -1
  35. package/dist/builder/ssr-seed.js +4 -3
  36. package/dist/builder/ssr-seed.js.map +1 -1
  37. package/dist/builder/version.d.ts +3 -0
  38. package/dist/builder/version.d.ts.map +1 -0
  39. package/dist/builder/version.js +3 -0
  40. package/dist/builder/version.js.map +1 -0
  41. package/dist/verify/get-verified-delivery.d.ts +4 -0
  42. package/dist/verify/get-verified-delivery.d.ts.map +1 -1
  43. package/dist/verify/get-verified-delivery.js +9 -3
  44. package/dist/verify/get-verified-delivery.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/builder/card-beacon.ts +201 -0
  47. package/src/builder/card-contract.ts +25 -3
  48. package/src/builder/card-placement.ts +208 -0
  49. package/src/builder/card-report.ts +79 -14
  50. package/src/builder/cert-review-card.tsx +100 -230
  51. package/src/builder/contract-card.tsx +65 -8
  52. package/src/builder/delivery-runtime.ts +17 -4
  53. package/src/builder/index.tsx +4 -4
  54. package/src/builder/ssr-seed.ts +5 -4
  55. package/src/builder/version.ts +2 -0
  56. package/src/verify/get-verified-delivery.ts +13 -3
@@ -24,6 +24,7 @@ import type { ResolvePublicKeyByKid } from '../contract/kernel.js'
24
24
  import { TtlCache } from '../verify/cache.js'
25
25
  import type { CachedDelivery } from '../verify/get-verified-delivery.js'
26
26
  import { forgetKeySetsForTests, keySetResolver } from '../verify/key-set.js'
27
+ import { setCardBeaconForTests } from './card-beacon.js'
27
28
  import { forgetCardDigestsForTests } from './card-contract.js'
28
29
  import { forgetCardReportsForTests } from './card-report.js'
29
30
  import { forgetFaceHashesForTests } from './delivered-face.js'
@@ -32,8 +33,17 @@ import { forgetPageSeedsForTests } from './ssr-seed.js'
32
33
 
33
34
  /** The card's server read deadline. A brand's server waits this long once per cold placement. */
34
35
  export const CARD_DELIVERY_TIMEOUT_MS = 8_000
35
- /** How long past its 60s TTL a certificate may still paint while it is refreshed or CertREV is down. */
36
- export const CARD_DELIVERY_STALE_MS = 300_000
36
+ /**
37
+ * 2.0.1: how long a verified answer is fresh. With the stale window below, a card adds at most one minute to the Delivery
38
+ * API's own cache (`max-age=60, stale-while-revalidate=240`), so a revoked card is gone within ADR 0001 §7's five to
39
+ * six minutes (2.0.0's 60 s + 300 s here stacked to about twelve).
40
+ */
41
+ export const CARD_DELIVERY_TTL_MS = 30_000
42
+ /**
43
+ * How long past its TTL a certificate may still paint while it is refreshed or CertREV is down (2.0.1: 30 s; 2.0.0:
44
+ * 300 s). Never longer: past it, a read that cannot reach CertREV prints nothing.
45
+ */
46
+ export const CARD_DELIVERY_STALE_MS = 30_000
37
47
 
38
48
  export interface CardDeliveryRuntime {
39
49
  /** The baked kids: the package's own trust root (and the only keys that may sign a published key set). */
@@ -54,7 +64,7 @@ const bakedResolveKid: ResolvePublicKeyByKid = (kid) =>
54
64
 
55
65
  function newCardCache(now?: () => number): TtlCache<CachedDelivery> {
56
66
  return new TtlCache<CachedDelivery>({
57
- ttlMs: 60_000,
67
+ ttlMs: CARD_DELIVERY_TTL_MS,
58
68
  negativeTtlMs: 5_000,
59
69
  staleMs: CARD_DELIVERY_STALE_MS,
60
70
  // A refresh the host runtime abandoned (workerd cancels a finished request's I/O) never
@@ -94,7 +104,8 @@ export function cardResolveKidFor(rt: CardDeliveryRuntime, baseUrl: string): Res
94
104
  /**
95
105
  * Test seam: replace parts of the runtime (a fresh cache unless one is given) and forget the page's envelope seeds
96
106
  * (a fresh runtime is a fresh page), (1.3.0) every delivered face's memoized hashes and (2.0.0) every card
97
- * document's digest and every published key set. Returns a restore.
107
+ * document's digest and every published key set. 2.0.1: beacons go nowhere (a test that wants them sets its own
108
+ * transport with `setCardBeaconForTests` after this). Returns a restore.
98
109
  */
99
110
  export function setCardDeliveryRuntimeForTests(overrides: Partial<CardDeliveryRuntime>): () => void {
100
111
  forgetPageSeedsForTests()
@@ -102,6 +113,7 @@ export function setCardDeliveryRuntimeForTests(overrides: Partial<CardDeliveryRu
102
113
  forgetCardDigestsForTests()
103
114
  forgetKeySetsForTests()
104
115
  forgetCardReportsForTests()
116
+ const restoreBeacon = setCardBeaconForTests()
105
117
  const previous = runtime
106
118
  const now = overrides.now ?? previous.now
107
119
  runtime = {
@@ -111,5 +123,6 @@ export function setCardDeliveryRuntimeForTests(overrides: Partial<CardDeliveryRu
111
123
  }
112
124
  return () => {
113
125
  runtime = previous
126
+ restoreBeacon()
114
127
  }
115
128
  }
@@ -31,10 +31,10 @@
31
31
  * (`face: { html, css }`, unsigned) and the verified envelope signs that face's sha256 digests for the
32
32
  * def version served (`content.faceDigest`), an unpinned whole-card block paints that face instead;
33
33
  * markup that does not hash to the signed digest is never painted. A revoked, expired, unverifiable or unreachable cert, a marker that
34
- * is not exactly that shape, or a missing entry id renders nothing. A block WITHOUT the marker
35
- * (every entry exported before 1.1.0) renders from its options exactly as 1.0.3 did, so the
36
- * paragraph above still describes it: an editor who deletes the marker gets the options face back,
37
- * which is the 1.0.3 trust boundary, not a new one.
34
+ * is not exactly that shape, or a missing entry id renders nothing. 2.0.1: a block WITHOUT the marker
35
+ * (every entry exported before 1.1.0) renders nothing too, and reports `no_marker`: through 2.0.0 it
36
+ * painted its options as 1.0.3 did, unsigned export-time claims an editor could change. So deleting the
37
+ * marker no longer brings any face back.
38
38
  *
39
39
  * Three things the card DOES enforce, and they are the limit of it: no chrome at all without a
40
40
  * `reviewerName` and a `verifyUrl`; a credential renders only welded to its dated verification
@@ -41,9 +41,10 @@
41
41
  * page carried for another version.
42
42
  *
43
43
  * WHEN THE SERVER HAD NO ENVELOPE TO PAINT (1.2.0) the same attribute carries a fallback record instead of a seed:
44
- * `{ v: 1, baseUrl, externalId, design?, face }`, where `face` is `'options'` (CertREV could not be reached, so the
45
- * server painted the block's own options face) or `'none'` (the server painted nothing: revoked, expired, unverifiable,
46
- * for another entry). A browser that renders the card again (hydration, or React's client re-render after a hydration
44
+ * `{ v: 1, baseUrl, externalId, design?, face, reason? }`, where `face` is `'none'` (the server painted nothing:
45
+ * revoked, expired, unverifiable, for another entry, or, 2.0.1, CertREV not reached, with `reason` naming why) or, on a
46
+ * page a 2.0.0 server rendered, `'options'` (CertREV could not be reached and that server painted the block's options
47
+ * face; 2.0.1 prints nothing for it). A browser that renders the card again (hydration, or React's client re-render after a hydration
47
48
  * error elsewhere on the page) paints what the record says, so it neither reads the Delivery API nor shows a card the
48
49
  * server withheld. A record holds no payload, so `parseSeed` never takes one for a seed. A `'none'` record sits on a
49
50
  * `hidden` element with nothing in it.
@@ -114,7 +115,7 @@ export interface PageSeed {
114
115
  readonly raw: string
115
116
  }
116
117
 
117
- /** What the server painted with no verified envelope: the block's options face, or nothing. */
118
+ /** What the server painted with no verified envelope: nothing (2.0.1 never writes `'options'`; a 2.0.0 page may hold one). */
118
119
  export type FallbackFace = 'options' | 'none'
119
120
 
120
121
  /** The record a server writes where it had no envelope to paint (the file header). */
@@ -0,0 +1,2 @@
1
+ /** This package's version, as its beacon reports it (held to `package.json` by `version.test.ts`). */
2
+ export const CERT_BLOCK_VERSION = '2.0.1'
@@ -106,6 +106,8 @@ export type CachedDelivery =
106
106
  readonly reason: CertSuppressReason
107
107
  /** 1.2.0: true when CertREV was not reached (no answer about the credential); see the module header. */
108
108
  readonly unreachable?: true
109
+ /** 2.0.1: the HTTP status, when CertREV answered with a non-OK status other than 404/410. */
110
+ readonly status?: number
109
111
  }
110
112
 
111
113
  /** The answer for one render: the verdict AT THIS RENDER'S CLOCK, and the def to paint it with. */
@@ -117,6 +119,8 @@ export interface VerifiedDelivery {
117
119
  readonly signature?: CertSignature
118
120
  /** 1.2.0: on a suppress verdict, true when CertREV was not reached; else undefined. */
119
121
  readonly unreachable?: true
122
+ /** 2.0.1: with `unreachable`, the HTTP status when CertREV answered with one (a 5xx, a 429, ...); else undefined. */
123
+ readonly status?: number
120
124
  /**
121
125
  * 1.3.0: on a `render` verdict, the server-rendered face served beside the artifact, when the response carried a
122
126
  * well-formed one; else undefined (never on a suppress verdict). UNVERIFIED: see `DeliveredFace`.
@@ -175,8 +179,13 @@ function suppressed(reason: CertSuppressReason): CachedDelivery {
175
179
  }
176
180
 
177
181
  /** A read that did not reach CertREV: the same least-wrong reason as before 1.2.0, marked `unreachable`. */
178
- function unreachable(): CachedDelivery {
179
- return { kind: 'suppressed', reason: 'unsupported_contract_version', unreachable: true }
182
+ function unreachable(status?: number): CachedDelivery {
183
+ return {
184
+ kind: 'suppressed',
185
+ reason: 'unsupported_contract_version',
186
+ unreachable: true,
187
+ ...(status === undefined ? {} : { status }),
188
+ }
180
189
  }
181
190
 
182
191
  const isUnreachable = (v: CachedDelivery): boolean => v.kind === 'suppressed' && v.unreachable === true
@@ -312,7 +321,7 @@ async function loadDelivery(opts: GetVerifiedDeliveryOptions): Promise<CachedDel
312
321
  // Nothing served for this placement: never certified, or withdrawn. A LIVE revocation is a 200
313
322
  // tombstone, so a 4xx here genuinely means "no credential for this subject".
314
323
  if (res.status === 404 || res.status === 410) return suppressed('subject_mismatch')
315
- if (!res.ok) return unreachable()
324
+ if (!res.ok) return unreachable(res.status)
316
325
  let body: unknown
317
326
  try {
318
327
  body = await res.json()
@@ -356,6 +365,7 @@ export function settleDelivery(cached: CachedDelivery, context: RenderContext):
356
365
  verdict: { decision: 'suppress', reason: cached.reason },
357
366
  renderDef: null,
358
367
  ...(cached.unreachable ? { unreachable: true as const } : {}),
368
+ ...(cached.unreachable && cached.status !== undefined ? { status: cached.status } : {}),
359
369
  }
360
370
  }
361
371
  const verdict = renderVerdict(cached.payload, context)