@scopeblind/passport 0.3.0 → 0.4.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/README.md +77 -128
- package/dist/chunk-LEODYLAY.mjs +401 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +834 -0
- package/dist/cli.mjs +708 -0
- package/dist/index.mjs +41 -385
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# @scopeblind/passport
|
|
2
2
|
|
|
3
|
-
Portable cryptographic identity
|
|
3
|
+
Portable cryptographic identity and local agent-pack builder for AI agents, MCP runtimes, and BlindLLM/OpenClaw-style flows.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Generate Ed25519 passports for agents and coaches
|
|
8
|
+
- Sign immutable manifests locally
|
|
9
|
+
- Export portable passport bundles
|
|
10
|
+
- Create a new local agent pack with `create`
|
|
11
|
+
- Wrap an existing OpenClaw / MCP config with `wrap`
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- **Verifiable** — Ed25519 signatures over canonical JSON. Verify offline, no API calls.
|
|
11
|
-
- **Privacy-preserving** — No PII required. Identity is a key fingerprint.
|
|
13
|
+
The CLI is local-first. It does not upload prompts, context, or keys to ScopeBlind.
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
@@ -18,115 +20,85 @@ npm install @scopeblind/passport
|
|
|
18
20
|
|
|
19
21
|
Requires Node.js >= 18. Browser entrypoint available at `@scopeblind/passport/browser`.
|
|
20
22
|
|
|
21
|
-
##
|
|
23
|
+
## CLI First
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
generatePassportKey,
|
|
26
|
-
createCoachManifest,
|
|
27
|
-
verifyManifest,
|
|
28
|
-
} from '@scopeblind/passport';
|
|
25
|
+
Create a new agent pack:
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const manifest = createCoachManifest(key, {
|
|
36
|
-
display_name: 'Atlas Nova',
|
|
37
|
-
model_family: 'claude',
|
|
38
|
-
system_prompt_hash: 'sha256:a1b2c3...',
|
|
39
|
-
capabilities: ['debate', 'analysis'],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// 3. Verify the signature
|
|
43
|
-
const result = verifyManifest(manifest);
|
|
44
|
-
console.log(result.valid); // true
|
|
27
|
+
```bash
|
|
28
|
+
npx @scopeblind/passport create \
|
|
29
|
+
--name "Luna" \
|
|
30
|
+
--runtime openclaw \
|
|
31
|
+
--policy shadow
|
|
45
32
|
```
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### Key Generation
|
|
34
|
+
Wrap an existing OpenClaw or MCP config:
|
|
50
35
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Derive a passport ID from an existing public key
|
|
57
|
-
const kid = derivePassportId('coach', publicKeyBytes);
|
|
58
|
-
// => "sb:coach:7Xq9kM..."
|
|
36
|
+
```bash
|
|
37
|
+
npx @scopeblind/passport wrap \
|
|
38
|
+
--runtime openclaw \
|
|
39
|
+
--config ./openclaw.json \
|
|
40
|
+
--policy email-safe
|
|
59
41
|
```
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
Both commands generate:
|
|
62
44
|
|
|
63
|
-
|
|
45
|
+
- `manifest.json`
|
|
46
|
+
- `passport.bundle.json`
|
|
47
|
+
- `protect-mcp.json`
|
|
48
|
+
- `keys/gateway.json`
|
|
49
|
+
- runtime-specific config guidance
|
|
50
|
+
- `VERIFY.md`
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
// Coach manifest (for AI coaches/tutors)
|
|
67
|
-
const manifest = createCoachManifest(key, {
|
|
68
|
-
display_name: 'Atlas Nova',
|
|
69
|
-
model_family: 'claude',
|
|
70
|
-
system_prompt_hash: 'sha256:...',
|
|
71
|
-
capabilities: ['debate', 'analysis', 'coaching'],
|
|
72
|
-
});
|
|
52
|
+
## What the CLI is for
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
const agentManifest = createAgentManifest(key, {
|
|
76
|
-
display_name: 'Research Bot',
|
|
77
|
-
model_family: 'gpt-4',
|
|
78
|
-
tool_names: ['web_search', 'file_read'],
|
|
79
|
-
max_actions_per_turn: 10,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Verify any manifest
|
|
83
|
-
const result = verifyManifest(manifest);
|
|
84
|
-
// { valid: true, kid: 'sb:coach:...', type: 'scopeblind:coach-manifest' }
|
|
85
|
-
```
|
|
54
|
+
This is not a no-code agent builder.
|
|
86
55
|
|
|
87
|
-
|
|
56
|
+
It is for:
|
|
88
57
|
|
|
89
|
-
|
|
58
|
+
- giving an agent a portable identity
|
|
59
|
+
- applying a `protect-mcp` policy pack
|
|
60
|
+
- generating runtime glue for OpenClaw / Claude Desktop / Cursor / generic MCP
|
|
61
|
+
- making the resulting pack independently verifiable
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
const attestation = createOwnershipAttestation(coachKey, {
|
|
93
|
-
subject_kid: agentKey.kid,
|
|
94
|
-
relationship: 'owns',
|
|
95
|
-
});
|
|
96
|
-
```
|
|
63
|
+
It is not for:
|
|
97
64
|
|
|
98
|
-
|
|
65
|
+
- hosted prompt management
|
|
66
|
+
- agent marketplaces
|
|
67
|
+
- non-local storage of context or secrets
|
|
99
68
|
|
|
100
|
-
|
|
69
|
+
## SDK Quick Start
|
|
101
70
|
|
|
102
71
|
```typescript
|
|
103
72
|
import {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
73
|
+
generatePassportKey,
|
|
74
|
+
createAgentManifest,
|
|
75
|
+
verifyManifest,
|
|
76
|
+
hashString,
|
|
107
77
|
} from '@scopeblind/passport';
|
|
108
78
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
79
|
+
const key = generatePassportKey('agent');
|
|
80
|
+
|
|
81
|
+
const manifest = createAgentManifest(key, {
|
|
82
|
+
public_profile: {
|
|
83
|
+
name: 'Research Bot',
|
|
84
|
+
description: 'Fetches and summarizes MCP-backed research tasks',
|
|
85
|
+
domain_lanes: ['analysis:research'],
|
|
86
|
+
},
|
|
87
|
+
configuration_attestations: {
|
|
88
|
+
model_family_hash: hashString('runtime-managed'),
|
|
89
|
+
memory_mode: 'isolated',
|
|
90
|
+
system_prompt: 'Runtime-managed prompt placeholder',
|
|
91
|
+
},
|
|
92
|
+
capability_declarations: {
|
|
93
|
+
lease_template_compatibility: ['shadow'],
|
|
94
|
+
requested_tool_classes: ['mcp:general'],
|
|
95
|
+
},
|
|
121
96
|
});
|
|
122
97
|
|
|
123
|
-
//
|
|
124
|
-
const verified = verifyEnvelope(receipt);
|
|
98
|
+
console.log(verifyManifest(manifest).valid); // true
|
|
125
99
|
```
|
|
126
100
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Export a passport for migration to another platform:
|
|
101
|
+
## Portable Export / Import
|
|
130
102
|
|
|
131
103
|
```typescript
|
|
132
104
|
import {
|
|
@@ -135,50 +107,27 @@ import {
|
|
|
135
107
|
importPassportBundle,
|
|
136
108
|
} from '@scopeblind/passport';
|
|
137
109
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
110
|
+
const bundle = exportPassportBundle({
|
|
111
|
+
key,
|
|
112
|
+
manifests: [manifest],
|
|
113
|
+
ownership_attestations: [],
|
|
114
|
+
status_records: [],
|
|
115
|
+
});
|
|
141
116
|
|
|
142
|
-
|
|
117
|
+
const json = serializeBundle(bundle);
|
|
143
118
|
const imported = importPassportBundle(json);
|
|
144
|
-
console.log(imported.key.kid); // same kid, different machine
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Display Helpers
|
|
148
119
|
|
|
149
|
-
|
|
150
|
-
import { formatKidShort, getCoachSummary } from '@scopeblind/passport';
|
|
151
|
-
|
|
152
|
-
formatKidShort('sb:coach:7Xq9kMvR...'); // "7Xq9kM"
|
|
153
|
-
getCoachSummary(manifest); // "Atlas Nova (claude) — sb:coach:7Xq9kM"
|
|
120
|
+
console.log(imported.valid); // true
|
|
154
121
|
```
|
|
155
122
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
For browser environments (uses IndexedDB for key storage):
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
import { ... } from '@scopeblind/passport/browser';
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Types
|
|
165
|
-
|
|
166
|
-
All types are exported and fully documented:
|
|
167
|
-
|
|
168
|
-
- `PassportKeyPair` — Ed25519 keypair with kid and role
|
|
169
|
-
- `CoachManifest` / `AgentManifest` — Immutable signed manifests
|
|
170
|
-
- `OwnershipAttestation` — Cross-key ownership proof
|
|
171
|
-
- `SignedEnvelope` — Canonical JSON wrapper with Ed25519 signature
|
|
172
|
-
- `ArenaBattleReceipt` / `CoachUpliftReceipt` / `FormalDebateReceipt` — Evidence types
|
|
173
|
-
- `PortablePassportBundle` — Exportable passport + manifest bundle
|
|
123
|
+
## Why it fits the current stack
|
|
174
124
|
|
|
175
|
-
|
|
125
|
+
- `protect-mcp` handles tool policy and signed receipts
|
|
126
|
+
- `@veritasacta/verify` handles independent verification
|
|
127
|
+
- `@scopeblind/passport` handles portable identity + pack generation
|
|
176
128
|
|
|
177
|
-
|
|
178
|
-
- **Canonical JSON** — Deterministic serialization (ASCII-only keys, sorted). Same input always produces same bytes.
|
|
179
|
-
- **Immutable manifests** — Config changes produce a new version with `previous_version` link. No in-place mutation.
|
|
180
|
-
- **No blockchain** — Verification is pure cryptography. No tokens, no chain, no consensus.
|
|
129
|
+
That split keeps the verifier MIT, keeps the local trust primitives local, and leaves the managed control plane as the paid product.
|
|
181
130
|
|
|
182
131
|
## License
|
|
183
132
|
|
|
184
|
-
FSL-1.1-MIT —
|
|
133
|
+
FSL-1.1-MIT — source-available, free to use, converts to MIT after 2 years.
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// src/display.ts
|
|
2
|
+
function formatKidShort(kid) {
|
|
3
|
+
const parts = kid.split(":");
|
|
4
|
+
const fingerprint = parts[2] || kid;
|
|
5
|
+
if (fingerprint.length <= 8) return fingerprint;
|
|
6
|
+
return `${fingerprint.slice(0, 4)}\u2026${fingerprint.slice(-4)}`;
|
|
7
|
+
}
|
|
8
|
+
function formatKidLabeled(kid) {
|
|
9
|
+
const parts = kid.split(":");
|
|
10
|
+
const role = parts[1] === "coach" ? "Coach" : parts[1] === "agent" ? "Agent" : "Unknown";
|
|
11
|
+
return `${role} ${formatKidShort(kid)}`;
|
|
12
|
+
}
|
|
13
|
+
function getCoachSummary(envelope) {
|
|
14
|
+
const m = envelope.payload;
|
|
15
|
+
return {
|
|
16
|
+
kid: m.id,
|
|
17
|
+
displayName: m.display_name,
|
|
18
|
+
shortId: formatKidShort(m.id),
|
|
19
|
+
version: m.version,
|
|
20
|
+
createdAt: m.created_at
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function getAgentSummary(envelope) {
|
|
24
|
+
const m = envelope.payload;
|
|
25
|
+
return {
|
|
26
|
+
kid: m.id,
|
|
27
|
+
name: m.public_profile.name,
|
|
28
|
+
shortId: formatKidShort(m.id),
|
|
29
|
+
version: m.version,
|
|
30
|
+
domainLanes: m.public_profile.domain_lanes,
|
|
31
|
+
createdAt: m.created_at
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isCoachManifest(manifest) {
|
|
35
|
+
return manifest.type === "scopeblind:coach-manifest";
|
|
36
|
+
}
|
|
37
|
+
function isAgentManifest(manifest) {
|
|
38
|
+
return manifest.type === "scopeblind:agent-manifest";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/keys.ts
|
|
42
|
+
import { ed25519 } from "@noble/curves/ed25519";
|
|
43
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
44
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
45
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
46
|
+
function base58Encode(bytes) {
|
|
47
|
+
let num = BigInt("0x" + bytesToHex(bytes));
|
|
48
|
+
const chars = [];
|
|
49
|
+
while (num > 0n) {
|
|
50
|
+
const remainder = Number(num % 58n);
|
|
51
|
+
chars.unshift(BASE58_ALPHABET[remainder]);
|
|
52
|
+
num = num / 58n;
|
|
53
|
+
}
|
|
54
|
+
for (const b of bytes) {
|
|
55
|
+
if (b === 0) chars.unshift("1");
|
|
56
|
+
else break;
|
|
57
|
+
}
|
|
58
|
+
return chars.join("") || "1";
|
|
59
|
+
}
|
|
60
|
+
function derivePassportId(publicKey, role) {
|
|
61
|
+
const fingerprint = base58Encode(publicKey).slice(0, 12);
|
|
62
|
+
return `sb:${role}:${fingerprint}`;
|
|
63
|
+
}
|
|
64
|
+
function generatePassportKey(role) {
|
|
65
|
+
const secretKeyRaw = ed25519.utils.randomPrivateKey();
|
|
66
|
+
const publicKey = ed25519.getPublicKey(secretKeyRaw);
|
|
67
|
+
const secretKey = new Uint8Array(64);
|
|
68
|
+
secretKey.set(secretKeyRaw, 0);
|
|
69
|
+
secretKey.set(publicKey, 32);
|
|
70
|
+
const kid = derivePassportId(publicKey, role);
|
|
71
|
+
return {
|
|
72
|
+
publicKey,
|
|
73
|
+
secretKey,
|
|
74
|
+
kid,
|
|
75
|
+
role,
|
|
76
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function getSigningKey(keyPair) {
|
|
80
|
+
return bytesToHex(keyPair.secretKey.slice(0, 32));
|
|
81
|
+
}
|
|
82
|
+
function getVerifyKey(keyPair) {
|
|
83
|
+
return bytesToHex(keyPair.publicKey);
|
|
84
|
+
}
|
|
85
|
+
function hashString(value) {
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
return bytesToHex(sha256(encoder.encode(value)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/manifest.ts
|
|
91
|
+
import { ed25519 as ed255192 } from "@noble/curves/ed25519";
|
|
92
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
93
|
+
import { bytesToHex as bytesToHex2, utf8ToBytes } from "@noble/hashes/utils";
|
|
94
|
+
function canonicalize(obj) {
|
|
95
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
96
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
97
|
+
const sorted = {};
|
|
98
|
+
for (const k of Object.keys(value).sort()) {
|
|
99
|
+
sorted[k] = value[k];
|
|
100
|
+
}
|
|
101
|
+
return sorted;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function canonicalHash(obj) {
|
|
107
|
+
return bytesToHex2(sha2562(utf8ToBytes(canonicalize(obj))));
|
|
108
|
+
}
|
|
109
|
+
function signPayload(payload, keyPair) {
|
|
110
|
+
const message = utf8ToBytes(canonicalize(payload));
|
|
111
|
+
const sigBytes = ed255192.sign(message, keyPair.secretKey.slice(0, 32));
|
|
112
|
+
return {
|
|
113
|
+
payload,
|
|
114
|
+
signature: {
|
|
115
|
+
alg: "EdDSA",
|
|
116
|
+
kid: keyPair.kid,
|
|
117
|
+
sig: bytesToHex2(sigBytes)
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function createCoachManifest(keyPair, input, previousVersion, version) {
|
|
122
|
+
const manifest = {
|
|
123
|
+
type: "scopeblind:coach-manifest",
|
|
124
|
+
id: keyPair.kid,
|
|
125
|
+
version: version || "1.0.0",
|
|
126
|
+
previous_version: previousVersion ?? null,
|
|
127
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
+
public_key: base58Encode(keyPair.publicKey),
|
|
129
|
+
display_name: input.display_name.trim().slice(0, 32)
|
|
130
|
+
};
|
|
131
|
+
return signPayload(manifest, keyPair);
|
|
132
|
+
}
|
|
133
|
+
function createAgentManifest(keyPair, input, previousVersion, version) {
|
|
134
|
+
const promptHash = hashString(input.configuration_attestations.system_prompt);
|
|
135
|
+
const manifest = {
|
|
136
|
+
type: "scopeblind:agent-manifest",
|
|
137
|
+
id: keyPair.kid,
|
|
138
|
+
version: version || "1.0.0",
|
|
139
|
+
previous_version: previousVersion ?? null,
|
|
140
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
141
|
+
public_key: base58Encode(keyPair.publicKey),
|
|
142
|
+
public_profile: input.public_profile,
|
|
143
|
+
configuration_attestations: {
|
|
144
|
+
model_family_hash: input.configuration_attestations.model_family_hash,
|
|
145
|
+
memory_mode: input.configuration_attestations.memory_mode,
|
|
146
|
+
prompt_hash: promptHash
|
|
147
|
+
},
|
|
148
|
+
capability_declarations: input.capability_declarations,
|
|
149
|
+
evidence_pointers: input.evidence_pointers || {}
|
|
150
|
+
};
|
|
151
|
+
return signPayload(manifest, keyPair);
|
|
152
|
+
}
|
|
153
|
+
function createOwnershipAttestation(coachKeyPair, agentId, agentManifestVersion) {
|
|
154
|
+
const attestation = {
|
|
155
|
+
type: "scopeblind:ownership-attestation",
|
|
156
|
+
coach_id: coachKeyPair.kid,
|
|
157
|
+
agent_id: agentId,
|
|
158
|
+
agent_manifest_version: agentManifestVersion,
|
|
159
|
+
granted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
160
|
+
};
|
|
161
|
+
return signPayload(attestation, coachKeyPair);
|
|
162
|
+
}
|
|
163
|
+
function createStatusRecord(signerKeyPair, targetId, status, reason) {
|
|
164
|
+
const record = {
|
|
165
|
+
type: "scopeblind:status-record",
|
|
166
|
+
target_id: targetId,
|
|
167
|
+
status,
|
|
168
|
+
changed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
169
|
+
issuer_id: signerKeyPair.kid,
|
|
170
|
+
...reason ? { reason } : {}
|
|
171
|
+
};
|
|
172
|
+
return signPayload(record, signerKeyPair);
|
|
173
|
+
}
|
|
174
|
+
function createCoachContribution(coachKeyPair, battleId, coachedSide, noteHash, noteLength, upliftVerdict) {
|
|
175
|
+
const contribution = {
|
|
176
|
+
type: "scopeblind:coach-contribution",
|
|
177
|
+
battle_id: battleId,
|
|
178
|
+
coach_id: coachKeyPair.kid,
|
|
179
|
+
coached_side: coachedSide,
|
|
180
|
+
note_hash: noteHash,
|
|
181
|
+
note_length: noteLength,
|
|
182
|
+
...upliftVerdict ? { uplift_verdict: upliftVerdict } : {}
|
|
183
|
+
};
|
|
184
|
+
return signPayload(contribution, coachKeyPair);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/verify.ts
|
|
188
|
+
import { ed25519 as ed255193 } from "@noble/curves/ed25519";
|
|
189
|
+
import { hexToBytes as hexToBytes2, utf8ToBytes as utf8ToBytes2 } from "@noble/hashes/utils";
|
|
190
|
+
function verifyEnvelope(envelope, expectedPublicKey) {
|
|
191
|
+
try {
|
|
192
|
+
if (!envelope.signature?.sig || !envelope.signature?.kid) {
|
|
193
|
+
return { valid: false, error: "missing_signature" };
|
|
194
|
+
}
|
|
195
|
+
const message = utf8ToBytes2(canonicalize(envelope.payload));
|
|
196
|
+
const hash = canonicalHash(envelope.payload);
|
|
197
|
+
const payload = envelope.payload;
|
|
198
|
+
let publicKeyBytes;
|
|
199
|
+
if (expectedPublicKey) {
|
|
200
|
+
publicKeyBytes = expectedPublicKey;
|
|
201
|
+
} else if (typeof payload.public_key === "string") {
|
|
202
|
+
publicKeyBytes = base58Decode(payload.public_key);
|
|
203
|
+
} else {
|
|
204
|
+
return { valid: false, error: "no_public_key_available" };
|
|
205
|
+
}
|
|
206
|
+
if (expectedPublicKey) {
|
|
207
|
+
const expectedKid = base58Encode(expectedPublicKey);
|
|
208
|
+
const signerFingerprint = envelope.signature.kid.split(":")[2];
|
|
209
|
+
const expectedFingerprint = expectedKid.slice(0, 12);
|
|
210
|
+
if (signerFingerprint !== expectedFingerprint) {
|
|
211
|
+
return { valid: false, hash, error: "kid_mismatch" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const valid = ed255193.verify(
|
|
215
|
+
hexToBytes2(envelope.signature.sig),
|
|
216
|
+
message,
|
|
217
|
+
publicKeyBytes
|
|
218
|
+
);
|
|
219
|
+
return valid ? { valid: true, hash } : { valid: false, hash, error: "invalid_signature" };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
error: `verification_error: ${err instanceof Error ? err.message : "unknown"}`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function verifyManifest(envelope) {
|
|
228
|
+
return verifyEnvelope(envelope);
|
|
229
|
+
}
|
|
230
|
+
function verifyOwnership(attestation, coachPublicKey) {
|
|
231
|
+
const result = verifyEnvelope(attestation, coachPublicKey);
|
|
232
|
+
if (!result.valid) return result;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
function base58Decode(str) {
|
|
236
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
237
|
+
let num = 0n;
|
|
238
|
+
for (const char of str) {
|
|
239
|
+
const idx = ALPHABET.indexOf(char);
|
|
240
|
+
if (idx === -1) throw new Error(`Invalid base58 character: ${char}`);
|
|
241
|
+
num = num * 58n + BigInt(idx);
|
|
242
|
+
}
|
|
243
|
+
const hex = num.toString(16).padStart(64, "0");
|
|
244
|
+
const bytes = new Uint8Array(32);
|
|
245
|
+
for (let i = 0; i < 32; i++) {
|
|
246
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
247
|
+
}
|
|
248
|
+
return bytes;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/portable.ts
|
|
252
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3 } from "@noble/hashes/utils";
|
|
253
|
+
import { ed25519 as ed255194 } from "@noble/curves/ed25519";
|
|
254
|
+
function exportPassportBundle(bundle) {
|
|
255
|
+
return {
|
|
256
|
+
v: 1,
|
|
257
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
258
|
+
passport: {
|
|
259
|
+
kid: bundle.key.kid,
|
|
260
|
+
role: bundle.key.role,
|
|
261
|
+
created_at: bundle.key.created_at,
|
|
262
|
+
publicKeyHex: bytesToHex3(bundle.key.publicKey),
|
|
263
|
+
secretKeyHex: bytesToHex3(bundle.key.secretKey)
|
|
264
|
+
},
|
|
265
|
+
manifests: bundle.manifests,
|
|
266
|
+
ownership_attestations: bundle.ownership_attestations,
|
|
267
|
+
status_records: bundle.status_records
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function serializeBundle(bundle) {
|
|
271
|
+
return JSON.stringify(bundle, null, 2);
|
|
272
|
+
}
|
|
273
|
+
function importPassportBundle(json) {
|
|
274
|
+
const errors = [];
|
|
275
|
+
const warnings = [];
|
|
276
|
+
let raw;
|
|
277
|
+
try {
|
|
278
|
+
raw = JSON.parse(json);
|
|
279
|
+
} catch {
|
|
280
|
+
return { valid: false, errors: ["Invalid JSON"], warnings };
|
|
281
|
+
}
|
|
282
|
+
const data = raw;
|
|
283
|
+
if (data.v !== 1) {
|
|
284
|
+
errors.push(`Unsupported bundle version: ${data.v}`);
|
|
285
|
+
return { valid: false, errors, warnings };
|
|
286
|
+
}
|
|
287
|
+
const passport = data.passport;
|
|
288
|
+
if (!passport) {
|
|
289
|
+
errors.push("Missing passport field");
|
|
290
|
+
return { valid: false, errors, warnings };
|
|
291
|
+
}
|
|
292
|
+
if (typeof passport.publicKeyHex !== "string" || typeof passport.secretKeyHex !== "string") {
|
|
293
|
+
errors.push("Missing or invalid key hex values");
|
|
294
|
+
return { valid: false, errors, warnings };
|
|
295
|
+
}
|
|
296
|
+
if (typeof passport.kid !== "string" || !passport.kid.startsWith("sb:")) {
|
|
297
|
+
errors.push("Invalid passport kid format");
|
|
298
|
+
return { valid: false, errors, warnings };
|
|
299
|
+
}
|
|
300
|
+
const role = passport.role;
|
|
301
|
+
if (role !== "coach" && role !== "agent") {
|
|
302
|
+
errors.push(`Invalid role: ${role}`);
|
|
303
|
+
return { valid: false, errors, warnings };
|
|
304
|
+
}
|
|
305
|
+
let publicKey;
|
|
306
|
+
let secretKey;
|
|
307
|
+
try {
|
|
308
|
+
publicKey = hexToBytes3(passport.publicKeyHex);
|
|
309
|
+
secretKey = hexToBytes3(passport.secretKeyHex);
|
|
310
|
+
} catch {
|
|
311
|
+
errors.push("Failed to decode key hex values");
|
|
312
|
+
return { valid: false, errors, warnings };
|
|
313
|
+
}
|
|
314
|
+
if (publicKey.length !== 32) {
|
|
315
|
+
errors.push(`Invalid public key length: ${publicKey.length} (expected 32)`);
|
|
316
|
+
return { valid: false, errors, warnings };
|
|
317
|
+
}
|
|
318
|
+
if (secretKey.length !== 64) {
|
|
319
|
+
errors.push(`Invalid secret key length: ${secretKey.length} (expected 64)`);
|
|
320
|
+
return { valid: false, errors, warnings };
|
|
321
|
+
}
|
|
322
|
+
const seed = secretKey.slice(0, 32);
|
|
323
|
+
let derivedPublicKey;
|
|
324
|
+
try {
|
|
325
|
+
derivedPublicKey = ed255194.getPublicKey(seed);
|
|
326
|
+
} catch {
|
|
327
|
+
errors.push("Secret key seed is not a valid Ed25519 private key");
|
|
328
|
+
return { valid: false, errors, warnings };
|
|
329
|
+
}
|
|
330
|
+
if (bytesToHex3(derivedPublicKey) !== bytesToHex3(publicKey)) {
|
|
331
|
+
errors.push("Key coherence failure: secret key does not derive to the claimed public key");
|
|
332
|
+
return { valid: false, errors, warnings };
|
|
333
|
+
}
|
|
334
|
+
const expectedKid = derivePassportId(publicKey, role);
|
|
335
|
+
if (passport.kid !== expectedKid) {
|
|
336
|
+
errors.push(`Kid mismatch: bundle claims ${passport.kid} but public key derives to ${expectedKid}`);
|
|
337
|
+
return { valid: false, errors, warnings };
|
|
338
|
+
}
|
|
339
|
+
const key = {
|
|
340
|
+
publicKey,
|
|
341
|
+
secretKey,
|
|
342
|
+
kid: passport.kid,
|
|
343
|
+
role,
|
|
344
|
+
created_at: passport.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
345
|
+
};
|
|
346
|
+
const rawManifests = Array.isArray(data.manifests) ? data.manifests : [];
|
|
347
|
+
const validManifests = [];
|
|
348
|
+
for (const m of rawManifests) {
|
|
349
|
+
const result = verifyManifest(m);
|
|
350
|
+
if (!result.valid) {
|
|
351
|
+
errors.push(`Manifest ${m.payload?.id || "unknown"} has invalid signature: ${result.error}`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (m.payload?.id && m.payload.id !== key.kid) {
|
|
355
|
+
warnings.push(`Dropped manifest ${m.payload.id}: does not belong to passport ${key.kid}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
validManifests.push(m);
|
|
359
|
+
}
|
|
360
|
+
const bundle = {
|
|
361
|
+
key,
|
|
362
|
+
manifests: validManifests,
|
|
363
|
+
ownership_attestations: Array.isArray(data.ownership_attestations) ? data.ownership_attestations : [],
|
|
364
|
+
status_records: Array.isArray(data.status_records) ? data.status_records : []
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
valid: errors.length === 0,
|
|
368
|
+
bundle,
|
|
369
|
+
errors,
|
|
370
|
+
warnings
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export {
|
|
375
|
+
formatKidShort,
|
|
376
|
+
formatKidLabeled,
|
|
377
|
+
getCoachSummary,
|
|
378
|
+
getAgentSummary,
|
|
379
|
+
isCoachManifest,
|
|
380
|
+
isAgentManifest,
|
|
381
|
+
base58Encode,
|
|
382
|
+
derivePassportId,
|
|
383
|
+
generatePassportKey,
|
|
384
|
+
getSigningKey,
|
|
385
|
+
getVerifyKey,
|
|
386
|
+
hashString,
|
|
387
|
+
canonicalize,
|
|
388
|
+
canonicalHash,
|
|
389
|
+
signPayload,
|
|
390
|
+
createCoachManifest,
|
|
391
|
+
createAgentManifest,
|
|
392
|
+
createOwnershipAttestation,
|
|
393
|
+
createStatusRecord,
|
|
394
|
+
createCoachContribution,
|
|
395
|
+
verifyEnvelope,
|
|
396
|
+
verifyManifest,
|
|
397
|
+
verifyOwnership,
|
|
398
|
+
exportPassportBundle,
|
|
399
|
+
serializeBundle,
|
|
400
|
+
importPassportBundle
|
|
401
|
+
};
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|