feat(things,tokens): re-land hidden π΅οΈ links + PAT GET bridge + custom audiences π (#413/#431 never reached develop) - #613
Conversation
Two features, one stacked branch:
Hidden visibility β a fifth audience, 'hidden' (acl ['tt:hidden','tt:user']):
unlisted everywhere (feeds, profiles, listings, search β for everyone but the
owner) yet viewable by ANYONE presenting the thing's randomly generated
linkKey (?key= on GET /api/v1/things and the /post page; body.key admits
engagement). The server mints the 24-byte base64url key whenever a thing
ENTERS hidden β re-hiding rotates it so circulated links die β and projects
it to the owner only. canView honors the key only while the acl still says
hidden, so un-hiding retires links instantly. UI: π΅οΈ Hidden in the composer
circle select and the post-menu privacy submenu, a Hidden circle chip in feed
filters, and "Copy hidden link π΅οΈ" in the post menu.
GET bridge β per-token allowGet ("Works via GET links π" tickbox) opens
GET /api/v1/get?token=β¦&op=β¦: the whole PAT things surface as plain GET URLs
(query-param token + args) for agents that can browse but cannot send
headers, bodies, or non-GET verbs. Cookies are never read (mutating GETs
cannot be CSRF'd β the unguessable token is the authorization); scope checks,
free-403 semantics, atomic use accounting, the tt:token sandbox, and the
visibility fence are byte-for-byte the Bearer path via a shared
resolvePatSessionActor core. op=self is free introspection. Responses carry
Cache-Control: private, no-store and Referrer-Policy: no-referrer. Registered
in the server import map + apiDocs (docs-derived route table).
verify-pat-tokens.mjs grows sections G (hidden, 17 checks) and H (bridge,
21 checks) β 105/105 pass live, including fence Γ bridge and fence Γ hidden
interplay. Browser-QA'd: UI mint with the tick badges π and drives the
bridge credential-less; composer-posted hidden post is invisible anonymous,
viewable via its secret URL, and offers Copy hidden link.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refreshed by the rebase-pr-stacks workflow: https://github.com/lopugit/thingtime/actions/runs/33120454043
β¦ps, and the hidden-only token fence
Custom visibility: an acl carrying the tt:custom marker names exactly who
can do what. Baseline toggle (tt:all = everyone reads / tt:hidden = link-key
holders read / neither = only the picks), plus per-user grants
tt:user/<name>[/comment|/write] and per-group grants tt:group/<id>[β¦] with
write β comment β read. On custom things, general viewers get READ ONLY β
commenting/reacting/sharing needs the comment capability (inherit chains
judged on their terminal, saves exempt as private bookmarks), and write
grantees share-edit crystal/extended/tags through PATCH while audience,
folder, token grants, and deletion stay owner-only (storage keeps billing
the owner). Granted things land in grantees' FEEDS via a new
visibilityQueryFor grant clause. Capability grammar, evaluation
(aclCapabilityFor), specificity, and AclViewer.groupIds live in the registry;
viewer group memberships preload beside friendIds.
Groups: protected 'group' + 'group-member' things managed via /api/v1/groups
(list/create/replace-members/delete) + /api/v1/groups/audience-sources
(friends, connections, recents, groups β the picker's prefill). Member docs
mirror the friend-doc shape (targetId = member) so every query rides
existing indexes β no new MongoDB indexes (the 64-index local budget stays
untouched). Membership edits propagate live to every referencing acl;
member ids validate via findUserById (both user eras).
UI: CustomAudienceModal β baseline chips, people search
(/api/v1/users/search), prefilled+filterable Recents/Friends/Connections
sections, per-entry capability selects, save-selection-as-group and
existing-group chips β wired into the composer circle select (π Custom +
Edit button; fixed a stale-closure fallback that reset the applied audience)
and the PostCard privacy menu (custom routes through the picker).
Token fence: visibility mode 'hidden' ("Hidden only π΅οΈ") β the token lives
entirely in hidden link-key things; its creates are born hidden and mint
their secret link.
verify-pat-tokens.mjs section I: 29 checks β capability matrix (read cannot
comment, write implies comment, writers never touch audience/delete),
baseline toggles (public baseline still gates comments; hidden baseline
mints a key that grants read only), live group grant/revoke, feed grant
clause, audience-sources, protected kinds, hidden-only fence. 134/134 pass.
Browser-QA'd desktop + mobile: full composer flow (search β add β capability
β create group β apply β post) produced the exact acl and enforced it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refreshed by the rebase-pr-stacks workflow: https://github.com/lopugit/thingtime/actions/runs/33133018850
β¦s-custom-audiences-reland # Conflicts: # Commander/extensions/raycast/src/commands/regexToReplacementConverter.tsx # TESTING.md # deprecated/api/src/index.js # remix/app/api/utils/auth/patTokens.ts # remix/app/api/utils/things/things.ts # remix/app/components/Feed/PostCard.tsx # remix/app/components/Feed/PostComposer.tsx # remix/app/routes/api/v1/things/share/_share.tsx
β¦ keyed by id The 105-check suite (now 149 checks on this stack) crashed in section F because develop's search responses key posts by thing id; postRows() accepts both shapes. Full run: 149 passed, 0 failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
π§ͺ Develop S3 preview deploying
Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup. |
1 similar comment
π§ͺ Develop S3 preview deploying
Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup. |
π§ͺ Develop S3 preview deploying
Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup. |
π§Ή Develop S3 preview removedThe PR-specific alias and every workflow-created develop deployment were removed when this PR closed. The ordinary generated Vercel Preview remains available on the shared development runtime. |
Lopu repository reviewLopu reviewed this PR against develop as Thingtime's principal PR and repository manager. Using Claude Opus 5. Lopu found no justified local change to publish from this review pass. Lopu review β PR #613
What I comparedFull head-vs-base diff in the PR worktree, then read the surrounding
Check stateAll checks green on this exact head β nothing failing, cancelled, or timed out, CodeQL: the trusted snapshot for this head is empty ( Key findingsCorrect, and worth recording as suchThese are the places where this kind of change usually goes wrong, and each is
Hardening item (owner's call β not a live exploit)A Why this is a hardening item and not a finding I'd block on: the exposure needs If you want it closed, the natural chokepoint is the write path, not the Notes
Changes madeNone. Everything I checked was already correct, and the one item I'd Validation runDependencies installed in the worktree (
295 passing, 0 failing. Lint and typecheck are already covered green on this One process note for the record: an initial run of HandoverA cooperative handover was requested during this session. PR #613 is complete β |
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 02:44 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 03:31 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π΅οΈ Lopu review β #613Read the full head ( Checks are green (23 pass / 63 skipping / 0 failing), and the CodeQL snapshot for this exact head is empty β nothing to fix or dispose there. What I specifically verified holdsThis PR adds a bearer-secret read path, a cookie-free mutating GET surface, and cross-owner write grants β three things that each deserve their own paranoia. All of these check out:
Two details I want to call out as genuinely nice: Changes I pushed to the worktreeBoth are in this PR's new code, and both are divergences from what
Validation
The live-stack Non-blocking observations
Happy to approve once you're comfortable with the two bridge fixes. π β Lopu |
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 03:47 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 04:00 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π΅οΈ Lopu review β #613Read the full head ( Checks are green (23 pass / 63 skipping / 0 failing), and I confirmed the API-suite / build+unit-test run Verdict: sound. One real defect found and fixed in the worktree. The defect π
It isn't a hint on the normal routes β it's a compare-and-swap. What makes this worth fixing rather than noting is what this branch adds. Before custom audiences, a thing had exactly one writer and the guard was near-decorative on a bridge aimed at browse-only agents.
Left alone deliberately: What I chased to its enforcement pointRather than trusting the (genuinely excellent) comments, I went after the places this stack would fail silently and badly:
Validation
Fix is uncommitted in the review worktree for the trusted publisher to push to this PR's head. |
π΅οΈπ Lopu review β #613: the picker offered a secret link the owner could never getRead the full head ( Checks are green on this exact head (47 success / 145 skipping / 1 neutral / zero failing), including API suite, Build + typecheck ratchet + unit tests, CodeQL and both Analyze jobs; I confirmed run Verdict: sound. One real defect, found and fixed. The defectThe picker's middle baseline is "π΅οΈ + secret link", hinted "Anyone holding its hidden link can also view".
But const hiddenLink = post.visibility === 'hidden' && post.linkKey ? β¦ : null;
The fix β derive from the key, not the nameconst hiddenLink = post.linkKey ? `${permalinkPath}?key=${encodeURIComponent(post.linkKey)}` : null;
I added What I chased to its enforcement point and left alone
Validation
Fix is uncommitted in the review worktree for the trusted publisher to push to this PR's exact head branch. |
π Lopu review β #613: cancelling the audience picker published private postsRead the full head ( Checks are green on this exact head (23 pass / zero failing, rest Verdict: sound. One real defect, found and fixed. The defect π β πThe composer flips if (!audienceAppliedRef.current) setVisibility((c) => (c === 'custom' ? 'public' : c));For a new post that's correct β
Same for π₯ Friends, π¨βπ©βπ§ Family and π΅οΈ Hidden. Cancel is the one gesture that should What convinced me it's an oversight rather than a decision: The fix β restore, don't defaultconst audiencePreviousVisibilityRef = React.useRef<PostVisibility>(editPost?.visibility || 'public');Captured on the way into the picker, restored on abandon. Narrower than it looks: the ref Added What I chased and left alone
One thing I'd like your call on βοΈThe picker is lossy on acl entries it doesn't model. I didn't fix it, because the naive fix is worse: preserving those entries reintroduces them Smaller notes, none blocking: Fix is uncommitted in the review worktree for the trusted publisher to push to this PR's exact head branch. A Lopu handover was requested mid-review β this PR was finished, validated and reported in full before ending. β Lopu |
|
Lopu review β head No blocking findings, and no changes made β I didn't find a defect that justified an edit, and I'd rather leave the branch alone than manufacture a commit. Check / CodeQL state
What I actually tried to breakThe audience model is the part of this stack that has to be right, so I read the enforcement paths rather than the diff:
Two notes, not defects
ValidationInstalled deps in the worktree and ran every suite covering the touched areas (the new test files are all picked up by existing globs β
π€ Posted by Lopu, Thingtime's review agent. |
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 11:08 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 11:17 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 12:58 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
π€ Lopu detected an out-of-date PR branchStatus: Work detected β Lopu is taking ownership. Current phase: Entering Lopu's serialized PR-resolution queue. Estimated completion: around 13:15 UTC (~20 minutes; this adjusts as the queue moves). Next automatic check-in: within 10 minutes while work remains active. You can stay on this PR; there is no need to find the Actions run. Time conversion (UTC source)
Los Angeles and Melbourne use their real IANA time zones, so PDT/PST and AEST/AEDT offsets change automatically. Lopu queue and PR pulse
Related PR context
Exact branch pair: Timeline
|
|
π€ Lopu review β PR #613 (head I compared the full head against its base (43 files, +3059/β161) and concentrated on the surfaces where this PR can actually cause harm rather than on style. Verdict: no blocking defect. I made no code changes β nothing justified one. What holds up
ValidationThis worktree has no
To be explicit about that last row so nobody chases it: Non-blocking notes (deliberately not changed here)
CodeQL is green and the trusted alert snapshot for this head is empty, so its disposition file stays |
|
π¦ Lopu review β PR #613 (head Reviewed for correctness, security, crashes, data/API compatibility and test Security fences I probed, and what closes each
I also checked the two documented link-lifecycle promises against the code: Registration verified in all three required places, so the new endpoints won't
|
π€ Lopu review β #613 (hidden π΅οΈ links + GET bridge + custom audiences π)Compared Verdict: approve as-is. No code changes made. Checks are fully green on this head (23 pass / 63 skipped β the skips are the control-plane's own conditional jobs, not suppressed verification), and the CodeQL snapshot for this head is empty, so there was nothing to fix or dispose. Things I specifically tried to break, and couldn't
One hardening item for your call (not a live exploit)
I did not change this, because the exposure requires the owner to voluntarily paste a foreign group id into a hand-authored acl: the picker only offers If you do want it closed, the write path is the right chokepoint β validating group ownership in Two smaller notes: ValidationInstalled deps in the worktree and ran the suites covering this diff as CI runs them ( For the record: a first pass at A cooperative handover was requested mid-session, so #613 is the only PR reviewed here β #295 is left untouched for the queued continuation. |
|
β
Promoted to An earlier run stood aside on this PR; that verdict no longer applies. |
|
β
Promotion #680 has verified source lineage at current |
|
π Promotion PR for |
β Develop S3 preview ready
2c9afc2ddevelopThe alias passed the develop bucket CORS preflight and a final live PR/SHA fence.
Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup.
Why this PR exists
PR #413 (hidden π΅οΈ link-key visibility + the
/api/v1/getPAT bridge) and PR #431 (custom audiences π with capability grants + groups) both show as merged, but they were merged into their stacked base branches a minute after PR #411 (that base) had already merged intodevelop. Neither merge commit is an ancestor ofdevelopormain: develop has no/api/v1/get, nott:custom, no/api/v1/groups, noCustomAudienceModal, andVISIBILITIESis still the four-circle list. This branch re-lands the published feature stack (origin/claude/hidden-links-get-bridge@ d826cf6, which contains #431) onto currentdevelop.What landed on the branch (unchanged from #413 / #431)
['tt:hidden','tt:user']+ rotating owner-onlylinkKey):?key=URLs, anonymous keyed viewers, "Copy hidden link π΅οΈ", key rotation on every entry into hidden./api/v1/getgated per token bymeta.allowGet("Works via GET links π"): cookie-free,private, no-store, mirrors every op's scopes and accounting.tt:custommarker + baseline +tt:user/<name>[/comment|/write]andtt:group/<id>[β¦]capability grants (write β comment β read), the audience picker, groups managed via/api/v1/groups, grant-aware feed/search circles, "Hidden only π΅οΈ" token fence mode.verify-pat-tokens.mjssections G/H/I cover the three features.Merge with develop (this PR's own work)
Eight files conflicted; resolutions:
things.ts: kept both import sets (groupIdsOf+emitMentionNotifications/NotificationActor), kept develop's exportedpatVisibilityBlocksAclwith the branch's bucket comment, keptREQUESTABLE_VISIBILITIESfor circle filtering alongside develop's tag normalization, and kept the custom-audience shared-editing branch and develop'sexpectedUpdatedAtoptimistic-concurrency check inupdateThing.patTokens.ts:allowGetand develop'screatedVia: 'chatgpt-oauth'both survive in the mint input and the stored row.PostCard.tsx: develop'sonChanged(post.id, β¦)contract applied to the branch's audience/linkKey updates (the feed contract test counts every call).PostComposer.tsx: custom acl on edit + develop's attachment-panel ids.share/_share.tsx:withLinkKeysviewer + develop'stags.TESTING.md: both sides' checklist items (dropped the branch's stale five-circle duplicate). Raycast converter and the deprecated API CORS shim: develop's versions.patScopes.test.ts: the visibility catalog test now expects the four modes (hiddenis this stack's addition).Verification
pnpm exec eslintclean on every conflict-resolved file.π€ Generated with Claude Code