pierre-review 0.1.57 → 0.1.58
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/api/plugins/auth.js +23 -4
- package/dist/api/routes/billing.js +204 -0
- package/dist/api/routes/me.js +4 -2
- package/dist/app.js +4 -0
- package/dist/auth/account.js +24 -0
- package/dist/coding/agent.js +1 -0
- package/dist/config.js +10 -2
- package/dist/db/migrations/0025_account_billing.sql +7 -0
- package/dist/db/migrations/meta/_journal.json +7 -0
- package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
- package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
- package/dist/db/migrations-pg/meta/_journal.json +7 -0
- package/dist/db/schema.pg.js +5 -1
- package/dist/db/schema.sqlite.js +8 -1
- package/dist/pro/contract.js +16 -2
- package/dist/review/agent.js +1 -0
- package/dist/review/pricing.js +3 -1
- package/package.json +2 -6
- package/public/assets/{index-BdkrDPhY.js → index-ChVJlk1B.js} +82 -82
- package/public/index.html +1 -1
- package/public-landing/assets/index-BPqLHHsO.css +1 -0
- package/public-landing/assets/index-tnZ3hmr4.js +40 -0
- package/public-landing/duel.svg +206 -0
- package/public-landing/index.html +39 -10
- package/public-landing/og-image.png +0 -0
- package/public-landing/shots/activity-feed-pro.png +0 -0
- package/public-landing/shots/activity-feed.png +0 -0
- package/public-landing/shots/ai-fix.png +0 -0
- package/public-landing/shots/claude-review.png +0 -0
- package/public-landing/shots/flow-fix-1-ci.png +0 -0
- package/public-landing/shots/flow-fix-2-analysis.png +0 -0
- package/public-landing/shots/flow-fix-3-diff.png +0 -0
- package/public-landing/shots/flow-fix-4-push.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/flow-review-1-run.png +0 -0
- package/public-landing/shots/flow-review-2-memory.png +0 -0
- package/public-landing/shots/flow-review-3-findings.png +0 -0
- package/public-landing/shots/flow-review-4-post.png +0 -0
- package/public-landing/shots/insights.png +0 -0
- package/public-landing/shots/open-pr-strip.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/repo-console-free.png +0 -0
- package/public-landing/shots/repo-console.png +0 -0
- package/public-landing/shots/settings.png +0 -0
- package/public-landing/shots/sprint-report.png +0 -0
- package/public-landing/shots/timeline.png +0 -0
- package/public-landing/sitemap.xml +8 -8
- package/public-landing/assets/index-DntgTImq.js +0 -40
- package/public-landing/assets/index-myqL3hQ2.css +0 -1
- package/public-landing/shots/analytics.png +0 -0
- package/public-landing/shots/feed.png +0 -0
- package/public-landing/shots/focus-mode.png +0 -0
- package/public-landing/shots/insights-panel.png +0 -0
- package/public-landing/shots/my-turn.png +0 -0
package/dist/api/plugins/auth.js
CHANGED
|
@@ -30,6 +30,8 @@ export function registerAccountContext(app) {
|
|
|
30
30
|
displayName: null,
|
|
31
31
|
avatarUrl: null,
|
|
32
32
|
isLocal: true,
|
|
33
|
+
plan: 'free',
|
|
34
|
+
stripeCustomerId: null,
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
});
|
|
@@ -65,10 +67,14 @@ export async function registerSession(app) {
|
|
|
65
67
|
},
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
|
-
// Cloud only: 401 any unauthenticated /api data route. Skips /api/health
|
|
69
|
-
// /api/auth/* (sign-in itself)
|
|
70
|
-
//
|
|
71
|
-
//
|
|
70
|
+
// Cloud only: 401 any unauthenticated /api data route. Skips /api/health,
|
|
71
|
+
// /api/auth/* (sign-in itself), /api/billing/webhook (Stripe posts
|
|
72
|
+
// unauthenticated — verified by signature instead) and all non-/api requests
|
|
73
|
+
// (the SPA + landing are served openly; the frontend gate handles the signed-out
|
|
74
|
+
// UI). MUST be registered AFTER registerAccountContext so req.account is already
|
|
75
|
+
// resolved. Also the cloud-only ENTITLEMENT gate: an authenticated free-plan
|
|
76
|
+
// account gets 402 on the Pro plugin's /api/pro/* routes (local accounts are
|
|
77
|
+
// always entitled — isLocal never reaches the cloud gate anyway).
|
|
72
78
|
export function registerAuthGate(app) {
|
|
73
79
|
app.addHook('onRequest', async (req, reply) => {
|
|
74
80
|
const path = req.url.split('?')[0] ?? req.url;
|
|
@@ -78,11 +84,24 @@ export function registerAuthGate(app) {
|
|
|
78
84
|
return;
|
|
79
85
|
if (path.startsWith('/api/auth/'))
|
|
80
86
|
return;
|
|
87
|
+
if (path === '/api/billing/webhook')
|
|
88
|
+
return;
|
|
89
|
+
// The pricing page's "Get Pro" is a plain browser navigation from the public
|
|
90
|
+
// landing; the route itself bounces anonymous visitors to sign-in instead of
|
|
91
|
+
// dead-ending them on a JSON 401.
|
|
92
|
+
if (path === '/api/billing/checkout')
|
|
93
|
+
return;
|
|
81
94
|
if (!req.account) {
|
|
82
95
|
await reply.code(401).send({
|
|
83
96
|
error: 'Unauthorized',
|
|
84
97
|
message: 'Sign in with GitHub to continue.',
|
|
85
98
|
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (path.startsWith('/api/pro/') &&
|
|
102
|
+
!req.account.isLocal &&
|
|
103
|
+
req.account.plan === 'free') {
|
|
104
|
+
await reply.code(402).send({ error: 'pro required' });
|
|
86
105
|
}
|
|
87
106
|
});
|
|
88
107
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Stripe billing seam ("carve the path"): a checkout redirect + the webhook that
|
|
2
|
+
// flips `accounts.plan`. Deliberately dependency-free — signature verification is
|
|
3
|
+
// hand-rolled with node:crypto (no stripe SDK), and the whole feature is inert
|
|
4
|
+
// until STRIPE_PAYMENT_LINK_URL / STRIPE_WEBHOOK_SECRET are set (the webhook
|
|
5
|
+
// replies 501 unconfigured). Registered unconditionally in app.ts, both modes.
|
|
6
|
+
//
|
|
7
|
+
// Entitlement flows from `accounts.plan`: /api/me intersects the capability
|
|
8
|
+
// singleton with the plan (pro/contract.ts entitledProCapabilities) and the cloud
|
|
9
|
+
// auth gate 402s free-plan /api/pro/* requests (api/plugins/auth.ts). Local
|
|
10
|
+
// accounts are always fully entitled — this file never affects local behavior.
|
|
11
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { config } from '../../config.js';
|
|
13
|
+
import { getAccountById, getAccountByStripeCustomerId, setAccountPlan, } from '../../auth/account.js';
|
|
14
|
+
// Stripe's default signature tolerance: reject events older/newer than 5 minutes
|
|
15
|
+
// (replay protection).
|
|
16
|
+
export const STRIPE_SIGNATURE_TOLERANCE_SEC = 300;
|
|
17
|
+
/**
|
|
18
|
+
* Verify a Stripe webhook signature (the `stripe-signature` header) without the
|
|
19
|
+
* stripe SDK. Header format: `t=<unix-seconds>,v1=<hex>[,v1=<hex>…]` (multiple
|
|
20
|
+
* v1 entries appear during secret rotation). The signed payload is
|
|
21
|
+
* `<t>.<rawBody>`; the signature is HMAC-SHA256(secret) over it, hex-encoded.
|
|
22
|
+
* Compared timing-safely against every v1; the timestamp must be within
|
|
23
|
+
* STRIPE_SIGNATURE_TOLERANCE_SEC of `nowMs`.
|
|
24
|
+
*/
|
|
25
|
+
export function verifyStripeSignature(rawBody, header, secret, nowMs = Date.now()) {
|
|
26
|
+
let ts = null;
|
|
27
|
+
const v1s = [];
|
|
28
|
+
for (const part of header.split(',')) {
|
|
29
|
+
const idx = part.indexOf('=');
|
|
30
|
+
if (idx < 0)
|
|
31
|
+
continue;
|
|
32
|
+
const key = part.slice(0, idx).trim();
|
|
33
|
+
const value = part.slice(idx + 1).trim();
|
|
34
|
+
if (!value)
|
|
35
|
+
continue;
|
|
36
|
+
if (key === 't') {
|
|
37
|
+
const n = Number.parseInt(value, 10);
|
|
38
|
+
if (Number.isFinite(n))
|
|
39
|
+
ts = n;
|
|
40
|
+
}
|
|
41
|
+
else if (key === 'v1') {
|
|
42
|
+
v1s.push(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (ts == null || v1s.length === 0)
|
|
46
|
+
return false;
|
|
47
|
+
if (Math.abs(nowMs / 1000 - ts) > STRIPE_SIGNATURE_TOLERANCE_SEC)
|
|
48
|
+
return false;
|
|
49
|
+
const expected = createHmac('sha256', secret)
|
|
50
|
+
.update(`${ts}.`)
|
|
51
|
+
.update(rawBody)
|
|
52
|
+
.digest('hex');
|
|
53
|
+
const expectedBuf = Buffer.from(expected, 'utf-8');
|
|
54
|
+
return v1s.some((sig) => {
|
|
55
|
+
const sigBuf = Buffer.from(sig, 'utf-8');
|
|
56
|
+
return (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Stripe `customer` fields are a string id or an expanded object with `.id`.
|
|
60
|
+
function customerIdOf(value) {
|
|
61
|
+
if (typeof value === 'string' && value !== '')
|
|
62
|
+
return value;
|
|
63
|
+
if (value != null &&
|
|
64
|
+
typeof value === 'object' &&
|
|
65
|
+
typeof value.id === 'string') {
|
|
66
|
+
return value.id;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** Map a parsed Stripe event to the plan action it implies. Pure. */
|
|
71
|
+
export function stripeEventAction(event) {
|
|
72
|
+
if (event == null || typeof event !== 'object')
|
|
73
|
+
return { kind: 'ignore' };
|
|
74
|
+
const { type, data } = event;
|
|
75
|
+
if (typeof type !== 'string')
|
|
76
|
+
return { kind: 'ignore' };
|
|
77
|
+
const obj = (data?.object ?? null);
|
|
78
|
+
if (obj == null || typeof obj !== 'object')
|
|
79
|
+
return { kind: 'ignore' };
|
|
80
|
+
if (type === 'checkout.session.completed' ||
|
|
81
|
+
type === 'checkout.session.async_payment_succeeded') {
|
|
82
|
+
// Delayed-notification methods (SEPA, ACH…) fire `completed` with
|
|
83
|
+
// payment_status 'unpaid' and confirm later via async_payment_succeeded —
|
|
84
|
+
// only grant once the money actually landed. `no_payment_required` covers
|
|
85
|
+
// trials.
|
|
86
|
+
const paid = obj['payment_status'] === 'paid' ||
|
|
87
|
+
obj['payment_status'] === 'no_payment_required';
|
|
88
|
+
if (!paid)
|
|
89
|
+
return { kind: 'ignore' };
|
|
90
|
+
const ref = obj['client_reference_id'];
|
|
91
|
+
const accountId = typeof ref === 'string' && /^\d+$/.test(ref)
|
|
92
|
+
? Number.parseInt(ref, 10)
|
|
93
|
+
: typeof ref === 'number' && Number.isInteger(ref)
|
|
94
|
+
? ref
|
|
95
|
+
: null;
|
|
96
|
+
return {
|
|
97
|
+
kind: 'checkout_completed',
|
|
98
|
+
accountId,
|
|
99
|
+
stripeCustomerId: customerIdOf(obj['customer']),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (type === 'customer.subscription.updated' ||
|
|
103
|
+
type === 'customer.subscription.deleted') {
|
|
104
|
+
const status = obj['status'];
|
|
105
|
+
const active = type !== 'customer.subscription.deleted' &&
|
|
106
|
+
(status === 'active' || status === 'trialing');
|
|
107
|
+
return {
|
|
108
|
+
kind: 'subscription_status',
|
|
109
|
+
customerId: customerIdOf(obj['customer']),
|
|
110
|
+
plan: active ? 'pro' : 'free',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { kind: 'ignore' };
|
|
114
|
+
}
|
|
115
|
+
export async function billingRoutes(app) {
|
|
116
|
+
// Redirect the signed-in user to the Stripe Payment Link, tagging the checkout
|
|
117
|
+
// with their account id (client_reference_id rides the Payment Link URL and
|
|
118
|
+
// comes back on checkout.session.completed). Exempted from the cloud auth gate
|
|
119
|
+
// (it's a plain browser navigation from the public pricing page), so it must
|
|
120
|
+
// handle anonymous visitors itself: bounce them to sign-in rather than a 401.
|
|
121
|
+
app.get('/api/billing/checkout', async (req, reply) => {
|
|
122
|
+
if (!config.stripePaymentLinkUrl) {
|
|
123
|
+
return reply.redirect('/pricing?checkout=unavailable', 302);
|
|
124
|
+
}
|
|
125
|
+
if (!req.account) {
|
|
126
|
+
return reply.redirect('/api/auth/login', 302);
|
|
127
|
+
}
|
|
128
|
+
const url = new URL(config.stripePaymentLinkUrl);
|
|
129
|
+
url.searchParams.set('client_reference_id', String(req.account.id));
|
|
130
|
+
return reply.redirect(url.toString(), 302);
|
|
131
|
+
});
|
|
132
|
+
// The webhook needs the RAW request body (the signature signs the exact bytes).
|
|
133
|
+
// Fastify content-type parsers are ENCAPSULATED, so registering the buffer
|
|
134
|
+
// parser inside this nested plugin scope affects ONLY the routes declared here
|
|
135
|
+
// — the rest of the API keeps normal JSON parsing (proven by billing.test.ts).
|
|
136
|
+
await app.register(async (scope) => {
|
|
137
|
+
scope.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
|
|
138
|
+
done(null, body);
|
|
139
|
+
});
|
|
140
|
+
// Stripe posts unauthenticated (exempted from the auth gate); authenticity
|
|
141
|
+
// comes from the signature. Never 500 on an unknown account — Stripe retries
|
|
142
|
+
// on non-2xx, and an unknown account is a data mismatch, not a server fault.
|
|
143
|
+
scope.post('/api/billing/webhook', async (req, reply) => {
|
|
144
|
+
if (!config.stripeWebhookSecret) {
|
|
145
|
+
return reply.code(501).send({ error: 'billing not configured' });
|
|
146
|
+
}
|
|
147
|
+
const raw = req.body;
|
|
148
|
+
if (!Buffer.isBuffer(raw)) {
|
|
149
|
+
return reply.code(400).send({ error: 'expected a JSON body' });
|
|
150
|
+
}
|
|
151
|
+
const sig = req.headers['stripe-signature'];
|
|
152
|
+
if (typeof sig !== 'string' ||
|
|
153
|
+
!verifyStripeSignature(raw, sig, config.stripeWebhookSecret)) {
|
|
154
|
+
return reply.code(400).send({ error: 'invalid signature' });
|
|
155
|
+
}
|
|
156
|
+
let event;
|
|
157
|
+
try {
|
|
158
|
+
event = JSON.parse(raw.toString('utf-8'));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return reply.code(400).send({ error: 'invalid JSON' });
|
|
162
|
+
}
|
|
163
|
+
const action = stripeEventAction(event);
|
|
164
|
+
if (action.kind === 'checkout_completed') {
|
|
165
|
+
const account = action.accountId != null ? await getAccountById(action.accountId) : null;
|
|
166
|
+
if (account) {
|
|
167
|
+
// client_reference_id is attacker-influencable (it rides the public
|
|
168
|
+
// Payment Link URL), so never let a new checkout re-bind an account
|
|
169
|
+
// that already has a DIFFERENT Stripe customer — that would orphan
|
|
170
|
+
// the original subscription's webhooks.
|
|
171
|
+
const rebind = account.stripeCustomerId != null &&
|
|
172
|
+
action.stripeCustomerId != null &&
|
|
173
|
+
account.stripeCustomerId !== action.stripeCustomerId;
|
|
174
|
+
await setAccountPlan(account.id, 'pro', rebind ? undefined : action.stripeCustomerId);
|
|
175
|
+
if (rebind) {
|
|
176
|
+
req.log.warn({
|
|
177
|
+
accountId: account.id,
|
|
178
|
+
existingCustomerId: account.stripeCustomerId,
|
|
179
|
+
newCustomerId: action.stripeCustomerId,
|
|
180
|
+
}, 'stripe checkout for an account already bound to a different customer — kept the existing binding');
|
|
181
|
+
}
|
|
182
|
+
req.log.info({ accountId: account.id }, 'stripe checkout completed — plan set to pro');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
req.log.warn({ clientReferenceId: action.accountId }, 'stripe checkout completed for unknown account — ignored');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (action.kind === 'subscription_status') {
|
|
189
|
+
const account = action.customerId
|
|
190
|
+
? await getAccountByStripeCustomerId(action.customerId)
|
|
191
|
+
: null;
|
|
192
|
+
if (account) {
|
|
193
|
+
await setAccountPlan(account.id, action.plan);
|
|
194
|
+
req.log.info({ accountId: account.id, plan: action.plan }, 'stripe subscription status — plan updated');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
req.log.warn({ customerId: action.customerId }, 'stripe subscription event for unknown customer — ignored');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { received: true };
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=billing.js.map
|
package/dist/api/routes/me.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { EMPTY_CAPABILITIES, entitledProCapabilities, } from '../../pro/contract.js';
|
|
5
5
|
import { getFyiProvider } from '../../feed/fyi-provider.js';
|
|
6
6
|
import { dismissMyTurn, getCompletedDismissals, getFeedLastSeenAt, getMyTurn, markFeedSeen, undismissMyTurn, } from '../../db/queries.js';
|
|
7
7
|
const dismissSchema = {
|
|
@@ -44,7 +44,9 @@ export async function meRoutes(app) {
|
|
|
44
44
|
newFeedItems,
|
|
45
45
|
// Claude Review is now the Pro `claudeReview` capability (in `pro` below).
|
|
46
46
|
deploymentMode: config.deploymentMode,
|
|
47
|
-
|
|
47
|
+
// Per-account entitlement: local = full capabilities (unchanged); cloud =
|
|
48
|
+
// full only when the account's plan isn't 'free' (Stripe billing seam).
|
|
49
|
+
pro: req.account ? entitledProCapabilities(req.account) : EMPTY_CAPABILITIES,
|
|
48
50
|
};
|
|
49
51
|
});
|
|
50
52
|
app.get('/api/my-turn', async (req) => getMyTurn(accountIdOf(req)));
|
package/dist/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { feedRoutes } from './api/routes/feed.js';
|
|
|
19
19
|
import { mergersRoutes } from './api/routes/mergers.js';
|
|
20
20
|
import { insightsRoutes } from './api/routes/insights.js';
|
|
21
21
|
import { activityRoutes } from './api/routes/activity.js';
|
|
22
|
+
import { billingRoutes } from './api/routes/billing.js';
|
|
22
23
|
export async function buildApp() {
|
|
23
24
|
const app = Fastify({
|
|
24
25
|
// In production (the installed CLI) the per-request "incoming request" /
|
|
@@ -131,6 +132,9 @@ export async function buildApp() {
|
|
|
131
132
|
await app.register(mergersRoutes);
|
|
132
133
|
await app.register(insightsRoutes);
|
|
133
134
|
await app.register(activityRoutes);
|
|
135
|
+
// Stripe billing seam (checkout redirect + webhook). Registered in both modes;
|
|
136
|
+
// inert until the STRIPE_* env vars are set (webhook 501s unconfigured).
|
|
137
|
+
await app.register(billingRoutes);
|
|
134
138
|
// Claude Review moved into the @pierre/pro plugin (its routes register there, gated on the
|
|
135
139
|
// `claudeReview` capability). The SDK-run / diff-prep / GitHub-post infra + the tables stay
|
|
136
140
|
// in core behind the ctx.review seam; nothing to register here.
|
package/dist/auth/account.js
CHANGED
|
@@ -32,6 +32,8 @@ function rowToAccount(row) {
|
|
|
32
32
|
displayName: row.displayName,
|
|
33
33
|
avatarUrl: row.avatarUrl,
|
|
34
34
|
isLocal: row.isLocal,
|
|
35
|
+
plan: row.plan === 'pro' ? 'pro' : 'free',
|
|
36
|
+
stripeCustomerId: row.stripeCustomerId,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
// Module cache of the local account, set by ensureLocalAccount() at startup so
|
|
@@ -178,6 +180,28 @@ export async function getAccountById(id) {
|
|
|
178
180
|
.execute();
|
|
179
181
|
return rows[0] ? rowToAccount(rows[0]) : null;
|
|
180
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Set an account's billing plan (and, when known, its Stripe customer id).
|
|
185
|
+
* Called only by the Stripe webhook handler (api/routes/billing.ts).
|
|
186
|
+
*/
|
|
187
|
+
export async function setAccountPlan(accountId, plan, stripeCustomerId) {
|
|
188
|
+
const { accounts } = schema;
|
|
189
|
+
const set = { plan };
|
|
190
|
+
if (stripeCustomerId != null)
|
|
191
|
+
set.stripeCustomerId = stripeCustomerId;
|
|
192
|
+
await db.update(accounts).set(set).where(eq(accounts.id, accountId)).execute();
|
|
193
|
+
}
|
|
194
|
+
/** Resolve an account by its Stripe customer id (subscription webhooks). */
|
|
195
|
+
export async function getAccountByStripeCustomerId(customerId) {
|
|
196
|
+
const { accounts } = schema;
|
|
197
|
+
const rows = await db
|
|
198
|
+
.select()
|
|
199
|
+
.from(accounts)
|
|
200
|
+
.where(eq(accounts.stripeCustomerId, customerId))
|
|
201
|
+
.limit(1)
|
|
202
|
+
.execute();
|
|
203
|
+
return rows[0] ? rowToAccount(rows[0]) : null;
|
|
204
|
+
}
|
|
181
205
|
/**
|
|
182
206
|
* Resolve an account's owner to a row in `users` (by login) if they've appeared
|
|
183
207
|
* in any synced repo. Returns null when "you" haven't authored/acted anywhere
|
package/dist/coding/agent.js
CHANGED
|
@@ -44,6 +44,7 @@ const RESOLVE_TOOLS = [
|
|
|
44
44
|
const RESOLVE_DISALLOWED_TOOLS = ['Bash', 'NotebookEdit'];
|
|
45
45
|
// Models that accept the `effort` option (Haiku 4.5 rejects it — the API 400s).
|
|
46
46
|
const EFFORT_CAPABLE_MODELS = new Set([
|
|
47
|
+
'claude-sonnet-5',
|
|
47
48
|
'claude-opus-4-8',
|
|
48
49
|
'claude-sonnet-4-6',
|
|
49
50
|
]);
|
package/dist/config.js
CHANGED
|
@@ -128,9 +128,17 @@ export const config = {
|
|
|
128
128
|
// HSTS_MAX_AGE=0 as a kill switch. `preload` is intentionally NOT sent — it is
|
|
129
129
|
// hard to undo; opt in manually once the domain is proven.
|
|
130
130
|
hstsMaxAge: intFromEnv('HSTS_MAX_AGE', 31536000),
|
|
131
|
+
// ---- Stripe billing (optional; NOT required by assertCloudConfig) ----
|
|
132
|
+
// Payment Link URL for the Pro plan — GET /api/billing/checkout 302s to it with
|
|
133
|
+
// client_reference_id=<accountId> appended. Empty = checkout unavailable.
|
|
134
|
+
stripePaymentLinkUrl: process.env.STRIPE_PAYMENT_LINK_URL ?? '',
|
|
135
|
+
// Webhook signing secret (whsec_…) for POST /api/billing/webhook. Empty = the
|
|
136
|
+
// webhook replies 501 and no billing state ever changes.
|
|
137
|
+
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? '',
|
|
131
138
|
// Pro plugin master gate. Pro is local-only for now; bind.ts skips the dynamic
|
|
132
|
-
// import entirely when false.
|
|
133
|
-
|
|
139
|
+
// import entirely when false. PRO_DISABLED=true forces pure-OSS mode even when
|
|
140
|
+
// the submodule is checked out (used to exercise/capture the free tier).
|
|
141
|
+
proEnabled: !isCloud && process.env.PRO_DISABLED !== 'true',
|
|
134
142
|
// Per-repo digest (Pro, Workstream 2) config — consumed by @pierre/pro, kept in
|
|
135
143
|
// core so the model id / budgets live in one place and aren't hardcoded at call
|
|
136
144
|
// sites. Inert until the plugin is present AND digestEnabled.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Stripe billing seam (additive). `plan` ('free' | 'pro', default 'free') is set ONLY by
|
|
2
|
+
-- the Stripe webhook (api/routes/billing.ts) — the OAuth upsert never touches it, so a
|
|
3
|
+
-- re-login can't reset a paid plan. `stripe_customer_id` (cus_…) is captured from
|
|
4
|
+
-- checkout.session.completed so subscription webhooks can resolve the account.
|
|
5
|
+
-- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
|
|
6
|
+
ALTER TABLE `accounts` ADD `plan` text NOT NULL DEFAULT 'free';--> statement-breakpoint
|
|
7
|
+
ALTER TABLE `accounts` ADD `stripe_customer_id` text;
|