Skip to content

Commit be15e81

Browse files
committed
Release v0.2.58
1 parent c77635d commit be15e81

118 files changed

Lines changed: 7241 additions & 1027 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎RELEASE.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
2-
"version": "0.2.57",
2+
"version": "0.2.58",
33
"license": "FSL-1.1-ALv2",
44
"baselineSha256": "7847f65a9b20b366a60d52d19ca3cd6bdd1bb09b8cb506659a270f27a21d8877",
55
"e2b": {
66
"recipeSha256": "761809ee2efa2b6c750451ace29283cbb8e55b6ce3b5b14c2bb4509df71df801",
77
"profileSha256": "414dbecee2d47de36016ea05c736e2edff3a0deb298babf923887ea184a01c10"
88
},
99
"dxd": {
10-
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.57/dxd-linux-x64",
11-
"sha256": "5102e95a5f9535d897d463249d448cf5a5fa5133f5dfd7acd78410e0c24c7e0b"
10+
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.58/dxd-linux-x64",
11+
"sha256": "2e6a8886fce9b3a1d382105db47001ee6acdb9f546a79494e44ef44f24a35477"
1212
},
1313
"toolchain": {
1414
"node": ">=22.19.0",

‎alchemy.run.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ export default Alchemy.Stack(
324324
Config.NonEmptyString("CLOUDFLARE_ACCOUNT_ID"),
325325
),
326326
exaApiKey: Config.option(Config.Redacted("EXA_API_KEY")),
327+
// Optional: Boat on the deployment's own account (hosted targets).
328+
boatApiKey: Config.option(Config.Redacted("BOAT_API_KEY")),
327329
// Self-host sign-in providers and admission, validated by the runner.
328330
signIn: Config.option(Config.NonEmptyString("DX_DEPLOYMENT_SIGN_IN")),
329331
googleClientSecret: Config.option(
@@ -627,9 +629,12 @@ export default Alchemy.Stack(
627629
configuration.workersDevSubdomain,
628630
(subdomain) => `https://${daemonIngressName}.${subdomain}.workers.dev/`,
629631
);
632+
// Replicas serve each request's D1 session near the person (#1386,
633+
// persistence/d1-session.ts); writes still go to the primary.
630634
const database = yield* Cloudflare.D1.Database("Database", {
631635
name: configuration.databaseName,
632636
migrations: `apps/core/${migrationManifest.d1Directory}`,
637+
readReplication: { mode: "auto" },
633638
});
634639
// Browsers upload attachment parts and read attachments straight from
635640
// R2 through presigned URLs, so the bucket admits this origin.
@@ -919,6 +924,9 @@ export default Alchemy.Stack(
919924
...(e2bApiKey === undefined ? {} : { E2B_API_KEY: e2bApiKey }),
920925
...(sarvamApiKey !== undefined ? { SARVAM_API_KEY: sarvamApiKey } : {}),
921926
...(exaApiKey !== undefined ? { EXA_API_KEY: exaApiKey } : {}),
927+
...(!selfhost && Option.isSome(configuration.boatApiKey)
928+
? { BOAT_API_KEY: configuration.boatApiKey.value }
929+
: {}),
922930
...(googleClientSecret !== undefined
923931
? { DX_GOOGLE_CLIENT_SECRET: googleClientSecret }
924932
: {}),

‎apps/core/migration-manifest.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@
159159
"filename": "0029_release_v0.2.57.sql",
160160
"sha256": "ab302fa93eb52d73ce25cd976cb30509e1dfbd268195915c8e36d35f7c80c090",
161161
"classification": "destructive"
162+
},
163+
{
164+
"filename": "0030_release_v0.2.58.sql",
165+
"sha256": "7d8392be476baf5114c9ae99556ad78c2d466b9798600dd01b28d45ba474985c",
166+
"classification": "non-destructive"
162167
}
163168
],
164169
"durableObjects": [
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-- A deployment's own account of a provider that is prepared at runtime
2+
-- (Boat with BOAT_API_KEY): one row per provider, holding the connector's
3+
-- preparation, never a credential. Every person's Threads on the
4+
-- deployment's account start from what it prepared.
5+
CREATE TABLE orb_deployment_preparation (
6+
provider_id TEXT NOT NULL PRIMARY KEY CHECK (
7+
length(provider_id) BETWEEN 1 AND 64
8+
AND provider_id NOT GLOB '*[^a-z0-9-]*'
9+
),
10+
recipe TEXT CHECK (recipe IS NULL OR length(recipe) BETWEEN 1 AND 64),
11+
account TEXT CHECK (account IS NULL OR length(account) BETWEEN 1 AND 128),
12+
status TEXT NOT NULL CHECK (status IN ('preparing', 'ready', 'failed')),
13+
state_json TEXT NOT NULL CHECK (
14+
json_valid(state_json) AND length(state_json) BETWEEN 2 AND 16384
15+
),
16+
progress_json TEXT CHECK (
17+
progress_json IS NULL
18+
OR (json_valid(progress_json) AND length(progress_json) <= 1024)
19+
),
20+
error TEXT CHECK (error IS NULL OR length(error) BETWEEN 1 AND 500),
21+
revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
22+
updated_at TEXT NOT NULL
23+
);
24+
25+
-- A Thread on the deployment's account of a prepared provider pins the
26+
-- target its preparation made, as a connection's Thread does.
27+
DROP TRIGGER execution_workspace_pin_written_before_update;
28+
29+
CREATE TRIGGER execution_workspace_pin_written_before_update
30+
BEFORE UPDATE OF account_scope ON execution_workspace
31+
WHEN OLD.account_scope IS NULL
32+
AND NEW.account_scope IS NOT NULL
33+
AND NOT (
34+
OLD.state = 'uninitialized'
35+
AND NEW.state = 'uninitialized'
36+
AND NEW.runner_profile_id IS NOT NULL
37+
AND (
38+
(NEW.account_scope = 'deployment'
39+
AND NEW.account_owner_id IS NULL
40+
AND NEW.provider_account IS NULL)
41+
OR (NEW.account_scope IN ('workspace', 'personal')
42+
AND NEW.account_owner_id IS NOT NULL
43+
AND NEW.provider_account IS NOT NULL
44+
AND NEW.provider_template IS NOT NULL)
45+
)
46+
)
47+
BEGIN
48+
SELECT RAISE(ABORT, 'invalid execution workspace pin');
49+
END;

‎apps/core/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@dx/core",
33
"private": true,
4-
"version": "0.2.57",
4+
"version": "0.2.58",
55
"license": "FSL-1.1-ALv2",
66
"type": "module",
77
"scripts": {
@@ -31,6 +31,7 @@
3131
"e2b": "2.44.0",
3232
"effect": "4.0.5",
3333
"fast-png": "8.0.0",
34+
"fflate": "0.8.3",
3435
"hono": "4.12.32",
3536
"jose": "6.2.3",
3637
"jpeg-js": "0.4.4",

‎apps/core/production-bindings.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
"type": "secret",
1212
"provenance": "authored-wrangler"
1313
},
14+
{
15+
"name": "BOAT_API_KEY",
16+
"type": "secret",
17+
"provenance": "alchemy",
18+
"optional": true
19+
},
1420
{
1521
"name": "BYOK_CREDENTIAL_COORDINATOR",
1622
"type": "durable-object",

‎apps/core/scripts/test-reference-lifecycle.mjs‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,21 @@ const worker = spawn(
146146
// without passing it on, which would leave wrangler and workerd listening.
147147
{ stdio: "inherit", detached: true },
148148
);
149+
// A fixture that dies mid-run fails the next request with "fetch failed";
150+
// say how it died, so the failure points at its cause.
151+
let stopping = false;
152+
let tests;
153+
worker.once("exit", (code, signal) => {
154+
if (stopping) return;
155+
// Without its fixture a test waits forever on its next request, so end the
156+
// run now and say how the fixture died.
157+
console.error(
158+
`[lifecycle] The fixture's wrangler dev exited during the run (code ${code}, signal ${signal}).`,
159+
);
160+
tests?.kill("SIGTERM");
161+
});
149162
const stopWorker = (signal) => {
163+
stopping = true;
150164
try {
151165
process.kill(-worker.pid, signal);
152166
} catch {}
@@ -175,15 +189,20 @@ try {
175189
await new Promise((resolve) => setTimeout(resolve, 100));
176190
}
177191

178-
const result = spawnSync(
192+
// Asynchronous, so the fixture's exit above is seen while the tests run.
193+
tests = spawn(
179194
"pnpm",
180195
["exec", "vitest", "run", "--config", "vitest.lifecycle.config.ts"],
181196
{
182197
stdio: "inherit",
183198
env: { ...process.env, DX_LIFECYCLE_FIXTURE_URL: fixtureUrl },
184199
},
185200
);
186-
if (result.status !== 0) process.exitCode = result.status ?? 1;
201+
const status = await new Promise((resolve) =>
202+
tests.once("exit", (code) => resolve(code)),
203+
);
204+
if (status !== 0 || worker.exitCode !== null || worker.signalCode !== null)
205+
process.exitCode = status || 1;
187206
} finally {
188207
if (worker.exitCode === null) {
189208
const exited = new Promise((resolve) => worker.once("exit", resolve));

‎apps/core/src/app.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { installFlueObservation } from "./observability/flue.js";
2828
import { installRuntimeTracing } from "./observability/platform-tracing.js";
2929
import { threadOrbDiskRoutes } from "./orb-disk/routes.js";
3030
import { threadOrbMemoryRoutes } from "./orb-memory/routes.js";
31+
import { d1Session } from "./persistence/d1-session.js";
3132
import { portalAuthRoutes } from "./portals/auth-routes.js";
3233
import { portalHostRouting } from "./portals/host-routing.js";
3334
import { threadPortalRoutes } from "./portals/routes.js";
@@ -40,6 +41,7 @@ import {
4041
} from "./settings/members/routes.js";
4142
import { settingsRoutes } from "./settings/routes.js";
4243
import { pluginTriggerIngressRoutes } from "./settings/triggers/routes.js";
44+
import { shellRoutes } from "./shell/routes.js";
4345
import { sidebarLayoutRoutes } from "./sidebar-layout/routes.js";
4446
import { bitbucketGitGatewayRoutes } from "./source-control/bitbucket/git-gateway.js";
4547
import { bitbucketControlPlaneRoutes } from "./source-control/bitbucket/routes.js";
@@ -74,6 +76,7 @@ const app = new Hono<AppEnv>();
7476
app.use("*", portalHostRouting);
7577
app.use("*", requestId);
7678
app.use("*", requestLogging);
79+
app.use("*", d1Session);
7780

7881
app.get("/healthz", healthHandler);
7982
app.get("/readyz", readinessHandler);
@@ -105,6 +108,7 @@ app.route("/v1/projects", projectRoutes);
105108
app.route("/v1/realtime", realtimeRoutes);
106109
app.route("/v1/settings", settingsRoutes);
107110
app.route("/v1/shared-threads", sharedThreadRoutes);
111+
app.route("/v1/shell", shellRoutes);
108112
app.route("/v1/sidebar-layout", sidebarLayoutRoutes);
109113
app.route("/v1/thread-search", threadSearchRoutes);
110114
app.use("/v1/threads/:threadId", shareThreadReads);
@@ -117,12 +121,11 @@ app.use("/v1/threads/:threadId/follow", authorizeThread);
117121
app.route("/v1/threads", threadMemberRoutes);
118122
app.use("/v1/threads/:threadId/bug-reports", authorizeThread);
119123
app.route("/v1/threads", threadBugReportRoutes);
120-
app.use("/v1/threads/:threadId/changes", authorizeThread);
124+
// `/changes/*` also matches `/changes`, and `/files/*` `/files`; registering
125+
// both would authorize twice, one more D1 round trip per request.
121126
app.use("/v1/threads/:threadId/changes/*", authorizeThread);
122127
app.route("/v1/threads", threadChangesRoutes);
123-
app.use("/v1/threads/:threadId/files", authorizeThread);
124128
app.use("/v1/threads/:threadId/files/*", authorizeThread);
125-
app.use("/v1/threads/:threadId/files", enforceThreadActive);
126129
app.use("/v1/threads/:threadId/files/*", enforceThreadActive);
127130
app.route("/v1/threads", threadFilesRoutes);
128131
app.use("/v1/threads/:threadId/files-sandbox", authorizeThread);
@@ -132,8 +135,9 @@ app.route("/v1/threads", threadSandboxFileRoutes);
132135
// while the Thread is archived. `/storage/*` also matches `/storage`.
133136
app.use("/v1/threads/:threadId/storage/*", authorizeThread);
134137
app.route("/v1/threads", threadStorageRoutes);
135-
app.use("/v1/threads/:threadId/terminal", authorizeThread);
136-
app.use("/v1/threads/:threadId/terminal", enforceThreadActive);
138+
// `/terminal/*` also matches `/terminal`: the attach and its warm-up.
139+
app.use("/v1/threads/:threadId/terminal/*", authorizeThread);
140+
app.use("/v1/threads/:threadId/terminal/*", enforceThreadActive);
137141
app.route("/v1/threads", threadTerminalRoutes);
138142
// `/portals/*` also matches `/portals`; authorizing twice would see the
139143
// owner a contributor is impersonating.

‎apps/core/src/auth/authenticate.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { MiddlewareHandler } from "hono";
1313
import type { AppEnv } from "../http/types.js";
1414
import { authenticationLogger } from "../logging.js";
1515
import { decodeD1Binding } from "../persistence/d1-binding.js";
16+
import { primaryDatabase } from "../persistence/d1-session.js";
1617
import {
1718
authenticateExternalApiApplication,
1819
decodeExternalApplicationBasicCredentials,
@@ -84,7 +85,10 @@ export const authenticate: MiddlewareHandler<AppEnv> = async (
8485
if (credentials === undefined) {
8586
return unauthorized(context, 'Basic realm="dx external API"');
8687
}
87-
const db = await Effect.runPromise(decodeD1Binding(context.env.DB));
88+
// Application credentials are read at the primary, like sessions.
89+
const db = await Effect.runPromise(
90+
decodeD1Binding(primaryDatabase(context.env.DB)),
91+
);
8892
const authentication = await Effect.runPromise(
8993
Effect.result(
9094
authenticateExternalApiApplication(db, {

‎apps/core/src/auth/better-auth.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { APIError, getOAuthState } from "better-auth/api";
1414
import { organization } from "better-auth/plugins";
1515
import { Effect, Option, Schema } from "effect";
1616
import type { Bindings } from "../http/types.js";
17+
import { primaryDatabase } from "../persistence/d1-session.js";
1718
import {
1819
admitNewIdentity,
1920
signInRejectionMessages,
@@ -303,10 +304,16 @@ const accountIssuer = (account: {
303304
: `local:${encodeURIComponent(account.providerId)}`;
304305

305306
export const createAuth = (
306-
bindings: Bindings,
307+
requestBindings: Bindings,
307308
requirements: AuthenticationRequirements,
308309
authenticationPlugin?: BetterAuthPlugin | readonly BetterAuthPlugin[],
309310
) => {
311+
// Sessions, their revocations, and API keys are read at the primary, so a
312+
// revocation stops every client's next request (persistence/d1-session.ts).
313+
const bindings = {
314+
...requestBindings,
315+
DB: primaryDatabase(requestBindings.DB),
316+
};
310317
const configuration = Effect.runSync(
311318
loadSignInConfiguration(bindings, requirements),
312319
);
@@ -419,6 +426,10 @@ export const createAuth = (
419426
storage: "database",
420427
window: 60,
421428
max: 100,
429+
// Counters live in D1: limiting the session read every page load
430+
// starts with cost two sequential round trips (#1386). It guards no
431+
// credential; sign-in and the other auth endpoints stay limited.
432+
customRules: { "/get-session": false },
422433
},
423434
advanced: {
424435
cookiePrefix: localCookiePrefix(requirements),

0 commit comments

Comments
 (0)