@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.
Files changed (51) hide show
  1. package/README.md +4 -2
  2. package/dist/src/crypto/secrets.js +58 -14
  3. package/dist/src/crypto/secrets.js.map +4 -4
  4. package/dist/src/db/index.d.ts +0 -1
  5. package/dist/src/db/queries/account-spend-caps.d.ts +0 -1
  6. package/dist/src/db/queries/account-spend-caps.js +1 -2
  7. package/dist/src/db/queries/account-spend-caps.js.map +3 -3
  8. package/dist/src/db/queries/account-usage.d.ts +1 -14
  9. package/dist/src/db/queries/account-usage.js +3 -52
  10. package/dist/src/db/queries/account-usage.js.map +4 -4
  11. package/dist/src/db/queries/accounts.d.ts +0 -1
  12. package/dist/src/db/queries/integrity.d.ts +0 -1
  13. package/dist/src/db/queries/projects.d.ts +0 -1
  14. package/dist/src/db/queries/provisioning-audit.d.ts +0 -1
  15. package/dist/src/db/queries/subgraph-gaps.d.ts +0 -1
  16. package/dist/src/db/queries/subgraphs.d.ts +0 -1
  17. package/dist/src/db/queries/subscriptions.d.ts +0 -1
  18. package/dist/src/db/queries/subscriptions.js +58 -14
  19. package/dist/src/db/queries/subscriptions.js.map +4 -4
  20. package/dist/src/db/queries/tenant-compute-addons.d.ts +0 -1
  21. package/dist/src/db/queries/tenants.d.ts +5 -6
  22. package/dist/src/db/queries/tenants.js +58 -14
  23. package/dist/src/db/queries/tenants.js.map +5 -5
  24. package/dist/src/db/queries/usage.d.ts +0 -1
  25. package/dist/src/db/schema.d.ts +0 -1
  26. package/dist/src/errors.d.ts +1 -6
  27. package/dist/src/errors.js +3 -11
  28. package/dist/src/errors.js.map +3 -3
  29. package/dist/src/index.d.ts +1 -9
  30. package/dist/src/index.js +4 -13
  31. package/dist/src/index.js.map +4 -4
  32. package/dist/src/mode.d.ts +1 -1
  33. package/dist/src/mode.js.map +1 -1
  34. package/dist/src/node/local-client.d.ts +0 -1
  35. package/dist/src/pricing.d.ts +1 -37
  36. package/dist/src/pricing.js +2 -42
  37. package/dist/src/pricing.js.map +3 -3
  38. package/dist/src/schemas/index.d.ts +0 -2
  39. package/dist/src/schemas/index.js +2 -3
  40. package/dist/src/schemas/index.js.map +3 -3
  41. package/dist/src/schemas/subgraphs.d.ts +0 -2
  42. package/dist/src/schemas/subgraphs.js +2 -3
  43. package/dist/src/schemas/subgraphs.js.map +3 -3
  44. package/migrations/0032_drop_streams_tables.ts +1 -1
  45. package/migrations/0036_tx_confirmed_notify.ts +2 -3
  46. package/migrations/0038_drop_workflow_tables.ts +3 -3
  47. package/migrations/0046_tenant_activity_signal.ts +2 -3
  48. package/migrations/0051_workflow_ai_usage_daily.ts +4 -8
  49. package/migrations/0057_subscriptions.ts +1 -1
  50. package/migrations/0058_drop_ai_cap_cents.ts +15 -0
  51. 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+Postgres), Zod schemas, HMAC signing, Stacks node clients.
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 { appendFileSync, existsSync, readFileSync } from "node:fs";
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
- 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];
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
- const hex = randomBytes(32).toString("hex");
54
- const line = `${existsSync(envPath) ? `
90
+ try {
91
+ const hex = randomBytes(32).toString("hex");
92
+ const line = `${existsSync(envPath) ? `
55
93
  ` : ""}${KEY_ENV}=${hex}
56
94
  `;
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;
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=982B15A94DFBA98464756E2164756E21
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, 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"
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": "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",
9
- "debugId": "982B15A94DFBA98464756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "B9CC14B7680BC22364756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -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=ECBEFE5FC18A7EFB64756E2164756E21
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\tai_cap_cents: patch.ai_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"
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,cAAc,MAAM,gBAAgB;AAAA,IACpC,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": "ECBEFE5FC18A7EFB64756E2164756E21",
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, getAiUsage, StorageUsage, SparklinePoint, ProjectRow, ComputeUsage, AiUsage };
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=D94D306CAF5E896064756E2164756E21
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
- "/**\n * Token → USD pricing constants for internal observability.\n *\n * **Not customer billing.** Tier pricing covers compute; these constants\n * let the dashboard display \"~$X spent in AI today\" and let us track\n * gross-margin internally. Update manually when providers change prices\n * (roughly twice per year).\n *\n * Source: public provider pricing pages as of 2026-04-17.\n */\n\nexport interface ModelPricing {\n\t/** USD per 1M input tokens */\n\tinputPerMTokens: number;\n\t/** USD per 1M output tokens */\n\toutputPerMTokens: number;\n}\n\nexport const MODEL_PRICING: Record<string, Record<string, ModelPricing>> = {\n\tanthropic: {\n\t\t\"claude-haiku-4-5\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-haiku-4-5-20251001\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-sonnet-4-6\": { inputPerMTokens: 3, outputPerMTokens: 15 },\n\t\t\"claude-opus-4-7\": { inputPerMTokens: 15, outputPerMTokens: 75 },\n\t},\n\topenai: {\n\t\t\"gpt-4.1\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o-mini\": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 },\n\t},\n\tgoogle: {\n\t\t\"gemini-2.5-pro\": { inputPerMTokens: 1.25, outputPerMTokens: 10 },\n\t\t\"gemini-2.5-flash\": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 },\n\t},\n};\n\nexport interface TokenUsage {\n\tinputTokens: number;\n\toutputTokens: number;\n}\n\n/**\n * Compute approximate USD cost for an AI step. Returns `null` if the\n * (provider, model) pair isn't in the pricing table — the caller should\n * display \"-\" rather than \"$0\".\n */\nexport function computeUsdCost(\n\tprovider: string,\n\tmodelId: string,\n\tusage: TokenUsage,\n): number | null {\n\tconst p = MODEL_PRICING[provider]?.[modelId];\n\tif (!p) return null;\n\treturn (\n\t\t(usage.inputTokens * p.inputPerMTokens) / 1_000_000 +\n\t\t(usage.outputTokens * p.outputPerMTokens) / 1_000_000\n\t);\n}\n\n// ── Per-tier AI eval caps (workflow-runner) ───────────────────────────\n//\n// Daily `step.ai` budget by tier. Hit the cap → runner throws\n// AI_CAP_REACHED and the step fails cleanly so condition-only paths\n// continue. Overage (past cap) bills to the `ai_evals` Stripe meter on\n// paid tiers.\n//\n// Caps set conservatively so Pro Micro stays margin-positive at\n// realistic AI utilization. Raise based on data, not aspiration.\n\nexport interface AiCap {\n\t/** Max step.ai / generateText / generateObject calls per UTC day. */\n\tevalsPerDay: number;\n\t/** Stripe meter events emitted for overage past this cap. */\n\toverageMeterEventName: \"ai_evals\";\n}\n\nconst AI_CAP_UNLIMITED: AiCap = {\n\tevalsPerDay: Number.POSITIVE_INFINITY,\n\toverageMeterEventName: \"ai_evals\",\n};\n\nconst AI_CAPS_BY_PLAN: Record<string, AiCap> = {\n\thobby: { evalsPerDay: 50, overageMeterEventName: \"ai_evals\" },\n\tlaunch: { evalsPerDay: 500, overageMeterEventName: \"ai_evals\" },\n\tgrow: { evalsPerDay: 1000, overageMeterEventName: \"ai_evals\" },\n\tscale: { evalsPerDay: 2500, overageMeterEventName: \"ai_evals\" },\n\tenterprise: AI_CAP_UNLIMITED,\n};\n\n/** Resolve the daily AI eval cap for a plan. Unknown plans get Hobby's\n * cap so a stray DB value can't accidentally become unlimited. */\nexport function getAiCapForPlan(plan: string): AiCap {\n\treturn AI_CAPS_BY_PLAN[plan] ?? AI_CAPS_BY_PLAN.hobby;\n}\n\n// ── 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\tgetAiCapForPlan,\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 AiUsage {\n\ttodayCount: number;\n\tperiodCount: number;\n\tdailyCap: 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\taiEvals: { todayCount: 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 getAiUsage(\n\t_db: Kysely<Database>,\n\t_accountId: string,\n\tplan: string,\n\t_periodStart: Date,\n\tnow: Date = new Date(),\n): Promise<AiUsage> {\n\tconst dailyCap = getAiCapForPlan(plan).evalsPerDay;\n\tconst sparkline: SparklinePoint[] = [];\n\tfor (const day of lastNDays(14, now)) sparkline.push({ day, value: 0 });\n\treturn { todayCount: 0, periodCount: 0, dailyCap, pct: 0, sparkline };\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 aiByTenant = new Map<string, number>();\n\n\tconst computeAllowance = getComputeAllowanceHours(plan);\n\tconst storageAllowance = getStorageAllowanceBytes(plan);\n\tconst aiDailyCap = getAiCapForPlan(plan).evalsPerDay;\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\t\tconst todayAi = aiByTenant.get(t.id) ?? 0;\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\taiEvals: { todayCount: todayAi, pct: pct(todayAi, aiDailyCap) },\n\t\t};\n\t});\n}\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": "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",
9
- "debugId": "D94D306CAF5E896064756E2164756E21",
8
+ "mappings": ";;;;;;;;;;;;;;;;;AAaA,IAAM,eAAe,QAAQ;AAM7B,IAAM,4BAAoD;AAAA,EACzD,OAAO,OAAO;AAAA,EACd,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,YAAY,OAAO;AACpB;AAEA,IAAM,kCAA0D;AAAA,EAC/D,OAAO,IAAI;AAAA,EACX,QAAQ,KAAK;AAAA,EACb,MAAM,MAAM;AAAA,EACZ,OAAO,OAAO;AAAA,EACd,YAAY,OAAO;AACpB;AAGO,SAAS,wBAAwB,CAAC,MAAsB;AAAA,EAC9D,OAAO,0BAA0B,SAAS,0BAA0B;AAAA;AAI9D,SAAS,wBAAwB,CAAC,MAAsB;AAAA,EAC9D,OACC,gCAAgC,SAChC,gCAAgC;AAAA;AAM3B,SAAS,iBAAiB,CAAC,MAAuB;AAAA,EACxD,OAAO,SAAS;AAAA;AAIjB,IAAM,2BAAmD;AAAA,EACxD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,YAAY;AACb;AAEO,SAAS,iBAAiB,CAAC,MAAsB;AAAA,EACvD,OAAO,yBAAyB,SAAS;AAAA;AAInC,SAAS,kBAAkB,CAAC,MAAsB;AAAA,EACxD,OAAO,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AAAA;;;ACrEnD;AAoBA,IAAM,gBAAgB,IAAI,KAAK,KAAK;AACpC,IAAM,eAAe,OAAO;AAmC5B,SAAS,QAAQ,CAAC,GAAiB;AAAA,EAClC,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA;AAGnC,UAAU,SAAS,CAAC,GAAW,cAAuC;AAAA,EACrE,MAAM,MAAM,IAAI,KAAK,YAAY;AAAA,EACjC,SAAS,IAAI,IAAI,EAAG,KAAK,GAAG,KAAK;AAAA,IAChC,MAAM,IAAI,IAAI,KAAK,GAAG;AAAA,IACtB,EAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAAA,IAC/B,MAAM,SAAS,CAAC;AAAA,EACjB;AAAA;AAGD,SAAS,kBAAkB,CAC1B,aACA,KACA,QAKS;AAAA,EACT,IAAI,OAAO,WAAW;AAAA,IAAU,OAAO;AAAA,EACvC,MAAM,aAAa,KAAK,IACvB,YAAY,QAAQ,GACpB,OAAO,WAAW,QAAQ,CAC3B;AAAA,EACA,MAAM,WAAW,KAAK,IACrB,IAAI,QAAQ,GACZ,OAAO,eAAe,QAAQ,IAAI,aACnC;AAAA,EACA,IAAI,YAAY;AAAA,IAAY,OAAO;AAAA,EACnC,QAAQ,WAAW,eAAe,OAAO,KAAK;AAAA;AAG/C,SAAS,GAAG,CAAC,MAAc,WAA2B;AAAA,EACrD,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa;AAAA,IAAG,OAAO;AAAA,EAC1D,OAAO,KAAK,IAAK,OAAO,YAAa,KAAK,GAAG;AAAA;AAK9C,eAAsB,eAAe,CACpC,IACA,WACA,MACA,aACA,MAAY,IAAI,MACQ;AAAA,EACxB,MAAM,UAAU,MAAM,GACpB,WAAW,SAAS,EACpB,OAAO,CAAC,MAAM,QAAQ,UAAU,cAAc,gBAAgB,CAAC,EAC/D,MAAM,cAAc,KAAK,SAAS,EAClC,MAAM,UAAU,MAAM,SAAS,EAC/B,QAAQ;AAAA,EAEV,IAAI,aAAa;AAAA,EACjB,WAAW,KAAK,SAAS;AAAA,IACxB,MAAM,QAAQ,mBAAmB,aAAa,KAAK;AAAA,MAClD,YAAY,EAAE;AAAA,MACd,gBAAgB,EAAE;AAAA,MAClB,QAAQ,OAAO,EAAE,MAAM;AAAA,IACxB,CAAC;AAAA,IACD,cAAc,QAAQ,OAAO,EAAE,IAAI;AAAA,EACpC;AAAA,EAEA,MAAM,YAAY,yBAAyB,IAAI;AAAA,EAG/C,MAAM,YAA8B,CAAC;AAAA,EACrC,WAAW,OAAO,UAAU,IAAI,GAAG,GAAG;AAAA,IACrC,MAAM,WAAW,IAAI,KAAK,GAAG,mBAAmB;AAAA,IAChD,MAAM,SAAS,IAAI,KAAK,GAAG,mBAAmB;AAAA,IAC9C,IAAI,WAAW;AAAA,IACf,WAAW,KAAK,SAAS;AAAA,MACxB,MAAM,QAAQ,mBAAmB,UAAU,QAAQ;AAAA,QAClD,YAAY,EAAE;AAAA,QACd,gBAAgB,EAAE;AAAA,QAClB,QAAQ,OAAO,EAAE,MAAM;AAAA,MACxB,CAAC;AAAA,MACD,YAAY,KAAK,IAAI,OAAO,EAAE,IAAI,OAAO,EAAE,IAAI;AAAA,IAChD;AAAA,IACA,UAAU,KAAK,EAAE,KAAK,OAAO,SAAS,CAAC;AAAA,EACxC;AAAA,EAEA,OAAO;AAAA,IACN,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,KAAK,IAAI,YAAY,SAAS;AAAA,IAC9B;AAAA,EACD;AAAA;AAGD,eAAsB,eAAe,CACpC,IACA,WACA,MACA,MAAY,IAAI,MACQ;AAAA,EAExB,MAAM,UAAU,MAAM,GACpB,WAAW,SAAS,EACpB,OAAO,uCAA+C,GAAG,IAAI,CAAC,EAC9D,MAAM,cAAc,KAAK,SAAS,EAClC,MAAM,UAAU,MAAM,SAAS,EAC/B,iBAAiB;AAAA,EAEnB,MAAM,YAAY,OAAO,SAAS,MAAM,CAAC,IAAI;AAAA,EAC7C,MAAM,YAAY,yBAAyB,IAAI;AAAA,EAK/C,MAAM,YAA8B,CAAC;AAAA,EACrC,WAAW,OAAO,UAAU,IAAI,GAAG,GAAG;AAAA,IACrC,UAAU,KAAK,EAAE,KAAK,OAAO,OAAO,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,EACxD;AAAA,EAEA,OAAO;AAAA,IACN;AAAA,IACA,gBAAgB;AAAA,IAChB,KAAK,IAAI,WAAW,SAAS;AAAA,IAC7B;AAAA,EACD;AAAA;AAGD,eAAsB,mBAAmB,CACxC,IACA,WACA,MACA,aACA,MAAY,IAAI,MACQ;AAAA,EACxB,MAAM,UAAU,MAAM,GACpB,WAAW,SAAS,EACpB,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC,EACA,MAAM,cAAc,KAAK,SAAS,EAClC,MAAM,UAAU,MAAM,SAAS,EAC/B,QAAQ,cAAc,MAAM,EAC5B,QAAQ;AAAA,EAEV,IAAI,QAAQ,WAAW;AAAA,IAAG,OAAO,CAAC;AAAA,EAElC,MAAM,mBAAmB,yBAAyB,IAAI;AAAA,EACtD,MAAM,mBAAmB,yBAAyB,IAAI;AAAA,EAEtD,OAAO,QAAQ,IAAI,CAAC,MAAM;AAAA,IACzB,MAAM,QACL,mBAAmB,aAAa,KAAK;AAAA,MACpC,YAAY,EAAE;AAAA,MACd,gBAAgB,EAAE;AAAA,MAClB,QAAQ,OAAO,EAAE,MAAM;AAAA,IACxB,CAAC,IAAI,OAAO,EAAE,IAAI;AAAA,IACnB,MAAM,QAAQ,OAAO,EAAE,mBAAmB,CAAC,IAAI;AAAA,IAE/C,OAAO;AAAA,MACN,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,QAAQ,OAAO,EAAE,MAAM;AAAA,MAGvB,eAAe;AAAA,MACf,SAAS,EAAE,OAAO,KAAK,IAAI,OAAO,gBAAgB,EAAE;AAAA,MACpD,SAAS,EAAE,OAAO,KAAK,IAAI,OAAO,gBAAgB,EAAE;AAAA,IACrD;AAAA,GACA;AAAA;",
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 { appendFileSync, existsSync, readFileSync } from "node:fs";
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
- if (existsSync(envPath)) {
100
- const contents = readFileSync(envPath, "utf8");
101
- const match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);
102
- if (match) {
103
- process.env[KEY_ENV] = match[1];
104
- return match[1];
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
- const hex = randomBytes2(32).toString("hex");
108
- const line = `${existsSync(envPath) ? `
144
+ try {
145
+ const hex = randomBytes2(32).toString("hex");
146
+ const line = `${existsSync(envPath) ? `
109
147
  ` : ""}${KEY_ENV}=${hex}
110
148
  `;
111
- appendFileSync(envPath, line, { mode: 384 });
112
- process.env[KEY_ENV] = hex;
113
- console.log(`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`);
114
- return hex;
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=6C6C97A09D12DAB864756E2164756E21
307
+ //# debugId=F3B788B22B9A594964756E2164756E21
264
308
  //# sourceMappingURL=subscriptions.js.map