@certrev/cert-block 0.1.0 → 0.1.2

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 (37) hide show
  1. package/dist/builder/index.d.ts +73 -0
  2. package/dist/builder/index.d.ts.map +1 -0
  3. package/dist/builder/index.js +63 -0
  4. package/dist/builder/index.js.map +1 -0
  5. package/dist/contract/fixtures.d.ts +1 -1
  6. package/dist/contract/fixtures.d.ts.map +1 -1
  7. package/dist/contract/fixtures.js +8 -4
  8. package/dist/contract/fixtures.js.map +1 -1
  9. package/dist/contract/kernel.d.ts +2 -2
  10. package/dist/contract/kernel.js +2 -2
  11. package/dist/index.d.ts +9 -5
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +11 -6
  14. package/dist/index.js.map +1 -1
  15. package/dist/verify/get-verified-envelope.d.ts.map +1 -1
  16. package/dist/verify/get-verified-envelope.js +28 -4
  17. package/dist/verify/get-verified-envelope.js.map +1 -1
  18. package/dist/verify/resolve-kid.d.ts.map +1 -1
  19. package/dist/verify/resolve-kid.js +3 -1
  20. package/dist/verify/resolve-kid.js.map +1 -1
  21. package/package.json +77 -69
  22. package/src/__tests__/verify.test.ts +36 -0
  23. package/src/builder/__tests__/builder.test.tsx +81 -0
  24. package/src/builder/index.tsx +100 -0
  25. package/src/contract/fixtures.ts +13 -9
  26. package/src/contract/kernel.ts +2 -2
  27. package/src/index.ts +11 -6
  28. package/src/verify/get-verified-envelope.ts +29 -4
  29. package/src/verify/resolve-kid.ts +3 -1
  30. package/dist/contract/kernel-contract.d.ts +0 -154
  31. package/dist/contract/kernel-contract.d.ts.map +0 -1
  32. package/dist/contract/kernel-contract.js +0 -35
  33. package/dist/contract/kernel-contract.js.map +0 -1
  34. package/dist/contract/kernel-stub.d.ts +0 -44
  35. package/dist/contract/kernel-stub.d.ts.map +0 -1
  36. package/dist/contract/kernel-stub.js +0 -163
  37. package/dist/contract/kernel-stub.js.map +0 -1
@@ -119,6 +119,42 @@ describe('getVerifiedEnvelope — metafield source', () => {
119
119
  })
120
120
  expect(verdict.decision).toBe('suppress')
121
121
  })
122
+
123
+ it('DISTINCT metafield envelopes at one placement do NOT collide on a shared cache (value-keyed)', async () => {
124
+ // Regression: the metafield cache key must include a hash of the envelope value, else
125
+ // the first verdict (render) is served for a later, DIFFERENT envelope — a fail-closed
126
+ // violation (an unverified/tampered credential rendered). Here a valid + a tampered
127
+ // envelope share the SAME resolver, context, and cache; each must get its own verdict.
128
+ const { envelope, resolveKid } = makeSignedEnvelope()
129
+ const tampered: CertDeliveryEnvelope = {
130
+ ...envelope,
131
+ payload: {
132
+ ...envelope.payload,
133
+ content: {
134
+ ...envelope.payload.content,
135
+ expert: { ...envelope.payload.content.expert, displayName: 'Dr. Forged' },
136
+ },
137
+ },
138
+ }
139
+ const cache = new TtlCache<import('../contract/kernel.js').CertVerdict>()
140
+ const ctx = { ...RENDER_CTX, now: new Date('2026-06-22T00:00:00Z') }
141
+
142
+ const validVerdict = await getVerifiedEnvelope({
143
+ source: { kind: 'metafield', value: envelope },
144
+ resolveKid,
145
+ context: ctx,
146
+ cache,
147
+ })
148
+ const tamperedVerdict = await getVerifiedEnvelope({
149
+ source: { kind: 'metafield', value: tampered },
150
+ resolveKid,
151
+ context: ctx,
152
+ cache,
153
+ })
154
+
155
+ expect(validVerdict.decision).toBe('render')
156
+ expect(tamperedVerdict).toEqual({ decision: 'suppress', reason: 'invalid_signature' })
157
+ })
122
158
  })
123
159
 
124
160
  describe('getVerifiedEnvelope — Delivery API source', () => {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`), exercised
3
+ * through `react-dom/server`'s `renderToStaticMarkup` — the same path Hydrogen/Next use to
4
+ * produce crawlable HTML on the edge. These prove the adapter's load-bearing contract:
5
+ *
6
+ * • a render-verdict placement renders the in-DOM cert badge (crawlable);
7
+ * • EVERY non-render path — suppress verdict, unknown placement, missing placementId —
8
+ * renders NOTHING (fail-closed survives the CMS boundary);
9
+ * • when one Content tree mixes a valid + a revoked placement (the /builder-cert proof
10
+ * shape), only the valid one renders.
11
+ *
12
+ * Authority comes solely from the loader-supplied verdict in <CertVerifyProvider>; the
13
+ * `placementId` is an opaque pointer the CMS cannot use to manufacture a badge.
14
+ */
15
+ import { renderToStaticMarkup } from 'react-dom/server'
16
+ import { describe, expect, it } from 'vitest'
17
+ import { makeMockPayload } from '../../contract/fixtures.js'
18
+ import type { CertVerdict } from '../../contract/kernel.js'
19
+ import { CertReviewBlock, CertVerifyProvider, certReviewBuilderComponent, type VerifiedPlacements } from '../index.js'
20
+
21
+ const renderVerdict: CertVerdict = { decision: 'render', payload: makeMockPayload() }
22
+ const suppressVerdict: CertVerdict = { decision: 'suppress', reason: 'revoked' }
23
+
24
+ function renderBlock(placements: VerifiedPlacements, placementId?: string): string {
25
+ return renderToStaticMarkup(
26
+ <CertVerifyProvider value={placements}>
27
+ <CertReviewBlock placementId={placementId} />
28
+ </CertVerifyProvider>,
29
+ )
30
+ }
31
+
32
+ describe('Builder adapter — registration', () => {
33
+ it('registers as "CertREV Review" with a single required string `placementId` input', () => {
34
+ expect(certReviewBuilderComponent.name).toBe('CertREV Review')
35
+ expect(certReviewBuilderComponent.component).toBe(CertReviewBlock)
36
+ const input = certReviewBuilderComponent.inputs?.find((i) => i.name === 'placementId')
37
+ expect(input).toBeDefined()
38
+ expect(input?.required).toBe(true)
39
+ expect(input?.type).toBe('string')
40
+ })
41
+ })
42
+
43
+ describe('Builder adapter — CertReviewBlock render', () => {
44
+ it('renders the verified badge for a render-verdict placement (in-DOM, crawlable)', () => {
45
+ const html = renderBlock({ 'p-valid': { verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' } }, 'p-valid')
46
+ expect(html).toContain('certrev-badge')
47
+ expect(html).toContain('Dr. Jane Doe')
48
+ expect(html).toContain('data-certrev-cert-id="cert_fixture_001"')
49
+ })
50
+
51
+ it('FAIL-CLOSED: a suppress-verdict placement renders nothing', () => {
52
+ expect(renderBlock({ 'p-revoked': { verdict: suppressVerdict } }, 'p-revoked')).toBe('')
53
+ })
54
+
55
+ it('FAIL-CLOSED: an unknown placementId renders nothing', () => {
56
+ expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, 'p-not-here')).toBe('')
57
+ })
58
+
59
+ it('FAIL-CLOSED: a missing placementId renders nothing', () => {
60
+ expect(renderBlock({ 'p-valid': { verdict: renderVerdict } }, undefined)).toBe('')
61
+ })
62
+
63
+ it('mixed content (valid + revoked in one Content tree): only the valid placement renders', () => {
64
+ const placements: VerifiedPlacements = {
65
+ 'p-valid': { verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' },
66
+ 'p-revoked': { verdict: suppressVerdict },
67
+ }
68
+ const html = renderToStaticMarkup(
69
+ <CertVerifyProvider value={placements}>
70
+ <div id="valid-block">
71
+ <CertReviewBlock placementId="p-valid" />
72
+ </div>
73
+ <div id="revoked-block">
74
+ <CertReviewBlock placementId="p-revoked" />
75
+ </div>
76
+ </CertVerifyProvider>,
77
+ )
78
+ expect(html).toContain('certrev-badge') // valid rendered
79
+ expect(html).toContain('<div id="revoked-block"></div>') // revoked empty — fail-closed through the CMS
80
+ })
81
+ })
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @certrev/cert-block/builder — the Builder.io adapter for the cert-block render edge.
3
+ *
4
+ * Lets a Builder.io content editor place a "CertREV Review" block into a page; the
5
+ * headless app's loader (Hydrogen/Next/Remix) crypto-verifies that placement's signed
6
+ * envelope and supplies the verdict via React context; this block renders cert-block's
7
+ * <CertReview> from the VERIFIED verdict — and renders NOTHING (fail-closed) on any
8
+ * suppress or unknown placement.
9
+ *
10
+ * THE TRUST BOUNDARY: the credential is never trusted from CMS content. A Builder block
11
+ * only carries an opaque `placementId` pointer; the authority comes solely from the
12
+ * loader-side WebCrypto verdict supplied through <CertVerifyProvider>. So a revoked /
13
+ * tampered / expired / wrong-subject placement that an editor drops into Builder renders
14
+ * nothing, even though the block is present in the published content.
15
+ *
16
+ * cert-block carries NO dependency on `@builder.io/sdk-react` — the registration's shape is
17
+ * declared structurally below, so the emitted JS pulls no Builder code and the type resolves
18
+ * without Builder installed. The consumer (who already has Builder) passes
19
+ * `certReviewBuilderComponent` to Builder's `<Content customComponents={[...]}>`; it is
20
+ * assignable to Builder's `RegisteredComponent` there.
21
+ */
22
+ import { createContext, type ReactNode, useContext } from 'react'
23
+ import { CertReview } from '../components/CertReview.js'
24
+ import type { CertVerdict } from '../contract/kernel.js'
25
+
26
+ /** A loader-verified placement: the verdict cert-block renders from, plus the canonical
27
+ * page URL for JSON-LD @id alignment. */
28
+ export interface VerifiedPlacement {
29
+ readonly verdict: CertVerdict
30
+ readonly pageUrl?: string
31
+ }
32
+
33
+ /** Map of `placementId` → the loader's verified result. Built server-side in the loader
34
+ * (it crypto-verifies each placement) and handed to <CertVerifyProvider>. */
35
+ export type VerifiedPlacements = Record<string, VerifiedPlacement>
36
+
37
+ const CertVerifyContext = createContext<VerifiedPlacements>({})
38
+
39
+ /** Wraps Builder's <Content>; carries the loader's verified verdicts down to every
40
+ * CertReview block, keyed by `placementId`. */
41
+ export function CertVerifyProvider({
42
+ value,
43
+ children,
44
+ }: {
45
+ readonly value: VerifiedPlacements
46
+ readonly children: ReactNode
47
+ }) {
48
+ return <CertVerifyContext.Provider value={value}>{children}</CertVerifyContext.Provider>
49
+ }
50
+
51
+ export interface CertReviewBlockProps {
52
+ /** The CertREV placement id the editor set on this Builder block. */
53
+ readonly placementId?: string
54
+ }
55
+
56
+ /**
57
+ * The component Builder renders for a "CertREV Review" block. It pulls the loader-verified
58
+ * verdict for its `placementId` from context and renders <CertReview>. Unknown placement
59
+ * OR a non-`render` verdict → renders nothing. cert-block's <CertReview> ALSO suppresses on
60
+ * a non-render verdict, so the boundary fails closed twice over (defense in depth).
61
+ */
62
+ export function CertReviewBlock({ placementId }: CertReviewBlockProps) {
63
+ const placements = useContext(CertVerifyContext)
64
+ const placement = placementId ? placements[placementId] : undefined
65
+ if (!placement) return null
66
+ return <CertReview verdict={placement.verdict} pageUrl={placement.pageUrl} />
67
+ }
68
+
69
+ /**
70
+ * Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — exactly the subset
71
+ * cert-block populates. Declared locally (not imported) so cert-block needs no Builder
72
+ * build- or runtime-dependency; the object below is assignable to Builder's
73
+ * `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
74
+ */
75
+ export interface CertReviewBuilderRegistration {
76
+ component: typeof CertReviewBlock
77
+ name: string
78
+ inputs: Array<{ name: string; type: 'string'; required?: boolean; helperText?: string }>
79
+ }
80
+
81
+ /**
82
+ * The registration object to pass to Builder's `<Content customComponents={[...]}>`.
83
+ * Editors see a "CertREV Review" component with one input — the placement id. Because the
84
+ * block is matched by NAME at render time, this also works for content created via the
85
+ * Builder Write API (no visual-editor registration required for rendering).
86
+ */
87
+ export const certReviewBuilderComponent: CertReviewBuilderRegistration = {
88
+ component: CertReviewBlock,
89
+ name: 'CertREV Review',
90
+ inputs: [
91
+ {
92
+ name: 'placementId',
93
+ type: 'string',
94
+ required: true,
95
+ helperText:
96
+ 'CertREV placement id (which certified article this badge attests). The headless ' +
97
+ 'loader verifies that placement’s signed envelope; only a render-verdict shows a badge.',
98
+ },
99
+ ],
100
+ }
@@ -16,14 +16,17 @@
16
16
  * end-to-end under the production `verifyEnvelope`, not a stand-in canonicalizer.
17
17
  */
18
18
 
19
- import { generateKeyPairSync, type KeyObject, sign as nodeSign } from 'node:crypto'
20
- import {
21
- canonicalPayloadBytes,
22
- type CertDeliveryEnvelope,
23
- type CertPayload,
24
- type Ed25519PublicKeyInput,
25
- type ResolvePublicKeyByKid,
19
+ import { generateKeyPairSync, type KeyObject } from 'node:crypto'
20
+ import type {
21
+ CertDeliveryEnvelope,
22
+ CertPayload,
23
+ Ed25519PublicKeyInput,
24
+ ResolvePublicKeyByKid,
26
25
  } from '@certrev/cert-contract'
26
+ // SIGN is Node-only and lives on the contract's signer subpath. Fixtures are themselves
27
+ // Node-only (they stand in for the issuer) and ship from the './fixtures' subpath, so the
28
+ // SDK's edge-safe main entry never pulls a Node builtin.
29
+ import { signPayloadEd25519 } from '@certrev/cert-contract/signer'
27
30
 
28
31
  const FIXTURE_KID = 'certrev-fixture-key-1'
29
32
 
@@ -89,8 +92,9 @@ export interface SignedEnvelopeFixture {
89
92
  export function makeSignedEnvelope(overrides: Partial<CertPayload> = {}, kid = FIXTURE_KID): SignedEnvelopeFixture {
90
93
  const { publicKey, privateKey } = generateKeyPairSync('ed25519')
91
94
  const payload = makeMockPayload(overrides)
92
- const bytes = canonicalPayloadBytes(payload)
93
- const sig = nodeSign(null, bytes, privateKey).toString('base64url')
95
+ // Sign the RFC-8785 canonical bytes via the contract's signer — the SAME bytes the
96
+ // edge-safe kernel recomputes on verify, so the fixture verifies end-to-end.
97
+ const sig = signPayloadEd25519(payload, privateKey)
94
98
 
95
99
  const envelope: CertDeliveryEnvelope = {
96
100
  payload,
@@ -7,8 +7,8 @@
7
7
  * never directly from `@certrev/cert-contract`, so the whole SDK's dependency on the
8
8
  * contract is funnelled through one module. The published package owns the envelope types
9
9
  * (`CertDeliveryEnvelope`, `CertPayload`, `CertCredential`, …), the RFC-8785
10
- * canonicalization (`canonicalPayloadBytes`), and the fail-closed kernel (`verifyEnvelope`,
11
- * `renderVerdict`, `verifySignatureOnly`, `toEd25519PublicKey`).
10
+ * canonicalization (`canonicalPayloadBytes`), and the fail-closed WebCrypto kernel
11
+ * (`verifyEnvelope`, `renderVerdict`, `verifySignatureOnly`, `importEd25519PublicKey`).
12
12
  *
13
13
  * `VerdictKernel` is the ONE name NOT re-exported from the package: the contract exposes
14
14
  * the kernel as free functions, and different edges bundle them with different key-resolver
package/src/index.ts CHANGED
@@ -8,11 +8,16 @@
8
8
  * graph, mergeable by @id (won't collide with Yoast).
9
9
  * • Verify layer — `getVerifiedEnvelope` (fetch from metafield OR Delivery API + run
10
10
  * the fail-closed VerdictKernel + cache), kid resolvers, the TTL/single-flight cache.
11
- * • Contract types — re-exported from `@certrev/cert-contract` (stubbed locally until
12
- * that package publishes; see the contract binding point in ./contract/kernel).
11
+ * • Contract types — re-exported from `@certrev/cert-contract` (the binding point in
12
+ * ./contract/kernel).
13
13
  *
14
- * The Web Component (`<certrev-badge>`) ships from the './webcomponent' subpath export so
15
- * importing the main entry doesn't touch `customElements` (which is undefined server-side).
14
+ * EDGE-RUNTIME SAFE. This main entry imports NO `node:crypto` and uses NO `Buffer`, so it
15
+ * loads + runs on edge/Workers runtimes (Shopify Oxygen, Cloudflare Workers, Vercel Edge).
16
+ * Two things are deliberately kept OFF this entry:
17
+ * • the Web Component (`<certrev-badge>`) → './webcomponent' subpath (touches
18
+ * `customElements`, undefined server-side);
19
+ * • the Node-only test/dev fixtures (`makeSignedEnvelope`, which signs with `node:crypto`)
20
+ * → './fixtures' subpath. Importing the main entry must never pull a Node builtin.
16
21
  */
17
22
 
18
23
  // ── React components ──────────────────────────────────────────────────────────────
@@ -31,9 +36,9 @@ export {
31
36
  type ResolvedDisplay,
32
37
  resolveDisplay,
33
38
  } from './components/format.js'
34
- // ── Test/dev fixtures (mock facts + signed-envelope generator) ─────────────────────
35
- export { FIXTURE_KID, makeMockPayload, makeSignedEnvelope, type SignedEnvelopeFixture } from './contract/fixtures.js'
36
39
  // ── Contract surface (types + kernel) — re-export the binding point ────────────────
40
+ // NOTE: the test/dev fixtures (makeSignedEnvelope, makeMockPayload, FIXTURE_KID) are
41
+ // Node-only (they sign with node:crypto) and ship from the './fixtures' subpath, NOT here.
37
42
  export {
38
43
  CANONICALIZATION,
39
44
  type CertContent,
@@ -80,14 +80,39 @@ function deliveryApiUrl(s: Extract<EnvelopeSource, { kind: 'delivery_api' }>): s
80
80
  return `${base}/api/cert/v1/delivery/${encodeURIComponent(s.platform)}/${encodeURIComponent(s.externalId)}`
81
81
  }
82
82
 
83
- /** Stable cache key per (source identity × render externalId). */
83
+ /**
84
+ * A short, fast, NON-cryptographic string hash (FNV-1a, 32-bit) — used ONLY to disambiguate
85
+ * cache keys, never for security. Edge-safe (no node:crypto). Distinct envelope values for
86
+ * the same placement must produce distinct keys so a push update (or, in tests/proofs,
87
+ * verifying several different envelopes against one render context) can't be served a stale
88
+ * verdict for a different envelope.
89
+ */
90
+ function fnv1a(s: string): string {
91
+ let h = 0x811c9dc5
92
+ for (let i = 0; i < s.length; i++) {
93
+ h ^= s.charCodeAt(i)
94
+ // 32-bit FNV prime multiply via shifts (stays in 32-bit unsigned)
95
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0
96
+ }
97
+ return h.toString(16).padStart(8, '0')
98
+ }
99
+
100
+ /** A stable string form of the metafield value for hashing (parsed → canonical-ish JSON). */
101
+ function metafieldValueKey(value: CertDeliveryEnvelope | string | null | undefined): string {
102
+ if (value == null) return 'null'
103
+ const s = typeof value === 'string' ? value : JSON.stringify(value)
104
+ return fnv1a(s)
105
+ }
106
+
107
+ /** Stable cache key per (source identity × render externalId × envelope value). */
84
108
  function cacheKey(source: EnvelopeSource, ctx: RenderContext): string {
85
109
  if (source.kind === 'delivery_api') {
86
110
  return `api:${source.platform}:${source.externalId}`
87
111
  }
88
- // Metafield: key on the rendering identity (the value is already in hand; the verdict
89
- // still depends on context). Include a short hash of the value to bust on push update.
90
- return `mf:${ctx.platform}:${ctx.externalId}`
112
+ // Metafield: key on the rendering identity AND a short hash of the envelope value, so a
113
+ // push update (new envelope at the same placement) busts the entry, and verifying
114
+ // different envelopes against one render context never collides on a stale verdict.
115
+ return `mf:${ctx.platform}:${ctx.externalId}:${metafieldValueKey(source.value)}`
91
116
  }
92
117
 
93
118
  async function fetchFromDeliveryApi(
@@ -17,6 +17,7 @@
17
17
  * Ed25519 `x` (base64url raw key).
18
18
  */
19
19
 
20
+ import { base64urlDecode } from '@certrev/cert-contract'
20
21
  import type { Ed25519PublicKeyInput, ResolvePublicKeyByKid } from '../contract/kernel.js'
21
22
  import { TtlCache } from './cache.js'
22
23
 
@@ -52,8 +53,9 @@ interface PublishedKeySetDoc {
52
53
  readonly keys: ReadonlyArray<PublishedJwk>
53
54
  }
54
55
 
56
+ // Runtime-agnostic base64url decode from the contract (no `Buffer` — edge/Workers safe).
55
57
  function base64urlToBytes(b64url: string): Uint8Array {
56
- return new Uint8Array(Buffer.from(b64url, 'base64url'))
58
+ return base64urlDecode(b64url)
57
59
  }
58
60
 
59
61
  function jwkToInput(jwk: PublishedJwk): Ed25519PublicKeyInput | null {
@@ -1,154 +0,0 @@
1
- /**
2
- * ─────────────────────────────────────────────────────────────────────────────
3
- * @certrev/cert-contract — INTERFACE STUB (delete on publish; see INTEGRATION below)
4
- * ─────────────────────────────────────────────────────────────────────────────
5
- *
6
- * The shared `CertDeliveryEnvelope` types + the fail-closed `VerdictKernel` live in
7
- * the published `@certrev/cert-contract` package. That package is not yet on a registry
8
- * this workspace can `pnpm install` from (it ships `file:`/workspace-only today), so
9
- * `@certrev/cert-block` imports the contract surface from THIS local interface module
10
- * instead. The types here are a byte-for-byte mirror of `cert-contract`'s `types.ts`
11
- * (the settled, panel-revised shape: structured FACTS, no rendered JSON-LD in the
12
- * payload; `subject` carries `logicalArticleId`, `canonicalUrls[]`, `installationId`,
13
- * `contentDigest`).
14
- *
15
- * INTEGRATION (when @certrev/cert-contract publishes — see README "Integration TODOs"):
16
- * 1. Delete this file and `./kernel-stub.ts`.
17
- * 2. Replace every `from '../contract/kernel-contract.js'` with
18
- * `from '@certrev/cert-contract'`.
19
- * 3. The exported names here are deliberately identical to cert-contract's exports
20
- * (`CertDeliveryEnvelope`, `CertPayload`, `CertVerdict`, `verifyEnvelope`,
21
- * `renderVerdict`, `verifySignatureOnly`, `ResolvePublicKeyByKid`, `RenderContext`,
22
- * `Ed25519PublicKeyInput`), so the swap is a find-and-replace of the import
23
- * specifier — no call-site churn.
24
- *
25
- * Because this is type-only for everything the SDK consumes at COMPILE time, swapping
26
- * the import path is the only change; the runtime kernel comes from `./kernel-stub.ts`
27
- * today and from `@certrev/cert-contract` after publish.
28
- */
29
- /** Bump on ANY breaking change to the payload shape or signing rules. */
30
- export declare const CONTRACT_VERSION: 1;
31
- export type ContractVersion = typeof CONTRACT_VERSION;
32
- /** RFC 8785 JSON Canonicalization Scheme — the bytes that get signed/hashed. */
33
- export declare const CANONICALIZATION: "RFC8785-JCS";
34
- /** Ed25519 (PureEdDSA). */
35
- export declare const SIGNATURE_ALG: "ed25519";
36
- export type SignatureAlg = typeof SIGNATURE_ALG;
37
- export interface CertSubject {
38
- readonly platform: string;
39
- readonly externalId: string;
40
- readonly logicalArticleId: string;
41
- readonly canonicalUrls: ReadonlyArray<string>;
42
- readonly installationId: string | null;
43
- readonly contentDigest: string | null;
44
- }
45
- export interface CertDisplayConfig {
46
- readonly accentColor?: string | null;
47
- readonly showExpertPhoto?: boolean;
48
- readonly showAuthor?: boolean;
49
- readonly showMemo?: boolean;
50
- readonly badgeStyle?: 'full' | 'compact';
51
- }
52
- export interface CertCredential {
53
- readonly abbreviation: string;
54
- readonly fullName: string;
55
- }
56
- export interface CertContent {
57
- readonly expert: {
58
- readonly displayName: string;
59
- readonly credentials: ReadonlyArray<CertCredential>;
60
- readonly profileUrl: string | null;
61
- readonly photoUrl: string | null;
62
- };
63
- readonly author: {
64
- readonly name: string;
65
- readonly title: string | null;
66
- };
67
- readonly memo: string | null;
68
- readonly certifiedAt: string;
69
- readonly contentModifiedAt: string | null;
70
- readonly verifyUrl: string;
71
- readonly display: CertDisplayConfig;
72
- }
73
- export interface CertLifecycle {
74
- readonly issuedAt: string;
75
- readonly expiresAt: string;
76
- readonly revokedAt: string | null;
77
- readonly revision: number;
78
- }
79
- export interface CertPayload {
80
- readonly contractVersion: ContractVersion;
81
- readonly certId: string;
82
- readonly subject: CertSubject;
83
- readonly content: CertContent;
84
- readonly lifecycle: CertLifecycle;
85
- }
86
- export interface CertSignature {
87
- readonly alg: SignatureAlg;
88
- readonly kid: string;
89
- readonly sig: string;
90
- readonly signedAt: string;
91
- }
92
- export interface CertDeliveryEnvelope {
93
- readonly payload: CertPayload;
94
- readonly signature: CertSignature;
95
- }
96
- export type CertVerdict = {
97
- readonly decision: 'render';
98
- readonly payload: CertPayload;
99
- } | {
100
- readonly decision: 'suppress';
101
- readonly reason: CertSuppressReason;
102
- };
103
- export type CertSuppressReason = 'unsupported_contract_version' | 'unsupported_alg' | 'unknown_key' | 'invalid_signature' | 'platform_mismatch' | 'subject_mismatch' | 'revoked' | 'expired' | 'content_drift';
104
- /** The Ed25519 public-key encodings a kid resolver may hand the kernel. */
105
- export type Ed25519PublicKeyInput = {
106
- readonly format: 'spki-der';
107
- readonly bytes: Uint8Array;
108
- } | {
109
- readonly format: 'spki-base64';
110
- readonly base64: string;
111
- } | {
112
- readonly format: 'pem';
113
- readonly pem: string;
114
- } | {
115
- readonly format: 'raw';
116
- readonly bytes: Uint8Array;
117
- };
118
- /**
119
- * A kid → public-key resolver. Returns a usable Ed25519 key for the given kid, or
120
- * null/undefined when the kid is unknown (→ suppress 'unknown_key'). May be async so
121
- * an edge can fetch + cache a published key set. The real kernel also accepts a Node
122
- * KeyObject; we model only the SDK-relevant inputs here.
123
- */
124
- export type ResolvePublicKeyByKid = (kid: string) => Promise<Ed25519PublicKeyInput | null | undefined> | Ed25519PublicKeyInput | null | undefined;
125
- /** Edge-supplied context for the policy phase. */
126
- export interface RenderContext {
127
- readonly platform: string;
128
- readonly externalId: string;
129
- readonly liveContentHash?: string | null;
130
- readonly now?: Date;
131
- }
132
- /** Full kernel: verify signature, then apply subject/lifecycle/drift policy. */
133
- export type VerifyEnvelope = (envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid, ctx: RenderContext) => Promise<CertVerdict>;
134
- /** Phase-2-only policy over an already-verified payload. */
135
- export type RenderVerdict = (payload: CertPayload, ctx: RenderContext) => CertVerdict;
136
- /** Phase-1-only cryptographic verification. */
137
- export type VerifySignatureOnly = (envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid) => Promise<{
138
- ok: true;
139
- } | {
140
- ok: false;
141
- reason: CertSuppressReason;
142
- }>;
143
- /**
144
- * The kernel function set as a single named interface — what the SDK depends on at
145
- * the value level. `@certrev/cert-contract` exposes these as free functions; the SDK
146
- * binds them through `./kernel-stub` today and re-points to the real exports on
147
- * publish.
148
- */
149
- export interface VerdictKernel {
150
- readonly verifyEnvelope: VerifyEnvelope;
151
- readonly renderVerdict: RenderVerdict;
152
- readonly verifySignatureOnly: VerifySignatureOnly;
153
- }
154
- //# sourceMappingURL=kernel-contract.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kernel-contract.d.ts","sourceRoot":"","sources":["../../src/contract/kernel-contract.ts"],"names":[],"mappings":"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"}
@@ -1,35 +0,0 @@
1
- /**
2
- * ─────────────────────────────────────────────────────────────────────────────
3
- * @certrev/cert-contract — INTERFACE STUB (delete on publish; see INTEGRATION below)
4
- * ─────────────────────────────────────────────────────────────────────────────
5
- *
6
- * The shared `CertDeliveryEnvelope` types + the fail-closed `VerdictKernel` live in
7
- * the published `@certrev/cert-contract` package. That package is not yet on a registry
8
- * this workspace can `pnpm install` from (it ships `file:`/workspace-only today), so
9
- * `@certrev/cert-block` imports the contract surface from THIS local interface module
10
- * instead. The types here are a byte-for-byte mirror of `cert-contract`'s `types.ts`
11
- * (the settled, panel-revised shape: structured FACTS, no rendered JSON-LD in the
12
- * payload; `subject` carries `logicalArticleId`, `canonicalUrls[]`, `installationId`,
13
- * `contentDigest`).
14
- *
15
- * INTEGRATION (when @certrev/cert-contract publishes — see README "Integration TODOs"):
16
- * 1. Delete this file and `./kernel-stub.ts`.
17
- * 2. Replace every `from '../contract/kernel-contract.js'` with
18
- * `from '@certrev/cert-contract'`.
19
- * 3. The exported names here are deliberately identical to cert-contract's exports
20
- * (`CertDeliveryEnvelope`, `CertPayload`, `CertVerdict`, `verifyEnvelope`,
21
- * `renderVerdict`, `verifySignatureOnly`, `ResolvePublicKeyByKid`, `RenderContext`,
22
- * `Ed25519PublicKeyInput`), so the swap is a find-and-replace of the import
23
- * specifier — no call-site churn.
24
- *
25
- * Because this is type-only for everything the SDK consumes at COMPILE time, swapping
26
- * the import path is the only change; the runtime kernel comes from `./kernel-stub.ts`
27
- * today and from `@certrev/cert-contract` after publish.
28
- */
29
- /** Bump on ANY breaking change to the payload shape or signing rules. */
30
- export const CONTRACT_VERSION = 1;
31
- /** RFC 8785 JSON Canonicalization Scheme — the bytes that get signed/hashed. */
32
- export const CANONICALIZATION = 'RFC8785-JCS';
33
- /** Ed25519 (PureEdDSA). */
34
- export const SIGNATURE_ALG = 'ed25519';
35
- //# sourceMappingURL=kernel-contract.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kernel-contract.js","sourceRoot":"","sources":["../../src/contract/kernel-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAA;AAG1C,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAsB,CAAA;AAEtD,2BAA2B;AAC3B,MAAM,CAAC,MAAM,aAAa,GAAG,SAAkB,CAAA"}
@@ -1,44 +0,0 @@
1
- /**
2
- * ─────────────────────────────────────────────────────────────────────────────
3
- * VerdictKernel — LOCAL STUB (delete on publish; mirrors @certrev/cert-contract)
4
- * ─────────────────────────────────────────────────────────────────────────────
5
- *
6
- * A faithful, self-contained implementation of the cert-contract VerdictKernel so the
7
- * SDK's verify layer + tests run NOW, before `@certrev/cert-contract` publishes to a
8
- * registry this workspace installs from. The behavior is identical to the real kernel:
9
- * • Ed25519 detached signature verified over the RFC-8785 (JCS) canonical bytes of
10
- * `payload` (lexicographic key order, minimal whitespace, UTF-8).
11
- * • Fail-closed pipeline: shape → alg → kid → signature → platform → subject →
12
- * revoked → expired → drift → render. Any failure short-circuits to `suppress`.
13
- * • Never throws on a bad envelope — a malformed input / throwing resolver fails
14
- * closed to `suppress`, not to an exception a caller might swallow into a render.
15
- *
16
- * The byte-identical canonicalization across PHP / Node is what makes the credential
17
- * portable; here we hand-roll a deterministic JCS serializer (sufficient for the
18
- * envelope's all-string/number/bool/array/object shape — no exotic numbers) so the SDK
19
- * carries no production crypto-canonicalization of its own that could DRIFT from the
20
- * contract. On publish, the real `canonicalize` (RFC 8785) from cert-contract is used.
21
- *
22
- * INTEGRATION: delete this file + `./kernel-contract.ts`; import `verifyEnvelope`,
23
- * `renderVerdict`, `verifySignatureOnly`, `toEd25519PublicKey` from
24
- * `@certrev/cert-contract`. See README "Integration TODOs".
25
- */
26
- import { type KeyObject } from 'node:crypto';
27
- import type { CertDeliveryEnvelope, CertPayload, CertSuppressReason, CertVerdict, Ed25519PublicKeyInput, RenderContext, ResolvePublicKeyByKid } from './kernel-contract.js';
28
- /** Mirror of cert-contract's `toEd25519PublicKey` for the input encodings the SDK uses. */
29
- declare function toEd25519PublicKey(input: Ed25519PublicKeyInput): KeyObject;
30
- type CryptoResult = {
31
- ok: true;
32
- } | {
33
- ok: false;
34
- reason: CertSuppressReason;
35
- };
36
- /** Phase-2-only policy verdict over an already-authentic payload. Pure + synchronous. */
37
- export declare function renderVerdict(payload: CertPayload, ctx: RenderContext): CertVerdict;
38
- /** Phase-1-only cryptographic verification. */
39
- export declare function verifySignatureOnly(envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid): Promise<CryptoResult>;
40
- /** Full kernel: verify signature, then apply policy. Fail-closed, never throws. */
41
- export declare function verifyEnvelope(envelope: CertDeliveryEnvelope, resolveKid: ResolvePublicKeyByKid, ctx: RenderContext): Promise<CertVerdict>;
42
- /** Re-export for tests that need to build a public key from raw/spki bytes. */
43
- export { toEd25519PublicKey };
44
- //# sourceMappingURL=kernel-stub.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kernel-stub.d.ts","sourceRoot":"","sources":["../../src/contract/kernel-stub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAmB,KAAK,SAAS,EAAwB,MAAM,aAAa,CAAA;AACnF,OAAO,KAAK,EACX,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,MAAM,sBAAsB,CAAA;AA4C7B,2FAA2F;AAC3F,iBAAS,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,CAkBnE;AAUD,KAAK,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAA;AA6B5E,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,GAAG,WAAW,CAoBnF;AAED,+CAA+C;AAC/C,wBAAsB,mBAAmB,CACxC,QAAQ,EAAE,oBAAoB,EAC9B,UAAU,EAAE,qBAAqB,GAC/B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,mFAAmF;AACnF,wBAAsB,cAAc,CACnC,QAAQ,EAAE,oBAAoB,EAC9B,UAAU,EAAE,qBAAqB,EACjC,GAAG,EAAE,aAAa,GAChB,OAAO,CAAC,WAAW,CAAC,CAWtB;AAED,+EAA+E;AAC/E,OAAO,EAAE,kBAAkB,EAAE,CAAA"}