Merge branch 'main' of github.com:pockethost/pockethost

This commit is contained in:
Ben Allfree 2024-11-21 18:14:58 +00:00
commit e87154dc68
71 changed files with 1684 additions and 996 deletions

View File

@ -2,10 +2,8 @@
"$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "master",
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

14
.changeset/pre.json Normal file
View File

@ -0,0 +1,14 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"@pockethost/dashboard": "0.4.1",
"pockethost": "1.7.0",
"pockethost-instance": "0.0.1"
},
"changesets": [
"funny-radios-behave",
"rare-beds-pull",
"tame-needles-play"
]
}

View File

@ -0,0 +1,5 @@
---
'pockethost': major
---
This release is a checkpoint release to jump over v1, which has been abandoned. v0 ended up becoming v2.

15
.cursorrules Normal file
View File

@ -0,0 +1,15 @@
# Project Context
- This project is about PocketBase, the PocketBase JS client, and the PocketBase JSVM.
# Programming Style
- Prefer factory functions that return an API object instead of classes
- Prefer early returns
# Generating Markdown
- When proposing an edit to a markdown file, first decide if there will be code snippets in the markdown file.
- If there are no code snippets, wrap the beginning and end of your answer in backticks and markdown as the language.
- If there are code snippets, indent the code snippets with two spaces and the correct language for proper rendering. Indentations level 0 and 4 is not allowed.
- If a markdown code block is indented with any value other than 2 spaces, automatically fix it

View File

@ -10,7 +10,7 @@ on:
env:
PUBLIC_APEX_DOMAIN: ${{ vars.PUBLIC_APEX_DOMAIN }}
MAIN_BRANCH: v0/main
MAIN_BRANCH: main
jobs:
publish:
@ -50,7 +50,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: pockethost-lander
projectName: pockethost
directory: ./packages/dashboard/build
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'

View File

@ -1,5 +0,0 @@
branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$branch" = "master" ]; then
npx --no -- commitlint --edit $1
fi

View File

View File

@ -1,6 +0,0 @@
# branch="$(git rev-parse --abbrev-ref HEAD)"
# if [ "$branch" = "v0/main" ]; then
# pnpm -r check:types
# pnpm -r build
# fi

View File

@ -16,6 +16,7 @@
"esbuild",
"eventsource",
"Flouder's",
"Frontends",
"fullchain",
"geoip",
"getenv",
@ -35,6 +36,7 @@
"mothership",
"multitenancy",
"multitenant",
"neverending",
"noaxis",
"nofile",
"nohup",

View File

@ -16,6 +16,7 @@
},
"type": "module",
"devDependencies": {
"@changesets/cli": "^2.27.9",
"prettier": "^3.0.3",
"prettier-plugin-jsdoc": "^1.3.0",
"prettier-plugin-organize-imports": "^4.1.0",

View File

@ -1,6 +1,6 @@
{
"name": "@pockethost/dashboard",
"version": "1.0.0-rc.1",
"version": "0.4.1",
"private": true,
"main": "./src/app.html",
"scripts": {
@ -8,6 +8,7 @@
"preview": "npx http-server@latest ./build -P \"http://localhost:8080?\"",
"dev": "vite dev",
"build": "NODE_ENV=production vite build",
"deploy": "wrangler pages deploy ./build",
"lint": "prettier --check .",
"format": "prettier --write .",
"postbuild": "svelte-sitemap --domain https://pockethost.io"
@ -47,6 +48,7 @@
"svelte-sitemap": "^2.6.0",
"tailwindcss": "^3.4.13",
"unist-util-visit": "^5.0.0",
"vite": "^5.4.8"
"vite": "^5.4.8",
"wrangler": "^3.87.0"
}
}

View File

@ -1,3 +1,4 @@
import { browser } from '$app/environment'
import { INSTANCE_URL } from '$src/env'
import { createGenericSyncEvent } from '$util/events'
import { fetchEventSource } from '@microsoft/fetch-event-source'
@ -210,13 +211,15 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
* token may be out of date, or fields in the user record may have changed
* in the backend.
*/
refreshAuthToken()
.catch((error) => {
client.authStore.clear()
})
.finally(() => {
fireAuthChange(client.authStore)
})
if (browser) {
refreshAuthToken()
.catch((error) => {
client.authStore.clear()
})
.finally(() => {
fireAuthChange(client.authStore)
})
}
/**
* Listen for auth state changes and subscribe to realtime _user events.

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { userSubscriptionType } from '$util/stores'
import { PLAN_NAMES, SubscriptionType } from 'pockethost'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
import { userStore } from '$util/stores'
import { onMount } from 'svelte'
import FlounderCard from './FlounderCard.svelte'

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { PLAN_NAMES, SubscriptionType } from 'pockethost'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
import { stats, userStore } from '$util/stores'
import PricingCard from '$src/routes/(static)/pricing/PricingCard.svelte'
@ -15,6 +15,11 @@
]
export let startDate: Date | null = null
export let endDate: Date | null = null
const logInFirst = () => {
alert('Please log in and verify your account first.')
window.location.href = '/login'
}
</script>
<PricingCard
@ -27,6 +32,7 @@
description="Epic elite! The Flounder's Edition is almost as good as the Founder's edition, and you'll help PocketHost go global."
{priceMonthly}
{priceAnnually}
requireAuthenticatedUser
checkoutMonthURL="https://store.pockethost.io/buy/9ff8775b-6b9e-4aa8-a0ab-dc5e58e25408?checkout[custom][user_id]={$userStore?.id}&checkout[email]={$userStore?.email}&checkout[discount_code]=G0MTI0OQ"
checkoutYearURL="https://store.pockethost.io/buy/82d79f7c-64f6-4c2b-9f58-dcc8951f1cdd?checkout[custom][user_id]={$userStore?.id}&checkout[email]={$userStore?.email}&checkout[discount_code]=G0MTI0OQ"
features={[

View File

@ -3,7 +3,7 @@
import { globalInstancesStore, userSubscriptionType } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import InstanceList from './InstanceList.svelte'
import { SubscriptionType } from 'pockethost'
import { SubscriptionType } from 'pockethost/common'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'

View File

@ -5,7 +5,7 @@
import { globalInstancesStore } from '$util/stores'
import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
import { values } from '@s-libs/micro-dash'
import { type InstanceId } from 'pockethost'
import { type InstanceId } from 'pockethost/common'
import Fa from 'svelte-fa'
const { updateInstance } = client()
@ -26,7 +26,7 @@
{#each values($globalInstancesStore).sort( (a, b) => a.subdomain.localeCompare(b.subdomain), ) as instance, index}
<button
class={`card min-w-80 lg:max-w-80 flex-1 m-4 transition hover:bg-base-300 ${instance.maintenance ? 'bg-base-200' : 'bg-neutral'}`}
on:click={_=>goto(`/instances/${instance.id}`)}
on:click={(_) => goto(`/instances/${instance.id}`)}
>
<div class="card-body w-full">
<div class="card-title">
@ -38,7 +38,7 @@
? 'bg-red-500 hover:bg-red-500'
: 'toggle-success'}"
checked={!instance.maintenance}
on:click={e=>e.stopPropagation()}
on:click={(e) => e.stopPropagation()}
on:change={handleMaintenanceChange(instance.id)}
/>
</div>

View File

@ -4,7 +4,7 @@
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import { reduce } from '@s-libs/micro-dash'
import { logger, type UpdateInstancePayload } from 'pockethost'
import { logger, type UpdateInstancePayload } from 'pockethost/common'
import Fa from 'svelte-fa'
import { faTrash } from '@fortawesome/free-solid-svg-icons'

View File

@ -1,52 +1,57 @@
<script>
// Define the width of the sidebar
const sidebarWidth = '300px'
import { page } from '$app/stores';
import { page } from '$app/stores'
import DocLink from './DocLink.svelte'
</script>
<div class="container-fluid mt-4 flex gap-6 flex-col-reverse md:flex-row">
<div class="w-full md:w-fit bg-base-200 rounded-r-lg py-4">
<ul class="menu text-base">
<li class="menu-title">Overview</li>
<li><a class:active={$page.url.pathname.endsWith("/docs")} href="/docs">Introduction</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/getting-started")} href="/docs/getting-started">Getting Started</a></li>
<DocLink path="introduction" title="Introduction" />
<DocLink path="getting-started" title="Getting Started" />
</ul>
<ul class="menu">
<li class="menu-title">Instance Management</li>
<li><a class:active={$page.url.pathname.endsWith("/docs/create")} href="/docs/create">Create</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/accessing")} href="/docs/accessing">Connect</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/power")} href="/docs/power">Power</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/rename-instance")} href="/docs/rename-instance">Rename</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/delete")} href="/docs/delete">Delete</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/limits")} href="/docs/limits">Limits</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/smtp")} href="/docs/smtp">Outgoing Email</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/versions")} href="/docs/versions">Changing Versions</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/s3")} href="/docs/s3">Using S3 Storage</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/custiom-binaries")} href="/docs/custom-binaries">Custom Binaries</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/custom-domains")} href="/docs/custom-domains">Custom Domains</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/backup-restore")} href="/docs/backup-restore">Backup/Restore</a></li>
<DocLink path="create" title="Create" />
<DocLink path="accessing" title="Connect" />
<DocLink path="power" title="Power" />
<DocLink path="rename-instance" title="Rename" />
<DocLink path="delete" title="Delete" />
<DocLink path="limits" title="Limits" />
<DocLink path="smtp" title="Outgoing Email" />
<DocLink path="versions" title="Changing Versions" />
<DocLink path="s3" title="Using S3 Storage" />
<DocLink path="custom-binaries" title="Custom Binaries" />
<DocLink path="custom-domains" title="Custom Domains" />
<DocLink path="backup-restore" title="Backup/Restore" />
</ul>
<ul class="menu">
<li class="menu-title">Daily Use Guide</li>
<li><a class:active={$page.url.pathname.endsWith("/docs/logs")} href="/docs/logs">Logging</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/dev-mode")} href="/docs/dev-mode">Dev Mode</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/secrets")} href="/docs/secrets">Secrets</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/ftp")} href="/docs/ftp">FTP Access</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/admin-sync")} href="/docs/admin-sync">Admin Sync</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/js")} href="/docs/js">Extending via JS</a></li>
<DocLink path="logs" title="Logging" />
<DocLink path="dev-mode" title="Dev Mode" />
<DocLink path="secrets" title="Secrets" />
<DocLink path="ftp" title="FTP Access" />
<DocLink path="admin-sync" title="Admin Sync" />
<DocLink path="js" title="Extending via JS" />
</ul>
<ul class="menu">
<li class="menu-title">Programming Guide</li>
<li><a class:active={$page.url.pathname.endsWith("/docs/programming")} href="/docs/programming">Frontends and JS Hooks</a></li>
<DocLink path="programming" title="Frontends and JS Hooks" />
<DocLink
path="server-side-pocketbase-antipattern"
title="Server-Side PocketBase is an Anti-Pattern"
/>
</ul>
<ul class="menu">
<li class="menu-title">Appendix</li>
<li><a class:active={$page.url.pathname.endsWith("/docs/account-creation")} href="/docs/account-creation">Account Creation</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/pricing-ethos")} href="/docs/pricing-ethos">Pricing Ethos</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/faq")} href="/docs/faq">FAQ</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/gs-gmail")} href="/docs/gs-gmail">Google Suite Gmail Setup</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/ses")} href="/docs/ses">Amazon SES Setup</a></li>
<li><a class:active={$page.url.pathname.endsWith("/docs/self-hosting")} href="/docs/self-hosting">Self-Hosting</a></li>
<DocLink path="account-creation" title="Account Creation" />
<DocLink path="pricing-ethos" title="Pricing Ethos" />
<DocLink path="faq" title="FAQ" />
<DocLink path="gs-gmail" title="Google Suite Gmail Setup" />
<DocLink path="ses" title="Amazon SES Setup" />
<DocLink path="self-hosting" title="Self-Hosting" />
</ul>
</div>
<div class="docs-content prose p-5 w-full md:w-fit md:max-w-[50%]">
@ -55,7 +60,8 @@
</div>
<style lang="scss">
.menu, .menu-title {
@apply text-base
.menu,
.menu-title {
@apply text-base;
}
</style>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { page } from '$app/stores'
export let path: string
export let title: string
$: isActive = $page.url.pathname.endsWith(`/docs/${path}`)
</script>
<li class:active={isActive} class="w-48">
<a href={`/docs/${path}`}>{title}</a>
</li>
<style lang="scss">
li.active {
@apply bg-neutral rounded-md;
}
</style>

View File

@ -0,0 +1,74 @@
---
title: Server-Side PocketBase is an Anti-Pattern
category: programming
description: Using PocketBase from SvelteKit or Next.js server files is generally an antipattern.
---
# Server-Side PocketBase is an Anti-Pattern
When building applications with PocketBase, it's tempting to access it from server-side code in frameworks like SvelteKit or Next.js. However, this approach often indicates architectural issues and should generally be avoided. Here's why:
## The Problem with Server-Side Access
### Double Network Hops
When you access PocketBase from your server-side code, requests make two network hops:
1. Client -> Your Server
2. Your Server -> PocketBase
This adds unnecessary latency compared to direct client-to-PocketBase communication.
### JWT State Management Complexity
Managing authentication state becomes more complex when you need to transfer JWT tokens between client and server. This often leads to security vulnerabilities when not handled properly.
### Rate Limiting Issues
Accessing PocketBase from a single backend IP address can trigger rate limits more easily than distributed client access. This is intentionally designed to encourage direct client communication.
## Better Approaches
### Use Direct Client Access
PocketBase is designed to be accessed directly from client applications. Its built-in security rules provide fine-grained access control without needing a middleware server.
### Leverage JS Hooks for Privileged Operations
If you need server-side logic or privileged operations, use PocketBase's JS hooks feature instead of wrapping PocketBase calls in a separate backend:
- Create custom API endpoints using JS hooks
- Handle privileged operations directly in PocketBase
- Implement business logic where it belongs
### Consider Static Site Generation
If you're using server-side rendering primarily to protect PocketBase access, consider:
- Moving to static site generation (SSG)
- Using PocketBase's security rules for protection
- Implementing sensitive operations via JS hooks
## When Server-Side Access Makes Sense
While generally an anti-pattern, there are valid cases for server-side PocketBase access:
- Complex data aggregation requiring server resources
- Integration with external services that can't be exposed to clients
- Specific security requirements that can't be met with API rules
However, even in these cases, consider whether the functionality could be implemented using PocketBase's native features first.
## Further Reading
Gani also posted about this on the PocketBase site: [JS SSR - issues and recommendations when interacting with PocketBase](https://github.com/pocketbase/pocketbase/discussions/5313)
## Conclusion
PocketBase is designed to be a complete backend solution with built-in security and extensibility. Adding an additional server layer often complicates the architecture unnecessarily. Before implementing server-side PocketBase access, consider whether you can:
1. Use client-side access with security rules
2. Implement the functionality via JS hooks
3. Restructure your application to leverage PocketBase's native capabilities
This will lead to simpler, more maintainable, and more performant applications.

View File

@ -7,6 +7,7 @@
import NewInstanceProcessingBlock from './NewInstanceProcessingBlock.svelte'
import Fa from 'svelte-fa'
import { faArrowRight, faRotate } from '@fortawesome/free-solid-svg-icons'
import { onMount } from 'svelte'
export let isSignUpView: boolean = false
let isProcessing: boolean = false
@ -30,34 +31,38 @@
instanceNameField.set(name)
}
instanceNameField.subscribe(async (name) => {
if (name === $instanceInfo.name) return
try {
instanceInfo.update((info) => ({
...info,
fetching: true,
}))
const res = await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
instanceInfo.update((info) => ({
...info,
fetching: false,
available: true,
name,
}))
} catch (e) {
instanceInfo.update((info) => ({
...info,
fetching: false,
available: false,
name,
}))
}
onMount(() => {
instanceNameField.subscribe(async (name) => {
if (name === $instanceInfo.name) return
try {
instanceInfo.update((info) => ({
...info,
fetching: true,
}))
const res = await client().client.send(
`/api/signup?name=${encodeURIComponent(name)}`,
{},
)
instanceInfo.update((info) => ({
...info,
fetching: false,
available: true,
name,
}))
} catch (e) {
instanceInfo.update((info) => ({
...info,
fetching: false,
available: false,
name,
}))
}
})
})
generateSlug()
onMount(() => {
generateSlug()
})
// Set up the variables to hold the form information
let email: string = ''

View File

@ -1,6 +1,6 @@
<script lang="ts">
import PricingCard from '$src/routes/(static)/pricing/PricingCard.svelte'
import { PLAN_NAMES, SubscriptionType } from 'pockethost'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
import PricingTable from './PricingTable.svelte'
import { userStore } from '$util/stores'
import FlounderCard from '$src/routes/(app)/account/FlounderCard.svelte'

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { faClock, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'
import { userStore } from '$util/stores'
import Fa from 'svelte-fa'
import { onMount, onDestroy } from 'svelte'
import { writable } from 'svelte/store'
@ -18,6 +19,7 @@
export let fundingGoals: string[] = []
export let startDate: Date | null = null
export let endDate: Date | null = null
export let requireAuthenticatedUser = false
const comingSoon = startDate && startDate > new Date()
@ -73,6 +75,9 @@
} else if (qtyRemaining <= 0) {
event.preventDefault()
alert(soldOutText)
} else if (requireAuthenticatedUser && !$userStore?.verified) {
event.preventDefault()
alert('Please create and verify your account first.')
}
}
</script>

View File

@ -3,18 +3,16 @@
</script>
{#if !$isMothershipReachable}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="alert alert-error max-w-2xl mx-4">
<div class="text-center">
<h2 class="text-xl font-bold mb-2">Connection Lost</h2>
<p>
The PocketHost mothership is temporarily unreachable. Your instances
are safe and still reachable. Go to the <a
href="https://status.pockethost.io"
class="link font-semibold underline">status page</a
> for more information.
</p>
</div>
<div class="bg-error text-error-content p-4 rounded-none">
<div class="text-center">
<h2 class="text-xl font-bold mb-2">Connection Lost</h2>
<p>
The PocketHost mothership is temporarily unreachable. Your instances are
safe and still reachable. Go to the <a
href="https://status.pockethost.io"
class="link font-semibold underline">status page</a
> for more information.
</p>
</div>
</div>
{/if}

View File

@ -1,16 +1,20 @@
<script lang="ts">
</script>
<div class="alert alert-info bg-yellow-300 rounded-none">
<div class="text-info-content">
This promo banner will go away after Dec 2. Please spread the word and help
close the Flouder's round.
</div>
<div class="alert alert-info bg-yellow-300 rounded-none mb-10">
<div>
<a
href="https://www.producthunt.com/posts/pockethost?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-pockethost"
class="btn btn-sm btn-neutral">Vote on Product Hunt</a
>
<a href="/pricing" class="btn btn-sm btn-neutral">Black Friday ON now!</a>
<div class="text-info-content">
This promo banner will go away after Dec 2. Please spread the word and
help close the Flouder's round.
</div>
<div>
<a
href="https://www.producthunt.com/posts/pockethost?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-pockethost"
class="btn btn-sm btn-neutral m-2">Vote on Product Hunt</a
>
<a href="/pricing" class="btn btn-sm btn-neutral m-2"
>Black Friday ON now!</a
>
</div>
</div>
</div>

View File

@ -41,21 +41,33 @@ export const stats = writable<{
total_flounder_subscribers: 0,
})
const checkStats = () => {
client()
.client.send(`/api/stats`, {})
.then((res) => {
stats.set(res)
isMothershipReachable.set(true)
// setTimeout(checkStats, 1000 * 60 * 5)
})
.catch(() => {
// isMothershipReachable.set(false)
// setTimeout(checkStats, 1000)
})
}
const continuouslyCheckMothershipReachability = () => {
setInterval(() => {
client()
.client.send(`/api/health`, {})
.then((res) => {
isMothershipReachable.set(true)
})
}, 5000)
}
export const init = () => {
const { onAuthChange } = client()
const checkStats = () => {
client()
.client.send(`/api/stats`, {})
.then((res) => {
stats.set(res)
setTimeout(checkStats, 1000 * 60 * 5)
})
.catch(() => {
isMothershipReachable.set(false)
setTimeout(checkStats, 1000)
})
}
checkStats()
onAuthChange((authStoreProps) => {
@ -121,15 +133,4 @@ export const init = () => {
}
tryInstanceSubscribe()
})
setInterval(() => {
client()
.client.send(`/api/health`, {})
.then((res) => {
isMothershipReachable.set(true)
})
.catch(() => {
isMothershipReachable.set(false)
})
}, 5000)
}

View File

@ -13,7 +13,7 @@ const config: UserConfig = {
server: {
port: 5174,
strictPort: true,
host: 'app.pockethost.lvh.me',
host: 'pockethost.lvh.me',
hmr: {
clientPort: 5174,
},

View File

@ -2,6 +2,7 @@
"name": "pockethost-instance",
"version": "0.0.1",
"scripts": {
"build": "docker build . -t benallfree/pockethost-instance:${npm_package_version} -t benallfree/pockethost-instance:latest"
"build": "docker build . -t benallfree/pockethost-instance:${npm_package_version} -t benallfree/pockethost-instance:latest",
"push": "docker push benallfree/pockethost-instance:${npm_package_version} && docker push benallfree/pockethost-instance:latest"
}
}

View File

@ -0,0 +1,5 @@
# pockethost
## 2.0.0-rc.0
This release is a checkpoint release to jump over v1, which has been abandoned. v0 ended up becoming v2.

View File

@ -1 +0,0 @@
export * from './src/common'

View File

@ -1,2 +0,0 @@
export * from './src/common'
export * from './src/core'

View File

@ -1,13 +1,19 @@
{
"name": "pockethost",
"version": "0.12.0",
"version": "2.0.0-rc.0",
"author": {
"name": "Ben Allfree",
"url": "https://github.com/benallfree"
},
"license": "MIT",
"main": "./common.ts",
"module": "./common.ts",
"exports": {
".": {
"import": "./src/index.ts"
},
"./common": {
"import": "./src/common/index.ts"
}
},
"type": "module",
"bin": {
"pockethost": "src/cli/index.ts"
@ -72,5 +78,11 @@
"@types/tail": "^2.2.3",
"@types/unzipper": "^0.10.8",
"@types/vhost": "^3.0.9"
}
},
"files": [
"src",
"CHANGELOG.md",
"LICENSE",
"README.md"
]
}

View File

@ -1,14 +1,14 @@
import Dockerode from 'dockerode'
import { ErrorRequestHandler } from 'express'
import { logger } from '../../../../../common'
import {
MOTHERSHIP_ADMIN_PASSWORD,
MOTHERSHIP_ADMIN_USERNAME,
MOTHERSHIP_URL,
discordAlert,
logger,
neverendingPromise,
tryFetch,
} from '../../../../../core'
} from '../../../../..'
import {
DOCKER_INSTANCE_IMAGE_NAME,
MothershipAdminClientService,

View File

@ -1,5 +1,5 @@
import { Command } from 'commander'
import { logger } from '../../../../../../common'
import { logger } from '../../../../../common'
import { daemon } from './daemon'
type Options = {

View File

@ -4,6 +4,7 @@ import { spawn } from 'child_process'
import { Mode, constants, createReadStream, createWriteStream } from 'fs'
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
import { dirname, isAbsolute, join, normalize, resolve, sep } from 'path'
import { DATA_ROOT } from '../../../../..'
import {
InstanceFields,
Logger,
@ -11,8 +12,10 @@ import {
assert,
seqid,
} from '../../../../../common'
import { DATA_ROOT } from '../../../../../core'
import { InstanceLogger, InstanceLoggerApi } from '../../../../../services'
import {
InstanceLogWriter,
InstanceLogWriterApi,
} from '../../../../../services'
import * as fsAsync from './fs-async'
import { MAINTENANCE_ONLY_INSTANCE_ROOTS } from './guards'
@ -48,7 +51,7 @@ const checkBun = (
[`bun.lockb`, `package.json`].includes(maybeImportant || ''))
if (isImportant) {
const logger = InstanceLogger(instance.id, `exec`, { ttl: 5000 })
const logger = InstanceLogWriter(instance.id, `exec`)
logger.info(`${maybeImportant} changed, running bun install`)
launchBunInstall(instance, virtualPath, cwd).catch(console.error)
}
@ -56,7 +59,7 @@ const checkBun = (
const runBun = (() => {
const bunLimiter = new Bottleneck({ maxConcurrent: 1 })
return (cwd: string, logger: InstanceLoggerApi) =>
return (cwd: string, logger: InstanceLogWriterApi) =>
bunLimiter.schedule(
() =>
new Promise<number | null>((resolve) => {
@ -100,7 +103,7 @@ const launchBunInstall = (() => {
runCache[cwd] = { runAgain: true }
while (runCache[cwd]!.runAgain) {
runCache[cwd]!.runAgain = false
const logger = InstanceLogger(instance.id, `exec`)
const logger = InstanceLogWriter(instance.id, `exec`)
logger.info(`Launching 'bun install' in ${virtualPath}`)
await runBun(cwd, logger)
}

View File

@ -1,11 +1,5 @@
import { readFileSync } from 'fs'
import { FtpSrv } from 'ftp-srv'
import {
PocketBase,
logger,
mergeConfig,
mkSingleton,
} from '../../../../../common'
import {
MOTHERSHIP_URL,
PH_FTP_PASV_IP,
@ -15,7 +9,13 @@ import {
SSL_CERT,
SSL_KEY,
asyncExitHook,
} from '../../../../../core'
} from '../../../../..'
import {
PocketBase,
logger,
mergeConfig,
mkSingleton,
} from '../../../../../common'
import { PhFs } from './PhFs'
export type FtpConfig = { mothershipUrl: string }

View File

@ -1,9 +1,5 @@
import { MOTHERSHIP_URL, neverendingPromise, tryFetch } from '../../../../..'
import { logger } from '../../../../../common'
import {
MOTHERSHIP_URL,
neverendingPromise,
tryFetch,
} from '../../../../../core'
import { ftpService } from '../FtpService'
export async function ftp() {

View File

@ -1,5 +1,5 @@
import { Command } from 'commander'
import { logger } from '../../../../../../common'
import { logger } from '../../../../../common'
import { ftp } from './ftp'
type Options = {

View File

@ -13,7 +13,7 @@ export const createIpWhitelistMiddleware = (blockedCIDRs: string[]) => {
const ip = req.ip // or req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (
blockedCIDRs.length === 0 ||
blockedCIDRObjects.some((cidr) => cidr.contains(ip))
(ip && blockedCIDRObjects.some((cidr) => cidr.contains(ip)))
) {
next()
} else {

View File

@ -7,7 +7,6 @@ import fs from 'fs'
import http from 'http'
import { createProxyMiddleware } from 'http-proxy-middleware'
import https from 'https'
import { logger } from '../../../../../common/Logger'
import {
APEX_DOMAIN,
DAEMON_PORT,
@ -18,7 +17,8 @@ import {
neverendingPromise,
SSL_CERT,
SSL_KEY,
} from '../../../../../core'
} from '../../../../..'
import { logger } from '../../../../../common/Logger'
import { createIpWhitelistMiddleware } from './cidr'
import { createVhostProxyMiddleware } from './createVhostProxyMiddleware'
@ -31,7 +31,7 @@ export const firewall = async () => {
const DEV_ROUTES = {
[`mail.${APEX_DOMAIN()}`]: `http://localhost:${1080}`,
[`${MOTHERSHIP_NAME()}.${APEX_DOMAIN()}`]: `http://localhost:${MOTHERSHIP_PORT()}`,
[`${APEX_DOMAIN()}`]: `http://localhost:${8080}`,
[`${APEX_DOMAIN()}`]: `http://localhost:${5174}`,
}
const hostnameRoutes = IS_DEV() ? DEV_ROUTES : PROD_ROUTES

View File

@ -1,5 +1,5 @@
import { Command } from 'commander'
import { logger } from '../../../../common'
import { logger } from '../../../common'
import { ServeCommand } from './ServeCommand'
type Options = {

View File

@ -9,7 +9,7 @@ import {
LoggerService,
MOTHERSHIP_PORT,
stringify,
} from '../../../../core'
} from '../../..'
export const checkHealth = async () => {
const { cpu, drive } = osu

View File

@ -1,7 +1,6 @@
import { execSync } from 'child_process'
import { globSync } from 'glob'
import { DATA_ROOT } from '../../../../core'
import { logger } from '../../../common/Logger'
import { DATA_ROOT, logger } from '../../..'
export const compact = async () => {
const { info, error } = logger()

View File

@ -1,5 +1,5 @@
import { Command } from 'commander'
import { logger } from '../../../../core'
import { logger } from '../../..'
type Options = {
debug: boolean

View File

@ -5,7 +5,7 @@ import {
MOTHERSHIP_DATA_ROOT,
MOTHERSHIP_MIGRATIONS_DIR,
MOTHERSHIP_SEMVER,
} from '../../../../../core'
} from '../../../..'
import { GobotService } from '../../../../services/GobotService'
export async function schema() {

View File

@ -1,5 +1,6 @@
import { GobotOptions } from 'gobot'
import {
APP_URL,
DISCORD_ALERT_CHANNEL_URL,
DISCORD_HEALTH_CHANNEL_URL,
DISCORD_STREAM_CHANNEL_URL,
@ -15,7 +16,7 @@ import {
TEST_EMAIL,
_MOTHERSHIP_APP_ROOT,
mkContainerHomePath,
} from '../../../../../core'
} from '../../../..'
import { GobotService } from '../../../../services/GobotService'
export type MothershipConfig = {}
@ -35,6 +36,7 @@ export async function mothership(cfg: MothershipConfig) {
DISCORD_HEALTH_CHANNEL_URL: DISCORD_HEALTH_CHANNEL_URL(),
DISCORD_ALERT_CHANNEL_URL: DISCORD_ALERT_CHANNEL_URL(),
TEST_EMAIL: TEST_EMAIL(),
APP_URL: APP_URL(),
}
dbg(env)

View File

@ -5,7 +5,7 @@ import {
MOTHERSHIP_HOOKS_DIR,
PH_ALLOWED_POCKETBASE_SEMVER,
stringify,
} from '../../../../../core'
} from '../../../..'
import { GobotService } from '../../../../services/GobotService'
function compareSemVer(a: string, b: string): number {

View File

@ -2,14 +2,14 @@ import { map } from '@s-libs/micro-dash'
import Database from 'better-sqlite3'
import Bottleneck from 'bottleneck'
import { Command, InvalidArgumentError } from 'commander'
import { PocketBase, UserFields, logger } from '../../../common'
import {
MOTHERSHIP_ADMIN_PASSWORD,
MOTHERSHIP_ADMIN_USERNAME,
MOTHERSHIP_DATA_DB,
MOTHERSHIP_URL,
TEST_EMAIL,
} from '../../../core'
} from '../../..'
import { PocketBase, UserFields, logger } from '../../../common'
const TBL_SENT_MESSAGES = `sent_messages`

View File

@ -1,6 +1,6 @@
import { Command } from 'commander'
import { neverendingPromise } from '../../..'
import { logger } from '../../../common'
import { neverendingPromise } from '../../../core'
import { daemon } from '../EdgeCommand/DaemonCommand/ServeCommand/daemon'
import { firewall } from '../FirewallCommand/ServeCommand/firewall/server'
import { mothership } from '../MothershipCommand/ServeCommand/mothership'

View File

@ -2,7 +2,7 @@
import { program } from 'commander'
import EventSource from 'eventsource'
import { LogLevelName, gracefulExit, logger } from '../../core'
import { LogLevelName, gracefulExit, logger } from '..'
import { version } from '../../package.json'
import { EdgeCommand } from './commands/EdgeCommand'
import { FirewallCommand } from './commands/FirewallCommand'

View File

@ -1,7 +1,7 @@
import { WinstonLoggerService } from '..'
import { version } from '../../package.json'
import { ioc } from '../common'
import { RegisterEnvSettingsService } from '../constants'
import { WinstonLoggerService } from '../core/winston'
import { GobotService } from '../services/GobotService'
export const initIoc = async () => {

View File

@ -12,15 +12,13 @@ import {
SettingsHandlerFactory,
SettingsService,
ioc,
} from '../core'
import {
mkBoolean,
mkCsvString,
mkNumber,
mkPath,
mkString,
} from './core/Settings'
import { settings } from './core/ioc'
settings,
} from '.'
const __dirname = dirname(fileURLToPath(import.meta.url))
@ -45,7 +43,6 @@ export const _HTTP_PROTOCOL = env
.get('HTTP_PROTOCOL')
.default('https:')
.asString()
export const _APP_NAME = env.get('APP_NAME').default('app').asString()
export const _MOTHERSHIP_NAME = env
.get('MOTHERSHIP_NAME')
.default('pockethost-central')
@ -78,16 +75,14 @@ const createDevCert = async () => {
writeFileSync(join(_SSL_HOME, `${TLS_PFX}.cert`), cert)
}
export const SETTINGS = {
export const createSettings = () => ({
PH_ALLOWED_POCKETBASE_SEMVER: mkString(`0.22.*`),
PH_HOME: mkPath(_PH_HOME, { create: true }),
PH_PROJECT_ROOT: mkPath(PH_PROJECT_ROOT()),
HTTP_PROTOCOL: mkString(_HTTP_PROTOCOL),
APP_NAME: mkString(_APP_NAME),
APP_URL: mkString(`${_HTTP_PROTOCOL}//${_APP_NAME}.${_APEX_DOMAIN}`),
BLOG_URL: mkString(`${_HTTP_PROTOCOL}//${_APEX_DOMAIN}`),
APP_URL: mkString(`${_HTTP_PROTOCOL}//${_APEX_DOMAIN}`),
APEX_DOMAIN: mkString(_APEX_DOMAIN),
IPCIDR_LIST: mkCsvString([]),
@ -142,7 +137,7 @@ export const SETTINGS = {
DOCKER_CONTAINER_HOST: mkString(`host.docker.internal`),
PH_GOBOT_ROOT: mkPath(join(_PH_HOME, 'gobot'), { create: true }),
}
})
export type Settings = ReturnType<typeof RegisterEnvSettingsService>
export type SettingsDefinition = {
@ -150,7 +145,7 @@ export type SettingsDefinition = {
}
export const RegisterEnvSettingsService = () => {
const _settings = SettingsService(SETTINGS)
const _settings = SettingsService(createSettings())
ioc('settings', _settings)
@ -173,11 +168,8 @@ export const DEBUG = () =>
env.get(`PH_DEBUG`).default(_IS_DEV.toString()).asBool()
export const HTTP_PROTOCOL = () => settings().HTTP_PROTOCOL
export const APP_URL = () => settings().APP_URL
export const APP_NAME = () => settings().APP_NAME
export const BLOG_URL = (...path: string[]) =>
join(settings().BLOG_URL, ...path)
export const DOCS_URL = (...path: string[]) => BLOG_URL(`docs`, ...path)
export const APP_URL = (...path: string[]) =>
[settings().APP_URL, path.join(`/`)].filter(Boolean).join('/')
export const APEX_DOMAIN = () => settings().APEX_DOMAIN
@ -186,13 +178,15 @@ export const DAEMON_PORT = () => settings().DAEMON_PORT
export const DAEMON_PB_IDLE_TTL = () => settings().DAEMON_PB_IDLE_TTL
export const MOTHERSHIP_URL = (...path: string[]) =>
join(
[
env
.get('MOTHERSHIP_URL')
.default(`${HTTP_PROTOCOL()}://${MOTHERSHIP_NAME()}:${APEX_DOMAIN()}`)
.asString(),
...path,
)
path.join('/'),
]
.filter(Boolean)
.join('/')
export const MOTHERSHIP_NAME = () => settings().MOTHERSHIP_NAME
export const MOTHERSHIP_ADMIN_USERNAME = () =>
@ -264,18 +258,15 @@ export const INSTANCE_DATA_DB = (id: InstanceId) =>
join(DATA_ROOT(), id, `pb_data`, `data.db`)
export const mkContainerHomePath = (...path: string[]) =>
join(`/home/pockethost`, ...path.filter((v) => !!v))
export const mkAppUrl = (path = '') => `${APP_URL()}${path}`
export const mkBlogUrl = (path = '') => `${BLOG_URL()}${path}`
export const mkDocUrl = (path = '') => mkBlogUrl(join('/docs', path))
export const DOC_URL = (...path: string[]) => APP_URL('docs', ...path)
export const mkInstanceCanonicalHostname = (instance: InstanceFields) =>
(instance.cname_active && instance.cname) || `${instance.id}.${APEX_DOMAIN()}`
export const mkInstanceHostname = (instance: InstanceFields) =>
`${instance.subdomain}.${APEX_DOMAIN()}`
[instance.subdomain, APEX_DOMAIN()].filter(Boolean).join('.')
export const mkInstanceUrl = (instance: InstanceFields, ...paths: string[]) =>
[
`${HTTP_PROTOCOL()}//${mkInstanceHostname(instance)}`,
paths.length ? join(...paths) : '',
].join('')
[`${HTTP_PROTOCOL()}//${mkInstanceHostname(instance)}`, paths.join(`/`)]
.filter(Boolean)
.join('/')
export const mkInstanceDataPath = (instanceId: string, ...path: string[]) =>
join(settings().DATA_ROOT, instanceId, ...path)
@ -287,9 +278,6 @@ export const logConstants = () => {
PH_PROJECT_ROOT,
HTTP_PROTOCOL,
APP_URL,
APP_NAME,
BLOG_URL,
DOCS_URL,
APEX_DOMAIN,
IPCIDR_LIST,
DAEMON_PORT,

View File

@ -0,0 +1,19 @@
import { existsSync, mkdirSync } from 'fs'
import { Logger } from '../common'
import { mkInstanceDataPath } from '../constants'
export function ensureInstanceDirectoryStructure(
instanceId: string,
logger: Logger,
) {
const { dbg } = logger
;['pb_data', 'pb_migrations', 'pb_public', 'logs', 'pb_hooks'].forEach(
(dir) => {
const path = mkInstanceDataPath(instanceId, dir)
if (!existsSync(path)) {
dbg(`Creating ${path}`)
mkdirSync(path, { recursive: true })
}
},
)
}

View File

@ -1,10 +1,13 @@
export * from '../constants'
export * from './asyncExecutionGuard'
export * from './dir'
export * from './discordAlert'
export * from './env'
export * from './exit'
export * from './internal'
export * from './ioc'
export * from './process'
export * from './Settings'
export * from './smartFetch'
export * from './tryFetch'
export * from './winston'

View File

@ -9,6 +9,7 @@ const format = winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
const final: string[] = []
// @ts-expect-error
;[...message, meta].forEach((m: string) => {
if (typeof m === 'string' && !!m.match(/\n/)) {
final.push(...m.split(/\n/))

View File

@ -1 +1,2 @@
export * from './common'
export * from './core'

View File

@ -1,11 +0,0 @@
const express = require('express')
const app = express()
app.get(`/apix/status`, (req, res) => {
res.json({ status: 'ok' })
})
app.listen(3000, () => {
console.log(`Listening on 3000`)
})

View File

@ -1,657 +0,0 @@
{
"name": "ph_app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ph_app",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@ -1,14 +0,0 @@
{
"name": "ph_app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
}

View File

@ -1,13 +1,20 @@
import * as fs from 'fs'
import { appendFile } from 'fs/promises'
import { Tail } from 'tail'
import { LoggerService, mkInstanceDataPath, stringify } from '../../../core'
import {
ensureInstanceDirectoryStructure,
LoggerService,
mkInstanceDataPath,
stringify,
} from '../..'
type UnsubFunc = () => void
export type InstanceLoggerApi = {
export type InstanceLogWriterApi = {
info: (msg: string) => void
error: (msg: string) => void
}
export type InstanceLogReaderApi = {
tail: (linesBack: number, data: (line: LogEntry) => void) => UnsubFunc
shutdown: () => void
}
@ -18,26 +25,11 @@ export type LogEntry = {
time: string
}
export type InstanceLoggerOptions = {}
export function InstanceLogWriter(instanceId: string, target: string) {
const logger = LoggerService().create(instanceId).breadcrumb({ target })
const { dbg, info, error, warn } = logger
const loggers: {
[key: string]: InstanceLoggerApi
} = {}
export function InstanceLogger(
instanceId: string,
target: string,
options: Partial<InstanceLoggerOptions> = {},
) {
const { dbg, info, error, warn } = LoggerService()
.create(instanceId)
.breadcrumb({ target })
const logDirectory = mkInstanceDataPath(instanceId, `logs`)
if (!fs.existsSync(logDirectory)) {
dbg(`Creating ${logDirectory}`)
fs.mkdirSync(logDirectory, { recursive: true })
}
ensureInstanceDirectoryStructure(instanceId, logger)
const logFile = mkInstanceDataPath(instanceId, `logs`, `${target}.log`)
@ -61,6 +53,18 @@ export function InstanceLogger(
error(msg)
appendLogEntry(msg, 'stderr')
},
}
return api
}
export function InstanceLogReader(instanceId: string, target: string) {
const logger = LoggerService().create(instanceId).breadcrumb({ target })
const { dbg, info, error, warn } = logger
ensureInstanceDirectoryStructure(instanceId, logger)
const api = {
tail: (linesBack: number, data: (line: LogEntry) => void): UnsubFunc => {
const logFile = mkInstanceDataPath(instanceId, `logs`, `${target}.log`)

View File

@ -4,8 +4,10 @@ import { globSync } from 'glob'
import { basename, join } from 'path'
import { AsyncReturnType } from 'type-fest'
import {
APP_URL,
ClientResponseError,
DAEMON_PB_IDLE_TTL,
DOC_URL,
EDGE_APEX_DOMAIN,
INSTANCE_APP_HOOK_DIR,
INSTANCE_APP_MIGRATIONS_DIR,
@ -15,17 +17,15 @@ import {
LoggerService,
SingletonBaseConfig,
asyncExitHook,
mkAppUrl,
mkContainerHomePath,
mkDocUrl,
mkInstanceUrl,
mkSingleton,
now,
stringify,
tryFetch,
} from '../../../core'
} from '../..'
import {
InstanceLogger,
InstanceLogWriter,
MothershipAdminClientService,
PocketbaseService,
SpawnConfig,
@ -71,7 +71,7 @@ export const instanceService = mkSingleton(
`${subdomain}:${id}:${version}`,
)
const { dbg, warn, error, info, trace } = systemInstanceLogger
const userInstanceLogger = InstanceLogger(instance.id, `exec`)
const userInstanceLogger = InstanceLogWriter(instance.id, `exec`)
shutdownManager.push(() => {
dbg(`Shutting down`)
@ -332,7 +332,7 @@ export const instanceService = mkSingleton(
dbg(`Checking for maintenance mode`)
if (instance.maintenance) {
throw new Error(
`This instance is powered off. See ${mkDocUrl(
`This instance is powered off. See ${DOC_URL(
`power`,
)} for more information.`,
)
@ -343,7 +343,7 @@ export const instanceService = mkSingleton(
*/
dbg(`Checking for verified account`)
if (!owner.verified) {
throw new Error(`Log in at ${mkAppUrl()} to verify your account.`)
throw new Error(`Log in at ${APP_URL()} to verify your account.`)
}
const api = await (instanceApis[instance.id] =

View File

@ -8,7 +8,7 @@ import {
PocketBase,
UserFields,
UserId,
} from '../../../core'
} from '../..'
export const mkInstanceCache = (client: PocketBase) => {
const { dbg } = LoggerService().create(`InstanceCache`)

View File

@ -6,7 +6,7 @@ import {
InstanceId,
InstanceStatus,
WithUser,
} from '../../../core'
} from '../..'
export type InstanceApi = ReturnType<typeof createInstanceMixin>

View File

@ -9,7 +9,7 @@ import {
RestMethods,
createRestHelper,
stringify,
} from '../../../core'
} from '../..'
import { createInstanceMixin } from './InstanceMIxin'
export type PocketbaseClientApi = ReturnType<typeof createAdminPbClient>

View File

@ -7,7 +7,7 @@ import {
PocketBase,
mergeConfig,
mkSingleton,
} from '../../../core'
} from '../..'
import { createAdminPbClient } from './createAdminPbClient'
export type ClientServiceConfig = {

View File

@ -15,9 +15,9 @@ import {
mkInstanceDataPath,
mkInternalUrl,
mkSingleton,
} from '../../../core'
} from '../..'
import { GobotService } from '../GobotService'
import { InstanceLogger } from '../InstanceLoggerService'
import { InstanceLogWriter } from '../InstanceLoggerService'
export type Env = { [_: string]: string }
export type SpawnConfig = {
@ -85,7 +85,7 @@ export const createPocketbaseService = async (
} = _cfg
logger.breadcrumb({ subdomain, instanceId })
const iLogger = InstanceLogger(instanceId, 'exec')
const iLogger = InstanceLogWriter(instanceId, 'exec')
const _version = version || maxVersion // If _version is blank, we use the max version available
const realVersion = await bot.maxSatisfyingVersion(_version)

View File

@ -11,7 +11,7 @@ import {
asyncExitHook,
mkSingleton,
seqid,
} from '../../core'
} from '..'
export type ProxyServiceApi = AsyncReturnType<typeof proxyService>
@ -30,79 +30,83 @@ export type ProxyMiddleware = (
export type ProxyServiceConfig = SingletonBaseConfig & {
coreInternalUrl: string
}
export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
const _proxyLogger = LoggerService().create('ProxyService')
const { dbg, error, info, trace, warn } = _proxyLogger
export const proxyService = mkSingleton(
async (
config: ProxyServiceConfig,
): Promise<{ use: ReturnType<typeof express>['use'] }> => {
const _proxyLogger = LoggerService().create('ProxyService')
const { dbg, error, info, trace, warn } = _proxyLogger
const { coreInternalUrl } = config
const { coreInternalUrl } = config
const proxy = httpProxy.createProxyServer({})
proxy.on('error', (err, req, res, target) => {
warn(`Proxy error ${err} on ${req.url} (${req.headers.host})`)
})
const proxy = httpProxy.createProxyServer({})
proxy.on('error', (err, req, res, target) => {
warn(`Proxy error ${err} on ${req.url} (${req.headers.host})`)
})
const server = express()
const server = express()
server.use(cors())
server.use(cors())
server.get('/_api/health', (req, res, next) => {
res.json({ status: 'ok' })
res.end
})
server.get('/_api/health', (req, res, next) => {
res.json({ status: 'ok' })
res.end
})
// Default locals
server.use((req, res, next) => {
const host = req.headers.host
res.locals.requestId = seqid()
res.locals.host = host
res.locals.coreInternalUrl = coreInternalUrl
next()
})
// Default locals
server.use((req, res, next) => {
const host = req.headers.host
res.locals.requestId = seqid()
res.locals.host = host
res.locals.coreInternalUrl = coreInternalUrl
next()
})
// Cloudflare signature
server.use((req, res, next) => {
const url = new URL(`https://${res.locals.host}${req.url}`)
const country = (req.headers['cf-ipcountry'] as string) || '<ct>'
const ip = (req.headers['x-forwarded-for'] as string) || '<ip>'
const method = req.method || '<m>'
const sig = [
res.locals.requestId,
method.padStart(10),
country.padStart(5),
ip.padEnd(45),
url.toString(),
].join(' ')
res.locals.sig = sig
next()
})
// Cloudflare signature
server.use((req, res, next) => {
const url = new URL(`https://${res.locals.host}${req.url}`)
const country = (req.headers['cf-ipcountry'] as string) || '<ct>'
const ip = (req.headers['x-forwarded-for'] as string) || '<ip>'
const method = req.method || '<m>'
const sig = [
res.locals.requestId,
method.padStart(10),
country.padStart(5),
ip.padEnd(45),
url.toString(),
].join(' ')
res.locals.sig = sig
next()
})
server.use((req, res, next) => {
res.locals.proxy = proxy
next()
})
server.use((req, res, next) => {
res.locals.proxy = proxy
next()
})
// Request logging
server.use((req, res, next) => {
if (!res.locals.host) {
throw new Error(`Host not found`)
}
next()
})
// Request logging
server.use((req, res, next) => {
if (!res.locals.host) {
throw new Error(`Host not found`)
}
next()
})
server.use((req, res, next) => {
info(`Incoming request ${res.locals.sig}`)
next()
})
server.use((req, res, next) => {
info(`Incoming request ${res.locals.sig}`)
next()
})
server.listen(DAEMON_PORT(), () => {
info(`daemon listening on port ${DAEMON_PORT()}`)
})
server.listen(DAEMON_PORT(), () => {
info(`daemon listening on port ${DAEMON_PORT()}`)
})
asyncExitHook(async () => {
info(`Shutting down proxy server`)
})
asyncExitHook(async () => {
info(`Shutting down proxy server`)
})
const use = server.use.bind(server)
const use = server.use.bind(server)
return { use }
})
return { use }
},
)

View File

@ -7,8 +7,8 @@ import {
PocketBase,
SingletonBaseConfig,
stringify,
} from '../../core'
import { InstanceLogger } from './InstanceLoggerService'
} from '..'
import { InstanceLogReader } from './InstanceLoggerService'
import { proxyService } from './ProxyService'
export type RealtimeLogConfig = SingletonBaseConfig & {}
@ -69,7 +69,7 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
dbg(`Instance is `, instance)
/** Get a database connection */
const instanceLogger = InstanceLogger(instance.id, `exec`)
const instanceLogger = InstanceLogReader(instance.id, `exec`)
/** Start the stream */
res.writeHead(200, {

1189
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff