Skip to content

fix(cli): stop promising a claim link to signed-in publishers - #3545

Open
miguel-heygen wants to merge 2 commits into
mainfrom
fix/publish-claim-notice
Open

fix(cli): stop promising a claim link to signed-in publishers#3545
miguel-heygen wants to merge 2 commits into
mainfrom
fix/publish-claim-notice

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

What

hyperframes publish no longer tells a signed-in user their project can be claimed. The consent notice now branches on whether a credential resolves, and both branches keep the exposure disclosure.

Before (every publish, signed in or not):

  hyperframes publish uploads this project and creates a stable public URL.
  Anyone with the URL can open the published project and claim it after authenticating.

After, when a credential resolves:

  hyperframes publish uploads this project and creates a stable public URL.
  Anyone with the URL can open the published project. You are signed in, so you own it on publish and there is no claim link.

The signed-out output is byte-identical to main, bold line included. Only the claim clause varies between branches.

Why

The notice described the anonymous path only. An authenticated publish comes back claimed: true with an empty claim_token (packages/cli/src/utils/publishProject.ts:69-76), and the result block prints no claim URL in that branch (packages/cli/src/commands/publish.ts:214-247) because there is nothing to claim. So a signed-in user read a promise of a claim link, then went looking through the output for a claim token that the server never issued.

The docs already get this right (docs/packages/cli.mdx: "You can publish while signed out. The printed URL then carries a claim token"). The CLI prompt was the one surface asserting it unconditionally.

How

Hoist the credential resolution above the consent block and pick the claim clause from it. The --update / --space authentication gate now reuses that same value instead of calling tryResolveCredential() a second time, so one resolution owns "is this publish authenticated".

Resolution moves earlier for a plain publish, which previously only resolved credentials inside publishProjectArchive. Same call, same failure surface; a broken credential store now fails before the upload starts rather than during it.

The exposure sentence stays on both branches. It is load-bearing, not boilerplate: GET /v1/hyperframes/projects/<id>/public carries no @HeyGenAPI decorator (the /claim route directly below it does), and its handler does not filter on is_public, so any published project is readable by anyone holding its id. --public governs whether the claimed session is public in the web app, not whether the link is readable. Ownership is not privacy here.

Review history

An earlier head (710a58ea) dropped "public" from the shared bold line and replaced rather than supplemented the signed-in dim line, which left a signed-in publisher approving an upload with no exposure disclosure anywhere in the flow. Caught in review and fixed at f86f39ef2. The description above describes the current head.

Test plan

  • Unit tests added/updated in packages/cli/src/commands/publish.test.ts: two cases drive the real command with a declined prompt and assert the claim clause per auth state, plus a parameterised case asserting both branches print creates a stable public URL and Anyone with the URL can open the published project. That last one exists because the first version of this PR regressed the disclosure and no test caught it.
  • Verified non-vacuous: forcing the branch back to the anonymous string fails the signed-in case; removing the disclosure from either branch fails the parameterised case.
  • Manual testing performed: ran the actual CLI signed in against a scratch project and read the printed notice.
$ echo n | bun packages/cli/src/cli.ts publish /tmp/hf-consent-demo

  hyperframes publish uploads this project and creates a stable public URL.
  Anyone with the URL can open the published project. You are signed in, so you own it on publish and there is no claim link.

◆  Publish this project?
  • bun run --filter @hyperframes/cli test -- src/commands/publish.test.ts → 11 passed.
  • bunx tsc --noEmit -p packages/cli/tsconfig.json → clean. oxlint + oxfmt clean.

Not covered: the signed-out prompt was not re-run against a real logged-out machine. The unit test covers that branch, and its output is byte-identical to main.

The publish consent notice always said "anyone with the URL can open the
published project and claim it after authenticating", but that is only true
for an anonymous publish. When a credential resolves, the server returns
claimed=true with an empty claim_token and the CLI prints no claim URL, so a
signed-in user was left hunting for a token that was never issued.

Resolve the credential once before the notice and branch the wording on it.
The --update/--space gate now reuses that same resolution instead of calling
tryResolveCredential a second time.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving exact head 710a58ea.

Verified that credential resolution is single-sourced, returns only a credential or null, and drives both the consent text and existing --update/--space authentication gate. Anonymous copy remains unchanged; signed-in copy no longer promises a claim link. The focused declined-prompt tests cover both branches, and no publish/import work runs after a declined confirmation.

— Jerrai

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at 710a58ea, on something that came along with the copy fix rather than on the fix itself. The claim-link problem is real and the branch is the right shape. I would take this immediately with the exposure sentence restored.

The signed-in notice no longer says the project is world-readable

Two edits combine, and the second is easy to miss because it sits outside the ternary:

  1. The shared bold line lost the word "public". publish.ts:125 on main reads "...creates a stable public URL."; :129 at this head reads "...creates a stable URL.". That console.log is unconditional, so it changes what both branches print.
  2. The dim line is replaced rather than supplemented on the signed-in branch. "Anyone with the URL can open the published project and claim it after authenticating." becomes "You are signed in, so the project is owned by your account on publish. There is no claim link."

Net effect for a signed-in publisher: the consent prompt they approve before anything uploads no longer states that the artifact is reachable by anyone holding the URL. Nothing later fills the gap either. The published.claimed result block prints Project / Files / URL / Status, and the word "Public" appears only on the anonymous branch (publish.ts:260). So a signed-in user now goes from prompt to published with no exposure disclosure anywhere in the flow.

I checked that the disclosure is true rather than defensive boilerplate before asking for it back. GET /v1/hyperframes/projects/<project_id>/public carries no @HeyGenAPI decorator at all (render_routes.py:161-162), so it is unauthenticated, and get_public_project does not filter on is_public. Any published project is readable by anyone with its id, --public or not, signed in or not. Ownership is not privacy here, and the new wording invites reading it that way.

Suggested shape: keep the exposure sentence on both branches and vary only the claim clause.

Anyone with the URL can open the published project and claim it after authenticating.
Anyone with the URL can open the published project. It is owned by your account on publish, so there is no claim link.

Then either restore "public" to the bold line, or drop it deliberately and say so in the description.

"The signed-out wording is unchanged" is not accurate

Both that line and "its wording is byte-identical to what shipped before" are false for the bold line, per the :125 / :129 comparison above. The before/after blocks in the description do show the change ("stable public URL" in Before, "stable URL" in After), but the After block is labelled "when a credential resolves", so a shared edit reads as if it were scoped to the signed-in branch.

Neither new test pins that line. offers the claim step when publishing anonymously asserts only toContain("claim it after authenticating"), which the dim line still satisfies on its own, so the signed-out change passes CI unnoticed. Worth an assertion on the first line in whichever direction you land.

Checked and clear

  • The credential hoist adds no new failure surface, and the description is right about why. publishProjectArchive already calls tryResolveCredential() on every publish including anonymous (publishProject.ts:719), so a broken credential store already aborted a plain publish. This only moves the abort earlier, before the prompt instead of mid-upload, which is the better place for it. I went looking for a regression here and there isn't one.
  • Reusing the hoisted value in the --update / --space gate is genuine de-duplication, not a semantic change: same function, same options, and nothing between the two former call sites mutates the store.
  • The anonymous dim line is byte-identical, so the claim-token path itself is untouched.

CI has since settled green on this head: 41 checks, none in flight, none failed. So this verdict is on the copy, not on a red lane.

Review by Rames

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseding my earlier approval at the same exact head 710a58ea.

The claim-link correction is sound, but the signed-in consent prompt now omits the material disclosure that anyone holding the URL can open the published project:

  • the shared headline changed from “stable public URL” to “stable URL”; and
  • the signed-in branch replaces, rather than supplements, the URL-accessibility sentence.

The public read endpoint is unauthenticated, and ownership does not make the project private. Please preserve the exposure disclosure in both branches while varying only the claim-token clause—for example: “Anyone with the URL can open the published project. It is owned by your account on publish, so there is no claim link.”

Also correct the description’s claim that signed-out wording is unchanged, or restore the shared headline, and add a focused assertion for the shared/exposure copy.

No functional concern with the credential-resolution hoist.

— Jerrai

The claim-link fix dropped "public" from the shared bold line and replaced,
rather than supplemented, the dim line on the signed-in branch. A signed-in
publisher then approved the upload without being told anywhere in the flow that
the artifact is reachable by anyone holding the URL: the owned result block
prints Project / Files / URL / Status, and "Public" only appears on the
anonymous branch.

The disclosure is true for an owned project, not defensive boilerplate. The
published-project read is the one hyperframes route with no authentication
decorator, so any URL holder can open it; --public governs whether the CLAIMED
session is public in the web app, not whether the link is readable.

Restore "stable public URL" and lead the signed-in line with the same exposure
sentence, keeping only the claim half branch-specific. A parameterised test now
asserts both branches carry it.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Valid, and I checked the exposure claim at the source rather than taking it from the review. GET /v1/hyperframes/projects/<id>/public is the one route in hyperframes/routes/render_routes.py with no @HeyGenAPI authentication decorator (the /claim route directly below it has one), so any URL holder can open a published project whether or not it is owned. --public governs whether the claimed session is public in the web app, not whether the link is readable. So the sentence is load-bearing and I should not have dropped it.

Fixed at f86f39ef2:

  • stable public URL restored on the shared bold line, byte-identical to main.
  • The signed-in branch now leads with the same exposure sentence and only the claim half differs: "Anyone with the URL can open the published project. You are signed in, so you own it on publish and there is no claim link."
  • Added a parameterised test asserting both branches print creates a stable public URL and Anyone with the URL can open the published project, so removing the disclosure from either branch fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants