@o3co/create-auth-provider 0.4.1 → 0.5.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/LICENSE +201 -0
- package/dist/__tests__/index.test.mjs +59 -4
- package/dist/__tests__/published-package.test.d.mts +2 -0
- package/dist/__tests__/published-package.test.d.mts.map +1 -0
- package/dist/__tests__/published-package.test.mjs +73 -0
- package/dist/cli.mjs +0 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -6
- package/dist/internal/template-filter.d.mts +17 -0
- package/dist/internal/template-filter.d.mts.map +1 -0
- package/dist/internal/template-filter.mjs +45 -0
- package/package.json +35 -27
- package/templates/standalone/README.ja.md +30 -10
- package/templates/standalone/README.md +30 -10
- package/templates/standalone/config/application.conf +7 -4
- package/templates/standalone/package.json +2 -1
- package/templates/standalone/src/__tests__/smoke.test.mts +86 -76
- package/templates/standalone/src/app.mts +38 -121
- package/templates/standalone/src/buildModules.mts +78 -0
- package/templates/standalone/src/modules.mts +184 -0
- package/templates/standalone/tests/index.test.js +2 -2
- package/templates/standalone/tests/package.json +0 -1
- package/templates/standalone/tsconfig.json +12 -1
- package/templates/versions.json +8 -7
- package/templates/standalone/tests/did-grant.test.js +0 -193
|
@@ -35,12 +35,12 @@ oauth {
|
|
|
35
35
|
}
|
|
36
36
|
grants {
|
|
37
37
|
session { enabled = true, enabled = ${?OAUTH_GRANTS_SESSION_ENABLED} }
|
|
38
|
-
|
|
38
|
+
authorization_code {
|
|
39
39
|
enabled = true
|
|
40
|
-
enabled = ${?
|
|
40
|
+
enabled = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED}
|
|
41
41
|
pkce {
|
|
42
42
|
requireS256 = false
|
|
43
|
-
requireS256 = ${?
|
|
43
|
+
requireS256 = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
refresh_token { enabled = true, enabled = ${?OAUTH_GRANTS_REFRESH_TOKEN_ENABLED} }
|
|
@@ -122,7 +122,10 @@ repositories {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
endpoints {
|
|
125
|
-
login {
|
|
125
|
+
login {
|
|
126
|
+
url = "/login"
|
|
127
|
+
url = ${?ENDPOINTS_LOGIN_URL}
|
|
128
|
+
}
|
|
126
129
|
client { url = ${?ENDPOINTS_CLIENT_URL} }
|
|
127
130
|
authCallback { url = ${?ENDPOINTS_AUTH_CALLBACK_URL} }
|
|
128
131
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-standalone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@o3co/auth-provider-oauth": "workspace:*",
|
|
24
24
|
"@o3co/auth-provider-session": "workspace:*",
|
|
25
25
|
"@o3co/auth-provider-foundation": "workspace:*",
|
|
26
|
+
"@o3co/auth-provider-redis": "workspace:*",
|
|
26
27
|
"@o3co/auth.utils": "^0.0.3",
|
|
27
28
|
"@o3co/ts.hocon": "^1.1.0",
|
|
28
29
|
"connect-redis": "^9.0.0",
|
|
@@ -17,26 +17,18 @@
|
|
|
17
17
|
import {
|
|
18
18
|
type AppConfig,
|
|
19
19
|
createApp,
|
|
20
|
-
createFederationTokenStoreFactory,
|
|
21
20
|
createKeyStoreFactory,
|
|
22
|
-
|
|
21
|
+
defineModule,
|
|
23
22
|
generateToken,
|
|
24
23
|
InMemoryClientRepository,
|
|
25
24
|
InMemoryCodeRepository,
|
|
26
25
|
InMemoryUserRepository,
|
|
27
|
-
registerBuiltinFederationTokenStores,
|
|
28
26
|
registerBuiltinKeyStores,
|
|
29
|
-
registerBuiltinUserSessionStores,
|
|
30
27
|
} from "@o3co/auth-provider-core";
|
|
31
|
-
import {
|
|
32
|
-
oauthAuthorizationModule,
|
|
33
|
-
oauthModule,
|
|
34
|
-
oauthSessionModule,
|
|
35
|
-
} from "@o3co/auth-provider-oauth";
|
|
36
|
-
import { sessionModule } from "@o3co/auth-provider-session";
|
|
37
28
|
import express from "express";
|
|
38
29
|
import request from "supertest";
|
|
39
|
-
import {
|
|
30
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
31
|
+
import { buildModules } from "../buildModules.mjs";
|
|
40
32
|
|
|
41
33
|
const config: AppConfig = {
|
|
42
34
|
http: { port: 0, trustProxy: false },
|
|
@@ -76,109 +68,127 @@ const config: AppConfig = {
|
|
|
76
68
|
code: { type: "memory", defaultExpiresIn: 600 },
|
|
77
69
|
},
|
|
78
70
|
endpoints: {
|
|
79
|
-
login: { url:
|
|
71
|
+
login: { url: "/login" },
|
|
80
72
|
client: { url: undefined },
|
|
81
73
|
authCallback: { url: undefined },
|
|
82
74
|
},
|
|
83
75
|
cors: { allowedOrigins: [] },
|
|
84
76
|
};
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Test-only repository module — bypasses the file-system-backed repositories
|
|
80
|
+
* that `repositoriesModule` provides in production. Smoke tests use in-memory
|
|
81
|
+
* empty repositories to verify the boot pipeline shape, not the data layer.
|
|
82
|
+
*/
|
|
83
|
+
const testRepositoriesModule = defineModule({
|
|
84
|
+
name: "test:repositories",
|
|
85
|
+
provides: {
|
|
86
|
+
clientRepository: () => new InMemoryClientRepository(new Map()),
|
|
87
|
+
userRepository: () => new InMemoryUserRepository(new Map()),
|
|
88
|
+
codeRepository: () => new InMemoryCodeRepository(),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const testKeyStoreModule = defineModule({
|
|
93
|
+
name: "test:key-store",
|
|
94
|
+
requires: ["config"] as const,
|
|
95
|
+
provides: {
|
|
96
|
+
keyStore: async ({ config: c }) => {
|
|
97
|
+
const factory = createKeyStoreFactory();
|
|
98
|
+
registerBuiltinKeyStores(factory);
|
|
99
|
+
return factory.create({
|
|
100
|
+
type: "local",
|
|
101
|
+
...((c as AppConfig).oauth.jwt.signingKey.local ?? {}),
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const keyStore = await keyStoreFactory.create({
|
|
98
|
-
type: "local",
|
|
99
|
-
...(config.oauth.jwt.signingKey.local ?? {}),
|
|
100
|
-
});
|
|
107
|
+
describe("standalone smoke test", () => {
|
|
108
|
+
let handleRef: Awaited<ReturnType<typeof buildApp>>["handle"] | undefined;
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const { init, router, grantRegistry } = createApp({
|
|
111
|
-
express,
|
|
112
|
-
config,
|
|
113
|
-
keyStore,
|
|
114
|
-
userSessionStore,
|
|
115
|
-
federationTokenStore,
|
|
116
|
-
modules: [
|
|
117
|
-
oauthModule({ clientRepository, codeRepository, express }),
|
|
118
|
-
sessionModule({ userRepository, express }),
|
|
119
|
-
oauthSessionModule({ clientRepository }),
|
|
120
|
-
oauthAuthorizationModule({ codeRepository, clientRepository }),
|
|
121
|
-
],
|
|
110
|
+
async function buildApp() {
|
|
111
|
+
const handle = await createApp({
|
|
112
|
+
modules: buildModules(config, {
|
|
113
|
+
keyStoreModule: testKeyStoreModule,
|
|
114
|
+
repositoriesModule: testRepositoriesModule,
|
|
115
|
+
}),
|
|
116
|
+
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
122
117
|
});
|
|
123
118
|
|
|
124
|
-
await init();
|
|
125
|
-
|
|
126
119
|
const app = express();
|
|
127
|
-
app.
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
app.get("/_healthcheck", (_req, res) => {
|
|
121
|
+
res.status(200).json({ status: "ok" });
|
|
122
|
+
});
|
|
123
|
+
app.use(handle.router);
|
|
124
|
+
return { app, handle };
|
|
130
125
|
}
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
await
|
|
127
|
+
afterEach(async () => {
|
|
128
|
+
await handleRef?.dispose();
|
|
129
|
+
handleRef = undefined;
|
|
134
130
|
});
|
|
135
131
|
|
|
136
132
|
it("GET /_healthcheck returns 200", async () => {
|
|
137
|
-
const { app,
|
|
138
|
-
|
|
139
|
-
_appRef = app;
|
|
140
|
-
|
|
133
|
+
const { app, handle } = await buildApp();
|
|
134
|
+
handleRef = handle;
|
|
141
135
|
const res = await request(app).get("/_healthcheck");
|
|
142
136
|
expect(res.status).toBe(200);
|
|
143
137
|
});
|
|
144
138
|
|
|
145
139
|
it("POST /oauth/token with unsupported grant_type returns 400", async () => {
|
|
146
|
-
const { app,
|
|
147
|
-
|
|
148
|
-
_appRef = app;
|
|
149
|
-
|
|
140
|
+
const { app, handle } = await buildApp();
|
|
141
|
+
handleRef = handle;
|
|
150
142
|
const res = await request(app)
|
|
151
143
|
.post("/oauth/token")
|
|
152
144
|
.type("form")
|
|
153
145
|
.send({ grant_type: "unsupported" });
|
|
154
|
-
|
|
155
146
|
expect(res.status).toBe(400);
|
|
156
147
|
});
|
|
157
148
|
|
|
158
|
-
it("POST /oauth/token
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Instead, test that 400 responses do NOT have Cache-Control (only successes do).
|
|
162
|
-
// The Cache-Control header is set only when "tokens" in result — covered by
|
|
163
|
-
// the introspect test below which exercises the full response path.
|
|
164
|
-
const { app, grantRegistry } = await buildApp();
|
|
165
|
-
grantRegistryRef = grantRegistry;
|
|
166
|
-
_appRef = app;
|
|
167
|
-
|
|
168
|
-
// 400 responses must NOT have Cache-Control: no-store
|
|
149
|
+
it("POST /oauth/token 400 responses do NOT have Cache-Control: no-store", async () => {
|
|
150
|
+
const { app, handle } = await buildApp();
|
|
151
|
+
handleRef = handle;
|
|
169
152
|
const res = await request(app)
|
|
170
153
|
.post("/oauth/token")
|
|
171
154
|
.type("form")
|
|
172
155
|
.send({ grant_type: "unsupported" });
|
|
173
|
-
|
|
174
156
|
expect(res.status).toBe(400);
|
|
175
157
|
expect(res.headers["cache-control"]).not.toBe("no-store");
|
|
176
158
|
});
|
|
177
159
|
|
|
160
|
+
// Regression guard: a freshly scaffolded standalone app must boot under
|
|
161
|
+
// the default `federations.google.enabled = false`. The earlier shape of
|
|
162
|
+
// app.mts unconditionally listed `googleFederationModule` and the config
|
|
163
|
+
// bridge `googleFederationConfigModule`, so the bridge's
|
|
164
|
+
// `extractFederationSection` returned undefined and threw at boot. The
|
|
165
|
+
// fix moved the manifest assembly into `buildModules`, which conditionally
|
|
166
|
+
// includes the federation pair. The first sub-assertion asserts directly
|
|
167
|
+
// on the manifest list — a regression that bypasses the gate fails the
|
|
168
|
+
// `not.toContain` even if `googleFederationConfigModule` is later made
|
|
169
|
+
// tolerant of `undefined` (which would otherwise mask the gating bug).
|
|
170
|
+
// The second sub-assertion verifies the resulting handle still boots
|
|
171
|
+
// (Codex Round 1 P1 reproducibility).
|
|
172
|
+
it("boots when google federation is disabled (default scaffold config)", async () => {
|
|
173
|
+
const modules = buildModules(config, {
|
|
174
|
+
keyStoreModule: testKeyStoreModule,
|
|
175
|
+
repositoriesModule: testRepositoriesModule,
|
|
176
|
+
});
|
|
177
|
+
const moduleNames = modules.map((m) => m.name);
|
|
178
|
+
expect(moduleNames).not.toContain("federation:google");
|
|
179
|
+
expect(moduleNames).not.toContain("standalone:google-federation-config");
|
|
180
|
+
|
|
181
|
+
const handle = await createApp({
|
|
182
|
+
modules,
|
|
183
|
+
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
184
|
+
});
|
|
185
|
+
handleRef = handle;
|
|
186
|
+
expect(handle).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
178
189
|
it("POST /oauth/introspect returns iat in active token response", async () => {
|
|
179
|
-
const { app,
|
|
180
|
-
|
|
181
|
-
_appRef = app;
|
|
190
|
+
const { app, handle } = await buildApp();
|
|
191
|
+
handleRef = handle;
|
|
182
192
|
|
|
183
193
|
const ksf = createKeyStoreFactory();
|
|
184
194
|
registerBuiltinKeyStores(ksf);
|
|
@@ -13,34 +13,12 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import path from "node:path";
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
18
17
|
import { gracefulShutdown } from "@o3co/auth.utils";
|
|
18
|
+
import { type AppConfig, AppConfigSchema, createApp } from "@o3co/auth-provider-core";
|
|
19
19
|
import {
|
|
20
|
-
type AppConfig,
|
|
21
|
-
AppConfigSchema,
|
|
22
|
-
createApp,
|
|
23
|
-
createDefaultFactories,
|
|
24
|
-
createFederationTokenStoreFactory,
|
|
25
|
-
createKeyStoreFactory,
|
|
26
|
-
createUserSessionStoreFactory,
|
|
27
|
-
registerBuiltinFederationTokenStores,
|
|
28
|
-
registerBuiltinKeyStores,
|
|
29
|
-
registerBuiltinUserSessionStores,
|
|
30
|
-
} from "@o3co/auth-provider-core";
|
|
31
|
-
import { registerGoogleFederation } from "@o3co/auth-provider-federation-google";
|
|
32
|
-
// import { registerGithubFederation } from "@o3co/auth-provider-federation-github";
|
|
33
|
-
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
34
|
-
import {
|
|
35
|
-
oauthAuthorizationModule,
|
|
36
|
-
oauthModule,
|
|
37
|
-
oauthSessionModule,
|
|
38
|
-
} from "@o3co/auth-provider-oauth";
|
|
39
|
-
import {
|
|
40
|
-
createFederationProviderFactory,
|
|
41
20
|
createSessionStoreFactory,
|
|
42
21
|
registerBuiltinSessionStores,
|
|
43
|
-
sessionModule,
|
|
44
22
|
} from "@o3co/auth-provider-session";
|
|
45
23
|
import { parseFile } from "@o3co/ts.hocon";
|
|
46
24
|
import { validate } from "@o3co/ts.hocon/zod";
|
|
@@ -49,12 +27,11 @@ import session from "express-session";
|
|
|
49
27
|
import helmet from "helmet";
|
|
50
28
|
|
|
51
29
|
import logger from "#/logger.mjs";
|
|
30
|
+
import { buildModules } from "./buildModules.mjs";
|
|
52
31
|
import { resolveConfigPaths } from "./configPath.mjs";
|
|
53
32
|
|
|
54
33
|
// Step 1: Load and validate application config (HOCON → Zod schema).
|
|
55
|
-
//
|
|
56
|
-
// ENV = CONFIG_ENV || NODE_ENV || "development". A missing {ENV}.conf is a
|
|
57
|
-
// boot-time error (fail-fast on typos or unconfigured environments).
|
|
34
|
+
// ENV = CONFIG_ENV || NODE_ENV || "development"; missing {ENV}.conf is fatal.
|
|
58
35
|
const env = process.env.CONFIG_ENV || process.env.NODE_ENV || "development";
|
|
59
36
|
const configDir = new URL("../config/", import.meta.url);
|
|
60
37
|
const configDirPath = fileURLToPath(configDir);
|
|
@@ -64,47 +41,9 @@ const config: AppConfig = validate(
|
|
|
64
41
|
AppConfigSchema,
|
|
65
42
|
);
|
|
66
43
|
|
|
67
|
-
const flattenAdapterConfig = (
|
|
68
|
-
section: ({ type: string } | { provider: string }) & Record<string, unknown>,
|
|
69
|
-
): { type: string } & Record<string, unknown> => {
|
|
70
|
-
const selector =
|
|
71
|
-
(section as { type?: string; provider?: string }).type ??
|
|
72
|
-
(section as { provider?: string }).provider;
|
|
73
|
-
if (typeof selector !== "string") {
|
|
74
|
-
throw new TypeError("flattenAdapterConfig: section requires 'type' or 'provider' string");
|
|
75
|
-
}
|
|
76
|
-
const sub = section[selector];
|
|
77
|
-
const flattenedSub =
|
|
78
|
-
typeof sub === "object" && sub !== null && !Array.isArray(sub)
|
|
79
|
-
? (sub as Record<string, unknown>)
|
|
80
|
-
: {};
|
|
81
|
-
return { type: selector, ...flattenedSub };
|
|
82
|
-
};
|
|
83
|
-
|
|
84
44
|
await (async (): Promise<void> => {
|
|
85
|
-
// Step 2:
|
|
86
|
-
const appDir = path.dirname(fileURLToPath(import.meta.url));
|
|
87
|
-
const { clientFactory, userFactory, codeFactory } = createDefaultFactories();
|
|
88
|
-
registerBuiltinAdapters({ userFactory, codeFactory, pathResolver: import.meta.resolve });
|
|
89
|
-
|
|
90
|
-
// Step 3: Instantiate client / user / code repositories from config.
|
|
91
|
-
const clientConfig = flattenAdapterConfig(
|
|
92
|
-
config.repositories.client as { type: string } & Record<string, unknown>,
|
|
93
|
-
);
|
|
94
|
-
if (typeof clientConfig.path === "string") {
|
|
95
|
-
clientConfig.path = path.resolve(appDir, "..", clientConfig.path);
|
|
96
|
-
}
|
|
97
|
-
const clientRepository = await clientFactory.create(clientConfig);
|
|
98
|
-
const userRepository = await userFactory.create(
|
|
99
|
-
flattenAdapterConfig(config.repositories.user as { type: string } & Record<string, unknown>),
|
|
100
|
-
);
|
|
101
|
-
const codeRepository = await codeFactory.create(
|
|
102
|
-
flattenAdapterConfig(config.repositories.code as { type: string } & Record<string, unknown>),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// Step 4: Create the Express app and apply base security middleware (trust proxy + helmet).
|
|
45
|
+
// Step 2: Create the Express app and apply base security middleware.
|
|
106
46
|
const app = express();
|
|
107
|
-
|
|
108
47
|
app.set("trust proxy", config.http.trustProxy);
|
|
109
48
|
app.use(
|
|
110
49
|
helmet({
|
|
@@ -117,19 +56,24 @@ await (async (): Promise<void> => {
|
|
|
117
56
|
}),
|
|
118
57
|
);
|
|
119
58
|
|
|
120
|
-
// Step
|
|
59
|
+
// Step 3: Build the Express session store and mount express-session.
|
|
60
|
+
// (express-session is host-environment middleware, not part of the auth
|
|
61
|
+
// boot pipeline — wired before createApp so it sits ahead of the router.)
|
|
121
62
|
const sessionStoreFactory = createSessionStoreFactory();
|
|
122
63
|
registerBuiltinSessionStores(sessionStoreFactory);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
64
|
+
const sessionStorageSlice = config.session.storage as {
|
|
65
|
+
type: string;
|
|
66
|
+
} & Record<string, unknown>;
|
|
67
|
+
const sessionStore = await sessionStoreFactory.create({
|
|
68
|
+
type: sessionStorageSlice.type,
|
|
69
|
+
...((sessionStorageSlice[sessionStorageSlice.type] ?? {}) as Record<string, unknown>),
|
|
70
|
+
});
|
|
127
71
|
app.use(
|
|
128
72
|
session({
|
|
129
73
|
secret: config.session.secret,
|
|
130
74
|
resave: false,
|
|
131
75
|
saveUninitialized: false,
|
|
132
|
-
store,
|
|
76
|
+
store: sessionStore,
|
|
133
77
|
cookie: {
|
|
134
78
|
path: "/",
|
|
135
79
|
httpOnly: true,
|
|
@@ -141,61 +85,34 @@ await (async (): Promise<void> => {
|
|
|
141
85
|
}),
|
|
142
86
|
);
|
|
143
87
|
|
|
144
|
-
// Step
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const federationTokenStoreFactory = createFederationTokenStoreFactory();
|
|
160
|
-
registerBuiltinFederationTokenStores(federationTokenStoreFactory);
|
|
161
|
-
const federationTokenStore = await federationTokenStoreFactory.create(
|
|
162
|
-
flattenAdapterConfig(
|
|
163
|
-
(config as { federationTokenStore?: { type: string } & Record<string, unknown> })
|
|
164
|
-
.federationTokenStore ?? { type: "memory" },
|
|
165
|
-
),
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const federationProviderFactory = createFederationProviderFactory();
|
|
169
|
-
registerGoogleFederation(federationProviderFactory);
|
|
170
|
-
// registerGithubFederation(federationProviderFactory);
|
|
171
|
-
|
|
172
|
-
// Step 8: Compose the OAuth / session modules and build the app router.
|
|
173
|
-
const { init, router, grantRegistry } = createApp({
|
|
174
|
-
express,
|
|
175
|
-
pathResolver: import.meta.resolve,
|
|
176
|
-
config,
|
|
177
|
-
keyStore,
|
|
178
|
-
userSessionStore,
|
|
179
|
-
federationTokenStore,
|
|
180
|
-
modules: [
|
|
181
|
-
oauthModule({ clientRepository, codeRepository, express }),
|
|
182
|
-
sessionModule({ userRepository, express, federationProviderFactory }),
|
|
183
|
-
oauthSessionModule({ clientRepository }),
|
|
184
|
-
oauthAuthorizationModule({ codeRepository, clientRepository }),
|
|
185
|
-
],
|
|
88
|
+
// Step 4: Boot the auth pipeline. bootstrapComponents carries only host-
|
|
89
|
+
// environment values (config + pathResolver per A2-γ §4 worked example);
|
|
90
|
+
// every other component flows through composition-root-local modules.
|
|
91
|
+
// `buildModules` is the single source of truth for the module list — it
|
|
92
|
+
// gates federation modules on `config.federations.<name>.enabled`, which
|
|
93
|
+
// the standalone scaffold defaults to false.
|
|
94
|
+
const handle = await createApp({
|
|
95
|
+
modules: buildModules(config),
|
|
96
|
+
bootstrapComponents: {
|
|
97
|
+
config,
|
|
98
|
+
pathResolver: import.meta.resolve,
|
|
99
|
+
},
|
|
186
100
|
});
|
|
187
101
|
|
|
188
|
-
// Step
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
102
|
+
// Step 5: Wire host-level routes (healthcheck) before the auth router so
|
|
103
|
+
// they remain reachable even when the auth pipeline is degraded.
|
|
104
|
+
app.get("/_healthcheck", (_req, res) => {
|
|
105
|
+
res.status(200).json({ status: "ok" });
|
|
106
|
+
});
|
|
193
107
|
|
|
194
|
-
// Step
|
|
108
|
+
// Step 6: Mount the composed auth router and start the HTTP server.
|
|
109
|
+
app.use(handle.router);
|
|
195
110
|
const server = app.listen(config.http.port, (): void => {
|
|
196
111
|
logger.info(`Server is running on http://localhost:${config.http.port}`);
|
|
197
112
|
});
|
|
198
113
|
|
|
199
|
-
// Step
|
|
200
|
-
|
|
114
|
+
// Step 7: Graceful shutdown — handle.dispose() runs reverse-topological
|
|
115
|
+
// per-component cleanup (per A2-β §8.1). Replaces the v0.4.x
|
|
116
|
+
// grantRegistry.cleanup() bridge.
|
|
117
|
+
gracefulShutdown(server, () => handle.dispose());
|
|
201
118
|
})();
|
|
@@ -0,0 +1,78 @@
|
|
|
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 {
|
|
17
|
+
type AppConfig,
|
|
18
|
+
defaultRefreshTokenFamilyRevocationModule,
|
|
19
|
+
defaultRefreshTokenFamilyRotationModule,
|
|
20
|
+
type Module,
|
|
21
|
+
memoryRefreshTokenFamilyStoreModule,
|
|
22
|
+
} from "@o3co/auth-provider-core";
|
|
23
|
+
import { googleFederationModule } from "@o3co/auth-provider-federation-google";
|
|
24
|
+
import {
|
|
25
|
+
oauthAuthorizationModule,
|
|
26
|
+
oauthModule,
|
|
27
|
+
oauthSessionModule,
|
|
28
|
+
} from "@o3co/auth-provider-oauth";
|
|
29
|
+
import { sessionModule } from "@o3co/auth-provider-session";
|
|
30
|
+
import {
|
|
31
|
+
googleFederationConfigModule,
|
|
32
|
+
keyStoreModule,
|
|
33
|
+
repositoriesModule,
|
|
34
|
+
storesModule,
|
|
35
|
+
} from "./modules.mjs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Test-only overrides allowing the smoke test to substitute in-memory
|
|
39
|
+
* implementations of the file-system-backed modules. Production callers
|
|
40
|
+
* should not pass overrides; the defaults match the standalone scaffold.
|
|
41
|
+
*/
|
|
42
|
+
export interface BuildModulesOverrides {
|
|
43
|
+
readonly keyStoreModule?: Module;
|
|
44
|
+
readonly repositoriesModule?: Module;
|
|
45
|
+
readonly storesModule?: Module;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compose the standalone v0.5.0 module list from `config`. Splitting this
|
|
50
|
+
* out of `app.mts` keeps the composition root testable: a smoke test can
|
|
51
|
+
* verify that disabling a federation removes its module pair from the
|
|
52
|
+
* manifest without spinning up a full HTTP server.
|
|
53
|
+
*
|
|
54
|
+
* Federation gating: `googleFederationModule` requires `googleFederationConfig`,
|
|
55
|
+
* which `googleFederationConfigModule` produces by reading
|
|
56
|
+
* `config.federations.google`. When google is disabled (or the section is
|
|
57
|
+
* absent), the config-bridge module's provider throws — so the entire pair
|
|
58
|
+
* MUST be conditionally included at composition time, not gated inside the
|
|
59
|
+
* provider.
|
|
60
|
+
*/
|
|
61
|
+
export function buildModules(config: AppConfig, overrides: BuildModulesOverrides = {}): Module[] {
|
|
62
|
+
const googleEnabled =
|
|
63
|
+
(config.federations?.google as { enabled?: boolean } | undefined)?.enabled === true;
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
oauthModule({ config }),
|
|
67
|
+
oauthSessionModule({ config }),
|
|
68
|
+
oauthAuthorizationModule({ config }),
|
|
69
|
+
sessionModule,
|
|
70
|
+
...(googleEnabled ? [googleFederationModule, googleFederationConfigModule] : []),
|
|
71
|
+
overrides.keyStoreModule ?? keyStoreModule,
|
|
72
|
+
overrides.repositoriesModule ?? repositoriesModule,
|
|
73
|
+
overrides.storesModule ?? storesModule,
|
|
74
|
+
memoryRefreshTokenFamilyStoreModule,
|
|
75
|
+
defaultRefreshTokenFamilyRotationModule,
|
|
76
|
+
defaultRefreshTokenFamilyRevocationModule,
|
|
77
|
+
];
|
|
78
|
+
}
|