ism-wifi 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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2026-07-26)
4
+
5
+ - Initial release
6
+ - Interactive setup wizard
7
+ - Connectivity monitoring with configurable interval
8
+ - Automatic login to IIT ISM Dhanbad captive portal
9
+ - Exponential backoff retry logic
10
+ - CLI commands: setup, config, login, reset, version, help
11
+ - Cross-platform support (Windows, Linux, macOS)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ism-wifi contributors
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,66 @@
1
+ # ism-wifi
2
+
3
+ Auto-login to the IIT ISM Dhanbad captive Wi-Fi portal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g ism-wifi
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ ism-wifi
15
+ ```
16
+
17
+ On first run you will be prompted for your credentials. The configuration is saved automatically. The tool monitors your internet connectivity and re-authenticates when the connection drops.
18
+
19
+ ## CLI Commands
20
+
21
+ | Command | Description |
22
+ |---------|-------------|
23
+ | `ism-wifi` | Start monitoring |
24
+ | `ism-wifi setup` | Re-run configuration setup |
25
+ | `ism-wifi config` | Display current configuration |
26
+ | `ism-wifi login` | Single login attempt |
27
+ | `ism-wifi reset` | Delete saved configuration |
28
+ | `ism-wifi change-interval` | Change the connectivity check interval |
29
+ | `ism-wifi version` | Print version |
30
+ | `ism-wifi help` | Display usage |
31
+
32
+ ## Configuration
33
+
34
+ Stored automatically at:
35
+
36
+ - **Windows:** `C:\Users\<username>\.ism-wifi\config.json`
37
+ - **Linux/macOS:** `~/.ism-wifi/config.json`
38
+
39
+ ## How It Works
40
+
41
+ 1. Every N seconds (default 30), the tool checks internet connectivity via `clients3.google.com/generate_204`
42
+ 2. If connectivity is lost, it posts your credentials to the ISM captive portal
43
+ 3. After a 3-second pause, it verifies the connection was restored
44
+ 4. If login fails, it retries with exponential backoff (1s, 2s, 4s, 8s, 16s, 30s)
45
+
46
+ ## Troubleshooting
47
+
48
+ **Login keeps failing:** Run `ism-wifi setup` to re-enter your credentials.
49
+
50
+ **SSL errors:** The tool disables SSL verification by default for the captive portal endpoint.
51
+
52
+ ## FAQ
53
+
54
+ **Q: Does this run in the background?**
55
+ A: No, it runs in the terminal until you press Ctrl+C.
56
+
57
+ **Q: Is my password stored securely?**
58
+ A: It is stored in plain text in your home directory. Use OS-level disk encryption for security.
59
+
60
+ ## Contributing
61
+
62
+ Contributions welcome. Please open an issue or PR.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/index.js'
4
+
5
+ run()
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "ism-wifi",
3
+ "version": "1.0.0",
4
+ "description": "Auto-login to IIT ISM Dhanbad captive Wi-Fi portal",
5
+ "type": "module",
6
+ "bin": {
7
+ "ism-wifi": "./bin/ism-wifi.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "package.json",
13
+ "README.md",
14
+ "LICENSE",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test",
19
+ "start": "node bin/ism-wifi.js"
20
+ },
21
+ "keywords": ["iit-ism", "wifi", "captive-portal", "cli"],
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@inquirer/prompts": "^7.0.0",
25
+ "chalk": "^5.4.0",
26
+ "commander": "^12.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ }
31
+ }
package/src/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ /** @module cli */
2
+
3
+ import { Command } from 'commander'
4
+ import { readFileSync } from 'fs'
5
+ import { dirname, join } from 'path'
6
+ import { fileURLToPath } from 'url'
7
+ import { loadConfig, runSetup, showConfig, deleteConfig, updateInterval } from './config.js'
8
+ import { startMonitor } from './monitor.js'
9
+ import { login } from './login.js'
10
+ import { sleep } from './utils.js'
11
+ import { ok, fail } from './logger.js'
12
+ import { BANNER, NO_CONFIG_MSG, SETUP_HEADER, CONFIG_SAVED_MSG, MONITORING_MSG } from './constants.js'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+ const pkgPath = join(__dirname, '..', 'package.json')
16
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
17
+
18
+ /**
19
+ * Ensure config exists. If not, run setup.
20
+ * @returns {Promise<import('./config.js').Config>}
21
+ */
22
+ async function ensureConfig() {
23
+ const existing = await loadConfig()
24
+ if (existing) return existing
25
+ return runSetup()
26
+ }
27
+
28
+ /**
29
+ * Default action — start monitoring.
30
+ */
31
+ async function defaultAction() {
32
+ const config = await ensureConfig()
33
+ await startMonitor(config)
34
+ }
35
+
36
+ /**
37
+ * Setup command — re-run configuration.
38
+ */
39
+ async function setupAction() {
40
+ await runSetup()
41
+ }
42
+
43
+ /**
44
+ * Config command — display current config.
45
+ */
46
+ async function configAction() {
47
+ const config = await loadConfig()
48
+ if (!config) {
49
+ console.log('\nNo configuration found. Run: ism-wifi setup\n')
50
+ return
51
+ }
52
+ showConfig(config)
53
+ }
54
+
55
+ /**
56
+ * Login command — single login attempt.
57
+ */
58
+ async function loginAction() {
59
+ const config = await loadConfig()
60
+ if (!config) {
61
+ console.log('\nNo configuration found. Run: ism-wifi setup\n')
62
+ return
63
+ }
64
+ console.log('')
65
+ const online = await login(config.username, config.password, config.verifySSL)
66
+ if (online) {
67
+ ok('Access Granted — Internet OK')
68
+ } else {
69
+ fail('Login failed')
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Reset command — delete configuration.
75
+ */
76
+ async function resetAction() {
77
+ const { confirm } = await import('@inquirer/prompts')
78
+ const answer = await confirm({
79
+ message: 'Are you sure you want to delete the saved configuration?',
80
+ default: false,
81
+ })
82
+ if (!answer) {
83
+ console.log('Aborted.')
84
+ return
85
+ }
86
+ const deleted = await deleteConfig()
87
+ if (deleted) {
88
+ console.log('Configuration deleted.')
89
+ } else {
90
+ console.log('No configuration found.')
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Build and return the CLI program.
96
+ * @returns {Command}
97
+ */
98
+ export function createProgram() {
99
+ const program = new Command()
100
+
101
+ program
102
+ .name('ism-wifi')
103
+ .description('Auto-login to IIT ISM Dhanbad captive Wi-Fi')
104
+ .version(pkg.version)
105
+ .helpOption('-h, --help', 'Display help')
106
+
107
+ program
108
+ .command('setup')
109
+ .description('Run interactive configuration setup')
110
+ .action(setupAction)
111
+
112
+ program
113
+ .command('config')
114
+ .description('Display current configuration')
115
+ .action(configAction)
116
+
117
+ program
118
+ .command('login')
119
+ .description('Perform a single login attempt and exit')
120
+ .action(loginAction)
121
+
122
+ program
123
+ .command('reset')
124
+ .description('Delete saved configuration')
125
+ .action(resetAction)
126
+
127
+ program
128
+ .command('change-interval')
129
+ .description('Change the connectivity check interval')
130
+ .action(() => updateInterval())
131
+
132
+ program
133
+ .command('version')
134
+ .description('Print package version')
135
+ .action(() => console.log(pkg.version))
136
+
137
+ program
138
+ .command('help')
139
+ .description('Display usage information')
140
+ .action(() => program.help())
141
+
142
+ return program
143
+ }
144
+
145
+ /**
146
+ * Main entry point.
147
+ */
148
+ export async function run() {
149
+ const program = createProgram()
150
+
151
+ if (process.argv.length <= 2) {
152
+ await defaultAction()
153
+ } else {
154
+ program.parse(process.argv)
155
+ }
156
+ }
package/src/config.js ADDED
@@ -0,0 +1,115 @@
1
+ /** @module config */
2
+
3
+ import { readFile, writeFile, unlink, mkdir } from 'fs/promises'
4
+ import { existsSync } from 'fs'
5
+ import { input, password, confirm, number } from '@inquirer/prompts'
6
+ import { getConfigDir, getConfigPath, clampDelay, maskPassword } from './utils.js'
7
+ import { BANNER, NO_CONFIG_MSG, SETUP_HEADER, CONFIG_SAVED_MSG, DEFAULT_CHECK_INTERVAL_S, MAX_RETRY_DELAY_S } from './constants.js'
8
+ import { plain, info, ok } from './logger.js'
9
+
10
+ /**
11
+ * @typedef {Object} Config
12
+ * @property {string} username
13
+ * @property {string} password
14
+ * @property {number} checkInterval
15
+ * @property {boolean} verifySSL
16
+ */
17
+
18
+ /**
19
+ * Load configuration from disk.
20
+ * @returns {Promise<Config|null>}
21
+ */
22
+ export async function loadConfig() {
23
+ const configPath = getConfigPath()
24
+ try {
25
+ const data = await readFile(configPath, 'utf-8')
26
+ return JSON.parse(data)
27
+ } catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Save configuration to disk.
34
+ * @param {Config} config
35
+ * @returns {Promise<void>}
36
+ */
37
+ export async function saveConfig(config) {
38
+ const dir = getConfigDir()
39
+ await mkdir(dir, { recursive: true })
40
+ await writeFile(getConfigPath(), JSON.stringify(config, null, 4), 'utf-8')
41
+ }
42
+
43
+ /**
44
+ * Delete the configuration file.
45
+ * @returns {Promise<boolean>}
46
+ */
47
+ export async function deleteConfig() {
48
+ const configPath = getConfigPath()
49
+ if (!existsSync(configPath)) return false
50
+ await unlink(configPath)
51
+ return true
52
+ }
53
+
54
+ /**
55
+ * Update the check interval interactively.
56
+ * @returns {Promise<void>}
57
+ */
58
+ export async function updateInterval() {
59
+ const config = await loadConfig()
60
+ if (!config) {
61
+ console.log('\nNo configuration found. Run: ism-wifi setup first.\n')
62
+ return
63
+ }
64
+
65
+ const oldInterval = config.checkInterval
66
+ const intervalStr = await input({
67
+ message: `New check interval in seconds (current: ${oldInterval}):`,
68
+ default: String(oldInterval),
69
+ validate: (val) => {
70
+ const num = Number(val)
71
+ if (Number.isNaN(num) || num < 1) return 'Must be a positive number'
72
+ if (num > MAX_RETRY_DELAY_S) return `Maximum is ${MAX_RETRY_DELAY_S} seconds`
73
+ return true
74
+ },
75
+ })
76
+
77
+ config.checkInterval = clampDelay(Number(intervalStr) || oldInterval)
78
+ await saveConfig(config)
79
+ console.log(`\nInterval updated: ${oldInterval} → ${config.checkInterval} seconds\n`)
80
+ }
81
+
82
+ /**
83
+ * Run interactive first-time (or re-) configuration.
84
+ * @returns {Promise<Config>}
85
+ */
86
+ export async function runSetup() {
87
+ plain(BANNER)
88
+ plain(NO_CONFIG_MSG)
89
+ info(SETUP_HEADER)
90
+
91
+ const username = await input({ message: 'Username:', required: true })
92
+ const pwd = await password({ message: 'Password:', mask: true, required: true })
93
+ const intervalStr = await input({
94
+ message: 'Connectivity check interval [30]:',
95
+ default: String(DEFAULT_CHECK_INTERVAL_S),
96
+ })
97
+ const checkInterval = clampDelay(Number(intervalStr) || DEFAULT_CHECK_INTERVAL_S)
98
+
99
+ const config = { username, password: pwd, checkInterval, verifySSL: false }
100
+ await saveConfig(config)
101
+ plain(CONFIG_SAVED_MSG)
102
+ return config
103
+ }
104
+
105
+ /**
106
+ * Display current configuration (password masked).
107
+ * @param {Config} config
108
+ */
109
+ export function showConfig(config) {
110
+ const { username, password, checkInterval, verifySSL } = config
111
+ console.log(`\nUsername : ${username}`)
112
+ console.log(`\nInterval : ${checkInterval} seconds`)
113
+ console.log(`\nSSL Verify : ${verifySSL}`)
114
+ console.log(`\nPassword : ${maskPassword(password)}\n`)
115
+ }
@@ -0,0 +1,22 @@
1
+ /** @module connectivity */
2
+
3
+ import { CONNECTIVITY_URL, CONNECTIVITY_TIMEOUT_MS } from './constants.js'
4
+
5
+ /**
6
+ * Check whether the internet is reachable.
7
+ * Sends a GET to clients3.google.com/generate_204.
8
+ * Returns true only on HTTP 204.
9
+ * @returns {Promise<boolean>}
10
+ */
11
+ export async function checkConnectivity() {
12
+ try {
13
+ const response = await fetch(CONNECTIVITY_URL, {
14
+ method: 'GET',
15
+ redirect: 'manual',
16
+ signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS),
17
+ })
18
+ return response.status === 204
19
+ } catch {
20
+ return false
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ /** @module constants */
2
+
3
+ export const PORTAL_URL = 'https://netaccess.iitism.ac.in:6082/?url='
4
+ export const CONNECTIVITY_URL = 'https://clients3.google.com/generate_204'
5
+ export const CONFIG_DIR_NAME = '.ism-wifi'
6
+ export const CONFIG_FILE_NAME = 'config.json'
7
+ export const CONNECTIVITY_TIMEOUT_MS = 10_000
8
+ export const VERIFY_WAIT_MS = 3_000
9
+ export const DEFAULT_CHECK_INTERVAL_S = 30
10
+ export const RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30]
11
+ export const MAX_RETRY_DELAY_S = 30
12
+
13
+ export const BANNER = `
14
+ ======================================
15
+ ISM WiFi Auto Login
16
+ ======================================
17
+ `
18
+
19
+ export const NO_CONFIG_MSG = '\nNo configuration found.\n'
20
+ export const SETUP_HEADER = "Let's configure ISM WiFi."
21
+ export const CONFIG_SAVED_MSG = '\nConfiguration saved.\n'
22
+ export const MONITORING_MSG = '\nStarting monitoring...\n'
23
+ export const STOPPING_MSG = '\nStopping ISM WiFi...\n'
24
+ export const GOODBYE_MSG = 'Goodbye!'
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from './cli.js'
package/src/logger.js ADDED
@@ -0,0 +1,46 @@
1
+ /** @module logger */
2
+
3
+ import chalk from 'chalk'
4
+ import { formatTime } from './utils.js'
5
+
6
+ /**
7
+ * Core log function.
8
+ * @param {string} message
9
+ * @param {'ok'|'fail'|'info'|'warn'|'granted'} type
10
+ */
11
+ function log(message, type) {
12
+ const timestamp = chalk.gray(`[${formatTime()}]`)
13
+ let symbol
14
+ switch (type) {
15
+ case 'ok':
16
+ symbol = chalk.green('✓')
17
+ break
18
+ case 'fail':
19
+ symbol = chalk.red('✗')
20
+ break
21
+ case 'granted':
22
+ symbol = chalk.green('✓')
23
+ break
24
+ default:
25
+ symbol = ''
26
+ }
27
+ console.log(`\n${timestamp} ${symbol} ${message}`)
28
+ }
29
+
30
+ /** @param {string} msg */
31
+ export function ok(msg) { log(msg, 'ok') }
32
+
33
+ /** @param {string} msg */
34
+ export function fail(msg) { log(msg, 'fail') }
35
+
36
+ /** @param {string} msg */
37
+ export function info(msg) { log(msg, 'info') }
38
+
39
+ /** @param {string} msg */
40
+ export function warn(msg) { log(msg, 'warn') }
41
+
42
+ /** @param {string} msg */
43
+ export function granted(msg) { log(msg, 'granted') }
44
+
45
+ /** @param {string} msg */
46
+ export function plain(msg) { console.log(msg) }
package/src/login.js ADDED
@@ -0,0 +1,38 @@
1
+ /** @module login */
2
+
3
+ import { Agent } from 'https'
4
+ import { PORTAL_URL, VERIFY_WAIT_MS } from './constants.js'
5
+ import { sleep } from './utils.js'
6
+ import { checkConnectivity } from './connectivity.js'
7
+
8
+ /**
9
+ * Attempt to log in to the ISM Wi-Fi portal.
10
+ * Posts credentials, waits, then verifies connectivity.
11
+ * @param {string} username
12
+ * @param {string} password
13
+ * @param {boolean} verifySSL
14
+ * @returns {Promise<boolean>}
15
+ */
16
+ export async function login(username, password, verifySSL) {
17
+ const body = new URLSearchParams({ username, password })
18
+
19
+ const agent = verifySSL ? undefined : new Agent({ rejectUnauthorized: false })
20
+
21
+ try {
22
+ const response = await fetch(PORTAL_URL, {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
25
+ body: body.toString(),
26
+ signal: AbortSignal.timeout(15_000),
27
+ ...(agent ? { agent } : {}),
28
+ })
29
+
30
+ if (!response.ok) return false
31
+
32
+ await sleep(VERIFY_WAIT_MS)
33
+ return checkConnectivity()
34
+ } catch {
35
+ await sleep(VERIFY_WAIT_MS)
36
+ return checkConnectivity()
37
+ }
38
+ }
package/src/monitor.js ADDED
@@ -0,0 +1,68 @@
1
+ /** @module monitor */
2
+
3
+ import { checkConnectivity } from './connectivity.js'
4
+ import { login } from './login.js'
5
+ import { sleep } from './utils.js'
6
+ import { RETRY_DELAYS_S, MONITORING_MSG, STOPPING_MSG, GOODBYE_MSG } from './constants.js'
7
+ import { ok, fail, info, granted, plain } from './logger.js'
8
+
9
+ let running = false
10
+
11
+ /**
12
+ * Start the monitoring loop.
13
+ * @param {import('./config.js').Config} config
14
+ */
15
+ export async function startMonitor(config) {
16
+ running = true
17
+
18
+ process.on('SIGINT', handleSigint)
19
+
20
+ plain(MONITORING_MSG)
21
+
22
+ while (running) {
23
+ const online = await checkConnectivity()
24
+
25
+ if (online) {
26
+ ok('Internet OK')
27
+ } else {
28
+ fail('Internet unavailable')
29
+ await attemptLoginWithBackoff(config)
30
+ }
31
+
32
+ if (!running) break
33
+ await sleep(config.checkInterval * 1000)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Attempt login with exponential backoff.
39
+ * @param {import('./config.js').Config} config
40
+ */
41
+ async function attemptLoginWithBackoff(config) {
42
+ for (const delay of RETRY_DELAYS_S) {
43
+ if (!running) return
44
+
45
+ info('Logging in...')
46
+
47
+ const success = await login(config.username, config.password, config.verifySSL)
48
+
49
+ if (success) {
50
+ granted('Access Granted')
51
+ ok('Internet restored')
52
+ return
53
+ }
54
+
55
+ fail('Login failed, retrying...')
56
+ await sleep(delay * 1000)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Handle Ctrl+C gracefully.
62
+ */
63
+ function handleSigint() {
64
+ running = false
65
+ plain(STOPPING_MSG)
66
+ plain(GOODBYE_MSG)
67
+ process.exit(0)
68
+ }
package/src/utils.js ADDED
@@ -0,0 +1,61 @@
1
+ /** @module utils */
2
+
3
+ import { homedir } from 'os'
4
+ import { join } from 'path'
5
+ import { CONFIG_DIR_NAME, CONFIG_FILE_NAME, MAX_RETRY_DELAY_S } from './constants.js'
6
+
7
+ /**
8
+ * Sleep for a given number of milliseconds.
9
+ * @param {number} ms
10
+ * @returns {Promise<void>}
11
+ */
12
+ export function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(resolve, ms))
14
+ }
15
+
16
+ /**
17
+ * Format a Date as HH:MM:SS.
18
+ * @param {Date} [date]
19
+ * @returns {string}
20
+ */
21
+ export function formatTime(date = new Date()) {
22
+ const h = String(date.getHours()).padStart(2, '0')
23
+ const m = String(date.getMinutes()).padStart(2, '0')
24
+ const s = String(date.getSeconds()).padStart(2, '0')
25
+ return `${h}:${m}:${s}`
26
+ }
27
+
28
+ /**
29
+ * Mask a string, showing only the first character.
30
+ * @param {string} str
31
+ * @returns {string}
32
+ */
33
+ export function maskPassword(str) {
34
+ if (!str || str.length === 0) return ''
35
+ return str[0] + '*'.repeat(str.length - 1)
36
+ }
37
+
38
+ /**
39
+ * Get the config directory path.
40
+ * @returns {string}
41
+ */
42
+ export function getConfigDir() {
43
+ return join(homedir(), CONFIG_DIR_NAME)
44
+ }
45
+
46
+ /**
47
+ * Get the full config file path.
48
+ * @returns {string}
49
+ */
50
+ export function getConfigPath() {
51
+ return join(getConfigDir(), CONFIG_FILE_NAME)
52
+ }
53
+
54
+ /**
55
+ * Clamp a value to max 30 seconds.
56
+ * @param {number} seconds
57
+ * @returns {number}
58
+ */
59
+ export function clampDelay(seconds) {
60
+ return Math.min(seconds, MAX_RETRY_DELAY_S)
61
+ }