@lukoweb/apitogo 0.2.38 → 0.2.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.js
CHANGED
|
@@ -6596,7 +6596,7 @@ function dependencyRules(config2, resolved, out) {
|
|
|
6596
6596
|
const identityOn = resolved.identity?.enabled === true;
|
|
6597
6597
|
const oidcGateway = gatewayOn && resolved.gateway?.auth?.mode === "oidc";
|
|
6598
6598
|
const hasIdentitySource = identityOn || oidcGateway;
|
|
6599
|
-
if (identityOn && resolved.portal?.enabled !== true && !gatewayOn) {
|
|
6599
|
+
if (identityOn && resolved.portal?.enabled !== true && !gatewayOn && !resolved.project.id) {
|
|
6600
6600
|
out.error({
|
|
6601
6601
|
code: "NOTHING_TO_HOST",
|
|
6602
6602
|
path: "identity",
|
|
@@ -7932,7 +7932,11 @@ function planPortal(profile, manifest) {
|
|
|
7932
7932
|
plan2.plansPlugin = {
|
|
7933
7933
|
apiBaseUrl: plans.apiBaseUrl,
|
|
7934
7934
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
7935
|
-
billing: plans.billing
|
|
7935
|
+
billing: plans.billing,
|
|
7936
|
+
links: {
|
|
7937
|
+
...profile.modules.docs ? { docs: "/introduction" } : profile.docs.url ? { docs: profile.docs.url } : {},
|
|
7938
|
+
...plan2.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {}
|
|
7939
|
+
}
|
|
7936
7940
|
};
|
|
7937
7941
|
}
|
|
7938
7942
|
return plan2;
|
|
@@ -8634,7 +8638,7 @@ async function buildPlans(context) {
|
|
|
8634
8638
|
const { options, dir, resolved } = context;
|
|
8635
8639
|
const o = options.plans ?? {};
|
|
8636
8640
|
const template = o.template ?? (options.template === "blank" ? "none" : options.template === "starter" ? "free" : "free");
|
|
8637
|
-
const billing = o.billing ?? "stripe";
|
|
8641
|
+
const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
|
|
8638
8642
|
if (template === "tiered" && billing === "none") {
|
|
8639
8643
|
throw new ServiceEditError(
|
|
8640
8644
|
"The tiered template has paid plans, which need a billing provider.",
|
|
@@ -9731,6 +9735,16 @@ var PlatformApiClient = class {
|
|
|
9731
9735
|
`/api/v1/projects/${projectId}/gateway/plans/${planId}`
|
|
9732
9736
|
);
|
|
9733
9737
|
}
|
|
9738
|
+
/**
|
|
9739
|
+
* For test runs only: puts the project's plans backend back to empty (accounts, subscriptions, keys, cached catalogs).
|
|
9740
|
+
* Answers 404 unless the platform has the test tools on and the project is a `tester-` project.
|
|
9741
|
+
*/
|
|
9742
|
+
resetDevPortalTestData(projectId) {
|
|
9743
|
+
return this.request(
|
|
9744
|
+
"POST",
|
|
9745
|
+
`/api/v1/projects/${projectId}/dev-portal/test-reset`
|
|
9746
|
+
);
|
|
9747
|
+
}
|
|
9734
9748
|
/**
|
|
9735
9749
|
* For test runs only: a test user and a one-time sign-in link for it. The platform answers 404 unless it allows test
|
|
9736
9750
|
* tools and the project is a `tester-` project.
|
|
@@ -10073,7 +10087,7 @@ import {
|
|
|
10073
10087
|
// package.json
|
|
10074
10088
|
var package_default = {
|
|
10075
10089
|
name: "@lukoweb/apitogo",
|
|
10076
|
-
version: "0.2.
|
|
10090
|
+
version: "0.2.40",
|
|
10077
10091
|
type: "module",
|
|
10078
10092
|
sideEffects: [
|
|
10079
10093
|
"**/*.css",
|
|
@@ -15095,6 +15109,14 @@ function capabilityIssues(gateway, answer) {
|
|
|
15095
15109
|
);
|
|
15096
15110
|
return found;
|
|
15097
15111
|
}
|
|
15112
|
+
function pendingIdentityNote(item, state) {
|
|
15113
|
+
if (item.code !== "GATEWAY_AUTH_INCOMPLETE" || !state.identityEnabled || state.identityDeployed)
|
|
15114
|
+
return void 0;
|
|
15115
|
+
return {
|
|
15116
|
+
...item,
|
|
15117
|
+
message: `${item.message} The identity is made by the first deploy, before the gateway, so this is expected until then.`
|
|
15118
|
+
};
|
|
15119
|
+
}
|
|
15098
15120
|
async function onlineGatewayRules(gateway, projectId, api, derivedCors = []) {
|
|
15099
15121
|
let spec;
|
|
15100
15122
|
const specFile = gateway.specs[0];
|
|
@@ -15413,7 +15435,12 @@ async function addGatewayOnlineRules(report, resolved, args, deps) {
|
|
|
15413
15435
|
const key = `${item.code}|${item.path}`;
|
|
15414
15436
|
if (seen.has(key)) continue;
|
|
15415
15437
|
seen.add(key);
|
|
15416
|
-
|
|
15438
|
+
const pending = pendingIdentityNote(item, {
|
|
15439
|
+
identityEnabled: resolved.identity?.enabled === true,
|
|
15440
|
+
identityDeployed: manifest?.services.identity !== void 0
|
|
15441
|
+
});
|
|
15442
|
+
if (pending) report.warnings.push(pending);
|
|
15443
|
+
else report.errors.push(item);
|
|
15417
15444
|
}
|
|
15418
15445
|
return true;
|
|
15419
15446
|
}
|
|
@@ -26983,6 +27010,18 @@ function describePlatformProblem(error) {
|
|
|
26983
27010
|
)
|
|
26984
27011
|
] : [];
|
|
26985
27012
|
}
|
|
27013
|
+
async function withServerRetry(request, sleep5, waitsMs = [5e3, 15e3]) {
|
|
27014
|
+
for (const wait of waitsMs) {
|
|
27015
|
+
try {
|
|
27016
|
+
return await request();
|
|
27017
|
+
} catch (error) {
|
|
27018
|
+
if (!(error instanceof PlatformApiError) || error.status < 500)
|
|
27019
|
+
throw error;
|
|
27020
|
+
await sleep5(wait);
|
|
27021
|
+
}
|
|
27022
|
+
}
|
|
27023
|
+
return request();
|
|
27024
|
+
}
|
|
26986
27025
|
function rejected(what, error) {
|
|
26987
27026
|
if (error instanceof PlatformApiError && error.status === 400) {
|
|
26988
27027
|
const lines = describePlatformProblem(error);
|
|
@@ -27002,7 +27041,7 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27002
27041
|
const client = deps.client;
|
|
27003
27042
|
const sleep5 = deps.sleep ?? defaultSleep;
|
|
27004
27043
|
const now = deps.now ?? Date.now;
|
|
27005
|
-
const timeoutMs = deps.reconcileTimeoutMs ??
|
|
27044
|
+
const timeoutMs = deps.reconcileTimeoutMs ?? 42e4;
|
|
27006
27045
|
const backendTimeoutMs = deps.backendTimeoutMs ?? 20 * 6e4;
|
|
27007
27046
|
const intervalMs = deps.pollIntervalMs ?? 2e3;
|
|
27008
27047
|
const backendStep = {
|
|
@@ -27070,7 +27109,10 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27070
27109
|
hash: () => specHash,
|
|
27071
27110
|
run: async () => {
|
|
27072
27111
|
try {
|
|
27073
|
-
const uploaded = await
|
|
27112
|
+
const uploaded = await withServerRetry(
|
|
27113
|
+
() => client.putGatewaySpec(projectId, spec),
|
|
27114
|
+
sleep5
|
|
27115
|
+
);
|
|
27074
27116
|
return {
|
|
27075
27117
|
specSha256: uploaded.sha256,
|
|
27076
27118
|
operationCount: String(uploaded.operationCount)
|
|
@@ -27086,7 +27128,10 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27086
27128
|
hash: () => configHash,
|
|
27087
27129
|
run: async () => {
|
|
27088
27130
|
try {
|
|
27089
|
-
const accepted = await
|
|
27131
|
+
const accepted = await withServerRetry(
|
|
27132
|
+
() => client.putGatewayConfig(projectId, body),
|
|
27133
|
+
sleep5
|
|
27134
|
+
);
|
|
27090
27135
|
return {
|
|
27091
27136
|
reconciliationId: accepted.reconciliationId,
|
|
27092
27137
|
desiredHash: accepted.desiredHash
|
|
@@ -27095,13 +27140,17 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27095
27140
|
return rejected("gateway config", error);
|
|
27096
27141
|
}
|
|
27097
27142
|
},
|
|
27098
|
-
// A reconciliation the platform gave up on is not a result worth keeping
|
|
27143
|
+
// A reconciliation the platform gave up on is not a result worth keeping, and neither is one for a gateway that was
|
|
27144
|
+
// removed since (a reset, a teardown, or the web app): the records here would say "deployed" for a gateway that is gone.
|
|
27099
27145
|
stillValid: async () => {
|
|
27100
27146
|
const ticket = await ticketOf();
|
|
27101
27147
|
if (!ticket) return false;
|
|
27102
27148
|
try {
|
|
27103
27149
|
const state = await client.getGatewayReconciliation(projectId, ticket);
|
|
27104
|
-
|
|
27150
|
+
if (state.state === "Failed" || state.state === "Superseded")
|
|
27151
|
+
return false;
|
|
27152
|
+
const gateway2 = await client.getGatewayStatus(projectId);
|
|
27153
|
+
return gateway2.status !== "Released";
|
|
27105
27154
|
} catch {
|
|
27106
27155
|
return false;
|
|
27107
27156
|
}
|
|
@@ -27132,7 +27181,7 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
|
27132
27181
|
throw new GatewayDeployError(
|
|
27133
27182
|
"GATEWAY_RECONCILE_TIMEOUT",
|
|
27134
27183
|
`The platform did not finish within ${Math.round(timeoutMs / 1e3)} seconds (state: ${state.state}).`,
|
|
27135
|
-
"
|
|
27184
|
+
"The gateway is still being made: run deploy again to keep waiting (a gateway made just after a reset takes the longest). If it stays Pending for many minutes, the platform worker that applies gateway configs may not be running."
|
|
27136
27185
|
);
|
|
27137
27186
|
await sleep5(intervalMs);
|
|
27138
27187
|
}
|
|
@@ -27263,6 +27312,14 @@ function referencedSecretNames(connections) {
|
|
|
27263
27312
|
)
|
|
27264
27313
|
];
|
|
27265
27314
|
}
|
|
27315
|
+
function identityProviderSwitch(resolved, known) {
|
|
27316
|
+
if (!known?.provider || !resolved.identity) return void 0;
|
|
27317
|
+
const to = resolved.identity.provider ?? "apitogo";
|
|
27318
|
+
return known.provider === to ? void 0 : { from: known.provider, to };
|
|
27319
|
+
}
|
|
27320
|
+
function identityProviderSwitchWarning(from, to) {
|
|
27321
|
+
return `warning: the identity provider changes from ${from} to ${to}. People are known by issuer and subject, so the users, subscriptions and keys made with ${from} are not carried over to ${to}.`;
|
|
27322
|
+
}
|
|
27266
27323
|
var PORTAL_CALLBACK_PATH = "/oauth/callback";
|
|
27267
27324
|
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27268
27325
|
function identityNotDeployableReason(resolved) {
|
|
@@ -27941,6 +27998,8 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27941
27998
|
details: []
|
|
27942
27999
|
};
|
|
27943
28000
|
const known = manifest?.services.identity;
|
|
28001
|
+
const switched = identityProviderSwitch(resolved, known);
|
|
28002
|
+
const switchNote = switched ? [identityProviderSwitchWarning(switched.from, switched.to)] : [];
|
|
27944
28003
|
if (identity.provider === "oidc") {
|
|
27945
28004
|
const same = known?.provider === "oidc" && known.issuer === identity.issuer?.replace(/\/+$/, "") && known.clientId === identity.clientId;
|
|
27946
28005
|
return {
|
|
@@ -27948,6 +28007,7 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27948
28007
|
action: same ? "skip" : "deploy",
|
|
27949
28008
|
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)",
|
|
27950
28009
|
details: [
|
|
28010
|
+
...switchNote,
|
|
27951
28011
|
`provider: oidc ${identity.issuer}`,
|
|
27952
28012
|
`client id: ${identity.clientId}`
|
|
27953
28013
|
]
|
|
@@ -27959,6 +28019,7 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27959
28019
|
const body = portalUrl ? identityRequestBody(resolved, portalUrl) : recorded ? identityBodyForRedirect(resolved, recorded) : void 0;
|
|
27960
28020
|
const hash2 = projectId && body ? identityInputsHash(projectId, body) : void 0;
|
|
27961
28021
|
const details = [
|
|
28022
|
+
...switchNote,
|
|
27962
28023
|
"provider: apitogo (one issuer for this project)",
|
|
27963
28024
|
`sign-up with email and password: ${identity.native ? "on" : "off"}`,
|
|
27964
28025
|
portalUrl ? `sign-in redirect: ${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}` : recorded ? `sign-in redirect: ${recorded} (sent by the last deploy)` : "sign-in redirect: the portal address is not known locally; deploy takes it from the project",
|
|
@@ -28566,6 +28627,20 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
|
|
|
28566
28627
|
if (failed) state = "deploy-failed";
|
|
28567
28628
|
return { service: "gateway", state, details: [...details, ...local] };
|
|
28568
28629
|
}
|
|
28630
|
+
async function hostingLine(resolved, manifest, remote) {
|
|
28631
|
+
const projectId = resolved.project.id ?? manifest?.project.id;
|
|
28632
|
+
if (!remote || !projectId) return [];
|
|
28633
|
+
try {
|
|
28634
|
+
const address = await remote(projectId);
|
|
28635
|
+
return address ? [`hosting: ready (${address})`] : [
|
|
28636
|
+
"hosting: not ready yet (the platform is still preparing it; wait a few minutes, then run `apitogo deploy`)"
|
|
28637
|
+
];
|
|
28638
|
+
} catch (error) {
|
|
28639
|
+
return [
|
|
28640
|
+
`hosting: not reachable (${error instanceof Error ? error.message : String(error)})`
|
|
28641
|
+
];
|
|
28642
|
+
}
|
|
28643
|
+
}
|
|
28569
28644
|
async function wantedSkus(resolved) {
|
|
28570
28645
|
if (!resolved.plans) return void 0;
|
|
28571
28646
|
try {
|
|
@@ -28608,7 +28683,8 @@ async function statusV2(dir, only, options = {}) {
|
|
|
28608
28683
|
...state === "built" ? [
|
|
28609
28684
|
`output: ${path82.relative(resolved.configDir, resolved.state.buildDir)}`
|
|
28610
28685
|
] : [],
|
|
28611
|
-
...url2 ? [`url: ${url2}`] : ["url: not known (portal is not deployed yet)"]
|
|
28686
|
+
...url2 ? [`url: ${url2}`] : ["url: not known (portal is not deployed yet)"],
|
|
28687
|
+
...await hostingLine(resolved, manifest, options.hostingRemote)
|
|
28612
28688
|
]
|
|
28613
28689
|
});
|
|
28614
28690
|
continue;
|
|
@@ -28886,6 +28962,16 @@ async function deployIdentityService(options, deps, projectId, named) {
|
|
|
28886
28962
|
);
|
|
28887
28963
|
return { service: "identity", status: "skipped", reason: why, steps: [] };
|
|
28888
28964
|
}
|
|
28965
|
+
const switched = identityProviderSwitch(
|
|
28966
|
+
resolved,
|
|
28967
|
+
(await readManifest(resolved.state))?.services.identity
|
|
28968
|
+
);
|
|
28969
|
+
if (switched && options.yes !== true)
|
|
28970
|
+
throw new DeployError(
|
|
28971
|
+
"CONFIRMATION_REQUIRED",
|
|
28972
|
+
identityProviderSwitchWarning(switched.from, switched.to),
|
|
28973
|
+
"`apitogo plan` shows it too. Run `apitogo deploy --yes` to go on."
|
|
28974
|
+
);
|
|
28889
28975
|
const client = deps.client;
|
|
28890
28976
|
if (resolved.identity?.provider !== "oidc" && (!client.putProjectIdentity || !client.getProjectIdentity || !client.getProject))
|
|
28891
28977
|
throw new Error("The platform client has no identity calls.");
|
|
@@ -28937,7 +29023,26 @@ async function deployPlansService(options, deps, projectId, named) {
|
|
|
28937
29023
|
...result.notes ? { notes: result.notes } : {}
|
|
28938
29024
|
};
|
|
28939
29025
|
}
|
|
29026
|
+
async function recordPortalAddressBeforeBuild(dir, deps, projectId) {
|
|
29027
|
+
const { resolved } = loadConfigV2(dir);
|
|
29028
|
+
if (!resolved.portal?.enabled) return;
|
|
29029
|
+
const manifest = await readManifest(resolved.state);
|
|
29030
|
+
if (manifest?.services.portal?.url) return;
|
|
29031
|
+
const url2 = resolveSwaUrl(await deps.client.getProject(projectId));
|
|
29032
|
+
if (!url2) return;
|
|
29033
|
+
const profile = portalProfile(resolved);
|
|
29034
|
+
await writeManifest(
|
|
29035
|
+
resolved.state,
|
|
29036
|
+
withPortalEntry(manifest, {
|
|
29037
|
+
projectId,
|
|
29038
|
+
projectName: resolved.project.name,
|
|
29039
|
+
url: url2,
|
|
29040
|
+
docs: { provider: profile.docs.provider, url: profile.docs.url }
|
|
29041
|
+
})
|
|
29042
|
+
);
|
|
29043
|
+
}
|
|
28940
29044
|
async function deployPortal(options, deps, projectId) {
|
|
29045
|
+
await recordPortalAddressBeforeBuild(options.dir, deps, projectId);
|
|
28941
29046
|
const { resolved, root, prepared } = await preparePortalWorkspace(
|
|
28942
29047
|
options.dir,
|
|
28943
29048
|
"production"
|
|
@@ -29021,6 +29126,7 @@ async function deployProject(argv) {
|
|
|
29021
29126
|
projectId: argv.projectId,
|
|
29022
29127
|
env: argv.env,
|
|
29023
29128
|
force: argv.force === true,
|
|
29129
|
+
yes: argv.yes === true,
|
|
29024
29130
|
jsonOnly: json
|
|
29025
29131
|
},
|
|
29026
29132
|
{
|
|
@@ -29083,6 +29189,11 @@ var deploy_default = {
|
|
|
29083
29189
|
type: "boolean",
|
|
29084
29190
|
describe: "Force redeploy of selected resources even when healthy",
|
|
29085
29191
|
default: false
|
|
29192
|
+
}).option("yes", {
|
|
29193
|
+
alias: "y",
|
|
29194
|
+
type: "boolean",
|
|
29195
|
+
describe: "Config version 2: confirm a change that cannot be undone, such as switching the identity provider",
|
|
29196
|
+
default: false
|
|
29086
29197
|
}).option("resume-from", {
|
|
29087
29198
|
type: "string",
|
|
29088
29199
|
describe: "Resume publish ledger from step id"
|
|
@@ -29523,10 +29634,19 @@ function plansRemote() {
|
|
|
29523
29634
|
return void 0;
|
|
29524
29635
|
}
|
|
29525
29636
|
}
|
|
29637
|
+
function hostingRemote() {
|
|
29638
|
+
try {
|
|
29639
|
+
const client = new PlatformApiClient(loadConfig());
|
|
29640
|
+
return async (projectId) => resolveSwaUrl(await client.getProject(projectId));
|
|
29641
|
+
} catch {
|
|
29642
|
+
return void 0;
|
|
29643
|
+
}
|
|
29644
|
+
}
|
|
29526
29645
|
var statusOfProject = (argv) => run2(
|
|
29527
29646
|
"status",
|
|
29528
29647
|
argv,
|
|
29529
29648
|
(dir, only) => statusV2(dir, only, {
|
|
29649
|
+
hostingRemote: hostingRemote(),
|
|
29530
29650
|
gatewayRemote: gatewayRemote(),
|
|
29531
29651
|
identityRemote: identityRemote(),
|
|
29532
29652
|
identityConnectionsRemote: identityConnectionsRemote(),
|
|
@@ -31315,10 +31435,11 @@ async function resetProjectV2(options, deps) {
|
|
|
31315
31435
|
"Resetting removes the plans, the identity with every user, and the gateway of the project.",
|
|
31316
31436
|
"Pass --yes to go on."
|
|
31317
31437
|
);
|
|
31438
|
+
const sleep5 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
31318
31439
|
const steps = [];
|
|
31319
31440
|
for (const service of STEPS) {
|
|
31320
31441
|
try {
|
|
31321
|
-
const
|
|
31442
|
+
const removeOnce = () => teardownV2(
|
|
31322
31443
|
{
|
|
31323
31444
|
dir: options.dir,
|
|
31324
31445
|
projectId,
|
|
@@ -31328,6 +31449,12 @@ async function resetProjectV2(options, deps) {
|
|
|
31328
31449
|
},
|
|
31329
31450
|
deps
|
|
31330
31451
|
);
|
|
31452
|
+
const result = await removeOnce().catch(async (error) => {
|
|
31453
|
+
if (!(error instanceof PlatformApiError) || error.status < 500)
|
|
31454
|
+
throw error;
|
|
31455
|
+
await sleep5(5e3);
|
|
31456
|
+
return removeOnce();
|
|
31457
|
+
});
|
|
31331
31458
|
if (result.status === "pending") {
|
|
31332
31459
|
steps.push({ service, outcome: "pending", message: result.message });
|
|
31333
31460
|
return {
|
|
@@ -31336,6 +31463,7 @@ async function resetProjectV2(options, deps) {
|
|
|
31336
31463
|
status: "pending",
|
|
31337
31464
|
steps,
|
|
31338
31465
|
cleared: [],
|
|
31466
|
+
notes: [],
|
|
31339
31467
|
message: `The ${service} is still being removed by the platform. Run the reset again in a minute.`
|
|
31340
31468
|
};
|
|
31341
31469
|
}
|
|
@@ -31348,6 +31476,22 @@ async function resetProjectV2(options, deps) {
|
|
|
31348
31476
|
throw error;
|
|
31349
31477
|
}
|
|
31350
31478
|
}
|
|
31479
|
+
let backendNote;
|
|
31480
|
+
if (deps.client.resetDevPortalTestData) {
|
|
31481
|
+
const emptyOnce = async () => {
|
|
31482
|
+
try {
|
|
31483
|
+
return (await deps.client.resetDevPortalTestData?.(projectId))?.plansBackend ?? "no-answer";
|
|
31484
|
+
} catch (error) {
|
|
31485
|
+
return `failed-${error instanceof Error ? error.message : String(error)}`;
|
|
31486
|
+
}
|
|
31487
|
+
};
|
|
31488
|
+
let outcome = await emptyOnce();
|
|
31489
|
+
if (outcome.startsWith("failed") || /^refused-5/.test(outcome)) {
|
|
31490
|
+
await sleep5(15e3);
|
|
31491
|
+
outcome = await emptyOnce();
|
|
31492
|
+
}
|
|
31493
|
+
backendNote = outcome === "notified" ? "plans backend: emptied" : outcome === "no-backend" ? "plans backend: nothing to empty yet (the project has none)" : `plans backend: not emptied (${outcome})`;
|
|
31494
|
+
}
|
|
31351
31495
|
const cleared = [];
|
|
31352
31496
|
const ledger = await readLedger(resolved.state);
|
|
31353
31497
|
const recorded = DEPLOY_ORDER.filter((service) => ledger.services[service]);
|
|
@@ -31363,6 +31507,7 @@ async function resetProjectV2(options, deps) {
|
|
|
31363
31507
|
status: "reset",
|
|
31364
31508
|
steps,
|
|
31365
31509
|
cleared,
|
|
31510
|
+
notes: backendNote ? [backendNote] : [],
|
|
31366
31511
|
message: "The project is reset: it is linked and its hosting is kept. Run `apitogo deploy` to build on it again."
|
|
31367
31512
|
};
|
|
31368
31513
|
}
|
|
@@ -31392,7 +31537,10 @@ async function resetProject(argv) {
|
|
|
31392
31537
|
return;
|
|
31393
31538
|
}
|
|
31394
31539
|
for (const step2 of result.steps)
|
|
31395
|
-
console.log(
|
|
31540
|
+
console.log(
|
|
31541
|
+
`${step2.service}: ${step2.outcome}${step2.outcome === "skipped" ? " (nothing was deployed)" : ""}`
|
|
31542
|
+
);
|
|
31543
|
+
for (const note of result.notes) console.log(note);
|
|
31396
31544
|
if (result.cleared.length > 0)
|
|
31397
31545
|
console.log(`Cleared locally: ${result.cleared.join(", ")}.`);
|
|
31398
31546
|
console.log(result.message);
|
|
@@ -599,6 +599,9 @@ export declare class PlatformApiClient {
|
|
|
599
599
|
getGatewayPlan(projectId: string, planId: string): Promise<GatewayPlanDto>;
|
|
600
600
|
updateGatewayPlan(projectId: string, planId: string, body: UpdateGatewayPlanRequest): Promise<GatewayPlanDto>;
|
|
601
601
|
deleteGatewayPlan(projectId: string, planId: string): Promise<void>;
|
|
602
|
+
resetDevPortalTestData(projectId: string): Promise<{
|
|
603
|
+
plansBackend: string;
|
|
604
|
+
}>;
|
|
602
605
|
createTestSignIn(projectId: string, body: {
|
|
603
606
|
email: string;
|
|
604
607
|
returnUrl?: string;
|
package/package.json
CHANGED
package/src/config/v2/rules.ts
CHANGED
|
@@ -73,8 +73,14 @@ function dependencyRules(
|
|
|
73
73
|
const oidcGateway = gatewayOn && resolved.gateway?.auth?.mode === "oidc";
|
|
74
74
|
const hasIdentitySource = identityOn || oidcGateway;
|
|
75
75
|
|
|
76
|
-
// `apitogo project create` hosts services through the portal or the gateway, so identity alone has nowhere to run.
|
|
77
|
-
|
|
76
|
+
// `apitogo project create` hosts services through the portal or the gateway, so identity alone has nowhere to run. A folder
|
|
77
|
+
// that is already linked to a project has its hosting, and `deploy` puts the identity there (found in scenario F).
|
|
78
|
+
if (
|
|
79
|
+
identityOn &&
|
|
80
|
+
resolved.portal?.enabled !== true &&
|
|
81
|
+
!gatewayOn &&
|
|
82
|
+
!resolved.project.id
|
|
83
|
+
) {
|
|
78
84
|
out.error({
|
|
79
85
|
code: "NOTHING_TO_HOST",
|
|
80
86
|
path: "identity",
|
|
@@ -313,10 +313,10 @@ async function buildPortal(
|
|
|
313
313
|
if (content.length === 0) {
|
|
314
314
|
let template = options.template;
|
|
315
315
|
if (!template && options.ask)
|
|
316
|
-
template = await options.ask.choose("Content of the new site", [
|
|
316
|
+
template = (await options.ask.choose("Content of the new site", [
|
|
317
317
|
{ value: "starter", label: "A starter page I can edit" },
|
|
318
318
|
{ value: "blank", label: "An empty pages folder" },
|
|
319
|
-
]);
|
|
319
|
+
])) as "starter" | "blank";
|
|
320
320
|
const pages = path.join(resolved.root, "portal", "pages");
|
|
321
321
|
if (template === "blank")
|
|
322
322
|
await addFile(context, path.join(pages, ".gitkeep"), "");
|
|
@@ -579,7 +579,9 @@ async function buildPlans(
|
|
|
579
579
|
: options.template === "starter"
|
|
580
580
|
? "free"
|
|
581
581
|
: "free");
|
|
582
|
-
|
|
582
|
+
// Only paid plans need a billing provider. A catalog of free plans takes no payment, so its default is `none` and the
|
|
583
|
+
// config checks clean right away; the tiered template (paid plans) defaults to Stripe, whose key is set with `secrets set`.
|
|
584
|
+
const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
|
|
583
585
|
if (template === "tiered" && billing === "none") {
|
|
584
586
|
throw new ServiceEditError(
|
|
585
587
|
"The tiered template has paid plans, which need a billing provider.",
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type PortalPlan = {
|
|
|
68
68
|
keysApiBaseUrl?: string;
|
|
69
69
|
/** The billing provider of the plans service (`none`, `stripe`, ...). `none`: the pages offer no payment. */
|
|
70
70
|
billing?: string;
|
|
71
|
+
/** Where the account pages link to: only places this portal has (hosted docs, an external docs site, the API page). */
|
|
72
|
+
links?: { docs?: string; api?: string };
|
|
71
73
|
};
|
|
72
74
|
/** Account pages. */
|
|
73
75
|
userPanel: boolean;
|
|
@@ -231,6 +233,14 @@ export function planPortal(
|
|
|
231
233
|
apiBaseUrl: plans.apiBaseUrl,
|
|
232
234
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
233
235
|
billing: plans.billing,
|
|
236
|
+
links: {
|
|
237
|
+
...(profile.modules.docs
|
|
238
|
+
? { docs: "/introduction" }
|
|
239
|
+
: profile.docs.url
|
|
240
|
+
? { docs: profile.docs.url }
|
|
241
|
+
: {}),
|
|
242
|
+
...(plan.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {}),
|
|
243
|
+
},
|
|
234
244
|
};
|
|
235
245
|
}
|
|
236
246
|
|