@o3co/create-auth-provider 0.17.0 → 0.18.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.md +2 -198
- package/main.js +4 -0
- package/package.json +8 -30
- package/README.ja.md +0 -200
- package/dist/cli.d.mts +0 -3
- package/dist/cli.d.mts.map +0 -1
- package/dist/cli.mjs +0 -18
- package/dist/index.d.mts +0 -48
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -325
- package/dist/internal/template-filter.d.mts +0 -14
- package/dist/internal/template-filter.d.mts.map +0 -1
- package/dist/internal/template-filter.mjs +0 -42
- package/templates/standalone/.dockerignore +0 -23
- package/templates/standalone/.env.example +0 -152
- package/templates/standalone/Dockerfile +0 -125
- package/templates/standalone/Makefile +0 -12
- package/templates/standalone/README.ja.md +0 -912
- package/templates/standalone/README.md +0 -1536
- package/templates/standalone/config/application.conf +0 -351
- package/templates/standalone/config/clients.yaml +0 -4
- package/templates/standalone/config/clients.yaml.example +0 -124
- package/templates/standalone/config/development.conf +0 -17
- package/templates/standalone/config/mailpit.conf +0 -13
- package/templates/standalone/config/production.conf +0 -15
- package/templates/standalone/config/reference.conf +0 -427
- package/templates/standalone/docker-compose.mailpit.yml +0 -39
- package/templates/standalone/docker-compose.production.yml +0 -229
- package/templates/standalone/docker-compose.test.yml +0 -16
- package/templates/standalone/docker-compose.yml +0 -67
- package/templates/standalone/gitignore +0 -23
- package/templates/standalone/package.json +0 -56
- package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
- package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
- package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
- package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
- package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
- package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
- package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
- package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
- package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
- package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
- package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
- package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
- package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
- package/templates/standalone/src/__tests__/listen.test.mts +0 -121
- package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
- package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
- package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
- package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
- package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
- package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
- package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
- package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
- package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
- package/templates/standalone/src/__tests__/sections.test.mts +0 -516
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
- package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
- package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
- package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
- package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
- package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
- package/templates/standalone/src/adapters.mts +0 -162
- package/templates/standalone/src/app.mts +0 -127
- package/templates/standalone/src/bootRefusal.mts +0 -117
- package/templates/standalone/src/buildModules.mts +0 -358
- package/templates/standalone/src/configPath.mts +0 -487
- package/templates/standalone/src/listen.mts +0 -60
- package/templates/standalone/src/logger.mts +0 -80
- package/templates/standalone/src/metrics.mts +0 -167
- package/templates/standalone/src/mfaSwitch.mts +0 -284
- package/templates/standalone/src/modules.mts +0 -705
- package/templates/standalone/src/rootRenames.mts +0 -95
- package/templates/standalone/src/routes.mts +0 -89
- package/templates/standalone/src/sections.mts +0 -378
- package/templates/standalone/src/securityHeaders.mts +0 -36
- package/templates/standalone/src/shutdown.mts +0 -213
- package/templates/standalone/tests/Makefile +0 -5
- package/templates/standalone/tests/index.test.js +0 -283
- package/templates/standalone/tests/package-lock.json +0 -1617
- package/templates/standalone/tests/package.json +0 -16
- package/templates/standalone/tests/vitest.config.mjs +0 -26
- package/templates/standalone/tsconfig.json +0 -31
- package/templates/standalone/vitest.config.mts +0 -31
- package/templates/standalone/vitest.supertest-loopback.mts +0 -278
- package/templates/versions.json +0 -20
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import {
|
|
18
|
-
consoleLogger,
|
|
19
|
-
createAuditSinkFactory,
|
|
20
|
-
createFederationTokenStoreFactory,
|
|
21
|
-
createInMemorySessionLifecycleStore,
|
|
22
|
-
createInMemorySubjectRevocation,
|
|
23
|
-
createInMemorySubjectSessionIndex,
|
|
24
|
-
createInMemoryUserSessionStore,
|
|
25
|
-
createKeyStoreFactory,
|
|
26
|
-
createRepositoryFactories,
|
|
27
|
-
defineModule,
|
|
28
|
-
type LifecycleRegistrar,
|
|
29
|
-
type Logger,
|
|
30
|
-
loggableError,
|
|
31
|
-
type Module,
|
|
32
|
-
type ReadinessRegistrar,
|
|
33
|
-
registerBuiltinAuditSinks,
|
|
34
|
-
registerBuiltinFederationTokenStores,
|
|
35
|
-
registerBuiltinKeyStores,
|
|
36
|
-
} from "@o3co/auth-provider-core";
|
|
37
|
-
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
38
|
-
import {
|
|
39
|
-
makeIoredisClients,
|
|
40
|
-
makeIoredisFederationGrantIntentStoreClient,
|
|
41
|
-
makeIoredisFederationGrantStoreClient,
|
|
42
|
-
} from "@o3co/auth-provider-redis/ioredis";
|
|
43
|
-
// Named import, not default: ioredis is CJS (`module.exports = Redis`, the
|
|
44
|
-
// class re-exported as both `default` and `Redis`), so under `module:
|
|
45
|
-
// "nodenext"` with esModuleInterop the default import resolves to the
|
|
46
|
-
// module-exports namespace object and `new Redis(...)` raises TS2351 "not
|
|
47
|
-
// constructable". Verified against ioredis 6.0.0. The repo's default-import
|
|
48
|
-
// sites live under `packages/redis/__tests__/`, which the strict tsc build
|
|
49
|
-
// excludes.
|
|
50
|
-
import { Redis } from "ioredis";
|
|
51
|
-
import { createAuditLogger, createLoggerAuditSink } from "./logger.mjs";
|
|
52
|
-
import {
|
|
53
|
-
type Adapters,
|
|
54
|
-
auditSinkSectionSchema,
|
|
55
|
-
httpSectionSchema,
|
|
56
|
-
inMemoryCodeRepositorySectionSchema,
|
|
57
|
-
keyStoreSectionSchema,
|
|
58
|
-
loggingSectionSchema,
|
|
59
|
-
redisClientsSectionSchema,
|
|
60
|
-
repositoriesSectionSchemaFor,
|
|
61
|
-
} from "./sections.mjs";
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Turn a `{ type, [type]: {...} }` adapter-config slice into the flat
|
|
65
|
-
* `{ type, ...rest }` shape `AdapterFactory<T>.create()` consumes.
|
|
66
|
-
* Composition-root concern; not a library export.
|
|
67
|
-
*/
|
|
68
|
-
function flattenAdapterConfig(
|
|
69
|
-
section: ({ type: string } | { provider: string }) & Record<string, unknown>,
|
|
70
|
-
): { type: string } & Record<string, unknown> {
|
|
71
|
-
const selector =
|
|
72
|
-
(section as { type?: string; provider?: string }).type ??
|
|
73
|
-
(section as { provider?: string }).provider;
|
|
74
|
-
if (typeof selector !== "string") {
|
|
75
|
-
throw new TypeError("flattenAdapterConfig: section requires 'type' or 'provider' string");
|
|
76
|
-
}
|
|
77
|
-
const sub = section[selector];
|
|
78
|
-
const flattenedSub =
|
|
79
|
-
typeof sub === "object" && sub !== null && !Array.isArray(sub)
|
|
80
|
-
? (sub as Record<string, unknown>)
|
|
81
|
-
: {};
|
|
82
|
-
return { type: selector, ...flattenedSub };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The template's `config/reference.conf`, resolved from this file, which sits
|
|
87
|
-
* one directory under the template's root in `src/` and in `dist/` alike.
|
|
88
|
-
*/
|
|
89
|
-
const TEMPLATE_REFERENCE_HREF: string = new URL("../config/reference.conf", import.meta.url).href;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The template's `config/reference.conf`, which every module here that owns a
|
|
93
|
-
* section declares. A new `URL` per call: a shared one could be changed in place.
|
|
94
|
-
*/
|
|
95
|
-
export function templateReference(): URL {
|
|
96
|
-
return new URL(TEMPLATE_REFERENCE_HREF);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Logging module: owns `logging {}`. It provides nothing: the logger is built
|
|
101
|
-
* before boot from this section (`readLogging`) and handed in as a bootstrap
|
|
102
|
-
* component, since the template logs while it chooses its modules.
|
|
103
|
-
* `LOG_LEVEL` is renamed after the path, `LOGGING_LEVEL`.
|
|
104
|
-
*/
|
|
105
|
-
export const loggingModule = defineModule({
|
|
106
|
-
name: "logging",
|
|
107
|
-
section: {
|
|
108
|
-
schema: loggingSectionSchema,
|
|
109
|
-
reference: templateReference(),
|
|
110
|
-
renamedVariables: { LOG_LEVEL: "logging.level" },
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/** What the host process reads of `http {}`, beside what the `httpSettings` slot carries. */
|
|
115
|
-
export interface HttpHostSettings {
|
|
116
|
-
/** The port the server listens on; `0` lets the OS pick one. */
|
|
117
|
-
readonly port: number;
|
|
118
|
-
/** The per-probe deadline of `/readyz` and the metrics scrape, in milliseconds. */
|
|
119
|
-
readonly readinessTimeoutMs: number;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
declare module "@o3co/auth-provider-core" {
|
|
123
|
-
interface ComponentMap {
|
|
124
|
-
/** What the host process reads of `http {}`: provided by the `http` module. */
|
|
125
|
-
readonly httpHostSettings?: HttpHostSettings;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* HTTP module: owns `http {}`, its CORS list (`http.cors`) included, and
|
|
131
|
-
* provides core's `httpSettings`, authoritative, and the host's
|
|
132
|
-
* `httpHostSettings`, both eager: only `app.mts` reads the host's, and no
|
|
133
|
-
* module requires it. The list moved from `cors`, and `CORS_ALLOWED_ORIGINS`
|
|
134
|
-
* with it.
|
|
135
|
-
*/
|
|
136
|
-
export const httpModule = defineModule({
|
|
137
|
-
name: "http",
|
|
138
|
-
section: {
|
|
139
|
-
schema: httpSectionSchema,
|
|
140
|
-
reference: templateReference(),
|
|
141
|
-
relocatedFrom: { cors: "cors" },
|
|
142
|
-
renamedVariables: { CORS_ALLOWED_ORIGINS: "cors.allowedOrigins" },
|
|
143
|
-
},
|
|
144
|
-
provides: {
|
|
145
|
-
httpSettings: ({ section }) =>
|
|
146
|
-
Object.freeze({
|
|
147
|
-
trustProxy: section.trustProxy,
|
|
148
|
-
cors: Object.freeze({ allowedOrigins: Object.freeze([...section.cors.allowedOrigins]) }),
|
|
149
|
-
}),
|
|
150
|
-
httpHostSettings: ({ section }): HttpHostSettings =>
|
|
151
|
-
Object.freeze({ port: section.port, readinessTimeoutMs: section.readinessTimeoutMs }),
|
|
152
|
-
},
|
|
153
|
-
authoritative: ["httpSettings"],
|
|
154
|
-
lifecycle: { httpSettings: { eager: true }, httpHostSettings: { eager: true } },
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* KeyStore module: owns `key-store {}` and provides the JWT signing KeyStore
|
|
159
|
-
* from it through the built-in local adapter. The section moved from
|
|
160
|
-
* `oauth.jwt.signingKey`, and the variables bound to it are renamed after
|
|
161
|
-
* their paths (`KEY_STORE_*`). Other deployments wire their own KeyStore
|
|
162
|
-
* through a module of the same shape.
|
|
163
|
-
*/
|
|
164
|
-
export const keyStoreModule: Module = defineModule({
|
|
165
|
-
name: "key-store",
|
|
166
|
-
section: {
|
|
167
|
-
schema: keyStoreSectionSchema,
|
|
168
|
-
reference: templateReference(),
|
|
169
|
-
relocatedFrom: { "oauth.jwt.signingKey": "" },
|
|
170
|
-
renamedVariables: {
|
|
171
|
-
OAUTH_JWT_SIGNING_KEY_PROVIDER: "oauth.jwt.signingKey.provider",
|
|
172
|
-
OAUTH_JWT_ALGORITHM: "oauth.jwt.signingKey.local.algorithm",
|
|
173
|
-
OAUTH_JWT_KID: "oauth.jwt.signingKey.local.kid",
|
|
174
|
-
OAUTH_JWT_SECRET: "oauth.jwt.signingKey.local.secret",
|
|
175
|
-
OAUTH_JWT_PRIVATE_KEY_PATH: "oauth.jwt.signingKey.local.privateKeyPath",
|
|
176
|
-
OAUTH_JWT_PUBLIC_KEY_PATH: "oauth.jwt.signingKey.local.publicKeyPath",
|
|
177
|
-
OAUTH_JWT_PRIVATE_KEY: "oauth.jwt.signingKey.local.privateKey",
|
|
178
|
-
OAUTH_JWT_PUBLIC_KEY: "oauth.jwt.signingKey.local.publicKey",
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
provides: {
|
|
182
|
-
keyStore: async ({ section }) => {
|
|
183
|
-
const factory = createKeyStoreFactory();
|
|
184
|
-
registerBuiltinKeyStores(factory);
|
|
185
|
-
return factory.create(flattenAdapterConfig(section));
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
/** Which adapter fills each repository slot the `repositories` module provides. */
|
|
191
|
-
export interface RepositorySelection {
|
|
192
|
-
readonly client: Adapters["clientRepository"];
|
|
193
|
-
readonly user: Adapters["userRepository"];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* `block`, the settings `repositories.<repository>.<adapter>` holds. The
|
|
198
|
-
* section's schema (`repositoriesSectionSchemaFor`) has refused a selected
|
|
199
|
-
* `static` without its block, so an absent one is a composition that built
|
|
200
|
-
* the section otherwise: an `Error` naming its path.
|
|
201
|
-
*/
|
|
202
|
-
function requiredBlock<B>(block: B | undefined, repository: string, adapter: string): B {
|
|
203
|
-
if (block === undefined) {
|
|
204
|
-
throw new Error(
|
|
205
|
-
`repositories.${repository}.${adapter}.path must be set when adapters.${repository}Repository is "${adapter}"`,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
return block;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Repositories module: owns `repositories {}` and provides the client and
|
|
213
|
-
* user repositories the composition root's `adapters` select, through the
|
|
214
|
-
* built-in adapter factories: the YAML client registry, and the YAML or the
|
|
215
|
-
* Store's HTTP user repository (`@o3co/auth-provider-foundation`); core's
|
|
216
|
-
* `static`, an alias of `yaml`, reads a block of its own. The
|
|
217
|
-
* variables bound to its keys are renamed after their paths
|
|
218
|
-
* (`REPOSITORIES_*`). `codeRepository` comes from the in-process or the Redis
|
|
219
|
-
* code repository module instead.
|
|
220
|
-
*/
|
|
221
|
-
export function repositoriesModuleFor(selection: RepositorySelection): Module {
|
|
222
|
-
return defineModule({
|
|
223
|
-
name: "repositories",
|
|
224
|
-
section: {
|
|
225
|
-
schema: repositoriesSectionSchemaFor(selection),
|
|
226
|
-
reference: templateReference(),
|
|
227
|
-
renamedVariables: {
|
|
228
|
-
CLIENT_PATH: "repositories.client.yaml.path",
|
|
229
|
-
CLIENT_USER_PATH: "repositories.user.yaml.path",
|
|
230
|
-
CLIENT_USER_AUTHENTICATE_URL: "repositories.user.http.authenticateUrl",
|
|
231
|
-
CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL: "repositories.user.http.authenticateByTokenUrl",
|
|
232
|
-
CLIENT_USER_LINK_FEDERATED_IDENTITY_URL: "repositories.user.http.linkFederatedIdentityUrl",
|
|
233
|
-
CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL:
|
|
234
|
-
"repositories.user.http.findSubjectByFederatedIdentityUrl",
|
|
235
|
-
CLIENT_USER_BEARER_TOKEN: "repositories.user.http.bearerToken",
|
|
236
|
-
CLIENT_USER_TIMEOUT: "repositories.user.http.timeout",
|
|
237
|
-
CLIENT_USER_MAX_RESPONSE_BYTES: "repositories.user.http.maxResponseBytes",
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
// Forwarded into the repository factories so client/user adapters can
|
|
241
|
-
// register disposal callbacks (file-watch closers, say). Without it, a
|
|
242
|
-
// builder's `ctx.lifecycle?.register` is a no-op and those resources leak.
|
|
243
|
-
// The logger is the composition's, so what a builder logs (the yaml user
|
|
244
|
-
// repository's `user_repository_in_memory`) goes where the template's
|
|
245
|
-
// logging goes, at its level, rather than to the console.
|
|
246
|
-
optional: ["lifecycleRegistrar", "logger"] as const,
|
|
247
|
-
provides: {
|
|
248
|
-
clientRepository: async ({ section, lifecycleRegistrar }) => {
|
|
249
|
-
const { clientFactory } = createRepositoryFactories({ lifecycle: lifecycleRegistrar });
|
|
250
|
-
const file = requiredBlock(section.client[selection.client], "client", selection.client);
|
|
251
|
-
return clientFactory.create({
|
|
252
|
-
type: selection.client,
|
|
253
|
-
path: path.resolve(process.cwd(), file.path),
|
|
254
|
-
});
|
|
255
|
-
},
|
|
256
|
-
userRepository: async ({ section, lifecycleRegistrar, logger }) => {
|
|
257
|
-
const { userFactory } = createRepositoryFactories({
|
|
258
|
-
lifecycle: lifecycleRegistrar,
|
|
259
|
-
logger,
|
|
260
|
-
});
|
|
261
|
-
registerBuiltinAdapters({ userFactory });
|
|
262
|
-
return userFactory.create(
|
|
263
|
-
flattenAdapterConfig({
|
|
264
|
-
type: selection.user,
|
|
265
|
-
[selection.user]: section.user[selection.user],
|
|
266
|
-
}),
|
|
267
|
-
);
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* In-memory CodeRepository module, wired by `buildModules` only when
|
|
275
|
-
* `adapters.codeRepository = "memory"`; the Redis choice swaps in
|
|
276
|
-
* `redisCodeRepositoryModule` from `@o3co/auth-provider-redis` (mutually
|
|
277
|
-
* exclusive: both provide the `codeRepository` slot). It reads its own
|
|
278
|
-
* section, which moved from `repositories.code.memory`; the
|
|
279
|
-
* `repositories.code.redis` block is removed.
|
|
280
|
-
*/
|
|
281
|
-
export const inMemoryCodeRepositoryModule: Module = defineModule({
|
|
282
|
-
name: "standalone-in-memory-code-repository",
|
|
283
|
-
section: {
|
|
284
|
-
schema: inMemoryCodeRepositorySectionSchema,
|
|
285
|
-
reference: templateReference(),
|
|
286
|
-
relocatedFrom: { "repositories.code.memory": "", "repositories.code.redis": null },
|
|
287
|
-
renamedVariables: {
|
|
288
|
-
CLIENT_CODE_DEFAULT_EXPIRES_IN: "repositories.code.memory.defaultExpiresIn",
|
|
289
|
-
CLIENT_CODE_ENDPOINT_URI: "repositories.code.redis.endpointUri",
|
|
290
|
-
CLIENT_CODE_PASSWORD: "repositories.code.redis.password",
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
// The replica-safety guard reads this off the manifest, not by module name.
|
|
294
|
-
replicaSafety: {
|
|
295
|
-
unsafe: true,
|
|
296
|
-
reason:
|
|
297
|
-
"authorization codes fork per replica — a code issued by the replica that served /authorize is unknown to the replica that receives the token request, so the exchange fails with invalid_grant everywhere but one replica, and a code redeemed on one replica can be redeemed again on another",
|
|
298
|
-
},
|
|
299
|
-
optional: ["lifecycleRegistrar"] as const,
|
|
300
|
-
provides: {
|
|
301
|
-
codeRepository: async ({ section, lifecycleRegistrar }) => {
|
|
302
|
-
const { codeFactory } = createRepositoryFactories({ lifecycle: lifecycleRegistrar });
|
|
303
|
-
return codeFactory.create({ type: "memory", defaultExpiresIn: section.defaultExpiresIn });
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* In-memory user-session stores module: the user-session store, the session
|
|
310
|
-
* lifecycle store and the subject-level revocation pair. Wired by
|
|
311
|
-
* `buildModules` only when `adapters.userSessionStores = "memory"`; the Redis
|
|
312
|
-
* branch swaps in `redisSessionStoresModule` from `@o3co/auth-provider-redis`.
|
|
313
|
-
*/
|
|
314
|
-
export const inMemorySessionStoresModule: Module = defineModule({
|
|
315
|
-
name: "standalone-in-memory-session-stores",
|
|
316
|
-
replicaSafety: {
|
|
317
|
-
unsafe: true,
|
|
318
|
-
reason:
|
|
319
|
-
"user sessions, the session lifecycle record and the subject-level revocation pair fork per replica — back-channel logout reaches only the replica that received it, so a logged-out session stays valid on the others, a close begun on one replica fences nothing on another, and a credential change enumerates and watermarks only the replica that handled it",
|
|
320
|
-
},
|
|
321
|
-
provides: {
|
|
322
|
-
userSessionStore: () => createInMemoryUserSessionStore(),
|
|
323
|
-
// The subject-level revocation slots. Without them `subjectRevocation`
|
|
324
|
-
// is undefined: `verifyJwt` skips the watermark, the refresh gate is
|
|
325
|
-
// inert and `revokeAllForSubject` reports `unavailable`, all silently.
|
|
326
|
-
// The boot guard refuses that state; the scaffold wires them rather than
|
|
327
|
-
// declaring the capability absent. Single-process only, like every
|
|
328
|
-
// other store on this branch.
|
|
329
|
-
subjectSessionIndex: () => createInMemorySubjectSessionIndex(),
|
|
330
|
-
subjectRevocation: () => createInMemorySubjectRevocation(),
|
|
331
|
-
// The session lifecycle's record, as the Redis branch provides it; read
|
|
332
|
-
// only where core's session lifecycle module is installed.
|
|
333
|
-
sessionLifecycleStore: () => createInMemorySessionLifecycleStore(),
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* In-memory federation token store module, wired by `buildModules` only when
|
|
339
|
-
* `adapters.federationTokenStore = "memory"` (the default). The redis branch
|
|
340
|
-
* swaps in `redisFederationTokenStoreModule` off the shared ioredis socket
|
|
341
|
-
* (mutually exclusive: both provide the `federationTokenStore` slot). One
|
|
342
|
-
* module per adapter, so the replica-safety guard can tell them apart and the
|
|
343
|
-
* Redis one can require its client. The slot is always wired, independent of
|
|
344
|
-
* the `adapters.userSessionStores` selection.
|
|
345
|
-
*
|
|
346
|
-
* The memory adapter comes through core's factory: that is where its
|
|
347
|
-
* "dev/test only" boot warning lives, said on the composition's `logger`.
|
|
348
|
-
*/
|
|
349
|
-
export const inMemoryFederationTokenStoreModule: Module = defineModule({
|
|
350
|
-
name: "standalone-in-memory-federation-token-store",
|
|
351
|
-
replicaSafety: {
|
|
352
|
-
unsafe: true,
|
|
353
|
-
reason:
|
|
354
|
-
"upstream federation tokens fork per replica — a token stored by the replica that completed the federation callback is missing on the others, so a session cannot refresh its upstream token from another replica, and logout removes only the tokens the replica it lands on can see",
|
|
355
|
-
},
|
|
356
|
-
optional: ["logger"] as const,
|
|
357
|
-
provides: {
|
|
358
|
-
federationTokenStore: async ({ logger }) => {
|
|
359
|
-
const factory = createFederationTokenStoreFactory();
|
|
360
|
-
registerBuiltinFederationTokenStores(factory, logger);
|
|
361
|
-
return factory.create({ type: "memory" });
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Audit-sink module: owns `audit-sink {}` and fills the `auditSink` slot every
|
|
368
|
-
* route that emits a security event reads, with the sink the composition
|
|
369
|
-
* root's `adapters.auditSink` names: the template's `"logger"` (its default)
|
|
370
|
-
* or one of core's built-ins, with that sink's options from
|
|
371
|
-
* `audit-sink.<name>`. The slot is `optional` on `oauthEndpointsModule`,
|
|
372
|
-
* `sessionModule` and `webauthnModule`, and `emitAuditEvent` is a no-op when
|
|
373
|
-
* it is empty, so this module is always in the manifest; a name no builder is
|
|
374
|
-
* registered under refuses boot. The sink's options moved from `audit.sink`.
|
|
375
|
-
*
|
|
376
|
-
* `createAuditSinkFactory()` takes no `BuilderContext`, so a sink builder
|
|
377
|
-
* receives `{}` and the `ctx.lifecycle?.register(…)` /
|
|
378
|
-
* `ctx.readiness?.register(…)` calls CONTRIBUTING.md requires of a builder
|
|
379
|
-
* opening a connection are silent no-ops: a sink holding a socket must own
|
|
380
|
-
* its cleanup another way.
|
|
381
|
-
*/
|
|
382
|
-
export function auditSinkModuleFor(sink: string): Module {
|
|
383
|
-
return defineModule({
|
|
384
|
-
name: "audit-sink",
|
|
385
|
-
section: {
|
|
386
|
-
schema: auditSinkSectionSchema,
|
|
387
|
-
reference: templateReference(),
|
|
388
|
-
relocatedFrom: {
|
|
389
|
-
"audit.sink": { to: "", environmentVariable: null },
|
|
390
|
-
"audit.sink.type": null,
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
provides: {
|
|
394
|
-
auditSink: async ({ section }) => {
|
|
395
|
-
const factory = createAuditSinkFactory();
|
|
396
|
-
registerBuiltinAuditSinks(factory);
|
|
397
|
-
factory.register("logger", () => createLoggerAuditSink(createAuditLogger()));
|
|
398
|
-
return factory.create(flattenAdapterConfig({ type: sink, [sink]: section?.[sink] }));
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Shared ioredis clients module: opens ONE long-lived ioredis connection per
|
|
406
|
-
* replica and derives every per-purpose client from it (`makeIoredisClients`,
|
|
407
|
-
* plus the two federation-grant store clients from their own factories),
|
|
408
|
-
* rather than a socket per Redis-backed module, to keep connection-pool
|
|
409
|
-
* pressure off the upstream Redis. Every store module
|
|
410
|
-
* `@o3co/auth-provider-redis` ships finds its client slot here, including
|
|
411
|
-
* those this template does not select; `all-modules-composition.multi.test.mts`
|
|
412
|
-
* pins that.
|
|
413
|
-
*
|
|
414
|
-
* The connection's URL and password are the module's own section,
|
|
415
|
-
* `redis-clients`, which moved from `refreshTokenFamilyStore.redis` with its
|
|
416
|
-
* variables (`REDIS_CLIENTS_*`). Per-store Redis instances belong in a
|
|
417
|
-
* custom composition root. An empty URL throws rather than falling back to
|
|
418
|
-
* localhost. `assumeNoEviction` is handed to every client, for the Redis
|
|
419
|
-
* stores' eviction gate. `io.quit()` is registered once with `lifecycleRegistrar`, so
|
|
420
|
-
* `handle.dispose()` closes the connection.
|
|
421
|
-
*
|
|
422
|
-
* `buildModules` adds this module only when some composed module needs a
|
|
423
|
-
* Redis client (`usingRedisAnywhere`), so memory-only compositions open no
|
|
424
|
-
* socket.
|
|
425
|
-
*/
|
|
426
|
-
export const standaloneRedisClientsModule: Module = defineModule({
|
|
427
|
-
name: "redis-clients",
|
|
428
|
-
section: {
|
|
429
|
-
schema: redisClientsSectionSchema,
|
|
430
|
-
reference: templateReference(),
|
|
431
|
-
relocatedFrom: { "refreshTokenFamilyStore.redis": "" },
|
|
432
|
-
renamedVariables: {
|
|
433
|
-
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "refreshTokenFamilyStore.redis.url",
|
|
434
|
-
REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD: "refreshTokenFamilyStore.redis.password",
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
optional: ["lifecycleRegistrar", "readinessRegistrar", "logger"] as const,
|
|
438
|
-
provides: {
|
|
439
|
-
refreshTokenFamilyClient: async ({
|
|
440
|
-
section,
|
|
441
|
-
lifecycleRegistrar,
|
|
442
|
-
readinessRegistrar,
|
|
443
|
-
logger,
|
|
444
|
-
}) => {
|
|
445
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
446
|
-
.refreshTokenFamilyClient;
|
|
447
|
-
},
|
|
448
|
-
userSessionStoreClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
449
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
450
|
-
.userSessionStoreClient;
|
|
451
|
-
},
|
|
452
|
-
// The grant store and the intent store, consumed by the Redis grant
|
|
453
|
-
// modules when their switches say "redis". Provided whenever this module
|
|
454
|
-
// is installed, as every slot here is: a slot is cheap, the socket is
|
|
455
|
-
// the cost.
|
|
456
|
-
federationGrantStoreClient: async ({
|
|
457
|
-
section,
|
|
458
|
-
lifecycleRegistrar,
|
|
459
|
-
readinessRegistrar,
|
|
460
|
-
logger,
|
|
461
|
-
}) => {
|
|
462
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
463
|
-
.federationGrantStoreClient;
|
|
464
|
-
},
|
|
465
|
-
federationGrantIntentStoreClient: async ({
|
|
466
|
-
section,
|
|
467
|
-
lifecycleRegistrar,
|
|
468
|
-
readinessRegistrar,
|
|
469
|
-
logger,
|
|
470
|
-
}) => {
|
|
471
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
472
|
-
.federationGrantIntentStoreClient;
|
|
473
|
-
},
|
|
474
|
-
// The replay seen-set behind private_key_jwt client assertions.
|
|
475
|
-
replaySeenSetClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
476
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
477
|
-
.replaySeenSetClient;
|
|
478
|
-
},
|
|
479
|
-
rateLimiterClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
480
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
481
|
-
.rateLimiterClient;
|
|
482
|
-
},
|
|
483
|
-
// `redisAttemptCounterModule` consumes this slot where a deployment installs it.
|
|
484
|
-
attemptCounterClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
485
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
486
|
-
.attemptCounterClient;
|
|
487
|
-
},
|
|
488
|
-
// `redisCodeRepositoryModule` consumes this slot when
|
|
489
|
-
// `adapters.codeRepository = "redis"`.
|
|
490
|
-
codeRepositoryClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
491
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
492
|
-
.codeRepositoryClient;
|
|
493
|
-
},
|
|
494
|
-
// A denylist is only worth having if every replica reads the same one,
|
|
495
|
-
// so it belongs on the connection the rest of the shared state uses
|
|
496
|
-
// rather than a second one.
|
|
497
|
-
accessTokenDenylistClient: async ({
|
|
498
|
-
section,
|
|
499
|
-
lifecycleRegistrar,
|
|
500
|
-
readinessRegistrar,
|
|
501
|
-
logger,
|
|
502
|
-
}) => {
|
|
503
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
504
|
-
.accessTokenDenylistClient;
|
|
505
|
-
},
|
|
506
|
-
// The subject-level revocation pair, both required by
|
|
507
|
-
// `redisSessionStoresModule`: the index enumerates what a credential
|
|
508
|
-
// change cascades over, the watermark refuses what the cascade missed.
|
|
509
|
-
// Nothing in the standalone consumes them directly; they exist so that
|
|
510
|
-
// module can be selected.
|
|
511
|
-
subjectSessionIndexClient: async ({
|
|
512
|
-
section,
|
|
513
|
-
lifecycleRegistrar,
|
|
514
|
-
readinessRegistrar,
|
|
515
|
-
logger,
|
|
516
|
-
}) => {
|
|
517
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
518
|
-
.subjectSessionIndexClient;
|
|
519
|
-
},
|
|
520
|
-
subjectRevocationClient: async ({
|
|
521
|
-
section,
|
|
522
|
-
lifecycleRegistrar,
|
|
523
|
-
readinessRegistrar,
|
|
524
|
-
logger,
|
|
525
|
-
}) => {
|
|
526
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
527
|
-
.subjectRevocationClient;
|
|
528
|
-
},
|
|
529
|
-
// Required by `redisSessionStoresModule`, which builds the session
|
|
530
|
-
// lifecycle store from it once a module reads that store.
|
|
531
|
-
sessionLifecycleStoreClient: async ({
|
|
532
|
-
section,
|
|
533
|
-
lifecycleRegistrar,
|
|
534
|
-
readinessRegistrar,
|
|
535
|
-
logger,
|
|
536
|
-
}) => {
|
|
537
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
538
|
-
.sessionLifecycleStoreClient;
|
|
539
|
-
},
|
|
540
|
-
// Required by `redisDeviceCodeStoreModule`. This template does not mount
|
|
541
|
-
// the device grant; the slot is provided anyway, so a deployment that
|
|
542
|
-
// adds `deviceAuthorizationGrantModule` with the Redis store is not refused at boot
|
|
543
|
-
// (`missing-required-component`) for a client slot nothing provided.
|
|
544
|
-
deviceCodeStoreClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
545
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
546
|
-
.deviceCodeStoreClient;
|
|
547
|
-
},
|
|
548
|
-
// The WebAuthn challenge store's client, for the device-code slot's
|
|
549
|
-
// reason: this template does not mount WebAuthn, and a deployment that
|
|
550
|
-
// adds `webauthnModule` with `redisChallengeStoreModule` would otherwise
|
|
551
|
-
// be refused at boot for a `challengeStoreClient` nothing provided.
|
|
552
|
-
challengeStoreClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
553
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
554
|
-
.challengeStoreClient;
|
|
555
|
-
},
|
|
556
|
-
// The consent stores' clients. `redisConsentStoreModule` requires both
|
|
557
|
-
// (it provides the consent records and the parked requests together),
|
|
558
|
-
// and `buildModules` selects it under `adapters.consentStore = "redis"`.
|
|
559
|
-
consentStoreClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
560
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
561
|
-
.consentStoreClient;
|
|
562
|
-
},
|
|
563
|
-
pendingConsentStoreClient: async ({
|
|
564
|
-
section,
|
|
565
|
-
lifecycleRegistrar,
|
|
566
|
-
readinessRegistrar,
|
|
567
|
-
logger,
|
|
568
|
-
}) => {
|
|
569
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
570
|
-
.pendingConsentStoreClient;
|
|
571
|
-
},
|
|
572
|
-
// Required by `redisFederationTokenStoreModule`.
|
|
573
|
-
federationTokenStoreClient: async ({
|
|
574
|
-
section,
|
|
575
|
-
lifecycleRegistrar,
|
|
576
|
-
readinessRegistrar,
|
|
577
|
-
logger,
|
|
578
|
-
}) => {
|
|
579
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
580
|
-
.federationTokenStoreClient;
|
|
581
|
-
},
|
|
582
|
-
// The two MFA stores' clients, required by `redisMfaFactorStoreModule`
|
|
583
|
-
// and `redisMfaTransactionStoreModule`, which `buildModules` selects
|
|
584
|
-
// with MFA on and `adapters.mfaFactorStore` / `.mfaTransactionStore`
|
|
585
|
-
// = "redis". Each module checks the server's eviction policy and
|
|
586
|
-
// persistence when it boots.
|
|
587
|
-
mfaFactorStoreClient: async ({ section, lifecycleRegistrar, readinessRegistrar, logger }) => {
|
|
588
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
589
|
-
.mfaFactorStoreClient;
|
|
590
|
-
},
|
|
591
|
-
mfaTransactionStoreClient: async ({
|
|
592
|
-
section,
|
|
593
|
-
lifecycleRegistrar,
|
|
594
|
-
readinessRegistrar,
|
|
595
|
-
logger,
|
|
596
|
-
}) => {
|
|
597
|
-
return getOrCreateClients(section, lifecycleRegistrar, readinessRegistrar, logger)
|
|
598
|
-
.mfaTransactionStoreClient;
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Failure-timing options for the one shared ioredis socket. Each value's
|
|
605
|
-
* reason, and the trade the offline queue buys, is in
|
|
606
|
-
* `docs/operator-runbook.md`, "Failure timing on the shared socket". In
|
|
607
|
-
* short: on the driver's defaults a partition produces *waiting*, not errors,
|
|
608
|
-
* so the Redis limiter's `redis-rate-limiter.failMode = "closed"` policy, which the
|
|
609
|
-
* guard (`createRateLimitGuard`) applies only when `limiter.check()` rejects,
|
|
610
|
-
* never engages.
|
|
611
|
-
* `commandTimeout` bounds every command, queued or on the wire, zombie socket
|
|
612
|
-
* included; `maxRetriesPerRequest` bounds the offline queue's depth; the
|
|
613
|
-
* queue stays on because the option is per connection and this socket also
|
|
614
|
-
* carries sessions, codes and refresh rotation.
|
|
615
|
-
*
|
|
616
|
-
* One second is ample: the socket's commands are O(1) primitives and small
|
|
617
|
-
* paged scans (`SET`/`GET`/`DEL`/`EXISTS`/`PTTL`/`INCR`/`EVAL`/`HSCAN`/
|
|
618
|
-
* `SSCAN`/`ZADD`/…), none blocking. `lazyConnect: false` is the driver
|
|
619
|
-
* default, declared so the boot-time-connect contract survives a future flip.
|
|
620
|
-
*/
|
|
621
|
-
const SHARED_REDIS_TIMEOUTS = {
|
|
622
|
-
commandTimeout: 1_000,
|
|
623
|
-
connectTimeout: 5_000,
|
|
624
|
-
maxRetriesPerRequest: 3,
|
|
625
|
-
enableOfflineQueue: true,
|
|
626
|
-
lazyConnect: false,
|
|
627
|
-
} as const;
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Every per-purpose client off the one socket: the general bundle, plus the
|
|
631
|
-
* two the federation-grant stores need. Those two have their own factories in
|
|
632
|
-
* the Redis package, so a Cluster deployment can give the grants a
|
|
633
|
-
* connection of their own; this template shares the socket, as it does for
|
|
634
|
-
* every other store.
|
|
635
|
-
*/
|
|
636
|
-
type StandaloneRedisClients = ReturnType<typeof makeIoredisClients> & {
|
|
637
|
-
readonly federationGrantStoreClient: ReturnType<typeof makeIoredisFederationGrantStoreClient>;
|
|
638
|
-
readonly federationGrantIntentStoreClient: ReturnType<
|
|
639
|
-
typeof makeIoredisFederationGrantIntentStoreClient
|
|
640
|
-
>;
|
|
641
|
-
};
|
|
642
|
-
// So every `provides.*` factory reuses one `Redis` instance (and its clients)
|
|
643
|
-
// within a createApp() call, rather than opening a socket per consumed slot.
|
|
644
|
-
// Keyed on the `lifecycleRegistrar` identity, fresh per boot: keying on
|
|
645
|
-
// `config` would share connections across boots that reuse a config object,
|
|
646
|
-
// and only the first boot's registrar would dispose them. Without a
|
|
647
|
-
// registrar (tests that seed none), each call creates a fresh client.
|
|
648
|
-
const clientsCache = new WeakMap<LifecycleRegistrar, StandaloneRedisClients>();
|
|
649
|
-
function getOrCreateClients(
|
|
650
|
-
section: {
|
|
651
|
-
readonly url: string;
|
|
652
|
-
readonly password?: string | undefined;
|
|
653
|
-
readonly assumeNoEviction: boolean;
|
|
654
|
-
},
|
|
655
|
-
lifecycleRegistrar: LifecycleRegistrar | undefined,
|
|
656
|
-
readinessRegistrar?: ReadinessRegistrar,
|
|
657
|
-
injectedLogger?: Logger,
|
|
658
|
-
): StandaloneRedisClients {
|
|
659
|
-
const logger = injectedLogger ?? consoleLogger;
|
|
660
|
-
const cached = lifecycleRegistrar ? clientsCache.get(lifecycleRegistrar) : undefined;
|
|
661
|
-
if (cached) return cached;
|
|
662
|
-
|
|
663
|
-
if (section.url.length === 0) {
|
|
664
|
-
throw new Error(
|
|
665
|
-
"standaloneRedisClientsModule: `redis-clients.url` is required when any " +
|
|
666
|
-
"Redis-backed adapter is selected. Set REDIS_CLIENTS_URL, or the " +
|
|
667
|
-
"key in a configuration layer, to a non-empty URL. Multi-replica deployments require " +
|
|
668
|
-
"a shared Redis 7.2+ instance.",
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const io = new Redis(section.url, { password: section.password, ...SHARED_REDIS_TIMEOUTS });
|
|
673
|
-
|
|
674
|
-
// Attach an error handler so unhandled "error" events do not crash the
|
|
675
|
-
// process. Initial connection failures surface here; downstream adapter
|
|
676
|
-
// operations then fail visibly. It logs the projection: ioredis puts the
|
|
677
|
-
// command a reply answered on the error, and for a refused handshake that
|
|
678
|
-
// is `AUTH` with the configured password.
|
|
679
|
-
io.on("error", (err: unknown) => {
|
|
680
|
-
logger.error({ err: loggableError(err) }, "standalone_redis_clients_error");
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
lifecycleRegistrar?.register(async () => {
|
|
684
|
-
await io.quit();
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
// One probe for the one socket, not one per consumed slot: the cache
|
|
688
|
-
// above means this runs once per boot, on the call that constructs the
|
|
689
|
-
// client.
|
|
690
|
-
readinessRegistrar?.register({
|
|
691
|
-
name: "redis",
|
|
692
|
-
check: () => io.ping(),
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// The wrapper opens its own connections for refresh rotation
|
|
696
|
-
// (`refreshTokenFamilyClient.duplicate()`), which inherit no listeners from
|
|
697
|
-
// `io`; passing the logger lets those report through the same channel.
|
|
698
|
-
const clients: StandaloneRedisClients = {
|
|
699
|
-
...makeIoredisClients(io, { logger, assumeNoEviction: section.assumeNoEviction }),
|
|
700
|
-
federationGrantStoreClient: makeIoredisFederationGrantStoreClient(io),
|
|
701
|
-
federationGrantIntentStoreClient: makeIoredisFederationGrantIntentStoreClient(io),
|
|
702
|
-
};
|
|
703
|
-
if (lifecycleRegistrar) clientsCache.set(lifecycleRegistrar, clients);
|
|
704
|
-
return clients;
|
|
705
|
-
}
|