@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 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);
@@ -11166,7 +11199,7 @@ import {
11166
11199
  // package.json
11167
11200
  var package_default = {
11168
11201
  name: "@lukoweb/apitogo",
11169
- version: "0.2.58",
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
- 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}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
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 })], {