pockethost 1.5.0 → 1.6.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 +34 -0
- package/changesetx.ts +110 -0
- package/package.json +8 -5
- package/readme.md +73 -16
- package/src/cli/commands/ConfigCommand/index.ts +4 -75
- package/src/cli/commands/PluginCommand/index.ts +5 -46
- package/src/cli/index.ts +5 -0
- package/src/common/plugin/action.ts +44 -12
- package/src/common/plugin/filter.ts +71 -12
- package/src/common/schema/BaseFields.ts +10 -0
- package/src/common/schema/Instance.ts +12 -2
- package/src/common/schema/util.ts +7 -1
- package/src/constants.ts +16 -9
- package/src/core/config.ts +108 -0
- package/src/core/index.ts +1 -1
- package/src/core/tryFetch.ts +6 -3
- package/src/plugin.ts +20 -2
- package/src/server.ts +27 -6
- package/src/core/logSettings.ts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# pockethost
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 40bbb3c: Improve tryFetch by accepting node-fetch params
|
|
8
|
+
- 12f029a: Add changeset maker
|
|
9
|
+
- b7277cf: Add public instance URL calculators and refactor HTTP protocol detection
|
|
10
|
+
- 45a8f45: Add internal app (remote control) support
|
|
11
|
+
- 55b43a1: Remove unneeded params from GetOrProvisionInstanceUrl filter
|
|
12
|
+
- 5be2c9e: Initial commit
|
|
13
|
+
- a236f3e: Added several more filters
|
|
14
|
+
- 35edc77: Enhancement: actions execute sequentially
|
|
15
|
+
- 879845a: Enhancement: unsubscribe from filters
|
|
16
|
+
- c473ed9: Added support for priorities in actions
|
|
17
|
+
- 795fcd7: New action: KillInstance
|
|
18
|
+
- c32b845: Added support for Settings filter whereby all settings are collected and displayed in DEBUG() mode
|
|
19
|
+
- cce72b3: New action: AfterPluginsLoaded
|
|
20
|
+
- 82dde2d: Enhancement: filters now have extensible data types
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- b55c585: Clean up dead code
|
|
25
|
+
- 0e4bf43: Updated commit parser deps
|
|
26
|
+
- f79b067: Added serve command details
|
|
27
|
+
- dd3ceaa: Added filter debugging
|
|
28
|
+
- 7931204: Fixed a bug where adding plugins was not saved correctly
|
|
29
|
+
- 6e14f57: Updated plugin authoring guide
|
|
30
|
+
- 7604930: Minor fix to GetOrProvisionInstanceUrl filter
|
|
31
|
+
- 52b70cb: Fixed bug where --extra-plugins was not defaulting to '' correctly
|
|
32
|
+
- Updated dependencies [c32b845]
|
|
33
|
+
- @pockethost/plugin-launcher-spawn@1.0.0
|
|
34
|
+
- @pockethost/plugin-auto-admin@1.0.0
|
|
35
|
+
- @pockethost/plugin-console-logger@1.0.0
|
|
36
|
+
|
|
3
37
|
## 1.5.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/changesetx.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { sync } from 'conventional-commits-parser'
|
|
4
|
+
import fs, { readFileSync } from 'fs'
|
|
5
|
+
import minimist from 'minimist'
|
|
6
|
+
import { dirname, join } from 'path'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const repoRootDir = join(__dirname, `../..`)
|
|
11
|
+
|
|
12
|
+
const argv = minimist(process.argv.slice(2))
|
|
13
|
+
const commitMessage = argv._[0]!
|
|
14
|
+
const { type, subject } = sync(commitMessage)
|
|
15
|
+
if (!type) {
|
|
16
|
+
console.log('Invalid commit message type')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getStagedPackageNames(filePaths: string[]): string[] {
|
|
21
|
+
const packageNames = new Set<string>()
|
|
22
|
+
|
|
23
|
+
filePaths.forEach((filePath) => {
|
|
24
|
+
const packageName = getPackageName(filePath)
|
|
25
|
+
if (packageName) {
|
|
26
|
+
packageNames.add(packageName)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return Array.from(packageNames)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPackageName(filePath: string): string | null {
|
|
34
|
+
const packagePath = filePath.split('/').slice(0, 2).join('/')
|
|
35
|
+
const packageJsonPath = join(repoRootDir, `${packagePath}/package.json`)
|
|
36
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
37
|
+
try {
|
|
38
|
+
return packageJson.name
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStagedFileNames(): string[] {
|
|
45
|
+
const output = execSync('git diff --name-only --cached').toString()
|
|
46
|
+
const fileNames = output
|
|
47
|
+
.trim()
|
|
48
|
+
.split('\n')
|
|
49
|
+
.filter((fileName) => fileName.startsWith(`packages/`))
|
|
50
|
+
return fileNames
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function createChangesetFile() {
|
|
54
|
+
const fileNames = getStagedFileNames()
|
|
55
|
+
const packageNames = getStagedPackageNames(fileNames)
|
|
56
|
+
|
|
57
|
+
if (packageNames.length === 0) {
|
|
58
|
+
console.log('No packages staged for commit')
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
console.log('Staged packages:', packageNames)
|
|
62
|
+
|
|
63
|
+
const prefixMap = {
|
|
64
|
+
feat: 'minor',
|
|
65
|
+
enh: 'minor',
|
|
66
|
+
fix: 'patch',
|
|
67
|
+
docs: 'patch',
|
|
68
|
+
chore: 'patch',
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
if (!(type! in prefixMap)) {
|
|
72
|
+
console.log('Invalid prefix found in commit message')
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const versionType = prefixMap[type as keyof typeof prefixMap]
|
|
77
|
+
|
|
78
|
+
const changesetFile = [
|
|
79
|
+
`---`,
|
|
80
|
+
...packageNames.map((packageName) => `'${packageName}': ${versionType}`),
|
|
81
|
+
`---`,
|
|
82
|
+
``,
|
|
83
|
+
subject,
|
|
84
|
+
].join('\n')
|
|
85
|
+
return changesetFile
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function writeChangesetFile(changesetFile: string) {
|
|
89
|
+
const filePath = join(repoRootDir, `.changeset`, `${+new Date()}.md`)
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(filePath, changesetFile)
|
|
92
|
+
console.log(`Changeset file written successfully at ${filePath}`)
|
|
93
|
+
execSync(`git add ${filePath}`)
|
|
94
|
+
console.log(`Changeset file added to git`)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Error writing changeset file: ${error}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function commit() {
|
|
101
|
+
execSync(`git commit -m "${commitMessage}"`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
const changesetFile = await createChangesetFile()
|
|
106
|
+
await writeChangesetFile(changesetFile)
|
|
107
|
+
commit()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pockethost",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "Ben Allfree",
|
|
6
6
|
"url": "https://github.com/benallfree"
|
|
@@ -41,19 +41,22 @@
|
|
|
41
41
|
"typescript": "^5.4.5",
|
|
42
42
|
"update-notifier": "^7.0.0",
|
|
43
43
|
"url-pattern": "^1.0.3",
|
|
44
|
-
"@pockethost/plugin-
|
|
45
|
-
"@pockethost/plugin-console-logger": "^0.
|
|
46
|
-
"@pockethost/plugin-
|
|
44
|
+
"@pockethost/plugin-launcher-spawn": "^0.4.0",
|
|
45
|
+
"@pockethost/plugin-console-logger": "^0.3.0",
|
|
46
|
+
"@pockethost/plugin-auto-admin": "^0.2.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
+
"@types/conventional-commits-parser": "^5.0.0",
|
|
49
50
|
"@types/express": "^4.17.21",
|
|
50
51
|
"@types/http-proxy": "^1.17.13",
|
|
52
|
+
"@types/inquirer": "^9.0.7",
|
|
51
53
|
"@types/json-stringify-safe": "^5.0.2",
|
|
52
54
|
"@types/minimist": "^1.2.5",
|
|
53
55
|
"@types/node": "^20.8.10",
|
|
54
56
|
"@types/semver": "^7.5.4",
|
|
55
57
|
"@types/tmp": "^0.2.5",
|
|
56
|
-
"@types/update-notifier": "^6.0.8"
|
|
58
|
+
"@types/update-notifier": "^6.0.8",
|
|
59
|
+
"conventional-commits-parser": "^5.0.0"
|
|
57
60
|
},
|
|
58
61
|
"scripts": {
|
|
59
62
|
"check-types": "tsc --noEmit ",
|
package/readme.md
CHANGED
|
@@ -10,21 +10,75 @@ Extend PocketHost with its Plugin ecosystem. Start with the bare-bones dev serve
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=2 orderedList=false} -->
|
|
14
|
+
|
|
15
|
+
<!-- code_chunk_output -->
|
|
16
|
+
|
|
17
|
+
- [Quickstart](#quickstart)
|
|
18
|
+
- [Introduction](#introduction)
|
|
19
|
+
- [The magic `serve` command](#the-magic-serve-command)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [Plugins Directory](#plugins-directory)
|
|
22
|
+
- [Writing Plugins](#writing-plugins)
|
|
23
|
+
- [Support](#support)
|
|
24
|
+
|
|
25
|
+
<!-- /code_chunk_output -->
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
13
29
|
## Quickstart
|
|
14
30
|
|
|
15
31
|
```
|
|
16
32
|
npx pockethost serve
|
|
17
33
|
```
|
|
18
34
|
|
|
35
|
+
## Introduction
|
|
36
|
+
|
|
37
|
+
PocketHost is a multitenant PocketBase hosting system. With it, you can run multiple instances of PocketBase on different subdomains, all on the same server.
|
|
38
|
+
|
|
39
|
+
Use PocketHost for development or production purposes.
|
|
40
|
+
|
|
41
|
+
PocketHost can be easily extended using its thoughtful plugin architecture. By choosing additional plugins, PocketHost's features can be extended according to your exact needs. You can even write your own plugins.
|
|
42
|
+
|
|
43
|
+
## The magic `serve` command
|
|
44
|
+
|
|
45
|
+
`pockethost serve` is a special command that can be decorated with plugins. Plugins can respond to the `serve` command by running their own services.
|
|
46
|
+
|
|
47
|
+
By convention, plugins that have some kind of `serve` functionality such as running a server, will make it available in two ways.
|
|
48
|
+
|
|
49
|
+
The first way runs ONLY the plugin's `serve` functionality:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pockethost <plugin-name> serve
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The second way runs ALL `serve` functionality from ALL plugins:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pockethost serve
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The exact plugins can also be narrowed:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pockethost serve --only=plugin1,plugin2,plugin3
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See the plugin authoring guide for more information on how to make a plugin that responds to both methods.
|
|
68
|
+
|
|
19
69
|
## Configuration
|
|
20
70
|
|
|
21
|
-
PocketHost supports configuration by environment variable
|
|
71
|
+
PocketHost supports configuration by environment variable, `.env` file, and also built-in settings via `pockethost config`.
|
|
72
|
+
|
|
73
|
+
All PocketHost variables begin with `PH_`.
|
|
22
74
|
|
|
23
75
|
PocketHost will search the current directory for an `.env` when it loads. Anything in `.env` will be merged with and override any `<PH_HOME>/.env` that may exist.
|
|
24
76
|
|
|
25
|
-
Use `pockethost config` to set global defaults
|
|
77
|
+
Use `pockethost config` to list and set global defaults.
|
|
26
78
|
|
|
27
|
-
### Variables
|
|
79
|
+
### Core Variables
|
|
80
|
+
|
|
81
|
+
See `pockethost config ls` for a full list of variables. Plugin variables will also be listed for loaded plugins.
|
|
28
82
|
|
|
29
83
|
| Name | Default | Discussion |
|
|
30
84
|
| -------------- | -------------------------- | ----------------------------------------------------------------------------- |
|
|
@@ -42,29 +96,32 @@ Use `pockethost config` to set global defaults stored in `<PH_HOME>/.env`.
|
|
|
42
96
|
- `@pockethost/plugin-launcher-spawn`
|
|
43
97
|
- `@pockethost/plugin-auto-admin`
|
|
44
98
|
|
|
45
|
-
## Plugins
|
|
99
|
+
## Plugins Directory
|
|
100
|
+
|
|
101
|
+
PocketHost uses an action/filter architecture to support a growing list plugins. Some plugins have a CLI component, whereas others add purely programmatic support to alter how PocketHost works.
|
|
46
102
|
|
|
47
|
-
|
|
103
|
+
### Core plugins
|
|
48
104
|
|
|
49
|
-
|
|
105
|
+
These plugins are maintained by the PocketHost project.
|
|
50
106
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
| plugin-console-logger | Routes log messages to the console
|
|
54
|
-
| plugin-auto-admin | Auto-creates a default admin account on instance launch
|
|
55
|
-
| plugin-launcher-spawn | Launches PocketBase instances via `spawn()` on the local machine. PocketHost uses this by default
|
|
56
|
-
| plugin-cloudflare-request-logger | Log additional data when running behind Cloudflare
|
|
107
|
+
| Name | Description |
|
|
108
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
109
|
+
| `@pockethost/plugin-console-logger` | Routes log messages to the console |
|
|
110
|
+
| `@pockethost/plugin-auto-admin` | Auto-creates a default admin account on instance launch |
|
|
111
|
+
| `@pockethost/plugin-launcher-spawn` | Launches PocketBase instances via `spawn()` on the local machine. PocketHost uses this by default |
|
|
112
|
+
| `@pockethost/plugin-cloudflare-request-logger` | Log additional data when running behind Cloudflare |
|
|
113
|
+
| `@pockethost/plugin-ftp-server` | Used to provide secure FTPS access to PocketBase instance data. This FTP server can run along-side a PocketHost server. |
|
|
57
114
|
|
|
58
115
|
Coming soon:
|
|
59
116
|
| name | description |
|
|
60
117
|
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
-
| plugin-ftp-server | Used to provide secure FTPS access to PocketBase instance data. This FTP server can run along-side a PocketHost server. |
|
|
62
118
|
| plugin-launcher-docker | Launches PocketBase instances via Docker. This is useful for running untrusted PocketBase code and instances. pockethost.io uses this. |
|
|
63
119
|
| plugin-waf | A Web Application Firewall |
|
|
64
|
-
| plugin-waf-enforce-ssl | Enforce SSL at the
|
|
65
|
-
|
|
120
|
+
| plugin-waf-enforce-ssl | Enforce SSL at the WAF
|
|
121
|
+
|
|
122
|
+
## Writing Plugins
|
|
66
123
|
|
|
67
|
-
See the [Plugin Authoring Guide](https://github.com/pockethost/pockethost/blob/master/packages/pockethost/plugin-guide.md) for details about
|
|
124
|
+
See the [Plugin Authoring Guide](https://github.com/pockethost/pockethost/blob/master/packages/pockethost/plugin-guide.md) for details about how to write your own plugins.
|
|
68
125
|
|
|
69
126
|
## Support
|
|
70
127
|
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
import { forEach, keys, omit } from '@s-libs/micro-dash'
|
|
2
1
|
import { Command } from 'commander'
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
5
|
-
import { info } from '../..'
|
|
6
|
-
import { PH_HOME, isSettingKey, settings } from '../../../constants'
|
|
7
|
-
|
|
8
|
-
const envFile = () => {
|
|
9
|
-
const envFile = PH_HOME(`.env`)
|
|
10
|
-
if (!existsSync(envFile)) {
|
|
11
|
-
writeFileSync(envFile, '')
|
|
12
|
-
}
|
|
13
|
-
return envFile
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const _parse = () =>
|
|
17
|
-
parse(readFileSync(envFile(), { encoding: 'utf8' }).toString())
|
|
2
|
+
import { listConfig, setConfig, unsetConfig } from '../../../core'
|
|
18
3
|
|
|
19
4
|
export const ConfigCommand = () => {
|
|
20
5
|
const cmd = new Command(`config`)
|
|
@@ -24,76 +9,20 @@ export const ConfigCommand = () => {
|
|
|
24
9
|
.argument(`<name>`, `Config name`)
|
|
25
10
|
.argument(`<value>`, `Config value`)
|
|
26
11
|
.description(`Set a config value`)
|
|
27
|
-
.action(
|
|
28
|
-
if (!isSettingKey(name))
|
|
29
|
-
throw new Error(`Invalid setting name ${name}`)
|
|
30
|
-
|
|
31
|
-
if (value === '=') throw new Error(`Invalid value ${value}`)
|
|
32
|
-
|
|
33
|
-
const values = _parse()
|
|
34
|
-
values[name] = value
|
|
35
|
-
writeFileSync(
|
|
36
|
-
envFile(),
|
|
37
|
-
Object.entries(values)
|
|
38
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
39
|
-
.join('\n'),
|
|
40
|
-
)
|
|
41
|
-
info(`Set ${name}=${value}`)
|
|
42
|
-
info(`Written to ${envFile()}`)
|
|
43
|
-
}),
|
|
12
|
+
.action(setConfig),
|
|
44
13
|
)
|
|
45
14
|
|
|
46
15
|
.addCommand(
|
|
47
16
|
new Command(`unset`)
|
|
48
17
|
.argument(`<name>`, `Config name`)
|
|
49
18
|
.description(`Unset a config value`)
|
|
50
|
-
.action(
|
|
51
|
-
if (!isSettingKey(name))
|
|
52
|
-
throw new Error(`Invalid setting name ${name}`)
|
|
53
|
-
|
|
54
|
-
const values = _parse()
|
|
55
|
-
delete values[name]
|
|
56
|
-
writeFileSync(
|
|
57
|
-
envFile(),
|
|
58
|
-
Object.entries(values)
|
|
59
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
60
|
-
.join('\n'),
|
|
61
|
-
)
|
|
62
|
-
info(`Unset ${name}`)
|
|
63
|
-
info(`Written to ${envFile()}`)
|
|
64
|
-
}),
|
|
19
|
+
.action(unsetConfig),
|
|
65
20
|
)
|
|
66
21
|
.addCommand(
|
|
67
22
|
new Command(`list`)
|
|
68
23
|
.description(`List all config values`)
|
|
69
24
|
.alias(`ls`)
|
|
70
|
-
.action(
|
|
71
|
-
const values = _parse()
|
|
72
|
-
|
|
73
|
-
if (keys(values).length > 0) {
|
|
74
|
-
info()
|
|
75
|
-
info(`Config values from ${envFile()}:`)
|
|
76
|
-
forEach(values, (v, k) => {
|
|
77
|
-
info(`\t${k}=${v}`)
|
|
78
|
-
})
|
|
79
|
-
info()
|
|
80
|
-
} else {
|
|
81
|
-
info(`No config values found in ${envFile()}`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const defaults = omit(settings, keys(values) as any)
|
|
85
|
-
if (keys(defaults).length > 0) {
|
|
86
|
-
info(`Default values:`)
|
|
87
|
-
forEach(settings, (v, k) => {
|
|
88
|
-
if (k in values) return
|
|
89
|
-
info(`\t${k}=${v}`)
|
|
90
|
-
})
|
|
91
|
-
} else {
|
|
92
|
-
info(
|
|
93
|
-
`No default values because all values are defined in ${envFile()}`,
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
}),
|
|
25
|
+
.action(listConfig),
|
|
97
26
|
)
|
|
98
27
|
return cmd
|
|
99
28
|
}
|
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
import { uniq } from '@s-libs/micro-dash'
|
|
2
1
|
import { Command } from 'commander'
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
5
3
|
import { error, info } from '../..'
|
|
6
|
-
import {
|
|
4
|
+
import { PH_PLUGINS, PH_PROJECT_DIR } from '../../../constants'
|
|
5
|
+
import { appendConfig, filterConfig } from '../../../core'
|
|
7
6
|
import { getCompatibleVersions, getPackageJson, removePackage } from './util'
|
|
8
7
|
|
|
9
|
-
const envFile = () => {
|
|
10
|
-
const envFile = PH_HOME(`.env`)
|
|
11
|
-
if (!existsSync(envFile)) {
|
|
12
|
-
writeFileSync(envFile, '')
|
|
13
|
-
}
|
|
14
|
-
return envFile
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const _parse = () =>
|
|
18
|
-
parse(readFileSync(envFile(), { encoding: 'utf8' }).toString())
|
|
19
|
-
|
|
20
8
|
export const PluginCommand = () => {
|
|
21
9
|
const cmd = new Command(`plugin`)
|
|
22
10
|
.description(`Manage PocketHost plugins`)
|
|
@@ -33,22 +21,7 @@ export const PluginCommand = () => {
|
|
|
33
21
|
const { version } = pkg
|
|
34
22
|
await getCompatibleVersions(`pockethost`, version, [name])
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
values[`PH_PLUGINS`] = uniq([
|
|
38
|
-
...(values[`PH_PLUGINS`]
|
|
39
|
-
?.split(/,/)
|
|
40
|
-
.map((v) => v.trim())
|
|
41
|
-
.filter((v) => !!v) || []),
|
|
42
|
-
name,
|
|
43
|
-
]).join(`,`)
|
|
44
|
-
writeFileSync(
|
|
45
|
-
envFile(),
|
|
46
|
-
Object.entries(values)
|
|
47
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
48
|
-
.join('\n'),
|
|
49
|
-
)
|
|
50
|
-
info(`Installed ${name}`)
|
|
51
|
-
info(`Written to ${envFile()}`)
|
|
24
|
+
appendConfig(`PH_PLUGINS`, name)
|
|
52
25
|
} catch (e) {
|
|
53
26
|
error(`${e}`)
|
|
54
27
|
}
|
|
@@ -81,21 +54,7 @@ export const PluginCommand = () => {
|
|
|
81
54
|
.action(async (name) => {
|
|
82
55
|
try {
|
|
83
56
|
await removePackage(name)
|
|
84
|
-
|
|
85
|
-
values[`PH_PLUGINS`] = uniq([
|
|
86
|
-
...(values[`PH_PLUGINS`]
|
|
87
|
-
?.split(/,/)
|
|
88
|
-
.map((v) => v.trim())
|
|
89
|
-
.filter((v) => v !== name) || []),
|
|
90
|
-
]).join(`,`)
|
|
91
|
-
writeFileSync(
|
|
92
|
-
envFile(),
|
|
93
|
-
Object.entries(values)
|
|
94
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
95
|
-
.join('\n'),
|
|
96
|
-
)
|
|
97
|
-
info(`Removed ${name}`)
|
|
98
|
-
info(`Written to ${envFile()}`)
|
|
57
|
+
filterConfig(`PH_PLUGINS`, name)
|
|
99
58
|
} catch (e) {
|
|
100
59
|
error(`${e}`)
|
|
101
60
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DEBUG,
|
|
11
11
|
LogLevelName,
|
|
12
12
|
LoggerService,
|
|
13
|
+
doAfterPluginsLoadedAction,
|
|
13
14
|
doCliCommandsFilter,
|
|
14
15
|
loadPlugins,
|
|
15
16
|
} from '../common'
|
|
@@ -36,13 +37,17 @@ export const main = async () => {
|
|
|
36
37
|
|
|
37
38
|
const argv = minimist<{ e: string }>(process.argv.slice(2), {
|
|
38
39
|
alias: { e: 'extra-plugins' },
|
|
40
|
+
default: { e: '' },
|
|
39
41
|
})
|
|
40
42
|
const extraPlugins = argv.e
|
|
41
43
|
.split(/,/)
|
|
42
44
|
.map((s) => s.trim())
|
|
43
45
|
.filter((v) => !!v)
|
|
46
|
+
|
|
44
47
|
await loadPlugins([pockethost, ...uniq(PH_PLUGINS()), ...extraPlugins])
|
|
45
48
|
|
|
49
|
+
await doAfterPluginsLoadedAction()
|
|
50
|
+
|
|
46
51
|
program
|
|
47
52
|
.name('pockethost')
|
|
48
53
|
.description('Multitenant PocketBase hosting')
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { uniqueId } from '@s-libs/micro-dash'
|
|
1
2
|
import { Express, Request, Response } from 'express'
|
|
3
|
+
import { dbg } from '../../cli'
|
|
2
4
|
import { LogLevelName } from '../Logger'
|
|
3
5
|
import { DEBUG } from '../debug'
|
|
4
6
|
import { InstanceFields } from '../schema'
|
|
5
7
|
|
|
6
8
|
enum CoreActions {
|
|
9
|
+
AfterPluginsLoaded = 'core_after_plugins_loaded',
|
|
7
10
|
Init = 'core_init',
|
|
8
11
|
Serve = 'core_serve',
|
|
9
12
|
AppMounted = 'core_on_app_mount',
|
|
@@ -18,6 +21,8 @@ enum CoreActions {
|
|
|
18
21
|
BeforeInstanceStarted = 'core_before_instance_started',
|
|
19
22
|
AfterInstanceStarted = 'core_after_instance_started',
|
|
20
23
|
|
|
24
|
+
KillInstance = 'core_kill_instance',
|
|
25
|
+
|
|
21
26
|
BeforeInstanceStopped = 'core_before_instance_stopped',
|
|
22
27
|
AfterInstanceStopped = 'core_after_instance_stopped',
|
|
23
28
|
|
|
@@ -27,20 +32,34 @@ enum CoreActions {
|
|
|
27
32
|
Request = 'core_edge_request',
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
const actions: {
|
|
31
|
-
[key: string]: ActionHandler<any>[]
|
|
32
|
-
} = {}
|
|
33
|
-
|
|
34
35
|
export type ActionHandler<TContext = {}> = (
|
|
35
36
|
context: TContext extends never ? [] : TContext,
|
|
36
37
|
) => Promise<void>
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
interface Action {
|
|
40
|
+
priority: number
|
|
41
|
+
handler: ActionHandler<any>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const actions: {
|
|
45
|
+
[key: string]: Action[]
|
|
46
|
+
} = {}
|
|
47
|
+
|
|
48
|
+
function registerAction(
|
|
39
49
|
action: string,
|
|
40
50
|
handler: (...args: any[]) => Promise<void>,
|
|
41
|
-
|
|
51
|
+
priority: number = 10,
|
|
52
|
+
): () => void {
|
|
42
53
|
if (!(action in actions)) actions[action] = []
|
|
43
|
-
|
|
54
|
+
const uid = uniqueId(`a:${action}:`)
|
|
55
|
+
dbg(`${uid}:subscribe`)
|
|
56
|
+
actions[action]!.push({ priority, handler })
|
|
57
|
+
actions[action]!.sort((a, b) => a.priority - b.priority)
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
dbg(`${uid}:unsubscribe`)
|
|
61
|
+
actions[action] = actions[action]!.filter((a) => a.handler !== handler)
|
|
62
|
+
}
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
async function action(actionName: string, context: any) {
|
|
@@ -49,7 +68,9 @@ async function action(actionName: string, context: any) {
|
|
|
49
68
|
}
|
|
50
69
|
const action = actions[actionName]
|
|
51
70
|
if (!action) return false
|
|
52
|
-
|
|
71
|
+
for (const { handler } of action) {
|
|
72
|
+
await handler(context)
|
|
73
|
+
}
|
|
53
74
|
return true
|
|
54
75
|
}
|
|
55
76
|
|
|
@@ -58,18 +79,22 @@ export function createCustomActionWithContext<TContext extends {}>(
|
|
|
58
79
|
) {
|
|
59
80
|
return [
|
|
60
81
|
async (context: TContext) => action(actionName, context),
|
|
61
|
-
|
|
62
|
-
registerAction(actionName, handler),
|
|
82
|
+
(handler: ActionHandler<TContext>, priority = 10) =>
|
|
83
|
+
registerAction(actionName, handler, priority),
|
|
63
84
|
] as const
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
export function createCustomAction(actionName: CoreActions) {
|
|
67
88
|
return [
|
|
68
89
|
async () => action(actionName, {}),
|
|
69
|
-
|
|
90
|
+
(handler: ActionHandler, priority = 10) =>
|
|
91
|
+
registerAction(actionName, handler, priority),
|
|
70
92
|
] as const
|
|
71
93
|
}
|
|
72
94
|
|
|
95
|
+
export const [doAfterPluginsLoadedAction, onAfterPluginsLoadedAction] =
|
|
96
|
+
createCustomAction(CoreActions.AfterPluginsLoaded)
|
|
97
|
+
|
|
73
98
|
export const [doRequestErrorAction, onRequestErrorAction] =
|
|
74
99
|
createCustomActionWithContext<{
|
|
75
100
|
err: Error
|
|
@@ -92,7 +117,9 @@ export const [doInstanceLogAction, onInstanceLogAction] =
|
|
|
92
117
|
}>(CoreActions.InstanceLog)
|
|
93
118
|
|
|
94
119
|
export const [doAppMountedAction, onAppMountedAction] =
|
|
95
|
-
createCustomActionWithContext<{ app: Express }>(
|
|
120
|
+
createCustomActionWithContext<{ app: Express; internalApp: Express }>(
|
|
121
|
+
CoreActions.AppMounted,
|
|
122
|
+
)
|
|
96
123
|
|
|
97
124
|
export const [doIncomingRequestAction, onIncomingRequestAction] =
|
|
98
125
|
createCustomActionWithContext<{ req: Request; res: Response; host: string }>(
|
|
@@ -117,6 +144,11 @@ export const [doAfterInstanceStartedAction, onAfterInstanceStartedAction] =
|
|
|
117
144
|
CoreActions.AfterInstanceStarted,
|
|
118
145
|
)
|
|
119
146
|
|
|
147
|
+
export const [doKillInstanceAction, onKillInstanceAction] =
|
|
148
|
+
createCustomActionWithContext<{ instance: InstanceFields }>(
|
|
149
|
+
CoreActions.KillInstance,
|
|
150
|
+
)
|
|
151
|
+
|
|
120
152
|
export const [doBeforeInstanceStoppedAction, onBeforeInstanceStoppedAction] =
|
|
121
153
|
createCustomActionWithContext<{ instance: InstanceFields; url: string }>(
|
|
122
154
|
CoreActions.BeforeInstanceStopped,
|
|
@@ -2,13 +2,18 @@ import { Command } from 'commander'
|
|
|
2
2
|
import { Request, Response } from 'express'
|
|
3
3
|
import { DEBUG } from '../debug'
|
|
4
4
|
import { InstanceFields, InstanceId } from '../schema'
|
|
5
|
+
import { UserId } from '../schema/BaseFields'
|
|
5
6
|
|
|
6
7
|
export enum CoreFilters {
|
|
8
|
+
Settings = 'core_settings',
|
|
7
9
|
CliCommands = 'core_cli_commands',
|
|
8
10
|
ServeSlugs = 'core_serve_slugs',
|
|
9
11
|
SpawnConfig = 'core_spawn_config',
|
|
12
|
+
AuthenticateUser = 'core_authenticate_user',
|
|
10
13
|
GetInstanceByRequestInfo = 'core_get_instance_by_request_info',
|
|
11
14
|
GetInstanceById = 'core_get_instance_by_id',
|
|
15
|
+
GetOneInstanceByExactCriteria = 'core_get_one_instance_by_exact_criteria',
|
|
16
|
+
GetAllInstancesByExactCriteria = 'core_get_all_instances_by_exact_criteria',
|
|
12
17
|
InstanceConfig = 'core_instance_config',
|
|
13
18
|
ErrorSpawningInstanceMessage = 'core_error_spawning_instance_message',
|
|
14
19
|
FailedToLaunchInstanceMessage = 'core_failed_to_launch_instance_message',
|
|
@@ -17,6 +22,7 @@ export enum CoreFilters {
|
|
|
17
22
|
GetOrProvisionInstanceUrl = 'core_get_or_provision_instance_url',
|
|
18
23
|
PocketBaseLaunchHandler = 'core_pocket_base_launch_handler',
|
|
19
24
|
NewInstanceRecord = 'core_new_instance_record',
|
|
25
|
+
IsInstanceRunning = 'core_is_instance_running',
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export type FilterHandler<TCarry, TContext> = (
|
|
@@ -31,14 +37,23 @@ const filters: {
|
|
|
31
37
|
[key: string]: FilterEntry<any>[]
|
|
32
38
|
} = {}
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
function registerFilter<TCarry, TContext extends {} = {}>(
|
|
35
41
|
filter: string,
|
|
36
42
|
handler: FilterHandler<TCarry, TContext>,
|
|
37
43
|
priority = 10,
|
|
38
|
-
) {
|
|
44
|
+
): () => void {
|
|
39
45
|
if (!(filter in filters)) filters[filter] = []
|
|
40
46
|
filters[filter]!.push({ priority, handler })
|
|
41
47
|
filters[filter]!.sort((a, b) => a.priority - b.priority)
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
const index = filters[filter]!.findIndex(
|
|
51
|
+
(entry) => entry.handler === handler,
|
|
52
|
+
)
|
|
53
|
+
if (index !== -1) {
|
|
54
|
+
filters[filter]!.splice(index, 1)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
async function filter<TCarry, TContext extends {} = {}>(
|
|
@@ -53,7 +68,14 @@ async function filter<TCarry, TContext extends {} = {}>(
|
|
|
53
68
|
if (!filter) return initialValue
|
|
54
69
|
return filter.reduce(
|
|
55
70
|
(carry, entry) =>
|
|
56
|
-
carry
|
|
71
|
+
carry
|
|
72
|
+
.then((carryRes) => entry.handler(carryRes, context))
|
|
73
|
+
.then((carryRes) => {
|
|
74
|
+
if (DEBUG()) {
|
|
75
|
+
console.log(`f:${filterName}`, carryRes, context)
|
|
76
|
+
}
|
|
77
|
+
return carryRes
|
|
78
|
+
}),
|
|
57
79
|
Promise.resolve(initialValue),
|
|
58
80
|
) as TCarry
|
|
59
81
|
}
|
|
@@ -64,8 +86,15 @@ function createCustomFilterWithContext<TCarry, TContext extends {} = {}>(
|
|
|
64
86
|
return [
|
|
65
87
|
async (initialValue: TCarry, context: TContext) =>
|
|
66
88
|
filter<TCarry, TContext>(filterName, initialValue, context),
|
|
67
|
-
async
|
|
68
|
-
|
|
89
|
+
async <TExtraCarry = unknown>(
|
|
90
|
+
handler: FilterHandler<TCarry & TExtraCarry, TContext>,
|
|
91
|
+
priority = 10,
|
|
92
|
+
) =>
|
|
93
|
+
registerFilter<TCarry & TExtraCarry, TContext>(
|
|
94
|
+
filterName,
|
|
95
|
+
handler,
|
|
96
|
+
priority,
|
|
97
|
+
),
|
|
69
98
|
] as const
|
|
70
99
|
}
|
|
71
100
|
|
|
@@ -73,11 +102,24 @@ function createCustomFilter<TCarry>(filterName: string) {
|
|
|
73
102
|
return [
|
|
74
103
|
async (initialValue: TCarry) =>
|
|
75
104
|
filter<TCarry>(filterName, initialValue, {}),
|
|
76
|
-
async
|
|
77
|
-
|
|
105
|
+
async <TExtraCarry = unknown>(
|
|
106
|
+
handler: FilterHandler<TCarry & TExtraCarry, {}>,
|
|
107
|
+
priority = 10,
|
|
108
|
+
) =>
|
|
109
|
+
registerFilter<TCarry & TExtraCarry, {}>(filterName, handler, priority),
|
|
78
110
|
] as const
|
|
79
111
|
}
|
|
80
112
|
|
|
113
|
+
export const [doSettingsFilter, onSettingsFilter] = createCustomFilter<{
|
|
114
|
+
[_: string]: any
|
|
115
|
+
}>(CoreFilters.Settings)
|
|
116
|
+
|
|
117
|
+
export const [doAuthenticateUserFilter, onAuthenticateUserFilter] =
|
|
118
|
+
createCustomFilterWithContext<
|
|
119
|
+
UserId | undefined,
|
|
120
|
+
{ username: string; password: string }
|
|
121
|
+
>(CoreFilters.AuthenticateUser)
|
|
122
|
+
|
|
81
123
|
export const [
|
|
82
124
|
doGetInstanceByRequestInfoFilter,
|
|
83
125
|
onGetInstanceByRequestInfoFilter,
|
|
@@ -92,6 +134,22 @@ export const [doGetInstanceByIdFilter, onGetInstanceByIdFilter] =
|
|
|
92
134
|
{ instanceId: InstanceId }
|
|
93
135
|
>(CoreFilters.GetInstanceById)
|
|
94
136
|
|
|
137
|
+
export const [
|
|
138
|
+
doGetOneInstanceByExactCriteriaFilter,
|
|
139
|
+
onGetOneInstanceByExactCriteriaFilter,
|
|
140
|
+
] = createCustomFilterWithContext<
|
|
141
|
+
InstanceFields | undefined,
|
|
142
|
+
Partial<InstanceFields & { [_: string]: string | number }>
|
|
143
|
+
>(CoreFilters.GetOneInstanceByExactCriteria)
|
|
144
|
+
|
|
145
|
+
export const [
|
|
146
|
+
doGetAllInstancesByExactCriteriaFilter,
|
|
147
|
+
onGetAllInstancesByExactCriteriaFilter,
|
|
148
|
+
] = createCustomFilterWithContext<
|
|
149
|
+
InstanceFields[],
|
|
150
|
+
Partial<InstanceFields & { [_: string]: string | number }>
|
|
151
|
+
>(CoreFilters.GetAllInstancesByExactCriteria)
|
|
152
|
+
|
|
95
153
|
export const [doCliCommandsFilter, onCliCommandsFilter] = createCustomFilter<
|
|
96
154
|
Command[]
|
|
97
155
|
>(CoreFilters.CliCommands)
|
|
@@ -106,12 +164,8 @@ export const [
|
|
|
106
164
|
doGetOrProvisionInstanceUrlFilter,
|
|
107
165
|
onGetOrProvisionInstanceUrlFilter,
|
|
108
166
|
] = createCustomFilterWithContext<
|
|
109
|
-
string,
|
|
167
|
+
string | undefined,
|
|
110
168
|
{
|
|
111
|
-
req: Request
|
|
112
|
-
res: Response
|
|
113
|
-
host: string
|
|
114
|
-
subdomain: string
|
|
115
169
|
instance: InstanceFields
|
|
116
170
|
}
|
|
117
171
|
>(CoreFilters.GetOrProvisionInstanceUrl)
|
|
@@ -136,3 +190,8 @@ export type InstanceConfig = {
|
|
|
136
190
|
}
|
|
137
191
|
export const [doInstanceConfigFilter, onInstanceConfigFilter] =
|
|
138
192
|
createCustomFilter<InstanceConfig>(CoreFilters.InstanceConfig)
|
|
193
|
+
|
|
194
|
+
export const [doIsInstanceRunningFilter, onIsInstanceRunningFilter] =
|
|
195
|
+
createCustomFilterWithContext<boolean, { instance: InstanceFields }>(
|
|
196
|
+
CoreFilters.IsInstanceRunning,
|
|
197
|
+
)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { doNewInstanceRecordFilter } from '../plugin'
|
|
2
|
+
import { IsoDate } from './BaseFields'
|
|
3
|
+
import { newId, pocketNow } from './util'
|
|
2
4
|
|
|
3
5
|
export type VersionId = string
|
|
4
6
|
|
|
@@ -21,16 +23,24 @@ export type InstanceSecretCollection = {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export type InstanceFields = {
|
|
26
|
+
id: string
|
|
24
27
|
subdomain: string
|
|
25
28
|
version: VersionId
|
|
26
29
|
secrets: InstanceSecretCollection
|
|
27
30
|
dev: boolean // Should instance run in --dev mode
|
|
31
|
+
created: IsoDate
|
|
32
|
+
updated: IsoDate
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export const mkInstance = (subdomain: string) =>
|
|
31
|
-
|
|
35
|
+
export const mkInstance = (subdomain: string, id = newId()) => {
|
|
36
|
+
const now = pocketNow()
|
|
37
|
+
return doNewInstanceRecordFilter({
|
|
38
|
+
id,
|
|
32
39
|
subdomain,
|
|
33
40
|
version: '*',
|
|
34
41
|
secrets: {},
|
|
35
42
|
dev: false,
|
|
43
|
+
created: now,
|
|
44
|
+
updated: now,
|
|
36
45
|
})
|
|
46
|
+
}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { customAlphabet } from 'nanoid'
|
|
2
|
+
import { IsoDate } from './BaseFields'
|
|
3
|
+
|
|
4
|
+
export const newId = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10)
|
|
5
|
+
export const pocketNow = () => toPocketDate(new Date())
|
|
6
|
+
export const toPocketDate = (date: Date) => date.toISOString()
|
|
7
|
+
export const fromPocketDate = (iso: IsoDate) => new Date(iso)
|
package/src/constants.ts
CHANGED
|
@@ -4,10 +4,9 @@ import { dirname, join } from 'path'
|
|
|
4
4
|
import { cwd } from 'process'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
6
|
import { mkBoolean, mkCsvString, mkNumber, mkPath, mkString } from '../core'
|
|
7
|
-
import { InstanceId } from './common'
|
|
7
|
+
import { InstanceFields, InstanceId } from './common'
|
|
8
8
|
import { DEBUG, IS_DEV } from './common/debug'
|
|
9
9
|
import { Settings } from './core/Settings'
|
|
10
|
-
import { logSettings } from './core/logSettings'
|
|
11
10
|
|
|
12
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
12
|
|
|
@@ -17,11 +16,7 @@ config({ path: [join(_PH_HOME, '.env'), join(cwd(), `.env`)] })
|
|
|
17
16
|
|
|
18
17
|
export const _PH_PROJECT_DIR = join(__dirname, '..')
|
|
19
18
|
export const _APEX_DOMAIN = process.env.APEX_DOMAIN || 'pockethost.lvh.me'
|
|
20
|
-
export const _HTTP_PROTOCOL = process.env.HTTP_PROTOCOL || `https:`
|
|
21
19
|
|
|
22
|
-
export function isSettingKey(key: string): key is keyof typeof settings {
|
|
23
|
-
return key in settings
|
|
24
|
-
}
|
|
25
20
|
export const settings = Settings({
|
|
26
21
|
PH_PLUGINS: mkCsvString([
|
|
27
22
|
`@pockethost/plugin-console-logger`,
|
|
@@ -32,6 +27,7 @@ export const settings = Settings({
|
|
|
32
27
|
PH_HOME: mkPath(_PH_HOME),
|
|
33
28
|
PH_PROJECT_DIR: mkPath(_PH_PROJECT_DIR),
|
|
34
29
|
|
|
30
|
+
PH_INTERNAL_HOST: mkString('localhost'),
|
|
35
31
|
PH_APEX_DOMAIN: mkString(_APEX_DOMAIN),
|
|
36
32
|
|
|
37
33
|
PH_PORT: mkNumber(3000),
|
|
@@ -39,6 +35,7 @@ export const settings = Settings({
|
|
|
39
35
|
PH_DATA_DIR: mkPath(join(_PH_HOME, 'data'), { create: true }),
|
|
40
36
|
PH_DEV: mkBoolean(IS_DEV()),
|
|
41
37
|
PH_DEBUG: mkBoolean(DEBUG()),
|
|
38
|
+
PH_INTERNAL_APP_SECRET: mkString(''),
|
|
42
39
|
})
|
|
43
40
|
|
|
44
41
|
/** Accessors */
|
|
@@ -57,7 +54,17 @@ export const DATA_DIR = (...paths: string[]) =>
|
|
|
57
54
|
export const NODE_ENV = () => process.env.NODE_ENV
|
|
58
55
|
export const INSTANCE_DATA_DIR = (id: InstanceId, ...paths: string[]) =>
|
|
59
56
|
join(DATA_DIR(), id, ...paths)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
export const INTERNAL_HOST = () => settings.PH_INTERNAL_HOST
|
|
58
|
+
export const INTERNAL_APP_SECRET = () => settings.PH_INTERNAL_APP_SECRET
|
|
59
|
+
export const INTERNAL_APP_URL = (...paths: string[]) =>
|
|
60
|
+
`${HTTP_PROTOCOL()}//${INTERNAL_HOST()}:${PORT()}/${INTERNAL_APP_PREFIX}/${join(
|
|
61
|
+
...paths,
|
|
62
|
+
)}`
|
|
63
|
+
export const INTERNAL_APP_PREFIX = `_internal`
|
|
64
|
+
export const INTERNAL_APP_AUTH_HEADER = `x-pockethost-authorization`
|
|
65
|
+
export const HTTP_PROTOCOL = () => (PORT() === 443 ? 'https:' : 'http:')
|
|
66
|
+
export const PUBLIC_INSTANCE_URL = ({ subdomain }: Partial<InstanceFields>) => {
|
|
67
|
+
const url = new URL(`${HTTP_PROTOCOL()}//${subdomain}.${APEX_DOMAIN()}`)
|
|
68
|
+
url.port = `${PORT() === 80 || PORT() == 443 ? '' : PORT()}`
|
|
69
|
+
return url
|
|
63
70
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { forEach, keys, omit, uniq } from '@s-libs/micro-dash'
|
|
2
|
+
import { parse } from 'dotenv'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
4
|
+
import { info } from '../cli'
|
|
5
|
+
import { PH_HOME, settings } from '../constants'
|
|
6
|
+
|
|
7
|
+
const envFile = () => {
|
|
8
|
+
const envFile = PH_HOME(`.env`)
|
|
9
|
+
if (!existsSync(envFile)) {
|
|
10
|
+
writeFileSync(envFile, '')
|
|
11
|
+
}
|
|
12
|
+
return envFile
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const setConfig = (name: string, value: string) => {
|
|
16
|
+
if (value === '=') throw new Error(`Invalid value ${value}`)
|
|
17
|
+
|
|
18
|
+
const values = _parse()
|
|
19
|
+
values[name] = value
|
|
20
|
+
writeFileSync(
|
|
21
|
+
envFile(),
|
|
22
|
+
Object.entries(values)
|
|
23
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
24
|
+
.join('\n'),
|
|
25
|
+
)
|
|
26
|
+
process.env[name] = value
|
|
27
|
+
info(`Set ${name}=${value}`)
|
|
28
|
+
info(`Written to ${envFile()}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const unsetConfig = (name: string) => {
|
|
32
|
+
const values = _parse()
|
|
33
|
+
delete values[name]
|
|
34
|
+
writeFileSync(
|
|
35
|
+
envFile(),
|
|
36
|
+
Object.entries(values)
|
|
37
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
38
|
+
.join('\n'),
|
|
39
|
+
)
|
|
40
|
+
info(`Unset ${name}`)
|
|
41
|
+
info(`Written to ${envFile()}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const listConfig = () => {
|
|
45
|
+
const values = _parse()
|
|
46
|
+
|
|
47
|
+
if (keys(values).length > 0) {
|
|
48
|
+
info()
|
|
49
|
+
info(`Config values from ${envFile()}:`)
|
|
50
|
+
forEach(values, (v, k) => {
|
|
51
|
+
info(`\t${k}=${v}`)
|
|
52
|
+
})
|
|
53
|
+
info()
|
|
54
|
+
} else {
|
|
55
|
+
info(`No config values found in ${envFile()}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaults = omit(settings, keys(values) as any)
|
|
59
|
+
if (keys(defaults).length > 0) {
|
|
60
|
+
info(`Default values:`)
|
|
61
|
+
forEach(settings, (v, k) => {
|
|
62
|
+
if (k in values) return
|
|
63
|
+
info(`\t${k}=${v}`)
|
|
64
|
+
})
|
|
65
|
+
} else {
|
|
66
|
+
info(`No default values because all values are defined in ${envFile()}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const appendConfig = (name: string, value: string) => {
|
|
71
|
+
const values = _parse()
|
|
72
|
+
values[name] = uniq([
|
|
73
|
+
...(values[name]
|
|
74
|
+
?.split(/,/)
|
|
75
|
+
.map((v) => v.trim())
|
|
76
|
+
.filter((v) => !!v) || []),
|
|
77
|
+
value,
|
|
78
|
+
]).join(`,`)
|
|
79
|
+
writeFileSync(
|
|
80
|
+
envFile(),
|
|
81
|
+
Object.entries(values)
|
|
82
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
83
|
+
.join('\n'),
|
|
84
|
+
)
|
|
85
|
+
info(`Added ${value} to ${name}`)
|
|
86
|
+
info(`Written to ${envFile()}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const filterConfig = (name: string, value: string) => {
|
|
90
|
+
const values = _parse()
|
|
91
|
+
values[name] = uniq([
|
|
92
|
+
...(values[name]
|
|
93
|
+
?.split(/,/)
|
|
94
|
+
.map((v) => v.trim())
|
|
95
|
+
.filter((v) => v !== value) || []),
|
|
96
|
+
]).join(`,`)
|
|
97
|
+
writeFileSync(
|
|
98
|
+
envFile(),
|
|
99
|
+
Object.entries(values)
|
|
100
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
101
|
+
.join('\n'),
|
|
102
|
+
)
|
|
103
|
+
info(`Filtered ${value} from ${name}`)
|
|
104
|
+
info(`Written to ${envFile()}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const _parse = () =>
|
|
108
|
+
parse(readFileSync(envFile(), { encoding: 'utf8' }).toString())
|
package/src/core/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export * from '../constants'
|
|
2
2
|
export * from './Settings'
|
|
3
3
|
export * from './asyncExecutionGuard'
|
|
4
|
+
export * from './config'
|
|
4
5
|
export * from './exit'
|
|
5
6
|
export * from './internal'
|
|
6
|
-
export * from './logSettings'
|
|
7
7
|
export * from './smartFetch'
|
|
8
8
|
export * from './tryFetch'
|
package/src/core/tryFetch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fetch, { Response } from 'node-fetch'
|
|
1
|
+
import fetch, { RequestInit, Response } from 'node-fetch'
|
|
2
2
|
import { LoggerService } from '../common'
|
|
3
3
|
|
|
4
4
|
export const TRYFETCH_RETRY_MS = 50
|
|
@@ -6,7 +6,7 @@ export const TRYFETCH_RETRY_MS = 50
|
|
|
6
6
|
export type TryFetchConfig = {
|
|
7
7
|
preflight: () => Promise<boolean>
|
|
8
8
|
retryMs: number
|
|
9
|
-
}
|
|
9
|
+
} & RequestInit
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @param url The URL to fetch
|
|
@@ -53,7 +53,10 @@ export const tryFetch = async (
|
|
|
53
53
|
}
|
|
54
54
|
try {
|
|
55
55
|
dbg(`Fetch: START`)
|
|
56
|
-
const res = await fetch(url, {
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
...config,
|
|
58
|
+
signal: AbortSignal.timeout(500),
|
|
59
|
+
})
|
|
57
60
|
dbg(`Fetch: SUCCESS`)
|
|
58
61
|
resolve(res)
|
|
59
62
|
} catch (e) {
|
package/src/plugin.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { forEach } from '@s-libs/micro-dash'
|
|
1
2
|
import { produce } from 'immer'
|
|
2
3
|
import {
|
|
3
4
|
PocketHostPlugin,
|
|
5
|
+
doSettingsFilter,
|
|
4
6
|
namespace,
|
|
7
|
+
newId,
|
|
8
|
+
onAfterPluginsLoadedAction,
|
|
5
9
|
onInstanceConfigFilter,
|
|
6
10
|
onInstanceLogAction,
|
|
7
11
|
onServeAction,
|
|
8
12
|
onServeSlugsFilter,
|
|
9
13
|
} from '../common'
|
|
10
|
-
import { dbg } from './cli'
|
|
11
|
-
import { PH_PROJECT_DIR } from './constants'
|
|
14
|
+
import { dbg, info } from './cli'
|
|
15
|
+
import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants'
|
|
16
|
+
import { setConfig } from './core/config'
|
|
12
17
|
import { serve } from './server'
|
|
13
18
|
|
|
14
19
|
export const pockethost: PocketHostPlugin = async ({}) => {
|
|
@@ -33,4 +38,17 @@ export const pockethost: PocketHostPlugin = async ({}) => {
|
|
|
33
38
|
onServeSlugsFilter(async (slugs) => {
|
|
34
39
|
return [...slugs, `pockethost`]
|
|
35
40
|
})
|
|
41
|
+
|
|
42
|
+
onAfterPluginsLoadedAction(async () => {
|
|
43
|
+
const allSettings = await doSettingsFilter(settings)
|
|
44
|
+
forEach(allSettings, (v, k) => {
|
|
45
|
+
dbg(`${k}: ${v}`)
|
|
46
|
+
})
|
|
47
|
+
}, 99)
|
|
48
|
+
|
|
49
|
+
onAfterPluginsLoadedAction(async () => {
|
|
50
|
+
if (INTERNAL_APP_SECRET()) return
|
|
51
|
+
info(`Generating internal app secret...`)
|
|
52
|
+
setConfig(`PH_INTERNAL_APP_SECRET`, newId(30))
|
|
53
|
+
})
|
|
36
54
|
}
|
package/src/server.ts
CHANGED
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
doRequestErrorAction,
|
|
13
13
|
doRequestErrorMessageFilter,
|
|
14
14
|
} from '../common'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
APEX_DOMAIN,
|
|
17
|
+
INTERNAL_APP_AUTH_HEADER,
|
|
18
|
+
INTERNAL_APP_PREFIX,
|
|
19
|
+
INTERNAL_APP_SECRET,
|
|
20
|
+
PORT,
|
|
21
|
+
asyncExitHook,
|
|
22
|
+
} from '../core'
|
|
16
23
|
|
|
17
24
|
export const serve = async () => {
|
|
18
25
|
const _proxyLogger = LoggerService().create('ProxyService')
|
|
@@ -45,7 +52,25 @@ export const serve = async () => {
|
|
|
45
52
|
}
|
|
46
53
|
app.use(errorHandler)
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
const internalApp = express()
|
|
56
|
+
internalApp.use((req, res, next) => {
|
|
57
|
+
dbg(`Got a request to internal app ${req.url}`)
|
|
58
|
+
next()
|
|
59
|
+
})
|
|
60
|
+
internalApp.use((req, res, next) => {
|
|
61
|
+
const authHeader = req.headers[INTERNAL_APP_AUTH_HEADER]
|
|
62
|
+
const internalAppSecret = INTERNAL_APP_SECRET()
|
|
63
|
+
|
|
64
|
+
if (authHeader !== internalAppSecret) {
|
|
65
|
+
res.status(403).send('Forbidden')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
next()
|
|
70
|
+
})
|
|
71
|
+
app.use(`/${INTERNAL_APP_PREFIX}`, internalApp)
|
|
72
|
+
|
|
73
|
+
doAppMountedAction({ app, internalApp })
|
|
49
74
|
|
|
50
75
|
app.all(`*`, async (req, res, next) => {
|
|
51
76
|
const method = req.method
|
|
@@ -83,11 +108,7 @@ export const serve = async () => {
|
|
|
83
108
|
})
|
|
84
109
|
|
|
85
110
|
const url = await doGetOrProvisionInstanceUrlFilter('', {
|
|
86
|
-
req,
|
|
87
|
-
res,
|
|
88
111
|
instance,
|
|
89
|
-
host,
|
|
90
|
-
subdomain,
|
|
91
112
|
})
|
|
92
113
|
|
|
93
114
|
dbg(`Forwarding request for ${req.url} to instance ${url}`)
|