Skip to content

feat(auth): sign the device app label into register and login - #128

Merged
kinqsradio merged 1 commit into
mainfrom
feat/device-app-label
Nov 11, 2026
Merged

kinqsradio merged 1 commit into
mainfrom
feat/device-app-label

Conversation

@kinqsradio

Copy link
Copy Markdown
Member

Summary

  • Device may name its app (planner/engineering); it signs device-register/v2 and device-login/v2 (v1 bytes + length-prefixed trimmed name, platform, app).
  • Server verifies v2 exactly when the request has an app, else v1; stores and lists the label (migration 0019, pending sign-up and second-factor state carry it).
  • A known device signing in again keeps its stored name and label.
  • Device client: ClientConfig.app; contracts bindings regenerated.
  • Part 1 of the approval work; the engine#73 approval binding and the approve/deny/remove API follow as separate PRs.

Testing

  • Full gate with Postgres: fmt, clippy, structure, wasm32, workspace tests (947 pass).
  • Mutation checks: 10 mutations, 9 killed; the survivor (client name trim) is equivalent since the server trims.
  • Not tested: none

Co-authors

none

AI usage

  • Written by hand, with no AI
  • Harness and version: Claude Code 2.1.317
  • Models: claude-sonnet-5-5 (all)
  • Effort: medium
  • Human role: set scope and reviews the signing change
  • Sources: none

Checklist

  • Works on Windows, macOS and Linux
  • docs/architecture.md with any rule change; generated wire types with any contract change
  • No copied source, secrets, credentials or personal data; no AI attribution in commits, title or branch

A device may name its app (planner or engineering). It then signs the
v2 register or login message, which adds its trimmed name, platform and
app to the v1 bytes. The server stores the label and lists it; a device
with no label signs the v1 messages as before.
let _turn = TURN.lock().await;
let server = server().await;
let email = email();
let upload = server.registration_upload(&email, b"pw").await;
// A new device signs in under the same rule and is listed with its label while pending.
let phone = DeviceKeys::generate().expect("keys");
let (status, phone_session) = server
.login_labelled(&email, b"pw", &phone, &labelled("Phone", "planner"))

// A known device that signs in again keeps what is stored; the claim does not rewrite it.
let (status, again) = server
.login_labelled(&email, b"pw", &keys, &labelled("Renamed", "planner"))
let _turn = TURN.lock().await;
let server = server().await;
let email = email();
let upload = server.registration_upload(&email, b"pw").await;
},
];
for (i, case) in cases.iter().enumerate() {
let answer = server.login_labelled(&email, b"pw", &phone, case).await;
let server = server().await;
let email = email();
let keys = DeviceKeys::generate().expect("keys");
let session = server.register(&email, b"pw", &keys).await;
let devices = server.device_list(&session).await;
assert_eq!(devices[0]["app"], Value::Null);
let phone = DeviceKeys::generate().expect("keys");
let signed_in = server.login(&email, b"pw", &phone).await;
let server = server().await;
let email = email();
let keys = DeviceKeys::generate().expect("keys");
let session = server.register(&email, b"pw", &keys).await;

// A new device with a label meets the factor; the label rides in the sealed state.
let phone = DeviceKeys::generate().expect("keys");
let (login, status, started) = server.login_start(&email, b"pw").await;
let (login, status, started) = server.login_start(&email, b"pw").await;
assert_eq!(status, StatusCode::OK, "{started}");
let finish = login
.finish(b"pw", &unb64(&started["credential_response"]))
@kinqsradio
kinqsradio merged commit bef2025 into main Nov 11, 2026
40 of 45 checks passed
@kinqsradio
kinqsradio deleted the feat/device-app-label branch November 11, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants