@@ -74,59 +74,54 @@ uniform; the counts are just always zero.
7474Container backend:
7575
7676``` ts
77- import { Workspace , WorkspaceProxy } from " @cloudflare/workspace" ;
77+ import { withWorkspace , WorkspaceProxy } from " @cloudflare/workspace" ;
7878import { CloudflareContainerBackend , withWorkspaceContainer }
7979 from " @cloudflare/workspace/backends/container" ;
8080import { DurableObject } from " cloudflare:workers" ;
8181
8282export { WorkspaceProxy };
8383
84- export class ContainerExample extends withWorkspaceContainer (class extends DurableObject <Env > {}) {
85- #workspace = new Workspace ({
86- storage: this .ctx .storage ,
84+ // `withWorkspace` constructs the Workspace and installs the plumbing
85+ // `getWorkspace` needs — no hand-written stub method. The options
86+ // callback runs after `super(...)`, so it can read `self.ctx`. Compose
87+ // it with `withWorkspaceContainer` when the durable object also owns
88+ // the container binding.
89+ export class ContainerExample extends withWorkspace (
90+ withWorkspaceContainer (class extends DurableObject <Env > {}),
91+ (self ) => ({
92+ storage: self .ctx .storage ,
8793 backends: [
8894 new CloudflareContainerBackend ({
89- container : () => this ,
90- workspace: { binding: " ContainerExample" , id: this .ctx .id .toString () },
95+ container : () => self ,
96+ workspace: { binding: " ContainerExample" , id: self .ctx .id .toString () },
9197 }),
9298 ],
93- });
94-
95- async getWorkspace(): Promise <WorkspaceStub > {
96- await this .#workspace .ready ();
97- return this .#workspace .stub ();
98- }
99-
100- override fetch(req : Request ) { return this .#workspace ; /* see example */ }
101- }
99+ }),
100+ ) {}
102101```
103102
104103Worker backend:
105104
106105``` ts
107- import { Workspace , WorkspaceServiceProxy } from " @cloudflare/workspace" ;
106+ import { withWorkspace , WorkspaceServiceProxy } from " @cloudflare/workspace" ;
108107import { WorkerBackend } from " @cloudflare/workspace/backends/worker" ;
109108import { DurableObject } from " cloudflare:workers" ;
110109
111110export { WorkspaceServiceProxy };
112111
113- export class ContainerExample extends DurableObject <Env > {
114- #workspace = new Workspace ({
115- storage: this .ctx .storage ,
112+ export class ContainerExample extends withWorkspace (
113+ class extends DurableObject <Env > {},
114+ (self ) => ({
115+ storage: self .ctx .storage ,
116116 backends: [
117117 new WorkerBackend ({
118- loader: env .LOADER ,
119- workspace: { binding: " ContainerExample" , id: this .ctx .id .toString () },
120- ctx ,
118+ loader: self . env .LOADER ,
119+ workspace: { binding: " ContainerExample" , id: self .ctx .id .toString () },
120+ ctx: self . ctx ,
121121 }),
122122 ],
123- });
124-
125- async getWorkspace(): Promise <WorkspaceStub > {
126- await this .#workspace .ready ();
127- return this .#workspace .stub ();
128- }
129- }
123+ }),
124+ ) {}
130125```
131126
132127Filesystem only — no backend, no shell:
@@ -242,20 +237,78 @@ for the caveat.
242237## Worker-side consumption
243238
244239``` ts
240+ import { getWorkspace } from " @cloudflare/workspace" ;
241+
245242export default {
246243 async fetch(request : Request , env : Env ): Promise <Response > {
247244 const id = env .ContainerExample .idFromName (" user-123" );
248- using ws = await env .ContainerExample .get (id ). getWorkspace ( );
245+ using ws = await getWorkspace ( env .ContainerExample .get (id ));
249246
250247 await ws .fs .writeFile (" /notes.md" , " hello" );
251- using handle = await ws .shell .exec (" ls /workspace" );
248+ using handle = await ws .shell .exec (" ls /workspace" , { encoding: " utf8 " } );
252249 const { exitCode, stdout } = await handle .result ();
253250
254251 return new Response (stdout , { status: exitCode === 0 ? 200 : 500 });
255252 },
256253} satisfies ExportedHandler <Env >;
257254```
258255
256+ ` getWorkspace(stub) ` calls the accessor the ` withWorkspace ` mixin
257+ installed on the durable object, then wraps the returned stub in a
258+ Worker-side client. Called with the durable object itself
259+ (` getWorkspace(this) ` ), it returns the same client backed by the
260+ in-isolate Workspace, so the surface is identical in both places. The
261+ client mirrors the stub surface (` fs ` , ` git ` , ` shell ` , ` artifacts ` ,
262+ ` assets ` ); the only difference is that
263+ ` shell.exec ` also accepts a tagged template, covered next.
264+
265+ ### Building commands safely
266+
267+ ` shell.exec ` runs one command string through ` /bin/sh -c ` in the
268+ container. Pasting a path or any other value straight into that string
269+ is a shell-injection risk: a value like ` x; rm -rf / ` breaks out of its
270+ argument. Call ` exec ` as a tagged template and interpolated values are
271+ escaped for you:
272+
273+ ``` ts
274+ const file = " my notes.md" ;
275+ const out = await (await ws .shell .exec ` cat ${file } ` ).result (); // cat 'my notes.md'
276+ ```
277+
278+ The tagged-template form defaults to string (` utf8 ` ) output, since a
279+ caller reaching for it almost always wants text back.
280+
281+ The plain ` exec(command, options) ` form is unchanged. Use it when you
282+ need ` cwd ` or ` backend ` , and wrap an interpolated command in the ` sh `
283+ tag to escape it:
284+
285+ ``` ts
286+ import { sh } from " @cloudflare/workspace" ;
287+
288+ await ws .shell .exec (sh ` cat ${file } ` , { cwd: " /workspace" });
289+ await ws .shell .exec (" npm test" , { cwd: " /workspace" , encoding: " utf8" });
290+ ```
291+
292+ The plain form defaults to ` Uint8Array ` output; pass
293+ ` { encoding: "utf8" } ` for a string.
294+
295+ ` sh ` quotes strings and numbers, quotes arrays element-by-element, and
296+ leaves the static template parts alone — they're the trusted command.
297+ When you really do mean shell syntax, wrap the value in ` { raw: "..." } `
298+ to opt out of escaping for that one value:
299+
300+ ``` ts
301+ await ws .shell .exec (sh ` ls ${dir } ${{ raw: " | wc -l" }} ` );
302+ ```
303+
304+ The escaping has to run in the caller, not on the durable-object side:
305+ when the command crosses Workers RPC, a tagged template's ` .raw `
306+ property doesn't survive structured clone, so the wrapper (and ` sh ` )
307+ collapse the template to a finished string before the call. The remote
308+ stub's ` exec ` rejects a raw tagged-template call so the unescaped path
309+ fails loudly. ` shellQuote ` is exported too, for the rare case where
310+ you need to quote a single argument outside a template.
311+
259312## Observability
260313
261314The package emits one span per documented operation through an optional
0 commit comments