@lukoweb/apitogo 0.2.47 → 0.2.49

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.
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { glob } from "glob";
4
4
  import { parse as parseYaml } from "yaml";
5
+ import { billingNeeded, stripeKeySecretName } from "./billing.js";
5
6
  import type { ConfigIssue } from "./errors.js";
6
7
  import { type ResolvedConfigV2, toGlobPath } from "./resolve.js";
7
8
  import type { ConfigV2 } from "./schema.js";
@@ -657,9 +658,9 @@ function identityRules(
657
658
  // --- plans (2.6) ----------------------------------------------------------------------------------------------------
658
659
 
659
660
  async function plansRules(
660
- config: ConfigV2,
661
661
  resolved: ResolvedConfigV2,
662
662
  out: Collector,
663
+ paid: boolean,
663
664
  ) {
664
665
  const plans = resolved.plans;
665
666
  if (!plans?.enabled) return;
@@ -683,16 +684,8 @@ async function plansRules(
683
684
  }
684
685
  }
685
686
 
686
- if (plans.billing.provider === "stripe") {
687
- if (!config.plans?.billing?.apiKey) {
688
- out.error({
689
- code: "PLANS_BILLING_INCOMPLETE",
690
- path: "plans.billing.apiKey",
691
- message:
692
- "plans.billing.apiKey is required when billing.provider is stripe.",
693
- hint: 'Set it to a secret reference, for example "secret:STRIPE_API_KEY".',
694
- });
695
- }
687
+ // Billing is on only when a plan has a price. The Stripe key defaults to the STRIPE_API_KEY secret.
688
+ if (paid) {
696
689
  const headless = !(resolved.portal?.enabled === true);
697
690
  for (const field of ["successUrl", "cancelUrl"] as const) {
698
691
  if (headless && !plans.billing[field]) {
@@ -711,11 +704,22 @@ async function plansRules(
711
704
 
712
705
  function secretRules(
713
706
  config: ConfigV2,
707
+ resolved: ResolvedConfigV2,
714
708
  context: CheckContext,
715
709
  out: Collector,
716
710
  skipped: string[],
711
+ paid: boolean,
717
712
  ) {
718
- const refs = collectSecretRefs(config);
713
+ // The billing secrets matter only when a plan has a price. The Stripe key is then needed even when the config does not
714
+ // name it (it defaults to STRIPE_API_KEY). `apitogo deploy` asks for it, so a missing key is a warning, not an error.
715
+ const stripeKeyPath = "plans.billing.apiKey";
716
+ const named = collectSecretRefs(config).filter(
717
+ (ref) => paid || !ref.path.startsWith("plans.billing"),
718
+ );
719
+ const refs =
720
+ paid && !named.some((ref) => ref.path === stripeKeyPath)
721
+ ? [...named, { name: stripeKeySecretName(resolved), path: stripeKeyPath }]
722
+ : named;
719
723
  if (refs.length === 0) return;
720
724
  if (!context.knownSecrets) {
721
725
  skipped.push(
@@ -724,7 +728,14 @@ function secretRules(
724
728
  return;
725
729
  }
726
730
  for (const ref of refs) {
727
- if (!context.knownSecrets.has(ref.name)) {
731
+ if (ref.path === stripeKeyPath && !context.knownSecrets.has(ref.name)) {
732
+ out.warn({
733
+ code: "SECRET_NOT_SET",
734
+ path: ref.path,
735
+ message: `a plan has a price, and the Stripe key (secret ${ref.name}) is not set yet.`,
736
+ hint: `apitogo deploy asks for it. Or run: apitogo secrets set ${ref.name}`,
737
+ });
738
+ } else if (!context.knownSecrets.has(ref.name)) {
728
739
  out.error({
729
740
  code: "SECRET_NOT_SET",
730
741
  path: ref.path,
@@ -751,12 +762,13 @@ export async function checkConfigV2(
751
762
  warn: (issue) => warnings.push(issue),
752
763
  };
753
764
 
765
+ const paid = await billingNeeded(resolved);
754
766
  dependencyRules(config, resolved, out);
755
- secretRules(config, context, out, skipped);
767
+ secretRules(config, resolved, context, out, skipped, paid);
756
768
  await portalRules(config, resolved, out);
757
769
  await gatewayRules(resolved, out);
758
770
  identityRules(config, resolved, out);
759
- await plansRules(config, resolved, out);
771
+ await plansRules(resolved, out, paid);
760
772
 
761
773
  return { errors, warnings, skipped };
762
774
  }
@@ -9,6 +9,7 @@ import {
9
9
  } from "node:fs/promises";
10
10
  import path from "node:path";
11
11
  import { ApitogoError } from "../../lib/util/invariant.js";
12
+ import { isPaidPlan } from "./billing.js";
12
13
  import {
13
14
  FULL_SERVER_HEALTH_PATH,
14
15
  FULL_SERVER_TIMEOUT_SECONDS,
@@ -182,7 +183,6 @@ export type AddOptions = {
182
183
  plans?: {
183
184
  file?: string;
184
185
  template?: "free" | "tiered" | "none";
185
- billing?: "stripe" | "none";
186
186
  };
187
187
  };
188
188
 
@@ -773,15 +773,8 @@ async function buildPlans(
773
773
  : isFilled(options.template)
774
774
  ? "starter"
775
775
  : "free");
776
- // Only paid plans need a billing provider. A catalog of free plans takes no payment, so its default is `none` and the
777
- // config checks clean right away; the tiered template (paid plans) defaults to Stripe, whose key is set with `secrets set`.
778
- const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
779
- if (template === "tiered" && billing === "none") {
780
- throw new ServiceEditError(
781
- "The tiered template has paid plans, which need a billing provider.",
782
- "Use --billing stripe, or --template free.",
783
- );
784
- }
776
+ // Stripe is the only billing provider, and it is needed only once a plan has a price: the config names no billing
777
+ // settings, and `apitogo deploy` asks for the Stripe key (secret STRIPE_API_KEY) the first time a paid plan is deployed.
785
778
  const file = o.file ? insideProject(dir, o.file, "Plans file") : undefined;
786
779
  const target = file
787
780
  ? path.join(dir, file)
@@ -791,22 +784,14 @@ async function buildPlans(
791
784
  target,
792
785
  `${JSON.stringify({ version: 1, plans: PLAN_TEMPLATES[template] }, null, 2)}\n`,
793
786
  );
794
- if (billing === "stripe") {
787
+ if (PLAN_TEMPLATES[template].some((plan) => isPaidPlan(plan))) {
795
788
  context.notes.push(
796
- "Set the billing key: apitogo secrets set STRIPE_API_KEY",
789
+ "A plan has a price, so payments go through Stripe: apitogo deploy asks for your Stripe secret key.",
797
790
  );
798
791
  }
799
792
  return {
800
793
  ...(options.disabled ? { enabled: false } : {}),
801
794
  ...(file ? { file } : {}),
802
- ...(billing === "stripe"
803
- ? {
804
- billing: {
805
- provider: "stripe" as const,
806
- apiKey: "secret:STRIPE_API_KEY",
807
- },
808
- }
809
- : { billing: { provider: "none" as const } }),
810
795
  };
811
796
  }
812
797