Skip to content

feat(ceremony)!: GoogleJwtRoots verifies through NotaryService; the login and transfer products are removed - #21

Merged
xgreenx merged 15 commits into
mainfrom
feat/jwks-roots-notary-service
Sep 3, 2026
Merged

xgreenx merged 15 commits into
mainfrom
feat/jwks-roots-notary-service

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Removed: the login and transfer products

Registry, WebWallet, WalletFactory, XZkVerifier, GoogleOidcVerifier, the Bank diamond (facets, libraries, deploy script) and the legacy Notary are removed outright, with their tests, fixtures, generated bb verifiers, Rust bindings, embedded artifacts, TS ABIs and the verifier-regeneration pipeline. They are not used any more. Anyone who wants to see how a product was built on notarized sessions before the ceremony reads the diff of this PR (commit 5c057b8, 20k lines) or checks out tag v0.6.0, the last release that shipped them.

What replaces the JWKS path

IdentityJwksRoots is now GoogleJwtRoots, under ceremony/ beside the only contract that reads it, GooglePlatformVerifier (the google/v1 profile's trusted signing keys; "identity" said nothing about it). It verifies a notarized reading of Google's JWKS through NotaryService like every other consumer: rotate(bytes attestedData, bytes proof) is payable and forwards exactly quoteRotation(). Rotation stays permissionless. On the decoded record it checks the authority, freshness, exact coverage with zero commitments, the origin-form request line, CRLF-only line endings, exactly one host: www.googleapis.com in the request head (googleapis serves many virtual hosts under one certificate), the 200 status line, exactly one framing header, then parses Google's pretty-printed key set on chain, bounded by MAX_KEYS. The list keeps two generations of Google's key set and nothing else: the set of the latest reading and the set of the reading before it, at most eight moduli each. A newer reading with the same set refreshes the timestamp; a different set shifts current to previous and installs the new one; a reading that is not newer reverts NotNewer, so a front-run keeper loses gas and not the fee. A key is trusted for READING_LIFETIME from its reading's own time, so nothing needs cleaning up and no owner function touches keys (prune, untrustModulus, the per-kid maps and the tracked list are gone). Each key's e must be AQAB, the exponent the circuit fixes. Storage is ERC-7201; the __gap layout is gone.

The crate and the npm package cover NotaryService, CeremonyProofVerifier, GoogleJwtRoots, IdentityNames, LibidFactory, WTIA9. Deploy.s.sol deploys the identity stack only.

Real data

ceremony/test/fixtures/google-jwks-session.json is a genuine MPC-TLS session with www.googleapis.com, captured 2026-09-03T10:32:07Z through the notary on libid-org/notary#7 (examples/notarize_jwks.rs, signed by the anvil #0 key). GoogleJwtRootsRealSession.t.sol verifies that exact record through a real NotaryService: all four kids Google served are installed with the same modulus hashes a content-length re-reading of the body yields; the request line on the wire is origin-form (libid-org/libID-rs#5 proven end to end); Google framed the body chunked with capitalised header names, which the normaliser handles. Cheating variants derived from the real record: a flipped byte and a stranger's signature (UntrustedNotary), the record past its window (StaleProof), and a bare-LF Host smuggled into the real request and re-signed by a trusted key (BareLineFeed). The builder reproduces the real 2811-byte record byte for byte, a second cross-language pin.

Audit

An adversarial audit ran 51 attacks as scratch forge tests and live probes: signature malleability and tampering, record layout edges, request-line and Host smuggling in every form, request bodies and pipelining, framing and chunk edge cases, JSON decoys, replay and timing games, reentrancy and fee ordering. None gets a key in without a trusted notary key or Google itself. An independent researcher confirmed live that Google's front end routes by the plaintext Host header, not the certificate (a request under the www.googleapis.com certificate with Host: storage.googleapis.com reaches Cloud Storage, and a bucket named oauth2 exists), so the Host pin is load-bearing; that ?callback= returns attacker-named JSONP in a 200, which only the exact request-line pin blocks (now a named test); and that no request header yields a 200 JSON body with different keys. Hardenings from it: EncodedBody refuses a content-encoding head explicitly, e is pinned to AQAB, and the keeper skips a modulus the contract would refuse. Governance note: the roots owner can point setNotaryService at a hostile contract; that lever belongs behind the same multisig as the notary key.

A second audit ran 27 scenarios against the two-generation state machine: ordering, replaying and front-running genuine readings from Google's inconsistent backends, refresh/shift interactions, equal stamps, boundaries, events, atomicity, storage layout and upgrade. Everything behaves as specified, with one documented caveat: two generations remember two distinct sets, so if Google ever serves three distinct sets at once where two lack the same live key (a remove-before-add rotation), an attacker ordering genuine readings can make that key untrusted until the keeper's next reading lists it again — at most the poll interval, tokens under that key fail meanwhile, no key can be inserted. Accepted for now; a bounded per-modulus last-seen list would close it if Google's rotation ever shows that shape.

Review

A four-lens adversarial review ran on the branch. One high finding, fixed in 6def733 with a live proof of concept: a second Host line behind a bare LF passed the CRLF-anchored count, and Google's front end routes on it. 604b42a closes three low findings: an empty key set was accepted, equal-time readings could swap a key, doubled framing headers were read from the first copy.

Verification

forge test: 404 passed in 25 suites, every revert by exact selector. cargo test --all 8 passed with anvil; pnpm -C ts test 24 passed; fmt, clippy, biome and regen-identity-handles.py --check clean. No IdentityJwksRoots, JwksRoots or archived left in the tree.

Before / after merge

…NotaryService

The Google JWKS trust list was the last consumer of the legacy Merkle
transcript proof and the legacy `Notary` contract. Everything else on the
verification path authenticates a section 9.1 record through `NotaryService`
-- `verify(bytes attestedData, bytes proof)`, one fixed fee, the decoded
record handed back only once a trusted key has vouched for it. The root list
now does the same: a rotation is an ordinary notarized session, paid for like
one, and `initialize` takes the Notary Service instead of `Notary`.

What the keeper's prover produces is fixed by the design: everything in both
directions revealed, nothing committed. That layout is what makes reading the
transcript by concatenation safe -- the direction tiles the signed length,
carries no commitment and starts at the origin, so the join IS the document
that crossed the wire. A Platform Verifier reading around a commitment cannot
make that claim, which is why this contract checks `HiddenBytes` where they
check framing.

The transcript checks are the contract's own, stated in the order they run:
the origin-form request line (`GET /oauth2/v3/certs HTTP/1.1`), exactly one
`Host` header counted over normalized bytes and equal to
`www.googleapis.com` -- googleapis.com serves many virtual hosts under one
certificate, so the TLS authority alone does not pin which backend answered
-- the `HTTP/1.1 200 ` status line, the FIRST head boundary (a chunked body
legitimately ends in `0\r\n\r\n`), and the body under whichever framing the
head declares: strict chunked coding, an exact `Content-Length`, or
`connection: close`. The body is read as the one document Google publishes:
`keys` located by counting, flat objects, `kid` and `n` located the same way,
and anything else refused by name.

Kept verbatim: the monotonic per-kid rule that makes open replay harmless (an
older reading is skipped, never reverted; an identical resubmission writes
nothing and re-stamps nothing), the self-pruning cap, the base64url decoder
and the 120-bit limb split -- the keeper's `decision::modulus_hash` and the
Google circuit compute the same value, and a test pins the keeper's vector
across both. Storage moves to ERC-7201 like every other contract here, with
explicit getters standing in for the public variables.

`CeremonyAttestation.normalizeHeaderBytes` is now `internal`: the `Host`
count reads it, and what it strips decides what a count can miss, so the
JWKS list shares the library's one implementation rather than carrying a
second.

Tests build the record with AttestationBuilder, sign with `vm.sign`, and run
against a real NotaryService behind a proxy charging a real fee. Google's
real body (fetched 2026-09-03) is a fixture, served chunked as Google serves
it. Every revert is asserted by its exact selector, including the library's
`CoverageGap` and NotaryService's `UntrustedNotary`. The legacy Merkle
fixture and its fs permission go; UpgradeSafety pins the new namespace slot
and initializes through the Notary Service; Deploy.s.sol passes the Notary
Service it already deploys (a full rewrite of the script follows).

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…dentity stack

The login (Registry, WebWallet, WalletFactory, the X ZK and Google OIDC
verifiers) and transfer (Bank diamond) products, and the legacy Notary they
verified through, move to solidity/archived/ -- outside every forge root,
so nothing compiles, tests or ships them. They stay in the tree rather than
only in history because they are the worked example of a full product built
on notarized sessions; archived/README.md says so and pins where they last
built (v0.6.0).

Why now: IdentityJwksRoots was the last consumer of the legacy Notary, and
it now reads a notarized session through NotaryService like every other
contract. With that, nothing under contracts/ referenced the wallet
products, and keeping them compiled meant keeping two bb-generated
verifiers reproducible against a circuits release the ceremony circuits are
replacing. So the regeneration machinery goes with them: regen-verifiers.sh,
its dispatch workflow, CI's verifiers job (and publish-crates' needs on it)
and the circuits-version pin. The ceremony circuits bring their own
regeneration when their release lands; git history keeps the script.

Deploy.s.sol deploys the identity stack only, in dependency order:
NotaryService -> CeremonyProofVerifier -> IdentityNames (a keyspace per
platform) -> IdentityJwksRoots pointed at the Notary Service. It reads
DEPLOYER_KEY/PRIVATE_KEY, NOTARY_ADDRESS and NOTARY_FEE_WEI; BACKEND_ADDRESS
and X_CLIENT_ID are gone. deploy.yml never named any of the removed ones.

The Rust crate and the npm package follow. The login/notary/oidc/transfer
bindings and the diamond helpers are deleted; a ceremony module binds
NotaryService and CeremonyProofVerifier; IdentityJwksRoots' binding is the
rotate(bytes,bytes) payable interface with quoteRotation and notaryService;
and the covered-artifact lists (COVERED, vendor-artifacts.sh, codegen.mjs)
name the seven contracts that remain. load_linked_bytecode stays although
nothing links a library today: the UltraHonk verifiers the ceremony circuits
bring do, and this is the path they will deploy through. The anvil tests
deploy the identity stack behind proxies and read the wiring back, rotate
and upgrade the Notary Service, and bootstrap the factory with a
NotaryService proxy; the TypeScript payable example is a rotation paying
the Notary Fee.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…nts the head only

The JWKS root list's whole safety argument is that a session to
www.googleapis.com carried exactly one `Host: www.googleapis.com`:
googleapis.com serves many virtual hosts under one certificate, and only
the Host header pins which backend answered. The count was CRLF-anchored
over normalized bytes, and normalization keeps CR and LF -- so a second
`Host` whose preceding line ended in a bare LF was never counted, and an
obsolete line fold (`\r\n host:`) was counted as the legitimate one.
Confirmed live: Google's front end honours the bare-LF
`Host: storage.googleapis.com` and routes to the storage backend, while
the contract saw one CRLF `Host`, the right one, and installed the keys.

The bearer-header count in CeremonyAttestation already guards against
both shapes before counting. Those two scans are now one internal helper,
`requireCrlfLineEndings`, shared the way `normalizeHeaderBytes` is: what
a scan refuses decides what a count can miss, so one implementation per
security-relevant scan rather than one per caller. `_requireJwksRequest`
runs it right after the request line, in the bearer path's order.

The `Host` count also stops at the request's head boundary. A `Host`
after the blank line is body bytes to the server, whatever a count over
the whole direction would make of it; a request whose head never ends is
refused with `NoHeadBoundary`, the same error the response side raises.
No new errors, so the ABI the bindings vendor is unchanged.

Tests: the live bare-LF request, the fold (alone and paired with a
bare-LF second host), a Host only in the body, and a head that never
ends. All four passed against the previous contract.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…framing headers

Three shapes the review found the list still accepted, each closed with
the test that shows it.

A reading whose key set is empty paid the fee, moved the freshest-reading
mark and wrote nothing; it now reverts EmptyKeySet.

Two readings dated the same second could disagree about a kid, and the
second one won: it swapped the key and retired the one Google still signs
with. The provenance floor is strict now, so equal evidence leaves the
entry alone whatever it carries -- the rule equal evidence gets everywhere
else in the naming system.

A response head carrying two Content-Length lines, two Transfer-Encoding
lines, or one of each was framed by whichever copy the parser met first.
The header the body is located by gets the rule every other revealed read
gets: a delimiter that matches twice is refused, not chosen from. A coding
other than chunked is refused too, rather than read as a raw body.

The upgrade-safety test lands a rotation before upgrading, so what has to
survive is real state in the namespaced slots and not the empty list
initialize left behind.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx force-pushed the feat/jwks-roots-notary-service branch from 64982a1 to 604b42a Compare September 3, 2026 10:05
@xgreenx
xgreenx changed the base branch from test/coverage-and-dead-code to main September 3, 2026 10:05
The previous commit moved Registry, WebWallet, WalletFactory, XZkVerifier,
GoogleOidcVerifier, the Bank diamond and the legacy Notary under
solidity/archived/ as frozen reference. A tree that is never compiled,
tested or shipped still has to be read around, kept out of every forge
root, and explained in two places; git history and tag v0.6.0 already hold
the code, and this change's diff is where a reader finds what was removed
and how it worked.

So the archive goes: the directory, the README layout entry, and the
foundry.toml note that kept it outside the source roots.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…verifier it serves

The list of Google signing keys served exactly one reader, the `google/v1`
Platform Verifier, yet lived under contracts/identity/ with "Identity" in
its name -- a placement that said nothing about what it was for and put it
one directory away from the contract that reads it. It is now
`GoogleJwtRoots` in contracts/ceremony/, with the test and the real-body
fixture moved alongside, and the ERC-7201 namespace follows the name:
`libid.storage.GoogleJwtRoots`, slot recomputed and pinned in
UpgradeSafety. A fresh testnet deployment follows, so no slot
compatibility with the old namespace is owed.

The verifier's side of the surface is renamed to match (IGoogleJwtRoots,
jwtRoots / setJwtRoots / JwtRootsChanged), as are the deploy script's
GOOGLE_JWT_ROOTS_ADDRESS line, the Rust binding (now in
bindings::ceremony, so identity.rs is IdentityNames alone), the vendored
artifact list, the TS ABI export (googleJwtRootsAbi, calls.googleJwtRoots)
and every README and crate description that named the old contract.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…ys to cheat with it

Every session GoogleJwtRoots.t.sol feeds the list is one a test author
built: a constant request, a response the test framed, a notary that is
vm.sign. That proves the parser against shapes we imagined, and nothing
against the shape the deployment will actually meet. The fixture added
here is a genuine MPC-TLS session with www.googleapis.com, captured on
2026-09-03 by notary/examples/notarize_jwks.rs through a notary signing
with anvil #0 -- request line, head, framing, key set and signature all
as they crossed the wire, none written by hand.

The real bytes and the real signature rotate the list through a real
NotaryService trusting that key, and every kid Google published that
minute lands: four today, not the two or three the design anticipated,
with member order varying inside each JWK. The same body re-read under
Content-Length by a second trusted key installs identical modulus
hashes on a fresh list, so the framing and the signer are transparent
to what gets written. Two facts about the wire are pinned while they
are cheap to pin: the request line is origin-form, which is the libid-rs
prover fix proven end to end, and Google frames the body chunked with
capitalised field names -- the one head the normalisation is tested
against that a server wrote rather than a builder.

The cheats are derived from the real record rather than invented, each
refused by its exact selector and arguments: one flipped bit under the
genuine signature recovers to a key nobody trusts (and the test names
which), a stranger's signature over the untouched bytes, the record
presented one second past its window, and a bare-LF Host smuggled into
the real request and re-signed by a key the service does trust. The
last one re-encodes through AttestationBuilder, which a test first shows
reproduces the real record byte for byte, so the forgery differs from
the wire in the edit alone.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx xgreenx changed the title feat(identity)!: IdentityJwksRoots verifies through NotaryService; the wallet products are archived feat(ceremony)!: GoogleJwtRoots verifies through NotaryService; the login and transfer products are removed Sep 3, 2026
… key set, and no history

The list was a per-kid state machine: a modulus, an expiry and a
monotonic stamp per kid, a tracked-kid enumeration with a cap, a prune
anyone could call, an untrust only the owner could. Google's JWKS is
none of that. It is an unordered set of two to four keys with no "next"
marker, so the faithful form of a reading is the set itself -- and the
decision is: no history of keys, no maintenance, bounded state. "We
don't need to store the history of all keys, and we don't need to
require any maintenance. No one will call untrustModulus."

So the list is two generations of the set and nothing else. `current`
is the latest reading applied (Google's set already lists the key it
will sign with next), `previous` the reading before it. A newer reading
of the same set only restarts `current`'s lifetime; a newer reading of
a different set shifts `current` into `previous` and drops what
`previous` held. `previous` is kept because Google's rotation and the
keeper's reading are not simultaneous: a token minted under the key
Google just rotated out is still being presented for the rest of its
hour, and a keeper's reading can land in between. A generation is
trusted until READING_LIFETIME after its reading's own `createdAt` --
the notary's clock -- so trust lapses by time alone, with no prune, no
untrust, no transaction from anyone.

A reading is read whole before any of it is written (`EmptyKeySet`,
`TooManyKeys(count)`, `DuplicateKey(modulusHash)`), a reading dated no
later than the one in force is ignored rather than refused (a
front-runner must not brick the keeper), and the set comparison is
order-insensitive. Removed: modulusOfKid, expiresAtKid, rotatedAtKid,
the tracked-kid set, MAX_TRACKED_KIDS, DEFAULT_MODULUS_TTL, prune,
untrustModulus, currentRoots, RootInfo, the four per-key events and
TooManyKids. Added: MAX_KEYS, READING_LIFETIME, currentKeys(), and the
events KeysRotated(observedAt, kids, moduli) / ReadingRefreshed.
`trustedHashExpiresAt`, what GooglePlatformVerifier reads, is unchanged
in shape. The ERC-7201 slot is unchanged; the struct behind it is not.

Three refusals from the audit ride along, since they are checks on the
same reading: a query in the request target (`?callback=` turns the
same path into JSONP whose body begins with requester-chosen bytes) is
refused by the pinned request line; a `Content-Encoding` on the
response is refused by name (`EncodedBody`) rather than as some byte
deep in the JSON walk; and every JWK's `e` must be exactly `AQAB`
(`WrongExponent`) -- the google/v1 circuit verifies RS256 under 65537
and the verifier trusts a key by its modulus alone.

Rust and TS follow the ABI: the binding declares `Generation`,
`currentKeys`, `KeysRotated`/`ReadingRefreshed`; the anvil test reads
both generations empty; the READMEs point at `currentKeys`.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
…time

needsRotation answered the empty case by arithmetic on a zero stamp, which
comes out true on a live chain and false on a chain younger than the
reading lifetime. Say it outright instead.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
rotate() returned silently when a reading was dated no later than the
one in force. That charged the honest keeper the Notary Fee for nothing
done and gave it no signal; a front-runner landing the same reading
first made the keeper's transaction succeed empty. Refuse it instead --
NotNewer(createdAt, observedAt), both clocks -- so the revert rolls the
fee back with the state and the keeper loses gas only.

"Never revert, or a front-runner could brick the keeper" was the rule
for the per-claim nullifier design, where the first submission consumed
the claim. Nothing is consumed here: the front-runner's reading IS the
keeper's, applied, so the keeper's transaction has nothing left to do
and its next reading lands as usual. The freshness checks still run
first, so a stale or future record is named as such.

Tests: the replay, same-second and older-reading cases expect NotNewer
with exact arguments and still assert the stamps; a new front-run test
shows the keeper's fee coming back and its next reading landing. The
Rust binding's doc follows. The ABI gains an error, so the vendored
artifacts and the TS ABI are regenerated (both gitignored).

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
test_upgrade_GoogleJwtRoots read both generations back through the ABI
after the upgrade, which proves the values survived but not where they
live. Read them by slot too: `notary` is word 0 of the namespace, a
Generation is two words (its stamp, then its array's length word, with
the elements off keccak256 of that slot), so `current` is words 1-2 and
`previous` words 3-4. A field added to Generation moves `previous`, and
an upgraded implementation would read the old generation's stamp as the
new field -- that is the change this test now fails on, loudly, before
any upgrade does. Checked by adding a uint256 to Generation: the word-2
pin fails first.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The state-machine audit showed that a remove-before-add rotation served
across Google's backends lets a submitter ordering genuine readings push a
still-live key out until the next reading that lists it lands. That is
liveness only and self-healing: nothing is inserted, tokens under the key
fail meanwhile, and the keeper's next poll sees it untrusted and restores
it. The user chose on 2026-09-03 to keep two generations over a
per-modulus last-seen list, which would close the gap at the cost of
remembering keys by modulus for a lifetime.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The handle-vector paragraph said the Rust and TypeScript outputs would
activate once those packages existed; they exist and CI checks them. The
package README documented bindCall and encodeGitHubProof, which left with
the hand-written constructions; the claim builder is generated now. The
deploy script pointed a 'rotation listener' at the roots; that is the
keeper. The roots contract said a second provider would want the same
list; it would want one of its own, pinned to its own host. Factory tests
named libid.registry and libid.bank, contracts that no longer exist.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx force-pushed the feat/jwks-roots-notary-service branch from bd8d5b6 to 3faa41f Compare September 3, 2026 12:47
@xgreenx
xgreenx marked this pull request as ready for review September 3, 2026 12:51
@xgreenx
xgreenx requested a review from SupremaLex September 3, 2026 12:51
Comments and names that still described what this branch removed, or an
older shape of what it kept. No behaviour changes.

- IdentityNames.sol: `bind` was the legacy login entry point; the one
  write path is `claim`. Five references renamed, the "two paths" /
  "both entry points" rationales on CeremonyBound and _write rewritten
  for the single ceremony path, and the Platform storage comment no
  longer promises a "current version" the struct does not hold.
- IdentityNames.t.sol: the non-normalizing-handle test is named after
  `claim`, the function it exercises.
- CeremonyProfile.t.sol: the fixture opens with the X API authority id,
  not a format tag, platform id and session tag.
- GitHubPlatformVerifier.t.sol: the mirrored X test sits at line 403.
- UpgradeSafety.t.sol: the MainPlatform/PrPlatform layouts, the unused
  `_emulateMainPlatform` helper and the HandleNormalizer import that
  served only them are gone (per-platform `latestVersion` no longer
  exists); the stale-version-word test no longer speaks of "the PR".
- ts codegen.mjs: the overload example named `webTransferV2`, a removed
  Bank function.
- ts calls.anvil.test.ts: the removed OIDC proof-artifact tests are no
  longer cited as the skip precedent.
- ts resolve.ts: names are claimed through a ceremony, and there is no
  wallet package to compare against.
- ts package.json / tsconfig.build.json: drop the src/identity/fixtures
  exclusion, a directory that no longer exists.
- regen-identity-handles.py: the Rust package exists and is a live,
  CI-checked output.
- rust bindings: one stack, and one module per contract directory
  rather than "product areas".

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The NatSpec of the newer-reading rule and two test comments narrated the
silent return that an earlier draft of this branch had, as a changelog
inside the contract. The commit that made the change carries that story;
the code says what holds now. The real-session test cited the capture
tool by a path that exists in the notary repository, not this one, and
the package README claimed ABIs for every libid contract where it ships
the identity stack's.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx merged commit de229ac into main Sep 3, 2026
11 checks passed
xgreenx added a commit that referenced this pull request Sep 3, 2026
Brings #21 under the resolver: the wallet products are gone, the JWKS
roots are GoogleJwtRoots under ceremony/, and the crate and package lists
name the identity stack. The three conflicts were the artifact lists the
resolver adds itself to; HandleResolver keeps its entry beside
IdentityNames and the removed contracts stay removed.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@Wondertan
Wondertan deleted the feat/jwks-roots-notary-service branch October 2, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants