pierre-review 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -30,6 +30,39 @@ export async function buildApp() {
30
30
  : { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss' } },
31
31
  },
32
32
  });
33
+ // Cloud only: canonicalise the host and pin HTTPS. Local mode runs on
34
+ // http://127.0.0.1, where HSTS would wrongly pin localhost to HTTPS and a www
35
+ // redirect is meaningless — so both are cloud-gated. Registered first so it runs
36
+ // before everything else (the redirect short-circuits before CORS/routing).
37
+ if (config.isCloud) {
38
+ let canonicalHost = '';
39
+ try {
40
+ // hostname (not host) so a configured port never enters the comparison.
41
+ canonicalHost = new URL(config.appBaseUrl).hostname.toLowerCase();
42
+ }
43
+ catch {
44
+ canonicalHost = '';
45
+ }
46
+ app.addHook('onRequest', async (req, reply) => {
47
+ // HSTS: keep browsers on HTTPS for this domain. Honored only over HTTPS
48
+ // (Railway terminates TLS); ignored on plain HTTP. `includeSubDomains` also
49
+ // covers www. No `preload` — it's hard to undo. HSTS_MAX_AGE=0 disables it.
50
+ if (config.hstsMaxAge > 0) {
51
+ reply.header('Strict-Transport-Security', `max-age=${config.hstsMaxAge}; includeSubDomains`);
52
+ }
53
+ // Canonical host: 301 www.<apex> → <apex> so the OAuth round-trip and the
54
+ // session cookie stay on a single origin (and crawlers see one canonical
55
+ // URL). The HSTS header set above still rides on the redirect response.
56
+ if (canonicalHost) {
57
+ // Strip any :port from the Host header before comparing hostnames.
58
+ const host = (req.headers.host ?? '').toLowerCase().split(':')[0] ?? '';
59
+ if (host !== canonicalHost &&
60
+ host.replace(/^www\./, '') === canonicalHost) {
61
+ return reply.redirect(`${config.appBaseUrl}${req.url}`, 301);
62
+ }
63
+ }
64
+ });
65
+ }
33
66
  // CORS: in cloud mode lock to the app's own origin and allow credentials so the
34
67
  // session cookie rides along; local mode reflects any origin (dev convenience).
35
68
  await app.register(cors, {
package/dist/config.js CHANGED
@@ -96,6 +96,11 @@ export const config = {
96
96
  sessionSecret: process.env.SESSION_SECRET ?? '',
97
97
  // 32-byte (64-hex) key for AES-256-GCM encryption of stored access tokens.
98
98
  encryptionKey: process.env.ENCRYPTION_KEY ?? '',
99
+ // HSTS max-age (seconds) for the cloud public origin. Sent only in cloud mode
100
+ // and honored only over HTTPS (Railway terminates TLS). Default 1 year. Set
101
+ // HSTS_MAX_AGE=0 as a kill switch. `preload` is intentionally NOT sent — it is
102
+ // hard to undo; opt in manually once the domain is proven.
103
+ hstsMaxAge: intFromEnv('HSTS_MAX_AGE', 31536000),
99
104
  // ---- Claude Review (agentic PR review; opt-in, LOCAL-ONLY) ----
100
105
  // OFF by default: the feature spends real money / Agent-SDK credits per run.
101
106
  // Enable with ENABLE_CLAUDE_REVIEW=true. FORCE-DISABLED in cloud mode (it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pierre-review",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
5
5
  "type": "module",
6
6
  "author": "Alex Wakeman",