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
@@ -49,8 +49,14 @@ function readPublicRoutes() {
// Extract excludedPaths from csrf-protection.js
function readCsrfExcluded() {
const content = fs.readFileSync(SRC_CSRF, 'utf8');
// Match string literals in arrays inside excludedPaths
const blockMatch = content.match(/excludedPaths\s*=\s*\[([^\]]+)\]/);
// Match string literals in arrays inside excludedPaths.
// The naive `[^\]]+` regex used to work but breaks once any comment line
// between entries contains a quoted word (e.g. "token's TTL") — the
// inner-quote regex then captures the comment text as a fake path.
// Fix: strip line comments (`// ...`) before scanning. Block comments
// don't appear in this file.
const stripped = content.replace(/\/\/[^\n]*/g, '');
const blockMatch = stripped.match(/excludedPaths\s*=\s*\[([^\]]+)\]/);
if (!blockMatch) return new Set();
const entries = [...blockMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
return new Set(entries);
@@ -123,6 +129,7 @@ function readMountedRoutes() {
'routes/config/index.js', // apiRouter.use(configRoutes(ctx)) // bare mount
'routes/themes.js', // apiRouter.use(themesRoutes({...})) // bare mount
'routes/license.js', // apiRouter.use('/license', licenseRoutes({...}))
'routes/share.js', // apiRouter.use(shareRoutes({...})) // bare mount (DC-053)
];
// Prefix map: explicit prefix from src/app.js's apiRouter.use() call
const prefixMap = {
@@ -145,7 +152,27 @@ function readMountedRoutes() {
if (typeof factory !== 'function') continue;
let router;
try {
router = factory(universalDeps);
// Per-mount deps override: factories that need a real implementation
// of a particular dep (not just a noopFn proxy) get one here. Without
// this, DC-053's shareRoutes returns an empty 404 router in the test
// (because universalDeps.shareStore.issuePublic is undefined), and the
// walker never sees the real /share/:token/* paths.
const deps = relPath === 'routes/share.js'
? Object.assign({}, universalDeps, {
shareStore: {
issuePublic: () => ({ ok: true }),
issueTailscale: () => ({ ok: true }),
peek: () => null,
getRaw: () => null,
recordPublicSubscribe: () => ({ ok: true }),
recordTailscaleUse: () => ({ ok: true }),
revoke: () => true,
list: () => [],
listForService: () => [],
},
})
: universalDeps;
router = factory(deps);
} catch (e) { continue; }
// Every direct mount is on apiRouter (which lives at /api/v1) plus an
// optional explicit prefix from src/app.js. Walk with the combined prefix