@flink-app/oidc-plugin 1.0.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +846 -0
- package/dist/OidcInternalContext.d.ts +15 -0
- package/dist/OidcInternalContext.d.ts.map +1 -0
- package/dist/OidcInternalContext.js +2 -0
- package/dist/OidcPlugin.d.ts +77 -0
- package/dist/OidcPlugin.d.ts.map +1 -0
- package/dist/OidcPlugin.js +274 -0
- package/dist/OidcPluginContext.d.ts +73 -0
- package/dist/OidcPluginContext.d.ts.map +1 -0
- package/dist/OidcPluginContext.js +2 -0
- package/dist/OidcPluginOptions.d.ts +267 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -0
- package/dist/OidcPluginOptions.js +2 -0
- package/dist/OidcProviderConfig.d.ts +77 -0
- package/dist/OidcProviderConfig.d.ts.map +1 -0
- package/dist/OidcProviderConfig.js +2 -0
- package/dist/handlers/CallbackOidc.d.ts +38 -0
- package/dist/handlers/CallbackOidc.d.ts.map +1 -0
- package/dist/handlers/CallbackOidc.js +219 -0
- package/dist/handlers/InitiateOidc.d.ts +35 -0
- package/dist/handlers/InitiateOidc.d.ts.map +1 -0
- package/dist/handlers/InitiateOidc.js +91 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/providers/OidcProvider.d.ts +90 -0
- package/dist/providers/OidcProvider.d.ts.map +1 -0
- package/dist/providers/OidcProvider.js +208 -0
- package/dist/providers/ProviderRegistry.d.ts +55 -0
- package/dist/providers/ProviderRegistry.d.ts.map +1 -0
- package/dist/providers/ProviderRegistry.js +94 -0
- package/dist/repos/OidcConnectionRepo.d.ts +75 -0
- package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
- package/dist/repos/OidcConnectionRepo.js +122 -0
- package/dist/repos/OidcSessionRepo.d.ts +57 -0
- package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
- package/dist/repos/OidcSessionRepo.js +91 -0
- package/dist/schemas/CallbackRequest.d.ts +37 -0
- package/dist/schemas/CallbackRequest.d.ts.map +1 -0
- package/dist/schemas/CallbackRequest.js +2 -0
- package/dist/schemas/InitiateRequest.d.ts +17 -0
- package/dist/schemas/InitiateRequest.d.ts.map +1 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/schemas/OidcConnection.d.ts +69 -0
- package/dist/schemas/OidcConnection.d.ts.map +1 -0
- package/dist/schemas/OidcConnection.js +2 -0
- package/dist/schemas/OidcProfile.d.ts +69 -0
- package/dist/schemas/OidcProfile.d.ts.map +1 -0
- package/dist/schemas/OidcProfile.js +2 -0
- package/dist/schemas/OidcSession.d.ts +46 -0
- package/dist/schemas/OidcSession.d.ts.map +1 -0
- package/dist/schemas/OidcSession.js +2 -0
- package/dist/schemas/OidcTokenSet.d.ts +42 -0
- package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
- package/dist/schemas/OidcTokenSet.js +2 -0
- package/dist/utils/claims-mapper.d.ts +46 -0
- package/dist/utils/claims-mapper.d.ts.map +1 -0
- package/dist/utils/claims-mapper.js +104 -0
- package/dist/utils/encryption-utils.d.ts +32 -0
- package/dist/utils/encryption-utils.d.ts.map +1 -0
- package/dist/utils/encryption-utils.js +82 -0
- package/dist/utils/error-utils.d.ts +65 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +150 -0
- package/dist/utils/response-utils.d.ts +18 -0
- package/dist/utils/response-utils.d.ts.map +1 -0
- package/dist/utils/response-utils.js +42 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.d.ts.map +1 -0
- package/dist/utils/state-utils.js +66 -0
- package/examples/basic-oidc.ts +151 -0
- package/examples/multi-provider.ts +146 -0
- package/package.json +44 -0
- package/spec/handlers/InitiateOidc.spec.ts +62 -0
- package/spec/helpers/reporter.ts +34 -0
- package/spec/helpers/test-helpers.ts +108 -0
- package/spec/plugin/OidcPlugin.spec.ts +126 -0
- package/spec/providers/ProviderRegistry.spec.ts +197 -0
- package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
- package/spec/repos/OidcSessionRepo.spec.ts +196 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/claims-mapper.spec.ts +257 -0
- package/spec/utils/encryption-utils.spec.ts +126 -0
- package/spec/utils/error-utils.spec.ts +107 -0
- package/spec/utils/state-utils.spec.ts +102 -0
- package/src/OidcInternalContext.ts +15 -0
- package/src/OidcPlugin.ts +290 -0
- package/src/OidcPluginContext.ts +76 -0
- package/src/OidcPluginOptions.ts +286 -0
- package/src/OidcProviderConfig.ts +87 -0
- package/src/handlers/CallbackOidc.ts +257 -0
- package/src/handlers/InitiateOidc.ts +110 -0
- package/src/index.ts +38 -0
- package/src/providers/OidcProvider.ts +237 -0
- package/src/providers/ProviderRegistry.ts +107 -0
- package/src/repos/OidcConnectionRepo.ts +132 -0
- package/src/repos/OidcSessionRepo.ts +99 -0
- package/src/schemas/CallbackRequest.ts +41 -0
- package/src/schemas/InitiateRequest.ts +17 -0
- package/src/schemas/OidcConnection.ts +80 -0
- package/src/schemas/OidcProfile.ts +79 -0
- package/src/schemas/OidcSession.ts +52 -0
- package/src/schemas/OidcTokenSet.ts +47 -0
- package/src/utils/claims-mapper.ts +114 -0
- package/src/utils/encryption-utils.ts +92 -0
- package/src/utils/error-utils.ts +167 -0
- package/src/utils/response-utils.ts +41 -0
- package/src/utils/state-utils.ts +66 -0
- package/tsconfig.dist.json +9 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import OidcProfile from "./schemas/OidcProfile";
|
|
2
|
+
import OidcTokenSet from "./schemas/OidcTokenSet";
|
|
3
|
+
import { OidcProviderConfig } from "./OidcProviderConfig";
|
|
4
|
+
/**
|
|
5
|
+
* OIDC error information passed to onAuthError callback
|
|
6
|
+
*/
|
|
7
|
+
export interface OidcError {
|
|
8
|
+
/**
|
|
9
|
+
* Error code (e.g., "invalid_state", "access_denied", "token_exchange_failed")
|
|
10
|
+
*/
|
|
11
|
+
code: string;
|
|
12
|
+
/**
|
|
13
|
+
* Human-readable error message
|
|
14
|
+
*/
|
|
15
|
+
message: string;
|
|
16
|
+
/**
|
|
17
|
+
* Additional error details for debugging
|
|
18
|
+
*/
|
|
19
|
+
details?: any;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Response from onAuthSuccess callback
|
|
23
|
+
* Must include JWT token generated by the application via jwt-auth-plugin
|
|
24
|
+
*/
|
|
25
|
+
export interface AuthSuccessCallbackResponse {
|
|
26
|
+
/**
|
|
27
|
+
* User object from your application
|
|
28
|
+
* Should include _id field for connection storage
|
|
29
|
+
*/
|
|
30
|
+
user: any;
|
|
31
|
+
/**
|
|
32
|
+
* JWT token for your application (not the OIDC tokens!)
|
|
33
|
+
* Generated using ctx.plugins.jwtAuth.createToken()
|
|
34
|
+
*/
|
|
35
|
+
token: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional redirect URL after authentication
|
|
38
|
+
* Plugin will append #token=... to this URL
|
|
39
|
+
*/
|
|
40
|
+
redirectUrl?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Response from onAuthError callback
|
|
44
|
+
*/
|
|
45
|
+
export interface AuthErrorCallbackResponse {
|
|
46
|
+
/**
|
|
47
|
+
* Optional redirect URL for error page
|
|
48
|
+
* e.g., "/login?error=authentication_failed"
|
|
49
|
+
*/
|
|
50
|
+
redirectUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Configuration options for OIDC Plugin with JWT integration
|
|
54
|
+
*
|
|
55
|
+
* The plugin depends on @flink-app/jwt-auth-plugin being installed and configured.
|
|
56
|
+
* The onAuthSuccess callback receives the Flink context as a second parameter,
|
|
57
|
+
* allowing the application to generate JWT tokens using ctx.plugins.jwtAuth.createToken().
|
|
58
|
+
*/
|
|
59
|
+
export interface OidcPluginOptions {
|
|
60
|
+
/**
|
|
61
|
+
* OIDC provider configurations
|
|
62
|
+
* Key = provider name (used in URLs: /oidc/{provider}/initiate)
|
|
63
|
+
* Value = provider configuration
|
|
64
|
+
*
|
|
65
|
+
* At least one provider must be configured.
|
|
66
|
+
*
|
|
67
|
+
* Example:
|
|
68
|
+
* {
|
|
69
|
+
* acme: {
|
|
70
|
+
* issuer: "https://idp.acme.com",
|
|
71
|
+
* clientId: "...",
|
|
72
|
+
* clientSecret: "...",
|
|
73
|
+
* callbackUrl: "https://myapp.com/oidc/acme/callback",
|
|
74
|
+
* discoveryUrl: "https://idp.acme.com/.well-known/openid-configuration"
|
|
75
|
+
* },
|
|
76
|
+
* contoso: {
|
|
77
|
+
* issuer: "https://login.contoso.com",
|
|
78
|
+
* clientId: "...",
|
|
79
|
+
* clientSecret: "...",
|
|
80
|
+
* callbackUrl: "https://myapp.com/oidc/contoso/callback"
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
providers: Record<string, OidcProviderConfig>;
|
|
85
|
+
/**
|
|
86
|
+
* Whether to store OIDC tokens for future API access
|
|
87
|
+
* If false, tokens are discarded after authentication (auth-only mode)
|
|
88
|
+
* If true, encrypted tokens are stored in MongoDB for later use
|
|
89
|
+
*
|
|
90
|
+
* Default: false
|
|
91
|
+
*
|
|
92
|
+
* Set to true if you need to:
|
|
93
|
+
* - Call IdP APIs on behalf of users
|
|
94
|
+
* - Access user's resources at the IdP
|
|
95
|
+
* - Use refresh tokens to maintain long-term access
|
|
96
|
+
*/
|
|
97
|
+
storeTokens?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Callback invoked after successful OIDC authentication
|
|
100
|
+
*
|
|
101
|
+
* Application responsibilities:
|
|
102
|
+
* 1. Find or create user based on OIDC profile (JIT provisioning)
|
|
103
|
+
* 2. Link OIDC provider to user account
|
|
104
|
+
* 3. Generate JWT token using ctx.plugins.jwtAuth.createToken(payload, roles)
|
|
105
|
+
* 4. Return user object, JWT token, and optional redirect URL
|
|
106
|
+
*
|
|
107
|
+
* @param params - OIDC profile, claims, provider name, and tokens (if storeTokens enabled)
|
|
108
|
+
* @param ctx - Flink context with access to repos and plugins (including jwtAuth)
|
|
109
|
+
* @returns User object, JWT token, and optional redirect URL
|
|
110
|
+
*
|
|
111
|
+
* Example:
|
|
112
|
+
* ```typescript
|
|
113
|
+
* onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
|
|
114
|
+
* // Find user by OIDC subject + issuer
|
|
115
|
+
* let user = await ctx.repos.userRepo.getOne({
|
|
116
|
+
* 'oidcConnections.subject': claims.sub,
|
|
117
|
+
* 'oidcConnections.issuer': claims.iss
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* if (!user) {
|
|
121
|
+
* // JIT provisioning - create new user
|
|
122
|
+
* user = await ctx.repos.userRepo.create({
|
|
123
|
+
* email: claims.email,
|
|
124
|
+
* name: claims.name,
|
|
125
|
+
* oidcConnections: [{
|
|
126
|
+
* issuer: claims.iss,
|
|
127
|
+
* subject: claims.sub,
|
|
128
|
+
* provider
|
|
129
|
+
* }]
|
|
130
|
+
* });
|
|
131
|
+
* }
|
|
132
|
+
*
|
|
133
|
+
* // Generate JWT token
|
|
134
|
+
* const token = await ctx.plugins.jwtAuth.createToken(
|
|
135
|
+
* { userId: user._id, email: user.email },
|
|
136
|
+
* ['user']
|
|
137
|
+
* );
|
|
138
|
+
*
|
|
139
|
+
* return {
|
|
140
|
+
* user,
|
|
141
|
+
* token,
|
|
142
|
+
* redirectUrl: '/dashboard'
|
|
143
|
+
* };
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
onAuthSuccess: (params: {
|
|
148
|
+
/**
|
|
149
|
+
* Normalized user profile from OIDC claims
|
|
150
|
+
*/
|
|
151
|
+
profile: OidcProfile;
|
|
152
|
+
/**
|
|
153
|
+
* Raw OIDC claims from ID token
|
|
154
|
+
* Contains all standard and custom claims
|
|
155
|
+
*/
|
|
156
|
+
claims: Record<string, any>;
|
|
157
|
+
/**
|
|
158
|
+
* Provider name (e.g., "acme", "contoso")
|
|
159
|
+
*/
|
|
160
|
+
provider: string;
|
|
161
|
+
/**
|
|
162
|
+
* OIDC tokens (only if storeTokens: true)
|
|
163
|
+
* Includes accessToken, idToken, refreshToken
|
|
164
|
+
*/
|
|
165
|
+
tokens?: OidcTokenSet;
|
|
166
|
+
}, ctx: any) => Promise<AuthSuccessCallbackResponse>;
|
|
167
|
+
/**
|
|
168
|
+
* Callback invoked on OIDC authentication errors
|
|
169
|
+
*
|
|
170
|
+
* Application responsibilities:
|
|
171
|
+
* - Log error for debugging
|
|
172
|
+
* - Optionally provide redirect URL for error page
|
|
173
|
+
*
|
|
174
|
+
* @param params - Error information and provider name
|
|
175
|
+
* @returns Optional redirect URL for error page
|
|
176
|
+
*
|
|
177
|
+
* Example:
|
|
178
|
+
* ```typescript
|
|
179
|
+
* onAuthError: async ({ error, provider }) => {
|
|
180
|
+
* console.error(`OIDC error for ${provider}:`, error);
|
|
181
|
+
*
|
|
182
|
+
* if (error.code === 'access_denied') {
|
|
183
|
+
* return {
|
|
184
|
+
* redirectUrl: '/login?error=user_cancelled'
|
|
185
|
+
* };
|
|
186
|
+
* }
|
|
187
|
+
*
|
|
188
|
+
* return {
|
|
189
|
+
* redirectUrl: '/login?error=authentication_failed'
|
|
190
|
+
* };
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
onAuthError?: (params: {
|
|
195
|
+
error: OidcError;
|
|
196
|
+
provider: string;
|
|
197
|
+
}) => Promise<AuthErrorCallbackResponse>;
|
|
198
|
+
/**
|
|
199
|
+
* Dynamic provider loader callback
|
|
200
|
+
*
|
|
201
|
+
* Called when a provider is accessed but not in the static providers config.
|
|
202
|
+
* Allows loading provider configurations from database at runtime.
|
|
203
|
+
*
|
|
204
|
+
* Use this for multi-tenant scenarios where each organization has different IdPs.
|
|
205
|
+
*
|
|
206
|
+
* @param providerName - Name of the provider to load
|
|
207
|
+
* @returns Provider configuration or null if not found
|
|
208
|
+
*
|
|
209
|
+
* Example:
|
|
210
|
+
* ```typescript
|
|
211
|
+
* providerLoader: async (providerName) => {
|
|
212
|
+
* const config = await ctx.repos.oidcProviderRepo.getByName(providerName);
|
|
213
|
+
* if (!config || !config.enabled) {
|
|
214
|
+
* return null;
|
|
215
|
+
* }
|
|
216
|
+
* return {
|
|
217
|
+
* issuer: config.issuer,
|
|
218
|
+
* clientId: config.clientId,
|
|
219
|
+
* clientSecret: decryptSecret(config.clientSecret),
|
|
220
|
+
* callbackUrl: config.callbackUrl,
|
|
221
|
+
* discoveryUrl: config.discoveryUrl,
|
|
222
|
+
* scope: config.scope
|
|
223
|
+
* };
|
|
224
|
+
* }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>;
|
|
228
|
+
/**
|
|
229
|
+
* Custom collection name for OIDC sessions
|
|
230
|
+
* Default: 'oidc_sessions'
|
|
231
|
+
*/
|
|
232
|
+
sessionsCollectionName?: string;
|
|
233
|
+
/**
|
|
234
|
+
* Custom collection name for OIDC connections (if storeTokens enabled)
|
|
235
|
+
* Default: 'oidc_connections'
|
|
236
|
+
*/
|
|
237
|
+
connectionsCollectionName?: string;
|
|
238
|
+
/**
|
|
239
|
+
* Session TTL in seconds
|
|
240
|
+
* Sessions are automatically cleaned up after this duration
|
|
241
|
+
* Default: 600 (10 minutes)
|
|
242
|
+
*
|
|
243
|
+
* This is the maximum time a user has to complete the OIDC flow
|
|
244
|
+
* (from /initiate to /callback). Increase if users need more time.
|
|
245
|
+
*/
|
|
246
|
+
sessionTTL?: number;
|
|
247
|
+
/**
|
|
248
|
+
* Encryption key for encrypting stored OIDC tokens
|
|
249
|
+
* If not provided, will be derived from first configured provider's client secret
|
|
250
|
+
*
|
|
251
|
+
* Recommended: Use a dedicated encryption key from environment variables
|
|
252
|
+
* Must be at least 32 characters
|
|
253
|
+
*
|
|
254
|
+
* Example: process.env.OIDC_ENCRYPTION_KEY
|
|
255
|
+
*/
|
|
256
|
+
encryptionKey?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Whether to register OIDC routes automatically
|
|
259
|
+
* If false, you must manually handle OIDC flow
|
|
260
|
+
* Default: true
|
|
261
|
+
*
|
|
262
|
+
* Set to false if you want to implement custom route handling
|
|
263
|
+
* or use a custom URL structure.
|
|
264
|
+
*/
|
|
265
|
+
registerRoutes?: boolean;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=OidcPluginOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,IAAI,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE9C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,aAAa,EAAE,CACX,MAAM,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,WAAW,CAAC;QAErB;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;QAEjB;;;WAGG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;KACzB,EACD,GAAG,EAAE,GAAG,KACP,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAErG;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAE9E;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAEnC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for a single OIDC provider
|
|
3
|
+
*
|
|
4
|
+
* Supports both OIDC discovery and manual endpoint configuration.
|
|
5
|
+
*/
|
|
6
|
+
export interface OidcProviderConfig {
|
|
7
|
+
/**
|
|
8
|
+
* OIDC issuer URL
|
|
9
|
+
* e.g., "https://idp.acme.com" or "https://login.microsoftonline.com/{tenant}/v2.0"
|
|
10
|
+
* Used to validate the 'iss' claim in ID tokens
|
|
11
|
+
*/
|
|
12
|
+
issuer: string;
|
|
13
|
+
/**
|
|
14
|
+
* OAuth 2.0 client ID
|
|
15
|
+
* Provided by the IdP when you register your application
|
|
16
|
+
*/
|
|
17
|
+
clientId: string;
|
|
18
|
+
/**
|
|
19
|
+
* OAuth 2.0 client secret
|
|
20
|
+
* Provided by the IdP - keep this secure!
|
|
21
|
+
*/
|
|
22
|
+
clientSecret: string;
|
|
23
|
+
/**
|
|
24
|
+
* Callback URL for OAuth redirect
|
|
25
|
+
* Must match the redirect URI registered with the IdP
|
|
26
|
+
* e.g., "https://myapp.com/oidc/acme/callback"
|
|
27
|
+
*/
|
|
28
|
+
callbackUrl: string;
|
|
29
|
+
/**
|
|
30
|
+
* OAuth 2.0 scopes to request
|
|
31
|
+
* Default: ["openid", "email", "profile"]
|
|
32
|
+
* "openid" is required for OIDC
|
|
33
|
+
*/
|
|
34
|
+
scope?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* OIDC discovery URL for automatic configuration
|
|
37
|
+
* e.g., "https://idp.acme.com/.well-known/openid-configuration"
|
|
38
|
+
* If provided, endpoints will be auto-discovered
|
|
39
|
+
* If not provided, must specify manual endpoints below
|
|
40
|
+
*/
|
|
41
|
+
discoveryUrl?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Manual endpoint configuration (if not using discovery)
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Authorization endpoint URL
|
|
47
|
+
* e.g., "https://idp.acme.com/authorize"
|
|
48
|
+
* Required if discoveryUrl not provided
|
|
49
|
+
*/
|
|
50
|
+
authorizationEndpoint?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Token endpoint URL
|
|
53
|
+
* e.g., "https://idp.acme.com/token"
|
|
54
|
+
* Required if discoveryUrl not provided
|
|
55
|
+
*/
|
|
56
|
+
tokenEndpoint?: string;
|
|
57
|
+
/**
|
|
58
|
+
* UserInfo endpoint URL
|
|
59
|
+
* e.g., "https://idp.acme.com/userinfo"
|
|
60
|
+
* Optional - if not provided, only ID token claims will be used
|
|
61
|
+
*/
|
|
62
|
+
userinfoEndpoint?: string;
|
|
63
|
+
/**
|
|
64
|
+
* JWKS (JSON Web Key Set) endpoint URL
|
|
65
|
+
* e.g., "https://idp.acme.com/.well-known/jwks.json"
|
|
66
|
+
* Required for ID token signature validation
|
|
67
|
+
* Required if discoveryUrl not provided
|
|
68
|
+
*/
|
|
69
|
+
jwksUri?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Additional custom claims to extract from ID token
|
|
72
|
+
* Map of app field names to OIDC claim paths
|
|
73
|
+
* e.g., { "department": "custom:department", "role": "custom:role" }
|
|
74
|
+
*/
|
|
75
|
+
claimMapping?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=OidcProviderConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OidcProviderConfig.d.ts","sourceRoot":"","sources":["../src/OidcProviderConfig.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IAEH;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Callback Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles the OIDC callback from the provider by:
|
|
5
|
+
* 1. Validating the state parameter to prevent CSRF attacks
|
|
6
|
+
* 2. Exchanging the authorization code for tokens (with PKCE validation)
|
|
7
|
+
* 3. Validating the ID token (signature, claims, nonce)
|
|
8
|
+
* 4. Fetching user profile from ID token and UserInfo endpoint
|
|
9
|
+
* 5. Calling the onAuthSuccess callback to create/link user and generate JWT token
|
|
10
|
+
* 6. Optionally storing the OIDC connection (if storeTokens enabled)
|
|
11
|
+
* 7. Returning the JWT token to the client (via JSON or redirect)
|
|
12
|
+
*
|
|
13
|
+
* Route: GET /oidc/:provider/callback?code=...&state=...&response_type=json
|
|
14
|
+
*/
|
|
15
|
+
import { GetHandler, RouteProps } from "@flink-app/flink";
|
|
16
|
+
import CallbackRequest from "../schemas/CallbackRequest";
|
|
17
|
+
/**
|
|
18
|
+
* Path parameters for the handler
|
|
19
|
+
*/
|
|
20
|
+
interface PathParams {
|
|
21
|
+
provider: string;
|
|
22
|
+
[key: string]: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Route configuration
|
|
26
|
+
* This handler is registered programmatically by the plugin
|
|
27
|
+
*/
|
|
28
|
+
export declare const Route: RouteProps;
|
|
29
|
+
/**
|
|
30
|
+
* OIDC Callback Handler
|
|
31
|
+
*
|
|
32
|
+
* Completes the OIDC flow by exchanging the authorization code for tokens,
|
|
33
|
+
* validating the ID token, building user profile, calling the app's onAuthSuccess
|
|
34
|
+
* callback to generate JWT token, and returning the token to the client.
|
|
35
|
+
*/
|
|
36
|
+
declare const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest>;
|
|
37
|
+
export default CallbackOidc;
|
|
38
|
+
//# sourceMappingURL=CallbackOidc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallbackOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/CallbackOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAwC,MAAM,kBAAkB,CAAC;AAC5G,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAMzD;;GAEG;AACH,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,UAGnB,CAAC;AAEF;;;;;;GAMG;AACH,QAAA,MAAM,YAAY,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,CAgNnE,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OIDC Callback Handler
|
|
4
|
+
*
|
|
5
|
+
* Handles the OIDC callback from the provider by:
|
|
6
|
+
* 1. Validating the state parameter to prevent CSRF attacks
|
|
7
|
+
* 2. Exchanging the authorization code for tokens (with PKCE validation)
|
|
8
|
+
* 3. Validating the ID token (signature, claims, nonce)
|
|
9
|
+
* 4. Fetching user profile from ID token and UserInfo endpoint
|
|
10
|
+
* 5. Calling the onAuthSuccess callback to create/link user and generate JWT token
|
|
11
|
+
* 6. Optionally storing the OIDC connection (if storeTokens enabled)
|
|
12
|
+
* 7. Returning the JWT token to the client (via JSON or redirect)
|
|
13
|
+
*
|
|
14
|
+
* Route: GET /oidc/:provider/callback?code=...&state=...&response_type=json
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Route = void 0;
|
|
18
|
+
const flink_1 = require("@flink-app/flink");
|
|
19
|
+
const state_utils_1 = require("../utils/state-utils");
|
|
20
|
+
const response_utils_1 = require("../utils/response-utils");
|
|
21
|
+
const encryption_utils_1 = require("../utils/encryption-utils");
|
|
22
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
23
|
+
/**
|
|
24
|
+
* Route configuration
|
|
25
|
+
* This handler is registered programmatically by the plugin
|
|
26
|
+
*/
|
|
27
|
+
exports.Route = {
|
|
28
|
+
path: "/oidc/:provider/callback",
|
|
29
|
+
method: flink_1.HttpMethod.get,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* OIDC Callback Handler
|
|
33
|
+
*
|
|
34
|
+
* Completes the OIDC flow by exchanging the authorization code for tokens,
|
|
35
|
+
* validating the ID token, building user profile, calling the app's onAuthSuccess
|
|
36
|
+
* callback to generate JWT token, and returning the token to the client.
|
|
37
|
+
*/
|
|
38
|
+
const CallbackOidc = async ({ ctx, req }) => {
|
|
39
|
+
const { provider } = req.params;
|
|
40
|
+
const { code, state, error: oidcError, error_description, response_type } = req.query;
|
|
41
|
+
try {
|
|
42
|
+
// Validate provider and response_type
|
|
43
|
+
(0, error_utils_1.validateProvider)(provider);
|
|
44
|
+
(0, error_utils_1.validateResponseType)(response_type);
|
|
45
|
+
// Check for OIDC provider errors (e.g., user denied access)
|
|
46
|
+
if (oidcError) {
|
|
47
|
+
const error = (0, error_utils_1.handleProviderError)({ error: oidcError, error_description });
|
|
48
|
+
// Call onAuthError callback if provided
|
|
49
|
+
const { options } = ctx.plugins.oidc;
|
|
50
|
+
if (options.onAuthError) {
|
|
51
|
+
const errorResult = await options.onAuthError({
|
|
52
|
+
error,
|
|
53
|
+
provider,
|
|
54
|
+
});
|
|
55
|
+
if (errorResult.redirectUrl) {
|
|
56
|
+
return {
|
|
57
|
+
status: 302,
|
|
58
|
+
headers: { Location: errorResult.redirectUrl },
|
|
59
|
+
data: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return (0, flink_1.badRequest)(error.message);
|
|
64
|
+
}
|
|
65
|
+
// Validate required parameters
|
|
66
|
+
if (!code || !state) {
|
|
67
|
+
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.MISSING_CODE, "Missing authorization code or state parameter", {
|
|
68
|
+
hasCode: !!code,
|
|
69
|
+
hasState: !!state,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Find OIDC session by state
|
|
73
|
+
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
74
|
+
if (!session) {
|
|
75
|
+
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.SESSION_EXPIRED, "OIDC session not found or expired. Please try logging in again.", { state });
|
|
76
|
+
}
|
|
77
|
+
// Validate state parameter (CSRF protection)
|
|
78
|
+
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
79
|
+
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
80
|
+
providedState: state.substring(0, 10) + "...",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Delete session immediately after validation (one-time use)
|
|
84
|
+
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
85
|
+
// Get plugin options
|
|
86
|
+
const { options } = ctx.plugins.oidc;
|
|
87
|
+
// Get provider instance
|
|
88
|
+
const providerRegistry = ctx.oidcProviderRegistry;
|
|
89
|
+
if (!providerRegistry) {
|
|
90
|
+
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "OIDC plugin not properly initialized");
|
|
91
|
+
}
|
|
92
|
+
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
93
|
+
// Exchange authorization code for tokens with PKCE validation
|
|
94
|
+
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
95
|
+
code,
|
|
96
|
+
codeVerifier: session.codeVerifier,
|
|
97
|
+
state: session.state,
|
|
98
|
+
nonce: session.nonce,
|
|
99
|
+
});
|
|
100
|
+
// Build user profile from ID token and UserInfo
|
|
101
|
+
const profile = await oidcProvider.buildProfile(tokenSet, true);
|
|
102
|
+
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
103
|
+
const authSuccessParams = {
|
|
104
|
+
profile,
|
|
105
|
+
claims: tokenSet.claims,
|
|
106
|
+
provider,
|
|
107
|
+
...(options.storeTokens ? { tokens: tokenSet } : {}),
|
|
108
|
+
};
|
|
109
|
+
let authResult;
|
|
110
|
+
try {
|
|
111
|
+
authResult = await options.onAuthSuccess(authSuccessParams, ctx);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Handle JWT generation or user creation errors
|
|
115
|
+
flink_1.log.error("OIDC onAuthSuccess callback failed:", error);
|
|
116
|
+
const oidcError = (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.JWT_GENERATION_FAILED, "Failed to complete authentication. Please try again.", {
|
|
117
|
+
originalError: error.message,
|
|
118
|
+
});
|
|
119
|
+
// Call onAuthError callback if provided
|
|
120
|
+
if (options.onAuthError) {
|
|
121
|
+
const errorResult = await options.onAuthError({
|
|
122
|
+
error: oidcError,
|
|
123
|
+
provider,
|
|
124
|
+
});
|
|
125
|
+
if (errorResult.redirectUrl) {
|
|
126
|
+
return {
|
|
127
|
+
status: 302,
|
|
128
|
+
headers: { Location: errorResult.redirectUrl },
|
|
129
|
+
data: {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return (0, flink_1.internalServerError)("Authentication failed. Please try again.");
|
|
134
|
+
}
|
|
135
|
+
// Extract user and JWT token from callback result
|
|
136
|
+
const { user, token, redirectUrl } = authResult;
|
|
137
|
+
if (!token) {
|
|
138
|
+
throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.JWT_GENERATION_FAILED, "No authentication token returned from callback", { hasUser: !!user });
|
|
139
|
+
}
|
|
140
|
+
// Store OIDC connection if token storage is enabled
|
|
141
|
+
if (options.storeTokens && user && user._id) {
|
|
142
|
+
// Get encryption secret (use provider client secret or global encryption key)
|
|
143
|
+
const staticProviderConfig = options.providers[provider];
|
|
144
|
+
const encryptionSecret = options.encryptionKey || staticProviderConfig?.clientSecret;
|
|
145
|
+
if (!encryptionSecret) {
|
|
146
|
+
flink_1.log.warn("No encryption secret available, tokens will not be stored");
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Encrypt tokens before storing
|
|
150
|
+
const encryptedAccessToken = (0, encryption_utils_1.encryptToken)(tokenSet.accessToken, encryptionSecret);
|
|
151
|
+
const encryptedIdToken = (0, encryption_utils_1.encryptToken)(tokenSet.idToken, encryptionSecret);
|
|
152
|
+
const encryptedRefreshToken = tokenSet.refreshToken ? (0, encryption_utils_1.encryptToken)(tokenSet.refreshToken, encryptionSecret) : undefined;
|
|
153
|
+
// Calculate token expiration
|
|
154
|
+
const expiresAt = tokenSet.expiresIn ? new Date(Date.now() + tokenSet.expiresIn * 1000) : undefined;
|
|
155
|
+
// Create or update OIDC connection
|
|
156
|
+
const existingConnection = await ctx.repos.oidcConnectionRepo.findByUserAndProvider(user._id, provider);
|
|
157
|
+
if (existingConnection) {
|
|
158
|
+
await ctx.repos.oidcConnectionRepo.updateOne(existingConnection._id, {
|
|
159
|
+
accessToken: encryptedAccessToken,
|
|
160
|
+
idToken: encryptedIdToken,
|
|
161
|
+
refreshToken: encryptedRefreshToken,
|
|
162
|
+
scope: tokenSet.scope || "",
|
|
163
|
+
expiresAt,
|
|
164
|
+
updatedAt: new Date(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await ctx.repos.oidcConnectionRepo.create({
|
|
169
|
+
userId: user._id,
|
|
170
|
+
provider,
|
|
171
|
+
subject: tokenSet.claims.sub,
|
|
172
|
+
issuer: tokenSet.claims.iss,
|
|
173
|
+
email: profile.email,
|
|
174
|
+
accessToken: encryptedAccessToken,
|
|
175
|
+
idToken: encryptedIdToken,
|
|
176
|
+
refreshToken: encryptedRefreshToken,
|
|
177
|
+
scope: tokenSet.scope || "",
|
|
178
|
+
expiresAt,
|
|
179
|
+
createdAt: new Date(),
|
|
180
|
+
updatedAt: new Date(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Return JWT token in requested format
|
|
186
|
+
return (0, response_utils_1.formatTokenResponse)(token, user, redirectUrl || session.redirectUri, response_type);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
flink_1.log.error("OIDC callback error:", error);
|
|
190
|
+
// Handle OIDC-specific errors
|
|
191
|
+
if (error.code && Object.values(error_utils_1.OidcErrorCodes).includes(error.code)) {
|
|
192
|
+
// Call onAuthError callback if provided
|
|
193
|
+
const { options } = ctx.plugins.oidc;
|
|
194
|
+
if (options.onAuthError) {
|
|
195
|
+
try {
|
|
196
|
+
const errorResult = await options.onAuthError({
|
|
197
|
+
error,
|
|
198
|
+
provider,
|
|
199
|
+
});
|
|
200
|
+
if (errorResult.redirectUrl) {
|
|
201
|
+
return {
|
|
202
|
+
status: 302,
|
|
203
|
+
headers: { Location: errorResult.redirectUrl },
|
|
204
|
+
data: {},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (callbackError) {
|
|
209
|
+
flink_1.log.error("onAuthError callback failed:", callbackError);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return (0, flink_1.badRequest)(error.message);
|
|
213
|
+
}
|
|
214
|
+
// Handle provider errors
|
|
215
|
+
const mappedError = (0, error_utils_1.handleProviderError)(error);
|
|
216
|
+
return (0, flink_1.internalServerError)(mappedError.message);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
exports.default = CallbackOidc;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Initiate Handler
|
|
3
|
+
*
|
|
4
|
+
* Initiates the OIDC authorization code flow by:
|
|
5
|
+
* 1. Validating the provider is supported and configured
|
|
6
|
+
* 2. Generating cryptographically secure state, code_verifier, and nonce
|
|
7
|
+
* 3. Creating an OIDC session to track the flow
|
|
8
|
+
* 4. Building the provider's authorization URL with PKCE
|
|
9
|
+
* 5. Redirecting the user to the provider for authorization
|
|
10
|
+
*
|
|
11
|
+
* Route: GET /oidc/:provider/initiate?redirectUri={optional_redirect_url}
|
|
12
|
+
*/
|
|
13
|
+
import { GetHandler, RouteProps } from "@flink-app/flink";
|
|
14
|
+
import InitiateRequest from "../schemas/InitiateRequest";
|
|
15
|
+
/**
|
|
16
|
+
* Path parameters for the handler
|
|
17
|
+
*/
|
|
18
|
+
interface PathParams {
|
|
19
|
+
provider: string;
|
|
20
|
+
[key: string]: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Route configuration
|
|
24
|
+
* This handler is registered programmatically by the plugin
|
|
25
|
+
*/
|
|
26
|
+
export declare const Route: RouteProps;
|
|
27
|
+
/**
|
|
28
|
+
* OIDC Initiate Handler
|
|
29
|
+
*
|
|
30
|
+
* Starts the OIDC flow by generating security parameters, creating a session,
|
|
31
|
+
* and redirecting to the OIDC provider's authorization URL.
|
|
32
|
+
*/
|
|
33
|
+
declare const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest>;
|
|
34
|
+
export default InitiateOidc;
|
|
35
|
+
//# sourceMappingURL=InitiateOidc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InitiateOidc.d.ts","sourceRoot":"","sources":["../../src/handlers/InitiateOidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAc,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AACvG,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAKzD;;GAEG;AACH,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,UAGnB,CAAC;AAEF;;;;;GAKG;AACH,QAAA,MAAM,YAAY,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,CAiEnE,CAAC;AAEF,eAAe,YAAY,CAAC"}
|