@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.
- package/dist/index.js +104 -10
- 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 = "
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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"]
|
|
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.
|
|
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
|
},
|