odac 0.9.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/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const {spawn} = require('child_process')
|
|
2
|
+
const fs = require('fs').promises
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// --- Constants ---
|
|
7
|
+
const CANDYPACK_HOME = path.join(os.homedir(), '.candypack')
|
|
8
|
+
const LOG_DIR = path.join(CANDYPACK_HOME, 'logs')
|
|
9
|
+
const SERVER_SCRIPT_PATH = path.join(__dirname, '..', '..', 'server', 'index.js')
|
|
10
|
+
|
|
11
|
+
const MAX_RESTARTS_IN_WINDOW = 100
|
|
12
|
+
const RESTART_WINDOW_MS = 1000 * 60 * 5 // 5 minutes
|
|
13
|
+
const SAVE_INTERVAL_MS = 1000 // 1 second
|
|
14
|
+
|
|
15
|
+
class Watchdog {
|
|
16
|
+
#logBuffer = ''
|
|
17
|
+
#errorBuffer = ''
|
|
18
|
+
#restartCount = 0
|
|
19
|
+
#lastRestartTimestamp = 0
|
|
20
|
+
#isSaving = false
|
|
21
|
+
|
|
22
|
+
init() {
|
|
23
|
+
// Set up periodic log saving. This is done only once.
|
|
24
|
+
setInterval(() => this.#saveLogs(), SAVE_INTERVAL_MS)
|
|
25
|
+
|
|
26
|
+
// Start the server for the first time.
|
|
27
|
+
this.#startServer()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Saves the buffered logs to their respective files.
|
|
32
|
+
* This function is designed to be called periodically.
|
|
33
|
+
* Keeps only the last 1000 lines in each log file.
|
|
34
|
+
*/
|
|
35
|
+
async #saveLogs() {
|
|
36
|
+
if (this.#isSaving) return
|
|
37
|
+
this.#isSaving = true
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Ensure log directory exists before attempting to write files
|
|
41
|
+
await fs.mkdir(LOG_DIR, {recursive: true})
|
|
42
|
+
const logFile = path.join(LOG_DIR, '.candypack.log')
|
|
43
|
+
const errFile = path.join(LOG_DIR, '.candypack_err.log')
|
|
44
|
+
|
|
45
|
+
// Limit log buffer to last 1000 lines
|
|
46
|
+
const logLines = this.#logBuffer.split('\n')
|
|
47
|
+
if (logLines.length > 1000) {
|
|
48
|
+
this.#logBuffer = logLines.slice(-1000).join('\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Limit error buffer to last 1000 lines
|
|
52
|
+
const errLines = this.#errorBuffer.split('\n')
|
|
53
|
+
if (errLines.length > 1000) {
|
|
54
|
+
this.#errorBuffer = errLines.slice(-1000).join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await fs.writeFile(logFile, this.#logBuffer, 'utf8')
|
|
58
|
+
await fs.writeFile(errFile, this.#errorBuffer, 'utf8')
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to save logs:', error)
|
|
61
|
+
} finally {
|
|
62
|
+
this.#isSaving = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Performs startup checks to ensure a clean environment.
|
|
68
|
+
* It kills any old watchdog or server processes that might still be running.
|
|
69
|
+
* It also creates the necessary configuration files and directories if they don't exist.
|
|
70
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the checks pass.
|
|
71
|
+
*/
|
|
72
|
+
async #performStartupChecks() {
|
|
73
|
+
try {
|
|
74
|
+
// Kill previous watchdog process if it exists and is different from the current one
|
|
75
|
+
if (Candy.core('Config').config.server.watchdog && Candy.core('Config').config.server.watchdog !== process.pid)
|
|
76
|
+
await Candy.core('Process').stop(Candy.core('Config').config.server.watchdog)
|
|
77
|
+
|
|
78
|
+
// Kill previous server process if it exists
|
|
79
|
+
if (Candy.core('Config').config.server.pid) await Candy.core('Process').stop(Candy.core('Config').config.server.pid)
|
|
80
|
+
|
|
81
|
+
for (const domain of Object.keys(Candy.core('Config').config?.websites ?? []))
|
|
82
|
+
if (Candy.core('Config').config.websites[domain].pid)
|
|
83
|
+
await Candy.core('Process').stop(Candy.core('Config').config.websites[domain].pid)
|
|
84
|
+
|
|
85
|
+
for (const service of Candy.core('Config').config.services ?? []) if (service.pid) await Candy.core('Process').stop(service.pid)
|
|
86
|
+
|
|
87
|
+
// Update config with current watchdog's info
|
|
88
|
+
Candy.core('Config').config.server.watchdog = process.pid
|
|
89
|
+
Candy.core('Config').config.server.started = Date.now()
|
|
90
|
+
Candy.core('Config').force()
|
|
91
|
+
|
|
92
|
+
return new Promise(resolve => setTimeout(() => resolve(true), 1000))
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Error during startup checks:', error)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Starts the server process and sets up monitoring.
|
|
101
|
+
*/
|
|
102
|
+
async #startServer() {
|
|
103
|
+
const checksPassed = await this.#performStartupChecks()
|
|
104
|
+
if (!checksPassed) {
|
|
105
|
+
console.error('Startup checks failed. Aborting.')
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Ensure log directory exists before starting
|
|
110
|
+
await fs.mkdir(LOG_DIR, {recursive: true})
|
|
111
|
+
|
|
112
|
+
const child = spawn('node', [SERVER_SCRIPT_PATH])
|
|
113
|
+
|
|
114
|
+
process.on('exit', () => child.kill())
|
|
115
|
+
|
|
116
|
+
Candy.core('Config').config.server.pid = child.pid
|
|
117
|
+
|
|
118
|
+
console.log(`Watchdog process started with PID: ${process.pid}`)
|
|
119
|
+
console.log(`Server process started with PID: ${child.pid}`)
|
|
120
|
+
|
|
121
|
+
child.stdout.on('data', data => {
|
|
122
|
+
this.#logBuffer += `[LOG][${new Date().toISOString()}] ${data.toString()}`
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
child.stderr.on('data', data => {
|
|
126
|
+
this.#logBuffer += `[ERR][${new Date().toISOString()}] ${data.toString()}`
|
|
127
|
+
this.#errorBuffer += `[ERR][${new Date().toISOString()}] ${data.toString()}`
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
child.on('close', code => {
|
|
131
|
+
Candy.core('Config').reload()
|
|
132
|
+
this.#errorBuffer += `[ERR][${new Date().toISOString()}] Process closed with code ${code}\n`
|
|
133
|
+
|
|
134
|
+
// Reset restart count if the last restart was a while ago
|
|
135
|
+
if (Date.now() - this.#lastRestartTimestamp > RESTART_WINDOW_MS) {
|
|
136
|
+
this.#restartCount = 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.#restartCount++
|
|
140
|
+
this.#lastRestartTimestamp = Date.now()
|
|
141
|
+
|
|
142
|
+
// If restart limit is not exceeded, restart the server
|
|
143
|
+
if (this.#restartCount < MAX_RESTARTS_IN_WINDOW) {
|
|
144
|
+
console.log('Server process closed. Restarting...')
|
|
145
|
+
// Relaunch the server process without setting up new intervals
|
|
146
|
+
this.#startServer()
|
|
147
|
+
} else {
|
|
148
|
+
console.error('Server has crashed too many times. Not restarting.')
|
|
149
|
+
// Final attempt to save logs before exiting
|
|
150
|
+
this.#saveLogs().then(() => process.exit(1))
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = new Watchdog()
|
package/web/config.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* About Page Controller
|
|
3
|
+
*
|
|
4
|
+
* This controller renders the about page using CandyPack's skeleton-based view system.
|
|
5
|
+
* Provides information about CandyPack and its key components.
|
|
6
|
+
*
|
|
7
|
+
* For AJAX requests, only content is returned. For full page loads, skeleton + content.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = function (Candy) {
|
|
11
|
+
// Set variables for AJAX responses
|
|
12
|
+
Candy.set(
|
|
13
|
+
{
|
|
14
|
+
pageTitle: 'About CandyPack',
|
|
15
|
+
version: '1.0.0'
|
|
16
|
+
},
|
|
17
|
+
true
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Candy.View.set({
|
|
21
|
+
skeleton: 'main',
|
|
22
|
+
head: 'main',
|
|
23
|
+
header: 'main',
|
|
24
|
+
content: 'about',
|
|
25
|
+
footer: 'main'
|
|
26
|
+
})
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Page Controller
|
|
3
|
+
*
|
|
4
|
+
* This controller renders the home page using CandyPack's skeleton-based view system.
|
|
5
|
+
* The skeleton provides the layout (header, nav, footer) and the view provides the content.
|
|
6
|
+
*
|
|
7
|
+
* For AJAX requests (candy-link navigation), only the content is returned.
|
|
8
|
+
* For full page loads, skeleton + content is returned.
|
|
9
|
+
*
|
|
10
|
+
* This page demonstrates:
|
|
11
|
+
* - Modern, responsive design
|
|
12
|
+
* - candy.js AJAX form handling
|
|
13
|
+
* - candy.js GET requests
|
|
14
|
+
* - Dynamic page loading with candy-link
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = function (Candy) {
|
|
18
|
+
// Set variables that will be available in AJAX responses
|
|
19
|
+
Candy.set(
|
|
20
|
+
{
|
|
21
|
+
welcomeMessage: 'Welcome to CandyPack!',
|
|
22
|
+
timestamp: Date.now()
|
|
23
|
+
},
|
|
24
|
+
true
|
|
25
|
+
) // true = include in AJAX responses
|
|
26
|
+
|
|
27
|
+
Candy.View.set({
|
|
28
|
+
skeleton: 'main',
|
|
29
|
+
head: 'main',
|
|
30
|
+
header: 'main',
|
|
31
|
+
content: 'home',
|
|
32
|
+
footer: 'main'
|
|
33
|
+
})
|
|
34
|
+
}
|
package/web/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{domain}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Website for {{domain_original}}",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "candy framework run",
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"candypack": "*"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"candypack",
|
|
14
|
+
"website"
|
|
15
|
+
],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC"
|
|
18
|
+
}
|