@o3co/create-auth-provider 0.15.0 → 0.17.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +139 -48
- package/README.md +138 -47
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +69 -24
- package/templates/standalone/Dockerfile +7 -3
- package/templates/standalone/README.ja.md +677 -184
- package/templates/standalone/README.md +699 -238
- package/templates/standalone/config/application.conf +203 -325
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +399 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +58 -30
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +4 -2
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
- package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
- package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
- package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
- package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
- package/templates/standalone/src/__tests__/listen.test.mts +121 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
- package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
- package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
- package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
- package/templates/standalone/src/adapters.mts +149 -0
- package/templates/standalone/src/app.mts +71 -115
- package/templates/standalone/src/buildModules.mts +229 -251
- package/templates/standalone/src/configPath.mts +422 -29
- package/templates/standalone/src/listen.mts +60 -0
- package/templates/standalone/src/logger.mts +31 -57
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +262 -0
- package/templates/standalone/src/modules.mts +480 -464
- package/templates/standalone/src/rootRenames.mts +79 -0
- package/templates/standalone/src/routes.mts +89 -0
- package/templates/standalone/src/sections.mts +366 -0
- package/templates/standalone/src/shutdown.mts +54 -126
- package/templates/standalone/tests/index.test.js +2 -2
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
package/dist/index.mjs
CHANGED
|
@@ -14,12 +14,46 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
|
-
import { cpSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
18
18
|
import { dirname, resolve } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { shouldCopyTemplateEntry } from "./internal/template-filter.mjs";
|
|
21
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const
|
|
22
|
+
const TEMPLATES_ROOT = resolve(__dirname, "../templates");
|
|
23
|
+
/** The template a scaffold is made from when none is named. */
|
|
24
|
+
export const DEFAULT_TEMPLATE = "standalone";
|
|
25
|
+
/**
|
|
26
|
+
* The templates under `templatesRoot`: every directory — not a symbolic link,
|
|
27
|
+
* not dot-named — holding a `package.json`, sorted; none when the directory is
|
|
28
|
+
* missing. This is the rule `scripts/templates.mjs` copies templates by, read
|
|
29
|
+
* here from the copy, where `versions.json` sits beside them. A name is only
|
|
30
|
+
* ever looked up in this list, so nothing outside the templates directory can
|
|
31
|
+
* be named as a template.
|
|
32
|
+
*/
|
|
33
|
+
export const availableTemplates = (templatesRoot = TEMPLATES_ROOT) => {
|
|
34
|
+
if (!existsSync(templatesRoot))
|
|
35
|
+
return [];
|
|
36
|
+
return readdirSync(templatesRoot, { withFileTypes: true })
|
|
37
|
+
.filter((entry) => entry.isDirectory() &&
|
|
38
|
+
!entry.name.startsWith(".") &&
|
|
39
|
+
existsSync(resolve(templatesRoot, entry.name, "package.json")))
|
|
40
|
+
.map((entry) => entry.name)
|
|
41
|
+
.sort();
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Why `template` cannot be scaffolded from `templates`, or `undefined` when it
|
|
45
|
+
* can: none is bundled (a checkout that never ran the prebuild script), or it
|
|
46
|
+
* is not one of them.
|
|
47
|
+
*/
|
|
48
|
+
export const templateRefusal = (template, templates) => {
|
|
49
|
+
if (templates.length === 0) {
|
|
50
|
+
return `No templates found at ${TEMPLATES_ROOT}. If developing locally, run the prebuild script first.`;
|
|
51
|
+
}
|
|
52
|
+
if (!templates.includes(template)) {
|
|
53
|
+
return `Unknown template '${template}'. Available templates: ${templates.join(", ")}.`;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
23
57
|
const getPackageVersions = () => {
|
|
24
58
|
const versionFile = resolve(__dirname, "../templates/versions.json");
|
|
25
59
|
if (existsSync(versionFile)) {
|
|
@@ -44,35 +78,47 @@ export const isValidDirName = (name) => {
|
|
|
44
78
|
return false;
|
|
45
79
|
return UNSCOPED_NAME_RE.test(name);
|
|
46
80
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Appended to the scaffold's `config/application.conf` by `noMfa`: the
|
|
83
|
+
* template's switch `mfaMode` written off, then bound to `MFA_MODE` so the
|
|
84
|
+
* variable wins over the written value. The MFA package stays a dependency.
|
|
85
|
+
*/
|
|
86
|
+
const MFA_OFF_LINES = [
|
|
87
|
+
"",
|
|
88
|
+
"# written by create-app --no-mfa: MFA is off unless MFA_MODE turns it on",
|
|
89
|
+
'mfaMode = "off"',
|
|
90
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: a HOCON substitution, not a template
|
|
91
|
+
"mfaMode = ${?MFA_MODE}",
|
|
92
|
+
];
|
|
93
|
+
export const scaffold = (targetDir, projectName, template = DEFAULT_TEMPLATE, options = {}) => {
|
|
94
|
+
const refusal = templateRefusal(template, availableTemplates());
|
|
95
|
+
if (refusal !== undefined)
|
|
96
|
+
throw new Error(refusal);
|
|
97
|
+
const templateDir = resolve(TEMPLATES_ROOT, template);
|
|
51
98
|
// Copy template to target
|
|
52
|
-
cpSync(
|
|
99
|
+
cpSync(templateDir, targetDir, {
|
|
53
100
|
recursive: true,
|
|
54
|
-
filter: (source) => shouldCopyTemplateEntry(source,
|
|
101
|
+
filter: (source) => shouldCopyTemplateEntry(source, templateDir),
|
|
55
102
|
});
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// create — and it would have shipped that way silently, because the file
|
|
61
|
-
// is present in the repository and only missing from the tarball.
|
|
103
|
+
// npm drops a file literally named `.gitignore` from a published package,
|
|
104
|
+
// so `copy-templates.mjs` stages it dot-less and it is restored here.
|
|
105
|
+
// Without it the first `git add .` in a scaffolded project commits the
|
|
106
|
+
// `.env` and the signing key the README's setup steps create.
|
|
62
107
|
const stagedGitignore = resolve(targetDir, "gitignore");
|
|
63
108
|
if (existsSync(stagedGitignore)) {
|
|
64
109
|
renameSync(stagedGitignore, resolve(targetDir, ".gitignore"));
|
|
65
110
|
}
|
|
111
|
+
if (options.noMfa === true) {
|
|
112
|
+
appendFileSync(resolve(targetDir, "config", "application.conf"), `${MFA_OFF_LINES.join("\n")}\n`);
|
|
113
|
+
}
|
|
66
114
|
// Rewrite package.json
|
|
67
115
|
const pkgPath = resolve(targetDir, "package.json");
|
|
68
116
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
69
117
|
pkg.name = projectName;
|
|
70
|
-
// `"private": true` is deliberately
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// service is the rare intent; the operator who has it states it by
|
|
75
|
-
// removing the field.
|
|
118
|
+
// `"private": true` is deliberately kept: the scaffold is an identity
|
|
119
|
+
// provider (keys, config, policy), so an accidental `npm publish` must fail
|
|
120
|
+
// by default. An operator who means to publish removes the field.
|
|
121
|
+
// o3co/auth.policy-verifier's scaffolder does the same; change the two together.
|
|
76
122
|
// Replace all workspace:* references with per-package published versions
|
|
77
123
|
const versions = getPackageVersions();
|
|
78
124
|
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
@@ -91,11 +137,9 @@ export const scaffold = (targetDir, projectName) => {
|
|
|
91
137
|
}
|
|
92
138
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
93
139
|
// pnpm ≥10.29 reads `onlyBuiltDependencies` only from pnpm-workspace.yaml,
|
|
94
|
-
// in single-package projects
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// running pnpm from the template directory. So the scaffold writes it,
|
|
98
|
-
// mirroring the allowlist the workspace root keeps for the same dependency.
|
|
140
|
+
// even in single-package projects. The template cannot ship the file:
|
|
141
|
+
// inside the monorepo it would shadow the workspace root. So the scaffold
|
|
142
|
+
// writes it, mirroring the workspace root's allowlist.
|
|
99
143
|
writeFileSync(resolve(targetDir, "pnpm-workspace.yaml"), [
|
|
100
144
|
"# pnpm (>= 10.29) reads onlyBuiltDependencies from this file, even in a",
|
|
101
145
|
"# single-package project. bcrypt's install hook compiles the native addon",
|
|
@@ -114,25 +158,19 @@ export const scaffold = (targetDir, projectName) => {
|
|
|
114
158
|
*/
|
|
115
159
|
const LOCKFILE_ARGS = ["install", "--lockfile-only", "--ignore-workspace"];
|
|
116
160
|
/**
|
|
117
|
-
* Launchers tried in order
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* unpublished version) would fail identically through any launcher, and
|
|
122
|
-
* EACCES-shaped errors surface as themselves rather than as "package manager
|
|
123
|
-
* missing".
|
|
161
|
+
* Launchers tried in order: `pnpm` on PATH, then `corepack pnpm` for a machine
|
|
162
|
+
* with Node's bundled corepack but no global pnpm. Only ENOENT falls through to
|
|
163
|
+
* the next launcher; a non-zero exit would fail identically through any of
|
|
164
|
+
* them, and other launch errors surface as themselves.
|
|
124
165
|
*/
|
|
125
166
|
const LOCKFILE_LAUNCHERS = [["pnpm"], ["corepack", "pnpm"]];
|
|
126
167
|
/**
|
|
127
|
-
* Resolve the scaffolded project's dependency graph into `pnpm-lock.yaml
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* resolved once, here, against the rewritten `package.json`. Best-effort by
|
|
134
|
-
* design — it needs a package manager and a reachable registry — so failure
|
|
135
|
-
* is a result, not a throw.
|
|
168
|
+
* Resolve the scaffolded project's dependency graph into `pnpm-lock.yaml`,
|
|
169
|
+
* which the template's `pnpm install --frozen-lockfile` build needs. The
|
|
170
|
+
* lockfile cannot ship with the template: the dependency set it pins exists
|
|
171
|
+
* only after `scaffold` has replaced every `workspace:*`. Best-effort (it needs
|
|
172
|
+
* a package manager and a reachable registry), so failure is a result, not a
|
|
173
|
+
* throw.
|
|
136
174
|
*/
|
|
137
175
|
export const generateLockfile = (targetDir) => {
|
|
138
176
|
const attempts = [];
|
|
@@ -161,31 +199,39 @@ export const generateLockfile = (targetDir) => {
|
|
|
161
199
|
}
|
|
162
200
|
return { ok: false, reason: attempts.join("; ") };
|
|
163
201
|
};
|
|
202
|
+
/** The flags that take a value, as `--flag <value>` or `--flag=<value>`. */
|
|
203
|
+
const VALUE_FLAGS = ["--dir", "--template"];
|
|
164
204
|
const parseArgs = (args) => {
|
|
165
205
|
const positionals = [];
|
|
166
|
-
|
|
167
|
-
let dirSeen = false;
|
|
206
|
+
const values = new Map();
|
|
168
207
|
let lockfile = true;
|
|
208
|
+
let noMfa = false;
|
|
209
|
+
const setValue = (flag, value) => {
|
|
210
|
+
if (values.has(flag))
|
|
211
|
+
throw new Error(`${flag} specified more than once`);
|
|
212
|
+
if (value === "")
|
|
213
|
+
throw new Error(`${flag} requires a value`);
|
|
214
|
+
values.set(flag, value);
|
|
215
|
+
};
|
|
169
216
|
for (let i = 0; i < args.length; i++) {
|
|
170
217
|
const a = args[i];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
const spaced = VALUE_FLAGS.find((flag) => a === flag);
|
|
219
|
+
const joined = VALUE_FLAGS.find((flag) => a.startsWith(`${flag}=`));
|
|
220
|
+
if (spaced !== undefined) {
|
|
174
221
|
if (i + 1 >= args.length)
|
|
175
|
-
throw new Error(
|
|
176
|
-
|
|
177
|
-
dirSeen = true;
|
|
222
|
+
throw new Error(`${spaced} requires a value`);
|
|
223
|
+
setValue(spaced, args[i + 1]);
|
|
178
224
|
i++;
|
|
179
225
|
}
|
|
180
|
-
else if (
|
|
181
|
-
|
|
182
|
-
throw new Error("--dir specified more than once");
|
|
183
|
-
dir = a.slice("--dir=".length);
|
|
184
|
-
dirSeen = true;
|
|
226
|
+
else if (joined !== undefined) {
|
|
227
|
+
setValue(joined, a.slice(`${joined}=`.length));
|
|
185
228
|
}
|
|
186
229
|
else if (a === "--no-lockfile") {
|
|
187
230
|
lockfile = false;
|
|
188
231
|
}
|
|
232
|
+
else if (a === "--no-mfa") {
|
|
233
|
+
noMfa = true;
|
|
234
|
+
}
|
|
189
235
|
else if (a.startsWith("-")) {
|
|
190
236
|
// Treats `--` and any --unknown as an unknown flag.
|
|
191
237
|
throw new Error(`unknown flag: ${a}`);
|
|
@@ -198,7 +244,13 @@ const parseArgs = (args) => {
|
|
|
198
244
|
throw new Error("missing <project-name>");
|
|
199
245
|
if (positionals.length > 1)
|
|
200
246
|
throw new Error("too many positional arguments");
|
|
201
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
projectName: positionals[0],
|
|
249
|
+
dir: values.get("--dir"),
|
|
250
|
+
template: values.get("--template"),
|
|
251
|
+
lockfile,
|
|
252
|
+
noMfa,
|
|
253
|
+
};
|
|
202
254
|
};
|
|
203
255
|
const deriveDirName = (projectName, dir) => {
|
|
204
256
|
if (dir !== undefined)
|
|
@@ -224,11 +276,13 @@ export const main = () => {
|
|
|
224
276
|
}
|
|
225
277
|
catch (e) {
|
|
226
278
|
console.error(`Error: ${e.message}`);
|
|
227
|
-
console.error("Usage: @o3co/create-auth-provider <project-name> [--dir <dir-name>] [--no-lockfile]");
|
|
279
|
+
console.error("Usage: @o3co/create-auth-provider <project-name> [--template <name>] [--dir <dir-name>] [--no-lockfile] [--no-mfa]");
|
|
228
280
|
console.error("<project-name> must be a valid npm package name (scoped like @scope/pkg, or unscoped).");
|
|
281
|
+
console.error(`--template names the template to copy (default: ${DEFAULT_TEMPLATE}); available: ${availableTemplates().join(", ")}.`);
|
|
229
282
|
process.exit(1);
|
|
230
283
|
}
|
|
231
|
-
const { projectName, dir, lockfile } = parsed;
|
|
284
|
+
const { projectName, dir, lockfile, noMfa } = parsed;
|
|
285
|
+
const template = parsed.template ?? DEFAULT_TEMPLATE;
|
|
232
286
|
if (!isValidProjectName(projectName)) {
|
|
233
287
|
console.error("Error: <project-name> must be a valid npm package name (scoped like @scope/pkg, or unscoped; max 214 chars; no backslashes; no extra '/' beyond the single scope separator).");
|
|
234
288
|
process.exit(1);
|
|
@@ -237,14 +291,19 @@ export const main = () => {
|
|
|
237
291
|
console.error("Error: --dir must be a valid unscoped package name (no '/', '\\', '@'; not '.' or '..'; max 214 chars).");
|
|
238
292
|
process.exit(1);
|
|
239
293
|
}
|
|
294
|
+
const refusal = templateRefusal(template, availableTemplates());
|
|
295
|
+
if (refusal !== undefined) {
|
|
296
|
+
console.error(`Error: ${refusal}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
240
299
|
const dirName = deriveDirName(projectName, dir);
|
|
241
300
|
const targetDir = resolve(process.cwd(), dirName);
|
|
242
301
|
if (existsSync(targetDir)) {
|
|
243
302
|
console.error(`Error: Directory '${dirName}' already exists.`);
|
|
244
303
|
process.exit(1);
|
|
245
304
|
}
|
|
246
|
-
console.log(`Creating ${projectName}...`);
|
|
247
|
-
scaffold(targetDir, projectName);
|
|
305
|
+
console.log(`Creating ${projectName} from the ${template} template...`);
|
|
306
|
+
scaffold(targetDir, projectName, template, { noMfa });
|
|
248
307
|
let lockfileGenerated = false;
|
|
249
308
|
if (lockfile) {
|
|
250
309
|
console.log("\nResolving dependencies into pnpm-lock.yaml...");
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Decide whether `cpSync` should copy a given source path.
|
|
3
3
|
*
|
|
4
|
-
* Only segments
|
|
5
|
-
* install
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `node_modules` directory (which is the v0.5.0 npx regression this fixes).
|
|
4
|
+
* Only segments inside `templateRoot` are checked against EXCLUDED_DIRS; the
|
|
5
|
+
* install prefix above it (e.g. `~/.npm/_npx/<hash>/node_modules/...` under
|
|
6
|
+
* `npx`) is ignored, or every file would be rejected whenever the package
|
|
7
|
+
* itself lives under a `node_modules` directory.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* hardcoded `"/"` — `cpSync` passes back-slash-delimited absolute paths on
|
|
14
|
-
* Windows, so segment splitting must follow the platform separator.
|
|
9
|
+
* `pathSep` lets tests exercise POSIX and Windows separators on any host.
|
|
10
|
+
* Production callers keep the `path.sep` default: `cpSync` passes
|
|
11
|
+
* backslash-delimited paths on Windows.
|
|
15
12
|
*/
|
|
16
13
|
export declare const shouldCopyTemplateEntry: (source: string, templateRoot: string, pathSep?: string) => boolean;
|
|
17
14
|
//# sourceMappingURL=template-filter.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-filter.d.mts","sourceRoot":"","sources":["../../src/internal/template-filter.mts"],"names":[],"mappings":"AAuBA
|
|
1
|
+
{"version":3,"file":"template-filter.d.mts","sourceRoot":"","sources":["../../src/internal/template-filter.mts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GACnC,QAAQ,MAAM,EACd,cAAc,MAAM,EACpB,UAAS,MAAY,KACnB,OAMF,CAAC"}
|
|
@@ -22,17 +22,14 @@ const EXCLUDED_DIRS = new Set(["node_modules", "dist"]);
|
|
|
22
22
|
/**
|
|
23
23
|
* Decide whether `cpSync` should copy a given source path.
|
|
24
24
|
*
|
|
25
|
-
* Only segments
|
|
26
|
-
* install
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `node_modules` directory (which is the v0.5.0 npx regression this fixes).
|
|
25
|
+
* Only segments inside `templateRoot` are checked against EXCLUDED_DIRS; the
|
|
26
|
+
* install prefix above it (e.g. `~/.npm/_npx/<hash>/node_modules/...` under
|
|
27
|
+
* `npx`) is ignored, or every file would be rejected whenever the package
|
|
28
|
+
* itself lives under a `node_modules` directory.
|
|
30
29
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* hardcoded `"/"` — `cpSync` passes back-slash-delimited absolute paths on
|
|
35
|
-
* Windows, so segment splitting must follow the platform separator.
|
|
30
|
+
* `pathSep` lets tests exercise POSIX and Windows separators on any host.
|
|
31
|
+
* Production callers keep the `path.sep` default: `cpSync` passes
|
|
32
|
+
* backslash-delimited paths on Windows.
|
|
36
33
|
*/
|
|
37
34
|
export const shouldCopyTemplateEntry = (source, templateRoot, pathSep = sep) => {
|
|
38
35
|
if (source === templateRoot)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/create-auth-provider",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a new o3co/auth.provider project from
|
|
3
|
+
"version": "0.17.0-rc.1",
|
|
4
|
+
"description": "Scaffold a new o3co/auth.provider project from one of its templates",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
"node": ">=22.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
31
|
+
"@o3co/ts.hocon": "^1.13.0",
|
|
32
|
+
"@types/node": "^26.6.3",
|
|
33
|
+
"typescript": "^6.0.3",
|
|
34
|
+
"vitest": "^5.0.2"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
37
|
"build": "tsc",
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
node_modules
|
|
2
2
|
dist
|
|
3
3
|
.git
|
|
4
|
-
.env*
|
|
5
|
-
!.env.example
|
|
6
4
|
Makefile
|
|
5
|
+
# #705: the secrets and the per-deployment configuration that `.gitignore`
|
|
6
|
+
# keeps out of git stay out of the build context as well. The builder stage
|
|
7
|
+
# COPYs `config/` and the runtime image takes it from there, so a
|
|
8
|
+
# `config/clients.yaml` in the working copy — client secrets included — was
|
|
9
|
+
# baked into a layer that travels with every push; the production compose
|
|
10
|
+
# mounts the registry instead. This is `.gitignore`'s secrets section, written
|
|
11
|
+
# with `**/` where it matches at any depth: a `.dockerignore` pattern is
|
|
12
|
+
# anchored at the context root. Keep the two in step; scaffold-assets.test.mts
|
|
13
|
+
# checks that they are.
|
|
14
|
+
**/.env*
|
|
15
|
+
!**/.env.example
|
|
16
|
+
**/*.pem
|
|
17
|
+
**/*.key
|
|
18
|
+
config/clients.yaml
|
|
19
|
+
config/*.local.conf
|
|
7
20
|
# .gitignore, the READMEs and the compose files stay in the context: the
|
|
8
21
|
# `test` image copies them because the shipped suite reads them (#512). The
|
|
9
22
|
# runtime image never copies them, so they cost the context a few kilobytes
|
|
@@ -12,10 +12,10 @@ OAUTH_JWT_ISSUER=
|
|
|
12
12
|
# openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
|
|
13
13
|
#
|
|
14
14
|
# Keep jwt-private.pem out of version control and off the image.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
#
|
|
15
|
+
KEY_STORE_LOCAL_PRIVATE_KEY_PATH=./jwt-private.pem
|
|
16
|
+
KEY_STORE_LOCAL_PUBLIC_KEY_PATH=./jwt-public.pem
|
|
17
|
+
# KEY_STORE_LOCAL_ALGORITHM=EdDSA # EdDSA | ES256 | RS256 | HS256
|
|
18
|
+
# KEY_STORE_LOCAL_KID=v0
|
|
19
19
|
#
|
|
20
20
|
# HS256 alternative — symmetric, so /.well-known/jwks.json publishes NOTHING
|
|
21
21
|
# and every relying party must hold a secret that can also MINT tokens. Only
|
|
@@ -23,22 +23,39 @@ OAUTH_JWT_PUBLIC_KEY_PATH=./jwt-public.pem
|
|
|
23
23
|
# openssl rand -hex 32
|
|
24
24
|
# Hex/base64 values are measured DECODED, so a 32-character hex string counts
|
|
25
25
|
# as 16 bytes and is refused.
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# KEY_STORE_LOCAL_ALGORITHM=HS256
|
|
27
|
+
# KEY_STORE_LOCAL_SECRET=
|
|
28
28
|
|
|
29
29
|
# --- Session (required) ------------------------------------------------------
|
|
30
30
|
# Signs the cookie that IS the authenticated session, so guessing it forges
|
|
31
31
|
# logins. Same >= 32-byte floor and same decoded measurement:
|
|
32
32
|
# openssl rand -hex 32
|
|
33
|
-
|
|
33
|
+
SESSION_STORE_SECRET=
|
|
34
34
|
|
|
35
35
|
# User repository
|
|
36
36
|
# Both URLs carry plaintext user credentials to the upstream store, so both must
|
|
37
37
|
# be absolute https URLs. http:// is accepted only for a loopback host
|
|
38
38
|
# (localhost, 127.0.0.0/8, [::1]) — a private-range address or a compose service
|
|
39
39
|
# name such as http://user-service:8080 still needs https. Boot fails otherwise.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# Each must be the endpoint that answers: no request follows a 3xx, so a URL that
|
|
41
|
+
# redirects fails every call.
|
|
42
|
+
REPOSITORIES_USER_HTTP_AUTHENTICATE_URL=
|
|
43
|
+
REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL=
|
|
44
|
+
# With MFA on: the Store's endpoint the MFA enrollment witness is written to,
|
|
45
|
+
# so a Store that loses a subject's factors cannot open a first binding to
|
|
46
|
+
# whoever holds the password. Your Store must then answer `mfaEnrolled` on
|
|
47
|
+
# both URLs above. Unset, boot warns (mfa_enrollment_witness_unwritable).
|
|
48
|
+
# REPOSITORIES_USER_HTTP_MARK_MFA_ENROLLED_URL=https://users.example.com/mfa/enrolled
|
|
49
|
+
# The credential this server presents to the Store: every Store request carries
|
|
50
|
+
# `Authorization: Bearer <token>`. Without one, anyone who can reach the Store
|
|
51
|
+
# can resolve an identity to its user, so your Store should refuse any request
|
|
52
|
+
# without it, with 401 and `WWW-Authenticate: Bearer error="invalid_token"`
|
|
53
|
+
# (which this server then reports as an outage naming the refused credential).
|
|
54
|
+
# The one token goes to every Store URL above: they must be one trust domain.
|
|
55
|
+
# At least 32 bytes, measured like SESSION_STORE_SECRET; boot fails on a weak or
|
|
56
|
+
# malformed value. Left commented out because a blank value is refused too:
|
|
57
|
+
# openssl rand -hex 32
|
|
58
|
+
# REPOSITORIES_USER_HTTP_BEARER_TOKEN=
|
|
42
59
|
|
|
43
60
|
# --- Reverse proxy (required in production) -----------------------------------
|
|
44
61
|
# Express `trust proxy`. Set this to the address or CIDR range of whatever
|
|
@@ -53,44 +70,72 @@ CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL=
|
|
|
53
70
|
# limit shares one bucket. A plain-HTTP local run needs nothing here.
|
|
54
71
|
HTTP_TRUST_PROXY=
|
|
55
72
|
|
|
56
|
-
# Redis (
|
|
73
|
+
# Redis (session storage + the shared client socket)
|
|
57
74
|
# Use redis://redis:6379 when running via docker compose
|
|
58
|
-
|
|
59
|
-
SESSION_STORAGE_REDIS_URL=redis://redis:6379
|
|
75
|
+
SESSION_STORE_STORAGE_REDIS_URL=redis://redis:6379
|
|
60
76
|
# #407: this one backs the SHARED ioredis socket -- the refresh-token family
|
|
61
77
|
# store, the access-token denylist and the rate limiter all ride on it. It
|
|
62
78
|
# defaults to redis://localhost:6379, which inside the container is the
|
|
63
79
|
# container, so leaving it unset makes the compose boot dial nothing.
|
|
64
|
-
|
|
80
|
+
REDIS_CLIENTS_URL=redis://redis:6379
|
|
65
81
|
|
|
66
82
|
# --- Federation grants (#593, optional) --------------------------------------
|
|
67
83
|
# Offline delegation of an upstream IdP's tokens to a backend or an agent, on a
|
|
68
84
|
# consent that survives logout. Off by default; on, boot refuses what is
|
|
69
85
|
# missing by name. The consent page is yours (README: Federation Grants). The
|
|
70
86
|
# connections and the encryption key ring are lists, so they live in a HOCON
|
|
71
|
-
# layer, not here. Both stores ship
|
|
72
|
-
#
|
|
87
|
+
# layer, not here. Both stores ship as none: select each (redis, or memory on
|
|
88
|
+
# one replica) with the two ADAPTERS_ lines below. Redis GRANTS need
|
|
89
|
+
# ADAPTERS_USER_SESSION_STORES=redis beside them (the compose files set it) —
|
|
73
90
|
# the grants would outlive a memory boundary that ends them.
|
|
74
91
|
# FEDERATION_GRANTS_ENABLED=true
|
|
75
92
|
# FEDERATION_GRANTS_CONSENT_URL=/consent/grants
|
|
76
|
-
# `required` seals credentials under
|
|
93
|
+
# `required` seals credentials under redis-federation-grant-store.encryptionKeys (HOCON);
|
|
77
94
|
# `allow-plaintext` is refused in production/staging and under multi.
|
|
78
|
-
#
|
|
95
|
+
# REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE=required
|
|
79
96
|
# `required` needs a user repository that covers every connection's
|
|
80
97
|
# registration: the shipped http one asks your Store at the URL below and
|
|
81
98
|
# covers what repositories.user.http.federatedIdentityLookupCoverage (HOCON)
|
|
82
99
|
# declares; `unsupported` records that the check is not made.
|
|
83
100
|
# FEDERATION_GRANTS_IDENTITY_LOOKUP=required
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
101
|
+
# REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL=https://users.example.com/find-subject-by-federated-identity
|
|
102
|
+
# ADAPTERS_FEDERATION_GRANT_STORE=redis
|
|
103
|
+
# ADAPTERS_FEDERATION_GRANT_INTENT_STORE=redis
|
|
104
|
+
|
|
105
|
+
# --- Multi-factor authentication (on by default) -----------------------------
|
|
106
|
+
# MFA_MODE: required (the default: every password login needs a second
|
|
107
|
+
# factor) | optional | off. The MFA page is yours (README: Multi-factor
|
|
108
|
+
# authentication). Development needs nothing here: config/development.conf
|
|
109
|
+
# holds a sample key, the codes are logged, and docker-compose.yml keeps the
|
|
110
|
+
# two MFA stores on its Redis. Outside development, set all of the following,
|
|
111
|
+
# or boot is refused naming them:
|
|
112
|
+
# - the two MFA stores, which ship in memory, refused outside development and
|
|
113
|
+
# test since a restart empties them of every factor: redis (or, for the
|
|
114
|
+
# factors, store);
|
|
115
|
+
# - a key of your own (openssl rand -base64 32);
|
|
116
|
+
# - the SMTP relay the account-email proof and the email factor's codes go
|
|
117
|
+
# through.
|
|
118
|
+
# The MFA page's URL defaults to /mfa, which you must serve, in development
|
|
119
|
+
# too, or every password login stops there.
|
|
120
|
+
# ADAPTERS_MFA_FACTOR_STORE=redis
|
|
121
|
+
# ADAPTERS_MFA_TRANSACTION_STORE=redis
|
|
122
|
+
# MFA_ENCRYPTION_KEY=
|
|
123
|
+
# STANDARD_SMTP_MAIL_SENDER_HOST=smtp.example.com
|
|
124
|
+
# STANDARD_SMTP_MAIL_SENDER_FROM=auth@example.com
|
|
125
|
+
# MFA_PAGE_URL=/mfa
|
|
126
|
+
# Or run without MFA:
|
|
127
|
+
# MFA_MODE=off
|
|
128
|
+
# To read the mail in development, leave every STANDARD_SMTP_MAIL_SENDER_*
|
|
129
|
+
# unset (USER and PASSWORD included) and run the Mailpit
|
|
130
|
+
# overlay, which sets them (README: Mailpit for the SMTP sender):
|
|
131
|
+
# docker compose -f docker-compose.yml -f docker-compose.mailpit.yml up --build
|
|
87
132
|
|
|
88
133
|
# Development (override for local HTTP).
|
|
89
134
|
# The default cookie name carries the `__Host-` prefix, which browsers only
|
|
90
135
|
# accept on a Secure cookie — so dropping Secure means renaming the cookie too,
|
|
91
136
|
# or boot refuses the combination.
|
|
92
|
-
|
|
93
|
-
|
|
137
|
+
SESSION_STORE_SECURE=false
|
|
138
|
+
SESSION_STORE_NAME=auth.sid
|
|
94
139
|
|
|
95
140
|
# Optional
|
|
96
141
|
# HTTP_PORT=3000
|
|
@@ -101,5 +146,5 @@ SESSION_NAME=auth.sid
|
|
|
101
146
|
# that one is unset.
|
|
102
147
|
# OAUTH_ACCESS_TOKEN_EXPIRES_IN=3600
|
|
103
148
|
# OAUTH_REFRESH_TOKEN_EXPIRES_IN=86400
|
|
104
|
-
#
|
|
105
|
-
#
|
|
149
|
+
# SESSION_STORE_MAX_AGE=3600000
|
|
150
|
+
# SESSION_STORE_SAME_SITE=lax # `none` additionally requires SESSION_STORE_SECURE=true
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# rebuild (#289). The digest is the multi-arch index, so amd64 and arm64 both
|
|
4
4
|
# resolve from it. Dependabot's `docker` ecosystem keeps the pair in step —
|
|
5
5
|
# bump the tag and the digest together, never the tag alone.
|
|
6
|
-
FROM node:26-alpine@sha256:
|
|
6
|
+
FROM node:26-alpine@sha256:0b36e8c136b94cd4fcf02188228e76c31ad5872eef3fec8cbd2eee500cfd9e80 AS node-base
|
|
7
7
|
|
|
8
8
|
ENV HOME=/home/node
|
|
9
9
|
|
|
@@ -48,6 +48,10 @@ FROM deps AS builder
|
|
|
48
48
|
USER node
|
|
49
49
|
|
|
50
50
|
COPY --chown=node:node tsconfig.json ./
|
|
51
|
+
# What `.dockerignore` lets through (#705): the committed configuration layers
|
|
52
|
+
# and the example, never `clients.yaml` or a key. The runtime image copies
|
|
53
|
+
# this directory, so anything per-deployment here would be baked into it; the
|
|
54
|
+
# production compose mounts the client registry instead.
|
|
51
55
|
COPY --chown=node:node config/ ./config/
|
|
52
56
|
COPY --chown=node:node src/ ./src/
|
|
53
57
|
|
|
@@ -92,7 +96,7 @@ EXPOSE ${HTTP_PORT}
|
|
|
92
96
|
# per-service, which keeps the restart-driving signal out of it.
|
|
93
97
|
#
|
|
94
98
|
# Use the shell form so ${HTTP_PORT} expands at probe time. The HOCON
|
|
95
|
-
# config reads `${?HTTP_PORT}` for `http.port`, so
|
|
99
|
+
# config reads `${?HTTP_PORT}` for `http.port`, so the listener and the
|
|
96
100
|
# probe stay in sync regardless of the operator's port choice.
|
|
97
101
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
98
102
|
CMD wget -q -O /dev/null "http://localhost:${HTTP_PORT}/_healthcheck" || exit 1
|
|
@@ -111,7 +115,7 @@ COPY --chown=node:node tests/ ./tests/
|
|
|
111
115
|
# on files that were never a test's business. `.dockerignore` lets them into
|
|
112
116
|
# the context for the same reason; nothing copies them into `runtime`.
|
|
113
117
|
COPY --chown=node:node Dockerfile .dockerignore .env.example .gitignore README.md README.ja.md \
|
|
114
|
-
docker-compose.yml docker-compose.production.yml ./
|
|
118
|
+
docker-compose.yml docker-compose.production.yml docker-compose.mailpit.yml ./
|
|
115
119
|
|
|
116
120
|
CMD ["pnpm", "run", "test", "--", "--passWithNoTests"]
|
|
117
121
|
|