@biznetgio/neo-app 0.4.75 → 0.4.78
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 +16 -1
- package/src/commands/db.js +6 -1
- package/src/index.js +4 -1
- package/src/mcp/tools.js +7 -18
- 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.78 · **Last updated:** 2026-11-09
|
|
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
|
|
@@ -141,6 +141,21 @@ tools — never the portal UI. The lifecycle is the same for every resource type
|
|
|
141
141
|
For a **scheduled job**, call `neo_cron_questionnaire` first to collect the schedule and
|
|
142
142
|
target, then `neo_create_cron`. To expose a managed database or queue to the internet,
|
|
143
143
|
use `neo_enable_db_public_access` / `neo_enable_queue_public_access` (deploy it first).
|
|
144
|
+
Secure public access is **PostgreSQL only** today — MySQL and Redis public access are
|
|
145
|
+
deferred; keep those private (in-cluster) and reach them from a service in the same project.
|
|
146
|
+
|
|
147
|
+
## Managed databases from the terminal (PostgreSQL · MySQL · Redis)
|
|
148
|
+
|
|
149
|
+
- **Discover from the CLI:** `neo-app db` lists the whole lifecycle; `neo-app db list` and
|
|
150
|
+
`neo-app db get <id>` run directly (read-only). Connection strings are **redacted** in the
|
|
151
|
+
terminal — reveal the credential in the Portal, never in chat or logs.
|
|
152
|
+
- **Create/mutate** through the MCP tools (`neo_create_database`, `neo_deploy_database`,
|
|
153
|
+
`neo_verify_database`, …) so cost-consent and audit apply.
|
|
154
|
+
- **Backup/restore boundaries:** logical backup covers all three engines; **restore is
|
|
155
|
+
PostgreSQL and MySQL only** (`neo_restore_database` takes a SQL dump). **Redis logical
|
|
156
|
+
restore is not supported** — recover Redis from its persistence/replication, not a dump.
|
|
157
|
+
- **Public access:** PostgreSQL only (see above); MySQL/Redis secure public access and Redis
|
|
158
|
+
auth/TLS are deferred follow-ups, not available capabilities.
|
|
144
159
|
|
|
145
160
|
Never deploy to **production** without an explicit request, and never default a missing
|
|
146
161
|
environment — every create/deploy names `staging` or `production`.
|
package/src/commands/db.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getCredential } from '../creds.js';
|
|
|
9
9
|
import { NeoApi } from '../api.js';
|
|
10
10
|
import { readLink } from '../link.js';
|
|
11
11
|
import { log, ok, fail, c } from '../util.js';
|
|
12
|
+
import { redactSecrets } from '../redact.js';
|
|
12
13
|
|
|
13
14
|
// The full lifecycle, with the MCP tool that performs each one. Shown by `neo-app db`.
|
|
14
15
|
const OPS = [
|
|
@@ -22,7 +23,7 @@ const OPS = [
|
|
|
22
23
|
['metrics', 'neo_get_database_metrics', 'Engine-aware metrics snapshot'],
|
|
23
24
|
['backup', 'neo_run_backup / neo_create_backup', 'Create and run a logical backup'],
|
|
24
25
|
['restore', 'neo_restore_database', 'Restore a SQL dump (postgres/mysql)'],
|
|
25
|
-
['public', 'neo_enable_db_public_access','Enable/disable TLS public access'],
|
|
26
|
+
['public', 'neo_enable_db_public_access','Enable/disable TLS public access (PostgreSQL only — MySQL/Redis public access deferred)'],
|
|
26
27
|
['delete', 'neo_delete_database', 'Delete a database (billable stop)'],
|
|
27
28
|
];
|
|
28
29
|
|
|
@@ -59,6 +60,9 @@ export async function db(sub, opts = {}, positionals = []) {
|
|
|
59
60
|
let dbs;
|
|
60
61
|
try { dbs = (await api.get(`/api/v1/projects/${projectId}/databases`)).data?.databases || []; }
|
|
61
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);
|
|
62
66
|
if (opts.json) return void process.stdout.write(JSON.stringify({ databases: dbs }, null, 2) + '\n');
|
|
63
67
|
if (!dbs.length) return void ok('no databases in this project');
|
|
64
68
|
for (const d of dbs) {
|
|
@@ -75,6 +79,7 @@ export async function db(sub, opts = {}, positionals = []) {
|
|
|
75
79
|
let view;
|
|
76
80
|
try { view = (await api.get(`/api/v1/databases/${id}`)).data; }
|
|
77
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)
|
|
78
83
|
if (opts.json) return void process.stdout.write(JSON.stringify(view, null, 2) + '\n');
|
|
79
84
|
for (const [k, v] of Object.entries(view)) if (v !== undefined && v !== null && v !== '') log(` ${c.dim(k.padEnd(18))} ${v}`);
|
|
80
85
|
return;
|
package/src/index.js
CHANGED
|
@@ -109,7 +109,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
109
109
|
// still error (nonzero exit) even with `--help`, so a bad command isn't silently
|
|
110
110
|
// accepted (JNG #97). Per-subcommand help is tracked in #89.
|
|
111
111
|
const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'db', 'version', 'help']);
|
|
112
|
-
|
|
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);
|
|
113
116
|
|
|
114
117
|
try {
|
|
115
118
|
// `agent setup` and `setup agent` both work (parity with InstaCloud's `setup agent`).
|
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
|
{
|
|
@@ -972,7 +959,7 @@ export const TOOLS = [
|
|
|
972
959
|
{
|
|
973
960
|
name: 'neo_estimate',
|
|
974
961
|
description:
|
|
975
|
-
'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)
|
|
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.',
|
|
976
963
|
mutating: false,
|
|
977
964
|
schema: {
|
|
978
965
|
type: 'object',
|
|
@@ -982,6 +969,8 @@ export const TOOLS = [
|
|
|
982
969
|
memoryMB: int('Memory in MB — priced per GB-RAM-hour (use with vcpu)'),
|
|
983
970
|
storageGB: int('Persistent/database storage in GB (optional)'),
|
|
984
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[]' },
|
|
985
974
|
replicas: int('Replica count (default 1)'),
|
|
986
975
|
orgId: str('Organization id (optional — applies that org\'s price overrides)'),
|
|
987
976
|
},
|
|
@@ -993,7 +982,7 @@ export const TOOLS = [
|
|
|
993
982
|
if (!orgId) { try { orgId = await api.resolveOrgId(); } catch { orgId = null; } }
|
|
994
983
|
if (!orgId) throw new Error('no organization for this account — cannot estimate cost');
|
|
995
984
|
const resources = specToResources(args);
|
|
996
|
-
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');
|
|
997
986
|
const { cost } = await api.estimate({ orgId }, { resources });
|
|
998
987
|
return cost; // normalized contract: currency/hourly/monthly/breakdown/variable/upfront/priceBook/priceBookVersion/assumptions
|
|
999
988
|
},
|
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
|
+
}
|