@trusset/sdk 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/dist/index.mjs ADDED
@@ -0,0 +1,570 @@
1
+ // src/utils/errors.ts
2
+ var TrussetError = class _TrussetError extends Error {
3
+ constructor(message, code, statusCode, requestId) {
4
+ super(message);
5
+ this.name = "TrussetError";
6
+ this.code = code;
7
+ this.statusCode = statusCode;
8
+ this.requestId = requestId ?? null;
9
+ Object.setPrototypeOf(this, _TrussetError.prototype);
10
+ }
11
+ };
12
+ var AuthError = class _AuthError extends TrussetError {
13
+ constructor(message, requestId) {
14
+ super(message, "AUTH_FAILED", 401, requestId);
15
+ this.name = "AuthError";
16
+ Object.setPrototypeOf(this, _AuthError.prototype);
17
+ }
18
+ };
19
+ var ValidationError = class _ValidationError extends TrussetError {
20
+ constructor(message) {
21
+ super(message, "VALIDATION_ERROR", 400);
22
+ this.name = "ValidationError";
23
+ Object.setPrototypeOf(this, _ValidationError.prototype);
24
+ }
25
+ };
26
+ var NetworkError = class _NetworkError extends TrussetError {
27
+ constructor(message) {
28
+ super(message, "NETWORK_ERROR", 0);
29
+ this.name = "NetworkError";
30
+ Object.setPrototypeOf(this, _NetworkError.prototype);
31
+ }
32
+ };
33
+
34
+ // src/utils/http.ts
35
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
36
+ var silentLogger = {
37
+ debug() {
38
+ },
39
+ info() {
40
+ },
41
+ warn() {
42
+ },
43
+ error() {
44
+ }
45
+ };
46
+ var HttpClient = class {
47
+ constructor(config) {
48
+ this.config = {
49
+ baseUrl: config.baseUrl ?? "https://api.trusset.org",
50
+ apiKey: config.apiKey,
51
+ timeoutMs: config.timeoutMs ?? 3e4,
52
+ maxRetries: config.maxRetries ?? 3,
53
+ logger: config.logger ?? silentLogger
54
+ };
55
+ }
56
+ async get(path, params) {
57
+ const url = this.buildUrl(path, params);
58
+ return this.request("GET", url);
59
+ }
60
+ async post(path, body) {
61
+ const url = this.buildUrl(path);
62
+ return this.request("POST", url, body);
63
+ }
64
+ async put(path, body) {
65
+ const url = this.buildUrl(path);
66
+ return this.request("PUT", url, body);
67
+ }
68
+ async delete(path) {
69
+ const url = this.buildUrl(path);
70
+ return this.request("DELETE", url);
71
+ }
72
+ // -----------------------------------------------------------------------
73
+ buildUrl(path, params) {
74
+ const base = this.config.baseUrl.replace(/\/+$/, "");
75
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
76
+ const url = new URL(`${base}${cleanPath}`);
77
+ if (params) {
78
+ for (const [k, v] of Object.entries(params)) {
79
+ if (v !== void 0 && v !== null) {
80
+ url.searchParams.set(k, String(v));
81
+ }
82
+ }
83
+ }
84
+ return url.toString();
85
+ }
86
+ async request(method, url, body) {
87
+ let lastError = null;
88
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
89
+ if (attempt > 0) {
90
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 1e4);
91
+ this.config.logger.debug(`Retry ${attempt}/${this.config.maxRetries} after ${delay}ms`, { url });
92
+ await sleep(delay);
93
+ }
94
+ try {
95
+ const controller = new AbortController();
96
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
97
+ const headers = {
98
+ "x-api-key": this.config.apiKey,
99
+ "Accept": "application/json",
100
+ "User-Agent": "@trusset/sdk/0.1.0"
101
+ };
102
+ if (body !== void 0) {
103
+ headers["Content-Type"] = "application/json";
104
+ }
105
+ const res = await fetch(url, {
106
+ method,
107
+ headers,
108
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
109
+ signal: controller.signal
110
+ });
111
+ clearTimeout(timer);
112
+ if (res.status === 401) {
113
+ const json2 = await res.json().catch(() => ({}));
114
+ throw new AuthError(
115
+ json2.error?.message ?? "Authentication failed",
116
+ json2.metadata?.requestId
117
+ );
118
+ }
119
+ if (RETRYABLE_STATUS.has(res.status)) {
120
+ lastError = new TrussetError(
121
+ `Server returned ${res.status}`,
122
+ "SERVER_ERROR",
123
+ res.status
124
+ );
125
+ continue;
126
+ }
127
+ const json = await res.json();
128
+ if (!json.success) {
129
+ throw new TrussetError(
130
+ json.error?.message ?? `Request failed with status ${res.status}`,
131
+ json.error?.code ?? "REQUEST_FAILED",
132
+ res.status,
133
+ json.metadata?.requestId
134
+ );
135
+ }
136
+ return json.data;
137
+ } catch (err) {
138
+ if (err instanceof TrussetError) {
139
+ if (!RETRYABLE_STATUS.has(err.statusCode)) throw err;
140
+ lastError = err;
141
+ continue;
142
+ }
143
+ if (err instanceof TypeError || err instanceof DOMException && err.name === "AbortError") {
144
+ lastError = new NetworkError(
145
+ err instanceof DOMException ? "Request timed out" : `Network error: ${err.message}`
146
+ );
147
+ continue;
148
+ }
149
+ throw err;
150
+ }
151
+ }
152
+ throw lastError ?? new NetworkError("Request failed after retries");
153
+ }
154
+ };
155
+ function sleep(ms) {
156
+ return new Promise((resolve) => setTimeout(resolve, ms));
157
+ }
158
+
159
+ // src/utils/validation.ts
160
+ var ETH_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
161
+ function requireString(value, field) {
162
+ if (typeof value !== "string" || value.trim().length === 0) {
163
+ throw new ValidationError(`${field} is required and must be a non-empty string`);
164
+ }
165
+ return value.trim();
166
+ }
167
+ function requireAddress(value, field) {
168
+ const s = requireString(value, field);
169
+ if (!ETH_ADDRESS_RE.test(s)) {
170
+ throw new ValidationError(`${field} must be a valid Ethereum address`);
171
+ }
172
+ return s;
173
+ }
174
+ function optionalAddress(value, field) {
175
+ if (value === void 0 || value === null || value === "") return void 0;
176
+ return requireAddress(value, field);
177
+ }
178
+ function requireCountry(value, field) {
179
+ const s = requireString(value, field);
180
+ if (s.length < 2 || s.length > 3) {
181
+ throw new ValidationError(`${field} must be a 2-3 letter country code`);
182
+ }
183
+ return s.toUpperCase();
184
+ }
185
+ function clampInt(value, min, max, fallback) {
186
+ if (value === void 0 || value === null) return fallback;
187
+ const n = Math.floor(value);
188
+ if (isNaN(n)) return fallback;
189
+ return Math.max(min, Math.min(max, n));
190
+ }
191
+
192
+ // src/modules/customers/management.ts
193
+ var CustomerManagement = class {
194
+ constructor(http) {
195
+ this.http = http;
196
+ }
197
+ /**
198
+ * List customers for the current instance.
199
+ */
200
+ async list(options = {}) {
201
+ const params = {
202
+ limit: clampInt(options.limit, 1, 200, 100),
203
+ offset: clampInt(options.offset, 0, Number.MAX_SAFE_INTEGER, 0),
204
+ includeArchived: options.includeArchived ? "true" : void 0,
205
+ search: options.search || void 0
206
+ };
207
+ return this.http.get("/customers/api/manage", params);
208
+ }
209
+ /**
210
+ * Get a customer by wallet address, ID, externalId, or referenceKey.
211
+ */
212
+ async get(identifier) {
213
+ requireString(identifier, "identifier");
214
+ return this.http.get(`/customers/api/manage/${encodeURIComponent(identifier)}`);
215
+ }
216
+ /**
217
+ * Create a new customer record.
218
+ */
219
+ async create(input) {
220
+ if (input.walletAddress) optionalAddress(input.walletAddress, "walletAddress");
221
+ return this.http.post("/customers/api/manage", input);
222
+ }
223
+ /**
224
+ * Update an existing customer.
225
+ */
226
+ async update(customerId, input) {
227
+ requireString(customerId, "customerId");
228
+ return this.http.put(`/customers/api/manage/${encodeURIComponent(customerId)}`, input);
229
+ }
230
+ /**
231
+ * Archive (soft-delete) a customer.
232
+ */
233
+ async archive(customerId) {
234
+ requireString(customerId, "customerId");
235
+ return this.http.delete(`/customers/api/manage/${encodeURIComponent(customerId)}`);
236
+ }
237
+ /**
238
+ * Link a wallet address to a customer created via referenceKey.
239
+ */
240
+ async linkWallet(input) {
241
+ requireString(input.referenceKey, "referenceKey");
242
+ requireAddress(input.walletAddress, "walletAddress");
243
+ return this.http.post("/customers/api/manage/link-wallet", input);
244
+ }
245
+ /**
246
+ * Batch-fetch country codes for a list of wallet addresses.
247
+ */
248
+ async batchGetCountries(addresses) {
249
+ if (!Array.isArray(addresses) || addresses.length === 0) {
250
+ return {};
251
+ }
252
+ return this.http.post("/customers/api/manage/countries/batch", { addresses });
253
+ }
254
+ // -----------------------------------------------------------------------
255
+ // Convenience search helpers
256
+ // -----------------------------------------------------------------------
257
+ /**
258
+ * Search customers by name substring.
259
+ */
260
+ async searchByName(name, options) {
261
+ return this.list({ ...options, search: name });
262
+ }
263
+ /**
264
+ * Find a customer by wallet address.
265
+ */
266
+ async searchByWallet(walletAddress) {
267
+ requireAddress(walletAddress, "walletAddress");
268
+ return this.get(walletAddress);
269
+ }
270
+ /**
271
+ * Find a customer by external ID (your system's unique identifier).
272
+ */
273
+ async searchByExternalId(externalId) {
274
+ requireString(externalId, "externalId");
275
+ return this.get(externalId);
276
+ }
277
+ /**
278
+ * Find a customer by reference key (for pre-wallet customers).
279
+ */
280
+ async searchByReference(referenceKey) {
281
+ requireString(referenceKey, "referenceKey");
282
+ return this.get(referenceKey);
283
+ }
284
+ };
285
+
286
+ // src/modules/customers/identity.ts
287
+ var CustomerIdentity = class {
288
+ constructor(http) {
289
+ this.http = http;
290
+ }
291
+ /**
292
+ * Verify a customer's identity on-chain via the IdentityRegistry contract.
293
+ * Requires a configured relayer on the instance.
294
+ */
295
+ async verify(input) {
296
+ requireAddress(input.walletAddress, "walletAddress");
297
+ requireCountry(input.country, "country");
298
+ return this.http.post("/customers/api/identity/verify", input);
299
+ }
300
+ /**
301
+ * Batch-verify up to 500 identities in a single transaction.
302
+ * Requires a configured relayer.
303
+ */
304
+ async batchVerify(entries) {
305
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > 500) {
306
+ throw new ValidationError("Provide 1-500 entries for batch verification");
307
+ }
308
+ for (const e of entries) {
309
+ requireAddress(e.walletAddress, "walletAddress");
310
+ requireCountry(e.country, "country");
311
+ }
312
+ return this.http.post("/customers/api/identity/batch-verify", { entries });
313
+ }
314
+ /**
315
+ * Revoke an identity on-chain. Requires a configured relayer.
316
+ */
317
+ async revoke(walletAddress) {
318
+ requireAddress(walletAddress, "walletAddress");
319
+ return this.http.post("/customers/api/identity/revoke", { walletAddress });
320
+ }
321
+ /**
322
+ * Get combined on-chain and off-chain identity status.
323
+ */
324
+ async getStatus(walletAddress) {
325
+ requireAddress(walletAddress, "walletAddress");
326
+ return this.http.get(`/customers/api/identity/status/${walletAddress}`);
327
+ }
328
+ /**
329
+ * Check whether a token transfer between two addresses is compliant.
330
+ * Runs identity checks + all registered compliance modules.
331
+ */
332
+ async canTransfer(input) {
333
+ requireAddress(input.from, "from");
334
+ requireAddress(input.to, "to");
335
+ return this.http.post("/customers/api/identity/can-transfer", input);
336
+ }
337
+ };
338
+
339
+ // src/modules/customers/id-links.ts
340
+ var CustomerIdLinks = class {
341
+ constructor(http) {
342
+ this.http = http;
343
+ }
344
+ /**
345
+ * Create a KYC verification link to send to an investor.
346
+ */
347
+ async create(input) {
348
+ if (!input.recipientEmail && !input.recipientWallet && !input.recipientPhone) {
349
+ throw new ValidationError("At least one of recipientEmail, recipientWallet, or recipientPhone is required");
350
+ }
351
+ return this.http.post("/customers/api/id-links", input);
352
+ }
353
+ /**
354
+ * List all verification links for the current instance.
355
+ */
356
+ async list(options = {}) {
357
+ const params = {
358
+ limit: clampInt(options.limit, 1, 200, 50),
359
+ offset: clampInt(options.offset, 0, Number.MAX_SAFE_INTEGER, 0),
360
+ status: options.status
361
+ };
362
+ return this.http.get("/customers/api/id-links", params);
363
+ }
364
+ /**
365
+ * Revoke (expire) a verification link so it can no longer be used.
366
+ */
367
+ async revoke(linkId) {
368
+ requireString(linkId, "linkId");
369
+ await this.http.put(`/customers/api/id-links/${encodeURIComponent(linkId)}/revoke`);
370
+ }
371
+ /**
372
+ * Resend a verification link via the specified channels.
373
+ */
374
+ async resend(linkId, channels) {
375
+ requireString(linkId, "linkId");
376
+ if (!Array.isArray(channels) || channels.length === 0) {
377
+ throw new ValidationError("channels array is required (email and/or sms)");
378
+ }
379
+ return this.http.post(`/customers/api/id-links/${encodeURIComponent(linkId)}/resend`, { channels });
380
+ }
381
+ };
382
+
383
+ // src/modules/customers/sync.ts
384
+ var DEFAULT_BATCH_SIZE = 50;
385
+ var MAX_BATCH_SIZE = 200;
386
+ var MAX_VERIFY_BATCH = 500;
387
+ var CustomerSync = class {
388
+ constructor(http) {
389
+ this.http = http;
390
+ }
391
+ /**
392
+ * Import customer records from an external system.
393
+ *
394
+ * Each record is mapped by `externalId`. If a customer with that externalId
395
+ * already exists, the `onConflict` strategy determines whether to skip or
396
+ * update.
397
+ *
398
+ * When `verifyOnChain` is true, records that include a `walletAddress` and
399
+ * `country` will be batch-verified on-chain after the database import.
400
+ */
401
+ async importRecords(records, options = {}) {
402
+ if (!Array.isArray(records) || records.length === 0) {
403
+ throw new ValidationError("records must be a non-empty array");
404
+ }
405
+ const batchSize = clampInt(options.batchSize, 1, MAX_BATCH_SIZE, DEFAULT_BATCH_SIZE);
406
+ const onConflict = options.onConflict ?? "update";
407
+ const result = { total: records.length, created: 0, updated: 0, skipped: 0, errors: [] };
408
+ const verifyQueue = [];
409
+ for (let i = 0; i < records.length; i += batchSize) {
410
+ const batch = records.slice(i, i + batchSize);
411
+ const batchResults = await Promise.allSettled(
412
+ batch.map((record) => this.processRecord(record, onConflict))
413
+ );
414
+ for (let j = 0; j < batchResults.length; j++) {
415
+ const settled = batchResults[j];
416
+ const record = batch[j];
417
+ if (settled.status === "fulfilled") {
418
+ const outcome = settled.value;
419
+ if (outcome.action === "created") result.created++;
420
+ else if (outcome.action === "updated") result.updated++;
421
+ else result.skipped++;
422
+ if (options.verifyOnChain && record.walletAddress && record.country && outcome.action !== "skipped") {
423
+ verifyQueue.push({
424
+ walletAddress: record.walletAddress,
425
+ country: record.country,
426
+ investorType: record.investorType,
427
+ softExpiryDays: options.verificationDefaults?.softExpiryDays,
428
+ hardExpiryDays: options.verificationDefaults?.hardExpiryDays
429
+ });
430
+ }
431
+ } else {
432
+ const err = settled.reason;
433
+ result.errors.push({
434
+ externalId: record.externalId,
435
+ code: err?.code ?? "UNKNOWN",
436
+ message: err?.message ?? "Unknown error"
437
+ });
438
+ }
439
+ }
440
+ options.onProgress?.(Math.min(i + batchSize, records.length), records.length);
441
+ }
442
+ if (verifyQueue.length > 0) {
443
+ await this.runBatchVerification(verifyQueue, result);
444
+ }
445
+ return result;
446
+ }
447
+ /**
448
+ * Export all customers from Trusset as flat records.
449
+ * Useful for syncing data back into your system.
450
+ */
451
+ async exportRecords(options = {}) {
452
+ const allRecords = [];
453
+ let offset = 0;
454
+ const limit = 200;
455
+ while (true) {
456
+ const page = await this.http.get("/customers/api/manage", {
457
+ limit,
458
+ offset,
459
+ includeArchived: options.includeArchived ? "true" : void 0
460
+ });
461
+ if (!page.customers || page.customers.length === 0) break;
462
+ for (const c of page.customers) {
463
+ allRecords.push({
464
+ externalId: c.externalId ?? c.id,
465
+ walletAddress: c.walletAddress ?? void 0,
466
+ name: c.name ?? void 0,
467
+ country: c.country ?? void 0,
468
+ investorType: c.investorType ?? void 0,
469
+ metadata: c.metadata ?? void 0
470
+ });
471
+ }
472
+ offset += page.customers.length;
473
+ if (offset >= page.total) break;
474
+ }
475
+ return allRecords;
476
+ }
477
+ // -----------------------------------------------------------------------
478
+ // Internal
479
+ // -----------------------------------------------------------------------
480
+ async processRecord(record, onConflict) {
481
+ requireString(record.externalId, "externalId");
482
+ let existing = null;
483
+ try {
484
+ existing = await this.http.get(
485
+ `/customers/api/manage/${encodeURIComponent(record.externalId)}`
486
+ );
487
+ } catch (err) {
488
+ const code = err?.code;
489
+ const statusCode = err?.statusCode;
490
+ if (statusCode !== 404 && code !== "NOT_FOUND") throw err;
491
+ }
492
+ if (existing) {
493
+ if (onConflict === "skip") {
494
+ return { action: "skipped" };
495
+ }
496
+ await this.http.put(`/customers/api/manage/${encodeURIComponent(existing.id)}`, {
497
+ name: record.name,
498
+ country: record.country,
499
+ investorType: record.investorType,
500
+ walletAddress: record.walletAddress,
501
+ metadata: record.metadata
502
+ });
503
+ return { action: "updated" };
504
+ }
505
+ await this.http.post("/customers/api/manage", {
506
+ externalId: record.externalId,
507
+ walletAddress: record.walletAddress,
508
+ name: record.name,
509
+ country: record.country,
510
+ investorType: record.investorType,
511
+ metadata: record.metadata
512
+ });
513
+ return { action: "created" };
514
+ }
515
+ async runBatchVerification(queue, result) {
516
+ for (let i = 0; i < queue.length; i += MAX_VERIFY_BATCH) {
517
+ const batch = queue.slice(i, i + MAX_VERIFY_BATCH);
518
+ try {
519
+ await this.http.post("/customers/api/identity/batch-verify", { entries: batch });
520
+ } catch (err) {
521
+ for (const entry of batch) {
522
+ result.errors.push({
523
+ externalId: entry.walletAddress,
524
+ code: "VERIFY_FAILED",
525
+ message: err?.message ?? "On-chain batch verification failed"
526
+ });
527
+ }
528
+ }
529
+ }
530
+ }
531
+ };
532
+
533
+ // src/modules/customers/index.ts
534
+ var Customers = class {
535
+ constructor(http) {
536
+ this.manage = new CustomerManagement(http);
537
+ this.identity = new CustomerIdentity(http);
538
+ this.idLinks = new CustomerIdLinks(http);
539
+ this.sync = new CustomerSync(http);
540
+ }
541
+ };
542
+
543
+ // src/client.ts
544
+ var TrussetClient = class {
545
+ constructor(config) {
546
+ if (!config.apiKey || typeof config.apiKey !== "string" || config.apiKey.length < 20) {
547
+ throw new ValidationError("A valid apiKey is required");
548
+ }
549
+ this.http = new HttpClient({
550
+ apiKey: config.apiKey,
551
+ baseUrl: config.baseUrl,
552
+ timeoutMs: config.timeoutMs,
553
+ maxRetries: config.maxRetries,
554
+ logger: config.logger
555
+ });
556
+ this.customers = new Customers(this.http);
557
+ }
558
+ };
559
+ export {
560
+ AuthError,
561
+ CustomerIdLinks,
562
+ CustomerIdentity,
563
+ CustomerManagement,
564
+ CustomerSync,
565
+ Customers,
566
+ NetworkError,
567
+ TrussetClient,
568
+ TrussetError,
569
+ ValidationError
570
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@trusset/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Trusset SDK - End-to-end tokenization infrastructure for financial institutions",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./customers": {
15
+ "types": "./dist/modules/customers/index.d.ts",
16
+ "import": "./dist/modules/customers/index.mjs",
17
+ "require": "./dist/modules/customers/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
25
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
26
+ "lint": "eslint src --ext .ts",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "trusset",
34
+ "tokenization",
35
+ "mica",
36
+ "kyc",
37
+ "security-tokens",
38
+ "blockchain",
39
+ "fintech"
40
+ ],
41
+ "author": "Trusset UG",
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=22.0.0"
45
+ },
46
+ "dependencies": {},
47
+ "devDependencies": {
48
+ "tsup": "^8.3.0",
49
+ "typescript": "^5.6.0",
50
+ "vitest": "^2.1.0",
51
+ "eslint": "^8.57.0",
52
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
53
+ "@typescript-eslint/parser": "^7.0.0"
54
+ }
55
+ }