insta 0.0.33 → 0.0.35
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 +1 -0
- package/dist/commands/feedback.js +232 -0
- package/dist/commands/services.js +5 -4
- package/dist/index.js +21 -3
- package/dist/redact.js +78 -0
- package/dist/resolve-service.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -184,6 +184,7 @@ build never reaches a production installer.
|
|
|
184
184
|
| `insta approvals` | `list` · `approve` · `deny` |
|
|
185
185
|
| `insta policy` | `get` · `set <action> <decision>` |
|
|
186
186
|
| `insta observe` | `install` · `uninstall` · `report` · `sync` — local credential audit |
|
|
187
|
+
| `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building; works logged-out |
|
|
187
188
|
| `insta upgrade` · `autoupdate` | Update the CLI; show or set auto-update |
|
|
188
189
|
|
|
189
190
|
## Configuration
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// `insta feedback` — report an InstaCloud-side hurdle to the InstaCloud team.
|
|
2
|
+
//
|
|
3
|
+
// Scope rule (also stated in the skill): this is for problems in OUR toolkit — the CLI, the MCP
|
|
4
|
+
// server, the platform, the skills, the docs. Never for problems in the app the user is building.
|
|
5
|
+
//
|
|
6
|
+
// The backend is InstaCloud dogfooding itself: the "InstaCloud Agent Feedback" project runs the
|
|
7
|
+
// ingest service (InsForge/insta-feedback repo) on a postgres + compute pair. It is NOT the
|
|
8
|
+
// control-plane API on purpose — feedback must work logged-out, unlinked, and from insta-oss,
|
|
9
|
+
// and a control-plane outage is exactly when we most want reports to still arrive.
|
|
10
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import * as clack from '@clack/prompts';
|
|
13
|
+
import { readGlobal, readProject } from '../config.js';
|
|
14
|
+
import { envForApiUrl } from '../env.js';
|
|
15
|
+
import { info, printJson } from '../util.js';
|
|
16
|
+
import { clean } from '../redact.js';
|
|
17
|
+
export const TYPES = ['bug', 'feature-request', 'friction', 'other'];
|
|
18
|
+
export const COMPONENTS = ['cli', 'mcp', 'platform', 'skills', 'docs', 'other'];
|
|
19
|
+
export const SEVERITIES = ['blocker', 'major', 'minor'];
|
|
20
|
+
// Field caps mirror the ingest service's LIMITS (insta-feedback src/app.ts) — the server
|
|
21
|
+
// truncates again, so a mismatch degrades gracefully instead of rejecting.
|
|
22
|
+
export const LIMITS = {
|
|
23
|
+
title: 200,
|
|
24
|
+
detail: 4000,
|
|
25
|
+
area: 100,
|
|
26
|
+
command: 500,
|
|
27
|
+
error: 2000,
|
|
28
|
+
expected: 1000,
|
|
29
|
+
workaround: 1000,
|
|
30
|
+
doc: 300,
|
|
31
|
+
};
|
|
32
|
+
// Hardcoded in source, not injected at build time: a build-time credential silently no-ops in
|
|
33
|
+
// local/tsx and fork builds, and feedback would appear to work while reports vanish. The token is
|
|
34
|
+
// public by design (it ships in this file); it only deflects drive-by scanners — real abuse
|
|
35
|
+
// control is server-side (per-IP rate limit + weekly dedup). Env overrides are for tests and
|
|
36
|
+
// emergency rotation.
|
|
37
|
+
const FEEDBACK_ENDPOINT = process.env.INSTA_FEEDBACK_URL ||
|
|
38
|
+
'https://feedback.instacloud.com/v1/feedback';
|
|
39
|
+
const FEEDBACK_INGEST_TOKEN = process.env.INSTA_FEEDBACK_TOKEN || 'insta-feedback-public-v1';
|
|
40
|
+
const FEEDBACK_TIMEOUT_MS = 10_000;
|
|
41
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
42
|
+
function resolveCliVersion() {
|
|
43
|
+
// Same resolution as index.ts: the standalone binary bakes INSTA_CLI_VERSION via --define;
|
|
44
|
+
// npm/node reads the installed package.json next to dist/.
|
|
45
|
+
if (process.env.INSTA_CLI_VERSION)
|
|
46
|
+
return process.env.INSTA_CLI_VERSION;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return '0.0.0';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function requireEnum(value, allowed, flag) {
|
|
55
|
+
if (!allowed.includes(value)) {
|
|
56
|
+
throw new Error(`${flag} must be one of: ${allowed.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
async function promptMissing(opts) {
|
|
61
|
+
clack.intro('insta feedback — report an InstaCloud-side hurdle');
|
|
62
|
+
if (!opts.type) {
|
|
63
|
+
const answer = await clack.select({
|
|
64
|
+
message: 'What kind of hurdle did you hit?',
|
|
65
|
+
options: [
|
|
66
|
+
{ value: 'bug', label: 'bug — something InstaCloud should do, but does not' },
|
|
67
|
+
{ value: 'feature-request', label: 'feature-request — something InstaCloud does not support yet' },
|
|
68
|
+
{ value: 'friction', label: 'friction — works, but confusing or awkward' },
|
|
69
|
+
{ value: 'other', label: 'other' },
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
if (clack.isCancel(answer))
|
|
73
|
+
process.exit(0);
|
|
74
|
+
opts.type = answer;
|
|
75
|
+
}
|
|
76
|
+
if (!opts.component) {
|
|
77
|
+
const answer = await clack.select({
|
|
78
|
+
message: 'Where in the InstaCloud toolkit is the issue?',
|
|
79
|
+
options: COMPONENTS.map((c) => ({ value: c, label: c })),
|
|
80
|
+
});
|
|
81
|
+
if (clack.isCancel(answer))
|
|
82
|
+
process.exit(0);
|
|
83
|
+
opts.component = answer;
|
|
84
|
+
}
|
|
85
|
+
if (!opts.title) {
|
|
86
|
+
const answer = await clack.text({
|
|
87
|
+
message: 'One-line summary:',
|
|
88
|
+
validate: (v) => (v.trim() ? undefined : 'required'),
|
|
89
|
+
});
|
|
90
|
+
if (clack.isCancel(answer))
|
|
91
|
+
process.exit(0);
|
|
92
|
+
opts.title = answer.trim();
|
|
93
|
+
}
|
|
94
|
+
if (!opts.detail && !opts.file) {
|
|
95
|
+
const answer = await clack.text({
|
|
96
|
+
message: 'What happened, and what did you expect?',
|
|
97
|
+
validate: (v) => (v.trim() ? undefined : 'required'),
|
|
98
|
+
});
|
|
99
|
+
if (clack.isCancel(answer))
|
|
100
|
+
process.exit(0);
|
|
101
|
+
opts.detail = answer.trim();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Pure payload assembly (unit-tested): validation, redaction, caps, and ambient context. */
|
|
105
|
+
export async function buildPayload(opts, ctx) {
|
|
106
|
+
const type = requireEnum(opts.type ?? '', TYPES, '--type');
|
|
107
|
+
const component = requireEnum(opts.component ?? '', COMPONENTS, '--component');
|
|
108
|
+
const severity = opts.severity ? requireEnum(opts.severity, SEVERITIES, '--severity') : 'minor';
|
|
109
|
+
let detail = opts.detail;
|
|
110
|
+
if (!detail && opts.file) {
|
|
111
|
+
try {
|
|
112
|
+
// detail is capped at 4000 chars — a file far beyond that is a mistake (wrong path, a log
|
|
113
|
+
// archive, a binary), so refuse before allocating it rather than truncating garbage.
|
|
114
|
+
// Regular files only: a FIFO/device node (e.g. /dev/zero) stats as size 0, sails past the
|
|
115
|
+
// byte ceiling, and then readFileSync reads unbounded.
|
|
116
|
+
const stat = statSync(opts.file);
|
|
117
|
+
if (!stat.isFile())
|
|
118
|
+
throw new Error(`--file ${opts.file} is not a regular file`);
|
|
119
|
+
const size = stat.size;
|
|
120
|
+
if (size > MAX_FILE_BYTES) {
|
|
121
|
+
throw new Error(`--file ${opts.file} is ${size} bytes — max ${MAX_FILE_BYTES} (detail is capped at ${LIMITS.detail} chars; trim the file first)`);
|
|
122
|
+
}
|
|
123
|
+
detail = readFileSync(opts.file, 'utf8');
|
|
124
|
+
if (detail.includes('\0'))
|
|
125
|
+
throw new Error(`--file ${opts.file} looks binary — feedback detail must be text`);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
129
|
+
throw new Error(msg.startsWith('--file') ? msg : `--file ${opts.file}: ${msg}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const title = clean(opts.title, LIMITS.title);
|
|
133
|
+
if (!title)
|
|
134
|
+
throw new Error('--title is required (one-line summary, ≤200 chars)');
|
|
135
|
+
const cleanedDetail = clean(detail, LIMITS.detail);
|
|
136
|
+
if (!cleanedDetail)
|
|
137
|
+
throw new Error('--detail (or --file <path>) is required: what happened vs what you expected');
|
|
138
|
+
const project = await readProject();
|
|
139
|
+
const { apiUrl } = await readGlobal();
|
|
140
|
+
// envForApiUrl → null means a custom host: insta-oss or a preview deployment (see env.ts).
|
|
141
|
+
const target = envForApiUrl(apiUrl) ? 'cloud' : 'oss';
|
|
142
|
+
return {
|
|
143
|
+
type,
|
|
144
|
+
component,
|
|
145
|
+
severity,
|
|
146
|
+
title,
|
|
147
|
+
detail: cleanedDetail,
|
|
148
|
+
area: clean(opts.area, LIMITS.area),
|
|
149
|
+
command: clean(opts.command, LIMITS.command),
|
|
150
|
+
error: clean(opts.error, LIMITS.error),
|
|
151
|
+
expected: clean(opts.expected, LIMITS.expected),
|
|
152
|
+
workaround: clean(opts.workaround, LIMITS.workaround),
|
|
153
|
+
doc_ref: clean(opts.doc, LIMITS.doc),
|
|
154
|
+
source: 'cli',
|
|
155
|
+
target,
|
|
156
|
+
client_version: ctx.cliVersion,
|
|
157
|
+
node_version: process.version,
|
|
158
|
+
os: `${os.platform()} ${os.release()}`,
|
|
159
|
+
project_id: project?.projectId,
|
|
160
|
+
org_id: project?.orgId,
|
|
161
|
+
branch: project?.branch,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** One POST, 10s timeout, zero retries — feedback is a side quest and must never hang the CLI.
|
|
165
|
+
* Transport and server failures come back as a result, not an exception: the caller downgrades
|
|
166
|
+
* them to a warning so a broken feedback backend can't fail the user's actual task. */
|
|
167
|
+
export async function submit(payload, fetchImpl) {
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = await fetchImpl(FEEDBACK_ENDPOINT, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
Authorization: `Bearer ${FEEDBACK_INGEST_TOKEN}`,
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(payload),
|
|
177
|
+
signal: AbortSignal.timeout(FEEDBACK_TIMEOUT_MS),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
const timedOut = e instanceof Error && e.name === 'TimeoutError';
|
|
182
|
+
return { status: 'error', error: timedOut ? `timed out after ${FEEDBACK_TIMEOUT_MS / 1000}s` : `network error: ${e instanceof Error ? e.message : String(e)}` };
|
|
183
|
+
}
|
|
184
|
+
let body = {};
|
|
185
|
+
try {
|
|
186
|
+
body = await res.json();
|
|
187
|
+
}
|
|
188
|
+
catch { /* non-JSON body — fall through to status handling */ }
|
|
189
|
+
if (!res.ok)
|
|
190
|
+
return { status: 'error', error: body?.error ?? `HTTP ${res.status}` };
|
|
191
|
+
return { status: body?.status === 'duplicate' ? 'duplicate' : 'received', id: body?.id ?? null };
|
|
192
|
+
}
|
|
193
|
+
export async function feedback(opts, deps = {}) {
|
|
194
|
+
const interactive = deps.interactive ?? (!opts.json && !!process.stdin.isTTY && !!process.stdout.isTTY);
|
|
195
|
+
const missingRequired = !opts.type || !opts.component || !opts.title || (!opts.detail && !opts.file);
|
|
196
|
+
if (missingRequired && interactive)
|
|
197
|
+
await promptMissing(opts);
|
|
198
|
+
// Bad/missing input exits 1 either way — an agent CAN fix its flags, so the error must be loud
|
|
199
|
+
// and self-teaching (it lists the exact enum values). But it must arrive on the channel the
|
|
200
|
+
// caller chose: --json gets a machine-readable object on stdout (uniform with the success and
|
|
201
|
+
// transport-failure shapes) instead of guard()'s plaintext stderr line.
|
|
202
|
+
let payload;
|
|
203
|
+
try {
|
|
204
|
+
payload = await buildPayload(opts, { cliVersion: deps.cliVersion ?? resolveCliVersion() });
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (!opts.json)
|
|
208
|
+
throw e;
|
|
209
|
+
printJson({ status: 'error', submitted: false, error: e instanceof Error ? e.message : String(e) });
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const result = await submit(payload, deps.fetchImpl ?? fetch);
|
|
214
|
+
if (result.status === 'error') {
|
|
215
|
+
// Deliberate exit 0: an agent CANNOT fix a down/rate-limited backend, and feedback must never
|
|
216
|
+
// fail or distract from the task the user actually asked for. Do not retry.
|
|
217
|
+
if (opts.json)
|
|
218
|
+
return printJson({ status: 'error', submitted: false, error: result.error });
|
|
219
|
+
process.stderr.write(`warning: feedback not submitted (${result.error}) — continue with your task, do not retry\n`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (opts.json)
|
|
223
|
+
return printJson({ status: result.status, id: result.id });
|
|
224
|
+
if (result.status === 'duplicate') {
|
|
225
|
+
info(`already reported this week — bumped its count instead (id: ${result.id})`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
info(`feedback submitted (id: ${result.id}) — thank you!`);
|
|
229
|
+
}
|
|
230
|
+
info('PII (emails, tokens, keys, home paths) was redacted before sending.');
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// `insta services` — manage a project's opt-in services (postgres | storage | compute).
|
|
1
|
+
// `insta services` — manage a project's opt-in services (postgres | storage | compute | redis).
|
|
2
2
|
import { ApiClient, requireProject } from '../api.js';
|
|
3
3
|
import { info, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
|
-
export const SERVICE_TYPES = ['postgres', 'storage', 'compute'];
|
|
4
|
+
export const SERVICE_TYPES = ['postgres', 'storage', 'compute', 'redis'];
|
|
5
5
|
const SERVICE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;
|
|
6
6
|
export function q(branch) {
|
|
7
7
|
return branch ? `?branch=${encodeURIComponent(branch)}` : '';
|
|
@@ -124,7 +124,8 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
124
124
|
export function serviceListLine(s) {
|
|
125
125
|
const extra = s.type === 'compute'
|
|
126
126
|
? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
|
|
127
|
-
: s.type === '
|
|
127
|
+
: s.type === 'redis' ? ` tcp/${s.port ?? 6379}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
|
|
128
|
+
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
|
|
128
129
|
return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
|
|
129
130
|
}
|
|
130
131
|
export async function servicesList(opts) {
|
|
@@ -135,7 +136,7 @@ export async function servicesList(opts) {
|
|
|
135
136
|
if (opts.json)
|
|
136
137
|
return printJson(services);
|
|
137
138
|
if (!services.length)
|
|
138
|
-
return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`);
|
|
139
|
+
return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute|redis> <name>\`)`);
|
|
139
140
|
for (const s of services)
|
|
140
141
|
info(serviceListLine(s));
|
|
141
142
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import * as observe from './commands/observe.js';
|
|
|
26
26
|
import * as obs from './commands/metrics.js';
|
|
27
27
|
import { billing, billingUpgrade, billingPortal } from './commands/billing.js';
|
|
28
28
|
import * as selfUpdate from './commands/upgrade.js';
|
|
29
|
+
import * as feedbackCmd from './commands/feedback.js';
|
|
29
30
|
function onError(e) {
|
|
30
31
|
if (e instanceof ApiError)
|
|
31
32
|
die(`${e.message} (HTTP ${e.status})`);
|
|
@@ -108,14 +109,14 @@ br.command('switch <name>').action(guard((name) => branch.branchSwitch(name)));
|
|
|
108
109
|
br.command('delete <name>').action(guard((name) => branch.branchDelete(name)));
|
|
109
110
|
br.command('merge <source>').description('Merge a branch service set into another (structural, no data)')
|
|
110
111
|
.option('--into <branch>', 'target branch (default: current)').action(guard((source, o) => branch.branchMerge(source, o)));
|
|
111
|
-
// ---- services (opt-in postgres/storage/compute) ----
|
|
112
|
-
const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
|
|
112
|
+
// ---- services (opt-in postgres/storage/compute/redis) ----
|
|
113
|
+
const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute|redis)');
|
|
113
114
|
// [type] [name] are optional so the command can answer "what can I add?" — a terminal is walked
|
|
114
115
|
// through the dashboard's Add Service kinds, anything else gets that list back as an error
|
|
115
116
|
// (resolve-service.ts). Picking Docker Image also fills in --image/--port from the answers.
|
|
116
117
|
svc.command('add [type] [name]').description('Provision a service on demand (assigns a default domain for postgres/compute); with no type/name, a terminal picks from the service kinds')
|
|
117
118
|
.option('--branch <branch>', 'target branch (default: current)')
|
|
118
|
-
.option('--region <region>', 'region for postgres/compute, e.g. us-east (see `insta regions`)')
|
|
119
|
+
.option('--region <region>', 'region for postgres/compute/redis, e.g. us-east (see `insta regions`)')
|
|
119
120
|
.option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
|
|
120
121
|
.option('--image <url>', 'compute only: run this container image at creation')
|
|
121
122
|
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
|
|
@@ -259,6 +260,23 @@ ob.command('sync').description('Upload findings into the project timeline').acti
|
|
|
259
260
|
const pol = program.command('policy').description('Governance policy');
|
|
260
261
|
pol.command('get').option('--json').action(guard((o) => govern.policyGet(o)));
|
|
261
262
|
pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade|service.setAccess|storage.read|storage.write|storage.delete; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)));
|
|
263
|
+
// ---- feedback (agent + human hurdle reports → the InstaCloud team) ----
|
|
264
|
+
program.command('feedback')
|
|
265
|
+
.description('Report an InstaCloud-side hurdle (bug / missing feature / friction) to the InstaCloud team — about the insta toolkit itself, NEVER about the app you are building. Works logged-out and unlinked.')
|
|
266
|
+
.option('--type <type>', `what kind of hurdle: ${feedbackCmd.TYPES.join(' | ')}`)
|
|
267
|
+
.option('--component <component>', `which part of the toolkit: ${feedbackCmd.COMPONENTS.join(' | ')}`)
|
|
268
|
+
.option('--title <title>', 'one-line summary (≤200 chars)')
|
|
269
|
+
.option('--detail <text>', 'what happened vs what you expected (≤4000 chars)')
|
|
270
|
+
.option('--file <path>', 'read the detail from a file instead of --detail')
|
|
271
|
+
.option('--area <area>', 'product area, free text: deploy, branch, secrets, db, storage, compute, governance, billing, …')
|
|
272
|
+
.option('--command <cmd>', 'the insta command that hit the issue')
|
|
273
|
+
.option('--error <text>', 'error output (redacted + truncated locally before sending)')
|
|
274
|
+
.option('--expected <text>', 'what the docs/skill said should happen')
|
|
275
|
+
.option('--workaround <text>', 'what you did instead, if anything worked')
|
|
276
|
+
.option('--doc <ref>', 'doc or skill file that led you here (for stale-instruction reports)')
|
|
277
|
+
.option('--severity <severity>', `${feedbackCmd.SEVERITIES.join(' | ')} (default: minor)`)
|
|
278
|
+
.option('--json')
|
|
279
|
+
.action(guard((o) => feedbackCmd.feedback(o)));
|
|
262
280
|
// ---- self-update ----
|
|
263
281
|
program.command('upgrade').description('Update the insta CLI to the latest release (binary or npm install)')
|
|
264
282
|
.action(guard(() => selfUpdate.upgrade()));
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Local PII redaction for outbound feedback text. Pattern-based: a safety net, not a license to
|
|
2
|
+
// paste credentials. The ingest service re-scrubs server-side with the same patterns
|
|
3
|
+
// (insta-feedback repo), so a drift here is caught by the second pass. IPv6 and phone numbers are
|
|
4
|
+
// deliberately not matched — the false-positive rate against UUIDs and hashes destroys the
|
|
5
|
+
// diagnostic value of error text.
|
|
6
|
+
const PATTERNS = [
|
|
7
|
+
// URL-embedded credentials: scheme://user:pass@host (DATABASE_URLs pasted into error output)
|
|
8
|
+
[/(\w+:\/\/)[^\s/@:]+:[^\s/@]+@/g, '$1[REDACTED]@'],
|
|
9
|
+
// JWTs
|
|
10
|
+
[/eyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{5,}/g, '[REDACTED_JWT]'],
|
|
11
|
+
// Bearer tokens
|
|
12
|
+
[/\b[Bb]earer\s+[\w~+/.=-]{8,}/g, 'Bearer [REDACTED]'],
|
|
13
|
+
// insta_ platform tokens are insta_ + a ≥24-char Better Auth apiKey. The tail must stay ≥24:
|
|
14
|
+
// MCP tool names (insta_feedback, insta_storage_download_url, …) share the prefix with tails
|
|
15
|
+
// up to 20 chars, and they are exactly what feedback text quotes most.
|
|
16
|
+
[/\binsta_[\w-]{24,}/g, '[REDACTED_KEY]'],
|
|
17
|
+
// Common third-party key prefixes
|
|
18
|
+
[/\b(?:uak_|sk_live_|sk_test_|whsec_|ghp_|github_pat_|npm_|AIza|xox[a-z]-)[\w-]{6,}/g, '[REDACTED_KEY]'],
|
|
19
|
+
[/\bsk-[\w-]{16,}/g, '[REDACTED_KEY]'],
|
|
20
|
+
[/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]'],
|
|
21
|
+
// Generic assignments: password=..., api_key: "..."
|
|
22
|
+
[/\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token)\b(\s*[:=]\s*)["']?[^\s"'&,;]{4,}["']?/gi, '$1$2[REDACTED]'],
|
|
23
|
+
// Emails
|
|
24
|
+
[/[\w.+-]+@[\w-]+\.[\w.-]+/g, '[REDACTED_EMAIL]'],
|
|
25
|
+
// Home directories carry the username (unix + windows)
|
|
26
|
+
[/\/(?:Users|home)\/[\w.-]+/g, '~'],
|
|
27
|
+
[/[A-Z]:[\\/]Users[\\/][\w.-]+/g, '~'],
|
|
28
|
+
];
|
|
29
|
+
// Public IPv4 only — private/loopback ranges are kept for their debug value.
|
|
30
|
+
// Known asymmetry with the IPv6/phone exemption above: a 4-part version or build string whose
|
|
31
|
+
// octets all fit 0–255 (e.g. "25.2.0.100") gets over-scrubbed. Accepted deliberately — dotted
|
|
32
|
+
// quads in error text are overwhelmingly real addresses, an over-scrub is recoverable noise,
|
|
33
|
+
// and an under-scrub is a leak; anchoring on surrounding context would trade that for misses.
|
|
34
|
+
const IPV4 = /\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/g;
|
|
35
|
+
function isPrivateIp(a, b) {
|
|
36
|
+
if (a === 10 || a === 127 || a === 0)
|
|
37
|
+
return true;
|
|
38
|
+
if (a === 192 && b === 168)
|
|
39
|
+
return true;
|
|
40
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
41
|
+
return true;
|
|
42
|
+
if (a === 169 && b === 254)
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
export function redactSensitive(text) {
|
|
47
|
+
let out = text;
|
|
48
|
+
for (const [re, sub] of PATTERNS)
|
|
49
|
+
out = out.replace(re, sub);
|
|
50
|
+
out = out.replace(IPV4, (m, a, b, c, d) => {
|
|
51
|
+
const [na, nb, nc, nd] = [Number(a), Number(b), Number(c), Number(d)];
|
|
52
|
+
if (na > 255 || nb > 255 || nc > 255 || nd > 255)
|
|
53
|
+
return m;
|
|
54
|
+
return isPrivateIp(na, nb) ? m : '[REDACTED_IP]';
|
|
55
|
+
});
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Middle truncation keeping 60% head + 40% tail — the start of an error names the failure, the
|
|
59
|
+
* end carries the actual cause; the middle is usually a stack. */
|
|
60
|
+
export function truncateMiddle(text, max) {
|
|
61
|
+
if (text.length <= max)
|
|
62
|
+
return text;
|
|
63
|
+
const marker = `…[${text.length - max} chars truncated]…`;
|
|
64
|
+
const head = Math.floor(max * 0.6);
|
|
65
|
+
const tail = max - head;
|
|
66
|
+
return text.slice(0, head) + marker + text.slice(text.length - tail);
|
|
67
|
+
}
|
|
68
|
+
/** Redact BEFORE truncating: truncating first could leave half a token visible at the cut and the
|
|
69
|
+
* redaction pattern would no longer match it. */
|
|
70
|
+
export function clean(value, max) {
|
|
71
|
+
if (typeof value !== 'string')
|
|
72
|
+
return undefined;
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
return undefined;
|
|
76
|
+
return truncateMiddle(redactSensitive(trimmed), max);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=redact.js.map
|
package/dist/resolve-service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `insta services add` with no type (or no name): the kinds are otherwise only discoverable by
|
|
2
|
-
// guessing wrong and reading `type must be postgres|storage|compute`, so missing arguments answer
|
|
2
|
+
// guessing wrong and reading `type must be postgres|storage|compute|redis`, so missing arguments answer
|
|
3
3
|
// "what can I add?" instead. The list mirrors the dashboard's Add Service menu (frontend
|
|
4
4
|
// `add-service-button.tsx`) — Docker Image sits BESIDE Empty Service, not under it, because
|
|
5
5
|
// picking an image is a different intent rather than a compute flag. An agent gets the same list
|
|
@@ -11,6 +11,7 @@ import { SERVICE_TYPES, assertServiceName, parsePort } from './commands/services
|
|
|
11
11
|
export const SERVICE_KINDS = [
|
|
12
12
|
{ id: 'image', label: 'Docker Image', type: 'compute', hint: 'run an existing container image', needsImage: true },
|
|
13
13
|
{ id: 'postgres', label: 'Postgres', type: 'postgres', hint: 'relational DB, usable as soon as it is added', defaultName: 'main-db' },
|
|
14
|
+
{ id: 'redis', label: 'Redis', type: 'redis', hint: 'private Redis-compatible cache', defaultName: 'cache' },
|
|
14
15
|
{ id: 'storage', label: 'Storage', type: 'storage', hint: 'S3-compatible bucket, private by default', defaultName: 'assets' },
|
|
15
16
|
{ id: 'compute', label: 'Empty Service', type: 'compute', hint: 'an app to deploy code to (empty until `insta deploy`)', defaultName: 'compute' },
|
|
16
17
|
];
|