@coolwithakay/uatp 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/README.md +236 -0
- package/dist/index.d.mts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +446 -0
- package/dist/index.mjs +406 -0
- package/package.json +60 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
export { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UATP TypeScript SDK - Type Definitions
|
|
5
|
+
*/
|
|
6
|
+
interface ReasoningStep {
|
|
7
|
+
step: number;
|
|
8
|
+
thought: string;
|
|
9
|
+
confidence: number;
|
|
10
|
+
action?: string;
|
|
11
|
+
data_sources?: DataSource[];
|
|
12
|
+
reasoning?: string;
|
|
13
|
+
plain_language?: string;
|
|
14
|
+
}
|
|
15
|
+
interface DataSource {
|
|
16
|
+
source: string;
|
|
17
|
+
value: unknown;
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
api_endpoint?: string;
|
|
20
|
+
}
|
|
21
|
+
interface CertifyOptions {
|
|
22
|
+
/** Description of the task */
|
|
23
|
+
task: string;
|
|
24
|
+
/** The AI's final decision */
|
|
25
|
+
decision: string;
|
|
26
|
+
/** List of reasoning steps */
|
|
27
|
+
reasoning: ReasoningStep[];
|
|
28
|
+
/** Optional passphrase for key encryption (recommended for production) */
|
|
29
|
+
passphrase?: string;
|
|
30
|
+
/** If true, derive passphrase from browser/device info (default: true) */
|
|
31
|
+
deviceBound?: boolean;
|
|
32
|
+
/** Overall confidence score (0-1). Auto-calculated if not provided */
|
|
33
|
+
confidence?: number;
|
|
34
|
+
/** Additional metadata to attach */
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
/** Request RFC 3161 timestamp from server (default: true) */
|
|
37
|
+
requestTimestamp?: boolean;
|
|
38
|
+
/** Store capsule on UATP server (default: false) */
|
|
39
|
+
storeOnServer?: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface SignedCapsule {
|
|
42
|
+
/** Unique capsule identifier */
|
|
43
|
+
capsuleId: string;
|
|
44
|
+
/** Ed25519 signature (hex) */
|
|
45
|
+
signature: string;
|
|
46
|
+
/** Public key for verification (hex) */
|
|
47
|
+
publicKey: string;
|
|
48
|
+
/** SHA-256 hash of content (hex) */
|
|
49
|
+
contentHash: string;
|
|
50
|
+
/** ISO timestamp of signing */
|
|
51
|
+
timestamp: string;
|
|
52
|
+
/** Original content that was signed */
|
|
53
|
+
content: CapsuleContent;
|
|
54
|
+
/** RFC 3161 timestamp token (if requested) */
|
|
55
|
+
timestampToken?: string;
|
|
56
|
+
/** Timestamp authority used */
|
|
57
|
+
timestampTsa?: string;
|
|
58
|
+
/** Whether stored on server */
|
|
59
|
+
serverStored?: boolean;
|
|
60
|
+
/** URL to verify on server */
|
|
61
|
+
proofUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
interface CapsuleContent {
|
|
64
|
+
task: string;
|
|
65
|
+
decision: string;
|
|
66
|
+
reasoning_chain: ReasoningStep[];
|
|
67
|
+
confidence: number;
|
|
68
|
+
metadata: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
interface CapsuleProof {
|
|
71
|
+
capsuleId: string;
|
|
72
|
+
capsuleType: string;
|
|
73
|
+
status: string;
|
|
74
|
+
timestamp: Date;
|
|
75
|
+
payload: Record<string, unknown>;
|
|
76
|
+
rawData: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
interface VerificationResult {
|
|
79
|
+
valid: boolean;
|
|
80
|
+
signatureValid: boolean;
|
|
81
|
+
hashValid: boolean;
|
|
82
|
+
timestampValid?: boolean;
|
|
83
|
+
errors?: string[];
|
|
84
|
+
}
|
|
85
|
+
interface UATPConfig {
|
|
86
|
+
/** API key for authenticated requests */
|
|
87
|
+
apiKey?: string;
|
|
88
|
+
/** Base URL of UATP server (default: http://localhost:8000) */
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
91
|
+
timeout?: number;
|
|
92
|
+
}
|
|
93
|
+
interface KeyPair {
|
|
94
|
+
privateKey: Uint8Array;
|
|
95
|
+
publicKey: Uint8Array;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* UATP TypeScript SDK - Client
|
|
100
|
+
*
|
|
101
|
+
* Zero-Trust Architecture:
|
|
102
|
+
* - Private keys NEVER leave your device
|
|
103
|
+
* - All signing happens locally using Ed25519
|
|
104
|
+
* - Only content hash sent to server for RFC 3161 timestamping
|
|
105
|
+
* - Capsules can be verified independently without UATP
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
declare class UATP {
|
|
109
|
+
private apiKey?;
|
|
110
|
+
private baseUrl;
|
|
111
|
+
private timeout;
|
|
112
|
+
/**
|
|
113
|
+
* Initialize UATP client.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* // Local development
|
|
118
|
+
* const client = new UATP();
|
|
119
|
+
*
|
|
120
|
+
* // Production
|
|
121
|
+
* const client = new UATP({
|
|
122
|
+
* apiKey: 'your-api-key',
|
|
123
|
+
* baseUrl: 'https://api.uatp.io'
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
constructor(config?: UATPConfig);
|
|
128
|
+
/**
|
|
129
|
+
* Create a cryptographically certified capsule for an AI decision.
|
|
130
|
+
*
|
|
131
|
+
* ZERO-TRUST: Private key NEVER leaves your device.
|
|
132
|
+
* - All signing happens locally using Ed25519
|
|
133
|
+
* - Only the content hash is sent to UATP for timestamping
|
|
134
|
+
* - Capsules can be verified independently without UATP
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const result = await client.certify({
|
|
139
|
+
* task: 'Loan decision',
|
|
140
|
+
* decision: 'Approved for $50,000 at 5.2% APR',
|
|
141
|
+
* reasoning: [
|
|
142
|
+
* { step: 1, thought: 'Credit score 720 (excellent)', confidence: 0.95 },
|
|
143
|
+
* { step: 2, thought: 'Debt-to-income 0.28 (acceptable)', confidence: 0.90 }
|
|
144
|
+
* ]
|
|
145
|
+
* });
|
|
146
|
+
*
|
|
147
|
+
* console.log(result.capsuleId);
|
|
148
|
+
* console.log(result.signature);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
certify(options: CertifyOptions): Promise<SignedCapsule>;
|
|
152
|
+
/**
|
|
153
|
+
* Retrieve full cryptographic proof for a capsule.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const proof = await client.getProof('cap_abc123');
|
|
158
|
+
* console.log(proof.payload.task);
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
getProof(capsuleId: string): Promise<CapsuleProof>;
|
|
162
|
+
/**
|
|
163
|
+
* List capsules from the server.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const capsules = await client.listCapsules(10);
|
|
168
|
+
* for (const cap of capsules) {
|
|
169
|
+
* console.log(cap.capsuleId, cap.status);
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
listCapsules(limit?: number): Promise<CapsuleProof[]>;
|
|
174
|
+
/**
|
|
175
|
+
* Verify a capsule locally without server.
|
|
176
|
+
*
|
|
177
|
+
* This can be done by anyone - no UATP infrastructure required.
|
|
178
|
+
* Only uses cryptographic data embedded in the capsule.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const result = client.verifyLocal(capsule);
|
|
183
|
+
* if (result.valid) {
|
|
184
|
+
* console.log('Capsule is authentic!');
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
verifyLocal(capsule: SignedCapsule): VerificationResult;
|
|
189
|
+
/**
|
|
190
|
+
* Record the actual outcome of an AI decision (ground truth).
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* await client.recordOutcome('cap_abc123', {
|
|
195
|
+
* result: 'successful',
|
|
196
|
+
* aiWasCorrect: true,
|
|
197
|
+
* financialImpact: 2500,
|
|
198
|
+
* notes: 'Loan fully paid on time'
|
|
199
|
+
* });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
recordOutcome(capsuleId: string, outcome: {
|
|
203
|
+
result?: string;
|
|
204
|
+
aiWasCorrect?: boolean;
|
|
205
|
+
financialImpact?: number;
|
|
206
|
+
customerSatisfaction?: number;
|
|
207
|
+
notes?: string;
|
|
208
|
+
}): Promise<boolean>;
|
|
209
|
+
/**
|
|
210
|
+
* Internal fetch wrapper with timeout and headers.
|
|
211
|
+
*/
|
|
212
|
+
private fetch;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* UATP TypeScript SDK - Cryptographic Operations
|
|
217
|
+
*
|
|
218
|
+
* Zero-Trust Architecture:
|
|
219
|
+
* - Private keys NEVER leave your device
|
|
220
|
+
* - All signing happens locally using Ed25519
|
|
221
|
+
* - Keys are derived from passphrase using PBKDF2 (480,000 iterations)
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Derive an Ed25519 key pair from a passphrase.
|
|
226
|
+
* Uses PBKDF2-HMAC-SHA256 with 480,000 iterations.
|
|
227
|
+
*/
|
|
228
|
+
declare function deriveKeyPair(passphrase: string): KeyPair;
|
|
229
|
+
/**
|
|
230
|
+
* Generate a device-bound passphrase.
|
|
231
|
+
*
|
|
232
|
+
* SECURITY BOUNDARY: This is a CONVENIENCE feature, not a security feature.
|
|
233
|
+
* For production, use an explicit passphrase.
|
|
234
|
+
*/
|
|
235
|
+
declare function deriveDevicePassphrase(): string;
|
|
236
|
+
/**
|
|
237
|
+
* Canonicalize content for signing.
|
|
238
|
+
* Ensures deterministic JSON serialization.
|
|
239
|
+
*/
|
|
240
|
+
declare function canonicalize(obj: unknown): string;
|
|
241
|
+
/**
|
|
242
|
+
* Hash content using SHA-256.
|
|
243
|
+
*/
|
|
244
|
+
declare function hashContent(content: CapsuleContent): string;
|
|
245
|
+
/**
|
|
246
|
+
* Sign content with Ed25519.
|
|
247
|
+
*/
|
|
248
|
+
declare function sign(content: CapsuleContent, privateKey: Uint8Array): string;
|
|
249
|
+
/**
|
|
250
|
+
* Verify an Ed25519 signature.
|
|
251
|
+
*/
|
|
252
|
+
declare function verify(content: CapsuleContent, signature: string, publicKey: string): boolean;
|
|
253
|
+
/**
|
|
254
|
+
* Create a signed capsule.
|
|
255
|
+
*/
|
|
256
|
+
declare function createSignedCapsule(content: CapsuleContent, keyPair: KeyPair): SignedCapsule;
|
|
257
|
+
/**
|
|
258
|
+
* Verify a signed capsule locally.
|
|
259
|
+
* No server required - pure cryptographic verification.
|
|
260
|
+
*/
|
|
261
|
+
declare function verifyCapsule(capsule: SignedCapsule): {
|
|
262
|
+
valid: boolean;
|
|
263
|
+
signatureValid: boolean;
|
|
264
|
+
hashValid: boolean;
|
|
265
|
+
errors: string[];
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export { type CapsuleContent, type CapsuleProof, type CertifyOptions, type DataSource, type KeyPair, type ReasoningStep, type SignedCapsule, UATP, type UATPConfig, type VerificationResult, canonicalize, createSignedCapsule, deriveDevicePassphrase, deriveKeyPair, hashContent, sign, verify, verifyCapsule };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
UATP: () => UATP,
|
|
34
|
+
bytesToHex: () => import_utils.bytesToHex,
|
|
35
|
+
canonicalize: () => canonicalize,
|
|
36
|
+
createSignedCapsule: () => createSignedCapsule,
|
|
37
|
+
deriveDevicePassphrase: () => deriveDevicePassphrase,
|
|
38
|
+
deriveKeyPair: () => deriveKeyPair,
|
|
39
|
+
hashContent: () => hashContent,
|
|
40
|
+
hexToBytes: () => import_utils.hexToBytes,
|
|
41
|
+
sign: () => sign2,
|
|
42
|
+
verify: () => verify2,
|
|
43
|
+
verifyCapsule: () => verifyCapsule
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/crypto.ts
|
|
48
|
+
var ed = __toESM(require("@noble/ed25519"));
|
|
49
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
50
|
+
var import_pbkdf2 = require("@noble/hashes/pbkdf2");
|
|
51
|
+
var import_utils = require("@noble/hashes/utils");
|
|
52
|
+
ed.etc.sha512Sync = (...m) => {
|
|
53
|
+
const { sha512 } = require("@noble/hashes/sha512");
|
|
54
|
+
return sha512(ed.etc.concatBytes(...m));
|
|
55
|
+
};
|
|
56
|
+
var PBKDF2_ITERATIONS = 48e4;
|
|
57
|
+
var SALT_PREFIX = "uatp-v1-key-derivation";
|
|
58
|
+
function deriveKeyPair(passphrase) {
|
|
59
|
+
const salt = new TextEncoder().encode(SALT_PREFIX);
|
|
60
|
+
const passphraseBytes = new TextEncoder().encode(passphrase);
|
|
61
|
+
const seed = (0, import_pbkdf2.pbkdf2)(import_sha256.sha256, passphraseBytes, salt, {
|
|
62
|
+
c: PBKDF2_ITERATIONS,
|
|
63
|
+
dkLen: 32
|
|
64
|
+
});
|
|
65
|
+
const privateKey = seed;
|
|
66
|
+
const publicKey = ed.getPublicKey(privateKey);
|
|
67
|
+
return { privateKey, publicKey };
|
|
68
|
+
}
|
|
69
|
+
function deriveDevicePassphrase() {
|
|
70
|
+
const factors = [];
|
|
71
|
+
if (typeof window !== "undefined") {
|
|
72
|
+
factors.push(window.navigator.userAgent);
|
|
73
|
+
factors.push(window.navigator.language);
|
|
74
|
+
factors.push(String(window.screen.width));
|
|
75
|
+
factors.push(String(window.screen.height));
|
|
76
|
+
factors.push(window.location.hostname);
|
|
77
|
+
} else if (typeof process !== "undefined") {
|
|
78
|
+
factors.push(process.platform);
|
|
79
|
+
factors.push(process.arch);
|
|
80
|
+
factors.push(process.env.USER || process.env.USERNAME || "");
|
|
81
|
+
factors.push(require("os").hostname());
|
|
82
|
+
}
|
|
83
|
+
const combined = factors.join(":");
|
|
84
|
+
const hash = (0, import_sha256.sha256)(new TextEncoder().encode(combined));
|
|
85
|
+
return `device_${(0, import_utils.bytesToHex)(hash).slice(0, 32)}`;
|
|
86
|
+
}
|
|
87
|
+
function canonicalize(obj) {
|
|
88
|
+
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
89
|
+
}
|
|
90
|
+
function hashContent(content) {
|
|
91
|
+
const canonical = canonicalize(content);
|
|
92
|
+
const hash = (0, import_sha256.sha256)(new TextEncoder().encode(canonical));
|
|
93
|
+
return (0, import_utils.bytesToHex)(hash);
|
|
94
|
+
}
|
|
95
|
+
function sign2(content, privateKey) {
|
|
96
|
+
const canonical = canonicalize(content);
|
|
97
|
+
const messageBytes = new TextEncoder().encode(canonical);
|
|
98
|
+
const signature = ed.sign(messageBytes, privateKey);
|
|
99
|
+
return (0, import_utils.bytesToHex)(signature);
|
|
100
|
+
}
|
|
101
|
+
function verify2(content, signature, publicKey) {
|
|
102
|
+
try {
|
|
103
|
+
const canonical = canonicalize(content);
|
|
104
|
+
const messageBytes = new TextEncoder().encode(canonical);
|
|
105
|
+
const signatureBytes = (0, import_utils.hexToBytes)(signature);
|
|
106
|
+
const publicKeyBytes = (0, import_utils.hexToBytes)(publicKey);
|
|
107
|
+
return ed.verify(signatureBytes, messageBytes, publicKeyBytes);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function generateCapsuleId() {
|
|
113
|
+
const timestamp = Date.now().toString(36);
|
|
114
|
+
const randomBytes = new Uint8Array(8);
|
|
115
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
116
|
+
crypto.getRandomValues(randomBytes);
|
|
117
|
+
} else {
|
|
118
|
+
const { randomBytes: nodeRandom } = require("crypto");
|
|
119
|
+
const buf = nodeRandom(8);
|
|
120
|
+
randomBytes.set(buf);
|
|
121
|
+
}
|
|
122
|
+
const random = (0, import_utils.bytesToHex)(randomBytes).slice(0, 8);
|
|
123
|
+
return `cap_${timestamp}_${random}`;
|
|
124
|
+
}
|
|
125
|
+
function createSignedCapsule(content, keyPair) {
|
|
126
|
+
const capsuleId = generateCapsuleId();
|
|
127
|
+
const contentHash = hashContent(content);
|
|
128
|
+
const signature = sign2(content, keyPair.privateKey);
|
|
129
|
+
const publicKey = (0, import_utils.bytesToHex)(keyPair.publicKey);
|
|
130
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
131
|
+
return {
|
|
132
|
+
capsuleId,
|
|
133
|
+
signature,
|
|
134
|
+
publicKey,
|
|
135
|
+
contentHash,
|
|
136
|
+
timestamp,
|
|
137
|
+
content
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function verifyCapsule(capsule) {
|
|
141
|
+
const errors = [];
|
|
142
|
+
const computedHash = hashContent(capsule.content);
|
|
143
|
+
const hashValid = computedHash === capsule.contentHash;
|
|
144
|
+
if (!hashValid) {
|
|
145
|
+
errors.push("Content hash mismatch - capsule may have been tampered with");
|
|
146
|
+
}
|
|
147
|
+
const signatureValid = verify2(
|
|
148
|
+
capsule.content,
|
|
149
|
+
capsule.signature,
|
|
150
|
+
capsule.publicKey
|
|
151
|
+
);
|
|
152
|
+
if (!signatureValid) {
|
|
153
|
+
errors.push("Signature verification failed");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
valid: hashValid && signatureValid,
|
|
157
|
+
signatureValid,
|
|
158
|
+
hashValid,
|
|
159
|
+
errors
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/client.ts
|
|
164
|
+
var DEFAULT_BASE_URL = "http://localhost:8000";
|
|
165
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
166
|
+
var SDK_VERSION = "0.1.0";
|
|
167
|
+
var UATP = class {
|
|
168
|
+
/**
|
|
169
|
+
* Initialize UATP client.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // Local development
|
|
174
|
+
* const client = new UATP();
|
|
175
|
+
*
|
|
176
|
+
* // Production
|
|
177
|
+
* const client = new UATP({
|
|
178
|
+
* apiKey: 'your-api-key',
|
|
179
|
+
* baseUrl: 'https://api.uatp.io'
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
constructor(config = {}) {
|
|
184
|
+
this.apiKey = config.apiKey;
|
|
185
|
+
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
186
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
187
|
+
if (this.baseUrl === DEFAULT_BASE_URL) {
|
|
188
|
+
console.warn("UATP client using localhost - set baseUrl for production");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create a cryptographically certified capsule for an AI decision.
|
|
193
|
+
*
|
|
194
|
+
* ZERO-TRUST: Private key NEVER leaves your device.
|
|
195
|
+
* - All signing happens locally using Ed25519
|
|
196
|
+
* - Only the content hash is sent to UATP for timestamping
|
|
197
|
+
* - Capsules can be verified independently without UATP
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* const result = await client.certify({
|
|
202
|
+
* task: 'Loan decision',
|
|
203
|
+
* decision: 'Approved for $50,000 at 5.2% APR',
|
|
204
|
+
* reasoning: [
|
|
205
|
+
* { step: 1, thought: 'Credit score 720 (excellent)', confidence: 0.95 },
|
|
206
|
+
* { step: 2, thought: 'Debt-to-income 0.28 (acceptable)', confidence: 0.90 }
|
|
207
|
+
* ]
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* console.log(result.capsuleId);
|
|
211
|
+
* console.log(result.signature);
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
async certify(options) {
|
|
215
|
+
const {
|
|
216
|
+
task,
|
|
217
|
+
decision,
|
|
218
|
+
reasoning,
|
|
219
|
+
passphrase,
|
|
220
|
+
deviceBound = true,
|
|
221
|
+
confidence,
|
|
222
|
+
metadata = {},
|
|
223
|
+
requestTimestamp = true,
|
|
224
|
+
storeOnServer = false
|
|
225
|
+
} = options;
|
|
226
|
+
if (!task || typeof task !== "string") {
|
|
227
|
+
throw new Error("task must be a non-empty string");
|
|
228
|
+
}
|
|
229
|
+
if (!decision || typeof decision !== "string") {
|
|
230
|
+
throw new Error("decision must be a non-empty string");
|
|
231
|
+
}
|
|
232
|
+
if (!reasoning || !Array.isArray(reasoning) || reasoning.length === 0) {
|
|
233
|
+
throw new Error("reasoning must be a non-empty array");
|
|
234
|
+
}
|
|
235
|
+
let signingPassphrase;
|
|
236
|
+
if (passphrase) {
|
|
237
|
+
if (passphrase.length < 8) {
|
|
238
|
+
throw new Error("passphrase must be at least 8 characters");
|
|
239
|
+
}
|
|
240
|
+
signingPassphrase = passphrase;
|
|
241
|
+
} else if (deviceBound) {
|
|
242
|
+
console.warn(
|
|
243
|
+
"Using device-bound passphrase (DEVELOPMENT ONLY). For production, provide an explicit passphrase."
|
|
244
|
+
);
|
|
245
|
+
signingPassphrase = deriveDevicePassphrase();
|
|
246
|
+
} else {
|
|
247
|
+
throw new Error("Either provide a passphrase or set deviceBound=true");
|
|
248
|
+
}
|
|
249
|
+
const calculatedConfidence = confidence ?? reasoning.reduce((sum, step) => sum + (step.confidence || 0.5), 0) / reasoning.length;
|
|
250
|
+
const content = {
|
|
251
|
+
task,
|
|
252
|
+
decision,
|
|
253
|
+
reasoning_chain: reasoning,
|
|
254
|
+
confidence: calculatedConfidence,
|
|
255
|
+
metadata: {
|
|
256
|
+
...metadata,
|
|
257
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
258
|
+
sdk_version: SDK_VERSION,
|
|
259
|
+
signing_mode: "local"
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const keyPair = deriveKeyPair(signingPassphrase);
|
|
263
|
+
const signed = createSignedCapsule(content, keyPair);
|
|
264
|
+
if (requestTimestamp) {
|
|
265
|
+
try {
|
|
266
|
+
const response = await this.fetch("/timestamp", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: JSON.stringify({ hash: signed.contentHash })
|
|
269
|
+
});
|
|
270
|
+
if (response.ok) {
|
|
271
|
+
const tsData = await response.json();
|
|
272
|
+
signed.timestampToken = tsData.rfc3161;
|
|
273
|
+
signed.timestampTsa = tsData.tsa;
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.warn(
|
|
277
|
+
"Could not obtain timestamp. Capsule is still valid, just without external timestamp.",
|
|
278
|
+
error
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (storeOnServer) {
|
|
283
|
+
try {
|
|
284
|
+
const capsuleData = {
|
|
285
|
+
...signed,
|
|
286
|
+
type: "reasoning_trace",
|
|
287
|
+
payload: content
|
|
288
|
+
};
|
|
289
|
+
const response = await this.fetch("/capsules/store", {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: JSON.stringify(capsuleData)
|
|
292
|
+
});
|
|
293
|
+
if (response.ok) {
|
|
294
|
+
signed.serverStored = true;
|
|
295
|
+
signed.proofUrl = `${this.baseUrl}/capsules/${signed.capsuleId}/verify`;
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.warn("Could not store on server. Capsule is still valid locally.", error);
|
|
299
|
+
signed.serverStored = false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return signed;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Retrieve full cryptographic proof for a capsule.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const proof = await client.getProof('cap_abc123');
|
|
310
|
+
* console.log(proof.payload.task);
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
async getProof(capsuleId) {
|
|
314
|
+
const response = await this.fetch(`/capsules/${capsuleId}`);
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
if (response.status === 404) {
|
|
317
|
+
throw new Error(`Capsule ${capsuleId} not found`);
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`Failed to retrieve proof: ${response.status}`);
|
|
320
|
+
}
|
|
321
|
+
const data = await response.json();
|
|
322
|
+
const capsule = data.capsule || data;
|
|
323
|
+
return {
|
|
324
|
+
capsuleId: capsule.capsule_id || capsule.id || capsuleId,
|
|
325
|
+
capsuleType: capsule.type || capsule.capsule_type || "unknown",
|
|
326
|
+
status: capsule.status || "unknown",
|
|
327
|
+
timestamp: new Date(capsule.timestamp),
|
|
328
|
+
payload: capsule.payload || {},
|
|
329
|
+
rawData: data
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* List capsules from the server.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```typescript
|
|
337
|
+
* const capsules = await client.listCapsules(10);
|
|
338
|
+
* for (const cap of capsules) {
|
|
339
|
+
* console.log(cap.capsuleId, cap.status);
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
async listCapsules(limit = 10) {
|
|
344
|
+
const response = await this.fetch(`/capsules?demo_mode=false&per_page=${limit}`);
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
throw new Error(`Failed to list capsules: ${response.status}`);
|
|
347
|
+
}
|
|
348
|
+
const data = await response.json();
|
|
349
|
+
return (data.capsules || []).slice(0, limit).map((item) => ({
|
|
350
|
+
capsuleId: item.capsule_id,
|
|
351
|
+
capsuleType: item.type || "unknown",
|
|
352
|
+
status: item.status || "unknown",
|
|
353
|
+
timestamp: new Date(item.timestamp),
|
|
354
|
+
payload: item.payload || {},
|
|
355
|
+
rawData: item
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Verify a capsule locally without server.
|
|
360
|
+
*
|
|
361
|
+
* This can be done by anyone - no UATP infrastructure required.
|
|
362
|
+
* Only uses cryptographic data embedded in the capsule.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* const result = client.verifyLocal(capsule);
|
|
367
|
+
* if (result.valid) {
|
|
368
|
+
* console.log('Capsule is authentic!');
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
verifyLocal(capsule) {
|
|
373
|
+
return verifyCapsule(capsule);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Record the actual outcome of an AI decision (ground truth).
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* await client.recordOutcome('cap_abc123', {
|
|
381
|
+
* result: 'successful',
|
|
382
|
+
* aiWasCorrect: true,
|
|
383
|
+
* financialImpact: 2500,
|
|
384
|
+
* notes: 'Loan fully paid on time'
|
|
385
|
+
* });
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
async recordOutcome(capsuleId, outcome) {
|
|
389
|
+
const params = new URLSearchParams();
|
|
390
|
+
if (outcome.result) params.set("outcome_status", outcome.result);
|
|
391
|
+
if (outcome.notes) params.set("notes", outcome.notes);
|
|
392
|
+
if (outcome.customerSatisfaction) {
|
|
393
|
+
params.set("rating", String(outcome.customerSatisfaction));
|
|
394
|
+
}
|
|
395
|
+
const response = await this.fetch(
|
|
396
|
+
`/capsules/${capsuleId}/outcome?${params.toString()}`,
|
|
397
|
+
{ method: "POST" }
|
|
398
|
+
);
|
|
399
|
+
if (response.status === 401) {
|
|
400
|
+
throw new Error("Authentication required. Outcome tracking requires a valid API key.");
|
|
401
|
+
}
|
|
402
|
+
if (response.status === 404) {
|
|
403
|
+
throw new Error(`Capsule ${capsuleId} not found`);
|
|
404
|
+
}
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(`Failed to record outcome: ${response.status}`);
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Internal fetch wrapper with timeout and headers.
|
|
412
|
+
*/
|
|
413
|
+
async fetch(path, options = {}) {
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
418
|
+
...options,
|
|
419
|
+
signal: controller.signal,
|
|
420
|
+
headers: {
|
|
421
|
+
"Content-Type": "application/json",
|
|
422
|
+
"User-Agent": `uatp-typescript-sdk/${SDK_VERSION}`,
|
|
423
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
424
|
+
...options.headers
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return response;
|
|
428
|
+
} finally {
|
|
429
|
+
clearTimeout(timeoutId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
434
|
+
0 && (module.exports = {
|
|
435
|
+
UATP,
|
|
436
|
+
bytesToHex,
|
|
437
|
+
canonicalize,
|
|
438
|
+
createSignedCapsule,
|
|
439
|
+
deriveDevicePassphrase,
|
|
440
|
+
deriveKeyPair,
|
|
441
|
+
hashContent,
|
|
442
|
+
hexToBytes,
|
|
443
|
+
sign,
|
|
444
|
+
verify,
|
|
445
|
+
verifyCapsule
|
|
446
|
+
});
|