Package v0.2.12→v0.2.124: manifest, actions, version graph
This commit is contained in:
+258
-4
@@ -1,8 +1,262 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { FileHelper } from '@start9labs/start-sdk'
|
||||
import { Volume } from '@start9labs/start-sdk/package/lib/util/Volume'
|
||||
import { z } from 'zod'
|
||||
|
||||
const mainVolume = new Volume('main')
|
||||
|
||||
// File the relay container reads to learn the URLs of co-installed
|
||||
// services. Written here at install/update time via the StartOS SDK's
|
||||
// service-interface lookup; the container exposes it via
|
||||
// /admin/btcpay/discover so the dashboard can offer one-click setup.
|
||||
const discoveryFile = FileHelper.json(
|
||||
{
|
||||
base: mainVolume,
|
||||
subpath: 'discovered-services.json',
|
||||
},
|
||||
z.object({
|
||||
btcpay: z
|
||||
.object({
|
||||
// LAN URL the operator's browser uses (with port). Used as
|
||||
// the base for the /api-keys/authorize redirect page.
|
||||
browser_url: z.string().nullable(),
|
||||
// Public clearnet URL buyers use to pay. Used by the
|
||||
// relay's rewriteCheckoutUrl to swap the host on BTCPay's
|
||||
// returned checkout link before handing it to a buyer.
|
||||
public_url: z.string().nullable().optional(),
|
||||
// Internal Start9 hostname for container-to-container calls.
|
||||
// Faster than the LAN URL since it stays inside docker.
|
||||
internal_url: z.string().nullable(),
|
||||
// ISO timestamp of last discovery — useful for debugging
|
||||
// stale entries if BTCPay has since been uninstalled.
|
||||
discovered_at: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
// The relay's own clearnet URL — used as the webhook URL we
|
||||
// register with BTCPay. BTCPay's container needs to be able to
|
||||
// resolve this to post InvoiceSettled events; mDNS .local
|
||||
// doesn't resolve inside docker. Clearnet does.
|
||||
self: z
|
||||
.object({
|
||||
public_url: z.string().nullable().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
// Look up BTCPay's service interfaces. Returns three URLs (one may
|
||||
// be null) used for different audiences:
|
||||
// - browser_url: what the OPERATOR opens in their browser for the
|
||||
// authorize flow (mDNS / LAN preferred — same-LAN
|
||||
// flow keeps everything local)
|
||||
// - public_url: what BUYERS open to pay (clearnet preferred —
|
||||
// buyers may be anywhere on the internet, not on
|
||||
// the operator's LAN). Used by rewriteCheckoutUrl
|
||||
// on the BTCPay-returned checkout link.
|
||||
// - internal_url: what the RELAY container uses for daemon-to-
|
||||
// daemon API calls (Start9 docker internal hostname).
|
||||
//
|
||||
// Returns null when BTCPay isn't installed or has no 'ui' interface.
|
||||
//
|
||||
// Picking the right URL is the whole trick. addressInfo.format()
|
||||
// returns EVERY hostname mapped to this binding — clearnet domain,
|
||||
// mDNS `.local`, onion, private IPs, link-local v6, and the LXC
|
||||
// bridge gateway (`10.0.3.1` shape) the relay container sees from
|
||||
// inside docker. Almost all of these are wrong for a browser link
|
||||
// the operator clicks in StartOS dashboard:
|
||||
//
|
||||
// - bridge IP (10.0.3.x) — only resolvable from inside the host
|
||||
// - link-local v6 (fe80::/10) — same problem
|
||||
// - localhost (127.0.0.1, ::1) — useless
|
||||
// - public domain (clearnet) — best, if configured
|
||||
// - mDNS .local — what operators actually use day-to-day
|
||||
// - private DNS — also fine
|
||||
//
|
||||
// Order of preference for the operator-facing browser URL:
|
||||
// 1. mDNS .local — same-LAN browser hop, keeps the auth + callback
|
||||
// flow entirely local. Most operators run BTCPay
|
||||
// and the relay on the same Start9, so mDNS Just
|
||||
// Works without involving any clearnet hop.
|
||||
// 2. private domain (LAN DNS, if the operator runs one)
|
||||
// 3. public clearnet domain — fallback for remote operators
|
||||
// 4. anything non-local that isn't bridge/link-local/localhost
|
||||
async function discoverBtcpay(effects: any): Promise<{
|
||||
browser_url: string | null
|
||||
public_url: string | null
|
||||
internal_url: string | null
|
||||
} | null> {
|
||||
try {
|
||||
const ifaces = await sdk.serviceInterface
|
||||
.getAll(effects, { packageId: 'btcpayserver' })
|
||||
.const()
|
||||
if (!ifaces || ifaces.length === 0) return null
|
||||
const ui = ifaces.find((i: any) => i?.type === 'ui')
|
||||
if (!ui?.addressInfo) return null
|
||||
|
||||
const addr = ui.addressInfo
|
||||
let browserUrl: string | null = null
|
||||
|
||||
// Each .filter() narrows the address set; .format() returns URL
|
||||
// strings. Walk preference tiers and stop at the first that
|
||||
// yields any URL. Wrapped in try/catch each tier because filter
|
||||
// calls can throw on empty matches in some SDK versions.
|
||||
// Walk preference tiers, stop at the first that yields a URL.
|
||||
const firstUrl = (urls: any): string | null => {
|
||||
if (Array.isArray(urls) && urls.length > 0 && urls[0]) return String(urls[0])
|
||||
return null
|
||||
}
|
||||
const browserTries: Array<() => string | null> = [
|
||||
// 1. mDNS .local — same-LAN flow
|
||||
() => firstUrl(addr.filter({ kind: 'mdns' }).format?.()),
|
||||
// 2. Private LAN domain — operator's own LAN DNS, if any
|
||||
() =>
|
||||
firstUrl(addr.filter({ visibility: 'private', kind: 'domain' }).format?.()),
|
||||
// 3. Public clearnet domain — fallback for remote operators
|
||||
() => firstUrl(addr.public?.filter({ kind: 'domain' }).format?.()),
|
||||
// 4. Any non-local address (excludes localhost, link-local, bridge)
|
||||
() =>
|
||||
firstUrl(
|
||||
addr.nonLocal
|
||||
?.filter({
|
||||
exclude: { kind: ['bridge', 'link-local', 'localhost'] },
|
||||
})
|
||||
.format?.(),
|
||||
),
|
||||
]
|
||||
for (const attempt of browserTries) {
|
||||
try {
|
||||
const u = attempt()
|
||||
if (u) {
|
||||
browserUrl = u
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// try next tier
|
||||
}
|
||||
}
|
||||
|
||||
// Public buyer-facing URL — used to rewrite BTCPay's returned
|
||||
// checkout link (`btcpayserver.startos:23000/i/...`) before
|
||||
// handing it to the buyer's browser. Buyer may be on clearnet
|
||||
// (e.g. accessing Recap via a StartTunnel public domain), so we
|
||||
// strongly prefer a clearnet domain here. Falls through to mDNS
|
||||
// / private DNS only when BTCPay isn't exposed on clearnet —
|
||||
// those operators can only sell to LAN buyers anyway.
|
||||
let publicUrl: string | null = null
|
||||
const publicTries: Array<() => string | null> = [
|
||||
// 1. Public clearnet domain — works for any buyer on the internet
|
||||
() => firstUrl(addr.public?.filter({ kind: 'domain' }).format?.()),
|
||||
// 2. Private LAN domain
|
||||
() =>
|
||||
firstUrl(addr.filter({ visibility: 'private', kind: 'domain' }).format?.()),
|
||||
// 3. mDNS .local — same-LAN buyers only
|
||||
() => firstUrl(addr.filter({ kind: 'mdns' }).format?.()),
|
||||
]
|
||||
for (const attempt of publicTries) {
|
||||
try {
|
||||
const u = attempt()
|
||||
if (u) {
|
||||
publicUrl = u
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// try next tier
|
||||
}
|
||||
}
|
||||
|
||||
// Server-to-server URL is the Start9 internal docker hostname.
|
||||
// This works because we declare btcpayserver as a required
|
||||
// `running` dependency in dependencies.ts — Start9 wires up the
|
||||
// DNS link inside our container as part of that declaration.
|
||||
// Port 23000 is BTCPay's standard internal container port (every
|
||||
// Start9 BTCPay install uses this); hardcoded rather than
|
||||
// discovered via the SDK because it's stable across BTCPay
|
||||
// versions and saves us a fragile lookup path.
|
||||
//
|
||||
// This mirrors how Keysat's licensing-service reaches BTCPay on
|
||||
// a co-installed Start9 — same hostname, same port.
|
||||
const internalUrl = 'http://btcpayserver.startos:23000'
|
||||
|
||||
return {
|
||||
browser_url: browserUrl,
|
||||
public_url: publicUrl,
|
||||
internal_url: internalUrl,
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
`[setup] BTCPay discovery skipped: ${err?.message || err}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Discover the relay's OWN clearnet URL by walking our own service
|
||||
// interfaces. Used as the webhook URL we register with BTCPay —
|
||||
// BTCPay's container needs a hostname it can resolve internally
|
||||
// (clearnet via standard DNS works, mDNS doesn't).
|
||||
async function discoverSelfPublicUrl(effects: any): Promise<string | null> {
|
||||
try {
|
||||
const ifaces = await sdk.serviceInterface.getAllOwn(effects).const()
|
||||
if (!ifaces || ifaces.length === 0) return null
|
||||
const ui = ifaces.find((i: any) => i?.type === 'ui') || ifaces[0]
|
||||
if (!ui?.addressInfo) return null
|
||||
const addr = ui.addressInfo
|
||||
const firstUrl = (urls: any): string | null => {
|
||||
if (Array.isArray(urls) && urls.length > 0 && urls[0]) return String(urls[0])
|
||||
return null
|
||||
}
|
||||
// Priority: public clearnet (best for BTCPay container's DNS),
|
||||
// then private domain. mDNS won't resolve from BTCPay's
|
||||
// container so we explicitly skip it.
|
||||
const tries: Array<() => string | null> = [
|
||||
() => firstUrl(addr.public?.filter({ kind: 'domain' }).format?.()),
|
||||
() =>
|
||||
firstUrl(addr.filter({ visibility: 'private', kind: 'domain' }).format?.()),
|
||||
]
|
||||
for (const attempt of tries) {
|
||||
try {
|
||||
const u = attempt()
|
||||
if (u) return u
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (err: any) {
|
||||
console.warn(`[setup] self-discovery skipped: ${err?.message || err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Recap needs no special initialization.
|
||||
// Directories are created by docker_entrypoint.sh and
|
||||
// config is loaded from the persistent volume at runtime.
|
||||
export const setup = sdk.setupOnInit(async (effects, kind) => {
|
||||
// Nothing to do on install, update, restore, or rebuild.
|
||||
// Probe for BTCPay on every install / update. We write the result
|
||||
// every time (even on "no BTCPay found") so the container always
|
||||
// sees a fresh snapshot — never a leftover hit from a previous
|
||||
// version that has since been uninstalled.
|
||||
const btcpay = await discoverBtcpay(effects)
|
||||
const selfPublicUrl = await discoverSelfPublicUrl(effects)
|
||||
await discoveryFile.write(effects, {
|
||||
btcpay: btcpay
|
||||
? {
|
||||
...btcpay,
|
||||
discovered_at: new Date().toISOString(),
|
||||
}
|
||||
: null,
|
||||
self: selfPublicUrl
|
||||
? { public_url: selfPublicUrl }
|
||||
: null,
|
||||
})
|
||||
if (btcpay) {
|
||||
console.info(
|
||||
`[setup] discovered BTCPay at ${btcpay.browser_url || '(no browser URL)'}`,
|
||||
)
|
||||
} else {
|
||||
console.info(
|
||||
`[setup] BTCPay not detected on this Start9 — credit-purchase flow will require a manual URL`,
|
||||
)
|
||||
}
|
||||
console.info(
|
||||
`[setup] self clearnet URL: ${selfPublicUrl || '(none — webhooks will use the operator-facing host header at finalize time, may fail if mDNS)'}`,
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user