@o3co/auth-provider-scaffold 0.0.0 → 0.18.0-rc.1

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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -3
  3. package/combinations/README.md +42 -0
  4. package/combinations/src/composed.d.ts +18 -0
  5. package/combinations/src/composed.js +132 -0
  6. package/combinations/src/contract.d.ts +13 -0
  7. package/combinations/src/contract.js +26 -0
  8. package/combinations/src/declared.d.ts +38 -0
  9. package/combinations/src/declared.js +75 -0
  10. package/combinations/src/kinds.d.ts +12 -0
  11. package/combinations/src/kinds.js +11 -0
  12. package/create-app/README.md +54 -0
  13. package/create-app/src/audit-events.d.ts +22 -0
  14. package/create-app/src/audit-events.js +76 -0
  15. package/create-app/src/command.d.ts +5 -0
  16. package/create-app/src/command.js +69 -0
  17. package/create-app/src/configuration.d.ts +44 -0
  18. package/create-app/src/configuration.js +161 -0
  19. package/create-app/src/contract.d.ts +6 -0
  20. package/create-app/src/contract.js +4 -0
  21. package/create-app/src/deployment.d.ts +8 -0
  22. package/create-app/src/deployment.js +100 -0
  23. package/create-app/src/fragments.d.ts +41 -0
  24. package/create-app/src/fragments.js +109 -0
  25. package/create-app/src/full.d.ts +2 -0
  26. package/create-app/src/full.js +18 -0
  27. package/create-app/src/generate.d.ts +21 -0
  28. package/create-app/src/generate.js +33 -0
  29. package/create-app/src/main.d.ts +2 -0
  30. package/create-app/src/main.js +3 -0
  31. package/create-app/src/packages.d.ts +17 -0
  32. package/create-app/src/packages.js +40 -0
  33. package/create-app/src/pattern.d.ts +9 -0
  34. package/create-app/src/pattern.js +31 -0
  35. package/create-app/src/patterns/README.md +26 -0
  36. package/create-app/src/patterns/a2a.d.ts +2 -0
  37. package/create-app/src/patterns/a2a.js +18 -0
  38. package/create-app/src/patterns/a2a.md +7 -0
  39. package/create-app/src/patterns/chosen.d.ts +2 -0
  40. package/create-app/src/patterns/chosen.js +35 -0
  41. package/create-app/src/patterns/device.d.ts +2 -0
  42. package/create-app/src/patterns/device.js +14 -0
  43. package/create-app/src/patterns/device.md +6 -0
  44. package/create-app/src/patterns/mfa-web.d.ts +2 -0
  45. package/create-app/src/patterns/mfa-web.js +25 -0
  46. package/create-app/src/patterns/mfa-web.md +8 -0
  47. package/create-app/src/patterns/native-app.d.ts +2 -0
  48. package/create-app/src/patterns/native-app.js +11 -0
  49. package/create-app/src/patterns/native-app.md +6 -0
  50. package/create-app/src/patterns/pattern.d.ts +28 -0
  51. package/create-app/src/patterns/pattern.js +31 -0
  52. package/create-app/src/patterns/patterns.d.ts +15 -0
  53. package/create-app/src/patterns/patterns.js +49 -0
  54. package/create-app/src/patterns/simple-web.d.ts +2 -0
  55. package/create-app/src/patterns/simple-web.js +34 -0
  56. package/create-app/src/patterns/simple-web.md +6 -0
  57. package/create-app/src/patterns/sso-federated.d.ts +2 -0
  58. package/create-app/src/patterns/sso-federated.js +23 -0
  59. package/create-app/src/patterns/sso-federated.md +6 -0
  60. package/create-app/src/project.d.ts +19 -0
  61. package/create-app/src/project.js +76 -0
  62. package/create-app/src/trial.d.ts +17 -0
  63. package/create-app/src/trial.js +93 -0
  64. package/create-app/src/write-configuration.d.ts +2 -0
  65. package/create-app/src/write-configuration.js +10 -0
  66. package/create-app/templates/full-readme.md +67 -0
  67. package/create-app/templates/main.ts.template +28 -0
  68. package/create-app/templates/pattern-readme.md +70 -0
  69. package/index.json +2709 -0
  70. package/package.json +24 -3
@@ -0,0 +1,18 @@
1
+ /** The full template: what `combinations` composes in full, with the features added. */
2
+ import { full, optIns } from "@o3co/auth-provider-scaffold/combinations";
3
+ import { writeDeployment } from "./deployment.js";
4
+ import { namesPackages } from "./project.js";
5
+ export async function generateFull(plan) {
6
+ const add = plan.add ?? [];
7
+ namesPackages(add);
8
+ const modules = full(add);
9
+ await writeDeployment(plan, modules, {
10
+ template: "full-readme.md",
11
+ fill: {
12
+ optIns: optIns()
13
+ .filter((name) => !add.includes(name))
14
+ .map((name) => `- \`${name}\``)
15
+ .join("\n"),
16
+ },
17
+ });
18
+ }
@@ -0,0 +1,21 @@
1
+ import { type PatternPlan } from "./pattern.js";
2
+ /** A project assembling the modules named. */
3
+ export interface ModulesPlan {
4
+ readonly target: string;
5
+ /** Package names whose `./module` the app assembles, in order; their default wirings and bridges come along. */
6
+ readonly modules: readonly string[];
7
+ /** When set, dependencies link to this workspace instead of a registry. */
8
+ readonly workspace?: string;
9
+ /** The package whose `./module` contributes the app's log sink. */
10
+ readonly sink?: string;
11
+ }
12
+ /** A project from the full template: what `combinations` composes in full, with the features added (the pattern `full`). */
13
+ export interface FullPlan {
14
+ readonly target: string;
15
+ readonly template: "full";
16
+ /** Features of the workspace to compose beside the template's, with their wirings and bridges. */
17
+ readonly add?: readonly string[];
18
+ readonly workspace?: string;
19
+ }
20
+ export type Plan = ModulesPlan | FullPlan | PatternPlan;
21
+ export declare function generate(given: Plan): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { resolve } from "node:path";
2
+ import { completed } from "@o3co/auth-provider-scaffold/combinations";
3
+ import { kernelConfig } from "@o3co/auth-provider-foundation/kernel";
4
+ import { generateFull } from "./full.js";
5
+ import { packagesFor } from "./packages.js";
6
+ import { generatePattern } from "./pattern.js";
7
+ import { fullName } from "./patterns/patterns.js";
8
+ import { mainSource, manifestOf, namesPackages, vacant, withKernel, writeProject, } from "./project.js";
9
+ const defaultSink = "@mod/log-stdout";
10
+ export async function generate(given) {
11
+ // One path, resolved once, is both checked and written to: `missing/..` must not pass as empty.
12
+ const plan = { ...given, target: resolve(given.target) };
13
+ if ("template" in plan) {
14
+ if (plan.template !== fullName)
15
+ return generatePattern(plan);
16
+ if ("axes" in plan || "features" in plan)
17
+ throw new Error("full takes no axes or groups");
18
+ return generateFull({ ...plan, template: fullName });
19
+ }
20
+ const sink = plan.sink ?? defaultSink;
21
+ namesPackages([...plan.modules, sink]);
22
+ await vacant(plan.target);
23
+ const asked = completed(plan.modules);
24
+ // Without a sink, the app's log lines would go nowhere.
25
+ const modules = asked.includes(sink) ? asked : [...asked, sink];
26
+ const packages = packagesFor(plan);
27
+ await writeProject(plan.target, {
28
+ "package.json": manifestOf("node src/main.ts", packages.dependencies(withKernel(modules))),
29
+ "src/main.ts": await mainSource(modules, packages.importOf),
30
+ "config/application.conf": `${JSON.stringify(kernelConfig(), null, "\t")}\n`,
31
+ ".gitignore": "node_modules/\nconfig/local*.conf\n",
32
+ });
33
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./command.js";
3
+ process.exitCode = await run(process.argv.slice(2));
@@ -0,0 +1,17 @@
1
+ import { type Fragment } from "./fragments.js";
2
+ /** Where the packages an app assembles come from, and how the app names them. */
3
+ export interface Packages {
4
+ /** Each name's section example, in the order given; a package without one is skipped. */
5
+ fragments(names: readonly string[]): Promise<Fragment[]>;
6
+ /** What the app depends on to have the names. */
7
+ dependencies(names: readonly string[]): Record<string, string>;
8
+ /** What the app imports a name's entries under. */
9
+ importOf(name: string): string;
10
+ }
11
+ /**
12
+ * The packages as published, from the index, where create-app is built into a bundle and no
13
+ * workspace is given; else the workspace's, linked when one is given.
14
+ */
15
+ export declare function packagesFor(plan: {
16
+ readonly workspace?: string;
17
+ }, index?: string): Packages;
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fragmentsIn, fragmentsOf } from "./fragments.js";
4
+ import { dependenciesOf, workspacePackages } from "./project.js";
5
+ /** The index distribution writes at the root of the bundle create-app is built into. */
6
+ const bundleIndex = join(import.meta.dirname, "..", "..", "index.json");
7
+ /** The workspace this package lies in, where `combinations` reads the modules it composes. */
8
+ const ownWorkspace = join(import.meta.dirname, "..", "..", "..", "..");
9
+ /**
10
+ * The packages as published, from the index, where create-app is built into a bundle and no
11
+ * workspace is given; else the workspace's, linked when one is given.
12
+ */
13
+ export function packagesFor(plan, index = bundleIndex) {
14
+ if (plan.workspace === undefined && existsSync(index)) {
15
+ return publishedIn(JSON.parse(readFileSync(index, "utf8")));
16
+ }
17
+ const dirs = workspacePackages(resolve(plan.workspace ?? ownWorkspace));
18
+ return {
19
+ fragments: (names) => fragmentsIn(names.flatMap((name) => dirs.get(name) ?? [])),
20
+ dependencies: (names) => dependenciesOf(names, plan.workspace ? dirs : undefined),
21
+ importOf: (name) => name,
22
+ };
23
+ }
24
+ function publishedIn(index) {
25
+ const byName = new Map(index.packages.map((entry) => [entry.name, entry]));
26
+ const found = (name) => {
27
+ const entry = byName.get(name);
28
+ if (!entry)
29
+ throw new Error(`${name} is no published package`);
30
+ return entry;
31
+ };
32
+ return {
33
+ fragments: async (names) => fragmentsOf(names.flatMap((name) => {
34
+ const { fragment } = found(name);
35
+ return fragment === undefined ? [] : [{ from: name, text: fragment }];
36
+ })),
37
+ dependencies: (names) => Object.fromEntries(names.map((name) => [found(name).package, index.version])),
38
+ importOf: (name) => found(name).entry,
39
+ };
40
+ }
@@ -0,0 +1,9 @@
1
+ import { type Choice, type Pattern } from "./patterns/pattern.js";
2
+ export interface PatternPlan extends Choice {
3
+ readonly target: string;
4
+ /** The pattern's name. */
5
+ readonly template: string;
6
+ readonly workspace?: string;
7
+ }
8
+ /** Generates the pattern the plan names, or the one given (a test's). */
9
+ export declare function generatePattern(plan: PatternPlan, pattern?: Pattern): Promise<void>;
@@ -0,0 +1,31 @@
1
+ /** A pattern's project: what `combinations` composes from the pattern's selection and the operator's choice. */
2
+ import { composed } from "@o3co/auth-provider-scaffold/combinations";
3
+ import { writeDeployment } from "./deployment.js";
4
+ import { chosenLines } from "./patterns/chosen.js";
5
+ import { oneInstance, selectionOf } from "./patterns/pattern.js";
6
+ import { cardOf, patternNamed } from "./patterns/patterns.js";
7
+ import { namesPackages } from "./project.js";
8
+ /** Generates the pattern the plan names, or the one given (a test's). */
9
+ export async function generatePattern(plan, pattern = patternNamed(plan.template)) {
10
+ const selection = selectionOf(pattern, plan);
11
+ namesPackages([
12
+ ...Object.values(selection.features).flat(),
13
+ ...Object.entries(selection.axes ?? {}).flat(),
14
+ ...(selection.add ?? []),
15
+ ]);
16
+ const modules = composed(selection);
17
+ const deployment = oneInstance(selection)
18
+ ? "`single`: the store is kept in this process, so the deployment runs as one instance."
19
+ : "`single` for one instance, `multi` for several.";
20
+ const card = await cardOf(pattern.name);
21
+ await writeDeployment(plan, modules, {
22
+ template: "pattern-readme.md",
23
+ fill: {
24
+ name: pattern.name,
25
+ use: pattern.use,
26
+ card,
27
+ chosen: chosenLines(modules).join("\n"),
28
+ deployment,
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,26 @@
1
+ # patterns
2
+
3
+ ## Responsibilities
4
+ - Holds the patterns: each a deployment's use and the selection that composes it, in one file (`<name>.ts`) with a card of at most 10 lines (`<name>.md`).
5
+ - Merges the operator's choice into a pattern's selection, and says when its store keeps the deployment to one instance.
6
+
7
+ ## Not here
8
+ - What a selection composes (needs, axes, wirings, bridges) and checking its names and kinds: `combinations`.
9
+ - The files generated and their configuration: `create-app`'s.
10
+ - Running the conformance areas against a pattern's app: the conformance base.
11
+
12
+ ## Offers
13
+ - `patterns()`: each pattern's name, use, conformance areas and the features conformance adds for its run alone (a small reviewed list, so its core is under test), `full` last (the full template itself, with no file here).
14
+ - The pattern of a name, and its card without the title. A name not known is refused, naming those that are.
15
+ - `selectionOf(pattern, choice)`: the features every pattern composes, the pattern's groups with the operator's in their place, the pattern's axes under the operator's, and the features added.
16
+ - The groups: `login`, `second-factor`, `grants`, `client-auth`, `binding`.
17
+
18
+ ## Invariants
19
+ - A pattern names only the features that decide its use; the foundations come with what needs them.
20
+ - A pattern writes no configuration value: its card names the keys the deployment provides for its use; what the store, users, audit and mail chosen ask is told from the modules composed (`chosen`), whatever the pattern's default.
21
+ - A group the operator names that is not one of the groups is refused, naming it.
22
+ - The memory store (`store` chosen as its own axis) means one instance.
23
+ - Module names are written here and in the operator's flags only.
24
+
25
+ ## Depends on
26
+ - `combinations` (the selection's shape).
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const a2a: Pattern;
@@ -0,0 +1,18 @@
1
+ export const a2a = {
2
+ name: "a2a",
3
+ use: "Tokens between services, with no user and no login: clients authenticate by a signed " +
4
+ "assertion or a secret, take client credentials or a JWT bearer grant from a trusted issuer, " +
5
+ "and may bind their tokens to a certificate (mTLS) or a key (DPoP).",
6
+ axes: {
7
+ "@mod/store": "@mod/store",
8
+ "@mod/audit": "@mod/audit",
9
+ "@mod/mail": "@mod/mail",
10
+ },
11
+ features: {
12
+ grants: ["@mod/jwt-bearer"],
13
+ "client-auth": ["@mod/client-assertion"],
14
+ binding: ["@mod/mtls", "@mod/dpop"],
15
+ },
16
+ areas: ["keys", "oauth", "mtls", "ops"],
17
+ conformanceAdd: ["@mod/token-exchange"],
18
+ };
@@ -0,0 +1,7 @@
1
+ # a2a
2
+
3
+ - Use: services only; no user, session or login. Client credentials and the JWT bearer grant (`jwt-bearer.issuers`), clients by `private_key_jwt` or a secret, tokens bound by mTLS or DPoP.
4
+ - The deployment provides the proxy that forwards client certificates (`http.client-certificate`), and lets the issuers' and clients' key sets be fetched (`outbound.allow`).
5
+ - `--add @mod/token-exchange` also brings refresh, session, session tokens and the code grant; `--add @mod/clients-cimd` brings authorization, consent, acr, the code grant, login gates, session and CSRF, and needs `authorize.login-page`.
6
+ - Either brings users, from an outside ledger unless `--axis @mod/users=@mod/users` keeps them in the configuration.
7
+ - Federation grants need a login: `--login @mod/login-password --grants @mod/jwt-bearer,@mod/federation-grants` (a group flag replaces the pattern's group).
@@ -0,0 +1,2 @@
1
+ /** One line for each choice the composed modules show. */
2
+ export declare function chosenLines(modules: readonly string[]): string[];
@@ -0,0 +1,35 @@
1
+ /** What each axis's choice asks of the deployment, told from the modules composed, not the pattern's default. */
2
+ const told = [
3
+ [
4
+ "@mod/store",
5
+ "Store in memory: one instance only (`kernel.deployment = single`), the memory store accepted (`store.accept-volatile = true`).",
6
+ ],
7
+ ["@mod/redis", "Store in Redis (`redis.url`, `redis.replication`)."],
8
+ [
9
+ "@mod/users-http",
10
+ "Users from an outside ledger (`users-http.verify-url`, `users-http.credential`).",
11
+ ],
12
+ [
13
+ "@mod/audit-http",
14
+ "Audit events posted to a receiver (`audit-http.url`, `audit.receivers = [http]`).",
15
+ ],
16
+ [
17
+ "@mod/mail-smtp",
18
+ "Mail through an SMTP relay (`mail-smtp.host`, `mail-smtp.from`; `mail-smtp.user` and `mail-smtp.password` in the local layer when it asks).",
19
+ ],
20
+ ["@mod/mail", "No mail is sent (`mail.accept-unsent = true`)."],
21
+ ];
22
+ /** Said when the owner is composed and no adapter of it is. */
23
+ const ownerOnly = [
24
+ ["@mod/users", "@mod/users-http", "Users in the configuration (`users.registered`)."],
25
+ ["@mod/audit", "@mod/audit-http", "Audit events to the log (`audit.receivers = [log]`)."],
26
+ ];
27
+ /** One line for each choice the composed modules show. */
28
+ export function chosenLines(modules) {
29
+ return [
30
+ ...told.filter(([name]) => modules.includes(name)).map(([, line]) => `- ${line}`),
31
+ ...ownerOnly
32
+ .filter(([owner, adapter]) => modules.includes(owner) && !modules.includes(adapter))
33
+ .map(([, , line]) => `- ${line}`),
34
+ ];
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const device: Pattern;
@@ -0,0 +1,14 @@
1
+ import { simpleWeb } from "./simple-web.js";
2
+ export const device = {
3
+ name: "device",
4
+ use: "Simple-web with the device authorization grant, for devices without a browser or a keyboard: " +
5
+ "the user reads a code on the device and approves it after logging in on another screen. " +
6
+ "It needs no outside system.",
7
+ axes: simpleWeb.axes,
8
+ features: {
9
+ ...simpleWeb.features,
10
+ grants: [...(simpleWeb.features.grants ?? []), "@mod/device-grant"],
11
+ },
12
+ areas: ["keys", "identity", "code", "oauth", "device", "ops"],
13
+ conformanceAdd: [],
14
+ };
@@ -0,0 +1,6 @@
1
+ # device
2
+
3
+ - Use: devices without a browser or a keyboard; the user approves a code on another screen. Also the code flow with refresh, and client credentials.
4
+ - Composes: simple-web's login and code flow (userinfo included), and the device authorization grant with its approval page.
5
+ - The deployment serves the login page (`authorize.login-page`), the consent page (`consent.page`) only for clients that are not first-party, and names where users enter the code (`device-grant.verification-uri`).
6
+ - No outside system. A client's back-channel logout goes only to the hosts in `outbound.allow`.
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const mfaWeb: Pattern;
@@ -0,0 +1,25 @@
1
+ import { simpleWeb } from "./simple-web.js";
2
+ export const mfaWeb = {
3
+ name: "mfa-web",
4
+ use: "Simple-web with a second factor: a one-time password app (TOTP), a code by email, and " +
5
+ "recovery codes. Its store is in Redis, since losing it loses the factors users have " +
6
+ "registered; mail goes through an SMTP relay.",
7
+ axes: {
8
+ ...simpleWeb.axes,
9
+ "@mod/store": "@mod/redis",
10
+ "@mod/mail": "@mod/mail-smtp",
11
+ },
12
+ features: {
13
+ ...simpleWeb.features,
14
+ "second-factor": [
15
+ "@mod/mfa",
16
+ "@mod/mfa-flows",
17
+ "@mod/mfa-acr",
18
+ "@mod/mfa-totp",
19
+ "@mod/mfa-email",
20
+ "@mod/mfa-recovery",
21
+ ],
22
+ },
23
+ areas: ["keys", "identity", "code", "oauth", "second-factor", "ops"],
24
+ conformanceAdd: [],
25
+ };
@@ -0,0 +1,8 @@
1
+ # mfa-web
2
+
3
+ - Use: a web login by password with a second factor (TOTP, a code by email, recovery codes); the code flow with refresh, and client credentials.
4
+ - Composes: simple-web's login and code flow (userinfo included), the second factor and its flows (enrolment, login, reset, lock release), and the acr it meets.
5
+ - Its store is in Redis: losing it loses the registered factors.
6
+ - Factors are sealed with `seal.keys`.
7
+ - Whether a second factor is required: `mfa.mode`.
8
+ - The deployment serves the login page (`authorize.login-page`), the page that raises authentication (`authorize.step-up-page`), and the consent page (`consent.page`) only for clients that are not first-party.
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const nativeApp: Pattern;
@@ -0,0 +1,11 @@
1
+ import { simpleWeb } from "./simple-web.js";
2
+ export const nativeApp = {
3
+ name: "native-app",
4
+ use: "A login for native and single-page apps: public clients use the authorization code flow " +
5
+ "with PKCE and refresh, and may bind their tokens to a key they hold (DPoP). It needs no " +
6
+ "outside system.",
7
+ axes: simpleWeb.axes,
8
+ features: { ...simpleWeb.features, binding: ["@mod/dpop"] },
9
+ areas: ["keys", "identity", "code", "oauth", "ops"],
10
+ conformanceAdd: [],
11
+ };
@@ -0,0 +1,6 @@
1
+ # native-app
2
+
3
+ - Use: native and single-page apps as public clients (no secret), by the code flow with PKCE and refresh.
4
+ - Composes: simple-web's login and code flow (userinfo included), and DPoP, so a client may bind its tokens to a key it holds.
5
+ - The deployment serves the login page (`authorize.login-page`), and the consent page (`consent.page`) only for clients that are not first-party.
6
+ - No outside system. A client's back-channel logout goes only to the hosts in `outbound.allow`.
@@ -0,0 +1,28 @@
1
+ import type { Selection } from "@o3co/auth-provider-scaffold/combinations";
2
+ /** The groups a pattern's features fall in, each overridden whole by the operator's flag of that name. */
3
+ export declare const groups: readonly ["login", "second-factor", "grants", "client-auth", "binding"];
4
+ export type Group = (typeof groups)[number];
5
+ /** A deployment's use, and the selection that composes it. */
6
+ export interface Pattern {
7
+ readonly name: string;
8
+ /** One paragraph: the generated guide opens with it. */
9
+ readonly use: string;
10
+ /** Axis owner → its adapter, or the owner itself; an axis left out keeps full's default. */
11
+ readonly axes: Readonly<Record<string, string>>;
12
+ /** The features that decide the use; what they need comes with them. */
13
+ readonly features: Readonly<Partial<Record<Group, readonly string[]>>>;
14
+ /** The conformance areas run against the pattern's app. */
15
+ readonly areas: readonly string[];
16
+ /** Features conformance adds for the pattern's run alone, so its core is under test; reviewed, kept small. */
17
+ readonly conformanceAdd: readonly string[];
18
+ }
19
+ /** What the operator changes: axes over the pattern's, groups in place of its, features added. */
20
+ export interface Choice {
21
+ readonly axes?: Readonly<Record<string, string>>;
22
+ readonly features?: Readonly<Record<string, readonly string[]>>;
23
+ readonly add?: readonly string[];
24
+ }
25
+ /** The pattern's selection with the operator's choice; a group not known is refused, naming it. */
26
+ export declare function selectionOf(pattern: Pattern, choice?: Choice): Selection;
27
+ /** Whether the selection's store lives in the process, so the deployment runs as one instance. */
28
+ export declare const oneInstance: (selection: Selection) => boolean;
@@ -0,0 +1,31 @@
1
+ /** The groups a pattern's features fall in, each overridden whole by the operator's flag of that name. */
2
+ export const groups = ["login", "second-factor", "grants", "client-auth", "binding"];
3
+ const isGroup = (name) => groups.includes(name);
4
+ /** The features every pattern composes; the foundations come with what needs them. */
5
+ const common = [
6
+ "@mod/issuer",
7
+ "@mod/discovery",
8
+ "@mod/tokens",
9
+ "@mod/sender-binding",
10
+ "@mod/token-endpoint",
11
+ "@mod/token-control",
12
+ "@mod/clients",
13
+ "@mod/subject-revocation",
14
+ "@mod/subject-tokens",
15
+ ];
16
+ /** The store kept in this process: safe on one instance only. */
17
+ const memoryStore = "@mod/store";
18
+ /** The pattern's selection with the operator's choice; a group not known is refused, naming it. */
19
+ export function selectionOf(pattern, choice = {}) {
20
+ const unknown = Object.keys(choice.features ?? {}).filter((group) => !isGroup(group));
21
+ if (unknown.length > 0) {
22
+ throw new Error(`${unknown.join(", ")}: not a group (${groups.join(", ")})`);
23
+ }
24
+ return {
25
+ features: { common, ...pattern.features, ...choice.features },
26
+ axes: { ...pattern.axes, ...choice.axes },
27
+ add: choice.add ?? [],
28
+ };
29
+ }
30
+ /** Whether the selection's store lives in the process, so the deployment runs as one instance. */
31
+ export const oneInstance = (selection) => selection.axes?.[memoryStore] === memoryStore;
@@ -0,0 +1,15 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const fullName = "full";
3
+ /** A pattern's name, its use and the conformance areas run against its app. */
4
+ export interface PatternSummary {
5
+ readonly name: string;
6
+ readonly use: string;
7
+ readonly areas: readonly string[];
8
+ /** What conformance adds for the pattern's run; none when left out. */
9
+ readonly conformanceAdd?: readonly string[];
10
+ }
11
+ export declare function patterns(): PatternSummary[];
12
+ /** The pattern of this name; one not known is refused, naming those that are. */
13
+ export declare function patternNamed(name: string): Pattern;
14
+ /** The pattern's card without its title: what it composes, and what the deployment provides. */
15
+ export declare function cardOf(name: string): Promise<string>;
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { a2a } from "./a2a.js";
4
+ import { device } from "./device.js";
5
+ import { mfaWeb } from "./mfa-web.js";
6
+ import { nativeApp } from "./native-app.js";
7
+ import { simpleWeb } from "./simple-web.js";
8
+ import { ssoFederated } from "./sso-federated.js";
9
+ /** The patterns besides full, which is `combinations`' full template itself. */
10
+ const known = [simpleWeb, mfaWeb, device, nativeApp, a2a, ssoFederated];
11
+ export const fullName = "full";
12
+ const fullSummary = {
13
+ name: fullName,
14
+ use: "Every feature that does not opt in, as the previous release composed by default, with its " +
15
+ "store in Redis, users from an outside ledger and audit events posted to a receiver.",
16
+ areas: [
17
+ "keys",
18
+ "identity",
19
+ "code",
20
+ "oauth",
21
+ "second-factor",
22
+ "device",
23
+ "mtls",
24
+ "passkey",
25
+ "exchange",
26
+ "ops",
27
+ ],
28
+ conformanceAdd: [],
29
+ };
30
+ export function patterns() {
31
+ return [
32
+ ...known.map(({ name, use, areas, conformanceAdd }) => ({ name, use, areas, conformanceAdd })),
33
+ fullSummary,
34
+ ];
35
+ }
36
+ /** The pattern of this name; one not known is refused, naming those that are. */
37
+ export function patternNamed(name) {
38
+ const found = known.find((p) => p.name === name);
39
+ if (!found) {
40
+ const names = patterns().map((p) => p.name);
41
+ throw new Error(`${name}: no such pattern (${names.join(", ")})`);
42
+ }
43
+ return found;
44
+ }
45
+ /** The pattern's card without its title: what it composes, and what the deployment provides. */
46
+ export async function cardOf(name) {
47
+ const card = await readFile(join(import.meta.dirname, `${name}.md`), "utf8");
48
+ return card.split("\n").slice(1).join("\n").trim();
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const simpleWeb: Pattern;
@@ -0,0 +1,34 @@
1
+ export const simpleWeb = {
2
+ name: "simple-web",
3
+ use: "A web login by password for clients that use the authorization code flow with refresh, and " +
4
+ "for services that use client credentials. It needs no outside system: the store is kept in " +
5
+ "memory, users are listed in the configuration, and audit events go to the log.",
6
+ axes: {
7
+ "@mod/store": "@mod/store",
8
+ "@mod/users": "@mod/users",
9
+ "@mod/audit": "@mod/audit",
10
+ "@mod/mail": "@mod/mail",
11
+ },
12
+ features: {
13
+ login: [
14
+ "@mod/login-password",
15
+ "@mod/login-gates",
16
+ "@mod/csrf",
17
+ "@mod/session",
18
+ "@mod/logout",
19
+ "@mod/rp-logout",
20
+ ],
21
+ grants: [
22
+ "@mod/authorize",
23
+ "@mod/code-grant",
24
+ "@mod/consent",
25
+ "@mod/oidc",
26
+ "@mod/acr",
27
+ "@mod/session-tokens",
28
+ "@mod/refresh",
29
+ "@mod/userinfo",
30
+ ],
31
+ },
32
+ areas: ["keys", "identity", "code", "oauth", "ops"],
33
+ conformanceAdd: [],
34
+ };
@@ -0,0 +1,6 @@
1
+ # simple-web
2
+
3
+ - Use: a web login by password; clients use the code flow with refresh, services client credentials.
4
+ - Composes: the login and its session, logout (a client's too), authorization, consent, OpenID Connect with its userinfo, refresh, and the token endpoint with introspection and revocation.
5
+ - The deployment serves the login page (`authorize.login-page`), and the consent page (`consent.page`) only for clients that are not first-party.
6
+ - No outside system. A client's back-channel logout goes only to the hosts in `outbound.allow`.
@@ -0,0 +1,2 @@
1
+ import type { Pattern } from "./pattern.js";
2
+ export declare const ssoFederated: Pattern;
@@ -0,0 +1,23 @@
1
+ import { simpleWeb } from "./simple-web.js";
2
+ export const ssoFederated = {
3
+ name: "sso-federated",
4
+ use: "Single sign-on through upstream identity providers (OpenID Connect, Google, GitHub, Apple), " +
5
+ "with no password of its own: clients use the authorization code flow with refresh, and " +
6
+ "services client credentials. Users come from an outside ledger.",
7
+ axes: { ...simpleWeb.axes, "@mod/users": "@mod/users-http" },
8
+ features: {
9
+ login: [
10
+ "@mod/upstreams",
11
+ "@mod/idp-oidc",
12
+ "@mod/idp-google",
13
+ "@mod/idp-github",
14
+ "@mod/idp-apple",
15
+ "@mod/federation-tokens",
16
+ "@mod/logout",
17
+ "@mod/rp-logout",
18
+ ],
19
+ grants: simpleWeb.features.grants ?? [],
20
+ },
21
+ areas: ["keys", "identity", "code", "oauth", "ops"],
22
+ conformanceAdd: ["@mod/login-password"],
23
+ };
@@ -0,0 +1,6 @@
1
+ # sso-federated
2
+
3
+ - Use: single sign-on through upstream identity providers, with no password of its own; the code flow with refresh, and client credentials.
4
+ - Composes: federation with OpenID Connect, Google, GitHub and Apple, its upstream tokens, logout (a client's too, sent on upstream), authorization, consent, OpenID Connect with its userinfo, and refresh.
5
+ - The providers are named in `federation.idps`. Users come from an outside ledger; `--axis @mod/users=@mod/users` lists them in the configuration instead. A password login is not composed: `--login` replaces this group, so it lists the federation modules with `@mod/login-password`.
6
+ - The deployment serves the login page that offers the providers (`authorize.login-page`), and the consent page (`consent.page`) only for clients that are not first-party; the providers and the ledger are reached through `outbound.allow`.
@@ -0,0 +1,19 @@
1
+ export declare const kernel = "@mod/kernel";
2
+ /** Refuses, before anything is written, a name the app could not import or depend on. */
3
+ export declare function namesPackages(names: readonly string[]): void;
4
+ /** A reading's catch: a missing file or directory reads as `absent`, and any other failure is thrown on. */
5
+ export declare const missingAs: <T>(absent: T) => (error: NodeJS.ErrnoException) => T;
6
+ /** A target that is missing or empty; one that holds anything is refused, never overwritten. */
7
+ export declare function vacant(target: string): Promise<void>;
8
+ /** Package directories by name, as the workspace definition places them. */
9
+ export declare function workspacePackages(root: string): Map<string, string>;
10
+ /** Each name's dependency: a link to its workspace directory when given a workspace, else any version. */
11
+ export declare function dependenciesOf(names: readonly string[], known: ReadonlyMap<string, string> | undefined): Record<string, string>;
12
+ export declare const manifestOf: (start: string, dependencies: Record<string, string>) => string;
13
+ /** Fills a file of `templates/` by its `{{name}}` marks; a mark with no value stays as written. */
14
+ export declare function filled(template: string, fill: Readonly<Record<string, string>>): Promise<string>;
15
+ /** The app's entry, importing each module under what `importOf` names it (the workspace's name, unless published). */
16
+ export declare const mainSource: (modules: readonly string[], importOf?: (name: string) => string) => Promise<string>;
17
+ export declare const withKernel: (modules: readonly string[]) => string[];
18
+ /** Writes every file, by its path in the project, once all are made: nothing is written for a project that fails. */
19
+ export declare function writeProject(target: string, files: Readonly<Record<string, string>>): Promise<void>;