Skip to content

Latest commit

 

History

History
374 lines (275 loc) · 11.8 KB

File metadata and controls

374 lines (275 loc) · 11.8 KB

Architecture

This document describes the design philosophy, module organization, and extension points for the AffixioClient SDK.

Design Philosophy

1. Stateless by Default

The SDK is designed to be fully stateless. Each verification call is independent:

  • No session management
  • No request caching
  • No persistent HTTP connections
  • Fresh authentication on every call (Bearer token in Authorization header)

Why: Stateless architecture is easier to horizontally scale, test in isolation, and reason about. Especially important for agentic systems where multiple agents may share the same SDK instance.

2. Strongly Typed

Full TypeScript throughout, with zero implicit any:

  • Type definitions in dedicated types.ts file
  • All public methods accept typed parameters and return typed responses
  • Custom error classes with typed metadata

Why: Catches errors at compile time rather than runtime. IDE autocompletion helps integrators discover and use the API correctly.

3. Minimal Dependencies

The SDK uses only:

  • Native Node.js fetch API (v18+)
  • TypeScript (dev-only)

No Express, axios, or heavy HTTP client libraries. Native fetch is sufficient and keeps the bundle small for Node.js and browser environments.

Why: Faster installation, fewer security updates, easier to audit dependencies.

4. Purpose-Built Helpers

Instead of forcing integrators to build their own request payloads, the SDK provides high-level helpers:

  • verifyIssuerAgentAuthorization() - turnkey issuer auth
  • verifyMerchantAgentCheckout() - turnkey merchant checkout
  • verifyAgentConsent() - turnkey consent verification
  • verifyAgentPaymentComposite() - multi-circuit orchestration

Each helper validates inputs and constructs the request correctly. Integrators don't need to know the exact JSON structure or circuit IDs; the helpers encode domain knowledge.

Why: Reduces errors and documentation overhead. Helpers are easier to version and deprecate than exposing raw circuit IDs.

5. Portable Pattern

The structure is designed to be easily ported to other languages:

  • Clear separation of concerns (client, http, types, errors, helpers)
  • No language-specific idioms in the structure
  • Helpers use simple transformations, not metaprogramming

Why: Once the TypeScript SDK is proven, porting to Go, Java, or Python should be straightforward.

Module Organization

src/
├── index.ts          # Public API exports
├── client.ts         # Main AffixioClient class
├── types.ts          # TypeScript type definitions
├── errors.ts         # Error classes
├── http.ts           # Low-level HTTP abstraction
└── helpers.ts        # Request builders and utilities

client.ts

Contains the main AffixioClient class with:

  • Constructor accepting AffixioClientConfig
  • Static fromEnv() factory method for environment-based setup
  • Public methods: verify(), verifyIssuerAgentAuthorization(), verifyMerchantAgentCheckout(), verifyAgentConsent(), verifyAgentPaymentComposite()

Each method:

  1. Validates input using validateRequired() helper
  2. Builds the request using a helper function
  3. Calls this.http.post() to execute
  4. Logs via the configured logger
  5. Returns the typed response or throws a typed error

types.ts

Defines all public and internal types:

  • AffixioClientConfig - client configuration
  • CircuitId - union of known circuits + string for extensibility
  • VerifyRequest<TContext> - generic verification request with typed context
  • VerifyResponse - response from a single verification
  • Request/response types for each agentic helper
  • Composite types for multi-circuit verification

No logic here; just pure type definitions.

errors.ts

Custom error classes inheriting from a base AffixioError:

  • AffixioTransportError - network/timeout issues; retryable
  • AffixioApiError - 4xx/5xx responses; includes status and request ID
  • AffixioValidationError - client-side validation; not retryable

All errors carry metadata for logging and debugging.

http.ts

Low-level HTTP client wrapping native fetch:

  • Methods: post<T>(), get<T>()
  • Handles:
    • Timeout enforcement (AbortController)
    • Response parsing (JSON fallback to text)
    • Error mapping (network errors → AffixioTransportError, 4xx/5xx → AffixioApiError)
    • Request ID propagation (X-Request-ID header)
    • Authorization header (Bearer token)

No retries or backoff; that's the integrator's responsibility.

helpers.ts

Utility functions used by the client:

  • buildIssuerAuthRequest() - converts high-level request to generic VerifyRequest
  • buildMerchantCheckoutRequest() - same for merchant checkout
  • buildAgentConsentRequest() - same for consent
  • buildCompositeContext() - assembles context for composite verification
  • validateRequired() - checks for missing required fields
  • aggregateProofs() - combines proofs from multiple circuits
  • generateRequestId() - creates unique request IDs

These are internal helpers but exported for testing and potential manual use.

index.ts

Public API exports. Only re-exports from other modules:

  • Client class
  • Error classes
  • Type definitions
  • Selected helper functions (for advanced use)

Integrators should only import from the main package:

import { AffixioClient, AffixioApiError, ... } from '@affix-io/verification-sdk';

Error Handling Strategy

Transport Errors (Retryable)

Network failures, timeouts, DNS issues:

  • Throw AffixioTransportError
  • Include original error for debugging
  • Metadata includes path, URL, timeout value
  • Integrator should implement retry logic with exponential backoff

API Errors (May be Retryable)

4xx or 5xx responses:

  • Throw AffixioApiError
  • Include HTTP status, status text, response body, request ID
  • status >= 500 typically retryable
  • status 4xx usually not retryable (validation error, auth error)
  • Request ID aids support requests and debugging

Validation Errors (Not Retryable)

Missing or invalid client-side fields:

  • Throw AffixioValidationError
  • Include field name and message
  • Fix input and retry with corrected data

Extension Points

Adding a New Verification Helper

To add support for a new agentic payment scenario:

  1. Define request type in types.ts:

    export interface MyNewVerifyRequest {
      agentId: string;
      // ... fields
    }
  2. Define context type in types.ts:

    export interface MyNewContext {
      // ... context fields
    }
  3. Create builder helper in helpers.ts:

    export function buildMyNewRequest(
      req: MyNewVerifyRequest
    ): VerifyRequest<MyNewContext> {
      return {
        circuit_id: 'appropriate-circuit',
        identifier: req.agentId,
        context: { /* ... */ },
      };
    }
  4. Add client method in client.ts:

    async verifyMyNew(request: MyNewVerifyRequest): Promise<VerifyResponse> {
      validateRequired(request, ['agentId', /* ... */]);
      const verifyRequest = buildMyNewRequest(request);
      return this.verify(verifyRequest);
    }
  5. Export types in index.ts:

    export type { MyNewVerifyRequest } from './types.js';

Supporting a New Circuit

If AffixIO adds a new circuit (e.g., agentic-payment-limit-check):

  1. Update the CircuitId union in types.ts:

    export type CircuitId =
      | 'agentic-payment-permission'
      | 'agentic-payment-limit-check'  // <-- new
      | ...
  2. Optionally add a new helper method if it represents a distinct use case.

The generic verify() method already works with any circuit ID, so no changes needed there.

Custom Logger

Integrators can provide a custom logger to integrate with their observability stack:

const client = new AffixioClient({
  apiKey: process.env.AFFIXIO_API_KEY!,
  logger: myCustomLogger,
});

The SDK calls:

  • logger.info() for successful verifications and state changes
  • logger.warn() for non-fatal issues (e.g., circuit failure in composite)
  • logger.error() for exceptions and API errors

Composite Circuit Configuration

By default, composite verification runs:

  1. agentic-payment-permission
  2. finance-account-standing
  3. finance-fraud-indicator

Integrators can override with the circuits parameter:

await client.verifyAgentPaymentComposite({
  // ...
  circuits: ['agentic-payment-permission', 'cross-mfa-verification'],
});

For new use cases, add new circuit IDs to the union type and pass them explicitly.

Testing Strategy

Unit Tests (tests/client.test.ts)

  • Mock fetch globally to avoid real API calls
  • Test happy path for each verification method
  • Test validation errors (missing fields)
  • Test error handling (transport, API, validation)
  • Test composite verification logic

Run with npm test.

Integration Tests (not included)

To test against a real (staging) AffixIO API:

  1. Set AFFIXIO_API_KEY=<staging-key>
  2. Create tests that don't mock fetch
  3. Run against staging environment

Manual Testing

Use the examples in examples/full-usage.ts:

AFFIXIO_API_KEY=test_key npx ts-node examples/full-usage.ts

Performance Considerations

Timeouts

Default timeout is 5 seconds. Configurable:

const client = new AffixioClient({
  apiKey: '...',
  timeoutMs: 10000, // 10 seconds
});

For high-latency integrations, increase timeout. For real-time (e.g., point-of-sale), keep it low and retry on timeout.

Batch Verification

The SDK doesn't provide batch verification. Call verify() in parallel if needed:

const results = await Promise.all([
  client.verifyIssuerAgentAuthorization(req1),
  client.verifyIssuerAgentAuthorization(req2),
  client.verifyIssuerAgentAuthorization(req3),
]);

Proof Aggregation

Composite proofs are concatenated with : separator for simplicity. For production, consider using a cryptographic hash (SHA-256) over all proofs:

export function aggregateProofs(proofs: string[]): string {
  const combined = proofs.join(':');
  return crypto.createHash('sha256').update(combined).digest('hex');
}

Future Roadmap

Planned

  • Webhook support: Server-push verification results for async workflows
  • Batch API: POST multiple circuits in one call
  • Circuit introspection: GET /v1/circuits to discover available circuits and their requirements

Possible Ports

  • Go: github.com/affix-io/verification-sdk-go
  • Python: pip install affixio-verification-sdk
  • Java: Maven Central com.affixio:verification-sdk

Security Considerations

API Key Storage

  • Never commit API keys to version control
  • Use environment variables (AFFIXIO_API_KEY)
  • Or use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)

Transport Security

  • All requests use HTTPS (enforced by default baseUrl)
  • Bearer token sent in Authorization header (not in URL)

Proof Validation

  • Proofs are opaque strings generated by the API
  • Don't trust proofs from other sources; always call /v1/verify
  • Use proofs for audit trails, not authorization decisions

Error Details

  • API error responses may include sensitive data
  • Don't log error bodies in production without sanitization
  • Always include request ID for debugging with support

Glossary

  • Circuit: A verification function (e.g., agentic-payment-permission)
  • Identifier: Unique ID for the entity being verified (agent, account, card)
  • Context: JSON-serializable metadata about the verification (amount, merchant, etc.)
  • Proof: Cryptographic proof of the verification result; use for audit trails
  • Eligible: Boolean outcome; true if the entity passes the circuit
  • Composite: Multi-circuit verification with aggregated result
  • Request ID: Unique identifier for a single verification call; use for tracing