@oaath/sdk
v0.3.13
Published
OAAth browser client and Kernel/ZeroDev runtime.
Downloads
2,390
Readme
@oaath/sdk
OAAth browser client and Kernel/ZeroDev runtime. See the repository README.
| Entry | What it gives you |
| --- | --- |
| @oaath/sdk | loginWithOAAth / completeOAAthLogin, createOAAth (owner, wallet and oauth approvals), requestOwnerOperationApproval, OaathClientError |
| @oaath/sdk/kernel | createKernelRuntime, kernelKey, permission approval and revocation, prepareOwnerOperation / verifyOwnerOperation, prepareOwnerPermissionUninstall, readKernelModules |
| @oaath/sdk/cetane | createCetaneChainPorts and the Grant-backed EIP-1193 provider |
| @oaath/sdk/advanced | custom-deployment ports, low-level Kernel v4 encoders, and the fully injected composition |
| @oaath/sdk/persistence | the IndexedDB store set and persisted record contracts |
| @oaath/sdk/testing | the deterministic in-memory store set (never a production dependency) |
The three createOAAth realms share one shape (connect(), then
requestPermission, resume, a Grant's sendCalls):
- no
approvals: owner-only execution from an existing Kernel account (Owner operations); approvals: { kind: "wallet", owner }: the connected wallet approves session Grants for an existing account (Wallet-approved Grants);approvals: { kind: "oauth", issuer, clientId, redirectUri, launch? }: the account root approves in the OAAth portal; a member's request may return pending until the root approves (Login with OAAth).
Wallet approvals take one optional session setting (OaathSession):
session?: { kind?: "ecdsa" | "webauthn", custody?: "browser", ... }.
Omitted, the realm generates an ECDSA session key in browser custody.
session: { kind: "webauthn", ...webauthnKeyInput } makes the owner review and
install the caller's passkey as the operator credential; no session key is
generated or stored. Any other custody fails with
oaath_client_capability_unsupported (source session_custody_unsupported)
before any session key, store, or signer request exists.
Custom Kernel sessions can set a fixed-window quota with
{ kind: "rate-limit", intervalSeconds: "86400", maximumOperations: "25" }
in sessionOperator({ key, policies }). Include a call profile; expiry and an
independent lifetime operation-limit can be included too. The quota belongs
to the onchain permission/account pair. Installation starts the first window;
the first validation after it ends replenishes the count and starts the next
interval. A validated operation consumes a slot even if execution reverts.
Reopening a runtime does not reset quota, and missing receipts still cannot
authorize resubmission.
For an existing Kernel v4 account, readKernelModules from @oaath/sdk/kernel
exposes Cetane's bounded authority inventory:
const inventory = await readKernelModules(client, {
address: accountAddress,
version: "4",
budget: { maxRequests: 64, timeout: 5_000 },
});It returns the root, validators, executors, fallbacks, hooks, and permissions
with their IDs, signers, and policies. Entries distinguish state-confirmed
from history-derived; unresolved module context remains visible. State reads
use one block number. Supply known permissionIds or fallback selectors when
opaque installation calldata prevents discovery. complete: false and reason
report incomplete history, unknown context, or an exhausted budget. Even a
state-confirmed entry is an observation at that block, not signing authority or
finality evidence. Kernel's contextless module events cannot prove exhaustive
enumeration; never turn an incomplete inventory into a claim that authority is
absent. This helper supports v4 only and sends no transactions.
To import a v4 UUPS account on another reviewed build, supply the deployment owner's additional implementation addresses and exact runtime hashes:
const deployment = kernelDeployment({
chainId,
reviewedImplementations: [{ address: implementationAddress, runtimeCodeHash }],
});
const runtime = createKernelRuntime({ deployment, operator, reads });
const account = await runtime.bindAccount({ address: accountAddress });The list is captured and frozen. Binding verifies the account's actual
implementation against the list and its code against the supplied hash;
unreviewed addresses, wrong hashes, or unreadable evidence fail before signing.
kernelAccountDeployment(account) retains that same reviewed profile. A hash
must come from the deployment owner's review of Kernel v4/EntryPoint 0.9 code,
including its chain-specific immutable values. Reading arbitrary code and
adding its hash automatically would bypass that review. Existing accounts need
no factory; deriving a new account still uses the canonical factory and build.
This option does not add runtime versions or change supported root credentials.
The reset policy is pinned by OAATH_KERNEL_RATE_LIMIT_POLICY and
OAATH_KERNEL_RATE_LIMIT_POLICY_RUNTIME_CODE_HASH from @oaath/sdk/kernel.
The matching module must already be deployed on the action chain; binding
fails with kernel_runtime_policy_unavailable for missing or different code.
The repository's packages/sdk/test/fixtures/kernel-rate-limit-deployment.json
contains the deterministic deployment input. Its complete Solidity input is
reproduced by bun run --filter @oaath/sdk check:rate-limit-artifact; bundled source
licenses are in that fixture directory's licenses/ folder. This primitive is
available through the Kernel API; the default permission-request schema still
exposes its existing lifetime operation bound.
Existing Kernel 0.3.3 accounts also support custom passkey sessions through
createKernelRuntime({ deployment: kernelAccountDeployment(account), reads,
operator: sessionOperator({ key: kernelKey(passkey), policies }) }) from
@oaath/sdk/kernel. The caller supplies the selected credential and authenticator
callback; the WebAuthn key checks its challenge, credential public key, RP ID, exact
HTTPS origin, user presence and verification before returning a signature.
Use the same approveKernelPermission / materializeKernelPermission
flow as ECDSA sessions. Root-owner binding remains ECDSA-only; the permission's
signer is independent of that root. This custom Kernel API does not replace
the application's durable operation journal or implement browser credential UI.
For approval and preparation with only public identity, use
kernelKey({ credential }) in the session operator. It
derives the same permission as the matching signing profile and cannot sign.
For custom revocation of a permission, prepareKernelPermissionRevocation
(from @oaath/sdk/kernel) prepares the exact owner calls for the approval's
Kernel version, and verifyKernelPermissionRevocation proves removal at a
finalized canonical block. A successful operation receipt alone does not prove
removal.
Address inputs accept lowercase, uppercase hex digits, or a valid EIP-55 checksum.
The SDK captures them in lowercase before hashing or persistence. Invalid mixed-case
checksums fail before signing; wallet RPC failures use -32602 with the field and
reason in error.data.address and a readable diagnostic in error.data.message.
Login with OAAth
loginWithOAAth({ issuer, clientId, redirectUri }) signs the user in through the
issuer's portal popup with OpenID Connect: PAR with PKCE (scope=openid), the
/authorize popup, the code exchange, and an ES256 id_token verified against
{issuer}/oauth/jwks (issuer, audience = clientId, nonce). Call it directly
from a click handler: the popup opens before anything is awaited. The page at
redirectUri (on the same origin) calls completeOAAthLogin(), which posts the
response to the opener on that origin only; state and the RFC 9207 iss are
checked before the code is redeemed. It resolves to
{ account, accountProfile, signer, accounts, verified: true, idToken }: the
account and signer the user chose, and accounts, every account the signer is
an active member of ({ address, role: "root" | "permission", status: "active" },
from the id_token's oaath_accounts). A malformed entry refuses the login with
oaath_client_identity_invalid. The signer proved control of its credential to OAAth
and is a member of the account; login is identity, never authority. Failures are OaathClientError codes: oaath_client_access_denied
(cancelled or closed), oaath_client_popup_blocked, oaath_client_login_timeout,
oaath_client_issuer_mismatch, oaath_client_state_mismatch, and
oaath_client_identity_invalid. The client is registered once with
POST {issuer}/oauth/clients; see examples/oauth-login.
requestOwnerOperationApproval({ issuer, clientId, redirectUri, request }) asks
the account root to approve one owner operation in the same popup (or through
launch, as for Grants). request is
the exact unsigned request from prepareOwnerOperation (@oaath/sdk/kernel).
The returned signed operation must be for that same request, and
verifyOwnerOperation checks the account binding and the root's signature (a
WebAuthn root asserts for the issuer's relying party). It resolves to
{ signed, entryPoint, userOperation }, ready for the caller's own
eth_sendUserOperation; OAAth never submits. A signed operation for another
request fails with oaath_client_state_conflict (oauth_operation_mismatch).
prepareOwnerPermissionUninstall({ account, chainId, approval, nonce, gas,
paymaster? }) (@oaath/sdk/kernel) prepares the owner operation that removes
one installed Kernel 0.4.0 permission: approval is the Grant's install
approval (authorization_details[0].enable), whose packages name the
permission's policies and signer; they are uninstalled in reverse install
order, then the signer. It is an ordinary owner-operation request for the same
approval and verification path, and refuses an approval for another account.
Portal-approved Grants (createOAAth({ chains, approvals: { kind: "oauth", issuer,
clientId, redirectUri } })) open the same popup from requestPermission. Where no
popup can open, as in an extension service worker, approvals.launch(url)
replaces it: it opens the portal (for example chrome.identity.launchWebAuthFlow)
and resolves with the redirect URL, which must be redirectUri itself; state,
iss, the code exchange and the id_token are checked exactly as for the popup.
See examples/extension.
A member's request waits for the account root to approve it in the portal: the
issuer answers the code exchange with authorization_pending, and
requestPermission resolves to { state: "pending", requestId, expiresAt }
instead of a Grant. The SDK journals the issued code, its PKCE verifier and
nonce in the realm's stores (one pending request per issuer, client and origin;
another requestPermission meanwhile fails with oaath_client_state_conflict,
source oauth_permission_pending). connection.redeemPending() makes exactly
one token request per call and never starts a new authorization: it returns the
Grant once the root approved, the same pending result while it has not, or
null when nothing is pending. Polling is the application's choice. A root
rejection fails with oaath_client_permission_rejected; it, an expired or spent
code (invalid_grant), and the request's expiry clear the journal.
An OAuth-approved Grant's revoke() revokes it locally exactly as any Grant,
then makes one bounded request asking the issuer to invalidate it
(POST {issuer}/oauth/grants/{id}/invalidate), authenticated by the Grant's own
session key: Authorization: OAAth-Grant-Proof <issuedAt>.<signature>, a
personal_sign over OAAth grant request v1 with the grant, method, path and
issue time. The realm never stores an access token. It resolves to { issuer }: invalidated, refused,
unavailable or not-attempted. The issuer's answer never blocks or undoes the
local revocation, and a later revoke() asks again.
Owner operations
For an existing Kernel account, execute calls directly as its root owner. The
account's Kernel version (0.3.3 or 0.4.0) is detected onchain on each send and
reported as review.kernelVersion; no version is configured. Omitting
approvals gives owner-only execution, which needs no issuer, relay, Grant, or
enable approval:
import { createOAAth } from "@oaath/sdk";
// Plain descriptors: createOAAth builds the default Cetane chain ports.
const chains = { 143: { publicRpcUrls: [publicRpcUrl], bundlerUrl } };
const oaath = createOAAth({ chains, account: existingKernelAddress });
const account = oaath.account(existingKernelAddress);
const owner = account.owner(walletClient);
const calls = { chain: 143, calls: [{ target, value: "0", data }] };
const review = await owner.reviewCalls(calls); // estimates capacity; no prompt or submission
const operation = await owner.sendCalls(calls); // one personal_sign prompt, one UserOperation
await operation.wait();
// Retain operation.id; after recreating the client, recovery requires no wallet:
const saved = await account.getOperation({ chain: 143, id: operation.id });
await oaath.close();The operation journal defaults to stores: { kind: "indexeddb" }. Tests and
non-browser development choose stores: { kind: "memory" }, and any backend
accepts a durable journal adapter, e.g.
stores: { kind: "memory", operations: postgresJournal }; the client owns its
close. An unresolved operation occupies
one account/chain slot. Concurrent sends and sends after reload fail with a state
conflict until observation resolves it; getOperation only observes the exact
saved identity. Closing releases resources and does not revoke account authority.
The owner key is an optional setting: a connected wallet (the ECDSA
default) or any kernelKey(...) signing profile, such as a raw P-256 key
(account.owner(kernelKey({ credential, sign }))). The account stays at its
existing address. Each send checks its implementation, EntryPoint, root
validator and current owner. The root validator must expose its owner onchain:
the reviewed ECDSA validator, or on Kernel 0.4.0 the pinned raw P-256 or
WebAuthn validator. WebAuthn uses the same kernelKey({ credential, credentialId,
rpId, origin, authenticate }) profile as a passkey session. Owner-only execution uses the
bundler route by default. Applications can explicitly estimate a session before
selecting owner execution, as described below; OAAth never silently changes the
signer of an operation.
Wallet-approved Grants
For scoped sessions without an issuer or portal, add
approvals: { kind: "wallet", owner } to the same options. owner is a
browser or local wallet implementing account, signMessage, and signTypedData, which approves with one typed-data prompt, or any
kernelKey(...) signing profile the owner operations above accept, which signs
the same approval digest. The account's
Kernel deployment is detected on every configured chain; chains that disagree
fail with local_account_deployment_mismatch:
const oaath = createOAAth({
chains,
account: existingKernelAddress,
approvals: {
kind: "wallet",
owner: walletClient,
onApproval: async (review) => { await showPermissionPolicy(review.policy); },
},
});
const connection = await oaath.connect();
const grant = await connection.resume() ?? await connection.requestPermission({
chainScope: "all",
permissions: [{ calls: [{ target, selectors: [selector], valueLimit: "0" }] }],
expiresIn: 3600,
perChainOperationLimit: 10,
});
const operation = await grant.sendCalls({ chain: 143, calls: [{ target, data, value: "0" }] });
await operation.wait();
await oaath.close();perChainOperationLimit: 10 is a lifetime cap on each chain. Pass
{ count: 10, intervalSeconds: 86_400 } for at most ten operations per chain per
day; the pinned rate-limit policy refills the quota once per fixed window, and a
validated operation uses a slot even when its execution reverts.
The session key is encrypted in IndexedDB before consent. One wallet EIP-712
approval covers the exact permission on all configured chains. The optional
approvals.onApproval callback displays the decoded policy before the wallet prompt and
may throw to cancel. The SDK verifies
their root owner and matching permission nonce before prompting. The first send
enables the permission and executes its calls together. Reopening the same
origin/account/owner restores the session and operation journal; covered calls
then need no owner prompt. resume() can also return a revoked, expired, or
revoking Grant for observation or cleanup; only an active covering Grant may send.
Another permission request requires explicit wallet consent. No issuer network
request is made. Chain RPC and bundler calls still use the configured ports.
To sign wallet-approved sessions with a passkey instead of the generated key, pass
session: { kind: "webauthn", credential, credentialId, rpId, origin, authenticate }
(the WebAuthn kernelKey input). The passkey stays in its authenticator; only its public
credential is recorded in the Grant, and reopening with the same passkey resumes it.
The pinned WebAuthn signer verifies through the Daimo P-256 verifier contract, not
the RIP-7212 precompile, so approval fails closed with
oaath_client_capability_unsupported before any prompt on a chain without it.
Outside a browser, supply an explicit origin and a stores setting. The
default { kind: "indexeddb" } fails with oaath_client_store_unavailable
where IndexedDB is missing; it never silently creates an ephemeral session.
{ kind: "memory" } runs without per-store wiring for tests and development,
but a restart forgets the session, Grant and operation IDs: a forgotten
operation is never resubmitted, and its calls must not be replayed blindly.
Individual stores can be overridden on either backend, e.g.
{ kind: "memory", operations: postgresJournal }, and
{ kind: "indexeddb", factory, name } selects the IndexedDB factory and
database. The same client also exposes
oaath.account(existingKernelAddress).owner(walletClient) and account-level
operation recovery. close() releases resources without revocation;
disconnect(grant) revokes installed or unused approval onchain, signs out
locally, and deletes local key custody only after revocation completes. Failed
cleanup remains retryable. Missing receipts never authorize another submission.
Choosing a Grant signer
Existing Grant users may explicitly prefer the available owner:
const request = { chain: 143, calls: [{ target, value: "0", data }], signer: "auto" as const };
const review = await grant.reviewCalls(request); // chosen signer and structured reason
const operation = await grant.sendCalls(request);Default sends (or signer: "session") still use only the approved session.
auto selects owner authority when the realm has a signer; a public-only owner
profile selects session. Each accepted plain call bundle encodes one atomic
UserOperation. The API does not split oversized bundles, and an estimate, wallet
rejection or uncertain submission never changes the selected signer or retries.
Root execution does not enable the permission or consume its operation limit.
Its review reports no onchain Grant call/expiry/count enforcement and null policy
bounds; the client still requires an active, unexpired Grant. Owner approval of
the exact calls authorizes that wider root operation. For a one-off change with
no Grant approval at all, use the standalone owner mode above.
Owner and session sends share the Grant/chain operation lane. getOperation
recovers either signer without another signature or submission. Custom injected
signing configurations declare an available owner unless they provide a
public-only kernelKey({ credential }); a failed signer never becomes a session fallback.
Runtime primitives
The same owner operation is available through the lower-level runtime:
import {
bindKernelAccount, createKernelReads, createKernelRuntime, kernelAccountDeployment,
kernelKey, ownerOperator,
} from "@oaath/sdk/kernel";
const reads = createKernelReads(publicClient);
// Detects the account's Kernel and EntryPoint versions onchain.
const existing = await bindKernelAccount({ chainId, address: existingKernelAddress, reads });
const runtime = createKernelRuntime({
deployment: kernelAccountDeployment(existing),
operator: ownerOperator({ key: kernelKey({ wallet: walletClient, validator: ecdsaValidator }) }),
reads,
});
const account = await runtime.bindAccount({ address: existingKernelAddress });
const prepared = runtime.prepareOperation({
kind: "execution", grantId: operationContextId, account,
nonceKey: "0", sequence, calls, gas,
});
const signature = await runtime.signOperation(prepared);walletClient is a connected wallet client with an account. A wallet key
requests one personal_sign signature over the exact 32-byte operation digest
and verifies the EIP-191 signature against that captured account locally. It
does not request accounts or retry a rejected signature. ecdsaValidator is
the account's root ECDSA validator module; it must support EIP-191, as the
reviewed ECDSA validator does. Passing deployment to bindKernelAccount, or
composing the runtime over another deployment, makes a mismatching account fail
with kernel_runtime_deployment_mismatch before anything is signed. Local accounts
using raw-hash signing can pass kernelKey({ account, validator }) instead. On
Kernel 0.4.0, a P-256 kernelKey({ credential, sign }) also binds an existing
account whose root validator is the pinned raw P-256 validator, whose stored
public key is readable onchain. A WebAuthn kernelKey({ credential, credentialId,
rpId, origin, authenticate }) binds the reviewed WebAuthn validator at
0x6f781fff97b830daa2e11ee0ad6344aff7131ef2. Binding checks its exact runtime code
and stored x/y before requesting an assertion. The credential ID, RP ID and origin
remain local assertion checks; the contract stores only x/y. The module artifact
is reproduced from kernel-7579-plugins commit a267d794; availability on a public
chain is not implied. Other root validators fail with kernel_runtime_binding_mismatch.
After binding, the same key approves approveKernelPermission; the owner runtime
signs revocations prepared with encodeKernelPermissionUninstallCalls from
@oaath/protocol, as well as ordinary owner operations.
Weighted Kernel v4 roots use the reviewed local WeightedECDSAValidatorV09 and
WeightedECDSASigner from plugin commit a267d794:
const key = kernelKey({
kind: "weighted-ecdsa",
guardians: [{ address: alice.address, weight: 1 }, { address: bob.address, weight: 2 },
{ address: carol.address, weight: 1 }],
threshold: 3,
signers: [alice, bob], // Digest-signing accounts or caller-owned signing capabilities.
});
const runtime = createKernelRuntime({ deployment, operator: ownerOperator({ key }), reads });
const account = await runtime.bindAccount({ address });The complete guardian configuration is separate from the participating quorum.
There may be 1–32 distinct guardians; weights and threshold are positive uint24s.
Signers are captured and sorted once, and must meet threshold before any signature
is requested. Plain key.sign(hash) collects the sorted ERC-1271 quorum used by
approveKernelPermission. runtime.signOperation(prepared) instead collects the
contract's proposal/final signatures, binding the full operation and configuration
epoch. Bind again after reload or a configuration change. An unreadable or changed
configuration fails before requesting signatures; no signing path submits or retries.
The same key works through sessionOperator; an enable operation may use a proven
absent signer configuration only when its packages install that exact key.
Supply gas for the full quorum: the estimation placeholder contains only a final
signature, avoiding the contract's non-guardian proposal revert. The module pins
identify reproducible CREATE2 inputs, not public-chain deployment evidence. These
modules require Kernel v4 / EntryPoint 0.9. Weighted profiles are Kernel API keys;
the protocol's Grant credential schema remains ECDSA, P-256 and WebAuthn.
sequence is the current EntryPoint nonce sequence for this account and key;
gas contains canonical decimal strings. The low-level prepared-operation
schema calls its context label grantId; no Grant is created or needed here.
Preparation and signing do not submit. The caller must retain the prepared
operation and submission evidence using its chosen transport. An unavailable
read returns kernel_runtime_read_unavailable; it never creates an account or
selects a different Kernel version.
The lower-level runtime also supports ECDSA sessions on existing accounts. No Kernel version is named; the approval follows the account's detected deployment:
import {
approveKernelPermission, bindKernelAccount, createKernelReads, createKernelRuntime,
kernelAccountDeployment, kernelKey, kernelPermissionNonce, materializeKernelPermission,
sessionOperator,
} from "@oaath/sdk/kernel";
const reads = createKernelReads(publicClient);
const existing = await bindKernelAccount({ chainId, address: existingKernelAddress, reads });
const runtime = createKernelRuntime({
deployment: kernelAccountDeployment(existing), reads,
operator: sessionOperator({
key: kernelKey({ account: sessionKey, validator: ecdsaValidator }),
policies: [{ kind: "call", permissions: [{ target, selector, valueLimit: "0" }] }],
}),
});
const account = await runtime.bindAccount({ address: existingKernelAddress });
const approval = await approveKernelPermission({
runtime, account,
owner: kernelKey({ wallet: walletClient, validator: ecdsaValidator }),
nonce: await kernelPermissionNonce({ runtime, account, reads, requestHash }),
});
const { prepared, signature } = await materializeKernelPermission({
runtime, account, approval, grantId: operationContextId,
nonceKey: "0", sequence, calls, gas,
});kernelPermissionEnableTypedData({ runtime, account, nonce }) returns the exact
EIP-712 value for a wallet's signTypedData prompt instead. On a Kernel 0.3.3
account the approval binds the account, effective validation nonce and exact
permission on every chain; requestHash is used only by Kernel 0.4.0, where it
selects a fresh install key. Store the approval using its versioned
representation and restore with parseKernelPermissionApproval. Each destination must have the same effective
validation nonce; stale or mismatched state rejects rather than requesting another
signature silently. When Kernel 0.3.3 chains differ, take the highest
kernelPermissionNonce as the target and, on each lower chain, execute
kernelPermissionNonceAlignmentCalls({ runtime, account, reads, nonce }) as one
owner operation. It installs and removes a throwaway permission that never
validates, never raises validNonceFrom, and leaves installed permissions
working; approvals signed for other permissions but not yet enabled on that
chain need a new nonce. The first operation enables and executes together; after confirmed installation, use the
same runtime's prepareOperation and signOperation in standard mode.
encodeKernelNonceKey({ deployment, mode, validation, nonceKey }) (from
@oaath/sdk/advanced) derives the EntryPoint key for each mode; read that
key's sequence before preparing. Enable and standard mode have distinct keys.
Kernel v3.3's replayable enable uses an EIP-712 domain with chainId: 0 and a
chain-zero session signing digest.
kernelOperationSigningHash({ deployment, operation }) (also on /advanced)
returns the digest for an external session signer; encodeVerifiedSignature verifies that
digest. The stored prepared operation always retains its actual chain and
EntryPoint hash. Installed sessions and owner operations sign that actual hash.
The Monad enable gas floor applies before hashing or signing. Missing signer or
policy deployments prevent binding. These primitives prepare and sign only;
submission journaling and observation remain the caller's responsibility when
using them directly. A missing receipt never authorizes another send.
The approval schema is oaath.kernel.v33-permission-approval/v1; earlier
chain-bound approval records are rejected and must be recreated.
Custom issuer configurations can execute a v3.3 Grant using an account profile
with version oaath.kernel-existing-account-profile/v1, kernelVersion: "0.3.3",
the existing address, EntryPoint version 0.7 (Kernel 0.3.3) or 0.9 (Kernel v4), and its current ECDSA
ownerCredential. The issuer supplies a v3.3 approval beside the permission
decision and binds it with kernelPermissionCapabilityHash(approval). The permission
packages must be derived from the exact approved policy and session credential.
grant.sendCalls then enables on first use and uses the installed session on
later calls, at the same address. It journals before signing; resume and
getOperation recover the original operation without resubmission after reload.
With the owner signer available, grant.revoke() removes installed permissions
and consumes unused approvals on the configured chains. An unused approval is
installed and removed atomically without application calls or global nonce
invalidation. Completion requires finalized permission absence and a consumed
enable nonce. An uncertain revocation remains revoking across reload and is
observed again without resubmission. Other permissions remain usable.
V3.3 external prepared-call signing and request-time validity attenuation are not
supported yet.
Chain ports
createOAAth builds these default ports from plain chains descriptors
(OaathChainDescriptor) with the default request budget. To set the budget,
retries, or fetch, or to reuse the ports elsewhere, build them explicitly and
pass the array as chains; any OaathChainCapability[] is accepted the same
way as a custom override:
import { createCetaneChainPorts } from "@oaath/sdk/cetane";
const chains = createCetaneChainPorts({
480: {
publicRpcUrls: [publicRpcUrl, backupPublicRpcUrl],
bundlerUrl,
paymasterUrl, // optional registered ERC-7677 service
},
}, {
retry: { attempts: 3 },
timeoutMs: 10_000,
maxRequests: 1_000,
maxConcurrency: 4,
});Account reads, nonce, fees, finalized usage, and transaction/block evidence use
the public pool. Each endpoint must report the configured chain. The bundler
receives only ERC-4337 discovery, estimation, submission, and operation-receipt
calls; paymaster methods go only to paymasterUrl. Usage comes from the pinned
RateLimitPolicy at an exact finalized canonical block. An unavailable or absent
policy contract is not zero usage. RPCs must support finalized and EIP-1898
block-hash reads. The default quote uses nonce namespace zero and Cetane's fee
estimation. Existing gas-floor configuration can be supplied as gas per chain.
Read failures such as HTTP 429/5xx, invalid JSON, and timeouts retry within the configured attempt count and fail over across public endpoints. Defaults are three attempts, 100 ms between attempts, and a 10-second deadline per request. Submissions, estimates, and paymaster stages make one attempt. A send timeout or ambiguous response remains uncertain and is never resubmitted. The default route uses the bundler, with no EOA fee payer or replacement-transaction indexer.
Identical concurrent reads within one pool share a request and its bounded retry sequence. Concurrent endpoint chain checks also share a request. Entries are discarded on success or failure: sequential reads, including canonicality rechecks, always fetch fresh evidence. Sends, estimates, and paymaster requests are never combined. Pools with different chains, endpoints, or headers remain independent.
The shared lifetime budget counts actual wire requests, including chain checks
and retries across all configured chains; exhaustion throws OaathRpcError with
oaath_rpc_budget_exhausted. Identical in-flight reads share one concurrency slot.
Concurrency above the limit fails with oaath_rpc_concurrency_exceeded rather
than queueing. Recreating ports explicitly starts a new budget; recover existing
operations for observation instead of repeating sends. Errors omit URLs,
provider prose, and request bodies. An optional fetch(Request) can supply an
application transport or local test fixture. Construction performs no I/O.
With paymasterUrl, the chain gets sponsorship: { kind: "erc7677", url },
whose public service identity omits the query and trailing slash. Use that
identity in a requested paymasterService capability or a call's payer.url;
transport still calls the exact configured endpoint, including its query
parameters. A chain capability sets at most one optional sponsorship: either
{ kind: "erc7677", url, request, estimate } or { kind: "erc7902-static",
configurationHash }, where the hash comes from
hashErc7902StaticPaymasterConfiguration. Omit it for no sponsorship.
With relayPaysGas: true, the chain's bundler pays chain gas itself (for
example bundle_rs in its default
fast mode): every quoted operation carries maxFeePerGas = 0 and
maxPriorityFeePerGas = 0, so the account needs no balance and no paymaster.
Gas limits still come from the bundler's eth_estimateUserOperationGas. It
requires bundlerUrl, and setting it together with paymasterUrl throws
oaath_rpc_config_invalid.
bundlerHeaders (for example { "x-api-key": key }) are sent only to the
chain's bundlerUrl, never to read or paymaster URLs, and headers are sent
only to read URLs. Both are captured at construction; values that are not
strings or that contain CR/LF throw oaath_rpc_config_invalid.
grant.sendCalls({ chain, calls }) starts a new operation and returns its handle
without waiting for inclusion. Retain { chain: operation.chainId, id: operation.id }
with the application's job. An unresolved operation occupies that grant/chain
lane, so another send fails with oaath_client_state_conflict.
grant.reviewCalls({ chain, calls }) returns immutable current execution facts:
the public grant/account identities, captured calls, session signer, selected
submission route, policy validity window, operation limit, and actual onchain
enforcement. It checks the same scope, runtime, and account evidence as sending,
without quoting a nonce, signing, submitting, or writing Grant/Operation state.
Failures use OaathClientError codes. An unreadable bundler is reported in
reasons and stays on the bundler route; it never authorizes fallback. Review
is a snapshot, not a reservation or authorization: sending rechecks current
state, and applications should review again after relevant facts change.
Grant and owner reviews share one versioned contract,
version: OAATH_CALLS_REVIEW_VERSION (oaath-calls-review-v1). Its semantic
fields are closed enums: signer, enforcement, validation, and
fallback.condition / fallback.feePayer. A new value there is a new version.
Its identity fields are opaque, bounded strings: account.implementation (for
example kernel:0.3.3), route and fallback.route. A new Kernel version or
transport adds a value without changing version. Validate a review at your
trust boundary with parseOaathCallsReview(review). It returns only the
contract fields. Any other version fails with
oaath_client_review_version_unsupported, and malformed or contradictory
fields fail with oaath_client_input_invalid. Check the semantic fields and
fingerprint the identity fields; do not enumerate them.
grant.reviewCalls({ chain, calls, estimate: true }) also estimates the exact
session operation and returns validation: "estimated" | "account-rejected".
The default is "not-estimated". Estimation writes no operation or permission
installation state and performs no signing or submission. "account-rejected"
requires a canonical EntryPoint account-validation rejection from the estimation
RPC; arbitrary error text, signature rejection, malformed responses and timeouts
cannot produce it. Other failures throw a structured client error. This option
currently requires the session signer, an unsponsored bundler route and no
installation in progress. If signer: "auto" selects the owner, estimate: true
returns session_estimation_unavailable without estimating or changing signers.
An application may offer owner execution after "account-rejected", but must
review that signer choice before sending. This result never permits resending an
operation that was already submitted or whose acceptance is uncertain.
After reconnecting and resuming the grant, grant.getOperation({ chain, id })
recovers that exact execution from local history, including terminal records
after a later operation replaces the lane. Lookup and observation do not quote,
sign, or submit, and work after grant expiry or revocation. null means no
matching retained record, never permission to retry a send.
operation.execution() reobserves that exact operation and returns immutable
finalized grant ID, sender, ordered calls, transaction/block identity, and success or
revert outcome. It checks the receipt and derives calls from the containing
EntryPoint v0.7/v0.9 transaction by recomputing the operation hash, then decoding the
supported atomic Kernel execution. These are top-level requested calls; a
reverted outcome means their effects did not persist. Pending, dropped,
unreadable, mismatched, and unsupported evidence fails with a structured
oaath_client_observation_unavailable error. It never signs or sends.
Its route is the retained transport acknowledgement (erc4337-bundler or
erc4337-handleops), or null when the adapter did not report it or
observation won the acknowledgement race. A direct route is reported only when
its acknowledged transaction hash matches the verified inclusion transaction.
The acknowledgement alone never proves inclusion, finality, or permission to
resubmit. Custom submission sessions may return
{ userOperationHash, submission: { route: "erc4337-bundler", transactionHash: null } }
or submission: { route: "erc4337-handleops", transactionHash }; omit
submission when the route is unknown.
For direct acknowledgements, default Cetane observation reads the retained
transaction from the public RPC and locates the exact EntryPoint event. Recovery
needs neither a wallet nor a bundler index. The observer still verifies the
receipt, transaction, canonical blocks, and finality; a missing event or a failed
outer transaction leaves the operation unresolved and never authorizes a send.
Finality uses the configured RPC's finalized tag and canonical blocks by
number. After verifying inclusion, the observer requires the finalized height
to cover it, rereads the canonical inclusion hash, and rebinds the finalized
anchor by number. This takes a fixed number of reads regardless of receipt age;
it does not walk every intervening parent. These are RPC-attested chain facts,
not local consensus verification. A missing finalized tag, changed hash, wrong
chain or head behind inclusion remains unresolved. Custom capabilities must
provide canonical-by-number evidence, never a block merely located by hash.
See the Ethereum RPC block semantics.
Custom observation adapters receive an optional
transaction: { hash, entryPoint } hint on user_operation_receipt. Receipt and
execution projection also pass the verified inclusion transaction as a hint.
Applications with their own operation journal can verify a saved public UserOperation reference without constructing an OAAth Grant or Operation:
import { createUserOperationObserver } from "@oaath/sdk/advanced";
import { createCetaneChainPorts } from "@oaath/sdk/cetane";
const [port] = createCetaneChainPorts({
[chainId]: { publicRpcUrls: [rpcUrl], bundlerUrl },
}, { retry: { attempts: 1 }, timeoutMs: 8_000, maxRequests: 96 });
const observer = createUserOperationObserver(port.observation);
try {
const result = await observer.observeReference({
// Exact fields; lowercase addresses/hash and a canonical decimal nonce.
reference: { chainId, entryPoint, account, nonce, userOperationHash },
observedAt: Date.now(),
timeoutMs: 25_000,
// transactionHash: savedDirectTransactionHash, when already known
});
// result.status: pending | unreadable | finalized
} finally {
await observer.close();
}parseUserOperationReference from @oaath/protocol captures the same immutable
identity at an application's input boundary. Observation verifies the exact
EntryPoint event, sender, nonce, hash, containing transaction, canonical block
and finality through the same pipeline as OAAth operation recovery. An
unreadable result may retain a verified receipt when finality is unproven;
only finalized proves finality. Receipt logs are scoped to that operation and
include its terminal UserOperationEvent. This reader neither derives executed
calls nor verifies application postconditions. It owns no journal, never checks
for replacements, and cannot authorize retries or release an application lane.
Recreate it after reload using the saved reference; close() drains active
bounded observations and closes the supplied capability. A missing or unreadable
receipt leaves the saved identity unresolved.
Operation records now use oaath.operation/v1. Older records are rejected;
IndexedDB schema 16 recreates older local state without migration. This pre-1.0
reset deletes retained keys, Grants, and operation history, so applications must
reconnect and authorize fresh permissions. It does not revoke onchain authority.
Plain Grant and owner calls take one optional payer setting that says who pays
gas. Omit it and the account pays through the chain's configured routes. A
connected EOA can pay as a fallback:
const request = {
chain,
calls,
payer: { kind: "connected-eoa", wallet: walletClient },
};
const review = await grant.reviewCalls(request); // initial route plus conditional fallback
const operation = await grant.sendCalls(request);The initial route stays erc4337-bundler. A closed pre-acceptance rejection from the
default RPC transport allows one wallet eth_sendTransaction carrying the exact
same signed operation through EntryPoint handleOps. The wallet must already be
connected to the requested chain and expose the captured EOA through
eth_accounts; the SDK neither connects nor switches it. The wallet approves
and funds the outer transaction. No second operation signature is requested.
Review reports the conditional fallback address without contacting the wallet.
Acceptance, timeouts, HTTP 502 responses, unknown RPC codes, and malformed
results never trigger fallback. HTTP status alone never proves rejection.
A late rejection after the submission
session closes cannot start it either. Wallet rejection or a lost response never
retries the transaction. Retain the operation ID and observe it. This option
cannot be combined with paymaster sponsorship, which remains on the bundler
route. Custom direct transports must preserve OaathRpcError conclusive
rejections from @oaath/sdk/cetane. The relay forwards their closed rejection
evidence only from submission failures. Generic relay errors grant no fallback.
Custom observation transports answer transaction_execution with exactly
{ hash, to, blockNumber, blockHash, input } from the requested chain's
transaction. Addresses, hashes, and input are lowercase hex; blockNumber is a
canonical RPC hex quantity. The SDK reads input transiently and never returns
or persists its signatures. Existing operation state owns identity and finality;
this method adds no durable execution artifact.
Custom deployment quotes receive the selected Kernel mode and validation.
Choose a nonce namespace, encode its EntryPoint key with @oaath/sdk/advanced's
encodeKernelNonceKey({ deployment, mode, validation, nonceKey }),
and read that key's sequence. The root, enable, and standard permission paths
have separate nonce domains; the SDK supplies the authority and the deployment
supplies its current chain sequence and gas.
Quotes also receive simulation.prepared and simulation.signature, built by
the same runtime as the final operation. They include the bound factory data,
call encoding, paymaster selection, and complete enable envelope when needed.
The simulation starts with nonce namespace and sequence zero, zero gas and fees,
and any applicable enable gas floor. For purpose: "estimate", read the actual
nonce and fees before estimating. For "sponsorship", read only the nonce and
fees: ERC-7677 estimates after obtaining paymaster stub data. For "revalidate",
read only the nonce and keep the simulation's retained gas, fees, and paymaster
bytes. Return the selected namespace, sequence, and gas. Never submit or persist
this simulation. Its signature can contain the retained owner approval, so never
log it.
Usage ports receive grantId, chainId, account, permissionId, and
maximumOperations before quoting. Read finalized usage for that exact account
and permission; an unavailable read must remain unavailable, never a zero count.
Configure an enable gas floor on each chain capability when needed:
createOAAth({
// Other existing client options...
chains: [{ ...monadPorts, gas: { enableVerificationGasFloor: 2_000_000n } }],
});Monad (143) defaults to 2_000_000n; other chains default to zero. An explicit
nonnegative uint120 bigint overrides that default. The first session-enable
operation uses the greater of the quoted verification gas and this floor.
Owner operations and installed-session operations keep their quoted gas.
grant.reviewCalls() reports enableVerificationGasFloor as a decimal string,
or null when no floor applies, without quoting or reserving a nonce.
The lower-level createKernelRuntime accepts the same gas option.
ERC-7677 sponsorship applies the floor before requesting final paymaster data. Both Grant and owner calls accept an explicit registered service as the payer:
const request = { chain, calls, payer: {
kind: "paymaster-service", url: registeredPaymasterUrl,
context: { policyId: "application-policy" },
} };
await grant.reviewCalls(request); // reports the selected URL; no sponsorship request
const operation = await grant.sendCalls(request); // owner.sendCalls accepts the same selectionThe URL must exactly match that chain's registered service. The SDK requests stub
data, estimates, and obtains final data once each before signing the final
operation. An invalid or unavailable sponsor fails the request; it never selects
an unsponsored send. Sponsorship requires the bundler route. reviewCalls
reports paymasterService: { url } or null and does not contact the sponsor.
Custom sponsorship adapters receive verificationGasFloor with their prepared
candidate and must honor it before authorizing the final gas. A reply below the
floor is rejected before signing; its authorized fields are never changed after
the paymaster response. This policy does not retry failed validation or submission.
When a bundler supplies ABI-encoded EntryPoint FailedOpWithRevert data for
AA23 reverted with an empty inner revert, preparation errors and uncertain
submission outcomes expose diagnostic.kind = "validation_gas_likely_insufficient"
and the attempted verificationGasLimit as a decimal string. The message says
"likely validation out-of-gas"; it does not establish the cause. Provider RPC
errors preserve the fixed numeric message and expose the hint in data.diagnostic.
Message-only errors and nonempty reverts remain generic. The diagnostic is
ephemeral, does not authorize a retry, and does not release an unresolved lane.
@oaath/sdk/kernel exposes prepareKernelPermissionApproval for an owner
approval of a canonical permission request. It binds the request's account
using public credentials and configured reads, derives its policy packages
through createKernelRuntime, and returns the Kernel signing request.
sign(ownerKey, decidedAt) takes one signature from a key profile, wherever
that key lives, and returns the permission decision plus install approval
consumed by the browser client. The caller owns
any device transport; the helper does not submit or persist anything. It
supports an existing Kernel 0.3.3 or 0.4.0 account whose root owner is
proven onchain, a factory-derived Kernel 0.4.0 account whose single root is
its ECDSA (through ECDSA_VALIDATOR), raw P-256, or WebAuthn owner, and the
current ECDSA/WebAuthn operator profiles. reads must serve
every supported deployment (createKernelReads). Any other request fails with
kernel_runtime_unsupported before signing. Wallet-approved mode prepares its approval through the same owner.
Approval preparation derives its install nonce from
hashPermissionRequest(request), as kernelPermissionNonce does: the first 192
hash bits select a request-specific Kernel install key at sequence zero. The
same request recreates the same signing packet, while different requests can
install in different orders across chains. This requires an unused key and
Kernel's global validNonceFrom() to remain zero on each destination chain;
accounts with an advanced global minimum require separate reconciliation.
The install nonce is separate from the EntryPoint operation nonce above.
For an untouched chain, @oaath/sdk/advanced's
encodeKernelInstallNonceInvalidationCall({ account,
installNonce }) encodes an owner self-call that advances that approval's key
to the next sequence. encodeKernelInstallNonceRead({ key }) encodes the
account's nonce(uint192) read for checking its effective sequence. An already
consumed or invalidated nonce requires observation; repeating the self-call can
revert because Kernel requires an increase. Installed permissions still need
uninstall calls. These codecs do not submit, establish finality, or complete
configured-chain revocation.
prepareKernelPermissionRevocation prepares one Kernel 0.3.3 owner
revocation; a Kernel 0.4.0 approval fails with kernel_runtime_unsupported
and revokes through its grant handle. Supply the chain, root operation nonce and
gas. An optional paymaster (EntryPoint address, verificationGasLimit,
postOpGasLimit, data) sponsors the operation; it defaults to null
(self-funded) and is part of the hashed identity that restore reproduces.
Retain the JSON preparation before requesting owner consent and recreate it with
restoreKernelPermissionRevocation({ preparation, reads }). sign(ownerKey)
returns the signature for that operation. Preparation and signing never submit
or prove revocation finished.
The shared revocation call codecs are owned by @oaath/protocol and re-exported
through @oaath/sdk/kernel; invalid input reports signing_request_invalid.
The headless Grant provider returns 4200 for wallet_showCallsStatus unless
the adopter supplies a wallet-owned status presenter. Executable
wallet_sendCalls entries without to are valid contract-creation requests,
but OAAth does not own a creation policy and refuses them with its fixed
provider execution error (-32000).
The Draft ERC-7836 prepared-call profile accepts only the approved operator's
external signature. secp256k1 supports frontend or application_backend
custody; webauthn-p256 supports frontend custody only. oaath_hosted is
rejected before preparation.
Its opaque five-minute context is current-version-only, durable, and consumed once. A recreated IndexedDB realm resumes the retained prepared operation and any ambiguous send without preparing, signing, or submitting another operation; older, unreadable, stale, and already-consumed contexts do not authorize a new operation.
Owner reviewCalls estimates the complete call list as one UserOperation and
returns capacity: { kind: "single-operation", detail }. detail is
transport-specific and outside the review contract. Estimation includes any
explicitly selected sponsorship. It prompts and submits nothing, does not
reserve an operation slot, and fails when capacity cannot be established.
sendCalls obtains a fresh quote through the existing operation journal; a
review estimate is not an inclusion guarantee.
The SDK runtime uses the exact Cetane tarball pinned in this repository.
@oaath/sdk/cetane exports createCetaneChainPorts and the Grant-backed EIP-1193 provider.
There is no @oaath/sdk/viem compatibility entry. Run bun run smoke:cetane
to verify the packed SDK without viem installed. The existing operation runner
continues to own submission, observation, recovery and lane occupancy.
Wallet RPC standards
The wallet-RPC surface intentionally distinguishes finalized standards from experiments:
| Surface | Standards status | OAAth status |
| --- | --- | --- |
| EIP-5792 wallet_sendCalls / status / capabilities | Final (2.0.0) | Implemented PoC path |
| ERC-7836 wallet_prepareCalls / wallet_sendPreparedCalls | Draft (1) | Experimental OAAth profile; approved secp256k1 external signer in frontend or application_backend custody, or approved WebAuthn external signer in frontend custody; current-version opaque five-minute context, one-time durable consumption and reload recovery; oaath_hosted custody is rejected |
| ERC-7677 paymasterService | Review | Experimental wallet_sendCalls and prepared-call paths for a deployment-registered same-service proxy and bundler estimator |
| ERC-7902 staticPaymasterConfiguration | Draft | Experimental bundled wallet_sendCalls path for one authenticated per-chain configuration commitment |
| ERC-7902 validityTimeRange | Draft | Experimental wallet_sendCalls and prepared-call paths only with a configured transaction confirmer and proof of the configured chain's pinned OAAth validity-policy runtime; validAfter and validUntil are inclusive |
The Draft profiles are not advertised as stable or as generic conformance.
ERC-7902 multiDimensionalNonce, AA gas parameter overrides, and
eip7702Auth are explicitly unsupported and deferred.
Kernel runtime
The Grant workflow uses Kernel v4 UUPS (0.4.0) through EntryPoint 0.9.
Existing ECDSA-root Kernel 0.3.3 accounts support
createOAAth({ chains, account }).account(address).owner(walletClient).sendCalls(...).
It prompts once, creates no Grant, and uses the existing address with no enable
approval. IndexedDB retains exact operations for wallet-free getOperation
recovery. See Owner operations, including the lower-level
createKernelRuntime path. Existing v3.3 accounts also support session Grants through
createOAAth({ chains, account, approvals: { kind: "wallet", owner: walletClient } }),
with one wallet typed-data approval, browser custody and reload recovery.
Wallet approvals need no portal or relay. Explicit Grant signer: "auto" prefers an
available owner for the atomic call bundle; execution review identifies that
choice and its wider authority before signing.
The v4 runtime pins Kernel PR #152,
merged at c960b42d2ed4adb0d5328f6e762962debdf8e57a. The upstream PR identifies
its production sources and compiler configuration as identical to the audited
revision; the audit report's publication remains upstream work. OAAth builds
that source with its pinned Solidity 0.8.33 profile and EntryPoint 0.9
constructor binding. Local integration tests prove that EntryPoint path.
Validator install data now contains only packed selectors; signer install data
contains the permission ID followed by selectors. The old inline hook encoder
argument and generic module type 4 are rejected; scoped execution hooks use
module type 11. The constructor, runtime artifacts and CREATE2 addresses change.
Prior v4 deployments and grants are unsupported and require fresh setup; existing
Kernel 0.3.3 accounts keep their separate deployment profile.
The deployment profile has the same CREATE2 addresses on every chain.
bindKernelAccount checks the factory runtime hash and the factory's
implementation binding. Every chain requires code at the canonical EntryPoint and implementation
address; Kernel's chain-dependent runtime hash is not checked. There is no
per-chain implementation hash table. These checks do not claim that the new
contracts have already been deployed on any public chain.
To reproduce the retained artifacts from a clean checkout of the pinned Kernel revision (including its committed dependencies):
bun run --filter @oaath/contracts kernel:check /path/to/kernel
# Update artifacts after an intentional pin change:
# bun run --filter @oaath/contracts kernel:generate /path/to/kernelCheck the runtime before integrating a chain:
npx @oaath/cli doctor --chain 143
npx @oaath/cli doctor --chain 143 --rpc https://rpc.monad.xyz --json
npx @oaath/cli deploy-runtime --chain 143 --rpc https://rpc.monad.xyz --dry-run@oaath/cli installs the oaath command and is part of the fixed package
release group. From a repository checkout, run bun run --filter @oaath/cli build
then node packages/cli/dist/cli.mjs doctor --chain 143.
See CLI usage for bounds, exit codes and evidence limits.
doctor checks the ECDSA session module set; the owner validator remains
application-selected. It sends no transactions and never treats an unreadable
RPC response as a missing contract.
deploy-runtime checks EntryPoint and the singleton deployer, deploys only the
missing deterministic core set, and retains an attempt journal before broadcast.
See the CLI instructions for the funded-wallet environment variable and recovery;
an uncertain transaction is observed, never automatically resent.
Public runtime readiness has not been rechecked for this contract revision.
The previous six-chain snapshot described the retired artifacts. Run doctor
for fresh evidence; production deployment writes remain deferred.
@oaath/sdk owns the native Kernel v4 Install[], validation nonce,
enable-signature, UUPS factory, and ERC-7579 execution encodings. The current
EntryPoint 0.9 factory is 0x3d6d678742e276b6388fd06c1b8ecd19e2d64c2d; its
UUPS implementation is 0x6250926dd0309d9deaaeb4a2c413da5f3c4de37a.
Credential kinds are pluggable through one interface. kernelKey({ kind?, ... })
returns the reviewed ECDSA, P-256 or WebAuthn KeyProfile, choosing the signing
source from the input it is given, and a consumer implements the same interface
to add a kind: { kind: "custom:<slug>", publicMaterial,
resolveValidator, signerModule, dummySignature, sign, verify } composes through
ownerOperator and sessionOperator into the one createKernelRuntime, with no
credential-specific runtime. A custom kind resolves no pinned module, so it binds
its own ERC-7579 validator and permission signer module (moduleType 6). Both are
proven to carry code on the action chain when this runtime binds the account —
before the account address depends on them. The permission ID is derived locally
before any chain read, and a descriptor bound by a different runtime skips this
runtime's code proof; either way a codeless module fails closed at Kernel's
on-chain validation rather than granting anything. Sessions stay permission-scoped: at least one
policy is required for every kind. A produced signature must verify against the
profile's own bound public material before it is wrapped in any authority
envelope, and a reviewed kind may never bind its own signer module.
A raw P-256 credential — an Apple Secure Enclave key, for instance — holds root
owner authority through a pinned reviewed validator module, and its session keys
are ECDSA because no reviewed raw P-256 permission signer exists. That validator
verifies through the RIP-7212 / EIP-7951 precompile and has no Solidity fallback,
so it can only be deployed on a chain that carries the precompile; on a chain that
does not, the pinned address holds no code and bindAccount fails closed with
kernel_runtime_validator_unavailable before any account address depends on it.
All-chain authority
chainScope: "all" is one owner approval, not one approval per chain. Every
module and account address in the runtime is CREATE2-derived, so one set of
initial packages yields one account address on every supported chain. The owner
signs Kernel v4's replayable enable digest once — a digest whose EIP-712 domain
omits the chain id and binds only the account, Kernel's install nonce and the
exact install packages — and materializeKernelPermission spends that one
signature on each chain the session first touches, including a chain that was not
configured when the owner approved. The session's first operation on a chain
carries the enable envelope; every later one is an ordinary standard-mode
operation against the installed permission.
Approval preparation selects a request-specific install nonce namespace,
so different grants can install in different orders on different chains. The
SDK starts each namespace at sequence zero: its key must be unused and Kernel's
global validNonceFrom() must still be zero on the destination chain. Advancing
that global minimum requires separate account reconciliation.
Authority is all-chain; evidence is not. The account state, Kernel's install nonce, the EntryPoint nonce, the operation identity, the submission route, and inclusion, finality and revocation evidence all stay chain-local, and no chain borrows another's. There is no global atomic install, execute, or revoke.
Revocation snapshots the client's configured chains and any previously bound
chains. revoked requires finalized permission absence and a consumed approval
install nonce on every chain in that snapshot, including unused chains. Relay
invalidation stops service admission; it does not invalidate the owner signature
onchain. A missing chain transport leaves the Grant revoking, even after reload.
A realm without the owner's signer leaves the Grant revoking until the owner
removes the permission onchain; a later revoke() observes that and completes.
Chains outside this snapshot are not covered by its revocation status.
Account descriptors are process-local evidence handles. After a process reload,
call bindKernelAccount again before preparing another operation; serialized
or copied descriptors are deliberately rejected. A descriptor also freezes the
account state observed at bind time: after a counterfactual account's first
operation deploys it, rebind before preparing the next operation, or EntryPoint
rejects the stale factory evidence (AA10 sender already constructed).
Gas values in the low-level Kernel helpers are caller-supplied decimal strings;
bring them from your own estimation source. createKernelReads adapts any viem-style public client into the account
read capability for every supported deployment, and asViemUserOperation maps a prepared operation into viem's
shape for signing and submission.
