@qlover/oauth-wrapper 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ #### โœจ Features
8
+
9
+ - **oauth-wrapper:** extract OAuth 2.0 core into @qlover/oauth-wrapper package ([d7f7f5c](https://github.com/qlover/fe-base/commit/d7f7f5c3a1d1f46b1ebf10f586426c3797c8c061)) ([#616](https://github.com/qlover/fe-base/pull/616))
10
+
11
+ Move server, client, and core modules from the next-oauth-wrapper example into a standalone publishable package with PKCE support, tests, and documentation.
12
+
13
+ - **react-seed:** add useLocale hook for locale management ([9fcd9f8](https://github.com/qlover/fe-base/commit/9fcd9f8645d5454e516fe0fdd4e75fca00598f76)) ([#616](https://github.com/qlover/fe-base/pull/616))
14
+
15
+ Introduced a new `useLocale` hook to manage locale settings within the application. This hook retrieves the current locale from route parameters or defaults to the configured locale, and provides a method to change the locale, updating the navigation accordingly. Additionally, updated `useOAuthLogin` to utilize the new `useLocale` hook for locale configuration during OAuth login processes.
16
+
17
+ Also, added a `patchConfig` method in the `OAuthClient` class to allow dynamic updates to the client configuration, including locale settings.
18
+
19
+ #### ๐Ÿž Bug Fixes
20
+
21
+ - **next-oauth-wrapper:** update local server ports in configuration files ([4fbb447](https://github.com/qlover/fe-base/commit/4fbb447e2a333435ae2824327b2d066eb3355972)) ([#616](https://github.com/qlover/fe-base/pull/616))
22
+
23
+ Changed the default local server ports in `robots.txt` and various files from 3120 to 3102 for consistency across the next-oauth-wrapper example. Additionally, added titles to `LocaleLink` components in the login page for better accessibility and user experience. This update ensures a smoother development environment and improves the clarity of navigation links.
24
+
25
+ ## 0.1.0
26
+
27
+ - Initial publish: extract OAuth 2.0 protocol core from `examples/next-oauth-wrapper/shared/oauth-wrapper`.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # @qlover/oauth-wrapper
2
+
3
+ > English: [README_EN.md](./README_EN.md)
4
+
5
+ ไธŽไธŠๆธธ็™ปๅฝ• API ๆ— ๅ…ณ็š„ **OAuth 2.0 ๆŽˆๆƒๆœๅŠกๅ™จๅ่ฎฎๅ†…ๆ ธ**๏ผšๆŽˆๆƒ็ ๆตใ€PKCEใ€consent ็ผ–ๆŽ’ใ€ๆข็ฅจใ€`userinfo` ๆ˜ ๅฐ„ใ€OAuth Client ็ฎก็†้€ป่พ‘ใ€‚
6
+
7
+ ้€š่ฟ‡ Port ๆŽฅๅฃๆŽฅๅ…ฅไปปๆ„็”จๆˆท็ณป็ปŸ๏ผˆ`OAuthUserAdapterInterface`๏ผ‰ใ€ๆŒไน…ๅŒ–๏ผˆ`OAuthWrapperRepositoryInterface`๏ผ‰ไธŽไผš่ฏ๏ผˆ`OAuthSessionInterface`๏ผ‰ใ€‚ๆœฌๅŒ…**ไธๅŒ…ๅซ**ๅ…ทไฝ“ Adapterใ€ๆ•ฐๆฎๅบ“ๅฎž็Žฐๆˆ– Web ๆก†ๆžถ่ทฏ็”ฑใ€‚
8
+
9
+ ## ็‰นๆ€ง
10
+
11
+ | ่ƒฝๅŠ› | ่ฏดๆ˜Ž |
12
+ | ----------- | ------------------------------------------------------------ |
13
+ | ๆŽˆๆƒ็ ๆต | ไป…ๆ”ฏๆŒ `response_type=code` |
14
+ | PKCE | ๅ…ฌๅผ€ๅฎขๆˆท็ซฏๅผบๅˆถ S256๏ผ›ๆœบๅฏ†ๅฎขๆˆท็ซฏๅฏ้€‰ PKCE ๆˆ– `client_secret` |
15
+ | Token ็ซฏ็‚น | `authorization_code` / `refresh_token` ๆข็ฅจ |
16
+ | Token ๆ’ค้”€ | RFC 7009๏ผŒๆ’ค้”€ middleware ๅฑ‚ `refresh_token`๏ผˆๅน‚็ญ‰๏ผ‰ |
17
+ | UserInfo | ๅฐ† Adapter ็”จๆˆท่ต„ๆ–™ๆ˜ ๅฐ„ไธบ `sub` / `email` / `name` / `roles` |
18
+ | Client ็ฎก็† | ๅผ€ๅ‘่€…ๆŽงๅˆถๅฐ CRUDใ€ๅฏ†้’ฅ่ฝฎๆข |
19
+ | ๆต่งˆๅ™จ SDK | `OAuthClient` + `OAuthGateway`๏ผŒๅ†…็ฝฎ PKCE ไผš่ฏไธŽๅ›ž่ฐƒๅŽป้‡ |
20
+
21
+ ## ๅฎ‰่ฃ…
22
+
23
+ ```bash
24
+ pnpm add @qlover/oauth-wrapper zod
25
+ ```
26
+
27
+ `zod` ไธบ peer dependency๏ผˆ`^3` ๆˆ– `^4`๏ผ‰๏ผŒ็”จไบŽ่ฏทๆฑ‚/ๅ“ๅบ” Schema ๆ ก้ชŒใ€‚
28
+
29
+ ## ๅŒ…ๅ…ฅๅฃ
30
+
31
+ | ๅญ่ทฏๅพ„ | ็”จ้€” |
32
+ | ------------------------------ | -------------------------------------------------- |
33
+ | `@qlover/oauth-wrapper` | ๆœๅŠก็ซฏ๏ผšServicesใ€Utilsใ€Core ๅ†ๅฏผๅ‡บ |
34
+ | `@qlover/oauth-wrapper/core` | ๅ…ฑไบซๅฅ‘็บฆ๏ผšPort ๆŽฅๅฃใ€Zod Schemaใ€RFC ้”™่ฏฏ็  |
35
+ | `@qlover/oauth-wrapper/client` | ๆต่งˆๅ™จ็ซฏ๏ผš`OAuthClient`ใ€`OAuthGateway`ใ€PKCE ๅทฅๅ…ท |
36
+
37
+ ## ๆžถๆž„ๆฆ‚่งˆ
38
+
39
+ ```
40
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
41
+ โ”‚ ไฝ ็š„ HTTP ๆก†ๆžถๅฑ‚ โ”‚
42
+ โ”‚ (Next.js Route / Express / ็ญ‰ โ€” ่‡ช่กŒๆŒ‚่ฝฝ) โ”‚
43
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
44
+ โ”‚
45
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
46
+ โ”‚ OAuthWrapperService โ”‚ OAuthTokenService โ”‚ OAuthClientsService โ”‚
47
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
48
+ โ”‚ โ”‚
49
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
50
+ โ”‚ OAuthSessionInterfaceโ”‚ โ”‚OAuthUserAdapter โ”‚ โ”‚OAuthWrapperRepositoryโ”‚
51
+ โ”‚ (็™ปๅฝ•ไผš่ฏ) โ”‚ โ”‚Interface (ไธŠๆธธ็”จๆˆท)โ”‚ โ”‚Interface (ๆŒไน…ๅŒ–) โ”‚
52
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
53
+ ```
54
+
55
+ **่ฎพ่ฎกๅŽŸๅˆ™**๏ผšๅ่ฎฎ้€ป่พ‘ไธŽๅŸบ็ก€่ฎพๆ–ฝ่งฃ่€ฆใ€‚ไฝ ๅฎž็Žฐไธ‰ไธช Port๏ผŒ็ป„่ฃ… Service๏ผŒๅ†ๆŠŠ HTTP ่ทฏ็”ฑๆŽฅๅˆฐไปปๆ„ๆก†ๆžถๅณๅฏใ€‚
56
+
57
+ ## ๆŽˆๆƒ็ ๆต๏ผˆๆœๅŠก็ซฏ + ๆต่งˆๅ™จ๏ผ‰
58
+
59
+ ```mermaid
60
+ sequenceDiagram
61
+ participant App as ็ฌฌไธ‰ๆ–นๅบ”็”จ (OAuthClient)
62
+ participant Auth as ๆŽˆๆƒๆœๅŠกๅ™จ (OAuthWrapperService)
63
+ participant User as ่ต„ๆบๆ‰€ๆœ‰่€…
64
+ participant Provider as ไธŠๆธธ็”จๆˆท็ณป็ปŸ (Adapter)
65
+
66
+ App->>Auth: GET /oauth/authorize?client_id&redirect_uri&PKCE...
67
+ Auth->>User: ๅฑ•็คบ consent ้กต (resolveAuthorizePage)
68
+ User->>Auth: POST consent (allow/deny)
69
+ Auth->>App: redirect_uri?code&state
70
+ App->>Auth: POST /oauth/token (code + code_verifier)
71
+ Auth->>Provider: exchangeAccessToken(sessionToken)
72
+ Auth->>App: access_token + refresh_token
73
+ App->>Auth: GET /userinfo (Bearer access_token)
74
+ Auth->>Provider: getUserInfoByAccessToken
75
+ Auth->>App: sub, email, name, roles?
76
+ ```
77
+
78
+ ## ๆœๅŠก็ซฏๅฟซ้€Ÿๅผ€ๅง‹
79
+
80
+ ### 1. ๅฎž็Žฐไธ‰ไธช Port
81
+
82
+ **`OAuthUserAdapterInterface`** โ€” ๅฏนๆŽฅไธŠๆธธ็”จๆˆท/่บซไปฝ็ณป็ปŸ๏ผš
83
+
84
+ ```ts
85
+ interface OAuthUserAdapterInterface {
86
+ login(email: string, password: string): Promise<OAuthUserCredentials>;
87
+ exchangeAccessToken(params: {
88
+ token: string;
89
+ lang?: string;
90
+ }): Promise<OAuthUserAccessToken>;
91
+ getUserInfo(sessionToken: string): Promise<OAuthUserProfile>;
92
+ getUserInfoByAccessToken(accessToken: string): Promise<OAuthUserProfile>;
93
+ }
94
+ ```
95
+
96
+ **`OAuthSessionInterface`** โ€” ๆŽˆๆƒๆœๅŠกๅ™จไพง็™ปๅฝ•ไผš่ฏ๏ผˆconsent ๅ‰็”จๆˆทๅฟ…้กปๅทฒ็™ปๅฝ•๏ผ‰๏ผš
97
+
98
+ ```ts
99
+ type OAuthSessionPayload = {
100
+ userId: number;
101
+ email: string;
102
+ name: string;
103
+ providerSessionToken: string;
104
+ };
105
+ ```
106
+
107
+ **`OAuthWrapperRepositoryInterface`** โ€” ๆŽˆๆƒ็ ใ€refresh tokenใ€็”จๆˆทๅ‡ญ่ฏใ€Client ๆณจๅ†ŒไฟกๆฏๆŒไน…ๅŒ–๏ผˆ็ปงๆ‰ฟ `OAuthClientsRepositoryInterface`๏ผ‰ใ€‚
108
+
109
+ ### 2. ็ป„่ฃ… Services
110
+
111
+ ```ts
112
+ import {
113
+ OAuthWrapperService,
114
+ OAuthTokenService,
115
+ OAuthClientsService,
116
+ type OAuthUserAdapterInterface,
117
+ type OAuthWrapperRepositoryInterface,
118
+ type OAuthSessionInterface
119
+ } from '@qlover/oauth-wrapper';
120
+
121
+ const tokenService = new OAuthTokenService(
122
+ tokenEncryption, // EncryptorInterface โ€” ๅŠ ๅฏ† provider refresh_token
123
+ userAdapter,
124
+ oauthRepo
125
+ );
126
+
127
+ const oauthService = new OAuthWrapperService(
128
+ oauthSession,
129
+ userAdapter,
130
+ tokenService,
131
+ oauthRepo
132
+ );
133
+
134
+ const clientsService = new OAuthClientsService(oauthRepo);
135
+ ```
136
+
137
+ ### 3. ๆŒ‚่ฝฝ HTTP ็ซฏ็‚น
138
+
139
+ ็บฆๅฎš่ทฏๅพ„๏ผˆๅฏ้€š่ฟ‡ `OAuthWrapperEndpoints` ๅผ•็”จ๏ผ‰๏ผš
140
+
141
+ | ๆ–นๆณ• | ่ทฏๅพ„ | Service ๆ–นๆณ• | ่ฏดๆ˜Ž |
142
+ | ---- | ------------------ | ----------------------------- | ------------------------------------------- |
143
+ | GET | `/oauth/authorize` | `resolveAuthorizePage(query)` | ๆ ก้ชŒๅ‚ๆ•ฐ๏ผŒ่ฟ”ๅ›ž consent ้กตๆ•ฐๆฎ |
144
+ | POST | consent ่ทฏ็”ฑ | `processConsent(body)` | `action: allow \| deny`๏ผŒ่ฟ”ๅ›ž `redirectUrl` |
145
+ | POST | `/oauth/token` | `exchangeToken(formFields)` | `application/x-www-form-urlencoded` |
146
+ | POST | `/oauth/revoke` | `revokeToken(formFields)` | RFC 7009๏ผŒๅน‚็ญ‰ |
147
+ | GET | `/userinfo` | `getUserInfo(bearerToken)` | `Authorization: Bearer โ€ฆ` |
148
+
149
+ **`processConsent` ่ฏทๆฑ‚ไฝ“**๏ผˆ`OAuthConsentBodySchema`๏ผ‰๏ผš
150
+
151
+ ```ts
152
+ {
153
+ action: 'allow' | 'deny';
154
+ client_id: string;
155
+ redirect_uri: string;
156
+ scope?: string;
157
+ state?: string;
158
+ code_challenge?: string; // ไธŽ authorize ไธ€่‡ด
159
+ code_challenge_method?: 'S256';
160
+ trust?: boolean; // ้ข„็•™๏ผŒๆš‚ๆœชๆŒไน…ๅŒ–
161
+ }
162
+ ```
163
+
164
+ **Token ่ฏทๆฑ‚**๏ผˆ`OAuthTokenRequestSchema`๏ผ‰ๆ”ฏๆŒ๏ผš
165
+
166
+ - `grant_type=authorization_code` โ€” ้œ€ `code`ใ€`redirect_uri`ใ€`client_id`๏ผ›ๅ…ฌๅผ€ๅฎขๆˆท็ซฏ้œ€ `code_verifier`๏ผŒๆœบๅฏ†ๅฎขๆˆท็ซฏๆ—  PKCE ๆ—ถ้œ€ `client_secret`
167
+ - `grant_type=refresh_token` โ€” ้œ€ `refresh_token`ใ€`client_id`
168
+
169
+ ๆŽˆๆƒ็  TTL๏ผš**5 ๅˆ†้’Ÿ**ใ€‚Middleware refresh token TTL๏ผš**90 ๅคฉ**๏ผˆ่ฝฎๆขๅผ refresh๏ผŒๆ—ง token ไฝฟ็”จๅŽๅณๆ’ค้”€๏ผ‰ใ€‚
170
+
171
+ ### 4. ้”™่ฏฏๅค„็†
172
+
173
+ - ๆŽˆๆƒ้กตๆ ก้ชŒๅคฑ่ดฅ๏ผš`resolveAuthorizePage` ่ฟ”ๅ›ž `{ ok: false, error: { errorKey, message } }`๏ผŒ`errorKey` ไธบ `OAuthRfcCodes` ๅธธ้‡
174
+ - Token / UserInfo ๅคฑ่ดฅ๏ผšๆŠ›ๅ‡บ `OAuthWrapperError`๏ผˆ็ปงๆ‰ฟ `ExecutorError`๏ผ‰๏ผŒๅซ RFC `error` ไธŽ HTTP `status`
175
+ - Consent ๅคฑ่ดฅ๏ผšๆŠ›ๅ‡บ `ExecutorError`
176
+
177
+ ```ts
178
+ import { OAuthRfcCodes, OAuthWrapperError } from '@qlover/oauth-wrapper';
179
+
180
+ // OAuthRfcCodes: invalid_request, invalid_client, invalid_grant,
181
+ // invalid_token, unauthorized_client, invalid_scope, access_denied, โ€ฆ
182
+ ```
183
+
184
+ ## ๆต่งˆๅ™จ็ซฏๅฟซ้€Ÿๅผ€ๅง‹
185
+
186
+ ไปŽ `@qlover/oauth-wrapper/client` ๅฏผๅ…ฅ๏ผš
187
+
188
+ ```ts
189
+ import { OAuthClient } from '@qlover/oauth-wrapper/client';
190
+
191
+ const oauthClient = new OAuthClient({
192
+ serverUrl: 'https://auth.example.com',
193
+ clientId: 'your-client-id',
194
+ scope: 'openid profile email',
195
+ redirectPath: 'oauth/callback',
196
+ origin: window.location.origin,
197
+ routerPrefix: '/app', // ๅฏ้€‰
198
+ locale: 'en', // ๅฏ้€‰๏ผŒๆ”ฏๆŒ path/query/header
199
+ mapUser: (userinfo, accessToken, refreshToken) => ({
200
+ id: userinfo.sub,
201
+ email: userinfo.email,
202
+ name: userinfo.name
203
+ })
204
+ });
205
+
206
+ // 1. ๅ‘่ตท็™ปๅฝ•๏ผˆ็”Ÿๆˆ PKCEใ€state๏ผŒ่ทณ่ฝฌ authorize๏ผ‰
207
+ await oauthClient.startOAuthLogin();
208
+
209
+ // 2. ๅ›ž่ฐƒ้กตๅค„็†
210
+ const params = oauthClient.parseOAuthCallbackSearchParams(
211
+ new URLSearchParams(window.location.search)
212
+ );
213
+ const user = await oauthClient.completeOAuthCallback(params);
214
+
215
+ // 3. ๆ’ค้”€ refresh token๏ผˆ็™ปๅ‡บๆ—ถ๏ผ‰
216
+ await oauthClient.revokeToken(refreshToken);
217
+ ```
218
+
219
+ ### OAuthClient ๆ ธๅฟƒ API
220
+
221
+ | ๆ–นๆณ• | ่ฏดๆ˜Ž |
222
+ | ---------------------------------- | --------------------------------------------- |
223
+ | `isConfigured()` | `serverUrl` + `clientId` ๆ˜ฏๅฆๅฐฑ็ปช |
224
+ | `startOAuthLogin(params?)` | ไฟๅญ˜ PKCE ไผš่ฏๅนถ่ทณ่ฝฌ authorize |
225
+ | `completeOAuthCallback(params?)` | ๆ ก้ชŒ stateใ€ๆข็ฅจใ€ๆ‹‰ userinfoใ€่ฐƒ็”จ `mapUser` |
226
+ | `parseOAuthCallbackSearchParams()` | ไปŽ URL ่งฃๆž `code` / `state` / `error` |
227
+ | `fetchUserInfo(accessToken)` | ๅ•็‹ฌๆ‹‰ๅ– userinfo |
228
+ | `revokeToken(refreshToken?)` | POST `/oauth/revoke` |
229
+ | `getGateway()` | ่Žทๅ–ๅบ•ๅฑ‚ `OAuthGateway` |
230
+ | `patchConfig(partial)` | ่ฟ่กŒๆ—ถๆ›ดๆ–ฐ้…็ฝฎ๏ผˆๅฆ‚ๅˆ‡ๆข locale๏ผ‰ |
231
+
232
+ `OAuthGateway` ๅ•็‹ฌไฝฟ็”จๆ—ถ้œ€ๅ…ˆ้€š่ฟ‡ `PKCESessionStore` ๅ†™ๅ…ฅ PKCE ไผš่ฏ๏ผ›`OAuthClient` ๅทฒๅฐ่ฃ…ๆญคๆต็จ‹ใ€‚ๅ›ž่ฐƒๅค„็†ๅ†…็ฝฎ **code+state ๅŽป้‡**๏ผŒ้ฟๅ… React Strict Mode ๅŒ่ฐƒ็”จใ€‚
233
+
234
+ ### ้…็ฝฎ้กน๏ผˆ`OAuthClientConfig`๏ผ‰
235
+
236
+ | ๅญ—ๆฎต | ้ป˜่ฎค | ่ฏดๆ˜Ž |
237
+ | --------------------- | ----------------------------- | ----------------------------------------------- |
238
+ | `serverUrl` | `''` | ๆŽˆๆƒๆœๅŠกๅ™จๆ น URL |
239
+ | `clientId` | `''` | OAuth Client ID |
240
+ | `scope` | `'openid profile email'` | ๆŽˆๆƒ scope |
241
+ | `redirectPath` | `'oauth/callback'` | ๅ›ž่ฐƒ่ทฏๅพ„๏ผˆ็›ธๅฏน origin๏ผ‰ |
242
+ | `origin` | ๅฝ“ๅ‰ `window.location.origin` | ็”จไบŽๆ‹ผ redirect_uri |
243
+ | `routerPrefix` | โ€” | ่ทฏ็”ฑๅ‰็ผ€๏ผˆๅฆ‚ `/en`๏ผ‰ |
244
+ | `locale` / `localeIn` | `'en'` / `'path'` | ๅคš่ฏญ่จ€๏ผš`path` \| `query` \| `header` \| `none` |
245
+
246
+ PKCE ไผš่ฏ้ป˜่ฎคๅญ˜ไบŽ `sessionStorage`๏ผŒ้”ฎๅ `oauth-wrapper-pkcesession`๏ผŒๅฏ้€š่ฟ‡ `pkceStorage` / `pkceStorageKey` ่‡ชๅฎšไน‰ใ€‚
247
+
248
+ ## Client ็ฎก็†๏ผˆๅผ€ๅ‘่€…ๆŽงๅˆถๅฐ๏ผ‰
249
+
250
+ `OAuthClientsService` ๆไพ›ๆŒ‰ `ownerUserId` ้š”็ฆป็š„ CRUD๏ผš
251
+
252
+ | ๆ–นๆณ• | ่ฏดๆ˜Ž |
253
+ | --------------------------------- | ------------------------------------------ |
254
+ | `listForOwner(userId)` | ๅˆ—ๅ‡บๆ‰€ๅฑž Client |
255
+ | `getByClientId(userId, clientId)` | ่ฏฆๆƒ… |
256
+ | `create(userId, input)` | ๅˆ›ๅปบ๏ผ›ๆœบๅฏ†ๅฎขๆˆท็ซฏ่ฟ”ๅ›žไธ€ๆฌกๆ€ง `client_secret` |
257
+ | `update(userId, clientId, input)` | ๆ›ดๆ–ฐๅ็งฐใ€URIใ€redirect_uris |
258
+ | `rotateSecret(userId, clientId)` | ่ฝฎๆขๅฏ†้’ฅ๏ผˆไป… confidential๏ผ‰ |
259
+ | `delete(userId, clientId)` | ๅˆ ้™ค |
260
+
261
+ ๅˆ›ๅปบ/ๆ›ดๆ–ฐ Schema๏ผš`OAuthClientCreateSchema`ใ€`OAuthClientUpdateSchema`๏ผˆ`redirect_uris` ๆ”ฏๆŒ HTTPS ไธŽ custom scheme๏ผ‰ใ€‚
262
+
263
+ ## Core ๆจกๅ—ๅ‚่€ƒ
264
+
265
+ ### Services๏ผˆ`@qlover/oauth-wrapper`๏ผ‰
266
+
267
+ | ็ฑป | ่Œ่ดฃ |
268
+ | --------------------- | --------------------------------------------------------------------------- |
269
+ | `OAuthWrapperService` | ๆŽˆๆƒ้กต่งฃๆžใ€`processConsent`ใ€ๆข็ฅจ/revoke ๅง”ๆ‰˜ใ€`getUserInfo`ใ€`logoutUser` |
270
+ | `OAuthTokenService` | Token ็ซฏ็‚น๏ผšๆ ก้ชŒ clientใ€ๆถˆ่ดนๆŽˆๆƒ็ ใ€PKCEใ€็ญพๅ‘ middleware refresh token |
271
+ | `OAuthClientsService` | ๅผ€ๅ‘่€…ๆŽงๅˆถๅฐ Client ไธšๅŠก้€ป่พ‘ |
272
+
273
+ ### Schema๏ผˆ`@qlover/oauth-wrapper/core`๏ผ‰
274
+
275
+ | Schema | ็”จ้€” |
276
+ | ------------------------------------------ | --------------------------------- |
277
+ | `OAuthAuthorizeQuerySchema` | GET authorize ๆŸฅ่ฏขๅ‚ๆ•ฐ |
278
+ | `OAuthConsentBodySchema` | Consent POST body |
279
+ | `OAuthTokenRequestSchema` | Token ็ซฏ็‚น๏ผˆdiscriminated union๏ผ‰ |
280
+ | `OAuthTokenRevokeSchema` | Revoke ็ซฏ็‚น |
281
+ | `OAuthClientCreateSchema` / `UpdateSchema` | Client ็ฎก็† |
282
+ | `OAuthUserInfoResponseSchema` | UserInfo ๅ“ๅบ” |
283
+ | `OAuthTokenResponseSchema` | Token ๅ“ๅบ” |
284
+
285
+ ### Utils๏ผˆๆœๅŠก็ซฏ๏ผ‰
286
+
287
+ | ๅทฅๅ…ท | ่ฏดๆ˜Ž |
288
+ | ---------------------------------------------------------------- | -------------------------- |
289
+ | `validatePkceParams` / `normalizeQuery` / `isRedirectUriAllowed` | ๆŽˆๆƒ่ฏทๆฑ‚ๆ ก้ชŒ |
290
+ | `buildOAuthRedirectUrl` / `parseScopeList` | Redirect ๆž„ๅปบไธŽ scope ่งฃๆž |
291
+ | `verifyPkceS256` / `isValidCodeChallenge` | PKCE S256 |
292
+ | `hashClientSecret` / `verifyClientSecret` | Client secret ๅ“ˆๅธŒ |
293
+ | `OAuthWrapperError` | ๅธฆ HTTP status ็š„ RFC ้”™่ฏฏ |
294
+
295
+ ### Utils๏ผˆๆต่งˆๅ™จ็ซฏ `@qlover/oauth-wrapper/client`๏ผ‰
296
+
297
+ | ๅทฅๅ…ท | ่ฏดๆ˜Ž |
298
+ | ------------------------------------------------------------------------ | ------------------ |
299
+ | `generatePkceVerifier` / `computePkceS256Challenge` / `randomOAuthState` | PKCE ไธŽ CSRF state |
300
+ | `buildOAuthAuthorizeUrl` / `buildOAuthRedirectUri` | URL ๆž„ๅปบ |
301
+ | `parseOAuthTokenResponse` / `parseOAuthUserInfoResponse` | ๅ“ๅบ”่งฃๆž |
302
+ | `PKCESessionStore` | ไผš่ฏ็บง PKCE ๅญ˜ๅ‚จ |
303
+
304
+ ## Monorepo ็คบไพ‹
305
+
306
+ | ็คบไพ‹ | ่ฏดๆ˜Ž |
307
+ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
308
+ | [`examples/next-oauth-wrapper`](../../examples/next-oauth-wrapper) | ๅฎŒๆ•ดๆŽˆๆƒๆœๅŠกๅ™จ้ƒจ็ฝฒ๏ผšAdapterใ€Repositoryใ€Sessionใ€Next.js ่ทฏ็”ฑใ€ๅผ€ๅ‘่€…ๆŽงๅˆถๅฐ UI |
309
+ | [`examples/react-seed`](../../examples/react-seed) | ๆต่งˆๅ™จ็ซฏ `OAuthClient` ้›†ๆˆ๏ผˆ`SeedOAuthClient` + `useOAuthLogin`๏ผ‰ |
310
+
311
+ ## ็‰ˆๆœฌ
312
+
313
+ ๅฝ“ๅ‰็‰ˆๆœฌ่ง [package.json](./package.json) ไธŽ [CHANGELOG.md](./CHANGELOG.md)ใ€‚
package/README_EN.md ADDED
@@ -0,0 +1,313 @@
1
+ # @qlover/oauth-wrapper
2
+
3
+ > ไธญๆ–‡: [README.md](./README.md)
4
+
5
+ Provider-agnostic **OAuth 2.0 authorization server core**: authorization code flow, PKCE, consent orchestration, token exchange, userinfo mapping, and OAuth client management.
6
+
7
+ Integrate any user system via `OAuthUserAdapterInterface`, storage via `OAuthWrapperRepositoryInterface`, and sessions via `OAuthSessionInterface`. This package does **not** ship concrete adapters, databases, or web framework routes.
8
+
9
+ ## Features
10
+
11
+ | Capability | Description |
12
+ | ---------- | ----------- |
13
+ | Authorization code flow | `response_type=code` only |
14
+ | PKCE | S256 required for public clients; confidential clients may use PKCE or `client_secret` |
15
+ | Token endpoint | `authorization_code` / `refresh_token` exchange |
16
+ | Token revocation | RFC 7009, revokes middleware-layer `refresh_token` (idempotent) |
17
+ | UserInfo | Maps adapter profiles to `sub` / `email` / `name` / `roles` |
18
+ | Client management | Developer console CRUD and secret rotation |
19
+ | Browser SDK | `OAuthClient` + `OAuthGateway` with PKCE session storage and callback deduplication |
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm add @qlover/oauth-wrapper zod
25
+ ```
26
+
27
+ `zod` is a peer dependency (`^3` or `^4`) used for request/response schema validation.
28
+
29
+ ## Package entry points
30
+
31
+ | Subpath | Purpose |
32
+ | ------- | ------- |
33
+ | `@qlover/oauth-wrapper` | Server: Services, Utils, re-exports Core |
34
+ | `@qlover/oauth-wrapper/core` | Shared contracts: Port interfaces, Zod schemas, RFC error codes |
35
+ | `@qlover/oauth-wrapper/client` | Browser: `OAuthClient`, `OAuthGateway`, PKCE utilities |
36
+
37
+ ## Architecture overview
38
+
39
+ ```
40
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
41
+ โ”‚ Your HTTP framework layer โ”‚
42
+ โ”‚ (Next.js Route / Express / etc. โ€” mount yourself) โ”‚
43
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
44
+ โ”‚
45
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
46
+ โ”‚ OAuthWrapperService โ”‚ OAuthTokenService โ”‚ OAuthClientsService โ”‚
47
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
48
+ โ”‚ โ”‚
49
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
50
+ โ”‚ OAuthSessionInterfaceโ”‚ โ”‚OAuthUserAdapter โ”‚ โ”‚OAuthWrapperRepositoryโ”‚
51
+ โ”‚ (login session) โ”‚ โ”‚Interface (upstream)โ”‚ โ”‚Interface (persistence)โ”‚
52
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
53
+ ```
54
+
55
+ **Design principle**: protocol logic is decoupled from infrastructure. Implement the three ports, compose services, and wire HTTP routes in any framework.
56
+
57
+ ## Authorization code flow (server + browser)
58
+
59
+ ```mermaid
60
+ sequenceDiagram
61
+ participant App as Third-party app (OAuthClient)
62
+ participant Auth as Authorization server (OAuthWrapperService)
63
+ participant User as Resource owner
64
+ participant Provider as Upstream user system (Adapter)
65
+
66
+ App->>Auth: GET /oauth/authorize?client_id&redirect_uri&PKCE...
67
+ Auth->>User: Show consent page (resolveAuthorizePage)
68
+ User->>Auth: POST consent (allow/deny)
69
+ Auth->>App: redirect_uri?code&state
70
+ App->>Auth: POST /oauth/token (code + code_verifier)
71
+ Auth->>Provider: exchangeAccessToken(sessionToken)
72
+ Auth->>App: access_token + refresh_token
73
+ App->>Auth: GET /userinfo (Bearer access_token)
74
+ Auth->>Provider: getUserInfoByAccessToken
75
+ Auth->>App: sub, email, name, roles?
76
+ ```
77
+
78
+ ## Server quick start
79
+
80
+ ### 1. Implement three ports
81
+
82
+ **`OAuthUserAdapterInterface`** โ€” connect to your upstream user/identity system:
83
+
84
+ ```ts
85
+ interface OAuthUserAdapterInterface {
86
+ login(email: string, password: string): Promise<OAuthUserCredentials>;
87
+ exchangeAccessToken(params: {
88
+ token: string;
89
+ lang?: string;
90
+ }): Promise<OAuthUserAccessToken>;
91
+ getUserInfo(sessionToken: string): Promise<OAuthUserProfile>;
92
+ getUserInfoByAccessToken(accessToken: string): Promise<OAuthUserProfile>;
93
+ }
94
+ ```
95
+
96
+ **`OAuthSessionInterface`** โ€” authorization-server login session (user must be signed in before consent):
97
+
98
+ ```ts
99
+ type OAuthSessionPayload = {
100
+ userId: number;
101
+ email: string;
102
+ name: string;
103
+ providerSessionToken: string;
104
+ };
105
+ ```
106
+
107
+ **`OAuthWrapperRepositoryInterface`** โ€” persist authorization codes, refresh tokens, user credentials, and registered clients (extends `OAuthClientsRepositoryInterface`).
108
+
109
+ ### 2. Compose services
110
+
111
+ ```ts
112
+ import {
113
+ OAuthWrapperService,
114
+ OAuthTokenService,
115
+ OAuthClientsService,
116
+ type OAuthUserAdapterInterface,
117
+ type OAuthWrapperRepositoryInterface,
118
+ type OAuthSessionInterface
119
+ } from '@qlover/oauth-wrapper';
120
+
121
+ const tokenService = new OAuthTokenService(
122
+ tokenEncryption, // EncryptorInterface โ€” encrypts provider refresh_token
123
+ userAdapter,
124
+ oauthRepo
125
+ );
126
+
127
+ const oauthService = new OAuthWrapperService(
128
+ oauthSession,
129
+ userAdapter,
130
+ tokenService,
131
+ oauthRepo
132
+ );
133
+
134
+ const clientsService = new OAuthClientsService(oauthRepo);
135
+ ```
136
+
137
+ ### 3. Mount HTTP endpoints
138
+
139
+ Conventional paths (reference via `OAuthWrapperEndpoints`):
140
+
141
+ | Method | Path | Service method | Description |
142
+ | ------ | ---- | -------------- | ----------- |
143
+ | GET | `/oauth/authorize` | `resolveAuthorizePage(query)` | Validate params, return consent page data |
144
+ | POST | consent route | `processConsent(body)` | `action: allow \| deny`, returns `redirectUrl` |
145
+ | POST | `/oauth/token` | `exchangeToken(formFields)` | `application/x-www-form-urlencoded` |
146
+ | POST | `/oauth/revoke` | `revokeToken(formFields)` | RFC 7009, idempotent |
147
+ | GET | `/userinfo` | `getUserInfo(bearerToken)` | `Authorization: Bearer โ€ฆ` |
148
+
149
+ **`processConsent` request body** (`OAuthConsentBodySchema`):
150
+
151
+ ```ts
152
+ {
153
+ action: 'allow' | 'deny';
154
+ client_id: string;
155
+ redirect_uri: string;
156
+ scope?: string;
157
+ state?: string;
158
+ code_challenge?: string; // must match authorize request
159
+ code_challenge_method?: 'S256';
160
+ trust?: boolean; // reserved, not persisted yet
161
+ }
162
+ ```
163
+
164
+ **Token requests** (`OAuthTokenRequestSchema`) support:
165
+
166
+ - `grant_type=authorization_code` โ€” requires `code`, `redirect_uri`, `client_id`; public clients need `code_verifier`; confidential clients without PKCE need `client_secret`
167
+ - `grant_type=refresh_token` โ€” requires `refresh_token`, `client_id`
168
+
169
+ Authorization code TTL: **5 minutes**. Middleware refresh token TTL: **90 days** (rotating refresh โ€” old token revoked after use).
170
+
171
+ ### 4. Error handling
172
+
173
+ - Authorize page validation failure: `resolveAuthorizePage` returns `{ ok: false, error: { errorKey, message } }` where `errorKey` is an `OAuthRfcCodes` constant
174
+ - Token / UserInfo failure: throws `OAuthWrapperError` (extends `ExecutorError`) with RFC `error` and HTTP `status`
175
+ - Consent failure: throws `ExecutorError`
176
+
177
+ ```ts
178
+ import { OAuthRfcCodes, OAuthWrapperError } from '@qlover/oauth-wrapper';
179
+
180
+ // OAuthRfcCodes: invalid_request, invalid_client, invalid_grant,
181
+ // invalid_token, unauthorized_client, invalid_scope, access_denied, โ€ฆ
182
+ ```
183
+
184
+ ## Browser quick start
185
+
186
+ Import from `@qlover/oauth-wrapper/client`:
187
+
188
+ ```ts
189
+ import { OAuthClient } from '@qlover/oauth-wrapper/client';
190
+
191
+ const oauthClient = new OAuthClient({
192
+ serverUrl: 'https://auth.example.com',
193
+ clientId: 'your-client-id',
194
+ scope: 'openid profile email',
195
+ redirectPath: 'oauth/callback',
196
+ origin: window.location.origin,
197
+ routerPrefix: '/app', // optional
198
+ locale: 'en', // optional; supports path/query/header
199
+ mapUser: (userinfo, accessToken, refreshToken) => ({
200
+ id: userinfo.sub,
201
+ email: userinfo.email,
202
+ name: userinfo.name
203
+ })
204
+ });
205
+
206
+ // 1. Start login (generate PKCE + state, redirect to authorize)
207
+ await oauthClient.startOAuthLogin();
208
+
209
+ // 2. Handle callback page
210
+ const params = oauthClient.parseOAuthCallbackSearchParams(
211
+ new URLSearchParams(window.location.search)
212
+ );
213
+ const user = await oauthClient.completeOAuthCallback(params);
214
+
215
+ // 3. Revoke refresh token (on logout)
216
+ await oauthClient.revokeToken(refreshToken);
217
+ ```
218
+
219
+ ### OAuthClient core API
220
+
221
+ | Method | Description |
222
+ | ------ | ----------- |
223
+ | `isConfigured()` | Whether `serverUrl` + `clientId` are ready |
224
+ | `startOAuthLogin(params?)` | Save PKCE session and redirect to authorize |
225
+ | `completeOAuthCallback(params?)` | Validate state, exchange code, fetch userinfo, call `mapUser` |
226
+ | `parseOAuthCallbackSearchParams()` | Parse `code` / `state` / `error` from URL |
227
+ | `fetchUserInfo(accessToken)` | Fetch userinfo only |
228
+ | `revokeToken(refreshToken?)` | POST `/oauth/revoke` |
229
+ | `getGateway()` | Access underlying `OAuthGateway` |
230
+ | `patchConfig(partial)` | Update config at runtime (e.g. switch locale) |
231
+
232
+ When using `OAuthGateway` directly, you must write the PKCE session via `PKCESessionStore` first; `OAuthClient` wraps this flow. Callback handling includes **code+state deduplication** to avoid double invocation under React Strict Mode.
233
+
234
+ ### Configuration (`OAuthClientConfig`)
235
+
236
+ | Field | Default | Description |
237
+ | ----- | ------- | ----------- |
238
+ | `serverUrl` | `''` | Authorization server base URL |
239
+ | `clientId` | `''` | OAuth Client ID |
240
+ | `scope` | `'openid profile email'` | Authorization scope |
241
+ | `redirectPath` | `'oauth/callback'` | Callback path (relative to origin) |
242
+ | `origin` | current `window.location.origin` | Used to build redirect_uri |
243
+ | `routerPrefix` | โ€” | Route prefix (e.g. `/en`) |
244
+ | `locale` / `localeIn` | `'en'` / `'path'` | i18n: `path` \| `query` \| `header` \| `none` |
245
+
246
+ PKCE session defaults to `sessionStorage` under key `oauth-wrapper-pkcesession`; customize via `pkceStorage` / `pkceStorageKey`.
247
+
248
+ ## Client management (developer console)
249
+
250
+ `OAuthClientsService` provides owner-scoped CRUD by `ownerUserId`:
251
+
252
+ | Method | Description |
253
+ | ------ | ----------- |
254
+ | `listForOwner(userId)` | List owned clients |
255
+ | `getByClientId(userId, clientId)` | Get details |
256
+ | `create(userId, input)` | Create; confidential clients return a one-time `client_secret` |
257
+ | `update(userId, clientId, input)` | Update name, URI, redirect_uris |
258
+ | `rotateSecret(userId, clientId)` | Rotate secret (confidential only) |
259
+ | `delete(userId, clientId)` | Delete |
260
+
261
+ Create/update schemas: `OAuthClientCreateSchema`, `OAuthClientUpdateSchema` (`redirect_uris` supports HTTPS and custom URL schemes).
262
+
263
+ ## Core module reference
264
+
265
+ ### Services (`@qlover/oauth-wrapper`)
266
+
267
+ | Class | Responsibility |
268
+ | ----- | -------------- |
269
+ | `OAuthWrapperService` | Authorize page parsing, `processConsent`, token/revoke delegation, `getUserInfo`, `logoutUser` |
270
+ | `OAuthTokenService` | Token endpoint: client validation, code consumption, PKCE, middleware refresh token issuance |
271
+ | `OAuthClientsService` | Developer console client business logic |
272
+
273
+ ### Schemas (`@qlover/oauth-wrapper/core`)
274
+
275
+ | Schema | Purpose |
276
+ | ------ | ------- |
277
+ | `OAuthAuthorizeQuerySchema` | GET authorize query params |
278
+ | `OAuthConsentBodySchema` | Consent POST body |
279
+ | `OAuthTokenRequestSchema` | Token endpoint (discriminated union) |
280
+ | `OAuthTokenRevokeSchema` | Revoke endpoint |
281
+ | `OAuthClientCreateSchema` / `UpdateSchema` | Client management |
282
+ | `OAuthUserInfoResponseSchema` | UserInfo response |
283
+ | `OAuthTokenResponseSchema` | Token response |
284
+
285
+ ### Utils (server)
286
+
287
+ | Utility | Description |
288
+ | ------- | ----------- |
289
+ | `validatePkceParams` / `normalizeQuery` / `isRedirectUriAllowed` | Authorize request validation |
290
+ | `buildOAuthRedirectUrl` / `parseScopeList` | Redirect building and scope parsing |
291
+ | `verifyPkceS256` / `isValidCodeChallenge` | PKCE S256 |
292
+ | `hashClientSecret` / `verifyClientSecret` | Client secret hashing |
293
+ | `OAuthWrapperError` | RFC errors with HTTP status |
294
+
295
+ ### Utils (browser `@qlover/oauth-wrapper/client`)
296
+
297
+ | Utility | Description |
298
+ | ------- | ----------- |
299
+ | `generatePkceVerifier` / `computePkceS256Challenge` / `randomOAuthState` | PKCE and CSRF state |
300
+ | `buildOAuthAuthorizeUrl` / `buildOAuthRedirectUri` | URL building |
301
+ | `parseOAuthTokenResponse` / `parseOAuthUserInfoResponse` | Response parsing |
302
+ | `PKCESessionStore` | Session-scoped PKCE storage |
303
+
304
+ ## Monorepo examples
305
+
306
+ | Example | Description |
307
+ | ------- | ----------- |
308
+ | [`examples/next-oauth-wrapper`](../../examples/next-oauth-wrapper) | Full authorization server: Adapter, Repository, Session, Next.js routes, developer console UI |
309
+ | [`examples/react-seed`](../../examples/react-seed) | Browser `OAuthClient` integration (`SeedOAuthClient` + `useOAuthLogin`) |
310
+
311
+ ## Version
312
+
313
+ See [package.json](./package.json) and [CHANGELOG.md](./CHANGELOG.md) for the current version.