@o3co/auth-provider-foundation 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +16 -50
- package/README.md +15 -49
- package/dist/index.d.mts +1 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +0 -9
- package/dist/repositories/__tests__/registerBuiltinAdapters.test.mjs +4 -30
- package/package.json +7 -12
- package/dist/repositories/RedisCodeRepository.d.mts +0 -28
- package/dist/repositories/RedisCodeRepository.d.mts.map +0 -1
- package/dist/repositories/RedisCodeRepository.mjs +0 -79
- package/dist/repositories/__tests__/RedisCodeRepository.test.d.mts +0 -2
- package/dist/repositories/__tests__/RedisCodeRepository.test.d.mts.map +0 -1
- package/dist/repositories/__tests__/RedisCodeRepository.test.mjs +0 -141
package/README.ja.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @o3co/auth-provider-foundation
|
|
2
2
|
|
|
3
|
-
auth.provider
|
|
3
|
+
auth.provider 向けの本番用 HTTP ユーザー認証アダプターパッケージ。`"http"` アダプター型を `UserRepository` ファクトリーに登録し、`authenticate` / `authenticateByToken` を上流 HTTP サービスへ委譲する。
|
|
4
|
+
|
|
5
|
+
このパッケージのスコープは v0.5.0 モジュールシステムにおける **本番用の非データベース / 外部サービスアダプター** であり、v0.5.0 時点では `HttpUserRepository` のみを提供する。以前同梱されていた Redis `CodeRepository` アダプターは Phase 10 で [`@o3co/auth-provider-redis`](../redis/README.ja.md) に移管された。
|
|
4
6
|
|
|
5
7
|
## インストール
|
|
6
8
|
|
|
@@ -8,27 +10,27 @@ auth.provider 向けの組み込みリポジトリ実装パッケージ。HTTP
|
|
|
8
10
|
npm install @o3co/auth-provider-foundation
|
|
9
11
|
# peer dependency(必須):
|
|
10
12
|
npm install @o3co/auth-provider-core
|
|
11
|
-
# peer dependency(任意 — RedisCodeRepository を使う場合のみ必要):
|
|
12
|
-
npm install redis
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## パブリック API
|
|
16
16
|
|
|
17
17
|
### `registerBuiltinAdapters`
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- `"http"` を `userFactory` に登録 → `HttpUserRepository` を生成
|
|
22
|
-
- `"redis"` を `codeFactory` に登録 → `RedisCodeRepository` を生成
|
|
19
|
+
`"http"` アダプター型を、指定された `userFactory` に登録する。
|
|
23
20
|
|
|
24
21
|
```typescript
|
|
25
22
|
function registerBuiltinAdapters(factories: {
|
|
26
23
|
userFactory: AdapterFactory<UserRepository>;
|
|
27
|
-
codeFactory: AdapterFactory<CodeRepository>;
|
|
28
|
-
pathResolver?: PathResolver; // 任意 — "redis" モジュールのパス解決に使用
|
|
29
24
|
}): void;
|
|
30
25
|
```
|
|
31
26
|
|
|
27
|
+
Redis を使った認可コードストレージが必要な場合は、`@o3co/auth-provider-redis` の builder を直接登録する:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { redisCodeRepositoryBuilder } from "@o3co/auth-provider-redis";
|
|
31
|
+
codeFactory.register("redis", redisCodeRepositoryBuilder);
|
|
32
|
+
```
|
|
33
|
+
|
|
32
34
|
### `HttpUserRepository`
|
|
33
35
|
|
|
34
36
|
上流の HTTP サービスに認証処理を委譲する `UserRepository` 実装。
|
|
@@ -52,45 +54,15 @@ class HttpUserRepository implements UserRepository {
|
|
|
52
54
|
- HTTP 401 または 403 の場合は `null` を返す。
|
|
53
55
|
- その他の非 OK ステータスの場合はエラーをスローする。
|
|
54
56
|
|
|
55
|
-
### `RedisCodeRepository`
|
|
56
|
-
|
|
57
|
-
Redis に認可コードを保存する `CodeRepository` 実装。キーは `oauth:code:<base64url-code>` 形式で保存され、TTL を設定できる。
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
class RedisCodeRepository implements CodeRepository {
|
|
61
|
-
constructor(redis: RedisClient, defaultExpiresIn?: number); // defaultExpiresIn のデフォルトは 600 秒
|
|
62
|
-
|
|
63
|
-
// ファクトリーメソッド — config から Redis クライアントを生成して接続する
|
|
64
|
-
static create(
|
|
65
|
-
config: Record<string, unknown>,
|
|
66
|
-
pathResolver?: PathResolver,
|
|
67
|
-
): Promise<RedisCodeRepository>;
|
|
68
|
-
// config のキー:
|
|
69
|
-
// endpointUri (string, 必須) — Redis 接続 URI
|
|
70
|
-
// password (string, 任意) — Redis パスワード
|
|
71
|
-
// defaultExpiresIn (number, 任意) — TTL(秒)、デフォルト 600
|
|
72
|
-
|
|
73
|
-
initialize(): Promise<void>; // Redis クライアントを接続する
|
|
74
|
-
createCode(params: {
|
|
75
|
-
code_challenge?: string;
|
|
76
|
-
code_challenge_method?: string;
|
|
77
|
-
expiresIn?: number; // このコードだけ defaultExpiresIn を上書きする
|
|
78
|
-
}): Promise<Code>;
|
|
79
|
-
getByCode(code: string): Promise<Code | null>;
|
|
80
|
-
consumeByCode(code: string): Promise<Code | null>; // アトミックな GET + DEL
|
|
81
|
-
removeByCode(code: string): Promise<void>;
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
57
|
## 使い方
|
|
86
58
|
|
|
87
59
|
```typescript
|
|
88
|
-
import {
|
|
60
|
+
import { createRepositoryFactories } from "@o3co/auth-provider-core";
|
|
89
61
|
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
90
62
|
|
|
91
|
-
const { userFactory
|
|
63
|
+
const { userFactory } = createRepositoryFactories();
|
|
92
64
|
|
|
93
|
-
registerBuiltinAdapters({ userFactory
|
|
65
|
+
registerBuiltinAdapters({ userFactory });
|
|
94
66
|
|
|
95
67
|
// ファクトリー経由で HTTP ユーザーリポジトリを生成
|
|
96
68
|
const userRepo = await userFactory.create({
|
|
@@ -99,16 +71,10 @@ const userRepo = await userFactory.create({
|
|
|
99
71
|
authenticateByTokenUrl: "https://users.example.com/authenticate-by-token",
|
|
100
72
|
timeout: 5000,
|
|
101
73
|
});
|
|
102
|
-
|
|
103
|
-
// ファクトリー経由で Redis コードリポジトリを生成
|
|
104
|
-
const codeRepo = await codeFactory.create({
|
|
105
|
-
type: "redis",
|
|
106
|
-
endpointUri: "redis://localhost:6379",
|
|
107
|
-
defaultExpiresIn: 300,
|
|
108
|
-
});
|
|
109
74
|
```
|
|
110
75
|
|
|
111
76
|
## 関連
|
|
112
77
|
|
|
113
|
-
- [`@o3co/auth-provider-core`](../core/README.md) — コアインターフェース(`UserRepository`、`CodeRepository`、`AdapterFactory`、`createAdapterFactory`、`BuilderContext`、`PathResolver`)
|
|
78
|
+
- [`@o3co/auth-provider-core`](../core/README.ja.md) — コアインターフェース(`UserRepository`、`CodeRepository`、`AdapterFactory`、`createAdapterFactory`、`BuilderContext`、`PathResolver`)
|
|
79
|
+
- [`@o3co/auth-provider-redis`](../redis/README.ja.md) — Redis バックエンドのアダプター群(challenges、replay-seen-set、refresh-token-family、user-sessions、federation-tokens、**code-repository**、rate-limiter)
|
|
114
80
|
- [auth.provider](../../README.md) — リポジトリ全体のドキュメント
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @o3co/auth-provider-foundation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Production HTTP user-authentication adapter for auth.provider. Registers the `"http"` adapter type into a `UserRepository` factory, which delegates `authenticate` / `authenticateByToken` to an upstream HTTP service.
|
|
4
|
+
|
|
5
|
+
This package's scope is **production non-database / external-service adapters** for the v0.5.0 module system. As of v0.5.0 it ships exactly one such adapter (`HttpUserRepository`); the previous Redis `CodeRepository` adapter was relocated to [`@o3co/auth-provider-redis`](../redis/README.md) in Phase 10.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,27 +10,27 @@ Built-in repository implementations for auth.provider. Provides HTTP-based user
|
|
|
8
10
|
npm install @o3co/auth-provider-foundation
|
|
9
11
|
# Peer dependency (required):
|
|
10
12
|
npm install @o3co/auth-provider-core
|
|
11
|
-
# Peer dependency (optional — required only when using RedisCodeRepository):
|
|
12
|
-
npm install redis
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Public API
|
|
16
16
|
|
|
17
17
|
### `registerBuiltinAdapters`
|
|
18
18
|
|
|
19
|
-
Registers the
|
|
20
|
-
|
|
21
|
-
- Registers `"http"` into `userFactory` → creates `HttpUserRepository`
|
|
22
|
-
- Registers `"redis"` into `codeFactory` → creates `RedisCodeRepository`
|
|
19
|
+
Registers the `"http"` adapter type into the provided `userFactory`.
|
|
23
20
|
|
|
24
21
|
```typescript
|
|
25
22
|
function registerBuiltinAdapters(factories: {
|
|
26
23
|
userFactory: AdapterFactory<UserRepository>;
|
|
27
|
-
codeFactory: AdapterFactory<CodeRepository>;
|
|
28
|
-
pathResolver?: PathResolver; // optional — used to resolve the "redis" module path
|
|
29
24
|
}): void;
|
|
30
25
|
```
|
|
31
26
|
|
|
27
|
+
For Redis-backed code storage, register the builder from `@o3co/auth-provider-redis` directly:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { redisCodeRepositoryBuilder } from "@o3co/auth-provider-redis";
|
|
31
|
+
codeFactory.register("redis", redisCodeRepositoryBuilder);
|
|
32
|
+
```
|
|
33
|
+
|
|
32
34
|
### `HttpUserRepository`
|
|
33
35
|
|
|
34
36
|
A `UserRepository` implementation that delegates authentication to an upstream HTTP service.
|
|
@@ -52,45 +54,15 @@ class HttpUserRepository implements UserRepository {
|
|
|
52
54
|
- Returns `null` on HTTP 401 or 403.
|
|
53
55
|
- Throws an error on any other non-OK HTTP status.
|
|
54
56
|
|
|
55
|
-
### `RedisCodeRepository`
|
|
56
|
-
|
|
57
|
-
A `CodeRepository` implementation that stores authorization codes in Redis. Keys are stored as `oauth:code:<base64url-code>` with a configurable TTL.
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
class RedisCodeRepository implements CodeRepository {
|
|
61
|
-
constructor(redis: RedisClient, defaultExpiresIn?: number); // defaultExpiresIn defaults to 600 seconds
|
|
62
|
-
|
|
63
|
-
// Factory method — creates and connects a Redis client from config
|
|
64
|
-
static create(
|
|
65
|
-
config: Record<string, unknown>,
|
|
66
|
-
pathResolver?: PathResolver,
|
|
67
|
-
): Promise<RedisCodeRepository>;
|
|
68
|
-
// config keys:
|
|
69
|
-
// endpointUri (string, required) — Redis connection URI
|
|
70
|
-
// password (string, optional) — Redis password
|
|
71
|
-
// defaultExpiresIn (number, optional) — TTL in seconds, default 600
|
|
72
|
-
|
|
73
|
-
initialize(): Promise<void>; // connects the Redis client
|
|
74
|
-
createCode(params: {
|
|
75
|
-
code_challenge?: string;
|
|
76
|
-
code_challenge_method?: string;
|
|
77
|
-
expiresIn?: number; // overrides defaultExpiresIn for this code
|
|
78
|
-
}): Promise<Code>;
|
|
79
|
-
getByCode(code: string): Promise<Code | null>;
|
|
80
|
-
consumeByCode(code: string): Promise<Code | null>; // atomic GET + DEL
|
|
81
|
-
removeByCode(code: string): Promise<void>;
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
57
|
## Usage Example
|
|
86
58
|
|
|
87
59
|
```typescript
|
|
88
|
-
import {
|
|
60
|
+
import { createRepositoryFactories } from "@o3co/auth-provider-core";
|
|
89
61
|
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
90
62
|
|
|
91
|
-
const { userFactory
|
|
63
|
+
const { userFactory } = createRepositoryFactories();
|
|
92
64
|
|
|
93
|
-
registerBuiltinAdapters({ userFactory
|
|
65
|
+
registerBuiltinAdapters({ userFactory });
|
|
94
66
|
|
|
95
67
|
// Create an HTTP user repository via the factory
|
|
96
68
|
const userRepo = await userFactory.create({
|
|
@@ -99,16 +71,10 @@ const userRepo = await userFactory.create({
|
|
|
99
71
|
authenticateByTokenUrl: "https://users.example.com/authenticate-by-token",
|
|
100
72
|
timeout: 5000,
|
|
101
73
|
});
|
|
102
|
-
|
|
103
|
-
// Create a Redis code repository via the factory
|
|
104
|
-
const codeRepo = await codeFactory.create({
|
|
105
|
-
type: "redis",
|
|
106
|
-
endpointUri: "redis://localhost:6379",
|
|
107
|
-
defaultExpiresIn: 300,
|
|
108
|
-
});
|
|
109
74
|
```
|
|
110
75
|
|
|
111
76
|
## See Also
|
|
112
77
|
|
|
113
78
|
- [`@o3co/auth-provider-core`](../core/README.md) — Core interfaces (`UserRepository`, `CodeRepository`, `AdapterFactory`, `createAdapterFactory`, `BuilderContext`, `PathResolver`)
|
|
79
|
+
- [`@o3co/auth-provider-redis`](../redis/README.md) — Redis-backed adapters (challenges, replay-seen-set, refresh-token-family, user-sessions, federation-tokens, **code-repository**, rate-limiter)
|
|
114
80
|
- [auth.provider](../../README.md) — Top-level repository documentation
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { AdapterFactory,
|
|
1
|
+
import type { AdapterFactory, UserRepository } from "@o3co/auth-provider-core";
|
|
2
2
|
export declare const registerBuiltinAdapters: (factories: {
|
|
3
3
|
userFactory: AdapterFactory<UserRepository>;
|
|
4
|
-
codeFactory: AdapterFactory<CodeRepository>;
|
|
5
|
-
pathResolver?: PathResolver;
|
|
6
4
|
}) => void;
|
|
7
5
|
export { HttpUserRepository } from "./repositories/HttpUserRepository.mjs";
|
|
8
|
-
export { RedisCodeRepository } from "./repositories/RedisCodeRepository.mjs";
|
|
9
6
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/E,eAAO,MAAM,uBAAuB,GAAI,WAAW;IAClD,WAAW,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;CAC5C,KAAG,IAyBH,CAAC;AAEF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { HttpUserRepository } from "./repositories/HttpUserRepository.mjs";
|
|
17
|
-
import { RedisCodeRepository } from "./repositories/RedisCodeRepository.mjs";
|
|
18
17
|
export const registerBuiltinAdapters = (factories) => {
|
|
19
|
-
const { pathResolver } = factories;
|
|
20
18
|
factories.userFactory.register("http", (config) => {
|
|
21
19
|
if (typeof config.authenticateUrl !== "string") {
|
|
22
20
|
throw new Error('HttpUserRepository requires "authenticateUrl" in config');
|
|
@@ -41,12 +39,5 @@ export const registerBuiltinAdapters = (factories) => {
|
|
|
41
39
|
})(),
|
|
42
40
|
});
|
|
43
41
|
});
|
|
44
|
-
factories.codeFactory.register("redis", (config) => {
|
|
45
|
-
if (typeof config.endpointUri !== "string") {
|
|
46
|
-
throw new Error('RedisCodeRepository requires "endpointUri" in config');
|
|
47
|
-
}
|
|
48
|
-
return RedisCodeRepository.create(config, pathResolver);
|
|
49
|
-
});
|
|
50
42
|
};
|
|
51
43
|
export { HttpUserRepository } from "./repositories/HttpUserRepository.mjs";
|
|
52
|
-
export { RedisCodeRepository } from "./repositories/RedisCodeRepository.mjs";
|
|
@@ -39,8 +39,7 @@ afterAll(() => server.close());
|
|
|
39
39
|
describe("registerBuiltinAdapters", () => {
|
|
40
40
|
it("registers 'http' type in userFactory", async () => {
|
|
41
41
|
const userFactory = createAdapterFactory("UserRepository");
|
|
42
|
-
|
|
43
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
42
|
+
registerBuiltinAdapters({ userFactory });
|
|
44
43
|
try {
|
|
45
44
|
await userFactory.create({ type: "unknown" });
|
|
46
45
|
throw new Error("should have thrown");
|
|
@@ -53,26 +52,9 @@ describe("registerBuiltinAdapters", () => {
|
|
|
53
52
|
expect(e.registered).toContain("http");
|
|
54
53
|
}
|
|
55
54
|
});
|
|
56
|
-
it("registers 'redis' type in codeFactory", async () => {
|
|
57
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
58
|
-
const codeFactory = createAdapterFactory("CodeRepository");
|
|
59
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
60
|
-
try {
|
|
61
|
-
await codeFactory.create({ type: "unknown" });
|
|
62
|
-
throw new Error("should have thrown");
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
expect(err).toBeInstanceOf(AdapterFactoryError);
|
|
66
|
-
const e = err;
|
|
67
|
-
expect(e.reason).toBe("unknown");
|
|
68
|
-
expect(e.kind).toBe("CodeRepository");
|
|
69
|
-
expect(e.registered).toContain("redis");
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
55
|
it("http builder creates a working HttpUserRepository", async () => {
|
|
73
56
|
const userFactory = createAdapterFactory("UserRepository");
|
|
74
|
-
|
|
75
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
57
|
+
registerBuiltinAdapters({ userFactory });
|
|
76
58
|
const repo = await userFactory.create({
|
|
77
59
|
type: "http",
|
|
78
60
|
authenticateUrl: `${BASE_URL}/auth`,
|
|
@@ -89,8 +71,7 @@ describe("registerBuiltinAdapters", () => {
|
|
|
89
71
|
});
|
|
90
72
|
it("http builder coerces string timeout to number (env-override path)", async () => {
|
|
91
73
|
const userFactory = createAdapterFactory("UserRepository");
|
|
92
|
-
|
|
93
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
74
|
+
registerBuiltinAdapters({ userFactory });
|
|
94
75
|
const repo = await userFactory.create({
|
|
95
76
|
type: "http",
|
|
96
77
|
authenticateUrl: `${BASE_URL}/auth`,
|
|
@@ -105,14 +86,7 @@ describe("registerBuiltinAdapters", () => {
|
|
|
105
86
|
});
|
|
106
87
|
it("http builder throws when authenticateUrl is missing", async () => {
|
|
107
88
|
const userFactory = createAdapterFactory("UserRepository");
|
|
108
|
-
|
|
109
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
89
|
+
registerBuiltinAdapters({ userFactory });
|
|
110
90
|
await expect(userFactory.create({ type: "http", timeout: 5000 })).rejects.toThrow(/authenticateUrl/);
|
|
111
91
|
});
|
|
112
|
-
it("redis builder throws when endpointUri is missing", async () => {
|
|
113
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
114
|
-
const codeFactory = createAdapterFactory("CodeRepository");
|
|
115
|
-
registerBuiltinAdapters({ userFactory, codeFactory });
|
|
116
|
-
await expect(codeFactory.create({ type: "redis" })).rejects.toThrow(/endpointUri/);
|
|
117
|
-
});
|
|
118
92
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-foundation",
|
|
3
|
-
"description": "Production
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Production HTTP user-authentication adapter for auth.provider",
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.mjs",
|
|
@@ -29,25 +29,20 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@o3co/auth-provider-core": "^0.0.0"
|
|
33
|
-
"redis": "^5.10.0"
|
|
34
|
-
},
|
|
35
|
-
"peerDependenciesMeta": {
|
|
36
|
-
"redis": {
|
|
37
|
-
"optional": true
|
|
38
|
-
}
|
|
32
|
+
"@o3co/auth-provider-core": "^0.0.0"
|
|
39
33
|
},
|
|
40
34
|
"devDependencies": {
|
|
41
35
|
"@types/node": "^25.1.0",
|
|
42
36
|
"msw": "^2.12.14",
|
|
43
|
-
"redis": "^5.10.0",
|
|
44
37
|
"pino": "^10.3.1",
|
|
45
38
|
"vitest": "^4.1.2",
|
|
46
|
-
"@
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
40
|
+
"@o3co/auth-provider-core": "0.5.0"
|
|
47
41
|
},
|
|
48
42
|
"scripts": {
|
|
49
43
|
"prebuild": "rimraf dist",
|
|
50
44
|
"build": "tsc",
|
|
51
|
-
"test": "vitest run"
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage"
|
|
52
47
|
}
|
|
53
48
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Code, CodeRepository, PathResolver } from "@o3co/auth-provider-core";
|
|
2
|
-
interface RedisClient {
|
|
3
|
-
connect(): Promise<void>;
|
|
4
|
-
get(key: string): Promise<string | null>;
|
|
5
|
-
set(key: string, value: string, options?: {
|
|
6
|
-
EX?: number;
|
|
7
|
-
}): Promise<unknown>;
|
|
8
|
-
getDel(key: string): Promise<string | null>;
|
|
9
|
-
del(key: string): Promise<number>;
|
|
10
|
-
}
|
|
11
|
-
export declare class RedisCodeRepository implements CodeRepository {
|
|
12
|
-
private redis;
|
|
13
|
-
private defaultExpiresIn;
|
|
14
|
-
constructor(redis: RedisClient, defaultExpiresIn?: number);
|
|
15
|
-
static create(config: Record<string, unknown>, pathResolver?: PathResolver): Promise<RedisCodeRepository>;
|
|
16
|
-
initialize(): Promise<void>;
|
|
17
|
-
createCode({ code_challenge, code_challenge_method, expiresIn, }: {
|
|
18
|
-
code_challenge?: string;
|
|
19
|
-
code_challenge_method?: string;
|
|
20
|
-
expiresIn?: number;
|
|
21
|
-
}): Promise<Code>;
|
|
22
|
-
getByCode(code: string): Promise<Code | null>;
|
|
23
|
-
consumeByCode(code: string): Promise<Code | null>;
|
|
24
|
-
removeByCode(code: string): Promise<void>;
|
|
25
|
-
private parseCodeValue;
|
|
26
|
-
}
|
|
27
|
-
export {};
|
|
28
|
-
//# sourceMappingURL=RedisCodeRepository.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RedisCodeRepository.d.mts","sourceRoot":"","sources":["../../src/repositories/RedisCodeRepository.mts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAMnF,UAAU,WAAW;IACpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,qBAAa,mBAAoB,YAAW,cAAc;IACzD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,gBAAgB,CAAS;gBAErB,KAAK,EAAE,WAAW,EAAE,gBAAgB,SAAM;WAKzC,MAAM,CAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,YAAY,CAAC,EAAE,YAAY,GACzB,OAAO,CAAC,mBAAmB,CAAC;IA2BzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,UAAU,CAAC,EAChB,cAAc,EACd,qBAAqB,EACrB,SAAiC,GACjC,EAAE;QACF,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYX,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAK7C,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAKjD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C,OAAO,CAAC,cAAc;CAUtB"}
|
|
@@ -1,79 +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 crypto from "node:crypto";
|
|
17
|
-
const KEY_PREFIX = "oauth:code:";
|
|
18
|
-
export class RedisCodeRepository {
|
|
19
|
-
redis;
|
|
20
|
-
defaultExpiresIn;
|
|
21
|
-
constructor(redis, defaultExpiresIn = 600) {
|
|
22
|
-
this.redis = redis;
|
|
23
|
-
this.defaultExpiresIn = defaultExpiresIn;
|
|
24
|
-
}
|
|
25
|
-
static async create(config, pathResolver) {
|
|
26
|
-
if (typeof config.endpointUri !== "string") {
|
|
27
|
-
throw new Error('RedisCodeRepository requires "endpointUri" in config');
|
|
28
|
-
}
|
|
29
|
-
const { createClient } = pathResolver
|
|
30
|
-
? (await import(pathResolver("redis")))
|
|
31
|
-
: await import("redis");
|
|
32
|
-
const redis = createClient({
|
|
33
|
-
url: config.endpointUri,
|
|
34
|
-
password: typeof config.password === "string" ? config.password : undefined,
|
|
35
|
-
socket: {
|
|
36
|
-
reconnectStrategy: (retries) => {
|
|
37
|
-
const jitter = Math.floor(Math.random() * 200);
|
|
38
|
-
const delay = Math.min(2 ** retries * 50, 2000);
|
|
39
|
-
return delay + jitter;
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
const defaultExpiresIn = typeof config.defaultExpiresIn === "number" ? config.defaultExpiresIn : undefined;
|
|
44
|
-
const repo = new RedisCodeRepository(redis, defaultExpiresIn);
|
|
45
|
-
await repo.initialize();
|
|
46
|
-
return repo;
|
|
47
|
-
}
|
|
48
|
-
async initialize() {
|
|
49
|
-
await this.redis.connect();
|
|
50
|
-
}
|
|
51
|
-
async createCode({ code_challenge, code_challenge_method, expiresIn = this.defaultExpiresIn, }) {
|
|
52
|
-
const code = crypto.randomBytes(32).toString("base64url");
|
|
53
|
-
await this.redis.set(KEY_PREFIX + code, JSON.stringify({ code_challenge, code_challenge_method }), { EX: expiresIn });
|
|
54
|
-
return { code, code_challenge, code_challenge_method, expiresIn };
|
|
55
|
-
}
|
|
56
|
-
async getByCode(code) {
|
|
57
|
-
const value = await this.redis.get(KEY_PREFIX + code);
|
|
58
|
-
return this.parseCodeValue(code, value);
|
|
59
|
-
}
|
|
60
|
-
async consumeByCode(code) {
|
|
61
|
-
const value = await this.redis.getDel(KEY_PREFIX + code);
|
|
62
|
-
return this.parseCodeValue(code, value);
|
|
63
|
-
}
|
|
64
|
-
async removeByCode(code) {
|
|
65
|
-
await this.redis.del(KEY_PREFIX + code);
|
|
66
|
-
}
|
|
67
|
-
parseCodeValue(code, value) {
|
|
68
|
-
if (!value)
|
|
69
|
-
return null;
|
|
70
|
-
try {
|
|
71
|
-
return { ...JSON.parse(value), code };
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
const codeHash = crypto.createHash("sha256").update(code).digest("hex").slice(0, 16);
|
|
75
|
-
console.error(`RedisCodeRepository: corrupted data for code (hash=${codeHash})`, err);
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RedisCodeRepository.test.d.mts","sourceRoot":"","sources":["../../../src/repositories/__tests__/RedisCodeRepository.test.mts"],"names":[],"mappings":""}
|
|
@@ -1,141 +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 { beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
-
const KEY_PREFIX = "oauth:code:";
|
|
18
|
-
// In-memory store simulating Redis
|
|
19
|
-
const store = new Map();
|
|
20
|
-
import { RedisCodeRepository } from "../RedisCodeRepository.mjs";
|
|
21
|
-
const createMockRedis = () => ({
|
|
22
|
-
connect: vi.fn().mockResolvedValue(undefined),
|
|
23
|
-
set: vi.fn().mockImplementation((key, value) => {
|
|
24
|
-
store.set(key, value);
|
|
25
|
-
return Promise.resolve("OK");
|
|
26
|
-
}),
|
|
27
|
-
get: vi.fn().mockImplementation((key) => {
|
|
28
|
-
return Promise.resolve(store.get(key) ?? null);
|
|
29
|
-
}),
|
|
30
|
-
getDel: vi.fn().mockImplementation((key) => {
|
|
31
|
-
const value = store.get(key) ?? null;
|
|
32
|
-
store.delete(key);
|
|
33
|
-
return Promise.resolve(value);
|
|
34
|
-
}),
|
|
35
|
-
del: vi.fn().mockImplementation((key) => {
|
|
36
|
-
store.delete(key);
|
|
37
|
-
return Promise.resolve(1);
|
|
38
|
-
}),
|
|
39
|
-
});
|
|
40
|
-
describe("RedisCodeRepository", () => {
|
|
41
|
-
let repo;
|
|
42
|
-
beforeEach(async () => {
|
|
43
|
-
store.clear();
|
|
44
|
-
const redis = createMockRedis();
|
|
45
|
-
repo = new RedisCodeRepository(redis);
|
|
46
|
-
await repo.initialize();
|
|
47
|
-
});
|
|
48
|
-
describe("createCode", () => {
|
|
49
|
-
it("generates a code string and stores it", async () => {
|
|
50
|
-
const result = await repo.createCode({});
|
|
51
|
-
expect(typeof result.code).toBe("string");
|
|
52
|
-
expect(result.code.length).toBeGreaterThan(0);
|
|
53
|
-
// Verify it was stored with namespace prefix
|
|
54
|
-
expect(store.has(`${KEY_PREFIX}${result.code}`)).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
it("returns code with code_challenge and code_challenge_method", async () => {
|
|
57
|
-
const result = await repo.createCode({
|
|
58
|
-
code_challenge: "abc123",
|
|
59
|
-
code_challenge_method: "S256",
|
|
60
|
-
});
|
|
61
|
-
expect(result.code_challenge).toBe("abc123");
|
|
62
|
-
expect(result.code_challenge_method).toBe("S256");
|
|
63
|
-
});
|
|
64
|
-
it("stores code_challenge and code_challenge_method in Redis", async () => {
|
|
65
|
-
const result = await repo.createCode({
|
|
66
|
-
code_challenge: "challenge-value",
|
|
67
|
-
code_challenge_method: "S256",
|
|
68
|
-
});
|
|
69
|
-
const stored = store.get(`${KEY_PREFIX}${result.code}`);
|
|
70
|
-
expect(stored).toBeDefined();
|
|
71
|
-
const parsed = JSON.parse(stored);
|
|
72
|
-
expect(parsed.code_challenge).toBe("challenge-value");
|
|
73
|
-
expect(parsed.code_challenge_method).toBe("S256");
|
|
74
|
-
});
|
|
75
|
-
it("uses default expiresIn when not provided", async () => {
|
|
76
|
-
const result = await repo.createCode({});
|
|
77
|
-
expect(result.expiresIn).toBe(600);
|
|
78
|
-
});
|
|
79
|
-
it("uses provided expiresIn", async () => {
|
|
80
|
-
const result = await repo.createCode({ expiresIn: 300 });
|
|
81
|
-
expect(result.expiresIn).toBe(300);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe("getByCode", () => {
|
|
85
|
-
it("returns stored code data", async () => {
|
|
86
|
-
const created = await repo.createCode({
|
|
87
|
-
code_challenge: "test-challenge",
|
|
88
|
-
code_challenge_method: "S256",
|
|
89
|
-
});
|
|
90
|
-
const found = await repo.getByCode(created.code);
|
|
91
|
-
expect(found).not.toBeNull();
|
|
92
|
-
expect(found?.code).toBe(created.code);
|
|
93
|
-
expect(found?.code_challenge).toBe("test-challenge");
|
|
94
|
-
expect(found?.code_challenge_method).toBe("S256");
|
|
95
|
-
});
|
|
96
|
-
it("returns null for unknown code", async () => {
|
|
97
|
-
const result = await repo.getByCode("nonexistent-code");
|
|
98
|
-
expect(result).toBeNull();
|
|
99
|
-
});
|
|
100
|
-
it("returns null for corrupted data", async () => {
|
|
101
|
-
store.set(`${KEY_PREFIX}corrupted`, "not-valid-json{{{");
|
|
102
|
-
const result = await repo.getByCode("corrupted");
|
|
103
|
-
expect(result).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe("consumeByCode", () => {
|
|
107
|
-
it("returns code data and removes it atomically", async () => {
|
|
108
|
-
const created = await repo.createCode({
|
|
109
|
-
code_challenge: "consume-test",
|
|
110
|
-
code_challenge_method: "S256",
|
|
111
|
-
});
|
|
112
|
-
const consumed = await repo.consumeByCode(created.code);
|
|
113
|
-
expect(consumed).not.toBeNull();
|
|
114
|
-
expect(consumed?.code).toBe(created.code);
|
|
115
|
-
expect(consumed?.code_challenge).toBe("consume-test");
|
|
116
|
-
// Should be gone from store
|
|
117
|
-
expect(store.has(`${KEY_PREFIX}${created.code}`)).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
it("returns null for unknown code", async () => {
|
|
120
|
-
expect(await repo.consumeByCode("nonexistent")).toBeNull();
|
|
121
|
-
});
|
|
122
|
-
it("second consume returns null (replay prevention)", async () => {
|
|
123
|
-
const created = await repo.createCode({});
|
|
124
|
-
const first = await repo.consumeByCode(created.code);
|
|
125
|
-
expect(first).not.toBeNull();
|
|
126
|
-
const second = await repo.consumeByCode(created.code);
|
|
127
|
-
expect(second).toBeNull();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe("removeByCode", () => {
|
|
131
|
-
it("removes a stored code", async () => {
|
|
132
|
-
const created = await repo.createCode({ code_challenge: "c" });
|
|
133
|
-
expect(store.has(`${KEY_PREFIX}${created.code}`)).toBe(true);
|
|
134
|
-
await repo.removeByCode(created.code);
|
|
135
|
-
expect(store.has(`${KEY_PREFIX}${created.code}`)).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
it("does not throw for unknown code", async () => {
|
|
138
|
-
await expect(repo.removeByCode("nonexistent")).resolves.toBeUndefined();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|