diff --git a/.env.lakebed.server.example b/.env.lakebed.server.example index f915444..acea25d 100644 --- a/.env.lakebed.server.example +++ b/.env.lakebed.server.example @@ -1,3 +1,4 @@ -OWNER_EMAILS=owner@example.com,alternate-owner@example.com +OWNER_EMAILS=owner@example.com +WORKSPACE_VIEWER_EMAILS=viewer@example.com,another-viewer@example.com PUBLISH_TOKEN=replace-with-a-long-random-secret ARTIFACTS_URL=https://your-artifacts.lakebed.app diff --git a/AGENTS.md b/AGENTS.md index 7e28c53..40783e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,8 +3,8 @@ This repository is a Lakebed v0 capsule. Keep application code within `server/`, `client/`, and `shared/`; do not add runtime npm dependencies or Node built-ins to capsule code. - Run `npm run build` after changes. -- Keep authorization server-side. Emails locate pending invitations; durable owner and recipient authorization uses immutable Lakebed user IDs. -- New artifacts must remain private until explicit recipient emails are saved. +- Keep authorization server-side. Emails locate pending invitations; durable owner, workspace-viewer, and per-artifact recipient authorization uses immutable Lakebed user IDs. +- New artifacts must remain non-public. Only owners and configured workspace viewers may read them until explicit per-artifact recipients are saved. - Only owners may change access. Preserve exact-email, domain, and public settings when replacing or republishing HTML. - Never add `allow-same-origin` to the artifact iframe sandbox. Artifact HTML is untrusted relative to the authenticated shell. - Keep HTML chunks below Lakebed's 64 KiB value limit, artifact HTML within the 768 KiB application budget, and total state within Lakebed's 1 MiB capsule limit. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0b80d4..ee90d4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,8 @@ Application code belongs in `server/`, `client/`, and `shared/`. Capsule code ma ## Security and privacy invariants -- New artifacts remain private until access is explicitly saved. -- Authorization stays server-side and uses durable Lakebed user IDs after invitation acceptance. +- New artifacts remain non-public; configured workspace viewers have read-only access to every artifact. +- Authorization stays server-side and uses durable Lakebed user IDs after owner, workspace-viewer, or per-artifact invitation acceptance. - Only deployment owners may publish, replace, delete, or change access. - Republishing preserves omitted exact-email, domain, and public settings. - The artifact iframe must never gain `allow-same-origin`. diff --git a/README.md b/README.md index f91ddf5..1b0ef77 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,16 @@ You need: ### 1. Clone, install, and deploy -Replace `you@example.com` with the Google email you will use to administer the service: +Replace `you@example.com` with the Google email you will use to administer the +service. Optionally add read-only workspace viewers who should be able to open +every artifact: ```sh git clone https://github.com/none23/codex-artifacts.git cd codex-artifacts npm ci -npm run setup -- --owner you@example.com +npm run setup -- --owner you@example.com \ + --viewer viewer@example.com,another-viewer@example.com ``` Follow the Lakebed sign-in prompt if one appears. The command creates your deployment, checks that it is healthy, and prints its service URL. @@ -57,7 +60,9 @@ From any project, ask your agent: Use codex-artifacts to create and publish a visual architecture report for this repository. ``` -The agent creates the HTML, publishes it to your deployment, and opens the private result in your browser. New artifacts are visible only to configured owners until you explicitly share them. +The agent creates the HTML, publishes it to your deployment, and opens the +private result in your browser. Owners and workspace viewers can open every +artifact. Additional recipients can be granted access per artifact. You can also publish an existing HTML file directly from the Codex Artifacts repository: @@ -73,6 +78,7 @@ The service provides: - Google sign-in with durable Lakebed identity bindings - Private-by-default artifact publishing +- Deployment-wide read-only workspace viewers - Per-artifact invitations by exact email or email domain - Optional public links - A browser UI for uploading, replacing, downloading, and deleting artifacts @@ -86,13 +92,18 @@ Useful options: ```sh --slug architecture-report # Reuse the URL on future updates ---share person@example.com # Replace the exact-email invitation list +--share person@example.com # Set additional exact-email recipients --public # Allow anyone with the link to view --no-open # Do not open the result in a browser -- --option-like-name.html # Publish a filename beginning with "-" ``` -Omitting `--share` or `--public` while updating preserves existing access. The publisher refuses to combine a process-level URL override with a token loaded from the configuration file; override `ARTIFACTS_URL` and `ARTIFACTS_PUBLISH_TOKEN` together. +Workspace viewers always retain access and are independent of `--share`. On +update, supplying `--share` replaces only the artifact's additional exact-email +invitation list; omitting it preserves existing access. Omitting `--public` on +update preserves the public setting. The publisher refuses to combine a +process-level URL override with a token loaded from the configuration file; +override `ARTIFACTS_URL` and `ARTIFACTS_PUBLISH_TOKEN` together. If the skill is copied away from this repository instead of linked, set `CODEX_ARTIFACTS_ENV` to the absolute path of `.env.lakebed.server`. @@ -106,17 +117,29 @@ The setup command: 4. Creates an owned deployment or updates the deployment already bound in `lakebed.json`. 5. Saves the deployment URL and verifies `/api/status`. -Re-run `npm run setup` after pulling an update. Existing owners, secrets, and a configured custom URL are preserved unless you explicitly replace the owner list with `--owner`. +Re-run `npm run setup` after pulling an update. Existing owners, workspace +viewers, secrets, and a configured custom URL are preserved unless you +explicitly replace them with `--owner`, `--viewer`, or `--clear-viewers`. ## Identity and access -`OWNER_EMAILS` and artifact recipient emails are invitations, not permanent authorization keys. +Owner, workspace-viewer, and per-artifact recipient emails are invitations, not +permanent authorization keys. - The first matching verified Google sign-in accepts an invitation and binds it to the account's immutable Lakebed user ID. - Later requests authorize the bound user ID rather than trusting current profile email. - Removing an owner invitation from `OWNER_EMAILS` and redeploying revokes that binding. +- Removing a workspace viewer invitation from `WORKSPACE_VIEWER_EMAILS` and + redeploying revokes read access across the workspace. - Removing an artifact email or domain rule immediately invalidates and removes grants created from that rule. - Every configured owner is a deployment administrator and can manage every artifact. +- Every configured workspace viewer can open every existing and future artifact + but cannot publish, delete, or change access. +- Per-artifact recipients can open only artifacts shared with their exact email + or domain. + +If an address appears in both `OWNER_EMAILS` and `WORKSPACE_VIEWER_EMAILS`, +owner access takes precedence. Domain invitations are broad. Do not add public mail domains such as `gmail.com`; every matching signed-in account could accept access. Prefer exact-email invitations for sensitive artifacts. @@ -133,6 +156,7 @@ Set: ```dotenv OWNER_EMAILS=you@example.com +WORKSPACE_VIEWER_EMAILS=viewer@example.com,another-viewer@example.com PUBLISH_TOKEN=replace-with-a-long-random-secret ARTIFACTS_URL=https://your-artifacts.lakebed.app ``` @@ -151,6 +175,7 @@ Set `ARTIFACTS_URL` to the deployed or custom URL. `lakebed.json` is intentional | Variable | Purpose | | --- | --- | | `OWNER_EMAILS` | Comma-separated pending/current owner invitations | +| `WORKSPACE_VIEWER_EMAILS` | Comma-separated read-only viewers of every artifact | | `PUBLISH_TOKEN` | Server-side automation secret | | `ARTIFACTS_URL` | Publisher destination in the local configuration file | | `ARTIFACTS_PUBLISH_TOKEN` | Process-level publisher token override | @@ -183,7 +208,7 @@ DEPLOY_ID="$(node -p "JSON.parse(require('fs').readFileSync('lakebed.json')).dep npm exec lakebed -- db export "$DEPLOY_ID" --out codex-artifacts-backup.json ``` -Lakebed export is not a point-in-time snapshot during concurrent writes. Keep backups private: they contain artifact HTML, owner invitations, and recipient access data. +Lakebed export is not a point-in-time snapshot during concurrent writes. Keep backups private: they contain artifact HTML, owner and workspace-viewer invitations, and per-artifact recipient access data. ### Rotate the publisher token @@ -193,6 +218,13 @@ Replace `PUBLISH_TOKEN` in `.env.lakebed.server` with a new 64-character hex val Update `OWNER_EMAILS` and run `npm run deploy`. Removing an email revokes its bound owner access. Adding an email creates a pending invitation that binds on that person's next sign-in. +### Change workspace viewers + +Run `npm run setup -- --viewer one@example.com,two@example.com` to replace the +read-only workspace viewer list, or use `--clear-viewers` to remove it. The +change applies to every existing and future artifact. A newly added viewer must +sign in with the matching verified Google email to bind the invitation. + ## Security and capacity Artifact HTML is untrusted. It runs in an iframe sandbox without `allow-same-origin` and cannot access the authenticated shell, but scripts, forms, popups, and outbound network requests are currently allowed inside the artifact. An artifact can transmit data embedded in its own HTML. Downloaded HTML is no longer sandboxed if you open it directly. Do not publish secrets, credentials, private source, or regulated data without reviewing the generated page. @@ -215,6 +247,6 @@ Lakebed local state resets when the dev process restarts. Real Google sign-in ac ## How it works -The project is a Lakebed v0 capsule. Lakebed supplies first-party Google authentication, transactional storage, and hosting. Artifact HTML is split into database-safe chunks. Owner and recipient invitations bind to durable Lakebed user IDs on first matching sign-in. HTML is rendered with `srcDoc` in a sandboxed iframe without `allow-same-origin`. +The project is a Lakebed v0 capsule. Lakebed supplies first-party Google authentication, transactional storage, and hosting. Artifact HTML is split into database-safe chunks. Owner, workspace-viewer, and recipient invitations bind to durable Lakebed user IDs on first matching sign-in. HTML is rendered with `srcDoc` in a sandboxed iframe without `allow-same-origin`. Codex Artifacts is an independent project and is not affiliated with or endorsed by OpenAI or Anthropic. diff --git a/SECURITY.md b/SECURITY.md index 84b4860..bd18707 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,4 +19,4 @@ Do not test against a deployment you do not own or have explicit permission to a ## Deployment secrets -Publishing tokens, Lakebed tokens, `.env.lakebed.server`, `.lakebed/`, database exports, and private artifact URLs must not be committed or included in reports. Rotate a token immediately if it is exposed. +Publishing tokens, Lakebed tokens, `.env.lakebed.server`, `.lakebed/`, database exports, workspace-viewer invitations, and private artifact URLs must not be committed or included in reports. Rotate a token immediately if it is exposed. diff --git a/client/index.tsx b/client/index.tsx index b7ebffb..2437d30 100644 --- a/client/index.tsx +++ b/client/index.tsx @@ -67,9 +67,19 @@ function rememberEmails(values: string[]): string[] { return emails; } -function accessLabel(access: { isPublic: boolean; sharedWith: string[]; sharedDomains: string[] }): string { +function accessLabel(access: { + isPublic: boolean; + sharedWith: string[]; + sharedDomains: string[]; + workspaceViewerCount?: number; +}): string { if (access.isPublic) return "Public"; const rules = access.sharedWith.length + access.sharedDomains.length; + const workspaceViewerCount = access.workspaceViewerCount ?? 0; + if (workspaceViewerCount && rules) { + return `Workspace + ${rules} rule${rules === 1 ? "" : "s"}`; + } + if (workspaceViewerCount) return "Workspace viewers"; if (!rules) return "Owners only"; return `${rules} access rule${rules === 1 ? "" : "s"}`; } @@ -155,7 +165,7 @@ function NewArtifactForm() {

New artifact

Publish an HTML file

-

Only you can see it until you add recipients. Maximum {formatBytes(MAX_ARTIFACT_BYTES)} per artifact; {formatBytes(MAX_TOTAL_ARTIFACT_BYTES)} workspace HTML budget.

+

Owners and workspace viewers can open every artifact; additional recipients can be added per artifact. Maximum {formatBytes(MAX_ARTIFACT_BYTES)} per artifact; {formatBytes(MAX_TOTAL_ARTIFACT_BYTES)} workspace HTML budget.

void submit(event)}>