@wopr-network/platform-core 1.39.7 → 1.42.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.
@@ -4,7 +4,7 @@ export const tenants = pgTable("tenants", {
4
4
  id: text("id").primaryKey(), // nanoid or crypto.randomUUID()
5
5
  name: text("name").notNull(),
6
6
  slug: text("slug").unique(),
7
- type: text("type").notNull(), // "personal" | "org"
7
+ type: text("type").notNull(), // "personal" | "org" | "platform_service"
8
8
  ownerId: text("owner_id").notNull(), // user who created it
9
9
  billingEmail: text("billing_email"),
10
10
  createdAt: bigint("created_at", { mode: "number" })
@@ -14,5 +14,5 @@ export const tenants = pgTable("tenants", {
14
14
  index("idx_tenants_slug").on(table.slug),
15
15
  index("idx_tenants_owner").on(table.ownerId),
16
16
  index("idx_tenants_type").on(table.type),
17
- check("chk_tenants_type", sql `${table.type} IN ('personal', 'org')`),
17
+ check("chk_tenants_type", sql `${table.type} IN ('personal', 'org', 'platform_service')`),
18
18
  ]);
@@ -0,0 +1,6 @@
1
+ import type { DrizzleDb } from "./index.js";
2
+ export interface PlatformServiceSeedResult {
3
+ tenantId: string;
4
+ serviceKey: string | null;
5
+ }
6
+ export declare function seedPlatformServiceTenant(db: DrizzleDb): Promise<PlatformServiceSeedResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Idempotent seed: creates the holyship-platform internal billing tenant
3
+ * with a stable service key for platform-to-gateway LLM billing.
4
+ *
5
+ * Safe to call on every boot — skips if the tenant already exists.
6
+ */
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { eq } from "drizzle-orm";
9
+ import { gatewayServiceKeys } from "./schema/gateway-service-keys.js";
10
+ import { tenants } from "./schema/tenants.js";
11
+ const PLATFORM_TENANT_ID = "holyship-platform";
12
+ const PLATFORM_TENANT_SLUG = "holyship-platform";
13
+ const PLATFORM_INSTANCE_ID = "holyship-platform-internal";
14
+ export async function seedPlatformServiceTenant(db) {
15
+ // Check if tenant already exists
16
+ const existing = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.id, PLATFORM_TENANT_ID)).limit(1);
17
+ if (existing.length > 0) {
18
+ return { tenantId: PLATFORM_TENANT_ID, serviceKey: null };
19
+ }
20
+ // Create the platform_service tenant
21
+ await db.insert(tenants).values({
22
+ id: PLATFORM_TENANT_ID,
23
+ name: "Holy Ship Platform",
24
+ slug: PLATFORM_TENANT_SLUG,
25
+ type: "platform_service",
26
+ ownerId: "system",
27
+ billingEmail: null,
28
+ createdAt: Date.now(),
29
+ });
30
+ // Generate a service key and store its hash
31
+ const rawKey = `sk-hs-${randomBytes(24).toString("hex")}`;
32
+ const keyHash = createHash("sha256").update(rawKey).digest("hex");
33
+ await db.insert(gatewayServiceKeys).values({
34
+ id: crypto.randomUUID(),
35
+ keyHash,
36
+ tenantId: PLATFORM_TENANT_ID,
37
+ instanceId: PLATFORM_INSTANCE_ID,
38
+ createdAt: Date.now(),
39
+ revokedAt: null,
40
+ });
41
+ return { tenantId: PLATFORM_TENANT_ID, serviceKey: rawKey };
42
+ }
@@ -63,7 +63,8 @@ export function capabilityRateLimit(config, repo) {
63
63
  return next();
64
64
  }
65
65
  const tenant = c.get("gatewayTenant");
66
- const tenantId = tenant?.id ?? "unknown";
66
+ const attributedTenantId = c.get("attributedTenantId");
67
+ const tenantId = attributedTenantId ?? tenant?.id ?? "unknown";
67
68
  const max = limits[category];
68
69
  const scope = `cap:${category}`;
69
70
  const now = Date.now();
@@ -43,4 +43,4 @@ export declare function creditBalanceCheck(c: Context<GatewayAuthEnv>, deps: Cre
43
43
  /**
44
44
  * Post-call credit debit. Fire-and-forget — never fails the response.
45
45
  */
46
- export declare function debitCredits(deps: CreditGateDeps, tenantId: string, costUsd: number, margin: number, capability: string, provider: string, attributedUserId?: string): Promise<void>;
46
+ export declare function debitCredits(deps: CreditGateDeps, tenantId: string, costUsd: number, margin: number, capability: string, provider: string, attributedUserId?: string, attributedTenantId?: string | null): Promise<void>;
@@ -21,6 +21,11 @@ export async function creditBalanceCheck(c, deps, estimatedCostCents = 0) {
21
21
  if (!deps.creditLedger)
22
22
  return null;
23
23
  const tenant = c.get("gatewayTenant");
24
+ // Platform service accounts bypass the pre-call balance gate.
25
+ // The company never 402s itself. Debit still runs post-call for P&L tracking.
26
+ if (tenant.type === "platform_service") {
27
+ return null;
28
+ }
24
29
  const balance = await deps.creditLedger.balance(tenant.id);
25
30
  const required = Math.max(0, estimatedCostCents);
26
31
  const graceBuffer = deps.graceBufferCents ?? 50; // default -$0.50
@@ -55,7 +60,7 @@ export async function creditBalanceCheck(c, deps, estimatedCostCents = 0) {
55
60
  /**
56
61
  * Post-call credit debit. Fire-and-forget — never fails the response.
57
62
  */
58
- export async function debitCredits(deps, tenantId, costUsd, margin, capability, provider, attributedUserId) {
63
+ export async function debitCredits(deps, tenantId, costUsd, margin, capability, provider, attributedUserId, attributedTenantId) {
59
64
  if (!deps.creditLedger)
60
65
  return;
61
66
  const chargeCredit = withMargin(Credit.fromDollars(costUsd), margin);
@@ -75,6 +80,7 @@ export async function debitCredits(deps, tenantId, costUsd, margin, capability,
75
80
  description: `Gateway ${capability} via ${provider}`,
76
81
  allowNegative: true,
77
82
  attributedUserId,
83
+ ...(attributedTenantId ? { attributed_tenant_id: attributedTenantId } : {}),
78
84
  });
79
85
  // Only fire on first zero-crossing (balance was positive before, now ≤ 0)
80
86
  if (deps.onBalanceExhausted) {
@@ -96,6 +96,7 @@ export function createAnthropicRoutes(deps) {
96
96
  function messagesHandler(deps) {
97
97
  return async (c) => {
98
98
  const tenant = c.get("gatewayTenant");
99
+ const attributedTenantId = c.get("attributedTenantId");
99
100
  // Budget check
100
101
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
101
102
  if (!budgetResult.allowed) {
@@ -174,7 +175,7 @@ function messagesHandler(deps) {
174
175
  provider: "openrouter",
175
176
  timestamp: Date.now(),
176
177
  });
177
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
178
+ debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
178
179
  }
179
180
  return new Response(res.body, {
180
181
  status: res.status,
@@ -109,6 +109,7 @@ export function createOpenAIRoutes(deps) {
109
109
  function chatCompletionsHandler(deps) {
110
110
  return async (c) => {
111
111
  const tenant = c.get("gatewayTenant");
112
+ const attributedTenantId = c.get("attributedTenantId");
112
113
  // Budget check
113
114
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
114
115
  if (!budgetResult.allowed) {
@@ -187,7 +188,7 @@ function chatCompletionsHandler(deps) {
187
188
  provider: "openrouter",
188
189
  timestamp: Date.now(),
189
190
  });
190
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
191
+ debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
191
192
  }
192
193
  return new Response(res.body, {
193
194
  status: res.status,
@@ -219,7 +220,7 @@ function chatCompletionsHandler(deps) {
219
220
  provider: "openrouter",
220
221
  timestamp: Date.now(),
221
222
  });
222
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
223
+ debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter", undefined, attributedTenantId);
223
224
  }
224
225
  return new Response(responseBody, {
225
226
  status: res.status,
@@ -246,6 +247,7 @@ function chatCompletionsHandler(deps) {
246
247
  function embeddingsHandler(deps) {
247
248
  return async (c) => {
248
249
  const tenant = c.get("gatewayTenant");
250
+ const attributedTenantId = c.get("attributedTenantId");
249
251
  // Budget check
250
252
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
251
253
  if (!budgetResult.allowed) {
@@ -311,7 +313,7 @@ function embeddingsHandler(deps) {
311
313
  provider: "openrouter",
312
314
  timestamp: Date.now(),
313
315
  });
314
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "embeddings", "openrouter");
316
+ debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "embeddings", "openrouter", undefined, attributedTenantId);
315
317
  }
316
318
  return new Response(responseBody, {
317
319
  status: res.status,
@@ -11,6 +11,7 @@ export interface GatewayAuthEnv {
11
11
  Variables: {
12
12
  gatewayTenant: GatewayTenant;
13
13
  webhookBody: Record<string, unknown>;
14
+ attributedTenantId: string | null;
14
15
  };
15
16
  }
16
17
  /**
@@ -61,6 +61,8 @@ export function serviceKeyAuth(resolveServiceKey) {
61
61
  }, 401);
62
62
  }
63
63
  c.set("gatewayTenant", tenant);
64
+ const attributedTenantId = c.req.header("x-attribute-to") ?? null;
65
+ c.set("attributedTenantId", attributedTenantId);
64
66
  return next();
65
67
  };
66
68
  }
@@ -34,6 +34,8 @@ export interface GatewayEndpoint {
34
34
  export interface GatewayTenant {
35
35
  /** Tenant ID */
36
36
  id: string;
37
+ /** Tenant type — platform_service accounts bypass the pre-call credit gate */
38
+ type?: "personal" | "org" | "platform_service";
37
39
  /** Spend limits for budget checking */
38
40
  spendLimits: SpendLimits;
39
41
  /** Plan tier for rate limit lookup */
@@ -0,0 +1,3 @@
1
+ ALTER TABLE "tenants" DROP CONSTRAINT "chk_tenants_type";
2
+ --> statement-breakpoint
3
+ ALTER TABLE "tenants" ADD CONSTRAINT "chk_tenants_type" CHECK ("tenants"."type" IN ('personal', 'org', 'platform_service'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.39.7",
3
+ "version": "1.42.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -7,7 +7,7 @@ export const tenants = pgTable(
7
7
  id: text("id").primaryKey(), // nanoid or crypto.randomUUID()
8
8
  name: text("name").notNull(),
9
9
  slug: text("slug").unique(),
10
- type: text("type").notNull(), // "personal" | "org"
10
+ type: text("type").notNull(), // "personal" | "org" | "platform_service"
11
11
  ownerId: text("owner_id").notNull(), // user who created it
12
12
  billingEmail: text("billing_email"),
13
13
  createdAt: bigint("created_at", { mode: "number" })
@@ -18,6 +18,6 @@ export const tenants = pgTable(
18
18
  index("idx_tenants_slug").on(table.slug),
19
19
  index("idx_tenants_owner").on(table.ownerId),
20
20
  index("idx_tenants_type").on(table.type),
21
- check("chk_tenants_type", sql`${table.type} IN ('personal', 'org')`),
21
+ check("chk_tenants_type", sql`${table.type} IN ('personal', 'org', 'platform_service')`),
22
22
  ],
23
23
  );
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Idempotent seed: creates the holyship-platform internal billing tenant
3
+ * with a stable service key for platform-to-gateway LLM billing.
4
+ *
5
+ * Safe to call on every boot — skips if the tenant already exists.
6
+ */
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { eq } from "drizzle-orm";
9
+ import type { DrizzleDb } from "./index.js";
10
+ import { gatewayServiceKeys } from "./schema/gateway-service-keys.js";
11
+ import { tenants } from "./schema/tenants.js";
12
+
13
+ const PLATFORM_TENANT_ID = "holyship-platform";
14
+ const PLATFORM_TENANT_SLUG = "holyship-platform";
15
+ const PLATFORM_INSTANCE_ID = "holyship-platform-internal";
16
+
17
+ export interface PlatformServiceSeedResult {
18
+ tenantId: string;
19
+ serviceKey: string | null; // null if tenant + key already existed
20
+ }
21
+
22
+ export async function seedPlatformServiceTenant(db: DrizzleDb): Promise<PlatformServiceSeedResult> {
23
+ // Check if tenant already exists
24
+ const existing = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.id, PLATFORM_TENANT_ID)).limit(1);
25
+
26
+ if (existing.length > 0) {
27
+ return { tenantId: PLATFORM_TENANT_ID, serviceKey: null };
28
+ }
29
+
30
+ // Create the platform_service tenant
31
+ await db.insert(tenants).values({
32
+ id: PLATFORM_TENANT_ID,
33
+ name: "Holy Ship Platform",
34
+ slug: PLATFORM_TENANT_SLUG,
35
+ type: "platform_service",
36
+ ownerId: "system",
37
+ billingEmail: null,
38
+ createdAt: Date.now(),
39
+ });
40
+
41
+ // Generate a service key and store its hash
42
+ const rawKey = `sk-hs-${randomBytes(24).toString("hex")}`;
43
+ const keyHash = createHash("sha256").update(rawKey).digest("hex");
44
+
45
+ await db.insert(gatewayServiceKeys).values({
46
+ id: crypto.randomUUID(),
47
+ keyHash,
48
+ tenantId: PLATFORM_TENANT_ID,
49
+ instanceId: PLATFORM_INSTANCE_ID,
50
+ createdAt: Date.now(),
51
+ revokedAt: null,
52
+ });
53
+
54
+ return { tenantId: PLATFORM_TENANT_ID, serviceKey: rawKey };
55
+ }
@@ -94,7 +94,8 @@ export function capabilityRateLimit(
94
94
  }
95
95
 
96
96
  const tenant = c.get("gatewayTenant") as GatewayTenant | undefined;
97
- const tenantId = tenant?.id ?? "unknown";
97
+ const attributedTenantId = c.get("attributedTenantId") as string | null | undefined;
98
+ const tenantId = attributedTenantId ?? tenant?.id ?? "unknown";
98
99
  const max = limits[category];
99
100
  const scope = `cap:${category}`;
100
101
  const now = Date.now();
@@ -54,6 +54,13 @@ export async function creditBalanceCheck(
54
54
  if (!deps.creditLedger) return null;
55
55
 
56
56
  const tenant = c.get("gatewayTenant");
57
+
58
+ // Platform service accounts bypass the pre-call balance gate.
59
+ // The company never 402s itself. Debit still runs post-call for P&L tracking.
60
+ if (tenant.type === "platform_service") {
61
+ return null;
62
+ }
63
+
57
64
  const balance = await deps.creditLedger.balance(tenant.id);
58
65
  const required = Math.max(0, estimatedCostCents);
59
66
  const graceBuffer = deps.graceBufferCents ?? 50; // default -$0.50
@@ -100,6 +107,7 @@ export async function debitCredits(
100
107
  capability: string,
101
108
  provider: string,
102
109
  attributedUserId?: string,
110
+ attributedTenantId?: string | null,
103
111
  ): Promise<void> {
104
112
  if (!deps.creditLedger) return;
105
113
 
@@ -122,6 +130,7 @@ export async function debitCredits(
122
130
  description: `Gateway ${capability} via ${provider}`,
123
131
  allowNegative: true,
124
132
  attributedUserId,
133
+ ...(attributedTenantId ? { attributed_tenant_id: attributedTenantId } : {}),
125
134
  });
126
135
 
127
136
  // Only fire on first zero-crossing (balance was positive before, now ≤ 0)
@@ -136,6 +136,7 @@ export function createAnthropicRoutes(deps: ProtocolDeps): Hono<GatewayAuthEnv>
136
136
  function messagesHandler(deps: ProtocolDeps) {
137
137
  return async (c: Context<GatewayAuthEnv>) => {
138
138
  const tenant = c.get("gatewayTenant");
139
+ const attributedTenantId = c.get("attributedTenantId");
139
140
 
140
141
  // Budget check
141
142
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
@@ -224,7 +225,16 @@ function messagesHandler(deps: ProtocolDeps) {
224
225
  provider: "openrouter",
225
226
  timestamp: Date.now(),
226
227
  });
227
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
228
+ debitCredits(
229
+ deps,
230
+ tenant.id,
231
+ costUsd,
232
+ deps.defaultMargin,
233
+ "chat-completions",
234
+ "openrouter",
235
+ undefined,
236
+ attributedTenantId,
237
+ );
228
238
  }
229
239
 
230
240
  return new Response(res.body, {
@@ -146,6 +146,7 @@ export function createOpenAIRoutes(deps: ProtocolDeps): Hono<GatewayAuthEnv> {
146
146
  function chatCompletionsHandler(deps: ProtocolDeps) {
147
147
  return async (c: Context<GatewayAuthEnv>) => {
148
148
  const tenant = c.get("gatewayTenant");
149
+ const attributedTenantId = c.get("attributedTenantId");
149
150
 
150
151
  // Budget check
151
152
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
@@ -241,7 +242,16 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
241
242
  provider: "openrouter",
242
243
  timestamp: Date.now(),
243
244
  });
244
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
245
+ debitCredits(
246
+ deps,
247
+ tenant.id,
248
+ costUsd,
249
+ deps.defaultMargin,
250
+ "chat-completions",
251
+ "openrouter",
252
+ undefined,
253
+ attributedTenantId,
254
+ );
245
255
  }
246
256
 
247
257
  return new Response(res.body, {
@@ -277,7 +287,16 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
277
287
  provider: "openrouter",
278
288
  timestamp: Date.now(),
279
289
  });
280
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "chat-completions", "openrouter");
290
+ debitCredits(
291
+ deps,
292
+ tenant.id,
293
+ costUsd,
294
+ deps.defaultMargin,
295
+ "chat-completions",
296
+ "openrouter",
297
+ undefined,
298
+ attributedTenantId,
299
+ );
281
300
  }
282
301
 
283
302
  return new Response(responseBody, {
@@ -309,6 +328,7 @@ function chatCompletionsHandler(deps: ProtocolDeps) {
309
328
  function embeddingsHandler(deps: ProtocolDeps) {
310
329
  return async (c: Context<GatewayAuthEnv>) => {
311
330
  const tenant = c.get("gatewayTenant");
331
+ const attributedTenantId = c.get("attributedTenantId");
312
332
 
313
333
  // Budget check
314
334
  const budgetResult = await deps.budgetChecker.check(tenant.id, tenant.spendLimits);
@@ -391,7 +411,16 @@ function embeddingsHandler(deps: ProtocolDeps) {
391
411
  provider: "openrouter",
392
412
  timestamp: Date.now(),
393
413
  });
394
- debitCredits(deps, tenant.id, costUsd, deps.defaultMargin, "embeddings", "openrouter");
414
+ debitCredits(
415
+ deps,
416
+ tenant.id,
417
+ costUsd,
418
+ deps.defaultMargin,
419
+ "embeddings",
420
+ "openrouter",
421
+ undefined,
422
+ attributedTenantId,
423
+ );
395
424
  }
396
425
 
397
426
  return new Response(responseBody, {
@@ -14,6 +14,7 @@ export interface GatewayAuthEnv {
14
14
  Variables: {
15
15
  gatewayTenant: GatewayTenant;
16
16
  webhookBody: Record<string, unknown>;
17
+ attributedTenantId: string | null;
17
18
  };
18
19
  }
19
20
 
@@ -90,6 +91,8 @@ export function serviceKeyAuth(
90
91
  }
91
92
 
92
93
  c.set("gatewayTenant", tenant);
94
+ const attributedTenantId = c.req.header("x-attribute-to") ?? null;
95
+ c.set("attributedTenantId", attributedTenantId);
93
96
  return next();
94
97
  };
95
98
  }
@@ -47,6 +47,8 @@ export interface GatewayEndpoint {
47
47
  export interface GatewayTenant {
48
48
  /** Tenant ID */
49
49
  id: string;
50
+ /** Tenant type — platform_service accounts bypass the pre-call credit gate */
51
+ type?: "personal" | "org" | "platform_service";
50
52
  /** Spend limits for budget checking */
51
53
  spendLimits: SpendLimits;
52
54
  /** Plan tier for rate limit lookup */