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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,37 @@ jobs:
- name: Smoke test built wheel
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py

agent-identity-node22:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile --filter @stripe/agent-identity...
- run: pnpm --filter @stripe/agent-identity build
# Build on the workspace runtime; test the SDK's supported consumer runtime.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Verify Agent Identity on Node 22
working-directory: packages/agent-identity
run: |
node node_modules/vitest/vitest.mjs run
node --test example/step-up/server.test.mjs
node scripts/check-package.mjs
node scripts/check-docs.mjs
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22.0.0
- name: Verify package on minimum supported Node
working-directory: packages/agent-identity
run: node scripts/check-package.mjs

ci:
runs-on: ubuntu-latest
steps:
Expand All @@ -65,6 +96,15 @@ jobs:
- run: pnpm turbo run typecheck
- run: pnpm biome check .
- run: pnpm turbo run test
- name: Check Agent Identity package, examples, and documentation
working-directory: packages/agent-identity
run: |
node --test example/step-up/server.test.mjs
node scripts/check-docs.mjs
node scripts/check-package.mjs
- name: Test wallet and verifier integration
working-directory: packages/agent-identity
run: node --test test/wallet.test.mjs

- name: Check Go SDK
run: |
Expand All @@ -82,4 +122,4 @@ jobs:
run: go test -race ./...

- name: Verify publishable
run: pnpm --filter @stripe/link-cli --filter @stripe/link-sdk --filter @stripe/link-integrations-better-auth --filter @stripe/link-integrations-eve publish --dry-run --no-git-checks
run: pnpm --filter @stripe/link-cli --filter @stripe/link-sdk --filter @stripe/link-integrations-better-auth --filter @stripe/link-integrations-eve --filter @stripe/agent-identity publish --dry-run --no-git-checks
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Link CLI — lets agents get secure, one-time-use payment credentials from a Lin
- **Link Python SDK** (`packages/sdk-python`): Python 3.11+ library covering the Go SDK's API resources with Python conventions. Distribution name: `link-sdk`; import name: `link`. HTTPX `Client` and `AsyncClient` expose typed keyword arguments and Pydantic response models. Uses uv for Python, dependencies, environments, builds, and development commands.
- **`@stripe/link-integrations-better-auth`** (`packages/integrations/better-auth`): Generic OAuth wrapper for Link sign-in and connecting wallets. Link's stable `/userinfo.id` identifies the provider account, using the SDK's `UserInfo` type through a development dependency. The `/client` export provides `linkClient()`: `link.connect()` wraps native `linkSocial`, while `link.disconnect()` checks an authoritative fresh session, ownership, provider, and last-account policy before revoking the stored refresh token and deleting the account. Revocation failures retain the account and credentials. Better Auth owns OAuth state, token storage, and refresh; wallet API calls remain in the SDK.
- **`@stripe/link-integrations-eve`** (`packages/integrations/eve`): Native Eve extension built with `eve extension build`. Static tool files wrap `@stripe/link-sdk/tools` and accept exactly one of `accessToken` or an Eve `auth` provider. OAuth tools use `ctx.getToken` and map Link 401s to `ctx.requireAuth`. The consuming application owns its OAuth provider, including token exchange, storage, refresh, and callback routing; this package supplies no OAuth client or storage abstraction. Static-token mode does not refresh. Interactive OAuth requires an authenticated Eve user. `create_spend_request` defaults to Eve approval via `always()`, which consumers can override; `request_approval: false` defers Link approval for a draft and does not authorize spending. Its `extension/skills/create-payment-credential/SKILL.md` and `extension/skills/financial-insights/SKILL.md` adapt the root skills to native tool calls and Eve auth; maintain these copies alongside shared wallet behavior and tool changes. Workspace development and CI require Node 24+.
- **`@stripe/agent-identity`** (`packages/agent-identity`): Service-side verification of Link bearer attestations and selectively disclosed identity presentations. Independent of the wallet client, using Node built-ins and `jose`, with ESM/CommonJS exports and a `/testing` entry point. Consumers require Node 22+; workspace development uses Node 24+. Uses tsup, TypeScript, and Vitest. Run `pnpm build`, `pnpm typecheck`, and `pnpm test` from `packages/agent-identity`; follow its README for package, documentation, and HTTP example checks. Guides live in READMEs; runnable examples use `example/`. Read its scoped `AGENTS.md` before changing verification behavior.
- **`@stripe/link-cli`** (`packages/cli`): Commander.js + Ink/React CLI that consumes `@stripe/link-sdk`. Entry: `src/cli.tsx`.

## Commands
Expand Down Expand Up @@ -192,7 +193,7 @@ Available in `--help` and `--llms`, but the command sets `mcp: false` so MCP cli
- The issued `cnf.jwk` is checked against the requested public key before returning the credential artifact.
- Local inspection: `identity credentials list` inspects `~/.link-cli/identity/credentials/current.json` for its path, issuer, cached expiry/`expired` status, holder-key path/thumbprint, and claim names. It never opens the private key or prints credential bytes or claim values. The command works without auth or API calls, uses `outputPolicy: 'all'`, and preserves the MCP exclusion. Inspection validates saved metadata without verifying signatures or scoping files to the active account.

- Presentation: `identity credentials present --aud <audience> --nonce <nonce> --claim email [--claim email_verified]` reads the current saved credential and existing holder key without auth or API calls. It returns `{ presentation }` with `outputPolicy: 'all'`, including terminal output, and remains excluded from MCP. `present.ts` selects original encoded disclosures, preserves the issuer JWT, and signs an Ed25519 `kb+jwt` containing the exact audience, nonce, current `iat`, and SHA-256 `sd_hash` over the selected SD-JWT including its trailing tilde. It requires explicit claims, checks validity and the holder key against the issuer JWT, and rejects unsupported nested disclosures or plaintext user claims. It does not regenerate keys, modify artifacts, or verify the issuer signature locally; the recipient verifier owns signature verification and nonce consumption. Clients should capture the sensitive presentation and send it as `Identity-Presentation`, without logging it.
- Presentation: `identity credentials present --aud <audience> --nonce <nonce> --claim email [--claim email_verified]` reads the current saved credential and existing holder key without auth or API calls. It returns `{ presentation }` with `outputPolicy: 'all'`, including terminal output, and remains excluded from MCP. `present.ts` selects original encoded disclosures, preserves the issuer JWT, and signs an Ed25519 `kb+jwt` containing the exact audience, nonce, current `iat`, and SHA-256 `sd_hash` over the selected SD-JWT including its trailing tilde. It requires explicit claims, checks validity and the holder key against the issuer JWT, and rejects unsupported nested disclosures or plaintext user claims. It does not regenerate keys, modify artifacts, or verify the issuer signature locally; the recipient verifier checks signatures and nonce equality, and the application owns interaction state and replay policy. Clients should capture the sensitive presentation and send it as `Identity-Presentation`, without logging it.

### serve command

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,13 @@ All commands accept `--auth <path>` to store auth credentials in a specific file

### Identity (beta)

Services can verify these credentials with [Agent Identity](packages/agent-identity/README.md), a separate SDK in this repository. The [event-registration example](packages/agent-identity/example/step-up/README.md) shows site access using an attestation and registration using a verified email.

Identity commands are available in `--help` and `--llms`, but remain excluded from MCP.

Both identity `request` commands save their artifacts to disk and return only the file path and metadata. This applies to every output format, including JSON, piped output, and `--full-output`. Request output never includes credentials, tokens, or claim values.

**Privacy-preserving tokens** that show Link attests to your agent:
**Privacy-preserving attestations** that let a service verify Link issued the token, without disclosing personal details or identifying the agent:

```bash
link-cli identity attestations request --count 10
Expand Down Expand Up @@ -418,7 +420,7 @@ Pool updates are serialized and saved atomically. A crash after removal can lose
link-cli identity credentials request
```

`identity credentials request` saves a signed credential to `~/.link-cli/identity/credentials/current.json`, bound to the CLI-managed holder key at `~/.link-cli/identity/holder-key.jwk`. Structured output includes `output_file`, issuer, expiry, holder-key path/thumbprint, and claim names. A script can read the saved credential and holder key to sign a presentation and send it through browser automation or an HTTP client without printing their contents into the agent transcript.
`identity credentials request` saves a signed credential to `~/.link-cli/identity/credentials/current.json`, bound to the CLI-managed holder key at `~/.link-cli/identity/holder-key.jwk`. Structured output includes `output_file`, issuer, expiry, holder-key path/thumbprint, and claim names. Use `identity credentials present` to sign selected claims, capture its JSON output programmatically, and send the presentation through browser automation or an HTTP client without printing it into the agent transcript.

**Presentations** disclose selected claims to a verifier using the saved credential and holder key:

Expand All @@ -438,7 +440,7 @@ Use the verifier challenge's exact audience and nonce. Repeat `--claim` to discl

Send the returned `presentation` as the `Identity-Presentation` HTTP header.

The verifier still validates the issuer signature, holder signature, audience, nonce, expiry, and required claims. Presentations include a fresh signing time and should be sent promptly; a verifier that has consumed the nonce requires a new challenge.
The verifier still validates the issuer signature, holder signature, audience, nonce, expiry, and required claims. Presentations include a fresh signing time and should be sent promptly. Follow the service's retry instructions. Completed interactions may return a saved result; a new operation or expired interaction may require a fresh challenge. The Agent Identity SDK verifies nonce equality; the service manages interaction state and replay policy.

**Local inspection** uses the same MCP exclusion:

Expand Down Expand Up @@ -665,6 +667,8 @@ please look at our [documentation and steps for integrating](https://docs.stripe

## SDKs

For services accepting agents, [Agent Identity](packages/agent-identity/README.md) verifies Link attestations and identity presentations. The wallet API clients below obtain credentials and call Link APIs.

Applications can use the credential-only Link client directly in
[TypeScript](packages/sdk/README.md), [Go](packages/sdk-go/README.md), or
[Python](packages/sdk-python/README.md). The Python SDK provides synchronous and
Expand Down
13 changes: 11 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
"indentStyle": "space",
"indentWidth": 2
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
Expand All @@ -28,7 +34,10 @@
},
"overrides": [
{
"includes": ["**/packages/sdk/src/**/*.test.ts"],
"includes": [
"**/packages/sdk/src/**/*.test.ts",
"**/packages/agent-identity/src/**/*.test.ts"
],
"linter": {
"rules": {
"style": {
Expand Down
76 changes: 76 additions & 0 deletions packages/agent-identity/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Integrating Agent Identity

Use `@stripe/agent-identity` to verify Link bearer Agent Attestation Tokens (AATs) and SD-JWT-VC identity presentations. Read [README.md](README.md) for the API, integration examples, and limitations.

## Scope

The SDK checks credentials. It does not sign or verify HTTP requests, resolve agent key directories, or process Web Bot Auth (WBA) headers. Do not add HTTP Message Signature requirements, raw body parsing, or `Content-Digest` validation to an integration with this SDK.

A bearer AAT proves issuance by Link. It does not identify the presenter or bind the token to your service or a particular request. Identity presentations require a holder-signed Key Binding JWT, audience, expiry, required-claims checks, and equality with the caller-provided expected nonce. These checks bind disclosures to the intended audience and expected interaction nonce.

## Integration

1. Follow [build and install from source](README.md#build-and-install-from-source); this package name is not published to npm yet. Workspace builds require Node 24+; the built package supports Node 22+, ESM and CommonJS. It uses Node built-ins and `jose`; runtimes without the required Node APIs are unsupported.
2. Create one `LinkVerifier` per process. Set `origin` from the service's configuration; it is the audience for identity claims. Use an explicit HTTP origin such as `http://localhost:3000` for local servers; bare authorities default to HTTPS.
3. Extract the `Authorization` or `Identity-Presentation` field value and pass the string directly. Reject duplicate credential fields if your framework would otherwise discard them. In Node or Express, use `req.headersDistinct`; the SDK cannot recover fields your framework dropped.
4. Inspect `result.valid`, or use the `OrThrow` variants and handle `VerificationError`. The result object itself is truthy even when verification fails.
5. Use `isRejection` to distinguish credential rejection (usually 401) from `issuer_unavailable` (usually 503). Handle errors from challenge builders and optional `warm()` separately; they throw on failure.
6. Request identity claims only when the application needs them. Associate the challenge nonce with the application interaction. Pass that nonce and explicit `requiredClaims` when verifying the presentation.
7. Apply the application's authorization and session policy after credential verification. Do not use `tokenKeyId` as a user or agent identity: it identifies an issuer signing key shared by many tokens.

```ts
import { LinkVerifier } from '@stripe/agent-identity';

const verifier = new LinkVerifier({ origin: 'https://shop.example' });
const attestation = await verifier.verifyAttestation(request.headers.get('Authorization'));

const claims = await verifier.verifyClaims(request.headers.get('Identity-Presentation'), {
nonce: nonceFromSession,
requiredClaims: ['email'],
});
```

Keep normal framework body parsing and size limits. Verification does not consume or authenticate the request body. Serve credential-bearing endpoints over HTTPS; the SDK receives credential strings and cannot check transport security.

## Claims and state

`claimsChallenge()` returns a nonce and advertised `expiresAt` value without storing either. The application associates the nonce with an interaction, enforces expiration, and atomically consumes or completes the interaction when its policy requires single use. `verifyClaims()` only checks that the holder-signed nonce equals the expected nonce supplied by the caller. Verification does not mutate application state, and the same valid presentation can verify again when passed the same expected nonce.

The `holderKeyThumbprint` returned by claims verification identifies the key in the credential's `cnf.jwk`. It does not authenticate the HTTP request or tie a separate anonymous AAT to that holder.

## Tests

Use the shipped `LinkFixture` and `CredentialFixture`. They use real cryptography and expose controls for invalid signatures, wrong audiences, expired credentials, and unsupported challenge digests.

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { LinkVerifier } from '@stripe/agent-identity';
import { LinkFixture } from '@stripe/agent-identity/testing';

test('verifies Link tokens without a request signature', async () => {
const link = await LinkFixture.create();
const verifier = new LinkVerifier({ origin: 'shop.example', fetchImpl: link.fetchImpl() });
const token = await link.mint();
assert.equal((await verifier.verifyAttestation(token.authorization)).valid, true);
const forged = await link.mint({ corruptAuthenticator: true });
assert.equal((await verifier.verifyAttestation(forged.authorization)).valid, false);
});
```

For credential tests, use `combineFetch` to serve Link metadata and credential JWKS and call `clearJwksCache()` between fixtures using different signing keys at the same issuer URL. After installing workspace dependencies, run `pnpm build`, `pnpm typecheck`, and `pnpm test` from `packages/agent-identity`. Follow the [development instructions](README.md#development) for the HTTP example, package installation, and documentation checks that also run in CI.

After changing issuance formats or wallet commands, also run `node --test test/wallet.test.mjs` from this package after building the wallet client SDK and CLI. Follow the [development instructions](README.md#development). This separate CI check uses the built wallet, a local issuer, and temporary storage to test issuance and presentation against the verifier and HTTP example.

## Limits to preserve in documentation

- AAT single use is not enforced. A bearer token can be replayed wherever it is trusted. Its lifetime is tied to issuer key acceptance, not challenge `max-age`.
- Key-bound AATs are unsupported and must fail with `challenge_mismatch`; this SDK does not establish possession of an agent signing key.
- Credential verification does not authorize an HTTP operation or authenticate its method, URL, or body. The application owns authorization and replay policy.
- Credential signing keys are cached process-wide for one hour; `issuerOptions.maxKeyAgeSeconds` controls only token keys.

AAT and identity-presentation double-spend detection and replay prevention belong to adopters and must be implemented in their own stack if needed. The SDK provides no spent-token or nonce store and no replay enforcement.

## Package maintenance

Keep the verifier independent of the wallet client: no runtime dependency on `@stripe/link-sdk` or `@stripe/link-cli`. Preserve ESM and CommonJS exports, the `/testing` entry point, Node 22 support for consumers, and the library declaration typecheck. Use Node built-ins and `jose` for standard byte and cryptography operations; retain identity-specific validation. Keep `jose` imports dynamic so the CommonJS build works on Node 22.0. Workspace development uses Node 24+. Examples use `example/`, matching the other integrations. Keep guidance in READMEs and examples; there is no documentation website. Preserve wire identifiers and published issuer paths when updating product names.
Loading
Loading