@wopr-network/platform-core 1.13.2 → 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.
- package/dist/db/schema/gateway-service-keys.d.ts +109 -0
- package/dist/db/schema/gateway-service-keys.js +18 -0
- package/dist/db/schema/index.d.ts +1 -0
- package/dist/db/schema/index.js +1 -0
- package/dist/gateway/gateway-routes.test.js +1 -1
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +1 -0
- package/dist/gateway/protocol/anthropic.js +1 -1
- package/dist/gateway/protocol/deps.d.ts +3 -3
- package/dist/gateway/protocol/openai.js +1 -1
- package/dist/gateway/proxy.d.ts +2 -2
- package/dist/gateway/route-mounting.test.js +1 -1
- package/dist/gateway/service-key-auth.d.ts +1 -1
- package/dist/gateway/service-key-auth.js +1 -1
- package/dist/gateway/service-key-repository.d.ts +27 -0
- package/dist/gateway/service-key-repository.js +64 -0
- package/dist/gateway/types.d.ts +3 -3
- package/dist/monetization/socket/socket.d.ts +3 -3
- package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/db/schema/gateway-service-keys.ts +23 -0
- package/src/db/schema/index.ts +1 -0
- package/src/gateway/gateway-routes.test.ts +1 -1
- package/src/gateway/index.ts +2 -0
- package/src/gateway/protocol/anthropic.ts +2 -2
- package/src/gateway/protocol/deps.ts +3 -3
- package/src/gateway/protocol/openai.ts +2 -2
- package/src/gateway/proxy.ts +2 -2
- package/src/gateway/route-mounting.test.ts +1 -1
- package/src/gateway/service-key-auth.ts +4 -2
- package/src/gateway/service-key-repository.ts +87 -0
- package/src/gateway/types.ts +3 -3
- package/src/monetization/socket/socket.ts +4 -4
|
@@ -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";
|
package/dist/db/schema/index.js
CHANGED
|
@@ -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";
|
package/dist/gateway/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/gateway/index.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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)}...`,
|
package/dist/gateway/proxy.d.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
22
|
+
budgetChecker: IBudgetChecker;
|
|
23
23
|
creditLedger?: ICreditLedger;
|
|
24
24
|
topUpUrl: string;
|
|
25
25
|
graceBufferCents?: number;
|
|
@@ -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
|
+
}
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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 {
|
|
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
|
-
/**
|
|
20
|
-
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");
|
package/package.json
CHANGED
|
@@ -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
|
+
);
|
package/src/db/schema/index.ts
CHANGED
|
@@ -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
|
|
72
|
+
budgetChecker,
|
|
73
73
|
creditLedger,
|
|
74
74
|
providers: { openrouter: { apiKey: "or-test-key" } },
|
|
75
75
|
fetchFn,
|
package/src/gateway/index.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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)}...`,
|
package/src/gateway/proxy.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/src/gateway/types.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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 {
|
|
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
|
-
/**
|
|
24
|
-
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?:
|
|
68
|
+
private readonly budgetChecker?: IBudgetChecker;
|
|
69
69
|
private readonly defaultMargin: number;
|
|
70
70
|
private readonly router?: ArbitrageRouter;
|
|
71
71
|
|