npm package: @hauska-sdk/vda
Description: CNS Protocol VDA SDK - Verified Digital Assets management
Author: Hauska SDK Team
Latest version: 0.1.0

# @hauska-sdk/vda

CNS Protocol VDA SDK - Verified Digital Assets management with cross-spoke search and access control.

## Installation

```bash
npm install @hauska-sdk/vda
```

## Quick Start

```typescript
import { VDASDK } from "@hauska-sdk/vda";
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
import { WalletManager } from "@hauska-sdk/wallet";
import { Pool } from "pg";

// Initialize SDK
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const sdk = new VDASDK({
  storageAdapter: new PostgreSQLStorageAdapter({
    pool,
    autoMigrate: true,
  }),
  walletManager: new WalletManager(),
  logHook: (level, message, data) => {
    console.log(`[${level}] ${message}`, data);
  },
});

// Mint a VDA
const vda = await sdk.mint({
  assetType: "deed",
  address: "123 Main St, Schertz, TX 78154",
  ownerWallet: "0x...",
  spoke: "real-estate",
});

// Create an access pass
const accessPass = await sdk.createAccessPass({
  grantorWallet: "0x...",
  recipientWallet: "0x...",
  accessibleVDAs: [vda.id],
  permissions: ["view", "download"],
  expiry: Date.now() + 3600000, // 1 hour
  spoke: "real-estate",
  address: "123 Main St",
});

// Verify ownership
const verification = await sdk.verifyOwnership(vda.id, "0x...");
if (verification.hasAccess) {
  // Grant access to resource
}
```

## Features

- ✅ **VDA Minting** - Create verified digital assets with universal metadata
- ✅ **Access Pass Management** - Create, revoke, and verify time-bound access passes
- ✅ **Ownership Verification** - Check direct ownership and access pass permissions
- ✅ **Ownership Transfer** - Transfer VDA ownership between wallets
- ✅ **Cross-Spoke Search** - Search VDAs across all spokes by universal metadata keys
- ✅ **Automatic Wallet Creation** - Seamless wallet management
- ✅ **Logging Hooks** - Optional monitoring and debugging support

## Universal Metadata Schema

VDAs use a universal metadata schema that enables cross-spoke search. At least one universal metadata key is required:

### Universal Metadata Keys

- **`address`** - Physical address (911 format: "123 Main St, Schertz, TX 78154")
  - Used by: real-estate, architect, municipal spokes
  - Enables cross-spoke property search

- **`legalDesc`** - Legal description (e.g., "Lot 5, Block 3, Schertz Heights")
  - Used by: real-estate, architect, municipal spokes
  - Alternative to address for property identification

- **`patientId`** - Patient identifier
  - Used by: healthcare spoke
  - Enables patient record aggregation across systems

- **`api14`** - API well number (format: "42-123-12345-00-00")
  - Used by: oil-gas spoke
  - Standard identifier for oil & gas wells

### Spoke Types

- `real-estate` - Real estate properties, deeds, titles
- `healthcare` - Patient records, medical documents
- `oil-gas` - Well logs, permits, leases
- `architect` - Blueprints, building plans
- `municipal` - Permits, licenses, city records

### Asset Types

- `deed` - Property deed
- `blueprint` - Architectural blueprint
- `permit` - Building or operational permit
- `contract` - Legal contract
- `health-record` - Medical record
- `well-log` - Oil & gas well log
- `access-pass` - Time-bound access pass
- `invoice` - Invoice document

## API Reference

### VDASDK

Main SDK class that provides all VDA functionality.

#### Constructor

```typescript
new VDASDK(config: VDASDKConfig)
```

**Configuration:**

```typescript
interface VDASDKConfig {
  storageAdapter: VDAStorageAdapter;  // Required
  walletManager?: WalletManager;      // Optional, auto-created if not provided
  logHook?: LogHook;                  // Optional logging hook
}
```

#### Methods

##### `mint(params: MintVDAParams): Promise<VDA>`

Mint a new VDA.

```typescript
const vda = await sdk.mint({
  assetType: "deed",
  address: "123 Main St, Schertz, TX 78154",
  ownerWallet: "0x...", // Optional if userId/password provided
  spoke: "real-estate",
  // Optional: userId, password for auto-wallet creation
});
```

##### `getVDA(vdaId: string): Promise<VDA | null>`

Get a VDA by ID.

```typescript
const vda = await sdk.getVDA("vda-id");
```

##### `listVDAsByOwner(wallet: string): Promise<VDA[]>`

List all VDAs owned by a wallet.

```typescript
const vdas = await sdk.listVDAsByOwner("0x...");
```

##### `createAccessPass(params: CreateAccessPassParams): Promise<VDA>`

Create an access pass VDA.

```typescript
const accessPass = await sdk.createAccessPass({
  grantorWallet: "0x...", // Must own all accessible VDAs
  recipientWallet: "0x...", // Optional if userId/password provided
  accessibleVDAs: ["vda-1", "vda-2"],
  permissions: ["view", "download"],
  expiry: Date.now() + 3600000, // Must be in the future
  spoke: "real-estate",
  address: "123 Main St", // Universal metadata
});
```

##### `revokeAccessPass(accessPassId: string, grantorWallet: string): Promise<VDA>`

Revoke an access pass.

```typescript
const revoked = await sdk.revokeAccessPass(
  accessPassId,
  grantorWallet
);
```

##### `verifyOwnership(vdaId: string, wallet: string, requiredPermissions?: string[]): Promise<OwnershipVerificationResult>`

Verify ownership of a VDA.

```typescript
const result = await sdk.verifyOwnership(
  vdaId,
  wallet,
  ["view", "download"] // Optional permissions to check
);

if (result.hasAccess) {
  console.log(`Access type: ${result.accessType}`); // "direct" or "access-pass"
  if (result.accessPass) {
    console.log(`Access via pass: ${result.accessPass.id}`);
  }
}
```

##### `transferOwnership(params: TransferOwnershipParams): Promise<VDA>`

Transfer ownership of a VDA.

```typescript
const updatedVDA = await sdk.transferOwnership({
  vdaId: "vda-id",
  currentOwner: "0x...", // Must match current owner
  newOwner: "0x...",
});
```

##### `searchByAddress(address: string, options?: SearchOptions): Promise<SearchResult>`

Search VDAs by address (supports partial matching).

```typescript
const result = await sdk.searchByAddress("Main St", {
  limit: 10,
  offset: 0,
});

console.log(`Found ${result.total} VDAs`);
console.log(`Showing ${result.results.length} results`);
```

##### `searchByPatientId(patientId: string, options?: SearchOptions): Promise<SearchResult>`

Search VDAs by patient ID (exact match).

```typescript
const result = await sdk.searchByPatientId("patient-123", {
  limit: 20,
  offset: 0,
});
```

##### `searchByAPI14(api14: string, options?: SearchOptions): Promise<SearchResult>`

Search VDAs by API14 (exact match).

```typescript
const result = await sdk.searchByAPI14("42-123-12345-00-00");
```

##### `searchUniversal(query: string, options?: SearchOptions): Promise<SearchResult>`

Search VDAs across all spokes by any universal metadata key. Automatically detects query type.

```typescript
// Automatically detects API14 format
const result1 = await sdk.searchUniversal("42-123-12345-00-00");

// Tries patient ID first, falls back to address
const result2 = await sdk.searchUniversal("patient-123");

// Searches by address
const result3 = await sdk.searchUniversal("123 Main St");
```

## Access Pass Usage Guide

Access passes are time-bound, revocable permissions that grant access to VDAs. They are themselves VDAs with `assetType: "access-pass"`.

### Creating Access Passes

```typescript
// Create an access pass for a data room
const dataRoomVDA = await sdk.mint({
  assetType: "deed",
  address: "123 Main St",
  ownerWallet: propertyOwnerWallet,
  spoke: "real-estate",
});

const accessPass = await sdk.createAccessPass({
  grantorWallet: propertyOwnerWallet, // Must own dataRoomVDA
  recipientWallet: buyerWallet,
  accessibleVDAs: [dataRoomVDA.id],
  permissions: ["view", "download"], // What the recipient can do
  expiry: Date.now() + 7 * 24 * 3600000, // 7 days
  spoke: "real-estate",
  address: "123 Main St",
});
```

### Verifying Access

```typescript
// Check if wallet has access to a VDA
const verification = await sdk.verifyOwnership(
  dataRoomVDA.id,
  buyerWallet,
  ["view"] // Required permission
);

if (verification.hasAccess) {
  if (verification.accessType === "direct") {
    console.log("User owns this VDA");
  } else if (verification.accessType === "access-pass") {
    console.log("User has access via access pass");
    console.log(`Access pass expires: ${verification.accessPass?.metadata.expiry}`);
  }
} else {
  console.log(`Access denied: ${verification.reason}`);
}
```

### Revoking Access Passes

```typescript
// Revoke an access pass before it expires
const revoked = await sdk.revokeAccessPass(
  accessPass.id,
  propertyOwnerWallet // Must be the grantor
);

// Access is immediately revoked
const verification = await sdk.verifyOwnership(
  dataRoomVDA.id,
  buyerWallet
);
// verification.hasAccess === false
```

### Access Pass Permissions

- `view` - Can view the VDA and associated documents
- `download` - Can download documents
- `write` - Can modify the VDA
- `annotate` - Can add annotations

## Code Examples

### Real Estate: Property Listing

```typescript
// Mint a property deed
const property = await sdk.mint({
  assetType: "deed",
  address: "123 Main St, Schertz, TX 78154",
  legalDesc: "Lot 5, Block 3, Schertz Heights",
  ownerWallet: ownerWallet,
  spoke: "real-estate",
  ipfsCid: "Qm...", // Link to property documents on IPFS
});

// Create a data room access pass for potential buyers
const dataRoomPass = await sdk.createAccessPass({
  grantorWallet: ownerWallet,
  recipientWallet: buyerWallet,
  accessibleVDAs: [property.id],
  permissions: ["view", "download"],
  expiry: Date.now() + 30 * 24 * 3600000, // 30 days
  spoke: "real-estate",
  address: "123 Main St, Schertz, TX 78154",
});
```

### Healthcare: Patient Record Management

```typescript
// Mint a patient record
const patientRecord = await sdk.mint({
  assetType: "health-record",
  patientId: "PATIENT-12345",
  ownerWallet: patientWallet,
  spoke: "healthcare",
  ipfsCid: "Qm...", // Encrypted medical records
});

// Grant doctor access
const doctorAccess = await sdk.createAccessPass({
  grantorWallet: patientWallet,
  recipientWallet: doctorWallet,
  accessibleVDAs: [patientRecord.id],
  permissions: ["view", "annotate"],
  expiry: Date.now() + 24 * 3600000, // 24 hours
  spoke: "healthcare",
  patientId: "PATIENT-12345",
});

// Search all records for a patient
const allRecords = await sdk.searchByPatientId("PATIENT-12345");
```

### Oil & Gas: Well Log Management

```typescript
// Mint a well log
const wellLog = await sdk.mint({
  assetType: "well-log",
  api14: "42-123-12345-00-00",
  ownerWallet: operatorWallet,
  spoke: "oil-gas",
  ipfsCid: "Qm...", // Well log data
});

// Grant regulator access
const regulatorAccess = await sdk.createAccessPass({
  grantorWallet: operatorWallet,
  recipientWallet: regulatorWallet,
  accessibleVDAs: [wellLog.id],
  permissions: ["view"],
  expiry: Date.now() + 365 * 24 * 3600000, // 1 year
  spoke: "oil-gas",
  api14: "42-123-12345-00-00",
});

// Search by API14
const wellData = await sdk.searchByAPI14("42-123-12345-00-00");
```

### Cross-Spoke Search

```typescript
// Search for all assets at a specific address across all spokes
const allAssets = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");

// Results may include:
// - Real estate deeds
// - Architectural blueprints
// - Municipal permits
// - All from different spokes but same address
```

## Express.js Integration

```typescript
import express from "express";
import { VDASDK } from "@hauska-sdk/vda";
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";

const app = express();
const sdk = new VDASDK({
  storageAdapter: new PostgreSQLStorageAdapter({
    pool: new Pool({ connectionString: process.env.DATABASE_URL }),
    autoMigrate: true,
  }),
});

// Mint a VDA
app.post("/api/vdas", async (req, res) => {
  try {
    const vda = await sdk.mint({
      assetType: req.body.assetType,
      address: req.body.address,
      ownerWallet: req.body.ownerWallet,
      spoke: req.body.spoke,
    });
    res.json(vda);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Verify access before serving document
app.get("/api/documents/:vdaId", async (req, res) => {
  const wallet = req.headers["x-wallet-address"] as string;
  
  const verification = await sdk.verifyOwnership(
    req.params.vdaId,
    wallet,
    ["view"]
  );

  if (!verification.hasAccess) {
    return res.status(403).json({ error: verification.reason });
  }

  // Fetch and serve document from IPFS
  // ...
});

// Create access pass
app.post("/api/access-passes", async (req, res) => {
  try {
    const accessPass = await sdk.createAccessPass({
      grantorWallet: req.body.grantorWallet,
      recipientWallet: req.body.recipientWallet,
      accessibleVDAs: req.body.accessibleVDAs,
      permissions: req.body.permissions,
      expiry: req.body.expiry,
      spoke: req.body.spoke,
      address: req.body.address,
    });
    res.json(accessPass);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
```

## Troubleshooting

### Common Issues

#### "Invalid VDA metadata: At least one universal metadata key is required"

**Problem:** VDA is missing required universal metadata (address, legalDesc, patientId, or api14).

**Solution:** Provide at least one universal metadata key:

```typescript
// ❌ Missing universal metadata
await sdk.mint({
  assetType: "deed",
  ownerWallet: "0x...",
  spoke: "real-estate",
});

// ✅ Include address or legalDesc
await sdk.mint({
  assetType: "deed",
  address: "123 Main St",
  ownerWallet: "0x...",
  spoke: "real-estate",
});
```

#### "Grantor does not own the following VDAs"

**Problem:** Trying to create an access pass for VDAs the grantor doesn't own.

**Solution:** Verify grantor owns all accessible VDAs:

```typescript
// Check ownership first
const verification = await sdk.verifyOwnership(
  vdaId,
  grantorWallet
);

if (verification.hasAccess && verification.accessType === "direct") {
  // Grantor owns it, can create access pass
}
```

#### "Expiry must be in the future"

**Problem:** Access pass expiry is set to a past timestamp.

**Solution:** Ensure expiry is in the future:

```typescript
// ❌ Past expiry
expiry: Date.now() - 1000

// ✅ Future expiry
expiry: Date.now() + 3600000 // 1 hour from now
```

#### "Access pass is already revoked"

**Problem:** Trying to revoke an already-revoked access pass.

**Solution:** Check revocation status before revoking:

```typescript
const accessPass = await sdk.getVDA(accessPassId);
if (accessPass?.metadata.revoked) {
  // Already revoked
  return;
}
await sdk.revokeAccessPass(accessPassId, grantorWallet);
```

#### Search returns no results

**Problem:** Search query doesn't match stored addresses.

**Solution:** 
- For address search, use partial matching (e.g., "Main St" instead of full address)
- Ensure addresses are normalized (case-insensitive)
- Check that VDAs were indexed correctly

```typescript
// ✅ Partial matching works
const results = await sdk.searchByAddress("Main St");

// ✅ Exact match also works
const results = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");
```

#### "VDA not found" after transfer

**Problem:** VDA ID doesn't exist or was deleted.

**Solution:** Verify VDA exists before transfer:

```typescript
const vda = await sdk.getVDA(vdaId);
if (!vda) {
  throw new Error("VDA not found");
}
await sdk.transferOwnership({ vdaId, currentOwner, newOwner });
```

### Performance Tips

1. **Use pagination for large result sets:**
   ```typescript
   const result = await sdk.searchByAddress("Main St", {
     limit: 50,
     offset: 0,
   });
   ```

2. **Index frequently searched fields:** The PostgreSQL adapter automatically creates indexes on `address`, `patient_id`, `api14`, and `owner_wallet`.

3. **Batch operations:** When creating multiple VDAs, use `Promise.all()` for parallel execution:
   ```typescript
   const vdas = await Promise.all(
     addresses.map(addr => sdk.mint({ address: addr, ... }))
   );
   ```

## Storage Adapters

### PostgreSQL (Production)

```typescript
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
import { Pool } from "pg";

const adapter = new PostgreSQLStorageAdapter({
  pool: new Pool({
    connectionString: process.env.DATABASE_URL,
  }),
  autoMigrate: true, // Automatically creates tables
});
```

### Mock (Testing)

```typescript
import { MockStorageAdapter } from "@hauska-sdk/adapters-storage-mock";

const adapter = new MockStorageAdapter();
```

## License

MIT