@moneysiren/app 0.1.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/apps/cli/src/cli.d.ts +59 -0
- package/dist/apps/cli/src/cli.js +199 -0
- package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
- package/dist/apps/cli/src/commands/dashboard.js +239 -0
- package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
- package/dist/apps/cli/src/commands/doctor.js +25 -0
- package/dist/apps/cli/src/commands/init.d.ts +3 -0
- package/dist/apps/cli/src/commands/init.js +18 -0
- package/dist/apps/cli/src/commands/install.d.ts +3 -0
- package/dist/apps/cli/src/commands/install.js +244 -0
- package/dist/apps/cli/src/commands/modes.d.ts +3 -0
- package/dist/apps/cli/src/commands/modes.js +73 -0
- package/dist/apps/cli/src/commands/notify.d.ts +3 -0
- package/dist/apps/cli/src/commands/notify.js +430 -0
- package/dist/apps/cli/src/commands/report.d.ts +3 -0
- package/dist/apps/cli/src/commands/report.js +206 -0
- package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
- package/dist/apps/cli/src/commands/runtime.js +499 -0
- package/dist/apps/cli/src/commands/shared.d.ts +9 -0
- package/dist/apps/cli/src/commands/shared.js +29 -0
- package/dist/apps/cli/src/commands/summary.d.ts +3 -0
- package/dist/apps/cli/src/commands/summary.js +15 -0
- package/dist/apps/cli/src/commands/sync.d.ts +3 -0
- package/dist/apps/cli/src/commands/sync.js +393 -0
- package/dist/apps/cli/src/commands/theme.d.ts +3 -0
- package/dist/apps/cli/src/commands/theme.js +181 -0
- package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
- package/dist/apps/cli/src/desktop-runtime.js +720 -0
- package/dist/apps/cli/src/home.d.ts +7 -0
- package/dist/apps/cli/src/home.js +124 -0
- package/dist/apps/cli/src/index.d.ts +3 -0
- package/dist/apps/cli/src/index.js +14 -0
- package/dist/apps/cli/src/install-profile.d.ts +35 -0
- package/dist/apps/cli/src/install-profile.js +124 -0
- package/dist/apps/cli/src/install-selector.d.ts +10 -0
- package/dist/apps/cli/src/install-selector.js +66 -0
- package/dist/apps/cli/src/interactive.d.ts +3 -0
- package/dist/apps/cli/src/interactive.js +32 -0
- package/dist/apps/cli/src/postinstall.d.ts +3 -0
- package/dist/apps/cli/src/postinstall.js +42 -0
- package/dist/apps/cli/src/release-installer.d.ts +57 -0
- package/dist/apps/cli/src/release-installer.js +432 -0
- package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
- package/dist/apps/cli/src/runtime-adapter.js +185 -0
- package/dist/apps/cli/src/slash.d.ts +15 -0
- package/dist/apps/cli/src/slash.js +229 -0
- package/dist/apps/cli/src/summary-model.d.ts +51 -0
- package/dist/apps/cli/src/summary-model.js +136 -0
- package/dist/apps/cli/src/theme.d.ts +18 -0
- package/dist/apps/cli/src/theme.js +118 -0
- package/dist/apps/cli/src/version.d.ts +2 -0
- package/dist/apps/cli/src/version.js +2 -0
- package/dist/packages/config/src/index.d.ts +3 -0
- package/dist/packages/config/src/index.js +3 -0
- package/dist/packages/config/src/load.d.ts +3 -0
- package/dist/packages/config/src/load.js +80 -0
- package/dist/packages/config/src/schema.d.ts +49 -0
- package/dist/packages/config/src/schema.js +28 -0
- package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
- package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
- package/dist/packages/connectors/aws/src/index.d.ts +35 -0
- package/dist/packages/connectors/aws/src/index.js +67 -0
- package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
- package/dist/packages/connectors/aws/src/normalize.js +141 -0
- package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
- package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
- package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
- package/dist/packages/connectors/cloudflare/src/client.js +107 -0
- package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
- package/dist/packages/connectors/cloudflare/src/index.js +81 -0
- package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
- package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
- package/dist/packages/connectors/mock/src/index.d.ts +58 -0
- package/dist/packages/connectors/mock/src/index.js +66 -0
- package/dist/packages/connectors/openai/src/index.d.ts +55 -0
- package/dist/packages/connectors/openai/src/index.js +169 -0
- package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
- package/dist/packages/connectors/openai/src/normalize.js +180 -0
- package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
- package/dist/packages/connectors/supabase/src/client.js +132 -0
- package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
- package/dist/packages/connectors/supabase/src/index.js +87 -0
- package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
- package/dist/packages/connectors/supabase/src/normalize.js +266 -0
- package/dist/packages/core/src/collector.d.ts +12 -0
- package/dist/packages/core/src/collector.js +68 -0
- package/dist/packages/core/src/index.d.ts +5 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/provider.d.ts +18 -0
- package/dist/packages/core/src/provider.js +2 -0
- package/dist/packages/core/src/risk-engine.d.ts +9 -0
- package/dist/packages/core/src/risk-engine.js +4 -0
- package/dist/packages/core/src/snapshots.d.ts +49 -0
- package/dist/packages/core/src/snapshots.js +9 -0
- package/dist/packages/db/src/client.d.ts +11 -0
- package/dist/packages/db/src/client.js +14 -0
- package/dist/packages/db/src/index.d.ts +6 -0
- package/dist/packages/db/src/index.js +6 -0
- package/dist/packages/db/src/local-store.d.ts +161 -0
- package/dist/packages/db/src/local-store.js +623 -0
- package/dist/packages/db/src/migrate.d.ts +17 -0
- package/dist/packages/db/src/migrate.js +35 -0
- package/dist/packages/db/src/schema.d.ts +5 -0
- package/dist/packages/db/src/schema.js +120 -0
- package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
- package/dist/packages/db/src/sqlite-bin.js +16 -0
- package/dist/packages/local-api/src/index.d.ts +2 -0
- package/dist/packages/local-api/src/index.js +2 -0
- package/dist/packages/local-api/src/server.d.ts +36 -0
- package/dist/packages/local-api/src/server.js +310 -0
- package/dist/packages/report/src/daily.d.ts +24 -0
- package/dist/packages/report/src/daily.js +9 -0
- package/dist/packages/report/src/index.d.ts +4 -0
- package/dist/packages/report/src/index.js +4 -0
- package/dist/packages/report/src/korean.d.ts +3 -0
- package/dist/packages/report/src/korean.js +62 -0
- package/dist/packages/report/src/slack.d.ts +34 -0
- package/dist/packages/report/src/slack.js +134 -0
- package/dist/packages/runtime/src/index.d.ts +2 -0
- package/dist/packages/runtime/src/index.js +2 -0
- package/dist/packages/runtime/src/runtime.d.ts +26 -0
- package/dist/packages/runtime/src/runtime.js +182 -0
- package/dist/packages/view-model/src/hud-model.d.ts +74 -0
- package/dist/packages/view-model/src/hud-model.js +295 -0
- package/dist/packages/view-model/src/index.d.ts +6 -0
- package/dist/packages/view-model/src/index.js +6 -0
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
- package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
- package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
- package/dist/packages/view-model/src/notification-preferences.js +36 -0
- package/dist/packages/view-model/src/sync-state.d.ts +47 -0
- package/dist/packages/view-model/src/sync-state.js +140 -0
- package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
- package/dist/packages/view-model/src/usage-progress.js +57 -0
- package/dist/packages/view-model/src/view-model.d.ts +215 -0
- package/dist/packages/view-model/src/view-model.js +826 -0
- package/package.json +40 -0
- package/scripts/postinstall.mjs +69 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const REQUIRED_TABLES = [
|
|
2
|
+
"providers",
|
|
3
|
+
"provider_accounts",
|
|
4
|
+
"usage_snapshots",
|
|
5
|
+
"billing_snapshots",
|
|
6
|
+
"service_health_snapshots",
|
|
7
|
+
"cost_estimates",
|
|
8
|
+
"alerts",
|
|
9
|
+
"report_runs",
|
|
10
|
+
];
|
|
11
|
+
export const INITIAL_SCHEMA_SQL = `
|
|
12
|
+
PRAGMA foreign_keys = ON;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
provider_key TEXT NOT NULL UNIQUE,
|
|
22
|
+
display_name TEXT NOT NULL,
|
|
23
|
+
connector_version TEXT NOT NULL,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS provider_accounts (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
|
|
31
|
+
account_label TEXT NOT NULL,
|
|
32
|
+
account_ref TEXT NOT NULL,
|
|
33
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
UNIQUE(provider_id, account_ref)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
|
|
41
|
+
provider_account_id TEXT REFERENCES provider_accounts(id) ON DELETE SET NULL,
|
|
42
|
+
collected_at TEXT NOT NULL,
|
|
43
|
+
service TEXT NOT NULL,
|
|
44
|
+
metric TEXT NOT NULL,
|
|
45
|
+
unit TEXT NOT NULL,
|
|
46
|
+
value REAL NOT NULL,
|
|
47
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS billing_snapshots (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
|
|
53
|
+
provider_account_id TEXT REFERENCES provider_accounts(id) ON DELETE SET NULL,
|
|
54
|
+
collected_at TEXT NOT NULL,
|
|
55
|
+
period_start TEXT NOT NULL,
|
|
56
|
+
period_end TEXT NOT NULL,
|
|
57
|
+
amount_minor INTEGER NOT NULL,
|
|
58
|
+
currency TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL,
|
|
60
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS service_health_snapshots (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
|
|
66
|
+
collected_at TEXT NOT NULL,
|
|
67
|
+
service TEXT NOT NULL,
|
|
68
|
+
region TEXT,
|
|
69
|
+
status TEXT NOT NULL,
|
|
70
|
+
message TEXT,
|
|
71
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS cost_estimates (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
provider_id TEXT NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
|
|
77
|
+
provider_account_id TEXT REFERENCES provider_accounts(id) ON DELETE SET NULL,
|
|
78
|
+
collected_at TEXT NOT NULL,
|
|
79
|
+
period_start TEXT NOT NULL,
|
|
80
|
+
period_end TEXT NOT NULL,
|
|
81
|
+
estimated_amount_minor INTEGER NOT NULL,
|
|
82
|
+
currency TEXT NOT NULL,
|
|
83
|
+
confidence TEXT NOT NULL,
|
|
84
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
provider_id TEXT REFERENCES providers(id) ON DELETE SET NULL,
|
|
90
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
severity TEXT NOT NULL,
|
|
92
|
+
category TEXT NOT NULL,
|
|
93
|
+
title TEXT NOT NULL,
|
|
94
|
+
message TEXT NOT NULL,
|
|
95
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS report_runs (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
101
|
+
report_date TEXT NOT NULL,
|
|
102
|
+
language TEXT NOT NULL,
|
|
103
|
+
delivery_target TEXT NOT NULL,
|
|
104
|
+
status TEXT NOT NULL,
|
|
105
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_collected_at ON usage_snapshots(collected_at);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_billing_snapshots_period ON billing_snapshots(period_start, period_end);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_service_health_snapshots_collected_at ON service_health_snapshots(collected_at);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_cost_estimates_period ON cost_estimates(period_start, period_end);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_alerts_created_at ON alerts(created_at);
|
|
113
|
+
`.trim();
|
|
114
|
+
export const READ_MODEL_INDEX_SQL = `
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_billing_snapshots_latest_logical
|
|
116
|
+
ON billing_snapshots(provider_id, provider_account_id, period_start, period_end, currency, collected_at, id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_cost_estimates_latest_logical
|
|
118
|
+
ON cost_estimates(provider_id, provider_account_id, period_start, period_end, currency, collected_at, id);
|
|
119
|
+
`.trim();
|
|
120
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
export const SQLITE_BIN_ENV_KEY = "MONEYSIREN_SQLITE_BIN";
|
|
3
|
+
const DEFAULT_POSIX_SQLITE_BIN = "/usr/bin/sqlite3";
|
|
4
|
+
const PATH_SQLITE_BIN = "sqlite3";
|
|
5
|
+
const WINDOWS_PATH_SQLITE_BIN = "sqlite3.exe";
|
|
6
|
+
export function resolveSqliteBin(env = process.env) {
|
|
7
|
+
const configuredBin = env[SQLITE_BIN_ENV_KEY]?.trim();
|
|
8
|
+
if (configuredBin !== undefined && configuredBin.length > 0) {
|
|
9
|
+
return configuredBin;
|
|
10
|
+
}
|
|
11
|
+
if (existsSync(DEFAULT_POSIX_SQLITE_BIN)) {
|
|
12
|
+
return DEFAULT_POSIX_SQLITE_BIN;
|
|
13
|
+
}
|
|
14
|
+
return process.platform === "win32" ? WINDOWS_PATH_SQLITE_BIN : PATH_SQLITE_BIN;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=sqlite-bin.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { type NotificationPreferences, type ReadTrayMenuModelOptions } from "../../view-model/src/index.js";
|
|
3
|
+
import { type LocalRuntime, type RuntimeLockOptions } from "../../runtime/src/index.js";
|
|
4
|
+
export interface LocalApiReaders {
|
|
5
|
+
readSummary: () => Promise<unknown>;
|
|
6
|
+
readTodayLiveView: () => Promise<unknown>;
|
|
7
|
+
readNotificationDigest: () => Promise<unknown>;
|
|
8
|
+
readTrayMenuModel: () => Promise<unknown>;
|
|
9
|
+
readNotificationPreferences: () => Promise<NotificationPreferences>;
|
|
10
|
+
writeNotificationPreferences: (preferences: NotificationPreferences) => Promise<NotificationPreferences>;
|
|
11
|
+
}
|
|
12
|
+
export interface LocalApiServerOptions {
|
|
13
|
+
host?: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
version?: string;
|
|
17
|
+
viewModel?: ReadTrayMenuModelOptions;
|
|
18
|
+
readers?: Partial<LocalApiReaders>;
|
|
19
|
+
runtimeLock?: false | RuntimeLockOptions;
|
|
20
|
+
preferences?: {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
env?: Record<string, string | undefined>;
|
|
23
|
+
path?: string;
|
|
24
|
+
};
|
|
25
|
+
localSessionToken?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface LocalApiServer {
|
|
28
|
+
server: Server;
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
runtime: LocalRuntime;
|
|
33
|
+
close: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare function startLocalApiServer(options?: LocalApiServerOptions): Promise<LocalApiServer>;
|
|
36
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { parseNotificationPreferences, readNotificationDigest, readNotificationPreferencesFile, readOperationsOverview, readTodayLiveView, readTrayMenuModel, writeNotificationPreferencesFile, } from "../../view-model/src/index.js";
|
|
3
|
+
import { assertLoopbackHost, isLoopbackHost, removeRuntimeLock, writeRuntimeLock, } from "../../runtime/src/index.js";
|
|
4
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
5
|
+
const DEFAULT_PORT = 47831;
|
|
6
|
+
const DEFAULT_VERSION = "0.1.0-alpha.9";
|
|
7
|
+
export async function startLocalApiServer(options = {}) {
|
|
8
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
9
|
+
const requestedPort = options.port ?? DEFAULT_PORT;
|
|
10
|
+
const version = options.version ?? DEFAULT_VERSION;
|
|
11
|
+
const now = options.now ?? (() => new Date());
|
|
12
|
+
assertLoopbackHost(host);
|
|
13
|
+
let runtime = {
|
|
14
|
+
pid: process.pid,
|
|
15
|
+
port: requestedPort,
|
|
16
|
+
baseUrl: `http://${host}:${requestedPort}`,
|
|
17
|
+
startedAt: now().toISOString(),
|
|
18
|
+
version,
|
|
19
|
+
};
|
|
20
|
+
const readers = createReaders(options, now);
|
|
21
|
+
const server = createServer((request, response) => {
|
|
22
|
+
void handleRequest(request, response, {
|
|
23
|
+
readers,
|
|
24
|
+
runtime: () => runtime,
|
|
25
|
+
localSessionToken: options.localSessionToken,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
const port = await listenWithPortFallback(server, requestedPort, host);
|
|
29
|
+
const baseUrl = `http://${host}:${port}`;
|
|
30
|
+
runtime = {
|
|
31
|
+
...runtime,
|
|
32
|
+
port,
|
|
33
|
+
baseUrl,
|
|
34
|
+
};
|
|
35
|
+
if (options.runtimeLock !== false) {
|
|
36
|
+
try {
|
|
37
|
+
await writeRuntimeLock(runtime, options.runtimeLock ?? {});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await closeServer(server);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
server,
|
|
46
|
+
host,
|
|
47
|
+
port,
|
|
48
|
+
baseUrl,
|
|
49
|
+
runtime,
|
|
50
|
+
close: async () => {
|
|
51
|
+
await closeServer(server);
|
|
52
|
+
if (options.runtimeLock !== false) {
|
|
53
|
+
await removeRuntimeLock(options.runtimeLock ?? {});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function handleRequest(request, response, context) {
|
|
59
|
+
if (!applyLocalCors(request, response) || !assertLocalHostRequest(request, response)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (request.method === "OPTIONS") {
|
|
63
|
+
response.writeHead(204);
|
|
64
|
+
response.end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const url = requestUrl(request);
|
|
68
|
+
if (request.method === "PUT" && url.pathname === "/api/local/notification-preferences") {
|
|
69
|
+
if (!hasValidLocalSessionToken(request, context.localSessionToken)) {
|
|
70
|
+
sendJson(response, 401, localError("local_session_required", "A local session token is required for write endpoints."));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const preferences = parseNotificationPreferences(await readJsonBody(request));
|
|
75
|
+
sendJson(response, 200, {
|
|
76
|
+
generatedAt: new Date().toISOString(),
|
|
77
|
+
localOnly: true,
|
|
78
|
+
secretsReturned: false,
|
|
79
|
+
preferences: await context.readers.writeNotificationPreferences(preferences),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
sendJson(response, 400, localError("invalid_preferences", error instanceof Error ? error.message : "Invalid notification preferences."));
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (request.method !== "GET") {
|
|
88
|
+
sendJson(response, 405, localError("method_not_allowed", "Only GET requests are supported for this endpoint."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (url.pathname === "/api/local/health") {
|
|
92
|
+
sendJson(response, 200, {
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
localOnly: true,
|
|
95
|
+
secretsReturned: false,
|
|
96
|
+
status: "ok",
|
|
97
|
+
loopbackOnly: true,
|
|
98
|
+
runtime: context.runtime(),
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (url.pathname === "/api/local/runtime") {
|
|
103
|
+
sendJson(response, 200, {
|
|
104
|
+
generatedAt: new Date().toISOString(),
|
|
105
|
+
localOnly: true,
|
|
106
|
+
secretsReturned: false,
|
|
107
|
+
runtime: context.runtime(),
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (url.pathname === "/api/local/summary") {
|
|
112
|
+
sendJson(response, 200, await context.readers.readSummary());
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (url.pathname === "/api/local/today-live") {
|
|
116
|
+
sendJson(response, 200, await context.readers.readTodayLiveView());
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (url.pathname === "/api/local/notification-digest") {
|
|
120
|
+
sendJson(response, 200, await context.readers.readNotificationDigest());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (url.pathname === "/api/local/tray-menu") {
|
|
124
|
+
sendJson(response, 200, await context.readers.readTrayMenuModel());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (url.pathname === "/api/local/notification-preferences") {
|
|
128
|
+
sendJson(response, 200, {
|
|
129
|
+
generatedAt: new Date().toISOString(),
|
|
130
|
+
localOnly: true,
|
|
131
|
+
secretsReturned: false,
|
|
132
|
+
preferences: await context.readers.readNotificationPreferences(),
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
sendJson(response, 404, localError("not_found", "Endpoint not found."));
|
|
137
|
+
}
|
|
138
|
+
function createReaders(options, now) {
|
|
139
|
+
const viewModel = options.now === undefined
|
|
140
|
+
? options.viewModel ?? {}
|
|
141
|
+
: {
|
|
142
|
+
...(options.viewModel ?? {}),
|
|
143
|
+
now,
|
|
144
|
+
};
|
|
145
|
+
const preferenceOptions = notificationPreferenceFileOptions(options);
|
|
146
|
+
const readNotificationPreferences = options.readers?.readNotificationPreferences ??
|
|
147
|
+
(() => readNotificationPreferencesFile(preferenceOptions));
|
|
148
|
+
return {
|
|
149
|
+
readSummary: options.readers?.readSummary ?? (() => readOperationsOverview(viewModel)),
|
|
150
|
+
readTodayLiveView: options.readers?.readTodayLiveView ?? (() => readTodayLiveView(viewModel)),
|
|
151
|
+
readNotificationDigest: options.readers?.readNotificationDigest ?? (async () => readNotificationDigest({
|
|
152
|
+
...viewModel,
|
|
153
|
+
notificationPreferences: await readNotificationPreferences(),
|
|
154
|
+
})),
|
|
155
|
+
readTrayMenuModel: options.readers?.readTrayMenuModel ?? (async () => readTrayMenuModel({
|
|
156
|
+
...viewModel,
|
|
157
|
+
notificationPreferences: await readNotificationPreferences(),
|
|
158
|
+
})),
|
|
159
|
+
readNotificationPreferences,
|
|
160
|
+
writeNotificationPreferences: options.readers?.writeNotificationPreferences ??
|
|
161
|
+
((preferences) => writeNotificationPreferencesFile(preferences, preferenceOptions)),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function notificationPreferenceFileOptions(options) {
|
|
165
|
+
const runtimeLock = options.runtimeLock === false ? undefined : options.runtimeLock;
|
|
166
|
+
const fileOptions = {};
|
|
167
|
+
const cwd = options.preferences?.cwd ?? runtimeLock?.cwd;
|
|
168
|
+
const env = options.preferences?.env ?? runtimeLock?.env;
|
|
169
|
+
const path = options.preferences?.path;
|
|
170
|
+
if (cwd !== undefined) {
|
|
171
|
+
fileOptions.cwd = cwd;
|
|
172
|
+
}
|
|
173
|
+
if (env !== undefined) {
|
|
174
|
+
fileOptions.env = env;
|
|
175
|
+
}
|
|
176
|
+
if (path !== undefined) {
|
|
177
|
+
fileOptions.path = path;
|
|
178
|
+
}
|
|
179
|
+
return fileOptions;
|
|
180
|
+
}
|
|
181
|
+
function applyLocalCors(request, response) {
|
|
182
|
+
const origin = request.headers.origin;
|
|
183
|
+
if (origin === undefined) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (!isLoopbackOrigin(origin)) {
|
|
187
|
+
sendJson(response, 403, localError("forbidden_origin", "Only loopback browser origins are allowed."));
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
191
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS");
|
|
192
|
+
response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-MoneySiren-Local-Session");
|
|
193
|
+
response.setHeader("Vary", "Origin");
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
function hasValidLocalSessionToken(request, expectedToken) {
|
|
197
|
+
if (expectedToken === undefined || expectedToken.trim().length === 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return request.headers["x-moneysiren-local-session"] === expectedToken;
|
|
201
|
+
}
|
|
202
|
+
async function readJsonBody(request) {
|
|
203
|
+
let body = "";
|
|
204
|
+
for await (const chunk of request) {
|
|
205
|
+
body += String(chunk);
|
|
206
|
+
if (body.length > 64_000) {
|
|
207
|
+
throw new Error("Local API request body is too large.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return body.trim().length === 0 ? {} : JSON.parse(body);
|
|
211
|
+
}
|
|
212
|
+
function assertLocalHostRequest(request, response) {
|
|
213
|
+
const host = request.headers.host;
|
|
214
|
+
if (host === undefined || isLoopbackHost(hostnameFromHeader(host))) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
sendJson(response, 403, localError("forbidden_host", "Only loopback host headers are allowed."));
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
function requestUrl(request) {
|
|
221
|
+
return new URL(request.url ?? "/", `http://${request.headers.host ?? DEFAULT_HOST}`);
|
|
222
|
+
}
|
|
223
|
+
function sendJson(response, status, payload) {
|
|
224
|
+
response.statusCode = status;
|
|
225
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
226
|
+
response.setHeader("Cache-Control", "no-store");
|
|
227
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
228
|
+
response.end(`${JSON.stringify(payload)}\n`);
|
|
229
|
+
}
|
|
230
|
+
function localError(code, message) {
|
|
231
|
+
return {
|
|
232
|
+
generatedAt: new Date().toISOString(),
|
|
233
|
+
localOnly: true,
|
|
234
|
+
secretsReturned: false,
|
|
235
|
+
error: {
|
|
236
|
+
code,
|
|
237
|
+
message,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function isLoopbackOrigin(origin) {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = new URL(origin);
|
|
244
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function hostnameFromHeader(host) {
|
|
251
|
+
if (host.startsWith("[")) {
|
|
252
|
+
return host.slice(1, host.indexOf("]"));
|
|
253
|
+
}
|
|
254
|
+
return host.split(":")[0] ?? host;
|
|
255
|
+
}
|
|
256
|
+
async function listenWithPortFallback(server, port, host) {
|
|
257
|
+
if (port === 0) {
|
|
258
|
+
await listen(server, port, host);
|
|
259
|
+
return listenedPort(server, port);
|
|
260
|
+
}
|
|
261
|
+
const maxAttempts = 20;
|
|
262
|
+
let nextPort = port;
|
|
263
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
264
|
+
try {
|
|
265
|
+
await listen(server, nextPort, host);
|
|
266
|
+
return listenedPort(server, nextPort);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!isAddressInUseError(error)) {
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
nextPort += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`No available MoneySiren local API port near ${port}.`);
|
|
276
|
+
}
|
|
277
|
+
function listen(server, port, host) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const onError = (error) => {
|
|
280
|
+
server.off("listening", onListening);
|
|
281
|
+
reject(error);
|
|
282
|
+
};
|
|
283
|
+
const onListening = () => {
|
|
284
|
+
server.off("error", onError);
|
|
285
|
+
resolve();
|
|
286
|
+
};
|
|
287
|
+
server.once("error", onError);
|
|
288
|
+
server.once("listening", onListening);
|
|
289
|
+
server.listen(port, host);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function listenedPort(server, fallbackPort) {
|
|
293
|
+
const address = server.address();
|
|
294
|
+
return typeof address === "object" && address !== null ? address.port : fallbackPort;
|
|
295
|
+
}
|
|
296
|
+
function isAddressInUseError(error) {
|
|
297
|
+
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
298
|
+
}
|
|
299
|
+
function closeServer(server) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
server.close((error) => {
|
|
302
|
+
if (error !== undefined) {
|
|
303
|
+
reject(error);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
resolve();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { renderKoreanDailyReport } from "./korean.js";
|
|
2
|
+
export type DailyReportLanguage = "ko";
|
|
3
|
+
export interface DailyReportInput {
|
|
4
|
+
reportDate: string;
|
|
5
|
+
generatedAt: string;
|
|
6
|
+
providerSummaries: readonly DailyProviderSummary[];
|
|
7
|
+
reportRunStatus: "rendered" | "sent" | "error";
|
|
8
|
+
}
|
|
9
|
+
export interface DailyProviderSummary {
|
|
10
|
+
provider: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
syncStatus: "ok" | "partial" | "error";
|
|
13
|
+
usageSnapshotCount: number;
|
|
14
|
+
billingSnapshotCount: number;
|
|
15
|
+
healthStatus: "ok" | "degraded" | "down" | "unknown";
|
|
16
|
+
estimatedAmountMinor: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
alertCount: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RenderDailyReportOptions {
|
|
21
|
+
lang: DailyReportLanguage;
|
|
22
|
+
}
|
|
23
|
+
export declare function renderDailyReport(input: DailyReportInput, options: RenderDailyReportOptions): string;
|
|
24
|
+
//# sourceMappingURL=daily.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { renderKoreanDailyReport } from "./korean.js";
|
|
2
|
+
export { renderKoreanDailyReport } from "./korean.js";
|
|
3
|
+
export function renderDailyReport(input, options) {
|
|
4
|
+
if (options.lang === "ko") {
|
|
5
|
+
return renderKoreanDailyReport(input);
|
|
6
|
+
}
|
|
7
|
+
throw new Error(`Unsupported daily report language: ${String(options.lang)}`);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=daily.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { renderDailyReport, type DailyProviderSummary, type DailyReportInput, type DailyReportLanguage, type RenderDailyReportOptions, } from "./daily.js";
|
|
2
|
+
export { renderKoreanDailyReport } from "./korean.js";
|
|
3
|
+
export { buildSlackReportPayload, sendSlackReport, SlackReportDeliveryError, type SendSlackReportOptions, type SlackReportDeliveryResult, type SlackReportPayload, type SlackReportTransport, type SlackReportTransportRequest, type SlackReportTransportResponse, } from "./slack.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function renderKoreanDailyReport(input) {
|
|
2
|
+
const lines = [
|
|
3
|
+
"*MoneySiren 일일 리포트*",
|
|
4
|
+
`- 날짜 ${input.reportDate}`,
|
|
5
|
+
`- 생성 ${input.generatedAt}`,
|
|
6
|
+
`- 리포트 상태 ${translateReportStatus(input.reportRunStatus)}`,
|
|
7
|
+
"",
|
|
8
|
+
];
|
|
9
|
+
if (input.providerSummaries.length === 0) {
|
|
10
|
+
lines.push("---", "*요약*", "- 수집된 공급자 데이터가 없습니다.");
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
for (const summary of input.providerSummaries) {
|
|
14
|
+
lines.push("---", ...renderProviderSummary(summary), "");
|
|
15
|
+
}
|
|
16
|
+
return lines.join("\n").trimEnd();
|
|
17
|
+
}
|
|
18
|
+
function renderProviderSummary(summary) {
|
|
19
|
+
return [
|
|
20
|
+
`*${summary.displayName}*`,
|
|
21
|
+
`- 동기화 상태 ${translateSyncStatus(summary.syncStatus)}`,
|
|
22
|
+
`- 사용량 스냅샷 ${summary.usageSnapshotCount}건`,
|
|
23
|
+
`- 청구 스냅샷 ${summary.billingSnapshotCount}건`,
|
|
24
|
+
`- 서비스 상태 ${translateHealthStatus(summary.healthStatus)}`,
|
|
25
|
+
`- 예상 비용 ${formatMinorCurrency(summary.estimatedAmountMinor, summary.currency)}`,
|
|
26
|
+
`- 알림 ${summary.alertCount}건`,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function translateReportStatus(status) {
|
|
30
|
+
if (status === "rendered") {
|
|
31
|
+
return "생성됨";
|
|
32
|
+
}
|
|
33
|
+
if (status === "sent") {
|
|
34
|
+
return "전송됨";
|
|
35
|
+
}
|
|
36
|
+
return "오류";
|
|
37
|
+
}
|
|
38
|
+
function translateSyncStatus(status) {
|
|
39
|
+
if (status === "ok") {
|
|
40
|
+
return "정상";
|
|
41
|
+
}
|
|
42
|
+
if (status === "partial") {
|
|
43
|
+
return "부분 성공";
|
|
44
|
+
}
|
|
45
|
+
return "오류";
|
|
46
|
+
}
|
|
47
|
+
function translateHealthStatus(status) {
|
|
48
|
+
if (status === "ok") {
|
|
49
|
+
return "정상";
|
|
50
|
+
}
|
|
51
|
+
if (status === "degraded") {
|
|
52
|
+
return "저하";
|
|
53
|
+
}
|
|
54
|
+
if (status === "down") {
|
|
55
|
+
return "중단";
|
|
56
|
+
}
|
|
57
|
+
return "알 수 없음";
|
|
58
|
+
}
|
|
59
|
+
function formatMinorCurrency(amountMinor, currency) {
|
|
60
|
+
return `${currency} ${(amountMinor / 100).toFixed(2)}`;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=korean.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SlackReportPayload {
|
|
2
|
+
text: string;
|
|
3
|
+
mrkdwn: true;
|
|
4
|
+
}
|
|
5
|
+
export interface SlackReportTransportRequest {
|
|
6
|
+
webhookUrl: string;
|
|
7
|
+
payload: SlackReportPayload;
|
|
8
|
+
signal: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface SlackReportTransportResponse {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
body?: string;
|
|
14
|
+
}
|
|
15
|
+
export type SlackReportTransport = (request: SlackReportTransportRequest) => Promise<SlackReportTransportResponse>;
|
|
16
|
+
export interface SendSlackReportOptions {
|
|
17
|
+
webhookUrl: string;
|
|
18
|
+
text: string;
|
|
19
|
+
transport?: SlackReportTransport;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SlackReportDeliveryResult {
|
|
23
|
+
status: "sent";
|
|
24
|
+
statusCode: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class SlackReportDeliveryError extends Error {
|
|
27
|
+
readonly statusCode?: number;
|
|
28
|
+
constructor(message: string, options?: {
|
|
29
|
+
statusCode?: number;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export declare function buildSlackReportPayload(text: string): SlackReportPayload;
|
|
33
|
+
export declare function sendSlackReport(options: SendSlackReportOptions): Promise<SlackReportDeliveryResult>;
|
|
34
|
+
//# sourceMappingURL=slack.d.ts.map
|