@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,182 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ // ── blocks ──────────────────────────────────────────────────────────
5
+ await db.schema
6
+ .createTable("blocks")
7
+ .addColumn("height", "bigint", (c) => c.primaryKey())
8
+ .addColumn("hash", "text", (c) => c.notNull())
9
+ .addColumn("parent_hash", "text", (c) => c.notNull())
10
+ .addColumn("burn_block_height", "bigint", (c) => c.notNull())
11
+ .addColumn("timestamp", "bigint", (c) => c.notNull())
12
+ .addColumn("canonical", "boolean", (c) => c.notNull().defaultTo(true))
13
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
14
+ .execute();
15
+
16
+ await db.schema.createIndex("blocks_hash_idx").on("blocks").column("hash").execute();
17
+ await db.schema.createIndex("blocks_canonical_height_idx").on("blocks").columns(["canonical", "height"]).execute();
18
+
19
+ // ── transactions ────────────────────────────────────────────────────
20
+ await db.schema
21
+ .createTable("transactions")
22
+ .addColumn("tx_id", "text", (c) => c.primaryKey())
23
+ .addColumn("block_height", "bigint", (c) => c.notNull().references("blocks.height"))
24
+ .addColumn("type", "text", (c) => c.notNull())
25
+ .addColumn("sender", "text", (c) => c.notNull())
26
+ .addColumn("status", "text", (c) => c.notNull())
27
+ .addColumn("contract_id", "text")
28
+ .addColumn("function_name", "text")
29
+ .addColumn("raw_tx", "text", (c) => c.notNull())
30
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
31
+ .execute();
32
+
33
+ await db.schema.createIndex("transactions_block_height_idx").on("transactions").column("block_height").execute();
34
+ await db.schema.createIndex("transactions_sender_idx").on("transactions").column("sender").execute();
35
+ await db.schema.createIndex("transactions_contract_id_idx").on("transactions").column("contract_id").execute();
36
+
37
+ // ── events ──────────────────────────────────────────────────────────
38
+ await db.schema
39
+ .createTable("events")
40
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
41
+ .addColumn("tx_id", "text", (c) => c.notNull().references("transactions.tx_id"))
42
+ .addColumn("block_height", "bigint", (c) => c.notNull().references("blocks.height"))
43
+ .addColumn("event_index", "integer", (c) => c.notNull())
44
+ .addColumn("type", "text", (c) => c.notNull())
45
+ .addColumn("data", "jsonb", (c) => c.notNull())
46
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
47
+ .execute();
48
+
49
+ await db.schema.createIndex("events_tx_id_idx").on("events").column("tx_id").execute();
50
+ await db.schema.createIndex("events_block_height_idx").on("events").column("block_height").execute();
51
+ await db.schema.createIndex("events_type_idx").on("events").column("type").execute();
52
+
53
+ // ── streams ─────────────────────────────────────────────────────────
54
+ await db.schema
55
+ .createTable("streams")
56
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
57
+ .addColumn("name", "text", (c) => c.notNull())
58
+ .addColumn("status", "text", (c) => c.notNull().defaultTo("active"))
59
+ .addColumn("filters", "jsonb", (c) => c.notNull())
60
+ .addColumn("options", "jsonb", (c) => c.notNull().defaultTo(sql`'{}'::jsonb`))
61
+ .addColumn("webhook_url", "text", (c) => c.notNull())
62
+ .addColumn("webhook_secret", "text")
63
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
64
+ .addColumn("updated_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
65
+ .execute();
66
+
67
+ await db.schema.createIndex("streams_status_idx").on("streams").column("status").execute();
68
+
69
+ // ── stream_metrics ──────────────────────────────────────────────────
70
+ await db.schema
71
+ .createTable("stream_metrics")
72
+ .addColumn("stream_id", "uuid", (c) => c.primaryKey().references("streams.id").onDelete("cascade"))
73
+ .addColumn("last_triggered_at", "timestamp")
74
+ .addColumn("last_triggered_block", "bigint")
75
+ .addColumn("total_deliveries", "integer", (c) => c.notNull().defaultTo(0))
76
+ .addColumn("failed_deliveries", "integer", (c) => c.notNull().defaultTo(0))
77
+ .addColumn("error_message", "text")
78
+ .execute();
79
+
80
+ await db.schema.createIndex("stream_metrics_last_triggered_at_idx").on("stream_metrics").column("last_triggered_at").execute();
81
+
82
+ // ── jobs ────────────────────────────────────────────────────────────
83
+ await db.schema
84
+ .createTable("jobs")
85
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
86
+ .addColumn("stream_id", "uuid", (c) => c.notNull().references("streams.id").onDelete("cascade"))
87
+ .addColumn("block_height", "bigint", (c) => c.notNull())
88
+ .addColumn("status", "text", (c) => c.notNull().defaultTo("pending"))
89
+ .addColumn("attempts", "integer", (c) => c.notNull().defaultTo(0))
90
+ .addColumn("locked_at", "timestamp")
91
+ .addColumn("locked_by", "text")
92
+ .addColumn("error", "text")
93
+ .addColumn("backfill", "boolean", (c) => c.notNull().defaultTo(false))
94
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
95
+ .addColumn("completed_at", "timestamp")
96
+ .execute();
97
+
98
+ await db.schema.createIndex("jobs_stream_id_idx").on("jobs").column("stream_id").execute();
99
+ await db.schema.createIndex("jobs_status_idx").on("jobs").column("status").execute();
100
+ await db.schema.createIndex("jobs_block_height_idx").on("jobs").column("block_height").execute();
101
+ await db.schema.createIndex("jobs_locked_at_idx").on("jobs").column("locked_at").execute();
102
+
103
+ // ── index_progress ──────────────────────────────────────────────────
104
+ await db.schema
105
+ .createTable("index_progress")
106
+ .addColumn("network", "text", (c) => c.primaryKey())
107
+ .addColumn("last_indexed_block", "bigint", (c) => c.notNull().defaultTo(0))
108
+ .addColumn("last_contiguous_block", "bigint", (c) => c.notNull().defaultTo(0))
109
+ .addColumn("highest_seen_block", "bigint", (c) => c.notNull().defaultTo(0))
110
+ .addColumn("updated_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
111
+ .execute();
112
+
113
+ // ── deliveries ──────────────────────────────────────────────────────
114
+ await db.schema
115
+ .createTable("deliveries")
116
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
117
+ .addColumn("stream_id", "uuid", (c) => c.notNull().references("streams.id").onDelete("cascade"))
118
+ .addColumn("job_id", "uuid", (c) => c.references("jobs.id").onDelete("set null"))
119
+ .addColumn("block_height", "bigint", (c) => c.notNull())
120
+ .addColumn("status", "text", (c) => c.notNull())
121
+ .addColumn("status_code", "integer")
122
+ .addColumn("response_time_ms", "integer")
123
+ .addColumn("attempts", "integer", (c) => c.notNull().defaultTo(1))
124
+ .addColumn("error", "text")
125
+ .addColumn("payload", "jsonb", (c) => c.notNull())
126
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
127
+ .execute();
128
+
129
+ await db.schema.createIndex("deliveries_stream_id_idx").on("deliveries").column("stream_id").execute();
130
+ await db.schema.createIndex("deliveries_status_idx").on("deliveries").column("status").execute();
131
+ await db.schema.createIndex("deliveries_block_height_idx").on("deliveries").column("block_height").execute();
132
+
133
+ // ── views ───────────────────────────────────────────────────────────
134
+ await db.schema
135
+ .createTable("views")
136
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
137
+ .addColumn("name", "text", (c) => c.notNull().unique())
138
+ .addColumn("version", "text", (c) => c.notNull().defaultTo("1.0.0"))
139
+ .addColumn("status", "text", (c) => c.notNull().defaultTo("active"))
140
+ .addColumn("definition", "jsonb", (c) => c.notNull())
141
+ .addColumn("schema_hash", "text", (c) => c.notNull())
142
+ .addColumn("handler_path", "text", (c) => c.notNull())
143
+ .addColumn("last_processed_block", "bigint", (c) => c.notNull().defaultTo(0))
144
+ .addColumn("last_error", "text")
145
+ .addColumn("last_error_at", "timestamp")
146
+ .addColumn("total_processed", "bigint", (c) => c.notNull().defaultTo(0))
147
+ .addColumn("total_errors", "bigint", (c) => c.notNull().defaultTo(0))
148
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
149
+ .addColumn("updated_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
150
+ .execute();
151
+
152
+ await db.schema.createIndex("views_name_idx").on("views").column("name").execute();
153
+ await db.schema.createIndex("views_status_idx").on("views").column("status").execute();
154
+
155
+ // Notify trigger for view changes (used by API hot-reload)
156
+ await sql`
157
+ CREATE OR REPLACE FUNCTION notify_view_changes() RETURNS trigger AS $$
158
+ BEGIN
159
+ PERFORM pg_notify('view_changes', json_build_object(
160
+ 'operation', TG_OP,
161
+ 'name', COALESCE(NEW.name, OLD.name)
162
+ )::text);
163
+ RETURN COALESCE(NEW, OLD);
164
+ END;
165
+ $$ LANGUAGE plpgsql
166
+ `.execute(db);
167
+
168
+ await sql`
169
+ CREATE TRIGGER views_notify_trigger
170
+ AFTER INSERT OR UPDATE OR DELETE ON "views"
171
+ FOR EACH ROW EXECUTE FUNCTION notify_view_changes()
172
+ `.execute(db);
173
+ }
174
+
175
+ export async function down(db: Kysely<any>): Promise<void> {
176
+ await sql`DROP TRIGGER IF EXISTS views_notify_trigger ON "views"`.execute(db);
177
+ await sql`DROP FUNCTION IF EXISTS notify_view_changes()`.execute(db);
178
+
179
+ for (const table of ["views", "deliveries", "index_progress", "jobs", "stream_metrics", "streams", "events", "transactions", "blocks"]) {
180
+ await db.schema.dropTable(table).ifExists().cascade().execute();
181
+ }
182
+ }
@@ -0,0 +1,38 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("api_keys")
6
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
7
+ .addColumn("key_hash", "text", (c) => c.notNull().unique())
8
+ .addColumn("key_prefix", "text", (c) => c.notNull())
9
+ .addColumn("name", "text")
10
+ .addColumn("status", "text", (c) => c.notNull().defaultTo("active"))
11
+ .addColumn("rate_limit", "integer", (c) => c.notNull().defaultTo(120))
12
+ .addColumn("ip_address", "text", (c) => c.notNull())
13
+ .addColumn("last_used_at", "timestamp")
14
+ .addColumn("revoked_at", "timestamp")
15
+ .addColumn("created_at", "timestamp", (c) => c.notNull().defaultTo(sql`now()`))
16
+ .execute();
17
+
18
+ await db.schema.createIndex("api_keys_key_hash_idx").on("api_keys").column("key_hash").execute();
19
+ await db.schema.createIndex("api_keys_status_idx").on("api_keys").column("status").execute();
20
+ await db.schema.createIndex("api_keys_ip_address_idx").on("api_keys").column("ip_address").execute();
21
+
22
+ // Add api_key_id to streams and views
23
+ await db.schema
24
+ .alterTable("streams")
25
+ .addColumn("api_key_id", "uuid", (c) => c.references("api_keys.id"))
26
+ .execute();
27
+
28
+ await db.schema
29
+ .alterTable("views")
30
+ .addColumn("api_key_id", "uuid", (c) => c.references("api_keys.id"))
31
+ .execute();
32
+ }
33
+
34
+ export async function down(db: Kysely<any>): Promise<void> {
35
+ await db.schema.alterTable("views").dropColumn("api_key_id").execute();
36
+ await db.schema.alterTable("streams").dropColumn("api_key_id").execute();
37
+ await db.schema.dropTable("api_keys").ifExists().cascade().execute();
38
+ }
@@ -0,0 +1,114 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ // 1. Drop PG schemas for views with null api_key_id (orphaned pre-product data)
5
+ const orphanedViews = await db
6
+ .selectFrom("views")
7
+ .select("name")
8
+ .where("api_key_id", "is", null)
9
+ .execute();
10
+
11
+ for (const view of orphanedViews) {
12
+ const schemaName = `view_${view.name.replace(/-/g, "_")}`;
13
+ await sql.raw(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`).execute(db);
14
+ }
15
+
16
+ // 2. Delete orphaned rows (null api_key_id)
17
+ await db.deleteFrom("views").where("api_key_id", "is", null).execute();
18
+
19
+ // Delete stream_metrics for orphaned streams first (FK)
20
+ const orphanedStreamIds = await db
21
+ .selectFrom("streams")
22
+ .select("id")
23
+ .where("api_key_id", "is", null)
24
+ .execute();
25
+
26
+ if (orphanedStreamIds.length > 0) {
27
+ await db
28
+ .deleteFrom("stream_metrics")
29
+ .where(
30
+ "stream_id",
31
+ "in",
32
+ orphanedStreamIds.map((s) => s.id),
33
+ )
34
+ .execute();
35
+
36
+ // Delete jobs + deliveries for orphaned streams
37
+ await db
38
+ .deleteFrom("deliveries")
39
+ .where(
40
+ "stream_id",
41
+ "in",
42
+ orphanedStreamIds.map((s) => s.id),
43
+ )
44
+ .execute();
45
+
46
+ await db
47
+ .deleteFrom("jobs")
48
+ .where(
49
+ "stream_id",
50
+ "in",
51
+ orphanedStreamIds.map((s) => s.id),
52
+ )
53
+ .execute();
54
+ }
55
+
56
+ await db.deleteFrom("streams").where("api_key_id", "is", null).execute();
57
+
58
+ // 3. Add schema_name column to views
59
+ await db.schema
60
+ .alterTable("views")
61
+ .addColumn("schema_name", "text")
62
+ .execute();
63
+
64
+ // Backfill schema_name for existing views
65
+ const existingViews = await db
66
+ .selectFrom("views")
67
+ .select(["id", "name"])
68
+ .execute();
69
+
70
+ for (const view of existingViews) {
71
+ const schemaName = `view_${view.name.replace(/-/g, "_")}`;
72
+ await db
73
+ .updateTable("views")
74
+ .set({ schema_name: schemaName })
75
+ .where("id", "=", view.id)
76
+ .execute();
77
+ }
78
+
79
+ // 4. Drop unique constraint on views(name), replace with views(name, api_key_id)
80
+ // The original unique constraint is from the initial migration on "name"
81
+ await sql.raw(`ALTER TABLE views DROP CONSTRAINT IF EXISTS views_name_key`).execute(db);
82
+ await sql.raw(`ALTER TABLE views DROP CONSTRAINT IF EXISTS views_name_unique`).execute(db);
83
+
84
+ await db.schema
85
+ .createIndex("views_name_api_key_id_unique")
86
+ .on("views")
87
+ .columns(["name", "api_key_id"])
88
+ .unique()
89
+ .execute();
90
+
91
+ // 5. Add indexes for tenant scoping
92
+ await db.schema
93
+ .createIndex("streams_api_key_id_idx")
94
+ .on("streams")
95
+ .column("api_key_id")
96
+ .execute();
97
+
98
+ await db.schema
99
+ .createIndex("views_api_key_id_idx")
100
+ .on("views")
101
+ .column("api_key_id")
102
+ .execute();
103
+ }
104
+
105
+ export async function down(db: Kysely<any>): Promise<void> {
106
+ await db.schema.dropIndex("views_api_key_id_idx").ifExists().execute();
107
+ await db.schema.dropIndex("streams_api_key_id_idx").ifExists().execute();
108
+ await db.schema.dropIndex("views_name_api_key_id_unique").ifExists().execute();
109
+
110
+ // Restore original unique constraint on name
111
+ await sql.raw(`ALTER TABLE views ADD CONSTRAINT views_name_key UNIQUE (name)`).execute(db);
112
+
113
+ await db.schema.alterTable("views").dropColumn("schema_name").execute();
114
+ }
@@ -0,0 +1,90 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ // 1. Create accounts table
5
+ await db.schema
6
+ .createTable("accounts")
7
+ .addColumn("id", "uuid", (col) =>
8
+ col.primaryKey().defaultTo(sql`gen_random_uuid()`),
9
+ )
10
+ .addColumn("email", "text", (col) => col.unique().notNull())
11
+ .addColumn("plan", "text", (col) => col.defaultTo("free").notNull())
12
+ .addColumn("created_at", "timestamptz", (col) =>
13
+ col.defaultTo(sql`NOW()`).notNull(),
14
+ )
15
+ .execute();
16
+
17
+ // 2. Create magic_links table
18
+ await db.schema
19
+ .createTable("magic_links")
20
+ .addColumn("id", "uuid", (col) =>
21
+ col.primaryKey().defaultTo(sql`gen_random_uuid()`),
22
+ )
23
+ .addColumn("email", "text", (col) => col.notNull())
24
+ .addColumn("token", "text", (col) => col.unique().notNull())
25
+ .addColumn("expires_at", "timestamptz", (col) => col.notNull())
26
+ .addColumn("used_at", "timestamptz")
27
+ .addColumn("created_at", "timestamptz", (col) =>
28
+ col.defaultTo(sql`NOW()`).notNull(),
29
+ )
30
+ .execute();
31
+
32
+ // 3. Create usage_daily table
33
+ await db.schema
34
+ .createTable("usage_daily")
35
+ .addColumn("account_id", "uuid", (col) =>
36
+ col.references("accounts.id").onDelete("cascade").notNull(),
37
+ )
38
+ .addColumn("date", "date", (col) => col.notNull())
39
+ .addColumn("api_requests", "integer", (col) => col.defaultTo(0).notNull())
40
+ .addColumn("deliveries", "integer", (col) => col.defaultTo(0).notNull())
41
+ .execute();
42
+
43
+ await sql`ALTER TABLE usage_daily ADD PRIMARY KEY (account_id, date)`.execute(db);
44
+
45
+ // 4. Create usage_snapshots table
46
+ await db.schema
47
+ .createTable("usage_snapshots")
48
+ .addColumn("id", "uuid", (col) =>
49
+ col.primaryKey().defaultTo(sql`gen_random_uuid()`),
50
+ )
51
+ .addColumn("account_id", "uuid", (col) =>
52
+ col.references("accounts.id").onDelete("cascade").notNull(),
53
+ )
54
+ .addColumn("measured_at", "timestamptz", (col) =>
55
+ col.defaultTo(sql`NOW()`).notNull(),
56
+ )
57
+ .addColumn("storage_bytes", "bigint", (col) => col.defaultTo(0).notNull())
58
+ .execute();
59
+
60
+ // 5. Add account_id FK to api_keys
61
+ await db.schema
62
+ .alterTable("api_keys")
63
+ .addColumn("account_id", "uuid", (col) =>
64
+ col.references("accounts.id").onDelete("cascade"),
65
+ )
66
+ .execute();
67
+
68
+ // 6. Delete orphan api_keys (no account_id)
69
+ await db.deleteFrom("api_keys").where("account_id", "is", null).execute();
70
+
71
+ // 7. Set NOT NULL on account_id
72
+ await sql`ALTER TABLE api_keys ALTER COLUMN account_id SET NOT NULL`.execute(db);
73
+
74
+ // 8. Index for account_id lookups
75
+ await db.schema
76
+ .createIndex("api_keys_account_id_idx")
77
+ .on("api_keys")
78
+ .column("account_id")
79
+ .execute();
80
+ }
81
+
82
+ export async function down(db: Kysely<any>): Promise<void> {
83
+ await db.schema.dropIndex("api_keys_account_id_idx").ifExists().execute();
84
+ await sql`ALTER TABLE api_keys ALTER COLUMN account_id DROP NOT NULL`.execute(db);
85
+ await db.schema.alterTable("api_keys").dropColumn("account_id").execute();
86
+ await db.schema.dropTable("usage_snapshots").ifExists().execute();
87
+ await db.schema.dropTable("usage_daily").ifExists().execute();
88
+ await db.schema.dropTable("magic_links").ifExists().execute();
89
+ await db.schema.dropTable("accounts").ifExists().execute();
90
+ }
@@ -0,0 +1,42 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("sessions")
6
+ .addColumn("id", "uuid", (col) =>
7
+ col.primaryKey().defaultTo(sql`gen_random_uuid()`),
8
+ )
9
+ .addColumn("token_hash", "text", (col) => col.unique().notNull())
10
+ .addColumn("token_prefix", "text", (col) => col.notNull())
11
+ .addColumn("account_id", "uuid", (col) =>
12
+ col.references("accounts.id").onDelete("cascade").notNull(),
13
+ )
14
+ .addColumn("ip_address", "text", (col) => col.notNull())
15
+ .addColumn("expires_at", "timestamptz", (col) =>
16
+ col.defaultTo(sql`NOW() + INTERVAL '90 days'`).notNull(),
17
+ )
18
+ .addColumn("revoked_at", "timestamptz")
19
+ .addColumn("last_used_at", "timestamptz")
20
+ .addColumn("created_at", "timestamptz", (col) =>
21
+ col.defaultTo(sql`NOW()`).notNull(),
22
+ )
23
+ .execute();
24
+
25
+ await db.schema
26
+ .createIndex("sessions_token_hash_idx")
27
+ .on("sessions")
28
+ .column("token_hash")
29
+ .execute();
30
+
31
+ await db.schema
32
+ .createIndex("sessions_account_id_idx")
33
+ .on("sessions")
34
+ .column("account_id")
35
+ .execute();
36
+ }
37
+
38
+ export async function down(db: Kysely<any>): Promise<void> {
39
+ await db.schema.dropIndex("sessions_account_id_idx").ifExists().execute();
40
+ await db.schema.dropIndex("sessions_token_hash_idx").ifExists().execute();
41
+ await db.schema.dropTable("sessions").ifExists().execute();
42
+ }
package/package.json ADDED
@@ -0,0 +1,128 @@
1
+ {
2
+ "name": "@secondlayer/shared",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/src/index.js",
6
+ "types": "./dist/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/src/index.d.ts",
10
+ "import": "./dist/src/index.js"
11
+ },
12
+ "./db": {
13
+ "types": "./dist/src/db/index.d.ts",
14
+ "import": "./dist/src/db/index.js"
15
+ },
16
+ "./db/queries/integrity": {
17
+ "types": "./dist/src/db/queries/integrity.d.ts",
18
+ "import": "./dist/src/db/queries/integrity.js"
19
+ },
20
+ "./db/queries/metrics": {
21
+ "types": "./dist/src/db/queries/metrics.d.ts",
22
+ "import": "./dist/src/db/queries/metrics.js"
23
+ },
24
+ "./db/queries/accounts": {
25
+ "types": "./dist/src/db/queries/accounts.d.ts",
26
+ "import": "./dist/src/db/queries/accounts.js"
27
+ },
28
+ "./db/queries/usage": {
29
+ "types": "./dist/src/db/queries/usage.d.ts",
30
+ "import": "./dist/src/db/queries/usage.js"
31
+ },
32
+ "./db/queries/views": {
33
+ "types": "./dist/src/db/queries/views.d.ts",
34
+ "import": "./dist/src/db/queries/views.js"
35
+ },
36
+ "./db/jsonb": {
37
+ "types": "./dist/src/db/jsonb.d.ts",
38
+ "import": "./dist/src/db/jsonb.js"
39
+ },
40
+ "./db/schema": {
41
+ "types": "./dist/src/db/schema.d.ts",
42
+ "import": "./dist/src/db/schema.js"
43
+ },
44
+ "./lib/plans": {
45
+ "types": "./dist/src/lib/plans.d.ts",
46
+ "import": "./dist/src/lib/plans.js"
47
+ },
48
+ "./queue": {
49
+ "types": "./dist/src/queue/index.d.ts",
50
+ "import": "./dist/src/queue/index.js"
51
+ },
52
+ "./queue/listener": {
53
+ "types": "./dist/src/queue/listener.d.ts",
54
+ "import": "./dist/src/queue/listener.js"
55
+ },
56
+ "./queue/recovery": {
57
+ "types": "./dist/src/queue/recovery.d.ts",
58
+ "import": "./dist/src/queue/recovery.js"
59
+ },
60
+ "./schemas": {
61
+ "types": "./dist/src/schemas/index.d.ts",
62
+ "import": "./dist/src/schemas/index.js"
63
+ },
64
+ "./schemas/filters": {
65
+ "types": "./dist/src/schemas/filters.d.ts",
66
+ "import": "./dist/src/schemas/filters.js"
67
+ },
68
+ "./schemas/views": {
69
+ "types": "./dist/src/schemas/views.d.ts",
70
+ "import": "./dist/src/schemas/views.js"
71
+ },
72
+ "./types": {
73
+ "types": "./dist/src/types.d.ts",
74
+ "import": "./dist/src/types.js"
75
+ },
76
+ "./env": {
77
+ "types": "./dist/src/env.d.ts",
78
+ "import": "./dist/src/env.js"
79
+ },
80
+ "./logger": {
81
+ "types": "./dist/src/logger.d.ts",
82
+ "import": "./dist/src/logger.js"
83
+ },
84
+ "./errors": {
85
+ "types": "./dist/src/errors.d.ts",
86
+ "import": "./dist/src/errors.js"
87
+ },
88
+ "./crypto": {
89
+ "types": "./dist/src/crypto/hmac.d.ts",
90
+ "import": "./dist/src/crypto/hmac.js"
91
+ },
92
+ "./crypto/hmac": {
93
+ "types": "./dist/src/crypto/hmac.d.ts",
94
+ "import": "./dist/src/crypto/hmac.js"
95
+ },
96
+ "./node": {
97
+ "types": "./dist/src/node/client.d.ts",
98
+ "import": "./dist/src/node/client.js"
99
+ },
100
+ "./node/hiro-client": {
101
+ "types": "./dist/src/node/hiro-client.d.ts",
102
+ "import": "./dist/src/node/hiro-client.js"
103
+ }
104
+ },
105
+ "files": [
106
+ "dist",
107
+ "migrations"
108
+ ],
109
+ "scripts": {
110
+ "build": "bunup",
111
+ "dev": "bunup --watch",
112
+ "test": "bun test",
113
+ "typecheck": "tsc --noEmit",
114
+ "migrate": "bun run src/db/migrate.ts",
115
+ "prepublishOnly": "bun run build"
116
+ },
117
+ "dependencies": {
118
+ "@stacks/transactions": "7.3.1",
119
+ "kysely": "0.28.10",
120
+ "kysely-postgres-js": "3.0.0",
121
+ "postgres": "^3.4.6",
122
+ "zod": "^3.24.1"
123
+ },
124
+ "devDependencies": {
125
+ "@types/bun": "latest",
126
+ "typescript": "^5"
127
+ }
128
+ }