@lukoweb/apitogo 0.2.37 → 0.2.39
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.39",
|
|
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) {
|
|
@@ -27294,7 +27351,7 @@ async function portalUrlOf(resolved, projectId, client) {
|
|
|
27294
27351
|
throw new IdentityDeployError(
|
|
27295
27352
|
"IDENTITY_PORTAL_ADDRESS_UNKNOWN",
|
|
27296
27353
|
"The portal has no public address yet, and the identity needs it for the sign-in redirect.",
|
|
27297
|
-
"Run `apitogo deploy --only portal`
|
|
27354
|
+
"The address comes with the project's hosting. Run `apitogo status`: while hosting is still being prepared, wait a few minutes and run `apitogo deploy` again. Once hosting is ready, `apitogo deploy --only portal` gives the portal its address, then deploy identity."
|
|
27298
27355
|
);
|
|
27299
27356
|
return url2;
|
|
27300
27357
|
}
|
|
@@ -27712,11 +27769,15 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
|
27712
27769
|
let created = 0;
|
|
27713
27770
|
let updated = 0;
|
|
27714
27771
|
let archived = 0;
|
|
27772
|
+
const backendSync = /* @__PURE__ */ new Set();
|
|
27773
|
+
const noteSync = (plan2) => {
|
|
27774
|
+
if (plan2.plansBackendSync) backendSync.add(plan2.plansBackendSync);
|
|
27775
|
+
};
|
|
27715
27776
|
for (const body of bodies) {
|
|
27716
27777
|
const found = existing.get(body.sku);
|
|
27717
27778
|
try {
|
|
27718
27779
|
if (found) {
|
|
27719
|
-
await client.updateGatewayPlan(projectId, found.id, {
|
|
27780
|
+
const result = await client.updateGatewayPlan(projectId, found.id, {
|
|
27720
27781
|
displayName: body.displayName,
|
|
27721
27782
|
isActive: true,
|
|
27722
27783
|
limitsJson: body.limitsJson ?? null,
|
|
@@ -27728,9 +27789,10 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
|
27728
27789
|
billingPeriod: body.billingPeriod,
|
|
27729
27790
|
sortOrder: body.sortOrder
|
|
27730
27791
|
});
|
|
27792
|
+
noteSync(result);
|
|
27731
27793
|
updated++;
|
|
27732
27794
|
} else {
|
|
27733
|
-
await client.createGatewayPlan(projectId, body);
|
|
27795
|
+
noteSync(await client.createGatewayPlan(projectId, body));
|
|
27734
27796
|
created++;
|
|
27735
27797
|
}
|
|
27736
27798
|
} catch (error) {
|
|
@@ -27741,7 +27803,7 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
|
27741
27803
|
for (const plan2 of existing.values()) {
|
|
27742
27804
|
if (!plan2.isActive || kept.has(plan2.sku.toLowerCase())) continue;
|
|
27743
27805
|
try {
|
|
27744
|
-
await client.updateGatewayPlan(projectId, plan2.id, {
|
|
27806
|
+
const result = await client.updateGatewayPlan(projectId, plan2.id, {
|
|
27745
27807
|
displayName: plan2.displayName,
|
|
27746
27808
|
isActive: false,
|
|
27747
27809
|
limitsJson: plan2.limitsJson ?? null,
|
|
@@ -27754,6 +27816,7 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
|
27754
27816
|
billingPeriod: plan2.billingPeriod,
|
|
27755
27817
|
sortOrder: plan2.sortOrder
|
|
27756
27818
|
});
|
|
27819
|
+
noteSync(result);
|
|
27757
27820
|
archived++;
|
|
27758
27821
|
} catch (error) {
|
|
27759
27822
|
refused(error);
|
|
@@ -27762,7 +27825,8 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
|
|
|
27762
27825
|
return {
|
|
27763
27826
|
created: String(created),
|
|
27764
27827
|
updated: String(updated),
|
|
27765
|
-
archived: String(archived)
|
|
27828
|
+
archived: String(archived),
|
|
27829
|
+
...backendSync.size > 0 ? { backendSync: [...backendSync].sort().join(",") } : {}
|
|
27766
27830
|
};
|
|
27767
27831
|
},
|
|
27768
27832
|
// A plan someone deleted on the platform is not a result worth keeping, and neither is one that should be
|
|
@@ -27934,6 +27998,8 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27934
27998
|
details: []
|
|
27935
27999
|
};
|
|
27936
28000
|
const known = manifest?.services.identity;
|
|
28001
|
+
const switched = identityProviderSwitch(resolved, known);
|
|
28002
|
+
const switchNote = switched ? [identityProviderSwitchWarning(switched.from, switched.to)] : [];
|
|
27937
28003
|
if (identity.provider === "oidc") {
|
|
27938
28004
|
const same = known?.provider === "oidc" && known.issuer === identity.issuer?.replace(/\/+$/, "") && known.clientId === identity.clientId;
|
|
27939
28005
|
return {
|
|
@@ -27941,6 +28007,7 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27941
28007
|
action: same ? "skip" : "deploy",
|
|
27942
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)",
|
|
27943
28009
|
details: [
|
|
28010
|
+
...switchNote,
|
|
27944
28011
|
`provider: oidc ${identity.issuer}`,
|
|
27945
28012
|
`client id: ${identity.clientId}`
|
|
27946
28013
|
]
|
|
@@ -27952,6 +28019,7 @@ async function planIdentityService(resolved, ledger, manifest) {
|
|
|
27952
28019
|
const body = portalUrl ? identityRequestBody(resolved, portalUrl) : recorded ? identityBodyForRedirect(resolved, recorded) : void 0;
|
|
27953
28020
|
const hash2 = projectId && body ? identityInputsHash(projectId, body) : void 0;
|
|
27954
28021
|
const details = [
|
|
28022
|
+
...switchNote,
|
|
27955
28023
|
"provider: apitogo (one issuer for this project)",
|
|
27956
28024
|
`sign-up with email and password: ${identity.native ? "on" : "off"}`,
|
|
27957
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",
|
|
@@ -28559,6 +28627,20 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
|
|
|
28559
28627
|
if (failed) state = "deploy-failed";
|
|
28560
28628
|
return { service: "gateway", state, details: [...details, ...local] };
|
|
28561
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
|
+
}
|
|
28562
28644
|
async function wantedSkus(resolved) {
|
|
28563
28645
|
if (!resolved.plans) return void 0;
|
|
28564
28646
|
try {
|
|
@@ -28601,7 +28683,8 @@ async function statusV2(dir, only, options = {}) {
|
|
|
28601
28683
|
...state === "built" ? [
|
|
28602
28684
|
`output: ${path82.relative(resolved.configDir, resolved.state.buildDir)}`
|
|
28603
28685
|
] : [],
|
|
28604
|
-
...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)
|
|
28605
28688
|
]
|
|
28606
28689
|
});
|
|
28607
28690
|
continue;
|
|
@@ -28719,6 +28802,12 @@ function parseDeployOnly(value) {
|
|
|
28719
28802
|
}
|
|
28720
28803
|
return DEPLOY_ORDER.filter((name) => names.includes(name));
|
|
28721
28804
|
}
|
|
28805
|
+
function isHostingNotReady(error) {
|
|
28806
|
+
if (!(error instanceof PlatformApiError)) return false;
|
|
28807
|
+
return /not provisioned yet|Could not retrieve deployment token/i.test(
|
|
28808
|
+
`${error.message} ${error.problem?.detail ?? ""}`
|
|
28809
|
+
);
|
|
28810
|
+
}
|
|
28722
28811
|
function publishStep(deps, options) {
|
|
28723
28812
|
return {
|
|
28724
28813
|
name: "publish",
|
|
@@ -28738,6 +28827,14 @@ function publishStep(deps, options) {
|
|
|
28738
28827
|
byteSize: String(upload.byteSize),
|
|
28739
28828
|
deployServerStatus: String(upload.deployServerStatus)
|
|
28740
28829
|
};
|
|
28830
|
+
} catch (error) {
|
|
28831
|
+
if (isHostingNotReady(error))
|
|
28832
|
+
throw new DeployError(
|
|
28833
|
+
"HOSTING_NOT_READY",
|
|
28834
|
+
"The portal's hosting is not ready yet: the platform is still preparing it, so there is nothing to publish to.",
|
|
28835
|
+
"Nothing is wrong with your files. Run `apitogo status` to see the hosting, wait a few minutes (a project made while the pool was full waits longest), then run `apitogo deploy` again."
|
|
28836
|
+
);
|
|
28837
|
+
throw error;
|
|
28741
28838
|
} finally {
|
|
28742
28839
|
await rm11(archive.archivePath, { force: true });
|
|
28743
28840
|
}
|
|
@@ -28865,6 +28962,16 @@ async function deployIdentityService(options, deps, projectId, named) {
|
|
|
28865
28962
|
);
|
|
28866
28963
|
return { service: "identity", status: "skipped", reason: why, steps: [] };
|
|
28867
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
|
+
);
|
|
28868
28975
|
const client = deps.client;
|
|
28869
28976
|
if (resolved.identity?.provider !== "oidc" && (!client.putProjectIdentity || !client.getProjectIdentity || !client.getProject))
|
|
28870
28977
|
throw new Error("The platform client has no identity calls.");
|
|
@@ -28916,7 +29023,26 @@ async function deployPlansService(options, deps, projectId, named) {
|
|
|
28916
29023
|
...result.notes ? { notes: result.notes } : {}
|
|
28917
29024
|
};
|
|
28918
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
|
+
}
|
|
28919
29044
|
async function deployPortal(options, deps, projectId) {
|
|
29045
|
+
await recordPortalAddressBeforeBuild(options.dir, deps, projectId);
|
|
28920
29046
|
const { resolved, root, prepared } = await preparePortalWorkspace(
|
|
28921
29047
|
options.dir,
|
|
28922
29048
|
"production"
|
|
@@ -29000,6 +29126,7 @@ async function deployProject(argv) {
|
|
|
29000
29126
|
projectId: argv.projectId,
|
|
29001
29127
|
env: argv.env,
|
|
29002
29128
|
force: argv.force === true,
|
|
29129
|
+
yes: argv.yes === true,
|
|
29003
29130
|
jsonOnly: json
|
|
29004
29131
|
},
|
|
29005
29132
|
{
|
|
@@ -29062,6 +29189,11 @@ var deploy_default = {
|
|
|
29062
29189
|
type: "boolean",
|
|
29063
29190
|
describe: "Force redeploy of selected resources even when healthy",
|
|
29064
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
|
|
29065
29197
|
}).option("resume-from", {
|
|
29066
29198
|
type: "string",
|
|
29067
29199
|
describe: "Resume publish ledger from step id"
|
|
@@ -29502,10 +29634,19 @@ function plansRemote() {
|
|
|
29502
29634
|
return void 0;
|
|
29503
29635
|
}
|
|
29504
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
|
+
}
|
|
29505
29645
|
var statusOfProject = (argv) => run2(
|
|
29506
29646
|
"status",
|
|
29507
29647
|
argv,
|
|
29508
29648
|
(dir, only) => statusV2(dir, only, {
|
|
29649
|
+
hostingRemote: hostingRemote(),
|
|
29509
29650
|
gatewayRemote: gatewayRemote(),
|
|
29510
29651
|
identityRemote: identityRemote(),
|
|
29511
29652
|
identityConnectionsRemote: identityConnectionsRemote(),
|
|
@@ -29647,7 +29788,7 @@ async function createProjectV2(options, deps) {
|
|
|
29647
29788
|
throw new ProjectCreateError(
|
|
29648
29789
|
"CREATE_FAILED",
|
|
29649
29790
|
"The project was created but hosting is still being prepared.",
|
|
29650
|
-
`The folder is linked (project.id ${outcome.projectId}). Wait a few minutes, then run \`apitogo
|
|
29791
|
+
`The folder is linked (project.id ${outcome.projectId}). Wait a few minutes (hosting takes 20 to 30 minutes, longer while the pool is full), then run \`apitogo project create\` again: it finishes the same project and does not make a second one. \`apitogo status\` shows how far it is.`
|
|
29651
29792
|
);
|
|
29652
29793
|
}
|
|
29653
29794
|
await setProjectId(options.dir, outcome.projectId);
|
|
@@ -29664,7 +29805,7 @@ function assertCreated(outcome) {
|
|
|
29664
29805
|
throw new ProjectCreateError(
|
|
29665
29806
|
"CREATE_FAILED",
|
|
29666
29807
|
`The platform could not create the project: ${outcome.message ?? outcome.status ?? "no answer"}.`,
|
|
29667
|
-
"
|
|
29808
|
+
/capacity/i.test(outcome.message ?? "") ? "There is no free hosting right now. Run `apitogo project create` again in a few minutes: it picks up where it stopped, and a project that was already made is not made twice. `apitogo status` shows whether this folder is linked." : "Try again, or check `apitogo status`."
|
|
29668
29809
|
);
|
|
29669
29810
|
}
|
|
29670
29811
|
}
|
|
@@ -31294,10 +31435,11 @@ async function resetProjectV2(options, deps) {
|
|
|
31294
31435
|
"Resetting removes the plans, the identity with every user, and the gateway of the project.",
|
|
31295
31436
|
"Pass --yes to go on."
|
|
31296
31437
|
);
|
|
31438
|
+
const sleep5 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
31297
31439
|
const steps = [];
|
|
31298
31440
|
for (const service of STEPS) {
|
|
31299
31441
|
try {
|
|
31300
|
-
const
|
|
31442
|
+
const removeOnce = () => teardownV2(
|
|
31301
31443
|
{
|
|
31302
31444
|
dir: options.dir,
|
|
31303
31445
|
projectId,
|
|
@@ -31307,6 +31449,12 @@ async function resetProjectV2(options, deps) {
|
|
|
31307
31449
|
},
|
|
31308
31450
|
deps
|
|
31309
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
|
+
});
|
|
31310
31458
|
if (result.status === "pending") {
|
|
31311
31459
|
steps.push({ service, outcome: "pending", message: result.message });
|
|
31312
31460
|
return {
|
|
@@ -31315,6 +31463,7 @@ async function resetProjectV2(options, deps) {
|
|
|
31315
31463
|
status: "pending",
|
|
31316
31464
|
steps,
|
|
31317
31465
|
cleared: [],
|
|
31466
|
+
notes: [],
|
|
31318
31467
|
message: `The ${service} is still being removed by the platform. Run the reset again in a minute.`
|
|
31319
31468
|
};
|
|
31320
31469
|
}
|
|
@@ -31327,6 +31476,22 @@ async function resetProjectV2(options, deps) {
|
|
|
31327
31476
|
throw error;
|
|
31328
31477
|
}
|
|
31329
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
|
+
}
|
|
31330
31495
|
const cleared = [];
|
|
31331
31496
|
const ledger = await readLedger(resolved.state);
|
|
31332
31497
|
const recorded = DEPLOY_ORDER.filter((service) => ledger.services[service]);
|
|
@@ -31342,6 +31507,7 @@ async function resetProjectV2(options, deps) {
|
|
|
31342
31507
|
status: "reset",
|
|
31343
31508
|
steps,
|
|
31344
31509
|
cleared,
|
|
31510
|
+
notes: backendNote ? [backendNote] : [],
|
|
31345
31511
|
message: "The project is reset: it is linked and its hosting is kept. Run `apitogo deploy` to build on it again."
|
|
31346
31512
|
};
|
|
31347
31513
|
}
|
|
@@ -31371,7 +31537,10 @@ async function resetProject(argv) {
|
|
|
31371
31537
|
return;
|
|
31372
31538
|
}
|
|
31373
31539
|
for (const step2 of result.steps)
|
|
31374
|
-
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);
|
|
31375
31544
|
if (result.cleared.length > 0)
|
|
31376
31545
|
console.log(`Cleared locally: ${result.cleared.join(", ")}.`);
|
|
31377
31546
|
console.log(result.message);
|
|
@@ -108,6 +108,7 @@ export interface GatewayPlanDto {
|
|
|
108
108
|
priceAmount?: number;
|
|
109
109
|
pricingModel?: "fixed" | "custom";
|
|
110
110
|
priceLabel?: string | null;
|
|
111
|
+
plansBackendSync?: string | null;
|
|
111
112
|
priceCurrency?: string;
|
|
112
113
|
billingPeriod?: string;
|
|
113
114
|
limitsJson?: string | null;
|
|
@@ -598,6 +599,9 @@ export declare class PlatformApiClient {
|
|
|
598
599
|
getGatewayPlan(projectId: string, planId: string): Promise<GatewayPlanDto>;
|
|
599
600
|
updateGatewayPlan(projectId: string, planId: string, body: UpdateGatewayPlanRequest): Promise<GatewayPlanDto>;
|
|
600
601
|
deleteGatewayPlan(projectId: string, planId: string): Promise<void>;
|
|
602
|
+
resetDevPortalTestData(projectId: string): Promise<{
|
|
603
|
+
plansBackend: string;
|
|
604
|
+
}>;
|
|
601
605
|
createTestSignIn(projectId: string, body: {
|
|
602
606
|
email: string;
|
|
603
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
|
|