@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.
Files changed (92) hide show
  1. package/README.ja.md +139 -48
  2. package/README.md +138 -47
  3. package/dist/index.d.mts +33 -10
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +118 -59
  6. package/dist/internal/template-filter.d.mts +7 -10
  7. package/dist/internal/template-filter.d.mts.map +1 -1
  8. package/dist/internal/template-filter.mjs +7 -10
  9. package/package.json +6 -4
  10. package/templates/standalone/.dockerignore +15 -2
  11. package/templates/standalone/.env.example +69 -24
  12. package/templates/standalone/Dockerfile +7 -3
  13. package/templates/standalone/README.ja.md +677 -184
  14. package/templates/standalone/README.md +699 -238
  15. package/templates/standalone/config/application.conf +203 -325
  16. package/templates/standalone/config/clients.yaml +2 -0
  17. package/templates/standalone/config/clients.yaml.example +4 -0
  18. package/templates/standalone/config/development.conf +13 -4
  19. package/templates/standalone/config/mailpit.conf +8 -0
  20. package/templates/standalone/config/production.conf +5 -6
  21. package/templates/standalone/config/reference.conf +399 -0
  22. package/templates/standalone/docker-compose.mailpit.yml +36 -0
  23. package/templates/standalone/docker-compose.production.yml +58 -30
  24. package/templates/standalone/docker-compose.yml +13 -3
  25. package/templates/standalone/gitignore +4 -2
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
  28. package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
  32. package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
  33. package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
  34. package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
  35. package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
  36. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
  37. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
  38. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
  39. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
  40. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
  41. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
  42. package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
  43. package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
  44. package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
  45. package/templates/standalone/src/__tests__/listen.test.mts +121 -0
  46. package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
  47. package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
  48. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
  49. package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
  50. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
  51. package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
  52. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
  53. package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
  54. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
  55. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
  56. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
  57. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
  58. package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
  59. package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
  60. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
  61. package/templates/standalone/src/__tests__/sections.test.mts +480 -0
  62. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
  63. package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
  64. package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
  65. package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
  66. package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
  67. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
  68. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
  69. package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
  70. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
  71. package/templates/standalone/src/adapters.mts +149 -0
  72. package/templates/standalone/src/app.mts +71 -115
  73. package/templates/standalone/src/buildModules.mts +229 -251
  74. package/templates/standalone/src/configPath.mts +422 -29
  75. package/templates/standalone/src/listen.mts +60 -0
  76. package/templates/standalone/src/logger.mts +31 -57
  77. package/templates/standalone/src/metrics.mts +24 -47
  78. package/templates/standalone/src/mfaSwitch.mts +262 -0
  79. package/templates/standalone/src/modules.mts +480 -464
  80. package/templates/standalone/src/rootRenames.mts +79 -0
  81. package/templates/standalone/src/routes.mts +89 -0
  82. package/templates/standalone/src/sections.mts +366 -0
  83. package/templates/standalone/src/shutdown.mts +54 -126
  84. package/templates/standalone/tests/index.test.js +2 -2
  85. package/templates/standalone/tests/package-lock.json +1615 -2223
  86. package/templates/standalone/tests/package.json +2 -2
  87. package/templates/standalone/tests/vitest.config.mjs +26 -0
  88. package/templates/standalone/vitest.config.mts +8 -7
  89. package/templates/standalone/vitest.supertest-loopback.mts +97 -69
  90. package/templates/versions.json +18 -15
  91. package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
  92. package/templates/standalone/src/terminalError.mts +0 -60
@@ -15,7 +15,11 @@
15
15
  # The pem pair is required: EdDSA is the default and there is no key-material
16
16
  # default, so a deployment that generates none fails at boot naming the keys to
17
17
  # set. It reaches the container as a compose secret rather than through
18
- # `config/`, which the image COPYs — see the `secrets:` block below.
18
+ # `config/`, which the image COPYs — see the `secrets:` block below. So does
19
+ # the client registry, `config/clients.yaml` (#705): it is per-deployment, so
20
+ # `.gitignore` keeps it out of git and `.dockerignore` out of the image, and
21
+ # the image carries no registry of its own. Create it from
22
+ # `config/clients.yaml.example` before the first `up`.
19
23
  #
20
24
  # What this file deliberately does NOT do:
21
25
  #
@@ -24,13 +28,13 @@
24
28
  # and every IP-keyed rate limit — means the client, not the proxy; compose
25
29
  # refuses to start until you supply it. See README "Multi-replica
26
30
  # deployments".
27
- # - **Scale out.** This is one replica (DEPLOYMENT_MODE=single). Before
28
- # `--scale app=N`, set DEPLOYMENT_MODE=multi and read that same README
31
+ # - **Scale out.** This is one replica (CORE_DEPLOYMENT_MODE=single). Before
32
+ # `--scale app=N`, set CORE_DEPLOYMENT_MODE=multi and read that same README
29
33
  # section — boot will then refuse any in-memory store that must be shared.
30
- # - **Invent secrets.** SESSION_SECRET (and any client secrets referenced by
31
- # config/clients.yaml) come from `.env`, which is required here — chmod 600
32
- # it, keep it out of git (`git local-files` or your secret manager of
33
- # choice), and never bake it into the image.
34
+ # - **Invent secrets.** SESSION_STORE_SECRET comes from `.env`, which is required
35
+ # here — chmod 600 it, keep it out of git (`git local-files` or your secret
36
+ # manager of choice), and never bake it into the image. The client
37
+ # registry's secrets are yours to write too; give them as bcrypt hashes.
34
38
 
35
39
  services:
36
40
  app:
@@ -49,11 +53,11 @@ services:
49
53
  # security group, a firewall rule, or the proxy's own network. Do not
50
54
  # widen this line and call it done.
51
55
  - "127.0.0.1:3000:3000"
52
- # #593 slice 7: compose's default grace is ten seconds — the HTTP drain
56
+ # Compose's default grace is ten seconds — the HTTP drain
53
57
  # budget alone. With federation grants on, `src/app.mts` gives cleanup 45
54
58
  # more for the drain of a rotated upstream credential's write, and SIGKILL
55
59
  # has to arrive after that: drain (10) + cleanup (45) + an exit margin.
56
- # The cleanup allowance grows by itself with `federationGrants.upstreamHardTimeoutMs`,
60
+ # The cleanup allowance grows by itself with `federation-grants.upstreamHardTimeoutMs`,
57
61
  # `persistRetryBudgetMs` and `lockWaitMs`; raise this to match when you raise them.
58
62
  stop_grace_period: 60s
59
63
  depends_on:
@@ -61,7 +65,7 @@ services:
61
65
  condition: service_healthy
62
66
  env_file:
63
67
  # Required, unlike the dev file: a production boot without
64
- # SESSION_SECRET (or with the sample one) must fail loudly here, not
68
+ # SESSION_STORE_SECRET (or with the sample one) must fail loudly here, not
65
69
  # serve quietly with a guessable secret.
66
70
  - path: .env
67
71
  required: true
@@ -80,6 +84,10 @@ services:
80
84
  # chmod 600 jwt-private.pem
81
85
  - jwt_private_key
82
86
  - jwt_public_key
87
+ # The client registry (#705), for the same reason: `config/clients.yaml`
88
+ # is per-deployment and may hold client secrets, so it is kept out of the
89
+ # image and mounted here. REPOSITORIES_CLIENT_YAML_PATH below points the process at it.
90
+ - client_registry
83
91
  environment:
84
92
  NODE_ENV: production
85
93
  # One replica. Under `single` the replica-safety guard is SILENT by
@@ -88,7 +96,7 @@ services:
88
96
  # why the adapter block further down states every one of them explicitly
89
97
  # instead of leaving them to inherit. Before `--scale app=N`, set `multi`
90
98
  # and read the README's "Multi-replica deployments".
91
- DEPLOYMENT_MODE: single
99
+ CORE_DEPLOYMENT_MODE: single
92
100
  # Behind the TLS terminator this file assumes, `req.ip` is the proxy's
93
101
  # address and `req.protocol` reads `http` until this names the hop. The
94
102
  # README calls it required for exactly that reason, and the cost of
@@ -109,29 +117,30 @@ services:
109
117
  HTTP_TRUST_PROXY: ${HTTP_TRUST_PROXY:?set to your TLS terminator's address or CIDR range (e.g. 10.0.0.0/8, or `loopback` for a same-host sidecar) — never `true` behind an open network}
110
118
  # Point the process at the mounted secrets rather than the working-copy
111
119
  # paths `.env` carries for local runs.
112
- OAUTH_JWT_PRIVATE_KEY_PATH: /run/secrets/jwt_private_key
113
- OAUTH_JWT_PUBLIC_KEY_PATH: /run/secrets/jwt_public_key
120
+ KEY_STORE_LOCAL_PRIVATE_KEY_PATH: /run/secrets/jwt_private_key
121
+ KEY_STORE_LOCAL_PUBLIC_KEY_PATH: /run/secrets/jwt_public_key
122
+ REPOSITORIES_CLIENT_YAML_PATH: /run/secrets/client_registry
114
123
  # `environment` wins over `env_file`, which is the point here:
115
- # `.env.example` ships SESSION_SECURE=false / SESSION_NAME=auth.sid so a
124
+ # `.env.example` ships SESSION_STORE_SECURE=false / SESSION_STORE_NAME=auth.sid so a
116
125
  # plain-HTTP local run works, and this file requires that same `.env`.
117
126
  # Behind the TLS this compose assumes, a non-Secure session cookie is the
118
127
  # session handed to anyone who can read one plaintext hop. The default
119
128
  # `__Host-` cookie name is restored with it — the prefix browsers only
120
129
  # honour on a Secure cookie.
121
- SESSION_SECURE: "true"
122
- SESSION_NAME: __Host-auth.session
130
+ SESSION_STORE_SECURE: "true"
131
+ SESSION_STORE_NAME: __Host-auth.session
123
132
  # Every Redis-backed store shares the compose-network Redis below.
124
133
  #
125
134
  # These are stated in full rather than inherited from
126
- # `config/application.conf`, so that the deployable file names its own
135
+ # `config/reference.conf`, so that the deployable file names its own
127
136
  # topology: an operator reading only this one can see which stores
128
- # survive a restart. `OAUTH_CODE_ADAPTER` and
129
- # `ACCESS_TOKEN_DENYLIST_ADAPTER` already ship as `redis` in that config
130
- # layer and are left there on purpose — restating a value that agrees is
137
+ # survive a restart. `ADAPTERS_CODE_REPOSITORY` and
138
+ # `ADAPTERS_ACCESS_TOKEN_DENYLIST` already ship as `redis` in that file
139
+ # and are left there on purpose — restating a value that agrees is
131
140
  # a second place to forget to change.
132
- REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: redis://redis:6379
133
- SESSION_STORAGE_TYPE: redis
134
- SESSION_STORAGE_REDIS_URL: redis://redis:6379
141
+ REDIS_CLIENTS_URL: redis://redis:6379
142
+ SESSION_STORE_STORAGE_TYPE: redis
143
+ SESSION_STORE_STORAGE_REDIS_URL: redis://redis:6379
135
144
  # The UserSession stores: the provider's own record of who is signed in,
136
145
  # which RPs a session has touched, and the family / federation indexes
137
146
  # hanging off it. They MUST be on Redis whenever the express-session
@@ -145,13 +154,13 @@ services:
145
154
  # authenticated session, finds no user session behind it, and sends the
146
155
  # browser to log in again, where the still-valid cookie puts it straight
147
156
  # back. A login loop that clears only when the user deletes the cookie.
148
- # `DEPLOYMENT_MODE: single` says nothing about it: the replica guard
157
+ # `CORE_DEPLOYMENT_MODE: single` says nothing about it: the replica guard
149
158
  # answers "can these stores be shared", not "do these two stores have the
150
159
  # same lifetime".
151
- USER_SESSION_STORES_ADAPTER: redis
160
+ ADAPTERS_USER_SESSION_STORES: redis
152
161
  # The memory rate limiter is per-process and evadable under bucket
153
162
  # exhaustion (see README) — production points it at Redis.
154
- RATE_LIMITER_ADAPTER: redis
163
+ ADAPTERS_RATE_LIMITER: redis
155
164
  # The federation token store is deliberately left at its `memory`
156
165
  # default, and that is coherent here rather than an oversight: this
157
166
  # template ships every federation disabled, so nothing writes to the
@@ -159,8 +168,24 @@ services:
159
168
  # (REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY) that this file must not
160
169
  # invent — the records are long-lived upstream refresh tokens. Enabling
161
170
  # a federation means setting BOTH, together:
162
- # FEDERATION_TOKEN_STORE_TYPE: redis
171
+ # ADAPTERS_FEDERATION_TOKEN_STORE: redis
163
172
  # REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: <openssl rand -base64 32, from .env>
173
+ #
174
+ # MFA is on by default (MFA_MODE, `required`): every password login
175
+ # needs a second factor. Its two stores are left at their `memory`
176
+ # default, which the template refuses in production before boot; that
177
+ # refusal names everything MFA needs and the opt-out, so it is the first
178
+ # thing a deployment that has decided nothing about MFA meets. Decide in
179
+ # `.env`: either turn it on with all of
180
+ # ADAPTERS_MFA_FACTOR_STORE=redis (or `store`, the factors in your Store)
181
+ # ADAPTERS_MFA_TRANSACTION_STORE=redis
182
+ # MFA_ENCRYPTION_KEY=<openssl rand -base64 32>
183
+ # STANDARD_SMTP_MAIL_SENDER_HOST=... and STANDARD_SMTP_MAIL_SENDER_FROM=...
184
+ # MFA_PAGE_URL=<your MFA page, /mfa by default>
185
+ # or keep the provider without MFA:
186
+ # MFA_MODE=off
187
+ # The Redis below persists (appendonly), as the MFA stores need: a lost
188
+ # factor lets whoever holds the password bind one of their own.
164
189
 
165
190
  redis:
166
191
  # Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
@@ -189,10 +214,13 @@ volumes:
189
214
  redis-data:
190
215
 
191
216
  secrets:
192
- # File-backed, so the pem pair stays on the host and reaches the container
193
- # read-only at /run/secrets. Keep both out of git alongside `.env`; a key
194
- # committed or COPYed into the image is a key you have to rotate.
217
+ # File-backed, so the pem pair and the client registry stay on the host and
218
+ # reach the container read-only at /run/secrets. Keep them out of git
219
+ # alongside `.env`; a key committed or COPYed into the image is a key you
220
+ # have to rotate.
195
221
  jwt_private_key:
196
222
  file: ./jwt-private.pem
197
223
  jwt_public_key:
198
224
  file: ./jwt-public.pem
225
+ client_registry:
226
+ file: ./config/clients.yaml
@@ -8,11 +8,11 @@ services:
8
8
  volumes:
9
9
  - ./src:/home/node/src
10
10
  - ./config:/home/node/config
11
- # #593 slice 7: compose's default grace is ten seconds — the HTTP drain
11
+ # Compose's default grace is ten seconds — the HTTP drain
12
12
  # budget alone. With federation grants on, `src/app.mts` gives cleanup 45
13
13
  # more for the drain of a rotated upstream credential's write, and SIGKILL
14
14
  # has to arrive after that: drain (10) + cleanup (45) + an exit margin.
15
- # The cleanup allowance grows by itself with `federationGrants.upstreamHardTimeoutMs`,
15
+ # The cleanup allowance grows by itself with `federation-grants.upstreamHardTimeoutMs`,
16
16
  # `persistRetryBudgetMs` and `lockWaitMs`; raise this to match when you raise them.
17
17
  stop_grace_period: 60s
18
18
  depends_on:
@@ -37,7 +37,17 @@ services:
37
37
  # debug it. Redis is already running and already required by three other
38
38
  # stores, so this costs nothing: the shared ioredis socket is open
39
39
  # either way.
40
- USER_SESSION_STORES_ADAPTER: redis
40
+ ADAPTERS_USER_SESSION_STORES: redis
41
+ # MFA is on by default (MFA_MODE). Its two stores ship in memory, which
42
+ # this run would empty on every save: every enrolled factor gone while
43
+ # the sessions above survive, so each password login after a save binds
44
+ # a factor anew. On the Redis above they last as the sessions do. These
45
+ # lines, like ADAPTERS_USER_SESSION_STORES above, override `.env`:
46
+ # compose's `environment:` wins over `env_file`. The key ring is
47
+ # config/development.conf's sample key and the mail sender logs each
48
+ # code; MFA_MODE=off in .env runs without MFA.
49
+ ADAPTERS_MFA_FACTOR_STORE: redis
50
+ ADAPTERS_MFA_TRANSACTION_STORE: redis
41
51
 
42
52
  redis:
43
53
  # Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
@@ -1,8 +1,10 @@
1
1
  # #407: without this file, the first `git add .` in a scaffolded project
2
- # commits the two things the README's own setup steps tell you to create right
3
- # here — the `.env` holding SESSION_SECRET, and the signing key pair.
2
+ # commits the two things a local setup creates right here — the signing key
3
+ # pair the README's Usage generates, and the `.env` (holding SESSION_SECRET)
4
+ # the compose files read.
4
5
 
5
6
  # Secrets and per-machine configuration
7
+ # `.dockerignore` keeps the same out of the image (#705); change both together.
6
8
  .env
7
9
  .env.*
8
10
  !.env.example
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/auth-provider-standalone",
3
- "version": "0.15.0",
3
+ "version": "0.17.0-rc.1",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -26,9 +26,11 @@
26
26
  "@o3co/auth-provider-federation-grants": "workspace:*",
27
27
  "@o3co/auth-provider-federation-oidc": "workspace:*",
28
28
  "@o3co/auth-provider-foundation": "workspace:*",
29
+ "@o3co/auth-provider-mfa": "workspace:*",
29
30
  "@o3co/auth-provider-oauth": "workspace:*",
30
31
  "@o3co/auth-provider-redis": "workspace:*",
31
32
  "@o3co/auth-provider-session": "workspace:*",
33
+ "@o3co/auth-provider-standard": "workspace:*",
32
34
  "@o3co/ts.hocon": "^1.13.0",
33
35
  "connect-redis": "^10.0.0",
34
36
  "express": "^5.2.1",
@@ -37,16 +39,17 @@
37
39
  "ioredis": "^6.0.0",
38
40
  "pino": "^10.3.1",
39
41
  "prom-client": "^15.1.3",
40
- "redis": "^6.2.1"
42
+ "redis": "^6.2.1",
43
+ "zod": "^4.6.5"
41
44
  },
42
45
  "devDependencies": {
43
46
  "@types/express": "^5.0.6",
44
47
  "@types/express-session": "^1",
45
- "@types/node": "^26.5.1",
48
+ "@types/node": "^26.6.3",
46
49
  "@types/supertest": "^7.2.1",
47
- "supertest": "^7.1.0",
48
- "tsx": "^4.23.13",
50
+ "supertest": "^7.3.0",
51
+ "tsx": "^4.23.15",
49
52
  "typescript": "^6.0.3",
50
- "vitest": "^5.0.0"
53
+ "vitest": "^5.0.2"
51
54
  }
52
55
  }
@@ -16,53 +16,84 @@
16
16
 
17
17
  /**
18
18
  * `oauth.accessToken.expiresIn` / OAUTH_ACCESS_TOKEN_EXPIRES_IN is a deprecated
19
- * alias of `defaultExpiresIn`, and the composition says so once — the OR-9
20
- * shape `repositories.code.type` already has.
19
+ * alias of `defaultExpiresIn`, and the composition says so once.
21
20
  *
22
- * The line has to be exact in both directions. Core's `reference.conf` keeps
23
- * the shipped lifetime on the deprecated key, so "the alias supplied the
24
- * default" describes every deployment that set nothing; warning on that would
25
- * train operators to ignore the line. Only an override of the old key is
26
- * something to move.
21
+ * Core's `reference.conf` keeps the shipped lifetime on the deprecated key, so
22
+ * the alias supplies the default for every deployment that set nothing; a
23
+ * warning there would train operators to ignore it. Only an override of the old
24
+ * key is something to move.
27
25
  */
28
26
 
29
27
  import { fileURLToPath } from "node:url";
30
- import { type AppConfig, AppConfigSchema } from "@o3co/auth-provider-core";
28
+ import { AppConfigSchema, coreReference, type Logger } from "@o3co/auth-provider-core";
31
29
  import { parseFile } from "@o3co/ts.hocon";
32
30
  import { validate } from "@o3co/ts.hocon/zod";
33
31
  import { afterEach, describe, expect, it, vi } from "vitest";
34
32
  import { buildModules } from "../buildModules.mjs";
35
- import { resolveConfigPaths, resolveLibraryReferenceConfPath } from "../configPath.mjs";
33
+ import { resolveConfigPaths, type Switches } from "../configPath.mjs";
34
+ import { templateReference } from "../modules.mjs";
35
+ import { rootSectionsOf } from "./library-references.fixture.mjs";
36
36
 
37
37
  const configDir = fileURLToPath(new URL("../../config", import.meta.url));
38
38
 
39
39
  const REQUIRED_ENV = {
40
- OAUTH_JWT_SECRET: "access-token-alias.at-least-32-bytes.ok",
40
+ KEY_STORE_LOCAL_SECRET: "access-token-alias.at-least-32-bytes.ok",
41
41
  OAUTH_JWT_ISSUER: "https://auth.test",
42
- SESSION_SECRET: "access-token-alias-session.at-least-32-bytes.ok",
42
+ SESSION_STORE_SECRET: "access-token-alias-session.at-least-32-bytes.ok",
43
43
  };
44
44
 
45
45
  /** The shipped layers, resolved the way `app.mts` resolves them. */
46
- function loadShipped(env: Record<string, string> = {}): AppConfig {
46
+ function loadShipped(env: Record<string, string> = {}): Switches {
47
47
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "development");
48
48
  const resolvedEnv = { ...REQUIRED_ENV, ...env };
49
- return validate(
50
- parseFile(envConfPath, { env: resolvedEnv })
51
- .withFallback(parseFile(applicationConfPath, { env: resolvedEnv }))
52
- .withFallback(parseFile(resolveLibraryReferenceConfPath(), { env: resolvedEnv })),
53
- AppConfigSchema,
54
- );
49
+ const layers = parseFile(envConfPath, { env: resolvedEnv })
50
+ .withFallback(parseFile(applicationConfPath, { env: resolvedEnv }))
51
+ .withFallback(parseFile(fileURLToPath(templateReference()), { env: resolvedEnv }))
52
+ .withFallback(parseFile(fileURLToPath(coreReference()), { env: resolvedEnv }));
53
+ return { ...validate(layers, AppConfigSchema), ...rootSectionsOf(layers, resolvedEnv) };
55
54
  }
56
55
 
57
- /** Every console warning `buildModules` emits about the deprecated key. */
58
- function aliasWarnings(config: AppConfig): string[] {
59
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
60
- buildModules(config);
61
- return warn.mock.calls
62
- .map((call) => String(call[0]))
63
- .filter((message) => message.includes("oauth.accessToken.expiresIn"));
56
+ /** What `buildModules` hands the logger it is given, per level. */
57
+ function logged(config: Switches): Array<{ level: string; args: unknown[] }> {
58
+ const calls: Array<{ level: string; args: unknown[] }> = [];
59
+ const record =
60
+ (level: string) =>
61
+ (...args: unknown[]): void => {
62
+ calls.push({ level, args });
63
+ };
64
+ const logger: Logger = {
65
+ trace: record("trace"),
66
+ debug: record("debug"),
67
+ info: record("info"),
68
+ warn: record("warn"),
69
+ error: record("error"),
70
+ fatal: record("fatal"),
71
+ child: () => logger,
72
+ };
73
+ buildModules(config, { logger });
74
+ return calls;
64
75
  }
65
76
 
77
+ /** The one line the alias is reported by: object-first, an event name, at warn. */
78
+ const ALIAS_WARNING = {
79
+ level: "warn",
80
+ args: [
81
+ {
82
+ key: "oauth.accessToken.expiresIn",
83
+ env: "OAUTH_ACCESS_TOKEN_EXPIRES_IN",
84
+ replacement: "oauth.accessToken.defaultExpiresIn",
85
+ replacementEnv: "OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN",
86
+ },
87
+ "config_key_deprecated",
88
+ ],
89
+ };
90
+
91
+ /** Every line `buildModules` writes about the deprecated key. */
92
+ const aliasWarnings = (config: Switches) =>
93
+ logged(config).filter(
94
+ ({ args }) => (args[0] as { key?: unknown } | undefined)?.key === "oauth.accessToken.expiresIn",
95
+ );
96
+
66
97
  describe("the deprecated access-token lifetime alias", () => {
67
98
  afterEach(() => {
68
99
  vi.restoreAllMocks();
@@ -81,12 +112,8 @@ describe("the deprecated access-token lifetime alias", () => {
81
112
  );
82
113
  });
83
114
 
84
- it("warns once when OAUTH_ACCESS_TOKEN_EXPIRES_IN still decides the default", () => {
85
- const warnings = aliasWarnings(loadShipped({ OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" }));
86
- expect(warnings).toHaveLength(1);
87
- expect(warnings[0]).toMatch(/deprecated/);
88
- expect(warnings[0]).toMatch(/oauth\.accessToken\.defaultExpiresIn/);
89
- expect(warnings[0]).toMatch(/OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN/);
115
+ it("warns once, object-first, when OAUTH_ACCESS_TOKEN_EXPIRES_IN still decides the default", () => {
116
+ expect(logged(loadShipped({ OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" }))).toEqual([ALIAS_WARNING]);
90
117
  });
91
118
 
92
119
  it("warns for a hand-built configuration that overrides only the old key", () => {
@@ -96,6 +123,12 @@ describe("the deprecated access-token lifetime alias", () => {
96
123
  ...config,
97
124
  oauth: { ...config.oauth, accessToken: { expiresIn: 900 } },
98
125
  }),
99
- ).toHaveLength(1);
126
+ ).toEqual([ALIAS_WARNING]);
127
+ });
128
+
129
+ it("writes the same line through consoleLogger when no logger is handed over", () => {
130
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
131
+ buildModules(loadShipped({ OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" }));
132
+ expect(warn.mock.calls).toEqual([ALIAS_WARNING.args]);
100
133
  });
101
134
  });