@wopr-network/platform-core 1.13.1 → 1.13.3

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 (36) hide show
  1. package/dist/api/routes/admin-audit-helper.d.ts +1 -1
  2. package/dist/db/schema/gateway-service-keys.d.ts +109 -0
  3. package/dist/db/schema/gateway-service-keys.js +18 -0
  4. package/dist/db/schema/index.d.ts +1 -0
  5. package/dist/db/schema/index.js +1 -0
  6. package/dist/gateway/gateway-routes.test.js +1 -1
  7. package/dist/gateway/index.d.ts +2 -0
  8. package/dist/gateway/index.js +1 -0
  9. package/dist/gateway/protocol/anthropic.js +1 -1
  10. package/dist/gateway/protocol/deps.d.ts +3 -3
  11. package/dist/gateway/protocol/openai.js +1 -1
  12. package/dist/gateway/proxy.d.ts +2 -2
  13. package/dist/gateway/route-mounting.test.js +1 -1
  14. package/dist/gateway/service-key-auth.d.ts +1 -1
  15. package/dist/gateway/service-key-auth.js +1 -1
  16. package/dist/gateway/service-key-repository.d.ts +27 -0
  17. package/dist/gateway/service-key-repository.js +64 -0
  18. package/dist/gateway/types.d.ts +3 -3
  19. package/dist/monetization/socket/socket.d.ts +3 -3
  20. package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
  21. package/drizzle/migrations/meta/_journal.json +7 -0
  22. package/package.json +1 -1
  23. package/src/api/routes/admin-audit-helper.ts +1 -1
  24. package/src/db/schema/gateway-service-keys.ts +23 -0
  25. package/src/db/schema/index.ts +1 -0
  26. package/src/gateway/gateway-routes.test.ts +1 -1
  27. package/src/gateway/index.ts +2 -0
  28. package/src/gateway/protocol/anthropic.ts +2 -2
  29. package/src/gateway/protocol/deps.ts +3 -3
  30. package/src/gateway/protocol/openai.ts +2 -2
  31. package/src/gateway/proxy.ts +2 -2
  32. package/src/gateway/route-mounting.test.ts +1 -1
  33. package/src/gateway/service-key-auth.ts +4 -2
  34. package/src/gateway/service-key-repository.ts +87 -0
  35. package/src/gateway/types.ts +3 -3
  36. package/src/monetization/socket/socket.ts +4 -4
@@ -1,7 +1,7 @@
1
1
  import type { AuditEntry } from "../../admin/audit-log.js";
2
2
  /** Minimal interface for admin audit logging in route factories. */
3
3
  export interface AdminAuditLogger {
4
- log(entry: AuditEntry): void | Promise<void>;
4
+ log(entry: AuditEntry): void | Promise<unknown>;
5
5
  }
6
6
  /** Safely log an admin audit entry — never throws. */
7
7
  export declare function safeAuditLog(logger: (() => AdminAuditLogger) | undefined, entry: AuditEntry): void;
@@ -0,0 +1,109 @@
1
+ export declare const gatewayServiceKeys: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "gateway_service_keys";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "gateway_service_keys";
8
+ dataType: "string";
9
+ columnType: "PgText";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: false;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: [string, ...string[]];
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ keyHash: import("drizzle-orm/pg-core").PgColumn<{
23
+ name: "key_hash";
24
+ tableName: "gateway_service_keys";
25
+ dataType: "string";
26
+ columnType: "PgText";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: false;
31
+ isPrimaryKey: false;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: [string, ...string[]];
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
40
+ name: "tenant_id";
41
+ tableName: "gateway_service_keys";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: false;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ instanceId: import("drizzle-orm/pg-core").PgColumn<{
57
+ name: "instance_id";
58
+ tableName: "gateway_service_keys";
59
+ dataType: "string";
60
+ columnType: "PgText";
61
+ data: string;
62
+ driverParam: string;
63
+ notNull: true;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: [string, ...string[]];
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
74
+ name: "created_at";
75
+ tableName: "gateway_service_keys";
76
+ dataType: "number";
77
+ columnType: "PgBigInt53";
78
+ data: number;
79
+ driverParam: string | number;
80
+ notNull: true;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: undefined;
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ revokedAt: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "revoked_at";
92
+ tableName: "gateway_service_keys";
93
+ dataType: "number";
94
+ columnType: "PgBigInt53";
95
+ data: number;
96
+ driverParam: string | number;
97
+ notNull: false;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ };
108
+ dialect: "pg";
109
+ }>;
@@ -0,0 +1,18 @@
1
+ import { bigint, index, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
2
+ export const gatewayServiceKeys = pgTable("gateway_service_keys", {
3
+ id: text("id").primaryKey(),
4
+ /** SHA-256 hex digest of the raw service key. Raw key is NEVER stored. */
5
+ keyHash: text("key_hash").notNull(),
6
+ /** Tenant this key bills against. */
7
+ tenantId: text("tenant_id").notNull(),
8
+ /** Instance ID this key was issued for (one key per instance). */
9
+ instanceId: text("instance_id").notNull(),
10
+ /** Unix epoch ms. */
11
+ createdAt: bigint("created_at", { mode: "number" }).notNull(),
12
+ /** Unix epoch ms. Null = not revoked. */
13
+ revokedAt: bigint("revoked_at", { mode: "number" }),
14
+ }, (table) => [
15
+ uniqueIndex("idx_gateway_service_keys_hash").on(table.keyHash),
16
+ index("idx_gateway_service_keys_tenant").on(table.tenantId),
17
+ index("idx_gateway_service_keys_instance").on(table.instanceId),
18
+ ]);
@@ -21,6 +21,7 @@ export * from "./email-notifications.js";
21
21
  export * from "./fleet-event-history.js";
22
22
  export * from "./fleet-events.js";
23
23
  export * from "./gateway-metrics.js";
24
+ export * from "./gateway-service-keys.js";
24
25
  export * from "./gpu-allocations.js";
25
26
  export * from "./gpu-configurations.js";
26
27
  export * from "./gpu-nodes.js";
@@ -21,6 +21,7 @@ export * from "./email-notifications.js";
21
21
  export * from "./fleet-event-history.js";
22
22
  export * from "./fleet-events.js";
23
23
  export * from "./gateway-metrics.js";
24
+ export * from "./gateway-service-keys.js";
24
25
  export * from "./gpu-allocations.js";
25
26
  export * from "./gpu-configurations.js";
26
27
  export * from "./gpu-nodes.js";
@@ -55,7 +55,7 @@ function buildTestConfig(overrides = {}) {
55
55
  }));
56
56
  return {
57
57
  meter: meter,
58
- budgetChecker: budgetChecker,
58
+ budgetChecker,
59
59
  creditLedger,
60
60
  providers: { openrouter: { apiKey: "or-test-key" } },
61
61
  fetchFn,
@@ -16,6 +16,8 @@ export { anthropicToOpenAI, createAnthropicRoutes, createOpenAIRoutes, estimateA
16
16
  export { buildProxyDeps, type ProxyDeps, phoneNumberList, phoneNumberProvision, phoneNumberRelease, smsDeliveryStatus, smsInbound, smsOutbound, } from "./proxy.js";
17
17
  export { createGatewayRoutes } from "./routes.js";
18
18
  export { type GatewayAuthEnv, serviceKeyAuth } from "./service-key-auth.js";
19
+ export type { IServiceKeyRepository } from "./service-key-repository.js";
20
+ export { DrizzleServiceKeyRepository } from "./service-key-repository.js";
19
21
  export { type SpendingCapConfig, type SpendingCaps, spendingCapCheck } from "./spending-cap.js";
20
22
  export type { ISpendingCapStore, SpendingCapRecord } from "./spending-cap-store.js";
21
23
  export { proxySSEStream } from "./streaming.js";
@@ -16,6 +16,7 @@ export { anthropicToOpenAI, createAnthropicRoutes, createOpenAIRoutes, estimateA
16
16
  export { buildProxyDeps, phoneNumberList, phoneNumberProvision, phoneNumberRelease, smsDeliveryStatus, smsInbound, smsOutbound, } from "./proxy.js";
17
17
  export { createGatewayRoutes } from "./routes.js";
18
18
  export { serviceKeyAuth } from "./service-key-auth.js";
19
+ export { DrizzleServiceKeyRepository } from "./service-key-repository.js";
19
20
  export { spendingCapCheck } from "./spending-cap.js";
20
21
  export { proxySSEStream } from "./streaming.js";
21
22
  export { validateTwilioSignature } from "./twilio-signature.js";
@@ -46,7 +46,7 @@ function anthropicAuth(resolveServiceKey) {
46
46
  },
47
47
  }, 401);
48
48
  }
49
- const tenant = resolveServiceKey(key);
49
+ const tenant = await resolveServiceKey(key);
50
50
  if (!tenant) {
51
51
  logger.warn("Invalid service key attempted (anthropic handler)");
52
52
  return c.json({
@@ -7,7 +7,7 @@
7
7
  import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
8
8
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
9
9
  import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
10
- import type { BudgetChecker } from "../../monetization/budget/budget-checker.js";
10
+ import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
11
11
  import type { CapabilityRateLimitConfig } from "../capability-rate-limit.js";
12
12
  import type { CircuitBreakerConfig } from "../circuit-breaker.js";
13
13
  import type { ICircuitBreakerRepository } from "../circuit-breaker-repository.js";
@@ -15,14 +15,14 @@ import type { SellRateLookupFn } from "../rate-lookup.js";
15
15
  import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
16
16
  export interface ProtocolDeps {
17
17
  meter: MeterEmitter;
18
- budgetChecker: BudgetChecker;
18
+ budgetChecker: IBudgetChecker;
19
19
  creditLedger?: ICreditLedger;
20
20
  topUpUrl: string;
21
21
  graceBufferCents?: number;
22
22
  providers: ProviderConfig;
23
23
  defaultMargin: number;
24
24
  fetchFn: FetchFn;
25
- resolveServiceKey: (key: string) => GatewayTenant | null;
25
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
26
26
  /** Apply margin to a cost. Defaults to withMargin from adapters/types. */
27
27
  withMarginFn: (cost: Credit, margin: number) => Credit;
28
28
  rateLookupFn?: SellRateLookupFn;
@@ -53,7 +53,7 @@ function openaiAuth(resolveServiceKey) {
53
53
  },
54
54
  }, 401);
55
55
  }
56
- const tenant = resolveServiceKey(key);
56
+ const tenant = await resolveServiceKey(key);
57
57
  if (!tenant) {
58
58
  logger.warn("Invalid service key attempted (openai handler)", {
59
59
  keyPrefix: `${key.slice(0, 8)}...`,
@@ -12,14 +12,14 @@
12
12
  import type { ICreditLedger } from "@wopr-network/platform-core/credits";
13
13
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
14
14
  import type { Context } from "hono";
15
- import type { BudgetChecker } from "../monetization/budget/budget-checker.js";
15
+ import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
16
16
  import type { SellRateLookupFn } from "./rate-lookup.js";
17
17
  import type { GatewayAuthEnv } from "./service-key-auth.js";
18
18
  import type { FetchFn, GatewayConfig, ProviderConfig } from "./types.js";
19
19
  /** Shared state for all proxy handlers. */
20
20
  export interface ProxyDeps {
21
21
  meter: MeterEmitter;
22
- budgetChecker: BudgetChecker;
22
+ budgetChecker: IBudgetChecker;
23
23
  creditLedger?: ICreditLedger;
24
24
  topUpUrl: string;
25
25
  graceBufferCents?: number;
@@ -51,7 +51,7 @@ function buildTestConfig(overrides = {}) {
51
51
  }));
52
52
  return {
53
53
  meter: meter,
54
- budgetChecker: budgetChecker,
54
+ budgetChecker,
55
55
  creditLedger,
56
56
  providers: { openrouter: { apiKey: "or-test-key" } },
57
57
  fetchFn,
@@ -22,7 +22,7 @@ export interface GatewayAuthEnv {
22
22
  *
23
23
  * @param resolveServiceKey - Function that maps a service key to a tenant (or null)
24
24
  */
25
- export declare function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null): (c: Context<GatewayAuthEnv>, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
25
+ export declare function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>): (c: Context<GatewayAuthEnv>, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
26
26
  error: {
27
27
  message: string;
28
28
  type: string;
@@ -47,7 +47,7 @@ export function serviceKeyAuth(resolveServiceKey) {
47
47
  },
48
48
  }, 401);
49
49
  }
50
- const tenant = resolveServiceKey(serviceKey);
50
+ const tenant = await resolveServiceKey(serviceKey);
51
51
  if (!tenant) {
52
52
  logger.warn("Invalid service key attempted", {
53
53
  keyPrefix: `${serviceKey.slice(0, 8)}...`,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+ import type { PlatformDb } from "../db/index.js";
9
+ import type { GatewayTenant } from "./types.js";
10
+ export interface IServiceKeyRepository {
11
+ /** Generate a new service key for an instance. Returns the raw key (caller must store it). */
12
+ generate(tenantId: string, instanceId: string): Promise<string>;
13
+ /** Resolve a raw bearer token to a GatewayTenant. Returns null if not found or revoked. */
14
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
15
+ /** Revoke the service key for a specific instance. */
16
+ revokeByInstance(instanceId: string): Promise<void>;
17
+ /** Revoke all service keys for a tenant (used when tenant is deleted). */
18
+ revokeByTenant(tenantId: string): Promise<void>;
19
+ }
20
+ export declare class DrizzleServiceKeyRepository implements IServiceKeyRepository {
21
+ private readonly db;
22
+ constructor(db: PlatformDb);
23
+ generate(tenantId: string, instanceId: string): Promise<string>;
24
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
25
+ revokeByInstance(instanceId: string): Promise<void>;
26
+ revokeByTenant(tenantId: string): Promise<void>;
27
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import { and, eq, isNull } from "drizzle-orm";
10
+ import { gatewayServiceKeys } from "../db/schema/gateway-service-keys.js";
11
+ /** Hash a raw key for storage/lookup. */
12
+ function hashKey(raw) {
13
+ return createHash("sha256").update(raw).digest("hex");
14
+ }
15
+ export class DrizzleServiceKeyRepository {
16
+ db;
17
+ constructor(db) {
18
+ this.db = db;
19
+ }
20
+ async generate(tenantId, instanceId) {
21
+ const raw = randomBytes(32).toString("hex");
22
+ const hash = hashKey(raw);
23
+ const id = randomBytes(16).toString("hex");
24
+ await this.db.insert(gatewayServiceKeys).values({
25
+ id,
26
+ keyHash: hash,
27
+ tenantId,
28
+ instanceId,
29
+ createdAt: Date.now(),
30
+ });
31
+ return raw;
32
+ }
33
+ async resolve(rawKey) {
34
+ const hash = hashKey(rawKey);
35
+ const rows = await this.db
36
+ .select({
37
+ tenantId: gatewayServiceKeys.tenantId,
38
+ instanceId: gatewayServiceKeys.instanceId,
39
+ })
40
+ .from(gatewayServiceKeys)
41
+ .where(and(eq(gatewayServiceKeys.keyHash, hash), isNull(gatewayServiceKeys.revokedAt)))
42
+ .limit(1);
43
+ const row = rows[0];
44
+ if (!row)
45
+ return null;
46
+ return {
47
+ id: row.tenantId,
48
+ instanceId: row.instanceId,
49
+ spendLimits: { maxSpendPerHour: null, maxSpendPerMonth: null },
50
+ };
51
+ }
52
+ async revokeByInstance(instanceId) {
53
+ await this.db
54
+ .update(gatewayServiceKeys)
55
+ .set({ revokedAt: Date.now() })
56
+ .where(and(eq(gatewayServiceKeys.instanceId, instanceId), isNull(gatewayServiceKeys.revokedAt)));
57
+ }
58
+ async revokeByTenant(tenantId) {
59
+ await this.db
60
+ .update(gatewayServiceKeys)
61
+ .set({ revokedAt: Date.now() })
62
+ .where(and(eq(gatewayServiceKeys.tenantId, tenantId), isNull(gatewayServiceKeys.revokedAt)));
63
+ }
64
+ }
@@ -8,7 +8,7 @@
8
8
  import type { ICreditLedger } from "@wopr-network/platform-core/credits";
9
9
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  import type { IRateLimitRepository } from "../api/rate-limit-repository.js";
11
- import type { BudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
11
+ import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
12
12
  import type { CapabilityRateLimitConfig } from "./capability-rate-limit.js";
13
13
  import type { CircuitBreakerConfig } from "./circuit-breaker.js";
14
14
  import type { ICircuitBreakerRepository } from "./circuit-breaker-repository.js";
@@ -101,7 +101,7 @@ export interface GatewayConfig {
101
101
  /** MeterEmitter instance for usage tracking */
102
102
  meter: MeterEmitter;
103
103
  /** BudgetChecker instance for pre-call budget validation */
104
- budgetChecker: BudgetChecker;
104
+ budgetChecker: IBudgetChecker;
105
105
  /** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
106
106
  creditLedger?: ICreditLedger;
107
107
  /** URL to direct users to when they need to add credits (default: "/dashboard/credits") */
@@ -119,7 +119,7 @@ export interface GatewayConfig {
119
119
  /** Optional cached rate lookup for model-specific token pricing (WOP-646) */
120
120
  rateLookupFn?: import("./rate-lookup.js").SellRateLookupFn;
121
121
  /** Function to resolve a service key to a tenant */
122
- resolveServiceKey: (key: string) => GatewayTenant | null;
122
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
123
123
  /** Base URL for Twilio webhook signature verification (e.g., https://api.wopr.network/v1). Required for Twilio/Telnyx webhook endpoints. */
124
124
  webhookBaseUrl?: string;
125
125
  /** Resolve a tenant from an inbound webhook request (e.g., from a tenantId URL path param). Required when webhookBaseUrl is set. */
@@ -12,12 +12,12 @@
12
12
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
13
13
  import type { AdapterCapability, ProviderAdapter } from "../adapters/types.js";
14
14
  import type { ArbitrageRouter } from "../arbitrage/router.js";
15
- import type { BudgetChecker, SpendLimits } from "../budget/budget-checker.js";
15
+ import type { IBudgetChecker, SpendLimits } from "../budget/budget-checker.js";
16
16
  export interface SocketConfig {
17
17
  /** MeterEmitter instance for usage tracking */
18
18
  meter: MeterEmitter;
19
- /** BudgetChecker instance for pre-call budget validation */
20
- budgetChecker?: BudgetChecker;
19
+ /** IBudgetChecker instance for pre-call budget validation */
20
+ budgetChecker?: IBudgetChecker;
21
21
  /** Default margin multiplier (default: 1.3) */
22
22
  defaultMargin?: number;
23
23
  /** ArbitrageRouter for cost-optimized routing (GPU-first, cheapest, 5xx failover) */
@@ -0,0 +1,14 @@
1
+ CREATE TABLE "gateway_service_keys" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "key_hash" text NOT NULL,
4
+ "tenant_id" text NOT NULL,
5
+ "instance_id" text NOT NULL,
6
+ "created_at" bigint NOT NULL,
7
+ "revoked_at" bigint
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE UNIQUE INDEX "idx_gateway_service_keys_hash" ON "gateway_service_keys" USING btree ("key_hash");
11
+ --> statement-breakpoint
12
+ CREATE INDEX "idx_gateway_service_keys_tenant" ON "gateway_service_keys" USING btree ("tenant_id");
13
+ --> statement-breakpoint
14
+ CREATE INDEX "idx_gateway_service_keys_instance" ON "gateway_service_keys" USING btree ("instance_id");
@@ -15,6 +15,13 @@
15
15
  "when": 1773279600000,
16
16
  "tag": "0001_infrastructure_extraction",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1741795200000,
23
+ "tag": "0002_gateway_service_keys",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.13.1",
3
+ "version": "1.13.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,7 +2,7 @@ import type { AuditEntry } from "../../admin/audit-log.js";
2
2
 
3
3
  /** Minimal interface for admin audit logging in route factories. */
4
4
  export interface AdminAuditLogger {
5
- log(entry: AuditEntry): void | Promise<void>;
5
+ log(entry: AuditEntry): void | Promise<unknown>;
6
6
  }
7
7
 
8
8
  /** Safely log an admin audit entry — never throws. */
@@ -0,0 +1,23 @@
1
+ import { bigint, index, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
2
+
3
+ export const gatewayServiceKeys = pgTable(
4
+ "gateway_service_keys",
5
+ {
6
+ id: text("id").primaryKey(),
7
+ /** SHA-256 hex digest of the raw service key. Raw key is NEVER stored. */
8
+ keyHash: text("key_hash").notNull(),
9
+ /** Tenant this key bills against. */
10
+ tenantId: text("tenant_id").notNull(),
11
+ /** Instance ID this key was issued for (one key per instance). */
12
+ instanceId: text("instance_id").notNull(),
13
+ /** Unix epoch ms. */
14
+ createdAt: bigint("created_at", { mode: "number" }).notNull(),
15
+ /** Unix epoch ms. Null = not revoked. */
16
+ revokedAt: bigint("revoked_at", { mode: "number" }),
17
+ },
18
+ (table) => [
19
+ uniqueIndex("idx_gateway_service_keys_hash").on(table.keyHash),
20
+ index("idx_gateway_service_keys_tenant").on(table.tenantId),
21
+ index("idx_gateway_service_keys_instance").on(table.instanceId),
22
+ ],
23
+ );
@@ -21,6 +21,7 @@ export * from "./email-notifications.js";
21
21
  export * from "./fleet-event-history.js";
22
22
  export * from "./fleet-events.js";
23
23
  export * from "./gateway-metrics.js";
24
+ export * from "./gateway-service-keys.js";
24
25
  export * from "./gpu-allocations.js";
25
26
  export * from "./gpu-configurations.js";
26
27
  export * from "./gpu-nodes.js";
@@ -69,7 +69,7 @@ function buildTestConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig
69
69
 
70
70
  return {
71
71
  meter: meter as unknown as import("@wopr-network/platform-core/metering").MeterEmitter,
72
- budgetChecker: budgetChecker as unknown as import("../monetization/budget/budget-checker.js").BudgetChecker,
72
+ budgetChecker,
73
73
  creditLedger,
74
74
  providers: { openrouter: { apiKey: "or-test-key" } },
75
75
  fetchFn,
@@ -51,6 +51,8 @@ export {
51
51
  } from "./proxy.js";
52
52
  export { createGatewayRoutes } from "./routes.js";
53
53
  export { type GatewayAuthEnv, serviceKeyAuth } from "./service-key-auth.js";
54
+ export type { IServiceKeyRepository } from "./service-key-repository.js";
55
+ export { DrizzleServiceKeyRepository } from "./service-key-repository.js";
54
56
  export { type SpendingCapConfig, type SpendingCaps, spendingCapCheck } from "./spending-cap.js";
55
57
  export type { ISpendingCapStore, SpendingCapRecord } from "./spending-cap-store.js";
56
58
  export { proxySSEStream } from "./streaming.js";
@@ -46,7 +46,7 @@ function anthropicErrorResponse(status: number, body: AnthropicError): Response
46
46
  // Auth middleware — Anthropic SDK sends x-api-key instead of Authorization
47
47
  // ---------------------------------------------------------------------------
48
48
 
49
- function anthropicAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
49
+ function anthropicAuth(resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>) {
50
50
  return async (c: Context<GatewayAuthEnv>, next: Next) => {
51
51
  // Anthropic SDK uses x-api-key header
52
52
  const apiKey = c.req.header("x-api-key");
@@ -70,7 +70,7 @@ function anthropicAuth(resolveServiceKey: (key: string) => GatewayTenant | null)
70
70
  );
71
71
  }
72
72
 
73
- const tenant = resolveServiceKey(key);
73
+ const tenant = await resolveServiceKey(key);
74
74
  if (!tenant) {
75
75
  logger.warn("Invalid service key attempted (anthropic handler)");
76
76
  return c.json(
@@ -8,7 +8,7 @@
8
8
  import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
9
9
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
11
- import type { BudgetChecker } from "../../monetization/budget/budget-checker.js";
11
+ import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
12
12
  import type { CapabilityRateLimitConfig } from "../capability-rate-limit.js";
13
13
  import type { CircuitBreakerConfig } from "../circuit-breaker.js";
14
14
  import type { ICircuitBreakerRepository } from "../circuit-breaker-repository.js";
@@ -17,14 +17,14 @@ import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
17
17
 
18
18
  export interface ProtocolDeps {
19
19
  meter: MeterEmitter;
20
- budgetChecker: BudgetChecker;
20
+ budgetChecker: IBudgetChecker;
21
21
  creditLedger?: ICreditLedger;
22
22
  topUpUrl: string;
23
23
  graceBufferCents?: number;
24
24
  providers: ProviderConfig;
25
25
  defaultMargin: number;
26
26
  fetchFn: FetchFn;
27
- resolveServiceKey: (key: string) => GatewayTenant | null;
27
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
28
28
  /** Apply margin to a cost. Defaults to withMargin from adapters/types. */
29
29
  withMarginFn: (cost: Credit, margin: number) => Credit;
30
30
  rateLookupFn?: SellRateLookupFn;
@@ -26,7 +26,7 @@ import type { ProtocolDeps } from "./deps.js";
26
26
  // Auth middleware — OpenAI SDK sends Authorization: Bearer
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
29
+ function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>) {
30
30
  return async (c: Context<GatewayAuthEnv>, next: Next) => {
31
31
  const authHeader = c.req.header("Authorization");
32
32
 
@@ -73,7 +73,7 @@ function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
73
73
  );
74
74
  }
75
75
 
76
- const tenant = resolveServiceKey(key);
76
+ const tenant = await resolveServiceKey(key);
77
77
  if (!tenant) {
78
78
  logger.warn("Invalid service key attempted (openai handler)", {
79
79
  keyPrefix: `${key.slice(0, 8)}...`,
@@ -19,7 +19,7 @@ import { logger } from "../config/logger.js";
19
19
  import type { TTSOutput } from "../monetization/adapters/types.js";
20
20
  import { withMargin } from "../monetization/adapters/types.js";
21
21
  import { NoProviderAvailableError } from "../monetization/arbitrage/types.js";
22
- import type { BudgetChecker } from "../monetization/budget/budget-checker.js";
22
+ import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
23
23
  import { PHONE_NUMBER_MONTHLY_COST } from "../monetization/credits/phone-billing.js";
24
24
  import { creditBalanceCheck, debitCredits } from "./credit-gate.js";
25
25
  import { mapBudgetError, mapProviderError } from "./error-mapping.js";
@@ -62,7 +62,7 @@ const smsDeliveryStatusBodySchema = z.object({
62
62
  /** Shared state for all proxy handlers. */
63
63
  export interface ProxyDeps {
64
64
  meter: MeterEmitter;
65
- budgetChecker: BudgetChecker;
65
+ budgetChecker: IBudgetChecker;
66
66
  creditLedger?: ICreditLedger;
67
67
  topUpUrl: string;
68
68
  graceBufferCents?: number;
@@ -65,7 +65,7 @@ function buildTestConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig
65
65
 
66
66
  return {
67
67
  meter: meter as unknown as import("@wopr-network/platform-core/metering").MeterEmitter,
68
- budgetChecker: budgetChecker as unknown as import("../monetization/budget/budget-checker.js").BudgetChecker,
68
+ budgetChecker,
69
69
  creditLedger,
70
70
  providers: { openrouter: { apiKey: "or-test-key" } },
71
71
  fetchFn,
@@ -26,7 +26,9 @@ export interface GatewayAuthEnv {
26
26
  *
27
27
  * @param resolveServiceKey - Function that maps a service key to a tenant (or null)
28
28
  */
29
- export function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
29
+ export function serviceKeyAuth(
30
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>,
31
+ ) {
30
32
  return async (c: Context<GatewayAuthEnv>, next: Next) => {
31
33
  const authHeader = c.req.header("Authorization");
32
34
  if (!authHeader) {
@@ -70,7 +72,7 @@ export function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant
70
72
  );
71
73
  }
72
74
 
73
- const tenant = resolveServiceKey(serviceKey);
75
+ const tenant = await resolveServiceKey(serviceKey);
74
76
  if (!tenant) {
75
77
  logger.warn("Invalid service key attempted", {
76
78
  keyPrefix: `${serviceKey.slice(0, 8)}...`,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+
9
+ import { createHash, randomBytes } from "node:crypto";
10
+ import { and, eq, isNull } from "drizzle-orm";
11
+ import type { PlatformDb } from "../db/index.js";
12
+ import { gatewayServiceKeys } from "../db/schema/gateway-service-keys.js";
13
+ import type { GatewayTenant } from "./types.js";
14
+
15
+ /** Hash a raw key for storage/lookup. */
16
+ function hashKey(raw: string): string {
17
+ return createHash("sha256").update(raw).digest("hex");
18
+ }
19
+
20
+ export interface IServiceKeyRepository {
21
+ /** Generate a new service key for an instance. Returns the raw key (caller must store it). */
22
+ generate(tenantId: string, instanceId: string): Promise<string>;
23
+
24
+ /** Resolve a raw bearer token to a GatewayTenant. Returns null if not found or revoked. */
25
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
26
+
27
+ /** Revoke the service key for a specific instance. */
28
+ revokeByInstance(instanceId: string): Promise<void>;
29
+
30
+ /** Revoke all service keys for a tenant (used when tenant is deleted). */
31
+ revokeByTenant(tenantId: string): Promise<void>;
32
+ }
33
+
34
+ export class DrizzleServiceKeyRepository implements IServiceKeyRepository {
35
+ constructor(private readonly db: PlatformDb) {}
36
+
37
+ async generate(tenantId: string, instanceId: string): Promise<string> {
38
+ const raw = randomBytes(32).toString("hex");
39
+ const hash = hashKey(raw);
40
+ const id = randomBytes(16).toString("hex");
41
+
42
+ await this.db.insert(gatewayServiceKeys).values({
43
+ id,
44
+ keyHash: hash,
45
+ tenantId,
46
+ instanceId,
47
+ createdAt: Date.now(),
48
+ });
49
+
50
+ return raw;
51
+ }
52
+
53
+ async resolve(rawKey: string): Promise<GatewayTenant | null> {
54
+ const hash = hashKey(rawKey);
55
+ const rows = await this.db
56
+ .select({
57
+ tenantId: gatewayServiceKeys.tenantId,
58
+ instanceId: gatewayServiceKeys.instanceId,
59
+ })
60
+ .from(gatewayServiceKeys)
61
+ .where(and(eq(gatewayServiceKeys.keyHash, hash), isNull(gatewayServiceKeys.revokedAt)))
62
+ .limit(1);
63
+
64
+ const row = rows[0];
65
+ if (!row) return null;
66
+
67
+ return {
68
+ id: row.tenantId,
69
+ instanceId: row.instanceId,
70
+ spendLimits: { maxSpendPerHour: null, maxSpendPerMonth: null },
71
+ };
72
+ }
73
+
74
+ async revokeByInstance(instanceId: string): Promise<void> {
75
+ await this.db
76
+ .update(gatewayServiceKeys)
77
+ .set({ revokedAt: Date.now() })
78
+ .where(and(eq(gatewayServiceKeys.instanceId, instanceId), isNull(gatewayServiceKeys.revokedAt)));
79
+ }
80
+
81
+ async revokeByTenant(tenantId: string): Promise<void> {
82
+ await this.db
83
+ .update(gatewayServiceKeys)
84
+ .set({ revokedAt: Date.now() })
85
+ .where(and(eq(gatewayServiceKeys.tenantId, tenantId), isNull(gatewayServiceKeys.revokedAt)));
86
+ }
87
+ }
@@ -9,7 +9,7 @@
9
9
  import type { ICreditLedger } from "@wopr-network/platform-core/credits";
10
10
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
11
11
  import type { IRateLimitRepository } from "../api/rate-limit-repository.js";
12
- import type { BudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
12
+ import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
13
13
  import type { CapabilityRateLimitConfig } from "./capability-rate-limit.js";
14
14
  import type { CircuitBreakerConfig } from "./circuit-breaker.js";
15
15
  import type { ICircuitBreakerRepository } from "./circuit-breaker-repository.js";
@@ -97,7 +97,7 @@ export interface GatewayConfig {
97
97
  /** MeterEmitter instance for usage tracking */
98
98
  meter: MeterEmitter;
99
99
  /** BudgetChecker instance for pre-call budget validation */
100
- budgetChecker: BudgetChecker;
100
+ budgetChecker: IBudgetChecker;
101
101
  /** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
102
102
  creditLedger?: ICreditLedger;
103
103
  /** URL to direct users to when they need to add credits (default: "/dashboard/credits") */
@@ -115,7 +115,7 @@ export interface GatewayConfig {
115
115
  /** Optional cached rate lookup for model-specific token pricing (WOP-646) */
116
116
  rateLookupFn?: import("./rate-lookup.js").SellRateLookupFn;
117
117
  /** Function to resolve a service key to a tenant */
118
- resolveServiceKey: (key: string) => GatewayTenant | null;
118
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
119
119
  /** Base URL for Twilio webhook signature verification (e.g., https://api.wopr.network/v1). Required for Twilio/Telnyx webhook endpoints. */
120
120
  webhookBaseUrl?: string;
121
121
  /** Resolve a tenant from an inbound webhook request (e.g., from a tenantId URL path param). Required when webhookBaseUrl is set. */
@@ -15,13 +15,13 @@ import type { MeterEmitter } from "@wopr-network/platform-core/metering";
15
15
  import type { AdapterCapability, AdapterResult, ProviderAdapter } from "../adapters/types.js";
16
16
  import { withMargin } from "../adapters/types.js";
17
17
  import type { ArbitrageRouter } from "../arbitrage/router.js";
18
- import type { BudgetChecker, SpendLimits } from "../budget/budget-checker.js";
18
+ import type { IBudgetChecker, SpendLimits } from "../budget/budget-checker.js";
19
19
 
20
20
  export interface SocketConfig {
21
21
  /** MeterEmitter instance for usage tracking */
22
22
  meter: MeterEmitter;
23
- /** BudgetChecker instance for pre-call budget validation */
24
- budgetChecker?: BudgetChecker;
23
+ /** IBudgetChecker instance for pre-call budget validation */
24
+ budgetChecker?: IBudgetChecker;
25
25
  /** Default margin multiplier (default: 1.3) */
26
26
  defaultMargin?: number;
27
27
  /** ArbitrageRouter for cost-optimized routing (GPU-first, cheapest, 5xx failover) */
@@ -65,7 +65,7 @@ const CAPABILITY_METHOD: Record<AdapterCapability, keyof ProviderAdapter> = {
65
65
  export class AdapterSocket {
66
66
  private readonly adapters = new Map<string, ProviderAdapter>();
67
67
  private readonly meter: MeterEmitter;
68
- private readonly budgetChecker?: BudgetChecker;
68
+ private readonly budgetChecker?: IBudgetChecker;
69
69
  private readonly defaultMargin: number;
70
70
  private readonly router?: ArbitrageRouter;
71
71