@secondlayer/shared 3.0.0 → 4.0.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.
- package/README.md +4 -2
- package/dist/src/crypto/secrets.js +58 -14
- package/dist/src/crypto/secrets.js.map +4 -4
- package/dist/src/db/index.d.ts +0 -1
- package/dist/src/db/queries/account-spend-caps.d.ts +0 -1
- package/dist/src/db/queries/account-spend-caps.js +1 -2
- package/dist/src/db/queries/account-spend-caps.js.map +3 -3
- package/dist/src/db/queries/account-usage.d.ts +1 -14
- package/dist/src/db/queries/account-usage.js +3 -52
- package/dist/src/db/queries/account-usage.js.map +4 -4
- package/dist/src/db/queries/accounts.d.ts +0 -1
- package/dist/src/db/queries/integrity.d.ts +0 -1
- package/dist/src/db/queries/projects.d.ts +0 -1
- package/dist/src/db/queries/provisioning-audit.d.ts +0 -1
- package/dist/src/db/queries/subgraph-gaps.d.ts +0 -1
- package/dist/src/db/queries/subgraphs.d.ts +0 -1
- package/dist/src/db/queries/subscriptions.d.ts +0 -1
- package/dist/src/db/queries/subscriptions.js +58 -14
- package/dist/src/db/queries/subscriptions.js.map +4 -4
- package/dist/src/db/queries/tenant-compute-addons.d.ts +0 -1
- package/dist/src/db/queries/tenants.d.ts +5 -6
- package/dist/src/db/queries/tenants.js +58 -14
- package/dist/src/db/queries/tenants.js.map +5 -5
- package/dist/src/db/queries/usage.d.ts +0 -1
- package/dist/src/db/schema.d.ts +0 -1
- package/dist/src/errors.d.ts +1 -6
- package/dist/src/errors.js +3 -11
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +1 -9
- package/dist/src/index.js +4 -13
- package/dist/src/index.js.map +4 -4
- package/dist/src/mode.d.ts +1 -1
- package/dist/src/mode.js.map +1 -1
- package/dist/src/node/local-client.d.ts +0 -1
- package/dist/src/pricing.d.ts +1 -37
- package/dist/src/pricing.js +2 -42
- package/dist/src/pricing.js.map +3 -3
- package/dist/src/schemas/index.d.ts +0 -2
- package/dist/src/schemas/index.js +2 -3
- package/dist/src/schemas/index.js.map +3 -3
- package/dist/src/schemas/subgraphs.d.ts +0 -2
- package/dist/src/schemas/subgraphs.js +2 -3
- package/dist/src/schemas/subgraphs.js.map +3 -3
- package/migrations/0032_drop_streams_tables.ts +1 -1
- package/migrations/0036_tx_confirmed_notify.ts +2 -3
- package/migrations/0038_drop_workflow_tables.ts +3 -3
- package/migrations/0046_tenant_activity_signal.ts +2 -3
- package/migrations/0051_workflow_ai_usage_daily.ts +4 -8
- package/migrations/0057_subscriptions.ts +1 -1
- package/migrations/0058_drop_ai_cap_cents.ts +15 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @secondlayer/shared
|
|
2
2
|
|
|
3
|
-
Foundational utilities for Second Layer services: DB layer (Kysely+
|
|
3
|
+
Foundational utilities for Second Layer services: DB layer (Kysely + postgres-js), Zod schemas, crypto (Standard Webhooks signing, AES-GCM secret envelope), logger, pricing, env/mode utils, Stacks node clients.
|
|
4
4
|
|
|
5
5
|
## Testing
|
|
6
6
|
|
|
@@ -36,6 +36,8 @@ DATABASE_URL=... bun run migrate
|
|
|
36
36
|
| `@secondlayer/shared/env` | Environment config |
|
|
37
37
|
| `@secondlayer/shared/logger` | Logger |
|
|
38
38
|
| `@secondlayer/shared/errors` | Error types |
|
|
39
|
-
| `@secondlayer/shared/crypto` | HMAC signing |
|
|
39
|
+
| `@secondlayer/shared/crypto` | HMAC helpers, Standard Webhooks signing, AES-GCM secret envelope |
|
|
40
|
+
| `@secondlayer/shared/pricing` | Plan definitions + billing helpers |
|
|
41
|
+
| `@secondlayer/shared/mode` | INSTANCE_MODE dispatch (platform / dedicated / oss) |
|
|
40
42
|
| `@secondlayer/shared/node` | Stacks node client |
|
|
41
43
|
| `@secondlayer/shared/node/hiro-pg-client` | Direct PG queries against Hiro DB |
|
|
@@ -35,29 +35,73 @@ function isDedicatedMode() {
|
|
|
35
35
|
|
|
36
36
|
// src/crypto/secrets.ts
|
|
37
37
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
appendFileSync,
|
|
40
|
+
closeSync,
|
|
41
|
+
existsSync,
|
|
42
|
+
openSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
unlinkSync
|
|
45
|
+
} from "node:fs";
|
|
39
46
|
import { resolve } from "node:path";
|
|
40
47
|
var KEY_ENV = "SECONDLAYER_SECRETS_KEY";
|
|
41
48
|
var IV_LEN = 12;
|
|
42
49
|
var TAG_LEN = 16;
|
|
50
|
+
function readExistingKey(envPath) {
|
|
51
|
+
if (!existsSync(envPath))
|
|
52
|
+
return null;
|
|
53
|
+
const contents = readFileSync(envPath, "utf8");
|
|
54
|
+
const match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);
|
|
55
|
+
return match ? match[1] : null;
|
|
56
|
+
}
|
|
57
|
+
var STALE_LOCK_MS = 1e4;
|
|
58
|
+
var POLL_MS = 25;
|
|
43
59
|
function bootstrapOssKey() {
|
|
44
60
|
const envPath = resolve(process.cwd(), ".env.local");
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
const existing = readExistingKey(envPath);
|
|
62
|
+
if (existing) {
|
|
63
|
+
process.env[KEY_ENV] = existing;
|
|
64
|
+
return existing;
|
|
65
|
+
}
|
|
66
|
+
const lockPath = `${envPath}.secret-bootstrap.lock`;
|
|
67
|
+
let lockFd = null;
|
|
68
|
+
try {
|
|
69
|
+
lockFd = openSync(lockPath, "wx", 384);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const e = err;
|
|
72
|
+
if (e.code !== "EEXIST")
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
if (lockFd === null) {
|
|
76
|
+
const deadline = Date.now() + STALE_LOCK_MS;
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
const key = readExistingKey(envPath);
|
|
79
|
+
if (key) {
|
|
80
|
+
process.env[KEY_ENV] = key;
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
Bun.sleepSync(POLL_MS);
|
|
51
84
|
}
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(lockPath);
|
|
87
|
+
} catch {}
|
|
88
|
+
return bootstrapOssKey();
|
|
52
89
|
}
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
try {
|
|
91
|
+
const hex = randomBytes(32).toString("hex");
|
|
92
|
+
const line = `${existsSync(envPath) ? `
|
|
55
93
|
` : ""}${KEY_ENV}=${hex}
|
|
56
94
|
`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
appendFileSync(envPath, line, { mode: 384 });
|
|
96
|
+
process.env[KEY_ENV] = hex;
|
|
97
|
+
console.log(`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`);
|
|
98
|
+
return hex;
|
|
99
|
+
} finally {
|
|
100
|
+
closeSync(lockFd);
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(lockPath);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
61
105
|
}
|
|
62
106
|
function loadKey() {
|
|
63
107
|
let hex = process.env[KEY_ENV];
|
|
@@ -109,5 +153,5 @@ export {
|
|
|
109
153
|
decryptSecret
|
|
110
154
|
};
|
|
111
155
|
|
|
112
|
-
//# debugId=
|
|
156
|
+
//# debugId=B9CC14B7680BC22364756E2164756E21
|
|
113
157
|
//# sourceMappingURL=secrets.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/mode.ts", "../src/crypto/secrets.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin,
|
|
6
|
-
"import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\nimport {
|
|
5
|
+
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin, tenants). 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 {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\topenSync,\n\treadFileSync,\n\tunlinkSync,\n} 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 readExistingKey(envPath: string): string | null {\n\tif (!existsSync(envPath)) return null;\n\tconst contents = readFileSync(envPath, \"utf8\");\n\tconst match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);\n\treturn match ? match[1]! : null;\n}\n\n/**\n * Atomic file lock via `openSync(..., \"wx\")` — O_CREAT | O_EXCL. If two\n * processes race on cold-compose start, exactly one creates the lock\n * file; the loser polls until the winner finishes writing `.env.local`,\n * then reads the winner's key. Stale locks (process crashed mid-write)\n * are cleaned after `STALE_LOCK_MS`.\n */\nconst STALE_LOCK_MS = 10_000;\nconst POLL_MS = 25;\n\nfunction bootstrapOssKey(): string {\n\tconst envPath = resolve(process.cwd(), \".env.local\");\n\n\t// Fast path — key already on disk from a prior run.\n\tconst existing = readExistingKey(envPath);\n\tif (existing) {\n\t\tprocess.env[KEY_ENV] = existing;\n\t\treturn existing;\n\t}\n\n\tconst lockPath = `${envPath}.secret-bootstrap.lock`;\n\tlet lockFd: number | null = null;\n\ttry {\n\t\tlockFd = openSync(lockPath, \"wx\", 0o600);\n\t} catch (err) {\n\t\tconst e = err as NodeJS.ErrnoException;\n\t\tif (e.code !== \"EEXIST\") throw err;\n\t}\n\n\tif (lockFd === null) {\n\t\t// Another process is bootstrapping. Poll for its result.\n\t\tconst deadline = Date.now() + STALE_LOCK_MS;\n\t\twhile (Date.now() < deadline) {\n\t\t\tconst key = readExistingKey(envPath);\n\t\t\tif (key) {\n\t\t\t\tprocess.env[KEY_ENV] = key;\n\t\t\t\treturn key;\n\t\t\t}\n\t\t\tBun.sleepSync(POLL_MS);\n\t\t}\n\t\t// Lock holder died mid-write — force-clean and retry once.\n\t\ttry {\n\t\t\tunlinkSync(lockPath);\n\t\t} catch {}\n\t\treturn bootstrapOssKey();\n\t}\n\n\ttry {\n\t\tconst hex = randomBytes(32).toString(\"hex\");\n\t\tconst line = `${existsSync(envPath) ? \"\\n\" : \"\"}${KEY_ENV}=${hex}\\n`;\n\t\tappendFileSync(envPath, line, { mode: 0o600 });\n\t\tprocess.env[KEY_ENV] = hex;\n\t\tconsole.log(\n\t\t\t`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`,\n\t\t);\n\t\treturn hex;\n\t} finally {\n\t\tcloseSync(lockFd);\n\t\ttry {\n\t\t\tunlinkSync(lockPath);\n\t\t} catch {}\n\t}\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
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAkBA,IAAM,cAAuC,CAAC,OAAO,aAAa,UAAU;AAOrE,SAAS,eAAe,GAAiB;AAAA,EAC/C,MAAM,MAAM,QAAQ,IAAI,eAAe,KAAK,EAAE,YAAY;AAAA,EAC1D,IAAI,OAAQ,YAAkC,SAAS,GAAG,GAAG;AAAA,IAC5D,OAAO;AAAA,EACR;AAAA,EACA,OAAO;AAAA;AAID,SAAS,cAAc,GAAY;AAAA,EACzC,OAAO,gBAAgB,MAAM;AAAA;AAIvB,SAAS,SAAS,GAAY;AAAA,EACpC,OAAO,gBAAgB,MAAM;AAAA;AAIvB,SAAS,eAAe,GAAY;AAAA,EAC1C,OAAO,gBAAgB,MAAM;AAAA;;;AC7C9B;AACA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "B9CC14B7680BC22364756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -364,7 +364,6 @@ interface AccountSpendCapsTable {
|
|
|
364
364
|
monthly_cap_cents: number | null;
|
|
365
365
|
compute_cap_cents: number | null;
|
|
366
366
|
storage_cap_cents: number | null;
|
|
367
|
-
ai_cap_cents: number | null;
|
|
368
367
|
alert_threshold_pct: Generated<number>;
|
|
369
368
|
alert_sent_at: Date | null;
|
|
370
369
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -25,7 +25,6 @@ async function upsertCaps(db, accountId, patch) {
|
|
|
25
25
|
monthly_cap_cents: patch.monthly_cap_cents ?? null,
|
|
26
26
|
compute_cap_cents: patch.compute_cap_cents ?? null,
|
|
27
27
|
storage_cap_cents: patch.storage_cap_cents ?? null,
|
|
28
|
-
ai_cap_cents: patch.ai_cap_cents ?? null,
|
|
29
28
|
alert_threshold_pct: patch.alert_threshold_pct ?? 80,
|
|
30
29
|
alert_sent_at: patch.alert_sent_at ?? null,
|
|
31
30
|
frozen_at: patch.frozen_at ?? null
|
|
@@ -56,5 +55,5 @@ export {
|
|
|
56
55
|
clearFreeze
|
|
57
56
|
};
|
|
58
57
|
|
|
59
|
-
//# debugId=
|
|
58
|
+
//# debugId=61F9CA6C05B3563A64756E2164756E21
|
|
60
59
|
//# sourceMappingURL=account-spend-caps.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/db/queries/account-spend-caps.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { Kysely } from \"kysely\";\nimport type {\n\tAccountSpendCap,\n\tDatabase,\n\tInsertAccountSpendCap,\n\tUpdateAccountSpendCap,\n} from \"../types.ts\";\n\n/**\n * Spend-cap state for an account. Both the metering crons (check + set\n * frozen_at) and the dashboard (read + update caps) call through here.\n */\n\nexport async function getCaps(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<AccountSpendCap | null> {\n\tconst row = await db\n\t\t.selectFrom(\"account_spend_caps\")\n\t\t.selectAll()\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\n/**\n * Upsert semantics: row is created on first write (default threshold\n * 80%), subsequent writes PATCH. `updated_at` is always bumped.\n */\nexport async function upsertCaps(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tpatch: Omit<UpdateAccountSpendCap, \"account_id\" | \"updated_at\">,\n): Promise<AccountSpendCap> {\n\tconst insert: InsertAccountSpendCap = {\n\t\taccount_id: accountId,\n\t\tmonthly_cap_cents: patch.monthly_cap_cents ?? null,\n\t\tcompute_cap_cents: patch.compute_cap_cents ?? null,\n\t\tstorage_cap_cents: patch.storage_cap_cents ?? null,\n\t\
|
|
5
|
+
"import type { Kysely } from \"kysely\";\nimport type {\n\tAccountSpendCap,\n\tDatabase,\n\tInsertAccountSpendCap,\n\tUpdateAccountSpendCap,\n} from \"../types.ts\";\n\n/**\n * Spend-cap state for an account. Both the metering crons (check + set\n * frozen_at) and the dashboard (read + update caps) call through here.\n */\n\nexport async function getCaps(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<AccountSpendCap | null> {\n\tconst row = await db\n\t\t.selectFrom(\"account_spend_caps\")\n\t\t.selectAll()\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\n/**\n * Upsert semantics: row is created on first write (default threshold\n * 80%), subsequent writes PATCH. `updated_at` is always bumped.\n */\nexport async function upsertCaps(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tpatch: Omit<UpdateAccountSpendCap, \"account_id\" | \"updated_at\">,\n): Promise<AccountSpendCap> {\n\tconst insert: InsertAccountSpendCap = {\n\t\taccount_id: accountId,\n\t\tmonthly_cap_cents: patch.monthly_cap_cents ?? null,\n\t\tcompute_cap_cents: patch.compute_cap_cents ?? null,\n\t\tstorage_cap_cents: patch.storage_cap_cents ?? null,\n\t\talert_threshold_pct: patch.alert_threshold_pct ?? 80,\n\t\talert_sent_at: patch.alert_sent_at ?? null,\n\t\tfrozen_at: patch.frozen_at ?? null,\n\t};\n\n\treturn db\n\t\t.insertInto(\"account_spend_caps\")\n\t\t.values(insert)\n\t\t.onConflict((oc) =>\n\t\t\toc.column(\"account_id\").doUpdateSet({\n\t\t\t\t...patch,\n\t\t\t\tupdated_at: new Date(),\n\t\t\t}),\n\t\t)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\n/** Mark an account frozen at the current time (cap just tripped). */\nexport async function freezeAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<void> {\n\tawait upsertCaps(db, accountId, { frozen_at: new Date() });\n}\n\n/**\n * Clear the frozen + alert state — called on `invoice.paid` webhook at\n * cycle rollover (new billing period starts fresh) OR when the user\n * explicitly raises their cap above current usage.\n */\nexport async function clearFreeze(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<void> {\n\tawait upsertCaps(db, accountId, {\n\t\tfrozen_at: null,\n\t\talert_sent_at: null,\n\t});\n}\n\n/** Is this account currently cap-frozen? Bulk-checked by metering crons. */\nexport async function listFrozenAccountIds(\n\tdb: Kysely<Database>,\n): Promise<Set<string>> {\n\tconst rows = await db\n\t\t.selectFrom(\"account_spend_caps\")\n\t\t.select(\"account_id\")\n\t\t.where(\"frozen_at\", \"is not\", null)\n\t\t.execute();\n\treturn new Set(rows.map((r) => r.account_id));\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAaA,eAAsB,OAAO,CAC5B,IACA,WACkC;AAAA,EAClC,MAAM,MAAM,MAAM,GAChB,WAAW,oBAAoB,EAC/B,UAAU,EACV,MAAM,cAAc,KAAK,SAAS,EAClC,iBAAiB;AAAA,EACnB,OAAO,OAAO;AAAA;AAOf,eAAsB,UAAU,CAC/B,IACA,WACA,OAC2B;AAAA,EAC3B,MAAM,SAAgC;AAAA,IACrC,YAAY;AAAA,IACZ,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAaA,eAAsB,OAAO,CAC5B,IACA,WACkC;AAAA,EAClC,MAAM,MAAM,MAAM,GAChB,WAAW,oBAAoB,EAC/B,UAAU,EACV,MAAM,cAAc,KAAK,SAAS,EAClC,iBAAiB;AAAA,EACnB,OAAO,OAAO;AAAA;AAOf,eAAsB,UAAU,CAC/B,IACA,WACA,OAC2B;AAAA,EAC3B,MAAM,SAAgC;AAAA,IACrC,YAAY;AAAA,IACZ,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,mBAAmB,MAAM,qBAAqB;AAAA,IAC9C,qBAAqB,MAAM,uBAAuB;AAAA,IAClD,eAAe,MAAM,iBAAiB;AAAA,IACtC,WAAW,MAAM,aAAa;AAAA,EAC/B;AAAA,EAEA,OAAO,GACL,WAAW,oBAAoB,EAC/B,OAAO,MAAM,EACb,WAAW,CAAC,OACZ,GAAG,OAAO,YAAY,EAAE,YAAY;AAAA,OAChC;AAAA,IACH,YAAY,IAAI;AAAA,EACjB,CAAC,CACF,EACC,aAAa,EACb,wBAAwB;AAAA;AAI3B,eAAsB,aAAa,CAClC,IACA,WACgB;AAAA,EAChB,MAAM,WAAW,IAAI,WAAW,EAAE,WAAW,IAAI,KAAO,CAAC;AAAA;AAQ1D,eAAsB,WAAW,CAChC,IACA,WACgB;AAAA,EAChB,MAAM,WAAW,IAAI,WAAW;AAAA,IAC/B,WAAW;AAAA,IACX,eAAe;AAAA,EAChB,CAAC;AAAA;AAIF,eAAsB,oBAAoB,CACzC,IACuB;AAAA,EACvB,MAAM,OAAO,MAAM,GACjB,WAAW,oBAAoB,EAC/B,OAAO,YAAY,EACnB,MAAM,aAAa,UAAU,IAAI,EACjC,QAAQ;AAAA,EACV,OAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;AAAA;",
|
|
8
|
+
"debugId": "61F9CA6C05B3563A64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -435,13 +434,6 @@ interface StorageUsage {
|
|
|
435
434
|
pct: number;
|
|
436
435
|
sparkline: SparklinePoint[];
|
|
437
436
|
}
|
|
438
|
-
interface AiUsage {
|
|
439
|
-
todayCount: number;
|
|
440
|
-
periodCount: number;
|
|
441
|
-
dailyCap: number;
|
|
442
|
-
pct: number;
|
|
443
|
-
sparkline: SparklinePoint[];
|
|
444
|
-
}
|
|
445
437
|
interface ProjectRow {
|
|
446
438
|
id: string;
|
|
447
439
|
slug: string;
|
|
@@ -456,13 +448,8 @@ interface ProjectRow {
|
|
|
456
448
|
bytes: number
|
|
457
449
|
pct: number
|
|
458
450
|
};
|
|
459
|
-
aiEvals: {
|
|
460
|
-
todayCount: number
|
|
461
|
-
pct: number
|
|
462
|
-
};
|
|
463
451
|
}
|
|
464
452
|
declare function getComputeUsage(db: Kysely<Database>, accountId: string, plan: string, periodStart: Date, now?: Date): Promise<ComputeUsage>;
|
|
465
453
|
declare function getStorageUsage(db: Kysely<Database>, accountId: string, plan: string, now?: Date): Promise<StorageUsage>;
|
|
466
|
-
declare function getAiUsage(_db: Kysely<Database>, _accountId: string, plan: string, _periodStart: Date, now?: Date): Promise<AiUsage>;
|
|
467
454
|
declare function getProjectBreakdown(db: Kysely<Database>, accountId: string, plan: string, periodStart: Date, now?: Date): Promise<ProjectRow[]>;
|
|
468
|
-
export { getStorageUsage, getProjectBreakdown, getComputeUsage,
|
|
455
|
+
export { getStorageUsage, getProjectBreakdown, getComputeUsage, StorageUsage, SparklinePoint, ProjectRow, ComputeUsage };
|
|
@@ -15,43 +15,6 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// src/pricing.ts
|
|
18
|
-
var MODEL_PRICING = {
|
|
19
|
-
anthropic: {
|
|
20
|
-
"claude-haiku-4-5": { inputPerMTokens: 1, outputPerMTokens: 5 },
|
|
21
|
-
"claude-haiku-4-5-20251001": { inputPerMTokens: 1, outputPerMTokens: 5 },
|
|
22
|
-
"claude-sonnet-4-6": { inputPerMTokens: 3, outputPerMTokens: 15 },
|
|
23
|
-
"claude-opus-4-7": { inputPerMTokens: 15, outputPerMTokens: 75 }
|
|
24
|
-
},
|
|
25
|
-
openai: {
|
|
26
|
-
"gpt-4.1": { inputPerMTokens: 2.5, outputPerMTokens: 10 },
|
|
27
|
-
"gpt-4o": { inputPerMTokens: 2.5, outputPerMTokens: 10 },
|
|
28
|
-
"gpt-4o-mini": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 }
|
|
29
|
-
},
|
|
30
|
-
google: {
|
|
31
|
-
"gemini-2.5-pro": { inputPerMTokens: 1.25, outputPerMTokens: 10 },
|
|
32
|
-
"gemini-2.5-flash": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 }
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
function computeUsdCost(provider, modelId, usage) {
|
|
36
|
-
const p = MODEL_PRICING[provider]?.[modelId];
|
|
37
|
-
if (!p)
|
|
38
|
-
return null;
|
|
39
|
-
return usage.inputTokens * p.inputPerMTokens / 1e6 + usage.outputTokens * p.outputPerMTokens / 1e6;
|
|
40
|
-
}
|
|
41
|
-
var AI_CAP_UNLIMITED = {
|
|
42
|
-
evalsPerDay: Number.POSITIVE_INFINITY,
|
|
43
|
-
overageMeterEventName: "ai_evals"
|
|
44
|
-
};
|
|
45
|
-
var AI_CAPS_BY_PLAN = {
|
|
46
|
-
hobby: { evalsPerDay: 50, overageMeterEventName: "ai_evals" },
|
|
47
|
-
launch: { evalsPerDay: 500, overageMeterEventName: "ai_evals" },
|
|
48
|
-
grow: { evalsPerDay: 1000, overageMeterEventName: "ai_evals" },
|
|
49
|
-
scale: { evalsPerDay: 2500, overageMeterEventName: "ai_evals" },
|
|
50
|
-
enterprise: AI_CAP_UNLIMITED
|
|
51
|
-
};
|
|
52
|
-
function getAiCapForPlan(plan) {
|
|
53
|
-
return AI_CAPS_BY_PLAN[plan] ?? AI_CAPS_BY_PLAN.hobby;
|
|
54
|
-
}
|
|
55
18
|
var BYTES_PER_GB = 1024 ** 3;
|
|
56
19
|
var COMPUTE_ALLOWANCE_BY_PLAN = {
|
|
57
20
|
hobby: Number.POSITIVE_INFINITY,
|
|
@@ -168,13 +131,6 @@ async function getStorageUsage(db, accountId, plan, now = new Date) {
|
|
|
168
131
|
sparkline
|
|
169
132
|
};
|
|
170
133
|
}
|
|
171
|
-
async function getAiUsage(_db, _accountId, plan, _periodStart, now = new Date) {
|
|
172
|
-
const dailyCap = getAiCapForPlan(plan).evalsPerDay;
|
|
173
|
-
const sparkline = [];
|
|
174
|
-
for (const day of lastNDays(14, now))
|
|
175
|
-
sparkline.push({ day, value: 0 });
|
|
176
|
-
return { todayCount: 0, periodCount: 0, dailyCap, pct: 0, sparkline };
|
|
177
|
-
}
|
|
178
134
|
async function getProjectBreakdown(db, accountId, plan, periodStart, now = new Date) {
|
|
179
135
|
const tenants = await db.selectFrom("tenants").select([
|
|
180
136
|
"id",
|
|
@@ -187,10 +143,8 @@ async function getProjectBreakdown(db, accountId, plan, periodStart, now = new D
|
|
|
187
143
|
]).where("account_id", "=", accountId).where("status", "!=", "deleted").orderBy("created_at", "desc").execute();
|
|
188
144
|
if (tenants.length === 0)
|
|
189
145
|
return [];
|
|
190
|
-
const aiByTenant = new Map;
|
|
191
146
|
const computeAllowance = getComputeAllowanceHours(plan);
|
|
192
147
|
const storageAllowance = getStorageAllowanceBytes(plan);
|
|
193
|
-
const aiDailyCap = getAiCapForPlan(plan).evalsPerDay;
|
|
194
148
|
return tenants.map((t) => {
|
|
195
149
|
const hours = computeActiveHours(periodStart, now, {
|
|
196
150
|
created_at: t.created_at,
|
|
@@ -198,7 +152,6 @@ async function getProjectBreakdown(db, accountId, plan, periodStart, now = new D
|
|
|
198
152
|
status: String(t.status)
|
|
199
153
|
}) * Number(t.cpus);
|
|
200
154
|
const bytes = Number(t.storage_used_mb ?? 0) * BYTES_PER_MB;
|
|
201
|
-
const todayAi = aiByTenant.get(t.id) ?? 0;
|
|
202
155
|
return {
|
|
203
156
|
id: t.id,
|
|
204
157
|
slug: t.slug,
|
|
@@ -206,17 +159,15 @@ async function getProjectBreakdown(db, accountId, plan, periodStart, now = new D
|
|
|
206
159
|
status: String(t.status),
|
|
207
160
|
subgraphCount: 0,
|
|
208
161
|
compute: { hours, pct: pct(hours, computeAllowance) },
|
|
209
|
-
storage: { bytes, pct: pct(bytes, storageAllowance) }
|
|
210
|
-
aiEvals: { todayCount: todayAi, pct: pct(todayAi, aiDailyCap) }
|
|
162
|
+
storage: { bytes, pct: pct(bytes, storageAllowance) }
|
|
211
163
|
};
|
|
212
164
|
});
|
|
213
165
|
}
|
|
214
166
|
export {
|
|
215
167
|
getStorageUsage,
|
|
216
168
|
getProjectBreakdown,
|
|
217
|
-
getComputeUsage
|
|
218
|
-
getAiUsage
|
|
169
|
+
getComputeUsage
|
|
219
170
|
};
|
|
220
171
|
|
|
221
|
-
//# debugId=
|
|
172
|
+
//# debugId=F9978FD423F1F8DF64756E2164756E21
|
|
222
173
|
//# sourceMappingURL=account-usage.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/pricing.ts", "../src/db/queries/account-usage.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"
|
|
6
|
-
"import { type Kysely, sql } from \"kysely\";\nimport {\n\
|
|
5
|
+
"// ── Per-tier compute + storage allowances (usage page) ──────────────\n//\n// \"Included\" amounts per billing period. Actual billing comes from\n// Stripe compute-hours + storage-overage meters; these constants power\n// the dashboard display and the approximation used by `/api/accounts/usage`.\n//\n// Values picked to match `project_supabase_pricing_model.md`:\n// Hobby — 50 h / 5 GB (Nano — free tier)\n// Launch — 500 h / 50 GB ($149/mo)\n// Grow — 1,000 h / 200 GB ($349/mo)\n// Scale — 2,500 h / 1 TB ($799/mo)\n// Enterprise — ∞ / ∞\n\nconst BYTES_PER_GB = 1024 ** 3;\n\n// Hobby has no compute-hour cap — auto-pause after 7d idle is the cap.\n// Paid tiers bill Stripe-metered hours past the included credit; the\n// hour equivalents below are approximate display values. Real billing\n// uses the `compute_hours` meter in Stripe, not these numbers.\nconst COMPUTE_ALLOWANCE_BY_PLAN: Record<string, number> = {\n\thobby: Number.POSITIVE_INFINITY,\n\tlaunch: 500,\n\tgrow: 1000,\n\tscale: 2500,\n\tenterprise: Number.POSITIVE_INFINITY,\n};\n\nconst STORAGE_ALLOWANCE_BYTES_BY_PLAN: Record<string, number> = {\n\thobby: 5 * BYTES_PER_GB,\n\tlaunch: 50 * BYTES_PER_GB,\n\tgrow: 200 * BYTES_PER_GB,\n\tscale: 1000 * BYTES_PER_GB,\n\tenterprise: Number.POSITIVE_INFINITY,\n};\n\n/** Included compute hours per billing period. Unknown → hobby. */\nexport function getComputeAllowanceHours(plan: string): number {\n\treturn COMPUTE_ALLOWANCE_BY_PLAN[plan] ?? COMPUTE_ALLOWANCE_BY_PLAN.hobby;\n}\n\n/** Included storage bytes. Unknown → hobby. */\nexport function getStorageAllowanceBytes(plan: string): number {\n\treturn (\n\t\tSTORAGE_ALLOWANCE_BYTES_BY_PLAN[plan] ??\n\t\tSTORAGE_ALLOWANCE_BYTES_BY_PLAN.hobby\n\t);\n}\n\n/** Whether this plan bills storage overage. Hobby has a hard cap\n * (no overage billing); paid tiers bill $2/GB over allowance. */\nexport function hasStorageOverage(plan: string): boolean {\n\treturn plan !== \"hobby\";\n}\n\n/** Base monthly price for a plan, in cents. Enterprise is custom → 0. */\nconst BASE_PRICE_CENTS_BY_PLAN: Record<string, number> = {\n\thobby: 0,\n\tlaunch: 14900,\n\tgrow: 34900,\n\tscale: 79900,\n\tenterprise: 0,\n};\n\nexport function getBasePriceCents(plan: string): number {\n\treturn BASE_PRICE_CENTS_BY_PLAN[plan] ?? 0;\n}\n\n/** Capitalized display name for a plan tier. */\nexport function getPlanDisplayName(plan: string): string {\n\treturn plan.charAt(0).toUpperCase() + plan.slice(1);\n}\n",
|
|
6
|
+
"import { type Kysely, sql } from \"kysely\";\nimport {\n\tgetComputeAllowanceHours,\n\tgetStorageAllowanceBytes,\n} from \"../../pricing.ts\";\nimport type { Database } from \"../types.ts\";\n\n/**\n * Rollup queries that power the `/platform/usage` page.\n *\n * Compute-hours approximation: each active tenant contributes\n * cpus × hours-in-period-while-active\n * where \"active\" is approximated from `last_active_at`. This undercounts\n * tenants that went idle between cron ticks and overcounts nothing.\n *\n * Actual Stripe billing happens in `packages/worker/src/jobs/compute-metering.ts`\n * — these numbers are for display only. Follow-up work: write-through\n * compute ledger so this query reads truth instead of estimating.\n */\n\nconst IDLE_GRACE_MS = 2 * 60 * 60 * 1000; // 2h\nconst BYTES_PER_MB = 1024 * 1024;\n\n// ── Types ────────────────────────────────────────────────────────────\n\nexport interface SparklinePoint {\n\tday: string; // YYYY-MM-DD\n\tvalue: number;\n}\n\nexport interface ComputeUsage {\n\tusedHours: number;\n\tallowanceHours: number;\n\tpct: number;\n\tsparkline: SparklinePoint[];\n}\n\nexport interface StorageUsage {\n\tusedBytes: number;\n\tallowanceBytes: number;\n\tpct: number;\n\tsparkline: SparklinePoint[];\n}\n\nexport interface ProjectRow {\n\tid: string;\n\tslug: string;\n\tname: string;\n\tstatus: string;\n\tsubgraphCount: number;\n\tcompute: { hours: number; pct: number };\n\tstorage: { bytes: number; pct: number };\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nfunction toDayKey(d: Date): string {\n\treturn d.toISOString().slice(0, 10);\n}\n\nfunction* lastNDays(n: number, endInclusive: Date): Generator<string> {\n\tconst end = new Date(endInclusive);\n\tfor (let i = n - 1; i >= 0; i--) {\n\t\tconst d = new Date(end);\n\t\td.setUTCDate(d.getUTCDate() - i);\n\t\tyield toDayKey(d);\n\t}\n}\n\nfunction computeActiveHours(\n\tperiodStart: Date,\n\tnow: Date,\n\ttenant: {\n\t\tcreated_at: Date;\n\t\tlast_active_at: Date;\n\t\tstatus: string;\n\t},\n): number {\n\tif (tenant.status !== \"active\") return 0;\n\tconst rangeStart = Math.max(\n\t\tperiodStart.getTime(),\n\t\ttenant.created_at.getTime(),\n\t);\n\tconst rangeEnd = Math.min(\n\t\tnow.getTime(),\n\t\ttenant.last_active_at.getTime() + IDLE_GRACE_MS,\n\t);\n\tif (rangeEnd <= rangeStart) return 0;\n\treturn (rangeEnd - rangeStart) / (1000 * 60 * 60);\n}\n\nfunction pct(used: number, allowance: number): number {\n\tif (!Number.isFinite(allowance) || allowance <= 0) return 0;\n\treturn Math.min((used / allowance) * 100, 100);\n}\n\n// ── Queries ──────────────────────────────────────────────────────────\n\nexport async function getComputeUsage(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tplan: string,\n\tperiodStart: Date,\n\tnow: Date = new Date(),\n): Promise<ComputeUsage> {\n\tconst tenants = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select([\"id\", \"cpus\", \"status\", \"created_at\", \"last_active_at\"])\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"!=\", \"deleted\")\n\t\t.execute();\n\n\tlet totalHours = 0;\n\tfor (const t of tenants) {\n\t\tconst hours = computeActiveHours(periodStart, now, {\n\t\t\tcreated_at: t.created_at,\n\t\t\tlast_active_at: t.last_active_at,\n\t\t\tstatus: String(t.status),\n\t\t});\n\t\ttotalHours += hours * Number(t.cpus);\n\t}\n\n\tconst allowance = getComputeAllowanceHours(plan);\n\n\t// 14-day sparkline: bucket the same formula per-day.\n\tconst sparkline: SparklinePoint[] = [];\n\tfor (const day of lastNDays(14, now)) {\n\t\tconst dayStart = new Date(`${day}T00:00:00.000Z`);\n\t\tconst dayEnd = new Date(`${day}T23:59:59.999Z`);\n\t\tlet dayHours = 0;\n\t\tfor (const t of tenants) {\n\t\t\tconst hours = computeActiveHours(dayStart, dayEnd, {\n\t\t\t\tcreated_at: t.created_at,\n\t\t\t\tlast_active_at: t.last_active_at,\n\t\t\t\tstatus: String(t.status),\n\t\t\t});\n\t\t\tdayHours += Math.min(hours, 24) * Number(t.cpus);\n\t\t}\n\t\tsparkline.push({ day, value: dayHours });\n\t}\n\n\treturn {\n\t\tusedHours: totalHours,\n\t\tallowanceHours: allowance,\n\t\tpct: pct(totalHours, allowance),\n\t\tsparkline,\n\t};\n}\n\nexport async function getStorageUsage(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tplan: string,\n\tnow: Date = new Date(),\n): Promise<StorageUsage> {\n\t// Current usage: sum of tenants.storage_used_mb for this account.\n\tconst current = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select(sql<string>`COALESCE(SUM(storage_used_mb), 0)`.as(\"mb\"))\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"!=\", \"deleted\")\n\t\t.executeTakeFirst();\n\n\tconst usedBytes = Number(current?.mb ?? 0) * BYTES_PER_MB;\n\tconst allowance = getStorageAllowanceBytes(plan);\n\n\t// 14-day sparkline: per-month snapshots only — fall back to a flat\n\t// line at the current value. When per-day storage history lands, swap\n\t// this for a real bucket query.\n\tconst sparkline: SparklinePoint[] = [];\n\tfor (const day of lastNDays(14, now)) {\n\t\tsparkline.push({ day, value: Number(current?.mb ?? 0) });\n\t}\n\n\treturn {\n\t\tusedBytes,\n\t\tallowanceBytes: allowance,\n\t\tpct: pct(usedBytes, allowance),\n\t\tsparkline,\n\t};\n}\n\nexport async function getProjectBreakdown(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tplan: string,\n\tperiodStart: Date,\n\tnow: Date = new Date(),\n): Promise<ProjectRow[]> {\n\tconst tenants = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select([\n\t\t\t\"id\",\n\t\t\t\"slug\",\n\t\t\t\"status\",\n\t\t\t\"cpus\",\n\t\t\t\"storage_used_mb\",\n\t\t\t\"created_at\",\n\t\t\t\"last_active_at\",\n\t\t])\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"!=\", \"deleted\")\n\t\t.orderBy(\"created_at\", \"desc\")\n\t\t.execute();\n\n\tif (tenants.length === 0) return [];\n\n\tconst computeAllowance = getComputeAllowanceHours(plan);\n\tconst storageAllowance = getStorageAllowanceBytes(plan);\n\n\treturn tenants.map((t) => {\n\t\tconst hours =\n\t\t\tcomputeActiveHours(periodStart, now, {\n\t\t\t\tcreated_at: t.created_at,\n\t\t\t\tlast_active_at: t.last_active_at,\n\t\t\t\tstatus: String(t.status),\n\t\t\t}) * Number(t.cpus);\n\t\tconst bytes = Number(t.storage_used_mb ?? 0) * BYTES_PER_MB;\n\n\t\treturn {\n\t\t\tid: t.id,\n\t\t\tslug: t.slug,\n\t\t\tname: t.slug,\n\t\t\tstatus: String(t.status),\n\t\t\t// subgraphCount not tracked at account-DB level (subgraphs live on\n\t\t\t// per-tenant DBs). Left at 0; later pass can ping each tenant API.\n\t\t\tsubgraphCount: 0,\n\t\t\tcompute: { hours, pct: pct(hours, computeAllowance) },\n\t\t\tstorage: { bytes, pct: pct(bytes, storageAllowance) },\n\t\t};\n\t});\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "F9978FD423F1F8DF64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -338,7 +338,6 @@ interface AccountSpendCapsTable {
|
|
|
338
338
|
monthly_cap_cents: number | null;
|
|
339
339
|
compute_cap_cents: number | null;
|
|
340
340
|
storage_cap_cents: number | null;
|
|
341
|
-
ai_cap_cents: number | null;
|
|
342
341
|
alert_threshold_pct: Generated<number>;
|
|
343
342
|
alert_sent_at: Date | null;
|
|
344
343
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -337,7 +337,6 @@ interface AccountSpendCapsTable {
|
|
|
337
337
|
monthly_cap_cents: number | null;
|
|
338
338
|
compute_cap_cents: number | null;
|
|
339
339
|
storage_cap_cents: number | null;
|
|
340
|
-
ai_cap_cents: number | null;
|
|
341
340
|
alert_threshold_pct: Generated<number>;
|
|
342
341
|
alert_sent_at: Date | null;
|
|
343
342
|
frozen_at: Date | null;
|
|
@@ -89,29 +89,73 @@ function isDedicatedMode() {
|
|
|
89
89
|
|
|
90
90
|
// src/crypto/secrets.ts
|
|
91
91
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "node:crypto";
|
|
92
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
appendFileSync,
|
|
94
|
+
closeSync,
|
|
95
|
+
existsSync,
|
|
96
|
+
openSync,
|
|
97
|
+
readFileSync,
|
|
98
|
+
unlinkSync
|
|
99
|
+
} from "node:fs";
|
|
93
100
|
import { resolve } from "node:path";
|
|
94
101
|
var KEY_ENV = "SECONDLAYER_SECRETS_KEY";
|
|
95
102
|
var IV_LEN = 12;
|
|
96
103
|
var TAG_LEN = 16;
|
|
104
|
+
function readExistingKey(envPath) {
|
|
105
|
+
if (!existsSync(envPath))
|
|
106
|
+
return null;
|
|
107
|
+
const contents = readFileSync(envPath, "utf8");
|
|
108
|
+
const match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);
|
|
109
|
+
return match ? match[1] : null;
|
|
110
|
+
}
|
|
111
|
+
var STALE_LOCK_MS = 1e4;
|
|
112
|
+
var POLL_MS = 25;
|
|
97
113
|
function bootstrapOssKey() {
|
|
98
114
|
const envPath = resolve(process.cwd(), ".env.local");
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
const existing = readExistingKey(envPath);
|
|
116
|
+
if (existing) {
|
|
117
|
+
process.env[KEY_ENV] = existing;
|
|
118
|
+
return existing;
|
|
119
|
+
}
|
|
120
|
+
const lockPath = `${envPath}.secret-bootstrap.lock`;
|
|
121
|
+
let lockFd = null;
|
|
122
|
+
try {
|
|
123
|
+
lockFd = openSync(lockPath, "wx", 384);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const e = err;
|
|
126
|
+
if (e.code !== "EEXIST")
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
if (lockFd === null) {
|
|
130
|
+
const deadline = Date.now() + STALE_LOCK_MS;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const key = readExistingKey(envPath);
|
|
133
|
+
if (key) {
|
|
134
|
+
process.env[KEY_ENV] = key;
|
|
135
|
+
return key;
|
|
136
|
+
}
|
|
137
|
+
Bun.sleepSync(POLL_MS);
|
|
105
138
|
}
|
|
139
|
+
try {
|
|
140
|
+
unlinkSync(lockPath);
|
|
141
|
+
} catch {}
|
|
142
|
+
return bootstrapOssKey();
|
|
106
143
|
}
|
|
107
|
-
|
|
108
|
-
|
|
144
|
+
try {
|
|
145
|
+
const hex = randomBytes2(32).toString("hex");
|
|
146
|
+
const line = `${existsSync(envPath) ? `
|
|
109
147
|
` : ""}${KEY_ENV}=${hex}
|
|
110
148
|
`;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
appendFileSync(envPath, line, { mode: 384 });
|
|
150
|
+
process.env[KEY_ENV] = hex;
|
|
151
|
+
console.log(`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`);
|
|
152
|
+
return hex;
|
|
153
|
+
} finally {
|
|
154
|
+
closeSync(lockFd);
|
|
155
|
+
try {
|
|
156
|
+
unlinkSync(lockPath);
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
115
159
|
}
|
|
116
160
|
function loadKey() {
|
|
117
161
|
let hex = process.env[KEY_ENV];
|
|
@@ -260,5 +304,5 @@ export {
|
|
|
260
304
|
createSubscription
|
|
261
305
|
};
|
|
262
306
|
|
|
263
|
-
//# debugId=
|
|
307
|
+
//# debugId=F3B788B22B9A594964756E2164756E21
|
|
264
308
|
//# sourceMappingURL=subscriptions.js.map
|