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 +201 -0
- package/README.md +58 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +719 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
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/
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|