@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,87 @@
|
|
|
1
|
+
import { normalizeSupabaseUsageHealth, } from "./normalize.js";
|
|
2
|
+
export { createStaticSupabaseUsageHealthClient, createSupabaseManagementClient, } from "./client.js";
|
|
3
|
+
export { normalizeSupabaseUsageHealth, redactedSupabaseProjectRef, } from "./normalize.js";
|
|
4
|
+
const EMPTY_SUPABASE_SNAPSHOTS = {
|
|
5
|
+
usage: [],
|
|
6
|
+
billing: [],
|
|
7
|
+
serviceHealth: [],
|
|
8
|
+
costEstimates: [],
|
|
9
|
+
};
|
|
10
|
+
export function createSupabaseUsageHealthConnector(options) {
|
|
11
|
+
return {
|
|
12
|
+
kind: "supabase",
|
|
13
|
+
displayName: "Supabase Usage/Health",
|
|
14
|
+
access: "read-only",
|
|
15
|
+
async collect(context) {
|
|
16
|
+
const collectedAt = context.now().toISOString();
|
|
17
|
+
try {
|
|
18
|
+
const payload = await options.client.fetchUsageHealth();
|
|
19
|
+
const alerts = unavailableSurfaceAlerts(payload.unavailable ?? [], collectedAt);
|
|
20
|
+
return {
|
|
21
|
+
collectedAt,
|
|
22
|
+
status: alerts.length === 0 ? "ok" : "partial",
|
|
23
|
+
snapshots: normalizeSupabaseUsageHealth({
|
|
24
|
+
payload,
|
|
25
|
+
collectedAt,
|
|
26
|
+
}),
|
|
27
|
+
alerts,
|
|
28
|
+
...(alerts.length === 0 ? {} : { errors: alerts.map((alert) => alert.message) }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {
|
|
33
|
+
collectedAt,
|
|
34
|
+
status: "error",
|
|
35
|
+
snapshots: EMPTY_SUPABASE_SNAPSHOTS,
|
|
36
|
+
alerts: [
|
|
37
|
+
{
|
|
38
|
+
provider: "supabase",
|
|
39
|
+
createdAt: collectedAt,
|
|
40
|
+
severity: "warning",
|
|
41
|
+
category: "provider-sync",
|
|
42
|
+
title: "Supabase usage/health sync failed",
|
|
43
|
+
message: "Supabase usage/health request failed before normalized snapshots were collected.",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
errors: ["Supabase usage/health request failed."],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function unavailableSurfaceAlerts(surfaces, collectedAt) {
|
|
53
|
+
const uniqueSurfaces = new Set(surfaces.map((surface) => surface.surface));
|
|
54
|
+
return [...uniqueSurfaces].sort().map((surface) => ({
|
|
55
|
+
provider: "supabase",
|
|
56
|
+
createdAt: collectedAt,
|
|
57
|
+
severity: "warning",
|
|
58
|
+
category: "provider-sync",
|
|
59
|
+
title: unavailableSurfaceTitle(surface),
|
|
60
|
+
message: unavailableSurfaceMessage(surface),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function unavailableSurfaceTitle(surface) {
|
|
64
|
+
if (surface === "projects") {
|
|
65
|
+
return "Supabase projects surface unavailable";
|
|
66
|
+
}
|
|
67
|
+
if (surface === "usage.api-counts") {
|
|
68
|
+
return "Supabase usage.api-counts surface unavailable";
|
|
69
|
+
}
|
|
70
|
+
if (surface === "usage.api-requests-count") {
|
|
71
|
+
return "Supabase usage.api-requests-count surface unavailable";
|
|
72
|
+
}
|
|
73
|
+
return "Supabase health surface unavailable";
|
|
74
|
+
}
|
|
75
|
+
function unavailableSurfaceMessage(surface) {
|
|
76
|
+
if (surface === "projects") {
|
|
77
|
+
return "Supabase projects request failed before normalized snapshots were collected.";
|
|
78
|
+
}
|
|
79
|
+
if (surface === "usage.api-counts") {
|
|
80
|
+
return "Supabase usage.api-counts request failed before normalized snapshots were collected.";
|
|
81
|
+
}
|
|
82
|
+
if (surface === "usage.api-requests-count") {
|
|
83
|
+
return "Supabase usage.api-requests-count request failed before normalized snapshots were collected.";
|
|
84
|
+
}
|
|
85
|
+
return "Supabase health request failed before normalized snapshots were collected.";
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
declare const SUPABASE_PROVIDER = "supabase";
|
|
2
|
+
export interface SupabaseUsageHealthPayload {
|
|
3
|
+
projects?: readonly SupabaseProject[];
|
|
4
|
+
usage?: readonly SupabaseProjectUsage[];
|
|
5
|
+
health?: readonly SupabaseProjectHealth[];
|
|
6
|
+
unavailable?: readonly SupabaseUnavailableSurface[];
|
|
7
|
+
}
|
|
8
|
+
export interface SupabaseProject {
|
|
9
|
+
id?: string;
|
|
10
|
+
ref?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
region?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
organization_id?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SupabaseProjectUsage {
|
|
17
|
+
ref?: string;
|
|
18
|
+
projectRef?: string;
|
|
19
|
+
apiCounts?: SupabaseApiCountsResponse;
|
|
20
|
+
apiRequestsCount?: SupabaseApiRequestsCountResponse;
|
|
21
|
+
}
|
|
22
|
+
export interface SupabaseApiCountsResponse {
|
|
23
|
+
result?: readonly SupabaseApiCountsRow[];
|
|
24
|
+
data?: readonly SupabaseApiCountsRow[];
|
|
25
|
+
}
|
|
26
|
+
export interface SupabaseApiCountsRow {
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
total_auth_requests?: number | string;
|
|
29
|
+
total_realtime_requests?: number | string;
|
|
30
|
+
total_rest_requests?: number | string;
|
|
31
|
+
total_storage_requests?: number | string;
|
|
32
|
+
}
|
|
33
|
+
export interface SupabaseApiRequestsCountResponse {
|
|
34
|
+
result?: readonly SupabaseApiRequestsCountRow[];
|
|
35
|
+
data?: readonly SupabaseApiRequestsCountRow[];
|
|
36
|
+
}
|
|
37
|
+
export interface SupabaseApiRequestsCountRow {
|
|
38
|
+
count?: number | string;
|
|
39
|
+
}
|
|
40
|
+
export interface SupabaseProjectHealth {
|
|
41
|
+
ref?: string;
|
|
42
|
+
projectRef?: string;
|
|
43
|
+
services?: readonly SupabaseProjectHealthService[];
|
|
44
|
+
result?: readonly SupabaseProjectHealthService[] | Record<string, unknown>;
|
|
45
|
+
data?: readonly SupabaseProjectHealthService[] | Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface SupabaseProjectHealthService {
|
|
48
|
+
name?: string;
|
|
49
|
+
service?: string;
|
|
50
|
+
status?: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SupabaseUnavailableSurface {
|
|
55
|
+
surface: "projects" | "usage.api-counts" | "usage.api-requests-count" | "health";
|
|
56
|
+
ref?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SupabaseNormalizedSnapshotBundle {
|
|
59
|
+
usage: readonly SupabaseUsageSnapshot[];
|
|
60
|
+
billing: readonly SupabaseBillingSnapshot[];
|
|
61
|
+
serviceHealth: readonly SupabaseServiceHealthSnapshot[];
|
|
62
|
+
costEstimates: readonly SupabaseCostEstimate[];
|
|
63
|
+
}
|
|
64
|
+
export interface SupabaseUsageSnapshot {
|
|
65
|
+
provider: typeof SUPABASE_PROVIDER;
|
|
66
|
+
collectedAt: string;
|
|
67
|
+
providerAccountRef: string;
|
|
68
|
+
service: string;
|
|
69
|
+
metric: "api_requests" | "auth_requests" | "realtime_requests" | "rest_requests" | "storage_requests";
|
|
70
|
+
unit: "requests";
|
|
71
|
+
value: number;
|
|
72
|
+
}
|
|
73
|
+
export interface SupabaseBillingSnapshot {
|
|
74
|
+
provider: typeof SUPABASE_PROVIDER;
|
|
75
|
+
collectedAt: string;
|
|
76
|
+
periodStart: string;
|
|
77
|
+
periodEnd: string;
|
|
78
|
+
amountMinor: number;
|
|
79
|
+
currency: string;
|
|
80
|
+
status: string;
|
|
81
|
+
}
|
|
82
|
+
export interface SupabaseServiceHealthSnapshot {
|
|
83
|
+
provider: typeof SUPABASE_PROVIDER;
|
|
84
|
+
collectedAt: string;
|
|
85
|
+
service: string;
|
|
86
|
+
region?: string;
|
|
87
|
+
status: "ok" | "degraded" | "down" | "unknown";
|
|
88
|
+
message?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface SupabaseCostEstimate {
|
|
91
|
+
provider: typeof SUPABASE_PROVIDER;
|
|
92
|
+
collectedAt: string;
|
|
93
|
+
periodStart: string;
|
|
94
|
+
periodEnd: string;
|
|
95
|
+
estimatedAmountMinor: number;
|
|
96
|
+
currency: string;
|
|
97
|
+
confidence: "low" | "medium" | "high";
|
|
98
|
+
}
|
|
99
|
+
export interface NormalizeSupabaseUsageHealthInput {
|
|
100
|
+
payload: SupabaseUsageHealthPayload;
|
|
101
|
+
collectedAt: string;
|
|
102
|
+
}
|
|
103
|
+
export declare function normalizeSupabaseUsageHealth(input: NormalizeSupabaseUsageHealthInput): SupabaseNormalizedSnapshotBundle;
|
|
104
|
+
export declare function redactedSupabaseProjectRef(ref: string): string;
|
|
105
|
+
export {};
|
|
106
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const SUPABASE_PROVIDER = "supabase";
|
|
3
|
+
const PROJECT_REF_HASH_PREFIX = "supabase-project";
|
|
4
|
+
const API_COUNT_COUNTERS = [
|
|
5
|
+
{
|
|
6
|
+
metric: "auth_requests",
|
|
7
|
+
servicePrefix: "auth",
|
|
8
|
+
field: "total_auth_requests",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
metric: "realtime_requests",
|
|
12
|
+
servicePrefix: "realtime",
|
|
13
|
+
field: "total_realtime_requests",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
metric: "rest_requests",
|
|
17
|
+
servicePrefix: "rest",
|
|
18
|
+
field: "total_rest_requests",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
metric: "storage_requests",
|
|
22
|
+
servicePrefix: "storage",
|
|
23
|
+
field: "total_storage_requests",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
export function normalizeSupabaseUsageHealth(input) {
|
|
27
|
+
return {
|
|
28
|
+
usage: normalizeUsage(input.payload.usage ?? [], input.collectedAt),
|
|
29
|
+
billing: [],
|
|
30
|
+
serviceHealth: normalizeHealth(input.payload, input.collectedAt),
|
|
31
|
+
costEstimates: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function redactedSupabaseProjectRef(ref) {
|
|
35
|
+
const trimmedRef = requireNonBlankString(ref, "Supabase project ref");
|
|
36
|
+
const digest = createHash("sha256").update(`supabase:${trimmedRef}`).digest("hex").slice(0, 16);
|
|
37
|
+
return `${PROJECT_REF_HASH_PREFIX}:${digest}`;
|
|
38
|
+
}
|
|
39
|
+
function normalizeUsage(projectUsages, collectedAt) {
|
|
40
|
+
const snapshots = [];
|
|
41
|
+
for (const projectUsage of projectUsages) {
|
|
42
|
+
const ref = readPayloadProjectRef(projectUsage);
|
|
43
|
+
const projectRef = redactedSupabaseProjectRef(ref);
|
|
44
|
+
const apiRequestCount = sumApiRequestsCount(projectUsage.apiRequestsCount);
|
|
45
|
+
pushUsageSnapshot(snapshots, {
|
|
46
|
+
collectedAt,
|
|
47
|
+
projectRef,
|
|
48
|
+
servicePrefix: "api",
|
|
49
|
+
metric: "api_requests",
|
|
50
|
+
value: apiRequestCount,
|
|
51
|
+
});
|
|
52
|
+
for (const counter of API_COUNT_COUNTERS) {
|
|
53
|
+
pushUsageSnapshot(snapshots, {
|
|
54
|
+
collectedAt,
|
|
55
|
+
projectRef,
|
|
56
|
+
servicePrefix: counter.servicePrefix,
|
|
57
|
+
metric: counter.metric,
|
|
58
|
+
value: sumApiCounts(projectUsage.apiCounts, counter.field),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return snapshots;
|
|
63
|
+
}
|
|
64
|
+
function normalizeHealth(payload, collectedAt) {
|
|
65
|
+
const snapshots = [];
|
|
66
|
+
const projectsByRef = new Map();
|
|
67
|
+
for (const project of payload.projects ?? []) {
|
|
68
|
+
const ref = readProjectRef(project);
|
|
69
|
+
if (ref === undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
projectsByRef.set(ref, project);
|
|
73
|
+
snapshots.push(buildProjectHealthSnapshot(project, ref, collectedAt));
|
|
74
|
+
}
|
|
75
|
+
for (const projectHealth of payload.health ?? []) {
|
|
76
|
+
const ref = readPayloadProjectRef(projectHealth);
|
|
77
|
+
const project = projectsByRef.get(ref);
|
|
78
|
+
const projectRef = redactedSupabaseProjectRef(ref);
|
|
79
|
+
const region = readOptionalNonBlankString(project?.region);
|
|
80
|
+
for (const service of readHealthServices(projectHealth)) {
|
|
81
|
+
const serviceName = normalizeServiceName(readServiceName(service));
|
|
82
|
+
const status = mapServiceStatus(readOptionalNonBlankString(service.status));
|
|
83
|
+
const message = sanitizeHealthMessage(readServiceMessage(service), ref);
|
|
84
|
+
snapshots.push({
|
|
85
|
+
provider: SUPABASE_PROVIDER,
|
|
86
|
+
collectedAt,
|
|
87
|
+
service: `${serviceName}:${projectRef}`,
|
|
88
|
+
...(region === undefined ? {} : { region }),
|
|
89
|
+
status,
|
|
90
|
+
...(message === undefined ? {} : { message }),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return snapshots;
|
|
95
|
+
}
|
|
96
|
+
function buildProjectHealthSnapshot(project, ref, collectedAt) {
|
|
97
|
+
const status = mapProjectStatus(readOptionalNonBlankString(project.status));
|
|
98
|
+
const region = readOptionalNonBlankString(project.region);
|
|
99
|
+
const message = projectStatusMessage(readOptionalNonBlankString(project.status));
|
|
100
|
+
return {
|
|
101
|
+
provider: SUPABASE_PROVIDER,
|
|
102
|
+
collectedAt,
|
|
103
|
+
service: `project:${redactedSupabaseProjectRef(ref)}`,
|
|
104
|
+
...(region === undefined ? {} : { region }),
|
|
105
|
+
status,
|
|
106
|
+
...(message === undefined ? {} : { message }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function pushUsageSnapshot(snapshots, input) {
|
|
110
|
+
if (input.value === undefined || input.value <= 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
snapshots.push({
|
|
114
|
+
provider: SUPABASE_PROVIDER,
|
|
115
|
+
collectedAt: input.collectedAt,
|
|
116
|
+
providerAccountRef: input.projectRef,
|
|
117
|
+
service: `${input.servicePrefix}:${input.projectRef}`,
|
|
118
|
+
metric: input.metric,
|
|
119
|
+
unit: "requests",
|
|
120
|
+
value: input.value,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function sumApiRequestsCount(response) {
|
|
124
|
+
const rows = response?.result ?? response?.data ?? [];
|
|
125
|
+
let total = 0;
|
|
126
|
+
let sawValue = false;
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const value = readOptionalFiniteNumber(row.count, "Supabase usage.api-requests-count count");
|
|
129
|
+
if (value !== undefined) {
|
|
130
|
+
total += value;
|
|
131
|
+
sawValue = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return sawValue ? total : undefined;
|
|
135
|
+
}
|
|
136
|
+
function sumApiCounts(response, field) {
|
|
137
|
+
const rows = response?.result ?? response?.data ?? [];
|
|
138
|
+
let total = 0;
|
|
139
|
+
let sawValue = false;
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const value = readOptionalFiniteNumber(row[field], `Supabase ${String(field)}`);
|
|
142
|
+
if (value !== undefined) {
|
|
143
|
+
total += value;
|
|
144
|
+
sawValue = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return sawValue ? total : undefined;
|
|
148
|
+
}
|
|
149
|
+
function readHealthServices(projectHealth) {
|
|
150
|
+
if (Array.isArray(projectHealth.services)) {
|
|
151
|
+
return [...projectHealth.services];
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(projectHealth.result)) {
|
|
154
|
+
return [...projectHealth.result];
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(projectHealth.data)) {
|
|
157
|
+
return [...projectHealth.data];
|
|
158
|
+
}
|
|
159
|
+
const objectResult = isRecord(projectHealth.result) ? projectHealth.result : projectHealth.data;
|
|
160
|
+
if (!isRecord(objectResult)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
return Object.entries(objectResult).map(([name, value]) => {
|
|
164
|
+
if (isRecord(value)) {
|
|
165
|
+
const status = readOptionalNonBlankString(value.status);
|
|
166
|
+
const message = readOptionalNonBlankString(value.message);
|
|
167
|
+
const error = readOptionalNonBlankString(value.error);
|
|
168
|
+
return {
|
|
169
|
+
name,
|
|
170
|
+
...(status === undefined ? {} : { status }),
|
|
171
|
+
...(message === undefined ? {} : { message }),
|
|
172
|
+
...(error === undefined ? {} : { error }),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
status: typeof value === "string" ? value : "unknown",
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function readProjectRef(project) {
|
|
182
|
+
const ref = readOptionalNonBlankString(project.id) ?? readOptionalNonBlankString(project.ref);
|
|
183
|
+
return ref;
|
|
184
|
+
}
|
|
185
|
+
function readPayloadProjectRef(value) {
|
|
186
|
+
return requireNonBlankString(value.ref ?? value.projectRef, "Supabase project ref");
|
|
187
|
+
}
|
|
188
|
+
function readServiceName(service) {
|
|
189
|
+
return requireNonBlankString(service.name ?? service.service, "Supabase service name");
|
|
190
|
+
}
|
|
191
|
+
function readServiceMessage(service) {
|
|
192
|
+
return readOptionalNonBlankString(service.message) ?? readOptionalNonBlankString(service.error);
|
|
193
|
+
}
|
|
194
|
+
function normalizeServiceName(serviceName) {
|
|
195
|
+
return serviceName.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "service";
|
|
196
|
+
}
|
|
197
|
+
function mapProjectStatus(status) {
|
|
198
|
+
const normalizedStatus = status?.trim().toLowerCase();
|
|
199
|
+
if (normalizedStatus === undefined) {
|
|
200
|
+
return "unknown";
|
|
201
|
+
}
|
|
202
|
+
if (["active", "available", "healthy", "ok", "running"].includes(normalizedStatus)) {
|
|
203
|
+
return "ok";
|
|
204
|
+
}
|
|
205
|
+
if (["paused", "inactive", "restoring", "upgrading"].includes(normalizedStatus)) {
|
|
206
|
+
return "degraded";
|
|
207
|
+
}
|
|
208
|
+
if (["failed", "unhealthy", "down", "error"].includes(normalizedStatus)) {
|
|
209
|
+
return "down";
|
|
210
|
+
}
|
|
211
|
+
return "unknown";
|
|
212
|
+
}
|
|
213
|
+
function projectStatusMessage(status) {
|
|
214
|
+
return status?.trim().toLowerCase() === "paused" ? "Project is paused." : undefined;
|
|
215
|
+
}
|
|
216
|
+
function mapServiceStatus(status) {
|
|
217
|
+
const normalizedStatus = status?.trim().toLowerCase();
|
|
218
|
+
if (normalizedStatus === undefined) {
|
|
219
|
+
return "unknown";
|
|
220
|
+
}
|
|
221
|
+
if (["active", "available", "healthy", "ok", "running", "operational"].includes(normalizedStatus)) {
|
|
222
|
+
return "ok";
|
|
223
|
+
}
|
|
224
|
+
if (["degraded", "warning", "limited"].includes(normalizedStatus)) {
|
|
225
|
+
return "degraded";
|
|
226
|
+
}
|
|
227
|
+
if (["failed", "unhealthy", "down", "error", "unavailable"].includes(normalizedStatus)) {
|
|
228
|
+
return "down";
|
|
229
|
+
}
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
function sanitizeHealthMessage(message, rawProjectRef) {
|
|
233
|
+
const trimmedMessage = readOptionalNonBlankString(message);
|
|
234
|
+
if (trimmedMessage === undefined) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return trimmedMessage
|
|
238
|
+
.replaceAll(rawProjectRef, "[REDACTED:supabase_project]")
|
|
239
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[REDACTED:email]")
|
|
240
|
+
.replace(/\b(?:proj|project)[_-][A-Za-z0-9_-]+\b/gi, "[REDACTED:project]")
|
|
241
|
+
.slice(0, 240);
|
|
242
|
+
}
|
|
243
|
+
function readOptionalFiniteNumber(value, label) {
|
|
244
|
+
if (value === undefined || value === null || value === "") {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
248
|
+
if (!Number.isFinite(numberValue)) {
|
|
249
|
+
throw new Error(`${label} must be a finite number.`);
|
|
250
|
+
}
|
|
251
|
+
return numberValue;
|
|
252
|
+
}
|
|
253
|
+
function requireNonBlankString(value, label) {
|
|
254
|
+
const trimmedValue = readOptionalNonBlankString(value);
|
|
255
|
+
if (trimmedValue === undefined) {
|
|
256
|
+
throw new Error(`${label} must be a non-blank string.`);
|
|
257
|
+
}
|
|
258
|
+
return trimmedValue;
|
|
259
|
+
}
|
|
260
|
+
function readOptionalNonBlankString(value) {
|
|
261
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
262
|
+
}
|
|
263
|
+
function isRecord(value) {
|
|
264
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Alert, CollectionStatus, ProviderKind, SnapshotBundle } from "./snapshots.js";
|
|
2
|
+
import type { ProviderCollectionContext, ProviderConnector } from "./provider.js";
|
|
3
|
+
export interface CollectedProviderSnapshots {
|
|
4
|
+
provider: ProviderKind;
|
|
5
|
+
collectedAt: string;
|
|
6
|
+
status: CollectionStatus;
|
|
7
|
+
snapshots: SnapshotBundle;
|
|
8
|
+
alerts: readonly Alert[];
|
|
9
|
+
errors?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function collectProviderSnapshots(provider: ProviderConnector, context?: ProviderCollectionContext): Promise<CollectedProviderSnapshots>;
|
|
12
|
+
//# sourceMappingURL=collector.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const DEFAULT_COLLECTION_CONTEXT = {
|
|
2
|
+
now: () => new Date(),
|
|
3
|
+
};
|
|
4
|
+
export async function collectProviderSnapshots(provider, context = DEFAULT_COLLECTION_CONTEXT) {
|
|
5
|
+
if (provider.access !== "read-only") {
|
|
6
|
+
throw new Error(`Provider ${provider.displayName} must be read-only.`);
|
|
7
|
+
}
|
|
8
|
+
const result = await provider.collect(context);
|
|
9
|
+
assertNoRawProviderPayload(result.snapshots);
|
|
10
|
+
assertSnapshotProvider(result.snapshots, provider.kind);
|
|
11
|
+
const collected = {
|
|
12
|
+
provider: provider.kind,
|
|
13
|
+
collectedAt: result.collectedAt,
|
|
14
|
+
status: result.status,
|
|
15
|
+
snapshots: result.snapshots,
|
|
16
|
+
alerts: result.alerts,
|
|
17
|
+
};
|
|
18
|
+
if (result.errors !== undefined) {
|
|
19
|
+
return {
|
|
20
|
+
...collected,
|
|
21
|
+
errors: result.errors,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return collected;
|
|
25
|
+
}
|
|
26
|
+
function assertSnapshotProvider(snapshots, provider) {
|
|
27
|
+
const snapshotGroups = [
|
|
28
|
+
...snapshots.usage,
|
|
29
|
+
...snapshots.billing,
|
|
30
|
+
...snapshots.serviceHealth,
|
|
31
|
+
...snapshots.costEstimates,
|
|
32
|
+
];
|
|
33
|
+
for (const snapshot of snapshotGroups) {
|
|
34
|
+
if (snapshot.provider !== provider) {
|
|
35
|
+
throw new Error(`Snapshot provider ${snapshot.provider} does not match connector ${provider}.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertNoRawProviderPayload(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
for (const item of value) {
|
|
42
|
+
assertNoRawProviderPayload(item);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
50
|
+
if (isForbiddenPayloadKey(key)) {
|
|
51
|
+
throw new Error(`Snapshot contains raw provider payload field: ${key}.`);
|
|
52
|
+
}
|
|
53
|
+
assertNoRawProviderPayload(nestedValue);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isForbiddenPayloadKey(key) {
|
|
57
|
+
const normalizedKey = key.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
58
|
+
return (normalizedKey === "raw" ||
|
|
59
|
+
normalizedKey === "rawpayload" ||
|
|
60
|
+
normalizedKey === "rawresponse" ||
|
|
61
|
+
normalizedKey === "providerpayload" ||
|
|
62
|
+
normalizedKey === "providerresponse" ||
|
|
63
|
+
normalizedKey === "billingprofile");
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return typeof value === "object" && value !== null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=collector.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { collectProviderSnapshots, type CollectedProviderSnapshots } from "./collector.js";
|
|
2
|
+
export { type ProviderCollectionContext, type ProviderCollectionResult, type ProviderConnector, } from "./provider.js";
|
|
3
|
+
export { evaluateRiskHints, type RiskHint } from "./risk-engine.js";
|
|
4
|
+
export { createEmptySnapshotBundle, type Alert, type BillingSnapshot, type CollectionStatus, type CostEstimate, type ProviderKind, type ServiceHealthSnapshot, type SnapshotBase, type SnapshotBundle, type UsageSnapshot, } from "./snapshots.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Alert, CollectionStatus, ProviderKind, SnapshotBundle } from "./snapshots.js";
|
|
2
|
+
export interface ProviderCollectionContext {
|
|
3
|
+
now(): Date;
|
|
4
|
+
}
|
|
5
|
+
export interface ProviderCollectionResult {
|
|
6
|
+
collectedAt: string;
|
|
7
|
+
status: CollectionStatus;
|
|
8
|
+
snapshots: SnapshotBundle;
|
|
9
|
+
alerts: readonly Alert[];
|
|
10
|
+
errors?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ProviderConnector {
|
|
13
|
+
kind: ProviderKind;
|
|
14
|
+
displayName: string;
|
|
15
|
+
access: "read-only";
|
|
16
|
+
collect(context: ProviderCollectionContext): Promise<ProviderCollectionResult>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderKind, SnapshotBundle } from "./snapshots.js";
|
|
2
|
+
export interface RiskHint {
|
|
3
|
+
provider?: ProviderKind;
|
|
4
|
+
severity: "info" | "warning" | "critical";
|
|
5
|
+
category: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function evaluateRiskHints(_snapshots: SnapshotBundle): readonly RiskHint[];
|
|
9
|
+
//# sourceMappingURL=risk-engine.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type ProviderKind = "mock" | "aws" | "openai" | "supabase" | "cloudflare";
|
|
2
|
+
export type CollectionStatus = "ok" | "partial" | "error";
|
|
3
|
+
export interface SnapshotBase {
|
|
4
|
+
provider: ProviderKind;
|
|
5
|
+
collectedAt: string;
|
|
6
|
+
providerAccountRef?: string;
|
|
7
|
+
service?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface UsageSnapshot extends SnapshotBase {
|
|
10
|
+
metric: string;
|
|
11
|
+
unit: string;
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
export interface BillingSnapshot extends SnapshotBase {
|
|
15
|
+
periodStart: string;
|
|
16
|
+
periodEnd: string;
|
|
17
|
+
amountMinor: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
status: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ServiceHealthSnapshot extends SnapshotBase {
|
|
22
|
+
service: string;
|
|
23
|
+
region?: string;
|
|
24
|
+
status: "ok" | "degraded" | "down" | "unknown";
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CostEstimate extends SnapshotBase {
|
|
28
|
+
periodStart: string;
|
|
29
|
+
periodEnd: string;
|
|
30
|
+
estimatedAmountMinor: number;
|
|
31
|
+
currency: string;
|
|
32
|
+
confidence: "low" | "medium" | "high";
|
|
33
|
+
}
|
|
34
|
+
export interface Alert {
|
|
35
|
+
provider?: ProviderKind;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
severity: "info" | "warning" | "critical";
|
|
38
|
+
category: string;
|
|
39
|
+
title: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SnapshotBundle {
|
|
43
|
+
usage: readonly UsageSnapshot[];
|
|
44
|
+
billing: readonly BillingSnapshot[];
|
|
45
|
+
serviceHealth: readonly ServiceHealthSnapshot[];
|
|
46
|
+
costEstimates: readonly CostEstimate[];
|
|
47
|
+
}
|
|
48
|
+
export declare function createEmptySnapshotBundle(): SnapshotBundle;
|
|
49
|
+
//# sourceMappingURL=snapshots.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MigrationExecutor, type MigrationRunResult } from "./migrate.js";
|
|
2
|
+
export interface MoneySirenDbClient {
|
|
3
|
+
dbPath: string;
|
|
4
|
+
migrate(): Promise<MigrationRunResult>;
|
|
5
|
+
}
|
|
6
|
+
export interface MoneySirenDbClientOptions {
|
|
7
|
+
dbPath: string;
|
|
8
|
+
executor: MigrationExecutor;
|
|
9
|
+
}
|
|
10
|
+
export declare function createMoneySirenDbClient(options: MoneySirenDbClientOptions): MoneySirenDbClient;
|
|
11
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runMigrations } from "./migrate.js";
|
|
2
|
+
export function createMoneySirenDbClient(options) {
|
|
3
|
+
const dbPath = options.dbPath.trim();
|
|
4
|
+
if (dbPath.length === 0) {
|
|
5
|
+
throw new Error("dbPath must not be blank.");
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
dbPath,
|
|
9
|
+
migrate() {
|
|
10
|
+
return runMigrations(options.executor);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=client.js.map
|