create-authup 1.0.0-beta.65

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 ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2021-2026 Peter Placzek <peter.placzek1996@gmail.com>
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # create-authup
2
+
3
+ Interactive wizard writing the deployment files for one authup installation: a `docker run` env file, a docker compose project, helm values, or a bare-metal project.
4
+
5
+ `npm create authup@latest` (npm resolves the `create-authup` package) asks a
6
+ handful of questions and writes the files for one of four targets into the
7
+ current directory. It scaffolds only. The `authup` CLI, or the `authup/authup`
8
+ image, is what runs the result; every file it writes does nothing but invoke
9
+ one of the two.
10
+
11
+ | Target | Files written | Next step |
12
+ |---|---|---|
13
+ | `docker` | `authup.env` | `docker run -d --name authup --restart unless-stopped --env-file authup.env -p <port>:3000 authup/authup:<version> start` |
14
+ | `compose` | `docker-compose.yml`, `.env` (plus `nginx.conf` and an nginx service under a console split) | `docker compose up -d` (with a worker split, `docker compose run --rm authup migration run` first) |
15
+ | `helm` | `values.yaml` | `helm repo add authup https://helm.authup.org`, then `helm install authup authup/authup -f values.yaml` |
16
+ | `bare-metal` | `package.json`, `authup.yml`, `.env` | `npm install`, `npx authup config validate`, `npm start` |
17
+
18
+ The prompt flow asks for the target, the public URL (the issuer, from which
19
+ every console url derives), the database (postgres or mysql, bundled alongside
20
+ or external; sqlite for bare metal only), whether registration and password
21
+ recovery are on and, when either is, the SMTP connection URL and whether
22
+ registration verifies email addresses, the admin password (the provisioning
23
+ default `start123`, a blank value and a value carrying both quote kinds are
24
+ refused; passwords are not echoed while typed), for helm over https whether
25
+ cert-manager issues the certificate, and for compose and helm whether the
26
+ worker and the consoles run as their own services. Any split turns redis on,
27
+ since a sign-in then crosses two API replicas through the cache; otherwise
28
+ redis is a question of its own.
29
+
30
+ The public URL decides two more things without a question. An explicit http
31
+ port is dialed directly, so it becomes the published or listen port; https or
32
+ a default port means a reverse proxy in front, and the listener stays on 3000.
33
+ `TRUST_PROXY` (`core.trustProxy` on bare metal) is written as the number of
34
+ proxies the deployment knows about, or `false` when nothing is in front, since
35
+ authup's own default trusts every `X-Forwarded-For` hop. Every emitted file
36
+ carries commented placeholders for `TRUSTED_ORIGINS`, `SECRETS_ENCRYPTION_KEY`
37
+ and the configuration file mount, so the options a real deployment reaches for
38
+ next are one uncomment away.
39
+
40
+ Two flags: `--force` overwrites files that already exist (without it the
41
+ conflicts are listed, nothing is written and the process exits 1), `--help`
42
+ prints the usage.
43
+
44
+ Versions are pinned to the wizard's own version: the image tag and the
45
+ `authup` dependency are both exactly `pkg.version`, never `latest` and never a
46
+ range, so run `npm create authup@latest` to scaffold the current release. Helm
47
+ is the exception and names no tag at all: the chart's `appVersion` owns the
48
+ image, so the chart version is what selects the release there. An emitted
49
+ `authup.yml` is checked with `npx authup config validate`, and the emitted helm
50
+ values are validated in this package's test suite against the chart's own
51
+ `values.schema.json`, vendored under `test/fixtures/`.
52
+
53
+ The package holds zero runtime dependencies: prompts ride
54
+ `node:readline/promises`, flags `node:util.parseArgs`, and every emitted
55
+ document is a plain string template. It never runs a service.
56
+
57
+ The deployment guides describe each target in full:
58
+ https://authup.org/guide/deployment/
@@ -0,0 +1 @@
1
+ export {}
package/dist/index.mjs ADDED
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env node
2
+ import fs, { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { parseArgs } from "node:util";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { Writable } from "node:stream";
8
+ //#region src/ask.ts
9
+ const CLOSED_MESSAGE = "Input ended before every question was answered.";
10
+ /**
11
+ * The readline seam. The streams are arguments rather than `process.stdin` / `process.stdout` reached for directly,
12
+ * so the echo behaviour below is testable without a terminal: a spec passes a pair of in-memory streams claiming to
13
+ * be one.
14
+ */
15
+ function createReadlineAsk(ctx = {}) {
16
+ const input = ctx.input || process.stdin;
17
+ const target = ctx.output || process.stdout;
18
+ const terminal = Boolean(input.isTTY && target.isTTY);
19
+ let muted = false;
20
+ const output = new Writable({ write(chunk, _encoding, callback) {
21
+ if (!muted) target.write(chunk);
22
+ callback();
23
+ } });
24
+ const rl = createInterface({
25
+ input,
26
+ output,
27
+ terminal
28
+ });
29
+ const lines = [];
30
+ let pending;
31
+ let closed = false;
32
+ rl.on("line", (line) => {
33
+ if (pending) {
34
+ const { resolve } = pending;
35
+ pending = void 0;
36
+ resolve(line);
37
+ return;
38
+ }
39
+ lines.push(line);
40
+ });
41
+ rl.on("close", () => {
42
+ closed = true;
43
+ if (pending) {
44
+ const { reject } = pending;
45
+ pending = void 0;
46
+ reject(/* @__PURE__ */ new Error(CLOSED_MESSAGE));
47
+ }
48
+ });
49
+ rl.on("SIGINT", () => process.exit(130));
50
+ function readLine() {
51
+ const buffered = lines.shift();
52
+ if (buffered !== void 0) return Promise.resolve({
53
+ line: buffered,
54
+ buffered: true
55
+ });
56
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error(CLOSED_MESSAGE));
57
+ return new Promise((resolve, reject) => {
58
+ pending = {
59
+ resolve: (line) => resolve({
60
+ line,
61
+ buffered: false
62
+ }),
63
+ reject
64
+ };
65
+ });
66
+ }
67
+ return {
68
+ ask: async (question, fallback, options) => {
69
+ rl.setPrompt(fallback === void 0 ? `${question}: ` : `${question} [${fallback}]: `);
70
+ rl.prompt();
71
+ const secret = terminal && Boolean(options?.secret);
72
+ muted = secret;
73
+ try {
74
+ const { line, buffered } = await readLine();
75
+ if (buffered && terminal && !secret) target.write(`${line}\n`);
76
+ return fallback !== void 0 && line.trim() === "" ? fallback : line;
77
+ } finally {
78
+ if (secret) {
79
+ muted = false;
80
+ target.write("\n");
81
+ }
82
+ }
83
+ },
84
+ close: () => rl.close()
85
+ };
86
+ }
87
+ //#endregion
88
+ //#region src/constants.ts
89
+ /**
90
+ * The emitted files that hold a password, written owner-only by `src/index.ts`. The compose file, `nginx.conf`,
91
+ * `package.json` and `authup.yml` carry none: every secret rides an `.env`, or `values.yaml` for helm.
92
+ * `test/unit/secret-files.spec.ts` holds the renderers to this list, so a new secret-bearing file cannot slip past it.
93
+ */
94
+ const SECRET_FILES = /* @__PURE__ */ new Set([
95
+ ".env",
96
+ "authup.env",
97
+ "values.yaml"
98
+ ]);
99
+ const TARGETS = [
100
+ "docker",
101
+ "compose",
102
+ "helm",
103
+ "bare-metal"
104
+ ];
105
+ //#endregion
106
+ //#region src/deployment.ts
107
+ /** The port the process listens on inside a container, and the default everywhere else. */
108
+ const LISTEN_PORT = 3e3;
109
+ function describeExposure(publicUrl) {
110
+ const url = new URL(publicUrl);
111
+ const behindProxy = url.protocol === "https:" || url.port === "";
112
+ return {
113
+ behindProxy,
114
+ hostPort: behindProxy ? LISTEN_PORT : Number(url.port)
115
+ };
116
+ }
117
+ function trustProxyValue(exposure, ownProxy = false) {
118
+ const hops = (exposure.behindProxy ? 1 : 0) + (ownProxy ? 1 : 0);
119
+ return hops > 0 ? String(hops) : "false";
120
+ }
121
+ //#endregion
122
+ //#region src/utils.ts
123
+ function quoteYaml(value) {
124
+ return JSON.stringify(value);
125
+ }
126
+ const PLAIN = /^[A-Za-z0-9_@:/.\-+=%?&,]*$/;
127
+ function isEnvRepresentable(value) {
128
+ return !value.includes("'") || !/\\[nr]/.test(value);
129
+ }
130
+ function quoteEnv(value) {
131
+ if (PLAIN.test(value)) return value;
132
+ if (!value.includes("'")) return `'${value}'`;
133
+ return `"${value}"`;
134
+ }
135
+ function quoteComposeEnv(value) {
136
+ if (PLAIN.test(value)) return value;
137
+ return `"${escapeComposeInterpolation(value.replace(/[\\"]/g, "\\$&"))}"`;
138
+ }
139
+ function escapeComposeInterpolation(value) {
140
+ return value.replace(/\$/g, "$$$$");
141
+ }
142
+ function indent(text, spaces) {
143
+ const prefix = " ".repeat(spaces);
144
+ return text.split("\n").map((line) => line.length > 0 ? prefix + line : line).join("\n");
145
+ }
146
+ //#endregion
147
+ //#region src/prompts.ts
148
+ const ENGINE_PORTS = {
149
+ postgres: 5432,
150
+ mysql: 3306
151
+ };
152
+ const BUNDLED_REDIS_URL = "redis://redis:6379";
153
+ const UNREPRESENTABLE = "A single quote together with a \\n or \\r sequence has no .env representation. Choose another value.";
154
+ const REDIS_NOTE = "Redis is required for a split deployment: the console sign-in and the token blocklist ride the cache.";
155
+ async function askUntil(ask, question, fallback, parse, options) {
156
+ let prefix = "";
157
+ for (;;) {
158
+ const parsed = parse(await ask(prefix + question, fallback, options));
159
+ if ("value" in parsed) return parsed.value;
160
+ prefix = `${parsed.error}\n`;
161
+ }
162
+ }
163
+ function askChoice(ask, question, options, fallback) {
164
+ return askUntil(ask, `${question} (${options.map((option, index) => `${index + 1}: ${option}`).join(", ")})`, fallback, (answer) => {
165
+ const normalized = answer.trim().toLowerCase();
166
+ if (options.includes(normalized)) return { value: normalized };
167
+ const numbered = options[Number(normalized) - 1];
168
+ if (numbered !== void 0) return { value: numbered };
169
+ return { error: `Pick one of ${options.join(", ")}, by name or number.` };
170
+ });
171
+ }
172
+ function askYesNo(ask, question, fallback) {
173
+ return askUntil(ask, `${question} (y/n)`, fallback ? "y" : "n", (answer) => {
174
+ const normalized = answer.trim().toLowerCase();
175
+ if (normalized === "y" || normalized === "yes") return { value: true };
176
+ if (normalized === "n" || normalized === "no") return { value: false };
177
+ return { error: "Answer y or n." };
178
+ });
179
+ }
180
+ function askUrl(ask, question, protocols = ["http:", "https:"], fallback) {
181
+ return askUntil(ask, question, fallback, (answer) => {
182
+ const trimmed = answer.trim();
183
+ try {
184
+ const url = new URL(trimmed);
185
+ if (protocols.includes(url.protocol) && url.hostname !== "") return { value: trimmed };
186
+ } catch {}
187
+ return { error: `Enter an absolute URL using ${protocols.map((protocol) => `${protocol}//`).join(" or ")}.` };
188
+ });
189
+ }
190
+ function askText(ask, question, fallback) {
191
+ return askUntil(ask, question, fallback, (answer) => {
192
+ const trimmed = answer.trim();
193
+ if (trimmed === "") return { error: "A value is required." };
194
+ return isEnvRepresentable(trimmed) ? { value: trimmed } : { error: UNREPRESENTABLE };
195
+ });
196
+ }
197
+ function askPublicUrl(ask, target) {
198
+ return askUntil(ask, "Public URL: the address a browser reaches authup at. It is the OIDC issuer, and every console url derives from it", void 0, (answer) => {
199
+ const trimmed = answer.trim();
200
+ let url;
201
+ try {
202
+ url = new URL(trimmed);
203
+ } catch {
204
+ return { error: "Enter an absolute URL using http:// or https://." };
205
+ }
206
+ if (url.protocol !== "http:" && url.protocol !== "https:") return { error: "Enter an absolute URL using http:// or https://." };
207
+ if (url.username || url.password || url.search || url.hash) return { error: "The public URL is the OIDC issuer: no credentials, query or fragment." };
208
+ if (target === "helm" && url.pathname !== "/") return { error: "The helm chart publishes authup at the origin root: enter the URL without a path." };
209
+ return { value: trimmed };
210
+ });
211
+ }
212
+ function askPort(ask, question, fallback) {
213
+ return askUntil(ask, question, String(fallback), (answer) => {
214
+ const port = Number(answer.trim());
215
+ if (Number.isSafeInteger(port) && port >= 1 && port <= 65535) return { value: port };
216
+ return { error: "Enter a port between 1 and 65535." };
217
+ });
218
+ }
219
+ function askSecret(ask, question) {
220
+ return askUntil(ask, question, void 0, (answer) => {
221
+ if (answer.trim() === "") return { error: "A value is required." };
222
+ if (answer === "start123") return { error: "start123 is the provisioning default the wizard refuses to emit. Choose another value." };
223
+ if (answer.length < 3 || answer.length > 256) return { error: "Use between 3 and 256 characters." };
224
+ if (!isEnvRepresentable(answer)) return { error: UNREPRESENTABLE };
225
+ return { value: answer };
226
+ }, { secret: true });
227
+ }
228
+ async function askDatabase(ask, target) {
229
+ const type = await askChoice(ask, "Database", target === "bare-metal" ? [
230
+ "postgres",
231
+ "mysql",
232
+ "sqlite"
233
+ ] : ["postgres", "mysql"], "postgres");
234
+ if (type === "sqlite") return { type: "better-sqlite3" };
235
+ if ((target === "compose" || target === "helm") && await askYesNo(ask, "Run the database alongside authup?", true)) return {
236
+ type,
237
+ bundled: true,
238
+ host: type,
239
+ port: ENGINE_PORTS[type],
240
+ username: "authup",
241
+ password: await askSecret(ask, "Database password"),
242
+ database: "authup"
243
+ };
244
+ return {
245
+ type,
246
+ bundled: false,
247
+ host: await askText(ask, target === "bare-metal" ? "Database host" : "Database host (a name the container resolves; 127.0.0.1 would be the container itself)", target === "bare-metal" ? "127.0.0.1" : void 0),
248
+ port: await askPort(ask, "Database port", ENGINE_PORTS[type]),
249
+ username: await askText(ask, "Database username", "authup"),
250
+ password: await askSecret(ask, "Database password"),
251
+ database: await askText(ask, "Database name", "authup")
252
+ };
253
+ }
254
+ async function collectAnswers(ask) {
255
+ const target = await askChoice(ask, "Deployment target", TARGETS, "compose");
256
+ const publicUrl = await askPublicUrl(ask, target);
257
+ const tlsCertManager = target === "helm" && publicUrl.startsWith("https:") && await askYesNo(ask, "Request the TLS certificate from cert-manager? The alternative is a <hostname>-tls secret in the release namespace", false);
258
+ const db = await askDatabase(ask, target);
259
+ const registrationEnabled = await askYesNo(ask, "Enable user registration?", false);
260
+ const passwordRecoveryEnabled = await askYesNo(ask, "Enable password recovery?", false);
261
+ const smtp = registrationEnabled || passwordRecoveryEnabled ? { url: await askUrl(ask, "SMTP connection URL (smtp(s)://user:pass@host:port)", ["smtp:", "smtps:"]) } : false;
262
+ const emailVerificationEnabled = registrationEnabled && await askYesNo(ask, "Verify email addresses on registration?", true);
263
+ const adminPassword = await askSecret(ask, "Admin password");
264
+ const splittable = target === "compose" || target === "helm";
265
+ const workerSplit = splittable && await askYesNo(ask, "Run the background worker as a separate service?", false);
266
+ const consoleSplit = splittable && await askYesNo(ask, "Run the consoles as a separate service?", false);
267
+ const notes = [];
268
+ let redis = false;
269
+ if (workerSplit || consoleSplit) {
270
+ redis = { url: BUNDLED_REDIS_URL };
271
+ notes.push(REDIS_NOTE);
272
+ } else if (await askYesNo(ask, "Use redis?", false)) redis = { url: splittable ? BUNDLED_REDIS_URL : await askUrl(ask, target === "bare-metal" ? "Redis URL" : "Redis URL (a host the container resolves)", ["redis:", "rediss:"], target === "bare-metal" ? "redis://127.0.0.1:6379" : void 0) };
273
+ return {
274
+ answers: {
275
+ target,
276
+ publicUrl,
277
+ db,
278
+ redis,
279
+ smtp,
280
+ registrationEnabled,
281
+ passwordRecoveryEnabled,
282
+ emailVerificationEnabled,
283
+ adminPassword,
284
+ workerSplit,
285
+ consoleSplit,
286
+ tlsCertManager
287
+ },
288
+ notes
289
+ };
290
+ }
291
+ //#endregion
292
+ //#region src/targets/docker.ts
293
+ function dockerRunCommand(version, hostPort) {
294
+ return `docker run -d --name authup --restart unless-stopped --env-file authup.env -p ${hostPort}:3000 authup/authup:${version} start`;
295
+ }
296
+ function renderDocker(answers, version) {
297
+ if (answers.db.type === "better-sqlite3") throw new Error("The docker image runs in production mode and refuses sqlite: configure a postgres or mysql database.");
298
+ if (answers.workerSplit || answers.consoleSplit) throw new Error("The docker run target is one container: a worker or console split needs the compose or helm target.");
299
+ const exposure = describeExposure(answers.publicUrl);
300
+ const lines = [
301
+ `PUBLIC_URL=${answers.publicUrl}`,
302
+ "# The proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default.",
303
+ `TRUST_PROXY=${trustProxyValue(exposure)}`,
304
+ `DB_TYPE=${answers.db.type}`,
305
+ `DB_HOST=${answers.db.host}`,
306
+ `DB_PORT=${answers.db.port}`,
307
+ `DB_USERNAME=${answers.db.username}`,
308
+ `DB_PASSWORD=${answers.db.password}`,
309
+ `DB_DATABASE=${answers.db.database}`
310
+ ];
311
+ if (answers.redis) lines.push(`REDIS=${answers.redis.url}`);
312
+ if (answers.smtp) lines.push(`SMTP=${answers.smtp.url}`);
313
+ if (answers.registrationEnabled) lines.push("REGISTRATION_ENABLED=true");
314
+ if (answers.passwordRecoveryEnabled) lines.push("PASSWORD_RECOVERY_ENABLED=true");
315
+ if (answers.emailVerificationEnabled) lines.push("EMAIL_VERIFICATION_ENABLED=true");
316
+ lines.push(`USER_ADMIN_PASSWORD=${answers.adminPassword}`, "# Every downstream application origin, comma-separated; each may obtain a full-permission token.", "# TRUSTED_ORIGINS=https://app.example.com", "# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.", "# SECRETS_ENCRYPTION_KEY=<base64 32 bytes>");
317
+ return { "authup.env": [
318
+ "# Environment of the authup container. It holds secrets: keep it out of version control.",
319
+ `# ${dockerRunCommand(version, exposure.hostPort)}`,
320
+ "# The database must exist already; the image runs in production mode and refuses sqlite.",
321
+ "# A configuration file or a provisioning directory is mounted with",
322
+ "# -v ./authup.yml:/etc/authup/authup.yml:ro -v ./provisioning:/etc/authup/provisioning:ro",
323
+ "# https://authup.org/guide/deployment/docker",
324
+ ...lines,
325
+ ""
326
+ ].join("\n") };
327
+ }
328
+ //#endregion
329
+ //#region src/targets/helm.ts
330
+ const CHART_REQUIREMENT = "# The worker and the console split need the authup helm chart 0.4.0 or later.";
331
+ const HELM_COMMANDS = ["helm repo add authup https://helm.authup.org", "helm install authup authup/authup -f values.yaml"];
332
+ function renderHelm(answers, _version) {
333
+ const { db } = answers;
334
+ if (db.type === "better-sqlite3") throw new Error("The helm chart needs a server database (postgres or mysql); the image refuses sqlite.");
335
+ const url = new URL(answers.publicUrl);
336
+ const tls = url.protocol === "https:";
337
+ const lines = [
338
+ `# Written by npm create authup. Install with: ${HELM_COMMANDS.join(" && ")}`,
339
+ "# Secrets in this file (auth.adminPassword, the database password, smtp.connectionString) can move to the chart's existingSecret keys; see the chart README.",
340
+ ...answers.workerSplit || answers.consoleSplit ? [CHART_REQUIREMENT] : [],
341
+ "server:",
342
+ ` publicUrl: ${quoteYaml(answers.publicUrl)}`,
343
+ ...tls && !answers.tlsCertManager ? [` # TLS needs a secret named ${url.hostname}-tls in the release namespace, or set server.ingress.certManager: true.`] : [],
344
+ " ingress:",
345
+ " enabled: true",
346
+ ` hostname: ${quoteYaml(url.hostname)}`,
347
+ ` tls: ${tls}`,
348
+ ...tls && answers.tlsCertManager ? [" certManager: true"] : []
349
+ ];
350
+ if (answers.registrationEnabled || answers.passwordRecoveryEnabled || answers.emailVerificationEnabled) {
351
+ lines.push(" features:");
352
+ if (answers.registrationEnabled) lines.push(" registration: true");
353
+ if (answers.passwordRecoveryEnabled) lines.push(" passwordRecovery: true");
354
+ if (answers.emailVerificationEnabled) lines.push(" emailVerification: true");
355
+ }
356
+ if (answers.workerSplit) lines.push(" migration:", " enabled: true");
357
+ if (answers.consoleSplit) lines.push(" splitConsoles: true");
358
+ lines.push(" # Every downstream application origin; each may obtain a full-permission token.", " # trustedOrigins:", " # - https://app.example.com", " # An authup.yml the chart mounts, for the options that only the file can carry.", " # configuration: |", " # core:", " # passwordMinLength: 12");
359
+ if (answers.workerSplit) lines.push("", "worker:", " enabled: true");
360
+ lines.push("", "database:", ` type: ${db.type}`, "");
361
+ for (const engine of ["postgresql", "mysql"]) {
362
+ const enabled = db.bundled && db.type === (engine === "postgresql" ? "postgres" : "mysql");
363
+ lines.push(`${engine}:`, ` enabled: ${enabled}`);
364
+ if (enabled) lines.push(" auth:", ` password: ${quoteYaml(db.password)}`);
365
+ }
366
+ if (!db.bundled) lines.push("externalDatabase:", ` host: ${quoteYaml(db.host)}`, ` port: ${quoteYaml(String(db.port))}`, ` user: ${quoteYaml(db.username)}`, ` database: ${quoteYaml(db.database)}`, ` password: ${quoteYaml(db.password)}`);
367
+ if (answers.redis) lines.push("", "valkey:", " enabled: true", "# For an external Redis set valkey.enabled to false and externalRedis.url to its redis:// URL instead.");
368
+ lines.push("", "auth:", ` adminPassword: ${quoteYaml(answers.adminPassword)}`, " # Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.", " # secretsEncryptionKey: <base64 32 bytes>");
369
+ if (answers.smtp) lines.push("", "smtp:", ` connectionString: ${quoteYaml(answers.smtp.url)}`);
370
+ return { "values.yaml": `${lines.join("\n")}\n` };
371
+ }
372
+ //#endregion
373
+ //#region src/targets/bare-metal.ts
374
+ const SCHEMA_COMMENT = "# yaml-language-server: $schema=https://authup.org/schema/config.json";
375
+ const DATABASE_DOCS_URL = "https://authup.org/guide/deployment/configuration-server-core-database";
376
+ function renderBareMetal(answers, version) {
377
+ if (answers.workerSplit || answers.consoleSplit) throw new Error("The bare-metal target runs one process: a worker or console split is a second process under a process manager, which the wizard does not write.");
378
+ return {
379
+ "package.json": renderPackageJson(version),
380
+ "authup.yml": renderConfig(answers),
381
+ ".env": renderEnv(answers)
382
+ };
383
+ }
384
+ function renderPackageJson(version) {
385
+ return `${JSON.stringify({
386
+ name: "authup-deployment",
387
+ private: true,
388
+ type: "module",
389
+ scripts: { start: "authup start" },
390
+ dependencies: { authup: version }
391
+ }, null, 4)}\n`;
392
+ }
393
+ function renderConfig(answers) {
394
+ const { db } = answers;
395
+ const lines = [SCHEMA_COMMENT, `publicUrl: ${quoteYaml(answers.publicUrl)}`];
396
+ if (db.type === "better-sqlite3") lines.push("# Production refuses sqlite, so this file keeps the development default of env.", `# That is fine for a local trial only: ${DATABASE_DOCS_URL}`, "", "# db.sqlite is written into this directory.");
397
+ else lines.push("env: production", "", "# The database connection is the DB_* set in .env.");
398
+ if (answers.redis) lines.push("", `redis: ${quoteYaml(answers.redis.url)}`);
399
+ if (answers.smtp) lines.push("", "# The connection URL is SMTP in .env.", "smtp: true");
400
+ lines.push("", "# Every downstream application origin; each may obtain a full-permission token.", "# trustedOrigins:", "# - https://app.example.com");
401
+ const exposure = describeExposure(answers.publicUrl);
402
+ lines.push("", "core:", ...exposure.hostPort !== 3e3 ? [` port: ${exposure.hostPort}`] : [], " # The proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default.", ` trustProxy: ${trustProxyValue(exposure)}`, ...answers.registrationEnabled ? [" registrationEnabled: true"] : [], ...answers.passwordRecoveryEnabled ? [" passwordRecoveryEnabled: true"] : [], ...answers.emailVerificationEnabled ? [" emailVerificationEnabled: true"] : []);
403
+ return `${lines.join("\n")}\n`;
404
+ }
405
+ function renderEnv(answers) {
406
+ const lines = ["# Secrets. authup start reads this file from the directory it is started in. Keep it out of version control."];
407
+ const { db } = answers;
408
+ if (db.type !== "better-sqlite3") lines.push(`DB_TYPE=${db.type}`, `DB_HOST=${quoteEnv(db.host)}`, `DB_PORT=${db.port}`, `DB_USERNAME=${quoteEnv(db.username)}`, `DB_PASSWORD=${quoteEnv(db.password)}`, `DB_DATABASE=${quoteEnv(db.database)}`);
409
+ lines.push(`USER_ADMIN_PASSWORD=${quoteEnv(answers.adminPassword)}`);
410
+ if (answers.smtp) lines.push(`SMTP=${quoteEnv(answers.smtp.url)}`);
411
+ lines.push("# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.", "# SECRETS_ENCRYPTION_KEY=");
412
+ return `${lines.join("\n")}\n`;
413
+ }
414
+ //#endregion
415
+ //#region src/targets/compose.ts
416
+ const DOCS_URL$1 = "https://authup.org/guide/deployment";
417
+ const NGINX_CONF = `# Written by npm create authup: the console split's routing, ${DOCS_URL$1}/console-replicas
418
+ # TLS, if any, terminates in front of this proxy; X-Forwarded-Proto is passed through as the scheme it sees.
419
+ upstream authup_api { server authup:3000; }
420
+ upstream authup_auth_console { server authup-console:3020; }
421
+ upstream authup_admin_console { server authup-console:3021; }
422
+ upstream authup_account_console { server authup-console:3022; }
423
+
424
+ server {
425
+ listen 80;
426
+ # nginx listens on 80 inside the container while the published port is another one: a redirect it builds itself
427
+ # (the trailing slash it adds to /console/<name>) must stay relative or it would send the browser to port 80.
428
+ absolute_redirect off;
429
+
430
+ # The sign-in pair stays on the API. Exact matches, so they win over the prefix rules below.
431
+ location = /console/admin/login/start { proxy_pass http://authup_api; }
432
+ location = /console/admin/callback { proxy_pass http://authup_api; }
433
+ location = /console/account/login/start { proxy_pass http://authup_api; }
434
+ location = /console/account/callback { proxy_pass http://authup_api; }
435
+
436
+ location /console/auth/ {
437
+ rewrite ^/console/auth(/.*)$ $1 break;
438
+ proxy_set_header Host $host;
439
+ proxy_set_header X-Forwarded-Proto $scheme;
440
+ proxy_set_header X-Forwarded-Tls-Client-Cert "";
441
+ proxy_pass http://authup_auth_console;
442
+ }
443
+
444
+ location /console/admin/ {
445
+ rewrite ^/console/admin(/.*)$ $1 break;
446
+ proxy_set_header Host $host;
447
+ proxy_set_header X-Forwarded-Proto $scheme;
448
+ proxy_set_header X-Forwarded-Tls-Client-Cert "";
449
+ proxy_pass http://authup_admin_console;
450
+ }
451
+
452
+ location /console/account/ {
453
+ rewrite ^/console/account(/.*)$ $1 break;
454
+ proxy_set_header Host $host;
455
+ proxy_set_header X-Forwarded-Proto $scheme;
456
+ proxy_set_header X-Forwarded-Tls-Client-Cert "";
457
+ proxy_pass http://authup_account_console;
458
+ }
459
+
460
+ location / {
461
+ proxy_set_header Host $host;
462
+ proxy_set_header X-Real-IP $remote_addr;
463
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
464
+ proxy_set_header X-Forwarded-Proto $scheme;
465
+ proxy_set_header X-Forwarded-Tls-Client-Cert "";
466
+ proxy_pass http://authup_api;
467
+ }
468
+ }
469
+ `;
470
+ const ENGINES = {
471
+ postgres: {
472
+ image: "postgres:17",
473
+ port: 5432,
474
+ dataPath: "/var/lib/postgresql/data"
475
+ },
476
+ mysql: {
477
+ image: "mysql:8.4",
478
+ port: 3306,
479
+ dataPath: "/var/lib/mysql"
480
+ }
481
+ };
482
+ function env(key, value) {
483
+ return `- ${quoteYaml(`${key}=${escapeComposeInterpolation(value)}`)}`;
484
+ }
485
+ function ref(key, variable) {
486
+ return `- ${quoteYaml(`${key}=\${${variable}}`)}`;
487
+ }
488
+ function block(key, lines) {
489
+ return [`${key}:`, ...indent(lines.join("\n"), 2).split("\n")];
490
+ }
491
+ function renderCompose(answers, version) {
492
+ const { db } = answers;
493
+ if (db.type === "better-sqlite3") throw new Error("The compose target needs a server database (postgres or mysql): the image runs in production mode and refuses sqlite.");
494
+ const image = `image: authup/authup:${version}`;
495
+ const restart = "restart: unless-stopped";
496
+ const engine = ENGINES[db.type];
497
+ const exposure = describeExposure(answers.publicUrl);
498
+ const dbEnv = [
499
+ env("DB_TYPE", db.type),
500
+ env("DB_HOST", db.bundled ? db.type : db.host),
501
+ env("DB_PORT", String(db.bundled ? engine.port : db.port)),
502
+ env("DB_USERNAME", db.username),
503
+ ref("DB_PASSWORD", "DB_PASSWORD"),
504
+ env("DB_DATABASE", db.database)
505
+ ];
506
+ if (answers.redis) dbEnv.push(env("REDIS", "redis://redis:6379"));
507
+ const dependsOn = [];
508
+ if (db.bundled) dependsOn.push(`${db.type}:`, " condition: service_healthy");
509
+ if (answers.redis) dependsOn.push("redis:", " condition: service_started");
510
+ const dependsOnBlock = dependsOn.length > 0 ? block("depends_on", dependsOn) : [];
511
+ const authupEnv = [
512
+ env("PUBLIC_URL", answers.publicUrl),
513
+ "# the proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default",
514
+ env("TRUST_PROXY", trustProxyValue(exposure, answers.consoleSplit)),
515
+ ...dbEnv,
516
+ ref("USER_ADMIN_PASSWORD", "USER_ADMIN_PASSWORD")
517
+ ];
518
+ if (answers.smtp) authupEnv.push(ref("SMTP", "SMTP"));
519
+ if (answers.registrationEnabled) authupEnv.push(env("REGISTRATION_ENABLED", "true"));
520
+ if (answers.passwordRecoveryEnabled) authupEnv.push(env("PASSWORD_RECOVERY_ENABLED", "true"));
521
+ if (answers.emailVerificationEnabled) authupEnv.push(env("EMAIL_VERIFICATION_ENABLED", "true"));
522
+ authupEnv.push("# every downstream application origin, comma-separated; each may obtain a full-permission token", "# - \"TRUSTED_ORIGINS=https://app.example.com\"", "# wraps the realm key store at rest (base64, 32 bytes); write-once, back it up", "# - \"SECRETS_ENCRYPTION_KEY=<base64 32 bytes>\"");
523
+ if (answers.workerSplit) authupEnv.push("# the sweeps run in the worker below", env("WORKER_ENABLED", "false"), "# run \"docker compose run --rm authup migration run\" once before starting", env("MIGRATION_ENABLED", "false"));
524
+ const services = block("authup", [
525
+ image,
526
+ restart,
527
+ ...answers.consoleSplit ? [] : block("ports", [`- "${exposure.hostPort}:3000"`]),
528
+ "# a configuration file and a provisioning directory are optional; mount them under /etc/authup",
529
+ "# volumes:",
530
+ "# - ./authup.yml:/etc/authup/authup.yml:ro",
531
+ "# - ./provisioning:/etc/authup/provisioning:ro",
532
+ ...dependsOnBlock,
533
+ ...block("environment", authupEnv),
534
+ `command: ${answers.consoleSplit ? "start core" : "start"}`
535
+ ]);
536
+ if (db.bundled) {
537
+ const credentials = db.type === "postgres" ? [
538
+ env("POSTGRES_USER", db.username),
539
+ ref("POSTGRES_PASSWORD", "DB_PASSWORD"),
540
+ env("POSTGRES_DB", db.database)
541
+ ] : [
542
+ env("MYSQL_USER", db.username),
543
+ ref("MYSQL_PASSWORD", "DB_PASSWORD"),
544
+ ref("MYSQL_ROOT_PASSWORD", "DB_PASSWORD"),
545
+ env("MYSQL_DATABASE", db.database)
546
+ ];
547
+ const probe = db.type === "postgres" ? `test: ["CMD", "pg_isready", "-h", "localhost", "-U", ${quoteYaml(escapeComposeInterpolation(db.username))}, "-d", ${quoteYaml(escapeComposeInterpolation(db.database))}]` : "test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"127.0.0.1\"]";
548
+ services.push("", ...block(db.type, [
549
+ `image: ${engine.image}`,
550
+ restart,
551
+ ...block("volumes", [`- ${db.type}_data:${engine.dataPath}`]),
552
+ ...block("environment", credentials),
553
+ ...block("healthcheck", [
554
+ probe,
555
+ "interval: 5s",
556
+ "timeout: 5s",
557
+ "retries: 24"
558
+ ])
559
+ ]));
560
+ }
561
+ if (answers.redis) services.push("", ...block("redis", ["image: redis:7", restart]));
562
+ if (answers.workerSplit) services.push("", "# one instance is enough, whatever the API scales to", ...block("authup-worker", [
563
+ image,
564
+ restart,
565
+ ...dependsOnBlock,
566
+ ...block("environment", [env("PUBLIC_URL", answers.publicUrl), ...dbEnv]),
567
+ "# the image healthcheck probes an HTTP port this process never opens",
568
+ ...block("healthcheck", ["disable: true"]),
569
+ "command: start worker"
570
+ ]));
571
+ if (answers.consoleSplit) services.push("", "# Two proxy rules, in this order: /console/<name>/login/start and", "# /console/<name>/callback (admin and account) go to the authup service;", "# everything else under /console/** goes here, with the /console/<name>", `# prefix stripped. See ${DOCS_URL$1}/console-replicas`, ...block("authup-console", [
572
+ image,
573
+ restart,
574
+ ...block("environment", [
575
+ env("PUBLIC_URL", answers.publicUrl),
576
+ "# the auth console renders server-side and calls the API itself: reach it on this network",
577
+ env("INTERNAL_URL", "http://authup:3000")
578
+ ]),
579
+ "# the image probes the API port, which this process never opens; the auth",
580
+ "# console cannot be disabled, so it is always listening (3020 by default)",
581
+ ...block("healthcheck", ["test: [\"CMD\", \"wget\", \"--spider\", \"--proxy\", \"off\", \"http://127.0.0.1:3020/healthy\"]", "interval: 10s"]),
582
+ "command: start console"
583
+ ]), "", "# The proxy owns the published port and carries the two console routing rules (nginx.conf).", ...block("nginx", [
584
+ "image: nginx:1.31-alpine",
585
+ restart,
586
+ ...block("ports", [`- "${exposure.hostPort}:80"`]),
587
+ ...block("volumes", ["- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro"]),
588
+ ...block("depends_on", [
589
+ "authup:",
590
+ " condition: service_started",
591
+ "authup-console:",
592
+ " condition: service_started"
593
+ ])
594
+ ]));
595
+ const compose = [`# Written by npm create authup. Secrets live in .env next to this file: ${DOCS_URL$1}/docker-compose`, ""];
596
+ if (db.bundled) compose.push(...block("volumes", [`${db.type}_data:`]), "");
597
+ compose.push(...block("services", services));
598
+ const dotenv = [
599
+ "# Secrets docker-compose.yml interpolates. Keep this file out of version control.",
600
+ `DB_PASSWORD=${quoteComposeEnv(db.password)}`,
601
+ `USER_ADMIN_PASSWORD=${quoteComposeEnv(answers.adminPassword)}`
602
+ ];
603
+ if (answers.smtp) dotenv.push(`SMTP=${quoteComposeEnv(answers.smtp.url)}`);
604
+ dotenv.push("# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.", "# SECRETS_ENCRYPTION_KEY=");
605
+ return {
606
+ "docker-compose.yml": `${compose.join("\n")}\n`,
607
+ ".env": `${dotenv.join("\n")}\n`,
608
+ ...answers.consoleSplit ? { "nginx.conf": NGINX_CONF } : {}
609
+ };
610
+ }
611
+ //#endregion
612
+ //#region src/targets/index.ts
613
+ const RENDERERS = {
614
+ docker: renderDocker,
615
+ compose: renderCompose,
616
+ helm: renderHelm,
617
+ "bare-metal": renderBareMetal
618
+ };
619
+ //#endregion
620
+ //#region src/version.ts
621
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
622
+ //#endregion
623
+ //#region src/index.ts
624
+ const DOCS_URL = "https://authup.org/guide/deployment/";
625
+ const USAGE = `Usage: npx create-authup [--force] [--help]
626
+ npm create authup -- [--force] [--help]
627
+
628
+ Writes the deployment files for one authup installation into the current directory.
629
+
630
+ Targets: ${TARGETS.join(", ")}
631
+
632
+ Flags:
633
+ --force overwrite files that already exist
634
+ --help print this text`;
635
+ function nextSteps(answers) {
636
+ return {
637
+ docker: [dockerRunCommand(VERSION, describeExposure(answers.publicUrl).hostPort), "The database must already exist: the image runs in production mode and refuses sqlite."],
638
+ compose: [
639
+ ...answers.workerSplit ? ["docker compose run --rm authup migration run"] : [],
640
+ "docker compose up -d",
641
+ ...answers.consoleSplit ? ["The nginx service publishes the port; TLS, if any, terminates in front of it."] : []
642
+ ],
643
+ helm: [...HELM_COMMANDS],
644
+ "bare-metal": [
645
+ "npm install",
646
+ "npx authup config validate",
647
+ "npm start"
648
+ ]
649
+ }[answers.target];
650
+ }
651
+ async function main() {
652
+ const { values } = parseArgs({
653
+ options: {
654
+ force: {
655
+ type: "boolean",
656
+ default: false
657
+ },
658
+ help: {
659
+ type: "boolean",
660
+ default: false
661
+ }
662
+ },
663
+ allowPositionals: false
664
+ });
665
+ if (values.help) {
666
+ console.log(USAGE);
667
+ return;
668
+ }
669
+ console.log(`create-authup ${VERSION}`);
670
+ console.log(`Files are written into the current directory (${process.cwd()}).`);
671
+ const { ask, close } = createReadlineAsk();
672
+ let collected;
673
+ try {
674
+ collected = await collectAnswers(ask);
675
+ } finally {
676
+ close();
677
+ }
678
+ const { answers, notes } = collected;
679
+ const rendered = RENDERERS[answers.target](answers, VERSION);
680
+ const files = Object.keys(rendered);
681
+ const directories = files.filter((name) => {
682
+ try {
683
+ return fs.statSync(path.join(process.cwd(), name)).isDirectory();
684
+ } catch {
685
+ return false;
686
+ }
687
+ });
688
+ if (directories.length > 0) {
689
+ console.error(`Cannot write over a directory: ${directories.join(", ")}.`);
690
+ process.exit(1);
691
+ }
692
+ const existing = files.filter((name) => fs.existsSync(path.join(process.cwd(), name)));
693
+ if (existing.length > 0 && !values.force) {
694
+ console.error(`Refusing to overwrite: ${existing.join(", ")}. Re-run with --force.`);
695
+ process.exit(1);
696
+ }
697
+ for (const name of files) {
698
+ const target = path.join(process.cwd(), name);
699
+ const secret = SECRET_FILES.has(name);
700
+ fs.writeFileSync(target, rendered[name], secret ? {
701
+ encoding: "utf8",
702
+ mode: 384
703
+ } : "utf8");
704
+ if (secret) fs.chmodSync(target, 384);
705
+ }
706
+ for (const note of notes) console.log(note);
707
+ console.log(`Written: ${files.join(", ")}`);
708
+ console.log("Next steps:");
709
+ for (const step of nextSteps(answers)) console.log(` ${step}`);
710
+ console.log(`Docs: ${DOCS_URL}`);
711
+ }
712
+ main().catch((e) => {
713
+ console.error(e instanceof Error ? e.message : String(e));
714
+ process.exit(1);
715
+ });
716
+ //#endregion
717
+ export {};
718
+
719
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["DOCS_URL"],"sources":["../src/ask.ts","../src/constants.ts","../src/deployment.ts","../src/utils.ts","../src/prompts.ts","../src/targets/docker.ts","../src/targets/helm.ts","../src/targets/bare-metal.ts","../src/targets/compose.ts","../src/targets/index.ts","../src/version.ts","../src/index.ts"],"sourcesContent":["/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport process from 'node:process';\nimport { createInterface } from 'node:readline/promises';\nimport { Writable } from 'node:stream';\nimport type { ReadlineAskContext, ReadlineAskResult } from './types.ts';\n\ntype PendingLine = {\n resolve: (line: string) => void,\n reject: (error: Error) => void\n};\n\nconst CLOSED_MESSAGE = 'Input ended before every question was answered.';\n\n/**\n * The readline seam. The streams are arguments rather than `process.stdin` / `process.stdout` reached for directly,\n * so the echo behaviour below is testable without a terminal: a spec passes a pair of in-memory streams claiming to\n * be one.\n */\nexport function createReadlineAsk(ctx: ReadlineAskContext = {}): ReadlineAskResult {\n const input = ctx.input || process.stdin;\n const target = ctx.output || process.stdout;\n\n // On a terminal readline echoes what is typed through the output stream itself, so muting that stream while a\n // secret's line is read is what keeps the password off the screen. Piped input takes none of this path.\n const terminal = Boolean(input.isTTY && target.isTTY);\n let muted = false;\n const output = new Writable({\n write(chunk, _encoding, callback) {\n if (!muted) {\n target.write(chunk);\n }\n callback();\n },\n });\n const rl = createInterface({\n input,\n output,\n terminal,\n });\n\n // readline emits every line of one stdin chunk synchronously, while the next question is only registered on a\n // later microtask, so `rl.question` drops all but the first answer of a piped script. The lines are buffered\n // instead, and a close with a question still open is an error rather than the silent exit 0 node answers with.\n const lines: string[] = [];\n let pending: PendingLine | undefined;\n let closed = false;\n\n rl.on('line', (line) => {\n if (pending) {\n const { resolve } = pending;\n pending = undefined;\n resolve(line);\n return;\n }\n\n lines.push(line);\n });\n\n rl.on('close', () => {\n closed = true;\n if (pending) {\n const { reject } = pending;\n pending = undefined;\n reject(new Error(CLOSED_MESSAGE));\n }\n });\n\n // Ctrl+C on a TTY reaches readline rather than the process; without this it closes the interface and reads as EOF.\n rl.on('SIGINT', () => process.exit(130));\n\n function readLine(): Promise<{ line: string, buffered: boolean }> {\n const buffered = lines.shift();\n if (buffered !== undefined) {\n return Promise.resolve({ line: buffered, buffered: true });\n }\n\n if (closed) {\n return Promise.reject(new Error(CLOSED_MESSAGE));\n }\n\n return new Promise((resolve, reject) => {\n pending = { resolve: (line) => resolve({ line, buffered: false }), reject };\n });\n }\n\n return {\n // The answer is handed over untrimmed: a secret keeps its whitespace, and each parser trims what it should.\n ask: async (question, fallback, options) => {\n rl.setPrompt(fallback === undefined ? `${question}: ` : `${question} [${fallback}]: `);\n rl.prompt();\n const secret = terminal && Boolean(options?.secret);\n muted = secret;\n try {\n const { line, buffered } = await readLine();\n if (buffered && terminal && !secret) {\n // a type-ahead answer never echoed after this prompt, so the next prompt would overwrite the line\n target.write(`${line}\\n`);\n }\n\n return fallback !== undefined && line.trim() === '' ? fallback : line;\n } finally {\n if (secret) {\n // the Enter was swallowed with the echo\n muted = false;\n target.write('\\n');\n }\n }\n },\n close: () => rl.close(),\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Target } from './types.ts';\n\n/**\n * The emitted files that hold a password, written owner-only by `src/index.ts`. The compose file, `nginx.conf`,\n * `package.json` and `authup.yml` carry none: every secret rides an `.env`, or `values.yaml` for helm.\n * `test/unit/secret-files.spec.ts` holds the renderers to this list, so a new secret-bearing file cannot slip past it.\n */\nexport const SECRET_FILES = new Set(['.env', 'authup.env', 'values.yaml']);\n\nexport const TARGETS: readonly Target[] = ['docker', 'compose', 'helm', 'bare-metal'] as const;\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Exposure } from './types.ts';\n\n/** The port the process listens on inside a container, and the default everywhere else. */\nexport const LISTEN_PORT = 3000;\n\n// A public url with an explicit port is dialed directly, so that port is the one to publish or to listen on. A\n// default port, or https, means a reverse proxy in front owns the port a browser dials and reaches the listener\n// on its own, so the default listen port stays. `http://localhost` with no port is read as a proxy too; the\n// operator who really serves port 80 directly edits the mapping.\nexport function describeExposure(publicUrl: string): Exposure {\n const url = new URL(publicUrl);\n const behindProxy = url.protocol === 'https:' || url.port === '';\n\n return {\n behindProxy,\n hostPort: behindProxy ? LISTEN_PORT : Number(url.port),\n };\n}\n\n// The value core.trustProxy / TRUST_PROXY takes: the number of proxy hops the deployment itself knows about\n// (the operator's proxy in front, plus the one the compose console split emits), or false when nothing is in front,\n// since authup's default trusts every hop and lets any direct client spoof its address.\nexport function trustProxyValue(exposure: Exposure, ownProxy = false): string {\n const hops = (exposure.behindProxy ? 1 : 0) + (ownProxy ? 1 : 0);\n\n return hops > 0 ? String(hops) : 'false';\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\n// A JSON string is a valid YAML double-quoted scalar, which also defuses the Norway problem and a leading `*`.\nexport function quoteYaml(value: string): string {\n return JSON.stringify(value);\n}\n\nconst PLAIN = /^[A-Za-z0-9_@:/.\\-+=%?&,]*$/;\n\n// Whether a value survives the .env round trip at all. dotenv strips the surrounding quotes and, inside DOUBLE quotes,\n// turns a literal \\n or \\r into the control character itself, with no escape that survives (it never unescapes a\n// doubled backslash). A value carrying a single quote, which rules the single-quoted form out, together with one of\n// those two sequences therefore has no representation, and `askSecret` refuses it rather than corrupting a password.\nexport function isEnvRepresentable(value: string): boolean {\n return !value.includes('\\'') || !/\\\\[nr]/.test(value);\n}\n\n// For the .env dotenv reads (`authup start` loads it from cwd): a value is single-quoted when it carries no single\n// quote and written raw inside double quotes otherwise, which round-trips everything `isEnvRepresentable` admits.\nexport function quoteEnv(value: string): string {\n if (PLAIN.test(value)) {\n return value;\n }\n\n if (!value.includes('\\'')) {\n return `'${value}'`;\n }\n\n return `\"${value}\"`;\n}\n\n// For the .env compose-go reads: double quotes take backslash escapes and `$` interpolates unless doubled, while a\n// single-quoted `\\'` there is an escaped quote rather than a literal, so one double-quoted branch covers every value.\nexport function quoteComposeEnv(value: string): string {\n if (PLAIN.test(value)) {\n return value;\n }\n\n return `\"${escapeComposeInterpolation(value.replace(/[\\\\\"]/g, '\\\\$&'))}\"`;\n}\n\n// compose interpolates `$name` and `${name}` in every yaml value and .env value; `$$` is its literal dollar.\nexport function escapeComposeInterpolation(value: string): string {\n return value.replace(/\\$/g, '$$$$');\n}\n\nexport function indent(text: string, spaces: number): string {\n const prefix = ' '.repeat(spaces);\n\n return text\n .split('\\n')\n .map((line) => (line.length > 0 ? prefix + line : line))\n .join('\\n');\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { TARGETS } from './constants.ts';\nimport { isEnvRepresentable } from './utils.ts';\nimport type {\n Answers,\n Ask,\n AskOptions,\n Database,\n Target,\n} from './types.ts';\n\ntype Parsed<T> = { value: T } | { error: string };\n\nconst ENGINE_PORTS = { postgres: 5432, mysql: 3306 } as const;\nconst BUNDLED_REDIS_URL = 'redis://redis:6379';\nconst UNREPRESENTABLE = 'A single quote together with a \\\\n or \\\\r sequence has no .env representation. Choose another value.';\nconst REDIS_NOTE = 'Redis is required for a split deployment: the console sign-in and the token blocklist ride the cache.';\n\n// A refused answer re-asks with the reason on the line above, so the flow stays pure over `ask`.\nasync function askUntil<T>(\n ask: Ask,\n question: string,\n fallback: string | undefined,\n parse: (answer: string) => Parsed<T>,\n options?: AskOptions,\n): Promise<T> {\n let prefix = '';\n\n for (;;) {\n const parsed = parse(await ask(prefix + question, fallback, options));\n if ('value' in parsed) {\n return parsed.value;\n }\n\n prefix = `${parsed.error}\\n`;\n }\n}\n\nfunction askChoice<T extends string>(ask: Ask, question: string, options: readonly T[], fallback: T): Promise<T> {\n const listed = options.map((option, index) => `${index + 1}: ${option}`).join(', ');\n\n return askUntil<T>(ask, `${question} (${listed})`, fallback, (answer) => {\n const normalized = answer.trim().toLowerCase();\n if ((options as readonly string[]).includes(normalized)) {\n return { value: normalized as T };\n }\n\n const numbered = options[Number(normalized) - 1];\n if (numbered !== undefined) {\n return { value: numbered };\n }\n\n return { error: `Pick one of ${options.join(', ')}, by name or number.` };\n });\n}\n\nfunction askYesNo(ask: Ask, question: string, fallback: boolean): Promise<boolean> {\n return askUntil<boolean>(ask, `${question} (y/n)`, fallback ? 'y' : 'n', (answer) => {\n const normalized = answer.trim().toLowerCase();\n if (normalized === 'y' || normalized === 'yes') {\n return { value: true };\n }\n if (normalized === 'n' || normalized === 'no') {\n return { value: false };\n }\n\n return { error: 'Answer y or n.' };\n });\n}\n\nfunction askUrl(ask: Ask, question: string, protocols: readonly string[] = ['http:', 'https:'], fallback?: string): Promise<string> {\n return askUntil<string>(ask, question, fallback, (answer) => {\n const trimmed = answer.trim();\n try {\n // a non-special scheme parses `smtp:mail` with an empty host, which would land in the file as written\n const url = new URL(trimmed);\n if (protocols.includes(url.protocol) && url.hostname !== '') {\n return { value: trimmed };\n }\n } catch {\n // falls through to the refusal\n }\n\n return { error: `Enter an absolute URL using ${protocols.map((protocol) => `${protocol}//`).join(' or ')}.` };\n });\n}\n\n// A free-text answer that must not be blank; trimmed, unlike a secret.\nfunction askText(ask: Ask, question: string, fallback?: string): Promise<string> {\n return askUntil<string>(ask, question, fallback, (answer) => {\n const trimmed = answer.trim();\n if (trimmed === '') {\n return { error: 'A value is required.' };\n }\n\n // a host, user or database name reaches the same .env the secrets do\n return isEnvRepresentable(trimmed) ? { value: trimmed } : { error: UNREPRESENTABLE };\n });\n}\n\n// The issuer: no credentials, query or fragment (OIDC Core 3.1.2.1), and for the helm target no path either, since\n// the chart publishes authup at the origin root and refuses a console split under a sub-path.\nfunction askPublicUrl(ask: Ask, target: Target): Promise<string> {\n return askUntil<string>(\n ask,\n 'Public URL: the address a browser reaches authup at. It is the OIDC issuer, and every console url derives from it',\n undefined,\n (answer) => {\n const trimmed = answer.trim();\n let url: URL;\n try {\n url = new URL(trimmed);\n } catch {\n return { error: 'Enter an absolute URL using http:// or https://.' };\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n return { error: 'Enter an absolute URL using http:// or https://.' };\n }\n if (url.username || url.password || url.search || url.hash) {\n return { error: 'The public URL is the OIDC issuer: no credentials, query or fragment.' };\n }\n if (target === 'helm' && url.pathname !== '/') {\n return { error: 'The helm chart publishes authup at the origin root: enter the URL without a path.' };\n }\n\n return { value: trimmed };\n },\n );\n}\n\nfunction askPort(ask: Ask, question: string, fallback: number): Promise<number> {\n return askUntil<number>(ask, question, String(fallback), (answer) => {\n const port = Number(answer.trim());\n if (Number.isSafeInteger(port) && port >= 1 && port <= 65535) {\n return { value: port };\n }\n\n return { error: 'Enter a port between 1 and 65535.' };\n });\n}\n\nfunction askSecret(ask: Ask, question: string): Promise<string> {\n return askUntil<string>(ask, question, undefined, (answer) => {\n if (answer.trim() === '') {\n return { error: 'A value is required.' };\n }\n if (answer === 'start123') {\n return { error: 'start123 is the provisioning default the wizard refuses to emit. Choose another value.' };\n }\n // the bounds the config registry declares for userAdminPassword\n if (answer.length < 3 || answer.length > 256) {\n return { error: 'Use between 3 and 256 characters.' };\n }\n if (!isEnvRepresentable(answer)) {\n return { error: UNREPRESENTABLE };\n }\n\n return { value: answer };\n }, { secret: true });\n}\n\nasync function askDatabase(ask: Ask, target: Target): Promise<Database> {\n const options: readonly ('postgres' | 'mysql' | 'sqlite')[] = target === 'bare-metal' ?\n ['postgres', 'mysql', 'sqlite'] :\n ['postgres', 'mysql'];\n const type = await askChoice(ask, 'Database', options, 'postgres');\n if (type === 'sqlite') {\n return { type: 'better-sqlite3' };\n }\n\n const bundled = (target === 'compose' || target === 'helm') &&\n await askYesNo(ask, 'Run the database alongside authup?', true);\n if (bundled) {\n return {\n type,\n bundled: true,\n host: type,\n port: ENGINE_PORTS[type],\n username: 'authup',\n password: await askSecret(ask, 'Database password'),\n database: 'authup',\n };\n }\n\n // 127.0.0.1 is the operator's machine on bare metal and the container itself everywhere else\n const hostQuestion = target === 'bare-metal' ?\n 'Database host' :\n 'Database host (a name the container resolves; 127.0.0.1 would be the container itself)';\n\n return {\n type,\n bundled: false,\n host: await askText(ask, hostQuestion, target === 'bare-metal' ? '127.0.0.1' : undefined),\n port: await askPort(ask, 'Database port', ENGINE_PORTS[type]),\n username: await askText(ask, 'Database username', 'authup'),\n password: await askSecret(ask, 'Database password'),\n database: await askText(ask, 'Database name', 'authup'),\n };\n}\n\nexport async function collectAnswers(ask: Ask): Promise<{ answers: Answers, notes: string[] }> {\n const target = await askChoice(ask, 'Deployment target', TARGETS, 'compose');\n const publicUrl = await askPublicUrl(ask, target);\n const tlsCertManager = target === 'helm' && publicUrl.startsWith('https:') &&\n await askYesNo(ask, 'Request the TLS certificate from cert-manager? The alternative is a <hostname>-tls secret in the release namespace', false);\n const db = await askDatabase(ask, target);\n\n const registrationEnabled = await askYesNo(ask, 'Enable user registration?', false);\n const passwordRecoveryEnabled = await askYesNo(ask, 'Enable password recovery?', false);\n const smtp = registrationEnabled || passwordRecoveryEnabled ?\n { url: await askUrl(ask, 'SMTP connection URL (smtp(s)://user:pass@host:port)', ['smtp:', 'smtps:']) } :\n false;\n\n const emailVerificationEnabled = registrationEnabled && await askYesNo(ask, 'Verify email addresses on registration?', true);\n\n const adminPassword = await askSecret(ask, 'Admin password');\n\n const splittable = target === 'compose' || target === 'helm';\n const workerSplit = splittable && await askYesNo(ask, 'Run the background worker as a separate service?', false);\n const consoleSplit = splittable && await askYesNo(ask, 'Run the consoles as a separate service?', false);\n\n const notes: string[] = [];\n let redis: Answers['redis'] = false;\n if (workerSplit || consoleSplit) {\n redis = { url: BUNDLED_REDIS_URL };\n notes.push(REDIS_NOTE);\n } else if (await askYesNo(ask, 'Use redis?', false)) {\n redis = {\n url: splittable ?\n BUNDLED_REDIS_URL :\n await askUrl(\n ask,\n target === 'bare-metal' ? 'Redis URL' : 'Redis URL (a host the container resolves)',\n ['redis:', 'rediss:'],\n target === 'bare-metal' ? 'redis://127.0.0.1:6379' : undefined,\n ),\n };\n }\n\n return {\n answers: {\n target,\n publicUrl,\n db,\n redis,\n smtp,\n registrationEnabled,\n passwordRecoveryEnabled,\n emailVerificationEnabled,\n adminPassword,\n workerSplit,\n consoleSplit,\n tlsCertManager,\n },\n notes,\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { describeExposure, trustProxyValue } from '../deployment.ts';\nimport type { Answers, Rendered } from '../types.ts';\n\nexport function dockerRunCommand(version: string, hostPort: number): string {\n return `docker run -d --name authup --restart unless-stopped --env-file authup.env -p ${hostPort}:3000 authup/authup:${version} start`;\n}\n\nexport function renderDocker(answers: Answers, version: string): Rendered {\n if (answers.db.type === 'better-sqlite3') {\n throw new Error('The docker image runs in production mode and refuses sqlite: configure a postgres or mysql database.');\n }\n\n if (answers.workerSplit || answers.consoleSplit) {\n throw new Error('The docker run target is one container: a worker or console split needs the compose or helm target.');\n }\n\n const exposure = describeExposure(answers.publicUrl);\n\n // `docker run --env-file` reads KEY=value verbatim and does no quote handling, so values are written raw rather than through quoteEnv.\n const lines = [\n `PUBLIC_URL=${answers.publicUrl}`,\n '# The proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default.',\n `TRUST_PROXY=${trustProxyValue(exposure)}`,\n `DB_TYPE=${answers.db.type}`,\n `DB_HOST=${answers.db.host}`,\n `DB_PORT=${answers.db.port}`,\n `DB_USERNAME=${answers.db.username}`,\n `DB_PASSWORD=${answers.db.password}`,\n `DB_DATABASE=${answers.db.database}`,\n ];\n\n if (answers.redis) {\n lines.push(`REDIS=${answers.redis.url}`);\n }\n\n if (answers.smtp) {\n lines.push(`SMTP=${answers.smtp.url}`);\n }\n\n if (answers.registrationEnabled) {\n lines.push('REGISTRATION_ENABLED=true');\n }\n\n if (answers.passwordRecoveryEnabled) {\n lines.push('PASSWORD_RECOVERY_ENABLED=true');\n }\n\n if (answers.emailVerificationEnabled) {\n lines.push('EMAIL_VERIFICATION_ENABLED=true');\n }\n\n lines.push(\n `USER_ADMIN_PASSWORD=${answers.adminPassword}`,\n '# Every downstream application origin, comma-separated; each may obtain a full-permission token.',\n '# TRUSTED_ORIGINS=https://app.example.com',\n '# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.',\n '# SECRETS_ENCRYPTION_KEY=<base64 32 bytes>',\n );\n\n return {\n 'authup.env': [\n '# Environment of the authup container. It holds secrets: keep it out of version control.',\n `# ${dockerRunCommand(version, exposure.hostPort)}`,\n '# The database must exist already; the image runs in production mode and refuses sqlite.',\n '# A configuration file or a provisioning directory is mounted with',\n '# -v ./authup.yml:/etc/authup/authup.yml:ro -v ./provisioning:/etc/authup/provisioning:ro',\n '# https://authup.org/guide/deployment/docker',\n ...lines,\n '',\n ].join('\\n'),\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Answers, Rendered } from '../types.ts';\nimport { quoteYaml } from '../utils.ts';\n\n// The chart version, not the authup version: it is what `helm install` selects, and 0.4.0 is the first that carries\n// worker.* and server.splitConsoles (0.3.0 rejects both through its values schema).\nexport const CHART_REQUIREMENT = '# The worker and the console split need the authup helm chart 0.4.0 or later.';\n\nexport const HELM_COMMANDS: string[] = [\n 'helm repo add authup https://helm.authup.org',\n 'helm install authup authup/authup -f values.yaml',\n];\n\nexport function renderHelm(answers: Answers, _version: string): Rendered {\n const { db } = answers;\n if (db.type === 'better-sqlite3') {\n throw new Error('The helm chart needs a server database (postgres or mysql); the image refuses sqlite.');\n }\n\n const url = new URL(answers.publicUrl);\n const tls = url.protocol === 'https:';\n const lines: string[] = [\n `# Written by npm create authup. Install with: ${HELM_COMMANDS.join(' && ')}`,\n '# Secrets in this file (auth.adminPassword, the database password, smtp.connectionString) can move to the chart\\'s existingSecret keys; see the chart README.',\n // worker.* and server.splitConsoles arrived with the chart that tracks v1.0.0-beta.64 (authup/helm PR #29); the\n // released 0.3.0 rejects both through its values schema, while a no-split document renders on either.\n ...(answers.workerSplit || answers.consoleSplit ? [CHART_REQUIREMENT] : []),\n 'server:',\n ` publicUrl: ${quoteYaml(answers.publicUrl)}`,\n ...(tls && !answers.tlsCertManager ?\n [` # TLS needs a secret named ${url.hostname}-tls in the release namespace, or set server.ingress.certManager: true.`] :\n []),\n ' ingress:',\n ' enabled: true',\n ` hostname: ${quoteYaml(url.hostname)}`,\n ` tls: ${tls}`,\n ...(tls && answers.tlsCertManager ? [' certManager: true'] : []),\n ];\n\n if (answers.registrationEnabled || answers.passwordRecoveryEnabled || answers.emailVerificationEnabled) {\n lines.push(' features:');\n if (answers.registrationEnabled) {\n lines.push(' registration: true');\n }\n if (answers.passwordRecoveryEnabled) {\n lines.push(' passwordRecovery: true');\n }\n if (answers.emailVerificationEnabled) {\n lines.push(' emailVerification: true');\n }\n }\n\n if (answers.workerSplit) {\n lines.push(' migration:', ' enabled: true');\n }\n\n if (answers.consoleSplit) {\n lines.push(' splitConsoles: true');\n }\n\n lines.push(\n ' # Every downstream application origin; each may obtain a full-permission token.',\n ' # trustedOrigins:',\n ' # - https://app.example.com',\n ' # An authup.yml the chart mounts, for the options that only the file can carry.',\n ' # configuration: |',\n ' # core:',\n ' # passwordMinLength: 12',\n );\n\n if (answers.workerSplit) {\n lines.push('', 'worker:', ' enabled: true');\n }\n\n lines.push(\n '',\n 'database:',\n ` type: ${db.type}`,\n '',\n );\n\n // The bundled engine takes the answered password; left empty, the chart would generate one and drop the answer.\n for (const engine of ['postgresql', 'mysql'] as const) {\n const enabled = db.bundled && db.type === (engine === 'postgresql' ? 'postgres' : 'mysql');\n lines.push(`${engine}:`, ` enabled: ${enabled}`);\n if (enabled) {\n lines.push(' auth:', ` password: ${quoteYaml(db.password)}`);\n }\n }\n\n if (!db.bundled) {\n // The chart schema types the port as a string (\"\" = engine default), so a bare integer fails helm's validation.\n lines.push(\n 'externalDatabase:',\n ` host: ${quoteYaml(db.host)}`,\n ` port: ${quoteYaml(String(db.port))}`,\n ` user: ${quoteYaml(db.username)}`,\n ` database: ${quoteYaml(db.database)}`,\n ` password: ${quoteYaml(db.password)}`,\n );\n }\n\n if (answers.redis) {\n lines.push(\n '',\n 'valkey:',\n ' enabled: true',\n '# For an external Redis set valkey.enabled to false and externalRedis.url to its redis:// URL instead.',\n );\n }\n\n lines.push(\n '',\n 'auth:',\n ` adminPassword: ${quoteYaml(answers.adminPassword)}`,\n ' # Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.',\n ' # secretsEncryptionKey: <base64 32 bytes>',\n );\n\n if (answers.smtp) {\n lines.push('', 'smtp:', ` connectionString: ${quoteYaml(answers.smtp.url)}`);\n }\n\n return { 'values.yaml': `${lines.join('\\n')}\\n` };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { LISTEN_PORT, describeExposure, trustProxyValue } from '../deployment.ts';\nimport type { Answers, Rendered } from '../types.ts';\nimport { quoteEnv, quoteYaml } from '../utils.ts';\n\nconst SCHEMA_COMMENT = '# yaml-language-server: $schema=https://authup.org/schema/config.json';\nconst DATABASE_DOCS_URL = 'https://authup.org/guide/deployment/configuration-server-core-database';\n\nexport function renderBareMetal(answers: Answers, version: string): Rendered {\n if (answers.workerSplit || answers.consoleSplit) {\n throw new Error('The bare-metal target runs one process: a worker or console split is a second process under a process manager, which the wizard does not write.');\n }\n\n return {\n 'package.json': renderPackageJson(version),\n 'authup.yml': renderConfig(answers),\n '.env': renderEnv(answers),\n };\n}\n\nfunction renderPackageJson(version: string): string {\n const manifest = {\n name: 'authup-deployment',\n private: true,\n type: 'module',\n scripts: { start: 'authup start' },\n // exact, like the image tag the container targets write: the wizard scaffolds one release, not a range\n dependencies: { authup: version },\n };\n\n return `${JSON.stringify(manifest, null, 4)}\\n`;\n}\n\nfunction renderConfig(answers: Answers): string {\n const { db } = answers;\n const lines = [\n SCHEMA_COMMENT,\n `publicUrl: ${quoteYaml(answers.publicUrl)}`,\n ];\n\n if (db.type === 'better-sqlite3') {\n lines.push(\n '# Production refuses sqlite, so this file keeps the development default of env.',\n `# That is fine for a local trial only: ${DATABASE_DOCS_URL}`,\n '',\n '# db.sqlite is written into this directory.',\n );\n } else {\n lines.push(\n 'env: production',\n '',\n // A `db` block here would be used AS IS: server-core never merges DB_* over it, so the password could not\n // stay in .env. The whole connection rides the DB_* variables there instead (the documented env form).\n '# The database connection is the DB_* set in .env.',\n );\n }\n\n if (answers.redis) {\n lines.push('', `redis: ${quoteYaml(answers.redis.url)}`);\n }\n\n if (answers.smtp) {\n lines.push('', '# The connection URL is SMTP in .env.', 'smtp: true');\n }\n\n lines.push(\n '',\n '# Every downstream application origin; each may obtain a full-permission token.',\n '# trustedOrigins:',\n '# - https://app.example.com',\n );\n\n // The process listens where the url says when nothing is in front; behind a proxy the default stays.\n const exposure = describeExposure(answers.publicUrl);\n lines.push(\n '',\n 'core:',\n ...(exposure.hostPort !== LISTEN_PORT ? [` port: ${exposure.hostPort}`] : []),\n ' # The proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default.',\n ` trustProxy: ${trustProxyValue(exposure)}`,\n ...(answers.registrationEnabled ? [' registrationEnabled: true'] : []),\n ...(answers.passwordRecoveryEnabled ? [' passwordRecoveryEnabled: true'] : []),\n ...(answers.emailVerificationEnabled ? [' emailVerificationEnabled: true'] : []),\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction renderEnv(answers: Answers): string {\n const lines = ['# Secrets. authup start reads this file from the directory it is started in. Keep it out of version control.'];\n\n const { db } = answers;\n if (db.type !== 'better-sqlite3') {\n lines.push(\n `DB_TYPE=${db.type}`,\n `DB_HOST=${quoteEnv(db.host)}`,\n `DB_PORT=${db.port}`,\n `DB_USERNAME=${quoteEnv(db.username)}`,\n `DB_PASSWORD=${quoteEnv(db.password)}`,\n `DB_DATABASE=${quoteEnv(db.database)}`,\n );\n }\n\n lines.push(`USER_ADMIN_PASSWORD=${quoteEnv(answers.adminPassword)}`);\n\n if (answers.smtp) {\n lines.push(`SMTP=${quoteEnv(answers.smtp.url)}`);\n }\n\n lines.push(\n '# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.',\n '# SECRETS_ENCRYPTION_KEY=',\n );\n\n return `${lines.join('\\n')}\\n`;\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { describeExposure, trustProxyValue } from '../deployment.ts';\nimport type { Answers, Rendered } from '../types.ts';\nimport {\n escapeComposeInterpolation,\n indent,\n quoteComposeEnv,\n quoteYaml,\n} from '../utils.ts';\n\nconst DOCS_URL = 'https://authup.org/guide/deployment';\n\nconst NGINX_CONF = `# Written by npm create authup: the console split's routing, ${DOCS_URL}/console-replicas\n# TLS, if any, terminates in front of this proxy; X-Forwarded-Proto is passed through as the scheme it sees.\nupstream authup_api { server authup:3000; }\nupstream authup_auth_console { server authup-console:3020; }\nupstream authup_admin_console { server authup-console:3021; }\nupstream authup_account_console { server authup-console:3022; }\n\nserver {\n listen 80;\n # nginx listens on 80 inside the container while the published port is another one: a redirect it builds itself\n # (the trailing slash it adds to /console/<name>) must stay relative or it would send the browser to port 80.\n absolute_redirect off;\n\n # The sign-in pair stays on the API. Exact matches, so they win over the prefix rules below.\n location = /console/admin/login/start { proxy_pass http://authup_api; }\n location = /console/admin/callback { proxy_pass http://authup_api; }\n location = /console/account/login/start { proxy_pass http://authup_api; }\n location = /console/account/callback { proxy_pass http://authup_api; }\n\n location /console/auth/ {\n rewrite ^/console/auth(/.*)$ $1 break;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Tls-Client-Cert \"\";\n proxy_pass http://authup_auth_console;\n }\n\n location /console/admin/ {\n rewrite ^/console/admin(/.*)$ $1 break;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Tls-Client-Cert \"\";\n proxy_pass http://authup_admin_console;\n }\n\n location /console/account/ {\n rewrite ^/console/account(/.*)$ $1 break;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Tls-Client-Cert \"\";\n proxy_pass http://authup_account_console;\n }\n\n location / {\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Tls-Client-Cert \"\";\n proxy_pass http://authup_api;\n }\n}\n`;\n\n\nconst ENGINES = {\n postgres: {\n image: 'postgres:17',\n port: 5432,\n dataPath: '/var/lib/postgresql/data',\n },\n mysql: {\n image: 'mysql:8.4',\n port: 3306,\n dataPath: '/var/lib/mysql',\n },\n} as const;\n\n// One list entry of a compose `environment` block; the whole KEY=value is one YAML scalar, so quoting it covers the\n// value, and an operator's `$` is doubled so compose does not interpolate it away.\nfunction env(key: string, value: string): string {\n return `- ${quoteYaml(`${key}=${escapeComposeInterpolation(value)}`)}`;\n}\n\n// The same entry carrying a deliberate `${VARIABLE}` reference into .env.\nfunction ref(key: string, variable: string): string {\n return `- ${quoteYaml(`${key}=\\${${variable}}`)}`;\n}\n\nfunction block(key: string, lines: string[]): string[] {\n return [`${key}:`, ...indent(lines.join('\\n'), 2).split('\\n')];\n}\n\nexport function renderCompose(answers: Answers, version: string): Rendered {\n const { db } = answers;\n if (db.type === 'better-sqlite3') {\n throw new Error('The compose target needs a server database (postgres or mysql): the image runs in production mode and refuses sqlite.');\n }\n\n const image = `image: authup/authup:${version}`;\n const restart = 'restart: unless-stopped';\n const engine = ENGINES[db.type];\n const exposure = describeExposure(answers.publicUrl);\n\n const dbEnv = [\n env('DB_TYPE', db.type),\n env('DB_HOST', db.bundled ? db.type : db.host),\n env('DB_PORT', String(db.bundled ? engine.port : db.port)),\n env('DB_USERNAME', db.username),\n ref('DB_PASSWORD', 'DB_PASSWORD'),\n env('DB_DATABASE', db.database),\n ];\n if (answers.redis) {\n dbEnv.push(env('REDIS', 'redis://redis:6379'));\n }\n\n const dependsOn: string[] = [];\n if (db.bundled) {\n // The one-off `migration run` and the services would otherwise race an engine still initializing its volume.\n dependsOn.push(`${db.type}:`, ' condition: service_healthy');\n }\n if (answers.redis) {\n dependsOn.push('redis:', ' condition: service_started');\n }\n const dependsOnBlock = dependsOn.length > 0 ? block('depends_on', dependsOn) : [];\n\n const authupEnv = [\n env('PUBLIC_URL', answers.publicUrl),\n '# the proxies in front whose X-Forwarded-For is trusted, or false; authup trusts every hop by default',\n env('TRUST_PROXY', trustProxyValue(exposure, answers.consoleSplit)),\n ...dbEnv,\n ref('USER_ADMIN_PASSWORD', 'USER_ADMIN_PASSWORD'),\n ];\n if (answers.smtp) {\n authupEnv.push(ref('SMTP', 'SMTP'));\n }\n if (answers.registrationEnabled) {\n authupEnv.push(env('REGISTRATION_ENABLED', 'true'));\n }\n if (answers.passwordRecoveryEnabled) {\n authupEnv.push(env('PASSWORD_RECOVERY_ENABLED', 'true'));\n }\n if (answers.emailVerificationEnabled) {\n authupEnv.push(env('EMAIL_VERIFICATION_ENABLED', 'true'));\n }\n authupEnv.push(\n '# every downstream application origin, comma-separated; each may obtain a full-permission token',\n '# - \"TRUSTED_ORIGINS=https://app.example.com\"',\n '# wraps the realm key store at rest (base64, 32 bytes); write-once, back it up',\n '# - \"SECRETS_ENCRYPTION_KEY=<base64 32 bytes>\"',\n );\n // A console-only split keeps one API process, which sweeps and migrates itself like a plain `start` does.\n if (answers.workerSplit) {\n authupEnv.push(\n '# the sweeps run in the worker below',\n env('WORKER_ENABLED', 'false'),\n '# run \"docker compose run --rm authup migration run\" once before starting',\n env('MIGRATION_ENABLED', 'false'),\n );\n }\n\n const services = block('authup', [\n image,\n restart,\n // under a console split the proxy below owns the published port\n ...(answers.consoleSplit ? [] : block('ports', [`- \"${exposure.hostPort}:3000\"`])),\n '# a configuration file and a provisioning directory are optional; mount them under /etc/authup',\n '# volumes:',\n '# - ./authup.yml:/etc/authup/authup.yml:ro',\n '# - ./provisioning:/etc/authup/provisioning:ro',\n ...dependsOnBlock,\n ...block('environment', authupEnv),\n `command: ${answers.consoleSplit ? 'start core' : 'start'}`,\n ]);\n\n if (db.bundled) {\n const credentials = db.type === 'postgres' ?\n [\n env('POSTGRES_USER', db.username),\n ref('POSTGRES_PASSWORD', 'DB_PASSWORD'),\n env('POSTGRES_DB', db.database),\n ] :\n [\n env('MYSQL_USER', db.username),\n ref('MYSQL_PASSWORD', 'DB_PASSWORD'),\n ref('MYSQL_ROOT_PASSWORD', 'DB_PASSWORD'),\n env('MYSQL_DATABASE', db.database),\n ];\n\n // Both official images run a temporary, network-less server while initializing an empty volume, so a probe\n // over TCP answers only once the real server is up (pg_isready over the socket would answer too early).\n const probe = db.type === 'postgres' ?\n `test: [\"CMD\", \"pg_isready\", \"-h\", \"localhost\", \"-U\", ${quoteYaml(escapeComposeInterpolation(db.username))}, \"-d\", ${quoteYaml(escapeComposeInterpolation(db.database))}]` :\n // mysqladmin ping exits 0 whenever the server takes the connection, credentials or not\n 'test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"127.0.0.1\"]';\n\n services.push('', ...block(db.type, [\n `image: ${engine.image}`,\n restart,\n ...block('volumes', [`- ${db.type}_data:${engine.dataPath}`]),\n ...block('environment', credentials),\n ...block('healthcheck', [probe, 'interval: 5s', 'timeout: 5s', 'retries: 24']),\n ]));\n }\n\n if (answers.redis) {\n services.push('', ...block('redis', ['image: redis:7', restart]));\n }\n\n if (answers.workerSplit) {\n services.push(\n '',\n '# one instance is enough, whatever the API scales to',\n ...block('authup-worker', [\n image,\n restart,\n ...dependsOnBlock,\n // the worker needs no url of its own; without one the CLI derives it and warns on every start\n ...block('environment', [env('PUBLIC_URL', answers.publicUrl), ...dbEnv]),\n '# the image healthcheck probes an HTTP port this process never opens',\n ...block('healthcheck', ['disable: true']),\n 'command: start worker',\n ]),\n );\n }\n\n if (answers.consoleSplit) {\n services.push(\n '',\n '# Two proxy rules, in this order: /console/<name>/login/start and',\n '# /console/<name>/callback (admin and account) go to the authup service;',\n '# everything else under /console/** goes here, with the /console/<name>',\n `# prefix stripped. See ${DOCS_URL}/console-replicas`,\n ...block('authup-console', [\n image,\n restart,\n ...block('environment', [\n env('PUBLIC_URL', answers.publicUrl),\n '# the auth console renders server-side and calls the API itself: reach it on this network',\n env('INTERNAL_URL', 'http://authup:3000'),\n ]),\n '# the image probes the API port, which this process never opens; the auth',\n '# console cannot be disabled, so it is always listening (3020 by default)',\n ...block('healthcheck', [\n 'test: [\"CMD\", \"wget\", \"--spider\", \"--proxy\", \"off\", \"http://127.0.0.1:3020/healthy\"]',\n 'interval: 10s',\n ]),\n 'command: start console',\n ]),\n '',\n '# The proxy owns the published port and carries the two console routing rules (nginx.conf).',\n ...block('nginx', [\n 'image: nginx:1.31-alpine',\n restart,\n ...block('ports', [`- \"${exposure.hostPort}:80\"`]),\n ...block('volumes', ['- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro']),\n ...block('depends_on', ['authup:', ' condition: service_started', 'authup-console:', ' condition: service_started']),\n ]),\n );\n }\n\n const compose = [\n `# Written by npm create authup. Secrets live in .env next to this file: ${DOCS_URL}/docker-compose`,\n '',\n ];\n if (db.bundled) {\n compose.push(...block('volumes', [`${db.type}_data:`]), '');\n }\n compose.push(...block('services', services));\n\n const dotenv = [\n '# Secrets docker-compose.yml interpolates. Keep this file out of version control.',\n `DB_PASSWORD=${quoteComposeEnv(db.password)}`,\n `USER_ADMIN_PASSWORD=${quoteComposeEnv(answers.adminPassword)}`,\n ];\n if (answers.smtp) {\n dotenv.push(`SMTP=${quoteComposeEnv(answers.smtp.url)}`);\n }\n dotenv.push(\n '# Wraps the realm key store at rest (base64, 32 bytes). Write-once: back it up.',\n '# SECRETS_ENCRYPTION_KEY=',\n );\n\n return {\n 'docker-compose.yml': `${compose.join('\\n')}\\n`,\n '.env': `${dotenv.join('\\n')}\\n`,\n ...(answers.consoleSplit ? { 'nginx.conf': NGINX_CONF } : {}),\n };\n}\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { Renderer, Target } from '../types.ts';\nimport { renderBareMetal } from './bare-metal.ts';\nimport { renderCompose } from './compose.ts';\nimport { renderDocker } from './docker.ts';\nimport { renderHelm } from './helm.ts';\n\nexport const RENDERERS: Record<Target, Renderer> = {\n docker: renderDocker,\n compose: renderCompose,\n helm: renderHelm,\n 'bare-metal': renderBareMetal,\n};\n","/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { readFileSync } from 'node:fs';\n\n// dist/index.mjs and src/version.ts both sit one level below the package root, so one relative URL serves the built bin and vitest alike.\nexport const VERSION: string = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;\n","#!/usr/bin/env node\n\n/*\n * Copyright (c) 2026.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\n/* eslint-disable no-console */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { parseArgs } from 'node:util';\nimport { createReadlineAsk } from './ask.ts';\nimport { SECRET_FILES, TARGETS } from './constants.ts';\nimport { describeExposure } from './deployment.ts';\nimport { collectAnswers } from './prompts.ts';\nimport { dockerRunCommand } from './targets/docker.ts';\nimport { HELM_COMMANDS } from './targets/helm.ts';\nimport { RENDERERS } from './targets/index.ts';\nimport type { Answers, Target } from './types.ts';\nimport { VERSION } from './version.ts';\n\nconst DOCS_URL = 'https://authup.org/guide/deployment/';\n\nconst USAGE = `Usage: npx create-authup [--force] [--help]\n npm create authup -- [--force] [--help]\n\nWrites the deployment files for one authup installation into the current directory.\n\nTargets: ${TARGETS.join(', ')}\n\nFlags:\n --force overwrite files that already exist\n --help print this text`;\n\nfunction nextSteps(answers: Answers): string[] {\n const steps: Record<Target, string[]> = {\n docker: [\n dockerRunCommand(VERSION, describeExposure(answers.publicUrl).hostPort),\n 'The database must already exist: the image runs in production mode and refuses sqlite.',\n ],\n compose: [\n ...(answers.workerSplit ? ['docker compose run --rm authup migration run'] : []),\n 'docker compose up -d',\n ...(answers.consoleSplit ? ['The nginx service publishes the port; TLS, if any, terminates in front of it.'] : []),\n ],\n helm: [...HELM_COMMANDS],\n 'bare-metal': ['npm install', 'npx authup config validate', 'npm start'],\n };\n\n return steps[answers.target];\n}\n\nasync function main(): Promise<void> {\n const { values } = parseArgs({\n options: {\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false },\n },\n allowPositionals: false,\n });\n if (values.help) {\n console.log(USAGE);\n return;\n }\n\n console.log(`create-authup ${VERSION}`);\n console.log(`Files are written into the current directory (${process.cwd()}).`);\n\n const { ask, close } = createReadlineAsk();\n let collected: Awaited<ReturnType<typeof collectAnswers>>;\n try {\n collected = await collectAnswers(ask);\n } finally {\n close();\n }\n const { answers, notes } = collected;\n\n const rendered = RENDERERS[answers.target](answers, VERSION);\n const files = Object.keys(rendered);\n const directories = files.filter((name) => {\n try {\n return fs.statSync(path.join(process.cwd(), name)).isDirectory();\n } catch {\n return false;\n }\n });\n if (directories.length > 0) {\n console.error(`Cannot write over a directory: ${directories.join(', ')}.`);\n process.exit(1);\n }\n\n const existing = files.filter((name) => fs.existsSync(path.join(process.cwd(), name)));\n if (existing.length > 0 && !values.force) {\n console.error(`Refusing to overwrite: ${existing.join(', ')}. Re-run with --force.`);\n process.exit(1);\n }\n\n for (const name of files) {\n const target = path.join(process.cwd(), name);\n const secret = SECRET_FILES.has(name);\n fs.writeFileSync(target, rendered[name], secret ? { encoding: 'utf8', mode: 0o600 } : 'utf8');\n if (secret) {\n // writeFileSync's mode applies to a file it creates, so an overwrite keeps whatever the old one had\n fs.chmodSync(target, 0o600);\n }\n }\n\n for (const note of notes) {\n console.log(note);\n }\n console.log(`Written: ${files.join(', ')}`);\n console.log('Next steps:');\n for (const step of nextSteps(answers)) {\n console.log(` ${step}`);\n }\n console.log(`Docs: ${DOCS_URL}`);\n}\n\nmain().catch((e) => {\n console.error(e instanceof Error ? e.message : String(e));\n process.exit(1);\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "create-authup",
3
+ "type": "module",
4
+ "version": "1.0.0-beta.65",
5
+ "description": "Interactive wizard writing the deployment files for an authup installation: docker run, docker compose, helm values or a bare-metal project.",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "create-authup": "dist/index.mjs"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build:js": "tsdown",
15
+ "build": "rimraf ./dist && npm run build:js",
16
+ "check:types": "tsc -p tsconfig.test.json",
17
+ "test": "cross-env NODE_ENV=test vitest run --config test/vitest.config.ts",
18
+ "test:artifacts": "node scripts/verify-emitted.mjs"
19
+ },
20
+ "engines": {
21
+ "node": "^22.13.0 || ^23.5.0 || >=24.0.0"
22
+ },
23
+ "keywords": [
24
+ "auth",
25
+ "authentication",
26
+ "oauth2",
27
+ "installer",
28
+ "wizard",
29
+ "scaffold",
30
+ "docker",
31
+ "compose",
32
+ "helm"
33
+ ],
34
+ "author": {
35
+ "name": "Peter Placzek",
36
+ "email": "contact@tada5hi.net",
37
+ "url": "https://github.com/tada5hi"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/authup/authup.git",
42
+ "directory": "packages/create-authup"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/authup/authup/issues"
46
+ },
47
+ "homepage": "https://github.com/authup/authup#readme",
48
+ "dependencies": {},
49
+ "devDependencies": {
50
+ "@authup/server-config": "^1.0.0-beta.65",
51
+ "@authup/server-config-kit": "^1.0.0-beta.65",
52
+ "@types/node": "^26.4.1",
53
+ "ajv": "^8.20.0",
54
+ "cross-env": "^10.1.0",
55
+ "dotenv": "^17.4.1",
56
+ "vitest": "^4.0.14",
57
+ "yaml": "^2.9.0"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }