integrate-sdk 0.3.6 → 0.3.7
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/dist/index.js +0 -102
- package/dist/src/adapters/nextjs-callback.d.ts +6 -2
- package/dist/src/adapters/nextjs-callback.d.ts.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/adapters/auto-routes.ts +217 -0
- package/src/adapters/base-handler.ts +212 -0
- package/src/adapters/nextjs-callback.tsx +160 -0
- package/src/adapters/nextjs.ts +318 -0
- package/src/adapters/tanstack-start.ts +264 -0
- package/src/client.ts +952 -0
- package/src/config/types.ts +180 -0
- package/src/errors.ts +207 -0
- package/src/index.ts +110 -0
- package/src/integrations/vercel-ai.ts +104 -0
- package/src/oauth/manager.ts +307 -0
- package/src/oauth/pkce.ts +127 -0
- package/src/oauth/types.ts +101 -0
- package/src/oauth/window-manager.ts +322 -0
- package/src/plugins/generic.ts +119 -0
- package/src/plugins/github-client.ts +345 -0
- package/src/plugins/github.ts +122 -0
- package/src/plugins/gmail-client.ts +114 -0
- package/src/plugins/gmail.ts +108 -0
- package/src/plugins/server-client.ts +20 -0
- package/src/plugins/types.ts +89 -0
- package/src/protocol/jsonrpc.ts +88 -0
- package/src/protocol/messages.ts +145 -0
- package/src/server.ts +117 -0
- package/src/transport/http-session.ts +322 -0
- package/src/transport/http-stream.ts +331 -0
- package/src/utils/naming.ts +52 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js OAuth Callback Handler
|
|
3
|
+
* Provides a pre-built OAuth callback page component for Next.js App Router
|
|
4
|
+
*
|
|
5
|
+
* This eliminates the need for users to manually create callback pages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
import type { OAuthCallbackHandlerConfig } from '../oauth/types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OAuth Callback Page Component
|
|
15
|
+
*
|
|
16
|
+
* This component:
|
|
17
|
+
* 1. Extracts OAuth callback parameters (code, state, error) from URL
|
|
18
|
+
* 2. Sends them to the opener window (for popup mode) via postMessage
|
|
19
|
+
* 3. Stores them in sessionStorage (for redirect mode)
|
|
20
|
+
* 4. Redirects to the configured URL
|
|
21
|
+
*
|
|
22
|
+
* @param config - Callback handler configuration
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // app/oauth/callback/page.tsx
|
|
27
|
+
* import { OAuthCallbackPage } from 'integrate-sdk/oauth-callback';
|
|
28
|
+
*
|
|
29
|
+
* export default function CallbackPage() {
|
|
30
|
+
* return <OAuthCallbackPage redirectUrl="/dashboard" />;
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function OAuthCallbackPage(config?: OAuthCallbackHandlerConfig) {
|
|
35
|
+
const redirectUrl = config?.redirectUrl || '/';
|
|
36
|
+
const errorRedirectUrl = config?.errorRedirectUrl || '/auth-error';
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const params = new URLSearchParams(window.location.search);
|
|
40
|
+
const code = params.get('code');
|
|
41
|
+
const state = params.get('state');
|
|
42
|
+
const error = params.get('error');
|
|
43
|
+
const errorDescription = params.get('error_description');
|
|
44
|
+
|
|
45
|
+
// Handle error case
|
|
46
|
+
if (error) {
|
|
47
|
+
const errorMsg = errorDescription || error;
|
|
48
|
+
console.error('[OAuth Callback] Error:', errorMsg);
|
|
49
|
+
window.location.href = `${errorRedirectUrl}?error=${encodeURIComponent(errorMsg)}`;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate required params
|
|
54
|
+
if (!code || !state) {
|
|
55
|
+
console.error('[OAuth Callback] Missing code or state parameter');
|
|
56
|
+
window.location.href = `${errorRedirectUrl}?error=${encodeURIComponent('Invalid OAuth callback')}`;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// For popup mode: send message to opener window
|
|
61
|
+
if (window.opener) {
|
|
62
|
+
// Send message immediately
|
|
63
|
+
window.opener.postMessage(
|
|
64
|
+
{
|
|
65
|
+
type: 'oauth_callback',
|
|
66
|
+
code,
|
|
67
|
+
state,
|
|
68
|
+
},
|
|
69
|
+
'*'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Close popup after a brief delay to ensure message is received
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
window.close();
|
|
75
|
+
}, 100);
|
|
76
|
+
} else {
|
|
77
|
+
// For redirect mode: store in sessionStorage and redirect
|
|
78
|
+
try {
|
|
79
|
+
sessionStorage.setItem(
|
|
80
|
+
'oauth_callback_params',
|
|
81
|
+
JSON.stringify({ code, state })
|
|
82
|
+
);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error('Failed to store OAuth callback params:', e);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Redirect to the configured URL
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
window.location.href = redirectUrl;
|
|
90
|
+
}, 500);
|
|
91
|
+
}
|
|
92
|
+
}, [redirectUrl, errorRedirectUrl]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
fontFamily:
|
|
98
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
minHeight: '100vh',
|
|
103
|
+
margin: 0,
|
|
104
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
105
|
+
color: 'white',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
|
109
|
+
<div
|
|
110
|
+
style={{
|
|
111
|
+
border: '3px solid rgba(255, 255, 255, 0.3)',
|
|
112
|
+
borderRadius: '50%',
|
|
113
|
+
borderTop: '3px solid white',
|
|
114
|
+
width: '40px',
|
|
115
|
+
height: '40px',
|
|
116
|
+
animation: 'spin 1s linear infinite',
|
|
117
|
+
margin: '0 auto 1rem',
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
<h1
|
|
121
|
+
style={{
|
|
122
|
+
margin: '0 0 0.5rem',
|
|
123
|
+
fontSize: '1.5rem',
|
|
124
|
+
fontWeight: 600,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
Authorization Complete
|
|
128
|
+
</h1>
|
|
129
|
+
<p style={{ margin: 0, opacity: 0.9, fontSize: '0.875rem' }}>
|
|
130
|
+
This window will close automatically...
|
|
131
|
+
</p>
|
|
132
|
+
<style>{`
|
|
133
|
+
@keyframes spin {
|
|
134
|
+
0% { transform: rotate(0deg); }
|
|
135
|
+
100% { transform: rotate(360deg); }
|
|
136
|
+
}
|
|
137
|
+
`}</style>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a default export wrapper for easier usage
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```tsx
|
|
148
|
+
* // app/oauth/callback/page.tsx
|
|
149
|
+
* import { createOAuthCallbackPage } from 'integrate-sdk/oauth-callback';
|
|
150
|
+
*
|
|
151
|
+
* export default createOAuthCallbackPage({
|
|
152
|
+
* redirectUrl: '/dashboard',
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function createOAuthCallbackPage(config?: OAuthCallbackHandlerConfig) {
|
|
157
|
+
return function OAuthCallback() {
|
|
158
|
+
return <OAuthCallbackPage {...config} />;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js OAuth Route Adapter
|
|
3
|
+
* Provides OAuth route handlers for Next.js App Router
|
|
4
|
+
*
|
|
5
|
+
* Note: This file uses type imports only to avoid requiring Next.js at build time.
|
|
6
|
+
* The actual Next.js types are used at runtime when available.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { OAuthHandler, type OAuthHandlerConfig } from './base-handler.js';
|
|
10
|
+
|
|
11
|
+
// Type-only imports to avoid requiring Next.js at build time
|
|
12
|
+
type NextRequest = any;
|
|
13
|
+
type NextResponse = any;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create Next.js OAuth route handlers
|
|
17
|
+
*
|
|
18
|
+
* Use this to create secure OAuth API routes in your Next.js application
|
|
19
|
+
* that handle authorization with server-side secrets.
|
|
20
|
+
*
|
|
21
|
+
* @param config - OAuth handler configuration with provider credentials
|
|
22
|
+
* @returns Object with authorize, callback, and status route handlers, plus a unified handler
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* **Simple Setup (Recommended)** - One route file handles everything:
|
|
26
|
+
*
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // app/api/integrate/oauth/[action]/route.ts
|
|
29
|
+
* import { createNextOAuthHandler } from 'integrate-sdk';
|
|
30
|
+
*
|
|
31
|
+
* const handler = createNextOAuthHandler({
|
|
32
|
+
* providers: {
|
|
33
|
+
* github: {
|
|
34
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
35
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
36
|
+
* },
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* export const { POST, GET } = handler.createRoutes();
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* **Advanced Setup** - Separate routes for each action:
|
|
45
|
+
*
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // app/api/integrate/oauth/authorize/route.ts
|
|
48
|
+
* export const POST = handler.authorize;
|
|
49
|
+
*
|
|
50
|
+
* // app/api/integrate/oauth/callback/route.ts
|
|
51
|
+
* export const POST = handler.callback;
|
|
52
|
+
*
|
|
53
|
+
* // app/api/integrate/oauth/status/route.ts
|
|
54
|
+
* export const GET = handler.status;
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function createNextOAuthHandler(config: OAuthHandlerConfig) {
|
|
58
|
+
const handler = new OAuthHandler(config);
|
|
59
|
+
|
|
60
|
+
const handlers = {
|
|
61
|
+
/**
|
|
62
|
+
* POST /api/integrate/oauth/authorize
|
|
63
|
+
*
|
|
64
|
+
* Request authorization URL from MCP server with server-side OAuth credentials
|
|
65
|
+
*
|
|
66
|
+
* Request body:
|
|
67
|
+
* ```json
|
|
68
|
+
* {
|
|
69
|
+
* "provider": "github",
|
|
70
|
+
* "scopes": ["repo", "user"],
|
|
71
|
+
* "state": "random-state-string",
|
|
72
|
+
* "codeChallenge": "pkce-code-challenge",
|
|
73
|
+
* "codeChallengeMethod": "S256",
|
|
74
|
+
* "redirectUri": "https://yourapp.com/oauth/callback"
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* Response:
|
|
79
|
+
* ```json
|
|
80
|
+
* {
|
|
81
|
+
* "authorizationUrl": "https://github.com/login/oauth/authorize?..."
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* // app/api/integrate/oauth/authorize/route.ts
|
|
88
|
+
* import { createNextOAuthHandler } from 'integrate-sdk';
|
|
89
|
+
*
|
|
90
|
+
* const handler = createNextOAuthHandler({
|
|
91
|
+
* * providers: {
|
|
92
|
+
* github: {
|
|
93
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
94
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
95
|
+
* },
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* export const POST = handler.authorize;
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
async authorize(req: NextRequest): Promise<NextResponse> {
|
|
103
|
+
try {
|
|
104
|
+
const body = await req.json();
|
|
105
|
+
const result = await handler.handleAuthorize(body);
|
|
106
|
+
return Response.json(result);
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
console.error('[OAuth Authorize] Error:', error);
|
|
109
|
+
return Response.json(
|
|
110
|
+
{ error: error.message || 'Failed to get authorization URL' },
|
|
111
|
+
{ status: 500 }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* POST /api/integrate/oauth/callback
|
|
118
|
+
*
|
|
119
|
+
* Exchange authorization code for session token
|
|
120
|
+
*
|
|
121
|
+
* Request body:
|
|
122
|
+
* ```json
|
|
123
|
+
* {
|
|
124
|
+
* "provider": "github",
|
|
125
|
+
* "code": "authorization-code",
|
|
126
|
+
* "codeVerifier": "pkce-code-verifier",
|
|
127
|
+
* "state": "state-from-authorize"
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* Response:
|
|
132
|
+
* ```json
|
|
133
|
+
* {
|
|
134
|
+
* "sessionToken": "session-token-123",
|
|
135
|
+
* "provider": "github",
|
|
136
|
+
* "scopes": ["repo", "user"],
|
|
137
|
+
* "expiresAt": 1234567890
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* // app/api/integrate/oauth/callback/route.ts
|
|
144
|
+
* import { createNextOAuthHandler } from 'integrate-sdk';
|
|
145
|
+
*
|
|
146
|
+
* const handler = createNextOAuthHandler({
|
|
147
|
+
* * providers: {
|
|
148
|
+
* github: {
|
|
149
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
150
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
151
|
+
* },
|
|
152
|
+
* },
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* export const POST = handler.callback;
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
async callback(req: NextRequest): Promise<NextResponse> {
|
|
159
|
+
try {
|
|
160
|
+
const body = await req.json();
|
|
161
|
+
const result = await handler.handleCallback(body);
|
|
162
|
+
return Response.json(result);
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
console.error('[OAuth Callback] Error:', error);
|
|
165
|
+
return Response.json(
|
|
166
|
+
{ error: error.message || 'Failed to exchange authorization code' },
|
|
167
|
+
{ status: 500 }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* GET /api/integrate/oauth/status?provider=github
|
|
174
|
+
*
|
|
175
|
+
* Check if a provider is currently authorized
|
|
176
|
+
*
|
|
177
|
+
* Query parameters:
|
|
178
|
+
* - provider: Provider to check (e.g., "github")
|
|
179
|
+
*
|
|
180
|
+
* Headers:
|
|
181
|
+
* - X-Session-Token: Session token from previous authorization
|
|
182
|
+
*
|
|
183
|
+
* Response:
|
|
184
|
+
* ```json
|
|
185
|
+
* {
|
|
186
|
+
* "authorized": true,
|
|
187
|
+
* "provider": "github",
|
|
188
|
+
* "scopes": ["repo", "user"],
|
|
189
|
+
* "expiresAt": 1234567890
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* // app/api/integrate/oauth/status/route.ts
|
|
196
|
+
* import { createNextOAuthHandler } from 'integrate-sdk';
|
|
197
|
+
*
|
|
198
|
+
* const handler = createNextOAuthHandler({
|
|
199
|
+
* * providers: {
|
|
200
|
+
* github: {
|
|
201
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
202
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
203
|
+
* },
|
|
204
|
+
* },
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* export const GET = handler.status;
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
async status(req: NextRequest): Promise<NextResponse> {
|
|
211
|
+
try {
|
|
212
|
+
const provider = req.nextUrl.searchParams.get('provider');
|
|
213
|
+
const sessionToken = req.headers.get('x-session-token');
|
|
214
|
+
|
|
215
|
+
if (!provider) {
|
|
216
|
+
return Response.json(
|
|
217
|
+
{ error: 'Missing provider query parameter' },
|
|
218
|
+
{ status: 400 }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!sessionToken) {
|
|
223
|
+
return Response.json(
|
|
224
|
+
{ error: 'Missing X-Session-Token header' },
|
|
225
|
+
{ status: 400 }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = await handler.handleStatus(provider, sessionToken);
|
|
230
|
+
return Response.json(result);
|
|
231
|
+
} catch (error: any) {
|
|
232
|
+
console.error('[OAuth Status] Error:', error);
|
|
233
|
+
return Response.json(
|
|
234
|
+
{ error: error.message || 'Failed to check authorization status' },
|
|
235
|
+
{ status: 500 }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create unified route handlers for catch-all route
|
|
242
|
+
*
|
|
243
|
+
* This is the simplest way to set up OAuth routes - create a single catch-all
|
|
244
|
+
* route file that handles all OAuth actions.
|
|
245
|
+
*
|
|
246
|
+
* @returns Object with POST and GET handlers for Next.js dynamic routes
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* // app/api/integrate/oauth/[action]/route.ts
|
|
251
|
+
* import { createNextOAuthHandler } from 'integrate-sdk';
|
|
252
|
+
*
|
|
253
|
+
* const handler = createNextOAuthHandler({
|
|
254
|
+
* providers: {
|
|
255
|
+
* github: {
|
|
256
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
257
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
258
|
+
* },
|
|
259
|
+
* },
|
|
260
|
+
* });
|
|
261
|
+
*
|
|
262
|
+
* export const { POST, GET } = handler.createRoutes();
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
createRoutes() {
|
|
266
|
+
return {
|
|
267
|
+
/**
|
|
268
|
+
* POST handler for authorize and callback actions
|
|
269
|
+
*/
|
|
270
|
+
async POST(
|
|
271
|
+
req: NextRequest,
|
|
272
|
+
context: { params: { action: string } | Promise<{ action: string }> }
|
|
273
|
+
): Promise<NextResponse> {
|
|
274
|
+
// Handle both Next.js 14 (sync params) and Next.js 15+ (async params)
|
|
275
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
276
|
+
const action = params.action;
|
|
277
|
+
|
|
278
|
+
if (action === 'authorize') {
|
|
279
|
+
return handlers.authorize(req);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (action === 'callback') {
|
|
283
|
+
return handlers.callback(req);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Response.json(
|
|
287
|
+
{ error: `Unknown action: ${action}` },
|
|
288
|
+
{ status: 404 }
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* GET handler for status action
|
|
294
|
+
*/
|
|
295
|
+
async GET(
|
|
296
|
+
req: NextRequest,
|
|
297
|
+
context: { params: { action: string } | Promise<{ action: string }> }
|
|
298
|
+
): Promise<NextResponse> {
|
|
299
|
+
// Handle both Next.js 14 (sync params) and Next.js 15+ (async params)
|
|
300
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
301
|
+
const action = params.action;
|
|
302
|
+
|
|
303
|
+
if (action === 'status') {
|
|
304
|
+
return handlers.status(req);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return Response.json(
|
|
308
|
+
{ error: `Unknown action: ${action}` },
|
|
309
|
+
{ status: 404 }
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return handlers;
|
|
317
|
+
}
|
|
318
|
+
|