feat: F5.2 sign-in with the dashboard's ticket and the service way

Server F checks the dashboard's entry ticket as itsdangerous does
(tools/ticket-oracle: tickets made by itsdangerous 2.2.0 itself), redeems
it at /__auth for a session cookie (only the key's SHA-256 is kept) and
decides at /sync by the session's grant: none, read or write; a frozen
project is read by everyone; every refusal names its reason (one list in
packages/protocol/src/sync.ts). A WebSocket from a page of another origin
is refused. /svc/* with the service key makes, deletes, freezes and
thaws projects, ends a person's grant or all their sessions (connections
let go) and lists the sessions. Snapshots, restoring and copying move to
F6 (they need the canonical doc.json and an epoch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 13:56:45 +00:00
co-authored by Claude Opus 5.5
parent 88db6eff7e
commit 5ded84cb33
24 changed files with 1794 additions and 151 deletions
+1
View File
@@ -1,6 +1,7 @@
// Public surface of the wire protocol.
export * from './host.js';
export * from './sim.js';
export * from './sync.js';
export * from './terminal.js';
export * from './dns.js';
export * from './echo.js';
+37
View File
@@ -0,0 +1,37 @@
// What F tells an editor about its place in a shared document (/sync, plan
// §5, §8.5) besides the document itself — the one list both sides import:
// why a connection is refused, why F lets it go, and the message that its
// access changed while it is connected (a project frozen or thawed).
/** Why F refuses a connection to a project (Hocuspocus' permission-denied, with this as its reason). */
export const SYNC_REFUSALS = [
/** The name is not a project's id. */
'notAProject',
/** No session (no cookie, or it ran out): the dashboard issues a new ticket. */
'notSignedIn',
/** A session, but no access to this project (never had one, or it was revoked). */
'noAccess',
/** F does not know the project (deleted). */
'unknownProject',
] as const;
export type SyncRefusal = (typeof SYNC_REFUSALS)[number];
/** Why F closes a connection (Hocuspocus' close, with this as its reason). */
export const SYNC_CLOSINGS = [
/** The person's access to the project was taken away. */
'revoked',
/** The project was deleted. */
'deleted',
] as const;
export type SyncClosing = (typeof SYNC_CLOSINGS)[number];
export const SYNC_PROTOCOL_VERSION = 1;
/** A stateless message from F: the connection's access now (sent when it changes on a connection that stays). */
export interface AccessMessage {
readonly v: 1;
readonly kind: 'access';
readonly readOnly: boolean;
/** The project is frozen (its deadline passed): no one writes, whatever their level. */
readonly frozen: boolean;
}