@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.
- package/LICENSE +201 -0
- package/README.md +2 -3
- package/combinations/README.md +42 -0
- package/combinations/src/composed.d.ts +18 -0
- package/combinations/src/composed.js +132 -0
- package/combinations/src/contract.d.ts +13 -0
- package/combinations/src/contract.js +26 -0
- package/combinations/src/declared.d.ts +38 -0
- package/combinations/src/declared.js +75 -0
- package/combinations/src/kinds.d.ts +12 -0
- package/combinations/src/kinds.js +11 -0
- package/create-app/README.md +54 -0
- package/create-app/src/audit-events.d.ts +22 -0
- package/create-app/src/audit-events.js +76 -0
- package/create-app/src/command.d.ts +5 -0
- package/create-app/src/command.js +69 -0
- package/create-app/src/configuration.d.ts +44 -0
- package/create-app/src/configuration.js +161 -0
- package/create-app/src/contract.d.ts +6 -0
- package/create-app/src/contract.js +4 -0
- package/create-app/src/deployment.d.ts +8 -0
- package/create-app/src/deployment.js +100 -0
- package/create-app/src/fragments.d.ts +41 -0
- package/create-app/src/fragments.js +109 -0
- package/create-app/src/full.d.ts +2 -0
- package/create-app/src/full.js +18 -0
- package/create-app/src/generate.d.ts +21 -0
- package/create-app/src/generate.js +33 -0
- package/create-app/src/main.d.ts +2 -0
- package/create-app/src/main.js +3 -0
- package/create-app/src/packages.d.ts +17 -0
- package/create-app/src/packages.js +40 -0
- package/create-app/src/pattern.d.ts +9 -0
- package/create-app/src/pattern.js +31 -0
- package/create-app/src/patterns/README.md +26 -0
- package/create-app/src/patterns/a2a.d.ts +2 -0
- package/create-app/src/patterns/a2a.js +18 -0
- package/create-app/src/patterns/a2a.md +7 -0
- package/create-app/src/patterns/chosen.d.ts +2 -0
- package/create-app/src/patterns/chosen.js +35 -0
- package/create-app/src/patterns/device.d.ts +2 -0
- package/create-app/src/patterns/device.js +14 -0
- package/create-app/src/patterns/device.md +6 -0
- package/create-app/src/patterns/mfa-web.d.ts +2 -0
- package/create-app/src/patterns/mfa-web.js +25 -0
- package/create-app/src/patterns/mfa-web.md +8 -0
- package/create-app/src/patterns/native-app.d.ts +2 -0
- package/create-app/src/patterns/native-app.js +11 -0
- package/create-app/src/patterns/native-app.md +6 -0
- package/create-app/src/patterns/pattern.d.ts +28 -0
- package/create-app/src/patterns/pattern.js +31 -0
- package/create-app/src/patterns/patterns.d.ts +15 -0
- package/create-app/src/patterns/patterns.js +49 -0
- package/create-app/src/patterns/simple-web.d.ts +2 -0
- package/create-app/src/patterns/simple-web.js +34 -0
- package/create-app/src/patterns/simple-web.md +6 -0
- package/create-app/src/patterns/sso-federated.d.ts +2 -0
- package/create-app/src/patterns/sso-federated.js +23 -0
- package/create-app/src/patterns/sso-federated.md +6 -0
- package/create-app/src/project.d.ts +19 -0
- package/create-app/src/project.js +76 -0
- package/create-app/src/trial.d.ts +17 -0
- package/create-app/src/trial.js +93 -0
- package/create-app/src/write-configuration.d.ts +2 -0
- package/create-app/src/write-configuration.js +10 -0
- package/create-app/templates/full-readme.md +67 -0
- package/create-app/templates/main.ts.template +28 -0
- package/create-app/templates/pattern-readme.md +70 -0
- package/index.json +2709 -0
- package/package.json +24 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** What every generated project shares: the target's check, the workspace's packages and the app's entry. */
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
export const kernel = "@mod/kernel";
|
|
6
|
+
/** An npm package name, scoped or not: what the app's imports and dependencies can name. */
|
|
7
|
+
const packageName = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
8
|
+
/** Refuses, before anything is written, a name the app could not import or depend on. */
|
|
9
|
+
export function namesPackages(names) {
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
if (!packageName.test(name) || name.length > 214) {
|
|
12
|
+
throw new Error(`${name} is not a package name`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** A reading's catch: a missing file or directory reads as `absent`, and any other failure is thrown on. */
|
|
17
|
+
export const missingAs = (absent) => (error) => {
|
|
18
|
+
if (error.code === "ENOENT")
|
|
19
|
+
return absent;
|
|
20
|
+
throw error;
|
|
21
|
+
};
|
|
22
|
+
/** A target that is missing or empty; one that holds anything is refused, never overwritten. */
|
|
23
|
+
export async function vacant(target) {
|
|
24
|
+
const held = await readdir(target).catch(missingAs([]));
|
|
25
|
+
if (held.length > 0)
|
|
26
|
+
throw new Error(`${target} is not empty`);
|
|
27
|
+
}
|
|
28
|
+
/** Package directories by name, as the workspace definition places them. */
|
|
29
|
+
export function workspacePackages(root) {
|
|
30
|
+
const listed = JSON.parse(execFileSync("pnpm", ["ls", "-r", "--json", "--depth", "-1"], { cwd: root, encoding: "utf8" }));
|
|
31
|
+
return new Map(listed.map((entry) => [entry.name, entry.path]));
|
|
32
|
+
}
|
|
33
|
+
/** Each name's dependency: a link to its workspace directory when given a workspace, else any version. */
|
|
34
|
+
export function dependenciesOf(names, known) {
|
|
35
|
+
const dependencies = {};
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const dir = known?.get(name);
|
|
38
|
+
if (known && !dir)
|
|
39
|
+
throw new Error(`${name} is not in the workspace`);
|
|
40
|
+
dependencies[name] = dir ? `link:${dir}` : "*";
|
|
41
|
+
}
|
|
42
|
+
return dependencies;
|
|
43
|
+
}
|
|
44
|
+
/** The project's manifest, starting the app with `start`. */
|
|
45
|
+
/** The pnpm the workspace builds with, so the lockfile reads alike in the image. */
|
|
46
|
+
const pnpm = "pnpm@11.3.0";
|
|
47
|
+
export const manifestOf = (start, dependencies) => `${JSON.stringify({
|
|
48
|
+
name: "auth-provider-app",
|
|
49
|
+
private: true,
|
|
50
|
+
type: "module",
|
|
51
|
+
packageManager: pnpm,
|
|
52
|
+
scripts: { start },
|
|
53
|
+
dependencies,
|
|
54
|
+
}, null, "\t")}\n`;
|
|
55
|
+
/** Fills a file of `templates/` by its `{{name}}` marks; a mark with no value stays as written. */
|
|
56
|
+
export async function filled(template, fill) {
|
|
57
|
+
const text = await readFile(join(import.meta.dirname, "..", "templates", template), "utf8");
|
|
58
|
+
return text.replace(/\{\{(\w+)\}\}/g, (all, key) => fill[key] ?? all);
|
|
59
|
+
}
|
|
60
|
+
/** The app's entry, importing each module under what `importOf` names it (the workspace's name, unless published). */
|
|
61
|
+
export const mainSource = (modules, importOf = (name) => name) => filled("main.ts.template", {
|
|
62
|
+
kernel: importOf(kernel),
|
|
63
|
+
imports: modules.map((name, i) => `import m${i} from "${importOf(name)}/module";`).join("\n"),
|
|
64
|
+
modules: modules.map((_, i) => `m${i}`).join(", "),
|
|
65
|
+
});
|
|
66
|
+
export const withKernel = (modules) => [
|
|
67
|
+
kernel,
|
|
68
|
+
...modules.filter((name) => name !== kernel),
|
|
69
|
+
];
|
|
70
|
+
/** Writes every file, by its path in the project, once all are made: nothing is written for a project that fails. */
|
|
71
|
+
export async function writeProject(target, files) {
|
|
72
|
+
for (const [path, text] of Object.entries(files)) {
|
|
73
|
+
await mkdir(dirname(join(target, path)), { recursive: true });
|
|
74
|
+
await writeFile(join(target, path), text);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial settings for the app generated in `target`, by the modules its entry imports: the
|
|
3
|
+
* environment, and a local layer to put before the generated configuration. Keys are made fresh.
|
|
4
|
+
* The sections in `leave` get neither a variable nor a block, so the caller's own layer decides them.
|
|
5
|
+
*/
|
|
6
|
+
export declare function trialSettings(target: string, settings: {
|
|
7
|
+
readonly redisUrl: string;
|
|
8
|
+
readonly port?: number;
|
|
9
|
+
readonly deployment?: "single" | "multi";
|
|
10
|
+
readonly leave?: readonly string[];
|
|
11
|
+
/** Hosts the caller's own layer needs reached, merged into the trial's one outbound block. */
|
|
12
|
+
readonly allow?: readonly string[];
|
|
13
|
+
readonly plainLoopback?: readonly string[];
|
|
14
|
+
}): Promise<{
|
|
15
|
+
readonly env: Record<string, string>;
|
|
16
|
+
readonly local: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Trial settings for a generated app: values that boot it for a test or a conformance run, never for a deployment. */
|
|
2
|
+
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { envName } from "./fragments.js";
|
|
6
|
+
/** Every key an app may read from its variable, by section, with a trial value; one the app does not read is left unread. */
|
|
7
|
+
function trialValues(redisUrl) {
|
|
8
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
9
|
+
return [
|
|
10
|
+
["issuer", "url", "https://login.example.invalid"],
|
|
11
|
+
["keys", "signing.kid", "signing-1"],
|
|
12
|
+
["keys", "signing.alg", "EdDSA"],
|
|
13
|
+
["keys", "signing.private-key", privateKey.export({ type: "pkcs8", format: "pem" }).toString()],
|
|
14
|
+
["tokens", "revocation", "denylist"],
|
|
15
|
+
["audit-http", "url", "https://audit.example.invalid/events"],
|
|
16
|
+
["redis", "url", redisUrl],
|
|
17
|
+
["redis", "replication", "none"],
|
|
18
|
+
["store", "accept-volatile", "true"],
|
|
19
|
+
["mail", "accept-unsent", "true"],
|
|
20
|
+
["device-grant", "verification-uri", "https://login.example.invalid/device"],
|
|
21
|
+
["mtls", "mode", "self-signed"],
|
|
22
|
+
["mail-smtp", "host", "smtp.example.invalid"],
|
|
23
|
+
["mail-smtp", "from", "auth@example.invalid"],
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
/** The local layer's blocks, by section: each kept where the app composes the module it is for. */
|
|
27
|
+
function trialBlocks(modules, deployment, port, added) {
|
|
28
|
+
const has = (name) => modules.includes(name);
|
|
29
|
+
const hosts = [
|
|
30
|
+
...(has("users-http") ? ["ledger.example.invalid"] : []),
|
|
31
|
+
...(has("audit-http") ? ["audit.example.invalid"] : []),
|
|
32
|
+
...added.allow,
|
|
33
|
+
];
|
|
34
|
+
const loopback = added.plainLoopback.length > 0
|
|
35
|
+
? `, plain-loopback = [${added.plainLoopback.map((h) => JSON.stringify(h)).join(", ")}]`
|
|
36
|
+
: "";
|
|
37
|
+
const blocks = [
|
|
38
|
+
[true, "kernel", `kernel { deployment = ${deployment} }`],
|
|
39
|
+
[true, "audit", `audit { receivers = [${has("audit-http") ? "http" : "log"}] }`],
|
|
40
|
+
[
|
|
41
|
+
has("authorize"),
|
|
42
|
+
"authorize",
|
|
43
|
+
`authorize { login-page = "https://login.example.invalid/login" }`,
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
has("users-http"),
|
|
47
|
+
"users-http",
|
|
48
|
+
`users-http { verify-url = "https://ledger.example.invalid/verify" }`,
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
has("http"),
|
|
52
|
+
"http",
|
|
53
|
+
`http { public-origin = "https://login.example.invalid"${port ? `, port = ${port}` : ""} }`,
|
|
54
|
+
],
|
|
55
|
+
[has("mtls"), "http", `http { client-certificate { dialect = pem, header = x-client-cert } }`],
|
|
56
|
+
[
|
|
57
|
+
has("outbound") && (hosts.length > 0 || loopback !== ""),
|
|
58
|
+
"outbound",
|
|
59
|
+
`outbound { allow = [${hosts.join(", ")}]${loopback} }`,
|
|
60
|
+
],
|
|
61
|
+
// The trial's alone: a scene that proves no factor would stop at enrolment under required.
|
|
62
|
+
[has("mfa"), "mfa", "mfa { mode = optional }"],
|
|
63
|
+
// The relying party is the trial's public origin, where passkeys are composed.
|
|
64
|
+
[
|
|
65
|
+
has("webauthn"),
|
|
66
|
+
"webauthn",
|
|
67
|
+
'webauthn { rp-id = login.example.invalid, rp-name = "Trial", origins = ["https://login.example.invalid"] }',
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
has("seal"),
|
|
71
|
+
"seal",
|
|
72
|
+
`seal { keys = [{ id = seal-1, key = "${randomBytes(32).toString("base64")}" }] }`,
|
|
73
|
+
],
|
|
74
|
+
];
|
|
75
|
+
return blocks.flatMap(([kept, section, block]) => (kept ? [{ section, block }] : []));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Trial settings for the app generated in `target`, by the modules its entry imports: the
|
|
79
|
+
* environment, and a local layer to put before the generated configuration. Keys are made fresh.
|
|
80
|
+
* The sections in `leave` get neither a variable nor a block, so the caller's own layer decides them.
|
|
81
|
+
*/
|
|
82
|
+
export async function trialSettings(target, settings) {
|
|
83
|
+
// The modules the entry imports, by their directory's name, as the workspace or a bundle names them.
|
|
84
|
+
const entry = await readFile(join(target, "src", "main.ts"), "utf8");
|
|
85
|
+
const modules = [...entry.matchAll(/from "(?:[^"]*\/)?([a-z0-9-]+)\/module"/g)].map((found) => found[1]);
|
|
86
|
+
const kept = (section) => !(settings.leave ?? []).includes(section);
|
|
87
|
+
const env = Object.fromEntries(trialValues(settings.redisUrl)
|
|
88
|
+
.filter(([section]) => kept(section))
|
|
89
|
+
.map(([section, key, value]) => [envName(`${section}.${key}`), value]));
|
|
90
|
+
const added = { allow: settings.allow ?? [], plainLoopback: settings.plainLoopback ?? [] };
|
|
91
|
+
const blocks = trialBlocks(modules, settings.deployment ?? "single", settings.port, added).filter((b) => kept(b.section));
|
|
92
|
+
return { env, local: `${blocks.map((b) => b.block).join("\n")}\n` };
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Writes the generated references, configuration and audit events, into the workspace this package lies in. */
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { auditReferenceFile, auditReferenceOf, declarationsIn } from "./audit-events.js";
|
|
6
|
+
import { referenceFile, referenceOf, sectionsIn } from "./configuration.js";
|
|
7
|
+
const root = join(import.meta.dirname, "..", "..", "..", "..");
|
|
8
|
+
await mkdir(dirname(referenceFile(root)), { recursive: true });
|
|
9
|
+
await writeFile(referenceFile(root), referenceOf(await sectionsIn(root)));
|
|
10
|
+
await writeFile(auditReferenceFile(root), auditReferenceOf(await declarationsIn(root)));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Auth provider
|
|
2
|
+
|
|
3
|
+
Generated by `create-app` from the full template. The modules' sections are configured from two places, both outside git:
|
|
4
|
+
|
|
5
|
+
- the environment (`.env`): every key `{{application}}` reads;
|
|
6
|
+
- the local layer (`{{local}}`): lists, values that span sections, and the keys no module's example holds (listed below).
|
|
7
|
+
|
|
8
|
+
Each key not written keeps its module's default.
|
|
9
|
+
|
|
10
|
+
## Start
|
|
11
|
+
|
|
12
|
+
1. `pnpm install`
|
|
13
|
+
2. Copy `.env.example` to `.env`, uncomment each line and replace its placeholder.
|
|
14
|
+
3. Copy `{{localExample}}` to `{{local}}`, uncomment each key and replace its placeholders, and add the keys this guide names that neither file holds.
|
|
15
|
+
4. `APP_CONFIG={{local}},{{application}} pnpm start`
|
|
16
|
+
|
|
17
|
+
Both examples hold every value in a comment: a copy left as it is sets nothing, and boot refuses each key still needed.
|
|
18
|
+
A secret's placeholder begins `REPLACE-`; uncommented as it is, boot refuses it. Delete the line of an optional key left unset.
|
|
19
|
+
|
|
20
|
+
Until every key the modules need is written, the app writes each refusal on standard error, one per line, and exits 78.
|
|
21
|
+
SIGTERM or SIGINT stops it, draining what is under way, within `kernel.stop-seconds` (30 by default); give an orchestrator a grace period longer than `kernel.stop-seconds`.
|
|
22
|
+
|
|
23
|
+
## Docker
|
|
24
|
+
|
|
25
|
+
1. `docker build -t auth-provider .`
|
|
26
|
+
2. `docker run --env-file .env -e APP_CONFIG={{local}},{{application}} -v "$PWD/{{local}}:/app/{{local}}:ro" -p {{port}}:{{port}} auth-provider`
|
|
27
|
+
|
|
28
|
+
Docker reads one line per variable from an env file: write a value of several lines, such as a PEM key, in the local layer instead.
|
|
29
|
+
The image runs as a user other than root and installs the locked production dependencies only. `tini` runs as process 1, so `docker stop` reaches the app and it stops cleanly.
|
|
30
|
+
Its health check asks `/_healthcheck`, which answers while the process runs; `/readyz` answers readiness, for a load balancer.
|
|
31
|
+
|
|
32
|
+
## Keys read from the environment
|
|
33
|
+
|
|
34
|
+
`{{application}}` reads each of these from its variable and holds no value.
|
|
35
|
+
|
|
36
|
+
| Key | Variable | Placeholder |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
{{settings}}
|
|
39
|
+
|
|
40
|
+
## Keys written in the local layer
|
|
41
|
+
|
|
42
|
+
{{written}}
|
|
43
|
+
|
|
44
|
+
## Keys no example holds
|
|
45
|
+
|
|
46
|
+
No module's example holds these: write them in the local layer when the deployment needs them.
|
|
47
|
+
|
|
48
|
+
- `authorize.step-up-page`: the page that raises authentication (optional).
|
|
49
|
+
- `http.trusted-proxies`: the proxies in front, whose forwarded addresses are believed.
|
|
50
|
+
- `http.client-certificate`: where a proxy forwards client certificates, when a feature needs them.
|
|
51
|
+
- `users.registered`: leave unset while the user ledger is composed; boot refuses both.
|
|
52
|
+
- `users-http.credential`: the ledger's bearer token, when the ledger asks for one.
|
|
53
|
+
- `mail-smtp.user`: the SMTP account, when the relay asks for one.
|
|
54
|
+
- `mail-smtp.password`: the SMTP account's password.
|
|
55
|
+
- `clients.registered`: a confidential client carries `sections.client-secret.hash`, a bcrypt or scrypt hash of its secret, never the secret itself.
|
|
56
|
+
|
|
57
|
+
Write secrets in `.env` or the local layer only; neither goes into git or the image.
|
|
58
|
+
|
|
59
|
+
## Adding a feature
|
|
60
|
+
|
|
61
|
+
Generate again with `create-app <target> --add <feature>`, with one `--add` for each feature, and carry the configuration over.
|
|
62
|
+
The feature comes with its wirings and bridges.
|
|
63
|
+
Boot names what the added feature needs, and what cannot be composed.
|
|
64
|
+
|
|
65
|
+
The template leaves these features out:
|
|
66
|
+
|
|
67
|
+
{{optIns}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assemble, BootRefused, loadConfig } from "{{kernel}}";
|
|
2
|
+
{{imports}}
|
|
3
|
+
|
|
4
|
+
/** A refused boot: each refusal on its own line of standard error, then exit 78. */
|
|
5
|
+
const refused = (error: unknown): never => {
|
|
6
|
+
if (!(error instanceof BootRefused)) throw error;
|
|
7
|
+
for (const refusal of error.refusals) {
|
|
8
|
+
process.stderr.write(`refused ${refusal.where}: ${refusal.reason}\n`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(78);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const files = (process.env.APP_CONFIG ?? "config/application.conf").split(",");
|
|
14
|
+
const app = await loadConfig({ files, env: process.env })
|
|
15
|
+
.then((config) => assemble({ modules: [{{modules}}], config }))
|
|
16
|
+
.catch(refused);
|
|
17
|
+
// Taken before starting, so a signal while starting stops once started instead of killing.
|
|
18
|
+
let started: Promise<void> = Promise.resolve();
|
|
19
|
+
const stop = async () => {
|
|
20
|
+
await started.catch(() => undefined);
|
|
21
|
+
// Within the kernel's `stop-seconds`.
|
|
22
|
+
await app.stop();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
};
|
|
25
|
+
process.once("SIGTERM", stop);
|
|
26
|
+
process.once("SIGINT", stop);
|
|
27
|
+
started = app.start();
|
|
28
|
+
await started;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Auth provider: {{name}}
|
|
2
|
+
|
|
3
|
+
{{use}}
|
|
4
|
+
|
|
5
|
+
Generated by `create-app` from the pattern `{{name}}`. The modules' sections are configured from two places, both outside git:
|
|
6
|
+
|
|
7
|
+
- the environment (`.env`): every key `{{application}}` reads;
|
|
8
|
+
- the local layer (`{{local}}`): lists, values that span sections, and keys no module's example holds.
|
|
9
|
+
|
|
10
|
+
Each key not written keeps its module's default.
|
|
11
|
+
|
|
12
|
+
## This pattern
|
|
13
|
+
|
|
14
|
+
{{card}}
|
|
15
|
+
|
|
16
|
+
## Store, users, audit and mail
|
|
17
|
+
|
|
18
|
+
{{chosen}}
|
|
19
|
+
|
|
20
|
+
## Start
|
|
21
|
+
|
|
22
|
+
1. `pnpm install`
|
|
23
|
+
2. Copy `.env.example` to `.env`, uncomment each line and replace its placeholder.
|
|
24
|
+
3. Copy `{{localExample}}` to `{{local}}`, uncomment each key and replace its placeholders, and add the keys this guide names that neither file holds.
|
|
25
|
+
4. `APP_CONFIG={{local}},{{application}} pnpm start`
|
|
26
|
+
|
|
27
|
+
Both examples hold every value in a comment: a copy left as it is sets nothing, and boot refuses each key still needed.
|
|
28
|
+
A secret's placeholder begins `REPLACE-`; uncommented as it is, boot refuses it. Delete the line of an optional key left unset.
|
|
29
|
+
|
|
30
|
+
Until every key the modules need is written, the app writes each refusal on standard error, one per line, and exits 78.
|
|
31
|
+
SIGTERM or SIGINT stops it, draining what is under way, within `kernel.stop-seconds` (30 by default); give an orchestrator a grace period longer than `kernel.stop-seconds`.
|
|
32
|
+
|
|
33
|
+
## Docker
|
|
34
|
+
|
|
35
|
+
1. `docker build -t auth-provider .`
|
|
36
|
+
2. `docker run --env-file .env -e APP_CONFIG={{local}},{{application}} -v "$PWD/{{local}}:/app/{{local}}:ro" -p {{port}}:{{port}} auth-provider`
|
|
37
|
+
|
|
38
|
+
Docker reads one line per variable from an env file: write a value of several lines, such as a PEM key, in the local layer instead.
|
|
39
|
+
The image runs as a user other than root and installs the locked production dependencies only. `tini` runs as process 1, so `docker stop` reaches the app and it stops cleanly.
|
|
40
|
+
Its health check asks `/_healthcheck`, which answers while the process runs; `/readyz` answers readiness, for a load balancer.
|
|
41
|
+
|
|
42
|
+
## Keys read from the environment
|
|
43
|
+
|
|
44
|
+
`{{application}}` reads each of these from its variable and holds no value.
|
|
45
|
+
|
|
46
|
+
| Key | Variable | Placeholder |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
{{settings}}
|
|
49
|
+
|
|
50
|
+
## Keys written in the local layer
|
|
51
|
+
|
|
52
|
+
{{written}}
|
|
53
|
+
|
|
54
|
+
## Values the operator decides
|
|
55
|
+
|
|
56
|
+
- `kernel.deployment`: {{deployment}}
|
|
57
|
+
- `tokens.revocation`: `denylist`, or `unsupported` to answer revocation as unsupported.
|
|
58
|
+
- `audit.receivers`: the names of the audit receivers composed; `log` writes events to the log.
|
|
59
|
+
|
|
60
|
+
Write secrets, such as the signing key, in `.env` or the local layer only; neither goes into git or the image.
|
|
61
|
+
|
|
62
|
+
## Changing the pattern
|
|
63
|
+
|
|
64
|
+
Generate again and carry the configuration over:
|
|
65
|
+
|
|
66
|
+
- `create-app <target> --pattern {{name}} --add <feature>` adds a feature, with what it needs, its wirings and bridges.
|
|
67
|
+
- `--axis <owner>=<adapter>` chooses an axis's adapter, or the owner itself.
|
|
68
|
+
- `--login`, `--second-factor`, `--grants`, `--client-auth` and `--binding` each replace that group of features with the comma-separated list given.
|
|
69
|
+
|
|
70
|
+
Boot names what a change needs, and what cannot be composed.
|