Add per-project non-guessable meeting URLs (#72)#143
Open
suiyangqiu wants to merge 2 commits into
Open
Conversation
Adds a per-project obfuscateUrls setting stored in a new Cosmos "projects"
container. When enabled, new dashboards deploy to a non-guessable
{project}/{guid}/ path (GUID reused from Cosmos on re-processing so the URL
is stable), and the public per-project index page is suppressed so it can't
re-expose the links. Default is unchanged (human-readable
{project}/{meeting-id}/), and existing dashboards keep their URLs.
The setting is read at deploy time via lib/cosmosClient.getProjectSettings,
which is default-safe (returns human-readable if Cosmos is missing or
unreachable, so a deploy never breaks on the settings store). A small admin
script (scripts/set-project-obfuscation.js) flips the flag; changing it needs
the Cosmos data-plane role via Azure RBAC, not repo access.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- getProjectSettings now fails CLOSED: it throws on any non-404 Cosmos
error (network/auth/throttle/5xx) so an ambiguous outage aborts the
deploy instead of silently downgrading an opted-in project to a
guessable URL and re-publishing the public index. 404 (missing
item/container/db = not opted in) and no-endpoint still default to
human-readable.
- deployProjectIndex now actively deletes any existing
{project}/index.html for obfuscated projects (idempotent, non-fatal)
rather than just skipping the write, so a landing page published
before opt-in stops listing historical meetings.
- resolveDeploySlug gains a dependency-injection seam; add tests for the
Cosmos-backed stable-GUID reuse, fresh-GUID, lookup-failure, and
fail-closed-abort paths.
- Extract projectDocId() helper (single source of truth for the settings
doc id) and document the security model: a GUID URL is an undiscoverable
bearer-token link, not access control, and relies on the $web container
keeping anonymous listing disabled (noted in dashboardStorage.bicep).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
suiyangqiu
marked this pull request as ready for review
July 10, 2026 05:52
yaqi-lyu
reviewed
Jul 10, 2026
Comment on lines
+293
to
+299
| } catch (err) { | ||
| const stderr = String(err.stderr || ""); | ||
| if (/BlobNotFound|does not exist/i.test(stderr)) { | ||
| log("info", "No existing project index to remove for obfuscated project", { blobName }); | ||
| } else { | ||
| log("warn", "Could not remove existing project index (non-fatal)", { blobName, stderr }); | ||
| } |
Member
There was a problem hiding this comment.
If deleting the old project index fails, we should fail the non 404 deploy instead of logging and continuing. /{project}/index.html may stay public and keep listing old meeting links
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 Summary of Changes
Corresponding issue: #72
/{project}/{meeting-id}/) to non-guessable ones (/{project}/{guid}/). When it is on, the public per-project index page (which listed every meeting) is suppressed too, so the only way to reach a dashboard is the exact link sent in the Teams notification.🤷♂️ Why
Tiger's dashboard URLs embed the project and meeting date, so anyone who knows a project can guess links to sensitive sprint retros, and the auto-generated index page hands out the full list. Projects that care about privacy need a way to make those URLs undiscoverable. The toggle is per-project so security-sensitive teams can turn it on without affecting everyone else.
🧪 Test plan
npm test- 82 unit tests pass, including new coverage for the settings lookup (default-safe when Cosmos is absent) and the deploy-slug decision (human-readable vs fresh GUID vs stable reuse) plus index suppression.crm/2026-04-03/; ON ->crm/<uuid>/; ON with a prior GUID -> same URL reused; project index -> suppressed (returns null).satigertestwebwith the flag on) is pending creation of the newprojectsCosmos container viainfra/setup-cosmos.sh test. That container-create needs an ARM DocumentDB/Contributor role; the authoring account only holds Key Vault Administrator oncosmos-tiger-*, so a sysadmin needs to run it once per environment.node scripts/set-project-obfuscation.js <project> true, process a meeting, confirm the Teams link is a GUID URL and/{project}/returns no index.🔒 Note on changing the flag
Tiger is a public repo, but flipping the setting writes to the
projectsCosmos container, gated by Azure RBAC (Cosmos DB Built-in Data Contributor oncosmos-tiger-*) - not by repo access. The admin script holds no credentials (DefaultAzureCredential +COSMOS_ENDPOINTonly). So the control point is Cosmos role membership, and this does not need to be a Keeper secret.🤖 Generated with Claude Code