DC-053: Public share links + Tailscale-mediated share (Pro-gated)
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

- Share-store: HMAC-signed tokens bound to serviceId+kind, persistent
  signing secret in dataDir/.share-secret, atomic writes, auto-prune
- Routes: admin endpoints gated on licenseManager.isPro() (402 Free);
  public endpoints CSRF-exempt (token IS proof)
- Tailscale path: mints single-use ephemeral pre-auth key, emails
  join link, rolls back share record if createAuthKey throws
- Email-failure path: exposes urlPath for manual delivery fallback
- 53 new tests (24 store + 29 routes), full suite 1372/1372
- Drift-test parser hardened against quoted-word comments
- share-store dataDir resolver handles Proxy/function values

CHANGELOG + BACKLOG updated.
This commit is contained in:
Krystie
2026-07-21 00:45:46 -07:00
parent f0afc4358c
commit d9e61ce1b7
10 changed files with 1588 additions and 4 deletions
+28
View File
@@ -22,6 +22,7 @@ const platformPaths = require('../platform-paths');
const { LicenseManager } = require('./managers/license-manager');
const credentialManager = require('./managers/credential-manager');
const authManager = require('./managers/auth-manager');
const { createShareStore } = require('./security/share-store');
const dockerSecurity = require('./security/docker-security');
const auditLogger = require('./security/audit-logger');
const portLockManager = require('./managers/port-lock-manager');
@@ -58,6 +59,7 @@ const healthRoutes = require('../routes/health');
const monitoringRoutes = require('../routes/monitoring');
const updatesRoutes = require('../routes/updates');
const authRoutes = require('../routes/auth');
const shareRoutes = require('../routes/share');
const configRoutes = require('../routes/config');
const dnsRoutes = require('../routes/dns');
const notificationRoutes = require('../routes/notifications');
@@ -125,6 +127,15 @@ async function createApp() {
const servicesStateManager = new StateManager(config.SERVICES_FILE);
const configStateManager = new StateManager(config.CONFIG_FILE);
// DC-053: share-store. Single shared instance, lazy file creation on first
// write. Lives alongside user-store/invite-store semantics (defensive
// dataDir resolver, atomic JSON writes). Always available — Free tier
// simply blocks creation via the route-level _requirePro gate.
const shareStore = createShareStore({
dataDir: platformPaths.dataDir,
log,
});
// Initialize license manager
const licenseManager = new LicenseManager(credentialManager, config.CONFIG_FILE, console);
licenseManager.loadSecret(config.LICENSE_SECRET_FILE);
@@ -334,6 +345,9 @@ async function createApp() {
servicesStateManager,
configStateManager,
// DC-053: share store + signing secret
shareStore,
// Managers
credentialManager,
authManager,
@@ -491,6 +505,20 @@ async function createApp() {
// Mount route modules
apiRouter.use(authRoutes(ctx));
apiRouter.use(configRoutes(ctx));
// DC-053: share routes (public share links + Tailscale-mediated share).
// Always mounted — Free tier enforcement is at the route level, not the
// mount level, so the API surface is uniform across tiers (operators can
// upgrade without restarting route registration).
apiRouter.use(shareRoutes({
shareStore: ctx.shareStore,
licenseManager: ctx.licenseManager,
tailscaleCoord: ctx.tailscaleCoord,
notificationManager: ctx.notification,
servicesStateManager: ctx.servicesStateManager,
servicesFile: platformPaths.servicesFile,
asyncHandler: ctx.asyncHandler,
log: ctx.log,
}));
apiRouter.use('/dns', dnsRoutes({
dns: ctx.dns,
siteConfig: ctx.siteConfig,