@oleary-labs/signet-sdk 0.1.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/dist/admin.d.ts +38 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +112 -0
- package/dist/admin.js.map +1 -0
- package/dist/authkey-session.d.ts +64 -0
- package/dist/authkey-session.d.ts.map +1 -0
- package/dist/authkey-session.js +164 -0
- package/dist/authkey-session.js.map +1 -0
- package/dist/bootstrap.d.ts +30 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +60 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundler.d.ts +85 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +160 -0
- package/dist/bundler.js.map +1 -0
- package/dist/delegate.d.ts +57 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +111 -0
- package/dist/delegate.js.map +1 -0
- package/dist/frostVerify.d.ts +23 -0
- package/dist/frostVerify.d.ts.map +1 -0
- package/dist/frostVerify.js +69 -0
- package/dist/frostVerify.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +28 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +81 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwt.d.ts +27 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +50 -0
- package/dist/jwt.js.map +1 -0
- package/dist/keygen.d.ts +26 -0
- package/dist/keygen.d.ts.map +1 -0
- package/dist/keygen.js +60 -0
- package/dist/keygen.js.map +1 -0
- package/dist/oauth.d.ts +34 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +119 -0
- package/dist/oauth.js.map +1 -0
- package/dist/request.d.ts +42 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +115 -0
- package/dist/request.js.map +1 -0
- package/dist/scopedSign.d.ts +82 -0
- package/dist/scopedSign.d.ts.map +1 -0
- package/dist/scopedSign.js +130 -0
- package/dist/scopedSign.js.map +1 -0
- package/dist/server-prover.d.ts +29 -0
- package/dist/server-prover.d.ts.map +1 -0
- package/dist/server-prover.js +54 -0
- package/dist/server-prover.js.map +1 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/userop.d.ts +104 -0
- package/dist/userop.d.ts.map +1 -0
- package/dist/userop.js +212 -0
- package/dist/userop.js.map +1 -0
- package/dist/x402.d.ts +127 -0
- package/dist/x402.d.ts.map +1 -0
- package/dist/x402.js +167 -0
- package/dist/x402.js.map +1 -0
- package/package.json +64 -0
- package/src/admin.ts +178 -0
- package/src/authkey-session.ts +241 -0
- package/src/bootstrap.ts +106 -0
- package/src/bundler.ts +256 -0
- package/src/delegate.ts +163 -0
- package/src/frostVerify.ts +79 -0
- package/src/generate-inputs.ts +158 -0
- package/src/index.ts +43 -0
- package/src/jwks.ts +92 -0
- package/src/jwt.ts +74 -0
- package/src/keygen.ts +89 -0
- package/src/oauth.ts +157 -0
- package/src/partial-sha.ts +99 -0
- package/src/proof.ts +99 -0
- package/src/request.ts +174 -0
- package/src/scopedSign.ts +184 -0
- package/src/server-prover.ts +76 -0
- package/src/session.ts +33 -0
- package/src/types.ts +63 -0
- package/src/userop.ts +368 -0
- package/src/witness.ts +132 -0
- package/src/x402.ts +275 -0
package/src/admin.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API authentication for Signet group management.
|
|
3
|
+
*
|
|
4
|
+
* Admin auth is stateless: sign SHA256(group_id : nonce : timestamp_BE)
|
|
5
|
+
* with a trusted auth key. For Schnorr auth keys (prefix 0x01), the
|
|
6
|
+
* signature is produced via FROST threshold signing through the bootstrap
|
|
7
|
+
* group.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SessionKeypair, IdTokenClaims } from "./types";
|
|
11
|
+
import { signSignRequest } from "./request";
|
|
12
|
+
import { bytesToHex } from "./session";
|
|
13
|
+
|
|
14
|
+
export interface AdminAuthConfig {
|
|
15
|
+
nodeProxyUrl: string;
|
|
16
|
+
bootstrapGroup: string;
|
|
17
|
+
bootstrapNodes: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AdminAuth {
|
|
21
|
+
group_id: string;
|
|
22
|
+
auth_key_pub: string;
|
|
23
|
+
signature: string;
|
|
24
|
+
nonce: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an admin auth payload by threshold-signing the admin hash
|
|
30
|
+
* via the bootstrap group.
|
|
31
|
+
*
|
|
32
|
+
* The admin hash is: SHA256(group_id + ":" + nonce + ":" + timestamp_8BE)
|
|
33
|
+
* The signature is a 65-byte FROST Schnorr signature.
|
|
34
|
+
*/
|
|
35
|
+
export async function buildAdminAuth(
|
|
36
|
+
config: AdminAuthConfig,
|
|
37
|
+
groupId: string,
|
|
38
|
+
authKeyPub: string,
|
|
39
|
+
sessionKeypair: SessionKeypair,
|
|
40
|
+
claims: IdTokenClaims,
|
|
41
|
+
): Promise<AdminAuth> {
|
|
42
|
+
const normalizedGroupId = groupId.toLowerCase();
|
|
43
|
+
const nonce = generateNonce();
|
|
44
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
45
|
+
|
|
46
|
+
// Build the admin hash: SHA256(group_id + ":" + nonce + ":" + timestamp_8BE)
|
|
47
|
+
const adminHash = await computeAdminHash(normalizedGroupId, nonce, timestamp);
|
|
48
|
+
|
|
49
|
+
// Threshold-sign the hash via bootstrap group
|
|
50
|
+
const signReq = await signSignRequest(
|
|
51
|
+
sessionKeypair,
|
|
52
|
+
claims,
|
|
53
|
+
config.bootstrapGroup,
|
|
54
|
+
adminHash,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const signRes = await fetch(config.nodeProxyUrl, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"x-node-url": config.bootstrapNodes[0],
|
|
62
|
+
"x-node-path": "/v1/sign",
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(signReq),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!signRes.ok) {
|
|
68
|
+
const body = await signRes.text();
|
|
69
|
+
throw new Error(`Admin signing failed: ${signRes.status} — ${body}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { ethereum_signature } = await signRes.json();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
group_id: normalizedGroupId,
|
|
76
|
+
auth_key_pub: authKeyPub,
|
|
77
|
+
signature: ethereum_signature,
|
|
78
|
+
nonce,
|
|
79
|
+
timestamp,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Call an admin endpoint with auth.
|
|
85
|
+
*
|
|
86
|
+
* If the signing request fails with a 401 "session not found" error and
|
|
87
|
+
* a reauthenticate callback is provided, re-establishes the node session
|
|
88
|
+
* and retries once.
|
|
89
|
+
*/
|
|
90
|
+
export async function adminRequest<T>(
|
|
91
|
+
config: AdminAuthConfig,
|
|
92
|
+
nodeUrl: string,
|
|
93
|
+
path: string,
|
|
94
|
+
groupId: string,
|
|
95
|
+
authKeyPub: string,
|
|
96
|
+
sessionKeypair: SessionKeypair,
|
|
97
|
+
claims: IdTokenClaims,
|
|
98
|
+
extraBody?: Record<string, unknown>,
|
|
99
|
+
reauthenticate?: () => Promise<void>,
|
|
100
|
+
): Promise<T> {
|
|
101
|
+
try {
|
|
102
|
+
return await adminRequestInner<T>(config, nodeUrl, path, groupId, authKeyPub, sessionKeypair, claims, extraBody);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
105
|
+
if (reauthenticate && /session not found/i.test(msg)) {
|
|
106
|
+
await reauthenticate();
|
|
107
|
+
return await adminRequestInner<T>(config, nodeUrl, path, groupId, authKeyPub, sessionKeypair, claims, extraBody);
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function adminRequestInner<T>(
|
|
114
|
+
config: AdminAuthConfig,
|
|
115
|
+
nodeUrl: string,
|
|
116
|
+
path: string,
|
|
117
|
+
groupId: string,
|
|
118
|
+
authKeyPub: string,
|
|
119
|
+
sessionKeypair: SessionKeypair,
|
|
120
|
+
claims: IdTokenClaims,
|
|
121
|
+
extraBody?: Record<string, unknown>,
|
|
122
|
+
): Promise<T> {
|
|
123
|
+
const auth = await buildAdminAuth(config, groupId, authKeyPub, sessionKeypair, claims);
|
|
124
|
+
|
|
125
|
+
const res = await fetch(config.nodeProxyUrl, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
"x-node-url": nodeUrl,
|
|
130
|
+
"x-node-path": path,
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ ...auth, ...extraBody }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const body = await res.text();
|
|
137
|
+
throw new Error(`Admin ${path} failed: ${res.status} — ${body}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return res.json();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function computeAdminHash(
|
|
144
|
+
groupId: string,
|
|
145
|
+
nonce: string,
|
|
146
|
+
timestamp: number
|
|
147
|
+
): Promise<Uint8Array> {
|
|
148
|
+
const enc = new TextEncoder();
|
|
149
|
+
const parts: Uint8Array[] = [];
|
|
150
|
+
|
|
151
|
+
parts.push(enc.encode(groupId));
|
|
152
|
+
parts.push(enc.encode(":"));
|
|
153
|
+
parts.push(enc.encode(nonce));
|
|
154
|
+
parts.push(enc.encode(":"));
|
|
155
|
+
|
|
156
|
+
// timestamp as 8-byte big-endian
|
|
157
|
+
const tsBuf = new ArrayBuffer(8);
|
|
158
|
+
const view = new DataView(tsBuf);
|
|
159
|
+
view.setBigUint64(0, BigInt(timestamp));
|
|
160
|
+
parts.push(new Uint8Array(tsBuf));
|
|
161
|
+
|
|
162
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
163
|
+
const buf = new Uint8Array(total);
|
|
164
|
+
let offset = 0;
|
|
165
|
+
for (const p of parts) {
|
|
166
|
+
buf.set(p, offset);
|
|
167
|
+
offset += p.length;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
171
|
+
return new Uint8Array(digest);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function generateNonce(): string {
|
|
175
|
+
const bytes = new Uint8Array(16);
|
|
176
|
+
crypto.getRandomValues(bytes);
|
|
177
|
+
return bytesToHex(bytes);
|
|
178
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth key certificate session flow.
|
|
3
|
+
*
|
|
4
|
+
* Creates a node session using an ECDSA auth key certificate instead of
|
|
5
|
+
* an OAuth/ZK proof. This enables keygen and signing without user login —
|
|
6
|
+
* useful for backend services, AI agents, or any programmatic access.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Sign a certificate binding an identity + session pub to the auth key
|
|
10
|
+
* 2. POST /v1/auth with the certificate to establish a session
|
|
11
|
+
* 3. Use the session key for subsequent keygen/sign requests (same as OAuth flow)
|
|
12
|
+
*
|
|
13
|
+
* The identity becomes the key namespace: keys are stored as "authkey:<identity>"
|
|
14
|
+
* (or "authkey:<identity>:<suffix>" with key_suffix).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { SessionKeypair } from "./types";
|
|
18
|
+
import { bytesToHex } from "./session";
|
|
19
|
+
|
|
20
|
+
export interface AuthKeyCertConfig {
|
|
21
|
+
/** Base URL or proxy URL for the node */
|
|
22
|
+
nodeUrl: string;
|
|
23
|
+
/** If set, requests go through this proxy (for CORS) */
|
|
24
|
+
proxyEndpoint?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuthKeyCertResult {
|
|
28
|
+
identity: string;
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authenticate with a node using an auth key certificate.
|
|
34
|
+
*
|
|
35
|
+
* @param config - Node URL / proxy config
|
|
36
|
+
* @param groupId - Group contract address
|
|
37
|
+
* @param authPrivateKey - 32-byte ECDSA private key (matches an on-chain auth key)
|
|
38
|
+
* @param identity - Logical identity string (e.g. "my-backend", "agent-1")
|
|
39
|
+
* @param sessionKeypair - Ephemeral session keypair for subsequent requests
|
|
40
|
+
* @param expiry - Certificate expiry (unix seconds). Default: 1 hour from now.
|
|
41
|
+
*/
|
|
42
|
+
export async function authenticateWithAuthKey(
|
|
43
|
+
config: AuthKeyCertConfig,
|
|
44
|
+
groupId: string,
|
|
45
|
+
authPrivateKey: Uint8Array,
|
|
46
|
+
identity: string,
|
|
47
|
+
sessionKeypair: SessionKeypair,
|
|
48
|
+
expiry?: number,
|
|
49
|
+
): Promise<AuthKeyCertResult> {
|
|
50
|
+
const { getPublicKey, signAsync } = await import("@noble/secp256k1");
|
|
51
|
+
|
|
52
|
+
const normalizedGroupId = groupId.toLowerCase();
|
|
53
|
+
const certExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600;
|
|
54
|
+
|
|
55
|
+
// Derive the auth key public key (with ECDSA prefix 0x00)
|
|
56
|
+
const authPubBytes = getPublicKey(authPrivateKey, true);
|
|
57
|
+
const authKeyPub = `0x00${bytesToHex(authPubBytes)}`;
|
|
58
|
+
|
|
59
|
+
// Sign certificate: SHA256(identity + ":" + group_id + ":" + session_pub_hex + ":" + expiry_8BE)
|
|
60
|
+
const certHash = await computeCertHash(
|
|
61
|
+
identity,
|
|
62
|
+
normalizedGroupId,
|
|
63
|
+
sessionKeypair.publicKeyHex,
|
|
64
|
+
certExpiry,
|
|
65
|
+
);
|
|
66
|
+
const certSig = await signAsync(certHash, authPrivateKey, {
|
|
67
|
+
lowS: true,
|
|
68
|
+
prehash: false,
|
|
69
|
+
});
|
|
70
|
+
const certSigHex = bytesToHex(new Uint8Array(certSig));
|
|
71
|
+
|
|
72
|
+
// POST /v1/auth with certificate
|
|
73
|
+
const url = config.proxyEndpoint ?? `${config.nodeUrl}/v1/auth`;
|
|
74
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
75
|
+
if (config.proxyEndpoint) {
|
|
76
|
+
headers["x-node-url"] = config.nodeUrl;
|
|
77
|
+
headers["x-node-path"] = "/v1/auth";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const res = await fetch(url, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
group_id: normalizedGroupId,
|
|
85
|
+
session_pub: sessionKeypair.publicKeyHex,
|
|
86
|
+
certificate: {
|
|
87
|
+
identity,
|
|
88
|
+
expiry: certExpiry,
|
|
89
|
+
auth_key_pub: authKeyPub,
|
|
90
|
+
signature: certSigHex,
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const body = await res.text();
|
|
97
|
+
throw new Error(`Auth key auth failed: ${res.status} — ${body}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
return {
|
|
102
|
+
identity: data.identity ?? identity,
|
|
103
|
+
expiresAt: data.expires_at ?? certExpiry,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Authenticate with a node using a Schnorr auth key certificate.
|
|
109
|
+
*
|
|
110
|
+
* The certificate hash is threshold-signed via the bootstrap group (FROST),
|
|
111
|
+
* producing a 65-byte Schnorr signature. This works for auth keys registered
|
|
112
|
+
* on-chain with the 0x01 (Schnorr) prefix.
|
|
113
|
+
*
|
|
114
|
+
* @param config - Bootstrap group config for threshold signing
|
|
115
|
+
* @param targetNodeUrl - The target group node to authenticate with
|
|
116
|
+
* @param proxyEndpoint - CORS proxy URL
|
|
117
|
+
* @param targetGroupId - The target group contract address
|
|
118
|
+
* @param authKeyPub - Auth key with 0x01 prefix (e.g. "0x0103d767f7...")
|
|
119
|
+
* @param identity - Logical identity string (e.g. "key-tester")
|
|
120
|
+
* @param sessionKeypair - Session keypair for the target group node
|
|
121
|
+
* @param bootstrapSessionKeypair - Session keypair for the bootstrap group (already authed)
|
|
122
|
+
* @param claims - OAuth claims for signing requests to the bootstrap group
|
|
123
|
+
* @param expiry - Certificate expiry (unix seconds). Default: 1 hour from now.
|
|
124
|
+
*/
|
|
125
|
+
export async function authenticateWithSchnorrAuthKey(
|
|
126
|
+
config: {
|
|
127
|
+
bootstrapGroup: string;
|
|
128
|
+
bootstrapNodes: string[];
|
|
129
|
+
nodeProxyUrl: string;
|
|
130
|
+
},
|
|
131
|
+
targetNodeUrl: string,
|
|
132
|
+
proxyEndpoint: string,
|
|
133
|
+
targetGroupId: string,
|
|
134
|
+
authKeyPub: string,
|
|
135
|
+
identity: string,
|
|
136
|
+
sessionKeypair: SessionKeypair,
|
|
137
|
+
bootstrapSessionKeypair: SessionKeypair,
|
|
138
|
+
claims: { iss: string; sub: string },
|
|
139
|
+
expiry?: number,
|
|
140
|
+
): Promise<AuthKeyCertResult> {
|
|
141
|
+
const { signSignRequest } = await import("./request");
|
|
142
|
+
|
|
143
|
+
const normalizedGroupId = targetGroupId.toLowerCase();
|
|
144
|
+
const certExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600;
|
|
145
|
+
|
|
146
|
+
// Compute certificate hash
|
|
147
|
+
const certHash = await computeCertHash(
|
|
148
|
+
identity,
|
|
149
|
+
normalizedGroupId,
|
|
150
|
+
sessionKeypair.publicKeyHex,
|
|
151
|
+
certExpiry,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Threshold-sign the cert hash via bootstrap group
|
|
155
|
+
const signReq = await signSignRequest(
|
|
156
|
+
bootstrapSessionKeypair,
|
|
157
|
+
claims as unknown as import("./types").IdTokenClaims,
|
|
158
|
+
config.bootstrapGroup,
|
|
159
|
+
certHash,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const signRes = await fetch(config.nodeProxyUrl, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
"x-node-url": config.bootstrapNodes[0],
|
|
167
|
+
"x-node-path": "/v1/sign",
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(signReq),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!signRes.ok) {
|
|
173
|
+
const body = await signRes.text();
|
|
174
|
+
throw new Error(`Schnorr cert signing failed: ${signRes.status} — ${body}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { ethereum_signature } = await signRes.json();
|
|
178
|
+
|
|
179
|
+
// POST /v1/auth on the target group node with the Schnorr-signed certificate
|
|
180
|
+
const res = await fetch(proxyEndpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"x-node-url": targetNodeUrl,
|
|
185
|
+
"x-node-path": "/v1/auth",
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
group_id: normalizedGroupId,
|
|
189
|
+
session_pub: sessionKeypair.publicKeyHex,
|
|
190
|
+
certificate: {
|
|
191
|
+
identity,
|
|
192
|
+
expiry: certExpiry,
|
|
193
|
+
auth_key_pub: authKeyPub,
|
|
194
|
+
signature: ethereum_signature,
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
const body = await res.text();
|
|
201
|
+
throw new Error(`Schnorr auth key auth failed: ${res.status} — ${body}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const data = await res.json();
|
|
205
|
+
return {
|
|
206
|
+
identity: data.identity ?? identity,
|
|
207
|
+
expiresAt: data.expires_at ?? certExpiry,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function computeCertHash(
|
|
212
|
+
identity: string,
|
|
213
|
+
groupId: string,
|
|
214
|
+
sessionPubHex: string,
|
|
215
|
+
expiry: number,
|
|
216
|
+
): Promise<Uint8Array> {
|
|
217
|
+
const enc = new TextEncoder();
|
|
218
|
+
const expiryBuf = new ArrayBuffer(8);
|
|
219
|
+
new DataView(expiryBuf).setBigUint64(0, BigInt(expiry));
|
|
220
|
+
|
|
221
|
+
const parts: Uint8Array[] = [
|
|
222
|
+
enc.encode(identity),
|
|
223
|
+
enc.encode(":"),
|
|
224
|
+
enc.encode(groupId),
|
|
225
|
+
enc.encode(":"),
|
|
226
|
+
enc.encode(sessionPubHex),
|
|
227
|
+
enc.encode(":"),
|
|
228
|
+
new Uint8Array(expiryBuf),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
232
|
+
const buf = new Uint8Array(total);
|
|
233
|
+
let offset = 0;
|
|
234
|
+
for (const p of parts) {
|
|
235
|
+
buf.set(p, offset);
|
|
236
|
+
offset += p.length;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
240
|
+
return new Uint8Array(digest);
|
|
241
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap group node authentication.
|
|
3
|
+
*
|
|
4
|
+
* After generating a ZK proof of the JWT, POST it to each
|
|
5
|
+
* bootstrap node to register the session key.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { NodeAuthRequest } from "./types";
|
|
9
|
+
import { bytesToHex } from "./session";
|
|
10
|
+
|
|
11
|
+
export interface BootstrapConfig {
|
|
12
|
+
groupId: string; // bootstrap group contract address
|
|
13
|
+
nodeUrls: string[]; // bootstrap node API URLs
|
|
14
|
+
/** Proxy endpoint for CORS — if set, requests go through this instead of directly to nodes */
|
|
15
|
+
proxyEndpoint?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuthResult {
|
|
19
|
+
identity: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Authenticate with all bootstrap nodes.
|
|
25
|
+
*
|
|
26
|
+
* Posts the ZK proof + session public key to each node's /v1/auth.
|
|
27
|
+
* All nodes must accept the session for signing to work.
|
|
28
|
+
*/
|
|
29
|
+
export async function authenticateWithBootstrap(
|
|
30
|
+
config: BootstrapConfig,
|
|
31
|
+
proof: Uint8Array,
|
|
32
|
+
sessionPubHex: string,
|
|
33
|
+
claims: { iss: string; sub: string; exp: number; aud: string; azp: string },
|
|
34
|
+
jwksModulusBytes: Uint8Array
|
|
35
|
+
): Promise<AuthResult> {
|
|
36
|
+
const request: NodeAuthRequest = {
|
|
37
|
+
group_id: config.groupId,
|
|
38
|
+
session_pub: sessionPubHex,
|
|
39
|
+
proof: bytesToHex(proof),
|
|
40
|
+
sub: claims.sub,
|
|
41
|
+
iss: claims.iss,
|
|
42
|
+
exp: claims.exp,
|
|
43
|
+
aud: claims.aud,
|
|
44
|
+
azp: claims.azp,
|
|
45
|
+
jwks_modulus: bytesToHex(jwksModulusBytes),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Auth with all nodes in parallel
|
|
49
|
+
const results = await Promise.allSettled(
|
|
50
|
+
config.nodeUrls.map((url) =>
|
|
51
|
+
authWithNode(url, request, config.proxyEndpoint)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Check that at least one succeeded
|
|
56
|
+
const successes = results.filter(
|
|
57
|
+
(r): r is PromiseFulfilledResult<AuthResult> => r.status === "fulfilled"
|
|
58
|
+
);
|
|
59
|
+
const failures = results.filter(
|
|
60
|
+
(r): r is PromiseRejectedResult => r.status === "rejected"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (successes.length === 0) {
|
|
64
|
+
const reasons = failures.map((f) => f.reason?.message ?? String(f.reason));
|
|
65
|
+
throw new Error(
|
|
66
|
+
`All bootstrap nodes rejected auth: ${reasons.join("; ")}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (failures.length > 0) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`${failures.length}/${config.nodeUrls.length} bootstrap nodes failed auth:`,
|
|
73
|
+
failures.map((f) => f.reason?.message)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return successes[0].value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function authWithNode(
|
|
81
|
+
baseUrl: string,
|
|
82
|
+
request: NodeAuthRequest,
|
|
83
|
+
proxyEndpoint?: string
|
|
84
|
+
): Promise<AuthResult> {
|
|
85
|
+
const url = proxyEndpoint ?? `${baseUrl}/v1/auth`;
|
|
86
|
+
const headers: Record<string, string> = {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
};
|
|
89
|
+
if (proxyEndpoint) {
|
|
90
|
+
headers["x-node-url"] = baseUrl;
|
|
91
|
+
headers["x-node-path"] = "/v1/auth";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const res = await fetch(url, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify(request),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const body = await res.text();
|
|
102
|
+
throw new Error(`${baseUrl}: ${res.status} — ${body}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return res.json();
|
|
106
|
+
}
|