@lukoweb/apitogo 0.2.36 → 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 +1558 -2704
- 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
package/src/config/v2/paths.ts
CHANGED
|
@@ -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(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
package/src/config/v2/resolve.ts
CHANGED
|
@@ -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 ?? "
|
|
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
|
}
|
package/src/config/v2/rules.ts
CHANGED
|
@@ -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({
|