@backstage/plugin-auth-backend 0.4.3 → 0.4.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/CHANGELOG.md +56 -0
- package/config.d.ts +29 -0
- package/dist/index.cjs.js +450 -91
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +180 -108
- package/package.json +11 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import { Logger } from 'winston';
|
|
3
4
|
import { PluginEndpointDiscovery, PluginDatabaseManager } from '@backstage/backend-common';
|
|
4
5
|
import { CatalogApi } from '@backstage/catalog-client';
|
|
5
6
|
import { UserEntity, Entity } from '@backstage/catalog-model';
|
|
6
7
|
import { Config } from '@backstage/config';
|
|
7
|
-
import { JSONWebKey } from 'jose';
|
|
8
8
|
import { Profile } from 'passport';
|
|
9
|
+
import { JSONWebKey } from 'jose';
|
|
9
10
|
|
|
10
11
|
/** Represents any form of serializable JWK */
|
|
11
12
|
interface AnyJWK extends Record<string, string> {
|
|
@@ -41,6 +42,101 @@ declare type TokenIssuer = {
|
|
|
41
42
|
}>;
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Common options for passport.js-based OAuth providers
|
|
47
|
+
*/
|
|
48
|
+
declare type OAuthProviderOptions = {
|
|
49
|
+
/**
|
|
50
|
+
* Client ID of the auth provider.
|
|
51
|
+
*/
|
|
52
|
+
clientId: string;
|
|
53
|
+
/**
|
|
54
|
+
* Client Secret of the auth provider.
|
|
55
|
+
*/
|
|
56
|
+
clientSecret: string;
|
|
57
|
+
/**
|
|
58
|
+
* Callback URL to be passed to the auth provider to redirect to after the user signs in.
|
|
59
|
+
*/
|
|
60
|
+
callbackUrl: string;
|
|
61
|
+
};
|
|
62
|
+
declare type OAuthResult = {
|
|
63
|
+
fullProfile: Profile;
|
|
64
|
+
params: {
|
|
65
|
+
id_token?: string;
|
|
66
|
+
scope: string;
|
|
67
|
+
expires_in: number;
|
|
68
|
+
};
|
|
69
|
+
accessToken: string;
|
|
70
|
+
refreshToken?: string;
|
|
71
|
+
};
|
|
72
|
+
declare type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
|
73
|
+
declare type OAuthProviderInfo = {
|
|
74
|
+
/**
|
|
75
|
+
* An access token issued for the signed in user.
|
|
76
|
+
*/
|
|
77
|
+
accessToken: string;
|
|
78
|
+
/**
|
|
79
|
+
* (Optional) Id token issued for the signed in user.
|
|
80
|
+
*/
|
|
81
|
+
idToken?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Expiry of the access token in seconds.
|
|
84
|
+
*/
|
|
85
|
+
expiresInSeconds?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Scopes granted for the access token.
|
|
88
|
+
*/
|
|
89
|
+
scope: string;
|
|
90
|
+
/**
|
|
91
|
+
* A refresh token issued for the signed in user
|
|
92
|
+
*/
|
|
93
|
+
refreshToken?: string;
|
|
94
|
+
};
|
|
95
|
+
declare type OAuthState = {
|
|
96
|
+
nonce: string;
|
|
97
|
+
env: string;
|
|
98
|
+
origin?: string;
|
|
99
|
+
};
|
|
100
|
+
declare type OAuthStartRequest = express.Request<{}> & {
|
|
101
|
+
scope: string;
|
|
102
|
+
state: OAuthState;
|
|
103
|
+
};
|
|
104
|
+
declare type OAuthRefreshRequest = express.Request<{}> & {
|
|
105
|
+
scope: string;
|
|
106
|
+
refreshToken: string;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Any OAuth provider needs to implement this interface which has provider specific
|
|
110
|
+
* handlers for different methods to perform authentication, get access tokens,
|
|
111
|
+
* refresh tokens and perform sign out.
|
|
112
|
+
*/
|
|
113
|
+
interface OAuthHandlers {
|
|
114
|
+
/**
|
|
115
|
+
* This method initiates a sign in request with an auth provider.
|
|
116
|
+
* @param {express.Request} req
|
|
117
|
+
* @param options
|
|
118
|
+
*/
|
|
119
|
+
start(req: OAuthStartRequest): Promise<RedirectInfo>;
|
|
120
|
+
/**
|
|
121
|
+
* Handles the redirect from the auth provider when the user has signed in.
|
|
122
|
+
* @param {express.Request} req
|
|
123
|
+
*/
|
|
124
|
+
handler(req: express.Request): Promise<{
|
|
125
|
+
response: AuthResponse<OAuthProviderInfo>;
|
|
126
|
+
refreshToken?: string;
|
|
127
|
+
}>;
|
|
128
|
+
/**
|
|
129
|
+
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
|
130
|
+
* @param {string} refreshToken
|
|
131
|
+
* @param {string} scope
|
|
132
|
+
*/
|
|
133
|
+
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
|
|
134
|
+
/**
|
|
135
|
+
* (Optional) Sign out of the auth provider.
|
|
136
|
+
*/
|
|
137
|
+
logout?(): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
|
|
44
140
|
/**
|
|
45
141
|
* A identity client to interact with auth-backend
|
|
46
142
|
* and authenticate backstage identity tokens
|
|
@@ -205,16 +301,6 @@ interface AuthProviderRouteHandlers {
|
|
|
205
301
|
*/
|
|
206
302
|
logout?(req: express.Request, res: express.Response): Promise<void>;
|
|
207
303
|
}
|
|
208
|
-
/**
|
|
209
|
-
* EXPERIMENTAL - this will almost certainly break in a future release.
|
|
210
|
-
*
|
|
211
|
-
* Used to resolve an identity from auth information in some auth providers.
|
|
212
|
-
*/
|
|
213
|
-
declare type ExperimentalIdentityResolver = (
|
|
214
|
-
/**
|
|
215
|
-
* An object containing information specific to the auth provider.
|
|
216
|
-
*/
|
|
217
|
-
payload: object, catalogApi: CatalogApi) => Promise<AuthResponse<any>>;
|
|
218
304
|
declare type AuthProviderFactoryOptions = {
|
|
219
305
|
providerId: string;
|
|
220
306
|
globalConfig: AuthProviderConfig;
|
|
@@ -223,7 +309,6 @@ declare type AuthProviderFactoryOptions = {
|
|
|
223
309
|
tokenIssuer: TokenIssuer;
|
|
224
310
|
discovery: PluginEndpointDiscovery;
|
|
225
311
|
catalogApi: CatalogApi;
|
|
226
|
-
identityResolver?: ExperimentalIdentityResolver;
|
|
227
312
|
};
|
|
228
313
|
declare type AuthProviderFactory = (options: AuthProviderFactoryOptions) => AuthProviderRouteHandlers;
|
|
229
314
|
declare type AuthResponse<ProviderInfo> = {
|
|
@@ -303,6 +388,9 @@ declare type AuthHandlerResult = {
|
|
|
303
388
|
* possible to use this function as a way to limit access to a certain group of users.
|
|
304
389
|
*/
|
|
305
390
|
declare type AuthHandler<AuthResult> = (input: AuthResult) => Promise<AuthHandlerResult>;
|
|
391
|
+
declare type StateEncoder = (req: OAuthStartRequest) => Promise<{
|
|
392
|
+
encodedState: string;
|
|
393
|
+
}>;
|
|
306
394
|
|
|
307
395
|
declare class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
|
|
308
396
|
private readonly handlers;
|
|
@@ -316,101 +404,6 @@ declare class OAuthEnvironmentHandler implements AuthProviderRouteHandlers {
|
|
|
316
404
|
private getProviderForEnv;
|
|
317
405
|
}
|
|
318
406
|
|
|
319
|
-
/**
|
|
320
|
-
* Common options for passport.js-based OAuth providers
|
|
321
|
-
*/
|
|
322
|
-
declare type OAuthProviderOptions = {
|
|
323
|
-
/**
|
|
324
|
-
* Client ID of the auth provider.
|
|
325
|
-
*/
|
|
326
|
-
clientId: string;
|
|
327
|
-
/**
|
|
328
|
-
* Client Secret of the auth provider.
|
|
329
|
-
*/
|
|
330
|
-
clientSecret: string;
|
|
331
|
-
/**
|
|
332
|
-
* Callback URL to be passed to the auth provider to redirect to after the user signs in.
|
|
333
|
-
*/
|
|
334
|
-
callbackUrl: string;
|
|
335
|
-
};
|
|
336
|
-
declare type OAuthResult = {
|
|
337
|
-
fullProfile: Profile;
|
|
338
|
-
params: {
|
|
339
|
-
id_token?: string;
|
|
340
|
-
scope: string;
|
|
341
|
-
expires_in: number;
|
|
342
|
-
};
|
|
343
|
-
accessToken: string;
|
|
344
|
-
refreshToken?: string;
|
|
345
|
-
};
|
|
346
|
-
declare type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
|
347
|
-
declare type OAuthProviderInfo = {
|
|
348
|
-
/**
|
|
349
|
-
* An access token issued for the signed in user.
|
|
350
|
-
*/
|
|
351
|
-
accessToken: string;
|
|
352
|
-
/**
|
|
353
|
-
* (Optional) Id token issued for the signed in user.
|
|
354
|
-
*/
|
|
355
|
-
idToken?: string;
|
|
356
|
-
/**
|
|
357
|
-
* Expiry of the access token in seconds.
|
|
358
|
-
*/
|
|
359
|
-
expiresInSeconds?: number;
|
|
360
|
-
/**
|
|
361
|
-
* Scopes granted for the access token.
|
|
362
|
-
*/
|
|
363
|
-
scope: string;
|
|
364
|
-
/**
|
|
365
|
-
* A refresh token issued for the signed in user
|
|
366
|
-
*/
|
|
367
|
-
refreshToken?: string;
|
|
368
|
-
};
|
|
369
|
-
declare type OAuthState = {
|
|
370
|
-
nonce: string;
|
|
371
|
-
env: string;
|
|
372
|
-
origin?: string;
|
|
373
|
-
};
|
|
374
|
-
declare type OAuthStartRequest = express.Request<{}> & {
|
|
375
|
-
scope: string;
|
|
376
|
-
state: OAuthState;
|
|
377
|
-
};
|
|
378
|
-
declare type OAuthRefreshRequest = express.Request<{}> & {
|
|
379
|
-
scope: string;
|
|
380
|
-
refreshToken: string;
|
|
381
|
-
};
|
|
382
|
-
/**
|
|
383
|
-
* Any OAuth provider needs to implement this interface which has provider specific
|
|
384
|
-
* handlers for different methods to perform authentication, get access tokens,
|
|
385
|
-
* refresh tokens and perform sign out.
|
|
386
|
-
*/
|
|
387
|
-
interface OAuthHandlers {
|
|
388
|
-
/**
|
|
389
|
-
* This method initiates a sign in request with an auth provider.
|
|
390
|
-
* @param {express.Request} req
|
|
391
|
-
* @param options
|
|
392
|
-
*/
|
|
393
|
-
start(req: OAuthStartRequest): Promise<RedirectInfo>;
|
|
394
|
-
/**
|
|
395
|
-
* Handles the redirect from the auth provider when the user has signed in.
|
|
396
|
-
* @param {express.Request} req
|
|
397
|
-
*/
|
|
398
|
-
handler(req: express.Request): Promise<{
|
|
399
|
-
response: AuthResponse<OAuthProviderInfo>;
|
|
400
|
-
refreshToken?: string;
|
|
401
|
-
}>;
|
|
402
|
-
/**
|
|
403
|
-
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
|
404
|
-
* @param {string} refreshToken
|
|
405
|
-
* @param {string} scope
|
|
406
|
-
*/
|
|
407
|
-
refresh?(req: OAuthRefreshRequest): Promise<AuthResponse<OAuthProviderInfo>>;
|
|
408
|
-
/**
|
|
409
|
-
* (Optional) Sign out of the auth provider.
|
|
410
|
-
*/
|
|
411
|
-
logout?(): Promise<void>;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
407
|
declare type Options = {
|
|
415
408
|
providerId: string;
|
|
416
409
|
secure: boolean;
|
|
@@ -472,6 +465,23 @@ declare type GithubProviderOptions = {
|
|
|
472
465
|
*/
|
|
473
466
|
resolver?: SignInResolver<GithubOAuthResult>;
|
|
474
467
|
};
|
|
468
|
+
/**
|
|
469
|
+
* The state encoder used to encode the 'state' parameter on the OAuth request.
|
|
470
|
+
*
|
|
471
|
+
* It should return a string that takes the state params (from the request), url encodes the params
|
|
472
|
+
* and finally base64 encodes them.
|
|
473
|
+
*
|
|
474
|
+
* Providing your own stateEncoder will allow you to add addition parameters to the state field.
|
|
475
|
+
*
|
|
476
|
+
* It is typed as follows:
|
|
477
|
+
* export type StateEncoder = (input: OAuthState) => Promise<{encodedState: string}>;
|
|
478
|
+
*
|
|
479
|
+
* Note: the stateEncoder must encode a 'nonce' value and an 'env' value. Without this, the OAuth flow will fail
|
|
480
|
+
* (These two values will be set by the req.state by default)
|
|
481
|
+
*
|
|
482
|
+
* For more information, please see the helper module in ../../oauth/helpers #readState
|
|
483
|
+
*/
|
|
484
|
+
stateEncoder?: StateEncoder;
|
|
475
485
|
};
|
|
476
486
|
declare const createGithubProvider: (options?: GithubProviderOptions | undefined) => AuthProviderFactory;
|
|
477
487
|
|
|
@@ -604,6 +614,68 @@ declare type BitbucketProviderOptions = {
|
|
|
604
614
|
};
|
|
605
615
|
declare const createBitbucketProvider: (options?: BitbucketProviderOptions | undefined) => AuthProviderFactory;
|
|
606
616
|
|
|
617
|
+
declare type AtlassianAuthProviderOptions = OAuthProviderOptions & {
|
|
618
|
+
scopes: string;
|
|
619
|
+
signInResolver?: SignInResolver<OAuthResult>;
|
|
620
|
+
authHandler: AuthHandler<OAuthResult>;
|
|
621
|
+
tokenIssuer: TokenIssuer;
|
|
622
|
+
catalogIdentityClient: CatalogIdentityClient;
|
|
623
|
+
logger: Logger;
|
|
624
|
+
};
|
|
625
|
+
declare class AtlassianAuthProvider implements OAuthHandlers {
|
|
626
|
+
private readonly _strategy;
|
|
627
|
+
private readonly signInResolver?;
|
|
628
|
+
private readonly authHandler;
|
|
629
|
+
private readonly tokenIssuer;
|
|
630
|
+
private readonly catalogIdentityClient;
|
|
631
|
+
private readonly logger;
|
|
632
|
+
constructor(options: AtlassianAuthProviderOptions);
|
|
633
|
+
start(req: OAuthStartRequest): Promise<RedirectInfo>;
|
|
634
|
+
handler(req: express.Request): Promise<{
|
|
635
|
+
response: OAuthResponse;
|
|
636
|
+
refreshToken: string;
|
|
637
|
+
}>;
|
|
638
|
+
private handleResult;
|
|
639
|
+
refresh(req: OAuthRefreshRequest): Promise<OAuthResponse>;
|
|
640
|
+
}
|
|
641
|
+
declare type AtlassianProviderOptions = {
|
|
642
|
+
/**
|
|
643
|
+
* The profile transformation function used to verify and convert the auth response
|
|
644
|
+
* into the profile that will be presented to the user.
|
|
645
|
+
*/
|
|
646
|
+
authHandler?: AuthHandler<OAuthResult>;
|
|
647
|
+
/**
|
|
648
|
+
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
|
649
|
+
*/
|
|
650
|
+
signIn?: {
|
|
651
|
+
resolver: SignInResolver<OAuthResult>;
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
declare const createAtlassianProvider: (options?: AtlassianProviderOptions | undefined) => AuthProviderFactory;
|
|
655
|
+
|
|
656
|
+
declare type AwsAlbResult = {
|
|
657
|
+
fullProfile: Profile;
|
|
658
|
+
expiresInSeconds?: number;
|
|
659
|
+
accessToken: string;
|
|
660
|
+
};
|
|
661
|
+
declare type AwsAlbProviderOptions = {
|
|
662
|
+
/**
|
|
663
|
+
* The profile transformation function used to verify and convert the auth response
|
|
664
|
+
* into the profile that will be presented to the user.
|
|
665
|
+
*/
|
|
666
|
+
authHandler?: AuthHandler<AwsAlbResult>;
|
|
667
|
+
/**
|
|
668
|
+
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
|
|
669
|
+
*/
|
|
670
|
+
signIn: {
|
|
671
|
+
/**
|
|
672
|
+
* Maps an auth result to a Backstage identity for the user.
|
|
673
|
+
*/
|
|
674
|
+
resolver: SignInResolver<AwsAlbResult>;
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
declare const createAwsAlbProvider: (options?: AwsAlbProviderOptions | undefined) => AuthProviderFactory;
|
|
678
|
+
|
|
607
679
|
declare const factories: {
|
|
608
680
|
[providerId: string]: AuthProviderFactory;
|
|
609
681
|
};
|
|
@@ -636,4 +708,4 @@ declare type WebMessageResponse = {
|
|
|
636
708
|
declare const postMessageResponse: (res: express.Response, appOrigin: string, response: WebMessageResponse) => void;
|
|
637
709
|
declare const ensuresXRequestedWith: (req: express.Request) => boolean;
|
|
638
710
|
|
|
639
|
-
export { AuthProviderFactory, AuthProviderFactoryOptions, AuthProviderRouteHandlers, AuthResponse, BackstageIdentity, BitbucketOAuthResult, BitbucketPassportProfile, BitbucketProviderOptions, GithubOAuthResult, GithubProviderOptions, GitlabProviderOptions, GoogleProviderOptions, IdentityClient, MicrosoftProviderOptions, OAuth2ProviderOptions, OAuthAdapter, OAuthEnvironmentHandler, OAuthHandlers, OAuthProviderInfo, OAuthProviderOptions, OAuthRefreshRequest, OAuthResponse, OAuthResult, OAuthStartRequest, OAuthState, OktaProviderOptions, ProfileInfo, RouterOptions, TokenIssuer, WebMessageResponse, bitbucketUserIdSignInResolver, bitbucketUsernameSignInResolver, createBitbucketProvider, createGithubProvider, createGitlabProvider, createGoogleProvider, createMicrosoftProvider, createOAuth2Provider, createOktaProvider, createOriginFilter, createRouter, factories as defaultAuthProviderFactories, encodeState, ensuresXRequestedWith, googleEmailSignInResolver, microsoftEmailSignInResolver, oktaEmailSignInResolver, postMessageResponse, readState, verifyNonce };
|
|
711
|
+
export { AtlassianAuthProvider, AtlassianProviderOptions, AuthProviderFactory, AuthProviderFactoryOptions, AuthProviderRouteHandlers, AuthResponse, AwsAlbProviderOptions, BackstageIdentity, BitbucketOAuthResult, BitbucketPassportProfile, BitbucketProviderOptions, GithubOAuthResult, GithubProviderOptions, GitlabProviderOptions, GoogleProviderOptions, IdentityClient, MicrosoftProviderOptions, OAuth2ProviderOptions, OAuthAdapter, OAuthEnvironmentHandler, OAuthHandlers, OAuthProviderInfo, OAuthProviderOptions, OAuthRefreshRequest, OAuthResponse, OAuthResult, OAuthStartRequest, OAuthState, OktaProviderOptions, ProfileInfo, RouterOptions, TokenIssuer, WebMessageResponse, bitbucketUserIdSignInResolver, bitbucketUsernameSignInResolver, createAtlassianProvider, createAwsAlbProvider, createBitbucketProvider, createGithubProvider, createGitlabProvider, createGoogleProvider, createMicrosoftProvider, createOAuth2Provider, createOktaProvider, createOriginFilter, createRouter, factories as defaultAuthProviderFactories, encodeState, ensuresXRequestedWith, googleEmailSignInResolver, microsoftEmailSignInResolver, oktaEmailSignInResolver, postMessageResponse, readState, verifyNonce };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-auth-backend",
|
|
3
3
|
"description": "A Backstage backend plugin that handles authentication",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.7",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -30,12 +30,13 @@
|
|
|
30
30
|
"clean": "backstage-cli clean"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@backstage/backend-common": "^0.9.
|
|
34
|
-
"@backstage/catalog-client": "^0.5.
|
|
35
|
-
"@backstage/catalog-model": "^0.9.
|
|
36
|
-
"@backstage/config": "^0.1.
|
|
37
|
-
"@backstage/errors": "^0.1.
|
|
38
|
-
"@backstage/test-utils": "^0.1.
|
|
33
|
+
"@backstage/backend-common": "^0.9.9",
|
|
34
|
+
"@backstage/catalog-client": "^0.5.1",
|
|
35
|
+
"@backstage/catalog-model": "^0.9.6",
|
|
36
|
+
"@backstage/config": "^0.1.11",
|
|
37
|
+
"@backstage/errors": "^0.1.4",
|
|
38
|
+
"@backstage/test-utils": "^0.1.21",
|
|
39
|
+
"@google-cloud/firestore": "^4.15.1",
|
|
39
40
|
"@types/express": "^4.17.6",
|
|
40
41
|
"@types/passport": "^1.0.3",
|
|
41
42
|
"compression": "^1.7.4",
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
"yn": "^4.0.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
|
-
"@backstage/cli": "^0.
|
|
76
|
+
"@backstage/cli": "^0.8.2",
|
|
76
77
|
"@types/body-parser": "^1.19.0",
|
|
77
78
|
"@types/cookie-parser": "^1.4.2",
|
|
78
79
|
"@types/express-session": "^1.17.2",
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"@types/passport-saml": "^1.1.3",
|
|
84
85
|
"@types/passport-strategy": "^0.2.35",
|
|
85
86
|
"@types/xml2js": "^0.4.7",
|
|
86
|
-
"msw": "^0.
|
|
87
|
+
"msw": "^0.35.0"
|
|
87
88
|
},
|
|
88
89
|
"files": [
|
|
89
90
|
"dist",
|
|
@@ -91,5 +92,5 @@
|
|
|
91
92
|
"config.d.ts"
|
|
92
93
|
],
|
|
93
94
|
"configSchema": "config.d.ts",
|
|
94
|
-
"gitHead": "
|
|
95
|
+
"gitHead": "5bdaccc40b4a814cf0b45d429f15a3afacc2f60b"
|
|
95
96
|
}
|