Skip to main content

jupyterlab_passkey_extension

GitHub Actions npm version PyPI version Total PyPI downloads JupyterLab 4 Brought To You By KOLOMOLO Donate PayPal

A generic passkey bridge for JupyterLab. It exposes the passkey (WebAuthn) capability of the user's browser or operating system to local clients that have no browser of their own - the JupyterLab terminal, a script, a CLI, or an AI agent working on the Jupyter server. The extension runs the browser-side ceremony and hands the result back to the requesting local process.

The bridge commands are purpose-agnostic and perform no cryptography of their own. Every caller supplies its own parameters, and a tool that wants a WebAuthn PRF value keeps all key handling to itself. The extension also ships a password vault built on the bridge - the one part that encrypts and stores secrets.

The way in is the CLI (jupyterlab-passkey, shipped with the package). Behind it sits an authenticated HTTP API - not a second way in, but the return path the browser posts its result to, and the relay's contract.

Features

  • Passkeys from a terminal - Windows Hello, Touch ID, or a security key, reachable from a process that has no browser. The user approves in their open tab; the result returns to the caller as a blocking call
  • Enroll and unlock - register a passkey (create) and assert it later (get)
  • Key material without a stored key - get --prf-salt yields a deterministic 32-byte WebAuthn PRF. The same credential and salt always return the same bytes, so a vault can derive its key from it and store none
  • Take a secret from the user, without it entering the transcript - passphrase prompts in the browser and prints only a reference to the staged value (keyctl:... or file:...), never the value. It never crosses the terminal, the shell history, a process argument, or the CLI itself
  • Hand a secret to the user, without leaving one behind - copy puts a value on the user's clipboard via a notification button. It rides a one-shot relay the server deletes as it reads, and never enters the notification itself
  • Show a code the page cannot be scraped for - show displays a value the user must read and type elsewhere - a one-time authenticator code - as a server-rendered, distorted image. The code is absent from the notification, the page DOM as text, and the accessibility tree, and an OCR pass on a screenshot still has to beat the distortion
  • Works detached - every command ignores SIGHUP, so a notification, popup or query raised by a backgrounded process keeps waiting for its click after the terminal that launched it has closed; the command that vault exec runs gets the SIGHUP handling the CLI was started with, so under nohup or setsid it outlives the terminal, and a hangup that arrived while vault exec still waited for the unlock click does not stop it: the command runs after the click, with no terminal for its output
  • A password vault in the lab - a sidebar panel, jupyterlab-passkey vault commands and a Python Vault class over one encrypted vault the Jupyter server keeps, unlocked with a passkey, with an unlock password and an authenticator code, or with the recovery passphrase
  • Purpose-agnostic bridge - the bridge commands do no cryptography and store no secret, and hold no opinion about what the passkey unlocks

Working with an AI agent

The passphrase and copy features are what make this usable when an AI agent is at the keyboard. An agent can run jupyterlab-passkey passphrase --once --prompt "GitHub token"; the user types the token into a browser dialog, and the agent receives a reference it hands to a consumer - so the token never appears in the agent's output, its context, or the session transcript. In the other direction, pass-cli get github/api ... | jupyterlab-passkey copy moves a secret from a vault to the user's clipboard through a pipe between two processes, so the bytes never pass through the agent either.

The CLI's --help is written to be read by an agent: every subcommand, bridge and vault alike, says what it prints and whether it waits for the browser, and carries worked examples.

The agent skill, .agents/skills/jupyterlab-passkey-extension/SKILL.md, points an agent at that help and adds only the rules the help cannot state: which commands keep a secret out of its output, and when to tell the user that a command waits. The skill ships in the repository and in the wheel, which installs it at <sys.prefix>/share/jupyter/agents/skills/jupyterlab-passkey-extension/SKILL.md. No agent reads that directory, and a wheel cannot write into the home directory, so one of the two links below is what makes it readable.

After pip install, with the Python that runs the lab:

mkdir -p ~/.agents/skills && ln -sfn "$(python -c 'import sys; print(sys.prefix)')/share/jupyter/agents/skills/jupyterlab-passkey-extension" ~/.agents/skills/jupyterlab-passkey-extension

From a clone, into Claude Code:

ln -sfn "$PWD/.agents/skills/jupyterlab-passkey-extension" ~/.claude/skills/jupyterlab-passkey-extension

How it works

A browser page can only talk back to the Jupyter server over HTTP, and a local process on that server cannot receive anything from the page directly. So the ceremony runs in the page, and its result returns through an authenticated endpoint that writes a relay - a keyctl key or an atomic 0600 file - the local client reads.

Inbound - a ceremony, browser to local client. passkey:passphrase takes the same shape, POSTing to /passphrase instead:

flowchart LR
    subgraph BROWSER["Browser - JupyterLab page"]
        direction TB
        TRIG["consumer triggers<br/>passkey:run"]
        CER["navigator.credentials<br/>get / create"]
        OS(["OS / authenticator<br/>Windows Hello, security key"])
        TRIG --> CER
        CER <--> OS
    end
    subgraph SERVER["Jupyter server"]
        direction TB
        EP["POST /result<br/>authenticated"]
        RELAY[("relay<br/>keyctl key or 0600 file")]
        EP --> RELAY
    end
    LOCAL["local client<br/>terminal / CLI / API"]
    CER -->|"POST JSON result"| EP
    RELAY -->|"reads, then shreds"| LOCAL

    style BROWSER stroke:#6b7280,stroke-width:3px
    style SERVER stroke:#6b7280,stroke-width:3px
    style TRIG stroke:#f59e0b,stroke-width:2px
    style CER stroke:#10b981,stroke-width:2px
    style OS stroke:#0284c7,stroke-width:2px
    style EP stroke:#10b981,stroke-width:2px
    style RELAY stroke:#3b82f6,stroke-width:2px
    style LOCAL stroke:#10b981,stroke-width:2px

Outbound - copy runs the same plumbing backwards: the local client writes and the server reads. The notification carries only a nonce (see Security), raised through the notifications extension - which is how every button in either direction gets on screen:

flowchart LR
    LOCAL2["local client<br/>CLI / agent"]
    subgraph SERVER2["Jupyter server"]
        direction TB
        ING["notifications ingest<br/>separate extension"]
        RELAY2[("relay<br/>keyctl key or 0600 file")]
        EP2["POST /secret<br/>authenticated"]
        RELAY2 -->|"reads, then unlinks"| EP2
    end
    subgraph BROWSER2["Browser - JupyterLab page"]
        direction TB
        BTN["notification button<br/>passkey:copy"]
        CLIP(["user's clipboard"])
        BTN --> CLIP
    end
    LOCAL2 -->|"stages the secret"| RELAY2
    LOCAL2 -->|"posts the nonce only"| ING
    ING -.->|"pushes to the page"| BTN
    BTN -->|"collects, once"| EP2
    EP2 -->|"the value"| BTN

    style BROWSER2 stroke:#6b7280,stroke-width:3px
    style SERVER2 stroke:#6b7280,stroke-width:3px
    style LOCAL2 stroke:#10b981,stroke-width:2px
    style ING stroke:#f59e0b,stroke-width:2px
    style RELAY2 stroke:#3b82f6,stroke-width:2px
    style EP2 stroke:#10b981,stroke-width:2px
    style BTN stroke:#f59e0b,stroke-width:2px
    style CLIP stroke:#0284c7,stroke-width:2px

Install

pip install jupyterlab_passkey_extension

Command line

jupyterlab-passkey ships with the package and is the intended way in. It turns a browser ceremony into a blocking local call: it posts the notification carrying the request, waits for your click, and prints the result. Every notification starts with who asks - Asked by <name>: - which is the project directory the command runs in, or the name the caller sets in JLAB_PASSKEY_CALLER; the name tells you which project raised the request and proves nothing. A caller needs to know none of the relay contract below, beyond the reference passphrase hands it.

Command Does Prints
create registers a new passkey its cred_id
get asserts a passkey the PRF (with --prf-salt) or the cred_id
passphrase prompts you for a secret in the browser a reference (keyctl:... or file:...), never the value
copy puts a secret on your clipboard nothing - it posts a button and returns
show shows a code as a scraper-resistant image nothing - it posts a button and returns

Exit status is the contract: 0 succeeded, 1 refused, timed out, or could not reach the server, 2 an argument the parser rejects. Only the result goes to stdout, so $(...) captures it clean.

Enroll a passkey, then derive key material from it:

cred_id=$(jupyterlab-passkey create --rp-id lab.example.com)

salt=$(head -c32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')
prf=$(jupyterlab-passkey get --rp-id lab.example.com --cred-id "$cred_id" --prf-salt "$salt")

Take a secret from the user and give it straight to a consumer, without it passing through the terminal:

# prints a reference - keyctl:<key> or file:<path> - which the consumer resolves
pass_ref=$(jupyterlab-passkey passphrase --prompt "Recovery passphrase") || exit 1
PASS_RECOVERY_REF="$pass_ref" pass-cli-open --ensure

# a token you paste rather than type - one field, no confirmation
tok_ref=$(jupyterlab-passkey passphrase --once --prompt "GitHub token") || exit 1

The || exit 1 matters: a prefix assignment does not propagate a command substitution's exit status, so PASS_RECOVERY_REF=$(jupyterlab-passkey passphrase) pass-cli-open would run the consumer with an empty reference after a timeout or a cancel.

Send a secret the other way, to the user's clipboard:

pass-cli get github/api --field password --quiet --no-clipboard \
  | jupyterlab-passkey copy --label "GitHub token"

copy reads from a file or stdin and returns immediately - the click is what collects it, one time only - so its exit code means posted, not copied. Add --block to wait until the browser collects the secret and delete it if that never happens; even then it means collected, not pasted, since the page writes the clipboard after the relay is already gone. It refuses a stdin that is a terminal, which would echo the secret into your scrollback.

Show a code the user must read and type elsewhere - an authenticator enrolment code - without the page being scrapeable for it:

printf '%s' "$totp_secret" | jupyterlab-passkey show --label "Authenticator code"

show renders the code to a distorted image server-side, so it never reaches the page as text - not the notification, the DOM, or the accessibility tree. The click shows the image; nothing is reported back.

Full flags in docs/cli-reference.md, or jupyterlab-passkey <command> --help.

Vault

The extension also keeps a password vault. The Jupyter server holds it: the entries sit in one encrypted file; a passkey, the unlock password with a code of an authenticator app, or the recovery passphrase unlocks it; and the panel, the CLI and Python code all reach the same unlocked vault through the server's authenticated API.

  • Encryption - every entry, names included, is one AES-256-GCM ciphertext under a random 32-byte data key. The data key is wrapped once per keyslot: each passkey slot with HKDF(PRF), the recovery slot and the unlock password slot with Scrypt of their secret (n=2^17, r=8, p=1). Any one slot decrypts the data key; the recovery slot is mandatory. The server keeps a key the unlock password slot gave only for a right code of the authenticator app: the code is a check by the server and takes no part in the encryption, so a person with a copy of the file and the unlock password decrypts it without a code
  • File - $XDG_DATA_HOME/jupyterlab-passkey/vault.json (~/.local/share/... by default), mode 0600, every change written to a temp file and renamed over it under a file lock; JLAB_PASSKEY_VAULT in the Jupyter server's environment moves it (a leading ~ is the home directory)
  • Passkeys are per hostname - a passkey opens the vault only from the hostname it was added at, or a subdomain of it. Add one per hostname you open JupyterLab under, or unlock with the unlock password and a code, or with the recovery passphrase
  • Unlock duration - unlockMinutes under Passkey Vault in the Settings Editor, default 240, range 1 to 1440; it applies to every client

The vault's design - file format, encryption, key holders, passkey hostnames, proofs and the REST API - is in docs/design-vault.md.

Panel

The Vault panel sits in the right sidebar; the sidebar setting moves it left. It creates the vault, unlocks it with a passkey, with the unlock password and a code, or with the recovery passphrase, and lists entries grouped by category; a locked vault shows no entry; the header's filter button shows a filter field. A row shows the entry's name and username; clicking it opens the entry in a read-only popup with Edit, Delete (asks first) and Close. The password shows as dots until the eye button is pressed. The eye asks a proof, and before the server returns the password it checks that proof: the PRF of a passkey request, which must open one of the vault's passkey slots, or - on a hostname with no passkey, or when the passkey does not answer - a code of the authenticator app, the unlock password or the recovery passphrase, typed in a row of the popup. For 60 seconds after the tab unlocked the vault or had a proof accepted, the eye asks no proof and shows the password at once; locking the vault in the tab or reloading the page ends that minute. The panel does not copy; vault copy and vault show in the CLI do. The cog icon opens the security view - the key holder and what it protects on this host, as short values whose tooltips explain them - and one Unlock methods section: a row for each passkey, for the password and authenticator app and for the recovery passphrase, with Remove or Change, and an Add unlock method button that offers two methods, each a flow of its own. Passkey asks for a name, such as "Work laptop", and the browser creates the passkey. Password and authenticator app is the way to unlock on a computer with no passkey: the flow asks a new unlock password twice (the passwordMinLength setting, default 12, is its shortest length), then shows a QR code and a setup key for an authenticator app (6-digit codes) and asks the app's code. The two are added and removed together, and the vault unlocks only with both: the password, then a code. While the vault is unlocked a code alone is a proof, and the unlock password alone shows a password; alone it cannot add an unlock method or change the recovery passphrase. Changing the recovery passphrase and adding an unlock method each need a proof in the same step: a request with a passkey added for this hostname; on a hostname with none, or when that request gets no answer (the passkey is on another device, or lost) or the passkey gives no PRF, a code of the authenticator app while the vault is unlocked, or the current recovery passphrase. The proof is asked before the browser creates anything; the first passkey, added while the vault is created, uses the passphrase just chosen. The proof asks the person at the screen at the moment of the change, so an unlocked vault left open is not enough. It is not a boundary against a process running as your user: while the vault is unlocked, such a process can read the data key from keyctl or gpg-agent, or use the Jupyter token to call vault get, and it can get a passkey's PRF through passkey:run with one click in the tab.

Command line and Python

jupyterlab-passkey vault init                      # recovery passphrase, then a passkey
jupyterlab-passkey vault add github/api -u me      # password prompted, piped, or --generate
jupyterlab-passkey vault get github/api            # the password on stdout; unlocks if needed
jupyterlab-passkey vault copy github/api           # to the browser clipboard
jupyterlab-passkey vault exec --env GITHUB_TOKEN=github/api -- gh repo list
jupyterlab-passkey vault status                    # state, time left, key holder, each protection explained
from jupyterlab_passkey_extension.vault import Vault

token = Vault().get("github/api")  # unlocks with the passkey (a notification) if locked

No command takes a secret as an option value. copy and show have the server stage the value for the existing clipboard and image flows, so it never enters the CLI process. import FILE adds entries from a JSON list, and import --pass-cli adds every entry of a pass-cli vault. Every subcommand: docs/cli-reference.md.

Where the unlocked key is kept

While the vault is unlocked its data key sits in a key holder - the first of these that works on the host:

Holder Where Needs
keyctl the kernel keyring, with a kernel-enforced expiry the keyctl command (keyutils), and a seccomp profile that allows add_key, keyctl and request_key - Docker's default profile blocks them; no root
gpg-agent a gpg-agent of the vault's own, with its own GnuPG home; expiry from its max-cache-ttl GnuPG (apt install gnupg)
memory the Jupyter server process: memfd_secret, else mlock with MADV_DONTDUMP, else plain memory nothing - this is the extension's own code

Each holder reports what it protects on the host - never in swap, never in crash dumps, expires on its own, locks on server restart, isolated from containers - measured where it can be rather than assumed. The first two describe the holder's own copy of the key: the Jupyter server also uses the key in ordinary memory for each request, which Python cannot wipe, so a copy can stay in the server process, and reach swap, until that memory is reused - even after a lock. vault status and --debug show all five; the panel's security view shows the first two and locks on server restart. When auto falls back to the extension's own code, it logs one line saying why gpg-agent was not used, and the panel shows the same line. JLAB_PASSKEY_VAULT_HOLDER=keyctl|gpg-agent|memory in the Jupyter server's environment pins the choice. Run vault lock before changing the holder or JLAB_PASSKEY_VAULT, or before removing the vault file: lock clears the key of the vault this server last unlocked and of the vault it points at now; a key held in keyctl or gpg-agent before a server restart that changed the holder or the vault file can stay until its unlock duration ends.

Server API

All endpoints live under the server base URL and require Jupyter authentication (@tornado.web.authenticated - a caller needs the Jupyter token or session).

Method Path Purpose
POST <base_url>/jupyterlab-passkey-extension/result ceremony result → a relay, <nonce>.json (a kernel key or an atomic 0600 file); 204
POST <base_url>/jupyterlab-passkey-extension/passphrase {nonce, passphrase} → raw <nonce>.pass, or {nonce, cancelled: true} → a <nonce>.cancel marker; 204
POST <base_url>/jupyterlab-passkey-extension/secret {nonce} ← raw <nonce>.secret; {"value": "..."}, or 404 once collected
POST <base_url>/jupyterlab-passkey-extension/render {nonce} ← raw <nonce>.code; {"png": "<base64>"}, or 404 once rendered
GET <base_url>/jupyterlab-passkey-extension/health { "ok": true }
GET, POST, PATCH <base_url>/jupyterlab-passkey-extension/vault/<action> the vault: 423 while locked; bodies, values and PRFs never logged

Every relay POST answers 400 on a bad nonce and touches no file when it does. The nonce is the relay filename, so it must match [A-Za-z0-9_-]{16,128}. Bodies and values are never logged.

secret and render are the endpoints that read rather than write: a local client stages the value, or the server does for vault copy and vault show. They are POSTs despite only reading: the read is destructive, and a GET would carry the nonce in the query string straight into the server's access log.

The relay contract

  • Directory - /dev/shm/jlab-passkey-$(id -u), mode 0700, ownership verified before every read and write; override with JLAB_PASSKEY_RELAY_DIR
  • Files - <nonce>.json (ceremony result), raw <nonce>.pass (captured secret), raw <nonce>.secret (secret going out to the clipboard), raw <nonce>.code (code going out to be shown as an image), <nonce>.cancel (no secret: a dismissed passphrase dialog, for the consumer to stop waiting)
  • Mode - 0600, written mkstemp-then-os.replace, so a reader never sees a partial write
  • Lifecycle - the consumer shreds .json and .pass, and removes a .cancel it saw; .secret and .code are server-enforced one-shot, unlinked as they are read
// passkey:run - create success
{ "nonce": "...", "ok": true, "cred_id": "<b64url>", "prf_enabled": false }

// get success (prf present only when prf_salt was supplied and evaluated)
{ "nonce": "...", "ok": true, "cred_id": "<b64url>", "prf": "<b64url>" }

// failure
{ "nonce": "...", "ok": false, "error": "no-prf" | "not-allowed" | "rp-id-mismatch" | "error: <name>" | "error" }

create never rejects on the create-time PRF flag - it always returns cred_id and a plain prf_enabled. Some authenticators (Windows Hello) report prf_enabled: false at registration yet yield a real PRF at assertion, so PRF availability is confirmed by a follow-up get with a prf_salt. not-allowed is WebAuthn's deliberate conflation of user-cancel, no-matching-credential, and wrong-RP into one privacy-preserving code. rp-id-mismatch is separate and nameable: a synchronous SecurityError thrown when rp_id is not a registrable suffix of the tab's origin, or the tab is at an IP address in a secure context (a loopback address such as 127.0.0.1, or HTTPS), so the CLI reports that --rp-id does not match the tab's URL rather than a bare error. A tab on plain HTTP at any other address has no WebAuthn at all and answers error.

For extension authors: the frontend commands

Six JupyterLab commands POST to the API above. Reach for them only when writing an extension that triggers a ceremony itself; everything else is better served by the CLI.

Command Args Does
passkey:run op, nonce, rp_id, cred_id?, prf_salt?, user? runs the ceremony and POSTs the result to /result
passkey:passphrase nonce, prompt?, once? opens the dialog and POSTs the value to /passphrase
passkey:copy nonce, label? collects a staged secret from /secret and writes it to the clipboard
passkey:show nonce, label? fetches a rendered image of a staged code from /render and shows it
passkey:vault-unlock nonce? unlocks the vault with a passkey for this hostname; answers ok, or error with a sentence, on /result
passkey:vault-register nonce?, label? takes a proof (a passkey for this hostname, else a code of the authenticator app or the recovery passphrase), then adds a passkey to the vault; answers ok, or error with a sentence, on /result

passkey:run reaches navigator.credentials.* before any await, so the trigger's user gesture survives into the ceremony. The challenge is a random 32-byte value the frontend generates itself - anti-replay plumbing nothing here verifies, so callers never supply it.

WebAuthn requires a user gesture, and this extension builds no request-submission UI of its own - that is the consumer's job. The reference trigger is a jupyterlab-notify notification whose action button is bound to the command; the click supplies the gesture and reaches the command with the app already in hand. This is what the CLI does for you.

jupyterlab-notify --now --no-auto-close -t info \
  -m "Approve passkey" \
  --action "Approve" \
  --cmd "passkey:run" \
  --command-args '{"op":"get","nonce":"<16-128 url-safe chars>","rp_id":"lab.example.com","cred_id":"<b64url>","prf_salt":"<b64url>"}'

Full argument, relay, and endpoint reference in docs/commands-reference.md; a worked consumer walkthrough that seals and opens a secret with a passkey in docs/example-secret-unlock.md.

Security

  • Every endpoint, the vault's included, is gated by @tornado.web.authenticated - a caller needs the Jupyter token or session
  • A secret is staged in one of two relay backends, chosen per process. The preferred is a uid-scoped kernel keyctl key: it never swaps to disk and the kernel destroys it at a TTL, so nothing survives a crash. The fallback is a /dev/shm 0600 file, created mkstemp + os.replace (a fresh file with no world-readable window, renamed onto its <nonce> name atomically, never appended to). Force the choice with JLAB_PASSKEY_RELAY_BACKEND=keyctl|shm|auto
  • Neither backend isolates a secret from your own processes. --alswrv grants your uid on the key, and the file is 0600 under your uid, so any process you run can read either. keyctl's win is no swap, self-destruct and no disk artifact, not access control; the same-uid exposure is unchanged from the file
  • Single-read is the consumer's responsibility for the ceremony and passphrase relays - the server does not destroy those, so a consumer reads once and (shm) shreds. The copy and show relays are the exceptions: the server reads and destroys each together, so collection is a server-enforced one shot
  • A secret never enters a notification. The notifications extension pushes every payload to each connected socket and holds it in an in-memory queue until a client drains it, so what travels there is a nonce - useless without the Jupyter token that collects it
  • On the shm fallback the relay directory's path is uid-scoped but predictable, and /dev/shm is world-writable (1777), so squatting it is checked rather than assumed away: before any read or write, the directory must be a real directory (not a symlink) owned by the current uid - anything foreign raises instead of falling back, so a co-tenant who gets there first is refused, not followed. A loose mode on a directory that is ours is tightened to 0700, not refused. The keyctl path has no filesystem to squat
  • The result body, the passphrase, and any PRF value are never written to logs, and a keyctl payload rides stdin, never a process argument
  • The bridge performs no cryptography and stores no secret; every parameter and all key handling belong to the caller. The vault is the one part that encrypts and stores secrets, and its unlock sends the PRF from the page straight to the server - never through the CLI or a relay
  • Once a secret reaches the clipboard it is an OS-wide value, readable by any application until overwritten - inherent to copy's purpose, and the reason nothing else here touches the clipboard
  • show renders a staged code to a distorted PNG server-side and returns only image bytes, so the code is absent from the notification, the page DOM as text, and the accessibility tree (the dialog image carries empty alt text by design). This raises the bar against a screen scraper and an OCR pass, but a value shown on screen is inherently visible to whoever can see or screenshot the screen - it is defence in depth, not a secrecy guarantee
  • Every command ignores SIGHUP, so a backgrounded process keeps waiting for its click after its terminal closes; a detached run works the same as an attached one, and nothing about detaching weakens the token gate or the relay. The command that vault exec runs is the exception: it gets the SIGHUP handling the CLI was started with, so under nohup or setsid it outlives the terminal; a hangup that arrived while vault exec still waited for the unlock click does not stop it, and the command runs after the click with no terminal for its output

Requirements

  • JupyterLab >= 4.0.0, opened by name - over HTTPS, or at localhost - WebAuthn needs a secure context, and a passkey belongs to a hostname, never an IP address such as 127.0.0.1
  • An open JupyterLab tab on the same server. The click in it is the user gesture WebAuthn requires, and a terminal has none
  • jupyterlab_notifications_extension - a hard dependency, installed for you. The CLI posts to its ingest endpoint to raise the button
  • A passkey authenticator for create and get: Windows Hello, Touch ID, a security key, or a browser password manager. passphrase, copy and show need none
  • GnuPG for the vault (apt install gnupg), so gpg-agent can hold the unlocked key where the kernel keyring is not allowed. Without it the vault still works: the extension's own code holds the key in server memory, and it says so

Development install

# from a clone of this repository
pip install -e "."
jupyter labextension develop . --overwrite
jlpm build

Rebuild after changes with jlpm build, or run jlpm watch in one terminal alongside JupyterLab. See CONTRIBUTING.md for the full development, testing, and release workflow.

Uninstall

pip uninstall jupyterlab_passkey_extension

License

BSD-3-Clause. See LICENSE.

Metadata

Release files for jupyterlab-passkey-extension 1.1.42

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jupyterlab-passkey-extension 1.1.42
File Size Uploaded
jupyterlab_passkey_extension-1.1.42.tar.gz 813.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jupyterlab-passkey-extension 1.1.42
File Interpreter ABI Platform
jupyterlab_passkey_extension-1.1.42-py3-none-any.whl Python 3 none any Details

Total release size: 1.1 MB

Release files / jupyterlab_passkey_extension-1.1.42.tar.gz

Download URL jupyterlab_passkey_extension-1.1.42.tar.gz
Size 813.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e42cf4b8380ebf9906f9881089325a2235bed346ba78f1ef6b1b8bea66b47704
BLAKE2b-256 checksum
How to use checksums
d4b9e9b6decd55cb5fc018a728098faebad1eb2f74eb22a86543f13cacdc9cab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / jupyterlab_passkey_extension-1.1.42-py3-none-any.whl

Download URL jupyterlab_passkey_extension-1.1.42-py3-none-any.whl
Size 290.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c4e2c5f427566b75056bbf399e98bfc19c69d894b74151e8fc1a8945945915d0
BLAKE2b-256 checksum
How to use checksums
49dfc5747189613c08eebc08b996ab6224e110c6f6dc98fcf9421501e3fc4c64
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

1.1.42 This release

2 release files

1.1.6

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.0

2 release files

1.0.43

2 release files

1.0.41

2 release files

1.0.38

2 release files

1.0.37

2 release files

1.0.36

2 release files

1.0.35

2 release files

1.0.32

2 release files

1.0.15

2 release files

1.0.12

2 release files

1.0.10

2 release files

1.0.4

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page