Skip to content

Application sessions and owner approval

Connect an application account and approve each NEAR operation with a wallet-origin owner passkey.

Application sessions and owner approval ​

The SDK accepts a handoff from an explicitly trusted application backend. Google and application-passkey login can use the same stable application user ID. The wallet connection grants settings access. Each transaction requires fresh wallet owner proof.

Configure the application issuer ​

Ask the Wallet operator to configure trust for your application issuer and Ed25519 public key. The issuer must be a canonical HTTPS origin. The public key contains 32 Ed25519 bytes in canonical base64url. Keep the private key on the application backend. The environment must allow the application origin and the hosted wallet origin. Trust changes increment the environment revision and revoke earlier external sessions. Console administration procedures remain in the operator's private documentation.

An application identity consists of the provider, issuer, and stable subject. Email matching never establishes wallet ownership. Changing from direct Google trust to application-issuer trust requires a fresh owner-approved association. Wallet keys remain unchanged.

Enroll and associate ​

Existing wallets need one association approved by an enrolled wallet-origin owner passkey. The owner does not need to open a reusable signing session.

ts
const attempt = await seams.auth.prepareExternalAssociation(walletId);
if (!attempt.ok) throw new Error(attempt.code);
const { code } = await applicationBackend.handoff(attempt.value);
const linked = await seams.auth.associateExternalAuth({
  method: 'handoff', walletId, attemptId: attempt.value.attemptId, code,
});
if (!linked.ok) throw new Error(linked.code);

For a new NEAR-only wallet, call registration.registerOwnerWallet with a wallet-origin passkey and an explicit NEAR signer selection. The result has kind: 'wallet_owner_registered'. It stores encrypted owner material and leaves the wallet locked. It issues no reusable session, signing quota, or export capability. Then associate the application identity as shown above. This onboarding API initially supports NEAR-only passkey wallets.

Connect an existing application session ​

ts
const attempt = await seams.auth.prepareExternalAuth(walletId);
if (!attempt.ok) throw new Error(attempt.code);
const { code } = await applicationBackend.handoff(attempt.value);
const connected = await seams.auth.authenticate({
  method: 'handoff', walletId, attemptId: attempt.value.attemptId, code,
});
if (!connected.ok) throw new Error(connected.code);

The backend reads its verified application session. It never accepts the subject from the browser. Use createApplicationAuthAssertion from @seams/wallet-server/application-auth to sign the attempt, wallet, environment, purpose, and session expiry. Send the assertion as proof: { kind: 'application', assertion } to POST /wallet/external-auth/backend-exchange with the environment's secret API key. Return only the handoff code to the browser.

Assertions expire within two minutes. The hosted wallet retains browser-binding secrets and the session credential. Read access lasts at most one hour and expires with the authenticated application session.

Approve one NEAR transaction ​

ts
const operation = seams.near.prepareOwnerTransaction({
  authentication: connected.value,
  nearAccountId,
  transaction: {
    receiverId: nearAccountId,
    actions: [{ type: ActionType.Transfer, amount: '1' }],
  },
});
const outcome = await operation.approve();

The wallet reviews the complete transaction and requests a fresh passkey assertion. Server authorization binds that proof to the wallet, signer, network, transaction contents, and expiry. Warm signing state cannot satisfy this API. The SDK disposes the temporary signing client after the operation. A second operation requires another approval. Cancellation preserves the external read connection.

An existing enrolled passkey and encrypted signing material must be available on the wallet origin in this browser. A GetSignal passkey authenticates GetSignal. Wallet approval initially uses a wallet-origin owner passkey. Direct approval with another relying party's passkey is outside this API.

Show your review first ​

Pass review on a readable useSeams().wallet to show your React component before wallet approval. Mount TransactionReviewHost above the caller.

tsx
const operation = wallet.near.prepareOwnerTransaction({
  transaction,
  review: {
    title: 'Review deposit',
    validity: { kind: 'unbounded' },
    render: (controls) => <DepositReview quote={quote} controls={controls} />,
  },
});
await saveDepositRecord(depositId, operation.getState().recovery);
const outcome = await operation.approve();

Here, DepositReview, quote, depositId, and saveDepositRecord belong to your application. approve() opens the review, and the state becomes reviewing. Continue to wallet moves the shared modal to wallet approval. The state then becomes approval, and its 120-second deadline starts. Review time cannot consume that deadline. A cancelled review returns cancelled. An expired quote returns expired. Other review failures return not_submitted. Core seams.near.prepareOwnerTransaction does not accept review.

Observe and recover ​

Use operation.getState() or React's useOwnerNearOperation(operation). States distinguish prepared, reviewing, approval, submitting, completed, cancelled, expired, not_submitted, reverted, unresolved, recovery_unavailable, unknown, retirement_available, retirement_approval, and retired. A React wallet with access: 'external' exposes owner-operation handles separately from reusable signing controls.

Persist state.recovery as non-secret transaction metadata before starting an operation. After reload, use near.resumeOwnerTransaction({ authentication, recovery }), then call recover(). Recovery reads the recorded transaction hash and queries its outcome. It never broadcasts again. An unresolved submission blocks another transaction for that wallet connection until recovery settles it.

Handle outcomes by their typed kind. Do not interpret error-message text.

OutcomeApplication action
cancelled, expired, not_submittedClear this attempt's pending state and offer a fresh approval.
completed, revertedRecord the final transaction outcome and clear its pending state.
unresolvedPersist transactionId and recovery. Check that transaction before another owner transaction.
unknown, recovery_unavailableKeep an unresolved entry and offer recover() again. Missing storage or a timeout does not prove failure.
retirement_availableNEAR can no longer execute the transaction, but it may have executed earlier. Keep the operation pending. Check your balance or ledger, then offer retire().
retired with proof.execution: 'not_executed'The transaction expired and never executed. Clear this operation only, and offer a fresh approval.
retired with proof.execution: 'unverified'The owner released an expired transaction. Check your balance or ledger before you offer a fresh approval.

The submitting state exposes transactionId before broadcast begins. The SDK field transactionId contains the NEAR transaction hash. Persist that hash as soon as it appears. Recovery failures retain a hash already known to the operation handle. Broadcast and status requests stop waiting after 30 seconds. A timeout returns unresolved and does not prove transaction failure.

If another operation blocks approval, the result names that existing operation in recovery. Persist the returned identity or the updated operation.getState().recovery. Do not continue checking the newly prepared operation's original ID. Keep explicit recovery controls available, and allow reads and unrelated wallet actions.

UNKNOWN_TRANSACTION alone cannot end an operation. A transaction stays unresolved until a finalized block passes its validity period, which is about 25 hours on NEAR. While it waits, unresolved has retirementWait. Call recover() again after retirementWait.lastAcceptedBlockHeight, because the block ends nothing by itself. Do not read this block from the error text. Other unresolved results have no retirementWait. Receipts from SDK 0.39.0 and earlier keep only a hash, so retirementWait.submission.kind is hash_only and this period starts at the first recover() after you upgrade. That limit is a safety wait, not a NEAR limit, and the transaction's real validity can end much earlier. retire() opens a wallet review and a passkey request. If the owner cancels, the result is unresolved and the operation stays pending. retire() never rebroadcasts. Do not clear a pending record for a different operation. Keep the unresolved record when preparing another operation. The maintained example disables new transfers until the current operation settles. If authentication expires, reconnect and resume the saved operation with the new authentication. You can change the NEAR RPC URL while an operation is pending, if the new RPC serves the same network. Operations from SDK 0.40.0 and earlier are an exception: finish them or open them once with their original URL before you change it. After another URL, they return recovery_unavailable and no longer block new transactions.

Maintained example ​

See examples/seams-auth-menu/application-auth.html. Its browser component covers onboarding, association, connection, approval, cancellation, and recovery. Its backend handler accepts the application's verified session reader. The SDK does not require GetSignal's source or retained Google tokens.

Use the release notes to select matching package versions.