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.
- package/AGENTS.md +179 -0
- package/README.md +474 -0
- package/cur.js +2 -0
- package/index.js +1 -0
- package/package.json +36 -0
- package/src/controllers/budget.controller.js +39 -0
- package/src/controllers/cost.controller.js +261 -0
- package/src/cur-discovery/cur-discovery.aws.js +82 -0
- package/src/cur-discovery/cur-discovery.repository.js +177 -0
- package/src/cur-discovery/cur-discovery.service.js +112 -0
- package/src/cur-discovery/cur-discovery.worker.js +57 -0
- package/src/cur-discovery/schema.js +48 -0
- package/src/lib/cost-utils.js +98 -0
- package/src/models/budget.model.js +26 -0
- package/src/models/cur-data-status.model.js +16 -0
- package/src/models/cur-ingestion.model.js +9 -0
- package/src/models/customer-aws-context.model.js +20 -0
- package/src/models/saas-cur-context.model.js +10 -0
- package/src/plugin.js +72 -0
- package/src/repositories/aws-onboarding.repository.js +134 -0
- package/src/repositories/budget-alert.repository.js +37 -0
- package/src/repositories/budget.repository.js +33 -0
- package/src/routes/index.js +22 -0
- package/src/schema/cost-budget.schema.js +9 -0
- package/src/services/aws-context.service.js +1 -0
- package/src/services/budget-alert.service.js +47 -0
- package/src/services/budget.service.js +32 -0
- package/src/services/cost-explorer.service.js +27 -0
- package/src/services/cur-provider.service.js +91 -0
- package/src/services/cur.service.js +355 -0
- package/src/services/customer-aws-context.service.js +62 -0
- 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
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
|
+
}
|