atlast-ecp-ts 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 +159 -0
- package/dist/batch.d.ts +5 -0
- package/dist/batch.js +64 -0
- package/dist/crypto.d.ts +15 -0
- package/dist/crypto.js +78 -0
- package/dist/identity.d.ts +7 -0
- package/dist/identity.js +44 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +45 -0
- package/dist/record.d.ts +20 -0
- package/dist/record.js +48 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +36 -0
- package/dist/track.d.ts +20 -0
- package/dist/track.js +60 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +5 -0
- package/dist/wrap.d.ts +18 -0
- package/dist/wrap.js +83 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @atlast/sdk
|
|
2
|
+
|
|
3
|
+
**ATLAST Evidence Chain Protocol (ECP) SDK for TypeScript/Node.js**
|
|
4
|
+
|
|
5
|
+
> At last, trust for the Agent economy.
|
|
6
|
+
|
|
7
|
+
Track your AI agent's work with cryptographic evidence chains. Every LLM call, tool use, and decision is recorded, hashed, and chained — creating an immutable audit trail.
|
|
8
|
+
|
|
9
|
+
Privacy-first: only SHA-256 hashes are stored. Content never leaves your device.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @atlast/sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Layer 1 — Wrap your OpenAI client (5 lines)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { wrap } from '@atlast/sdk';
|
|
23
|
+
import OpenAI from 'openai';
|
|
24
|
+
|
|
25
|
+
const client = wrap(new OpenAI(), { agentId: 'my-agent' });
|
|
26
|
+
|
|
27
|
+
// All chat.completions.create() calls are now automatically tracked
|
|
28
|
+
const response = await client.chat.completions.create({
|
|
29
|
+
model: 'gpt-4',
|
|
30
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
31
|
+
});
|
|
32
|
+
// → ECP record saved to ~/.ecp/records/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Track custom functions
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { track } from '@atlast/sdk';
|
|
39
|
+
|
|
40
|
+
const myAgentStep = track('my-agent', async (query: string) => {
|
|
41
|
+
// Your agent logic here
|
|
42
|
+
return await processQuery(query);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await myAgentStep('What is the weather?');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Manual record creation
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createRecord, storeRecord } from '@atlast/sdk';
|
|
52
|
+
|
|
53
|
+
const record = createRecord({
|
|
54
|
+
agentId: 'my-agent',
|
|
55
|
+
input: 'user query',
|
|
56
|
+
output: 'agent response',
|
|
57
|
+
model: 'claude-sonnet-4-20250514',
|
|
58
|
+
latencyMs: 1234,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
storeRecord('my-agent', record);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Upload to ATLAST Backend
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { uploadBatch } from '@atlast/sdk';
|
|
68
|
+
|
|
69
|
+
// Upload batched records (merkle root + record hashes)
|
|
70
|
+
await uploadBatch({
|
|
71
|
+
agentId: 'my-agent',
|
|
72
|
+
apiUrl: 'https://api.llachat.com', // or set ATLAST_API_URL env
|
|
73
|
+
apiKey: 'your-agent-api-key', // or set ATLAST_API_KEY env
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How It Works
|
|
78
|
+
|
|
79
|
+
1. **Record**: Each LLM call creates an ECP record with input/output SHA-256 hashes (content stays local)
|
|
80
|
+
2. **Chain**: Records are linked via hash chains, forming an immutable evidence trail
|
|
81
|
+
3. **Sign**: Records are signed with your agent's Ed25519 key (auto-generated DID)
|
|
82
|
+
4. **Upload**: Batch upload merkle roots to ATLAST backend via `POST /v1/batches` with `X-Agent-Key` header
|
|
83
|
+
|
|
84
|
+
## API Reference
|
|
85
|
+
|
|
86
|
+
### Core
|
|
87
|
+
|
|
88
|
+
| Export | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `wrap(client, opts)` | Wrap OpenAI-compatible client for automatic ECP tracking |
|
|
91
|
+
| `track(agentId, fn)` | Track a function's execution as an ECP record |
|
|
92
|
+
| `createRecord(opts)` | Create an ECP record manually |
|
|
93
|
+
|
|
94
|
+
### Storage
|
|
95
|
+
|
|
96
|
+
| Export | Description |
|
|
97
|
+
|--------|-------------|
|
|
98
|
+
| `storeRecord(id, rec)` | Store a record locally (JSONL in `~/.ecp/records/`) |
|
|
99
|
+
| `loadRecords(agentId)` | Load stored records for an agent |
|
|
100
|
+
| `collectBatch(agentId)` | Collect records into a batch for upload |
|
|
101
|
+
|
|
102
|
+
### Identity & Crypto
|
|
103
|
+
|
|
104
|
+
| Export | Description |
|
|
105
|
+
|--------|-------------|
|
|
106
|
+
| `loadOrCreateIdentity(id)` | Load or create agent DID + Ed25519 keys |
|
|
107
|
+
| `getIdentity(id)` | Get existing identity (returns null if none) |
|
|
108
|
+
| `sha256(data)` | SHA-256 hash with `sha256:` prefix |
|
|
109
|
+
| `hashRecord(record)` | Compute canonical hash of an ECP record |
|
|
110
|
+
| `buildMerkleRoot(hashes)` | Build merkle root from record hashes |
|
|
111
|
+
| `generateDID(publicKey)` | Generate `did:ecp:` identifier from public key |
|
|
112
|
+
| `verifySignature(data, sig, pubkey)` | Verify Ed25519 signature |
|
|
113
|
+
|
|
114
|
+
### Network
|
|
115
|
+
|
|
116
|
+
| Export | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `uploadBatch(config)` | Upload batch to ATLAST backend (`POST /v1/batches`) |
|
|
119
|
+
|
|
120
|
+
### Types
|
|
121
|
+
|
|
122
|
+
| Export | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `ECPRecord` | ECP record type definition |
|
|
125
|
+
| `ECPIdentity` | Agent identity (DID + keys) |
|
|
126
|
+
| `ATLASTConfig` | SDK configuration options |
|
|
127
|
+
| `CreateRecordOptions` | Options for `createRecord()` |
|
|
128
|
+
| `WrapOptions` | Options for `wrap()` |
|
|
129
|
+
| `TrackOptions` | Options for `track()` |
|
|
130
|
+
| `BatchUploadRequest` | Batch upload request shape |
|
|
131
|
+
| `BatchUploadResponse` | Batch upload response shape |
|
|
132
|
+
|
|
133
|
+
## Environment Variables
|
|
134
|
+
|
|
135
|
+
| Variable | Default | Description |
|
|
136
|
+
|----------|---------|-------------|
|
|
137
|
+
| `ATLAST_ECP_DIR` | `~/.ecp` | Directory for ECP data (identity, records) |
|
|
138
|
+
| `ATLAST_API_URL` | `https://api.llachat.com` | Backend API endpoint for batch upload |
|
|
139
|
+
| `ATLAST_API_KEY` | — | Agent API key (sent as `X-Agent-Key` header) |
|
|
140
|
+
|
|
141
|
+
## ECP Compatibility
|
|
142
|
+
|
|
143
|
+
This SDK produces ECP v0.1 records (nested `step` format). It is compatible with the Python SDK (`atlast-ecp`) which also supports ECP v1.0 (flat format). Both formats are valid and can coexist.
|
|
144
|
+
|
|
145
|
+
For zero-code recording via transparent proxy, see the Python SDK:
|
|
146
|
+
```bash
|
|
147
|
+
pip install atlast-ecp[proxy]
|
|
148
|
+
atlast run python my_agent.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Links
|
|
152
|
+
|
|
153
|
+
- [ECP Specification](https://github.com/willau95/atlast-ecp/blob/main/ECP-SPEC.md)
|
|
154
|
+
- [Python SDK](https://pypi.org/project/atlast-ecp/)
|
|
155
|
+
- [ATLAST Protocol](https://llachat.com)
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/dist/batch.d.ts
ADDED
package/dist/batch.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Batch — collect records and upload to backend
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadBatch = uploadBatch;
|
|
7
|
+
const crypto_1 = require("./crypto");
|
|
8
|
+
const storage_1 = require("./storage");
|
|
9
|
+
const identity_1 = require("./identity");
|
|
10
|
+
const DEFAULT_API_URL = 'https://api.llachat.com';
|
|
11
|
+
async function uploadBatch(config) {
|
|
12
|
+
const identity = (0, identity_1.loadOrCreateIdentity)(config.agentId);
|
|
13
|
+
const { records, hashes } = (0, storage_1.collectBatch)(config.agentId, config.batchSize || 100);
|
|
14
|
+
if (records.length === 0) {
|
|
15
|
+
return { batch_id: '', status: 'empty', message: 'No records to upload', attestation_uid: undefined };
|
|
16
|
+
}
|
|
17
|
+
const merkleRoot = (0, crypto_1.buildMerkleRoot)(hashes);
|
|
18
|
+
const sig = (0, crypto_1.signData)(identity.private_key, merkleRoot);
|
|
19
|
+
// Compute flag counts
|
|
20
|
+
const flagCounts = {};
|
|
21
|
+
let totalLatency = 0;
|
|
22
|
+
let latencyCount = 0;
|
|
23
|
+
for (const r of records) {
|
|
24
|
+
for (const f of r.flags) {
|
|
25
|
+
flagCounts[f] = (flagCounts[f] || 0) + 1;
|
|
26
|
+
}
|
|
27
|
+
if (r.latency_ms) {
|
|
28
|
+
totalLatency += r.latency_ms;
|
|
29
|
+
latencyCount++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const avgLatency = latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0;
|
|
33
|
+
const payload = {
|
|
34
|
+
agent_did: identity.did,
|
|
35
|
+
merkle_root: merkleRoot,
|
|
36
|
+
record_count: records.length,
|
|
37
|
+
avg_latency_ms: avgLatency,
|
|
38
|
+
batch_ts: Date.now(),
|
|
39
|
+
sig,
|
|
40
|
+
ecp_version: '0.5',
|
|
41
|
+
flag_counts: flagCounts,
|
|
42
|
+
record_hashes: records.map((r) => ({
|
|
43
|
+
id: r.id,
|
|
44
|
+
hash: r.chain.hash,
|
|
45
|
+
flags: r.flags,
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
const apiUrl = config.apiUrl || process.env.ATLAST_API_URL || DEFAULT_API_URL;
|
|
49
|
+
const apiKey = config.apiKey || process.env.ATLAST_API_KEY;
|
|
50
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
51
|
+
if (apiKey) {
|
|
52
|
+
headers['X-Agent-Key'] = apiKey;
|
|
53
|
+
}
|
|
54
|
+
const resp = await fetch(`${apiUrl}/v1/batches`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers,
|
|
57
|
+
body: JSON.stringify(payload),
|
|
58
|
+
});
|
|
59
|
+
if (!resp.ok) {
|
|
60
|
+
const errText = await resp.text();
|
|
61
|
+
throw new Error(`Batch upload failed (${resp.status}): ${errText}`);
|
|
62
|
+
}
|
|
63
|
+
return (await resp.json());
|
|
64
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Crypto — hashing, signing, merkle tree
|
|
3
|
+
*/
|
|
4
|
+
export declare function sha256(data: string): string;
|
|
5
|
+
export declare function hashRecord(record: Record<string, unknown>): string;
|
|
6
|
+
export declare function generateId(prefix?: string): string;
|
|
7
|
+
export declare function generateDID(): string;
|
|
8
|
+
export declare function buildMerkleRoot(hashes: string[]): string;
|
|
9
|
+
export interface KeyPair {
|
|
10
|
+
publicKey: string;
|
|
11
|
+
privateKey: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function generateKeyPair(): KeyPair;
|
|
14
|
+
export declare function signData(privateKeyHex: string, data: string): string;
|
|
15
|
+
export declare function verifySignature(publicKeyHex: string, data: string, signatureHex: string): boolean;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Crypto — hashing, signing, merkle tree
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sha256 = sha256;
|
|
7
|
+
exports.hashRecord = hashRecord;
|
|
8
|
+
exports.generateId = generateId;
|
|
9
|
+
exports.generateDID = generateDID;
|
|
10
|
+
exports.buildMerkleRoot = buildMerkleRoot;
|
|
11
|
+
exports.generateKeyPair = generateKeyPair;
|
|
12
|
+
exports.signData = signData;
|
|
13
|
+
exports.verifySignature = verifySignature;
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
function sha256(data) {
|
|
16
|
+
return (0, crypto_1.createHash)('sha256').update(data).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
function hashRecord(record) {
|
|
19
|
+
// Zero out chain.hash and sig before hashing
|
|
20
|
+
const clone = JSON.parse(JSON.stringify(record));
|
|
21
|
+
if (clone.chain)
|
|
22
|
+
clone.chain.hash = '';
|
|
23
|
+
delete clone.sig;
|
|
24
|
+
const canonical = JSON.stringify(clone, Object.keys(clone).sort());
|
|
25
|
+
return sha256(canonical);
|
|
26
|
+
}
|
|
27
|
+
function generateId(prefix = 'rec') {
|
|
28
|
+
return `${prefix}_${(0, crypto_1.randomBytes)(6).toString('hex')}`;
|
|
29
|
+
}
|
|
30
|
+
function generateDID() {
|
|
31
|
+
const id = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
32
|
+
return `did:ecp:${id}`;
|
|
33
|
+
}
|
|
34
|
+
function buildMerkleRoot(hashes) {
|
|
35
|
+
if (hashes.length === 0)
|
|
36
|
+
return sha256('');
|
|
37
|
+
if (hashes.length === 1)
|
|
38
|
+
return hashes[0];
|
|
39
|
+
const next = [];
|
|
40
|
+
for (let i = 0; i < hashes.length; i += 2) {
|
|
41
|
+
const left = hashes[i];
|
|
42
|
+
const right = i + 1 < hashes.length ? hashes[i + 1] : left;
|
|
43
|
+
next.push(sha256(left + right));
|
|
44
|
+
}
|
|
45
|
+
return buildMerkleRoot(next);
|
|
46
|
+
}
|
|
47
|
+
function generateKeyPair() {
|
|
48
|
+
const { publicKey, privateKey } = (0, crypto_1.generateKeyPairSync)('ed25519', {
|
|
49
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
50
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
publicKey: publicKey.toString('hex'),
|
|
54
|
+
privateKey: privateKey.toString('hex'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function signData(privateKeyHex, data) {
|
|
58
|
+
const keyObj = (0, crypto_1.createPrivateKey)({
|
|
59
|
+
key: Buffer.from(privateKeyHex, 'hex'),
|
|
60
|
+
format: 'der',
|
|
61
|
+
type: 'pkcs8',
|
|
62
|
+
});
|
|
63
|
+
const signature = (0, crypto_1.sign)(null, Buffer.from(data), keyObj);
|
|
64
|
+
return signature.toString('hex');
|
|
65
|
+
}
|
|
66
|
+
function verifySignature(publicKeyHex, data, signatureHex) {
|
|
67
|
+
try {
|
|
68
|
+
const keyObj = (0, crypto_1.createPublicKey)({
|
|
69
|
+
key: Buffer.from(publicKeyHex, 'hex'),
|
|
70
|
+
format: 'der',
|
|
71
|
+
type: 'spki',
|
|
72
|
+
});
|
|
73
|
+
return (0, crypto_1.verify)(null, Buffer.from(data), keyObj, Buffer.from(signatureHex, 'hex'));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Identity — DID management and key storage
|
|
3
|
+
*/
|
|
4
|
+
import type { ECPIdentity } from './types';
|
|
5
|
+
export declare function getECPDir(): string;
|
|
6
|
+
export declare function loadOrCreateIdentity(agentId: string): ECPIdentity;
|
|
7
|
+
export declare function getIdentity(agentId: string): ECPIdentity | null;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Identity — DID management and key storage
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getECPDir = getECPDir;
|
|
7
|
+
exports.loadOrCreateIdentity = loadOrCreateIdentity;
|
|
8
|
+
exports.getIdentity = getIdentity;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const crypto_1 = require("./crypto");
|
|
12
|
+
const DEFAULT_ECP_DIR = (0, path_1.join)(process.env.HOME || '~', '.ecp');
|
|
13
|
+
function getECPDir() {
|
|
14
|
+
return process.env.ATLAST_ECP_DIR || DEFAULT_ECP_DIR;
|
|
15
|
+
}
|
|
16
|
+
function identityPath(agentId) {
|
|
17
|
+
const dir = (0, path_1.join)(getECPDir(), 'agents', agentId);
|
|
18
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
19
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
20
|
+
return (0, path_1.join)(dir, 'identity.json');
|
|
21
|
+
}
|
|
22
|
+
function loadOrCreateIdentity(agentId) {
|
|
23
|
+
const path = identityPath(agentId);
|
|
24
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
25
|
+
const data = JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
const keyPair = (0, crypto_1.generateKeyPair)();
|
|
29
|
+
const identity = {
|
|
30
|
+
did: (0, crypto_1.generateDID)(),
|
|
31
|
+
agent_id: agentId,
|
|
32
|
+
public_key: keyPair.publicKey,
|
|
33
|
+
private_key: keyPair.privateKey,
|
|
34
|
+
created_at: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(identity, null, 2));
|
|
37
|
+
return identity;
|
|
38
|
+
}
|
|
39
|
+
function getIdentity(agentId) {
|
|
40
|
+
const path = identityPath(agentId);
|
|
41
|
+
if (!(0, fs_1.existsSync)(path))
|
|
42
|
+
return null;
|
|
43
|
+
return JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlast/sdk — ATLAST Evidence Chain Protocol SDK for TypeScript/Node.js
|
|
3
|
+
*
|
|
4
|
+
* Layer 1 integration: 5 lines of code to track your AI agent's work.
|
|
5
|
+
*
|
|
6
|
+
* Quick Start:
|
|
7
|
+
* import { wrap } from '@atlast/sdk';
|
|
8
|
+
* import OpenAI from 'openai';
|
|
9
|
+
*
|
|
10
|
+
* const client = wrap(new OpenAI(), { agentId: 'my-agent' });
|
|
11
|
+
* // All LLM calls are now tracked with evidence chains
|
|
12
|
+
*/
|
|
13
|
+
export type { ECPRecord, ECPIdentity, BatchUploadRequest, BatchUploadResponse, ATLASTConfig } from './types';
|
|
14
|
+
export { loadOrCreateIdentity, getIdentity } from './identity';
|
|
15
|
+
export { createRecord, setChainHead, getChainHead } from './record';
|
|
16
|
+
export type { CreateRecordOptions } from './record';
|
|
17
|
+
export { storeRecord, loadRecords, collectBatch } from './storage';
|
|
18
|
+
export { uploadBatch } from './batch';
|
|
19
|
+
export { wrap } from './wrap';
|
|
20
|
+
export type { WrapOptions } from './wrap';
|
|
21
|
+
export { track } from './track';
|
|
22
|
+
export type { TrackOptions } from './track';
|
|
23
|
+
export { sha256, hashRecord, buildMerkleRoot, generateDID, verifySignature } from './crypto';
|
|
24
|
+
export type { LocalConfig } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @atlast/sdk — ATLAST Evidence Chain Protocol SDK for TypeScript/Node.js
|
|
4
|
+
*
|
|
5
|
+
* Layer 1 integration: 5 lines of code to track your AI agent's work.
|
|
6
|
+
*
|
|
7
|
+
* Quick Start:
|
|
8
|
+
* import { wrap } from '@atlast/sdk';
|
|
9
|
+
* import OpenAI from 'openai';
|
|
10
|
+
*
|
|
11
|
+
* const client = wrap(new OpenAI(), { agentId: 'my-agent' });
|
|
12
|
+
* // All LLM calls are now tracked with evidence chains
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.verifySignature = exports.generateDID = exports.buildMerkleRoot = exports.hashRecord = exports.sha256 = exports.track = exports.wrap = exports.uploadBatch = exports.collectBatch = exports.loadRecords = exports.storeRecord = exports.getChainHead = exports.setChainHead = exports.createRecord = exports.getIdentity = exports.loadOrCreateIdentity = void 0;
|
|
16
|
+
// Identity (DID management)
|
|
17
|
+
var identity_1 = require("./identity");
|
|
18
|
+
Object.defineProperty(exports, "loadOrCreateIdentity", { enumerable: true, get: function () { return identity_1.loadOrCreateIdentity; } });
|
|
19
|
+
Object.defineProperty(exports, "getIdentity", { enumerable: true, get: function () { return identity_1.getIdentity; } });
|
|
20
|
+
// Record creation
|
|
21
|
+
var record_1 = require("./record");
|
|
22
|
+
Object.defineProperty(exports, "createRecord", { enumerable: true, get: function () { return record_1.createRecord; } });
|
|
23
|
+
Object.defineProperty(exports, "setChainHead", { enumerable: true, get: function () { return record_1.setChainHead; } });
|
|
24
|
+
Object.defineProperty(exports, "getChainHead", { enumerable: true, get: function () { return record_1.getChainHead; } });
|
|
25
|
+
// Storage
|
|
26
|
+
var storage_1 = require("./storage");
|
|
27
|
+
Object.defineProperty(exports, "storeRecord", { enumerable: true, get: function () { return storage_1.storeRecord; } });
|
|
28
|
+
Object.defineProperty(exports, "loadRecords", { enumerable: true, get: function () { return storage_1.loadRecords; } });
|
|
29
|
+
Object.defineProperty(exports, "collectBatch", { enumerable: true, get: function () { return storage_1.collectBatch; } });
|
|
30
|
+
// Batch upload
|
|
31
|
+
var batch_1 = require("./batch");
|
|
32
|
+
Object.defineProperty(exports, "uploadBatch", { enumerable: true, get: function () { return batch_1.uploadBatch; } });
|
|
33
|
+
// Wrap (Layer 1 — transparent client wrapper)
|
|
34
|
+
var wrap_1 = require("./wrap");
|
|
35
|
+
Object.defineProperty(exports, "wrap", { enumerable: true, get: function () { return wrap_1.wrap; } });
|
|
36
|
+
// Track (decorator-style function tracking)
|
|
37
|
+
var track_1 = require("./track");
|
|
38
|
+
Object.defineProperty(exports, "track", { enumerable: true, get: function () { return track_1.track; } });
|
|
39
|
+
// Crypto utilities
|
|
40
|
+
var crypto_1 = require("./crypto");
|
|
41
|
+
Object.defineProperty(exports, "sha256", { enumerable: true, get: function () { return crypto_1.sha256; } });
|
|
42
|
+
Object.defineProperty(exports, "hashRecord", { enumerable: true, get: function () { return crypto_1.hashRecord; } });
|
|
43
|
+
Object.defineProperty(exports, "buildMerkleRoot", { enumerable: true, get: function () { return crypto_1.buildMerkleRoot; } });
|
|
44
|
+
Object.defineProperty(exports, "generateDID", { enumerable: true, get: function () { return crypto_1.generateDID; } });
|
|
45
|
+
Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return crypto_1.verifySignature; } });
|
package/dist/record.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Record — create and chain evidence records
|
|
3
|
+
*/
|
|
4
|
+
import type { ECPRecord, ECPIdentity } from './types';
|
|
5
|
+
export declare function setChainHead(hash: string): void;
|
|
6
|
+
export declare function getChainHead(): string;
|
|
7
|
+
export interface CreateRecordOptions {
|
|
8
|
+
agentId: string;
|
|
9
|
+
stepType?: ECPRecord['step_type'];
|
|
10
|
+
model?: string;
|
|
11
|
+
input: string;
|
|
12
|
+
output: string;
|
|
13
|
+
tokensIn?: number;
|
|
14
|
+
tokensOut?: number;
|
|
15
|
+
latencyMs?: number;
|
|
16
|
+
flags?: string[];
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
identity?: ECPIdentity;
|
|
19
|
+
}
|
|
20
|
+
export declare function createRecord(opts: CreateRecordOptions): ECPRecord;
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Record — create and chain evidence records
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setChainHead = setChainHead;
|
|
7
|
+
exports.getChainHead = getChainHead;
|
|
8
|
+
exports.createRecord = createRecord;
|
|
9
|
+
const crypto_1 = require("./crypto");
|
|
10
|
+
let lastHash = 'genesis';
|
|
11
|
+
function setChainHead(hash) {
|
|
12
|
+
lastHash = hash;
|
|
13
|
+
}
|
|
14
|
+
function getChainHead() {
|
|
15
|
+
return lastHash;
|
|
16
|
+
}
|
|
17
|
+
function createRecord(opts) {
|
|
18
|
+
const record = {
|
|
19
|
+
id: (0, crypto_1.generateId)('rec'),
|
|
20
|
+
ts: Date.now(),
|
|
21
|
+
agent_id: opts.agentId,
|
|
22
|
+
step_type: opts.stepType || 'llm_call',
|
|
23
|
+
model: opts.model,
|
|
24
|
+
input_hash: (0, crypto_1.sha256)(opts.input),
|
|
25
|
+
output_hash: (0, crypto_1.sha256)(opts.output),
|
|
26
|
+
tokens_in: opts.tokensIn,
|
|
27
|
+
tokens_out: opts.tokensOut,
|
|
28
|
+
latency_ms: opts.latencyMs,
|
|
29
|
+
flags: opts.flags || [],
|
|
30
|
+
metadata: opts.metadata,
|
|
31
|
+
chain: {
|
|
32
|
+
prev: lastHash,
|
|
33
|
+
hash: '',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// Detect signals
|
|
37
|
+
if (opts.latencyMs && opts.latencyMs > 5000) {
|
|
38
|
+
record.flags.push('high_latency');
|
|
39
|
+
}
|
|
40
|
+
// Compute chain hash
|
|
41
|
+
record.chain.hash = (0, crypto_1.hashRecord)(record);
|
|
42
|
+
// Sign if identity provided
|
|
43
|
+
if (opts.identity) {
|
|
44
|
+
record.sig = (0, crypto_1.signData)(opts.identity.private_key, record.chain.hash);
|
|
45
|
+
}
|
|
46
|
+
lastHash = record.chain.hash;
|
|
47
|
+
return record;
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Storage — local record storage (JSONL)
|
|
3
|
+
*/
|
|
4
|
+
import type { ECPRecord } from './types';
|
|
5
|
+
export declare function storeRecord(agentId: string, record: ECPRecord): void;
|
|
6
|
+
export declare function loadRecords(agentId: string): ECPRecord[];
|
|
7
|
+
export declare function collectBatch(agentId: string, maxRecords?: number): {
|
|
8
|
+
records: ECPRecord[];
|
|
9
|
+
hashes: string[];
|
|
10
|
+
};
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Storage — local record storage (JSONL)
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.storeRecord = storeRecord;
|
|
7
|
+
exports.loadRecords = loadRecords;
|
|
8
|
+
exports.collectBatch = collectBatch;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const identity_1 = require("./identity");
|
|
12
|
+
function recordsPath(agentId) {
|
|
13
|
+
const dir = (0, path_1.join)((0, identity_1.getECPDir)(), 'agents', agentId);
|
|
14
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
15
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
16
|
+
return (0, path_1.join)(dir, 'records.jsonl');
|
|
17
|
+
}
|
|
18
|
+
function storeRecord(agentId, record) {
|
|
19
|
+
const path = recordsPath(agentId);
|
|
20
|
+
(0, fs_1.appendFileSync)(path, JSON.stringify(record) + '\n');
|
|
21
|
+
}
|
|
22
|
+
function loadRecords(agentId) {
|
|
23
|
+
const path = recordsPath(agentId);
|
|
24
|
+
if (!(0, fs_1.existsSync)(path))
|
|
25
|
+
return [];
|
|
26
|
+
return (0, fs_1.readFileSync)(path, 'utf-8')
|
|
27
|
+
.trim()
|
|
28
|
+
.split('\n')
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((line) => JSON.parse(line));
|
|
31
|
+
}
|
|
32
|
+
function collectBatch(agentId, maxRecords = 100) {
|
|
33
|
+
const records = loadRecords(agentId).slice(-maxRecords);
|
|
34
|
+
const hashes = records.map((r) => r.chain.hash);
|
|
35
|
+
return { records, hashes };
|
|
36
|
+
}
|
package/dist/track.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Track — decorator-style tracking for agent functions
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { track } from '@atlast/sdk';
|
|
6
|
+
*
|
|
7
|
+
* const myFunction = track('my-agent', async (input: string) => {
|
|
8
|
+
* // ... agent logic
|
|
9
|
+
* return result;
|
|
10
|
+
* });
|
|
11
|
+
*/
|
|
12
|
+
export interface TrackOptions {
|
|
13
|
+
agentId: string;
|
|
14
|
+
stepType?: 'llm_call' | 'tool_call' | 'decision' | 'custom';
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Track a function's execution as an ECP record.
|
|
19
|
+
*/
|
|
20
|
+
export declare function track<TArgs extends unknown[], TResult>(agentIdOrOpts: string | TrackOptions, fn: (...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<TResult>;
|
package/dist/track.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Track — decorator-style tracking for agent functions
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { track } from '@atlast/sdk';
|
|
7
|
+
*
|
|
8
|
+
* const myFunction = track('my-agent', async (input: string) => {
|
|
9
|
+
* // ... agent logic
|
|
10
|
+
* return result;
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.track = track;
|
|
15
|
+
const record_1 = require("./record");
|
|
16
|
+
const storage_1 = require("./storage");
|
|
17
|
+
const identity_1 = require("./identity");
|
|
18
|
+
/**
|
|
19
|
+
* Track a function's execution as an ECP record.
|
|
20
|
+
*/
|
|
21
|
+
function track(agentIdOrOpts, fn) {
|
|
22
|
+
const opts = typeof agentIdOrOpts === 'string' ? { agentId: agentIdOrOpts } : agentIdOrOpts;
|
|
23
|
+
const identity = (0, identity_1.loadOrCreateIdentity)(opts.agentId);
|
|
24
|
+
return async function tracked(...args) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const inputStr = JSON.stringify(args);
|
|
27
|
+
const flags = [];
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
result = await fn(...args);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
flags.push('error');
|
|
34
|
+
const record = (0, record_1.createRecord)({
|
|
35
|
+
agentId: opts.agentId,
|
|
36
|
+
stepType: opts.stepType || 'custom',
|
|
37
|
+
input: inputStr,
|
|
38
|
+
output: `error: ${e.message}`,
|
|
39
|
+
latencyMs: Date.now() - startTime,
|
|
40
|
+
flags,
|
|
41
|
+
metadata: opts.metadata,
|
|
42
|
+
identity,
|
|
43
|
+
});
|
|
44
|
+
(0, storage_1.storeRecord)(opts.agentId, record);
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
const record = (0, record_1.createRecord)({
|
|
48
|
+
agentId: opts.agentId,
|
|
49
|
+
stepType: opts.stepType || 'custom',
|
|
50
|
+
input: inputStr,
|
|
51
|
+
output: JSON.stringify(result),
|
|
52
|
+
latencyMs: Date.now() - startTime,
|
|
53
|
+
flags,
|
|
54
|
+
metadata: opts.metadata,
|
|
55
|
+
identity,
|
|
56
|
+
});
|
|
57
|
+
(0, storage_1.storeRecord)(opts.agentId, record);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Types — Evidence Chain Protocol data structures
|
|
3
|
+
*/
|
|
4
|
+
export interface ECPRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
ts: number;
|
|
7
|
+
agent_id: string;
|
|
8
|
+
step_type: 'llm_call' | 'tool_call' | 'decision' | 'custom';
|
|
9
|
+
model?: string;
|
|
10
|
+
input_hash: string;
|
|
11
|
+
output_hash: string;
|
|
12
|
+
tokens_in?: number;
|
|
13
|
+
tokens_out?: number;
|
|
14
|
+
latency_ms?: number;
|
|
15
|
+
flags: string[];
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
chain: {
|
|
18
|
+
prev: string;
|
|
19
|
+
hash: string;
|
|
20
|
+
};
|
|
21
|
+
sig?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ECPIdentity {
|
|
24
|
+
did: string;
|
|
25
|
+
agent_id: string;
|
|
26
|
+
public_key: string;
|
|
27
|
+
private_key: string;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
export interface BatchUploadRequest {
|
|
31
|
+
agent_did: string;
|
|
32
|
+
merkle_root: string;
|
|
33
|
+
record_count: number;
|
|
34
|
+
avg_latency_ms: number;
|
|
35
|
+
batch_ts: number;
|
|
36
|
+
sig: string;
|
|
37
|
+
ecp_version: string;
|
|
38
|
+
flag_counts?: Record<string, number>;
|
|
39
|
+
record_hashes?: Array<{
|
|
40
|
+
id: string;
|
|
41
|
+
hash: string;
|
|
42
|
+
flags?: string[];
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export interface BatchUploadResponse {
|
|
46
|
+
batch_id: string;
|
|
47
|
+
attestation_uid?: string;
|
|
48
|
+
eas_url?: string;
|
|
49
|
+
status: string;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ATLASTConfig {
|
|
53
|
+
apiUrl?: string;
|
|
54
|
+
apiKey?: string;
|
|
55
|
+
ecpDir?: string;
|
|
56
|
+
agentId: string;
|
|
57
|
+
autoUpload?: boolean;
|
|
58
|
+
batchSize?: number;
|
|
59
|
+
flushIntervalMs?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface LocalConfig {
|
|
62
|
+
agent_did?: string;
|
|
63
|
+
agent_api_key?: string;
|
|
64
|
+
endpoint?: string;
|
|
65
|
+
}
|
package/dist/types.js
ADDED
package/dist/wrap.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATLAST ECP Wrap — transparent LLM client wrapper (Layer 1)
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { wrap } from '@atlast/sdk';
|
|
6
|
+
* const client = wrap(new OpenAI(), { agentId: 'my-agent' });
|
|
7
|
+
* // All chat.completions.create() calls are now tracked
|
|
8
|
+
*/
|
|
9
|
+
export interface WrapOptions {
|
|
10
|
+
agentId: string;
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
autoUpload?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wrap an OpenAI-compatible client to automatically record ECP evidence.
|
|
16
|
+
* Intercepts chat.completions.create() calls.
|
|
17
|
+
*/
|
|
18
|
+
export declare function wrap<T extends object>(client: T, options: WrapOptions): T;
|
package/dist/wrap.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ATLAST ECP Wrap — transparent LLM client wrapper (Layer 1)
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { wrap } from '@atlast/sdk';
|
|
7
|
+
* const client = wrap(new OpenAI(), { agentId: 'my-agent' });
|
|
8
|
+
* // All chat.completions.create() calls are now tracked
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.wrap = wrap;
|
|
12
|
+
const record_1 = require("./record");
|
|
13
|
+
const storage_1 = require("./storage");
|
|
14
|
+
const identity_1 = require("./identity");
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an OpenAI-compatible client to automatically record ECP evidence.
|
|
17
|
+
* Intercepts chat.completions.create() calls.
|
|
18
|
+
*/
|
|
19
|
+
function wrap(client, options) {
|
|
20
|
+
const identity = (0, identity_1.loadOrCreateIdentity)(options.agentId);
|
|
21
|
+
// Deep proxy to intercept chat.completions.create
|
|
22
|
+
return new Proxy(client, {
|
|
23
|
+
get(target, prop, receiver) {
|
|
24
|
+
const value = Reflect.get(target, prop, receiver);
|
|
25
|
+
if (prop === 'chat' && typeof value === 'object' && value !== null) {
|
|
26
|
+
return new Proxy(value, {
|
|
27
|
+
get(chatTarget, chatProp, chatReceiver) {
|
|
28
|
+
const chatValue = Reflect.get(chatTarget, chatProp, chatReceiver);
|
|
29
|
+
if (chatProp === 'completions' && typeof chatValue === 'object' && chatValue !== null) {
|
|
30
|
+
return new Proxy(chatValue, {
|
|
31
|
+
get(compTarget, compProp, compReceiver) {
|
|
32
|
+
const compValue = Reflect.get(compTarget, compProp, compReceiver);
|
|
33
|
+
if (compProp === 'create' && typeof compValue === 'function') {
|
|
34
|
+
return async function wrappedCreate(...args) {
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
const params = args[0];
|
|
37
|
+
const inputStr = JSON.stringify(params?.messages || '');
|
|
38
|
+
let result;
|
|
39
|
+
let error;
|
|
40
|
+
const flags = [];
|
|
41
|
+
try {
|
|
42
|
+
result = await compValue.apply(compTarget, args);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
error = e;
|
|
46
|
+
flags.push('error');
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
const latencyMs = Date.now() - startTime;
|
|
51
|
+
const outputStr = error
|
|
52
|
+
? `error: ${error.message}`
|
|
53
|
+
: JSON.stringify(result?.choices || '');
|
|
54
|
+
const usage = result?.usage;
|
|
55
|
+
const record = (0, record_1.createRecord)({
|
|
56
|
+
agentId: options.agentId,
|
|
57
|
+
stepType: 'llm_call',
|
|
58
|
+
model: params?.model,
|
|
59
|
+
input: inputStr,
|
|
60
|
+
output: outputStr,
|
|
61
|
+
tokensIn: usage?.prompt_tokens,
|
|
62
|
+
tokensOut: usage?.completion_tokens,
|
|
63
|
+
latencyMs,
|
|
64
|
+
flags,
|
|
65
|
+
identity,
|
|
66
|
+
});
|
|
67
|
+
(0, storage_1.storeRecord)(options.agentId, record);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return compValue;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return chatValue;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atlast-ecp-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ATLAST ECP SDK for TypeScript/Node.js — Evidence Chain Protocol for AI agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"atlast",
|
|
17
|
+
"ecp",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"trust",
|
|
20
|
+
"evidence-chain"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/willau95/atlast-sdk-ts"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.5.0",
|
|
29
|
+
"typescript": "^5.4.0",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|