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 ready
The 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. |
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. PR #613 β feat(things,tokens): re-land hidden π΅οΈ links + PAT GET bridge + custom audiences πHead Checks: all green. No CodeQL alerts on this head. What I comparedFull head-vs-base diff, weighted heavily toward the new trust boundary. The GET bridge is well-defendedI went looking for the failure modes this design invites, and each one is already closed:
Prototype pollution deserves specific mention, because this is where a query-param-driven arg Value coercion is also conservative in the right direction: only Registration is correct
Test failures I investigated and cleared
This PR does modify I ran the same test file from PR #611's worktree, which does not touch any of those three files Validation
FindingsNo changes needed. For a feature that puts a credential in a query string, the threat model Notes (no change made)
RecommendationMerge-ready. |
π΅οΈ Lopu β went looking for holes in the bridge and the ACL model; found two notes, no defectsThis is the highest-risk PR in the batch β a bearer secret, a new capability model, and CRUD over plain GET β so I read the security surfaces end to end rather than skimming. Recording what I actually verified, so a future reader doesn't have to redo it. The GET bridge is CSRF-safe, and for a non-obvious reasonThe auth cookie is Other things I checked and found already closed:
The hidden-link lifecycle192 bits of entropy, and the state machine is right: entering Adding
|
π€ 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 06:41 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 β one open question on
|
π€ Lopu live PR updateStatus: βͺοΈ Resolver finished; a newer conflict remains Current phase: The next detector event will own the current branch state Estimated completion: Done β no further active-work ETA. 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
Technical run details β optional; this comment is the human-facing source of truth. |
|
π€ Lopu review β green. Reviewed This is the riskiest thing in the current review batch by construction β a credential in a URL,
The prototype-pollution handling is worth singling out, because this is exactly where a Value coercion errs the right way too: only On the 4
|
|
π Lopu preserved the live No stale merge commit was pushed. View this guarded attempt. |
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