From a62874bcd594d18297f3b9f1f014c9bef1572589 Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Sat, 17 Oct 2026 00:56:05 -0700 Subject: [PATCH] chore(domain): stop guiding users to buy or delegate new domains The platform paused new domain orders (domain_purchase_paused) and new BYO zone delegation (byo_delegation_paused) over the Cloudflare zone quota. Hide `domain search`, `domain buy` and `domain zone delegate` from help, keeping them registered so re-enabling is unhiding, and drop every printed hint that sends the reader to buy, search or re-run delegate. Attach/check/detach, bought-domain management and existing BYO zones are unchanged. --- README.md | 2 +- src/commands/domain.ts | 23 ++++++++++------------- src/index.ts | 13 +++++++------ test/domain.test.ts | 22 +++++++++++----------- test/help-surface.test.ts | 20 ++++++++++++++++++++ test/zone.test.ts | 13 +++++++------ 6 files changed, 56 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1ebf007..680de65 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ build never reaches a production installer. | `insta branch` | `create` · `list` · `switch` · `delete` · `merge` | | `insta service` (`services`, `svc`) | `add` · `list` · `remove` · `rename` | | `insta secrets` | Write `.env`, plus `list` · `set` · `unset` · `bind` · `unbind` · `bindings` · `sources` · `tree` | -| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach`; buy through InstaCloud: `search` · `buy` · `list` · `status` · `records …` | +| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach`. Domains already bought through InstaCloud: `list` · `status` · `records …` | | `insta compute` | `start` · `stop` · `suspend` · `restart` · `status` · `scale` · `limits` · `volume` · `always-on` · `exec` · `ssh` · `repo` · `connect-repo` · `watch-paths` · `disconnect-repo` · `logs` · `metrics` | | `insta postgres` | `url` (print the DSN) · `connect` (psql) · `stats` · `limits` · `restart` · `volume` · `always-on` · `private-access` · `public-access` · `logs` · `metrics` — every verb takes `[service]` | | `insta redis` · `mysql` · `mongodb` | `query` · `status` · `limits` · `volume` · `always-on` · `logs` · `metrics` | diff --git a/src/commands/domain.ts b/src/commands/domain.ts index 576e502..f58c7eb 100644 --- a/src/commands/domain.ts +++ b/src/commands/domain.ts @@ -37,8 +37,6 @@ export async function domainSearch(keyword: string, opts: { tlds?: string; org?: const r = await api.request<{ results: Quote[] }>('GET', `/orgs/${orgId}/domains/search?${qs}`) if (opts.json) return printJson(r) for (const line of searchLines(r.results)) info(line) - const buyable = r.results.find((x) => x.purchasable) - if (buyable) info(`buy one: insta domain buy ${buyable.domainName}`) } // ---- buy / attach ---- @@ -125,9 +123,9 @@ function domainLines(d: Purchased, linked = true): string[] { return out } -function orderStatusLines(o: Order, linked = true): string[] { +function orderStatusLines(o: Order): string[] { const out = [`order ${o.id}: ${o.domainName} — ${o.status}${o.failedReason ? ` — ${o.failedReason}` : ''}`] - if (linked && o.status === 'canceled') out.push(` the checkout closed without payment — order again: insta domain buy ${o.domainName}`) + if (o.status === 'canceled') out.push(' the checkout closed without payment') return out } @@ -135,7 +133,7 @@ export async function domainList(opts: { org?: string; json?: boolean }, deps?: const { api, orgId } = await orgDeps(opts, deps) const r = await api.request<{ items: Purchased[] }>('GET', `/orgs/${orgId}/domains`) if (opts.json) return printJson(r) - if (!r.items.length) return info('no domains bought through InstaCloud in this org (search: insta domain search )') + if (!r.items.length) return info('no domains bought through InstaCloud in this org') for (const d of r.items) for (const line of domainLines(d, !opts.org)) info(line) } @@ -150,7 +148,7 @@ export async function domainStatus(name: string, opts: { org?: string; json?: bo const order = orders.find((o) => o.domainName === host) ?? null if (!domain && !order) die(`${host} was not bought through this org`) if (opts.json) return printJson({ domain, order }) - for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order!, !opts.org)) info(line) + for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order!)) info(line) } // ---- nameservers and transferring out ---- @@ -382,10 +380,9 @@ export function zoneRecordLines(records: ZoneRecord[]): string[] { * into the zone itself — apexes included — instead of printing them for hand-copying. The zone is * SEEDED by the provider's record scan, which is a heuristic: the review-then-switch contract * (printed, and enforced by nothing else) is to compare `zone records` against the domain's current - * DNS and add what is missing at the CURRENT provider — re-running delegate re-imports — before - * re-pointing. A domain carrying live MX records is refused outright: a DNS move that can drop - * mail is never done implicitly. 202 approval_required in agent mode (zone.delegate); org admin - * either way. + * DNS before re-pointing. A domain carrying live MX records is refused outright: a DNS move that + * can drop mail is never done implicitly. 202 approval_required in agent mode (zone.delegate); org + * admin either way. */ const orgArgOf = (opts: RecordsOpts): string => (opts.org ? ` --org ${opts.org}` : '') @@ -402,7 +399,7 @@ export async function zoneDelegate(domainName: string, opts: RecordsOpts, deps?: if (opts.json) return printJson(res.body) const z = res.body as OrgZone for (const line of zoneLines(z, opts.org)) info(line) - 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.`) + info(`the zone was seeded by a provider scan, which can miss records. Check \`insta domain zone records ${z.domainName}${orgArgOf(opts)}\` against your current DNS, and only then switch the nameservers.`) } export async function zoneList(opts: RecordsOpts, deps?: DomainDeps): Promise { @@ -421,7 +418,7 @@ export async function zoneList(opts: RecordsOpts, deps?: DomainDeps): Promise') + if (!r.items.length) return info('no delegated zones') for (const z of r.items) for (const line of zoneLines(z, opts.org)) info(line) } @@ -430,7 +427,7 @@ export async function zoneRecords(domainName: string, opts: RecordsOpts, deps?: const r = await api.request<{ items: ZoneRecord[] }>('GET', `${zonePath(orgId, domainName)}/records`) if (opts.json) return printJson(r) for (const line of zoneRecordLines(r.items)) info(line) - if (r.items.length) 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`) + if (r.items.length) info('every type shows here. The provider scan can miss records, so compare them against your current DNS before switching nameservers') } /** diff --git a/src/index.ts b/src/index.ts index b602aeb..b11f96f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -284,11 +284,12 @@ cron.command('preview ').description('Validate an expression and pri .option('--json').action(guard((expression, o) => cronCmd.cronPreview(expression, o))) // ---- domain (bought here, or bring your own; hostnames on compute services; DNS of bought zones) ---- -const dom = program.command('domain').description('Domains: buy through InstaCloud or bring your own — attach / check / detach hostnames on compute services; DNS records of bought domains') -dom.command('search ').description('Search purchasable names with prices (a label like "myapp" or a full name like "myapp.com")') +const dom = program.command('domain').description('Domains: attach, check and detach hostnames on compute services, and manage domains already bought through InstaCloud') +// search, buy and zone delegate are hidden while the platform pauses new orders and BYO delegation. +dom.command('search ', { hidden: true }).description('Search purchasable names with prices (a label like "myapp" or a full name like "myapp.com")') .option('--tlds ', 'comma-separated TLDs to include').option('--org ', "target org (default: linked project's org)").option('--json') .action(guard((keyword, o) => domainCmd.domainSearch(keyword, o))) -dom.command('buy ').description('Buy a domain — pay at the printed Stripe Checkout link. It serves nothing until you attach it (gated: domain.purchase)') +dom.command('buy ', { hidden: true }).description('Buy a domain — pay at the printed Stripe Checkout link. It serves nothing until you attach it (gated: domain.purchase)') .option('--years ', 'registration term in years (default 1)') .option('--no-open', 'print the checkout URL instead of opening a browser').option('--json') .action(guard((name, o) => domainCmd.domainBuy(name, o))) @@ -321,14 +322,14 @@ ns.command('reset ').description("Put the zone back on the registrar's o .option('--org ', "target org (default: linked project's org)").option('--json') .action(guard((domain, o) => domainCmd.domainNameserversReset(domain, o))) -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") -zone.command('delegate ').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)") +const zone = dom.command('zone').description("Bring-your-own domains already delegated to an InstaCloud-managed zone: list them, review their records, or release them. For a hostname under a domain you own elsewhere, `insta domain attach` prints the records to add in your own DNS") +zone.command('delegate ', { hidden: true }).description("Delegate a domain you own elsewhere: builds its managed zone (seeded by a provider record scan — a HEURISTIC, so review `zone records` against your current DNS BEFORE switching nameservers) 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)") .option('--org ', "target org (default: linked project's org)").option('--json') .action(guard((domain, o) => domainCmd.zoneDelegate(domain, o))) zone.command('list').description("The org's delegated zones: which are still waiting for the nameserver switch and which are serving") .option('--org ', "target org (default: linked project's org)").option('--json') .action(guard((o) => domainCmd.zoneList(o))) -zone.command('records ').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") +zone.command('records ').description("Every record in the delegated zone, for review before you switch nameservers. The provider scan seeds common records but is not exhaustive, so compare it against your current DNS first") .option('--org ', "target org (default: linked project's org)").option('--json') .action(guard((domain, o) => domainCmd.zoneRecords(domain, o))) zone.command('release ').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)") diff --git a/test/domain.test.ts b/test/domain.test.ts index 8389ac9..370d90c 100644 --- a/test/domain.test.ts +++ b/test/domain.test.ts @@ -40,7 +40,8 @@ describe('domain search', () => { expect(calls[0]).toMatchObject({ method: 'GET', path: '/orgs/org1/domains/search?q=MyApp&tlds=com%2Cdev' }) expect(out()).toContain('myapp.com $15.59 (renews $23.99/yr)') expect(out()).toContain('myapp.dev unavailable — Domain unavailable') - expect(out()).toContain('buy one: insta domain buy myapp.com') + // New orders are paused, so the results name no buy command. + expect(out()).not.toContain('insta domain buy') }) it('--org overrides the linked org; --json is the platform body', async () => { const { deps: d, calls } = deps({ '/domains/search': { results: [quote] } }) @@ -228,21 +229,20 @@ describe('domain list / status', () => { expect(out()).not.toContain('order o1') expect(out()).toContain('api.myapp.com active → api') }) - it('a canceled checkout says how to order again', async () => { + // New orders are paused (platform #689), so a canceled checkout names no way to order again. + it('a canceled checkout says why and does not send the reader to buy again', async () => { const { deps: d } = deps({ '/domains/orders': { items: [{ ...order, status: 'canceled', failedReason: 'checkout expired before payment' }] }, '/orgs/org1/domains': { items: [] } }) await domainStatus('myapp.com', {}, d) expect(out()).toContain('canceled — checkout expired before payment') - expect(out()).toContain('order again: insta domain buy myapp.com') - }) - // `buy` and `attach` take no --org: they act on the linked project's org, so naming another one - // would send the reader to a command that acts somewhere else. - it('drops the follow-up commands when --org names an org they do not act on', async () => { - const routes = { '/domains/orders': { items: [{ ...order, status: 'canceled', failedReason: 'x' }] }, '/orgs/org9/domains': { items: [] } } - const { deps: d } = deps(routes) - await domainStatus('myapp.com', { org: 'org9' }, d) - expect(out()).toContain('canceled') + expect(out()).toContain('the checkout closed without payment') expect(out()).not.toContain('insta domain buy') }) + it('an empty list says so without pointing at search', async () => { + const { deps: d } = deps({ '/orgs/org1/domains': { items: [] } }) + await domainList({}, d) + expect(out()).toContain('no domains bought through InstaCloud in this org') + expect(out()).not.toContain('insta domain search') + }) it('status of a name never bought here fails plainly, --json included', async () => { const { deps: d } = deps({ '/domains/orders': { items: [] }, '/orgs/org1/domains': { items: [] } }) await expect(domainStatus('other.com', { json: true }, d)).rejects.toThrow('exit 1') diff --git a/test/help-surface.test.ts b/test/help-surface.test.ts index 83b002c..65f1203 100644 --- a/test/help-surface.test.ts +++ b/test/help-surface.test.ts @@ -124,6 +124,26 @@ describe('group shapes', () => { expect(run(['compute', 'scale', '--help']).stdout).toContain('Usage: insta compute scale [options] [count] [service]') expect(run(['storage', 'set-access', '--help']).stdout).toContain('Usage: insta storage set-access [options] ') }, 30_000) + // New domain orders and BYO delegation are paused (platform #689, #687). Hidden, not removed, + // so re-enabling them is unhiding. The Usage line proves each path resolved to itself. + it('hides domain search, buy and zone delegate from help but keeps them registered', () => { + const domainHelp = run(['domain', '--help']).stdout + const domain = commandNames(domainHelp) + expect(domain).not.toContain('search') + expect(domain).not.toContain('buy') + expect(domain).toContain('zone') + expect(commandNames(run(['domain', 'zone', '--help']).stdout)).toEqual(['list', 'records', 'release']) + expect(`${run(['--help']).stdout}${domainHelp}`).not.toMatch(/\bbuy\b|zone delegate/i) + for (const [path, usage] of [ + [['domain', 'search'], 'Usage: insta domain search [options] '], + [['domain', 'buy'], 'Usage: insta domain buy [options] '], + [['domain', 'zone', 'delegate'], 'Usage: insta domain zone delegate [options] '], + ] as const) { + const r = run([...path, '--help']) + expect(r.status, path.join(' ')).toBe(0) + expect(r.stdout, path.join(' ')).toContain(usage) + } + }, 30_000) it('reads source-build output under `build logs`, not a top-level `build-logs`', () => { expect(commandNames(run(['build', '--help']).stdout)).toContain('logs') const r = run(['build', 'logs', '--help']) diff --git a/test/zone.test.ts b/test/zone.test.ts index 499a77d..3d44a6a 100644 --- a/test/zone.test.ts +++ b/test/zone.test.ts @@ -42,6 +42,8 @@ describe('domain zone delegate', () => { // The review-then-switch contract is the safety of the whole flow — it prints every time. expect(out()).toContain('insta domain zone records byo.example') expect(out()).toContain('only then switch the nameservers') + // BYO delegation is paused (platform #687), so re-running delegate is no longer a step. + expect(out()).not.toContain('re-running delegate') }) it('under --org naming ANOTHER org the call goes projectless — the wrong project must not sign', async () => { const { deps: d, calls } = deps({}, { status: 200, body: awaiting }) @@ -72,6 +74,7 @@ describe('domain zone list', () => { const { deps: d2 } = deps({ '/zones': { items: [] } }) await zoneList({}, d2) expect(out()).toContain('no delegated zones') + expect(out()).not.toContain('zone delegate') }) it('combines pages into the existing JSON response and stops on the terminal cursor', async () => { const second = { ...awaiting, domainName: 'second.example' } @@ -93,7 +96,7 @@ describe('domain zone list', () => { }) describe('domain zone records', () => { - it('lists EVERY type (the review must see CAA) and prints the add-then-reimport reminder', async () => { + it('lists EVERY type (the review must see CAA) and prints the compare-before-switching reminder', async () => { const { deps: d, calls } = deps({ '/zones/byo.example/records': { items: [ { type: 'CNAME', host: '@', answer: 'edge.example', proxied: true }, @@ -105,11 +108,9 @@ describe('domain zone records', () => { expect(calls[0]).toMatchObject({ method: 'GET', path: '/orgs/org1/zones/byo.example/records' }) expect(out()).toContain('CAA') expect(out()).toContain('(proxied)') - // The instruction is copy-pasteable VERBATIM: domain and org scope included. - expect(out()).toContain('re-run `insta domain zone delegate byo.example` to re-import') - stdout.length = 0 - await zoneRecords('Byo.Example', { org: 'org9' }, d) - expect(out()).toContain('re-run `insta domain zone delegate byo.example --org org9` to re-import') + expect(out()).toContain('compare them against your current DNS before switching nameservers') + // BYO delegation is paused (platform #687): the reminder no longer sends anyone to re-run it. + expect(out()).not.toContain('zone delegate') }) it('says plainly when the scan has not landed yet', () => { expect(zoneRecordLines([])[0]).toContain('run this again in a moment')