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.
Files changed (3) hide show
  1. package/README.md +87 -0
  2. package/index.js +66 -0
  3. 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
+ }