turbine-orm 0.60.1 → 0.62.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/README.md +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/cli/studio.d.ts
CHANGED
|
@@ -141,6 +141,19 @@ export interface StudioContext {
|
|
|
141
141
|
* (saved queries persist to `<stateDir>/studio-queries.json`).
|
|
142
142
|
*/
|
|
143
143
|
memorySavedQueries?: SavedQueriesFile;
|
|
144
|
+
/**
|
|
145
|
+
* Set when a generated metadata file was found but its PII tags could NOT be
|
|
146
|
+
* read (`PiiScan.ok === false`). Studio then fails CLOSED: every column of
|
|
147
|
+
* every table is marked `pii`, so the existing enforcement points redact
|
|
148
|
+
* everything, and this record drives the persistent UI banner explaining why.
|
|
149
|
+
* Absent when the scan succeeded, when there was no metadata file, or when
|
|
150
|
+
* `--show-pii` is on.
|
|
151
|
+
*/
|
|
152
|
+
piiScanFailed?: {
|
|
153
|
+
path: string;
|
|
154
|
+
reason: string;
|
|
155
|
+
columns: number;
|
|
156
|
+
};
|
|
144
157
|
}
|
|
145
158
|
/**
|
|
146
159
|
* Start the Studio server. Returns a handle with the session token, a pre-built
|
|
@@ -204,4 +217,14 @@ export declare function apiCreateSavedQuery(req: IncomingMessage, res: ServerRes
|
|
|
204
217
|
export declare function apiDeleteSavedQuery(res: ServerResponse, ctx: StudioContext, id: string): void;
|
|
205
218
|
/** The literal replacement value for a redacted PII cell. */
|
|
206
219
|
export declare const PII_REDACTED = "\u2022\u2022 redacted \u2022\u2022";
|
|
220
|
+
/**
|
|
221
|
+
* Mark every column of every table `pii`, and return how many were touched.
|
|
222
|
+
*
|
|
223
|
+
* The fail-closed posture for "the generated metadata exists but its tags are
|
|
224
|
+
* unreadable". Deliberately expressed as data (tags on the metadata) rather
|
|
225
|
+
* than as a new flag threaded through the redaction code: every enforcement
|
|
226
|
+
* point in this file already asks `col.pii === true`, so there is no path that
|
|
227
|
+
* can forget to consult a separate switch.
|
|
228
|
+
*/
|
|
229
|
+
export declare function forceTagAllColumnsPii(metadata: SchemaMetadata): number;
|
|
207
230
|
export {};
|
package/dist/cli/studio.js
CHANGED
|
@@ -72,6 +72,7 @@ export async function startStudio(options) {
|
|
|
72
72
|
let dialect;
|
|
73
73
|
let statementTimeout;
|
|
74
74
|
let piiTags = null;
|
|
75
|
+
let piiScanFailed;
|
|
75
76
|
if (demo) {
|
|
76
77
|
// Seeded in-memory SQLite store: no DATABASE_URL, no network. Each launch
|
|
77
78
|
// starts pristine and nothing is ever persisted.
|
|
@@ -120,8 +121,35 @@ export async function startStudio(options) {
|
|
|
120
121
|
// what happened so the CLI can be explicit about it at startup.
|
|
121
122
|
if (options.metadataDir) {
|
|
122
123
|
const source = loadPiiTags(options.metadataDir);
|
|
123
|
-
if (source)
|
|
124
|
+
if (source && !source.scan.ok) {
|
|
125
|
+
// FAIL CLOSED. The file exists and is meant to carry the tags, but the
|
|
126
|
+
// scanner could not prove anything about it (`ok: false` is "we do not
|
|
127
|
+
// know", never "no tagged columns"). Redacting nothing here and then
|
|
128
|
+
// reporting a clean tag count is the exact silent-failure shape this
|
|
129
|
+
// module was rewritten to end, and Studio is also the tool with a
|
|
130
|
+
// `--write` mode. So every column of every table is marked PII, which
|
|
131
|
+
// routes through the enforcement points that already exist (row
|
|
132
|
+
// redaction, builder rows, the write echo, and the orderBy / search /
|
|
133
|
+
// filter refusals) with no second code path. `--show-pii` is the
|
|
134
|
+
// override, and it overrides forced tags exactly as it overrides real
|
|
135
|
+
// ones.
|
|
136
|
+
piiScanFailed = {
|
|
137
|
+
path: source.path,
|
|
138
|
+
reason: source.scan.reason ?? 'unrecognized shape',
|
|
139
|
+
columns: forceTagAllColumnsPii(metadata),
|
|
140
|
+
};
|
|
141
|
+
// `applied` is the count of columns being redacted, which is what it
|
|
142
|
+
// means to the reader of the startup line. The warning below is what
|
|
143
|
+
// says these are forced, not declared.
|
|
144
|
+
piiTags = { path: source.path, applied: piiScanFailed.columns };
|
|
145
|
+
console.warn(`[turbine studio] WARNING: ${source.path} exists but its PII tags could not be read ` +
|
|
146
|
+
`(${piiScanFailed.reason}). Failing closed: EVERY column of every table is redacted, sorting, ` +
|
|
147
|
+
'searching, and filtering on any column are refused, and the Query tab is disabled. Re-run ' +
|
|
148
|
+
'`turbine generate` to fix this, or pass --show-pii to run unredacted anyway.');
|
|
149
|
+
}
|
|
150
|
+
else if (source) {
|
|
124
151
|
piiTags = { path: source.path, applied: applyPiiTags(metadata, source.tags) };
|
|
152
|
+
}
|
|
125
153
|
}
|
|
126
154
|
statementTimeout = options.adapter?.statementTimeout?.(30) ?? {
|
|
127
155
|
// Postgres rejects parameters in `SET LOCAL` (`SET LOCAL ... = $1` is a
|
|
@@ -147,6 +175,7 @@ export async function startStudio(options) {
|
|
|
147
175
|
// live. Non-demo honors the CLI flags.
|
|
148
176
|
writable: demo ? false : options.write === true,
|
|
149
177
|
showPii: demo ? false : options.showPii === true,
|
|
178
|
+
piiScanFailed,
|
|
150
179
|
demo,
|
|
151
180
|
dialect,
|
|
152
181
|
// Demo never touches disk: saved queries live (and die) with the process,
|
|
@@ -419,6 +448,12 @@ async function apiSchema(res, ctx) {
|
|
|
419
448
|
showPii: ctx.showPii === true,
|
|
420
449
|
// Demo flag drives the in-UI mode switcher + persistent demo banner.
|
|
421
450
|
demo: ctx.demo === true,
|
|
451
|
+
// Present only when the PII tag scan failed and Studio is redacting
|
|
452
|
+
// everything as a result. Drives a persistent banner: a screen of redaction
|
|
453
|
+
// markers with no explanation reads like a bug, not like a safety posture.
|
|
454
|
+
piiScanFailed: ctx.piiScanFailed
|
|
455
|
+
? { path: ctx.piiScanFailed.path, reason: ctx.piiScanFailed.reason, columns: ctx.piiScanFailed.columns }
|
|
456
|
+
: null,
|
|
422
457
|
});
|
|
423
458
|
}
|
|
424
459
|
/** True when `columnName` on `table` is PII-tagged and currently redacted. */
|
|
@@ -884,6 +919,35 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
884
919
|
sendJson(res, 400, { error: unknownTableMessage(tableName, ctx) });
|
|
885
920
|
return;
|
|
886
921
|
}
|
|
922
|
+
// Fail-closed posture: the generated metadata exists but its PII tags could
|
|
923
|
+
// not be read, so every column is treated as PII. The Query tab is then
|
|
924
|
+
// REFUSED outright rather than half-served. A default projection that
|
|
925
|
+
// excludes every column emits `SELECT FROM "t"`, which the database answers
|
|
926
|
+
// with a syntax error, and letting an explicit `select` through would be a
|
|
927
|
+
// way to name any column in a database whose sensitive columns are, by
|
|
928
|
+
// assumption, unknown. The Data tab still works and shows every cell
|
|
929
|
+
// redacted, so the shape of the database is still visible.
|
|
930
|
+
if (ctx.piiScanFailed && !ctx.showPii) {
|
|
931
|
+
sendJson(res, 400, {
|
|
932
|
+
error: `The Query tab is disabled: PII tags could not be read from ${ctx.piiScanFailed.path} ` +
|
|
933
|
+
`(${ctx.piiScanFailed.reason}), so Studio treats every column as PII. Re-run \`turbine generate\`, or ` +
|
|
934
|
+
'restart Studio with --show-pii to query unredacted.',
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
// The same empty-projection trap without the fail-closed posture: a schema
|
|
939
|
+
// that genuinely tags every column of a table. Explicit `select` is allowed
|
|
940
|
+
// here (the user named the columns, and the values are redacted on the way
|
|
941
|
+
// out); only the empty default projection is refused, with a reason instead
|
|
942
|
+
// of a syntax error.
|
|
943
|
+
const target = ctx.metadata.tables[tableName];
|
|
944
|
+
if (target && !ctx.showPii && args.select === undefined && target.columns.every((c) => c.pii === true)) {
|
|
945
|
+
sendJson(res, 400, {
|
|
946
|
+
error: `Every column of "${tableName}" is PII-tagged, so the default projection is empty. Name the columns you ` +
|
|
947
|
+
'want in `select`, or run Studio with --show-pii.',
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
887
951
|
let deferred;
|
|
888
952
|
try {
|
|
889
953
|
const qi = new QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
|
|
@@ -917,17 +981,30 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
917
981
|
const elapsedMs = Date.now() - started;
|
|
918
982
|
if (!ctx.demo)
|
|
919
983
|
await client.query('COMMIT');
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
984
|
+
// Run the driver rows through the QUERY'S OWN transform rather than reading
|
|
985
|
+
// `result.rows`. The transform is what turns a raw result into the entity
|
|
986
|
+
// shape the ORM contracts for: column names mapped to camelCase field names,
|
|
987
|
+
// dates coerced, and relation columns re-nested under the relation name at
|
|
988
|
+
// every depth (whatever encoding the plan chose, and whatever the driver
|
|
989
|
+
// handed back, pg's parsed json objects or the SQLite demo driver's raw JSON
|
|
990
|
+
// strings, which is why this also replaces the demo-only re-parse).
|
|
991
|
+
//
|
|
992
|
+
// SECURITY, not cosmetics: `relationLoadStrategy: 'flatten'` projects a
|
|
993
|
+
// to-one relation's columns FLAT onto the parent row under generated
|
|
994
|
+
// prefixed aliases (`f0__email`). `redactBuilderRows` redacts a relation's
|
|
995
|
+
// PII by walking the `with` tree into the NESTED object, so on flat rows it
|
|
996
|
+
// walked into nothing and every prefixed PII cell was served in the clear.
|
|
997
|
+
// `assertNoPiiPredicates` deliberately permits `select` on a PII column
|
|
998
|
+
// because "those values are redacted on the way out", and only the
|
|
999
|
+
// transform makes that true. The flat shape must never reach redaction.
|
|
1000
|
+
const rows = deferred.transform(result);
|
|
1001
|
+
const redactedRows = ctx.showPii ? rows : redactBuilderRows(rows, tableName, args.with, ctx.metadata);
|
|
928
1002
|
sendJson(res, 200, {
|
|
929
1003
|
sql: deferred.sql,
|
|
930
|
-
|
|
1004
|
+
// Columns come from the TRANSFORMED rows: `result.fields` names raw SQL
|
|
1005
|
+
// output columns (`created_at`, `f0__email`), which are neither what the
|
|
1006
|
+
// rows are keyed by nor what a user would write in a query.
|
|
1007
|
+
columns: entityResultColumns(redactedRows),
|
|
931
1008
|
rows: redactedRows.map((r) => serializeRow(r)),
|
|
932
1009
|
rowCount: result.rowCount ?? result.rows.length,
|
|
933
1010
|
elapsedMs,
|
|
@@ -1291,6 +1368,27 @@ export function apiDeleteSavedQuery(res, ctx, id) {
|
|
|
1291
1368
|
// ---------------------------------------------------------------------------
|
|
1292
1369
|
/** The literal replacement value for a redacted PII cell. */
|
|
1293
1370
|
export const PII_REDACTED = '•• redacted ••';
|
|
1371
|
+
/**
|
|
1372
|
+
* Mark every column of every table `pii`, and return how many were touched.
|
|
1373
|
+
*
|
|
1374
|
+
* The fail-closed posture for "the generated metadata exists but its tags are
|
|
1375
|
+
* unreadable". Deliberately expressed as data (tags on the metadata) rather
|
|
1376
|
+
* than as a new flag threaded through the redaction code: every enforcement
|
|
1377
|
+
* point in this file already asks `col.pii === true`, so there is no path that
|
|
1378
|
+
* can forget to consult a separate switch.
|
|
1379
|
+
*/
|
|
1380
|
+
export function forceTagAllColumnsPii(metadata) {
|
|
1381
|
+
let count = 0;
|
|
1382
|
+
for (const table of Object.values(metadata.tables)) {
|
|
1383
|
+
for (const col of table.columns) {
|
|
1384
|
+
if (col.pii !== true) {
|
|
1385
|
+
col.pii = true;
|
|
1386
|
+
count++;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return count;
|
|
1391
|
+
}
|
|
1294
1392
|
/** Shared empty key set for the `--show-pii` fast path (no redaction). */
|
|
1295
1393
|
const NO_PII_KEYS = new Set();
|
|
1296
1394
|
/**
|
|
@@ -1367,50 +1465,24 @@ function redactBuilderRows(rows, tableName, withClause, metadata) {
|
|
|
1367
1465
|
});
|
|
1368
1466
|
}
|
|
1369
1467
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1468
|
+
* Column descriptors for rows that have already been through a query's
|
|
1469
|
+
* `transform`, i.e. entity rows keyed by camelCase FIELD name with relations
|
|
1470
|
+
* nested under the relation name.
|
|
1471
|
+
*
|
|
1472
|
+
* Derived from the rows themselves rather than from `result.fields`, which
|
|
1473
|
+
* names raw SQL output columns (`created_at`, or a flatten plan's `f0__email`)
|
|
1474
|
+
* and so would label the grid with keys the rows do not have. Keys are unioned
|
|
1475
|
+
* across every row in first-seen order: a `select` is uniform, but a null
|
|
1476
|
+
* to-one relation and a JSON-shaped row can still differ, and a column missing
|
|
1477
|
+
* from row 0 must not disappear from the header.
|
|
1376
1478
|
*/
|
|
1377
|
-
function
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
if (relEntries.length === 0)
|
|
1385
|
-
return rows;
|
|
1386
|
-
return rows.map((row) => {
|
|
1387
|
-
const out = { ...row };
|
|
1388
|
-
for (const [relName, relVal] of relEntries) {
|
|
1389
|
-
const rel = table.relations[relName];
|
|
1390
|
-
if (!rel)
|
|
1391
|
-
continue;
|
|
1392
|
-
let child = out[relName];
|
|
1393
|
-
if (typeof child === 'string') {
|
|
1394
|
-
try {
|
|
1395
|
-
child = JSON.parse(child);
|
|
1396
|
-
}
|
|
1397
|
-
catch {
|
|
1398
|
-
continue;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
const nestedWith = relVal && typeof relVal === 'object' ? relVal.with : undefined;
|
|
1402
|
-
if (Array.isArray(child)) {
|
|
1403
|
-
out[relName] = parseDemoRelationRows(child, rel.to, nestedWith, metadata);
|
|
1404
|
-
}
|
|
1405
|
-
else if (child && typeof child === 'object') {
|
|
1406
|
-
out[relName] = parseDemoRelationRows([child], rel.to, nestedWith, metadata)[0];
|
|
1407
|
-
}
|
|
1408
|
-
else {
|
|
1409
|
-
out[relName] = child;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
return out;
|
|
1413
|
-
});
|
|
1479
|
+
function entityResultColumns(rows) {
|
|
1480
|
+
const seen = new Set();
|
|
1481
|
+
for (const row of rows) {
|
|
1482
|
+
for (const key of Object.keys(row))
|
|
1483
|
+
seen.add(key);
|
|
1484
|
+
}
|
|
1485
|
+
return [...seen].map((name) => ({ name, dataTypeID: 0 }));
|
|
1414
1486
|
}
|
|
1415
1487
|
/**
|
|
1416
1488
|
* A fresh CSP nonce for one HTML response. Base64 of 16 random bytes; the value
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare const symbols: {
|
|
|
33
33
|
readonly info: "i" | "ℹ";
|
|
34
34
|
readonly warning: "⚠" | "!";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
|
-
readonly line: "
|
|
36
|
+
readonly line: "─" | "-";
|
|
37
37
|
readonly vertLine: "|" | "│";
|
|
38
38
|
readonly topLeft: "+" | "╭";
|
|
39
39
|
readonly topRight: "+" | "╮";
|
|
@@ -70,4 +70,18 @@ export declare function divider(): void;
|
|
|
70
70
|
export declare function banner(): void;
|
|
71
71
|
export declare function elapsed(startMs: number): string;
|
|
72
72
|
export declare function stripAnsi(s: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* A query-string parameter whose name ENDS in `password`: libpq's `password`
|
|
75
|
+
* and `sslpassword`, plus any vendor spelling of the same idea. Group 1 is
|
|
76
|
+
* `?`/`&` + the key, group 2 is the value, which runs to the next `&`, `#`, or
|
|
77
|
+
* the end of the string, and may be empty.
|
|
78
|
+
*
|
|
79
|
+
* Exported because "does this string carry a secret" and "redact the secret in
|
|
80
|
+
* this string" must agree on ONE definition. They did not: this redactor knew
|
|
81
|
+
* about `?password=` while `connectionStringHasPassword` in config.ts did not,
|
|
82
|
+
* so `turbine init --url` committed a query-form password into
|
|
83
|
+
* `turbine.config.ts` while printing the redacted spelling of the same string
|
|
84
|
+
* one line above. config.ts now matches with this exact pattern.
|
|
85
|
+
*/
|
|
86
|
+
export declare const PASSWORD_QUERY_PARAM_PATTERN = "([?&][^=&#]*password)=([^&#]*)";
|
|
73
87
|
export declare function redactUrl(url: string): string;
|
package/dist/cli/ui.js
CHANGED
|
@@ -210,11 +210,25 @@ export function stripAnsi(s) {
|
|
|
210
210
|
// ---------------------------------------------------------------------------
|
|
211
211
|
// Redact password from connection URL
|
|
212
212
|
// ---------------------------------------------------------------------------
|
|
213
|
+
/**
|
|
214
|
+
* A query-string parameter whose name ENDS in `password`: libpq's `password`
|
|
215
|
+
* and `sslpassword`, plus any vendor spelling of the same idea. Group 1 is
|
|
216
|
+
* `?`/`&` + the key, group 2 is the value, which runs to the next `&`, `#`, or
|
|
217
|
+
* the end of the string, and may be empty.
|
|
218
|
+
*
|
|
219
|
+
* Exported because "does this string carry a secret" and "redact the secret in
|
|
220
|
+
* this string" must agree on ONE definition. They did not: this redactor knew
|
|
221
|
+
* about `?password=` while `connectionStringHasPassword` in config.ts did not,
|
|
222
|
+
* so `turbine init --url` committed a query-form password into
|
|
223
|
+
* `turbine.config.ts` while printing the redacted spelling of the same string
|
|
224
|
+
* one line above. config.ts now matches with this exact pattern.
|
|
225
|
+
*/
|
|
226
|
+
export const PASSWORD_QUERY_PARAM_PATTERN = '([?&][^=&#]*password)=([^&#]*)';
|
|
227
|
+
const PASSWORD_QUERY_PARAM_RE = new RegExp(PASSWORD_QUERY_PARAM_PATTERN, 'gi');
|
|
213
228
|
export function redactUrl(url) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.replace(/([?&][^=&#]*password)=([^&#]*)/gi, '$1=***'));
|
|
229
|
+
// `String.replace` with a /g regex resets `lastIndex`, so sharing the compiled
|
|
230
|
+
// instance across calls is safe.
|
|
231
|
+
return redactUserinfo(url).replace(PASSWORD_QUERY_PARAM_RE, '$1=***');
|
|
218
232
|
}
|
|
219
233
|
/**
|
|
220
234
|
* Replace `<scheme>://<user>:<password>@` with `<scheme>://<user>:***@`, for
|