@o3co/create-auth-provider 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +107 -0
- package/README.md +107 -0
- package/dist/__tests__/index.test.d.mts +2 -0
- package/dist/__tests__/index.test.d.mts.map +1 -0
- package/dist/__tests__/index.test.mjs +226 -0
- package/dist/cli.d.mts +3 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +18 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +166 -0
- package/package.json +28 -0
- package/templates/standalone/.dockerignore +9 -0
- package/templates/standalone/.env.example +21 -0
- package/templates/standalone/Dockerfile +58 -0
- package/templates/standalone/Makefile +12 -0
- package/templates/standalone/README.ja.md +205 -0
- package/templates/standalone/README.md +205 -0
- package/templates/standalone/config/application.conf +132 -0
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +14 -0
- package/templates/standalone/config/development.conf +8 -0
- package/templates/standalone/config/production.conf +10 -0
- package/templates/standalone/docker-compose.test.yml +16 -0
- package/templates/standalone/docker-compose.yml +26 -0
- package/templates/standalone/package.json +45 -0
- package/templates/standalone/src/__tests__/configPath.test.mts +50 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +204 -0
- package/templates/standalone/src/app.mts +201 -0
- package/templates/standalone/src/configPath.mts +37 -0
- package/templates/standalone/src/logger.mts +18 -0
- package/templates/standalone/tests/Makefile +5 -0
- package/templates/standalone/tests/did-grant.test.js +193 -0
- package/templates/standalone/tests/index.test.js +218 -0
- package/templates/standalone/tests/package-lock.json +2225 -0
- package/templates/standalone/tests/package.json +17 -0
- package/templates/standalone/tsconfig.json +12 -0
- package/templates/standalone/vitest.config.mts +7 -0
- package/templates/versions.json +9 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
target: develop
|
|
6
|
+
ports:
|
|
7
|
+
- "3000:3000"
|
|
8
|
+
volumes:
|
|
9
|
+
- ./src:/home/node/src
|
|
10
|
+
- ./config:/home/node/config
|
|
11
|
+
depends_on:
|
|
12
|
+
redis:
|
|
13
|
+
condition: service_healthy
|
|
14
|
+
env_file:
|
|
15
|
+
- path: .env
|
|
16
|
+
required: false
|
|
17
|
+
|
|
18
|
+
redis:
|
|
19
|
+
image: redis:7-alpine
|
|
20
|
+
ports:
|
|
21
|
+
- "127.0.0.1:6379:6379"
|
|
22
|
+
healthcheck:
|
|
23
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
24
|
+
interval: 5s
|
|
25
|
+
timeout: 3s
|
|
26
|
+
retries: 5
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@o3co/auth-provider-standalone",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"packageManager": "pnpm@10.29.3",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/app.mjs",
|
|
11
|
+
"debug": "NODE_OPTIONS='--conditions=development' tsx watch src/app.mts",
|
|
12
|
+
"test": "vitest run --passWithNoTests"
|
|
13
|
+
},
|
|
14
|
+
"imports": {
|
|
15
|
+
"#/*": {
|
|
16
|
+
"development": "./src/*",
|
|
17
|
+
"default": "./dist/*"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@o3co/auth-provider-core": "workspace:*",
|
|
22
|
+
"@o3co/auth-provider-federation-google": "workspace:*",
|
|
23
|
+
"@o3co/auth-provider-oauth": "workspace:*",
|
|
24
|
+
"@o3co/auth-provider-session": "workspace:*",
|
|
25
|
+
"@o3co/auth-provider-foundation": "workspace:*",
|
|
26
|
+
"@o3co/auth.utils": "^0.0.3",
|
|
27
|
+
"@o3co/ts.hocon": "^1.1.0",
|
|
28
|
+
"connect-redis": "^9.0.0",
|
|
29
|
+
"express": "^5.2.1",
|
|
30
|
+
"express-session": "^1.19.0",
|
|
31
|
+
"helmet": "^8.1.0",
|
|
32
|
+
"pino": "^10.3.1",
|
|
33
|
+
"redis": "^5.10.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/express": "^5.0.6",
|
|
37
|
+
"@types/express-session": "^1",
|
|
38
|
+
"@types/node": "^25.1.0",
|
|
39
|
+
"@types/supertest": "^7.2.0",
|
|
40
|
+
"supertest": "^7.1.0",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.1.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { resolveConfigPaths } from "../configPath.mjs";
|
|
19
|
+
|
|
20
|
+
describe("resolveConfigPaths", () => {
|
|
21
|
+
it("accepts a configDirPath with a trailing slash (regression: fileURLToPath preserves trailing /)", () => {
|
|
22
|
+
const { applicationConfPath, envConfPath } = resolveConfigPaths(
|
|
23
|
+
"/home/node/templates/standalone/config/",
|
|
24
|
+
"production",
|
|
25
|
+
);
|
|
26
|
+
expect(applicationConfPath).toBe("/home/node/templates/standalone/config/application.conf");
|
|
27
|
+
expect(envConfPath).toBe("/home/node/templates/standalone/config/production.conf");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("accepts a configDirPath without a trailing slash", () => {
|
|
31
|
+
const { applicationConfPath, envConfPath } = resolveConfigPaths(
|
|
32
|
+
"/home/node/templates/standalone/config",
|
|
33
|
+
"development",
|
|
34
|
+
);
|
|
35
|
+
expect(applicationConfPath).toBe("/home/node/templates/standalone/config/application.conf");
|
|
36
|
+
expect(envConfPath).toBe("/home/node/templates/standalone/config/development.conf");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects env names that resolve outside configDirPath (path traversal)", () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
resolveConfigPaths("/home/node/templates/standalone/config/", "../secrets"),
|
|
42
|
+
).toThrow(/resolves outside/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects env names containing a path separator", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
resolveConfigPaths("/home/node/templates/standalone/config/", "nested/env"),
|
|
48
|
+
).toThrow(/resolves outside/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
|
|
17
|
+
import {
|
|
18
|
+
type AppConfig,
|
|
19
|
+
createApp,
|
|
20
|
+
createFederationTokenStoreFactory,
|
|
21
|
+
createKeyStoreFactory,
|
|
22
|
+
createUserSessionStoreFactory,
|
|
23
|
+
generateToken,
|
|
24
|
+
InMemoryClientRepository,
|
|
25
|
+
InMemoryCodeRepository,
|
|
26
|
+
InMemoryUserRepository,
|
|
27
|
+
registerBuiltinFederationTokenStores,
|
|
28
|
+
registerBuiltinKeyStores,
|
|
29
|
+
registerBuiltinUserSessionStores,
|
|
30
|
+
} 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
|
+
import express from "express";
|
|
38
|
+
import request from "supertest";
|
|
39
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
40
|
+
|
|
41
|
+
const config: AppConfig = {
|
|
42
|
+
http: { port: 0, trustProxy: false },
|
|
43
|
+
oauth: {
|
|
44
|
+
jwt: {
|
|
45
|
+
signingKey: {
|
|
46
|
+
provider: "local",
|
|
47
|
+
local: {
|
|
48
|
+
algorithm: "HS256",
|
|
49
|
+
secret: "test-secret-for-smoke-test",
|
|
50
|
+
kid: "v0",
|
|
51
|
+
previousKeys: [],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
accessToken: { expiresIn: 3600 },
|
|
56
|
+
refreshToken: { expiresIn: 86400 },
|
|
57
|
+
grants: {},
|
|
58
|
+
},
|
|
59
|
+
session: {
|
|
60
|
+
secret: "test-session-secret",
|
|
61
|
+
maxAge: 3600000,
|
|
62
|
+
secure: false,
|
|
63
|
+
sameSite: "lax",
|
|
64
|
+
domain: null,
|
|
65
|
+
storage: { type: "memory", redis: { url: "redis://localhost:6379" } },
|
|
66
|
+
},
|
|
67
|
+
rateLimit: {
|
|
68
|
+
login: { windowMs: 60000, limit: 10 },
|
|
69
|
+
},
|
|
70
|
+
federations: {
|
|
71
|
+
google: { enabled: false },
|
|
72
|
+
},
|
|
73
|
+
repositories: {
|
|
74
|
+
client: { type: "yaml", path: "./config/clients.yaml" },
|
|
75
|
+
user: { type: "yaml", path: "./config/users.yaml", timeout: 5000 },
|
|
76
|
+
code: { type: "memory", defaultExpiresIn: 600 },
|
|
77
|
+
},
|
|
78
|
+
endpoints: {
|
|
79
|
+
login: { url: undefined },
|
|
80
|
+
client: { url: undefined },
|
|
81
|
+
authCallback: { url: undefined },
|
|
82
|
+
},
|
|
83
|
+
cors: { allowedOrigins: [] },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe("standalone smoke test", () => {
|
|
87
|
+
let grantRegistryRef: Awaited<ReturnType<typeof buildApp>>["grantRegistry"];
|
|
88
|
+
let _appRef: ReturnType<typeof express>;
|
|
89
|
+
|
|
90
|
+
async function buildApp() {
|
|
91
|
+
const clientRepository = new InMemoryClientRepository(new Map());
|
|
92
|
+
const userRepository = new InMemoryUserRepository(new Map());
|
|
93
|
+
const codeRepository = new InMemoryCodeRepository();
|
|
94
|
+
|
|
95
|
+
const keyStoreFactory = createKeyStoreFactory();
|
|
96
|
+
registerBuiltinKeyStores(keyStoreFactory);
|
|
97
|
+
const keyStore = await keyStoreFactory.create({
|
|
98
|
+
type: "local",
|
|
99
|
+
...(config.oauth.jwt.signingKey.local ?? {}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const userSessionStoreFactory = createUserSessionStoreFactory();
|
|
103
|
+
registerBuiltinUserSessionStores(userSessionStoreFactory);
|
|
104
|
+
const userSessionStore = await userSessionStoreFactory.create({ type: "memory" });
|
|
105
|
+
|
|
106
|
+
const federationTokenStoreFactory = createFederationTokenStoreFactory();
|
|
107
|
+
registerBuiltinFederationTokenStores(federationTokenStoreFactory);
|
|
108
|
+
const federationTokenStore = await federationTokenStoreFactory.create({ type: "memory" });
|
|
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
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await init();
|
|
125
|
+
|
|
126
|
+
const app = express();
|
|
127
|
+
app.use(router);
|
|
128
|
+
|
|
129
|
+
return { app, grantRegistry };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
afterAll(async () => {
|
|
133
|
+
await grantRegistryRef?.cleanup();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("GET /_healthcheck returns 200", async () => {
|
|
137
|
+
const { app, grantRegistry } = await buildApp();
|
|
138
|
+
grantRegistryRef = grantRegistry;
|
|
139
|
+
_appRef = app;
|
|
140
|
+
|
|
141
|
+
const res = await request(app).get("/_healthcheck");
|
|
142
|
+
expect(res.status).toBe(200);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("POST /oauth/token with unsupported grant_type returns 400", async () => {
|
|
146
|
+
const { app, grantRegistry } = await buildApp();
|
|
147
|
+
grantRegistryRef = grantRegistry;
|
|
148
|
+
_appRef = app;
|
|
149
|
+
|
|
150
|
+
const res = await request(app)
|
|
151
|
+
.post("/oauth/token")
|
|
152
|
+
.type("form")
|
|
153
|
+
.send({ grant_type: "unsupported" });
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(400);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("POST /oauth/token successful response has Cache-Control: no-store", async () => {
|
|
159
|
+
// We cannot easily trigger a full token issuance without a session,
|
|
160
|
+
// so we verify the header via a session grant with an authenticated session.
|
|
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
|
|
169
|
+
const res = await request(app)
|
|
170
|
+
.post("/oauth/token")
|
|
171
|
+
.type("form")
|
|
172
|
+
.send({ grant_type: "unsupported" });
|
|
173
|
+
|
|
174
|
+
expect(res.status).toBe(400);
|
|
175
|
+
expect(res.headers["cache-control"]).not.toBe("no-store");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("POST /oauth/introspect returns iat in active token response", async () => {
|
|
179
|
+
const { app, grantRegistry } = await buildApp();
|
|
180
|
+
grantRegistryRef = grantRegistry;
|
|
181
|
+
_appRef = app;
|
|
182
|
+
|
|
183
|
+
const ksf = createKeyStoreFactory();
|
|
184
|
+
registerBuiltinKeyStores(ksf);
|
|
185
|
+
const keyStore = await ksf.create({
|
|
186
|
+
type: "local",
|
|
187
|
+
...(config.oauth.jwt.signingKey.local ?? {}),
|
|
188
|
+
});
|
|
189
|
+
const { token } = await generateToken(
|
|
190
|
+
{},
|
|
191
|
+
{ keyStore, subject: "u1", expiresIn: 3600, tokenType: "at+jwt" },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const res = await request(app)
|
|
195
|
+
.post("/oauth/introspect")
|
|
196
|
+
.set("Authorization", `Bearer ${token}`)
|
|
197
|
+
.type("form")
|
|
198
|
+
.send({ token });
|
|
199
|
+
|
|
200
|
+
expect(res.status).toBe(200);
|
|
201
|
+
expect(res.body.active).toBe(true);
|
|
202
|
+
expect(typeof res.body.iat).toBe("number");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
18
|
+
import { gracefulShutdown } from "@o3co/auth.utils";
|
|
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
|
+
createSessionStoreFactory,
|
|
42
|
+
registerBuiltinSessionStores,
|
|
43
|
+
sessionModule,
|
|
44
|
+
} from "@o3co/auth-provider-session";
|
|
45
|
+
import { parseFile } from "@o3co/ts.hocon";
|
|
46
|
+
import { validate } from "@o3co/ts.hocon/zod";
|
|
47
|
+
import express from "express";
|
|
48
|
+
import session from "express-session";
|
|
49
|
+
import helmet from "helmet";
|
|
50
|
+
|
|
51
|
+
import logger from "#/logger.mjs";
|
|
52
|
+
import { resolveConfigPaths } from "./configPath.mjs";
|
|
53
|
+
|
|
54
|
+
// Step 1: Load and validate application config (HOCON → Zod schema).
|
|
55
|
+
// Layering: {ENV}.conf overrides application.conf.
|
|
56
|
+
// ENV = CONFIG_ENV || NODE_ENV || "development". A missing {ENV}.conf is a
|
|
57
|
+
// boot-time error (fail-fast on typos or unconfigured environments).
|
|
58
|
+
const env = process.env.CONFIG_ENV || process.env.NODE_ENV || "development";
|
|
59
|
+
const configDir = new URL("../config/", import.meta.url);
|
|
60
|
+
const configDirPath = fileURLToPath(configDir);
|
|
61
|
+
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDirPath, env);
|
|
62
|
+
const config: AppConfig = validate(
|
|
63
|
+
parseFile(envConfPath).withFallback(parseFile(applicationConfPath)),
|
|
64
|
+
AppConfigSchema,
|
|
65
|
+
);
|
|
66
|
+
|
|
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
|
+
await (async (): Promise<void> => {
|
|
85
|
+
// Step 2: Build repository factories and register built-in adapters (memory / file / etc.).
|
|
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).
|
|
106
|
+
const app = express();
|
|
107
|
+
|
|
108
|
+
app.set("trust proxy", config.http.trustProxy);
|
|
109
|
+
app.use(
|
|
110
|
+
helmet({
|
|
111
|
+
contentSecurityPolicy: {
|
|
112
|
+
directives: {
|
|
113
|
+
defaultSrc: ["'none'"],
|
|
114
|
+
frameAncestors: ["'none'"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Step 5: Build the session store from config and mount express-session middleware.
|
|
121
|
+
const sessionStoreFactory = createSessionStoreFactory();
|
|
122
|
+
registerBuiltinSessionStores(sessionStoreFactory);
|
|
123
|
+
const store = await sessionStoreFactory.create(
|
|
124
|
+
flattenAdapterConfig(config.session.storage as { type: string } & Record<string, unknown>),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
app.use(
|
|
128
|
+
session({
|
|
129
|
+
secret: config.session.secret,
|
|
130
|
+
resave: false,
|
|
131
|
+
saveUninitialized: false,
|
|
132
|
+
store,
|
|
133
|
+
cookie: {
|
|
134
|
+
path: "/",
|
|
135
|
+
httpOnly: true,
|
|
136
|
+
secure: config.session.secure,
|
|
137
|
+
maxAge: config.session.maxAge,
|
|
138
|
+
sameSite: config.session.sameSite,
|
|
139
|
+
domain: config.session.domain || undefined,
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Step 6: Build the JWT signing KeyStore used to issue access / ID tokens.
|
|
145
|
+
const keyStoreFactory = createKeyStoreFactory();
|
|
146
|
+
registerBuiltinKeyStores(keyStoreFactory);
|
|
147
|
+
const keyStore = await keyStoreFactory.create(flattenAdapterConfig(config.oauth.jwt.signingKey));
|
|
148
|
+
|
|
149
|
+
// Step 7: Build UserSessionStore and FederationTokenStore (required by sessionModule).
|
|
150
|
+
const userSessionStoreFactory = createUserSessionStoreFactory();
|
|
151
|
+
registerBuiltinUserSessionStores(userSessionStoreFactory);
|
|
152
|
+
const userSessionStore = await userSessionStoreFactory.create(
|
|
153
|
+
flattenAdapterConfig(
|
|
154
|
+
(config as { userSessionStore?: { type: string } & Record<string, unknown> })
|
|
155
|
+
.userSessionStore ?? { type: "memory" },
|
|
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
|
+
],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Step 9: Run async module init (resolves external deps via pathResolver).
|
|
189
|
+
await init();
|
|
190
|
+
|
|
191
|
+
// Step 10: Mount the composed router onto the Express app.
|
|
192
|
+
app.use(router);
|
|
193
|
+
|
|
194
|
+
// Step 11: Start the HTTP server.
|
|
195
|
+
const server = app.listen(config.http.port, (): void => {
|
|
196
|
+
logger.info(`Server is running on http://localhost:${config.http.port}`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Step 12: Register graceful shutdown so in-flight grants are cleaned up on SIGTERM / SIGINT.
|
|
200
|
+
gracefulShutdown(server, () => grantRegistry.cleanup());
|
|
201
|
+
})();
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface ResolvedConfigPaths {
|
|
20
|
+
readonly applicationConfPath: string;
|
|
21
|
+
readonly envConfPath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveConfigPaths(configDirPath: string, env: string): ResolvedConfigPaths {
|
|
25
|
+
// path.resolve strips trailing slashes that fileURLToPath may preserve, so
|
|
26
|
+
// the containment check below compares equal shapes (path.dirname never
|
|
27
|
+
// returns a trailing separator).
|
|
28
|
+
const normalizedConfigDir = path.resolve(configDirPath);
|
|
29
|
+
const applicationConfPath = path.join(normalizedConfigDir, "application.conf");
|
|
30
|
+
const envConfPath = path.resolve(normalizedConfigDir, `${env}.conf`);
|
|
31
|
+
if (path.dirname(envConfPath) !== normalizedConfigDir) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid config environment name: "${env}" resolves outside ${normalizedConfigDir}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { applicationConfPath, envConfPath };
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { createLogger } from "@o3co/auth.utils";
|
|
17
|
+
|
|
18
|
+
export default createLogger("provider");
|