New:Socket for Asana Is Now Available.Learn more
Get Started

insta

Package Overview
Dependencies
Maintainers
3
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

insta - npm Package Compare versions

Comparing version
0.0.45
to
0.0.47
+6
-2
dist/commands/auth.js

@@ -197,4 +197,8 @@ import { createServer } from 'node:http';

info(`opening browser to authorize with ${provider}…`);
if (!openUrl(authorizeUrl))
info(`open this URL to continue:\n ${authorizeUrl}`);
// Always print the URL: a launcher that fails to start reports it on spawn's ASYNC error
// event, so openUrl's return value cannot see it (e.g. powershell.exe blocked by AppLocker
// on hardened fleets) — and the silent variant of that failure looks exactly like a hang.
info(`if nothing opens, use this URL:\n ${authorizeUrl}`);
openUrl(authorizeUrl);
info('waiting for you to finish in the browser… (times out in 2m; ctrl-c to abort)');
timer = setTimeout(() => { server.close(); reject(new Error('timed out waiting for browser login (2m)')); }, 120_000);

@@ -201,0 +205,0 @@ });

@@ -70,3 +70,6 @@ import { ApiClient, requireProject } from '../api.js';

}
// Print the URL and, unless --no-open, try to open it in the browser.
// Print the URL and, unless --no-open, try to open it in the browser. The message says
// "opening", not "opened": a launcher that starts and then fails reports it asynchronously,
// so openUrl's true return is an attempt, not a confirmation (see util.ts) — and the URL is
// already printed above for exactly that case.
function presentUrl(url, label, open) {

@@ -76,4 +79,4 @@ info(label);

if (open !== false && openUrl(url))
info('(opened in your default browser)');
info('(opening in your default browser…)');
}
//# sourceMappingURL=billing.js.map

@@ -0,4 +1,6 @@

import { spawn } from 'node:child_process';
import { constants as osConstants } from 'node:os';
import { ApiClient, ApiError, requireProject } from '../api.js';
import { info, printJson, handleApproval } from '../util.js';
import { parseVolumeGib } from './services.js';
import { parseVolumeGib, q, resolveSoleService } from './services.js';
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,

@@ -240,2 +242,95 @@ // cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at

}
// Resolve the postgres service (sole, or --group) and its connection string. Two reads: the
// branch's services list names the service; GET /services/:id/credentials (gated secrets.read)
// carries the value. Provider-minted credentials are canonical within their source service
// (DATABASE_URL) and deliberately absent from the general `insta secrets` bundle, so this is the
// read that yields the DSN. The credentials call carries no branch param — the service id is
// already branch-scoped by the list. Returns null when the read parked on an approval
// (handleApproval already spoke). Takes the client as an argument so tests drive it with a stub,
// per this repo's pure-seam convention.
export async function resolveDbUrl(api, projectId, branch, group, json) {
const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
const svc = resolveSoleService(services, 'postgres', group);
const res = await api.rawRequest('GET', `/projects/${projectId}/services/${svc.id}/credentials`);
if (handleApproval(res, json))
return null;
const url = res.body?.credentials?.DATABASE_URL;
if (typeof url !== 'string' || !url) {
throw new Error(`postgres ${svc.name} has no DATABASE_URL credential yet — still provisioning? (\`insta services list\` shows status)`);
}
return { serviceName: svc.name, url };
}
// Print the postgres connection string: the bare DSN on stdout, nothing else — pipe-friendly
// (`psql "$(insta db url)"`), like `storage get --json` keeps stdout parseable.
export async function dbUrl(opts) {
const api = await ApiClient.load();
const p = await requireProject();
const branch = opts.branch ?? p.branch;
const r = await resolveDbUrl(api, p.projectId, branch, opts.group, opts.json);
if (!r)
return;
if (opts.json)
return printJson({ service: r.serviceName, branch: branch ?? null, url: r.url });
process.stdout.write(r.url + '\n');
}
// Decompose a postgres DSN into libpq PG* environment variables. Pure, exported for tests.
// The credential must NOT ride in psql's argv — process arguments are visible to every local
// user via `ps`, so a secrets.read-gated value would leak the moment the session starts. Child
// environment is not (the `insta run` model), and PG* env is a libpq-supported mechanism, so
// psql runs with an empty argv. `sslmode` is the only query param the platform's DSNs carry;
// anything else would be a platform-side change this mapping should then learn about.
export function psqlEnvFromUrl(url) {
const u = new URL(url);
const env = {};
if (u.hostname)
env.PGHOST = decodeURIComponent(u.hostname);
if (u.port)
env.PGPORT = u.port;
if (u.username)
env.PGUSER = decodeURIComponent(u.username);
if (u.password)
env.PGPASSWORD = decodeURIComponent(u.password);
const db = u.pathname.replace(/^\//, '');
if (db)
env.PGDATABASE = decodeURIComponent(db);
const sslmode = u.searchParams.get('sslmode');
if (sslmode)
env.PGSSLMODE = sslmode;
return env;
}
/** Core, dependency-injected for tests: spawn psql against the DSN (via PG* env, never argv), return its exit code. */
export async function connectWithPsql(url, spawnImpl = spawn) {
// Strip ambient PG* first: parent-env PGHOSTADDR/PGSERVICE/PGOPTIONS/PGSSL* would silently
// redirect or reshape the connection away from the service this command just resolved.
const env = { ...process.env };
// Case-insensitive: Windows env names are case-insensitive, so ambient `pgservice` redirects
// psql just as PGSERVICE does.
for (const k of Object.keys(env))
if (k.slice(0, 2).toUpperCase() === 'PG')
delete env[k];
Object.assign(env, psqlEnvFromUrl(url));
return await new Promise((resolve, reject) => {
const child = spawnImpl('psql', [], { stdio: 'inherit', env });
child.on('error', (e) => reject(e.code === 'ENOENT'
? new Error('psql not found on PATH — install the postgres client, or print the DSN with `insta db url`')
: e));
// Signal death reports code null — map to the conventional 128+signo (full table from
// os.constants) so the advertised exit-status passthrough holds for Ctrl-C'd/killed sessions.
child.on('close', (code, signal) => resolve(code ?? (signal ? 128 + (osConstants.signals[signal] ?? 0) : 1)));
});
}
// Open an interactive psql session on the postgres service. The DSN never touches disk or argv
// history beyond the child process. Exits with psql's own exit code (agents rely on this, as
// with `compute exec`).
export async function dbConnect(opts) {
const api = await ApiClient.load();
const p = await requireProject();
const branch = opts.branch ?? p.branch;
const r = await resolveDbUrl(api, p.projectId, branch, opts.group, opts.json);
if (!r)
return;
// stderr: stdout belongs to psql (the `insta run` rule).
process.stderr.write(`psql → postgres/${r.serviceName}${branch ? ` (branch ${branch})` : ''} — a suspended instance wakes on connect, so the first prompt can take a few seconds\n`);
process.exit(await connectWithPsql(r.url));
}
//# sourceMappingURL=db.js.map

@@ -123,2 +123,10 @@ // `insta services` — manage a project's opt-in services (postgres | storage | compute | redis | mysql | mongodb).

info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${svc.domain ? ` — ${svc.domain}` : ''}`);
// Discoverability: the DB is directly dialable, but its DSN is deliberately absent from the
// general `insta secrets` bundle — without this line nothing in the product says how to reach it.
if (type === 'postgres') {
// The hint must be runnable as printed: carry --branch when the service was created on a
// branch other than the linked one, and --group so it survives multiple postgres services.
const flags = `${opts.branch ? ` --branch ${opts.branch}` : ''} --group ${name}`;
info(` connect: \`insta db url${flags}\` prints the connection string, \`insta db connect${flags}\` opens psql (--group optional with a single postgres service)`);
}
renderNextActions(res.body.nextActions);

@@ -125,0 +133,0 @@ }

@@ -354,5 +354,6 @@ // `insta template` — browse the platform template registry and deploy a template (by registry

info(` ${u}`);
info('next: run `insta secrets` to refresh .env with the new service credentials');
// Provider credentials are not in the `insta secrets` bundle — point at the paths that exist.
info('next: `insta db url` prints the postgres DSN; bind service credentials into compute with `insta secrets bind`; `insta secrets` refreshes user-defined secrets in .env');
renderNextActions(dep.nextActions);
}
//# sourceMappingURL=template.js.map

@@ -226,3 +226,9 @@ #!/usr/bin/env node

// ---- db (postgres service controls) ----
const db = program.command('db').description('Postgres service controls (limits / volume / always-on / scale-to-zero)');
const db = program.command('db').description('Postgres service controls (url / connect / limits / volume / always-on / scale-to-zero)');
db.command('url').description('Print the postgres connection string (DSN) — bare on stdout for piping, e.g. `psql "$(insta db url)"` (gated: secrets.read). Provider credentials are not in `insta secrets` — this is the command that yields the DSN')
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
.action(guard((o) => dbCmd.dbUrl(o)));
db.command('connect').description("Open an interactive psql session on the postgres service (needs psql on PATH; gated: secrets.read). A suspended instance wakes on connect — the first prompt can take a few seconds. Exits with psql's own exit code")
.option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
.action(guard((o) => dbCmd.dbConnect(o)));
db.command('limits').description("Show or set a postgres service's resource ceiling (paid plans; insta-db-backed only). Moves both directions")

@@ -229,0 +235,0 @@ .option('--cpu <n>', "vCPU ceiling, e.g. 2 or 2500m").option('--memory <size>', "memory ceiling, e.g. 4Gi")

// Output + small pure helpers (env serialization is unit-tested).
import { createInterface } from 'node:readline';
import { spawn } from 'node:child_process';
// Best-effort: open a URL in the user's default browser. Returns false if we couldn't launch.
/** How to launch the default browser for `url` on `platform`. Pure so the Windows encoding is
* testable. On Windows NO shell may ever parse the URL: cmd.exe splits at bare `&` (which #138
* fixed by quoting) but ALSO expands `%…%` sequences even inside quotes, and a percent-encoded
* OAuth redirect (`http%3A%2F%2F127.0.0.1…`) is nothing but such sequences. So the launch goes
* through PowerShell's -EncodedCommand: a pure-ASCII script travels as base64(UTF-16LE) — no
* argument parsing anywhere — and the URL itself rides as a second base64 payload INSIDE that
* script, decoded by .NET at runtime, so no URL byte ever appears in PowerShell source (see the
* win32 branch). Start-Process on a URL is ShellExecute, i.e. the default browser. */
export function openUrlSpawn(url, platform = process.platform,
// Absolute path, not bare `powershell`: CreateProcess-style lookup searches the current
// directory before PATH, so a planted powershell.exe beside the user's shell would win.
systemRoot = process.env.SYSTEMROOT ?? process.env.windir ?? 'C:\\Windows') {
if (platform === 'win32') {
// The URL never appears in PowerShell SOURCE at all: it travels as base64 inside the script
// and is decoded by .NET at runtime. Interpolating it into a quoted literal is not enough —
// PowerShell honors smart quotes (U+2018–U+201B) as string delimiters too, so ASCII-only
// escaping still leaves a breakout. The script below is pure ASCII by construction (the
// base64 alphabet), so no byte of any URL can terminate anything.
const urlB64 = Buffer.from(url, 'utf8').toString('base64');
const script = `Start-Process ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${urlB64}')))`;
return {
cmd: `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')],
};
}
return { cmd: platform === 'darwin' ? 'open' : 'xdg-open', args: [url] };
}
// ShellExecute-family launchers (Start-Process/open/xdg-open) run ANY target they're handed —
// a UNC path is an execution, not a navigation — so only web URLs may reach them.
export const isWebUrl = (url) => /^https?:\/\//i.test(url);
// Best-effort: open a URL in the user's default browser. Returns false if we couldn't launch —
// but a launcher that starts and THEN fails (ENOENT arrives on the async 'error' event) still
// reads as true, so callers must not treat true as proof the browser opened.
export function openUrl(url) {
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
if (!isWebUrl(url))
return false;
const { cmd, args } = openUrlSpawn(url);
try {

@@ -9,0 +42,0 @@ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });

{
"name": "insta",
"version": "0.0.45",
"version": "0.0.47",
"type": "module",

@@ -5,0 +5,0 @@ "description": "InstaCloud CLI — a thin client of the platform control-plane API.",

@@ -68,2 +68,3 @@ # insta-cli

insta services add compute api
insta secrets bind DATABASE_URL postgres/db --to compute/api
insta secrets

@@ -74,4 +75,5 @@ insta deploy .

`project create` makes an empty project and links the current directory. Services are
opt-in, so you add only what you need. `secrets` writes the current branch's credentials to
`./.env`. `deploy .` builds the directory remotely and ships it to the branch's compute
opt-in, so you add only what you need. `secrets` writes the current branch's user-defined
secrets to `./.env` (the postgres connection string is read with `insta db url`). `deploy .`
builds the directory remotely and ships it to the branch's compute
service; it needs a `Dockerfile`, but no local Docker.

@@ -109,6 +111,8 @@

`insta secrets` fetches the current branch's bundle and writes `./.env`. `insta run <cmd>`
does the same without touching disk, injecting the bundle into the child process only.
Credential names are per service — `DATABASE_URL`, `BUCKET_NAME`, `AWS_ACCESS_KEY_ID` —
suffixed with the service name when a project has more than one service of a type.
`insta secrets` fetches the current branch's **user-defined** secrets and writes `./.env`.
`insta run <cmd>` does the same without touching disk, injecting them into the child process
only. Provider-minted service credentials (`DATABASE_URL`, `BUCKET_NAME`,
`AWS_ACCESS_KEY_ID`, …) are not in that bundle — they reach compute through explicit
`insta secrets bind` rules, and the postgres connection string is read directly with
`insta db url` (or `insta db connect` for a psql session).

@@ -203,2 +207,3 @@ ### Destructive actions can require approval

| `insta compute` | `start` · `stop` · `suspend` · `status` · `set-domain` · `check-domain` · `remove-domain` |
| `insta db` | `url` (print the postgres DSN) · `connect` (psql session) · `limits` · `stats` · `always-on` · `volume` |
| `insta regions` | Regions available for postgres and compute |

@@ -205,0 +210,0 @@ | `insta manifest` | Agent-legible view of every branch and its URLs |