@secondlayer/shared 0.2.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 (76) hide show
  1. package/README.md +19 -0
  2. package/dist/src/crypto/hmac.d.ts +26 -0
  3. package/dist/src/crypto/hmac.js +75 -0
  4. package/dist/src/crypto/hmac.js.map +10 -0
  5. package/dist/src/db/index.d.ts +227 -0
  6. package/dist/src/db/index.js +75 -0
  7. package/dist/src/db/index.js.map +11 -0
  8. package/dist/src/db/jsonb.d.ts +13 -0
  9. package/dist/src/db/jsonb.js +35 -0
  10. package/dist/src/db/jsonb.js.map +10 -0
  11. package/dist/src/db/queries/accounts.d.ts +179 -0
  12. package/dist/src/db/queries/accounts.js +39 -0
  13. package/dist/src/db/queries/accounts.js.map +10 -0
  14. package/dist/src/db/queries/integrity.d.ts +178 -0
  15. package/dist/src/db/queries/integrity.js +68 -0
  16. package/dist/src/db/queries/integrity.js.map +10 -0
  17. package/dist/src/db/queries/metrics.d.ts +179 -0
  18. package/dist/src/db/queries/metrics.js +51 -0
  19. package/dist/src/db/queries/metrics.js.map +10 -0
  20. package/dist/src/db/queries/usage.d.ts +205 -0
  21. package/dist/src/db/queries/usage.js +117 -0
  22. package/dist/src/db/queries/usage.js.map +11 -0
  23. package/dist/src/db/queries/views.d.ts +191 -0
  24. package/dist/src/db/queries/views.js +111 -0
  25. package/dist/src/db/queries/views.js.map +11 -0
  26. package/dist/src/db/schema.d.ts +207 -0
  27. package/dist/src/db/schema.js +3 -0
  28. package/dist/src/db/schema.js.map +9 -0
  29. package/dist/src/env.d.ts +7 -0
  30. package/dist/src/env.js +60 -0
  31. package/dist/src/env.js.map +10 -0
  32. package/dist/src/errors.d.ts +51 -0
  33. package/dist/src/errors.js +103 -0
  34. package/dist/src/errors.js.map +10 -0
  35. package/dist/src/index.d.ts +464 -0
  36. package/dist/src/index.js +642 -0
  37. package/dist/src/index.js.map +19 -0
  38. package/dist/src/lib/plans.d.ts +10 -0
  39. package/dist/src/lib/plans.js +34 -0
  40. package/dist/src/lib/plans.js.map +10 -0
  41. package/dist/src/logger.d.ts +2 -0
  42. package/dist/src/logger.js +130 -0
  43. package/dist/src/logger.js.map +11 -0
  44. package/dist/src/node/client.d.ts +35 -0
  45. package/dist/src/node/client.js +56 -0
  46. package/dist/src/node/client.js.map +10 -0
  47. package/dist/src/node/hiro-client.d.ts +186 -0
  48. package/dist/src/node/hiro-client.js +410 -0
  49. package/dist/src/node/hiro-client.js.map +12 -0
  50. package/dist/src/queue/index.d.ts +50 -0
  51. package/dist/src/queue/index.js +176 -0
  52. package/dist/src/queue/index.js.map +12 -0
  53. package/dist/src/queue/listener.d.ts +20 -0
  54. package/dist/src/queue/listener.js +63 -0
  55. package/dist/src/queue/listener.js.map +10 -0
  56. package/dist/src/queue/recovery.d.ts +14 -0
  57. package/dist/src/queue/recovery.js +100 -0
  58. package/dist/src/queue/recovery.js.map +12 -0
  59. package/dist/src/schemas/filters.d.ts +30 -0
  60. package/dist/src/schemas/filters.js +133 -0
  61. package/dist/src/schemas/filters.js.map +10 -0
  62. package/dist/src/schemas/index.d.ts +109 -0
  63. package/dist/src/schemas/index.js +228 -0
  64. package/dist/src/schemas/index.js.map +12 -0
  65. package/dist/src/schemas/views.d.ts +51 -0
  66. package/dist/src/schemas/views.js +29 -0
  67. package/dist/src/schemas/views.js.map +10 -0
  68. package/dist/src/types.d.ts +102 -0
  69. package/dist/src/types.js +3 -0
  70. package/dist/src/types.js.map +9 -0
  71. package/migrations/0001_initial.ts +182 -0
  72. package/migrations/0002_api_keys.ts +38 -0
  73. package/migrations/0003_tenant_isolation.ts +114 -0
  74. package/migrations/0004_accounts_and_usage.ts +90 -0
  75. package/migrations/0005_sessions.ts +42 -0
  76. package/package.json +128 -0
@@ -0,0 +1,51 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+
13
+ // src/db/queries/metrics.ts
14
+ import { sql } from "kysely";
15
+ async function getStreamMetrics(db, streamId) {
16
+ return await db.selectFrom("stream_metrics").selectAll().where("stream_id", "=", streamId).executeTakeFirst() ?? null;
17
+ }
18
+ async function updateStreamMetrics(db, streamId, updates) {
19
+ await db.insertInto("stream_metrics").values({
20
+ stream_id: streamId,
21
+ last_triggered_at: updates.lastTriggeredAt ?? null,
22
+ last_triggered_block: updates.lastTriggeredBlock ?? null,
23
+ total_deliveries: updates.totalDeliveries ?? 0,
24
+ failed_deliveries: updates.failedDeliveries ?? 0,
25
+ error_message: updates.errorMessage ?? null
26
+ }).onConflict((oc) => oc.column("stream_id").doUpdateSet({
27
+ ...updates.lastTriggeredAt !== undefined ? { last_triggered_at: updates.lastTriggeredAt } : {},
28
+ ...updates.lastTriggeredBlock !== undefined ? { last_triggered_block: updates.lastTriggeredBlock } : {},
29
+ ...updates.totalDeliveries !== undefined ? { total_deliveries: updates.totalDeliveries } : {},
30
+ ...updates.failedDeliveries !== undefined ? { failed_deliveries: updates.failedDeliveries } : {},
31
+ ...updates.errorMessage !== undefined ? { error_message: updates.errorMessage } : {}
32
+ })).execute();
33
+ }
34
+ async function incrementDeliveryCount(db, streamId, failed) {
35
+ await db.insertInto("stream_metrics").values({
36
+ stream_id: streamId,
37
+ total_deliveries: 1,
38
+ failed_deliveries: failed ? 1 : 0
39
+ }).onConflict((oc) => oc.column("stream_id").doUpdateSet({
40
+ total_deliveries: sql`stream_metrics.total_deliveries + 1`,
41
+ ...failed ? { failed_deliveries: sql`stream_metrics.failed_deliveries + 1` } : {}
42
+ })).execute();
43
+ }
44
+ export {
45
+ updateStreamMetrics,
46
+ incrementDeliveryCount,
47
+ getStreamMetrics
48
+ };
49
+
50
+ //# debugId=A4E370524682609864756E2164756E21
51
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/db/queries/metrics.ts"],
4
+ "sourcesContent": [
5
+ "import { sql, type Kysely } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\nexport async function getStreamMetrics(db: Kysely<Database>, streamId: string) {\n return (\n await db\n .selectFrom(\"stream_metrics\")\n .selectAll()\n .where(\"stream_id\", \"=\", streamId)\n .executeTakeFirst()\n ) ?? null;\n}\n\nexport async function updateStreamMetrics(\n db: Kysely<Database>,\n streamId: string,\n updates: Partial<{\n lastTriggeredAt: Date;\n lastTriggeredBlock: number;\n totalDeliveries: number;\n failedDeliveries: number;\n errorMessage: string | null;\n }>,\n) {\n await db\n .insertInto(\"stream_metrics\")\n .values({\n stream_id: streamId,\n last_triggered_at: updates.lastTriggeredAt ?? null,\n last_triggered_block: updates.lastTriggeredBlock ?? null,\n total_deliveries: updates.totalDeliveries ?? 0,\n failed_deliveries: updates.failedDeliveries ?? 0,\n error_message: updates.errorMessage ?? null,\n })\n .onConflict((oc) =>\n oc.column(\"stream_id\").doUpdateSet({\n ...(updates.lastTriggeredAt !== undefined ? { last_triggered_at: updates.lastTriggeredAt } : {}),\n ...(updates.lastTriggeredBlock !== undefined ? { last_triggered_block: updates.lastTriggeredBlock } : {}),\n ...(updates.totalDeliveries !== undefined ? { total_deliveries: updates.totalDeliveries } : {}),\n ...(updates.failedDeliveries !== undefined ? { failed_deliveries: updates.failedDeliveries } : {}),\n ...(updates.errorMessage !== undefined ? { error_message: updates.errorMessage } : {}),\n }),\n )\n .execute();\n}\n\nexport async function incrementDeliveryCount(\n db: Kysely<Database>,\n streamId: string,\n failed: boolean,\n) {\n await db\n .insertInto(\"stream_metrics\")\n .values({\n stream_id: streamId,\n total_deliveries: 1,\n failed_deliveries: failed ? 1 : 0,\n })\n .onConflict((oc) =>\n oc.column(\"stream_id\").doUpdateSet({\n total_deliveries: sql`stream_metrics.total_deliveries + 1`,\n ...(failed ? { failed_deliveries: sql`stream_metrics.failed_deliveries + 1` } : {}),\n }),\n )\n .execute();\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;AAAA;AAGA,eAAsB,gBAAgB,CAAC,IAAsB,UAAkB;AAAA,EAC7E,OACE,MAAM,GACH,WAAW,gBAAgB,EAC3B,UAAU,EACV,MAAM,aAAa,KAAK,QAAQ,EAChC,iBAAiB,KACjB;AAAA;AAGP,eAAsB,mBAAmB,CACvC,IACA,UACA,SAOA;AAAA,EACA,MAAM,GACH,WAAW,gBAAgB,EAC3B,OAAO;AAAA,IACN,WAAW;AAAA,IACX,mBAAmB,QAAQ,mBAAmB;AAAA,IAC9C,sBAAsB,QAAQ,sBAAsB;AAAA,IACpD,kBAAkB,QAAQ,mBAAmB;AAAA,IAC7C,mBAAmB,QAAQ,oBAAoB;AAAA,IAC/C,eAAe,QAAQ,gBAAgB;AAAA,EACzC,CAAC,EACA,WAAW,CAAC,OACX,GAAG,OAAO,WAAW,EAAE,YAAY;AAAA,OAC7B,QAAQ,oBAAoB,YAAY,EAAE,mBAAmB,QAAQ,gBAAgB,IAAI,CAAC;AAAA,OAC1F,QAAQ,uBAAuB,YAAY,EAAE,sBAAsB,QAAQ,mBAAmB,IAAI,CAAC;AAAA,OACnG,QAAQ,oBAAoB,YAAY,EAAE,kBAAkB,QAAQ,gBAAgB,IAAI,CAAC;AAAA,OACzF,QAAQ,qBAAqB,YAAY,EAAE,mBAAmB,QAAQ,iBAAiB,IAAI,CAAC;AAAA,OAC5F,QAAQ,iBAAiB,YAAY,EAAE,eAAe,QAAQ,aAAa,IAAI,CAAC;AAAA,EACtF,CAAC,CACH,EACC,QAAQ;AAAA;AAGb,eAAsB,sBAAsB,CAC1C,IACA,UACA,QACA;AAAA,EACA,MAAM,GACH,WAAW,gBAAgB,EAC3B,OAAO;AAAA,IACN,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,mBAAmB,SAAS,IAAI;AAAA,EAClC,CAAC,EACA,WAAW,CAAC,OACX,GAAG,OAAO,WAAW,EAAE,YAAY;AAAA,IACjC,kBAAkB;AAAA,OACd,SAAS,EAAE,mBAAmB,0CAA0C,IAAI,CAAC;AAAA,EACnF,CAAC,CACH,EACC,QAAQ;AAAA;",
8
+ "debugId": "A4E370524682609864756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,205 @@
1
+ interface PlanLimits {
2
+ streams: number;
3
+ views: number;
4
+ apiRequestsPerDay: number;
5
+ deliveriesPerMonth: number;
6
+ storageBytes: number;
7
+ }
8
+ declare function getPlanLimits(plan: string): PlanLimits;
9
+ import { Kysely } from "kysely";
10
+ import { Generated } from "kysely";
11
+ interface BlocksTable {
12
+ height: number;
13
+ hash: string;
14
+ parent_hash: string;
15
+ burn_block_height: number;
16
+ timestamp: number;
17
+ canonical: Generated<boolean>;
18
+ created_at: Generated<Date>;
19
+ }
20
+ interface TransactionsTable {
21
+ tx_id: string;
22
+ block_height: number;
23
+ type: string;
24
+ sender: string;
25
+ status: string;
26
+ contract_id: string | null;
27
+ function_name: string | null;
28
+ raw_tx: string;
29
+ created_at: Generated<Date>;
30
+ }
31
+ interface EventsTable {
32
+ id: Generated<string>;
33
+ tx_id: string;
34
+ block_height: number;
35
+ event_index: number;
36
+ type: string;
37
+ data: unknown;
38
+ created_at: Generated<Date>;
39
+ }
40
+ interface StreamsTable {
41
+ id: Generated<string>;
42
+ name: string;
43
+ status: Generated<string>;
44
+ filters: unknown;
45
+ options: Generated<unknown>;
46
+ webhook_url: string;
47
+ webhook_secret: string | null;
48
+ api_key_id: string | null;
49
+ created_at: Generated<Date>;
50
+ updated_at: Generated<Date>;
51
+ }
52
+ interface StreamMetricsTable {
53
+ stream_id: string;
54
+ last_triggered_at: Date | null;
55
+ last_triggered_block: number | null;
56
+ total_deliveries: Generated<number>;
57
+ failed_deliveries: Generated<number>;
58
+ error_message: string | null;
59
+ }
60
+ interface JobsTable {
61
+ id: Generated<string>;
62
+ stream_id: string;
63
+ block_height: number;
64
+ status: Generated<string>;
65
+ attempts: Generated<number>;
66
+ locked_at: Date | null;
67
+ locked_by: string | null;
68
+ error: string | null;
69
+ backfill: Generated<boolean>;
70
+ created_at: Generated<Date>;
71
+ completed_at: Date | null;
72
+ }
73
+ interface IndexProgressTable {
74
+ network: string;
75
+ last_indexed_block: Generated<number>;
76
+ last_contiguous_block: Generated<number>;
77
+ highest_seen_block: Generated<number>;
78
+ updated_at: Generated<Date>;
79
+ }
80
+ interface DeliveriesTable {
81
+ id: Generated<string>;
82
+ stream_id: string;
83
+ job_id: string | null;
84
+ block_height: number;
85
+ status: string;
86
+ status_code: number | null;
87
+ response_time_ms: number | null;
88
+ attempts: Generated<number>;
89
+ error: string | null;
90
+ payload: unknown;
91
+ created_at: Generated<Date>;
92
+ }
93
+ interface ViewsTable {
94
+ id: Generated<string>;
95
+ name: string;
96
+ version: Generated<string>;
97
+ status: Generated<string>;
98
+ definition: unknown;
99
+ schema_hash: string;
100
+ handler_path: string;
101
+ schema_name: string | null;
102
+ last_processed_block: Generated<number>;
103
+ last_error: string | null;
104
+ last_error_at: Date | null;
105
+ total_processed: Generated<number>;
106
+ total_errors: Generated<number>;
107
+ api_key_id: string | null;
108
+ created_at: Generated<Date>;
109
+ updated_at: Generated<Date>;
110
+ }
111
+ interface ApiKeysTable {
112
+ id: Generated<string>;
113
+ key_hash: string;
114
+ key_prefix: string;
115
+ name: string | null;
116
+ status: Generated<string>;
117
+ rate_limit: Generated<number>;
118
+ ip_address: string;
119
+ account_id: string;
120
+ last_used_at: Date | null;
121
+ revoked_at: Date | null;
122
+ created_at: Generated<Date>;
123
+ }
124
+ interface AccountsTable {
125
+ id: Generated<string>;
126
+ email: string;
127
+ plan: Generated<string>;
128
+ created_at: Generated<Date>;
129
+ }
130
+ interface SessionsTable {
131
+ id: Generated<string>;
132
+ token_hash: string;
133
+ token_prefix: string;
134
+ account_id: string;
135
+ ip_address: string;
136
+ expires_at: Generated<Date>;
137
+ revoked_at: Date | null;
138
+ last_used_at: Date | null;
139
+ created_at: Generated<Date>;
140
+ }
141
+ interface MagicLinksTable {
142
+ id: Generated<string>;
143
+ email: string;
144
+ token: string;
145
+ expires_at: Date;
146
+ used_at: Date | null;
147
+ created_at: Generated<Date>;
148
+ }
149
+ interface UsageDailyTable {
150
+ account_id: string;
151
+ date: string;
152
+ api_requests: Generated<number>;
153
+ deliveries: Generated<number>;
154
+ }
155
+ interface UsageSnapshotsTable {
156
+ id: Generated<string>;
157
+ account_id: string;
158
+ measured_at: Generated<Date>;
159
+ storage_bytes: Generated<number>;
160
+ }
161
+ interface Database {
162
+ blocks: BlocksTable;
163
+ transactions: TransactionsTable;
164
+ events: EventsTable;
165
+ streams: StreamsTable;
166
+ stream_metrics: StreamMetricsTable;
167
+ jobs: JobsTable;
168
+ index_progress: IndexProgressTable;
169
+ deliveries: DeliveriesTable;
170
+ views: ViewsTable;
171
+ api_keys: ApiKeysTable;
172
+ accounts: AccountsTable;
173
+ sessions: SessionsTable;
174
+ magic_links: MagicLinksTable;
175
+ usage_daily: UsageDailyTable;
176
+ usage_snapshots: UsageSnapshotsTable;
177
+ }
178
+ /** Increment API request counter for today. Fire-and-forget safe. */
179
+ declare function incrementApiRequests(db: Kysely<Database>, accountId: string): Promise<void>;
180
+ /** Increment delivery counter for today. */
181
+ declare function incrementDeliveries(db: Kysely<Database>, accountId: string, count?: number): Promise<void>;
182
+ interface UsageSummary {
183
+ apiRequestsToday: number;
184
+ deliveriesThisMonth: number;
185
+ storageBytes: number;
186
+ }
187
+ /** Get current usage for an account. */
188
+ declare function getUsage(db: Kysely<Database>, accountId: string): Promise<UsageSummary>;
189
+ interface LimitCheck {
190
+ allowed: boolean;
191
+ limits: ReturnType<typeof getPlanLimits>;
192
+ current: UsageSummary & {
193
+ streams: number
194
+ views: number
195
+ };
196
+ exceeded?: string;
197
+ }
198
+ /** Check if an account is within plan limits. */
199
+ declare function checkLimits(db: Kysely<Database>, accountId: string, plan: string): Promise<LimitCheck>;
200
+ /**
201
+ * Measure storage for all accounts by querying pg_total_relation_size
202
+ * for each tenant's view schemas.
203
+ */
204
+ declare function measureStorage(db: Kysely<Database>): Promise<void>;
205
+ export { measureStorage, incrementDeliveries, incrementApiRequests, getUsage, checkLimits, UsageSummary, LimitCheck };
@@ -0,0 +1,117 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+
13
+ // src/lib/plans.ts
14
+ var FREE_PLAN = {
15
+ streams: 3,
16
+ views: 2,
17
+ apiRequestsPerDay: 1000,
18
+ deliveriesPerMonth: 5000,
19
+ storageBytes: 100 * 1024 * 1024
20
+ };
21
+ function getPlanLimits(plan) {
22
+ switch (plan) {
23
+ case "free":
24
+ default:
25
+ return FREE_PLAN;
26
+ }
27
+ }
28
+
29
+ // src/db/queries/usage.ts
30
+ import { sql } from "kysely";
31
+ async function incrementApiRequests(db, accountId) {
32
+ const today = new Date().toISOString().slice(0, 10);
33
+ await db.insertInto("usage_daily").values({ account_id: accountId, date: today, api_requests: 1, deliveries: 0 }).onConflict((oc) => oc.columns(["account_id", "date"]).doUpdateSet({
34
+ api_requests: sql`usage_daily.api_requests + 1`
35
+ })).execute();
36
+ }
37
+ async function incrementDeliveries(db, accountId, count = 1) {
38
+ const today = new Date().toISOString().slice(0, 10);
39
+ await db.insertInto("usage_daily").values({ account_id: accountId, date: today, api_requests: 0, deliveries: count }).onConflict((oc) => oc.columns(["account_id", "date"]).doUpdateSet({
40
+ deliveries: sql`usage_daily.deliveries + ${count}`
41
+ })).execute();
42
+ }
43
+ async function getUsage(db, accountId) {
44
+ const today = new Date().toISOString().slice(0, 10);
45
+ const monthStart = today.slice(0, 7) + "-01";
46
+ const dailyRow = await db.selectFrom("usage_daily").select("api_requests").where("account_id", "=", accountId).where("date", "=", today).executeTakeFirst();
47
+ const monthlyRow = await db.selectFrom("usage_daily").select(sql`COALESCE(SUM(deliveries), 0)`.as("total")).where("account_id", "=", accountId).where("date", ">=", monthStart).executeTakeFirst();
48
+ const storageRow = await db.selectFrom("usage_snapshots").select("storage_bytes").where("account_id", "=", accountId).orderBy("measured_at", "desc").limit(1).executeTakeFirst();
49
+ return {
50
+ apiRequestsToday: dailyRow?.api_requests ?? 0,
51
+ deliveriesThisMonth: Number(monthlyRow?.total ?? 0),
52
+ storageBytes: Number(storageRow?.storage_bytes ?? 0)
53
+ };
54
+ }
55
+ async function checkLimits(db, accountId, plan) {
56
+ const limits = getPlanLimits(plan);
57
+ const usage = await getUsage(db, accountId);
58
+ const streamCount = await db.selectFrom("streams").innerJoin("api_keys", "streams.api_key_id", "api_keys.id").select(sql`count(*)`.as("count")).where("api_keys.account_id", "=", accountId).executeTakeFirst();
59
+ const viewCount = await db.selectFrom("views").innerJoin("api_keys", "views.api_key_id", "api_keys.id").select(sql`count(*)`.as("count")).where("api_keys.account_id", "=", accountId).executeTakeFirst();
60
+ const current = {
61
+ ...usage,
62
+ streams: Number(streamCount?.count ?? 0),
63
+ views: Number(viewCount?.count ?? 0)
64
+ };
65
+ if (current.streams >= limits.streams) {
66
+ return { allowed: false, limits, current, exceeded: "streams" };
67
+ }
68
+ if (current.views >= limits.views) {
69
+ return { allowed: false, limits, current, exceeded: "views" };
70
+ }
71
+ if (current.apiRequestsToday >= limits.apiRequestsPerDay) {
72
+ return { allowed: false, limits, current, exceeded: "api_requests" };
73
+ }
74
+ if (current.deliveriesThisMonth >= limits.deliveriesPerMonth) {
75
+ return { allowed: false, limits, current, exceeded: "deliveries" };
76
+ }
77
+ if (current.storageBytes >= limits.storageBytes) {
78
+ return { allowed: false, limits, current, exceeded: "storage" };
79
+ }
80
+ return { allowed: true, limits, current };
81
+ }
82
+ async function measureStorage(db) {
83
+ const accountViews = await db.selectFrom("views").innerJoin("api_keys", "views.api_key_id", "api_keys.id").select(["api_keys.account_id", "views.schema_name"]).where("views.schema_name", "is not", null).execute();
84
+ const byAccount = new Map;
85
+ for (const row of accountViews) {
86
+ const schemas = byAccount.get(row.account_id) ?? [];
87
+ if (row.schema_name)
88
+ schemas.push(row.schema_name);
89
+ byAccount.set(row.account_id, schemas);
90
+ }
91
+ for (const [accountId, schemas] of byAccount) {
92
+ let totalBytes = 0;
93
+ for (const schema of schemas) {
94
+ try {
95
+ const result = await sql`
96
+ SELECT COALESCE(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))), 0)::text as size
97
+ FROM pg_tables WHERE schemaname = ${schema}
98
+ `.execute(db);
99
+ totalBytes += Number(result.rows[0]?.size ?? 0);
100
+ } catch {}
101
+ }
102
+ await db.insertInto("usage_snapshots").values({
103
+ account_id: accountId,
104
+ storage_bytes: totalBytes
105
+ }).execute();
106
+ }
107
+ }
108
+ export {
109
+ measureStorage,
110
+ incrementDeliveries,
111
+ incrementApiRequests,
112
+ getUsage,
113
+ checkLimits
114
+ };
115
+
116
+ //# debugId=51CB827BBF658C6C64756E2164756E21
117
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/plans.ts", "../src/db/queries/usage.ts"],
4
+ "sourcesContent": [
5
+ "export interface PlanLimits {\n streams: number;\n views: number;\n apiRequestsPerDay: number;\n deliveriesPerMonth: number;\n storageBytes: number;\n}\n\nexport const FREE_PLAN: PlanLimits = {\n streams: 3,\n views: 2,\n apiRequestsPerDay: 1_000,\n deliveriesPerMonth: 5_000,\n storageBytes: 100 * 1024 * 1024, // 100MB\n};\n\nexport function getPlanLimits(plan: string): PlanLimits {\n switch (plan) {\n case \"free\":\n default:\n return FREE_PLAN;\n }\n}\n",
6
+ "import { sql, type Kysely } from \"kysely\";\nimport type { Database } from \"../types.ts\";\nimport { getPlanLimits } from \"../../lib/plans.ts\";\n\n/** Increment API request counter for today. Fire-and-forget safe. */\nexport async function incrementApiRequests(\n db: Kysely<Database>,\n accountId: string,\n): Promise<void> {\n const today = new Date().toISOString().slice(0, 10);\n await db\n .insertInto(\"usage_daily\")\n .values({ account_id: accountId, date: today, api_requests: 1, deliveries: 0 })\n .onConflict((oc) =>\n oc.columns([\"account_id\", \"date\"]).doUpdateSet({\n api_requests: sql`usage_daily.api_requests + 1`,\n }),\n )\n .execute();\n}\n\n/** Increment delivery counter for today. */\nexport async function incrementDeliveries(\n db: Kysely<Database>,\n accountId: string,\n count = 1,\n): Promise<void> {\n const today = new Date().toISOString().slice(0, 10);\n await db\n .insertInto(\"usage_daily\")\n .values({ account_id: accountId, date: today, api_requests: 0, deliveries: count })\n .onConflict((oc) =>\n oc.columns([\"account_id\", \"date\"]).doUpdateSet({\n deliveries: sql`usage_daily.deliveries + ${count}`,\n }),\n )\n .execute();\n}\n\nexport interface UsageSummary {\n apiRequestsToday: number;\n deliveriesThisMonth: number;\n storageBytes: number;\n}\n\n/** Get current usage for an account. */\nexport async function getUsage(\n db: Kysely<Database>,\n accountId: string,\n): Promise<UsageSummary> {\n const today = new Date().toISOString().slice(0, 10);\n const monthStart = today.slice(0, 7) + \"-01\"; // YYYY-MM-01\n\n // Today's API requests\n const dailyRow = await db\n .selectFrom(\"usage_daily\")\n .select(\"api_requests\")\n .where(\"account_id\", \"=\", accountId)\n .where(\"date\", \"=\", today)\n .executeTakeFirst();\n\n // This month's deliveries\n const monthlyRow = await db\n .selectFrom(\"usage_daily\")\n .select(sql<number>`COALESCE(SUM(deliveries), 0)`.as(\"total\"))\n .where(\"account_id\", \"=\", accountId)\n .where(\"date\", \">=\", monthStart)\n .executeTakeFirst();\n\n // Latest storage snapshot\n const storageRow = await db\n .selectFrom(\"usage_snapshots\")\n .select(\"storage_bytes\")\n .where(\"account_id\", \"=\", accountId)\n .orderBy(\"measured_at\", \"desc\")\n .limit(1)\n .executeTakeFirst();\n\n return {\n apiRequestsToday: dailyRow?.api_requests ?? 0,\n deliveriesThisMonth: Number(monthlyRow?.total ?? 0),\n storageBytes: Number(storageRow?.storage_bytes ?? 0),\n };\n}\n\nexport interface LimitCheck {\n allowed: boolean;\n limits: ReturnType<typeof getPlanLimits>;\n current: UsageSummary & { streams: number; views: number };\n exceeded?: string;\n}\n\n/** Check if an account is within plan limits. */\nexport async function checkLimits(\n db: Kysely<Database>,\n accountId: string,\n plan: string,\n): Promise<LimitCheck> {\n const limits = getPlanLimits(plan);\n const usage = await getUsage(db, accountId);\n\n // Count streams owned by this account's keys\n const streamCount = await db\n .selectFrom(\"streams\")\n .innerJoin(\"api_keys\", \"streams.api_key_id\", \"api_keys.id\")\n .select(sql<number>`count(*)`.as(\"count\"))\n .where(\"api_keys.account_id\", \"=\", accountId)\n .executeTakeFirst();\n\n const viewCount = await db\n .selectFrom(\"views\")\n .innerJoin(\"api_keys\", \"views.api_key_id\", \"api_keys.id\")\n .select(sql<number>`count(*)`.as(\"count\"))\n .where(\"api_keys.account_id\", \"=\", accountId)\n .executeTakeFirst();\n\n const current = {\n ...usage,\n streams: Number(streamCount?.count ?? 0),\n views: Number(viewCount?.count ?? 0),\n };\n\n // Check each limit\n if (current.streams >= limits.streams) {\n return { allowed: false, limits, current, exceeded: \"streams\" };\n }\n if (current.views >= limits.views) {\n return { allowed: false, limits, current, exceeded: \"views\" };\n }\n if (current.apiRequestsToday >= limits.apiRequestsPerDay) {\n return { allowed: false, limits, current, exceeded: \"api_requests\" };\n }\n if (current.deliveriesThisMonth >= limits.deliveriesPerMonth) {\n return { allowed: false, limits, current, exceeded: \"deliveries\" };\n }\n if (current.storageBytes >= limits.storageBytes) {\n return { allowed: false, limits, current, exceeded: \"storage\" };\n }\n\n return { allowed: true, limits, current };\n}\n\n/**\n * Measure storage for all accounts by querying pg_total_relation_size\n * for each tenant's view schemas.\n */\nexport async function measureStorage(db: Kysely<Database>): Promise<void> {\n // Get all accounts with views\n const accountViews = await db\n .selectFrom(\"views\")\n .innerJoin(\"api_keys\", \"views.api_key_id\", \"api_keys.id\")\n .select([\"api_keys.account_id\", \"views.schema_name\"])\n .where(\"views.schema_name\", \"is not\", null)\n .execute();\n\n // Group schemas by account\n const byAccount = new Map<string, string[]>();\n for (const row of accountViews) {\n const schemas = byAccount.get(row.account_id) ?? [];\n if (row.schema_name) schemas.push(row.schema_name);\n byAccount.set(row.account_id, schemas);\n }\n\n for (const [accountId, schemas] of byAccount) {\n let totalBytes = 0;\n for (const schema of schemas) {\n try {\n const 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 totalBytes += Number((result.rows[0] as any)?.size ?? 0);\n } catch {\n // Schema may not exist\n }\n }\n\n await db\n .insertInto(\"usage_snapshots\")\n .values({\n account_id: accountId,\n storage_bytes: totalBytes,\n })\n .execute();\n }\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "51CB827BBF658C6C64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,191 @@
1
+ import { Kysely } from "kysely";
2
+ import { Generated } from "kysely";
3
+ interface BlocksTable {
4
+ height: number;
5
+ hash: string;
6
+ parent_hash: string;
7
+ burn_block_height: number;
8
+ timestamp: number;
9
+ canonical: Generated<boolean>;
10
+ created_at: Generated<Date>;
11
+ }
12
+ interface TransactionsTable {
13
+ tx_id: string;
14
+ block_height: number;
15
+ type: string;
16
+ sender: string;
17
+ status: string;
18
+ contract_id: string | null;
19
+ function_name: string | null;
20
+ raw_tx: string;
21
+ created_at: Generated<Date>;
22
+ }
23
+ interface EventsTable {
24
+ id: Generated<string>;
25
+ tx_id: string;
26
+ block_height: number;
27
+ event_index: number;
28
+ type: string;
29
+ data: unknown;
30
+ created_at: Generated<Date>;
31
+ }
32
+ interface StreamsTable {
33
+ id: Generated<string>;
34
+ name: string;
35
+ status: Generated<string>;
36
+ filters: unknown;
37
+ options: Generated<unknown>;
38
+ webhook_url: string;
39
+ webhook_secret: string | null;
40
+ api_key_id: string | null;
41
+ created_at: Generated<Date>;
42
+ updated_at: Generated<Date>;
43
+ }
44
+ interface StreamMetricsTable {
45
+ stream_id: string;
46
+ last_triggered_at: Date | null;
47
+ last_triggered_block: number | null;
48
+ total_deliveries: Generated<number>;
49
+ failed_deliveries: Generated<number>;
50
+ error_message: string | null;
51
+ }
52
+ interface JobsTable {
53
+ id: Generated<string>;
54
+ stream_id: string;
55
+ block_height: number;
56
+ status: Generated<string>;
57
+ attempts: Generated<number>;
58
+ locked_at: Date | null;
59
+ locked_by: string | null;
60
+ error: string | null;
61
+ backfill: Generated<boolean>;
62
+ created_at: Generated<Date>;
63
+ completed_at: Date | null;
64
+ }
65
+ interface IndexProgressTable {
66
+ network: string;
67
+ last_indexed_block: Generated<number>;
68
+ last_contiguous_block: Generated<number>;
69
+ highest_seen_block: Generated<number>;
70
+ updated_at: Generated<Date>;
71
+ }
72
+ interface DeliveriesTable {
73
+ id: Generated<string>;
74
+ stream_id: string;
75
+ job_id: string | null;
76
+ block_height: number;
77
+ status: string;
78
+ status_code: number | null;
79
+ response_time_ms: number | null;
80
+ attempts: Generated<number>;
81
+ error: string | null;
82
+ payload: unknown;
83
+ created_at: Generated<Date>;
84
+ }
85
+ interface ViewsTable {
86
+ id: Generated<string>;
87
+ name: string;
88
+ version: Generated<string>;
89
+ status: Generated<string>;
90
+ definition: unknown;
91
+ schema_hash: string;
92
+ handler_path: string;
93
+ schema_name: string | null;
94
+ last_processed_block: Generated<number>;
95
+ last_error: string | null;
96
+ last_error_at: Date | null;
97
+ total_processed: Generated<number>;
98
+ total_errors: Generated<number>;
99
+ api_key_id: string | null;
100
+ created_at: Generated<Date>;
101
+ updated_at: Generated<Date>;
102
+ }
103
+ interface ApiKeysTable {
104
+ id: Generated<string>;
105
+ key_hash: string;
106
+ key_prefix: string;
107
+ name: string | null;
108
+ status: Generated<string>;
109
+ rate_limit: Generated<number>;
110
+ ip_address: string;
111
+ account_id: string;
112
+ last_used_at: Date | null;
113
+ revoked_at: Date | null;
114
+ created_at: Generated<Date>;
115
+ }
116
+ interface AccountsTable {
117
+ id: Generated<string>;
118
+ email: string;
119
+ plan: Generated<string>;
120
+ created_at: Generated<Date>;
121
+ }
122
+ interface SessionsTable {
123
+ id: Generated<string>;
124
+ token_hash: string;
125
+ token_prefix: string;
126
+ account_id: string;
127
+ ip_address: string;
128
+ expires_at: Generated<Date>;
129
+ revoked_at: Date | null;
130
+ last_used_at: Date | null;
131
+ created_at: Generated<Date>;
132
+ }
133
+ interface MagicLinksTable {
134
+ id: Generated<string>;
135
+ email: string;
136
+ token: string;
137
+ expires_at: Date;
138
+ used_at: Date | null;
139
+ created_at: Generated<Date>;
140
+ }
141
+ interface UsageDailyTable {
142
+ account_id: string;
143
+ date: string;
144
+ api_requests: Generated<number>;
145
+ deliveries: Generated<number>;
146
+ }
147
+ interface UsageSnapshotsTable {
148
+ id: Generated<string>;
149
+ account_id: string;
150
+ measured_at: Generated<Date>;
151
+ storage_bytes: Generated<number>;
152
+ }
153
+ interface Database {
154
+ blocks: BlocksTable;
155
+ transactions: TransactionsTable;
156
+ events: EventsTable;
157
+ streams: StreamsTable;
158
+ stream_metrics: StreamMetricsTable;
159
+ jobs: JobsTable;
160
+ index_progress: IndexProgressTable;
161
+ deliveries: DeliveriesTable;
162
+ views: ViewsTable;
163
+ api_keys: ApiKeysTable;
164
+ accounts: AccountsTable;
165
+ sessions: SessionsTable;
166
+ magic_links: MagicLinksTable;
167
+ usage_daily: UsageDailyTable;
168
+ usage_snapshots: UsageSnapshotsTable;
169
+ }
170
+ /**
171
+ * Convert a view name to its PostgreSQL schema name.
172
+ * With keyPrefix: "view_{prefix}_{name}" (tenant-isolated)
173
+ * Without keyPrefix: "view_{name}" (backward compat)
174
+ */
175
+ declare function pgSchemaName(viewName: string, keyPrefix?: string): string;
176
+ declare function registerView(db: Kysely<Database>, data: {
177
+ name: string
178
+ version: string
179
+ definition: Record<string, unknown>
180
+ schemaHash: string
181
+ handlerPath: string
182
+ apiKeyId?: string
183
+ schemaName?: string
184
+ });
185
+ declare function getView(db: Kysely<Database>, name: string, apiKeyId?: string);
186
+ declare function listViews(db: Kysely<Database>, apiKeyId?: string);
187
+ declare function updateViewStatus(db: Kysely<Database>, name: string, status: string, lastProcessedBlock?: number);
188
+ declare function recordViewProcessed(db: Kysely<Database>, name: string, processed: number, errors: number, lastError?: string);
189
+ declare function updateViewHandlerPath(db: Kysely<Database>, name: string, handlerPath: string);
190
+ declare function deleteView(db: Kysely<Database>, name: string, apiKeyId?: string);
191
+ export { updateViewStatus, updateViewHandlerPath, registerView, recordViewProcessed, pgSchemaName, listViews, getView, deleteView };