@agentuity/auth 0.0.100 → 0.0.102
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/AGENTS.md +27 -558
- package/dist/auth0/client.d.ts +44 -0
- package/dist/auth0/client.d.ts.map +1 -0
- package/dist/auth0/client.js +79 -0
- package/dist/auth0/client.js.map +1 -0
- package/dist/auth0/index.d.ts +35 -0
- package/dist/auth0/index.d.ts.map +1 -0
- package/dist/auth0/index.js +38 -0
- package/dist/auth0/index.js.map +1 -0
- package/dist/auth0/server.d.ts +91 -0
- package/dist/auth0/server.d.ts.map +1 -0
- package/dist/auth0/server.js +237 -0
- package/dist/auth0/server.js.map +1 -0
- package/dist/clerk/index.d.ts +1 -1
- package/dist/clerk/index.d.ts.map +1 -1
- package/dist/clerk/server.d.ts +9 -10
- package/dist/clerk/server.d.ts.map +1 -1
- package/dist/clerk/server.js +3 -2
- package/dist/clerk/server.js.map +1 -1
- package/docs/adding-providers.md +260 -0
- package/package.json +27 -8
- package/src/auth0/client.tsx +109 -0
- package/src/auth0/index.ts +40 -0
- package/src/auth0/server.ts +378 -0
- package/src/clerk/index.ts +1 -1
- package/src/clerk/server.ts +13 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 server-side authentication middleware for Hono.
|
|
3
|
+
*
|
|
4
|
+
* @module auth0/server
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createMiddleware as createHonoMiddleware } from 'hono/factory';
|
|
8
|
+
import jwt from 'jsonwebtoken';
|
|
9
|
+
import jwksClient from 'jwks-rsa';
|
|
10
|
+
import type { AgentuityAuth, AgentuityAuthUser } from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Environment type for Auth0 middleware - provides typed context variables.
|
|
14
|
+
*/
|
|
15
|
+
export type Auth0Env = {
|
|
16
|
+
Variables: {
|
|
17
|
+
auth: AgentuityAuth<Auth0User, Auth0JWTPayload>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Auth0 JWT payload structure.
|
|
23
|
+
*/
|
|
24
|
+
export interface Auth0JWTPayload {
|
|
25
|
+
/** Subject (user ID) */
|
|
26
|
+
sub: string;
|
|
27
|
+
/** Email address */
|
|
28
|
+
email?: string;
|
|
29
|
+
/** Email verification status */
|
|
30
|
+
email_verified?: boolean;
|
|
31
|
+
/** Full name */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Given name */
|
|
34
|
+
given_name?: string;
|
|
35
|
+
/** Family name */
|
|
36
|
+
family_name?: string;
|
|
37
|
+
/** Picture URL */
|
|
38
|
+
picture?: string;
|
|
39
|
+
/** Additional claims */
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Auth0 user info from Management API.
|
|
45
|
+
*/
|
|
46
|
+
export interface Auth0User {
|
|
47
|
+
/** User ID */
|
|
48
|
+
user_id: string;
|
|
49
|
+
/** Email address */
|
|
50
|
+
email?: string;
|
|
51
|
+
/** Email verification status */
|
|
52
|
+
email_verified?: boolean;
|
|
53
|
+
/** Full name */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Given name */
|
|
56
|
+
given_name?: string;
|
|
57
|
+
/** Family name */
|
|
58
|
+
family_name?: string;
|
|
59
|
+
/** Picture URL */
|
|
60
|
+
picture?: string;
|
|
61
|
+
/** Additional user metadata */
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for Auth0 middleware.
|
|
67
|
+
*/
|
|
68
|
+
export interface Auth0MiddlewareOptions {
|
|
69
|
+
/** Auth0 domain (defaults to process.env.AUTH0_DOMAIN) */
|
|
70
|
+
domain?: string;
|
|
71
|
+
|
|
72
|
+
/** Auth0 audience/API identifier (defaults to process.env.AUTH0_AUDIENCE) */
|
|
73
|
+
audience?: string;
|
|
74
|
+
|
|
75
|
+
/** Auth0 issuer (defaults to https://{domain}/) */
|
|
76
|
+
issuer?: string;
|
|
77
|
+
|
|
78
|
+
/** Custom token extractor function */
|
|
79
|
+
getToken?: (authHeader: string) => string;
|
|
80
|
+
|
|
81
|
+
/** Whether to fetch full user profile from Management API (requires AUTH0_M2M_CLIENT_ID and AUTH0_M2M_CLIENT_SECRET) */
|
|
82
|
+
fetchUserProfile?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create Hono middleware for Auth0 authentication.
|
|
87
|
+
*
|
|
88
|
+
* This middleware:
|
|
89
|
+
* - Extracts and validates JWT tokens from Authorization header
|
|
90
|
+
* - Returns 401 if token is missing or invalid
|
|
91
|
+
* - Exposes authenticated user via c.var.auth
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { createMiddleware } from '@agentuity/auth/auth0';
|
|
96
|
+
*
|
|
97
|
+
* router.get('/api/profile', createMiddleware(), async (c) => {
|
|
98
|
+
* const user = await c.var.auth.getUser();
|
|
99
|
+
* return c.json({ email: user.email });
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function createMiddleware(options: Auth0MiddlewareOptions = {}) {
|
|
104
|
+
const domain =
|
|
105
|
+
options.domain || process.env.AGENTUITY_PUBLIC_AUTH0_DOMAIN || process.env.AUTH0_DOMAIN;
|
|
106
|
+
const audience =
|
|
107
|
+
options.audience || process.env.AGENTUITY_PUBLIC_AUTH0_AUDIENCE || process.env.AUTH0_AUDIENCE;
|
|
108
|
+
const issuer = options.issuer || (domain ? `https://${domain}/` : undefined);
|
|
109
|
+
|
|
110
|
+
if (!domain) {
|
|
111
|
+
console.error(
|
|
112
|
+
'[Auth0 Auth] AUTH0_DOMAIN is not set. Add it to your .env file or pass domain option to createMiddleware()'
|
|
113
|
+
);
|
|
114
|
+
throw new Error('Auth0 domain is required (set AUTH0_DOMAIN or pass domain option)');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!issuer) {
|
|
118
|
+
throw new Error('Auth0 issuer is required');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create JWKS client for fetching signing keys
|
|
122
|
+
const client = jwksClient({
|
|
123
|
+
jwksUri: `https://${domain}/.well-known/jwks.json`,
|
|
124
|
+
cache: true,
|
|
125
|
+
cacheMaxAge: 86400000, // 24 hours
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Get signing key function for jwt.verify
|
|
129
|
+
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
|
|
130
|
+
if (!header.kid) {
|
|
131
|
+
callback(new Error('No kid in token header'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
client.getSigningKey(header.kid, (err: Error | null, key?: jwksClient.SigningKey) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
callback(err);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!key) {
|
|
140
|
+
callback(new Error('No signing key found'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const signingKey = key.getPublicKey();
|
|
144
|
+
callback(null, signingKey);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return createHonoMiddleware<Auth0Env>(async (c, next) => {
|
|
149
|
+
const authHeader = c.req.header('Authorization');
|
|
150
|
+
|
|
151
|
+
if (!authHeader) {
|
|
152
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Extract token from Bearer header
|
|
157
|
+
let token: string;
|
|
158
|
+
if (options.getToken) {
|
|
159
|
+
token = options.getToken(authHeader);
|
|
160
|
+
} else {
|
|
161
|
+
// Validate Authorization scheme is Bearer
|
|
162
|
+
if (!authHeader.match(/^Bearer\s+/i)) {
|
|
163
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
164
|
+
}
|
|
165
|
+
token = authHeader.replace(/^Bearer\s+/i, '');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Ensure token is not empty
|
|
169
|
+
if (!token || token.trim().length === 0) {
|
|
170
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Verify token with Auth0
|
|
174
|
+
const verifyOptions: jwt.VerifyOptions = {
|
|
175
|
+
issuer,
|
|
176
|
+
algorithms: ['RS256'],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Only validate audience if it's configured
|
|
180
|
+
if (audience) {
|
|
181
|
+
verifyOptions.audience = audience;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const payload = await new Promise<Auth0JWTPayload>((resolve, reject) => {
|
|
185
|
+
jwt.verify(
|
|
186
|
+
token,
|
|
187
|
+
getKey,
|
|
188
|
+
verifyOptions,
|
|
189
|
+
(err: jwt.VerifyErrors | null, decoded: string | jwt.JwtPayload | undefined) => {
|
|
190
|
+
if (err) {
|
|
191
|
+
reject(err);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!decoded || typeof decoded !== 'object') {
|
|
195
|
+
reject(new Error('Invalid token payload'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
resolve(decoded as Auth0JWTPayload);
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Validate payload has required subject claim
|
|
204
|
+
if (!payload.sub || typeof payload.sub !== 'string') {
|
|
205
|
+
throw new Error('Invalid token: missing or invalid subject claim');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Memoize user fetch to avoid multiple API calls
|
|
209
|
+
let cachedUser: AgentuityAuthUser<Auth0User> | null = null;
|
|
210
|
+
|
|
211
|
+
// Create auth object with Auth0 payload types
|
|
212
|
+
const auth: AgentuityAuth<Auth0User, Auth0JWTPayload> = {
|
|
213
|
+
async getUser() {
|
|
214
|
+
if (cachedUser) {
|
|
215
|
+
return cachedUser;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// If fetchUserProfile is enabled, fetch from Management API (more complete data)
|
|
219
|
+
if (options.fetchUserProfile) {
|
|
220
|
+
const user = await fetchUserFromManagementAPI(payload.sub);
|
|
221
|
+
cachedUser = mapAuth0UserToAgentuityUser(user);
|
|
222
|
+
} else {
|
|
223
|
+
// Fetch from /userinfo endpoint (access token has openid scope)
|
|
224
|
+
const user = await fetchUserFromUserInfo(domain!, token);
|
|
225
|
+
cachedUser = mapAuth0UserToAgentuityUser(user);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return cachedUser;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async getToken() {
|
|
232
|
+
return token;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
raw: payload,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
c.set('auth', auth);
|
|
239
|
+
await next();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const hasErrorCode =
|
|
242
|
+
error && typeof error === 'object' && 'code' in error && typeof error.code === 'string';
|
|
243
|
+
console.error('[Auth0 Auth] Authentication failed', { hasErrorCode });
|
|
244
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Fetch user info from Auth0 /userinfo endpoint using access token.
|
|
251
|
+
*/
|
|
252
|
+
async function fetchUserFromUserInfo(domain: string, accessToken: string): Promise<Auth0User> {
|
|
253
|
+
const response = await fetch(`https://${domain}/userinfo`, {
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${accessToken}`,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
throw new Error(`Failed to fetch user info: ${response.status}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const userInfo = (await response.json()) as {
|
|
264
|
+
sub: string;
|
|
265
|
+
email?: string;
|
|
266
|
+
email_verified?: boolean;
|
|
267
|
+
name?: string;
|
|
268
|
+
given_name?: string;
|
|
269
|
+
family_name?: string;
|
|
270
|
+
picture?: string;
|
|
271
|
+
[key: string]: unknown;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
user_id: userInfo.sub,
|
|
276
|
+
email: userInfo.email,
|
|
277
|
+
email_verified: userInfo.email_verified,
|
|
278
|
+
name: userInfo.name,
|
|
279
|
+
given_name: userInfo.given_name,
|
|
280
|
+
family_name: userInfo.family_name,
|
|
281
|
+
picture: userInfo.picture,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Map Auth0 User to AgentuityAuthUser.
|
|
287
|
+
*/
|
|
288
|
+
function mapAuth0UserToAgentuityUser(user: Auth0User): AgentuityAuthUser<Auth0User> {
|
|
289
|
+
return {
|
|
290
|
+
id: user.user_id,
|
|
291
|
+
name:
|
|
292
|
+
user.name ||
|
|
293
|
+
(user.given_name && user.family_name
|
|
294
|
+
? `${user.given_name} ${user.family_name}`.trim()
|
|
295
|
+
: user.given_name || user.family_name),
|
|
296
|
+
email: user.email,
|
|
297
|
+
raw: user,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// M2M token cache to avoid fetching on every request
|
|
302
|
+
let cachedM2MToken: { token: string; expiresAt: number } | null = null;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get M2M access token for Management API, with caching.
|
|
306
|
+
*/
|
|
307
|
+
async function getM2MAccessToken(
|
|
308
|
+
domain: string,
|
|
309
|
+
clientId: string,
|
|
310
|
+
clientSecret: string
|
|
311
|
+
): Promise<string> {
|
|
312
|
+
// Return cached token if still valid (with 60s buffer before expiry)
|
|
313
|
+
if (cachedM2MToken && Date.now() < cachedM2MToken.expiresAt - 60000) {
|
|
314
|
+
return cachedM2MToken.token;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const tokenResponse = await fetch(`https://${domain}/oauth/token`, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
client_id: clientId,
|
|
322
|
+
client_secret: clientSecret,
|
|
323
|
+
audience: `https://${domain}/api/v2/`,
|
|
324
|
+
grant_type: 'client_credentials',
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!tokenResponse.ok) {
|
|
329
|
+
throw new Error('Failed to get Management API access token');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { access_token, expires_in } = (await tokenResponse.json()) as {
|
|
333
|
+
access_token: string;
|
|
334
|
+
expires_in: number;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
cachedM2MToken = {
|
|
338
|
+
token: access_token,
|
|
339
|
+
expiresAt: Date.now() + expires_in * 1000,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return access_token;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Fetch user profile from Auth0 Management API.
|
|
347
|
+
*/
|
|
348
|
+
async function fetchUserFromManagementAPI(userId: string): Promise<Auth0User> {
|
|
349
|
+
const clientId = process.env.AUTH0_M2M_CLIENT_ID;
|
|
350
|
+
const clientSecret = process.env.AUTH0_M2M_CLIENT_SECRET;
|
|
351
|
+
const domain = process.env.AGENTUITY_PUBLIC_AUTH0_DOMAIN || process.env.AUTH0_DOMAIN;
|
|
352
|
+
|
|
353
|
+
if (!clientId || !clientSecret || !domain) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
'AUTH0_M2M_CLIENT_ID, AUTH0_M2M_CLIENT_SECRET, and AUTH0_DOMAIN must be set to fetch user profile'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get cached or fresh Management API access token
|
|
360
|
+
const accessToken = await getM2MAccessToken(domain, clientId, clientSecret);
|
|
361
|
+
|
|
362
|
+
// Fetch user from Management API
|
|
363
|
+
const userResponse = await fetch(
|
|
364
|
+
`https://${domain}/api/v2/users/${encodeURIComponent(userId)}`,
|
|
365
|
+
{
|
|
366
|
+
headers: {
|
|
367
|
+
Authorization: `Bearer ${accessToken}`,
|
|
368
|
+
'Content-Type': 'application/json',
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (!userResponse.ok) {
|
|
374
|
+
throw new Error('Failed to fetch user from Management API');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return (await userResponse.json()) as Auth0User;
|
|
378
|
+
}
|
package/src/clerk/index.ts
CHANGED
|
@@ -34,4 +34,4 @@
|
|
|
34
34
|
export { AgentuityClerk } from './client';
|
|
35
35
|
export type { AgentuityClerkProps } from './client';
|
|
36
36
|
export { createMiddleware } from './server';
|
|
37
|
-
export type { ClerkMiddlewareOptions, ClerkJWTPayload } from './server';
|
|
37
|
+
export type { ClerkMiddlewareOptions, ClerkJWTPayload, ClerkEnv } from './server';
|
package/src/clerk/server.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module clerk/server
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { createMiddleware as createHonoMiddleware } from 'hono/factory';
|
|
8
8
|
import { createClerkClient, verifyToken } from '@clerk/backend';
|
|
9
9
|
import type { User } from '@clerk/backend';
|
|
10
10
|
import type { AgentuityAuth, AgentuityAuthUser } from '../types';
|
|
@@ -19,6 +19,15 @@ export interface ClerkJWTPayload {
|
|
|
19
19
|
[key: string]: unknown;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Environment type for Clerk middleware - provides typed context variables.
|
|
24
|
+
*/
|
|
25
|
+
export type ClerkEnv = {
|
|
26
|
+
Variables: {
|
|
27
|
+
auth: AgentuityAuth<User, ClerkJWTPayload>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* Options for Clerk middleware.
|
|
24
33
|
*/
|
|
@@ -51,7 +60,7 @@ export interface ClerkMiddlewareOptions {
|
|
|
51
60
|
* });
|
|
52
61
|
* ```
|
|
53
62
|
*/
|
|
54
|
-
export function createMiddleware(options: ClerkMiddlewareOptions = {})
|
|
63
|
+
export function createMiddleware(options: ClerkMiddlewareOptions = {}) {
|
|
55
64
|
const secretKey = options.secretKey || process.env.CLERK_SECRET_KEY;
|
|
56
65
|
const publishableKey =
|
|
57
66
|
options.publishableKey ||
|
|
@@ -76,7 +85,7 @@ export function createMiddleware(options: ClerkMiddlewareOptions = {}): Middlewa
|
|
|
76
85
|
// Create Clerk client instance
|
|
77
86
|
const clerkClient = createClerkClient({ secretKey });
|
|
78
87
|
|
|
79
|
-
return async (c, next) => {
|
|
88
|
+
return createHonoMiddleware<ClerkEnv>(async (c, next) => {
|
|
80
89
|
const authHeader = c.req.header('Authorization');
|
|
81
90
|
|
|
82
91
|
if (!authHeader) {
|
|
@@ -143,7 +152,7 @@ export function createMiddleware(options: ClerkMiddlewareOptions = {}): Middlewa
|
|
|
143
152
|
console.error(`[Clerk Auth] Authentication failed: ${errorCode} - ${errorMessage}`);
|
|
144
153
|
return c.json({ error: 'Unauthorized' }, 401);
|
|
145
154
|
}
|
|
146
|
-
};
|
|
155
|
+
});
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
/**
|
|
@@ -157,12 +166,3 @@ function mapClerkUserToAgentuityUser(clerkUser: User): AgentuityAuthUser<User> {
|
|
|
157
166
|
raw: clerkUser,
|
|
158
167
|
};
|
|
159
168
|
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Augment Hono's context types to include auth.
|
|
163
|
-
*/
|
|
164
|
-
declare module 'hono' {
|
|
165
|
-
interface ContextVariableMap {
|
|
166
|
-
auth: AgentuityAuth<User, ClerkJWTPayload>;
|
|
167
|
-
}
|
|
168
|
-
}
|