@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,22 @@
1
+ /** One declared event, as `audit`'s `auditEvents` takes it. */
2
+ export interface Declared {
3
+ readonly attributes: readonly string[];
4
+ readonly when: string;
5
+ }
6
+ /** Each package exporting `./audit-events`, with its declarations, by package name. */
7
+ export declare function declarationsIn(root: string): Promise<{
8
+ readonly owner: string;
9
+ readonly events: Readonly<Record<string, Declared>>;
10
+ }[]>;
11
+ /** Each kind more than one module declares, unlike in some way: one kind holds one meaning. */
12
+ export declare function unlikeKinds(modules: readonly {
13
+ readonly owner: string;
14
+ readonly events: Readonly<Record<string, Declared>>;
15
+ }[]): string[];
16
+ /** The reference, generated: one table per module, its kinds in code point order. */
17
+ export declare function auditReferenceOf(modules: readonly {
18
+ readonly owner: string;
19
+ readonly events: Readonly<Record<string, Declared>>;
20
+ }[]): string;
21
+ /** Where the reference lies in the workspace. */
22
+ export declare const auditReferenceFile: (root: string) => string;
@@ -0,0 +1,76 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /** The audit event reference: every module's declared events, as one document. */
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { workspacePackages } from "./project.js";
13
+ /** Each package exporting `./audit-events`, with its declarations, by package name. */
14
+ export async function declarationsIn(root) {
15
+ const found = [];
16
+ for (const [name, dir] of workspacePackages(root)) {
17
+ if (dir === root)
18
+ continue;
19
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
20
+ const entry = manifest.exports?.["./audit-events"];
21
+ if (!entry)
22
+ continue;
23
+ const events = (await import(__rewriteRelativeImportExtension(join(dir, entry)))).default;
24
+ found.push({ owner: name, events });
25
+ }
26
+ return found.sort((a, b) => (a.owner < b.owner ? -1 : 1));
27
+ }
28
+ /** Each kind more than one module declares, unlike in some way: one kind holds one meaning. */
29
+ export function unlikeKinds(modules) {
30
+ const first = new Map();
31
+ const unlike = new Set();
32
+ for (const { owner, events } of modules) {
33
+ for (const [kind, declared] of Object.entries(events)) {
34
+ const shape = JSON.stringify({
35
+ attributes: [...declared.attributes].sort(),
36
+ when: declared.when,
37
+ });
38
+ const held = first.get(kind);
39
+ if (!held)
40
+ first.set(kind, { owner, declared: shape });
41
+ else if (held.declared !== shape)
42
+ unlike.add(`${kind}: ${held.owner} and ${owner} declare it differently`);
43
+ }
44
+ }
45
+ return [...unlike];
46
+ }
47
+ const text = (value) => value.replaceAll("|", "\\|").replaceAll("\n", " ");
48
+ /** The reference, generated: one table per module, its kinds in code point order. */
49
+ export function auditReferenceOf(modules) {
50
+ const blocks = modules.map(({ owner, events }) => [
51
+ `## ${owner}`,
52
+ "",
53
+ "| Kind | Attributes | Recorded when |",
54
+ "|---|---|---|",
55
+ ...Object.keys(events)
56
+ .sort()
57
+ .map((kind) => {
58
+ const e = events[kind];
59
+ const attributes = e.attributes.map((a) => `\`${a}\``).join(", ");
60
+ return `| \`${kind}\` | ${attributes} | ${text(e.when)} |`;
61
+ }),
62
+ ].join("\n"));
63
+ return `${[
64
+ "# Audit events",
65
+ "",
66
+ "Generated by `create-app` from each module's `./audit-events` declarations: do not edit; run `pnpm --filter @mod/create-app run configuration`.",
67
+ "Every event carries its kind and the time `kernel`'s clock gives it, and a subject where the table's module names one.",
68
+ "`audit` bounds a `userAgent`, the request's own text, before any receiver gets it: a `?` for each character outside RFC 6749's NQSCHAR, at most 200 characters; the subject and other attributes stay as written.",
69
+ "",
70
+ ...blocks.flatMap((b) => [b, ""]),
71
+ ]
72
+ .join("\n")
73
+ .trimEnd()}\n`;
74
+ }
75
+ /** Where the reference lies in the workspace. */
76
+ export const auditReferenceFile = (root) => join(root, "docs", "audit-events.md");
@@ -0,0 +1,5 @@
1
+ /** Runs the command on its arguments, writing to `io`: 0 once the project is written, 64 for a usage error. */
2
+ export declare function run(args: readonly string[], io?: {
3
+ readonly out: (text: string) => void;
4
+ readonly err: (text: string) => void;
5
+ }): Promise<number>;
@@ -0,0 +1,69 @@
1
+ import { parseArgs } from "node:util";
2
+ import { generate } from "./generate.js";
3
+ import { groups } from "./patterns/pattern.js";
4
+ /** Runs the command on its arguments, writing to `io`: 0 once the project is written, 64 for a usage error. */
5
+ export async function run(args, io = {
6
+ out: (text) => process.stdout.write(text),
7
+ err: (text) => process.stderr.write(text),
8
+ }) {
9
+ const usage = () => {
10
+ io.err("usage: create-app <target> [--workspace <root>] [--pattern <name>] [--add <feature>]...\n" +
11
+ " [--axis <owner>=<module>]... [--<group> <feature>,...]...\n" +
12
+ ` groups: ${groups.join(", ")}; axes and groups need a pattern other than full\n` +
13
+ " create-app <target> [--workspace <root>] [--sink <package>] <package>...\n");
14
+ return 64;
15
+ };
16
+ const groupOptions = Object.fromEntries(groups.map((group) => [group, { type: "string" }]));
17
+ const { values, positionals } = parseArgs({
18
+ args: [...args],
19
+ allowPositionals: true,
20
+ options: {
21
+ workspace: { type: "string" },
22
+ sink: { type: "string" },
23
+ pattern: { type: "string" },
24
+ add: { type: "string", multiple: true },
25
+ axis: { type: "string", multiple: true },
26
+ ...groupOptions,
27
+ },
28
+ });
29
+ const [target, ...modules] = positionals;
30
+ const pattern = values.pattern ?? "full";
31
+ const axes = (values.axis ?? []).map((given) => given.split("="));
32
+ const chosen = groups.flatMap((group) => {
33
+ const listed = values[group];
34
+ return typeof listed === "string" ? [[group, listed.split(",")]] : [];
35
+ });
36
+ // Packages listed make the app of those; none makes a pattern's, full when none is named.
37
+ const listed = modules.length > 0;
38
+ const patterned = values.add || values.pattern || axes.length > 0 || chosen.length > 0;
39
+ const malformedAxis = axes.some((pair) => pair.length !== 2 || pair.includes(""));
40
+ const choiceForFull = pattern === "full" && (axes.length > 0 || chosen.length > 0);
41
+ if (!target ||
42
+ (listed && patterned) ||
43
+ (!listed && values.sink) ||
44
+ malformedAxis ||
45
+ choiceForFull) {
46
+ return usage();
47
+ }
48
+ const workspace = values.workspace ? { workspace: values.workspace } : {};
49
+ const add = values.add ? { add: values.add } : {};
50
+ function planOf(target) {
51
+ if (listed)
52
+ return { target, modules, ...workspace, ...(values.sink ? { sink: values.sink } : {}) };
53
+ if (pattern === "full")
54
+ return { target, template: "full", ...workspace, ...add };
55
+ return {
56
+ target,
57
+ template: pattern,
58
+ ...workspace,
59
+ ...add,
60
+ ...(axes.length > 0 ? { axes: Object.fromEntries(axes) } : {}),
61
+ ...(chosen.length > 0 ? { features: Object.fromEntries(chosen) } : {}),
62
+ };
63
+ }
64
+ await generate(planOf(target ?? ""));
65
+ io.out(listed
66
+ ? `created ${target}\nnext: cd ${target} && pnpm install && pnpm start\n`
67
+ : `created ${target}\nnext: cd ${target} and follow its README.md\n`);
68
+ return 0;
69
+ }
@@ -0,0 +1,44 @@
1
+ /** The part of a JSON Schema the reference reads. */
2
+ export interface JsonSchema {
3
+ readonly type?: string | readonly string[];
4
+ readonly properties?: Readonly<Record<string, JsonSchema>>;
5
+ readonly required?: readonly string[];
6
+ readonly items?: JsonSchema;
7
+ readonly enum?: readonly unknown[];
8
+ readonly anyOf?: readonly JsonSchema[];
9
+ readonly oneOf?: readonly JsonSchema[];
10
+ readonly const?: unknown;
11
+ readonly default?: unknown;
12
+ readonly description?: string;
13
+ readonly minimum?: number;
14
+ readonly maximum?: number;
15
+ }
16
+ /** One key of a section, as the reference shows it. */
17
+ export interface Row {
18
+ readonly key: string;
19
+ readonly type: string;
20
+ readonly default?: unknown;
21
+ readonly required: boolean;
22
+ readonly env?: string;
23
+ readonly example?: unknown;
24
+ readonly description?: string;
25
+ }
26
+ /** Each key of a schema, objects with properties walked into; `at` reads the fragment's value and env. */
27
+ export declare function rowsOf(schema: JsonSchema, path: readonly string[], at: (key: string) => {
28
+ readonly env?: string;
29
+ readonly example?: unknown;
30
+ }): Row[];
31
+ /** Every section of the workspace: its owner, and its rows. */
32
+ export declare function sectionsIn(root: string): Promise<{
33
+ readonly section: string;
34
+ readonly owner: string;
35
+ readonly rows: Row[];
36
+ }[]>;
37
+ /** The reference, generated: one table per section. */
38
+ export declare function referenceOf(sections: readonly {
39
+ readonly section: string;
40
+ readonly owner: string;
41
+ readonly rows: readonly Row[];
42
+ }[]): string;
43
+ /** Where the reference lies in the workspace. */
44
+ export declare const referenceFile: (root: string) => string;
@@ -0,0 +1,161 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /** The configuration reference: every module's section, from its schema and its fragment, as one document. */
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fragmentsIn, settingsOf } from "./fragments.js";
13
+ import { kernel, workspacePackages } from "./project.js";
14
+ const typeOf = (s) => {
15
+ if (s.const !== undefined)
16
+ return String(s.const);
17
+ if (s.enum)
18
+ return s.enum.map((v) => String(v)).join(" | ");
19
+ const either = s.anyOf ?? s.oneOf;
20
+ if (either)
21
+ return [...new Set(either.map(typeOf))].join(" | ");
22
+ if (s.type === "array")
23
+ return `list of ${s.items ? typeOf(s.items) : "any"}`;
24
+ if (s.type === "object")
25
+ return "map";
26
+ const name = Array.isArray(s.type) ? s.type.join(" | ") : (s.type ?? "any");
27
+ const range = s.minimum !== undefined || s.maximum !== undefined
28
+ ? ` (${s.minimum ?? ""}–${s.maximum ?? ""})`
29
+ : "";
30
+ return `${name}${range}`;
31
+ };
32
+ /** An object's keys, or a union of objects' keys merged: required only where every variant requires it. */
33
+ function objectOf(s) {
34
+ if (s.properties)
35
+ return s;
36
+ const variants = (s.oneOf ?? s.anyOf ?? []).map(objectOf);
37
+ if (variants.length === 0 || variants.some((v) => v === undefined))
38
+ return undefined;
39
+ const properties = {};
40
+ for (const v of variants) {
41
+ for (const [name, p] of Object.entries(v.properties ?? {})) {
42
+ const held = properties[name];
43
+ const either = held ? [...(held.anyOf ?? [held]), p] : undefined;
44
+ properties[name] = either
45
+ ? {
46
+ anyOf: either,
47
+ ...(held?.default !== undefined ? { default: held.default } : {}),
48
+ ...(held?.description ? { description: held.description } : {}),
49
+ }
50
+ : p;
51
+ }
52
+ }
53
+ const required = Object.keys(properties).filter((name) => variants.every((v) => (v.required ?? []).includes(name)));
54
+ return { type: "object", properties, required };
55
+ }
56
+ /** Each key of a schema, objects with properties walked into; `at` reads the fragment's value and env. */
57
+ export function rowsOf(schema, path, at) {
58
+ const object = objectOf(schema);
59
+ return Object.entries(object?.properties ?? {}).flatMap(([name, s]) => {
60
+ const keyPath = [...path, name];
61
+ const inner = objectOf(s);
62
+ if (inner)
63
+ return rowsOf(inner, keyPath, at);
64
+ const key = keyPath.join(".");
65
+ return [
66
+ {
67
+ key,
68
+ type: typeOf(s),
69
+ ...(s.default === undefined ? {} : { default: s.default }),
70
+ required: (object?.required ?? []).includes(name) && s.default === undefined,
71
+ ...at(key),
72
+ ...(s.description ? { description: s.description } : {}),
73
+ },
74
+ ];
75
+ });
76
+ }
77
+ /** A fragment's own leaves, typed by their sample, for a section read without a schema. */
78
+ function fragmentRows(fragment, at) {
79
+ const walk = (tree, path) => Object.entries(tree).flatMap(([name, value]) => {
80
+ const keyPath = [...path, name];
81
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
82
+ return walk(value, keyPath);
83
+ }
84
+ const key = keyPath.join(".");
85
+ const type = Array.isArray(value) ? "list" : typeof value;
86
+ return [{ key, type, required: false, ...at(key), example: value }];
87
+ });
88
+ return walk(fragment.tree, [fragment.section]);
89
+ }
90
+ /** Every section of the workspace: its owner, and its rows. */
91
+ export async function sectionsIn(root) {
92
+ const found = [];
93
+ for (const [name, dir] of workspacePackages(root)) {
94
+ if (dir === root)
95
+ continue;
96
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
97
+ const entry = manifest.exports?.["./module"];
98
+ const module = entry
99
+ ? (await import(__rewriteRelativeImportExtension(join(dir, entry)))).default
100
+ : undefined;
101
+ const [fragment] = await fragmentsIn([dir]);
102
+ const env = new Map(fragment ? settingsOf([fragment]).map((s) => [s.key, s.env]) : []);
103
+ const example = new Map(fragment ? fragmentRows(fragment, () => ({})).map((r) => [r.key, r.example]) : []);
104
+ const at = (key) => {
105
+ const variable = env.get(key);
106
+ return {
107
+ ...(variable === undefined ? {} : { env: variable }),
108
+ ...(example.has(key) ? { example: example.get(key) } : {}),
109
+ };
110
+ };
111
+ const schema = module?.section?.["~standard"].jsonSchema?.input({ target: "draft-2020-12" });
112
+ if (module && schema) {
113
+ found.push({
114
+ section: module.name,
115
+ owner: name,
116
+ rows: rowsOf(schema, [module.name], at),
117
+ });
118
+ }
119
+ else if (fragment && name === kernel) {
120
+ // The kernel reads its own section, by the schema its contract exports.
121
+ const own = (await import(__rewriteRelativeImportExtension(join(dir, manifest.exports["."])))).kernelSection;
122
+ const read = own?.["~standard"].jsonSchema?.input({ target: "draft-2020-12" });
123
+ const rows = read
124
+ ? rowsOf(read, [fragment.section], at)
125
+ : fragmentRows(fragment, at);
126
+ found.push({ section: fragment.section, owner: name, rows });
127
+ }
128
+ }
129
+ return found.sort((a, b) => (a.section < b.section ? -1 : 1));
130
+ }
131
+ const cell = (value) => value === undefined
132
+ ? ""
133
+ : `\`${(typeof value === "string" ? value : JSON.stringify(value)).replaceAll("|", "\\|")}\``;
134
+ const text = (value) => (value ?? "").replaceAll("|", "\\|").replaceAll("\n", " ");
135
+ /** The reference, generated: one table per section. */
136
+ export function referenceOf(sections) {
137
+ const blocks = sections.map(({ section, owner, rows }) => {
138
+ const lines = rows.map((r) => `| \`${r.key}\` | ${text(r.type)} | ${cell(r.default)} | ${r.required ? "yes" : ""} | ${r.env ? `\`${r.env}\`` : ""} | ${cell(r.example)} | ${text(r.description)} |`);
139
+ return [
140
+ `## ${section}`,
141
+ "",
142
+ `From \`${owner}\`.`,
143
+ "",
144
+ "| Key | Type | Default | Required | Variable | Example | Description |",
145
+ "|---|---|---|---|---|---|---|",
146
+ ...lines,
147
+ ].join("\n");
148
+ });
149
+ return `${[
150
+ "# Configuration",
151
+ "",
152
+ "Generated by `create-app` from each module's section schema and its `config.example`: do not edit; run `pnpm --filter @mod/create-app run configuration`.",
153
+ "A variable is read by the configuration `create-app` generates, for a key the module's example holds; any other key is written in the local layer.",
154
+ "",
155
+ ...blocks.flatMap((b) => [b, ""]),
156
+ ]
157
+ .join("\n")
158
+ .trimEnd()}\n`;
159
+ }
160
+ /** Where the reference lies in the workspace. */
161
+ export const referenceFile = (root) => join(root, "docs", "configuration.md");
@@ -0,0 +1,6 @@
1
+ export { run } from "./command.js";
2
+ export { type FullPlan, generate, type ModulesPlan, type Plan } from "./generate.js";
3
+ export type { PatternPlan } from "./pattern.js";
4
+ export type { Choice } from "./patterns/pattern.js";
5
+ export { type PatternSummary, patterns } from "./patterns/patterns.js";
6
+ export { trialSettings } from "./trial.js";
@@ -0,0 +1,4 @@
1
+ export { run } from "./command.js";
2
+ export { generate } from "./generate.js";
3
+ export { patterns } from "./patterns/patterns.js";
4
+ export { trialSettings } from "./trial.js";
@@ -0,0 +1,8 @@
1
+ /** Writes the project assembling `modules` into an empty target, its guide from `readme`'s template and values. */
2
+ export declare function writeDeployment(plan: {
3
+ readonly target: string;
4
+ readonly workspace?: string;
5
+ }, modules: readonly string[], readme: {
6
+ readonly template: string;
7
+ readonly fill: Readonly<Record<string, string>>;
8
+ }): Promise<void>;
@@ -0,0 +1,100 @@
1
+ /** A deployment's project from a list of modules: configuration from their fragments, the image and a guide. */
2
+ import { applicationConf, envExample, envName, localConfExample, settingsOf, writtenOf, } from "./fragments.js";
3
+ import { packagesFor } from "./packages.js";
4
+ import { filled, mainSource, manifestOf, vacant, withKernel, writeProject } from "./project.js";
5
+ /** The files the generated project names in its guide, each written where the guide says. */
6
+ const paths = {
7
+ application: "config/application.conf",
8
+ local: "config/local.conf",
9
+ localExample: "config/local.conf.example",
10
+ main: "src/main.ts",
11
+ };
12
+ /** Node 24 by version and by digest (the multi-platform index); update both together. */
13
+ const nodeImage = "node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1";
14
+ /** The corepack that gives the image the manifest's pnpm; pinned, so a build never takes the latest. */
15
+ const corepack = "0.36.0";
16
+ /** http's default port: the image changes only the interface it listens on. */
17
+ const httpPort = 8080;
18
+ /** Secrets of any file name stay out of git: env files but their example, the local layer, keys. */
19
+ const gitIgnored = [
20
+ "node_modules/",
21
+ "config/local*.conf",
22
+ ".env",
23
+ ".env.*",
24
+ "!.env.example",
25
+ "*.pem",
26
+ "*.key",
27
+ ];
28
+ /** The image's build context leaves out git, every env file, the local layer, keys and the host's install. */
29
+ const dockerIgnored = [
30
+ ".git",
31
+ ".env",
32
+ ".env.*",
33
+ "config/local*.conf",
34
+ "*.pem",
35
+ "*.key",
36
+ "node_modules/",
37
+ ];
38
+ const lines = (entries) => `${entries.join("\n")}\n`;
39
+ /** Writes the project assembling `modules` into an empty target, its guide from `readme`'s template and values. */
40
+ export async function writeDeployment(plan, modules, readme) {
41
+ await vacant(plan.target);
42
+ const names = withKernel(modules);
43
+ const packages = packagesFor(plan);
44
+ const fragments = await packages.fragments(names);
45
+ const settings = settingsOf(fragments);
46
+ await writeProject(plan.target, {
47
+ "package.json": manifestOf("node --env-file-if-exists=.env src/main.ts", packages.dependencies(names)),
48
+ [paths.main]: await mainSource(modules, packages.importOf),
49
+ [paths.application]: applicationConf(fragments),
50
+ [paths.localExample]: localConfExample(fragments),
51
+ ".env.example": envExample(settings),
52
+ Dockerfile: dockerfile(),
53
+ ".dockerignore": lines(dockerIgnored),
54
+ ".gitignore": lines(gitIgnored),
55
+ "README.md": await filled(readme.template, {
56
+ ...paths,
57
+ ...guideLists(settings, writtenOf(fragments)),
58
+ port: String(httpPort),
59
+ ...readme.fill,
60
+ }),
61
+ });
62
+ }
63
+ /** The guide's lists: the keys read from env as table rows, and those written in the local layer. */
64
+ const guideLists = (settings, written) => ({
65
+ settings: settings
66
+ .map((s) => `| \`${s.key}\` | \`${s.env}\` | \`${s.placeholder}\` |`)
67
+ .join("\n"),
68
+ written: written
69
+ .map((w) => `- \`${w.key}\`${w.spansSections ? " (spans sections: written by the operator)" : ""}`)
70
+ .join("\n"),
71
+ });
72
+ function dockerfile() {
73
+ return `# syntax=docker/dockerfile:1
74
+ # Node 24 pinned by version and digest, corepack pinned too; pnpm is the manifest's packageManager.
75
+ FROM ${nodeImage} AS base
76
+ # An init as process 1 passes signals on and reaps what node leaves behind.
77
+ RUN apk add --no-cache tini && npm install --global corepack@${corepack} && corepack enable
78
+ WORKDIR /app
79
+
80
+ FROM base AS deps
81
+ COPY package.json pnpm-lock.yaml ./
82
+ RUN pnpm install --frozen-lockfile --prod
83
+
84
+ FROM base AS runtime
85
+ ENV NODE_ENV=production
86
+ COPY --from=deps /app/node_modules ./node_modules
87
+ COPY package.json ./
88
+ COPY src ./src
89
+ COPY ${paths.application} ./${paths.application}
90
+ # Listens on every interface inside the container; the port is http's, which the health check follows.
91
+ ENV ${envName("http.host")}=0.0.0.0
92
+ ENV ${envName("http.port")}=${httpPort}
93
+ USER node
94
+ EXPOSE \${${envName("http.port")}}
95
+ # Liveness, not readiness: the container is restarted only when the process stops answering.
96
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["node", "-e", "fetch('http://127.0.0.1:' + process.env.${envName("http.port")} + '/_healthcheck').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
97
+ ENTRYPOINT ["/sbin/tini", "--"]
98
+ CMD ["node", "${paths.main}"]
99
+ `;
100
+ }
@@ -0,0 +1,41 @@
1
+ /** The file beside a package's manifest that holds its section's example. */
2
+ export declare const fragmentFile = "config.example";
3
+ type Tree = {
4
+ readonly [key: string]: unknown;
5
+ };
6
+ /** One section's example, as its owner wrote it: placeholders only. */
7
+ export interface Fragment {
8
+ readonly section: string;
9
+ readonly tree: Tree;
10
+ }
11
+ /** A scalar key of a fragment, read from the environment variable named for it. */
12
+ export interface Setting {
13
+ readonly key: string;
14
+ readonly env: string;
15
+ readonly placeholder: string;
16
+ }
17
+ /** A key the configuration file does not read from env: a list, or a value the operator writes. */
18
+ export interface Written {
19
+ readonly key: string;
20
+ readonly example: string;
21
+ /** Whether it spans sections: the fragment's value is for its schema only, and the operator writes it. */
22
+ readonly spansSections: boolean;
23
+ }
24
+ /** The fragments beside the packages in `dirs`, in that order; a package without one is skipped. */
25
+ export declare function fragmentsIn(dirs: readonly string[]): Promise<Fragment[]>;
26
+ /** Fragments from their texts, each of exactly one section, no section twice; `from` names it in a refusal. */
27
+ export declare function fragmentsOf(texts: readonly {
28
+ readonly from: string;
29
+ readonly text: string;
30
+ }[]): Fragment[];
31
+ export declare const envName: (key: string) => string;
32
+ /** The fragments' scalar keys with their variables; refuses two keys reading one, naming both, and an unnameable one. */
33
+ export declare function settingsOf(fragments: readonly Fragment[]): Setting[];
34
+ /** The keys the operator writes in a local layer: the fragments' lists, and the values that span sections. */
35
+ export declare function writtenOf(fragments: readonly Fragment[]): Written[];
36
+ /** Every scalar key reads its environment variable, and holds no value. */
37
+ export declare function applicationConf(fragments: readonly Fragment[]): string;
38
+ /** Each list with its placeholders, and each value that spans sections, by its dotted key. */
39
+ export declare function localConfExample(fragments: readonly Fragment[]): string;
40
+ export declare function envExample(settings: readonly Setting[]): string;
41
+ export {};
@@ -0,0 +1,109 @@
1
+ /** The boundary between the modules' sections and the project's environment: fragments mapped to env and the local layer. */
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { parse } from "@o3co/ts.hocon";
5
+ import { missingAs } from "./project.js";
6
+ /** The file beside a package's manifest that holds its section's example. */
7
+ export const fragmentFile = "config.example";
8
+ /** Values spanning sections, in a fragment for its schema only: copied, they would boot; the operator writes them. */
9
+ const spanningSections = new Map([
10
+ ["audit.receivers", "[REPLACE-RECEIVER-NAMES]"],
11
+ ]);
12
+ const isTree = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
+ /** The fragments beside the packages in `dirs`, in that order; a package without one is skipped. */
14
+ export async function fragmentsIn(dirs) {
15
+ const read = [];
16
+ for (const dir of dirs) {
17
+ const file = join(dir, fragmentFile);
18
+ const text = await readFile(file, "utf8").catch(missingAs(undefined));
19
+ if (text !== undefined)
20
+ read.push({ from: file, text });
21
+ }
22
+ return fragmentsOf(read);
23
+ }
24
+ /** Fragments from their texts, each of exactly one section, no section twice; `from` names it in a refusal. */
25
+ export function fragmentsOf(texts) {
26
+ const fragments = [];
27
+ for (const { from, text } of texts) {
28
+ const tree = parse(text).toObject();
29
+ const roots = isTree(tree) ? Object.entries(tree) : [];
30
+ const [root] = roots;
31
+ if (roots.length !== 1 || root === undefined || !isTree(root[1])) {
32
+ throw new Error(`${from} does not hold exactly one section`);
33
+ }
34
+ if (fragments.some((f) => f.section === root[0])) {
35
+ throw new Error(`${from}: a second fragment for ${root[0]}`);
36
+ }
37
+ fragments.push({ section: root[0], tree: root[1] });
38
+ }
39
+ return fragments;
40
+ }
41
+ /** Each leaf of a tree with its path: objects are walked into, lists and scalars are leaves. */
42
+ function leaves(tree, path) {
43
+ return Object.entries(tree).flatMap(([key, value]) => isTree(value) ? leaves(value, [...path, key]) : [[[...path, key], value]]);
44
+ }
45
+ const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
46
+ /** The environment variable a key reads: the path joined by `_`, upper-cased, `-` as `_`. */
47
+ const envOf = (path) => path.join("_").toUpperCase().replaceAll("-", "_");
48
+ export const envName = (key) => envOf(key.split("."));
49
+ /** The fragments' scalar keys with their variables; refuses two keys reading one, naming both, and an unnameable one. */
50
+ export function settingsOf(fragments) {
51
+ const settings = fragments.flatMap((f) => leaves(f.tree, [f.section])
52
+ .filter(([path, value]) => scalar(value) && !spanningSections.has(path.join(".")))
53
+ .map(([path, value]) => ({
54
+ key: path.join("."),
55
+ env: envOf(path),
56
+ placeholder: String(value),
57
+ })));
58
+ const refusals = settings.flatMap((s, i) => {
59
+ if (!/^[A-Z][A-Z0-9_]*$/.test(s.env))
60
+ return [`${s.key}: no environment variable name`];
61
+ const first = settings.findIndex((other) => other.env === s.env);
62
+ return first < i ? [`${settings[first]?.key} and ${s.key} both read ${s.env}`] : [];
63
+ });
64
+ if (refusals.length > 0)
65
+ throw new Error(refusals.join("; "));
66
+ return settings;
67
+ }
68
+ /** The keys the operator writes in a local layer: the fragments' lists, and the values that span sections. */
69
+ export function writtenOf(fragments) {
70
+ return fragments.flatMap((f) => leaves(f.tree, [f.section]).flatMap(([path, value]) => {
71
+ const key = path.join(".");
72
+ const noted = spanningSections.get(key);
73
+ if (noted !== undefined)
74
+ return [{ key, example: noted, spansSections: true }];
75
+ return Array.isArray(value)
76
+ ? [{ key, example: JSON.stringify(value, null, "\t"), spansSections: false }]
77
+ : [];
78
+ }));
79
+ }
80
+ const hoconKey = (key) => (/^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key));
81
+ /** HOCON for the fragments, nested as they nest: `line` writes each leaf it keeps; a block keeping none is left out. */
82
+ function render(header, fragments, line) {
83
+ const block = (tree, path) => Object.entries(tree).flatMap(([name, value]) => {
84
+ const at = [...path, name];
85
+ if (!isTree(value))
86
+ return line(at.join("."), hoconKey(name))?.split("\n") ?? [];
87
+ const inner = block(value, at).map((l) => `\t${l}`);
88
+ return inner.length === 0 ? [] : [`${hoconKey(name)} {`, ...inner, "}"];
89
+ });
90
+ const tree = Object.fromEntries(fragments.map((f) => [f.section, f.tree]));
91
+ return `${[header, ...block(tree, [])].join("\n")}\n`;
92
+ }
93
+ /** Every scalar key reads its environment variable, and holds no value. */
94
+ export function applicationConf(fragments) {
95
+ const env = new Map(settingsOf(fragments).map((s) => [s.key, s.env]));
96
+ return render("# Each key reads its environment variable (see .env.example); this file holds no values.", fragments, (key, name) => (env.has(key) ? `${name} = \${?${env.get(key)}}` : undefined));
97
+ }
98
+ /** No live line in an example: `## ` opens prose, `# ` a value, so a copy sets nothing. */
99
+ const prose = "## ";
100
+ const commented = (text) => text.replace(/^/gm, "# ");
101
+ const example = (header, values) => `${[`${prose}${header}`, ...values.map(commented)].join("\n")}\n`;
102
+ /** Each list with its placeholders, and each value that spans sections, by its dotted key. */
103
+ export function localConfExample(fragments) {
104
+ return example("Copy to config/local.conf, uncomment each key and replace its placeholders; git and the image leave that file out.", writtenOf(fragments).map((w) => `${w.key.split(".").map(hoconKey).join(".")} = ${w.example}`));
105
+ }
106
+ export function envExample(settings) {
107
+ const quoted = (value) => (/^[^\s"'#\\]*$/.test(value) ? value : JSON.stringify(value));
108
+ return example("Copy to .env, uncomment each line and replace its placeholder; git and the image leave .env out.", settings.map((s) => `${s.env}=${quoted(s.placeholder)}`));
109
+ }
@@ -0,0 +1,2 @@
1
+ import type { FullPlan } from "./contract.js";
2
+ export declare function generateFull(plan: FullPlan): Promise<void>;