@lukoweb/apitogo 0.2.30 → 0.2.31

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/dist/cli/cli.js CHANGED
@@ -7092,7 +7092,7 @@ function identityRules(config2, resolved, out) {
7092
7092
  );
7093
7093
  }
7094
7094
  }
7095
- for (const [list2, label] of [
7095
+ for (const [list2, label2] of [
7096
7096
  [identity.connections, "connections"],
7097
7097
  [identity.clients, "clients"]
7098
7098
  ]) {
@@ -7101,8 +7101,8 @@ function identityRules(config2, resolved, out) {
7101
7101
  if (seen.has(item.id)) {
7102
7102
  out.error({
7103
7103
  code: "IDENTITY_DUPLICATE_ID",
7104
- path: `identity.${label}[${index}].id`,
7105
- message: `the id "${item.id}" is used more than once in identity.${label}.`
7104
+ path: `identity.${label2}[${index}].id`,
7105
+ message: `the id "${item.id}" is used more than once in identity.${label2}.`
7106
7106
  });
7107
7107
  }
7108
7108
  seen.add(item.id);
@@ -9133,7 +9133,9 @@ var PlatformApiClient = class {
9133
9133
  headers: { "Content-Type": "application/json" },
9134
9134
  body: JSON.stringify({
9135
9135
  planSlug: body.planSlug,
9136
- currency: body.currency ?? null
9136
+ currency: body.currency ?? null,
9137
+ // Only a platform admin may skip the payment; the platform refuses it for anyone else.
9138
+ ...body.skipPayment ? { skipPayment: true } : {}
9137
9139
  })
9138
9140
  }
9139
9141
  );
@@ -9431,6 +9433,20 @@ var PlatformApiClient = class {
9431
9433
  );
9432
9434
  return result ?? null;
9433
9435
  }
9436
+ /**
9437
+ * Gives the plans backend the tenant's own Stripe settings. `apiKey` and `webhookSecret` are `secret:NAME`
9438
+ * references to project secrets, never values. Queues a job that updates the running backend.
9439
+ */
9440
+ putDevPortalBilling(projectId, body) {
9441
+ return this.request(
9442
+ "PUT",
9443
+ `/api/v1/projects/${projectId}/dev-portal/billing`,
9444
+ {
9445
+ headers: { "Content-Type": "application/json" },
9446
+ body: JSON.stringify(body)
9447
+ }
9448
+ );
9449
+ }
9434
9450
  /** Applies the project's identity to its plans backend as the sign-in provider. Queues a job. */
9435
9451
  linkDevPortalIdentity(projectId) {
9436
9452
  return this.request(
@@ -9980,7 +9996,7 @@ import {
9980
9996
  // package.json
9981
9997
  var package_default = {
9982
9998
  name: "@lukoweb/apitogo",
9983
- version: "0.2.30",
9999
+ version: "0.2.31",
9984
10000
  type: "module",
9985
10001
  sideEffects: [
9986
10002
  "**/*.css",
@@ -12090,8 +12106,8 @@ var extractRichH1 = (content) => {
12090
12106
  if (!h1) return void 0;
12091
12107
  const children = h1.children;
12092
12108
  const hasJsx = children.some(isMdxJsxElement);
12093
- const label = mdastToString(h1).trim();
12094
- return hasJsx ? { label, rich: children } : { label };
12109
+ const label2 = mdastToString(h1).trim();
12110
+ return hasJsx ? { label: label2, rich: children } : { label: label2 };
12095
12111
  } catch {
12096
12112
  return void 0;
12097
12113
  }
@@ -12152,12 +12168,12 @@ var NavigationResolver = class {
12152
12168
  return void 0;
12153
12169
  }
12154
12170
  const richH1 = extractRichH1(content);
12155
- const label = data.navigation_label ?? data.sidebar_label ?? data.title ?? richH1?.label ?? extractTitleFromContent(content) ?? filePath;
12171
+ const label2 = data.navigation_label ?? data.sidebar_label ?? data.title ?? richH1?.label ?? extractTitleFromContent(content) ?? filePath;
12156
12172
  const icon = data.navigation_icon ?? data.sidebar_icon;
12157
12173
  const doc = {
12158
12174
  type: "doc",
12159
12175
  file: filePath,
12160
- label,
12176
+ label: label2,
12161
12177
  icon,
12162
12178
  display: data.navigation_display,
12163
12179
  categoryLabel,
@@ -12543,8 +12559,8 @@ var viteApiPlugin = async () => {
12543
12559
  return acc;
12544
12560
  }, /* @__PURE__ */ new Map());
12545
12561
  const categoryList = Array.from(categories.entries()).map(
12546
- ([label, tags]) => ({
12547
- label,
12562
+ ([label2, tags]) => ({
12563
+ label: label2,
12548
12564
  tags: Array.from(tags)
12549
12565
  })
12550
12566
  );
@@ -15137,14 +15153,14 @@ async function readPlansFile(file) {
15137
15153
  return parsed.data.plans;
15138
15154
  }
15139
15155
  var BILLING_PERIOD = { month: "monthly", year: "annual" };
15140
- function toPlatformPlans(plans) {
15156
+ function toPlatformPlans(plans, options = {}) {
15141
15157
  return plans.map((plan2, index) => {
15142
15158
  const paid = plan2.pricingModel !== "custom" && (plan2.priceAmount ?? 0) > 0;
15143
- if (paid)
15159
+ if (paid && !options.billing)
15144
15160
  throw new PlansFileError(
15145
15161
  "PLANS_PAID_UNSUPPORTED",
15146
- `The plan "${plan2.sku}" has a price, and deploying paid plans is not available in this CLI version yet.`,
15147
- "Remove the plan, or set priceAmount to 0 (or pricingModel to custom for a contact-us plan)."
15162
+ `The plan "${plan2.sku}" has a price, and plans.billing.provider is not stripe, so nobody could pay for it.`,
15163
+ "Set plans.billing.provider to stripe (with plans.billing.apiKey), or set priceAmount to 0 (or pricingModel to custom for a contact-us plan)."
15148
15164
  );
15149
15165
  const limits = plan2.limits ? {
15150
15166
  ...plan2.limits.rateLimit ? {
@@ -15233,7 +15249,11 @@ async function plansFileProblem(resolved) {
15233
15249
  const plans = resolved.plans;
15234
15250
  if (!plans?.enabled) return { wanted: [] };
15235
15251
  try {
15236
- return { wanted: toPlatformPlans(await readPlansFile(plans.file)) };
15252
+ return {
15253
+ wanted: toPlatformPlans(await readPlansFile(plans.file), {
15254
+ billing: plans.billing.provider === "stripe"
15255
+ })
15256
+ };
15237
15257
  } catch (error) {
15238
15258
  if (!(error instanceof PlansFileError)) throw error;
15239
15259
  return {
@@ -15483,7 +15503,7 @@ async function runCheckV2(dir, args = {}, deps = {}) {
15483
15503
  ...report
15484
15504
  };
15485
15505
  }
15486
- var line = (label, issue4) => `${label}: ${formatConfigIssue(issue4)}`;
15506
+ var line = (label2, issue4) => `${label2}: ${formatConfigIssue(issue4)}`;
15487
15507
  function formatCheckV2(output4) {
15488
15508
  const lines = [
15489
15509
  ...output4.errors.map((issue4) => line("error", issue4)),
@@ -17126,11 +17146,11 @@ function buildThinWrapperWithCustomPages(tsxRoutePaths) {
17126
17146
  const customPages = tsxRoutePaths.map((routePath) => {
17127
17147
  const slug = routePath.replace(/^\//, "");
17128
17148
  const name = pageComponentName(slug);
17129
- const label = labelFromRoutePath(routePath);
17149
+ const label2 = labelFromRoutePath(routePath);
17130
17150
  return ` {
17131
17151
  type: "custom-page",
17132
17152
  path: ${JSON.stringify(routePath)},
17133
- label: ${JSON.stringify(label)},
17153
+ label: ${JSON.stringify(label2)},
17134
17154
  element: <${name} />,
17135
17155
  },`;
17136
17156
  }).join("\n");
@@ -17213,7 +17233,7 @@ function ensureHeaderDocsNavigation(config2, docRoutes2) {
17213
17233
  }
17214
17234
  return { ...config2, header: { ...header, navigation } };
17215
17235
  }
17216
- function ensureHeaderApiReferenceNavigation(config2, to, label = HEADER_API_REFERENCE_LABEL) {
17236
+ function ensureHeaderApiReferenceNavigation(config2, to, label2 = HEADER_API_REFERENCE_LABEL) {
17217
17237
  const current = readHeaderNavigation(config2);
17218
17238
  if (!current) return config2;
17219
17239
  const { header, navigation } = current;
@@ -17221,7 +17241,7 @@ function ensureHeaderApiReferenceNavigation(config2, to, label = HEADER_API_REFE
17221
17241
  return config2;
17222
17242
  }
17223
17243
  const docsIndex = navigation.findIndex(isDocsItem);
17224
- const item = { label, to };
17244
+ const item = { label: label2, to };
17225
17245
  if (docsIndex >= 0) {
17226
17246
  navigation.splice(docsIndex + 1, 0, item);
17227
17247
  } else {
@@ -19555,10 +19575,10 @@ ${importStatement}${content.slice(lastImportEnd)}`;
19555
19575
  return `${importStatement}
19556
19576
  ${content}`;
19557
19577
  }
19558
- function formatCustomPageNavigationEntry(slug, label, indent) {
19578
+ function formatCustomPageNavigationEntry(slug, label2, indent) {
19559
19579
  const componentName = pageComponentName2(slug);
19560
19580
  const inner = `${indent} `;
19561
- const labelLine = label?.trim() ? `${inner}label: ${JSON.stringify(label.trim())},
19581
+ const labelLine = label2?.trim() ? `${inner}label: ${JSON.stringify(label2.trim())},
19562
19582
  ` : "";
19563
19583
  return `${indent}{
19564
19584
  ${inner}type: "custom-page",
@@ -19566,21 +19586,21 @@ ${inner}path: "/${normalizePageSlug2(slug)}",
19566
19586
  ` + labelLine + `${inner}element: <${componentName} />,
19567
19587
  ${indent}},`;
19568
19588
  }
19569
- function formatDocumentationCategoryWithPage(slug, label, indent) {
19589
+ function formatDocumentationCategoryWithPage(slug, label2, indent) {
19570
19590
  const inner = `${indent} `;
19571
19591
  const itemIndent = `${inner} `;
19572
19592
  return `${indent}{
19573
19593
  ${inner}type: "category",
19574
19594
  ${inner}label: ${JSON.stringify(DOCUMENTATION_NAV_LABEL2)},
19575
19595
  ${inner}items: [
19576
- ${formatCustomPageNavigationEntry(slug, label, itemIndent)}
19596
+ ${formatCustomPageNavigationEntry(slug, label2, itemIndent)}
19577
19597
  ${inner}],
19578
19598
  ${indent}},`;
19579
19599
  }
19580
- function findMatchingArrayClose(content, open3) {
19600
+ function findMatchingArrayClose(content, open4) {
19581
19601
  let depth = 0;
19582
19602
  let inString = null;
19583
- for (let i = open3; i < content.length; i++) {
19603
+ for (let i = open4; i < content.length; i++) {
19584
19604
  const char = content[i];
19585
19605
  if (inString) {
19586
19606
  if (char === "\\") {
@@ -19619,8 +19639,8 @@ function findDocumentationCategoryItemsRange(content, from, to) {
19619
19639
  if (!itemsMatch) {
19620
19640
  return null;
19621
19641
  }
19622
- const open3 = afterLabel + itemsMatch.index + itemsMatch[0].length - 1;
19623
- const close3 = findMatchingArrayClose(content, open3);
19642
+ const open4 = afterLabel + itemsMatch.index + itemsMatch[0].length - 1;
19643
+ const close3 = findMatchingArrayClose(content, open4);
19624
19644
  if (close3 === null || close3 > to) {
19625
19645
  return null;
19626
19646
  }
@@ -19631,7 +19651,7 @@ function findDocumentationCategoryItemsRange(content, from, to) {
19631
19651
  const indentMatch = /^[ \t]*/.exec(content.slice(lineStart));
19632
19652
  const itemsIndent = indentMatch?.[0] ?? "";
19633
19653
  const entryIndent = `${itemsIndent} `;
19634
- return { open: open3, close: close3, indent: entryIndent };
19654
+ return { open: open4, close: close3, indent: entryIndent };
19635
19655
  }
19636
19656
  function appendEntryToArrayRange(content, range, entry) {
19637
19657
  const before = content.slice(0, range.close);
@@ -19664,7 +19684,7 @@ ${normalizedEntry}
19664
19684
  ${range.indent}${trimmedInner}
19665
19685
  ${closeIndent}${content.slice(range.close)}`;
19666
19686
  }
19667
- function appendCustomPageToDirectBuildExport(content, slug, label) {
19687
+ function appendCustomPageToDirectBuildExport(content, slug, label2) {
19668
19688
  const exportPrefix = "export default ";
19669
19689
  const callPrefix = "buildApitogoConfig(";
19670
19690
  const exportIndex = content.indexOf(`${exportPrefix}${callPrefix}`);
@@ -19672,11 +19692,11 @@ function appendCustomPageToDirectBuildExport(content, slug, label) {
19672
19692
  return null;
19673
19693
  }
19674
19694
  const callStart = exportIndex + exportPrefix.length;
19675
- const open3 = callStart + callPrefix.length - 1;
19695
+ const open4 = callStart + callPrefix.length - 1;
19676
19696
  let depth = 0;
19677
19697
  let inString = null;
19678
19698
  let close3 = -1;
19679
- for (let i = open3; i < content.length; i++) {
19699
+ for (let i = open4; i < content.length; i++) {
19680
19700
  const char = content[i];
19681
19701
  if (inString) {
19682
19702
  if (char === "\\") {
@@ -19711,7 +19731,7 @@ function appendCustomPageToDirectBuildExport(content, slug, label) {
19711
19731
  replaceEnd += 1;
19712
19732
  }
19713
19733
  const buildCall = content.slice(callStart, close3 + 1);
19714
- const category = formatDocumentationCategoryWithPage(slug, label, " ");
19734
+ const category = formatDocumentationCategoryWithPage(slug, label2, " ");
19715
19735
  const replacement = `const config = ${buildCall};
19716
19736
 
19717
19737
  export default {
@@ -19737,10 +19757,10 @@ function findNavigationArrayRange(content) {
19737
19757
  return null;
19738
19758
  }
19739
19759
  const indent = match[1] ?? "";
19740
- const open3 = match.index + match[0].length - 1;
19760
+ const open4 = match.index + match[0].length - 1;
19741
19761
  let depth = 0;
19742
19762
  let inString = null;
19743
- for (let i = open3; i < content.length; i++) {
19763
+ for (let i = open4; i < content.length; i++) {
19744
19764
  const char = content[i];
19745
19765
  if (inString) {
19746
19766
  if (char === "\\") {
@@ -19759,13 +19779,13 @@ function findNavigationArrayRange(content) {
19759
19779
  } else if (char === "]") {
19760
19780
  depth -= 1;
19761
19781
  if (depth === 0) {
19762
- return { open: open3, close: i, indent };
19782
+ return { open: open4, close: i, indent };
19763
19783
  }
19764
19784
  }
19765
19785
  }
19766
19786
  return null;
19767
19787
  }
19768
- function appendCustomPageToNavigation(content, slug, label, _options) {
19788
+ function appendCustomPageToNavigation(content, slug, label2, _options) {
19769
19789
  const pagePath = `"/${normalizePageSlug2(slug)}"`;
19770
19790
  if (content.includes(`path: ${pagePath}`)) {
19771
19791
  return content;
@@ -19773,7 +19793,7 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19773
19793
  const rewrittenBuildExport = appendCustomPageToDirectBuildExport(
19774
19794
  content,
19775
19795
  slug,
19776
- label
19796
+ label2
19777
19797
  );
19778
19798
  if (rewrittenBuildExport) {
19779
19799
  return rewrittenBuildExport;
@@ -19790,14 +19810,14 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19790
19810
  if (docsItems) {
19791
19811
  const entry = formatCustomPageNavigationEntry(
19792
19812
  slug,
19793
- label,
19813
+ label2,
19794
19814
  docsItems.indent
19795
19815
  );
19796
19816
  return appendEntryToArrayRange(content, docsItems, entry);
19797
19817
  }
19798
19818
  const category = formatDocumentationCategoryWithPage(
19799
19819
  slug,
19800
- label,
19820
+ label2,
19801
19821
  `${range.indent} `
19802
19822
  );
19803
19823
  return prependEntryToArrayRange(
@@ -19806,11 +19826,11 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19806
19826
  category
19807
19827
  );
19808
19828
  }
19809
- function patchApitogoConfigForNewPage(content, slug, label, options) {
19829
+ function patchApitogoConfigForNewPage(content, slug, label2, options) {
19810
19830
  const withNavigation = appendCustomPageToNavigation(
19811
19831
  content,
19812
19832
  slug,
19813
- label,
19833
+ label2,
19814
19834
  options
19815
19835
  );
19816
19836
  if (withNavigation === content) {
@@ -19818,13 +19838,13 @@ function patchApitogoConfigForNewPage(content, slug, label, options) {
19818
19838
  }
19819
19839
  return ensurePageImport(withNavigation, pageComponentName2(slug), slug);
19820
19840
  }
19821
- async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label) {
19841
+ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19822
19842
  const configPath = await resolveApitogoConfigPath(sourceDirectory);
19823
19843
  if (!configPath) {
19824
19844
  return { updated: false };
19825
19845
  }
19826
19846
  const original = await readFile20(configPath, "utf8");
19827
- const patched = patchApitogoConfigForNewPage(original, slug, label, {
19847
+ const patched = patchApitogoConfigForNewPage(original, slug, label2, {
19828
19848
  typescript: configPath.endsWith(".tsx")
19829
19849
  });
19830
19850
  if (patched === original) {
@@ -21805,8 +21825,8 @@ function createScopedConsoleProgressReporter(stream, stepLabel, shared) {
21805
21825
  }
21806
21826
  },
21807
21827
  isCancelled: () => false,
21808
- child(label) {
21809
- return createScopedConsoleProgressReporter(stream, label, shared);
21828
+ child(label2) {
21829
+ return createScopedConsoleProgressReporter(stream, label2, shared);
21810
21830
  }
21811
21831
  };
21812
21832
  const baseOnNewMessages = reporter.onNewMessages.bind(reporter);
@@ -27904,15 +27924,41 @@ var PlansDeployError = class extends ApitogoError {
27904
27924
  this.name = "PlansDeployError";
27905
27925
  }
27906
27926
  };
27907
- var PLANS_STEPS = ["catalog", "backend", "identity-link"];
27927
+ var PLANS_STEPS = [
27928
+ "catalog",
27929
+ "backend",
27930
+ "billing",
27931
+ "identity-link"
27932
+ ];
27908
27933
  var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27909
27934
  var BACKEND_MODULE = "dev-portal";
27910
27935
  var PLANS_CAPABILITIES = { catalog: true, keys: true, usage: true };
27936
+ var STRIPE_WEBHOOK_EVENTS2 = [
27937
+ "checkout.session.completed",
27938
+ "customer.subscription.updated",
27939
+ "customer.subscription.deleted",
27940
+ "invoice.payment_failed"
27941
+ ];
27942
+ function billingNotes(billing) {
27943
+ const notes = [
27944
+ billing.mode === "live" ? "Stripe: LIVE mode. Customers pay with real money." : "Stripe: test mode. Use Stripe's test cards; no real money moves."
27945
+ ];
27946
+ notes.push(
27947
+ billing.hasWebhookSecret ? `Stripe webhook endpoint (add it in your Stripe account if it is not there yet): ${billing.webhookUrl}` : `A payment does not activate a plan until Stripe can confirm it. In your Stripe account add the webhook endpoint ${billing.webhookUrl} (events: ${STRIPE_WEBHOOK_EVENTS2.join(", ")}), store its signing secret with "apitogo secrets set STRIPE_WEBHOOK", set plans.billing.webhookSecret to "secret:STRIPE_WEBHOOK", and deploy again.`
27948
+ );
27949
+ return notes;
27950
+ }
27911
27951
  async function plansInputs(resolved, projectId) {
27912
27952
  const plans = resolved.plans;
27913
27953
  if (!plans) throw new Error("plans is not in the config.");
27914
- const wanted = toPlatformPlans(await readPlansFile(plans.file));
27954
+ const billing = plans.billing;
27955
+ const wanted = toPlatformPlans(await readPlansFile(plans.file), {
27956
+ billing: billing.provider === "stripe"
27957
+ });
27915
27958
  const manifest = await readManifest(resolved.state);
27959
+ const ledger = await readLedger(resolved.state);
27960
+ const billingOn = billing.provider === "stripe";
27961
+ const billingStep = billingOn || stepOf(ledger, "plans", "billing") !== void 0;
27916
27962
  const identity = manifest?.services.identity;
27917
27963
  const ownIdentity = Boolean(
27918
27964
  identity?.provider === "apitogo" && identity.issuer
@@ -27923,6 +27969,16 @@ async function plansInputs(resolved, projectId) {
27923
27969
  ownIdentity,
27924
27970
  catalogHash: stableJson({ projectId, plans: wanted }),
27925
27971
  backendHash: stableJson({ projectId }),
27972
+ billingStep,
27973
+ // Only the references to the project's secrets: a rotated value is applied with --force.
27974
+ billingHash: stableJson({
27975
+ projectId,
27976
+ provider: billing.provider,
27977
+ apiKey: billingOn ? billing.apiKey : void 0,
27978
+ webhookSecret: billingOn ? billing.webhookSecret : void 0,
27979
+ successUrl: billingOn ? billing.successUrl : void 0,
27980
+ cancelUrl: billingOn ? billing.cancelUrl : void 0
27981
+ }),
27926
27982
  linkHash: stableJson({
27927
27983
  projectId,
27928
27984
  issuer: identity?.issuer,
@@ -27959,6 +28015,17 @@ function refused(error) {
27959
28015
  }
27960
28016
  throw error;
27961
28017
  }
28018
+ function billingRefused(error) {
28019
+ if (error instanceof PlatformApiError && error.status === 400) {
28020
+ const lines = describePlatformProblem(error);
28021
+ throw new PlansDeployError(
28022
+ "PLANS_BILLING_REJECTED",
28023
+ `The platform refused the billing settings: ${lines.join(" ") || error.message}`,
28024
+ "Check plans.billing in the config and the project's secrets (apitogo secrets list). The Stripe key starts with sk_ or rk_, the webhook secret with whsec_."
28025
+ );
28026
+ }
28027
+ throw error;
28028
+ }
27962
28029
  async function deployPlans(resolved, projectId, deps, options = {}) {
27963
28030
  const plans = resolved.plans;
27964
28031
  if (!plans) throw new Error("plans is not in the config.");
@@ -27968,7 +28035,16 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
27968
28035
  const intervalMs = deps.pollIntervalMs ?? 2e3;
27969
28036
  const backendTimeoutMs = deps.backendTimeoutMs ?? 9e5;
27970
28037
  const linkTimeoutMs = deps.linkTimeoutMs ?? 6e5;
27971
- const { wanted, manifest, ownIdentity, catalogHash, backendHash, linkHash } = await plansInputs(resolved, projectId);
28038
+ const {
28039
+ wanted,
28040
+ manifest,
28041
+ ownIdentity,
28042
+ billingStep,
28043
+ billingHash,
28044
+ catalogHash,
28045
+ backendHash,
28046
+ linkHash
28047
+ } = await plansInputs(resolved, projectId);
27972
28048
  const catalogStep = {
27973
28049
  name: "catalog",
27974
28050
  hash: () => catalogHash,
@@ -28055,35 +28131,73 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28055
28131
  },
28056
28132
  stillValid: async () => Boolean(await backendOf())
28057
28133
  };
28134
+ const waitForBackendUpdate = async (jobId, what) => {
28135
+ const deadline = now() + linkTimeoutMs;
28136
+ for (; ; ) {
28137
+ const job = (await client.listProvisioningJobs(projectId)).jobs.find(
28138
+ (item) => item.id === jobId
28139
+ );
28140
+ const status2 = job?.status.toLowerCase();
28141
+ if (status2?.startsWith("completed")) return { jobId };
28142
+ if (status2 === "failed")
28143
+ throw new PlansDeployError(
28144
+ `${what.code}_FAILED`,
28145
+ `Applying ${what.text} to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
28146
+ "Run deploy again; it is applied again."
28147
+ );
28148
+ if (now() >= deadline)
28149
+ throw new PlansDeployError(
28150
+ `${what.code}_TIMEOUT`,
28151
+ `Applying ${what.text} did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
28152
+ "Run deploy again to keep waiting."
28153
+ );
28154
+ await sleep5(intervalMs);
28155
+ }
28156
+ };
28157
+ let billingNote;
28158
+ const billingStepDef = {
28159
+ name: "billing",
28160
+ hash: () => billingHash,
28161
+ run: async () => {
28162
+ const body = plans.billing.provider === "stripe" ? {
28163
+ provider: "stripe",
28164
+ apiKey: plans.billing.apiKey,
28165
+ webhookSecret: plans.billing.webhookSecret,
28166
+ successUrl: plans.billing.successUrl,
28167
+ cancelUrl: plans.billing.cancelUrl
28168
+ } : { provider: "none" };
28169
+ let queued;
28170
+ try {
28171
+ queued = await client.putDevPortalBilling(projectId, body);
28172
+ } catch (error) {
28173
+ billingRefused(error);
28174
+ }
28175
+ const done = await waitForBackendUpdate(queued.jobId, {
28176
+ code: "PLANS_BILLING",
28177
+ text: "the billing settings"
28178
+ });
28179
+ if (plans.billing.provider === "stripe")
28180
+ billingNote = billingNotes(queued);
28181
+ return { ...done, mode: queued.mode };
28182
+ }
28183
+ };
28058
28184
  const linkStep = {
28059
28185
  name: "identity-link",
28060
28186
  hash: () => linkHash,
28061
28187
  run: async () => {
28062
28188
  const queued = await client.linkDevPortalIdentity(projectId);
28063
- const deadline = now() + linkTimeoutMs;
28064
- for (; ; ) {
28065
- const job = (await client.listProvisioningJobs(projectId)).jobs.find(
28066
- (item) => item.id === queued.jobId
28067
- );
28068
- const status2 = job?.status.toLowerCase();
28069
- if (status2?.startsWith("completed")) return { jobId: queued.jobId };
28070
- if (status2 === "failed")
28071
- throw new PlansDeployError(
28072
- "PLANS_LINK_FAILED",
28073
- `Applying the identity to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
28074
- "Run deploy again; the link is applied again."
28075
- );
28076
- if (now() >= deadline)
28077
- throw new PlansDeployError(
28078
- "PLANS_LINK_TIMEOUT",
28079
- `Applying the identity did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
28080
- "Run deploy again to keep waiting."
28081
- );
28082
- await sleep5(intervalMs);
28083
- }
28189
+ return waitForBackendUpdate(queued.jobId, {
28190
+ code: "PLANS_LINK",
28191
+ text: "the identity"
28192
+ });
28084
28193
  }
28085
28194
  };
28086
- const steps = [catalogStep, backendStep, ...ownIdentity ? [linkStep] : []];
28195
+ const steps = [
28196
+ catalogStep,
28197
+ backendStep,
28198
+ ...billingStep ? [billingStepDef] : [],
28199
+ ...ownIdentity ? [linkStep] : []
28200
+ ];
28087
28201
  const outcomes = await runPipeline(
28088
28202
  resolved.state,
28089
28203
  "plans",
@@ -28108,7 +28222,7 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28108
28222
  apiBaseUrl,
28109
28223
  billing: plans.billing.provider,
28110
28224
  catalogRevision: catalog && !catalog.skipped ? previous + 1 : Math.max(previous, 1),
28111
- capabilities: PLANS_CAPABILITIES
28225
+ capabilities: plans.billing.provider === "stripe" ? { ...PLANS_CAPABILITIES, checkout: true } : PLANS_CAPABILITIES
28112
28226
  })
28113
28227
  );
28114
28228
  }
@@ -28119,7 +28233,8 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28119
28233
  step: outcome.step,
28120
28234
  skipped: outcome.skipped
28121
28235
  })),
28122
- apiBaseUrl
28236
+ apiBaseUrl,
28237
+ ...billingNote ? { notes: billingNote } : {}
28123
28238
  };
28124
28239
  }
28125
28240
 
@@ -28238,14 +28353,18 @@ async function planPlansService(resolved, ledger) {
28238
28353
  const hashes = {
28239
28354
  catalog: projectId ? inputs.catalogHash : void 0,
28240
28355
  backend: projectId ? inputs.backendHash : void 0,
28356
+ billing: projectId ? inputs.billingHash : void 0,
28241
28357
  "identity-link": projectId ? inputs.linkHash : void 0
28242
28358
  };
28243
28359
  const steps = PLANS_STEPS.filter(
28244
- (name) => name !== "identity-link" || inputs.ownIdentity
28360
+ (name) => (name !== "identity-link" || inputs.ownIdentity) && (name !== "billing" || inputs.billingStep)
28245
28361
  );
28246
28362
  const details = [
28247
28363
  ...inputs.wanted.length === 0 ? ["plans: none in the file"] : inputs.wanted.map((plan2) => `plan ${plan2.sku}: ${plan2.displayName}`),
28248
28364
  inputs.ownIdentity ? "sign-in: the project's identity will be applied to the plans backend" : "sign-in: no identity of the platform deployed, so nothing is applied to the plans backend",
28365
+ ...inputs.billingStep ? [
28366
+ resolved.plans?.billing.provider === "stripe" ? "billing: the Stripe key and webhook secret of the project's secrets will be applied to the plans backend" : "billing: switched off, so the stored Stripe settings will be removed from the plans backend"
28367
+ ] : [],
28249
28368
  ...steps.map(
28250
28369
  (name) => `step ${name}: ${stepState(stepOf(ledger, "plans", name), hashes[name])}`
28251
28370
  )
@@ -28486,7 +28605,7 @@ function describePolicies(gateway) {
28486
28605
  function dryRunLines(dryRun) {
28487
28606
  const changes = dryRun.changes;
28488
28607
  if (!changes) return [];
28489
- const list2 = (label, items) => items.length > 0 ? [` ${label}: ${items.join(", ")}`] : [];
28608
+ const list2 = (label2, items) => items.length > 0 ? [` ${label2}: ${items.join(", ")}`] : [];
28490
28609
  if (changes.wouldCreate)
28491
28610
  return [
28492
28611
  "platform dry run: the gateway does not exist yet; everything would be created",
@@ -29074,7 +29193,8 @@ async function deployPlansService(options, deps, projectId, named) {
29074
29193
  service: "plans",
29075
29194
  status: result.status,
29076
29195
  steps: result.steps,
29077
- url: result.apiBaseUrl
29196
+ url: result.apiBaseUrl,
29197
+ ...result.notes ? { notes: result.notes } : {}
29078
29198
  };
29079
29199
  }
29080
29200
  async function deployPortal(options, deps, projectId) {
@@ -29208,10 +29328,10 @@ var deploy_default = {
29208
29328
  describe: "Override project GUID"
29209
29329
  }).option("project-name", {
29210
29330
  type: "string",
29211
- describe: "Display name when creating a project"
29331
+ describe: "Version 1: display name when creating a project. Version 2 ignores it: the project is made by `apitogo project create` and named by project.name"
29212
29332
  }).option("platform-plan", {
29213
29333
  type: "string",
29214
- describe: "Platform hosting tier",
29334
+ describe: "Version 1: platform hosting tier. Version 2 ignores it: a project made by `apitogo project create` is on the Free plan",
29215
29335
  choices: ["free", "starter", "professional"]
29216
29336
  }).option("only", {
29217
29337
  type: "string",
@@ -30546,6 +30666,7 @@ var plan_default = {
30546
30666
  // src/cli/project/create-handler.ts
30547
30667
  init_invariant();
30548
30668
  import path93 from "node:path";
30669
+ import open3 from "open";
30549
30670
 
30550
30671
  // src/cli/project/v2.ts
30551
30672
  init_invariant();
@@ -30572,15 +30693,52 @@ async function createProjectV2(options, deps) {
30572
30693
  );
30573
30694
  }
30574
30695
  const name = options.name?.trim() || resolved.project.name;
30696
+ const plan2 = options.plan ?? "free";
30697
+ const onProjectResolved = async (projectId) => {
30698
+ await setProjectId(options.dir, projectId);
30699
+ };
30700
+ let continueWith = linkedProjectId;
30701
+ if (plan2 !== "free") {
30702
+ const first = parse4(
30703
+ await deps.bootstrap({
30704
+ name,
30705
+ description: options.description,
30706
+ modules,
30707
+ wait: false,
30708
+ projectId: linkedProjectId,
30709
+ onProjectResolved,
30710
+ paidPlan: true
30711
+ })
30712
+ );
30713
+ assertCreated(first);
30714
+ const paid = await settlePaidPlan(
30715
+ first.projectId,
30716
+ plan2,
30717
+ options,
30718
+ deps
30719
+ );
30720
+ if (paid.status === "awaiting_checkout") {
30721
+ await setProjectId(options.dir, first.projectId);
30722
+ return {
30723
+ projectId: first.projectId,
30724
+ projectName: name,
30725
+ modules,
30726
+ resumed: first.resumed === true,
30727
+ plan: plan2,
30728
+ status: "awaiting_checkout",
30729
+ checkoutUrl: paid.url
30730
+ };
30731
+ }
30732
+ continueWith = first.projectId;
30733
+ }
30575
30734
  const raw = await deps.bootstrap({
30576
30735
  name,
30577
30736
  description: options.description,
30578
30737
  modules,
30579
30738
  wait: options.wait !== false,
30580
- projectId: linkedProjectId,
30581
- onProjectResolved: async (projectId) => {
30582
- await setProjectId(options.dir, projectId);
30583
- }
30739
+ projectId: continueWith,
30740
+ onProjectResolved,
30741
+ ...plan2 !== "free" ? { paidPlan: true } : {}
30584
30742
  });
30585
30743
  const outcome = parse4(raw);
30586
30744
  if (outcome.requiresPlatformPlanSelection) {
@@ -30590,13 +30748,7 @@ async function createProjectV2(options, deps) {
30590
30748
  "Pick another name with --name, or choose a plan for that project in the web app."
30591
30749
  );
30592
30750
  }
30593
- if (outcome.status === "failed" || outcome.status === "blocked" || !outcome.projectId) {
30594
- throw new ProjectCreateError(
30595
- "CREATE_FAILED",
30596
- `The platform could not create the project: ${outcome.message ?? outcome.status ?? "no answer"}.`,
30597
- "Nothing was written to this folder. Try again, or check `apitogo status`."
30598
- );
30599
- }
30751
+ assertCreated(outcome);
30600
30752
  if (outcome.status === "running" || outcome.status === "pending") {
30601
30753
  await setProjectId(options.dir, outcome.projectId);
30602
30754
  throw new ProjectCreateError(
@@ -30610,9 +30762,82 @@ async function createProjectV2(options, deps) {
30610
30762
  projectId: outcome.projectId,
30611
30763
  projectName: name,
30612
30764
  modules,
30613
- resumed: outcome.resumed === true
30765
+ resumed: outcome.resumed === true,
30766
+ ...plan2 !== "free" ? { plan: plan2 } : {}
30614
30767
  };
30615
30768
  }
30769
+ function assertCreated(outcome) {
30770
+ if (outcome.status === "failed" || outcome.status === "blocked" || !outcome.projectId) {
30771
+ throw new ProjectCreateError(
30772
+ "CREATE_FAILED",
30773
+ `The platform could not create the project: ${outcome.message ?? outcome.status ?? "no answer"}.`,
30774
+ "Nothing was written to this folder. Try again, or check `apitogo status`."
30775
+ );
30776
+ }
30777
+ }
30778
+ var defaultSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
30779
+ var label = (plan2) => plan2.charAt(0).toUpperCase() + plan2.slice(1);
30780
+ async function settlePaidPlan(projectId, plan2, options, deps) {
30781
+ const api = deps.platform;
30782
+ if (!api) throw new Error("The platform client has no plan calls.");
30783
+ const sleep5 = api.sleep ?? defaultSleep4;
30784
+ const now = api.now ?? Date.now;
30785
+ const subscription = await api.getSubscription(projectId);
30786
+ const tier = subscription.planTier.toLowerCase();
30787
+ const onThisPlan = tier === plan2;
30788
+ if (onThisPlan && isPaidPlanActive(subscription)) return { status: "paid" };
30789
+ if (!isPlatformPlanUnselected(subscription.planTier) && !onThisPlan) {
30790
+ throw new ProjectCreateError(
30791
+ "PLAN_CHANGE_UNSUPPORTED",
30792
+ `This project is already on the ${label(tier)} plan, so it cannot be created on ${label(plan2)}.`,
30793
+ "Moving a project to another plan is not supported yet. Create a new project with --plan in a new folder, or change the plan in the web app."
30794
+ );
30795
+ }
30796
+ if (!onThisPlan) {
30797
+ let session;
30798
+ try {
30799
+ session = await api.createCheckoutSession(projectId, {
30800
+ planSlug: plan2,
30801
+ ...options.skipPayment ? { skipPayment: true } : {}
30802
+ });
30803
+ } catch (error) {
30804
+ if (options.skipPayment && error instanceof PlatformApiError && error.status === 403)
30805
+ throw new ProjectCreateError(
30806
+ "SKIP_PAYMENT_REFUSED",
30807
+ "Only a platform admin may skip the payment.",
30808
+ "Run it without --skip-payment to pay through the platform's checkout."
30809
+ );
30810
+ throw error;
30811
+ }
30812
+ if (session.granted) return { status: "paid" };
30813
+ if (!session.url)
30814
+ throw new ProjectCreateError(
30815
+ "CREATE_FAILED",
30816
+ "The platform gave no checkout link.",
30817
+ "Run the command again, or choose the plan in the web app."
30818
+ );
30819
+ api.say?.(`Pay for the ${label(plan2)} plan here: ${session.url}`);
30820
+ if (options.waitForPayment === false)
30821
+ return { status: "awaiting_checkout", url: session.url };
30822
+ if (options.open !== false)
30823
+ await api.openUrl?.(session.url).catch(() => {
30824
+ });
30825
+ }
30826
+ const timeoutMs = options.paymentTimeoutMs ?? 9e5;
30827
+ const deadline = now() + timeoutMs;
30828
+ for (; ; ) {
30829
+ const current = await api.getSubscription(projectId);
30830
+ if (current.planTier.toLowerCase() === plan2 && isPaidPlanActive(current))
30831
+ return { status: "paid" };
30832
+ if (now() >= deadline)
30833
+ throw new ProjectCreateError(
30834
+ "CHECKOUT_PENDING",
30835
+ `The payment for the ${label(plan2)} plan is not confirmed yet.`,
30836
+ `Finish paying with the link above, then run \`apitogo project create --plan ${plan2}\` again; it continues the same project (project.id ${projectId}).`
30837
+ );
30838
+ await sleep5(api.pollIntervalMs ?? 3e3);
30839
+ }
30840
+ }
30616
30841
  function parse4(raw) {
30617
30842
  try {
30618
30843
  return JSON.parse(raw);
@@ -30641,9 +30866,22 @@ async function createLinkedProject(argv, dir) {
30641
30866
  dir,
30642
30867
  name: argv.name,
30643
30868
  description: argv.description,
30644
- wait: argv.wait !== false
30869
+ wait: argv.wait !== false,
30870
+ plan: argv.plan ?? "free",
30871
+ skipPayment: argv.skipPayment === true,
30872
+ open: argv.open !== false,
30873
+ // In JSON mode the link is returned and a later run continues; otherwise the command waits for the payment.
30874
+ waitForPayment: !json
30645
30875
  },
30646
30876
  {
30877
+ platform: {
30878
+ getSubscription: (projectId) => client.getSubscription(projectId),
30879
+ createCheckoutSession: (projectId, body) => client.createCheckoutSession(projectId, body),
30880
+ openUrl: async (url2) => {
30881
+ await open3(url2);
30882
+ },
30883
+ ...json ? {} : { say: (message) => printDiagnosticsToConsole(message) }
30884
+ },
30647
30885
  bootstrap: (args) => handleBootstrapProject(
30648
30886
  client,
30649
30887
  config2,
@@ -30651,7 +30889,7 @@ async function createLinkedProject(argv, dir) {
30651
30889
  name: args.name,
30652
30890
  description: args.description,
30653
30891
  modules: args.modules,
30654
- selectFreePlan: true,
30892
+ selectFreePlan: args.paidPlan !== true,
30655
30893
  wait: args.wait,
30656
30894
  configureGatewayBaseline: false,
30657
30895
  projectId: args.projectId,
@@ -30666,8 +30904,14 @@ async function createLinkedProject(argv, dir) {
30666
30904
  console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));
30667
30905
  return;
30668
30906
  }
30907
+ if (result.status === "awaiting_checkout") {
30908
+ printResultToConsole(
30909
+ `Project ${result.projectName} (${result.projectId}) is created and linked. Pay for the ${result.plan} plan at ${result.checkoutUrl}, then run \`apitogo project create --plan ${result.plan}\` again to continue.`
30910
+ );
30911
+ return;
30912
+ }
30669
30913
  printDiagnosticsToConsole(
30670
- `Project ${result.projectName} (${result.projectId}) is ready on the free plan${result.modules.length > 0 ? ` with: ${result.modules.join(", ")}` : " (the gateway is created by `apitogo deploy`)"}.`
30914
+ `Project ${result.projectName} (${result.projectId}) is ready on the ${result.plan ?? "free"} plan${result.modules.length > 0 ? ` with: ${result.modules.join(", ")}` : " (the gateway is created by `apitogo deploy`)"}.`
30671
30915
  );
30672
30916
  printResultToConsole(
30673
30917
  "The folder is linked. Run `apitogo deploy` to publish."
@@ -30697,6 +30941,12 @@ async function createProject(argv) {
30697
30941
  await createLinkedProject(argv, dir);
30698
30942
  return;
30699
30943
  }
30944
+ if (argv.plan && argv.plan !== "free") {
30945
+ await printCriticalFailureToConsoleAndExit(
30946
+ "--plan is for config version 2 folders. For a version 1 project use `apitogo deploy --platform-plan`."
30947
+ );
30948
+ return;
30949
+ }
30700
30950
  const name = argv.name?.trim();
30701
30951
  if (!name) {
30702
30952
  await printCriticalFailureToConsoleAndExit(
@@ -30763,11 +31013,24 @@ var project_create_default = {
30763
31013
  describe: "Optional project description"
30764
31014
  }).option("modules", {
30765
31015
  type: "string",
30766
- describe: "Comma-separated modules to activate (default: apihub)",
31016
+ describe: "Version 1 only: comma-separated modules to activate (default: apihub). Version 2 takes them from the config (the portal needs dev-portal) and ignores this option",
30767
31017
  default: "apihub"
31018
+ }).option("plan", {
31019
+ type: "string",
31020
+ choices: ["free", "starter", "professional"],
31021
+ describe: "Version 2: the platform plan of the new project (default: free). Starter and Professional are paid through the platform's own checkout, and a project needs one of them to sell paid plans"
31022
+ }).option("open", {
31023
+ type: "boolean",
31024
+ describe: "Open the checkout page in the browser for a paid plan (use --no-open to only print the link)",
31025
+ default: true
31026
+ }).option("skip-payment", {
31027
+ type: "boolean",
31028
+ describe: "Platform admins only: give the paid plan without payment. The platform refuses it for anyone else",
31029
+ default: false,
31030
+ hidden: true
30768
31031
  }).option("wait", {
30769
31032
  type: "boolean",
30770
- describe: "Wait for warm pool when a platform plan is already selected",
31033
+ describe: "Wait until the hosting from the warm pool is ready before returning, once a platform plan is selected (always so in version 2). Use --no-wait to return sooner",
30771
31034
  default: true
30772
31035
  }).option("json", {
30773
31036
  type: "boolean",