diff --git a/.changeset/1719706482967.md b/.changeset/1719706482967.md new file mode 100644 index 00000000..ccc8d26b --- /dev/null +++ b/.changeset/1719706482967.md @@ -0,0 +1,5 @@ +--- +'pockethost': minor +--- + +Add internal app (remote control) support \ No newline at end of file diff --git a/packages/pockethost/src/common/plugin/action.ts b/packages/pockethost/src/common/plugin/action.ts index 49a3af48..66004726 100644 --- a/packages/pockethost/src/common/plugin/action.ts +++ b/packages/pockethost/src/common/plugin/action.ts @@ -117,7 +117,9 @@ export const [doInstanceLogAction, onInstanceLogAction] = }>(CoreActions.InstanceLog) export const [doAppMountedAction, onAppMountedAction] = - createCustomActionWithContext<{ app: Express }>(CoreActions.AppMounted) + createCustomActionWithContext<{ app: Express; internalApp: Express }>( + CoreActions.AppMounted, + ) export const [doIncomingRequestAction, onIncomingRequestAction] = createCustomActionWithContext<{ req: Request; res: Response; host: string }>( diff --git a/packages/pockethost/src/common/schema/BaseFields.ts b/packages/pockethost/src/common/schema/BaseFields.ts new file mode 100644 index 00000000..c8136b48 --- /dev/null +++ b/packages/pockethost/src/common/schema/BaseFields.ts @@ -0,0 +1,10 @@ +export type RecordId = string +export type UserId = RecordId +export type IsoDate = string +export type BaseFields = { + id: RecordId + created: IsoDate + updated: IsoDate +} + +export type AnyField = { [_: string]: string | number } diff --git a/packages/pockethost/src/common/schema/Instance.ts b/packages/pockethost/src/common/schema/Instance.ts index 2e1eb651..a72778e1 100644 --- a/packages/pockethost/src/common/schema/Instance.ts +++ b/packages/pockethost/src/common/schema/Instance.ts @@ -1,4 +1,6 @@ import { doNewInstanceRecordFilter } from '../plugin' +import { IsoDate } from './BaseFields' +import { newId, pocketNow } from './util' export type VersionId = string @@ -21,16 +23,24 @@ export type InstanceSecretCollection = { } export type InstanceFields = { + id: string subdomain: string version: VersionId secrets: InstanceSecretCollection dev: boolean // Should instance run in --dev mode + created: IsoDate + updated: IsoDate } -export const mkInstance = (subdomain: string) => - doNewInstanceRecordFilter({ +export const mkInstance = (subdomain: string, id = newId()) => { + const now = pocketNow() + return doNewInstanceRecordFilter({ + id, subdomain, version: '*', secrets: {}, dev: false, + created: now, + updated: now, }) +} diff --git a/packages/pockethost/src/common/schema/util.ts b/packages/pockethost/src/common/schema/util.ts index f972ed18..86fd4859 100644 --- a/packages/pockethost/src/common/schema/util.ts +++ b/packages/pockethost/src/common/schema/util.ts @@ -1 +1,7 @@ -export const pocketNow = () => new Date().toISOString() +import { customAlphabet } from 'nanoid' +import { IsoDate } from './BaseFields' + +export const newId = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10) +export const pocketNow = () => toPocketDate(new Date()) +export const toPocketDate = (date: Date) => date.toISOString() +export const fromPocketDate = (iso: IsoDate) => new Date(iso) diff --git a/packages/pockethost/src/constants.ts b/packages/pockethost/src/constants.ts index ed9cc6d0..49641f47 100644 --- a/packages/pockethost/src/constants.ts +++ b/packages/pockethost/src/constants.ts @@ -4,7 +4,7 @@ import { dirname, join } from 'path' import { cwd } from 'process' import { fileURLToPath } from 'url' import { mkBoolean, mkCsvString, mkNumber, mkPath, mkString } from '../core' -import { InstanceId } from './common' +import { InstanceFields, InstanceId } from './common' import { DEBUG, IS_DEV } from './common/debug' import { Settings } from './core/Settings' import { logSettings } from './core/logSettings' @@ -32,6 +32,7 @@ export const settings = Settings({ PH_HOME: mkPath(_PH_HOME), PH_PROJECT_DIR: mkPath(_PH_PROJECT_DIR), + PH_INTERNAL_HOST: mkString('localhost'), PH_APEX_DOMAIN: mkString(_APEX_DOMAIN), PH_PORT: mkNumber(3000), @@ -39,6 +40,7 @@ export const settings = Settings({ PH_DATA_DIR: mkPath(join(_PH_HOME, 'data'), { create: true }), PH_DEV: mkBoolean(IS_DEV()), PH_DEBUG: mkBoolean(DEBUG()), + PH_INTERNAL_APP_SECRET: mkString(''), }) /** Accessors */ @@ -57,6 +59,14 @@ export const DATA_DIR = (...paths: string[]) => export const NODE_ENV = () => process.env.NODE_ENV export const INSTANCE_DATA_DIR = (id: InstanceId, ...paths: string[]) => join(DATA_DIR(), id, ...paths) +export const INTERNAL_HOST = () => settings.PH_INTERNAL_HOST +export const INTERNAL_APP_SECRET = () => settings.PH_INTERNAL_APP_SECRET +export const INTERNAL_APP_URL = (...paths: string[]) => + `${HTTP_PROTOCOL()}//${INTERNAL_HOST()}:${PORT()}/${INTERNAL_APP_PREFIX}/${join( + ...paths, + )}` +export const INTERNAL_APP_PREFIX = `_internal` +export const INTERNAL_APP_AUTH_HEADER = `x-pockethost-authorization` export const HTTP_PROTOCOL = () => (PORT() === 443 ? 'https:' : 'http:') export const PUBLIC_INSTANCE_URL = ({ subdomain }: Partial) => { const url = new URL(`${HTTP_PROTOCOL()}//${subdomain}.${APEX_DOMAIN()}`) diff --git a/packages/pockethost/src/plugin.ts b/packages/pockethost/src/plugin.ts index 60307f20..96ddca06 100644 --- a/packages/pockethost/src/plugin.ts +++ b/packages/pockethost/src/plugin.ts @@ -4,6 +4,7 @@ import { PocketHostPlugin, doSettingsFilter, namespace, + newId, onAfterPluginsLoadedAction, onInstanceConfigFilter, onInstanceLogAction, @@ -12,6 +13,7 @@ import { } from '../common' import { dbg, info } from './cli' import { INTERNAL_APP_SECRET, PH_PROJECT_DIR, settings } from './constants' +import { setConfig } from './core/config' import { serve } from './server' export const pockethost: PocketHostPlugin = async ({}) => { @@ -44,4 +46,9 @@ export const pockethost: PocketHostPlugin = async ({}) => { }) }, 99) + onAfterPluginsLoadedAction(async () => { + if (INTERNAL_APP_SECRET()) return + info(`Generating internal app secret...`) + setConfig(`PH_INTERNAL_APP_SECRET`, newId(30)) + }) } diff --git a/packages/pockethost/src/server.ts b/packages/pockethost/src/server.ts index 49155c51..ec048e67 100644 --- a/packages/pockethost/src/server.ts +++ b/packages/pockethost/src/server.ts @@ -12,7 +12,14 @@ import { doRequestErrorAction, doRequestErrorMessageFilter, } from '../common' -import { APEX_DOMAIN, PORT, asyncExitHook } from '../core' +import { + APEX_DOMAIN, + INTERNAL_APP_AUTH_HEADER, + INTERNAL_APP_PREFIX, + INTERNAL_APP_SECRET, + PORT, + asyncExitHook, +} from '../core' export const serve = async () => { const _proxyLogger = LoggerService().create('ProxyService') @@ -45,7 +52,25 @@ export const serve = async () => { } app.use(errorHandler) - doAppMountedAction({ app }) + const internalApp = express() + internalApp.use((req, res, next) => { + dbg(`Got a request to internal app ${req.url}`) + next() + }) + internalApp.use((req, res, next) => { + const authHeader = req.headers[INTERNAL_APP_AUTH_HEADER] + const internalAppSecret = INTERNAL_APP_SECRET() + + if (authHeader !== internalAppSecret) { + res.status(403).send('Forbidden') + return + } + + next() + }) + app.use(`/${INTERNAL_APP_PREFIX}`, internalApp) + + doAppMountedAction({ app, internalApp }) app.all(`*`, async (req, res, next) => { const method = req.method