pockethost 1.7.0 → 3.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +138 -86
  2. package/README.md +133 -0
  3. package/dist/cli.js +21932 -0
  4. package/package.json +59 -57
  5. package/changesetx.ts +0 -110
  6. package/common.ts +0 -1
  7. package/core.ts +0 -1
  8. package/readme.md +0 -129
  9. package/src/cli/commands/ConfigCommand/index.ts +0 -28
  10. package/src/cli/commands/PluginCommand/index.ts +0 -113
  11. package/src/cli/commands/PluginCommand/plugin-template/LICENSE.md +0 -16
  12. package/src/cli/commands/PluginCommand/plugin-template/package.json +0 -38
  13. package/src/cli/commands/PluginCommand/plugin-template/readme.md +0 -33
  14. package/src/cli/commands/PluginCommand/plugin-template/src/constants.ts +0 -22
  15. package/src/cli/commands/PluginCommand/plugin-template/src/db.ts +0 -30
  16. package/src/cli/commands/PluginCommand/plugin-template/src/index.ts +0 -3
  17. package/src/cli/commands/PluginCommand/plugin-template/src/log.ts +0 -5
  18. package/src/cli/commands/PluginCommand/plugin-template/src/plugin.ts +0 -58
  19. package/src/cli/commands/PluginCommand/util.ts +0 -140
  20. package/src/cli/commands/ServeCommand/index.ts +0 -36
  21. package/src/cli/index.ts +0 -70
  22. package/src/common/CleanupManager.ts +0 -65
  23. package/src/common/Logger.ts +0 -174
  24. package/src/common/ResourceAllocator.ts +0 -49
  25. package/src/common/TimerManager.ts +0 -64
  26. package/src/common/assert.ts +0 -26
  27. package/src/common/debug.ts +0 -8
  28. package/src/common/error.ts +0 -16
  29. package/src/common/events.ts +0 -53
  30. package/src/common/index.ts +0 -12
  31. package/src/common/mergeConfig.ts +0 -27
  32. package/src/common/mkSingleton.ts +0 -28
  33. package/src/common/plugin/action.ts +0 -164
  34. package/src/common/plugin/filter.ts +0 -197
  35. package/src/common/plugin/index.ts +0 -21
  36. package/src/common/promiseSerializer.ts +0 -22
  37. package/src/common/schema/BaseFields.ts +0 -10
  38. package/src/common/schema/Instance.ts +0 -46
  39. package/src/common/schema/InstanceLog.ts +0 -12
  40. package/src/common/schema/index.ts +0 -3
  41. package/src/common/schema/util.ts +0 -7
  42. package/src/constants.ts +0 -70
  43. package/src/core/Settings.ts +0 -119
  44. package/src/core/asyncExecutionGuard.ts +0 -49
  45. package/src/core/config.ts +0 -108
  46. package/src/core/exit.ts +0 -11
  47. package/src/core/index.ts +0 -8
  48. package/src/core/internal.ts +0 -3
  49. package/src/core/smartFetch.ts +0 -32
  50. package/src/core/tryFetch.ts +0 -69
  51. package/src/plugin.ts +0 -54
  52. package/src/server.ts +0 -129
@@ -1,119 +0,0 @@
1
- import { boolean as castToBoolean } from 'boolean'
2
- import { existsSync, mkdirSync } from 'fs'
3
-
4
- export type SettingsCaster<TValue, TConfig = {}> = {
5
- stringToType: (value: string, config?: Partial<TConfig>) => TValue
6
- typeToString: (value: TValue, config?: Partial<TConfig>) => string
7
- }
8
-
9
- export type SettingsHandler<TValue> = {
10
- get: () => TValue
11
- set: (value: TValue) => void
12
- }
13
-
14
- export type SettingsHandlerFactory<TValue> = (
15
- key: string,
16
- ) => SettingsHandler<TValue>
17
-
18
- export type SettingsMaker<TValue, TConfig = {}> = (
19
- _default?: TValue,
20
- config?: Partial<TConfig>,
21
- ) => SettingsHandlerFactory<TValue>
22
-
23
- const mkMaker =
24
- <TValue, TConfig = {}>(
25
- caster: SettingsCaster<TValue, TConfig>,
26
- ): SettingsMaker<TValue, TConfig> =>
27
- (_default, config) =>
28
- (name: string) => {
29
- return {
30
- get(): TValue {
31
- const v = process.env[name]
32
- if (typeof v === `undefined`) {
33
- if (typeof _default === `undefined`)
34
- throw new Error(`${name} must be defined`)
35
- this.set(_default)
36
- return this.get()
37
- }
38
- try {
39
- return caster.stringToType(v, config)
40
- } catch (e) {
41
- throw new Error(`${name}: ${e}`)
42
- }
43
- },
44
- set(v: TValue) {
45
- try {
46
- process.env[name] = caster.typeToString(v, config)
47
- } catch (e) {
48
- throw new Error(`${name}: ${e}`)
49
- }
50
- },
51
- }
52
- }
53
-
54
- export const mkBoolean = mkMaker<boolean>({
55
- stringToType: (v) => castToBoolean(v),
56
- typeToString: (v) => `${v}`,
57
- })
58
-
59
- export const mkNumber = mkMaker<number>({
60
- stringToType: (s) => parseInt(s, 10),
61
- typeToString: (v) => v.toString(),
62
- })
63
-
64
- export const mkPath = mkMaker<string, { create: boolean; required: boolean }>({
65
- stringToType: (v, options) => {
66
- const { create = false, required = true } = options || {}
67
- if (create) {
68
- mkdirSync(v, { recursive: true })
69
- }
70
- if (required && !existsSync(v)) {
71
- throw new Error(`${v} must exist.`)
72
- }
73
- return v
74
- },
75
- typeToString: (v, options) => {
76
- const { create = false, required = true } = options || {}
77
- if (create) {
78
- mkdirSync(v, { recursive: true })
79
- }
80
- if (required && !existsSync(v)) {
81
- throw new Error(`${v} must exist.`)
82
- }
83
- return v
84
- },
85
- })
86
-
87
- export const mkString = mkMaker<string>({
88
- typeToString: (v) => v,
89
- stringToType: (v) => v,
90
- })
91
-
92
- export const mkCsvString = mkMaker<string[]>({
93
- typeToString: (v) => v.join(','),
94
- stringToType: (s) =>
95
- s
96
- .split(/,/)
97
- .map((s) => s.trim())
98
- .filter((v) => !!v),
99
- })
100
-
101
- type Config<T> = {
102
- [K in keyof T]: SettingsHandlerFactory<T[K]>
103
- }
104
-
105
- export const Settings = <T extends Object>(config: Config<T>) => {
106
- const lookup: Partial<T> = {}
107
-
108
- for (const key in config) {
109
- const handler = config[key as keyof T](key)
110
- Object.defineProperty(lookup, key, {
111
- get: () => handler.get(),
112
- set: (value) => handler.set(value),
113
- enumerable: true,
114
- })
115
- handler.get() // Initialize process.env
116
- }
117
-
118
- return lookup as T
119
- }
@@ -1,49 +0,0 @@
1
- import { uniqueId } from '@s-libs/micro-dash'
2
- import Bottleneck from 'bottleneck'
3
- import { SetReturnType } from 'type-fest'
4
- import { LoggerService } from '../common/Logger'
5
-
6
- const limiters: { [lane: string]: Bottleneck } = {}
7
- export const serialAsyncExecutionGuard = <
8
- T extends (...args: any[]) => Promise<any>,
9
- >(
10
- cb: T,
11
- lane?: SetReturnType<T, string>,
12
- ): T => {
13
- const uuid = uniqueId()
14
- const _lane = lane || (() => uuid)
15
- const wrapper = (...args: Parameters<T>) => {
16
- const { dbg } = LoggerService().create('serialAsyncExecutionGuard')
17
- const key = _lane(...args)
18
- if (!limiters[key]) {
19
- dbg(`New serial limiter with key ${key}`)
20
- limiters[key] = new Bottleneck({ maxConcurrent: 1 })
21
- }
22
- const limiter = limiters[key]!
23
- return limiter.schedule(() => cb(...args)) as unknown as ReturnType<T>
24
- }
25
- return wrapper as unknown as T
26
- }
27
-
28
- const singletons: { [_: string]: Promise<any> } = {}
29
- export const singletonAsyncExecutionGuard = <
30
- T extends (...args: any[]) => Promise<any>,
31
- >(
32
- cb: T,
33
- key: SetReturnType<T, string>,
34
- ): T => {
35
- const uuid = uniqueId()
36
- const keyFactory = key || (() => uuid)
37
- const wrapper = (...args: Parameters<T>) => {
38
- const { dbg } = LoggerService().create(`singletonAsyncExecutionGuard`)
39
- const key = keyFactory(...args)
40
- //@ts-ignore
41
- if (singletons[key]) {
42
- return singletons[key] as unknown as ReturnType<T>
43
- }
44
- dbg(`New singleton guard with key ${key}`)
45
- singletons[key] = cb(...args)
46
- return singletons[key] as unknown as ReturnType<T>
47
- }
48
- return wrapper as unknown as T
49
- }
@@ -1,108 +0,0 @@
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/exit.ts DELETED
@@ -1,11 +0,0 @@
1
- import exitHook, { asyncExitHook as _, gracefulExit as __ } from 'exit-hook'
2
-
3
- export const asyncExitHook = (cb: () => Promise<void>) => _(cb, { wait: 5000 })
4
-
5
- export const gracefulExit = async (signal?: number) => {
6
- __(signal)
7
- await new Promise((resolve) => {
8
- process.on('exit', resolve)
9
- })
10
- }
11
- export { exitHook }
package/src/core/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from '../constants'
2
- export * from './Settings'
3
- export * from './asyncExecutionGuard'
4
- export * from './config'
5
- export * from './exit'
6
- export * from './internal'
7
- export * from './smartFetch'
8
- export * from './tryFetch'
@@ -1,3 +0,0 @@
1
- export const mkInternalAddress = (port: number) => `127.0.0.1:${port}`
2
- export const mkInternalUrl = (port: number) =>
3
- `http://${mkInternalAddress(port)}`
@@ -1,32 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
2
- import fetch from 'node-fetch'
3
- import { dirname } from 'path'
4
- import { LoggerService } from '../common'
5
-
6
- export const smartFetch = async <TRet>(
7
- url: string,
8
- path: string,
9
- ): Promise<TRet> => {
10
- const { dbg } = LoggerService().create(`smartFetch`)
11
-
12
- const data = await (async () => {
13
- try {
14
- const res = await fetch(url)
15
- if (res.status !== 200) {
16
- throw new Error(`API appears to be down`)
17
- }
18
- const data = (await res.json()) as TRet
19
- return data
20
- } catch (e) {
21
- dbg(`Caught error on fetch: ${e}`)
22
- if (!existsSync(path)) {
23
- throw new Error(`API down and no cache`)
24
- }
25
- dbg(`Using data from cache`)
26
- return JSON.parse(readFileSync(path).toString()) as TRet
27
- }
28
- })()
29
- mkdirSync(dirname(path), { recursive: true })
30
- writeFileSync(path, JSON.stringify(data))
31
- return data as TRet
32
- }
@@ -1,69 +0,0 @@
1
- import fetch, { RequestInit, Response } from 'node-fetch'
2
- import { LoggerService } from '../common'
3
-
4
- export const TRYFETCH_RETRY_MS = 50
5
-
6
- export type TryFetchConfig = {
7
- preflight: () => Promise<boolean>
8
- retryMs: number
9
- } & RequestInit
10
-
11
- /**
12
- * @param url The URL to fetch
13
- * @param config { preflight: ()=>Promise<boolean>, retryMs: number}
14
- * @returns Promise<Response>
15
- *
16
- * TryFetch will continuously try to fetch a URL every `retryMs`, with an
17
- * optional `preflight`. If `preflight` is specified, it must resolve to
18
- * `true` before tryFetch will attempt to fetch the URL. If `preflight`
19
- * resolves to `false`, tryFetch will delay by `retryMs` and then check again.
20
- * If `preflight` rejects, then tryFetch rejects and exits.
21
- *
22
- * Note: tryFetch exits ONLY on success or a rejected preflight.
23
- */
24
- export const tryFetch = async (
25
- url: string,
26
- config?: Partial<TryFetchConfig>,
27
- ) => {
28
- const { preflight, retryMs }: TryFetchConfig = {
29
- preflight: async () => true,
30
- retryMs: TRYFETCH_RETRY_MS,
31
- ...config,
32
- }
33
- const logger = LoggerService().create(`tryFetch`).breadcrumb(url)
34
- const { dbg } = logger
35
- return new Promise<Response>((resolve, reject) => {
36
- const again = () => setTimeout(_real_tryFetch, retryMs)
37
- const _real_tryFetch = async () => {
38
- if (preflight) {
39
- try {
40
- dbg(`Preflight: CHECK`)
41
- const shouldFetch = await preflight()
42
- if (!shouldFetch) {
43
- dbg(`Preflight: NOT READY`)
44
- again()
45
- return
46
- }
47
- dbg(`Preflight: READY`)
48
- } catch (e) {
49
- dbg(`Preflight: ABORT`)
50
- reject(e)
51
- return
52
- }
53
- }
54
- try {
55
- dbg(`Fetch: START`)
56
- const res = await fetch(url, {
57
- ...config,
58
- signal: AbortSignal.timeout(500),
59
- })
60
- dbg(`Fetch: SUCCESS`)
61
- resolve(res)
62
- } catch (e) {
63
- dbg(`Fetch: ERROR: trying again in ${retryMs}ms | ${e}`)
64
- again()
65
- }
66
- }
67
- _real_tryFetch()
68
- })
69
- }
package/src/plugin.ts DELETED
@@ -1,54 +0,0 @@
1
- import { forEach } from '@s-libs/micro-dash'
2
- import { produce } from 'immer'
3
- import {
4
- PocketHostPlugin,
5
- doSettingsFilter,
6
- namespace,
7
- newId,
8
- onAfterPluginsLoadedAction,
9
- onInstanceConfigFilter,
10
- onInstanceLogAction,
11
- onServeAction,
12
- onServeSlugsFilter,
13
- } from '../common'
14
- import { dbg, info } from './cli'
15
- import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants'
16
- import { setConfig } from './core/config'
17
- import { serve } from './server'
18
-
19
- export const pockethost: PocketHostPlugin = async ({}) => {
20
- onInstanceConfigFilter(async (config) => {
21
- return produce(config, (draft) => {
22
- draft.binds.hooks.push({
23
- src: PH_PROJECT_DIR(`src/instance-app/hooks/**/*`),
24
- base: PH_PROJECT_DIR(`src/instance-app/hooks`),
25
- })
26
- })
27
- })
28
-
29
- onInstanceLogAction(async ({ instance, type, data }) => {
30
- dbg(namespace(instance.subdomain, type), data.toString())
31
- })
32
-
33
- onServeAction(async ({ only }) => {
34
- if (!only.includes(`pockethost`)) return
35
- serve()
36
- })
37
-
38
- onServeSlugsFilter(async (slugs) => {
39
- return [...slugs, `pockethost`]
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
- })
54
- }
package/src/server.ts DELETED
@@ -1,129 +0,0 @@
1
- import express, { ErrorRequestHandler } from 'express'
2
- import 'express-async-errors'
3
- import { default as httpProxy } from 'http-proxy'
4
- import {
5
- LoggerService,
6
- doAfterInstanceFoundAction,
7
- doAfterServerStartAction,
8
- doAppMountedAction,
9
- doGetInstanceByRequestInfoFilter,
10
- doGetOrProvisionInstanceUrlFilter,
11
- doIncomingRequestAction,
12
- doRequestErrorAction,
13
- doRequestErrorMessageFilter,
14
- } from '../common'
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'
23
-
24
- export const serve = async () => {
25
- const _proxyLogger = LoggerService().create('ProxyService')
26
- const { dbg, error, info, trace, warn } = _proxyLogger
27
-
28
- // registerFilter(
29
- // CoreFilters.Waf_VHosts,
30
- // async (routes: { [_: string]: string }) => {
31
- // return { ...routes, '*': `http://localhost:${PORT()}` }
32
- // },
33
- // 99,
34
- // )
35
-
36
- const proxy = httpProxy.createProxyServer({})
37
- proxy.on('error', (err, req, res, target) => {
38
- warn(`Proxy error ${err} on ${req.url} (${req.headers.host})`)
39
- console.log(err)
40
- })
41
-
42
- const app = express()
43
-
44
- app.get('/_api/health', (req, res, next) => {
45
- res.json({ status: 'ok' })
46
- res.end
47
- })
48
-
49
- const errorHandler: ErrorRequestHandler = async (err, req, res, next) => {
50
- doRequestErrorAction({ err })
51
- res.status(500).send(await doRequestErrorMessageFilter(err.toString()))
52
- }
53
- app.use(errorHandler)
54
-
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 })
74
-
75
- app.all(`*`, async (req, res, next) => {
76
- const method = req.method
77
- const fullUrl = new URL(
78
- req.protocol + '://' + req.get('host') + req.originalUrl,
79
- )
80
- const host = fullUrl.hostname
81
- if (!host) {
82
- throw new Error(`Host not found`)
83
- }
84
- const subdomain = host.slice(0, -`.${APEX_DOMAIN()}`.length)
85
-
86
- dbg(
87
- `${method} ${fullUrl} (${subdomain}) -> ${`http://localhost:${PORT()}`}`,
88
- )
89
-
90
- await doIncomingRequestAction({ req, res, host })
91
-
92
- const instance = await doGetInstanceByRequestInfoFilter(undefined, {
93
- req,
94
- res,
95
- host,
96
- subdomain,
97
- })
98
- if (!instance) {
99
- res.status(404).end(`${host} not found`)
100
- return
101
- }
102
-
103
- await doAfterInstanceFoundAction({
104
- instance,
105
- req,
106
- res,
107
- host,
108
- })
109
-
110
- const url = await doGetOrProvisionInstanceUrlFilter('', {
111
- instance,
112
- })
113
-
114
- dbg(`Forwarding request for ${req.url} to instance ${url}`)
115
-
116
- proxy.web(req, res, { target: url })
117
-
118
- // next()
119
- })
120
-
121
- app.listen(PORT(), () => {
122
- info(`pockethost listening on port ${PORT()}`)
123
- doAfterServerStartAction()
124
- })
125
-
126
- asyncExitHook(async () => {
127
- info(`Shutting down pockethost`)
128
- })
129
- }