insta 0.0.82 → 0.1.0
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 +28 -29
- package/dist/agent.js +2 -2
- package/dist/api.js +19 -4
- package/dist/build-logs.js +32 -8
- package/dist/commands/agent-policy.js +1 -1
- package/dist/commands/auth.js +32 -18
- package/dist/commands/billing.js +5 -5
- package/dist/commands/compute.js +83 -41
- package/dist/commands/db-query.js +18 -14
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/domain.js +49 -2
- package/dist/commands/env.js +1 -1
- package/dist/commands/managed-db.js +34 -0
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/metrics.js +6 -5
- package/dist/commands/observe.js +2 -2
- package/dist/commands/{db.js → postgres.js} +32 -32
- package/dist/commands/regions.js +1 -1
- package/dist/commands/services.js +31 -70
- package/dist/commands/setup.js +8 -7
- package/dist/commands/storage.js +17 -2
- package/dist/commands/template.js +4 -4
- package/dist/commands/upgrade.js +20 -8
- package/dist/config.js +49 -31
- package/dist/deploy-archive.js +65 -56
- package/dist/ensure-skills.js +1 -1
- package/dist/index.js +227 -161
- package/dist/observe/hook.js +1 -1
- package/dist/observe/install.js +1 -1
- package/dist/resolve-service.js +4 -4
- package/dist/telemetry.js +8 -8
- package/dist/util.js +9 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,15 +33,17 @@ version-pinned `npm install -g` fallback to run yourself). E2e-validated on macO
|
|
|
33
33
|
and Windows (PowerShell + cmd):
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
npx -y insta@latest setup
|
|
36
|
+
npx -y insta@latest agent setup
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
`insta setup agent` is still accepted as a hidden alias, so older instructions keep working.
|
|
40
|
+
|
|
39
41
|
This command means **production** (CLI ≥ 0.0.38): if the machine was previously switched to
|
|
40
42
|
staging it switches back — announced, session dropped, like `insta env use prod`. Staging is
|
|
41
43
|
its own explicit command, which also persists the choice:
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
|
-
npx -y insta@latest setup
|
|
46
|
+
npx -y insta@latest agent setup --env staging
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
On macOS/Linux without Node, the native-binary installer puts the `insta` CLI on PATH (the
|
|
@@ -56,7 +58,7 @@ curl -fsSL agents.instacloud.com | sh
|
|
|
56
58
|
|
|
57
59
|
Pin a version with `INSTA_VERSION=v0.0.22`; change the install directory with
|
|
58
60
|
`INSTA_INSTALL_DIR`. While the CLI is pre-1.0 it updates itself on new releases. Turn that
|
|
59
|
-
off with `insta autoupdate off`.
|
|
61
|
+
off with `insta config autoupdate off`.
|
|
60
62
|
|
|
61
63
|
## Quickstart
|
|
62
64
|
|
|
@@ -72,7 +74,7 @@ insta deploy .
|
|
|
72
74
|
|
|
73
75
|
`project create` makes an empty project and links the current directory. Services are
|
|
74
76
|
opt-in, so you add only what you need. `secrets` writes the current branch's user-defined
|
|
75
|
-
secrets to `./.env` (the postgres connection string is read with `insta
|
|
77
|
+
secrets to `./.env` (the postgres connection string is read with `insta postgres url`). `deploy .`
|
|
76
78
|
builds the directory remotely and ships it to the branch's compute
|
|
77
79
|
service, with no local Docker. Whether it needs a `Dockerfile` depends on where the
|
|
78
80
|
service runs: on insta-compute it is optional, and a directory without one is built
|
|
@@ -119,7 +121,7 @@ the two branches diverge independently. A project is capped at 10 branches.
|
|
|
119
121
|
only. Provider-minted service credentials (`DATABASE_URL`, `BUCKET_NAME`,
|
|
120
122
|
`AWS_ACCESS_KEY_ID`, …) are not in that bundle — they reach compute through explicit
|
|
121
123
|
`insta secrets bind` rules, and the postgres connection string is read directly with
|
|
122
|
-
`insta
|
|
124
|
+
`insta postgres url` (or `insta postgres connect` for a psql session).
|
|
123
125
|
|
|
124
126
|
Secrets can be scoped per compute service, so several services may each define the same name — and
|
|
125
127
|
a flat bundle cannot carry two values for one name. Such a name is **withheld** from the bundle and
|
|
@@ -133,8 +135,8 @@ it as `insta run --service compute/<name>` to inject exactly what that one servi
|
|
|
133
135
|
|
|
134
136
|
Agent requests are governed by the project's `agent-policy`; human requests use normal RBAC.
|
|
135
137
|
Where the agent policy says `approve`, the command stops and prints an approval id for a human
|
|
136
|
-
admin to grant with `insta approvals approve <id>`. The agent then retries the unchanged request.
|
|
137
|
-
Run `insta --agent agent
|
|
138
|
+
admin to grant with `insta agent approvals approve <id>`. The agent then retries the unchanged request.
|
|
139
|
+
Run `insta --agent agent policy get --json` for stored overrides, `defaultRules`, `effectiveRules`,
|
|
138
140
|
`bootstrapRules` and `ruleNotes`. Rules distinguish no affected branches (`project`), unprotected
|
|
139
141
|
branches and protected branches. They describe policy, not authorization: RBAC, session checks,
|
|
140
142
|
actual affected resources and compound actions still apply. An empty override object does not
|
|
@@ -143,8 +145,8 @@ mean rules are unavailable. Text output also lists effective rules. The old `pol
|
|
|
143
145
|
|
|
144
146
|
### Agents get the same surface
|
|
145
147
|
|
|
146
|
-
`insta manifest` prints an agent-legible view of every branch and its URLs. `insta
|
|
147
|
-
|
|
148
|
+
`insta agent manifest` prints an agent-legible view of every branch and its URLs. `insta agent
|
|
149
|
+
setup` installs the InstaCloud skill and registers the remote MCP server for the coding
|
|
148
150
|
agents on the machine — and, when running from the npx cache with no durable `insta` on
|
|
149
151
|
PATH, first installs the CLI itself globally.
|
|
150
152
|
|
|
@@ -211,29 +213,26 @@ build never reaches a production installer.
|
|
|
211
213
|
| Command | What it covers |
|
|
212
214
|
|---|---|
|
|
213
215
|
| `insta login` · `logout` · `status` | Browser sign-in (default), `--email` + password, or `--oauth github\|google`; `status` shows the environment, login and linked project/branch |
|
|
214
|
-
| `insta env` | `show` · `use <prod\|staging>` |
|
|
215
|
-
| `insta setup` | `agent` — install the CLI (if missing), the skill, and MCP for every coding agent; targets prod, `--env staging` for staging |
|
|
216
|
-
| `insta mcp` | `install` — register the remote MCP server only |
|
|
217
216
|
| `insta org` | `list` · `create` (one free org per user) |
|
|
218
217
|
| `insta project` | `create` · `list` · `link` · `delete` |
|
|
219
218
|
| `insta branch` | `create` · `list` · `switch` · `delete` · `merge` |
|
|
220
|
-
| `insta services` | `add` · `list` · `remove` · `rename`
|
|
221
|
-
| `insta secrets` | Write `.env`, plus `list` · `set` · `unset` · `tree` |
|
|
219
|
+
| `insta service` (`services`, `svc`) | `add` · `list` · `remove` · `rename` |
|
|
220
|
+
| `insta secrets` | Write `.env`, plus `list` · `set` · `unset` · `bind` · `unbind` · `bindings` · `sources` · `tree` |
|
|
221
|
+
| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach`; buy through InstaCloud: `search` · `buy` · `list` · `status` · `records …` |
|
|
222
|
+
| `insta compute` | `start` · `stop` · `suspend` · `restart` · `status` · `scale` · `limits` · `volume` · `always-on` · `exec` · `ssh` · `repo` · `connect-repo` · `watch-paths` · `disconnect-repo` · `logs` · `metrics` |
|
|
223
|
+
| `insta postgres` | `url` (print the DSN) · `connect` (psql) · `stats` · `limits` · `volume` · `always-on` · `logs` · `metrics` — every verb takes `[service]` |
|
|
224
|
+
| `insta redis` · `mysql` · `mongodb` | `query` · `status` · `limits` · `volume` · `always-on` · `logs` · `metrics` |
|
|
225
|
+
| `insta storage` | `list` · `get` · `delete` · `set-access` |
|
|
226
|
+
| `insta build [dir]` · `deploy [dir]` | Verify a source dir would build; deploy a source directory (built remotely) or `--image <url>`. `insta build logs <id>` reads source-build output: `--source archive` (default) uses the deploy operation ID, `--source github` uses a GitHub build ID; `--follow` watches output, `--json` returns one snapshot |
|
|
222
227
|
| `insta run <cmd>` | Run a command with the branch bundle injected, nothing written to disk |
|
|
223
|
-
| `insta
|
|
224
|
-
| `insta
|
|
225
|
-
| `insta
|
|
226
|
-
| `insta
|
|
227
|
-
| `insta regions` | Regions available for postgres and compute |
|
|
228
|
-
| `insta manifest` | Agent-legible view of every branch and its URLs |
|
|
229
|
-
| `insta build-logs <id>` | Read source-build output; `--source archive` (default) uses the deploy operation ID, `--source github` uses a GitHub build ID; `--follow` watches output, `--json` returns one snapshot |
|
|
230
|
-
| `insta metrics` · `logs` · `events` | Service metrics; runtime logs (`--deploy` for deploy events); audit timeline |
|
|
231
|
-
| `insta usage` · `billing` | Usage by billing dimension; `billing upgrade` · `billing portal` |
|
|
232
|
-
| `insta approvals` | `list` · `approve` · `deny` |
|
|
233
|
-
| `insta agent-policy` | `get` · `set <mode>` · `protect-branch` · `unprotect-branch` · `rule set <action> <decision>` · `revoke-sessions` |
|
|
234
|
-
| `insta observe` | `install` · `uninstall` · `report` · `sync` — local credential audit |
|
|
228
|
+
| `insta template` | `list` · `info` · `deploy` |
|
|
229
|
+
| `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
|
|
230
|
+
| `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
|
|
231
|
+
| `insta config` | `install-mcp` · `regions` · `autoupdate` |
|
|
235
232
|
| `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building; works logged-out |
|
|
236
|
-
| `insta upgrade`
|
|
233
|
+
| `insta upgrade` | Update the CLI |
|
|
234
|
+
|
|
235
|
+
Every command accepts `--api-url <url>` for this invocation only (internal debugging); for `compute exec`, place it before `compute`. `insta --help` documents it.
|
|
237
236
|
|
|
238
237
|
## Configuration
|
|
239
238
|
|
|
@@ -256,10 +255,10 @@ build never reaches a production installer.
|
|
|
256
255
|
## Agent skills
|
|
257
256
|
|
|
258
257
|
The `insta` skill and its task guides live in
|
|
259
|
-
[InsForge/instacloud-skills](https://github.com/InsForge/instacloud-skills). `insta setup
|
|
258
|
+
[InsForge/instacloud-skills](https://github.com/InsForge/instacloud-skills). `insta agent setup`
|
|
260
259
|
installs it user-globally for every coding agent on the machine. `insta project create` and
|
|
261
260
|
`insta project link` additionally install the stack skills (Tigris, Better Auth) into the
|
|
262
|
-
project, along with the `insta observe` credential-audit hook. Postgres needs no stack
|
|
261
|
+
project, along with the `insta agent observe` credential-audit hook. Postgres needs no stack
|
|
263
262
|
skill — it's plain Postgres, reached directly via `DATABASE_URL`.
|
|
264
263
|
|
|
265
264
|
## Contributing
|
package/dist/agent.js
CHANGED
|
@@ -17,7 +17,7 @@ export function canonicalTarget(path) {
|
|
|
17
17
|
const url = new URL(path, 'https://platform.invalid');
|
|
18
18
|
return url.pathname + url.search;
|
|
19
19
|
}
|
|
20
|
-
const guidance = 'agent session missing, expired, or for another project/environment — run `insta setup
|
|
20
|
+
const guidance = 'agent session missing, expired, or for another project/environment — run `insta agent setup`';
|
|
21
21
|
export async function issueAgentSession(api, projectId) {
|
|
22
22
|
const pair = generateKeyPairSync('ed25519');
|
|
23
23
|
const client = mode?.client ?? detectAgent(false)?.client ?? 'unknown';
|
|
@@ -30,7 +30,7 @@ export async function saveAgentSession(session, cwd = process.cwd()) {
|
|
|
30
30
|
const root = await findProjectRoot(cwd) ?? cwd;
|
|
31
31
|
const rel = '.insta/agent-session.json';
|
|
32
32
|
if (alreadyTracked(root, [rel]).length)
|
|
33
|
-
throw new Error('agent-session.json is tracked by Git; untrack it before running insta setup
|
|
33
|
+
throw new Error('agent-session.json is tracked by Git; untrack it before running insta agent setup');
|
|
34
34
|
ensureGitignore(root, [rel], '# Local agent credentials');
|
|
35
35
|
const dir = join(root, '.insta');
|
|
36
36
|
await mkdir(dir, { recursive: true });
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Thin API client over the platform control-plane. Handles bearer auth + one-shot refresh on 401.
|
|
2
2
|
// 2xx (including 202 approval_required) returns the parsed body; >=400 throws ApiError.
|
|
3
|
-
import { readGlobal, writeGlobal, readProject, persistAutoLink, resolveProjectLink, foreignLinkMessage } from './config.js';
|
|
3
|
+
import { readGlobal, readPersistedGlobal, writeGlobal, readProject, persistAutoLink, resolveProjectLink, foreignLinkMessage } from './config.js';
|
|
4
4
|
import { autoResolveProject, promptChoice } from './resolve-project.js';
|
|
5
5
|
import { die } from './util.js';
|
|
6
6
|
import { USER_AGENT } from './version.js';
|
|
@@ -45,8 +45,23 @@ export class ApiClient {
|
|
|
45
45
|
static async load() { return new ApiClient(await readGlobal()); }
|
|
46
46
|
get apiUrl() { return this.cfg.apiUrl; }
|
|
47
47
|
get config() { return this.cfg; }
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// `this.cfg` came from readGlobal(), which has already folded in the AMBIENT control-plane
|
|
49
|
+
// override (--api-url, INSTA_API_URL, INSTA_ENV) — so writing it back verbatim persists a
|
|
50
|
+
// debugging override as this machine's control plane. `insta logout --api-url <staging>` did
|
|
51
|
+
// exactly that: it re-pointed the stored apiUrl at staging and dropped the real login with it
|
|
52
|
+
// (readGlobal scrubs the session of a foreign deployment, and that scrubbed view was the thing
|
|
53
|
+
// being written). Only an EXPLICIT setApiUrl — a login, which chose the deployment it
|
|
54
|
+
// authenticated against — may move the stored URL; everything else keeps what is on disk.
|
|
55
|
+
async persist() {
|
|
56
|
+
if (this.apiUrlExplicit)
|
|
57
|
+
return writeGlobal(this.cfg);
|
|
58
|
+
await writeGlobal({ ...this.cfg, apiUrl: (await readPersistedGlobal()).apiUrl });
|
|
59
|
+
}
|
|
60
|
+
apiUrlExplicit = false;
|
|
61
|
+
setApiUrl(url) {
|
|
62
|
+
this.cfg.apiUrl = url;
|
|
63
|
+
this.apiUrlExplicit = true;
|
|
64
|
+
}
|
|
50
65
|
setSession(tokens, user) {
|
|
51
66
|
this.cfg.accessToken = tokens.accessToken;
|
|
52
67
|
this.cfg.refreshToken = tokens.refreshToken;
|
|
@@ -144,7 +159,7 @@ export async function requireProject(deps = {}) {
|
|
|
144
159
|
if (r)
|
|
145
160
|
return r.link;
|
|
146
161
|
if (agentMode())
|
|
147
|
-
die('agent mode requires a linked project — run `insta setup
|
|
162
|
+
die('agent mode requires a linked project — run `insta agent setup --project <id>`');
|
|
148
163
|
if (deps.autoResolve)
|
|
149
164
|
return deps.autoResolve();
|
|
150
165
|
// One command, just works: unlinked ≠ error. Resolve the project (auto when there's one,
|
package/dist/build-logs.js
CHANGED
|
@@ -24,7 +24,20 @@ export async function readBuildLogs(api, projectId, source, buildId, signal = Ab
|
|
|
24
24
|
if (++requests > 200)
|
|
25
25
|
throw new Error('build logs exceed the per-read page limit');
|
|
26
26
|
const params = new URLSearchParams({ ...(step ? { step } : {}), ...(cursor ? { cursor } : {}) });
|
|
27
|
-
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
const request = new AbortController();
|
|
29
|
+
const abort = () => request.abort(signal.reason);
|
|
30
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
31
|
+
const timeout = setTimeout(() => request.abort(new DOMException('build log request timed out', 'TimeoutError')), 20_000);
|
|
32
|
+
let body;
|
|
33
|
+
try {
|
|
34
|
+
const response = await api.rawRequest('GET', `/projects/${projectId}/builds/${source}/${encodeURIComponent(buildId)}/logs?${params}`, undefined, { signal: request.signal });
|
|
35
|
+
body = response.body;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
signal.removeEventListener('abort', abort);
|
|
40
|
+
}
|
|
28
41
|
if (!body || !['ready', 'pending', 'unsupported', 'unavailable'].includes(body.state) || !Array.isArray(body.steps) || !Array.isArray(body.entries))
|
|
29
42
|
throw new Error('invalid build log response');
|
|
30
43
|
bytes += Buffer.byteLength(JSON.stringify(body));
|
|
@@ -143,20 +156,24 @@ export function archiveLogWatcher(api, projectId, write, wait = sleep) {
|
|
|
143
156
|
const follow = { tails: new Map(), emit: snapshot => printer.print(snapshot, write) };
|
|
144
157
|
let warned = '';
|
|
145
158
|
let unavailable = false;
|
|
146
|
-
return async (buildId, finished, remainingMs = 30_000) => {
|
|
159
|
+
return async (buildId, finished, remainingMs = 30_000, cancelSignal) => {
|
|
147
160
|
if (unavailable)
|
|
148
161
|
return;
|
|
149
|
-
const
|
|
162
|
+
const started = Date.now();
|
|
163
|
+
const deadline = started + remainingMs;
|
|
164
|
+
const refreshDeadline = started + Math.min(remainingMs, finished ? 18_000 : remainingMs);
|
|
150
165
|
for (let attempt = 0; attempt < (finished ? 6 : 1); attempt++) {
|
|
151
166
|
if (attempt > 0)
|
|
152
|
-
await wait(Math.min(3000, Math.max(0,
|
|
153
|
-
const
|
|
167
|
+
await wait(Math.min(3000, Math.max(0, refreshDeadline - Date.now())));
|
|
168
|
+
const finalRead = finished && (attempt === 5 || Date.now() >= refreshDeadline);
|
|
169
|
+
const remaining = Math.min(deadline - Date.now(), finalRead ? 30_000 : refreshDeadline - Date.now());
|
|
154
170
|
if (remaining <= 0)
|
|
155
171
|
return;
|
|
156
|
-
if (finished && (attempt === 0 ||
|
|
172
|
+
if (finished && (attempt === 0 || finalRead))
|
|
157
173
|
follow.tails.clear();
|
|
174
|
+
const signal = cancelSignal ?? AbortSignal.timeout(remaining);
|
|
158
175
|
try {
|
|
159
|
-
const snapshot = await readBuildLogs(api, projectId, 'archive', buildId,
|
|
176
|
+
const snapshot = await readBuildLogs(api, projectId, 'archive', buildId, signal, follow);
|
|
160
177
|
if (snapshot.state === 'unsupported') {
|
|
161
178
|
printer.finishLine(write);
|
|
162
179
|
write('Build logs are not supported for this build provider.\n');
|
|
@@ -168,14 +185,21 @@ export function archiveLogWatcher(api, projectId, write, wait = sleep) {
|
|
|
168
185
|
warned = '';
|
|
169
186
|
}
|
|
170
187
|
catch (error) {
|
|
188
|
+
if (cancelSignal?.aborted)
|
|
189
|
+
return;
|
|
190
|
+
if (finished && signal.aborted && !finalRead && Date.now() < deadline)
|
|
191
|
+
continue;
|
|
171
192
|
if (error instanceof ApiError && error.status === 400)
|
|
172
193
|
follow.tails.clear();
|
|
173
194
|
printer.finishLine(write);
|
|
174
|
-
const
|
|
195
|
+
const reason = signal.aborted || (error instanceof Error && error.name === 'TimeoutError') ? ' (timed out)' : error instanceof ApiError ? ` (HTTP ${error.status})` : '';
|
|
196
|
+
const message = `Could not read build logs${reason}. Retry with: insta build logs ${buildId}\n`;
|
|
175
197
|
if (warned !== message)
|
|
176
198
|
write(message);
|
|
177
199
|
warned = message;
|
|
178
200
|
}
|
|
201
|
+
if (finalRead)
|
|
202
|
+
return;
|
|
179
203
|
}
|
|
180
204
|
};
|
|
181
205
|
}
|
|
@@ -82,7 +82,7 @@ export function applyRule(policy, out, action, decision) {
|
|
|
82
82
|
return { ...policy, branchDeveloperRules: { ...(policy.branchDeveloperRules ?? {}), [action]: decision } };
|
|
83
83
|
const entry = catalog.find(e => e.action === action);
|
|
84
84
|
if (!entry)
|
|
85
|
-
throw new Error(`unknown action: ${action}\nrun: insta agent
|
|
85
|
+
throw new Error(`unknown action: ${action}\nrun: insta agent policy get --json`);
|
|
86
86
|
if (!entry.editable)
|
|
87
87
|
throw new Error(`${action} is a fixed policy invariant and cannot be overridden`);
|
|
88
88
|
// Snapshot taken from the resolved unprotected-branch view, which is the context these rules
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { ApiClient, ApiError, linkedProject } from '../api.js';
|
|
4
|
+
import { readGlobal, readPersistedGlobal } from '../config.js';
|
|
4
5
|
import { agentMode } from '../agent.js';
|
|
5
|
-
import { ENVS, ENV_NAMES, envForApiUrl, isEnvName } from '../env.js';
|
|
6
|
+
import { ENVS, ENV_NAMES, envForApiUrl, isEnvName, normalizeUrl } from '../env.js';
|
|
6
7
|
import { info, die, printJson, promptPassword, openUrl } from '../util.js';
|
|
7
8
|
/** --api-url and --env both set the target host; --api-url wins (more specific), matching the
|
|
8
9
|
* INSTA_API_URL > INSTA_ENV precedence in config.ts. Returns the URL to point at, or undefined
|
|
9
|
-
* to leave whatever is already resolved alone.
|
|
10
|
+
* to leave whatever is already resolved alone.
|
|
11
|
+
*
|
|
12
|
+
* Every login entry point feeds this into `api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl)`: a
|
|
13
|
+
* login is the one command that MAY move the machine's stored control-plane URL, and it must
|
|
14
|
+
* store the deployment it actually authenticated against — flag, env var or stored URL alike.
|
|
15
|
+
* Without the explicit set, ApiClient.persist() keeps the URL already on disk (see its comment),
|
|
16
|
+
* which would file a session minted on one deployment under another one's URL. */
|
|
10
17
|
function targetApiUrl(opts) {
|
|
11
18
|
if (opts.apiUrl)
|
|
12
19
|
return opts.apiUrl;
|
|
@@ -50,9 +57,7 @@ export async function login(opts, device = loginDevice, claim = loginClaim) {
|
|
|
50
57
|
return device(opts, openUrl);
|
|
51
58
|
}
|
|
52
59
|
const api = await ApiClient.load();
|
|
53
|
-
|
|
54
|
-
if (target)
|
|
55
|
-
api.setApiUrl(target);
|
|
60
|
+
api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl);
|
|
56
61
|
const password = opts.password ?? process.env.INSTA_PASSWORD ?? (await promptPassword());
|
|
57
62
|
const res = await api.request('POST', '/auth/login', { email: opts.email, password }, { auth: false });
|
|
58
63
|
api.setSession(res, res.user);
|
|
@@ -65,9 +70,7 @@ export async function loginOauth(provider, opts) {
|
|
|
65
70
|
if (provider !== 'github' && provider !== 'google')
|
|
66
71
|
die('provider must be github or google');
|
|
67
72
|
const api = await ApiClient.load();
|
|
68
|
-
|
|
69
|
-
if (target)
|
|
70
|
-
api.setApiUrl(target);
|
|
73
|
+
api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl);
|
|
71
74
|
const token = await browserOauth(api.apiUrl, provider);
|
|
72
75
|
api.setSession({ accessToken: token, refreshToken: token });
|
|
73
76
|
const me = await api.request('GET', '/me');
|
|
@@ -82,9 +85,7 @@ export async function loginOauth(provider, opts) {
|
|
|
82
85
|
// (which owns the signin round-trip), and poll the platform until they approve.
|
|
83
86
|
export async function loginDevice(opts, open) {
|
|
84
87
|
const api = await ApiClient.load();
|
|
85
|
-
|
|
86
|
-
if (target)
|
|
87
|
-
api.setApiUrl(target);
|
|
88
|
+
api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl);
|
|
88
89
|
const token = await deviceGrant((path, body) => api.request('POST', path, body, { auth: false }), sleepSeconds, open);
|
|
89
90
|
api.setSession({ accessToken: token, refreshToken: token });
|
|
90
91
|
const me = await api.request('GET', '/me');
|
|
@@ -96,9 +97,7 @@ export async function loginDevice(opts, open) {
|
|
|
96
97
|
// the console, the platform mints an insta_ key, and it is stored exactly as --api-key stores one.
|
|
97
98
|
export async function loginClaim(email, opts, open, grant = claimGrant) {
|
|
98
99
|
const api = await ApiClient.load();
|
|
99
|
-
|
|
100
|
-
if (target)
|
|
101
|
-
api.setApiUrl(target);
|
|
100
|
+
api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl);
|
|
102
101
|
const client = agentMode()?.client ?? 'unknown';
|
|
103
102
|
const key = await grant(email, client, (path, body, signal) => api.request('POST', path, body, { auth: false, signal }), sleepSeconds, open);
|
|
104
103
|
const user = await applyApiKeyLogin(api, key);
|
|
@@ -108,9 +107,7 @@ export async function loginClaim(email, opts, open, grant = claimGrant) {
|
|
|
108
107
|
// Non-interactive login with a durable insta_ key (minted via POST /tokens): store it and confirm against /me. No browser, no polling.
|
|
109
108
|
export async function loginApiKey(key, opts) {
|
|
110
109
|
const api = await ApiClient.load();
|
|
111
|
-
|
|
112
|
-
if (target)
|
|
113
|
-
api.setApiUrl(target);
|
|
110
|
+
api.setApiUrl(targetApiUrl(opts) ?? api.apiUrl);
|
|
114
111
|
const user = await applyApiKeyLogin(api, key);
|
|
115
112
|
await api.persist();
|
|
116
113
|
info(`logged in as ${user.email ?? user.id} @ ${api.apiUrl}`);
|
|
@@ -344,8 +341,25 @@ function browserOauth(apiUrl, provider) {
|
|
|
344
341
|
});
|
|
345
342
|
});
|
|
346
343
|
}
|
|
344
|
+
/** Log out: revoke the session on the server, then clear the local tokens.
|
|
345
|
+
*
|
|
346
|
+
* Built from the PERSISTED config, not from `ApiClient.load()`'s override-resolved view. There is
|
|
347
|
+
* exactly one stored session, so a runtime `--api-url` (or INSTA_API_URL / INSTA_ENV) has no
|
|
348
|
+
* subject here — and pointing at a foreign deployment made this actively unsafe: readGlobal()
|
|
349
|
+
* scrubs a foreign deployment's tokens, so the revoke below was skipped for want of a refresh
|
|
350
|
+
* token while the local tokens were deleted anyway, leaving the session valid on the server with
|
|
351
|
+
* nothing left on this machine to revoke it with. The revoke now always goes to the deployment
|
|
352
|
+
* the session belongs to, with the real refresh token. `persist()` keeps the stored URL (see its
|
|
353
|
+
* comment): logout never sets one explicitly. */
|
|
347
354
|
export async function logout() {
|
|
348
|
-
const
|
|
355
|
+
const stored = await readPersistedGlobal();
|
|
356
|
+
// Say so rather than ignoring it silently: exiting 0 with a bare "logged out" while the flag
|
|
357
|
+
// named a different deployment reads as if that deployment was the one logged out of.
|
|
358
|
+
const resolved = (await readGlobal()).apiUrl;
|
|
359
|
+
if (normalizeUrl(resolved) !== normalizeUrl(stored.apiUrl)) {
|
|
360
|
+
info(`note: the control-plane override (${resolved}) does not apply to logout — there is one stored session; logging out of ${stored.apiUrl}`);
|
|
361
|
+
}
|
|
362
|
+
const api = new ApiClient(stored);
|
|
349
363
|
if (api.config.refreshToken) {
|
|
350
364
|
try {
|
|
351
365
|
await api.request('POST', '/auth/logout', { refreshToken: api.config.refreshToken }, { auth: false });
|
package/dist/commands/billing.js
CHANGED
|
@@ -49,17 +49,17 @@ export function billingLines(s, org) {
|
|
|
49
49
|
const lapsed = s.subscriptionStatus === 'past_due' || s.subscriptionStatus === 'unpaid';
|
|
50
50
|
const ended = s.subscriptionStatus === 'canceled' || s.subscriptionStatus === 'incomplete_expired';
|
|
51
51
|
lines.push(s.tier === 'free'
|
|
52
|
-
? `⚠ org suspended — billing limit reached; resumes next cycle (or \`insta billing
|
|
52
|
+
? `⚠ org suspended — billing limit reached; resumes next cycle (or \`insta billing subscribe pro${flag}\`)`
|
|
53
53
|
: lapsed
|
|
54
54
|
? `⚠ org suspended — subscription payment did not go through; settle it in \`insta billing portal${flag}\``
|
|
55
55
|
: ended
|
|
56
56
|
? s.tier === 'enterprise'
|
|
57
|
-
// Per-deal, and `billing
|
|
57
|
+
// Per-deal, and `billing subscribe` cannot create one: naming a self-serve tier here
|
|
58
58
|
// would move them off the plan they negotiated.
|
|
59
59
|
? '⚠ org suspended — the subscription ended; contact support to restore this plan'
|
|
60
|
-
// Their OWN tier, not a hardcoded one: suggesting `
|
|
60
|
+
// Their OWN tier, not a hardcoded one: suggesting `subscribe pro` to a Team org
|
|
61
61
|
// resubscribes it onto the wrong plan.
|
|
62
|
-
: `⚠ org suspended — the subscription ended; resubscribe with \`insta billing
|
|
62
|
+
: `⚠ org suspended — the subscription ended; resubscribe with \`insta billing subscribe ${s.tier}${flag}\``
|
|
63
63
|
// Deliberately claims nothing about the subscription: `incomplete` reaches here too,
|
|
64
64
|
// and that one is neither current nor failed. All this branch knows is that the
|
|
65
65
|
// suspension has no billing cause it can name.
|
|
@@ -87,7 +87,7 @@ export async function billing(opts) {
|
|
|
87
87
|
for (const l of billingLines(s, opts.org))
|
|
88
88
|
info(l);
|
|
89
89
|
}
|
|
90
|
-
// insta billing
|
|
90
|
+
// insta billing subscribe <tier> — start a Stripe Checkout to subscribe the org to a paid tier.
|
|
91
91
|
export async function billingUpgrade(tier, opts) {
|
|
92
92
|
// pro|team, matching what POST /orgs/:orgId/billing/checkout accepts: `team` is a real
|
|
93
93
|
// self-serve tier, and `enterprise` is per-deal and 400s at the server.
|