@volcanicminds/tools 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Volcanic Minds
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,64 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+ [![opensource](https://img.shields.io/badge/open-source-blue)](https://en.wikipedia.org/wiki/Open_source)
3
+ [![volcanic-typeorm](https://img.shields.io/badge/volcanic-minds-orange)](https://github.com/volcanicminds/volcanic-typeorm)
4
+ [![npm](https://img.shields.io/badge/package-npm-white)](https://www.npmjs.com/package/@volcanicminds/tools)
5
+
6
+ # volcanic-tools
7
+
8
+ Tools for the volcanic (minds) backend. This library provides a collection of modular utilities designed to be tree-shakeable.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @volcanicminds/tools
14
+ ```
15
+
16
+ ## How to upgrade packages
17
+
18
+ ```bash
19
+ npm run upgrade-deps
20
+ ```
21
+
22
+ ## Environment
23
+
24
+ ```bash
25
+ # or automatically use LOG_LEVEL
26
+ SOME_KEY=true
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ This package supports both root imports and sub-path imports to optimize bundle size.
32
+
33
+ ### Import everything
34
+
35
+ ```typescript
36
+ import { mfa, log } from '@volcanicminds/tools'
37
+ ```
38
+
39
+ ### Import specific features (Recommended for smaller bundles)
40
+
41
+ ```typescript
42
+ import * as mfa from '@volcanicminds/tools/mfa'
43
+ import * as logger from '@volcanicminds/tools/logger'
44
+ ```
45
+
46
+ ## Features
47
+
48
+ ### MFA (Multi-Factor Authentication)
49
+
50
+ Utilities for generating secrets, QR codes, and verifying TOTP tokens.
51
+
52
+ ```typescript
53
+ import * as mfa from '@volcanicminds/tools/mfa'
54
+
55
+ // Generate Setup
56
+ const { secret, uri, qrCode } = await mfa.generateSetupDetails('MyApp', 'user@example.com')
57
+
58
+ // Verify Token
59
+ const isValid = mfa.verifyToken('123456', secret)
60
+ ```
61
+
62
+ ## Logging
63
+
64
+ Use Pino logger if in your project you have a `global.log` with a valid instance.
@@ -0,0 +1,3 @@
1
+ export * as mfa from './lib/mfa/index.js';
2
+ export * as log from './lib/util/logger.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * as mfa from './lib/mfa/index.js';
2
+ export * as log from './lib/util/logger.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare function main(): void;
2
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../lib/main.ts"],"names":[],"mappings":"AAAA,wBAAgB,IAAI,SAEnB"}
@@ -0,0 +1,4 @@
1
+ export function main() {
2
+ console.log('This is the main export');
3
+ }
4
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../lib/main.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAA;AACxC,CAAC"}
@@ -0,0 +1,10 @@
1
+ export interface MfaSetupDetails {
2
+ secret: string;
3
+ uri: string;
4
+ qrCode: string;
5
+ }
6
+ export declare function generateSecret(size?: number): string;
7
+ export declare function generateSetupDetails(appName: string, username: string, secret?: string): Promise<MfaSetupDetails>;
8
+ export declare function verifyToken(token: string, secret: string, window?: number): boolean;
9
+ export declare function generateToken(secret: string): string;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/mfa/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf;AAOD,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAW,GAAG,MAAM,CAGxD;AAQD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,CAAC,CAqB1B;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU,GAAG,OAAO,CAatF;AAOD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASpD"}
@@ -0,0 +1,47 @@
1
+ import * as OTPAuth from 'otpauth';
2
+ import QRCode from 'qrcode';
3
+ export function generateSecret(size = 20) {
4
+ const secret = new OTPAuth.Secret({ size });
5
+ return secret.base32;
6
+ }
7
+ export async function generateSetupDetails(appName, username, secret) {
8
+ const mfaSecret = secret ? OTPAuth.Secret.fromBase32(secret) : new OTPAuth.Secret({ size: 20 });
9
+ const base32Secret = mfaSecret.base32;
10
+ const totp = new OTPAuth.TOTP({
11
+ issuer: appName,
12
+ label: username,
13
+ algorithm: 'SHA1',
14
+ digits: 6,
15
+ period: 30,
16
+ secret: mfaSecret
17
+ });
18
+ const uri = totp.toString();
19
+ const qrCode = await QRCode.toDataURL(uri);
20
+ return {
21
+ secret: base32Secret,
22
+ uri,
23
+ qrCode
24
+ };
25
+ }
26
+ export function verifyToken(token, secret, window = 1) {
27
+ if (!token || !secret)
28
+ return false;
29
+ const totp = new OTPAuth.TOTP({
30
+ algorithm: 'SHA1',
31
+ digits: 6,
32
+ period: 30,
33
+ secret: OTPAuth.Secret.fromBase32(secret)
34
+ });
35
+ const delta = totp.validate({ token, window });
36
+ return delta !== null;
37
+ }
38
+ export function generateToken(secret) {
39
+ const totp = new OTPAuth.TOTP({
40
+ algorithm: 'SHA1',
41
+ digits: 6,
42
+ period: 30,
43
+ secret: OTPAuth.Secret.fromBase32(secret)
44
+ });
45
+ return totp.generate();
46
+ }
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/mfa/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAClC,OAAO,MAAM,MAAM,QAAQ,CAAA;AAa3B,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE;IAC9C,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,OAAO,MAAM,CAAC,MAAM,CAAA;AACtB,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,QAAgB,EAChB,MAAe;IAEf,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAC/F,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,CAAA;IAErC,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;QAC5B,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,SAAS;KAClB,CAAC,CAAA;IAEF,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;IAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;IAE1C,OAAO;QACL,MAAM,EAAE,YAAY;QACpB,GAAG;QACH,MAAM;KACP,CAAA;AACH,CAAC;AASD,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,MAAc,EAAE,SAAiB,CAAC;IAC3E,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEnC,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;QAC5B,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;KAC1C,CAAC,CAAA;IAGF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAC9C,OAAO,KAAK,KAAK,IAAI,CAAA;AACvB,CAAC;AAOD,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;QAC5B,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;KAC1C,CAAC,CAAA;IAEF,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAA;AACxB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare function trace(data: any): void;
2
+ export declare function debug(data: any): void;
3
+ export declare function info(data: any): void;
4
+ export declare function warn(data: any): void;
5
+ export declare function error(data: any): void;
6
+ export declare function fatal(data: any): void;
7
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../lib/util/logger.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,IAAI,KAAA,QAEzB;AAED,wBAAgB,KAAK,CAAC,IAAI,KAAA,QAEzB;AAED,wBAAgB,IAAI,CAAC,IAAI,KAAA,QAExB;AAED,wBAAgB,IAAI,CAAC,IAAI,KAAA,QAExB;AAED,wBAAgB,KAAK,CAAC,IAAI,KAAA,QAEzB;AAED,wBAAgB,KAAK,CAAC,IAAI,KAAA,QAEzB"}
@@ -0,0 +1,19 @@
1
+ export function trace(data) {
2
+ global.isLoggingEnabled && global.log?.trace && global.log.trace(data);
3
+ }
4
+ export function debug(data) {
5
+ global.isLoggingEnabled && global.log?.debug && global.log.debug(data);
6
+ }
7
+ export function info(data) {
8
+ global.isLoggingEnabled && global.log?.info && global.log.info(data);
9
+ }
10
+ export function warn(data) {
11
+ global.isLoggingEnabled && global.log?.warn && global.log.warn(data);
12
+ }
13
+ export function error(data) {
14
+ global.isLoggingEnabled && global.log?.error && global.log.error(data);
15
+ }
16
+ export function fatal(data) {
17
+ global.isLoggingEnabled && global.log?.fatal && global.log.fatal(data);
18
+ }
19
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../lib/util/logger.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,KAAK,CAAC,IAAI;IACxB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,IAAI;IACxB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,IAAI;IACvB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACtE,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,IAAI;IACvB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACtE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,IAAI;IACxB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,IAAI;IACxB,MAAM,CAAC,gBAAgB,IAAI,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACxE,CAAC"}
package/lib/main.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function main() {
2
+ console.log('This is the main export')
3
+ }
@@ -0,0 +1,89 @@
1
+ import * as OTPAuth from 'otpauth'
2
+ import QRCode from 'qrcode'
3
+
4
+ export interface MfaSetupDetails {
5
+ secret: string
6
+ uri: string
7
+ qrCode: string
8
+ }
9
+
10
+ /**
11
+ * Generates a new random base32 secret.
12
+ * @param size The size of the secret in bytes (default 20).
13
+ * @returns The base32 string representation of the secret.
14
+ */
15
+ export function generateSecret(size: number = 20): string {
16
+ const secret = new OTPAuth.Secret({ size })
17
+ return secret.base32
18
+ }
19
+
20
+ /**
21
+ * Generates setup details for the client, including the secret, the otpauth URI, and a QR code Data URL.
22
+ * @param appName The name of the application (Issuer).
23
+ * @param username The username (Label).
24
+ * @param secret The base32 secret (optional, will be generated if not provided).
25
+ */
26
+ export async function generateSetupDetails(
27
+ appName: string,
28
+ username: string,
29
+ secret?: string
30
+ ): Promise<MfaSetupDetails> {
31
+ const mfaSecret = secret ? OTPAuth.Secret.fromBase32(secret) : new OTPAuth.Secret({ size: 20 })
32
+ const base32Secret = mfaSecret.base32
33
+
34
+ const totp = new OTPAuth.TOTP({
35
+ issuer: appName,
36
+ label: username,
37
+ algorithm: 'SHA1',
38
+ digits: 6,
39
+ period: 30,
40
+ secret: mfaSecret
41
+ })
42
+
43
+ const uri = totp.toString()
44
+ const qrCode = await QRCode.toDataURL(uri)
45
+
46
+ return {
47
+ secret: base32Secret,
48
+ uri,
49
+ qrCode
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validates a TOTP token against a secret.
55
+ * @param token The token to verify.
56
+ * @param secret The base32 secret.
57
+ * @param window The acceptable time window (default 1, meaning +/- 30 seconds).
58
+ * @returns True if valid, false otherwise.
59
+ */
60
+ export function verifyToken(token: string, secret: string, window: number = 1): boolean {
61
+ if (!token || !secret) return false
62
+
63
+ const totp = new OTPAuth.TOTP({
64
+ algorithm: 'SHA1',
65
+ digits: 6,
66
+ period: 30,
67
+ secret: OTPAuth.Secret.fromBase32(secret)
68
+ })
69
+
70
+ // validate returns null if invalid, or the delta (integer) if valid
71
+ const delta = totp.validate({ token, window })
72
+ return delta !== null
73
+ }
74
+
75
+ /**
76
+ * Generates the current TOTP token for a given secret.
77
+ * Useful for testing or recovery scenarios.
78
+ * @param secret The base32 secret.
79
+ */
80
+ export function generateToken(secret: string): string {
81
+ const totp = new OTPAuth.TOTP({
82
+ algorithm: 'SHA1',
83
+ digits: 6,
84
+ period: 30,
85
+ secret: OTPAuth.Secret.fromBase32(secret)
86
+ })
87
+
88
+ return totp.generate()
89
+ }
@@ -0,0 +1,23 @@
1
+ export function trace(data) {
2
+ global.isLoggingEnabled && global.log?.trace && global.log.trace(data)
3
+ }
4
+
5
+ export function debug(data) {
6
+ global.isLoggingEnabled && global.log?.debug && global.log.debug(data)
7
+ }
8
+
9
+ export function info(data) {
10
+ global.isLoggingEnabled && global.log?.info && global.log.info(data)
11
+ }
12
+
13
+ export function warn(data) {
14
+ global.isLoggingEnabled && global.log?.warn && global.log.warn(data)
15
+ }
16
+
17
+ export function error(data) {
18
+ global.isLoggingEnabled && global.log?.error && global.log.error(data)
19
+ }
20
+
21
+ export function fatal(data) {
22
+ global.isLoggingEnabled && global.log?.fatal && global.log.fatal(data)
23
+ }
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@volcanicminds/tools",
3
+ "version": "0.0.2",
4
+ "description": "Tools for the volcanic (minds) backend",
5
+ "keywords": [
6
+ "volcanic",
7
+ "open source",
8
+ "tools",
9
+ "typescript",
10
+ "esm",
11
+ "mfa",
12
+ "otp",
13
+ "totp"
14
+ ],
15
+ "homepage": "https://volcanicminds.com",
16
+ "bugs": {
17
+ "url": "https://github.com/volcanicminds/volcanic-tools/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/volcanicminds/volcanic-tools.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Volcanic Minds <developers@volcanicminds.com> (https://volcanicminds.com)",
25
+ "maintainers": [
26
+ {
27
+ "name": "Developers",
28
+ "email": "developers@volcanicminds.com",
29
+ "url": "https://volcanicminds.com"
30
+ }
31
+ ],
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.js"
38
+ },
39
+ "./mfa": {
40
+ "types": "./dist/lib/mfa/index.d.ts",
41
+ "import": "./dist/lib/mfa/index.js",
42
+ "require": "./dist/lib/mfa/index.js"
43
+ },
44
+ "./logger": {
45
+ "types": "./dist/lib/util/logger.d.ts",
46
+ "import": "./dist/lib/util/logger.js",
47
+ "require": "./dist/lib/util/logger.js"
48
+ }
49
+ },
50
+ "main": "dist/index.js",
51
+ "types": "dist/index.d.ts",
52
+ "directories": {
53
+ "lib": "lib"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "lib"
58
+ ],
59
+ "scripts": {
60
+ "clean": "rm -rf dist",
61
+ "prebuild": "npm run clean",
62
+ "build": "tsc",
63
+ "reset": "npm install && npm update && npm run build",
64
+ "upgrade-deps": "npx npm-check-updates -u",
65
+ "combine": "node combine.js"
66
+ },
67
+ "dependencies": {
68
+ "otpauth": "^9.3.5",
69
+ "qrcode": "^1.5.4"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^24.10.1",
73
+ "@types/qrcode": "^1.5.5",
74
+ "tsx": "^4.19.2",
75
+ "typescript": "^5.9.3"
76
+ },
77
+ "engines": {
78
+ "node": ">=24"
79
+ },
80
+ "module": "dist/index.js",
81
+ "sideEffects": false,
82
+ "typesVersions": {
83
+ "*": {
84
+ "*": [
85
+ "dist/index.d.ts"
86
+ ],
87
+ "mfa": [
88
+ "dist/lib/mfa/index.d.ts"
89
+ ],
90
+ "logger": [
91
+ "dist/lib/util/logger.d.ts"
92
+ ]
93
+ }
94
+ }
95
+ }