insta 0.1.3 → 0.1.5
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 -1
- package/dist/commands/domain.js +87 -0
- package/dist/commands/feedback.js +50 -10
- package/dist/index.js +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ build never reaches a production installer.
|
|
|
230
230
|
| `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
|
|
231
231
|
| `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
|
|
232
232
|
| `insta config` | `install-mcp` · `regions` · `autoupdate` |
|
|
233
|
-
| `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building;
|
|
233
|
+
| `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building; on InstaCloud it needs `insta login`, so the team can reply |
|
|
234
234
|
| `insta upgrade` | Update the CLI |
|
|
235
235
|
|
|
236
236
|
Every command accepts `--api-url <url>` for this invocation only (internal debugging); for `compute exec`, place it before `compute`. `insta --help` documents it.
|
package/dist/commands/domain.js
CHANGED
|
@@ -336,4 +336,91 @@ export async function domainDetach(host, opts, deps) {
|
|
|
336
336
|
}
|
|
337
337
|
return removeDomain(name, opts, d);
|
|
338
338
|
}
|
|
339
|
+
const zonePath = (orgId, domainName) => `/orgs/${encodeURIComponent(orgId)}/zones${domainName ? `/${encodeURIComponent(domainName.trim().toLowerCase())}` : ''}`;
|
|
340
|
+
export function zoneLines(z, orgFlag) {
|
|
341
|
+
const orgArg = orgFlag ? ` --org ${orgFlag}` : '';
|
|
342
|
+
if (z.status === 'active')
|
|
343
|
+
return [`${z.domainName} delegated (${z.nameservers.join(', ')})`];
|
|
344
|
+
return [
|
|
345
|
+
`${z.domainName} waiting for nameservers`,
|
|
346
|
+
` set these at your domain's registrar: ${z.nameservers.join(', ')}`,
|
|
347
|
+
` review the zone BEFORE switching: insta domain zone records ${z.domainName}${orgArg}`,
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
export function zoneRecordLines(records) {
|
|
351
|
+
if (!records.length)
|
|
352
|
+
return ['no records in the zone yet — the provider scan runs shortly after delegating; run this again in a moment'];
|
|
353
|
+
const w = (pick) => Math.max(...records.map((r) => pick(r).length));
|
|
354
|
+
const typeW = w((r) => r.type), hostW = w((r) => r.host), answerW = w((r) => r.answer);
|
|
355
|
+
return records.map((r) => ` ${r.type.padEnd(typeW)} ${r.host.padEnd(hostW)} ${r.answer.padEnd(answerW)}${r.ttl !== undefined ? ` ttl ${r.ttl}` : ''}${r.priority !== undefined ? ` prio ${r.priority}` : ''}${r.proxied ? ' (proxied)' : ''}`);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Delegate a bring-your-own domain: the platform builds a managed zone for it and answers the two
|
|
359
|
+
* nameservers to set at the domain's own registrar. From then on every attach publishes its records
|
|
360
|
+
* into the zone itself — apexes included — instead of printing them for hand-copying. The zone is
|
|
361
|
+
* SEEDED by the provider's record scan, which is a heuristic: the review-then-switch contract
|
|
362
|
+
* (printed, and enforced by nothing else) is to compare `zone records` against the domain's current
|
|
363
|
+
* DNS and add what is missing at the CURRENT provider — re-running delegate re-imports — before
|
|
364
|
+
* re-pointing. A domain carrying live MX records is refused outright: a DNS move that can drop
|
|
365
|
+
* mail is never done implicitly. 202 approval_required in agent mode (zone.delegate); org admin
|
|
366
|
+
* either way.
|
|
367
|
+
*/
|
|
368
|
+
const orgArgOf = (opts) => (opts.org ? ` --org ${opts.org}` : '');
|
|
369
|
+
export async function zoneDelegate(domainName, opts, deps) {
|
|
370
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
371
|
+
// Same precedent as `domain delegate`: the org route signs for the linked project in agent mode
|
|
372
|
+
// (zone.delegate is read at the session project), but only when the link belongs to the org
|
|
373
|
+
// being mutated — under `--org` naming another org the call goes projectless and the platform's
|
|
374
|
+
// org-administration path judges it.
|
|
375
|
+
const link = deps?.project ?? (await readProject()) ?? undefined;
|
|
376
|
+
const projectId = link && link.orgId === orgId ? link.projectId : undefined;
|
|
377
|
+
const res = await api.rawRequest('POST', zonePath(orgId), { domainName }, projectId ? { projectId } : undefined);
|
|
378
|
+
if (handleApproval(res, opts.json))
|
|
379
|
+
return;
|
|
380
|
+
if (opts.json)
|
|
381
|
+
return printJson(res.body);
|
|
382
|
+
const z = res.body;
|
|
383
|
+
for (const line of zoneLines(z, opts.org))
|
|
384
|
+
info(line);
|
|
385
|
+
info(`the zone was seeded by a provider scan — a heuristic. Check \`insta domain zone records ${z.domainName}${orgArgOf(opts)}\` against your current DNS, add anything missing at your CURRENT provider (re-running delegate re-imports), and only then switch the nameservers.`);
|
|
386
|
+
}
|
|
387
|
+
export async function zoneList(opts, deps) {
|
|
388
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
389
|
+
const r = await api.request('GET', zonePath(orgId));
|
|
390
|
+
if (opts.json)
|
|
391
|
+
return printJson(r);
|
|
392
|
+
if (!r.items.length)
|
|
393
|
+
return info('no delegated zones — start one: insta domain zone delegate <domain>');
|
|
394
|
+
for (const z of r.items)
|
|
395
|
+
for (const line of zoneLines(z, opts.org))
|
|
396
|
+
info(line);
|
|
397
|
+
}
|
|
398
|
+
export async function zoneRecords(domainName, opts, deps) {
|
|
399
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
400
|
+
const r = await api.request('GET', `${zonePath(orgId, domainName)}/records`);
|
|
401
|
+
if (opts.json)
|
|
402
|
+
return printJson(r);
|
|
403
|
+
for (const line of zoneRecordLines(r.items))
|
|
404
|
+
info(line);
|
|
405
|
+
if (r.items.length)
|
|
406
|
+
info(`every type shows here (the scan is a heuristic) — add anything missing at your current DNS provider and re-run \`insta domain zone delegate ${domainName.trim().toLowerCase()}${orgArgOf(opts)}\` to re-import before switching nameservers`);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Release a delegated zone: the platform prunes the records it published and deletes the managed
|
|
410
|
+
* zone. The customer's next step — printed — is pointing the domain's nameservers back at their
|
|
411
|
+
* own provider; hostnames then re-verify on the records path.
|
|
412
|
+
*/
|
|
413
|
+
export async function zoneRelease(domainName, opts, deps) {
|
|
414
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
415
|
+
const link = deps?.project ?? (await readProject()) ?? undefined;
|
|
416
|
+
const projectId = link && link.orgId === orgId ? link.projectId : undefined;
|
|
417
|
+
const res = await api.rawRequest('DELETE', zonePath(orgId, domainName), undefined, projectId ? { projectId } : undefined);
|
|
418
|
+
if (handleApproval(res, opts.json))
|
|
419
|
+
return;
|
|
420
|
+
if (opts.json)
|
|
421
|
+
return printJson(res.body);
|
|
422
|
+
const r = res.body;
|
|
423
|
+
info(`${r.domainName} released`);
|
|
424
|
+
info(`point the domain's nameservers back at your DNS provider — hostnames re-verify on the records path (insta domain attach prints them)`);
|
|
425
|
+
}
|
|
339
426
|
//# sourceMappingURL=domain.js.map
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The backend is InstaCloud dogfooding itself: the "InstaCloud Agent Feedback" project runs the
|
|
7
7
|
// ingest service (InsForge/instacloud-feedback repo) on a postgres + compute pair. It is NOT the
|
|
8
|
-
// control-plane API on purpose — feedback must work
|
|
9
|
-
//
|
|
8
|
+
// control-plane API on purpose — feedback must work unlinked, from insta-oss, and through a
|
|
9
|
+
// control-plane outage, which is exactly when we most want reports to still arrive.
|
|
10
10
|
import { readFileSync, statSync } from 'node:fs';
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import * as clack from '@clack/prompts';
|
|
13
|
+
import { ApiClient, ApiError } from '../api.js';
|
|
13
14
|
import { readGlobal, readProject } from '../config.js';
|
|
14
15
|
import { envForApiUrl } from '../env.js';
|
|
15
|
-
import { info, printJson, CliCancel } from '../util.js';
|
|
16
|
+
import { info, printJson, refuse, CliCancel } from '../util.js';
|
|
16
17
|
import { clean } from '../redact.js';
|
|
17
18
|
import { cliVersion } from '../version.js';
|
|
18
19
|
export const TYPES = ['bug', 'feature-request', 'friction', 'other'];
|
|
@@ -42,6 +43,8 @@ const FEEDBACK_INGEST_TOKEN = process.env.INSTA_FEEDBACK_TOKEN || 'insta-feedbac
|
|
|
42
43
|
// the DB wake and persists, so a report can land after a shorter deadline gave up on it).
|
|
43
44
|
// An expired deadline is reported as UNCONFIRMED, not failed — the report may well be stored.
|
|
44
45
|
const FEEDBACK_TIMEOUT_MS = 15_000;
|
|
46
|
+
// A slow control plane may cost a report its ticket, never the report itself.
|
|
47
|
+
const ASSERTION_TIMEOUT_MS = 5_000;
|
|
45
48
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
46
49
|
function requireEnum(value, allowed, flag) {
|
|
47
50
|
if (!allowed.includes(value)) {
|
|
@@ -156,7 +159,7 @@ export async function buildPayload(opts, ctx) {
|
|
|
156
159
|
/** One POST, one bounded attempt (FEEDBACK_TIMEOUT_MS), zero retries — feedback is a side quest and must never hang the CLI.
|
|
157
160
|
* Transport and server failures come back as a result, not an exception: the caller downgrades
|
|
158
161
|
* them to a warning so a broken feedback backend can't fail the user's actual task. */
|
|
159
|
-
export async function submit(payload, fetchImpl) {
|
|
162
|
+
export async function submit(payload, fetchImpl, assertion) {
|
|
160
163
|
let res;
|
|
161
164
|
try {
|
|
162
165
|
res = await fetchImpl(FEEDBACK_ENDPOINT, {
|
|
@@ -164,6 +167,7 @@ export async function submit(payload, fetchImpl) {
|
|
|
164
167
|
headers: {
|
|
165
168
|
'Content-Type': 'application/json',
|
|
166
169
|
Authorization: `Bearer ${FEEDBACK_INGEST_TOKEN}`,
|
|
170
|
+
...(assertion ? { 'Insta-User-Assertion': assertion } : {}),
|
|
167
171
|
},
|
|
168
172
|
body: JSON.stringify(payload),
|
|
169
173
|
signal: AbortSignal.timeout(FEEDBACK_TIMEOUT_MS),
|
|
@@ -184,7 +188,34 @@ export async function submit(payload, fetchImpl) {
|
|
|
184
188
|
return { status: 'error', error: body?.error ?? `HTTP ${res.status}` };
|
|
185
189
|
return { status: body?.status === 'duplicate' ? 'duplicate' : 'received', id: body?.id ?? null };
|
|
186
190
|
}
|
|
191
|
+
const SIGNED_OUT = 'not signed in to InstaCloud — run `insta login`, then send this again so the team can reply to you';
|
|
192
|
+
const STAGING = 'not accepted from staging — send InstaCloud feedback from production';
|
|
193
|
+
// Exit 2, not the submit path's 0: the caller can act on this one.
|
|
194
|
+
function refuseFeedback(message, json) {
|
|
195
|
+
if (json)
|
|
196
|
+
printJson({ status: 'refused', submitted: false, error: message });
|
|
197
|
+
refuse([`insta feedback: ${message}`]);
|
|
198
|
+
}
|
|
199
|
+
function inputError(e, json) {
|
|
200
|
+
if (!json)
|
|
201
|
+
throw e;
|
|
202
|
+
printJson({ status: 'error', submitted: false, error: e instanceof Error ? e.message : String(e) });
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
}
|
|
187
205
|
export async function feedback(opts, deps = {}) {
|
|
206
|
+
let api;
|
|
207
|
+
try {
|
|
208
|
+
api = deps.api ?? await ApiClient.load();
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
return inputError(e, opts.json);
|
|
212
|
+
}
|
|
213
|
+
const env = envForApiUrl(api.apiUrl);
|
|
214
|
+
if (env === 'staging')
|
|
215
|
+
refuseFeedback(STAGING, opts.json);
|
|
216
|
+
// Before the prompts, so nobody types out a report only to be told to sign in.
|
|
217
|
+
if (env === 'prod' && !api.config.accessToken)
|
|
218
|
+
refuseFeedback(SIGNED_OUT, opts.json);
|
|
188
219
|
const interactive = deps.interactive ?? (!opts.json && !!process.stdin.isTTY && !!process.stdout.isTTY);
|
|
189
220
|
const missingRequired = !opts.type || !opts.component || !opts.title || (!opts.detail && !opts.file);
|
|
190
221
|
if (missingRequired && interactive)
|
|
@@ -198,13 +229,22 @@ export async function feedback(opts, deps = {}) {
|
|
|
198
229
|
payload = await buildPayload(opts, { cliVersion: deps.cliVersion ?? cliVersion() });
|
|
199
230
|
}
|
|
200
231
|
catch (e) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
return inputError(e, opts.json);
|
|
233
|
+
}
|
|
234
|
+
// Fetched after the prompts: it lives five minutes, and a person can take longer than that to type.
|
|
235
|
+
let assertion;
|
|
236
|
+
if (env === 'prod') {
|
|
237
|
+
try {
|
|
238
|
+
// Bearer only: agent evidence adds a session round trip the timeout cannot bound, and 401s signing in cannot fix.
|
|
239
|
+
assertion = (await api.request('GET', '/me/feedback-assertion', undefined, { evidence: false, signal: AbortSignal.timeout(ASSERTION_TIMEOUT_MS) })).token;
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (e instanceof ApiError && e.status === 401)
|
|
243
|
+
refuseFeedback(SIGNED_OUT, opts.json);
|
|
244
|
+
process.stderr.write(`warning: could not confirm who you are (${e instanceof Error ? e.message : String(e)}) — sending anyway, but nobody can reply to this report\n`);
|
|
245
|
+
}
|
|
206
246
|
}
|
|
207
|
-
const result = await submit(payload, deps.fetchImpl ?? fetch);
|
|
247
|
+
const result = await submit(payload, deps.fetchImpl ?? fetch, assertion);
|
|
208
248
|
if (result.status === 'unconfirmed') {
|
|
209
249
|
// NOT a failure claim: the request was still in flight at the deadline and the server
|
|
210
250
|
// finishes what it started, so saying "not submitted" here would be a false negative.
|
package/dist/index.js
CHANGED
|
@@ -279,6 +279,19 @@ ns.command('set <domain> <nameservers...>').description("Delegate the zone to na
|
|
|
279
279
|
ns.command('reset <domain>').description("Put the zone back on the registrar's own nameservers — from your own delegation or from an InstaCloud-managed zone alike; a hostname `set` took down is re-attached with `insta domain attach`, one a managed zone was serving re-verifies on its own")
|
|
280
280
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
281
281
|
.action(guard((domain, o) => domainCmd.domainNameserversReset(domain, o)));
|
|
282
|
+
const zone = dom.command('zone').description("Bring-your-own domains served from an InstaCloud-managed zone (nameserver delegation): point the domain's registrar at the pair `zone delegate` answers, and every attach's records — apexes included — are published for you. The alternative stays available: `insta domain attach` alone prints records to paste into your own zone");
|
|
283
|
+
zone.command('delegate <domain>').description("Delegate a domain you own elsewhere: builds its managed zone (seeded by a provider record scan — a HEURISTIC, so review `zone records` and add anything missing at your current provider BEFORE switching nameservers; re-running delegate re-imports) and answers the two nameservers to set at your registrar. A domain carrying live MX records is refused — move mail first or keep the records path (org admin; gated: zone.delegate — agent mode gates from a linked project, an unlinked --org call falls under org administration instead)")
|
|
284
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
285
|
+
.action(guard((domain, o) => domainCmd.zoneDelegate(domain, o)));
|
|
286
|
+
zone.command('list').description("The org's delegated zones: which are still waiting for the nameserver switch and which are serving")
|
|
287
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
288
|
+
.action(guard((o) => domainCmd.zoneList(o)));
|
|
289
|
+
zone.command('records <domain>').description("Every record in the delegated zone — the pre-switch review. The scan seeds common records but is not exhaustive: compare against your current DNS and add what is missing at your CURRENT provider, then re-run `zone delegate` to re-import")
|
|
290
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
291
|
+
.action(guard((domain, o) => domainCmd.zoneRecords(domain, o)));
|
|
292
|
+
zone.command('release <domain>').description("Release a delegated zone: the platform's records are pruned and the zone deleted. Point the nameservers back at your own provider; hostnames re-verify on the records path (org admin; gated: zone.delegate — agent mode gates from a linked project, an unlinked --org call falls under org administration instead)")
|
|
293
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
294
|
+
.action(guard((domain, o) => domainCmd.zoneRelease(domain, o)));
|
|
282
295
|
const xfer = dom.command('transfer').description('Take a bought domain to another registrar — open the lock, then read the code (all three need org admin)');
|
|
283
296
|
xfer.command('lock <domain> <mode>').description("Open or close the registrar transfer lock (mode: on|off). ICANN's own 60-day lock on a new registration outranks it")
|
|
284
297
|
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
@@ -561,7 +574,7 @@ const setupCompat = program.command('setup', { hidden: true }).description('Comp
|
|
|
561
574
|
withSetupAgentOptions(setupCompat.command('agent').description('Alias of `insta agent setup`, kept for the console one-liner'));
|
|
562
575
|
// ---- feedback (agent + human hurdle reports → the InstaCloud team) ----
|
|
563
576
|
program.command('feedback')
|
|
564
|
-
.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.
|
|
577
|
+
.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. On InstaCloud it needs `insta login`, so the team can reply; works unlinked.')
|
|
565
578
|
.option('--type <type>', `what kind of hurdle: ${feedbackCmd.TYPES.join(' | ')}`)
|
|
566
579
|
.option('--component <component>', `which part of the toolkit: ${feedbackCmd.COMPONENTS.join(' | ')}`)
|
|
567
580
|
.option('--title <title>', 'one-line summary (≤200 chars)')
|