@o3co/create-auth-provider 0.9.0 → 0.11.0

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 (37) hide show
  1. package/dist/index.d.mts +20 -0
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +110 -7
  4. package/package.json +4 -4
  5. package/templates/standalone/.env.example +49 -5
  6. package/templates/standalone/Dockerfile +59 -16
  7. package/templates/standalone/README.ja.md +92 -13
  8. package/templates/standalone/README.md +358 -18
  9. package/templates/standalone/config/application.conf +132 -4
  10. package/templates/standalone/config/clients.yaml.example +56 -2
  11. package/templates/standalone/config/production.conf +9 -3
  12. package/templates/standalone/docker-compose.production.yml +132 -0
  13. package/templates/standalone/docker-compose.yml +7 -0
  14. package/templates/standalone/gitignore +21 -0
  15. package/templates/standalone/package.json +15 -20
  16. package/templates/standalone/src/__tests__/audit-sink.test.mts +343 -0
  17. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +423 -0
  18. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +114 -0
  19. package/templates/standalone/src/__tests__/metrics.test.mts +231 -0
  20. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +76 -1
  21. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +117 -0
  22. package/templates/standalone/src/__tests__/shutdown.test.mts +244 -0
  23. package/templates/standalone/src/__tests__/smoke.test.mts +213 -11
  24. package/templates/standalone/src/__tests__/terminal-error.test.mts +96 -0
  25. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +117 -7
  26. package/templates/standalone/src/app.mts +78 -10
  27. package/templates/standalone/src/buildModules.mts +43 -1
  28. package/templates/standalone/src/configPath.mts +2 -2
  29. package/templates/standalone/src/logger.mts +90 -2
  30. package/templates/standalone/src/metrics.mts +190 -0
  31. package/templates/standalone/src/modules.mts +319 -33
  32. package/templates/standalone/src/shutdown.mts +162 -0
  33. package/templates/standalone/src/terminalError.mts +60 -0
  34. package/templates/standalone/tests/index.test.js +69 -5
  35. package/templates/standalone/tsconfig.json +9 -1
  36. package/templates/standalone/vitest.config.mts +8 -0
  37. package/templates/versions.json +12 -11
@@ -9,10 +9,46 @@
9
9
  http {
10
10
  port = 3000
11
11
  port = ${?HTTP_PORT}
12
+ # false | true | <hop count> | <address list>. Prefer naming the proxy:
13
+ # HTTP_TRUST_PROXY=10.0.0.0/8 (or `loopback` for a sidecar) rather than
14
+ # `true`, which trusts a forwarded client address from anyone who can reach
15
+ # this process. See the header comment in the library's reference.conf.
12
16
  trustProxy = false
13
17
  trustProxy = ${?HTTP_TRUST_PROXY}
14
18
  }
15
19
 
20
+ # #287: where the audit trail goes. Before this block the template wired no
21
+ # sink at all, so every security event the routes emit -- token.issued.failure,
22
+ # authorize.rejected / authorize.granted, logout.cascade_failed, and the shared
23
+ # rate-limit guard's rate_limit.unavailable -- was discarded by the artifact
24
+ # operators deploy. Nothing failed and nothing warned: `emitAuditEvent` is a
25
+ # no-op when the slot is empty.
26
+ #
27
+ # "logger" rather than reference.conf's "console": both write newline-delimited
28
+ # JSON to stdout, but "logger" writes it through the same pino stream that
29
+ # carries every other line this template emits, so a log aggregator ingests
30
+ # application logs and audit events with one parser and separates them on
31
+ # `name` ("provider" vs "audit"). "console" writes the bare event with no log
32
+ # envelope -- no level, no time -- which most pipelines then timestamp by
33
+ # ingestion. Set AUDIT_SINK_TYPE=console if that is what your pipeline wants.
34
+ #
35
+ # The audit stream's level is FIXED at info and `logging.level` does not gate
36
+ # it. LOG_LEVEL=warn is an ordinary production setting and LOG_LEVEL=silent a
37
+ # legitimate one; either silencing the audit trail would be #287 again, reached
38
+ # from the operator's side. Where events go is chosen here, and this selector
39
+ # has no "none" -- an unknown type fails boot naming the sinks that exist,
40
+ # rather than producing a deployment with no audit trail (#304).
41
+ #
42
+ # To point this at a real sink (SIEM, log pipeline, message bus): register the
43
+ # builder in `auditSinkModule` (src/modules.mts), name it here, and put its
44
+ # options in this block -- `sink { type = "splunk-hec", splunk-hec { ... } }`.
45
+ audit {
46
+ sink {
47
+ type = "logger"
48
+ type = ${?AUDIT_SINK_TYPE}
49
+ }
50
+ }
51
+
16
52
  oauth {
17
53
  grants {
18
54
  # Template is a typical web-app OP — enable the universal user-auth path.
@@ -30,13 +66,18 @@ oauth {
30
66
  enabled = true
31
67
  enabled = ${?OAUTH_GRANTS_SESSION_ENABLED}
32
68
  }
69
+ # #273: there is no `pkce` block here any more. PKCE is mandatory for
70
+ # every authorization-code client and `S256` is the only method the
71
+ # provider accepts — that is fixed policy (OAuth 2.1 §4.1.1 / RFC 9700
72
+ # §2.1.1), not a deployment setting, so `requireS256` / `required` /
73
+ # `defaultMethod` / `supportedMethods` are gone. A legacy client that
74
+ # genuinely cannot compute SHA-256 is opted into `plain` one client at a
75
+ # time, with `allowPlainPkce: true` in clients.yaml. The
76
+ # OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256 tombstone in core's
77
+ # reference.conf makes a still-exported env var warn at boot.
33
78
  authorization_code {
34
79
  enabled = true
35
80
  enabled = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED}
36
- pkce {
37
- requireS256 = false
38
- requireS256 = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256}
39
- }
40
81
  }
41
82
  refresh_token {
42
83
  enabled = true
@@ -53,10 +94,44 @@ oauth {
53
94
  # restart and across replicas — multi-replica production MUST use "redis".
54
95
  # Supersedes the legacy `repositories.code.type` switch (kept below as
55
96
  # deprecated for backward compat — see CHANGELOG for the removal version).
97
+ # #267: /authorize refuses any client not marked `firstParty: true` in
98
+ # clients.yaml — the template's example clients are marked. There is no
99
+ # opt-out: the one-time `allowUnmarkedClients` migration flag was removed
100
+ # (#330), and a config or environment still setting it fails at boot with
101
+ # migration instructions (via the tombstone in core's reference.conf).
56
102
  code {
57
103
  adapter = "redis"
58
104
  adapter = ${?OAUTH_CODE_ADAPTER}
59
105
  }
106
+ # #277: POST /oauth/revoke revokes access tokens by writing their `jti` to the
107
+ # denylist wired below, and boot FAILS if that denylist is missing — an
108
+ # RFC 7009 endpoint must answer 200, so an unwired denylist would tell every
109
+ # operator their token is revoked while it kept working until expiry.
110
+ #
111
+ # Deployments that genuinely do not revoke access tokens (short-lived tokens
112
+ # plus refresh-token revocation only) set "unsupported" instead, and the
113
+ # endpoint then answers `unsupported_token_type` for
114
+ # `token_type_hint = access_token`. Refresh-token revocation works in both
115
+ # modes and needs no denylist.
116
+ revocation {
117
+ accessToken = "denylist"
118
+ accessToken = ${?OAUTH_REVOCATION_ACCESS_TOKEN}
119
+ }
120
+ }
121
+
122
+ # #277: backend for the access-token denylist.
123
+ #
124
+ # "redis" by default, not "memory": the memory adapter keeps revocations in one
125
+ # process, so a token revoked on one replica stays valid on every other — and
126
+ # `deployment.mode = "multi"` refuses it by name for exactly that reason. It
127
+ # shares the ioredis socket configured under `refreshTokenFamilyStore.redis`
128
+ # below, so switching it on costs no extra connection.
129
+ #
130
+ # Single-instance local development can set ACCESS_TOKEN_DENYLIST_ADAPTER=memory
131
+ # to run without Redis.
132
+ accessTokenDenylist {
133
+ adapter = "redis"
134
+ adapter = ${?ACCESS_TOKEN_DENYLIST_ADAPTER}
60
135
  }
61
136
 
62
137
  session {
@@ -74,6 +149,17 @@ session {
74
149
  sameSite = ${?SESSION_SAME_SITE}
75
150
  domain = null
76
151
  domain = ${?SESSION_DOMAIN}
152
+ # #405: every URL `POST /session/login` may accept as `redirect_to`, matched
153
+ # EXACTLY -- scheme, host, port, path and query all significant. Empty (the
154
+ # default) refuses every `redirect_to`, so leave it alone unless your login
155
+ # page actually posts one.
156
+ #
157
+ # redirectAllowlist = ["https://app.example.com/welcome"]
158
+ #
159
+ # `http://` is accepted for loopback hosts only. With `domain` set above, an
160
+ # entry outside it fails boot rather than being silently ignored. The
161
+ # federation flow has a separate list per provider (see `federations` below).
162
+ redirectAllowlist = []
77
163
  storage {
78
164
  type = "redis"
79
165
  type = ${?SESSION_STORAGE_TYPE}
@@ -113,6 +199,39 @@ federations {
113
199
  clientSecret = ${?FEDERATIONS_GOOGLE_CLIENT_SECRET}
114
200
  callbackURL = "http://localhost:3000/session/oauth/federation/google/callback"
115
201
  callbackURL = ${?FEDERATIONS_GOOGLE_CALLBACK_URL}
202
+
203
+ # Every URL a `redirect_to` query parameter may name on
204
+ # /session/oauth/federation/google. Matching is EXACT — scheme, host, port,
205
+ # path, query and fragment all count — so list each landing page in full.
206
+ # There is no wildcard and no subdomain matching.
207
+ #
208
+ # An empty list refuses every `redirect_to`, which is the right setting for
209
+ # a deployment that does not use the parameter. It is NOT a way to allow
210
+ # everything: before #278 an unset allowlist accepted any http(s) URL, which
211
+ # made this an open redirect. Nothing falls back to that any more.
212
+ #
213
+ # https is required, except for a loopback host (localhost, 127.0.0.0/8,
214
+ # [::1]) — that carve-out is what lets a local dev front-end and a native
215
+ # client on an ephemeral loopback listener work without a certificate. The
216
+ # port is still part of the match, so list the port the client binds.
217
+ #
218
+ # redirectAllowlist = [
219
+ # "https://app.example.com/welcome"
220
+ # "http://localhost:5173/welcome"
221
+ # ]
222
+ redirectAllowlist = []
223
+
224
+ # Cookie / session domain. When set, every non-loopback redirectAllowlist
225
+ # entry must be inside it, checked at boot — an entry outside it fails
226
+ # startup rather than sitting here looking effective.
227
+ # sessionDomain = ${?FEDERATIONS_GOOGLE_SESSION_DOMAIN}
228
+
229
+ # Post-callback bridge page. Required only if `redirect_to` is used at all:
230
+ # the browser lands here as `<authCallbackUrl>?redirect_to=<encoded>`.
231
+ # authCallbackUrl = ${?FEDERATIONS_GOOGLE_AUTH_CALLBACK_URL}
232
+
233
+ # Where the browser goes after a callback carrying no `redirect_to`.
234
+ # clientUrl = ${?FEDERATIONS_GOOGLE_CLIENT_URL}
116
235
  }
117
236
  }
118
237
 
@@ -132,10 +251,19 @@ repositories {
132
251
  path = "./config/users.yaml"
133
252
  }
134
253
  http {
254
+ # Both URLs carry plaintext user credentials, so both must be absolute
255
+ # https URLs. http:// is accepted only for a loopback host (localhost,
256
+ # 127.0.0.0/8, [::1]); a private-range address or a container-network
257
+ # service name still needs https. Boot fails if either is not.
135
258
  authenticateUrl = ${?CLIENT_USER_AUTHENTICATE_URL}
136
259
  authenticateByTokenUrl = ${?CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL}
260
+ # Positive integer, milliseconds, <= 2147483647. Boot fails otherwise —
261
+ # including on a blank env override, which HOCON substitutes as "".
137
262
  timeout = 5000
138
263
  timeout = ${?CLIENT_USER_TIMEOUT}
264
+ # Largest upstream response body accepted, in bytes. Positive integer.
265
+ maxResponseBytes = 1048576
266
+ maxResponseBytes = ${?CLIENT_USER_MAX_RESPONSE_BYTES}
139
267
  }
140
268
  }
141
269
  code {
@@ -7,24 +7,78 @@
7
7
  # - "none": public client — no clientSecret; PKCE/S256 mandatory at /authorize
8
8
  #
9
9
  # Secret format (only for confidential clients — basic / post):
10
- # - Plain text: "my-secret"
11
- # - Bcrypt hash: "$2b$10$..." (generate with: node -e "require('bcrypt').hash('secret',10).then(console.log)")
10
+ # - Bcrypt hash: "$2b$10$..." <-- use this
11
+ # - Plain text: "my-secret" <-- accepted, for local development only
12
+ #
13
+ # Generate a real one:
14
+ # SECRET=$(openssl rand -base64 32)
15
+ # node -e "require('bcrypt').hash(process.argv[1],10).then(console.log)" "$SECRET"
16
+ # Give $SECRET to the client; put the hash in this file.
17
+ #
18
+ # #407: the schema accepts any non-empty string here, unlike `session.secret`
19
+ # and the JWT signing secret, which have carried a 256-bit floor since #282.
20
+ # That is deliberate — this field also holds a bcrypt hash, whose shape those
21
+ # floors would reject — but it means NOTHING stops you from shipping
22
+ # `clientSecret: "changeme"`. A client secret authenticates a confidential
23
+ # client at the token endpoint; treat it as you would any other credential
24
+ # and give it 256 bits of randomness.
25
+ #
26
+ # `firstParty` (#267): REQUIRED to be `true` for a client to use `/authorize`.
27
+ # `/authorize` mints a code as soon as the session is authenticated, with no
28
+ # consent step, so a forced top-level navigation from an attacker's page
29
+ # makes a logged-in user's browser mint one for whatever client is named.
30
+ # That is defensible for clients you operate and only for those. Mark a
31
+ # client `firstParty: true` only if you would be comfortable with it
32
+ # receiving the user's identity without the user being asked.
33
+ #
34
+ # A client without it is refused at `/authorize`, as is one carrying an
35
+ # explicit `firstParty: false`. There is no opt-out: mark every client you
36
+ # operate before pointing it at `/authorize`.
37
+ #
38
+ # `allowPlainPkce` (#273): PKCE is mandatory for EVERY client — confidential
39
+ # ones included — and `S256` is the only method accepted. There is no
40
+ # server-wide setting that changes either half. This per-client flag is the
41
+ # one exception hatch, and only for the method: `allowPlainPkce: true` lets
42
+ # that one client use the RFC 7636 `plain` challenge, which stores the
43
+ # verifier itself as the challenge and so proves nothing against anyone who
44
+ # can read the authorization request. Set it only for a legacy client that
45
+ # genuinely cannot compute SHA-256, and treat it as a migration deadline.
12
46
 
13
47
  my-app:
14
48
  tokenEndpointAuthMethod: "client_secret_basic"
49
+ firstParty: true
15
50
  clientSecret: "$2b$10$REPLACE_WITH_BCRYPT_HASH"
16
51
  allowedRedirectUris:
17
52
  - "http://localhost:3000/callback"
18
53
  allowedScopes:
19
54
  - "read"
20
55
  - "write"
56
+ # What an omitted `scope` parameter grants (#396). Must be a subset of
57
+ # allowedScopes (checked at boot). Without this, a request that omits
58
+ # `scope` is refused with invalid_scope — the old behavior granted the
59
+ # ENTIRE allowlist, which made "forgot to send scope" the maximum grant.
60
+ # Declare what an unscoped request should mean, not the ceiling.
61
+ defaultScopes:
62
+ - "read"
21
63
 
22
64
  # Public client example (e.g., a single-page app or mobile app):
23
65
  # my-spa:
24
66
  # tokenEndpointAuthMethod: "none"
67
+ # firstParty: true
25
68
  # # clientSecret MUST be absent for "none" clients.
26
69
  # allowedRedirectUris:
27
70
  # - "https://app.example/callback"
28
71
  # allowedScopes:
29
72
  # - "openid"
30
73
  # - "profile"
74
+
75
+ # Legacy client that cannot compute SHA-256 (#273). Prefer fixing the client.
76
+ # my-legacy-device:
77
+ # tokenEndpointAuthMethod: "client_secret_basic"
78
+ # firstParty: true
79
+ # clientSecret: "$2b$10$REPLACE_WITH_BCRYPT_HASH"
80
+ # allowPlainPkce: true
81
+ # allowedRedirectUris:
82
+ # - "https://legacy.example/callback"
83
+ # allowedScopes:
84
+ # - "openid"
@@ -2,9 +2,15 @@
2
2
  # Loaded when CONFIG_ENV (or NODE_ENV) is "production".
3
3
  # Values here override application.conf.
4
4
  #
5
- # Most production-sensitive values (OAUTH_JWT_SECRET, SESSION_SECRET, REDIS URLs,
6
- # federation client secrets, etc.) should be supplied via environment variables —
7
- # see the ${?VAR} substitutions in application.conf.
5
+ # Most production-sensitive values (SESSION_SECRET, the JWT signing key, REDIS
6
+ # URLs, federation client secrets, etc.) should be supplied via environment
7
+ # variables — see the ${?VAR} substitutions in application.conf.
8
+ #
9
+ # #407: this used to name OAUTH_JWT_SECRET as the typical example. That is the
10
+ # HMAC secret, and it stopped being the default when EdDSA became one (#282) —
11
+ # the shipped path is now a key pair delivered as files
12
+ # (OAUTH_JWT_PRIVATE_KEY_PATH / OAUTH_JWT_PUBLIC_KEY_PATH), which
13
+ # docker-compose.production.yml mounts as compose secrets.
8
14
  #
9
15
  # Override entries here only when a value must be fixed per-environment rather
10
16
  # than per-deployment (e.g. feature flags turned on only in production).
@@ -0,0 +1,132 @@
1
+ # Production compose example (#293 item 16).
2
+ #
3
+ # `docker-compose.yml` is a DEVELOPMENT file: it builds the `develop` target
4
+ # and bind-mounts ./src and ./config into the container, so copy-paste
5
+ # deploying it ships a hot-reload server running your working tree. This file
6
+ # is the deployable shape: the `runtime` target (compiled JS only, tini,
7
+ # healthcheck), no source mounts, restart policies, and a Redis that is
8
+ # reachable from the app network only and persists to a volume.
9
+ #
10
+ # openssl genpkey -algorithm ed25519 -out jwt-private.pem
11
+ # openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
12
+ # chmod 600 jwt-private.pem
13
+ # docker compose -f docker-compose.production.yml up -d --build
14
+ #
15
+ # The pem pair is required: EdDSA is the default and there is no key-material
16
+ # default, so a deployment that generates none fails at boot naming the keys to
17
+ # set. It reaches the container as a compose secret rather than through
18
+ # `config/`, which the image COPYs — see the `secrets:` block below.
19
+ #
20
+ # What this file deliberately does NOT do:
21
+ #
22
+ # - **TLS.** Terminate it in front (reverse proxy / load balancer), and set
23
+ # HTTP_TRUST_PROXY to that hop's address so `req.ip` — and every IP-keyed
24
+ # rate limit — means the client, not the proxy. See README
25
+ # "Multi-replica deployments".
26
+ # - **Scale out.** This is one replica (DEPLOYMENT_MODE=single). Before
27
+ # `--scale app=N`, set DEPLOYMENT_MODE=multi and read that same README
28
+ # section — boot will then refuse any in-memory store that must be shared.
29
+ # - **Invent secrets.** SESSION_SECRET (and any client secrets referenced by
30
+ # config/clients.yaml) come from `.env`, which is required here — chmod 600
31
+ # it, keep it out of git (`git local-files` or your secret manager of
32
+ # choice), and never bake it into the image.
33
+
34
+ services:
35
+ app:
36
+ build:
37
+ context: .
38
+ target: runtime
39
+ restart: unless-stopped
40
+ ports:
41
+ # Loopback-bound on purpose (#407). This file assumes TLS is terminated
42
+ # in front, and the README says to keep `/metrics` off the public
43
+ # listener — a bare "3000:3000" publishes on every interface and
44
+ # contradicts both. A reverse proxy on the same host reaches it here.
45
+ #
46
+ # If your load balancer is on another host, publish it instead
47
+ # (`- "3000:3000"`) and put the access control somewhere real: a
48
+ # security group, a firewall rule, or the proxy's own network. Do not
49
+ # widen this line and call it done.
50
+ - "127.0.0.1:3000:3000"
51
+ depends_on:
52
+ redis:
53
+ condition: service_healthy
54
+ env_file:
55
+ # Required, unlike the dev file: a production boot without
56
+ # SESSION_SECRET (or with the sample one) must fail loudly here, not
57
+ # serve quietly with a guessable secret.
58
+ - path: .env
59
+ required: true
60
+ secrets:
61
+ # The signing keys. EdDSA is the default algorithm and there is no key
62
+ # material default, so the process needs the pem pair at boot — and the
63
+ # `runtime` image deliberately does not contain it: `Dockerfile` COPYs
64
+ # `config/`, so a key parked there is baked into a layer that travels
65
+ # with every image push. Compose secrets mount read-only under
66
+ # /run/secrets and are the reason the env below overrides the
67
+ # `./jwt-*.pem` paths in `.env.example`.
68
+ #
69
+ # Generate them next to this file before the first `up`:
70
+ # openssl genpkey -algorithm ed25519 -out jwt-private.pem
71
+ # openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
72
+ # chmod 600 jwt-private.pem
73
+ - jwt_private_key
74
+ - jwt_public_key
75
+ environment:
76
+ NODE_ENV: production
77
+ DEPLOYMENT_MODE: single
78
+ # Point the process at the mounted secrets rather than the working-copy
79
+ # paths `.env` carries for local runs.
80
+ OAUTH_JWT_PRIVATE_KEY_PATH: /run/secrets/jwt_private_key
81
+ OAUTH_JWT_PUBLIC_KEY_PATH: /run/secrets/jwt_public_key
82
+ # `environment` wins over `env_file`, which is the point here:
83
+ # `.env.example` ships SESSION_SECURE=false / SESSION_NAME=auth.sid so a
84
+ # plain-HTTP local run works, and this file requires that same `.env`.
85
+ # Behind the TLS this compose assumes, a non-Secure session cookie is the
86
+ # session handed to anyone who can read one plaintext hop. The default
87
+ # `__Host-` cookie name is restored with it — the prefix browsers only
88
+ # honour on a Secure cookie.
89
+ SESSION_SECURE: "true"
90
+ SESSION_NAME: __Host-auth.session
91
+ # Every Redis-backed store shares the compose-network Redis below.
92
+ REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: redis://redis:6379
93
+ SESSION_STORAGE_TYPE: redis
94
+ SESSION_STORAGE_REDIS_URL: redis://redis:6379
95
+ # The memory rate limiter is per-process and evadable under bucket
96
+ # exhaustion (see README) — production points it at Redis.
97
+ RATE_LIMITER_ADAPTER: redis
98
+
99
+ redis:
100
+ # Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
101
+ # and that argument holds here too — a tag is mutable, so two `up`s a month
102
+ # apart can run different Redis builds. Pin the digest for a production
103
+ # deployment you need to reproduce:
104
+ # image: redis:7.2-alpine@sha256:<digest>
105
+ # Left as a tag in the template because a digest goes stale the week it is
106
+ # written and a scaffold that ships a stale one teaches nothing (#407).
107
+ image: redis:7.2-alpine
108
+ restart: unless-stopped
109
+ # No `ports:`, deliberately — the app reaches Redis over the compose
110
+ # network, and nothing else should. The dev file publishes 6379 to
111
+ # loopback for redis-cli convenience; a published production Redis with
112
+ # no AUTH is an incident.
113
+ command: ["redis-server", "--appendonly", "yes"]
114
+ volumes:
115
+ - redis-data:/data
116
+ healthcheck:
117
+ test: ["CMD", "redis-cli", "ping"]
118
+ interval: 5s
119
+ timeout: 3s
120
+ retries: 5
121
+
122
+ volumes:
123
+ redis-data:
124
+
125
+ secrets:
126
+ # File-backed, so the pem pair stays on the host and reaches the container
127
+ # read-only at /run/secrets. Keep both out of git alongside `.env`; a key
128
+ # committed or COPYed into the image is a key you have to rotate.
129
+ jwt_private_key:
130
+ file: ./jwt-private.pem
131
+ jwt_public_key:
132
+ file: ./jwt-public.pem
@@ -16,6 +16,13 @@ services:
16
16
  required: false
17
17
 
18
18
  redis:
19
+ # Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
20
+ # and that argument holds here too — a tag is mutable, so two `up`s a month
21
+ # apart can run different Redis builds. Pin the digest for a production
22
+ # deployment you need to reproduce:
23
+ # image: redis:7.2-alpine@sha256:<digest>
24
+ # Left as a tag in the template because a digest goes stale the week it is
25
+ # written and a scaffold that ships a stale one teaches nothing (#407).
19
26
  image: redis:7.2-alpine
20
27
  ports:
21
28
  - "127.0.0.1:6379:6379"
@@ -0,0 +1,21 @@
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.
4
+
5
+ # Secrets and per-machine configuration
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ *.pem
10
+ *.key
11
+ config/clients.yaml
12
+ config/*.local.conf
13
+
14
+ # Build output and dependencies
15
+ node_modules/
16
+ dist/
17
+ coverage/
18
+
19
+ # Editor / OS noise
20
+ .DS_Store
21
+ *.log
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@o3co/auth-provider-standalone",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
7
- "node": ">=18.19.0"
7
+ "node": ">=22.0.0"
8
8
  },
9
9
  "packageManager": "pnpm@10.29.3",
10
10
  "type": "module",
@@ -23,33 +23,28 @@
23
23
  "dependencies": {
24
24
  "@o3co/auth-provider-core": "workspace:*",
25
25
  "@o3co/auth-provider-federation-google": "workspace:*",
26
- "@o3co/auth-provider-oauth": "workspace:*",
27
- "@o3co/auth-provider-session": "workspace:*",
28
26
  "@o3co/auth-provider-foundation": "workspace:*",
27
+ "@o3co/auth-provider-oauth": "workspace:*",
29
28
  "@o3co/auth-provider-redis": "workspace:*",
30
- "@o3co/auth.utils": "^0.0.4",
31
- "@o3co/ts.hocon": "^1.2.0",
32
- "connect-redis": "^9.0.0",
29
+ "@o3co/auth-provider-session": "workspace:*",
30
+ "@o3co/ts.hocon": "^1.13.0",
31
+ "connect-redis": "^10.0.0",
33
32
  "express": "^5.2.1",
34
33
  "express-session": "^1.19.0",
35
- "helmet": "^8.1.0",
36
- "ioredis": "^5.11.0",
34
+ "helmet": "^8.3.0",
35
+ "ioredis": "^6.0.0",
37
36
  "pino": "^10.3.1",
38
- "redis": "^5.12.1"
37
+ "prom-client": "^15.1.3",
38
+ "redis": "^6.2.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/express": "^5.0.6",
42
42
  "@types/express-session": "^1",
43
- "@types/node": "^25.9.1",
44
- "@types/supertest": "^7.2.0",
43
+ "@types/node": "^26.2.0",
44
+ "@types/supertest": "^7.2.1",
45
45
  "supertest": "^7.1.0",
46
- "tsx": "^4.22.3",
47
- "typescript": "^5.9.3",
48
- "vitest": "^4.1.8"
49
- },
50
- "pnpm": {
51
- "onlyBuiltDependencies": [
52
- "bcrypt"
53
- ]
46
+ "tsx": "^4.23.10",
47
+ "typescript": "^6.0.3",
48
+ "vitest": "^4.1.11"
54
49
  }
55
50
  }