@biznetgio/neo-app 0.4.74 → 0.4.77
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/package.json +1 -1
- package/prompt.md +7 -1
- package/src/commands/db.js +98 -0
- package/src/index.js +10 -4
- package/src/mcp/tools.js +19 -26
- package/src/redact.js +26 -0
package/package.json
CHANGED
package/prompt.md
CHANGED
|
@@ -6,7 +6,7 @@ Keep this in sync with cli/prompt.md (the npm package's bundled copy).
|
|
|
6
6
|
-->
|
|
7
7
|
# Set this repo up on NEO App
|
|
8
8
|
|
|
9
|
-
**Version:** 0.4.
|
|
9
|
+
**Version:** 0.4.77 · **Last updated:** 2026-11-07
|
|
10
10
|
|
|
11
11
|
These are the official setup instructions from NEO App (Biznet Gio's managed-app
|
|
12
12
|
platform). If you are an AI coding agent, complete the steps below yourself by running
|
|
@@ -123,6 +123,12 @@ tools — never the portal UI. The lifecycle is the same for every resource type
|
|
|
123
123
|
explicit **Approve & Create**. Individual creates (`neo_create_database`,
|
|
124
124
|
`neo_create_queue`) instead return the cost and create nothing until you re-call
|
|
125
125
|
with `confirm: true`.
|
|
126
|
+
**Pricing is resource-based and hourly (#144).** Compute is priced per **vCPU-hour**
|
|
127
|
+
and per **GB-RAM-hour** independently — there are no packages/tiers. Size a service
|
|
128
|
+
with `vcpu` + `memoryMB` (or `cpuLimit`/`memoryLimit`); a legacy tier key still works
|
|
129
|
+
but resolves to its concrete vCPU/RAM. Storage and object storage bill per GB-hour.
|
|
130
|
+
Time-based components accrue hourly and are **capped at 730 billable hours/month**; a
|
|
131
|
+
monthly figure is a non-authoritative estimate (hourly × up to 730).
|
|
126
132
|
3. **Apply**: `neo_apply_stack({planId, planHash})`. If the price book changed since the
|
|
127
133
|
plan, apply is refused — re-plan to re-quote. A `pending_approval` result is the
|
|
128
134
|
approval policy working, not an error; wait for it.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// `neo-app db` — terminal discoverability for the managed-database lifecycle (#149 retest).
|
|
2
|
+
// Read-only inspection (list/get) runs here directly; billable/mutating operations
|
|
3
|
+
// (create, verify, query, backup, restore, redis-command, delete) stay on the MCP
|
|
4
|
+
// server's audited cost-consent path and are listed here so a terminal-only user can
|
|
5
|
+
// discover them from the CLI command tree instead of only through an agent.
|
|
6
|
+
|
|
7
|
+
import { resolveConfig } from '../config.js';
|
|
8
|
+
import { getCredential } from '../creds.js';
|
|
9
|
+
import { NeoApi } from '../api.js';
|
|
10
|
+
import { readLink } from '../link.js';
|
|
11
|
+
import { log, ok, fail, c } from '../util.js';
|
|
12
|
+
import { redactSecrets } from '../redact.js';
|
|
13
|
+
|
|
14
|
+
// The full lifecycle, with the MCP tool that performs each one. Shown by `neo-app db`.
|
|
15
|
+
const OPS = [
|
|
16
|
+
['list', 'neo-app db list', 'List databases in the linked project (read-only)'],
|
|
17
|
+
['get <id>', 'neo-app db get <id>', 'Show one database, incl. redacted connection string (read-only)'],
|
|
18
|
+
['create', 'neo_create_database', 'Provision PostgreSQL / MySQL / Redis (billable — cost consent)'],
|
|
19
|
+
['deploy', 'neo_deploy_database', 'Start/roll out the database pods'],
|
|
20
|
+
['verify', 'neo_verify_database', 'Round-trip health probe (engine-aware)'],
|
|
21
|
+
['query', 'neo_db_query', 'Read-only SQL (postgres/mysql)'],
|
|
22
|
+
['redis', 'neo_redis_command', 'Run a safe Redis command (denylist-guarded)'],
|
|
23
|
+
['metrics', 'neo_get_database_metrics', 'Engine-aware metrics snapshot'],
|
|
24
|
+
['backup', 'neo_run_backup / neo_create_backup', 'Create and run a logical backup'],
|
|
25
|
+
['restore', 'neo_restore_database', 'Restore a SQL dump (postgres/mysql)'],
|
|
26
|
+
['public', 'neo_enable_db_public_access','Enable/disable TLS public access'],
|
|
27
|
+
['delete', 'neo_delete_database', 'Delete a database (billable stop)'],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function usage() {
|
|
31
|
+
log(`${c.bold('neo-app db')} — managed database lifecycle (PostgreSQL · MySQL · Redis)\n`);
|
|
32
|
+
log(c.bold('Run directly from the CLI (read-only):'));
|
|
33
|
+
log(' neo-app db list');
|
|
34
|
+
log(' neo-app db get <database-id>\n');
|
|
35
|
+
log(c.bold('Mutating / billable operations run through the MCP server'));
|
|
36
|
+
log(`(start it with ${c.bold('neo-app mcp')} — your agent calls these; the Portal exposes them too):`);
|
|
37
|
+
for (const [, how, desc] of OPS.slice(2)) log(` · ${c.bold(how.padEnd(34))} ${c.dim(desc)}`);
|
|
38
|
+
log(`\nPrice a database before creating it: ${c.bold('neo-app estimate --storage <GB>')}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function connect(opts) {
|
|
42
|
+
const cfg = resolveConfig(opts);
|
|
43
|
+
const cred = getCredential(cfg.baseUrl);
|
|
44
|
+
if (!cred?.token) { fail(`not authenticated for ${cfg.baseUrl} — run 'neo-app login'`); process.exit(1); }
|
|
45
|
+
return new NeoApi({ baseUrl: cfg.baseUrl, token: cred.token });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requireProject(opts) {
|
|
49
|
+
const projectId = opts.project || readLink()?.projectId;
|
|
50
|
+
if (!projectId) { fail("no linked project — run 'neo-app link <project-id>' or pass --project <id>"); process.exit(1); }
|
|
51
|
+
return projectId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function db(sub, opts = {}, positionals = []) {
|
|
55
|
+
if (!sub || sub === 'help') return void usage();
|
|
56
|
+
|
|
57
|
+
if (sub === 'list' || sub === 'ls') {
|
|
58
|
+
const api = connect(opts);
|
|
59
|
+
const projectId = requireProject(opts);
|
|
60
|
+
let dbs;
|
|
61
|
+
try { dbs = (await api.get(`/api/v1/projects/${projectId}/databases`)).data?.databases || []; }
|
|
62
|
+
catch (e) { fail(`list failed: ${e.message || e}`); process.exit(1); }
|
|
63
|
+
// Credentials are write-only: the API hands an owner/admin an unredacted
|
|
64
|
+
// connectionString (Portal reveal), but a terminal must never echo it (#149 retest).
|
|
65
|
+
dbs = redactSecrets(dbs);
|
|
66
|
+
if (opts.json) return void process.stdout.write(JSON.stringify({ databases: dbs }, null, 2) + '\n');
|
|
67
|
+
if (!dbs.length) return void ok('no databases in this project');
|
|
68
|
+
for (const d of dbs) {
|
|
69
|
+
const status = d.status || (d.host ? 'deployed' : 'not_deployed');
|
|
70
|
+
log(` ${c.bold(d.name)} ${c.dim(`${d.engine} ${d.version || ''}`.trim())} ${status} ${c.dim(d.environment || 'production')} ${c.dim(d.id)}`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sub === 'get' || sub === 'show') {
|
|
76
|
+
const id = positionals[0];
|
|
77
|
+
if (!id) { fail('usage: neo-app db get <database-id>'); process.exit(2); }
|
|
78
|
+
const api = connect(opts);
|
|
79
|
+
let view;
|
|
80
|
+
try { view = (await api.get(`/api/v1/databases/${id}`)).data; }
|
|
81
|
+
catch (e) { fail(`get failed: ${e.message || e}`); process.exit(1); }
|
|
82
|
+
view = redactSecrets(view); // write-only credentials never reach the terminal (#149 retest)
|
|
83
|
+
if (opts.json) return void process.stdout.write(JSON.stringify(view, null, 2) + '\n');
|
|
84
|
+
for (const [k, v] of Object.entries(view)) if (v !== undefined && v !== null && v !== '') log(` ${c.dim(k.padEnd(18))} ${v}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Any lifecycle verb typed at the CLI: point the user at the discoverable path.
|
|
89
|
+
const known = OPS.find(([key]) => key.split(' ')[0] === sub);
|
|
90
|
+
if (known) {
|
|
91
|
+
log(`${c.bold(sub)} runs through the MCP server (${c.bold(known[1])}) or the Portal — not as a direct CLI write.`);
|
|
92
|
+
log(`Start the MCP server with ${c.bold('neo-app mcp')} so your agent can call it, or use the Portal.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fail(`unknown db subcommand: ${sub}`);
|
|
96
|
+
usage();
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,8 @@ ${c.bold('Commands')}
|
|
|
20
20
|
(or pass the same via --source <path|.zip|git-url>)
|
|
21
21
|
project create [name] Create a project (defaults to the dir name) and link it
|
|
22
22
|
project list List projects in your organization (find ids to link)
|
|
23
|
+
db [list|get|...] Managed database lifecycle (PostgreSQL/MySQL/Redis);
|
|
24
|
+
list/get run here, the rest via the MCP server
|
|
23
25
|
status | whoami Show connected account, org, environment, linked project
|
|
24
26
|
mcp Run the MCP server: local stdio (default), or hosted
|
|
25
27
|
Streamable HTTP + SSE with --http [--port <n>] [--host <h>]
|
|
@@ -38,8 +40,8 @@ ${c.bold('Options')}
|
|
|
38
40
|
--yes Non-interactive (headless/CI); also approves a billable deploy without the cost prompt
|
|
39
41
|
--dry-run Print intended changes without writing
|
|
40
42
|
--json Machine-readable output incl. the normalized cost (deploy, estimate)
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
+
--vcpu <n> --mem <MB> estimate: compute, priced per vCPU-hour + GB-RAM-hour (#144)
|
|
44
|
+
--tier <key> estimate: legacy size shorthand (micro|…|xlarge) → vCPU/RAM
|
|
43
45
|
--storage <GB> estimate: database/persistent storage
|
|
44
46
|
--object <GB> estimate: object-storage quota (billed monthly-upfront)
|
|
45
47
|
--queue / --cron estimate: a queue (per-message) / cron (per-run) resource
|
|
@@ -106,8 +108,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
106
108
|
// Root help, and `--help` on a KNOWN command, print usage. An UNKNOWN command must
|
|
107
109
|
// still error (nonzero exit) even with `--help`, so a bad command isn't silently
|
|
108
110
|
// accepted (JNG #97). Per-subcommand help is tracked in #89.
|
|
109
|
-
const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'version', 'help']);
|
|
110
|
-
|
|
111
|
+
const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'db', 'version', 'help']);
|
|
112
|
+
// `db --help` renders the DB-specific help (its usage() lists the lifecycle), not the
|
|
113
|
+
// generic top-level help — #149 retest CLI discoverability caveat. Other known commands
|
|
114
|
+
// still fall back to the root HELP (per-subcommand help tracked in #89).
|
|
115
|
+
if (positionals.length === 0 || (opts.help && KNOWN_COMMANDS.has(cmd) && cmd !== 'db')) return void console.log(HELP);
|
|
111
116
|
|
|
112
117
|
try {
|
|
113
118
|
// `agent setup` and `setup agent` both work (parity with InstaCloud's `setup agent`).
|
|
@@ -123,6 +128,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
123
128
|
if (cmd === 'project' && sub === 'create') return void (await (await import('./commands/link.js')).createProject(positionals[2], opts));
|
|
124
129
|
if (cmd === 'project' && (sub === 'list' || sub === 'ls')) return void (await (await import('./commands/link.js')).listProjects(opts));
|
|
125
130
|
if (cmd === 'status' || cmd === 'whoami') return void (await (await import('./commands/status.js')).status(opts));
|
|
131
|
+
if (cmd === 'db') return void (await (await import('./commands/db.js')).db(sub, opts, positionals.slice(2)));
|
|
126
132
|
if (cmd === 'mcp') {
|
|
127
133
|
const mcp = await import('./mcp/server.js');
|
|
128
134
|
// Hosted mode: Streamable HTTP + SSE over one listener (auth per request). Default: stdio.
|
package/src/mcp/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { statSync, readFileSync } from 'node:fs';
|
|
|
7
7
|
import { deploySource } from '../deploySource.js';
|
|
8
8
|
import { pkgVersion } from '../util.js';
|
|
9
9
|
import { specToResources, formatCost } from '../cost.js';
|
|
10
|
+
import { redactSecrets } from '../redact.js';
|
|
10
11
|
|
|
11
12
|
const str = (description) => ({ type: 'string', description });
|
|
12
13
|
const int = (description) => ({ type: 'integer', description });
|
|
@@ -245,22 +246,8 @@ async function billableGate(api, { projectId, kind, name, confirmationId, confir
|
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
// queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also
|
|
251
|
-
// hit resourceId/source-of-truth-style names (substring match would catch "reSOURCEid").
|
|
252
|
-
const SECRET_EXACT = new Set(['source', 'installed']);
|
|
253
|
-
function redactSecrets(value) {
|
|
254
|
-
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
255
|
-
if (value && typeof value === 'object') {
|
|
256
|
-
const out = {};
|
|
257
|
-
for (const [k, v] of Object.entries(value)) {
|
|
258
|
-
out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
|
|
259
|
-
}
|
|
260
|
-
return out;
|
|
261
|
-
}
|
|
262
|
-
return value;
|
|
263
|
-
}
|
|
249
|
+
// redactSecrets lives in ../redact.js — the single redactor the CLI `db` commands share,
|
|
250
|
+
// so every terminal/JSON output path masks credentials identically (#149 retest).
|
|
264
251
|
|
|
265
252
|
export const TOOLS = [
|
|
266
253
|
{
|
|
@@ -484,9 +471,9 @@ export const TOOLS = [
|
|
|
484
471
|
type: 'object',
|
|
485
472
|
properties: {
|
|
486
473
|
appId: str('Service/app id'),
|
|
487
|
-
computeTier: str('
|
|
488
|
-
cpuLimit: str('
|
|
489
|
-
memoryLimit: str('
|
|
474
|
+
computeTier: str('Legacy size shorthand (micro|small|medium|large|xlarge) — maps to its concrete vCPU/RAM. Prefer cpuLimit+memoryLimit. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour, no packages.'),
|
|
475
|
+
cpuLimit: str('CPU, e.g. "500m" or "1" (use with memoryLimit) — resource-based billing'),
|
|
476
|
+
memoryLimit: str('Memory, e.g. "512Mi" or "1Gi" (use with cpuLimit) — resource-based billing'),
|
|
490
477
|
replicas: int('Fixed replica count'),
|
|
491
478
|
autoscaleMin: int('Autoscale min replicas'),
|
|
492
479
|
autoscaleMax: int('Autoscale max replicas'),
|
|
@@ -514,7 +501,9 @@ export const TOOLS = [
|
|
|
514
501
|
const { appId, confirm, orgId, computeTier, allowCrossEnvironment, ...rest } = args;
|
|
515
502
|
await assertResourceEnv(api, `/api/v1/apps/${appId}`, allowCrossEnvironment); // JNG #136 §4
|
|
516
503
|
const body = { ...rest };
|
|
517
|
-
|
|
504
|
+
// #144: compute is resource-based. A legacy tier shorthand is passed through as
|
|
505
|
+
// cpuLimit; the control plane maps it to concrete vCPU/RAM and bills unbundled.
|
|
506
|
+
if (computeTier) { body.cpuLimit = computeTier; }
|
|
518
507
|
else if (args.cpuLimit || args.memoryLimit) { body.billingMode = 'unbundled'; }
|
|
519
508
|
const costChanging = computeTier || args.cpuLimit || args.memoryLimit || args.replicas !== undefined || args.autoscaleMax !== undefined || args.autoscaleMin !== undefined || args.volumeSize !== undefined || (Array.isArray(args.mounts) && args.mounts.length > 0);
|
|
520
509
|
if (costChanging && confirm !== true) {
|
|
@@ -970,16 +959,18 @@ export const TOOLS = [
|
|
|
970
959
|
{
|
|
971
960
|
name: 'neo_estimate',
|
|
972
961
|
description:
|
|
973
|
-
'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Covers compute (a tier
|
|
962
|
+
'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour independently (no packages), hourly, capped at 730 billable hours/month. Covers compute (vcpu+memoryMB, or a legacy tier shorthand that resolves to concrete vCPU/RAM), persistent storage, and object storage (all per GB-hour), plus queue and cron (0 fixed cost — their per-message/per-run rates are disclosed in variable[]). The monthly figure is a non-authoritative estimate (hourly × up to 730). Pass orgId to apply that org\'s price overrides.',
|
|
974
963
|
mutating: false,
|
|
975
964
|
schema: {
|
|
976
965
|
type: 'object',
|
|
977
966
|
properties: {
|
|
978
|
-
computeTier: str('
|
|
979
|
-
vcpu: { type: 'number', description: '
|
|
980
|
-
memoryMB: int('
|
|
967
|
+
computeTier: str('Legacy size shorthand (micro|small|medium|large|xlarge) — resolves to concrete vCPU/RAM. Prefer vcpu+memoryMB. Compute is resource-based (#144), not packaged.'),
|
|
968
|
+
vcpu: { type: 'number', description: 'vCPU count — priced per vCPU-hour (preferred over computeTier)' },
|
|
969
|
+
memoryMB: int('Memory in MB — priced per GB-RAM-hour (use with vcpu)'),
|
|
981
970
|
storageGB: int('Persistent/database storage in GB (optional)'),
|
|
982
971
|
objectStorageGB: int('Object-storage quota in GB (optional)'),
|
|
972
|
+
queue: { type: 'boolean', description: 'Include a NEO Queue — 0 fixed cost; discloses the per-message (queue_message) rate in variable[]' },
|
|
973
|
+
cron: { type: 'boolean', description: 'Include a Scheduled Job — 0 fixed cost; discloses the per-run (cron_run) rate in variable[]' },
|
|
983
974
|
replicas: int('Replica count (default 1)'),
|
|
984
975
|
orgId: str('Organization id (optional — applies that org\'s price overrides)'),
|
|
985
976
|
},
|
|
@@ -991,7 +982,7 @@ export const TOOLS = [
|
|
|
991
982
|
if (!orgId) { try { orgId = await api.resolveOrgId(); } catch { orgId = null; } }
|
|
992
983
|
if (!orgId) throw new Error('no organization for this account — cannot estimate cost');
|
|
993
984
|
const resources = specToResources(args);
|
|
994
|
-
if (!resources.length) throw new Error('nothing to estimate — pass computeTier (or vcpu+memoryMB), storageGB, and/or
|
|
985
|
+
if (!resources.length) throw new Error('nothing to estimate — pass computeTier (or vcpu+memoryMB), storageGB, objectStorageGB, queue, and/or cron');
|
|
995
986
|
const { cost } = await api.estimate({ orgId }, { resources });
|
|
996
987
|
return cost; // normalized contract: currency/hourly/monthly/breakdown/variable/upfront/priceBook/priceBookVersion/assumptions
|
|
997
988
|
},
|
|
@@ -1779,7 +1770,9 @@ export const TOOLS = [
|
|
|
1779
1770
|
branch: str('app: git branch'),
|
|
1780
1771
|
deps: { type: 'array', items: { type: 'string', enum: ['database', 'queue', 'object_storage'] }, description: 'app: managed-resource dependencies this service needs (from neo_scan_repo `deps`). The plan flags any dep not satisfied by a binding as incompleteTopology and apply refuses it unless acknowledged — so a service needing NATS_URL/DATABASE_URL isn\'t deployed into a 503 (JNG #112 §7).' },
|
|
1781
1772
|
port: int('app: container port'),
|
|
1782
|
-
computeTier: str('app
|
|
1773
|
+
computeTier: str('app: legacy size shorthand (micro|small|medium|large|xlarge) → concrete vCPU/RAM; prefer cpuLimit+memoryLimit. Compute is resource-based (#144).'),
|
|
1774
|
+
cpuLimit: str('app: CPU, e.g. "0.5" or "1" (use with memoryLimit) — resource-based'),
|
|
1775
|
+
memoryLimit: str('app: memory, e.g. "512M" or "1Gi" (use with cpuLimit) — resource-based'),
|
|
1783
1776
|
engine: str('database: postgres | mysql'),
|
|
1784
1777
|
version: str('database: engine version, e.g. 16'),
|
|
1785
1778
|
volumeSize: str('database: storage size, e.g. 10Gi'),
|
package/src/redact.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Single redactor for every CLI/MCP path that prints a control-plane response to a
|
|
2
|
+
// terminal, a file, or JSON (#149 retest SECURITY BLOCKER): managed-DB/queue credentials
|
|
3
|
+
// are write-only. The control-plane API still returns an unredacted connectionString to an
|
|
4
|
+
// owner/admin (the Portal's RBAC-gated reveal), so a terminal tool that echoes that response
|
|
5
|
+
// raw leaks a plaintext password. Every CLI/MCP output routes through redactSecrets so a
|
|
6
|
+
// credential never reaches a shell, a log, or a JSON dump — matching MCP's write-only stance.
|
|
7
|
+
|
|
8
|
+
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
|
|
9
|
+
// Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
|
|
10
|
+
// queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also hit
|
|
11
|
+
// resourceId/source-of-truth-style names (a substring match would catch "reSOURCEid").
|
|
12
|
+
const SECRET_EXACT = new Set(['source', 'installed']);
|
|
13
|
+
|
|
14
|
+
export const REDACTED = '[redacted — credentials are write-only; bind them to a consumer instead]';
|
|
15
|
+
|
|
16
|
+
export function redactSecrets(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const [k, v] of Object.entries(value)) {
|
|
21
|
+
out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? REDACTED : redactSecrets(v);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|