@lukoweb/apitogo 0.2.54 → 0.2.56
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 +1192 -834
- package/dist/declarations/cli/platform/client/platform-api.d.ts +23 -0
- package/dist/declarations/cli/platform/helpers/platform-plan-catalog.d.ts +6 -0
- package/docs/cli/walkthrough.md +1 -1
- package/package.json +1 -1
- package/src/config/v2/services.ts +12 -13
- package/src/config/v2/wrapper.ts +14 -0
package/dist/cli/cli.js
CHANGED
|
@@ -8418,6 +8418,13 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
|
|
|
8418
8418
|
for (const page of tsxPages)
|
|
8419
8419
|
if (page.order !== void 0 && !pageOrders.has(page.route))
|
|
8420
8420
|
pageOrders.set(page.route, page.order);
|
|
8421
|
+
if (plan2.plansPlugin) {
|
|
8422
|
+
const featured = (await readDemoPlans(resolved.plans?.file))?.find(
|
|
8423
|
+
(item) => typeof item === "object" && item !== null && item.featured === true && typeof item.sku === "string"
|
|
8424
|
+
);
|
|
8425
|
+
if (featured)
|
|
8426
|
+
plan2.plansPlugin.featuredPlanSku = featured.sku.trim().toLowerCase();
|
|
8427
|
+
}
|
|
8421
8428
|
if (plan2.plansPlugin && plan2.docs) {
|
|
8422
8429
|
const landing = hostedDocRoutes.some(
|
|
8423
8430
|
(route) => route === "/" || route === "/index"
|
|
@@ -9022,21 +9029,23 @@ var PLAN_TEMPLATES = {
|
|
|
9022
9029
|
displayName: "Free",
|
|
9023
9030
|
default: true,
|
|
9024
9031
|
limits: {
|
|
9025
|
-
// The platform's Free tier allows at most
|
|
9026
|
-
rateLimit: { calls:
|
|
9032
|
+
// The platform's Free tier allows at most 60 calls per minute per plan (its rate_limit_per_minute ceiling).
|
|
9033
|
+
rateLimit: { calls: 60, periodSeconds: 60 },
|
|
9027
9034
|
quota: { calls: 1e3, periodDays: 30 }
|
|
9028
9035
|
}
|
|
9029
9036
|
}
|
|
9030
9037
|
],
|
|
9031
|
-
// Sample plans for the
|
|
9032
|
-
//
|
|
9038
|
+
// Sample plans for the full template: a free plan and one paid example (API-124). The example's price is in the file
|
|
9039
|
+
// but it is a contact-us plan, so the first deploy works on a Free project (a plan with a price needs Starter or
|
|
9040
|
+
// Professional and billing). `apitogo plan` names the command that starts selling it. It has no limits of its own,
|
|
9041
|
+
// so it stays within any project's ceiling until the tenant sets them.
|
|
9033
9042
|
starter: [
|
|
9034
9043
|
{
|
|
9035
9044
|
sku: "free",
|
|
9036
9045
|
displayName: "Free",
|
|
9037
9046
|
default: true,
|
|
9038
9047
|
limits: {
|
|
9039
|
-
rateLimit: { calls:
|
|
9048
|
+
rateLimit: { calls: 60, periodSeconds: 60 },
|
|
9040
9049
|
quota: { calls: 1e3, periodDays: 30 }
|
|
9041
9050
|
}
|
|
9042
9051
|
},
|
|
@@ -9044,13 +9053,10 @@ var PLAN_TEMPLATES = {
|
|
|
9044
9053
|
sku: "pro",
|
|
9045
9054
|
displayName: "Pro",
|
|
9046
9055
|
pricingModel: "custom",
|
|
9047
|
-
priceLabel: "Contact us"
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
displayName: "Enterprise",
|
|
9052
|
-
pricingModel: "custom",
|
|
9053
|
-
priceLabel: "Contact us"
|
|
9056
|
+
priceLabel: "Contact us",
|
|
9057
|
+
priceAmount: 19,
|
|
9058
|
+
priceCurrency: "usd",
|
|
9059
|
+
billingInterval: "month"
|
|
9054
9060
|
}
|
|
9055
9061
|
],
|
|
9056
9062
|
tiered: [
|
|
@@ -9059,8 +9065,8 @@ var PLAN_TEMPLATES = {
|
|
|
9059
9065
|
displayName: "Free",
|
|
9060
9066
|
default: true,
|
|
9061
9067
|
limits: {
|
|
9062
|
-
// The platform's Free tier allows at most
|
|
9063
|
-
rateLimit: { calls:
|
|
9068
|
+
// The platform's Free tier allows at most 60 calls per minute per plan (its rate_limit_per_minute ceiling).
|
|
9069
|
+
rateLimit: { calls: 60, periodSeconds: 60 },
|
|
9064
9070
|
quota: { calls: 1e3, periodDays: 30 }
|
|
9065
9071
|
}
|
|
9066
9072
|
},
|
|
@@ -10295,6 +10301,17 @@ var PlatformApiClient = class {
|
|
|
10295
10301
|
`/api/v1/projects/${projectId}/dev-portal/identity-link`
|
|
10296
10302
|
);
|
|
10297
10303
|
}
|
|
10304
|
+
/** The active platform hosting plans, the list apitogo.com shows. */
|
|
10305
|
+
listPlatformPlans() {
|
|
10306
|
+
return this.request("GET", "/api/v1/platform-plans");
|
|
10307
|
+
}
|
|
10308
|
+
/** Whether Stripe events reach the project's plans backend; null when there is none or it did not answer. */
|
|
10309
|
+
getBillingWebhookStatus(projectId) {
|
|
10310
|
+
return this.request(
|
|
10311
|
+
"GET",
|
|
10312
|
+
`/api/v1/projects/${projectId}/dev-portal/billing/webhook-status`
|
|
10313
|
+
);
|
|
10314
|
+
}
|
|
10298
10315
|
getDevPortalBackend(projectId) {
|
|
10299
10316
|
return this.request(
|
|
10300
10317
|
"GET",
|
|
@@ -10861,7 +10878,7 @@ import {
|
|
|
10861
10878
|
// package.json
|
|
10862
10879
|
var package_default = {
|
|
10863
10880
|
name: "@lukoweb/apitogo",
|
|
10864
|
-
version: "0.2.
|
|
10881
|
+
version: "0.2.56",
|
|
10865
10882
|
type: "module",
|
|
10866
10883
|
sideEffects: [
|
|
10867
10884
|
"**/*.css",
|
|
@@ -16020,6 +16037,8 @@ var PlanFilePlanSchema = z14.strictObject({
|
|
|
16020
16037
|
pricingModel: z14.enum(["fixed", "custom"]).optional(),
|
|
16021
16038
|
priceLabel: z14.string().optional(),
|
|
16022
16039
|
sortOrder: z14.int().optional(),
|
|
16040
|
+
/** Shown as "Popular" on the pricing page (API-124). Only the portal reads it; the platform never sees it. */
|
|
16041
|
+
featured: z14.boolean().optional(),
|
|
16023
16042
|
/** Action keys such as `get:/orders`. Left out, the plan includes every action of the gateway. */
|
|
16024
16043
|
includedActions: z14.array(z14.string().trim().min(1)).optional()
|
|
16025
16044
|
});
|
|
@@ -16095,15 +16114,16 @@ function toPlatformPlans(plans, options = {}) {
|
|
|
16095
16114
|
}
|
|
16096
16115
|
} : {}
|
|
16097
16116
|
} : void 0;
|
|
16117
|
+
const custom = plan2.pricingModel === "custom";
|
|
16098
16118
|
return {
|
|
16099
16119
|
sku: plan2.sku.trim().toLowerCase(),
|
|
16100
16120
|
displayName: plan2.displayName,
|
|
16101
16121
|
...limits ? { limitsJson: JSON.stringify(limits) } : {},
|
|
16102
16122
|
includedActionKeys: plan2.includedActions ?? null,
|
|
16103
16123
|
...plan2.pricingModel ? { pricingModel: plan2.pricingModel } : {},
|
|
16104
|
-
...plan2.priceAmount !== void 0 ? { priceAmount: plan2.priceAmount } : {},
|
|
16124
|
+
...plan2.priceAmount !== void 0 && !custom ? { priceAmount: plan2.priceAmount } : {},
|
|
16105
16125
|
...plan2.priceLabel !== void 0 ? { priceLabel: plan2.priceLabel } : {},
|
|
16106
|
-
...plan2.priceCurrency ? { priceCurrency: plan2.priceCurrency } : {},
|
|
16126
|
+
...plan2.priceCurrency && !custom ? { priceCurrency: plan2.priceCurrency } : {},
|
|
16107
16127
|
...plan2.billingInterval ? { billingPeriod: BILLING_PERIOD[plan2.billingInterval] } : {},
|
|
16108
16128
|
sortOrder: plan2.sortOrder ?? index
|
|
16109
16129
|
};
|
|
@@ -19378,7 +19398,7 @@ function buildWizardPlan(input5) {
|
|
|
19378
19398
|
} : {}
|
|
19379
19399
|
});
|
|
19380
19400
|
}
|
|
19381
|
-
async function
|
|
19401
|
+
async function readPlanCatalog(project) {
|
|
19382
19402
|
const plans = project.resolved.plans;
|
|
19383
19403
|
if (!plans) {
|
|
19384
19404
|
throw new ConfigEditError(
|
|
@@ -19404,27 +19424,129 @@ async function upsertPlan(project, plan2) {
|
|
|
19404
19424
|
}
|
|
19405
19425
|
catalog = { version: 1, plans: raw.plans };
|
|
19406
19426
|
}
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
else next[index] = plan2;
|
|
19413
|
-
await mkdir15(path62.dirname(plans.file), { recursive: true });
|
|
19427
|
+
return { file: plans.file, billing: plans.billing, catalog };
|
|
19428
|
+
}
|
|
19429
|
+
var skuOf = (item) => isPlainObject3(item) && typeof item.sku === "string" ? item.sku.trim().toLowerCase() : void 0;
|
|
19430
|
+
async function writePlanCatalog(file, catalog, plans) {
|
|
19431
|
+
await mkdir15(path62.dirname(file), { recursive: true });
|
|
19414
19432
|
await writeFile18(
|
|
19415
|
-
|
|
19416
|
-
`${JSON.stringify({ ...catalog, plans
|
|
19433
|
+
file,
|
|
19434
|
+
`${JSON.stringify({ ...catalog, plans }, null, 2)}
|
|
19417
19435
|
`,
|
|
19418
19436
|
"utf8"
|
|
19419
19437
|
);
|
|
19438
|
+
}
|
|
19439
|
+
async function upsertPlan(project, plan2) {
|
|
19440
|
+
const { file, billing, catalog } = await readPlanCatalog(project);
|
|
19441
|
+
const index = catalog.plans.findIndex((item) => skuOf(item) === plan2.sku);
|
|
19442
|
+
const next = plan2.featured ? catalog.plans.map((item) => {
|
|
19443
|
+
if (!isPlainObject3(item) || item.featured !== true) return item;
|
|
19444
|
+
const { featured: _featured, ...rest } = item;
|
|
19445
|
+
return rest;
|
|
19446
|
+
}) : [...catalog.plans];
|
|
19447
|
+
if (index === -1) next.push(plan2);
|
|
19448
|
+
else next[index] = plan2;
|
|
19449
|
+
await writePlanCatalog(file, catalog, next);
|
|
19420
19450
|
const paid = isPaidPlan(plan2);
|
|
19421
19451
|
return {
|
|
19422
19452
|
action: index === -1 ? "added" : "updated",
|
|
19423
19453
|
sku: plan2.sku,
|
|
19424
|
-
file: toPosix2(path62.relative(project.dir,
|
|
19454
|
+
file: toPosix2(path62.relative(project.dir, file)),
|
|
19425
19455
|
paid,
|
|
19426
|
-
needsBilling: paid &&
|
|
19456
|
+
needsBilling: paid && billing.provider !== "stripe"
|
|
19457
|
+
};
|
|
19458
|
+
}
|
|
19459
|
+
var pairOf = (value, flag, unit) => {
|
|
19460
|
+
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(value);
|
|
19461
|
+
const calls = match?.[1] ? Number(match[1]) : Number.NaN;
|
|
19462
|
+
const period = match?.[2] ? Number(match[2]) : Number.NaN;
|
|
19463
|
+
if (!(calls > 0) || !(period > 0)) {
|
|
19464
|
+
throw new ConfigEditError(
|
|
19465
|
+
`${flag} must be <calls>/<${unit}>, both whole numbers above 0, not "${value}".`,
|
|
19466
|
+
flag === "--plan-rate" ? "For example --plan-rate 60/60 is 60 calls per minute." : "For example --plan-quota 50000/30 is 50,000 calls per 30 days."
|
|
19467
|
+
);
|
|
19468
|
+
}
|
|
19469
|
+
return [calls, period];
|
|
19470
|
+
};
|
|
19471
|
+
async function planFromFlags(project, flags) {
|
|
19472
|
+
const sku = flags.sku.trim().toLowerCase();
|
|
19473
|
+
if (!sku) throw new ConfigEditError("--plan-sku must not be empty.");
|
|
19474
|
+
const { catalog } = await readPlanCatalog(project);
|
|
19475
|
+
const existing = catalog.plans.find((item) => skuOf(item) === sku);
|
|
19476
|
+
const base = isPlainObject3(existing) ? { ...existing } : {};
|
|
19477
|
+
const plan2 = {
|
|
19478
|
+
...base,
|
|
19479
|
+
sku,
|
|
19480
|
+
displayName: flags.name?.trim() || (typeof base.displayName === "string" ? base.displayName : void 0) || sku.charAt(0).toUpperCase() + sku.slice(1)
|
|
19427
19481
|
};
|
|
19482
|
+
if (flags.price !== void 0) {
|
|
19483
|
+
const price = flags.price.trim().toLowerCase();
|
|
19484
|
+
if (price === "custom") {
|
|
19485
|
+
plan2.pricingModel = "custom";
|
|
19486
|
+
plan2.priceLabel = typeof base.priceLabel === "string" ? base.priceLabel : "Contact us";
|
|
19487
|
+
delete plan2.priceAmount;
|
|
19488
|
+
delete plan2.priceCurrency;
|
|
19489
|
+
delete plan2.billingInterval;
|
|
19490
|
+
} else {
|
|
19491
|
+
const amount = Number(price);
|
|
19492
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
19493
|
+
throw new ConfigEditError(
|
|
19494
|
+
`--plan-price must be an amount (0 for free) or "custom", not "${flags.price}".`,
|
|
19495
|
+
"For example --plan-price 19 for 19 a month, or --plan-price custom for a contact-us plan."
|
|
19496
|
+
);
|
|
19497
|
+
}
|
|
19498
|
+
delete plan2.pricingModel;
|
|
19499
|
+
delete plan2.priceLabel;
|
|
19500
|
+
plan2.priceAmount = amount;
|
|
19501
|
+
if (amount > 0) {
|
|
19502
|
+
plan2.priceCurrency = flags.currency?.trim().toLowerCase() || base.priceCurrency || "usd";
|
|
19503
|
+
plan2.billingInterval = flags.interval ?? base.billingInterval ?? "month";
|
|
19504
|
+
} else {
|
|
19505
|
+
delete plan2.priceCurrency;
|
|
19506
|
+
delete plan2.billingInterval;
|
|
19507
|
+
}
|
|
19508
|
+
}
|
|
19509
|
+
} else {
|
|
19510
|
+
if (flags.currency !== void 0)
|
|
19511
|
+
plan2.priceCurrency = flags.currency.trim().toLowerCase();
|
|
19512
|
+
if (flags.interval !== void 0) plan2.billingInterval = flags.interval;
|
|
19513
|
+
}
|
|
19514
|
+
const limits = isPlainObject3(base.limits) ? { ...base.limits } : {};
|
|
19515
|
+
if (flags.rate !== void 0) {
|
|
19516
|
+
const [calls, periodSeconds] = pairOf(flags.rate, "--plan-rate", "seconds");
|
|
19517
|
+
limits.rateLimit = { calls, periodSeconds };
|
|
19518
|
+
}
|
|
19519
|
+
if (flags.quota !== void 0) {
|
|
19520
|
+
const [calls, periodDays] = pairOf(flags.quota, "--plan-quota", "days");
|
|
19521
|
+
limits.quota = { calls, periodDays };
|
|
19522
|
+
}
|
|
19523
|
+
if (Object.keys(limits).length > 0) plan2.limits = limits;
|
|
19524
|
+
if (flags.order !== void 0) {
|
|
19525
|
+
if (!Number.isInteger(flags.order)) {
|
|
19526
|
+
throw new ConfigEditError(
|
|
19527
|
+
`--plan-order must be a whole number, not ${flags.order}.`,
|
|
19528
|
+
"Lower numbers come first on the pricing page."
|
|
19529
|
+
);
|
|
19530
|
+
}
|
|
19531
|
+
plan2.sortOrder = flags.order;
|
|
19532
|
+
}
|
|
19533
|
+
if (flags.featured === true) plan2.featured = true;
|
|
19534
|
+
else if (flags.featured === false) delete plan2.featured;
|
|
19535
|
+
return parsePlan(plan2);
|
|
19536
|
+
}
|
|
19537
|
+
async function removePlan(project, rawSku) {
|
|
19538
|
+
const sku = rawSku.trim().toLowerCase();
|
|
19539
|
+
const { file, catalog } = await readPlanCatalog(project);
|
|
19540
|
+
const next = catalog.plans.filter((item) => skuOf(item) !== sku);
|
|
19541
|
+
if (next.length === catalog.plans.length) {
|
|
19542
|
+
const known = catalog.plans.map(skuOf).filter(Boolean).join(", ");
|
|
19543
|
+
throw new ConfigEditError(
|
|
19544
|
+
`There is no plan "${sku}" in ${toPosix2(path62.relative(project.dir, file))}.`,
|
|
19545
|
+
known ? `The plans are: ${known}.` : "The plans file has no plans."
|
|
19546
|
+
);
|
|
19547
|
+
}
|
|
19548
|
+
await writePlanCatalog(file, catalog, next);
|
|
19549
|
+
return { sku, file: toPosix2(path62.relative(project.dir, file)) };
|
|
19428
19550
|
}
|
|
19429
19551
|
|
|
19430
19552
|
// src/cli/make-config/wrap-up.ts
|
|
@@ -19457,7 +19579,7 @@ function printPageResult(page) {
|
|
|
19457
19579
|
);
|
|
19458
19580
|
}
|
|
19459
19581
|
}
|
|
19460
|
-
function printPlanResult(plan2) {
|
|
19582
|
+
function printPlanResult(plan2, platformTier) {
|
|
19461
19583
|
printSuccessToConsole(`Plan "${plan2.sku}" ${plan2.action} in ${plan2.file}.`);
|
|
19462
19584
|
if (plan2.needsBilling) {
|
|
19463
19585
|
printWarningToConsole(
|
|
@@ -19469,7 +19591,8 @@ function printPlanResult(plan2) {
|
|
|
19469
19591
|
"Payments go through Stripe: apitogo deploy asks for your Stripe secret key if the project has none yet."
|
|
19470
19592
|
);
|
|
19471
19593
|
}
|
|
19472
|
-
if (plan2.paid
|
|
19594
|
+
if (plan2.paid && (platformTier === void 0 || platformTier.toLowerCase() === "free"))
|
|
19595
|
+
printPaidPlanPlatformWarning();
|
|
19473
19596
|
}
|
|
19474
19597
|
function printPaidPlanPlatformWarning() {
|
|
19475
19598
|
printWarningToConsole(
|
|
@@ -19642,7 +19765,7 @@ async function promptMakeConfigInteractive(handlers) {
|
|
|
19642
19765
|
|
|
19643
19766
|
// src/cli/make-config/handler.ts
|
|
19644
19767
|
function hasMutatorFlags(argv) {
|
|
19645
|
-
return argv.title !== void 0 || argv.openapi !== void 0 || argv.source !== void 0 || argv.runtime !== void 0 || argv.runtimeVersion !== void 0 || argv.pageSlug !== void 0 || argv.plan !== void 0 || argv.patch !== void 0 || argv.json === true;
|
|
19768
|
+
return argv.title !== void 0 || argv.openapi !== void 0 || argv.source !== void 0 || argv.runtime !== void 0 || argv.runtimeVersion !== void 0 || argv.pageSlug !== void 0 || argv.plan !== void 0 || argv.planSku !== void 0 || argv.removePlan !== void 0 || argv.patch !== void 0 || argv.json === true;
|
|
19646
19769
|
}
|
|
19647
19770
|
function shouldRunInteractive(argv) {
|
|
19648
19771
|
if (argv.interactive === true) return true;
|
|
@@ -19656,6 +19779,15 @@ var describeError = (error) => {
|
|
|
19656
19779
|
return hint ? `${error.message}
|
|
19657
19780
|
${hint}` : error.message;
|
|
19658
19781
|
};
|
|
19782
|
+
async function platformTierOf(project) {
|
|
19783
|
+
const projectId = project.resolved.project.id;
|
|
19784
|
+
if (!projectId) return void 0;
|
|
19785
|
+
try {
|
|
19786
|
+
return (await new PlatformApiClient(loadConfig()).getSubscription(projectId)).planTier;
|
|
19787
|
+
} catch {
|
|
19788
|
+
return void 0;
|
|
19789
|
+
}
|
|
19790
|
+
}
|
|
19659
19791
|
async function applyConfigFlags(project, argv, changes) {
|
|
19660
19792
|
let next = project;
|
|
19661
19793
|
let openApi = null;
|
|
@@ -19704,6 +19836,7 @@ async function makeConfig(argv) {
|
|
|
19704
19836
|
let openApi = null;
|
|
19705
19837
|
let page = null;
|
|
19706
19838
|
let plan2 = null;
|
|
19839
|
+
let removed = null;
|
|
19707
19840
|
if (interactive) {
|
|
19708
19841
|
const prompted = await promptMakeConfigInteractive({
|
|
19709
19842
|
defaultSource: project.config.gateway?.origin.source,
|
|
@@ -19755,6 +19888,31 @@ async function makeConfig(argv) {
|
|
|
19755
19888
|
changes.push(plan2.action === "added" ? "plan_add" : "plan_update");
|
|
19756
19889
|
}
|
|
19757
19890
|
}
|
|
19891
|
+
if (argv.planSku !== void 0) {
|
|
19892
|
+
plan2 = await upsertPlan(
|
|
19893
|
+
project,
|
|
19894
|
+
await planFromFlags(project, {
|
|
19895
|
+
sku: argv.planSku,
|
|
19896
|
+
name: argv.planName,
|
|
19897
|
+
price: argv.planPrice,
|
|
19898
|
+
currency: argv.planCurrency,
|
|
19899
|
+
interval: argv.planInterval,
|
|
19900
|
+
rate: argv.planRate,
|
|
19901
|
+
quota: argv.planQuota,
|
|
19902
|
+
order: argv.planOrder,
|
|
19903
|
+
featured: argv.planFeatured
|
|
19904
|
+
})
|
|
19905
|
+
);
|
|
19906
|
+
changes.push(plan2.action === "added" ? "plan_add" : "plan_update");
|
|
19907
|
+
} else if (argv.planName !== void 0 || argv.planPrice !== void 0 || argv.planRate !== void 0 || argv.planQuota !== void 0 || argv.planOrder !== void 0 || argv.planFeatured !== void 0) {
|
|
19908
|
+
throw new ApitogoError("The --plan-* options need --plan-sku.", {
|
|
19909
|
+
developerHint: "Name the plan they change, for example --plan-sku pro --plan-price 19."
|
|
19910
|
+
});
|
|
19911
|
+
}
|
|
19912
|
+
if (argv.removePlan !== void 0) {
|
|
19913
|
+
removed = await removePlan(project, argv.removePlan);
|
|
19914
|
+
changes.push("plan_remove");
|
|
19915
|
+
}
|
|
19758
19916
|
}
|
|
19759
19917
|
const message = changes.length === 0 ? "Nothing to change." : (
|
|
19760
19918
|
// The change codes (page_add, plan_update, ...) are for scripts: they stay in the JSON output (API-126).
|
|
@@ -19771,6 +19929,7 @@ async function makeConfig(argv) {
|
|
|
19771
19929
|
openApi,
|
|
19772
19930
|
page,
|
|
19773
19931
|
plan: plan2,
|
|
19932
|
+
removed,
|
|
19774
19933
|
message
|
|
19775
19934
|
},
|
|
19776
19935
|
null,
|
|
@@ -19786,7 +19945,15 @@ async function makeConfig(argv) {
|
|
|
19786
19945
|
);
|
|
19787
19946
|
}
|
|
19788
19947
|
if (page) printPageResult(page);
|
|
19789
|
-
if (plan2)
|
|
19948
|
+
if (plan2)
|
|
19949
|
+
printPlanResult(
|
|
19950
|
+
plan2,
|
|
19951
|
+
plan2.paid ? await platformTierOf(project) : void 0
|
|
19952
|
+
);
|
|
19953
|
+
if (removed)
|
|
19954
|
+
printSuccessToConsole(
|
|
19955
|
+
`Removed plan "${removed.sku}" from ${removed.file}. Run \`apitogo plan\` to see what deploy does with it.`
|
|
19956
|
+
);
|
|
19790
19957
|
printResultToConsole(message);
|
|
19791
19958
|
}
|
|
19792
19959
|
} catch (error) {
|
|
@@ -19852,7 +20019,44 @@ function addConfigOptions(yargs2) {
|
|
|
19852
20019
|
type: "boolean",
|
|
19853
20020
|
default: false,
|
|
19854
20021
|
describe: "Update an existing page instead of adding one"
|
|
19855
|
-
}).option("plan", {
|
|
20022
|
+
}).option("plan-sku", {
|
|
20023
|
+
type: "string",
|
|
20024
|
+
describe: "Add or change the plan with this SKU in the plans file; the --plan-* options set its fields"
|
|
20025
|
+
}).option("plan-name", {
|
|
20026
|
+
type: "string",
|
|
20027
|
+
describe: "Name of --plan-sku on the pricing page (default: the SKU, capitalised)"
|
|
20028
|
+
}).option("plan-price", {
|
|
20029
|
+
type: "string",
|
|
20030
|
+
describe: "Price of --plan-sku per interval: an amount such as 19 (0 is free), or custom for a contact-us plan"
|
|
20031
|
+
}).option("plan-currency", {
|
|
20032
|
+
type: "string",
|
|
20033
|
+
describe: "Currency of --plan-price (default: usd)"
|
|
20034
|
+
}).option("plan-interval", {
|
|
20035
|
+
type: "string",
|
|
20036
|
+
choices: ["month", "year"],
|
|
20037
|
+
describe: "How often --plan-price is charged (default: month)"
|
|
20038
|
+
}).option("plan-rate", {
|
|
20039
|
+
type: "string",
|
|
20040
|
+
describe: "Rate limit of --plan-sku as <calls>/<seconds>, for example 60/60"
|
|
20041
|
+
}).option("plan-quota", {
|
|
20042
|
+
type: "string",
|
|
20043
|
+
describe: "Quota of --plan-sku as <calls>/<days>, for example 50000/30"
|
|
20044
|
+
}).option("plan-order", {
|
|
20045
|
+
type: "number",
|
|
20046
|
+
describe: "Place of --plan-sku on the pricing page: lower numbers come first"
|
|
20047
|
+
}).option("plan-featured", {
|
|
20048
|
+
type: "boolean",
|
|
20049
|
+
describe: 'Show --plan-sku as "Popular" on the pricing page (--no-plan-featured takes it off); no plan is unless you say so'
|
|
20050
|
+
}).option("remove-plan", {
|
|
20051
|
+
type: "string",
|
|
20052
|
+
describe: "Take the plan with this SKU out of the plans file"
|
|
20053
|
+
}).example(
|
|
20054
|
+
"$0 config --plan-sku pro --plan-name Pro --plan-price 19 --plan-rate 60/60 --plan-quota 50000/30",
|
|
20055
|
+
"A paid plan: 19 USD a month, 60 calls a minute, 50,000 calls per 30 days"
|
|
20056
|
+
).example(
|
|
20057
|
+
"$0 config --plan-sku enterprise --plan-price custom",
|
|
20058
|
+
"A contact-us plan"
|
|
20059
|
+
).example("$0 config --remove-plan enterprise", "Take a plan out").option("plan", {
|
|
19856
20060
|
type: "string",
|
|
19857
20061
|
describe: "JSON plan added to the plans file (plans.file, default apitogo/plans.json), or replacing the one with the same sku"
|
|
19858
20062
|
}).option("patch", {
|
|
@@ -20199,7 +20403,7 @@ var create_default = {
|
|
|
20199
20403
|
};
|
|
20200
20404
|
|
|
20201
20405
|
// src/cli/cmds/deploy.ts
|
|
20202
|
-
import
|
|
20406
|
+
import path84 from "node:path";
|
|
20203
20407
|
|
|
20204
20408
|
// src/cli/apply/handler.ts
|
|
20205
20409
|
import path77 from "node:path";
|
|
@@ -22667,6 +22871,25 @@ var PLATFORM_PLAN_CATALOG = [
|
|
|
22667
22871
|
]
|
|
22668
22872
|
}
|
|
22669
22873
|
];
|
|
22874
|
+
var CHOOSABLE = ["free", "starter", "professional"];
|
|
22875
|
+
function toPlanDisplays(plans) {
|
|
22876
|
+
return [...plans].sort((a, b) => a.sortOrder - b.sortOrder).map((plan2) => ({
|
|
22877
|
+
id: plan2.id,
|
|
22878
|
+
displayName: plan2.displayName,
|
|
22879
|
+
priceLabel: [plan2.priceAmount, plan2.pricePeriod].map((part) => part?.trim()).filter(Boolean).join(" "),
|
|
22880
|
+
summary: plan2.tagline,
|
|
22881
|
+
features: plan2.highlights,
|
|
22882
|
+
limitations: CHOOSABLE.includes(plan2.id) ? [] : [`Not chosen here: ${plan2.ctaLabel || "contact us"} on apitogo.com.`]
|
|
22883
|
+
}));
|
|
22884
|
+
}
|
|
22885
|
+
async function loadPlanDisplays(listPlatformPlans) {
|
|
22886
|
+
try {
|
|
22887
|
+
const plans = await listPlatformPlans();
|
|
22888
|
+
if (plans.length > 0) return toPlanDisplays(plans);
|
|
22889
|
+
} catch {
|
|
22890
|
+
}
|
|
22891
|
+
return PLATFORM_PLAN_CATALOG;
|
|
22892
|
+
}
|
|
22670
22893
|
var PLATFORM_PLAN_USER_PROMPT = "Which API ToGo platform plan do you want for this project \u2014 Free, Starter, or Professional?";
|
|
22671
22894
|
function formatFreeProjectsQuotaHint(quota) {
|
|
22672
22895
|
if (quota.remaining > 0) {
|
|
@@ -25637,9 +25860,9 @@ function printPlanOptions(catalog) {
|
|
|
25637
25860
|
}
|
|
25638
25861
|
}
|
|
25639
25862
|
}
|
|
25640
|
-
async function pickPlatformPlan() {
|
|
25863
|
+
async function pickPlatformPlan(loadPlans = async () => PLATFORM_PLAN_CATALOG) {
|
|
25641
25864
|
printDiagnosticsToConsole("Which API ToGo plan should this project use?");
|
|
25642
|
-
printPlanOptions(
|
|
25865
|
+
printPlanOptions(await loadPlans());
|
|
25643
25866
|
printDiagnosticsToConsole("");
|
|
25644
25867
|
return askPlanChoice();
|
|
25645
25868
|
}
|
|
@@ -25982,7 +26205,7 @@ async function apply(argv) {
|
|
|
25982
26205
|
}
|
|
25983
26206
|
|
|
25984
26207
|
// src/cli/deploy/command.ts
|
|
25985
|
-
import
|
|
26208
|
+
import path83 from "node:path";
|
|
25986
26209
|
init_invariant();
|
|
25987
26210
|
|
|
25988
26211
|
// src/cli/project/create-handler.ts
|
|
@@ -26201,7 +26424,11 @@ async function createLinkedProject(argv, dir, options = {}) {
|
|
|
26201
26424
|
assertConfigChecksOut(await runCheckV2(dir));
|
|
26202
26425
|
let plan2 = argv.plan;
|
|
26203
26426
|
if (!plan2 && !json && process.stdin.isTTY && process.stdout.isTTY && !loadConfigV2(dir).resolved.project.id) {
|
|
26204
|
-
const picked = await pickPlatformPlan(
|
|
26427
|
+
const picked = await pickPlatformPlan(
|
|
26428
|
+
() => loadPlanDisplays(
|
|
26429
|
+
() => new PlatformApiClient(loadConfig()).listPlatformPlans()
|
|
26430
|
+
)
|
|
26431
|
+
);
|
|
26205
26432
|
if (!picked) {
|
|
26206
26433
|
printResultToConsole(
|
|
26207
26434
|
"No plan selected. Run `apitogo project create --plan free|starter|professional` when you are ready."
|
|
@@ -26355,6 +26582,10 @@ async function createProject2(argv) {
|
|
|
26355
26582
|
}
|
|
26356
26583
|
}
|
|
26357
26584
|
|
|
26585
|
+
// src/cli/secrets/handler.ts
|
|
26586
|
+
import path79 from "node:path";
|
|
26587
|
+
init_invariant();
|
|
26588
|
+
|
|
26358
26589
|
// src/cli/secrets/input.ts
|
|
26359
26590
|
import { createInterface as createInterface6 } from "node:readline/promises";
|
|
26360
26591
|
var CTRL_C = "";
|
|
@@ -26406,8 +26637,164 @@ async function confirm(question) {
|
|
|
26406
26637
|
}
|
|
26407
26638
|
}
|
|
26408
26639
|
|
|
26640
|
+
// src/cli/secrets/handler.ts
|
|
26641
|
+
var SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
26642
|
+
var SecretsError = class extends ApitogoError {
|
|
26643
|
+
constructor(message, developerHint) {
|
|
26644
|
+
super(message, { developerHint, title: "Cannot change secrets" });
|
|
26645
|
+
this.name = "SecretsError";
|
|
26646
|
+
}
|
|
26647
|
+
};
|
|
26648
|
+
async function resolveProjectId2(argv, deps) {
|
|
26649
|
+
const explicit = argv.projectId?.trim();
|
|
26650
|
+
if (explicit) return explicit;
|
|
26651
|
+
const dir = path79.resolve(process.cwd(), argv.dir ?? ".");
|
|
26652
|
+
if (await isConfigV2(dir)) {
|
|
26653
|
+
const id = loadConfigV2(dir).resolved.project.id;
|
|
26654
|
+
if (id) return id;
|
|
26655
|
+
}
|
|
26656
|
+
if (deps.envProjectId?.trim()) return deps.envProjectId.trim();
|
|
26657
|
+
throw new SecretsError(
|
|
26658
|
+
"This folder is not linked to a project yet.",
|
|
26659
|
+
"Pass --project-id <id>, run `apitogo deploy` once, or set APITOGO_DEFAULT_PROJECT_ID."
|
|
26660
|
+
);
|
|
26661
|
+
}
|
|
26662
|
+
function requireName(name, action) {
|
|
26663
|
+
if (!name) {
|
|
26664
|
+
throw new SecretsError(
|
|
26665
|
+
`secrets ${action} needs a name.`,
|
|
26666
|
+
`For example: apitogo secrets ${action} STRIPE_API_KEY`
|
|
26667
|
+
);
|
|
26668
|
+
}
|
|
26669
|
+
if (!SECRET_NAME.test(name)) {
|
|
26670
|
+
throw new SecretsError(
|
|
26671
|
+
`"${name}" is not a valid secret name.`,
|
|
26672
|
+
"Use letters, digits and underscores, starting with a letter or underscore (at most 128 characters)."
|
|
26673
|
+
);
|
|
26674
|
+
}
|
|
26675
|
+
return name;
|
|
26676
|
+
}
|
|
26677
|
+
async function runSecrets(argv, deps) {
|
|
26678
|
+
if (!["set", "list", "rm"].includes(argv.action)) {
|
|
26679
|
+
throw new SecretsError(
|
|
26680
|
+
`Unknown action: ${argv.action}.`,
|
|
26681
|
+
"Use set, list or rm."
|
|
26682
|
+
);
|
|
26683
|
+
}
|
|
26684
|
+
if (argv.action === "list") {
|
|
26685
|
+
const projectId2 = await resolveProjectId2(argv, deps);
|
|
26686
|
+
const { secrets: secrets2 } = await deps.client.listSecrets(projectId2);
|
|
26687
|
+
return { action: "list", projectId: projectId2, secrets: secrets2 };
|
|
26688
|
+
}
|
|
26689
|
+
const name = requireName(argv.name, argv.action);
|
|
26690
|
+
const projectId = await resolveProjectId2(argv, deps);
|
|
26691
|
+
if (argv.action === "rm") {
|
|
26692
|
+
if (argv.yes !== true) {
|
|
26693
|
+
if (!deps.isTerminal)
|
|
26694
|
+
throw new SecretsError(
|
|
26695
|
+
"This is not an interactive terminal.",
|
|
26696
|
+
"Pass --yes to remove the secret."
|
|
26697
|
+
);
|
|
26698
|
+
if (!await deps.confirm(`Remove secret ${name}? (y/N) `))
|
|
26699
|
+
throw new SecretsError("Cancelled. Nothing was removed.");
|
|
26700
|
+
}
|
|
26701
|
+
await deps.client.deleteSecret(projectId, name);
|
|
26702
|
+
return { action: "rm", projectId, name };
|
|
26703
|
+
}
|
|
26704
|
+
let value;
|
|
26705
|
+
if (deps.isTerminal) {
|
|
26706
|
+
value = await deps.askValue(`Value for ${name} (hidden): `);
|
|
26707
|
+
const again = await deps.askValue("Type it again: ");
|
|
26708
|
+
if (again !== value)
|
|
26709
|
+
throw new SecretsError(
|
|
26710
|
+
"The two values are not the same. Nothing was saved."
|
|
26711
|
+
);
|
|
26712
|
+
} else {
|
|
26713
|
+
value = await deps.readPiped();
|
|
26714
|
+
}
|
|
26715
|
+
if (value.length === 0)
|
|
26716
|
+
throw new SecretsError("The value is empty. Nothing was saved.");
|
|
26717
|
+
const result = await deps.client.setSecret(projectId, name, value);
|
|
26718
|
+
const wiredTo = await wireKnownSecret(argv, result.name);
|
|
26719
|
+
return {
|
|
26720
|
+
action: "set",
|
|
26721
|
+
projectId,
|
|
26722
|
+
name: result.name,
|
|
26723
|
+
created: result.created,
|
|
26724
|
+
...wiredTo ? { wiredTo } : {}
|
|
26725
|
+
};
|
|
26726
|
+
}
|
|
26727
|
+
var KNOWN_SECRET_PLACES = {
|
|
26728
|
+
STRIPE_WEBHOOK: {
|
|
26729
|
+
path: "plans.billing.webhookSecret",
|
|
26730
|
+
patch: { plans: { billing: { webhookSecret: "secret:STRIPE_WEBHOOK" } } }
|
|
26731
|
+
}
|
|
26732
|
+
};
|
|
26733
|
+
async function wireKnownSecret(argv, name) {
|
|
26734
|
+
const place = KNOWN_SECRET_PLACES[name.toUpperCase()];
|
|
26735
|
+
if (!place) return void 0;
|
|
26736
|
+
const dir = path79.resolve(process.cwd(), argv.dir ?? ".");
|
|
26737
|
+
if (!await isConfigV2(dir)) return void 0;
|
|
26738
|
+
const project = await openConfigProject(dir);
|
|
26739
|
+
const plans = project.resolved.plans;
|
|
26740
|
+
if (!plans || plans.billing.webhookSecret) return void 0;
|
|
26741
|
+
await saveConfigProject(applyConfigPatch(project, place.patch));
|
|
26742
|
+
return place.path;
|
|
26743
|
+
}
|
|
26744
|
+
function formatSecrets(result) {
|
|
26745
|
+
if (result.action === "list") {
|
|
26746
|
+
return result.secrets.length === 0 ? ["No secrets are set for this project."] : result.secrets.map((s) => `${s.name} (updated ${s.updatedAtUtc})`);
|
|
26747
|
+
}
|
|
26748
|
+
if (result.action === "set") {
|
|
26749
|
+
return [
|
|
26750
|
+
`Secret ${result.name} ${result.created ? "created" : "replaced"}.`,
|
|
26751
|
+
...result.wiredTo ? [
|
|
26752
|
+
`${result.wiredTo} now points to it. Run \`apitogo deploy --only plans\` to apply it.`
|
|
26753
|
+
] : []
|
|
26754
|
+
];
|
|
26755
|
+
}
|
|
26756
|
+
return [`Secret ${result.name} removed.`];
|
|
26757
|
+
}
|
|
26758
|
+
async function secrets(argv) {
|
|
26759
|
+
const json = argv.json === true;
|
|
26760
|
+
try {
|
|
26761
|
+
const config2 = loadConfig();
|
|
26762
|
+
const result = await runSecrets(argv, {
|
|
26763
|
+
client: new PlatformApiClient(config2),
|
|
26764
|
+
envProjectId: config2.defaultProjectId,
|
|
26765
|
+
isTerminal: process.stdin.isTTY === true && !json,
|
|
26766
|
+
askValue: (prompt) => readHidden(prompt),
|
|
26767
|
+
readPiped: () => readStdin(),
|
|
26768
|
+
confirm
|
|
26769
|
+
});
|
|
26770
|
+
if (json) {
|
|
26771
|
+
console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));
|
|
26772
|
+
return;
|
|
26773
|
+
}
|
|
26774
|
+
for (const line2 of formatSecrets(result)) printDiagnosticsToConsole(line2);
|
|
26775
|
+
if (result.action !== "list") printResultToConsole("Done.");
|
|
26776
|
+
} catch (error) {
|
|
26777
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26778
|
+
const hint = error instanceof ApitogoError ? error.developerHint : void 0;
|
|
26779
|
+
if (json) {
|
|
26780
|
+
console.log(
|
|
26781
|
+
JSON.stringify({
|
|
26782
|
+
status: "failed",
|
|
26783
|
+
error: message,
|
|
26784
|
+
hint: hint ?? null
|
|
26785
|
+
})
|
|
26786
|
+
);
|
|
26787
|
+
process.exitCode = 1;
|
|
26788
|
+
return;
|
|
26789
|
+
}
|
|
26790
|
+
await printCriticalFailureToConsoleAndExit(
|
|
26791
|
+
hint ? `${message} ${hint}` : message
|
|
26792
|
+
);
|
|
26793
|
+
}
|
|
26794
|
+
}
|
|
26795
|
+
|
|
26409
26796
|
// src/cli/deploy/stripe-key.ts
|
|
26410
|
-
import
|
|
26797
|
+
import path80 from "node:path";
|
|
26411
26798
|
init_invariant();
|
|
26412
26799
|
var StripeKeyError = class extends ApitogoError {
|
|
26413
26800
|
constructor(message, developerHint) {
|
|
@@ -26425,7 +26812,7 @@ async function ensureStripeKey(argv, deps) {
|
|
|
26425
26812
|
if (!wantsPlans(argv.only)) return { status: "not-needed" };
|
|
26426
26813
|
let resolved;
|
|
26427
26814
|
try {
|
|
26428
|
-
resolved = loadConfigV2(
|
|
26815
|
+
resolved = loadConfigV2(path80.resolve(process.cwd(), argv.dir)).resolved;
|
|
26429
26816
|
} catch {
|
|
26430
26817
|
return { status: "not-needed" };
|
|
26431
26818
|
}
|
|
@@ -26467,13 +26854,11 @@ async function ensureStripeKey(argv, deps) {
|
|
|
26467
26854
|
return { status: "saved", name };
|
|
26468
26855
|
}
|
|
26469
26856
|
|
|
26470
|
-
// src/cli/deploy/
|
|
26471
|
-
import { rm as rm11 } from "node:fs/promises";
|
|
26857
|
+
// src/cli/deploy/stripe-webhook.ts
|
|
26472
26858
|
init_invariant();
|
|
26473
26859
|
|
|
26474
|
-
// src/cli/
|
|
26475
|
-
|
|
26476
|
-
import path80 from "node:path";
|
|
26860
|
+
// src/cli/deploy/plans.ts
|
|
26861
|
+
init_invariant();
|
|
26477
26862
|
|
|
26478
26863
|
// src/cli/deploy/gateway.ts
|
|
26479
26864
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -26884,723 +27269,814 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
26884
27269
|
};
|
|
26885
27270
|
}
|
|
26886
27271
|
|
|
26887
|
-
// src/cli/deploy/
|
|
26888
|
-
|
|
26889
|
-
var IdentityDeployError = class extends ApitogoError {
|
|
27272
|
+
// src/cli/deploy/plans.ts
|
|
27273
|
+
var PlansDeployError = class extends ApitogoError {
|
|
26890
27274
|
constructor(code, message, developerHint) {
|
|
26891
|
-
super(message, { developerHint, title: "Cannot deploy
|
|
27275
|
+
super(message, { developerHint, title: "Cannot deploy plans" });
|
|
26892
27276
|
this.code = code;
|
|
26893
|
-
this.name = "
|
|
27277
|
+
this.name = "PlansDeployError";
|
|
26894
27278
|
}
|
|
26895
27279
|
};
|
|
26896
|
-
var
|
|
26897
|
-
|
|
26898
|
-
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
26902
|
-
|
|
26903
|
-
|
|
26904
|
-
}
|
|
26905
|
-
|
|
26906
|
-
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
26910
|
-
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
"
|
|
26915
|
-
IDENTITY_CONNECTIONS_STEP
|
|
26916
|
-
)?.output?.ids;
|
|
26917
|
-
return ids ? ids.split(",").filter(Boolean) : [];
|
|
26918
|
-
}
|
|
26919
|
-
function redirectAddressesOf(output4) {
|
|
26920
|
-
return Object.entries(output4 ?? {}).filter(([key]) => key.startsWith("redirect.")).map(([key, redirectUri]) => ({
|
|
26921
|
-
id: key.slice("redirect.".length),
|
|
26922
|
-
redirectUri
|
|
26923
|
-
}));
|
|
26924
|
-
}
|
|
26925
|
-
function connectionBody(connection) {
|
|
26926
|
-
return {
|
|
26927
|
-
authority: connection.authority,
|
|
26928
|
-
clientId: connection.clientId,
|
|
26929
|
-
clientSecret: connection.clientSecret,
|
|
26930
|
-
scopes: connection.scopes,
|
|
26931
|
-
displayName: connection.displayName
|
|
26932
|
-
};
|
|
26933
|
-
}
|
|
26934
|
-
function referencedSecretNames(connections) {
|
|
26935
|
-
return [
|
|
26936
|
-
...new Set(
|
|
26937
|
-
connections.map(
|
|
26938
|
-
(connection) => connection.clientSecret.slice(SECRET_PREFIX.length)
|
|
26939
|
-
)
|
|
26940
|
-
)
|
|
27280
|
+
var PLANS_STEPS = [
|
|
27281
|
+
"catalog",
|
|
27282
|
+
"backend",
|
|
27283
|
+
"billing",
|
|
27284
|
+
"identity-link"
|
|
27285
|
+
];
|
|
27286
|
+
var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27287
|
+
var BACKEND_MODULE = "dev-portal";
|
|
27288
|
+
var PLANS_CAPABILITIES = { catalog: true, keys: true, usage: true };
|
|
27289
|
+
var STRIPE_WEBHOOK_PATH = "/api/v1/webhooks/stripe";
|
|
27290
|
+
var STRIPE_WEBHOOK_EVENTS2 = [
|
|
27291
|
+
"checkout.session.completed",
|
|
27292
|
+
"customer.subscription.updated",
|
|
27293
|
+
"customer.subscription.deleted",
|
|
27294
|
+
"invoice.payment_failed"
|
|
27295
|
+
];
|
|
27296
|
+
function billingNotes(billing) {
|
|
27297
|
+
const notes = [
|
|
27298
|
+
billing.mode === "live" ? "Stripe: LIVE mode. Customers pay with real money." : "Stripe: test mode. Use Stripe's test cards; no real money moves."
|
|
26941
27299
|
];
|
|
27300
|
+
notes.push(
|
|
27301
|
+
billing.hasWebhookSecret ? `Stripe webhook endpoint: ${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.`
|
|
27302
|
+
);
|
|
27303
|
+
return notes;
|
|
26942
27304
|
}
|
|
26943
|
-
function
|
|
26944
|
-
if (!
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
26948
|
-
|
|
26949
|
-
|
|
26950
|
-
|
|
26951
|
-
|
|
26952
|
-
|
|
26953
|
-
|
|
26954
|
-
|
|
26955
|
-
if (!identity) return "identity is not in the config";
|
|
26956
|
-
if (identity.provider === "oidc" && (!identity.issuer || !identity.clientId))
|
|
26957
|
-
return 'identity.provider "oidc" needs issuer and clientId';
|
|
26958
|
-
return void 0;
|
|
27305
|
+
function webhookStatusLine(status2, now = Date.now()) {
|
|
27306
|
+
if (!status2) return void 0;
|
|
27307
|
+
if (!status2.webhookSecretConfigured)
|
|
27308
|
+
return 'Stripe webhook: no signing secret yet, so a payment does not activate a plan. Add the endpoint in Stripe, then run "apitogo secrets set STRIPE_WEBHOOK" and deploy.';
|
|
27309
|
+
if (!status2.lastEventAtUtc)
|
|
27310
|
+
return "Stripe webhook: set up, but no event has arrived yet. Make a test payment to see that Stripe reaches it.";
|
|
27311
|
+
const minutes = Math.max(
|
|
27312
|
+
0,
|
|
27313
|
+
Math.round((now - Date.parse(status2.lastEventAtUtc)) / 6e4)
|
|
27314
|
+
);
|
|
27315
|
+
const ago = minutes < 1 ? "just now" : minutes < 120 ? `${minutes} min ago` : minutes < 48 * 60 ? `${Math.round(minutes / 60)} h ago` : `${Math.round(minutes / (24 * 60))} days ago`;
|
|
27316
|
+
return `Stripe webhook: verified (last event ${ago}${status2.lastEventType ? `, ${status2.lastEventType}` : ""})`;
|
|
26959
27317
|
}
|
|
26960
|
-
function
|
|
27318
|
+
async function plansInputs(resolved, projectId) {
|
|
27319
|
+
const plans = resolved.plans;
|
|
27320
|
+
if (!plans) throw new Error("plans is not in the config.");
|
|
27321
|
+
const billing = plans.billing;
|
|
27322
|
+
const filePlans = await readPlansFile(plans.file);
|
|
27323
|
+
const wanted = toPlatformPlans(filePlans, {
|
|
27324
|
+
billing: billing.provider === "stripe"
|
|
27325
|
+
});
|
|
27326
|
+
const manifest = await readManifest(resolved.state);
|
|
27327
|
+
const ledger = await readLedger(resolved.state);
|
|
27328
|
+
const billingOn = billing.provider === "stripe" && filePlans.some((plan2) => isPaidPlan(plan2));
|
|
27329
|
+
const billingStep = billingOn || stepOf(ledger, "plans", "billing") !== void 0;
|
|
27330
|
+
const identity = manifest?.services.identity;
|
|
27331
|
+
const ownIdentity = Boolean(
|
|
27332
|
+
identity?.provider === "apitogo" && identity.issuer
|
|
27333
|
+
);
|
|
26961
27334
|
return {
|
|
26962
|
-
|
|
26963
|
-
|
|
27335
|
+
wanted,
|
|
27336
|
+
filePlans,
|
|
27337
|
+
manifest,
|
|
27338
|
+
ownIdentity,
|
|
27339
|
+
catalogHash: stableJson({ projectId, plans: wanted }),
|
|
27340
|
+
backendHash: stableJson({ projectId }),
|
|
27341
|
+
billingOn,
|
|
27342
|
+
billingStep,
|
|
27343
|
+
// Only the references to the project's secrets: a rotated value is applied with --force.
|
|
27344
|
+
billingHash: stableJson({
|
|
27345
|
+
projectId,
|
|
27346
|
+
provider: billingOn ? "stripe" : "none",
|
|
27347
|
+
apiKey: billingOn ? billing.apiKey : void 0,
|
|
27348
|
+
webhookSecret: billingOn ? billing.webhookSecret : void 0,
|
|
27349
|
+
successUrl: billingOn ? billing.successUrl : void 0,
|
|
27350
|
+
cancelUrl: billingOn ? billing.cancelUrl : void 0
|
|
27351
|
+
}),
|
|
27352
|
+
linkHash: stableJson({
|
|
27353
|
+
projectId,
|
|
27354
|
+
issuer: identity?.issuer,
|
|
27355
|
+
clientId: identity?.clientId
|
|
27356
|
+
})
|
|
26964
27357
|
};
|
|
26965
27358
|
}
|
|
26966
|
-
function
|
|
26967
|
-
return
|
|
26968
|
-
resolved,
|
|
26969
|
-
`${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}`
|
|
26970
|
-
);
|
|
26971
|
-
}
|
|
26972
|
-
function identityInputsHash(projectId, body) {
|
|
26973
|
-
return stableJson({ projectId, body });
|
|
26974
|
-
}
|
|
26975
|
-
async function portalUrlOf(resolved, projectId, client) {
|
|
26976
|
-
const known = (await readManifest(resolved.state))?.services.portal?.url;
|
|
26977
|
-
const url2 = known ?? resolveSwaUrl(await client.getProject(projectId));
|
|
26978
|
-
if (!url2)
|
|
26979
|
-
throw new IdentityDeployError(
|
|
26980
|
-
"IDENTITY_PORTAL_ADDRESS_UNKNOWN",
|
|
26981
|
-
"The portal has no public address yet, and the identity needs it for the sign-in redirect.",
|
|
26982
|
-
"The address comes with the project's hosting. Run `apitogo status`: while hosting is still being prepared, wait a few minutes and run `apitogo deploy` again. Once hosting is ready, `apitogo deploy --only portal` gives the portal its address, then deploy identity."
|
|
26983
|
-
);
|
|
26984
|
-
return url2;
|
|
27359
|
+
function plansNotDeployableReason(resolved) {
|
|
27360
|
+
return resolved.plans ? void 0 : "plans is not in the config";
|
|
26985
27361
|
}
|
|
26986
|
-
function
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26990
|
-
|
|
26991
|
-
|
|
26992
|
-
|
|
27362
|
+
async function allActionKeys(client, projectId) {
|
|
27363
|
+
const keys = [];
|
|
27364
|
+
try {
|
|
27365
|
+
for (let page = 1; ; page++) {
|
|
27366
|
+
const result = await client.listGatewayActions(projectId, page, 100);
|
|
27367
|
+
keys.push(...result.items.map((item) => item.actionKey));
|
|
27368
|
+
if (result.items.length === 0 || page * result.pageSize >= result.totalCount)
|
|
27369
|
+
break;
|
|
27370
|
+
}
|
|
27371
|
+
} catch {
|
|
27372
|
+
return [];
|
|
26993
27373
|
}
|
|
27374
|
+
return keys;
|
|
27375
|
+
}
|
|
27376
|
+
function refused(error) {
|
|
26994
27377
|
if (error instanceof PlatformApiError && error.status === 400) {
|
|
26995
27378
|
const lines = describePlatformProblem(error);
|
|
26996
|
-
throw new
|
|
26997
|
-
"
|
|
26998
|
-
`The platform refused
|
|
27379
|
+
throw new PlansDeployError(
|
|
27380
|
+
"PLANS_REJECTED",
|
|
27381
|
+
`The platform refused a plan:
|
|
26999
27382
|
${lines.join("\n ") || error.message}`,
|
|
27000
|
-
"Fix the
|
|
27383
|
+
"Fix the plan in the plans file and run deploy again."
|
|
27001
27384
|
);
|
|
27002
27385
|
}
|
|
27003
27386
|
throw error;
|
|
27004
27387
|
}
|
|
27005
|
-
function
|
|
27006
|
-
if (error instanceof PlatformApiError && error.status === 409) {
|
|
27007
|
-
throw new IdentityDeployError(
|
|
27008
|
-
"IDENTITY_CONNECTION_REJECTED",
|
|
27009
|
-
`The identity is not active yet, so connection "${id}" cannot be saved.`,
|
|
27010
|
-
"Run deploy again once the identity is Active."
|
|
27011
|
-
);
|
|
27012
|
-
}
|
|
27388
|
+
function billingRefused(error) {
|
|
27013
27389
|
if (error instanceof PlatformApiError && error.status === 400) {
|
|
27014
27390
|
const lines = describePlatformProblem(error);
|
|
27015
|
-
throw new
|
|
27016
|
-
"
|
|
27017
|
-
`The platform refused
|
|
27018
|
-
|
|
27019
|
-
"Fix the connection in apitogo.config.json (identity.connections) and run deploy again."
|
|
27391
|
+
throw new PlansDeployError(
|
|
27392
|
+
"PLANS_BILLING_REJECTED",
|
|
27393
|
+
`The platform refused the billing settings: ${lines.join(" ") || error.message}`,
|
|
27394
|
+
"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_."
|
|
27020
27395
|
);
|
|
27021
27396
|
}
|
|
27022
27397
|
throw error;
|
|
27023
27398
|
}
|
|
27024
|
-
async function
|
|
27025
|
-
const
|
|
27026
|
-
|
|
27027
|
-
const clientId = identity?.clientId;
|
|
27028
|
-
if (!issuer || !clientId)
|
|
27029
|
-
throw new Error("identity.issuer and identity.clientId are needed.");
|
|
27030
|
-
const known = (await readManifest(resolved.state))?.services.identity;
|
|
27031
|
-
const changed = known?.provider !== "oidc" || known.issuer !== issuer || known.clientId !== clientId;
|
|
27032
|
-
if (changed)
|
|
27033
|
-
await writeManifest(
|
|
27034
|
-
resolved.state,
|
|
27035
|
-
withIdentityEntry(await readManifest(resolved.state), {
|
|
27036
|
-
projectId,
|
|
27037
|
-
projectName: resolved.project.name,
|
|
27038
|
-
provider: "oidc",
|
|
27039
|
-
issuer,
|
|
27040
|
-
clientId,
|
|
27041
|
-
nativeSignup: false
|
|
27042
|
-
})
|
|
27043
|
-
);
|
|
27044
|
-
return {
|
|
27045
|
-
status: changed ? "deployed" : "unchanged",
|
|
27046
|
-
steps: [],
|
|
27047
|
-
issuer,
|
|
27048
|
-
clientId
|
|
27049
|
-
};
|
|
27050
|
-
}
|
|
27051
|
-
async function deployIdentity(resolved, projectId, deps, options = {}) {
|
|
27052
|
-
if (resolved.identity?.provider === "oidc")
|
|
27053
|
-
return recordOwnProvider(resolved, projectId);
|
|
27399
|
+
async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
27400
|
+
const plans = resolved.plans;
|
|
27401
|
+
if (!plans) throw new Error("plans is not in the config.");
|
|
27054
27402
|
const client = deps.client;
|
|
27055
27403
|
const sleep5 = deps.sleep ?? defaultSleep3;
|
|
27056
27404
|
const now = deps.now ?? Date.now;
|
|
27057
|
-
const timeoutMs = deps.activateTimeoutMs ?? 12e4;
|
|
27058
27405
|
const intervalMs = deps.pollIntervalMs ?? 2e3;
|
|
27059
|
-
const
|
|
27060
|
-
const
|
|
27061
|
-
const
|
|
27062
|
-
|
|
27063
|
-
|
|
27064
|
-
|
|
27065
|
-
|
|
27066
|
-
|
|
27067
|
-
|
|
27068
|
-
|
|
27069
|
-
|
|
27070
|
-
|
|
27071
|
-
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
},
|
|
27076
|
-
// An identity that was removed, or that failed, is not a result worth keeping.
|
|
27077
|
-
stillValid: async () => {
|
|
27078
|
-
const found = await client.getProjectIdentity(projectId);
|
|
27079
|
-
return found !== null && found.status !== "Failed" && found.status !== "Released" && found.status !== "Releasing";
|
|
27080
|
-
}
|
|
27081
|
-
};
|
|
27082
|
-
const activateStep = {
|
|
27083
|
-
name: "activate",
|
|
27084
|
-
hash: () => inputsHash,
|
|
27406
|
+
const backendTimeoutMs = deps.backendTimeoutMs ?? 9e5;
|
|
27407
|
+
const linkTimeoutMs = deps.linkTimeoutMs ?? 6e5;
|
|
27408
|
+
const {
|
|
27409
|
+
wanted,
|
|
27410
|
+
manifest,
|
|
27411
|
+
ownIdentity,
|
|
27412
|
+
billingOn,
|
|
27413
|
+
billingStep,
|
|
27414
|
+
billingHash,
|
|
27415
|
+
catalogHash,
|
|
27416
|
+
backendHash,
|
|
27417
|
+
linkHash
|
|
27418
|
+
} = await plansInputs(resolved, projectId);
|
|
27419
|
+
const catalogStep = {
|
|
27420
|
+
name: "catalog",
|
|
27421
|
+
hash: () => catalogHash,
|
|
27085
27422
|
run: async () => {
|
|
27086
|
-
|
|
27087
|
-
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27092
|
-
|
|
27093
|
-
|
|
27094
|
-
|
|
27095
|
-
|
|
27096
|
-
|
|
27097
|
-
|
|
27098
|
-
|
|
27099
|
-
|
|
27100
|
-
|
|
27101
|
-
|
|
27102
|
-
|
|
27103
|
-
|
|
27104
|
-
|
|
27105
|
-
|
|
27106
|
-
|
|
27107
|
-
|
|
27108
|
-
|
|
27423
|
+
if (wanted.length === 0) return { created: "0", updated: "0" };
|
|
27424
|
+
const fallback = wanted.some((plan2) => plan2.includedActionKeys === null) ? await allActionKeys(client, projectId) : [];
|
|
27425
|
+
const bodies = wanted.map((plan2) => ({
|
|
27426
|
+
...plan2,
|
|
27427
|
+
includedActionKeys: plan2.includedActionKeys ?? fallback
|
|
27428
|
+
}));
|
|
27429
|
+
const checked = await client.validateGatewayPlans(bodies, { projectId });
|
|
27430
|
+
if (!checked.valid)
|
|
27431
|
+
throw new PlansDeployError(
|
|
27432
|
+
"PLANS_REJECTED",
|
|
27433
|
+
`The platform refused the plans:
|
|
27434
|
+
${checked.errors.map((issue4) => `${issue4.field}: ${issue4.message}`).join("\n ")}`,
|
|
27435
|
+
"Fix the listed problems in the plans file and run deploy again."
|
|
27436
|
+
);
|
|
27437
|
+
const existing = new Map(
|
|
27438
|
+
(await client.listGatewayPlans(projectId)).map((plan2) => [
|
|
27439
|
+
plan2.sku.toLowerCase(),
|
|
27440
|
+
plan2
|
|
27441
|
+
])
|
|
27442
|
+
);
|
|
27443
|
+
let created = 0;
|
|
27444
|
+
let updated = 0;
|
|
27445
|
+
let archived = 0;
|
|
27446
|
+
const backendSync = /* @__PURE__ */ new Set();
|
|
27447
|
+
const noteSync = (plan2) => {
|
|
27448
|
+
if (plan2.plansBackendSync) backendSync.add(plan2.plansBackendSync);
|
|
27449
|
+
};
|
|
27450
|
+
for (const body of bodies) {
|
|
27451
|
+
const found = existing.get(body.sku);
|
|
27452
|
+
try {
|
|
27453
|
+
if (found) {
|
|
27454
|
+
const result = await client.updateGatewayPlan(projectId, found.id, {
|
|
27455
|
+
displayName: body.displayName,
|
|
27456
|
+
isActive: true,
|
|
27457
|
+
limitsJson: body.limitsJson ?? null,
|
|
27458
|
+
includedActionKeys: body.includedActionKeys,
|
|
27459
|
+
priceAmount: body.priceAmount,
|
|
27460
|
+
pricingModel: body.pricingModel,
|
|
27461
|
+
priceLabel: body.priceLabel,
|
|
27462
|
+
priceCurrency: body.priceCurrency,
|
|
27463
|
+
billingPeriod: body.billingPeriod,
|
|
27464
|
+
sortOrder: body.sortOrder
|
|
27465
|
+
});
|
|
27466
|
+
noteSync(result);
|
|
27467
|
+
updated++;
|
|
27468
|
+
} else {
|
|
27469
|
+
noteSync(await client.createGatewayPlan(projectId, body));
|
|
27470
|
+
created++;
|
|
27471
|
+
}
|
|
27472
|
+
} catch (error) {
|
|
27473
|
+
refused(error);
|
|
27474
|
+
}
|
|
27475
|
+
}
|
|
27476
|
+
const kept = new Set(bodies.map((body) => body.sku));
|
|
27477
|
+
for (const plan2 of existing.values()) {
|
|
27478
|
+
if (!plan2.isActive || kept.has(plan2.sku.toLowerCase())) continue;
|
|
27479
|
+
try {
|
|
27480
|
+
const result = await client.updateGatewayPlan(projectId, plan2.id, {
|
|
27481
|
+
displayName: plan2.displayName,
|
|
27482
|
+
isActive: false,
|
|
27483
|
+
limitsJson: plan2.limitsJson ?? null,
|
|
27484
|
+
includedActionKeys: plan2.includedActions.map(
|
|
27485
|
+
(action) => action.actionKey
|
|
27486
|
+
),
|
|
27487
|
+
priceAmount: plan2.priceAmount,
|
|
27488
|
+
pricingModel: plan2.pricingModel,
|
|
27489
|
+
priceCurrency: plan2.priceCurrency,
|
|
27490
|
+
billingPeriod: plan2.billingPeriod,
|
|
27491
|
+
sortOrder: plan2.sortOrder
|
|
27492
|
+
});
|
|
27493
|
+
noteSync(result);
|
|
27494
|
+
archived++;
|
|
27495
|
+
} catch (error) {
|
|
27496
|
+
refused(error);
|
|
27497
|
+
}
|
|
27109
27498
|
}
|
|
27499
|
+
return {
|
|
27500
|
+
created: String(created),
|
|
27501
|
+
updated: String(updated),
|
|
27502
|
+
archived: String(archived),
|
|
27503
|
+
...backendSync.size > 0 ? { backendSync: [...backendSync].sort().join(",") } : {}
|
|
27504
|
+
};
|
|
27110
27505
|
},
|
|
27111
|
-
|
|
27506
|
+
// A plan someone deleted on the platform is not a result worth keeping, and neither is one that should be
|
|
27507
|
+
// archived and is not yet.
|
|
27508
|
+
stillValid: async () => {
|
|
27509
|
+
if (wanted.length === 0) return true;
|
|
27510
|
+
const active2 = (await client.listGatewayPlans(projectId)).filter(
|
|
27511
|
+
(plan2) => plan2.isActive
|
|
27512
|
+
);
|
|
27513
|
+
const present = new Set(active2.map((plan2) => plan2.sku.toLowerCase()));
|
|
27514
|
+
return wanted.every((plan2) => present.has(plan2.sku)) && active2.every(
|
|
27515
|
+
(plan2) => wanted.some((w) => w.sku === plan2.sku.toLowerCase())
|
|
27516
|
+
);
|
|
27517
|
+
}
|
|
27112
27518
|
};
|
|
27113
|
-
const
|
|
27114
|
-
const
|
|
27115
|
-
|
|
27116
|
-
|
|
27519
|
+
const backendOf = async () => (await client.getDevPortalBackend(projectId))?.fqdn;
|
|
27520
|
+
const backendStep = {
|
|
27521
|
+
name: "backend",
|
|
27522
|
+
hash: () => backendHash,
|
|
27523
|
+
run: async () => {
|
|
27524
|
+
let fqdn = await backendOf();
|
|
27525
|
+
if (!fqdn) {
|
|
27526
|
+
await client.activateModule(projectId, BACKEND_MODULE);
|
|
27527
|
+
const deadline = now() + backendTimeoutMs;
|
|
27528
|
+
for (; ; ) {
|
|
27529
|
+
fqdn = await backendOf();
|
|
27530
|
+
if (fqdn) break;
|
|
27531
|
+
if (now() >= deadline)
|
|
27532
|
+
throw new PlansDeployError(
|
|
27533
|
+
"PLANS_BACKEND_TIMEOUT",
|
|
27534
|
+
`The plans backend did not get an address within ${Math.round(backendTimeoutMs / 1e3)} seconds.`,
|
|
27535
|
+
"Run deploy again to keep waiting."
|
|
27536
|
+
);
|
|
27537
|
+
await sleep5(intervalMs);
|
|
27538
|
+
}
|
|
27539
|
+
}
|
|
27540
|
+
return { fqdn };
|
|
27541
|
+
},
|
|
27542
|
+
stillValid: async () => Boolean(await backendOf())
|
|
27543
|
+
};
|
|
27544
|
+
const waitForBackendUpdate = async (jobId, what) => {
|
|
27545
|
+
const deadline = now() + linkTimeoutMs;
|
|
27546
|
+
for (; ; ) {
|
|
27547
|
+
const job = (await client.listProvisioningJobs(projectId)).jobs.find(
|
|
27548
|
+
(item) => item.id === jobId
|
|
27549
|
+
);
|
|
27550
|
+
const status2 = job?.status.toLowerCase();
|
|
27551
|
+
if (status2?.startsWith("completed")) return { jobId };
|
|
27552
|
+
if (status2 === "failed")
|
|
27553
|
+
throw new PlansDeployError(
|
|
27554
|
+
`${what.code}_FAILED`,
|
|
27555
|
+
`Applying ${what.text} to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
|
|
27556
|
+
"Run deploy again; it is applied again."
|
|
27557
|
+
);
|
|
27558
|
+
if (now() >= deadline)
|
|
27559
|
+
throw new PlansDeployError(
|
|
27560
|
+
`${what.code}_TIMEOUT`,
|
|
27561
|
+
`Applying ${what.text} did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
|
|
27562
|
+
"Run deploy again to keep waiting."
|
|
27563
|
+
);
|
|
27564
|
+
await sleep5(intervalMs);
|
|
27565
|
+
}
|
|
27566
|
+
};
|
|
27567
|
+
let billingNote;
|
|
27568
|
+
let webhookUrl;
|
|
27569
|
+
const billingStepDef = {
|
|
27570
|
+
name: "billing",
|
|
27571
|
+
hash: () => billingHash,
|
|
27572
|
+
run: async () => {
|
|
27573
|
+
const body = billingOn ? {
|
|
27574
|
+
provider: "stripe",
|
|
27575
|
+
apiKey: plans.billing.apiKey,
|
|
27576
|
+
webhookSecret: plans.billing.webhookSecret,
|
|
27577
|
+
successUrl: plans.billing.successUrl,
|
|
27578
|
+
cancelUrl: plans.billing.cancelUrl
|
|
27579
|
+
} : { provider: "none" };
|
|
27580
|
+
let queued;
|
|
27581
|
+
try {
|
|
27582
|
+
queued = await client.putDevPortalBilling(projectId, body);
|
|
27583
|
+
} catch (error) {
|
|
27584
|
+
billingRefused(error);
|
|
27585
|
+
}
|
|
27586
|
+
const done = await waitForBackendUpdate(queued.jobId, {
|
|
27587
|
+
code: "PLANS_BILLING",
|
|
27588
|
+
text: "the billing settings"
|
|
27589
|
+
});
|
|
27590
|
+
if (billingOn) {
|
|
27591
|
+
billingNote = billingNotes(queued);
|
|
27592
|
+
webhookUrl = queued.webhookUrl;
|
|
27593
|
+
}
|
|
27594
|
+
return { ...done, mode: queued.mode };
|
|
27595
|
+
}
|
|
27596
|
+
};
|
|
27597
|
+
const linkStep = {
|
|
27598
|
+
name: "identity-link",
|
|
27599
|
+
hash: () => linkHash,
|
|
27600
|
+
run: async () => {
|
|
27601
|
+
const queued = await client.linkDevPortalIdentity(projectId);
|
|
27602
|
+
return waitForBackendUpdate(queued.jobId, {
|
|
27603
|
+
code: "PLANS_LINK",
|
|
27604
|
+
text: "the identity"
|
|
27605
|
+
});
|
|
27606
|
+
}
|
|
27607
|
+
};
|
|
27608
|
+
const steps = [
|
|
27609
|
+
catalogStep,
|
|
27610
|
+
backendStep,
|
|
27611
|
+
...billingStep ? [billingStepDef] : [],
|
|
27612
|
+
...ownIdentity ? [linkStep] : []
|
|
27613
|
+
];
|
|
27117
27614
|
const outcomes = await runPipeline(
|
|
27118
27615
|
resolved.state,
|
|
27119
|
-
"
|
|
27616
|
+
"plans",
|
|
27120
27617
|
steps,
|
|
27121
|
-
{
|
|
27618
|
+
{
|
|
27619
|
+
force: options.force
|
|
27620
|
+
}
|
|
27122
27621
|
);
|
|
27123
|
-
const
|
|
27124
|
-
const ran =
|
|
27125
|
-
const known =
|
|
27126
|
-
let
|
|
27127
|
-
if (ran || known?.
|
|
27128
|
-
|
|
27129
|
-
if (
|
|
27622
|
+
const catalog = outcomes.find((outcome) => outcome.step === "catalog");
|
|
27623
|
+
const ran = outcomes.some((outcome) => !outcome.skipped);
|
|
27624
|
+
const known = manifest?.services.plans;
|
|
27625
|
+
let apiBaseUrl = known?.apiBaseUrl;
|
|
27626
|
+
if (ran || known?.apiBaseUrl === void 0) {
|
|
27627
|
+
apiBaseUrl = toApiBaseUrl(await backendOf()) ?? apiBaseUrl;
|
|
27628
|
+
if (apiBaseUrl) {
|
|
27629
|
+
const previous = known?.catalogRevision ?? 0;
|
|
27130
27630
|
await writeManifest(
|
|
27131
27631
|
resolved.state,
|
|
27132
|
-
|
|
27632
|
+
withPlansEntry(await readManifest(resolved.state), {
|
|
27133
27633
|
projectId,
|
|
27134
27634
|
projectName: resolved.project.name,
|
|
27135
|
-
|
|
27136
|
-
|
|
27137
|
-
|
|
27138
|
-
|
|
27635
|
+
apiBaseUrl,
|
|
27636
|
+
billing: billingOn ? "stripe" : "none",
|
|
27637
|
+
catalogRevision: catalog && !catalog.skipped ? previous + 1 : Math.max(previous, 1),
|
|
27638
|
+
capabilities: billingOn ? { ...PLANS_CAPABILITIES, checkout: true } : PLANS_CAPABILITIES
|
|
27139
27639
|
})
|
|
27140
27640
|
);
|
|
27141
27641
|
}
|
|
27142
27642
|
}
|
|
27143
|
-
|
|
27144
|
-
|
|
27145
|
-
)
|
|
27643
|
+
let webhookLine;
|
|
27644
|
+
let stripeWebhook;
|
|
27645
|
+
if (billingOn && client.getBillingWebhookStatus) {
|
|
27646
|
+
try {
|
|
27647
|
+
const status2 = await client.getBillingWebhookStatus(projectId);
|
|
27648
|
+
webhookLine = webhookStatusLine(status2, now());
|
|
27649
|
+
const url2 = webhookUrl ?? (apiBaseUrl ? `${apiBaseUrl.replace(/\/+$/, "")}${STRIPE_WEBHOOK_PATH}` : void 0);
|
|
27650
|
+
if (status2 && url2)
|
|
27651
|
+
stripeWebhook = {
|
|
27652
|
+
url: url2,
|
|
27653
|
+
secretConfigured: status2.webhookSecretConfigured
|
|
27654
|
+
};
|
|
27655
|
+
} catch {
|
|
27656
|
+
}
|
|
27657
|
+
}
|
|
27658
|
+
const notes = [...billingNote ?? [], ...webhookLine ? [webhookLine] : []];
|
|
27146
27659
|
return {
|
|
27147
|
-
status: ran
|
|
27660
|
+
status: ran ? "deployed" : "unchanged",
|
|
27148
27661
|
steps: outcomes.map((outcome) => ({
|
|
27149
27662
|
step: outcome.step,
|
|
27150
27663
|
skipped: outcome.skipped
|
|
27151
27664
|
})),
|
|
27152
|
-
|
|
27153
|
-
|
|
27154
|
-
|
|
27155
|
-
stepOf(
|
|
27156
|
-
await readLedger(resolved.state),
|
|
27157
|
-
"identity",
|
|
27158
|
-
IDENTITY_CONNECTIONS_STEP
|
|
27159
|
-
)?.output
|
|
27160
|
-
)
|
|
27665
|
+
apiBaseUrl,
|
|
27666
|
+
...notes.length > 0 ? { notes } : {},
|
|
27667
|
+
...stripeWebhook ? { stripeWebhook } : {}
|
|
27161
27668
|
};
|
|
27162
27669
|
}
|
|
27163
|
-
|
|
27164
|
-
|
|
27165
|
-
|
|
27166
|
-
|
|
27167
|
-
|
|
27168
|
-
|
|
27169
|
-
|
|
27170
|
-
|
|
27171
|
-
|
|
27172
|
-
|
|
27173
|
-
|
|
27174
|
-
|
|
27175
|
-
|
|
27176
|
-
|
|
27177
|
-
|
|
27178
|
-
|
|
27179
|
-
|
|
27180
|
-
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
|
|
27184
|
-
|
|
27185
|
-
|
|
27186
|
-
|
|
27187
|
-
|
|
27188
|
-
|
|
27189
|
-
|
|
27190
|
-
|
|
27191
|
-
|
|
27192
|
-
|
|
27193
|
-
|
|
27194
|
-
|
|
27195
|
-
|
|
27196
|
-
|
|
27197
|
-
|
|
27198
|
-
|
|
27199
|
-
|
|
27200
|
-
|
|
27201
|
-
|
|
27202
|
-
|
|
27203
|
-
|
|
27204
|
-
|
|
27205
|
-
|
|
27206
|
-
|
|
27207
|
-
|
|
27208
|
-
|
|
27209
|
-
|
|
27210
|
-
}
|
|
27211
|
-
}
|
|
27212
|
-
for (const old of existing)
|
|
27213
|
-
if (!wanted.some((connection) => connection.id === old.id))
|
|
27214
|
-
await deleteProjectIdentityConnection.call(client, projectId, old.id);
|
|
27215
|
-
return {
|
|
27216
|
-
ids: wanted.map((connection) => connection.id).join(","),
|
|
27217
|
-
secretsStamp: stableJson(recorded),
|
|
27218
|
-
...redirects
|
|
27219
|
-
};
|
|
27220
|
-
},
|
|
27221
|
-
// The platform still has exactly these connections, and none of the secrets was changed since.
|
|
27222
|
-
stillValid: async () => {
|
|
27223
|
-
const found = await listProjectIdentityConnections.call(
|
|
27224
|
-
client,
|
|
27225
|
-
projectId
|
|
27226
|
-
);
|
|
27227
|
-
if (found.length !== wanted.length) return false;
|
|
27228
|
-
for (const connection of wanted) {
|
|
27229
|
-
const there = found.find((f) => f.id === connection.id);
|
|
27230
|
-
if (!there || there.clientId !== connection.clientId || !sameAddress(there.authority, connection.authority))
|
|
27231
|
-
return false;
|
|
27232
|
-
}
|
|
27233
|
-
const before = stepOf(
|
|
27234
|
-
await readLedger(resolved.state),
|
|
27235
|
-
"identity",
|
|
27236
|
-
IDENTITY_CONNECTIONS_STEP
|
|
27237
|
-
)?.output?.secretsStamp;
|
|
27238
|
-
return before === void 0 || before === stableJson(await stamps());
|
|
27239
|
-
}
|
|
27240
|
-
};
|
|
27670
|
+
|
|
27671
|
+
// src/cli/deploy/stripe-webhook.ts
|
|
27672
|
+
var STRIPE_WEBHOOK_SECRET = "STRIPE_WEBHOOK";
|
|
27673
|
+
var SIGNING_SECRET = /^whsec_[A-Za-z0-9]+$/;
|
|
27674
|
+
var StripeWebhookError = class extends ApitogoError {
|
|
27675
|
+
constructor(message, developerHint) {
|
|
27676
|
+
super(message, { developerHint, title: "Stripe webhook secret" });
|
|
27677
|
+
this.name = "StripeWebhookError";
|
|
27678
|
+
}
|
|
27679
|
+
};
|
|
27680
|
+
async function offerStripeWebhookSecret(output4, deps) {
|
|
27681
|
+
const webhook = output4.services.find(
|
|
27682
|
+
(service) => service.service === "plans"
|
|
27683
|
+
)?.stripeWebhook;
|
|
27684
|
+
if (!webhook || webhook.secretConfigured || !deps.interactive)
|
|
27685
|
+
return { status: "not-needed" };
|
|
27686
|
+
deps.print("");
|
|
27687
|
+
deps.print(
|
|
27688
|
+
"Payments need one more step: Stripe must be able to tell your plans backend that a payment went through."
|
|
27689
|
+
);
|
|
27690
|
+
deps.print(
|
|
27691
|
+
`1. In Stripe, open Developers > Webhooks and add an endpoint with the URL ${webhook.url}`
|
|
27692
|
+
);
|
|
27693
|
+
deps.print(`2. Select the events: ${STRIPE_WEBHOOK_EVENTS2.join(", ")}`);
|
|
27694
|
+
deps.print(
|
|
27695
|
+
"3. Open the endpoint, reveal its signing secret (starts with whsec_) and paste it here."
|
|
27696
|
+
);
|
|
27697
|
+
const secret = (await deps.askHidden(
|
|
27698
|
+
"Stripe webhook signing secret (hidden, Enter to skip): "
|
|
27699
|
+
)).trim();
|
|
27700
|
+
if (!secret) {
|
|
27701
|
+
deps.print(
|
|
27702
|
+
`Skipped. Until it is set, a payment does not activate a plan. Run \`apitogo secrets set ${STRIPE_WEBHOOK_SECRET}\`, then \`apitogo deploy --only plans\`.`
|
|
27703
|
+
);
|
|
27704
|
+
return { status: "skipped" };
|
|
27705
|
+
}
|
|
27706
|
+
if (!SIGNING_SECRET.test(secret))
|
|
27707
|
+
throw new StripeWebhookError(
|
|
27708
|
+
"That is not a Stripe webhook signing secret. It was not saved.",
|
|
27709
|
+
`A signing secret starts with whsec_. Run \`apitogo secrets set ${STRIPE_WEBHOOK_SECRET}\` with the right one, then \`apitogo deploy --only plans\`.`
|
|
27710
|
+
);
|
|
27711
|
+
await deps.client().setSecret(output4.projectId, STRIPE_WEBHOOK_SECRET, secret);
|
|
27712
|
+
const wired = await deps.wireConfig();
|
|
27713
|
+
deps.print(
|
|
27714
|
+
`Signing secret saved as the project secret ${STRIPE_WEBHOOK_SECRET}${wired ? `, and ${wired} points to it` : ""}. Deploying the plans again so the backend gets it.`
|
|
27715
|
+
);
|
|
27716
|
+
return { status: "saved", output: await deps.redeployPlans() };
|
|
27241
27717
|
}
|
|
27242
27718
|
|
|
27243
|
-
// src/cli/deploy/
|
|
27719
|
+
// src/cli/deploy/v2.ts
|
|
27720
|
+
import { rm as rm11 } from "node:fs/promises";
|
|
27244
27721
|
init_invariant();
|
|
27245
|
-
|
|
27722
|
+
|
|
27723
|
+
// src/cli/portal/report.ts
|
|
27724
|
+
import { access as access9 } from "node:fs/promises";
|
|
27725
|
+
import path81 from "node:path";
|
|
27726
|
+
|
|
27727
|
+
// src/cli/deploy/identity.ts
|
|
27728
|
+
init_invariant();
|
|
27729
|
+
var IdentityDeployError = class extends ApitogoError {
|
|
27246
27730
|
constructor(code, message, developerHint) {
|
|
27247
|
-
super(message, { developerHint, title: "Cannot deploy
|
|
27731
|
+
super(message, { developerHint, title: "Cannot deploy identity" });
|
|
27248
27732
|
this.code = code;
|
|
27249
|
-
this.name = "
|
|
27733
|
+
this.name = "IdentityDeployError";
|
|
27250
27734
|
}
|
|
27251
27735
|
};
|
|
27252
|
-
var
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
27260
|
-
var PLANS_CAPABILITIES = { catalog: true, keys: true, usage: true };
|
|
27261
|
-
var STRIPE_WEBHOOK_EVENTS2 = [
|
|
27262
|
-
"checkout.session.completed",
|
|
27263
|
-
"customer.subscription.updated",
|
|
27264
|
-
"customer.subscription.deleted",
|
|
27265
|
-
"invoice.payment_failed"
|
|
27266
|
-
];
|
|
27267
|
-
function billingNotes(billing) {
|
|
27268
|
-
const notes = [
|
|
27269
|
-
billing.mode === "live" ? "Stripe: LIVE mode. Customers pay with real money." : "Stripe: test mode. Use Stripe's test cards; no real money moves."
|
|
27270
|
-
];
|
|
27271
|
-
notes.push(
|
|
27272
|
-
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.`
|
|
27273
|
-
);
|
|
27274
|
-
return notes;
|
|
27736
|
+
var IDENTITY_STEPS = ["request", "activate"];
|
|
27737
|
+
var IDENTITY_CONNECTIONS_STEP = "connections";
|
|
27738
|
+
var SECRET_PREFIX = "secret:";
|
|
27739
|
+
function normalizedConnections(resolved) {
|
|
27740
|
+
return [...resolved.identity?.connections ?? []].map((connection) => ({
|
|
27741
|
+
...connection,
|
|
27742
|
+
authority: connection.authority.trim().replace(/\/+$/, "")
|
|
27743
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
27275
27744
|
}
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
|
|
27279
|
-
|
|
27280
|
-
const filePlans = await readPlansFile(plans.file);
|
|
27281
|
-
const wanted = toPlatformPlans(filePlans, {
|
|
27282
|
-
billing: billing.provider === "stripe"
|
|
27745
|
+
function identityConnectionsHash(projectId, resolved) {
|
|
27746
|
+
return stableJson({
|
|
27747
|
+
projectId,
|
|
27748
|
+
connections: normalizedConnections(resolved)
|
|
27283
27749
|
});
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
const
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
|
|
27291
|
-
);
|
|
27750
|
+
}
|
|
27751
|
+
async function recordedConnectionIds(resolved) {
|
|
27752
|
+
const ids = stepOf(
|
|
27753
|
+
await readLedger(resolved.state),
|
|
27754
|
+
"identity",
|
|
27755
|
+
IDENTITY_CONNECTIONS_STEP
|
|
27756
|
+
)?.output?.ids;
|
|
27757
|
+
return ids ? ids.split(",").filter(Boolean) : [];
|
|
27758
|
+
}
|
|
27759
|
+
function redirectAddressesOf(output4) {
|
|
27760
|
+
return Object.entries(output4 ?? {}).filter(([key]) => key.startsWith("redirect.")).map(([key, redirectUri]) => ({
|
|
27761
|
+
id: key.slice("redirect.".length),
|
|
27762
|
+
redirectUri
|
|
27763
|
+
}));
|
|
27764
|
+
}
|
|
27765
|
+
function connectionBody(connection) {
|
|
27292
27766
|
return {
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
27296
|
-
|
|
27297
|
-
|
|
27298
|
-
billingOn,
|
|
27299
|
-
billingStep,
|
|
27300
|
-
// Only the references to the project's secrets: a rotated value is applied with --force.
|
|
27301
|
-
billingHash: stableJson({
|
|
27302
|
-
projectId,
|
|
27303
|
-
provider: billingOn ? "stripe" : "none",
|
|
27304
|
-
apiKey: billingOn ? billing.apiKey : void 0,
|
|
27305
|
-
webhookSecret: billingOn ? billing.webhookSecret : void 0,
|
|
27306
|
-
successUrl: billingOn ? billing.successUrl : void 0,
|
|
27307
|
-
cancelUrl: billingOn ? billing.cancelUrl : void 0
|
|
27308
|
-
}),
|
|
27309
|
-
linkHash: stableJson({
|
|
27310
|
-
projectId,
|
|
27311
|
-
issuer: identity?.issuer,
|
|
27312
|
-
clientId: identity?.clientId
|
|
27313
|
-
})
|
|
27767
|
+
authority: connection.authority,
|
|
27768
|
+
clientId: connection.clientId,
|
|
27769
|
+
clientSecret: connection.clientSecret,
|
|
27770
|
+
scopes: connection.scopes,
|
|
27771
|
+
displayName: connection.displayName
|
|
27314
27772
|
};
|
|
27315
27773
|
}
|
|
27316
|
-
function
|
|
27317
|
-
return
|
|
27318
|
-
|
|
27319
|
-
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
keys.push(...result.items.map((item) => item.actionKey));
|
|
27325
|
-
if (result.items.length === 0 || page * result.pageSize >= result.totalCount)
|
|
27326
|
-
break;
|
|
27327
|
-
}
|
|
27328
|
-
} catch {
|
|
27329
|
-
return [];
|
|
27330
|
-
}
|
|
27331
|
-
return keys;
|
|
27774
|
+
function referencedSecretNames(connections) {
|
|
27775
|
+
return [
|
|
27776
|
+
...new Set(
|
|
27777
|
+
connections.map(
|
|
27778
|
+
(connection) => connection.clientSecret.slice(SECRET_PREFIX.length)
|
|
27779
|
+
)
|
|
27780
|
+
)
|
|
27781
|
+
];
|
|
27332
27782
|
}
|
|
27333
|
-
function
|
|
27334
|
-
if (
|
|
27335
|
-
|
|
27336
|
-
|
|
27337
|
-
"PLANS_REJECTED",
|
|
27338
|
-
`The platform refused a plan:
|
|
27339
|
-
${lines.join("\n ") || error.message}`,
|
|
27340
|
-
"Fix the plan in the plans file and run deploy again."
|
|
27341
|
-
);
|
|
27342
|
-
}
|
|
27343
|
-
throw error;
|
|
27783
|
+
function identityProviderSwitch(resolved, known) {
|
|
27784
|
+
if (!known?.provider || !resolved.identity) return void 0;
|
|
27785
|
+
const to = resolved.identity.provider ?? "apitogo";
|
|
27786
|
+
return known.provider === to ? void 0 : { from: known.provider, to };
|
|
27344
27787
|
}
|
|
27345
|
-
function
|
|
27346
|
-
|
|
27347
|
-
const lines = describePlatformProblem(error);
|
|
27348
|
-
throw new PlansDeployError(
|
|
27349
|
-
"PLANS_BILLING_REJECTED",
|
|
27350
|
-
`The platform refused the billing settings: ${lines.join(" ") || error.message}`,
|
|
27351
|
-
"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_."
|
|
27352
|
-
);
|
|
27353
|
-
}
|
|
27354
|
-
throw error;
|
|
27788
|
+
function identityProviderSwitchWarning(from, to) {
|
|
27789
|
+
return `warning: the identity provider changes from ${from} to ${to}. People are known by issuer and subject, so the users, subscriptions and keys made with ${from} are not carried over to ${to}.`;
|
|
27355
27790
|
}
|
|
27356
|
-
|
|
27357
|
-
|
|
27358
|
-
|
|
27791
|
+
var PORTAL_CALLBACK_PATH = "/oauth/callback";
|
|
27792
|
+
var defaultSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27793
|
+
function identityNotDeployableReason(resolved) {
|
|
27794
|
+
const identity = resolved.identity;
|
|
27795
|
+
if (!identity) return "identity is not in the config";
|
|
27796
|
+
if (identity.provider === "oidc" && (!identity.issuer || !identity.clientId))
|
|
27797
|
+
return 'identity.provider "oidc" needs issuer and clientId';
|
|
27798
|
+
return void 0;
|
|
27799
|
+
}
|
|
27800
|
+
function identityBodyForRedirect(resolved, redirectUri) {
|
|
27801
|
+
return {
|
|
27802
|
+
portalRedirectUris: [redirectUri],
|
|
27803
|
+
nativeSignup: resolved.identity?.native ?? true
|
|
27804
|
+
};
|
|
27805
|
+
}
|
|
27806
|
+
function identityRequestBody(resolved, portalUrl) {
|
|
27807
|
+
return identityBodyForRedirect(
|
|
27808
|
+
resolved,
|
|
27809
|
+
`${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}`
|
|
27810
|
+
);
|
|
27811
|
+
}
|
|
27812
|
+
function identityInputsHash(projectId, body) {
|
|
27813
|
+
return stableJson({ projectId, body });
|
|
27814
|
+
}
|
|
27815
|
+
async function portalUrlOf(resolved, projectId, client) {
|
|
27816
|
+
const known = (await readManifest(resolved.state))?.services.portal?.url;
|
|
27817
|
+
const url2 = known ?? resolveSwaUrl(await client.getProject(projectId));
|
|
27818
|
+
if (!url2)
|
|
27819
|
+
throw new IdentityDeployError(
|
|
27820
|
+
"IDENTITY_PORTAL_ADDRESS_UNKNOWN",
|
|
27821
|
+
"The portal has no public address yet, and the identity needs it for the sign-in redirect.",
|
|
27822
|
+
"The address comes with the project's hosting. Run `apitogo status`: while hosting is still being prepared, wait a few minutes and run `apitogo deploy` again. Once hosting is ready, `apitogo deploy --only portal` gives the portal its address, then deploy identity."
|
|
27823
|
+
);
|
|
27824
|
+
return url2;
|
|
27825
|
+
}
|
|
27826
|
+
function rejected2(error) {
|
|
27827
|
+
if (error instanceof PlatformApiError && error.status === 409) {
|
|
27828
|
+
throw new IdentityDeployError(
|
|
27829
|
+
"IDENTITY_SETTINGS_CHANGED",
|
|
27830
|
+
"This project already has an identity with other settings, and its audience and portal address cannot be changed in place.",
|
|
27831
|
+
"Keep the portal address as it was, or remove the identity first (this deletes every user in it)."
|
|
27832
|
+
);
|
|
27833
|
+
}
|
|
27834
|
+
if (error instanceof PlatformApiError && error.status === 400) {
|
|
27835
|
+
const lines = describePlatformProblem(error);
|
|
27836
|
+
throw new IdentityDeployError(
|
|
27837
|
+
"IDENTITY_REJECTED",
|
|
27838
|
+
`The platform refused the identity request:
|
|
27839
|
+
${lines.join("\n ") || error.message}`,
|
|
27840
|
+
"Fix the cause in apitogo.config.json and run deploy again."
|
|
27841
|
+
);
|
|
27842
|
+
}
|
|
27843
|
+
throw error;
|
|
27844
|
+
}
|
|
27845
|
+
function connectionRejected(error, id) {
|
|
27846
|
+
if (error instanceof PlatformApiError && error.status === 409) {
|
|
27847
|
+
throw new IdentityDeployError(
|
|
27848
|
+
"IDENTITY_CONNECTION_REJECTED",
|
|
27849
|
+
`The identity is not active yet, so connection "${id}" cannot be saved.`,
|
|
27850
|
+
"Run deploy again once the identity is Active."
|
|
27851
|
+
);
|
|
27852
|
+
}
|
|
27853
|
+
if (error instanceof PlatformApiError && error.status === 400) {
|
|
27854
|
+
const lines = describePlatformProblem(error);
|
|
27855
|
+
throw new IdentityDeployError(
|
|
27856
|
+
"IDENTITY_CONNECTION_REJECTED",
|
|
27857
|
+
`The platform refused login connection "${id}":
|
|
27858
|
+
${lines.join("\n ") || error.message}`,
|
|
27859
|
+
"Fix the connection in apitogo.config.json (identity.connections) and run deploy again."
|
|
27860
|
+
);
|
|
27861
|
+
}
|
|
27862
|
+
throw error;
|
|
27863
|
+
}
|
|
27864
|
+
async function recordOwnProvider(resolved, projectId) {
|
|
27865
|
+
const identity = resolved.identity;
|
|
27866
|
+
const issuer = identity?.issuer?.replace(/\/+$/, "");
|
|
27867
|
+
const clientId = identity?.clientId;
|
|
27868
|
+
if (!issuer || !clientId)
|
|
27869
|
+
throw new Error("identity.issuer and identity.clientId are needed.");
|
|
27870
|
+
const known = (await readManifest(resolved.state))?.services.identity;
|
|
27871
|
+
const changed = known?.provider !== "oidc" || known.issuer !== issuer || known.clientId !== clientId;
|
|
27872
|
+
if (changed)
|
|
27873
|
+
await writeManifest(
|
|
27874
|
+
resolved.state,
|
|
27875
|
+
withIdentityEntry(await readManifest(resolved.state), {
|
|
27876
|
+
projectId,
|
|
27877
|
+
projectName: resolved.project.name,
|
|
27878
|
+
provider: "oidc",
|
|
27879
|
+
issuer,
|
|
27880
|
+
clientId,
|
|
27881
|
+
nativeSignup: false
|
|
27882
|
+
})
|
|
27883
|
+
);
|
|
27884
|
+
return {
|
|
27885
|
+
status: changed ? "deployed" : "unchanged",
|
|
27886
|
+
steps: [],
|
|
27887
|
+
issuer,
|
|
27888
|
+
clientId
|
|
27889
|
+
};
|
|
27890
|
+
}
|
|
27891
|
+
async function deployIdentity(resolved, projectId, deps, options = {}) {
|
|
27892
|
+
if (resolved.identity?.provider === "oidc")
|
|
27893
|
+
return recordOwnProvider(resolved, projectId);
|
|
27359
27894
|
const client = deps.client;
|
|
27360
27895
|
const sleep5 = deps.sleep ?? defaultSleep4;
|
|
27361
27896
|
const now = deps.now ?? Date.now;
|
|
27897
|
+
const timeoutMs = deps.activateTimeoutMs ?? 12e4;
|
|
27362
27898
|
const intervalMs = deps.pollIntervalMs ?? 2e3;
|
|
27363
|
-
const
|
|
27364
|
-
const
|
|
27365
|
-
const
|
|
27366
|
-
|
|
27367
|
-
|
|
27368
|
-
|
|
27369
|
-
billingOn,
|
|
27370
|
-
billingStep,
|
|
27371
|
-
billingHash,
|
|
27372
|
-
catalogHash,
|
|
27373
|
-
backendHash,
|
|
27374
|
-
linkHash
|
|
27375
|
-
} = await plansInputs(resolved, projectId);
|
|
27376
|
-
const catalogStep = {
|
|
27377
|
-
name: "catalog",
|
|
27378
|
-
hash: () => catalogHash,
|
|
27899
|
+
const portalUrl = await portalUrlOf(resolved, projectId, client);
|
|
27900
|
+
const body = identityRequestBody(resolved, portalUrl);
|
|
27901
|
+
const inputsHash = identityInputsHash(projectId, body);
|
|
27902
|
+
const requestStep = {
|
|
27903
|
+
name: "request",
|
|
27904
|
+
hash: () => inputsHash,
|
|
27379
27905
|
run: async () => {
|
|
27380
|
-
|
|
27381
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
|
|
27388
|
-
throw new PlansDeployError(
|
|
27389
|
-
"PLANS_REJECTED",
|
|
27390
|
-
`The platform refused the plans:
|
|
27391
|
-
${checked.errors.map((issue4) => `${issue4.field}: ${issue4.message}`).join("\n ")}`,
|
|
27392
|
-
"Fix the listed problems in the plans file and run deploy again."
|
|
27393
|
-
);
|
|
27394
|
-
const existing = new Map(
|
|
27395
|
-
(await client.listGatewayPlans(projectId)).map((plan2) => [
|
|
27396
|
-
plan2.sku.toLowerCase(),
|
|
27397
|
-
plan2
|
|
27398
|
-
])
|
|
27399
|
-
);
|
|
27400
|
-
let created = 0;
|
|
27401
|
-
let updated = 0;
|
|
27402
|
-
let archived = 0;
|
|
27403
|
-
const backendSync = /* @__PURE__ */ new Set();
|
|
27404
|
-
const noteSync = (plan2) => {
|
|
27405
|
-
if (plan2.plansBackendSync) backendSync.add(plan2.plansBackendSync);
|
|
27406
|
-
};
|
|
27407
|
-
for (const body of bodies) {
|
|
27408
|
-
const found = existing.get(body.sku);
|
|
27409
|
-
try {
|
|
27410
|
-
if (found) {
|
|
27411
|
-
const result = await client.updateGatewayPlan(projectId, found.id, {
|
|
27412
|
-
displayName: body.displayName,
|
|
27413
|
-
isActive: true,
|
|
27414
|
-
limitsJson: body.limitsJson ?? null,
|
|
27415
|
-
includedActionKeys: body.includedActionKeys,
|
|
27416
|
-
priceAmount: body.priceAmount,
|
|
27417
|
-
pricingModel: body.pricingModel,
|
|
27418
|
-
priceLabel: body.priceLabel,
|
|
27419
|
-
priceCurrency: body.priceCurrency,
|
|
27420
|
-
billingPeriod: body.billingPeriod,
|
|
27421
|
-
sortOrder: body.sortOrder
|
|
27422
|
-
});
|
|
27423
|
-
noteSync(result);
|
|
27424
|
-
updated++;
|
|
27425
|
-
} else {
|
|
27426
|
-
noteSync(await client.createGatewayPlan(projectId, body));
|
|
27427
|
-
created++;
|
|
27428
|
-
}
|
|
27429
|
-
} catch (error) {
|
|
27430
|
-
refused(error);
|
|
27431
|
-
}
|
|
27432
|
-
}
|
|
27433
|
-
const kept = new Set(bodies.map((body) => body.sku));
|
|
27434
|
-
for (const plan2 of existing.values()) {
|
|
27435
|
-
if (!plan2.isActive || kept.has(plan2.sku.toLowerCase())) continue;
|
|
27436
|
-
try {
|
|
27437
|
-
const result = await client.updateGatewayPlan(projectId, plan2.id, {
|
|
27438
|
-
displayName: plan2.displayName,
|
|
27439
|
-
isActive: false,
|
|
27440
|
-
limitsJson: plan2.limitsJson ?? null,
|
|
27441
|
-
includedActionKeys: plan2.includedActions.map(
|
|
27442
|
-
(action) => action.actionKey
|
|
27443
|
-
),
|
|
27444
|
-
priceAmount: plan2.priceAmount,
|
|
27445
|
-
pricingModel: plan2.pricingModel,
|
|
27446
|
-
priceCurrency: plan2.priceCurrency,
|
|
27447
|
-
billingPeriod: plan2.billingPeriod,
|
|
27448
|
-
sortOrder: plan2.sortOrder
|
|
27449
|
-
});
|
|
27450
|
-
noteSync(result);
|
|
27451
|
-
archived++;
|
|
27452
|
-
} catch (error) {
|
|
27453
|
-
refused(error);
|
|
27454
|
-
}
|
|
27906
|
+
try {
|
|
27907
|
+
const answer = await client.putProjectIdentity(projectId, body);
|
|
27908
|
+
return {
|
|
27909
|
+
status: answer.status,
|
|
27910
|
+
redirectUri: body.portalRedirectUris[0] ?? ""
|
|
27911
|
+
};
|
|
27912
|
+
} catch (error) {
|
|
27913
|
+
return rejected2(error);
|
|
27455
27914
|
}
|
|
27456
|
-
return {
|
|
27457
|
-
created: String(created),
|
|
27458
|
-
updated: String(updated),
|
|
27459
|
-
archived: String(archived),
|
|
27460
|
-
...backendSync.size > 0 ? { backendSync: [...backendSync].sort().join(",") } : {}
|
|
27461
|
-
};
|
|
27462
27915
|
},
|
|
27463
|
-
//
|
|
27464
|
-
// archived and is not yet.
|
|
27916
|
+
// An identity that was removed, or that failed, is not a result worth keeping.
|
|
27465
27917
|
stillValid: async () => {
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
(plan2) => plan2.isActive
|
|
27469
|
-
);
|
|
27470
|
-
const present = new Set(active2.map((plan2) => plan2.sku.toLowerCase()));
|
|
27471
|
-
return wanted.every((plan2) => present.has(plan2.sku)) && active2.every(
|
|
27472
|
-
(plan2) => wanted.some((w) => w.sku === plan2.sku.toLowerCase())
|
|
27473
|
-
);
|
|
27918
|
+
const found = await client.getProjectIdentity(projectId);
|
|
27919
|
+
return found !== null && found.status !== "Failed" && found.status !== "Released" && found.status !== "Releasing";
|
|
27474
27920
|
}
|
|
27475
27921
|
};
|
|
27476
|
-
const
|
|
27477
|
-
|
|
27478
|
-
|
|
27479
|
-
hash: () => backendHash,
|
|
27922
|
+
const activateStep = {
|
|
27923
|
+
name: "activate",
|
|
27924
|
+
hash: () => inputsHash,
|
|
27480
27925
|
run: async () => {
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
await client.
|
|
27484
|
-
|
|
27485
|
-
|
|
27486
|
-
|
|
27487
|
-
|
|
27488
|
-
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
|
|
27495
|
-
|
|
27926
|
+
const deadline = now() + timeoutMs;
|
|
27927
|
+
for (; ; ) {
|
|
27928
|
+
const found = await client.getProjectIdentity(projectId);
|
|
27929
|
+
if (found?.status === "Active")
|
|
27930
|
+
return {
|
|
27931
|
+
issuer: found.issuer,
|
|
27932
|
+
clientId: found.clientId,
|
|
27933
|
+
audience: found.audience,
|
|
27934
|
+
nativeSignup: String(found.nativeSignup)
|
|
27935
|
+
};
|
|
27936
|
+
if (found?.status === "Failed")
|
|
27937
|
+
throw new IdentityDeployError(
|
|
27938
|
+
"IDENTITY_FAILED",
|
|
27939
|
+
`The platform could not make the identity: ${[found.errorCode, found.errorMessage].filter(Boolean).join(" ") || "it failed"}.`,
|
|
27940
|
+
"Fix the cause and run deploy again; the request is sent again."
|
|
27941
|
+
);
|
|
27942
|
+
if (now() >= deadline)
|
|
27943
|
+
throw new IdentityDeployError(
|
|
27944
|
+
"IDENTITY_TIMEOUT",
|
|
27945
|
+
`The identity did not become active within ${Math.round(timeoutMs / 1e3)} seconds (state: ${found?.status ?? "not found"}).`,
|
|
27946
|
+
"Run deploy again to keep waiting."
|
|
27947
|
+
);
|
|
27948
|
+
await sleep5(intervalMs);
|
|
27496
27949
|
}
|
|
27497
|
-
return { fqdn };
|
|
27498
27950
|
},
|
|
27499
|
-
stillValid: async () =>
|
|
27500
|
-
};
|
|
27501
|
-
const waitForBackendUpdate = async (jobId, what) => {
|
|
27502
|
-
const deadline = now() + linkTimeoutMs;
|
|
27503
|
-
for (; ; ) {
|
|
27504
|
-
const job = (await client.listProvisioningJobs(projectId)).jobs.find(
|
|
27505
|
-
(item) => item.id === jobId
|
|
27506
|
-
);
|
|
27507
|
-
const status2 = job?.status.toLowerCase();
|
|
27508
|
-
if (status2?.startsWith("completed")) return { jobId };
|
|
27509
|
-
if (status2 === "failed")
|
|
27510
|
-
throw new PlansDeployError(
|
|
27511
|
-
`${what.code}_FAILED`,
|
|
27512
|
-
`Applying ${what.text} to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
|
|
27513
|
-
"Run deploy again; it is applied again."
|
|
27514
|
-
);
|
|
27515
|
-
if (now() >= deadline)
|
|
27516
|
-
throw new PlansDeployError(
|
|
27517
|
-
`${what.code}_TIMEOUT`,
|
|
27518
|
-
`Applying ${what.text} did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
|
|
27519
|
-
"Run deploy again to keep waiting."
|
|
27520
|
-
);
|
|
27521
|
-
await sleep5(intervalMs);
|
|
27522
|
-
}
|
|
27523
|
-
};
|
|
27524
|
-
let billingNote;
|
|
27525
|
-
const billingStepDef = {
|
|
27526
|
-
name: "billing",
|
|
27527
|
-
hash: () => billingHash,
|
|
27528
|
-
run: async () => {
|
|
27529
|
-
const body = billingOn ? {
|
|
27530
|
-
provider: "stripe",
|
|
27531
|
-
apiKey: plans.billing.apiKey,
|
|
27532
|
-
webhookSecret: plans.billing.webhookSecret,
|
|
27533
|
-
successUrl: plans.billing.successUrl,
|
|
27534
|
-
cancelUrl: plans.billing.cancelUrl
|
|
27535
|
-
} : { provider: "none" };
|
|
27536
|
-
let queued;
|
|
27537
|
-
try {
|
|
27538
|
-
queued = await client.putDevPortalBilling(projectId, body);
|
|
27539
|
-
} catch (error) {
|
|
27540
|
-
billingRefused(error);
|
|
27541
|
-
}
|
|
27542
|
-
const done = await waitForBackendUpdate(queued.jobId, {
|
|
27543
|
-
code: "PLANS_BILLING",
|
|
27544
|
-
text: "the billing settings"
|
|
27545
|
-
});
|
|
27546
|
-
if (billingOn) billingNote = billingNotes(queued);
|
|
27547
|
-
return { ...done, mode: queued.mode };
|
|
27548
|
-
}
|
|
27549
|
-
};
|
|
27550
|
-
const linkStep = {
|
|
27551
|
-
name: "identity-link",
|
|
27552
|
-
hash: () => linkHash,
|
|
27553
|
-
run: async () => {
|
|
27554
|
-
const queued = await client.linkDevPortalIdentity(projectId);
|
|
27555
|
-
return waitForBackendUpdate(queued.jobId, {
|
|
27556
|
-
code: "PLANS_LINK",
|
|
27557
|
-
text: "the identity"
|
|
27558
|
-
});
|
|
27559
|
-
}
|
|
27951
|
+
stillValid: async () => (await client.getProjectIdentity(projectId))?.status === "Active"
|
|
27560
27952
|
};
|
|
27561
|
-
const
|
|
27562
|
-
|
|
27563
|
-
|
|
27564
|
-
|
|
27565
|
-
...ownIdentity ? [linkStep] : []
|
|
27566
|
-
];
|
|
27953
|
+
const wanted = normalizedConnections(resolved);
|
|
27954
|
+
const steps = [requestStep, activateStep];
|
|
27955
|
+
if (wanted.length > 0 || (await recordedConnectionIds(resolved)).length > 0)
|
|
27956
|
+
steps.push(connectionsStep(resolved, projectId, client, wanted));
|
|
27567
27957
|
const outcomes = await runPipeline(
|
|
27568
27958
|
resolved.state,
|
|
27569
|
-
"
|
|
27959
|
+
"identity",
|
|
27570
27960
|
steps,
|
|
27571
|
-
{
|
|
27572
|
-
force: options.force
|
|
27573
|
-
}
|
|
27961
|
+
{ force: options.force }
|
|
27574
27962
|
);
|
|
27575
|
-
const
|
|
27576
|
-
const ran =
|
|
27577
|
-
const known =
|
|
27578
|
-
let
|
|
27579
|
-
if (ran || known?.
|
|
27580
|
-
|
|
27581
|
-
if (
|
|
27582
|
-
const previous = known?.catalogRevision ?? 0;
|
|
27963
|
+
const activate = outcomes.find((outcome) => outcome.step === "activate");
|
|
27964
|
+
const ran = activate !== void 0 && !activate.skipped;
|
|
27965
|
+
const known = (await readManifest(resolved.state))?.services.identity;
|
|
27966
|
+
let learned;
|
|
27967
|
+
if (ran || known?.issuer === void 0) {
|
|
27968
|
+
learned = await client.getProjectIdentity(projectId) ?? void 0;
|
|
27969
|
+
if (learned?.status === "Active") {
|
|
27583
27970
|
await writeManifest(
|
|
27584
27971
|
resolved.state,
|
|
27585
|
-
|
|
27586
|
-
projectId,
|
|
27587
|
-
projectName: resolved.project.name,
|
|
27588
|
-
|
|
27589
|
-
|
|
27590
|
-
|
|
27591
|
-
|
|
27972
|
+
withIdentityEntry(await readManifest(resolved.state), {
|
|
27973
|
+
projectId,
|
|
27974
|
+
projectName: resolved.project.name,
|
|
27975
|
+
issuer: learned.issuer,
|
|
27976
|
+
clientId: learned.clientId,
|
|
27977
|
+
audience: learned.audience,
|
|
27978
|
+
nativeSignup: learned.nativeSignup
|
|
27592
27979
|
})
|
|
27593
27980
|
);
|
|
27594
27981
|
}
|
|
27595
27982
|
}
|
|
27983
|
+
const connectionsRan = outcomes.some(
|
|
27984
|
+
(outcome) => outcome.step === IDENTITY_CONNECTIONS_STEP && !outcome.skipped
|
|
27985
|
+
);
|
|
27596
27986
|
return {
|
|
27597
|
-
status: ran ? "deployed" : "unchanged",
|
|
27987
|
+
status: ran || connectionsRan ? "deployed" : "unchanged",
|
|
27598
27988
|
steps: outcomes.map((outcome) => ({
|
|
27599
27989
|
step: outcome.step,
|
|
27600
27990
|
skipped: outcome.skipped
|
|
27601
27991
|
})),
|
|
27602
|
-
|
|
27603
|
-
|
|
27992
|
+
issuer: learned?.issuer ?? known?.issuer,
|
|
27993
|
+
clientId: learned?.clientId ?? known?.clientId,
|
|
27994
|
+
connections: redirectAddressesOf(
|
|
27995
|
+
stepOf(
|
|
27996
|
+
await readLedger(resolved.state),
|
|
27997
|
+
"identity",
|
|
27998
|
+
IDENTITY_CONNECTIONS_STEP
|
|
27999
|
+
)?.output
|
|
28000
|
+
)
|
|
28001
|
+
};
|
|
28002
|
+
}
|
|
28003
|
+
function connectionsStep(resolved, projectId, client, wanted) {
|
|
28004
|
+
const {
|
|
28005
|
+
putProjectIdentityConnection,
|
|
28006
|
+
listProjectIdentityConnections,
|
|
28007
|
+
deleteProjectIdentityConnection,
|
|
28008
|
+
listSecrets
|
|
28009
|
+
} = client;
|
|
28010
|
+
if (!putProjectIdentityConnection || !listProjectIdentityConnections || !deleteProjectIdentityConnection || !listSecrets)
|
|
28011
|
+
throw new Error("The platform client has no login connection calls.");
|
|
28012
|
+
const stamps = async () => {
|
|
28013
|
+
const names = referencedSecretNames(wanted);
|
|
28014
|
+
if (names.length === 0) return {};
|
|
28015
|
+
const listed = await listSecrets.call(client, projectId);
|
|
28016
|
+
const byName2 = new Map(listed.secrets.map((s) => [s.name, s.updatedAtUtc]));
|
|
28017
|
+
const missing = names.filter((name) => !byName2.has(name));
|
|
28018
|
+
if (missing.length > 0)
|
|
28019
|
+
throw new IdentityDeployError(
|
|
28020
|
+
"IDENTITY_SECRET_MISSING",
|
|
28021
|
+
`The secret ${missing.join(", ")} is not set for this project, and a login connection needs it.`,
|
|
28022
|
+
`Run \`apitogo secrets set ${missing[0]}\` with the value from the provider, then deploy again.`
|
|
28023
|
+
);
|
|
28024
|
+
return Object.fromEntries(
|
|
28025
|
+
names.map((name) => [name, byName2.get(name)])
|
|
28026
|
+
);
|
|
28027
|
+
};
|
|
28028
|
+
const sameAddress = (a, b) => a.replace(/\/+$/, "").toLowerCase() === b.replace(/\/+$/, "").toLowerCase();
|
|
28029
|
+
return {
|
|
28030
|
+
name: IDENTITY_CONNECTIONS_STEP,
|
|
28031
|
+
hash: () => identityConnectionsHash(projectId, resolved),
|
|
28032
|
+
run: async () => {
|
|
28033
|
+
const recorded = await stamps();
|
|
28034
|
+
const existing = await listProjectIdentityConnections.call(
|
|
28035
|
+
client,
|
|
28036
|
+
projectId
|
|
28037
|
+
);
|
|
28038
|
+
const redirects = {};
|
|
28039
|
+
for (const connection of wanted) {
|
|
28040
|
+
try {
|
|
28041
|
+
const saved = await putProjectIdentityConnection.call(
|
|
28042
|
+
client,
|
|
28043
|
+
projectId,
|
|
28044
|
+
connection.id,
|
|
28045
|
+
connectionBody(connection)
|
|
28046
|
+
);
|
|
28047
|
+
redirects[`redirect.${connection.id}`] = saved.redirectUri;
|
|
28048
|
+
} catch (error) {
|
|
28049
|
+
return connectionRejected(error, connection.id);
|
|
28050
|
+
}
|
|
28051
|
+
}
|
|
28052
|
+
for (const old of existing)
|
|
28053
|
+
if (!wanted.some((connection) => connection.id === old.id))
|
|
28054
|
+
await deleteProjectIdentityConnection.call(client, projectId, old.id);
|
|
28055
|
+
return {
|
|
28056
|
+
ids: wanted.map((connection) => connection.id).join(","),
|
|
28057
|
+
secretsStamp: stableJson(recorded),
|
|
28058
|
+
...redirects
|
|
28059
|
+
};
|
|
28060
|
+
},
|
|
28061
|
+
// The platform still has exactly these connections, and none of the secrets was changed since.
|
|
28062
|
+
stillValid: async () => {
|
|
28063
|
+
const found = await listProjectIdentityConnections.call(
|
|
28064
|
+
client,
|
|
28065
|
+
projectId
|
|
28066
|
+
);
|
|
28067
|
+
if (found.length !== wanted.length) return false;
|
|
28068
|
+
for (const connection of wanted) {
|
|
28069
|
+
const there = found.find((f) => f.id === connection.id);
|
|
28070
|
+
if (!there || there.clientId !== connection.clientId || !sameAddress(there.authority, connection.authority))
|
|
28071
|
+
return false;
|
|
28072
|
+
}
|
|
28073
|
+
const before = stepOf(
|
|
28074
|
+
await readLedger(resolved.state),
|
|
28075
|
+
"identity",
|
|
28076
|
+
IDENTITY_CONNECTIONS_STEP
|
|
28077
|
+
)?.output?.secretsStamp;
|
|
28078
|
+
return before === void 0 || before === stableJson(await stamps());
|
|
28079
|
+
}
|
|
27604
28080
|
};
|
|
27605
28081
|
}
|
|
27606
28082
|
|
|
@@ -27742,6 +28218,12 @@ async function planPlansService(resolved, ledger, plansRemote2) {
|
|
|
27742
28218
|
const archiving = await plansToArchive(projectId, inputs.wanted, plansRemote2);
|
|
27743
28219
|
const details = [
|
|
27744
28220
|
...inputs.wanted.length === 0 ? ["plans: none in the file"] : inputs.wanted.map((plan2) => `plan ${plan2.sku}: ${plan2.displayName}`),
|
|
28221
|
+
// A contact-us plan with a price in the file waits to be sold (the full template's example, API-124).
|
|
28222
|
+
...inputs.filePlans.filter(
|
|
28223
|
+
(plan2) => plan2.pricingModel === "custom" && typeof plan2.priceAmount === "number" && plan2.priceAmount > 0
|
|
28224
|
+
).map(
|
|
28225
|
+
(plan2) => `plan ${plan2.sku}: "contact us" for now. On a Starter or Professional project, sell it at ${plan2.priceAmount} ${(plan2.priceCurrency ?? "usd").toUpperCase()} a ${plan2.billingInterval ?? "month"} with: apitogo config --plan-sku ${plan2.sku} --plan-price ${plan2.priceAmount}`
|
|
28226
|
+
),
|
|
27745
28227
|
...archiving.map(
|
|
27746
28228
|
(sku) => `plan ${sku}: will archive (no longer in the plans file: it leaves the pricing page and takes no new subscribers; current subscribers keep access)`
|
|
27747
28229
|
),
|
|
@@ -27941,7 +28423,7 @@ async function planPortalService(resolved, ledger) {
|
|
|
27941
28423
|
const hash2 = await portalBuildHash(resolved, prepared);
|
|
27942
28424
|
const previous = stepOf(ledger, "portal", "build");
|
|
27943
28425
|
const built = await exists5(
|
|
27944
|
-
|
|
28426
|
+
path81.join(resolved.state.buildDir, "index.html")
|
|
27945
28427
|
);
|
|
27946
28428
|
if (previous?.status === "succeeded" && previous.hash === hash2 && built) {
|
|
27947
28429
|
return {
|
|
@@ -28327,7 +28809,7 @@ async function statusV2(dir, only, options = {}) {
|
|
|
28327
28809
|
if (service === "portal") {
|
|
28328
28810
|
const build2 = stepOf(ledger, "portal", "build");
|
|
28329
28811
|
const built = await exists5(
|
|
28330
|
-
|
|
28812
|
+
path81.join(resolved.state.buildDir, "index.html")
|
|
28331
28813
|
);
|
|
28332
28814
|
const state = build2?.status === "failed" ? "build-failed" : build2?.status === "succeeded" && built ? "built" : "not-built";
|
|
28333
28815
|
services.push({
|
|
@@ -28338,7 +28820,7 @@ async function statusV2(dir, only, options = {}) {
|
|
|
28338
28820
|
...build2?.finishedAt ? [`last build: ${build2.finishedAt}`] : [],
|
|
28339
28821
|
...build2?.status === "failed" ? [`error: ${build2.error ?? "unknown"}`] : [],
|
|
28340
28822
|
...state === "built" ? [
|
|
28341
|
-
`output: ${
|
|
28823
|
+
`output: ${path81.relative(resolved.configDir, resolved.state.buildDir)}`
|
|
28342
28824
|
] : [],
|
|
28343
28825
|
...url2 ? [`url: ${url2}`] : ["url: not known (portal is not deployed yet)"],
|
|
28344
28826
|
...await hostingLine(resolved, manifest, options.hostingRemote)
|
|
@@ -28405,7 +28887,7 @@ function formatStatus(output4) {
|
|
|
28405
28887
|
|
|
28406
28888
|
// src/cli/deploy/static-web-config.ts
|
|
28407
28889
|
import { access as access10, writeFile as writeFile22 } from "node:fs/promises";
|
|
28408
|
-
import
|
|
28890
|
+
import path82 from "node:path";
|
|
28409
28891
|
var STATIC_WEB_CONFIG_FILE = "staticwebapp.config.json";
|
|
28410
28892
|
var STATIC_WEB_CONFIG_VERSION = 3;
|
|
28411
28893
|
var CLIENT_ONLY_ROUTES = [
|
|
@@ -28423,7 +28905,7 @@ var DEFAULT_STATIC_WEB_CONFIG = {
|
|
|
28423
28905
|
}
|
|
28424
28906
|
};
|
|
28425
28907
|
async function ensureStaticWebConfig(buildDir) {
|
|
28426
|
-
const file =
|
|
28908
|
+
const file = path82.join(buildDir, STATIC_WEB_CONFIG_FILE);
|
|
28427
28909
|
try {
|
|
28428
28910
|
await access10(file);
|
|
28429
28911
|
return false;
|
|
@@ -28680,7 +29162,8 @@ async function deployPlansService(options, deps, projectId, named) {
|
|
|
28680
29162
|
steps: result.steps,
|
|
28681
29163
|
url: result.apiBaseUrl,
|
|
28682
29164
|
summary: `${planCount} plan${planCount === 1 ? "" : "s"}${billing && billing !== "none" ? `, billing: ${billing}` : ""}`,
|
|
28683
|
-
...result.notes ? { notes: result.notes } : {}
|
|
29165
|
+
...result.notes ? { notes: result.notes } : {},
|
|
29166
|
+
...result.stripeWebhook ? { stripeWebhook: result.stripeWebhook } : {}
|
|
28684
29167
|
};
|
|
28685
29168
|
}
|
|
28686
29169
|
async function recordPortalAddressBeforeBuild(dir, deps, projectId) {
|
|
@@ -28777,7 +29260,7 @@ function formatDeploy(output4) {
|
|
|
28777
29260
|
// src/cli/deploy/command.ts
|
|
28778
29261
|
async function ensureLinkedProject(argv, deps) {
|
|
28779
29262
|
if (argv.projectId?.trim() || deps.envProjectId?.trim()) return true;
|
|
28780
|
-
const dir =
|
|
29263
|
+
const dir = path83.resolve(process.cwd(), argv.dir);
|
|
28781
29264
|
let linked;
|
|
28782
29265
|
try {
|
|
28783
29266
|
linked = Boolean(loadConfigV2(dir).resolved.project.id);
|
|
@@ -28827,26 +29310,37 @@ async function deployProject(argv) {
|
|
|
28827
29310
|
);
|
|
28828
29311
|
try {
|
|
28829
29312
|
const config2 = loadConfig();
|
|
28830
|
-
const
|
|
29313
|
+
const client = new PlatformApiClient(config2);
|
|
29314
|
+
const dir = path83.resolve(process.cwd(), argv.dir);
|
|
29315
|
+
const deploy = (only) => deployV2(
|
|
28831
29316
|
{
|
|
28832
|
-
dir
|
|
28833
|
-
only: Array.isArray(argv.only) ? argv.only.join(",") : argv.only,
|
|
29317
|
+
dir,
|
|
29318
|
+
only: only ?? (Array.isArray(argv.only) ? argv.only.join(",") : argv.only),
|
|
28834
29319
|
projectId: argv.projectId,
|
|
28835
29320
|
env: argv.env,
|
|
28836
29321
|
force: argv.force === true,
|
|
28837
29322
|
yes: argv.yes === true,
|
|
28838
29323
|
jsonOnly: quiet
|
|
28839
29324
|
},
|
|
28840
|
-
{
|
|
28841
|
-
client: new PlatformApiClient(config2),
|
|
28842
|
-
envProjectId: config2.defaultProjectId
|
|
28843
|
-
}
|
|
29325
|
+
{ client, envProjectId: config2.defaultProjectId }
|
|
28844
29326
|
);
|
|
29327
|
+
const output4 = await deploy();
|
|
28845
29328
|
if (json) {
|
|
28846
29329
|
console.log(JSON.stringify({ status: "succeeded", ...output4 }, null, 2));
|
|
28847
29330
|
return;
|
|
28848
29331
|
}
|
|
28849
29332
|
for (const line2 of formatDeploy(output4)) printDiagnosticsToConsole(line2);
|
|
29333
|
+
const webhook = await offerStripeWebhookSecret(output4, {
|
|
29334
|
+
interactive,
|
|
29335
|
+
client: () => client,
|
|
29336
|
+
askHidden: (prompt) => readHidden(prompt),
|
|
29337
|
+
print: printDiagnosticsToConsole,
|
|
29338
|
+
wireConfig: () => wireKnownSecret({ action: "set", dir }, STRIPE_WEBHOOK_SECRET),
|
|
29339
|
+
redeployPlans: () => deploy("plans")
|
|
29340
|
+
});
|
|
29341
|
+
if (webhook.status === "saved")
|
|
29342
|
+
for (const line2 of formatDeploy(webhook.output))
|
|
29343
|
+
printDiagnosticsToConsole(line2);
|
|
28850
29344
|
printResultToConsole("Deploy finished.");
|
|
28851
29345
|
} catch (error) {
|
|
28852
29346
|
await reportFailure(error, json);
|
|
@@ -28925,7 +29419,7 @@ var deploy_default = {
|
|
|
28925
29419
|
}),
|
|
28926
29420
|
handler: async (argv) => {
|
|
28927
29421
|
await captureEvent({ argv, event: "apitogo deploy" });
|
|
28928
|
-
if (await isConfigV2(
|
|
29422
|
+
if (await isConfigV2(path84.resolve(process.cwd(), argv.dir))) {
|
|
28929
29423
|
await deployProject(argv);
|
|
28930
29424
|
return;
|
|
28931
29425
|
}
|
|
@@ -28935,7 +29429,7 @@ var deploy_default = {
|
|
|
28935
29429
|
|
|
28936
29430
|
// src/cli/export/handler.ts
|
|
28937
29431
|
import { mkdir as mkdir19, writeFile as writeFile23 } from "node:fs/promises";
|
|
28938
|
-
import
|
|
29432
|
+
import path85 from "node:path";
|
|
28939
29433
|
init_invariant();
|
|
28940
29434
|
var ExportError = class extends ApitogoError {
|
|
28941
29435
|
constructor(message, developerHint) {
|
|
@@ -28949,7 +29443,7 @@ async function exportOpenApi(args, deps) {
|
|
|
28949
29443
|
`Cannot export "${args.target}".`,
|
|
28950
29444
|
"Only `apitogo export openapi` exists."
|
|
28951
29445
|
);
|
|
28952
|
-
const dir =
|
|
29446
|
+
const dir = path85.resolve(process.cwd(), args.dir ?? ".");
|
|
28953
29447
|
if (!await isConfigV2(dir))
|
|
28954
29448
|
throw new ExportError(
|
|
28955
29449
|
"This command needs a config version 2 project.",
|
|
@@ -28974,8 +29468,8 @@ async function exportOpenApi(args, deps) {
|
|
|
28974
29468
|
throw error;
|
|
28975
29469
|
}
|
|
28976
29470
|
if (!args.out) return { text: text2 };
|
|
28977
|
-
const file =
|
|
28978
|
-
await mkdir19(
|
|
29471
|
+
const file = path85.resolve(process.cwd(), args.out);
|
|
29472
|
+
await mkdir19(path85.dirname(file), { recursive: true });
|
|
28979
29473
|
await writeFile23(file, text2);
|
|
28980
29474
|
return { text: text2, file };
|
|
28981
29475
|
}
|
|
@@ -29251,7 +29745,7 @@ var logout_default = {
|
|
|
29251
29745
|
};
|
|
29252
29746
|
|
|
29253
29747
|
// src/cli/portal/commands.ts
|
|
29254
|
-
import
|
|
29748
|
+
import path86 from "node:path";
|
|
29255
29749
|
init_invariant();
|
|
29256
29750
|
function parseOnly(value) {
|
|
29257
29751
|
if (value === void 0) return void 0;
|
|
@@ -29268,7 +29762,7 @@ function parseOnly(value) {
|
|
|
29268
29762
|
async function run2(command2, argv, compute, format) {
|
|
29269
29763
|
const json = argv.json === true;
|
|
29270
29764
|
try {
|
|
29271
|
-
const dir =
|
|
29765
|
+
const dir = path86.resolve(process.cwd(), argv.dir ?? ".");
|
|
29272
29766
|
if (!await isConfigV2(dir)) {
|
|
29273
29767
|
throw new ServiceEditError(
|
|
29274
29768
|
`${command2} works with a config version 2 project, and there is none in ${dir}.`,
|
|
@@ -29493,14 +29987,14 @@ var remove_default = {
|
|
|
29493
29987
|
|
|
29494
29988
|
// src/cli/dev/handler.ts
|
|
29495
29989
|
init_joinUrl();
|
|
29496
|
-
import
|
|
29990
|
+
import path89 from "node:path";
|
|
29497
29991
|
|
|
29498
29992
|
// src/vite/dev-server.ts
|
|
29499
29993
|
init_logger();
|
|
29500
29994
|
import fs11 from "node:fs/promises";
|
|
29501
29995
|
import http from "node:http";
|
|
29502
29996
|
import https from "node:https";
|
|
29503
|
-
import
|
|
29997
|
+
import path88 from "node:path";
|
|
29504
29998
|
import { createHttpTerminator } from "http-terminator";
|
|
29505
29999
|
import {
|
|
29506
30000
|
createServer as createViteServer,
|
|
@@ -29533,7 +30027,7 @@ init_loader();
|
|
|
29533
30027
|
// src/vite/pagefind-dev-index.ts
|
|
29534
30028
|
init_invariant();
|
|
29535
30029
|
init_joinUrl();
|
|
29536
|
-
import
|
|
30030
|
+
import path87 from "node:path";
|
|
29537
30031
|
import { createIndex as createIndex2 } from "pagefind";
|
|
29538
30032
|
import { isRunnableDevEnvironment } from "vite";
|
|
29539
30033
|
async function* buildPagefindDevIndex(vite, config2) {
|
|
@@ -29586,7 +30080,7 @@ async function* buildPagefindDevIndex(vite, config2) {
|
|
|
29586
30080
|
path: urlPath
|
|
29587
30081
|
};
|
|
29588
30082
|
}
|
|
29589
|
-
const outputPath =
|
|
30083
|
+
const outputPath = path87.join(vite.config.publicDir, "pagefind");
|
|
29590
30084
|
await pagefindIndex.writeFiles({ outputPath });
|
|
29591
30085
|
yield { type: "complete", success: true, indexed };
|
|
29592
30086
|
}
|
|
@@ -29605,9 +30099,9 @@ var DevServer = class {
|
|
|
29605
30099
|
this.protocol = "https";
|
|
29606
30100
|
const { dir } = this.options;
|
|
29607
30101
|
const [key, cert, ca] = await Promise.all([
|
|
29608
|
-
fs11.readFile(
|
|
29609
|
-
fs11.readFile(
|
|
29610
|
-
config2.https.ca ? fs11.readFile(
|
|
30102
|
+
fs11.readFile(path88.resolve(dir, config2.https.key)),
|
|
30103
|
+
fs11.readFile(path88.resolve(dir, config2.https.cert)),
|
|
30104
|
+
config2.https.ca ? fs11.readFile(path88.resolve(dir, config2.https.ca)) : void 0
|
|
29611
30105
|
]);
|
|
29612
30106
|
return https.createServer({ key, cert, ca });
|
|
29613
30107
|
}
|
|
@@ -29624,7 +30118,7 @@ var DevServer = class {
|
|
|
29624
30118
|
);
|
|
29625
30119
|
const server = await this.createNodeServer(config2);
|
|
29626
30120
|
if (config2.search?.type === "pagefind" && viteConfig.publicDir !== false) {
|
|
29627
|
-
const publicRoot =
|
|
30121
|
+
const publicRoot = path88.resolve(
|
|
29628
30122
|
this.options.dir,
|
|
29629
30123
|
typeof viteConfig.publicDir === "string" ? viteConfig.publicDir : "public"
|
|
29630
30124
|
);
|
|
@@ -29641,7 +30135,7 @@ var DevServer = class {
|
|
|
29641
30135
|
// built-in transform middleware which would treat the path as a static asset.
|
|
29642
30136
|
name: "apitogo:entry-client",
|
|
29643
30137
|
configureServer(server2) {
|
|
29644
|
-
const entryPath =
|
|
30138
|
+
const entryPath = path88.posix.join(
|
|
29645
30139
|
server2.config.base,
|
|
29646
30140
|
"/__z/entry.client.tsx"
|
|
29647
30141
|
);
|
|
@@ -29806,7 +30300,7 @@ init_package_json();
|
|
|
29806
30300
|
async function dev(argv) {
|
|
29807
30301
|
const packageJson3 = getZudokuPackageJson();
|
|
29808
30302
|
process.env.NODE_ENV = "development";
|
|
29809
|
-
let dir =
|
|
30303
|
+
let dir = path89.resolve(process.cwd(), argv.dir);
|
|
29810
30304
|
if (await isConfigV2(dir)) {
|
|
29811
30305
|
const { root, prepared } = await preparePortalWorkspace(dir, "development");
|
|
29812
30306
|
dir = root;
|
|
@@ -29904,142 +30398,6 @@ var run_default = {
|
|
|
29904
30398
|
}
|
|
29905
30399
|
};
|
|
29906
30400
|
|
|
29907
|
-
// src/cli/secrets/handler.ts
|
|
29908
|
-
import path89 from "node:path";
|
|
29909
|
-
init_invariant();
|
|
29910
|
-
var SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
29911
|
-
var SecretsError = class extends ApitogoError {
|
|
29912
|
-
constructor(message, developerHint) {
|
|
29913
|
-
super(message, { developerHint, title: "Cannot change secrets" });
|
|
29914
|
-
this.name = "SecretsError";
|
|
29915
|
-
}
|
|
29916
|
-
};
|
|
29917
|
-
async function resolveProjectId2(argv, deps) {
|
|
29918
|
-
const explicit = argv.projectId?.trim();
|
|
29919
|
-
if (explicit) return explicit;
|
|
29920
|
-
const dir = path89.resolve(process.cwd(), argv.dir ?? ".");
|
|
29921
|
-
if (await isConfigV2(dir)) {
|
|
29922
|
-
const id = loadConfigV2(dir).resolved.project.id;
|
|
29923
|
-
if (id) return id;
|
|
29924
|
-
}
|
|
29925
|
-
if (deps.envProjectId?.trim()) return deps.envProjectId.trim();
|
|
29926
|
-
throw new SecretsError(
|
|
29927
|
-
"This folder is not linked to a project yet.",
|
|
29928
|
-
"Pass --project-id <id>, run `apitogo deploy` once, or set APITOGO_DEFAULT_PROJECT_ID."
|
|
29929
|
-
);
|
|
29930
|
-
}
|
|
29931
|
-
function requireName(name, action) {
|
|
29932
|
-
if (!name) {
|
|
29933
|
-
throw new SecretsError(
|
|
29934
|
-
`secrets ${action} needs a name.`,
|
|
29935
|
-
`For example: apitogo secrets ${action} STRIPE_API_KEY`
|
|
29936
|
-
);
|
|
29937
|
-
}
|
|
29938
|
-
if (!SECRET_NAME.test(name)) {
|
|
29939
|
-
throw new SecretsError(
|
|
29940
|
-
`"${name}" is not a valid secret name.`,
|
|
29941
|
-
"Use letters, digits and underscores, starting with a letter or underscore (at most 128 characters)."
|
|
29942
|
-
);
|
|
29943
|
-
}
|
|
29944
|
-
return name;
|
|
29945
|
-
}
|
|
29946
|
-
async function runSecrets(argv, deps) {
|
|
29947
|
-
if (!["set", "list", "rm"].includes(argv.action)) {
|
|
29948
|
-
throw new SecretsError(
|
|
29949
|
-
`Unknown action: ${argv.action}.`,
|
|
29950
|
-
"Use set, list or rm."
|
|
29951
|
-
);
|
|
29952
|
-
}
|
|
29953
|
-
if (argv.action === "list") {
|
|
29954
|
-
const projectId2 = await resolveProjectId2(argv, deps);
|
|
29955
|
-
const { secrets: secrets2 } = await deps.client.listSecrets(projectId2);
|
|
29956
|
-
return { action: "list", projectId: projectId2, secrets: secrets2 };
|
|
29957
|
-
}
|
|
29958
|
-
const name = requireName(argv.name, argv.action);
|
|
29959
|
-
const projectId = await resolveProjectId2(argv, deps);
|
|
29960
|
-
if (argv.action === "rm") {
|
|
29961
|
-
if (argv.yes !== true) {
|
|
29962
|
-
if (!deps.isTerminal)
|
|
29963
|
-
throw new SecretsError(
|
|
29964
|
-
"This is not an interactive terminal.",
|
|
29965
|
-
"Pass --yes to remove the secret."
|
|
29966
|
-
);
|
|
29967
|
-
if (!await deps.confirm(`Remove secret ${name}? (y/N) `))
|
|
29968
|
-
throw new SecretsError("Cancelled. Nothing was removed.");
|
|
29969
|
-
}
|
|
29970
|
-
await deps.client.deleteSecret(projectId, name);
|
|
29971
|
-
return { action: "rm", projectId, name };
|
|
29972
|
-
}
|
|
29973
|
-
let value;
|
|
29974
|
-
if (deps.isTerminal) {
|
|
29975
|
-
value = await deps.askValue(`Value for ${name} (hidden): `);
|
|
29976
|
-
const again = await deps.askValue("Type it again: ");
|
|
29977
|
-
if (again !== value)
|
|
29978
|
-
throw new SecretsError(
|
|
29979
|
-
"The two values are not the same. Nothing was saved."
|
|
29980
|
-
);
|
|
29981
|
-
} else {
|
|
29982
|
-
value = await deps.readPiped();
|
|
29983
|
-
}
|
|
29984
|
-
if (value.length === 0)
|
|
29985
|
-
throw new SecretsError("The value is empty. Nothing was saved.");
|
|
29986
|
-
const result = await deps.client.setSecret(projectId, name, value);
|
|
29987
|
-
return {
|
|
29988
|
-
action: "set",
|
|
29989
|
-
projectId,
|
|
29990
|
-
name: result.name,
|
|
29991
|
-
created: result.created
|
|
29992
|
-
};
|
|
29993
|
-
}
|
|
29994
|
-
function formatSecrets(result) {
|
|
29995
|
-
if (result.action === "list") {
|
|
29996
|
-
return result.secrets.length === 0 ? ["No secrets are set for this project."] : result.secrets.map((s) => `${s.name} (updated ${s.updatedAtUtc})`);
|
|
29997
|
-
}
|
|
29998
|
-
if (result.action === "set") {
|
|
29999
|
-
return [
|
|
30000
|
-
`Secret ${result.name} ${result.created ? "created" : "replaced"}.`
|
|
30001
|
-
];
|
|
30002
|
-
}
|
|
30003
|
-
return [`Secret ${result.name} removed.`];
|
|
30004
|
-
}
|
|
30005
|
-
async function secrets(argv) {
|
|
30006
|
-
const json = argv.json === true;
|
|
30007
|
-
try {
|
|
30008
|
-
const config2 = loadConfig();
|
|
30009
|
-
const result = await runSecrets(argv, {
|
|
30010
|
-
client: new PlatformApiClient(config2),
|
|
30011
|
-
envProjectId: config2.defaultProjectId,
|
|
30012
|
-
isTerminal: process.stdin.isTTY === true && !json,
|
|
30013
|
-
askValue: (prompt) => readHidden(prompt),
|
|
30014
|
-
readPiped: () => readStdin(),
|
|
30015
|
-
confirm
|
|
30016
|
-
});
|
|
30017
|
-
if (json) {
|
|
30018
|
-
console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));
|
|
30019
|
-
return;
|
|
30020
|
-
}
|
|
30021
|
-
for (const line2 of formatSecrets(result)) printDiagnosticsToConsole(line2);
|
|
30022
|
-
if (result.action !== "list") printResultToConsole("Done.");
|
|
30023
|
-
} catch (error) {
|
|
30024
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
30025
|
-
const hint = error instanceof ApitogoError ? error.developerHint : void 0;
|
|
30026
|
-
if (json) {
|
|
30027
|
-
console.log(
|
|
30028
|
-
JSON.stringify({
|
|
30029
|
-
status: "failed",
|
|
30030
|
-
error: message,
|
|
30031
|
-
hint: hint ?? null
|
|
30032
|
-
})
|
|
30033
|
-
);
|
|
30034
|
-
process.exitCode = 1;
|
|
30035
|
-
return;
|
|
30036
|
-
}
|
|
30037
|
-
await printCriticalFailureToConsoleAndExit(
|
|
30038
|
-
hint ? `${message} ${hint}` : message
|
|
30039
|
-
);
|
|
30040
|
-
}
|
|
30041
|
-
}
|
|
30042
|
-
|
|
30043
30401
|
// src/cli/cmds/secrets.ts
|
|
30044
30402
|
var secrets_default = {
|
|
30045
30403
|
desc: "Set, list or remove the project's secrets (values are hidden, encrypted on the platform and never shown again)",
|