insta 0.1.24 → 0.1.25

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.
@@ -6,7 +6,7 @@ import { info, die, printJson, handleApproval, renderNextActions, CliExit } from
6
6
  import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
7
7
  import { packDirectory, windowsModeCaveat } from '../pack.js';
8
8
  import { deployArchive, uploadArchive } from '../deploy-archive.js';
9
- import { parseEndpoint, parsePort } from './services.js';
9
+ import { isV6Endpoint, parseEndpoint, parsePort } from './services.js';
10
10
  // With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
11
11
  // line moves to stderr.
12
12
  const note = (opts) => (opts.json ? (m) => void process.stderr.write(m + '\n') : info);
@@ -23,11 +23,19 @@ export function deployRequestBody(source, branch, opts) {
23
23
  replaceSource: opts.replaceSource ? true : undefined,
24
24
  };
25
25
  }
26
- // Where the deploy answers: the HTTPS URL, or for an endpoint tcp service its tcp address (the
27
- // platform reports no URL for one). Pure, so it's unit-tested.
28
- export function deployedTarget(r, port) {
29
- if (r.endpointHost)
30
- return `tcp://${r.endpointHost}:${port ?? '?'}`;
26
+ // Where the deploy answers: the HTTPS URL, or for a v6-lane service (tcp, udp, tcp+udp) its
27
+ // address, one scheme per protocol (the platform reports no URL for one). The scheme comes from the
28
+ // RESULT's `endpoint` (instacloud-platform#681 reports it beside endpointHost, including on a
29
+ // redeploy that passed none -- the endpoint the service kept); --endpoint is only the fallback for
30
+ // an older platform that names the host but not the endpoint. With neither, the address prints
31
+ // without a scheme rather than guessing tcp for what may be a udp service. Pure, so it's unit-tested.
32
+ export function deployedTarget(r, port, flag) {
33
+ if (r.endpointHost) {
34
+ const endpoint = isV6Endpoint(r.endpoint) ? r.endpoint : isV6Endpoint(flag) ? flag : undefined;
35
+ if (endpoint === undefined)
36
+ return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`;
37
+ return (endpoint === 'tcp+udp' ? ['tcp', 'udp'] : [endpoint]).map((pr) => `${pr}://${r.endpointHost}:${port ?? '?'}`).join(', ');
38
+ }
31
39
  return r.url || '(no URL: a worker)';
32
40
  }
33
41
  // Ask the platform which lane serves this target, so the CLI stops knowing which provider backs
@@ -152,7 +160,7 @@ export async function deploy(dir, opts) {
152
160
  if (!dir && !opts.image)
153
161
  die('usage: insta deploy <dir> | --image <url> [--branch <b>] [--group <g>] [--port <n>]');
154
162
  // Normalized here, once, so both lanes get the same validated value: the archive lane's body
155
- // (deployArchive) otherwise silently drops anything that isn't exactly 'http'/'tcp' to
163
+ // (deployArchive) otherwise silently drops anything that isn't exactly one of the endpoints to
156
164
  // undefined, which let a typo like --endpoint tpc build and deploy using the service's existing
157
165
  // endpoint instead of failing locally. Checked before ApiClient.load()/requireProject(): those
158
166
  // can resolve a project and persist an automatic link for an unlinked directory, so a typo must
@@ -192,8 +200,8 @@ export async function deploy(dir, opts) {
192
200
  // nextActions, which the operation read does not carry.
193
201
  const d = source.deployed;
194
202
  if (opts.json)
195
- return printJson({ image: d.image, url: d.url, ...(d.endpointHost ? { endpointHost: d.endpointHost } : {}), branch: d.branch, group: d.group, machineId: d.machineId });
196
- info(`deployed ${d.image} -> ${deployedTarget(d, effOpts.port)} (branch ${d.branch}, group ${d.group})`);
203
+ return printJson({ image: d.image, url: d.url, ...(d.endpointHost ? { endpointHost: d.endpointHost } : {}), ...(d.endpoint ? { endpoint: d.endpoint } : {}), branch: d.branch, group: d.group, machineId: d.machineId });
204
+ info(`deployed ${d.image} -> ${deployedTarget(d, effOpts.port, endpoint)} (branch ${d.branch}, group ${d.group})`);
197
205
  return;
198
206
  }
199
207
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, deployRequestBody(source, branch, effOpts))
@@ -203,7 +211,7 @@ export async function deploy(dir, opts) {
203
211
  const what = source.image;
204
212
  if (opts.json)
205
213
  return printJson({ image: source.image, ...res.body });
206
- info(`deployed ${what} -> ${deployedTarget(res.body, effOpts.port)} (branch ${res.body.branch}, group ${res.body.group})`);
214
+ info(`deployed ${what} -> ${deployedTarget(res.body, effOpts.port, endpoint)} (branch ${res.body.branch}, group ${res.body.group})`);
207
215
  renderNextActions(res.body.nextActions);
208
216
  }
209
217
  // The platform refuses an image deploy onto a repo-connected service and names the body field it
@@ -0,0 +1,101 @@
1
+ // `insta compute public-access` and `insta <redis|mysql|mongodb> public-access`: show, close or
2
+ // re-open a compute-plane service's public access (the platform's PUT .../services/:id/access).
3
+ //
4
+ // Every insta-compute service has a private address in its project branch: `<name>.insta.internal`
5
+ // for compute, `<name>-<type>.insta.internal` for a managed database (the platform's private_host;
6
+ // this file only prints what the platform reports, never composes one). Closing public access makes
7
+ // the service private-only: its URL answers 403 (a database's public *_URL stops connecting) and
8
+ // only services in the same branch reach it. Like `insta postgres public-access off`, closing asks
9
+ // first, and a non-interactive caller (agent, CI, --json) must pass --yes.
10
+ import { ApiClient, ApiError, requireProject } from '../api.js';
11
+ import { handleApproval, info, printJson, refuse } from '../util.js';
12
+ import { q, resolveSoleService } from './services.js';
13
+ import { alwaysOnArgs, confirmOnTerminal } from './postgres.js';
14
+ const isDb = (type) => type !== 'compute';
15
+ const credential = (type) => `${type.toUpperCase()}_URL`;
16
+ const privateCredential = (type) => `${type.toUpperCase()}_PRIVATE_URL`;
17
+ /** `<host>:<port>` for the private address, or undefined when the platform reports none. */
18
+ export function privateAddress(s) {
19
+ return s.private_host ? `${s.private_host}${s.port ? `:${s.port}` : ''}` : undefined;
20
+ }
21
+ // The show block. Pure, exported for tests.
22
+ export function publicAccessLines(s) {
23
+ const pub = s.public;
24
+ const head = `${s.type} ${s.name}: public access ${pub === false ? 'off' : pub === true ? 'on' : 'unknown'}`;
25
+ const why = pub === false
26
+ ? (isDb(s.type) ? ` — ${credential(s.type)} does not connect; apps in this branch use ${privateCredential(s.type)}` : ' — its URL answers 403; only services in this branch reach it')
27
+ : pub === true ? (isDb(s.type) ? '' : ' — reachable from the internet at its URL') : '';
28
+ const lines = [head + why];
29
+ const addr = privateAddress(s);
30
+ if (addr) {
31
+ lines.push(` private: ${addr}${s.type === 'compute' ? ' (TCP and UDP, from services in this branch)' : ` (from services in this branch; ${privateCredential(s.type)} holds the connection string)`}`);
32
+ if (s.private_fqdn)
33
+ lines.push(` fqdn: ${s.private_fqdn} (for tools that skip the search domain, e.g. nslookup, dig)`);
34
+ }
35
+ else {
36
+ lines.push(' private: none yet — the platform reports no private address for this service (not yet back-filled, a worker, or a region without the private network)');
37
+ }
38
+ return lines;
39
+ }
40
+ // Map a refusal to a CLI-shaped message. Pure, exported for tests.
41
+ export function publicAccessErrorMessage(e, ctx) {
42
+ const code = e.body?.code;
43
+ const b = ctx.branch ? ` --branch ${ctx.branch}` : '';
44
+ if (code === 'private_network_disabled')
45
+ return `the private network is not yet available on this deployment — ${ctx.type} ${ctx.service} stays public`;
46
+ if (code === 'private_network_unavailable_in_region')
47
+ return `the region of ${ctx.type} ${ctx.service} has no private network yet — it stays public`;
48
+ if (code === 'no_private_name')
49
+ return `${ctx.type} ${ctx.service} has no private network name, so closing public access would leave it unreachable — rename it first: \`insta service rename ${ctx.type} ${ctx.service} <new-name>${b}\``;
50
+ if (code === 'private_name_taken')
51
+ return `${e.message} — rename one of the services: \`insta service rename ${ctx.type} ${ctx.service} <new-name>${b}\``;
52
+ return `${ctx.what} failed (${e.status}): ${e.message}`;
53
+ }
54
+ // What closing will break, said before asking. Pure, exported for tests.
55
+ export function closeWarning(s) {
56
+ return isDb(s.type)
57
+ ? `closing public access: ${credential(s.type)} (the public address) stops connecting — external clients, CI and local development can no longer reach ${s.type} ${s.name}, and apps in this branch must use ${privateCredential(s.type)} (bind it and redeploy them)`
58
+ : `closing public access: ${s.type} ${s.name}'s URL will answer 403 and stop waking it — browsers, webhooks and every caller outside this branch lose it; services in this branch keep reaching it at its private address`;
59
+ }
60
+ export async function servicePublicAccess(type, first, second, opts, deps = {}) {
61
+ const { mode, service } = alwaysOnArgs(first, second);
62
+ const api = await ApiClient.load();
63
+ const p = await requireProject();
64
+ const branch = opts.branch ?? p.branch;
65
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
66
+ const svc = resolveSoleService(services, type, service);
67
+ if (!mode) {
68
+ if (opts.json)
69
+ return printJson({ name: svc.name, type: svc.type, public: svc.public ?? null, private_host: svc.private_host ?? null, private_fqdn: svc.private_fqdn ?? null, port: svc.port ?? null });
70
+ for (const l of publicAccessLines(svc))
71
+ info(l);
72
+ return;
73
+ }
74
+ if (mode === 'off' && !opts.yes) {
75
+ const tty = deps.tty ?? (!!process.stdin.isTTY && !!process.stdout.isTTY);
76
+ if (opts.json || !tty)
77
+ refuse([closeWarning(svc), `${type} ${svc.name}: re-run with --yes to confirm`]);
78
+ info(closeWarning(svc));
79
+ const ok = await (deps.confirm ?? confirmOnTerminal)(`Close public access to ${type} ${svc.name}?`);
80
+ if (!ok)
81
+ return info(`${type} ${svc.name}: public access unchanged`);
82
+ }
83
+ let res;
84
+ try {
85
+ res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/access`, { public: mode === 'on' });
86
+ }
87
+ catch (e) {
88
+ if (e instanceof ApiError)
89
+ throw new Error(publicAccessErrorMessage(e, { type, service: svc.name, branch: opts.branch, what: mode === 'off' ? 'closing public access' : 'opening public access' }));
90
+ throw e;
91
+ }
92
+ if (handleApproval(res, opts.json))
93
+ return;
94
+ if (opts.json)
95
+ return printJson(res.body);
96
+ const notice = typeof res.body?.notice === 'string' && res.body.notice ? res.body.notice : undefined;
97
+ info(`${type} ${svc.name}: public access ${mode === 'off' ? 'CLOSED' : 'open'}`);
98
+ if (notice)
99
+ info(` ${notice}`);
100
+ }
101
+ //# sourceMappingURL=private-network.js.map
@@ -105,20 +105,32 @@ export function resolveComputeServiceId(services, name) {
105
105
  function defaultDatabasePort(type) {
106
106
  return type === 'mysql' ? 3306 : type === 'mongodb' ? 27017 : 6379;
107
107
  }
108
- // How a compute service is reached: http (the default HTTPS domain) or tcp (raw TCP on its port at
109
- // its own IPv6 address). Checked locally so a typo fails before any network call; which ports tcp
110
- // accepts is the platform's answer, not repeated here.
108
+ export const ENDPOINTS = ['http', 'tcp', 'udp', 'tcp+udp'];
111
109
  export function parseEndpoint(raw) {
112
110
  const v = raw.trim().toLowerCase();
113
- if (v !== 'http' && v !== 'tcp')
114
- throw new Error(`--endpoint must be http or tcp, got: ${raw}`);
111
+ if (!ENDPOINTS.includes(v))
112
+ throw new Error(`--endpoint must be http, tcp, udp or tcp+udp, got: ${raw}`);
115
113
  return v;
116
114
  }
115
+ // tcp, udp and tcp+udp are the same v6 lane with a different protocol set: no HTTP domain, and the
116
+ // address is endpoint_host on the service port.
117
+ export const isV6Endpoint = (e) => e === 'tcp' || e === 'udp' || e === 'tcp+udp';
117
118
  // Where a tcp service is dialed. The host is set once a deploy has confirmed it; before that the
118
119
  // row only knows its port.
119
120
  export function tcpAddress(s) {
120
121
  return s.endpoint_host ? `tcp://${s.endpoint_host}:${s.port}` : `tcp/${s.port ?? '?'} (address after the first deploy)`;
121
122
  }
123
+ // Where a v6-lane service (tcp, udp or tcp+udp) is reached, one scheme per protocol it serves:
124
+ // tcp://host:port, udp://host:port, or both for tcp+udp. tcp is exactly tcpAddress. Before a
125
+ // deploy has confirmed the host, the row only knows the protocol and port.
126
+ export function v6Address(s) {
127
+ if (s.endpoint === 'tcp')
128
+ return tcpAddress(s);
129
+ const protos = s.endpoint === 'tcp+udp' ? ['tcp', 'udp'] : ['udp'];
130
+ if (!s.endpoint_host)
131
+ return `${s.endpoint === 'tcp+udp' ? 'tcp+udp' : 'udp'}/${s.port ?? '?'} (address after the first deploy)`;
132
+ return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port}`).join(', ');
133
+ }
122
134
  // Map service-add options to the platform POST body. Pure, so it's unit-tested without a network
123
135
  // mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which
124
136
  // type) stays in servicesAdd, ahead of any network/config access.
@@ -199,7 +211,7 @@ export function serviceAddedLine(type, name, branch, svc) {
199
211
  const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at ${svc.volume_mount_path ?? "/data"}` : '';
200
212
  // The major belongs next to the connect hint: it decides which psql/pg_dump to reach for.
201
213
  const pg = svc.type === 'postgres' ? pgBadge(svc.pg_version) : '';
202
- const where = svc.endpoint === 'tcp' ? ` — ${tcpAddress(svc)}` : svc.domain ? ` — ${svc.domain}` : '';
214
+ const where = isV6Endpoint(svc.endpoint) ? ` — ${v6Address(svc)}` : svc.domain ? ` — ${svc.domain}` : '';
203
215
  return `added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${pg}${where}`;
204
216
  }
205
217
  // ` pg <major>` for a postgres row, or '' when the platform sent nothing usable. The field arrives
@@ -218,9 +230,13 @@ export function serviceListLine(s) {
218
230
  // Postgres major, so the reader picks matching pg_dump/psql BEFORE connecting (a newer client
219
231
  // dumps statements an older server cannot restore). Older platforms send no pg_version.
220
232
  : s.type === 'postgres' ? pgBadge(s.pg_version) : '';
221
- // A tcp compute service has no HTTP domain; its address is the tcp one.
222
- const where = s.type === 'compute' && s.endpoint === 'tcp' ? ` ${tcpAddress(s)}` : s.domain ? ` ${s.domain}` : '';
223
- return `${s.type}/${s.name} [${s.status}]${extra}${where} ${s.id}`;
233
+ // A tcp/udp/tcp+udp compute service has no HTTP domain; its address is the v6-lane one.
234
+ const where = s.type === 'compute' && isV6Endpoint(s.endpoint) ? ` ${v6Address(s)}` : s.domain ? ` ${s.domain}` : '';
235
+ // The private address the platform reports (never composed here), and a closed compute service
236
+ // says so beside the URL it no longer serves.
237
+ const closed = s.type === 'compute' && s.public === false ? ' public off' : '';
238
+ const priv = s.private_host ? ` private ${s.private_host}${s.port ? `:${s.port}` : ''}` : '';
239
+ return `${s.type}/${s.name} [${s.status}]${extra}${where}${closed}${priv} ${s.id}`;
224
240
  }
225
241
  export async function servicesList(opts) {
226
242
  const api = await ApiClient.load();
@@ -1,5 +1,6 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
2
  import { handleApproval } from './util.js';
3
+ import { ENDPOINTS, isV6Endpoint } from './commands/services.js';
3
4
  export function archiveBuildSpec(hasDockerfile) {
4
5
  return hasDockerfile ? { type: 'dockerfile' } : { type: 'nixpacks' };
5
6
  }
@@ -72,7 +73,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
72
73
  archive: ref,
73
74
  port: opts.port ? Number(opts.port) : undefined,
74
75
  websocket: typeof opts.websocket === 'boolean' ? opts.websocket : undefined,
75
- endpoint: opts.endpoint === 'http' || opts.endpoint === 'tcp' ? opts.endpoint : undefined,
76
+ endpoint: ENDPOINTS.includes(opts.endpoint ?? '') ? opts.endpoint : undefined,
76
77
  replaceSource: opts.replaceSource === true ? true : undefined,
77
78
  });
78
79
  // An approval is a 202 too, told apart by its status word; anything else here is our operation.
@@ -122,8 +123,11 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
122
123
  if (state === 'live') {
123
124
  const image = res.body?.imageRef;
124
125
  const url = res.body?.url;
125
- // An endpoint tcp service has no URL: its answer is the tcp host instead. A worker has neither.
126
+ // A tcp/udp/tcp+udp (v6-lane) service has no URL: its answer is the v6 host instead. A worker has neither.
126
127
  const endpointHost = typeof res.body?.endpointHost === 'string' && res.body.endpointHost ? res.body.endpointHost : undefined;
128
+ // The endpoint the service is on (instacloud-platform#681), so the address gets its scheme;
129
+ // an older platform omits it, and a value this CLI does not know is treated as absent.
130
+ const endpoint = endpointHost && isV6Endpoint(res.body?.endpoint) ? res.body.endpoint : undefined;
127
131
  if (typeof image !== 'string' || !image || typeof url !== 'string' || (!url && opts.port !== '0' && !endpointHost)) {
128
132
  throw new Error('the deploy finished but the platform returned no image or URL for it — check `insta status`');
129
133
  }
@@ -135,7 +139,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
135
139
  return v;
136
140
  };
137
141
  return {
138
- image, url, ...(endpointHost ? { endpointHost } : {}),
142
+ image, url, ...(endpointHost ? { endpointHost } : {}), ...(endpoint ? { endpoint } : {}),
139
143
  branch: optionalString('branch', res.body.branch) ?? branch,
140
144
  group: optionalString('group', res.body.group) ?? opts.group ?? '',
141
145
  machineId: optionalString('machineId', res.body.machineId),
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { buildLogs } from './commands/build-logs.js';
28
28
  import * as computeCmd from './commands/compute.js';
29
29
  import * as githubCmd from './commands/github.js';
30
30
  import * as pgCmd from './commands/postgres.js';
31
+ import * as privateNet from './commands/private-network.js';
31
32
  import * as dbQueryCmd from './commands/db-query.js';
32
33
  import * as managedDb from './commands/managed-db.js';
33
34
  import * as storageCmd from './commands/storage.js';
@@ -155,7 +156,7 @@ svc.command('add [type] [name]').description('Provision a service on demand (com
155
156
  .option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
156
157
  .option('--image <url>', 'compute only: run this container image at creation')
157
158
  .option('--port <n>', 'compute only: port the image listens on (default 8080; 0 for a worker without a routed port)')
158
- .option('--endpoint <http|tcp>', 'compute only: how the service is reached — http (default: HTTPS at its domain) or tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only, no platform TLS, no custom domain). tcp needs --port and refuses commonly scanned, web (80/443) and platform-reserved ports')
159
+ .option('--endpoint <http|tcp|udp|tcp+udp>', 'compute only: how the service is reached — http (default: HTTPS at its domain), or tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only, no platform TLS, no custom domain). tcp needs --port and refuses commonly scanned, web (80/443) and platform-reserved ports; udp refuses amplification-prone (DNS, NTP, SNMP, memcached, …) and platform-reserved ports; tcp+udp refuses both lists. A udp datagram wakes a suspended service and what the wake held is re-sent until the service first replies (an app may see it twice); a receive-only udp service (never replies) needs --always-on to stay awake')
159
160
  .option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way). A worker (--port 0) is always-on regardless')
160
161
  .option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request (the default for new compute services; refused with --port 0, a worker cannot scale to zero)')
161
162
  .option('--mount-path <path>', 'compute only: container mount path for a new volume (requires --volume; default /data)')
@@ -365,7 +366,7 @@ function addObservability(group, component, noun) {
365
366
  // optional makes commander unable to hold that boundary itself).
366
367
  const { argv: computeArgv, command: execCommand, windowsFallback: execWindowsFallback, } = computeCmd.splitExecArgs(process.argv);
367
368
  // ---- compute ----
368
- const compute = program.command('compute').description('Compute services: lifecycle (start/stop/suspend/restart/status), scale, limits, volume, always-on, exec, ssh, GitHub source, logs, metrics');
369
+ const compute = program.command('compute').description('Compute services: lifecycle (start/stop/suspend/restart/status), scale, limits, volume, public access, always-on, exec, ssh, GitHub source, logs, metrics');
369
370
  compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
370
371
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));
371
372
  compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
@@ -384,6 +385,10 @@ compute.command('scale [count] [service]').description('Set a compute service sa
384
385
  compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
385
386
  .option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
386
387
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
388
+ compute.command('public-access [mode] [service]').description("Show, close or re-open a compute service's public access (mode: on|off; no mode = print it and the service's private address). Every compute service is reachable by the other services in its project branch at <name>.insta.internal:<port> (TCP and UDP); off = private-only: its URL answers 403 and no longer wakes it, so only services in the same branch reach it. Immediate, no redeploy. Asks first; --yes when there is no terminal or with --json. insta-compute only (gated: service.setAccess)")
389
+ .option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
390
+ .option('--json').option('--branch <branch>', 'branch (default: current)')
391
+ .action(guard((mode, service, o) => privateNet.servicePublicAccess('compute', mode, service, o)));
387
392
  compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = scale-to-zero (the default for new compute services; refused on a worker, port 0). All plans; billing is actual usage either way')
388
393
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
389
394
  compute.command('ssh [service]')
@@ -463,7 +468,7 @@ pg.command('public-access [mode] [service]').description("Show, close or re-open
463
468
  addObservability(pg, 'db', 'postgres');
464
469
  // ---- redis / mysql / mongodb (managed databases) ----
465
470
  for (const type of ['redis', 'mysql', 'mongodb']) {
466
- const g = program.command(type).description(`Managed ${type} services: query, status, resource ceiling, volume, always-on, logs, metrics`);
471
+ const g = program.command(type).description(`Managed ${type} services: query, status, resource ceiling, volume, public access, always-on, logs, metrics`);
467
472
  const query = g.command('query <service> [args...]').description(type === 'redis'
468
473
  ? 'Run a redis command against the service via the console exec API — a pre-tokenized argv, e.g. `GET mykey`'
469
474
  : `Run one quoted ${type} statement against the service via the console exec API`)
@@ -482,6 +487,10 @@ for (const type of ['redis', 'mysql', 'mongodb']) {
482
487
  .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
483
488
  .option('--json').option('--branch <branch>', 'branch (default: current)')
484
489
  .action(guard((service, o) => computeCmd.serviceVolume(type, service, o)));
490
+ g.command('public-access [mode] [service]').description(`Show, close or re-open a ${type} service's public access (mode: on|off; no mode = print it and the private address). On insta-compute every ${type} is reachable from services in its project branch at <name>-${type}.insta.internal (${type.toUpperCase()}_PRIVATE_URL); off = ${type.toUpperCase()}_URL stops connecting and only apps in the branch reach it, through ${type.toUpperCase()}_PRIVATE_URL. Asks first; --yes when there is no terminal or with --json (gated: service.setAccess)`)
491
+ .option('-y, --yes', 'close without asking (required when there is no terminal, or with --json)')
492
+ .option('--json').option('--branch <branch>', 'branch (default: current)')
493
+ .action(guard((mode, service, o) => privateNet.servicePublicAccess(type, mode, service, o)));
485
494
  g.command('always-on <mode> [service]').description(`Set a ${type} service always-on (mode: on|off). on = machines never scale to zero; off = scale-to-zero. Billing is actual usage either way`)
486
495
  .option('--json').option('--branch <branch>', 'branch (default: current)')
487
496
  .action(guard((mode, service, o) => computeCmd.serviceAlwaysOn(type, mode, service, o)));
@@ -526,7 +535,7 @@ buildCmd.command('logs <build-id>').description('Read source-build output for a
526
535
  program.command('deploy [dir]').description('Deploy a source directory (built remotely; on insta-compute a Dockerfile is optional and nixpacks detects the runtime) or a prebuilt --image to a branch compute group')
527
536
  .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>', 'port the app listens on (0 for a worker without a routed port)')
528
537
  .option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
529
- .option('--endpoint <http|tcp>', 'switch how the service is reached from this deploy on: tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
538
+ .option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
530
539
  .option('--replace-source', 'the service deploys from a connected GitHub repo: switch it to this image and remove the repo connection (admin); without it such a deploy is refused')
531
540
  .option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
532
541
  .action(guard((dir, o) => deploy(dir, o)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [