@newtype-ai/nit 0.4.9 → 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
@@ -114,6 +114,9 @@ Pure Node.js builtins. No bloat.
114
114
  | `nit branch [name]` | List branches or create a new one |
115
115
  | `nit checkout <branch>` | Switch branch (overwrites agent-card.json) |
116
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 |
117
120
  | `nit sign "msg"` | Sign a message with your Ed25519 key |
118
121
  | `nit sign --login <domain>` | Auto-switch to domain branch + generate login payload |
119
122
  | `nit remote` | Show remote URL and credential status |
@@ -123,6 +126,8 @@ Pure Node.js builtins. No bloat.
123
126
  | `nit broadcast --chain <c> <tx>` | Broadcast signed transaction to configured RPC endpoint |
124
127
  | `nit rpc` | Show configured RPC endpoints |
125
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) |
126
131
 
127
132
  ## How It Works
128
133
 
@@ -186,7 +191,7 @@ your-project/
186
191
  ```typescript
187
192
  import {
188
193
  init, commit, checkout, branch, push, status, sign, loginPayload,
189
- loadRawKeyPair, getWalletAddresses, signTx, broadcast, rpcSetUrl,
194
+ loadRawKeyPair, getWalletAddresses, signTx, broadcast, rpcSetUrl, authSet, authShow,
190
195
  } from '@newtype-ai/nit';
191
196
 
192
197
  await init();
@@ -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
+ };