@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.
Files changed (151) hide show
  1. package/README.md +2 -1
  2. package/bin/rebase.js +143 -4
  3. package/dist/{agent-setup-BYb2Tbdz.js → agent-setup-D9mHRZ5R.js} +125 -3
  4. package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
  5. package/dist/{api-keys-CSMbsmAn.js → api-keys-CyJ26KEJ.js} +41 -9
  6. package/dist/api-keys-CyJ26KEJ.js.map +1 -0
  7. package/dist/{apps-iBXNZhVN.js → apps-CnLDV5b6.js} +6 -6
  8. package/dist/{apps-iBXNZhVN.js.map → apps-CnLDV5b6.js.map} +1 -1
  9. package/dist/{atlas-dev-database-BusbHP6n.js → atlas-dev-database-BODqdCWt.js} +2 -2
  10. package/dist/{atlas-dev-database-BusbHP6n.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
  11. package/dist/{auth-DRsTWPky.js → auth-dII2jCO1.js} +10 -7
  12. package/dist/auth-dII2jCO1.js.map +1 -0
  13. package/dist/{branch-pointer-Bn_FjNld.js → branch-pointer-BQUF6lYa.js} +1 -1
  14. package/dist/{build-et7Rccxi.js → build-Cza7ZQrp.js} +6 -6
  15. package/dist/{build-et7Rccxi.js.map → build-Cza7ZQrp.js.map} +1 -1
  16. package/dist/{bundle-DlqkDYSe.js → bundle-C6yEtMJ-.js} +53 -5
  17. package/dist/bundle-C6yEtMJ-.js.map +1 -0
  18. package/dist/bundle.d.ts +14 -0
  19. package/dist/{cloud-B-ZIjHrA.js → cloud-BdnmqQaZ.js} +8527 -4340
  20. package/dist/cloud-BdnmqQaZ.js.map +1 -0
  21. package/dist/collections-dir-DlvDhime.js +69 -0
  22. package/dist/collections-dir-DlvDhime.js.map +1 -0
  23. package/dist/commands/agent-setup.d.ts +58 -0
  24. package/dist/commands/api-keys.d.ts +13 -0
  25. package/dist/commands/cloud/activity.d.ts +25 -0
  26. package/dist/commands/cloud/app-key.d.ts +18 -0
  27. package/dist/commands/cloud/auth.d.ts +1 -0
  28. package/dist/commands/cloud/billing-invoices.d.ts +39 -0
  29. package/dist/commands/cloud/claim.d.ts +7 -0
  30. package/dist/commands/cloud/context.d.ts +115 -6
  31. package/dist/commands/cloud/deploy-summary.d.ts +162 -0
  32. package/dist/commands/cloud/deploy.d.ts +64 -0
  33. package/dist/commands/cloud/env.d.ts +24 -0
  34. package/dist/commands/cloud/index.d.ts +13 -1
  35. package/dist/commands/cloud/init.d.ts +143 -0
  36. package/dist/commands/cloud/login-flow.d.ts +51 -0
  37. package/dist/commands/cloud/notifications.d.ts +28 -0
  38. package/dist/commands/cloud/orgs.d.ts +13 -0
  39. package/dist/commands/cloud/previews.d.ts +123 -0
  40. package/dist/commands/cloud/resources.d.ts +68 -14
  41. package/dist/commands/cloud/runtime-logs.d.ts +176 -0
  42. package/dist/commands/cloud/service-key.d.ts +15 -0
  43. package/dist/commands/cloud/token-capabilities.d.ts +61 -8
  44. package/dist/commands/cloud/tokens.d.ts +18 -3
  45. package/dist/commands/cloud/usage.d.ts +63 -0
  46. package/dist/commands/dev.d.ts +10 -0
  47. package/dist/commands/doctor.d.ts +1 -1
  48. package/dist/commands/generate-view.d.ts +94 -0
  49. package/dist/commands/generate_sdk.d.ts +15 -0
  50. package/dist/commands/skills.d.ts +4 -3
  51. package/dist/commands/storage.d.ts +4 -0
  52. package/dist/commands/verify.d.ts +96 -0
  53. package/dist/{consent-BaB-eQKG.js → consent-BD7F29T3.js} +2 -2
  54. package/dist/{consent-BaB-eQKG.js.map → consent-BD7F29T3.js.map} +1 -1
  55. package/dist/{console-BlD9kjFW.js → console-BXB6-8GT.js} +12 -6
  56. package/dist/console-BXB6-8GT.js.map +1 -0
  57. package/dist/{context-DHiiiJ5C.js → context-GUxkZm-S.js} +481 -24
  58. package/dist/context-GUxkZm-S.js.map +1 -0
  59. package/dist/{daemon-DDMRbz4T.js → daemon-Cp7oNXth.js} +2 -2
  60. package/dist/{daemon-DDMRbz4T.js.map → daemon-Cp7oNXth.js.map} +1 -1
  61. package/dist/{db-9P6MnmdC.js → db-KzlqddGD.js} +37 -30
  62. package/dist/db-KzlqddGD.js.map +1 -0
  63. package/dist/{derive-MTdClnkc.js → derive-ae9UOoOB.js} +2 -2
  64. package/dist/{derive-MTdClnkc.js.map → derive-ae9UOoOB.js.map} +1 -1
  65. package/dist/{dev-BDN_OKXA.js → dev-DTqVMgB3.js} +59 -32
  66. package/dist/dev-DTqVMgB3.js.map +1 -0
  67. package/dist/{dev-preflight-FT9GbJVj.js → dev-preflight-Cxv3TiD1.js} +2 -2
  68. package/dist/{dev-preflight-FT9GbJVj.js.map → dev-preflight-Cxv3TiD1.js.map} +1 -1
  69. package/dist/{dev-server-CIZwAfZa.js → dev-server-OJsUGZEB.js} +2 -2
  70. package/dist/{dev-server-CIZwAfZa.js.map → dev-server-OJsUGZEB.js.map} +1 -1
  71. package/dist/{doctor-D8YXz7df.js → doctor-BRWOlqeG.js} +13 -11
  72. package/dist/doctor-BRWOlqeG.js.map +1 -0
  73. package/dist/{eject-izG2KjMo.js → eject-DABcDnPN.js} +3 -3
  74. package/dist/{eject-izG2KjMo.js.map → eject-DABcDnPN.js.map} +1 -1
  75. package/dist/{fold-static-CybSay_W.js → fold-static-BnAK-anQ.js} +3 -3
  76. package/dist/{fold-static-CybSay_W.js.map → fold-static-BnAK-anQ.js.map} +1 -1
  77. package/dist/generate-view-IFwCADvb.js +662 -0
  78. package/dist/generate-view-IFwCADvb.js.map +1 -0
  79. package/dist/{generate_sdk-B4R65oUV.js → generate_sdk-BvcWvoUh.js} +33 -7
  80. package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
  81. package/dist/index.es.js +57 -38
  82. package/dist/index.es.js.map +1 -1
  83. package/dist/{init-DYaeppEN.js → init-CaU6GhwI.js} +8 -8
  84. package/dist/{init-DYaeppEN.js.map → init-CaU6GhwI.js.map} +1 -1
  85. package/dist/last-dev-database-DpW5dfoX.js +2 -0
  86. package/dist/{manifest-nvPdLlJY.js → manifest-BtzDy7KQ.js} +36 -3
  87. package/dist/{manifest-nvPdLlJY.js.map → manifest-BtzDy7KQ.js.map} +1 -1
  88. package/dist/{normalize-imports-BYw8ad87.js → normalize-imports-D0zXlM6g.js} +3 -3
  89. package/dist/{normalize-imports-BYw8ad87.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
  90. package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
  91. package/dist/path-flags-DNvtEi2-.js.map +1 -0
  92. package/dist/{prepare-1rVNd2PF.js → prepare-CTw9NEkh.js} +4 -4
  93. package/dist/{prepare-1rVNd2PF.js.map → prepare-CTw9NEkh.js.map} +1 -1
  94. package/dist/{project-g4K6kV1t.js → project-CasK56vN.js} +2 -2
  95. package/dist/{project-g4K6kV1t.js.map → project-CasK56vN.js.map} +1 -1
  96. package/dist/resolve-D647F1LA.js +2 -0
  97. package/dist/{resources-CovP8AZ-.js → resources-CPSC5Kcu.js} +4 -4
  98. package/dist/{resources-CovP8AZ-.js.map → resources-CPSC5Kcu.js.map} +1 -1
  99. package/dist/{schema-nuxvvc50.js → schema-BSXwZSiI.js} +9 -7
  100. package/dist/schema-BSXwZSiI.js.map +1 -0
  101. package/dist/{skills-DeUIsZcT.js → skills-BU2flMOy.js} +52 -7
  102. package/dist/skills-BU2flMOy.js.map +1 -0
  103. package/dist/{start-RzZqDroW.js → start-mVb6upwY.js} +5 -5
  104. package/dist/{start-RzZqDroW.js.map → start-mVb6upwY.js.map} +1 -1
  105. package/dist/static-syntax-check.d.ts +64 -0
  106. package/dist/{status-D9Zm88NZ.js → status-BREvtzJm.js} +7 -7
  107. package/dist/{status-D9Zm88NZ.js.map → status-BREvtzJm.js.map} +1 -1
  108. package/dist/{storage-BB3umRfw.js → storage-DtOXGflp.js} +159 -15
  109. package/dist/storage-DtOXGflp.js.map +1 -0
  110. package/dist/{telemetry-HfU5uAh1.js → telemetry-CgwJPVMO.js} +2 -2
  111. package/dist/{telemetry-HfU5uAh1.js.map → telemetry-CgwJPVMO.js.map} +1 -1
  112. package/dist/{telemetry-TseNLk6X.js → telemetry-DwK5RQJl.js} +4 -4
  113. package/dist/{telemetry-TseNLk6X.js.map → telemetry-DwK5RQJl.js.map} +1 -1
  114. package/dist/{upgrade-apiPKKE_.js → upgrade-B7sVwL8S.js} +4 -4
  115. package/dist/{upgrade-apiPKKE_.js.map → upgrade-B7sVwL8S.js.map} +1 -1
  116. package/dist/utils/collections-dir.d.ts +14 -0
  117. package/dist/utils/path-flags.d.ts +2 -1
  118. package/dist/utils/tool-stdio.d.ts +1 -1
  119. package/dist/verify-D7UsV_qe.js +730 -0
  120. package/dist/verify-D7UsV_qe.js.map +1 -0
  121. package/package.json +9 -7
  122. package/templates/eject/backend/src/index.ts +9 -1
  123. package/templates/generated-schemas/ecommerce.ts +3 -0
  124. package/templates/overlays/baas/ai-instructions.md +13 -0
  125. package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
  126. package/templates/template/.claude/settings.json +17 -0
  127. package/templates/template/.env.example +10 -1
  128. package/templates/template/ai-instructions.md +21 -1
  129. package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
  130. package/templates/template/docker-compose.yml +4 -1
  131. package/templates/template/frontend/package.json +1 -0
  132. package/templates/views/AppView.tsx.tmpl +138 -0
  133. package/templates/views/EntityView.tsx.tmpl +64 -0
  134. package/templates/views/index.ts.tmpl +8 -0
  135. package/dist/agent-setup-BYb2Tbdz.js.map +0 -1
  136. package/dist/api-keys-CSMbsmAn.js.map +0 -1
  137. package/dist/auth-DRsTWPky.js.map +0 -1
  138. package/dist/bundle-DlqkDYSe.js.map +0 -1
  139. package/dist/cloud-B-ZIjHrA.js.map +0 -1
  140. package/dist/console-BlD9kjFW.js.map +0 -1
  141. package/dist/context-DHiiiJ5C.js.map +0 -1
  142. package/dist/db-9P6MnmdC.js.map +0 -1
  143. package/dist/dev-BDN_OKXA.js.map +0 -1
  144. package/dist/doctor-D8YXz7df.js.map +0 -1
  145. package/dist/generate_sdk-B4R65oUV.js.map +0 -1
  146. package/dist/last-dev-database-B_75fxZC.js +0 -2
  147. package/dist/path-flags-BFgG9V6y.js.map +0 -1
  148. package/dist/resolve-BQUAC3Wi.js +0 -2
  149. package/dist/schema-nuxvvc50.js.map +0 -1
  150. package/dist/skills-DeUIsZcT.js.map +0 -1
  151. package/dist/storage-BB3umRfw.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { S as switchOn, b as parseCommandArgs, c as findProjectRoot } from "./project-g4K6kV1t.js";
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
- fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 384 });
429
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
233
430
  try {
234
- fs.chmodSync(file, 384);
235
- } catch {}
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
- let tokenAccepted = false;
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. Otherwise it is the `cloud login`
466
- * session — see {@link requireSessionClient}.
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) return requireSessionClient(rawArgs);
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 manage tokens. Run ${chalk.bold("rebase cloud login")} first.`, "session_required");
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 { projectHost as A, requireProject as B, lookupProjectId as C, parseCloudArgs as D, openUrl as E, reportError as F, resolveProjectRef as G, requireSessionClient as H, requireClient as I, setCurrentContext as J, resolveTimeoutMs as K, requireCloudRow as L, readLink as M, refuseDirectLink as N, printGroupHelp as O, removeLink as P, wantsRawError as Q, requireInteractive as R, keyValues as S, noteBlank as T, resolveCloudTarget as U, requireProjectRef as V, resolveCloudUrl as W, warn as X, success as Y, writeLink as Z, freshAccessToken as _, cloudRow as a, isJsonMode as b, confirmDestructive as c, emit as d, emitHelp as f, fetchTenantBaseDomain as g, fetchDeployTargets as h, billingAccountIdOf as i, projectLinkPath as j, printJson as k, createCloudClient as l, fail as m, GLOBAL_HELP_FLAGS as n, cloudRows as o, envToken as p, setContextOrg as q, TOKEN_ENV as r, colorStatus as s, GLOBAL_CLOUD_FLAGS as t, displayProjectRef as u, getContextOrg as v, note as w, isKnownControlPlane as x, initOutputMode as y, requireKnownAction as z };
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-DHiiiJ5C.js.map
1690
+ //# sourceMappingURL=context-GUxkZm-S.js.map