@prodverdict/engine 0.0.1
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/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/parse.d.ts +4 -0
- package/dist/config/parse.d.ts.map +1 -0
- package/dist/config/parse.js +40 -0
- package/dist/config/parse.js.map +1 -0
- package/dist/config/schema.d.ts +468 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +66 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/connectors/fixture.d.ts +5 -0
- package/dist/connectors/fixture.d.ts.map +1 -0
- package/dist/connectors/fixture.js +16 -0
- package/dist/connectors/fixture.js.map +1 -0
- package/dist/connectors/index.d.ts +8 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +6 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/load-fixtures.d.ts +9 -0
- package/dist/connectors/load-fixtures.d.ts.map +1 -0
- package/dist/connectors/load-fixtures.js +27 -0
- package/dist/connectors/load-fixtures.js.map +1 -0
- package/dist/connectors/postgres-live.d.ts +4 -0
- package/dist/connectors/postgres-live.d.ts.map +1 -0
- package/dist/connectors/postgres-live.js +51 -0
- package/dist/connectors/postgres-live.js.map +1 -0
- package/dist/connectors/sql-identifiers.d.ts +3 -0
- package/dist/connectors/sql-identifiers.d.ts.map +1 -0
- package/dist/connectors/sql-identifiers.js +13 -0
- package/dist/connectors/sql-identifiers.js.map +1 -0
- package/dist/connectors/stripe-live.d.ts +3 -0
- package/dist/connectors/stripe-live.d.ts.map +1 -0
- package/dist/connectors/stripe-live.js +32 -0
- package/dist/connectors/stripe-live.js.map +1 -0
- package/dist/connectors/types.d.ts +23 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +2 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/evaluators/access.d.ts +9 -0
- package/dist/evaluators/access.d.ts.map +1 -0
- package/dist/evaluators/access.js +132 -0
- package/dist/evaluators/access.js.map +1 -0
- package/dist/evaluators/config.d.ts +22 -0
- package/dist/evaluators/config.d.ts.map +1 -0
- package/dist/evaluators/config.js +191 -0
- package/dist/evaluators/config.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +3 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +8 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +39 -0
- package/src/config/index.ts +2 -0
- package/src/config/parse.test.ts +63 -0
- package/src/config/parse.ts +48 -0
- package/src/config/schema.ts +83 -0
- package/src/connectors/fixture.ts +18 -0
- package/src/connectors/index.ts +7 -0
- package/src/connectors/load-fixtures.test.ts +19 -0
- package/src/connectors/load-fixtures.ts +45 -0
- package/src/connectors/postgres-live.ts +61 -0
- package/src/connectors/sql-identifiers.test.ts +14 -0
- package/src/connectors/sql-identifiers.ts +16 -0
- package/src/connectors/stripe-live.ts +38 -0
- package/src/connectors/types.ts +25 -0
- package/src/evaluators/access.test.ts +206 -0
- package/src/evaluators/access.ts +159 -0
- package/src/evaluators/config.test.ts +266 -0
- package/src/evaluators/config.ts +213 -0
- package/src/index.ts +12 -0
- package/src/types.ts +27 -0
- package/src/verdict.test.ts +29 -0
- package/src/verdict.ts +7 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createLiveStripeReader } from './stripe-live.js';
|
|
2
|
+
export { createLivePostgresReader } from './postgres-live.js';
|
|
3
|
+
export { createFixtureStripeReader, createFixtureDatabaseReader } from './fixture.js';
|
|
4
|
+
export { loadFixtureSubscriptions, loadFixtureUsers, defaultFixturePaths } from './load-fixtures.js';
|
|
5
|
+
export { assertSqlIdentifier, assertSqlIdentifiers } from './sql-identifiers.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/connectors/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAErG,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StripeSubscription, AppUser } from './types.js';
|
|
2
|
+
export interface FixturePaths {
|
|
3
|
+
stripeSubscriptions: string;
|
|
4
|
+
dbUsers: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadFixtureSubscriptions(filePath: string): StripeSubscription[];
|
|
7
|
+
export declare function loadFixtureUsers(filePath: string): AppUser[];
|
|
8
|
+
export declare function defaultFixturePaths(fixturesDir: string): FixturePaths;
|
|
9
|
+
//# sourceMappingURL=load-fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-fixtures.d.ts","sourceRoot":"","sources":["../../src/connectors/load-fixtures.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAa/E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAa5D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CAKrE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
export function loadFixtureSubscriptions(filePath) {
|
|
4
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
5
|
+
return raw.map((item) => ({
|
|
6
|
+
id: item.id,
|
|
7
|
+
customerId: item.customerId,
|
|
8
|
+
status: item.status,
|
|
9
|
+
priceIds: item.priceIds,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export function loadFixtureUsers(filePath) {
|
|
13
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
14
|
+
return raw.map((row) => ({
|
|
15
|
+
id: row.id,
|
|
16
|
+
stripeCustomerId: row.stripe_customer_id,
|
|
17
|
+
hasPaidAccess: row.has_paid_access,
|
|
18
|
+
plan: row.plan,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export function defaultFixturePaths(fixturesDir) {
|
|
22
|
+
return {
|
|
23
|
+
stripeSubscriptions: join(fixturesDir, 'stripe', 'subscriptions.json'),
|
|
24
|
+
dbUsers: join(fixturesDir, 'db', 'users.json'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=load-fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-fixtures.js","sourceRoot":"","sources":["../../src/connectors/load-fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAQ5B,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAKnD,CAAC;IACH,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACxB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;KACxB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAKnD,CAAC;IACH,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACvB,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,gBAAgB,EAAE,GAAG,CAAC,kBAAkB;QACxC,aAAa,EAAE,GAAG,CAAC,eAAe;QAClC,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,OAAO;QACL,mBAAmB,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,oBAAoB,CAAC;QACtE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,YAAY,CAAC;KAC/C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-live.d.ts","sourceRoot":"","sources":["../../src/connectors/postgres-live.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAShE,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,oBAAoB,GAAG,cAAc,CAiDlF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { assertSqlIdentifier, assertSqlIdentifiers } from './sql-identifiers.js';
|
|
3
|
+
function makeConnectorError(message) {
|
|
4
|
+
const err = new Error(message);
|
|
5
|
+
err.code = 'CONNECTOR_ERROR';
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
export function createLivePostgresReader(cfg) {
|
|
9
|
+
const connectionString = process.env[cfg.database.url_env];
|
|
10
|
+
if (!connectionString) {
|
|
11
|
+
throw makeConnectorError(`Missing required env var "${cfg.database.url_env}" for database connector. ` +
|
|
12
|
+
'Provide a read-only database connection string.');
|
|
13
|
+
}
|
|
14
|
+
const cols = cfg.database.columns;
|
|
15
|
+
const table = cfg.database.users_table;
|
|
16
|
+
assertSqlIdentifier(table, 'database.users_table');
|
|
17
|
+
assertSqlIdentifiers(cols, 'database.columns');
|
|
18
|
+
const pool = new pg.Pool({ connectionString, max: 2 });
|
|
19
|
+
let closed = false;
|
|
20
|
+
return {
|
|
21
|
+
async listUsers() {
|
|
22
|
+
if (closed) {
|
|
23
|
+
throw makeConnectorError('Database reader is closed.');
|
|
24
|
+
}
|
|
25
|
+
let client;
|
|
26
|
+
try {
|
|
27
|
+
client = await pool.connect();
|
|
28
|
+
const res = await client.query(`SELECT ${cols.id}, ${cols.stripe_customer_id}, ${cols.has_paid_access}, ${cols.plan} FROM ${table}`);
|
|
29
|
+
return res.rows.map((row) => ({
|
|
30
|
+
id: String(row[cols.id]),
|
|
31
|
+
stripeCustomerId: row[cols.stripe_customer_id],
|
|
32
|
+
hasPaidAccess: Boolean(row[cols.has_paid_access]),
|
|
33
|
+
plan: row[cols.plan],
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw makeConnectorError(`Database query failed: ${String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
client?.release();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async close() {
|
|
44
|
+
if (!closed) {
|
|
45
|
+
closed = true;
|
|
46
|
+
await pool.end();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=postgres-live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-live.js","sourceRoot":"","sources":["../../src/connectors/postgres-live.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjF,SAAS,kBAAkB,CAAC,OAAe;IACzC,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,CAAwC,CAAC;IACtE,GAAG,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAC7B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,GAAyB;IAChE,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3D,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,kBAAkB,CACtB,6BAA6B,GAAG,CAAC,QAAQ,CAAC,OAAO,4BAA4B;YAC3E,iDAAiD,CACpD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC;IAClC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC;IACvC,mBAAmB,CAAC,KAAK,EAAE,sBAAsB,CAAC,CAAC;IACnD,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;IAE/C,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACvD,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,OAAO;QACL,KAAK,CAAC,SAAS;YACb,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,kBAAkB,CAAC,4BAA4B,CAAC,CAAC;YACzD,CAAC;YAED,IAAI,MAAiC,CAAC;YACtC,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC9B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,UAAU,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,kBAAkB,KAAK,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC,IAAI,SAAS,KAAK,EAAE,CACrG,CAAC;gBACF,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAW,EAAE,CAAC,CAAC;oBACrC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACxB,gBAAgB,EAAE,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAkB;oBAC/D,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;oBACjD,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAkB;iBACtC,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,kBAAkB,CAAC,0BAA0B,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpE,CAAC;oBAAS,CAAC;gBACT,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,CAAC;QACH,CAAC;QAED,KAAK,CAAC,KAAK;YACT,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,GAAG,IAAI,CAAC;gBACd,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-identifiers.d.ts","sourceRoot":"","sources":["../../src/connectors/sql-identifiers.ts"],"names":[],"mappings":"AAGA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMtE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAI9F"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Matches safe Postgres identifiers for table/column names from config */
|
|
2
|
+
const SQL_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3
|
+
export function assertSqlIdentifier(value, label) {
|
|
4
|
+
if (!SQL_IDENTIFIER.test(value)) {
|
|
5
|
+
throw new Error(`Invalid SQL identifier for ${label}: "${value}". Use only letters, numbers, and underscores.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function assertSqlIdentifiers(values, labelPrefix) {
|
|
9
|
+
for (const [key, value] of Object.entries(values)) {
|
|
10
|
+
assertSqlIdentifier(value, `${labelPrefix}.${key}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=sql-identifiers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-identifiers.js","sourceRoot":"","sources":["../../src/connectors/sql-identifiers.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAElD,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,KAAa;IAC9D,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,8BAA8B,KAAK,MAAM,KAAK,gDAAgD,CAC/F,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAA8B,EAAE,WAAmB;IACtF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,mBAAmB,CAAC,KAAK,EAAE,GAAG,WAAW,IAAI,GAAG,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-live.d.ts","sourceRoot":"","sources":["../../src/connectors/stripe-live.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AAQnE,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,YAAY,CA4B5E"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
function makeConnectorError(message) {
|
|
3
|
+
const err = new Error(message);
|
|
4
|
+
err.code = 'CONNECTOR_ERROR';
|
|
5
|
+
return err;
|
|
6
|
+
}
|
|
7
|
+
export function createLiveStripeReader(secretKeyEnvVar) {
|
|
8
|
+
const key = process.env[secretKeyEnvVar];
|
|
9
|
+
if (!key) {
|
|
10
|
+
throw makeConnectorError(`Missing required env var "${secretKeyEnvVar}" for Stripe connector. ` +
|
|
11
|
+
'Provide a restricted read-only Stripe secret key.');
|
|
12
|
+
}
|
|
13
|
+
const client = new Stripe(key, { apiVersion: '2024-06-20' });
|
|
14
|
+
return {
|
|
15
|
+
async listSubscriptions() {
|
|
16
|
+
const results = [];
|
|
17
|
+
for await (const sub of client.subscriptions.list({ limit: 100 })) {
|
|
18
|
+
const priceIds = (sub.items?.data ?? [])
|
|
19
|
+
.map((item) => item.price?.id)
|
|
20
|
+
.filter((id) => Boolean(id));
|
|
21
|
+
results.push({
|
|
22
|
+
id: sub.id,
|
|
23
|
+
customerId: typeof sub.customer === 'string' ? sub.customer : sub.customer.id,
|
|
24
|
+
status: sub.status,
|
|
25
|
+
priceIds,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=stripe-live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-live.js","sourceRoot":"","sources":["../../src/connectors/stripe-live.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,SAAS,kBAAkB,CAAC,OAAe;IACzC,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,CAAwC,CAAC;IACtE,GAAG,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAC7B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,eAAuB;IAC5D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACzC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,kBAAkB,CACtB,6BAA6B,eAAe,0BAA0B;YACpE,mDAAmD,CACtD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAC;IAE7D,OAAO;QACL,KAAK,CAAC,iBAAiB;YACrB,MAAM,OAAO,GAAyB,EAAE,CAAC;YACzC,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;gBAClE,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC;qBACrC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;qBAC7B,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7C,OAAO,CAAC,IAAI,CAAC;oBACX,EAAE,EAAE,GAAG,CAAC,EAAE;oBACV,UAAU,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;oBAC7E,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,QAAQ;iBACT,CAAC,CAAC;YACL,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Minimal Stripe subscription data needed for access evaluation */
|
|
2
|
+
export interface StripeSubscription {
|
|
3
|
+
id: string;
|
|
4
|
+
customerId: string;
|
|
5
|
+
status: string;
|
|
6
|
+
/** Active price IDs on the subscription (usually one) */
|
|
7
|
+
priceIds: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface StripeReader {
|
|
10
|
+
listSubscriptions(): Promise<StripeSubscription[]>;
|
|
11
|
+
}
|
|
12
|
+
/** Minimal app user row needed for access evaluation */
|
|
13
|
+
export interface AppUser {
|
|
14
|
+
id: string;
|
|
15
|
+
stripeCustomerId: string | null;
|
|
16
|
+
hasPaidAccess: boolean;
|
|
17
|
+
plan: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface DatabaseReader {
|
|
20
|
+
listUsers(): Promise<AppUser[]>;
|
|
21
|
+
close?(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/connectors/types.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;CACpD;AAED,wDAAwD;AACxD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAChC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/connectors/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { AccessContractConfig } from '../config/schema.js';
|
|
3
|
+
import type { StripeReader, DatabaseReader } from '../connectors/types.js';
|
|
4
|
+
export interface AccessDataSources {
|
|
5
|
+
stripe: StripeReader;
|
|
6
|
+
database: DatabaseReader;
|
|
7
|
+
}
|
|
8
|
+
export declare function evaluateAccess(cfg: AccessContractConfig, sources: AccessDataSources): Promise<Finding[]>;
|
|
9
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/evaluators/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAA+B,MAAM,wBAAwB,CAAC;AAKxG,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,OAAO,EAAE,CAAC,CAmIpB"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const LAPSED_STATUSES = new Set(['canceled', 'unpaid', 'past_due', 'incomplete_expired']);
|
|
2
|
+
const ACTIVE_STATUSES = new Set(['active', 'trialing']);
|
|
3
|
+
export async function evaluateAccess(cfg, sources) {
|
|
4
|
+
const [subscriptions, users] = await Promise.all([
|
|
5
|
+
sources.stripe.listSubscriptions(),
|
|
6
|
+
sources.database.listUsers(),
|
|
7
|
+
]);
|
|
8
|
+
const findings = [];
|
|
9
|
+
const defaultFix = cfg.fix;
|
|
10
|
+
const severity = cfg.severity;
|
|
11
|
+
const subsByCustomerId = groupByCustomerId(subscriptions);
|
|
12
|
+
const usersByCustomerId = new Map();
|
|
13
|
+
for (const user of users) {
|
|
14
|
+
if (!user.stripeCustomerId)
|
|
15
|
+
continue;
|
|
16
|
+
const arr = usersByCustomerId.get(user.stripeCustomerId) ?? [];
|
|
17
|
+
arr.push(user);
|
|
18
|
+
usersByCustomerId.set(user.stripeCustomerId, arr);
|
|
19
|
+
}
|
|
20
|
+
// Check 1: duplicate stripe_customer_id across multiple app users
|
|
21
|
+
for (const [customerId, mapped] of usersByCustomerId) {
|
|
22
|
+
if (mapped.length > 1) {
|
|
23
|
+
const ids = mapped.map((u) => u.id).join(', ');
|
|
24
|
+
findings.push({
|
|
25
|
+
contract: 'access',
|
|
26
|
+
severity: 'medium',
|
|
27
|
+
entity: `customer:${customerId}`,
|
|
28
|
+
message: `stripe_customer_id "${customerId}" is linked to ${mapped.length} users (${ids}). Duplicate mapping.`,
|
|
29
|
+
fix: 'Ensure each Stripe customer maps to exactly one app user.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Check 2: per-user access state vs Stripe state
|
|
34
|
+
for (const user of users) {
|
|
35
|
+
if (!user.stripeCustomerId) {
|
|
36
|
+
// No Stripe link at all — skip access checks (not necessarily an error)
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const subs = subsByCustomerId.get(user.stripeCustomerId);
|
|
40
|
+
if (!subs || subs.length === 0) {
|
|
41
|
+
// App has a customer ID but Stripe has no record
|
|
42
|
+
findings.push({
|
|
43
|
+
contract: 'access',
|
|
44
|
+
severity: 'medium',
|
|
45
|
+
entity: `user:${user.id}`,
|
|
46
|
+
message: `User has stripe_customer_id "${user.stripeCustomerId}" but no Stripe subscriptions were found.`,
|
|
47
|
+
fix: 'Verify the stripe_customer_id is correct or remove the stale reference.',
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Use the most relevant subscription (active/trialing preferred; otherwise latest)
|
|
52
|
+
const activeSub = subs.find((s) => ACTIVE_STATUSES.has(s.status));
|
|
53
|
+
const lapsedSub = subs.find((s) => LAPSED_STATUSES.has(s.status));
|
|
54
|
+
const anySub = activeSub ?? lapsedSub ?? subs[0];
|
|
55
|
+
if (activeSub) {
|
|
56
|
+
// Should have access
|
|
57
|
+
if (!user.hasPaidAccess) {
|
|
58
|
+
findings.push({
|
|
59
|
+
contract: 'access',
|
|
60
|
+
severity,
|
|
61
|
+
entity: `user:${user.id}`,
|
|
62
|
+
message: `User has an active/trialing Stripe subscription (${activeSub.id}, status: ${activeSub.status}) ` +
|
|
63
|
+
`but has_paid_access is false. Revenue leak — user cannot access paid features.`,
|
|
64
|
+
fix: defaultFix ?? 'Set has_paid_access=true and assign the correct plan on subscription activation.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Check plan mapping drift
|
|
68
|
+
if (cfg.plans) {
|
|
69
|
+
for (const priceId of activeSub.priceIds) {
|
|
70
|
+
const expectedPlan = cfg.plans[priceId];
|
|
71
|
+
if (expectedPlan === undefined) {
|
|
72
|
+
findings.push({
|
|
73
|
+
contract: 'access',
|
|
74
|
+
severity,
|
|
75
|
+
entity: `price:${priceId}`,
|
|
76
|
+
message: `Subscription ${activeSub.id} uses price "${priceId}" which is not in the plans map.`,
|
|
77
|
+
fix: `Add "${priceId}" to the plans map in prodverdict.yml, or remove it from Stripe if deprecated.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else if (user.plan !== null && user.plan !== expectedPlan) {
|
|
81
|
+
findings.push({
|
|
82
|
+
contract: 'access',
|
|
83
|
+
severity,
|
|
84
|
+
entity: `user:${user.id}`,
|
|
85
|
+
message: `User plan is "${user.plan}" but active Stripe price "${priceId}" maps to plan "${expectedPlan}".`,
|
|
86
|
+
fix: defaultFix ?? `Update the user's plan to "${expectedPlan}" to match the Stripe price.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (lapsedSub) {
|
|
93
|
+
// Should not have access
|
|
94
|
+
if (user.hasPaidAccess) {
|
|
95
|
+
findings.push({
|
|
96
|
+
contract: 'access',
|
|
97
|
+
severity,
|
|
98
|
+
entity: `user:${user.id}`,
|
|
99
|
+
message: `User has a ${anySub.status} Stripe subscription (${anySub.id}) ` +
|
|
100
|
+
`but has_paid_access is still true. Wrongful access — user is accessing paid features without a valid subscription.`,
|
|
101
|
+
fix: defaultFix ?? 'Set has_paid_access=false and revoke plan access in the cancellation/webhook handler.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check 3: Stripe customers with subscriptions but no matching app user
|
|
107
|
+
for (const [customerId, subs] of subsByCustomerId) {
|
|
108
|
+
if (!usersByCustomerId.has(customerId)) {
|
|
109
|
+
const activeSub = subs.find((s) => ACTIVE_STATUSES.has(s.status));
|
|
110
|
+
if (activeSub) {
|
|
111
|
+
findings.push({
|
|
112
|
+
contract: 'access',
|
|
113
|
+
severity: 'low',
|
|
114
|
+
entity: `customer:${customerId}`,
|
|
115
|
+
message: `Stripe customer "${customerId}" has an active subscription (${activeSub.id}) but no matching app user row.`,
|
|
116
|
+
fix: 'Verify the customer was not deleted from the app, or handle the Stripe subscription cleanup.',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
}
|
|
123
|
+
function groupByCustomerId(subscriptions) {
|
|
124
|
+
const map = new Map();
|
|
125
|
+
for (const sub of subscriptions) {
|
|
126
|
+
const arr = map.get(sub.customerId) ?? [];
|
|
127
|
+
arr.push(sub);
|
|
128
|
+
map.set(sub.customerId, arr);
|
|
129
|
+
}
|
|
130
|
+
return map;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/evaluators/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { ConfigContractConfig } from '../config/schema.js';
|
|
3
|
+
export interface ConfigDataSources {
|
|
4
|
+
/** Absolute path to the repository root to scan */
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
/**
|
|
7
|
+
* Resolved env vars available at check time.
|
|
8
|
+
* Typically `process.env` but can be overridden for testing.
|
|
9
|
+
*/
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Scan JS/TS source files in the repo for environment variable references.
|
|
14
|
+
* Returns all unique variable names referenced via process.env.X or import.meta.env.X.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanEnvReferences(repoRoot: string): Set<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Parse a .env or .env.example file into a map of key -> value (or empty string if no value).
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseEnvFile(filePath: string): Map<string, string>;
|
|
21
|
+
export declare function evaluateConfig(cfg: ConfigContractConfig, sources: ConfigDataSources): Promise<Finding[]>;
|
|
22
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/evaluators/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAIhE,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACzC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA4C/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBlE;AAwBD,wBAAsB,cAAc,CAClC,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+FpB"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Scan JS/TS source files in the repo for environment variable references.
|
|
5
|
+
* Returns all unique variable names referenced via process.env.X or import.meta.env.X.
|
|
6
|
+
*/
|
|
7
|
+
export function scanEnvReferences(repoRoot) {
|
|
8
|
+
const referenced = new Set();
|
|
9
|
+
const PROCESS_ENV_RE = /process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
10
|
+
const META_ENV_RE = /import\.meta\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
11
|
+
const ENV_CALL_RE = /process\.env\['([A-Z][A-Z0-9_]*)'\]/g;
|
|
12
|
+
const SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts']);
|
|
13
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.nuxt', 'build', 'coverage']);
|
|
14
|
+
function walk(dir) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (SKIP_DIRS.has(entry.name))
|
|
24
|
+
continue;
|
|
25
|
+
const fullPath = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
walk(fullPath);
|
|
28
|
+
}
|
|
29
|
+
else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) {
|
|
30
|
+
// Skip test/spec files — they contain fixture env var names, not production usage
|
|
31
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|mts)$/i.test(entry.name))
|
|
32
|
+
continue;
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const re of [PROCESS_ENV_RE, META_ENV_RE, ENV_CALL_RE]) {
|
|
41
|
+
re.lastIndex = 0;
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = re.exec(content)) !== null) {
|
|
44
|
+
referenced.add(match[1]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
walk(repoRoot);
|
|
51
|
+
return referenced;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse a .env or .env.example file into a map of key -> value (or empty string if no value).
|
|
55
|
+
*/
|
|
56
|
+
export function parseEnvFile(filePath) {
|
|
57
|
+
const vars = new Map();
|
|
58
|
+
let content;
|
|
59
|
+
try {
|
|
60
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return vars;
|
|
64
|
+
}
|
|
65
|
+
for (const line of content.split('\n')) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
68
|
+
continue;
|
|
69
|
+
const eqIdx = trimmed.indexOf('=');
|
|
70
|
+
if (eqIdx === -1) {
|
|
71
|
+
vars.set(trimmed, '');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
75
|
+
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
76
|
+
if (key)
|
|
77
|
+
vars.set(key, val);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return vars;
|
|
81
|
+
}
|
|
82
|
+
const PLACEHOLDER_VALUES = new Set([
|
|
83
|
+
'',
|
|
84
|
+
'your_key_here',
|
|
85
|
+
'your-key-here',
|
|
86
|
+
'changeme',
|
|
87
|
+
'change_me',
|
|
88
|
+
'replace_me',
|
|
89
|
+
'placeholder',
|
|
90
|
+
'xxx',
|
|
91
|
+
'todo',
|
|
92
|
+
'fixme',
|
|
93
|
+
'secret',
|
|
94
|
+
'password',
|
|
95
|
+
'123456',
|
|
96
|
+
'localhost',
|
|
97
|
+
'example',
|
|
98
|
+
]);
|
|
99
|
+
function isPlaceholder(value) {
|
|
100
|
+
return PLACEHOLDER_VALUES.has(value.toLowerCase());
|
|
101
|
+
}
|
|
102
|
+
export async function evaluateConfig(cfg, sources) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
const { repoRoot, env } = sources;
|
|
105
|
+
// ── 1. Required vars must be set ────────────────────────────────────────────
|
|
106
|
+
for (const rule of cfg.rules) {
|
|
107
|
+
if (rule.type === 'required') {
|
|
108
|
+
const { name, severity = cfg.severity, description } = rule;
|
|
109
|
+
const value = env[name];
|
|
110
|
+
if (value === undefined || value === '') {
|
|
111
|
+
findings.push({
|
|
112
|
+
contract: 'config',
|
|
113
|
+
severity,
|
|
114
|
+
entity: `env:${name}`,
|
|
115
|
+
message: `Required environment variable "${name}" is not set.${description ? ` (${description})` : ''}`,
|
|
116
|
+
fix: `Set ${name} in your environment or CI secrets.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (cfg.check_placeholders && isPlaceholder(value)) {
|
|
120
|
+
findings.push({
|
|
121
|
+
contract: 'config',
|
|
122
|
+
severity: 'medium',
|
|
123
|
+
entity: `env:${name}`,
|
|
124
|
+
message: `Environment variable "${name}" appears to contain a placeholder value.`,
|
|
125
|
+
fix: `Replace the placeholder value of ${name} with a real secret.`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (rule.type === 'not_default') {
|
|
130
|
+
const { name, forbidden_values, severity = cfg.severity } = rule;
|
|
131
|
+
const value = env[name];
|
|
132
|
+
if (value !== undefined && forbidden_values.some((f) => value === f || value.toLowerCase() === f.toLowerCase())) {
|
|
133
|
+
findings.push({
|
|
134
|
+
contract: 'config',
|
|
135
|
+
severity,
|
|
136
|
+
entity: `env:${name}`,
|
|
137
|
+
message: `Environment variable "${name}" is set to a forbidden/default value.`,
|
|
138
|
+
fix: `Change ${name} from its default value before deploying to production.`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── 2. Scan repo for env var references and compare against .env.example ────
|
|
144
|
+
if (cfg.scan_references) {
|
|
145
|
+
const referencedVars = scanEnvReferences(repoRoot);
|
|
146
|
+
// Load .env.example if it exists
|
|
147
|
+
const examplePath = path.join(repoRoot, cfg.env_example_file ?? '.env.example');
|
|
148
|
+
const exampleVars = parseEnvFile(examplePath);
|
|
149
|
+
// Load .env if it exists (for local dev)
|
|
150
|
+
const localEnvPath = path.join(repoRoot, '.env');
|
|
151
|
+
const localEnvVars = parseEnvFile(localEnvPath);
|
|
152
|
+
// Combine all "known" declared vars (example + local + current env)
|
|
153
|
+
const knownVars = new Set([
|
|
154
|
+
...exampleVars.keys(),
|
|
155
|
+
...localEnvVars.keys(),
|
|
156
|
+
...Object.keys(env).filter((k) => env[k] !== undefined),
|
|
157
|
+
]);
|
|
158
|
+
// Common infrastructure vars that are universally available and not worth warning about
|
|
159
|
+
const ALWAYS_AVAILABLE = new Set([
|
|
160
|
+
'NODE_ENV',
|
|
161
|
+
'PORT',
|
|
162
|
+
'HOST',
|
|
163
|
+
'HOME',
|
|
164
|
+
'PATH',
|
|
165
|
+
'SHELL',
|
|
166
|
+
'USER',
|
|
167
|
+
'PWD',
|
|
168
|
+
'CI',
|
|
169
|
+
'GITHUB_ACTIONS',
|
|
170
|
+
'VERCEL',
|
|
171
|
+
'VERCEL_ENV',
|
|
172
|
+
'VERCEL_URL',
|
|
173
|
+
'NEXT_PUBLIC_VERCEL_URL',
|
|
174
|
+
]);
|
|
175
|
+
for (const varName of referencedVars) {
|
|
176
|
+
if (ALWAYS_AVAILABLE.has(varName))
|
|
177
|
+
continue;
|
|
178
|
+
if (!knownVars.has(varName) && !(cfg.ignore_vars ?? []).includes(varName)) {
|
|
179
|
+
findings.push({
|
|
180
|
+
contract: 'config',
|
|
181
|
+
severity: 'low',
|
|
182
|
+
entity: `env:${varName}`,
|
|
183
|
+
message: `"${varName}" is referenced in source code but not declared in ${cfg.env_example_file ?? '.env.example'}.`,
|
|
184
|
+
fix: `Add ${varName} to ${cfg.env_example_file ?? '.env.example'} so all environments know about it.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return findings;
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/evaluators/config.ts"],"names":[],"mappings":"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"}
|