@moneysiren/app 0.1.0-alpha.10

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/apps/cli/src/cli.d.ts +59 -0
  4. package/dist/apps/cli/src/cli.js +199 -0
  5. package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
  6. package/dist/apps/cli/src/commands/dashboard.js +239 -0
  7. package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
  8. package/dist/apps/cli/src/commands/doctor.js +25 -0
  9. package/dist/apps/cli/src/commands/init.d.ts +3 -0
  10. package/dist/apps/cli/src/commands/init.js +18 -0
  11. package/dist/apps/cli/src/commands/install.d.ts +3 -0
  12. package/dist/apps/cli/src/commands/install.js +244 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +73 -0
  15. package/dist/apps/cli/src/commands/notify.d.ts +3 -0
  16. package/dist/apps/cli/src/commands/notify.js +430 -0
  17. package/dist/apps/cli/src/commands/report.d.ts +3 -0
  18. package/dist/apps/cli/src/commands/report.js +206 -0
  19. package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
  20. package/dist/apps/cli/src/commands/runtime.js +499 -0
  21. package/dist/apps/cli/src/commands/shared.d.ts +9 -0
  22. package/dist/apps/cli/src/commands/shared.js +29 -0
  23. package/dist/apps/cli/src/commands/summary.d.ts +3 -0
  24. package/dist/apps/cli/src/commands/summary.js +15 -0
  25. package/dist/apps/cli/src/commands/sync.d.ts +3 -0
  26. package/dist/apps/cli/src/commands/sync.js +393 -0
  27. package/dist/apps/cli/src/commands/theme.d.ts +3 -0
  28. package/dist/apps/cli/src/commands/theme.js +181 -0
  29. package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
  30. package/dist/apps/cli/src/desktop-runtime.js +720 -0
  31. package/dist/apps/cli/src/home.d.ts +7 -0
  32. package/dist/apps/cli/src/home.js +124 -0
  33. package/dist/apps/cli/src/index.d.ts +3 -0
  34. package/dist/apps/cli/src/index.js +14 -0
  35. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  36. package/dist/apps/cli/src/install-profile.js +124 -0
  37. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  38. package/dist/apps/cli/src/install-selector.js +66 -0
  39. package/dist/apps/cli/src/interactive.d.ts +3 -0
  40. package/dist/apps/cli/src/interactive.js +32 -0
  41. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  42. package/dist/apps/cli/src/postinstall.js +42 -0
  43. package/dist/apps/cli/src/release-installer.d.ts +57 -0
  44. package/dist/apps/cli/src/release-installer.js +432 -0
  45. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  46. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  47. package/dist/apps/cli/src/slash.d.ts +15 -0
  48. package/dist/apps/cli/src/slash.js +229 -0
  49. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  50. package/dist/apps/cli/src/summary-model.js +136 -0
  51. package/dist/apps/cli/src/theme.d.ts +18 -0
  52. package/dist/apps/cli/src/theme.js +118 -0
  53. package/dist/apps/cli/src/version.d.ts +2 -0
  54. package/dist/apps/cli/src/version.js +2 -0
  55. package/dist/packages/config/src/index.d.ts +3 -0
  56. package/dist/packages/config/src/index.js +3 -0
  57. package/dist/packages/config/src/load.d.ts +3 -0
  58. package/dist/packages/config/src/load.js +80 -0
  59. package/dist/packages/config/src/schema.d.ts +49 -0
  60. package/dist/packages/config/src/schema.js +28 -0
  61. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  62. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  63. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  64. package/dist/packages/connectors/aws/src/index.js +67 -0
  65. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  66. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  67. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  68. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  69. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  70. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  71. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  72. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  73. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  74. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  75. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  76. package/dist/packages/connectors/mock/src/index.js +66 -0
  77. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  78. package/dist/packages/connectors/openai/src/index.js +169 -0
  79. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  80. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  81. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  82. package/dist/packages/connectors/supabase/src/client.js +132 -0
  83. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  84. package/dist/packages/connectors/supabase/src/index.js +87 -0
  85. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  86. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  87. package/dist/packages/core/src/collector.d.ts +12 -0
  88. package/dist/packages/core/src/collector.js +68 -0
  89. package/dist/packages/core/src/index.d.ts +5 -0
  90. package/dist/packages/core/src/index.js +4 -0
  91. package/dist/packages/core/src/provider.d.ts +18 -0
  92. package/dist/packages/core/src/provider.js +2 -0
  93. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  94. package/dist/packages/core/src/risk-engine.js +4 -0
  95. package/dist/packages/core/src/snapshots.d.ts +49 -0
  96. package/dist/packages/core/src/snapshots.js +9 -0
  97. package/dist/packages/db/src/client.d.ts +11 -0
  98. package/dist/packages/db/src/client.js +14 -0
  99. package/dist/packages/db/src/index.d.ts +6 -0
  100. package/dist/packages/db/src/index.js +6 -0
  101. package/dist/packages/db/src/local-store.d.ts +161 -0
  102. package/dist/packages/db/src/local-store.js +623 -0
  103. package/dist/packages/db/src/migrate.d.ts +17 -0
  104. package/dist/packages/db/src/migrate.js +35 -0
  105. package/dist/packages/db/src/schema.d.ts +5 -0
  106. package/dist/packages/db/src/schema.js +120 -0
  107. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  108. package/dist/packages/db/src/sqlite-bin.js +16 -0
  109. package/dist/packages/local-api/src/index.d.ts +2 -0
  110. package/dist/packages/local-api/src/index.js +2 -0
  111. package/dist/packages/local-api/src/server.d.ts +36 -0
  112. package/dist/packages/local-api/src/server.js +310 -0
  113. package/dist/packages/report/src/daily.d.ts +24 -0
  114. package/dist/packages/report/src/daily.js +9 -0
  115. package/dist/packages/report/src/index.d.ts +4 -0
  116. package/dist/packages/report/src/index.js +4 -0
  117. package/dist/packages/report/src/korean.d.ts +3 -0
  118. package/dist/packages/report/src/korean.js +62 -0
  119. package/dist/packages/report/src/slack.d.ts +34 -0
  120. package/dist/packages/report/src/slack.js +134 -0
  121. package/dist/packages/runtime/src/index.d.ts +2 -0
  122. package/dist/packages/runtime/src/index.js +2 -0
  123. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  124. package/dist/packages/runtime/src/runtime.js +182 -0
  125. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  126. package/dist/packages/view-model/src/hud-model.js +295 -0
  127. package/dist/packages/view-model/src/index.d.ts +6 -0
  128. package/dist/packages/view-model/src/index.js +6 -0
  129. package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
  130. package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
  131. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  132. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  133. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  134. package/dist/packages/view-model/src/sync-state.js +140 -0
  135. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  136. package/dist/packages/view-model/src/usage-progress.js +57 -0
  137. package/dist/packages/view-model/src/view-model.d.ts +215 -0
  138. package/dist/packages/view-model/src/view-model.js +826 -0
  139. package/package.json +40 -0
  140. package/scripts/postinstall.mjs +69 -0
@@ -0,0 +1,623 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
5
+ import { dirname } from "node:path";
6
+ import { runMigrations } from "./migrate.js";
7
+ import { resolveSqliteBin, SQLITE_BIN_ENV_KEY } from "./sqlite-bin.js";
8
+ const EMPTY_METADATA_JSON = "{}";
9
+ const requireNodeModule = createRequire(import.meta.url);
10
+ const FORBIDDEN_KEY_PATTERN = /^(raw|rawPayload|rawResponse|providerPayload|providerResponse|billingProfile)$/i;
11
+ const FORBIDDEN_STRING_PATTERN = /acct_|project_|invoice_|sk-|hooks\.slack|@/i;
12
+ export async function initializeLocalStore(options) {
13
+ const dbPath = normalizeDbPath(options.dbPath);
14
+ await mkdir(dirname(dbPath), { recursive: true });
15
+ return runMigrations({
16
+ async getAppliedMigrationIds() {
17
+ return getAppliedMigrationIds(dbPath);
18
+ },
19
+ async execute(sql) {
20
+ await executeSqlite(dbPath, sql);
21
+ },
22
+ async recordMigration(id) {
23
+ await executeSqlite(dbPath, `INSERT INTO schema_migrations (id) VALUES (${sqlString(id)}) ON CONFLICT(id) DO NOTHING;`);
24
+ },
25
+ });
26
+ }
27
+ export async function readLocalStore(options) {
28
+ const dbPath = normalizeDbPath(options.dbPath);
29
+ return {
30
+ appliedMigrationIds: getAppliedMigrationIds(dbPath),
31
+ providers: readProviders(dbPath),
32
+ usageSnapshots: readUsageSnapshots(dbPath),
33
+ billingSnapshots: readBillingSnapshots(dbPath),
34
+ serviceHealthSnapshots: readServiceHealthSnapshots(dbPath),
35
+ costEstimates: readCostEstimates(dbPath),
36
+ alerts: readAlerts(dbPath),
37
+ reportRuns: readReportRuns(dbPath),
38
+ };
39
+ }
40
+ export async function saveLocalProviderCollection(input) {
41
+ assertSafeForPersistence(input);
42
+ await initializeLocalStore({ dbPath: input.dbPath });
43
+ const dbPath = normalizeDbPath(input.dbPath);
44
+ const providerId = providerIdFor(input.provider.key);
45
+ const providerAccountRefs = collectProviderAccountRefs(input);
46
+ const statements = [
47
+ upsertProviderSql({
48
+ id: providerId,
49
+ key: input.provider.key,
50
+ displayName: input.provider.displayName,
51
+ connectorVersion: input.provider.connectorVersion,
52
+ timestamp: input.collectedAt,
53
+ }),
54
+ ];
55
+ if (input.status === "ok") {
56
+ statements.push(deleteProviderSyncAlertsSql(providerId));
57
+ }
58
+ for (const providerAccountRef of providerAccountRefs) {
59
+ statements.push(upsertProviderAccountSql(providerId, input.provider.key, providerAccountRef, input.collectedAt));
60
+ }
61
+ for (const snapshot of input.snapshots.usage) {
62
+ statements.push(insertUsageSnapshotSql(providerId, input.provider.key, snapshot));
63
+ }
64
+ for (const snapshot of input.snapshots.billing) {
65
+ statements.push(insertBillingSnapshotSql(providerId, input.provider.key, snapshot));
66
+ }
67
+ for (const snapshot of input.snapshots.serviceHealth) {
68
+ statements.push(insertServiceHealthSnapshotSql(providerId, snapshot));
69
+ }
70
+ for (const snapshot of input.snapshots.costEstimates) {
71
+ statements.push(insertCostEstimateSql(providerId, input.provider.key, snapshot));
72
+ }
73
+ for (const alert of input.alerts) {
74
+ statements.push(insertAlertSql(alert));
75
+ }
76
+ executeSqliteTransaction(dbPath, statements);
77
+ }
78
+ export async function recordLocalReportRun(input) {
79
+ assertSafeForPersistence(input);
80
+ await initializeLocalStore({ dbPath: input.dbPath });
81
+ executeSqliteTransaction(normalizeDbPath(input.dbPath), [
82
+ `
83
+ INSERT INTO report_runs (id, created_at, report_date, language, delivery_target, status, metadata_json)
84
+ VALUES (
85
+ ${sqlString(randomUUID())},
86
+ ${sqlString(input.createdAt)},
87
+ ${sqlString(input.reportDate)},
88
+ ${sqlString(input.language)},
89
+ ${sqlString(input.deliveryTarget)},
90
+ ${sqlString(input.status)},
91
+ ${sqlString(EMPTY_METADATA_JSON)}
92
+ );
93
+ `,
94
+ ]);
95
+ }
96
+ function getAppliedMigrationIds(dbPath) {
97
+ const schemaMigrationTables = querySqliteRowsSync(dbPath, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations';");
98
+ if (schemaMigrationTables.length === 0) {
99
+ return [];
100
+ }
101
+ return querySqliteRowsSync(dbPath, "SELECT id FROM schema_migrations ORDER BY id;").map((row) => row.id);
102
+ }
103
+ function readProviders(dbPath) {
104
+ return querySqliteRowsSync(dbPath, `
105
+ SELECT
106
+ id,
107
+ provider_key AS key,
108
+ display_name AS displayName,
109
+ connector_version AS connectorVersion,
110
+ created_at AS createdAt,
111
+ updated_at AS updatedAt
112
+ FROM providers
113
+ ORDER BY provider_key;
114
+ `);
115
+ }
116
+ function readUsageSnapshots(dbPath) {
117
+ return querySqliteRowsSync(dbPath, `
118
+ SELECT
119
+ usage_snapshots.id AS id,
120
+ providers.provider_key AS providerKey,
121
+ provider_accounts.account_ref AS providerAccountRef,
122
+ usage_snapshots.collected_at AS collectedAt,
123
+ usage_snapshots.service AS service,
124
+ usage_snapshots.metric AS metric,
125
+ usage_snapshots.unit AS unit,
126
+ usage_snapshots.value AS value,
127
+ usage_snapshots.metadata_json AS metadataJson
128
+ FROM usage_snapshots
129
+ JOIN providers ON providers.id = usage_snapshots.provider_id
130
+ LEFT JOIN provider_accounts ON provider_accounts.id = usage_snapshots.provider_account_id
131
+ ORDER BY usage_snapshots.collected_at, usage_snapshots.id;
132
+ `).map((row) => ({
133
+ id: row.id,
134
+ providerKey: row.providerKey,
135
+ collectedAt: row.collectedAt,
136
+ service: row.service,
137
+ metric: row.metric,
138
+ unit: row.unit,
139
+ value: row.value,
140
+ metadataJson: emptyMetadata(row.metadataJson),
141
+ ...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
142
+ }));
143
+ }
144
+ function readBillingSnapshots(dbPath) {
145
+ return querySqliteRowsSync(dbPath, `
146
+ SELECT
147
+ billing_snapshots.id AS id,
148
+ providers.provider_key AS providerKey,
149
+ provider_accounts.account_ref AS providerAccountRef,
150
+ billing_snapshots.collected_at AS collectedAt,
151
+ billing_snapshots.period_start AS periodStart,
152
+ billing_snapshots.period_end AS periodEnd,
153
+ billing_snapshots.amount_minor AS amountMinor,
154
+ billing_snapshots.currency AS currency,
155
+ billing_snapshots.status AS status,
156
+ billing_snapshots.metadata_json AS metadataJson
157
+ FROM billing_snapshots
158
+ JOIN providers ON providers.id = billing_snapshots.provider_id
159
+ LEFT JOIN provider_accounts ON provider_accounts.id = billing_snapshots.provider_account_id
160
+ WHERE NOT EXISTS (
161
+ SELECT 1
162
+ FROM billing_snapshots AS newer_billing_snapshot
163
+ WHERE newer_billing_snapshot.provider_id = billing_snapshots.provider_id
164
+ AND newer_billing_snapshot.provider_account_id IS billing_snapshots.provider_account_id
165
+ AND newer_billing_snapshot.period_start = billing_snapshots.period_start
166
+ AND newer_billing_snapshot.period_end = billing_snapshots.period_end
167
+ AND newer_billing_snapshot.currency = billing_snapshots.currency
168
+ AND (
169
+ newer_billing_snapshot.collected_at > billing_snapshots.collected_at
170
+ OR (
171
+ newer_billing_snapshot.collected_at = billing_snapshots.collected_at
172
+ AND newer_billing_snapshot.id > billing_snapshots.id
173
+ )
174
+ )
175
+ )
176
+ ORDER BY billing_snapshots.collected_at, billing_snapshots.id;
177
+ `).map((row) => ({
178
+ id: row.id,
179
+ providerKey: row.providerKey,
180
+ collectedAt: row.collectedAt,
181
+ periodStart: row.periodStart,
182
+ periodEnd: row.periodEnd,
183
+ amountMinor: row.amountMinor,
184
+ currency: row.currency,
185
+ status: row.status,
186
+ metadataJson: emptyMetadata(row.metadataJson),
187
+ ...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
188
+ }));
189
+ }
190
+ function readServiceHealthSnapshots(dbPath) {
191
+ return querySqliteRowsSync(dbPath, `
192
+ SELECT
193
+ service_health_snapshots.id AS id,
194
+ providers.provider_key AS providerKey,
195
+ service_health_snapshots.collected_at AS collectedAt,
196
+ service_health_snapshots.service AS service,
197
+ service_health_snapshots.region AS region,
198
+ service_health_snapshots.status AS status,
199
+ service_health_snapshots.message AS message,
200
+ service_health_snapshots.metadata_json AS metadataJson
201
+ FROM service_health_snapshots
202
+ JOIN providers ON providers.id = service_health_snapshots.provider_id
203
+ ORDER BY service_health_snapshots.collected_at, service_health_snapshots.id;
204
+ `).map((row) => ({
205
+ id: row.id,
206
+ providerKey: row.providerKey,
207
+ collectedAt: row.collectedAt,
208
+ service: row.service,
209
+ status: row.status,
210
+ metadataJson: emptyMetadata(row.metadataJson),
211
+ ...(row.region === null ? {} : { region: row.region }),
212
+ ...(row.message === null ? {} : { message: row.message }),
213
+ }));
214
+ }
215
+ function readCostEstimates(dbPath) {
216
+ return querySqliteRowsSync(dbPath, `
217
+ SELECT
218
+ cost_estimates.id AS id,
219
+ providers.provider_key AS providerKey,
220
+ provider_accounts.account_ref AS providerAccountRef,
221
+ cost_estimates.collected_at AS collectedAt,
222
+ cost_estimates.period_start AS periodStart,
223
+ cost_estimates.period_end AS periodEnd,
224
+ cost_estimates.estimated_amount_minor AS estimatedAmountMinor,
225
+ cost_estimates.currency AS currency,
226
+ cost_estimates.confidence AS confidence,
227
+ cost_estimates.metadata_json AS metadataJson
228
+ FROM cost_estimates
229
+ JOIN providers ON providers.id = cost_estimates.provider_id
230
+ LEFT JOIN provider_accounts ON provider_accounts.id = cost_estimates.provider_account_id
231
+ WHERE NOT EXISTS (
232
+ SELECT 1
233
+ FROM cost_estimates AS newer_cost_estimate
234
+ WHERE newer_cost_estimate.provider_id = cost_estimates.provider_id
235
+ AND newer_cost_estimate.provider_account_id IS cost_estimates.provider_account_id
236
+ AND newer_cost_estimate.period_start = cost_estimates.period_start
237
+ AND newer_cost_estimate.period_end = cost_estimates.period_end
238
+ AND newer_cost_estimate.currency = cost_estimates.currency
239
+ AND (
240
+ newer_cost_estimate.collected_at > cost_estimates.collected_at
241
+ OR (
242
+ newer_cost_estimate.collected_at = cost_estimates.collected_at
243
+ AND newer_cost_estimate.id > cost_estimates.id
244
+ )
245
+ )
246
+ )
247
+ ORDER BY cost_estimates.collected_at, cost_estimates.id;
248
+ `).map((row) => ({
249
+ id: row.id,
250
+ providerKey: row.providerKey,
251
+ collectedAt: row.collectedAt,
252
+ periodStart: row.periodStart,
253
+ periodEnd: row.periodEnd,
254
+ estimatedAmountMinor: row.estimatedAmountMinor,
255
+ currency: row.currency,
256
+ confidence: row.confidence,
257
+ metadataJson: emptyMetadata(row.metadataJson),
258
+ ...(row.providerAccountRef === null ? {} : { providerAccountRef: row.providerAccountRef }),
259
+ }));
260
+ }
261
+ function readAlerts(dbPath) {
262
+ return querySqliteRowsSync(dbPath, `
263
+ SELECT
264
+ alerts.id AS id,
265
+ providers.provider_key AS providerKey,
266
+ alerts.created_at AS createdAt,
267
+ alerts.severity AS severity,
268
+ alerts.category AS category,
269
+ alerts.title AS title,
270
+ alerts.message AS message,
271
+ alerts.metadata_json AS metadataJson
272
+ FROM alerts
273
+ LEFT JOIN providers ON providers.id = alerts.provider_id
274
+ ORDER BY alerts.created_at, alerts.id;
275
+ `).map((row) => ({
276
+ id: row.id,
277
+ createdAt: row.createdAt,
278
+ severity: row.severity,
279
+ category: row.category,
280
+ title: row.title,
281
+ message: row.message,
282
+ metadataJson: emptyMetadata(row.metadataJson),
283
+ ...(row.providerKey === null ? {} : { providerKey: row.providerKey }),
284
+ }));
285
+ }
286
+ function readReportRuns(dbPath) {
287
+ return querySqliteRowsSync(dbPath, `
288
+ SELECT
289
+ id,
290
+ created_at AS createdAt,
291
+ report_date AS reportDate,
292
+ language,
293
+ delivery_target AS deliveryTarget,
294
+ status,
295
+ metadata_json AS metadataJson
296
+ FROM report_runs
297
+ ORDER BY created_at, id;
298
+ `).map((row) => ({
299
+ id: row.id,
300
+ createdAt: row.createdAt,
301
+ reportDate: row.reportDate,
302
+ language: row.language,
303
+ deliveryTarget: row.deliveryTarget,
304
+ status: row.status,
305
+ metadataJson: emptyMetadata(row.metadataJson),
306
+ }));
307
+ }
308
+ function upsertProviderSql(input) {
309
+ return `
310
+ INSERT INTO providers (id, provider_key, display_name, connector_version, created_at, updated_at)
311
+ VALUES (
312
+ ${sqlString(input.id)},
313
+ ${sqlString(input.key)},
314
+ ${sqlString(input.displayName)},
315
+ ${sqlString(input.connectorVersion)},
316
+ ${sqlString(input.timestamp)},
317
+ ${sqlString(input.timestamp)}
318
+ )
319
+ ON CONFLICT(provider_key) DO UPDATE SET
320
+ display_name = excluded.display_name,
321
+ connector_version = excluded.connector_version,
322
+ updated_at = excluded.updated_at;
323
+ `;
324
+ }
325
+ function upsertProviderAccountSql(providerId, providerKey, providerAccountRef, timestamp) {
326
+ return `
327
+ INSERT INTO provider_accounts (id, provider_id, account_label, account_ref, created_at, updated_at)
328
+ VALUES (
329
+ ${sqlString(providerAccountIdFor(providerKey, providerAccountRef))},
330
+ ${sqlString(providerId)},
331
+ ${sqlString("redacted-provider-account")},
332
+ ${sqlString(providerAccountDigest(providerAccountRef))},
333
+ ${sqlString(timestamp)},
334
+ ${sqlString(timestamp)}
335
+ )
336
+ ON CONFLICT(provider_id, account_ref) DO UPDATE SET
337
+ updated_at = excluded.updated_at;
338
+ `;
339
+ }
340
+ function insertUsageSnapshotSql(providerId, providerKey, snapshot) {
341
+ return `
342
+ INSERT INTO usage_snapshots (
343
+ id, provider_id, provider_account_id, collected_at, service, metric, unit, value, metadata_json
344
+ )
345
+ VALUES (
346
+ ${sqlString(randomUUID())},
347
+ ${sqlString(providerId)},
348
+ ${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
349
+ ${sqlString(snapshot.collectedAt)},
350
+ ${sqlString(snapshot.service ?? "unknown")},
351
+ ${sqlString(snapshot.metric)},
352
+ ${sqlString(snapshot.unit)},
353
+ ${sqlNumber(snapshot.value)},
354
+ ${sqlString(EMPTY_METADATA_JSON)}
355
+ );
356
+ `;
357
+ }
358
+ function insertBillingSnapshotSql(providerId, providerKey, snapshot) {
359
+ return `
360
+ INSERT INTO billing_snapshots (
361
+ id, provider_id, provider_account_id, collected_at, period_start, period_end, amount_minor, currency, status,
362
+ metadata_json
363
+ )
364
+ VALUES (
365
+ ${sqlString(randomUUID())},
366
+ ${sqlString(providerId)},
367
+ ${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
368
+ ${sqlString(snapshot.collectedAt)},
369
+ ${sqlString(snapshot.periodStart)},
370
+ ${sqlString(snapshot.periodEnd)},
371
+ ${sqlInteger(snapshot.amountMinor)},
372
+ ${sqlString(snapshot.currency)},
373
+ ${sqlString(snapshot.status)},
374
+ ${sqlString(EMPTY_METADATA_JSON)}
375
+ );
376
+ `;
377
+ }
378
+ function insertServiceHealthSnapshotSql(providerId, snapshot) {
379
+ return `
380
+ INSERT INTO service_health_snapshots (
381
+ id, provider_id, collected_at, service, region, status, message, metadata_json
382
+ )
383
+ VALUES (
384
+ ${sqlString(randomUUID())},
385
+ ${sqlString(providerId)},
386
+ ${sqlString(snapshot.collectedAt)},
387
+ ${sqlString(snapshot.service)},
388
+ ${sqlNullableString(snapshot.region)},
389
+ ${sqlString(snapshot.status)},
390
+ ${sqlNullableString(snapshot.message)},
391
+ ${sqlString(EMPTY_METADATA_JSON)}
392
+ );
393
+ `;
394
+ }
395
+ function insertCostEstimateSql(providerId, providerKey, snapshot) {
396
+ return `
397
+ INSERT INTO cost_estimates (
398
+ id, provider_id, provider_account_id, collected_at, period_start, period_end, estimated_amount_minor, currency,
399
+ confidence, metadata_json
400
+ )
401
+ VALUES (
402
+ ${sqlString(randomUUID())},
403
+ ${sqlString(providerId)},
404
+ ${sqlProviderAccountId(providerKey, snapshot.providerAccountRef)},
405
+ ${sqlString(snapshot.collectedAt)},
406
+ ${sqlString(snapshot.periodStart)},
407
+ ${sqlString(snapshot.periodEnd)},
408
+ ${sqlInteger(snapshot.estimatedAmountMinor)},
409
+ ${sqlString(snapshot.currency)},
410
+ ${sqlString(snapshot.confidence)},
411
+ ${sqlString(EMPTY_METADATA_JSON)}
412
+ );
413
+ `;
414
+ }
415
+ function insertAlertSql(alert) {
416
+ return `
417
+ INSERT INTO alerts (id, provider_id, created_at, severity, category, title, message, metadata_json)
418
+ VALUES (
419
+ ${sqlString(randomUUID())},
420
+ ${alert.provider === undefined ? "NULL" : sqlString(providerIdFor(alert.provider))},
421
+ ${sqlString(alert.createdAt)},
422
+ ${sqlString(alert.severity)},
423
+ ${sqlString(alert.category)},
424
+ ${sqlString(alert.title)},
425
+ ${sqlString(alert.message)},
426
+ ${sqlString(EMPTY_METADATA_JSON)}
427
+ );
428
+ `;
429
+ }
430
+ function deleteProviderSyncAlertsSql(providerId) {
431
+ return `
432
+ DELETE FROM alerts
433
+ WHERE provider_id = ${sqlString(providerId)}
434
+ AND category = 'provider-sync';
435
+ `;
436
+ }
437
+ function collectProviderAccountRefs(input) {
438
+ const refs = new Set();
439
+ const collect = (providerAccountRef) => {
440
+ if (providerAccountRef !== undefined) {
441
+ refs.add(providerAccountRef);
442
+ }
443
+ };
444
+ for (const snapshot of input.snapshots.usage) {
445
+ collect(snapshot.providerAccountRef);
446
+ }
447
+ for (const snapshot of input.snapshots.billing) {
448
+ collect(snapshot.providerAccountRef);
449
+ }
450
+ for (const snapshot of input.snapshots.costEstimates) {
451
+ collect(snapshot.providerAccountRef);
452
+ }
453
+ return [...refs].sort();
454
+ }
455
+ function executeSqliteTransaction(dbPath, statements) {
456
+ if (statements.length === 0) {
457
+ return;
458
+ }
459
+ executeSqliteSync(dbPath, ["BEGIN;", ...statements, "COMMIT;"].join("\n"));
460
+ }
461
+ async function executeSqlite(dbPath, sql) {
462
+ try {
463
+ executeSqliteWithCli(dbPath, sql);
464
+ }
465
+ catch (caught) {
466
+ if (!shouldFallbackToNodeSqlite(caught)) {
467
+ throw caught;
468
+ }
469
+ executeSqliteWithNodeSync(dbPath, sql);
470
+ }
471
+ }
472
+ function executeSqliteSync(dbPath, sql) {
473
+ try {
474
+ executeSqliteWithCli(dbPath, sql);
475
+ }
476
+ catch (caught) {
477
+ if (!shouldFallbackToNodeSqlite(caught)) {
478
+ throw caught;
479
+ }
480
+ executeSqliteWithNodeSync(dbPath, sql);
481
+ }
482
+ }
483
+ function querySqliteRowsSync(dbPath, sql) {
484
+ try {
485
+ return querySqliteRowsWithCli(dbPath, sql);
486
+ }
487
+ catch (caught) {
488
+ if (!shouldFallbackToNodeSqlite(caught)) {
489
+ throw caught;
490
+ }
491
+ return querySqliteRowsWithNodeSync(dbPath, sql);
492
+ }
493
+ }
494
+ function executeSqliteWithCli(dbPath, sql) {
495
+ execFileSync(resolveSqliteBin(), [dbPath], {
496
+ input: sqliteInput(sql),
497
+ encoding: "utf8",
498
+ maxBuffer: 1024 * 1024,
499
+ });
500
+ }
501
+ function querySqliteRowsWithCli(dbPath, sql) {
502
+ const output = execFileSync(resolveSqliteBin(), ["-json", dbPath, sql], {
503
+ encoding: "utf8",
504
+ maxBuffer: 1024 * 1024,
505
+ }).trim();
506
+ return parseSqliteJsonRows(output);
507
+ }
508
+ function executeSqliteWithNodeSync(dbPath, sql) {
509
+ const database = createNodeSqliteDatabase(dbPath);
510
+ try {
511
+ database.exec(sqliteInput(sql));
512
+ }
513
+ finally {
514
+ database.close();
515
+ }
516
+ }
517
+ function querySqliteRowsWithNodeSync(dbPath, sql) {
518
+ const database = createNodeSqliteDatabase(dbPath);
519
+ try {
520
+ database.exec("PRAGMA foreign_keys = ON;");
521
+ return database.prepare(sql).all();
522
+ }
523
+ finally {
524
+ database.close();
525
+ }
526
+ }
527
+ function createNodeSqliteDatabase(dbPath) {
528
+ try {
529
+ const nodeSqlite = requireNodeModule("node:sqlite");
530
+ return new nodeSqlite.DatabaseSync(dbPath);
531
+ }
532
+ catch (caught) {
533
+ throw new Error(`SQLite is unavailable. Install sqlite3 and put it on PATH, set ${SQLITE_BIN_ENV_KEY}, or run MoneySiren with a Node.js version that includes node:sqlite. ${caught instanceof Error ? caught.message : "node:sqlite could not be loaded."}`);
534
+ }
535
+ }
536
+ function parseSqliteJsonRows(output) {
537
+ if (output.length === 0) {
538
+ return [];
539
+ }
540
+ return JSON.parse(output);
541
+ }
542
+ function sqliteInput(sql) {
543
+ return `PRAGMA foreign_keys = ON;\n${sql.trim()}\n`;
544
+ }
545
+ function shouldFallbackToNodeSqlite(caught) {
546
+ if (process.env[SQLITE_BIN_ENV_KEY]?.trim()) {
547
+ return false;
548
+ }
549
+ const error = caught;
550
+ const message = typeof error.message === "string" ? error.message : "";
551
+ return error.code === "ENOENT" || /spawnSync .*ENOENT/i.test(message);
552
+ }
553
+ function normalizeDbPath(dbPath) {
554
+ const normalized = dbPath.trim();
555
+ if (normalized.length === 0) {
556
+ throw new Error("dbPath must not be blank.");
557
+ }
558
+ return normalized;
559
+ }
560
+ function providerIdFor(providerKey) {
561
+ return `provider:${providerKey}`;
562
+ }
563
+ function providerAccountIdFor(providerKey, providerAccountRef) {
564
+ return `provider-account:${providerKey}:${providerAccountDigest(providerAccountRef).slice(0, 32)}`;
565
+ }
566
+ function providerAccountDigest(providerAccountRef) {
567
+ return createHash("sha256").update(providerAccountRef).digest("hex");
568
+ }
569
+ function sqlProviderAccountId(providerKey, providerAccountRef) {
570
+ if (providerAccountRef === undefined) {
571
+ return "NULL";
572
+ }
573
+ return sqlString(providerAccountIdFor(providerKey, providerAccountRef));
574
+ }
575
+ function sqlString(value) {
576
+ return `'${value.replaceAll("'", "''")}'`;
577
+ }
578
+ function sqlNullableString(value) {
579
+ return value === undefined ? "NULL" : sqlString(value);
580
+ }
581
+ function sqlNumber(value) {
582
+ if (!Number.isFinite(value)) {
583
+ throw new Error("SQLite numeric value must be finite.");
584
+ }
585
+ return String(value);
586
+ }
587
+ function sqlInteger(value) {
588
+ if (!Number.isSafeInteger(value)) {
589
+ throw new Error("SQLite integer value must be a safe integer.");
590
+ }
591
+ return String(value);
592
+ }
593
+ function emptyMetadata(metadataJson) {
594
+ if (metadataJson !== EMPTY_METADATA_JSON) {
595
+ const parsed = JSON.parse(metadataJson);
596
+ assertSafeForPersistence(parsed);
597
+ }
598
+ return {};
599
+ }
600
+ function assertSafeForPersistence(value) {
601
+ if (Array.isArray(value)) {
602
+ for (const item of value) {
603
+ assertSafeForPersistence(item);
604
+ }
605
+ return;
606
+ }
607
+ if (!isRecord(value)) {
608
+ if (typeof value === "string" && FORBIDDEN_STRING_PATTERN.test(value)) {
609
+ throw new Error("Sensitive provider value cannot be persisted.");
610
+ }
611
+ return;
612
+ }
613
+ for (const [key, nestedValue] of Object.entries(value)) {
614
+ if (FORBIDDEN_KEY_PATTERN.test(key)) {
615
+ throw new Error(`Raw provider payload field cannot be persisted: ${key}`);
616
+ }
617
+ assertSafeForPersistence(nestedValue);
618
+ }
619
+ }
620
+ function isRecord(value) {
621
+ return typeof value === "object" && value !== null;
622
+ }
623
+ //# sourceMappingURL=local-store.js.map
@@ -0,0 +1,17 @@
1
+ export interface Migration {
2
+ id: string;
3
+ sql: string;
4
+ }
5
+ export interface MigrationExecutor {
6
+ getAppliedMigrationIds(): Promise<readonly string[]>;
7
+ execute(sql: string): Promise<void>;
8
+ recordMigration(id: string): Promise<void>;
9
+ }
10
+ export interface MigrationRunResult {
11
+ appliedMigrationIds: readonly string[];
12
+ skippedMigrationIds: readonly string[];
13
+ }
14
+ export declare const MIGRATIONS: readonly Migration[];
15
+ export declare function getPendingMigrations(appliedMigrationIds: readonly string[], migrations?: readonly Migration[]): readonly Migration[];
16
+ export declare function runMigrations(executor: MigrationExecutor, migrations?: readonly Migration[]): Promise<MigrationRunResult>;
17
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { INITIAL_SCHEMA_SQL, READ_MODEL_INDEX_SQL } from "./schema.js";
2
+ export const MIGRATIONS = [
3
+ {
4
+ id: "0001_init",
5
+ sql: INITIAL_SCHEMA_SQL,
6
+ },
7
+ {
8
+ id: "0002_read_model_indexes",
9
+ sql: READ_MODEL_INDEX_SQL,
10
+ },
11
+ ];
12
+ export function getPendingMigrations(appliedMigrationIds, migrations = MIGRATIONS) {
13
+ const applied = new Set(appliedMigrationIds);
14
+ return migrations.filter((migration) => !applied.has(migration.id));
15
+ }
16
+ export async function runMigrations(executor, migrations = MIGRATIONS) {
17
+ const appliedMigrationIds = await executor.getAppliedMigrationIds();
18
+ const applied = new Set(appliedMigrationIds);
19
+ const pendingMigrations = getPendingMigrations(appliedMigrationIds, migrations);
20
+ const newlyAppliedMigrationIds = [];
21
+ for (const migration of pendingMigrations) {
22
+ await executor.execute(migration.sql);
23
+ await executor.recordMigration(migration.id);
24
+ newlyAppliedMigrationIds.push(migration.id);
25
+ applied.add(migration.id);
26
+ }
27
+ return {
28
+ appliedMigrationIds: newlyAppliedMigrationIds,
29
+ skippedMigrationIds: migrations
30
+ .filter((migration) => !newlyAppliedMigrationIds.includes(migration.id))
31
+ .filter((migration) => applied.has(migration.id))
32
+ .map((migration) => migration.id),
33
+ };
34
+ }
35
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1,5 @@
1
+ export declare const REQUIRED_TABLES: readonly ["providers", "provider_accounts", "usage_snapshots", "billing_snapshots", "service_health_snapshots", "cost_estimates", "alerts", "report_runs"];
2
+ export type RequiredTable = (typeof REQUIRED_TABLES)[number];
3
+ export declare const INITIAL_SCHEMA_SQL: string;
4
+ export declare const READ_MODEL_INDEX_SQL: string;
5
+ //# sourceMappingURL=schema.d.ts.map