Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/native-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Native Workers deploy ordinary Cloudflare Worker modules with their default hand

An app belongs to a private or team library and a workspace. Its UUID permanently owns provider resources; source revisions, export names, logical library moves and binding names do not change those identities.

Give a native Worker a root URL like any other artifact: use **Link** under its **Settings** in the gallery, or `artifact_link({kind:"app",name,slug,access})`. Slugs share the deployment-wide namespace with artifacts. Private links admit the app's current library, following it across library moves; public links admit any caller, and the app can implement its own authentication. The app receives paths relative to the slug, so `/my-app/jobs` arrives as `/jobs`. `app_read` and `app_list` report the link's `slug`, `access` and `url`.
Give a native Worker a root URL like any other artifact: use **Sharing** under its **Settings** in the gallery, or `artifact_link({kind:"app",name,slug,access})`. Slugs share the deployment-wide namespace with artifacts. Private links admit the app's current library, following it across library moves; public links admit any caller, and the app can implement its own authentication. The app receives paths relative to the slug, so `/my-app/jobs` arrives as `/jobs`. `app_read` and `app_list` report the link's `slug`, `access` and `url`.

Every app also keeps its management URL, which is always private:

Expand All @@ -26,7 +26,7 @@ Every app also keeps its management URL, which is always private:
/team/research/my-app/
```

Open and **Settings → Link** use the saved slug when one exists. Otherwise they use the app's authenticated `management_url`, also returned by `app_read`, `app_write`, and `app_list`. A team link works for signed-in teammates. A personal link works only for its owner, even if the viewer has an app with the same name. Personal usernames default to the lowercase part of the sign-in email before the @: for example, `nico@example.com` gets `nico`, then another Nico gets `nico2` if that name is free. If the sign-in provides no usable email prefix, the base is `user`. The assigned name stays with the account if the email changes. You can choose a different permanent username in a personal app's **Settings → Link**; previously copied links still work.
Open and **Settings → Sharing** use the saved slug when one exists. Otherwise they use the app's authenticated `management_url`, also returned by `app_read`, `app_write`, and `app_list`. A team link works for signed-in teammates. A personal link works only for its owner, even if the viewer has an app with the same name. Personal usernames default to the lowercase part of the sign-in email before the @: for example, `nico@example.com` gets `nico`, then another Nico gets `nico2` if that name is free. If the sign-in provides no usable email prefix, the base is `user`. The assigned name stays with the account if the email changes. You can choose a different permanent username in a personal app's **Settings → Sharing**; previously copied links still work.

Relative links keep the same workspace and library, and query parameters belong to the app. Older `/apps/<name>/?workspace=…&library=…` URLs still work. The `users` and `team` root slugs can no longer be claimed. On deployments that already use either slug, the root link remains, but paths matching the new app URL shapes now route to authenticated apps.

Expand Down
19 changes: 17 additions & 2 deletions e2e/gallery-native-apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ test("Worker gallery deploys drafts, manages revisions/secrets and preserves edi
let failFirst = true;
let failRestore = true;
let failReconcile = true;
let secretNames: string[] = [];
const server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
Expand Down Expand Up @@ -217,7 +218,8 @@ test("Worker gallery deploys drafts, manages revisions/secrets and preserves edi
saved!.status = "active";
return result({ ...saved, ok: true });
case "app_secrets":
return result({ names: ["TOKEN"] });
if (call.arguments.secrets) secretNames = ["TOKEN", "UNDECLARED"];
return result({ names: secretNames });
case "app_move":
saved = null;
return result({ ok: true });
Expand Down Expand Up @@ -329,17 +331,30 @@ test("Worker gallery deploys drafts, manages revisions/secrets and preserves edi
expect(new URL(page.url()).searchParams.get("app")).toBe("example");
expect(new URL(page.url()).searchParams.get("appTab")).toBe("deployments");
await panel.getByRole("tab", { name: "Settings", exact: true }).click();
await panel.getByLabel("Secret name", { exact: true }).fill("TOKEN");
await panel.getByLabel("Personal username").fill("nico");
await panel.getByRole("button", { name: "Save username" }).click();
await panel.getByLabel("Personal username").waitFor({ state: "hidden" });
expect(await panel.getByLabel("Share link").inputValue()).toBe(new URL("/users/nico/test/example/", server.url).href);
expect(await panel.getByRole("link", { name: "Open app" }).getAttribute("href")).toBe("/users/nico/test/example/");
const settings = panel.getByRole("tabpanel", { name: "Settings", exact: true });
const secretRows = settings.getByRole("list", { name: "Secret names", exact: true }).getByRole("listitem");
await secretRows.filter({ hasText: "TOKEN" }).getByText("Needs a value", { exact: true }).waitFor();
await secretRows.filter({ hasText: "TOKEN" }).getByRole("button", { name: "Set value", exact: true }).click();
expect(await panel.getByLabel("Secret name", { exact: true }).inputValue()).toBe("TOKEN");
expect(await panel.getByLabel("Secret value", { exact: true }).evaluate((input) => input === document.activeElement)).toBe(true);
// Sections that save on their own sit inside the panel, below the deploy action that does not cover them.
const deployBox = await settings.getByRole("button", { name: "Save and deploy Worker", exact: true }).boundingBox();
for (const heading of ["Secrets", "Sharing", "Library"]) {
const headingBox = await settings.getByRole("heading", { name: heading, exact: true }).boundingBox();
expect(headingBox!.y).toBeGreaterThan(deployBox!.y);
}
await panel.getByLabel("Secret value", { exact: true }).fill("secret-fixture-value");
const writesBeforeSecret = calls.filter((call) => call.name === "app_write").length;
await panel.getByRole("button", { name: "Save secret", exact: true }).click();
await panel.getByText("Secret saved", { exact: true }).waitFor();
expect(await panel.getByLabel("Secret value", { exact: true }).inputValue()).toBe("");
expect(await settings.getByText("Needs a value", { exact: true }).count()).toBe(0);
await secretRows.filter({ hasText: "UNDECLARED" }).getByText("Not passed to the Worker", { exact: true }).waitFor();
expect(calls.filter((call) => call.name === "app_write").length).toBe(writesBeforeSecret);
expect(await panel.locator(".cm-content").innerText()).toContain("local edit");
await panel.getByRole("button", { name: "Save and deploy Worker", exact: true }).click();
Expand Down
Loading
Loading