diff --git a/check.html b/check.html index e5476be..4cda699 100644 --- a/check.html +++ b/check.html @@ -200,12 +200,14 @@

The example, and why it is this one

a GET /v1/refusals response, which is the shape a console's copy button puts on your clipboard, and it is signed under op.enforcement.refusal.v3. So the example here is the same thing you would be holding, in the same shape, under the construction the enforcement point issues today.

-

The published engine reads v2 records. The v3 construction is in no published release. A v3 record - checks on this page, which carries the construction from the engine commit that issues it; it does not check with - npm install, whose signableFromRefusal rebuilds a v3 store record without v3's fields and does - not read the served row at all. Measured 24 August 2026 against the version npm install serves.

+

The published engine reads v2 and v3 records. A v3 record + checks on this page and with npm install: @observer-protocol/policy-engine rebuilds a v3 + store record with v3's fields, and reads the served row through signableFromRefusalRow. + Corrected 24 August 2026: this said the v3 construction was in no published release and did not check + with npm install. That held through 24 August 2026, until rc.22 was published; the publish makes it false. + Measured 24 August 2026 against the version npm install serves.

It is signed by a demonstration key that no party stands behind. The key the record names belongs to the - demonstration service that issued it, the same key that signs the five records CI holds this page's v3 construction to. A + demonstration service that issued it, the same key that signs the five v3 records this page carries as regression vectors. A green result here establishes the construction and the key: that these bytes, rebuilt under the construction the record states, were signed by the key the record names. It does not establish who held that key, and for this example nobody does. The same limit is stated on the Lightning entry in the registry, where the issuer was a demonstration @@ -223,13 +225,14 @@

What this runs

A refusal record runs the same sequence as signableFromRefusal and refusalPayload: the signed payload is rebuilt from the record's own fields, under the construction the record itself states rather than whichever version this page ships, and the signature is checked against the key the record names.

-

The attestation sequence and the v1 and v2 refusal constructions are what @observer-protocol/policy-engine exports at - 1.0.0-rc.21, the version npm's latest tag serves a reader today. - The v3 refusal construction is in no published release; this page carries it from the unpublished engine commit that - issues it. Corrected 24 August 2026: this paragraph said both constructions were the package's, and for v3 - that was false. This page loads nothing, so it cannot import the package; it carries its own copy of those constructions and CI asserts, - on every build, that the v1 and v2 bytes it produces are identical to the package's own over every such refusal this repository publishes, - and that its v3 bytes match five records the enforcement point signed, because there is no package to hold them to. +

The attestation sequence and the v1, v2 and v3 refusal constructions are what @observer-protocol/policy-engine exports at + 1.0.0-rc.22, the version npm's latest tag serves a reader today. + Corrected 24 August 2026: until rc.22 was published this paragraph said the v3 construction was in no + published release and this page carried it from an unpublished engine commit; the publish makes v3 the package's like v1 and + v2, and moved the version above from rc.21 to rc.22. This page loads nothing, so it cannot import the package; it carries its + own copy of those constructions and CI asserts, on every build, that the v1, v2 and v3 bytes it produces are identical to the + package's own over every such refusal this repository publishes, with the five v3 records the enforcement point signed kept as + regression vectors their own signatures verify. A divergence turns the build red rather than turning a verdict here wrong. The same checks, run from npm on your own machine โ†—

diff --git a/docs.html b/docs.html index 7cbeb6e..6fb9348 100644 --- a/docs.html +++ b/docs.html @@ -219,7 +219,7 @@

Choose your path.

๐Ÿ“ฆ
-
NPM INSTALL ยท v1.0.0-rc.21
+
NPM INSTALL ยท v1.0.0-rc.22
Verify a credential
npm install @observer-protocol/policy-engine
Verify a delegation credential yourself โ€” issuer, structure, validity window, eddsa-jcs-2022 proof against the issuer DID document, and revocation. No API key, no token, no Observer Protocol API in the path. It fetches the issuer's DID document and the revocation status list over ordinary HTTPS from the origins the credential names; point offline.didDocumentPath at a local copy and it makes no network call at all. The hosted verifier is a separate deployment running a different engine version; see the SDK section.
@@ -448,7 +448,7 @@

@observer-protocol/policy-engine

unauthenticated POST /v1/verify returns 200 with a signed result.

It is a different build, though. The hosted service reports - engine.running: "0.3.3"; the package above is 1.0.0-rc.21. Re-measured against + engine.running: "0.3.3"; the package above is 1.0.0-rc.22. Re-measured against rc.10 on 9 August 2026: they agree on 7 of the 8 artifacts this site publishes. The eighth is never evaluated by the hosted engine at all — it is refused at that deployment's issuer allowlist, which does not carry the testbed issuer. So the agreement is on samples rather diff --git a/index.html b/index.html index 1f30995..2e1fd9e 100644 --- a/index.html +++ b/index.html @@ -1008,7 +1008,7 @@

It fails closed, and it will refuse us too. onUnreachable: 'cache-then-deny' is the only accepted value: if the revocation list cannot be fetched, a cached answer is used and then the credential is denied. A status list hosted on an origin other than the pinned issuer's is refused until you allowlist it, and Observer Protocol's own clause-zero revocation demonstration is exactly such a pair, so it does not verify out of the box. That limit is published in the package.

-
package ยท @observer-protocol/policy-engine 1.0.0-rc.21
+
package ยท @observer-protocol/policy-engine 1.0.0-rc.22
licence ยท MIT ยท self-hostable ยท no account
limits ยท KNOWN-LIMITS.md โ†—
@@ -1142,7 +1142,7 @@

The register, stated plainly.

Offline verification with nothing from us
-
@observer-protocol/policy-engine 1.0.0-rc.21 on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.
+
@observer-protocol/policy-engine 1.0.0-rc.22 on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.
Live
diff --git a/results/engine-payload-exports.json b/results/engine-payload-exports.json index 18a8cf3..efbe6a5 100644 --- a/results/engine-payload-exports.json +++ b/results/engine-payload-exports.json @@ -8,7 +8,7 @@ "The caveats used to live only in this script, which the site returns 404 for, and in CI", "logs, which no reader sees." ], - "measuredOn": "2026-08-23", + "measuredOn": "2026-08-25", "provenance": { "whatWasMeasured": "Which symbols each published tarball of @observer-protocol/policy-engine exports, read out of the tarballs themselves rather than out of a CHANGELOG.", "registryDependent": { @@ -19,9 +19,9 @@ "versionCount" ], "computedAgainstRegistryState": { - "npmLatest": "1.0.0-rc.21", - "npmRc": "1.0.0-rc.21", - "versionCount": 26 + "npmLatest": "1.0.0-rc.22", + "npmRc": "1.0.0-rc.22", + "versionCount": 27 }, "goesStaleWhen": "a dist-tag moves or a new version is published, without this file being touched." }, @@ -33,9 +33,9 @@ } }, "package": "@observer-protocol/policy-engine", - "npmLatest": "1.0.0-rc.21", - "npmRc": "1.0.0-rc.21", - "versionCount": 26, + "npmLatest": "1.0.0-rc.22", + "npmRc": "1.0.0-rc.22", + "versionCount": 27, "withdrawals": { "resolutionPayload": [ { @@ -387,6 +387,22 @@ ], "verdictPayloadType": "op.evaluation.verdict.v4", "refusalPayloadType": "op.enforcement.refusal.v2" + }, + { + "version": "1.0.0-rc.22", + "exports": [ + "evaluationVerdictPayload", + "lapsePayload", + "refusalPayload", + "resolutionPayload" + ], + "verifiers": [ + "verifyDecisionAttestation", + "verifyCredentialObject", + "signableFromRefusal" + ], + "verdictPayloadType": "op.evaluation.verdict.v4", + "refusalPayloadType": "op.enforcement.refusal.v3" } ] } diff --git a/scripts/check-page-payload-parity.mjs b/scripts/check-page-payload-parity.mjs index f598262..b70eb8f 100644 --- a/scripts/check-page-payload-parity.mjs +++ b/scripts/check-page-payload-parity.mjs @@ -52,7 +52,7 @@ import { readFileSync, readdirSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { signableFromRefusal, refusalPayload } from '@observer-protocol/policy-engine'; +import { signableFromRefusal, refusalPayload, signableFromRefusalRow } from '@observer-protocol/policy-engine'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const html = readFileSync(join(root, 'check.html'), 'utf8'); @@ -175,14 +175,12 @@ if (forCoverage) { // โ”€โ”€โ”€ 5. The served shape, against the signature โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // -// Real served rows. The package cannot be asked what these should rebuild to, because the -// normalisation lives in op-mcp-payment-server/src/http/reads.ts and is exported from no -// package this repository depends on. So the check is not a comparison, it is a challenge: -// rebuild the bytes through the page's own normalisation and require the record's OWN -// signature to verify over them. -// -// That is a stronger oracle than a byte comparison against a second copy would be. A copy can -// be wrong in the same way twice. A signature cannot. +// Real served rows. Since rc.22 the package exports signableFromRefusalRow, so these ARE held to +// the package byte-for-byte in the "held to the published package" block below. Here the record's +// OWN signature is also required to verify over the page's rebuild. Two oracles, not one: a byte +// comparison catches a construction that drifts from the package; a signature catches one that +// drifts from what the enforcement point actually signed. A copy can be wrong the same way twice; +// a signature cannot. import { ed25519Verify, base58Decode } from '@observer-protocol/policy-engine'; const servedPath = join(root, 'scripts/__fixtures__/refusals-served.json'); @@ -226,21 +224,17 @@ for (const row of signable) { } console.log(`ok served rows reach a verdict through OP_CHECK.check ${servedThroughPage}/${signable.length}`); -// โ”€โ”€โ”€ v3, AGAINST THE ENGINE'S OWN BYTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -// -// The pinned package has no v3: rc.12 is what npm serves and the v3 construction is unpublished, -// living on op-policy-engine at 6f58fcb. So there is nothing here to compare the page against -// field by field, exactly as with the served shape, and the answer is the same one: the -// signature. +// โ”€โ”€โ”€ v3 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // -// These fixtures were signed over bytes THE ENGINE produced. A page whose rebuild differs from -// the engine's in any field cannot verify them. That makes them an oracle for the v3 -// construction without waiting on a publish, and it is why they are worth committing rather -// than deriving here: a fixture this file generated from this file's own understanding would -// agree with it by construction and establish nothing. +// rc.22 publishes the v3 construction (REFUSAL_PAYLOAD_TYPE_V3, and signableFromRefusalRow for the +// served shape), so v3 is now held to the package byte-for-byte, exactly as v1 and v2 are, in the +// "held to the published package" block below. These five fixtures are DEMOTED to regression +// vectors: they were signed over bytes the enforcement point produced, and requiring their own +// signatures to verify over the page's rebuild catches a drift from what was actually signed, +// which a byte comparison against the package cannot. // // WHAT THEY DO NOT ESTABLISH: anything about a production record. The keys are throwaway and -// the records are engine-derived. A production v3 record spanning both arms is asked for. +// the records are engine-derived. A production v3 record spanning both arms is still asked for. const v3Path = join(root, 'scripts/__fixtures__/refusals-v3.json'); let v3Rows = []; try { @@ -329,6 +323,36 @@ for (const row of v3Rows) { } } +// โ”€โ”€โ”€ HELD TO THE PUBLISHED PACKAGE (rc.22): the served and v3 shapes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// v1 and v2 store records are compared to the package above. Since rc.22 the package also ships +// the v3 construction and signableFromRefusalRow, so the served rows and the v3 vectors are held +// to it the same way: the page's rebuild bytes must EQUAL the package's, or a page whose +// construction has drifted from the one npm serves goes red here rather than at a reader. The +// signature checks above remain as regression vectors against what the enforcement point signed. +const pkgBytesOf = (row) => { + const store = shapeOf(row) === 'served' ? signableFromRefusalRow(row) : row; + return refusalPayload(signableFromRefusal(store)); +}; +const pageBytesOf = (row) => OP.refusalPayload(OP.SHAPES[shapeOf(row)].toSignable(row)); +let heldToPackage = 0; +for (const row of [...signable, ...v3Rows]) { + const id = row.$vector ?? `${row.refusalId} (${row.code})`; + let pkg, page, pkgErr, pageErr; + try { pkg = pkgBytesOf(row); } catch (e) { pkgErr = e; } + try { page = pageBytesOf(row); } catch (e) { pageErr = e; } + if (pkgErr && pageErr) { heldToPackage++; continue; } + if (pkgErr) { fail(`${id}: the package refuses to rebuild this ${shapeOf(row)} row (${pkgErr.message.slice(0, 110)}) and the page builds it. The page is more permissive than the construction npm serves.`); continue; } + if (pageErr) { fail(`${id}: the page refuses to rebuild a ${shapeOf(row)} row the package builds (${pageErr.message.slice(0, 110)}).`); continue; } + if (pkg !== page) { + const at = firstDifference(pkg, page); + fail(`${id} (${cell(row)}): the page and the PACKAGE build different payloads.\n first difference at byte ${at}\n package: ${JSON.stringify(pkg.slice(Math.max(0, at - 40), at + 60))}\n page: ${JSON.stringify(page.slice(Math.max(0, at - 40), at + 60))}`); + continue; + } + heldToPackage++; +} +console.log(`ok v3 and served held byte-identical to the package ${heldToPackage}/${signable.length + v3Rows.length}`); + // THE EMPTY CELL: served x recorded x note. The live service emits no such row, because the // ceiling refusal builds {state, limit, unit, observed} and sets no note. Held by projecting the // constructed store record, so the signature stays the enforcement point's and a wrong projection @@ -520,4 +544,4 @@ if (failures.length) { } console.log(`The page builds byte-identical refusal payloads to @observer-protocol/policy-engine`); console.log(`${engineVersion}, over ${compared} store-shape record(s), and ${servedVerified} served-shape row(s)`); -console.log(`rebuild to bytes their own signatures verify, and ${v3Verified} v3 record(s) rebuild to the\nengine's own bytes at 6f58fcb. Two shapes covered; a third needs its own run.`); +console.log(`rebuild to bytes their own signatures verify. All ${heldToPackage} served and v3 record(s) also\nrebuild byte-identically to the published package; the five v3 fixtures are regression vectors\nagainst the enforcement point's own signatures.`); diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 6778d21..ef14942 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -6,13 +6,13 @@ "": { "name": "op-website-credential-check", "dependencies": { - "@observer-protocol/policy-engine": "1.0.0-rc.21" + "@observer-protocol/policy-engine": "^1.0.0-rc.22" } }, "node_modules/@observer-protocol/policy-engine": { - "version": "1.0.0-rc.21", - "resolved": "https://registry.npmjs.org/@observer-protocol/policy-engine/-/policy-engine-1.0.0-rc.21.tgz", - "integrity": "sha512-OFFRtmDMs+DWW1pyAWWwRcoXHPPMp/8y48NZ0xyzDKjy310PqFb8S7NPGoW9H7gUWGAm2tEROE3fZq2Y2gOKbA==", + "version": "1.0.0-rc.22", + "resolved": "https://registry.npmjs.org/@observer-protocol/policy-engine/-/policy-engine-1.0.0-rc.22.tgz", + "integrity": "sha512-eSiOLPsXGw2wgybOCdyx4WSLBW1Mel4mqjKO4YXJbyGtY4oqXZzzkSnRRxmkl/Owf5HdOnDFCVSfpAgiK9ltdw==", "license": "MIT", "engines": { "node": ">=18" diff --git a/scripts/package.json b/scripts/package.json index d8770ec..376b665 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -7,6 +7,6 @@ "verify:credentials": "node ./verify-published-credentials.mjs" }, "dependencies": { - "@observer-protocol/policy-engine": "1.0.0-rc.21" + "@observer-protocol/policy-engine": "^1.0.0-rc.22" } } diff --git a/sitemap.xml b/sitemap.xml index 780ad5f..64c189d 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -122,7 +122,7 @@ https://observerprotocol.org/verify.html - 2026-08-23 + 2026-08-24 https://observerprotocol.org/wdk.html diff --git a/verify.html b/verify.html index db5dee7..2b86294 100644 --- a/verify.html +++ b/verify.html @@ -120,7 +120,7 @@

Install, one call, done.

-
verify.mjs@observer-protocol/policy-engine 1.0.0-rc.21 ยท MIT
+
verify.mjs@observer-protocol/policy-engine 1.0.0-rc.22 ยท MIT
$ npm install @observer-protocol/policy-engine
 $ curl -O https://observerprotocol.org/verify-samples/verifies-delegation-mandate.json
 
@@ -354,7 +354,7 @@ 

There is no verifier for a PolicyEvaluationCredent

A resolution record now checks with the version this page tells you to install. It did not, for four releases.

An approval resolution is the record of a human decision, signed at both ends: the routing and the outcome. Rebuilding its signed bytes needs resolutionPayload. The version npm install serves exports it. For four releases it did not.

-

Read out of every published tarball rather than out of a release note: resolutionPayload was exported at 1.0.0-rc.8, withdrawn across 4 consecutive releases, 1.0.0-rc.9 through 1.0.0-rc.12, and restored at 1.0.0-rc.13. npm's latest tag no longer points inside that band, at 1.0.0-rc.21, which is the version this page documents and the version the install line above gives you.

+

Read out of every published tarball rather than out of a release note: resolutionPayload was exported at 1.0.0-rc.8, withdrawn across 4 consecutive releases, 1.0.0-rc.9 through 1.0.0-rc.12, and restored at 1.0.0-rc.13. npm's latest tag no longer points inside that band, at 1.0.0-rc.22, which is the version this page documents and the version the install line above gives you.

The 122 signed resolution records this estate holds were always rebuildable by someone who installed @rc; what they were not was rebuildable by someone who followed the instruction on this page. That was worse than a missing feature: the instruction was complete, it ran, and it produced a package silently missing one constructor. That number is now 0: every signed resolution record this estate holds is rebuildable at the version npm install serves. It was 122 for as long as latest pointed inside the band.

The withdrawal appears in no changelog. The package's own release notes record neither the removal nor the restoration, so nothing but the tarballs could have told you. That is why the figures in this paragraph are read from a measurement in results/ and compared against npm on every build, instead of being typed here where nothing could contradict them.

What to do about it. Nothing. The version this page documents and the version its install line gives you both rebuild a resolution record, because npm’s latest tag now points past the withdrawal band rather than inside it. This site stays pinned to what latest serves, for the same reason it did while that was the broken version: a page that documents a version nobody receives is the failure this whole surface is written against.

@@ -501,7 +501,7 @@

Convenient, ours, and now the same engine two releases back.

Correction: the two verifiers converged, and this section said otherwise for a week

This section used to argue that the hosted service was a different build. It reported engine.running: "0.3.3" and concluded that any agreement between the two was "agreement observed on samples, not a shared code path". That argument is false and has been since 9 August 2026. Convergence landed that day, in commit d3278efb3cb2, built 2026-08-09, and nothing here recorded it. The page went on making an argument its own subject had stopped supporting.

-

Read from the service's own /version today: engine.running 1.0.0-rc.10, engine.builtAgainst 1.0.0-rc.10, agree: true. It is the published package. The published package is 1.0.0-rc.21, so the honest description is the same engine, two releases behind, and not a separate implementation. What remains true is that a version gap is a real gap: rc.10 and rc.12 are not the same code, and a difference between them is a difference you can meet.

+

Read from the service's own /version today: engine.running 1.0.0-rc.10, engine.builtAgainst 1.0.0-rc.10, agree: true. It is the published package. The published package is 1.0.0-rc.22, so the honest description is the same engine, two releases behind, and not a separate implementation. What remains true is that a version gap is a real gap: rc.10 and rc.12 are not the same code, and a difference between them is a difference you can meet.

The old "agree on 7 of 8" figure is withdrawn, and the denominator was the defect rather than the agreement. Three of those eight artifacts never reach the hosted engine at all, so a ratio over eight was never reproducible by a reader. One is the testbed-issuer demonstration credential, and that deployment refuses it at its issuer allowlist, so the refusal is evidence about the allowlist and not about the engine. The other two are the PolicyEvaluationCredential artifacts, for the reason in the next paragraph. A figure whose denominator counts artifacts that cannot be submitted is not a measurement of agreement; it is a measurement of how many things we tried.

A PolicyEvaluationCredential cannot be submitted here at all. Post one as the request body and the service answers HTTP 400, body must carry agentDid (string) and mandate (object). There is no agentDid to supply: a PEC's credentialSubject carries decision, evaluator, proposal and evaluatedAt, and no id. The endpoint's shape assumes a delegation credential granting authority to a subject, and an evaluation credential has no subject in that sense. This is the same root cause as the gap in section 02, met at the transport rather than in the validator: both the package and the service are built around one credential shape, and the artifact that records a determination does not have it. Wrapping a PEC in an invented agentDid does get a response, and what comes back is a structural refusal about credentialSchema, which is the offline failure repeated rather than a second opinion about it.

The service is also candid about a second limit, in its own /version response: the schema and issuer allowlists it enforces come from environment variables on the host, inRepo: false, so a change to what it accepts "leaves no reviewable record". The offline path has no such property, because the allowlist is the one you wrote.

@@ -544,7 +544,7 @@

Checking a record is not approving a vendor.