autho 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/autho.js +327 -37
  2. package/package.json +1 -1
package/dist/autho.js CHANGED
@@ -590,7 +590,7 @@ var package_default;
590
590
  var init_package = __esm(() => {
591
591
  package_default = {
592
592
  name: "autho",
593
- version: "3.0.4",
593
+ version: "3.0.5",
594
594
  description: "Local-first secret manager for humans and coding agents, rebuilt on Bun",
595
595
  type: "module",
596
596
  bin: {
@@ -4105,7 +4105,7 @@ var init_palette = __esm(() => {
4105
4105
  // apps/cli/src/tui/settings.tsx
4106
4106
  import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
4107
4107
  import { homedir as homedir2 } from "os";
4108
- import { dirname as dirname3, join as join3, resolve as resolve4 } from "path";
4108
+ import { dirname as dirname3, join as join3, resolve as resolve5 } from "path";
4109
4109
  import { useKeyboard as useKeyboard4, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
4110
4110
  import { useCallback, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef5, useState as useState4 } from "react";
4111
4111
  import { jsx as jsx4, jsxs as jsxs4, Fragment } from "@opentui/react/jsx-runtime";
@@ -4372,7 +4372,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
4372
4372
  return;
4373
4373
  case "vaultDir": {
4374
4374
  const config = { ...loadConfig() };
4375
- const target = first.trim() ? resolve4(first.trim()).replace(/\\/g, "/") : null;
4375
+ const target = first.trim() ? resolve5(first.trim()).replace(/\\/g, "/") : null;
4376
4376
  if (target)
4377
4377
  config.vaultDir = target;
4378
4378
  else
@@ -4440,7 +4440,7 @@ function SettingsScreen({ vaultPath, setHints, onBack, passkeyRunner = runPasske
4440
4440
  registerPasskey(first.trim());
4441
4441
  return;
4442
4442
  case "recoveryPath": {
4443
- const target = resolve4(first.trim() || join3(homedir2(), "autho-recovery.txt"));
4443
+ const target = resolve5(first.trim() || join3(homedir2(), "autho-recovery.txt"));
4444
4444
  if (existsSync5(target)) {
4445
4445
  setError("That file already exists \u2014 pick another name");
4446
4446
  return;
@@ -6238,10 +6238,9 @@ var init_tui = __esm(() => {
6238
6238
  init_clipboard();
6239
6239
  init_passkey();
6240
6240
  init_package();
6241
- import { spawn as spawn3 } from "child_process";
6242
6241
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
6243
6242
  import { createInterface as createInterface2 } from "readline/promises";
6244
- import { basename as basename4, isAbsolute, resolve as resolve5 } from "path";
6243
+ import { basename as basename4, isAbsolute, resolve as resolve6 } from "path";
6245
6244
 
6246
6245
  // apps/cli/src/password.ts
6247
6246
  import { createInterface } from "readline/promises";
@@ -6683,6 +6682,321 @@ async function daemonStop(options) {
6683
6682
  // apps/cli/src/index.ts
6684
6683
  init_src3();
6685
6684
  init_os_secrets();
6685
+
6686
+ // apps/web/src/server.ts
6687
+ init_src();
6688
+ init_src3();
6689
+ init_os_secrets();
6690
+ import { randomBytes as randomBytes6 } from "crypto";
6691
+ import { resolve as resolve4 } from "path";
6692
+ function json2(data, status = 200, headers) {
6693
+ return new Response(JSON.stringify(data, null, 2), {
6694
+ headers: {
6695
+ "content-type": "application/json; charset=utf-8",
6696
+ ...headers ?? {}
6697
+ },
6698
+ status
6699
+ });
6700
+ }
6701
+ function html(body) {
6702
+ return new Response(body, {
6703
+ headers: {
6704
+ "content-type": "text/html; charset=utf-8"
6705
+ }
6706
+ });
6707
+ }
6708
+ function parseCookies(request) {
6709
+ const cookieHeader = request.headers.get("cookie") ?? "";
6710
+ return Object.fromEntries(cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
6711
+ const index = part.indexOf("=");
6712
+ return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))];
6713
+ }));
6714
+ }
6715
+ async function openSession(vaultPath, state) {
6716
+ return new VaultSession(new AuthoDatabase(vaultPath), state.rootKey, {
6717
+ sealBound: state.sealBound,
6718
+ sealRecord: await loadSealRecord(vaultPath)
6719
+ });
6720
+ }
6721
+ function requireSession(request, sessions) {
6722
+ const cookies = parseCookies(request);
6723
+ const sessionId = cookies.autho_session;
6724
+ if (!sessionId) {
6725
+ throw new Error("Missing web session");
6726
+ }
6727
+ const session = sessions.get(sessionId);
6728
+ if (!session) {
6729
+ throw new Error("Unknown web session");
6730
+ }
6731
+ if (Date.parse(session.expiresAt) <= Date.now()) {
6732
+ sessions.delete(sessionId);
6733
+ throw new Error("Expired web session");
6734
+ }
6735
+ return { ...session, id: sessionId };
6736
+ }
6737
+ async function readJson2(request) {
6738
+ return await request.json();
6739
+ }
6740
+ function page2() {
6741
+ return `<!doctype html>
6742
+ <html lang="en">
6743
+ <head>
6744
+ <meta charset="utf-8" />
6745
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6746
+ <title>Autho</title>
6747
+ <style>
6748
+ :root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
6749
+ body { margin: 0; background: #f5f1e8; color: #1f2937; }
6750
+ main { max-width: 760px; margin: 0 auto; padding: 32px 20px 64px; }
6751
+ section { background: #fffdf8; border: 1px solid #d6cbb8; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 8px 30px rgba(83,58,18,0.08); }
6752
+ h1 { margin-top: 0; font-size: 2rem; }
6753
+ input, select, button { font: inherit; padding: 10px 12px; border-radius: 10px; border: 1px solid #bca889; }
6754
+ button { background: #1f4d3a; color: white; border: none; cursor: pointer; }
6755
+ form { display: flex; gap: 8px; flex-wrap: wrap; }
6756
+ pre { background: #1f2937; color: #f9fafb; padding: 12px; border-radius: 12px; overflow: auto; }
6757
+ ul { padding-left: 18px; }
6758
+ </style>
6759
+ </head>
6760
+ <body>
6761
+ <main>
6762
+ <section>
6763
+ <h1>Autho Local Web</h1>
6764
+ <p>Local-only Bun web UI for vault unlock and secret browsing.</p>
6765
+ <form id="unlock-form">
6766
+ <input id="password" type="password" placeholder="Master password" required />
6767
+ <input id="totp" inputmode="numeric" autocomplete="one-time-code" placeholder="Authenticator code (if enabled)" />
6768
+ <button type="submit">Unlock</button>
6769
+ </form>
6770
+ </section>
6771
+ <section>
6772
+ <h2>Secrets</h2>
6773
+ <button id="refresh">Refresh</button>
6774
+ <ul id="secret-list"></ul>
6775
+ <pre id="output"></pre>
6776
+ </section>
6777
+ <section id="linked" hidden>
6778
+ <h2>Linked to <span id="linked-name"></span></h2>
6779
+ <ul id="linked-list"></ul>
6780
+ <p id="linked-from"></p>
6781
+ <form id="link-form">
6782
+ <input id="link-ref" placeholder="Secret name or id to link" required />
6783
+ <button type="submit">Link</button>
6784
+ </form>
6785
+ </section>
6786
+ </main>
6787
+ <script>
6788
+ const output = document.getElementById('output');
6789
+ const secretList = document.getElementById('secret-list');
6790
+ async function refresh() {
6791
+ const response = await fetch('/api/secrets');
6792
+ if (!response.ok) {
6793
+ output.textContent = await response.text();
6794
+ return;
6795
+ }
6796
+ const data = await response.json();
6797
+ secretList.innerHTML = '';
6798
+ data.data.forEach((secret) => {
6799
+ const item = document.createElement('li');
6800
+ const open = document.createElement('a');
6801
+ open.href = '#';
6802
+ open.textContent = secret.name + " (" + secret.type + ")" + (secret.linkCount ? " \xB7 " + secret.linkCount + " linked" : "");
6803
+ open.addEventListener('click', (event) => { event.preventDefault(); showSecret(secret.id); });
6804
+ item.appendChild(open);
6805
+ secretList.appendChild(item);
6806
+ });
6807
+ output.textContent = JSON.stringify(data, null, 2);
6808
+ }
6809
+ let current = null;
6810
+ async function showSecret(ref) {
6811
+ const response = await fetch('/api/secrets/' + encodeURIComponent(ref));
6812
+ if (!response.ok) {
6813
+ output.textContent = await response.text();
6814
+ return;
6815
+ }
6816
+ const secret = (await response.json()).data;
6817
+ current = secret;
6818
+ const { links, linkedFrom, ...rest } = secret;
6819
+ output.textContent = JSON.stringify(rest, null, 2);
6820
+ document.getElementById('linked').hidden = false;
6821
+ document.getElementById('linked-name').textContent = secret.name;
6822
+ const list = document.getElementById('linked-list');
6823
+ list.innerHTML = '';
6824
+ if (!links.length) {
6825
+ list.innerHTML = '<li>No linked secrets</li>';
6826
+ }
6827
+ links.forEach((linked) => {
6828
+ const item = document.createElement('li');
6829
+ const open = document.createElement('a');
6830
+ open.href = '#';
6831
+ open.textContent = linked.name + " (" + linked.type + ") ";
6832
+ open.addEventListener('click', (event) => { event.preventDefault(); showSecret(linked.id); });
6833
+ const unlink = document.createElement('button');
6834
+ unlink.textContent = 'Unlink';
6835
+ unlink.addEventListener('click', () => changeLink('DELETE', linked.id));
6836
+ item.append(open, unlink);
6837
+ list.appendChild(item);
6838
+ });
6839
+ document.getElementById('linked-from').textContent = linkedFrom.length
6840
+ ? 'Linked from: ' + linkedFrom.map((primary) => primary.name).join(', ')
6841
+ : '';
6842
+ }
6843
+ async function changeLink(method, secret) {
6844
+ const response = await fetch('/api/links', {
6845
+ method,
6846
+ headers: { 'content-type': 'application/json' },
6847
+ body: JSON.stringify({ primary: current.id, secret })
6848
+ });
6849
+ if (!response.ok) {
6850
+ output.textContent = await response.text();
6851
+ return;
6852
+ }
6853
+ await showSecret(current.id);
6854
+ await refresh();
6855
+ }
6856
+ document.getElementById('link-form').addEventListener('submit', async (event) => {
6857
+ event.preventDefault();
6858
+ const input = document.getElementById('link-ref');
6859
+ await changeLink('POST', input.value);
6860
+ input.value = '';
6861
+ });
6862
+ document.getElementById('unlock-form').addEventListener('submit', async (event) => {
6863
+ event.preventDefault();
6864
+ const response = await fetch('/api/session/unlock', {
6865
+ method: 'POST',
6866
+ headers: { 'content-type': 'application/json' },
6867
+ body: JSON.stringify({
6868
+ password: document.getElementById('password').value,
6869
+ totp: document.getElementById('totp').value || undefined
6870
+ })
6871
+ });
6872
+ output.textContent = await response.text();
6873
+ if (response.ok) {
6874
+ await refresh();
6875
+ }
6876
+ });
6877
+ document.getElementById('refresh').addEventListener('click', refresh);
6878
+ </script>
6879
+ </body>
6880
+ </html>`;
6881
+ }
6882
+ async function startWebServer(options = {}) {
6883
+ const vaultPath = resolve4(options.vaultPath ?? defaultVaultPath());
6884
+ const host = options.host ?? "127.0.0.1";
6885
+ const port = options.port ?? 8061;
6886
+ const sessions = new Map;
6887
+ const cleanup = () => {
6888
+ for (const [id, session] of sessions.entries()) {
6889
+ if (Date.parse(session.expiresAt) <= Date.now()) {
6890
+ sessions.delete(id);
6891
+ }
6892
+ }
6893
+ };
6894
+ const server = Bun.serve({
6895
+ fetch: async (request) => {
6896
+ cleanup();
6897
+ const url = new URL(request.url);
6898
+ try {
6899
+ if (request.method === "GET" && url.pathname === "/health") {
6900
+ return json2({ ok: true, running: true, vaultPath });
6901
+ }
6902
+ if (request.method === "GET" && url.pathname === "/") {
6903
+ return html(page2());
6904
+ }
6905
+ if (request.method === "POST" && url.pathname === "/api/session/unlock") {
6906
+ const body = await readJson2(request);
6907
+ const ttlSeconds = body.ttlSeconds ?? 3600;
6908
+ if (ttlSeconds <= 0 || ttlSeconds > 86400) {
6909
+ return json2({ error: "Session TTL must be between 1 and 86400 seconds" }, 400);
6910
+ }
6911
+ const { rootKey, sealBound } = VaultService.unlockRootKey(vaultPath, {
6912
+ password: body.password,
6913
+ sealRecord: await loadSealRecord(vaultPath),
6914
+ totp: body.totp
6915
+ });
6916
+ const sessionId = randomBytes6(16).toString("hex");
6917
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
6918
+ sessions.set(sessionId, { expiresAt, rootKey, sealBound });
6919
+ return json2({ expiresAt, sessionId }, 200, { "set-cookie": `autho_session=${sessionId}; HttpOnly; SameSite=Strict; Path=/; Secure` });
6920
+ }
6921
+ if (request.method === "POST" && url.pathname === "/api/session/lock") {
6922
+ const cookies = parseCookies(request);
6923
+ if (cookies.autho_session) {
6924
+ sessions.delete(cookies.autho_session);
6925
+ }
6926
+ return json2({ locked: true }, 200, { "set-cookie": "autho_session=; Max-Age=0; Path=/; Secure" });
6927
+ }
6928
+ if (url.pathname.startsWith("/api/")) {
6929
+ const webSession = requireSession(request, sessions);
6930
+ const session = await openSession(vaultPath, webSession);
6931
+ try {
6932
+ if (request.method === "GET" && url.pathname === "/api/status") {
6933
+ return json2({
6934
+ initialized: true,
6935
+ secretCount: session.listSecrets().length,
6936
+ unlocked: true,
6937
+ vaultPath
6938
+ });
6939
+ }
6940
+ if (request.method === "GET" && url.pathname === "/api/secrets") {
6941
+ return json2({ data: session.listSecretIndex() });
6942
+ }
6943
+ if (request.method === "GET" && url.pathname.startsWith("/api/secrets/")) {
6944
+ const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
6945
+ const { linkedFrom, links } = session.getLinks(ref);
6946
+ return json2({ data: { ...session.getSecret(ref), linkedFrom, links } });
6947
+ }
6948
+ if ((request.method === "POST" || request.method === "DELETE") && url.pathname === "/api/links") {
6949
+ const body = await readJson2(request);
6950
+ return json2({
6951
+ data: request.method === "POST" ? session.linkSecrets(body.primary, body.secret) : session.unlinkSecrets(body.primary, body.secret)
6952
+ });
6953
+ }
6954
+ if (request.method === "POST" && url.pathname === "/api/secrets") {
6955
+ const body = await readJson2(request);
6956
+ return json2({
6957
+ data: session.addSecret({
6958
+ metadata: Object.fromEntries(Object.entries({
6959
+ algorithm: body.algorithm,
6960
+ description: body.description,
6961
+ digits: body.digits,
6962
+ url: body.url
6963
+ }).filter(([, value]) => value !== undefined)),
6964
+ name: body.name,
6965
+ type: body.type,
6966
+ username: body.username,
6967
+ value: body.value
6968
+ })
6969
+ }, 201);
6970
+ }
6971
+ if (request.method === "DELETE" && url.pathname.startsWith("/api/secrets/")) {
6972
+ const ref = decodeURIComponent(url.pathname.slice("/api/secrets/".length));
6973
+ return json2({ data: session.removeSecret(ref, { withLinked: url.searchParams.get("withLinked") === "1" }) });
6974
+ }
6975
+ if (request.method === "GET" && url.pathname.startsWith("/api/otp/")) {
6976
+ const ref = decodeURIComponent(url.pathname.slice("/api/otp/".length));
6977
+ return json2({ data: session.generateOtp(ref) });
6978
+ }
6979
+ } finally {
6980
+ await closeSessionAndRecordSeal(vaultPath, session);
6981
+ }
6982
+ }
6983
+ } catch (error) {
6984
+ const message = error instanceof Error ? error.message : String(error);
6985
+ const status = /session/i.test(message) ? 401 : 400;
6986
+ return json2({ error: message }, status);
6987
+ }
6988
+ return json2({ error: "Not found" }, 404);
6989
+ },
6990
+ hostname: host,
6991
+ port
6992
+ });
6993
+ process.on("SIGINT", () => process.exit(0));
6994
+ process.on("SIGTERM", () => process.exit(0));
6995
+ console.log(`Autho web server listening on http://${host}:${server.port}`);
6996
+ await new Promise(() => {});
6997
+ }
6998
+
6999
+ // apps/cli/src/index.ts
6686
7000
  function parseArgs(argv) {
6687
7001
  const dashDashIndex = argv.indexOf("--");
6688
7002
  const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
@@ -6776,7 +7090,7 @@ function printSealChanges(changes, diffTrusted) {
6776
7090
  }
6777
7091
  }
6778
7092
  function absolutePath(path) {
6779
- return resolve5(path);
7093
+ return resolve6(path);
6780
7094
  }
6781
7095
  function buildSecretMetadata(args) {
6782
7096
  return Object.fromEntries(Object.entries({
@@ -7031,34 +7345,10 @@ async function runInitWizard(vaultPath, password, existingCreds) {
7031
7345
  console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
7032
7346
  }
7033
7347
  async function runWebServer(vaultPath, args) {
7034
- const commandArgs = [
7035
- "run",
7036
- absolutePath("./apps/web/src/index.ts"),
7037
- "serve",
7038
- "--vault",
7039
- absolutePath(vaultPath)
7040
- ];
7041
- const host = getString(args, "host");
7042
- const port = getString(args, "port") ?? String(numberPreference("webPort"));
7043
- if (host) {
7044
- commandArgs.push("--host", host);
7045
- }
7046
- if (port) {
7047
- commandArgs.push("--port", port);
7048
- }
7049
- await new Promise((resolvePromise, rejectPromise) => {
7050
- const child = spawn3(process.execPath, commandArgs, {
7051
- cwd: process.cwd(),
7052
- stdio: "inherit"
7053
- });
7054
- child.on("exit", (code) => {
7055
- if (code === 0) {
7056
- resolvePromise();
7057
- return;
7058
- }
7059
- rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
7060
- });
7061
- child.on("error", rejectPromise);
7348
+ await startWebServer({
7349
+ host: getString(args, "host"),
7350
+ port: Number(getString(args, "port") ?? numberPreference("webPort")),
7351
+ vaultPath: absolutePath(vaultPath)
7062
7352
  });
7063
7353
  }
7064
7354
  function help() {
@@ -7235,7 +7525,7 @@ async function main() {
7235
7525
  console.log(" Tip: Use a cloud-synced folder (e.g. Google Drive) to share the vault across machines.");
7236
7526
  const customDir = (await readLine(` Path (Enter to keep default): `)).trim();
7237
7527
  if (customDir && customDir !== currentDir) {
7238
- const resolved = resolve5(customDir).replace(/\\/g, "/");
7528
+ const resolved = resolve6(customDir).replace(/\\/g, "/");
7239
7529
  saveConfig({ ...config, vaultDir: resolved });
7240
7530
  effectiveVaultPath = resolved + "/vault.db";
7241
7531
  console.log(` \x1B[32m\u2713\x1B[0m Vault directory set to ${resolved}`);
@@ -7303,7 +7593,7 @@ async function main() {
7303
7593
  if (key === "autoLock") {
7304
7594
  updated.autoLock = value === "true";
7305
7595
  } else if (key === "vaultDir") {
7306
- updated.vaultDir = isAbsolute(value) ? value : resolve5(value);
7596
+ updated.vaultDir = isAbsolute(value) ? value : resolve6(value);
7307
7597
  } else if (key === "editor") {
7308
7598
  updated.editor = value;
7309
7599
  } else if (key === "autoLockTimeout") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autho",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "Local-first secret manager for humans and coding agents, rebuilt on Bun",
5
5
  "type": "module",
6
6
  "bin": {