@lukoweb/apitogo 0.2.58 → 0.2.60

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
@@ -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
- /** The logo in the portal header: one image, or one per color scheme (API-129). */
5935
- logo: z.union([
5936
- path3,
5937
- z.strictObject({ light: path3, dark: path3, alt: z.string().optional() })
5938
- ]).optional(),
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: logoSource(configDir, portal.logo.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);
@@ -8234,6 +8267,9 @@ function planPortal(profile, manifest) {
8234
8267
  preview: manifest.preview === true
8235
8268
  };
8236
8269
  if (!profile.enabled) return plan2;
8270
+ if (profile.pipelines.search === "pagefind") plan2.search = "pagefind";
8271
+ else if (profile.pipelines.search === "inkeep")
8272
+ skip("search", "inkeep needs keys that portal.search cannot hold yet");
8237
8273
  if (profile.pipelines.openapi) {
8238
8274
  if (!gateway) skip("api reference", "services.json has no gateway entry");
8239
8275
  else if (!serviceReady(gateway))
@@ -8358,6 +8394,7 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
8358
8394
  )
8359
8395
  }
8360
8396
  } : {},
8397
+ ...plan2.search ? { search: { type: plan2.search } } : {},
8361
8398
  ...plan2.apiReference ? {
8362
8399
  apis: [
8363
8400
  {
@@ -11166,7 +11203,7 @@ import {
11166
11203
  // package.json
11167
11204
  var package_default = {
11168
11205
  name: "@lukoweb/apitogo",
11169
- version: "0.2.58",
11206
+ version: "0.2.60",
11170
11207
  type: "module",
11171
11208
  sideEffects: [
11172
11209
  "**/*.css",
@@ -16589,6 +16626,7 @@ async function addGatewayOnlineRules(report, resolved, args, deps) {
16589
16626
  deps.gateway,
16590
16627
  derivedCorsSites({
16591
16628
  portalUrl: manifest?.services.portal?.url,
16629
+ portalCustomDomain: manifest?.services.portal?.customDomain,
16592
16630
  docsUrl: resolved.portal?.docs.provider === "external" ? resolved.portal.docs.url : void 0
16593
16631
  })
16594
16632
  );
@@ -27172,6 +27210,22 @@ init_invariant();
27172
27210
  import { createHash as createHash4 } from "node:crypto";
27173
27211
  import { readFile as readFile28 } from "node:fs/promises";
27174
27212
  init_invariant();
27213
+
27214
+ // src/cli/deploy/custom-domain.ts
27215
+ async function readyCustomDomainOrigin(client, projectId) {
27216
+ if (!client.getCustomDomain) return void 0;
27217
+ let domain;
27218
+ try {
27219
+ domain = await client.getCustomDomain(projectId);
27220
+ } catch {
27221
+ return void 0;
27222
+ }
27223
+ const hostname = domain.hostname?.trim().toLowerCase();
27224
+ if (domain.status?.toLowerCase() !== "ready" || !hostname) return null;
27225
+ return `https://${hostname}`;
27226
+ }
27227
+
27228
+ // src/cli/deploy/gateway.ts
27175
27229
  var GatewayDeployError = class extends ApitogoError {
27176
27230
  constructor(code, message, developerHint) {
27177
27231
  super(message, { developerHint, title: "Cannot deploy the gateway" });
@@ -27217,6 +27271,15 @@ async function readSpec(gateway) {
27217
27271
  );
27218
27272
  }
27219
27273
  }
27274
+ async function recordPortalCustomDomain(resolved, projectId, client) {
27275
+ const origin = await readyCustomDomainOrigin(client, projectId);
27276
+ if (origin === void 0) return;
27277
+ const updated = withPortalCustomDomain(
27278
+ await readManifest(resolved.state),
27279
+ origin ?? void 0
27280
+ );
27281
+ if (updated) await writeManifest(resolved.state, updated);
27282
+ }
27220
27283
  async function gatewayDeployInputs(resolved, projectId) {
27221
27284
  const gateway = resolved.gateway;
27222
27285
  if (!gateway) throw new Error("gateway is not configured");
@@ -27226,6 +27289,7 @@ async function gatewayDeployInputs(resolved, projectId) {
27226
27289
  gateway,
27227
27290
  derivedCorsSites({
27228
27291
  portalUrl: manifest?.services.portal?.url,
27292
+ portalCustomDomain: manifest?.services.portal?.customDomain,
27229
27293
  docsUrl: resolved.portal?.docs.provider === "external" ? resolved.portal.docs.url : void 0
27230
27294
  })
27231
27295
  );
@@ -27314,6 +27378,7 @@ function rejected(what, error) {
27314
27378
  throw error;
27315
27379
  }
27316
27380
  async function deployGateway(resolved, projectId, deps, options = {}) {
27381
+ await recordPortalCustomDomain(resolved, projectId, deps.client);
27317
27382
  const { gateway, spec, body, specHash, configHash } = await gatewayDeployInputs(resolved, projectId);
27318
27383
  assertDeployable(gateway);
27319
27384
  const backend = await backendDeployInputs(gateway, projectId);
@@ -28130,16 +28195,9 @@ function identityBrandingBody(resolved) {
28130
28195
  var IDENTITY_BRANDING_STEP = "branding";
28131
28196
  var IDENTITY_PORTAL_ADDRESS_STEP = "portal-address";
28132
28197
  async function customDomainCallbackOf(client, projectId, portalUrl) {
28133
- if (!client.getCustomDomain) return void 0;
28134
- let domain;
28135
- try {
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}`;
28198
+ const origin = await readyCustomDomainOrigin(client, projectId);
28199
+ if (!origin) return void 0;
28200
+ const callback = `${origin}${PORTAL_CALLBACK_PATH}`;
28143
28201
  const current = `${portalUrl.replace(/\/+$/, "")}${PORTAL_CALLBACK_PATH}`;
28144
28202
  return callback.toLowerCase() === current.toLowerCase() ? void 0 : callback;
28145
28203
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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 external docs site
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
@@ -70,6 +70,7 @@ export {
70
70
  withGatewayEntry,
71
71
  withIdentityEntry,
72
72
  withPlansEntry,
73
+ withPortalCustomDomain,
73
74
  withPortalEntry,
74
75
  writeManifest,
75
76
  } from "./manifest.js";
@@ -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.
@@ -169,7 +169,11 @@ export function resolveConfigV2(
169
169
  }
170
170
  : {
171
171
  light: logoSource(configDir, portal.logo.light),
172
- dark: logoSource(configDir, portal.logo.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
  }
@@ -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
- /** The logo in the portal header: one image, or one per color scheme (API-129). */
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
- path,
56
- z.strictObject({ light: path, dark: path, alt: z.string().optional() }),
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 })], {
@@ -71,6 +71,8 @@ const PLANS_CONSOLE_CAPABILITIES = [
71
71
  export type PortalPlan = {
72
72
  /** Docs pages, navigation and search APItoGo builds. */
73
73
  docs: boolean;
74
+ /** The search box the site shows; only for docs APItoGo hosts. Absent: the site has no search. */
75
+ search?: "pagefind";
74
76
  /** OpenAPI reference pages and playground, built from the gateway's OpenAPI file. */
75
77
  apiReference: boolean;
76
78
  /** Where the reference points its calls, and the header of the API key; set when `apiReference` is true. */
@@ -215,6 +217,12 @@ export function planPortal(
215
217
 
216
218
  if (!profile.enabled) return plan;
217
219
 
220
+ // The build already writes the Pagefind index for hosted docs; the site shows the search box only when its config
221
+ // names the search. Inkeep needs keys that `portal.search` cannot carry yet, so it is left out, not half-registered.
222
+ if (profile.pipelines.search === "pagefind") plan.search = "pagefind";
223
+ else if (profile.pipelines.search === "inkeep")
224
+ skip("search", "inkeep needs keys that portal.search cannot hold yet");
225
+
218
226
  // The API page is built from the gateway's OpenAPI file and calls the gateway, so it needs the gateway's address.
219
227
  if (profile.pipelines.openapi) {
220
228
  if (!gateway) skip("api reference", "services.json has no gateway entry");
@@ -390,6 +398,7 @@ export function buildSiteConfig(
390
398
  },
391
399
  }
392
400
  : {}),
401
+ ...(plan.search ? { search: { type: plan.search } } : {}),
393
402
  ...(plan.apiReference
394
403
  ? {
395
404
  apis: [