myapi-asc 0.1.1
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 +87 -0
- package/index.js +66 -0
- package/package.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# myapi-asc
|
|
2
|
+
|
|
3
|
+
Ed25519 per-request signing for AI agents talking to MyApi. The agent's identity is its
|
|
4
|
+
public key, not its IP or User-Agent — so workers can rotate freely without tripping the
|
|
5
|
+
"suspicious device" alert.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
OAuth tokens identify the *client*, not the *device*. AI agents legitimately run from
|
|
10
|
+
load-balanced workers with rotating IPs, missing User-Agent headers, and short-lived
|
|
11
|
+
hosts. Binding a token to a device fingerprint produced false-positive alerts.
|
|
12
|
+
|
|
13
|
+
ASC fixes this: every request carries a fresh Ed25519 signature over `<timestamp>:<tokenId>`.
|
|
14
|
+
The server verifies the signature against a registered public key. The key fingerprint is
|
|
15
|
+
the agent's identity.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
In-repo for now — copy the `packages/myapi-asc` directory or symlink it. npm publish later.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install ./packages/myapi-asc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
### Generate a keypair (once per agent)
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const { generateKeypair } = require('myapi-asc');
|
|
31
|
+
const { publicKey, privateKey } = generateKeypair();
|
|
32
|
+
// Save privateKey securely on the agent host.
|
|
33
|
+
// Register publicKey in the MyApi dashboard → Connect an AI Agent.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Sign each request
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const { sign } = require('myapi-asc');
|
|
40
|
+
const headers = sign({ privateKey, tokenId: 'tok_abcd...' });
|
|
41
|
+
// headers = { 'X-Agent-PublicKey': '...', 'X-Agent-Signature': '...', 'X-Agent-Timestamp': '...' }
|
|
42
|
+
|
|
43
|
+
await fetch('https://api.myapi.com/api/v1/identity', {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${rawToken}`,
|
|
46
|
+
...headers,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Drop-in fetch wrapper
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const { wrapFetch } = require('myapi-asc');
|
|
55
|
+
const myFetch = wrapFetch(fetch, {
|
|
56
|
+
tokenId: 'tok_abcd...',
|
|
57
|
+
privateKey,
|
|
58
|
+
bearer: rawToken,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await myFetch('https://api.myapi.com/api/v1/identity');
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Curl (no SDK)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
TS=$(date +%s)
|
|
68
|
+
SIG=$(printf "$TS:$TOKEN_ID" | openssl pkeyutl -sign -inkey ed25519.pem -rawin | base64 -w0)
|
|
69
|
+
PUB=$(openssl pkey -in ed25519.pem -pubout -outform DER | tail -c 32 | base64 -w0)
|
|
70
|
+
|
|
71
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
72
|
+
-H "X-Agent-PublicKey: $PUB" \
|
|
73
|
+
-H "X-Agent-Signature: $SIG" \
|
|
74
|
+
-H "X-Agent-Timestamp: $TS" \
|
|
75
|
+
https://api.myapi.com/api/v1/identity
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## First-time approval
|
|
79
|
+
|
|
80
|
+
The first signed request from an unregistered public key returns `403 DEVICE_APPROVAL_REQUIRED`
|
|
81
|
+
and surfaces a pending approval in the dashboard. Once the user clicks **Approve**, that
|
|
82
|
+
public key is permanently associated with the token. Subsequent signed requests pass.
|
|
83
|
+
|
|
84
|
+
## Replay protection
|
|
85
|
+
|
|
86
|
+
The server rejects requests whose `X-Agent-Timestamp` is more than 60s out of sync.
|
|
87
|
+
Sign every request fresh — do not cache headers.
|
package/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// MyApi ASC (Agentic Secure Connection) — Ed25519 per-request signing for AI agents.
|
|
2
|
+
//
|
|
3
|
+
// Identity = your Ed25519 public key. Each request signs `${timestamp}:${fingerprint}` so the
|
|
4
|
+
// server can prove the request came from the key-holder and was issued in the last 60s.
|
|
5
|
+
// No User-Agent / IP dependency — your agent can move across IPs, workers, or hosts without
|
|
6
|
+
// tripping false-positive "suspicious device" alerts.
|
|
7
|
+
//
|
|
8
|
+
// const { generateKeypair, sign, wrapFetch } = require('myapi-asc');
|
|
9
|
+
// const { publicKey, privateKey } = generateKeypair();
|
|
10
|
+
// // 1) Register the publicKey via the MyApi dashboard (Connect-an-AI-Agent flow)
|
|
11
|
+
// // 2) Sign requests:
|
|
12
|
+
// const headers = sign({ privateKey });
|
|
13
|
+
// await fetch(url, { headers: { Authorization: `Bearer ${rawToken}`, ...headers } });
|
|
14
|
+
//
|
|
15
|
+
// // or, drop-in fetch wrapper:
|
|
16
|
+
// const myFetch = wrapFetch(fetch, { privateKey, bearer: rawToken });
|
|
17
|
+
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
function generateKeypair() {
|
|
21
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
22
|
+
const pubDer = publicKey.export({ format: 'der', type: 'spki' });
|
|
23
|
+
// SPKI Ed25519 wrapper is 12 bytes (302a300506032b6570032100), raw key follows
|
|
24
|
+
const rawPub = pubDer.subarray(12);
|
|
25
|
+
return {
|
|
26
|
+
publicKey: rawPub.toString('base64'),
|
|
27
|
+
privateKey: privateKey.export({ format: 'pem', type: 'pkcs8' }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sign({ privateKey }) {
|
|
32
|
+
if (!privateKey) throw new Error('sign() requires privateKey');
|
|
33
|
+
const keyObj = typeof privateKey === 'string' || Buffer.isBuffer(privateKey)
|
|
34
|
+
? crypto.createPrivateKey(privateKey)
|
|
35
|
+
: privateKey;
|
|
36
|
+
const pubDer = crypto.createPublicKey(keyObj).export({ format: 'der', type: 'spki' });
|
|
37
|
+
const rawPub = pubDer.subarray(12);
|
|
38
|
+
const rawPubB64 = rawPub.toString('base64');
|
|
39
|
+
const fingerprint = crypto.createHash('sha256').update(rawPub).digest('hex').substring(0, 32);
|
|
40
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
41
|
+
const message = Buffer.from(`${timestamp}:${fingerprint}`);
|
|
42
|
+
const signature = crypto.sign(null, message, keyObj);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
'X-Agent-PublicKey': rawPubB64,
|
|
46
|
+
'X-Agent-Signature': signature.toString('base64'),
|
|
47
|
+
'X-Agent-Timestamp': timestamp,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function wrapFetch(fetchImpl, { privateKey, bearer }) {
|
|
52
|
+
if (!fetchImpl) throw new Error('wrapFetch() requires a fetch implementation');
|
|
53
|
+
return async (input, init = {}) => {
|
|
54
|
+
const signedHeaders = sign({ privateKey });
|
|
55
|
+
const headers = {
|
|
56
|
+
...(init.headers || {}),
|
|
57
|
+
...signedHeaders,
|
|
58
|
+
};
|
|
59
|
+
if (bearer && !headers.Authorization && !headers.authorization) {
|
|
60
|
+
headers.Authorization = `Bearer ${bearer}`;
|
|
61
|
+
}
|
|
62
|
+
return fetchImpl(input, { ...init, headers });
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { generateKeypair, sign, wrapFetch };
|
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "myapi-asc",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MyApi Agentic Secure Connection — Ed25519 per-request signing for AI agents.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": ["myapi", "ed25519", "agent", "asc", "ai", "signing"]
|
|
11
|
+
}
|