passport-steam-openid 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/.eslintrc.js ADDED
@@ -0,0 +1,26 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ parserOptions: {
4
+ project: 'tsconfig.json',
5
+ sourceType: 'module',
6
+ },
7
+ plugins: ['@typescript-eslint/eslint-plugin', 'sonarjs'],
8
+ extends: [
9
+ 'plugin:@typescript-eslint/recommended',
10
+ 'plugin:prettier/recommended',
11
+ 'plugin:sonarjs/recommended',
12
+ ],
13
+ root: true,
14
+ env: {
15
+ node: true,
16
+ jest: true,
17
+ },
18
+ ignorePatterns: ['.eslintrc.js', 'sample/*'],
19
+ rules: {
20
+ '@typescript-eslint/interface-name-prefix': 'off',
21
+ '@typescript-eslint/explicit-function-return-type': 'off',
22
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
23
+ '@typescript-eslint/no-explicit-any': 'off',
24
+ 'sonarjs/no-duplicate-string': 'off'
25
+ },
26
+ };
@@ -0,0 +1,23 @@
1
+ name: Pull Request
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ master ]
6
+
7
+ jobs:
8
+ test_pull_request:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ name: Checkout repository
13
+
14
+ - uses: actions/setup-node@v3
15
+ with:
16
+ node-version: 18
17
+ name: Setup Node.JS
18
+
19
+ - run: npm ci
20
+ name: Install packages
21
+
22
+ - run: npm run build
23
+ name: Compile library
@@ -0,0 +1,29 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ master
6
+ jobs:
7
+ release:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v3
11
+ name: Checkout repository
12
+
13
+ - uses: actions/setup-node@v3
14
+ with:
15
+ node-version: '18'
16
+ name: Setup Node.JS
17
+
18
+ - name: Install packages
19
+ run: npm ci
20
+
21
+ - run: npm run build
22
+ name: Compile library
23
+
24
+ - name: Release
25
+ env:
26
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
28
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29
+ run: npx semantic-release
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ exec < /dev/tty && npx cz --hook || true
@@ -0,0 +1,3 @@
1
+ {
2
+ "*.ts": "npx eslint"
3
+ }
@@ -0,0 +1 @@
1
+ src/constant.ts
package/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all"
4
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 GlenCoco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # passport-steam-openid
2
+ Passport strategy for authenticating with steam openid without the use of 3rd party openid packages,
3
+ which have been proved to be source of many exploits of steam openid system, apparently by design.
4
+ This package only relies on [passport](https://www.passportjs.org/) and [axios](https://axios-http.com/).
5
+
6
+ ## Usage
7
+ ```ts
8
+ import { SteamOpenIdStrategy } from 'passport-steam-openid';
9
+
10
+ passport.use(
11
+ new SteamOpenIdStrategy({
12
+ returnURL: 'http://localhost:3000/auth/steam',
13
+ profile: true,
14
+ apiKey: '<insert steam api key>'
15
+ }, (
16
+ req: Request,
17
+ identifier: string,
18
+ profile: SteamOpenIdUserProfile,
19
+ done: VerifyCallback
20
+ ) => {
21
+ // Optional callback called only when successful authentication occurs
22
+ // You can save the user to database here.
23
+ })
24
+ )
25
+
26
+ app.get('/auth/steam', passport.authenticate('steam-openid'), (req, res) => {
27
+ // When requested first by user,
28
+ // they will get redirected to steam and this function is never called.
29
+ // Second time, after user is authenticated, this function is called.
30
+ })
31
+ ```
32
+
33
+ ### Error handling
34
+ ```ts
35
+ app.use(
36
+ (err: Error, req: Request, res: Response): void => {
37
+ if (err instanceof SteamOpenIdError) {
38
+ switch (err.code) {
39
+ case SteamOpenIdErrorType.InvalidQuery:
40
+ // Supplied querystring was invalid
41
+ case SteamOpenIdErrorType.Unauthorized:
42
+ // Steam rejected this authentication request
43
+ case SteamOpenIdErrorType.InvalidSteamId:
44
+ // Steam profile doesn't exist
45
+ }
46
+ }
47
+ // ...
48
+ },
49
+ );
50
+ ```
51
+
52
+ ### Sample
53
+ Visit [sample](./sample) directory for more elaborate usage example.
54
+
55
+ ## Instalation
56
+ Install using npm:
57
+ ```
58
+ npm install passport-steam-openid
59
+ ```
60
+ Released in early stage, library's api is subject to change.
61
+
62
+ ## License
63
+ This library is released under MIT license.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Query parameters that are allowed for be on the authentication request.
3
+ */
4
+ export declare const OPENID_QUERY_PROPS: readonly ["openid.ns", "openid.mode", "openid.op_endpoint", "openid.claimed_id", "openid.identity", "openid.return_to", "openid.response_nonce", "openid.assoc_handle", "openid.signed", "openid.sig"];
5
+ export declare const VALID_NONCE = "http://specs.openid.net/auth/2.0";
6
+ export declare const VALID_ID_SELECT: string;
7
+ export declare const VALID_IDENTITY_ENDPOINT = "https://steamcommunity.com/openid/id";
8
+ export declare const VALID_OPENID_ENDPOINT = "https://steamcommunity.com/openid/login";
9
+ export declare const PLAYER_SUMMARY_URL = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2";
10
+ //# sourceMappingURL=constant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constant.d.ts","sourceRoot":"","sources":["../src/constant.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,kBAAkB,wMAWrB,CAAC;AAGX,eAAO,MAAM,WAAW,qCAAqC,CAAC;AAC9D,eAAO,MAAM,eAAe,QAAqC,CAAC;AAClE,eAAO,MAAM,uBAAuB,yCAAyC,CAAC;AAC9E,eAAO,MAAM,qBAAqB,4CAA4C,CAAC;AAC/E,eAAO,MAAM,kBAAkB,kEAAkE,CAAC"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLAYER_SUMMARY_URL = exports.VALID_OPENID_ENDPOINT = exports.VALID_IDENTITY_ENDPOINT = exports.VALID_ID_SELECT = exports.VALID_NONCE = exports.OPENID_QUERY_PROPS = void 0;
4
+ /**
5
+ * Query parameters that are allowed for be on the authentication request.
6
+ */
7
+ exports.OPENID_QUERY_PROPS = [
8
+ 'openid.ns',
9
+ 'openid.mode',
10
+ 'openid.op_endpoint',
11
+ 'openid.claimed_id',
12
+ 'openid.identity',
13
+ 'openid.return_to',
14
+ 'openid.response_nonce',
15
+ 'openid.assoc_handle',
16
+ 'openid.signed',
17
+ 'openid.sig',
18
+ ];
19
+ // All URLs required for this package.
20
+ exports.VALID_NONCE = 'http://specs.openid.net/auth/2.0';
21
+ exports.VALID_ID_SELECT = `${exports.VALID_NONCE}/identifier_select`;
22
+ exports.VALID_IDENTITY_ENDPOINT = 'https://steamcommunity.com/openid/id';
23
+ exports.VALID_OPENID_ENDPOINT = 'https://steamcommunity.com/openid/login';
24
+ exports.PLAYER_SUMMARY_URL = 'https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2';
@@ -0,0 +1,12 @@
1
+ import { SteamOpenIdErrorType } from './type';
2
+ /**
3
+ * Class used to distinguish between errors caused by user and internal ones.
4
+ *
5
+ * @class SteamOpenIdError
6
+ * @param code can be used to see which kind of error occured programmatically.
7
+ */
8
+ export declare class SteamOpenIdError extends Error {
9
+ readonly code: SteamOpenIdErrorType;
10
+ constructor(message: string, code: SteamOpenIdErrorType);
11
+ }
12
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aACI,IAAI,EAAE,oBAAoB;gBAA3D,OAAO,EAAE,MAAM,EAAkB,IAAI,EAAE,oBAAoB;CAGxE"}
package/dist/error.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SteamOpenIdError = void 0;
4
+ /**
5
+ * Class used to distinguish between errors caused by user and internal ones.
6
+ *
7
+ * @class SteamOpenIdError
8
+ * @param code can be used to see which kind of error occured programmatically.
9
+ */
10
+ class SteamOpenIdError extends Error {
11
+ constructor(message, code) {
12
+ super(message);
13
+ this.code = code;
14
+ }
15
+ }
16
+ exports.SteamOpenIdError = SteamOpenIdError;
@@ -0,0 +1,5 @@
1
+ export * from './strategy';
2
+ export * from './type';
3
+ export * from './error';
4
+ export * from './constant';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./strategy"), exports);
5
+ tslib_1.__exportStar(require("./type"), exports);
6
+ tslib_1.__exportStar(require("./error"), exports);
7
+ tslib_1.__exportStar(require("./constant"), exports);
@@ -0,0 +1,168 @@
1
+ /// <reference types="node" />
2
+ import { ParsedUrlQuery } from 'querystring';
3
+ import { AxiosInstance } from 'axios';
4
+ import { Strategy } from 'passport';
5
+ import { SteamOpenIdUserProfile, SteamOpenIdUser, SteamOpenIdQuery, SteamOpenIdStrategyOptionsWithProfile, SteamOpenIdStrategyOptionsWithoutProfile, VerifyCallback } from './type';
6
+ /**
7
+ * Strategy that authenticates you via steam openid without the use of any external openid libraries,
8
+ * which can and are source of many vulnerabilities.
9
+ *
10
+ * Functionality should be similar to `passport-steam`.
11
+ *
12
+ * @class SteamOpenIdStrategy
13
+ */
14
+ export declare class SteamOpenIdStrategy<TOptions extends SteamOpenIdStrategyOptionsWithProfile | SteamOpenIdStrategyOptionsWithoutProfile, TUser extends SteamOpenIdUser | SteamOpenIdUserProfile = TOptions extends SteamOpenIdStrategyOptionsWithProfile ? SteamOpenIdUserProfile : SteamOpenIdUser> extends Strategy {
15
+ /**
16
+ * Axios instance used for validating the request and fetching profile.
17
+ */
18
+ protected readonly axios: AxiosInstance;
19
+ /**
20
+ * Where in your app, you want to return to from steam.
21
+ *
22
+ * This route has to have passport authentication middleware.
23
+ */
24
+ protected readonly returnURL: string;
25
+ /**
26
+ * Steam Api key used for fetching profile.
27
+ */
28
+ protected readonly apiKey?: string;
29
+ /**
30
+ * Signalizes, if profile should be fetched.
31
+ */
32
+ protected readonly profile: boolean;
33
+ /**
34
+ * optional callback, called when user is successfully authenticated
35
+ */
36
+ protected verify?: VerifyCallback<TUser>;
37
+ /**
38
+ * @constructor
39
+ *
40
+ * @param options.returnURL where steam redirects after parameters are passed
41
+ * @param options.profile if set, we will fetch user's profile from steam api
42
+ * @param options.apiKey api key to fetch user profile, not used if profile is false
43
+ * @param verify optional callback, called when user is successfully authenticated
44
+ */
45
+ constructor(options: TOptions, verify?: VerifyCallback<TUser>);
46
+ /**
47
+ * Passport handle for authentication. We handle the query, passport does rest.
48
+ *
49
+ * @param req Base IncommingMessage request enhanced with parsed querystring.
50
+ */
51
+ authenticate(req: any): Promise<void>;
52
+ /**
53
+ * Handles validation request.
54
+ *
55
+ * Can be used in a middleware, if you don't like passport.
56
+ *
57
+ * @param req Base IncommingMessage request enhanced with parsed querystring.
58
+ * @returns
59
+ * @throws {SteamOpenIdError} User related problem, such as:
60
+ * - open.mode was not correct
61
+ * - query did not was pass validation
62
+ * - steam rejected this query
63
+ * - steamid is invalid
64
+ * @throws {Error} Non-recoverable errors, such as query object missing.
65
+ */
66
+ handleRequest(req: any): Promise<TUser>;
67
+ /**
68
+ * Checks if error is retryable,
69
+ * meaning user gets redirected to steam openid page.
70
+ *
71
+ * @param err from catch clause
72
+ * @returns true, if error should be retried
73
+ * @returns false, if error is not retriable
74
+ * and should be handled by the app.
75
+ */
76
+ protected isRetryableError(err: unknown): boolean;
77
+ /**
78
+ * Retrieves query parameter from req object and checks if it is an object.
79
+ *
80
+ * @param req Base IncommingMessage request enhanced with parsed querystring.
81
+ * @returns query from said request
82
+ * @throws Error if query cannot be found, non-recoverable error.
83
+ */
84
+ protected getQuery(req: any): ParsedUrlQuery;
85
+ /**
86
+ * Checks if `mode` field from query is correct and thus authentication can begin
87
+ *
88
+ * @param query original query user submitted
89
+ * @returns true, if mode is correct, equal to `id_res`
90
+ * @returns false, if mode is incorrect
91
+ */
92
+ protected hasAuthQuery(query: ParsedUrlQuery): boolean;
93
+ /**
94
+ * Builds a redirect url for user that is about to authenticate
95
+ *
96
+ * @returns redirect url built with proper parameters
97
+ */
98
+ buildRedirectUrl(): string;
99
+ /**
100
+ * Validates user submitted query, if it contains correct parameters.
101
+ * No excess parameters can be used.
102
+ *
103
+ * @param query original query user submitted
104
+ * @returns true, query contains correct parameters
105
+ * @returns false, query contains incorrect parameters
106
+ */
107
+ protected isQueryValid(query: ParsedUrlQuery): query is SteamOpenIdQuery;
108
+ /**
109
+ * Checks if identity starts with correct link.
110
+ *
111
+ * @param identity from querystring
112
+ * @returns true, if identity is a string and starts with correct endpoint
113
+ * @return false, if above criteria was violated
114
+ */
115
+ protected isValidIdentity(identity: string | unknown): boolean;
116
+ /**
117
+ * Query trusted steam endpoint to validate supplied query.
118
+ *
119
+ * @param query original query user submitted
120
+ * @returns true, if positive response was received
121
+ * @returns false, if request failed, status is incorrect or data signals invalid
122
+ */
123
+ protected validateAgainstSteam(query: SteamOpenIdQuery): Promise<boolean>;
124
+ /**
125
+ * Clones query from authentication request, changes mode and stringifies to form data.
126
+ * @param query original query user submitted
127
+ * @returns stringified form data with changed mode
128
+ */
129
+ protected getOpenIdValidationRequestBody(query: SteamOpenIdQuery): string;
130
+ /**
131
+ * Validates response from steam to see if query is correct.
132
+ *
133
+ * @param response response received from steama
134
+ * @returns true, if data was in correct format and signals valid query
135
+ * @return false, if data was corrupted or invalid query was signaled
136
+ */
137
+ protected isSteamResponseValid(response: any): boolean;
138
+ /**
139
+ * Parses steamId from `claimed_id` field, which is what openid 2.0 uses.
140
+ *
141
+ * @param query original query user submitted
142
+ * @returns parsed steamId
143
+ */
144
+ protected getSteamId(query: SteamOpenIdQuery): string;
145
+ /**
146
+ * Abstract method for getting user that has been authenticated.
147
+ * You can implement fetching user from steamid and thus validating even more,
148
+ * or if you are satisified with just steamId, you can return it as an object
149
+ * and continue without need of an steam api key.
150
+ *
151
+ * @param steamId steamId parsed from `claimed_id`
152
+ * @return generic that was chosen by child class
153
+ */
154
+ protected getUser(steamId: string): Promise<TUser>;
155
+ /**
156
+ * Fetches profile data for authenticated user.
157
+ * Validates the steamId even more.
158
+ *
159
+ * @param steamId parsed steamId from `claimed_id`
160
+ * @returns profile belonging to said steamId
161
+ *
162
+ * @throws {Error} if malformed response was received
163
+ * @throws {AxiosError} if status was not 200
164
+ * @throws {SteamOpenIdError} if profile was not found
165
+ */
166
+ protected fetchPlayerSummary(steamId: string): Promise<SteamOpenIdUserProfile>;
167
+ }
168
+ //# sourceMappingURL=strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../src/strategy.ts"],"names":[],"mappings":";AAAA,OAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjD,OAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAUpC,OAAO,EACL,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAGhB,qCAAqC,EACrC,wCAAwC,EACxC,cAAc,EACf,MAAM,QAAQ,CAAC;AAEhB;;;;;;;GAOG;AACH,qBAAa,mBAAmB,CAC9B,QAAQ,SACJ,qCAAqC,GACrC,wCAAwC,EAC5C,KAAK,SACD,eAAe,GACf,sBAAsB,GAAG,QAAQ,SAAS,qCAAqC,GAC/E,sBAAsB,GACtB,eAAe,CACnB,SAAQ,QAAQ;IAChB;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAExC;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEnC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAEpC;;OAEG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAEzC;;;;;;;OAOG;gBACS,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC;IAW7D;;;;OAIG;IACmB,YAAY,CAAC,GAAG,EAAE,GAAG;IA+B3C;;;;;;;;;;;;;OAaG;IACU,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC;IA8BpD;;;;;;;;OAQG;IACH,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,OAAO;IAOvC;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc;IAQ5C;;;;;;OAMG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,cAAc;IAI5C;;;;OAIG;IACI,gBAAgB;IAYvB;;;;;;;OAOG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,IAAI,gBAAgB;IAcxE;;;;;;OAMG;IACH,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IASpD;;;;;;OAMG;IACH,SAAS,CAAC,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBzE;;;;OAIG;IACH,SAAS,CAAC,8BAA8B,CAAC,KAAK,EAAE,gBAAgB;IAMhE;;;;;;OAMG;IACH,SAAS,CAAC,oBAAoB,CAAC,QAAQ,EAAE,GAAG;IAQ5C;;;;;OAKG;IACH,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,gBAAgB;IAM5C;;;;;;;;OAQG;cACa,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IASxD;;;;;;;;;;OAUG;cACa,kBAAkB,CAChC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,sBAAsB,CAAC;CA0BnC"}