@hauska-sdk/vda 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog - @hauska-sdk/vda
2
+
3
+ All notable changes to the VDA SDK will be documented in this file.
4
+
5
+ ## [0.1.0] - 2025-02-03
6
+
7
+ ### Added
8
+ - Initial release of CNS Protocol VDA SDK
9
+ - VDA (Verified Digital Asset) minting with metadata
10
+ - Access pass creation and management
11
+ - Ownership verification
12
+ - Ownership transfer
13
+ - Access pass revocation
14
+ - Cross-spoke search (real-estate, healthcare, architect, etc.)
15
+ - Universal metadata support (address, legalDesc, patientId, api14)
16
+ - PostgreSQL storage adapter support
17
+ - Automatic wallet creation integration
18
+
19
+ ---
20
+
21
+ [0.1.0]: https://github.com/hauska-sdk/hauska-sdk/releases/tag/@hauska-sdk/vda@0.1.0
package/README.md ADDED
@@ -0,0 +1,655 @@
1
+ # @hauska-sdk/vda
2
+
3
+ CNS Protocol VDA SDK - Verified Digital Assets management with cross-spoke search and access control.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hauska-sdk/vda
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { VDASDK } from "@hauska-sdk/vda";
15
+ import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
16
+ import { WalletManager } from "@hauska-sdk/wallet";
17
+ import { Pool } from "pg";
18
+
19
+ // Initialize SDK
20
+ const pool = new Pool({
21
+ connectionString: process.env.DATABASE_URL,
22
+ });
23
+
24
+ const sdk = new VDASDK({
25
+ storageAdapter: new PostgreSQLStorageAdapter({
26
+ pool,
27
+ autoMigrate: true,
28
+ }),
29
+ walletManager: new WalletManager(),
30
+ logHook: (level, message, data) => {
31
+ console.log(`[${level}] ${message}`, data);
32
+ },
33
+ });
34
+
35
+ // Mint a VDA
36
+ const vda = await sdk.mint({
37
+ assetType: "deed",
38
+ address: "123 Main St, Schertz, TX 78154",
39
+ ownerWallet: "0x...",
40
+ spoke: "real-estate",
41
+ });
42
+
43
+ // Create an access pass
44
+ const accessPass = await sdk.createAccessPass({
45
+ grantorWallet: "0x...",
46
+ recipientWallet: "0x...",
47
+ accessibleVDAs: [vda.id],
48
+ permissions: ["view", "download"],
49
+ expiry: Date.now() + 3600000, // 1 hour
50
+ spoke: "real-estate",
51
+ address: "123 Main St",
52
+ });
53
+
54
+ // Verify ownership
55
+ const verification = await sdk.verifyOwnership(vda.id, "0x...");
56
+ if (verification.hasAccess) {
57
+ // Grant access to resource
58
+ }
59
+ ```
60
+
61
+ ## Features
62
+
63
+ - ✅ **VDA Minting** - Create verified digital assets with universal metadata
64
+ - ✅ **Access Pass Management** - Create, revoke, and verify time-bound access passes
65
+ - ✅ **Ownership Verification** - Check direct ownership and access pass permissions
66
+ - ✅ **Ownership Transfer** - Transfer VDA ownership between wallets
67
+ - ✅ **Cross-Spoke Search** - Search VDAs across all spokes by universal metadata keys
68
+ - ✅ **Automatic Wallet Creation** - Seamless wallet management
69
+ - ✅ **Logging Hooks** - Optional monitoring and debugging support
70
+
71
+ ## Universal Metadata Schema
72
+
73
+ VDAs use a universal metadata schema that enables cross-spoke search. At least one universal metadata key is required:
74
+
75
+ ### Universal Metadata Keys
76
+
77
+ - **`address`** - Physical address (911 format: "123 Main St, Schertz, TX 78154")
78
+ - Used by: real-estate, architect, municipal spokes
79
+ - Enables cross-spoke property search
80
+
81
+ - **`legalDesc`** - Legal description (e.g., "Lot 5, Block 3, Schertz Heights")
82
+ - Used by: real-estate, architect, municipal spokes
83
+ - Alternative to address for property identification
84
+
85
+ - **`patientId`** - Patient identifier
86
+ - Used by: healthcare spoke
87
+ - Enables patient record aggregation across systems
88
+
89
+ - **`api14`** - API well number (format: "42-123-12345-00-00")
90
+ - Used by: oil-gas spoke
91
+ - Standard identifier for oil & gas wells
92
+
93
+ ### Spoke Types
94
+
95
+ - `real-estate` - Real estate properties, deeds, titles
96
+ - `healthcare` - Patient records, medical documents
97
+ - `oil-gas` - Well logs, permits, leases
98
+ - `architect` - Blueprints, building plans
99
+ - `municipal` - Permits, licenses, city records
100
+
101
+ ### Asset Types
102
+
103
+ - `deed` - Property deed
104
+ - `blueprint` - Architectural blueprint
105
+ - `permit` - Building or operational permit
106
+ - `contract` - Legal contract
107
+ - `health-record` - Medical record
108
+ - `well-log` - Oil & gas well log
109
+ - `access-pass` - Time-bound access pass
110
+ - `invoice` - Invoice document
111
+
112
+ ## API Reference
113
+
114
+ ### VDASDK
115
+
116
+ Main SDK class that provides all VDA functionality.
117
+
118
+ #### Constructor
119
+
120
+ ```typescript
121
+ new VDASDK(config: VDASDKConfig)
122
+ ```
123
+
124
+ **Configuration:**
125
+
126
+ ```typescript
127
+ interface VDASDKConfig {
128
+ storageAdapter: VDAStorageAdapter; // Required
129
+ walletManager?: WalletManager; // Optional, auto-created if not provided
130
+ logHook?: LogHook; // Optional logging hook
131
+ }
132
+ ```
133
+
134
+ #### Methods
135
+
136
+ ##### `mint(params: MintVDAParams): Promise<VDA>`
137
+
138
+ Mint a new VDA.
139
+
140
+ ```typescript
141
+ const vda = await sdk.mint({
142
+ assetType: "deed",
143
+ address: "123 Main St, Schertz, TX 78154",
144
+ ownerWallet: "0x...", // Optional if userId/password provided
145
+ spoke: "real-estate",
146
+ // Optional: userId, password for auto-wallet creation
147
+ });
148
+ ```
149
+
150
+ ##### `getVDA(vdaId: string): Promise<VDA | null>`
151
+
152
+ Get a VDA by ID.
153
+
154
+ ```typescript
155
+ const vda = await sdk.getVDA("vda-id");
156
+ ```
157
+
158
+ ##### `listVDAsByOwner(wallet: string): Promise<VDA[]>`
159
+
160
+ List all VDAs owned by a wallet.
161
+
162
+ ```typescript
163
+ const vdas = await sdk.listVDAsByOwner("0x...");
164
+ ```
165
+
166
+ ##### `createAccessPass(params: CreateAccessPassParams): Promise<VDA>`
167
+
168
+ Create an access pass VDA.
169
+
170
+ ```typescript
171
+ const accessPass = await sdk.createAccessPass({
172
+ grantorWallet: "0x...", // Must own all accessible VDAs
173
+ recipientWallet: "0x...", // Optional if userId/password provided
174
+ accessibleVDAs: ["vda-1", "vda-2"],
175
+ permissions: ["view", "download"],
176
+ expiry: Date.now() + 3600000, // Must be in the future
177
+ spoke: "real-estate",
178
+ address: "123 Main St", // Universal metadata
179
+ });
180
+ ```
181
+
182
+ ##### `revokeAccessPass(accessPassId: string, grantorWallet: string): Promise<VDA>`
183
+
184
+ Revoke an access pass.
185
+
186
+ ```typescript
187
+ const revoked = await sdk.revokeAccessPass(
188
+ accessPassId,
189
+ grantorWallet
190
+ );
191
+ ```
192
+
193
+ ##### `verifyOwnership(vdaId: string, wallet: string, requiredPermissions?: string[]): Promise<OwnershipVerificationResult>`
194
+
195
+ Verify ownership of a VDA.
196
+
197
+ ```typescript
198
+ const result = await sdk.verifyOwnership(
199
+ vdaId,
200
+ wallet,
201
+ ["view", "download"] // Optional permissions to check
202
+ );
203
+
204
+ if (result.hasAccess) {
205
+ console.log(`Access type: ${result.accessType}`); // "direct" or "access-pass"
206
+ if (result.accessPass) {
207
+ console.log(`Access via pass: ${result.accessPass.id}`);
208
+ }
209
+ }
210
+ ```
211
+
212
+ ##### `transferOwnership(params: TransferOwnershipParams): Promise<VDA>`
213
+
214
+ Transfer ownership of a VDA.
215
+
216
+ ```typescript
217
+ const updatedVDA = await sdk.transferOwnership({
218
+ vdaId: "vda-id",
219
+ currentOwner: "0x...", // Must match current owner
220
+ newOwner: "0x...",
221
+ });
222
+ ```
223
+
224
+ ##### `searchByAddress(address: string, options?: SearchOptions): Promise<SearchResult>`
225
+
226
+ Search VDAs by address (supports partial matching).
227
+
228
+ ```typescript
229
+ const result = await sdk.searchByAddress("Main St", {
230
+ limit: 10,
231
+ offset: 0,
232
+ });
233
+
234
+ console.log(`Found ${result.total} VDAs`);
235
+ console.log(`Showing ${result.results.length} results`);
236
+ ```
237
+
238
+ ##### `searchByPatientId(patientId: string, options?: SearchOptions): Promise<SearchResult>`
239
+
240
+ Search VDAs by patient ID (exact match).
241
+
242
+ ```typescript
243
+ const result = await sdk.searchByPatientId("patient-123", {
244
+ limit: 20,
245
+ offset: 0,
246
+ });
247
+ ```
248
+
249
+ ##### `searchByAPI14(api14: string, options?: SearchOptions): Promise<SearchResult>`
250
+
251
+ Search VDAs by API14 (exact match).
252
+
253
+ ```typescript
254
+ const result = await sdk.searchByAPI14("42-123-12345-00-00");
255
+ ```
256
+
257
+ ##### `searchUniversal(query: string, options?: SearchOptions): Promise<SearchResult>`
258
+
259
+ Search VDAs across all spokes by any universal metadata key. Automatically detects query type.
260
+
261
+ ```typescript
262
+ // Automatically detects API14 format
263
+ const result1 = await sdk.searchUniversal("42-123-12345-00-00");
264
+
265
+ // Tries patient ID first, falls back to address
266
+ const result2 = await sdk.searchUniversal("patient-123");
267
+
268
+ // Searches by address
269
+ const result3 = await sdk.searchUniversal("123 Main St");
270
+ ```
271
+
272
+ ## Access Pass Usage Guide
273
+
274
+ Access passes are time-bound, revocable permissions that grant access to VDAs. They are themselves VDAs with `assetType: "access-pass"`.
275
+
276
+ ### Creating Access Passes
277
+
278
+ ```typescript
279
+ // Create an access pass for a data room
280
+ const dataRoomVDA = await sdk.mint({
281
+ assetType: "deed",
282
+ address: "123 Main St",
283
+ ownerWallet: propertyOwnerWallet,
284
+ spoke: "real-estate",
285
+ });
286
+
287
+ const accessPass = await sdk.createAccessPass({
288
+ grantorWallet: propertyOwnerWallet, // Must own dataRoomVDA
289
+ recipientWallet: buyerWallet,
290
+ accessibleVDAs: [dataRoomVDA.id],
291
+ permissions: ["view", "download"], // What the recipient can do
292
+ expiry: Date.now() + 7 * 24 * 3600000, // 7 days
293
+ spoke: "real-estate",
294
+ address: "123 Main St",
295
+ });
296
+ ```
297
+
298
+ ### Verifying Access
299
+
300
+ ```typescript
301
+ // Check if wallet has access to a VDA
302
+ const verification = await sdk.verifyOwnership(
303
+ dataRoomVDA.id,
304
+ buyerWallet,
305
+ ["view"] // Required permission
306
+ );
307
+
308
+ if (verification.hasAccess) {
309
+ if (verification.accessType === "direct") {
310
+ console.log("User owns this VDA");
311
+ } else if (verification.accessType === "access-pass") {
312
+ console.log("User has access via access pass");
313
+ console.log(`Access pass expires: ${verification.accessPass?.metadata.expiry}`);
314
+ }
315
+ } else {
316
+ console.log(`Access denied: ${verification.reason}`);
317
+ }
318
+ ```
319
+
320
+ ### Revoking Access Passes
321
+
322
+ ```typescript
323
+ // Revoke an access pass before it expires
324
+ const revoked = await sdk.revokeAccessPass(
325
+ accessPass.id,
326
+ propertyOwnerWallet // Must be the grantor
327
+ );
328
+
329
+ // Access is immediately revoked
330
+ const verification = await sdk.verifyOwnership(
331
+ dataRoomVDA.id,
332
+ buyerWallet
333
+ );
334
+ // verification.hasAccess === false
335
+ ```
336
+
337
+ ### Access Pass Permissions
338
+
339
+ - `view` - Can view the VDA and associated documents
340
+ - `download` - Can download documents
341
+ - `write` - Can modify the VDA
342
+ - `annotate` - Can add annotations
343
+
344
+ ## Code Examples
345
+
346
+ ### Real Estate: Property Listing
347
+
348
+ ```typescript
349
+ // Mint a property deed
350
+ const property = await sdk.mint({
351
+ assetType: "deed",
352
+ address: "123 Main St, Schertz, TX 78154",
353
+ legalDesc: "Lot 5, Block 3, Schertz Heights",
354
+ ownerWallet: ownerWallet,
355
+ spoke: "real-estate",
356
+ ipfsCid: "Qm...", // Link to property documents on IPFS
357
+ });
358
+
359
+ // Create a data room access pass for potential buyers
360
+ const dataRoomPass = await sdk.createAccessPass({
361
+ grantorWallet: ownerWallet,
362
+ recipientWallet: buyerWallet,
363
+ accessibleVDAs: [property.id],
364
+ permissions: ["view", "download"],
365
+ expiry: Date.now() + 30 * 24 * 3600000, // 30 days
366
+ spoke: "real-estate",
367
+ address: "123 Main St, Schertz, TX 78154",
368
+ });
369
+ ```
370
+
371
+ ### Healthcare: Patient Record Management
372
+
373
+ ```typescript
374
+ // Mint a patient record
375
+ const patientRecord = await sdk.mint({
376
+ assetType: "health-record",
377
+ patientId: "PATIENT-12345",
378
+ ownerWallet: patientWallet,
379
+ spoke: "healthcare",
380
+ ipfsCid: "Qm...", // Encrypted medical records
381
+ });
382
+
383
+ // Grant doctor access
384
+ const doctorAccess = await sdk.createAccessPass({
385
+ grantorWallet: patientWallet,
386
+ recipientWallet: doctorWallet,
387
+ accessibleVDAs: [patientRecord.id],
388
+ permissions: ["view", "annotate"],
389
+ expiry: Date.now() + 24 * 3600000, // 24 hours
390
+ spoke: "healthcare",
391
+ patientId: "PATIENT-12345",
392
+ });
393
+
394
+ // Search all records for a patient
395
+ const allRecords = await sdk.searchByPatientId("PATIENT-12345");
396
+ ```
397
+
398
+ ### Oil & Gas: Well Log Management
399
+
400
+ ```typescript
401
+ // Mint a well log
402
+ const wellLog = await sdk.mint({
403
+ assetType: "well-log",
404
+ api14: "42-123-12345-00-00",
405
+ ownerWallet: operatorWallet,
406
+ spoke: "oil-gas",
407
+ ipfsCid: "Qm...", // Well log data
408
+ });
409
+
410
+ // Grant regulator access
411
+ const regulatorAccess = await sdk.createAccessPass({
412
+ grantorWallet: operatorWallet,
413
+ recipientWallet: regulatorWallet,
414
+ accessibleVDAs: [wellLog.id],
415
+ permissions: ["view"],
416
+ expiry: Date.now() + 365 * 24 * 3600000, // 1 year
417
+ spoke: "oil-gas",
418
+ api14: "42-123-12345-00-00",
419
+ });
420
+
421
+ // Search by API14
422
+ const wellData = await sdk.searchByAPI14("42-123-12345-00-00");
423
+ ```
424
+
425
+ ### Cross-Spoke Search
426
+
427
+ ```typescript
428
+ // Search for all assets at a specific address across all spokes
429
+ const allAssets = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");
430
+
431
+ // Results may include:
432
+ // - Real estate deeds
433
+ // - Architectural blueprints
434
+ // - Municipal permits
435
+ // - All from different spokes but same address
436
+ ```
437
+
438
+ ## Express.js Integration
439
+
440
+ ```typescript
441
+ import express from "express";
442
+ import { VDASDK } from "@hauska-sdk/vda";
443
+ import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
444
+
445
+ const app = express();
446
+ const sdk = new VDASDK({
447
+ storageAdapter: new PostgreSQLStorageAdapter({
448
+ pool: new Pool({ connectionString: process.env.DATABASE_URL }),
449
+ autoMigrate: true,
450
+ }),
451
+ });
452
+
453
+ // Mint a VDA
454
+ app.post("/api/vdas", async (req, res) => {
455
+ try {
456
+ const vda = await sdk.mint({
457
+ assetType: req.body.assetType,
458
+ address: req.body.address,
459
+ ownerWallet: req.body.ownerWallet,
460
+ spoke: req.body.spoke,
461
+ });
462
+ res.json(vda);
463
+ } catch (error) {
464
+ res.status(400).json({ error: error.message });
465
+ }
466
+ });
467
+
468
+ // Verify access before serving document
469
+ app.get("/api/documents/:vdaId", async (req, res) => {
470
+ const wallet = req.headers["x-wallet-address"] as string;
471
+
472
+ const verification = await sdk.verifyOwnership(
473
+ req.params.vdaId,
474
+ wallet,
475
+ ["view"]
476
+ );
477
+
478
+ if (!verification.hasAccess) {
479
+ return res.status(403).json({ error: verification.reason });
480
+ }
481
+
482
+ // Fetch and serve document from IPFS
483
+ // ...
484
+ });
485
+
486
+ // Create access pass
487
+ app.post("/api/access-passes", async (req, res) => {
488
+ try {
489
+ const accessPass = await sdk.createAccessPass({
490
+ grantorWallet: req.body.grantorWallet,
491
+ recipientWallet: req.body.recipientWallet,
492
+ accessibleVDAs: req.body.accessibleVDAs,
493
+ permissions: req.body.permissions,
494
+ expiry: req.body.expiry,
495
+ spoke: req.body.spoke,
496
+ address: req.body.address,
497
+ });
498
+ res.json(accessPass);
499
+ } catch (error) {
500
+ res.status(400).json({ error: error.message });
501
+ }
502
+ });
503
+ ```
504
+
505
+ ## Troubleshooting
506
+
507
+ ### Common Issues
508
+
509
+ #### "Invalid VDA metadata: At least one universal metadata key is required"
510
+
511
+ **Problem:** VDA is missing required universal metadata (address, legalDesc, patientId, or api14).
512
+
513
+ **Solution:** Provide at least one universal metadata key:
514
+
515
+ ```typescript
516
+ // ❌ Missing universal metadata
517
+ await sdk.mint({
518
+ assetType: "deed",
519
+ ownerWallet: "0x...",
520
+ spoke: "real-estate",
521
+ });
522
+
523
+ // ✅ Include address or legalDesc
524
+ await sdk.mint({
525
+ assetType: "deed",
526
+ address: "123 Main St",
527
+ ownerWallet: "0x...",
528
+ spoke: "real-estate",
529
+ });
530
+ ```
531
+
532
+ #### "Grantor does not own the following VDAs"
533
+
534
+ **Problem:** Trying to create an access pass for VDAs the grantor doesn't own.
535
+
536
+ **Solution:** Verify grantor owns all accessible VDAs:
537
+
538
+ ```typescript
539
+ // Check ownership first
540
+ const verification = await sdk.verifyOwnership(
541
+ vdaId,
542
+ grantorWallet
543
+ );
544
+
545
+ if (verification.hasAccess && verification.accessType === "direct") {
546
+ // Grantor owns it, can create access pass
547
+ }
548
+ ```
549
+
550
+ #### "Expiry must be in the future"
551
+
552
+ **Problem:** Access pass expiry is set to a past timestamp.
553
+
554
+ **Solution:** Ensure expiry is in the future:
555
+
556
+ ```typescript
557
+ // ❌ Past expiry
558
+ expiry: Date.now() - 1000
559
+
560
+ // ✅ Future expiry
561
+ expiry: Date.now() + 3600000 // 1 hour from now
562
+ ```
563
+
564
+ #### "Access pass is already revoked"
565
+
566
+ **Problem:** Trying to revoke an already-revoked access pass.
567
+
568
+ **Solution:** Check revocation status before revoking:
569
+
570
+ ```typescript
571
+ const accessPass = await sdk.getVDA(accessPassId);
572
+ if (accessPass?.metadata.revoked) {
573
+ // Already revoked
574
+ return;
575
+ }
576
+ await sdk.revokeAccessPass(accessPassId, grantorWallet);
577
+ ```
578
+
579
+ #### Search returns no results
580
+
581
+ **Problem:** Search query doesn't match stored addresses.
582
+
583
+ **Solution:**
584
+ - For address search, use partial matching (e.g., "Main St" instead of full address)
585
+ - Ensure addresses are normalized (case-insensitive)
586
+ - Check that VDAs were indexed correctly
587
+
588
+ ```typescript
589
+ // ✅ Partial matching works
590
+ const results = await sdk.searchByAddress("Main St");
591
+
592
+ // ✅ Exact match also works
593
+ const results = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");
594
+ ```
595
+
596
+ #### "VDA not found" after transfer
597
+
598
+ **Problem:** VDA ID doesn't exist or was deleted.
599
+
600
+ **Solution:** Verify VDA exists before transfer:
601
+
602
+ ```typescript
603
+ const vda = await sdk.getVDA(vdaId);
604
+ if (!vda) {
605
+ throw new Error("VDA not found");
606
+ }
607
+ await sdk.transferOwnership({ vdaId, currentOwner, newOwner });
608
+ ```
609
+
610
+ ### Performance Tips
611
+
612
+ 1. **Use pagination for large result sets:**
613
+ ```typescript
614
+ const result = await sdk.searchByAddress("Main St", {
615
+ limit: 50,
616
+ offset: 0,
617
+ });
618
+ ```
619
+
620
+ 2. **Index frequently searched fields:** The PostgreSQL adapter automatically creates indexes on `address`, `patient_id`, `api14`, and `owner_wallet`.
621
+
622
+ 3. **Batch operations:** When creating multiple VDAs, use `Promise.all()` for parallel execution:
623
+ ```typescript
624
+ const vdas = await Promise.all(
625
+ addresses.map(addr => sdk.mint({ address: addr, ... }))
626
+ );
627
+ ```
628
+
629
+ ## Storage Adapters
630
+
631
+ ### PostgreSQL (Production)
632
+
633
+ ```typescript
634
+ import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
635
+ import { Pool } from "pg";
636
+
637
+ const adapter = new PostgreSQLStorageAdapter({
638
+ pool: new Pool({
639
+ connectionString: process.env.DATABASE_URL,
640
+ }),
641
+ autoMigrate: true, // Automatically creates tables
642
+ });
643
+ ```
644
+
645
+ ### Mock (Testing)
646
+
647
+ ```typescript
648
+ import { MockStorageAdapter } from "@hauska-sdk/adapters-storage-mock";
649
+
650
+ const adapter = new MockStorageAdapter();
651
+ ```
652
+
653
+ ## License
654
+
655
+ MIT