@secondlayer/shared 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/crypto/secrets.d.ts +5 -0
- package/dist/src/crypto/secrets.js +69 -0
- package/dist/src/crypto/secrets.js.map +10 -0
- package/dist/src/db/index.d.ts +91 -6
- package/dist/src/db/index.js +53 -29
- package/dist/src/db/index.js.map +4 -4
- package/dist/src/db/jsonb.js.map +2 -2
- package/dist/src/db/queries/accounts.d.ts +59 -2
- package/dist/src/db/queries/integrity.d.ts +59 -2
- package/dist/src/db/queries/marketplace.d.ts +59 -2
- package/dist/src/db/queries/marketplace.js +6 -9
- package/dist/src/db/queries/marketplace.js.map +3 -3
- package/dist/src/db/queries/projects.d.ts +59 -2
- package/dist/src/db/queries/projects.js.map +2 -2
- package/dist/src/db/queries/subgraph-gaps.d.ts +59 -2
- package/dist/src/db/queries/subgraphs.d.ts +63 -5
- package/dist/src/db/queries/subgraphs.js +3 -9
- package/dist/src/db/queries/subgraphs.js.map +4 -4
- package/dist/src/db/queries/tenants.d.ts +493 -0
- package/dist/src/db/queries/tenants.js +194 -0
- package/dist/src/db/queries/tenants.js.map +11 -0
- package/dist/src/db/queries/usage.d.ts +59 -2
- package/dist/src/db/queries/usage.js +3 -3
- package/dist/src/db/queries/usage.js.map +3 -3
- package/dist/src/db/queries/workflows.d.ts +59 -2
- package/dist/src/db/queries/workflows.js +31 -3
- package/dist/src/db/queries/workflows.js.map +4 -4
- package/dist/src/db/schema.d.ts +69 -3
- package/dist/src/env.d.ts +10 -0
- package/dist/src/env.js +3 -1
- package/dist/src/env.js.map +3 -3
- package/dist/src/errors.d.ts +17 -3
- package/dist/src/errors.js +34 -3
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +117 -8
- package/dist/src/index.js +88 -31
- package/dist/src/index.js.map +6 -6
- package/dist/src/logger.js +3 -1
- package/dist/src/logger.js.map +3 -3
- package/dist/src/mode.d.ts +30 -0
- package/dist/src/mode.js +43 -0
- package/dist/src/mode.js.map +10 -0
- package/dist/src/node/archive-client.js +3 -1
- package/dist/src/node/archive-client.js.map +3 -3
- package/dist/src/node/hiro-client.js +3 -1
- package/dist/src/node/hiro-client.js.map +3 -3
- package/dist/src/node/local-client.d.ts +59 -2
- package/dist/src/pricing.d.ts +28 -0
- package/dist/src/pricing.js +47 -0
- package/dist/src/pricing.js.map +10 -0
- package/dist/src/queue/listener.d.ts +11 -2
- package/dist/src/queue/listener.js +11 -12
- package/dist/src/queue/listener.js.map +3 -3
- package/dist/src/types.d.ts +10 -0
- package/migrations/0033_workflow_steps_memo_key.ts +54 -0
- package/migrations/0034_workflow_signer_secrets.ts +42 -0
- package/migrations/0035_workflow_budgets.ts +53 -0
- package/migrations/0036_tx_confirmed_notify.ts +36 -0
- package/migrations/0037_nullable_api_key.ts +35 -0
- package/migrations/0038_drop_workflow_tables.ts +46 -0
- package/migrations/0039_tenants.ts +66 -0
- package/migrations/0040_tenant_key_generations.ts +29 -0
- package/migrations/0041_subgraphs_drop_api_key_id.ts +49 -0
- package/migrations/0042_tenant_project_id.ts +25 -0
- package/package.json +18 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
|
-
import { Generated } from "kysely";
|
|
2
|
+
import { ColumnType, Generated } from "kysely";
|
|
3
3
|
interface BlocksTable {
|
|
4
4
|
height: number;
|
|
5
5
|
hash: string;
|
|
@@ -56,7 +56,6 @@ interface SubgraphsTable {
|
|
|
56
56
|
last_error_at: Date | null;
|
|
57
57
|
total_processed: Generated<number>;
|
|
58
58
|
total_errors: Generated<number>;
|
|
59
|
-
api_key_id: string | null;
|
|
60
59
|
account_id: string;
|
|
61
60
|
handler_code: string | null;
|
|
62
61
|
source_code: string | null;
|
|
@@ -295,6 +294,8 @@ interface WorkflowStepsTable {
|
|
|
295
294
|
started_at: Date | null;
|
|
296
295
|
completed_at: Date | null;
|
|
297
296
|
duration_ms: number | null;
|
|
297
|
+
memo_key: string | null;
|
|
298
|
+
parent_step_id: string | null;
|
|
298
299
|
created_at: Generated<Date>;
|
|
299
300
|
}
|
|
300
301
|
interface WorkflowQueueTable {
|
|
@@ -356,6 +357,62 @@ interface Database {
|
|
|
356
357
|
workflow_queue: WorkflowQueueTable;
|
|
357
358
|
workflow_schedules: WorkflowSchedulesTable;
|
|
358
359
|
workflow_cursors: WorkflowCursorsTable;
|
|
360
|
+
workflow_signer_secrets: WorkflowSignerSecretsTable;
|
|
361
|
+
workflow_budgets: WorkflowBudgetsTable;
|
|
362
|
+
tenants: TenantsTable;
|
|
363
|
+
}
|
|
364
|
+
type TenantStatus = "provisioning" | "active" | "suspended" | "error" | "deleted";
|
|
365
|
+
interface TenantsTable {
|
|
366
|
+
id: Generated<string>;
|
|
367
|
+
account_id: string;
|
|
368
|
+
slug: string;
|
|
369
|
+
status: ColumnType<TenantStatus, TenantStatus | undefined, TenantStatus>;
|
|
370
|
+
plan: string;
|
|
371
|
+
cpus: ColumnType<number, number | string, number | string>;
|
|
372
|
+
memory_mb: number;
|
|
373
|
+
storage_limit_mb: number;
|
|
374
|
+
storage_used_mb: number | null;
|
|
375
|
+
pg_container_id: string | null;
|
|
376
|
+
api_container_id: string | null;
|
|
377
|
+
processor_container_id: string | null;
|
|
378
|
+
target_database_url_enc: Buffer;
|
|
379
|
+
tenant_jwt_secret_enc: Buffer;
|
|
380
|
+
anon_key_enc: Buffer;
|
|
381
|
+
service_key_enc: Buffer;
|
|
382
|
+
api_url_internal: string;
|
|
383
|
+
api_url_public: string;
|
|
384
|
+
trial_ends_at: Date;
|
|
385
|
+
suspended_at: Date | null;
|
|
386
|
+
last_health_check_at: Date | null;
|
|
387
|
+
service_gen: Generated<number>;
|
|
388
|
+
anon_gen: Generated<number>;
|
|
389
|
+
project_id: string | null;
|
|
390
|
+
created_at: Generated<Date>;
|
|
391
|
+
updated_at: Generated<Date>;
|
|
392
|
+
}
|
|
393
|
+
interface WorkflowBudgetsTable {
|
|
394
|
+
id: Generated<string>;
|
|
395
|
+
workflow_definition_id: string;
|
|
396
|
+
/** Period key: "daily:YYYY-MM-DD" | "weekly:YYYY-Www" | "per-run:<uuid>". */
|
|
397
|
+
period: string;
|
|
398
|
+
ai_usd_used: Generated<string>;
|
|
399
|
+
ai_tokens_used: Generated<string>;
|
|
400
|
+
chain_microstx_used: Generated<string>;
|
|
401
|
+
chain_tx_count: Generated<number>;
|
|
402
|
+
run_count: Generated<number>;
|
|
403
|
+
step_count: Generated<number>;
|
|
404
|
+
reset_at: Date;
|
|
405
|
+
created_at: Generated<Date>;
|
|
406
|
+
updated_at: Generated<Date>;
|
|
407
|
+
}
|
|
408
|
+
interface WorkflowSignerSecretsTable {
|
|
409
|
+
id: Generated<string>;
|
|
410
|
+
account_id: string;
|
|
411
|
+
name: string;
|
|
412
|
+
/** AES-GCM ciphertext bytes produced by the runner's KMS on write. */
|
|
413
|
+
encrypted_value: Buffer;
|
|
414
|
+
created_at: Generated<Date>;
|
|
415
|
+
updated_at: Generated<Date>;
|
|
359
416
|
}
|
|
360
417
|
/** Matches the NewBlockPayload shape expected by the indexer's /new_block endpoint */
|
|
361
418
|
interface ReplayBlockPayload {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token → USD pricing constants for internal observability.
|
|
3
|
+
*
|
|
4
|
+
* **Not customer billing.** Tier pricing covers compute; these constants
|
|
5
|
+
* let the dashboard display "~$X spent in AI today" and let us track
|
|
6
|
+
* gross-margin internally. Update manually when providers change prices
|
|
7
|
+
* (roughly twice per year).
|
|
8
|
+
*
|
|
9
|
+
* Source: public provider pricing pages as of 2026-04-17.
|
|
10
|
+
*/
|
|
11
|
+
interface ModelPricing {
|
|
12
|
+
/** USD per 1M input tokens */
|
|
13
|
+
inputPerMTokens: number;
|
|
14
|
+
/** USD per 1M output tokens */
|
|
15
|
+
outputPerMTokens: number;
|
|
16
|
+
}
|
|
17
|
+
declare const MODEL_PRICING: Record<string, Record<string, ModelPricing>>;
|
|
18
|
+
interface TokenUsage {
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute approximate USD cost for an AI step. Returns `null` if the
|
|
24
|
+
* (provider, model) pair isn't in the pricing table — the caller should
|
|
25
|
+
* display "-" rather than "$0".
|
|
26
|
+
*/
|
|
27
|
+
declare function computeUsdCost(provider: string, modelId: string, usage: TokenUsage): number | null;
|
|
28
|
+
export { computeUsdCost, TokenUsage, ModelPricing, MODEL_PRICING };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/pricing.ts
|
|
18
|
+
var MODEL_PRICING = {
|
|
19
|
+
anthropic: {
|
|
20
|
+
"claude-haiku-4-5": { inputPerMTokens: 1, outputPerMTokens: 5 },
|
|
21
|
+
"claude-haiku-4-5-20251001": { inputPerMTokens: 1, outputPerMTokens: 5 },
|
|
22
|
+
"claude-sonnet-4-6": { inputPerMTokens: 3, outputPerMTokens: 15 },
|
|
23
|
+
"claude-opus-4-7": { inputPerMTokens: 15, outputPerMTokens: 75 }
|
|
24
|
+
},
|
|
25
|
+
openai: {
|
|
26
|
+
"gpt-4.1": { inputPerMTokens: 2.5, outputPerMTokens: 10 },
|
|
27
|
+
"gpt-4o": { inputPerMTokens: 2.5, outputPerMTokens: 10 },
|
|
28
|
+
"gpt-4o-mini": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 }
|
|
29
|
+
},
|
|
30
|
+
google: {
|
|
31
|
+
"gemini-2.5-pro": { inputPerMTokens: 1.25, outputPerMTokens: 10 },
|
|
32
|
+
"gemini-2.5-flash": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function computeUsdCost(provider, modelId, usage) {
|
|
36
|
+
const p = MODEL_PRICING[provider]?.[modelId];
|
|
37
|
+
if (!p)
|
|
38
|
+
return null;
|
|
39
|
+
return usage.inputTokens * p.inputPerMTokens / 1e6 + usage.outputTokens * p.outputPerMTokens / 1e6;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
computeUsdCost,
|
|
43
|
+
MODEL_PRICING
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//# debugId=4DD95AFC388E4B9664756E2164756E21
|
|
47
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/pricing.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Token → USD pricing constants for internal observability.\n *\n * **Not customer billing.** Tier pricing covers compute; these constants\n * let the dashboard display \"~$X spent in AI today\" and let us track\n * gross-margin internally. Update manually when providers change prices\n * (roughly twice per year).\n *\n * Source: public provider pricing pages as of 2026-04-17.\n */\n\nexport interface ModelPricing {\n\t/** USD per 1M input tokens */\n\tinputPerMTokens: number;\n\t/** USD per 1M output tokens */\n\toutputPerMTokens: number;\n}\n\nexport const MODEL_PRICING: Record<string, Record<string, ModelPricing>> = {\n\tanthropic: {\n\t\t\"claude-haiku-4-5\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-haiku-4-5-20251001\": { inputPerMTokens: 1, outputPerMTokens: 5 },\n\t\t\"claude-sonnet-4-6\": { inputPerMTokens: 3, outputPerMTokens: 15 },\n\t\t\"claude-opus-4-7\": { inputPerMTokens: 15, outputPerMTokens: 75 },\n\t},\n\topenai: {\n\t\t\"gpt-4.1\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o\": { inputPerMTokens: 2.5, outputPerMTokens: 10 },\n\t\t\"gpt-4o-mini\": { inputPerMTokens: 0.15, outputPerMTokens: 0.6 },\n\t},\n\tgoogle: {\n\t\t\"gemini-2.5-pro\": { inputPerMTokens: 1.25, outputPerMTokens: 10 },\n\t\t\"gemini-2.5-flash\": { inputPerMTokens: 0.3, outputPerMTokens: 2.5 },\n\t},\n};\n\nexport interface TokenUsage {\n\tinputTokens: number;\n\toutputTokens: number;\n}\n\n/**\n * Compute approximate USD cost for an AI step. Returns `null` if the\n * (provider, model) pair isn't in the pricing table — the caller should\n * display \"-\" rather than \"$0\".\n */\nexport function computeUsdCost(\n\tprovider: string,\n\tmodelId: string,\n\tusage: TokenUsage,\n): number | null {\n\tconst p = MODEL_PRICING[provider]?.[modelId];\n\tif (!p) return null;\n\treturn (\n\t\t(usage.inputTokens * p.inputPerMTokens) / 1_000_000 +\n\t\t(usage.outputTokens * p.outputPerMTokens) / 1_000_000\n\t);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAkBO,IAAM,gBAA8D;AAAA,EAC1E,WAAW;AAAA,IACV,oBAAoB,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IAC9D,6BAA6B,EAAE,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,IACvE,qBAAqB,EAAE,iBAAiB,GAAG,kBAAkB,GAAG;AAAA,IAChE,mBAAmB,EAAE,iBAAiB,IAAI,kBAAkB,GAAG;AAAA,EAChE;AAAA,EACA,QAAQ;AAAA,IACP,WAAW,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACxD,UAAU,EAAE,iBAAiB,KAAK,kBAAkB,GAAG;AAAA,IACvD,eAAe,EAAE,iBAAiB,MAAM,kBAAkB,IAAI;AAAA,EAC/D;AAAA,EACA,QAAQ;AAAA,IACP,kBAAkB,EAAE,iBAAiB,MAAM,kBAAkB,GAAG;AAAA,IAChE,oBAAoB,EAAE,iBAAiB,KAAK,kBAAkB,IAAI;AAAA,EACnE;AACD;AAYO,SAAS,cAAc,CAC7B,UACA,SACA,OACgB;AAAA,EAChB,MAAM,IAAI,cAAc,YAAY;AAAA,EACpC,IAAI,CAAC;AAAA,IAAG,OAAO;AAAA,EACf,OACE,MAAM,cAAc,EAAE,kBAAmB,MACzC,MAAM,eAAe,EAAE,mBAAoB;AAAA;",
|
|
8
|
+
"debugId": "4DD95AFC388E4B9664756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
interface ListenOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Connection string to LISTEN on. Defaults to `process.env.DATABASE_URL`.
|
|
4
|
+
* In dual-DB mode, pass `SOURCE_DATABASE_URL` for indexer-fired channels
|
|
5
|
+
* (`indexer:new_block`, `subgraph_reorg`, `tx:confirmed`) and
|
|
6
|
+
* `TARGET_DATABASE_URL` for tenant-local channels (`subgraph_changes`).
|
|
7
|
+
*/
|
|
8
|
+
connectionString?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function listen(channel: string, callback: (payload?: string) => void, opts?: ListenOptions): Promise<() => Promise<void>>;
|
|
11
|
+
declare function notify(channel: string, payload?: string, opts?: ListenOptions): Promise<void>;
|
|
3
12
|
export { notify, listen };
|
|
@@ -16,12 +16,15 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// src/queue/listener.ts
|
|
18
18
|
import postgres from "postgres";
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
22
|
-
throw new Error("
|
|
19
|
+
function resolveUrl(opts) {
|
|
20
|
+
const url = opts?.connectionString ?? process.env.DATABASE_URL;
|
|
21
|
+
if (!url) {
|
|
22
|
+
throw new Error("listen/notify requires a connection string (opts.connectionString or DATABASE_URL)");
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
async function listen(channel, callback, opts) {
|
|
27
|
+
const client = postgres(resolveUrl(opts), {
|
|
25
28
|
max: 1,
|
|
26
29
|
onnotice: () => {}
|
|
27
30
|
});
|
|
@@ -32,12 +35,8 @@ async function listen(channel, callback) {
|
|
|
32
35
|
await client.end();
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
async function notify(channel, payload) {
|
|
36
|
-
const
|
|
37
|
-
if (!connectionString) {
|
|
38
|
-
throw new Error("DATABASE_URL is required");
|
|
39
|
-
}
|
|
40
|
-
const client = postgres(connectionString, { max: 1 });
|
|
38
|
+
async function notify(channel, payload, opts) {
|
|
39
|
+
const client = postgres(resolveUrl(opts), { max: 1 });
|
|
41
40
|
try {
|
|
42
41
|
if (payload) {
|
|
43
42
|
await client`SELECT pg_notify(${channel}, ${payload})`;
|
|
@@ -53,5 +52,5 @@ export {
|
|
|
53
52
|
listen
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
//# debugId=
|
|
55
|
+
//# debugId=B44CC52F9682E31564756E2164756E21
|
|
57
56
|
//# sourceMappingURL=listener.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/queue/listener.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import postgres from \"postgres\";\n\
|
|
5
|
+
"import postgres from \"postgres\";\n\ninterface ListenOptions {\n\t/**\n\t * Connection string to LISTEN on. Defaults to `process.env.DATABASE_URL`.\n\t * In dual-DB mode, pass `SOURCE_DATABASE_URL` for indexer-fired channels\n\t * (`indexer:new_block`, `subgraph_reorg`, `tx:confirmed`) and\n\t * `TARGET_DATABASE_URL` for tenant-local channels (`subgraph_changes`).\n\t */\n\tconnectionString?: string;\n}\n\nfunction resolveUrl(opts?: ListenOptions): string {\n\tconst url = opts?.connectionString ?? process.env.DATABASE_URL;\n\tif (!url) {\n\t\tthrow new Error(\n\t\t\t\"listen/notify requires a connection string (opts.connectionString or DATABASE_URL)\",\n\t\t);\n\t}\n\treturn url;\n}\n\nexport async function listen(\n\tchannel: string,\n\tcallback: (payload?: string) => void,\n\topts?: ListenOptions,\n): Promise<() => Promise<void>> {\n\tconst client = postgres(resolveUrl(opts), {\n\t\tmax: 1,\n\t\tonnotice: () => {},\n\t});\n\n\tawait client.listen(channel, (payload) => {\n\t\tcallback(payload);\n\t});\n\n\treturn async () => {\n\t\tawait client.end();\n\t};\n}\n\nexport async function notify(\n\tchannel: string,\n\tpayload?: string,\n\topts?: ListenOptions,\n): Promise<void> {\n\tconst client = postgres(resolveUrl(opts), { max: 1 });\n\n\ttry {\n\t\tif (payload) {\n\t\t\tawait client`SELECT pg_notify(${channel}, ${payload})`;\n\t\t} else {\n\t\t\tawait client`SELECT pg_notify(${channel}, '')`;\n\t\t}\n\t} finally {\n\t\tawait client.end();\n\t}\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAYA,SAAS,UAAU,CAAC,MAA8B;AAAA,EACjD,MAAM,MAAM,MAAM,oBAAoB,QAAQ,IAAI;AAAA,EAClD,IAAI,CAAC,KAAK;AAAA,IACT,MAAM,IAAI,MACT,oFACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAGR,eAAsB,MAAM,CAC3B,SACA,UACA,MAC+B;AAAA,EAC/B,MAAM,SAAS,SAAS,WAAW,IAAI,GAAG;AAAA,IACzC,KAAK;AAAA,IACL,UAAU,MAAM;AAAA,EACjB,CAAC;AAAA,EAED,MAAM,OAAO,OAAO,SAAS,CAAC,YAAY;AAAA,IACzC,SAAS,OAAO;AAAA,GAChB;AAAA,EAED,OAAO,YAAY;AAAA,IAClB,MAAM,OAAO,IAAI;AAAA;AAAA;AAInB,eAAsB,MAAM,CAC3B,SACA,SACA,MACgB;AAAA,EAChB,MAAM,SAAS,SAAS,WAAW,IAAI,GAAG,EAAE,KAAK,EAAE,CAAC;AAAA,EAEpD,IAAI;AAAA,IACH,IAAI,SAAS;AAAA,MACZ,MAAM,0BAA0B,YAAY;AAAA,IAC7C,EAAO;AAAA,MACN,MAAM,0BAA0B;AAAA;AAAA,YAEhC;AAAA,IACD,MAAM,OAAO,IAAI;AAAA;AAAA;",
|
|
8
|
+
"debugId": "B44CC52F9682E31564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -48,6 +48,16 @@ type IndexProgress = Selectable<IndexProgressTable>;
|
|
|
48
48
|
type InsertIndexProgress = Insertable<IndexProgressTable>;
|
|
49
49
|
interface EnvSchemaOutput {
|
|
50
50
|
DATABASE_URL?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Shared indexer DB (blocks/txs/events). Falls back to DATABASE_URL.
|
|
53
|
+
* Set this alongside TARGET_DATABASE_URL to enable dual-DB mode.
|
|
54
|
+
*/
|
|
55
|
+
SOURCE_DATABASE_URL?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Tenant DB (subgraph schemas + subgraphs table). Falls back to DATABASE_URL.
|
|
58
|
+
* Set this alongside SOURCE_DATABASE_URL to enable dual-DB mode.
|
|
59
|
+
*/
|
|
60
|
+
TARGET_DATABASE_URL?: string;
|
|
51
61
|
NETWORK?: "mainnet" | "testnet";
|
|
52
62
|
NETWORKS?: ("mainnet" | "testnet")[];
|
|
53
63
|
LOG_LEVEL: "debug" | "info" | "warn" | "error";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v2 step memoization:
|
|
5
|
+
*
|
|
6
|
+
* 1. `memo_key` — SHA-256 of `(stepId, canonicalJSON(stableInputs))`. Replaces
|
|
7
|
+
* the v1 `(run_id, step_id)` lookup so that editing a prompt in source
|
|
8
|
+
* invalidates the cache on the next run. Partial UNIQUE allows legacy
|
|
9
|
+
* pre-v2 rows with NULL memo_key to coexist.
|
|
10
|
+
*
|
|
11
|
+
* 2. `parent_step_id` — nullable self-FK for sub-step rows. AI tool calls
|
|
12
|
+
* inside `generateText`/`generateObject` persist as children of the
|
|
13
|
+
* parent AI step; on retry, previously successful tool calls return
|
|
14
|
+
* cached results instead of re-invoking `execute`.
|
|
15
|
+
*/
|
|
16
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
17
|
+
await db.schema
|
|
18
|
+
.alterTable("workflow_steps")
|
|
19
|
+
.addColumn("memo_key", "text")
|
|
20
|
+
.addColumn("parent_step_id", "uuid", (c) =>
|
|
21
|
+
c.references("workflow_steps.id").onDelete("cascade"),
|
|
22
|
+
)
|
|
23
|
+
.execute();
|
|
24
|
+
|
|
25
|
+
// Drop v1 composite UNIQUE. Replaced by memo_key-based UNIQUE below.
|
|
26
|
+
await sql`DROP INDEX IF EXISTS workflow_steps_dedup_idx`.execute(db);
|
|
27
|
+
|
|
28
|
+
// Partial UNIQUE: only constrain rows with memo_key set (v2 rows).
|
|
29
|
+
// Legacy NULL memo_key rows coexist without constraint violations.
|
|
30
|
+
await sql`CREATE UNIQUE INDEX workflow_steps_memo_idx ON workflow_steps (run_id, memo_key) WHERE memo_key IS NOT NULL`.execute(
|
|
31
|
+
db,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Fan-out lookup for sub-step tool-call replay.
|
|
35
|
+
await sql`CREATE INDEX workflow_steps_parent_idx ON workflow_steps (parent_step_id) WHERE parent_step_id IS NOT NULL`.execute(
|
|
36
|
+
db,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
41
|
+
await sql`DROP INDEX IF EXISTS workflow_steps_parent_idx`.execute(db);
|
|
42
|
+
await sql`DROP INDEX IF EXISTS workflow_steps_memo_idx`.execute(db);
|
|
43
|
+
|
|
44
|
+
// Restore v1 composite UNIQUE.
|
|
45
|
+
await sql`CREATE UNIQUE INDEX workflow_steps_dedup_idx ON workflow_steps (run_id, step_id)`.execute(
|
|
46
|
+
db,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await db.schema
|
|
50
|
+
.alterTable("workflow_steps")
|
|
51
|
+
.dropColumn("parent_step_id")
|
|
52
|
+
.dropColumn("memo_key")
|
|
53
|
+
.execute();
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow signer secrets — HMAC shared secrets used by the runner to
|
|
5
|
+
* authenticate requests to customer-hosted remote signer endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Stored per-account, keyed by a user-chosen name referenced via
|
|
8
|
+
* `signer.remote({ hmacRef: "<name>" })` in workflow source. The
|
|
9
|
+
* `encrypted_value` column holds the secret encrypted at rest (runner
|
|
10
|
+
* KMS-decrypts on read). This separation lets customers rotate secrets
|
|
11
|
+
* without redeploying every workflow that references them.
|
|
12
|
+
*/
|
|
13
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
14
|
+
await db.schema
|
|
15
|
+
.createTable("workflow_signer_secrets")
|
|
16
|
+
.addColumn("id", "uuid", (c) =>
|
|
17
|
+
c.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
|
18
|
+
)
|
|
19
|
+
.addColumn("account_id", "uuid", (c) =>
|
|
20
|
+
c.notNull().references("accounts.id").onDelete("cascade"),
|
|
21
|
+
)
|
|
22
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
23
|
+
.addColumn("encrypted_value", "bytea", (c) => c.notNull())
|
|
24
|
+
.addColumn("created_at", "timestamptz", (c) =>
|
|
25
|
+
c.notNull().defaultTo(sql`now()`),
|
|
26
|
+
)
|
|
27
|
+
.addColumn("updated_at", "timestamptz", (c) =>
|
|
28
|
+
c.notNull().defaultTo(sql`now()`),
|
|
29
|
+
)
|
|
30
|
+
.execute();
|
|
31
|
+
|
|
32
|
+
await sql`CREATE UNIQUE INDEX workflow_signer_secrets_account_name_idx ON workflow_signer_secrets (account_id, name)`.execute(
|
|
33
|
+
db,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
38
|
+
await sql`DROP INDEX IF EXISTS workflow_signer_secrets_account_name_idx`.execute(
|
|
39
|
+
db,
|
|
40
|
+
);
|
|
41
|
+
await db.schema.dropTable("workflow_signer_secrets").execute();
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow budget counters — one row per `(workflow_definition_id, period)`.
|
|
5
|
+
* The runner increments these in `budget/enforcer.ts` after each AI/chain/
|
|
6
|
+
* run event and refuses further work when any configured cap is reached.
|
|
7
|
+
*
|
|
8
|
+
* `period` is a string key derived from the `WorkflowDefinition.budget.reset`
|
|
9
|
+
* setting and the current wall-clock: `"daily:2026-04-17"`,
|
|
10
|
+
* `"weekly:2026-W16"`, or `"per-run:<runId>"`. Old periods are retained for
|
|
11
|
+
* historical observability (budget burn-down view); a weekly TTL cron
|
|
12
|
+
* prunes them past 30 days of history.
|
|
13
|
+
*/
|
|
14
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
15
|
+
await db.schema
|
|
16
|
+
.createTable("workflow_budgets")
|
|
17
|
+
.addColumn("id", "uuid", (c) =>
|
|
18
|
+
c.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
|
19
|
+
)
|
|
20
|
+
.addColumn("workflow_definition_id", "uuid", (c) =>
|
|
21
|
+
c.notNull().references("workflow_definitions.id").onDelete("cascade"),
|
|
22
|
+
)
|
|
23
|
+
.addColumn("period", "text", (c) => c.notNull())
|
|
24
|
+
.addColumn("ai_usd_used", "numeric(12, 4)", (c) => c.notNull().defaultTo(0))
|
|
25
|
+
.addColumn("ai_tokens_used", "bigint", (c) => c.notNull().defaultTo(0))
|
|
26
|
+
.addColumn("chain_microstx_used", "numeric(30, 0)", (c) =>
|
|
27
|
+
c.notNull().defaultTo(0),
|
|
28
|
+
)
|
|
29
|
+
.addColumn("chain_tx_count", "integer", (c) => c.notNull().defaultTo(0))
|
|
30
|
+
.addColumn("run_count", "integer", (c) => c.notNull().defaultTo(0))
|
|
31
|
+
.addColumn("step_count", "integer", (c) => c.notNull().defaultTo(0))
|
|
32
|
+
.addColumn("reset_at", "timestamptz", (c) => c.notNull())
|
|
33
|
+
.addColumn("created_at", "timestamptz", (c) =>
|
|
34
|
+
c.notNull().defaultTo(sql`now()`),
|
|
35
|
+
)
|
|
36
|
+
.addColumn("updated_at", "timestamptz", (c) =>
|
|
37
|
+
c.notNull().defaultTo(sql`now()`),
|
|
38
|
+
)
|
|
39
|
+
.execute();
|
|
40
|
+
|
|
41
|
+
await sql`CREATE UNIQUE INDEX workflow_budgets_def_period_idx ON workflow_budgets (workflow_definition_id, period)`.execute(
|
|
42
|
+
db,
|
|
43
|
+
);
|
|
44
|
+
await sql`CREATE INDEX workflow_budgets_reset_at_idx ON workflow_budgets (reset_at)`.execute(
|
|
45
|
+
db,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
50
|
+
await sql`DROP INDEX IF EXISTS workflow_budgets_reset_at_idx`.execute(db);
|
|
51
|
+
await sql`DROP INDEX IF EXISTS workflow_budgets_def_period_idx`.execute(db);
|
|
52
|
+
await db.schema.dropTable("workflow_budgets").execute();
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pg_notify trigger on the core `transactions` table. Fires on INSERT and
|
|
5
|
+
* publishes the txid on the `tx:confirmed` channel. The workflow runner's
|
|
6
|
+
* `confirmation/subgraph.ts` listens on this channel to resolve pending
|
|
7
|
+
* `broadcast({ awaitConfirmation: true })` promises.
|
|
8
|
+
*
|
|
9
|
+
* Payload is just the tx_id — listeners dedupe + look up details
|
|
10
|
+
* themselves. Keeping the payload small keeps pg_notify throughput high.
|
|
11
|
+
*/
|
|
12
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
13
|
+
await sql`
|
|
14
|
+
CREATE OR REPLACE FUNCTION notify_tx_confirmed() RETURNS trigger AS $$
|
|
15
|
+
BEGIN
|
|
16
|
+
PERFORM pg_notify('tx:confirmed', NEW.tx_id);
|
|
17
|
+
RETURN NEW;
|
|
18
|
+
END;
|
|
19
|
+
$$ LANGUAGE plpgsql;
|
|
20
|
+
`.execute(db);
|
|
21
|
+
|
|
22
|
+
await sql`
|
|
23
|
+
DROP TRIGGER IF EXISTS tx_confirmed_notify ON transactions;
|
|
24
|
+
CREATE TRIGGER tx_confirmed_notify
|
|
25
|
+
AFTER INSERT ON transactions
|
|
26
|
+
FOR EACH ROW
|
|
27
|
+
EXECUTE FUNCTION notify_tx_confirmed();
|
|
28
|
+
`.execute(db);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
32
|
+
await sql`DROP TRIGGER IF EXISTS tx_confirmed_notify ON transactions`.execute(
|
|
33
|
+
db,
|
|
34
|
+
);
|
|
35
|
+
await sql`DROP FUNCTION IF EXISTS notify_tx_confirmed()`.execute(db);
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Make `subgraphs.api_key_id` nullable to support oss/dedicated modes where
|
|
5
|
+
* there's no per-tenant API key concept. In platform mode the column stays
|
|
6
|
+
* populated on every insert; in oss/dedicated it's NULL.
|
|
7
|
+
*
|
|
8
|
+
* A partial unique index on `(name) WHERE api_key_id IS NULL` enforces the
|
|
9
|
+
* single-tenant constraint: within a non-platform instance, subgraph names
|
|
10
|
+
* are globally unique (there's only one "tenant"). Platform mode's existing
|
|
11
|
+
* uniqueness constraint on `(api_key_id, name)` stays in place for the
|
|
12
|
+
* multi-tenant case.
|
|
13
|
+
*/
|
|
14
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
15
|
+
// Fail fast instead of hanging when a live service holds ACCESS SHARE on
|
|
16
|
+
// `subgraphs` (e.g. subgraph-processor's 5s poll). Deploy script stops
|
|
17
|
+
// dependent services before running migrations — this is a safety net.
|
|
18
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
19
|
+
|
|
20
|
+
await sql`ALTER TABLE subgraphs ALTER COLUMN api_key_id DROP NOT NULL`.execute(
|
|
21
|
+
db,
|
|
22
|
+
);
|
|
23
|
+
await sql`
|
|
24
|
+
CREATE UNIQUE INDEX IF NOT EXISTS subgraphs_name_unique_no_key
|
|
25
|
+
ON subgraphs (name)
|
|
26
|
+
WHERE api_key_id IS NULL
|
|
27
|
+
`.execute(db);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
31
|
+
await sql`DROP INDEX IF EXISTS subgraphs_name_unique_no_key`.execute(db);
|
|
32
|
+
// Intentionally does NOT re-add NOT NULL — any rows inserted while the
|
|
33
|
+
// constraint was relaxed would break the migration. Operators should
|
|
34
|
+
// backfill api_key_id before re-applying NOT NULL manually if desired.
|
|
35
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drops all workflow-related tables + the `tx_confirmed_notify` trigger.
|
|
5
|
+
*
|
|
6
|
+
* Workflows are going on the back burner while we ship dedicated-hosted
|
|
7
|
+
* subgraphs. We've learned enough from building the subgraph tenant model
|
|
8
|
+
* that reviving workflows later can follow the same dedicated-per-tenant
|
|
9
|
+
* pattern with a fresh schema designed for that architecture — no reason
|
|
10
|
+
* to keep dormant tables on the shared platform DB.
|
|
11
|
+
*
|
|
12
|
+
* Tables dropped (in FK-safe order via CASCADE):
|
|
13
|
+
* workflow_steps, workflow_runs, workflow_queue, workflow_schedules,
|
|
14
|
+
* workflow_cursors, workflow_signer_secrets, workflow_budgets,
|
|
15
|
+
* workflow_definitions
|
|
16
|
+
*
|
|
17
|
+
* Also drops `tx_confirmed_notify` — nobody listens on `tx:confirmed` now
|
|
18
|
+
* that workflow-runner is unmounted. The trigger fires on every
|
|
19
|
+
* transactions insert (indexer hot path), so leaving it is wasted work.
|
|
20
|
+
*
|
|
21
|
+
* `down` is intentionally a no-op. When workflows revive, they get fresh
|
|
22
|
+
* migrations designed for the tenant model — don't try to reverse into the
|
|
23
|
+
* old platform-shared schema.
|
|
24
|
+
*/
|
|
25
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
26
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
27
|
+
|
|
28
|
+
await sql`DROP TRIGGER IF EXISTS tx_confirmed_notify ON transactions`.execute(
|
|
29
|
+
db,
|
|
30
|
+
);
|
|
31
|
+
await sql`DROP FUNCTION IF EXISTS notify_tx_confirmed()`.execute(db);
|
|
32
|
+
|
|
33
|
+
await sql`DROP TABLE IF EXISTS workflow_steps CASCADE`.execute(db);
|
|
34
|
+
await sql`DROP TABLE IF EXISTS workflow_runs CASCADE`.execute(db);
|
|
35
|
+
await sql`DROP TABLE IF EXISTS workflow_queue CASCADE`.execute(db);
|
|
36
|
+
await sql`DROP TABLE IF EXISTS workflow_schedules CASCADE`.execute(db);
|
|
37
|
+
await sql`DROP TABLE IF EXISTS workflow_cursors CASCADE`.execute(db);
|
|
38
|
+
await sql`DROP TABLE IF EXISTS workflow_signer_secrets CASCADE`.execute(db);
|
|
39
|
+
await sql`DROP TABLE IF EXISTS workflow_budgets CASCADE`.execute(db);
|
|
40
|
+
await sql`DROP TABLE IF EXISTS workflow_definitions CASCADE`.execute(db);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function down(_db: Kysely<unknown>): Promise<void> {
|
|
44
|
+
// Intentional no-op. Revived workflows will use fresh migrations sized
|
|
45
|
+
// for the tenant-per-customer model.
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dedicated-hosting tenant registry.
|
|
5
|
+
*
|
|
6
|
+
* One row per customer instance. Provisioner is stateless — it does Docker
|
|
7
|
+
* ops + returns values; control plane (this table) owns the persistent
|
|
8
|
+
* mapping between accounts and their per-tenant stack.
|
|
9
|
+
*
|
|
10
|
+
* Encrypted fields use `packages/shared/src/crypto/secrets.ts` (AES-GCM
|
|
11
|
+
* envelope keyed by `SECONDLAYER_SECRETS_KEY`). Never log them in plaintext.
|
|
12
|
+
*
|
|
13
|
+
* Storage is soft-enforced: `storage_used_mb` is updated by the health
|
|
14
|
+
* cron; alerts + overage billing live in the control plane, not the DB.
|
|
15
|
+
*/
|
|
16
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
17
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
18
|
+
|
|
19
|
+
await sql`
|
|
20
|
+
CREATE TABLE tenants (
|
|
21
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
22
|
+
account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
|
|
23
|
+
slug text NOT NULL UNIQUE,
|
|
24
|
+
status text NOT NULL DEFAULT 'provisioning',
|
|
25
|
+
|
|
26
|
+
plan text NOT NULL,
|
|
27
|
+
cpus numeric(4,2) NOT NULL,
|
|
28
|
+
memory_mb integer NOT NULL,
|
|
29
|
+
storage_limit_mb integer NOT NULL,
|
|
30
|
+
storage_used_mb integer,
|
|
31
|
+
|
|
32
|
+
pg_container_id text,
|
|
33
|
+
api_container_id text,
|
|
34
|
+
processor_container_id text,
|
|
35
|
+
|
|
36
|
+
target_database_url_enc bytea NOT NULL,
|
|
37
|
+
tenant_jwt_secret_enc bytea NOT NULL,
|
|
38
|
+
anon_key_enc bytea NOT NULL,
|
|
39
|
+
service_key_enc bytea NOT NULL,
|
|
40
|
+
|
|
41
|
+
api_url_internal text NOT NULL,
|
|
42
|
+
api_url_public text NOT NULL,
|
|
43
|
+
|
|
44
|
+
trial_ends_at timestamptz NOT NULL,
|
|
45
|
+
suspended_at timestamptz,
|
|
46
|
+
last_health_check_at timestamptz,
|
|
47
|
+
|
|
48
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
49
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
50
|
+
)
|
|
51
|
+
`.execute(db);
|
|
52
|
+
|
|
53
|
+
await sql`CREATE INDEX tenants_account_idx ON tenants (account_id)`.execute(
|
|
54
|
+
db,
|
|
55
|
+
);
|
|
56
|
+
await sql`CREATE INDEX tenants_status_idx ON tenants (status) WHERE status <> 'deleted'`.execute(
|
|
57
|
+
db,
|
|
58
|
+
);
|
|
59
|
+
await sql`CREATE INDEX tenants_trial_ends_idx ON tenants (trial_ends_at) WHERE status IN ('provisioning', 'active')`.execute(
|
|
60
|
+
db,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
65
|
+
await sql`DROP TABLE IF EXISTS tenants`.execute(db);
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-tenant key generation counters. Each JWT carries a `gen` claim; the
|
|
5
|
+
* tenant API rejects tokens whose `gen` doesn't match the current counter
|
|
6
|
+
* for that role. Bumping a counter invalidates all JWTs of that role
|
|
7
|
+
* immediately, without rotating the signing secret (which would force
|
|
8
|
+
* both keys to rotate together).
|
|
9
|
+
*
|
|
10
|
+
* UX: user can rotate service alone (leaked server-side key) OR anon alone
|
|
11
|
+
* (client-side embedding exposed) OR both together (offboarding panic).
|
|
12
|
+
*/
|
|
13
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
14
|
+
await sql`SET lock_timeout = '30s'`.execute(db);
|
|
15
|
+
|
|
16
|
+
await sql`
|
|
17
|
+
ALTER TABLE tenants
|
|
18
|
+
ADD COLUMN service_gen integer NOT NULL DEFAULT 1,
|
|
19
|
+
ADD COLUMN anon_gen integer NOT NULL DEFAULT 1
|
|
20
|
+
`.execute(db);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
24
|
+
await sql`
|
|
25
|
+
ALTER TABLE tenants
|
|
26
|
+
DROP COLUMN IF EXISTS service_gen,
|
|
27
|
+
DROP COLUMN IF EXISTS anon_gen
|
|
28
|
+
`.execute(db);
|
|
29
|
+
}
|