@wopr-network/platform-core 1.42.3 → 1.43.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.
@@ -1,132 +1,116 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { BTCPayClient, loadCryptoConfig } from "./client.js";
3
-
4
- describe("BTCPayClient", () => {
5
- it("createInvoice sends correct request and returns id + checkoutLink", async () => {
6
- const mockResponse = { id: "inv-001", checkoutLink: "https://btcpay.example.com/i/inv-001" };
7
- const fetchSpy = vi
8
- .spyOn(globalThis, "fetch")
9
- .mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
10
-
11
- const client = new BTCPayClient({
12
- apiKey: "test-key",
13
- baseUrl: "https://btcpay.example.com",
14
- storeId: "store-abc",
15
- });
16
-
17
- const result = await client.createInvoice({
18
- amountUsd: 25,
19
- orderId: "order-123",
20
- buyerEmail: "test@example.com",
21
- });
2
+ import { BTCPayClient, CryptoServiceClient, loadCryptoConfig } from "./client.js";
22
3
 
23
- expect(result.id).toBe("inv-001");
24
- expect(result.checkoutLink).toBe("https://btcpay.example.com/i/inv-001");
4
+ describe("CryptoServiceClient", () => {
5
+ afterEach(() => vi.restoreAllMocks());
25
6
 
26
- expect(fetchSpy).toHaveBeenCalledOnce();
27
- const [url, opts] = fetchSpy.mock.calls[0];
28
- expect(url).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices");
29
- expect(opts?.method).toBe("POST");
7
+ it("deriveAddress sends POST /address with chain", async () => {
8
+ const mockResponse = { address: "bc1q...", index: 42, chain: "bitcoin", token: "BTC" };
9
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 201 }));
30
10
 
31
- const headers = opts?.headers as Record<string, string>;
32
- expect(headers.Authorization).toBe("token test-key");
33
- expect(headers["Content-Type"]).toBe("application/json");
11
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
12
+ const result = await client.deriveAddress("btc");
34
13
 
35
- const body = JSON.parse(opts?.body as string);
36
- expect(body.amount).toBe("25");
37
- expect(body.currency).toBe("USD");
38
- expect(body.metadata.orderId).toBe("order-123");
39
- expect(body.metadata.buyerEmail).toBe("test@example.com");
40
- expect(body.checkout.speedPolicy).toBe("MediumSpeed");
14
+ expect(result.address).toBe("bc1q...");
15
+ expect(result.index).toBe(42);
41
16
 
42
- fetchSpy.mockRestore();
17
+ const [url, opts] = vi.mocked(fetch).mock.calls[0];
18
+ expect(url).toBe("http://localhost:3100/address");
19
+ expect(opts?.method).toBe("POST");
20
+ expect(JSON.parse(opts?.body as string)).toEqual({ chain: "btc" });
43
21
  });
44
22
 
45
- it("createInvoice includes redirectURL when provided", async () => {
46
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
47
- new Response(JSON.stringify({ id: "inv-002", checkoutLink: "https://btcpay.example.com/i/inv-002" }), {
48
- status: 200,
49
- }),
50
- );
51
-
52
- const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "s" });
53
- await client.createInvoice({ amountUsd: 10, orderId: "o", redirectURL: "https://app.example.com/success" });
23
+ it("createCharge sends POST /charges", async () => {
24
+ const mockResponse = {
25
+ chargeId: "btc:bc1q...",
26
+ address: "bc1q...",
27
+ chain: "btc",
28
+ token: "BTC",
29
+ amountUsd: 50,
30
+ derivationIndex: 42,
31
+ expiresAt: "2026-03-20T04:00:00Z",
32
+ };
33
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 201 }));
34
+
35
+ const client = new CryptoServiceClient({
36
+ baseUrl: "http://localhost:3100",
37
+ serviceKey: "sk-test",
38
+ tenantId: "tenant-1",
39
+ });
40
+ const result = await client.createCharge({ chain: "btc", amountUsd: 50 });
54
41
 
55
- const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
56
- expect(body.checkout.redirectURL).toBe("https://app.example.com/success");
42
+ expect(result.chargeId).toBe("btc:bc1q...");
43
+ expect(result.address).toBe("bc1q...");
57
44
 
58
- fetchSpy.mockRestore();
45
+ const [, opts] = vi.mocked(fetch).mock.calls[0];
46
+ const headers = opts?.headers as Record<string, string>;
47
+ expect(headers.Authorization).toBe("Bearer sk-test");
48
+ expect(headers["X-Tenant-Id"]).toBe("tenant-1");
59
49
  });
60
50
 
61
- it("createInvoice throws on non-ok response", async () => {
62
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Unauthorized", { status: 401 }));
51
+ it("getCharge sends GET /charges/:id", async () => {
52
+ const mockResponse = { chargeId: "btc:bc1q...", status: "confirmed" };
53
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
63
54
 
64
- const client = new BTCPayClient({ apiKey: "bad-key", baseUrl: "https://btcpay.example.com", storeId: "s" });
65
- await expect(client.createInvoice({ amountUsd: 10, orderId: "o" })).rejects.toThrow(
66
- "BTCPay createInvoice failed (401)",
67
- );
55
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
56
+ const result = await client.getCharge("btc:bc1q...");
68
57
 
69
- fetchSpy.mockRestore();
58
+ expect(result.status).toBe("confirmed");
59
+ expect(vi.mocked(fetch).mock.calls[0][0]).toBe("http://localhost:3100/charges/btc%3Abc1q...");
70
60
  });
71
61
 
72
- it("getInvoice sends correct request", async () => {
73
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
74
- new Response(JSON.stringify({ id: "inv-001", status: "Settled", amount: "25", currency: "USD" }), {
75
- status: 200,
76
- }),
77
- );
62
+ it("listChains sends GET /chains", async () => {
63
+ const mockResponse = [{ id: "btc", token: "BTC", chain: "bitcoin", decimals: 8 }];
64
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
78
65
 
79
- const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "store-abc" });
80
- const result = await client.getInvoice("inv-001");
66
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
67
+ const result = await client.listChains();
81
68
 
82
- expect(result.status).toBe("Settled");
83
- expect(fetchSpy.mock.calls[0][0]).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices/inv-001");
84
-
85
- fetchSpy.mockRestore();
69
+ expect(result).toHaveLength(1);
70
+ expect(result[0].token).toBe("BTC");
86
71
  });
87
- });
88
72
 
89
- describe("loadCryptoConfig", () => {
90
- beforeEach(() => {
91
- delete process.env.BTCPAY_API_KEY;
92
- delete process.env.BTCPAY_BASE_URL;
93
- delete process.env.BTCPAY_STORE_ID;
94
- });
73
+ it("throws on non-ok response", async () => {
74
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not found", { status: 404 }));
95
75
 
96
- afterEach(() => {
97
- vi.unstubAllEnvs();
76
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
77
+ await expect(client.getCharge("missing")).rejects.toThrow("CryptoService getCharge failed (404)");
98
78
  });
79
+ });
99
80
 
100
- it("returns null when BTCPAY_API_KEY is missing", () => {
101
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
102
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
103
- expect(loadCryptoConfig()).toBeNull();
81
+ describe("BTCPayClient (deprecated)", () => {
82
+ it("throws on createInvoice", async () => {
83
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://example.com", storeId: "s" });
84
+ await expect(client.createInvoice({ amountUsd: 10, orderId: "o" })).rejects.toThrow("deprecated");
104
85
  });
105
86
 
106
- it("returns null when BTCPAY_BASE_URL is missing", () => {
107
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
108
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
109
- expect(loadCryptoConfig()).toBeNull();
87
+ it("throws on getInvoice", async () => {
88
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://example.com", storeId: "s" });
89
+ await expect(client.getInvoice("inv-1")).rejects.toThrow("deprecated");
110
90
  });
91
+ });
111
92
 
112
- it("returns null when BTCPAY_STORE_ID is missing", () => {
113
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
114
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
115
- expect(loadCryptoConfig()).toBeNull();
93
+ describe("loadCryptoConfig", () => {
94
+ beforeEach(() => {
95
+ delete process.env.CRYPTO_SERVICE_URL;
96
+ delete process.env.CRYPTO_SERVICE_KEY;
97
+ delete process.env.TENANT_ID;
116
98
  });
117
99
 
118
- it("returns config when all env vars are set", () => {
119
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
120
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
121
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
100
+ afterEach(() => vi.unstubAllEnvs());
101
+
102
+ it("returns config when CRYPTO_SERVICE_URL is set", () => {
103
+ vi.stubEnv("CRYPTO_SERVICE_URL", "http://10.120.0.5:3100");
104
+ vi.stubEnv("CRYPTO_SERVICE_KEY", "sk-test");
105
+ vi.stubEnv("TENANT_ID", "tenant-1");
122
106
  expect(loadCryptoConfig()).toEqual({
123
- apiKey: "test-key",
124
- baseUrl: "https://btcpay.test",
125
- storeId: "store-1",
107
+ baseUrl: "http://10.120.0.5:3100",
108
+ serviceKey: "sk-test",
109
+ tenantId: "tenant-1",
126
110
  });
127
111
  });
128
112
 
129
- it("returns null when all env vars are missing", () => {
113
+ it("returns null when CRYPTO_SERVICE_URL is missing", () => {
130
114
  expect(loadCryptoConfig()).toBeNull();
131
115
  });
132
116
  });
@@ -1,86 +1,166 @@
1
- import type { CryptoBillingConfig } from "./types.js";
1
+ /**
2
+ * Crypto Key Server client — for products to call the shared service.
3
+ *
4
+ * Replaces BTCPayClient. Products set CRYPTO_SERVICE_URL instead of
5
+ * BTCPAY_API_KEY + BTCPAY_BASE_URL + BTCPAY_STORE_ID.
6
+ */
7
+
8
+ export interface CryptoServiceConfig {
9
+ /** Base URL of the crypto key server (e.g. http://10.120.0.5:3100) */
10
+ baseUrl: string;
11
+ /** Service key for auth (reuses gateway service key) */
12
+ serviceKey?: string;
13
+ /** Tenant ID header */
14
+ tenantId?: string;
15
+ }
16
+
17
+ export interface DeriveAddressResult {
18
+ address: string;
19
+ index: number;
20
+ chain: string;
21
+ token: string;
22
+ }
2
23
 
3
- export type { CryptoBillingConfig as CryptoConfig };
24
+ export interface CreateChargeResult {
25
+ chargeId: string;
26
+ address: string;
27
+ chain: string;
28
+ token: string;
29
+ amountUsd: number;
30
+ derivationIndex: number;
31
+ expiresAt: string;
32
+ }
33
+
34
+ export interface ChargeStatus {
35
+ chargeId: string;
36
+ status: string;
37
+ address: string | null;
38
+ chain: string | null;
39
+ token: string | null;
40
+ amountUsdCents: number;
41
+ creditedAt: string | null;
42
+ }
43
+
44
+ export interface ChainInfo {
45
+ id: string;
46
+ token: string;
47
+ chain: string;
48
+ decimals: number;
49
+ displayName: string;
50
+ contractAddress: string | null;
51
+ confirmations: number;
52
+ }
4
53
 
5
54
  /**
6
- * Lightweight BTCPay Server Greenfield API client.
7
- *
8
- * Uses plain fetch — zero vendor dependencies.
9
- * Auth header format: "token <apiKey>" (NOT "Bearer").
55
+ * Client for the shared crypto key server.
56
+ * Products use this instead of running local watchers + holding xpubs.
10
57
  */
11
- export class BTCPayClient {
12
- constructor(private readonly config: CryptoBillingConfig) {}
58
+ export class CryptoServiceClient {
59
+ constructor(private readonly config: CryptoServiceConfig) {}
13
60
 
14
61
  private headers(): Record<string, string> {
15
- return {
16
- "Content-Type": "application/json",
17
- Authorization: `token ${this.config.apiKey}`,
18
- };
62
+ const h: Record<string, string> = { "Content-Type": "application/json" };
63
+ if (this.config.serviceKey) h.Authorization = `Bearer ${this.config.serviceKey}`;
64
+ if (this.config.tenantId) h["X-Tenant-Id"] = this.config.tenantId;
65
+ return h;
19
66
  }
20
67
 
21
- /**
22
- * Create an invoice on the BTCPay store.
23
- *
24
- * Returns the invoice ID and checkout link (URL to redirect the user).
25
- */
26
- async createInvoice(opts: {
27
- amountUsd: number;
28
- orderId: string;
29
- buyerEmail?: string;
30
- redirectURL?: string;
31
- }): Promise<{ id: string; checkoutLink: string }> {
32
- const url = `${this.config.baseUrl}/api/v1/stores/${this.config.storeId}/invoices`;
33
- const body = {
34
- amount: String(opts.amountUsd),
35
- currency: "USD",
36
- metadata: {
37
- orderId: opts.orderId,
38
- buyerEmail: opts.buyerEmail,
39
- },
40
- checkout: {
41
- speedPolicy: "MediumSpeed",
42
- expirationMinutes: 30,
43
- ...(opts.redirectURL ? { redirectURL: opts.redirectURL } : {}),
44
- },
45
- };
46
-
47
- const res = await fetch(url, {
68
+ /** Derive the next unused address for a chain. */
69
+ async deriveAddress(chain: string): Promise<DeriveAddressResult> {
70
+ const res = await fetch(`${this.config.baseUrl}/address`, {
48
71
  method: "POST",
49
72
  headers: this.headers(),
50
- body: JSON.stringify(body),
73
+ body: JSON.stringify({ chain }),
51
74
  });
52
-
53
75
  if (!res.ok) {
54
76
  const text = await res.text().catch(() => "");
55
- throw new Error(`BTCPay createInvoice failed (${res.status}): ${text}`);
77
+ throw new Error(`CryptoService deriveAddress failed (${res.status}): ${text}`);
56
78
  }
79
+ return (await res.json()) as DeriveAddressResult;
80
+ }
57
81
 
58
- const data = (await res.json()) as { id: string; checkoutLink: string };
59
- return { id: data.id, checkoutLink: data.checkoutLink };
82
+ /** Create a payment charge derives address, sets expiry, starts watching. */
83
+ async createCharge(opts: {
84
+ chain: string;
85
+ amountUsd: number;
86
+ callbackUrl?: string;
87
+ metadata?: Record<string, unknown>;
88
+ }): Promise<CreateChargeResult> {
89
+ const res = await fetch(`${this.config.baseUrl}/charges`, {
90
+ method: "POST",
91
+ headers: this.headers(),
92
+ body: JSON.stringify(opts),
93
+ });
94
+ if (!res.ok) {
95
+ const text = await res.text().catch(() => "");
96
+ throw new Error(`CryptoService createCharge failed (${res.status}): ${text}`);
97
+ }
98
+ return (await res.json()) as CreateChargeResult;
60
99
  }
61
100
 
62
- /** Get invoice status by ID. */
63
- async getInvoice(invoiceId: string): Promise<{ id: string; status: string; amount: string; currency: string }> {
64
- const url = `${this.config.baseUrl}/api/v1/stores/${this.config.storeId}/invoices/${invoiceId}`;
65
- const res = await fetch(url, { headers: this.headers() });
101
+ /** Check charge status. */
102
+ async getCharge(chargeId: string): Promise<ChargeStatus> {
103
+ const res = await fetch(`${this.config.baseUrl}/charges/${encodeURIComponent(chargeId)}`, {
104
+ headers: this.headers(),
105
+ });
106
+ if (!res.ok) {
107
+ const text = await res.text().catch(() => "");
108
+ throw new Error(`CryptoService getCharge failed (${res.status}): ${text}`);
109
+ }
110
+ return (await res.json()) as ChargeStatus;
111
+ }
66
112
 
113
+ /** List all enabled payment methods (for checkout UI). */
114
+ async listChains(): Promise<ChainInfo[]> {
115
+ const res = await fetch(`${this.config.baseUrl}/chains`, {
116
+ headers: this.headers(),
117
+ });
67
118
  if (!res.ok) {
68
119
  const text = await res.text().catch(() => "");
69
- throw new Error(`BTCPay getInvoice failed (${res.status}): ${text}`);
120
+ throw new Error(`CryptoService listChains failed (${res.status}): ${text}`);
70
121
  }
122
+ return (await res.json()) as ChainInfo[];
123
+ }
124
+ }
71
125
 
72
- return (await res.json()) as { id: string; status: string; amount: string; currency: string };
126
+ /**
127
+ * Load crypto service config from environment.
128
+ * Returns null if CRYPTO_SERVICE_URL is not set.
129
+ *
130
+ * Also supports legacy BTCPay env vars for backwards compat during migration.
131
+ */
132
+ export function loadCryptoConfig(): CryptoServiceConfig | null {
133
+ const baseUrl = process.env.CRYPTO_SERVICE_URL;
134
+ if (baseUrl) {
135
+ return {
136
+ baseUrl,
137
+ serviceKey: process.env.CRYPTO_SERVICE_KEY,
138
+ tenantId: process.env.TENANT_ID,
139
+ };
73
140
  }
141
+ return null;
74
142
  }
75
143
 
144
+ // Legacy type alias for backwards compat
145
+ export type CryptoConfig = CryptoServiceConfig;
146
+
76
147
  /**
77
- * Load BTCPay config from environment variables.
78
- * Returns null if any required var is missing.
148
+ * @deprecated Use CryptoServiceClient instead. BTCPay is replaced by the crypto key server.
149
+ * Kept for backwards compat products still import BTCPayClient during migration.
79
150
  */
80
- export function loadCryptoConfig(): CryptoBillingConfig | null {
81
- const apiKey = process.env.BTCPAY_API_KEY;
82
- const baseUrl = process.env.BTCPAY_BASE_URL;
83
- const storeId = process.env.BTCPAY_STORE_ID;
84
- if (!apiKey || !baseUrl || !storeId) return null;
85
- return { apiKey, baseUrl, storeId };
151
+ export class BTCPayClient {
152
+ constructor(_config: { apiKey: string; baseUrl: string; storeId: string }) {}
153
+
154
+ async createInvoice(_opts: {
155
+ amountUsd: number;
156
+ orderId: string;
157
+ buyerEmail?: string;
158
+ redirectURL?: string;
159
+ }): Promise<{ id: string; checkoutLink: string }> {
160
+ throw new Error("BTCPayClient is deprecated — migrate to CryptoServiceClient");
161
+ }
162
+
163
+ async getInvoice(_invoiceId: string): Promise<{ id: string; status: string; amount: string; currency: string }> {
164
+ throw new Error("BTCPayClient is deprecated — migrate to CryptoServiceClient");
165
+ }
86
166
  }
@@ -2,11 +2,20 @@ export * from "./btc/index.js";
2
2
  export type { CryptoChargeRecord, CryptoDepositChargeInput, ICryptoChargeRepository } from "./charge-store.js";
3
3
  export { CryptoChargeRepository, DrizzleCryptoChargeRepository } from "./charge-store.js";
4
4
  export { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
5
- export type { CryptoConfig } from "./client.js";
6
- export { BTCPayClient, loadCryptoConfig } from "./client.js";
5
+ export type {
6
+ ChainInfo,
7
+ ChargeStatus,
8
+ CreateChargeResult,
9
+ CryptoConfig,
10
+ CryptoServiceConfig,
11
+ DeriveAddressResult,
12
+ } from "./client.js";
13
+ export { BTCPayClient, CryptoServiceClient, loadCryptoConfig } from "./client.js";
7
14
  export type { IWatcherCursorStore } from "./cursor-store.js";
8
15
  export { DrizzleWatcherCursorStore } from "./cursor-store.js";
9
16
  export * from "./evm/index.js";
17
+ export type { KeyServerDeps } from "./key-server.js";
18
+ export { createKeyServerApp } from "./key-server.js";
10
19
  export * from "./oracle/index.js";
11
20
  export type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
12
21
  export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Standalone entry point for the crypto key server.
3
+ *
4
+ * Deploys on the chain server (pay.wopr.bot:3100).
5
+ * Boots: postgres → migrations → key server routes → watchers → serve.
6
+ *
7
+ * Usage: node dist/billing/crypto/key-server-entry.js
8
+ */
9
+ /* biome-ignore-all lint/suspicious/noConsole: standalone entry point */
10
+ import { serve } from "@hono/node-server";
11
+ import { drizzle } from "drizzle-orm/node-postgres";
12
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
13
+ import pg from "pg";
14
+ import * as schema from "../../db/schema/index.js";
15
+ import { DrizzleCryptoChargeRepository } from "./charge-store.js";
16
+ import { createKeyServerApp } from "./key-server.js";
17
+ import { DrizzlePaymentMethodStore } from "./payment-method-store.js";
18
+
19
+ const PORT = Number(process.env.PORT ?? "3100");
20
+ const DATABASE_URL = process.env.DATABASE_URL;
21
+ const SERVICE_KEY = process.env.SERVICE_KEY;
22
+ const ADMIN_TOKEN = process.env.ADMIN_TOKEN;
23
+
24
+ if (!DATABASE_URL) {
25
+ console.error("DATABASE_URL is required");
26
+ process.exit(1);
27
+ }
28
+
29
+ async function main(): Promise<void> {
30
+ const pool = new pg.Pool({ connectionString: DATABASE_URL });
31
+
32
+ // Run migrations FIRST, before creating schema-typed db
33
+ console.log("[crypto-key-server] Running migrations...");
34
+ await migrate(drizzle(pool), { migrationsFolder: "./drizzle/migrations" });
35
+
36
+ // Now create the schema-typed db (columns guaranteed to exist)
37
+ console.log("[crypto-key-server] Connecting...");
38
+ const db = drizzle(pool, { schema }) as unknown as import("../../db/index.js").DrizzleDb;
39
+
40
+ const chargeStore = new DrizzleCryptoChargeRepository(db);
41
+ const methodStore = new DrizzlePaymentMethodStore(db);
42
+
43
+ const app = createKeyServerApp({ db, chargeStore, methodStore, serviceKey: SERVICE_KEY, adminToken: ADMIN_TOKEN });
44
+
45
+ console.log(`[crypto-key-server] Listening on :${PORT}`);
46
+ serve({ fetch: app.fetch, port: PORT });
47
+ }
48
+
49
+ main().catch((err) => {
50
+ console.error("[crypto-key-server] Fatal:", err);
51
+ process.exit(1);
52
+ });