@newtype-ai/nit 0.4.8 → 0.4.10

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 CHANGED
@@ -1,7 +1,20 @@
1
1
  # nit
2
2
 
3
+ [![Node.js 18+](https://img.shields.io/badge/Node.js-18%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org)
4
+ [![npm](https://img.shields.io/npm/v/@newtype-ai/nit?color=cb0000)](https://www.npmjs.com/package/@newtype-ai/nit)
5
+
3
6
  Git for agent identity.
4
7
 
8
+ ```
9
+ _ _ _____
10
+ | \ |"| ___ |_ " _|
11
+ <| \| |> |_"_| | |
12
+ U| |\ |u | | /| |\
13
+ |_| \_| U/| |\u u |_|U
14
+ || \\,-.-,_|___|_,-._// \\_
15
+ (_") (_/ \_)-' '-(_/(__) (__)
16
+ ```
17
+
5
18
  **One identity. Any apps.**
6
19
 
7
20
  - **Self-sovereign** — your keys, your identity. No authority assigns or revokes it
@@ -58,9 +71,12 @@ One keypair, multiple chains. No seed phrases, no extra key management.
58
71
 
59
72
  - **Solana** — your Ed25519 public key *is* your Solana address
60
73
  - **EVM** (Ethereum, BSC, Polygon, Arbitrum, etc.) — deterministic secp256k1 derivation from your Ed25519 seed
74
+ - **Sign & broadcast** — sign transactions and send them to any RPC endpoint
61
75
 
62
76
  ```bash
63
77
  nit status # shows your wallet addresses
78
+ nit sign-tx --chain evm <hash> # sign a transaction
79
+ nit broadcast --chain evm <tx> # broadcast to RPC
64
80
  ```
65
81
 
66
82
  ### Skill resolution
@@ -98,11 +114,20 @@ Pure Node.js builtins. No bloat.
98
114
  | `nit branch [name]` | List branches or create a new one |
99
115
  | `nit checkout <branch>` | Switch branch (overwrites agent-card.json) |
100
116
  | `nit push [--all]` | Push branch(es) to remote |
117
+ | `nit pull [--all]` | Pull branch(es) from remote |
118
+ | `nit reset [target]` | Restore agent-card.json from HEAD or target |
119
+ | `nit show [target]` | Show commit metadata and card content |
101
120
  | `nit sign "msg"` | Sign a message with your Ed25519 key |
102
121
  | `nit sign --login <domain>` | Auto-switch to domain branch + generate login payload |
103
122
  | `nit remote` | Show remote URL and credential status |
104
123
  | `nit remote add <name> <url>` | Add a new remote |
105
124
  | `nit remote set-url <name> <url>` | Change a remote's URL |
125
+ | `nit sign-tx --chain <c> <data>` | Sign transaction data (EVM: 32-byte hash, Solana: message bytes) |
126
+ | `nit broadcast --chain <c> <tx>` | Broadcast signed transaction to configured RPC endpoint |
127
+ | `nit rpc` | Show configured RPC endpoints |
128
+ | `nit rpc set-url <chain> <url>` | Set RPC endpoint for a chain |
129
+ | `nit auth set <domain> --provider <p> --account <a>` | Configure OAuth auth for a domain branch |
130
+ | `nit auth show [domain]` | Show auth config for branch(es) |
106
131
 
107
132
  ## How It Works
108
133
 
@@ -164,7 +189,10 @@ your-project/
164
189
  ## Programmatic API
165
190
 
166
191
  ```typescript
167
- import { init, commit, checkout, branch, push, status, sign, loginPayload, loadRawKeyPair, getWalletAddresses } from '@newtype-ai/nit';
192
+ import {
193
+ init, commit, checkout, branch, push, status, sign, loginPayload,
194
+ loadRawKeyPair, getWalletAddresses, signTx, broadcast, rpcSetUrl, authSet, authShow,
195
+ } from '@newtype-ai/nit';
168
196
 
169
197
  await init();
170
198
 
@@ -183,6 +211,13 @@ const keypair = await loadRawKeyPair('/path/to/.nit');
183
211
  // Get blockchain wallet addresses (derived from your identity)
184
212
  const addresses = await getWalletAddresses('/path/to/.nit');
185
213
  // → { solana: "C54kvW3...", ethereum: "0x2317..." }
214
+
215
+ // Sign and broadcast transactions
216
+ await rpcSetUrl('evm', 'https://eth.llamarpc.com');
217
+ const sig = await signTx('evm', '0x<32-byte-keccak256-hash>');
218
+ // → { chain: 'evm', signature: '0x...', recovery: 0, address: '0x...' }
219
+ await broadcast('evm', '0x<signed-tx-hex>');
220
+ // → { chain: 'evm', txHash: '0x...', rpcUrl: 'https://...' }
186
221
  ```
187
222
 
188
223
  ## License
@@ -0,0 +1,291 @@
1
+ // nit — version control for agent cards
2
+
3
+ // src/remote.ts
4
+ import { createHash as createHash2 } from "crypto";
5
+
6
+ // src/identity.ts
7
+ import {
8
+ createHash,
9
+ generateKeyPairSync,
10
+ createPrivateKey,
11
+ createPublicKey,
12
+ sign,
13
+ verify
14
+ } from "crypto";
15
+ import { promises as fs } from "fs";
16
+ import { join } from "path";
17
+ function base64urlToBase64(b64url) {
18
+ let s = b64url.replace(/-/g, "+").replace(/_/g, "/");
19
+ while (s.length % 4 !== 0) s += "=";
20
+ return s;
21
+ }
22
+ function base64ToBase64url(b64) {
23
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
24
+ }
25
+ async function generateKeypair(nitDir) {
26
+ const identityDir = join(nitDir, "identity");
27
+ await fs.mkdir(identityDir, { recursive: true });
28
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
29
+ const pubJwk = publicKey.export({ format: "jwk" });
30
+ const privJwk = privateKey.export({ format: "jwk" });
31
+ const pubBase64 = base64urlToBase64(pubJwk.x);
32
+ const privBase64 = base64urlToBase64(privJwk.d);
33
+ const pubPath = join(identityDir, "agent.pub");
34
+ const keyPath = join(identityDir, "agent.key");
35
+ await fs.writeFile(pubPath, pubBase64 + "\n", "utf-8");
36
+ await fs.writeFile(keyPath, privBase64 + "\n", {
37
+ mode: 384,
38
+ encoding: "utf-8"
39
+ });
40
+ return { publicKey: pubBase64, privateKey: privBase64 };
41
+ }
42
+ async function loadPublicKey(nitDir) {
43
+ const pubPath = join(nitDir, "identity", "agent.pub");
44
+ try {
45
+ return (await fs.readFile(pubPath, "utf-8")).trim();
46
+ } catch {
47
+ throw new Error(
48
+ "No identity found. Run `nit init` to generate a keypair."
49
+ );
50
+ }
51
+ }
52
+ async function loadPrivateKey(nitDir) {
53
+ const pubBase64 = await loadPublicKey(nitDir);
54
+ const keyPath = join(nitDir, "identity", "agent.key");
55
+ let privBase64;
56
+ try {
57
+ privBase64 = (await fs.readFile(keyPath, "utf-8")).trim();
58
+ } catch {
59
+ throw new Error(
60
+ "Private key not found at .nit/identity/agent.key. Regenerate with `nit init`."
61
+ );
62
+ }
63
+ const xB64url = base64ToBase64url(pubBase64);
64
+ const dB64url = base64ToBase64url(privBase64);
65
+ return createPrivateKey({
66
+ key: { kty: "OKP", crv: "Ed25519", x: xB64url, d: dB64url },
67
+ format: "jwk"
68
+ });
69
+ }
70
+ async function loadRawKeyPair(nitDir) {
71
+ const pubBase64 = await loadPublicKey(nitDir);
72
+ const keyPath = join(nitDir, "identity", "agent.key");
73
+ let privBase64;
74
+ try {
75
+ privBase64 = (await fs.readFile(keyPath, "utf-8")).trim();
76
+ } catch {
77
+ throw new Error(
78
+ "Private key not found at .nit/identity/agent.key. Regenerate with `nit init`."
79
+ );
80
+ }
81
+ const seed = Buffer.from(privBase64, "base64");
82
+ const pubkey = Buffer.from(pubBase64, "base64");
83
+ const keypair = new Uint8Array(64);
84
+ keypair.set(seed, 0);
85
+ keypair.set(pubkey, 32);
86
+ return keypair;
87
+ }
88
+ function formatPublicKeyField(pubBase64) {
89
+ return `ed25519:${pubBase64}`;
90
+ }
91
+ function parsePublicKeyField(field) {
92
+ const prefix = "ed25519:";
93
+ if (!field.startsWith(prefix)) {
94
+ throw new Error(
95
+ `Invalid publicKey format: expected "ed25519:<base64>", got "${field}"`
96
+ );
97
+ }
98
+ return field.slice(prefix.length);
99
+ }
100
+ async function signChallenge(nitDir, challenge) {
101
+ const privateKey = await loadPrivateKey(nitDir);
102
+ const sig = sign(null, Buffer.from(challenge, "utf-8"), privateKey);
103
+ return sig.toString("base64");
104
+ }
105
+ async function signMessage(nitDir, message) {
106
+ const privateKey = await loadPrivateKey(nitDir);
107
+ const sig = sign(null, Buffer.from(message, "utf-8"), privateKey);
108
+ return sig.toString("base64");
109
+ }
110
+ var NIT_NAMESPACE = "801ba518-f326-47e5-97c9-d1efd1865a19";
111
+ function deriveAgentId(publicKeyField) {
112
+ return uuidv5(publicKeyField, NIT_NAMESPACE);
113
+ }
114
+ async function loadAgentId(nitDir) {
115
+ const idPath = join(nitDir, "identity", "agent-id");
116
+ try {
117
+ return (await fs.readFile(idPath, "utf-8")).trim();
118
+ } catch {
119
+ throw new Error(
120
+ "No agent ID found. Run `nit init` to generate identity."
121
+ );
122
+ }
123
+ }
124
+ async function saveAgentId(nitDir, agentId) {
125
+ const idPath = join(nitDir, "identity", "agent-id");
126
+ await fs.writeFile(idPath, agentId + "\n", "utf-8");
127
+ }
128
+ function parseUuid(uuid) {
129
+ const hex = uuid.replace(/-/g, "");
130
+ return Buffer.from(hex, "hex");
131
+ }
132
+ function formatUuid(bytes) {
133
+ const hex = bytes.toString("hex");
134
+ return [
135
+ hex.slice(0, 8),
136
+ hex.slice(8, 12),
137
+ hex.slice(12, 16),
138
+ hex.slice(16, 20),
139
+ hex.slice(20, 32)
140
+ ].join("-");
141
+ }
142
+ function uuidv5(name, namespace) {
143
+ const namespaceBytes = parseUuid(namespace);
144
+ const nameBytes = Buffer.from(name, "utf-8");
145
+ const data = Buffer.concat([namespaceBytes, nameBytes]);
146
+ const hash = createHash("sha1").update(data).digest();
147
+ const uuid = Buffer.from(hash.subarray(0, 16));
148
+ uuid[6] = uuid[6] & 15 | 80;
149
+ uuid[8] = uuid[8] & 63 | 128;
150
+ return formatUuid(uuid);
151
+ }
152
+
153
+ // src/remote.ts
154
+ function sha256Hex(data) {
155
+ return createHash2("sha256").update(data, "utf-8").digest("hex");
156
+ }
157
+ async function buildAuthHeaders(nitDir, method, path, body) {
158
+ const agentId = await loadAgentId(nitDir);
159
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
160
+ let message = `${method}
161
+ ${path}
162
+ ${agentId}
163
+ ${timestamp}`;
164
+ if (body !== void 0) {
165
+ message += `
166
+ ${sha256Hex(body)}`;
167
+ }
168
+ const signature = await signMessage(nitDir, message);
169
+ return {
170
+ "X-Nit-Agent-Id": agentId,
171
+ "X-Nit-Timestamp": timestamp,
172
+ "X-Nit-Signature": signature
173
+ };
174
+ }
175
+ async function pushBranch(nitDir, apiBase, branch, cardJson, commitHash) {
176
+ const path = `/agent-card/branches/${encodeURIComponent(branch)}`;
177
+ const body = JSON.stringify({ card_json: cardJson, commit_hash: commitHash });
178
+ try {
179
+ const authHeaders = await buildAuthHeaders(nitDir, "PUT", path, body);
180
+ const res = await fetch(`${apiBase}${path}`, {
181
+ method: "PUT",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ ...authHeaders
185
+ },
186
+ body
187
+ });
188
+ if (!res.ok) {
189
+ const text = await res.text();
190
+ return {
191
+ branch,
192
+ commitHash,
193
+ remoteUrl: apiBase,
194
+ success: false,
195
+ error: `HTTP ${res.status}: ${text}`
196
+ };
197
+ }
198
+ return { branch, commitHash, remoteUrl: apiBase, success: true };
199
+ } catch (err) {
200
+ return {
201
+ branch,
202
+ commitHash,
203
+ remoteUrl: apiBase,
204
+ success: false,
205
+ error: err instanceof Error ? err.message : String(err)
206
+ };
207
+ }
208
+ }
209
+ async function pushAll(nitDir, apiBase, branches) {
210
+ const results = [];
211
+ for (const b of branches) {
212
+ const result = await pushBranch(nitDir, apiBase, b.name, b.cardJson, b.commitHash);
213
+ results.push(result);
214
+ }
215
+ return results;
216
+ }
217
+ async function listRemoteBranches(nitDir, apiBase) {
218
+ const path = "/agent-card/branches";
219
+ const authHeaders = await buildAuthHeaders(nitDir, "GET", path);
220
+ const res = await fetch(`${apiBase}${path}`, {
221
+ headers: authHeaders
222
+ });
223
+ if (!res.ok) {
224
+ throw new Error(`Failed to list remote branches: HTTP ${res.status}`);
225
+ }
226
+ const data = await res.json();
227
+ return data.branches.map((b) => b.name);
228
+ }
229
+ async function deleteRemoteBranch(nitDir, apiBase, branch) {
230
+ const path = `/agent-card/branches/${encodeURIComponent(branch)}`;
231
+ const authHeaders = await buildAuthHeaders(nitDir, "DELETE", path);
232
+ const res = await fetch(`${apiBase}${path}`, {
233
+ method: "DELETE",
234
+ headers: authHeaders
235
+ });
236
+ return res.ok;
237
+ }
238
+ async function fetchBranchCard(cardUrl, branch, nitDir) {
239
+ const baseUrl = cardUrl.replace(/\/$/, "");
240
+ let url = `${baseUrl}/.well-known/agent-card.json`;
241
+ if (branch !== "main") {
242
+ url += `?branch=${encodeURIComponent(branch)}`;
243
+ }
244
+ const res = await fetch(url);
245
+ if (res.ok) {
246
+ return await res.json();
247
+ }
248
+ if (res.status === 401 && branch !== "main") {
249
+ if (!nitDir) {
250
+ throw new Error(
251
+ `Branch "${branch}" requires authentication. Provide nitDir for signing.`
252
+ );
253
+ }
254
+ const challengeData = await res.json();
255
+ const signature = await signChallenge(nitDir, challengeData.challenge);
256
+ const authRes = await fetch(url, {
257
+ headers: {
258
+ "X-Nit-Challenge": challengeData.challenge,
259
+ "X-Nit-Signature": signature
260
+ }
261
+ });
262
+ if (!authRes.ok) {
263
+ const body = await authRes.text();
264
+ throw new Error(
265
+ `Failed to fetch branch "${branch}" after challenge: HTTP ${authRes.status} ${body}`
266
+ );
267
+ }
268
+ return await authRes.json();
269
+ }
270
+ throw new Error(`Failed to fetch card: HTTP ${res.status}`);
271
+ }
272
+
273
+ export {
274
+ generateKeypair,
275
+ loadPublicKey,
276
+ loadPrivateKey,
277
+ loadRawKeyPair,
278
+ formatPublicKeyField,
279
+ parsePublicKeyField,
280
+ signChallenge,
281
+ signMessage,
282
+ NIT_NAMESPACE,
283
+ deriveAgentId,
284
+ loadAgentId,
285
+ saveAgentId,
286
+ pushBranch,
287
+ pushAll,
288
+ listRemoteBranches,
289
+ deleteRemoteBranch,
290
+ fetchBranchCard
291
+ };