provenance-protocol 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/LICENSE +21 -0
- package/README.md +134 -0
- package/package.json +36 -0
- package/src/index.d.ts +180 -0
- package/src/index.js +570 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ilucky21c
|
|
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,134 @@
|
|
|
1
|
+
# provenance-protocol
|
|
2
|
+
|
|
3
|
+
SDK for querying the [Provenance](https://provenance.dev) agent identity index.
|
|
4
|
+
|
|
5
|
+
Drop this into any receiving system — marketplace, API, agent orchestrator —
|
|
6
|
+
to verify an AI agent's identity and trust profile before allowing it in.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install provenance-protocol
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { provenance } from 'provenance-protocol';
|
|
18
|
+
|
|
19
|
+
// Check a single agent
|
|
20
|
+
const trust = await provenance.check('provenance:github:alice/research-assistant');
|
|
21
|
+
console.log(trust);
|
|
22
|
+
// {
|
|
23
|
+
// found: true,
|
|
24
|
+
// declared: true,
|
|
25
|
+
// age_days: 142,
|
|
26
|
+
// confidence: 0.9,
|
|
27
|
+
// capabilities: ['read:web', 'write:summaries'],
|
|
28
|
+
// constraints: ['no:financial:transact', 'no:pii'],
|
|
29
|
+
// incidents: 0,
|
|
30
|
+
// status: 'active'
|
|
31
|
+
// }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## gate() — all checks in one call
|
|
37
|
+
|
|
38
|
+
The most useful method for receiving systems.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const result = await provenance.gate('provenance:github:alice/agent', {
|
|
42
|
+
requireDeclared: true, // must have PROVENANCE.yml
|
|
43
|
+
requireConstraints: ['no:financial:transact', 'no:pii'], // must have committed to these
|
|
44
|
+
requireClean: true, // no open incidents
|
|
45
|
+
requireMinAge: 30, // must be at least 30 days old
|
|
46
|
+
requireMinConfidence: 0.7, // classification confidence
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!result.allowed) {
|
|
50
|
+
return res.status(403).json({ error: result.reason });
|
|
51
|
+
// e.g. "Agent has not committed to constraint: no:financial:transact"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// result.trust has the full profile if you need it
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Individual methods
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// Boolean checks
|
|
63
|
+
await provenance.hasConstraint(id, 'no:financial:transact'); // → true/false
|
|
64
|
+
await provenance.hasCapability(id, 'read:web'); // → true/false
|
|
65
|
+
await provenance.isClean(id); // → true/false
|
|
66
|
+
await provenance.isOldEnough(id, 90); // → true/false (90+ days)
|
|
67
|
+
|
|
68
|
+
// Search for agents
|
|
69
|
+
const results = await provenance.search({
|
|
70
|
+
capabilities: ['read:web'],
|
|
71
|
+
constraints: ['no:financial:transact'],
|
|
72
|
+
declared: true,
|
|
73
|
+
limit: 10,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { Provenance } from 'provenance-protocol';
|
|
83
|
+
|
|
84
|
+
const provenance = new Provenance({
|
|
85
|
+
apiUrl: 'https://your-own-provenance-instance.com',
|
|
86
|
+
cacheTTL: 300, // Cache results for 5 minutes (default)
|
|
87
|
+
onApiError: 'deny' // 'throw' | 'allow' | 'deny' (default: 'throw')
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Caching
|
|
92
|
+
|
|
93
|
+
All `check()` calls are automatically cached with a configurable TTL (default 5 minutes). This dramatically reduces latency and load when used in middleware or hot paths.
|
|
94
|
+
|
|
95
|
+
### Fail-Safe Behavior
|
|
96
|
+
|
|
97
|
+
When the Provenance API is unreachable, you can configure how `gate()` responds:
|
|
98
|
+
|
|
99
|
+
- `'throw'` (default): Throws an error, letting your app handle it
|
|
100
|
+
- `'deny'`: Returns `{ allowed: false }` — fail closed
|
|
101
|
+
- `'allow'`: Returns `{ allowed: true }` — fail open
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// Fail closed if API is down
|
|
105
|
+
const result = await provenance.gate(id, {
|
|
106
|
+
requireDeclared: true,
|
|
107
|
+
onApiError: 'deny'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.fallback) {
|
|
111
|
+
console.warn('Verification skipped due to API unavailability');
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## What the trust object contains
|
|
118
|
+
|
|
119
|
+
| Field | Type | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `found` | boolean | Agent exists in Provenance index |
|
|
122
|
+
| `declared` | boolean | Has a PROVENANCE.yml file |
|
|
123
|
+
| `age_days` | number | Days since first indexed |
|
|
124
|
+
| `confidence` | number | 0–1 classification confidence |
|
|
125
|
+
| `capabilities` | string[] | What the agent declares it can do |
|
|
126
|
+
| `constraints` | string[] | What the agent has publicly committed never to do |
|
|
127
|
+
| `incidents` | number | Number of open incidents |
|
|
128
|
+
| `status` | string | active / suspended / removed |
|
|
129
|
+
| `model` | object | `{ provider, model_id }` if declared |
|
|
130
|
+
| `first_seen` | string | ISO date of first public appearance |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## MIT License — provenance.dev
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "provenance-protocol",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "SDK for querying the Provenance agent identity index",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/index.js",
|
|
16
|
+
"src/index.d.ts",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"agent-identity",
|
|
22
|
+
"provenance",
|
|
23
|
+
"trust",
|
|
24
|
+
"llm",
|
|
25
|
+
"verification"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/ilucky21c/provenance-protocol"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://provenance.dev",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=14.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance-protocol TypeScript definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TrustProfile {
|
|
6
|
+
found: boolean;
|
|
7
|
+
provenance_id: string;
|
|
8
|
+
platform?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
declared?: boolean;
|
|
11
|
+
confidence?: number;
|
|
12
|
+
age_days?: number | null;
|
|
13
|
+
capabilities?: string[];
|
|
14
|
+
constraints?: string[];
|
|
15
|
+
incidents?: number;
|
|
16
|
+
model?: {
|
|
17
|
+
provider: string;
|
|
18
|
+
model_id: string;
|
|
19
|
+
} | null;
|
|
20
|
+
status?: string;
|
|
21
|
+
first_seen?: string | null;
|
|
22
|
+
url?: string;
|
|
23
|
+
/** Base64-encoded Ed25519 SPKI public key, if the agent registered one. */
|
|
24
|
+
public_key?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GateResult {
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
reason: string | null;
|
|
30
|
+
trust: TrustProfile | null;
|
|
31
|
+
fallback?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SignedProof {
|
|
35
|
+
/** The nonce you sent to the agent. */
|
|
36
|
+
nonce: string;
|
|
37
|
+
/** The base64 signature the agent returned. */
|
|
38
|
+
signature: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface VerifyResult {
|
|
42
|
+
verified: boolean;
|
|
43
|
+
reason: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GateOptions {
|
|
47
|
+
requireDeclared?: boolean;
|
|
48
|
+
requireConstraints?: string[];
|
|
49
|
+
requireCapabilities?: string[];
|
|
50
|
+
requireClean?: boolean;
|
|
51
|
+
requireMinAge?: number;
|
|
52
|
+
requireMinConfidence?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Cryptographically verify the agent controls its declared private key.
|
|
55
|
+
* The agent must have signed `${provenanceId}:${nonce}` with its private key.
|
|
56
|
+
*/
|
|
57
|
+
requireSignedProof?: SignedProof;
|
|
58
|
+
onApiError?: 'throw' | 'allow' | 'deny';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SearchParams {
|
|
62
|
+
q?: string;
|
|
63
|
+
platform?: string;
|
|
64
|
+
capabilities?: string[];
|
|
65
|
+
constraints?: string[];
|
|
66
|
+
declared?: boolean;
|
|
67
|
+
minConfidence?: number;
|
|
68
|
+
limit?: number;
|
|
69
|
+
offset?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SearchResult {
|
|
73
|
+
agents: TrustProfile[];
|
|
74
|
+
total: number;
|
|
75
|
+
limit: number;
|
|
76
|
+
offset: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ProvenanceOptions {
|
|
80
|
+
apiUrl?: string;
|
|
81
|
+
cacheTTL?: number;
|
|
82
|
+
onApiError?: 'throw' | 'allow' | 'deny';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class Provenance {
|
|
86
|
+
constructor(options?: ProvenanceOptions);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check an agent's trust profile.
|
|
90
|
+
* Results are cached for the configured TTL (default 5 minutes).
|
|
91
|
+
*/
|
|
92
|
+
check(provenanceId: string): Promise<TrustProfile>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns true if the agent has publicly committed to a constraint.
|
|
96
|
+
*/
|
|
97
|
+
hasConstraint(provenanceId: string, constraint: string): Promise<boolean>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns true if the agent has declared a capability.
|
|
101
|
+
*/
|
|
102
|
+
hasCapability(provenanceId: string, capability: string): Promise<boolean>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns true if the agent has no open incidents.
|
|
106
|
+
*/
|
|
107
|
+
isClean(provenanceId: string): Promise<boolean>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns true if the agent has existed for at least minDays.
|
|
111
|
+
*/
|
|
112
|
+
isOldEnough(provenanceId: string, minDays: number): Promise<boolean>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run all your trust requirements in one call.
|
|
116
|
+
* Supports fail-safe behavior via onApiError option.
|
|
117
|
+
* Pass requireSignedProof to also verify cryptographic identity.
|
|
118
|
+
*/
|
|
119
|
+
gate(provenanceId: string, options?: GateOptions): Promise<GateResult>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Verify that a running agent cryptographically owns the identity it claims.
|
|
123
|
+
*
|
|
124
|
+
* Protocol: send the agent a nonce, ask it to sign `${provenanceId}:${nonce}`,
|
|
125
|
+
* then call this to verify the returned signature against the public key in the index.
|
|
126
|
+
*
|
|
127
|
+
* @param provenanceId The agent's Provenance ID
|
|
128
|
+
* @param nonce The nonce you sent to the agent
|
|
129
|
+
* @param signature The base64 signature the agent returned
|
|
130
|
+
*/
|
|
131
|
+
verifySignature(provenanceId: string, nonce: string, signature: string): Promise<VerifyResult>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Search for agents by capabilities, constraints, platform etc.
|
|
135
|
+
*/
|
|
136
|
+
search(params?: SearchParams): Promise<SearchResult>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check multiple agents in a single request (max 50).
|
|
140
|
+
* More efficient than calling check() multiple times.
|
|
141
|
+
*/
|
|
142
|
+
checkBatch(provenanceIds: string[]): Promise<Record<string, TrustProfile>>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Gate multiple agents in a single request.
|
|
146
|
+
*/
|
|
147
|
+
gateBatch(provenanceIds: string[], options?: GateOptions): Promise<Record<string, GateResult>>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Register or update this agent in the Provenance index.
|
|
151
|
+
* Call once at agent startup — idempotent, safe to call on every boot.
|
|
152
|
+
*/
|
|
153
|
+
register(profile: RegisterProfile): Promise<RegisterResult>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface RegisterProfile {
|
|
157
|
+
id: string;
|
|
158
|
+
url: string;
|
|
159
|
+
name?: string;
|
|
160
|
+
description?: string;
|
|
161
|
+
capabilities?: string[];
|
|
162
|
+
constraints?: string[];
|
|
163
|
+
model_provider?: string;
|
|
164
|
+
model_id?: string;
|
|
165
|
+
contact_url?: string;
|
|
166
|
+
ajp_endpoint?: string;
|
|
167
|
+
public_key?: string;
|
|
168
|
+
version?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface RegisterResult {
|
|
172
|
+
created?: boolean;
|
|
173
|
+
updated?: boolean;
|
|
174
|
+
agent: object;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Default instance pointing at provenance.dev
|
|
179
|
+
*/
|
|
180
|
+
export const provenance: Provenance;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance-protocol SDK
|
|
3
|
+
*
|
|
4
|
+
* Drop this into any receiving system — marketplace, API, agent orchestrator.
|
|
5
|
+
* Query the Provenance index before trusting an agent.
|
|
6
|
+
*
|
|
7
|
+
* npm install provenance-protocol
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { Provenance } from 'provenance-protocol';
|
|
11
|
+
* const trust = await Provenance.check('provenance:github:alice/research-assistant');
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Cryptographic helpers ──────────────────────────────────────────────────
|
|
15
|
+
// Uses the Web Crypto API (crypto.subtle) which is available in:
|
|
16
|
+
// - All modern browsers
|
|
17
|
+
// - Node.js 18+ (globalThis.crypto.subtle)
|
|
18
|
+
// No external dependencies needed.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify an Ed25519 signature against a public key stored in the index.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} publicKeyBase64 Base64-encoded SPKI DER public key
|
|
24
|
+
* @param {string} signatureBase64 Base64-encoded raw signature (64 bytes)
|
|
25
|
+
* @param {string} message The message that was signed (UTF-8 string)
|
|
26
|
+
* @returns {Promise<boolean>}
|
|
27
|
+
*/
|
|
28
|
+
async function _verifyEd25519(publicKeyBase64, signatureBase64, message) {
|
|
29
|
+
const subtle = globalThis.crypto?.subtle;
|
|
30
|
+
if (!subtle) throw new Error('Web Crypto API (crypto.subtle) not available');
|
|
31
|
+
|
|
32
|
+
const keyBuffer = Uint8Array.from(Buffer.from(publicKeyBase64, 'base64'));
|
|
33
|
+
const sigBuffer = Uint8Array.from(Buffer.from(signatureBase64, 'base64'));
|
|
34
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
35
|
+
|
|
36
|
+
const cryptoKey = await subtle.importKey(
|
|
37
|
+
'spki',
|
|
38
|
+
keyBuffer,
|
|
39
|
+
{ name: 'Ed25519' },
|
|
40
|
+
false,
|
|
41
|
+
['verify']
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return subtle.verify('Ed25519', cryptoKey, sigBuffer, msgBuffer);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_API = 'https://provenance.dev';
|
|
48
|
+
const DEFAULT_CACHE_TTL = 300; // 5 minutes
|
|
49
|
+
|
|
50
|
+
// Simple LRU cache
|
|
51
|
+
class Cache {
|
|
52
|
+
constructor(ttlSeconds = DEFAULT_CACHE_TTL) {
|
|
53
|
+
this.cache = new Map();
|
|
54
|
+
this.ttl = ttlSeconds * 1000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get(key) {
|
|
58
|
+
const item = this.cache.get(key);
|
|
59
|
+
if (!item) return null;
|
|
60
|
+
if (Date.now() > item.expiry) {
|
|
61
|
+
this.cache.delete(key);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return item.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set(key, value) {
|
|
68
|
+
this.cache.set(key, {
|
|
69
|
+
value,
|
|
70
|
+
expiry: Date.now() + this.ttl
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
clear() {
|
|
75
|
+
this.cache.clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class Provenance {
|
|
80
|
+
|
|
81
|
+
constructor({
|
|
82
|
+
apiUrl = DEFAULT_API,
|
|
83
|
+
cacheTTL = DEFAULT_CACHE_TTL,
|
|
84
|
+
onApiError = 'throw' // 'throw' | 'allow' | 'deny'
|
|
85
|
+
} = {}) {
|
|
86
|
+
this.apiUrl = apiUrl.replace(/\/$/, '');
|
|
87
|
+
this.cache = new Cache(cacheTTL);
|
|
88
|
+
this.onApiError = onApiError;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Main method — the one most receiving systems need ────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check an agent's trust profile.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} provenanceId e.g. "provenance:github:alice/research-assistant"
|
|
97
|
+
* @returns {object} trust summary
|
|
98
|
+
*
|
|
99
|
+
* Example:
|
|
100
|
+
* const trust = await provenance.check('provenance:github:alice/research-assistant');
|
|
101
|
+
* // {
|
|
102
|
+
* // found: true,
|
|
103
|
+
* // declared: true, — has PROVENANCE.yml
|
|
104
|
+
* // age_days: 142, — how long this agent has existed publicly
|
|
105
|
+
* // confidence: 0.9,
|
|
106
|
+
* // capabilities: ['read:web', 'write:summaries'],
|
|
107
|
+
* // constraints: ['no:financial:transact', 'no:pii'],
|
|
108
|
+
* // incidents: 0,
|
|
109
|
+
* // model: { provider: 'anthropic', model_id: 'claude-sonnet-4-5' },
|
|
110
|
+
* // status: 'active',
|
|
111
|
+
* // }
|
|
112
|
+
*/
|
|
113
|
+
async check(provenanceId) {
|
|
114
|
+
// Check cache first
|
|
115
|
+
const cached = this.cache.get(provenanceId);
|
|
116
|
+
if (cached) return cached;
|
|
117
|
+
|
|
118
|
+
const path = this._idToPath(provenanceId);
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(`${this.apiUrl}/api/agent/${path}`);
|
|
121
|
+
if (res.status === 404) {
|
|
122
|
+
const notFound = { found: false, provenance_id: provenanceId };
|
|
123
|
+
this.cache.set(provenanceId, notFound);
|
|
124
|
+
return notFound;
|
|
125
|
+
}
|
|
126
|
+
if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
|
|
129
|
+
const result = {
|
|
130
|
+
found: true,
|
|
131
|
+
provenance_id: data.provenance_id,
|
|
132
|
+
platform: data.platform,
|
|
133
|
+
name: data.name,
|
|
134
|
+
declared: data.declared,
|
|
135
|
+
confidence: data.confidence,
|
|
136
|
+
age_days: data.timestamps?.first_seen
|
|
137
|
+
? Math.floor((Date.now() - new Date(data.timestamps.first_seen)) / 86400000)
|
|
138
|
+
: null,
|
|
139
|
+
capabilities: data.capabilities || [],
|
|
140
|
+
constraints: data.constraints || [],
|
|
141
|
+
incidents: data.incident_count || 0,
|
|
142
|
+
model: data.model || null,
|
|
143
|
+
status: data.status || 'unknown',
|
|
144
|
+
first_seen: data.timestamps?.first_seen || null,
|
|
145
|
+
url: data.url,
|
|
146
|
+
public_key: data.public_key || null,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Cache the result
|
|
150
|
+
this.cache.set(provenanceId, result);
|
|
151
|
+
return result;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
throw new Error(`Provenance.check failed: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Convenience guard methods — boolean checks ───────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns true if the agent has publicly committed to a constraint.
|
|
161
|
+
*
|
|
162
|
+
* Example:
|
|
163
|
+
* if (!await provenance.hasConstraint(id, 'no:financial:transact')) {
|
|
164
|
+
* throw new Error('Agent not cleared for financial operations');
|
|
165
|
+
* }
|
|
166
|
+
*/
|
|
167
|
+
async hasConstraint(provenanceId, constraint) {
|
|
168
|
+
const trust = await this.check(provenanceId);
|
|
169
|
+
return trust.found && trust.constraints.includes(constraint);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns true if the agent has declared a capability.
|
|
174
|
+
*/
|
|
175
|
+
async hasCapability(provenanceId, capability) {
|
|
176
|
+
const trust = await this.check(provenanceId);
|
|
177
|
+
return trust.found && trust.capabilities.includes(capability);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns true if the agent has no open incidents.
|
|
182
|
+
*/
|
|
183
|
+
async isClean(provenanceId) {
|
|
184
|
+
const trust = await this.check(provenanceId);
|
|
185
|
+
return trust.found && trust.incidents === 0 && trust.status === 'active';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns true if the agent has existed for at least minDays.
|
|
190
|
+
* Age is a proxy for reliability — a 6-month-old agent with no incidents
|
|
191
|
+
* is more trustworthy than a brand-new one.
|
|
192
|
+
*/
|
|
193
|
+
async isOldEnough(provenanceId, minDays) {
|
|
194
|
+
const trust = await this.check(provenanceId);
|
|
195
|
+
return trust.found && trust.age_days !== null && trust.age_days >= minDays;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Cryptographic identity verification ──────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Verify that a running agent cryptographically owns the identity it claims.
|
|
202
|
+
*
|
|
203
|
+
* This closes the gap between "a repo declares this identity" and "the agent
|
|
204
|
+
* talking to you actually controls that repo's private key."
|
|
205
|
+
*
|
|
206
|
+
* Protocol (challenge-response):
|
|
207
|
+
* 1. Receiving system generates a nonce: const nonce = crypto.randomUUID()
|
|
208
|
+
* 2. Receiving system sends nonce to agent
|
|
209
|
+
* 3. Agent signs: signChallenge(privateKey, provenanceId, nonce) → signature
|
|
210
|
+
* 4. Receiving system verifies: await provenance.verifySignature(id, nonce, signature)
|
|
211
|
+
*
|
|
212
|
+
* @param {string} provenanceId e.g. "provenance:github:alice/research-assistant"
|
|
213
|
+
* @param {string} nonce The nonce you sent to the agent (UUID or random string)
|
|
214
|
+
* @param {string} signatureBase64 Base64 signature returned by the agent
|
|
215
|
+
* @returns {{ verified: boolean, reason: string | null }}
|
|
216
|
+
*
|
|
217
|
+
* Example:
|
|
218
|
+
* const nonce = crypto.randomUUID();
|
|
219
|
+
* // ... send nonce to agent, receive signature back ...
|
|
220
|
+
* const result = await provenance.verifySignature(
|
|
221
|
+
* 'provenance:github:alice/research-assistant',
|
|
222
|
+
* nonce,
|
|
223
|
+
* agentSignature
|
|
224
|
+
* );
|
|
225
|
+
* if (!result.verified) throw new Error(`Identity check failed: ${result.reason}`);
|
|
226
|
+
*/
|
|
227
|
+
async verifySignature(provenanceId, nonce, signatureBase64) {
|
|
228
|
+
let trust;
|
|
229
|
+
try {
|
|
230
|
+
trust = await this.check(provenanceId);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return { verified: false, reason: `Could not fetch agent profile: ${e.message}` };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!trust.found) {
|
|
236
|
+
return { verified: false, reason: 'Agent not found in Provenance index' };
|
|
237
|
+
}
|
|
238
|
+
if (!trust.public_key) {
|
|
239
|
+
return { verified: false, reason: 'Agent has no public key registered in PROVENANCE.yml' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The signed message is always: "<provenanceId>:<nonce>"
|
|
243
|
+
// This binds the signature to both the agent's identity and the specific challenge,
|
|
244
|
+
// preventing replay attacks and cross-agent signature reuse.
|
|
245
|
+
const message = `${provenanceId}:${nonce}`;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const verified = await _verifyEd25519(trust.public_key, signatureBase64, message);
|
|
249
|
+
return { verified, reason: verified ? null : 'Signature is invalid' };
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { verified: false, reason: `Signature verification error: ${e.message}` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Gate method — combine all checks in one call ─────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run all your trust requirements in one call.
|
|
259
|
+
* Returns { allowed, reason, trust }.
|
|
260
|
+
*
|
|
261
|
+
* Example:
|
|
262
|
+
* const result = await provenance.gate('provenance:github:alice/agent', {
|
|
263
|
+
* requireDeclared: true,
|
|
264
|
+
* requireConstraints: ['no:financial:transact', 'no:pii'],
|
|
265
|
+
* requireClean: true,
|
|
266
|
+
* requireMinAge: 30,
|
|
267
|
+
* requireMinConfidence: 0.7,
|
|
268
|
+
* });
|
|
269
|
+
*
|
|
270
|
+
* if (!result.allowed) {
|
|
271
|
+
* return res.status(403).json({ error: result.reason });
|
|
272
|
+
* }
|
|
273
|
+
*/
|
|
274
|
+
async gate(provenanceId, {
|
|
275
|
+
requireDeclared = false,
|
|
276
|
+
requireConstraints = [],
|
|
277
|
+
requireCapabilities = [],
|
|
278
|
+
requireClean = true,
|
|
279
|
+
requireMinAge = 0,
|
|
280
|
+
requireMinConfidence = 0,
|
|
281
|
+
requireSignedProof = null,
|
|
282
|
+
// { nonce: string, signature: string }
|
|
283
|
+
// When provided, cryptographically verifies the agent controls its declared
|
|
284
|
+
// private key. The agent must have signed `${provenanceId}:${nonce}`.
|
|
285
|
+
onApiError, // Override instance default if provided
|
|
286
|
+
} = {}) {
|
|
287
|
+
const errorPolicy = onApiError || this.onApiError;
|
|
288
|
+
let trust;
|
|
289
|
+
try {
|
|
290
|
+
trust = await this.check(provenanceId);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
// Handle API failures based on policy
|
|
293
|
+
if (errorPolicy === 'allow') {
|
|
294
|
+
return {
|
|
295
|
+
allowed: true,
|
|
296
|
+
reason: 'Verification skipped (API unavailable)',
|
|
297
|
+
trust: null,
|
|
298
|
+
fallback: true
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (errorPolicy === 'deny') {
|
|
302
|
+
return {
|
|
303
|
+
allowed: false,
|
|
304
|
+
reason: `Verification failed (API unavailable): ${e.message}`,
|
|
305
|
+
trust: null,
|
|
306
|
+
fallback: true
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// errorPolicy === 'throw'
|
|
310
|
+
throw e;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!trust.found) {
|
|
314
|
+
return { allowed: false, reason: 'Agent not found in Provenance index', trust };
|
|
315
|
+
}
|
|
316
|
+
if (trust.status !== 'active') {
|
|
317
|
+
return { allowed: false, reason: `Agent status is ${trust.status}`, trust };
|
|
318
|
+
}
|
|
319
|
+
if (requireDeclared && !trust.declared) {
|
|
320
|
+
return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
|
|
321
|
+
}
|
|
322
|
+
if (requireClean && trust.incidents > 0) {
|
|
323
|
+
return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
|
|
324
|
+
}
|
|
325
|
+
if (requireMinConfidence && trust.confidence < requireMinConfidence) {
|
|
326
|
+
return { allowed: false, reason: `Agent confidence ${trust.confidence} below required ${requireMinConfidence}`, trust };
|
|
327
|
+
}
|
|
328
|
+
if (requireMinAge && (trust.age_days === null || trust.age_days < requireMinAge)) {
|
|
329
|
+
return { allowed: false, reason: `Agent is ${trust.age_days ?? 0} days old, minimum is ${requireMinAge}`, trust };
|
|
330
|
+
}
|
|
331
|
+
for (const constraint of requireConstraints) {
|
|
332
|
+
if (!trust.constraints.includes(constraint)) {
|
|
333
|
+
return { allowed: false, reason: `Agent has not committed to constraint: ${constraint}`, trust };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const capability of requireCapabilities) {
|
|
337
|
+
if (!trust.capabilities.includes(capability)) {
|
|
338
|
+
return { allowed: false, reason: `Agent does not declare capability: ${capability}`, trust };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (requireSignedProof) {
|
|
342
|
+
const { nonce, signature } = requireSignedProof;
|
|
343
|
+
const result = await this.verifySignature(provenanceId, nonce, signature);
|
|
344
|
+
if (!result.verified) {
|
|
345
|
+
return { allowed: false, reason: `Cryptographic identity verification failed: ${result.reason}`, trust };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { allowed: true, reason: null, trust };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Search for agents by capabilities, constraints, platform etc.
|
|
356
|
+
*
|
|
357
|
+
* Example:
|
|
358
|
+
* const agents = await provenance.search({
|
|
359
|
+
* capabilities: ['read:web'],
|
|
360
|
+
* constraints: ['no:financial:transact'],
|
|
361
|
+
* declared: true,
|
|
362
|
+
* });
|
|
363
|
+
*/
|
|
364
|
+
async search(params = {}) {
|
|
365
|
+
const qs = new URLSearchParams();
|
|
366
|
+
if (params.q) qs.set('q', params.q);
|
|
367
|
+
if (params.platform) qs.set('platform', params.platform);
|
|
368
|
+
if (params.capabilities?.length) qs.set('capabilities', params.capabilities.join(','));
|
|
369
|
+
if (params.constraints?.length) qs.set('constraints', params.constraints.join(','));
|
|
370
|
+
if (params.declared !== undefined) qs.set('declared', String(params.declared));
|
|
371
|
+
if (params.minConfidence) qs.set('min_confidence', String(params.minConfidence));
|
|
372
|
+
if (params.limit) qs.set('limit', String(params.limit));
|
|
373
|
+
if (params.offset) qs.set('offset', String(params.offset));
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const res = await fetch(`${this.apiUrl}/api/search?${qs}`);
|
|
377
|
+
if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
|
|
378
|
+
return res.json();
|
|
379
|
+
} catch (e) {
|
|
380
|
+
throw new Error(`Provenance.search failed: ${e.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Batch operations ────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Check multiple agents in a single request.
|
|
388
|
+
* More efficient than calling check() multiple times.
|
|
389
|
+
*
|
|
390
|
+
* @param {string[]} provenanceIds - Array of provenance IDs (max 50)
|
|
391
|
+
* @returns {object} Map of provenance_id → trust profile
|
|
392
|
+
*/
|
|
393
|
+
async checkBatch(provenanceIds) {
|
|
394
|
+
if (!Array.isArray(provenanceIds) || provenanceIds.length === 0) {
|
|
395
|
+
throw new Error('provenanceIds must be a non-empty array');
|
|
396
|
+
}
|
|
397
|
+
if (provenanceIds.length > 50) {
|
|
398
|
+
throw new Error('Maximum 50 IDs per batch request');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check cache first, collect uncached IDs
|
|
402
|
+
const results = {};
|
|
403
|
+
const uncached = [];
|
|
404
|
+
|
|
405
|
+
for (const id of provenanceIds) {
|
|
406
|
+
const cached = this.cache.get(id);
|
|
407
|
+
if (cached) {
|
|
408
|
+
results[id] = cached;
|
|
409
|
+
} else {
|
|
410
|
+
uncached.push(id);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// If all cached, return immediately
|
|
415
|
+
if (uncached.length === 0) {
|
|
416
|
+
return results;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Fetch uncached from API
|
|
420
|
+
try {
|
|
421
|
+
const res = await fetch(`${this.apiUrl}/api/agents/batch`, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: { 'Content-Type': 'application/json' },
|
|
424
|
+
body: JSON.stringify({ ids: uncached }),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
|
|
428
|
+
|
|
429
|
+
const data = await res.json();
|
|
430
|
+
|
|
431
|
+
// Cache and merge results
|
|
432
|
+
for (const id of uncached) {
|
|
433
|
+
const profile = data.results?.[id] || { found: false, provenance_id: id };
|
|
434
|
+
this.cache.set(id, profile);
|
|
435
|
+
results[id] = profile;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return results;
|
|
439
|
+
} catch (e) {
|
|
440
|
+
throw new Error(`Provenance.checkBatch failed: ${e.message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Gate multiple agents in a single request.
|
|
446
|
+
*
|
|
447
|
+
* @param {string[]} provenanceIds - Array of provenance IDs
|
|
448
|
+
* @param {object} options - Same options as gate()
|
|
449
|
+
* @returns {object} Map of provenance_id → gate result
|
|
450
|
+
*/
|
|
451
|
+
async gateBatch(provenanceIds, options = {}) {
|
|
452
|
+
const profiles = await this.checkBatch(provenanceIds);
|
|
453
|
+
const results = {};
|
|
454
|
+
|
|
455
|
+
for (const id of provenanceIds) {
|
|
456
|
+
const trust = profiles[id];
|
|
457
|
+
results[id] = this._evaluateGate(trust, options);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return results;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Internal: evaluate gate rules against a trust profile
|
|
464
|
+
_evaluateGate(trust, options) {
|
|
465
|
+
const {
|
|
466
|
+
requireDeclared = false,
|
|
467
|
+
requireConstraints = [],
|
|
468
|
+
requireCapabilities = [],
|
|
469
|
+
requireClean = true,
|
|
470
|
+
requireMinAge = 0,
|
|
471
|
+
requireMinConfidence = 0,
|
|
472
|
+
} = options;
|
|
473
|
+
|
|
474
|
+
if (!trust || !trust.found) {
|
|
475
|
+
return { allowed: false, reason: 'Agent not found in Provenance index', trust };
|
|
476
|
+
}
|
|
477
|
+
if (trust.status !== 'active') {
|
|
478
|
+
return { allowed: false, reason: `Agent status is ${trust.status}`, trust };
|
|
479
|
+
}
|
|
480
|
+
if (requireDeclared && !trust.declared) {
|
|
481
|
+
return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
|
|
482
|
+
}
|
|
483
|
+
for (const c of requireConstraints) {
|
|
484
|
+
if (!trust.constraints?.includes(c)) {
|
|
485
|
+
return { allowed: false, reason: `Agent has not committed to constraint: ${c}`, trust };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const c of requireCapabilities) {
|
|
489
|
+
if (!trust.capabilities?.includes(c)) {
|
|
490
|
+
return { allowed: false, reason: `Agent does not have capability: ${c}`, trust };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (requireClean && trust.incidents > 0) {
|
|
494
|
+
return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
|
|
495
|
+
}
|
|
496
|
+
if (requireMinAge > 0 && (trust.age_days || 0) < requireMinAge) {
|
|
497
|
+
return { allowed: false, reason: `Agent is only ${trust.age_days || 0} days old (minimum: ${requireMinAge})`, trust };
|
|
498
|
+
}
|
|
499
|
+
if (requireMinConfidence > 0 && (trust.confidence || 0) < requireMinConfidence) {
|
|
500
|
+
return { allowed: false, reason: `Agent confidence ${trust.confidence} below minimum ${requireMinConfidence}`, trust };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return { allowed: true, reason: null, trust };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
_idToPath(provenanceId) {
|
|
509
|
+
// provenance:github:alice/research-assistant
|
|
510
|
+
// → github/alice/research-assistant
|
|
511
|
+
return provenanceId.replace('provenance:', '').replace(':', '/');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── Self-registration ─────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Register or update this agent in the Provenance index.
|
|
519
|
+
* Call once at agent startup — idempotent, safe to call on every boot.
|
|
520
|
+
*
|
|
521
|
+
* Example:
|
|
522
|
+
* import { provenance } from 'provenance-protocol';
|
|
523
|
+
*
|
|
524
|
+
* await provenance.register({
|
|
525
|
+
* id: 'provenance:github:your-org/your-agent',
|
|
526
|
+
* url: 'https://github.com/your-org/your-agent',
|
|
527
|
+
* name: 'Your Agent',
|
|
528
|
+
* description: 'What it does',
|
|
529
|
+
* capabilities: ['read:web', 'write:summaries'],
|
|
530
|
+
* constraints: ['no:pii', 'no:financial:transact'],
|
|
531
|
+
* });
|
|
532
|
+
*
|
|
533
|
+
* @param {object} profile
|
|
534
|
+
* @param {string} profile.id provenance:<platform>:<owner>/<name>
|
|
535
|
+
* @param {string} profile.url Canonical URL (GitHub repo, package page, etc.)
|
|
536
|
+
* @param {string} [profile.name] Display name
|
|
537
|
+
* @param {string} [profile.description] One-sentence description
|
|
538
|
+
* @param {string[]} [profile.capabilities]
|
|
539
|
+
* @param {string[]} [profile.constraints]
|
|
540
|
+
* @param {string} [profile.model_provider]
|
|
541
|
+
* @param {string} [profile.model_id]
|
|
542
|
+
* @param {string} [profile.contact_url]
|
|
543
|
+
* @param {string} [profile.ajp_endpoint]
|
|
544
|
+
* @param {string} [profile.public_key] Ed25519 public key: "ed25519:<base64>"
|
|
545
|
+
* @param {string} [profile.version]
|
|
546
|
+
* @returns {{ created: boolean, updated: boolean, agent: object }}
|
|
547
|
+
*/
|
|
548
|
+
async register(profile = {}) {
|
|
549
|
+
const { id, ...rest } = profile;
|
|
550
|
+
if (!id) throw new Error('profile.id is required');
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const res = await fetch(`${this.apiUrl}/api/agents/register`, {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
556
|
+
body: JSON.stringify({ provenance_id: id, ...rest }),
|
|
557
|
+
});
|
|
558
|
+
if (!res.ok) {
|
|
559
|
+
const err = await res.json().catch(() => ({}));
|
|
560
|
+
throw new Error(err.error || `Registration failed: ${res.status}`);
|
|
561
|
+
}
|
|
562
|
+
return res.json();
|
|
563
|
+
} catch (e) {
|
|
564
|
+
throw new Error(`Provenance.register failed: ${e.message}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Default instance pointing at provenance.dev
|
|
570
|
+
export const provenance = new Provenance();
|