@secondlayer/shared 2.1.0 → 3.0.0-beta.1
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/README.md +2 -2
- package/dist/src/crypto/secrets.js +47 -3
- package/dist/src/crypto/secrets.js.map +5 -4
- package/dist/src/db/index.d.ts +112 -137
- package/dist/src/db/index.js.map +2 -2
- package/dist/src/db/jsonb.d.ts +5 -1
- package/dist/src/db/jsonb.js.map +2 -2
- package/dist/src/db/queries/account-spend-caps.d.ts +444 -0
- package/dist/src/db/queries/account-spend-caps.js +60 -0
- package/dist/src/db/queries/account-spend-caps.js.map +10 -0
- package/dist/src/db/queries/account-usage.d.ts +468 -0
- package/dist/src/db/queries/account-usage.js +222 -0
- package/dist/src/db/queries/account-usage.js.map +11 -0
- package/dist/src/db/queries/accounts.d.ts +100 -109
- package/dist/src/db/queries/accounts.js +15 -1
- package/dist/src/db/queries/accounts.js.map +3 -3
- package/dist/src/db/queries/integrity.d.ts +85 -107
- package/dist/src/db/queries/projects.d.ts +87 -109
- package/dist/src/db/queries/provisioning-audit.d.ts +85 -107
- package/dist/src/db/queries/subgraph-gaps.d.ts +85 -107
- package/dist/src/db/queries/subgraphs.d.ts +86 -109
- package/dist/src/db/queries/subgraphs.js +2 -3
- package/dist/src/db/queries/subgraphs.js.map +4 -4
- package/dist/src/db/queries/{workflows.d.ts → tenant-compute-addons.d.ts} +108 -142
- package/dist/src/db/queries/tenant-compute-addons.js +47 -0
- package/dist/src/db/queries/tenant-compute-addons.js.map +10 -0
- package/dist/src/db/queries/tenants.d.ts +98 -110
- package/dist/src/db/queries/tenants.js +55 -8
- package/dist/src/db/queries/tenants.js.map +6 -5
- package/dist/src/db/queries/usage.d.ts +86 -132
- package/dist/src/db/queries/usage.js +5 -64
- package/dist/src/db/queries/usage.js.map +4 -5
- package/dist/src/db/schema.d.ts +107 -136
- package/dist/src/errors.d.ts +8 -7
- package/dist/src/errors.js +11 -12
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +119 -143
- package/dist/src/index.js +11 -12
- package/dist/src/index.js.map +4 -4
- package/dist/src/node/local-client.d.ts +85 -107
- package/dist/src/pricing.d.ts +20 -1
- package/dist/src/pricing.js +58 -1
- package/dist/src/pricing.js.map +3 -3
- package/migrations/0045_drop_marketplace_columns.ts +47 -0
- package/migrations/0046_tenant_activity_signal.ts +47 -0
- package/migrations/0047_usage_daily_tenant_id.ts +73 -0
- package/migrations/0048_tenant_compute_addons.ts +49 -0
- package/migrations/0049_accounts_stripe_customer_id.ts +30 -0
- package/migrations/0050_account_spend_caps.ts +45 -0
- package/migrations/0051_workflow_ai_usage_daily.ts +40 -0
- package/migrations/0052_sentries.ts +61 -0
- package/migrations/0053_workflow_runtime.ts +88 -0
- package/migrations/0054_accounts_plan_hobby.ts +32 -0
- package/migrations/0055_ai_usage_account_scope.ts +108 -0
- package/migrations/0056_drop_workflow_sentry_residuals.ts +23 -0
- package/migrations/0057_subscriptions.ts +137 -0
- package/package.json +26 -14
- package/dist/src/db/queries/workflows.js +0 -260
- package/dist/src/db/queries/workflows.js.map +0 -12
- package/dist/src/lib/plans.d.ts +0 -9
- package/dist/src/lib/plans.js +0 -37
- package/dist/src/lib/plans.js.map +0 -10
- package/dist/src/schemas/workflows.d.ts +0 -70
- package/dist/src/schemas/workflows.js +0 -43
- package/dist/src/schemas/workflows.js.map +0 -10
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/db/queries/tenant-compute-addons.ts
|
|
18
|
+
import { sql } from "kysely";
|
|
19
|
+
async function listActiveAddonsForTenant(db, tenantId, now = new Date) {
|
|
20
|
+
return db.selectFrom("tenant_compute_addons").selectAll().where("tenant_id", "=", tenantId).where("effective_from", "<=", now).where((eb) => eb.or([
|
|
21
|
+
eb("effective_until", "is", null),
|
|
22
|
+
eb("effective_until", ">", now)
|
|
23
|
+
])).execute();
|
|
24
|
+
}
|
|
25
|
+
async function computeEffectiveCompute(db, tenantId, base, now = new Date) {
|
|
26
|
+
const row = await db.selectFrom("tenant_compute_addons").select([
|
|
27
|
+
sql`coalesce(sum(memory_mb_delta), 0)`.as("mem_delta"),
|
|
28
|
+
sql`coalesce(sum(cpu_delta), 0)`.as("cpu_delta"),
|
|
29
|
+
sql`coalesce(sum(storage_mb_delta), 0)`.as("stor_delta")
|
|
30
|
+
]).where("tenant_id", "=", tenantId).where("effective_from", "<=", now).where((eb) => eb.or([
|
|
31
|
+
eb("effective_until", "is", null),
|
|
32
|
+
eb("effective_until", ">", now)
|
|
33
|
+
])).executeTakeFirst();
|
|
34
|
+
if (!row)
|
|
35
|
+
return base;
|
|
36
|
+
const cpus = base.cpus + Number(row.cpu_delta ?? 0);
|
|
37
|
+
const memoryMb = base.memoryMb + Number(row.mem_delta ?? 0);
|
|
38
|
+
const storageLimitMb = base.storageLimitMb === -1 ? -1 : base.storageLimitMb + Number(row.stor_delta ?? 0);
|
|
39
|
+
return { cpus, memoryMb, storageLimitMb };
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
listActiveAddonsForTenant,
|
|
43
|
+
computeEffectiveCompute
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//# debugId=70D8508BE398EADC64756E2164756E21
|
|
47
|
+
//# sourceMappingURL=tenant-compute-addons.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/db/queries/tenant-compute-addons.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { type Kysely, sql } from \"kysely\";\nimport type { Database, TenantComputeAddon } from \"../types.ts\";\n\n/**\n * Compute add-ons — extras on top of a plan's base spec.\n *\n * Effective compute is NEVER derived from just the `tenants.plan`\n * column — always run `computeEffectiveCompute(tenantId, planBase)`\n * to fold in active add-ons. Provisioning, resize, and Stripe metering\n * all share this source of truth.\n */\n\n/** Active = open-ended (effective_until IS NULL) OR not yet expired. */\nexport async function listActiveAddonsForTenant(\n\tdb: Kysely<Database>,\n\ttenantId: string,\n\tnow: Date = new Date(),\n): Promise<TenantComputeAddon[]> {\n\treturn db\n\t\t.selectFrom(\"tenant_compute_addons\")\n\t\t.selectAll()\n\t\t.where(\"tenant_id\", \"=\", tenantId)\n\t\t.where(\"effective_from\", \"<=\", now)\n\t\t.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"effective_until\", \"is\", null),\n\t\t\t\teb(\"effective_until\", \">\", now),\n\t\t\t]),\n\t\t)\n\t\t.execute();\n}\n\nexport interface ComputeSpec {\n\tcpus: number;\n\tmemoryMb: number;\n\tstorageLimitMb: number;\n}\n\n/**\n * Apply active add-ons on top of a base spec. `storageLimitMb` of -1\n * (enterprise unlimited) passes through unchanged — add-ons don't\n * further modify unlimited storage.\n */\nexport async function computeEffectiveCompute(\n\tdb: Kysely<Database>,\n\ttenantId: string,\n\tbase: ComputeSpec,\n\tnow: Date = new Date(),\n): Promise<ComputeSpec> {\n\tconst row = await db\n\t\t.selectFrom(\"tenant_compute_addons\")\n\t\t.select([\n\t\t\tsql<number>`coalesce(sum(memory_mb_delta), 0)`.as(\"mem_delta\"),\n\t\t\tsql<string>`coalesce(sum(cpu_delta), 0)`.as(\"cpu_delta\"),\n\t\t\tsql<number>`coalesce(sum(storage_mb_delta), 0)`.as(\"stor_delta\"),\n\t\t])\n\t\t.where(\"tenant_id\", \"=\", tenantId)\n\t\t.where(\"effective_from\", \"<=\", now)\n\t\t.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"effective_until\", \"is\", null),\n\t\t\t\teb(\"effective_until\", \">\", now),\n\t\t\t]),\n\t\t)\n\t\t.executeTakeFirst();\n\n\tif (!row) return base;\n\n\tconst cpus = base.cpus + Number(row.cpu_delta ?? 0);\n\tconst memoryMb = base.memoryMb + Number(row.mem_delta ?? 0);\n\tconst storageLimitMb =\n\t\tbase.storageLimitMb === -1\n\t\t\t? -1\n\t\t\t: base.storageLimitMb + Number(row.stor_delta ?? 0);\n\n\treturn { cpus, memoryMb, storageLimitMb };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAaA,eAAsB,yBAAyB,CAC9C,IACA,UACA,MAAY,IAAI,MACgB;AAAA,EAChC,OAAO,GACL,WAAW,uBAAuB,EAClC,UAAU,EACV,MAAM,aAAa,KAAK,QAAQ,EAChC,MAAM,kBAAkB,MAAM,GAAG,EACjC,MAAM,CAAC,OACP,GAAG,GAAG;AAAA,IACL,GAAG,mBAAmB,MAAM,IAAI;AAAA,IAChC,GAAG,mBAAmB,KAAK,GAAG;AAAA,EAC/B,CAAC,CACF,EACC,QAAQ;AAAA;AAcX,eAAsB,uBAAuB,CAC5C,IACA,UACA,MACA,MAAY,IAAI,MACO;AAAA,EACvB,MAAM,MAAM,MAAM,GAChB,WAAW,uBAAuB,EAClC,OAAO;AAAA,IACP,uCAA+C,GAAG,WAAW;AAAA,IAC7D,iCAAyC,GAAG,WAAW;AAAA,IACvD,wCAAgD,GAAG,YAAY;AAAA,EAChE,CAAC,EACA,MAAM,aAAa,KAAK,QAAQ,EAChC,MAAM,kBAAkB,MAAM,GAAG,EACjC,MAAM,CAAC,OACP,GAAG,GAAG;AAAA,IACL,GAAG,mBAAmB,MAAM,IAAI;AAAA,IAChC,GAAG,mBAAmB,KAAK,GAAG;AAAA,EAC/B,CAAC,CACF,EACC,iBAAiB;AAAA,EAEnB,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EAEjB,MAAM,OAAO,KAAK,OAAO,OAAO,IAAI,aAAa,CAAC;AAAA,EAClD,MAAM,WAAW,KAAK,WAAW,OAAO,IAAI,aAAa,CAAC;AAAA,EAC1D,MAAM,iBACL,KAAK,mBAAmB,KACrB,KACA,KAAK,iBAAiB,OAAO,IAAI,cAAc,CAAC;AAAA,EAEpD,OAAO,EAAE,MAAM,UAAU,eAAe;AAAA;",
|
|
8
|
+
"debugId": "70D8508BE398EADC64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -60,10 +60,6 @@ interface SubgraphsTable {
|
|
|
60
60
|
handler_code: string | null;
|
|
61
61
|
source_code: string | null;
|
|
62
62
|
project_id: string | null;
|
|
63
|
-
is_public: Generated<boolean>;
|
|
64
|
-
tags: Generated<string[]>;
|
|
65
|
-
description: string | null;
|
|
66
|
-
forked_from_id: string | null;
|
|
67
63
|
created_at: Generated<Date>;
|
|
68
64
|
updated_at: Generated<Date>;
|
|
69
65
|
}
|
|
@@ -98,6 +94,7 @@ interface AccountsTable {
|
|
|
98
94
|
bio: string | null;
|
|
99
95
|
avatar_url: string | null;
|
|
100
96
|
slug: string | null;
|
|
97
|
+
stripe_customer_id: string | null;
|
|
101
98
|
created_at: Generated<Date>;
|
|
102
99
|
}
|
|
103
100
|
interface SessionsTable {
|
|
@@ -123,6 +120,7 @@ interface MagicLinksTable {
|
|
|
123
120
|
}
|
|
124
121
|
interface UsageDailyTable {
|
|
125
122
|
account_id: string;
|
|
123
|
+
tenant_id: string | null;
|
|
126
124
|
date: string;
|
|
127
125
|
api_requests: Generated<number>;
|
|
128
126
|
deliveries: Generated<number>;
|
|
@@ -249,83 +247,6 @@ interface ChatMessagesTable {
|
|
|
249
247
|
metadata: unknown | null;
|
|
250
248
|
created_at: Generated<Date>;
|
|
251
249
|
}
|
|
252
|
-
interface WorkflowDefinitionsTable {
|
|
253
|
-
id: Generated<string>;
|
|
254
|
-
name: string;
|
|
255
|
-
version: Generated<string>;
|
|
256
|
-
status: Generated<string>;
|
|
257
|
-
trigger_type: string;
|
|
258
|
-
trigger_config: unknown;
|
|
259
|
-
handler_path: string;
|
|
260
|
-
source_code: string | null;
|
|
261
|
-
retries_config: unknown | null;
|
|
262
|
-
timeout_ms: number | null;
|
|
263
|
-
api_key_id: string;
|
|
264
|
-
project_id: string | null;
|
|
265
|
-
created_at: Generated<Date>;
|
|
266
|
-
updated_at: Generated<Date>;
|
|
267
|
-
}
|
|
268
|
-
interface WorkflowRunsTable {
|
|
269
|
-
id: Generated<string>;
|
|
270
|
-
definition_id: string;
|
|
271
|
-
status: Generated<string>;
|
|
272
|
-
trigger_type: string;
|
|
273
|
-
trigger_data: unknown | null;
|
|
274
|
-
dedup_key: string | null;
|
|
275
|
-
error: string | null;
|
|
276
|
-
started_at: Date | null;
|
|
277
|
-
completed_at: Date | null;
|
|
278
|
-
duration_ms: number | null;
|
|
279
|
-
total_ai_tokens: Generated<number>;
|
|
280
|
-
created_at: Generated<Date>;
|
|
281
|
-
}
|
|
282
|
-
interface WorkflowStepsTable {
|
|
283
|
-
id: Generated<string>;
|
|
284
|
-
run_id: string;
|
|
285
|
-
step_index: number;
|
|
286
|
-
step_id: string;
|
|
287
|
-
step_type: string;
|
|
288
|
-
status: Generated<string>;
|
|
289
|
-
input: unknown | null;
|
|
290
|
-
output: unknown | null;
|
|
291
|
-
error: string | null;
|
|
292
|
-
retry_count: Generated<number>;
|
|
293
|
-
ai_tokens_used: Generated<number>;
|
|
294
|
-
started_at: Date | null;
|
|
295
|
-
completed_at: Date | null;
|
|
296
|
-
duration_ms: number | null;
|
|
297
|
-
memo_key: string | null;
|
|
298
|
-
parent_step_id: string | null;
|
|
299
|
-
created_at: Generated<Date>;
|
|
300
|
-
}
|
|
301
|
-
interface WorkflowQueueTable {
|
|
302
|
-
id: Generated<string>;
|
|
303
|
-
run_id: string;
|
|
304
|
-
status: Generated<string>;
|
|
305
|
-
attempts: Generated<number>;
|
|
306
|
-
max_attempts: Generated<number>;
|
|
307
|
-
scheduled_for: Generated<Date>;
|
|
308
|
-
locked_at: Date | null;
|
|
309
|
-
locked_by: string | null;
|
|
310
|
-
error: string | null;
|
|
311
|
-
created_at: Generated<Date>;
|
|
312
|
-
completed_at: Date | null;
|
|
313
|
-
}
|
|
314
|
-
interface WorkflowSchedulesTable {
|
|
315
|
-
id: Generated<string>;
|
|
316
|
-
definition_id: string;
|
|
317
|
-
cron_expr: string;
|
|
318
|
-
timezone: Generated<string>;
|
|
319
|
-
next_run_at: Date;
|
|
320
|
-
last_run_at: Date | null;
|
|
321
|
-
enabled: Generated<boolean>;
|
|
322
|
-
created_at: Generated<Date>;
|
|
323
|
-
}
|
|
324
|
-
interface WorkflowCursorsTable {
|
|
325
|
-
name: string;
|
|
326
|
-
block_height: Generated<number>;
|
|
327
|
-
updated_at: Generated<Date>;
|
|
328
|
-
}
|
|
329
250
|
interface Database {
|
|
330
251
|
blocks: BlocksTable;
|
|
331
252
|
transactions: TransactionsTable;
|
|
@@ -351,17 +272,14 @@ interface Database {
|
|
|
351
272
|
team_invitations: TeamInvitationsTable;
|
|
352
273
|
chat_sessions: ChatSessionsTable;
|
|
353
274
|
chat_messages: ChatMessagesTable;
|
|
354
|
-
workflow_definitions: WorkflowDefinitionsTable;
|
|
355
|
-
workflow_runs: WorkflowRunsTable;
|
|
356
|
-
workflow_steps: WorkflowStepsTable;
|
|
357
|
-
workflow_queue: WorkflowQueueTable;
|
|
358
|
-
workflow_schedules: WorkflowSchedulesTable;
|
|
359
|
-
workflow_cursors: WorkflowCursorsTable;
|
|
360
|
-
workflow_signer_secrets: WorkflowSignerSecretsTable;
|
|
361
|
-
workflow_budgets: WorkflowBudgetsTable;
|
|
362
275
|
tenants: TenantsTable;
|
|
363
276
|
tenant_usage_monthly: TenantUsageMonthlyTable;
|
|
277
|
+
tenant_compute_addons: TenantComputeAddonsTable;
|
|
278
|
+
account_spend_caps: AccountSpendCapsTable;
|
|
364
279
|
provisioning_audit_log: ProvisioningAuditLogTable;
|
|
280
|
+
subscriptions: SubscriptionsTable;
|
|
281
|
+
subscription_outbox: SubscriptionOutboxTable;
|
|
282
|
+
subscription_deliveries: SubscriptionDeliveriesTable;
|
|
365
283
|
}
|
|
366
284
|
type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
|
|
367
285
|
interface TenantsTable {
|
|
@@ -383,9 +301,9 @@ interface TenantsTable {
|
|
|
383
301
|
service_key_enc: Buffer;
|
|
384
302
|
api_url_internal: string;
|
|
385
303
|
api_url_public: string;
|
|
386
|
-
trial_ends_at: Date;
|
|
387
304
|
suspended_at: Date | null;
|
|
388
305
|
last_health_check_at: Date | null;
|
|
306
|
+
last_active_at: Generated<Date>;
|
|
389
307
|
service_gen: Generated<number>;
|
|
390
308
|
anon_gen: Generated<number>;
|
|
391
309
|
project_id: string | null;
|
|
@@ -404,6 +322,28 @@ interface TenantUsageMonthlyTable {
|
|
|
404
322
|
first_at: Generated<Date>;
|
|
405
323
|
last_at: Generated<Date>;
|
|
406
324
|
}
|
|
325
|
+
interface TenantComputeAddonsTable {
|
|
326
|
+
id: Generated<string>;
|
|
327
|
+
tenant_id: string;
|
|
328
|
+
memory_mb_delta: Generated<number>;
|
|
329
|
+
cpu_delta: Generated<number | string>;
|
|
330
|
+
storage_mb_delta: Generated<number>;
|
|
331
|
+
effective_from: Generated<Date>;
|
|
332
|
+
effective_until: Date | null;
|
|
333
|
+
stripe_subscription_item_id: string | null;
|
|
334
|
+
created_at: Generated<Date>;
|
|
335
|
+
}
|
|
336
|
+
interface AccountSpendCapsTable {
|
|
337
|
+
account_id: string;
|
|
338
|
+
monthly_cap_cents: number | null;
|
|
339
|
+
compute_cap_cents: number | null;
|
|
340
|
+
storage_cap_cents: number | null;
|
|
341
|
+
ai_cap_cents: number | null;
|
|
342
|
+
alert_threshold_pct: Generated<number>;
|
|
343
|
+
alert_sent_at: Date | null;
|
|
344
|
+
frozen_at: Date | null;
|
|
345
|
+
updated_at: Generated<Date>;
|
|
346
|
+
}
|
|
407
347
|
type ProvisioningAuditEvent = "provision.start" | "provision.success" | "provision.failure" | "suspend" | "resume" | "resize" | "keys.rotate" | "bastion.key.upload" | "bastion.key.revoke" | "teardown";
|
|
408
348
|
type ProvisioningAuditStatus = "ok" | "error";
|
|
409
349
|
interface ProvisioningAuditLogTable {
|
|
@@ -418,29 +358,67 @@ interface ProvisioningAuditLogTable {
|
|
|
418
358
|
error: string | null;
|
|
419
359
|
created_at: Generated<Date>;
|
|
420
360
|
}
|
|
421
|
-
|
|
361
|
+
type SubscriptionStatus = "active" | "paused" | "error";
|
|
362
|
+
type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
|
|
363
|
+
type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
|
|
364
|
+
interface SubscriptionsTable {
|
|
422
365
|
id: Generated<string>;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
366
|
+
account_id: string;
|
|
367
|
+
project_id: string | null;
|
|
368
|
+
name: string;
|
|
369
|
+
status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
|
|
370
|
+
subgraph_name: string;
|
|
371
|
+
table_name: string;
|
|
372
|
+
filter: Generated<unknown>;
|
|
373
|
+
format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
|
|
374
|
+
runtime: SubscriptionRuntime | null;
|
|
375
|
+
url: string;
|
|
376
|
+
signing_secret_enc: Buffer;
|
|
377
|
+
auth_config: Generated<unknown>;
|
|
378
|
+
max_retries: Generated<number>;
|
|
379
|
+
timeout_ms: Generated<number>;
|
|
380
|
+
concurrency: Generated<number>;
|
|
381
|
+
circuit_failures: Generated<number>;
|
|
382
|
+
circuit_opened_at: Date | null;
|
|
383
|
+
last_delivery_at: Date | null;
|
|
384
|
+
last_success_at: Date | null;
|
|
385
|
+
last_error: string | null;
|
|
433
386
|
created_at: Generated<Date>;
|
|
434
387
|
updated_at: Generated<Date>;
|
|
435
388
|
}
|
|
436
|
-
|
|
389
|
+
type OutboxStatus = "pending" | "delivered" | "dead";
|
|
390
|
+
interface SubscriptionOutboxTable {
|
|
437
391
|
id: Generated<string>;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
392
|
+
subscription_id: string;
|
|
393
|
+
subgraph_name: string;
|
|
394
|
+
table_name: string;
|
|
395
|
+
block_height: number | bigint;
|
|
396
|
+
tx_id: string | null;
|
|
397
|
+
row_pk: unknown;
|
|
398
|
+
event_type: string;
|
|
399
|
+
payload: unknown;
|
|
400
|
+
dedup_key: string;
|
|
401
|
+
attempt: Generated<number>;
|
|
402
|
+
next_attempt_at: Generated<Date>;
|
|
403
|
+
status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
|
|
404
|
+
is_replay: Generated<boolean>;
|
|
405
|
+
delivered_at: Date | null;
|
|
406
|
+
failed_at: Date | null;
|
|
407
|
+
locked_by: string | null;
|
|
408
|
+
locked_until: Date | null;
|
|
442
409
|
created_at: Generated<Date>;
|
|
443
|
-
|
|
410
|
+
}
|
|
411
|
+
interface SubscriptionDeliveriesTable {
|
|
412
|
+
id: Generated<string>;
|
|
413
|
+
outbox_id: string;
|
|
414
|
+
subscription_id: string;
|
|
415
|
+
attempt: number;
|
|
416
|
+
status_code: number | null;
|
|
417
|
+
response_headers: unknown | null;
|
|
418
|
+
response_body: string | null;
|
|
419
|
+
error_message: string | null;
|
|
420
|
+
duration_ms: number | null;
|
|
421
|
+
dispatched_at: Generated<Date>;
|
|
444
422
|
}
|
|
445
423
|
/**
|
|
446
424
|
* Tenant registry queries. Encrypted columns are stored as `bytea` and
|
|
@@ -466,14 +444,24 @@ interface NewTenantInput {
|
|
|
466
444
|
serviceKey: string;
|
|
467
445
|
apiUrlInternal: string;
|
|
468
446
|
apiUrlPublic: string;
|
|
469
|
-
trialEndsAt: Date;
|
|
470
447
|
projectId?: string;
|
|
471
448
|
}
|
|
472
449
|
declare function insertTenant(db: Kysely<Database>, input: NewTenantInput): Promise<Tenant>;
|
|
473
450
|
declare function getTenantByAccount(db: Kysely<Database>, accountId: string): Promise<Tenant | null>;
|
|
474
451
|
declare function getTenantBySlug(db: Kysely<Database>, slug: string): Promise<Tenant | null>;
|
|
475
452
|
declare function listTenantsByStatus(db: Kysely<Database>, status: TenantStatus): Promise<Tenant[]>;
|
|
476
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Tenants considered "idle" for auto-pause on the Hobby tier. Active = any
|
|
455
|
+
* successful tenant-API query OR workflow run wrote `last_active_at` within
|
|
456
|
+
* the threshold.
|
|
457
|
+
*/
|
|
458
|
+
declare function listIdleHobbyTenants(db: Kysely<Database>, idleSince: Date): Promise<Tenant[]>;
|
|
459
|
+
/**
|
|
460
|
+
* Bump `last_active_at` for a tenant. Callers are expected to throttle
|
|
461
|
+
* (don't hammer on every request) — the activity middleware + workflow-
|
|
462
|
+
* runner enforce a 60s per-tenant min between writes.
|
|
463
|
+
*/
|
|
464
|
+
declare function bumpTenantActivity(db: Kysely<Database>, slug: string): Promise<void>;
|
|
477
465
|
declare function listSuspendedOlderThan(db: Kysely<Database>, olderThan: Date): Promise<Tenant[]>;
|
|
478
466
|
declare function setTenantStatus(db: Kysely<Database>, slug: string, status: TenantStatus): Promise<void>;
|
|
479
467
|
declare function recordHealthCheck(db: Kysely<Database>, slug: string, storageUsedMb: number | null): Promise<void>;
|
|
@@ -524,4 +512,4 @@ interface TenantCredentials {
|
|
|
524
512
|
* CLI). Never log the returned object.
|
|
525
513
|
*/
|
|
526
514
|
declare function getTenantCredentials(db: Kysely<Database>, slug: string): Promise<TenantCredentials | null>;
|
|
527
|
-
export { updateTenantPlan, updateTenantKeys, setTenantStatus, recordMonthlyUsage, recordHealthCheck, listTenantsByStatus, listSuspendedOlderThan,
|
|
515
|
+
export { updateTenantPlan, updateTenantKeys, setTenantStatus, recordMonthlyUsage, recordHealthCheck, listTenantsByStatus, listSuspendedOlderThan, listIdleHobbyTenants, insertTenant, getTenantCredentials, getTenantBySlug, getTenantByAccount, deleteTenant, bumpTenantKeyGen, bumpTenantActivity, TenantCredentials, RotateType, NewTenantInput };
|
|
@@ -14,15 +14,59 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/mode.ts
|
|
18
|
+
var VALID_MODES = ["oss", "dedicated", "platform"];
|
|
19
|
+
function getInstanceMode() {
|
|
20
|
+
const raw = process.env.INSTANCE_MODE?.trim().toLowerCase();
|
|
21
|
+
if (raw && VALID_MODES.includes(raw)) {
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
return "oss";
|
|
25
|
+
}
|
|
26
|
+
function isPlatformMode() {
|
|
27
|
+
return getInstanceMode() === "platform";
|
|
28
|
+
}
|
|
29
|
+
function isOssMode() {
|
|
30
|
+
return getInstanceMode() === "oss";
|
|
31
|
+
}
|
|
32
|
+
function isDedicatedMode() {
|
|
33
|
+
return getInstanceMode() === "dedicated";
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
// src/crypto/secrets.ts
|
|
18
37
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
38
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
39
|
+
import { resolve } from "node:path";
|
|
19
40
|
var KEY_ENV = "SECONDLAYER_SECRETS_KEY";
|
|
20
41
|
var IV_LEN = 12;
|
|
21
42
|
var TAG_LEN = 16;
|
|
43
|
+
function bootstrapOssKey() {
|
|
44
|
+
const envPath = resolve(process.cwd(), ".env.local");
|
|
45
|
+
if (existsSync(envPath)) {
|
|
46
|
+
const contents = readFileSync(envPath, "utf8");
|
|
47
|
+
const match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);
|
|
48
|
+
if (match) {
|
|
49
|
+
process.env[KEY_ENV] = match[1];
|
|
50
|
+
return match[1];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const hex = randomBytes(32).toString("hex");
|
|
54
|
+
const line = `${existsSync(envPath) ? `
|
|
55
|
+
` : ""}${KEY_ENV}=${hex}
|
|
56
|
+
`;
|
|
57
|
+
appendFileSync(envPath, line, { mode: 384 });
|
|
58
|
+
process.env[KEY_ENV] = hex;
|
|
59
|
+
console.log(`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`);
|
|
60
|
+
return hex;
|
|
61
|
+
}
|
|
22
62
|
function loadKey() {
|
|
23
|
-
|
|
63
|
+
let hex = process.env[KEY_ENV];
|
|
24
64
|
if (!hex) {
|
|
25
|
-
|
|
65
|
+
if (getInstanceMode() === "oss") {
|
|
66
|
+
hex = bootstrapOssKey();
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`);
|
|
69
|
+
}
|
|
26
70
|
}
|
|
27
71
|
const key = Buffer.from(hex, "hex");
|
|
28
72
|
if (key.length !== 32) {
|
|
@@ -80,7 +124,6 @@ async function insertTenant(db, input) {
|
|
|
80
124
|
service_key_enc: encryptSecret(input.serviceKey),
|
|
81
125
|
api_url_internal: input.apiUrlInternal,
|
|
82
126
|
api_url_public: input.apiUrlPublic,
|
|
83
|
-
trial_ends_at: input.trialEndsAt,
|
|
84
127
|
project_id: input.projectId ?? null
|
|
85
128
|
};
|
|
86
129
|
return db.insertInto("tenants").values(row).returningAll().executeTakeFirstOrThrow();
|
|
@@ -96,8 +139,11 @@ async function getTenantBySlug(db, slug) {
|
|
|
96
139
|
async function listTenantsByStatus(db, status) {
|
|
97
140
|
return db.selectFrom("tenants").selectAll().where("status", "=", status).execute();
|
|
98
141
|
}
|
|
99
|
-
async function
|
|
100
|
-
return db.selectFrom("tenants").selectAll().where("status", "
|
|
142
|
+
async function listIdleHobbyTenants(db, idleSince) {
|
|
143
|
+
return db.selectFrom("tenants").selectAll().where("status", "=", "active").where("plan", "=", "hobby").where("last_active_at", "<", idleSince).execute();
|
|
144
|
+
}
|
|
145
|
+
async function bumpTenantActivity(db, slug) {
|
|
146
|
+
await db.updateTable("tenants").set({ last_active_at: new Date }).where("slug", "=", slug).execute();
|
|
101
147
|
}
|
|
102
148
|
async function listSuspendedOlderThan(db, olderThan) {
|
|
103
149
|
return db.selectFrom("tenants").selectAll().where("status", "=", "suspended").where("suspended_at", "<", olderThan).execute();
|
|
@@ -207,14 +253,15 @@ export {
|
|
|
207
253
|
recordHealthCheck,
|
|
208
254
|
listTenantsByStatus,
|
|
209
255
|
listSuspendedOlderThan,
|
|
210
|
-
|
|
256
|
+
listIdleHobbyTenants,
|
|
211
257
|
insertTenant,
|
|
212
258
|
getTenantCredentials,
|
|
213
259
|
getTenantBySlug,
|
|
214
260
|
getTenantByAccount,
|
|
215
261
|
deleteTenant,
|
|
216
|
-
bumpTenantKeyGen
|
|
262
|
+
bumpTenantKeyGen,
|
|
263
|
+
bumpTenantActivity
|
|
217
264
|
};
|
|
218
265
|
|
|
219
|
-
//# debugId=
|
|
266
|
+
//# debugId=9AAE380FD6880D8264756E2164756E21
|
|
220
267
|
//# sourceMappingURL=tenants.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/crypto/secrets.ts", "../src/db/queries/tenants.ts"],
|
|
3
|
+
"sources": ["../src/mode.ts", "../src/crypto/secrets.ts", "../src/db/queries/tenants.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"
|
|
6
|
-
"import {
|
|
5
|
+
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin, workflows). Everything runs against a single\n * `DATABASE_URL`. Intended for `docker compose up`.\n *\n * - `dedicated`: per-customer managed instance. JWT-based auth (anon =\n * read-only, service = full). Dual-DB mode — shared source indexer DB for\n * block reads, per-tenant target DB for subgraph data. No platform-wide\n * routes mounted (no cross-tenant accounts).\n *\n * - `platform`: control-plane mode. Magic-link auth, API keys, projects,\n * tenants, admin. Serves the dashboard + CLI against a single shared DB.\n */\n\nexport type InstanceMode = \"oss\" | \"dedicated\" | \"platform\";\n\nconst VALID_MODES: readonly InstanceMode[] = [\"oss\", \"dedicated\", \"platform\"];\n\n/**\n * Resolve the active instance mode from `process.env.INSTANCE_MODE`.\n * Defaults to `\"oss\"` — the safest default for self-hosters who deploy\n * without setting the variable.\n */\nexport function getInstanceMode(): InstanceMode {\n\tconst raw = process.env.INSTANCE_MODE?.trim().toLowerCase();\n\tif (raw && (VALID_MODES as readonly string[]).includes(raw)) {\n\t\treturn raw as InstanceMode;\n\t}\n\treturn \"oss\";\n}\n\n/** True when the active mode is `\"platform\"` (shared multi-tenant). */\nexport function isPlatformMode(): boolean {\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"oss\"` (self-hosted). */\nexport function isOssMode(): boolean {\n\treturn getInstanceMode() === \"oss\";\n}\n\n/** True when the active mode is `\"dedicated\"` (per-tenant managed). */\nexport function isDedicatedMode(): boolean {\n\treturn getInstanceMode() === \"dedicated\";\n}\n",
|
|
6
|
+
"import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\nimport { appendFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { getInstanceMode } from \"../mode.ts\";\n\n/**\n * AES-256-GCM symmetric envelope for encrypted secrets at rest (tenant keys,\n * subscription signing secrets, etc.).\n *\n * Ciphertext layout: `iv (12 bytes) || authTag (16 bytes) || ciphertext`\n *\n * The key comes from `SECONDLAYER_SECRETS_KEY` — 32 bytes hex. In OSS mode,\n * if the env var is unset on first use we autogenerate a key and persist it\n * to `.env.local` in the current working directory so subsequent restarts\n * pick it up without user intervention. Dedicated/platform modes throw —\n * those runtimes must provision the key explicitly.\n *\n * Rotation strategy: re-encrypt all rows with the new key and swap the env\n * var. Not zero-downtime, but acceptable at v2 scale. For real KMS (AWS\n * KMS, Vault, GCP KMS), wrap the same byte layout behind an\n * `EncryptSecret`/`DecryptSecret` interface and swap at startup.\n */\n\nconst KEY_ENV = \"SECONDLAYER_SECRETS_KEY\";\nconst IV_LEN = 12;\nconst TAG_LEN = 16;\n\nfunction bootstrapOssKey(): string {\n\tconst envPath = resolve(process.cwd(), \".env.local\");\n\n\t// Check existing .env.local first — prior run may have written it.\n\tif (existsSync(envPath)) {\n\t\tconst contents = readFileSync(envPath, \"utf8\");\n\t\tconst match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);\n\t\tif (match) {\n\t\t\tprocess.env[KEY_ENV] = match[1];\n\t\t\treturn match[1];\n\t\t}\n\t}\n\n\tconst hex = randomBytes(32).toString(\"hex\");\n\tconst line = `${existsSync(envPath) ? \"\\n\" : \"\"}${KEY_ENV}=${hex}\\n`;\n\tappendFileSync(envPath, line, { mode: 0o600 });\n\tprocess.env[KEY_ENV] = hex;\n\tconsole.log(\n\t\t`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`,\n\t);\n\treturn hex;\n}\n\nfunction loadKey(): Buffer {\n\tlet hex = process.env[KEY_ENV];\n\tif (!hex) {\n\t\tif (getInstanceMode() === \"oss\") {\n\t\t\thex = bootstrapOssKey();\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`,\n\t\t\t);\n\t\t}\n\t}\n\tconst key = Buffer.from(hex, \"hex\");\n\tif (key.length !== 32) {\n\t\tthrow new Error(`${KEY_ENV} must be 32 bytes hex (got ${key.length})`);\n\t}\n\treturn key;\n}\n\nlet _cachedKey: Buffer | null = null;\nfunction getKey(): Buffer {\n\tif (!_cachedKey) _cachedKey = loadKey();\n\treturn _cachedKey;\n}\n\nexport function encryptSecret(plaintext: string): Buffer {\n\tconst key = getKey();\n\tconst iv = randomBytes(IV_LEN);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, \"utf8\"),\n\t\tcipher.final(),\n\t]);\n\tconst tag = cipher.getAuthTag();\n\treturn Buffer.concat([iv, tag, ciphertext]);\n}\n\nexport function decryptSecret(envelope: Buffer): string {\n\tconst key = getKey();\n\tconst iv = envelope.subarray(0, IV_LEN);\n\tconst tag = envelope.subarray(IV_LEN, IV_LEN + TAG_LEN);\n\tconst ciphertext = envelope.subarray(IV_LEN + TAG_LEN);\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, iv);\n\tdecipher.setAuthTag(tag);\n\treturn decipher.update(ciphertext).toString(\"utf8\") + decipher.final(\"utf8\");\n}\n\n/** Generate a fresh 32-byte hex key suitable for `SECONDLAYER_SECRETS_KEY`. */\nexport function generateSecretsKey(): string {\n\treturn randomBytes(32).toString(\"hex\");\n}\n",
|
|
7
|
+
"import { type Kysely, sql } from \"kysely\";\nimport { decryptSecret, encryptSecret } from \"../../crypto/secrets.ts\";\nimport type { Database, InsertTenant, Tenant, TenantStatus } from \"../types.ts\";\n\n/**\n * Tenant registry queries. Encrypted columns are stored as `bytea` and\n * transparently encrypted/decrypted via `encryptSecret`/`decryptSecret`.\n *\n * Never return decrypted values from listTenants — only `getTenantCredentials`\n * surfaces plaintext, and only when explicitly called by a caller that\n * needs to hand creds to a CLI or dashboard session.\n */\n\nexport interface NewTenantInput {\n\taccountId: string;\n\tslug: string;\n\tplan: string;\n\tcpus: number;\n\tmemoryMb: number;\n\tstorageLimitMb: number;\n\tpgContainerId: string;\n\tapiContainerId: string;\n\tprocessorContainerId: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n\tprojectId?: string;\n}\n\nexport async function insertTenant(\n\tdb: Kysely<Database>,\n\tinput: NewTenantInput,\n): Promise<Tenant> {\n\tconst row: InsertTenant = {\n\t\taccount_id: input.accountId,\n\t\tslug: input.slug,\n\t\tstatus: \"active\",\n\t\tplan: input.plan,\n\t\tcpus: input.cpus,\n\t\tmemory_mb: input.memoryMb,\n\t\tstorage_limit_mb: input.storageLimitMb,\n\t\tpg_container_id: input.pgContainerId,\n\t\tapi_container_id: input.apiContainerId,\n\t\tprocessor_container_id: input.processorContainerId,\n\t\ttarget_database_url_enc: encryptSecret(input.targetDatabaseUrl),\n\t\ttenant_jwt_secret_enc: encryptSecret(input.tenantJwtSecret),\n\t\tanon_key_enc: encryptSecret(input.anonKey),\n\t\tservice_key_enc: encryptSecret(input.serviceKey),\n\t\tapi_url_internal: input.apiUrlInternal,\n\t\tapi_url_public: input.apiUrlPublic,\n\t\tproject_id: input.projectId ?? null,\n\t};\n\treturn db\n\t\t.insertInto(\"tenants\")\n\t\t.values(row)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getTenantByAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"<>\", \"deleted\")\n\t\t.orderBy(\"created_at\", \"desc\")\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function getTenantBySlug(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function listTenantsByStatus(\n\tdb: Kysely<Database>,\n\tstatus: TenantStatus,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", status)\n\t\t.execute();\n}\n\n/**\n * Tenants considered \"idle\" for auto-pause on the Hobby tier. Active = any\n * successful tenant-API query OR workflow run wrote `last_active_at` within\n * the threshold.\n */\nexport async function listIdleHobbyTenants(\n\tdb: Kysely<Database>,\n\tidleSince: Date,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"active\")\n\t\t.where(\"plan\", \"=\", \"hobby\")\n\t\t.where(\"last_active_at\", \"<\", idleSince)\n\t\t.execute();\n}\n\n/**\n * Bump `last_active_at` for a tenant. Callers are expected to throttle\n * (don't hammer on every request) — the activity middleware + workflow-\n * runner enforce a 60s per-tenant min between writes.\n */\nexport async function bumpTenantActivity(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({ last_active_at: new Date() })\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport async function listSuspendedOlderThan(\n\tdb: Kysely<Database>,\n\tolderThan: Date,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"suspended\")\n\t\t.where(\"suspended_at\", \"<\", olderThan)\n\t\t.execute();\n}\n\nexport async function setTenantStatus(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstatus: TenantStatus,\n): Promise<void> {\n\tconst patch: Record<string, unknown> = {\n\t\tstatus,\n\t\tupdated_at: new Date(),\n\t};\n\tif (status === \"suspended\") patch.suspended_at = new Date();\n\tif (status === \"active\") patch.suspended_at = null;\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\nexport async function recordHealthCheck(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstorageUsedMb: number | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tlast_health_check_at: new Date(),\n\t\t\tstorage_used_mb: storageUsedMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\n/**\n * Record a storage measurement into the current calendar month's bucket.\n * Maintains peak, running average, and the most recent value in a single\n * upsert. Billing will consume this later; for now the table just gives\n * us evidence of usage over time.\n */\nexport async function recordMonthlyUsage(\n\tdb: Kysely<Database>,\n\ttenantId: string,\n\tstorageMb: number,\n): Promise<void> {\n\t// Bucket is the first day of the current month (UTC), so the unique\n\t// (tenant_id, period_month) constraint groups all samples cleanly.\n\tconst now = new Date();\n\tconst periodMonth = new Date(\n\t\tDate.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),\n\t);\n\n\t// Running mean: avg_new = (avg_old * n + x) / (n + 1). Doing it in SQL\n\t// keeps the write atomic — no read-modify-write race between ticks.\n\tawait sql`\n\t\tINSERT INTO tenant_usage_monthly (\n\t\t\ttenant_id, period_month,\n\t\t\tstorage_peak_mb, storage_avg_mb, storage_last_mb,\n\t\t\tmeasurements, first_at, last_at\n\t\t) VALUES (\n\t\t\t${tenantId}, ${periodMonth},\n\t\t\t${storageMb}, ${storageMb}, ${storageMb},\n\t\t\t1, now(), now()\n\t\t)\n\t\tON CONFLICT (tenant_id, period_month) DO UPDATE SET\n\t\t\tstorage_peak_mb = GREATEST(tenant_usage_monthly.storage_peak_mb, EXCLUDED.storage_last_mb),\n\t\t\tstorage_avg_mb = (\n\t\t\t\t(tenant_usage_monthly.storage_avg_mb * tenant_usage_monthly.measurements + EXCLUDED.storage_last_mb)\n\t\t\t\t/ (tenant_usage_monthly.measurements + 1)\n\t\t\t),\n\t\t\tstorage_last_mb = EXCLUDED.storage_last_mb,\n\t\t\tmeasurements = tenant_usage_monthly.measurements + 1,\n\t\t\tlast_at = now()\n\t`.execute(db);\n}\n\nexport async function updateTenantPlan(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tplan: string,\n\tcpus: number,\n\tmemoryMb: number,\n\tstorageLimitMb: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tplan,\n\t\t\tcpus,\n\t\t\tmemory_mb: memoryMb,\n\t\t\tstorage_limit_mb: storageLimitMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport type RotateType = \"service\" | \"anon\" | \"both\";\n\n/**\n * Bump the selected gen counter(s) by 1 and return the new values.\n * Used by the key-rotate endpoint to force the tenant API to reject\n * previously-issued tokens of the rotated role(s).\n */\nexport async function bumpTenantKeyGen(\n\tdb: Kysely<Database>,\n\tslug: string,\n\ttype: RotateType,\n): Promise<{ serviceGen: number; anonGen: number }> {\n\tconst bumpService = type === \"service\" || type === \"both\";\n\tconst bumpAnon = type === \"anon\" || type === \"both\";\n\tconst row = await db\n\t\t.updateTable(\"tenants\")\n\t\t.set((eb) => ({\n\t\t\tservice_gen: bumpService\n\t\t\t\t? eb(\"service_gen\", \"+\", 1)\n\t\t\t\t: eb.ref(\"service_gen\"),\n\t\t\tanon_gen: bumpAnon ? eb(\"anon_gen\", \"+\", 1) : eb.ref(\"anon_gen\"),\n\t\t\tupdated_at: new Date(),\n\t\t}))\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.returning([\"service_gen\", \"anon_gen\"])\n\t\t.executeTakeFirstOrThrow();\n\treturn { serviceGen: row.service_gen, anonGen: row.anon_gen };\n}\n\n/**\n * Replace the encrypted key columns after a successful rotate. Only the\n * rotated column(s) are written — the other stays untouched.\n */\nexport async function updateTenantKeys(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tkeys: { serviceKey?: string; anonKey?: string },\n): Promise<void> {\n\tconst patch: Record<string, unknown> = { updated_at: new Date() };\n\tif (keys.serviceKey) patch.service_key_enc = encryptSecret(keys.serviceKey);\n\tif (keys.anonKey) patch.anon_key_enc = encryptSecret(keys.anonKey);\n\tif (Object.keys(patch).length === 1) return; // only updated_at — nothing to write\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\n/**\n * Hard-delete a tenant row. Call only AFTER the provisioner has torn down\n * containers + volume; otherwise orphaned resources linger. Returns whether\n * a row was actually deleted.\n */\nexport async function deleteTenant(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<boolean> {\n\tconst res = await db\n\t\t.deleteFrom(\"tenants\")\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn (res.numDeletedRows ?? 0n) > 0n;\n}\n\nexport interface TenantCredentials {\n\tslug: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n}\n\n/**\n * Decrypts the four encrypted columns and returns them plaintext. Call\n * this only when surfacing credentials to an authorized caller (dashboard,\n * CLI). Never log the returned object.\n */\nexport async function getTenantCredentials(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<TenantCredentials | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select([\n\t\t\t\"slug\",\n\t\t\t\"target_database_url_enc\",\n\t\t\t\"tenant_jwt_secret_enc\",\n\t\t\t\"anon_key_enc\",\n\t\t\t\"service_key_enc\",\n\t\t\t\"api_url_internal\",\n\t\t\t\"api_url_public\",\n\t\t])\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\tif (!row) return null;\n\treturn {\n\t\tslug: row.slug,\n\t\ttargetDatabaseUrl: decryptSecret(row.target_database_url_enc),\n\t\ttenantJwtSecret: decryptSecret(row.tenant_jwt_secret_enc),\n\t\tanonKey: decryptSecret(row.anon_key_enc),\n\t\tserviceKey: decryptSecret(row.service_key_enc),\n\t\tapiUrlInternal: row.api_url_internal,\n\t\tapiUrlPublic: row.api_url_public,\n\t};\n}\n"
|
|
7
8
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA;
|
|
9
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "9AAE380FD6880D8264756E2164756E21",
|
|
10
11
|
"names": []
|
|
11
12
|
}
|