Repository navigation
Signed-in visitors: a public app's players sign in with a key (app-repos work item 4) - #624
Open
Evanfeenstra wants to merge 4 commits into
Open
Evanfeenstra wants to merge 4 commits into
Evanfeenstra wants to merge 4 commits into
Conversation
…h a key plans/app-repos.md work item 4 (§5). A public app's visitor may sign in and is then that key's actor on this server: its own app/state, channels, rows of the app's declared graph types, and launches of granted workflows that spend nothing. - auth.ts: "is this session the key's equal" is an allowlist (no role, or admin), so a new role opens nothing. Role and scope `visitor`; the visitor's session in its own cookie (strut_visitor) and as a bearer token (or x-strut-visitor); the challenge, a minute, good once, bound to the host. - src/visitors.ts: the verifier seam (ecdsa-p256, ecdsa-secp256k1, bip340; webauthn named), the methods (key, ticket, lnurl, nostr), LNURL-auth's pending logins, NIP-98, per-actor daily limits with a per-address budget on top, and §5.1's rules over the anonymous visitor's. - /visitors, /visitors/challenge, /visitors/session, /visitors/lnurl; the gate's visitor scope (GET /auth/session and its app's channel ticket); /public/mcp as the signed-in visitor. - serve.yaml's signIn, visitorKey and limits, written as the public view's `visitors`. - The kit's useVisitor() over @strut/kit/visitor; the host page's sign-in sheet and providers (a WebCrypto key in IndexedDB, Nostr, LNURL by QR).
…ach visitor's daily limits The record's `visitors` (plans/app-repos.md §5) from the Publish dialog, for a strut that is not serving (a served box's serve.yaml writes it). The helpers keep the record as the server does: no method, no `visitors`, so a Save leaves the draft unchanged; the defaults and the method list are held equal to the server's by public-view.test.ts.
# Conflicts: # AGENTS.md # plans/app-repos.md
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.
Work item 4 of
plans/app-repos.md(§5): a visitor of a public app may sign in by signing strut's challenge with a key, and is then that key's actor on this server. Parts A and B both land here: the seam,keyandticket, then LNURL-auth and Nostr.What a signed-in visitor gets (§5.1)
On
POST /public/mcp, over the anonymous visitor's rules:app/state, kept across visits, andapp/report-error;POST /apps/:name/channel-ticketnames the visitor, for a public app at the version the public view serves;data.graphtypes, each KEPT as astep:<type>run with the visitor as actor, andgraph/findon those types;run/launchof a granted workflow whose closure spends nothing (runnableCan), as the visitor, and the run reads of its own runs of it. A spending workflow launches only if it isrunnable, through the public run door.Refused, as for any app page:
job/start, authoring,llm/agent/exec. A visitor is not a member: past what is open to anyone, the gate passesGET /auth/sessionand its app's channel ticket, and every other route is 403 (a sweep, cookie and bearer).The trap, fixed first
auth.tsdecided "is this session the key's equal" asrole !== "member".fullSessionis now an allowlist: the key login (no role) oradmin. A visitor's token copied into the operator's cookie opens nothing (tested).The seam
GET /visitors/challenge→{ challenge, exp, methods, visitorKey }. The challenge isstrut-visitor.<claims>.<hmac>, claims{ host, visitorKey, nonce, exp }, 60 s, good once, bound to the request's host. It is spent only after its signature holds.POST /visitors/session { challenge, method, … }→ the visitor's cookie and{ actor, token, exp };DELETEsigns out;GET /visitorssays the methods and who is signed in.src/visitors.ts):ecdsa-p256→p256:<compressed key>;ecdsa-secp256k1andbip340→secp:<x-only key>, so one secp256k1 key is one person whichever way it signed.webauthnis named, not built.key: a non-extractable WebCrypto P-256 key in this site's IndexedDB;ticket:POST /auth/ticket { actor, role: "visitor" }, traded atPOST /auth/session, which now returns the token for a visitor;lnurl: LUD-04 by QR. The wallet calls backGET /visitors/lnurl, and the page takes the session for itsk1with its challenge;nostr: NIP-07 signs a NIP-98 event over the body's hash. Root key only: not offered undervisitorKey: domain.serve.yaml'slimitsover strut's defaults, and a fixed per-address budget on top.serve.yamlgainssignIn,visitorKey,limits, written as the public view'svisitors.The kit and the host
useVisitor()→{ available, methods, actor, signIn(), signOut() }, over@strut/kit/visitor(experimental["strut/visitor"]inui/initialize,strut/visitor/sign-inand/sign-out), the wallet's pattern. On a change the page'suseAppStateand queries are read again as the new viewer's.web/src/visitor-signin.ts,SignInSheet.tsx) draws the sheet over the frame and holds the session. It is one tap when this browser's key is all there is. It keeps the token in this site's localStorage and sends it when the cookie is dropped (Ruling 4). The app never sees a key.Choices beyond the plan
strut_visitor), so signing in on a box never replaces the operator's console session on the same origin. It lasts 30 days.POST /public/mcp,Authorizationalready carries a priced launch's credit token (L402 …). So the host page sends the visitor's token there asx-strut-visitor, which the server accepts besideAuthorization: Bearer.inputandkeyalone: no job, conversation, files or params. A visitor cannot cancel a run.graph/create-nodealone for a leaderboard.ticketneeds no entry insignIn: minting one already takes the key.limitssays. Counted in memory, so a restart starts the day over./visitors/*share the/publicper-address request budget (1200/min): the routes are open, and an LNURL login keeps state (in memory, at most 10 000 pending, 5 minutes each).visitors. A served box'sserve.yamlwrites it instead.Known limits
x-forwarded-fornames the address, so the per-address budget is a budget, not a boundary (as for/publictoday).Tests
src/visitors.test.ts(19). The seam: verifiers, root vs per-domain child keys, NIP-98, LNURL logins, limits, the rules. A public app signed in: two keys are two actors with their ownapp/state; the bearer token andx-strut-visitorwork as the cookie does; a replayed challenge or one for another host is refused, and a bad signature does not burn the challenge. One root key is one actor on two hosts, and withvisitorKey: domaineach host's derived child (through LNURL-auth end to end) is another, and Nostr is not offered. A non-spending launch reads back to its visitor alone; a spending one outsiderunnableis refused; per-actor limits hold; no Jamie, no other app. On Ladybug: rows kept with their actor, read by every visitor, undeclared types refused.createStrut.test.ts: a visitor's session sweep (403 on every route outside its scope, as cookie, bearer andx-strut-visitor); the 401, peer and member sweeps updated for the open/visitors/*routes.kit/src/visitor/visitor.test.ts,web/src/page.test.ts,src/packages/serve.test.ts,web/src/public-view.test.ts(the Sign-in section's helpers; the defaults and methods held equal to the server's).npm test: 4349 tests, 0 fail, 54 skipped. Webtscandvite buildclean.Checked in a browser
A served strut (
STRUT_SERVE=file:a fixture package whose app usesuseVisitoranduseAppState,signIn: [key, lnurl]) in the machine's Chrome via Playwright. The built-in pane was hidden, and the app frame is an opaque srcdoc it cannot read into. Results:+1stays local. Sign in shows the sheet; "Continue on this device" signs in asp256:….count: 2. With the cookies cleared, the token alone carries on.The Public view tab's Sign-in section, on a scratch strut: ticking the browser key and Nostr, 500 writes a day, and a key per domain, then Save.
GET /visitorsthen offeredkeyalone withvisitorKey: domain, Nostr dropped by the server; a fresh browser showed the saved state.Not touched: #622's files (item 3).
plans/app-repos.md's status block will need a trivial merge with #622's.