Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Identity

Identity

In development and tests

On this page

In jadpo dev, no email leaves your machine. It lands in the local inbox (The dev inbox), and its link and code work there. The link opens your frontend's page. An agent reads the token or code with jadpo email links <id> or jadpo email show <id>, and posts it to POST /auth/magic-link/confirm itself.

Most route tests don't sign in through a magic link. A request's as: signs the test in as a user directly (Acting as a user).

To test the sign-in itself, a route test asks for a link with POST /auth/magic-link, reads the email from email.sent, and follows its link or posts its code. It calls the generated routes with request { }, as it calls your own. Each takes a built-in input, named as the API record names it, which only a test, a fixture or a seed builds:

Route input: Its fields output
POST /auth/magic-link MagicLinkAsk address: Email, return_to: Text?, locale: Locale? none
POST /auth/magic-link/confirm MagicLinkConfirmation token: SignInToken?, address: Email?, code: SignInCode? MagicLinkConfirmed, its return_to: Text
POST /auth/magic-link/redeem MagicLinkCode address: Email, code: SignInCode MagicLinkToken, its token: ApiToken
  • The redeem route is there only when you declare api_token:.
  • Leave out any field that may be none, as a request's body may.
  • Every request a test sends carries your app's own Origin, as a browser's does, so the confirm route takes it.
  • The sign-in email lands in email.sent with its link's token in sign_in_token and its code in sign_in_code. Both are none on every other email, so a test never reads them out of the text.
  • Built anywhere but a test, a fixture or a seed, an input is type.not_record.
example
test "an emailed link signs a new person in" {    var asked: Response = request {        method: HttpMethod.post        path: "/auth/magic-link"        input: MagicLinkAsk {            address: "ada@notes.example"        }    }    assert asked.status == 202    match list.first(email.sent) {        none -> {            assert list.count(email.sent) == 1        }        some(sent) -> {            assert sent.to == "ada@notes.example"            var came_back: Response = request {                method: HttpMethod.post                path: "/auth/magic-link/confirm"                input: MagicLinkConfirmation {                    token: sent.sign_in_token                }            }            assert came_back.status == 200        }    }}test "the emailed code signs a new person in" {    var asked: Response = request {        method: HttpMethod.post        path: "/auth/magic-link"        input: MagicLinkAsk {            address: "ada@notes.example"        }    }    assert asked.status == 202    match list.first(email.sent) {        none -> {            assert list.count(email.sent) == 1        }        some(sent) -> {            var came_back: Response = request {                method: HttpMethod.post                path: "/auth/magic-link/confirm"                input: MagicLinkConfirmation {                    address: "ada@notes.example"                    code: sent.sign_in_code                }            }            assert came_back.status == 200        }    }}

To test on_first_sign_in:'s action, call it directly with a FirstSignIn you build, giving every field. A subscriber's action takes a sign-in event built the same way:

example
test "an unproved address is refused" {    assert create_member(FirstSignIn {        method: SignInMethod.magic_link        address: "ada@example.com"        address_verified: false        subject: none        name: none    }) fails SignUpClosed}

Passkeys in jadpo dev

Browsers allow passkeys on http://localhost without TLS. So in jadpo dev, a browser's own passkeys work against a browser_origin of http://localhost:<port>. An agent uses the browser's virtual authenticator, Chrome DevTools' WebAuthn panel or WebDriver's virtual authenticator commands, rather than a real device.

Passkeys in tests

A route test has test_passkeys, a software authenticator that drives the generated routes for real. Only tests can call it:

Operation Does
test_passkeys.register(user: Principal.user, name: Text) -> TestPasskey Adds a passkey for user through POST /auth/passkey/register/options and POST /auth/passkey/register, signed in as as: signs in, and gives the device holding it
test_passkeys.sign_in(passkey: TestPasskey) -> Response Signs in with it through the two sign-in routes and gives the second's Response

Each TestPasskey is a device of its own holding one passkey, made by the program running the tests, so only a test ever holds one. Called anywhere else, test_passkeys is semantic.request_invalid.

If a route refuses register, or the first step of sign_in, the test ends as a fault naming the route and its code, since the test meant to have a passkey. A refusal of the sign-in itself comes back in its Response, for the test to check.

example
test "a passkey signs Ada in" {    var ada: Principal.user = member_called("ada")    var phone: TestPasskey = test_passkeys.register(ada, "Phone")    var response: Response = test_passkeys.sign_in(phone)    assert response.status == 204}

Markdown for agents: /docs/in-development-and-tests.md.Built from Jadpo main at a707efb.