authproof-sdk 1.0.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/LICENSE +21 -0
- package/README.md +382 -0
- package/package.json +51 -0
- package/src/authproof.js +446 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuthProof
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# AuthProof
|
|
2
|
+
|
|
3
|
+
**Cryptographically signed delegation receipts for AI agents.**
|
|
4
|
+
|
|
5
|
+
Define exactly what an AI agent can and can't do. Sign it. Share it. Verify it anywhere.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import AuthProof from 'authproof';
|
|
9
|
+
|
|
10
|
+
const { privateKey, publicJwk } = await AuthProof.generateKey();
|
|
11
|
+
|
|
12
|
+
const { receiptId, systemPrompt } = await AuthProof.create({
|
|
13
|
+
scope: 'Search the web for competitor pricing and summarize findings.',
|
|
14
|
+
boundaries: 'Do not send emails. Do not make purchases.',
|
|
15
|
+
instructions: 'Cite every source. Keep the report under 500 words.',
|
|
16
|
+
ttlHours: 4,
|
|
17
|
+
privateKey,
|
|
18
|
+
publicJwk,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Paste systemPrompt into any AI chat or agent system prompt
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## The Problem
|
|
27
|
+
|
|
28
|
+
AI agents are taking real actions — browsing the web, scheduling meetings, sending emails, making purchases. But there's no standard way to define what an agent is authorized to do, or prove it after the fact.
|
|
29
|
+
|
|
30
|
+
AuthProof fixes that with a simple, tamper-proof delegation receipt:
|
|
31
|
+
|
|
32
|
+
- **Cryptographically signed** — ECDSA P-256 via the native Web Crypto API
|
|
33
|
+
- **Content-addressed** — SHA-256 proof ID that changes if anything is modified
|
|
34
|
+
- **Time-windowed** — auto-expires, no open-ended permissions
|
|
35
|
+
- **Instantly revocable** — one call to invalidate
|
|
36
|
+
- **Zero dependencies** — works in Node.js, browsers, Deno, Bun, Cloudflare Workers
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install authproof
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or load directly in a browser / Deno:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import AuthProof from 'https://esm.sh/authproof@1';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### 1. Generate a key pair
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import AuthProof from 'authproof';
|
|
60
|
+
|
|
61
|
+
const { privateKey, publicJwk, privateJwk } = await AuthProof.generateKey();
|
|
62
|
+
|
|
63
|
+
// Store privateJwk securely (env var, secrets manager, etc.)
|
|
64
|
+
// publicJwk is safe to store anywhere
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Create a receipt
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const { receipt, receiptId, systemPrompt } = await AuthProof.create({
|
|
71
|
+
scope: 'Read the user\'s calendar for the next 7 days and identify conflicts.',
|
|
72
|
+
boundaries: 'Do not create, edit, or delete calendar events. Do not read emails.',
|
|
73
|
+
instructions: 'Present the 3 best available slots with a brief reason for each.',
|
|
74
|
+
ttlHours: 1, // Expires in 1 hour
|
|
75
|
+
privateKey,
|
|
76
|
+
publicJwk,
|
|
77
|
+
agentId: 'calendar-agent-v1', // optional
|
|
78
|
+
metadata: { requestedBy: 'user@example.com' }, // optional
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(receiptId); // 64-char SHA-256 proof ID
|
|
82
|
+
console.log(systemPrompt); // ready-to-paste AI system prompt
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Use the system prompt
|
|
86
|
+
|
|
87
|
+
Paste `systemPrompt` into any AI:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
You are authorized to act within the following scope:
|
|
91
|
+
|
|
92
|
+
Read the user's calendar for the next 7 days and identify conflicts.
|
|
93
|
+
|
|
94
|
+
You must not:
|
|
95
|
+
Do not create, edit, or delete calendar events. Do not read emails.
|
|
96
|
+
|
|
97
|
+
Operator instructions:
|
|
98
|
+
Present the 3 best available slots with a brief reason for each.
|
|
99
|
+
|
|
100
|
+
This authorization is valid until: Apr 5, 2026, 3:00 PM
|
|
101
|
+
Authorization ID: a3f9c2...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Verify a receipt
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const result = await AuthProof.verify(receipt, receiptId, {
|
|
108
|
+
action: 'Check calendar for next Tuesday and find a free hour',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (result.authorized) {
|
|
112
|
+
console.log('✓ Action authorized');
|
|
113
|
+
} else {
|
|
114
|
+
console.log('✗ Denied:', result.checks.filter(c => !c.passed));
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## API Reference
|
|
121
|
+
|
|
122
|
+
### `AuthProof.generateKey()`
|
|
123
|
+
|
|
124
|
+
Generate a new ECDSA P-256 signing key pair.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const { privateKey, publicJwk, privateJwk } = await AuthProof.generateKey();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
- `privateKey` — `CryptoKey` for signing
|
|
132
|
+
- `publicJwk` — plain object, safe to store publicly
|
|
133
|
+
- `privateJwk` — plain object, **store securely**
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `AuthProof.importPrivateKey(privateJwk)`
|
|
138
|
+
|
|
139
|
+
Load a signing key from a stored JWK (e.g., from an env var).
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const privateJwk = JSON.parse(process.env.AUTHPROOF_PRIVATE_JWK);
|
|
143
|
+
const privateKey = await AuthProof.importPrivateKey(privateJwk);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### `AuthProof.create(options)`
|
|
149
|
+
|
|
150
|
+
Create a signed delegation receipt.
|
|
151
|
+
|
|
152
|
+
| Option | Type | Required | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `scope` | string | ✓ | What the AI is authorized to do |
|
|
155
|
+
| `boundaries` | string | ✓ | What the AI must never do |
|
|
156
|
+
| `instructions` | string | ✓ | How the AI should approach the task |
|
|
157
|
+
| `ttlHours` | number | — | Validity window in hours (default: `1`) |
|
|
158
|
+
| `privateKey` | CryptoKey | ✓ | ECDSA P-256 signing key |
|
|
159
|
+
| `publicJwk` | object | ✓ | Corresponding public key JWK |
|
|
160
|
+
| `agentId` | string | — | Optional agent identifier |
|
|
161
|
+
| `metadata` | object | — | Optional arbitrary metadata |
|
|
162
|
+
|
|
163
|
+
Returns `{ receipt, receiptId, systemPrompt }`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `AuthProof.verify(receipt, receiptId, options?)`
|
|
168
|
+
|
|
169
|
+
Verify a receipt locally — no network required.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const result = await AuthProof.verify(receipt, receiptId, {
|
|
173
|
+
revoked: false, // pass true if you've marked it revoked in your store
|
|
174
|
+
action: 'Proposed action text to scope-check',
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
```js
|
|
180
|
+
{
|
|
181
|
+
authorized: true | false,
|
|
182
|
+
result: 'Human-readable verdict',
|
|
183
|
+
checks: [
|
|
184
|
+
{ name: 'Not revoked', passed: true, detail: 'Active' },
|
|
185
|
+
{ name: 'Within time window',passed: true, detail: 'Expires Apr 5, 2026' },
|
|
186
|
+
{ name: 'Signature valid', passed: true, detail: 'ECDSA P-256 verified' },
|
|
187
|
+
{ name: 'Receipt ID matches',passed: true, detail: 'SHA-256 hash verified' },
|
|
188
|
+
{ name: 'Instructions intact',passed: true, detail: 'Instructions hash verified' },
|
|
189
|
+
// if action provided:
|
|
190
|
+
{ name: 'Action within scope', passed: true, detail: '72% keyword match' },
|
|
191
|
+
{ name: 'Action not blocked', passed: true, detail: '8% overlap with boundaries' },
|
|
192
|
+
],
|
|
193
|
+
receiptContext: { scope, boundaries, operatorInstructions, timeWindow, ... }
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `AuthProof.buildSystemPrompt(receipt, receiptId, verifyUrl?)`
|
|
200
|
+
|
|
201
|
+
Build a ready-to-use system prompt from a receipt.
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
const prompt = AuthProof.buildSystemPrompt(receipt, receiptId, 'https://authproof.dev');
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### `AuthProof.checkScope(action, receipt)`
|
|
210
|
+
|
|
211
|
+
Quick keyword-overlap scope check without full verification.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const { withinScope, scopeScore, boundaryScore } = AuthProof.checkScope(
|
|
215
|
+
'Search Google for competitor pricing',
|
|
216
|
+
receipt
|
|
217
|
+
);
|
|
218
|
+
// { withinScope: true, scopeScore: 72, boundaryScore: 5 }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### `AuthProof.isActive(receipt, revoked?)`
|
|
224
|
+
|
|
225
|
+
Returns `true` if the receipt is currently within its time window and not revoked.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
AuthProof.isActive(receipt); // true
|
|
229
|
+
AuthProof.isActive(receipt, true); // false (revoked)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### `AuthProof.secondsRemaining(receipt)`
|
|
235
|
+
|
|
236
|
+
Seconds until the receipt expires. Returns `0` if already expired.
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
const secs = AuthProof.secondsRemaining(receipt);
|
|
240
|
+
console.log(`Expires in ${Math.floor(secs / 60)} minutes`);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### `AuthProof.receiptId(receipt)`
|
|
246
|
+
|
|
247
|
+
Compute the SHA-256 receipt ID from a receipt object.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
const id = await AuthProof.receiptId(receipt);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Integrations
|
|
256
|
+
|
|
257
|
+
### LangChain / LangGraph
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
const { systemPrompt, receiptId } = await AuthProof.create({ ... });
|
|
261
|
+
|
|
262
|
+
const messages = [
|
|
263
|
+
{ role: 'system', content: systemPrompt },
|
|
264
|
+
{ role: 'user', content: userMessage },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
// Pass to your LangChain chain or LangGraph node
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### n8n
|
|
271
|
+
|
|
272
|
+
In a **Code node** before your AI node:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
const AuthProof = require('authproof');
|
|
276
|
+
const privateKey = await AuthProof.importPrivateKey(JSON.parse($env.AUTHPROOF_PRIVATE_JWK));
|
|
277
|
+
const publicJwk = JSON.parse($env.AUTHPROOF_PUBLIC_JWK);
|
|
278
|
+
|
|
279
|
+
const { systemPrompt, receiptId } = await AuthProof.create({
|
|
280
|
+
scope: $input.item.json.scope,
|
|
281
|
+
boundaries: $input.item.json.boundaries,
|
|
282
|
+
instructions: $input.item.json.instructions,
|
|
283
|
+
ttlHours: 1,
|
|
284
|
+
privateKey,
|
|
285
|
+
publicJwk,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return [{ json: { ...$input.item.json, systemPrompt, receiptId } }];
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Express.js Middleware
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
import { authproofMiddleware } from 'authproof/examples/integrations.js';
|
|
295
|
+
|
|
296
|
+
app.use('/api/agent', authproofMiddleware(receiptStore));
|
|
297
|
+
// Agent must send: X-AuthProof-Receipt-Id header
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Cloudflare Workers / Deno
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
import AuthProof from 'https://esm.sh/authproof@1';
|
|
304
|
+
// All APIs work identically — uses native Web Crypto
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Receipt Schema
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"delegationId": "auth-1712345678901-a3f9c",
|
|
314
|
+
"issuedAt": "2026-04-05T14:00:00.000Z",
|
|
315
|
+
"scope": "Search the web for competitor pricing...",
|
|
316
|
+
"boundaries": "Do not send emails. Do not make purchases.",
|
|
317
|
+
"timeWindow": {
|
|
318
|
+
"start": "2026-04-05T14:00:00.000Z",
|
|
319
|
+
"end": "2026-04-05T18:00:00.000Z"
|
|
320
|
+
},
|
|
321
|
+
"operatorInstructions": "Cite every source. Keep under 500 words.",
|
|
322
|
+
"instructionsHash": "a3f9c2d1...",
|
|
323
|
+
"signerPublicKey": {
|
|
324
|
+
"kty": "EC", "crv": "P-256", "x": "...", "y": "..."
|
|
325
|
+
},
|
|
326
|
+
"agentId": "my-agent-v1",
|
|
327
|
+
"metadata": {},
|
|
328
|
+
"signature": "3045022100..."
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The **receipt ID** is `SHA-256(JSON.stringify(receipt))` — if any field changes, the ID changes.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Supabase Backend (optional)
|
|
337
|
+
|
|
338
|
+
For persistent receipts with remote verify/revoke, deploy the included Edge Functions.
|
|
339
|
+
See the SQL schema and function code in [`examples/integrations.js`](examples/integrations.js).
|
|
340
|
+
|
|
341
|
+
```sql
|
|
342
|
+
create table receipts (
|
|
343
|
+
hash text primary key,
|
|
344
|
+
delegation_id text not null,
|
|
345
|
+
scope text not null,
|
|
346
|
+
boundaries text not null,
|
|
347
|
+
time_window_start timestamptz not null,
|
|
348
|
+
time_window_end timestamptz not null,
|
|
349
|
+
operator_instructions text not null,
|
|
350
|
+
instructions_hash text not null,
|
|
351
|
+
signer_public_key jsonb not null,
|
|
352
|
+
signature text not null,
|
|
353
|
+
issued_at timestamptz not null,
|
|
354
|
+
revoked boolean default false,
|
|
355
|
+
revoked_at timestamptz
|
|
356
|
+
);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Interactive Tool
|
|
362
|
+
|
|
363
|
+
Not a developer? Use the **[AuthProof web app](https://authproof.dev)** — no code required.
|
|
364
|
+
Create, verify, and manage receipts visually in your browser.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT — free for personal and commercial use.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Contributing
|
|
375
|
+
|
|
376
|
+
PRs welcome. Run tests with:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
node tests/authproof.test.js
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
40 tests, zero dependencies, zero build step required.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authproof-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cryptographically signed delegation receipts for AI agents. Define exactly what an AI can and can't do — signed, verifiable, tamper-proof.",
|
|
5
|
+
"main": "src/authproof.js",
|
|
6
|
+
"module": "src/authproof.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/authproof.js",
|
|
11
|
+
"require": "./src/authproof.cjs.js",
|
|
12
|
+
"default": "./src/authproof.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --experimental-vm-modules tests/authproof.test.js",
|
|
17
|
+
"build:cjs": "node scripts/build-cjs.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agents",
|
|
22
|
+
"authorization",
|
|
23
|
+
"delegation",
|
|
24
|
+
"receipt",
|
|
25
|
+
"cryptography",
|
|
26
|
+
"ecdsa",
|
|
27
|
+
"permissions",
|
|
28
|
+
"ai-safety",
|
|
29
|
+
"llm",
|
|
30
|
+
"agent-security"
|
|
31
|
+
],
|
|
32
|
+
"author": "AuthProof",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/Commonguy25/authproof-sdk"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://authproof.dev",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Commonguy25/authproof-sdk/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"src/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
],
|
|
50
|
+
"devDependencies": {}
|
|
51
|
+
}
|
package/src/authproof.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthProof SDK
|
|
3
|
+
* Cryptographically signed delegation receipts for AI agents.
|
|
4
|
+
*
|
|
5
|
+
* Works in: Node.js 18+, modern browsers, Deno, Bun, Cloudflare Workers
|
|
6
|
+
* Zero dependencies. Uses native Web Crypto API everywhere.
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────
|
|
15
|
+
// CRYPTO PRIMITIVES
|
|
16
|
+
// ─────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const _enc = s => new TextEncoder().encode(s);
|
|
19
|
+
const _hex = b => Array.from(new Uint8Array(b)).map(x => x.toString(16).padStart(2, '0')).join('');
|
|
20
|
+
const _fromHex = h => new Uint8Array(h.match(/.{2}/g).map(b => parseInt(b, 16)));
|
|
21
|
+
|
|
22
|
+
async function _sha256(str) {
|
|
23
|
+
const buf = await crypto.subtle.digest('SHA-256', _enc(str));
|
|
24
|
+
return _hex(buf);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function _generateKeyPair() {
|
|
28
|
+
return crypto.subtle.generateKey(
|
|
29
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
30
|
+
true,
|
|
31
|
+
['sign', 'verify']
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function _exportPublicJwk(publicKey) {
|
|
36
|
+
const jwk = await crypto.subtle.exportKey('jwk', publicKey);
|
|
37
|
+
const { kty, crv, x, y } = jwk;
|
|
38
|
+
return { kty, crv, x, y };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function _sign(privateKey, str) {
|
|
42
|
+
const sig = await crypto.subtle.sign(
|
|
43
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
44
|
+
privateKey,
|
|
45
|
+
_enc(str)
|
|
46
|
+
);
|
|
47
|
+
return _hex(sig);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function _verify(publicJwk, signature, str) {
|
|
51
|
+
try {
|
|
52
|
+
const pk = await crypto.subtle.importKey(
|
|
53
|
+
'jwk',
|
|
54
|
+
{ ...publicJwk, key_ops: ['verify'] },
|
|
55
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
56
|
+
false,
|
|
57
|
+
['verify']
|
|
58
|
+
);
|
|
59
|
+
return crypto.subtle.verify(
|
|
60
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
61
|
+
pk,
|
|
62
|
+
_fromHex(signature),
|
|
63
|
+
_enc(str)
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────
|
|
71
|
+
// SCOPE MATCHING
|
|
72
|
+
// ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const STOP_WORDS = new Set([
|
|
75
|
+
'the','a','an','and','or','but','in','on','at','to','for','of','with',
|
|
76
|
+
'by','from','is','are','was','were','be','been','have','has','had','do',
|
|
77
|
+
'does','did','will','would','could','should','may','might','can','not',
|
|
78
|
+
'no','this','that','these','those','i','you','he','she','it','we','they',
|
|
79
|
+
'what','which','who','all','any','each','some','only','just','as','if',
|
|
80
|
+
'then','up','out'
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
function _tokenize(text) {
|
|
84
|
+
return new Set(
|
|
85
|
+
text.toLowerCase()
|
|
86
|
+
.split(/[\s,;:.()\[\]{}'"""]+/)
|
|
87
|
+
.filter(w => w.length > 3 && !STOP_WORDS.has(w))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _overlapScore(a, b) {
|
|
92
|
+
const ta = _tokenize(a);
|
|
93
|
+
const tb = _tokenize(b);
|
|
94
|
+
if (ta.size === 0) return 0;
|
|
95
|
+
let hits = 0;
|
|
96
|
+
for (const w of ta) if (tb.has(w)) hits++;
|
|
97
|
+
return hits / ta.size;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a proposed action falls within scope and doesn't violate boundaries.
|
|
102
|
+
* Returns { withinScope: boolean, scopeScore: number, boundaryScore: number }
|
|
103
|
+
*/
|
|
104
|
+
function checkScope(action, receipt) {
|
|
105
|
+
const scopeScore = _overlapScore(action, receipt.scope);
|
|
106
|
+
const boundaryScore = _overlapScore(action, receipt.boundaries);
|
|
107
|
+
return {
|
|
108
|
+
withinScope: scopeScore >= 0.3 && boundaryScore < 0.5,
|
|
109
|
+
scopeScore: Math.round(scopeScore * 100),
|
|
110
|
+
boundaryScore: Math.round(boundaryScore * 100),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────
|
|
115
|
+
// KEY MANAGEMENT
|
|
116
|
+
// ─────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate a new ECDSA P-256 signing key pair.
|
|
120
|
+
* Returns exportable JWK representations for storage.
|
|
121
|
+
*
|
|
122
|
+
* @returns {{ privateJwk: object, publicJwk: object, privateKey: CryptoKey, publicKey: CryptoKey }}
|
|
123
|
+
*/
|
|
124
|
+
async function generateKey() {
|
|
125
|
+
const kp = await _generateKeyPair();
|
|
126
|
+
const privateJwk = await crypto.subtle.exportKey('jwk', kp.privateKey);
|
|
127
|
+
const publicJwk = await _exportPublicJwk(kp.publicKey);
|
|
128
|
+
return {
|
|
129
|
+
privateKey: kp.privateKey,
|
|
130
|
+
publicKey: kp.publicKey,
|
|
131
|
+
privateJwk,
|
|
132
|
+
publicJwk,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Load a signing key from a stored private JWK.
|
|
138
|
+
* @param {object} privateJwk
|
|
139
|
+
* @returns {CryptoKey}
|
|
140
|
+
*/
|
|
141
|
+
async function importPrivateKey(privateJwk) {
|
|
142
|
+
return crypto.subtle.importKey(
|
|
143
|
+
'jwk',
|
|
144
|
+
privateJwk,
|
|
145
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
146
|
+
false,
|
|
147
|
+
['sign']
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────
|
|
152
|
+
// RECEIPT CREATION
|
|
153
|
+
// ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a signed delegation receipt.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} options
|
|
159
|
+
* @param {string} options.scope - What the AI is authorized to do
|
|
160
|
+
* @param {string} options.boundaries - What the AI must never do
|
|
161
|
+
* @param {string} options.instructions - Operator instructions locked into the receipt
|
|
162
|
+
* @param {number} [options.ttlHours=1] - How long the receipt is valid (hours)
|
|
163
|
+
* @param {CryptoKey} options.privateKey - ECDSA P-256 private signing key
|
|
164
|
+
* @param {object} options.publicJwk - Corresponding public key JWK
|
|
165
|
+
* @param {string} [options.agentId] - Optional identifier for the agent being authorized
|
|
166
|
+
* @param {object} [options.metadata] - Optional arbitrary metadata attached to receipt
|
|
167
|
+
*
|
|
168
|
+
* @returns {{ receipt: object, receiptId: string, systemPrompt: string }}
|
|
169
|
+
*/
|
|
170
|
+
async function create(options) {
|
|
171
|
+
const {
|
|
172
|
+
scope,
|
|
173
|
+
boundaries,
|
|
174
|
+
instructions,
|
|
175
|
+
ttlHours = 1,
|
|
176
|
+
privateKey,
|
|
177
|
+
publicJwk,
|
|
178
|
+
agentId,
|
|
179
|
+
metadata,
|
|
180
|
+
} = options;
|
|
181
|
+
|
|
182
|
+
if (!scope) throw new Error('AuthProof: scope is required');
|
|
183
|
+
if (!boundaries) throw new Error('AuthProof: boundaries is required');
|
|
184
|
+
if (!instructions) throw new Error('AuthProof: instructions is required');
|
|
185
|
+
if (!privateKey) throw new Error('AuthProof: privateKey is required');
|
|
186
|
+
if (!publicJwk) throw new Error('AuthProof: publicJwk is required');
|
|
187
|
+
|
|
188
|
+
const now = new Date();
|
|
189
|
+
const end = new Date(now.getTime() + ttlHours * 3_600_000);
|
|
190
|
+
const id = `auth-${now.getTime()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
191
|
+
const instructionsHash = await _sha256(instructions);
|
|
192
|
+
|
|
193
|
+
const { kty, crv, x, y } = publicJwk;
|
|
194
|
+
|
|
195
|
+
const body = {
|
|
196
|
+
delegationId: id,
|
|
197
|
+
issuedAt: now.toISOString(),
|
|
198
|
+
scope,
|
|
199
|
+
boundaries,
|
|
200
|
+
timeWindow: { start: now.toISOString(), end: end.toISOString() },
|
|
201
|
+
operatorInstructions: instructions,
|
|
202
|
+
instructionsHash,
|
|
203
|
+
signerPublicKey: { kty, crv, x, y },
|
|
204
|
+
...(agentId ? { agentId } : {}),
|
|
205
|
+
...(metadata ? { metadata } : {}),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const signature = await _sign(privateKey, JSON.stringify(body));
|
|
209
|
+
const receipt = { ...body, signature };
|
|
210
|
+
const receiptId = await _sha256(JSON.stringify(receipt));
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
receipt,
|
|
214
|
+
receiptId,
|
|
215
|
+
systemPrompt: buildSystemPrompt(receipt, receiptId),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─────────────────────────────────────────────
|
|
220
|
+
// RECEIPT VERIFICATION
|
|
221
|
+
// ─────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Verify a receipt locally (no network call).
|
|
225
|
+
* Checks: found, not revoked, within time window, signature valid, hash matches,
|
|
226
|
+
* and optionally whether a proposed action falls within scope.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} receipt - The receipt object
|
|
229
|
+
* @param {string} receiptId - The SHA-256 receipt ID to verify against
|
|
230
|
+
* @param {object} [options]
|
|
231
|
+
* @param {boolean} [options.revoked] - Whether the receipt has been revoked (from your store)
|
|
232
|
+
* @param {string} [options.action] - Proposed action to scope-check
|
|
233
|
+
*
|
|
234
|
+
* @returns {{ authorized: boolean, checks: Array, receiptContext: object }}
|
|
235
|
+
*/
|
|
236
|
+
async function verify(receipt, receiptId, options = {}) {
|
|
237
|
+
const { revoked = false, action } = options;
|
|
238
|
+
const checks = [];
|
|
239
|
+
|
|
240
|
+
// 1. Not revoked
|
|
241
|
+
const notRevoked = !revoked && !receipt.revoked;
|
|
242
|
+
checks.push({
|
|
243
|
+
name: 'Not revoked',
|
|
244
|
+
passed: notRevoked,
|
|
245
|
+
detail: notRevoked ? 'Active' : 'This authorization has been revoked',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 2. Within time window
|
|
249
|
+
const now = new Date();
|
|
250
|
+
const start = new Date(receipt.timeWindow.start);
|
|
251
|
+
const end = new Date(receipt.timeWindow.end);
|
|
252
|
+
const inWindow = now >= start && now <= end;
|
|
253
|
+
checks.push({
|
|
254
|
+
name: 'Within time window',
|
|
255
|
+
passed: inWindow,
|
|
256
|
+
detail: inWindow
|
|
257
|
+
? `Expires ${end.toLocaleString()}`
|
|
258
|
+
: now < start
|
|
259
|
+
? `Not yet valid — starts ${start.toLocaleString()}`
|
|
260
|
+
: `Expired ${end.toLocaleString()}`,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 3. Signature valid
|
|
264
|
+
const { signature, ...bodyWithoutSig } = receipt;
|
|
265
|
+
const sigValid = await _verify(
|
|
266
|
+
receipt.signerPublicKey,
|
|
267
|
+
signature,
|
|
268
|
+
JSON.stringify(bodyWithoutSig)
|
|
269
|
+
);
|
|
270
|
+
checks.push({
|
|
271
|
+
name: 'Signature valid',
|
|
272
|
+
passed: sigValid,
|
|
273
|
+
detail: sigValid
|
|
274
|
+
? 'Cryptographic signature verified (ECDSA P-256)'
|
|
275
|
+
: 'Signature verification failed — receipt may be tampered',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 4. Receipt ID matches
|
|
279
|
+
const computedId = await _sha256(JSON.stringify(receipt));
|
|
280
|
+
const idMatches = computedId === receiptId;
|
|
281
|
+
checks.push({
|
|
282
|
+
name: 'Receipt ID matches',
|
|
283
|
+
passed: idMatches,
|
|
284
|
+
detail: idMatches
|
|
285
|
+
? 'SHA-256 content hash verified'
|
|
286
|
+
: 'ID mismatch — receipt content may have been altered',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 5. Instructions hash matches
|
|
290
|
+
const computedIH = await _sha256(receipt.operatorInstructions);
|
|
291
|
+
const ihMatches = computedIH === receipt.instructionsHash;
|
|
292
|
+
checks.push({
|
|
293
|
+
name: 'Instructions intact',
|
|
294
|
+
passed: ihMatches,
|
|
295
|
+
detail: ihMatches
|
|
296
|
+
? 'Instructions hash verified'
|
|
297
|
+
: 'Instructions hash mismatch — instructions may have been altered',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// 6. Optional scope check
|
|
301
|
+
if (action) {
|
|
302
|
+
const { withinScope, scopeScore, boundaryScore } = checkScope(action, receipt);
|
|
303
|
+
checks.push({
|
|
304
|
+
name: 'Action within scope',
|
|
305
|
+
passed: scopeScore >= 30,
|
|
306
|
+
detail: `${scopeScore}% keyword match with authorized scope`,
|
|
307
|
+
});
|
|
308
|
+
checks.push({
|
|
309
|
+
name: 'Action not blocked',
|
|
310
|
+
passed: boundaryScore < 50,
|
|
311
|
+
detail: `${boundaryScore}% overlap with off-limits boundaries`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const authorized = checks.every(c => c.passed);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
authorized,
|
|
319
|
+
result: authorized
|
|
320
|
+
? 'This action is authorized'
|
|
321
|
+
: 'Authorization denied — see checks for details',
|
|
322
|
+
checks,
|
|
323
|
+
receiptContext: {
|
|
324
|
+
scope: receipt.scope,
|
|
325
|
+
boundaries: receipt.boundaries,
|
|
326
|
+
operatorInstructions: receipt.operatorInstructions,
|
|
327
|
+
timeWindow: receipt.timeWindow,
|
|
328
|
+
issuedAt: receipt.issuedAt,
|
|
329
|
+
agentId: receipt.agentId,
|
|
330
|
+
revoked,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─────────────────────────────────────────────
|
|
336
|
+
// SYSTEM PROMPT BUILDER
|
|
337
|
+
// ─────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Build a ready-to-use system prompt block from a receipt.
|
|
341
|
+
* Paste this directly into any AI chat or agent system prompt.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} receipt
|
|
344
|
+
* @param {string} receiptId
|
|
345
|
+
* @param {string} [verifyUrl] - Optional URL to your verify endpoint
|
|
346
|
+
* @returns {string}
|
|
347
|
+
*/
|
|
348
|
+
function buildSystemPrompt(receipt, receiptId, verifyUrl) {
|
|
349
|
+
const expiry = new Date(receipt.timeWindow.end).toLocaleString();
|
|
350
|
+
const verifyLine = verifyUrl
|
|
351
|
+
? `\nVerify this authorization: ${verifyUrl}?receipt=${receiptId}`
|
|
352
|
+
: `\nAuthorization ID: ${receiptId}`;
|
|
353
|
+
|
|
354
|
+
return `You are authorized to act within the following scope:
|
|
355
|
+
|
|
356
|
+
${receipt.scope}
|
|
357
|
+
|
|
358
|
+
You must not:
|
|
359
|
+
${receipt.boundaries}
|
|
360
|
+
|
|
361
|
+
Operator instructions:
|
|
362
|
+
${receipt.operatorInstructions}
|
|
363
|
+
|
|
364
|
+
This authorization is valid until: ${expiry}${verifyLine}
|
|
365
|
+
|
|
366
|
+
Before taking any significant action, confirm it falls within the authorized scope above. If uncertain, ask for clarification rather than proceeding.`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─────────────────────────────────────────────
|
|
370
|
+
// UTILITY
|
|
371
|
+
// ─────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Compute the receipt ID (SHA-256 of the full receipt JSON).
|
|
375
|
+
* Useful for re-deriving the ID from a stored receipt.
|
|
376
|
+
*
|
|
377
|
+
* @param {object} receipt
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
async function receiptId(receipt) {
|
|
381
|
+
return _sha256(JSON.stringify(receipt));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if a receipt is currently active (not expired, not revoked).
|
|
386
|
+
* @param {object} receipt
|
|
387
|
+
* @param {boolean} [revoked=false]
|
|
388
|
+
* @returns {boolean}
|
|
389
|
+
*/
|
|
390
|
+
function isActive(receipt, revoked = false) {
|
|
391
|
+
if (revoked || receipt.revoked) return false;
|
|
392
|
+
const now = new Date();
|
|
393
|
+
return now >= new Date(receipt.timeWindow.start) && now <= new Date(receipt.timeWindow.end);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get seconds remaining before a receipt expires.
|
|
398
|
+
* Returns 0 if already expired.
|
|
399
|
+
* @param {object} receipt
|
|
400
|
+
* @returns {number}
|
|
401
|
+
*/
|
|
402
|
+
function secondsRemaining(receipt) {
|
|
403
|
+
const remaining = new Date(receipt.timeWindow.end) - new Date();
|
|
404
|
+
return Math.max(0, Math.floor(remaining / 1000));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─────────────────────────────────────────────
|
|
408
|
+
// EXPORTS
|
|
409
|
+
// ─────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
const AuthProof = {
|
|
412
|
+
// Key management
|
|
413
|
+
generateKey,
|
|
414
|
+
importPrivateKey,
|
|
415
|
+
|
|
416
|
+
// Core protocol
|
|
417
|
+
create,
|
|
418
|
+
verify,
|
|
419
|
+
|
|
420
|
+
// Utilities
|
|
421
|
+
buildSystemPrompt,
|
|
422
|
+
checkScope,
|
|
423
|
+
receiptId,
|
|
424
|
+
isActive,
|
|
425
|
+
secondsRemaining,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// ESM + CJS compatible export
|
|
429
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
430
|
+
module.exports = AuthProof;
|
|
431
|
+
} else if (typeof globalThis !== 'undefined') {
|
|
432
|
+
globalThis.AuthProof = AuthProof;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export default AuthProof;
|
|
436
|
+
export {
|
|
437
|
+
generateKey,
|
|
438
|
+
importPrivateKey,
|
|
439
|
+
create,
|
|
440
|
+
verify,
|
|
441
|
+
buildSystemPrompt,
|
|
442
|
+
checkScope,
|
|
443
|
+
receiptId,
|
|
444
|
+
isActive,
|
|
445
|
+
secondsRemaining,
|
|
446
|
+
};
|