@lukoweb/apitogo 0.2.20 → 0.2.22
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 +1508 -227
- package/dist/declarations/cli/platform/client/platform-api.d.ts +22 -0
- package/dist/declarations/cli/platform/tools/lifecycle.d.ts +3 -0
- package/docs/configuration/gateway.md +221 -0
- package/package.json +1 -1
- package/src/config/v2/index.ts +2 -0
- package/src/config/v2/manifest.ts +89 -0
- package/src/config/v2/services.ts +4 -2
- package/src/vite/config.ts +2 -2
- package/src/vite/watch-ignore.ts +34 -9
package/dist/cli/cli.js
CHANGED
|
@@ -5729,8 +5729,8 @@ async function warnIfOutdatedVersion(currentVersion) {
|
|
|
5729
5729
|
if (!process.env.ZUDOKU_OVERRIDE_CI_TO_TEST && (process.env.CI || process.env.ZUDOKU_DISABLE_UPDATE_CHECK)) {
|
|
5730
5730
|
return false;
|
|
5731
5731
|
}
|
|
5732
|
-
const versionCheckInfo = await getVersionCheckInfo();
|
|
5733
|
-
if (!versionCheckInfo
|
|
5732
|
+
const versionCheckInfo = await getVersionCheckInfo().catch(() => void 0);
|
|
5733
|
+
if (!versionCheckInfo?.latestVersion) {
|
|
5734
5734
|
return false;
|
|
5735
5735
|
}
|
|
5736
5736
|
const shouldWarn = gt(versionCheckInfo.latestVersion, currentVersion) && !process.env.ZUDOKU_INTERNAL_DEV;
|
|
@@ -5814,19 +5814,19 @@ import { createInterface as createInterface2 } from "node:readline/promises";
|
|
|
5814
5814
|
|
|
5815
5815
|
// src/config/v2/errors.ts
|
|
5816
5816
|
init_invariant();
|
|
5817
|
-
function formatConfigIssue(
|
|
5818
|
-
const hint =
|
|
5819
|
-
const rest =
|
|
5820
|
-
const startsWithPath =
|
|
5821
|
-
const text = startsWithPath ?
|
|
5822
|
-
return `${text}${hint} [${
|
|
5817
|
+
function formatConfigIssue(issue4) {
|
|
5818
|
+
const hint = issue4.hint ? ` ${issue4.hint}` : "";
|
|
5819
|
+
const rest = issue4.message.slice(issue4.path.length);
|
|
5820
|
+
const startsWithPath = issue4.path !== "" && issue4.message.startsWith(issue4.path) && (rest === "" || /^[\s:]/.test(rest));
|
|
5821
|
+
const text = startsWithPath ? issue4.message : `${issue4.path === "" ? "(root)" : issue4.path}: ${issue4.message}`;
|
|
5822
|
+
return `${text}${hint} [${issue4.code}]`;
|
|
5823
5823
|
}
|
|
5824
5824
|
var ConfigError = class extends ApitogoError {
|
|
5825
5825
|
issues;
|
|
5826
5826
|
constructor(file, issues) {
|
|
5827
5827
|
super(
|
|
5828
5828
|
`${file} has ${issues.length} problem${issues.length === 1 ? "" : "s"}:
|
|
5829
|
-
${issues.map((
|
|
5829
|
+
${issues.map((issue4) => ` - ${formatConfigIssue(issue4)}`).join("\n")}`,
|
|
5830
5830
|
{ title: "Invalid apitogo config" }
|
|
5831
5831
|
);
|
|
5832
5832
|
this.name = "ConfigError";
|
|
@@ -6247,17 +6247,17 @@ function unknownKeyHint(fullPath, key, known) {
|
|
|
6247
6247
|
if (closest) return `Did you mean "${closest.candidate}"?`;
|
|
6248
6248
|
return known.length > 0 ? `Allowed keys: ${known.join(", ")}.` : void 0;
|
|
6249
6249
|
}
|
|
6250
|
-
function mapIssue(
|
|
6251
|
-
const path103 = pathToString(
|
|
6252
|
-
switch (
|
|
6250
|
+
function mapIssue(issue4) {
|
|
6251
|
+
const path103 = pathToString(issue4.path);
|
|
6252
|
+
switch (issue4.code) {
|
|
6253
6253
|
case "unrecognized_keys":
|
|
6254
|
-
return
|
|
6255
|
-
const fullPath = pathToString([...
|
|
6254
|
+
return issue4.keys.map((key) => {
|
|
6255
|
+
const fullPath = pathToString([...issue4.path, key]);
|
|
6256
6256
|
return {
|
|
6257
6257
|
code: "CONFIG_UNKNOWN_KEY",
|
|
6258
6258
|
path: fullPath,
|
|
6259
6259
|
message: `Unknown key "${key}".`,
|
|
6260
|
-
hint: unknownKeyHint(fullPath, key, knownKeysAt(
|
|
6260
|
+
hint: unknownKeyHint(fullPath, key, knownKeysAt(issue4.path))
|
|
6261
6261
|
};
|
|
6262
6262
|
});
|
|
6263
6263
|
case "invalid_value":
|
|
@@ -6265,19 +6265,19 @@ function mapIssue(issue3) {
|
|
|
6265
6265
|
{
|
|
6266
6266
|
code: "CONFIG_INVALID_VALUE",
|
|
6267
6267
|
path: path103,
|
|
6268
|
-
message: `must be one of: ${
|
|
6268
|
+
message: `must be one of: ${issue4.values.map((v) => JSON.stringify(v)).join(", ")}.`
|
|
6269
6269
|
}
|
|
6270
6270
|
];
|
|
6271
6271
|
case "custom": {
|
|
6272
|
-
const code = typeof
|
|
6273
|
-
return [{ code, path: path103, message:
|
|
6272
|
+
const code = typeof issue4.params?.code === "string" ? issue4.params.code : "CONFIG_INVALID_VALUE";
|
|
6273
|
+
return [{ code, path: path103, message: issue4.message ?? "is invalid." }];
|
|
6274
6274
|
}
|
|
6275
6275
|
case "invalid_type":
|
|
6276
6276
|
return [
|
|
6277
|
-
/received undefined/.test(
|
|
6277
|
+
/received undefined/.test(issue4.message) ? { code: "CONFIG_MISSING_FIELD", path: path103, message: "is required." } : { code: "CONFIG_INVALID_VALUE", path: path103, message: issue4.message }
|
|
6278
6278
|
];
|
|
6279
6279
|
default:
|
|
6280
|
-
return [{ code: "CONFIG_INVALID_VALUE", path: path103, message:
|
|
6280
|
+
return [{ code: "CONFIG_INVALID_VALUE", path: path103, message: issue4.message }];
|
|
6281
6281
|
}
|
|
6282
6282
|
}
|
|
6283
6283
|
function isRecord(value) {
|
|
@@ -6727,7 +6727,7 @@ function dependencyRules(config2, resolved, out) {
|
|
|
6727
6727
|
function checkDependencies(config2, resolved) {
|
|
6728
6728
|
const issues = [];
|
|
6729
6729
|
dependencyRules(config2, resolved, {
|
|
6730
|
-
error: (
|
|
6730
|
+
error: (issue4) => void issues.push(issue4),
|
|
6731
6731
|
warn: () => void 0
|
|
6732
6732
|
});
|
|
6733
6733
|
return issues;
|
|
@@ -7197,8 +7197,8 @@ async function checkConfigV2(config2, resolved, context = {}) {
|
|
|
7197
7197
|
"online rules were not run (provider capabilities, origin reachability, OIDC discovery, tier limits)"
|
|
7198
7198
|
];
|
|
7199
7199
|
const out = {
|
|
7200
|
-
error: (
|
|
7201
|
-
warn: (
|
|
7200
|
+
error: (issue4) => errors.push(issue4),
|
|
7201
|
+
warn: (issue4) => warnings.push(issue4)
|
|
7202
7202
|
};
|
|
7203
7203
|
dependencyRules(config2, resolved, out);
|
|
7204
7204
|
secretRules(config2, context, out, skipped);
|
|
@@ -7457,6 +7457,57 @@ function withGatewayEntry(existing, info) {
|
|
|
7457
7457
|
}
|
|
7458
7458
|
};
|
|
7459
7459
|
}
|
|
7460
|
+
function withIdentityEntry(existing, info) {
|
|
7461
|
+
const base = existing ?? parseManifest({
|
|
7462
|
+
schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
|
|
7463
|
+
project: { name: info.projectName }
|
|
7464
|
+
});
|
|
7465
|
+
return {
|
|
7466
|
+
...base,
|
|
7467
|
+
schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
|
|
7468
|
+
generatedAt: (info.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
7469
|
+
preview: void 0,
|
|
7470
|
+
project: { ...base.project, id: info.projectId, name: info.projectName },
|
|
7471
|
+
services: {
|
|
7472
|
+
...base.services,
|
|
7473
|
+
identity: {
|
|
7474
|
+
...base.services.identity,
|
|
7475
|
+
provider: info.provider ?? "apitogo",
|
|
7476
|
+
status: "ready",
|
|
7477
|
+
issuer: info.issuer.replace(/\/+$/, ""),
|
|
7478
|
+
clientId: info.clientId,
|
|
7479
|
+
scopes: ["openid", "profile", "email"],
|
|
7480
|
+
...info.audience ? { audience: info.audience } : {},
|
|
7481
|
+
capabilities: { nativeSignup: info.nativeSignup }
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7486
|
+
function withPlansEntry(existing, info) {
|
|
7487
|
+
const base = existing ?? parseManifest({
|
|
7488
|
+
schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
|
|
7489
|
+
project: { name: info.projectName }
|
|
7490
|
+
});
|
|
7491
|
+
return {
|
|
7492
|
+
...base,
|
|
7493
|
+
schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
|
|
7494
|
+
generatedAt: (info.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
7495
|
+
preview: void 0,
|
|
7496
|
+
project: { ...base.project, id: info.projectId, name: info.projectName },
|
|
7497
|
+
services: {
|
|
7498
|
+
...base.services,
|
|
7499
|
+
plans: {
|
|
7500
|
+
...base.services.plans,
|
|
7501
|
+
provider: "apitogo",
|
|
7502
|
+
status: "ready",
|
|
7503
|
+
apiBaseUrl: info.apiBaseUrl.replace(/\/+$/, ""),
|
|
7504
|
+
billing: info.billing,
|
|
7505
|
+
catalogRevision: info.catalogRevision,
|
|
7506
|
+
capabilities: info.capabilities
|
|
7507
|
+
}
|
|
7508
|
+
}
|
|
7509
|
+
};
|
|
7510
|
+
}
|
|
7460
7511
|
async function writeManifest(state, manifest) {
|
|
7461
7512
|
const file = manifestPathFor(state);
|
|
7462
7513
|
await mkdir3(path10.dirname(file), { recursive: true });
|
|
@@ -8266,7 +8317,8 @@ var PLAN_TEMPLATES = {
|
|
|
8266
8317
|
displayName: "Free",
|
|
8267
8318
|
default: true,
|
|
8268
8319
|
limits: {
|
|
8269
|
-
|
|
8320
|
+
// The platform's Free tier allows at most 3 calls per 60 seconds per plan; a higher number is refused.
|
|
8321
|
+
rateLimit: { calls: 3, periodSeconds: 60 },
|
|
8270
8322
|
quota: { calls: 1e3, periodDays: 30 }
|
|
8271
8323
|
}
|
|
8272
8324
|
}
|
|
@@ -8277,7 +8329,8 @@ var PLAN_TEMPLATES = {
|
|
|
8277
8329
|
displayName: "Free",
|
|
8278
8330
|
default: true,
|
|
8279
8331
|
limits: {
|
|
8280
|
-
|
|
8332
|
+
// The platform's Free tier allows at most 3 calls per 60 seconds per plan; a higher number is refused.
|
|
8333
|
+
rateLimit: { calls: 3, periodSeconds: 60 },
|
|
8281
8334
|
quota: { calls: 1e3, periodDays: 30 }
|
|
8282
8335
|
}
|
|
8283
8336
|
},
|
|
@@ -8564,12 +8617,12 @@ async function planAddService(service, options) {
|
|
|
8564
8617
|
)
|
|
8565
8618
|
);
|
|
8566
8619
|
const added = dependencyProblems(next, resolved.configPath).filter(
|
|
8567
|
-
(
|
|
8620
|
+
(issue4) => !before.has(issue4.code + issue4.path)
|
|
8568
8621
|
);
|
|
8569
8622
|
if (added.length > 0) {
|
|
8570
8623
|
throw new ServiceEditError(
|
|
8571
|
-
added.map((
|
|
8572
|
-
added.map((
|
|
8624
|
+
added.map((issue4) => issue4.message).join(" "),
|
|
8625
|
+
added.map((issue4) => issue4.hint).filter(Boolean).join(" ") || "Add the missing service first."
|
|
8573
8626
|
);
|
|
8574
8627
|
}
|
|
8575
8628
|
}
|
|
@@ -8598,11 +8651,11 @@ async function planRemoveService(service, options) {
|
|
|
8598
8651
|
dependencyProblems(config2, resolved.configPath).map((i) => i.code + i.path)
|
|
8599
8652
|
);
|
|
8600
8653
|
const caused = dependencyProblems(next, resolved.configPath).filter(
|
|
8601
|
-
(
|
|
8654
|
+
(issue4) => !before.has(issue4.code + issue4.path)
|
|
8602
8655
|
);
|
|
8603
8656
|
if (caused.length > 0) {
|
|
8604
8657
|
throw new ServiceEditError(
|
|
8605
|
-
`${service} cannot be removed while something depends on it: ${caused.map((
|
|
8658
|
+
`${service} cannot be removed while something depends on it: ${caused.map((issue4) => issue4.message).join(" ")}`,
|
|
8606
8659
|
"Remove or change the dependent service first."
|
|
8607
8660
|
);
|
|
8608
8661
|
}
|
|
@@ -8836,7 +8889,7 @@ init_invariant();
|
|
|
8836
8889
|
|
|
8837
8890
|
// src/cli/check/v2.ts
|
|
8838
8891
|
init_apitogo_config_core();
|
|
8839
|
-
import { readFile as
|
|
8892
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
8840
8893
|
import path49 from "node:path";
|
|
8841
8894
|
|
|
8842
8895
|
// src/cli/platform/client/platform-api.ts
|
|
@@ -9264,6 +9317,52 @@ var PlatformApiClient = class {
|
|
|
9264
9317
|
`/api/v1/projects/${projectId}/gateway`
|
|
9265
9318
|
);
|
|
9266
9319
|
}
|
|
9320
|
+
/**
|
|
9321
|
+
* Asks for the project's identity. The same request twice changes nothing; a different request for an identity that
|
|
9322
|
+
* exists is refused (409), because its audience and redirect addresses cannot change in place. Answers 200 when it is
|
|
9323
|
+
* already Active and 202 otherwise: read `status` and poll `getProjectIdentity`.
|
|
9324
|
+
*/
|
|
9325
|
+
putProjectIdentity(projectId, body) {
|
|
9326
|
+
return this.request(
|
|
9327
|
+
"PUT",
|
|
9328
|
+
`/api/v1/projects/${projectId}/identity`,
|
|
9329
|
+
{
|
|
9330
|
+
headers: { "Content-Type": "application/json" },
|
|
9331
|
+
body: JSON.stringify(body)
|
|
9332
|
+
}
|
|
9333
|
+
);
|
|
9334
|
+
}
|
|
9335
|
+
/** The state of the project's identity, or null when it has none. */
|
|
9336
|
+
async getProjectIdentity(projectId) {
|
|
9337
|
+
try {
|
|
9338
|
+
return await this.request(
|
|
9339
|
+
"GET",
|
|
9340
|
+
`/api/v1/projects/${projectId}/identity`
|
|
9341
|
+
);
|
|
9342
|
+
} catch (error) {
|
|
9343
|
+
if (error instanceof PlatformApiError && error.status === 404)
|
|
9344
|
+
return null;
|
|
9345
|
+
throw error;
|
|
9346
|
+
}
|
|
9347
|
+
}
|
|
9348
|
+
/**
|
|
9349
|
+
* Removes the project's identity and DELETES EVERY USER in it, for good. Always sends `confirm=true`: ask the person
|
|
9350
|
+
* before calling. Returns null when there was nothing to remove, otherwise the state to poll until Released.
|
|
9351
|
+
*/
|
|
9352
|
+
async deleteProjectIdentity(projectId) {
|
|
9353
|
+
const result = await this.request(
|
|
9354
|
+
"DELETE",
|
|
9355
|
+
`/api/v1/projects/${projectId}/identity?confirm=true`
|
|
9356
|
+
);
|
|
9357
|
+
return result ?? null;
|
|
9358
|
+
}
|
|
9359
|
+
/** Applies the project's identity to its plans backend as the sign-in provider. Queues a job. */
|
|
9360
|
+
linkDevPortalIdentity(projectId) {
|
|
9361
|
+
return this.request(
|
|
9362
|
+
"POST",
|
|
9363
|
+
`/api/v1/projects/${projectId}/dev-portal/identity-link`
|
|
9364
|
+
);
|
|
9365
|
+
}
|
|
9267
9366
|
getDevPortalBackend(projectId) {
|
|
9268
9367
|
return this.request(
|
|
9269
9368
|
"GET",
|
|
@@ -9552,8 +9651,8 @@ var PlatformApiClient = class {
|
|
|
9552
9651
|
}
|
|
9553
9652
|
};
|
|
9554
9653
|
async function readFileBytes(filePath) {
|
|
9555
|
-
const { readFile:
|
|
9556
|
-
const buffer = await
|
|
9654
|
+
const { readFile: readFile32 } = await import("node:fs/promises");
|
|
9655
|
+
const buffer = await readFile32(filePath);
|
|
9557
9656
|
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
|
|
9558
9657
|
new Uint8Array(arrayBuffer).set(buffer);
|
|
9559
9658
|
return arrayBuffer;
|
|
@@ -9807,7 +9906,7 @@ import {
|
|
|
9807
9906
|
// package.json
|
|
9808
9907
|
var package_default = {
|
|
9809
9908
|
name: "@lukoweb/apitogo",
|
|
9810
|
-
version: "0.2.
|
|
9909
|
+
version: "0.2.22",
|
|
9811
9910
|
type: "module",
|
|
9812
9911
|
sideEffects: [
|
|
9813
9912
|
"**/*.css",
|
|
@@ -13861,14 +13960,18 @@ import path40 from "node:path";
|
|
|
13861
13960
|
var STATE_INSIDE_CONTENT = /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
|
|
13862
13961
|
var within = (base, file) => file === base || file.startsWith(base + path40.sep);
|
|
13863
13962
|
var unique = (paths) => [...new Set(paths)];
|
|
13864
|
-
function
|
|
13865
|
-
let workspace = false;
|
|
13963
|
+
function isWorkspace(root) {
|
|
13866
13964
|
try {
|
|
13867
|
-
|
|
13965
|
+
return path40.basename(root) === "generated" && lstatSync(path40.join(root, "content")).isSymbolicLink();
|
|
13868
13966
|
} catch {
|
|
13869
|
-
|
|
13967
|
+
return false;
|
|
13870
13968
|
}
|
|
13871
|
-
|
|
13969
|
+
}
|
|
13970
|
+
function tenantDir(root) {
|
|
13971
|
+
return isWorkspace(root) ? realpathSync(path40.join(root, "content")) : void 0;
|
|
13972
|
+
}
|
|
13973
|
+
function stateIgnore(root) {
|
|
13974
|
+
if (!isWorkspace(root)) return void 0;
|
|
13872
13975
|
const state = path40.dirname(root);
|
|
13873
13976
|
const states = unique([state, realpathSync(state)]);
|
|
13874
13977
|
const roots = unique([root, realpathSync(root)]);
|
|
@@ -13878,6 +13981,10 @@ function stateIgnore(root) {
|
|
|
13878
13981
|
return STATE_INSIDE_CONTENT.test(file);
|
|
13879
13982
|
};
|
|
13880
13983
|
}
|
|
13984
|
+
function devServerAllow(root, moduleDir) {
|
|
13985
|
+
const tenant = tenantDir(root);
|
|
13986
|
+
return [root, path40.dirname(root), moduleDir, ...tenant ? [tenant] : []];
|
|
13987
|
+
}
|
|
13881
13988
|
|
|
13882
13989
|
// src/vite/config.ts
|
|
13883
13990
|
var resolveMergedPublicDir = (rootDir, merged) => {
|
|
@@ -13963,7 +14070,7 @@ async function getViteConfig(dir, configEnv) {
|
|
|
13963
14070
|
middlewareMode: true,
|
|
13964
14071
|
open: true,
|
|
13965
14072
|
fs: {
|
|
13966
|
-
allow:
|
|
14073
|
+
allow: devServerAllow(dir, config2.__meta.moduleDir)
|
|
13967
14074
|
},
|
|
13968
14075
|
watch: {
|
|
13969
14076
|
ignored: [
|
|
@@ -14875,11 +14982,239 @@ async function onlineGatewayRules(gateway, projectId, api, derivedCors = []) {
|
|
|
14875
14982
|
}
|
|
14876
14983
|
}
|
|
14877
14984
|
|
|
14985
|
+
// src/cli/deploy/plans-file.ts
|
|
14986
|
+
init_invariant();
|
|
14987
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
14988
|
+
import { z as z14 } from "zod";
|
|
14989
|
+
var PlansFileError = class extends ApitogoError {
|
|
14990
|
+
constructor(code, message, developerHint) {
|
|
14991
|
+
super(message, { developerHint, title: "Cannot read the plans file" });
|
|
14992
|
+
this.code = code;
|
|
14993
|
+
this.name = "PlansFileError";
|
|
14994
|
+
}
|
|
14995
|
+
};
|
|
14996
|
+
var Limits = z14.strictObject({
|
|
14997
|
+
rateLimit: z14.strictObject({
|
|
14998
|
+
calls: z14.int().positive(),
|
|
14999
|
+
periodSeconds: z14.int().positive()
|
|
15000
|
+
}).optional(),
|
|
15001
|
+
quota: z14.strictObject({
|
|
15002
|
+
calls: z14.int().positive(),
|
|
15003
|
+
periodDays: z14.int().positive()
|
|
15004
|
+
}).optional()
|
|
15005
|
+
});
|
|
15006
|
+
var Plan = z14.strictObject({
|
|
15007
|
+
sku: z14.string().trim().min(1),
|
|
15008
|
+
displayName: z14.string().trim().min(1),
|
|
15009
|
+
/** The plan the portal offers first. The platform has no such flag; the portal reads the file order. */
|
|
15010
|
+
default: z14.boolean().optional(),
|
|
15011
|
+
limits: Limits.optional(),
|
|
15012
|
+
priceAmount: z14.number().min(0).optional(),
|
|
15013
|
+
priceCurrency: z14.string().trim().min(1).optional(),
|
|
15014
|
+
billingInterval: z14.enum(["month", "year"]).optional(),
|
|
15015
|
+
pricingModel: z14.enum(["fixed", "custom"]).optional(),
|
|
15016
|
+
priceLabel: z14.string().optional(),
|
|
15017
|
+
sortOrder: z14.int().optional(),
|
|
15018
|
+
/** Action keys such as `get:/orders`. Left out, the plan includes every action of the gateway. */
|
|
15019
|
+
includedActions: z14.array(z14.string().trim().min(1)).optional()
|
|
15020
|
+
});
|
|
15021
|
+
var PlansFile = z14.strictObject({
|
|
15022
|
+
version: z14.literal(1),
|
|
15023
|
+
plans: z14.array(Plan)
|
|
15024
|
+
});
|
|
15025
|
+
var issuesOf = (error) => error.issues.map(
|
|
15026
|
+
(issue4) => `${issue4.path.join(".") || "(file)"}: ${issue4.message}`
|
|
15027
|
+
);
|
|
15028
|
+
async function readPlansFile(file) {
|
|
15029
|
+
let text;
|
|
15030
|
+
try {
|
|
15031
|
+
text = await readFile14(file, "utf8");
|
|
15032
|
+
} catch {
|
|
15033
|
+
throw new PlansFileError(
|
|
15034
|
+
"PLANS_FILE_UNREADABLE",
|
|
15035
|
+
`The plans file ${file} could not be read.`,
|
|
15036
|
+
"Create it with `apitogo add plans`, or set plans.file in the config."
|
|
15037
|
+
);
|
|
15038
|
+
}
|
|
15039
|
+
let raw;
|
|
15040
|
+
try {
|
|
15041
|
+
raw = JSON.parse(text);
|
|
15042
|
+
} catch (error) {
|
|
15043
|
+
throw new PlansFileError(
|
|
15044
|
+
"PLANS_FILE_INVALID",
|
|
15045
|
+
`The plans file ${file} is not valid JSON (${error.message}).`
|
|
15046
|
+
);
|
|
15047
|
+
}
|
|
15048
|
+
const parsed = PlansFile.safeParse(raw);
|
|
15049
|
+
if (!parsed.success)
|
|
15050
|
+
throw new PlansFileError(
|
|
15051
|
+
"PLANS_FILE_INVALID",
|
|
15052
|
+
`The plans file ${file} is not valid:
|
|
15053
|
+
${issuesOf(parsed.error).join("\n ")}`,
|
|
15054
|
+
'Expected { "version": 1, "plans": [ { "sku", "displayName", "limits": { "rateLimit": { "calls", "periodSeconds" }, "quota": { "calls", "periodDays" } } } ] }.'
|
|
15055
|
+
);
|
|
15056
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15057
|
+
for (const plan2 of parsed.data.plans) {
|
|
15058
|
+
const sku = plan2.sku.toLowerCase();
|
|
15059
|
+
if (seen.has(sku))
|
|
15060
|
+
throw new PlansFileError(
|
|
15061
|
+
"PLANS_FILE_INVALID",
|
|
15062
|
+
`The plans file ${file} names the sku "${plan2.sku}" twice.`,
|
|
15063
|
+
"Every plan needs its own sku."
|
|
15064
|
+
);
|
|
15065
|
+
seen.add(sku);
|
|
15066
|
+
}
|
|
15067
|
+
return parsed.data.plans;
|
|
15068
|
+
}
|
|
15069
|
+
var BILLING_PERIOD = { month: "monthly", year: "annual" };
|
|
15070
|
+
function toPlatformPlans(plans) {
|
|
15071
|
+
return plans.map((plan2, index) => {
|
|
15072
|
+
const paid = plan2.pricingModel !== "custom" && (plan2.priceAmount ?? 0) > 0;
|
|
15073
|
+
if (paid)
|
|
15074
|
+
throw new PlansFileError(
|
|
15075
|
+
"PLANS_PAID_UNSUPPORTED",
|
|
15076
|
+
`The plan "${plan2.sku}" has a price, and deploying paid plans is not available in this CLI version yet.`,
|
|
15077
|
+
"Remove the plan, or set priceAmount to 0 (or pricingModel to custom for a contact-us plan)."
|
|
15078
|
+
);
|
|
15079
|
+
const limits = plan2.limits ? {
|
|
15080
|
+
...plan2.limits.rateLimit ? {
|
|
15081
|
+
rateLimit: {
|
|
15082
|
+
calls: plan2.limits.rateLimit.calls,
|
|
15083
|
+
renewalPeriodSeconds: plan2.limits.rateLimit.periodSeconds
|
|
15084
|
+
}
|
|
15085
|
+
} : {},
|
|
15086
|
+
...plan2.limits.quota ? {
|
|
15087
|
+
quota: {
|
|
15088
|
+
calls: plan2.limits.quota.calls,
|
|
15089
|
+
renewalPeriodDays: plan2.limits.quota.periodDays
|
|
15090
|
+
}
|
|
15091
|
+
} : {}
|
|
15092
|
+
} : void 0;
|
|
15093
|
+
return {
|
|
15094
|
+
sku: plan2.sku.trim().toLowerCase(),
|
|
15095
|
+
displayName: plan2.displayName,
|
|
15096
|
+
...limits ? { limitsJson: JSON.stringify(limits) } : {},
|
|
15097
|
+
includedActionKeys: plan2.includedActions ?? null,
|
|
15098
|
+
...plan2.pricingModel ? { pricingModel: plan2.pricingModel } : {},
|
|
15099
|
+
...plan2.priceAmount !== void 0 ? { priceAmount: plan2.priceAmount } : {},
|
|
15100
|
+
...plan2.priceLabel !== void 0 ? { priceLabel: plan2.priceLabel } : {},
|
|
15101
|
+
...plan2.priceCurrency ? { priceCurrency: plan2.priceCurrency } : {},
|
|
15102
|
+
...plan2.billingInterval ? { billingPeriod: BILLING_PERIOD[plan2.billingInterval] } : {},
|
|
15103
|
+
sortOrder: plan2.sortOrder ?? index
|
|
15104
|
+
};
|
|
15105
|
+
});
|
|
15106
|
+
}
|
|
15107
|
+
|
|
15108
|
+
// src/cli/check/services-online.ts
|
|
15109
|
+
var issue3 = (code, path103, message, hint) => ({
|
|
15110
|
+
code,
|
|
15111
|
+
path: path103,
|
|
15112
|
+
message,
|
|
15113
|
+
hint
|
|
15114
|
+
});
|
|
15115
|
+
var trimSlash = (value) => value.replace(/\/+$/, "");
|
|
15116
|
+
async function onlineIdentityRules(resolved, manifest, api) {
|
|
15117
|
+
const identity = resolved.identity;
|
|
15118
|
+
if (!identity?.enabled) return { errors: [], warnings: [] };
|
|
15119
|
+
const issuer = manifest?.services.identity?.issuer;
|
|
15120
|
+
if (!issuer)
|
|
15121
|
+
return {
|
|
15122
|
+
errors: [],
|
|
15123
|
+
warnings: [],
|
|
15124
|
+
skipped: "the identity's online rules were not run: identity is not deployed yet (run `apitogo deploy --only identity`)"
|
|
15125
|
+
};
|
|
15126
|
+
let answer;
|
|
15127
|
+
try {
|
|
15128
|
+
answer = await api.discover(issuer);
|
|
15129
|
+
} catch (error) {
|
|
15130
|
+
return {
|
|
15131
|
+
errors: [],
|
|
15132
|
+
warnings: [],
|
|
15133
|
+
skipped: `the identity's online rules were not run: the issuer could not be reached (${error instanceof Error ? error.message : String(error)})`
|
|
15134
|
+
};
|
|
15135
|
+
}
|
|
15136
|
+
if ("status" in answer)
|
|
15137
|
+
return {
|
|
15138
|
+
errors: [
|
|
15139
|
+
issue3(
|
|
15140
|
+
"IDENTITY_ISSUER_NOT_ANSWERING",
|
|
15141
|
+
"identity",
|
|
15142
|
+
`the issuer ${issuer} answered ${answer.status} instead of its discovery document.`,
|
|
15143
|
+
"Run `apitogo status --only identity` to see what the platform says, then deploy identity again."
|
|
15144
|
+
)
|
|
15145
|
+
],
|
|
15146
|
+
warnings: []
|
|
15147
|
+
};
|
|
15148
|
+
if (!answer.document.issuer || trimSlash(answer.document.issuer) !== trimSlash(issuer))
|
|
15149
|
+
return {
|
|
15150
|
+
errors: [
|
|
15151
|
+
issue3(
|
|
15152
|
+
"IDENTITY_ISSUER_MISMATCH",
|
|
15153
|
+
"identity",
|
|
15154
|
+
`the discovery document of ${issuer} names a different issuer (${answer.document.issuer ?? "none"}).`,
|
|
15155
|
+
"Deploy identity again so services.json is rewritten."
|
|
15156
|
+
)
|
|
15157
|
+
],
|
|
15158
|
+
warnings: []
|
|
15159
|
+
};
|
|
15160
|
+
return { errors: [], warnings: [] };
|
|
15161
|
+
}
|
|
15162
|
+
async function plansFileProblem(resolved) {
|
|
15163
|
+
const plans = resolved.plans;
|
|
15164
|
+
if (!plans?.enabled) return { wanted: [] };
|
|
15165
|
+
try {
|
|
15166
|
+
return { wanted: toPlatformPlans(await readPlansFile(plans.file)) };
|
|
15167
|
+
} catch (error) {
|
|
15168
|
+
if (!(error instanceof PlansFileError)) throw error;
|
|
15169
|
+
return {
|
|
15170
|
+
wanted: [],
|
|
15171
|
+
issue: issue3(
|
|
15172
|
+
error.code,
|
|
15173
|
+
"plans.file",
|
|
15174
|
+
error.message,
|
|
15175
|
+
error.developerHint
|
|
15176
|
+
)
|
|
15177
|
+
};
|
|
15178
|
+
}
|
|
15179
|
+
}
|
|
15180
|
+
async function onlinePlansRules(resolved, projectId, api) {
|
|
15181
|
+
const plans = resolved.plans;
|
|
15182
|
+
if (!plans?.enabled) return { errors: [], warnings: [] };
|
|
15183
|
+
const file = await plansFileProblem(resolved);
|
|
15184
|
+
if (file.issue) return { errors: [file.issue], warnings: [] };
|
|
15185
|
+
const wanted = file.wanted;
|
|
15186
|
+
if (wanted.length === 0) return { errors: [], warnings: [] };
|
|
15187
|
+
try {
|
|
15188
|
+
const answer = await api.validate(
|
|
15189
|
+
projectId,
|
|
15190
|
+
wanted.map(({ includedActionKeys, ...plan2 }) => ({
|
|
15191
|
+
...plan2,
|
|
15192
|
+
...includedActionKeys ? { includedActionKeys } : {}
|
|
15193
|
+
}))
|
|
15194
|
+
);
|
|
15195
|
+
const convert = (item) => issue3(
|
|
15196
|
+
"PLANS_PLAN_INVALID",
|
|
15197
|
+
`plans.file ${item.field}`,
|
|
15198
|
+
`${item.sku ? `plan ${item.sku}: ` : ""}${item.message}`
|
|
15199
|
+
);
|
|
15200
|
+
return {
|
|
15201
|
+
errors: answer.errors.map(convert),
|
|
15202
|
+
warnings: answer.warnings.map(convert)
|
|
15203
|
+
};
|
|
15204
|
+
} catch (error) {
|
|
15205
|
+
return {
|
|
15206
|
+
errors: [],
|
|
15207
|
+
warnings: [],
|
|
15208
|
+
skipped: `the plans' online rules were not run: the platform could not be asked (${error instanceof Error ? error.message : String(error)})`
|
|
15209
|
+
};
|
|
15210
|
+
}
|
|
15211
|
+
}
|
|
15212
|
+
|
|
14878
15213
|
// src/cli/check/v2.ts
|
|
14879
15214
|
async function isConfigV2(dir) {
|
|
14880
15215
|
try {
|
|
14881
15216
|
return /"version"\s*:\s*2\b/.test(
|
|
14882
|
-
await
|
|
15217
|
+
await readFile15(path49.join(dir, APITOGO_CONFIG_FILENAME), "utf8")
|
|
14883
15218
|
);
|
|
14884
15219
|
} catch {
|
|
14885
15220
|
return false;
|
|
@@ -14900,13 +15235,13 @@ async function fetchSecretNames(config2, configuredProjectId, args, deps) {
|
|
|
14900
15235
|
}
|
|
14901
15236
|
async function addGatewayOnlineRules(report, resolved, args, deps) {
|
|
14902
15237
|
const gateway = resolved.gateway;
|
|
14903
|
-
if (args.offline || !deps.gateway || !gateway?.enabled) return;
|
|
15238
|
+
if (args.offline || !deps.gateway || !gateway?.enabled) return false;
|
|
14904
15239
|
const projectId = resolved.project.id || deps.envProjectId?.trim();
|
|
14905
15240
|
if (!projectId) {
|
|
14906
15241
|
report.skipped.push(
|
|
14907
15242
|
"the gateway's online rules were not run: this folder is not linked to a project (run `apitogo project create`)"
|
|
14908
15243
|
);
|
|
14909
|
-
return;
|
|
15244
|
+
return false;
|
|
14910
15245
|
}
|
|
14911
15246
|
const manifest = await readManifest(resolved.state);
|
|
14912
15247
|
const result = await onlineGatewayRules(
|
|
@@ -14920,7 +15255,7 @@ async function addGatewayOnlineRules(report, resolved, args, deps) {
|
|
|
14920
15255
|
);
|
|
14921
15256
|
if (result.skipped) {
|
|
14922
15257
|
report.skipped.push(result.skipped);
|
|
14923
|
-
return;
|
|
15258
|
+
return false;
|
|
14924
15259
|
}
|
|
14925
15260
|
const seen = new Set(
|
|
14926
15261
|
[...report.errors, ...report.warnings].map((i) => `${i.code}|${i.path}`)
|
|
@@ -14931,12 +15266,60 @@ async function addGatewayOnlineRules(report, resolved, args, deps) {
|
|
|
14931
15266
|
seen.add(key);
|
|
14932
15267
|
report.errors.push(item);
|
|
14933
15268
|
}
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
15269
|
+
return true;
|
|
15270
|
+
}
|
|
15271
|
+
async function addServiceOnlineRules(report, resolved, args, deps) {
|
|
15272
|
+
if (args.offline) return [];
|
|
15273
|
+
const ran = [];
|
|
15274
|
+
const seen = new Set(
|
|
15275
|
+
[...report.errors, ...report.warnings].map((i) => `${i.code}|${i.path}`)
|
|
14939
15276
|
);
|
|
15277
|
+
const take = (list2, into) => {
|
|
15278
|
+
for (const item of list2) {
|
|
15279
|
+
const key = `${item.code}|${item.path}`;
|
|
15280
|
+
if (seen.has(key)) continue;
|
|
15281
|
+
if (item.path === "plans.file" && report.errors.some((e) => e.path === "plans.file"))
|
|
15282
|
+
continue;
|
|
15283
|
+
seen.add(key);
|
|
15284
|
+
into.push(item);
|
|
15285
|
+
}
|
|
15286
|
+
};
|
|
15287
|
+
if (deps.identity && resolved.identity?.enabled) {
|
|
15288
|
+
const result = await onlineIdentityRules(
|
|
15289
|
+
resolved,
|
|
15290
|
+
await readManifest(resolved.state),
|
|
15291
|
+
deps.identity
|
|
15292
|
+
);
|
|
15293
|
+
if (result.skipped) report.skipped.push(result.skipped);
|
|
15294
|
+
else {
|
|
15295
|
+
take(result.errors, report.errors);
|
|
15296
|
+
take(result.warnings, report.warnings);
|
|
15297
|
+
ran.push("identity");
|
|
15298
|
+
}
|
|
15299
|
+
}
|
|
15300
|
+
if (deps.plans && resolved.plans?.enabled) {
|
|
15301
|
+
const projectId = resolved.project.id || deps.envProjectId?.trim();
|
|
15302
|
+
if (!projectId)
|
|
15303
|
+
report.skipped.push(
|
|
15304
|
+
"the plans' online rules were not run: this folder is not linked to a project (run `apitogo project create`)"
|
|
15305
|
+
);
|
|
15306
|
+
else {
|
|
15307
|
+
const result = await onlinePlansRules(resolved, projectId, deps.plans);
|
|
15308
|
+
if (result.skipped) report.skipped.push(result.skipped);
|
|
15309
|
+
else {
|
|
15310
|
+
take(result.errors, report.errors);
|
|
15311
|
+
take(result.warnings, report.warnings);
|
|
15312
|
+
ran.push("plans");
|
|
15313
|
+
}
|
|
15314
|
+
}
|
|
15315
|
+
}
|
|
15316
|
+
return ran;
|
|
15317
|
+
}
|
|
15318
|
+
function onlineRulesNote(ran) {
|
|
15319
|
+
if (ran.length === 1 && ran[0] === "gateway")
|
|
15320
|
+
return "online rules ran for the gateway only; the other services have none yet";
|
|
15321
|
+
const names = ran.length > 1 ? `${ran.slice(0, -1).join(", ")} and ${ran[ran.length - 1]}` : ran[0] ?? "";
|
|
15322
|
+
return `online rules ran for the ${names}`;
|
|
14940
15323
|
}
|
|
14941
15324
|
async function addApiReferenceWarning(report, resolved) {
|
|
14942
15325
|
const portal = resolved.portal;
|
|
@@ -14960,7 +15343,7 @@ async function validatePortalBuild(dir) {
|
|
|
14960
15343
|
return {
|
|
14961
15344
|
code: error.issues[0]?.code ?? "PORTAL_BUILD_FAILED",
|
|
14962
15345
|
path: error.issues[0]?.path ?? "portal",
|
|
14963
|
-
message: error.issues.map((
|
|
15346
|
+
message: error.issues.map((issue4) => issue4.message).join(" "),
|
|
14964
15347
|
hint: error.issues[0]?.hint
|
|
14965
15348
|
};
|
|
14966
15349
|
}
|
|
@@ -14989,7 +15372,19 @@ async function runCheckV2(dir, args = {}, deps = {}) {
|
|
|
14989
15372
|
knownSecrets: online.names
|
|
14990
15373
|
});
|
|
14991
15374
|
if (online.failure) report.skipped.push(online.failure);
|
|
14992
|
-
await
|
|
15375
|
+
const file = await plansFileProblem(resolved);
|
|
15376
|
+
if (file.issue && !report.errors.some((e) => e.path === file.issue.path))
|
|
15377
|
+
report.errors.push(file.issue);
|
|
15378
|
+
const ran = [];
|
|
15379
|
+
if (await addGatewayOnlineRules(report, resolved, args, deps))
|
|
15380
|
+
ran.push("gateway");
|
|
15381
|
+
ran.push(...await addServiceOnlineRules(report, resolved, args, deps));
|
|
15382
|
+
if (ran.length > 0) {
|
|
15383
|
+
report.skipped = report.skipped.filter(
|
|
15384
|
+
(note) => !note.startsWith("online rules were not run")
|
|
15385
|
+
);
|
|
15386
|
+
report.skipped.unshift(onlineRulesNote(ran));
|
|
15387
|
+
}
|
|
14993
15388
|
await addApiReferenceWarning(report, resolved);
|
|
14994
15389
|
} catch (error) {
|
|
14995
15390
|
if (!(error instanceof ConfigError)) throw error;
|
|
@@ -15017,11 +15412,11 @@ async function runCheckV2(dir, args = {}, deps = {}) {
|
|
|
15017
15412
|
...report
|
|
15018
15413
|
};
|
|
15019
15414
|
}
|
|
15020
|
-
var line = (label,
|
|
15415
|
+
var line = (label, issue4) => `${label}: ${formatConfigIssue(issue4)}`;
|
|
15021
15416
|
function formatCheckV2(output4) {
|
|
15022
15417
|
const lines = [
|
|
15023
|
-
...output4.errors.map((
|
|
15024
|
-
...output4.warnings.map((
|
|
15418
|
+
...output4.errors.map((issue4) => line("error", issue4)),
|
|
15419
|
+
...output4.warnings.map((issue4) => line("warning", issue4)),
|
|
15025
15420
|
...output4.skipped.map((note) => `not checked: ${note}`)
|
|
15026
15421
|
];
|
|
15027
15422
|
const errors = `${output4.errors.length} error${output4.errors.length === 1 ? "" : "s"}`;
|
|
@@ -15031,6 +15426,14 @@ function formatCheckV2(output4) {
|
|
|
15031
15426
|
);
|
|
15032
15427
|
return lines;
|
|
15033
15428
|
}
|
|
15429
|
+
async function discoverIssuer(issuer) {
|
|
15430
|
+
const response = await fetch(
|
|
15431
|
+
`${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`,
|
|
15432
|
+
{ signal: AbortSignal.timeout(1e4) }
|
|
15433
|
+
);
|
|
15434
|
+
if (!response.ok) return { status: response.status };
|
|
15435
|
+
return { document: await response.json() };
|
|
15436
|
+
}
|
|
15034
15437
|
function platformDependencies(args) {
|
|
15035
15438
|
if (args.offline) return {};
|
|
15036
15439
|
try {
|
|
@@ -15042,6 +15445,10 @@ function platformDependencies(args) {
|
|
|
15042
15445
|
gateway: {
|
|
15043
15446
|
check: (projectId, body) => client.checkGateway(projectId, body),
|
|
15044
15447
|
capabilities: (projectId) => client.getGatewayCapabilities(projectId)
|
|
15448
|
+
},
|
|
15449
|
+
identity: { discover: discoverIssuer },
|
|
15450
|
+
plans: {
|
|
15451
|
+
validate: (projectId, plans) => client.validateGatewayPlans(plans, { projectId })
|
|
15045
15452
|
}
|
|
15046
15453
|
};
|
|
15047
15454
|
} catch {
|
|
@@ -15068,7 +15475,7 @@ init_package_json();
|
|
|
15068
15475
|
|
|
15069
15476
|
// src/cli/platform/helpers/npm-build.ts
|
|
15070
15477
|
import { spawn } from "node:child_process";
|
|
15071
|
-
import { access as access5, readFile as
|
|
15478
|
+
import { access as access5, readFile as readFile16 } from "node:fs/promises";
|
|
15072
15479
|
import path50 from "node:path";
|
|
15073
15480
|
var BUILD_LOG_TAIL_BYTES = 8192;
|
|
15074
15481
|
var MIN_NODE_MAJOR = 20;
|
|
@@ -16020,7 +16427,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
|
|
|
16020
16427
|
// src/cli/platform/helpers/dev-portal-manifest.ts
|
|
16021
16428
|
init_gateway_limits();
|
|
16022
16429
|
import { existsSync as existsSync7 } from "node:fs";
|
|
16023
|
-
import { access as access6, mkdir as mkdir13, readFile as
|
|
16430
|
+
import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile15 } from "node:fs/promises";
|
|
16024
16431
|
import path57 from "node:path";
|
|
16025
16432
|
|
|
16026
16433
|
// src/cli/make-config/json-config.ts
|
|
@@ -17320,7 +17727,7 @@ function applyOpenApiToSiteConfig(config2, options) {
|
|
|
17320
17727
|
async function readManifest2(siteDirectory) {
|
|
17321
17728
|
const filePath = manifestFilePath(siteDirectory);
|
|
17322
17729
|
if (await pathExists2(filePath)) {
|
|
17323
|
-
const raw = await
|
|
17730
|
+
const raw = await readFile17(filePath, "utf8");
|
|
17324
17731
|
const config2 = JSON.parse(raw);
|
|
17325
17732
|
return extractManifestFromConfig(config2);
|
|
17326
17733
|
}
|
|
@@ -17331,7 +17738,7 @@ async function writeManifest2(siteDirectory, manifest) {
|
|
|
17331
17738
|
await mkdir13(path57.dirname(filePath), { recursive: true });
|
|
17332
17739
|
let config2 = {};
|
|
17333
17740
|
if (await pathExists2(filePath)) {
|
|
17334
|
-
const raw = await
|
|
17741
|
+
const raw = await readFile17(filePath, "utf8");
|
|
17335
17742
|
config2 = JSON.parse(raw);
|
|
17336
17743
|
}
|
|
17337
17744
|
const {
|
|
@@ -17350,7 +17757,7 @@ import {
|
|
|
17350
17757
|
access as access7,
|
|
17351
17758
|
appendFile,
|
|
17352
17759
|
mkdir as mkdir14,
|
|
17353
|
-
readFile as
|
|
17760
|
+
readFile as readFile18,
|
|
17354
17761
|
unlink,
|
|
17355
17762
|
writeFile as writeFile16
|
|
17356
17763
|
} from "node:fs/promises";
|
|
@@ -17456,7 +17863,7 @@ function applyEnvOverrides2(config2, env) {
|
|
|
17456
17863
|
}
|
|
17457
17864
|
async function readEnvFile(filePath) {
|
|
17458
17865
|
try {
|
|
17459
|
-
const raw = await
|
|
17866
|
+
const raw = await readFile18(filePath, "utf8");
|
|
17460
17867
|
const env = {};
|
|
17461
17868
|
for (const line2 of raw.split("\n")) {
|
|
17462
17869
|
const trimmed = line2.trim();
|
|
@@ -17633,7 +18040,7 @@ async function writeScaffoldEnv(siteDirectory) {
|
|
|
17633
18040
|
for (const legacyName of [".env.example"]) {
|
|
17634
18041
|
const legacyPath = path58.join(resolvedDirectory, legacyName);
|
|
17635
18042
|
try {
|
|
17636
|
-
const legacy = await
|
|
18043
|
+
const legacy = await readFile18(legacyPath, "utf8");
|
|
17637
18044
|
if (!envFileHasActiveAssignments(legacy)) {
|
|
17638
18045
|
await unlink(legacyPath);
|
|
17639
18046
|
filesWritten.push(`removed:${legacyName}`);
|
|
@@ -17644,7 +18051,7 @@ async function writeScaffoldEnv(siteDirectory) {
|
|
|
17644
18051
|
const gitignorePath = path58.join(resolvedDirectory, ".gitignore");
|
|
17645
18052
|
let gitignore = "";
|
|
17646
18053
|
try {
|
|
17647
|
-
gitignore = await
|
|
18054
|
+
gitignore = await readFile18(gitignorePath, "utf8");
|
|
17648
18055
|
} catch {
|
|
17649
18056
|
gitignore = "";
|
|
17650
18057
|
}
|
|
@@ -17676,7 +18083,7 @@ var SECRETS_COLLECTION_INSTRUCTION = "For go-live, plans must already be in apit
|
|
|
17676
18083
|
|
|
17677
18084
|
// src/cli/platform/helpers/dev-portal-site-validation.ts
|
|
17678
18085
|
init_gateway_limits();
|
|
17679
|
-
import { readFile as
|
|
18086
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
17680
18087
|
import { createRequire as createRequire3 } from "node:module";
|
|
17681
18088
|
import path59 from "node:path";
|
|
17682
18089
|
var cachedFallbackTs;
|
|
@@ -17854,7 +18261,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
|
|
|
17854
18261
|
let parsed;
|
|
17855
18262
|
let parseFailed = false;
|
|
17856
18263
|
try {
|
|
17857
|
-
parsed = JSON.parse(await
|
|
18264
|
+
parsed = JSON.parse(await readFile19(jsonPath, "utf8"));
|
|
17858
18265
|
} catch (error) {
|
|
17859
18266
|
parseFailed = true;
|
|
17860
18267
|
issues.push({
|
|
@@ -17893,7 +18300,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
|
|
|
17893
18300
|
} else {
|
|
17894
18301
|
for (const relativeFile of existingTsxFiles) {
|
|
17895
18302
|
checkedFiles.push(relativeFile);
|
|
17896
|
-
const content = await
|
|
18303
|
+
const content = await readFile19(
|
|
17897
18304
|
path59.join(resolvedDir, relativeFile),
|
|
17898
18305
|
"utf8"
|
|
17899
18306
|
);
|
|
@@ -17914,7 +18321,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
|
|
|
17914
18321
|
};
|
|
17915
18322
|
}
|
|
17916
18323
|
function formatSiteValidationErrors(result) {
|
|
17917
|
-
return result.issues.map((
|
|
18324
|
+
return result.issues.map((issue4) => `${issue4.file}: ${issue4.message}`).join("; ");
|
|
17918
18325
|
}
|
|
17919
18326
|
function buildSiteValidationResponseFields(result) {
|
|
17920
18327
|
if (result.valid) {
|
|
@@ -18318,7 +18725,7 @@ async function checkPublishReadiness(client, _config, args) {
|
|
|
18318
18725
|
const mergedPrerequisites = mergePublishPrerequisitesWithPlatformValidation(
|
|
18319
18726
|
publishPrerequisites,
|
|
18320
18727
|
platformValidation.issues.map(
|
|
18321
|
-
(
|
|
18728
|
+
(issue4) => `${issue4.field}: ${issue4.message}`
|
|
18322
18729
|
)
|
|
18323
18730
|
);
|
|
18324
18731
|
const siteValidation = await validateDevPortalSiteConfig(sourceDirectory);
|
|
@@ -18440,7 +18847,7 @@ var check_default = {
|
|
|
18440
18847
|
|
|
18441
18848
|
// src/cli/make-config/handler.ts
|
|
18442
18849
|
import { constants as constants2 } from "node:fs";
|
|
18443
|
-
import { access as access12, mkdir as mkdir17, readFile as
|
|
18850
|
+
import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile21 } from "node:fs/promises";
|
|
18444
18851
|
import path71 from "node:path";
|
|
18445
18852
|
import { stdin as input2 } from "node:process";
|
|
18446
18853
|
import { glob as glob8 } from "glob";
|
|
@@ -18580,7 +18987,7 @@ function withDefaultIncludedActionKeys(plan2, actionKeys) {
|
|
|
18580
18987
|
}
|
|
18581
18988
|
|
|
18582
18989
|
// src/cli/scaffold/dev-portal-config-pages.ts
|
|
18583
|
-
import { access as access8, readFile as
|
|
18990
|
+
import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile17 } from "node:fs/promises";
|
|
18584
18991
|
import path63 from "node:path";
|
|
18585
18992
|
async function writeConfigAtomically(filePath, content) {
|
|
18586
18993
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -19345,7 +19752,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label) {
|
|
|
19345
19752
|
if (!configPath) {
|
|
19346
19753
|
return { updated: false };
|
|
19347
19754
|
}
|
|
19348
|
-
const original = await
|
|
19755
|
+
const original = await readFile20(configPath, "utf8");
|
|
19349
19756
|
const patched = patchApitogoConfigForNewPage(original, slug, label, {
|
|
19350
19757
|
typescript: configPath.endsWith(".tsx")
|
|
19351
19758
|
});
|
|
@@ -19357,7 +19764,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label) {
|
|
|
19357
19764
|
}
|
|
19358
19765
|
|
|
19359
19766
|
// src/cli/scaffold/wire-openapi.ts
|
|
19360
|
-
import { access as access10, readFile as
|
|
19767
|
+
import { access as access10, readFile as readFile22, writeFile as writeFile19 } from "node:fs/promises";
|
|
19361
19768
|
import path68 from "node:path";
|
|
19362
19769
|
|
|
19363
19770
|
// src/cli/platform/helpers/openapi.ts
|
|
@@ -19561,7 +19968,7 @@ async function validateOpenApiFile(filePath) {
|
|
|
19561
19968
|
}
|
|
19562
19969
|
|
|
19563
19970
|
// src/cli/scaffold/scaffold-dev-portal-site.ts
|
|
19564
|
-
import { mkdir as mkdir15, readFile as
|
|
19971
|
+
import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile18 } from "node:fs/promises";
|
|
19565
19972
|
import path67 from "node:path";
|
|
19566
19973
|
|
|
19567
19974
|
// src/cli/scaffold/apitogo-npm-packages.ts
|
|
@@ -19969,7 +20376,7 @@ function patchDashboardPageConfig(content) {
|
|
|
19969
20376
|
async function ensureAccountPlansPage(outputDirectory) {
|
|
19970
20377
|
const pagePath = path67.join(outputDirectory, ACCOUNT_PLANS_PAGE_PATH);
|
|
19971
20378
|
try {
|
|
19972
|
-
await
|
|
20379
|
+
await readFile21(pagePath, "utf8");
|
|
19973
20380
|
return void 0;
|
|
19974
20381
|
} catch {
|
|
19975
20382
|
await mkdir15(path67.dirname(pagePath), { recursive: true });
|
|
@@ -19980,7 +20387,7 @@ async function ensureAccountPlansPage(outputDirectory) {
|
|
|
19980
20387
|
async function ensureDashboardPage(outputDirectory) {
|
|
19981
20388
|
const pagePath = path67.join(outputDirectory, DASHBOARD_PAGE_PATH);
|
|
19982
20389
|
try {
|
|
19983
|
-
await
|
|
20390
|
+
await readFile21(pagePath, "utf8");
|
|
19984
20391
|
return void 0;
|
|
19985
20392
|
} catch {
|
|
19986
20393
|
await mkdir15(path67.dirname(pagePath), { recursive: true });
|
|
@@ -20042,7 +20449,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
|
|
|
20042
20449
|
const filesWritten = [];
|
|
20043
20450
|
const packageJsonPath = path67.join(outputDirectory, "package.json");
|
|
20044
20451
|
if (options.patchPackageJson !== false) {
|
|
20045
|
-
const packageJson3 = await
|
|
20452
|
+
const packageJson3 = await readFile21(packageJsonPath, "utf8");
|
|
20046
20453
|
await writeFile18(
|
|
20047
20454
|
packageJsonPath,
|
|
20048
20455
|
rewritePackageJson(packageJson3, {
|
|
@@ -20054,7 +20461,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
|
|
|
20054
20461
|
filesWritten.push("package.json");
|
|
20055
20462
|
}
|
|
20056
20463
|
const configPath = path67.join(outputDirectory, "apitogo.config.tsx");
|
|
20057
|
-
const configContent = await
|
|
20464
|
+
const configContent = await readFile21(configPath, "utf8");
|
|
20058
20465
|
await writeFile18(
|
|
20059
20466
|
configPath,
|
|
20060
20467
|
patchDashboardPageConfig(
|
|
@@ -20161,7 +20568,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
|
|
|
20161
20568
|
}
|
|
20162
20569
|
const jsonConfigPath = manifestFilePath(resolvedSource);
|
|
20163
20570
|
if (await pathExists4(jsonConfigPath)) {
|
|
20164
|
-
const raw = await
|
|
20571
|
+
const raw = await readFile22(jsonConfigPath, "utf8");
|
|
20165
20572
|
const siteConfig = JSON.parse(raw);
|
|
20166
20573
|
const updated = applyOpenApiToSiteConfig(siteConfig, {
|
|
20167
20574
|
input: input5,
|
|
@@ -20175,7 +20582,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
|
|
|
20175
20582
|
);
|
|
20176
20583
|
} else {
|
|
20177
20584
|
const tsxConfigPath = path68.join(resolvedSource, "apitogo.config.tsx");
|
|
20178
|
-
const configContent = await
|
|
20585
|
+
const configContent = await readFile22(tsxConfigPath, "utf8");
|
|
20179
20586
|
try {
|
|
20180
20587
|
const patched = patchApitogoConfigOpenApiToFile(configContent, input5);
|
|
20181
20588
|
await writeFile19(tsxConfigPath, patched);
|
|
@@ -20531,7 +20938,7 @@ export default function ${componentName}() {
|
|
|
20531
20938
|
// src/cli/make-config/scaffold-project.ts
|
|
20532
20939
|
init_package_json();
|
|
20533
20940
|
import { constants } from "node:fs";
|
|
20534
|
-
import { access as access11, mkdir as mkdir16, readFile as
|
|
20941
|
+
import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile20 } from "node:fs/promises";
|
|
20535
20942
|
import path70 from "node:path";
|
|
20536
20943
|
import { glob as glob7 } from "glob";
|
|
20537
20944
|
|
|
@@ -20613,7 +21020,7 @@ async function ensurePackageJson2(dir) {
|
|
|
20613
21020
|
`, "utf8");
|
|
20614
21021
|
return true;
|
|
20615
21022
|
}
|
|
20616
|
-
const raw = await
|
|
21023
|
+
const raw = await readFile23(pkgPath, "utf8");
|
|
20617
21024
|
let pkg;
|
|
20618
21025
|
try {
|
|
20619
21026
|
pkg = JSON.parse(raw);
|
|
@@ -20742,7 +21149,7 @@ var fileExists3 = async (filePath) => {
|
|
|
20742
21149
|
}
|
|
20743
21150
|
};
|
|
20744
21151
|
var readJsonConfig = async (filePath) => {
|
|
20745
|
-
const raw = await
|
|
21152
|
+
const raw = await readFile24(filePath, "utf8");
|
|
20746
21153
|
const parsed = JSON.parse(raw);
|
|
20747
21154
|
if (!isPlainObject3(parsed)) {
|
|
20748
21155
|
throw new Error(`${APITOGO_JSON_FILENAME} must be a JSON object.`);
|
|
@@ -20792,7 +21199,7 @@ async function persistConfig(config2, ctx) {
|
|
|
20792
21199
|
`, "utf8");
|
|
20793
21200
|
const tsxPath = path71.join(ctx.dir, "apitogo.config.tsx");
|
|
20794
21201
|
if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
|
|
20795
|
-
const tsxSource = await
|
|
21202
|
+
const tsxSource = await readFile24(tsxPath, "utf8");
|
|
20796
21203
|
if (isManagedConfigWrapper(tsxSource)) {
|
|
20797
21204
|
await writeFile21(
|
|
20798
21205
|
tsxPath,
|
|
@@ -21188,15 +21595,15 @@ function buildGatewayPlansValidationRequest(plans) {
|
|
|
21188
21595
|
};
|
|
21189
21596
|
});
|
|
21190
21597
|
}
|
|
21191
|
-
function toConfigIssue(
|
|
21598
|
+
function toConfigIssue(issue4) {
|
|
21192
21599
|
return {
|
|
21193
|
-
...
|
|
21194
|
-
field:
|
|
21600
|
+
...issue4,
|
|
21601
|
+
field: issue4.field.replace(/\.limitsJson$/, ".limits")
|
|
21195
21602
|
};
|
|
21196
21603
|
}
|
|
21197
|
-
function formatGatewayPlanIssue(
|
|
21198
|
-
const sku =
|
|
21199
|
-
return `${
|
|
21604
|
+
function formatGatewayPlanIssue(issue4) {
|
|
21605
|
+
const sku = issue4.sku ? ` (${issue4.sku})` : "";
|
|
21606
|
+
return `${issue4.field}${sku}: ${issue4.message}`;
|
|
21200
21607
|
}
|
|
21201
21608
|
async function checkGatewayPlansOnPlatform(client, args) {
|
|
21202
21609
|
let result;
|
|
@@ -21219,10 +21626,10 @@ async function checkGatewayPlansOnPlatform(client, args) {
|
|
|
21219
21626
|
async function assertGatewayPlansValidForPublish(client, args) {
|
|
21220
21627
|
const localErrors = args.localErrors ?? [];
|
|
21221
21628
|
const check2 = await checkGatewayPlansOnPlatform(client, args);
|
|
21222
|
-
const localFields = new Set(localErrors.map((
|
|
21629
|
+
const localFields = new Set(localErrors.map((issue4) => issue4.field));
|
|
21223
21630
|
const errors = [
|
|
21224
21631
|
...localErrors,
|
|
21225
|
-
...(check2?.errors ?? []).filter((
|
|
21632
|
+
...(check2?.errors ?? []).filter((issue4) => !localFields.has(issue4.field))
|
|
21226
21633
|
];
|
|
21227
21634
|
if (errors.length === 0) {
|
|
21228
21635
|
return { blocked: false };
|
|
@@ -22185,7 +22592,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
|
|
|
22185
22592
|
}
|
|
22186
22593
|
|
|
22187
22594
|
// src/cli/platform/helpers/openapi-gateway-servers.ts
|
|
22188
|
-
import { mkdir as mkdir18, readFile as
|
|
22595
|
+
import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile22 } from "node:fs/promises";
|
|
22189
22596
|
import path74 from "node:path";
|
|
22190
22597
|
import YAML3 from "yaml";
|
|
22191
22598
|
var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
|
|
@@ -22217,7 +22624,7 @@ async function resolveProjectGatewayUrl(client, projectId) {
|
|
|
22217
22624
|
return null;
|
|
22218
22625
|
}
|
|
22219
22626
|
async function readOpenApiDocument(filePath) {
|
|
22220
|
-
const content = await
|
|
22627
|
+
const content = await readFile25(filePath, "utf8");
|
|
22221
22628
|
const ext = path74.extname(filePath).toLowerCase();
|
|
22222
22629
|
const parsed = ext === ".json" ? JSON.parse(content) : YAML3.parse(content);
|
|
22223
22630
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -22246,7 +22653,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
|
|
|
22246
22653
|
path74.resolve(siteDirectory),
|
|
22247
22654
|
APITOGO_CONFIG_FILENAME2
|
|
22248
22655
|
);
|
|
22249
|
-
const raw = await
|
|
22656
|
+
const raw = await readFile25(configPath, "utf8");
|
|
22250
22657
|
const config2 = JSON.parse(raw);
|
|
22251
22658
|
const previousApis = config2.apis;
|
|
22252
22659
|
const previousBackend = config2.backend;
|
|
@@ -22263,7 +22670,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
|
|
|
22263
22670
|
`, "utf8");
|
|
22264
22671
|
return {
|
|
22265
22672
|
restore: async () => {
|
|
22266
|
-
const currentRaw = await
|
|
22673
|
+
const currentRaw = await readFile25(configPath, "utf8");
|
|
22267
22674
|
const current = JSON.parse(currentRaw);
|
|
22268
22675
|
if (previousApis === void 0) {
|
|
22269
22676
|
delete current.apis;
|
|
@@ -22298,7 +22705,7 @@ async function prepareOpenApiServersForGatewayBuild(options) {
|
|
|
22298
22705
|
);
|
|
22299
22706
|
if (!openApiPath) {
|
|
22300
22707
|
try {
|
|
22301
|
-
const raw = await
|
|
22708
|
+
const raw = await readFile25(
|
|
22302
22709
|
path74.join(siteDirectory, APITOGO_CONFIG_FILENAME2),
|
|
22303
22710
|
"utf8"
|
|
22304
22711
|
);
|
|
@@ -22372,7 +22779,7 @@ function collectManifestPublicActionKeys(gateway) {
|
|
|
22372
22779
|
}
|
|
22373
22780
|
|
|
22374
22781
|
// src/cli/platform/helpers/publish-ledger.ts
|
|
22375
|
-
import { mkdir as mkdir19, readFile as
|
|
22782
|
+
import { mkdir as mkdir19, readFile as readFile26, writeFile as writeFile23 } from "node:fs/promises";
|
|
22376
22783
|
import path75 from "node:path";
|
|
22377
22784
|
var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
|
|
22378
22785
|
function publishLedgerPath(siteDirectory) {
|
|
@@ -22381,7 +22788,7 @@ function publishLedgerPath(siteDirectory) {
|
|
|
22381
22788
|
async function readPublishLedger(siteDirectory) {
|
|
22382
22789
|
const filePath = publishLedgerPath(siteDirectory);
|
|
22383
22790
|
try {
|
|
22384
|
-
const raw = await
|
|
22791
|
+
const raw = await readFile26(filePath, "utf8");
|
|
22385
22792
|
const parsed = JSON.parse(raw);
|
|
22386
22793
|
if (parsed?.version !== 1 || !Array.isArray(parsed.steps)) {
|
|
22387
22794
|
return null;
|
|
@@ -24084,6 +24491,10 @@ function resumedCreateStep(projectId) {
|
|
|
24084
24491
|
};
|
|
24085
24492
|
}
|
|
24086
24493
|
async function resolveBootstrapProjectId(client, args, steps) {
|
|
24494
|
+
if (args.projectId) {
|
|
24495
|
+
steps.push(resumedCreateStep(args.projectId));
|
|
24496
|
+
return { projectId: args.projectId, resumed: true };
|
|
24497
|
+
}
|
|
24087
24498
|
const existing = await findExistingProjectByName(client, args.name);
|
|
24088
24499
|
if (existing) {
|
|
24089
24500
|
steps.push(resumedCreateStep(existing.id));
|
|
@@ -24143,11 +24554,14 @@ async function handleBootstrapProject(client, config2, args, progress) {
|
|
|
24143
24554
|
{
|
|
24144
24555
|
name: args.name,
|
|
24145
24556
|
description: args.description,
|
|
24146
|
-
modules
|
|
24557
|
+
modules,
|
|
24558
|
+
projectId: args.projectId
|
|
24147
24559
|
},
|
|
24148
24560
|
steps
|
|
24149
24561
|
);
|
|
24150
|
-
|
|
24562
|
+
await args.onProjectResolved?.(projectId);
|
|
24563
|
+
const continuingWithoutPlan = selectFreePlan && args.projectId !== void 0 && isPlatformPlanUnselected((await client.getProject(projectId)).planTier);
|
|
24564
|
+
if (selectFreePlan && (!resumed || continuingWithoutPlan)) {
|
|
24151
24565
|
await bootstrapProgress?.onStep("Selecting free plan");
|
|
24152
24566
|
const freeResult = await handleSelectFreePlan(
|
|
24153
24567
|
client,
|
|
@@ -24159,7 +24573,7 @@ async function handleBootstrapProject(client, config2, args, progress) {
|
|
|
24159
24573
|
return freeResult;
|
|
24160
24574
|
}
|
|
24161
24575
|
steps.push({ step: "select_free_plan", status: "succeeded" });
|
|
24162
|
-
} else if (selectFreePlan && resumed) {
|
|
24576
|
+
} else if (selectFreePlan && resumed && !continuingWithoutPlan) {
|
|
24163
24577
|
steps.push({
|
|
24164
24578
|
step: "select_free_plan",
|
|
24165
24579
|
status: "skipped",
|
|
@@ -24348,9 +24762,9 @@ async function handleTeardownProject(client, config2, args, progress) {
|
|
|
24348
24762
|
|
|
24349
24763
|
// src/cli/platform/tools/dev-portal-bootstrap-shared.ts
|
|
24350
24764
|
var BootstrapStepError = class extends Error {
|
|
24351
|
-
constructor(
|
|
24765
|
+
constructor(failedStep2, message, recoveryHint) {
|
|
24352
24766
|
super(message);
|
|
24353
|
-
this.failedStep =
|
|
24767
|
+
this.failedStep = failedStep2;
|
|
24354
24768
|
this.recoveryHint = recoveryHint;
|
|
24355
24769
|
this.name = "BootstrapStepError";
|
|
24356
24770
|
}
|
|
@@ -24553,7 +24967,7 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
|
|
|
24553
24967
|
import path81 from "node:path";
|
|
24554
24968
|
|
|
24555
24969
|
// src/cli/platform/helpers/dev-portal-site-api-url.ts
|
|
24556
|
-
import { readdir as readdir3, readFile as
|
|
24970
|
+
import { readdir as readdir3, readFile as readFile27, writeFile as writeFile24 } from "node:fs/promises";
|
|
24557
24971
|
import path78 from "node:path";
|
|
24558
24972
|
var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
|
|
24559
24973
|
var PLACEHOLDER_HOST_MARKERS = [
|
|
@@ -24578,7 +24992,7 @@ async function readSiteApiBaseUrl(sourceDirectory) {
|
|
|
24578
24992
|
const resolved = path78.resolve(sourceDirectory);
|
|
24579
24993
|
for (const envFile of [".env", ".env.production"]) {
|
|
24580
24994
|
try {
|
|
24581
|
-
const content = await
|
|
24995
|
+
const content = await readFile27(path78.join(resolved, envFile), "utf8");
|
|
24582
24996
|
const lines = [...content.matchAll(ENV_LINE_GLOBAL_RE)];
|
|
24583
24997
|
if (lines.length === 0) {
|
|
24584
24998
|
continue;
|
|
@@ -24609,7 +25023,7 @@ async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
|
|
|
24609
25023
|
for (const envFile of [".env.production", ".env"]) {
|
|
24610
25024
|
const envPath = path78.join(resolved, envFile);
|
|
24611
25025
|
try {
|
|
24612
|
-
const content = await
|
|
25026
|
+
const content = await readFile27(envPath, "utf8");
|
|
24613
25027
|
const withoutAssignments = content.replace(ENV_LINE_GLOBAL_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
24614
25028
|
const patched = withoutAssignments ? `${withoutAssignments}
|
|
24615
25029
|
${line2}
|
|
@@ -24689,7 +25103,7 @@ async function assertDeployableDistHasLiveApiBaseUrl(distDirectory, expectedApiB
|
|
|
24689
25103
|
let found = false;
|
|
24690
25104
|
let foundPlaceholder = false;
|
|
24691
25105
|
for await (const file of walkFiles(dist)) {
|
|
24692
|
-
const content = await
|
|
25106
|
+
const content = await readFile27(file, "utf8");
|
|
24693
25107
|
if (content.includes(needle)) {
|
|
24694
25108
|
found = true;
|
|
24695
25109
|
}
|
|
@@ -25374,7 +25788,7 @@ function throwOnManifestIssues(issues) {
|
|
|
25374
25788
|
return;
|
|
25375
25789
|
}
|
|
25376
25790
|
throw new Error(
|
|
25377
|
-
`apitogo.config.json is not platform-compatible: ${issues.map((
|
|
25791
|
+
`apitogo.config.json is not platform-compatible: ${issues.map((issue4) => `${issue4.field}: ${issue4.message}`).join("; ")}`
|
|
25378
25792
|
);
|
|
25379
25793
|
}
|
|
25380
25794
|
async function runApplyFromSiteDirectory(client, config2, args, progress) {
|
|
@@ -26392,14 +26806,14 @@ function formatBlockedOrFailedResultForHuman(parsed) {
|
|
|
26392
26806
|
const lines = [
|
|
26393
26807
|
typeof parsed.userMessage === "string" ? parsed.userMessage : "Deploy is blocked: apitogo.config.json has errors."
|
|
26394
26808
|
];
|
|
26395
|
-
for (const
|
|
26396
|
-
lines.push(` - ${formatGatewayPlanIssue(
|
|
26809
|
+
for (const issue4 of Array.isArray(parsed.errors) ? parsed.errors : []) {
|
|
26810
|
+
lines.push(` - ${formatGatewayPlanIssue(issue4)}`);
|
|
26397
26811
|
}
|
|
26398
26812
|
const warnings = Array.isArray(parsed.warnings) ? parsed.warnings : [];
|
|
26399
26813
|
if (warnings.length > 0) {
|
|
26400
26814
|
lines.push("", "Warnings:");
|
|
26401
|
-
for (const
|
|
26402
|
-
lines.push(` - ${formatGatewayPlanIssue(
|
|
26815
|
+
for (const issue4 of warnings) {
|
|
26816
|
+
lines.push(` - ${formatGatewayPlanIssue(issue4)}`);
|
|
26403
26817
|
}
|
|
26404
26818
|
}
|
|
26405
26819
|
lines.push(
|
|
@@ -26828,7 +27242,7 @@ import path83 from "node:path";
|
|
|
26828
27242
|
|
|
26829
27243
|
// src/cli/deploy/gateway.ts
|
|
26830
27244
|
import { createHash as createHash4 } from "node:crypto";
|
|
26831
|
-
import { readFile as
|
|
27245
|
+
import { readFile as readFile28 } from "node:fs/promises";
|
|
26832
27246
|
init_invariant();
|
|
26833
27247
|
var GatewayDeployError = class extends ApitogoError {
|
|
26834
27248
|
constructor(code, message, developerHint) {
|
|
@@ -26868,7 +27282,7 @@ async function readSpec(gateway) {
|
|
|
26868
27282
|
"Add one to gateway.specs in the config."
|
|
26869
27283
|
);
|
|
26870
27284
|
try {
|
|
26871
|
-
return await
|
|
27285
|
+
return await readFile28(file);
|
|
26872
27286
|
} catch {
|
|
26873
27287
|
throw new GatewayDeployError(
|
|
26874
27288
|
"GATEWAY_SPEC_UNREADABLE",
|
|
@@ -27062,67 +27476,693 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27062
27476
|
};
|
|
27063
27477
|
}
|
|
27064
27478
|
|
|
27065
|
-
// src/cli/
|
|
27066
|
-
|
|
27067
|
-
|
|
27068
|
-
|
|
27069
|
-
|
|
27070
|
-
|
|
27071
|
-
|
|
27072
|
-
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
|
|
27077
|
-
|
|
27479
|
+
// src/cli/deploy/identity.ts
|
|
27480
|
+
init_invariant();
|
|
27481
|
+
var IdentityDeployError = class extends ApitogoError {
|
|
27482
|
+
constructor(code, message, developerHint) {
|
|
27483
|
+
super(message, { developerHint, title: "Cannot deploy identity" });
|
|
27484
|
+
this.code = code;
|
|
27485
|
+
this.name = "IdentityDeployError";
|
|
27486
|
+
}
|
|
27487
|
+
};
|
|
27488
|
+
var IDENTITY_STEPS = ["request", "activate"];
|
|
27489
|
+
var PORTAL_CALLBACK_PATH = "/oauth/callback";
|
|
27490
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27491
|
+
function identityNotDeployableReason(resolved) {
|
|
27492
|
+
const identity = resolved.identity;
|
|
27493
|
+
if (!identity) return "identity is not in the config";
|
|
27494
|
+
if (identity.provider === "oidc" && (!identity.issuer || !identity.clientId))
|
|
27495
|
+
return 'identity.provider "oidc" needs issuer and clientId';
|
|
27496
|
+
return void 0;
|
|
27078
27497
|
}
|
|
27079
|
-
function
|
|
27080
|
-
return
|
|
27498
|
+
function identityRequestBody(resolved, portalUrl) {
|
|
27499
|
+
return {
|
|
27500
|
+
portalRedirectUris: [
|
|
27501
|
+
`${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}`
|
|
27502
|
+
],
|
|
27503
|
+
nativeSignup: resolved.identity?.native ?? true
|
|
27504
|
+
};
|
|
27081
27505
|
}
|
|
27082
|
-
|
|
27083
|
-
|
|
27084
|
-
|
|
27085
|
-
|
|
27086
|
-
|
|
27087
|
-
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27092
|
-
`
|
|
27093
|
-
`manifest: ${prepared.manifestSource === "file" ? "services.json" : "not needed"}`,
|
|
27094
|
-
...[
|
|
27095
|
-
plan2.auth ? "sign-in" : void 0,
|
|
27096
|
-
plan2.signup ? "sign-up" : void 0,
|
|
27097
|
-
plan2.userPanel ? "account pages" : void 0,
|
|
27098
|
-
plan2.plansPlugin ? "pricing, keys and usage pages" : void 0,
|
|
27099
|
-
plan2.apiReference ? "API reference" : void 0
|
|
27100
|
-
].filter((feature) => feature !== void 0).map((feature) => `will appear: ${feature}`),
|
|
27101
|
-
...plan2.skipped.map((s) => `left out: ${s.piece} (${s.reason})`)
|
|
27102
|
-
];
|
|
27103
|
-
const hash2 = await portalBuildHash(resolved, prepared);
|
|
27104
|
-
const previous = stepOf(ledger, "portal", "build");
|
|
27105
|
-
const built = await exists4(
|
|
27106
|
-
path83.join(resolved.state.buildDir, "index.html")
|
|
27506
|
+
function identityInputsHash(projectId, body) {
|
|
27507
|
+
return stableJson({ projectId, body });
|
|
27508
|
+
}
|
|
27509
|
+
async function portalUrlOf(resolved, projectId, client) {
|
|
27510
|
+
const known = (await readManifest(resolved.state))?.services.portal?.url;
|
|
27511
|
+
const url2 = known ?? resolveSwaUrl(await client.getProject(projectId));
|
|
27512
|
+
if (!url2)
|
|
27513
|
+
throw new IdentityDeployError(
|
|
27514
|
+
"IDENTITY_PORTAL_ADDRESS_UNKNOWN",
|
|
27515
|
+
"The portal has no public address yet, and the identity needs it for the sign-in redirect.",
|
|
27516
|
+
"Run `apitogo deploy --only portal` once so the platform gives the portal an address, then deploy identity."
|
|
27107
27517
|
);
|
|
27108
|
-
|
|
27109
|
-
|
|
27110
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
|
|
27117
|
-
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
27125
|
-
|
|
27518
|
+
return url2;
|
|
27519
|
+
}
|
|
27520
|
+
function rejected2(error) {
|
|
27521
|
+
if (error instanceof PlatformApiError && error.status === 409) {
|
|
27522
|
+
throw new IdentityDeployError(
|
|
27523
|
+
"IDENTITY_SETTINGS_CHANGED",
|
|
27524
|
+
"This project already has an identity with other settings, and its audience and portal address cannot be changed in place.",
|
|
27525
|
+
"Keep the portal address as it was, or remove the identity first (this deletes every user in it)."
|
|
27526
|
+
);
|
|
27527
|
+
}
|
|
27528
|
+
if (error instanceof PlatformApiError && error.status === 400) {
|
|
27529
|
+
const lines = describePlatformProblem(error);
|
|
27530
|
+
throw new IdentityDeployError(
|
|
27531
|
+
"IDENTITY_REJECTED",
|
|
27532
|
+
`The platform refused the identity request:
|
|
27533
|
+
${lines.join("\n ") || error.message}`,
|
|
27534
|
+
"Fix the cause in apitogo.config.json and run deploy again."
|
|
27535
|
+
);
|
|
27536
|
+
}
|
|
27537
|
+
throw error;
|
|
27538
|
+
}
|
|
27539
|
+
async function recordOwnProvider(resolved, projectId) {
|
|
27540
|
+
const identity = resolved.identity;
|
|
27541
|
+
const issuer = identity?.issuer?.replace(/\/+$/, "");
|
|
27542
|
+
const clientId = identity?.clientId;
|
|
27543
|
+
if (!issuer || !clientId)
|
|
27544
|
+
throw new Error("identity.issuer and identity.clientId are needed.");
|
|
27545
|
+
const known = (await readManifest(resolved.state))?.services.identity;
|
|
27546
|
+
const changed = known?.provider !== "oidc" || known.issuer !== issuer || known.clientId !== clientId;
|
|
27547
|
+
if (changed)
|
|
27548
|
+
await writeManifest(
|
|
27549
|
+
resolved.state,
|
|
27550
|
+
withIdentityEntry(await readManifest(resolved.state), {
|
|
27551
|
+
projectId,
|
|
27552
|
+
projectName: resolved.project.name,
|
|
27553
|
+
provider: "oidc",
|
|
27554
|
+
issuer,
|
|
27555
|
+
clientId,
|
|
27556
|
+
nativeSignup: false
|
|
27557
|
+
})
|
|
27558
|
+
);
|
|
27559
|
+
return {
|
|
27560
|
+
status: changed ? "deployed" : "unchanged",
|
|
27561
|
+
steps: [],
|
|
27562
|
+
issuer,
|
|
27563
|
+
clientId
|
|
27564
|
+
};
|
|
27565
|
+
}
|
|
27566
|
+
async function deployIdentity(resolved, projectId, deps, options = {}) {
|
|
27567
|
+
if (resolved.identity?.provider === "oidc")
|
|
27568
|
+
return recordOwnProvider(resolved, projectId);
|
|
27569
|
+
const client = deps.client;
|
|
27570
|
+
const sleep5 = deps.sleep ?? defaultSleep2;
|
|
27571
|
+
const now = deps.now ?? Date.now;
|
|
27572
|
+
const timeoutMs = deps.activateTimeoutMs ?? 12e4;
|
|
27573
|
+
const intervalMs = deps.pollIntervalMs ?? 2e3;
|
|
27574
|
+
const portalUrl = await portalUrlOf(resolved, projectId, client);
|
|
27575
|
+
const body = identityRequestBody(resolved, portalUrl);
|
|
27576
|
+
const inputsHash = identityInputsHash(projectId, body);
|
|
27577
|
+
const requestStep = {
|
|
27578
|
+
name: "request",
|
|
27579
|
+
hash: () => inputsHash,
|
|
27580
|
+
run: async () => {
|
|
27581
|
+
try {
|
|
27582
|
+
const answer = await client.putProjectIdentity(projectId, body);
|
|
27583
|
+
return { status: answer.status };
|
|
27584
|
+
} catch (error) {
|
|
27585
|
+
return rejected2(error);
|
|
27586
|
+
}
|
|
27587
|
+
},
|
|
27588
|
+
// An identity that was removed, or that failed, is not a result worth keeping.
|
|
27589
|
+
stillValid: async () => {
|
|
27590
|
+
const found = await client.getProjectIdentity(projectId);
|
|
27591
|
+
return found !== null && found.status !== "Failed" && found.status !== "Released" && found.status !== "Releasing";
|
|
27592
|
+
}
|
|
27593
|
+
};
|
|
27594
|
+
const activateStep = {
|
|
27595
|
+
name: "activate",
|
|
27596
|
+
hash: () => inputsHash,
|
|
27597
|
+
run: async () => {
|
|
27598
|
+
const deadline = now() + timeoutMs;
|
|
27599
|
+
for (; ; ) {
|
|
27600
|
+
const found = await client.getProjectIdentity(projectId);
|
|
27601
|
+
if (found?.status === "Active")
|
|
27602
|
+
return {
|
|
27603
|
+
issuer: found.issuer,
|
|
27604
|
+
clientId: found.clientId,
|
|
27605
|
+
audience: found.audience,
|
|
27606
|
+
nativeSignup: String(found.nativeSignup)
|
|
27607
|
+
};
|
|
27608
|
+
if (found?.status === "Failed")
|
|
27609
|
+
throw new IdentityDeployError(
|
|
27610
|
+
"IDENTITY_FAILED",
|
|
27611
|
+
`The platform could not make the identity: ${[found.errorCode, found.errorMessage].filter(Boolean).join(" ") || "it failed"}.`,
|
|
27612
|
+
"Fix the cause and run deploy again; the request is sent again."
|
|
27613
|
+
);
|
|
27614
|
+
if (now() >= deadline)
|
|
27615
|
+
throw new IdentityDeployError(
|
|
27616
|
+
"IDENTITY_TIMEOUT",
|
|
27617
|
+
`The identity did not become active within ${Math.round(timeoutMs / 1e3)} seconds (state: ${found?.status ?? "not found"}).`,
|
|
27618
|
+
"Run deploy again to keep waiting."
|
|
27619
|
+
);
|
|
27620
|
+
await sleep5(intervalMs);
|
|
27621
|
+
}
|
|
27622
|
+
},
|
|
27623
|
+
stillValid: async () => (await client.getProjectIdentity(projectId))?.status === "Active"
|
|
27624
|
+
};
|
|
27625
|
+
const outcomes = await runPipeline(
|
|
27626
|
+
resolved.state,
|
|
27627
|
+
"identity",
|
|
27628
|
+
[requestStep, activateStep],
|
|
27629
|
+
{ force: options.force }
|
|
27630
|
+
);
|
|
27631
|
+
const activate = outcomes.find((outcome) => outcome.step === "activate");
|
|
27632
|
+
const ran = activate !== void 0 && !activate.skipped;
|
|
27633
|
+
const known = (await readManifest(resolved.state))?.services.identity;
|
|
27634
|
+
let learned;
|
|
27635
|
+
if (ran || known?.issuer === void 0) {
|
|
27636
|
+
learned = await client.getProjectIdentity(projectId) ?? void 0;
|
|
27637
|
+
if (learned?.status === "Active") {
|
|
27638
|
+
await writeManifest(
|
|
27639
|
+
resolved.state,
|
|
27640
|
+
withIdentityEntry(await readManifest(resolved.state), {
|
|
27641
|
+
projectId,
|
|
27642
|
+
projectName: resolved.project.name,
|
|
27643
|
+
issuer: learned.issuer,
|
|
27644
|
+
clientId: learned.clientId,
|
|
27645
|
+
audience: learned.audience,
|
|
27646
|
+
nativeSignup: learned.nativeSignup
|
|
27647
|
+
})
|
|
27648
|
+
);
|
|
27649
|
+
}
|
|
27650
|
+
}
|
|
27651
|
+
return {
|
|
27652
|
+
status: ran ? "deployed" : "unchanged",
|
|
27653
|
+
steps: outcomes.map((outcome) => ({
|
|
27654
|
+
step: outcome.step,
|
|
27655
|
+
skipped: outcome.skipped
|
|
27656
|
+
})),
|
|
27657
|
+
issuer: learned?.issuer ?? known?.issuer,
|
|
27658
|
+
clientId: learned?.clientId ?? known?.clientId
|
|
27659
|
+
};
|
|
27660
|
+
}
|
|
27661
|
+
|
|
27662
|
+
// src/cli/deploy/plans.ts
|
|
27663
|
+
init_invariant();
|
|
27664
|
+
var PlansDeployError = class extends ApitogoError {
|
|
27665
|
+
constructor(code, message, developerHint) {
|
|
27666
|
+
super(message, { developerHint, title: "Cannot deploy plans" });
|
|
27667
|
+
this.code = code;
|
|
27668
|
+
this.name = "PlansDeployError";
|
|
27669
|
+
}
|
|
27670
|
+
};
|
|
27671
|
+
var PLANS_STEPS = ["catalog", "backend", "identity-link"];
|
|
27672
|
+
var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27673
|
+
var BACKEND_MODULE = "dev-portal";
|
|
27674
|
+
var PLANS_CAPABILITIES = { catalog: true, keys: true, usage: true };
|
|
27675
|
+
async function plansInputs(resolved, projectId) {
|
|
27676
|
+
const plans = resolved.plans;
|
|
27677
|
+
if (!plans) throw new Error("plans is not in the config.");
|
|
27678
|
+
const wanted = toPlatformPlans(await readPlansFile(plans.file));
|
|
27679
|
+
const manifest = await readManifest(resolved.state);
|
|
27680
|
+
const identity = manifest?.services.identity;
|
|
27681
|
+
const ownIdentity = Boolean(
|
|
27682
|
+
identity?.provider === "apitogo" && identity.issuer
|
|
27683
|
+
);
|
|
27684
|
+
return {
|
|
27685
|
+
wanted,
|
|
27686
|
+
manifest,
|
|
27687
|
+
ownIdentity,
|
|
27688
|
+
catalogHash: stableJson({ projectId, plans: wanted }),
|
|
27689
|
+
backendHash: stableJson({ projectId }),
|
|
27690
|
+
linkHash: stableJson({
|
|
27691
|
+
projectId,
|
|
27692
|
+
issuer: identity?.issuer,
|
|
27693
|
+
clientId: identity?.clientId
|
|
27694
|
+
})
|
|
27695
|
+
};
|
|
27696
|
+
}
|
|
27697
|
+
function plansNotDeployableReason(resolved) {
|
|
27698
|
+
return resolved.plans ? void 0 : "plans is not in the config";
|
|
27699
|
+
}
|
|
27700
|
+
async function allActionKeys(client, projectId) {
|
|
27701
|
+
const keys = [];
|
|
27702
|
+
try {
|
|
27703
|
+
for (let page = 1; ; page++) {
|
|
27704
|
+
const result = await client.listGatewayActions(projectId, page, 100);
|
|
27705
|
+
keys.push(...result.items.map((item) => item.actionKey));
|
|
27706
|
+
if (result.items.length === 0 || page * result.pageSize >= result.totalCount)
|
|
27707
|
+
break;
|
|
27708
|
+
}
|
|
27709
|
+
} catch {
|
|
27710
|
+
return [];
|
|
27711
|
+
}
|
|
27712
|
+
return keys;
|
|
27713
|
+
}
|
|
27714
|
+
function refused(error) {
|
|
27715
|
+
if (error instanceof PlatformApiError && error.status === 400) {
|
|
27716
|
+
const lines = describePlatformProblem(error);
|
|
27717
|
+
throw new PlansDeployError(
|
|
27718
|
+
"PLANS_REJECTED",
|
|
27719
|
+
`The platform refused a plan:
|
|
27720
|
+
${lines.join("\n ") || error.message}`,
|
|
27721
|
+
"Fix the plan in the plans file and run deploy again."
|
|
27722
|
+
);
|
|
27723
|
+
}
|
|
27724
|
+
throw error;
|
|
27725
|
+
}
|
|
27726
|
+
async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
27727
|
+
const plans = resolved.plans;
|
|
27728
|
+
if (!plans) throw new Error("plans is not in the config.");
|
|
27729
|
+
const client = deps.client;
|
|
27730
|
+
const sleep5 = deps.sleep ?? defaultSleep3;
|
|
27731
|
+
const now = deps.now ?? Date.now;
|
|
27732
|
+
const intervalMs = deps.pollIntervalMs ?? 2e3;
|
|
27733
|
+
const backendTimeoutMs = deps.backendTimeoutMs ?? 9e5;
|
|
27734
|
+
const linkTimeoutMs = deps.linkTimeoutMs ?? 6e5;
|
|
27735
|
+
const { wanted, manifest, ownIdentity, catalogHash, backendHash, linkHash } = await plansInputs(resolved, projectId);
|
|
27736
|
+
const catalogStep = {
|
|
27737
|
+
name: "catalog",
|
|
27738
|
+
hash: () => catalogHash,
|
|
27739
|
+
run: async () => {
|
|
27740
|
+
if (wanted.length === 0) return { created: "0", updated: "0" };
|
|
27741
|
+
const fallback = wanted.some((plan2) => plan2.includedActionKeys === null) ? await allActionKeys(client, projectId) : [];
|
|
27742
|
+
const bodies = wanted.map((plan2) => ({
|
|
27743
|
+
...plan2,
|
|
27744
|
+
includedActionKeys: plan2.includedActionKeys ?? fallback
|
|
27745
|
+
}));
|
|
27746
|
+
const checked = await client.validateGatewayPlans(bodies, { projectId });
|
|
27747
|
+
if (!checked.valid)
|
|
27748
|
+
throw new PlansDeployError(
|
|
27749
|
+
"PLANS_REJECTED",
|
|
27750
|
+
`The platform refused the plans:
|
|
27751
|
+
${checked.errors.map((issue4) => `${issue4.field}: ${issue4.message}`).join("\n ")}`,
|
|
27752
|
+
"Fix the listed problems in the plans file and run deploy again."
|
|
27753
|
+
);
|
|
27754
|
+
const existing = new Map(
|
|
27755
|
+
(await client.listGatewayPlans(projectId)).map((plan2) => [
|
|
27756
|
+
plan2.sku.toLowerCase(),
|
|
27757
|
+
plan2
|
|
27758
|
+
])
|
|
27759
|
+
);
|
|
27760
|
+
let created = 0;
|
|
27761
|
+
let updated = 0;
|
|
27762
|
+
for (const body of bodies) {
|
|
27763
|
+
const found = existing.get(body.sku);
|
|
27764
|
+
try {
|
|
27765
|
+
if (found) {
|
|
27766
|
+
await client.updateGatewayPlan(projectId, found.id, {
|
|
27767
|
+
displayName: body.displayName,
|
|
27768
|
+
isActive: true,
|
|
27769
|
+
limitsJson: body.limitsJson ?? null,
|
|
27770
|
+
includedActionKeys: body.includedActionKeys,
|
|
27771
|
+
priceAmount: body.priceAmount,
|
|
27772
|
+
pricingModel: body.pricingModel,
|
|
27773
|
+
priceLabel: body.priceLabel,
|
|
27774
|
+
priceCurrency: body.priceCurrency,
|
|
27775
|
+
billingPeriod: body.billingPeriod,
|
|
27776
|
+
sortOrder: body.sortOrder
|
|
27777
|
+
});
|
|
27778
|
+
updated++;
|
|
27779
|
+
} else {
|
|
27780
|
+
await client.createGatewayPlan(projectId, body);
|
|
27781
|
+
created++;
|
|
27782
|
+
}
|
|
27783
|
+
} catch (error) {
|
|
27784
|
+
refused(error);
|
|
27785
|
+
}
|
|
27786
|
+
}
|
|
27787
|
+
return { created: String(created), updated: String(updated) };
|
|
27788
|
+
},
|
|
27789
|
+
// A plan someone deleted on the platform is not a result worth keeping.
|
|
27790
|
+
stillValid: async () => {
|
|
27791
|
+
const present = new Set(
|
|
27792
|
+
(await client.listGatewayPlans(projectId)).filter((plan2) => plan2.isActive).map((plan2) => plan2.sku.toLowerCase())
|
|
27793
|
+
);
|
|
27794
|
+
return wanted.every((plan2) => present.has(plan2.sku));
|
|
27795
|
+
}
|
|
27796
|
+
};
|
|
27797
|
+
const backendOf = async () => (await client.getDevPortalBackend(projectId))?.fqdn;
|
|
27798
|
+
const backendStep = {
|
|
27799
|
+
name: "backend",
|
|
27800
|
+
hash: () => backendHash,
|
|
27801
|
+
run: async () => {
|
|
27802
|
+
let fqdn = await backendOf();
|
|
27803
|
+
if (!fqdn) {
|
|
27804
|
+
await client.activateModule(projectId, BACKEND_MODULE);
|
|
27805
|
+
const deadline = now() + backendTimeoutMs;
|
|
27806
|
+
for (; ; ) {
|
|
27807
|
+
fqdn = await backendOf();
|
|
27808
|
+
if (fqdn) break;
|
|
27809
|
+
if (now() >= deadline)
|
|
27810
|
+
throw new PlansDeployError(
|
|
27811
|
+
"PLANS_BACKEND_TIMEOUT",
|
|
27812
|
+
`The plans backend did not get an address within ${Math.round(backendTimeoutMs / 1e3)} seconds.`,
|
|
27813
|
+
"Run deploy again to keep waiting."
|
|
27814
|
+
);
|
|
27815
|
+
await sleep5(intervalMs);
|
|
27816
|
+
}
|
|
27817
|
+
}
|
|
27818
|
+
return { fqdn };
|
|
27819
|
+
},
|
|
27820
|
+
stillValid: async () => Boolean(await backendOf())
|
|
27821
|
+
};
|
|
27822
|
+
const linkStep = {
|
|
27823
|
+
name: "identity-link",
|
|
27824
|
+
hash: () => linkHash,
|
|
27825
|
+
run: async () => {
|
|
27826
|
+
const queued = await client.linkDevPortalIdentity(projectId);
|
|
27827
|
+
const deadline = now() + linkTimeoutMs;
|
|
27828
|
+
for (; ; ) {
|
|
27829
|
+
const job = (await client.listProvisioningJobs(projectId)).jobs.find(
|
|
27830
|
+
(item) => item.id === queued.jobId
|
|
27831
|
+
);
|
|
27832
|
+
const status2 = job?.status.toLowerCase();
|
|
27833
|
+
if (status2?.startsWith("completed")) return { jobId: queued.jobId };
|
|
27834
|
+
if (status2 === "failed")
|
|
27835
|
+
throw new PlansDeployError(
|
|
27836
|
+
"PLANS_LINK_FAILED",
|
|
27837
|
+
`Applying the identity to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
|
|
27838
|
+
"Run deploy again; the link is applied again."
|
|
27839
|
+
);
|
|
27840
|
+
if (now() >= deadline)
|
|
27841
|
+
throw new PlansDeployError(
|
|
27842
|
+
"PLANS_LINK_TIMEOUT",
|
|
27843
|
+
`Applying the identity did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
|
|
27844
|
+
"Run deploy again to keep waiting."
|
|
27845
|
+
);
|
|
27846
|
+
await sleep5(intervalMs);
|
|
27847
|
+
}
|
|
27848
|
+
}
|
|
27849
|
+
};
|
|
27850
|
+
const steps = [catalogStep, backendStep, ...ownIdentity ? [linkStep] : []];
|
|
27851
|
+
const outcomes = await runPipeline(
|
|
27852
|
+
resolved.state,
|
|
27853
|
+
"plans",
|
|
27854
|
+
steps,
|
|
27855
|
+
{
|
|
27856
|
+
force: options.force
|
|
27857
|
+
}
|
|
27858
|
+
);
|
|
27859
|
+
const catalog = outcomes.find((outcome) => outcome.step === "catalog");
|
|
27860
|
+
const ran = outcomes.some((outcome) => !outcome.skipped);
|
|
27861
|
+
const known = manifest?.services.plans;
|
|
27862
|
+
let apiBaseUrl = known?.apiBaseUrl;
|
|
27863
|
+
if (ran || known?.apiBaseUrl === void 0) {
|
|
27864
|
+
apiBaseUrl = toApiBaseUrl(await backendOf()) ?? apiBaseUrl;
|
|
27865
|
+
if (apiBaseUrl) {
|
|
27866
|
+
const previous = known?.catalogRevision ?? 0;
|
|
27867
|
+
await writeManifest(
|
|
27868
|
+
resolved.state,
|
|
27869
|
+
withPlansEntry(await readManifest(resolved.state), {
|
|
27870
|
+
projectId,
|
|
27871
|
+
projectName: resolved.project.name,
|
|
27872
|
+
apiBaseUrl,
|
|
27873
|
+
billing: plans.billing.provider,
|
|
27874
|
+
catalogRevision: catalog && !catalog.skipped ? previous + 1 : Math.max(previous, 1),
|
|
27875
|
+
capabilities: PLANS_CAPABILITIES
|
|
27876
|
+
})
|
|
27877
|
+
);
|
|
27878
|
+
}
|
|
27879
|
+
}
|
|
27880
|
+
return {
|
|
27881
|
+
status: ran ? "deployed" : "unchanged",
|
|
27882
|
+
steps: outcomes.map((outcome) => ({
|
|
27883
|
+
step: outcome.step,
|
|
27884
|
+
skipped: outcome.skipped
|
|
27885
|
+
})),
|
|
27886
|
+
apiBaseUrl
|
|
27887
|
+
};
|
|
27888
|
+
}
|
|
27889
|
+
|
|
27890
|
+
// src/cli/portal/services-report.ts
|
|
27891
|
+
function stepState(record, hash2) {
|
|
27892
|
+
if (!record) return "not done yet";
|
|
27893
|
+
if (record.status === "failed")
|
|
27894
|
+
return `failed last time (${record.error ?? "no message"})`;
|
|
27895
|
+
if (record.status === "running") return "did not finish last time";
|
|
27896
|
+
if (hash2 === void 0)
|
|
27897
|
+
return "done (cannot compare: an input is not known locally)";
|
|
27898
|
+
return record.hash === hash2 ? "done, unchanged" : "inputs changed";
|
|
27899
|
+
}
|
|
27900
|
+
var isDone = (record, hash2) => record?.status === "succeeded" && hash2 !== void 0 && record.hash === hash2;
|
|
27901
|
+
async function planIdentityService(resolved, ledger, manifest) {
|
|
27902
|
+
const base = { service: "identity", issues: [] };
|
|
27903
|
+
const identity = resolved.identity;
|
|
27904
|
+
if (!identity) throw new Error("identity is not configured");
|
|
27905
|
+
const why = identityNotDeployableReason(resolved);
|
|
27906
|
+
if (why)
|
|
27907
|
+
return {
|
|
27908
|
+
...base,
|
|
27909
|
+
action: "not-deployable-yet",
|
|
27910
|
+
reason: `configured, but ${why}`,
|
|
27911
|
+
details: []
|
|
27912
|
+
};
|
|
27913
|
+
const known = manifest?.services.identity;
|
|
27914
|
+
if (identity.provider === "oidc") {
|
|
27915
|
+
const same = known?.provider === "oidc" && known.issuer === identity.issuer?.replace(/\/+$/, "") && known.clientId === identity.clientId;
|
|
27916
|
+
return {
|
|
27917
|
+
...base,
|
|
27918
|
+
action: same ? "skip" : "deploy",
|
|
27919
|
+
reason: same ? "deploy skipped: services.json already records your sign-in provider" : "will record your own sign-in provider in services.json (nothing is made on the platform)",
|
|
27920
|
+
details: [
|
|
27921
|
+
`provider: oidc ${identity.issuer}`,
|
|
27922
|
+
`client id: ${identity.clientId}`
|
|
27923
|
+
]
|
|
27924
|
+
};
|
|
27925
|
+
}
|
|
27926
|
+
const projectId = resolved.project.id;
|
|
27927
|
+
const portalUrl = manifest?.services.portal?.url;
|
|
27928
|
+
const body = portalUrl ? identityRequestBody(resolved, portalUrl) : void 0;
|
|
27929
|
+
const hash2 = projectId && body ? identityInputsHash(projectId, body) : void 0;
|
|
27930
|
+
const details = [
|
|
27931
|
+
"provider: apitogo (one issuer for this project)",
|
|
27932
|
+
`sign-up with email and password: ${identity.native ? "on" : "off"}`,
|
|
27933
|
+
portalUrl ? `sign-in redirect: ${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}` : "sign-in redirect: the portal address is not known locally; deploy takes it from the project",
|
|
27934
|
+
...known?.issuer ? [`issuer: ${known.issuer}`] : [],
|
|
27935
|
+
...IDENTITY_STEPS.map(
|
|
27936
|
+
(name) => `step ${name}: ${stepState(stepOf(ledger, "identity", name), hash2)}`
|
|
27937
|
+
)
|
|
27938
|
+
];
|
|
27939
|
+
const allDone = IDENTITY_STEPS.every(
|
|
27940
|
+
(name) => isDone(stepOf(ledger, "identity", name), hash2)
|
|
27941
|
+
);
|
|
27942
|
+
return {
|
|
27943
|
+
...base,
|
|
27944
|
+
action: allDone ? "skip" : "deploy",
|
|
27945
|
+
reason: allDone ? "deploy skipped: nothing it depends on has changed" : "will deploy: the identity is not made yet, or its settings changed",
|
|
27946
|
+
details
|
|
27947
|
+
};
|
|
27948
|
+
}
|
|
27949
|
+
async function planPlansService(resolved, ledger) {
|
|
27950
|
+
const base = { service: "plans", issues: [] };
|
|
27951
|
+
const why = plansNotDeployableReason(resolved);
|
|
27952
|
+
if (why)
|
|
27953
|
+
return {
|
|
27954
|
+
...base,
|
|
27955
|
+
action: "not-deployable-yet",
|
|
27956
|
+
reason: `configured, but ${why}`,
|
|
27957
|
+
details: []
|
|
27958
|
+
};
|
|
27959
|
+
const projectId = resolved.project.id ?? "";
|
|
27960
|
+
let inputs;
|
|
27961
|
+
try {
|
|
27962
|
+
inputs = await plansInputs(resolved, projectId);
|
|
27963
|
+
} catch (error) {
|
|
27964
|
+
if (!(error instanceof PlansFileError)) throw error;
|
|
27965
|
+
return {
|
|
27966
|
+
...base,
|
|
27967
|
+
action: "blocked",
|
|
27968
|
+
reason: "cannot deploy until this is fixed",
|
|
27969
|
+
details: [],
|
|
27970
|
+
issues: [
|
|
27971
|
+
{
|
|
27972
|
+
code: error.code,
|
|
27973
|
+
path: "plans.file",
|
|
27974
|
+
message: error.message,
|
|
27975
|
+
hint: error.developerHint
|
|
27976
|
+
}
|
|
27977
|
+
]
|
|
27978
|
+
};
|
|
27979
|
+
}
|
|
27980
|
+
const hashes = {
|
|
27981
|
+
catalog: projectId ? inputs.catalogHash : void 0,
|
|
27982
|
+
backend: projectId ? inputs.backendHash : void 0,
|
|
27983
|
+
"identity-link": projectId ? inputs.linkHash : void 0
|
|
27984
|
+
};
|
|
27985
|
+
const steps = PLANS_STEPS.filter(
|
|
27986
|
+
(name) => name !== "identity-link" || inputs.ownIdentity
|
|
27987
|
+
);
|
|
27988
|
+
const details = [
|
|
27989
|
+
...inputs.wanted.length === 0 ? ["plans: none in the file"] : inputs.wanted.map((plan2) => `plan ${plan2.sku}: ${plan2.displayName}`),
|
|
27990
|
+
inputs.ownIdentity ? "sign-in: the project's identity will be applied to the plans backend" : "sign-in: no identity of the platform deployed, so nothing is applied to the plans backend",
|
|
27991
|
+
...steps.map(
|
|
27992
|
+
(name) => `step ${name}: ${stepState(stepOf(ledger, "plans", name), hashes[name])}`
|
|
27993
|
+
)
|
|
27994
|
+
];
|
|
27995
|
+
const allDone = steps.every(
|
|
27996
|
+
(name) => isDone(stepOf(ledger, "plans", name), hashes[name])
|
|
27997
|
+
);
|
|
27998
|
+
return {
|
|
27999
|
+
...base,
|
|
28000
|
+
action: allDone ? "skip" : "deploy",
|
|
28001
|
+
reason: allDone ? "deploy skipped: nothing it depends on has changed" : "will deploy: the plans file, the backend or the identity is not applied yet",
|
|
28002
|
+
details
|
|
28003
|
+
};
|
|
28004
|
+
}
|
|
28005
|
+
var failedStep = (ledger, service, names) => names.map((name) => ({ name, record: stepOf(ledger, service, name) })).find((step2) => step2.record?.status === "failed");
|
|
28006
|
+
var unreachable = (error) => `platform: not reachable (${error instanceof Error ? error.message : String(error)}); showing local records only`;
|
|
28007
|
+
async function identityStatus(resolved, ledger, manifest, remote) {
|
|
28008
|
+
const entry = manifest?.services.identity;
|
|
28009
|
+
const failed = failedStep(ledger, "identity", IDENTITY_STEPS);
|
|
28010
|
+
const details = [];
|
|
28011
|
+
if (entry?.provider) details.push(`provider: ${entry.provider}`);
|
|
28012
|
+
if (entry?.issuer) details.push(`issuer: ${entry.issuer}`);
|
|
28013
|
+
if (entry?.clientId) details.push(`client id: ${entry.clientId}`);
|
|
28014
|
+
let state = entry ? "ready" : "not-deployed";
|
|
28015
|
+
const projectId = resolved.project.id ?? manifest?.project.id;
|
|
28016
|
+
if (resolved.identity?.provider === "oidc") {
|
|
28017
|
+
details.push("platform: nothing to ask (your own sign-in provider)");
|
|
28018
|
+
} else if (remote && projectId) {
|
|
28019
|
+
try {
|
|
28020
|
+
const platform = await remote(projectId);
|
|
28021
|
+
if (!platform) {
|
|
28022
|
+
details.push("platform: no identity for this project yet");
|
|
28023
|
+
if (entry) state = "drift";
|
|
28024
|
+
} else {
|
|
28025
|
+
details.push(
|
|
28026
|
+
`platform state: ${platform.status}`,
|
|
28027
|
+
`sign-up with email and password: ${platform.nativeSignup ? "on" : "off"}`,
|
|
28028
|
+
`sign-in redirect: ${platform.portalRedirectUris.join(", ") || "none"}`
|
|
28029
|
+
);
|
|
28030
|
+
if (platform.status === "Failed")
|
|
28031
|
+
details.push(
|
|
28032
|
+
`platform problem: ${[platform.errorCode, platform.errorMessage].filter(Boolean).join(" ") || "no message"}`
|
|
28033
|
+
);
|
|
28034
|
+
const same = entry?.issuer === platform.issuer.replace(/\/+$/, "");
|
|
28035
|
+
if (entry && (!same || platform.status !== "Active")) {
|
|
28036
|
+
details.push(
|
|
28037
|
+
"drift: services.json does not match the platform's identity"
|
|
28038
|
+
);
|
|
28039
|
+
state = "drift";
|
|
28040
|
+
}
|
|
28041
|
+
}
|
|
28042
|
+
} catch (error) {
|
|
28043
|
+
details.push(unreachable(error));
|
|
28044
|
+
}
|
|
28045
|
+
} else if (!projectId) {
|
|
28046
|
+
details.push("platform: not asked, this folder is not linked to a project");
|
|
28047
|
+
} else {
|
|
28048
|
+
details.push("platform: not asked, you are not logged in");
|
|
28049
|
+
}
|
|
28050
|
+
if (failed) {
|
|
28051
|
+
details.push(`error: ${failed.record?.error ?? "unknown"}`);
|
|
28052
|
+
state = "deploy-failed";
|
|
28053
|
+
}
|
|
28054
|
+
return { service: "identity", state, details };
|
|
28055
|
+
}
|
|
28056
|
+
async function plansStatus(resolved, ledger, manifest, remote, wantedSkus2) {
|
|
28057
|
+
const entry = manifest?.services.plans;
|
|
28058
|
+
const failed = failedStep(ledger, "plans", PLANS_STEPS);
|
|
28059
|
+
const details = [];
|
|
28060
|
+
if (entry?.apiBaseUrl) details.push(`plans backend: ${entry.apiBaseUrl}`);
|
|
28061
|
+
if (entry?.catalogRevision !== void 0)
|
|
28062
|
+
details.push(`catalog revision: ${entry.catalogRevision}`);
|
|
28063
|
+
let state = entry ? "ready" : "not-deployed";
|
|
28064
|
+
const projectId = resolved.project.id ?? manifest?.project.id;
|
|
28065
|
+
if (remote && projectId) {
|
|
28066
|
+
try {
|
|
28067
|
+
const platform = await remote(projectId);
|
|
28068
|
+
const active2 = platform.plans.filter((plan2) => plan2.isActive).map((plan2) => plan2.sku.toLowerCase());
|
|
28069
|
+
details.push(
|
|
28070
|
+
`plans on the platform: ${active2.length > 0 ? active2.join(", ") : "none"}`
|
|
28071
|
+
);
|
|
28072
|
+
const missing = (wantedSkus2 ?? []).filter((sku) => !active2.includes(sku));
|
|
28073
|
+
if (missing.length > 0) {
|
|
28074
|
+
details.push(
|
|
28075
|
+
`drift: plans in the file but not on the platform: ${missing.join(", ")}`
|
|
28076
|
+
);
|
|
28077
|
+
if (entry) state = "drift";
|
|
28078
|
+
}
|
|
28079
|
+
if (!platform.backendAddress) {
|
|
28080
|
+
details.push("platform: the plans backend has no address");
|
|
28081
|
+
if (entry) state = "drift";
|
|
28082
|
+
} else if (entry?.apiBaseUrl && entry.apiBaseUrl !== platform.backendAddress.replace(/\/+$/, "")) {
|
|
28083
|
+
details.push(
|
|
28084
|
+
`drift: the plans backend is at ${platform.backendAddress}, services.json says ${entry.apiBaseUrl}`
|
|
28085
|
+
);
|
|
28086
|
+
state = "drift";
|
|
28087
|
+
}
|
|
28088
|
+
} catch (error) {
|
|
28089
|
+
details.push(
|
|
28090
|
+
error instanceof PlatformApiError && error.status === 404 ? "platform: no plans for this project yet" : unreachable(error)
|
|
28091
|
+
);
|
|
28092
|
+
}
|
|
28093
|
+
} else if (!projectId) {
|
|
28094
|
+
details.push("platform: not asked, this folder is not linked to a project");
|
|
28095
|
+
} else {
|
|
28096
|
+
details.push("platform: not asked, you are not logged in");
|
|
28097
|
+
}
|
|
28098
|
+
if (failed) {
|
|
28099
|
+
details.push(`error: ${failed.record?.error ?? "unknown"}`);
|
|
28100
|
+
state = "deploy-failed";
|
|
28101
|
+
}
|
|
28102
|
+
return { service: "plans", state, details };
|
|
28103
|
+
}
|
|
28104
|
+
|
|
28105
|
+
// src/cli/portal/report.ts
|
|
28106
|
+
var DEPLOY_ORDER = [
|
|
28107
|
+
"identity",
|
|
28108
|
+
"gateway",
|
|
28109
|
+
"plans",
|
|
28110
|
+
"portal"
|
|
28111
|
+
];
|
|
28112
|
+
var exists4 = (file) => access13(file).then(
|
|
28113
|
+
() => true,
|
|
28114
|
+
() => false
|
|
28115
|
+
);
|
|
28116
|
+
function sectionOf(resolved, service) {
|
|
28117
|
+
return resolved[service];
|
|
28118
|
+
}
|
|
28119
|
+
function chooseServices(only) {
|
|
28120
|
+
return DEPLOY_ORDER.filter((name) => !only || only.includes(name));
|
|
28121
|
+
}
|
|
28122
|
+
async function planPortalService(resolved, ledger) {
|
|
28123
|
+
const base = { service: "portal", details: [] };
|
|
28124
|
+
try {
|
|
28125
|
+
const manifest = await readManifest(resolved.state);
|
|
28126
|
+
const prepared = await preparePortal(resolved, manifest, "production", {
|
|
28127
|
+
write: false
|
|
28128
|
+
});
|
|
28129
|
+
const { profile, plan: plan2 } = prepared;
|
|
28130
|
+
const details = [
|
|
28131
|
+
`docs: ${!profile.docs.enabled ? "off" : profile.docs.provider === "external" ? `external (${profile.docs.url})` : "built by apitogo"}`,
|
|
28132
|
+
`console: ${profile.console.enabled ? "on" : "off"}`,
|
|
28133
|
+
`manifest: ${prepared.manifestSource === "file" ? "services.json" : "not needed"}`,
|
|
28134
|
+
...[
|
|
28135
|
+
plan2.auth ? "sign-in" : void 0,
|
|
28136
|
+
plan2.signup ? "sign-up" : void 0,
|
|
28137
|
+
plan2.userPanel ? "account pages" : void 0,
|
|
28138
|
+
plan2.plansPlugin ? "pricing, keys and usage pages" : void 0,
|
|
28139
|
+
plan2.apiReference ? "API reference" : void 0
|
|
28140
|
+
].filter((feature) => feature !== void 0).map((feature) => `will appear: ${feature}`),
|
|
28141
|
+
...plan2.skipped.map((s) => `left out: ${s.piece} (${s.reason})`)
|
|
28142
|
+
];
|
|
28143
|
+
const hash2 = await portalBuildHash(resolved, prepared);
|
|
28144
|
+
const previous = stepOf(ledger, "portal", "build");
|
|
28145
|
+
const built = await exists4(
|
|
28146
|
+
path83.join(resolved.state.buildDir, "index.html")
|
|
28147
|
+
);
|
|
28148
|
+
if (previous?.status === "succeeded" && previous.hash === hash2 && built) {
|
|
28149
|
+
return {
|
|
28150
|
+
...base,
|
|
28151
|
+
action: "skip",
|
|
28152
|
+
reason: "build skipped: nothing it depends on has changed",
|
|
28153
|
+
details,
|
|
28154
|
+
issues: []
|
|
28155
|
+
};
|
|
28156
|
+
}
|
|
28157
|
+
const reason = !previous ? "no earlier build" : previous.status === "failed" ? `the last build failed (${previous.error ?? "no message"})` : previous.status === "running" ? "the last build did not finish" : !built ? "the build folder is gone" : "its inputs changed since the last build";
|
|
28158
|
+
return {
|
|
28159
|
+
...base,
|
|
28160
|
+
action: "build",
|
|
28161
|
+
reason: `will build: ${reason}`,
|
|
28162
|
+
details,
|
|
28163
|
+
issues: []
|
|
28164
|
+
};
|
|
28165
|
+
} catch (error) {
|
|
27126
28166
|
if (!(error instanceof ConfigError)) throw error;
|
|
27127
28167
|
return {
|
|
27128
28168
|
...base,
|
|
@@ -27302,21 +28342,20 @@ async function planV2(dir, only, options = {}) {
|
|
|
27302
28342
|
services.push(
|
|
27303
28343
|
await planGatewayService(resolved, ledger, options.gatewayDryRun)
|
|
27304
28344
|
);
|
|
28345
|
+
} else if (service === "identity") {
|
|
28346
|
+
services.push(
|
|
28347
|
+
await planIdentityService(
|
|
28348
|
+
resolved,
|
|
28349
|
+
ledger,
|
|
28350
|
+
await readManifest(resolved.state)
|
|
28351
|
+
)
|
|
28352
|
+
);
|
|
27305
28353
|
} else {
|
|
27306
|
-
services.push(
|
|
27307
|
-
service,
|
|
27308
|
-
action: "not-deployable-yet",
|
|
27309
|
-
reason: "configured; deploying it is not available in this CLI version yet",
|
|
27310
|
-
details: [],
|
|
27311
|
-
issues: []
|
|
27312
|
-
});
|
|
28354
|
+
services.push(await planPlansService(resolved, ledger));
|
|
27313
28355
|
}
|
|
27314
28356
|
}
|
|
27315
28357
|
return { configPath: resolved.configPath, services };
|
|
27316
28358
|
}
|
|
27317
|
-
function entryStatus(manifest, service) {
|
|
27318
|
-
return manifest?.services[service];
|
|
27319
|
-
}
|
|
27320
28359
|
function originHealthLines(origin) {
|
|
27321
28360
|
if (!origin.lastProbeAtUtc) return ["origin check: not checked yet"];
|
|
27322
28361
|
return [
|
|
@@ -27390,6 +28429,16 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
|
|
|
27390
28429
|
if (failed) state = "deploy-failed";
|
|
27391
28430
|
return { service: "gateway", state, details: [...details, ...local] };
|
|
27392
28431
|
}
|
|
28432
|
+
async function wantedSkus(resolved) {
|
|
28433
|
+
if (!resolved.plans) return void 0;
|
|
28434
|
+
try {
|
|
28435
|
+
return (await readPlansFile(resolved.plans.file)).map(
|
|
28436
|
+
(plan2) => plan2.sku.trim().toLowerCase()
|
|
28437
|
+
);
|
|
28438
|
+
} catch {
|
|
28439
|
+
return void 0;
|
|
28440
|
+
}
|
|
28441
|
+
}
|
|
27393
28442
|
async function statusV2(dir, only, options = {}) {
|
|
27394
28443
|
const { resolved } = loadConfigV2(dir);
|
|
27395
28444
|
const ledger = await readLedger(resolved.state);
|
|
@@ -27405,7 +28454,6 @@ async function statusV2(dir, only, options = {}) {
|
|
|
27405
28454
|
services.push({ service, state: "disabled", details: [] });
|
|
27406
28455
|
continue;
|
|
27407
28456
|
}
|
|
27408
|
-
const entry = entryStatus(manifest, service);
|
|
27409
28457
|
const url2 = service === "portal" ? manifest?.services.portal?.url : void 0;
|
|
27410
28458
|
if (service === "portal") {
|
|
27411
28459
|
const build2 = stepOf(ledger, "portal", "build");
|
|
@@ -27434,14 +28482,26 @@ async function statusV2(dir, only, options = {}) {
|
|
|
27434
28482
|
);
|
|
27435
28483
|
continue;
|
|
27436
28484
|
}
|
|
27437
|
-
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
|
|
27441
|
-
|
|
27442
|
-
|
|
27443
|
-
|
|
27444
|
-
|
|
28485
|
+
if (service === "identity") {
|
|
28486
|
+
services.push(
|
|
28487
|
+
await identityStatus(
|
|
28488
|
+
resolved,
|
|
28489
|
+
ledger,
|
|
28490
|
+
manifest,
|
|
28491
|
+
options.identityRemote
|
|
28492
|
+
)
|
|
28493
|
+
);
|
|
28494
|
+
continue;
|
|
28495
|
+
}
|
|
28496
|
+
services.push(
|
|
28497
|
+
await plansStatus(
|
|
28498
|
+
resolved,
|
|
28499
|
+
ledger,
|
|
28500
|
+
manifest,
|
|
28501
|
+
options.plansRemote,
|
|
28502
|
+
await wantedSkus(resolved)
|
|
28503
|
+
)
|
|
28504
|
+
);
|
|
27445
28505
|
}
|
|
27446
28506
|
return {
|
|
27447
28507
|
configPath: resolved.configPath,
|
|
@@ -27458,7 +28518,7 @@ function formatPlan(output4) {
|
|
|
27458
28518
|
`${svc.service}: ${svc.reason}`,
|
|
27459
28519
|
...svc.details.map((line2) => ` ${line2}`),
|
|
27460
28520
|
...svc.issues.map(
|
|
27461
|
-
(
|
|
28521
|
+
(issue4) => ` problem: ${issue4.message}${issue4.hint ? ` ${issue4.hint}` : ""} [${issue4.code}]`
|
|
27462
28522
|
)
|
|
27463
28523
|
]);
|
|
27464
28524
|
}
|
|
@@ -27555,19 +28615,14 @@ async function deployV2(options, deps) {
|
|
|
27555
28615
|
results.push(await deployGatewayService(options, deps, projectId, named));
|
|
27556
28616
|
continue;
|
|
27557
28617
|
}
|
|
27558
|
-
if (service
|
|
27559
|
-
|
|
27560
|
-
|
|
27561
|
-
|
|
27562
|
-
|
|
27563
|
-
|
|
27564
|
-
|
|
27565
|
-
results.push(
|
|
27566
|
-
service,
|
|
27567
|
-
status: "skipped",
|
|
27568
|
-
reason: "deploying it is not available in this CLI version yet",
|
|
27569
|
-
steps: []
|
|
27570
|
-
});
|
|
28618
|
+
if (service === "identity") {
|
|
28619
|
+
results.push(
|
|
28620
|
+
await deployIdentityService(options, deps, projectId, named)
|
|
28621
|
+
);
|
|
28622
|
+
continue;
|
|
28623
|
+
}
|
|
28624
|
+
if (service === "plans") {
|
|
28625
|
+
results.push(await deployPlansService(options, deps, projectId, named));
|
|
27571
28626
|
continue;
|
|
27572
28627
|
}
|
|
27573
28628
|
results.push(await deployPortal(options, deps, projectId));
|
|
@@ -27611,6 +28666,65 @@ async function deployGatewayService(options, deps, projectId, named) {
|
|
|
27611
28666
|
operationCount: result.operationCount
|
|
27612
28667
|
};
|
|
27613
28668
|
}
|
|
28669
|
+
async function deployIdentityService(options, deps, projectId, named) {
|
|
28670
|
+
const { resolved } = loadConfigV2(options.dir);
|
|
28671
|
+
const why = identityNotDeployableReason(resolved);
|
|
28672
|
+
if (why) {
|
|
28673
|
+
if (named)
|
|
28674
|
+
throw new DeployError(
|
|
28675
|
+
"SERVICE_NOT_DEPLOYABLE",
|
|
28676
|
+
`Identity cannot be deployed: ${why}.`,
|
|
28677
|
+
"Fix the identity section of the config, or deploy another service."
|
|
28678
|
+
);
|
|
28679
|
+
return { service: "identity", status: "skipped", reason: why, steps: [] };
|
|
28680
|
+
}
|
|
28681
|
+
const client = deps.client;
|
|
28682
|
+
if (resolved.identity?.provider !== "oidc" && (!client.putProjectIdentity || !client.getProjectIdentity || !client.getProject))
|
|
28683
|
+
throw new Error("The platform client has no identity calls.");
|
|
28684
|
+
const result = await deployIdentity(
|
|
28685
|
+
resolved,
|
|
28686
|
+
projectId,
|
|
28687
|
+
{
|
|
28688
|
+
...deps.identity,
|
|
28689
|
+
client
|
|
28690
|
+
},
|
|
28691
|
+
{ force: options.force }
|
|
28692
|
+
);
|
|
28693
|
+
return {
|
|
28694
|
+
service: "identity",
|
|
28695
|
+
status: result.status,
|
|
28696
|
+
steps: result.steps,
|
|
28697
|
+
url: result.issuer
|
|
28698
|
+
};
|
|
28699
|
+
}
|
|
28700
|
+
async function deployPlansService(options, deps, projectId, named) {
|
|
28701
|
+
const { resolved } = loadConfigV2(options.dir);
|
|
28702
|
+
const why = plansNotDeployableReason(resolved);
|
|
28703
|
+
if (why) {
|
|
28704
|
+
if (named)
|
|
28705
|
+
throw new DeployError(
|
|
28706
|
+
"SERVICE_NOT_DEPLOYABLE",
|
|
28707
|
+
`Plans cannot be deployed: ${why}.`,
|
|
28708
|
+
"Add them with `apitogo add plans`, or deploy another service."
|
|
28709
|
+
);
|
|
28710
|
+
return { service: "plans", status: "skipped", reason: why, steps: [] };
|
|
28711
|
+
}
|
|
28712
|
+
const client = deps.client;
|
|
28713
|
+
if (!client.listGatewayPlans || !client.createGatewayPlan || !client.updateGatewayPlan || !client.validateGatewayPlans || !client.listGatewayActions || !client.getDevPortalBackend || !client.activateModule || !client.linkDevPortalIdentity || !client.listProvisioningJobs)
|
|
28714
|
+
throw new Error("The platform client has no plans calls.");
|
|
28715
|
+
const result = await deployPlans(
|
|
28716
|
+
resolved,
|
|
28717
|
+
projectId,
|
|
28718
|
+
{ ...deps.plans, client },
|
|
28719
|
+
{ force: options.force }
|
|
28720
|
+
);
|
|
28721
|
+
return {
|
|
28722
|
+
service: "plans",
|
|
28723
|
+
status: result.status,
|
|
28724
|
+
steps: result.steps,
|
|
28725
|
+
url: result.apiBaseUrl
|
|
28726
|
+
};
|
|
28727
|
+
}
|
|
27614
28728
|
async function deployPortal(options, deps, projectId) {
|
|
27615
28729
|
const { resolved, root, prepared } = await preparePortalWorkspace(
|
|
27616
28730
|
options.dir,
|
|
@@ -27874,11 +28988,11 @@ var export_default = {
|
|
|
27874
28988
|
import path90 from "node:path";
|
|
27875
28989
|
|
|
27876
28990
|
// src/cli/scaffold/run-scaffold-discovery.ts
|
|
27877
|
-
import { access as access15, readFile as
|
|
28991
|
+
import { access as access15, readFile as readFile31, writeFile as writeFile27 } from "node:fs/promises";
|
|
27878
28992
|
import path89 from "node:path";
|
|
27879
28993
|
|
|
27880
28994
|
// src/cli/scaffold/detect-backend.ts
|
|
27881
|
-
import { readdir as readdir4, readFile as
|
|
28995
|
+
import { readdir as readdir4, readFile as readFile29 } from "node:fs/promises";
|
|
27882
28996
|
import path87 from "node:path";
|
|
27883
28997
|
var DEFAULT_RUNTIME_VERSION = {
|
|
27884
28998
|
dotnet: "DOTNET_10",
|
|
@@ -27896,7 +29010,7 @@ async function listFiles(directory) {
|
|
|
27896
29010
|
}
|
|
27897
29011
|
async function readText(filePath) {
|
|
27898
29012
|
try {
|
|
27899
|
-
return await
|
|
29013
|
+
return await readFile29(filePath, "utf8");
|
|
27900
29014
|
} catch {
|
|
27901
29015
|
return void 0;
|
|
27902
29016
|
}
|
|
@@ -27982,7 +29096,7 @@ import {
|
|
|
27982
29096
|
lstat as lstat2,
|
|
27983
29097
|
mkdir as mkdir21,
|
|
27984
29098
|
readdir as readdir5,
|
|
27985
|
-
readFile as
|
|
29099
|
+
readFile as readFile30,
|
|
27986
29100
|
readlink,
|
|
27987
29101
|
symlink as symlink2,
|
|
27988
29102
|
writeFile as writeFile26
|
|
@@ -28213,7 +29327,7 @@ async function applyDocsNavigation(siteDirectory, routes) {
|
|
|
28213
29327
|
if (!await pathExists5(configPath)) {
|
|
28214
29328
|
return;
|
|
28215
29329
|
}
|
|
28216
|
-
const raw = await
|
|
29330
|
+
const raw = await readFile30(configPath, "utf8");
|
|
28217
29331
|
const parsed = JSON.parse(raw);
|
|
28218
29332
|
if (!isPlainObject5(parsed)) {
|
|
28219
29333
|
return;
|
|
@@ -28398,7 +29512,7 @@ async function pathExists6(filePath) {
|
|
|
28398
29512
|
async function wireOpenApiIntoJsonConfig(siteDirectory, openApiPath) {
|
|
28399
29513
|
const input5 = toSiteRelativeOpenApiInput(siteDirectory, openApiPath);
|
|
28400
29514
|
const jsonConfigPath = path89.join(siteDirectory, "apitogo.config.json");
|
|
28401
|
-
const raw = await
|
|
29515
|
+
const raw = await readFile31(jsonConfigPath, "utf8");
|
|
28402
29516
|
const siteConfig = JSON.parse(raw);
|
|
28403
29517
|
const updated = applyOpenApiToSiteConfig(siteConfig, {
|
|
28404
29518
|
input: input5,
|
|
@@ -28425,7 +29539,7 @@ async function discoverAndWireOpenApiLite(siteDirectory, mode) {
|
|
|
28425
29539
|
const invalidCandidates = [];
|
|
28426
29540
|
let selected;
|
|
28427
29541
|
for (const candidate of search.candidates) {
|
|
28428
|
-
const content = await
|
|
29542
|
+
const content = await readFile31(candidate, "utf8");
|
|
28429
29543
|
const parsed = liteParseOpenApi(content, candidate);
|
|
28430
29544
|
if (parsed.valid) {
|
|
28431
29545
|
selected = {
|
|
@@ -28505,7 +29619,7 @@ async function discoverAndWireBackend(siteDirectory, mode) {
|
|
|
28505
29619
|
}
|
|
28506
29620
|
try {
|
|
28507
29621
|
const siteConfig = JSON.parse(
|
|
28508
|
-
await
|
|
29622
|
+
await readFile31(jsonConfigPath, "utf8")
|
|
28509
29623
|
);
|
|
28510
29624
|
const existing = siteConfig.backend && typeof siteConfig.backend === "object" && !Array.isArray(siteConfig.backend) ? siteConfig.backend : {};
|
|
28511
29625
|
if (typeof existing.sourceDirectory === "string" && existing.sourceDirectory.trim()) {
|
|
@@ -29011,10 +30125,35 @@ function gatewayDryRun() {
|
|
|
29011
30125
|
return void 0;
|
|
29012
30126
|
}
|
|
29013
30127
|
}
|
|
30128
|
+
function identityRemote() {
|
|
30129
|
+
try {
|
|
30130
|
+
const client = new PlatformApiClient(loadConfig());
|
|
30131
|
+
return (projectId) => client.getProjectIdentity(projectId);
|
|
30132
|
+
} catch {
|
|
30133
|
+
return void 0;
|
|
30134
|
+
}
|
|
30135
|
+
}
|
|
30136
|
+
function plansRemote() {
|
|
30137
|
+
try {
|
|
30138
|
+
const client = new PlatformApiClient(loadConfig());
|
|
30139
|
+
return async (projectId) => ({
|
|
30140
|
+
plans: await client.listGatewayPlans(projectId),
|
|
30141
|
+
backendAddress: toApiBaseUrl(
|
|
30142
|
+
(await client.getDevPortalBackend(projectId))?.fqdn
|
|
30143
|
+
)
|
|
30144
|
+
});
|
|
30145
|
+
} catch {
|
|
30146
|
+
return void 0;
|
|
30147
|
+
}
|
|
30148
|
+
}
|
|
29014
30149
|
var statusOfProject = (argv) => run2(
|
|
29015
30150
|
"status",
|
|
29016
30151
|
argv,
|
|
29017
|
-
(dir, only) => statusV2(dir, only, {
|
|
30152
|
+
(dir, only) => statusV2(dir, only, {
|
|
30153
|
+
gatewayRemote: gatewayRemote(),
|
|
30154
|
+
identityRemote: identityRemote(),
|
|
30155
|
+
plansRemote: plansRemote()
|
|
30156
|
+
}),
|
|
29018
30157
|
formatStatus
|
|
29019
30158
|
);
|
|
29020
30159
|
|
|
@@ -29061,13 +30200,7 @@ function modulesForConfig(config2) {
|
|
|
29061
30200
|
var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || config2.gateway && config2.gateway.enabled !== false;
|
|
29062
30201
|
async function createProjectV2(options, deps) {
|
|
29063
30202
|
const { config: config2, resolved } = loadConfigV2(options.dir);
|
|
29064
|
-
|
|
29065
|
-
throw new ProjectCreateError(
|
|
29066
|
-
"ALREADY_LINKED",
|
|
29067
|
-
`This folder is already linked to project ${resolved.project.id}.`,
|
|
29068
|
-
"Run `apitogo deploy`. To start over, run `apitogo teardown` first."
|
|
29069
|
-
);
|
|
29070
|
-
}
|
|
30203
|
+
const linkedProjectId = resolved.project.id;
|
|
29071
30204
|
const modules = modulesForConfig(config2);
|
|
29072
30205
|
if (!hostsAnything(config2)) {
|
|
29073
30206
|
throw new ProjectCreateError(
|
|
@@ -29081,7 +30214,9 @@ async function createProjectV2(options, deps) {
|
|
|
29081
30214
|
name,
|
|
29082
30215
|
description: options.description,
|
|
29083
30216
|
modules,
|
|
29084
|
-
wait: options.wait !== false
|
|
30217
|
+
wait: options.wait !== false,
|
|
30218
|
+
projectId: linkedProjectId,
|
|
30219
|
+
onProjectResolved: (projectId) => setProjectId(options.dir, projectId)
|
|
29085
30220
|
});
|
|
29086
30221
|
const outcome = parse4(raw);
|
|
29087
30222
|
if (outcome.requiresPlatformPlanSelection) {
|
|
@@ -29154,7 +30289,9 @@ async function createLinkedProject(argv, dir) {
|
|
|
29154
30289
|
modules: args.modules,
|
|
29155
30290
|
selectFreePlan: true,
|
|
29156
30291
|
wait: args.wait,
|
|
29157
|
-
configureGatewayBaseline: false
|
|
30292
|
+
configureGatewayBaseline: false,
|
|
30293
|
+
projectId: args.projectId,
|
|
30294
|
+
onProjectResolved: args.onProjectResolved
|
|
29158
30295
|
},
|
|
29159
30296
|
json ? void 0 : progress
|
|
29160
30297
|
)
|
|
@@ -30082,12 +31219,14 @@ var TeardownError = class extends ApitogoError {
|
|
|
30082
31219
|
};
|
|
30083
31220
|
async function teardownV2(options, deps) {
|
|
30084
31221
|
if (options.only) {
|
|
30085
|
-
|
|
30086
|
-
|
|
31222
|
+
const only = options.only.trim();
|
|
31223
|
+
if (only === "gateway") return teardownGateway(options, deps);
|
|
31224
|
+
if (only === "identity") return teardownIdentity(options, deps);
|
|
31225
|
+
if (only === "plans") return teardownPlans(options, deps);
|
|
30087
31226
|
throw new TeardownError(
|
|
30088
31227
|
"PARTIAL_TEARDOWN_UNSUPPORTED",
|
|
30089
31228
|
"The platform cannot release the hosting of one service on its own: it is released by deleting the project.",
|
|
30090
|
-
"Run `apitogo teardown` without --only to delete the whole project, or `apitogo teardown --only gateway` to remove just
|
|
31229
|
+
"Run `apitogo teardown` without --only to delete the whole project, or `apitogo teardown --only gateway`, `--only identity` or `--only plans` to remove just that service. Your local files are kept."
|
|
30091
31230
|
);
|
|
30092
31231
|
}
|
|
30093
31232
|
const { resolved } = loadConfigV2(options.dir);
|
|
@@ -30267,6 +31406,148 @@ Type the project name to confirm: `
|
|
|
30267
31406
|
message: "The gateway was removed. The project and your local files are kept."
|
|
30268
31407
|
};
|
|
30269
31408
|
}
|
|
31409
|
+
function projectIdOf(options, deps, resolved, what) {
|
|
31410
|
+
const projectId = options.projectId?.trim() || resolved.project.id || deps.envProjectId?.trim() || "";
|
|
31411
|
+
if (!projectId) {
|
|
31412
|
+
throw new TeardownError(
|
|
31413
|
+
"PROJECT_REQUIRED",
|
|
31414
|
+
`This folder is not linked to a project, so there is no ${what} to remove.`,
|
|
31415
|
+
"Pass --project-id <id> to name the project."
|
|
31416
|
+
);
|
|
31417
|
+
}
|
|
31418
|
+
return projectId;
|
|
31419
|
+
}
|
|
31420
|
+
async function confirmByName(options, deps, name, prompt, what) {
|
|
31421
|
+
if (options.yes === true) return;
|
|
31422
|
+
if (!deps.isTerminal) {
|
|
31423
|
+
throw new TeardownError(
|
|
31424
|
+
"CONFIRMATION_REQUIRED",
|
|
31425
|
+
"This is not an interactive terminal.",
|
|
31426
|
+
`Pass --yes to remove the ${what} without asking.`
|
|
31427
|
+
);
|
|
31428
|
+
}
|
|
31429
|
+
const typed = await deps.askName(prompt);
|
|
31430
|
+
if (typed.trim() !== name) {
|
|
31431
|
+
throw new TeardownError(
|
|
31432
|
+
"CANCELLED",
|
|
31433
|
+
"The name did not match. Nothing was removed."
|
|
31434
|
+
);
|
|
31435
|
+
}
|
|
31436
|
+
}
|
|
31437
|
+
async function clearService(resolved, service) {
|
|
31438
|
+
const cleared = [];
|
|
31439
|
+
const ledger = await readLedger(resolved.state);
|
|
31440
|
+
if (ledger.services[service]) {
|
|
31441
|
+
await clearServices(resolved.state, [service]);
|
|
31442
|
+
cleared.push(`state.json entries of ${service}`);
|
|
31443
|
+
}
|
|
31444
|
+
if ((await readManifest(resolved.state))?.services[service]) {
|
|
31445
|
+
await removeServicesFromManifest(resolved.state, [service]);
|
|
31446
|
+
cleared.push(`the ${service} entry of services.json`);
|
|
31447
|
+
}
|
|
31448
|
+
return cleared;
|
|
31449
|
+
}
|
|
31450
|
+
async function teardownIdentity(options, deps) {
|
|
31451
|
+
const { resolved } = loadConfigV2(options.dir);
|
|
31452
|
+
const projectId = projectIdOf(options, deps, resolved, "identity");
|
|
31453
|
+
const client = deps.client;
|
|
31454
|
+
if (!client.getProjectIdentity || !client.deleteProjectIdentity)
|
|
31455
|
+
throw new Error("The platform client has no identity calls.");
|
|
31456
|
+
const project = await client.getProject(projectId);
|
|
31457
|
+
const name = project.name;
|
|
31458
|
+
const current = await client.getProjectIdentity(projectId);
|
|
31459
|
+
if (!current || current.status === "Released")
|
|
31460
|
+
throw new TeardownError(
|
|
31461
|
+
"IDENTITY_NOT_DEPLOYED",
|
|
31462
|
+
"The platform has no identity for this project.",
|
|
31463
|
+
"There is nothing to remove. Clear stale local records by deleting the identity entries in the state folder."
|
|
31464
|
+
);
|
|
31465
|
+
await confirmByName(
|
|
31466
|
+
options,
|
|
31467
|
+
deps,
|
|
31468
|
+
name,
|
|
31469
|
+
`This removes the identity of project "${name}" (${projectId}) and DELETES EVERY USER in it, for good. Nobody can sign in to the portal or the API until a new identity is made. The project, the portal and the gateway stay.
|
|
31470
|
+
Type the project name to confirm: `,
|
|
31471
|
+
"identity"
|
|
31472
|
+
);
|
|
31473
|
+
const answer = await client.deleteProjectIdentity(projectId);
|
|
31474
|
+
if (answer !== null) {
|
|
31475
|
+
const sleep5 = deps.identity?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
31476
|
+
const deadline = Date.now() + (options.wait === false ? 0 : deps.identity?.timeoutMs ?? 12e4);
|
|
31477
|
+
for (; ; ) {
|
|
31478
|
+
const state = await client.getProjectIdentity(projectId);
|
|
31479
|
+
if (state === null || state.status === "Released") break;
|
|
31480
|
+
if (state.status === "Failed")
|
|
31481
|
+
throw new TeardownError(
|
|
31482
|
+
"DELETE_FAILED",
|
|
31483
|
+
`The platform could not remove the identity: ${[state.errorCode, state.errorMessage].filter(Boolean).join(" ") || state.status}.`,
|
|
31484
|
+
"Nothing local was changed. Try again, or contact support with the project id."
|
|
31485
|
+
);
|
|
31486
|
+
if (Date.now() >= deadline) {
|
|
31487
|
+
return {
|
|
31488
|
+
projectId,
|
|
31489
|
+
projectName: name,
|
|
31490
|
+
status: "pending",
|
|
31491
|
+
cleared: [],
|
|
31492
|
+
message: "The identity is queued for removal and the platform finishes it in the background. Local records are kept until then: run `apitogo teardown --only identity` again later to clear them."
|
|
31493
|
+
};
|
|
31494
|
+
}
|
|
31495
|
+
await sleep5(deps.identity?.pollIntervalMs ?? 2e3);
|
|
31496
|
+
}
|
|
31497
|
+
}
|
|
31498
|
+
return {
|
|
31499
|
+
projectId,
|
|
31500
|
+
projectName: name,
|
|
31501
|
+
status: "removed",
|
|
31502
|
+
scope: "identity",
|
|
31503
|
+
cleared: await clearService(resolved, "identity"),
|
|
31504
|
+
message: "The identity and its users were removed. The project and your local files are kept."
|
|
31505
|
+
};
|
|
31506
|
+
}
|
|
31507
|
+
async function teardownPlans(options, deps) {
|
|
31508
|
+
const { resolved } = loadConfigV2(options.dir);
|
|
31509
|
+
const projectId = projectIdOf(options, deps, resolved, "plans");
|
|
31510
|
+
const client = deps.client;
|
|
31511
|
+
if (!client.listGatewayPlans || !client.deleteGatewayPlan)
|
|
31512
|
+
throw new Error("The platform client has no plans calls.");
|
|
31513
|
+
const project = await client.getProject(projectId);
|
|
31514
|
+
const name = project.name;
|
|
31515
|
+
const plans = await client.listGatewayPlans(projectId);
|
|
31516
|
+
if (plans.length === 0)
|
|
31517
|
+
throw new TeardownError(
|
|
31518
|
+
"PLANS_NOT_DEPLOYED",
|
|
31519
|
+
"The platform has no plans for this project.",
|
|
31520
|
+
"There is nothing to remove. Clear stale local records by deleting the plans entries in the state folder."
|
|
31521
|
+
);
|
|
31522
|
+
await confirmByName(
|
|
31523
|
+
options,
|
|
31524
|
+
deps,
|
|
31525
|
+
name,
|
|
31526
|
+
`This removes the plans (${plans.map((plan2) => plan2.sku).join(", ")}) of project "${name}" (${projectId}). Customers' subscriptions and API keys on them stop working. The plans backend stays: it is released with the project.
|
|
31527
|
+
Type the project name to confirm: `,
|
|
31528
|
+
"plans"
|
|
31529
|
+
);
|
|
31530
|
+
for (const plan2 of plans) {
|
|
31531
|
+
try {
|
|
31532
|
+
await client.deleteGatewayPlan(projectId, plan2.id);
|
|
31533
|
+
} catch (error) {
|
|
31534
|
+
if (error instanceof PlatformApiError && error.status === 404) continue;
|
|
31535
|
+
throw new TeardownError(
|
|
31536
|
+
"DELETE_FAILED",
|
|
31537
|
+
`The platform could not remove the plan "${plan2.sku}": ${error instanceof Error ? error.message : String(error)}.`,
|
|
31538
|
+
"Plans removed before it are gone. Nothing local was changed; run the command again after fixing the cause."
|
|
31539
|
+
);
|
|
31540
|
+
}
|
|
31541
|
+
}
|
|
31542
|
+
return {
|
|
31543
|
+
projectId,
|
|
31544
|
+
projectName: name,
|
|
31545
|
+
status: "removed",
|
|
31546
|
+
scope: "plans",
|
|
31547
|
+
cleared: await clearService(resolved, "plans"),
|
|
31548
|
+
message: "The plans were removed. The plans backend, the project and your local files are kept."
|
|
31549
|
+
};
|
|
31550
|
+
}
|
|
30270
31551
|
|
|
30271
31552
|
// src/cli/teardown/command.ts
|
|
30272
31553
|
async function ask3(prompt) {
|
|
@@ -30316,7 +31597,7 @@ async function teardownProject(argv) {
|
|
|
30316
31597
|
return;
|
|
30317
31598
|
}
|
|
30318
31599
|
printDiagnosticsToConsole(
|
|
30319
|
-
result.scope === "gateway" ? `Removed the
|
|
31600
|
+
result.scope === "gateway" || result.scope === "identity" || result.scope === "plans" ? `Removed the ${result.scope} of project ${result.projectName} (${result.projectId}).` : `${result.status === "removed" ? "Deleted" : "Queued deletion of"} project ${result.projectName} (${result.projectId}).`
|
|
30320
31601
|
);
|
|
30321
31602
|
if (result.cleared.length > 0)
|
|
30322
31603
|
printDiagnosticsToConsole(
|
|
@@ -30421,7 +31702,7 @@ var teardown_default = {
|
|
|
30421
31702
|
describe: "Override project GUID"
|
|
30422
31703
|
}).option("only", {
|
|
30423
31704
|
type: "string",
|
|
30424
|
-
describe: "Config version 2:
|
|
31705
|
+
describe: "Config version 2: `gateway`, `identity` or `plans` removes just that service and keeps the project (`identity` deletes every user in it); other hosting is released only by deleting the whole project"
|
|
30425
31706
|
}).option("yes", {
|
|
30426
31707
|
alias: "y",
|
|
30427
31708
|
type: "boolean",
|