@secondlayer/shared 3.0.0-alpha.0 → 3.0.0-beta.2

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.
@@ -14,15 +14,59 @@ var __export = (target, all) => {
14
14
  });
15
15
  };
16
16
 
17
+ // src/mode.ts
18
+ var VALID_MODES = ["oss", "dedicated", "platform"];
19
+ function getInstanceMode() {
20
+ const raw = process.env.INSTANCE_MODE?.trim().toLowerCase();
21
+ if (raw && VALID_MODES.includes(raw)) {
22
+ return raw;
23
+ }
24
+ return "oss";
25
+ }
26
+ function isPlatformMode() {
27
+ return getInstanceMode() === "platform";
28
+ }
29
+ function isOssMode() {
30
+ return getInstanceMode() === "oss";
31
+ }
32
+ function isDedicatedMode() {
33
+ return getInstanceMode() === "dedicated";
34
+ }
35
+
17
36
  // src/crypto/secrets.ts
18
37
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
38
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
39
+ import { resolve } from "node:path";
19
40
  var KEY_ENV = "SECONDLAYER_SECRETS_KEY";
20
41
  var IV_LEN = 12;
21
42
  var TAG_LEN = 16;
43
+ function bootstrapOssKey() {
44
+ const envPath = resolve(process.cwd(), ".env.local");
45
+ if (existsSync(envPath)) {
46
+ const contents = readFileSync(envPath, "utf8");
47
+ const match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);
48
+ if (match) {
49
+ process.env[KEY_ENV] = match[1];
50
+ return match[1];
51
+ }
52
+ }
53
+ const hex = randomBytes(32).toString("hex");
54
+ const line = `${existsSync(envPath) ? `
55
+ ` : ""}${KEY_ENV}=${hex}
56
+ `;
57
+ appendFileSync(envPath, line, { mode: 384 });
58
+ process.env[KEY_ENV] = hex;
59
+ console.log(`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`);
60
+ return hex;
61
+ }
22
62
  function loadKey() {
23
- const hex = process.env[KEY_ENV];
63
+ let hex = process.env[KEY_ENV];
24
64
  if (!hex) {
25
- throw new Error(`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`);
65
+ if (getInstanceMode() === "oss") {
66
+ hex = bootstrapOssKey();
67
+ } else {
68
+ throw new Error(`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`);
69
+ }
26
70
  }
27
71
  const key = Buffer.from(hex, "hex");
28
72
  if (key.length !== 32) {
@@ -65,5 +109,5 @@ export {
65
109
  decryptSecret
66
110
  };
67
111
 
68
- //# debugId=0FD7F496A1099A3864756E2164756E21
112
+ //# debugId=982B15A94DFBA98464756E2164756E21
69
113
  //# sourceMappingURL=secrets.js.map
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/crypto/secrets.ts"],
3
+ "sources": ["../src/mode.ts", "../src/crypto/secrets.ts"],
4
4
  "sourcesContent": [
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"
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"
6
7
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAiBA,IAAM,UAAU;AAChB,IAAM,SAAS;AACf,IAAM,UAAU;AAEhB,SAAS,OAAO,GAAW;AAAA,EAC1B,MAAM,MAAM,QAAQ,IAAI;AAAA,EACxB,IAAI,CAAC,KAAK;AAAA,IACT,MAAM,IAAI,MACT,GAAG,0DACJ;AAAA,EACD;AAAA,EACA,MAAM,MAAM,OAAO,KAAK,KAAK,KAAK;AAAA,EAClC,IAAI,IAAI,WAAW,IAAI;AAAA,IACtB,MAAM,IAAI,MAAM,GAAG,qCAAqC,IAAI,SAAS;AAAA,EACtE;AAAA,EACA,OAAO;AAAA;AAGR,IAAI,aAA4B;AAChC,SAAS,MAAM,GAAW;AAAA,EACzB,IAAI,CAAC;AAAA,IAAY,aAAa,QAAQ;AAAA,EACtC,OAAO;AAAA;AAGD,SAAS,aAAa,CAAC,WAA2B;AAAA,EACxD,MAAM,MAAM,OAAO;AAAA,EACnB,MAAM,KAAK,YAAY,MAAM;AAAA,EAC7B,MAAM,SAAS,eAAe,eAAe,KAAK,EAAE;AAAA,EACpD,MAAM,aAAa,OAAO,OAAO;AAAA,IAChC,OAAO,OAAO,WAAW,MAAM;AAAA,IAC/B,OAAO,MAAM;AAAA,EACd,CAAC;AAAA,EACD,MAAM,MAAM,OAAO,WAAW;AAAA,EAC9B,OAAO,OAAO,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC;AAAA;AAGpC,SAAS,aAAa,CAAC,UAA0B;AAAA,EACvD,MAAM,MAAM,OAAO;AAAA,EACnB,MAAM,KAAK,SAAS,SAAS,GAAG,MAAM;AAAA,EACtC,MAAM,MAAM,SAAS,SAAS,QAAQ,SAAS,OAAO;AAAA,EACtD,MAAM,aAAa,SAAS,SAAS,SAAS,OAAO;AAAA,EACrD,MAAM,WAAW,iBAAiB,eAAe,KAAK,EAAE;AAAA,EACxD,SAAS,WAAW,GAAG;AAAA,EACvB,OAAO,SAAS,OAAO,UAAU,EAAE,SAAS,MAAM,IAAI,SAAS,MAAM,MAAM;AAAA;AAIrE,SAAS,kBAAkB,GAAW;AAAA,EAC5C,OAAO,YAAY,EAAE,EAAE,SAAS,KAAK;AAAA;",
8
- "debugId": "0FD7F496A1099A3864756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "982B15A94DFBA98464756E2164756E21",
9
10
  "names": []
10
11
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Standard Webhooks signing helpers — https://standardwebhooks.com
3
+ *
4
+ * Produces the three headers that every Standard Webhooks receiver expects:
5
+ * webhook-id — UUID identifying the delivery (used for dedup)
6
+ * webhook-timestamp — unix seconds, receiver rejects if skew > tolerance
7
+ * webhook-signature — space-separated list of `vN,<base64-hmac>` tuples
8
+ *
9
+ * The signed content is `{id}.{timestamp}.{body}`. The HMAC key is the raw
10
+ * bytes of the secret. If the secret is a `whsec_`-prefixed base64 string
11
+ * (the Svix convention) we base64-decode after stripping the prefix;
12
+ * otherwise we use the UTF-8 bytes directly.
13
+ */
14
+ interface StandardWebhooksHeaders {
15
+ "webhook-id": string;
16
+ "webhook-timestamp": string;
17
+ "webhook-signature": string;
18
+ }
19
+ interface SignOptions {
20
+ /** Override the delivery id. Defaults to a random UUID v4. */
21
+ id?: string;
22
+ /** Override the timestamp (unix seconds). Defaults to `Date.now()`. */
23
+ timestampSeconds?: number;
24
+ }
25
+ declare function sign(body: string, secret: string, opts?: SignOptions): StandardWebhooksHeaders;
26
+ interface VerifyOptions {
27
+ /** Max clock skew in seconds. Default 5 minutes per spec. */
28
+ toleranceSeconds?: number;
29
+ /** Current time in unix seconds. Injectable for testing. */
30
+ nowSeconds?: number;
31
+ }
32
+ declare function verify(body: string, headers: StandardWebhooksHeaders | Record<string, string | string[] | undefined>, secret: string, opts?: VerifyOptions): boolean;
33
+ export { verify, sign, VerifyOptions, StandardWebhooksHeaders, SignOptions };
@@ -0,0 +1,77 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/crypto/standard-webhooks.ts
18
+ import { createHmac, randomUUID } from "node:crypto";
19
+ function secretToKey(secret) {
20
+ if (secret.startsWith("whsec_")) {
21
+ return Buffer.from(secret.slice("whsec_".length), "base64");
22
+ }
23
+ return Buffer.from(secret, "utf8");
24
+ }
25
+ function sign(body, secret, opts = {}) {
26
+ const id = opts.id ?? randomUUID();
27
+ const timestamp = String(opts.timestampSeconds ?? Math.floor(Date.now() / 1000));
28
+ const toSign = `${id}.${timestamp}.${body}`;
29
+ const key = secretToKey(secret);
30
+ const signature = createHmac("sha256", key).update(toSign).digest("base64");
31
+ return {
32
+ "webhook-id": id,
33
+ "webhook-timestamp": timestamp,
34
+ "webhook-signature": `v1,${signature}`
35
+ };
36
+ }
37
+ function verify(body, headers, secret, opts = {}) {
38
+ const pick = (k) => {
39
+ const v = headers[k];
40
+ return typeof v === "string" ? v : undefined;
41
+ };
42
+ const id = pick("webhook-id");
43
+ const timestamp = pick("webhook-timestamp");
44
+ const sigHeader = pick("webhook-signature");
45
+ if (!id || !timestamp || !sigHeader)
46
+ return false;
47
+ const ts = Number.parseInt(timestamp, 10);
48
+ if (!Number.isFinite(ts))
49
+ return false;
50
+ const tolerance = opts.toleranceSeconds ?? 5 * 60;
51
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1000);
52
+ if (Math.abs(now - ts) > tolerance)
53
+ return false;
54
+ const key = secretToKey(secret);
55
+ const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
56
+ for (const part of sigHeader.split(" ")) {
57
+ const [version, sig] = part.split(",", 2);
58
+ if (version !== "v1" || !sig)
59
+ continue;
60
+ if (sig.length !== expected.length)
61
+ continue;
62
+ let diff = 0;
63
+ for (let i = 0;i < sig.length; i++) {
64
+ diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
65
+ }
66
+ if (diff === 0)
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ export {
72
+ verify,
73
+ sign
74
+ };
75
+
76
+ //# debugId=AE46F7E38D913C0D64756E2164756E21
77
+ //# sourceMappingURL=standard-webhooks.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/crypto/standard-webhooks.ts"],
4
+ "sourcesContent": [
5
+ "import { createHmac, randomUUID } from \"node:crypto\";\n\n/**\n * Standard Webhooks signing helpers — https://standardwebhooks.com\n *\n * Produces the three headers that every Standard Webhooks receiver expects:\n * webhook-id — UUID identifying the delivery (used for dedup)\n * webhook-timestamp — unix seconds, receiver rejects if skew > tolerance\n * webhook-signature — space-separated list of `vN,<base64-hmac>` tuples\n *\n * The signed content is `{id}.{timestamp}.{body}`. The HMAC key is the raw\n * bytes of the secret. If the secret is a `whsec_`-prefixed base64 string\n * (the Svix convention) we base64-decode after stripping the prefix;\n * otherwise we use the UTF-8 bytes directly.\n */\n\nexport interface StandardWebhooksHeaders {\n\t\"webhook-id\": string;\n\t\"webhook-timestamp\": string;\n\t\"webhook-signature\": string;\n}\n\nexport interface SignOptions {\n\t/** Override the delivery id. Defaults to a random UUID v4. */\n\tid?: string;\n\t/** Override the timestamp (unix seconds). Defaults to `Date.now()`. */\n\ttimestampSeconds?: number;\n}\n\nfunction secretToKey(secret: string): Buffer {\n\tif (secret.startsWith(\"whsec_\")) {\n\t\treturn Buffer.from(secret.slice(\"whsec_\".length), \"base64\");\n\t}\n\treturn Buffer.from(secret, \"utf8\");\n}\n\nexport function sign(\n\tbody: string,\n\tsecret: string,\n\topts: SignOptions = {},\n): StandardWebhooksHeaders {\n\tconst id = opts.id ?? randomUUID();\n\tconst timestamp = String(\n\t\topts.timestampSeconds ?? Math.floor(Date.now() / 1000),\n\t);\n\tconst toSign = `${id}.${timestamp}.${body}`;\n\tconst key = secretToKey(secret);\n\tconst signature = createHmac(\"sha256\", key).update(toSign).digest(\"base64\");\n\treturn {\n\t\t\"webhook-id\": id,\n\t\t\"webhook-timestamp\": timestamp,\n\t\t\"webhook-signature\": `v1,${signature}`,\n\t};\n}\n\nexport interface VerifyOptions {\n\t/** Max clock skew in seconds. Default 5 minutes per spec. */\n\ttoleranceSeconds?: number;\n\t/** Current time in unix seconds. Injectable for testing. */\n\tnowSeconds?: number;\n}\n\nexport function verify(\n\tbody: string,\n\theaders:\n\t\t| StandardWebhooksHeaders\n\t\t| Record<string, string | string[] | undefined>,\n\tsecret: string,\n\topts: VerifyOptions = {},\n): boolean {\n\tconst pick = (k: string) => {\n\t\tconst v = (headers as Record<string, unknown>)[k];\n\t\treturn typeof v === \"string\" ? v : undefined;\n\t};\n\tconst id = pick(\"webhook-id\");\n\tconst timestamp = pick(\"webhook-timestamp\");\n\tconst sigHeader = pick(\"webhook-signature\");\n\tif (!id || !timestamp || !sigHeader) return false;\n\n\tconst ts = Number.parseInt(timestamp, 10);\n\tif (!Number.isFinite(ts)) return false;\n\tconst tolerance = opts.toleranceSeconds ?? 5 * 60;\n\tconst now = opts.nowSeconds ?? Math.floor(Date.now() / 1000);\n\tif (Math.abs(now - ts) > tolerance) return false;\n\n\tconst key = secretToKey(secret);\n\tconst expected = createHmac(\"sha256\", key)\n\t\t.update(`${id}.${timestamp}.${body}`)\n\t\t.digest(\"base64\");\n\n\t// webhook-signature can carry multiple versions: \"v1,abc v1a,def\"\n\tfor (const part of sigHeader.split(\" \")) {\n\t\tconst [version, sig] = part.split(\",\", 2);\n\t\tif (version !== \"v1\" || !sig) continue;\n\t\tif (sig.length !== expected.length) continue;\n\t\tlet diff = 0;\n\t\tfor (let i = 0; i < sig.length; i++) {\n\t\t\tdiff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n\t\t}\n\t\tif (diff === 0) return true;\n\t}\n\treturn false;\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AA6BA,SAAS,WAAW,CAAC,QAAwB;AAAA,EAC5C,IAAI,OAAO,WAAW,QAAQ,GAAG;AAAA,IAChC,OAAO,OAAO,KAAK,OAAO,MAAM,SAAS,MAAM,GAAG,QAAQ;AAAA,EAC3D;AAAA,EACA,OAAO,OAAO,KAAK,QAAQ,MAAM;AAAA;AAG3B,SAAS,IAAI,CACnB,MACA,QACA,OAAoB,CAAC,GACK;AAAA,EAC1B,MAAM,KAAK,KAAK,MAAM,WAAW;AAAA,EACjC,MAAM,YAAY,OACjB,KAAK,oBAAoB,KAAK,MAAM,KAAK,IAAI,IAAI,IAAI,CACtD;AAAA,EACA,MAAM,SAAS,GAAG,MAAM,aAAa;AAAA,EACrC,MAAM,MAAM,YAAY,MAAM;AAAA,EAC9B,MAAM,YAAY,WAAW,UAAU,GAAG,EAAE,OAAO,MAAM,EAAE,OAAO,QAAQ;AAAA,EAC1E,OAAO;AAAA,IACN,cAAc;AAAA,IACd,qBAAqB;AAAA,IACrB,qBAAqB,MAAM;AAAA,EAC5B;AAAA;AAUM,SAAS,MAAM,CACrB,MACA,SAGA,QACA,OAAsB,CAAC,GACb;AAAA,EACV,MAAM,OAAO,CAAC,MAAc;AAAA,IAC3B,MAAM,IAAK,QAAoC;AAAA,IAC/C,OAAO,OAAO,MAAM,WAAW,IAAI;AAAA;AAAA,EAEpC,MAAM,KAAK,KAAK,YAAY;AAAA,EAC5B,MAAM,YAAY,KAAK,mBAAmB;AAAA,EAC1C,MAAM,YAAY,KAAK,mBAAmB;AAAA,EAC1C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC;AAAA,IAAW,OAAO;AAAA,EAE5C,MAAM,KAAK,OAAO,SAAS,WAAW,EAAE;AAAA,EACxC,IAAI,CAAC,OAAO,SAAS,EAAE;AAAA,IAAG,OAAO;AAAA,EACjC,MAAM,YAAY,KAAK,oBAAoB,IAAI;AAAA,EAC/C,MAAM,MAAM,KAAK,cAAc,KAAK,MAAM,KAAK,IAAI,IAAI,IAAI;AAAA,EAC3D,IAAI,KAAK,IAAI,MAAM,EAAE,IAAI;AAAA,IAAW,OAAO;AAAA,EAE3C,MAAM,MAAM,YAAY,MAAM;AAAA,EAC9B,MAAM,WAAW,WAAW,UAAU,GAAG,EACvC,OAAO,GAAG,MAAM,aAAa,MAAM,EACnC,OAAO,QAAQ;AAAA,EAGjB,WAAW,QAAQ,UAAU,MAAM,GAAG,GAAG;AAAA,IACxC,OAAO,SAAS,OAAO,KAAK,MAAM,KAAK,CAAC;AAAA,IACxC,IAAI,YAAY,QAAQ,CAAC;AAAA,MAAK;AAAA,IAC9B,IAAI,IAAI,WAAW,SAAS;AAAA,MAAQ;AAAA,IACpC,IAAI,OAAO;AAAA,IACX,SAAS,IAAI,EAAG,IAAI,IAAI,QAAQ,KAAK;AAAA,MACpC,QAAQ,IAAI,WAAW,CAAC,IAAI,SAAS,WAAW,CAAC;AAAA,IAClD;AAAA,IACA,IAAI,SAAS;AAAA,MAAG,OAAO;AAAA,EACxB;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "AE46F7E38D913C0D64756E2164756E21",
9
+ "names": []
10
+ }
@@ -295,6 +295,9 @@ interface Database {
295
295
  tenant_compute_addons: TenantComputeAddonsTable;
296
296
  account_spend_caps: AccountSpendCapsTable;
297
297
  provisioning_audit_log: ProvisioningAuditLogTable;
298
+ subscriptions: SubscriptionsTable;
299
+ subscription_outbox: SubscriptionOutboxTable;
300
+ subscription_deliveries: SubscriptionDeliveriesTable;
298
301
  }
299
302
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
300
303
  interface TenantsTable {
@@ -434,6 +437,76 @@ type InsertChatSession = Insertable<ChatSessionsTable>;
434
437
  type UpdateChatSession = Updateable<ChatSessionsTable>;
435
438
  type ChatMessage = Selectable<ChatMessagesTable>;
436
439
  type InsertChatMessage = Insertable<ChatMessagesTable>;
440
+ type SubscriptionStatus = "active" | "paused" | "error";
441
+ type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
442
+ type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
443
+ interface SubscriptionsTable {
444
+ id: Generated<string>;
445
+ account_id: string;
446
+ project_id: string | null;
447
+ name: string;
448
+ status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
449
+ subgraph_name: string;
450
+ table_name: string;
451
+ filter: Generated<unknown>;
452
+ format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
453
+ runtime: SubscriptionRuntime | null;
454
+ url: string;
455
+ signing_secret_enc: Buffer;
456
+ auth_config: Generated<unknown>;
457
+ max_retries: Generated<number>;
458
+ timeout_ms: Generated<number>;
459
+ concurrency: Generated<number>;
460
+ circuit_failures: Generated<number>;
461
+ circuit_opened_at: Date | null;
462
+ last_delivery_at: Date | null;
463
+ last_success_at: Date | null;
464
+ last_error: string | null;
465
+ created_at: Generated<Date>;
466
+ updated_at: Generated<Date>;
467
+ }
468
+ type Subscription = Selectable<SubscriptionsTable>;
469
+ type InsertSubscription = Insertable<SubscriptionsTable>;
470
+ type UpdateSubscription = Updateable<SubscriptionsTable>;
471
+ type OutboxStatus = "pending" | "delivered" | "dead";
472
+ interface SubscriptionOutboxTable {
473
+ id: Generated<string>;
474
+ subscription_id: string;
475
+ subgraph_name: string;
476
+ table_name: string;
477
+ block_height: number | bigint;
478
+ tx_id: string | null;
479
+ row_pk: unknown;
480
+ event_type: string;
481
+ payload: unknown;
482
+ dedup_key: string;
483
+ attempt: Generated<number>;
484
+ next_attempt_at: Generated<Date>;
485
+ status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
486
+ is_replay: Generated<boolean>;
487
+ delivered_at: Date | null;
488
+ failed_at: Date | null;
489
+ locked_by: string | null;
490
+ locked_until: Date | null;
491
+ created_at: Generated<Date>;
492
+ }
493
+ type SubscriptionOutbox = Selectable<SubscriptionOutboxTable>;
494
+ type InsertSubscriptionOutbox = Insertable<SubscriptionOutboxTable>;
495
+ type UpdateSubscriptionOutbox = Updateable<SubscriptionOutboxTable>;
496
+ interface SubscriptionDeliveriesTable {
497
+ id: Generated<string>;
498
+ outbox_id: string;
499
+ subscription_id: string;
500
+ attempt: number;
501
+ status_code: number | null;
502
+ response_headers: unknown | null;
503
+ response_body: string | null;
504
+ error_message: string | null;
505
+ duration_ms: number | null;
506
+ dispatched_at: Generated<Date>;
507
+ }
508
+ type SubscriptionDelivery = Selectable<SubscriptionDeliveriesTable>;
509
+ type InsertSubscriptionDelivery = Insertable<SubscriptionDeliveriesTable>;
437
510
  import { sql } from "kysely";
438
511
  /**
439
512
  * Kysely instance for the SOURCE DB (block/tx/event reads from the shared
@@ -459,4 +532,4 @@ declare function getDb(connectionString?: string): Kysely<Database>;
459
532
  declare function getRawClient(role?: "source" | "target"): ReturnType<typeof postgres>;
460
533
  /** Close all DB connection pools. Call in CLI commands to allow process exit. */
461
534
  declare function closeDb(): Promise<void>;
462
- export { sql, parseJsonb, jsonb, getTargetDb, getSourceDb, getRawClient, getDb, closeDb, WaitlistTable, UsageSnapshotsTable, UsageSnapshot, UsageDailyTable, UsageDaily, UpdateTransaction, UpdateTenantUsageMonthly, UpdateTenantComputeAddon, UpdateTenant, UpdateSubgraph, UpdateProject, UpdateIndexProgress, UpdateEvent, UpdateChatSession, UpdateBlock, UpdateApiKey, UpdateAccountSpendCap, TransactionsTable, Transaction, TenantsTable, TenantUsageMonthlyTable, TenantUsageMonthly, TenantStatus, TenantComputeAddonsTable, TenantComputeAddon, Tenant, TeamMembersTable, TeamMember, TeamInvitationsTable, TeamInvitation, SubgraphsTable, SubgraphUsageDailyTable, SubgraphUsageDaily, SubgraphTableSnapshotsTable, SubgraphProcessingStatsTable, SubgraphHealthSnapshotsTable, SubgraphHealthSnapshot, SubgraphGapsTable, SubgraphGap, Subgraph, SessionsTable, Session, ProvisioningAuditStatus, ProvisioningAuditLogTable, ProvisioningAuditLog, ProvisioningAuditEvent, ProjectsTable, Project, MagicLinksTable, MagicLink, InsertTransaction, InsertTenantUsageMonthly, InsertTenantComputeAddon, InsertTenant, InsertTeamMember, InsertTeamInvitation, InsertSubgraphUsageDaily, InsertSubgraphHealthSnapshot, InsertSubgraphGap, InsertSubgraph, InsertSession, InsertProvisioningAuditLog, InsertProject, InsertMagicLink, InsertIndexProgress, InsertEvent, InsertChatSession, InsertChatMessage, InsertBlock, InsertApiKey, InsertAccountSpendCap, InsertAccountInsight, InsertAccountAgentRun, InsertAccount, IndexProgressTable, IndexProgress, EventsTable, Event, Database, ChatSessionsTable, ChatSession, ChatMessagesTable, ChatMessage, BlocksTable, Block, ApiKeysTable, ApiKey, AccountsTable, AccountSpendCapsTable, AccountSpendCap, AccountInsightsTable, AccountInsight, AccountAgentRunsTable, AccountAgentRun, Account };
535
+ export { sql, parseJsonb, jsonb, getTargetDb, getSourceDb, getRawClient, getDb, closeDb, WaitlistTable, UsageSnapshotsTable, UsageSnapshot, UsageDailyTable, UsageDaily, UpdateTransaction, UpdateTenantUsageMonthly, UpdateTenantComputeAddon, UpdateTenant, UpdateSubscriptionOutbox, UpdateSubscription, UpdateSubgraph, UpdateProject, UpdateIndexProgress, UpdateEvent, UpdateChatSession, UpdateBlock, UpdateApiKey, UpdateAccountSpendCap, TransactionsTable, Transaction, TenantsTable, TenantUsageMonthlyTable, TenantUsageMonthly, TenantStatus, TenantComputeAddonsTable, TenantComputeAddon, Tenant, TeamMembersTable, TeamMember, TeamInvitationsTable, TeamInvitation, SubscriptionsTable, SubscriptionStatus, SubscriptionRuntime, SubscriptionOutboxTable, SubscriptionOutbox, SubscriptionFormat, SubscriptionDelivery, SubscriptionDeliveriesTable, Subscription, SubgraphsTable, SubgraphUsageDailyTable, SubgraphUsageDaily, SubgraphTableSnapshotsTable, SubgraphProcessingStatsTable, SubgraphHealthSnapshotsTable, SubgraphHealthSnapshot, SubgraphGapsTable, SubgraphGap, Subgraph, SessionsTable, Session, ProvisioningAuditStatus, ProvisioningAuditLogTable, ProvisioningAuditLog, ProvisioningAuditEvent, ProjectsTable, Project, OutboxStatus, MagicLinksTable, MagicLink, InsertTransaction, InsertTenantUsageMonthly, InsertTenantComputeAddon, InsertTenant, InsertTeamMember, InsertTeamInvitation, InsertSubscriptionOutbox, InsertSubscriptionDelivery, InsertSubscription, InsertSubgraphUsageDaily, InsertSubgraphHealthSnapshot, InsertSubgraphGap, InsertSubgraph, InsertSession, InsertProvisioningAuditLog, InsertProject, InsertMagicLink, InsertIndexProgress, InsertEvent, InsertChatSession, InsertChatMessage, InsertBlock, InsertApiKey, InsertAccountSpendCap, InsertAccountInsight, InsertAccountAgentRun, InsertAccount, IndexProgressTable, IndexProgress, EventsTable, Event, Database, ChatSessionsTable, ChatSession, ChatMessagesTable, ChatMessage, BlocksTable, Block, ApiKeysTable, ApiKey, AccountsTable, AccountSpendCapsTable, AccountSpendCap, AccountInsightsTable, AccountInsight, AccountAgentRunsTable, AccountAgentRun, Account };
@@ -277,6 +277,9 @@ interface Database {
277
277
  tenant_compute_addons: TenantComputeAddonsTable;
278
278
  account_spend_caps: AccountSpendCapsTable;
279
279
  provisioning_audit_log: ProvisioningAuditLogTable;
280
+ subscriptions: SubscriptionsTable;
281
+ subscription_outbox: SubscriptionOutboxTable;
282
+ subscription_deliveries: SubscriptionDeliveriesTable;
280
283
  }
281
284
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
282
285
  interface TenantsTable {
@@ -356,6 +359,68 @@ interface ProvisioningAuditLogTable {
356
359
  error: string | null;
357
360
  created_at: Generated<Date>;
358
361
  }
362
+ type SubscriptionStatus = "active" | "paused" | "error";
363
+ type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
364
+ type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
365
+ interface SubscriptionsTable {
366
+ id: Generated<string>;
367
+ account_id: string;
368
+ project_id: string | null;
369
+ name: string;
370
+ status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
371
+ subgraph_name: string;
372
+ table_name: string;
373
+ filter: Generated<unknown>;
374
+ format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
375
+ runtime: SubscriptionRuntime | null;
376
+ url: string;
377
+ signing_secret_enc: Buffer;
378
+ auth_config: Generated<unknown>;
379
+ max_retries: Generated<number>;
380
+ timeout_ms: Generated<number>;
381
+ concurrency: Generated<number>;
382
+ circuit_failures: Generated<number>;
383
+ circuit_opened_at: Date | null;
384
+ last_delivery_at: Date | null;
385
+ last_success_at: Date | null;
386
+ last_error: string | null;
387
+ created_at: Generated<Date>;
388
+ updated_at: Generated<Date>;
389
+ }
390
+ type OutboxStatus = "pending" | "delivered" | "dead";
391
+ interface SubscriptionOutboxTable {
392
+ id: Generated<string>;
393
+ subscription_id: string;
394
+ subgraph_name: string;
395
+ table_name: string;
396
+ block_height: number | bigint;
397
+ tx_id: string | null;
398
+ row_pk: unknown;
399
+ event_type: string;
400
+ payload: unknown;
401
+ dedup_key: string;
402
+ attempt: Generated<number>;
403
+ next_attempt_at: Generated<Date>;
404
+ status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
405
+ is_replay: Generated<boolean>;
406
+ delivered_at: Date | null;
407
+ failed_at: Date | null;
408
+ locked_by: string | null;
409
+ locked_until: Date | null;
410
+ created_at: Generated<Date>;
411
+ }
412
+ interface SubscriptionDeliveriesTable {
413
+ id: Generated<string>;
414
+ outbox_id: string;
415
+ subscription_id: string;
416
+ attempt: number;
417
+ status_code: number | null;
418
+ response_headers: unknown | null;
419
+ response_body: string | null;
420
+ error_message: string | null;
421
+ duration_ms: number | null;
422
+ dispatched_at: Generated<Date>;
423
+ }
359
424
  /**
360
425
  * Spend-cap state for an account. Both the metering crons (check + set
361
426
  * frozen_at) and the dashboard (read + update caps) call through here.
@@ -277,6 +277,9 @@ interface Database {
277
277
  tenant_compute_addons: TenantComputeAddonsTable;
278
278
  account_spend_caps: AccountSpendCapsTable;
279
279
  provisioning_audit_log: ProvisioningAuditLogTable;
280
+ subscriptions: SubscriptionsTable;
281
+ subscription_outbox: SubscriptionOutboxTable;
282
+ subscription_deliveries: SubscriptionDeliveriesTable;
280
283
  }
281
284
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
282
285
  interface TenantsTable {
@@ -354,6 +357,68 @@ interface ProvisioningAuditLogTable {
354
357
  error: string | null;
355
358
  created_at: Generated<Date>;
356
359
  }
360
+ type SubscriptionStatus = "active" | "paused" | "error";
361
+ type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
362
+ type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
363
+ interface SubscriptionsTable {
364
+ id: Generated<string>;
365
+ account_id: string;
366
+ project_id: string | null;
367
+ name: string;
368
+ status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
369
+ subgraph_name: string;
370
+ table_name: string;
371
+ filter: Generated<unknown>;
372
+ format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
373
+ runtime: SubscriptionRuntime | null;
374
+ url: string;
375
+ signing_secret_enc: Buffer;
376
+ auth_config: Generated<unknown>;
377
+ max_retries: Generated<number>;
378
+ timeout_ms: Generated<number>;
379
+ concurrency: Generated<number>;
380
+ circuit_failures: Generated<number>;
381
+ circuit_opened_at: Date | null;
382
+ last_delivery_at: Date | null;
383
+ last_success_at: Date | null;
384
+ last_error: string | null;
385
+ created_at: Generated<Date>;
386
+ updated_at: Generated<Date>;
387
+ }
388
+ type OutboxStatus = "pending" | "delivered" | "dead";
389
+ interface SubscriptionOutboxTable {
390
+ id: Generated<string>;
391
+ subscription_id: string;
392
+ subgraph_name: string;
393
+ table_name: string;
394
+ block_height: number | bigint;
395
+ tx_id: string | null;
396
+ row_pk: unknown;
397
+ event_type: string;
398
+ payload: unknown;
399
+ dedup_key: string;
400
+ attempt: Generated<number>;
401
+ next_attempt_at: Generated<Date>;
402
+ status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
403
+ is_replay: Generated<boolean>;
404
+ delivered_at: Date | null;
405
+ failed_at: Date | null;
406
+ locked_by: string | null;
407
+ locked_until: Date | null;
408
+ created_at: Generated<Date>;
409
+ }
410
+ interface SubscriptionDeliveriesTable {
411
+ id: Generated<string>;
412
+ outbox_id: string;
413
+ subscription_id: string;
414
+ attempt: number;
415
+ status_code: number | null;
416
+ response_headers: unknown | null;
417
+ response_body: string | null;
418
+ error_message: string | null;
419
+ duration_ms: number | null;
420
+ dispatched_at: Generated<Date>;
421
+ }
357
422
  interface SparklinePoint {
358
423
  day: string;
359
424
  value: number;
@@ -278,6 +278,9 @@ interface Database {
278
278
  tenant_compute_addons: TenantComputeAddonsTable;
279
279
  account_spend_caps: AccountSpendCapsTable;
280
280
  provisioning_audit_log: ProvisioningAuditLogTable;
281
+ subscriptions: SubscriptionsTable;
282
+ subscription_outbox: SubscriptionOutboxTable;
283
+ subscription_deliveries: SubscriptionDeliveriesTable;
281
284
  }
282
285
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
283
286
  interface TenantsTable {
@@ -356,6 +359,68 @@ interface ProvisioningAuditLogTable {
356
359
  created_at: Generated<Date>;
357
360
  }
358
361
  type Account = Selectable<AccountsTable>;
362
+ type SubscriptionStatus = "active" | "paused" | "error";
363
+ type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
364
+ type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
365
+ interface SubscriptionsTable {
366
+ id: Generated<string>;
367
+ account_id: string;
368
+ project_id: string | null;
369
+ name: string;
370
+ status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
371
+ subgraph_name: string;
372
+ table_name: string;
373
+ filter: Generated<unknown>;
374
+ format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
375
+ runtime: SubscriptionRuntime | null;
376
+ url: string;
377
+ signing_secret_enc: Buffer;
378
+ auth_config: Generated<unknown>;
379
+ max_retries: Generated<number>;
380
+ timeout_ms: Generated<number>;
381
+ concurrency: Generated<number>;
382
+ circuit_failures: Generated<number>;
383
+ circuit_opened_at: Date | null;
384
+ last_delivery_at: Date | null;
385
+ last_success_at: Date | null;
386
+ last_error: string | null;
387
+ created_at: Generated<Date>;
388
+ updated_at: Generated<Date>;
389
+ }
390
+ type OutboxStatus = "pending" | "delivered" | "dead";
391
+ interface SubscriptionOutboxTable {
392
+ id: Generated<string>;
393
+ subscription_id: string;
394
+ subgraph_name: string;
395
+ table_name: string;
396
+ block_height: number | bigint;
397
+ tx_id: string | null;
398
+ row_pk: unknown;
399
+ event_type: string;
400
+ payload: unknown;
401
+ dedup_key: string;
402
+ attempt: Generated<number>;
403
+ next_attempt_at: Generated<Date>;
404
+ status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
405
+ is_replay: Generated<boolean>;
406
+ delivered_at: Date | null;
407
+ failed_at: Date | null;
408
+ locked_by: string | null;
409
+ locked_until: Date | null;
410
+ created_at: Generated<Date>;
411
+ }
412
+ interface SubscriptionDeliveriesTable {
413
+ id: Generated<string>;
414
+ outbox_id: string;
415
+ subscription_id: string;
416
+ attempt: number;
417
+ status_code: number | null;
418
+ response_headers: unknown | null;
419
+ response_body: string | null;
420
+ error_message: string | null;
421
+ duration_ms: number | null;
422
+ dispatched_at: Generated<Date>;
423
+ }
359
424
  declare function upsertAccount(db: Kysely<Database>, email: string): Promise<Account>;
360
425
  declare function getAccountById(db: Kysely<Database>, id: string): Promise<Account | null>;
361
426
  declare function updateAccountProfile(db: Kysely<Database>, id: string, data: {
@@ -277,6 +277,9 @@ interface Database {
277
277
  tenant_compute_addons: TenantComputeAddonsTable;
278
278
  account_spend_caps: AccountSpendCapsTable;
279
279
  provisioning_audit_log: ProvisioningAuditLogTable;
280
+ subscriptions: SubscriptionsTable;
281
+ subscription_outbox: SubscriptionOutboxTable;
282
+ subscription_deliveries: SubscriptionDeliveriesTable;
280
283
  }
281
284
  type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
282
285
  interface TenantsTable {
@@ -354,6 +357,68 @@ interface ProvisioningAuditLogTable {
354
357
  error: string | null;
355
358
  created_at: Generated<Date>;
356
359
  }
360
+ type SubscriptionStatus = "active" | "paused" | "error";
361
+ type SubscriptionFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
362
+ type SubscriptionRuntime = "inngest" | "trigger" | "cloudflare" | "node";
363
+ interface SubscriptionsTable {
364
+ id: Generated<string>;
365
+ account_id: string;
366
+ project_id: string | null;
367
+ name: string;
368
+ status: ColumnType<SubscriptionStatus, SubscriptionStatus | undefined, SubscriptionStatus>;
369
+ subgraph_name: string;
370
+ table_name: string;
371
+ filter: Generated<unknown>;
372
+ format: ColumnType<SubscriptionFormat, SubscriptionFormat | undefined, SubscriptionFormat>;
373
+ runtime: SubscriptionRuntime | null;
374
+ url: string;
375
+ signing_secret_enc: Buffer;
376
+ auth_config: Generated<unknown>;
377
+ max_retries: Generated<number>;
378
+ timeout_ms: Generated<number>;
379
+ concurrency: Generated<number>;
380
+ circuit_failures: Generated<number>;
381
+ circuit_opened_at: Date | null;
382
+ last_delivery_at: Date | null;
383
+ last_success_at: Date | null;
384
+ last_error: string | null;
385
+ created_at: Generated<Date>;
386
+ updated_at: Generated<Date>;
387
+ }
388
+ type OutboxStatus = "pending" | "delivered" | "dead";
389
+ interface SubscriptionOutboxTable {
390
+ id: Generated<string>;
391
+ subscription_id: string;
392
+ subgraph_name: string;
393
+ table_name: string;
394
+ block_height: number | bigint;
395
+ tx_id: string | null;
396
+ row_pk: unknown;
397
+ event_type: string;
398
+ payload: unknown;
399
+ dedup_key: string;
400
+ attempt: Generated<number>;
401
+ next_attempt_at: Generated<Date>;
402
+ status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
403
+ is_replay: Generated<boolean>;
404
+ delivered_at: Date | null;
405
+ failed_at: Date | null;
406
+ locked_by: string | null;
407
+ locked_until: Date | null;
408
+ created_at: Generated<Date>;
409
+ }
410
+ interface SubscriptionDeliveriesTable {
411
+ id: Generated<string>;
412
+ outbox_id: string;
413
+ subscription_id: string;
414
+ attempt: number;
415
+ status_code: number | null;
416
+ response_headers: unknown | null;
417
+ response_body: string | null;
418
+ error_message: string | null;
419
+ duration_ms: number | null;
420
+ dispatched_at: Generated<Date>;
421
+ }
357
422
  interface Gap {
358
423
  gapStart: number;
359
424
  gapEnd: number;