Skip to content

Commit 8aec189

Browse files
committed
computer: reject an empty workspace artifacts session id
The artifacts wrapper rejects an empty session id so a blank value cannot quietly turn one tenant's client into a client over every repository in the namespace. The Workspace path read an empty id as an absent one instead, which walked around that check: a caller deriving the session id from an unset variable or a missing request field got the namespace-wide client rather than an error. An empty id now reaches the wrapper and fails there, as it did before the session id became optional. Omitting the id, or passing null, remains the way to ask for a client over the whole namespace.
1 parent a4ab816 commit 8aec189

3 files changed

Lines changed: 40 additions & 7 deletions

File tree

docs/15_artifacts_interface.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,12 @@ explicit `artifacts: { binding, sessionId: null }`, the opt-out for
313313
a workspace that has a session id but wants artifacts across every
314314
session — `workspace.artifacts` spans the namespace.
315315

316+
An empty session id fails here the same way it fails at
317+
`createArtifact`: the constructor throws `InvalidSessionIdError`.
318+
A workspace whose session id comes from an unset variable or a
319+
missing request field stops rather than handing that tenant a
320+
client over every other tenant's repositories.
321+
316322
When `artifacts` is omitted from `Workspace`, the command still
317323
exists, but operations fail with a clear "Workspace Artifacts binding
318324
is not configured" error.

packages/computer/src/workspace.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SQLiteTestStorage } from "@cloudflare/dofs/testing";
22
import { describe, expect, it, vi } from "vitest";
33

44
import { FakeArtifactsBinding } from "../tests/utilities/fake-artifacts-binding.js";
5-
import { createArtifact } from "./artifacts/index.js";
5+
import { createArtifact, InvalidSessionIdError } from "./artifacts/index.js";
66
import type { BackendHandle, WorkspaceBackend } from "./backend.js";
77
import { createGitClient } from "./git/index.js";
88
import type { WorkspaceModuleBackend } from "./runtime/types.js";
@@ -817,6 +817,27 @@ describe("Workspace backend selection", () => {
817817
expect(await ws.artifacts.list()).toHaveLength(1);
818818
});
819819

820+
it("rejects an empty workspace session id instead of widening the client", () => {
821+
// A session id derived from an unset variable or a missing
822+
// request field must not turn into namespace-wide access.
823+
const binding = new FakeArtifactsBinding();
824+
expect(
825+
() => new Workspace({ storage: makeStorage(), sessionId: "", artifacts: { binding } }),
826+
).toThrow(InvalidSessionIdError);
827+
});
828+
829+
it("rejects an empty artifacts session id", () => {
830+
const binding = new FakeArtifactsBinding();
831+
expect(
832+
() =>
833+
new Workspace({
834+
storage: makeStorage(),
835+
sessionId: "sess1",
836+
artifacts: { binding, sessionId: "" },
837+
}),
838+
).toThrow(InvalidSessionIdError);
839+
});
840+
820841
it("reports no session id when no binding is configured", async () => {
821842
const ws = new Workspace({ storage: makeStorage() });
822843
expect(ws.artifacts.sessionId).toBeUndefined();

packages/computer/src/workspace.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ export interface WorkspaceOptions {
183183
// and reaches every repository, including those other sessions
184184
// own. Scope it unless the caller is meant to administer the
185185
// namespace.
186+
//
187+
// An empty session id is a construction error rather than a way
188+
// to ask for the namespace-wide client, so an id that came out
189+
// blank by accident cannot widen one tenant's client to all of
190+
// them.
186191
artifacts?: {
187192
binding: Artifacts;
188193
sessionId?: string | null;
@@ -1335,16 +1340,17 @@ function positiveRetryOption(value: number | undefined, fallback: number, name:
13351340
* artifacts-specific one when the caller set it, otherwise the
13361341
* workspace's own, and undefined for a namespace-wide client.
13371342
*
1338-
* `WorkspaceOptions.sessionId` documents the empty string as its
1339-
* unset value, so an empty id here means the workspace has no
1340-
* session to scope by rather than a session named "". The wrapper
1341-
* itself is stricter: it rejects an empty id outright, since a
1342-
* caller reaching it directly has no such convention.
1343+
* An empty id is passed through rather than read as an absent one.
1344+
* A caller that derives the session id from an unset variable or a
1345+
* missing request field gets the wrapper's `InvalidSessionIdError`
1346+
* at construction instead of a client that reaches every
1347+
* repository in the namespace. Ask for that client by omitting the
1348+
* id or passing `null`.
13431349
*/
13441350
function artifactsSessionId(options: WorkspaceOptions): string | null | undefined {
13451351
const configured = options.artifacts?.sessionId;
13461352
if (configured !== undefined) return configured;
1347-
return options.sessionId === "" ? undefined : options.sessionId;
1353+
return options.sessionId;
13481354
}
13491355

13501356
function createDisabledArtifactsClient(): ArtifactClient {

0 commit comments

Comments
 (0)