@moneysiren/cli 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/apps/cli/src/cli.d.ts +56 -0
  4. package/dist/apps/cli/src/cli.js +182 -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 +116 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +65 -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 +5 -0
  20. package/dist/apps/cli/src/commands/runtime.js +133 -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/home.d.ts +7 -0
  30. package/dist/apps/cli/src/home.js +97 -0
  31. package/dist/apps/cli/src/index.d.ts +3 -0
  32. package/dist/apps/cli/src/index.js +14 -0
  33. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  34. package/dist/apps/cli/src/install-profile.js +124 -0
  35. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  36. package/dist/apps/cli/src/install-selector.js +66 -0
  37. package/dist/apps/cli/src/interactive.d.ts +3 -0
  38. package/dist/apps/cli/src/interactive.js +32 -0
  39. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  40. package/dist/apps/cli/src/postinstall.js +42 -0
  41. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  42. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  43. package/dist/apps/cli/src/slash.d.ts +15 -0
  44. package/dist/apps/cli/src/slash.js +202 -0
  45. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  46. package/dist/apps/cli/src/summary-model.js +136 -0
  47. package/dist/apps/cli/src/theme.d.ts +18 -0
  48. package/dist/apps/cli/src/theme.js +118 -0
  49. package/dist/packages/config/src/index.d.ts +3 -0
  50. package/dist/packages/config/src/index.js +3 -0
  51. package/dist/packages/config/src/load.d.ts +3 -0
  52. package/dist/packages/config/src/load.js +77 -0
  53. package/dist/packages/config/src/schema.d.ts +46 -0
  54. package/dist/packages/config/src/schema.js +25 -0
  55. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  56. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  57. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  58. package/dist/packages/connectors/aws/src/index.js +67 -0
  59. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  60. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  61. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  62. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  63. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  64. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  65. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  66. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  67. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  68. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  69. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  70. package/dist/packages/connectors/mock/src/index.js +66 -0
  71. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  72. package/dist/packages/connectors/openai/src/index.js +169 -0
  73. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  74. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  75. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  76. package/dist/packages/connectors/supabase/src/client.js +132 -0
  77. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  78. package/dist/packages/connectors/supabase/src/index.js +87 -0
  79. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  80. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  81. package/dist/packages/core/src/collector.d.ts +12 -0
  82. package/dist/packages/core/src/collector.js +68 -0
  83. package/dist/packages/core/src/index.d.ts +5 -0
  84. package/dist/packages/core/src/index.js +4 -0
  85. package/dist/packages/core/src/provider.d.ts +18 -0
  86. package/dist/packages/core/src/provider.js +2 -0
  87. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  88. package/dist/packages/core/src/risk-engine.js +4 -0
  89. package/dist/packages/core/src/snapshots.d.ts +49 -0
  90. package/dist/packages/core/src/snapshots.js +9 -0
  91. package/dist/packages/db/src/client.d.ts +11 -0
  92. package/dist/packages/db/src/client.js +14 -0
  93. package/dist/packages/db/src/index.d.ts +6 -0
  94. package/dist/packages/db/src/index.js +6 -0
  95. package/dist/packages/db/src/local-store.d.ts +161 -0
  96. package/dist/packages/db/src/local-store.js +623 -0
  97. package/dist/packages/db/src/migrate.d.ts +17 -0
  98. package/dist/packages/db/src/migrate.js +35 -0
  99. package/dist/packages/db/src/schema.d.ts +5 -0
  100. package/dist/packages/db/src/schema.js +120 -0
  101. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  102. package/dist/packages/db/src/sqlite-bin.js +16 -0
  103. package/dist/packages/local-api/src/index.d.ts +2 -0
  104. package/dist/packages/local-api/src/index.js +2 -0
  105. package/dist/packages/local-api/src/server.d.ts +36 -0
  106. package/dist/packages/local-api/src/server.js +310 -0
  107. package/dist/packages/report/src/daily.d.ts +24 -0
  108. package/dist/packages/report/src/daily.js +9 -0
  109. package/dist/packages/report/src/index.d.ts +4 -0
  110. package/dist/packages/report/src/index.js +4 -0
  111. package/dist/packages/report/src/korean.d.ts +3 -0
  112. package/dist/packages/report/src/korean.js +62 -0
  113. package/dist/packages/report/src/slack.d.ts +34 -0
  114. package/dist/packages/report/src/slack.js +134 -0
  115. package/dist/packages/runtime/src/index.d.ts +2 -0
  116. package/dist/packages/runtime/src/index.js +2 -0
  117. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  118. package/dist/packages/runtime/src/runtime.js +182 -0
  119. package/dist/packages/view-model/src/index.d.ts +3 -0
  120. package/dist/packages/view-model/src/index.js +3 -0
  121. package/dist/packages/view-model/src/notification-preferences-model.d.ts +47 -0
  122. package/dist/packages/view-model/src/notification-preferences-model.js +218 -0
  123. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  124. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  125. package/dist/packages/view-model/src/view-model.d.ts +193 -0
  126. package/dist/packages/view-model/src/view-model.js +684 -0
  127. package/package.json +49 -0
  128. package/scripts/postinstall.mjs +11 -0
@@ -0,0 +1,25 @@
1
+ export const DEFAULT_DB_PATH = ".moneysiren/moneysiren.sqlite";
2
+ export const PROVIDER_ENV_KEYS = {
3
+ aws: ["AWS_PROFILE"],
4
+ openai: ["OPENAI_ADMIN_KEY"],
5
+ supabase: ["SUPABASE_ACCESS_TOKEN"],
6
+ cloudflare: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_IDS"],
7
+ gcp: ["GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT"],
8
+ azure: ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID"],
9
+ oracle: ["OCI_CONFIG_FILE", "OCI_PROFILE"],
10
+ anthropic: ["ANTHROPIC_ADMIN_KEY"],
11
+ gemini: ["GEMINI_API_KEY", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT"],
12
+ vercel: ["VERCEL_API_TOKEN"],
13
+ "github-actions": ["GITHUB_TOKEN"],
14
+ railway: ["RAILWAY_API_TOKEN"],
15
+ fly: ["FLY_ACCESS_TOKEN"],
16
+ netlify: ["NETLIFY_AUTH_TOKEN"],
17
+ render: ["RENDER_API_KEY"],
18
+ neon: ["NEON_API_KEY"],
19
+ "mongodb-atlas": ["MONGODB_ATLAS_PUBLIC_KEY", "MONGODB_ATLAS_PRIVATE_KEY", "MONGODB_ATLAS_ORG_ID"],
20
+ datadog: ["DATADOG_API_KEY", "DATADOG_APP_KEY", "DATADOG_SITE"],
21
+ sentry: ["SENTRY_AUTH_TOKEN", "SENTRY_ORG"],
22
+ "codex-cli": [],
23
+ "claude-cli": [],
24
+ };
25
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,34 @@
1
+ import type { AwsCostExplorerGetCostAndUsageOutput } from "./normalize.js";
2
+ export interface AwsGetCostAndUsageInput {
3
+ TimePeriod: {
4
+ Start: string;
5
+ End: string;
6
+ };
7
+ Granularity: "MONTHLY";
8
+ Metrics: readonly ["UnblendedCost"];
9
+ GroupBy: readonly [
10
+ {
11
+ Type: "DIMENSION";
12
+ Key: "SERVICE";
13
+ }
14
+ ];
15
+ }
16
+ export interface AwsCostExplorerCommand {
17
+ name: "GetCostAndUsage";
18
+ input: AwsGetCostAndUsageInput;
19
+ }
20
+ export interface AwsCostExplorerClientAdapter {
21
+ send(command: AwsCostExplorerCommand): Promise<AwsCostExplorerGetCostAndUsageOutput>;
22
+ }
23
+ export interface AwsCostExplorerCommandAdapter {
24
+ createGetCostAndUsageCommand(input: AwsGetCostAndUsageInput): AwsCostExplorerCommand;
25
+ }
26
+ export interface AwsBillingPeriod {
27
+ start: string;
28
+ end: string;
29
+ }
30
+ export declare const defaultAwsCostExplorerCommandAdapter: AwsCostExplorerCommandAdapter;
31
+ export declare function createGetCostAndUsageInput(period: AwsBillingPeriod): AwsGetCostAndUsageInput;
32
+ export declare function createCurrentBillingPeriod(now: Date): AwsBillingPeriod;
33
+ export declare function createStaticCostExplorerClient(response: AwsCostExplorerGetCostAndUsageOutput): AwsCostExplorerClientAdapter;
34
+ //# sourceMappingURL=cost-explorer.d.ts.map
@@ -0,0 +1,43 @@
1
+ export const defaultAwsCostExplorerCommandAdapter = {
2
+ createGetCostAndUsageCommand(input) {
3
+ return {
4
+ name: "GetCostAndUsage",
5
+ input,
6
+ };
7
+ },
8
+ };
9
+ export function createGetCostAndUsageInput(period) {
10
+ return {
11
+ TimePeriod: {
12
+ Start: period.start,
13
+ End: period.end,
14
+ },
15
+ Granularity: "MONTHLY",
16
+ Metrics: ["UnblendedCost"],
17
+ GroupBy: [
18
+ {
19
+ Type: "DIMENSION",
20
+ Key: "SERVICE",
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ export function createCurrentBillingPeriod(now) {
26
+ const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
27
+ const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
28
+ return {
29
+ start: formatAwsDate(start),
30
+ end: formatAwsDate(end),
31
+ };
32
+ }
33
+ export function createStaticCostExplorerClient(response) {
34
+ return {
35
+ async send() {
36
+ return response;
37
+ },
38
+ };
39
+ }
40
+ function formatAwsDate(date) {
41
+ return date.toISOString().slice(0, 10);
42
+ }
43
+ //# sourceMappingURL=cost-explorer.js.map
@@ -0,0 +1,35 @@
1
+ import { type AwsCostExplorerClientAdapter, type AwsCostExplorerCommandAdapter } from "./cost-explorer.js";
2
+ import { type AwsNormalizedSnapshotBundle } from "./normalize.js";
3
+ export { createCurrentBillingPeriod, createGetCostAndUsageInput, createStaticCostExplorerClient, defaultAwsCostExplorerCommandAdapter, type AwsCostExplorerClientAdapter, type AwsCostExplorerCommand, type AwsCostExplorerCommandAdapter, type AwsGetCostAndUsageInput, } from "./cost-explorer.js";
4
+ export { createAwsSdkCostExplorerClient, type CreateAwsSdkCostExplorerClientOptions, } from "./sdk-client.js";
5
+ export { decimalAmountToMinorUnits, normalizeCostExplorerResponse, type AwsBillingSnapshot, type AwsCostEstimate, type AwsCostExplorerGetCostAndUsageOutput, type AwsCostExplorerGroup, type AwsCostExplorerMetricAmount, type AwsCostExplorerMetrics, type AwsCostExplorerResultByTime, type AwsCostExplorerTimePeriod, type AwsNormalizedSnapshotBundle, type AwsServiceHealthSnapshot, type AwsUsageSnapshot, } from "./normalize.js";
6
+ export interface AwsProviderCollectionContext {
7
+ now(): Date;
8
+ }
9
+ export interface AwsProviderConnector {
10
+ kind: "aws";
11
+ displayName: "AWS Cost Explorer";
12
+ access: "read-only";
13
+ collect(context: AwsProviderCollectionContext): Promise<AwsProviderCollectionResult>;
14
+ }
15
+ export interface AwsProviderCollectionResult {
16
+ collectedAt: string;
17
+ status: "ok" | "error";
18
+ snapshots: AwsNormalizedSnapshotBundle;
19
+ alerts: readonly AwsProviderAlert[];
20
+ errors?: readonly string[];
21
+ }
22
+ export interface AwsProviderAlert {
23
+ provider: "aws";
24
+ createdAt: string;
25
+ severity: "warning";
26
+ category: "provider-sync";
27
+ title: "AWS Cost Explorer sync failed";
28
+ message: string;
29
+ }
30
+ export interface AwsCostExplorerConnectorOptions {
31
+ costExplorerClient: AwsCostExplorerClientAdapter;
32
+ commandAdapter?: AwsCostExplorerCommandAdapter;
33
+ }
34
+ export declare function createAwsCostExplorerConnector(options: AwsCostExplorerConnectorOptions): AwsProviderConnector;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { createCurrentBillingPeriod, createGetCostAndUsageInput, defaultAwsCostExplorerCommandAdapter, } from "./cost-explorer.js";
2
+ import { normalizeCostExplorerResponse } from "./normalize.js";
3
+ export { createCurrentBillingPeriod, createGetCostAndUsageInput, createStaticCostExplorerClient, defaultAwsCostExplorerCommandAdapter, } from "./cost-explorer.js";
4
+ export { createAwsSdkCostExplorerClient, } from "./sdk-client.js";
5
+ export { decimalAmountToMinorUnits, normalizeCostExplorerResponse, } from "./normalize.js";
6
+ const EMPTY_AWS_SNAPSHOTS = {
7
+ usage: [],
8
+ billing: [],
9
+ serviceHealth: [],
10
+ costEstimates: [],
11
+ };
12
+ export function createAwsCostExplorerConnector(options) {
13
+ const commandAdapter = options.commandAdapter ?? defaultAwsCostExplorerCommandAdapter;
14
+ return {
15
+ kind: "aws",
16
+ displayName: "AWS Cost Explorer",
17
+ access: "read-only",
18
+ async collect(context) {
19
+ const collectedAt = context.now().toISOString();
20
+ const period = createCurrentBillingPeriod(context.now());
21
+ const command = commandAdapter.createGetCostAndUsageCommand(createGetCostAndUsageInput(period));
22
+ try {
23
+ const response = await options.costExplorerClient.send(command);
24
+ return {
25
+ collectedAt,
26
+ status: "ok",
27
+ snapshots: normalizeCostExplorerResponse({
28
+ response,
29
+ collectedAt,
30
+ }),
31
+ alerts: [],
32
+ };
33
+ }
34
+ catch (caught) {
35
+ const message = `AWS Cost Explorer request failed: ${safeAwsErrorMessage(caught)}`;
36
+ return {
37
+ collectedAt,
38
+ status: "error",
39
+ snapshots: EMPTY_AWS_SNAPSHOTS,
40
+ alerts: [
41
+ {
42
+ provider: "aws",
43
+ createdAt: collectedAt,
44
+ severity: "warning",
45
+ category: "provider-sync",
46
+ title: "AWS Cost Explorer sync failed",
47
+ message,
48
+ },
49
+ ],
50
+ errors: [message],
51
+ };
52
+ }
53
+ },
54
+ };
55
+ }
56
+ function safeAwsErrorMessage(caught) {
57
+ const rawMessage = caught instanceof Error && caught.message.trim().length > 0
58
+ ? caught.message
59
+ : "unknown AWS SDK error";
60
+ return rawMessage
61
+ .replace(/\b(AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16})\b/g, "[redacted]")
62
+ .replace(/\b\d{12}\b/g, "[redacted]")
63
+ .replace(/\barn:aws[^\s,;)]*/gi, "[redacted]")
64
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]")
65
+ .slice(0, 240);
66
+ }
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,69 @@
1
+ declare const MONEYSIREN_COST_METRIC = "unblended_cost";
2
+ export interface AwsCostExplorerGetCostAndUsageOutput {
3
+ ResultsByTime?: readonly AwsCostExplorerResultByTime[];
4
+ }
5
+ export interface AwsCostExplorerResultByTime {
6
+ TimePeriod?: AwsCostExplorerTimePeriod;
7
+ Total?: AwsCostExplorerMetrics;
8
+ Groups?: readonly AwsCostExplorerGroup[];
9
+ Estimated?: boolean;
10
+ }
11
+ export interface AwsCostExplorerTimePeriod {
12
+ Start?: string;
13
+ End?: string;
14
+ }
15
+ export type AwsCostExplorerMetrics = Record<string, AwsCostExplorerMetricAmount | undefined>;
16
+ export interface AwsCostExplorerMetricAmount {
17
+ Amount?: string;
18
+ Unit?: string;
19
+ }
20
+ export interface AwsCostExplorerGroup {
21
+ Keys?: readonly string[];
22
+ Metrics?: AwsCostExplorerMetrics;
23
+ }
24
+ export interface AwsNormalizedSnapshotBundle {
25
+ usage: readonly AwsUsageSnapshot[];
26
+ billing: readonly AwsBillingSnapshot[];
27
+ serviceHealth: readonly AwsServiceHealthSnapshot[];
28
+ costEstimates: readonly AwsCostEstimate[];
29
+ }
30
+ export interface AwsUsageSnapshot {
31
+ provider: "aws";
32
+ collectedAt: string;
33
+ service: string;
34
+ metric: typeof MONEYSIREN_COST_METRIC;
35
+ unit: string;
36
+ value: number;
37
+ }
38
+ export interface AwsBillingSnapshot {
39
+ provider: "aws";
40
+ collectedAt: string;
41
+ periodStart: string;
42
+ periodEnd: string;
43
+ amountMinor: number;
44
+ currency: string;
45
+ status: "estimated" | "final";
46
+ }
47
+ export interface AwsServiceHealthSnapshot {
48
+ provider: "aws";
49
+ collectedAt: string;
50
+ service: string;
51
+ status: "ok" | "degraded" | "down" | "unknown";
52
+ }
53
+ export interface AwsCostEstimate {
54
+ provider: "aws";
55
+ collectedAt: string;
56
+ periodStart: string;
57
+ periodEnd: string;
58
+ estimatedAmountMinor: number;
59
+ currency: string;
60
+ confidence: "medium" | "high";
61
+ }
62
+ export interface NormalizeCostExplorerResponseInput {
63
+ response: AwsCostExplorerGetCostAndUsageOutput;
64
+ collectedAt: string;
65
+ }
66
+ export declare function normalizeCostExplorerResponse(input: NormalizeCostExplorerResponseInput): AwsNormalizedSnapshotBundle;
67
+ export declare function decimalAmountToMinorUnits(amount: string): number;
68
+ export {};
69
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1,141 @@
1
+ const UNBLENDED_COST_METRIC = "UnblendedCost";
2
+ const MONEYSIREN_COST_METRIC = "unblended_cost";
3
+ export function normalizeCostExplorerResponse(input) {
4
+ const usage = [];
5
+ const billing = [];
6
+ const costEstimates = [];
7
+ for (const result of input.response.ResultsByTime ?? []) {
8
+ const period = requireTimePeriod(result.TimePeriod);
9
+ const serviceCosts = normalizeServiceCosts(result.Groups ?? []);
10
+ const totalMetric = readMetric(result.Total, UNBLENDED_COST_METRIC);
11
+ const currency = totalMetric === undefined
12
+ ? requireInferredCurrency(serviceCosts)
13
+ : requireMetricCurrency(totalMetric, "Cost Explorer total");
14
+ const amountMinor = totalMetric === undefined
15
+ ? sumMinorUnits(serviceCosts.map((serviceCost) => serviceCost.amountMinor))
16
+ : decimalAmountToMinorUnits(requireMetricAmount(totalMetric, "Cost Explorer total"));
17
+ const status = result.Estimated === true ? "estimated" : "final";
18
+ for (const serviceCost of serviceCosts) {
19
+ if (serviceCost.unit !== currency) {
20
+ throw new Error(`Cost Explorer currency mismatch for service ${serviceCost.service}.`);
21
+ }
22
+ usage.push({
23
+ provider: "aws",
24
+ collectedAt: input.collectedAt,
25
+ service: serviceCost.service,
26
+ metric: MONEYSIREN_COST_METRIC,
27
+ unit: serviceCost.unit,
28
+ value: serviceCost.amountMinor / 100,
29
+ });
30
+ }
31
+ billing.push({
32
+ provider: "aws",
33
+ collectedAt: input.collectedAt,
34
+ periodStart: period.Start,
35
+ periodEnd: period.End,
36
+ amountMinor,
37
+ currency,
38
+ status,
39
+ });
40
+ costEstimates.push({
41
+ provider: "aws",
42
+ collectedAt: input.collectedAt,
43
+ periodStart: period.Start,
44
+ periodEnd: period.End,
45
+ estimatedAmountMinor: amountMinor,
46
+ currency,
47
+ confidence: result.Estimated === true ? "medium" : "high",
48
+ });
49
+ }
50
+ return {
51
+ usage,
52
+ billing,
53
+ serviceHealth: [],
54
+ costEstimates,
55
+ };
56
+ }
57
+ function normalizeServiceCosts(groups) {
58
+ return groups.map((group) => {
59
+ const service = requireServiceName(group);
60
+ const context = `Cost Explorer service ${service}`;
61
+ const groupMetric = requireMetric(group.Metrics, UNBLENDED_COST_METRIC, context);
62
+ return {
63
+ service,
64
+ unit: requireMetricCurrency(groupMetric, context),
65
+ amountMinor: decimalAmountToMinorUnits(requireMetricAmount(groupMetric, context)),
66
+ };
67
+ });
68
+ }
69
+ function requireInferredCurrency(serviceCosts) {
70
+ const currencies = new Set(serviceCosts.map((serviceCost) => serviceCost.unit));
71
+ if (currencies.size === 0) {
72
+ throw new Error("Cost Explorer total is missing UnblendedCost and service groups cannot infer a currency.");
73
+ }
74
+ if (currencies.size > 1) {
75
+ throw new Error("Cost Explorer service groups use multiple currencies.");
76
+ }
77
+ return [...currencies][0] ?? "USD";
78
+ }
79
+ function sumMinorUnits(amounts) {
80
+ return amounts.reduce((total, amount) => total + amount, 0);
81
+ }
82
+ export function decimalAmountToMinorUnits(amount) {
83
+ const trimmed = amount.trim();
84
+ const sign = trimmed.startsWith("-") ? -1n : 1n;
85
+ const unsigned = sign === -1n ? trimmed.slice(1) : trimmed;
86
+ if (!/^\d+(\.\d+)?$/.test(unsigned)) {
87
+ throw new Error(`AWS currency amount must be a decimal string: ${amount}`);
88
+ }
89
+ const [wholePart = "0", fractionPart = ""] = unsigned.split(".");
90
+ const minorDigits = `${fractionPart}00`.slice(0, 2);
91
+ const roundingDigit = Number(`${fractionPart}000`.slice(2, 3));
92
+ let minorUnits = BigInt(wholePart) * 100n + BigInt(minorDigits);
93
+ if (roundingDigit >= 5) {
94
+ minorUnits += 1n;
95
+ }
96
+ const signedMinorUnits = minorUnits * sign;
97
+ const asNumber = Number(signedMinorUnits);
98
+ if (!Number.isSafeInteger(asNumber)) {
99
+ throw new Error(`AWS currency amount is outside safe integer range: ${amount}`);
100
+ }
101
+ return asNumber;
102
+ }
103
+ function requireTimePeriod(period) {
104
+ if (period?.Start === undefined || period.End === undefined) {
105
+ throw new Error("Cost Explorer result is missing TimePeriod Start or End.");
106
+ }
107
+ return {
108
+ Start: period.Start,
109
+ End: period.End,
110
+ };
111
+ }
112
+ function requireMetric(metrics, metricName, context) {
113
+ const metric = readMetric(metrics, metricName);
114
+ if (metric === undefined) {
115
+ throw new Error(`${context} is missing ${metricName}.`);
116
+ }
117
+ return metric;
118
+ }
119
+ function readMetric(metrics, metricName) {
120
+ return metrics?.[metricName];
121
+ }
122
+ function requireMetricAmount(metric, context) {
123
+ if (metric.Amount === undefined || metric.Amount.trim().length === 0) {
124
+ throw new Error(`${context} is missing amount.`);
125
+ }
126
+ return metric.Amount;
127
+ }
128
+ function requireMetricCurrency(metric, context) {
129
+ if (metric.Unit === undefined || metric.Unit.trim().length === 0) {
130
+ throw new Error(`${context} is missing currency unit.`);
131
+ }
132
+ return metric.Unit;
133
+ }
134
+ function requireServiceName(group) {
135
+ const service = group.Keys?.[0]?.trim();
136
+ if (service === undefined || service.length === 0) {
137
+ throw new Error("Cost Explorer service group is missing a service key.");
138
+ }
139
+ return service;
140
+ }
141
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1,6 @@
1
+ import type { AwsCostExplorerClientAdapter } from "./cost-explorer.js";
2
+ export interface CreateAwsSdkCostExplorerClientOptions {
3
+ region?: string;
4
+ }
5
+ export declare function createAwsSdkCostExplorerClient(options?: CreateAwsSdkCostExplorerClientOptions): AwsCostExplorerClientAdapter;
6
+ //# sourceMappingURL=sdk-client.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { CostExplorerClient, GetCostAndUsageCommand } from "@aws-sdk/client-cost-explorer";
2
+ const DEFAULT_COST_EXPLORER_REGION = "us-east-1";
3
+ export function createAwsSdkCostExplorerClient(options = {}) {
4
+ const region = options.region?.trim() || DEFAULT_COST_EXPLORER_REGION;
5
+ const client = new CostExplorerClient({ region });
6
+ return {
7
+ async send(command) {
8
+ if (command.name !== "GetCostAndUsage") {
9
+ throw new Error(`Unsupported AWS Cost Explorer command: ${command.name}`);
10
+ }
11
+ const input = {
12
+ ...command.input,
13
+ Metrics: [...command.input.Metrics],
14
+ GroupBy: command.input.GroupBy.map((group) => ({ ...group })),
15
+ };
16
+ const output = await client.send(new GetCostAndUsageCommand(input));
17
+ return output;
18
+ },
19
+ };
20
+ }
21
+ //# sourceMappingURL=sdk-client.js.map
@@ -0,0 +1,23 @@
1
+ import type { CloudflareBillingUsagePayload } from "./normalize.js";
2
+ export interface CloudflareBillingUsageClient {
3
+ fetchBillingUsage(): Promise<CloudflareBillingUsagePayload>;
4
+ }
5
+ export type CloudflareApiPath = `/accounts/${string}/billable/usage` | `/accounts/${string}/paygo-usage`;
6
+ export interface CloudflareApiRequest {
7
+ path: CloudflareApiPath;
8
+ query: Record<string, string | readonly string[]>;
9
+ headers: {
10
+ Authorization: string;
11
+ };
12
+ }
13
+ export interface CloudflareApiTransport {
14
+ getJson(request: CloudflareApiRequest): Promise<unknown>;
15
+ }
16
+ export interface CreateCloudflareBillingUsageClientOptions {
17
+ apiToken: string;
18
+ accountIds: readonly string[];
19
+ transport?: CloudflareApiTransport;
20
+ }
21
+ export declare function createCloudflareBillingUsageClient(options: CreateCloudflareBillingUsageClientOptions): CloudflareBillingUsageClient;
22
+ export declare function createStaticCloudflareBillingUsageClient(payload: CloudflareBillingUsagePayload): CloudflareBillingUsageClient;
23
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,107 @@
1
+ const CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
2
+ const defaultCloudflareApiTransport = {
3
+ async getJson(request) {
4
+ const url = new URL(request.path, CLOUDFLARE_API_BASE_URL);
5
+ for (const [key, value] of Object.entries(request.query)) {
6
+ if (isReadonlyStringArray(value)) {
7
+ for (const nestedValue of value) {
8
+ url.searchParams.append(key, nestedValue);
9
+ }
10
+ continue;
11
+ }
12
+ url.searchParams.set(key, value);
13
+ }
14
+ const response = await fetch(url, {
15
+ method: "GET",
16
+ headers: request.headers,
17
+ });
18
+ if (!response.ok) {
19
+ throw new Error(`Cloudflare API request failed with status ${response.status}.`);
20
+ }
21
+ return response.json();
22
+ },
23
+ };
24
+ export function createCloudflareBillingUsageClient(options) {
25
+ const apiToken = options.apiToken.trim();
26
+ if (apiToken.length === 0) {
27
+ throw new Error("CLOUDFLARE_API_TOKEN must not be blank.");
28
+ }
29
+ const accountIds = [...new Set(options.accountIds.map((accountId) => accountId.trim()).filter(Boolean))];
30
+ if (accountIds.length === 0) {
31
+ throw new Error("Cloudflare account IDs must not be empty.");
32
+ }
33
+ const transport = options.transport ?? defaultCloudflareApiTransport;
34
+ return {
35
+ async fetchBillingUsage() {
36
+ const headers = {
37
+ Authorization: `Bearer ${apiToken}`,
38
+ };
39
+ const billableUsage = [];
40
+ const paygoUsage = [];
41
+ const unavailable = [];
42
+ for (const accountId of accountIds) {
43
+ const encodedAccountId = encodeURIComponent(accountId);
44
+ const billableRecords = await tryFetchSurface(transport, {
45
+ path: `/accounts/${encodedAccountId}/billable/usage`,
46
+ query: {},
47
+ headers,
48
+ }, unavailable, "billable-usage", accountId);
49
+ if (billableRecords !== undefined) {
50
+ billableUsage.push(...billableRecords);
51
+ if (billableRecords.length > 0) {
52
+ continue;
53
+ }
54
+ }
55
+ const paygoRecords = await tryFetchSurface(transport, {
56
+ path: `/accounts/${encodedAccountId}/paygo-usage`,
57
+ query: {},
58
+ headers,
59
+ }, unavailable, "paygo-usage", accountId);
60
+ if (paygoRecords !== undefined) {
61
+ paygoUsage.push(...paygoRecords);
62
+ }
63
+ }
64
+ return {
65
+ billableUsage,
66
+ paygoUsage,
67
+ unavailable,
68
+ };
69
+ },
70
+ };
71
+ }
72
+ export function createStaticCloudflareBillingUsageClient(payload) {
73
+ return {
74
+ async fetchBillingUsage() {
75
+ return payload;
76
+ },
77
+ };
78
+ }
79
+ async function tryFetchSurface(transport, request, unavailable, surface, accountId) {
80
+ try {
81
+ return coerceCloudflareResultList(await transport.getJson(request));
82
+ }
83
+ catch {
84
+ unavailable.push({
85
+ surface,
86
+ accountId,
87
+ reason: "restricted-or-unavailable",
88
+ });
89
+ return undefined;
90
+ }
91
+ }
92
+ function coerceCloudflareResultList(value) {
93
+ if (Array.isArray(value)) {
94
+ return value;
95
+ }
96
+ if (isRecord(value) && Array.isArray(value.result)) {
97
+ return value.result;
98
+ }
99
+ return [];
100
+ }
101
+ function isReadonlyStringArray(value) {
102
+ return Array.isArray(value);
103
+ }
104
+ function isRecord(value) {
105
+ return typeof value === "object" && value !== null;
106
+ }
107
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,33 @@
1
+ import { type CloudflareNormalizedSnapshotBundle } from "./normalize.js";
2
+ export { createCloudflareBillingUsageClient, createStaticCloudflareBillingUsageClient, type CloudflareApiPath, type CloudflareApiRequest, type CloudflareApiTransport, type CloudflareBillingUsageClient, type CreateCloudflareBillingUsageClientOptions, } from "./client.js";
3
+ export { cloudflareAmountToMinorUnits, normalizeCloudflareBillingUsage, redactedCloudflareAccountId, type CloudflareAccount, type CloudflareBillableUsageRecord, type CloudflareBillingSnapshot, type CloudflareBillingUsagePayload, type CloudflareCostEstimate, type CloudflareNormalizedSnapshotBundle, type CloudflarePaygoUsageRecord, type CloudflareServiceHealthSnapshot, type CloudflareStatusSignal, type CloudflareUnavailableSurface, type CloudflareUsageSnapshot, type NormalizeCloudflareBillingUsageInput, } from "./normalize.js";
4
+ import type { CloudflareBillingUsageClient } from "./client.js";
5
+ export interface CloudflareProviderCollectionContext {
6
+ now(): Date;
7
+ }
8
+ export interface CloudflareProviderConnector {
9
+ kind: "cloudflare";
10
+ displayName: "Cloudflare Billing/Usage Experimental";
11
+ access: "read-only";
12
+ collect(context: CloudflareProviderCollectionContext): Promise<CloudflareProviderCollectionResult>;
13
+ }
14
+ export interface CloudflareProviderCollectionResult {
15
+ collectedAt: string;
16
+ status: "ok" | "partial" | "error";
17
+ snapshots: CloudflareNormalizedSnapshotBundle;
18
+ alerts: readonly CloudflareProviderAlert[];
19
+ errors?: readonly string[];
20
+ }
21
+ export interface CloudflareProviderAlert {
22
+ provider: "cloudflare";
23
+ createdAt: string;
24
+ severity: "warning";
25
+ category: "provider-sync";
26
+ title: "Cloudflare billing/usage sync failed" | "Cloudflare billable usage surface unavailable" | "Cloudflare PayGo usage surface unavailable" | "Cloudflare subscriptions surface unavailable";
27
+ message: "Cloudflare billing/usage request failed before normalized snapshots were collected." | "Cloudflare billable usage API was restricted or unavailable; normalized sync continued with available data." | "Cloudflare PayGo usage API was restricted or unavailable; normalized sync continued with available data." | "Cloudflare subscriptions API was restricted or unavailable; normalized sync continued with available data.";
28
+ }
29
+ export interface CloudflareBillingUsageConnectorOptions {
30
+ client: CloudflareBillingUsageClient;
31
+ }
32
+ export declare function createCloudflareBillingUsageConnector(options: CloudflareBillingUsageConnectorOptions): CloudflareProviderConnector;
33
+ //# sourceMappingURL=index.d.ts.map