@lukoweb/apitogo 0.2.36 → 0.2.38

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.
@@ -1,54 +1,63 @@
1
- import path from "node:path";
2
- import type { Layout } from "./schema.js";
3
-
4
- /** Names inside the generated-state directory (Tenant Service Model, section 4). All of it is regenerable. */
5
- export const STATE_FILE = "state.json";
6
- /** Service manifest written by `deploy` (Reference, section 4). Public values only. */
7
- export const MANIFEST_FILE = "services.json";
8
- export const GENERATED_DIR = "generated";
9
- export const BUILD_DIR = "build";
10
-
11
- /**
12
- * Directory that holds the tenant-authored content for a layout: the config's own directory for `standalone`,
13
- * an `apitogo/` folder next to it for `embedded`.
14
- */
15
- export function layoutRoot(configDir: string, layout: Layout): string {
16
- return layout === "embedded" ? path.join(configDir, "apitogo") : configDir;
17
- }
18
-
19
- /**
20
- * Directory for generated and observed state. It carries its own `.gitignore` containing `*`, so the tenant's root
21
- * `.gitignore` never needs to change.
22
- */
23
- export function stateDirFor(configDir: string, layout: Layout): string {
24
- return layout === "embedded"
25
- ? path.join(configDir, "apitogo", ".state")
26
- : path.join(configDir, ".apitogo");
27
- }
28
-
29
- /** Path of the service manifest inside a state directory. */
30
- export const manifestPathFor = (state: { dir: string }): string =>
31
- path.join(state.dir, MANIFEST_FILE);
32
-
33
- export type StatePaths = {
34
- dir: string;
35
- /** Per-service ledger and artifact hashes. */
36
- file: string;
37
- /** Generated portal wrapper, resolved config snapshot, processed OpenAPI schemas. */
38
- generatedDir: string;
39
- /** Build output; replaces the old root `dist/`. */
40
- buildDir: string;
41
- /** The state directory's own ignore file. */
42
- gitignore: string;
43
- };
44
-
45
- export function statePathsFor(configDir: string, layout: Layout): StatePaths {
46
- const dir = stateDirFor(configDir, layout);
47
- return {
48
- dir,
49
- file: path.join(dir, STATE_FILE),
50
- generatedDir: path.join(dir, GENERATED_DIR),
51
- buildDir: path.join(dir, BUILD_DIR),
52
- gitignore: path.join(dir, ".gitignore"),
53
- };
54
- }
1
+ import path from "node:path";
2
+ import type { Layout } from "./schema.js";
3
+
4
+ /** Names inside the generated-state directory (Tenant Service Model, section 4). All of it is regenerable. */
5
+ export const STATE_FILE = "state.json";
6
+ /** Service manifest written by `deploy` (Reference, section 4). Public values only. */
7
+ export const MANIFEST_FILE = "services.json";
8
+ export const GENERATED_DIR = "generated";
9
+ export const BUILD_DIR = "build";
10
+
11
+ /**
12
+ * Directory that holds the tenant-authored content for a layout: the config's own directory for `standalone`,
13
+ * an `apitogo/` folder next to it for `embedded`.
14
+ */
15
+ export function layoutRoot(
16
+ configDir: string,
17
+ layout: Layout = "embedded",
18
+ ): string {
19
+ return layout === "embedded" ? path.join(configDir, "apitogo") : configDir;
20
+ }
21
+
22
+ /**
23
+ * Directory for generated and observed state. It carries its own `.gitignore` containing `*`, so the tenant's root
24
+ * `.gitignore` never needs to change.
25
+ */
26
+ export function stateDirFor(
27
+ configDir: string,
28
+ layout: Layout = "embedded",
29
+ ): string {
30
+ return layout === "embedded"
31
+ ? path.join(configDir, "apitogo", ".state")
32
+ : path.join(configDir, ".apitogo");
33
+ }
34
+
35
+ /** Path of the service manifest inside a state directory. */
36
+ export const manifestPathFor = (state: { dir: string }): string =>
37
+ path.join(state.dir, MANIFEST_FILE);
38
+
39
+ export type StatePaths = {
40
+ dir: string;
41
+ /** Per-service ledger and artifact hashes. */
42
+ file: string;
43
+ /** Generated portal wrapper, resolved config snapshot, processed OpenAPI schemas. */
44
+ generatedDir: string;
45
+ /** Build output; replaces the old root `dist/`. */
46
+ buildDir: string;
47
+ /** The state directory's own ignore file. */
48
+ gitignore: string;
49
+ };
50
+
51
+ export function statePathsFor(
52
+ configDir: string,
53
+ layout: Layout = "embedded",
54
+ ): StatePaths {
55
+ const dir = stateDirFor(configDir, layout);
56
+ return {
57
+ dir,
58
+ file: path.join(dir, STATE_FILE),
59
+ generatedDir: path.join(dir, GENERATED_DIR),
60
+ buildDir: path.join(dir, BUILD_DIR),
61
+ gitignore: path.join(dir, ".gitignore"),
62
+ };
63
+ }
@@ -0,0 +1,43 @@
1
+ export type RequireAuthSetting = boolean | { exclude: string[] } | undefined;
2
+
3
+ export type ProtectedRoutesInput = {
4
+ requireAuth: RequireAuthSetting;
5
+ /** The portal can sign people in (the identity or an OpenID provider is wired). Without it nothing can be protected. */
6
+ hasAuth: boolean;
7
+ /** Routes of the hosted docs pages (see `docRoutes`). */
8
+ docRoutes: string[];
9
+ /** The API reference page, at this path. */
10
+ apiReferencePath?: string;
11
+ };
12
+
13
+ /**
14
+ * The pages a visitor must sign in to see when `portal.requireAuth` is on: the home page, the hosted docs pages and the
15
+ * API reference. The generated site config can only list paths to protect, not "everything except", and the sign-in
16
+ * pages and the return address (`/oauth/callback`) must stay open or signing in would send the visitor in a circle, so
17
+ * the list names the content pages. The console pages that need a session (account, keys, usage) are protected by
18
+ * their own plugins; pricing stays public. `exclude` takes pages out again (an entry also covers the pages below it).
19
+ * This is a soft gate: the site is static, the HTML of the pages is still published (API-79).
20
+ */
21
+ export function protectedRoutesFor(
22
+ input: ProtectedRoutesInput,
23
+ ): string[] | undefined {
24
+ const { requireAuth } = input;
25
+ if (!input.hasAuth || requireAuth === undefined || requireAuth === false)
26
+ return undefined;
27
+
28
+ const excluded =
29
+ typeof requireAuth === "object"
30
+ ? requireAuth.exclude.map((route) => route.replace(/\/+$/, "") || "/")
31
+ : [];
32
+ const isExcluded = (route: string) =>
33
+ excluded.some(
34
+ (e) => route === e || (e !== "/" && route.startsWith(`${e}/`)),
35
+ );
36
+
37
+ const routes = new Set<string>(["/", ...input.docRoutes]);
38
+ if (input.apiReferencePath) {
39
+ routes.add(input.apiReferencePath);
40
+ routes.add(`${input.apiReferencePath}/*`);
41
+ }
42
+ return [...routes].filter((route) => !isExcluded(route));
43
+ }
@@ -101,7 +101,7 @@ export function resolveConfigV2(
101
101
  ): ResolvedConfigV2 {
102
102
  const absoluteConfigPath = path.resolve(configPath);
103
103
  const configDir = path.dirname(absoluteConfigPath);
104
- const layout = config.layout ?? "standalone";
104
+ const layout = config.layout ?? "embedded";
105
105
  const root = layoutRoot(configDir, layout);
106
106
  const projectName = config.project?.name ?? path.basename(configDir);
107
107
 
@@ -206,5 +206,44 @@ export function resolveConfigV2(
206
206
  };
207
207
  }
208
208
 
209
+ // With a sign-in provider of the customer's own there is no identity of the platform for the gateway to read the
210
+ // tokens from, so `auth.mode: identity` means "the tokens of that provider": its issuer is the identity's issuer, its
211
+ // metadata is the provider's standard document, and the audience is the client id (what an OpenID sign-in token carries).
212
+ // A config that spells these out itself uses `auth.mode: oidc` and is never changed (API-97).
213
+ if (
214
+ resolved.gateway?.auth?.mode === "identity" &&
215
+ resolved.identity?.enabled &&
216
+ resolved.identity.provider === "oidc" &&
217
+ resolved.identity.issuer
218
+ ) {
219
+ const issuer = resolved.identity.issuer;
220
+ resolved.gateway.auth = {
221
+ mode: "oidc",
222
+ metadataUrl: `${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`,
223
+ issuers: [issuer],
224
+ ...(resolved.identity.clientId
225
+ ? { audiences: [resolved.identity.clientId] }
226
+ : {}),
227
+ };
228
+ }
229
+
230
+ // Without plans there are no keys handed out, so a valid login token is the credential. Before this default the
231
+ // gateway also demanded an API key and a good token alone got 401 (API-76, API-80). A config that sets
232
+ // `access.apiKey` itself, or that switches the token check off (a key-only gateway), is never changed.
233
+ if (
234
+ resolved.gateway &&
235
+ !resolved.plans?.enabled &&
236
+ resolved.gateway.auth !== undefined
237
+ ) {
238
+ const access = resolved.gateway.policies.access;
239
+ const tokenChecked = access?.jwt ?? true;
240
+ if (tokenChecked && access?.apiKey === undefined) {
241
+ resolved.gateway.policies = {
242
+ ...resolved.gateway.policies,
243
+ access: { ...access, jwt: true, apiKey: false },
244
+ };
245
+ }
246
+ }
247
+
209
248
  return resolved;
210
249
  }
@@ -73,6 +73,33 @@ 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
+ if (identityOn && resolved.portal?.enabled !== true && !gatewayOn) {
78
+ out.error({
79
+ code: "NOTHING_TO_HOST",
80
+ path: "identity",
81
+ message:
82
+ "identity needs the portal or the gateway: a project with only identity cannot be created yet.",
83
+ hint: "Enable the gateway (apitogo add gateway) or the portal (apitogo add portal) next to identity.",
84
+ });
85
+ }
86
+
87
+ // The plans backend signs customers in through the platform's own identity and gets no connection to a provider of the
88
+ // customer's own, so the portal's login page would have no sign-in method at all (API-96).
89
+ if (
90
+ resolved.plans?.enabled &&
91
+ identityOn &&
92
+ resolved.identity?.provider === "oidc"
93
+ ) {
94
+ out.error({
95
+ code: "PLANS_OWN_PROVIDER_UNSUPPORTED",
96
+ path: "plans",
97
+ message:
98
+ "plans cannot be used with your own sign-in provider yet (identity.provider is oidc): the plans backend signs customers in through the platform's identity, so nobody could sign in.",
99
+ hint: 'Use identity.provider "apitogo", or remove the plans section.',
100
+ });
101
+ }
102
+
76
103
  if (resolved.plans?.enabled) {
77
104
  if (!gatewayOn) {
78
105
  out.error({