@secondlayer/shared 2.0.0 → 3.0.0-alpha.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 (78) hide show
  1. package/README.md +2 -2
  2. package/dist/src/db/index.d.ts +64 -130
  3. package/dist/src/db/index.js.map +2 -2
  4. package/dist/src/db/jsonb.d.ts +5 -1
  5. package/dist/src/db/jsonb.js.map +2 -2
  6. package/dist/src/db/queries/account-spend-caps.d.ts +379 -0
  7. package/dist/src/db/queries/account-spend-caps.js +60 -0
  8. package/dist/src/db/queries/account-spend-caps.js.map +10 -0
  9. package/dist/src/db/queries/account-usage.d.ts +403 -0
  10. package/dist/src/db/queries/account-usage.js +222 -0
  11. package/dist/src/db/queries/account-usage.js.map +11 -0
  12. package/dist/src/db/queries/accounts.d.ts +61 -108
  13. package/dist/src/db/queries/accounts.js +15 -1
  14. package/dist/src/db/queries/accounts.js.map +3 -3
  15. package/dist/src/db/queries/integrity.d.ts +47 -107
  16. package/dist/src/db/queries/projects.d.ts +47 -107
  17. package/dist/src/db/queries/{workflows.d.ts → provisioning-audit.d.ts} +70 -142
  18. package/dist/src/db/queries/provisioning-audit.js +40 -0
  19. package/dist/src/db/queries/provisioning-audit.js.map +10 -0
  20. package/dist/src/db/queries/subgraph-gaps.d.ts +47 -107
  21. package/dist/src/db/queries/subgraphs.d.ts +47 -108
  22. package/dist/src/db/queries/subgraphs.js +2 -3
  23. package/dist/src/db/queries/subgraphs.js.map +4 -4
  24. package/dist/src/db/queries/{marketplace.d.ts → tenant-compute-addons.d.ts} +66 -159
  25. package/dist/src/db/queries/tenant-compute-addons.js +47 -0
  26. package/dist/src/db/queries/tenant-compute-addons.js.map +10 -0
  27. package/dist/src/db/queries/tenants.d.ts +67 -110
  28. package/dist/src/db/queries/tenants.js +35 -6
  29. package/dist/src/db/queries/tenants.js.map +3 -3
  30. package/dist/src/db/queries/usage.d.ts +48 -132
  31. package/dist/src/db/queries/usage.js +5 -64
  32. package/dist/src/db/queries/usage.js.map +4 -5
  33. package/dist/src/db/schema.d.ts +59 -129
  34. package/dist/src/errors.d.ts +8 -7
  35. package/dist/src/errors.js +11 -12
  36. package/dist/src/errors.js.map +3 -3
  37. package/dist/src/index.d.ts +98 -212
  38. package/dist/src/index.js +69 -80
  39. package/dist/src/index.js.map +6 -6
  40. package/dist/src/mode.d.ts +4 -5
  41. package/dist/src/mode.js.map +2 -2
  42. package/dist/src/node/local-client.d.ts +47 -107
  43. package/dist/src/pricing.d.ts +20 -1
  44. package/dist/src/pricing.js +58 -1
  45. package/dist/src/pricing.js.map +3 -3
  46. package/dist/src/schemas/accounts.d.ts +14 -0
  47. package/dist/src/schemas/{marketplace.js → accounts.js} +4 -14
  48. package/dist/src/schemas/accounts.js.map +10 -0
  49. package/dist/src/schemas/index.d.ts +28 -77
  50. package/dist/src/schemas/index.js +59 -69
  51. package/dist/src/schemas/index.js.map +4 -4
  52. package/migrations/0043_tenant_usage_monthly.ts +36 -0
  53. package/migrations/0044_provisioning_audit_log.ts +40 -0
  54. package/migrations/0045_drop_marketplace_columns.ts +47 -0
  55. package/migrations/0046_tenant_activity_signal.ts +47 -0
  56. package/migrations/0047_usage_daily_tenant_id.ts +73 -0
  57. package/migrations/0048_tenant_compute_addons.ts +49 -0
  58. package/migrations/0049_accounts_stripe_customer_id.ts +30 -0
  59. package/migrations/0050_account_spend_caps.ts +45 -0
  60. package/migrations/0051_workflow_ai_usage_daily.ts +40 -0
  61. package/migrations/0052_sentries.ts +61 -0
  62. package/migrations/0053_workflow_runtime.ts +88 -0
  63. package/migrations/0054_accounts_plan_hobby.ts +32 -0
  64. package/migrations/0055_ai_usage_account_scope.ts +108 -0
  65. package/migrations/0056_drop_workflow_sentry_residuals.ts +23 -0
  66. package/package.json +33 -21
  67. package/dist/src/db/queries/marketplace.js +0 -139
  68. package/dist/src/db/queries/marketplace.js.map +0 -10
  69. package/dist/src/db/queries/workflows.js +0 -260
  70. package/dist/src/db/queries/workflows.js.map +0 -12
  71. package/dist/src/lib/plans.d.ts +0 -9
  72. package/dist/src/lib/plans.js +0 -37
  73. package/dist/src/lib/plans.js.map +0 -10
  74. package/dist/src/schemas/marketplace.d.ts +0 -63
  75. package/dist/src/schemas/marketplace.js.map +0 -10
  76. package/dist/src/schemas/workflows.d.ts +0 -70
  77. package/dist/src/schemas/workflows.js +0 -43
  78. package/dist/src/schemas/workflows.js.map +0 -10
@@ -3,9 +3,9 @@
3
3
  "sources": ["../src/crypto/secrets.ts", "../src/db/queries/tenants.ts"],
4
4
  "sourcesContent": [
5
5
  "import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\n\n/**\n * AES-256-GCM symmetric envelope for workflow signer secrets.\n *\n * Ciphertext layout: `iv (12 bytes) || authTag (16 bytes) || ciphertext`\n *\n * The key comes from `SECONDLAYER_SECRETS_KEY` — 32 bytes hex. Callers must\n * load + cache the key once per process. Rotation strategy: when a customer\n * wants to rotate keys, re-encrypt all rows with the new key and swap the\n * env var. Not zero-downtime, but acceptable at v2 scale.\n *\n * For real KMS (AWS KMS, HashiCorp Vault, GCP KMS), wrap the same byte\n * layout behind an `EncryptSecret` / `DecryptSecret` interface in the\n * runner and swap the implementation at startup.\n */\n\nconst KEY_ENV = \"SECONDLAYER_SECRETS_KEY\";\nconst IV_LEN = 12;\nconst TAG_LEN = 16;\n\nfunction loadKey(): Buffer {\n\tconst hex = process.env[KEY_ENV];\n\tif (!hex) {\n\t\tthrow new Error(\n\t\t\t`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`,\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",
6
- "import type { Kysely } from \"kysely\";\nimport { decryptSecret, encryptSecret } from \"../../crypto/secrets.ts\";\nimport type { Database, InsertTenant, Tenant, TenantStatus } from \"../types.ts\";\n\n/**\n * Tenant registry queries. Encrypted columns are stored as `bytea` and\n * transparently encrypted/decrypted via `encryptSecret`/`decryptSecret`.\n *\n * Never return decrypted values from listTenants — only `getTenantCredentials`\n * surfaces plaintext, and only when explicitly called by a caller that\n * needs to hand creds to a CLI or dashboard session.\n */\n\nexport interface NewTenantInput {\n\taccountId: string;\n\tslug: string;\n\tplan: string;\n\tcpus: number;\n\tmemoryMb: number;\n\tstorageLimitMb: number;\n\tpgContainerId: string;\n\tapiContainerId: string;\n\tprocessorContainerId: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n\ttrialEndsAt: Date;\n\tprojectId?: string;\n}\n\nexport async function insertTenant(\n\tdb: Kysely<Database>,\n\tinput: NewTenantInput,\n): Promise<Tenant> {\n\tconst row: InsertTenant = {\n\t\taccount_id: input.accountId,\n\t\tslug: input.slug,\n\t\tstatus: \"active\",\n\t\tplan: input.plan,\n\t\tcpus: input.cpus,\n\t\tmemory_mb: input.memoryMb,\n\t\tstorage_limit_mb: input.storageLimitMb,\n\t\tpg_container_id: input.pgContainerId,\n\t\tapi_container_id: input.apiContainerId,\n\t\tprocessor_container_id: input.processorContainerId,\n\t\ttarget_database_url_enc: encryptSecret(input.targetDatabaseUrl),\n\t\ttenant_jwt_secret_enc: encryptSecret(input.tenantJwtSecret),\n\t\tanon_key_enc: encryptSecret(input.anonKey),\n\t\tservice_key_enc: encryptSecret(input.serviceKey),\n\t\tapi_url_internal: input.apiUrlInternal,\n\t\tapi_url_public: input.apiUrlPublic,\n\t\ttrial_ends_at: input.trialEndsAt,\n\t\tproject_id: input.projectId ?? null,\n\t};\n\treturn db\n\t\t.insertInto(\"tenants\")\n\t\t.values(row)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getTenantByAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"<>\", \"deleted\")\n\t\t.orderBy(\"created_at\", \"desc\")\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function getTenantBySlug(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function listTenantsByStatus(\n\tdb: Kysely<Database>,\n\tstatus: TenantStatus,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", status)\n\t\t.execute();\n}\n\nexport async function listExpiredTrials(\n\tdb: Kysely<Database>,\n\tnow: Date = new Date(),\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"in\", [\"provisioning\", \"active\"])\n\t\t.where(\"trial_ends_at\", \"<\", now)\n\t\t.execute();\n}\n\nexport async function listSuspendedOlderThan(\n\tdb: Kysely<Database>,\n\tolderThan: Date,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"suspended\")\n\t\t.where(\"suspended_at\", \"<\", olderThan)\n\t\t.execute();\n}\n\nexport async function setTenantStatus(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstatus: TenantStatus,\n): Promise<void> {\n\tconst patch: Record<string, unknown> = {\n\t\tstatus,\n\t\tupdated_at: new Date(),\n\t};\n\tif (status === \"suspended\") patch.suspended_at = new Date();\n\tif (status === \"active\") patch.suspended_at = null;\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\nexport async function recordHealthCheck(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstorageUsedMb: number | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tlast_health_check_at: new Date(),\n\t\t\tstorage_used_mb: storageUsedMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport async function updateTenantPlan(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tplan: string,\n\tcpus: number,\n\tmemoryMb: number,\n\tstorageLimitMb: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tplan,\n\t\t\tcpus,\n\t\t\tmemory_mb: memoryMb,\n\t\t\tstorage_limit_mb: storageLimitMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport type RotateType = \"service\" | \"anon\" | \"both\";\n\n/**\n * Bump the selected gen counter(s) by 1 and return the new values.\n * Used by the key-rotate endpoint to force the tenant API to reject\n * previously-issued tokens of the rotated role(s).\n */\nexport async function bumpTenantKeyGen(\n\tdb: Kysely<Database>,\n\tslug: string,\n\ttype: RotateType,\n): Promise<{ serviceGen: number; anonGen: number }> {\n\tconst bumpService = type === \"service\" || type === \"both\";\n\tconst bumpAnon = type === \"anon\" || type === \"both\";\n\tconst row = await db\n\t\t.updateTable(\"tenants\")\n\t\t.set((eb) => ({\n\t\t\tservice_gen: bumpService\n\t\t\t\t? eb(\"service_gen\", \"+\", 1)\n\t\t\t\t: eb.ref(\"service_gen\"),\n\t\t\tanon_gen: bumpAnon ? eb(\"anon_gen\", \"+\", 1) : eb.ref(\"anon_gen\"),\n\t\t\tupdated_at: new Date(),\n\t\t}))\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.returning([\"service_gen\", \"anon_gen\"])\n\t\t.executeTakeFirstOrThrow();\n\treturn { serviceGen: row.service_gen, anonGen: row.anon_gen };\n}\n\n/**\n * Replace the encrypted key columns after a successful rotate. Only the\n * rotated column(s) are written — the other stays untouched.\n */\nexport async function updateTenantKeys(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tkeys: { serviceKey?: string; anonKey?: string },\n): Promise<void> {\n\tconst patch: Record<string, unknown> = { updated_at: new Date() };\n\tif (keys.serviceKey) patch.service_key_enc = encryptSecret(keys.serviceKey);\n\tif (keys.anonKey) patch.anon_key_enc = encryptSecret(keys.anonKey);\n\tif (Object.keys(patch).length === 1) return; // only updated_at — nothing to write\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\n/**\n * Hard-delete a tenant row. Call only AFTER the provisioner has torn down\n * containers + volume; otherwise orphaned resources linger. Returns whether\n * a row was actually deleted.\n */\nexport async function deleteTenant(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<boolean> {\n\tconst res = await db\n\t\t.deleteFrom(\"tenants\")\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn (res.numDeletedRows ?? 0n) > 0n;\n}\n\nexport interface TenantCredentials {\n\tslug: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n}\n\n/**\n * Decrypts the four encrypted columns and returns them plaintext. Call\n * this only when surfacing credentials to an authorized caller (dashboard,\n * CLI). Never log the returned object.\n */\nexport async function getTenantCredentials(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<TenantCredentials | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select([\n\t\t\t\"slug\",\n\t\t\t\"target_database_url_enc\",\n\t\t\t\"tenant_jwt_secret_enc\",\n\t\t\t\"anon_key_enc\",\n\t\t\t\"service_key_enc\",\n\t\t\t\"api_url_internal\",\n\t\t\t\"api_url_public\",\n\t\t])\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\tif (!row) return null;\n\treturn {\n\t\tslug: row.slug,\n\t\ttargetDatabaseUrl: decryptSecret(row.target_database_url_enc),\n\t\ttenantJwtSecret: decryptSecret(row.tenant_jwt_secret_enc),\n\t\tanonKey: decryptSecret(row.anon_key_enc),\n\t\tserviceKey: decryptSecret(row.service_key_enc),\n\t\tapiUrlInternal: row.api_url_internal,\n\t\tapiUrlPublic: row.api_url_public,\n\t};\n}\n"
6
+ "import { type Kysely, sql } from \"kysely\";\nimport { decryptSecret, encryptSecret } from \"../../crypto/secrets.ts\";\nimport type { Database, InsertTenant, Tenant, TenantStatus } from \"../types.ts\";\n\n/**\n * Tenant registry queries. Encrypted columns are stored as `bytea` and\n * transparently encrypted/decrypted via `encryptSecret`/`decryptSecret`.\n *\n * Never return decrypted values from listTenants — only `getTenantCredentials`\n * surfaces plaintext, and only when explicitly called by a caller that\n * needs to hand creds to a CLI or dashboard session.\n */\n\nexport interface NewTenantInput {\n\taccountId: string;\n\tslug: string;\n\tplan: string;\n\tcpus: number;\n\tmemoryMb: number;\n\tstorageLimitMb: number;\n\tpgContainerId: string;\n\tapiContainerId: string;\n\tprocessorContainerId: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n\tprojectId?: string;\n}\n\nexport async function insertTenant(\n\tdb: Kysely<Database>,\n\tinput: NewTenantInput,\n): Promise<Tenant> {\n\tconst row: InsertTenant = {\n\t\taccount_id: input.accountId,\n\t\tslug: input.slug,\n\t\tstatus: \"active\",\n\t\tplan: input.plan,\n\t\tcpus: input.cpus,\n\t\tmemory_mb: input.memoryMb,\n\t\tstorage_limit_mb: input.storageLimitMb,\n\t\tpg_container_id: input.pgContainerId,\n\t\tapi_container_id: input.apiContainerId,\n\t\tprocessor_container_id: input.processorContainerId,\n\t\ttarget_database_url_enc: encryptSecret(input.targetDatabaseUrl),\n\t\ttenant_jwt_secret_enc: encryptSecret(input.tenantJwtSecret),\n\t\tanon_key_enc: encryptSecret(input.anonKey),\n\t\tservice_key_enc: encryptSecret(input.serviceKey),\n\t\tapi_url_internal: input.apiUrlInternal,\n\t\tapi_url_public: input.apiUrlPublic,\n\t\tproject_id: input.projectId ?? null,\n\t};\n\treturn db\n\t\t.insertInto(\"tenants\")\n\t\t.values(row)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getTenantByAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"status\", \"<>\", \"deleted\")\n\t\t.orderBy(\"created_at\", \"desc\")\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function getTenantBySlug(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<Tenant | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn row ?? null;\n}\n\nexport async function listTenantsByStatus(\n\tdb: Kysely<Database>,\n\tstatus: TenantStatus,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", status)\n\t\t.execute();\n}\n\n/**\n * Tenants considered \"idle\" for auto-pause on the Hobby tier. Active = any\n * successful tenant-API query OR workflow run wrote `last_active_at` within\n * the threshold.\n */\nexport async function listIdleHobbyTenants(\n\tdb: Kysely<Database>,\n\tidleSince: Date,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"active\")\n\t\t.where(\"plan\", \"=\", \"hobby\")\n\t\t.where(\"last_active_at\", \"<\", idleSince)\n\t\t.execute();\n}\n\n/**\n * Bump `last_active_at` for a tenant. Callers are expected to throttle\n * (don't hammer on every request) — the activity middleware + workflow-\n * runner enforce a 60s per-tenant min between writes.\n */\nexport async function bumpTenantActivity(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({ last_active_at: new Date() })\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport async function listSuspendedOlderThan(\n\tdb: Kysely<Database>,\n\tolderThan: Date,\n): Promise<Tenant[]> {\n\treturn db\n\t\t.selectFrom(\"tenants\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"suspended\")\n\t\t.where(\"suspended_at\", \"<\", olderThan)\n\t\t.execute();\n}\n\nexport async function setTenantStatus(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstatus: TenantStatus,\n): Promise<void> {\n\tconst patch: Record<string, unknown> = {\n\t\tstatus,\n\t\tupdated_at: new Date(),\n\t};\n\tif (status === \"suspended\") patch.suspended_at = new Date();\n\tif (status === \"active\") patch.suspended_at = null;\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\nexport async function recordHealthCheck(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tstorageUsedMb: number | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tlast_health_check_at: new Date(),\n\t\t\tstorage_used_mb: storageUsedMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\n/**\n * Record a storage measurement into the current calendar month's bucket.\n * Maintains peak, running average, and the most recent value in a single\n * upsert. Billing will consume this later; for now the table just gives\n * us evidence of usage over time.\n */\nexport async function recordMonthlyUsage(\n\tdb: Kysely<Database>,\n\ttenantId: string,\n\tstorageMb: number,\n): Promise<void> {\n\t// Bucket is the first day of the current month (UTC), so the unique\n\t// (tenant_id, period_month) constraint groups all samples cleanly.\n\tconst now = new Date();\n\tconst periodMonth = new Date(\n\t\tDate.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),\n\t);\n\n\t// Running mean: avg_new = (avg_old * n + x) / (n + 1). Doing it in SQL\n\t// keeps the write atomic — no read-modify-write race between ticks.\n\tawait sql`\n\t\tINSERT INTO tenant_usage_monthly (\n\t\t\ttenant_id, period_month,\n\t\t\tstorage_peak_mb, storage_avg_mb, storage_last_mb,\n\t\t\tmeasurements, first_at, last_at\n\t\t) VALUES (\n\t\t\t${tenantId}, ${periodMonth},\n\t\t\t${storageMb}, ${storageMb}, ${storageMb},\n\t\t\t1, now(), now()\n\t\t)\n\t\tON CONFLICT (tenant_id, period_month) DO UPDATE SET\n\t\t\tstorage_peak_mb = GREATEST(tenant_usage_monthly.storage_peak_mb, EXCLUDED.storage_last_mb),\n\t\t\tstorage_avg_mb = (\n\t\t\t\t(tenant_usage_monthly.storage_avg_mb * tenant_usage_monthly.measurements + EXCLUDED.storage_last_mb)\n\t\t\t\t/ (tenant_usage_monthly.measurements + 1)\n\t\t\t),\n\t\t\tstorage_last_mb = EXCLUDED.storage_last_mb,\n\t\t\tmeasurements = tenant_usage_monthly.measurements + 1,\n\t\t\tlast_at = now()\n\t`.execute(db);\n}\n\nexport async function updateTenantPlan(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tplan: string,\n\tcpus: number,\n\tmemoryMb: number,\n\tstorageLimitMb: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"tenants\")\n\t\t.set({\n\t\t\tplan,\n\t\t\tcpus,\n\t\t\tmemory_mb: memoryMb,\n\t\t\tstorage_limit_mb: storageLimitMb,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.execute();\n}\n\nexport type RotateType = \"service\" | \"anon\" | \"both\";\n\n/**\n * Bump the selected gen counter(s) by 1 and return the new values.\n * Used by the key-rotate endpoint to force the tenant API to reject\n * previously-issued tokens of the rotated role(s).\n */\nexport async function bumpTenantKeyGen(\n\tdb: Kysely<Database>,\n\tslug: string,\n\ttype: RotateType,\n): Promise<{ serviceGen: number; anonGen: number }> {\n\tconst bumpService = type === \"service\" || type === \"both\";\n\tconst bumpAnon = type === \"anon\" || type === \"both\";\n\tconst row = await db\n\t\t.updateTable(\"tenants\")\n\t\t.set((eb) => ({\n\t\t\tservice_gen: bumpService\n\t\t\t\t? eb(\"service_gen\", \"+\", 1)\n\t\t\t\t: eb.ref(\"service_gen\"),\n\t\t\tanon_gen: bumpAnon ? eb(\"anon_gen\", \"+\", 1) : eb.ref(\"anon_gen\"),\n\t\t\tupdated_at: new Date(),\n\t\t}))\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.returning([\"service_gen\", \"anon_gen\"])\n\t\t.executeTakeFirstOrThrow();\n\treturn { serviceGen: row.service_gen, anonGen: row.anon_gen };\n}\n\n/**\n * Replace the encrypted key columns after a successful rotate. Only the\n * rotated column(s) are written — the other stays untouched.\n */\nexport async function updateTenantKeys(\n\tdb: Kysely<Database>,\n\tslug: string,\n\tkeys: { serviceKey?: string; anonKey?: string },\n): Promise<void> {\n\tconst patch: Record<string, unknown> = { updated_at: new Date() };\n\tif (keys.serviceKey) patch.service_key_enc = encryptSecret(keys.serviceKey);\n\tif (keys.anonKey) patch.anon_key_enc = encryptSecret(keys.anonKey);\n\tif (Object.keys(patch).length === 1) return; // only updated_at — nothing to write\n\tawait db.updateTable(\"tenants\").set(patch).where(\"slug\", \"=\", slug).execute();\n}\n\n/**\n * Hard-delete a tenant row. Call only AFTER the provisioner has torn down\n * containers + volume; otherwise orphaned resources linger. Returns whether\n * a row was actually deleted.\n */\nexport async function deleteTenant(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<boolean> {\n\tconst res = await db\n\t\t.deleteFrom(\"tenants\")\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\treturn (res.numDeletedRows ?? 0n) > 0n;\n}\n\nexport interface TenantCredentials {\n\tslug: string;\n\ttargetDatabaseUrl: string;\n\ttenantJwtSecret: string;\n\tanonKey: string;\n\tserviceKey: string;\n\tapiUrlInternal: string;\n\tapiUrlPublic: string;\n}\n\n/**\n * Decrypts the four encrypted columns and returns them plaintext. Call\n * this only when surfacing credentials to an authorized caller (dashboard,\n * CLI). Never log the returned object.\n */\nexport async function getTenantCredentials(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<TenantCredentials | null> {\n\tconst row = await db\n\t\t.selectFrom(\"tenants\")\n\t\t.select([\n\t\t\t\"slug\",\n\t\t\t\"target_database_url_enc\",\n\t\t\t\"tenant_jwt_secret_enc\",\n\t\t\t\"anon_key_enc\",\n\t\t\t\"service_key_enc\",\n\t\t\t\"api_url_internal\",\n\t\t\t\"api_url_public\",\n\t\t])\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\tif (!row) return null;\n\treturn {\n\t\tslug: row.slug,\n\t\ttargetDatabaseUrl: decryptSecret(row.target_database_url_enc),\n\t\ttenantJwtSecret: decryptSecret(row.tenant_jwt_secret_enc),\n\t\tanonKey: decryptSecret(row.anon_key_enc),\n\t\tserviceKey: decryptSecret(row.service_key_enc),\n\t\tapiUrlInternal: row.api_url_internal,\n\t\tapiUrlPublic: row.api_url_public,\n\t};\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "1D0DA0A21FCE7D7664756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "D0CC1ED1445094FE64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -1,10 +1,3 @@
1
- interface PlanLimits {
2
- subgraphs: number;
3
- apiRequestsPerDay: number;
4
- deliveriesPerMonth: number;
5
- storageBytes: number;
6
- }
7
- declare function getPlanLimits(plan: string): PlanLimits;
8
1
  import { Kysely } from "kysely";
9
2
  import { ColumnType, Generated } from "kysely";
10
3
  interface BlocksTable {
@@ -67,10 +60,6 @@ interface SubgraphsTable {
67
60
  handler_code: string | null;
68
61
  source_code: string | null;
69
62
  project_id: string | null;
70
- is_public: Generated<boolean>;
71
- tags: Generated<string[]>;
72
- description: string | null;
73
- forked_from_id: string | null;
74
63
  created_at: Generated<Date>;
75
64
  updated_at: Generated<Date>;
76
65
  }
@@ -105,6 +94,7 @@ interface AccountsTable {
105
94
  bio: string | null;
106
95
  avatar_url: string | null;
107
96
  slug: string | null;
97
+ stripe_customer_id: string | null;
108
98
  created_at: Generated<Date>;
109
99
  }
110
100
  interface SessionsTable {
@@ -130,6 +120,7 @@ interface MagicLinksTable {
130
120
  }
131
121
  interface UsageDailyTable {
132
122
  account_id: string;
123
+ tenant_id: string | null;
133
124
  date: string;
134
125
  api_requests: Generated<number>;
135
126
  deliveries: Generated<number>;
@@ -256,83 +247,6 @@ interface ChatMessagesTable {
256
247
  metadata: unknown | null;
257
248
  created_at: Generated<Date>;
258
249
  }
259
- interface WorkflowDefinitionsTable {
260
- id: Generated<string>;
261
- name: string;
262
- version: Generated<string>;
263
- status: Generated<string>;
264
- trigger_type: string;
265
- trigger_config: unknown;
266
- handler_path: string;
267
- source_code: string | null;
268
- retries_config: unknown | null;
269
- timeout_ms: number | null;
270
- api_key_id: string;
271
- project_id: string | null;
272
- created_at: Generated<Date>;
273
- updated_at: Generated<Date>;
274
- }
275
- interface WorkflowRunsTable {
276
- id: Generated<string>;
277
- definition_id: string;
278
- status: Generated<string>;
279
- trigger_type: string;
280
- trigger_data: unknown | null;
281
- dedup_key: string | null;
282
- error: string | null;
283
- started_at: Date | null;
284
- completed_at: Date | null;
285
- duration_ms: number | null;
286
- total_ai_tokens: Generated<number>;
287
- created_at: Generated<Date>;
288
- }
289
- interface WorkflowStepsTable {
290
- id: Generated<string>;
291
- run_id: string;
292
- step_index: number;
293
- step_id: string;
294
- step_type: string;
295
- status: Generated<string>;
296
- input: unknown | null;
297
- output: unknown | null;
298
- error: string | null;
299
- retry_count: Generated<number>;
300
- ai_tokens_used: Generated<number>;
301
- started_at: Date | null;
302
- completed_at: Date | null;
303
- duration_ms: number | null;
304
- memo_key: string | null;
305
- parent_step_id: string | null;
306
- created_at: Generated<Date>;
307
- }
308
- interface WorkflowQueueTable {
309
- id: Generated<string>;
310
- run_id: string;
311
- status: Generated<string>;
312
- attempts: Generated<number>;
313
- max_attempts: Generated<number>;
314
- scheduled_for: Generated<Date>;
315
- locked_at: Date | null;
316
- locked_by: string | null;
317
- error: string | null;
318
- created_at: Generated<Date>;
319
- completed_at: Date | null;
320
- }
321
- interface WorkflowSchedulesTable {
322
- id: Generated<string>;
323
- definition_id: string;
324
- cron_expr: string;
325
- timezone: Generated<string>;
326
- next_run_at: Date;
327
- last_run_at: Date | null;
328
- enabled: Generated<boolean>;
329
- created_at: Generated<Date>;
330
- }
331
- interface WorkflowCursorsTable {
332
- name: string;
333
- block_height: Generated<number>;
334
- updated_at: Generated<Date>;
335
- }
336
250
  interface Database {
337
251
  blocks: BlocksTable;
338
252
  transactions: TransactionsTable;
@@ -358,15 +272,11 @@ interface Database {
358
272
  team_invitations: TeamInvitationsTable;
359
273
  chat_sessions: ChatSessionsTable;
360
274
  chat_messages: ChatMessagesTable;
361
- workflow_definitions: WorkflowDefinitionsTable;
362
- workflow_runs: WorkflowRunsTable;
363
- workflow_steps: WorkflowStepsTable;
364
- workflow_queue: WorkflowQueueTable;
365
- workflow_schedules: WorkflowSchedulesTable;
366
- workflow_cursors: WorkflowCursorsTable;
367
- workflow_signer_secrets: WorkflowSignerSecretsTable;
368
- workflow_budgets: WorkflowBudgetsTable;
369
275
  tenants: TenantsTable;
276
+ tenant_usage_monthly: TenantUsageMonthlyTable;
277
+ tenant_compute_addons: TenantComputeAddonsTable;
278
+ account_spend_caps: AccountSpendCapsTable;
279
+ provisioning_audit_log: ProvisioningAuditLogTable;
370
280
  }
371
281
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
372
282
  interface TenantsTable {
@@ -388,38 +298,61 @@ interface TenantsTable {
388
298
  service_key_enc: Buffer;
389
299
  api_url_internal: string;
390
300
  api_url_public: string;
391
- trial_ends_at: Date;
392
301
  suspended_at: Date | null;
393
302
  last_health_check_at: Date | null;
303
+ last_active_at: Generated<Date>;
394
304
  service_gen: Generated<number>;
395
305
  anon_gen: Generated<number>;
396
306
  project_id: string | null;
397
307
  created_at: Generated<Date>;
398
308
  updated_at: Generated<Date>;
399
309
  }
400
- interface WorkflowBudgetsTable {
310
+ interface TenantUsageMonthlyTable {
311
+ id: Generated<string>;
312
+ tenant_id: string;
313
+ period_month: Date;
314
+ storage_peak_mb: Generated<number>;
315
+ storage_avg_mb: Generated<number>;
316
+ storage_last_mb: Generated<number>;
317
+ measurements: Generated<number>;
318
+ first_at: Generated<Date>;
319
+ last_at: Generated<Date>;
320
+ }
321
+ interface TenantComputeAddonsTable {
401
322
  id: Generated<string>;
402
- workflow_definition_id: string;
403
- /** Period key: "daily:YYYY-MM-DD" | "weekly:YYYY-Www" | "per-run:<uuid>". */
404
- period: string;
405
- ai_usd_used: Generated<string>;
406
- ai_tokens_used: Generated<string>;
407
- chain_microstx_used: Generated<string>;
408
- chain_tx_count: Generated<number>;
409
- run_count: Generated<number>;
410
- step_count: Generated<number>;
411
- reset_at: Date;
323
+ tenant_id: string;
324
+ memory_mb_delta: Generated<number>;
325
+ cpu_delta: Generated<number | string>;
326
+ storage_mb_delta: Generated<number>;
327
+ effective_from: Generated<Date>;
328
+ effective_until: Date | null;
329
+ stripe_subscription_item_id: string | null;
412
330
  created_at: Generated<Date>;
331
+ }
332
+ interface AccountSpendCapsTable {
333
+ account_id: string;
334
+ monthly_cap_cents: number | null;
335
+ compute_cap_cents: number | null;
336
+ storage_cap_cents: number | null;
337
+ ai_cap_cents: number | null;
338
+ alert_threshold_pct: Generated<number>;
339
+ alert_sent_at: Date | null;
340
+ frozen_at: Date | null;
413
341
  updated_at: Generated<Date>;
414
342
  }
415
- interface WorkflowSignerSecretsTable {
343
+ type ProvisioningAuditEvent = "provision.start" | "provision.success" | "provision.failure" | "suspend" | "resume" | "resize" | "keys.rotate" | "bastion.key.upload" | "bastion.key.revoke" | "teardown";
344
+ type ProvisioningAuditStatus = "ok" | "error";
345
+ interface ProvisioningAuditLogTable {
416
346
  id: Generated<string>;
417
- account_id: string;
418
- name: string;
419
- /** AES-GCM ciphertext bytes produced by the runner's KMS on write. */
420
- encrypted_value: Buffer;
347
+ tenant_id: string | null;
348
+ tenant_slug: string | null;
349
+ account_id: string | null;
350
+ actor: string;
351
+ event: ProvisioningAuditEvent;
352
+ status: ProvisioningAuditStatus;
353
+ detail: unknown | null;
354
+ error: string | null;
421
355
  created_at: Generated<Date>;
422
- updated_at: Generated<Date>;
423
356
  }
424
357
  /** Increment API request counter for today. Fire-and-forget safe. */
425
358
  declare function incrementApiRequests(db: Kysely<Database>, accountId: string): Promise<void>;
@@ -430,26 +363,9 @@ interface UsageSummary {
430
363
  }
431
364
  /** Get current usage for an account. */
432
365
  declare function getUsage(db: Kysely<Database>, accountId: string): Promise<UsageSummary>;
433
- interface DailyUsage {
434
- date: string;
435
- apiRequests: number;
436
- deliveries: number;
437
- }
438
- /** Get last 7 days of daily usage, filling missing days with 0. */
439
- declare function getDailyUsage(db: Kysely<Database>, accountId: string): Promise<DailyUsage[]>;
440
- interface LimitCheck {
441
- allowed: boolean;
442
- limits: ReturnType<typeof getPlanLimits>;
443
- current: UsageSummary & {
444
- subgraphs: number
445
- };
446
- exceeded?: string;
447
- }
448
- /** Check if an account is within plan limits. */
449
- declare function checkLimits(db: Kysely<Database>, accountId: string, plan: string): Promise<LimitCheck>;
450
366
  /**
451
367
  * Measure storage for all accounts by querying pg_total_relation_size
452
368
  * for each tenant's subgraph schemas.
453
369
  */
454
370
  declare function measureStorage(db: Kysely<Database>): Promise<void>;
455
- export { measureStorage, incrementApiRequests, getUsage, getDailyUsage, checkLimits, UsageSummary, LimitCheck, DailyUsage };
371
+ export { measureStorage, incrementApiRequests, getUsage, UsageSummary };
@@ -14,21 +14,6 @@ var __export = (target, all) => {
14
14
  });
15
15
  };
16
16
 
17
- // src/lib/plans.ts
18
- var FREE_PLAN = {
19
- subgraphs: 2,
20
- apiRequestsPerDay: 1000,
21
- deliveriesPerMonth: 5000,
22
- storageBytes: 100 * 1024 * 1024
23
- };
24
- function getPlanLimits(plan) {
25
- switch (plan) {
26
- case "free":
27
- default:
28
- return FREE_PLAN;
29
- }
30
- }
31
-
32
17
  // src/db/queries/usage.ts
33
18
  import { sql } from "kysely";
34
19
  async function incrementApiRequests(db, accountId) {
@@ -44,7 +29,7 @@ async function incrementApiRequests(db, accountId) {
44
29
  }
45
30
  async function getUsage(db, accountId) {
46
31
  const today = new Date().toISOString().slice(0, 10);
47
- const monthStart = today.slice(0, 7) + "-01";
32
+ const monthStart = `${today.slice(0, 7)}-01`;
48
33
  const dailyRow = await db.selectFrom("usage_daily").select("api_requests").where("account_id", "=", accountId).where("date", "=", today).executeTakeFirst();
49
34
  const monthlyRow = await db.selectFrom("usage_daily").select(sql`COALESCE(SUM(deliveries), 0)`.as("total")).where("account_id", "=", accountId).where("date", ">=", monthStart).executeTakeFirst();
50
35
  const storageRow = await db.selectFrom("usage_snapshots").select("storage_bytes").where("account_id", "=", accountId).orderBy("measured_at", "desc").limit(1).executeTakeFirst();
@@ -54,49 +39,6 @@ async function getUsage(db, accountId) {
54
39
  storageBytes: Number(storageRow?.storage_bytes ?? 0)
55
40
  };
56
41
  }
57
- async function getDailyUsage(db, accountId) {
58
- const rows = await db.selectFrom("usage_daily").select(["date", "api_requests", "deliveries"]).where("account_id", "=", accountId).where("date", ">=", sql`NOW()::date - 6`).orderBy("date", "asc").execute();
59
- const byDate = new Map(rows.map((r) => {
60
- const d = r.date;
61
- const key = d instanceof Date ? d.toISOString().slice(0, 10) : String(d).slice(0, 10);
62
- return [key, r];
63
- }));
64
- const result = [];
65
- for (let i = 6;i >= 0; i--) {
66
- const d = new Date;
67
- d.setDate(d.getDate() - i);
68
- const dateStr = d.toISOString().slice(0, 10);
69
- const row = byDate.get(dateStr);
70
- result.push({
71
- date: dateStr,
72
- apiRequests: row?.api_requests ?? 0,
73
- deliveries: row?.deliveries ?? 0
74
- });
75
- }
76
- return result;
77
- }
78
- async function checkLimits(db, accountId, plan) {
79
- const limits = getPlanLimits(plan);
80
- const usage = await getUsage(db, accountId);
81
- const subgraphCount = await db.selectFrom("subgraphs").select(sql`count(*)`.as("count")).where("account_id", "=", accountId).executeTakeFirst();
82
- const current = {
83
- ...usage,
84
- subgraphs: Number(subgraphCount?.count ?? 0)
85
- };
86
- if (current.subgraphs >= limits.subgraphs) {
87
- return { allowed: false, limits, current, exceeded: "subgraphs" };
88
- }
89
- if (current.apiRequestsToday >= limits.apiRequestsPerDay) {
90
- return { allowed: false, limits, current, exceeded: "api_requests" };
91
- }
92
- if (current.deliveriesThisMonth >= limits.deliveriesPerMonth) {
93
- return { allowed: false, limits, current, exceeded: "deliveries" };
94
- }
95
- if (current.storageBytes >= limits.storageBytes) {
96
- return { allowed: false, limits, current, exceeded: "storage" };
97
- }
98
- return { allowed: true, limits, current };
99
- }
100
42
  async function measureStorage(db) {
101
43
  const accountSubgraphs = await db.selectFrom("subgraphs").select(["account_id", "schema_name"]).where("schema_name", "is not", null).execute();
102
44
  const byAccount = new Map;
@@ -114,7 +56,8 @@ async function measureStorage(db) {
114
56
  SELECT COALESCE(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))), 0)::text as size
115
57
  FROM pg_tables WHERE schemaname = ${schema}
116
58
  `.execute(db);
117
- totalBytes += Number(result.rows[0]?.size ?? 0);
59
+ const row = result.rows[0];
60
+ totalBytes += Number(row?.size ?? 0);
118
61
  } catch {}
119
62
  }
120
63
  await db.insertInto("usage_snapshots").values({
@@ -126,10 +69,8 @@ async function measureStorage(db) {
126
69
  export {
127
70
  measureStorage,
128
71
  incrementApiRequests,
129
- getUsage,
130
- getDailyUsage,
131
- checkLimits
72
+ getUsage
132
73
  };
133
74
 
134
- //# debugId=7A7AEE74ECF0C63664756E2164756E21
75
+ //# debugId=93865CC393EF182964756E2164756E21
135
76
  //# sourceMappingURL=usage.js.map
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib/plans.ts", "../src/db/queries/usage.ts"],
3
+ "sources": ["../src/db/queries/usage.ts"],
4
4
  "sourcesContent": [
5
- "export interface PlanLimits {\n\tsubgraphs: number;\n\tapiRequestsPerDay: number;\n\tdeliveriesPerMonth: number;\n\tstorageBytes: number;\n}\n\nexport const FREE_PLAN: PlanLimits = {\n\tsubgraphs: 2,\n\tapiRequestsPerDay: 1_000,\n\tdeliveriesPerMonth: 5_000,\n\tstorageBytes: 100 * 1024 * 1024,\n};\n\nexport function getPlanLimits(plan: string): PlanLimits {\n\tswitch (plan) {\n\t\tcase \"free\":\n\t\tdefault:\n\t\t\treturn FREE_PLAN;\n\t}\n}\n",
6
- "import { type Kysely, sql } from \"kysely\";\nimport { getPlanLimits } from \"../../lib/plans.ts\";\nimport type { Database } from \"../types.ts\";\n\n/** Increment API request counter for today. Fire-and-forget safe. */\nexport async function incrementApiRequests(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<void> {\n\tconst today = new Date().toISOString().slice(0, 10);\n\tawait db\n\t\t.insertInto(\"usage_daily\")\n\t\t.values({\n\t\t\taccount_id: accountId,\n\t\t\tdate: today,\n\t\t\tapi_requests: 1,\n\t\t\tdeliveries: 0,\n\t\t})\n\t\t.onConflict((oc) =>\n\t\t\toc.columns([\"account_id\", \"date\"]).doUpdateSet({\n\t\t\t\tapi_requests: sql`usage_daily.api_requests + 1`,\n\t\t\t}),\n\t\t)\n\t\t.execute();\n}\n\nexport interface UsageSummary {\n\tapiRequestsToday: number;\n\tdeliveriesThisMonth: number;\n\tstorageBytes: number;\n}\n\n/** Get current usage for an account. */\nexport async function getUsage(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<UsageSummary> {\n\tconst today = new Date().toISOString().slice(0, 10);\n\tconst monthStart = today.slice(0, 7) + \"-01\"; // YYYY-MM-01\n\n\t// Today's API requests\n\tconst dailyRow = await db\n\t\t.selectFrom(\"usage_daily\")\n\t\t.select(\"api_requests\")\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"date\", \"=\", today)\n\t\t.executeTakeFirst();\n\n\t// This month's deliveries\n\tconst monthlyRow = await db\n\t\t.selectFrom(\"usage_daily\")\n\t\t.select(sql<number>`COALESCE(SUM(deliveries), 0)`.as(\"total\"))\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"date\", \">=\", monthStart)\n\t\t.executeTakeFirst();\n\n\t// Latest storage snapshot\n\tconst storageRow = await db\n\t\t.selectFrom(\"usage_snapshots\")\n\t\t.select(\"storage_bytes\")\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.orderBy(\"measured_at\", \"desc\")\n\t\t.limit(1)\n\t\t.executeTakeFirst();\n\n\treturn {\n\t\tapiRequestsToday: dailyRow?.api_requests ?? 0,\n\t\tdeliveriesThisMonth: Number(monthlyRow?.total ?? 0),\n\t\tstorageBytes: Number(storageRow?.storage_bytes ?? 0),\n\t};\n}\n\nexport interface DailyUsage {\n\tdate: string;\n\tapiRequests: number;\n\tdeliveries: number;\n}\n\n/** Get last 7 days of daily usage, filling missing days with 0. */\nexport async function getDailyUsage(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<DailyUsage[]> {\n\tconst rows = await db\n\t\t.selectFrom(\"usage_daily\")\n\t\t.select([\"date\", \"api_requests\", \"deliveries\"])\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"date\", \">=\", sql<string>`NOW()::date - 6`)\n\t\t.orderBy(\"date\", \"asc\")\n\t\t.execute();\n\n\t// Fill missing days with 0 (normalize date to YYYY-MM-DD string)\n\tconst byDate = new Map(\n\t\trows.map((r) => {\n\t\t\tconst d = r.date as unknown;\n\t\t\tconst key =\n\t\t\t\td instanceof Date\n\t\t\t\t\t? d.toISOString().slice(0, 10)\n\t\t\t\t\t: String(d).slice(0, 10);\n\t\t\treturn [key, r];\n\t\t}),\n\t);\n\tconst result: DailyUsage[] = [];\n\tfor (let i = 6; i >= 0; i--) {\n\t\tconst d = new Date();\n\t\td.setDate(d.getDate() - i);\n\t\tconst dateStr = d.toISOString().slice(0, 10);\n\t\tconst row = byDate.get(dateStr);\n\t\tresult.push({\n\t\t\tdate: dateStr,\n\t\t\tapiRequests: row?.api_requests ?? 0,\n\t\t\tdeliveries: row?.deliveries ?? 0,\n\t\t});\n\t}\n\treturn result;\n}\n\nexport interface LimitCheck {\n\tallowed: boolean;\n\tlimits: ReturnType<typeof getPlanLimits>;\n\tcurrent: UsageSummary & { subgraphs: number };\n\texceeded?: string;\n}\n\n/** Check if an account is within plan limits. */\nexport async function checkLimits(\n\tdb: Kysely<Database>,\n\taccountId: string,\n\tplan: string,\n): Promise<LimitCheck> {\n\tconst limits = getPlanLimits(plan);\n\tconst usage = await getUsage(db, accountId);\n\n\tconst subgraphCount = await db\n\t\t.selectFrom(\"subgraphs\")\n\t\t.select(sql<number>`count(*)`.as(\"count\"))\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.executeTakeFirst();\n\n\tconst current = {\n\t\t...usage,\n\t\tsubgraphs: Number(subgraphCount?.count ?? 0),\n\t};\n\n\tif (current.subgraphs >= limits.subgraphs) {\n\t\treturn { allowed: false, limits, current, exceeded: \"subgraphs\" };\n\t}\n\tif (current.apiRequestsToday >= limits.apiRequestsPerDay) {\n\t\treturn { allowed: false, limits, current, exceeded: \"api_requests\" };\n\t}\n\tif (current.deliveriesThisMonth >= limits.deliveriesPerMonth) {\n\t\treturn { allowed: false, limits, current, exceeded: \"deliveries\" };\n\t}\n\tif (current.storageBytes >= limits.storageBytes) {\n\t\treturn { allowed: false, limits, current, exceeded: \"storage\" };\n\t}\n\n\treturn { allowed: true, limits, current };\n}\n\n/**\n * Measure storage for all accounts by querying pg_total_relation_size\n * for each tenant's subgraph schemas.\n */\nexport async function measureStorage(db: Kysely<Database>): Promise<void> {\n\t// Get all accounts with subgraphs\n\tconst accountSubgraphs = await db\n\t\t.selectFrom(\"subgraphs\")\n\t\t.select([\"account_id\", \"schema_name\"])\n\t\t.where(\"schema_name\", \"is not\", null)\n\t\t.execute();\n\n\t// Group schemas by account\n\tconst byAccount = new Map<string, string[]>();\n\tfor (const row of accountSubgraphs) {\n\t\tconst schemas = byAccount.get(row.account_id) ?? [];\n\t\tif (row.schema_name) schemas.push(row.schema_name);\n\t\tbyAccount.set(row.account_id, schemas);\n\t}\n\n\tfor (const [accountId, schemas] of byAccount) {\n\t\tlet totalBytes = 0;\n\t\tfor (const schema of schemas) {\n\t\t\ttry {\n\t\t\t\tconst result = await sql<{ size: string }>`\n SELECT COALESCE(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))), 0)::text as size\n FROM pg_tables WHERE schemaname = ${schema}\n `.execute(db);\n\t\t\t\ttotalBytes += Number((result.rows[0] as any)?.size ?? 0);\n\t\t\t} catch {\n\t\t\t\t// Schema may not exist\n\t\t\t}\n\t\t}\n\n\t\tawait db\n\t\t\t.insertInto(\"usage_snapshots\")\n\t\t\t.values({\n\t\t\t\taccount_id: accountId,\n\t\t\t\tstorage_bytes: totalBytes,\n\t\t\t})\n\t\t\t.execute();\n\t}\n}\n"
5
+ "import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\n/** Increment API request counter for today. Fire-and-forget safe. */\nexport async function incrementApiRequests(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<void> {\n\tconst today = new Date().toISOString().slice(0, 10);\n\tawait db\n\t\t.insertInto(\"usage_daily\")\n\t\t.values({\n\t\t\taccount_id: accountId,\n\t\t\tdate: today,\n\t\t\tapi_requests: 1,\n\t\t\tdeliveries: 0,\n\t\t})\n\t\t.onConflict((oc) =>\n\t\t\toc.columns([\"account_id\", \"date\"]).doUpdateSet({\n\t\t\t\tapi_requests: sql`usage_daily.api_requests + 1`,\n\t\t\t}),\n\t\t)\n\t\t.execute();\n}\n\nexport interface UsageSummary {\n\tapiRequestsToday: number;\n\tdeliveriesThisMonth: number;\n\tstorageBytes: number;\n}\n\n/** Get current usage for an account. */\nexport async function getUsage(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<UsageSummary> {\n\tconst today = new Date().toISOString().slice(0, 10);\n\tconst monthStart = `${today.slice(0, 7)}-01`; // YYYY-MM-01\n\n\t// Today's API requests\n\tconst dailyRow = await db\n\t\t.selectFrom(\"usage_daily\")\n\t\t.select(\"api_requests\")\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"date\", \"=\", today)\n\t\t.executeTakeFirst();\n\n\t// This month's deliveries\n\tconst monthlyRow = await db\n\t\t.selectFrom(\"usage_daily\")\n\t\t.select(sql<number>`COALESCE(SUM(deliveries), 0)`.as(\"total\"))\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.where(\"date\", \">=\", monthStart)\n\t\t.executeTakeFirst();\n\n\t// Latest storage snapshot\n\tconst storageRow = await db\n\t\t.selectFrom(\"usage_snapshots\")\n\t\t.select(\"storage_bytes\")\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.orderBy(\"measured_at\", \"desc\")\n\t\t.limit(1)\n\t\t.executeTakeFirst();\n\n\treturn {\n\t\tapiRequestsToday: dailyRow?.api_requests ?? 0,\n\t\tdeliveriesThisMonth: Number(monthlyRow?.total ?? 0),\n\t\tstorageBytes: Number(storageRow?.storage_bytes ?? 0),\n\t};\n}\n\n/**\n * Measure storage for all accounts by querying pg_total_relation_size\n * for each tenant's subgraph schemas.\n */\nexport async function measureStorage(db: Kysely<Database>): Promise<void> {\n\t// Get all accounts with subgraphs\n\tconst accountSubgraphs = await db\n\t\t.selectFrom(\"subgraphs\")\n\t\t.select([\"account_id\", \"schema_name\"])\n\t\t.where(\"schema_name\", \"is not\", null)\n\t\t.execute();\n\n\t// Group schemas by account\n\tconst byAccount = new Map<string, string[]>();\n\tfor (const row of accountSubgraphs) {\n\t\tconst schemas = byAccount.get(row.account_id) ?? [];\n\t\tif (row.schema_name) schemas.push(row.schema_name);\n\t\tbyAccount.set(row.account_id, schemas);\n\t}\n\n\tfor (const [accountId, schemas] of byAccount) {\n\t\tlet totalBytes = 0;\n\t\tfor (const schema of schemas) {\n\t\t\ttry {\n\t\t\t\tconst result = await sql<{ size: string }>`\n SELECT COALESCE(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))), 0)::text as size\n FROM pg_tables WHERE schemaname = ${schema}\n `.execute(db);\n\t\t\t\tconst row = result.rows[0] as { size?: string } | undefined;\n\t\t\t\ttotalBytes += Number(row?.size ?? 0);\n\t\t\t} catch {\n\t\t\t\t// Schema may not exist\n\t\t\t}\n\t\t}\n\n\t\tawait db\n\t\t\t.insertInto(\"usage_snapshots\")\n\t\t\t.values({\n\t\t\t\taccount_id: accountId,\n\t\t\t\tstorage_bytes: totalBytes,\n\t\t\t})\n\t\t\t.execute();\n\t}\n}\n"
7
6
  ],
8
- "mappings": "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",
9
- "debugId": "7A7AEE74ECF0C63664756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAIA,eAAsB,oBAAoB,CACzC,IACA,WACgB;AAAA,EAChB,MAAM,QAAQ,IAAI,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,EAClD,MAAM,GACJ,WAAW,aAAa,EACxB,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,cAAc;AAAA,IACd,YAAY;AAAA,EACb,CAAC,EACA,WAAW,CAAC,OACZ,GAAG,QAAQ,CAAC,cAAc,MAAM,CAAC,EAAE,YAAY;AAAA,IAC9C,cAAc;AAAA,EACf,CAAC,CACF,EACC,QAAQ;AAAA;AAUX,eAAsB,QAAQ,CAC7B,IACA,WACwB;AAAA,EACxB,MAAM,QAAQ,IAAI,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,EAClD,MAAM,aAAa,GAAG,MAAM,MAAM,GAAG,CAAC;AAAA,EAGtC,MAAM,WAAW,MAAM,GACrB,WAAW,aAAa,EACxB,OAAO,cAAc,EACrB,MAAM,cAAc,KAAK,SAAS,EAClC,MAAM,QAAQ,KAAK,KAAK,EACxB,iBAAiB;AAAA,EAGnB,MAAM,aAAa,MAAM,GACvB,WAAW,aAAa,EACxB,OAAO,kCAA0C,GAAG,OAAO,CAAC,EAC5D,MAAM,cAAc,KAAK,SAAS,EAClC,MAAM,QAAQ,MAAM,UAAU,EAC9B,iBAAiB;AAAA,EAGnB,MAAM,aAAa,MAAM,GACvB,WAAW,iBAAiB,EAC5B,OAAO,eAAe,EACtB,MAAM,cAAc,KAAK,SAAS,EAClC,QAAQ,eAAe,MAAM,EAC7B,MAAM,CAAC,EACP,iBAAiB;AAAA,EAEnB,OAAO;AAAA,IACN,kBAAkB,UAAU,gBAAgB;AAAA,IAC5C,qBAAqB,OAAO,YAAY,SAAS,CAAC;AAAA,IAClD,cAAc,OAAO,YAAY,iBAAiB,CAAC;AAAA,EACpD;AAAA;AAOD,eAAsB,cAAc,CAAC,IAAqC;AAAA,EAEzE,MAAM,mBAAmB,MAAM,GAC7B,WAAW,WAAW,EACtB,OAAO,CAAC,cAAc,aAAa,CAAC,EACpC,MAAM,eAAe,UAAU,IAAI,EACnC,QAAQ;AAAA,EAGV,MAAM,YAAY,IAAI;AAAA,EACtB,WAAW,OAAO,kBAAkB;AAAA,IACnC,MAAM,UAAU,UAAU,IAAI,IAAI,UAAU,KAAK,CAAC;AAAA,IAClD,IAAI,IAAI;AAAA,MAAa,QAAQ,KAAK,IAAI,WAAW;AAAA,IACjD,UAAU,IAAI,IAAI,YAAY,OAAO;AAAA,EACtC;AAAA,EAEA,YAAY,WAAW,YAAY,WAAW;AAAA,IAC7C,IAAI,aAAa;AAAA,IACjB,WAAW,UAAU,SAAS;AAAA,MAC7B,IAAI;AAAA,QACH,MAAM,SAAS,MAAM;AAAA;AAAA,8CAEqB;AAAA,UACpC,QAAQ,EAAE;AAAA,QAChB,MAAM,MAAM,OAAO,KAAK;AAAA,QACxB,cAAc,OAAO,KAAK,QAAQ,CAAC;AAAA,QAClC,MAAM;AAAA,IAGT;AAAA,IAEA,MAAM,GACJ,WAAW,iBAAiB,EAC5B,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,eAAe;AAAA,IAChB,CAAC,EACA,QAAQ;AAAA,EACX;AAAA;",
8
+ "debugId": "93865CC393EF182964756E2164756E21",
10
9
  "names": []
11
10
  }