@lukoweb/apitogo 0.2.58 → 0.2.59
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 +72 -18
- package/docs/configuration/portal.md +2 -1
- package/package.json +1 -1
- package/src/config/v2/gateway-request.ts +5 -2
- package/src/config/v2/index.ts +1 -0
- package/src/config/v2/manifest.ts +25 -0
- package/src/config/v2/resolve.ts +5 -1
- package/src/config/v2/schema.ts +18 -5
package/dist/cli/cli.js
CHANGED
|
@@ -5931,11 +5931,23 @@ var PortalSchema = z.strictObject({
|
|
|
5931
5931
|
landing: path3.optional(),
|
|
5932
5932
|
/** The footer: a React component file shown under every page except the console's (API-129). */
|
|
5933
5933
|
footer: path3.optional(),
|
|
5934
|
-
/**
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5934
|
+
/**
|
|
5935
|
+
* The logo in the portal header: one image, or one per color scheme (API-129). `dark` is optional; without it the
|
|
5936
|
+
* light image is used in both.
|
|
5937
|
+
*/
|
|
5938
|
+
logo: z.union(
|
|
5939
|
+
[
|
|
5940
|
+
path3,
|
|
5941
|
+
z.strictObject({
|
|
5942
|
+
light: path3,
|
|
5943
|
+
dark: path3.optional(),
|
|
5944
|
+
alt: z.string().optional()
|
|
5945
|
+
})
|
|
5946
|
+
],
|
|
5947
|
+
{
|
|
5948
|
+
error: 'must be an image path or https address, or { "light": "...", "dark": "...", "alt": "..." } (dark and alt are optional)'
|
|
5949
|
+
}
|
|
5950
|
+
).optional(),
|
|
5939
5951
|
requireAuth: z.union([z.boolean(), z.strictObject({ exclude: pathList })], {
|
|
5940
5952
|
error: 'must be true, false or { "exclude": ["/route", ...] }'
|
|
5941
5953
|
}).optional(),
|
|
@@ -6507,7 +6519,11 @@ function resolveConfigV2(config2, configPath) {
|
|
|
6507
6519
|
dark: logoSource(configDir, portal.logo)
|
|
6508
6520
|
} : {
|
|
6509
6521
|
light: logoSource(configDir, portal.logo.light),
|
|
6510
|
-
dark
|
|
6522
|
+
// Without a dark image, the light one is used in both color schemes.
|
|
6523
|
+
dark: logoSource(
|
|
6524
|
+
configDir,
|
|
6525
|
+
portal.logo.dark ?? portal.logo.light
|
|
6526
|
+
),
|
|
6511
6527
|
...portal.logo.alt ? { alt: portal.logo.alt } : {}
|
|
6512
6528
|
}
|
|
6513
6529
|
} : {},
|
|
@@ -7472,6 +7488,8 @@ var ServiceManifestSchema = z3.looseObject({
|
|
|
7472
7488
|
services: z3.looseObject({
|
|
7473
7489
|
portal: z3.looseObject({
|
|
7474
7490
|
url: z3.string().optional(),
|
|
7491
|
+
/** The portal's custom domain once it is ready, such as `https://docs.acme.com` (API-130). */
|
|
7492
|
+
customDomain: z3.string().optional(),
|
|
7475
7493
|
docs: z3.looseObject({ provider: z3.string(), url: z3.string().optional() }).optional(),
|
|
7476
7494
|
console: z3.looseObject({ links: z3.record(z3.string(), z3.string()).optional() }).optional()
|
|
7477
7495
|
}).optional(),
|
|
@@ -7643,6 +7661,20 @@ function withPortalEntry(existing, info) {
|
|
|
7643
7661
|
}
|
|
7644
7662
|
};
|
|
7645
7663
|
}
|
|
7664
|
+
function withPortalCustomDomain(existing, customDomain) {
|
|
7665
|
+
if (!existing) return void 0;
|
|
7666
|
+
const portal = existing.services.portal;
|
|
7667
|
+
if (portal?.customDomain === customDomain) return void 0;
|
|
7668
|
+
if (!portal && customDomain === void 0) return void 0;
|
|
7669
|
+
const { customDomain: _previous, ...rest } = portal ?? {};
|
|
7670
|
+
return {
|
|
7671
|
+
...existing,
|
|
7672
|
+
services: {
|
|
7673
|
+
...existing.services,
|
|
7674
|
+
portal: customDomain === void 0 ? rest : { ...rest, customDomain }
|
|
7675
|
+
}
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7646
7678
|
function withGatewayEntry(existing, info) {
|
|
7647
7679
|
const base = existing ?? parseManifest({
|
|
7648
7680
|
schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
|
|
@@ -7922,7 +7954,8 @@ function derivedCorsSites(sites) {
|
|
|
7922
7954
|
};
|
|
7923
7955
|
const found = [
|
|
7924
7956
|
{ origin: originOf(sites.docsUrl), source: "docs" },
|
|
7925
|
-
{ origin: originOf(sites.portalUrl), source: "portal" }
|
|
7957
|
+
{ origin: originOf(sites.portalUrl), source: "portal" },
|
|
7958
|
+
{ origin: originOf(sites.portalCustomDomain), source: "portal" }
|
|
7926
7959
|
].flatMap((site) => site.origin ? [{ ...site, origin: site.origin }] : []);
|
|
7927
7960
|
const seen = /* @__PURE__ */ new Set();
|
|
7928
7961
|
return found.filter((site) => !seen.has(site.origin) && seen.add(site.origin)).sort((a, b) => a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
|
|
@@ -11166,7 +11199,7 @@ import {
|
|
|
11166
11199
|
// package.json
|
|
11167
11200
|
var package_default = {
|
|
11168
11201
|
name: "@lukoweb/apitogo",
|
|
11169
|
-
version: "0.2.
|
|
11202
|
+
version: "0.2.59",
|
|
11170
11203
|
type: "module",
|
|
11171
11204
|
sideEffects: [
|
|
11172
11205
|
"**/*.css",
|
|
@@ -16589,6 +16622,7 @@ async function addGatewayOnlineRules(report, resolved, args, deps) {
|
|
|
16589
16622
|
deps.gateway,
|
|
16590
16623
|
derivedCorsSites({
|
|
16591
16624
|
portalUrl: manifest?.services.portal?.url,
|
|
16625
|
+
portalCustomDomain: manifest?.services.portal?.customDomain,
|
|
16592
16626
|
docsUrl: resolved.portal?.docs.provider === "external" ? resolved.portal.docs.url : void 0
|
|
16593
16627
|
})
|
|
16594
16628
|
);
|
|
@@ -27172,6 +27206,22 @@ init_invariant();
|
|
|
27172
27206
|
import { createHash as createHash4 } from "node:crypto";
|
|
27173
27207
|
import { readFile as readFile28 } from "node:fs/promises";
|
|
27174
27208
|
init_invariant();
|
|
27209
|
+
|
|
27210
|
+
// src/cli/deploy/custom-domain.ts
|
|
27211
|
+
async function readyCustomDomainOrigin(client, projectId) {
|
|
27212
|
+
if (!client.getCustomDomain) return void 0;
|
|
27213
|
+
let domain;
|
|
27214
|
+
try {
|
|
27215
|
+
domain = await client.getCustomDomain(projectId);
|
|
27216
|
+
} catch {
|
|
27217
|
+
return void 0;
|
|
27218
|
+
}
|
|
27219
|
+
const hostname = domain.hostname?.trim().toLowerCase();
|
|
27220
|
+
if (domain.status?.toLowerCase() !== "ready" || !hostname) return null;
|
|
27221
|
+
return `https://${hostname}`;
|
|
27222
|
+
}
|
|
27223
|
+
|
|
27224
|
+
// src/cli/deploy/gateway.ts
|
|
27175
27225
|
var GatewayDeployError = class extends ApitogoError {
|
|
27176
27226
|
constructor(code, message, developerHint) {
|
|
27177
27227
|
super(message, { developerHint, title: "Cannot deploy the gateway" });
|
|
@@ -27217,6 +27267,15 @@ async function readSpec(gateway) {
|
|
|
27217
27267
|
);
|
|
27218
27268
|
}
|
|
27219
27269
|
}
|
|
27270
|
+
async function recordPortalCustomDomain(resolved, projectId, client) {
|
|
27271
|
+
const origin = await readyCustomDomainOrigin(client, projectId);
|
|
27272
|
+
if (origin === void 0) return;
|
|
27273
|
+
const updated = withPortalCustomDomain(
|
|
27274
|
+
await readManifest(resolved.state),
|
|
27275
|
+
origin ?? void 0
|
|
27276
|
+
);
|
|
27277
|
+
if (updated) await writeManifest(resolved.state, updated);
|
|
27278
|
+
}
|
|
27220
27279
|
async function gatewayDeployInputs(resolved, projectId) {
|
|
27221
27280
|
const gateway = resolved.gateway;
|
|
27222
27281
|
if (!gateway) throw new Error("gateway is not configured");
|
|
@@ -27226,6 +27285,7 @@ async function gatewayDeployInputs(resolved, projectId) {
|
|
|
27226
27285
|
gateway,
|
|
27227
27286
|
derivedCorsSites({
|
|
27228
27287
|
portalUrl: manifest?.services.portal?.url,
|
|
27288
|
+
portalCustomDomain: manifest?.services.portal?.customDomain,
|
|
27229
27289
|
docsUrl: resolved.portal?.docs.provider === "external" ? resolved.portal.docs.url : void 0
|
|
27230
27290
|
})
|
|
27231
27291
|
);
|
|
@@ -27314,6 +27374,7 @@ function rejected(what, error) {
|
|
|
27314
27374
|
throw error;
|
|
27315
27375
|
}
|
|
27316
27376
|
async function deployGateway(resolved, projectId, deps, options = {}) {
|
|
27377
|
+
await recordPortalCustomDomain(resolved, projectId, deps.client);
|
|
27317
27378
|
const { gateway, spec, body, specHash, configHash } = await gatewayDeployInputs(resolved, projectId);
|
|
27318
27379
|
assertDeployable(gateway);
|
|
27319
27380
|
const backend = await backendDeployInputs(gateway, projectId);
|
|
@@ -28130,16 +28191,9 @@ function identityBrandingBody(resolved) {
|
|
|
28130
28191
|
var IDENTITY_BRANDING_STEP = "branding";
|
|
28131
28192
|
var IDENTITY_PORTAL_ADDRESS_STEP = "portal-address";
|
|
28132
28193
|
async function customDomainCallbackOf(client, projectId, portalUrl) {
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
domain = await client.getCustomDomain(projectId);
|
|
28137
|
-
} catch {
|
|
28138
|
-
return void 0;
|
|
28139
|
-
}
|
|
28140
|
-
const hostname = domain.hostname?.trim().toLowerCase();
|
|
28141
|
-
if (domain.status?.toLowerCase() !== "ready" || !hostname) return void 0;
|
|
28142
|
-
const callback = `https://${hostname}${PORTAL_CALLBACK_PATH}`;
|
|
28194
|
+
const origin = await readyCustomDomainOrigin(client, projectId);
|
|
28195
|
+
if (!origin) return void 0;
|
|
28196
|
+
const callback = `${origin}${PORTAL_CALLBACK_PATH}`;
|
|
28143
28197
|
const current = `${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}`;
|
|
28144
28198
|
return callback.toLowerCase() === current.toLowerCase() ? void 0 : callback;
|
|
28145
28199
|
}
|
|
@@ -79,7 +79,8 @@ An image in the portal's `public/` folder, or an `https://` address:
|
|
|
79
79
|
"logo": "apitogo/portal/public/logo.svg"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
One image per color scheme, with the text read out by screen readers
|
|
82
|
+
One image per color scheme, with the text read out by screen readers. `dark` and `alt` are optional;
|
|
83
|
+
without `dark` the light image is shown in both:
|
|
83
84
|
|
|
84
85
|
```json
|
|
85
86
|
"logo": {
|
package/package.json
CHANGED
|
@@ -39,11 +39,13 @@ export type GatewayConfigBody = {
|
|
|
39
39
|
export type DerivedCorsSite = { origin: string; source: "portal" | "docs" };
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* The browser origins of this project's own sites: the portal (from `services.json`) and an
|
|
43
|
-
* (from the config). Only the origin is kept, never a path. Sorted, so the same sites always give the same body.
|
|
42
|
+
* The browser origins of this project's own sites: the portal and its custom domain (from `services.json`) and an
|
|
43
|
+
* external docs site (from the config). Only the origin is kept, never a path. Sorted, so the same sites always give the same body.
|
|
44
44
|
*/
|
|
45
45
|
export function derivedCorsSites(sites: {
|
|
46
46
|
portalUrl?: string;
|
|
47
|
+
/** The portal's custom domain, once it is ready (API-130). */
|
|
48
|
+
portalCustomDomain?: string;
|
|
47
49
|
docsUrl?: string;
|
|
48
50
|
}): DerivedCorsSite[] {
|
|
49
51
|
const originOf = (url?: string) => {
|
|
@@ -58,6 +60,7 @@ export function derivedCorsSites(sites: {
|
|
|
58
60
|
const found = [
|
|
59
61
|
{ origin: originOf(sites.docsUrl), source: "docs" as const },
|
|
60
62
|
{ origin: originOf(sites.portalUrl), source: "portal" as const },
|
|
63
|
+
{ origin: originOf(sites.portalCustomDomain), source: "portal" as const },
|
|
61
64
|
].flatMap((site) => (site.origin ? [{ ...site, origin: site.origin }] : []));
|
|
62
65
|
const seen = new Set<string>();
|
|
63
66
|
return found
|
package/src/config/v2/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ const ServiceManifestSchema = z.looseObject({
|
|
|
29
29
|
portal: z
|
|
30
30
|
.looseObject({
|
|
31
31
|
url: z.string().optional(),
|
|
32
|
+
/** The portal's custom domain once it is ready, such as `https://docs.acme.com` (API-130). */
|
|
33
|
+
customDomain: z.string().optional(),
|
|
32
34
|
docs: z
|
|
33
35
|
.looseObject({ provider: z.string(), url: z.string().optional() })
|
|
34
36
|
.optional(),
|
|
@@ -291,6 +293,29 @@ export function withPortalEntry(
|
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Records the portal's custom domain (an https origin), or removes it when undefined (API-130). The gateway lets the
|
|
298
|
+
* browser call it from there, and `plan` reads it from here, so it agrees with deploy without asking the platform.
|
|
299
|
+
* Returns undefined when nothing changes, so an unchanged deploy leaves the file (and the portal's build hash) alone.
|
|
300
|
+
*/
|
|
301
|
+
export function withPortalCustomDomain(
|
|
302
|
+
existing: ServiceManifest | undefined,
|
|
303
|
+
customDomain: string | undefined,
|
|
304
|
+
): ServiceManifest | undefined {
|
|
305
|
+
if (!existing) return undefined;
|
|
306
|
+
const portal = existing.services.portal;
|
|
307
|
+
if (portal?.customDomain === customDomain) return undefined;
|
|
308
|
+
if (!portal && customDomain === undefined) return undefined;
|
|
309
|
+
const { customDomain: _previous, ...rest } = portal ?? {};
|
|
310
|
+
return {
|
|
311
|
+
...existing,
|
|
312
|
+
services: {
|
|
313
|
+
...existing.services,
|
|
314
|
+
portal: customDomain === undefined ? rest : { ...rest, customDomain },
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
/**
|
|
295
320
|
* Adds the gateway entry a gateway deploy learned. Only public values: the base URL and what the platform says the
|
|
296
321
|
* gateway can do. Entries of other services are kept as they are.
|
package/src/config/v2/resolve.ts
CHANGED
|
@@ -169,7 +169,11 @@ export function resolveConfigV2(
|
|
|
169
169
|
}
|
|
170
170
|
: {
|
|
171
171
|
light: logoSource(configDir, portal.logo.light),
|
|
172
|
-
dark
|
|
172
|
+
// Without a dark image, the light one is used in both color schemes.
|
|
173
|
+
dark: logoSource(
|
|
174
|
+
configDir,
|
|
175
|
+
portal.logo.dark ?? portal.logo.light,
|
|
176
|
+
),
|
|
173
177
|
...(portal.logo.alt ? { alt: portal.logo.alt } : {}),
|
|
174
178
|
},
|
|
175
179
|
}
|
package/src/config/v2/schema.ts
CHANGED
|
@@ -49,12 +49,25 @@ export const PortalSchema = z.strictObject({
|
|
|
49
49
|
landing: path.optional(),
|
|
50
50
|
/** The footer: a React component file shown under every page except the console's (API-129). */
|
|
51
51
|
footer: path.optional(),
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The logo in the portal header: one image, or one per color scheme (API-129). `dark` is optional; without it the
|
|
54
|
+
* light image is used in both.
|
|
55
|
+
*/
|
|
53
56
|
logo: z
|
|
54
|
-
.union(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
.union(
|
|
58
|
+
[
|
|
59
|
+
path,
|
|
60
|
+
z.strictObject({
|
|
61
|
+
light: path,
|
|
62
|
+
dark: path.optional(),
|
|
63
|
+
alt: z.string().optional(),
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
{
|
|
67
|
+
error:
|
|
68
|
+
'must be an image path or https address, or { "light": "...", "dark": "...", "alt": "..." } (dark and alt are optional)',
|
|
69
|
+
},
|
|
70
|
+
)
|
|
58
71
|
.optional(),
|
|
59
72
|
requireAuth: z
|
|
60
73
|
.union([z.boolean(), z.strictObject({ exclude: pathList })], {
|