@certrev/cert-block 1.2.0-canary.7 → 1.2.1-canary.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.
- package/CHANGELOG.md +155 -63
- package/README.md +130 -21
- package/dist/builder/card-reader-css.d.ts +14 -0
- package/dist/builder/card-reader-css.d.ts.map +1 -0
- package/dist/builder/card-reader-css.js +17 -0
- package/dist/builder/card-reader-css.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +8 -0
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +271 -41
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/delivery-runtime.d.ts +15 -1
- package/dist/builder/delivery-runtime.d.ts.map +1 -1
- package/dist/builder/delivery-runtime.js +31 -3
- package/dist/builder/delivery-runtime.js.map +1 -1
- package/dist/builder/ssr-seed.d.ts +168 -0
- package/dist/builder/ssr-seed.d.ts.map +1 -0
- package/dist/builder/ssr-seed.js +421 -0
- package/dist/builder/ssr-seed.js.map +1 -0
- package/dist/verify/cache.d.ts +39 -2
- package/dist/verify/cache.d.ts.map +1 -1
- package/dist/verify/cache.js +61 -12
- package/dist/verify/cache.js.map +1 -1
- package/dist/verify/get-verified-delivery.d.ts +28 -3
- package/dist/verify/get-verified-delivery.d.ts.map +1 -1
- package/dist/verify/get-verified-delivery.js +46 -9
- package/dist/verify/get-verified-delivery.js.map +1 -1
- package/package.json +112 -112
- package/src/builder/card-reader-css.ts +19 -0
- package/src/builder/cert-review-card.tsx +359 -56
- package/src/builder/delivery-runtime.ts +33 -3
- package/src/builder/ssr-seed.ts +496 -0
- package/src/verify/cache.ts +87 -11
- package/src/verify/get-verified-delivery.ts +77 -11
|
@@ -29,12 +29,20 @@
|
|
|
29
29
|
* `peekVerifiedDelivery` is the synchronous read of the same cache: a React component suspends on
|
|
30
30
|
* `getVerifiedDelivery` once, then reads the settled value without a second promise (which is what
|
|
31
31
|
* keeps a streaming SSR render from suspending forever on a fresh promise every retry).
|
|
32
|
+
*
|
|
33
|
+
* 1.2.0: a suppressed read that only failed to REACH CertREV (the fetch threw or hit its deadline, a 5xx or other
|
|
34
|
+
* non-OK status but 404/410, a body that is not JSON or not an object) is marked `unreachable`. It says nothing
|
|
35
|
+
* about the credential, so a cache with a stale window keeps a verified entry over it (stale-if-error), and the
|
|
36
|
+
* card paints its options face instead of nothing. A definitive answer (404/410, a key or signature that does not
|
|
37
|
+
* verify, a revoked, expired or mismatched artifact) is never `unreachable`. `readVerifiedDelivery` is the
|
|
38
|
+
* synchronous read that also serves a stale entry while refreshing it.
|
|
32
39
|
*/
|
|
33
40
|
|
|
34
41
|
import type { CertChromeRenderDef } from '../builder/cert-chrome-data.js'
|
|
35
42
|
import type {
|
|
36
43
|
CertDeliveryArtifact,
|
|
37
44
|
CertPayload,
|
|
45
|
+
CertSignature,
|
|
38
46
|
CertSuppressReason,
|
|
39
47
|
CertVerdict,
|
|
40
48
|
RenderContext,
|
|
@@ -44,16 +52,34 @@ import { renderVerdict, verifyArtifact } from '../contract/kernel.js'
|
|
|
44
52
|
import { TtlCache } from './cache.js'
|
|
45
53
|
import { DEFAULT_FETCH_TIMEOUT_MS, fetchWithDeadline } from './fetch-with-deadline.js'
|
|
46
54
|
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* What one Delivery read settles to, as cached: a verified payload + its def (and, from 1.2.0, the signature it
|
|
57
|
+
* verified with, so a server render can hand the signed envelope to the browser), or the reason not.
|
|
58
|
+
*/
|
|
48
59
|
export type CachedDelivery =
|
|
49
|
-
| {
|
|
50
|
-
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'verified'
|
|
62
|
+
readonly payload: CertPayload
|
|
63
|
+
readonly renderDef: CertChromeRenderDef | null
|
|
64
|
+
/** Absent only in a cache filled by hand (a test, a custom cache); a read always keeps it. */
|
|
65
|
+
readonly signature?: CertSignature
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: 'suppressed'
|
|
69
|
+
readonly reason: CertSuppressReason
|
|
70
|
+
/** 1.2.0: true when CertREV was not reached (no answer about the credential); see the module header. */
|
|
71
|
+
readonly unreachable?: true
|
|
72
|
+
}
|
|
51
73
|
|
|
52
74
|
/** The answer for one render: the verdict AT THIS RENDER'S CLOCK, and the def to paint it with. */
|
|
53
75
|
export interface VerifiedDelivery {
|
|
54
76
|
readonly verdict: CertVerdict
|
|
55
77
|
/** The brand's render def, or null (no def resolved, a malformed one, or a suppress verdict). */
|
|
56
78
|
readonly renderDef: CertChromeRenderDef | null
|
|
79
|
+
/** The verified envelope's signature, on a `render` verdict whose read kept it; else undefined. */
|
|
80
|
+
readonly signature?: CertSignature
|
|
81
|
+
/** 1.2.0: on a suppress verdict, true when CertREV was not reached; else undefined. */
|
|
82
|
+
readonly unreachable?: true
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
export interface GetVerifiedDeliveryOptions {
|
|
@@ -70,7 +96,7 @@ export interface GetVerifiedDeliveryOptions {
|
|
|
70
96
|
readonly cache?: TtlCache<CachedDelivery>
|
|
71
97
|
/** TTL in ms for a VERIFIED read (default: the cache's positive TTL, 60_000 on the shared one). */
|
|
72
98
|
readonly ttlMs?: number
|
|
73
|
-
/** Deadline in ms for the fetch (default `DEFAULT_FETCH_TIMEOUT_MS`, 3s). */
|
|
99
|
+
/** Deadline in ms for the fetch (default `DEFAULT_FETCH_TIMEOUT_MS`, 3s; the Builder card's runtime passes 8s). */
|
|
74
100
|
readonly timeoutMs?: number
|
|
75
101
|
/**
|
|
76
102
|
* 1.2.0-canary.5: read one saved design version instead of the brand's live def. CertREV's own canary
|
|
@@ -96,6 +122,13 @@ function suppressed(reason: CertSuppressReason): CachedDelivery {
|
|
|
96
122
|
return { kind: 'suppressed', reason }
|
|
97
123
|
}
|
|
98
124
|
|
|
125
|
+
/** A read that did not reach CertREV: the same least-wrong reason as before 1.2.0, marked `unreachable`. */
|
|
126
|
+
function unreachable(): CachedDelivery {
|
|
127
|
+
return { kind: 'suppressed', reason: 'unsupported_contract_version', unreachable: true }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isUnreachable = (v: CachedDelivery): boolean => v.kind === 'suppressed' && v.unreachable === true
|
|
131
|
+
|
|
99
132
|
/** The Delivery route; a design pin adds `?design=<v>` and, with a brand, `&designBrand=<brand>`. */
|
|
100
133
|
export function deliveryUrl(baseUrl: string, platform: string, externalId: string, design?: DesignPin): string {
|
|
101
134
|
const base = baseUrl.replace(/\/+$/, '')
|
|
@@ -210,19 +243,19 @@ async function loadDelivery(opts: GetVerifiedDeliveryOptions): Promise<CachedDel
|
|
|
210
243
|
)
|
|
211
244
|
} catch {
|
|
212
245
|
// Unreachable, DNS/TLS, or the deadline. Same least-wrong member as getVerifiedEnvelope.
|
|
213
|
-
return
|
|
246
|
+
return unreachable()
|
|
214
247
|
}
|
|
215
248
|
// Nothing served for this placement: never certified, or withdrawn. A LIVE revocation is a 200
|
|
216
249
|
// tombstone, so a 4xx here genuinely means "no credential for this subject".
|
|
217
250
|
if (res.status === 404 || res.status === 410) return suppressed('subject_mismatch')
|
|
218
|
-
if (!res.ok) return
|
|
251
|
+
if (!res.ok) return unreachable()
|
|
219
252
|
let body: unknown
|
|
220
253
|
try {
|
|
221
254
|
body = await res.json()
|
|
222
255
|
} catch {
|
|
223
|
-
return
|
|
256
|
+
return unreachable()
|
|
224
257
|
}
|
|
225
|
-
if (!isRecord(body)) return
|
|
258
|
+
if (!isRecord(body)) return unreachable()
|
|
226
259
|
let verdict: CertVerdict
|
|
227
260
|
try {
|
|
228
261
|
// `verifyArtifact`, so a tombstone verifies as itself and reports 'revoked'. The kernel reads
|
|
@@ -233,14 +266,28 @@ async function loadDelivery(opts: GetVerifiedDeliveryOptions): Promise<CachedDel
|
|
|
233
266
|
return suppressed('unknown_key')
|
|
234
267
|
}
|
|
235
268
|
if (verdict.decision !== 'render') return suppressed(verdict.reason)
|
|
236
|
-
|
|
269
|
+
// A `render` verdict came from an envelope (a tombstone never renders), so `signature` is the one just verified.
|
|
270
|
+
const signature = isRecord(body.signature) ? (body.signature as unknown as CertSignature) : undefined
|
|
271
|
+
return {
|
|
272
|
+
kind: 'verified',
|
|
273
|
+
payload: verdict.payload,
|
|
274
|
+
renderDef: parseRenderDef(body.renderDef),
|
|
275
|
+
...(signature ? { signature } : {}),
|
|
276
|
+
}
|
|
237
277
|
}
|
|
238
278
|
|
|
239
279
|
/** Apply the render-time policy to a cached read: lifecycle + subject at THIS render's clock. */
|
|
240
280
|
export function settleDelivery(cached: CachedDelivery, context: RenderContext): VerifiedDelivery {
|
|
241
|
-
if (cached.kind === 'suppressed')
|
|
281
|
+
if (cached.kind === 'suppressed') {
|
|
282
|
+
return {
|
|
283
|
+
verdict: { decision: 'suppress', reason: cached.reason },
|
|
284
|
+
renderDef: null,
|
|
285
|
+
...(cached.unreachable ? { unreachable: true as const } : {}),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
242
288
|
const verdict = renderVerdict(cached.payload, context)
|
|
243
|
-
|
|
289
|
+
if (verdict.decision !== 'render') return { verdict, renderDef: null }
|
|
290
|
+
return { verdict, renderDef: cached.renderDef, ...(cached.signature ? { signature: cached.signature } : {}) }
|
|
244
291
|
}
|
|
245
292
|
|
|
246
293
|
/**
|
|
@@ -255,10 +302,29 @@ export async function getVerifiedDelivery(opts: GetVerifiedDeliveryOptions): Pro
|
|
|
255
302
|
() => loadDelivery(opts),
|
|
256
303
|
(v) => v.kind === 'suppressed',
|
|
257
304
|
opts.ttlMs,
|
|
305
|
+
isUnreachable,
|
|
258
306
|
)
|
|
259
307
|
return settleDelivery(cached, opts.context)
|
|
260
308
|
}
|
|
261
309
|
|
|
310
|
+
/**
|
|
311
|
+
* 1.2.0, the synchronous read a render uses: the settled answer for a fresh cached read; or, inside the cache's
|
|
312
|
+
* stale window, the stale read's answer at once while ONE background refresh (the same fetch `getVerifiedDelivery`
|
|
313
|
+
* makes) replaces it; else `undefined`, and the caller awaits `getVerifiedDelivery`. The verdict is still settled
|
|
314
|
+
* at `context.now`, so a stale payload that has expired or been revoked in its own fields still suppresses.
|
|
315
|
+
*/
|
|
316
|
+
export function readVerifiedDelivery(opts: GetVerifiedDeliveryOptions): VerifiedDelivery | undefined {
|
|
317
|
+
const cache = opts.cache ?? sharedDeliveryCache
|
|
318
|
+
const cached = cache.peekOrRefresh(
|
|
319
|
+
deliveryCacheKey(opts),
|
|
320
|
+
() => loadDelivery(opts),
|
|
321
|
+
(v) => v.kind === 'suppressed',
|
|
322
|
+
opts.ttlMs,
|
|
323
|
+
isUnreachable,
|
|
324
|
+
)
|
|
325
|
+
return cached === undefined ? undefined : settleDelivery(cached, opts.context)
|
|
326
|
+
}
|
|
327
|
+
|
|
262
328
|
/**
|
|
263
329
|
* The synchronous read: the settled answer when a fresh read is cached, else `undefined` (the
|
|
264
330
|
* caller then awaits `getVerifiedDelivery`). Never fetches.
|