Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions check.html
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ <h4>The example, and why it is this one</h4>
a <code>GET /v1/refusals</code> response, which is the shape a console's copy button puts on your clipboard, and it is
signed under <code>op.enforcement.refusal.v3</code>. So the example here is the same thing you would be holding, in the
same shape, under the construction the enforcement point issues today.</p>
<p><strong>The published engine reads v2 records. The v3 construction is in no published release.</strong> A v3 record
checks on this page, which carries the construction from the engine commit that issues it; it does not check with
<code>npm install</code>, whose <code>signableFromRefusal</code> 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 <code>npm install</code> serves.</p>
<p><strong>The published engine reads v2 and v3 records.</strong> A v3 record
checks on this page and with <code>npm install</code>: <code>@observer-protocol/policy-engine</code> rebuilds a v3
store record with v3's fields, and reads the served row through <code>signableFromRefusalRow</code>.
<strong>Corrected 24 August 2026:</strong> this said the v3 construction was in no published release and did not check
with <code>npm install</code>. That held through 24 August 2026, until rc.22 was published; the publish makes it false.
Measured 24 August 2026 against the version <code>npm install</code> serves.</p>
<p><strong>It is signed by a demonstration key that no party stands behind.</strong> 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
Expand All @@ -223,13 +225,14 @@ <h4>What this runs</h4>
<p><strong>A refusal record</strong> runs the same sequence as <code>signableFromRefusal</code> and <code>refusalPayload</code>: the
signed payload is rebuilt from the record's own fields, under the construction <em>the record itself states</em> rather than
whichever version this page ships, and the signature is checked against the key the record names.</p>
<p>The attestation sequence and the v1 and v2 refusal constructions are what <code>@observer-protocol/policy-engine</code> exports at
<span data-engine-version="current">1.0.0-rc.21</span>, the version npm's <code>latest</code> 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. <strong>Corrected 24 August 2026:</strong> 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.
<p>The attestation sequence and the v1, v2 and v3 refusal constructions are what <code>@observer-protocol/policy-engine</code> exports at
<span data-engine-version="current">1.0.0-rc.22</span>, the version npm's <code>latest</code> tag serves a reader today.
<strong>Corrected 24 August 2026:</strong> 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.
<a href="/verify.html" style="color:var(--amber); text-decoration:none;">The same checks, run from npm on your own machine ↗</a></p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ <h2>Choose your path.</h2>
</a>
<a href="https://github.com/observer-protocol/op-policy-engine" target="_blank" class="qs-card">
<div class="qs-icon">📦</div>
<div class="qs-tag">NPM INSTALL · v<span data-engine-version="current">1.0.0-rc.21</span></div>
<div class="qs-tag">NPM INSTALL · v<span data-engine-version="current">1.0.0-rc.22</span></div>
<div class="qs-name">Verify a credential</div>
<div class="qs-meta">npm install @observer-protocol/policy-engine</div>
<div class="qs-desc" data-shared-copy="offline-scope">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 <code>offline.didDocumentPath</code> 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.</div>
Expand Down Expand Up @@ -448,7 +448,7 @@ <h2>@observer-protocol/policy-engine</h2>
unauthenticated <code>POST /v1/verify</code> returns 200 with a signed result.
<br><br>
It is a <em>different build</em>, though. The hosted service reports
<code>engine.running: "0.3.3"</code>; the package above is <code><span data-engine-version="current">1.0.0-rc.21</span></code>. Re-measured against
<code>engine.running: "0.3.3"</code>; the package above is <code><span data-engine-version="current">1.0.0-rc.22</span></code>. 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 &mdash; it is refused at that deployment's issuer
allowlist, which does not carry the testbed issuer. So the agreement is on samples rather
Expand Down
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ <h3 style="font-family:'Fraunces',serif; font-size:1.3rem; font-weight:600; marg
<strong style="color:var(--text);">It fails closed, and it will refuse us too.</strong> <code>onUnreachable: 'cache-then-deny'</code> 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.
</p>
<div style="font-family:'IBM Plex Mono',monospace; font-size:0.68rem; color:var(--text-faint); line-height:2; border-top:1px solid var(--border); padding-top:1rem;">
<div>package · <span style="color:var(--amber);">@observer-protocol/policy-engine</span> <span data-engine-version="current">1.0.0-rc.21</span></div>
<div>package · <span style="color:var(--amber);">@observer-protocol/policy-engine</span> <span data-engine-version="current">1.0.0-rc.22</span></div>
<div>licence · MIT · self-hostable · no account</div>
<div>limits · <a href="https://github.com/observer-protocol/op-policy-engine/blob/main/KNOWN-LIMITS.md" target="_blank" rel="noopener" style="color:var(--amber); text-decoration:none;">KNOWN-LIMITS.md ↗</a></div>
</div>
Expand Down Expand Up @@ -1142,7 +1142,7 @@ <h2>The register, stated plainly.</h2>
<div class="reg-row" id="reg-offline-verification-with-nothing-from-us">
<div>
<div class="reg-what" data-shared-copy="offline-scope">Offline verification with nothing from us</div>
<div class="reg-detail" data-shared-copy="offline-scope"><code>@observer-protocol/policy-engine</code> <span data-engine-version="current">1.0.0-rc.21</span> on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.</div>
<div class="reg-detail" data-shared-copy="offline-scope"><code>@observer-protocol/policy-engine</code> <span data-engine-version="current">1.0.0-rc.22</span> on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.</div>
</div>
<div class="reg-state live">Live</div>
</div>
Expand Down
30 changes: 23 additions & 7 deletions results/engine-payload-exports.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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."
},
Expand All @@ -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": [
{
Expand Down Expand Up @@ -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"
}
]
}
68 changes: 46 additions & 22 deletions scripts/check-page-payload-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.`);
8 changes: 4 additions & 4 deletions scripts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
</url>
<url>
<loc>https://observerprotocol.org/verify.html</loc>
<lastmod>2026-08-23</lastmod>
<lastmod>2026-08-24</lastmod>
</url>
<url>
<loc>https://observerprotocol.org/wdk.html</loc>
Expand Down
Loading
Loading