Skip to content

Commit 289ee7b

Browse files
authored
Merge branch 'main' into feature/add-ref-to-search-results
2 parents df90ef4 + 7c127b9 commit 289ee7b

30 files changed

Lines changed: 1387 additions & 757 deletions

.github/workflows/vulnerability-triage.yml

Lines changed: 232 additions & 112 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Added per-step token cost tracking and estimated tool call token usage to Ask Sourcebot chat history. [#1353](https://github.com/sourcebot-dev/sourcebot/pull/1353)
12+
13+
## [5.0.4] - 2026-06-18
14+
15+
### Changed
16+
- Decoupled offline-license anonymous access from the seat cap. [#1349](https://github.com/sourcebot-dev/sourcebot/pull/1349)
17+
18+
### Added
19+
- Recorded service ping history locally and added a "Download usage report" button to the offline license settings page, so offline deployments can export their usage and send it to us. [#1348](https://github.com/sourcebot-dev/sourcebot/pull/1348)
20+
21+
### Fixed
22+
- Upgraded `@grpc/grpc-js` to `^1.14.4`. [#1315](https://github.com/sourcebot-dev/sourcebot/pull/1315)
23+
- Upgraded `vite` to `^8.0.16`. [#1313](https://github.com/sourcebot-dev/sourcebot/pull/1313)
24+
- Upgraded `ws` to `^8.21.0`. [#1324](https://github.com/sourcebot-dev/sourcebot/pull/1324)
25+
- Upgraded `@babel/core` to `^7.29.6`. [#1333](https://github.com/sourcebot-dev/sourcebot/pull/1333)
26+
- Upgraded `markdown-it` to `^14.2.0`. [#1321](https://github.com/sourcebot-dev/sourcebot/pull/1321)
27+
- Upgraded `form-data` to `^4.0.6`. [#1316](https://github.com/sourcebot-dev/sourcebot/pull/1316)
28+
- Upgraded `hono` to `^4.12.25`. [#1322](https://github.com/sourcebot-dev/sourcebot/pull/1322)
29+
- Upgraded `dompurify` to `^3.4.11`. [#1332](https://github.com/sourcebot-dev/sourcebot/pull/1332)
30+
- Upgraded `nodemailer` to `^8.0.11`. [#1328](https://github.com/sourcebot-dev/sourcebot/pull/1328)
31+
- Upgraded `js-yaml` to `^4.2.0`. [#1335](https://github.com/sourcebot-dev/sourcebot/pull/1335)
32+
- Upgraded `protobufjs` to `^7.6.4`. [#1336](https://github.com/sourcebot-dev/sourcebot/pull/1336)
33+
- Upgraded `tar` to `^7.5.16`. [#1338](https://github.com/sourcebot-dev/sourcebot/pull/1338)
34+
- Upgraded `esbuild` to `^0.28.1`. [#1342](https://github.com/sourcebot-dev/sourcebot/pull/1342)
35+
- Enabled Next.js version skew protection to fix "Failed to load chunk" errors during rolling deploys. [#1346](https://github.com/sourcebot-dev/sourcebot/pull/1346)
36+
1037
## [5.0.3] - 2026-06-17
1138

1239
### Changed

docs/api-reference/sourcebot-public.openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.3",
33
"info": {
44
"title": "Sourcebot Public API",
5-
"version": "v5.0.3",
5+
"version": "v5.0.4",
66
"description": "OpenAPI description for the public Sourcebot REST endpoints used for search, repository listing, and file browsing. Authentication is instance-dependent: API keys are the standard integration mechanism, OAuth bearer tokens are EE-only, and some instances may allow anonymous access."
77
},
88
"tags": [

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"teeny-request@npm:^10.0.0": "^10.1.2",
6060
"uuid": "^14.0.0",
6161
"fast-uri@npm:^3.0.1": "^3.1.2",
62-
"shell-quote@npm:1.8.3": "^1.8.4"
62+
"shell-quote@npm:1.8.3": "^1.8.4",
63+
"ws@npm:~8.20.1": "^8.21.0"
6364
}
6465
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- CreateTable
2+
CREATE TABLE "ServicePingEvent" (
3+
"id" TEXT NOT NULL,
4+
"payload" JSONB NOT NULL,
5+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
"orgId" INTEGER NOT NULL,
7+
8+
CONSTRAINT "ServicePingEvent_pkey" PRIMARY KEY ("id")
9+
);
10+
11+
-- AddForeignKey
12+
ALTER TABLE "ServicePingEvent" ADD CONSTRAINT "ServicePingEvent_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;

packages/db/prisma/schema.prisma

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ model Org {
318318
mcpServers McpServer[]
319319
320320
license License?
321+
servicePingEvents ServicePingEvent[]
321322
}
322323

323324
model License {
@@ -358,6 +359,15 @@ model License {
358359
updatedAt DateTime @updatedAt
359360
}
360361

362+
model ServicePingEvent {
363+
id String @id @default(cuid())
364+
payload Json
365+
createdAt DateTime @default(now())
366+
367+
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
368+
orgId Int
369+
}
370+
361371
enum OrgRole {
362372
OWNER
363373
MEMBER

packages/shared/src/entitlements.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ const encodeOfflineKey = (payload: object): string => {
4040
const futureDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toISOString();
4141
const pastDate = new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString();
4242

43-
const validOfflineKey = (overrides: { seats?: number; expiryDate?: string } = {}) =>
43+
const validOfflineKey = (overrides: { seats?: number; anonymousAccess?: boolean; expiryDate?: string } = {}) =>
4444
encodeOfflineKey({
4545
id: 'test-customer',
4646
expiryDate: overrides.expiryDate ?? futureDate,
4747
...(overrides.seats !== undefined ? { seats: overrides.seats } : {}),
48+
...(overrides.anonymousAccess !== undefined ? { anonymousAccess: overrides.anonymousAccess } : {}),
4849
sig: 'fake-sig',
4950
});
5051

@@ -112,23 +113,35 @@ describe('isAnonymousAccessAvailable', () => {
112113
});
113114

114115
describe('with an offline license key', () => {
115-
test('returns false when offline key has a seat count', () => {
116+
test('returns false when offline key does not grant anonymous access', () => {
116117
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ seats: 100 });
117118
expect(isAnonymousAccessAvailable(null)).toBe(false);
118119
});
119120

120-
test('returns true when offline key has no seat count (unlimited)', () => {
121+
test('returns false when offline key is uncapped but does not grant anonymous access', () => {
122+
// Uncapped (no seats) no longer implies anonymous access — it must
123+
// be granted explicitly via the `anonymousAccess` flag.
121124
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey();
125+
expect(isAnonymousAccessAvailable(null)).toBe(false);
126+
});
127+
128+
test('returns true when offline key explicitly grants anonymous access', () => {
129+
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ anonymousAccess: true });
122130
expect(isAnonymousAccessAvailable(null)).toBe(true);
123131
});
124132

125-
test('unlimited offline key beats an active online license', () => {
126-
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey();
133+
test('anonymous access is independent of the seat cap', () => {
134+
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ seats: 100, anonymousAccess: true });
135+
expect(isAnonymousAccessAvailable(null)).toBe(true);
136+
});
137+
138+
test('anonymous-access offline key beats an active online license', () => {
139+
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ anonymousAccess: true });
127140
expect(isAnonymousAccessAvailable(makeLicense({ status: 'active' }))).toBe(true);
128141
});
129142

130143
test('falls through to online license check when offline key is expired', () => {
131-
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ seats: 100, expiryDate: pastDate });
144+
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ anonymousAccess: true, expiryDate: pastDate });
132145
expect(isAnonymousAccessAvailable(null)).toBe(true);
133146
expect(isAnonymousAccessAvailable(makeLicense({ status: 'active' }))).toBe(false);
134147
});
@@ -144,7 +157,7 @@ describe('isAnonymousAccessAvailable', () => {
144157
});
145158

146159
test('falls through when offline key signature is invalid', () => {
147-
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ seats: 100 });
160+
mocks.env.SOURCEBOT_EE_LICENSE_KEY = validOfflineKey({ anonymousAccess: true });
148161
mocks.verifySignature.mockReturnValue(false);
149162
expect(isAnonymousAccessAvailable(null)).toBe(true);
150163
});

packages/shared/src/entitlements.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const offlineLicensePrefix = "sourcebot_ee_";
1212
const offlineLicensePayloadSchema = z.object({
1313
id: z.string(),
1414
seats: z.number().optional(),
15+
// Whether anonymous (unauthenticated) access is permitted.
16+
anonymousAccess: z.boolean().optional(),
1517
// ISO 8601 date string
1618
expiryDate: z.string().datetime(),
1719
sig: z.string(),
@@ -50,7 +52,13 @@ const decodeOfflineLicenseKeyPayload = (payload: string): getValidOfflineLicense
5052
const payloadJson = JSON.parse(decodedPayload);
5153
const licenseData = offlineLicensePayloadSchema.parse(payloadJson);
5254

55+
// Keys are listed alphabetically to match the canonical JSON the
56+
// signer produces (Python `json.dumps(..., sort_keys=True)`).
57+
// `JSON.stringify` drops `undefined` values, so omitted optional
58+
// fields (e.g. a legacy key without `anonymousAccess`) verify exactly
59+
// as they were originally signed.
5360
const dataToVerify = JSON.stringify({
61+
anonymousAccess: licenseData.anonymousAccess,
5462
expiryDate: licenseData.expiryDate,
5563
id: licenseData.id,
5664
seats: licenseData.seats
@@ -120,17 +128,25 @@ const getValidOnlineLicense = (_license: License | null): License | null => {
120128
return null;
121129
}
122130

131+
export const isValidOfflineLicenseActive = (): boolean => {
132+
return getValidOfflineLicense() !== null;
133+
}
134+
135+
export const isValidOnlineLicenseActive = (_license: License | null): boolean => {
136+
return getValidOnlineLicense(_license) !== null;
137+
}
138+
123139
export const isValidLicenseActive = (_license: License | null): boolean => {
124140
return (
125-
getValidOfflineLicense() !== null ||
126-
getValidOnlineLicense(_license) !== null
141+
isValidOfflineLicenseActive() ||
142+
isValidOnlineLicenseActive(_license)
127143
);
128144
}
129145

130146
export const isAnonymousAccessAvailable = (_license: License | null): boolean => {
131147
const offlineKey = getValidOfflineLicense();
132148
if (offlineKey) {
133-
return offlineKey.seats === undefined;
149+
return offlineKey.anonymousAccess === true;
134150
}
135151

136152
const onlineLicense = getValidOnlineLicense(_license);
@@ -163,6 +179,7 @@ export const hasEntitlement = (entitlement: Entitlement, _license: License | nul
163179
export type OfflineLicenseMetadata = {
164180
id: string;
165181
seats?: number;
182+
anonymousAccess?: boolean;
166183
expiryDate: string;
167184
}
168185

@@ -178,6 +195,7 @@ export const getOfflineLicenseMetadata = (): OfflineLicenseMetadata | null => {
178195
return {
179196
id: license.id,
180197
seats: license.seats,
198+
anonymousAccess: license.anonymousAccess,
181199
expiryDate: license.expiryDate,
182200
};
183201
}

packages/shared/src/index.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export {
66
getEntitlements as _getEntitlements,
77
isAnonymousAccessAvailable as _isAnonymousAccessAvailable,
88
isValidLicenseActive as _isValidLicenseActive,
9+
isValidOfflineLicenseActive,
10+
isValidOnlineLicenseActive as _isValidOnlineLicenseActive,
911
getSeatCap,
1012
getOfflineLicenseMetadata,
1113
STALE_ONLINE_LICENSE_THRESHOLD_MS,

packages/shared/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// This file is auto-generated by .github/workflows/release-sourcebot.yml
2-
export const SOURCEBOT_VERSION = "v5.0.3";
2+
export const SOURCEBOT_VERSION = "v5.0.4";

0 commit comments

Comments
 (0)