@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/dist/index.mjs ADDED
@@ -0,0 +1,406 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/crypto.ts
9
+ import * as ed from "@noble/ed25519";
10
+ import { sha256 } from "@noble/hashes/sha256";
11
+ import { pbkdf2 } from "@noble/hashes/pbkdf2";
12
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
13
+ ed.etc.sha512Sync = (...m) => {
14
+ const { sha512 } = __require("@noble/hashes/sha512");
15
+ return sha512(ed.etc.concatBytes(...m));
16
+ };
17
+ var PBKDF2_ITERATIONS = 48e4;
18
+ var SALT_PREFIX = "uatp-v1-key-derivation";
19
+ function deriveKeyPair(passphrase) {
20
+ const salt = new TextEncoder().encode(SALT_PREFIX);
21
+ const passphraseBytes = new TextEncoder().encode(passphrase);
22
+ const seed = pbkdf2(sha256, passphraseBytes, salt, {
23
+ c: PBKDF2_ITERATIONS,
24
+ dkLen: 32
25
+ });
26
+ const privateKey = seed;
27
+ const publicKey = ed.getPublicKey(privateKey);
28
+ return { privateKey, publicKey };
29
+ }
30
+ function deriveDevicePassphrase() {
31
+ const factors = [];
32
+ if (typeof window !== "undefined") {
33
+ factors.push(window.navigator.userAgent);
34
+ factors.push(window.navigator.language);
35
+ factors.push(String(window.screen.width));
36
+ factors.push(String(window.screen.height));
37
+ factors.push(window.location.hostname);
38
+ } else if (typeof process !== "undefined") {
39
+ factors.push(process.platform);
40
+ factors.push(process.arch);
41
+ factors.push(process.env.USER || process.env.USERNAME || "");
42
+ factors.push(__require("os").hostname());
43
+ }
44
+ const combined = factors.join(":");
45
+ const hash = sha256(new TextEncoder().encode(combined));
46
+ return `device_${bytesToHex(hash).slice(0, 32)}`;
47
+ }
48
+ function canonicalize(obj) {
49
+ return JSON.stringify(obj, Object.keys(obj).sort());
50
+ }
51
+ function hashContent(content) {
52
+ const canonical = canonicalize(content);
53
+ const hash = sha256(new TextEncoder().encode(canonical));
54
+ return bytesToHex(hash);
55
+ }
56
+ function sign2(content, privateKey) {
57
+ const canonical = canonicalize(content);
58
+ const messageBytes = new TextEncoder().encode(canonical);
59
+ const signature = ed.sign(messageBytes, privateKey);
60
+ return bytesToHex(signature);
61
+ }
62
+ function verify2(content, signature, publicKey) {
63
+ try {
64
+ const canonical = canonicalize(content);
65
+ const messageBytes = new TextEncoder().encode(canonical);
66
+ const signatureBytes = hexToBytes(signature);
67
+ const publicKeyBytes = hexToBytes(publicKey);
68
+ return ed.verify(signatureBytes, messageBytes, publicKeyBytes);
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ function generateCapsuleId() {
74
+ const timestamp = Date.now().toString(36);
75
+ const randomBytes = new Uint8Array(8);
76
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
77
+ crypto.getRandomValues(randomBytes);
78
+ } else {
79
+ const { randomBytes: nodeRandom } = __require("crypto");
80
+ const buf = nodeRandom(8);
81
+ randomBytes.set(buf);
82
+ }
83
+ const random = bytesToHex(randomBytes).slice(0, 8);
84
+ return `cap_${timestamp}_${random}`;
85
+ }
86
+ function createSignedCapsule(content, keyPair) {
87
+ const capsuleId = generateCapsuleId();
88
+ const contentHash = hashContent(content);
89
+ const signature = sign2(content, keyPair.privateKey);
90
+ const publicKey = bytesToHex(keyPair.publicKey);
91
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
92
+ return {
93
+ capsuleId,
94
+ signature,
95
+ publicKey,
96
+ contentHash,
97
+ timestamp,
98
+ content
99
+ };
100
+ }
101
+ function verifyCapsule(capsule) {
102
+ const errors = [];
103
+ const computedHash = hashContent(capsule.content);
104
+ const hashValid = computedHash === capsule.contentHash;
105
+ if (!hashValid) {
106
+ errors.push("Content hash mismatch - capsule may have been tampered with");
107
+ }
108
+ const signatureValid = verify2(
109
+ capsule.content,
110
+ capsule.signature,
111
+ capsule.publicKey
112
+ );
113
+ if (!signatureValid) {
114
+ errors.push("Signature verification failed");
115
+ }
116
+ return {
117
+ valid: hashValid && signatureValid,
118
+ signatureValid,
119
+ hashValid,
120
+ errors
121
+ };
122
+ }
123
+
124
+ // src/client.ts
125
+ var DEFAULT_BASE_URL = "http://localhost:8000";
126
+ var DEFAULT_TIMEOUT = 3e4;
127
+ var SDK_VERSION = "0.1.0";
128
+ var UATP = class {
129
+ /**
130
+ * Initialize UATP client.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * // Local development
135
+ * const client = new UATP();
136
+ *
137
+ * // Production
138
+ * const client = new UATP({
139
+ * apiKey: 'your-api-key',
140
+ * baseUrl: 'https://api.uatp.io'
141
+ * });
142
+ * ```
143
+ */
144
+ constructor(config = {}) {
145
+ this.apiKey = config.apiKey;
146
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
147
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
148
+ if (this.baseUrl === DEFAULT_BASE_URL) {
149
+ console.warn("UATP client using localhost - set baseUrl for production");
150
+ }
151
+ }
152
+ /**
153
+ * Create a cryptographically certified capsule for an AI decision.
154
+ *
155
+ * ZERO-TRUST: Private key NEVER leaves your device.
156
+ * - All signing happens locally using Ed25519
157
+ * - Only the content hash is sent to UATP for timestamping
158
+ * - Capsules can be verified independently without UATP
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const result = await client.certify({
163
+ * task: 'Loan decision',
164
+ * decision: 'Approved for $50,000 at 5.2% APR',
165
+ * reasoning: [
166
+ * { step: 1, thought: 'Credit score 720 (excellent)', confidence: 0.95 },
167
+ * { step: 2, thought: 'Debt-to-income 0.28 (acceptable)', confidence: 0.90 }
168
+ * ]
169
+ * });
170
+ *
171
+ * console.log(result.capsuleId);
172
+ * console.log(result.signature);
173
+ * ```
174
+ */
175
+ async certify(options) {
176
+ const {
177
+ task,
178
+ decision,
179
+ reasoning,
180
+ passphrase,
181
+ deviceBound = true,
182
+ confidence,
183
+ metadata = {},
184
+ requestTimestamp = true,
185
+ storeOnServer = false
186
+ } = options;
187
+ if (!task || typeof task !== "string") {
188
+ throw new Error("task must be a non-empty string");
189
+ }
190
+ if (!decision || typeof decision !== "string") {
191
+ throw new Error("decision must be a non-empty string");
192
+ }
193
+ if (!reasoning || !Array.isArray(reasoning) || reasoning.length === 0) {
194
+ throw new Error("reasoning must be a non-empty array");
195
+ }
196
+ let signingPassphrase;
197
+ if (passphrase) {
198
+ if (passphrase.length < 8) {
199
+ throw new Error("passphrase must be at least 8 characters");
200
+ }
201
+ signingPassphrase = passphrase;
202
+ } else if (deviceBound) {
203
+ console.warn(
204
+ "Using device-bound passphrase (DEVELOPMENT ONLY). For production, provide an explicit passphrase."
205
+ );
206
+ signingPassphrase = deriveDevicePassphrase();
207
+ } else {
208
+ throw new Error("Either provide a passphrase or set deviceBound=true");
209
+ }
210
+ const calculatedConfidence = confidence ?? reasoning.reduce((sum, step) => sum + (step.confidence || 0.5), 0) / reasoning.length;
211
+ const content = {
212
+ task,
213
+ decision,
214
+ reasoning_chain: reasoning,
215
+ confidence: calculatedConfidence,
216
+ metadata: {
217
+ ...metadata,
218
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
219
+ sdk_version: SDK_VERSION,
220
+ signing_mode: "local"
221
+ }
222
+ };
223
+ const keyPair = deriveKeyPair(signingPassphrase);
224
+ const signed = createSignedCapsule(content, keyPair);
225
+ if (requestTimestamp) {
226
+ try {
227
+ const response = await this.fetch("/timestamp", {
228
+ method: "POST",
229
+ body: JSON.stringify({ hash: signed.contentHash })
230
+ });
231
+ if (response.ok) {
232
+ const tsData = await response.json();
233
+ signed.timestampToken = tsData.rfc3161;
234
+ signed.timestampTsa = tsData.tsa;
235
+ }
236
+ } catch (error) {
237
+ console.warn(
238
+ "Could not obtain timestamp. Capsule is still valid, just without external timestamp.",
239
+ error
240
+ );
241
+ }
242
+ }
243
+ if (storeOnServer) {
244
+ try {
245
+ const capsuleData = {
246
+ ...signed,
247
+ type: "reasoning_trace",
248
+ payload: content
249
+ };
250
+ const response = await this.fetch("/capsules/store", {
251
+ method: "POST",
252
+ body: JSON.stringify(capsuleData)
253
+ });
254
+ if (response.ok) {
255
+ signed.serverStored = true;
256
+ signed.proofUrl = `${this.baseUrl}/capsules/${signed.capsuleId}/verify`;
257
+ }
258
+ } catch (error) {
259
+ console.warn("Could not store on server. Capsule is still valid locally.", error);
260
+ signed.serverStored = false;
261
+ }
262
+ }
263
+ return signed;
264
+ }
265
+ /**
266
+ * Retrieve full cryptographic proof for a capsule.
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * const proof = await client.getProof('cap_abc123');
271
+ * console.log(proof.payload.task);
272
+ * ```
273
+ */
274
+ async getProof(capsuleId) {
275
+ const response = await this.fetch(`/capsules/${capsuleId}`);
276
+ if (!response.ok) {
277
+ if (response.status === 404) {
278
+ throw new Error(`Capsule ${capsuleId} not found`);
279
+ }
280
+ throw new Error(`Failed to retrieve proof: ${response.status}`);
281
+ }
282
+ const data = await response.json();
283
+ const capsule = data.capsule || data;
284
+ return {
285
+ capsuleId: capsule.capsule_id || capsule.id || capsuleId,
286
+ capsuleType: capsule.type || capsule.capsule_type || "unknown",
287
+ status: capsule.status || "unknown",
288
+ timestamp: new Date(capsule.timestamp),
289
+ payload: capsule.payload || {},
290
+ rawData: data
291
+ };
292
+ }
293
+ /**
294
+ * List capsules from the server.
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * const capsules = await client.listCapsules(10);
299
+ * for (const cap of capsules) {
300
+ * console.log(cap.capsuleId, cap.status);
301
+ * }
302
+ * ```
303
+ */
304
+ async listCapsules(limit = 10) {
305
+ const response = await this.fetch(`/capsules?demo_mode=false&per_page=${limit}`);
306
+ if (!response.ok) {
307
+ throw new Error(`Failed to list capsules: ${response.status}`);
308
+ }
309
+ const data = await response.json();
310
+ return (data.capsules || []).slice(0, limit).map((item) => ({
311
+ capsuleId: item.capsule_id,
312
+ capsuleType: item.type || "unknown",
313
+ status: item.status || "unknown",
314
+ timestamp: new Date(item.timestamp),
315
+ payload: item.payload || {},
316
+ rawData: item
317
+ }));
318
+ }
319
+ /**
320
+ * Verify a capsule locally without server.
321
+ *
322
+ * This can be done by anyone - no UATP infrastructure required.
323
+ * Only uses cryptographic data embedded in the capsule.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * const result = client.verifyLocal(capsule);
328
+ * if (result.valid) {
329
+ * console.log('Capsule is authentic!');
330
+ * }
331
+ * ```
332
+ */
333
+ verifyLocal(capsule) {
334
+ return verifyCapsule(capsule);
335
+ }
336
+ /**
337
+ * Record the actual outcome of an AI decision (ground truth).
338
+ *
339
+ * @example
340
+ * ```typescript
341
+ * await client.recordOutcome('cap_abc123', {
342
+ * result: 'successful',
343
+ * aiWasCorrect: true,
344
+ * financialImpact: 2500,
345
+ * notes: 'Loan fully paid on time'
346
+ * });
347
+ * ```
348
+ */
349
+ async recordOutcome(capsuleId, outcome) {
350
+ const params = new URLSearchParams();
351
+ if (outcome.result) params.set("outcome_status", outcome.result);
352
+ if (outcome.notes) params.set("notes", outcome.notes);
353
+ if (outcome.customerSatisfaction) {
354
+ params.set("rating", String(outcome.customerSatisfaction));
355
+ }
356
+ const response = await this.fetch(
357
+ `/capsules/${capsuleId}/outcome?${params.toString()}`,
358
+ { method: "POST" }
359
+ );
360
+ if (response.status === 401) {
361
+ throw new Error("Authentication required. Outcome tracking requires a valid API key.");
362
+ }
363
+ if (response.status === 404) {
364
+ throw new Error(`Capsule ${capsuleId} not found`);
365
+ }
366
+ if (!response.ok) {
367
+ throw new Error(`Failed to record outcome: ${response.status}`);
368
+ }
369
+ return true;
370
+ }
371
+ /**
372
+ * Internal fetch wrapper with timeout and headers.
373
+ */
374
+ async fetch(path, options = {}) {
375
+ const controller = new AbortController();
376
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
377
+ try {
378
+ const response = await fetch(`${this.baseUrl}${path}`, {
379
+ ...options,
380
+ signal: controller.signal,
381
+ headers: {
382
+ "Content-Type": "application/json",
383
+ "User-Agent": `uatp-typescript-sdk/${SDK_VERSION}`,
384
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
385
+ ...options.headers
386
+ }
387
+ });
388
+ return response;
389
+ } finally {
390
+ clearTimeout(timeoutId);
391
+ }
392
+ }
393
+ };
394
+ export {
395
+ UATP,
396
+ bytesToHex,
397
+ canonicalize,
398
+ createSignedCapsule,
399
+ deriveDevicePassphrase,
400
+ deriveKeyPair,
401
+ hashContent,
402
+ hexToBytes,
403
+ sign2 as sign,
404
+ verify2 as verify,
405
+ verifyCapsule
406
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@coolwithakay/uatp",
3
+ "version": "0.1.0",
4
+ "description": "Cryptographic audit trails for AI decisions",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "test": "vitest",
22
+ "lint": "eslint src/",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "uatp",
28
+ "ai",
29
+ "audit",
30
+ "cryptography",
31
+ "ed25519",
32
+ "signatures",
33
+ "reasoning",
34
+ "compliance"
35
+ ],
36
+ "author": "UATP Team",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/KayronCalloway/uatp.git",
41
+ "directory": "sdk/typescript"
42
+ },
43
+ "homepage": "https://github.com/KayronCalloway/uatp#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/KayronCalloway/uatp/issues"
46
+ },
47
+ "dependencies": {
48
+ "@noble/ed25519": "^2.1.0",
49
+ "@noble/hashes": "^1.4.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.0.0",
53
+ "tsup": "^8.0.0",
54
+ "typescript": "^5.0.0",
55
+ "vitest": "^1.0.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }