@rebasepro/cli 0.25.1 → 0.26.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 +2 -1
- package/bin/rebase.js +143 -4
- package/dist/{agent-setup-BYb2Tbdz.js → agent-setup-D9mHRZ5R.js} +125 -3
- package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
- package/dist/{api-keys-CSMbsmAn.js → api-keys-CyJ26KEJ.js} +41 -9
- package/dist/api-keys-CyJ26KEJ.js.map +1 -0
- package/dist/{apps-iBXNZhVN.js → apps-CnLDV5b6.js} +6 -6
- package/dist/{apps-iBXNZhVN.js.map → apps-CnLDV5b6.js.map} +1 -1
- package/dist/{atlas-dev-database-BusbHP6n.js → atlas-dev-database-BODqdCWt.js} +2 -2
- package/dist/{atlas-dev-database-BusbHP6n.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
- package/dist/{auth-DRsTWPky.js → auth-dII2jCO1.js} +10 -7
- package/dist/auth-dII2jCO1.js.map +1 -0
- package/dist/{branch-pointer-Bn_FjNld.js → branch-pointer-BQUF6lYa.js} +1 -1
- package/dist/{build-et7Rccxi.js → build-Cza7ZQrp.js} +6 -6
- package/dist/{build-et7Rccxi.js.map → build-Cza7ZQrp.js.map} +1 -1
- package/dist/{bundle-DlqkDYSe.js → bundle-C6yEtMJ-.js} +53 -5
- package/dist/bundle-C6yEtMJ-.js.map +1 -0
- package/dist/bundle.d.ts +14 -0
- package/dist/{cloud-B-ZIjHrA.js → cloud-BdnmqQaZ.js} +8527 -4340
- package/dist/cloud-BdnmqQaZ.js.map +1 -0
- package/dist/collections-dir-DlvDhime.js +69 -0
- package/dist/collections-dir-DlvDhime.js.map +1 -0
- package/dist/commands/agent-setup.d.ts +58 -0
- package/dist/commands/api-keys.d.ts +13 -0
- package/dist/commands/cloud/activity.d.ts +25 -0
- package/dist/commands/cloud/app-key.d.ts +18 -0
- package/dist/commands/cloud/auth.d.ts +1 -0
- package/dist/commands/cloud/billing-invoices.d.ts +39 -0
- package/dist/commands/cloud/claim.d.ts +7 -0
- package/dist/commands/cloud/context.d.ts +115 -6
- package/dist/commands/cloud/deploy-summary.d.ts +162 -0
- package/dist/commands/cloud/deploy.d.ts +64 -0
- package/dist/commands/cloud/env.d.ts +24 -0
- package/dist/commands/cloud/index.d.ts +13 -1
- package/dist/commands/cloud/init.d.ts +143 -0
- package/dist/commands/cloud/login-flow.d.ts +51 -0
- package/dist/commands/cloud/notifications.d.ts +28 -0
- package/dist/commands/cloud/orgs.d.ts +13 -0
- package/dist/commands/cloud/previews.d.ts +123 -0
- package/dist/commands/cloud/resources.d.ts +68 -14
- package/dist/commands/cloud/runtime-logs.d.ts +176 -0
- package/dist/commands/cloud/service-key.d.ts +15 -0
- package/dist/commands/cloud/token-capabilities.d.ts +61 -8
- package/dist/commands/cloud/tokens.d.ts +18 -3
- package/dist/commands/cloud/usage.d.ts +63 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/generate-view.d.ts +94 -0
- package/dist/commands/generate_sdk.d.ts +15 -0
- package/dist/commands/skills.d.ts +4 -3
- package/dist/commands/storage.d.ts +4 -0
- package/dist/commands/verify.d.ts +96 -0
- package/dist/{consent-BaB-eQKG.js → consent-BD7F29T3.js} +2 -2
- package/dist/{consent-BaB-eQKG.js.map → consent-BD7F29T3.js.map} +1 -1
- package/dist/{console-BlD9kjFW.js → console-BXB6-8GT.js} +12 -6
- package/dist/console-BXB6-8GT.js.map +1 -0
- package/dist/{context-DHiiiJ5C.js → context-GUxkZm-S.js} +481 -24
- package/dist/context-GUxkZm-S.js.map +1 -0
- package/dist/{daemon-DDMRbz4T.js → daemon-Cp7oNXth.js} +2 -2
- package/dist/{daemon-DDMRbz4T.js.map → daemon-Cp7oNXth.js.map} +1 -1
- package/dist/{db-9P6MnmdC.js → db-KzlqddGD.js} +37 -30
- package/dist/db-KzlqddGD.js.map +1 -0
- package/dist/{derive-MTdClnkc.js → derive-ae9UOoOB.js} +2 -2
- package/dist/{derive-MTdClnkc.js.map → derive-ae9UOoOB.js.map} +1 -1
- package/dist/{dev-BDN_OKXA.js → dev-DTqVMgB3.js} +59 -32
- package/dist/dev-DTqVMgB3.js.map +1 -0
- package/dist/{dev-preflight-FT9GbJVj.js → dev-preflight-Cxv3TiD1.js} +2 -2
- package/dist/{dev-preflight-FT9GbJVj.js.map → dev-preflight-Cxv3TiD1.js.map} +1 -1
- package/dist/{dev-server-CIZwAfZa.js → dev-server-OJsUGZEB.js} +2 -2
- package/dist/{dev-server-CIZwAfZa.js.map → dev-server-OJsUGZEB.js.map} +1 -1
- package/dist/{doctor-D8YXz7df.js → doctor-BRWOlqeG.js} +13 -11
- package/dist/doctor-BRWOlqeG.js.map +1 -0
- package/dist/{eject-izG2KjMo.js → eject-DABcDnPN.js} +3 -3
- package/dist/{eject-izG2KjMo.js.map → eject-DABcDnPN.js.map} +1 -1
- package/dist/{fold-static-CybSay_W.js → fold-static-BnAK-anQ.js} +3 -3
- package/dist/{fold-static-CybSay_W.js.map → fold-static-BnAK-anQ.js.map} +1 -1
- package/dist/generate-view-IFwCADvb.js +662 -0
- package/dist/generate-view-IFwCADvb.js.map +1 -0
- package/dist/{generate_sdk-B4R65oUV.js → generate_sdk-BvcWvoUh.js} +33 -7
- package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
- package/dist/index.es.js +57 -38
- package/dist/index.es.js.map +1 -1
- package/dist/{init-DYaeppEN.js → init-CaU6GhwI.js} +8 -8
- package/dist/{init-DYaeppEN.js.map → init-CaU6GhwI.js.map} +1 -1
- package/dist/last-dev-database-DpW5dfoX.js +2 -0
- package/dist/{manifest-nvPdLlJY.js → manifest-BtzDy7KQ.js} +36 -3
- package/dist/{manifest-nvPdLlJY.js.map → manifest-BtzDy7KQ.js.map} +1 -1
- package/dist/{normalize-imports-BYw8ad87.js → normalize-imports-D0zXlM6g.js} +3 -3
- package/dist/{normalize-imports-BYw8ad87.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
- package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
- package/dist/path-flags-DNvtEi2-.js.map +1 -0
- package/dist/{prepare-1rVNd2PF.js → prepare-CTw9NEkh.js} +4 -4
- package/dist/{prepare-1rVNd2PF.js.map → prepare-CTw9NEkh.js.map} +1 -1
- package/dist/{project-g4K6kV1t.js → project-CasK56vN.js} +2 -2
- package/dist/{project-g4K6kV1t.js.map → project-CasK56vN.js.map} +1 -1
- package/dist/resolve-D647F1LA.js +2 -0
- package/dist/{resources-CovP8AZ-.js → resources-CPSC5Kcu.js} +4 -4
- package/dist/{resources-CovP8AZ-.js.map → resources-CPSC5Kcu.js.map} +1 -1
- package/dist/{schema-nuxvvc50.js → schema-BSXwZSiI.js} +9 -7
- package/dist/schema-BSXwZSiI.js.map +1 -0
- package/dist/{skills-DeUIsZcT.js → skills-BU2flMOy.js} +52 -7
- package/dist/skills-BU2flMOy.js.map +1 -0
- package/dist/{start-RzZqDroW.js → start-mVb6upwY.js} +5 -5
- package/dist/{start-RzZqDroW.js.map → start-mVb6upwY.js.map} +1 -1
- package/dist/static-syntax-check.d.ts +64 -0
- package/dist/{status-D9Zm88NZ.js → status-BREvtzJm.js} +7 -7
- package/dist/{status-D9Zm88NZ.js.map → status-BREvtzJm.js.map} +1 -1
- package/dist/{storage-BB3umRfw.js → storage-DtOXGflp.js} +159 -15
- package/dist/storage-DtOXGflp.js.map +1 -0
- package/dist/{telemetry-HfU5uAh1.js → telemetry-CgwJPVMO.js} +2 -2
- package/dist/{telemetry-HfU5uAh1.js.map → telemetry-CgwJPVMO.js.map} +1 -1
- package/dist/{telemetry-TseNLk6X.js → telemetry-DwK5RQJl.js} +4 -4
- package/dist/{telemetry-TseNLk6X.js.map → telemetry-DwK5RQJl.js.map} +1 -1
- package/dist/{upgrade-apiPKKE_.js → upgrade-B7sVwL8S.js} +4 -4
- package/dist/{upgrade-apiPKKE_.js.map → upgrade-B7sVwL8S.js.map} +1 -1
- package/dist/utils/collections-dir.d.ts +14 -0
- package/dist/utils/path-flags.d.ts +2 -1
- package/dist/utils/tool-stdio.d.ts +1 -1
- package/dist/verify-D7UsV_qe.js +730 -0
- package/dist/verify-D7UsV_qe.js.map +1 -0
- package/package.json +9 -7
- package/templates/eject/backend/src/index.ts +9 -1
- package/templates/generated-schemas/ecommerce.ts +3 -0
- package/templates/overlays/baas/ai-instructions.md +13 -0
- package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
- package/templates/template/.claude/settings.json +17 -0
- package/templates/template/.env.example +10 -1
- package/templates/template/ai-instructions.md +21 -1
- package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
- package/templates/template/docker-compose.yml +4 -1
- package/templates/template/frontend/package.json +1 -0
- package/templates/views/AppView.tsx.tmpl +138 -0
- package/templates/views/EntityView.tsx.tmpl +64 -0
- package/templates/views/index.ts.tmpl +8 -0
- package/dist/agent-setup-BYb2Tbdz.js.map +0 -1
- package/dist/api-keys-CSMbsmAn.js.map +0 -1
- package/dist/auth-DRsTWPky.js.map +0 -1
- package/dist/bundle-DlqkDYSe.js.map +0 -1
- package/dist/cloud-B-ZIjHrA.js.map +0 -1
- package/dist/console-BlD9kjFW.js.map +0 -1
- package/dist/context-DHiiiJ5C.js.map +0 -1
- package/dist/db-9P6MnmdC.js.map +0 -1
- package/dist/dev-BDN_OKXA.js.map +0 -1
- package/dist/doctor-D8YXz7df.js.map +0 -1
- package/dist/generate_sdk-B4R65oUV.js.map +0 -1
- package/dist/last-dev-database-B_75fxZC.js +0 -2
- package/dist/path-flags-BFgG9V6y.js.map +0 -1
- package/dist/resolve-BQUAC3Wi.js +0 -2
- package/dist/schema-nuxvvc50.js.map +0 -1
- package/dist/skills-DeUIsZcT.js.map +0 -1
- package/dist/storage-BB3umRfw.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as switchOn, c as findProjectRoot, x as parseCommandArgs } from "./project-CasK56vN.js";
|
|
2
2
|
import { t as cliUserAgent } from "./version-Buyyo1ut.js";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import arg from "arg";
|
|
@@ -166,6 +166,194 @@ function wantsRawError(argv = process.argv) {
|
|
|
166
166
|
return switchOn(argv, "--debug") || parseEnvBoolean(process.env.REBASE_DEBUG) === true;
|
|
167
167
|
}
|
|
168
168
|
//#endregion
|
|
169
|
+
//#region src/commands/cloud/token-capabilities.ts
|
|
170
|
+
/** Every capability, in the order they are offered. */
|
|
171
|
+
var TOKEN_CAPABILITY_NAMES = [
|
|
172
|
+
"deploy",
|
|
173
|
+
"logs",
|
|
174
|
+
"env",
|
|
175
|
+
"database",
|
|
176
|
+
"backups"
|
|
177
|
+
];
|
|
178
|
+
/**
|
|
179
|
+
* Scopes every token holds, whatever it was created for: resolving a project
|
|
180
|
+
* by its slug, reading the deployment a command follows (a production one, or
|
|
181
|
+
* a preview's), the organization a project belongs to, and the control plane's
|
|
182
|
+
* public description of itself.
|
|
183
|
+
*/
|
|
184
|
+
var ALWAYS_SCOPES = [
|
|
185
|
+
"data:read:projects",
|
|
186
|
+
"data:read:deployments",
|
|
187
|
+
"data:read:preview-deployments",
|
|
188
|
+
"data:read:organizations",
|
|
189
|
+
"functions:invoke:platform-config"
|
|
190
|
+
];
|
|
191
|
+
/** Each capability: what it lets a token do, and the scopes that does on one project. */
|
|
192
|
+
var TOKEN_CAPABILITIES = {
|
|
193
|
+
deploy: {
|
|
194
|
+
label: "Deploy the project and follow its builds",
|
|
195
|
+
scopes: (projectId) => [
|
|
196
|
+
`project:read:${projectId}`,
|
|
197
|
+
`project:deploy:${projectId}`,
|
|
198
|
+
"functions:invoke:deploy",
|
|
199
|
+
"functions:invoke:previews"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
logs: {
|
|
203
|
+
label: "Read build and runtime logs, cron history, live metrics and usage",
|
|
204
|
+
scopes: (projectId) => [
|
|
205
|
+
`project:read:${projectId}`,
|
|
206
|
+
`project:logs:${projectId}`,
|
|
207
|
+
"functions:invoke:runtime-logs",
|
|
208
|
+
"functions:invoke:metrics",
|
|
209
|
+
"functions:invoke:runtime-token",
|
|
210
|
+
"functions:invoke:previews",
|
|
211
|
+
"functions:invoke:usage"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
env: {
|
|
215
|
+
label: "Read and change environment variables, secrets included",
|
|
216
|
+
scopes: (projectId) => [
|
|
217
|
+
`project:env:${projectId}`,
|
|
218
|
+
"functions:invoke:env-vars",
|
|
219
|
+
"functions:invoke:previews"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
database: {
|
|
223
|
+
label: "Connect to the database and read its password",
|
|
224
|
+
scopes: (projectId) => [
|
|
225
|
+
`project:read:${projectId}`,
|
|
226
|
+
`project:database:${projectId}`,
|
|
227
|
+
"functions:invoke:db-info",
|
|
228
|
+
"data:read:databases"
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
backups: {
|
|
232
|
+
label: "List, create and download backups (restoring needs a signed-in person)",
|
|
233
|
+
scopes: (projectId) => [
|
|
234
|
+
`project:read:${projectId}`,
|
|
235
|
+
`project:backups:${projectId}`,
|
|
236
|
+
"functions:invoke:backup"
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
/** Whether a word names a capability. */
|
|
241
|
+
function isTokenCapability(value) {
|
|
242
|
+
return TOKEN_CAPABILITY_NAMES.includes(value);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The scopes a token for `capabilities` on one project holds: {@link ALWAYS_SCOPES}
|
|
246
|
+
* first, then each capability's, each scope once.
|
|
247
|
+
*
|
|
248
|
+
* Throws on a project id a scope cannot carry — empty, or with whitespace — and
|
|
249
|
+
* on an unknown capability, both of which are a caller's mistake rather than
|
|
250
|
+
* something to mint a token from.
|
|
251
|
+
*/
|
|
252
|
+
function scopesForCapabilities(projectId, capabilities) {
|
|
253
|
+
if (projectId.length === 0 || /\s/.test(projectId)) throw new Error(`"${projectId}" is not a project id a scope can be narrowed to.`);
|
|
254
|
+
const scopes = new Set(ALWAYS_SCOPES);
|
|
255
|
+
for (const capability of capabilities) {
|
|
256
|
+
if (!isTokenCapability(capability)) throw new Error(`"${String(capability)}" is not a token capability. Capabilities: ${TOKEN_CAPABILITY_NAMES.join(", ")}.`);
|
|
257
|
+
for (const scope of TOKEN_CAPABILITIES[capability].scopes(projectId)) scopes.add(scope);
|
|
258
|
+
}
|
|
259
|
+
return [...scopes];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read a token's scopes back as capabilities: the projects its project scopes
|
|
263
|
+
* name, and, for each, the capabilities it holds every scope of.
|
|
264
|
+
*
|
|
265
|
+
* For showing a token — `rebase cloud tokens list`, `rebase cloud whoami` — so
|
|
266
|
+
* a list of fifteen scope strings reads as "deploy, logs on <project>".
|
|
267
|
+
*/
|
|
268
|
+
function capabilitiesInScopes(scopes) {
|
|
269
|
+
const held = new Set(scopes);
|
|
270
|
+
const projectIds = [];
|
|
271
|
+
for (const scope of scopes) {
|
|
272
|
+
const match = /^project:[a-z][a-z0-9-]*:(.+)$/.exec(scope);
|
|
273
|
+
if (match && !projectIds.includes(match[1])) projectIds.push(match[1]);
|
|
274
|
+
}
|
|
275
|
+
return projectIds.map((projectId) => ({
|
|
276
|
+
projectId,
|
|
277
|
+
capabilities: TOKEN_CAPABILITY_NAMES.filter((capability) => TOKEN_CAPABILITIES[capability].scopes(projectId).every((scope) => held.has(scope)))
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether a token's scopes reach one project: a project scope narrowed to it,
|
|
282
|
+
* or an unnarrowed one, which reaches every project.
|
|
283
|
+
*
|
|
284
|
+
* What decides, in a directory `rebase cloud init --claim` linked, whether
|
|
285
|
+
* `REBASE_TOKEN` or the token stored for that project acts on it: a
|
|
286
|
+
* `REBASE_TOKEN` made for other projects would be refused on this one.
|
|
287
|
+
*/
|
|
288
|
+
function scopesReachProject(scopes, projectId) {
|
|
289
|
+
return scopes.some((scope) => {
|
|
290
|
+
const match = /^project:[a-z][a-z0-9-]*(?::(.+))?$/.exec(scope);
|
|
291
|
+
return match !== null && (match[1] === void 0 || match[1] === projectId);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** Every organization capability, in the order they are offered. */
|
|
295
|
+
var ORG_CAPABILITY_NAMES = [
|
|
296
|
+
"billing",
|
|
297
|
+
"audit",
|
|
298
|
+
"notifications"
|
|
299
|
+
];
|
|
300
|
+
/** Each organization capability: what it lets a token do, and the scopes that does on one organization. */
|
|
301
|
+
var ORG_CAPABILITIES = {
|
|
302
|
+
billing: {
|
|
303
|
+
label: "Read the organization's billing: card on file, invoices and the next invoice (owners and admins)",
|
|
304
|
+
scopes: (organizationId) => [
|
|
305
|
+
`org:billing:${organizationId}`,
|
|
306
|
+
"functions:invoke:stripe-billing",
|
|
307
|
+
"data:read:billing-accounts"
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
audit: {
|
|
311
|
+
label: "Read the organization's activity log (owners and admins)",
|
|
312
|
+
scopes: (organizationId) => [`org:audit:${organizationId}`, "functions:invoke:audit"]
|
|
313
|
+
},
|
|
314
|
+
notifications: {
|
|
315
|
+
label: "Manage notifications: your own email choices, and its projects' webhooks",
|
|
316
|
+
scopes: (organizationId) => [`org:notifications:${organizationId}`, "functions:invoke:notifications"]
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
/** Whether a word names an organization capability. */
|
|
320
|
+
function isOrgCapability(value) {
|
|
321
|
+
return ORG_CAPABILITY_NAMES.includes(value);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The scopes `capabilities` hold on one organization, each once — added to a
|
|
325
|
+
* token's project scopes, so not {@link ALWAYS_SCOPES}.
|
|
326
|
+
*
|
|
327
|
+
* Throws on an organization id a scope cannot carry, and on an unknown
|
|
328
|
+
* capability.
|
|
329
|
+
*/
|
|
330
|
+
function scopesForOrgCapabilities(organizationId, capabilities) {
|
|
331
|
+
if (organizationId.length === 0 || /\s/.test(organizationId)) throw new Error(`"${organizationId}" is not an organization id a scope can be narrowed to.`);
|
|
332
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const capability of capabilities) {
|
|
334
|
+
if (!isOrgCapability(capability)) throw new Error(`"${String(capability)}" is not an organization capability. Capabilities: ${ORG_CAPABILITY_NAMES.join(", ")}.`);
|
|
335
|
+
for (const scope of ORG_CAPABILITIES[capability].scopes(organizationId)) scopes.add(scope);
|
|
336
|
+
}
|
|
337
|
+
return [...scopes];
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Read a token's `org:*` scopes back as organization capabilities: the
|
|
341
|
+
* organizations they name, and, for each, the capabilities it holds every
|
|
342
|
+
* scope of.
|
|
343
|
+
*/
|
|
344
|
+
function orgCapabilitiesInScopes(scopes) {
|
|
345
|
+
const held = new Set(scopes);
|
|
346
|
+
const organizationIds = [];
|
|
347
|
+
for (const scope of scopes) {
|
|
348
|
+
const match = /^org:[a-z][a-z0-9-]*:(.+)$/.exec(scope);
|
|
349
|
+
if (match && !organizationIds.includes(match[1])) organizationIds.push(match[1]);
|
|
350
|
+
}
|
|
351
|
+
return organizationIds.map((organizationId) => ({
|
|
352
|
+
organizationId,
|
|
353
|
+
capabilities: ORG_CAPABILITY_NAMES.filter((capability) => ORG_CAPABILITIES[capability].scopes(organizationId).every((scope) => held.has(scope)))
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
169
357
|
//#region src/commands/cloud/context.ts
|
|
170
358
|
/**
|
|
171
359
|
* Shared foundation for the `rebase cloud` command family.
|
|
@@ -226,13 +414,123 @@ function readCredentials() {
|
|
|
226
414
|
return { contexts: {} };
|
|
227
415
|
}
|
|
228
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Replace the credentials file with `data`: written beside it, then renamed
|
|
419
|
+
* over it. Every command reads the whole file and writes it back, and several
|
|
420
|
+
* may run at once (MCP runs cloud tools concurrently). Written in place, a
|
|
421
|
+
* reader landing between the truncate and the write saw a half file, read it
|
|
422
|
+
* as empty, and wrote back one without the login and the grants; a disk-full
|
|
423
|
+
* error left it empty. A rename is atomic: a reader sees the old file or the
|
|
424
|
+
* new one.
|
|
425
|
+
*/
|
|
229
426
|
function writeCredentials(data) {
|
|
230
427
|
const file = credentialsPath();
|
|
231
428
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
232
|
-
|
|
429
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
233
430
|
try {
|
|
234
|
-
fs.
|
|
235
|
-
|
|
431
|
+
fs.writeFileSync(temporary, JSON.stringify(data, null, 2), { mode: 384 });
|
|
432
|
+
try {
|
|
433
|
+
fs.chmodSync(temporary, 384);
|
|
434
|
+
} catch {}
|
|
435
|
+
fs.renameSync(temporary, file);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
fs.rmSync(temporary, { force: true });
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Keep an unclaimed project's token and claim link for this host. */
|
|
442
|
+
function saveEphemeralGrant(url, projectId, grant) {
|
|
443
|
+
const creds = readCredentials();
|
|
444
|
+
const entry = creds.contexts[url] || {};
|
|
445
|
+
entry.ephemeral = {
|
|
446
|
+
...entry.ephemeral ?? {},
|
|
447
|
+
[projectId]: grant
|
|
448
|
+
};
|
|
449
|
+
creds.contexts[url] = entry;
|
|
450
|
+
writeCredentials(creds);
|
|
451
|
+
}
|
|
452
|
+
/** Drop a grant — once its project is claimed, or its token is refused. */
|
|
453
|
+
function forgetEphemeralGrant(url, projectId) {
|
|
454
|
+
const creds = readCredentials();
|
|
455
|
+
const entry = creds.contexts[url];
|
|
456
|
+
if (!entry?.ephemeral?.[projectId]) return;
|
|
457
|
+
delete entry.ephemeral[projectId];
|
|
458
|
+
writeCredentials(creds);
|
|
459
|
+
}
|
|
460
|
+
/** The grant for a project on a host, while it has not expired. */
|
|
461
|
+
function ephemeralGrantFor(url, projectId, now = Date.now()) {
|
|
462
|
+
const grant = readCredentials().contexts[url]?.ephemeral?.[projectId];
|
|
463
|
+
if (!grant || !(Date.parse(grant.expiresAt) > now)) return void 0;
|
|
464
|
+
return grant;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The grant as it is stored, expired or not: what a message about a refused
|
|
468
|
+
* token can still show (the claim link) after the token itself stops counting.
|
|
469
|
+
*/
|
|
470
|
+
function storedEphemeralGrant(url, projectId) {
|
|
471
|
+
return readCredentials().contexts[url]?.ephemeral?.[projectId];
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The grant for the project this directory is linked to, when the command
|
|
475
|
+
* targets the host it was provisioned on.
|
|
476
|
+
*/
|
|
477
|
+
function linkedEphemeralGrant(url) {
|
|
478
|
+
const link = readLink();
|
|
479
|
+
if (!link?.projectId || link.mode === "direct" || normalizeUrl(link.url) !== url) return void 0;
|
|
480
|
+
const grant = ephemeralGrantFor(url, link.projectId);
|
|
481
|
+
return grant ? {
|
|
482
|
+
projectId: link.projectId,
|
|
483
|
+
grant
|
|
484
|
+
} : void 0;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The unexpired grant for the project a link names, on the host it names: what
|
|
488
|
+
* `status`, `unlink` and `init` read the claim URL back from. Undefined for a
|
|
489
|
+
* claimed project (its grant is gone), a direct link, or no link.
|
|
490
|
+
*/
|
|
491
|
+
function grantForLink(link) {
|
|
492
|
+
if (!link?.projectId || link.mode === "direct") return void 0;
|
|
493
|
+
return ephemeralGrantFor(normalizeUrl(link.url), link.projectId);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* The grant a link names as it is stored, expired or not, with the host it is
|
|
497
|
+
* kept under: what `rebase cloud claim` reads, so it can say "expired" rather
|
|
498
|
+
* than "never had one".
|
|
499
|
+
*/
|
|
500
|
+
function storedGrantForLink(link) {
|
|
501
|
+
if (!link?.projectId || link.mode === "direct") return void 0;
|
|
502
|
+
const url = normalizeUrl(link.url);
|
|
503
|
+
const grant = storedEphemeralGrant(url, link.projectId);
|
|
504
|
+
return grant ? {
|
|
505
|
+
url,
|
|
506
|
+
grant
|
|
507
|
+
} : void 0;
|
|
508
|
+
}
|
|
509
|
+
/** Whether a `.env` sets a variable to something: an empty `KEY=` or `KEY=""` does not. */
|
|
510
|
+
function dotenvSets(file, key) {
|
|
511
|
+
if (!fs.existsSync(file)) return false;
|
|
512
|
+
const pattern = new RegExp(`^\\s*(export\\s+)?${key}\\s*=(.*)$`);
|
|
513
|
+
return fs.readFileSync(file, "utf-8").split("\n").some((l) => {
|
|
514
|
+
const value = pattern.exec(l)?.[2]?.trim();
|
|
515
|
+
return value !== void 0 && value !== "" && value !== "\"\"" && value !== "''";
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/** The variable `init --claim` writes the app's key to, and the MCP server and the SDK read. */
|
|
519
|
+
var APP_KEY_ENV = "REBASE_API_TOKEN";
|
|
520
|
+
/** Whether this project's `.env` (at its root, else here) holds an app key. */
|
|
521
|
+
function projectDotenvSetsAppKey() {
|
|
522
|
+
return dotenvSets(path.join(findProjectRoot() ?? process.cwd(), ".env"), APP_KEY_ENV);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* When the app key `init --claim` minted stops working, and what to do about
|
|
526
|
+
* it. It is minted to expire with the claim window; a claim hands it over to
|
|
527
|
+
* the person who claims (the control plane lifts its expiry), so an unclaimed
|
|
528
|
+
* project's key stops at the window's end and a claimed one's does not. The
|
|
529
|
+
* claimer replaces it — the agent holds a copy — with `rebase cloud app-key
|
|
530
|
+
* rotate`. Every command that knows the date says so.
|
|
531
|
+
*/
|
|
532
|
+
function appKeyExpiryNote(expiresAt) {
|
|
533
|
+
return `${APP_KEY_ENV} in .env stops working at ${new Date(expiresAt).toLocaleString()} unless the project is claimed first: claiming hands the key to the person who claims it, and it stops expiring. They can replace it with \`rebase cloud app-key rotate\`.`;
|
|
236
534
|
}
|
|
237
535
|
/** Host that a bare `rebase cloud` command should target, if any. */
|
|
238
536
|
function currentContextUrl() {
|
|
@@ -388,6 +686,16 @@ function envToken() {
|
|
|
388
686
|
}
|
|
389
687
|
/** The token each token-authenticated client was built with — see {@link freshAccessToken}. */
|
|
390
688
|
var STATIC_TOKENS = /* @__PURE__ */ new WeakMap();
|
|
689
|
+
/** The project each client built from an `init --claim` grant acts for — see {@link grantProjectOf}. */
|
|
690
|
+
var GRANT_CLIENTS = /* @__PURE__ */ new WeakMap();
|
|
691
|
+
/**
|
|
692
|
+
* The project whose `rebase cloud init --claim` token a client holds, or
|
|
693
|
+
* undefined for a session or `REBASE_TOKEN`: what `whoami` reports, since
|
|
694
|
+
* {@link requireClient} may pick the grant over `REBASE_TOKEN`.
|
|
695
|
+
*/
|
|
696
|
+
function grantProjectOf(client) {
|
|
697
|
+
return GRANT_CLIENTS.get(client);
|
|
698
|
+
}
|
|
391
699
|
/**
|
|
392
700
|
* A client that sends `token` as its bearer and nothing else.
|
|
393
701
|
*
|
|
@@ -397,11 +705,12 @@ var STATIC_TOKENS = /* @__PURE__ */ new WeakMap();
|
|
|
397
705
|
* rather than by the SDK's session refresh, because there is no session to
|
|
398
706
|
* refresh and "log in again" is the wrong remedy for a token.
|
|
399
707
|
*/
|
|
400
|
-
function createTokenClient(url, token) {
|
|
708
|
+
function createTokenClient(url, token, ephemeralProjectId) {
|
|
401
709
|
const client = createRebaseClient({
|
|
402
710
|
baseUrl: url,
|
|
403
711
|
realtime: false,
|
|
404
712
|
headers: { "User-Agent": cliUserAgent() },
|
|
713
|
+
fetch: deprecationAwareFetch,
|
|
405
714
|
token,
|
|
406
715
|
auth: {
|
|
407
716
|
storage: createMemoryStorage(),
|
|
@@ -409,10 +718,109 @@ function createTokenClient(url, token) {
|
|
|
409
718
|
autoRefresh: false
|
|
410
719
|
}
|
|
411
720
|
});
|
|
412
|
-
client.setOnUnauthorized(() => explainRefusedToken(url, token));
|
|
721
|
+
client.setOnUnauthorized(() => ephemeralProjectId ? explainRefusedGrant(url, ephemeralProjectId, token) : explainRefusedToken(url, token));
|
|
413
722
|
STATIC_TOKENS.set(client, token);
|
|
723
|
+
if (ephemeralProjectId) GRANT_CLIENTS.set(client, ephemeralProjectId);
|
|
414
724
|
return client;
|
|
415
725
|
}
|
|
726
|
+
/** The deprecation notices already printed, once each per process. */
|
|
727
|
+
var TOLD_DEPRECATED = /* @__PURE__ */ new Set();
|
|
728
|
+
/**
|
|
729
|
+
* Say, once, that the control plane marked a token's request as deprecated.
|
|
730
|
+
*
|
|
731
|
+
* The control plane answers a request it still serves, but will not in a later
|
|
732
|
+
* release, with `Deprecation: true` (RFC 9745), a `Link` with
|
|
733
|
+
* `rel="deprecation"` to the page that explains it, and an
|
|
734
|
+
* `X-Rebase-Deprecation` sentence saying what to do — a token from before
|
|
735
|
+
* `org:billing` reading billing is the first. CI logs are where that is seen,
|
|
736
|
+
* so it goes to stderr as a warning, once per notice per process, and never
|
|
737
|
+
* into the `--json` value on stdout.
|
|
738
|
+
*
|
|
739
|
+
* Exported so its test can hand it a response.
|
|
740
|
+
*/
|
|
741
|
+
function noticeDeprecation(response) {
|
|
742
|
+
if (response.headers?.get?.("Deprecation") == null) return;
|
|
743
|
+
const notice = response.headers.get("X-Rebase-Deprecation")?.trim() || "The control plane marked a request this command made as deprecated: it may stop working in a later release.";
|
|
744
|
+
if (TOLD_DEPRECATED.has(notice)) return;
|
|
745
|
+
TOLD_DEPRECATED.add(notice);
|
|
746
|
+
const link = /<([^>]+)>\s*;\s*rel="?deprecation"?/i.exec(response.headers.get("Link") ?? "")?.[1];
|
|
747
|
+
warn(notice, link ? `See ${link}` : void 0);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* `fetch` for a token client: the global one — read at call time, so a test's
|
|
751
|
+
* stand-in is used — with every answer shown to {@link noticeDeprecation}.
|
|
752
|
+
*/
|
|
753
|
+
var deprecationAwareFetch = async (input, init) => {
|
|
754
|
+
const response = await globalThis.fetch(input, init);
|
|
755
|
+
noticeDeprecation(response);
|
|
756
|
+
return response;
|
|
757
|
+
};
|
|
758
|
+
/** What `/api/auth/scopes` said each token holds, asked once per process. */
|
|
759
|
+
var TOKEN_SCOPES = /* @__PURE__ */ new Map();
|
|
760
|
+
/** The projects already told that `REBASE_TOKEN` was set aside for their own token. */
|
|
761
|
+
var TOLD_GRANT_PREFERRED = /* @__PURE__ */ new Set();
|
|
762
|
+
/**
|
|
763
|
+
* The scopes the control plane says a token holds — one `/api/auth/scopes`
|
|
764
|
+
* request per token and host for the life of the process — or undefined when
|
|
765
|
+
* it could not say: refused, unreachable, or an answer without `held`.
|
|
766
|
+
*/
|
|
767
|
+
function tokenScopes(url, token) {
|
|
768
|
+
const key = `${url}\n${token}`;
|
|
769
|
+
let pending = TOKEN_SCOPES.get(key);
|
|
770
|
+
if (!pending) {
|
|
771
|
+
pending = (async () => {
|
|
772
|
+
try {
|
|
773
|
+
const res = await fetch(`${url}/api/auth/scopes`, { headers: {
|
|
774
|
+
Authorization: `Bearer ${token}`,
|
|
775
|
+
"User-Agent": cliUserAgent()
|
|
776
|
+
} });
|
|
777
|
+
if (!res.ok) return void 0;
|
|
778
|
+
const body = await res.json();
|
|
779
|
+
const held = typeof body === "object" && body !== null ? body.held : void 0;
|
|
780
|
+
return Array.isArray(held) && held.every((s) => typeof s === "string") ? held : void 0;
|
|
781
|
+
} catch {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
})();
|
|
785
|
+
TOKEN_SCOPES.set(key, pending);
|
|
786
|
+
}
|
|
787
|
+
return pending;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Whether a command line names a project other than the linked one with
|
|
791
|
+
* `--project`: the grant acts only on its own project, so such a command
|
|
792
|
+
* keeps `REBASE_TOKEN` whatever it holds.
|
|
793
|
+
*/
|
|
794
|
+
function namesAnotherProject(rawArgs, projectId) {
|
|
795
|
+
const ref = arg({
|
|
796
|
+
"--project": String,
|
|
797
|
+
"-p": "--project"
|
|
798
|
+
}, {
|
|
799
|
+
argv: rawArgs.slice(2),
|
|
800
|
+
permissive: true
|
|
801
|
+
})["--project"];
|
|
802
|
+
if (!ref) return false;
|
|
803
|
+
const link = readLink();
|
|
804
|
+
return ref !== projectId && ref !== link?.slug;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Whether the control plane still accepts a token: one request to
|
|
808
|
+
* `/api/auth/scopes`, the route every token may read. `refused` only when it
|
|
809
|
+
* answered 401 or 403; `unknown` when it could not be asked, or answered
|
|
810
|
+
* something else — which proves nothing about the token.
|
|
811
|
+
*/
|
|
812
|
+
async function probeToken(url, token) {
|
|
813
|
+
try {
|
|
814
|
+
const probe = await fetch(`${url}/api/auth/scopes`, { headers: {
|
|
815
|
+
Authorization: `Bearer ${token}`,
|
|
816
|
+
"User-Agent": cliUserAgent()
|
|
817
|
+
} });
|
|
818
|
+
if (probe.ok) return "accepted";
|
|
819
|
+
return probe.status === 401 || probe.status === 403 ? "refused" : "unknown";
|
|
820
|
+
} catch {
|
|
821
|
+
return "unknown";
|
|
822
|
+
}
|
|
823
|
+
}
|
|
416
824
|
/**
|
|
417
825
|
* Why the control plane answered a token-authenticated request with 401, and
|
|
418
826
|
* what to do about it. Never returns.
|
|
@@ -425,14 +833,7 @@ function createTokenClient(url, token) {
|
|
|
425
833
|
* neither is answered with the other's remedy.
|
|
426
834
|
*/
|
|
427
835
|
async function explainRefusedToken(url, token) {
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
tokenAccepted = (await fetch(`${url}/api/auth/scopes`, { headers: {
|
|
431
|
-
Authorization: `Bearer ${token}`,
|
|
432
|
-
"User-Agent": cliUserAgent()
|
|
433
|
-
} })).ok;
|
|
434
|
-
} catch {}
|
|
435
|
-
if (tokenAccepted) fail(`This command needs a signed-in session, and ${TOKEN_ENV} is an API token.`, `Unset ${TOKEN_ENV} and run ${chalk.bold("rebase cloud login")} to run it.`, "session_required");
|
|
836
|
+
if (await probeToken(url, token) === "accepted") fail(`This command needs a signed-in session, and ${TOKEN_ENV} is an API token.`, `Unset ${TOKEN_ENV} and run ${chalk.bold("rebase cloud login")} to run it.`, "session_required");
|
|
436
837
|
fail(`${TOKEN_ENV} was rejected by ${url}: it has expired, been revoked, or was not issued by this control plane.`, `Create a new one with ${chalk.bold("rebase cloud tokens create")} and put it in ${TOKEN_ENV}.`, "token_rejected");
|
|
437
838
|
}
|
|
438
839
|
/** Two minutes of head-room before a token is treated as expired. */
|
|
@@ -459,17 +860,71 @@ async function freshAccessToken(client) {
|
|
|
459
860
|
return (await client.auth.refreshSession()).accessToken;
|
|
460
861
|
}
|
|
461
862
|
/**
|
|
863
|
+
* A request made with an unclaimed project's token was refused. Never returns.
|
|
864
|
+
*
|
|
865
|
+
* Two causes reach that 401, as for {@link explainRefusedToken}. The project was
|
|
866
|
+
* claimed (which revokes the token) or expired, and the token is dead. Or the
|
|
867
|
+
* command called a route that takes only a person's session — `whoami` and
|
|
868
|
+
* `projects create` read `/auth/me` — which refuses a perfectly good token too.
|
|
869
|
+
* The grant holds the project's only token and the only stored copy of its
|
|
870
|
+
* claim link, so it is forgotten only once `/api/auth/scopes` refuses the token
|
|
871
|
+
* as well; and even then the claim link is printed before it goes.
|
|
872
|
+
*/
|
|
873
|
+
async function explainRefusedGrant(url, projectId, token) {
|
|
874
|
+
const verdict = await probeToken(url, token);
|
|
875
|
+
if (verdict === "accepted") fail(`This command needs a signed-in session, and this directory uses the token \`rebase cloud init --claim\` stored for project ${projectId}.`, `That token still deploys, reads logs and metrics, sets environment variables and runs \`db info\`, but reads no secret back (no variable's value, no database password) and cannot open \`db connect\` or a preview; ${chalk.bold("rebase cloud whoami")} shows it, the claim URL and when it expires. Run this command as a person once the project is claimed: ${chalk.bold("rebase cloud login")}.`, "session_required");
|
|
876
|
+
if (verdict === "unknown") fail(`The token \`rebase cloud init --claim\` stored for project ${projectId} was refused by ${url}, and the control plane could not say why.`, `The token and the claim URL are kept. Check your connection and run the command again; ${chalk.bold("rebase cloud whoami")} shows the claim URL.`, "token_rejected");
|
|
877
|
+
const grant = storedEphemeralGrant(url, projectId);
|
|
878
|
+
forgetEphemeralGrant(url, projectId);
|
|
879
|
+
const keyNote = grant && projectDotenvSetsAppKey() ? ` ${appKeyExpiryNote(grant.expiresAt)}` : "";
|
|
880
|
+
if (readCredentials().contexts[url]?.auth) fail(`The token \`rebase cloud init --claim\` stored for project ${projectId} was refused by ${url}: the project was claimed, or it expired.`, `It is forgotten, and you are signed in to ${url}: run the command again and it uses your login.${keyNote}`, "token_rejected");
|
|
881
|
+
const claim = grant ? ` If nobody has claimed it yet, its claim URL was ${chalk.bold(grant.claimUrl)} (expires ${new Date(grant.expiresAt).toLocaleString()}).` + keyNote : "";
|
|
882
|
+
fail(`The token \`rebase cloud init --claim\` stored for project ${projectId} was refused by ${url}.`, `Once a project is claimed its agent token stops working: sign in with ${chalk.bold("rebase cloud login")} as the account that claimed it, or create a token there with ${chalk.bold("rebase cloud tokens create")}.${claim}`, "token_rejected");
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
462
885
|
* Return an authenticated client for the resolved host.
|
|
463
886
|
*
|
|
464
887
|
* With `REBASE_TOKEN` set, that token is the credential for every request and
|
|
465
|
-
* the stored login is not consulted at all
|
|
466
|
-
*
|
|
888
|
+
* the stored login is not consulted at all — except in a directory linked to a
|
|
889
|
+
* project `init --claim` provisioned, when `REBASE_TOKEN` holds no scope on that
|
|
890
|
+
* project (one `/api/auth/scopes` call): the project's own token acts there
|
|
891
|
+
* instead, and the command says so once. Next, a directory linked to a
|
|
892
|
+
* project `rebase cloud init --claim` provisioned uses the token stored for it
|
|
893
|
+
* (see {@link EphemeralGrant}) until it expires: nobody can sign in as that
|
|
894
|
+
* project's owner, so a login could not reach it anyway. Otherwise it is the
|
|
895
|
+
* `cloud login` session — see {@link requireSessionClient}.
|
|
467
896
|
*/
|
|
468
897
|
async function requireClient(rawArgs) {
|
|
469
898
|
const token = envToken();
|
|
470
|
-
if (!token)
|
|
899
|
+
if (!token) {
|
|
900
|
+
const linked = linkedEphemeralGrant(resolveCloudUrl(rawArgs));
|
|
901
|
+
if (!linked) return requireSessionClient(rawArgs);
|
|
902
|
+
refuseDirectLink(rawArgs);
|
|
903
|
+
const url = resolveCloudUrl(rawArgs);
|
|
904
|
+
return {
|
|
905
|
+
client: createTokenClient(url, linked.grant.token, linked.projectId),
|
|
906
|
+
url
|
|
907
|
+
};
|
|
908
|
+
}
|
|
471
909
|
refuseDirectLink(rawArgs);
|
|
472
910
|
const url = resolveCloudUrl(rawArgs);
|
|
911
|
+
const displaced = linkedEphemeralGrant(url);
|
|
912
|
+
if (displaced) {
|
|
913
|
+
if (!namesAnotherProject(rawArgs, displaced.projectId)) {
|
|
914
|
+
const held = await tokenScopes(url, token);
|
|
915
|
+
if (held && !scopesReachProject(held, displaced.projectId)) {
|
|
916
|
+
if (!TOLD_GRANT_PREFERRED.has(displaced.projectId)) {
|
|
917
|
+
TOLD_GRANT_PREFERRED.add(displaced.projectId);
|
|
918
|
+
warn(`${TOKEN_ENV} holds no scope on project ${displaced.projectId}, so the token \`rebase cloud init --claim\` stored for it is used instead.`, `Pass --project to act on another project with ${TOKEN_ENV}, or give ${TOKEN_ENV} a capability on this one.`);
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
client: createTokenClient(url, displaced.grant.token, displaced.projectId),
|
|
922
|
+
url
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
warn(`${TOKEN_ENV} is set, so it is used instead of the token \`rebase cloud init --claim\` stored for project ${displaced.projectId}.`, `Unset ${TOKEN_ENV} to act on that project with its own token.`);
|
|
927
|
+
}
|
|
473
928
|
return {
|
|
474
929
|
client: createTokenClient(url, token),
|
|
475
930
|
url
|
|
@@ -483,15 +938,17 @@ async function requireClient(rawArgs) {
|
|
|
483
938
|
*
|
|
484
939
|
* For the commands a token cannot run — minting and revoking tokens: a key that
|
|
485
940
|
* could mint keys could mint its own successor, so the control plane refuses
|
|
486
|
-
* every key on those routes.
|
|
941
|
+
* every key on those routes. Inviting people is the other: it hands out access
|
|
942
|
+
* to every project in an organization, which no token capability grants.
|
|
943
|
+
* `action` names what the refusal says a token cannot do.
|
|
487
944
|
*/
|
|
488
|
-
async function requireSessionClient(rawArgs) {
|
|
945
|
+
async function requireSessionClient(rawArgs, action = "manage tokens") {
|
|
489
946
|
refuseDirectLink(rawArgs);
|
|
490
947
|
const url = resolveCloudUrl(rawArgs);
|
|
491
948
|
const client = createCloudClient(url);
|
|
492
949
|
const session = client.auth.getSession();
|
|
493
950
|
if (!session || !session.accessToken) {
|
|
494
|
-
if (envToken()) fail(`This command needs a signed-in session for ${chalk.cyan(url)}, and ${TOKEN_ENV} is an API token.`, `A token cannot
|
|
951
|
+
if (envToken()) fail(`This command needs a signed-in session for ${chalk.cyan(url)}, and ${TOKEN_ENV} is an API token.`, `A token cannot ${action}. Run ${chalk.bold("rebase cloud login")} first.`, "session_required");
|
|
495
952
|
fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first.`, "not_logged_in");
|
|
496
953
|
}
|
|
497
954
|
if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
|
|
@@ -701,12 +1158,12 @@ var JSON_MODE = false;
|
|
|
701
1158
|
* `cloudCommand`, before anything can print or `fail`. Returns the resolved mode
|
|
702
1159
|
* (handy for tests, which otherwise leave it at its `false` default).
|
|
703
1160
|
*/
|
|
704
|
-
function initOutputMode(rawArgs) {
|
|
1161
|
+
function initOutputMode(rawArgs, opts = {}) {
|
|
705
1162
|
if (arg({ "--json": Boolean }, {
|
|
706
1163
|
argv: rawArgs.slice(2),
|
|
707
1164
|
permissive: true
|
|
708
1165
|
})["--json"]) JSON_MODE = true;
|
|
709
|
-
else JSON_MODE = parseEnvBoolean(process.env.REBASE_JSON) ?? process.stdout.isTTY !== true;
|
|
1166
|
+
else JSON_MODE = parseEnvBoolean(process.env.REBASE_JSON) ?? (opts.whenPiped === "human" ? false : process.stdout.isTTY !== true);
|
|
710
1167
|
return JSON_MODE;
|
|
711
1168
|
}
|
|
712
1169
|
/** Whether the current invocation is emitting machine-readable JSON. */
|
|
@@ -1228,6 +1685,6 @@ function requireCloudRow(row, what) {
|
|
|
1228
1685
|
return parsed;
|
|
1229
1686
|
}
|
|
1230
1687
|
//#endregion
|
|
1231
|
-
export {
|
|
1688
|
+
export { resolveCloudUrl as $, lookupProjectId as A, projectLinkPath as B, grantForLink as C, isKnownControlPlane as D, isJsonMode as E, printGroupHelp as F, requireClient as G, refuseDirectLink as H, printJson as I, requireKnownAction as J, requireCloudRow as K, probeToken as L, noteBlank as M, openUrl as N, keyValues as O, parseCloudArgs as P, resolveCloudTarget as Q, projectDotenvSetsAppKey as R, getContextOrg as S, initOutputMode as T, removeLink as U, readLink as V, reportError as W, requireProjectRef as X, requireProject as Y, requireSessionClient as Z, fail as _, orgCapabilitiesInScopes as _t, billingAccountIdOf as a, storedGrantForLink as at, forgetEphemeralGrant as b, wantsRawError as bt, colorStatus as c, writeLink as ct, displayProjectRef as d, ORG_CAPABILITY_NAMES as dt, resolveProjectRef as et, dotenvSets as f, TOKEN_CAPABILITIES as ft, ephemeralGrantFor as g, isTokenCapability as gt, envToken as h, isOrgCapability as ht, appKeyExpiryNote as i, setCurrentContext as it, note as j, linkedEphemeralGrant as k, confirmDestructive as l, ALWAYS_SCOPES as lt, emitHelp as m, capabilitiesInScopes as mt, GLOBAL_HELP_FLAGS as n, saveEphemeralGrant as nt, cloudRow as o, success as ot, emit as p, TOKEN_CAPABILITY_NAMES as pt, requireInteractive as q, TOKEN_ENV as r, setContextOrg as rt, cloudRows as s, warn as st, GLOBAL_CLOUD_FLAGS as t, resolveTimeoutMs as tt, createCloudClient as u, ORG_CAPABILITIES as ut, fetchDeployTargets as v, scopesForCapabilities as vt, grantProjectOf as w, freshAccessToken as x, fetchTenantBaseDomain as y, scopesForOrgCapabilities as yt, projectHost as z };
|
|
1232
1689
|
|
|
1233
|
-
//# sourceMappingURL=context-
|
|
1690
|
+
//# sourceMappingURL=context-GUxkZm-S.js.map
|