@umbra-privacy/ceremony 0.1.1 → 0.1.4

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/index.js +104 -10
  2. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import { createWriteStream } from "fs";
33
33
  import { readFile, unlink } from "fs/promises";
34
34
  import { pipeline } from "stream/promises";
35
35
  import { Readable } from "stream";
36
- var DEFAULT_API_URL = "http://ceremony.api.umbraprivacy.com";
36
+ var DEFAULT_API_URL = "https://ceremony.api.umbraprivacy.com";
37
37
  var BASE = (process.env["CEREMONY_API_URL"] ?? DEFAULT_API_URL).replace(/\/$/, "");
38
38
  async function request(path, options = {}) {
39
39
  const res = await fetch(`${BASE}${path}`, {
@@ -111,13 +111,44 @@ var api = {
111
111
  `/api/ceremonies/${ceremonyId2}/contributions/${contributionId}/receipt`
112
112
  );
113
113
  },
114
- // Admin
115
- adminDashboard(ceremonyId2, adminKey) {
116
- return request(`/api/admin/ceremonies/${ceremonyId2}/dashboard`, {
117
- headers: { "X-Admin-Key": adminKey }
118
- });
114
+ // Admin — signed-request flow. Caller supplies the 64-byte Solana keypair
115
+ // bytes (32 secret + 32 public). The helper fetches a one-time challenge,
116
+ // builds the canonical message, hashes it with Keccak256, signs with
117
+ // Ed25519, and posts the actual GET with the three X-Admin-* headers.
118
+ async adminDashboard(ceremonyId2, keypair) {
119
+ const path = `/api/admin/ceremonies/${ceremonyId2}/dashboard`;
120
+ const headers = await signAdminRequest(keypair, "GET", path, new Uint8Array());
121
+ return request(path, { headers });
119
122
  }
120
123
  };
124
+ async function signAdminRequest(keypair, method, path, body) {
125
+ const { default: bs58 } = await import("bs58");
126
+ const ed = await import("@noble/ed25519");
127
+ const { keccak_256 } = await import("@noble/hashes/sha3");
128
+ const pubkeyB58 = bs58.encode(keypair.publicKey);
129
+ const chRes = await fetch(`${BASE}/api/admin/challenge`, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({ pubkey: pubkeyB58 })
133
+ });
134
+ if (!chRes.ok) {
135
+ const text = await chRes.text();
136
+ throw new Error(`admin challenge failed: HTTP ${chRes.status}: ${text}`);
137
+ }
138
+ const { challenge } = await chRes.json();
139
+ const bodyHashHex = createHash("sha256").update(Buffer.from(body)).digest("hex");
140
+ const canonical = `${challenge}
141
+ ${method}
142
+ ${path}
143
+ ${bodyHashHex}`;
144
+ const digest = keccak_256(new TextEncoder().encode(canonical));
145
+ const signature = await ed.signAsync(digest, keypair.secretKey);
146
+ return {
147
+ "X-Admin-Pubkey": pubkeyB58,
148
+ "X-Admin-Challenge": challenge,
149
+ "X-Admin-Signature": Buffer.from(signature).toString("base64")
150
+ };
151
+ }
121
152
  var ChallengeIntegrityError = class extends Error {
122
153
  name = "ChallengeIntegrityError";
123
154
  };
@@ -659,11 +690,15 @@ function Attestation({ contribution }) {
659
690
 
660
691
  // src/components/App.tsx
661
692
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
662
- function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anonymous" }) {
693
+ var NAME_MAX_LEN = 100;
694
+ var NAME_VALID_RE = /^[\p{L}\p{N} _.\-]*$/u;
695
+ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName }) {
663
696
  const { exit } = useApp();
664
697
  const [activeCeremonyId, setActiveCeremonyId] = useState4(initialCeremonyId);
698
+ const [displayName2, setDisplayName] = useState4(initialDisplayName ?? "anonymous");
699
+ const [nameSet, setNameSet] = useState4(initialDisplayName !== void 0);
665
700
  const [screen, setScreen] = useState4(
666
- initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
701
+ initialDisplayName === void 0 ? { name: "name-input", value: "" } : initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
667
702
  );
668
703
  const [ceremony, setCeremony] = useState4(null);
669
704
  const [session, setSession] = useState4(null);
@@ -671,12 +706,13 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
671
706
  const [selectedIdx, setSelectedIdx] = useState4(0);
672
707
  const [tab, setTab] = useState4(0);
673
708
  useEffect4(() => {
709
+ if (!nameSet) return;
674
710
  if (!initialCeremonyId) {
675
711
  loadCeremonies();
676
712
  } else {
677
713
  boot(initialCeremonyId);
678
714
  }
679
- }, []);
715
+ }, [nameSet]);
680
716
  async function loadCeremonies() {
681
717
  try {
682
718
  const { ceremonies } = await api.listCeremonies();
@@ -708,6 +744,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
708
744
  setScreen({ name: "error", message: "No tracks found in this ceremony.", recoverable: false });
709
745
  return;
710
746
  }
747
+ setSelectedIdx(0);
711
748
  setScreen({ name: "tracks", tracks });
712
749
  } catch (e) {
713
750
  if (e.code === "INVALID_SESSION" || e.status === 401) {
@@ -720,6 +757,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
720
757
  setScreen({ name: "error", message: "No tracks found in this ceremony.", recoverable: false });
721
758
  return;
722
759
  }
760
+ setSelectedIdx(0);
723
761
  setScreen({ name: "tracks", tracks });
724
762
  } catch (e2) {
725
763
  setScreen({ name: "error", message: e2.message ?? "Failed to load tracks.", recoverable: false });
@@ -729,6 +767,22 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
729
767
  setScreen({ name: "error", message: e.message ?? "Failed to load tracks.", recoverable: false });
730
768
  }
731
769
  }
770
+ function commitName(raw) {
771
+ const trimmed = raw.trim();
772
+ if (trimmed.length === 0) return;
773
+ setDisplayName(trimmed);
774
+ setNameSet(true);
775
+ setScreen(
776
+ initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
777
+ );
778
+ }
779
+ function randomAnonName() {
780
+ const hex = Array.from(
781
+ { length: 6 },
782
+ () => Math.floor(Math.random() * 16).toString(16)
783
+ ).join("");
784
+ return `anon-${hex}`;
785
+ }
732
786
  async function joinTrack(track) {
733
787
  if (!session) return;
734
788
  setScreen({ name: "joining" });
@@ -764,6 +818,29 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
764
818
  }, [screen.name]);
765
819
  useInput2((input, key) => {
766
820
  const q = input.toLowerCase();
821
+ if (screen.name === "name-input") {
822
+ if (key.escape) {
823
+ exit();
824
+ return;
825
+ }
826
+ if (key.tab) {
827
+ commitName(randomAnonName());
828
+ return;
829
+ }
830
+ if (key.return) {
831
+ commitName(screen.value);
832
+ return;
833
+ }
834
+ if (key.backspace || key.delete) {
835
+ setScreen({ name: "name-input", value: screen.value.slice(0, -1) });
836
+ return;
837
+ }
838
+ if (input && input.length === 1 && NAME_VALID_RE.test(input)) {
839
+ if (screen.value.length >= NAME_MAX_LEN) return;
840
+ setScreen({ name: "name-input", value: screen.value + input });
841
+ }
842
+ return;
843
+ }
767
844
  if (q === "q" && screen.name !== "entropy") {
768
845
  if (screen.name === "queue" && session) {
769
846
  clearQueueCleanup();
@@ -835,6 +912,23 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
835
912
  goHome();
836
913
  }
837
914
  });
915
+ if (screen.name === "name-input") {
916
+ const { value } = screen;
917
+ const canSubmit = value.trim().length > 0;
918
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
919
+ /* @__PURE__ */ jsx6(Header, { ceremony: null }),
920
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, gap: 1, children: [
921
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Who are you contributing as?" }),
922
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Your display name appears next to your contributions in the public transcript. Pick anything \u2014 your real name, a handle, or hit Tab for a random anonymous name." }),
923
+ /* @__PURE__ */ jsxs6(Box6, { children: [
924
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: " > " }),
925
+ /* @__PURE__ */ jsx6(Text6, { children: value }),
926
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", inverse: true, children: " " })
927
+ ] }),
928
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: canSubmit ? "Enter to continue \xB7 Tab for random \xB7 \u232B backspace \xB7 Esc to quit" : "Type a name, or Tab for a random anonymous name \xB7 Esc to quit" })
929
+ ] })
930
+ ] });
931
+ }
838
932
  if (screen.name === "ceremony-picker") {
839
933
  const { ceremonies, loading } = screen;
840
934
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
@@ -1094,7 +1188,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: displayName2 = "anony
1094
1188
  // src/index.tsx
1095
1189
  import { jsx as jsx7 } from "react/jsx-runtime";
1096
1190
  var ceremonyId = process.env["CEREMONY_ID"] ?? "";
1097
- var displayName = process.env["CONTRIBUTOR_NAME"] ?? "anonymous";
1191
+ var displayName = process.env["CONTRIBUTOR_NAME"];
1098
1192
  process.stdout.write("\x1B[?1049h\x1B[H");
1099
1193
  function restoreScreen() {
1100
1194
  process.stdout.write("\x1B[?1049l");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umbra-privacy/ceremony",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Terminal UI for the Umbra Phase 2 trusted setup ceremony",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,8 +26,12 @@
26
26
  "node": ">=18.0.0"
27
27
  },
28
28
  "dependencies": {
29
+ "@noble/ed25519": "^2.1.0",
30
+ "@noble/hashes": "^1.5.0",
31
+ "bs58": "^6.0.0",
29
32
  "ink": "^5.1.0",
30
33
  "ink-spinner": "^5.0.0",
34
+ "js-sha3": "^0.9.3",
31
35
  "react": "^18.3.1",
32
36
  "snarkjs": "^0.7.6"
33
37
  },