@lukoweb/apitogo 0.2.35 → 0.2.37
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 +1567 -2709
- package/dist/declarations/cli/platform/client/platform-api.d.ts +9 -0
- package/dist/declarations/lib/authentication/providers/openid.d.ts +6 -1
- package/package.json +1 -1
- package/src/config/v2/home-redirect.ts +8 -0
- package/src/config/v2/index.ts +2 -0
- package/src/config/v2/init.ts +180 -273
- package/src/config/v2/paths.ts +63 -54
- package/src/config/v2/protected-routes.ts +43 -0
- package/src/config/v2/resolve.ts +40 -1
- package/src/config/v2/rules.ts +27 -0
- package/src/config/v2/schema.ts +246 -245
- package/src/config/v2/services.ts +97 -15
- package/src/config/v2/test-project.ts +31 -0
- package/src/config/v2/wrapper.ts +28 -2
- package/src/lib/authentication/providers/openid.tsx +611 -516
- package/src/lib/authentication/providers/supabase.tsx +412 -412
|
@@ -56,6 +56,18 @@ const DEFAULT_RUNTIME_VERSION = {
|
|
|
56
56
|
dotnet: "DOTNET_8",
|
|
57
57
|
} as const;
|
|
58
58
|
|
|
59
|
+
/** What users read first (decision 28); the technical name follows in brackets. */
|
|
60
|
+
export const SERVICE_LABELS: Record<ServiceName, string> = {
|
|
61
|
+
portal: "Developer website",
|
|
62
|
+
gateway: "API protection",
|
|
63
|
+
identity: "Login for API users",
|
|
64
|
+
plans: "Pricing and API keys",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** For example `API protection (gateway)`. */
|
|
68
|
+
export const serviceLabel = (service: ServiceName): string =>
|
|
69
|
+
`${SERVICE_LABELS[service]} (${service})`;
|
|
70
|
+
|
|
59
71
|
export function isServiceName(value: string): value is ServiceName {
|
|
60
72
|
return (SERVICES as readonly string[]).includes(value);
|
|
61
73
|
}
|
|
@@ -69,6 +81,8 @@ export type AddOptions = {
|
|
|
69
81
|
disabled?: boolean;
|
|
70
82
|
/** Include every docs folder found instead of asking. */
|
|
71
83
|
acceptAllDocs?: boolean;
|
|
84
|
+
/** Content to create: `blank` is an empty shell, `starter` a filled example (portal, gateway and plans). */
|
|
85
|
+
template?: "blank" | "starter";
|
|
72
86
|
portal?: {
|
|
73
87
|
name?: string;
|
|
74
88
|
docs?: string[] | false;
|
|
@@ -124,6 +138,8 @@ export type ServiceEdit = {
|
|
|
124
138
|
/** Files created by an add; files that already exist are listed in `keep` and never overwritten. */
|
|
125
139
|
files: PlannedFile[];
|
|
126
140
|
keep: string[];
|
|
141
|
+
/** Files and folders a `remove --delete-files` will delete (absolute paths). */
|
|
142
|
+
deletes: string[];
|
|
127
143
|
/** Human-readable follow-ups: secrets to set, files left behind after a remove. */
|
|
128
144
|
notes: string[];
|
|
129
145
|
};
|
|
@@ -137,6 +153,13 @@ const exists = (target: string) =>
|
|
|
137
153
|
const rel = (dir: string, target: string) =>
|
|
138
154
|
path.relative(dir, target).split(path.sep).join("/") || ".";
|
|
139
155
|
|
|
156
|
+
const BLANK_SPEC = (name: string) => `openapi: 3.1.0
|
|
157
|
+
info:
|
|
158
|
+
title: ${JSON.stringify(name)}
|
|
159
|
+
version: 1.0.0
|
|
160
|
+
paths: {}
|
|
161
|
+
`;
|
|
162
|
+
|
|
140
163
|
const STARTER_SPEC = (name: string) => `openapi: 3.1.0
|
|
141
164
|
info:
|
|
142
165
|
title: ${JSON.stringify(name)}
|
|
@@ -288,11 +311,21 @@ async function buildPortal(
|
|
|
288
311
|
}
|
|
289
312
|
}
|
|
290
313
|
if (content.length === 0) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
let template = options.template;
|
|
315
|
+
if (!template && options.ask)
|
|
316
|
+
template = await options.ask.choose("Content of the new site", [
|
|
317
|
+
{ value: "starter", label: "A starter page I can edit" },
|
|
318
|
+
{ value: "blank", label: "An empty pages folder" },
|
|
319
|
+
]);
|
|
320
|
+
const pages = path.join(resolved.root, "portal", "pages");
|
|
321
|
+
if (template === "blank")
|
|
322
|
+
await addFile(context, path.join(pages, ".gitkeep"), "");
|
|
323
|
+
else
|
|
324
|
+
await addFile(
|
|
325
|
+
context,
|
|
326
|
+
path.join(pages, "index.mdx"),
|
|
327
|
+
starterPage(name),
|
|
328
|
+
);
|
|
296
329
|
}
|
|
297
330
|
}
|
|
298
331
|
|
|
@@ -391,7 +424,9 @@ async function buildGateway(
|
|
|
391
424
|
|
|
392
425
|
let specs: string[];
|
|
393
426
|
let requestedSpecs = o.spec ?? [];
|
|
394
|
-
let createSpec =
|
|
427
|
+
let createSpec =
|
|
428
|
+
o.createSpec === true ||
|
|
429
|
+
(requestedSpecs.length === 0 && options.template !== undefined);
|
|
395
430
|
if (requestedSpecs.length === 0 && !createSpec && ask) {
|
|
396
431
|
const answer = await ask.choose("OpenAPI file of your API", [
|
|
397
432
|
{ value: "create", label: "Start from a small file the command creates" },
|
|
@@ -421,7 +456,13 @@ async function buildGateway(
|
|
|
421
456
|
}
|
|
422
457
|
} else if (createSpec) {
|
|
423
458
|
const target = path.join(resolved.apisDir, "openapi.yaml");
|
|
424
|
-
await addFile(
|
|
459
|
+
await addFile(
|
|
460
|
+
context,
|
|
461
|
+
target,
|
|
462
|
+
options.template === "blank"
|
|
463
|
+
? BLANK_SPEC(resolved.project.name)
|
|
464
|
+
: STARTER_SPEC(resolved.project.name),
|
|
465
|
+
);
|
|
425
466
|
specs = [rel(dir, target)];
|
|
426
467
|
} else {
|
|
427
468
|
throw new ServiceEditError(
|
|
@@ -531,7 +572,13 @@ async function buildPlans(
|
|
|
531
572
|
): Promise<NonNullable<ConfigV2["plans"]>> {
|
|
532
573
|
const { options, dir, resolved } = context;
|
|
533
574
|
const o = options.plans ?? {};
|
|
534
|
-
const template =
|
|
575
|
+
const template =
|
|
576
|
+
o.template ??
|
|
577
|
+
(options.template === "blank"
|
|
578
|
+
? "none"
|
|
579
|
+
: options.template === "starter"
|
|
580
|
+
? "free"
|
|
581
|
+
: "free");
|
|
535
582
|
const billing = o.billing ?? "stripe";
|
|
536
583
|
if (template === "tiered" && billing === "none") {
|
|
537
584
|
throw new ServiceEditError(
|
|
@@ -655,6 +702,7 @@ export async function planAddService(
|
|
|
655
702
|
config: next,
|
|
656
703
|
files: context.files,
|
|
657
704
|
keep: context.keep,
|
|
705
|
+
deletes: [],
|
|
658
706
|
notes: context.notes,
|
|
659
707
|
};
|
|
660
708
|
}
|
|
@@ -665,7 +713,7 @@ export async function planAddService(
|
|
|
665
713
|
*/
|
|
666
714
|
export async function planRemoveService(
|
|
667
715
|
service: ServiceName,
|
|
668
|
-
options: { dir: string },
|
|
716
|
+
options: { dir: string; deleteFiles?: boolean },
|
|
669
717
|
): Promise<ServiceEdit> {
|
|
670
718
|
const dir = path.resolve(options.dir);
|
|
671
719
|
const { config, resolved } = loadConfigV2(dir);
|
|
@@ -690,14 +738,31 @@ export async function planRemoveService(
|
|
|
690
738
|
);
|
|
691
739
|
}
|
|
692
740
|
|
|
693
|
-
const left: string[] = [];
|
|
694
741
|
const r = resolved;
|
|
695
|
-
|
|
696
|
-
if (service === "
|
|
697
|
-
if (service === "
|
|
698
|
-
|
|
742
|
+
const owned: string[] = [];
|
|
743
|
+
if (service === "portal" && r.portal) owned.push(r.portal.dir);
|
|
744
|
+
if (service === "plans" && r.plans) owned.push(r.plans.file);
|
|
745
|
+
if (service === "gateway" && r.gateway) owned.push(...r.gateway.specs);
|
|
699
746
|
void removed;
|
|
700
747
|
|
|
748
|
+
// `--delete-files` only deletes what lives inside the project's own `apitogo/` folder. A docs folder, a backend
|
|
749
|
+
// or a spec that the tenant pointed the config at is theirs and is always kept.
|
|
750
|
+
const inside = (target: string) => {
|
|
751
|
+
const relative = path.relative(resolved.root, target);
|
|
752
|
+
return (
|
|
753
|
+
relative !== "" &&
|
|
754
|
+
!relative.startsWith("..") &&
|
|
755
|
+
!path.isAbsolute(relative)
|
|
756
|
+
);
|
|
757
|
+
};
|
|
758
|
+
const deletes: string[] = [];
|
|
759
|
+
const left: string[] = [];
|
|
760
|
+
for (const target of owned) {
|
|
761
|
+
if (options.deleteFiles && inside(target) && (await exists(target)))
|
|
762
|
+
deletes.push(target);
|
|
763
|
+
else left.push(rel(dir, target));
|
|
764
|
+
}
|
|
765
|
+
|
|
701
766
|
return {
|
|
702
767
|
service,
|
|
703
768
|
action: "remove",
|
|
@@ -706,7 +771,16 @@ export async function planRemoveService(
|
|
|
706
771
|
config: next,
|
|
707
772
|
files: [],
|
|
708
773
|
keep: [],
|
|
709
|
-
|
|
774
|
+
deletes,
|
|
775
|
+
notes: [
|
|
776
|
+
...(left.length > 0
|
|
777
|
+
? [
|
|
778
|
+
options.deleteFiles
|
|
779
|
+
? `Files outside ${rel(dir, resolved.root)}/ are kept: ${left.join(", ")}.`
|
|
780
|
+
: `Files are kept: ${left.join(", ")}. Use --delete-files to delete the ones APItoGo created.`,
|
|
781
|
+
]
|
|
782
|
+
: []),
|
|
783
|
+
],
|
|
710
784
|
};
|
|
711
785
|
}
|
|
712
786
|
|
|
@@ -740,6 +814,8 @@ export async function applyServiceEdit(
|
|
|
740
814
|
}
|
|
741
815
|
}
|
|
742
816
|
await writeConfig(edit.configPath, edit.config);
|
|
817
|
+
for (const target of edit.deletes)
|
|
818
|
+
await rm(target, { recursive: true, force: true });
|
|
743
819
|
return { created };
|
|
744
820
|
}
|
|
745
821
|
|
|
@@ -754,6 +830,12 @@ export function formatServiceEdit(edit: ServiceEdit): string[] {
|
|
|
754
830
|
...edit.files.map((file) => ` ${rel(edit.dir, file.path)}`),
|
|
755
831
|
]
|
|
756
832
|
: []),
|
|
833
|
+
...(edit.deletes.length > 0
|
|
834
|
+
? [
|
|
835
|
+
"Will delete:",
|
|
836
|
+
...edit.deletes.map((file) => ` ${rel(edit.dir, file)}`),
|
|
837
|
+
]
|
|
838
|
+
: []),
|
|
757
839
|
...(edit.keep.length > 0
|
|
758
840
|
? [
|
|
759
841
|
`Already there, left alone: ${edit.keep.map((file) => rel(edit.dir, file)).join(", ")}`,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { applyInitPlan, planInit } from "./init.js";
|
|
4
|
+
import type { Layout } from "./schema.js";
|
|
5
|
+
import { applyServiceEdit, planAddService } from "./services.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A project as the tests of `add`, `check` and `deploy` expect to find it: `init`, then the portal with its starter
|
|
9
|
+
* page. `layout: "standalone"` is the legacy layout, kept so older projects still load.
|
|
10
|
+
*/
|
|
11
|
+
export async function makeProject(
|
|
12
|
+
dir: string,
|
|
13
|
+
options: { layout?: Layout; portal?: boolean; name?: string } = {},
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
await applyInitPlan(await planInit({ dir, name: options.name }));
|
|
16
|
+
const configPath = path.join(dir, "apitogo.config.json");
|
|
17
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
18
|
+
const layout = options.layout ?? "standalone";
|
|
19
|
+
await writeFile(
|
|
20
|
+
configPath,
|
|
21
|
+
`${JSON.stringify({ ...config, layout }, null, 2)}\n`,
|
|
22
|
+
);
|
|
23
|
+
if (options.portal !== false)
|
|
24
|
+
await applyServiceEdit(
|
|
25
|
+
await planAddService("portal", {
|
|
26
|
+
dir,
|
|
27
|
+
template: "starter",
|
|
28
|
+
portal: { docs: false },
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
stubManifest,
|
|
17
17
|
} from "./manifest.js";
|
|
18
18
|
import { type PortalProfile, portalProfile } from "./portal-profile.js";
|
|
19
|
+
import { protectedRoutesFor } from "./protected-routes.js";
|
|
19
20
|
import type { ResolvedConfigV2 } from "./resolve.js";
|
|
20
21
|
|
|
21
22
|
export const WRAPPER_FILE = "apitogo.config.tsx";
|
|
@@ -62,7 +63,12 @@ export type PortalPlan = {
|
|
|
62
63
|
audience?: string;
|
|
63
64
|
};
|
|
64
65
|
/** Pricing, subscriptions, API keys and usage pages. */
|
|
65
|
-
plansPlugin?: {
|
|
66
|
+
plansPlugin?: {
|
|
67
|
+
apiBaseUrl: string;
|
|
68
|
+
keysApiBaseUrl?: string;
|
|
69
|
+
/** The billing provider of the plans service (`none`, `stripe`, ...). `none`: the pages offer no payment. */
|
|
70
|
+
billing?: string;
|
|
71
|
+
};
|
|
66
72
|
/** Account pages. */
|
|
67
73
|
userPanel: boolean;
|
|
68
74
|
/** Public sign-up page; only when identity declares `nativeSignup`. */
|
|
@@ -224,6 +230,7 @@ export function planPortal(
|
|
|
224
230
|
plan.plansPlugin = {
|
|
225
231
|
apiBaseUrl: plans.apiBaseUrl,
|
|
226
232
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
233
|
+
billing: plans.billing,
|
|
227
234
|
};
|
|
228
235
|
}
|
|
229
236
|
|
|
@@ -249,7 +256,11 @@ export function buildSiteConfig(
|
|
|
249
256
|
resolved: ResolvedConfigV2,
|
|
250
257
|
plan: PortalPlan,
|
|
251
258
|
theme?: unknown,
|
|
252
|
-
extras: {
|
|
259
|
+
extras: {
|
|
260
|
+
redirects?: HomeRedirect[];
|
|
261
|
+
docsNavigation?: unknown[];
|
|
262
|
+
protectedRoutes?: string[];
|
|
263
|
+
} = {},
|
|
253
264
|
): Record<string, unknown> {
|
|
254
265
|
const portal = resolved.portal;
|
|
255
266
|
const redirects = extras.redirects ?? [];
|
|
@@ -310,6 +321,9 @@ export function buildSiteConfig(
|
|
|
310
321
|
: {}),
|
|
311
322
|
...(navigation.length > 0 ? { navigation } : {}),
|
|
312
323
|
...(redirects.length > 0 ? { redirects } : {}),
|
|
324
|
+
...(extras.protectedRoutes?.length
|
|
325
|
+
? { protectedRoutes: extras.protectedRoutes }
|
|
326
|
+
: {}),
|
|
313
327
|
...(theme !== undefined ? { theme } : {}),
|
|
314
328
|
...(plan.auth ? { authentication: authenticationFor(plan) } : {}),
|
|
315
329
|
...(plan.preview ? { preview: true } : {}),
|
|
@@ -490,6 +504,10 @@ export async function preparePortal(
|
|
|
490
504
|
externalDocsUrl: profile.docs.url,
|
|
491
505
|
apiReferencePath: plan.apiReference ? `/${API_REFERENCE_PATH}` : undefined,
|
|
492
506
|
userPanel: plan.userPanel,
|
|
507
|
+
pricingPath:
|
|
508
|
+
plan.plansPlugin && hasCapability(effective.services.plans, "catalog")
|
|
509
|
+
? CONSOLE_ROUTES.pricing
|
|
510
|
+
: undefined,
|
|
493
511
|
});
|
|
494
512
|
const siteConfig = buildSiteConfig(
|
|
495
513
|
resolved,
|
|
@@ -498,6 +516,14 @@ export async function preparePortal(
|
|
|
498
516
|
{
|
|
499
517
|
redirects: home ? [home] : [],
|
|
500
518
|
docsNavigation: docsNavigation(hostedDocRoutes),
|
|
519
|
+
protectedRoutes: protectedRoutesFor({
|
|
520
|
+
requireAuth: resolved.portal?.requireAuth,
|
|
521
|
+
hasAuth: !!plan.auth,
|
|
522
|
+
docRoutes: hostedDocRoutes,
|
|
523
|
+
apiReferencePath: plan.apiReference
|
|
524
|
+
? `/${API_REFERENCE_PATH}`
|
|
525
|
+
: undefined,
|
|
526
|
+
}),
|
|
501
527
|
},
|
|
502
528
|
);
|
|
503
529
|
const wrapper = renderWrapper(plan);
|