mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-05 06:05:28 +00:00
pwa: read vault values in the browser with a pasted identity key (#225)
* pwa: read vault values in the browser with a pasted identity key The web app listed vaults but could never show a value: decryption needs the member's X25519 secret key, which only lives in identity.json on the machine that ran `logicsrc login`. Each vault now has a page (/teams/:slug/vaults/:id, linked from the dashboard) listing its secret names. Paste the identity key (or the whole identity.json) and public/vault.js decrypts in the browser with the same libsodium calls as the CLI: crypto_box_seal_open for the grant, then crypto_secretbox_open_easy per value. Nothing is sent to the server. - The pasted key is checked against the public key the server has on file before use, so a key from another machine is named, not a generic failure. - Show / Copy per value, Download .env, Forget key. The key is kept in sessionStorage unless "remember on this device" is ticked. - A member with no identity yet can create one in the browser. That registers only the public half, and is offered only when no key is registered, since replacing one would orphan every grant sealed to it. - libsodium is served from node_modules at /vendor (no CDN), like simplewebauthn. - Vault pages are no-store and the service worker no longer caches no-store pages. - CLI: `logicsrc teams key` prints the secret key to stdout alone (pipe to pbcopy) and warns when the server holds a different public key. The page also gives a jq one-liner for CLIs released before this command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pwa: rename data-secret to data-key-name ThreatCrush read data-secret="…" as a hardcoded credential (2 high, both the attribute name, never a value). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CLI 0.5.0 for teams key; install via install.sh, not npm @logicsrc/cli is not on npm; the CLI ships through curl -fsSL https://logicsrc.com/install.sh | sh, which builds master and reports the version from packages/cli/package.json. 0.5.0 marks the first build with teams key, and the vault page now says so and points older installs at the jq fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b75af67a43
commit
5bebc50beb
11 changed files with 586 additions and 9 deletions
|
|
@ -23,6 +23,7 @@ import {
|
|||
teamsCategoriesAction,
|
||||
teamsSecretsAction,
|
||||
teamsExportAction,
|
||||
teamsKeyAction,
|
||||
teamsGrantAction,
|
||||
teamsTuiAction,
|
||||
teamsPushAction,
|
||||
|
|
@ -734,6 +735,7 @@ Quick start (<team> is e.g. profullstack; see yours with "logicsrc teams list"):
|
|||
logicsrc teams pull <team> <project> <env> vault -> ./.env
|
||||
logicsrc teams push <team> <project> <env> ./.env -> vault
|
||||
logicsrc teams grant <team> <project> <env> dev@example.com
|
||||
logicsrc teams key your key, to read values in the web app
|
||||
|
||||
Categories: logicsrc teams categories. Help for one command: logicsrc teams <command> --help
|
||||
`
|
||||
|
|
@ -860,6 +862,23 @@ Vaults you have no access to are skipped and listed at the end.
|
|||
teamsExportAction(slug, { project, env, category: options.category, search: options.search }, { out: options.out, yes: options.yes })
|
||||
);
|
||||
|
||||
teams
|
||||
.command("key")
|
||||
.description("Print this machine's identity secret key, to read vault values in the web app.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
`
|
||||
The web app lists secret names but decrypts values only in your browser, with
|
||||
this key. Open a vault from the dashboard, paste the key, and the values open
|
||||
there; the key is checked against your registered public key and never sent.
|
||||
|
||||
Examples:
|
||||
logicsrc teams key print it
|
||||
logicsrc teams key | pbcopy straight to the clipboard (macOS; wl-copy / xclip on Linux)
|
||||
`
|
||||
)
|
||||
.action(() => teamsKeyAction());
|
||||
|
||||
teams
|
||||
.command("tui")
|
||||
.alias("ui")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
resolveApiUrl,
|
||||
createCredentialEngine,
|
||||
identityPath,
|
||||
verifyIdentityIntegrity,
|
||||
unwrapVaultKey,
|
||||
wrapVaultKey,
|
||||
decryptValue,
|
||||
|
|
@ -301,6 +302,43 @@ export async function whoamiAction(format: OutputFormat): Promise<void> {
|
|||
print({ loggedIn: true, email: me.user.email, apiUrl: resolveApiUrl(identity), publicKey: me.user.publicKey, teams: me.teams.map((t) => t.slug) }, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* `logicsrc teams key` — print this machine's identity secret key, for pasting
|
||||
* into the web app's vault page, which decrypts in the browser with it.
|
||||
*
|
||||
* The key goes to stdout alone (so `| pbcopy` works); everything else goes to
|
||||
* stderr. When logged in it also checks the server holds the matching public
|
||||
* key, because the web app refuses any other key and the reason is otherwise
|
||||
* invisible from the browser.
|
||||
*/
|
||||
export async function teamsKeyAction(): Promise<void> {
|
||||
const identity = readIdentity();
|
||||
if (!identity?.keys?.secretKey) {
|
||||
throw new Error(`No identity key on this machine (${identityPath()}). Run "logicsrc login" first.`);
|
||||
}
|
||||
if (!(await verifyIdentityIntegrity(identity))) {
|
||||
throw new Error(`${identityPath()} is damaged: its public key does not match its secret key.`);
|
||||
}
|
||||
let origin = resolveApiUrl(identity);
|
||||
if (identity.apiToken) {
|
||||
const { client } = authedClient();
|
||||
const me = await client.me();
|
||||
if (me.user.publicKey && me.user.publicKey !== identity.keys.publicKey) {
|
||||
console.error(
|
||||
`Warning: ${me.user.email} has a different key registered (from another machine's login). ` +
|
||||
"The web app will reject this one; use the key from that machine."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
origin = defaultApiUrl();
|
||||
}
|
||||
process.stdout.write(`${identity.keys.secretKey}\n`);
|
||||
console.error(
|
||||
`Your identity secret key. Paste it on a vault page at ${origin}/dashboard to read values in the browser. ` +
|
||||
"Anyone holding it can read every vault you can: keep it in a password manager, never in chat or a ticket."
|
||||
);
|
||||
}
|
||||
|
||||
export async function teamsCreateAction(slug: string, options: { name?: string; format: OutputFormat }): Promise<void> {
|
||||
const { client } = authedClient();
|
||||
const { team } = await client.createTeam(slug, options.name);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue