meyi-cost-server 1.0.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 (32) hide show
  1. package/AGENTS.md +179 -0
  2. package/README.md +474 -0
  3. package/cur.js +2 -0
  4. package/index.js +1 -0
  5. package/package.json +36 -0
  6. package/src/controllers/budget.controller.js +39 -0
  7. package/src/controllers/cost.controller.js +261 -0
  8. package/src/cur-discovery/cur-discovery.aws.js +82 -0
  9. package/src/cur-discovery/cur-discovery.repository.js +177 -0
  10. package/src/cur-discovery/cur-discovery.service.js +112 -0
  11. package/src/cur-discovery/cur-discovery.worker.js +57 -0
  12. package/src/cur-discovery/schema.js +48 -0
  13. package/src/lib/cost-utils.js +98 -0
  14. package/src/models/budget.model.js +26 -0
  15. package/src/models/cur-data-status.model.js +16 -0
  16. package/src/models/cur-ingestion.model.js +9 -0
  17. package/src/models/customer-aws-context.model.js +20 -0
  18. package/src/models/saas-cur-context.model.js +10 -0
  19. package/src/plugin.js +72 -0
  20. package/src/repositories/aws-onboarding.repository.js +134 -0
  21. package/src/repositories/budget-alert.repository.js +37 -0
  22. package/src/repositories/budget.repository.js +33 -0
  23. package/src/routes/index.js +22 -0
  24. package/src/schema/cost-budget.schema.js +9 -0
  25. package/src/services/aws-context.service.js +1 -0
  26. package/src/services/budget-alert.service.js +47 -0
  27. package/src/services/budget.service.js +32 -0
  28. package/src/services/cost-explorer.service.js +27 -0
  29. package/src/services/cur-provider.service.js +91 -0
  30. package/src/services/cur.service.js +355 -0
  31. package/src/services/customer-aws-context.service.js +62 -0
  32. package/src/services/saas-athena-context.service.js +54 -0
package/AGENTS.md ADDED
@@ -0,0 +1,179 @@
1
+ # Insight Cost Server Agent Guide
2
+
3
+ ## Scope
4
+
5
+ This package is a tenant-aware Express plugin for AWS cost overview, reports,
6
+ budgets, alert dismissals, Cost Explorer, and optional CUR/Athena data. It is
7
+ designed to be mounted inside a host backend; it does not own login, tenant
8
+ onboarding, or the host plugin registry.
9
+
10
+ Never weaken tenant scoping or expose AWS credentials. A management/payer role
11
+ may return organization and member-account costs. A member role normally only
12
+ returns the costs visible to that member account.
13
+
14
+ ## Folder structure
15
+
16
+ ```text
17
+ insight-cost-server/
18
+ |-- src/
19
+ | |-- controllers/ # Maps validated HTTP input to service calls
20
+ | |-- routes/ # Express paths and common error handling
21
+ | |-- models/ # Immutable customer, SaaS CUR, and status models
22
+ | |-- repositories/ # Tenant-scoped onboarding persistence reads
23
+ | |-- services/ # Cost Explorer, SaaS Athena, context, budgets
24
+ | |-- schema/ # Plugin-owned database tables and installation
25
+ | |-- lib/ # Stateless date, cost, and filter helpers
26
+ | `-- plugin.js # Dependency composition and lifecycle
27
+ |-- index.js # Public createInsightCost export
28
+ |-- cur.js # CUR compatibility export
29
+ |-- README.md # Runtime and AWS configuration
30
+ |-- package.json
31
+ `-- AGENTS.md
32
+ ```
33
+
34
+ Keep HTTP concerns in routes/controllers, AWS and database behavior in services,
35
+ schema creation in `schema`, and pure calculations in `lib`. `index.js` and
36
+ `cur.js` must remain thin compatibility entry points.
37
+
38
+ ## Request and data flow
39
+
40
+ ```mermaid
41
+ flowchart LR
42
+ Host[Host Express app] --> Auth[Host authentication middleware]
43
+ Auth --> Enabled[Tenant plugin-enabled check]
44
+ Enabled --> Router[/api/v1/cost router]
45
+ Router --> Controller[Cost controller]
46
+ Controller --> Customer[Customer AWS context service]
47
+ Customer -->|Customer role| CE[AWS Cost Explorer]
48
+ Controller --> Source{Configured source}
49
+ Source -->|CUR| SaaS[SaaS Athena context service]
50
+ SaaS --> Athena[Central tenant CUR partition]
51
+ Source -->|auto fallback| CE
52
+ Controller --> Budget[Budget and dismissal services]
53
+ Budget --> DB[(Tenant-scoped PostgreSQL)]
54
+ Athena --> Controller
55
+ CE --> Controller
56
+ ```
57
+
58
+ The plugin currently owns these route groups under `${apiBaseUri}/cost`:
59
+
60
+ - `GET /accounts`, `/data-status`, `/overview`, `/filter-options`, `/reports`, `/tags`
61
+ - `GET /budgets` and `POST /budgets`
62
+ - `DELETE /budgets/:id`
63
+ - `GET /budget-alert-dismissals` and `POST /budget-alert-dismissals`
64
+
65
+ ## Integrating with another Express application
66
+
67
+ Install from the registry after publishing, or use a local file dependency:
68
+
69
+ ```bash
70
+ npm install meyi-insight-cost-server
71
+ ```
72
+
73
+ ```js
74
+ import express from "express";
75
+ import { drizzle } from "drizzle-orm/node-postgres";
76
+ import { Pool } from "pg";
77
+ import { createInsightCost } from "meyi-insight-cost-server";
78
+
79
+ const app = express();
80
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
81
+ const db = drizzle(pool);
82
+
83
+ // Register host authentication before starting the cost plugin.
84
+ app.use("/api/v1/cost", authenticateRequest);
85
+
86
+ const costPlugin = createInsightCost({
87
+ app,
88
+ db,
89
+ apiBaseUri: "/api/v1",
90
+ logger: console,
91
+ });
92
+
93
+ await costPlugin.install();
94
+ await costPlugin.start();
95
+ ```
96
+
97
+ Call `install()` during plugin installation/migration so plugin-owned tables
98
+ exist, then call `start()` once per process to mount routes. Call `stop()` from
99
+ the host lifecycle when supported.
100
+
101
+ The host authentication layer must populate the authenticated user and tenant
102
+ context expected by the adapter (`req.user.id`, `req.user.tenant_id`, or the
103
+ approved tenant header). The host must verify that this plugin is enabled for
104
+ that tenant before requests reach its router. Do not trust an arbitrary tenant
105
+ ID without authenticated host validation.
106
+
107
+ The current Meyi Connect adapter is
108
+ `meyi-connect/backend/src/plugins/cost/index.mjs`. It supplies the Drizzle
109
+ database, authentication, tenant plugin checks, and host lifecycle wiring.
110
+
111
+ ## AWS and database configuration
112
+
113
+ Use onboarding role metadata in production. Environment credentials are an
114
+ explicit local-testing fallback and must stay backend-only. See `README.md` for
115
+ the supported Cost Explorer, assume-role, CUR/Athena, and local-test variables.
116
+
117
+ Important invariants:
118
+
119
+ - Resolve the customer AWS role per tenant for Cost Explorer/account metadata.
120
+ - Use only the SaaS runtime identity or `COST_CUR_ROLE_ARN` for Athena. Never
121
+ pass `CustomerAwsContext.credentials` into an Athena client.
122
+ - Keep central catalog configuration in SaaS environment settings. A customer
123
+ onboarding record may supply source-bucket and tenant-partition metadata, but
124
+ cannot override the central catalog unless the explicit legacy compatibility
125
+ switch is enabled.
126
+ - Scope account records, budgets, and alert dismissals by tenant.
127
+ - Preserve payer/organization access when showing linked member accounts.
128
+ - Keep CUR/Athena optional; `auto` may fall back to Cost Explorer, while `cur`
129
+ must surface CUR configuration/access errors.
130
+ - Do not send access keys, secret keys, or session tokens in API responses or
131
+ logs.
132
+ - Keep SQL parameterized and schema-qualified through the existing helpers.
133
+
134
+ ## Development and validation
135
+
136
+ This package is native Node.js ESM and has no transpilation build step:
137
+
138
+ ```bash
139
+ npm install
140
+ node --check index.js
141
+ node --check src/plugin.js
142
+ npm test
143
+ npm pack --dry-run
144
+ ```
145
+
146
+ For any change, syntax-check every changed JavaScript file and exercise route
147
+ creation with an Express app and a stub database. Changes to AWS queries should
148
+ also test empty results, missing permissions, assumed-role failures, and both
149
+ Cost Explorer and CUR source selection. Never use real customer credentials in
150
+ automated tests.
151
+
152
+ When packaging in Docker, copy `index.js`, `cur.js`, and the complete `src`
153
+ directory. Copying only the entry point will produce runtime module-not-found
154
+ errors after the package was split into layers.
155
+
156
+ ## Version and npm publishing
157
+
158
+ Publishing changes external state. Only publish when explicitly authorized:
159
+
160
+ ```bash
161
+ npm version patch
162
+ npm pack --dry-run
163
+ npm publish --access public
164
+ ```
165
+
166
+ Confirm that the dry-run archive contains both entry points, all `src` layers,
167
+ `README.md`, and `AGENTS.md`. Do not publish `.env` files, AWS credentials,
168
+ database dumps, logs, screenshots, or host application source.
169
+
170
+ ## Change checklist
171
+
172
+ 1. Preserve `createInsightCost({ app, db, apiBaseUri, logger })` and its
173
+ `install`, `start`, and `stop` lifecycle.
174
+ 2. Add or change routes in the router, controller, service, and shared types as
175
+ appropriate; do not place the whole feature in one file.
176
+ 3. Validate tenant isolation for every query and mutation.
177
+ 4. Keep response shapes compatible with `meyi-cost-ui`, or update both packages
178
+ and the host adapter in the same release.
179
+ 5. Run syntax checks and package dry-run, then build the consuming host backend.
package/README.md ADDED
@@ -0,0 +1,474 @@
1
+ # Meyi Insight Cost Server
2
+
3
+ Tenant-aware Express plugin that provides AWS cost overview, reports, dynamic
4
+ filters, budgets, and budget-alert dismissals. It supports AWS Cost Explorer and
5
+ optional centralized Cost and Usage Report (CUR) queries through Athena. The
6
+ customer role and SaaS Athena identity are deliberately separate security
7
+ boundaries.
8
+
9
+ The package is designed to be mounted by a host backend. The host owns login,
10
+ token verification, tenant/plugin enablement, PostgreSQL connection creation,
11
+ and AWS onboarding. The plugin owns cost routes and its budget-related tables.
12
+
13
+ ## Package contract
14
+
15
+ - Package name: `meyi-insight-cost-server`
16
+ - Runtime: Node.js 20 or newer, ESM
17
+ - Framework: Express 4
18
+ - Database API: injected Drizzle PostgreSQL instance
19
+ - Peer dependency: `pg >= 8`
20
+ - Default route: `/api/v1/cost`
21
+ - Public factory: `createInsightCost({ app, db, apiBaseUri, logger })`
22
+
23
+ ## Features
24
+
25
+ - Tenant-isolated payer/organization and member-account cost views
26
+ - Monthly and yearly overview data
27
+ - Service, account, region, resource, and cost-allocation-tag reports
28
+ - Dynamic service, region, linked-account, and tag filter options
29
+ - Standard and comparison report data used by `meyi-cost-ui`
30
+ - CUR/Athena active-resource counts and detailed cost data
31
+ - Cost Explorer fallback when CUR is unavailable in `auto` mode
32
+ - Tenant-scoped budget rules
33
+ - User-, tenant-, month-, and status-scoped budget-alert dismissals
34
+
35
+ Budgets are application rules stored in PostgreSQL; they are not AWS Budgets
36
+ resources. The consuming application evaluates them against current cost and
37
+ shows the notification UI.
38
+
39
+ ## Architecture and request flow
40
+
41
+ ```mermaid
42
+ flowchart LR
43
+ Host[External Express application] --> Auth[Authentication]
44
+ Auth --> Enabled[Tenant plugin check]
45
+ Enabled --> Router[/api/v1/cost]
46
+ Router --> Controller[Cost controller]
47
+ Controller --> Customer[Customer AWS context]
48
+ Customer -->|Customer role| CE[AWS Cost Explorer]
49
+ Controller --> Source{Data source}
50
+ Source -->|CUR| SaaS[SaaS Athena context]
51
+ SaaS --> Athena[Central tenant-partitioned CUR]
52
+ Source -->|auto fallback| CE
53
+ Controller --> Budget[Budget services]
54
+ Budget --> DB[(PostgreSQL)]
55
+ ```
56
+
57
+ ```text
58
+ src/
59
+ |-- controllers/ HTTP-to-service orchestration
60
+ |-- routes/ Express routes and error handling
61
+ |-- models/ Normalized immutable domain models
62
+ |-- repositories/ Tenant onboarding persistence access
63
+ |-- services/ AWS identities, Cost Explorer, CUR, budgets
64
+ |-- schema/ Budget and dismissal table installation
65
+ |-- lib/ Cost, date, filter, and SQL helpers
66
+ `-- plugin.js Dependency composition and lifecycle
67
+ ```
68
+
69
+ ## Local development and validation
70
+
71
+ Requirements:
72
+
73
+ - Node.js 20 or newer
74
+ - npm
75
+ - PostgreSQL available to the host used for integration testing
76
+
77
+ Install dependencies:
78
+
79
+ ```bash
80
+ npm install
81
+ ```
82
+
83
+ This package is native JavaScript ESM and has no transpilation build step.
84
+ Validate syntax and package contents with:
85
+
86
+ ```bash
87
+ node --check index.js
88
+ node --check cur.js
89
+ node --check src/plugin.js
90
+ npm pack --dry-run
91
+ ```
92
+
93
+ When files below `src` change, syntax-check every changed `.js` file. The npm
94
+ archive must include `index.js`, `cur.js`, the complete `src` directory,
95
+ `README.md`, and `AGENTS.md`.
96
+
97
+ For a local package installation test:
98
+
99
+ ```bash
100
+ npm pack
101
+ npm install /path/to/meyi-insight-cost-server-1.0.0.tgz
102
+ ```
103
+
104
+ ## Publish to npm
105
+
106
+ Publishing changes the external registry. Run these commands only after release
107
+ authorization:
108
+
109
+ ```bash
110
+ npm login
111
+ npm whoami
112
+ npm version patch
113
+ npm pack --dry-run
114
+ npm publish --access public
115
+ ```
116
+
117
+ Use `npm version minor` or `npm version major` when appropriate. This server
118
+ package currently has no compile step or `prepublishOnly` script, so syntax and
119
+ integration validation must be completed before publishing.
120
+
121
+ After publishing:
122
+
123
+ ```bash
124
+ npm view meyi-insight-cost-server version
125
+ npm install meyi-insight-cost-server@<published-version>
126
+ ```
127
+
128
+ ## Integrate with an external Express application
129
+
130
+ Install the package and its PostgreSQL peer dependency:
131
+
132
+ ```bash
133
+ npm install meyi-insight-cost-server pg
134
+ ```
135
+
136
+ For local development, use a file dependency:
137
+
138
+ ```json
139
+ {
140
+ "dependencies": {
141
+ "meyi-insight-cost-server": "file:../../meyi-market-places/insight-cost-server"
142
+ }
143
+ }
144
+ ```
145
+
146
+ Create the plugin using the host's Express app and Drizzle database:
147
+
148
+ ```js
149
+ import express from "express";
150
+ import { drizzle } from "drizzle-orm/node-postgres";
151
+ import { Pool } from "pg";
152
+ import { createInsightCost } from "meyi-insight-cost-server";
153
+
154
+ const app = express();
155
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
156
+ const db = drizzle(pool);
157
+
158
+ app.use(express.json());
159
+
160
+ // These host middlewares must be registered before plugin.start().
161
+ app.use("/api/v1/cost", authenticateRequest);
162
+ app.use("/api/v1/cost", requireCostPluginForTenant);
163
+
164
+ const costPlugin = createInsightCost({
165
+ app,
166
+ db,
167
+ apiBaseUri: "/api/v1",
168
+ logger: console,
169
+ });
170
+
171
+ await costPlugin.install();
172
+ await costPlugin.start();
173
+ ```
174
+
175
+ Lifecycle behavior:
176
+
177
+ - `install()` creates or verifies plugin-owned budget and dismissal tables.
178
+ - `start()` mounts the router once at `${apiBaseUri}/cost`.
179
+ - `stop()` is currently a no-op but is available for host lifecycle symmetry.
180
+ - `router` is also returned for hosts that need custom mounting.
181
+
182
+ `app` is optional only when the host mounts the returned `router` itself. `db`
183
+ is required.
184
+
185
+ ## Host authentication and tenant contract
186
+
187
+ Before a request reaches the plugin, the host should authenticate it and set:
188
+
189
+ ```js
190
+ req.user = {
191
+ id: "authenticated-user-id",
192
+ tenant_id: "authenticated-tenant-id",
193
+ };
194
+ ```
195
+
196
+ The tenant resolver checks `req.user.tenant_id`, then `req.user.tenantId`, then
197
+ `x-tenant-id`, then `DEFAULT_TENANT_ID`. In production, prefer an authenticated
198
+ `req.user` tenant and do not accept an untrusted tenant header.
199
+
200
+ The Marketplace onboarding integration loads tenant access from:
201
+
202
+ - `<DB_SCHEMA>.aws_connections`
203
+ - `<DB_SCHEMA>.aws_accounts`
204
+ - `<DB_SCHEMA>.cost_cur_config`
205
+ - `<DB_SCHEMA>.cost_cur_discovery_jobs`
206
+ - `<DB_SCHEMA>.cost_cur_discovery_job_logs`
207
+
208
+ Only a connected row containing `Cost` with verified Cost access is accepted.
209
+ Connection metadata supplies the payer role and external ID; the dedicated Cost
210
+ CUR table supplies the tenant partition and delivery/readiness state. The
211
+ customer role is used for Cost Explorer and organization/account-name access
212
+ only. It is never passed to the central Athena client.
213
+
214
+ CUR discovery runs as one shared scheduler with independent tenant job rows.
215
+ Each job checks only its configured S3 prefix, finds a tenant-safe Glue table,
216
+ and verifies the tenant partition through Athena. A successful job becomes
217
+ `READY` and is no longer scheduled. Job events are retained separately in
218
+ `cost_cur_discovery_job_logs` and also include tenant and job IDs in application
219
+ logs.
220
+
221
+ ## Data-source behavior
222
+
223
+ ### Cost Explorer
224
+
225
+ Cost Explorer is the default fallback and returns gross positive unblended
226
+ charges using the supported AWS record types. It supplies overview, trends,
227
+ accounts, services, regions, tags, filters, forecasts, and report groupings.
228
+
229
+ Cost Explorer does not provide the exact active-resource total used by this
230
+ plugin. That metric requires CUR resource IDs through Athena.
231
+
232
+ ### Central CUR through SaaS Athena
233
+
234
+ CUR applies `line_item_unblended_cost > 0` for displayed gross cost and can also
235
+ calculate net cost, credits/adjustments, active resources, detailed resource
236
+ rows, account/region relationships, and allocation-tag data.
237
+
238
+ The SaaS runtime identity, or the dedicated role configured by
239
+ `COST_CUR_ROLE_ARN`, queries the SaaS-owned Athena/Glue/S3 resources. Customer
240
+ onboarding creates a CUR 2.0 Data Export directly into a tenant prefix in the
241
+ Meyi-owned bucket; customer AWS credentials are not reused for Athena.
242
+
243
+ In `auto` mode, CUR is used when its database and Athena output location are
244
+ configured. A CUR failure is logged and the request falls back to Cost Explorer.
245
+ In `cur` mode, CUR is mandatory and failures are returned to the caller.
246
+
247
+ ## Environment variables
248
+
249
+ All variables below belong to the **backend runtime**. Never place AWS or
250
+ database secrets in `meyi-cost-ui`, a browser environment, source control, or a
251
+ Docker image layer. Supply them at container/process runtime.
252
+
253
+ ### Database and tenant variables
254
+
255
+ | Variable | Default | Required | Purpose |
256
+ | --- | --- | --- | --- |
257
+ | `DB_SCHEMA` | `meyiconnect` | No | PostgreSQL schema containing onboarding and plugin-owned tables. |
258
+ | `DEFAULT_TENANT_ID` | `default` | No | Final tenant fallback when authenticated request context is absent. Prefer authenticated tenant context in production. |
259
+ | `DATABASE_URL` | None | Host-specific | Not read by the package directly. The host commonly uses it to create the injected `db` connection. |
260
+
261
+ ### Cost Explorer and assume-role variables
262
+
263
+ | Variable | Default | Required | Purpose |
264
+ | --- | --- | --- | --- |
265
+ | `COST_EXPLORER_ROLE_ARN` | None | No | Fallback AWS role to assume when onboarding metadata has no payer/cross-account role. |
266
+ | `COST_EXPLORER_EXTERNAL_ID` | None | No | External ID sent to STS for the fallback role. Onboarding `externalId` takes precedence. |
267
+ | `COST_EXPLORER_ALLOW_ENV_CREDENTIALS` | `false` | Local testing only | Explicitly enables the process-wide AWS access-key fallback. Both access key and secret key must exist. |
268
+ | `COST_EXPLORER_PREFER_ENV_CREDENTIALS` | `false` | No | When environment credentials are allowed and present, bypasses a configured role and uses those credentials. Useful only for controlled local testing. |
269
+ | `COST_EXPLORER_ALLOW_RUNTIME_IDENTITY` | `false` | No | Explicitly lets Cost Explorer use the host runtime identity. Keep disabled in multi-tenant SaaS unless that identity is intentionally the customer identity. |
270
+ | `COST_EXPLORER_ACCOUNT_IDS` | Empty | No | Comma-separated linked-account IDs used as local test accounts when onboarding returned none. |
271
+ | `AWS_ACCESS_KEY_ID` | None | Local fallback | AWS SDK access key. Effective for this fallback only when `COST_EXPLORER_ALLOW_ENV_CREDENTIALS=true`. |
272
+ | `AWS_SECRET_ACCESS_KEY` | None | Local fallback | AWS SDK secret key paired with `AWS_ACCESS_KEY_ID`. |
273
+ | `AWS_SESSION_TOKEN` | None | Temporary credentials only | Session token required when the access key was issued by STS/SSO. |
274
+
275
+ `ALLOW` and `PREFER` are intentionally different:
276
+
277
+ - `ALLOW=true` permits environment credentials when no usable assumed role is
278
+ selected.
279
+ - `PREFER=true` additionally tells the plugin to bypass an available role and
280
+ use environment credentials instead. It has no effect unless `ALLOW=true`
281
+ and both access-key variables are present.
282
+
283
+ Production tenants should use per-tenant onboarding role metadata. Environment
284
+ credentials are process-wide and therefore unsuitable as the normal credential
285
+ source for a multi-tenant service.
286
+
287
+ When customer environment credentials are enabled, central Athena is disabled
288
+ unless `COST_CUR_ROLE_ARN` is also configured. This prevents the AWS SDK default
289
+ credential chain from accidentally using a customer's local test key against
290
+ SaaS Athena resources.
291
+
292
+ ### CUR/Athena variables
293
+
294
+ | Variable | Default | Required | Purpose |
295
+ | --- | --- | --- | --- |
296
+ | `COST_DATA_SOURCE` | `auto` | No | `auto` uses CUR when configured and falls back to Cost Explorer; `cur` requires CUR; `cost-explorer` disables CUR. |
297
+ | `COST_CUR_DATABASE` | Empty | CUR | Athena/Glue database containing the CUR table. |
298
+ | `COST_CUR_TABLE` | Empty | No | Optional manual Glue table override. When empty, the tenant discovery job supplies the verified table. |
299
+ | `COST_CUR_OUTPUT_LOCATION` | Empty | CUR | S3 URI where Athena writes query results. |
300
+ | `COST_CUR_REGION` | `AWS_REGION` or `us-east-1` | No | Region for the Athena client. |
301
+ | `COST_CUR_WORKGROUP` | `meyi-cost` | No | Meyi-owned Athena workgroup used for every query. |
302
+ | `AWS_REGION` | `us-east-1` for CUR fallback | No | Used as the CUR region fallback; Cost Explorer itself is created in `us-east-1`. |
303
+ | `COST_CUR_TENANT_COLUMN` | `tenant_id` | No | CUR column used to isolate the Hive-style tenant partition. |
304
+ | `COST_CUR_TENANT_PARTITION` | Request tenant ID | No | Overrides the partition value. Avoid a global override in multi-tenant production unless every request is intentionally mapped to that partition. |
305
+ | `COST_CUR_MAX_ROWS` | `1000` | No | Resource-report row cap, clamped between 1 and 5000. |
306
+ | `COST_CUR_ROLE_ARN` | Empty | No | Dedicated SaaS-side role assumed only for central Athena/Glue/S3 queries. |
307
+ | `COST_CUR_EXTERNAL_ID` | Empty | No | External ID for the dedicated SaaS CUR role. |
308
+ | `COST_CUR_ALLOW_TENANT_CATALOG` | `false` | No | Compatibility switch allowing tenant metadata to override the central catalog. Keep `false` for centralized SaaS CUR. |
309
+ | `COST_CUR_INGESTION_MODE` | `central` | No | Describes the CUR ingestion contract returned by readiness status. |
310
+ | `COST_CUR_STATUS_CACHE_MS` | `300000` | No | Cache duration for the live tenant CUR readiness query. |
311
+ | `COST_CUR_DISCOVERY_ENABLED` | `true` | No | Enables tenant-specific background CUR discovery. |
312
+ | `COST_CUR_DISCOVERY_INTERVAL_MS` | `3600000` | No | Retry interval for non-ready tenant jobs; minimum 60000 ms. |
313
+ | `COST_CUR_DISCOVERY_BATCH_SIZE` | `25` | No | Maximum due tenant jobs selected per scheduler tick. |
314
+
315
+ Central catalog settings come from the SaaS backend environment. Only the
316
+ tenant partition and customer source metadata come from onboarding by default.
317
+ Legacy per-tenant catalog overrides are accepted only when
318
+ `COST_CUR_ALLOW_TENANT_CATALOG=true`.
319
+
320
+ ### Example: production role-based Cost Explorer
321
+
322
+ The preferred production setup stores the payer/cross-account role and external
323
+ ID during tenant onboarding. Only shared database settings may be required:
324
+
325
+ ```env
326
+ DB_SCHEMA=meyiconnect
327
+ DEFAULT_TENANT_ID=default
328
+ COST_DATA_SOURCE=cost-explorer
329
+ ```
330
+
331
+ The host runtime identity must be able to call `sts:AssumeRole`, and the target
332
+ role trust policy must trust that identity.
333
+
334
+ ### Example: controlled local testing before onboarding
335
+
336
+ ```env
337
+ DB_SCHEMA=meyiconnect
338
+ DEFAULT_TENANT_ID=default
339
+ COST_DATA_SOURCE=cost-explorer
340
+ COST_EXPLORER_ALLOW_ENV_CREDENTIALS=true
341
+ COST_EXPLORER_PREFER_ENV_CREDENTIALS=true
342
+ AWS_ACCESS_KEY_ID=REPLACE_ME
343
+ AWS_SECRET_ACCESS_KEY=REPLACE_ME
344
+ AWS_SESSION_TOKEN=REPLACE_IF_TEMPORARY
345
+ COST_EXPLORER_ACCOUNT_IDS=111111111111,222222222222
346
+ ```
347
+
348
+ Omit `COST_EXPLORER_ACCOUNT_IDS` to query all linked accounts visible to the
349
+ credential and derive the accessible accounts from Cost Explorer results.
350
+
351
+ ### Example: centralized CUR/Athena
352
+
353
+ ```env
354
+ COST_DATA_SOURCE=auto
355
+ COST_CUR_DATABASE=meyi_central_cur
356
+ COST_CUR_TABLE=
357
+ COST_CUR_OUTPUT_LOCATION=s3://meyi-saas-athena-results/
358
+ COST_CUR_REGION=us-east-1
359
+ COST_CUR_WORKGROUP=meyi-cost
360
+ COST_CUR_TENANT_COLUMN=tenant_id
361
+ COST_CUR_MAX_ROWS=1000
362
+ COST_CUR_ROLE_ARN=arn:aws:iam::SAAS_ACCOUNT_ID:role/meyi-cur-query
363
+ COST_CUR_ALLOW_TENANT_CATALOG=false
364
+ COST_CUR_INGESTION_MODE=central
365
+ COST_CUR_DISCOVERY_ENABLED=true
366
+ COST_CUR_DISCOVERY_INTERVAL_MS=3600000
367
+ ```
368
+
369
+ ## Required AWS read-only access
370
+
371
+ Cost Explorer mode needs access to the operations used by the plugin, including:
372
+
373
+ - `ce:GetCostAndUsage`
374
+ - `ce:GetCostForecast`
375
+ - `ce:GetDimensionValues`
376
+ - `ce:GetTags`
377
+
378
+ Assume-role mode also needs `sts:AssumeRole` on the target role, plus a matching
379
+ target-role trust policy and external ID when configured.
380
+
381
+ CUR mode additionally needs the **SaaS identity** to query Athena, read the Glue
382
+ catalog and central CUR bucket, and write to the Athena results bucket. The
383
+ customer onboarding role needs only the customer-side permissions required by
384
+ the chosen CUR export/DataSync design. These policies must remain separate.
385
+
386
+ A payer/management-account role can expose organization-wide linked-account
387
+ costs. A member-account role normally exposes only the costs AWS makes visible
388
+ to that member. Separate member-account roles are not required merely to group
389
+ organization cost by linked account when the payer role already has that data.
390
+
391
+ ## HTTP API
392
+
393
+ Routes are mounted below `${apiBaseUri}/cost`:
394
+
395
+ | Method | Path | Purpose |
396
+ | --- | --- | --- |
397
+ | `GET` | `/accounts` | Selected or discovered AWS accounts. |
398
+ | `GET` | `/data-status` | Central CUR configuration, ingestion, readiness, row count, and freshness without exposing secrets. |
399
+ | `GET` | `/overview` | Totals, trends, accounts, services, regions, and active resources when CUR is available. |
400
+ | `GET` | `/filter-options` | Dynamic report filter values. |
401
+ | `GET` | `/reports` | Standard/comparison source data grouped by service, account, region, resource, or tag. |
402
+ | `GET` | `/tags` | Available cost-allocation tags. |
403
+ | `GET` | `/budgets` | Tenant budget rules. |
404
+ | `POST` | `/budgets` | Create a tenant budget rule. |
405
+ | `DELETE` | `/budgets/:id` | Delete a tenant budget rule. |
406
+ | `GET` | `/budget-alert-dismissals` | Current user's persisted dismissals. |
407
+ | `POST` | `/budget-alert-dismissals` | Dismiss one budget/month/status alert. |
408
+
409
+ ## Docker packaging
410
+
411
+ When embedding the local package in a Docker build, copy all published package
412
+ files, not only `index.js`:
413
+
414
+ ```text
415
+ index.js
416
+ cur.js
417
+ src/
418
+ package.json
419
+ README.md
420
+ AGENTS.md
421
+ ```
422
+
423
+ Environment files should not be copied into the image. Pass required variables
424
+ with the deployment platform, Docker Compose `env_file`, or secret manager at
425
+ runtime.
426
+
427
+ ## Reference Meyi Connect integration
428
+
429
+ The current host adapter is:
430
+
431
+ `meyi-connect/backend/src/plugins/cost/index.mjs`
432
+
433
+ It supplies the shared Drizzle database, authenticated token middleware,
434
+ tenant-level plugin enablement, and plugin lifecycle wiring.
435
+
436
+ ## Consumer validation checklist
437
+
438
+ 1. Run server syntax checks and `npm pack --dry-run`.
439
+ 2. Install or refresh the package in the host backend.
440
+ 3. Run `install()` against a disposable/test database.
441
+ 4. Verify unauthenticated and disabled-tenant requests are rejected by the host.
442
+ 5. Verify two tenants cannot access each other's accounts, budgets, or alert
443
+ dismissals.
444
+ 6. Test Cost Explorer, CUR `auto` fallback, and mandatory `cur` failure modes.
445
+ 7. Verify payer and member-account visibility matches the AWS role used.
446
+ 8. Build and start the consuming host backend before creating a Docker image.
447
+
448
+ ## Central CUR ingestion contract
449
+
450
+ The package implements the application side of the FinOps-style design: strict
451
+ credential separation, tenant-partitioned Athena queries, Cost Explorer
452
+ fallback, and live readiness/freshness reporting. The AWS data-transfer plane
453
+ must deliver each customer's CUR objects into the central CUR layout consumed
454
+ by Glue. That is deployment infrastructure, not an HTTP request made with a
455
+ customer access key.
456
+
457
+ Onboarding may persist these non-secret fields in session `meta`:
458
+
459
+ ```json
460
+ {
461
+ "curSourceBucket": "customer-cur-bucket",
462
+ "curSourcePrefix": "reports/meyi/",
463
+ "curSourceRegion": "us-east-1",
464
+ "curTenantPartition": "authenticated-tenant-id",
465
+ "curIngestionMode": "central"
466
+ }
467
+ ```
468
+
469
+ The transfer pipeline must copy or replicate only that source prefix into the
470
+ SaaS central location for the matching tenant partition. `GET /data-status`
471
+ then verifies that Athena can see rows for that partition. Until rows arrive,
472
+ `COST_DATA_SOURCE=auto` safely uses Cost Explorer, so cost data remains
473
+ available while detailed CUR-only metrics such as active resources wait for
474
+ ingestion.
package/cur.js ADDED
@@ -0,0 +1,2 @@
1
+ // Backwards-compatible export for existing consumers. New code uses the layered service.
2
+ export * from "./src/services/cur.service.js";
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createInsightCost, createInsightCost as default } from "./src/plugin.js";
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "meyi-cost-server",
3
+ "version": "1.0.0",
4
+ "description": "Tenant-aware AWS Cost Explorer plugin server for MeyiConnect",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "cur.js",
16
+ "src",
17
+ "README.md",
18
+ "AGENTS.md"
19
+ ],
20
+ "dependencies": {
21
+ "@aws-sdk/client-athena": "^3.850.0",
22
+ "@aws-sdk/client-cost-explorer": "^3.850.0",
23
+ "@aws-sdk/client-glue": "^3.850.0",
24
+ "@aws-sdk/client-s3": "^3.850.0",
25
+ "@aws-sdk/client-sts": "^3.850.0",
26
+ "@aws-sdk/credential-providers": "^3.850.0",
27
+ "drizzle-orm": "^0.44.7",
28
+ "express": "^4.21.1"
29
+ },
30
+ "peerDependencies": {
31
+ "pg": ">=8"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ }
36
+ }