pierre-review 0.1.14 → 0.1.16

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.
@@ -1,7 +1,7 @@
1
1
  import { config } from '../../config.js';
2
2
  import { accountToLocalUser } from '../../auth/account.js';
3
3
  import { accountIdOf } from '../plugins/auth.js';
4
- import { dismissMyTurn, getMyTurn } from '../../db/queries.js';
4
+ import { dismissMyTurn, getCompletedDismissals, getMyTurn, undismissMyTurn, } from '../../db/queries.js';
5
5
  const dismissSchema = {
6
6
  body: {
7
7
  type: 'object',
@@ -34,5 +34,13 @@ export async function meRoutes(app) {
34
34
  await dismissMyTurn(accountIdOf(req), kind, refId);
35
35
  return { status: 'ok' };
36
36
  });
37
+ // The "Done" tab: entries dismissed in the past 90 days (review_request + thread).
38
+ app.get('/api/my-turn/done', async (req) => getCompletedDismissals(accountIdOf(req), 90));
39
+ // Un-dismiss: move a completed entry back to the inbox.
40
+ app.post('/api/my-turn/undismiss', { schema: dismissSchema }, async (req) => {
41
+ const { kind, refId } = req.body;
42
+ await undismissMyTurn(accountIdOf(req), kind, refId);
43
+ return { status: 'ok' };
44
+ });
37
45
  }
38
46
  //# sourceMappingURL=me.js.map
@@ -5,6 +5,10 @@ import { upsertRepo } from '../../sync/upsert.js';
5
5
  import { getSyncStatus, isSyncRunning, requestSyncCancel, runSyncForRepo, waitForSyncToStop, } from '../../sync/sync-manager.js';
6
6
  import { deleteRepo, getRepo, getWatchedRepoNodeIds, listRepos, } from '../../db/queries.js';
7
7
  import { accountIdOf } from '../plugins/auth.js';
8
+ // Local copy of the shared MAX_REPOS_PER_ACCOUNT value. `@pierre-review/shared` is
9
+ // a types-only package (not shipped in the published tarball), so the backend must
10
+ // only `import type` from it — runtime constants are duplicated here. Keep in sync.
11
+ const MAX_REPOS_PER_ACCOUNT = 15;
8
12
  const createRepoSchema = {
9
13
  body: {
10
14
  type: 'object',
@@ -131,6 +135,18 @@ export async function repoRoutes(app) {
131
135
  message: `Repository ${owner}/${name} not found or inaccessible. Check the name and your gh auth / SSO.`,
132
136
  };
133
137
  }
138
+ // Enforce the per-account repo cap. Re-adding an already-watched repo is an
139
+ // idempotent no-op (it doesn't grow the count), so only genuinely NEW repos
140
+ // are blocked once at the limit.
141
+ const watched = await getWatchedRepoNodeIds(accountId);
142
+ if (watched.size >= MAX_REPOS_PER_ACCOUNT &&
143
+ !watched.has(resp.repository.id)) {
144
+ reply.status(409);
145
+ return {
146
+ error: 'RepoLimitExceeded',
147
+ message: `You can watch at most ${MAX_REPOS_PER_ACCOUNT} repositories. Remove one to add another.`,
148
+ };
149
+ }
134
150
  const canonOwner = resp.repository.owner.login;
135
151
  const canonName = resp.repository.name;
136
152
  const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId);
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
@@ -0,0 +1,10 @@
1
+ -- PR diff-size metadata (additive). Adds the GraphQL additions/deletions/
2
+ -- changedFiles totals and the per-file breakdown (files JSON) to pull_requests,
3
+ -- powering the PR-detail LOC summary label and the new "Changes" tab. This is
4
+ -- small metadata (not bulky user text), so it's stored in BOTH modes regardless
5
+ -- of lean storage. Existing rows get the 0 defaults / NULL files until the next
6
+ -- sync backfills real values.
7
+ ALTER TABLE `pull_requests` ADD `additions` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
8
+ ALTER TABLE `pull_requests` ADD `deletions` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
9
+ ALTER TABLE `pull_requests` ADD `changed_files` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
10
+ ALTER TABLE `pull_requests` ADD `files` text;
@@ -0,0 +1,7 @@
1
+ -- Claude Review findings are now INCLUDED by default (the UI is opt-OUT via an
2
+ -- "Ignore" button, replacing the old opt-IN "include" checkbox). New findings are
3
+ -- inserted with included=1 (see review/persist.ts); this backfills existing rows so
4
+ -- already-generated reviews show their findings as included rather than ignored.
5
+ -- SQLite-only: Claude Review is force-disabled in cloud, so the Postgres
6
+ -- claude_review_findings table is never populated.
7
+ UPDATE `claude_review_findings` SET `included` = 1 WHERE `included` = 0;
@@ -78,6 +78,20 @@
78
78
  "when": 1780800000001,
79
79
  "tag": "0010_lean_bodies_nullable",
80
80
  "breakpoints": true
81
+ },
82
+ {
83
+ "idx": 11,
84
+ "version": "6",
85
+ "when": 1780800000002,
86
+ "tag": "0011_pr_diff_size",
87
+ "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "6",
92
+ "when": 1780800000003,
93
+ "tag": "0012_findings_included_default",
94
+ "breakpoints": true
81
95
  }
82
96
  ]
83
97
  }
@@ -0,0 +1,4 @@
1
+ ALTER TABLE "pull_requests" ADD COLUMN "additions" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE "pull_requests" ADD COLUMN "deletions" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
3
+ ALTER TABLE "pull_requests" ADD COLUMN "changed_files" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
4
+ ALTER TABLE "pull_requests" ADD COLUMN "files" jsonb;