Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d1c1004
feat(core): let the backend decide whether a request is served
mariano-aguero Sep 8, 2026
3136e62
feat(core): let keys name the dApp a call acts for
mariano-aguero Sep 8, 2026
527abb4
feat(core): carry the calling dApp on the bundler transport too
mariano-aguero Sep 8, 2026
cc53971
fix(core): stop the dApp header hanging off the production host
mariano-aguero Sep 8, 2026
87f0c65
fix(core): name the calling dApp on the userOp path too
mariano-aguero Sep 9, 2026
300016b
fix(core): name the calling dApp on relay calls too
mariano-aguero Sep 9, 2026
79873ed
fix(core): keep the rpc error envelope on a failing status
mariano-aguero Sep 9, 2026
b51bb62
fix(core): refuse a keyless app-specific config when it is written
mariano-aguero Sep 9, 2026
8421771
fix(core): tie the dApp origin to the request being served
mariano-aguero Sep 9, 2026
912c35d
fix(core): keep the dApp header off routes that do not need it
mariano-aguero Sep 9, 2026
74c0b05
fix(core): treat a body that is not an rpc response as a failure
mariano-aguero Sep 9, 2026
afd819a
fix(core): name the calling dApp on any backend of ours
mariano-aguero Sep 14, 2026
fed2a72
feat(core): report analytics without an api key
mariano-aguero Sep 14, 2026
56b2e99
Merge remote-tracking branch 'origin/main' into feat/core-optional-ap…
mariano-aguero Sep 14, 2026
84f2e5c
feat(core): report receipts without an api key
mariano-aguero Sep 16, 2026
7185751
fix(ui): resolve chain icon and addresses without a key
mariano-aguero Sep 16, 2026
0ab142a
test(keys): dispatch storage events without the jsdom IDL check
mariano-aguero Sep 16, 2026
e861410
feat(core): drop the api key gate on receipt reporting
mariano-aguero Sep 16, 2026
d85f6d6
perf(core): run one receipt waiter per user operation
mariano-aguero Sep 16, 2026
ecb65fd
test(core): cover receipt reporting on the direct send path
mariano-aguero Sep 16, 2026
c8bbfcd
fix(ui): retry a chain icon lookup that failed
mariano-aguero Sep 16, 2026
06b7ecc
perf(ui): stop retrying a backfill that resolved nothing
mariano-aguero Sep 16, 2026
689294a
test(keys): cover the storage area and clear guards
mariano-aguero Sep 16, 2026
d8b0e4a
feat(core): make the account api key optional
mariano-aguero Sep 16, 2026
02cb6e0
fix(ui): drop a backfill memo that misread failures
mariano-aguero Sep 16, 2026
78a1cdb
docs: make the api key optional in cross-platform mode
mariano-aguero Sep 16, 2026
c92f1c5
docs: name the origin registry as what serves a keyless call
mariano-aguero Sep 16, 2026
cee4d99
refactor(ui): keep one cache for chain capabilities (#342)
mariano-aguero Sep 16, 2026
cd4fb24
fix(core): reject a 2xx body that carries no rpc result
mariano-aguero Sep 17, 2026
376fce6
fix(core): match our backend by origin, not by prefix
mariano-aguero Sep 17, 2026
b192136
perf(core): hold a failed capabilities lookup briefly
mariano-aguero Sep 17, 2026
753bb36
fix(ui): drop the old chain icon while the next loads
mariano-aguero Sep 17, 2026
c9ac459
refactor(core): move the dapp origin setter off the public api
mariano-aguero Sep 17, 2026
2ba3640
fix(core): keep the internal entry point on one store
mariano-aguero Sep 17, 2026
a24df55
refactor(core): parse our own origins once
mariano-aguero Sep 17, 2026
92499ae
refactor(core): drop a delete that cannot be observed
mariano-aguero Sep 17, 2026
5caf04d
refactor(keys): keep one name for the request origin
mariano-aguero Sep 17, 2026
77f7bee
fix(ui): stop bundling a second copy of core
mariano-aguero Sep 17, 2026
0e048c4
build(core): say why a cjs require of internal fails
mariano-aguero Sep 17, 2026
870bab0
fix(ui): clear the chain icon when the chain goes away
mariano-aguero Sep 17, 2026
1475041
style(core): format the cjs error shim
mariano-aguero Sep 17, 2026
f05b00c
fix(keys): restore and sign in a session that has no api key
mariano-aguero Sep 18, 2026
c412f9c
test(keys): cover the restore path a keyless session takes
mariano-aguero Sep 18, 2026
0961cfa
fix(keys): restore again when the key lands mid-flight
mariano-aguero Sep 18, 2026
b44f37f
Merge remote-tracking branch 'origin/main' into feat/core-optional-ap…
mariano-aguero Sep 18, 2026
3728b68
fix(core): cache a refusal, not every failed lookup
mariano-aguero Sep 18, 2026
c8ee012
fix(core): name the dapp on the token quote call too
mariano-aguero Sep 18, 2026
19fefec
fix(ui): let the chain stack resolve icons without a key
mariano-aguero Sep 18, 2026
c48e1de
perf(ui): paint a cached chain icon on the first frame
mariano-aguero Sep 18, 2026
17b6f87
refactor(keys): read the request api key in one place
mariano-aguero Sep 18, 2026
1518cbe
fix(core): mark a refusal at the transport, not by its code
mariano-aguero Sep 18, 2026
6c3dda4
refactor(core): keep the refusal marker off the dapp error
mariano-aguero Sep 18, 2026
3f5c310
fix(core): send analytics only for something shaped like an address
mariano-aguero Sep 18, 2026
66fbc2b
refactor(keys): drop two passkey helpers nothing calls
mariano-aguero Sep 18, 2026
c379c68
fix(core): keep a backend refusal from ending the session
mariano-aguero Sep 18, 2026
7659fae
fix(keys): show the fee row in a keyless permission dialog
mariano-aguero Sep 18, 2026
91abfa2
fix(core): let a later chain entry replace the stored one
mariano-aguero Sep 21, 2026
76bde63
fix(keys): set the dapp origin at config message time
mariano-aguero Sep 21, 2026
5b03cf5
chore: merge main and keep both storage event fixes
mariano-aguero Sep 21, 2026
3331ae2
fix(ui): name the dapp on reads to the jaw rpc
mariano-aguero Sep 22, 2026
6c5fbe1
fix(core): keep the paymaster context when backfilling addresses
mariano-aguero Sep 22, 2026
2290837
chore: merge main and keep ccip refusal with dapp transport
mariano-aguero Sep 22, 2026
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
4 changes: 2 additions & 2 deletions apps/docs/docs/pages/account/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ Configuration for creating or loading an account. React Native options (`nativeG
interface AccountConfig {
/** Chain ID for the account */
chainId: number;
/** API key for JAW services (required) */
apiKey: string;
/** API key for JAW services, if the caller has one */
apiKey?: string;
/** Custom paymaster URL for gas sponsorship */
paymasterUrl?: string;
/** Custom paymaster context for gas sponsorship */
Expand Down
14 changes: 13 additions & 1 deletion apps/docs/docs/pages/configuration/apiKey.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
Your JAW API key for authentication with JAW services.

**Type:** `string`
**Required:** Yes
**Required:** In [app-specific mode](/configuration/mode/app-specific). Optional in cross-platform mode, which is the default.

In cross-platform mode the SDK sends whatever it has and the backend decides whether to answer. With a key, the request is attributed to the project that key belongs to, and the allowed domains below are what the key is checked against. Without a key, the caller is identified by the origin the request comes from, and only origins registered with JustaName are served, so ask the team to register yours before dropping the option.

App-specific mode always needs a key, because it hands one to the UI handler your application implements. `JAW.create()` rejects an app-specific configuration without one.

## Usage

Expand All @@ -27,6 +31,14 @@ const jaw = JAW.create({
});
```

### Without a key

```typescript
const connector = jaw({ appName: 'My DApp' });
```

Requests then carry no `api-key` parameter, and the origin your application runs on is what names it. An origin nobody registered is refused.

## How to Get an API Key

1. Visit the [JAW Dashboard](https://dashboard.jaw.id/)
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/docs/pages/configuration/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ await jaw.disconnect();

## Minimal Configuration

The only required option is `apiKey`:
Every option is optional in cross-platform mode, the default. A request without an [apiKey](/configuration/apiKey) is identified by the origin it comes from, which JustaName has to have registered beforehand.

```typescript
// Wagmi
Expand All @@ -127,6 +127,8 @@ const connector = jaw({ apiKey: 'your-api-key' });
const jaw = JAW.create({ apiKey: 'your-api-key' });
```

[App-specific mode](/configuration/mode/app-specific) is the exception: it requires `apiKey` and a `uiHandler`.

## Related

- [Wagmi Integration](/wagmi) - Using with Wagmi
Expand Down
19 changes: 17 additions & 2 deletions apps/keys-jaw-id/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { TransactionRequestData } from '../components/TransactionModal';
import { useAuth, usePasskeys } from '../hooks';
import { SignInScreen, type AuthenticatedAccount } from '../components/OnboardingSection';
import { PasskeyManager, type PasskeyAccount } from '@jaw.id/core';
import { setDappOrigin } from '@jaw.id/core/internal';
import { SiweModal } from '../components/SiweModal';
import { ensureIntNumber, type SignInWithEthereumCapabilityRequest } from '@jaw.id/core';
import { ConnectModal } from '../components/ConnectModal';
Expand Down Expand Up @@ -317,6 +318,11 @@ function KeysJawIdAppContent({
if (message.data.apiKey) {
setApiKey(message.data.apiKey);
}
// The keyless counterpart of that bootstrap. Without a key the origin is the only
// thing that names the dApp, and the account screen below reads addresses over the
// proxy before any handshake runs. The message that triggered this handler is what
// locked the origin, so it is already available here.
setDappOrigin(communicator.getOrigin() || undefined);

// Apply the dApp's theme tokens so the embedded dialog matches its
// look & feel (accent color, border radius, light/dark), translated
Expand Down Expand Up @@ -560,9 +566,14 @@ function KeysJawIdAppContent({
}

// Get origin and set it as current context
const origin = communicator.getOrigin() || '';
const origin = communicator.getOrigin() ?? '';
setCurrentOrigin(origin);
cryptoHandler.setOrigin(origin);
// Our own Origin is the same whichever dApp opened us, so the backend
// cannot tell which one a call belongs to unless we say. An unknown origin
// goes through as unset rather than as '': it clears whatever the last
// request left behind, so a call is never credited to the wrong dApp.
setDappOrigin(origin || undefined);
Comment thread
mariano-aguero marked this conversation as resolved.

const peerPublicKey = request.sender;
const method = request.content.handshake.method;
Expand Down Expand Up @@ -720,10 +731,14 @@ function KeysJawIdAppContent({

try {
// Load session for this origin
const origin = communicator.getOrigin() || '';
const origin = communicator.getOrigin() ?? '';

// Update React state with current origin (needed for useAuth hook)
setCurrentOrigin(origin);
// Set again rather than relying on the handshake having run in this
// document: the origin the backend is told comes from the request being
// served, not from an earlier one.
setDappOrigin(origin || undefined);

// Reply to the SDK with a reconnect-required sentinel (tied to this
// request id, carries no secret) so it re-establishes a session against
Expand Down
13 changes: 2 additions & 11 deletions apps/keys-jaw-id/src/components/AddFundsModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type Address,
} from '@jaw.id/core';
import { useAuth } from '../../hooks';
import { apiKeyFromChain } from '../../lib/api-key';

export interface AddFundsModalProps {
/** The dapp's raw params, validated here before anything renders. */
Expand Down Expand Up @@ -67,17 +68,7 @@ export const AddFundsModal = ({
}
}, [params]);

const prodApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
return new URL(chain.rpcUrl).searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const prodApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

const mainnetRpcUrl = prodApiKey ? `${JAW_RPC_URL}?chainId=1&api-key=${prodApiKey}` : `${JAW_RPC_URL}?chainId=1`;

Expand Down
33 changes: 8 additions & 25 deletions apps/keys-jaw-id/src/components/ConnectModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMemo, useState } from 'react';
import type { chain } from '../../lib/sdk-types';
import { getChainNameFromId } from '../../lib/chain-handlers';
import { standardErrorCodes, JAW_RPC_URL } from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

export interface ConnectModalProps {
origin: string;
Expand Down Expand Up @@ -33,36 +34,18 @@ export const ConnectModal = ({
const [isProcessing, setIsProcessing] = useState<boolean>(false);

// Extract API key from rpcUrl if not provided as prop
const effectiveApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const effectiveApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Get chain name and icon
const chainName = useMemo(() => (chain ? getChainNameFromId(chain.id) : undefined), [chain]);
const chainIcon = useChainIconURI(chain?.id || 1, effectiveApiKey, 24);

// Extract API key from chain.rpcUrl for mainnet RPC URL
const mainnetRpcUrl = useMemo(() => {
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
const apiKey = url.searchParams.get('api-key');
return apiKey ? `${JAW_RPC_URL}?chainId=1&api-key=${apiKey}` : `${JAW_RPC_URL}?chainId=1`;
} catch {
return `${JAW_RPC_URL}?chainId=1`;
}
}
return `${JAW_RPC_URL}?chainId=1`;
}, [chain?.rpcUrl]);
// Mainnet, for ENS, under whatever key this request carries: the same one the
// chain icon above resolves with, so the two cannot disagree about who is asking.
const mainnetRpcUrl = useMemo(
() => (effectiveApiKey ? `${JAW_RPC_URL}?chainId=1&api-key=${effectiveApiKey}` : `${JAW_RPC_URL}?chainId=1`),
[effectiveApiKey]
);

const handleConnect = async () => {
try {
Expand Down
14 changes: 2 additions & 12 deletions apps/keys-jaw-id/src/components/Eip712Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from 'react';
import type { chain } from '../../lib/sdk-types';
import { getChainNameFromId } from '../../lib/chain-handlers';
import { standardErrorCodes, JAW_RPC_URL } from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

export interface Eip712ModalProps {
origin: string;
Expand Down Expand Up @@ -56,18 +57,7 @@ export const Eip712Modal = ({
const [signatureStatus, setSignatureStatus] = useState<string>('');

// Extract API key for other uses (chain icon, mainnet RPC)
const effectiveApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const effectiveApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Compute mainnet RPC URL for JustaName SDK (ENS resolution)
const mainnetRpcUrl = useMemo(() => {
Expand Down
25 changes: 7 additions & 18 deletions apps/keys-jaw-id/src/components/PermissionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
handleGetCapabilitiesRequest,
type FeeTokenCapability,
} from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

// Known function selectors mapping
// Permission request data
Expand Down Expand Up @@ -149,22 +150,7 @@ export const PermissionModal = ({
const [feeTokens, setFeeTokens] = useState<FeeTokenOption[]>([]);
const [feeTokensLoading, setFeeTokensLoading] = useState<boolean>(true);

// Extract API key from rpcUrl if not provided as prop
const extractedApiKey = useMemo(() => {
if (apiKey) return apiKey;

if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch (error) {
console.error('Failed to parse rpcUrl:', error);
return '';
}
}

return '';
}, [apiKey, chain?.rpcUrl]);
const extractedApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Note: Account initialization is handled by useSessionAccount hook

Expand Down Expand Up @@ -493,7 +479,10 @@ export const PermissionModal = ({
let isMounted = true;

const fetchFeeTokensData = async () => {
if (!viemChain || !extractedApiKey) {
// The chain, and nothing else: keyless the capabilities come back on the
// origin, and gating on the key here left this dialog without its fee row
// while the transaction dialog of the same session showed one.
if (!viemChain) {
setFeeTokensLoading(false);
return;
}
Expand All @@ -504,7 +493,7 @@ export const PermissionModal = ({
// Fetch capabilities from JAW RPC
const capabilities = await handleGetCapabilitiesRequest(
{ method: 'wallet_getCapabilities', params: [] },
extractedApiKey || '',
extractedApiKey,
true // showTestnets
);

Expand Down
14 changes: 2 additions & 12 deletions apps/keys-jaw-id/src/components/SignatureModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from 'react';
import type { chain } from '../../lib/sdk-types';
import { getChainNameFromId } from '../../lib/chain-handlers';
import { standardErrorCodes, JAW_RPC_URL } from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

export interface SignatureModalProps {
origin: string;
Expand Down Expand Up @@ -48,18 +49,7 @@ export const SignatureModal = ({
const [signatureStatus, setSignatureStatus] = useState<string>('');

// Extract API key for other uses (chain icon, mainnet RPC)
const effectiveApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const effectiveApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Compute mainnet RPC URL for JustaName SDK (ENS resolution)
const mainnetRpcUrl = useMemo(() => {
Expand Down
14 changes: 2 additions & 12 deletions apps/keys-jaw-id/src/components/SiweModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from 'react';
import type { chain } from '../../lib/sdk-types';
import { getChainNameFromId } from '../../lib/chain-handlers';
import { standardErrorCodes, JAW_RPC_URL } from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

export interface SiweModalProps {
origin: string;
Expand Down Expand Up @@ -50,18 +51,7 @@ export const SiweModal = ({
const [siweStatus, setSiweStatus] = useState<string>('');

// Extract API key for other uses (chain icon, mainnet RPC)
const effectiveApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const effectiveApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Compute mainnet RPC URL for JustaName SDK (ENS resolution)
const mainnetRpcUrl = useMemo(() => {
Expand Down
16 changes: 3 additions & 13 deletions apps/keys-jaw-id/src/components/TransactionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
JAW_RPC_URL,
type FeeTokenCapability,
} from '@jaw.id/core';
import { apiKeyFromChain } from '../../lib/api-key';

// Transaction execution result
export interface TransactionResult {
Expand Down Expand Up @@ -103,18 +104,7 @@ export const TransactionModal = ({
const [feeTokensLoading, setFeeTokensLoading] = useState(false);

// Extract API key from rpcUrl if not provided as prop
const effectiveApiKey = useMemo(() => {
if (apiKey) return apiKey;
if (chain?.rpcUrl) {
try {
const url = new URL(chain.rpcUrl);
return url.searchParams.get('api-key') || '';
} catch {
return '';
}
}
return '';
}, [apiKey, chain?.rpcUrl]);
const effectiveApiKey = useMemo(() => apiKeyFromChain(apiKey, chain?.rpcUrl), [apiKey, chain?.rpcUrl]);

// Determine if sponsored based on transactionRequest or prop
const isSponsored = useMemo(() => {
Expand Down Expand Up @@ -302,7 +292,7 @@ export const TransactionModal = ({
// Fetch capabilities from JAW RPC
const capabilities = await handleGetCapabilitiesRequest(
{ method: 'wallet_getCapabilities', params: [] },
effectiveApiKey || '',
effectiveApiKey,
true // showTestnets
);

Expand Down
Loading
Loading