@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.
Files changed (95) hide show
  1. package/dist/admin.d.ts +38 -0
  2. package/dist/admin.d.ts.map +1 -0
  3. package/dist/admin.js +112 -0
  4. package/dist/admin.js.map +1 -0
  5. package/dist/authkey-session.d.ts +64 -0
  6. package/dist/authkey-session.d.ts.map +1 -0
  7. package/dist/authkey-session.js +164 -0
  8. package/dist/authkey-session.js.map +1 -0
  9. package/dist/bootstrap.d.ts +30 -0
  10. package/dist/bootstrap.d.ts.map +1 -0
  11. package/dist/bootstrap.js +60 -0
  12. package/dist/bootstrap.js.map +1 -0
  13. package/dist/bundler.d.ts +85 -0
  14. package/dist/bundler.d.ts.map +1 -0
  15. package/dist/bundler.js +160 -0
  16. package/dist/bundler.js.map +1 -0
  17. package/dist/delegate.d.ts +57 -0
  18. package/dist/delegate.d.ts.map +1 -0
  19. package/dist/delegate.js +111 -0
  20. package/dist/delegate.js.map +1 -0
  21. package/dist/frostVerify.d.ts +23 -0
  22. package/dist/frostVerify.d.ts.map +1 -0
  23. package/dist/frostVerify.js +69 -0
  24. package/dist/frostVerify.js.map +1 -0
  25. package/dist/index.d.ts +32 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +38 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jwks.d.ts +28 -0
  30. package/dist/jwks.d.ts.map +1 -0
  31. package/dist/jwks.js +81 -0
  32. package/dist/jwks.js.map +1 -0
  33. package/dist/jwt.d.ts +27 -0
  34. package/dist/jwt.d.ts.map +1 -0
  35. package/dist/jwt.js +50 -0
  36. package/dist/jwt.js.map +1 -0
  37. package/dist/keygen.d.ts +26 -0
  38. package/dist/keygen.d.ts.map +1 -0
  39. package/dist/keygen.js +60 -0
  40. package/dist/keygen.js.map +1 -0
  41. package/dist/oauth.d.ts +34 -0
  42. package/dist/oauth.d.ts.map +1 -0
  43. package/dist/oauth.js +119 -0
  44. package/dist/oauth.js.map +1 -0
  45. package/dist/request.d.ts +42 -0
  46. package/dist/request.d.ts.map +1 -0
  47. package/dist/request.js +115 -0
  48. package/dist/request.js.map +1 -0
  49. package/dist/scopedSign.d.ts +82 -0
  50. package/dist/scopedSign.d.ts.map +1 -0
  51. package/dist/scopedSign.js +130 -0
  52. package/dist/scopedSign.js.map +1 -0
  53. package/dist/server-prover.d.ts +29 -0
  54. package/dist/server-prover.d.ts.map +1 -0
  55. package/dist/server-prover.js +54 -0
  56. package/dist/server-prover.js.map +1 -0
  57. package/dist/session.d.ts +14 -0
  58. package/dist/session.d.ts.map +1 -0
  59. package/dist/session.js +29 -0
  60. package/dist/session.js.map +1 -0
  61. package/dist/types.d.ts +56 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/userop.d.ts +104 -0
  66. package/dist/userop.d.ts.map +1 -0
  67. package/dist/userop.js +212 -0
  68. package/dist/userop.js.map +1 -0
  69. package/dist/x402.d.ts +127 -0
  70. package/dist/x402.d.ts.map +1 -0
  71. package/dist/x402.js +167 -0
  72. package/dist/x402.js.map +1 -0
  73. package/package.json +64 -0
  74. package/src/admin.ts +178 -0
  75. package/src/authkey-session.ts +241 -0
  76. package/src/bootstrap.ts +106 -0
  77. package/src/bundler.ts +256 -0
  78. package/src/delegate.ts +163 -0
  79. package/src/frostVerify.ts +79 -0
  80. package/src/generate-inputs.ts +158 -0
  81. package/src/index.ts +43 -0
  82. package/src/jwks.ts +92 -0
  83. package/src/jwt.ts +74 -0
  84. package/src/keygen.ts +89 -0
  85. package/src/oauth.ts +157 -0
  86. package/src/partial-sha.ts +99 -0
  87. package/src/proof.ts +99 -0
  88. package/src/request.ts +174 -0
  89. package/src/scopedSign.ts +184 -0
  90. package/src/server-prover.ts +76 -0
  91. package/src/session.ts +33 -0
  92. package/src/types.ts +63 -0
  93. package/src/userop.ts +368 -0
  94. package/src/witness.ts +132 -0
  95. 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
+ }
@@ -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
+ }