aismemory 0.3.0 → 0.5.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/__tests__/device-flow-recovery.test.d.ts +1 -0
- package/dist/__tests__/device-flow-recovery.test.js +206 -0
- package/dist/__tests__/device-flow-recovery.test.js.map +1 -0
- package/dist/__tests__/key-auth.test.d.ts +1 -0
- package/dist/__tests__/key-auth.test.js +284 -0
- package/dist/__tests__/key-auth.test.js.map +1 -0
- package/dist/__tests__/mcp-init-no-auth.test.d.ts +1 -0
- package/dist/__tests__/mcp-init-no-auth.test.js +75 -0
- package/dist/__tests__/mcp-init-no-auth.test.js.map +1 -0
- package/dist/cli/enable-key-auth.d.ts +1 -0
- package/dist/cli/enable-key-auth.js +131 -0
- package/dist/cli/enable-key-auth.js.map +1 -0
- package/dist/index.js +199 -47
- package/dist/index.js.map +1 -1
- package/dist/key-auth.d.ts +66 -0
- package/dist/key-auth.js +179 -0
- package/dist/key-auth.js.map +1 -0
- package/package.json +1 -1
package/dist/key-auth.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-based authentication for the aismemory client.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 of the DID-auth ladder: the user generates an Ed25519 keypair on
|
|
5
|
+
* their device (via `enable-key-auth`), publishes the public key to AIS once,
|
|
6
|
+
* and from then on signs server-issued challenges with the private key to
|
|
7
|
+
* obtain owner JWTs. The browser activate flow is needed only at enrollment.
|
|
8
|
+
*
|
|
9
|
+
* Threat model:
|
|
10
|
+
* - Private key lives at `~/.aismemory/keys/<userDid>.json`, mode 0600.
|
|
11
|
+
* Same handling story as an SSH private key without a passphrase.
|
|
12
|
+
* - The server NEVER sees the private key — only the public half.
|
|
13
|
+
* - Owner JWTs are NOT persisted; they live in memory for the session.
|
|
14
|
+
* When the JWT expires (or AIS rotates JWT_SECRET) the client silently
|
|
15
|
+
* re-runs challenge/prove. The user notices nothing.
|
|
16
|
+
*/
|
|
17
|
+
import { generateKeyPairSync, createPrivateKey, sign as edSign, } from 'node:crypto';
|
|
18
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, chmodSync, } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Filesystem helpers
|
|
23
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
24
|
+
/** Default location for key files. Override via `keysDir` arg in tests. */
|
|
25
|
+
function defaultKeysDir() {
|
|
26
|
+
return join(homedir(), '.aismemory', 'keys');
|
|
27
|
+
}
|
|
28
|
+
function keyFilePath(userDid, keysDir) {
|
|
29
|
+
return join(keysDir ?? defaultKeysDir(), `${userDid}.json`);
|
|
30
|
+
}
|
|
31
|
+
/** Read a key file. Returns null if the file is absent. Throws on malformed JSON. */
|
|
32
|
+
export function readKeyFile(opts) {
|
|
33
|
+
const path = keyFilePath(opts.userDid, opts.keysDir);
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return null;
|
|
36
|
+
const raw = readFileSync(path, 'utf-8');
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (parsed.version !== 1) {
|
|
39
|
+
throw new Error(`Unsupported key file version: ${parsed.version}. Run \`aismemory enable-key-auth\` to re-enroll.`);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Discover a single key file in `keysDir` when the caller doesn't know which
|
|
45
|
+
* userDid to load. Common case: the MCP server boots, sees a single key on
|
|
46
|
+
* disk, and uses it. Returns null if zero or multiple keys are present.
|
|
47
|
+
*/
|
|
48
|
+
function discoverSingleKeyFile(keysDir) {
|
|
49
|
+
const dir = keysDir ?? defaultKeysDir();
|
|
50
|
+
if (!existsSync(dir))
|
|
51
|
+
return null;
|
|
52
|
+
const entries = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
53
|
+
if (entries.length !== 1)
|
|
54
|
+
return null;
|
|
55
|
+
const path = join(dir, entries[0]);
|
|
56
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
57
|
+
if (parsed.version !== 1)
|
|
58
|
+
return null;
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generate a fresh Ed25519 keypair, write it to disk, and return the result.
|
|
63
|
+
* The directory is created with mode 0700 and the key file with mode 0600.
|
|
64
|
+
*
|
|
65
|
+
* This is called once per device by the `enable-key-auth` CLI bootstrap.
|
|
66
|
+
*/
|
|
67
|
+
export async function generateAndSaveKeypair(opts) {
|
|
68
|
+
const dir = opts.keysDir ?? defaultKeysDir();
|
|
69
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
71
|
+
// Export to JWK form so we have a canonical wire shape AND the raw bytes.
|
|
72
|
+
const publicJwk = publicKey.export({ format: 'jwk' });
|
|
73
|
+
const privateJwk = privateKey.export({ format: 'jwk' });
|
|
74
|
+
const file = {
|
|
75
|
+
version: 1,
|
|
76
|
+
userId: opts.userId,
|
|
77
|
+
userDid: opts.userDid,
|
|
78
|
+
keyType: 'Ed25519',
|
|
79
|
+
privateKey: privateJwk.d,
|
|
80
|
+
publicKey: publicJwk.x,
|
|
81
|
+
publicKeyJwk: { kty: 'OKP', crv: 'Ed25519', x: publicJwk.x },
|
|
82
|
+
createdAt: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
const path = keyFilePath(opts.userDid, opts.keysDir);
|
|
85
|
+
writeFileSync(path, JSON.stringify(file, null, 2));
|
|
86
|
+
// chmod after write — writeFileSync's mode arg is umasked, so explicit
|
|
87
|
+
// chmod is the only reliable path to 0600 on POSIX.
|
|
88
|
+
if (process.platform !== 'win32') {
|
|
89
|
+
chmodSync(path, 0o600);
|
|
90
|
+
}
|
|
91
|
+
return file;
|
|
92
|
+
}
|
|
93
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Crypto: sign the canonical challenge string
|
|
95
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Sign the canonical challenge bytes with the user's private key. Returns
|
|
98
|
+
* the signature as a base64url string ready for the wire.
|
|
99
|
+
*
|
|
100
|
+
* Canonical form: `<userDid>\n<nonce>\n<exp>` as UTF-8 bytes. Must match the
|
|
101
|
+
* server's verification (see did-auth.ts in agent-identity-service).
|
|
102
|
+
*/
|
|
103
|
+
function signChallenge(args) {
|
|
104
|
+
// Import the raw private key bytes back into a Node KeyObject. The JWK
|
|
105
|
+
// round-trip is the simplest way to do this without depending on PKCS#8
|
|
106
|
+
// formatting nuances.
|
|
107
|
+
const priv = createPrivateKey({
|
|
108
|
+
key: {
|
|
109
|
+
kty: 'OKP',
|
|
110
|
+
crv: 'Ed25519',
|
|
111
|
+
x: args.publicKeyB64,
|
|
112
|
+
d: args.privateKeyB64,
|
|
113
|
+
},
|
|
114
|
+
format: 'jwk',
|
|
115
|
+
});
|
|
116
|
+
const msg = Buffer.from(`${args.userDid}\n${args.nonce}\n${args.exp}`, 'utf8');
|
|
117
|
+
return edSign(null, msg, priv).toString('base64url');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Attempt key-based authentication against AIS. Returns null if no usable
|
|
121
|
+
* key file exists (caller should fall back to device flow). Throws on any
|
|
122
|
+
* fatal error (bad signature, network failure, server rejection).
|
|
123
|
+
*/
|
|
124
|
+
export async function tryKeyAuth(opts) {
|
|
125
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
126
|
+
// 1. Locate a key file.
|
|
127
|
+
let keyFile;
|
|
128
|
+
if (opts.userDid) {
|
|
129
|
+
keyFile = readKeyFile({ userDid: opts.userDid, keysDir: opts.keysDir });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
keyFile = discoverSingleKeyFile(opts.keysDir);
|
|
133
|
+
}
|
|
134
|
+
if (!keyFile)
|
|
135
|
+
return null;
|
|
136
|
+
// 2. Issue a challenge.
|
|
137
|
+
const challengeRes = await fetchImpl(`${opts.aisUrl}/v1/auth/challenge`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ userDid: keyFile.userDid }),
|
|
141
|
+
});
|
|
142
|
+
const challengeBody = (await challengeRes.json());
|
|
143
|
+
if (!challengeRes.ok || !challengeBody.success || !challengeBody.data) {
|
|
144
|
+
throw new Error(`key-auth: challenge failed (${challengeRes.status}): ${challengeBody.error?.message ?? challengeBody.error?.code ?? 'unknown'}`);
|
|
145
|
+
}
|
|
146
|
+
const { nonce, exp, hmac } = challengeBody.data;
|
|
147
|
+
// 3. Sign the canonical message.
|
|
148
|
+
const signature = signChallenge({
|
|
149
|
+
privateKeyB64: keyFile.privateKey,
|
|
150
|
+
publicKeyB64: keyFile.publicKey,
|
|
151
|
+
userDid: keyFile.userDid,
|
|
152
|
+
nonce,
|
|
153
|
+
exp,
|
|
154
|
+
});
|
|
155
|
+
// 4. Submit the signed proof.
|
|
156
|
+
const proveRes = await fetchImpl(`${opts.aisUrl}/v1/auth/did-prove`, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: { 'Content-Type': 'application/json' },
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
userDid: keyFile.userDid,
|
|
161
|
+
nonce,
|
|
162
|
+
exp,
|
|
163
|
+
hmac,
|
|
164
|
+
signature,
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
const proveBody = (await proveRes.json());
|
|
168
|
+
if (!proveRes.ok || !proveBody.success || !proveBody.data) {
|
|
169
|
+
throw new Error(`key-auth: did-prove failed (${proveRes.status}): ${proveBody.error?.message ?? proveBody.error?.code ?? 'unknown'}`);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
userId: keyFile.userId,
|
|
173
|
+
userDid: keyFile.userDid,
|
|
174
|
+
token: proveBody.data.bearerToken,
|
|
175
|
+
expiresAt: proveBody.data.expiresAt,
|
|
176
|
+
tokenType: 'owner',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=key-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-auth.js","sourceRoot":"","sources":["../src/key-auth.ts"],"names":[],"mappings":"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"}
|