Skip to content

feat(identity): add /identity schemas subpath for dual-sig auth#31

Open
whoabuddy wants to merge 1 commit into
mainfrom
feat/identity-schemas
Open

feat(identity): add /identity schemas subpath for dual-sig auth#31
whoabuddy wants to merge 1 commit into
mainfrom
feat/identity-schemas

Conversation

@whoabuddy
Copy link
Copy Markdown
Contributor

Summary

  • Adds @aibtc/tx-schemas/identity subpath with zod schemas for the dual-signature auth flow currently duplicated across 8+ sites in landing-page, agent-news, x402-sponsor-relay, and x402-api.
  • Pure schemas — no runtime/Cloudflare deps. The matching verifier implementations will live in @aibtc/platform/auth (separate repo, created in Phase 0 of the cost-fix sprint).

What's in the subpath

  • Challenge, ChallengeStoreRecord, ChallengeResponse (matches landing-page/lib/challenge.ts shape)
  • BIP-137 + BIP-322 Bitcoin signature payload + supported address kinds
  • SIP-018 Stacks signature payload + domain (Sip018Domain, StacksNetwork)
  • DualSigClaim and resolved AddressPair
  • BitcoinAuthHeaders and StacksAuthHeaders for request-level auth

Why now

Per cloudflare-bill-audit-2026-04.md Section 5 / D4: the @aibtc/platform/auth migration plan needs the identity schemas in place first. Landing-page, agent-news, and the relay all carry their own copies of the BIP-322/SIP-018 payload shapes today; codifying the schema is a prerequisite to consolidating the verifier implementations.

This PR is intentionally landing on @aibtc/tx-schemas (the existing package) rather than waiting for the rename to @aibtc/schemas. When the rename PR ships, this subpath moves cleanly.

Test plan

  • npm run typecheck clean
  • npm test — 241 tests pass (14 new identity tests + 227 existing)
  • npm run build emits all subpath dist files
  • npm pack --dry-run includes dist/identity/*
  • Reviewer checks the subpath exports against landing-page's existing types so the eventual migration is mechanical

🤖 Generated with Claude Code

Adds @aibtc/tx-schemas/identity with shared zod schemas for the
dual-signature auth flow that's currently duplicated across 8+ sites
(landing-page, agent-news, x402-sponsor-relay, x402-api):

- challenge envelope (Challenge, ChallengeStoreRecord, ChallengeResponse)
- BIP-137 + BIP-322 Bitcoin signature payload + address kinds
- SIP-018 Stacks signature payload + domain
- dual-sig claim and resolved address-pair
- BTC and STX request auth headers

These are pure schemas — no runtime/Cloudflare deps. The matching
verifier implementations land in @aibtc/platform/auth (separate repo,
to be created in Phase 0 of the cost-fix sprint).

Refs: cloudflare-bill-audit-2026-04.md (D4, Phase 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 1, 2026 21:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a new @aibtc/tx-schemas/identity subpath that centralizes Zod schemas for the dual-signature auth flow (challenge envelopes, BTC/Stacks signature payloads, dual-sig claim, and request auth headers) so downstream services can share a single schema source.

Changes:

  • Added src/identity/* schemas (challenge, bitcoin signature payload, stacks signature payload, dual-sig claim/address pair, auth headers) and an identity barrel export.
  • Exposed the new identity subpath via package root exports (src/index.ts) and package.json exports entries.
  • Added Vitest coverage for the new identity schemas.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/identity/identity.test.ts Adds tests covering the new identity schemas’ basic acceptance/rejection cases.
src/index.ts Re-exports identity subpath from the package root.
src/identity/challenge.ts Adds challenge, stored challenge record, and challenge response schemas.
src/identity/bitcoin-signature.ts Adds BTC signature payload + scheme/address-kind enums.
src/identity/stacks-signature.ts Adds SIP-018 domain + Stacks signature payload schemas.
src/identity/dual-sig.ts Adds dual-sig claim schema and resolved {btcAddress, stxAddress} pair schema.
src/identity/auth-headers.ts Adds request auth-header payload schemas for BTC and STX auth.
src/identity/index.ts Identity subpath barrel exports.
package.json Adds exports mappings for ./identity and its leaf modules.
README.md Documents the new @aibtc/tx-schemas/identity subpath.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/identity/challenge.ts
Comment on lines +10 to +14
export const ChallengeSchema = z.object({
message: z.string().min(1),
expiresAt: z.string().datetime({ offset: true }),
action: z.string().min(1),
});
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the shared IsoDateTimeSchema from src/core/primitives instead of repeating z.string().datetime({ offset: true }) here (and for createdAt below). This keeps datetime validation/error messages consistent across subpaths and makes future changes (e.g., tighter datetime rules) apply everywhere automatically.

Copilot uses AI. Check for mistakes.
Comment thread src/identity/challenge.ts
Comment on lines +23 to +25
export const ChallengeStoreRecordSchema = ChallengeSchema.extend({
createdAt: z.string().datetime({ offset: true }),
});
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChallengeStoreRecordSchema should also reuse the shared IsoDateTimeSchema (from src/core/primitives) for createdAt to match the datetime convention used throughout core/http/rpc/news schemas.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +38
export const StacksSignaturePayloadSchema = z.object({
domain: Sip018DomainSchema,
message: z.string().min(1),
signature: z.string().min(1),
stxAddress: z.string().min(1),
});
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stxAddress should use the shared StacksAddressSchema from src/core/primitives rather than z.string().min(1). This keeps identity schemas aligned with the rest of the package (http/rpc/core all use StacksAddressSchema) and allows central tightening of address validation later without touching identity schemas.

Copilot uses AI. Check for mistakes.
Comment thread src/identity/dual-sig.ts
Comment on lines +27 to +30
export const AddressPairSchema = z.object({
btcAddress: z.string().min(1),
stxAddress: z.string().min(1),
});
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddressPairSchema should use the shared StacksAddressSchema for stxAddress (and consider a shared BTC address schema if one exists later) rather than z.string().min(1), to stay consistent with how Stacks addresses are validated elsewhere in this package.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +19
export const BitcoinAuthHeadersSchema = z.object({
address: z.string().min(1),
signature: z.string().min(1),
/** Unix seconds, as a string (HTTP header value) */
timestamp: z.string().regex(/^\d+$/),
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc describes X-BTC-Signature as base64, but signature is currently only validated as non-empty. Either add base64 validation (so the schema matches the documented contract) or update the docs to clarify this is an opaque string.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
* `X-STX-Domain` (JSON-encoded SIP-018 domain).
*/
export const StacksAuthHeadersSchema = z.object({
stxAddress: z.string().min(1),
signature: z.string().min(1),
timestamp: z.string().regex(/^\d+$/),
/** JSON-encoded Sip018Domain — verifier parses + checks against expected */
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says domain is a JSON-encoded SIP-018 domain, but the schema only enforces min(1). Consider validating that this string is valid JSON and conforms to Sip018DomainSchema (or rename/update docs to indicate it’s an unvalidated raw header value).

Suggested change
* `X-STX-Domain` (JSON-encoded SIP-018 domain).
*/
export const StacksAuthHeadersSchema = z.object({
stxAddress: z.string().min(1),
signature: z.string().min(1),
timestamp: z.string().regex(/^\d+$/),
/** JSON-encoded Sip018Domain — verifier parses + checks against expected */
* `X-STX-Domain` (raw header value containing a JSON-encoded SIP-018 domain).
*/
export const StacksAuthHeadersSchema = z.object({
stxAddress: z.string().min(1),
signature: z.string().min(1),
timestamp: z.string().regex(/^\d+$/),
/** Raw `X-STX-Domain` header value; verifier parses JSON and validates the SIP-018 domain separately */

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a centralized `@aibtc/tx-schemas/identity` subpath for the BIP-322/BIP-137 + SIP-018 dual-sig auth flow — a prerequisite the `@aibtc/platform/auth` migration needs before it can consolidate verifiers across landing-page, agent-news, and the relay.

What works well:

  • Consistent naming pattern (`FooBarSchema` + `FooBar` type) matching the rest of the repo
  • Both subpath index and leaf exports in `package.json` allow selective tree-shaking
  • `BitcoinAuthHeadersSchema.timestamp` regex (`/^\d+$/`) correctly enforces a numeric string — we've seen non-numeric timestamp headers slip through in older services
  • `p2tr` included in `BitcoinAddressKindSchema` with a "reserved" comment is the right call: valid enum value today, production-ready later
  • Test coverage is solid — happy paths + rejection cases for all schemas, and the import paths use `.js` extensions for ESM correctness

[question] `DualSigClaimSchema` has no shared action field (`src/identity/dual-sig.ts:13-19`)

The PR description says "both signatures verify and reference the same action" — but the schema doesn't capture that linkage. The BTC side has an independent `message`, the STX side has an independent `message`, and nothing ties them together at the data layer. The verifier in `@aibtc/platform/auth` presumably enforces that both messages cover the same action by convention.

Is this intentional? The risk is that call sites assemble `DualSigClaim` objects with mismatched BTC/STX messages and get a runtime verification failure with no schema-level hint about why. Worth considering a top-level `action` field:

```suggestion
export const DualSigClaimSchema = z.object({
action: z.string().min(1),
bitcoin: BitcoinSignaturePayloadSchema,
stacks: StacksSignaturePayloadSchema,
});
```

Happy to leave this as a follow-up if the existing migration sites don't have the field yet — but flagging it as a future footgun.

[suggestion] `scheme` optional in `BitcoinSignaturePayloadSchema` (`src/identity/bitcoin-signature.ts:43`)

The optional scheme is pragmatic (BIP-322 and BIP-137 can be auto-detected). We use BIP-137 for AIBTC inbox signing from `bc1q` addresses and have hit verifier confusion when the scheme wasn't set. Consider documenting the detection heuristic in a comment so call sites know what "absent" means at the verifier end — or encouraging callers to always set it.

[nit] `StacksAuthHeadersSchema.domain` accepts any non-empty string, but the comment says "verifier parses + checks against expected" — this is fine for transport, but a JSDoc type cross-reference (`@see Sip018DomainSchema`) would help implementors know what to parse it into.

Code quality notes:

  • No obvious reuse gaps — the subpath structure mirrors `/rpc`, `/http`, `/news` exactly
  • Comments are more detailed than typical for this repo, but given this is a shared auth contract across 8+ sites, the extra context is earned
  • No dead code or unused imports

Operational context: We sign AIBTC inbox messages with BIP-137 from our `bc1q` wallet (`p2wpkh`). The `SIP-018` domain pattern in `StacksSignaturePayloadSchema` matches what we observe in x402 sponsor relay auth calls. The JSON-encoded `domain` string in `StacksAuthHeadersSchema` is consistent with how the relay currently passes domain context via headers — this schema will let us validate those payloads rather than accepting raw strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants