Dated source reference. Current release status: handoff state. Full original relative paths are in the developer archive.
Private collaboration service contract
Version0.3.0.dev2. This service authorizes browser roster editing; it is separate from the public in-game OpenHoops HTTPv1 API. It never starts or attaches to the game.
All JSON responses and private downloads use Cache-Control: no-store. Use an HttpOnly account session cookie or Authorization: Bearer <capability> for project requests. Cookie writes require the exact same Origin and X-OpenHoops-CSRF returned by account/me. All account POSTs additionally require X-OpenHoops-Request: 1. Client-supplied internal account headers are stripped. Validation errors are422; stale values/revisions are409; revoked or insufficient access is403. The client must retain a failed draft and review conflicts, not silently force an overwrite.
| Route | Method | Purpose |
|---|---|---|
/rostereditor/api/status |
GET | Version and service identity; not writer readiness |
/rostereditor/api/projects |
POST | Create a private workspace; signed-in accounts may select a starting roster |
/rostereditor/api/projects/{id}/info |
GET | Title, role, head revision, row epoch and roster inventory |
/rostereditor/api/projects/{id}/read |
POST | Paged grid, catalogs, roster fields, health, conference report, references, comparisons, donor-team inspection and CSV export |
/rostereditor/api/projects/{id}/command |
POST | Idempotent roster mutation |
/rostereditor/api/projects/{id}/history |
GET | Latest100 revisions; before pages backward |
/rostereditor/api/projects/{id}/revision |
GET | revision and optional start; immutable change details in pages of100 |
/rostereditor/api/projects/{id}/export |
GET | Validated ROS/companion ZIP; optional revision downloads any saved version |
/rostereditor/api/projects/{id}/comments |
GET/POST | Read, add and resolve comments |
/rostereditor/api/projects/{id}/access |
GET/POST | Owner-only list, creation and revocation of individual links |
/rostereditor/api/projects/{id}/settings |
POST | Owner-only title, viewer-download policy and archive/reopen |
/rostereditor/api/projects/{id}/socket |
GET/upgrade | Authenticated live revision/presence notifications |
Starting rosters
Account creation accepts {id,title,starter}. starter is optional/empty for manual import, or one of base-default, base-blank, expanded-default, expanded-blank. The creation reservation stores its choice and rejects another choice under the same ID. Retry pending setup with the original ID and starter. Completion creates revision1 with the checked writer; interrupted setup remains retryable in the library. Setup-key/owner-proof creation retains its existing empty-workspace behavior.
An editor or owner can initialize an existing empty workspace using a normal checked command: {id,actor,revision:0,epoch:0,operation:{op:"starter",starter:"expanded-blank"}}. Normal command receipts, archive/recovery/access guards and exact revision checks apply. No template operation replaces an existing roster. The archive stays in private R2 and its size/SHA are verified before import. There is no public roster asset route. Contents, identifiers and evidence.
Accounts and workspace library
| Route | Method | Purpose |
|---|---|---|
/rostereditor/api/account/me |
GET | Signed-in profile and current CSRF token, or null |
/rostereditor/api/account/start |
POST | Send an eight-digit email code: email, optional new-account name, remember |
/rostereditor/api/account/finish |
POST | Verify email, challenge, code; sets cookie, returns profile/CSRF and first-signup recovery key |
/rostereditor/api/account/recover |
POST | email, offline recovery; rotates recovery key and revokes prior sessions |
/rostereditor/api/account/profile |
POST | Display name or rotateRecovery; key rotation requires recent sign-in |
/rostereditor/api/account/logout |
POST | Sign out this session; optional all: true |
/rostereditor/api/account/library |
GET | Account's authoritative accessible-workspace summaries and invitations |
/rostereditor/api/account/preferences |
POST | Per-account project, folder, favorite |
/rostereditor/api/account/dismiss |
POST | Dismiss inbox invitation by id |
/rostereditor/api/account/forget |
POST | Remove inaccessible shortcut by project; accessible projects must be left/archived |
/rostereditor/api/projects/{id}/members |
GET | Owner-only named membership/invitation list |
/rostereditor/api/projects/{id}/membership |
POST | save, leave, invite, cancel-invite, role, remove |
/rostereditor/api/projects/{id}/join |
POST | Accept/decline a named invitation: id, action |
/rostereditor/api/projects/{id}/backup |
GET | Owner-only streaming complete .ohworkspace.gz backup |
/rostereditor/api/projects/{id}/recovery |
POST | Owner-only resumable recovery into a separate empty workspace |
An email account is created only after code verification. Codes expire in ten minutes, allow five guesses and cannot be reused. Sessions last30days with remember enabled or12hours otherwise; only token hashes are stored. Production cookies are Secure/HttpOnly/SameSite=Lax, scoped to /rostereditor. Verified email control identifies an account; display names are user chosen. Normal sign-in sends a fresh code rather than a password. An offline key is optional recovery for lost inbox access; using it rotates the key and signs out old sessions. Never log codes, cookies or recovery keys.
Signed-in creation requires id (16–100 URL-safe characters, typically a UUID) and title, with the same ID reused after an interrupted request. It returns id/role without a share secret. An account-scoped reservation makes retries initialize the same project and exposes unfinished setup in the dashboard. Named membership is authoritative at the project, while library folders/favorites belong to each account. Public bounds:10 creations/day,50 owned workspaces,200 library entries,30 invitations/day/account,20 email starts/IP/hour and30 verification/recovery attempts/IP/15minutes; a recipient has a60-second resend guard and at most five recent outstanding challenges/15minutes. These finite bounds are not unlimited-service guarantees.
Invitations use verified email addresses with owner/editor/viewer roles and expire after seven days. They appear in the inbox before or after first signup. Only that email account can accept/decline them. Invitations and library notifications use a durable outbox. Role changes, removals, signout and account recovery revoke matching live access; HTTP authorization is checked on every command. At least one named owner or active owner link must remain. Archive blocks roster changes but preserves data/history/access and can be reopened by an owner.
membership with action: "save" explicitly adds an opened private link to the signed-in library. An owner link grants independent named ownership; saved editor/viewer access stays tied to that link, so revocation still removes access. Existing links continue working without accounts. New explicit named invitations create independent memberships. Accounts are not inferred from local browser shortcuts.
Complete workspace backup
The gzip file contains version1 newline-delimited openhoops-workspace records: begin/header, SQL rows, immutable object-start/chunk/end records and a final checksum chain. It includes original/current files, all saved revisions and patches, receipts, discussion, memberships, invitations and private-link permission hashes. Live share secrets, account sessions and recovery keys are absent. Handle backups privately because they contain roster data, discussion and permission identities.
Recovery uses action: "begin", header, restoreAccess; then action: "append", job id, zero-based start and an array of original record strings. status returns the active checkpoint. Retrying identical records is safe; changed records, gaps, missing objects and checksum failures refuse publication. Batches are at most100 records, each record at most8MiB, object parts5MiB and each saved object at most40MiB. Header counts are bounded to1,000,000 rows/100,000 objects. Browser batches are bounded to8MiB. Exhaustive maximum-payload memory certification is not claimed.
The target stays unavailable for roster edits until final verification commits its head/history/receipts atomically. Retries recognize a completed restore after a lost final response or reload. Source workspaces remain unchanged. Default recovery is a private copy that retains permission records without activating them. restoreAccess: true explicitly reactivates saved memberships and link hashes. The recovered copy has a new project ID/URL; old invitation URLs continue pointing to the source. Hashes cannot reconstruct lost link secrets. A recovery owner remains present. This is workspace backup/recovery, not a service-wide administrator/account database restore or automated retention policy.
Roster and legacy capability contract
Create-project JSON accepts title and optional ownerProject; the Authorization header contains the setup key or that project's owner token. The response contains the new project ID and owner token. There is no anonymous public upload or project listing.
Read JSON uses op. Supported operations are grid, catalog, roster_fields, health, conferences, references, review, compare_roster, inspect_roster and export_csv. The response wraps result, revision and epoch. User-supplied snapshots cannot replace server state. Comparisons use the immutable original or another explicitly uploaded ROS/companion pair and never mutate the head.
Grid parameters: section, start, limit, optional columns, term, conditions, sort, descending. The result includes the full field descriptor inventory, requested columns, physical row IDs, cell values, decoding errors, total and matched row counts. Metadata cells distinguish {state:"unset"}, {state:"null",value:null} and {state:"value",value:...}. Native decoding errors are read-only. IDs are physical record IDs; row moves change the epoch. Standard college fields use the shared vocabulary.
Filter conditions are {field,operator,value} with at most16 clauses. Operators are eq, ne, contains, gt, ge, lt, le, unset, unknown. Number/integer comparisons require a finite number; text comparison preserves identifiers such as 001 and uses case-folded text. unset and unknown test metadata state. Bulk arithmetic requires an actual numeric field/value and refuses numeric-looking strings.
export_csv returns CSV beginning ID,ROS_SHA256,EXTRAS_SHA256,.... Each row carries the current native serialization hash and metadata fingerprint. import_csv validates both against the saved head before applying any row. Thus a metadata-only peer edit invalidates an old spreadsheet even when native bytes are unchanged. Native-only legacy CSV beginning ID,ROS_SHA256,... is accepted; metadata columns require a fresh export with EXTRAS_SHA256. Metadata cell tokens are @unset and @unknown; prefix literal text beginning @ with another @. Existing custom numeric/boolean types apply to unset rows too. A single invalid row refuses the complete import.
A command has id, actor:{client,name}, revision, epoch, operation. Reuse the identical command ID and payload after an interrupted save. Changing a used payload is refused. Supported operations: upload, cells, bulk, import_csv, extras, clone_team, clone_rows, move_rows, delete_rows, import_team, script, undo, restore.
cells identifies one section and an array of {row,field,expected,state,value} changes. expected is the complete previously observed cell object. All values validate together before one saved commit. bulk supports set, add, multiply, unset, unknown with all, filtered or selected scope. Arithmetic requires known numeric values. Native Division10–15 is refused because the observed original conference lookup dispatch is unsupported; college classifications belong in metadata.
Uploads use roster_zlib containing base64 zlib-compressed native bytes (or raw base64 roster), plus an optional matching companion JSON string/object. Alternatively use archive containing a base64 exported roster ZIP, bounded to16MiB compressed with exactly one ROS and its optional correctly named companion; unsafe/duplicate filenames, extra files, damaged archives and mismatched metadata are refused. Public browsers compress before upload. An existing project cannot be silently replaced with another file; create a separate project. Team import, donor inspection and comparisons accept an uploaded pair or roster ZIP as source. inspect_roster returns donor inventory and named team row choices without changing the current roster.
Comments with a cell anchor may include its observed epoch; an earlier layout is refused. Live comments notifications refresh the list without replacing a draft in the browser composer.
The browser may attach historyIntent to a command for its local Undo/Redo bookkeeping. It does not grant server privileges or change mutation semantics, and is part of the immutable payload on retries. Other clients may omit it.
WebSockets offer openhoops-v1 and cap.<capability>; the server selects only openhoops-v1. Signed-in sockets instead offer only openhoops-v1 and use the session cookie. It validates authorization before accepting. Server messages include ready, revision, presence, comments, settings, access and pong. Presence sends {type:"presence",actor,selection:{section,row,field}|null}. Presence is ephemeral. Signed-in author labels are derived from the verified account; anonymous link labels remain unverified. Roster changes use durable HTTP commands. Reconnects reload the head. Revoked links lose both HTTP and socket access.
Snapshots and patches are internal, immutable storage representations. They are not public ROS formats or stable game/API contracts. Use exported ROS/companion pairs for game transfer or complete workspace backups for editor recovery. Custom metadata never gains native game behavior merely by being stored in this editor.
Named revisions store their verified account ownership independently from client IDs. An editor may undo their own named changes from another client/device; copying a client ID does not impersonate a different account. History returns mine and hides raw author email. Older anonymous history retains its original client-scoped behavior. Owner privileges remain explicit.